diff --git a/.eslintignore b/.eslintignore index dda0884b381..f186c7ecd78 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,7 +3,6 @@ **/vs/css.build.js **/vs/css.js **/vs/loader.js -**/promise-polyfill/** **/insane/** **/marked/** **/test/**/*.js diff --git a/.eslintrc.json b/.eslintrc.json index 5414b07f942..57c99ae5a66 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -35,7 +35,8 @@ "external", "status", "origin", - "orientation" + "orientation", + "context" ], // non-complete list of globals that are easy to access unintentionally "no-var": "warn", "jsdoc/no-types": "warn", diff --git a/build/builtInExtensions.json b/build/builtInExtensions.json index 84091196b00..bd9188f8ee1 100644 --- a/build/builtInExtensions.json +++ b/build/builtInExtensions.json @@ -1,7 +1,7 @@ [ { "name": "ms-vscode.node-debug", - "version": "1.43.1", + "version": "1.43.2", "repo": "https://github.com/Microsoft/vscode-node-debug", "metadata": { "id": "b6ded8fb-a0a0-4c1c-acbd-ab2a3bc995a6", @@ -46,7 +46,7 @@ }, { "name": "ms-vscode.js-debug-nightly", - "version": "2020.2.2507", + "version": "2020.2.2617", "forQualities": [ "insider" ], diff --git a/build/gulpfile.editor.js b/build/gulpfile.editor.js index 9278f689ec3..bdc34affadc 100644 --- a/build/gulpfile.editor.js +++ b/build/gulpfile.editor.js @@ -72,13 +72,8 @@ const extractEditorSrcTask = task.define('extract-editor-src', () => { apiusages, extrausages ], - libs: [ - `lib.es5.d.ts`, - `lib.dom.d.ts`, - `lib.webworker.importscripts.d.ts` - ], shakeLevel: 2, // 0-Files, 1-InnerFile, 2-ClassMembers - importIgnorePattern: /(^vs\/css!)|(promise-polyfill\/polyfill)/, + importIgnorePattern: /(^vs\/css!)/, destRoot: path.join(root, 'out-editor-src'), redirects: [] }); diff --git a/build/gulpfile.hygiene.js b/build/gulpfile.hygiene.js index ccf965a9dc4..75c8413ae5d 100644 --- a/build/gulpfile.hygiene.js +++ b/build/gulpfile.hygiene.js @@ -114,7 +114,6 @@ const copyrightFilter = [ '!**/*.disabled', '!**/*.code-workspace', '!**/*.js.map', - '!**/promise-polyfill/polyfill.js', '!build/**/*.init', '!resources/linux/snap/snapcraft.yaml', '!resources/linux/snap/electron-launch', diff --git a/build/lib/asar.js b/build/lib/asar.js index 21c5f65a45b..4a15a200be5 100644 --- a/build/lib/asar.js +++ b/build/lib/asar.js @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); +exports.createAsar = void 0; const path = require("path"); const es = require("event-stream"); const pickle = require('chromium-pickle-js'); diff --git a/build/lib/bundle.js b/build/lib/bundle.js index 881e8ff6c7f..7d0c8d9b55e 100644 --- a/build/lib/bundle.js +++ b/build/lib/bundle.js @@ -4,6 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); +exports.bundle = void 0; const fs = require("fs"); const path = require("path"); const vm = require("vm"); diff --git a/build/lib/compilation.js b/build/lib/compilation.js index 59bf1a250f6..c4a3230424b 100644 --- a/build/lib/compilation.js +++ b/build/lib/compilation.js @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); +exports.watchTask = exports.compileTask = void 0; const es = require("event-stream"); const fs = require("fs"); const gulp = require("gulp"); diff --git a/build/lib/electron.js b/build/lib/electron.js index b38a1f6edc9..abf6baab419 100644 --- a/build/lib/electron.js +++ b/build/lib/electron.js @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); +exports.config = exports.getElectronVersion = void 0; const fs = require("fs"); const path = require("path"); const vfs = require("vinyl-fs"); diff --git a/build/lib/eslint/utils.js b/build/lib/eslint/utils.js index ec59aef3b7d..c58e4e24be1 100644 --- a/build/lib/eslint/utils.js +++ b/build/lib/eslint/utils.js @@ -4,6 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); +exports.createImportRuleListener = void 0; function createImportRuleListener(validateImport) { function _checkImport(node) { if (node && node.type === 'Literal' && typeof node.value === 'string') { diff --git a/build/lib/extensions.js b/build/lib/extensions.js index e45b0d4e35a..8ee26bb40dd 100644 --- a/build/lib/extensions.js +++ b/build/lib/extensions.js @@ -4,6 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); +exports.packageMarketplaceExtensionsStream = exports.packageLocalExtensionsStream = exports.fromMarketplace = void 0; const es = require("event-stream"); const fs = require("fs"); const glob = require("glob"); diff --git a/build/lib/git.js b/build/lib/git.js index da5d66fd8d2..1726f76fcc7 100644 --- a/build/lib/git.js +++ b/build/lib/git.js @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); +exports.getVersion = void 0; const path = require("path"); const fs = require("fs"); /** diff --git a/build/lib/i18n.js b/build/lib/i18n.js index 27a4054a1e4..ea1758ee57e 100644 --- a/build/lib/i18n.js +++ b/build/lib/i18n.js @@ -4,6 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); +exports.prepareIslFiles = exports.prepareI18nPackFiles = exports.pullI18nPackFiles = exports.prepareI18nFiles = exports.pullSetupXlfFiles = exports.pullCoreAndExtensionsXlfFiles = exports.findObsoleteResources = exports.pushXlfFiles = exports.createXlfFilesForIsl = exports.createXlfFilesForExtensions = exports.createXlfFilesForCoreBundle = exports.getResource = exports.processNlsFiles = exports.Limiter = exports.XLF = exports.Line = exports.externalExtensionsWithTranslations = exports.extraLanguages = exports.defaultLanguages = void 0; const path = require("path"); const fs = require("fs"); const event_stream_1 = require("event-stream"); @@ -100,155 +101,158 @@ class TextModel { return this._lines; } } -class XLF { - constructor(project) { - this.project = project; - this.buffer = []; - this.files = Object.create(null); - this.numberOfMessages = 0; - } - toString() { - this.appendHeader(); - for (let file in this.files) { - this.appendNewLine(``, 2); - for (let item of this.files[file]) { - this.addStringItem(item); - } - this.appendNewLine('', 2); +let XLF = /** @class */ (() => { + class XLF { + constructor(project) { + this.project = project; + this.buffer = []; + this.files = Object.create(null); + this.numberOfMessages = 0; } - this.appendFooter(); - return this.buffer.join('\r\n'); - } - addFile(original, keys, messages) { - if (keys.length === 0) { - console.log('No keys in ' + original); - return; - } - if (keys.length !== messages.length) { - throw new Error(`Unmatching keys(${keys.length}) and messages(${messages.length}).`); - } - this.numberOfMessages += keys.length; - this.files[original] = []; - let existingKeys = new Set(); - for (let i = 0; i < keys.length; i++) { - let key = keys[i]; - let realKey; - let comment; - if (Is.string(key)) { - realKey = key; - comment = undefined; - } - else if (LocalizeInfo.is(key)) { - realKey = key.key; - if (key.comment && key.comment.length > 0) { - comment = key.comment.map(comment => encodeEntities(comment)).join('\r\n'); + toString() { + this.appendHeader(); + for (let file in this.files) { + this.appendNewLine(``, 2); + for (let item of this.files[file]) { + this.addStringItem(item); } + this.appendNewLine('', 2); } - if (!realKey || existingKeys.has(realKey)) { - continue; + this.appendFooter(); + return this.buffer.join('\r\n'); + } + addFile(original, keys, messages) { + if (keys.length === 0) { + console.log('No keys in ' + original); + return; } - existingKeys.add(realKey); - let message = encodeEntities(messages[i]); - this.files[original].push({ id: realKey, message: message, comment: comment }); + if (keys.length !== messages.length) { + throw new Error(`Unmatching keys(${keys.length}) and messages(${messages.length}).`); + } + this.numberOfMessages += keys.length; + this.files[original] = []; + let existingKeys = new Set(); + for (let i = 0; i < keys.length; i++) { + let key = keys[i]; + let realKey; + let comment; + if (Is.string(key)) { + realKey = key; + comment = undefined; + } + else if (LocalizeInfo.is(key)) { + realKey = key.key; + if (key.comment && key.comment.length > 0) { + comment = key.comment.map(comment => encodeEntities(comment)).join('\r\n'); + } + } + if (!realKey || existingKeys.has(realKey)) { + continue; + } + existingKeys.add(realKey); + let message = encodeEntities(messages[i]); + this.files[original].push({ id: realKey, message: message, comment: comment }); + } + } + addStringItem(item) { + if (!item.id || !item.message) { + throw new Error(`No item ID or value specified: ${JSON.stringify(item)}`); + } + this.appendNewLine(``, 4); + this.appendNewLine(`${item.message}`, 6); + if (item.comment) { + this.appendNewLine(`${item.comment}`, 6); + } + this.appendNewLine('', 4); + } + appendHeader() { + this.appendNewLine('', 0); + this.appendNewLine('', 0); + } + appendFooter() { + this.appendNewLine('', 0); + } + appendNewLine(content, indent) { + let line = new Line(indent); + line.append(content); + this.buffer.push(line.toString()); } } - addStringItem(item) { - if (!item.id || !item.message) { - throw new Error(`No item ID or value specified: ${JSON.stringify(item)}`); - } - this.appendNewLine(``, 4); - this.appendNewLine(`${item.message}`, 6); - if (item.comment) { - this.appendNewLine(`${item.comment}`, 6); - } - this.appendNewLine('', 4); - } - appendHeader() { - this.appendNewLine('', 0); - this.appendNewLine('', 0); - } - appendFooter() { - this.appendNewLine('', 0); - } - appendNewLine(content, indent) { - let line = new Line(indent); - line.append(content); - this.buffer.push(line.toString()); - } -} + XLF.parsePseudo = function (xlfString) { + return new Promise((resolve) => { + let parser = new xml2js.Parser(); + let files = []; + parser.parseString(xlfString, function (_err, result) { + const fileNodes = result['xliff']['file']; + fileNodes.forEach(file => { + const originalFilePath = file.$.original; + const messages = {}; + const transUnits = file.body[0]['trans-unit']; + if (transUnits) { + transUnits.forEach((unit) => { + const key = unit.$.id; + const val = pseudify(unit.source[0]['_'].toString()); + if (key && val) { + messages[key] = decodeEntities(val); + } + }); + files.push({ messages: messages, originalFilePath: originalFilePath, language: 'ps' }); + } + }); + resolve(files); + }); + }); + }; + XLF.parse = function (xlfString) { + return new Promise((resolve, reject) => { + let parser = new xml2js.Parser(); + let files = []; + parser.parseString(xlfString, function (err, result) { + if (err) { + reject(new Error(`XLF parsing error: Failed to parse XLIFF string. ${err}`)); + } + const fileNodes = result['xliff']['file']; + if (!fileNodes) { + reject(new Error(`XLF parsing error: XLIFF file does not contain "xliff" or "file" node(s) required for parsing.`)); + } + fileNodes.forEach((file) => { + const originalFilePath = file.$.original; + if (!originalFilePath) { + reject(new Error(`XLF parsing error: XLIFF file node does not contain original attribute to determine the original location of the resource file.`)); + } + let language = file.$['target-language']; + if (!language) { + reject(new Error(`XLF parsing error: XLIFF file node does not contain target-language attribute to determine translated language.`)); + } + const messages = {}; + const transUnits = file.body[0]['trans-unit']; + if (transUnits) { + transUnits.forEach((unit) => { + const key = unit.$.id; + if (!unit.target) { + return; // No translation available + } + let val = unit.target[0]; + if (typeof val !== 'string') { + val = val._; + } + if (key && val) { + messages[key] = decodeEntities(val); + } + else { + reject(new Error(`XLF parsing error: XLIFF file ${originalFilePath} does not contain full localization data. ID or target translation for one of the trans-unit nodes is not present.`)); + } + }); + files.push({ messages: messages, originalFilePath: originalFilePath, language: language.toLowerCase() }); + } + }); + resolve(files); + }); + }); + }; + return XLF; +})(); exports.XLF = XLF; -XLF.parsePseudo = function (xlfString) { - return new Promise((resolve) => { - let parser = new xml2js.Parser(); - let files = []; - parser.parseString(xlfString, function (_err, result) { - const fileNodes = result['xliff']['file']; - fileNodes.forEach(file => { - const originalFilePath = file.$.original; - const messages = {}; - const transUnits = file.body[0]['trans-unit']; - if (transUnits) { - transUnits.forEach((unit) => { - const key = unit.$.id; - const val = pseudify(unit.source[0]['_'].toString()); - if (key && val) { - messages[key] = decodeEntities(val); - } - }); - files.push({ messages: messages, originalFilePath: originalFilePath, language: 'ps' }); - } - }); - resolve(files); - }); - }); -}; -XLF.parse = function (xlfString) { - return new Promise((resolve, reject) => { - let parser = new xml2js.Parser(); - let files = []; - parser.parseString(xlfString, function (err, result) { - if (err) { - reject(new Error(`XLF parsing error: Failed to parse XLIFF string. ${err}`)); - } - const fileNodes = result['xliff']['file']; - if (!fileNodes) { - reject(new Error(`XLF parsing error: XLIFF file does not contain "xliff" or "file" node(s) required for parsing.`)); - } - fileNodes.forEach((file) => { - const originalFilePath = file.$.original; - if (!originalFilePath) { - reject(new Error(`XLF parsing error: XLIFF file node does not contain original attribute to determine the original location of the resource file.`)); - } - let language = file.$['target-language']; - if (!language) { - reject(new Error(`XLF parsing error: XLIFF file node does not contain target-language attribute to determine translated language.`)); - } - const messages = {}; - const transUnits = file.body[0]['trans-unit']; - if (transUnits) { - transUnits.forEach((unit) => { - const key = unit.$.id; - if (!unit.target) { - return; // No translation available - } - let val = unit.target[0]; - if (typeof val !== 'string') { - val = val._; - } - if (key && val) { - messages[key] = decodeEntities(val); - } - else { - reject(new Error(`XLF parsing error: XLIFF file ${originalFilePath} does not contain full localization data. ID or target translation for one of the trans-unit nodes is not present.`)); - } - }); - files.push({ messages: messages, originalFilePath: originalFilePath, language: language.toLowerCase() }); - } - }); - resolve(files); - }); - }); -}; class Limiter { constructor(maxDegreeOfParalellism) { this.maxDegreeOfParalellism = maxDegreeOfParalellism; diff --git a/build/lib/optimize.js b/build/lib/optimize.js index 45f11698463..f5db4afaaaa 100644 --- a/build/lib/optimize.js +++ b/build/lib/optimize.js @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); +exports.minifyTask = exports.optimizeTask = exports.loaderConfig = void 0; const es = require("event-stream"); const fs = require("fs"); const gulp = require("gulp"); diff --git a/build/lib/reporter.js b/build/lib/reporter.js index e0461dc6d9d..67615bf48dc 100644 --- a/build/lib/reporter.js +++ b/build/lib/reporter.js @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); +exports.createReporter = void 0; const es = require("event-stream"); const _ = require("underscore"); const fancyLog = require("fancy-log"); diff --git a/build/lib/standalone.js b/build/lib/standalone.js index ccfd7b45d23..531194c35fd 100644 --- a/build/lib/standalone.js +++ b/build/lib/standalone.js @@ -4,6 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); +exports.createESMSourcesAndResources2 = exports.extractEditor = void 0; const ts = require("typescript"); const fs = require("fs"); const path = require("path"); diff --git a/build/lib/stats.js b/build/lib/stats.js index 99ad665f223..2ff02e405a6 100644 --- a/build/lib/stats.js +++ b/build/lib/stats.js @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); +exports.submitAllStats = exports.createStatsStream = void 0; const es = require("event-stream"); const fancyLog = require("fancy-log"); const ansiColors = require("ansi-colors"); diff --git a/build/lib/task.js b/build/lib/task.js index f1e6e3f6245..d08ab8acde8 100644 --- a/build/lib/task.js +++ b/build/lib/task.js @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); +exports.define = exports.parallel = exports.series = void 0; const fancyLog = require("fancy-log"); const ansiColors = require("ansi-colors"); function _isPromise(p) { diff --git a/build/lib/treeshaking.js b/build/lib/treeshaking.js index 65dd88f585c..eeb954c4fcf 100644 --- a/build/lib/treeshaking.js +++ b/build/lib/treeshaking.js @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); +exports.shake = exports.toStringShakeLevel = exports.ShakeLevel = void 0; const fs = require("fs"); const path = require("path"); const ts = require("typescript"); @@ -75,11 +76,7 @@ function createTypeScriptLanguageService(options) { FILES[typing] = fs.readFileSync(filePath).toString(); }); // Resolve libs - const RESOLVED_LIBS = {}; - options.libs.forEach((filename) => { - const filepath = path.join(TYPESCRIPT_LIB_FOLDER, filename); - RESOLVED_LIBS[`defaultLib:${filename}`] = fs.readFileSync(filepath).toString(); - }); + const RESOLVED_LIBS = processLibFiles(options); const compilerOptions = ts.convertCompilerOptionsFromJson(options.compilerOptions, options.sourcesRoot).options; const host = new TypeScriptLanguageServiceHost(RESOLVED_LIBS, FILES, compilerOptions); return ts.createLanguageService(host); @@ -137,6 +134,29 @@ function discoverAndReadFiles(options) { } return FILES; } +/** + * Read lib files and follow lib references + */ +function processLibFiles(options) { + const stack = [...options.compilerOptions.lib]; + const result = {}; + while (stack.length > 0) { + const filename = `lib.${stack.shift().toLowerCase()}.d.ts`; + const key = `defaultLib:${filename}`; + if (!result[key]) { + // add this file + const filepath = path.join(TYPESCRIPT_LIB_FOLDER, filename); + const sourceText = fs.readFileSync(filepath).toString(); + result[key] = sourceText; + // precess dependencies and "recurse" + const info = ts.preProcessFile(sourceText); + for (let ref of info.libReferenceDirectives) { + stack.push(ref.fileName); + } + } + } + return result; +} /** * A TypeScript language service host */ diff --git a/build/lib/treeshaking.ts b/build/lib/treeshaking.ts index 89f562ad1b8..19e0dc8bb4d 100644 --- a/build/lib/treeshaking.ts +++ b/build/lib/treeshaking.ts @@ -18,7 +18,7 @@ export const enum ShakeLevel { } export function toStringShakeLevel(shakeLevel: ShakeLevel): string { - switch(shakeLevel) { + switch (shakeLevel) { case ShakeLevel.Files: return 'Files (0)'; case ShakeLevel.InnerFile: @@ -42,11 +42,6 @@ export interface ITreeShakingOptions { * Inline usages. */ inlineEntryPoints: string[]; - /** - * TypeScript libs. - * e.g. `lib.d.ts`, `lib.es2015.collection.d.ts` - */ - libs: string[]; /** * Other .d.ts files */ @@ -130,11 +125,7 @@ function createTypeScriptLanguageService(options: ITreeShakingOptions): ts.Langu }); // Resolve libs - const RESOLVED_LIBS: ILibMap = {}; - options.libs.forEach((filename) => { - const filepath = path.join(TYPESCRIPT_LIB_FOLDER, filename); - RESOLVED_LIBS[`defaultLib:${filename}`] = fs.readFileSync(filepath).toString(); - }); + const RESOLVED_LIBS = processLibFiles(options); const compilerOptions = ts.convertCompilerOptionsFromJson(options.compilerOptions, options.sourcesRoot).options; @@ -205,6 +196,34 @@ function discoverAndReadFiles(options: ITreeShakingOptions): IFileMap { return FILES; } +/** + * Read lib files and follow lib references + */ +function processLibFiles(options: ITreeShakingOptions): ILibMap { + + const stack: string[] = [...options.compilerOptions.lib]; + const result: ILibMap = {}; + + while (stack.length > 0) { + const filename = `lib.${stack.shift()!.toLowerCase()}.d.ts`; + const key = `defaultLib:${filename}`; + if (!result[key]) { + // add this file + const filepath = path.join(TYPESCRIPT_LIB_FOLDER, filename); + const sourceText = fs.readFileSync(filepath).toString(); + result[key] = sourceText; + + // precess dependencies and "recurse" + const info = ts.preProcessFile(sourceText); + for (let ref of info.libReferenceDirectives) { + stack.push(ref.fileName); + } + } + } + + return result; +} + interface ILibMap { [libName: string]: string; } interface IFileMap { [fileName: string]: string; } @@ -475,7 +494,7 @@ function markNodes(languageService: ts.LanguageService, options: ITreeShakingOpt } if (black_queue.length === 0) { - for (let i = 0; i< gray_queue.length; i++) { + for (let i = 0; i < gray_queue.length; i++) { const node = gray_queue[i]; const nodeParent = node.parent; if ((ts.isClassDeclaration(nodeParent) || ts.isInterfaceDeclaration(nodeParent)) && nodeOrChildIsBlack(nodeParent)) { diff --git a/build/lib/util.js b/build/lib/util.js index 752d9fb63f0..d42670e67a5 100644 --- a/build/lib/util.js +++ b/build/lib/util.js @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); +exports.streamToPromise = exports.versionStringToNumber = exports.filter = exports.rebase = exports.getVersion = exports.ensureDir = exports.rreddir = exports.rimraf = exports.stripSourceMappingURL = exports.loadSourcemaps = exports.cleanNodeModules = exports.skipDirectories = exports.toFileUri = exports.setExecutableBit = exports.fixWin32DirectoryPermissions = exports.incremental = void 0; const es = require("event-stream"); const debounce = require("debounce"); const _filter = require("gulp-filter"); diff --git a/build/monaco/ThirdPartyNotices.txt b/build/monaco/ThirdPartyNotices.txt index 1de70ddaab6..8b488daf191 100644 --- a/build/monaco/ThirdPartyNotices.txt +++ b/build/monaco/ThirdPartyNotices.txt @@ -33,32 +33,6 @@ USE OR OTHER DEALINGS IN THE SOFTWARE. END OF nodejs path library NOTICES AND INFORMATION -%% promise-polyfill version 8.1.0 (https://github.com/taylorhakes/promise-polyfill) -========================================= -Copyright (c) 2014 Taylor Hakes -Copyright (c) 2014 Forbes Lindesay - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -========================================= -END OF winjs NOTICES AND INFORMATION - - %% string_scorer version 0.1.20 (https://github.com/joshaven/string_score) diff --git a/build/monaco/api.js b/build/monaco/api.js index bd656f2bcd1..1de24d4065c 100644 --- a/build/monaco/api.js +++ b/build/monaco/api.js @@ -4,6 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); +exports.execute = exports.run3 = exports.DeclarationResolver = exports.FSProvider = exports.RECIPE_PATH = void 0; const fs = require("fs"); const ts = require("typescript"); const path = require("path"); diff --git a/build/package.json b/build/package.json index d1cd1b063b7..8196dd52002 100644 --- a/build/package.json +++ b/build/package.json @@ -43,7 +43,7 @@ "minimist": "^1.2.0", "request": "^2.85.0", "terser": "4.3.8", - "typescript": "3.8.2", + "typescript": "^3.9.0-dev.20200229", "vsce": "1.48.0", "vscode-telemetry-extractor": "^1.5.4", "xml2js": "^0.4.17" diff --git a/build/yarn.lock b/build/yarn.lock index 53bc78757a8..fd19a1b7221 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -2453,16 +2453,16 @@ typed-rest-client@^0.9.0: tunnel "0.0.4" underscore "1.8.3" -typescript@3.8.2: - version "3.8.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.2.tgz#91d6868aaead7da74f493c553aeff76c0c0b1d5a" - integrity sha512-EgOVgL/4xfVrCMbhYKUQTdF37SQn4Iw73H5BgCrF1Abdun7Kwy/QZsE/ssAy0y4LxBbvua3PIbFsbRczWWnDdQ== - typescript@^3.0.1: version "3.5.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.3.tgz#c830f657f93f1ea846819e929092f5fe5983e977" integrity sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g== +typescript@^3.9.0-dev.20200229: + version "3.9.0-dev.20200229" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.0-dev.20200229.tgz#45f0821d5c420a4c7d6d894c64531e1301dfa9bd" + integrity sha512-DtSLzxoiUir0qRc3+JJBxiAe6NvTEM3uDxnPxVWJU6sRDhUi8Ssx6DBjGWCZAQJlLk5A+jk2ptf3JvvZrQlLNQ== + typical@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4" diff --git a/extensions/git/package.json b/extensions/git/package.json index 1eaf39ad44b..5246020c6c2 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -1825,37 +1825,37 @@ }, "viewsWelcome": [ { - "view": "workbench.scm", + "view": "scm", "contents": "%view.workbench.scm.disabled%", "when": "!config.git.enabled" }, { - "view": "workbench.scm", + "view": "scm", "contents": "%view.workbench.scm.missing%", "when": "config.git.enabled && git.missing" }, { - "view": "workbench.scm", + "view": "scm", "contents": "%view.workbench.scm.empty%", "when": "config.git.enabled && !git.missing && workbenchState == empty" }, { - "view": "workbench.scm", + "view": "scm", "contents": "%view.workbench.scm.folder%", "when": "config.git.enabled && !git.missing && workbenchState == folder" }, { - "view": "workbench.scm", + "view": "scm", "contents": "%view.workbench.scm.workspace%", "when": "config.git.enabled && !git.missing && workbenchState == workspace && workspaceFolderCount != 0" }, { - "view": "workbench.scm", + "view": "scm", "contents": "%view.workbench.scm.emptyWorkspace%", "when": "config.git.enabled && !git.missing && workbenchState == workspace && workspaceFolderCount == 0" }, { - "view": "workbench.explorer.emptyView", + "view": "explorer", "contents": "%view.workbench.cloneRepository%" } ] diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 6328edd99bf..e163ffad48d 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -150,11 +150,11 @@ "colors.ignored": "Color for ignored resources.", "colors.conflict": "Color for resources with conflicts.", "colors.submodule": "Color for submodule resources.", - "view.workbench.scm.missing": "A valid git installation was not detected, more details can be found in the [git output](command:git.showOutput).\nPlease [install git](https://git-scm.com/), or learn more about how to use Git and source control in VS Code in [our docs](https://aka.ms/vscode-scm).\nIf you're using a different version control system, you can [search the Marketplace](command:workbench.extensions.search?%22%40category%3A%5C%22scm%20providers%5C%22%22) for additional extensions.", - "view.workbench.scm.disabled": "If you would like to use git features, please enable git in your [settings](command:workbench.action.openSettings?%5B%22git.enabled%22%5D).\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", - "view.workbench.scm.empty": "In order to use git features, you can open a folder containing a git repository or clone from a URL.\n[Open Folder](command:vscode.openFolder)\n[Clone Repository](command:git.clone)\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", - "view.workbench.scm.folder": "The folder currently open doesn't have a git repository.\n[Initialize Repository](command:git.init?%5Btrue%5D)\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", - "view.workbench.scm.workspace": "The workspace currently open doesn't have any folders containing git repositories.\n[Initialize Repository](command:git.init)\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", - "view.workbench.scm.emptyWorkspace": "The workspace currently open doesn't have any folders containing git repositories.\n[Add Folder to Workspace](command:workbench.action.addRootFolder)\nTo learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", - "view.workbench.cloneRepository": "You can also clone a repository from a URL. To learn more about how to use Git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).\n[Clone Repository](command:git.clone)" + "view.workbench.scm.missing": "A valid git installation was not detected, more details can be found in the [git output](command:git.showOutput).\nPlease [install git](https://git-scm.com/), or learn more about how to use git and source control in VS Code in [our docs](https://aka.ms/vscode-scm).\nIf you're using a different version control system, you can [search the Marketplace](command:workbench.extensions.search?%22%40category%3A%5C%22scm%20providers%5C%22%22) for additional extensions.", + "view.workbench.scm.disabled": "If you would like to use git features, please enable git in your [settings](command:workbench.action.openSettings?%5B%22git.enabled%22%5D).\nTo learn more about how to use git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", + "view.workbench.scm.empty": "In order to use git features, you can open a folder containing a git repository or clone from a URL.\n[Open Folder](command:vscode.openFolder)\n[Clone Repository](command:git.clone)\nTo learn more about how to use git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", + "view.workbench.scm.folder": "The folder currently open doesn't have a git repository.\n[Initialize Repository](command:git.init?%5Btrue%5D)\nTo learn more about how to use git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", + "view.workbench.scm.workspace": "The workspace currently open doesn't have any folders containing git repositories.\n[Initialize Repository](command:git.init)\nTo learn more about how to use git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", + "view.workbench.scm.emptyWorkspace": "The workspace currently open doesn't have any folders containing git repositories.\n[Add Folder to Workspace](command:workbench.action.addRootFolder)\nTo learn more about how to use git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).", + "view.workbench.cloneRepository": "You can also clone a repository from a URL. To learn more about how to use git and source control in VS Code [read our docs](https://aka.ms/vscode-scm).\n[Clone Repository](command:git.clone)" } diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index ad4d47485e7..a27fa783fc0 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -1397,12 +1397,16 @@ export class CommandCenter { opts.signoff = true; } + const smartCommitChanges = config.get<'all' | 'tracked'>('smartCommitChanges'); + if ( ( // no changes (noStagedChanges && noUnstagedChanges) // or no staged changes and not `all` || (!opts.all && noStagedChanges) + // no staged changes and no tracked unstaged changes + || (noStagedChanges && smartCommitChanges === 'tracked' && repository.workingTreeGroup.resourceStates.every(r => r.type === Status.UNTRACKED)) ) && !opts.empty ) { @@ -1416,7 +1420,7 @@ export class CommandCenter { return false; } - if (opts.all && config.get<'all' | 'tracked'>('smartCommitChanges') === 'tracked') { + if (opts.all && smartCommitChanges === 'tracked') { opts.all = 'tracked'; } @@ -2353,7 +2357,7 @@ export class CommandCenter { else if (item.previousRef === 'HEAD' && item.ref === '~') { title = localize('git.title.index', '{0} (Index)', basename); } else { - title = localize('git.title.diffRefs', '{0} ({1}) \u27f7 {0} ({2})', basename, item.shortPreviousRef, item.shortRef); + title = localize('git.title.diffRefs', '{0} ({1}) ⟷ {0} ({2})', basename, item.shortPreviousRef, item.shortRef); } return commands.executeCommand('vscode.diff', toGitUri(uri, item.previousRef), item.ref === '' ? uri : toGitUri(uri, item.ref), title); diff --git a/extensions/git/src/timelineProvider.ts b/extensions/git/src/timelineProvider.ts index 3fe8e306c95..872e5c36a80 100644 --- a/extensions/git/src/timelineProvider.ts +++ b/extensions/git/src/timelineProvider.ts @@ -80,7 +80,7 @@ export class GitTimelineProvider implements TimelineProvider { constructor(private readonly _model: Model) { this._disposable = Disposable.from( _model.onDidOpenRepository(this.onRepositoriesChanged, this), - workspace.registerTimelineProvider('*', this), + workspace.registerTimelineProvider(['file', 'git', 'gitlens-git'], this), ); } @@ -114,9 +114,9 @@ export class GitTimelineProvider implements TimelineProvider { // TODO[ECA]: Ensure that the uri is a file -- if not we could get the history of the repo? let limit: number | undefined; - if (typeof options.limit === 'string') { + if (options.limit !== undefined && typeof options.limit !== 'number') { try { - const result = await this._model.git.exec(repo.root, ['rev-list', '--count', `${options.limit}..`, '--', uri.fsPath]); + const result = await this._model.git.exec(repo.root, ['rev-list', '--count', `${options.limit.cursor}..`, '--', uri.fsPath]); if (!result.exitCode) { // Ask for 1 more than so we can determine if there are more commits limit = Number(result.stdout) + 1; @@ -182,7 +182,7 @@ export class GitTimelineProvider implements TimelineProvider { const item = new GitTimelineItem('~', 'HEAD', localize('git.timeline.stagedChanges', 'Staged Changes'), date.getTime(), 'index', 'git:file:index'); // TODO[ECA]: Replace with a better icon -- reflecting its status maybe? item.iconPath = new (ThemeIcon as any)('git-commit'); - item.description = you; + item.description = ''; item.detail = localize('git.timeline.detail', '{0} \u2014 {1}\n{2}\n\n{3}', you, localize('git.index', 'Index'), dateFormatter.format('MMMM Do, YYYY h:mma'), Resource.getStatusText(index.type)); item.command = { title: 'Open Comparison', @@ -201,7 +201,7 @@ export class GitTimelineProvider implements TimelineProvider { const item = new GitTimelineItem('', index ? '~' : 'HEAD', localize('git.timeline.uncommitedChanges', 'Uncommited Changes'), date.getTime(), 'working', 'git:file:working'); // TODO[ECA]: Replace with a better icon -- reflecting its status maybe? item.iconPath = new (ThemeIcon as any)('git-commit'); - item.description = you; + item.description = ''; item.detail = localize('git.timeline.detail', '{0} \u2014 {1}\n{2}\n\n{3}', you, localize('git.workingTree', 'Working Tree'), dateFormatter.format('MMMM Do, YYYY h:mma'), Resource.getStatusText(working.type)); item.command = { title: 'Open Comparison', diff --git a/extensions/github-authentication/src/extension.ts b/extensions/github-authentication/src/extension.ts index 943db6f20b5..c377e020939 100644 --- a/extensions/github-authentication/src/extension.ts +++ b/extensions/github-authentication/src/extension.ts @@ -16,7 +16,7 @@ export async function activate(context: vscode.ExtensionContext) { await loginService.initialize(); vscode.authentication.registerAuthenticationProvider({ - id: 'GitHub', + id: 'github', displayName: 'GitHub', onDidChangeSessions: onDidChangeSessions.event, getSessions: () => Promise.resolve(loginService.sessions), diff --git a/extensions/github-authentication/src/github.ts b/extensions/github-authentication/src/github.ts index 0f46d20d81b..52c7c4e333a 100644 --- a/extensions/github-authentication/src/github.ts +++ b/extensions/github-authentication/src/github.ts @@ -71,7 +71,7 @@ export class GitHubAuthenticationProvider { id: session.id, accountName: session.accountName, scopes: session.scopes, - accessToken: () => Promise.resolve(session.accessToken) + getAccessToken: () => Promise.resolve(session.accessToken) }; }); } catch (e) { @@ -84,7 +84,7 @@ export class GitHubAuthenticationProvider { private async storeSessions(): Promise { const sessionData: SessionData[] = await Promise.all(this._sessions.map(async session => { - const resolvedAccessToken = await session.accessToken(); + const resolvedAccessToken = await session.getAccessToken(); return { id: session.id, accountName: session.accountName, @@ -111,7 +111,7 @@ export class GitHubAuthenticationProvider { const userInfo = await this._githubServer.getUserInfo(token); return { id: userInfo.id, - accessToken: () => Promise.resolve(token), + getAccessToken: () => Promise.resolve(token), accountName: userInfo.accountName, scopes: scopes }; diff --git a/extensions/image-preview/package.json b/extensions/image-preview/package.json index 998f3799f4d..48c7ae314a9 100644 --- a/extensions/image-preview/package.json +++ b/extensions/image-preview/package.json @@ -17,7 +17,7 @@ "Other" ], "activationEvents": [ - "onWebviewEditor:imagePreview.previewEditor", + "onCustomEditor:imagePreview.previewEditor", "onCommand:imagePreview.zoomIn", "onCommand:imagePreview.zoomOut" ], diff --git a/extensions/json-language-features/server/src/jsonServerMain.ts b/extensions/json-language-features/server/src/jsonServerMain.ts index 4f40bd5a3fd..182eef7e9ee 100644 --- a/extensions/json-language-features/server/src/jsonServerMain.ts +++ b/extensions/json-language-features/server/src/jsonServerMain.ts @@ -160,7 +160,10 @@ connection.onInitialize((params: InitializeParams): InitializeResult => { formatterMaxNumberOfEdits = params.initializationOptions?.customCapabilities?.rangeFormatting?.editLimit || Number.MAX_VALUE; const capabilities: ServerCapabilities = { textDocumentSync: TextDocumentSyncKind.Incremental, - completionProvider: clientSnippetSupport ? { resolveProvider: true, triggerCharacters: ['"', ':'] } : undefined, + completionProvider: clientSnippetSupport ? { + resolveProvider: false, // turn off resolving as the current language service doesn't do anything on resolve. Also fixes #91747 + triggerCharacters: ['"', ':'] + } : undefined, hoverProvider: true, documentSymbolProvider: true, documentRangeFormattingProvider: params.initializationOptions.provideFormatter === true, diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index f13d46a4b1c..dbc8746c913 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -26,7 +26,7 @@ "onCommand:markdown.showPreviewSecuritySelector", "onCommand:markdown.api.render", "onWebviewPanel:markdown.preview", - "onWebviewEditor:vscode.markdown.preview.editor" + "onCustomEditor:vscode.markdown.preview.editor" ], "contributes": { "commands": [ diff --git a/extensions/package.json b/extensions/package.json index b8e304b2343..7b4e8171c1f 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "3.8.2" + "typescript": "3.8.3" }, "scripts": { "postinstall": "node ./postinstall" diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index 7392a267a56..b3f12439bc8 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -638,6 +638,42 @@ "description": "%typescript.preferences.importModuleSpecifier%", "scope": "resource" }, + "javascript.preferences.importModuleSpecifierEnding": { + "type": "string", + "enum": [ + "auto", + "minimal", + "index", + "js" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.importModuleSpecifierEnding.auto%", + "%typescript.preferences.importModuleSpecifierEnding.minimal%", + "%typescript.preferences.importModuleSpecifierEnding.index%", + "%typescript.preferences.importModuleSpecifierEnding.js%" + ], + "default": "auto", + "description": "%typescript.preferences.importModuleSpecifierEnding%", + "scope": "resource" + }, + "typescript.preferences.importModuleSpecifierEnding": { + "type": "string", + "enum": [ + "auto", + "minimal", + "index", + "js" + ], + "markdownEnumDescriptions": [ + "%typescript.preferences.importModuleSpecifierEnding.auto%", + "%typescript.preferences.importModuleSpecifierEnding.minimal%", + "%typescript.preferences.importModuleSpecifierEnding.index%", + "%typescript.preferences.importModuleSpecifierEnding.js%" + ], + "default": "auto", + "description": "%typescript.preferences.importModuleSpecifierEnding%", + "scope": "resource" + }, "javascript.preferences.renameShorthandProperties": { "type": "boolean", "default": true, diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index a421936acb9..a2bf1f3eeed 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -70,6 +70,11 @@ "typescript.preferences.importModuleSpecifier.auto": "Automatically select import path style. Prefers using a relative import if `baseUrl` is configured and the relative path has fewer segments than the non-relative import.", "typescript.preferences.importModuleSpecifier.relative": "Relative to the file location.", "typescript.preferences.importModuleSpecifier.nonRelative": "Based on the `baseUrl` configured in your `jsconfig.json` / `tsconfig.json`.", + "typescript.preferences.importModuleSpecifierEnding": "Preferred path ending for auto imports.", + "typescript.preferences.importModuleSpecifierEnding.auto": "Use project settings to select a default.", + "typescript.preferences.importModuleSpecifierEnding.minimal": "Shorten `./component/index.js` to `./component`.", + "typescript.preferences.importModuleSpecifierEnding.index": "Shorten `./component/index.js` to `./component/index`", + "typescript.preferences.importModuleSpecifierEnding.js": "Do not shorten path endings; include the `.js` extension.", "typescript.updateImportsOnFileMove.enabled": "Enable/disable automatic updating of import paths when you rename or move a file in VS Code. Requires using TypeScript 2.9 or newer in the workspace.", "typescript.updateImportsOnFileMove.enabled.prompt": "Prompt on each rename.", "typescript.updateImportsOnFileMove.enabled.always": "Always update paths automatically.", diff --git a/extensions/typescript-language-features/src/features/fileConfigurationManager.ts b/extensions/typescript-language-features/src/features/fileConfigurationManager.ts index cad4e5196b9..be7431eac9f 100644 --- a/extensions/typescript-language-features/src/features/fileConfigurationManager.ts +++ b/extensions/typescript-language-features/src/features/fileConfigurationManager.ts @@ -7,9 +7,10 @@ import * as vscode from 'vscode'; import type * as Proto from '../protocol'; import { ITypeScriptServiceClient } from '../typescriptService'; import API from '../utils/api'; +import { Disposable } from '../utils/dispose'; +import * as fileSchemes from '../utils/fileSchemes'; import { isTypeScriptDocument } from '../utils/languageModeIds'; import { ResourceMap } from '../utils/resourceMap'; -import { Disposable } from '../utils/dispose'; function objsAreEqual(a: T, b: T): boolean { @@ -144,9 +145,7 @@ export default class FileConfigurationManager extends Disposable { isTypeScriptDocument(document) ? 'typescript.format' : 'javascript.format', document.uri); - // `semicolons` added to `Proto.FormatCodeSettings` in TypeScript 3.7: - // remove intersection type after upgrading TypeScript. - const settings: Proto.FormatCodeSettings & { semicolons?: string } = { + return { tabSize: options.tabSize, indentSize: options.tabSize, convertTabsToSpaces: options.insertSpaces, @@ -169,8 +168,6 @@ export default class FileConfigurationManager extends Disposable { placeOpenBraceOnNewLineForControlBlocks: config.get('placeOpenBraceOnNewLineForControlBlocks'), semicolons: config.get('semicolons'), }; - - return settings; } private getPreferences(document: vscode.TextDocument): Proto.UserPreferences { @@ -182,13 +179,18 @@ export default class FileConfigurationManager extends Disposable { isTypeScriptDocument(document) ? 'typescript.preferences' : 'javascript.preferences', document.uri); - return { + // `importModuleSpecifierEnding` added to `Proto.UserPreferences` in TypeScript 3.9: + // remove intersection type after upgrading TypeScript. + const preferences: Proto.UserPreferences & { importModuleSpecifierEnding?: string } = { quotePreference: this.getQuoteStylePreference(config), importModuleSpecifierPreference: getImportModuleSpecifierPreference(config), - allowTextChangesInNewFiles: document.uri.scheme === 'file', + importModuleSpecifierEnding: getImportModuleSpecifierEndingPreference(config), + allowTextChangesInNewFiles: document.uri.scheme === fileSchemes.file, providePrefixAndSuffixTextForRename: config.get('renameShorthandProperties', true), allowRenameOfImportPath: true, }; + + return preferences; } private getQuoteStylePreference(config: vscode.WorkspaceConfiguration) { @@ -207,3 +209,12 @@ function getImportModuleSpecifierPreference(config: vscode.WorkspaceConfiguratio default: return undefined; } } + +function getImportModuleSpecifierEndingPreference(config: vscode.WorkspaceConfiguration) { + switch (config.get('importModuleSpecifierEnding')) { + case 'minimal': return 'minimal'; + case 'index': return 'index'; + case 'js': return 'js'; + default: return 'auto'; + } +} diff --git a/extensions/typescript-language-features/src/typescriptServiceClient.ts b/extensions/typescript-language-features/src/typescriptServiceClient.ts index 2d256281438..64527b2918e 100644 --- a/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -735,7 +735,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType "command" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } */ - this.logTelemetry('fatalError', { command, ...(error instanceof TypeScriptServerError ? error.telemetry : {}) }); + this.logTelemetry('fatalError', { ...(error instanceof TypeScriptServerError ? error.telemetry : { command }) }); console.error(`A non-recoverable error occured while executing tsserver command: ${command}`); if (error instanceof TypeScriptServerError && error.serverErrorText) { console.error(error.serverErrorText); diff --git a/extensions/vscode-account/package.json b/extensions/vscode-account/package.json index c4df52ce732..cd579aef85a 100644 --- a/extensions/vscode-account/package.json +++ b/extensions/vscode-account/package.json @@ -39,5 +39,8 @@ "tslint": "^5.12.1", "@types/node": "^10.12.21", "@types/keytar": "^4.0.1" + }, + "dependencies": { + "vscode-nls": "^4.1.1" } } diff --git a/extensions/vscode-account/package.nls.json b/extensions/vscode-account/package.nls.json index 8211a3f6e9c..c0bb4c4a6a0 100644 --- a/extensions/vscode-account/package.nls.json +++ b/extensions/vscode-account/package.nls.json @@ -1,6 +1,6 @@ { "displayName": "Microsoft Account", "description": "Microsoft authentication provider", - "signIn": "Sign in", - "signOut": "Sign out" + "signIn": "Sign In", + "signOut": "Sign Out" } diff --git a/extensions/vscode-account/src/AADHelper.ts b/extensions/vscode-account/src/AADHelper.ts index 5f193828141..16e7990be0b 100644 --- a/extensions/vscode-account/src/AADHelper.ts +++ b/extensions/vscode-account/src/AADHelper.ts @@ -184,7 +184,7 @@ export class AzureActiveDirectoryService { private convertToSession(token: IToken): vscode.AuthenticationSession { return { id: token.sessionId, - accessToken: () => this.resolveAccessToken(token), + getAccessToken: () => this.resolveAccessToken(token), accountName: token.accountName, scopes: token.scope.split(' ') }; @@ -192,7 +192,9 @@ export class AzureActiveDirectoryService { private async resolveAccessToken(token: IToken): Promise { if (token.accessToken && (!token.expiresAt || token.expiresAt > Date.now())) { - Logger.info('Token available from cache'); + token.expiresAt + ? Logger.info(`Token available from cache, expires in ${token.expiresAt - Date.now()} milliseconds`) + : Logger.info('Token available from cache'); return Promise.resolve(token.accessToken); } diff --git a/extensions/vscode-account/src/extension.ts b/extensions/vscode-account/src/extension.ts index 88f5f3133ed..e70fbe59cb0 100644 --- a/extensions/vscode-account/src/extension.ts +++ b/extensions/vscode-account/src/extension.ts @@ -5,6 +5,9 @@ import * as vscode from 'vscode'; import { AzureActiveDirectoryService, onDidChangeSessions } from './AADHelper'; +import * as nls from 'vscode-nls'; + +const localize = nls.loadMessageBundle(); export const DEFAULT_SCOPES = 'https://management.core.windows.net/.default offline_access'; @@ -15,7 +18,7 @@ export async function activate(context: vscode.ExtensionContext) { await loginService.initialize(); context.subscriptions.push(vscode.authentication.registerAuthenticationProvider({ - id: 'MSA', + id: 'microsoft', displayName: 'Microsoft', onDidChangeSessions: onDidChangeSessions.event, getSessions: () => Promise.resolve(loginService.sessions), @@ -45,6 +48,7 @@ export async function activate(context: vscode.ExtensionContext) { if (sessions.length === 1) { await loginService.logout(loginService.sessions[0].id); onDidChangeSessions.fire(); + vscode.window.showInformationMessage(localize('signedOut', "Successfully signed out.")); return; } @@ -58,6 +62,7 @@ export async function activate(context: vscode.ExtensionContext) { if (selectedSession) { await loginService.logout(selectedSession.id); onDidChangeSessions.fire(); + vscode.window.showInformationMessage(localize('signedOut', "Successfully signed out.")); return; } })); diff --git a/extensions/vscode-account/yarn.lock b/extensions/vscode-account/yarn.lock index 3acdda242e9..4a86ea6a2a2 100644 --- a/extensions/vscode-account/yarn.lock +++ b/extensions/vscode-account/yarn.lock @@ -635,6 +635,11 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= +vscode-nls@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.1.tgz#f9916b64e4947b20322defb1e676a495861f133c" + integrity sha512-4R+2UoUUU/LdnMnFjePxfLqNhBS8lrAFyX7pjb2ud/lqDkrUavFUTcG7wR0HBZFakae0Q6KLBFjMS6W93F403A== + which-pm-runs@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb" diff --git a/extensions/yarn.lock b/extensions/yarn.lock index 43a70c058c2..83bd84cdd90 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -typescript@3.8.2: - version "3.8.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.2.tgz#91d6868aaead7da74f493c553aeff76c0c0b1d5a" - integrity sha512-EgOVgL/4xfVrCMbhYKUQTdF37SQn4Iw73H5BgCrF1Abdun7Kwy/QZsE/ssAy0y4LxBbvua3PIbFsbRczWWnDdQ== +typescript@3.8.3: + version "3.8.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061" + integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w== diff --git a/package.json b/package.json index 7910be5d9ae..3cdbce18f68 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", - "version": "1.43.0", - "distro": "d9ed2bff7e779c71264b0c08311d08192924f473", + "version": "1.44.0", + "distro": "5ddb4bf6f3cbd0c5940960a7dd5dd3790d1d844e", "author": { "name": "Microsoft Corporation" }, @@ -150,13 +150,13 @@ "source-map": "^0.4.4", "style-loader": "^1.0.0", "ts-loader": "^4.4.2", - "typescript": "3.8.2", + "typescript": "^3.9.0-dev.20200229", "typescript-formatter": "7.1.0", "underscore": "^1.8.2", "vinyl": "^2.0.0", "vinyl-fs": "^3.0.0", "vsce": "1.48.0", - "vscode-debugprotocol": "1.39.0-pre.0", + "vscode-debugprotocol": "1.39.0", "vscode-nls-dev": "^3.3.1", "webpack": "^4.16.5", "webpack-cli": "^3.3.8", @@ -177,4 +177,4 @@ "windows-mutex": "0.3.0", "windows-process-tree": "0.2.4" } -} +} \ No newline at end of file diff --git a/resources/linux/snap/electron-launch b/resources/linux/snap/electron-launch index 2a1c4395187..9f4eb6a23b8 100755 --- a/resources/linux/snap/electron-launch +++ b/resources/linux/snap/electron-launch @@ -2,7 +2,7 @@ # On Fedora $SNAP is under /var and there is some magic to map it to /snap. # We need to handle that case and reset $SNAP -SNAP=$(echo $SNAP | sed -e "s|/var/lib/snapd||g") +SNAP=$(echo "$SNAP" | sed -e "s|/var/lib/snapd||g") if [ "$SNAP_ARCH" == "amd64" ]; then ARCH="x86_64-linux-gnu" @@ -14,21 +14,21 @@ else ARCH="$SNAP_ARCH-linux-gnu" fi -export XDG_CACHE_HOME=$SNAP_USER_COMMON/.cache -if [[ -d $SNAP_USER_DATA/.cache && ! -e $XDG_CACHE_HOME ]]; then +GDK_CACHE_DIR="$SNAP_USER_COMMON/.cache" +if [[ -d "$SNAP_USER_DATA/.cache" && ! -e "$GDK_CACHE_DIR" ]]; then # the .cache directory used to be stored under $SNAP_USER_DATA, migrate it - mv $SNAP_USER_DATA/.cache $SNAP_USER_COMMON/ + mv "$SNAP_USER_DATA/.cache" "$SNAP_USER_COMMON/" fi -mkdir -p $XDG_CACHE_HOME +[ ! -d "$GDK_CACHE_DIR" ] && mkdir -p "$GDK_CACHE_DIR" # Gdk-pixbuf loaders -export GDK_PIXBUF_MODULE_FILE=$XDG_CACHE_HOME/gdk-pixbuf-loaders.cache -export GDK_PIXBUF_MODULEDIR=$SNAP/usr/lib/$ARCH/gdk-pixbuf-2.0/2.10.0/loaders -if [ -f $SNAP/usr/lib/$ARCH/gdk-pixbuf-2.0/gdk-pixbuf-query-loaders ]; then - $SNAP/usr/lib/$ARCH/gdk-pixbuf-2.0/gdk-pixbuf-query-loaders > $GDK_PIXBUF_MODULE_FILE +export GDK_PIXBUF_MODULE_FILE="$GDK_CACHE_DIR/gdk-pixbuf-loaders.cache" +export GDK_PIXBUF_MODULEDIR="$SNAP/usr/lib/$ARCH/gdk-pixbuf-2.0/2.10.0/loaders" +if [ -f "$SNAP/usr/lib/$ARCH/gdk-pixbuf-2.0/gdk-pixbuf-query-loaders" ]; then + "$SNAP/usr/lib/$ARCH/gdk-pixbuf-2.0/gdk-pixbuf-query-loaders" > "$GDK_PIXBUF_MODULE_FILE" fi # Create $XDG_RUNTIME_DIR if not exists (to be removed when https://pad.lv/1656340 is fixed) -[ -n "$XDG_RUNTIME_DIR" ] && mkdir -p $XDG_RUNTIME_DIR -m 700 +[ -n "$XDG_RUNTIME_DIR" ] && mkdir -p "$XDG_RUNTIME_DIR" -m 700 exec "$@" diff --git a/src/bootstrap-window.js b/src/bootstrap-window.js index 0ab195f924f..cc925afa0b7 100644 --- a/src/bootstrap-window.js +++ b/src/bootstrap-window.js @@ -31,7 +31,7 @@ exports.load = function (modulePaths, resultCallback, options) { const args = parseURLQueryArgs(); /** - * // configuration: IWindowConfiguration + * // configuration: INativeWindowConfiguration * @type {{ * zoomLevel?: number, * extensionDevelopmentPath?: string[], diff --git a/src/main.js b/src/main.js index bc268c8cd12..e1df5deb07c 100644 --- a/src/main.js +++ b/src/main.js @@ -175,7 +175,7 @@ function configureCommandlineSwitchesSync(cliArgs) { app.commandLine.appendSwitch('js-flags', jsFlags); } - // TODO@Ben TODO@Deepak Electron 7 workaround for https://github.com/microsoft/vscode/issues/88873 + // TODO@Deepak Electron 7 workaround for https://github.com/microsoft/vscode/issues/88873 app.commandLine.appendSwitch('disable-features', 'LayoutNG'); return argvConfig; diff --git a/src/tsconfig.base.json b/src/tsconfig.base.json index 73985c6c9af..b8dab46bfb6 100644 --- a/src/tsconfig.base.json +++ b/src/tsconfig.base.json @@ -12,6 +12,13 @@ "vs/*": [ "./vs/*" ] - } + }, + "lib": [ + "ES2015", + "ES2018.Promise", + "DOM", + "DOM.Iterable", + "WebWorker.ImportScripts" + ] } } diff --git a/src/tsconfig.json b/src/tsconfig.json index b8cc1caf2cd..9419d9af0f4 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -6,11 +6,6 @@ "sourceMap": false, "outDir": "../out", "target": "es2017", - "lib": [ - "dom", - "es5", - "es2015.iterable" - ], "types": [ "keytar", "mocha", @@ -22,8 +17,5 @@ "include": [ "./typings", "./vs" - ], - "exclude": [ - "./typings/es6-promise.d.ts" ] } diff --git a/src/tsconfig.monaco.json b/src/tsconfig.monaco.json index a6430a44ccb..1e169f96b3a 100644 --- a/src/tsconfig.monaco.json +++ b/src/tsconfig.monaco.json @@ -8,17 +8,14 @@ "moduleResolution": "classic", "removeComments": false, "preserveConstEnums": true, - "target": "es5", + "target": "es6", "sourceMap": false, "declaration": true }, "include": [ "typings/require.d.ts", "typings/thenable.d.ts", - "typings/es6-promise.d.ts", - "typings/lib.es2018.promise.d.ts", "typings/lib.array-ext.d.ts", - "typings/lib.ie11_safe_es6.d.ts", "vs/css.d.ts", "vs/monaco.d.ts", "vs/nls.d.ts", diff --git a/src/typings/es2015-proxy.d.ts b/src/typings/es2015-proxy.d.ts deleted file mode 100644 index 00f7c2b0642..00000000000 --- a/src/typings/es2015-proxy.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -// from TypeScript: lib.es2015.proxy.d.ts - -interface ProxyHandler { - getPrototypeOf?(target: T): object | null; - setPrototypeOf?(target: T, v: any): boolean; - isExtensible?(target: T): boolean; - preventExtensions?(target: T): boolean; - getOwnPropertyDescriptor?(target: T, p: PropertyKey): PropertyDescriptor | undefined; - has?(target: T, p: PropertyKey): boolean; - get?(target: T, p: PropertyKey, receiver: any): any; - set?(target: T, p: PropertyKey, value: any, receiver: any): boolean; - deleteProperty?(target: T, p: PropertyKey): boolean; - defineProperty?(target: T, p: PropertyKey, attributes: PropertyDescriptor): boolean; - enumerate?(target: T): PropertyKey[]; - ownKeys?(target: T): PropertyKey[]; - apply?(target: T, thisArg: any, argArray?: any): any; - construct?(target: T, argArray: any, newTarget?: any): object; -} - -interface ProxyConstructor { - revocable(target: T, handler: ProxyHandler): { proxy: T; revoke: () => void; }; - new (target: T, handler: ProxyHandler): T; -} -declare var Proxy: ProxyConstructor; diff --git a/src/typings/es6-promise.d.ts b/src/typings/es6-promise.d.ts deleted file mode 100644 index 2d3271e2848..00000000000 --- a/src/typings/es6-promise.d.ts +++ /dev/null @@ -1,89 +0,0 @@ -// Type definitions for es6-promise -// Project: https://github.com/jakearchibald/ES6-Promise -// Definitions by: François de Campredon , vvakame -// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped - -interface Thenable { - then(onFulfilled?: (value: T) => U | Thenable, onRejected?: (error: any) => U | Thenable): Thenable; - then(onFulfilled?: (value: T) => U | Thenable, onRejected?: (error: any) => void): Thenable; -} - -declare class Promise implements Thenable { - /** - * If you call resolve in the body of the callback passed to the constructor, - * your promise is fulfilled with result object passed to resolve. - * If you call reject your promise is rejected with the object passed to reject. - * For consistency and debugging (eg stack traces), obj should be an instanceof Error. - * Any errors thrown in the constructor callback will be implicitly passed to reject(). - */ - constructor(callback: (resolve: (value?: T | Thenable) => void, reject: (error?: any) => void) => void); - - /** - * onFulfilled is called when/if "promise" resolves. onRejected is called when/if "promise" rejects. - * Both are optional, if either/both are omitted the next onFulfilled/onRejected in the chain is called. - * Both callbacks have a single parameter , the fulfillment value or rejection reason. - * "then" returns a new promise equivalent to the value you return from onFulfilled/onRejected after being passed through Promise.resolve. - * If an error is thrown in the callback, the returned promise rejects with that error. - * - * @param onFulfilled called when/if "promise" resolves - * @param onRejected called when/if "promise" rejects - */ - then(onFulfilled?: (value: T) => U | Thenable, onRejected?: (error: any) => U | Thenable): Promise; - then(onFulfilled?: (value: T) => U | Thenable, onRejected?: (error: any) => void): Promise; - - /** - * Sugar for promise.then(undefined, onRejected) - * - * @param onRejected called when/if "promise" rejects - */ - catch(onRejected?: (error: any) => U | Thenable): Promise; -} - -declare namespace Promise { - /** - * Make a new promise from the thenable. - * A thenable is promise-like in as far as it has a "then" method. - */ - function resolve(value: T | Thenable): Promise; - - /** - * - */ - function resolve(): Promise; - - /** - * Make a promise that rejects to obj. For consistency and debugging (eg stack traces), obj should be an instanceof Error - */ - function reject(error: any): Promise; - function reject(error: T): Promise; - - /** - * Make a promise that fulfills when every item in the array fulfills, and rejects if (and when) any item rejects. - * the array passed to all can be a mixture of promise-like objects and other objects. - * The fulfillment value is an array (in order) of fulfillment values. The rejection value is the first rejection value. - */ - function all(values: [T1 | Thenable, T2 | Thenable, T3 | Thenable, T4 | Thenable, T5 | Thenable, T6 | Thenable, T7 | Thenable, T8 | Thenable, T9 | Thenable, T10 | Thenable]): Promise<[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10]>; - function all(values: [T1 | Thenable, T2 | Thenable, T3 | Thenable, T4 | Thenable, T5 | Thenable, T6 | Thenable, T7 | Thenable, T8 | Thenable, T9 | Thenable]): Promise<[T1, T2, T3, T4, T5, T6, T7, T8, T9]>; - function all(values: [T1 | Thenable, T2 | Thenable, T3 | Thenable, T4 | Thenable, T5 | Thenable, T6 | Thenable, T7 | Thenable, T8 | Thenable]): Promise<[T1, T2, T3, T4, T5, T6, T7, T8]>; - function all(values: [T1 | Thenable, T2 | Thenable, T3 | Thenable, T4 | Thenable, T5 | Thenable, T6 | Thenable, T7 | Thenable]): Promise<[T1, T2, T3, T4, T5, T6, T7]>; - function all(values: [T1 | Thenable, T2 | Thenable, T3 | Thenable, T4 | Thenable, T5 | Thenable, T6 | Thenable]): Promise<[T1, T2, T3, T4, T5, T6]>; - function all(values: [T1 | Thenable, T2 | Thenable, T3 | Thenable, T4 | Thenable, T5 | Thenable]): Promise<[T1, T2, T3, T4, T5]>; - function all(values: [T1 | Thenable, T2 | Thenable, T3 | Thenable, T4 | Thenable]): Promise<[T1, T2, T3, T4]>; - function all(values: [T1 | Thenable, T2 | Thenable, T3 | Thenable]): Promise<[T1, T2, T3]>; - function all(values: [T1 | Thenable, T2 | Thenable]): Promise<[T1, T2]>; - function all(values: (T | Thenable)[]): Promise; - - /** - * Make a Promise that fulfills when any item fulfills, and rejects if any item rejects. - */ - function race(promises: (T | Thenable)[]): Promise; -} - -declare module 'es6-promise' { - var foo: typeof Promise; // Temp variable to reference Promise in local context - namespace rsvp { - export var Promise: typeof foo; - export function polyfill(): void; - } - export = rsvp; -} diff --git a/src/typings/lib.es2018.promise.d.ts b/src/typings/lib.es2018.promise.d.ts deleted file mode 100644 index 9f7b2d38cb2..00000000000 --- a/src/typings/lib.es2018.promise.d.ts +++ /dev/null @@ -1,27 +0,0 @@ -/*! ***************************************************************************** -Copyright (c) Microsoft Corporation. All rights reserved. -Licensed under the Apache License, Version 2.0 (the "License"); you may not use -this file except in compliance with the License. You may obtain a copy of the -License at http://www.apache.org/licenses/LICENSE-2.0 - -THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED -WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, -MERCHANTABLITY OR NON-INFRINGEMENT. - -See the Apache Version 2.0 License for specific language governing permissions -and limitations under the License. -***************************************************************************** */ - -/** - * Represents the completion of an asynchronous operation - */ -interface Promise { - /** - * Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected). The - * resolved value cannot be modified from the callback. - * @param onfinally The callback to execute when the Promise is settled (fulfilled or rejected). - * @returns A Promise for the completion of the callback. - */ - finally(onfinally?: (() => void) | undefined | null): Promise; -} diff --git a/src/typings/lib.ie11_safe_es6.d.ts b/src/typings/lib.ie11_safe_es6.d.ts deleted file mode 100644 index 4d54d3c08ef..00000000000 --- a/src/typings/lib.ie11_safe_es6.d.ts +++ /dev/null @@ -1,821 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -// Defined a subset of ES6 built ins that run in IE11 -// CHECK WITH http://kangax.github.io/compat-table/es6/#ie11 - -interface Map { - clear(): void; - delete(key: K): boolean; - forEach(callbackfn: (value: V, index: K, map: Map) => void, thisArg?: any): void; - get(key: K): V | undefined; - has(key: K): boolean; - set(key: K, value: V): Map; - readonly size: number; - - // not supported on IE11: - // entries(): IterableIterator<[K, V]>; - // keys(): IterableIterator; - // values(): IterableIterator; - // [Symbol.iterator]():IterableIterator<[K,V]>; - // [Symbol.toStringTag]: string; -} - -interface MapConstructor { - new (): Map; - readonly prototype: Map; - - // not supported on IE11: - // new (iterable: Iterable<[K, V]>): Map; -} -declare var Map: MapConstructor; - - -interface Set { - add(value: T): Set; - clear(): void; - delete(value: T): boolean; - forEach(callbackfn: (value: T, index: T, set: Set) => void, thisArg?: any): void; - has(value: T): boolean; - readonly size: number; - - // not supported on IE11: - // entries(): IterableIterator<[T, T]>; - // keys(): IterableIterator; - // values(): IterableIterator; - // [Symbol.iterator]():IterableIterator; - // [Symbol.toStringTag]: string; -} - -interface SetConstructor { - new (): Set; - readonly prototype: Set; - - // not supported on IE11: - // new (iterable: Iterable): Set; -} -declare var Set: SetConstructor; - - -interface WeakMap { - delete(key: K): boolean; - get(key: K): V | undefined; - has(key: K): boolean; - // IE11 doesn't return this - // set(key: K, value?: V): this; - set(key: K, value?: V): undefined; -} - -interface WeakMapConstructor { - new(): WeakMap; - new (): WeakMap; - // new (entries?: [K, V][]): WeakMap; - readonly prototype: WeakMap; -} -declare var WeakMap: WeakMapConstructor; - - -// /** -// * Represents a raw buffer of binary data, which is used to store data for the -// * different typed arrays. ArrayBuffers cannot be read from or written to directly, -// * but can be passed to a typed array or DataView Object to interpret the raw -// * buffer as needed. -// */ -// interface ArrayBuffer { -// /** -// * Read-only. The length of the ArrayBuffer (in bytes). -// */ -// readonly byteLength: number; - -// /** -// * Returns a section of an ArrayBuffer. -// */ -// slice(begin: number, end?: number): ArrayBuffer; -// } - -// interface ArrayBufferConstructor { -// readonly prototype: ArrayBuffer; -// new (byteLength: number): ArrayBuffer; -// isView(arg: any): arg is ArrayBufferView; -// } -// declare const ArrayBuffer: ArrayBufferConstructor; - -// interface ArrayBufferView { -// /** -// * The ArrayBuffer instance referenced by the array. -// */ -// buffer: ArrayBuffer; - -// /** -// * The length in bytes of the array. -// */ -// byteLength: number; - -// /** -// * The offset in bytes of the array. -// */ -// byteOffset: number; -// } - -// interface DataView { -// readonly buffer: ArrayBuffer; -// readonly byteLength: number; -// readonly byteOffset: number; -// /** -// * Gets the Float32 value at the specified byte offset from the start of the view. There is -// * no alignment constraint; multi-byte values may be fetched from any offset. -// * @param byteOffset The place in the buffer at which the value should be retrieved. -// */ -// getFloat32(byteOffset: number, littleEndian?: boolean): number; - -// /** -// * Gets the Float64 value at the specified byte offset from the start of the view. There is -// * no alignment constraint; multi-byte values may be fetched from any offset. -// * @param byteOffset The place in the buffer at which the value should be retrieved. -// */ -// getFloat64(byteOffset: number, littleEndian?: boolean): number; - -// /** -// * Gets the Int8 value at the specified byte offset from the start of the view. There is -// * no alignment constraint; multi-byte values may be fetched from any offset. -// * @param byteOffset The place in the buffer at which the value should be retrieved. -// */ -// getInt8(byteOffset: number): number; - -// /** -// * Gets the Int16 value at the specified byte offset from the start of the view. There is -// * no alignment constraint; multi-byte values may be fetched from any offset. -// * @param byteOffset The place in the buffer at which the value should be retrieved. -// */ -// getInt16(byteOffset: number, littleEndian?: boolean): number; -// /** -// * Gets the Int32 value at the specified byte offset from the start of the view. There is -// * no alignment constraint; multi-byte values may be fetched from any offset. -// * @param byteOffset The place in the buffer at which the value should be retrieved. -// */ -// getInt32(byteOffset: number, littleEndian?: boolean): number; - -// /** -// * Gets the Uint8 value at the specified byte offset from the start of the view. There is -// * no alignment constraint; multi-byte values may be fetched from any offset. -// * @param byteOffset The place in the buffer at which the value should be retrieved. -// */ -// getUint8(byteOffset: number): number; - -// /** -// * Gets the Uint16 value at the specified byte offset from the start of the view. There is -// * no alignment constraint; multi-byte values may be fetched from any offset. -// * @param byteOffset The place in the buffer at which the value should be retrieved. -// */ -// getUint16(byteOffset: number, littleEndian?: boolean): number; - -// /** -// * Gets the Uint32 value at the specified byte offset from the start of the view. There is -// * no alignment constraint; multi-byte values may be fetched from any offset. -// * @param byteOffset The place in the buffer at which the value should be retrieved. -// */ -// getUint32(byteOffset: number, littleEndian?: boolean): number; - -// /** -// * Stores an Float32 value at the specified byte offset from the start of the view. -// * @param byteOffset The place in the buffer at which the value should be set. -// * @param value The value to set. -// * @param littleEndian If false or undefined, a big-endian value should be written, -// * otherwise a little-endian value should be written. -// */ -// setFloat32(byteOffset: number, value: number, littleEndian?: boolean): void; - -// /** -// * Stores an Float64 value at the specified byte offset from the start of the view. -// * @param byteOffset The place in the buffer at which the value should be set. -// * @param value The value to set. -// * @param littleEndian If false or undefined, a big-endian value should be written, -// * otherwise a little-endian value should be written. -// */ -// setFloat64(byteOffset: number, value: number, littleEndian?: boolean): void; - -// /** -// * Stores an Int8 value at the specified byte offset from the start of the view. -// * @param byteOffset The place in the buffer at which the value should be set. -// * @param value The value to set. -// */ -// setInt8(byteOffset: number, value: number): void; - -// /** -// * Stores an Int16 value at the specified byte offset from the start of the view. -// * @param byteOffset The place in the buffer at which the value should be set. -// * @param value The value to set. -// * @param littleEndian If false or undefined, a big-endian value should be written, -// * otherwise a little-endian value should be written. -// */ -// setInt16(byteOffset: number, value: number, littleEndian?: boolean): void; - -// /** -// * Stores an Int32 value at the specified byte offset from the start of the view. -// * @param byteOffset The place in the buffer at which the value should be set. -// * @param value The value to set. -// * @param littleEndian If false or undefined, a big-endian value should be written, -// * otherwise a little-endian value should be written. -// */ -// setInt32(byteOffset: number, value: number, littleEndian?: boolean): void; - -// /** -// * Stores an Uint8 value at the specified byte offset from the start of the view. -// * @param byteOffset The place in the buffer at which the value should be set. -// * @param value The value to set. -// */ -// setUint8(byteOffset: number, value: number): void; - -// /** -// * Stores an Uint16 value at the specified byte offset from the start of the view. -// * @param byteOffset The place in the buffer at which the value should be set. -// * @param value The value to set. -// * @param littleEndian If false or undefined, a big-endian value should be written, -// * otherwise a little-endian value should be written. -// */ -// setUint16(byteOffset: number, value: number, littleEndian?: boolean): void; - -// /** -// * Stores an Uint32 value at the specified byte offset from the start of the view. -// * @param byteOffset The place in the buffer at which the value should be set. -// * @param value The value to set. -// * @param littleEndian If false or undefined, a big-endian value should be written, -// * otherwise a little-endian value should be written. -// */ -// setUint32(byteOffset: number, value: number, littleEndian?: boolean): void; -// } - -// interface DataViewConstructor { -// new (buffer: ArrayBuffer, byteOffset?: number, byteLength?: number): DataView; -// } -// declare const DataView: DataViewConstructor; - - -// /** -// * A typed array of 8-bit integer values. The contents are initialized to 0. If the requested -// * number of bytes could not be allocated an exception is raised. -// */ -// interface Int8Array { -// /** -// * The size in bytes of each element in the array. -// */ -// readonly BYTES_PER_ELEMENT: number; - -// /** -// * The ArrayBuffer instance referenced by the array. -// */ -// readonly buffer: ArrayBuffer; - -// /** -// * The length in bytes of the array. -// */ -// readonly byteLength: number; - -// /** -// * The offset in bytes of the array. -// */ -// readonly byteOffset: number; - -// /** -// * The length of the array. -// */ -// readonly length: number; - -// /** -// * Sets a value or an array of values. -// * @param index The index of the location to set. -// * @param value The value to set. -// */ -// set(index: number, value: number): void; - -// /** -// * Sets a value or an array of values. -// * @param array A typed or untyped array of values to set. -// * @param offset The index in the current array at which the values are to be written. -// */ -// set(array: ArrayLike, offset?: number): void; - -// /** -// * Converts a number to a string by using the current locale. -// */ -// toLocaleString(): string; - -// /** -// * Returns a string representation of an array. -// */ -// toString(): string; - -// [index: number]: number; -// } -// interface Int8ArrayConstructor { -// readonly prototype: Int8Array; -// new (length: number): Int8Array; -// new (array: ArrayLike): Int8Array; -// new (buffer: ArrayBuffer, byteOffset?: number, length?: number): Int8Array; - -// /** -// * The size in bytes of each element in the array. -// */ -// readonly BYTES_PER_ELEMENT: number; - -// } -// declare const Int8Array: Int8ArrayConstructor; - -// /** -// * A typed array of 8-bit unsigned integer values. The contents are initialized to 0. If the -// * requested number of bytes could not be allocated an exception is raised. -// */ -// interface Uint8Array { -// /** -// * The size in bytes of each element in the array. -// */ -// readonly BYTES_PER_ELEMENT: number; - -// /** -// * The ArrayBuffer instance referenced by the array. -// */ -// readonly buffer: ArrayBuffer; - -// /** -// * The length in bytes of the array. -// */ -// readonly byteLength: number; - -// /** -// * The offset in bytes of the array. -// */ -// readonly byteOffset: number; - -// /** -// * The length of the array. -// */ -// readonly length: number; - -// /** -// * Sets a value or an array of values. -// * @param index The index of the location to set. -// * @param value The value to set. -// */ -// set(index: number, value: number): void; - -// /** -// * Sets a value or an array of values. -// * @param array A typed or untyped array of values to set. -// * @param offset The index in the current array at which the values are to be written. -// */ -// set(array: ArrayLike, offset?: number): void; - -// /** -// * Converts a number to a string by using the current locale. -// */ -// toLocaleString(): string; - -// /** -// * Returns a string representation of an array. -// */ -// toString(): string; - -// [index: number]: number; -// } - -// interface Uint8ArrayConstructor { -// readonly prototype: Uint8Array; -// new (length: number): Uint8Array; -// new (array: ArrayLike): Uint8Array; -// new (buffer: ArrayBuffer, byteOffset?: number, length?: number): Uint8Array; - -// /** -// * The size in bytes of each element in the array. -// */ -// readonly BYTES_PER_ELEMENT: number; - -// } -// declare const Uint8Array: Uint8ArrayConstructor; - - -// /** -// * A typed array of 16-bit signed integer values. The contents are initialized to 0. If the -// * requested number of bytes could not be allocated an exception is raised. -// */ -// interface Int16Array { -// /** -// * The size in bytes of each element in the array. -// */ -// readonly BYTES_PER_ELEMENT: number; - -// /** -// * The ArrayBuffer instance referenced by the array. -// */ -// readonly buffer: ArrayBuffer; - -// /** -// * The length in bytes of the array. -// */ -// readonly byteLength: number; - -// /** -// * The offset in bytes of the array. -// */ -// readonly byteOffset: number; - -// /** -// * The length of the array. -// */ -// readonly length: number; - -// /** -// * Sets a value or an array of values. -// * @param index The index of the location to set. -// * @param value The value to set. -// */ -// set(index: number, value: number): void; - -// /** -// * Sets a value or an array of values. -// * @param array A typed or untyped array of values to set. -// * @param offset The index in the current array at which the values are to be written. -// */ -// set(array: ArrayLike, offset?: number): void; - -// /** -// * Converts a number to a string by using the current locale. -// */ -// toLocaleString(): string; - -// /** -// * Returns a string representation of an array. -// */ -// toString(): string; - -// [index: number]: number; -// } - -// interface Int16ArrayConstructor { -// readonly prototype: Int16Array; -// new (length: number): Int16Array; -// new (array: ArrayLike): Int16Array; -// new (buffer: ArrayBuffer, byteOffset?: number, length?: number): Int16Array; - -// /** -// * The size in bytes of each element in the array. -// */ -// readonly BYTES_PER_ELEMENT: number; - -// } -// declare const Int16Array: Int16ArrayConstructor; - -// /** -// * A typed array of 16-bit unsigned integer values. The contents are initialized to 0. If the -// * requested number of bytes could not be allocated an exception is raised. -// */ -// interface Uint16Array { -// /** -// * The size in bytes of each element in the array. -// */ -// readonly BYTES_PER_ELEMENT: number; - -// /** -// * The ArrayBuffer instance referenced by the array. -// */ -// readonly buffer: ArrayBuffer; - -// /** -// * The length in bytes of the array. -// */ -// readonly byteLength: number; - -// /** -// * The offset in bytes of the array. -// */ -// readonly byteOffset: number; - -// /** -// * The length of the array. -// */ -// readonly length: number; - -// /** -// * Sets a value or an array of values. -// * @param index The index of the location to set. -// * @param value The value to set. -// */ -// set(index: number, value: number): void; - -// /** -// * Sets a value or an array of values. -// * @param array A typed or untyped array of values to set. -// * @param offset The index in the current array at which the values are to be written. -// */ -// set(array: ArrayLike, offset?: number): void; - -// /** -// * Converts a number to a string by using the current locale. -// */ -// toLocaleString(): string; - -// /** -// * Returns a string representation of an array. -// */ -// toString(): string; - -// [index: number]: number; -// } - -// interface Uint16ArrayConstructor { -// readonly prototype: Uint16Array; -// new (length: number): Uint16Array; -// new (array: ArrayLike): Uint16Array; -// new (buffer: ArrayBuffer, byteOffset?: number, length?: number): Uint16Array; - -// /** -// * The size in bytes of each element in the array. -// */ -// readonly BYTES_PER_ELEMENT: number; - -// } -// declare const Uint16Array: Uint16ArrayConstructor; -// /** -// * A typed array of 32-bit signed integer values. The contents are initialized to 0. If the -// * requested number of bytes could not be allocated an exception is raised. -// */ -// interface Int32Array { -// /** -// * The size in bytes of each element in the array. -// */ -// readonly BYTES_PER_ELEMENT: number; - -// /** -// * The ArrayBuffer instance referenced by the array. -// */ -// readonly buffer: ArrayBuffer; - -// /** -// * The length in bytes of the array. -// */ -// readonly byteLength: number; - -// /** -// * The offset in bytes of the array. -// */ -// readonly byteOffset: number; - -// /** -// * The length of the array. -// */ -// readonly length: number; - -// /** -// * Sets a value or an array of values. -// * @param index The index of the location to set. -// * @param value The value to set. -// */ -// set(index: number, value: number): void; - -// /** -// * Sets a value or an array of values. -// * @param array A typed or untyped array of values to set. -// * @param offset The index in the current array at which the values are to be written. -// */ -// set(array: ArrayLike, offset?: number): void; - -// /** -// * Converts a number to a string by using the current locale. -// */ -// toLocaleString(): string; - -// /** -// * Returns a string representation of an array. -// */ -// toString(): string; - -// [index: number]: number; -// } - -// interface Int32ArrayConstructor { -// readonly prototype: Int32Array; -// new (length: number): Int32Array; -// new (array: ArrayLike): Int32Array; -// new (buffer: ArrayBuffer, byteOffset?: number, length?: number): Int32Array; - -// /** -// * The size in bytes of each element in the array. -// */ -// readonly BYTES_PER_ELEMENT: number; -// } - -// declare const Int32Array: Int32ArrayConstructor; - -// /** -// * A typed array of 32-bit unsigned integer values. The contents are initialized to 0. If the -// * requested number of bytes could not be allocated an exception is raised. -// */ -// interface Uint32Array { -// /** -// * The size in bytes of each element in the array. -// */ -// readonly BYTES_PER_ELEMENT: number; - -// /** -// * The ArrayBuffer instance referenced by the array. -// */ -// readonly buffer: ArrayBuffer; - -// /** -// * The length in bytes of the array. -// */ -// readonly byteLength: number; - -// /** -// * The offset in bytes of the array. -// */ -// readonly byteOffset: number; - -// /** -// * The length of the array. -// */ -// readonly length: number; - -// /** -// * Sets a value or an array of values. -// * @param index The index of the location to set. -// * @param value The value to set. -// */ -// set(index: number, value: number): void; - -// /** -// * Sets a value or an array of values. -// * @param array A typed or untyped array of values to set. -// * @param offset The index in the current array at which the values are to be written. -// */ -// set(array: ArrayLike, offset?: number): void; - -// /** -// * Converts a number to a string by using the current locale. -// */ -// toLocaleString(): string; - -// /** -// * Returns a string representation of an array. -// */ -// toString(): string; - -// [index: number]: number; -// } - -// interface Uint32ArrayConstructor { -// readonly prototype: Uint32Array; -// new (length: number): Uint32Array; -// new (array: ArrayLike): Uint32Array; -// new (buffer: ArrayBuffer, byteOffset?: number, length?: number): Uint32Array; - -// /** -// * The size in bytes of each element in the array. -// */ -// readonly BYTES_PER_ELEMENT: number; -// } - -// declare const Uint32Array: Uint32ArrayConstructor; - -// /** -// * A typed array of 32-bit float values. The contents are initialized to 0. If the requested number -// * of bytes could not be allocated an exception is raised. -// */ -// interface Float32Array { -// /** -// * The size in bytes of each element in the array. -// */ -// readonly BYTES_PER_ELEMENT: number; - -// /** -// * The ArrayBuffer instance referenced by the array. -// */ -// readonly buffer: ArrayBuffer; - -// /** -// * The length in bytes of the array. -// */ -// readonly byteLength: number; - -// /** -// * The offset in bytes of the array. -// */ -// readonly byteOffset: number; - -// /** -// * The length of the array. -// */ -// readonly length: number; - -// /** -// * Sets a value or an array of values. -// * @param index The index of the location to set. -// * @param value The value to set. -// */ -// set(index: number, value: number): void; - -// /** -// * Sets a value or an array of values. -// * @param array A typed or untyped array of values to set. -// * @param offset The index in the current array at which the values are to be written. -// */ -// set(array: ArrayLike, offset?: number): void; - -// /** -// * Converts a number to a string by using the current locale. -// */ -// toLocaleString(): string; - -// /** -// * Returns a string representation of an array. -// */ -// toString(): string; - -// [index: number]: number; -// } - -// interface Float32ArrayConstructor { -// readonly prototype: Float32Array; -// new (length: number): Float32Array; -// new (array: ArrayLike): Float32Array; -// new (buffer: ArrayBuffer, byteOffset?: number, length?: number): Float32Array; - -// /** -// * The size in bytes of each element in the array. -// */ -// readonly BYTES_PER_ELEMENT: number; - -// } -// declare const Float32Array: Float32ArrayConstructor; - -// /** -// * A typed array of 64-bit float values. The contents are initialized to 0. If the requested -// * number of bytes could not be allocated an exception is raised. -// */ -// interface Float64Array { -// /** -// * The size in bytes of each element in the array. -// */ -// readonly BYTES_PER_ELEMENT: number; - -// /** -// * The ArrayBuffer instance referenced by the array. -// */ -// readonly buffer: ArrayBuffer; - -// /** -// * The length in bytes of the array. -// */ -// readonly byteLength: number; - -// /** -// * The offset in bytes of the array. -// */ -// readonly byteOffset: number; - -// /** -// * The length of the array. -// */ -// readonly length: number; - -// /** -// * Sets a value or an array of values. -// * @param index The index of the location to set. -// * @param value The value to set. -// */ -// set(index: number, value: number): void; - -// /** -// * Sets a value or an array of values. -// * @param array A typed or untyped array of values to set. -// * @param offset The index in the current array at which the values are to be written. -// */ -// set(array: ArrayLike, offset?: number): void; - -// /** -// * Converts a number to a string by using the current locale. -// */ -// toLocaleString(): string; - -// /** -// * Returns a string representation of an array. -// */ -// toString(): string; - -// [index: number]: number; -// } - -// interface Float64ArrayConstructor { -// readonly prototype: Float64Array; -// new (length: number): Float64Array; -// new (array: ArrayLike): Float64Array; -// new (buffer: ArrayBuffer, byteOffset?: number, length?: number): Float64Array; - -// /** -// * The size in bytes of each element in the array. -// */ -// readonly BYTES_PER_ELEMENT: number; -// } - -// declare const Float64Array: Float64ArrayConstructor; diff --git a/src/typings/lib.webworker.importscripts.d.ts b/src/typings/lib.webworker.importscripts.d.ts deleted file mode 100644 index e84f717c9a4..00000000000 --- a/src/typings/lib.webworker.importscripts.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -/*! ***************************************************************************** -Copyright (c) Microsoft Corporation. All rights reserved. -Licensed under the Apache License, Version 2.0 (the "License"); you may not use -this file except in compliance with the License. You may obtain a copy of the -License at http://www.apache.org/licenses/LICENSE-2.0 - -THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED -WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, -MERCHANTABLITY OR NON-INFRINGEMENT. - -See the Apache Version 2.0 License for specific language governing permissions -and limitations under the License. -***************************************************************************** */ - - - - -///////////////////////////// -/// WorkerGlobalScope APIs -///////////////////////////// -// These are only available in a Web Worker -declare function importScripts(...urls: string[]): void; diff --git a/src/vs/base/browser/browser.ts b/src/vs/base/browser/browser.ts index 715019d3048..5f811db8d6b 100644 --- a/src/vs/base/browser/browser.ts +++ b/src/vs/base/browser/browser.ts @@ -110,10 +110,7 @@ export const onDidChangeFullscreen = WindowManager.INSTANCE.onDidChangeFullscree const userAgent = navigator.userAgent; -export const isIE = (userAgent.indexOf('Trident') >= 0); export const isEdge = (userAgent.indexOf('Edge/') >= 0); -export const isEdgeOrIE = isIE || isEdge; - export const isOpera = (userAgent.indexOf('Opera') >= 0); export const isFirefox = (userAgent.indexOf('Firefox') >= 0); export const isWebKit = (userAgent.indexOf('AppleWebKit') >= 0); diff --git a/src/vs/base/browser/canIUse.ts b/src/vs/base/browser/canIUse.ts index cf8211e919e..0a425479e59 100644 --- a/src/vs/base/browser/canIUse.ts +++ b/src/vs/base/browser/canIUse.ts @@ -27,10 +27,6 @@ export const BrowserFeatures = { || !!(navigator && navigator.clipboard && navigator.clipboard.readText) ), richText: (() => { - if (browser.isIE) { - return false; - } - if (browser.isEdge) { let index = navigator.userAgent.indexOf('Edge/'); let version = parseInt(navigator.userAgent.substring(index + 5, navigator.userAgent.indexOf('.', index)), 10); diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 69f1d93b2b7..0bcfa43b5d1 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -8,7 +8,6 @@ import { domEvent } from 'vs/base/browser/event'; import { IKeyboardEvent, StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IMouseEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { TimeoutTimer } from 'vs/base/common/async'; -import { CharCode } from 'vs/base/common/charCode'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -49,117 +48,7 @@ interface IDomClassList { toggleClass(node: HTMLElement | SVGElement, className: string, shouldHaveIt?: boolean): void; } -const _manualClassList = new class implements IDomClassList { - - private _lastStart: number = -1; - private _lastEnd: number = -1; - - private _findClassName(node: HTMLElement, className: string): void { - - let classes = node.className; - if (!classes) { - this._lastStart = -1; - return; - } - - className = className.trim(); - - let classesLen = classes.length, - classLen = className.length; - - if (classLen === 0) { - this._lastStart = -1; - return; - } - - if (classesLen < classLen) { - this._lastStart = -1; - return; - } - - if (classes === className) { - this._lastStart = 0; - this._lastEnd = classesLen; - return; - } - - let idx = -1, - idxEnd: number; - - while ((idx = classes.indexOf(className, idx + 1)) >= 0) { - - idxEnd = idx + classLen; - - // a class that is followed by another class - if ((idx === 0 || classes.charCodeAt(idx - 1) === CharCode.Space) && classes.charCodeAt(idxEnd) === CharCode.Space) { - this._lastStart = idx; - this._lastEnd = idxEnd + 1; - return; - } - - // last class - if (idx > 0 && classes.charCodeAt(idx - 1) === CharCode.Space && idxEnd === classesLen) { - this._lastStart = idx - 1; - this._lastEnd = idxEnd; - return; - } - - // equal - duplicate of cmp above - if (idx === 0 && idxEnd === classesLen) { - this._lastStart = 0; - this._lastEnd = idxEnd; - return; - } - } - - this._lastStart = -1; - } - - hasClass(node: HTMLElement, className: string): boolean { - this._findClassName(node, className); - return this._lastStart !== -1; - } - - addClasses(node: HTMLElement, ...classNames: string[]): void { - classNames.forEach(nameValue => nameValue.split(' ').forEach(name => this.addClass(node, name))); - } - - addClass(node: HTMLElement, className: string): void { - if (!node.className) { // doesn't have it for sure - node.className = className; - } else { - this._findClassName(node, className); // see if it's already there - if (this._lastStart === -1) { - node.className = node.className + ' ' + className; - } - } - } - - removeClass(node: HTMLElement, className: string): void { - this._findClassName(node, className); - if (this._lastStart === -1) { - return; // Prevent styles invalidation if not necessary - } else { - node.className = node.className.substring(0, this._lastStart) + node.className.substring(this._lastEnd); - } - } - - removeClasses(node: HTMLElement, ...classNames: string[]): void { - classNames.forEach(nameValue => nameValue.split(' ').forEach(name => this.removeClass(node, name))); - } - - toggleClass(node: HTMLElement, className: string, shouldHaveIt?: boolean): void { - this._findClassName(node, className); - if (this._lastStart !== -1 && (shouldHaveIt === undefined || !shouldHaveIt)) { - this.removeClass(node, className); - } - if (this._lastStart === -1 && (shouldHaveIt === undefined || shouldHaveIt)) { - this.addClass(node, className); - } - } -}; - -const _nativeClassList = new class implements IDomClassList { +const _classList: IDomClassList = new class implements IDomClassList { hasClass(node: HTMLElement, className: string): boolean { return Boolean(className) && node.classList && node.classList.contains(className); } @@ -191,9 +80,6 @@ const _nativeClassList = new class implements IDomClassList { } }; -// In IE11 there is only partial support for `classList` which makes us keep our -// custom implementation. Otherwise use the native implementation, see: http://caniuse.com/#search=classlist -const _classList: IDomClassList = browser.isIE ? _manualClassList : _nativeClassList; export const hasClass: (node: HTMLElement | SVGElement, className: string) => boolean = _classList.hasClass.bind(_classList); export const addClass: (node: HTMLElement | SVGElement, className: string) => void = _classList.addClass.bind(_classList); export const addClasses: (node: HTMLElement | SVGElement, ...classNames: string[]) => void = _classList.addClasses.bind(_classList); diff --git a/src/vs/base/browser/fastDomNode.ts b/src/vs/base/browser/fastDomNode.ts index 28ce15064f9..a5f9c18b2d6 100644 --- a/src/vs/base/browser/fastDomNode.ts +++ b/src/vs/base/browser/fastDomNode.ts @@ -244,11 +244,11 @@ export class FastDomNode { this.domNode.removeAttribute(name); } - public appendChild(child: FastDomNode): void { + public appendChild(child: FastDomNode): void { this.domNode.appendChild(child.domNode); } - public removeChild(child: FastDomNode): void { + public removeChild(child: FastDomNode): void { this.domNode.removeChild(child.domNode); } } diff --git a/src/vs/base/browser/globalMouseMoveMonitor.ts b/src/vs/base/browser/globalMouseMoveMonitor.ts index 8fddd54b7b3..328ecaee03d 100644 --- a/src/vs/base/browser/globalMouseMoveMonitor.ts +++ b/src/vs/base/browser/globalMouseMoveMonitor.ts @@ -5,7 +5,6 @@ import * as dom from 'vs/base/browser/dom'; import * as platform from 'vs/base/common/platform'; -import * as browser from 'vs/base/browser/browser'; import { IframeUtils } from 'vs/base/browser/iframe'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; @@ -103,7 +102,7 @@ export class GlobalMouseMoveMonitor implements I for (const element of listenTo) { this._hooks.add(dom.addDisposableThrottledListener(element, mouseMove, (data: R) => { - if (!browser.isIE && data.buttons !== initialButtons) { + if (data.buttons !== initialButtons) { // Buttons state has changed in the meantime this.stopMonitoring(true); return; diff --git a/src/vs/base/browser/iframe.ts b/src/vs/base/browser/iframe.ts index 7868cafbba2..ade89e96ccc 100644 --- a/src/vs/base/browser/iframe.ts +++ b/src/vs/base/browser/iframe.ts @@ -98,7 +98,7 @@ export class IframeUtils { /** * Returns the position of `childWindow` relative to `ancestorWindow` */ - public static getPositionOfChildWindowRelativeToAncestorWindow(childWindow: Window, ancestorWindow: any) { + public static getPositionOfChildWindowRelativeToAncestorWindow(childWindow: Window, ancestorWindow: Window | null) { if (!ancestorWindow || childWindow === ancestorWindow) { return { diff --git a/src/vs/base/browser/keyboardEvent.ts b/src/vs/base/browser/keyboardEvent.ts index 03bdffc95ed..90a84b5890f 100644 --- a/src/vs/base/browser/keyboardEvent.ts +++ b/src/vs/base/browser/keyboardEvent.ts @@ -145,9 +145,7 @@ let INVERSE_KEY_CODE_MAP: KeyCode[] = new Array(KeyCode.MAX_VALUE); */ define(229, KeyCode.KEY_IN_COMPOSITION); - if (browser.isIE) { - define(91, KeyCode.Meta); - } else if (browser.isFirefox) { + if (browser.isFirefox) { define(59, KeyCode.US_SEMICOLON); define(107, KeyCode.US_EQUAL); define(109, KeyCode.US_MINUS); diff --git a/src/vs/base/browser/touch.ts b/src/vs/base/browser/touch.ts index f8b32d9cb76..9cf55c77875 100644 --- a/src/vs/base/browser/touch.ts +++ b/src/vs/base/browser/touch.ts @@ -131,7 +131,7 @@ export class Gesture extends Disposable { @memoize private static isTouchDevice(): boolean { - return 'ontouchstart' in window as any || navigator.maxTouchPoints > 0 || window.navigator.msMaxTouchPoints > 0; + return 'ontouchstart' in window || navigator.maxTouchPoints > 0 || window.navigator.msMaxTouchPoints > 0; } public dispose(): void { @@ -247,7 +247,7 @@ export class Gesture extends Disposable { } private newGestureEvent(type: string, initialTarget?: EventTarget): GestureEvent { - let event = (document.createEvent('CustomEvent')); + let event = document.createEvent('CustomEvent') as unknown as GestureEvent; event.initEvent(type, false, true); event.initialTarget = initialTarget; event.tapCount = 0; diff --git a/src/vs/base/browser/ui/actionbar/actionbar.ts b/src/vs/base/browser/ui/actionbar/actionbar.ts index 28e9c4b55ed..523a242daaa 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.ts +++ b/src/vs/base/browser/ui/actionbar/actionbar.ts @@ -104,7 +104,7 @@ export class BaseActionViewItem extends Disposable implements IActionViewItem { return this._action.enabled; } - setActionContext(newContext: any): void { + setActionContext(newContext: unknown): void { this._context = newContext; } @@ -248,7 +248,7 @@ export class ActionViewItem extends BaseActionViewItem { private cssClass?: string; - constructor(context: any, action: IAction, options: IActionViewItemOptions = {}) { + constructor(context: unknown, action: IAction, options: IActionViewItemOptions = {}) { super(context, action, options); this.options = options; @@ -423,7 +423,7 @@ export class ActionBar extends Disposable implements IActionRunner { options: IActionBarOptions; private _actionRunner: IActionRunner; - private _context: any; + private _context: unknown; // View Items viewItems: IActionViewItem[]; @@ -821,7 +821,7 @@ export class ActionBar extends Disposable implements IActionRunner { this._onDidCancel.fire(); } - run(action: IAction, context?: any): Promise { + run(action: IAction, context?: unknown): Promise { return this._actionRunner.run(action, context); } @@ -838,7 +838,7 @@ export class ActionBar extends Disposable implements IActionRunner { export class SelectActionViewItem extends BaseActionViewItem { protected selectBox: SelectBox; - constructor(ctx: any, action: IAction, options: ISelectOptionItem[], selected: number, contextViewProvider: IContextViewProvider, selectBoxOptions?: ISelectBoxOptions) { + constructor(ctx: unknown, action: IAction, options: ISelectOptionItem[], selected: number, contextViewProvider: IContextViewProvider, selectBoxOptions?: ISelectBoxOptions) { super(ctx, action); this.selectBox = new SelectBox(options, selected, contextViewProvider, undefined, selectBoxOptions); diff --git a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css index 41efce5c63d..b71ef2dafa5 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?279add2ec8b3d516ca20a123230cbf9f") format("truetype"); + src: url("./codicon.ttf?b5dd8f5aa953889dc1f4c9fa9b44d3dd") format("truetype"); } .codicon[class*='codicon-'] { @@ -415,5 +415,6 @@ .codicon-group-by-ref-type:before { content: "\eb97" } .codicon-ungroup-by-ref-type:before { content: "\eb98" } .codicon-bell-dot:before { content: "\f101" } -.codicon-debug-alt-2:before { content: "\f102" } -.codicon-debug-alt:before { content: "\f103" } +.codicon-bell-progress:before { content: "\f102" } +.codicon-debug-alt-2:before { content: "\f103" } +.codicon-debug-alt:before { content: "\f104" } diff --git a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf index df86f7d4d9a..845205f5b3a 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/browser/ui/dropdown/dropdown.ts b/src/vs/base/browser/ui/dropdown/dropdown.ts index c36015f710b..f54d01606cb 100644 --- a/src/vs/base/browser/ui/dropdown/dropdown.ts +++ b/src/vs/base/browser/ui/dropdown/dropdown.ts @@ -271,7 +271,7 @@ export class DropdownMenu extends BaseDropdown { } export class DropdownMenuActionViewItem extends BaseActionViewItem { - private menuActionsOrProvider: any; + private menuActionsOrProvider: ReadonlyArray | IActionProvider; private dropdownMenu: DropdownMenu | undefined; private contextMenuProvider: IContextMenuProvider; private actionViewItemProvider?: IActionViewItemProvider; @@ -317,7 +317,7 @@ export class DropdownMenuActionViewItem extends BaseActionViewItem { if (Array.isArray(this.menuActionsOrProvider)) { options.actions = this.menuActionsOrProvider; } else { - options.actionProvider = this.menuActionsOrProvider; + options.actionProvider = this.menuActionsOrProvider as IActionProvider; } this.dropdownMenu = this._register(new DropdownMenu(container, options)); @@ -341,7 +341,7 @@ export class DropdownMenuActionViewItem extends BaseActionViewItem { } } - setActionContext(newContext: any): void { + setActionContext(newContext: unknown): void { super.setActionContext(newContext); if (this.dropdownMenu) { diff --git a/src/vs/base/browser/ui/inputbox/inputBox.ts b/src/vs/base/browser/ui/inputbox/inputBox.ts index cd57d7f772f..96d5013909b 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.ts +++ b/src/vs/base/browser/ui/inputbox/inputBox.ts @@ -6,7 +6,6 @@ import 'vs/css!./inputBox'; import * as nls from 'vs/nls'; -import * as Bal from 'vs/base/browser/browser'; import * as dom from 'vs/base/browser/dom'; import { MarkdownRenderOptions } from 'vs/base/browser/markdownRenderer'; import { renderFormattedText, renderText } from 'vs/base/browser/formattedTextRenderer'; @@ -212,14 +211,6 @@ export class InputBox extends Widget { this.onblur(this.input, () => this.onBlur()); this.onfocus(this.input, () => this.onFocus()); - // Add placeholder shim for IE because IE decides to hide the placeholder on focus (we dont want that!) - if (this.placeholder && Bal.isIE) { - this.onclick(this.input, (e) => { - dom.EventHelper.stop(e, true); - this.input.focus(); - }); - } - this.ignoreGesture(this.input); setTimeout(() => this.updateMirror(), 0); diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index 7b520501582..a07235210e3 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -21,6 +21,7 @@ import { equals, distinct } from 'vs/base/common/arrays'; import { DataTransfers, StaticDND, IDragAndDropData } from 'vs/base/browser/dnd'; import { disposableTimeout, Delayer } from 'vs/base/common/async'; import { isFirefox } from 'vs/base/browser/browser'; +import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; interface IItem { readonly id: string; @@ -198,6 +199,7 @@ export class ListView implements ISpliceable, IDisposable { get onDidScroll(): Event { return this.scrollableElement.onScroll; } get onWillScroll(): Event { return this.scrollableElement.onWillScroll; } + get containerDomNode(): HTMLElement { return this.rowsContainer; } constructor( container: HTMLElement, @@ -273,6 +275,31 @@ export class ListView implements ISpliceable, IDisposable { this.layout(); } + triggerScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent) { + this.scrollableElement.triggerScrollFromMouseWheelEvent(browserEvent); + } + + updateElementHeight(index: number, size: number): void { + if (this.items[index].size === size) { + return; + } + + const lastRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight); + + const heightDiff = index < lastRenderRange.start ? size - this.items[index].size : 0; + this.rangeMap.splice(index, 1, [{ size: size }]); + this.items[index].size = size; + + this.render(lastRenderRange, this.lastRenderTop + heightDiff, this.lastRenderHeight, undefined, undefined, true); + + this.eventuallyUpdateScrollDimensions(); + + if (this.supportDynamicHeights) { + this._rerender(this.lastRenderTop, this.lastRenderHeight); + } + return; + } + splice(start: number, deleteCount: number, elements: T[] = []): T[] { if (this.splicing) { throw new Error('Can\'t run recursive splices.'); @@ -516,14 +543,21 @@ export class ListView implements ISpliceable, IDisposable { // Render - private render(renderTop: number, renderHeight: number, renderLeft: number, scrollWidth: number): void { - const previousRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight); + private render(previousRenderRange: IRange, renderTop: number, renderHeight: number, renderLeft: number | undefined, scrollWidth: number | undefined, updateItemsInDOM: boolean = false): void { const renderRange = this.getRenderRange(renderTop, renderHeight); const rangesToInsert = Range.relativeComplement(renderRange, previousRenderRange); const rangesToRemove = Range.relativeComplement(previousRenderRange, renderRange); const beforeElement = this.getNextToLastElement(rangesToInsert); + if (updateItemsInDOM) { + const rangesToUpdate = Range.intersect(previousRenderRange, renderRange); + + for (let i = rangesToUpdate.start; i < rangesToUpdate.end; i++) { + this.updateItemInDOM(this.items[i], i); + } + } + for (const range of rangesToInsert) { for (let i = range.start; i < range.end; i++) { this.insertItemInDOM(i, beforeElement); @@ -536,10 +570,13 @@ export class ListView implements ISpliceable, IDisposable { } } - this.rowsContainer.style.left = `-${renderLeft}px`; + if (renderLeft !== undefined) { + this.rowsContainer.style.left = `-${renderLeft}px`; + } + this.rowsContainer.style.top = `-${renderTop}px`; - if (this.horizontalScrolling) { + if (this.horizontalScrolling && scrollWidth !== undefined) { this.rowsContainer.style.width = `${Math.max(scrollWidth, this.renderWidth)}px`; } @@ -554,7 +591,7 @@ export class ListView implements ISpliceable, IDisposable { if (!item.row) { item.row = this.cache.alloc(item.templateId); - const role = this.ariaProvider.getRole ? this.ariaProvider.getRole(item.element) : 'treeitem'; + const role = this.ariaProvider.getRole ? this.ariaProvider.getRole(item.element) : 'listitem'; item.row!.domNode!.setAttribute('role', role); const checked = this.ariaProvider.isChecked ? this.ariaProvider.isChecked(item.element) : undefined; if (typeof checked !== 'undefined') { @@ -741,7 +778,8 @@ export class ListView implements ISpliceable, IDisposable { private onScroll(e: ScrollEvent): void { try { - this.render(e.scrollTop, e.height, e.scrollLeft, e.scrollWidth); + const previousRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight); + this.render(previousRenderRange, e.scrollTop, e.height, e.scrollLeft, e.scrollWidth); if (this.supportDynamicHeights) { this._rerender(e.scrollTop, e.height); @@ -1097,6 +1135,14 @@ export class ListView implements ISpliceable, IDisposable { } const size = item.size; + + if (item.row && item.row.domNode) { + let newSize = item.row.domNode.offsetHeight; + item.size = newSize; + item.lastDynamicHeightWidth = this.renderWidth; + return newSize - size; + } + const row = this.cache.alloc(item.templateId); row.domNode!.style.height = ''; diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index aa3febf7c85..b37c69e125a 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -1115,7 +1115,7 @@ export class List implements ISpliceable, IDisposable { private focus: Trait; private selection: Trait; private eventBufferer = new EventBufferer(); - private view: ListView; + protected view: ListView; private spliceable: ISpliceable; private styleController: IStyleController; private typeLabelController?: TypeLabelController; @@ -1310,6 +1310,10 @@ export class List implements ISpliceable, IDisposable { this.view.updateWidth(index); } + updateElementHeight(index: number, size: number): void { + this.view.updateElementHeight(index, size); + } + rerender(): void { this.view.rerender(); } diff --git a/src/vs/base/browser/ui/menu/menu.ts b/src/vs/base/browser/ui/menu/menu.ts index 0adbfe23b90..3c85a375813 100644 --- a/src/vs/base/browser/ui/menu/menu.ts +++ b/src/vs/base/browser/ui/menu/menu.ts @@ -6,7 +6,7 @@ import 'vs/css!./menu'; import * as nls from 'vs/nls'; import * as strings from 'vs/base/common/strings'; -import { IActionRunner, IAction, Action, IActionViewItem } from 'vs/base/common/actions'; +import { IActionRunner, IAction, Action } from 'vs/base/common/actions'; import { ActionBar, IActionViewItemProvider, ActionsOrientation, Separator, ActionViewItem, IActionViewItemOptions, BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { ResolvedKeybinding, KeyCode } from 'vs/base/common/keyCodes'; import { addClass, EventType, EventHelper, EventLike, removeTabIndexAndUpdateFocus, isAncestor, hasClass, addDisposableListener, removeClass, append, $, addClasses, removeClasses, clearNode } from 'vs/base/browser/dom'; @@ -205,7 +205,7 @@ export class Menu extends ActionBar { container.appendChild(this.scrollableElement.getDomNode()); this.scrollableElement.scanDomNode(); - this.viewItems.filter(item => !(item instanceof MenuSeparatorActionViewItem)).forEach((item: IActionViewItem, index: number, array: any[]) => { + this.viewItems.filter(item => !(item instanceof MenuSeparatorActionViewItem)).forEach((item, index, array) => { (item as BaseMenuActionViewItem).updatePositionInSet(index + 1, array.length); }); } @@ -363,7 +363,7 @@ class BaseMenuActionViewItem extends BaseActionViewItem { private cssClass: string; protected menuStyle: IMenuStyles | undefined; - constructor(ctx: any, action: IAction, options: IMenuItemOptions = {}) { + constructor(ctx: unknown, action: IAction, options: IMenuItemOptions = {}) { options.isMenu = true; super(action, action, options); diff --git a/src/vs/base/browser/ui/scrollbar/scrollableElement.ts b/src/vs/base/browser/ui/scrollbar/scrollableElement.ts index 4c34a8bafcd..dcbccfbee64 100644 --- a/src/vs/base/browser/ui/scrollbar/scrollableElement.ts +++ b/src/vs/base/browser/ui/scrollbar/scrollableElement.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/scrollbars'; -import { isEdgeOrIE } from 'vs/base/browser/browser'; +import { isEdge } from 'vs/base/browser/browser'; import * as dom from 'vs/base/browser/dom'; import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; import { IMouseEvent, StandardWheelEvent, IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; @@ -303,6 +303,10 @@ export abstract class AbstractScrollableElement extends Widget { this._revealOnScroll = value; } + public triggerScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent) { + this._onMouseWheel(new StandardWheelEvent(browserEvent)); + } + // -------------------- mouse wheel scrolling -------------------- private _setListeningToMouseWheel(shouldListen: boolean): void { @@ -322,7 +326,7 @@ export abstract class AbstractScrollableElement extends Widget { this._onMouseWheel(new StandardWheelEvent(browserEvent)); }; - this._mouseWheelToDispose.push(dom.addDisposableListener(this._listenOnDomNode, isEdgeOrIE ? 'mousewheel' : 'wheel', onMouseWheel, { passive: false })); + this._mouseWheelToDispose.push(dom.addDisposableListener(this._listenOnDomNode, isEdge ? 'mousewheel' : 'wheel', onMouseWheel, { passive: false })); } } diff --git a/src/vs/base/browser/ui/toolbar/toolbar.ts b/src/vs/base/browser/ui/toolbar/toolbar.ts index 846e156dfa3..8a7a9705228 100644 --- a/src/vs/base/browser/ui/toolbar/toolbar.ts +++ b/src/vs/base/browser/ui/toolbar/toolbar.ts @@ -86,7 +86,7 @@ export class ToolBar extends Disposable { return this.actionBar.actionRunner; } - set context(context: any) { + set context(context: unknown) { this.actionBar.context = context; if (this.toggleMenuActionViewItem.value) { this.toggleMenuActionViewItem.value.setActionContext(context); @@ -166,10 +166,8 @@ class ToggleMenuAction extends Action { this.toggleDropdownMenu = toggleDropdownMenu; } - run(): Promise { + async run(): Promise { this.toggleDropdownMenu(); - - return Promise.resolve(true); } get menuActions(): ReadonlyArray { diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index 9e584efcc56..ee6dd9e3a6b 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -14,7 +14,7 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { ITreeModel, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent, ITreeFilter, ITreeNavigator, ICollapseStateChangeEvent, ITreeDragAndDrop, TreeDragOverBubble, TreeVisibility, TreeFilterResult, ITreeModelSpliceEvent, TreeMouseEventTarget } from 'vs/base/browser/ui/tree/tree'; import { ISpliceable } from 'vs/base/common/sequence'; import { IDragAndDropData, StaticDND, DragAndDropData } from 'vs/base/browser/dnd'; -import { range, equals, distinctES6, fromSet } from 'vs/base/common/arrays'; +import { range, equals, distinctES6 } from 'vs/base/common/arrays'; import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; import { domEvent } from 'vs/base/browser/event'; import { fuzzyScore, FuzzyScore } from 'vs/base/common/filters'; @@ -196,7 +196,7 @@ function asListOptions(modelProvider: () => ITreeModel { return options.ariaProvider!.getRole!(node.element); - } : undefined + } : () => 'treeitem' } }; } @@ -1320,7 +1320,7 @@ export abstract class AbstractTree implements IDisposable set.add(node); } - return fromSet(set); + return values(set); }).event; if (_options.keyboardSupport !== false) { diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index 6ddb3fe5cb8..0cd13de8079 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -267,7 +267,7 @@ function asObjectTreeOptions(options?: IAsyncDataTreeOpt }, getRole: options.ariaProvider!.getRole ? (el) => { return options.ariaProvider!.getRole!(el.element as T); - } : undefined, + } : () => 'treeitem', isChecked: options.ariaProvider!.isChecked ? (e) => { return options.ariaProvider?.isChecked!(e.element as T); } : undefined diff --git a/src/vs/base/common/arrays.ts b/src/vs/base/common/arrays.ts index 9a9b53a100c..afb3ae9f697 100644 --- a/src/vs/base/common/arrays.ts +++ b/src/vs/base/common/arrays.ts @@ -372,12 +372,6 @@ export function distinctES6(array: ReadonlyArray): T[] { }); } -export function fromSet(set: Set): T[] { - const result: T[] = []; - set.forEach(o => result.push(o)); - return result; -} - export function uniqueFilter(keyFn: (t: T) => string): (t: T) => boolean { const seen: { [key: string]: boolean; } = Object.create(null); @@ -405,6 +399,9 @@ export function lastIndex(array: ReadonlyArray, fn: (item: T) => boolean): return -1; } +/** + * @deprecated ES6: use `Array.findIndex` + */ export function firstIndex(array: ReadonlyArray, fn: (item: T) => boolean): number { for (let i = 0; i < array.length; i++) { const element = array[i]; @@ -417,6 +414,10 @@ export function firstIndex(array: ReadonlyArray, fn: (item: T) => boolean) return -1; } + +/** + * @deprecated ES6: use `Array.find` + */ export function first(array: ReadonlyArray, fn: (item: T) => boolean, notFoundValue: T): T; export function first(array: ReadonlyArray, fn: (item: T) => boolean): T | undefined; export function first(array: ReadonlyArray, fn: (item: T) => boolean, notFoundValue: T | undefined = undefined): T | undefined { @@ -471,6 +472,9 @@ export function range(arg: number, to?: number): number[] { return result; } +/** + * @deprecated ES6: use `Array.fill` + */ export function fill(num: number, value: T, arr: T[] = []): T[] { for (let i = 0; i < num; i++) { arr[i] = value; @@ -564,6 +568,10 @@ export function pushToEnd(arr: T[], value: T): void { } } + +/** + * @deprecated ES6: use `Array.find` + */ export function find(arr: ArrayLike, predicate: (value: T, index: number, arr: ArrayLike) => any): T | undefined { for (let i = 0; i < arr.length; i++) { const element = arr[i]; diff --git a/src/vs/base/common/assert.ts b/src/vs/base/common/assert.ts index 1e227df640e..9e2510b4d0b 100644 --- a/src/vs/base/common/assert.ts +++ b/src/vs/base/common/assert.ts @@ -6,8 +6,8 @@ /** * Throws an error with the provided message if the provided value does not evaluate to a true Javascript value. */ -export function ok(value?: any, message?: string) { +export function ok(value?: unknown, message?: string) { if (!value) { - throw new Error(message ? 'Assertion failed (' + message + ')' : 'Assertion Failed'); + throw new Error(message ? `Assertion failed (${message})` : 'Assertion Failed'); } } diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index e5ff745c876..9951ca26b7e 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -837,7 +837,7 @@ export class TaskSequentializer { this._pending?.cancel(); } - setPending(taskId: number, promise: Promise, onCancel?: () => void, ): Promise { + setPending(taskId: number, promise: Promise, onCancel?: () => void,): Promise { this._pending = { taskId: taskId, cancel: () => onCancel?.(), promise }; promise.then(() => this.donePending(taskId), () => this.donePending(taskId)); diff --git a/src/vs/base/common/cancellation.ts b/src/vs/base/common/cancellation.ts index 75b615de669..cb8c8c6da2d 100644 --- a/src/vs/base/common/cancellation.ts +++ b/src/vs/base/common/cancellation.ts @@ -31,7 +31,7 @@ const shortcutEvent: Event = Object.freeze(function (callback, context?): I export namespace CancellationToken { - export function isCancellationToken(thing: any): thing is CancellationToken { + export function isCancellationToken(thing: unknown): thing is CancellationToken { if (thing === CancellationToken.None || thing === CancellationToken.Cancelled) { return true; } diff --git a/src/vs/base/common/collections.ts b/src/vs/base/common/collections.ts index f185d439651..e2de5aadc57 100644 --- a/src/vs/base/common/collections.ts +++ b/src/vs/base/common/collections.ts @@ -95,11 +95,6 @@ export function fromMap(original: Map): IStringDictionary { return result; } -export function mapValues(map: Map): V[] { - const result: V[] = []; - map.forEach(v => result.push(v)); - return result; -} export class SetMap { diff --git a/src/vs/base/common/errorsWithActions.ts b/src/vs/base/common/errorsWithActions.ts index 133febb8453..fa92b7f4526 100644 --- a/src/vs/base/common/errorsWithActions.ts +++ b/src/vs/base/common/errorsWithActions.ts @@ -13,7 +13,7 @@ export interface IErrorWithActions { actions?: ReadonlyArray; } -export function isErrorWithActions(obj: any): obj is IErrorWithActions { +export function isErrorWithActions(obj: unknown): obj is IErrorWithActions { return obj instanceof Error && Array.isArray((obj as IErrorWithActions).actions); } diff --git a/src/vs/base/common/functional.ts b/src/vs/base/common/functional.ts index 4587a5b7542..b437cc98c46 100644 --- a/src/vs/base/common/functional.ts +++ b/src/vs/base/common/functional.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -export function once(this: any, fn: T): T { +export function once(this: unknown, fn: T): T { const _this = this; let didCall = false; - let result: any; + let result: unknown; return function () { if (didCall) { @@ -17,5 +17,5 @@ export function once(this: any, fn: T): T { result = fn.apply(_this, arguments); return result; - } as any as T; -} \ No newline at end of file + } as unknown as T; +} diff --git a/src/vs/base/common/iterator.ts b/src/vs/base/common/iterator.ts index 6c87c85cf48..2f4e7d38561 100644 --- a/src/vs/base/common/iterator.ts +++ b/src/vs/base/common/iterator.ts @@ -34,6 +34,24 @@ export interface NativeIterator { next(): NativeIteratorResult; } +export namespace Iterable { + + export function some(iterable: IterableIterator, predicate: (t: T) => boolean): boolean { + for (const element of iterable) { + if (predicate(element)) { + return true; + } + } + return false; + } + + export function* map(iterable: IterableIterator, fn: (t: T) => R): IterableIterator { + for (const element of iterable) { + return yield fn(element); + } + } +} + export module Iterator { const _empty: Iterator = { next() { diff --git a/src/vs/base/common/lazy.ts b/src/vs/base/common/lazy.ts index 7f1bef48d65..ce6339106ef 100644 --- a/src/vs/base/common/lazy.ts +++ b/src/vs/base/common/lazy.ts @@ -21,7 +21,7 @@ export class Lazy { private _didRun: boolean = false; private _value?: T; - private _error: any; + private _error: Error | undefined; constructor( private readonly executor: () => T, diff --git a/src/vs/base/common/lifecycle.ts b/src/vs/base/common/lifecycle.ts index a99fc45bd50..7394eab4ce0 100644 --- a/src/vs/base/common/lifecycle.ts +++ b/src/vs/base/common/lifecycle.ts @@ -49,8 +49,7 @@ export interface IDisposable { } export function isDisposable(thing: E): thing is E & IDisposable { - return typeof (thing).dispose === 'function' - && (thing).dispose.length === 0; + return typeof (thing).dispose === 'function' && (thing).dispose.length === 0; } export function dispose(disposable: T): T; @@ -124,7 +123,7 @@ export class DisposableStore implements IDisposable { if (!t) { return t; } - if ((t as any as DisposableStore) === this) { + if ((t as unknown as DisposableStore) === this) { throw new Error('Cannot register a disposable on itself!'); } @@ -158,7 +157,7 @@ export abstract class Disposable implements IDisposable { } protected _register(t: T): T { - if ((t as any as Disposable) === this) { + if ((t as unknown as Disposable) === this) { throw new Error('Cannot register a disposable on itself!'); } return this._store.add(t); diff --git a/src/vs/base/common/linkedText.ts b/src/vs/base/common/linkedText.ts index ee268d9fdcd..5f6c6a05dcf 100644 --- a/src/vs/base/common/linkedText.ts +++ b/src/vs/base/common/linkedText.ts @@ -23,7 +23,7 @@ export class LinkedText { } } -const LINK_REGEX = /\[([^\]]+)\]\(((?:https?:\/\/|command:)[^\)\s]+)(?: "([^"]+)")?\)/gi; +const LINK_REGEX = /\[([^\]]+)\]\(((?:https?:\/\/|command:)[^\)\s]+)(?: ("|')([^\3]+)(\3))?\)/gi; export function parseLinkedText(text: string): LinkedText { const result: LinkedTextNode[] = []; @@ -36,7 +36,7 @@ export function parseLinkedText(text: string): LinkedText { result.push(text.substring(index, match.index)); } - const [, label, href, title] = match; + const [, label, href, , title] = match; if (title) { result.push({ label, href, title }); diff --git a/src/vs/base/common/map.ts b/src/vs/base/common/map.ts index 4f6b55c3fe5..4d07f50b6b8 100644 --- a/src/vs/base/common/map.ts +++ b/src/vs/base/common/map.ts @@ -7,7 +7,9 @@ import { URI } from 'vs/base/common/uri'; import { CharCode } from 'vs/base/common/charCode'; import { Iterator, IteratorResult, FIN } from './iterator'; - +/** + * @deprecated ES6: use `[...SetOrMap.values()]` + */ export function values(set: Set): V[]; export function values(map: Map): V[]; export function values(forEachable: { forEach(callback: (value: V, ...more: any[]) => any): void }): V[] { @@ -16,6 +18,9 @@ export function values(forEachable: { forEach(callback: (value: V, ...more: a return result; } +/** + * @deprecated ES6: use `[...map.keys()]` + */ export function keys(map: Map): K[] { const result: K[] = []; map.forEach((_value, key) => result.push(key)); @@ -51,6 +56,9 @@ export function setToString(set: Set): string { return `Set(${set.size}) {${entries.join(', ')}}`; } +/** + * @deprecated ES6: use `...Map.entries()` + */ export function mapToSerializable(map: Map): [string, string][] { const serializable: [string, string][] = []; @@ -61,6 +69,9 @@ export function mapToSerializable(map: Map): [string, string][] return serializable; } +/** + * @deprecated ES6: use `new Map([[key1, value1],[key2, value2]])` + */ export function serializableToMap(serializable: [string, string][]): Map { const items = new Map(); diff --git a/src/vs/base/common/normalization.ts b/src/vs/base/common/normalization.ts index b6304df31a0..9438f72b5ba 100644 --- a/src/vs/base/common/normalization.ts +++ b/src/vs/base/common/normalization.ts @@ -11,7 +11,7 @@ import { LRUCache } from 'vs/base/common/map'; * * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/normalize} */ -export const canNormalize = typeof (('').normalize) === 'function'; +export const canNormalize = typeof (String.prototype as any /* standalone editor compilation */).normalize === 'function'; const nfcCache = new LRUCache(10000); // bounded to 10000 elements export function normalizeNFC(str: string): string { diff --git a/src/vs/base/common/resourceTree.ts b/src/vs/base/common/resourceTree.ts index 4adca4ca4f2..2de60729275 100644 --- a/src/vs/base/common/resourceTree.ts +++ b/src/vs/base/common/resourceTree.ts @@ -8,8 +8,7 @@ import * as paths from 'vs/base/common/path'; import { Iterator } from 'vs/base/common/iterator'; import { relativePath, joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; -import { mapValues } from 'vs/base/common/collections'; -import { PathIterator } from 'vs/base/common/map'; +import { PathIterator, values } from 'vs/base/common/map'; export interface IResourceNode { readonly uri: URI; @@ -32,7 +31,7 @@ class Node implements IResourceNode { } get children(): Iterator> { - return Iterator.fromArray(mapValues(this._children)); + return Iterator.fromArray(values(this._children)); } @memoize diff --git a/src/vs/base/common/stream.ts b/src/vs/base/common/stream.ts index 1172496a897..8234770bd19 100644 --- a/src/vs/base/common/stream.ts +++ b/src/vs/base/common/stream.ts @@ -95,8 +95,8 @@ export interface WriteableStream extends ReadableStream { end(result?: T | Error): void; } -export function isReadableStream(obj: any): obj is ReadableStream { - const candidate: ReadableStream = obj; +export function isReadableStream(obj: unknown): obj is ReadableStream { + const candidate = obj as ReadableStream; return candidate && [candidate.on, candidate.pause, candidate.resume, candidate.destroy].every(fn => typeof fn === 'function'); } diff --git a/src/vs/base/common/strings.ts b/src/vs/base/common/strings.ts index 8d67e493772..51336f3eb99 100644 --- a/src/vs/base/common/strings.ts +++ b/src/vs/base/common/strings.ts @@ -5,6 +5,7 @@ import { CharCode } from 'vs/base/common/charCode'; import { Constants } from 'vs/base/common/uint'; +import { canNormalize, normalizeNFD } from 'vs/base/common/normalization'; export function isFalsyOrWhitespace(str: string | undefined): boolean { if (!str || typeof str !== 'string') { @@ -14,7 +15,7 @@ export function isFalsyOrWhitespace(str: string | undefined): boolean { } /** - * @returns the provided number with the given number of preceding zeros. + * @deprecated ES6: use `String.padStart` */ export function pad(n: number, l: number, char: string = '0'): string { const str = '' + n; @@ -145,7 +146,7 @@ export function stripWildcards(pattern: string): string { } /** - * Determines if haystack starts with needle. + * @deprecated ES6: use `String.startsWith` */ export function startsWith(haystack: string, needle: string): boolean { if (haystack.length < needle.length) { @@ -166,7 +167,7 @@ export function startsWith(haystack: string, needle: string): boolean { } /** - * Determines if haystack ends with needle. + * @deprecated ES6: use `String.endsWith` */ export function endsWith(haystack: string, needle: string): boolean { const diff = haystack.length - needle.length; @@ -240,7 +241,7 @@ export function regExpFlags(regexp: RegExp): string { return (regexp.global ? 'g' : '') + (regexp.ignoreCase ? 'i' : '') + (regexp.multiline ? 'm' : '') - + ((regexp as any).unicode ? 'u' : ''); + + ((regexp as any /* standalone editor compilation */).unicode ? 'u' : ''); } /** @@ -853,15 +854,15 @@ export function removeAnsiEscapeCodes(str: string): string { } export const removeAccents: (str: string) => string = (function () { - if (typeof (String.prototype as any).normalize !== 'function') { - // ☹️ no ES6 features... + if (!canNormalize) { + // no ES6 features... return function (str: string) { return str; }; } else { // transform into NFD form and remove accents // see: https://stackoverflow.com/questions/990904/remove-accents-diacritics-in-a-string-in-javascript/37511463#37511463 const regex = /[\u0300-\u036f]/g; return function (str: string) { - return (str as any).normalize('NFD').replace(regex, ''); + return normalizeNFD(str).replace(regex, ''); }; } })(); diff --git a/src/vs/base/node/crypto.ts b/src/vs/base/node/crypto.ts index f18503ab882..7323a92770a 100644 --- a/src/vs/base/node/crypto.ts +++ b/src/vs/base/node/crypto.ts @@ -5,19 +5,17 @@ import * as fs from 'fs'; import * as crypto from 'crypto'; -import * as stream from 'stream'; import { once } from 'vs/base/common/functional'; export function checksum(path: string, sha1hash: string | undefined): Promise { const promise = new Promise((c, e) => { const input = fs.createReadStream(path); const hash = crypto.createHash('sha1'); - const hashStream = hash as any as stream.PassThrough; - input.pipe(hashStream); + input.pipe(hash); const done = once((err?: Error, result?: string) => { input.removeAllListeners(); - hashStream.removeAllListeners(); + hash.removeAllListeners(); if (err) { e(err); @@ -28,8 +26,8 @@ export function checksum(path: string, sha1hash: string | undefined): Promise done(undefined, data.toString('hex'))); + hash.once('error', done); + hash.once('data', (data: Buffer) => done(undefined, data.toString('hex'))); }); return promise.then(hash => { diff --git a/src/vs/base/parts/composite/browser/compositeDnd.ts b/src/vs/base/parts/composite/browser/compositeDnd.ts index ca8b52d491a..6091c8a10d3 100644 --- a/src/vs/base/parts/composite/browser/compositeDnd.ts +++ b/src/vs/base/parts/composite/browser/compositeDnd.ts @@ -21,4 +21,5 @@ export class CompositeDragAndDropData implements IDragAndDropData { export interface ICompositeDragAndDrop { drop(data: IDragAndDropData, target: string | undefined, originalEvent: DragEvent): void; onDragOver(data: IDragAndDropData, target: string | undefined, originalEvent: DragEvent): boolean; + onDragEnter(data: IDragAndDropData, target: string | undefined, originalEvent: DragEvent): boolean; } diff --git a/src/vs/base/parts/quickinput/browser/quickInput.ts b/src/vs/base/parts/quickinput/browser/quickInput.ts index afb94d70395..d3d94acb6ad 100644 --- a/src/vs/base/parts/quickinput/browser/quickInput.ts +++ b/src/vs/base/parts/quickinput/browser/quickInput.ts @@ -397,7 +397,7 @@ class QuickPick extends QuickInput implements IQuickPi private _valueSelection: Readonly<[number, number]> | undefined; private valueSelectionUpdated = true; private _validationMessage: string | undefined; - private _ok = false; + private _ok: boolean | 'default' = 'default'; private _customButton = false; private _customButtonLabel: string | undefined; private _customButtonHover: string | undefined; @@ -566,7 +566,7 @@ class QuickPick extends QuickInput implements IQuickPi return this._ok; } - set ok(showOkButton: boolean) { + set ok(showOkButton: boolean | 'default') { this._ok = showOkButton; this.update(); } @@ -575,6 +575,10 @@ class QuickPick extends QuickInput implements IQuickPi return this.visible ? this.ui.inputBox.hasFocus() : false; } + public focusOnInput() { + this.ui.inputBox.setFocus(); + } + onDidChangeSelection = this.onDidChangeSelectionEmitter.event; onDidTriggerItemButton = this.onDidTriggerItemButtonEmitter.event; @@ -753,7 +757,8 @@ class QuickPick extends QuickInput implements IQuickPi if (!this.visible) { return; } - this.ui.setVisibilities(this.canSelectMany ? { title: !!this.title || !!this.step, description: !!this.description, checkAll: true, inputBox: true, visibleCount: true, count: true, ok: this.ok, list: true, message: !!this.validationMessage, customButton: this.customButton } : { title: !!this.title || !!this.step, description: !!this.description, inputBox: true, visibleCount: true, list: true, message: !!this.validationMessage, customButton: this.customButton, ok: this.ok }); + const ok = this.ok === 'default' ? this.canSelectMany : this.ok; + this.ui.setVisibilities(this.canSelectMany ? { title: !!this.title || !!this.step, description: !!this.description, checkAll: true, inputBox: true, visibleCount: true, count: true, ok, list: true, message: !!this.validationMessage, customButton: this.customButton } : { title: !!this.title || !!this.step, description: !!this.description, inputBox: true, visibleCount: true, list: true, message: !!this.validationMessage, customButton: this.customButton, ok }); super.update(); if (this.ui.inputBox.value !== this.value) { this.ui.inputBox.value = this.value; diff --git a/src/vs/base/parts/quickinput/common/quickInput.ts b/src/vs/base/parts/quickinput/common/quickInput.ts index 8cbf10553e7..10c8255a444 100644 --- a/src/vs/base/parts/quickinput/common/quickInput.ts +++ b/src/vs/base/parts/quickinput/common/quickInput.ts @@ -113,7 +113,7 @@ export interface IInputOptions { placeHolder?: string; /** - * set to true to show a password prompt that will not show the typed value + * Controls if a password input is shown. Password input hides the typed text. */ password?: boolean; @@ -162,7 +162,7 @@ export interface IQuickPick extends IQuickInput { readonly onDidAccept: Event; - ok: boolean; + ok: boolean | 'default'; readonly onDidCustom: Event; @@ -209,6 +209,8 @@ export interface IQuickPick extends IQuickInput { validationMessage: string | undefined; inputHasFocus(): boolean; + + focusOnInput(): void; } export interface IInputBox extends IQuickInput { diff --git a/src/vs/base/parts/storage/common/storage.ts b/src/vs/base/parts/storage/common/storage.ts index 03dedeca57f..2c2c909a9d1 100644 --- a/src/vs/base/parts/storage/common/storage.ts +++ b/src/vs/base/parts/storage/common/storage.ts @@ -27,7 +27,8 @@ export interface IUpdateRequest { } export interface IStorageItemsChangeEvent { - items: Map; + changed?: Map; + deleted?: Set; } export interface IStorageDatabase { @@ -104,10 +105,11 @@ export class Storage extends Disposable implements IStorage { // items that change external require us to update our // caches with the values. we just accept the value and // emit an event if there is a change. - e.items.forEach((value, key) => this.accept(key, value)); + e.changed?.forEach((value, key) => this.accept(key, value)); + e.deleted?.forEach(key => this.accept(key, undefined)); } - private accept(key: string, value: string): void { + private accept(key: string, value: string | undefined): void { if (this.state === StorageState.Closed) { return; // Return early if we are already closed } @@ -315,4 +317,4 @@ export class InMemoryStorageDatabase implements IStorageDatabase { close(): Promise { return Promise.resolve(); } -} \ No newline at end of file +} diff --git a/src/vs/base/parts/storage/test/node/storage.test.ts b/src/vs/base/parts/storage/test/node/storage.test.ts index 9c966c0868b..5c25b92dffc 100644 --- a/src/vs/base/parts/storage/test/node/storage.test.ts +++ b/src/vs/base/parts/storage/test/node/storage.test.ts @@ -124,28 +124,27 @@ suite('Storage Library', () => { changes.clear(); // Nothing happens if changing to same value - const change = new Map(); - change.set('foo', 'bar'); - database.fireDidChangeItemsExternal({ items: change }); + const changed = new Map(); + changed.set('foo', 'bar'); + database.fireDidChangeItemsExternal({ changed }); equal(changes.size, 0); // Change is accepted if valid - change.set('foo', 'bar1'); - database.fireDidChangeItemsExternal({ items: change }); + changed.set('foo', 'bar1'); + database.fireDidChangeItemsExternal({ changed }); ok(changes.has('foo')); equal(storage.get('foo'), 'bar1'); changes.clear(); // Delete is accepted - change.set('foo', undefined!); - database.fireDidChangeItemsExternal({ items: change }); + const deleted = new Set(['foo']); + database.fireDidChangeItemsExternal({ deleted }); ok(changes.has('foo')); - equal(storage.get('foo', null!), null); + equal(storage.get('foo', undefined), undefined); changes.clear(); // Nothing happens if changing to same value - change.set('foo', undefined!); - database.fireDidChangeItemsExternal({ items: change }); + database.fireDidChangeItemsExternal({ deleted }); equal(changes.size, 0); await storage.close(); diff --git a/src/vs/base/parts/tree/browser/treeView.ts b/src/vs/base/parts/tree/browser/treeView.ts index 51d69f93036..790aeb52339 100644 --- a/src/vs/base/parts/tree/browser/treeView.ts +++ b/src/vs/base/parts/tree/browser/treeView.ts @@ -375,11 +375,6 @@ class RootViewItem extends ViewItem { } } -interface IThrottledGestureEvent { - translationX: number; - translationY: number; -} - function reactionEquals(one: _.IDragOverReaction, other: _.IDragOverReaction | null): boolean { if (!one && !other) { return true; @@ -417,7 +412,6 @@ export class TreeView extends HeightMap { private scrollableElement: ScrollableElement; private msGesture: MSGesture | undefined; private lastPointerType: string = ''; - private lastClickTimeStamp: number = 0; private horizontalScrolling: boolean; private contentWidthUpdateDelayer = new Delayer(50); @@ -520,12 +514,7 @@ export class TreeView extends HeightMap { this._onDidScroll.fire(); }); - if (Browser.isIE) { - this.wrapper.style.msTouchAction = 'none'; - this.wrapper.style.msContentZooming = 'none'; - } else { - this.gestureDisposable = Touch.Gesture.addTarget(this.wrapper); - } + this.gestureDisposable = Touch.Gesture.addTarget(this.wrapper); this.rowsContainer = document.createElement('div'); this.rowsContainer.className = 'monaco-tree-rows'; @@ -552,26 +541,6 @@ export class TreeView extends HeightMap { this.viewListeners.push(DOM.addDisposableListener(this.wrapper, Touch.EventType.Tap, (e) => this.onTap(e))); this.viewListeners.push(DOM.addDisposableListener(this.wrapper, Touch.EventType.Change, (e) => this.onTouchChange(e))); - if (Browser.isIE) { - this.viewListeners.push(DOM.addDisposableListener(this.wrapper, 'MSPointerDown', (e) => this.onMsPointerDown(e))); - this.viewListeners.push(DOM.addDisposableListener(this.wrapper, 'MSGestureTap', (e) => this.onMsGestureTap(e))); - - // these events come too fast, we throttle them - this.viewListeners.push(DOM.addDisposableThrottledListener(this.wrapper, 'MSGestureChange', e => this.onThrottledMsGestureChange(e), (lastEvent, event) => { - event.stopPropagation(); - event.preventDefault(); - - let result = { translationY: event.translationY, translationX: event.translationX }; - - if (lastEvent) { - result.translationY += lastEvent.translationY; - result.translationX += lastEvent.translationX; - } - - return result; - })); - } - this.viewListeners.push(DOM.addDisposableListener(window, 'dragover', (e) => this.onDragOver(e))); this.viewListeners.push(DOM.addDisposableListener(this.wrapper, 'drop', (e) => this.onDrop(e))); this.viewListeners.push(DOM.addDisposableListener(window, 'dragend', (e) => this.onDragEnd(e))); @@ -1144,15 +1113,6 @@ export class TreeView extends HeightMap { return; } - if (Browser.isIE && Date.now() - this.lastClickTimeStamp < 300) { - // IE10+ doesn't set the detail property correctly. While IE10 simply - // counts the number of clicks, IE11 reports always 1. To align with - // other browser, we set the value to 2 if clicks events come in a 300ms - // sequence. - event.detail = 2; - } - this.lastClickTimeStamp = Date.now(); - this.context.controller!.onClick(this.context.tree, item.model.getElement(), event); } @@ -1563,39 +1523,6 @@ export class TreeView extends HeightMap { this._onDOMBlur.fire(); } - // MS specific DOM Events - - private onMsPointerDown(event: MSPointerEvent): void { - if (!this.msGesture) { - return; - } - - // Circumvent IE11 breaking change in e.pointerType & TypeScript's stale definitions - let pointerType = event.pointerType; - if (pointerType === ((event).MSPOINTER_TYPE_MOUSE || 'mouse')) { - this.lastPointerType = 'mouse'; - return; - } else if (pointerType === ((event).MSPOINTER_TYPE_TOUCH || 'touch')) { - this.lastPointerType = 'touch'; - } else { - return; - } - - event.stopPropagation(); - event.preventDefault(); - - this.msGesture.addPointer(event.pointerId); - } - - private onThrottledMsGestureChange(event: IThrottledGestureEvent): void { - this.scrollTop -= event.translationY; - } - - private onMsGestureTap(event: MSGestureEvent): void { - (event).initialTarget = document.elementFromPoint(event.clientX, event.clientY); - this.onTap(event); - } - // DOM changes private insertItemInDOM(item: ViewItem): void { diff --git a/src/vs/base/test/common/extpath.test.ts b/src/vs/base/test/common/extpath.test.ts index eb3d8da7a46..02aa3a96377 100644 --- a/src/vs/base/test/common/extpath.test.ts +++ b/src/vs/base/test/common/extpath.test.ts @@ -6,6 +6,7 @@ import * as assert from 'assert'; import * as extpath from 'vs/base/common/extpath'; import * as platform from 'vs/base/common/platform'; +import { CharCode } from 'vs/base/common/charCode'; suite('Paths', () => { @@ -114,4 +115,19 @@ suite('Paths', () => { assert.ok(!extpath.isRootOrDriveLetter('/path')); } }); + + test('isWindowsDriveLetter', () => { + assert.ok(!extpath.isWindowsDriveLetter(0)); + assert.ok(!extpath.isWindowsDriveLetter(-1)); + assert.ok(extpath.isWindowsDriveLetter(CharCode.A)); + assert.ok(extpath.isWindowsDriveLetter(CharCode.z)); + }); + + test('indexOfPath', () => { + assert.equal(extpath.indexOfPath('/foo', '/bar', true), -1); + assert.equal(extpath.indexOfPath('/foo', '/FOO', false), -1); + assert.equal(extpath.indexOfPath('/foo', '/FOO', true), 0); + assert.equal(extpath.indexOfPath('/some/long/path', '/some/long', false), 0); + assert.equal(extpath.indexOfPath('/some/long/path', '/PATH', true), 10); + }); }); diff --git a/src/vs/base/test/common/linkedText.test.ts b/src/vs/base/test/common/linkedText.test.ts index 8e1cb887485..a7b61a558c2 100644 --- a/src/vs/base/test/common/linkedText.test.ts +++ b/src/vs/base/test/common/linkedText.test.ts @@ -21,6 +21,21 @@ suite('LinkedText', () => { { label: 'link text', href: 'http://link.href', title: 'and a title' }, '.' ]); + assert.deepEqual(parseLinkedText('Some message with [link text](http://link.href \'and a title\').').nodes, [ + 'Some message with ', + { label: 'link text', href: 'http://link.href', title: 'and a title' }, + '.' + ]); + assert.deepEqual(parseLinkedText('Some message with [link text](http://link.href "and a \'title\'").').nodes, [ + 'Some message with ', + { label: 'link text', href: 'http://link.href', title: 'and a \'title\'' }, + '.' + ]); + assert.deepEqual(parseLinkedText('Some message with [link text](http://link.href \'and a "title"\').').nodes, [ + 'Some message with ', + { label: 'link text', href: 'http://link.href', title: 'and a "title"' }, + '.' + ]); assert.deepEqual(parseLinkedText('Some message with [link text](random stuff).').nodes, [ 'Some message with [link text](random stuff).' ]); diff --git a/src/vs/base/test/node/glob.test.ts b/src/vs/base/test/node/glob.test.ts index f7e8dfd1356..2c0288e3897 100644 --- a/src/vs/base/test/node/glob.test.ts +++ b/src/vs/base/test/node/glob.test.ts @@ -239,10 +239,7 @@ suite('Glob', () => { assertGlobMatch(p, 'some/folder/project.json'); assertNoGlobMatch(p, 'some/folder/file_project.json'); assertNoGlobMatch(p, 'some/folder/fileproject.json'); - // assertNoGlobMatch(p, '/rrproject.json'); TODO@ben this still fails if T1-3 are disabled assertNoGlobMatch(p, 'some/rrproject.json'); - // assertNoGlobMatch(p, 'rrproject.json'); - // assertNoGlobMatch(p, '\\rrproject.json'); assertNoGlobMatch(p, 'some\\rrproject.json'); p = 'test/**'; diff --git a/src/vs/code/electron-browser/issue/issueReporterMain.ts b/src/vs/code/electron-browser/issue/issueReporterMain.ts index 169dbf0c326..8ed46109857 100644 --- a/src/vs/code/electron-browser/issue/issueReporterMain.ts +++ b/src/vs/code/electron-browser/issue/issueReporterMain.ts @@ -39,7 +39,7 @@ import { ITelemetryServiceConfig, TelemetryService } from 'vs/platform/telemetry import { combinedAppender, LogAppender, NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProperties'; import { TelemetryAppenderClient } from 'vs/platform/telemetry/node/telemetryIpc'; -import { IWindowConfiguration } from 'vs/platform/windows/common/windows'; +import { INativeWindowConfiguration } from 'vs/platform/windows/node/window'; const MAX_URL_LENGTH = 2045; @@ -49,7 +49,7 @@ interface SearchResult { state?: string; } -export interface IssueReporterConfiguration extends IWindowConfiguration { +export interface IssueReporterConfiguration extends INativeWindowConfiguration { data: IssueReporterData; features: IssueReporterFeatures; } @@ -316,7 +316,7 @@ export class IssueReporter extends Disposable { } } - private initServices(configuration: IWindowConfiguration): void { + private initServices(configuration: INativeWindowConfiguration): void { const serviceCollection = new ServiceCollection(); const mainProcessService = new MainProcessService(configuration.windowId); serviceCollection.set(IMainProcessService, mainProcessService); diff --git a/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts b/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts index 09ccca9b823..9684cbd4c20 100644 --- a/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts +++ b/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts @@ -75,7 +75,11 @@ function getProcessItem(processes: FormattedProcessItem[], item: ProcessItem, in // Recurse into children if any if (Array.isArray(item.children)) { - item.children.forEach(child => getProcessItem(processes, child, indent + 1, isLocal)); + item.children.forEach(child => { + if (child) { + getProcessItem(processes, child, indent + 1, isLocal); + } + }); } } diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 4ba8c87cb98..cda40703e32 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -133,7 +133,7 @@ export class CodeApplication extends Disposable { // // !!! DO NOT CHANGE without consulting the documentation !!! // - // app.on('remote-get-guest-web-contents', event => event.preventDefault()); // TODO@Ben TODO@Matt revisit this need for + // app.on('remote-get-guest-web-contents', event => event.preventDefault()); // TODO@Matt revisit this need for app.on('remote-require', (event, sender, module) => { this.logService.trace('App#on(remote-require): prevented'); @@ -511,7 +511,7 @@ export class CodeApplication extends Disposable { type: 'info', message: localize('trace.message', "Successfully created trace."), detail: localize('trace.detail', "Please create an issue and manually attach the following file:\n{0}", path), - buttons: [localize('trace.ok', "Ok")] + buttons: [localize('trace.ok', "OK")] }, withNullAsUndefined(BrowserWindow.getFocusedWindow())); } } else { diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 7871c40dc59..12aebae23c3 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -276,7 +276,7 @@ class CodeMain { // Skip this if we are running with --wait where it is expected that we wait for a while. // Also skip when gathering diagnostics (--status) which can take a longer time. let startupWarningDialogHandle: NodeJS.Timeout | undefined = undefined; - if (!environmentService.wait && !environmentService.status) { + if (!environmentService.args.wait && !environmentService.args.status) { startupWarningDialogHandle = setTimeout(() => { this.showStartupWarningDialog( localize('secondInstanceNoResponse', "Another instance of {0} is running but not responding", product.nameShort), diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index 1f1c08a58f1..7e4ddbbe0a3 100644 --- a/src/vs/code/electron-main/window.ts +++ b/src/vs/code/electron-main/window.ts @@ -14,10 +14,11 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv'; import product from 'vs/platform/product/common/product'; -import { IWindowSettings, MenuBarVisibility, IWindowConfiguration, ReadyState, getTitleBarStyle, getMenuBarVisibility } from 'vs/platform/windows/common/windows'; +import { IWindowSettings, MenuBarVisibility, ReadyState, getTitleBarStyle, getMenuBarVisibility } from 'vs/platform/windows/common/windows'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; import { ICodeWindow, IWindowState, WindowMode } from 'vs/platform/windows/electron-main/windows'; +import { INativeWindowConfiguration } from 'vs/platform/windows/node/window'; import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; @@ -32,6 +33,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; const RUN_TEXTMATE_IN_WORKER = false; @@ -84,7 +86,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { private readonly whenReadyCallbacks: { (window: ICodeWindow): void }[]; - private pendingLoadConfig?: IWindowConfiguration; + private pendingLoadConfig?: INativeWindowConfiguration; private marketplaceHeadersPromise: Promise; @@ -100,7 +102,8 @@ export class CodeWindow extends Disposable implements ICodeWindow { @IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService, @IBackupMainService private readonly backupMainService: IBackupMainService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IDialogMainService private readonly dialogMainService: IDialogMainService + @IDialogMainService private readonly dialogMainService: IDialogMainService, + @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService ) { super(); @@ -229,8 +232,8 @@ export class CodeWindow extends Disposable implements ICodeWindow { this.registerListeners(); } - private currentConfig: IWindowConfiguration | undefined; - get config(): IWindowConfiguration | undefined { return this.currentConfig; } + private currentConfig: INativeWindowConfiguration | undefined; + get config(): INativeWindowConfiguration | undefined { return this.currentConfig; } private _id: number; get id(): number { return this._id; } @@ -244,6 +247,8 @@ export class CodeWindow extends Disposable implements ICodeWindow { get isExtensionTestHost(): boolean { return !!(this.config && this.config.extensionTestsPath); } + get isExtensionDevelopmentTestFromCli(): boolean { return this.isExtensionDevelopmentHost && this.isExtensionTestHost && !this.config?.debugId; } + setRepresentedFilename(filename: string): void { if (isMacintosh) { this.win.setRepresentedFilename(filename); @@ -447,6 +452,15 @@ export class CodeWindow extends Disposable implements ICodeWindow { private onWindowError(error: WindowError): void { this.logService.error(error === WindowError.CRASHED ? '[VS Code]: render process crashed!' : '[VS Code]: detected unresponsive'); + // If we run extension tests from CLI, showing a dialog is not + // very helpful in this case. Rather, we bring down the test run + // to signal back a failing run. + if (this.isExtensionDevelopmentTestFromCli) { + this.lifecycleMainService.kill(1); + return; + } + + // Telemetry type WindowErrorClassification = { type: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth', isMeasurement: true }; }; @@ -539,7 +553,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { } } - load(config: IWindowConfiguration, isReload?: boolean, disableExtensions?: boolean): void { + load(config: INativeWindowConfiguration, isReload?: boolean, disableExtensions?: boolean): void { // If this is the first time the window is loaded, we associate the paths // directly with the window because we assume the loading will just work @@ -599,7 +613,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { this._onLoad.fire(); } - reload(configurationIn?: IWindowConfiguration, cli?: ParsedArgs): void { + reload(configurationIn?: INativeWindowConfiguration, cli?: ParsedArgs): void { // If config is not provided, copy our current one const configuration = configurationIn ? configurationIn : objects.mixin({}, this.currentConfig); @@ -626,7 +640,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { this.load(configuration, true, disableExtensions); } - private getUrl(windowConfiguration: IWindowConfiguration): string { + private getUrl(windowConfiguration: INativeWindowConfiguration): string { // Set window ID windowConfiguration.windowId = this._win.id; diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index 609395b535a..f1fe93e5f0c 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -128,7 +128,7 @@ export async function main(argv: string[]): Promise { delete env['ELECTRON_RUN_AS_NODE']; - const processCallbacks: ((child: ChildProcess) => Promise)[] = []; + const processCallbacks: ((child: ChildProcess) => Promise)[] = []; const verbose = args.verbose || args.status; if (verbose) { diff --git a/src/vs/editor/browser/controller/mouseHandler.ts b/src/vs/editor/browser/controller/mouseHandler.ts index 5bab90e5eb1..479fa090c6e 100644 --- a/src/vs/editor/browser/controller/mouseHandler.ts +++ b/src/vs/editor/browser/controller/mouseHandler.ts @@ -6,7 +6,7 @@ import * as browser from 'vs/base/browser/browser'; import * as dom from 'vs/base/browser/dom'; import { StandardWheelEvent, IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; -import { RunOnceScheduler, TimeoutTimer } from 'vs/base/common/async'; +import { TimeoutTimer } from 'vs/base/common/async'; import { Disposable } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; import { HitTestContext, IViewZoneData, MouseTarget, MouseTargetFactory, PointerHandlerLastRenderData } from 'vs/editor/browser/controller/mouseTarget'; @@ -69,7 +69,6 @@ export class MouseHandler extends ViewEventHandler { protected viewController: ViewController; protected viewHelper: IPointerHandlerHelper; protected mouseTargetFactory: MouseTargetFactory; - private readonly _asyncFocus: RunOnceScheduler; protected readonly _mouseDownOperation: MouseDownOperation; private lastMouseLeaveTime: number; @@ -89,8 +88,6 @@ export class MouseHandler extends ViewEventHandler { (e) => this._getMouseColumn(e) )); - this._asyncFocus = this._register(new RunOnceScheduler(() => this.viewHelper.focusTextArea(), 0)); - this.lastMouseLeaveTime = -1; const mouseEvents = new EditorMouseEventFactory(this.viewHelper.viewDomNode); @@ -122,7 +119,7 @@ export class MouseHandler extends ViewEventHandler { e.stopPropagation(); } }; - this._register(dom.addDisposableListener(this.viewHelper.viewDomNode, browser.isEdgeOrIE ? 'mousewheel' : 'wheel', onMouseWheel, { capture: true, passive: false })); + this._register(dom.addDisposableListener(this.viewHelper.viewDomNode, browser.isEdge ? 'mousewheel' : 'wheel', onMouseWheel, { capture: true, passive: false })); this._context.addEventHandler(this); } @@ -137,9 +134,7 @@ export class MouseHandler extends ViewEventHandler { this._mouseDownOperation.onCursorStateChanged(e); return false; } - private _isFocused = false; public onFocusChanged(e: viewEvents.ViewFocusChangedEvent): boolean { - this._isFocused = e.isFocused; return false; } public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean { @@ -223,15 +218,8 @@ export class MouseHandler extends ViewEventHandler { } const focus = () => { - // In IE11, if the focus is in the browser's address bar and - // then you click in the editor, calling preventDefault() - // will not move focus properly (focus remains the address bar) - if (browser.isIE && !this._isFocused) { - this._asyncFocus.schedule(); - } else { - e.preventDefault(); - this.viewHelper.focusTextArea(); - } + e.preventDefault(); + this.viewHelper.focusTextArea(); }; if (shouldHandle && (targetIsContent || (targetIsLineNumbers && selectOnLineNumbers))) { diff --git a/src/vs/editor/browser/controller/textAreaHandler.ts b/src/vs/editor/browser/controller/textAreaHandler.ts index 7a22ab779d3..6bb0877f9de 100644 --- a/src/vs/editor/browser/controller/textAreaHandler.ts +++ b/src/vs/editor/browser/controller/textAreaHandler.ts @@ -53,7 +53,7 @@ class VisibleTextAreaData { } } -const canUseZeroSizeTextarea = (browser.isEdgeOrIE || browser.isFirefox); +const canUseZeroSizeTextarea = (browser.isEdge || browser.isFirefox); export class TextAreaHandler extends ViewPart { @@ -283,7 +283,7 @@ export class TextAreaHandler extends ViewPart { })); this._register(this._textAreaInput.onCompositionUpdate((e: ICompositionData) => { - if (browser.isEdgeOrIE) { + if (browser.isEdge) { // Due to isEdgeOrIE (where the textarea was not cleared initially) // we cannot assume the text consists only of the composited text this._visibleTextArea = this._visibleTextArea!.setWidth(0); diff --git a/src/vs/editor/browser/controller/textAreaInput.ts b/src/vs/editor/browser/controller/textAreaInput.ts index 8d0f1962ab2..ba6a141c70a 100644 --- a/src/vs/editor/browser/controller/textAreaInput.ts +++ b/src/vs/editor/browser/controller/textAreaInput.ts @@ -191,7 +191,7 @@ export class TextAreaInput extends Disposable { this._isDoingComposition = true; // In IE we cannot set .value when handling 'compositionstart' because the entire composition will get canceled. - if (!browser.isEdgeOrIE) { + if (!browser.isEdge) { this._setAndWriteTextAreaState('compositionstart', TextAreaState.EMPTY); } @@ -225,15 +225,7 @@ export class TextAreaInput extends Disposable { // Multi-part Japanese compositions reset cursor in Edge/IE, Chinese and Korean IME don't have this issue. // The reason that we can't use this path for all CJK IME is IE and Edge behave differently when handling Korean IME, // which breaks this path of code. - if (browser.isEdgeOrIE && locale === 'ja') { - return true; - } - - // https://github.com/Microsoft/monaco-editor/issues/545 - // On IE11, we can't trust composition data when typing Chinese as IE11 doesn't emit correct - // events when users type numbers in IME. - // Chinese: zh-Hans-CN, zh-Hans-SG, zh-Hant-TW, zh-Hant-HK - if (browser.isIE && locale.indexOf('zh-Han') === 0) { + if (browser.isEdge && locale === 'ja') { return true; } @@ -274,7 +266,7 @@ export class TextAreaInput extends Disposable { // Due to isEdgeOrIE (where the textarea was not cleared initially) and isChrome (the textarea is not updated correctly when composition ends) // we cannot assume the text at the end consists only of the composited text - if (browser.isEdgeOrIE || browser.isChrome) { + if (browser.isEdge || browser.isChrome) { this._textAreaState = TextAreaState.readFromTextArea(this._textArea); } diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index 85ee85e34f7..3b9f5d03e24 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -13,7 +13,7 @@ import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import * as editorCommon from 'vs/editor/common/editorCommon'; -import { IIdentifiedSingleEditOperation, IModelDecoration, IModelDeltaDecoration, ITextModel, ICursorStateComputer } from 'vs/editor/common/model'; +import { IIdentifiedSingleEditOperation, IModelDecoration, IModelDeltaDecoration, ITextModel, ICursorStateComputer, IWordAtPosition } from 'vs/editor/common/model'; import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent } from 'vs/editor/common/model/textModelEvents'; import { OverviewRulerZone } from 'vs/editor/common/view/overviewZoneManager'; import { IEditorWhitespace } from 'vs/editor/common/viewLayout/linesLayout'; @@ -567,6 +567,11 @@ export interface ICodeEditor extends editorCommon.IEditor { */ getRawOptions(): IEditorOptions; + /** + * @internal + */ + getConfiguredWordAtPosition(position: Position): IWordAtPosition | null; + /** * Get value of the current model attached to this editor. * @see `ITextModel.getValue` diff --git a/src/vs/editor/browser/editorExtensions.ts b/src/vs/editor/browser/editorExtensions.ts index f6d836aac53..149d562a3d4 100644 --- a/src/vs/editor/browser/editorExtensions.ts +++ b/src/vs/editor/browser/editorExtensions.ts @@ -15,7 +15,7 @@ import { IModelService } from 'vs/editor/common/services/modelService'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { CommandsRegistry, ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; -import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, IContextKeyService, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { IConstructorSignature1, ServicesAccessor as InstantiationServicesAccessor, BrandedService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindings, KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -39,26 +39,26 @@ export interface IDiffEditorContributionDescription { //#region Command export interface ICommandKeybindingsOptions extends IKeybindings { - kbExpr?: ContextKeyExpr | null; + kbExpr?: ContextKeyExpression | null; weight: number; } export interface ICommandMenuOptions { menuId: MenuId; group: string; order: number; - when?: ContextKeyExpr; + when?: ContextKeyExpression; title: string; } export interface ICommandOptions { id: string; - precondition: ContextKeyExpr | undefined; + precondition: ContextKeyExpression | undefined; kbOpts?: ICommandKeybindingsOptions; description?: ICommandHandlerDescription; menuOpts?: ICommandMenuOptions | ICommandMenuOptions[]; } export abstract class Command { public readonly id: string; - public readonly precondition: ContextKeyExpr | undefined; + public readonly precondition: ContextKeyExpression | undefined; private readonly _kbOpts: ICommandKeybindingsOptions | undefined; private readonly _menuOpts: ICommandMenuOptions | ICommandMenuOptions[] | undefined; private readonly _description: ICommandHandlerDescription | undefined; @@ -193,7 +193,7 @@ export abstract class EditorCommand extends Command { export interface IEditorActionContextMenuOptions { group: string; order: number; - when?: ContextKeyExpr; + when?: ContextKeyExpression; menuId?: MenuId; } export interface IActionOptions extends ICommandOptions { diff --git a/src/vs/editor/browser/services/bulkEditService.ts b/src/vs/editor/browser/services/bulkEditService.ts index 3d18020da6f..f6fdac9c874 100644 --- a/src/vs/editor/browser/services/bulkEditService.ts +++ b/src/vs/editor/browser/services/bulkEditService.ts @@ -16,6 +16,7 @@ export interface IBulkEditOptions { progress?: IProgress; showPreview?: boolean; label?: string; + quotableLabel?: string; } export interface IBulkEditResult { diff --git a/src/vs/editor/browser/viewParts/lines/viewLine.ts b/src/vs/editor/browser/viewParts/lines/viewLine.ts index 9c93f326a6f..11a33fd32a3 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLine.ts @@ -42,7 +42,7 @@ const canUseFastRenderedViewLine = (function () { return true; })(); -const alwaysRenderInlineSelection = (browser.isEdgeOrIE); +const alwaysRenderInlineSelection = (browser.isEdge); export class DomReadingContext { diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.ts b/src/vs/editor/browser/viewParts/minimap/minimap.ts index d5aa95d0611..4c1fe639300 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.ts +++ b/src/vs/editor/browser/viewParts/minimap/minimap.ts @@ -46,7 +46,7 @@ class MinimapOptions { public readonly renderMinimap: RenderMinimap; - public readonly mode: 'actual' | 'cover' | 'contain'; + public readonly size: 'proportional' | 'fill' | 'fit'; public readonly minimapHeightIsEditorHeight: boolean; @@ -108,7 +108,7 @@ class MinimapOptions { const minimapOpts = options.get(EditorOption.minimap); this.renderMinimap = layoutInfo.renderMinimap | 0; - this.mode = minimapOpts.mode; + this.size = minimapOpts.size; this.minimapHeightIsEditorHeight = layoutInfo.minimapHeightIsEditorHeight; this.scrollBeyondLastLine = options.get(EditorOption.scrollBeyondLastLine); this.showSlider = minimapOpts.showSlider; @@ -144,7 +144,7 @@ class MinimapOptions { public equals(other: MinimapOptions): boolean { return (this.renderMinimap === other.renderMinimap - && this.mode === other.mode + && this.size === other.size && this.minimapHeightIsEditorHeight === other.minimapHeightIsEditorHeight && this.scrollBeyondLastLine === other.scrollBeyondLastLine && this.showSlider === other.showSlider @@ -163,7 +163,7 @@ class MinimapOptions { && this.fontScale === other.fontScale && this.minimapLineHeight === other.minimapLineHeight && this.minimapCharWidth === other.minimapCharWidth - && this.backgroundColor.equals(other.backgroundColor) + && this.backgroundColor && this.backgroundColor.equals(other.backgroundColor) ); } } @@ -1111,7 +1111,7 @@ class InnerMinimap extends Disposable { if (!this._lastRenderData) { return; } - if (this._model.options.minimapHeightIsEditorHeight) { + if (this._model.options.size !== 'proportional') { if (e.leftButton && this._lastRenderData) { // pretend the click occured in the center of the slider const position = dom.getDomNodePagePosition(this._slider.domNode); diff --git a/src/vs/editor/browser/viewParts/selections/selections.ts b/src/vs/editor/browser/viewParts/selections/selections.ts index d50b0f5679b..bdedea154e3 100644 --- a/src/vs/editor/browser/viewParts/selections/selections.ts +++ b/src/vs/editor/browser/viewParts/selections/selections.ts @@ -60,7 +60,7 @@ function toStyled(item: LineVisibleRanges): LineVisibleRangesWithStyle { // TODO@Alex: Remove this once IE11 fixes Bug #524217 // The problem in IE11 is that it does some sort of auto-zooming to accomodate for displays with different pixel density. // Unfortunately, this auto-zooming is buggy around dealing with rounded borders -const isIEWithZoomingIssuesNearRoundedBorders = browser.isEdgeOrIE; +const isIEWithZoomingIssuesNearRoundedBorders = browser.isEdge; export class SelectionsOverlay extends DynamicViewOverlay { diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index c8be02dfba7..dd790f97f10 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -32,7 +32,7 @@ import { ISelection, Selection } from 'vs/editor/common/core/selection'; import { InternalEditorAction } from 'vs/editor/common/editorAction'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { EndOfLinePreference, IIdentifiedSingleEditOperation, IModelDecoration, IModelDecorationOptions, IModelDecorationsChangeAccessor, IModelDeltaDecoration, ITextModel, ICursorStateComputer } from 'vs/editor/common/model'; +import { EndOfLinePreference, IIdentifiedSingleEditOperation, IModelDecoration, IModelDecorationOptions, IModelDecorationsChangeAccessor, IModelDeltaDecoration, ITextModel, ICursorStateComputer, IWordAtPosition } from 'vs/editor/common/model'; import { ClassName } from 'vs/editor/common/model/intervalTree'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent } from 'vs/editor/common/model/textModelEvents'; @@ -52,6 +52,7 @@ import { IAccessibilityService } from 'vs/platform/accessibility/common/accessib import { withNullAsUndefined } from 'vs/base/common/types'; import { MonospaceLineBreaksComputerFactory } from 'vs/editor/common/viewModel/monospaceLineBreaksComputer'; import { DOMLineBreaksComputerFactory } from 'vs/editor/browser/view/domLineBreaksComputer'; +import { WordOperations } from 'vs/editor/common/controller/cursorWordOperations'; let EDITOR_ID = 0; @@ -376,6 +377,13 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE return this._configuration.getRawOptions(); } + public getConfiguredWordAtPosition(position: Position): IWordAtPosition | null { + if (!this._modelData) { + return null; + } + return WordOperations.getWordAtPosition(this._modelData.model, this._configuration.options.get(EditorOption.wordSeparators), position); + } + public getValue(options: { preserveBOM: boolean; lineEnding: string; } | null = null): string { if (!this._modelData) { return ''; diff --git a/src/vs/editor/browser/widget/diffReview.ts b/src/vs/editor/browser/widget/diffReview.ts index cfa24de682f..eb22ce3dba0 100644 --- a/src/vs/editor/browser/widget/diffReview.ts +++ b/src/vs/editor/browser/widget/diffReview.ts @@ -98,7 +98,7 @@ export class DiffReview extends Disposable { this.actionBarContainer.domNode )); - this._actionBar.push(new Action('diffreview.close', nls.localize('label.close', "Close"), 'close-diff-review', true, () => { + this._actionBar.push(new Action('diffreview.close', nls.localize('label.close', "Close"), 'close-diff-review codicon-close', true, () => { this.hide(); return Promise.resolve(null); }), { label: false, icon: true }); @@ -617,10 +617,11 @@ export class DiffReview extends Disposable { header.setAttribute('role', 'listitem'); container.appendChild(header); + const lineHeight = modifiedOptions.get(EditorOption.lineHeight); let modLine = minModifiedLine; for (let i = 0, len = diffs.length; i < len; i++) { const diffEntry = diffs[i]; - DiffReview._renderSection(container, diffEntry, modLine, this._width, originalOptions, originalModel, originalModelOpts, modifiedOptions, modifiedModel, modifiedModelOpts); + DiffReview._renderSection(container, diffEntry, modLine, lineHeight, this._width, originalOptions, originalModel, originalModelOpts, modifiedOptions, modifiedModel, modifiedModelOpts); if (diffEntry.modifiedLineStart !== 0) { modLine = diffEntry.modifiedLineEnd; } @@ -632,7 +633,7 @@ export class DiffReview extends Disposable { } private static _renderSection( - dest: HTMLElement, diffEntry: DiffEntry, modLine: number, width: number, + dest: HTMLElement, diffEntry: DiffEntry, modLine: number, lineHeight: number, width: number, originalOptions: IComputedEditorOptions, originalModel: ITextModel, originalModelOpts: TextModelResolvedOptions, modifiedOptions: IComputedEditorOptions, modifiedModel: ITextModel, modifiedModelOpts: TextModelResolvedOptions ): void { @@ -641,17 +642,18 @@ export class DiffReview extends Disposable { let rowClassName: string = 'diff-review-row'; let lineNumbersExtraClassName: string = ''; - let spacerClassName: string = 'diff-review-spacer'; + const spacerClassName: string = 'diff-review-spacer'; + let spacerCodiconName: string | null = null; switch (type) { case DiffEntryType.Insert: rowClassName = 'diff-review-row line-insert'; lineNumbersExtraClassName = ' char-insert'; - spacerClassName = 'diff-review-spacer insert-sign'; + spacerCodiconName = 'codicon codicon-add'; break; case DiffEntryType.Delete: rowClassName = 'diff-review-row line-delete'; lineNumbersExtraClassName = ' char-delete'; - spacerClassName = 'diff-review-spacer delete-sign'; + spacerCodiconName = 'codicon codicon-remove'; break; } @@ -686,6 +688,7 @@ export class DiffReview extends Disposable { let cell = document.createElement('div'); cell.className = 'diff-review-cell'; + cell.style.height = `${lineHeight}px`; row.appendChild(cell); const originalLineNumber = document.createElement('span'); @@ -713,7 +716,15 @@ export class DiffReview extends Disposable { const spacer = document.createElement('span'); spacer.className = spacerClassName; - spacer.innerHTML = '  '; + + if (spacerCodiconName) { + const spacerCodicon = document.createElement('span'); + spacerCodicon.className = spacerCodiconName; + spacerCodicon.innerHTML = '  '; + spacer.appendChild(spacerCodicon); + } else { + spacer.innerHTML = '  '; + } cell.appendChild(spacer); let lineContent: string; diff --git a/src/vs/editor/browser/widget/media/diffReview.css b/src/vs/editor/browser/widget/media/diffReview.css index b2b17028489..56c6f15cbf6 100644 --- a/src/vs/editor/browser/widget/media/diffReview.css +++ b/src/vs/editor/browser/widget/media/diffReview.css @@ -37,13 +37,14 @@ width: 100%; } -.monaco-diff-editor .diff-review-cell { - display: table-cell; -} - .monaco-diff-editor .diff-review-spacer { display: inline-block; width: 10px; + vertical-align: middle; +} + +.monaco-diff-editor .diff-review-spacer > .codicon { + font-size: 9px !important; } .monaco-diff-editor .diff-review-actions { diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index de910398d63..63ac7bf2133 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -510,7 +510,7 @@ export interface IEditorOptions { * Controls whether clicking on the empty content after a folded line will unfold the line. * Defaults to false. */ - unfoldOnClickInEmptyContent?: boolean; + unfoldOnClickAfterEndOfLine?: boolean; /** * Enable highlighting of matching brackets. * Defaults to 'always'. @@ -1332,7 +1332,7 @@ export class EditorFontLigatures extends BaseEditorOption= 2 ? Math.round(minimap.scale * 2) : minimap.scale); const minimapMaxColumn = minimap.maxColumn | 0; - const minimapMode = minimap.mode; + const minimapSize = minimap.size; const scrollbar = options.get(EditorOption.scrollbar); const verticalScrollbarWidth = scrollbar.verticalScrollbarSize | 0; @@ -1866,7 +1866,7 @@ export class EditorLayoutInfoComputer extends ComputedEditorOption minimapCanvasInnerHeight) { + if (minimapSize === 'fill' || effectiveMinimapHeight > minimapCanvasInnerHeight) { minimapHeightIsEditorHeight = true; const configuredFontScale = minimapScale; minimapLineHeight = Math.min(lineHeight * pixelRatio, Math.max(1, Math.floor(1 / desiredRatio))); @@ -2074,7 +2074,7 @@ export interface IEditorMinimapOptions { * Control the minimap rendering mode. * Defaults to 'actual'. */ - mode?: 'actual' | 'cover' | 'contain'; + size?: 'proportional' | 'fill' | 'fit'; /** * Control the rendering of the minimap slider. * Defaults to 'mouseover'. @@ -2103,7 +2103,7 @@ class EditorMinimap extends BaseEditorOption(input.mode, this.defaultValue.mode, ['actual', 'cover', 'contain']), + size: EditorStringEnumOption.stringSet<'proportional' | 'fill' | 'fit'>(input.size, this.defaultValue.size, ['proportional', 'fill', 'fit']), side: EditorStringEnumOption.stringSet<'right' | 'left'>(input.side, this.defaultValue.side, ['right', 'left']), showSlider: EditorStringEnumOption.stringSet<'always' | 'mouseover'>(input.showSlider, this.defaultValue.showSlider, ['always', 'mouseover']), renderCharacters: EditorBooleanOption.boolean(input.renderCharacters, this.defaultValue.renderCharacters), @@ -2834,9 +2835,14 @@ export interface ISuggestOptions { */ showSnippets?: boolean; /** - * Controls the visibility of the status bar at the bottom of the suggest widget. + * Status bar related settings. */ - hideStatusBar?: boolean; + statusBar?: { + /** + * Controls the visibility of the status bar at the bottom of the suggest widget. + */ + visible?: boolean; + } } export type InternalSuggestOptions = Readonly>; @@ -2878,7 +2884,9 @@ class EditorSuggest extends BaseEditorOption Promise; private readonly _contextKeyService: IContextKeyService; @@ -20,7 +20,7 @@ export class InternalEditorAction implements IEditorAction { id: string, label: string, alias: string, - precondition: ContextKeyExpr | undefined, + precondition: ContextKeyExpression | undefined, run: () => Promise, contextKeyService: IContextKeyService ) { diff --git a/src/vs/editor/common/editorContextKeys.ts b/src/vs/editor/common/editorContextKeys.ts index 01668420569..10337bd1bcc 100644 --- a/src/vs/editor/common/editorContextKeys.ts +++ b/src/vs/editor/common/editorContextKeys.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; export namespace EditorContextKeys { @@ -24,13 +24,13 @@ export namespace EditorContextKeys { export const readOnly = new RawContextKey('editorReadonly', false); export const columnSelection = new RawContextKey('editorColumnSelection', false); - export const writable: ContextKeyExpr = readOnly.toNegated(); + export const writable = readOnly.toNegated(); export const hasNonEmptySelection = new RawContextKey('editorHasSelection', false); - export const hasOnlyEmptySelection: ContextKeyExpr = hasNonEmptySelection.toNegated(); + export const hasOnlyEmptySelection = hasNonEmptySelection.toNegated(); export const hasMultipleSelections = new RawContextKey('editorHasMultipleSelections', false); - export const hasSingleSelection: ContextKeyExpr = hasMultipleSelections.toNegated(); + export const hasSingleSelection = hasMultipleSelections.toNegated(); export const tabMovesFocus = new RawContextKey('editorTabMovesFocus', false); - export const tabDoesNotMoveFocus: ContextKeyExpr = tabMovesFocus.toNegated(); + export const tabDoesNotMoveFocus = tabMovesFocus.toNegated(); export const isInEmbeddedEditor = new RawContextKey('isInEmbeddedEditor', false); export const canUndo = new RawContextKey('canUndo', false); export const canRedo = new RawContextKey('canRedo', false); diff --git a/src/vs/editor/common/model/textModelSearch.ts b/src/vs/editor/common/model/textModelSearch.ts index d41d1a2a1ba..ce5b545c6f9 100644 --- a/src/vs/editor/common/model/textModelSearch.ts +++ b/src/vs/editor/common/model/textModelSearch.ts @@ -515,7 +515,7 @@ export class Searcher { private _prevMatchStartIndex: number; private _prevMatchLength: number; - constructor(wordSeparators: WordCharacterClassifier | null, searchRegex: RegExp, ) { + constructor(wordSeparators: WordCharacterClassifier | null, searchRegex: RegExp,) { this._wordSeparators = wordSeparators; this._searchRegex = searchRegex; this._prevMatchStartIndex = -1; diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index c348245a8ec..d892eee8f62 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -494,7 +494,7 @@ export interface CompletionItem { preselect?: boolean; /** * A string or snippet that should be inserted in a document when selecting - * this completion. When `falsy` the [label](#CompletionItem.label) + * this completion. * is used. */ insertText: string; @@ -1246,11 +1246,11 @@ export interface SelectionRangeProvider { export interface FoldingContext { } /** - * A provider of colors for editor models. + * A provider of folding ranges for editor models. */ export interface FoldingRangeProvider { /** - * Provides the color ranges for a specific model. + * Provides the folding ranges for a specific model. */ provideFoldingRanges(model: model.ITextModel, context: FoldingContext, token: CancellationToken): ProviderResult; } @@ -1373,7 +1373,7 @@ export interface RenameProvider { */ export interface AuthenticationSession { id: string; - accessToken(): Promise; + getAccessToken(): Thenable; accountName: string; } @@ -1589,6 +1589,7 @@ export interface SemanticTokensEdits { } export interface DocumentSemanticTokensProvider { + onDidChange?: Event; getLegend(): SemanticTokensLegend; provideDocumentSemanticTokens(model: model.ITextModel, lastResultId: string | null, token: CancellationToken): ProviderResult; releaseDocumentSemanticTokens(resultId: string | undefined): void; diff --git a/src/vs/editor/common/services/modelServiceImpl.ts b/src/vs/editor/common/services/modelServiceImpl.ts index 20e32b9d2a7..67deb4ee702 100644 --- a/src/vs/editor/common/services/modelServiceImpl.ts +++ b/src/vs/editor/common/services/modelServiceImpl.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; import * as errors from 'vs/base/common/errors'; import { URI } from 'vs/base/common/uri'; @@ -724,6 +724,7 @@ class ModelSemanticColoring extends Disposable { private readonly _fetchSemanticTokens: RunOnceScheduler; private _currentResponse: SemanticTokensResponse | null; private _currentRequestCancellationTokenSource: CancellationTokenSource | null; + private _providersChangeListeners: IDisposable[]; constructor(model: ITextModel, themeService: IThemeService, stylingProvider: SemanticStyling) { super(); @@ -734,13 +735,28 @@ class ModelSemanticColoring extends Disposable { this._fetchSemanticTokens = this._register(new RunOnceScheduler(() => this._fetchSemanticTokensNow(), 300)); this._currentResponse = null; this._currentRequestCancellationTokenSource = null; + this._providersChangeListeners = []; this._register(this._model.onDidChangeContent(e => { if (!this._fetchSemanticTokens.isScheduled()) { this._fetchSemanticTokens.schedule(); } })); - this._register(DocumentSemanticTokensProviderRegistry.onDidChange(e => this._fetchSemanticTokens.schedule())); + const bindChangeListeners = () => { + dispose(this._providersChangeListeners); + this._providersChangeListeners = []; + for (const provider of DocumentSemanticTokensProviderRegistry.all(model)) { + if (typeof provider.onDidChange === 'function') { + this._providersChangeListeners.push(provider.onDidChange(() => this._fetchSemanticTokens.schedule(0))); + } + } + }; + bindChangeListeners(); + this._register(DocumentSemanticTokensProviderRegistry.onDidChange(e => { + bindChangeListeners(); + this._fetchSemanticTokens.schedule(); + })); + if (themeService) { // workaround for tests which use undefined... :/ this._register(themeService.onThemeChange(_ => { diff --git a/src/vs/editor/common/standalone/promise-polyfill/cgmanifest.json b/src/vs/editor/common/standalone/promise-polyfill/cgmanifest.json deleted file mode 100644 index b62e25bccff..00000000000 --- a/src/vs/editor/common/standalone/promise-polyfill/cgmanifest.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "registrations": [ - { - "component": { - "type": "git", - "git": { - "name": "promise-polyfill", - "repositoryUrl": "https://github.com/taylorhakes/promise-polyfill", - "commitHash": "efe662be6ea569c439ec92a4f8662c0a7faf0b96" - } - }, - "license": "MIT", - "version": "8.0.0" - } - ], - "version": 1 -} diff --git a/src/vs/editor/common/standalone/promise-polyfill/polyfill.js b/src/vs/editor/common/standalone/promise-polyfill/polyfill.js deleted file mode 100644 index 4ddfcab7cd0..00000000000 --- a/src/vs/editor/common/standalone/promise-polyfill/polyfill.js +++ /dev/null @@ -1,291 +0,0 @@ -/*! -Copyright (c) 2014 Taylor Hakes -Copyright (c) 2014 Forbes Lindesay - */ -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? factory() : - typeof define === 'function' && define.amd ? define(factory) : - (factory()); -}(this, (function () { - 'use strict'; - - /** - * @this {Promise} - */ - function finallyConstructor(callback) { - var constructor = this.constructor; - return this.then( - function (value) { - return constructor.resolve(callback()).then(function () { - return value; - }); - }, - function (reason) { - return constructor.resolve(callback()).then(function () { - return constructor.reject(reason); - }); - } - ); - } - - // Store setTimeout reference so promise-polyfill will be unaffected by - // other code modifying setTimeout (like sinon.useFakeTimers()) - var setTimeoutFunc = setTimeout; - - function noop() { } - - // Polyfill for Function.prototype.bind - function bind(fn, thisArg) { - return function () { - fn.apply(thisArg, arguments); - }; - } - - /** - * @constructor - * @param {Function} fn - */ - function Promise(fn) { - if (!(this instanceof Promise)) - throw new TypeError('Promises must be constructed via new'); - if (typeof fn !== 'function') throw new TypeError('not a function'); - /** @type {!number} */ - this._state = 0; - /** @type {!boolean} */ - this._handled = false; - /** @type {Promise|undefined} */ - this._value = undefined; - /** @type {!Array} */ - this._deferreds = []; - - doResolve(fn, this); - } - - function handle(self, deferred) { - while (self._state === 3) { - self = self._value; - } - if (self._state === 0) { - self._deferreds.push(deferred); - return; - } - self._handled = true; - Promise._immediateFn(function () { - var cb = self._state === 1 ? deferred.onFulfilled : deferred.onRejected; - if (cb === null) { - (self._state === 1 ? resolve : reject)(deferred.promise, self._value); - return; - } - var ret; - try { - ret = cb(self._value); - } catch (e) { - reject(deferred.promise, e); - return; - } - resolve(deferred.promise, ret); - }); - } - - function resolve(self, newValue) { - try { - // Promise Resolution Procedure: https://github.com/promises-aplus/promises-spec#the-promise-resolution-procedure - if (newValue === self) - throw new TypeError('A promise cannot be resolved with itself.'); - if ( - newValue && - (typeof newValue === 'object' || typeof newValue === 'function') - ) { - var then = newValue.then; - if (newValue instanceof Promise) { - self._state = 3; - self._value = newValue; - finale(self); - return; - } else if (typeof then === 'function') { - doResolve(bind(then, newValue), self); - return; - } - } - self._state = 1; - self._value = newValue; - finale(self); - } catch (e) { - reject(self, e); - } - } - - function reject(self, newValue) { - self._state = 2; - self._value = newValue; - finale(self); - } - - function finale(self) { - if (self._state === 2 && self._deferreds.length === 0) { - Promise._immediateFn(function () { - if (!self._handled) { - Promise._unhandledRejectionFn(self._value); - } - }); - } - - for (var i = 0, len = self._deferreds.length; i < len; i++) { - handle(self, self._deferreds[i]); - } - self._deferreds = null; - } - - /** - * @constructor - */ - function Handler(onFulfilled, onRejected, promise) { - this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null; - this.onRejected = typeof onRejected === 'function' ? onRejected : null; - this.promise = promise; - } - - /** - * Take a potentially misbehaving resolver function and make sure - * onFulfilled and onRejected are only called once. - * - * Makes no guarantees about asynchrony. - */ - function doResolve(fn, self) { - var done = false; - try { - fn( - function (value) { - if (done) return; - done = true; - resolve(self, value); - }, - function (reason) { - if (done) return; - done = true; - reject(self, reason); - } - ); - } catch (ex) { - if (done) return; - done = true; - reject(self, ex); - } - } - - Promise.prototype['catch'] = function (onRejected) { - return this.then(null, onRejected); - }; - - Promise.prototype.then = function (onFulfilled, onRejected) { - // @ts-ignore - var prom = new this.constructor(noop); - - handle(this, new Handler(onFulfilled, onRejected, prom)); - return prom; - }; - - Promise.prototype['finally'] = finallyConstructor; - - Promise.all = function (arr) { - return new Promise(function (resolve, reject) { - if (!arr || typeof arr.length === 'undefined') - throw new TypeError('Promise.all accepts an array'); - var args = Array.prototype.slice.call(arr); - if (args.length === 0) return resolve([]); - var remaining = args.length; - - function res(i, val) { - try { - if (val && (typeof val === 'object' || typeof val === 'function')) { - var then = val.then; - if (typeof then === 'function') { - then.call( - val, - function (val) { - res(i, val); - }, - reject - ); - return; - } - } - args[i] = val; - if (--remaining === 0) { - resolve(args); - } - } catch (ex) { - reject(ex); - } - } - - for (var i = 0; i < args.length; i++) { - res(i, args[i]); - } - }); - }; - - Promise.resolve = function (value) { - if (value && typeof value === 'object' && value.constructor === Promise) { - return value; - } - - return new Promise(function (resolve) { - resolve(value); - }); - }; - - Promise.reject = function (value) { - return new Promise(function (resolve, reject) { - reject(value); - }); - }; - - Promise.race = function (values) { - return new Promise(function (resolve, reject) { - for (var i = 0, len = values.length; i < len; i++) { - values[i].then(resolve, reject); - } - }); - }; - - // Use polyfill for setImmediate for performance gains - Promise._immediateFn = - (typeof setImmediate === 'function' && - function (fn) { - setImmediate(fn); - }) || - function (fn) { - setTimeoutFunc(fn, 0); - }; - - Promise._unhandledRejectionFn = function _unhandledRejectionFn(err) { - if (typeof console !== 'undefined' && console) { - console.warn('Possible Unhandled Promise Rejection:', err); // eslint-disable-line no-console - } - }; - - /** @suppress {undefinedVars} */ - var globalNS = (function () { - // the only reliable means to get the global object is - // `Function('return this')()` - // However, this causes CSP violations in Chrome apps. - if (typeof self !== 'undefined') { - return self; - } - if (typeof window !== 'undefined') { - return window; - } - if (typeof global !== 'undefined') { - return global; - } - throw new Error('unable to locate global object'); - })(); - - if (!('Promise' in globalNS)) { - globalNS['Promise'] = Promise; - } else if (!globalNS.Promise.prototype['finally']) { - globalNS.Promise.prototype['finally'] = finallyConstructor; - } - -}))); diff --git a/src/vs/editor/common/standalone/promise-polyfill/polyfill.license.txt b/src/vs/editor/common/standalone/promise-polyfill/polyfill.license.txt deleted file mode 100644 index 6f7c0123162..00000000000 --- a/src/vs/editor/common/standalone/promise-polyfill/polyfill.license.txt +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (c) 2014 Taylor Hakes -Copyright (c) 2014 Forbes Lindesay - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/src/vs/editor/common/standalone/standaloneBase.ts b/src/vs/editor/common/standalone/standaloneBase.ts index 377b5185c28..2239e8d0234 100644 --- a/src/vs/editor/common/standalone/standaloneBase.ts +++ b/src/vs/editor/common/standalone/standaloneBase.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/editor/common/standalone/promise-polyfill/polyfill'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; import { KeyChord, KeyMod as ConstKeyMod } from 'vs/base/common/keyCodes'; diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index 458faca8bf2..418fe492a04 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -199,7 +199,7 @@ export enum EditorOption { folding = 31, foldingStrategy = 32, foldingHighlight = 33, - unfoldOnClickInEmptyContent = 34, + unfoldOnClickAfterEndOfLine = 34, fontFamily = 35, fontInfo = 36, fontLigatures = 37, diff --git a/src/vs/editor/common/standaloneStrings.ts b/src/vs/editor/common/standaloneStrings.ts index f0c107c0cbe..130e4884908 100644 --- a/src/vs/editor/common/standaloneStrings.ts +++ b/src/vs/editor/common/standaloneStrings.ts @@ -71,7 +71,6 @@ export namespace QuickOutlineNLS { export namespace StandaloneCodeEditorNLS { export const editorViewAccessibleLabel = nls.localize('editorViewAccessibleLabel', "Editor content"); - export const accessibilityHelpMessageIE = nls.localize('accessibilityHelpMessageIE', "Press Ctrl+F1 for Accessibility Options."); export const accessibilityHelpMessage = nls.localize('accessibilityHelpMessage', "Press Alt+F1 for Accessibility Options."); } diff --git a/src/vs/editor/contrib/clipboard/clipboard.ts b/src/vs/editor/contrib/clipboard/clipboard.ts index cb106b53b82..afa05e3bf9f 100644 --- a/src/vs/editor/contrib/clipboard/clipboard.ts +++ b/src/vs/editor/contrib/clipboard/clipboard.ts @@ -23,7 +23,7 @@ const CLIPBOARD_CONTEXT_MENU_GROUP = '9_cutcopypaste'; const supportsCut = (platform.isNative || document.queryCommandSupported('cut')); const supportsCopy = (platform.isNative || document.queryCommandSupported('copy')); // IE and Edge have trouble with setting html content in clipboard -const supportsCopyWithSyntaxHighlighting = (supportsCopy && !browser.isEdgeOrIE); +const supportsCopyWithSyntaxHighlighting = (supportsCopy && !browser.isEdge); // Chrome incorrectly returns true for document.queryCommandSupported('paste') // when the paste feature is available but the calling script has insufficient // privileges to actually perform the action @@ -161,6 +161,7 @@ class ExecCommandPasteAction extends ExecCommandAction { kbExpr: EditorContextKeys.textInputFocus, primary: KeyMod.CtrlCmd | KeyCode.KEY_V, win: { primary: KeyMod.CtrlCmd | KeyCode.KEY_V, secondary: [KeyMod.Shift | KeyCode.Insert] }, + linux: { primary: KeyMod.CtrlCmd | KeyCode.KEY_V, secondary: [KeyMod.Shift | KeyCode.Insert] }, weight: KeybindingWeight.EditorContrib }; // Do not bind paste keybindings in the browser, diff --git a/src/vs/editor/contrib/codeAction/codeActionCommands.ts b/src/vs/editor/contrib/codeAction/codeActionCommands.ts index 185ebd86867..13b92c1289d 100644 --- a/src/vs/editor/contrib/codeAction/codeActionCommands.ts +++ b/src/vs/editor/contrib/codeAction/codeActionCommands.ts @@ -164,7 +164,7 @@ export async function applyCodeAction( }); if (action.edit) { - await bulkEditService.apply(action.edit, { editor }); + await bulkEditService.apply(action.edit, { editor, label: action.title }); } if (action.command) { diff --git a/src/vs/editor/contrib/find/findController.ts b/src/vs/editor/contrib/find/findController.ts index fee09b8429d..556a6bf0ad0 100644 --- a/src/vs/editor/contrib/find/findController.ts +++ b/src/vs/editor/contrib/find/findController.ts @@ -39,7 +39,7 @@ export function getSelectionSearchString(editor: ICodeEditor): string | null { // if selection spans multiple lines, default search string to empty if (selection.startLineNumber === selection.endLineNumber) { if (selection.isEmpty()) { - let wordAtPosition = editor.getModel().getWordAtPosition(selection.getStartPosition()); + const wordAtPosition = editor.getConfiguredWordAtPosition(selection.getStartPosition()); if (wordAtPosition) { return wordAtPosition.word; } diff --git a/src/vs/editor/contrib/find/findModel.ts b/src/vs/editor/contrib/find/findModel.ts index 0653ac4f5c4..6866bd48fec 100644 --- a/src/vs/editor/contrib/find/findModel.ts +++ b/src/vs/editor/contrib/find/findModel.ts @@ -20,12 +20,12 @@ import { FindDecorations } from 'vs/editor/contrib/find/findDecorations'; import { FindReplaceState, FindReplaceStateChangedEvent } from 'vs/editor/contrib/find/findState'; import { ReplaceAllCommand } from 'vs/editor/contrib/find/replaceAllCommand'; import { ReplacePattern, parseReplaceString } from 'vs/editor/contrib/find/replacePattern'; -import { ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IKeybindings } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; export const CONTEXT_FIND_WIDGET_VISIBLE = new RawContextKey('findWidgetVisible', false); -export const CONTEXT_FIND_WIDGET_NOT_VISIBLE: ContextKeyExpr = CONTEXT_FIND_WIDGET_VISIBLE.toNegated(); +export const CONTEXT_FIND_WIDGET_NOT_VISIBLE = CONTEXT_FIND_WIDGET_VISIBLE.toNegated(); // Keep ContextKey use of 'Focussed' to not break when clauses export const CONTEXT_FIND_INPUT_FOCUSED = new RawContextKey('findInputFocussed', false); export const CONTEXT_REPLACE_INPUT_FOCUSED = new RawContextKey('replaceInputFocussed', false); diff --git a/src/vs/editor/contrib/folding/folding.ts b/src/vs/editor/contrib/folding/folding.ts index c2d01a5ce0e..e80d1f68e5f 100644 --- a/src/vs/editor/contrib/folding/folding.ts +++ b/src/vs/editor/contrib/folding/folding.ts @@ -62,7 +62,7 @@ export class FoldingController extends Disposable implements IEditorContribution private readonly editor: ICodeEditor; private _isEnabled: boolean; private _useFoldingProviders: boolean; - private _unfoldOnClickInEmptyContent: boolean; + private _unfoldOnClickAfterEndOfLine: boolean; private readonly foldingDecorationProvider: FoldingDecorationProvider; @@ -92,7 +92,7 @@ export class FoldingController extends Disposable implements IEditorContribution const options = this.editor.getOptions(); this._isEnabled = options.get(EditorOption.folding); this._useFoldingProviders = options.get(EditorOption.foldingStrategy) !== 'indentation'; - this._unfoldOnClickInEmptyContent = options.get(EditorOption.unfoldOnClickInEmptyContent); + this._unfoldOnClickAfterEndOfLine = options.get(EditorOption.unfoldOnClickAfterEndOfLine); this.foldingModel = null; this.hiddenRangeModel = null; @@ -114,8 +114,7 @@ export class FoldingController extends Disposable implements IEditorContribution this._register(this.editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => { if (e.hasChanged(EditorOption.folding)) { - const options = this.editor.getOptions(); - this._isEnabled = options.get(EditorOption.folding); + this._isEnabled = this.editor.getOptions().get(EditorOption.folding); this.foldingEnabled.set(this._isEnabled); this.onModelChanged(); } @@ -126,12 +125,11 @@ export class FoldingController extends Disposable implements IEditorContribution this.onModelContentChanged(); } if (e.hasChanged(EditorOption.foldingStrategy)) { - const options = this.editor.getOptions(); - this._useFoldingProviders = options.get(EditorOption.foldingStrategy) !== 'indentation'; + this._useFoldingProviders = this.editor.getOptions().get(EditorOption.foldingStrategy) !== 'indentation'; this.onFoldingStrategyChanged(); } - if (e.hasChanged(EditorOption.unfoldOnClickInEmptyContent)) { - this._unfoldOnClickInEmptyContent = options.get(EditorOption.unfoldOnClickInEmptyContent); + if (e.hasChanged(EditorOption.unfoldOnClickAfterEndOfLine)) { + this._unfoldOnClickAfterEndOfLine = this.editor.getOptions().get(EditorOption.unfoldOnClickAfterEndOfLine); } })); this.onModelChanged(); @@ -370,7 +368,7 @@ export class FoldingController extends Disposable implements IEditorContribution iconClicked = true; break; case MouseTargetType.CONTENT_EMPTY: { - if (this._unfoldOnClickInEmptyContent && this.hiddenRangeModel.hasRanges()) { + if (this._unfoldOnClickAfterEndOfLine && this.hiddenRangeModel.hasRanges()) { const data = e.target.detail as IEmptyContentData; if (!data.isAfterLines) { break; diff --git a/src/vs/editor/contrib/folding/foldingModel.ts b/src/vs/editor/contrib/folding/foldingModel.ts index 917217b929b..e7b7858a855 100644 --- a/src/vs/editor/contrib/folding/foldingModel.ts +++ b/src/vs/editor/contrib/folding/foldingModel.ts @@ -318,7 +318,7 @@ export function setCollapseStateLevelsUp(foldingModel: FoldingModel, doCollapse: export function setCollapseStateUp(foldingModel: FoldingModel, doCollapse: boolean, lineNumbers: number[]): void { let toToggle: FoldingRegion[] = []; for (let lineNumber of lineNumbers) { - let regions = foldingModel.getAllRegionsAtLine(lineNumber, (region, ) => region.isCollapsed !== doCollapse); + let regions = foldingModel.getAllRegionsAtLine(lineNumber, (region,) => region.isCollapsed !== doCollapse); if (regions.length > 0) { toToggle.push(regions[0]); } diff --git a/src/vs/editor/contrib/gotoSymbol/link/clickLinkGesture.ts b/src/vs/editor/contrib/gotoSymbol/link/clickLinkGesture.ts index 003ed988f84..bc3e6cad236 100644 --- a/src/vs/editor/contrib/gotoSymbol/link/clickLinkGesture.ts +++ b/src/vs/editor/contrib/gotoSymbol/link/clickLinkGesture.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { KeyCode } from 'vs/base/common/keyCodes'; -import * as browser from 'vs/base/browser/browser'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { ICodeEditor, IEditorMouseEvent, IMouseTarget } from 'vs/editor/browser/editorBrowser'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -31,7 +30,7 @@ export class ClickLinkMouseEvent { this.target = source.target; this.hasTriggerModifier = hasModifier(source.event, opts.triggerModifier); this.hasSideBySideModifier = hasModifier(source.event, opts.triggerSideBySideModifier); - this.isNoneOrSingleMouseDown = (browser.isIE || source.event.detail <= 1); // IE does not support event.detail properly + this.isNoneOrSingleMouseDown = (source.event.detail <= 1); } } diff --git a/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.ts b/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.ts index a2ac8275f37..68c3275a7a4 100644 --- a/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.ts +++ b/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.ts @@ -338,9 +338,8 @@ export class GotoDefinitionAtPositionEditorContribution implements IEditorContri private gotoDefinition(position: Position, openToSide: boolean): Promise { this.editor.setPosition(position); - const definitionLinkOpensInPeek = this.editor.getOption(EditorOption.definitionLinkOpensInPeek); return this.editor.invokeWithinContext((accessor) => { - const canPeek = definitionLinkOpensInPeek && !this.isInPeekEditor(accessor); + const canPeek = !openToSide && this.editor.getOption(EditorOption.definitionLinkOpensInPeek) && !this.isInPeekEditor(accessor); const action = new DefinitionAction({ openToSide, openInPeek: canPeek, muteMessage: true }, { alias: '', label: '', id: '', precondition: undefined }); return action.run(accessor, this.editor); }); diff --git a/src/vs/editor/contrib/linesOperations/linesOperations.ts b/src/vs/editor/contrib/linesOperations/linesOperations.ts index c9bf8a68056..174302f90a1 100644 --- a/src/vs/editor/contrib/linesOperations/linesOperations.ts +++ b/src/vs/editor/contrib/linesOperations/linesOperations.ts @@ -960,7 +960,7 @@ export abstract class AbstractCaseAction extends EditorAction { let selection = selections[i]; if (selection.isEmpty()) { let cursor = selection.getStartPosition(); - let word = model.getWordAtPosition(cursor); + const word = editor.getConfiguredWordAtPosition(cursor); if (!word) { continue; diff --git a/src/vs/editor/contrib/links/getLinks.ts b/src/vs/editor/contrib/links/getLinks.ts index a85e7f8c942..0ee40d5166e 100644 --- a/src/vs/editor/contrib/links/getLinks.ts +++ b/src/vs/editor/contrib/links/getLinks.ts @@ -77,8 +77,8 @@ export class LinksList extends Disposable { const newLinks = list.links.map(link => new Link(link, provider)); links = LinksList._union(links, newLinks); // register disposables - if (isDisposable(provider)) { - this._register(provider); + if (isDisposable(list)) { + this._register(list); } } this.links = links; diff --git a/src/vs/editor/contrib/links/links.ts b/src/vs/editor/contrib/links/links.ts index 14b1b29fd01..9e3c455e31a 100644 --- a/src/vs/editor/contrib/links/links.ts +++ b/src/vs/editor/contrib/links/links.ts @@ -101,7 +101,7 @@ class LinkOccurrence { } } -class LinkDetector implements IEditorContribution { +export class LinkDetector implements IEditorContribution { public static readonly ID: string = 'editor.linkDetector'; diff --git a/src/vs/editor/contrib/multicursor/multicursor.ts b/src/vs/editor/contrib/multicursor/multicursor.ts index b23cc0c09f8..2388b709ff4 100644 --- a/src/vs/editor/contrib/multicursor/multicursor.ts +++ b/src/vs/editor/contrib/multicursor/multicursor.ts @@ -286,7 +286,7 @@ export class MultiCursorSession { if (s.isEmpty()) { // selection is empty => expand to current word - const word = editor.getModel().getWordAtPosition(s.getStartPosition()); + const word = editor.getConfiguredWordAtPosition(s.getStartPosition()); if (!word) { return null; } @@ -505,7 +505,7 @@ export class MultiCursorSelectionController extends Disposable implements IEdito if (!selection.isEmpty()) { return selection; } - const word = model.getWordAtPosition(selection.getStartPosition()); + const word = this._editor.getConfiguredWordAtPosition(selection.getStartPosition()); if (!word) { return selection; } diff --git a/src/vs/editor/contrib/peekView/peekView.ts b/src/vs/editor/contrib/peekView/peekView.ts index 32503e62570..83c36e037d7 100644 --- a/src/vs/editor/contrib/peekView/peekView.ts +++ b/src/vs/editor/contrib/peekView/peekView.ts @@ -17,7 +17,7 @@ import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; import { IOptions, IStyles, ZoneWidget } from 'vs/editor/contrib/zoneWidget/zoneWidget'; import * as nls from 'vs/nls'; -import { ContextKeyExpr, RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor, createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IDisposable } from 'vs/base/common/lifecycle'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -57,7 +57,7 @@ registerSingleton(IPeekViewService, class implements IPeekViewService { export namespace PeekContext { export const inPeekEditor = new RawContextKey('inReferenceSearchEditor', true); - export const notInPeekEditor: ContextKeyExpr = inPeekEditor.toNegated(); + export const notInPeekEditor = inPeekEditor.toNegated(); } class PeekContextController implements IEditorContribution { diff --git a/src/vs/editor/contrib/rename/rename.ts b/src/vs/editor/contrib/rename/rename.ts index 28aba650e6a..360986dd666 100644 --- a/src/vs/editor/contrib/rename/rename.ts +++ b/src/vs/editor/contrib/rename/rename.ts @@ -206,7 +206,8 @@ class RenameController implements IEditorContribution { this._bulkEditService.apply(renameResult, { editor: this.editor, showPreview: inputFieldResult.wantsPreview, - label: nls.localize('label', "Renaming '{0}'", loc?.text) + label: nls.localize('label', "Renaming '{0}'", loc?.text), + quotableLabel: nls.localize('quotableLabel', "Renaming {0}", loc?.text), }).then(result => { if (result.ariaSummary) { alert(nls.localize('aria', "Successfully renamed '{0}' to '{1}'. Summary: {2}", loc!.text, inputFieldResult.newName, result.ariaSummary)); diff --git a/src/vs/editor/contrib/suggest/media/suggest.css b/src/vs/editor/contrib/suggest/media/suggest.css index 821cb7d2623..be783ef5177 100644 --- a/src/vs/editor/contrib/suggest/media/suggest.css +++ b/src/vs/editor/contrib/suggest/media/suggest.css @@ -176,7 +176,7 @@ } .monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .left > .signature-label { - overflow: auto; + overflow: hidden; text-overflow: ellipsis; } @@ -228,13 +228,14 @@ .monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .left { flex-shrink: 1; + flex-grow: 1; overflow: hidden; } .monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .left > .monaco-icon-label { flex-shrink: 0; } .monaco-editor .suggest-widget .monaco-list .monaco-list-row:not(.string-label) > .contents > .main > .left > .monaco-icon-label { - max-width: 80%; + max-width: 100%; } .monaco-editor .suggest-widget .monaco-list .monaco-list-row.string-label > .contents > .main > .left > .monaco-icon-label { flex-shrink: 1; @@ -392,6 +393,10 @@ word-wrap: break-word; } +.monaco-editor .suggest-widget .details > .monaco-scrollable-element > .body > .docs.markdown-docs .codicon { + vertical-align: sub; +} + .monaco-editor .suggest-widget .details > .monaco-scrollable-element > .body > p:empty { display: none; } diff --git a/src/vs/editor/contrib/suggest/suggestWidget.ts b/src/vs/editor/contrib/suggest/suggestWidget.ts index 5baa237e29a..332f1dfb27d 100644 --- a/src/vs/editor/contrib/suggest/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/suggestWidget.ts @@ -144,11 +144,10 @@ class ItemRenderer implements IListRenderer | null = null; @@ -552,7 +552,7 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate toggleClass(this.element, 'with-status-bar', !this.editor.getOption(EditorOption.suggest).hideStatusBar); + const applyStatusBarStyle = () => toggleClass(this.element, 'with-status-bar', this.editor.getOption(EditorOption.suggest).statusBar.visible); applyStatusBarStyle(); this.statusBarElement = append(this.element, $('.suggest-status-bar')); @@ -645,9 +645,6 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate { @@ -811,6 +808,11 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate(); - public readonly onDidChangeFormatters: Event = this._onDidRegisterFormatter.event; + public readonly onDidChangeFormatters: Event = Event.None; public getUriLabel(resource: URI, options?: { relative?: boolean, forceNoTildify?: boolean }): string { if (resource.scheme === 'file') { diff --git a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts index b9793e2a86b..6ff1f85c8f7 100644 --- a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as browser from 'vs/base/browser/browser'; import * as aria from 'vs/base/browser/ui/aria/aria'; import { Disposable, IDisposable, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser'; @@ -233,11 +232,7 @@ export class StandaloneCodeEditor extends CodeEditorWidget implements IStandalon ) { options = options || {}; options.ariaLabel = options.ariaLabel || StandaloneCodeEditorNLS.editorViewAccessibleLabel; - options.ariaLabel = options.ariaLabel + ';' + ( - browser.isIE - ? StandaloneCodeEditorNLS.accessibilityHelpMessageIE - : StandaloneCodeEditorNLS.accessibilityHelpMessage - ); + options.ariaLabel = options.ariaLabel + ';' + (StandaloneCodeEditorNLS.accessibilityHelpMessage); super(domElement, options, {}, instantiationService, codeEditorService, commandService, contextKeyService, themeService, notificationService, accessibilityService); if (keybindingService instanceof StandaloneKeybindingService) { diff --git a/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts b/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts index ff7e6ef7fbc..f9e64360021 100644 --- a/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts +++ b/src/vs/editor/test/common/viewLayout/editorLayoutProvider.test.ts @@ -33,7 +33,7 @@ interface IEditorLayoutProviderOpts { readonly minimapSide: 'left' | 'right'; readonly minimapRenderCharacters: boolean; readonly minimapMaxColumn: number; - minimapMode?: 'actual' | 'cover' | 'contain'; + minimapSize?: 'proportional' | 'fill' | 'fit'; readonly pixelRatio: number; } @@ -47,7 +47,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { options._write(EditorOption.folding, false); const minimapOptions: EditorMinimapOptions = { enabled: input.minimap, - mode: input.minimapMode || 'actual', + size: input.minimapSize || 'proportional', side: input.minimapSide, renderCharacters: input.minimapRenderCharacters, maxColumn: input.minimapMaxColumn, @@ -978,7 +978,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { minimapSide: 'right', minimapRenderCharacters: true, minimapMaxColumn: 150, - minimapMode: 'cover', + minimapSize: 'fill', pixelRatio: 2, }, { width: 1000, @@ -1042,7 +1042,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { minimapSide: 'right', minimapRenderCharacters: true, minimapMaxColumn: 150, - minimapMode: 'cover', + minimapSize: 'fill', pixelRatio: 2, }, { width: 1000, @@ -1106,7 +1106,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { minimapSide: 'right', minimapRenderCharacters: true, minimapMaxColumn: 150, - minimapMode: 'contain', + minimapSize: 'fit', pixelRatio: 2, }, { width: 1000, @@ -1170,7 +1170,7 @@ suite('Editor ViewLayout - EditorLayoutProvider', () => { minimapSide: 'right', minimapRenderCharacters: true, minimapMaxColumn: 150, - minimapMode: 'contain', + minimapSize: 'fit', pixelRatio: 2, }, { width: 1000, diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 65f7b8f4e4f..ccdfcfcece0 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -381,7 +381,6 @@ declare namespace monaco { */ MAX_VALUE = 112 } - export class KeyMod { static readonly CtrlCmd: number; static readonly Shift: number; @@ -3035,7 +3034,7 @@ declare namespace monaco.editor { * Controls whether clicking on the empty content after a folded line will unfold the line. * Defaults to false. */ - unfoldOnClickInEmptyContent?: boolean; + unfoldOnClickAfterEndOfLine?: boolean; /** * Enable highlighting of matching brackets. * Defaults to 'always'. @@ -3444,7 +3443,7 @@ declare namespace monaco.editor { * Control the minimap rendering mode. * Defaults to 'actual'. */ - mode?: 'actual' | 'cover' | 'contain'; + size?: 'proportional' | 'fill' | 'fit'; /** * Control the rendering of the minimap slider. * Defaults to 'mouseover'. @@ -3754,9 +3753,14 @@ declare namespace monaco.editor { */ showSnippets?: boolean; /** - * Controls the visibility of the status bar at the bottom of the suggest widget. + * Status bar related settings. */ - hideStatusBar?: boolean; + statusBar?: { + /** + * Controls the visibility of the status bar at the bottom of the suggest widget. + */ + visible?: boolean; + }; } export type InternalSuggestOptions = Readonly>; @@ -3825,7 +3829,7 @@ declare namespace monaco.editor { folding = 31, foldingStrategy = 32, foldingHighlight = 33, - unfoldOnClickInEmptyContent = 34, + unfoldOnClickAfterEndOfLine = 34, fontFamily = 35, fontInfo = 36, fontLigatures = 37, @@ -3941,7 +3945,7 @@ declare namespace monaco.editor { folding: IEditorOption; foldingStrategy: IEditorOption; foldingHighlight: IEditorOption; - unfoldOnClickInEmptyContent: IEditorOption; + unfoldOnClickAfterEndOfLine: IEditorOption; fontFamily: IEditorOption; fontInfo: IEditorOption; fontLigatures2: IEditorOption; @@ -5447,7 +5451,7 @@ declare namespace monaco.languages { preselect?: boolean; /** * A string or snippet that should be inserted in a document when selecting - * this completion. When `falsy` the [label](#CompletionItem.label) + * this completion. * is used. */ insertText: string; @@ -6031,11 +6035,11 @@ declare namespace monaco.languages { } /** - * A provider of colors for editor models. + * A provider of folding ranges for editor models. */ export interface FoldingRangeProvider { /** - * Provides the color ranges for a specific model. + * Provides the folding ranges for a specific model. */ provideFoldingRanges(model: editor.ITextModel, context: FoldingContext, token: CancellationToken): ProviderResult; } @@ -6178,6 +6182,7 @@ declare namespace monaco.languages { } export interface DocumentSemanticTokensProvider { + onDidChange?: IEvent; getLegend(): SemanticTokensLegend; provideDocumentSemanticTokens(model: editor.ITextModel, lastResultId: string | null, token: CancellationToken): ProviderResult; releaseDocumentSemanticTokens(resultId: string | undefined): void; diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index ffef4c6d2d4..72dc61aa5af 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -7,7 +7,7 @@ import { Action } from 'vs/base/common/actions'; import { SyncDescriptor0, createSyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IConstructorSignature2, createDecorator, BrandedService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindings, KeybindingsRegistry, IKeybindingRule } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, IContextKeyService, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { ICommandService, CommandsRegistry, ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; @@ -25,8 +25,8 @@ export interface ICommandAction { title: string | ILocalizedString; category?: string | ILocalizedString; icon?: { dark?: URI; light?: URI; } | ThemeIcon; - precondition?: ContextKeyExpr; - toggled?: ContextKeyExpr; + precondition?: ContextKeyExpression; + toggled?: ContextKeyExpression; } export type ISerializableCommandAction = UriDto; @@ -34,7 +34,7 @@ export type ISerializableCommandAction = UriDto; export interface IMenuItem { command: ICommandAction; alt?: ICommandAction; - when?: ContextKeyExpr; + when?: ContextKeyExpression; group?: 'navigation' | string; order?: number; } @@ -42,7 +42,7 @@ export interface IMenuItem { export interface ISubmenuItem { title: string | ILocalizedString; submenu: MenuId; - when?: ContextKeyExpr; + when?: ContextKeyExpression; group?: 'navigation' | string; order?: number; } @@ -314,17 +314,17 @@ export class SyncActionDescriptor { private readonly _id: string; private readonly _label?: string; private readonly _keybindings: IKeybindings | undefined; - private readonly _keybindingContext: ContextKeyExpr | undefined; + private readonly _keybindingContext: ContextKeyExpression | undefined; private readonly _keybindingWeight: number | undefined; public static create(ctor: { new(id: string, label: string, ...services: Services): Action }, - id: string, label: string | undefined, keybindings?: IKeybindings, keybindingContext?: ContextKeyExpr, keybindingWeight?: number + id: string, label: string | undefined, keybindings?: IKeybindings, keybindingContext?: ContextKeyExpression, keybindingWeight?: number ): SyncActionDescriptor { return new SyncActionDescriptor(ctor as IConstructorSignature2, id, label, keybindings, keybindingContext, keybindingWeight); } private constructor(ctor: IConstructorSignature2, - id: string, label: string | undefined, keybindings?: IKeybindings, keybindingContext?: ContextKeyExpr, keybindingWeight?: number + id: string, label: string | undefined, keybindings?: IKeybindings, keybindingContext?: ContextKeyExpression, keybindingWeight?: number ) { this._id = id; this._label = label; @@ -350,7 +350,7 @@ export class SyncActionDescriptor { return this._keybindings; } - public get keybindingContext(): ContextKeyExpr | undefined { + public get keybindingContext(): ContextKeyExpression | undefined { return this._keybindingContext; } diff --git a/src/vs/platform/actions/common/menuService.ts b/src/vs/platform/actions/common/menuService.ts index fac16754098..2bfa84d15eb 100644 --- a/src/vs/platform/actions/common/menuService.ts +++ b/src/vs/platform/actions/common/menuService.ts @@ -7,7 +7,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IMenu, IMenuActionOptions, IMenuItem, IMenuService, isIMenuItem, ISubmenuItem, MenuId, MenuItemAction, MenuRegistry, SubmenuItemAction, ILocalizedString } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { ContextKeyExpr, IContextKeyService, IContextKeyChangeEvent } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, IContextKeyChangeEvent, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; export class MenuService implements IMenuService { @@ -125,7 +125,7 @@ class Menu implements IMenu { return result; } - private static _fillInKbExprKeys(exp: ContextKeyExpr | undefined, set: Set): void { + private static _fillInKbExprKeys(exp: ContextKeyExpression | undefined, set: Set): void { if (exp) { for (let key of exp.keys()) { set.add(key); diff --git a/src/vs/platform/contextkey/browser/contextKeyService.ts b/src/vs/platform/contextkey/browser/contextKeyService.ts index 6e033c32c6a..58d6e846e52 100644 --- a/src/vs/platform/contextkey/browser/contextKeyService.ts +++ b/src/vs/platform/contextkey/browser/contextKeyService.ts @@ -8,7 +8,7 @@ import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { keys } from 'vs/base/common/map'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ContextKeyExpr, IContext, IContextKey, IContextKeyChangeEvent, IContextKeyService, IContextKeyServiceTarget, IReadableSet, SET_CONTEXT_COMMAND_ID } from 'vs/platform/contextkey/common/contextkey'; +import { IContext, IContextKey, IContextKeyChangeEvent, IContextKeyService, IContextKeyServiceTarget, IReadableSet, SET_CONTEXT_COMMAND_ID, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingResolver } from 'vs/platform/keybinding/common/keybindingResolver'; const KEYBINDING_CONTEXT_ATTR = 'data-keybinding-context'; @@ -265,7 +265,7 @@ export abstract class AbstractContextKeyService implements IContextKeyService { return new ScopedContextKeyService(this, domNode); } - public contextMatchesRules(rules: ContextKeyExpr | undefined): boolean { + public contextMatchesRules(rules: ContextKeyExpression | undefined): boolean { if (this._isDisposed) { throw new Error(`AbstractContextKeyService has been disposed`); } diff --git a/src/vs/platform/contextkey/common/contextkey.ts b/src/vs/platform/contextkey/common/contextkey.ts index 784f586779a..414b6a3d3c0 100644 --- a/src/vs/platform/contextkey/common/contextkey.ts +++ b/src/vs/platform/contextkey/common/contextkey.ts @@ -6,57 +6,94 @@ import { Event } from 'vs/base/common/event'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { isMacintosh, isLinux, isWindows, isWeb } from 'vs/base/common/platform'; + +const STATIC_VALUES = new Map(); +STATIC_VALUES.set('false', false); +STATIC_VALUES.set('true', true); +STATIC_VALUES.set('isMac', isMacintosh); +STATIC_VALUES.set('isLinux', isLinux); +STATIC_VALUES.set('isWindows', isWindows); +STATIC_VALUES.set('isWeb', isWeb); +STATIC_VALUES.set('isMacNative', isMacintosh && !isWeb); export const enum ContextKeyExprType { - Defined = 1, - Not = 2, - Equals = 3, - NotEquals = 4, - And = 5, - Regex = 6, - NotRegex = 7, - Or = 8 + False = 0, + True = 1, + Defined = 2, + Not = 3, + Equals = 4, + NotEquals = 5, + And = 6, + Regex = 7, + NotRegex = 8, + Or = 9 } export interface IContextKeyExprMapper { - mapDefined(key: string): ContextKeyExpr; - mapNot(key: string): ContextKeyExpr; - mapEquals(key: string, value: any): ContextKeyExpr; - mapNotEquals(key: string, value: any): ContextKeyExpr; + mapDefined(key: string): ContextKeyExpression; + mapNot(key: string): ContextKeyExpression; + mapEquals(key: string, value: any): ContextKeyExpression; + mapNotEquals(key: string, value: any): ContextKeyExpression; mapRegex(key: string, regexp: RegExp | null): ContextKeyRegexExpr; } +export interface IContextKeyExpression { + cmp(other: ContextKeyExpression): number; + equals(other: ContextKeyExpression): boolean; + evaluate(context: IContext): boolean; + serialize(): string; + keys(): string[]; + map(mapFnc: IContextKeyExprMapper): ContextKeyExpression; + negate(): ContextKeyExpression; + +} + +export type ContextKeyExpression = ( + ContextKeyFalseExpr | ContextKeyTrueExpr | ContextKeyDefinedExpr | ContextKeyNotExpr + | ContextKeyEqualsExpr | ContextKeyNotEqualsExpr | ContextKeyRegexExpr + | ContextKeyNotRegexExpr | ContextKeyAndExpr | ContextKeyOrExpr +); + export abstract class ContextKeyExpr { - public static has(key: string): ContextKeyExpr { + public static false(): ContextKeyExpression { + return ContextKeyFalseExpr.INSTANCE; + } + + public static true(): ContextKeyExpression { + return ContextKeyTrueExpr.INSTANCE; + } + + public static has(key: string): ContextKeyExpression { return ContextKeyDefinedExpr.create(key); } - public static equals(key: string, value: any): ContextKeyExpr { + public static equals(key: string, value: any): ContextKeyExpression { return ContextKeyEqualsExpr.create(key, value); } - public static notEquals(key: string, value: any): ContextKeyExpr { + public static notEquals(key: string, value: any): ContextKeyExpression { return ContextKeyNotEqualsExpr.create(key, value); } - public static regex(key: string, value: RegExp): ContextKeyExpr { + public static regex(key: string, value: RegExp): ContextKeyExpression { return ContextKeyRegexExpr.create(key, value); } - public static not(key: string): ContextKeyExpr { + public static not(key: string): ContextKeyExpression { return ContextKeyNotExpr.create(key); } - public static and(...expr: Array): ContextKeyExpr | undefined { + public static and(...expr: Array): ContextKeyExpression | undefined { return ContextKeyAndExpr.create(expr); } - public static or(...expr: Array): ContextKeyExpr | undefined { + public static or(...expr: Array): ContextKeyExpression | undefined { return ContextKeyOrExpr.create(expr); } - public static deserialize(serialized: string | null | undefined, strict: boolean = false): ContextKeyExpr | undefined { + public static deserialize(serialized: string | null | undefined, strict: boolean = false): ContextKeyExpression | undefined { if (!serialized) { return undefined; } @@ -64,17 +101,17 @@ export abstract class ContextKeyExpr { return this._deserializeOrExpression(serialized, strict); } - private static _deserializeOrExpression(serialized: string, strict: boolean): ContextKeyExpr | undefined { + private static _deserializeOrExpression(serialized: string, strict: boolean): ContextKeyExpression | undefined { let pieces = serialized.split('||'); return ContextKeyOrExpr.create(pieces.map(p => this._deserializeAndExpression(p, strict))); } - private static _deserializeAndExpression(serialized: string, strict: boolean): ContextKeyExpr | undefined { + private static _deserializeAndExpression(serialized: string, strict: boolean): ContextKeyExpression | undefined { let pieces = serialized.split('&&'); return ContextKeyAndExpr.create(pieces.map(p => this._deserializeOne(p, strict))); } - private static _deserializeOne(serializedOne: string, strict: boolean): ContextKeyExpr { + private static _deserializeOne(serializedOne: string, strict: boolean): ContextKeyExpression { serializedOne = serializedOne.trim(); if (serializedOne.indexOf('!=') >= 0) { @@ -153,55 +190,104 @@ export abstract class ContextKeyExpr { return null; } } - - public abstract getType(): ContextKeyExprType; - public abstract equals(other: ContextKeyExpr): boolean; - public abstract evaluate(context: IContext): boolean; - public abstract serialize(): string; - public abstract keys(): string[]; - public abstract map(mapFnc: IContextKeyExprMapper): ContextKeyExpr; - public abstract negate(): ContextKeyExpr; } -function cmp(a: ContextKeyExpr, b: ContextKeyExpr): number { - let aType = a.getType(); - let bType = b.getType(); - if (aType !== bType) { - return aType - bType; +function cmp(a: ContextKeyExpression, b: ContextKeyExpression): number { + return a.cmp(b); +} + +export class ContextKeyFalseExpr implements IContextKeyExpression { + public static INSTANCE = new ContextKeyFalseExpr(); + + public readonly type = ContextKeyExprType.False; + + protected constructor() { } - switch (aType) { - case ContextKeyExprType.Defined: - return (a).cmp(b); - case ContextKeyExprType.Not: - return (a).cmp(b); - case ContextKeyExprType.Equals: - return (a).cmp(b); - case ContextKeyExprType.NotEquals: - return (a).cmp(b); - case ContextKeyExprType.Regex: - return (a).cmp(b); - case ContextKeyExprType.NotRegex: - return (a).cmp(b); - case ContextKeyExprType.And: - return (a).cmp(b); - default: - throw new Error('Unknown ContextKeyExpr!'); + + public cmp(other: ContextKeyExpression): number { + return this.type - other.type; + } + + public equals(other: ContextKeyExpression): boolean { + return (other.type === this.type); + } + + public evaluate(context: IContext): boolean { + return false; + } + + public serialize(): string { + return 'false'; + } + + public keys(): string[] { + return []; + } + + public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression { + return this; + } + + public negate(): ContextKeyExpression { + return ContextKeyTrueExpr.INSTANCE; } } -export class ContextKeyDefinedExpr implements ContextKeyExpr { - public static create(key: string): ContextKeyDefinedExpr { +export class ContextKeyTrueExpr implements IContextKeyExpression { + public static INSTANCE = new ContextKeyTrueExpr(); + + public readonly type = ContextKeyExprType.True; + + protected constructor() { + } + + public cmp(other: ContextKeyExpression): number { + return this.type - other.type; + } + + public equals(other: ContextKeyExpression): boolean { + return (other.type === this.type); + } + + public evaluate(context: IContext): boolean { + return true; + } + + public serialize(): string { + return 'true'; + } + + public keys(): string[] { + return []; + } + + public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression { + return this; + } + + public negate(): ContextKeyExpression { + return ContextKeyFalseExpr.INSTANCE; + } +} + +export class ContextKeyDefinedExpr implements IContextKeyExpression { + public static create(key: string): ContextKeyExpression { + const staticValue = STATIC_VALUES.get(key); + if (typeof staticValue === 'boolean') { + return staticValue ? ContextKeyTrueExpr.INSTANCE : ContextKeyFalseExpr.INSTANCE; + } return new ContextKeyDefinedExpr(key); } - protected constructor(protected key: string) { + public readonly type = ContextKeyExprType.Defined; + + protected constructor(protected readonly key: string) { } - public getType(): ContextKeyExprType { - return ContextKeyExprType.Defined; - } - - public cmp(other: ContextKeyDefinedExpr): number { + public cmp(other: ContextKeyExpression): number { + if (other.type !== this.type) { + return this.type - other.type; + } if (this.key < other.key) { return -1; } @@ -211,8 +297,8 @@ export class ContextKeyDefinedExpr implements ContextKeyExpr { return 0; } - public equals(other: ContextKeyExpr): boolean { - if (other instanceof ContextKeyDefinedExpr) { + public equals(other: ContextKeyExpression): boolean { + if (other.type === this.type) { return (this.key === other.key); } return false; @@ -230,35 +316,38 @@ export class ContextKeyDefinedExpr implements ContextKeyExpr { return [this.key]; } - public map(mapFnc: IContextKeyExprMapper): ContextKeyExpr { + public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression { return mapFnc.mapDefined(this.key); } - public negate(): ContextKeyExpr { + public negate(): ContextKeyExpression { return ContextKeyNotExpr.create(this.key); } } -export class ContextKeyEqualsExpr implements ContextKeyExpr { +export class ContextKeyEqualsExpr implements IContextKeyExpression { - public static create(key: string, value: any): ContextKeyExpr { + public static create(key: string, value: any): ContextKeyExpression { if (typeof value === 'boolean') { - if (value) { - return ContextKeyDefinedExpr.create(key); - } - return ContextKeyNotExpr.create(key); + return (value ? ContextKeyDefinedExpr.create(key) : ContextKeyNotExpr.create(key)); + } + const staticValue = STATIC_VALUES.get(key); + if (typeof staticValue === 'boolean') { + const trueValue = staticValue ? 'true' : 'false'; + return (value === trueValue ? ContextKeyTrueExpr.INSTANCE : ContextKeyFalseExpr.INSTANCE); } return new ContextKeyEqualsExpr(key, value); } + public readonly type = ContextKeyExprType.Equals; + private constructor(private readonly key: string, private readonly value: any) { } - public getType(): ContextKeyExprType { - return ContextKeyExprType.Equals; - } - - public cmp(other: ContextKeyEqualsExpr): number { + public cmp(other: ContextKeyExpression): number { + if (other.type !== this.type) { + return this.type - other.type; + } if (this.key < other.key) { return -1; } @@ -274,8 +363,8 @@ export class ContextKeyEqualsExpr implements ContextKeyExpr { return 0; } - public equals(other: ContextKeyExpr): boolean { - if (other instanceof ContextKeyEqualsExpr) { + public equals(other: ContextKeyExpression): boolean { + if (other.type === this.type) { return (this.key === other.key && this.value === other.value); } return false; @@ -295,35 +384,41 @@ export class ContextKeyEqualsExpr implements ContextKeyExpr { return [this.key]; } - public map(mapFnc: IContextKeyExprMapper): ContextKeyExpr { + public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression { return mapFnc.mapEquals(this.key, this.value); } - public negate(): ContextKeyExpr { + public negate(): ContextKeyExpression { return ContextKeyNotEqualsExpr.create(this.key, this.value); } } -export class ContextKeyNotEqualsExpr implements ContextKeyExpr { +export class ContextKeyNotEqualsExpr implements IContextKeyExpression { - public static create(key: string, value: any): ContextKeyExpr { + public static create(key: string, value: any): ContextKeyExpression { if (typeof value === 'boolean') { if (value) { return ContextKeyNotExpr.create(key); } return ContextKeyDefinedExpr.create(key); } + const staticValue = STATIC_VALUES.get(key); + if (typeof staticValue === 'boolean') { + const falseValue = staticValue ? 'true' : 'false'; + return (value === falseValue ? ContextKeyFalseExpr.INSTANCE : ContextKeyTrueExpr.INSTANCE); + } return new ContextKeyNotEqualsExpr(key, value); } - private constructor(private key: string, private value: any) { + public readonly type = ContextKeyExprType.NotEquals; + + private constructor(private readonly key: string, private readonly value: any) { } - public getType(): ContextKeyExprType { - return ContextKeyExprType.NotEquals; - } - - public cmp(other: ContextKeyNotEqualsExpr): number { + public cmp(other: ContextKeyExpression): number { + if (other.type !== this.type) { + return this.type - other.type; + } if (this.key < other.key) { return -1; } @@ -339,8 +434,8 @@ export class ContextKeyNotEqualsExpr implements ContextKeyExpr { return 0; } - public equals(other: ContextKeyExpr): boolean { - if (other instanceof ContextKeyNotEqualsExpr) { + public equals(other: ContextKeyExpression): boolean { + if (other.type === this.type) { return (this.key === other.key && this.value === other.value); } return false; @@ -360,29 +455,34 @@ export class ContextKeyNotEqualsExpr implements ContextKeyExpr { return [this.key]; } - public map(mapFnc: IContextKeyExprMapper): ContextKeyExpr { + public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression { return mapFnc.mapNotEquals(this.key, this.value); } - public negate(): ContextKeyExpr { + public negate(): ContextKeyExpression { return ContextKeyEqualsExpr.create(this.key, this.value); } } -export class ContextKeyNotExpr implements ContextKeyExpr { +export class ContextKeyNotExpr implements IContextKeyExpression { - public static create(key: string): ContextKeyExpr { + public static create(key: string): ContextKeyExpression { + const staticValue = STATIC_VALUES.get(key); + if (typeof staticValue === 'boolean') { + return (staticValue ? ContextKeyFalseExpr.INSTANCE : ContextKeyTrueExpr.INSTANCE); + } return new ContextKeyNotExpr(key); } - private constructor(private key: string) { + public readonly type = ContextKeyExprType.Not; + + private constructor(private readonly key: string) { } - public getType(): ContextKeyExprType { - return ContextKeyExprType.Not; - } - - public cmp(other: ContextKeyNotExpr): number { + public cmp(other: ContextKeyExpression): number { + if (other.type !== this.type) { + return this.type - other.type; + } if (this.key < other.key) { return -1; } @@ -392,8 +492,8 @@ export class ContextKeyNotExpr implements ContextKeyExpr { return 0; } - public equals(other: ContextKeyExpr): boolean { - if (other instanceof ContextKeyNotExpr) { + public equals(other: ContextKeyExpression): boolean { + if (other.type === this.type) { return (this.key === other.key); } return false; @@ -411,30 +511,31 @@ export class ContextKeyNotExpr implements ContextKeyExpr { return [this.key]; } - public map(mapFnc: IContextKeyExprMapper): ContextKeyExpr { + public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression { return mapFnc.mapNot(this.key); } - public negate(): ContextKeyExpr { + public negate(): ContextKeyExpression { return ContextKeyDefinedExpr.create(this.key); } } -export class ContextKeyRegexExpr implements ContextKeyExpr { +export class ContextKeyRegexExpr implements IContextKeyExpression { public static create(key: string, regexp: RegExp | null): ContextKeyRegexExpr { return new ContextKeyRegexExpr(key, regexp); } - private constructor(private key: string, private regexp: RegExp | null) { + public readonly type = ContextKeyExprType.Regex; + + private constructor(private readonly key: string, private readonly regexp: RegExp | null) { // } - public getType(): ContextKeyExprType { - return ContextKeyExprType.Regex; - } - - public cmp(other: ContextKeyRegexExpr): number { + public cmp(other: ContextKeyExpression): number { + if (other.type !== this.type) { + return this.type - other.type; + } if (this.key < other.key) { return -1; } @@ -452,8 +553,8 @@ export class ContextKeyRegexExpr implements ContextKeyExpr { return 0; } - public equals(other: ContextKeyExpr): boolean { - if (other instanceof ContextKeyRegexExpr) { + public equals(other: ContextKeyExpression): boolean { + if (other.type === this.type) { const thisSource = this.regexp ? this.regexp.source : ''; const otherSource = other.regexp ? other.regexp.source : ''; return (this.key === other.key && thisSource === otherSource); @@ -481,31 +582,32 @@ export class ContextKeyRegexExpr implements ContextKeyExpr { return mapFnc.mapRegex(this.key, this.regexp); } - public negate(): ContextKeyExpr { + public negate(): ContextKeyExpression { return ContextKeyNotRegexExpr.create(this); } } -export class ContextKeyNotRegexExpr implements ContextKeyExpr { +export class ContextKeyNotRegexExpr implements IContextKeyExpression { - public static create(actual: ContextKeyRegexExpr): ContextKeyExpr { + public static create(actual: ContextKeyRegexExpr): ContextKeyExpression { return new ContextKeyNotRegexExpr(actual); } + public readonly type = ContextKeyExprType.NotRegex; + private constructor(private readonly _actual: ContextKeyRegexExpr) { // } - public getType(): ContextKeyExprType { - return ContextKeyExprType.NotRegex; - } - - public cmp(other: ContextKeyNotRegexExpr): number { + public cmp(other: ContextKeyExpression): number { + if (other.type !== this.type) { + return this.type - other.type; + } return this._actual.cmp(other._actual); } - public equals(other: ContextKeyExpr): boolean { - if (other instanceof ContextKeyNotRegexExpr) { + public equals(other: ContextKeyExpression): boolean { + if (other.type === this.type) { return this._actual.equals(other._actual); } return false; @@ -523,18 +625,18 @@ export class ContextKeyNotRegexExpr implements ContextKeyExpr { return this._actual.keys(); } - public map(mapFnc: IContextKeyExprMapper): ContextKeyExpr { + public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression { return new ContextKeyNotRegexExpr(this._actual.map(mapFnc)); } - public negate(): ContextKeyExpr { + public negate(): ContextKeyExpression { return this._actual; } } -export class ContextKeyAndExpr implements ContextKeyExpr { +export class ContextKeyAndExpr implements IContextKeyExpression { - public static create(_expr: ReadonlyArray): ContextKeyExpr | undefined { + public static create(_expr: ReadonlyArray): ContextKeyExpression | undefined { const expr = ContextKeyAndExpr._normalizeArr(_expr); if (expr.length === 0) { return undefined; @@ -547,14 +649,15 @@ export class ContextKeyAndExpr implements ContextKeyExpr { return new ContextKeyAndExpr(expr); } - private constructor(public readonly expr: ContextKeyExpr[]) { + public readonly type = ContextKeyExprType.And; + + private constructor(public readonly expr: ContextKeyExpression[]) { } - public getType(): ContextKeyExprType { - return ContextKeyExprType.And; - } - - public cmp(other: ContextKeyAndExpr): number { + public cmp(other: ContextKeyExpression): number { + if (other.type !== this.type) { + return this.type - other.type; + } if (this.expr.length < other.expr.length) { return -1; } @@ -570,8 +673,8 @@ export class ContextKeyAndExpr implements ContextKeyExpr { return 0; } - public equals(other: ContextKeyExpr): boolean { - if (other instanceof ContextKeyAndExpr) { + public equals(other: ContextKeyExpression): boolean { + if (other.type === this.type) { if (this.expr.length !== other.expr.length) { return false; } @@ -594,20 +697,32 @@ export class ContextKeyAndExpr implements ContextKeyExpr { return true; } - private static _normalizeArr(arr: ReadonlyArray): ContextKeyExpr[] { - const expr: ContextKeyExpr[] = []; + private static _normalizeArr(arr: ReadonlyArray): ContextKeyExpression[] { + const expr: ContextKeyExpression[] = []; + let hasTrue = false; for (const e of arr) { if (!e) { continue; } - if (e instanceof ContextKeyAndExpr) { + if (e.type === ContextKeyExprType.True) { + // anything && true ==> anything + hasTrue = true; + continue; + } + + if (e.type === ContextKeyExprType.False) { + // anything && false ==> false + return [ContextKeyFalseExpr.INSTANCE]; + } + + if (e.type === ContextKeyExprType.And) { expr.push(...e.expr); continue; } - if (e instanceof ContextKeyOrExpr) { + if (e.type === ContextKeyExprType.Or) { // Not allowed, because we don't have parens! throw new Error(`It is not allowed to have an or expression here due to lack of parens! For example "a && (b||c)" is not supported, use "(a&&b) || (a&&c)" instead.`); } @@ -615,6 +730,10 @@ export class ContextKeyAndExpr implements ContextKeyExpr { expr.push(e); } + if (expr.length === 0 && hasTrue) { + return [ContextKeyTrueExpr.INSTANCE]; + } + expr.sort(cmp); return expr; @@ -632,12 +751,12 @@ export class ContextKeyAndExpr implements ContextKeyExpr { return result; } - public map(mapFnc: IContextKeyExprMapper): ContextKeyExpr { + public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression { return new ContextKeyAndExpr(this.expr.map(expr => expr.map(mapFnc))); } - public negate(): ContextKeyExpr { - let result: ContextKeyExpr[] = []; + public negate(): ContextKeyExpression { + let result: ContextKeyExpression[] = []; for (let expr of this.expr) { result.push(expr.negate()); } @@ -645,9 +764,9 @@ export class ContextKeyAndExpr implements ContextKeyExpr { } } -export class ContextKeyOrExpr implements ContextKeyExpr { +export class ContextKeyOrExpr implements IContextKeyExpression { - public static create(_expr: ReadonlyArray): ContextKeyExpr | undefined { + public static create(_expr: ReadonlyArray): ContextKeyExpression | undefined { const expr = ContextKeyOrExpr._normalizeArr(_expr); if (expr.length === 0) { return undefined; @@ -660,15 +779,32 @@ export class ContextKeyOrExpr implements ContextKeyExpr { return new ContextKeyOrExpr(expr); } - private constructor(public readonly expr: ContextKeyExpr[]) { + public readonly type = ContextKeyExprType.Or; + + private constructor(public readonly expr: ContextKeyExpression[]) { } - public getType(): ContextKeyExprType { - return ContextKeyExprType.Or; + public cmp(other: ContextKeyExpression): number { + if (other.type !== this.type) { + return this.type - other.type; + } + if (this.expr.length < other.expr.length) { + return -1; + } + if (this.expr.length > other.expr.length) { + return 1; + } + for (let i = 0, len = this.expr.length; i < len; i++) { + const r = cmp(this.expr[i], other.expr[i]); + if (r !== 0) { + return r; + } + } + return 0; } - public equals(other: ContextKeyExpr): boolean { - if (other instanceof ContextKeyOrExpr) { + public equals(other: ContextKeyExpression): boolean { + if (other.type === this.type) { if (this.expr.length !== other.expr.length) { return false; } @@ -691,17 +827,29 @@ export class ContextKeyOrExpr implements ContextKeyExpr { return false; } - private static _normalizeArr(arr: ReadonlyArray): ContextKeyExpr[] { - let expr: ContextKeyExpr[] = []; + private static _normalizeArr(arr: ReadonlyArray): ContextKeyExpression[] { + let expr: ContextKeyExpression[] = []; + let hasFalse = false; if (arr) { for (let i = 0, len = arr.length; i < len; i++) { - let e: ContextKeyExpr | null | undefined = arr[i]; + const e = arr[i]; if (!e) { continue; } - if (e instanceof ContextKeyOrExpr) { + if (e.type === ContextKeyExprType.False) { + // anything || false ==> anything + hasFalse = true; + continue; + } + + if (e.type === ContextKeyExprType.True) { + // anything || true ==> true + return [ContextKeyTrueExpr.INSTANCE]; + } + + if (e.type === ContextKeyExprType.Or) { expr = expr.concat(e.expr); continue; } @@ -709,6 +857,10 @@ export class ContextKeyOrExpr implements ContextKeyExpr { expr.push(e); } + if (expr.length === 0 && hasFalse) { + return [ContextKeyFalseExpr.INSTANCE]; + } + expr.sort(cmp); } @@ -727,18 +879,18 @@ export class ContextKeyOrExpr implements ContextKeyExpr { return result; } - public map(mapFnc: IContextKeyExprMapper): ContextKeyExpr { + public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression { return new ContextKeyOrExpr(this.expr.map(expr => expr.map(mapFnc))); } - public negate(): ContextKeyExpr { - let result: ContextKeyExpr[] = []; + public negate(): ContextKeyExpression { + let result: ContextKeyExpression[] = []; for (let expr of this.expr) { result.push(expr.negate()); } - const terminals = (node: ContextKeyExpr) => { - if (node instanceof ContextKeyOrExpr) { + const terminals = (node: ContextKeyExpression) => { + if (node.type === ContextKeyExprType.Or) { return node.expr; } return [node]; @@ -750,7 +902,7 @@ export class ContextKeyOrExpr implements ContextKeyExpr { const LEFT = result.shift()!; const RIGHT = result.shift()!; - const all: ContextKeyExpr[] = []; + const all: ContextKeyExpression[] = []; for (const left of terminals(LEFT)) { for (const right of terminals(RIGHT)) { all.push(ContextKeyExpr.and(left, right)!); @@ -765,7 +917,7 @@ export class ContextKeyOrExpr implements ContextKeyExpr { export class RawContextKey extends ContextKeyDefinedExpr { - private _defaultValue: T | undefined; + private readonly _defaultValue: T | undefined; constructor(key: string, defaultValue: T | undefined) { super(key); @@ -780,15 +932,15 @@ export class RawContextKey extends ContextKeyDefinedExpr { return target.getContextKeyValue(this.key); } - public toNegated(): ContextKeyExpr { + public toNegated(): ContextKeyExpression { return ContextKeyExpr.not(this.key); } - public isEqualTo(value: string): ContextKeyExpr { + public isEqualTo(value: string): ContextKeyExpression { return ContextKeyExpr.equals(this.key, value); } - public notEqualsTo(value: string): ContextKeyExpr { + public notEqualsTo(value: string): ContextKeyExpression { return ContextKeyExpr.notEquals(this.key, value); } } @@ -830,7 +982,7 @@ export interface IContextKeyService { createKey(key: string, defaultValue: T | undefined): IContextKey; - contextMatchesRules(rules: ContextKeyExpr | undefined): boolean; + contextMatchesRules(rules: ContextKeyExpression | undefined): boolean; getContextKeyValue(key: string): T | undefined; createScoped(target?: IContextKeyServiceTarget): IContextKeyService; diff --git a/src/vs/platform/contextkey/common/contextkeys.ts b/src/vs/platform/contextkey/common/contextkeys.ts index c3206a24d9a..8c17906ab67 100644 --- a/src/vs/platform/contextkey/common/contextkeys.ts +++ b/src/vs/platform/contextkey/common/contextkeys.ts @@ -3,9 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { isMacintosh, isLinux, isWindows, isWeb } from 'vs/base/common/platform'; + +export const IsMacContext = new RawContextKey('isMac', isMacintosh); +export const IsLinuxContext = new RawContextKey('isLinux', isLinux); +export const IsWindowsContext = new RawContextKey('isWindows', isWindows); + +export const IsWebContext = new RawContextKey('isWeb', isWeb); +export const IsMacNativeContext = new RawContextKey('isMacNative', isMacintosh && !isWeb); + +export const IsDevelopmentContext = new RawContextKey('isDevelopment', false); export const InputFocusedContextKey = 'inputFocus'; export const InputFocusedContext = new RawContextKey(InputFocusedContextKey, false); - -export const FalseContext: ContextKeyExpr = new RawContextKey('__false', false); \ No newline at end of file diff --git a/src/vs/platform/contextkey/test/common/contextkey.test.ts b/src/vs/platform/contextkey/test/common/contextkey.test.ts index 72929eff428..f5a04ce1cf2 100644 --- a/src/vs/platform/contextkey/test/common/contextkey.test.ts +++ b/src/vs/platform/contextkey/test/common/contextkey.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { isMacintosh, isLinux, isWindows } from 'vs/base/common/platform'; function createContext(ctx: any) { return { @@ -89,6 +90,8 @@ suite('ContextKeyExpr', () => { testBatch('d', 'd'); testBatch('z', undefined); + testExpression('true', true); + testExpression('false', false); testExpression('a && !b', true && !false); testExpression('a && b', true && false); testExpression('a && !b && c == 5', true && !false && '5' === '5'); @@ -107,10 +110,30 @@ suite('ContextKeyExpr', () => { const actual = ContextKeyExpr.deserialize(expr)!.negate().serialize(); assert.strictEqual(actual, expected); } + testNegate('true', 'false'); + testNegate('false', 'true'); testNegate('a', '!a'); testNegate('a && b || c', '!a && !c || !b && !c'); testNegate('a && b || c || d', '!a && !c && !d || !b && !c && !d'); testNegate('!a && !b || !c && !d', 'a && c || a && d || b && c || b && d'); testNegate('!a && !b || !c && !d || !e && !f', 'a && c && e || a && c && f || a && d && e || a && d && f || b && c && e || b && c && f || b && d && e || b && d && f'); }); + + test('false, true', () => { + function testNormalize(expr: string, expected: string): void { + const actual = ContextKeyExpr.deserialize(expr)!.serialize(); + assert.strictEqual(actual, expected); + } + testNormalize('true', 'true'); + testNormalize('!true', 'false'); + testNormalize('false', 'false'); + testNormalize('!false', 'true'); + testNormalize('a && true', 'a'); + testNormalize('a && false', 'false'); + testNormalize('a || true', 'true'); + testNormalize('a || false', 'a'); + testNormalize('isMac', isMacintosh ? 'true' : 'false'); + testNormalize('isLinux', isLinux ? 'true' : 'false'); + testNormalize('isWindows', isWindows ? 'true' : 'false'); + }); }); diff --git a/src/vs/platform/editor/common/editor.ts b/src/vs/platform/editor/common/editor.ts index 11e627666d7..c973a6e3418 100644 --- a/src/vs/platform/editor/common/editor.ts +++ b/src/vs/platform/editor/common/editor.ts @@ -65,7 +65,7 @@ export interface IResourceInput extends IBaseResourceInput { /** * The resource URI of the resource to open. */ - resource: URI; + readonly resource: URI; /** * The encoding of the text input if known. diff --git a/src/vs/platform/electron/electron-main/electronMainService.ts b/src/vs/platform/electron/electron-main/electronMainService.ts index 6a7310f2dc7..27ef89f17e2 100644 --- a/src/vs/platform/electron/electron-main/electronMainService.ts +++ b/src/vs/platform/electron/electron-main/electronMainService.ts @@ -21,7 +21,7 @@ import { URI } from 'vs/base/common/uri'; import { ITelemetryData, ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -export interface IElectronMainService extends AddFirstParameterToFunctions /* only methods, not events */, number | undefined /* window ID */> { } +export interface IElectronMainService extends AddFirstParameterToFunctions /* only methods, not events */, number | undefined /* window ID */> { } export const IElectronMainService = createDecorator('electronMainService'); diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index abd1e33b185..123c84684da 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -111,7 +111,6 @@ export interface IEnvironmentService extends IUserHomeProvider { args: ParsedArgs; execPath: string; - cliPath: string; appRoot: string; userHome: string; @@ -132,7 +131,6 @@ export interface IEnvironmentService extends IUserHomeProvider { settingsSyncPreviewResource: URI; keybindingsSyncPreviewResource: URI; - machineSettingsHome: URI; machineSettingsResource: URI; globalStorageHome: string; @@ -154,10 +152,7 @@ export interface IEnvironmentService extends IUserHomeProvider { debugExtensionHost: IExtensionHostDebugParams; isBuilt: boolean; - wait: boolean; - status: boolean; - log?: string; logsPath: string; verbose: boolean; @@ -168,7 +163,6 @@ export interface IEnvironmentService extends IUserHomeProvider { installSourcePath: string; disableUpdates: boolean; - disableCrashReporter: boolean; driverHandle?: string; driverVerbose: boolean; diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts index 0428e1e888a..58b8f6ffcd1 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts @@ -112,7 +112,7 @@ export class EnvironmentService implements IEnvironmentService { get settingsResource(): URI { return resources.joinPath(this.userRoamingDataHome, 'settings.json'); } @memoize - get userDataSyncHome(): URI { return resources.joinPath(this.userRoamingDataHome, '.sync'); } + get userDataSyncHome(): URI { return resources.joinPath(this.userRoamingDataHome, 'sync'); } @memoize get settingsSyncPreviewResource(): URI { return resources.joinPath(this.userDataSyncHome, 'settings.json'); } @@ -124,10 +124,7 @@ export class EnvironmentService implements IEnvironmentService { get userDataSyncLogResource(): URI { return URI.file(path.join(this.logsPath, 'userDataSync.log')); } @memoize - get machineSettingsHome(): URI { return URI.file(path.join(this.userDataPath, 'Machine')); } - - @memoize - get machineSettingsResource(): URI { return resources.joinPath(this.machineSettingsHome, 'settings.json'); } + get machineSettingsResource(): URI { return resources.joinPath(URI.file(path.join(this.userDataPath, 'Machine')), 'settings.json'); } @memoize get globalStorageHome(): string { return path.join(this.appSettingsHome.fsPath, 'globalStorage'); } @@ -248,10 +245,6 @@ export class EnvironmentService implements IEnvironmentService { get verbose(): boolean { return !!this._args.verbose; } get log(): string | undefined { return this._args.log; } - get wait(): boolean { return !!this._args.wait; } - - get status(): boolean { return !!this._args.status; } - @memoize get mainIPCHandle(): string { return getIPCHandle(this.userDataPath, 'main'); } diff --git a/src/vs/platform/environment/node/stdin.ts b/src/vs/platform/environment/node/stdin.ts index 2cd928e2507..e870ac6e704 100644 --- a/src/vs/platform/environment/node/stdin.ts +++ b/src/vs/platform/environment/node/stdin.ts @@ -39,18 +39,20 @@ 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 { +export async 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); - }); + let encoding = await resolveTerminalEncoding(verbose); + + const iconv = await import('iconv-lite'); + if (!iconv.encodingExists(encoding)) { + console.log(`Unsupported terminal encoding: ${encoding}, falling back to UTF-8.`); + encoding = 'utf8'; + } + + // Pipe into tmp file using terminals encoding + const converterStream = iconv.decodeStream(encoding); + process.stdin.pipe(converterStream).pipe(stdinFileStream); } diff --git a/src/vs/platform/files/test/node/diskFileService.test.ts b/src/vs/platform/files/test/electron-browser/diskFileService.test.ts similarity index 98% rename from src/vs/platform/files/test/node/diskFileService.test.ts rename to src/vs/platform/files/test/electron-browser/diskFileService.test.ts index 7b012bb19fd..b0af12007e7 100644 --- a/src/vs/platform/files/test/node/diskFileService.test.ts +++ b/src/vs/platform/files/test/electron-browser/diskFileService.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { tmpdir } from 'os'; import { FileService } from 'vs/platform/files/common/fileService'; import { Schemas } from 'vs/base/common/network'; -import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; +import { DiskFileSystemProvider } from 'vs/platform/files/electron-browser/diskFileSystemProvider'; import { getRandomTestPath } from 'vs/base/test/node/testUtils'; import { generateUuid } from 'vs/base/common/uuid'; import { join, basename, dirname, posix } from 'vs/base/common/path'; @@ -67,6 +67,7 @@ export class TestDiskFileSystemProvider extends DiskFileSystemProvider { FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.FileOpenReadWriteClose | FileSystemProviderCapabilities.FileReadStream | + FileSystemProviderCapabilities.Trash | FileSystemProviderCapabilities.FileFolderCopy; if (isLinux) { @@ -459,13 +460,21 @@ suite('Disk File Service', function () { }); test('deleteFile', async () => { + return testDeleteFile(false); + }); + + (isLinux /* trash is unreliable on Linux */ ? test.skip : test)('deleteFile (useTrash)', async () => { + return testDeleteFile(true); + }); + + async function testDeleteFile(useTrash: boolean): Promise { let event: FileOperationEvent; disposables.add(service.onDidRunOperation(e => event = e)); const resource = URI.file(join(testDir, 'deep', 'conway.js')); const source = await service.resolve(resource); - await service.del(source.resource); + await service.del(source.resource, { useTrash }); assert.equal(existsSync(source.resource.fsPath), false); @@ -475,14 +484,14 @@ suite('Disk File Service', function () { let error: Error | undefined = undefined; try { - await service.del(source.resource); + await service.del(source.resource, { useTrash }); } catch (e) { error = e; } assert.ok(error); assert.equal((error).fileOperationResult, FileOperationResult.FILE_NOT_FOUND); - }); + } test('deleteFile - symbolic link (exists)', async () => { if (isWindows) { @@ -531,19 +540,27 @@ suite('Disk File Service', function () { }); test('deleteFolder (recursive)', async () => { + return testDeleteFolderRecursive(false); + }); + + (isLinux /* trash is unreliable on Linux */ ? test.skip : test)('deleteFolder (recursive, useTrash)', async () => { + return testDeleteFolderRecursive(true); + }); + + async function testDeleteFolderRecursive(useTrash: boolean): Promise { let event: FileOperationEvent; disposables.add(service.onDidRunOperation(e => event = e)); const resource = URI.file(join(testDir, 'deep')); const source = await service.resolve(resource); - await service.del(source.resource, { recursive: true }); + await service.del(source.resource, { recursive: true, useTrash }); assert.equal(existsSync(source.resource.fsPath), false); assert.ok(event!); assert.equal(event!.resource.fsPath, resource.fsPath); assert.equal(event!.operation, FileOperation.DELETE); - }); + } test('deleteFolder (non recursive)', async () => { const resource = URI.file(join(testDir, 'deep')); diff --git a/src/vs/platform/files/test/node/fixtures/resolver/examples/company.js b/src/vs/platform/files/test/electron-browser/fixtures/resolver/examples/company.js similarity index 100% rename from src/vs/platform/files/test/node/fixtures/resolver/examples/company.js rename to src/vs/platform/files/test/electron-browser/fixtures/resolver/examples/company.js diff --git a/src/vs/platform/files/test/node/fixtures/resolver/examples/conway.js b/src/vs/platform/files/test/electron-browser/fixtures/resolver/examples/conway.js similarity index 100% rename from src/vs/platform/files/test/node/fixtures/resolver/examples/conway.js rename to src/vs/platform/files/test/electron-browser/fixtures/resolver/examples/conway.js diff --git a/src/vs/platform/files/test/node/fixtures/resolver/examples/employee.js b/src/vs/platform/files/test/electron-browser/fixtures/resolver/examples/employee.js similarity index 100% rename from src/vs/platform/files/test/node/fixtures/resolver/examples/employee.js rename to src/vs/platform/files/test/electron-browser/fixtures/resolver/examples/employee.js diff --git a/src/vs/platform/files/test/node/fixtures/resolver/examples/small.js b/src/vs/platform/files/test/electron-browser/fixtures/resolver/examples/small.js similarity index 100% rename from src/vs/platform/files/test/node/fixtures/resolver/examples/small.js rename to src/vs/platform/files/test/electron-browser/fixtures/resolver/examples/small.js diff --git a/src/vs/platform/files/test/node/fixtures/resolver/index.html b/src/vs/platform/files/test/electron-browser/fixtures/resolver/index.html similarity index 100% rename from src/vs/platform/files/test/node/fixtures/resolver/index.html rename to src/vs/platform/files/test/electron-browser/fixtures/resolver/index.html diff --git a/src/vs/platform/files/test/node/fixtures/resolver/other/deep/company.js b/src/vs/platform/files/test/electron-browser/fixtures/resolver/other/deep/company.js similarity index 100% rename from src/vs/platform/files/test/node/fixtures/resolver/other/deep/company.js rename to src/vs/platform/files/test/electron-browser/fixtures/resolver/other/deep/company.js diff --git a/src/vs/platform/files/test/node/fixtures/resolver/other/deep/conway.js b/src/vs/platform/files/test/electron-browser/fixtures/resolver/other/deep/conway.js similarity index 100% rename from src/vs/platform/files/test/node/fixtures/resolver/other/deep/conway.js rename to src/vs/platform/files/test/electron-browser/fixtures/resolver/other/deep/conway.js diff --git a/src/vs/platform/files/test/node/fixtures/resolver/other/deep/employee.js b/src/vs/platform/files/test/electron-browser/fixtures/resolver/other/deep/employee.js similarity index 100% rename from src/vs/platform/files/test/node/fixtures/resolver/other/deep/employee.js rename to src/vs/platform/files/test/electron-browser/fixtures/resolver/other/deep/employee.js diff --git a/src/vs/platform/files/test/node/fixtures/resolver/other/deep/small.js b/src/vs/platform/files/test/electron-browser/fixtures/resolver/other/deep/small.js similarity index 100% rename from src/vs/platform/files/test/node/fixtures/resolver/other/deep/small.js rename to src/vs/platform/files/test/electron-browser/fixtures/resolver/other/deep/small.js diff --git a/src/vs/platform/files/test/node/fixtures/resolver/site.css b/src/vs/platform/files/test/electron-browser/fixtures/resolver/site.css similarity index 100% rename from src/vs/platform/files/test/node/fixtures/resolver/site.css rename to src/vs/platform/files/test/electron-browser/fixtures/resolver/site.css diff --git a/src/vs/platform/files/test/node/fixtures/service/binary.txt b/src/vs/platform/files/test/electron-browser/fixtures/service/binary.txt similarity index 100% rename from src/vs/platform/files/test/node/fixtures/service/binary.txt rename to src/vs/platform/files/test/electron-browser/fixtures/service/binary.txt diff --git a/src/vs/platform/files/test/node/fixtures/service/deep/company.js b/src/vs/platform/files/test/electron-browser/fixtures/service/deep/company.js similarity index 100% rename from src/vs/platform/files/test/node/fixtures/service/deep/company.js rename to src/vs/platform/files/test/electron-browser/fixtures/service/deep/company.js diff --git a/src/vs/platform/files/test/node/fixtures/service/deep/conway.js b/src/vs/platform/files/test/electron-browser/fixtures/service/deep/conway.js similarity index 100% rename from src/vs/platform/files/test/node/fixtures/service/deep/conway.js rename to src/vs/platform/files/test/electron-browser/fixtures/service/deep/conway.js diff --git a/src/vs/platform/files/test/node/fixtures/service/deep/employee.js b/src/vs/platform/files/test/electron-browser/fixtures/service/deep/employee.js similarity index 100% rename from src/vs/platform/files/test/node/fixtures/service/deep/employee.js rename to src/vs/platform/files/test/electron-browser/fixtures/service/deep/employee.js diff --git a/src/vs/platform/files/test/node/fixtures/service/deep/small.js b/src/vs/platform/files/test/electron-browser/fixtures/service/deep/small.js similarity index 100% rename from src/vs/platform/files/test/node/fixtures/service/deep/small.js rename to src/vs/platform/files/test/electron-browser/fixtures/service/deep/small.js diff --git a/src/vs/platform/files/test/node/fixtures/service/index.html b/src/vs/platform/files/test/electron-browser/fixtures/service/index.html similarity index 100% rename from src/vs/platform/files/test/node/fixtures/service/index.html rename to src/vs/platform/files/test/electron-browser/fixtures/service/index.html diff --git a/src/vs/platform/files/test/node/fixtures/service/lorem.txt b/src/vs/platform/files/test/electron-browser/fixtures/service/lorem.txt similarity index 100% rename from src/vs/platform/files/test/node/fixtures/service/lorem.txt rename to src/vs/platform/files/test/electron-browser/fixtures/service/lorem.txt diff --git a/src/vs/platform/files/test/node/fixtures/service/small.txt b/src/vs/platform/files/test/electron-browser/fixtures/service/small.txt similarity index 100% rename from src/vs/platform/files/test/node/fixtures/service/small.txt rename to src/vs/platform/files/test/electron-browser/fixtures/service/small.txt diff --git a/src/vs/platform/files/test/node/fixtures/service/small_umlaut.txt b/src/vs/platform/files/test/electron-browser/fixtures/service/small_umlaut.txt similarity index 100% rename from src/vs/platform/files/test/node/fixtures/service/small_umlaut.txt rename to src/vs/platform/files/test/electron-browser/fixtures/service/small_umlaut.txt diff --git a/src/vs/platform/files/test/node/fixtures/service/some_utf16le.css b/src/vs/platform/files/test/electron-browser/fixtures/service/some_utf16le.css similarity index 100% rename from src/vs/platform/files/test/node/fixtures/service/some_utf16le.css rename to src/vs/platform/files/test/electron-browser/fixtures/service/some_utf16le.css diff --git a/src/vs/platform/files/test/node/fixtures/service/some_utf8_bom.txt b/src/vs/platform/files/test/electron-browser/fixtures/service/some_utf8_bom.txt similarity index 100% rename from src/vs/platform/files/test/node/fixtures/service/some_utf8_bom.txt rename to src/vs/platform/files/test/electron-browser/fixtures/service/some_utf8_bom.txt diff --git a/src/vs/platform/files/test/node/normalizer.test.ts b/src/vs/platform/files/test/electron-browser/normalizer.test.ts similarity index 100% rename from src/vs/platform/files/test/node/normalizer.test.ts rename to src/vs/platform/files/test/electron-browser/normalizer.test.ts diff --git a/src/vs/platform/keybinding/common/abstractKeybindingService.ts b/src/vs/platform/keybinding/common/abstractKeybindingService.ts index 6440a346f59..1fce530772a 100644 --- a/src/vs/platform/keybinding/common/abstractKeybindingService.ts +++ b/src/vs/platform/keybinding/common/abstractKeybindingService.ts @@ -35,6 +35,10 @@ export abstract class AbstractKeybindingService extends Disposable implements IK private _currentChordChecker: IntervalTimer; private _currentChordStatusMessage: IDisposable | null; + public get inChordMode(): boolean { + return !!this._currentChord; + } + constructor( private _contextKeyService: IContextKeyService, protected _commandService: ICommandService, diff --git a/src/vs/platform/keybinding/common/keybinding.ts b/src/vs/platform/keybinding/common/keybinding.ts index e11bd846c43..17ab82b8184 100644 --- a/src/vs/platform/keybinding/common/keybinding.ts +++ b/src/vs/platform/keybinding/common/keybinding.ts @@ -50,6 +50,8 @@ export const IKeybindingService = createDecorator('keybindin export interface IKeybindingService { _serviceBrand: undefined; + readonly inChordMode: boolean; + onDidUpdateKeybindings: Event; /** diff --git a/src/vs/platform/keybinding/common/keybindingResolver.ts b/src/vs/platform/keybinding/common/keybindingResolver.ts index 2ae709887b3..951d01f4c16 100644 --- a/src/vs/platform/keybinding/common/keybindingResolver.ts +++ b/src/vs/platform/keybinding/common/keybindingResolver.ts @@ -6,12 +6,15 @@ import { isNonEmptyArray } from 'vs/base/common/arrays'; import { MenuRegistry } from 'vs/platform/actions/common/actions'; import { CommandsRegistry, ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; -import { ContextKeyExpr, IContext, ContextKeyOrExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IContext, ContextKeyExpression, ContextKeyExprType } from 'vs/platform/contextkey/common/contextkey'; import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem'; import { keys } from 'vs/base/common/map'; export interface IResolveResult { + /** Whether the resolved keybinding is entering a chord */ enterChord: boolean; + /** Whether the resolved keybinding is leaving (and executing) a chord */ + leaveChord: boolean; commandId: string | null; commandArgs: any; bubble: boolean; @@ -46,12 +49,17 @@ export class KeybindingResolver { continue; } + if (k.when && k.when.type === ContextKeyExprType.False) { + // when condition is false + continue; + } + // TODO@chords this._addKeyPress(k.keypressParts[0], k); } } - private static _isTargetedForRemoval(defaultKb: ResolvedKeybindingItem, keypressFirstPart: string | null, keypressChordPart: string | null, command: string, when: ContextKeyExpr | undefined): boolean { + private static _isTargetedForRemoval(defaultKb: ResolvedKeybindingItem, keypressFirstPart: string | null, keypressChordPart: string | null, command: string, when: ContextKeyExpression | undefined): boolean { if (defaultKb.command !== command) { return false; } @@ -172,7 +180,7 @@ export class KeybindingResolver { /** * Returns true if it is provable `a` implies `b`. */ - public static whenIsEntirelyIncluded(a: ContextKeyExpr | null | undefined, b: ContextKeyExpr | null | undefined): boolean { + public static whenIsEntirelyIncluded(a: ContextKeyExpression | null | undefined, b: ContextKeyExpression | null | undefined): boolean { if (!b) { return true; } @@ -186,11 +194,11 @@ export class KeybindingResolver { /** * Returns true if it is provable `p` implies `q`. */ - private static _implies(p: ContextKeyExpr, q: ContextKeyExpr): boolean { + private static _implies(p: ContextKeyExpression, q: ContextKeyExpression): boolean { const notP = p.negate(); - const terminals = (node: ContextKeyExpr) => { - if (node instanceof ContextKeyOrExpr) { + const terminals = (node: ContextKeyExpression) => { + if (node.type === ContextKeyExprType.Or) { return node.expr; } return [node]; @@ -285,6 +293,7 @@ export class KeybindingResolver { if (currentChord === null && result.keypressParts.length > 1 && result.keypressParts[1] !== null) { return { enterChord: true, + leaveChord: false, commandId: null, commandArgs: null, bubble: false @@ -293,6 +302,7 @@ export class KeybindingResolver { return { enterChord: false, + leaveChord: result.keypressParts.length > 1, commandId: result.command, commandArgs: result.commandArgs, bubble: result.bubble @@ -313,7 +323,7 @@ export class KeybindingResolver { return null; } - public static contextMatchesRules(context: IContext, rules: ContextKeyExpr | null | undefined): boolean { + public static contextMatchesRules(context: IContext, rules: ContextKeyExpression | null | undefined): boolean { if (!rules) { return true; } diff --git a/src/vs/platform/keybinding/common/keybindingsRegistry.ts b/src/vs/platform/keybinding/common/keybindingsRegistry.ts index 5c3de02794b..6f249867f8e 100644 --- a/src/vs/platform/keybinding/common/keybindingsRegistry.ts +++ b/src/vs/platform/keybinding/common/keybindingsRegistry.ts @@ -6,14 +6,14 @@ import { KeyCode, Keybinding, SimpleKeybinding, createKeybinding } from 'vs/base/common/keyCodes'; import { OS, OperatingSystem } from 'vs/base/common/platform'; import { CommandsRegistry, ICommandHandler, ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { Registry } from 'vs/platform/registry/common/platform'; export interface IKeybindingItem { keybinding: Keybinding; command: string; commandArgs?: any; - when: ContextKeyExpr | null | undefined; + when: ContextKeyExpression | null | undefined; weight1: number; weight2: number; } @@ -39,7 +39,7 @@ export interface IKeybindingRule extends IKeybindings { id: string; weight: number; args?: any; - when: ContextKeyExpr | null | undefined; + when: ContextKeyExpression | null | undefined; } export interface IKeybindingRule2 { @@ -50,7 +50,7 @@ export interface IKeybindingRule2 { id: string; args?: any; weight: number; - when: ContextKeyExpr | undefined; + when: ContextKeyExpression | undefined; } export const enum KeybindingWeight { @@ -209,7 +209,7 @@ class KeybindingsRegistryImpl implements IKeybindingsRegistry { } } - private _registerDefaultKeybinding(keybinding: Keybinding, commandId: string, commandArgs: any, weight1: number, weight2: number, when: ContextKeyExpr | null | undefined): void { + private _registerDefaultKeybinding(keybinding: Keybinding, commandId: string, commandArgs: any, weight1: number, weight2: number, when: ContextKeyExpression | null | undefined): void { if (OS === OperatingSystem.Windows) { this._assertNoCtrlAlt(keybinding.parts[0], commandId); } diff --git a/src/vs/platform/keybinding/common/resolvedKeybindingItem.ts b/src/vs/platform/keybinding/common/resolvedKeybindingItem.ts index 00b05e05213..a32518446e7 100644 --- a/src/vs/platform/keybinding/common/resolvedKeybindingItem.ts +++ b/src/vs/platform/keybinding/common/resolvedKeybindingItem.ts @@ -5,7 +5,7 @@ import { CharCode } from 'vs/base/common/charCode'; import { ResolvedKeybinding } from 'vs/base/common/keyCodes'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; export class ResolvedKeybindingItem { _resolvedKeybindingItemBrand: void; @@ -15,10 +15,10 @@ export class ResolvedKeybindingItem { public readonly bubble: boolean; public readonly command: string | null; public readonly commandArgs: any; - public readonly when: ContextKeyExpr | undefined; + public readonly when: ContextKeyExpression | undefined; public readonly isDefault: boolean; - constructor(resolvedKeybinding: ResolvedKeybinding | undefined, command: string | null, commandArgs: any, when: ContextKeyExpr | undefined, isDefault: boolean) { + constructor(resolvedKeybinding: ResolvedKeybinding | undefined, command: string | null, commandArgs: any, when: ContextKeyExpression | undefined, isDefault: boolean) { this.resolvedKeybinding = resolvedKeybinding; this.keypressParts = resolvedKeybinding ? removeElementsAfterNulls(resolvedKeybinding.getDispatchParts()) : []; this.bubble = (command ? command.charCodeAt(0) === CharCode.Caret : false); diff --git a/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts b/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts index 05a93605d42..286ea8a57ae 100644 --- a/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts +++ b/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts @@ -7,7 +7,7 @@ import { KeyChord, KeyCode, KeyMod, Keybinding, ResolvedKeybinding, SimpleKeybin import { OS } from 'vs/base/common/platform'; import Severity from 'vs/base/common/severity'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { ContextKeyExpr, IContext, IContextKeyService, IContextKeyServiceTarget } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, IContext, IContextKeyService, IContextKeyServiceTarget, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { AbstractKeybindingService } from 'vs/platform/keybinding/common/abstractKeybindingService'; import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding'; import { KeybindingResolver } from 'vs/platform/keybinding/common/keybindingResolver'; @@ -182,7 +182,7 @@ suite('AbstractKeybindingService', () => { statusMessageCallsDisposed = null; }); - function kbItem(keybinding: number, command: string, when?: ContextKeyExpr): ResolvedKeybindingItem { + function kbItem(keybinding: number, command: string, when?: ContextKeyExpression): ResolvedKeybindingItem { const resolvedKeybinding = (keybinding !== 0 ? new USLayoutResolvedKeybinding(createKeybinding(keybinding, OS)!, OS) : undefined); return new ResolvedKeybindingItem( resolvedKeybinding, diff --git a/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts b/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts index c85003be1ac..7a71aad81d6 100644 --- a/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts +++ b/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { KeyChord, KeyCode, KeyMod, SimpleKeybinding, createKeybinding, createSimpleKeybinding } from 'vs/base/common/keyCodes'; import { OS } from 'vs/base/common/platform'; -import { ContextKeyExpr, IContext } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, IContext, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingResolver } from 'vs/platform/keybinding/common/keybindingResolver'; import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem'; import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding'; @@ -20,7 +20,7 @@ function createContext(ctx: any) { suite('KeybindingResolver', () => { - function kbItem(keybinding: number, command: string, commandArgs: any, when: ContextKeyExpr | undefined, isDefault: boolean): ResolvedKeybindingItem { + function kbItem(keybinding: number, command: string, commandArgs: any, when: ContextKeyExpression | undefined, isDefault: boolean): ResolvedKeybindingItem { const resolvedKeybinding = (keybinding !== 0 ? new USLayoutResolvedKeybinding(createKeybinding(keybinding, OS)!, OS) : undefined); return new ResolvedKeybindingItem( resolvedKeybinding, @@ -225,7 +225,7 @@ suite('KeybindingResolver', () => { test('resolve command', function () { - function _kbItem(keybinding: number, command: string, when: ContextKeyExpr | undefined): ResolvedKeybindingItem { + function _kbItem(keybinding: number, command: string, when: ContextKeyExpression | undefined): ResolvedKeybindingItem { return kbItem(keybinding, command, null, when, true); } diff --git a/src/vs/platform/keybinding/test/common/mockKeybindingService.ts b/src/vs/platform/keybinding/test/common/mockKeybindingService.ts index c2062a4e4b4..998169968bd 100644 --- a/src/vs/platform/keybinding/test/common/mockKeybindingService.ts +++ b/src/vs/platform/keybinding/test/common/mockKeybindingService.ts @@ -6,7 +6,7 @@ import { Event } from 'vs/base/common/event'; import { Keybinding, ResolvedKeybinding, SimpleKeybinding } from 'vs/base/common/keyCodes'; import { OS } from 'vs/base/common/platform'; -import { ContextKeyExpr, IContextKey, IContextKeyChangeEvent, IContextKeyService, IContextKeyServiceTarget } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKey, IContextKeyChangeEvent, IContextKeyService, IContextKeyServiceTarget, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { IKeybindingEvent, IKeybindingService, IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding'; import { IResolveResult } from 'vs/platform/keybinding/common/keybindingResolver'; import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem'; @@ -47,7 +47,7 @@ export class MockContextKeyService implements IContextKeyService { this._keys.set(key, ret); return ret; } - public contextMatchesRules(rules: ContextKeyExpr): boolean { + public contextMatchesRules(rules: ContextKeyExpression): boolean { return false; } public get onDidChangeContext(): Event { @@ -71,6 +71,8 @@ export class MockContextKeyService implements IContextKeyService { export class MockKeybindingService implements IKeybindingService { public _serviceBrand: undefined; + public readonly inChordMode: boolean = false; + public get onDidUpdateKeybindings(): Event { return Event.None; } diff --git a/src/vs/platform/label/common/label.ts b/src/vs/platform/label/common/label.ts index b2422cfa473..64fe1845042 100644 --- a/src/vs/platform/label/common/label.ts +++ b/src/vs/platform/label/common/label.ts @@ -26,7 +26,11 @@ export interface ILabelService { getHostLabel(scheme: string, authority?: string): string; getSeparator(scheme: string, authority?: string): '/' | '\\'; registerFormatter(formatter: ResourceLabelFormatter): IDisposable; - onDidChangeFormatters: Event; + onDidChangeFormatters: Event; +} + +export interface IFormatterChangeEvent { + scheme: string; } export interface ResourceLabelFormatter { diff --git a/src/vs/platform/notification/common/notification.ts b/src/vs/platform/notification/common/notification.ts index aff1675b214..7b19313df62 100644 --- a/src/vs/platform/notification/common/notification.ts +++ b/src/vs/platform/notification/common/notification.ts @@ -21,20 +21,20 @@ export interface INotificationProperties { * Sticky notifications are not automatically removed after a certain timeout. By * default, notifications with primary actions and severity error are always sticky. */ - sticky?: boolean; + readonly sticky?: boolean; /** * Silent notifications are not shown to the user unless the notification center * is opened. The status bar will still indicate all number of notifications to * catch some attention. */ - silent?: boolean; + readonly silent?: boolean; /** * Adds an action to never show the notification again. The choice will be persisted * such as future requests will not cause the notification to show again. */ - neverShowAgain?: INeverShowAgainOptions; + readonly neverShowAgain?: INeverShowAgainOptions; } export enum NeverShowAgainScope { @@ -55,19 +55,19 @@ export interface INeverShowAgainOptions { /** * The id is used to persist the selection of not showing the notification again. */ - id: string; + readonly id: string; /** * By default the action will show up as primary action. Setting this to true will * make it a secondary action instead. */ - isSecondary?: boolean; + readonly isSecondary?: boolean; /** * Whether to persist the choice in the current workspace or for all workspaces. By * default it will be persisted for all workspaces. */ - scope?: NeverShowAgainScope; + readonly scope?: NeverShowAgainScope; } export interface INotification extends INotificationProperties { @@ -75,18 +75,18 @@ export interface INotification extends INotificationProperties { /** * The severity of the notification. Either `Info`, `Warning` or `Error`. */ - severity: Severity; + readonly severity: Severity; /** * The message of the notification. This can either be a `string` or `Error`. Messages * can optionally include links in the format: `[text](link)` */ - message: NotificationMessage; + readonly message: NotificationMessage; /** * The source of the notification appears as additional information. */ - source?: string; + readonly source?: string; /** * Actions to show as part of the notification. Primary actions show up as @@ -101,6 +101,12 @@ export interface INotification extends INotificationProperties { * this usecase and much easier to use! */ actions?: INotificationActions; + + /** + * The initial set of progress properties for the notification. To update progress + * later on, access the `INotificationHandle.progress` property. + */ + readonly progress?: INotificationProgressProperties; } export interface INotificationActions { @@ -109,14 +115,32 @@ export interface INotificationActions { * Primary actions show up as buttons as part of the message and will close * the notification once clicked. */ - primary?: ReadonlyArray; + readonly primary?: ReadonlyArray; /** * Secondary actions are meant to provide additional configuration or context * for the notification and will show up less prominent. A notification does not * close automatically when invoking a secondary action. */ - secondary?: ReadonlyArray; + readonly secondary?: ReadonlyArray; +} + +export interface INotificationProgressProperties { + + /** + * Causes the progress bar to spin infinitley. + */ + readonly infinite?: boolean; + + /** + * Indicate the total amount of work. + */ + readonly total?: number; + + /** + * Indicate that a specific chunk of work is done. + */ + readonly worked?: number; } export interface INotificationProgress { @@ -149,6 +173,13 @@ export interface INotificationHandle { */ readonly onDidClose: Event; + /** + * Will be fired whenever the visibility of the notification changes. + * A notification can either be visible as toast or inside the notification + * center if it is visible. + */ + readonly onDidChangeVisibility: Event; + /** * Allows to indicate progress on the notification even after the * notification is already visible. @@ -183,19 +214,19 @@ export interface IPromptChoice { /** * Label to show for the choice to the user. */ - label: string; + readonly label: string; /** * Primary choices show up as buttons in the notification below the message. * Secondary choices show up under the gear icon in the header of the notification. */ - isSecondary?: boolean; + readonly isSecondary?: boolean; /** * Whether to keep the notification open after the choice was selected * by the user. By default, will close the notification upon click. */ - keepOpen?: boolean; + readonly keepOpen?: boolean; /** * Triggered when the user selects the choice. @@ -218,13 +249,13 @@ export interface IStatusMessageOptions { * An optional timeout after which the status message should show. By default * the status message will show immediately. */ - showAfter?: number; + readonly showAfter?: number; /** * An optional timeout after which the status message is to be hidden. By default * the status message will not hide until another status message is displayed. */ - hideAfter?: number; + readonly hideAfter?: number; } export enum NotificationsFilter { diff --git a/src/vs/platform/progress/common/progress.ts b/src/vs/platform/progress/common/progress.ts index a0cea0a3659..b6ff5e5057b 100644 --- a/src/vs/platform/progress/common/progress.ts +++ b/src/vs/platform/progress/common/progress.ts @@ -17,7 +17,7 @@ export interface IProgressService { _serviceBrand: undefined; - withProgress( + withProgress( options: IProgressOptions | IProgressNotificationOptions | IProgressWindowOptions | IProgressCompositeOptions, task: (progress: IProgress) => Promise, onDidCancel?: (choice?: number) => void @@ -36,7 +36,7 @@ export interface IProgressIndicator { * Indicate progress for the duration of the provided promise. Progress will stop in * any case of promise completion, error or cancellation. */ - showWhile(promise: Promise, delay?: number): Promise; + showWhile(promise: Promise, delay?: number): Promise; } export const enum ProgressLocation { @@ -98,7 +98,7 @@ export interface IProgress { export class Progress implements IProgress { - static readonly None: IProgress = Object.freeze({ report() { } }); + static readonly None: IProgress = Object.freeze({ report() { } }); private _value?: T; get value(): T | undefined { return this._value; } diff --git a/src/vs/platform/remote/common/remoteAuthorityResolver.ts b/src/vs/platform/remote/common/remoteAuthorityResolver.ts index 0f05864a61d..786adeba46b 100644 --- a/src/vs/platform/remote/common/remoteAuthorityResolver.ts +++ b/src/vs/platform/remote/common/remoteAuthorityResolver.ts @@ -40,28 +40,24 @@ export enum RemoteAuthorityResolverErrorCode { export class RemoteAuthorityResolverError extends Error { - public static isHandledNotAvailable(err: any): boolean { - if (err instanceof RemoteAuthorityResolverError) { - if (err._code === RemoteAuthorityResolverErrorCode.NotAvailable && err._detail === true) { - return true; - } - } - - return this.isTemporarilyNotAvailable(err); - } - public static isTemporarilyNotAvailable(err: any): boolean { return (err instanceof RemoteAuthorityResolverError) && err._code === RemoteAuthorityResolverErrorCode.TemporarilyNotAvailable; } - public static isNoResolverFound(err: any): boolean { + public static isNoResolverFound(err: any): err is RemoteAuthorityResolverError { return (err instanceof RemoteAuthorityResolverError) && err._code === RemoteAuthorityResolverErrorCode.NoResolverFound; } + public static isHandled(err: any): boolean { + return (err instanceof RemoteAuthorityResolverError) && err.isHandled; + } + public readonly _message: string | undefined; public readonly _code: RemoteAuthorityResolverErrorCode; public readonly _detail: any; + public isHandled: boolean; + constructor(message?: string, code: RemoteAuthorityResolverErrorCode = RemoteAuthorityResolverErrorCode.Unknown, detail?: any) { super(message); @@ -69,6 +65,8 @@ export class RemoteAuthorityResolverError extends Error { this._code = code; this._detail = detail; + this.isHandled = (code === RemoteAuthorityResolverErrorCode.NotAvailable) && detail === true; + // workaround when extending builtin objects and when compiling to ES5, see: // https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work if (typeof (Object).setPrototypeOf === 'function') { diff --git a/src/vs/platform/storage/browser/storageService.ts b/src/vs/platform/storage/browser/storageService.ts index 839f2c94ff9..1c8174147b0 100644 --- a/src/vs/platform/storage/browser/storageService.ts +++ b/src/vs/platform/storage/browser/storageService.ts @@ -240,9 +240,36 @@ export class FileStorageDatabase extends Disposable implements IStorageDatabase private async onDidStorageChangeExternal(): Promise { const items = await this.doGetItemsFromFile(); + // pervious cache, diff for changes + let changed = new Map(); + let deleted = new Set(); + if (this.cache) { + items.forEach((value, key) => { + const existingValue = this.cache?.get(key); + if (existingValue !== value) { + changed.set(key, value); + } + }); + + this.cache.forEach((_, key) => { + if (!items.has(key)) { + deleted.add(key); + } + }); + } + + // no previous cache, consider all as changed + else { + changed = items; + } + + // Update cache this.cache = items; - this._onDidChangeItemsExternal.fire({ items }); + // Emit as event as needed + if (changed.size > 0 || deleted.size > 0) { + this._onDidChangeItemsExternal.fire({ changed, deleted }); + } } async getItems(): Promise> { diff --git a/src/vs/platform/storage/node/storageIpc.ts b/src/vs/platform/storage/node/storageIpc.ts index e2bbca21641..a6f2367f355 100644 --- a/src/vs/platform/storage/node/storageIpc.ts +++ b/src/vs/platform/storage/node/storageIpc.ts @@ -24,15 +24,16 @@ interface ISerializableUpdateRequest { } interface ISerializableItemsChangeEvent { - items: Item[]; + changed?: Item[]; + deleted?: Key[]; } export class GlobalStorageDatabaseChannel extends Disposable implements IServerChannel { private static readonly STORAGE_CHANGE_DEBOUNCE_TIME = 100; - private readonly _onDidChangeItems: Emitter = this._register(new Emitter()); - readonly onDidChangeItems: Event = this._onDidChangeItems.event; + private readonly _onDidChangeItems = this._register(new Emitter()); + readonly onDidChangeItems = this._onDidChangeItems.event; private whenReady: Promise; @@ -99,15 +100,18 @@ export class GlobalStorageDatabaseChannel extends Disposable implements IServerC } private serializeEvents(events: IStorageChangeEvent[]): ISerializableItemsChangeEvent { - const items = new Map(); + const changed = new Map(); + const deleted = new Set(); events.forEach(event => { const existing = this.storageMainService.get(event.key); if (typeof existing === 'string') { - items.set(event.key, existing); + changed.set(event.key, existing); + } else { + deleted.add(event.key); } }); - return { items: mapToSerializable(items) }; + return { changed: mapToSerializable(changed), deleted: values(deleted) }; } listen(_: unknown, event: string): Event { @@ -170,8 +174,11 @@ export class GlobalStorageDatabaseChannelClient extends Disposable implements IS } private onDidChangeItemsOnMain(e: ISerializableItemsChangeEvent): void { - if (Array.isArray(e.items)) { - this._onDidChangeItemsExternal.fire({ items: serializableToMap(e.items) }); + if (Array.isArray(e.changed) || Array.isArray(e.deleted)) { + this._onDidChangeItemsExternal.fire({ + changed: e.changed ? serializableToMap(e.changed) : undefined, + deleted: e.deleted ? new Set(e.deleted) : undefined + }); } } diff --git a/src/vs/platform/storage/node/storageService.ts b/src/vs/platform/storage/node/storageService.ts index 1aed6216d8f..cf68e7b5aac 100644 --- a/src/vs/platform/storage/node/storageService.ts +++ b/src/vs/platform/storage/node/storageService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { Event, Emitter } from 'vs/base/common/event'; +import { Emitter } from 'vs/base/common/event'; import { ILogService, LogLevel } from 'vs/platform/log/common/log'; import { IWorkspaceStorageChangeEvent, IStorageService, StorageScope, IWillSaveStateEvent, WillSaveStateReason, logStorage } from 'vs/platform/storage/common/storage'; import { SQLiteStorageDatabase, ISQLiteStorageDatabaseLoggingOptions } from 'vs/base/parts/storage/node/storage'; @@ -25,11 +25,11 @@ export class NativeStorageService extends Disposable implements IStorageService private static readonly WORKSPACE_STORAGE_NAME = 'state.vscdb'; private static readonly WORKSPACE_META_NAME = 'workspace.json'; - 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; diff --git a/src/vs/platform/undoRedo/common/undoRedoService.ts b/src/vs/platform/undoRedo/common/undoRedoService.ts index 1f5191e2a74..8568767aa3d 100644 --- a/src/vs/platform/undoRedo/common/undoRedoService.ts +++ b/src/vs/platform/undoRedo/common/undoRedoService.ts @@ -260,7 +260,7 @@ export class UndoRedoService implements IUndoRedoService { this._splitPastWorkspaceElement(element, element.removedResources.set); const message = nls.localize('cannotWorkspaceUndo', "Could not undo '{0}' across all files. {1}", element.label, element.removedResources.createMessage()); this._notificationService.info(message); - return; + return this.undo(resource); } // this must be the last past element in all the impacted resources! @@ -281,18 +281,25 @@ export class UndoRedoService implements IUndoRedoService { const paths = cannotUndoDueToResources.map(r => r.scheme === Schemas.file ? r.fsPath : r.path); const message = nls.localize('cannotWorkspaceUndoDueToChanges', "Could not undo '{0}' across all files because changes were made to {1}", element.label, paths.join(', ')); this._notificationService.info(message); - return; + return this.undo(resource); } return this._dialogService.show( Severity.Info, nls.localize('confirmWorkspace', "Would you like to undo '{0}' across all files?", element.label), [ - nls.localize('ok', "Yes, change {0} files.", affectedEditStacks.length), - nls.localize('nok', "No, change only this file.") - ] + nls.localize('ok', "Undo In {0} Files", affectedEditStacks.length), + nls.localize('nok', "Undo This File"), + nls.localize('cancel', "Cancel"), + ], + { + cancelId: 2 + } ).then((result) => { - if (result.choice === 0) { + if (result.choice === 2) { + // cancel + return; + } else if (result.choice === 0) { for (const editStack of affectedEditStacks) { editStack.past.pop(); editStack.future.push(element); @@ -344,7 +351,7 @@ export class UndoRedoService implements IUndoRedoService { this._splitFutureWorkspaceElement(element, element.removedResources.set); const message = nls.localize('cannotWorkspaceRedo', "Could not redo '{0}' across all files. {1}", element.label, element.removedResources.createMessage()); this._notificationService.info(message); - return; + return this.redo(resource); } // this must be the last future element in all the impacted resources! @@ -365,7 +372,7 @@ export class UndoRedoService implements IUndoRedoService { const paths = cannotRedoDueToResources.map(r => r.scheme === Schemas.file ? r.fsPath : r.path); const message = nls.localize('cannotWorkspaceRedoDueToChanges', "Could not redo '{0}' across all files because changes were made to {1}", element.label, paths.join(', ')); this._notificationService.info(message); - return; + return this.redo(resource); } for (const editStack of affectedEditStacks) { diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index a82acd2b357..03edf498efd 100644 --- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -68,7 +68,7 @@ export abstract class AbstractSynchroniser extends Disposable { ) { super(); this.syncFolder = joinPath(environmentService.userDataSyncHome, source); - this.lastSyncResource = joinPath(this.syncFolder, `.lastSync${source}.json`); + this.lastSyncResource = joinPath(this.syncFolder, `lastSync${source}.json`); this.cleanUpDelayer = new ThrottledDelayer(50); this.cleanUpBackup(); } @@ -89,10 +89,10 @@ export abstract class AbstractSynchroniser extends Disposable { } } - protected get enabled(): boolean { return this.userDataSyncEnablementService.isResourceEnabled(this.resourceKey); } + protected isEnabled(): boolean { return this.userDataSyncEnablementService.isResourceEnabled(this.resourceKey); } - async sync(ref?: string, donotUseLastSyncUserData?: boolean): Promise { - if (!this.enabled) { + async sync(ref?: string): Promise { + if (!this.isEnabled()) { this.logService.info(`${this.source}: Skipped synchronizing ${this.source.toLowerCase()} as it is disabled.`); return; } @@ -108,24 +108,40 @@ export abstract class AbstractSynchroniser extends Disposable { this.logService.trace(`${this.source}: Started synchronizing ${this.source.toLowerCase()}...`); this.setStatus(SyncStatus.Syncing); - const lastSyncUserData = donotUseLastSyncUserData ? null : await this.getLastSyncUserData(); + const lastSyncUserData = await this.getLastSyncUserData(); const remoteUserData = ref && lastSyncUserData && lastSyncUserData.ref === ref ? lastSyncUserData : await this.getRemoteUserData(lastSyncUserData); + let status: SyncStatus = SyncStatus.Idle; + try { + status = await this.doSync(remoteUserData, lastSyncUserData); + if (status === SyncStatus.HasConflicts) { + this.logService.info(`${this.source}: Detected conflicts while synchronizing ${this.source.toLowerCase()}.`); + } else if (status === SyncStatus.Idle) { + this.logService.trace(`${this.source}: Finished synchronizing ${this.source.toLowerCase()}.`); + } + } finally { + this.setStatus(status); + } + } + + protected async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { if (remoteUserData.syncData && remoteUserData.syncData.version > this.version) { // current version is not compatible with cloud version this.telemetryService.publicLog2<{ source: string }, SyncSourceClassification>('sync/incompatible', { source: this.source }); throw new UserDataSyncError(localize('incompatible', "Cannot sync {0} as its version {1} is not compatible with cloud {2}", this.source, this.version, remoteUserData.syncData.version), UserDataSyncErrorCode.Incompatible, this.source); } - try { - await this.doSync(remoteUserData, lastSyncUserData); + const status = await this.performSync(remoteUserData, lastSyncUserData); + return status; } catch (e) { if (e instanceof UserDataSyncError) { switch (e.code) { case UserDataSyncErrorCode.RemotePreconditionFailed: // Rejected as there is a new remote version. Syncing again, this.logService.info(`${this.source}: Failed to synchronize as there is a new remote version available. Synchronizing again...`); - return this.sync(undefined, true); + // Avoid cache and get latest remote user data - https://github.com/microsoft/vscode/issues/90624 + remoteUserData = await this.getRemoteUserData(null); + return this.doSync(remoteUserData, lastSyncUserData); } } throw e; @@ -202,7 +218,7 @@ export abstract class AbstractSynchroniser extends Disposable { } protected async backupLocal(content: VSBuffer): Promise { - const resource = joinPath(this.syncFolder, toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')); + const resource = joinPath(this.syncFolder, `${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}.json`); try { await this.fileService.writeFile(resource, content); } catch (e) { @@ -213,9 +229,12 @@ export abstract class AbstractSynchroniser extends Disposable { private async cleanUpBackup(): Promise { try { + if (!(await this.fileService.exists(this.syncFolder))) { + return; + } const stat = await this.fileService.resolve(this.syncFolder); if (stat.children) { - const all = stat.children.filter(stat => stat.isFile && /^\d{8}T\d{6}$/.test(stat.name)).sort(); + const all = stat.children.filter(stat => stat.isFile && /^\d{8}T\d{6}(\.json)?$/.test(stat.name)).sort(); const backUpMaxAge = 1000 * 60 * 60 * 24 * (this.configurationService.getValue('sync.localBackupDuration') || 30 /* Default 30 days */); let toDelete = all.filter(stat => { const ctime = stat.ctime || new Date( @@ -244,7 +263,7 @@ export abstract class AbstractSynchroniser extends Disposable { abstract readonly resourceKey: ResourceKey; protected abstract readonly version: number; - protected abstract doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise; + protected abstract performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise; } export interface IFileSyncPreviewResult { @@ -329,7 +348,7 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser { return; } - if (!this.enabled) { + if (!this.isEnabled()) { return; } @@ -337,7 +356,7 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser { if (this.status === SyncStatus.HasConflicts) { this.syncPreviewResultPromise?.then(result => { this.cancel(); - this.doSync(result.remoteUserData, result.lastSyncUserData); + this.doSync(result.remoteUserData, result.lastSyncUserData).then(status => this.setStatus(status)); }); } diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index b932d1ecd10..1e9c81523e0 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -37,6 +37,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse readonly resourceKey: ResourceKey = 'extensions'; protected readonly version: number = 2; + protected isEnabled(): boolean { return super.isEnabled() && this.extensionGalleryService.isEnabled(); } constructor( @IEnvironmentService environmentService: IEnvironmentService, @@ -61,7 +62,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse } async pull(): Promise { - if (!this.enabled) { + if (!this.isEnabled()) { this.logService.info('Extensions: Skipped pulling extensions as it is disabled.'); return; } @@ -94,7 +95,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse } async push(): Promise { - if (!this.enabled) { + if (!this.isEnabled()) { this.logService.info('Extensions: Skipped pushing extensions as it is disabled.'); return; } @@ -118,14 +119,6 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse } - async sync(ref?: string): Promise { - if (!this.extensionGalleryService.isEnabled()) { - this.logService.info('Extensions: Skipping synchronizing extensions as gallery is disabled.'); - return; - } - return super.sync(ref); - } - async stop(): Promise { } accept(content: string): Promise { @@ -148,17 +141,10 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse return null; } - protected async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise { - try { - const previewResult = await this.getPreview(remoteUserData, lastSyncUserData); - await this.apply(previewResult); - } catch (e) { - this.setStatus(SyncStatus.Idle); - throw e; - } - - this.logService.trace('Extensions: Finished synchronizing extensions.'); - this.setStatus(SyncStatus.Idle); + protected async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise { + const previewResult = await this.getPreview(remoteUserData, lastSyncUserData); + await this.apply(previewResult); + return SyncStatus.Idle; } private async getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise { @@ -194,7 +180,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse if (added.length || removed.length || updated.length) { // back up all disabled or market place extensions const backUpExtensions = localExtensions.filter(e => e.disabled || !!e.identifier.uuid); - await this.backupLocal(VSBuffer.fromString(JSON.stringify(backUpExtensions))); + await this.backupLocal(VSBuffer.fromString(JSON.stringify(backUpExtensions, null, '\t'))); skippedExtensions = await this.updateLocalExtensions(added, removed, updated, skippedExtensions); } diff --git a/src/vs/platform/userDataSync/common/globalStateSync.ts b/src/vs/platform/userDataSync/common/globalStateSync.ts index 9e6b1d16a43..ff9dc75d808 100644 --- a/src/vs/platform/userDataSync/common/globalStateSync.ts +++ b/src/vs/platform/userDataSync/common/globalStateSync.ts @@ -47,7 +47,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs } async pull(): Promise { - if (!this.enabled) { + if (!this.isEnabled()) { this.logService.info('UI State: Skipped pulling ui state as it is disabled.'); return; } @@ -79,7 +79,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs } async push(): Promise { - if (!this.enabled) { + if (!this.isEnabled()) { this.logService.info('UI State: Skipped pushing UI State as it is disabled.'); return; } @@ -124,20 +124,13 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs return null; } - protected async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { - try { - const result = await this.getPreview(remoteUserData, lastSyncUserData); - await this.apply(result); - this.logService.trace('UI State: Finished synchronizing ui state.'); - } catch (e) { - this.setStatus(SyncStatus.Idle); - throw e; - } finally { - this.setStatus(SyncStatus.Idle); - } + protected async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { + const result = await this.getPreview(remoteUserData, lastSyncUserData); + await this.apply(result); + return SyncStatus.Idle; } - private async getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, ): Promise { + private async getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null,): Promise { const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null; const lastSyncGlobalState = lastSyncUserData && lastSyncUserData.syncData ? JSON.parse(lastSyncUserData.syncData.content) : null; @@ -165,7 +158,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs if (local) { // update local this.logService.trace('UI State: Updating local ui state...'); - await this.backupLocal(VSBuffer.fromString(JSON.stringify(localUserData))); + await this.backupLocal(VSBuffer.fromString(JSON.stringify(localUserData, null, '\t'))); await this.writeLocalGlobalState(local); this.logService.info('UI State: Updated local ui state'); } diff --git a/src/vs/platform/userDataSync/common/keybindingsSync.ts b/src/vs/platform/userDataSync/common/keybindingsSync.ts index 8d1d78409fd..e6e7e2931da 100644 --- a/src/vs/platform/userDataSync/common/keybindingsSync.ts +++ b/src/vs/platform/userDataSync/common/keybindingsSync.ts @@ -47,7 +47,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem } async pull(): Promise { - if (!this.enabled) { + if (!this.isEnabled()) { this.logService.info('Keybindings: Skipped pulling keybindings as it is disabled.'); return; } @@ -89,7 +89,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem } async push(): Promise { - if (!this.enabled) { + if (!this.isEnabled()) { this.logService.info('Keybindings: Skipped pushing keybindings as it is disabled.'); return; } @@ -161,29 +161,22 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem return content !== null ? this.getKeybindingsContentFromSyncContent(content) : null; } - protected async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { + protected async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { try { const result = await this.getPreview(remoteUserData, lastSyncUserData); if (result.hasConflicts) { - this.logService.info('Keybindings: Detected conflicts while synchronizing keybindings.'); - this.setStatus(SyncStatus.HasConflicts); - return; - } - try { - await this.apply(); - this.logService.trace('Keybindings: Finished synchronizing keybindings...'); - } finally { - this.setStatus(SyncStatus.Idle); + return SyncStatus.HasConflicts; } + await this.apply(); + return SyncStatus.Idle; } catch (e) { this.syncPreviewResultPromise = null; - this.setStatus(SyncStatus.Idle); if (e instanceof UserDataSyncError) { switch (e.code) { case UserDataSyncErrorCode.LocalPreconditionFailed: // Rejected as there is a new local version. Syncing again. this.logService.info('Keybindings: Failed to synchronize keybindings as there is a new local version available. Synchronizing again...'); - return this.sync(remoteUserData.ref); + return this.performSync(remoteUserData, lastSyncUserData); } } throw e; diff --git a/src/vs/platform/userDataSync/common/settingsSync.ts b/src/vs/platform/userDataSync/common/settingsSync.ts index 4dcce8bb0ee..dbbe9972f31 100644 --- a/src/vs/platform/userDataSync/common/settingsSync.ts +++ b/src/vs/platform/userDataSync/common/settingsSync.ts @@ -77,7 +77,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement } async pull(): Promise { - if (!this.enabled) { + if (!this.isEnabled()) { this.logService.info('Settings: Skipped pulling settings as it is disabled.'); return; } @@ -123,7 +123,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement } async push(): Promise { - if (!this.enabled) { + if (!this.isEnabled()) { this.logService.info('Settings: Skipped pushing settings as it is disabled.'); return; } @@ -220,33 +220,26 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement if (this.status === SyncStatus.HasConflicts) { const preview = await this.syncPreviewResultPromise!; this.cancel(); - await this.doSync(preview.remoteUserData, preview.lastSyncUserData, resolvedConflicts); + await this.performSync(preview.remoteUserData, preview.lastSyncUserData, resolvedConflicts); } } - protected async doSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: { key: string, value: any | undefined }[] = []): Promise { + protected async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: { key: string, value: any | undefined }[] = []): Promise { try { const result = await this.getPreview(remoteUserData, lastSyncUserData, resolvedConflicts); if (result.hasConflicts) { - this.logService.info('Settings: Detected conflicts while synchronizing settings.'); - this.setStatus(SyncStatus.HasConflicts); - return; - } - try { - await this.apply(); - this.logService.trace('Settings: Finished synchronizing settings.'); - } finally { - this.setStatus(SyncStatus.Idle); + return SyncStatus.HasConflicts; } + await this.apply(); + return SyncStatus.Idle; } catch (e) { this.syncPreviewResultPromise = null; - this.setStatus(SyncStatus.Idle); if (e instanceof UserDataSyncError) { switch (e.code) { case UserDataSyncErrorCode.LocalPreconditionFailed: // Rejected as there is a new local version. Syncing again. this.logService.info('Settings: Failed to synchronize settings as there is a new local version available. Synchronizing again...'); - return this.sync(remoteUserData.ref); + return this.performSync(remoteUserData, lastSyncUserData, resolvedConflicts); } } throw e; diff --git a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts index b64192c62d6..b623b9754a0 100644 --- a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts @@ -8,6 +8,11 @@ import { Event, Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { IUserDataSyncLogService, IUserDataSyncService, SyncStatus, IUserDataAutoSyncService, UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; + +type AutoSyncTriggerClassification = { + source: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; +}; export class UserDataAutoSyncService extends Disposable implements IUserDataAutoSyncService { @@ -25,6 +30,7 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, @IAuthenticationTokenService private readonly authTokenService: IAuthenticationTokenService, + @ITelemetryService private readonly telemetryService: ITelemetryService, ) { super(); this.updateEnablement(false, true); @@ -32,7 +38,7 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto this._register(Event.any(authTokenService.onDidChangeToken)(() => this.updateEnablement(true, true))); this._register(Event.any(userDataSyncService.onDidChangeStatus)(() => this.updateEnablement(true, true))); this._register(this.userDataSyncEnablementService.onDidChangeEnablement(() => this.updateEnablement(true, false))); - this._register(this.userDataSyncEnablementService.onDidChangeResourceEnablement(() => this.triggerAutoSync())); + this._register(this.userDataSyncEnablementService.onDidChangeResourceEnablement(() => this.triggerAutoSync(['resourceEnablement']))); } private async updateEnablement(stopIfDisabled: boolean, auto: boolean): Promise { @@ -99,7 +105,8 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto this.successiveFailures = 0; } - async triggerAutoSync(): Promise { + async triggerAutoSync(sources: string[]): Promise { + sources.forEach(source => this.telemetryService.publicLog2<{ source: string }, AutoSyncTriggerClassification>('sync/triggerAutoSync', { source })); if (this.enabled) { return this.syncDelayer.trigger(() => { this.logService.info('Auto Sync: Triggered.'); diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 23bd81b6526..b2911d2a364 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -96,7 +96,6 @@ export function registerConfiguration(): IDisposable { const defaultIgnoredSettings = getDefaultIgnoredSettings().filter(s => s !== CONFIGURATION_SYNC_STORE_KEY); const settings = Object.keys(allSettings.properties).filter(setting => defaultIgnoredSettings.indexOf(setting) === -1); const ignoredSettings = defaultIgnoredSettings.filter(setting => disallowedIgnoredSettings.indexOf(setting) === -1); - console.log(ignoredSettings); const ignoredSettingsSchema: IJSONSchema = { items: { type: 'string', @@ -278,7 +277,7 @@ export interface IUserDataSyncService { readonly conflictsSources: SyncSource[]; readonly onDidChangeConflicts: Event; - readonly onDidChangeLocal: Event; + readonly onDidChangeLocal: Event; readonly onSyncErrors: Event<[SyncSource, UserDataSyncError][]>; readonly lastSyncTime: number | undefined; @@ -299,7 +298,7 @@ export const IUserDataAutoSyncService = createDecorator; - triggerAutoSync(): Promise; + triggerAutoSync(sources: string[]): Promise; } export const IUserDataSyncUtilService = createDecorator('IUserDataSyncUtilService'); diff --git a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts index 04c8649a703..a2b8c25e64d 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts @@ -86,7 +86,7 @@ export class UserDataAutoSyncChannel implements IServerChannel { call(context: any, command: string, args?: any): Promise { switch (command) { - case 'triggerAutoSync': return this.service.triggerAutoSync(); + case 'triggerAutoSync': return this.service.triggerAutoSync(args[0]); } throw new Error('Invalid call'); } diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index 07203267160..4d0a2619afe 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -34,7 +34,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ private _onDidChangeStatus: Emitter = this._register(new Emitter()); readonly onDidChangeStatus: Event = this._onDidChangeStatus.event; - readonly onDidChangeLocal: Event; + readonly onDidChangeLocal: Event; private _conflictsSources: SyncSource[] = []; get conflictsSources(): SyncSource[] { return this._conflictsSources; } @@ -74,7 +74,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } this._lastSyncTime = this.storageService.getNumber(LAST_SYNC_TIME_KEY, StorageScope.GLOBAL, undefined); - this.onDidChangeLocal = Event.any(...this.synchronisers.map(s => s.onDidChangeLocal)); + this.onDidChangeLocal = Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeLocal, () => s.source))); } async pull(): Promise { diff --git a/src/vs/platform/userDataSync/electron-browser/userDataAutoSyncService.ts b/src/vs/platform/userDataSync/electron-browser/userDataAutoSyncService.ts index 770865e5e26..6fa694e1d27 100644 --- a/src/vs/platform/userDataSync/electron-browser/userDataAutoSyncService.ts +++ b/src/vs/platform/userDataSync/electron-browser/userDataAutoSyncService.ts @@ -8,6 +8,7 @@ import { Event } from 'vs/base/common/event'; import { IElectronService } from 'vs/platform/electron/node/electron'; import { UserDataAutoSyncService as BaseUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataAutoSyncService'; import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; export class UserDataAutoSyncService extends BaseUserDataAutoSyncService { @@ -17,15 +18,15 @@ export class UserDataAutoSyncService extends BaseUserDataAutoSyncService { @IElectronService electronService: IElectronService, @IUserDataSyncLogService logService: IUserDataSyncLogService, @IAuthenticationTokenService authTokenService: IAuthenticationTokenService, + @ITelemetryService telemetryService: ITelemetryService, ) { - super(userDataSyncEnablementService, userDataSyncService, logService, authTokenService); + super(userDataSyncEnablementService, userDataSyncService, logService, authTokenService, telemetryService); - // Sync immediately if there is a local change. - this._register(Event.debounce(Event.any( - electronService.onWindowFocus, - electronService.onWindowOpen, + this._register(Event.debounce(Event.any( + Event.map(electronService.onWindowFocus, () => 'windowFocus'), + Event.map(electronService.onWindowOpen, () => 'windowOpen'), userDataSyncService.onDidChangeLocal, - ), () => undefined, 500)(() => this.triggerAutoSync())); + ), (last, source) => last ? [...last, source] : [source], 1000)(sources => this.triggerAutoSync(sources))); } } diff --git a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts new file mode 100644 index 00000000000..14c69ca7bc1 --- /dev/null +++ b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts @@ -0,0 +1,200 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ResourceKey, IUserDataSyncStoreService, SyncSource, SyncStatus, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; +import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { AbstractSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { Barrier } from 'vs/base/common/async'; +import { Emitter, Event } from 'vs/base/common/event'; + +class TestSynchroniser extends AbstractSynchroniser { + + syncBarrier: Barrier = new Barrier(); + syncResult: { status?: SyncStatus, error?: boolean } = {}; + onDoSyncCall: Emitter = this._register(new Emitter()); + + readonly resourceKey: ResourceKey = 'settings'; + protected readonly version: number = 1; + + private cancelled: boolean = false; + + protected async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { + this.cancelled = false; + this.onDoSyncCall.fire(); + await this.syncBarrier.wait(); + + if (this.cancelled) { + return SyncStatus.Idle; + } + + if (this.syncResult.error) { + throw new Error('failed'); + } + + await this.apply(remoteUserData.ref); + return this.syncResult.status || SyncStatus.Idle; + } + + async apply(ref: string): Promise { + ref = await this.userDataSyncStoreService.write(this.resourceKey, '', ref); + await this.updateLastSyncUserData({ ref, syncData: { content: '', version: this.version } }); + } + + stop(): void { + this.cancelled = true; + this.syncBarrier.open(); + } + +} + +suite('TestSynchronizer', () => { + + const disposableStore = new DisposableStore(); + const server = new UserDataSyncTestServer(); + let client: UserDataSyncClient; + let userDataSyncStoreService: IUserDataSyncStoreService; + + setup(async () => { + client = disposableStore.add(new UserDataSyncClient(server)); + await client.setUp(); + userDataSyncStoreService = client.instantiationService.get(IUserDataSyncStoreService); + disposableStore.add(toDisposable(() => userDataSyncStoreService.clear())); + }); + + teardown(() => disposableStore.clear()); + + test('status is syncing', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncSource.Settings); + + const actual: SyncStatus[] = []; + disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); + + const promise = Event.toPromise(testObject.onDoSyncCall.event); + + testObject.sync(); + await promise; + + assert.deepEqual(actual, [SyncStatus.Syncing]); + assert.deepEqual(testObject.status, SyncStatus.Syncing); + + testObject.stop(); + }); + + test('status is set correctly when sync is finished', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncSource.Settings); + testObject.syncBarrier.open(); + + const actual: SyncStatus[] = []; + disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); + await testObject.sync(); + + assert.deepEqual(actual, [SyncStatus.Syncing, SyncStatus.Idle]); + assert.deepEqual(testObject.status, SyncStatus.Idle); + }); + + test('status is set correctly when sync has conflicts', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncSource.Settings); + testObject.syncResult = { status: SyncStatus.HasConflicts }; + testObject.syncBarrier.open(); + + const actual: SyncStatus[] = []; + disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); + await testObject.sync(); + + assert.deepEqual(actual, [SyncStatus.Syncing, SyncStatus.HasConflicts]); + assert.deepEqual(testObject.status, SyncStatus.HasConflicts); + }); + + test('status is set correctly when sync has errors', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncSource.Settings); + testObject.syncResult = { error: true }; + testObject.syncBarrier.open(); + + const actual: SyncStatus[] = []; + disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); + + try { + await testObject.sync(); + assert.fail('Should fail'); + } catch (e) { + assert.deepEqual(actual, [SyncStatus.Syncing, SyncStatus.Idle]); + assert.deepEqual(testObject.status, SyncStatus.Idle); + } + }); + + test('sync should not run if syncing already', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncSource.Settings); + const promise = Event.toPromise(testObject.onDoSyncCall.event); + + testObject.sync(); + await promise; + + const actual: SyncStatus[] = []; + disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); + await testObject.sync(); + + assert.deepEqual(actual, []); + assert.deepEqual(testObject.status, SyncStatus.Syncing); + + testObject.stop(); + }); + + test('sync should not run if disabled', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncSource.Settings); + client.instantiationService.get(IUserDataSyncEnablementService).setResourceEnablement(testObject.resourceKey, false); + + const actual: SyncStatus[] = []; + disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); + + await testObject.sync(); + + assert.deepEqual(actual, []); + assert.deepEqual(testObject.status, SyncStatus.Idle); + }); + + test('sync should not run if there are conflicts', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncSource.Settings); + testObject.syncResult = { status: SyncStatus.HasConflicts }; + testObject.syncBarrier.open(); + await testObject.sync(); + + const actual: SyncStatus[] = []; + disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); + await testObject.sync(); + + assert.deepEqual(actual, []); + assert.deepEqual(testObject.status, SyncStatus.HasConflicts); + }); + + test('request latest data on precondition failure', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncSource.Settings); + // Sync once + testObject.syncBarrier.open(); + await testObject.sync(); + testObject.syncBarrier = new Barrier(); + + // update remote data before syncing so that 412 is thrown by server + const disposable = testObject.onDoSyncCall.event(async () => { + disposable.dispose(); + await testObject.apply(ref); + server.reset(); + testObject.syncBarrier.open(); + }); + + // Start sycing + const { ref } = await userDataSyncStoreService.read(testObject.resourceKey, null); + await testObject.sync(ref); + + assert.deepEqual(server.requests, [ + { type: 'POST', url: `${server.url}/v1/resource/${testObject.resourceKey}`, headers: { 'If-Match': ref } }, + { type: 'GET', url: `${server.url}/v1/resource/${testObject.resourceKey}/latest`, headers: {} }, + { type: 'POST', url: `${server.url}/v1/resource/${testObject.resourceKey}`, headers: { 'If-Match': `${parseInt(ref) + 1}` } }, + ]); + }); + + +}); diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index 74a42082391..5b95a50591e 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -61,7 +61,14 @@ export class UserDataSyncClient extends Disposable { const logService = new NullLogService(); this.instantiationService.stub(ILogService, logService); - this.instantiationService.stub(IProductService, { _serviceBrand: undefined, ...product }); + this.instantiationService.stub(IProductService, { + _serviceBrand: undefined, ...product, ...{ + 'configurationSync.store': { + url: this.testServer.url, + authenticationProviderId: 'test' + } + } + }); const fileService = this._register(new FileService(logService)); fileService.registerProvider(Schemas.inMemory, new InMemoryFileSystemProvider()); @@ -69,13 +76,6 @@ export class UserDataSyncClient extends Disposable { this.instantiationService.stub(IStorageService, new InMemoryStorageService()); - await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ - 'configurationSync.store': { - url: this.testServer.url, - authenticationProviderId: 'test' - } - }))); - const configurationService = new ConfigurationService(environmentService.settingsResource, fileService); await configurationService.initialize(); this.instantiationService.stub(IConfigurationService, configurationService); @@ -106,9 +106,8 @@ export class UserDataSyncClient extends Disposable { this.instantiationService.stub(ISettingsSyncService, this.instantiationService.createInstance(SettingsSynchroniser)); this.instantiationService.stub(IUserDataSyncService, this.instantiationService.createInstance(UserDataSyncService)); - if (empty) { - await fileService.del(environmentService.settingsResource); - } else { + if (!empty) { + await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({}))); await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([]))); await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'en' }))); } diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index 78210255a5f..348d06948f6 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -3,11 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IProcessEnvironment, isMacintosh, isLinux, isWeb } from 'vs/base/common/platform'; +import { isMacintosh, isLinux, isWeb } from 'vs/base/common/platform'; import { ParsedArgs, IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; -import { ExportData } from 'vs/base/common/performance'; -import { LogLevel } from 'vs/platform/log/common/log'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -220,41 +218,17 @@ export interface IAddFoldersRequest { } export interface IWindowConfiguration extends ParsedArgs { - machineId?: string; // NOTE: This is undefined in the web, the telemetry service directly resolves this. - windowId: number; // TODO: should we deprecate this in favor of sessionId? sessionId: string; - logLevel: LogLevel; - mainPid: number; - - appRoot: string; - execPath: string; - isInitialStartup?: boolean; - - userEnv: IProcessEnvironment; - nodeCachedDataDir?: string; - - backupPath?: string; backupWorkspaceResource?: URI; - workspace?: IWorkspaceIdentifier; - folderUri?: ISingleFolderWorkspaceIdentifier; - remoteAuthority?: string; connectionToken?: string; - zoomLevel?: number; - fullscreen?: boolean; - maximized?: boolean; highContrast?: boolean; - accessibilitySupport?: boolean; - partsSplashPath?: string; - - perfEntries: ExportData; filesToOpenOrCreate?: IPath[]; filesToDiff?: IPath[]; - filesToWait?: IPathsToWaitFor; } export interface IRunActionInWindowRequest { diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index 9d519473683..3e05c84fcd9 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { OpenContext, IWindowConfiguration, IWindowOpenable, IOpenEmptyWindowOptions } from 'vs/platform/windows/common/windows'; +import { OpenContext, IWindowOpenable, IOpenEmptyWindowOptions } from 'vs/platform/windows/common/windows'; +import { INativeWindowConfiguration } from 'vs/platform/windows/node/window'; import { ParsedArgs } from 'vs/platform/environment/common/environment'; import { Event } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -39,7 +40,7 @@ export interface ICodeWindow extends IDisposable { readonly id: number; readonly win: BrowserWindow; - readonly config: IWindowConfiguration | undefined; + readonly config: INativeWindowConfiguration | undefined; readonly openedFolderUri?: URI; readonly openedWorkspace?: IWorkspaceIdentifier; @@ -60,8 +61,8 @@ export interface ICodeWindow extends IDisposable { addTabbedWindow(window: ICodeWindow): void; - load(config: IWindowConfiguration, isReload?: boolean): void; - reload(configuration?: IWindowConfiguration, cli?: ParsedArgs): void; + load(config: INativeWindowConfiguration, isReload?: boolean): void; + reload(configuration?: INativeWindowConfiguration, cli?: ParsedArgs): void; focus(): void; close(): void; diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index bc8e53b1e21..8562c033266 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -18,8 +18,8 @@ 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 { IWindowSettings, OpenContext, IPath, IPathsToWaitFor, isFileToOpen, isWorkspaceToOpen, isFolderToOpen, IWindowOpenable, IOpenEmptyWindowOptions, IAddFoldersRequest } from 'vs/platform/windows/common/windows'; +import { getLastActiveWindow, findBestWindowOrFolderForFile, findWindowOnWorkspace, findWindowOnExtensionDevelopmentPath, findWindowOnWorkspaceOrFolderUri, INativeWindowConfiguration } from 'vs/platform/windows/node/window'; import { Emitter } from 'vs/base/common/event'; import product from 'vs/platform/product/common/product'; import { IWindowsMainService, IOpenConfiguration, IWindowsCountChangedEvent, ICodeWindow, IWindowState as ISingleWindowState, WindowMode } from 'vs/platform/windows/electron-main/windows'; @@ -1354,8 +1354,8 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic private openInBrowserWindow(options: IOpenBrowserWindowOptions): ICodeWindow { - // Build IWindowConfiguration from config and options - const configuration: IWindowConfiguration = mixin({}, options.cli); // inherit all properties from CLI + // Build INativeWindowConfiguration from config and options + const configuration: INativeWindowConfiguration = mixin({}, options.cli); // inherit all properties from CLI configuration.appRoot = this.environmentService.appRoot; configuration.machineId = this.machineId; configuration.nodeCachedDataDir = this.environmentService.nodeCachedDataDir; @@ -1482,7 +1482,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return window; } - private doOpenInBrowserWindow(window: ICodeWindow, configuration: IWindowConfiguration, options: IOpenBrowserWindowOptions): void { + private doOpenInBrowserWindow(window: ICodeWindow, configuration: INativeWindowConfiguration, options: IOpenBrowserWindowOptions): void { // Register window for backups if (!configuration.extensionDevelopmentPath) { @@ -1500,7 +1500,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic window.load(configuration); } - private getNewWindowState(configuration: IWindowConfiguration): INewWindowState { + private getNewWindowState(configuration: INativeWindowConfiguration): INewWindowState { const lastActive = this.getLastActiveWindow(); // Restore state unless we are running extension tests diff --git a/src/vs/platform/windows/node/window.ts b/src/vs/platform/windows/node/window.ts index 08a4e800d3b..ae9c2b70e58 100644 --- a/src/vs/platform/windows/node/window.ts +++ b/src/vs/platform/windows/node/window.ts @@ -3,12 +3,42 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { OpenContext, IOpenWindowOptions } from 'vs/platform/windows/common/windows'; +import { OpenContext, IOpenWindowOptions, IWindowConfiguration, IPathsToWaitFor } from 'vs/platform/windows/common/windows'; import { URI } from 'vs/base/common/uri'; import * as platform from 'vs/base/common/platform'; import * as extpath from 'vs/base/common/extpath'; import { IWorkspaceIdentifier, IResolvedWorkspace, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { isEqual, isEqualOrParent } from 'vs/base/common/resources'; +import { LogLevel } from 'vs/platform/log/common/log'; +import { ExportData } from 'vs/base/common/performance'; + +export interface INativeWindowConfiguration extends IWindowConfiguration { + mainPid: number; + + windowId: number; + machineId: string; // NOTE: This is undefined in the web, the telemetry service directly resolves this. + + appRoot: string; + execPath: string; + backupPath?: string; + + nodeCachedDataDir?: string; + partsSplashPath: string; + + workspace?: IWorkspaceIdentifier; + folderUri?: ISingleFolderWorkspaceIdentifier; + + isInitialStartup?: boolean; + logLevel: LogLevel; + zoomLevel?: number; + fullscreen?: boolean; + maximized?: boolean; + accessibilitySupport?: boolean; + perfEntries: ExportData; + + userEnv: platform.IProcessEnvironment; + filesToWait?: IPathsToWaitFor; +} export interface INativeOpenWindowOptions extends IOpenWindowOptions { diffMode?: boolean; diff --git a/src/vs/platform/workspace/common/workspace.ts b/src/vs/platform/workspace/common/workspace.ts index 7e31738058d..b10c023d005 100644 --- a/src/vs/platform/workspace/common/workspace.ts +++ b/src/vs/platform/workspace/common/workspace.ts @@ -81,7 +81,7 @@ export interface IWorkspaceFoldersChangeEvent { } export namespace IWorkspace { - export function isIWorkspace(thing: any): thing is IWorkspace { + export function isIWorkspace(thing: unknown): thing is IWorkspace { return thing && typeof thing === 'object' && typeof (thing as IWorkspace).id === 'string' && Array.isArray((thing as IWorkspace).folders); @@ -115,7 +115,7 @@ export interface IWorkspaceFolderData { /** * The name of this workspace folder. Defaults to - * the basename its [uri-path](#Uri.path) + * the basename of its [uri-path](#Uri.path) */ readonly name: string; @@ -126,7 +126,7 @@ export interface IWorkspaceFolderData { } export namespace IWorkspaceFolder { - export function isIWorkspaceFolder(thing: any): thing is IWorkspaceFolder { + export function isIWorkspaceFolder(thing: unknown): thing is IWorkspaceFolder { return thing && typeof thing === 'object' && URI.isUri((thing as IWorkspaceFolder).uri) && typeof (thing as IWorkspaceFolder).name === 'string' diff --git a/src/vs/platform/workspaces/common/workspaces.ts b/src/vs/platform/workspaces/common/workspaces.ts index efef7f31107..b904c106efa 100644 --- a/src/vs/platform/workspaces/common/workspaces.ts +++ b/src/vs/platform/workspaces/common/workspaces.ts @@ -93,7 +93,7 @@ export function reviveWorkspaceIdentifier(workspace: { id: string, configPath: U return { id: workspace.id, configPath: URI.revive(workspace.configPath) }; } -export function isStoredWorkspaceFolder(thing: any): thing is IStoredWorkspaceFolder { +export function isStoredWorkspaceFolder(thing: unknown): thing is IStoredWorkspaceFolder { return isRawFileWorkspaceFolder(thing) || isRawUriWorkspaceFolder(thing); } @@ -148,11 +148,11 @@ export interface IEnterWorkspaceResult { backupPath?: string; } -export function isSingleFolderWorkspaceIdentifier(obj: any): obj is ISingleFolderWorkspaceIdentifier { +export function isSingleFolderWorkspaceIdentifier(obj: unknown): obj is ISingleFolderWorkspaceIdentifier { return obj instanceof URI; } -export function isWorkspaceIdentifier(obj: any): obj is IWorkspaceIdentifier { +export function isWorkspaceIdentifier(obj: unknown): obj is IWorkspaceIdentifier { const workspaceIdentifier = obj as IWorkspaceIdentifier; return workspaceIdentifier && typeof workspaceIdentifier.id === 'string' && workspaceIdentifier.configPath instanceof URI; diff --git a/src/vs/platform/workspaces/electron-main/workspacesService.ts b/src/vs/platform/workspaces/electron-main/workspacesService.ts index 70f2bd9bb79..3084a7ea969 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesService.ts @@ -10,7 +10,7 @@ import { IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/wor import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; -export class WorkspacesService implements AddFirstParameterToFunctions /* only methods, not events */, number /* window ID */> { +export class WorkspacesService implements AddFirstParameterToFunctions /* only methods, not events */, number /* window ID */> { _serviceBrand: undefined; diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index ead7e54c39f..9e852eca701 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -1500,7 +1500,7 @@ declare module 'vscode' { /** * A file system watcher notifies about changes to files and folders - * on disk. + * on disk or from other [FileSystemProviders](#FileSystemProvider). * * To get an instance of a `FileSystemWatcher` use * [createFileSystemWatcher](#workspace.createFileSystemWatcher). @@ -1806,7 +1806,7 @@ declare module 'vscode' { placeHolder?: string; /** - * Set to `true` to show a password prompt that will not show the typed value. + * Controls if a password input is shown. Password input hides the typed text. */ password?: boolean; @@ -2020,7 +2020,7 @@ declare module 'vscode' { * Base kind for source actions: `source` * * Source code actions apply to the entire file. They must be explicitly requested and will not show in the - * normal [light bulb](https://code.visualstudio.com/docs/editor/editingevolved#_code-action) menu. Source actions + * normal [lightbulb](https://code.visualstudio.com/docs/editor/editingevolved#_code-action) menu. Source actions * can be run on save using `editor.codeActionsOnSave` and are also shown in the `source` context menu. */ static readonly Source: CodeActionKind; @@ -2086,7 +2086,7 @@ declare module 'vscode' { /** * Requested kind of actions to return. * - * Actions not of this kind are filtered out before being shown by the lightbulb. + * Actions not of this kind are filtered out before being shown by the [lightbulb](https://code.visualstudio.com/docs/editor/editingevolved#_code-action). */ readonly only?: CodeActionKind; } @@ -2138,7 +2138,15 @@ declare module 'vscode' { /** * Marks that the code action cannot currently be applied. * - * Disabled code actions will be surfaced in the refactor UI but cannot be applied. + * - Disabled code actions are not shown in automatic [lightbulb](https://code.visualstudio.com/docs/editor/editingevolved#_code-action) + * code action menu. + * + * - Disabled actions are shown as faded out in the code action menu when the user request a more specific type + * of code action, such as refactorings. + * + * - If the user has a [keybinding](https://code.visualstudio.com/docs/editor/refactoring#_keybindings-for-code-actions) + * that auto applies a code action and only a disabled code actions are returned, VS Code will show the user a + * message with `reason` in the editor. */ disabled?: { /** @@ -2163,7 +2171,7 @@ declare module 'vscode' { /** * The code action interface defines the contract between extensions and - * the [light bulb](https://code.visualstudio.com/docs/editor/editingevolved#_code-action) feature. + * the [lightbulb](https://code.visualstudio.com/docs/editor/editingevolved#_code-action) feature. * * A code action can be any command that is [known](#commands.getCommands) to the system. */ @@ -2495,16 +2503,18 @@ declare module 'vscode' { /** * The evaluatable expression provider interface defines the contract between extensions and - * the debug hover. + * the debug hover. In this contract the provider returns an evaluatable expression for a given position + * in a document and VS Code evaluates this expression in the active debug session and shows the result in a debug hover. */ export interface EvaluatableExpressionProvider { /** * Provide an evaluatable expression for the given document and position. + * VS Code will evaluate this expression in the active debug session and will show the result in the debug hover. * The expression can be implicitly specified by the range in the underlying document or by explicitly returning an expression. * - * @param document The document in which the debug hover is opened. - * @param position The position in the document where the debug hover is opened. + * @param document The document for which the debug hover is about to appear. + * @param position The line and character position in the document where the debug hover is about to appear. * @param token A cancellation token. * @return An EvaluatableExpression or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined` or `null`. @@ -9174,6 +9184,7 @@ declare module 'vscode' { /** * Register a provider that locates evaluatable expressions in text documents. + * VS Code will evaluate the expression in the active debug session and will show the result in the debug hover. * * If multiple providers are registered for a language an arbitrary provider will be used. * diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 50c03acaa3f..c5a0c707ce5 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -20,7 +20,7 @@ declare module 'vscode' { export interface AuthenticationSession { id: string; - accessToken(): Promise; + getAccessToken(): Thenable; accountName: string; scopes: string[] } @@ -58,13 +58,13 @@ declare module 'vscode' { /** * Returns an array of current sessions. */ - getSessions(): Promise>; + getSessions(): Thenable>; /** * Prompts a user to login. */ - login(scopes: string[]): Promise; - logout(sessionId: string): Promise; + login(scopes: string[]): Thenable; + logout(sessionId: string): Thenable; } export namespace authentication { @@ -257,6 +257,11 @@ declare module 'vscode' { * semantic tokens. */ export interface DocumentSemanticTokensProvider { + /** + * An optional event to signal that the semantic tokens from this provider have changed. + */ + onDidChangeSemanticTokens?: Event; + /** * A file can contain many tokens, perhaps even hundreds of thousands of tokens. Therefore, to improve * the memory consumption around describing semantic tokens, we have decided to avoid allocating an object @@ -314,6 +319,9 @@ declare module 'vscode' { * // 1st token, 2nd token, 3rd token * [ 2,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] * ``` + * + * *NOTE*: When doing edits, it is possible that multiple edits occur until VS Code decides to invoke the semantic tokens provider. + * *NOTE*: If the provider cannot temporarily compute semantic tokens, it can indicate this by throwing an error with the message 'Busy'. */ provideDocumentSemanticTokens(document: TextDocument, token: CancellationToken): ProviderResult; @@ -366,7 +374,6 @@ declare module 'vscode' { * edit: { start: 10, deleteCount: 1, data: [1,3,5,0,2,2] } // replace integer at offset 10 with [1,3,5,0,2,2] * ``` * - * *NOTE*: When doing edits, it is possible that multiple edits occur until VS Code decides to invoke the semantic tokens provider. * *NOTE*: If the provider cannot compute `SemanticTokensEdits`, it can "give up" and return all the tokens in the document again. * *NOTE*: All edits in `SemanticTokensEdits` contain indices in the old integers array, so they all refer to the previous result state. */ @@ -1273,14 +1280,14 @@ declare module 'vscode' { * in an operation that takes time to complete, your extension may decide to finish the ongoing backup rather * than cancelling it to ensure that VS Code has some valid backup. */ - backup(cancellation: CancellationToken): Thenable; + backup(cancellation: CancellationToken): Thenable; } /** * Represents a custom document for a custom webview editor. * - * Custom documents are only used within a given `WebviewCustomEditorProvider`. The lifecycle of a - * `WebviewEditorCustomDocument` is managed by VS Code. When more more references remain to a given `WebviewEditorCustomDocument` + * Custom documents are only used within a given `CustomEditorProvider`. The lifecycle of a + * `CustomDocument` is managed by VS Code. When more more references remain to a given `CustomDocument` * then it is disposed of. * * @param UserDataType Type of custom object that extensions can store on the document. @@ -1297,7 +1304,7 @@ declare module 'vscode' { readonly uri: Uri; /** - * Event fired when there are no more references to the `WebviewEditorCustomDocument`. + * Event fired when there are no more references to the `CustomDocument`. */ readonly onDidDispose: Event; @@ -1313,7 +1320,7 @@ declare module 'vscode' { /** * Provider for webview editors that use a custom data model. * - * Custom webview editors use [`WebviewEditorCustomDocument`](#WebviewEditorCustomDocument) as their data model. + * Custom webview editors use [`CustomDocument`](#CustomDocument) as their data model. * This gives extensions full control over actions such as edit, save, and backup. * * You should use custom text based editors when dealing with binary files or more complex scenarios. For simple text @@ -1321,24 +1328,26 @@ declare module 'vscode' { */ export interface CustomEditorProvider { /** - * Create the model for a given + * Resolve the model for a given resource. * - * @param document Resource being resolved. + * @param document Document to resolve. + * + * @return The capabilities of the resolved document. */ resolveCustomDocument(document: CustomDocument): Thenable; /** * Resolve a webview editor for a given resource. * - * To resolve a webview editor, a provider must fill in its initial html content and hook up all + * To resolve a webview editor, the provider must fill in its initial html content and hook up all * the event listeners it is interested it. The provider should also take ownership of the passed in `WebviewPanel`. * - * @param document Document for resource being resolved. - * @param webview Webview being resolved. The provider should take ownership of this webview. + * @param document Document for the resource being resolved. + * @param webviewPanel Webview to resolve. The provider should take ownership of this webview. * * @return Thenable indicating that the webview editor has been resolved. */ - resolveCustomEditor(document: CustomDocument, webview: WebviewPanel): Thenable; + resolveCustomEditor(document: CustomDocument, webviewPanel: WebviewPanel): Thenable; } /** @@ -1349,7 +1358,7 @@ declare module 'vscode' { * undo and backup. The provider is responsible for synchronizing text changes between the webview and the `TextDocument`. * * You should use text based webview editors when dealing with text based file formats, such as `xml` or `json`. - * For binary files or more specialized use cases, see [WebviewCustomEditorProvider](#WebviewCustomEditorProvider). + * For binary files or more specialized use cases, see [CustomEditorProvider](#CustomEditorProvider). */ export interface CustomTextEditorProvider { /** @@ -1359,11 +1368,11 @@ declare module 'vscode' { * the event listeners it is interested it. The provider should also take ownership of the passed in `WebviewPanel`. * * @param document Resource being resolved. - * @param webview Webview being resolved. The provider should take ownership of this webview. + * @param webviewPanel Webview to resolve. The provider should take ownership of this webview. * * @return Thenable indicating that the webview editor has been resolved. */ - resolveCustomTextEditor(document: TextDocument, webview: WebviewPanel): Thenable; + resolveCustomTextEditor(document: TextDocument, webviewPanel: WebviewPanel): Thenable; } namespace window { @@ -1598,7 +1607,7 @@ declare module 'vscode' { /** * The maximum number or the ending cursor of timeline items that should be returned. */ - limit?: number | string; + limit?: number | { cursor: string }; } export interface TimelineProvider { @@ -1680,9 +1689,10 @@ declare module 'vscode' { * * The documentation is shown in the code actions menu if either: * - * - Code actions of `kind` are requested by VS Code. Note that in this case, we always pick the most specific - * documentation. For example, if documentation for both `Refactor` and `RefactorExtract` is provided, and we - * request code actions for `RefactorExtract`, we prefer the more specific documentation for `RefactorExtract`. + * - Code actions of `kind` are requested by VS Code. In this case, VS Code will show the documentation that + * most closely matches the requested code action kind. For example, if a provider has documentation for + * both `Refactor` and `RefactorExtract`, when the user requests code actions for `RefactorExtract`, + * VS Code will use the documentation for `RefactorExtract` intead of the documentation for `Refactor`. * * - Any code actions of `kind` are returned by the provider. */ @@ -1703,7 +1713,10 @@ declare module 'vscode' { */ export interface OpenDialogOptions { /** - * Dialog title + * Dialog title. + * + * Depending on the underlying operating system this parameter might be ignored, since some + * systems do not present title on open dialogs. */ title?: string; } @@ -1713,7 +1726,10 @@ declare module 'vscode' { */ export interface SaveDialogOptions { /** - * Dialog title + * Dialog title. + * + * Depending on the underlying operating system this parameter might be ignored, since some + * systems do not present title on save dialogs. */ title?: string; } diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index 311d89790db..15af9ba8bc6 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -25,7 +25,7 @@ export class MainThreadAuthenticationProvider { return { id: session.id, accountName: session.accountName, - accessToken: () => this._proxy.$getSessionAccessToken(this.id, session.id) + getAccessToken: () => this._proxy.$getSessionAccessToken(this.id, session.id) }; }); } @@ -35,7 +35,7 @@ export class MainThreadAuthenticationProvider { return { id: session.id, accountName: session.accountName, - accessToken: () => this._proxy.$getSessionAccessToken(this.id, session.id) + getAccessToken: () => this._proxy.$getSessionAccessToken(this.id, session.id) }; }); } @@ -75,48 +75,52 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu async $getSessionsPrompt(providerId: string, providerName: string, extensionId: string, extensionName: string): Promise { const alwaysAllow = this.storageService.get(`${extensionId}-${providerId}`, StorageScope.GLOBAL); if (alwaysAllow) { - return true; + return alwaysAllow === 'true'; } - const { choice } = await this.dialogService.show( + const { choice, checkboxChecked } = await this.dialogService.show( Severity.Info, nls.localize('confirmAuthenticationAccess', "The extension '{0}' is trying to access authentication information from {1}.", extensionName, providerName), - [nls.localize('cancel', "Cancel"), nls.localize('allow', "Allow"), nls.localize('alwaysAllow', "Always Allow"),], - { cancelId: 0 } + [nls.localize('cancel', "Cancel"), nls.localize('allow', "Allow")], + { + cancelId: 0, + checkbox: { + label: nls.localize('neverAgain', "Don't Show Again") + } + } ); - switch (choice) { - case 1/** Allow */: - return true; - case 2 /** Always Allow */: - this.storageService.store(`${extensionId}-${providerId}`, 'true', StorageScope.GLOBAL); - return true; - default: - return false; + const allow = choice === 1; + if (checkboxChecked) { + this.storageService.store(`${extensionId}-${providerId}`, allow ? 'true' : 'false', StorageScope.GLOBAL); } + + return allow; } async $loginPrompt(providerId: string, providerName: string, extensionId: string, extensionName: string): Promise { const alwaysAllow = this.storageService.get(`${extensionId}-${providerId}`, StorageScope.GLOBAL); if (alwaysAllow) { - return true; + return alwaysAllow === 'true'; } - const { choice } = await this.dialogService.show( + const { choice, checkboxChecked } = await this.dialogService.show( Severity.Info, nls.localize('confirmLogin', "The extension '{0}' wants to sign in using {1}.", extensionName, providerName), - [nls.localize('cancel', "Cancel"), nls.localize('continue', "Continue"), nls.localize('neverAgain', "Don't Show Again")], - { cancelId: 0 } + [nls.localize('cancel', "Cancel"), nls.localize('continue', "Continue")], + { + cancelId: 0, + checkbox: { + label: nls.localize('neverAgain', "Don't Show Again") + } + } ); - switch (choice) { - case 1/** Allow */: - return true; - case 2 /** Always Allow */: - this.storageService.store(`${extensionId}-${providerId}`, 'true', StorageScope.GLOBAL); - return true; - default: - return false; + const allow = choice === 1; + if (checkboxChecked) { + this.storageService.store(`${extensionId}-${providerId}`, allow ? 'true' : 'false', StorageScope.GLOBAL); } + + return allow; } } diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 4287e53a8c0..c079a43ef36 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IDisposable } from 'vs/base/common/lifecycle'; -import { Emitter } from 'vs/base/common/event'; +import { Emitter, Event } from 'vs/base/common/event'; import { ITextModel, ISingleEditOperation } from 'vs/editor/common/model'; import * as modes from 'vs/editor/common/modes'; import * as search from 'vs/workbench/contrib/search/common/search'; @@ -367,8 +367,21 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha // --- semantic tokens - $registerDocumentSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: modes.SemanticTokensLegend): void { - this._registrations.set(handle, modes.DocumentSemanticTokensProviderRegistry.register(selector, new MainThreadDocumentSemanticTokensProvider(this._proxy, handle, legend))); + $registerDocumentSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: modes.SemanticTokensLegend, eventHandle: number | undefined): void { + let event: Event | undefined = undefined; + if (typeof eventHandle === 'number') { + const emitter = new Emitter(); + this._registrations.set(eventHandle, emitter); + event = emitter.event; + } + this._registrations.set(handle, modes.DocumentSemanticTokensProviderRegistry.register(selector, new MainThreadDocumentSemanticTokensProvider(this._proxy, handle, legend, event))); + } + + $emitDocumentSemanticTokensEvent(eventHandle: number): void { + const obj = this._registrations.get(eventHandle); + if (obj instanceof Emitter) { + obj.fire(undefined); + } } $registerDocumentRangeSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: modes.SemanticTokensLegend): void { @@ -661,6 +674,7 @@ export class MainThreadDocumentSemanticTokensProvider implements modes.DocumentS private readonly _proxy: ExtHostLanguageFeaturesShape, private readonly _handle: number, private readonly _legend: modes.SemanticTokensLegend, + public readonly onDidChange: Event | undefined, ) { } diff --git a/src/vs/workbench/api/browser/mainThreadTimeline.ts b/src/vs/workbench/api/browser/mainThreadTimeline.ts index cfeb1c6c38c..c8d6d0a9a96 100644 --- a/src/vs/workbench/api/browser/mainThreadTimeline.ts +++ b/src/vs/workbench/api/browser/mainThreadTimeline.ts @@ -9,7 +9,7 @@ import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; import { MainContext, MainThreadTimelineShape, IExtHostContext, ExtHostTimelineShape, ExtHostContext } from 'vs/workbench/api/common/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; -import { TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor, ITimelineService } from 'vs/workbench/contrib/timeline/common/timeline'; +import { TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor, ITimelineService, InternalTimelineOptions } from 'vs/workbench/contrib/timeline/common/timeline'; @extHostNamedCustomer(MainContext.MainThreadTimeline) export class MainThreadTimeline implements MainThreadTimelineShape { @@ -39,7 +39,7 @@ export class MainThreadTimeline implements MainThreadTimelineShape { this._timelineService.registerTimelineProvider({ ...provider, onDidChange: onDidChange.event, - provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: { cacheResults?: boolean }) { + provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: InternalTimelineOptions) { return proxy.$getTimeline(provider.id, uri, options, token, internalOptions); }, dispose() { diff --git a/src/vs/workbench/api/browser/mainThreadWebview.ts b/src/vs/workbench/api/browser/mainThreadWebview.ts index 8764b50ffe8..dda6ad071d9 100644 --- a/src/vs/workbench/api/browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/browser/mainThreadWebview.ts @@ -122,7 +122,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma this._register(_webviewWorkbenchService.registerResolver({ canResolve: (webview: WebviewInput) => { if (webview instanceof CustomEditorInput) { - extensionService.activateByEvent(`onWebviewEditor:${webview.viewType}`); + extensionService.activateByEvent(`onCustomEditor:${webview.viewType}`); return false; } diff --git a/src/vs/workbench/api/browser/mainThreadWorkspace.ts b/src/vs/workbench/api/browser/mainThreadWorkspace.ts index 7d67852018a..bf9dc7b1088 100644 --- a/src/vs/workbench/api/browser/mainThreadWorkspace.ts +++ b/src/vs/workbench/api/browser/mainThreadWorkspace.ts @@ -12,7 +12,7 @@ import { isNative } from 'vs/base/common/platform'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; import { IFileMatch, IPatternInfo, ISearchProgressItem, ISearchService } from 'vs/workbench/services/search/common/search'; -import { IWorkspaceContextService, WorkbenchState, IWorkspace } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService, WorkbenchState, IWorkspace, toWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -138,7 +138,7 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { } const query = this._queryBuilder.file( - includeFolder ? [includeFolder] : workspace.folders.map(f => f.uri), + includeFolder ? [toWorkspaceFolder(includeFolder)] : workspace.folders, { maxResults: withNullAsUndefined(maxResults), disregardExcludeSettings: (excludePatternOrDisregardExcludes === false) || undefined, @@ -190,7 +190,7 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { $checkExists(folders: UriComponents[], includes: string[], token: CancellationToken): Promise { const queryBuilder = this._instantiationService.createInstance(QueryBuilder); - const query = queryBuilder.file(folders.map(folder => URI.revive(folder)), { + const query = queryBuilder.file(folders.map(folder => toWorkspaceFolder(URI.revive(folder))), { _reason: 'checkExists', includePattern: includes.join(', '), expandPatterns: true, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index a54b3d0766a..9394de339c0 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -49,7 +49,7 @@ import { SaveReason } from 'vs/workbench/common/editor'; import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtensionActivator'; import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; import { TunnelOptions } from 'vs/platform/remote/common/tunnel'; -import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor } from 'vs/workbench/contrib/timeline/common/timeline'; +import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor, InternalTimelineOptions } from 'vs/workbench/contrib/timeline/common/timeline'; import { revive } from 'vs/base/common/marshalling'; import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { Dto } from 'vs/base/common/types'; @@ -367,7 +367,8 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerOnTypeFormattingSupport(handle: number, selector: IDocumentFilterDto[], autoFormatTriggerCharacters: string[], extensionId: ExtensionIdentifier): void; $registerNavigateTypeSupport(handle: number): void; $registerRenameSupport(handle: number, selector: IDocumentFilterDto[], supportsResolveInitialValues: boolean): void; - $registerDocumentSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: modes.SemanticTokensLegend): void; + $registerDocumentSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: modes.SemanticTokensLegend, eventHandle: number | undefined): void; + $emitDocumentSemanticTokensEvent(eventHandle: number): void; $registerDocumentRangeSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: modes.SemanticTokensLegend): void; $registerSuggestSupport(handle: number, selector: IDocumentFilterDto[], triggerCharacters: string[], supportsResolveDetails: boolean, extensionId: ExtensionIdentifier): void; $registerSignatureHelpProvider(handle: number, selector: IDocumentFilterDto[], metadata: ISignatureHelpProviderMetadataDto): void; @@ -620,7 +621,7 @@ export interface ExtHostWebviewsShape { $onSave(resource: UriComponents, viewType: string): Promise; $onSaveAs(resource: UriComponents, viewType: string, targetResource: UriComponents): Promise; - $backup(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise; + $backup(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise; } export interface MainThreadUrlsShape extends IDisposable { @@ -1468,7 +1469,7 @@ export interface ExtHostTunnelServiceShape { } export interface ExtHostTimelineShape { - $getTimeline(source: string, uri: UriComponents, options: TimelineOptions, token: CancellationToken, internalOptions?: { cacheResults?: boolean }): Promise; + $getTimeline(source: string, uri: UriComponents, options: TimelineOptions, token: CancellationToken, internalOptions?: InternalTimelineOptions): Promise; } // --- proxy identifiers diff --git a/src/vs/workbench/api/common/extHostAuthentication.ts b/src/vs/workbench/api/common/extHostAuthentication.ts index d9bcc811cbf..b3b2d4d0f4a 100644 --- a/src/vs/workbench/api/common/extHostAuthentication.ts +++ b/src/vs/workbench/api/common/extHostAuthentication.ts @@ -34,7 +34,7 @@ export class AuthenticationProviderWrapper implements vscode.AuthenticationProvi id: session.id, accountName: session.accountName, scopes: session.scopes, - accessToken: async () => { + getAccessToken: async () => { const isAllowed = await this._proxy.$getSessionsPrompt( this._provider.id, this.displayName, @@ -45,7 +45,7 @@ export class AuthenticationProviderWrapper implements vscode.AuthenticationProvi throw new Error('User did not consent to token access.'); } - return session.accessToken(); + return session.getAccessToken(); } }; }); @@ -60,7 +60,7 @@ export class AuthenticationProviderWrapper implements vscode.AuthenticationProvi return this._provider.login(scopes); } - logout(sessionId: string): Promise { + logout(sessionId: string): Thenable { return this._provider.logout(sessionId); } } @@ -137,7 +137,7 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { const sessions = await authProvider.getSessions(); const session = sessions.find(session => session.id === sessionId); if (session) { - return session.accessToken(); + return session.getAccessToken(); } throw new Error(`Unable to find session with id: ${sessionId}`); diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 479a5126b3c..a2d7ca79b98 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -1702,9 +1702,19 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF //#region semantic coloring registerDocumentSemanticTokensProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.DocumentSemanticTokensProvider, legend: vscode.SemanticTokensLegend): vscode.Disposable { - const handle = this._addNewAdapter(new DocumentSemanticTokensAdapter(this._documents, provider), extension); - this._proxy.$registerDocumentSemanticTokensProvider(handle, this._transformDocumentSelector(selector), legend); - return this._createDisposable(handle); + const handle = this._nextHandle(); + const eventHandle = (typeof provider.onDidChangeSemanticTokens === 'function' ? this._nextHandle() : undefined); + + this._adapter.set(handle, new AdapterData(new DocumentSemanticTokensAdapter(this._documents, provider), extension)); + this._proxy.$registerDocumentSemanticTokensProvider(handle, this._transformDocumentSelector(selector), legend, eventHandle); + let result = this._createDisposable(handle); + + if (eventHandle) { + const subscription = provider.onDidChangeSemanticTokens!(_ => this._proxy.$emitDocumentSemanticTokensEvent(eventHandle)); + result = Disposable.from(result, subscription); + } + + return result; } $provideDocumentSemanticTokens(handle: number, resource: UriComponents, previousResultId: number, token: CancellationToken): Promise { diff --git a/src/vs/workbench/api/common/extHostTimeline.ts b/src/vs/workbench/api/common/extHostTimeline.ts index 87fefc82cfc..4df94da98d5 100644 --- a/src/vs/workbench/api/common/extHostTimeline.ts +++ b/src/vs/workbench/api/common/extHostTimeline.ts @@ -7,7 +7,7 @@ import * as vscode from 'vscode'; import { UriComponents, URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ExtHostTimelineShape, MainThreadTimelineShape, IMainContext, MainContext } from 'vs/workbench/api/common/extHost.protocol'; -import { Timeline, TimelineItem, TimelineOptions, TimelineProvider } from 'vs/workbench/contrib/timeline/common/timeline'; +import { Timeline, TimelineItem, TimelineOptions, TimelineProvider, InternalTimelineOptions } from 'vs/workbench/contrib/timeline/common/timeline'; import { IDisposable, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { CancellationToken } from 'vs/base/common/cancellation'; import { CommandsConverter, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; @@ -16,21 +16,19 @@ import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; export interface IExtHostTimeline extends ExtHostTimelineShape { readonly _serviceBrand: undefined; - $getTimeline(id: string, uri: UriComponents, options: vscode.TimelineOptions, token: vscode.CancellationToken, internalOptions?: { cacheResults?: boolean }): Promise; + $getTimeline(id: string, uri: UriComponents, options: vscode.TimelineOptions, token: vscode.CancellationToken, internalOptions?: InternalTimelineOptions): Promise; } export const IExtHostTimeline = createDecorator('IExtHostTimeline'); export class ExtHostTimeline implements IExtHostTimeline { - private static handlePool = 0; - _serviceBrand: undefined; private _proxy: MainThreadTimelineShape; private _providers = new Map(); - private _itemsBySourceByUriMap = new Map>>(); + private _itemsBySourceAndUriMap = new Map>>(); constructor( mainContext: IMainContext, @@ -42,7 +40,7 @@ export class ExtHostTimeline implements IExtHostTimeline { processArgument: arg => { if (arg && arg.$mid === 11) { const uri = arg.uri === undefined ? undefined : URI.revive(arg.uri); - return this._itemsBySourceByUriMap.get(getUriKey(uri))?.get(arg.source)?.get(arg.handle); + return this._itemsBySourceAndUriMap.get(arg.source)?.get(getUriKey(uri))?.get(arg.handle); } return arg; @@ -50,7 +48,7 @@ export class ExtHostTimeline implements IExtHostTimeline { }); } - async $getTimeline(id: string, uri: UriComponents, options: vscode.TimelineOptions, token: vscode.CancellationToken, internalOptions?: { cacheResults?: boolean }): Promise { + async $getTimeline(id: string, uri: UriComponents, options: vscode.TimelineOptions, token: vscode.CancellationToken, internalOptions?: InternalTimelineOptions): Promise { const provider = this._providers.get(id); return provider?.provideTimeline(URI.revive(uri), options, token, internalOptions); } @@ -62,20 +60,21 @@ export class ExtHostTimeline implements IExtHostTimeline { let disposable: IDisposable | undefined; if (provider.onDidChange) { - disposable = provider.onDidChange(this.emitTimelineChangeEvent(provider.id), this); + disposable = provider.onDidChange(e => this._proxy.$emitTimelineChangeEvent({ ...e, id: provider.id }), this); } - const itemsBySourceByUriMap = this._itemsBySourceByUriMap; + const itemsBySourceAndUriMap = this._itemsBySourceAndUriMap; return this.registerTimelineProviderCore({ ...provider, scheme: scheme, onDidChange: undefined, - async provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: { cacheResults?: boolean }) { - timelineDisposables.clear(); + async provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: InternalTimelineOptions) { + if (internalOptions?.resetCache) { + timelineDisposables.clear(); - // For now, only allow the caching of a single Uri - if (internalOptions?.cacheResults && !itemsBySourceByUriMap.has(getUriKey(uri))) { - itemsBySourceByUriMap.clear(); + // For now, only allow the caching of a single Uri + // itemsBySourceAndUriMap.get(provider.id)?.get(getUriKey(uri))?.clear(); + itemsBySourceAndUriMap.get(provider.id)?.clear(); } const result = await provider.provideTimeline(uri, options, token); @@ -85,8 +84,9 @@ export class ExtHostTimeline implements IExtHostTimeline { return undefined; } - // TODO: Determine if we should cache dependent on who calls us (internal vs external) - const convertItem = convertTimelineItem(uri, internalOptions?.cacheResults ?? false); + // TODO: Should we bother converting all the data if we aren't caching? Meaning it is being requested by an extension? + + const convertItem = convertTimelineItem(uri, internalOptions); return { ...result, source: provider.id, @@ -94,6 +94,10 @@ export class ExtHostTimeline implements IExtHostTimeline { }; }, dispose() { + for (const sourceMap of itemsBySourceAndUriMap.values()) { + sourceMap.get(provider.id)?.clear(); + } + disposable?.dispose(); timelineDisposables.dispose(); } @@ -101,29 +105,28 @@ export class ExtHostTimeline implements IExtHostTimeline { } private convertTimelineItem(source: string, commandConverter: CommandsConverter, disposables: DisposableStore) { - return (uri: URI, cacheResults: boolean) => { - let itemsMap: Map | undefined; - if (cacheResults) { - const uriKey = getUriKey(uri); - - let sourceMap = this._itemsBySourceByUriMap.get(uriKey); - if (sourceMap === undefined) { - sourceMap = new Map(); - this._itemsBySourceByUriMap.set(uriKey, sourceMap); + return (uri: URI, options?: InternalTimelineOptions) => { + let items: Map | undefined; + if (options?.cacheResults) { + let itemsByUri = this._itemsBySourceAndUriMap.get(source); + if (itemsByUri === undefined) { + itemsByUri = new Map(); + this._itemsBySourceAndUriMap.set(source, itemsByUri); } - itemsMap = sourceMap.get(source); - if (itemsMap === undefined) { - itemsMap = new Map(); - sourceMap.set(source, itemsMap); + const uriKey = getUriKey(uri); + items = itemsByUri.get(uriKey); + if (items === undefined) { + items = new Map(); + itemsByUri.set(uriKey, items); } } return (item: vscode.TimelineItem): TimelineItem => { const { iconPath, ...props } = item; - const handle = `${source}|${item.id ?? `${item.timestamp}-${ExtHostTimeline.handlePool++}`}`; - itemsMap?.set(handle, item); + const handle = `${source}|${item.id ?? item.timestamp}`; + items?.set(handle, item); let icon; let iconDark; @@ -155,22 +158,6 @@ export class ExtHostTimeline implements IExtHostTimeline { }; } - private emitTimelineChangeEvent(id: string) { - return (e: vscode.TimelineChangeEvent) => { - // Clear caches - if (e?.uri === undefined) { - for (const sourceMap of this._itemsBySourceByUriMap.values()) { - sourceMap.get(id)?.clear(); - } - } - else { - this._itemsBySourceByUriMap.get(getUriKey(e.uri))?.clear(); - } - - this._proxy.$emitTimelineChangeEvent({ ...e, id: id }); - }; - } - private registerTimelineProviderCore(provider: TimelineProvider): IDisposable { // console.log(`ExtHostTimeline#registerTimelineProvider: id=${provider.id}`); @@ -187,7 +174,7 @@ export class ExtHostTimeline implements IExtHostTimeline { this._providers.set(provider.id, provider); return toDisposable(() => { - for (const sourceMap of this._itemsBySourceByUriMap.values()) { + for (const sourceMap of this._itemsBySourceAndUriMap.values()) { sourceMap.get(provider.id)?.clear(); } diff --git a/src/vs/workbench/api/common/extHostWebview.ts b/src/vs/workbench/api/common/extHostWebview.ts index 121f1615990..17b6b4719ec 100644 --- a/src/vs/workbench/api/common/extHostWebview.ts +++ b/src/vs/workbench/api/common/extHostWebview.ts @@ -104,19 +104,20 @@ export class ExtHostWebviewEditor extends Disposable implements vscode.WebviewPa private _title: string; private _iconPath?: IconPath; - private readonly _options: vscode.WebviewPanelOptions; - private readonly _webview: ExtHostWebview; - private _viewColumn: vscode.ViewColumn | undefined; - private _visible: boolean = true; - private _active: boolean = true; + readonly #options: vscode.WebviewPanelOptions; + readonly #webview: ExtHostWebview; - _isDisposed: boolean = false; + #viewColumn: vscode.ViewColumn | undefined = undefined; + #visible: boolean = true; + #active: boolean = true; - readonly _onDisposeEmitter = this._register(new Emitter()); - public readonly onDidDispose: Event = this._onDisposeEmitter.event; + #isDisposed: boolean = false; - readonly _onDidChangeViewStateEmitter = this._register(new Emitter()); - public readonly onDidChangeViewState: Event = this._onDidChangeViewStateEmitter.event; + readonly #onDidDispose = this._register(new Emitter()); + public readonly onDidDispose = this.#onDidDispose.event; + + readonly #onDidChangeViewState = this._register(new Emitter()); + public readonly onDidChangeViewState = this.#onDidChangeViewState.event; constructor( handle: WebviewPanelHandle, @@ -131,27 +132,28 @@ export class ExtHostWebviewEditor extends Disposable implements vscode.WebviewPa this._handle = handle; this._proxy = proxy; this._viewType = viewType; - this._options = editorOptions; - this._viewColumn = viewColumn; + this.#options = editorOptions; + this.#viewColumn = viewColumn; this._title = title; - this._webview = webview; + this.#webview = webview; } public dispose() { - if (this._isDisposed) { + if (this.#isDisposed) { return; } - this._isDisposed = true; - this._onDisposeEmitter.fire(); + + this.#isDisposed = true; + this.#onDidDispose.fire(); this._proxy.$disposeWebview(this._handle); - this._webview.dispose(); + this.#webview.dispose(); super.dispose(); } get webview() { this.assertNotDisposed(); - return this._webview; + return this.#webview; } get viewType(): string { @@ -187,42 +189,40 @@ export class ExtHostWebviewEditor extends Disposable implements vscode.WebviewPa } get options() { - return this._options; + return this.#options; } get viewColumn(): vscode.ViewColumn | undefined { this.assertNotDisposed(); - if (typeof this._viewColumn === 'number' && this._viewColumn < 0) { + if (typeof this.#viewColumn === 'number' && this.#viewColumn < 0) { // We are using a symbolic view column // Return undefined instead to indicate that the real view column is currently unknown but will be resolved. return undefined; } - return this._viewColumn; - } - - _setViewColumn(value: vscode.ViewColumn) { - this.assertNotDisposed(); - this._viewColumn = value; + return this.#viewColumn; } public get active(): boolean { this.assertNotDisposed(); - return this._active; - } - - _setActive(value: boolean) { - this.assertNotDisposed(); - this._active = value; + return this.#active; } public get visible(): boolean { this.assertNotDisposed(); - return this._visible; + return this.#visible; } - _setVisible(value: boolean) { - this.assertNotDisposed(); - this._visible = value; + _updateViewState(newState: { active: boolean; visible: boolean; viewColumn: vscode.ViewColumn; }) { + if (this.#isDisposed) { + return; + } + + if (this.active !== newState.active || this.visible !== newState.visible || this.viewColumn !== newState.viewColumn) { + this.#active = newState.active; + this.#visible = newState.visible; + this.#viewColumn = newState.viewColumn; + this.#onDidChangeViewState.fire({ webviewPanel: this }); + } } public postMessage(message: any): Promise { @@ -239,7 +239,7 @@ export class ExtHostWebviewEditor extends Disposable implements vscode.WebviewPa } private assertNotDisposed() { - if (this._isDisposed) { + if (this.#isDisposed) { throw new Error('Webview is disposed'); } } @@ -247,143 +247,160 @@ export class ExtHostWebviewEditor extends Disposable implements vscode.WebviewPa type EditType = unknown; -class WebviewEditorCustomDocument extends Disposable implements vscode.CustomDocument { - private _currentEditIndex: number = -1; - private _savePoint: number = -1; - private readonly _edits: Array = []; +class CustomDocument extends Disposable implements vscode.CustomDocument { - public userData: unknown; - - public _capabilities?: vscode.CustomEditorCapabilities = undefined; - - constructor( - private readonly _proxy: MainThreadWebviewsShape, - public readonly viewType: string, - public readonly uri: vscode.Uri, - ) { - super(); + public static create(proxy: MainThreadWebviewsShape, viewType: string, uri: vscode.Uri) { + return Object.seal(new CustomDocument(proxy, viewType, uri)); } - _setCapabilities(capabilities: vscode.CustomEditorCapabilities) { - if (this._capabilities) { - throw new Error('Capabilities already provided'); - } + // Explicitly initialize all properties as we seal the object after creation! - this._capabilities = capabilities; - capabilities.editing?.onDidEdit(edit => { - this.pushEdit(edit, this); - }); + #currentEditIndex: number = -1; + #savePoint: number = -1; + readonly #edits: Array = []; + + readonly #proxy: MainThreadWebviewsShape; + readonly #viewType: string; + readonly #uri: vscode.Uri; + + #capabilities: vscode.CustomEditorCapabilities | undefined = undefined; + + private constructor(proxy: MainThreadWebviewsShape, viewType: string, uri: vscode.Uri) { + super(); + this.#proxy = proxy; + this.#viewType = viewType; + this.#uri = uri; + } + + dispose() { + this.#onDidDispose.fire(); + super.dispose(); } //#region Public API - #_onDidDispose = this._register(new Emitter()); - public readonly onDidDispose = this.#_onDidDispose.event; + public get viewType(): string { return this.#viewType; } + + public get uri(): vscode.Uri { return this.#uri; } + + #onDidDispose = this._register(new Emitter()); + public readonly onDidDispose = this.#onDidDispose.event; + + public userData: unknown = undefined; //#endregion - dispose() { - this.#_onDidDispose.fire(); - super.dispose(); + //#region Internal + + /** @internal*/ _setCapabilities(capabilities: vscode.CustomEditorCapabilities) { + if (this.#capabilities) { + throw new Error('Capabilities already provided'); + } + + this.#capabilities = capabilities; + capabilities.editing?.onDidEdit(edit => { + this.pushEdit(edit); + }); } - private pushEdit(edit: EditType, trigger: any) { - this.spliceEdits(edit); - - this._currentEditIndex = this._edits.length - 1; - this.updateState(); - // this._onApplyEdit.fire({ edits: [edit], trigger }); - } - - private updateState() { - const dirty = this._edits.length > 0 && this._savePoint !== this._currentEditIndex; - this._proxy.$onDidChangeCustomDocumentState(this.uri, this.viewType, { dirty }); - } - - private spliceEdits(editToInsert?: EditType) { - const start = this._currentEditIndex + 1; - const toRemove = this._edits.length - this._currentEditIndex; - - editToInsert - ? this._edits.splice(start, toRemove, editToInsert) - : this._edits.splice(start, toRemove); - } - - revert() { + /** @internal*/ _revert() { const editing = this.getEditingCapability(); - if (this._currentEditIndex === this._savePoint) { + if (this.#currentEditIndex === this.#savePoint) { return true; } - if (this._currentEditIndex >= this._savePoint) { - const editsToUndo = this._edits.slice(this._savePoint, this._currentEditIndex); + if (this.#currentEditIndex >= this.#savePoint) { + const editsToUndo = this.#edits.slice(this.#savePoint, this.#currentEditIndex); editing.undoEdits(editsToUndo.reverse()); - } else if (this._currentEditIndex < this._savePoint) { - const editsToRedo = this._edits.slice(this._currentEditIndex, this._savePoint); + } else if (this.#currentEditIndex < this.#savePoint) { + const editsToRedo = this.#edits.slice(this.#currentEditIndex, this.#savePoint); editing.applyEdits(editsToRedo); } - this._currentEditIndex = this._savePoint; + this.#currentEditIndex = this.#savePoint; this.spliceEdits(); this.updateState(); return true; } - undo() { + /** @internal*/ _undo() { const editing = this.getEditingCapability(); - if (this._currentEditIndex < 0) { + if (this.#currentEditIndex < 0) { // nothing to undo return; } - const undoneEdit = this._edits[this._currentEditIndex]; - --this._currentEditIndex; + const undoneEdit = this.#edits[this.#currentEditIndex]; + --this.#currentEditIndex; editing.undoEdits([undoneEdit]); this.updateState(); } - redo() { + /** @internal*/ _redo() { const editing = this.getEditingCapability(); - if (this._currentEditIndex >= this._edits.length - 1) { + if (this.#currentEditIndex >= this.#edits.length - 1) { // nothing to redo return; } - ++this._currentEditIndex; - const redoneEdit = this._edits[this._currentEditIndex]; + ++this.#currentEditIndex; + const redoneEdit = this.#edits[this.#currentEditIndex]; editing.applyEdits([redoneEdit]); this.updateState(); } - save() { + /** @internal*/ _save() { return this.getEditingCapability().save(); } - saveAs(target: vscode.Uri) { + /** @internal*/ _saveAs(target: vscode.Uri) { return this.getEditingCapability().saveAs(target); } - backup(cancellation: CancellationToken) { + /** @internal*/ _backup(cancellation: CancellationToken) { return this.getEditingCapability().backup(cancellation); } + //#endregion + + private pushEdit(edit: EditType) { + this.spliceEdits(edit); + + this.#currentEditIndex = this.#edits.length - 1; + this.updateState(); + } + + private updateState() { + const dirty = this.#edits.length > 0 && this.#savePoint !== this.#currentEditIndex; + this.#proxy.$onDidChangeCustomDocumentState(this.uri, this.viewType, { dirty }); + } + + private spliceEdits(editToInsert?: EditType) { + const start = this.#currentEditIndex + 1; + const toRemove = this.#edits.length - this.#currentEditIndex; + + editToInsert + ? this.#edits.splice(start, toRemove, editToInsert) + : this.#edits.splice(start, toRemove); + } + private getEditingCapability(): vscode.CustomEditorEditingCapability { - if (!this._capabilities?.editing) { + if (!this.#capabilities?.editing) { throw new Error('Document is not editable'); } - return this._capabilities.editing; + return this.#capabilities.editing; } } class WebviewDocumentStore { - private readonly _documents = new Map(); + private readonly _documents = new Map(); - public get(viewType: string, resource: vscode.Uri): WebviewEditorCustomDocument | undefined { + public get(viewType: string, resource: vscode.Uri): CustomDocument | undefined { return this._documents.get(this.key(viewType, resource)); } - public add(document: WebviewEditorCustomDocument) { + public add(document: CustomDocument) { const key = this.key(document.viewType, document.uri); if (this._documents.has(key)) { throw new Error(`Document already exists for viewType:${document.viewType} resource:${document.uri}`); @@ -391,7 +408,7 @@ class WebviewDocumentStore { this._documents.set(key, document); } - public delete(document: WebviewEditorCustomDocument) { + public delete(document: CustomDocument) { const key = this.key(document.viewType, document.uri); this._documents.delete(key); } @@ -568,18 +585,16 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { for (const handle of handles) { const panel = this.getWebviewPanel(handle); - if (!panel || panel._isDisposed) { + if (!panel) { continue; } const newState = newStates[handle]; - const viewColumn = typeConverters.ViewColumn.to(newState.position); - if (panel.active !== newState.active || panel.visible !== newState.visible || panel.viewColumn !== viewColumn) { - panel._setActive(newState.active); - panel._setVisible(newState.visible); - panel._setViewColumn(viewColumn); - panel._onDidChangeViewStateEmitter.fire({ webviewPanel: panel }); - } + panel._updateViewState({ + active: newState.active, + visible: newState.visible, + viewColumn: typeConverters.ViewColumn.to(newState.position), + }); } } @@ -622,7 +637,7 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { } const revivedResource = URI.revive(resource); - const document = Object.seal(new WebviewEditorCustomDocument(this._proxy, viewType, revivedResource)); + const document = CustomDocument.create(this._proxy, viewType, revivedResource); const capabilities = await entry.provider.resolveCustomDocument(document); document._setCapabilities(capabilities); this._documents.add(document); @@ -687,39 +702,39 @@ export class ExtHostWebviews implements ExtHostWebviewsShape { async $undo(resourceComponents: UriComponents, viewType: string): Promise { const document = this.getDocument(viewType, resourceComponents); - document.undo(); + document._undo(); } async $redo(resourceComponents: UriComponents, viewType: string): Promise { const document = this.getDocument(viewType, resourceComponents); - document.redo(); + document._redo(); } async $revert(resourceComponents: UriComponents, viewType: string): Promise { const document = this.getDocument(viewType, resourceComponents); - document.revert(); + document._revert(); } async $onSave(resourceComponents: UriComponents, viewType: string): Promise { const document = this.getDocument(viewType, resourceComponents); - document.save(); + document._save(); } async $onSaveAs(resourceComponents: UriComponents, viewType: string, targetResource: UriComponents): Promise { const document = this.getDocument(viewType, resourceComponents); - return document.saveAs(URI.revive(targetResource)); + return document._saveAs(URI.revive(targetResource)); } - async $backup(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise { + async $backup(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise { const document = this.getDocument(viewType, resourceComponents); - return document.backup(cancellation); + return document._backup(cancellation); } private getWebviewPanel(handle: WebviewPanelHandle): ExtHostWebviewEditor | undefined { return this._webviewPanels.get(handle); } - private getDocument(viewType: string, resource: UriComponents): WebviewEditorCustomDocument { + private getDocument(viewType: string, resource: UriComponents): CustomDocument { const document = this._documents.get(viewType, URI.revive(resource)); if (!document) { throw new Error('No webview editor custom document found'); diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index 50906972e6c..cb77aab68f5 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -18,16 +18,17 @@ import { KeyMod, KeyCode, KeyChord } from 'vs/base/common/keyCodes'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { getMenuBarVisibility } from 'vs/platform/windows/common/windows'; import { isWindows, isLinux, isWeb } from 'vs/base/common/platform'; -import { IsMacNativeContext } from 'vs/workbench/browser/contextkeys'; +import { IsMacNativeContext } from 'vs/platform/contextkey/common/contextkeys'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { InEditorZenModeContext, IsCenteredLayoutContext, EditorAreaVisibleContext } from 'vs/workbench/common/editor'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { SideBarVisibleContext } from 'vs/workbench/common/viewlet'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IViewDescriptorService, IViewContainersRegistry, Extensions as ViewContainerExtensions, IViewsService, FocusedViewContext, ViewContainerLocation } from 'vs/workbench/common/views'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; +import { IActivityBarService } from 'vs/workbench/services/activityBar/browser/activityBarService'; const registry = Registry.as(WorkbenchExtensions.WorkbenchActions); const viewCategory = nls.localize('view', "View"); @@ -49,10 +50,8 @@ export class CloseSidebarAction extends Action { this.enabled = !!this.layoutService; } - run(): Promise { + async run(): Promise { this.layoutService.setSideBarHidden(true); - - return Promise.resolve(); } } @@ -78,7 +77,7 @@ export class ToggleActivityBarVisibilityAction extends Action { this.enabled = !!this.layoutService; } - run(): Promise { + run(): Promise { const visibility = this.layoutService.isVisible(Parts.ACTIVITYBAR_PART); const newVisibilityValue = !visibility; @@ -114,10 +113,8 @@ class ToggleCenteredLayout extends Action { this.enabled = !!this.layoutService; } - run(): Promise { + async run(): Promise { this.layoutService.centerEditorLayout(!this.layoutService.isEditorLayoutCentered()); - - return Promise.resolve(); } } @@ -164,11 +161,9 @@ export class ToggleEditorLayoutAction extends Action { this.enabled = this.editorGroupService.count > 1; } - run(): Promise { + async run(): Promise { const newOrientation = (this.editorGroupService.orientation === GroupOrientation.VERTICAL) ? GroupOrientation.HORIZONTAL : GroupOrientation.VERTICAL; this.editorGroupService.setGroupOrientation(newOrientation); - - return Promise.resolve(); } } @@ -204,7 +199,7 @@ export class ToggleSidebarPositionAction extends Action { this.enabled = !!this.layoutService && !!this.configurationService; } - run(): Promise { + run(): Promise { const position = this.layoutService.getSideBarPosition(); const newPositionValue = (position === Position.LEFT) ? 'right' : 'left'; @@ -254,10 +249,8 @@ export class ToggleEditorVisibilityAction extends Action { this.enabled = !!this.layoutService; } - run(): Promise { + async run(): Promise { this.layoutService.toggleMaximizedPanel(); - - return Promise.resolve(); } } @@ -288,11 +281,9 @@ export class ToggleSidebarVisibilityAction extends Action { this.enabled = !!this.layoutService; } - run(): Promise { + async run(): Promise { const hideSidebar = this.layoutService.isVisible(Parts.SIDEBAR_PART); this.layoutService.setSideBarHidden(hideSidebar); - - return Promise.resolve(); } } @@ -335,7 +326,7 @@ export class ToggleStatusbarVisibilityAction extends Action { this.enabled = !!this.layoutService; } - run(): Promise { + run(): Promise { const visibility = this.layoutService.isVisible(Parts.STATUSBAR_PART); const newVisibilityValue = !visibility; @@ -372,7 +363,7 @@ class ToggleTabsVisibilityAction extends Action { super(id, label); } - run(): Promise { + run(): Promise { const visibility = this.configurationService.getValue(ToggleTabsVisibilityAction.tabsVisibleKey); const newVisibilityValue = !visibility; @@ -402,10 +393,8 @@ class ToggleZenMode extends Action { this.enabled = !!this.layoutService; } - run(): Promise { + async run(): Promise { this.layoutService.toggleZenMode(); - - return Promise.resolve(); } } @@ -465,9 +454,7 @@ export class ToggleMenuBarAction extends Action { newVisibilityValue = (isWeb && currentVisibilityValue === 'hidden') ? 'compact' : 'default'; } - this.configurationService.updateValue(ToggleMenuBarAction.menuBarVisibilityKey, newVisibilityValue, ConfigurationTarget.USER); - - return Promise.resolve(); + return this.configurationService.updateValue(ToggleMenuBarAction.menuBarVisibilityKey, newVisibilityValue, ConfigurationTarget.USER); } } @@ -500,7 +487,7 @@ export class ResetViewLocationsAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { const viewContainerRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); viewContainerRegistry.all.forEach(viewContainer => { const viewDescriptors = this.viewDescriptorService.getViewDescriptors(viewContainer); @@ -514,8 +501,6 @@ export class ResetViewLocationsAction extends Action { } }); }); - - return Promise.resolve(); } } @@ -534,78 +519,83 @@ export class MoveFocusedViewAction extends Action { @IQuickInputService private quickInputService: IQuickInputService, @IContextKeyService private contextKeyService: IContextKeyService, @INotificationService private notificationService: INotificationService, + @IActivityBarService private activityBarService: IActivityBarService, @IViewletService private viewletService: IViewletService ) { super(id, label); } - run(): Promise { + async run(): Promise { const viewContainerRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); - const focusedView = FocusedViewContext.getValue(this.contextKeyService); + const focusedViewId = FocusedViewContext.getValue(this.contextKeyService); - if (focusedView === undefined || focusedView.trim() === '') { + if (focusedViewId === undefined || focusedViewId.trim() === '') { this.notificationService.error(nls.localize('moveFocusedView.error.noFocusedView', "There is no view currently focused.")); - return Promise.resolve(); + return; } - const viewDescriptor = this.viewDescriptorService.getViewDescriptor(focusedView); + const viewDescriptor = this.viewDescriptorService.getViewDescriptor(focusedViewId); if (!viewDescriptor || !viewDescriptor.canMoveView) { - this.notificationService.error(nls.localize('moveFocusedView.error.nonMovableView', "The currently focused view is not movable {0}.", focusedView)); - return Promise.resolve(); + this.notificationService.error(nls.localize('moveFocusedView.error.nonMovableView', "The currently focused view is not movable.")); + return; } const quickPick = this.quickInputService.createQuickPick(); - quickPick.placeholder = nls.localize('moveFocusedView.selectDestination', "Select a destination area for the view..."); - quickPick.autoFocusOnList = true; + quickPick.placeholder = nls.localize('moveFocusedView.selectDestination', "Select a Destination for the View"); - quickPick.items = [ - { - id: 'sidebar', - label: nls.localize('sidebar', "Sidebar") - }, - { - id: 'panel', + const pinnedViewlets = this.activityBarService.getPinnedViewletIds(); + const items: Array = this.viewletService.getViewlets() + .filter(viewlet => { + if (viewlet.id === this.viewDescriptorService.getViewContainer(focusedViewId)!.id) { + return false; + } + + return !viewContainerRegistry.get(viewlet.id)!.rejectAddedViews && pinnedViewlets.indexOf(viewlet.id) !== -1; + }) + .map(viewlet => { + return { + id: viewlet.id, + label: viewlet.name, + }; + }); + + if (this.viewDescriptorService.getViewLocation(focusedViewId) !== ViewContainerLocation.Panel) { + items.unshift({ + type: 'separator', + label: nls.localize('sidebar', "Side Bar") + }); + items.push({ + type: 'separator', label: nls.localize('panel', "Panel") - } - ]; + }); + items.push({ + id: '_.panel.newcontainer', + label: nls.localize('moveFocusedView.newContainerInPanel', "New Container in Panel"), + }); + } + + quickPick.items = items; quickPick.onDidAccept(() => { const destination = quickPick.selectedItems[0]; - if (destination.id === 'panel') { - quickPick.hide(); + if (destination.id === '_.panel.newcontainer') { this.viewDescriptorService.moveViewToLocation(viewDescriptor!, ViewContainerLocation.Panel); - this.viewsService.openView(focusedView, true); - - return; - } else if (destination.id === 'sidebar') { - quickPick.placeholder = nls.localize('moveFocusedView.selectDestinationContainer', "Select a destination view group..."); - quickPick.items = this.viewletService.getViewlets().map(viewlet => { - return { - id: viewlet.id, - label: viewlet.name - }; - }); - - return; + this.viewsService.openView(focusedViewId, true); } else if (destination.id) { - quickPick.hide(); this.viewDescriptorService.moveViewsToContainer([viewDescriptor], viewContainerRegistry.get(destination.id)!); - this.viewsService.openView(focusedView, true); - return; + this.viewsService.openView(focusedViewId, true); } quickPick.hide(); }); quickPick.show(); - - return Promise.resolve(); } } -registry.registerWorkbenchAction(SyncActionDescriptor.create(MoveFocusedViewAction, MoveFocusedViewAction.ID, MoveFocusedViewAction.LABEL), 'View: Move Focused View', viewCategory); +registry.registerWorkbenchAction(SyncActionDescriptor.create(MoveFocusedViewAction, MoveFocusedViewAction.ID, MoveFocusedViewAction.LABEL), 'View: Move Focused View', viewCategory, FocusedViewContext.notEqualsTo('')); // --- Resize View diff --git a/src/vs/workbench/browser/actions/navigationActions.ts b/src/vs/workbench/browser/actions/navigationActions.ts index a3669a60af3..e9dc403f254 100644 --- a/src/vs/workbench/browser/actions/navigationActions.ts +++ b/src/vs/workbench/browser/actions/navigationActions.ts @@ -30,7 +30,7 @@ abstract class BaseNavigationAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { const isEditorFocus = this.layoutService.hasFocus(Parts.EDITOR_PART); const isPanelFocus = this.layoutService.hasFocus(Parts.PANEL_PART); const isSidebarFocus = this.layoutService.hasFocus(Parts.SIDEBAR_PART); @@ -39,7 +39,7 @@ abstract class BaseNavigationAction extends Action { if (isEditorFocus) { const didNavigate = this.navigateAcrossEditorGroup(this.toGroupDirection(this.direction)); if (didNavigate) { - return Promise.resolve(true); + return true; } neighborPart = this.layoutService.getVisibleNeighborPart(Parts.EDITOR_PART, this.direction); @@ -54,7 +54,7 @@ abstract class BaseNavigationAction extends Action { } if (neighborPart === Parts.EDITOR_PART) { - return Promise.resolve(this.navigateToEditorGroup(this.direction === Direction.Right ? GroupLocation.FIRST : GroupLocation.LAST)); + return this.navigateToEditorGroup(this.direction === Direction.Right ? GroupLocation.FIRST : GroupLocation.LAST); } if (neighborPart === Parts.SIDEBAR_PART) { @@ -65,7 +65,7 @@ abstract class BaseNavigationAction extends Action { return this.navigateToPanel(); } - return Promise.resolve(false); + return false; } private async navigateToPanel(): Promise { @@ -90,12 +90,12 @@ abstract class BaseNavigationAction extends Action { private async navigateToSidebar(): Promise { if (!this.layoutService.isVisible(Parts.SIDEBAR_PART)) { - return Promise.resolve(false); + return false; } const activeViewlet = this.viewletService.getActiveViewlet(); if (!activeViewlet) { - return Promise.resolve(false); + return false; } const activeViewletId = activeViewlet.getId(); diff --git a/src/vs/workbench/browser/actions/windowActions.ts b/src/vs/workbench/browser/actions/windowActions.ts index 707ba426404..f2c406df86a 100644 --- a/src/vs/workbench/browser/actions/windowActions.ts +++ b/src/vs/workbench/browser/actions/windowActions.ts @@ -12,7 +12,8 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { Registry } from 'vs/platform/registry/common/platform'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { IsFullscreenContext, IsDevelopmentContext, IsMacNativeContext } from 'vs/workbench/browser/contextkeys'; +import { IsFullscreenContext } from 'vs/workbench/browser/contextkeys'; +import { IsMacNativeContext, IsDevelopmentContext } from 'vs/platform/contextkey/common/contextkeys'; import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IQuickInputButton, IQuickInputService, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; diff --git a/src/vs/workbench/browser/actions/workspaceActions.ts b/src/vs/workbench/browser/actions/workspaceActions.ts index c8941a826a1..6607abc7716 100644 --- a/src/vs/workbench/browser/actions/workspaceActions.ts +++ b/src/vs/workbench/browser/actions/workspaceActions.ts @@ -37,7 +37,7 @@ export class OpenFileAction extends Action { super(id, label); } - run(event?: any, data?: ITelemetryData): Promise { + run(event?: unknown, data?: ITelemetryData): Promise { return this.dialogService.pickFileAndOpen({ forceNewWindow: false, telemetryExtraData: data }); } } @@ -55,7 +55,7 @@ export class OpenFolderAction extends Action { super(id, label); } - run(event?: any, data?: ITelemetryData): Promise { + run(event?: unknown, data?: ITelemetryData): Promise { return this.dialogService.pickFolderAndOpen({ forceNewWindow: false, telemetryExtraData: data }); } } @@ -73,7 +73,7 @@ export class OpenFileFolderAction extends Action { super(id, label); } - run(event?: any, data?: ITelemetryData): Promise { + run(event?: unknown, data?: ITelemetryData): Promise { return this.dialogService.pickFileFolderAndOpen({ forceNewWindow: false, telemetryExtraData: data }); } } @@ -91,7 +91,7 @@ export class OpenWorkspaceAction extends Action { super(id, label); } - run(event?: any, data?: ITelemetryData): Promise { + run(event?: unknown, data?: ITelemetryData): Promise { return this.dialogService.pickWorkspaceAndOpen({ telemetryExtraData: data }); } } @@ -139,12 +139,11 @@ export class OpenWorkspaceConfigFileAction extends Action { this.enabled = !!this.workspaceContextService.getWorkspace().configuration; } - run(): Promise { + async run(): Promise { const configuration = this.workspaceContextService.getWorkspace().configuration; if (configuration) { - return this.editorService.openEditor({ resource: configuration }); + await this.editorService.openEditor({ resource: configuration }); } - return Promise.resolve(); } } @@ -161,7 +160,7 @@ export class AddRootFolderAction extends Action { super(id, label); } - run(): Promise { + run(): Promise { return this.commandService.executeCommand(ADD_ROOT_FOLDER_COMMAND_ID); } } @@ -181,7 +180,7 @@ export class GlobalRemoveRootFolderAction extends Action { super(id, label); } - async run(): Promise { + async run(): Promise { const state = this.contextService.getWorkbenchState(); // Workspace / Folder @@ -191,8 +190,6 @@ export class GlobalRemoveRootFolderAction extends Action { await this.workspaceEditingService.removeFolders([folder.uri]); } } - - return true; } } @@ -211,7 +208,7 @@ export class SaveWorkspaceAsAction extends Action { super(id, label); } - async run(): Promise { + async run(): Promise { const configPathUri = await this.workspaceEditingService.pickNewWorkspacePath(); if (configPathUri && hasWorkspaceFileExtension(configPathUri)) { switch (this.contextService.getWorkbenchState()) { @@ -243,7 +240,7 @@ export class DuplicateWorkspaceInNewWindowAction extends Action { super(id, label); } - async run(): Promise { + async run(): Promise { const folders = this.workspaceContextService.getWorkspace().folders; const remoteAuthority = this.environmentService.configuration.remoteAuthority; diff --git a/src/vs/workbench/browser/contextkeys.ts b/src/vs/workbench/browser/contextkeys.ts index 2f138cb0c65..3b6ef87ceb7 100644 --- a/src/vs/workbench/browser/contextkeys.ts +++ b/src/vs/workbench/browser/contextkeys.ts @@ -6,8 +6,7 @@ import { Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { IContextKeyService, IContextKey, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { InputFocusedContext } from 'vs/platform/contextkey/common/contextkeys'; -import { IWindowsConfiguration } from 'vs/platform/windows/common/windows'; +import { InputFocusedContext, IsMacContext, IsLinuxContext, IsWindowsContext, IsWebContext, IsMacNativeContext, IsDevelopmentContext } from 'vs/platform/contextkey/common/contextkeys'; import { ActiveEditorContext, EditorsVisibleContext, TextCompareEditorVisibleContext, TextCompareEditorActiveContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, TEXT_DIFF_EDITOR_ID, SplitEditorsVertically, InEditorZenModeContext, IsCenteredLayoutContext, ActiveEditorGroupIndexContext, ActiveEditorGroupLastContext, ActiveEditorIsReadonlyContext, EditorAreaVisibleContext, DirtyWorkingCopiesContext } from 'vs/workbench/common/editor'; import { trackFocus, addDisposableListener, EventType } from 'vs/base/browser/dom'; import { preferredSideBySideGroupDirection, GroupDirection, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -18,27 +17,15 @@ import { WorkbenchState, IWorkspaceContextService } from 'vs/platform/workspace/ import { SideBarVisibleContext } from 'vs/workbench/common/viewlet'; 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'; import { getRemoteName } from 'vs/platform/remote/common/remoteHosts'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -export const IsMacContext = new RawContextKey('isMac', isMacintosh); -export const IsLinuxContext = new RawContextKey('isLinux', isLinux); -export const IsWindowsContext = new RawContextKey('isWindows', isWindows); - -export const IsWebContext = new RawContextKey('isWeb', isWeb); -export const IsMacNativeContext = new RawContextKey('isMacNative', isMacintosh && !isWeb); - export const Deprecated_RemoteAuthorityContext = new RawContextKey('remoteAuthority', ''); export const RemoteNameContext = new RawContextKey('remoteName', ''); export const RemoteConnectionState = new RawContextKey<'' | 'initializing' | 'disconnected' | 'connected'>('remoteConnectionState', ''); -export const HasMacNativeTabsContext = new RawContextKey('hasMacNativeTabs', false); - -export const IsDevelopmentContext = new RawContextKey('isDevelopment', false); - export const WorkbenchStateContext = new RawContextKey('workbenchState', undefined); export const WorkspaceFolderCountContext = new RawContextKey('workspaceFolderCount', 0); @@ -98,10 +85,6 @@ export class WorkbenchContextKeysHandler extends Disposable { RemoteNameContext.bindTo(this.contextKeyService).set(getRemoteName(this.environmentService.configuration.remoteAuthority) || ''); - // macOS Native Tabs - const windowConfig = this.configurationService.getValue(); - HasMacNativeTabsContext.bindTo(this.contextKeyService).set(windowConfig?.window?.nativeTabs); - // Development IsDevelopmentContext.bindTo(this.contextKeyService).set(!this.environmentService.isBuilt || this.environmentService.isExtensionDevelopment); diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index 6467fe70efd..7e26d2f9376 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -246,7 +246,7 @@ export class ResourcesDropHandler { } // File: ensure the file is not dirty or opened already - else if (this.textFileService.isDirty(droppedDirtyEditor.resource) || this.editorService.isOpen(this.editorService.createInput({ resource: droppedDirtyEditor.resource, forceFile: true }))) { + else if (this.textFileService.isDirty(droppedDirtyEditor.resource) || this.editorService.isOpen({ resource: droppedDirtyEditor.resource })) { return false; } diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index eece5353caa..6f4aa04679a 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -137,14 +137,14 @@ export class ResourceLabels extends Disposable { })); // notify when label formatters change - this._register(this.labelService.onDidChangeFormatters(() => { - this._widgets.forEach(widget => widget.notifyFormattersChange()); + this._register(this.labelService.onDidChangeFormatters(e => { + this._widgets.forEach(widget => widget.notifyFormattersChange(e.scheme)); })); // notify when untitled labels change - this.textFileService.untitled.onDidChangeLabel(model => { + this._register(this.textFileService.untitled.onDidChangeLabel(model => { this._widgets.forEach(widget => widget.notifyUntitledLabelChange(model.resource)); - }); + })); } get(index: number): IResourceLabel { @@ -311,8 +311,10 @@ class ResourceLabelWidget extends IconLabel { this.render(true); } - notifyFormattersChange(): void { - this.render(false); + notifyFormattersChange(scheme: string): void { + if (this.label?.resource?.scheme === scheme) { + this.render(false); + } } notifyUntitledLabelChange(resource: URI): void { diff --git a/src/vs/workbench/browser/panel.ts b/src/vs/workbench/browser/panel.ts index f9ff5312187..89830e45418 100644 --- a/src/vs/workbench/browser/panel.ts +++ b/src/vs/workbench/browser/panel.ts @@ -101,7 +101,7 @@ export abstract class TogglePanelAction extends Action { super(id, label, cssClass); } - async run(): Promise { + async run(): Promise { if (this.isPanelFocused()) { this.layoutService.setPanelHidden(true); } else { diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts index 45da3780a68..eb88e507397 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts @@ -74,15 +74,15 @@ export class ViewletActivityAction extends ActivityAction { this.activity = activity; } - async run(event: any): Promise { + async run(event: unknown): Promise { if (event instanceof MouseEvent && event.button === 2) { - return false; // do not run on right click + return; // do not run on right click } // prevent accident trigger on a doubleclick (to help nervous people) const now = Date.now(); if (now > this.lastRun /* https://github.com/Microsoft/vscode/issues/25830 */ && now - this.lastRun < ViewletActivityAction.preventDoubleClickDelay) { - return true; + return; } this.lastRun = now; @@ -93,7 +93,7 @@ export class ViewletActivityAction extends ActivityAction { if (sideBarVisible && activeViewlet?.getId() === this.activity.id) { this.logAction('hide'); this.layoutService.setSideBarHidden(true); - return true; + return; } this.logAction('show'); @@ -120,17 +120,17 @@ export class ToggleViewletAction extends Action { super(_viewlet.id, _viewlet.name); } - run(): Promise { + async run(): Promise { const sideBarVisible = this.layoutService.isVisible(Parts.SIDEBAR_PART); const activeViewlet = this.viewletService.getActiveViewlet(); // Hide sidebar if selected viewlet already visible if (sideBarVisible && activeViewlet?.getId() === this._viewlet.id) { this.layoutService.setSideBarHidden(true); - return Promise.resolve(); + return; } - return this.viewletService.openViewlet(this._viewlet.id, true); + await this.viewletService.openViewlet(this._viewlet.id, true); } } @@ -226,7 +226,7 @@ class SwitchSideBarViewAction extends Action { super(id, name); } - run(offset: number): Promise { + async run(offset: number): Promise { const pinnedViewletIds = this.activityBarService.getPinnedViewletIds(); const activeViewlet = this.viewletService.getActiveViewlet(); @@ -240,7 +240,8 @@ class SwitchSideBarViewAction extends Action { break; } } - return this.viewletService.openViewlet(targetViewletId, true); + + await this.viewletService.openViewlet(targetViewletId, true); } } @@ -258,7 +259,7 @@ export class PreviousSideBarViewAction extends SwitchSideBarViewAction { super(id, name, viewletService, activityBarService); } - run(): Promise { + run(): Promise { return super.run(-1); } } @@ -277,7 +278,7 @@ export class NextSideBarViewAction extends SwitchSideBarViewAction { super(id, name, viewletService, activityBarService); } - run(): Promise { + run(): Promise { return super.run(1); } } diff --git a/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css b/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css index cbc3d14705b..6e3ff5bf0f4 100644 --- a/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css +++ b/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css @@ -27,6 +27,10 @@ margin-bottom: auto; } +.monaco-workbench .activitybar > .content > .composite-bar-excess { + height: 100%; +} + .monaco-workbench .activitybar .menubar { width: 100%; height: 35px; diff --git a/src/vs/workbench/browser/parts/compositeBar.ts b/src/vs/workbench/browser/parts/compositeBar.ts index 5c19e2a5b99..8ff7cfd6917 100644 --- a/src/vs/workbench/browser/parts/compositeBar.ts +++ b/src/vs/workbench/browser/parts/compositeBar.ts @@ -17,13 +17,15 @@ import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { Widget } from 'vs/base/browser/ui/widget'; import { isUndefinedOrNull } from 'vs/base/common/types'; -import { LocalSelectionTransfer } from 'vs/workbench/browser/dnd'; -import { ITheme } from 'vs/platform/theme/common/themeService'; +import { LocalSelectionTransfer, DragAndDropObserver } from 'vs/workbench/browser/dnd'; +import { ITheme, IThemeService } from 'vs/platform/theme/common/themeService'; import { Emitter } from 'vs/base/common/event'; import { DraggedViewIdentifier } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { Registry } from 'vs/platform/registry/common/platform'; import { IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainerLocation, IViewDescriptorService } from 'vs/workbench/common/views'; import { ICompositeDragAndDrop, CompositeDragAndDropData } from 'vs/base/parts/composite/browser/compositeDnd'; +import { IPaneComposite } from 'vs/workbench/common/panecomposite'; +import { IComposite } from 'vs/workbench/common/composite'; export interface ICompositeBarItem { id: string; @@ -38,7 +40,7 @@ export class CompositeDragAndDrop implements ICompositeDragAndDrop { constructor( private viewDescriptorService: IViewDescriptorService, private targetContainerLocation: ViewContainerLocation, - private openComposite: (id: string, focus?: boolean) => void, + private openComposite: (id: string, focus?: boolean) => Promise, private moveComposite: (from: string, to: string) => void, private getVisibleCompositeIds: () => string[] ) { } @@ -52,9 +54,14 @@ export class CompositeDragAndDrop implements ICompositeDragAndDrop { if (targetCompositeId) { if (currentLocation !== this.targetContainerLocation && this.targetContainerLocation !== ViewContainerLocation.Panel) { const destinationContainer = viewContainerRegistry.get(targetCompositeId); - if (destinationContainer) { - this.viewDescriptorService.moveViewsToContainer(this.viewDescriptorService.getViewDescriptors(currentContainer)!.allViewDescriptors.filter(vd => vd.canMoveView), destinationContainer); - this.openComposite(targetCompositeId, true); + if (destinationContainer && !destinationContainer.rejectAddedViews) { + const viewsToMove = this.viewDescriptorService.getViewDescriptors(currentContainer)!.allViewDescriptors.filter(vd => vd.canMoveView); + this.viewDescriptorService.moveViewsToContainer(viewsToMove, destinationContainer); + this.openComposite(targetCompositeId, true).then(composite => { + if (composite && viewsToMove.length === 1) { + composite.openView(viewsToMove[0].id, true); + } + }); } } else { this.moveComposite(dragData.id, targetCompositeId); @@ -73,10 +80,14 @@ export class CompositeDragAndDrop implements ICompositeDragAndDrop { if (viewDescriptor && viewDescriptor.canMoveView) { if (targetCompositeId) { const destinationContainer = viewContainerRegistry.get(targetCompositeId); - if (destinationContainer) { + if (destinationContainer && !destinationContainer.rejectAddedViews) { if (this.targetContainerLocation === ViewContainerLocation.Sidebar) { this.viewDescriptorService.moveViewsToContainer([viewDescriptor], destinationContainer); - this.openComposite(targetCompositeId, true); + this.openComposite(targetCompositeId, true).then(composite => { + if (composite) { + composite.openView(viewDescriptor.id, true); + } + }); } else { this.viewDescriptorService.moveViewToLocation(viewDescriptor, this.targetContainerLocation); this.moveComposite(this.viewDescriptorService.getViewContainer(viewDescriptor.id)!.id, targetCompositeId); @@ -91,13 +102,25 @@ export class CompositeDragAndDrop implements ICompositeDragAndDrop { this.moveComposite(newCompositeId, targetId); } - this.openComposite(newCompositeId, true); + this.openComposite(newCompositeId, true).then(composite => { + if (composite) { + composite.openView(viewDescriptor.id, true); + } + }); } } } } + onDragEnter(data: CompositeDragAndDropData, targetCompositeId: string | undefined, originalEvent: DragEvent): boolean { + return this.canDrop(data, targetCompositeId); + } + onDragOver(data: CompositeDragAndDropData, targetCompositeId: string | undefined, originalEvent: DragEvent): boolean { + return this.canDrop(data, targetCompositeId); + } + + private canDrop(data: CompositeDragAndDropData, targetCompositeId: string | undefined): boolean { const dragData = data.getData(); const viewContainerRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); @@ -134,6 +157,7 @@ export class CompositeDragAndDrop implements ICompositeDragAndDrop { if (this.targetContainerLocation === ViewContainerLocation.Sidebar) { const destinationContainer = viewContainerRegistry.get(targetCompositeId); return !!destinationContainer && + !destinationContainer.rejectAddedViews && this.viewDescriptorService.getViewDescriptors(currentContainer)!.allViewDescriptors.some(vd => vd.canMoveView); } // ... from sidebar to the panel @@ -155,10 +179,9 @@ export class CompositeDragAndDrop implements ICompositeDragAndDrop { } // ... into a destination - return true; + const destinationContainer = viewContainerRegistry.get(targetCompositeId); + return !!destinationContainer && !destinationContainer.rejectAddedViews; } - - return false; } } @@ -175,7 +198,7 @@ export interface ICompositeBarOptions { getOnCompositeClickAction: (compositeId: string) => Action; getContextMenuActions: () => Action[]; getContextMenuActionsForComposite: (compositeId: string) => Action[]; - openComposite: (compositeId: string) => Promise; + openComposite: (compositeId: string) => Promise; getDefaultCompositeId: () => string; hidePart: () => void; } @@ -200,6 +223,7 @@ export class CompositeBar extends Widget implements ICompositeBar { constructor( items: ICompositeBarItem[], private options: ICompositeBarOptions, + @IThemeService private readonly themeService: IThemeService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextMenuService private readonly contextMenuService: IContextMenuService ) { @@ -228,6 +252,7 @@ export class CompositeBar extends Widget implements ICompositeBar { create(parent: HTMLElement): HTMLElement { const actionBarDiv = parent.appendChild($('.composite-bar')); + const excessDiv = parent.appendChild($('.composite-bar-excess')); this.compositeSwitcherBar = this._register(new ActionBar(actionBarDiv, { actionViewItemProvider: (action: IAction) => { @@ -254,58 +279,99 @@ export class CompositeBar extends Widget implements ICompositeBar { this._register(addDisposableListener(parent, EventType.CONTEXT_MENU, e => this.showContextMenu(e))); // Allow to drop at the end to move composites to the end - this._register(addDisposableListener(parent, EventType.DROP, (e: DragEvent) => { - if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype)) { - EventHelper.stop(e, true); + this._register(new DragAndDropObserver(excessDiv, { + onDragOver: (e: DragEvent) => { + if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype)) { + EventHelper.stop(e, true); - const data = this.compositeTransfer.getData(DraggedCompositeIdentifier.prototype); - if (Array.isArray(data)) { - const draggedCompositeId = data[0].id; - this.compositeTransfer.clearData(DraggedCompositeIdentifier.prototype); + const data = this.compositeTransfer.getData(DraggedCompositeIdentifier.prototype); + if (Array.isArray(data)) { + const draggedCompositeId = data[0].id; - this.options.dndHandler.drop(new CompositeDragAndDropData('composite', draggedCompositeId), undefined, e); - } - } - - if (this.compositeTransfer.hasData(DraggedViewIdentifier.prototype)) { - const data = this.compositeTransfer.getData(DraggedViewIdentifier.prototype); - if (Array.isArray(data)) { - const draggedViewId = data[0].id; - this.compositeTransfer.clearData(DraggedViewIdentifier.prototype); - - this.options.dndHandler.drop(new CompositeDragAndDropData('view', draggedViewId), undefined, e); - } - } - })); - - this._register(addDisposableListener(parent, EventType.DRAG_OVER, (e: DragEvent) => { - if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype)) { - EventHelper.stop(e, true); - - const data = this.compositeTransfer.getData(DraggedCompositeIdentifier.prototype); - if (Array.isArray(data)) { - const draggedCompositeId = data[0].id; - - // Check if drop is allowed - if (e.dataTransfer && !this.options.dndHandler.onDragOver(new CompositeDragAndDropData('composite', draggedCompositeId), undefined, e)) { - e.dataTransfer.dropEffect = 'none'; + // Check if drop is allowed + if (e.dataTransfer && !this.options.dndHandler.onDragOver(new CompositeDragAndDropData('composite', draggedCompositeId), undefined, e)) { + e.dataTransfer.dropEffect = 'none'; + } } } - } - if (this.compositeTransfer.hasData(DraggedViewIdentifier.prototype)) { - EventHelper.stop(e, true); + if (this.compositeTransfer.hasData(DraggedViewIdentifier.prototype)) { + EventHelper.stop(e, true); - const data = this.compositeTransfer.getData(DraggedViewIdentifier.prototype); - if (Array.isArray(data)) { - const draggedViewId = data[0].id; + const data = this.compositeTransfer.getData(DraggedViewIdentifier.prototype); + if (Array.isArray(data)) { + const draggedViewId = data[0].id; - // Check if drop is allowed - if (e.dataTransfer && !this.options.dndHandler.onDragOver(new CompositeDragAndDropData('view', draggedViewId), undefined, e)) { - e.dataTransfer.dropEffect = 'none'; + // Check if drop is allowed + if (e.dataTransfer && !this.options.dndHandler.onDragOver(new CompositeDragAndDropData('view', draggedViewId), undefined, e)) { + e.dataTransfer.dropEffect = 'none'; + } } } - } + }, + + onDragEnter: (e: DragEvent) => { + if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype)) { + EventHelper.stop(e, true); + + const data = this.compositeTransfer.getData(DraggedCompositeIdentifier.prototype); + if (Array.isArray(data)) { + const draggedCompositeId = data[0].id; + + // Check if drop is allowed + const validDropTarget = this.options.dndHandler.onDragEnter(new CompositeDragAndDropData('composite', draggedCompositeId), undefined, e); + this.updateFromDragging(excessDiv, validDropTarget); + } + } + + if (this.compositeTransfer.hasData(DraggedViewIdentifier.prototype)) { + EventHelper.stop(e, true); + + const data = this.compositeTransfer.getData(DraggedViewIdentifier.prototype); + if (Array.isArray(data)) { + const draggedViewId = data[0].id; + + // Check if drop is allowed + const validDropTarget = this.options.dndHandler.onDragEnter(new CompositeDragAndDropData('view', draggedViewId), undefined, e); + this.updateFromDragging(excessDiv, validDropTarget); + } + } + }, + + onDragLeave: (e: DragEvent) => { + if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype) || + this.compositeTransfer.hasData(DraggedViewIdentifier.prototype)) { + this.updateFromDragging(excessDiv, false); + } + }, + onDragEnd: (e: DragEvent) => { + // no-op, will not be called + }, + onDrop: (e: DragEvent) => { + if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype)) { + EventHelper.stop(e, true); + + const data = this.compositeTransfer.getData(DraggedCompositeIdentifier.prototype); + if (Array.isArray(data)) { + const draggedCompositeId = data[0].id; + this.compositeTransfer.clearData(DraggedCompositeIdentifier.prototype); + + this.options.dndHandler.drop(new CompositeDragAndDropData('composite', draggedCompositeId), undefined, e); + this.updateFromDragging(excessDiv, false); + } + } + + if (this.compositeTransfer.hasData(DraggedViewIdentifier.prototype)) { + const data = this.compositeTransfer.getData(DraggedViewIdentifier.prototype); + if (Array.isArray(data)) { + const draggedViewId = data[0].id; + this.compositeTransfer.clearData(DraggedViewIdentifier.prototype); + + this.options.dndHandler.drop(new CompositeDragAndDropData('view', draggedViewId), undefined, e); + this.updateFromDragging(excessDiv, false); + } + } + }, })); return actionBarDiv; @@ -410,6 +476,13 @@ export class CompositeBar extends Widget implements ICompositeBar { } } + private updateFromDragging(element: HTMLElement, isDragging: boolean): void { + const theme = this.themeService.getTheme(); + const dragBackground = this.options.colors(theme).dragAndDropBackground; + + element.style.backgroundColor = isDragging && dragBackground ? dragBackground.toString() : ''; + } + private resetActiveComposite(compositeId: string) { const defaultCompositeId = this.options.getDefaultCompositeId(); diff --git a/src/vs/workbench/browser/parts/compositeBarActions.ts b/src/vs/workbench/browser/parts/compositeBarActions.ts index b025bf17474..af39c3cfdf6 100644 --- a/src/vs/workbench/browser/parts/compositeBarActions.ts +++ b/src/vs/workbench/browser/parts/compositeBarActions.ts @@ -364,10 +364,8 @@ export class CompositeOverflowActivityAction extends ActivityAction { }); } - run(event: any): Promise { + async run(): Promise { this.showMenu(); - - return Promise.resolve(true); } } @@ -442,7 +440,7 @@ class ManageExtensionAction extends Action { super('activitybar.manage.extension', nls.localize('manageExtension', "Manage Extension")); } - run(id: string): Promise { + run(id: string): Promise { return this.commandService.executeCommand('_extensions.manage', id); } } @@ -544,14 +542,16 @@ export class CompositeActionViewItem extends ActivityActionViewItem { if (this.compositeTransfer.hasData(DraggedCompositeIdentifier.prototype)) { const data = this.compositeTransfer.getData(DraggedCompositeIdentifier.prototype); if (Array.isArray(data) && data[0].id !== this.activity.id) { - this.updateFromDragging(container, true); + const validDropTarget = this.dndHandler.onDragEnter(new CompositeDragAndDropData('composite', data[0].id), this.activity.id, e); + this.updateFromDragging(container, validDropTarget); } } if (this.compositeTransfer.hasData(DraggedViewIdentifier.prototype)) { const data = this.compositeTransfer.getData(DraggedViewIdentifier.prototype); if (Array.isArray(data) && data[0].id !== this.activity.id) { - this.updateFromDragging(container, true); + const validDropTarget = this.dndHandler.onDragEnter(new CompositeDragAndDropData('view', data[0].id), this.activity.id, e); + this.updateFromDragging(container, validDropTarget); } } }, @@ -616,6 +616,8 @@ export class CompositeActionViewItem extends ActivityActionViewItem { const data = this.compositeTransfer.getData(DraggedViewIdentifier.prototype); if (Array.isArray(data)) { const draggedViewId = data[0].id; + this.updateFromDragging(container, false); + this.compositeTransfer.clearData(DraggedViewIdentifier.prototype); this.dndHandler.drop(new CompositeDragAndDropData('view', draggedViewId), this.activity.id, e); } @@ -729,7 +731,7 @@ export class ToggleCompositePinnedAction extends Action { this.checked = !!this.activity && this.compositeBar.isPinned(this.activity.id); } - run(context: string): Promise { + async run(context: string): Promise { const id = this.activity ? this.activity.id : context; if (this.compositeBar.isPinned(id)) { @@ -737,7 +739,5 @@ export class ToggleCompositePinnedAction extends Action { } else { this.compositeBar.pin(id); } - - return Promise.resolve(true); } } diff --git a/src/vs/workbench/browser/parts/editor/baseEditor.ts b/src/vs/workbench/browser/parts/editor/baseEditor.ts index 1d49f9aed7a..0aa41ce8a0c 100644 --- a/src/vs/workbench/browser/parts/editor/baseEditor.ts +++ b/src/vs/workbench/browser/parts/editor/baseEditor.ts @@ -62,10 +62,6 @@ export abstract class BaseEditor extends Panel implements IEditor { return this._input; } - get options(): EditorOptions | undefined { - return this._options; - } - get group(): IEditorGroup | undefined { return this._group; } diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index a5d1e5f4f4b..10f1f735dd4 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -42,7 +42,7 @@ import * as editorCommands from 'vs/workbench/browser/parts/editor/editorCommand import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { getQuickNavigateHandler, inQuickOpenContext } from 'vs/workbench/browser/parts/quickopen/quickopen'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { isMacintosh } from 'vs/base/common/platform'; import { AllEditorsByAppearancePicker, ActiveGroupEditorsByMostRecentlyUsedPicker, AllEditorsByMostRecentlyUsedPicker } from 'vs/workbench/browser/parts/editor/editorPicker'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; @@ -273,13 +273,13 @@ export class QuickOpenActionContributor extends ActionBarContributor { super(); } - hasActions(context: any): boolean { + hasActions(context: unknown): boolean { const entry = this.getEntry(context); return !!entry; } - getActions(context: any): ReadonlyArray { + getActions(context: unknown): ReadonlyArray { const actions: Action[] = []; const entry = this.getEntry(context); @@ -511,7 +511,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: editorCommands. interface IEditorToolItem { id: string; title: string; icon?: { dark?: URI; light?: URI; } | ThemeIcon; } -function appendEditorToolItem(primary: IEditorToolItem, when: ContextKeyExpr | undefined, order: number, alternative?: IEditorToolItem, precondition?: ContextKeyExpr | undefined): void { +function appendEditorToolItem(primary: IEditorToolItem, when: ContextKeyExpression | undefined, order: number, alternative?: IEditorToolItem, precondition?: ContextKeyExpression | undefined): void { const item: IMenuItem = { command: { id: primary.id, diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index 4d1f372052f..c145f1ca68d 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -37,7 +37,7 @@ export class ExecuteCommandAction extends Action { super(id, label); } - run(): Promise { + run(): Promise { return this.commandService.executeCommand(this.commandId, this.commandArgs); } } @@ -71,7 +71,7 @@ export class BaseSplitEditorAction extends Action { })); } - async run(context?: IEditorIdentifier): Promise { + async run(context?: IEditorIdentifier): Promise { splitEditor(this.editorGroupService, this.direction, context); } } @@ -181,7 +181,7 @@ export class JoinTwoGroupsAction extends Action { super(id, label); } - async run(context?: IEditorIdentifier): Promise { + async run(context?: IEditorIdentifier): Promise { let sourceGroup: IEditorGroup | undefined; if (context && typeof context.groupId === 'number') { sourceGroup = this.editorGroupService.getGroup(context.groupId); @@ -216,7 +216,7 @@ export class JoinAllGroupsAction extends Action { super(id, label); } - async run(context?: IEditorIdentifier): Promise { + async run(): Promise { mergeAllGroups(this.editorGroupService); } } @@ -234,7 +234,7 @@ export class NavigateBetweenGroupsAction extends Action { super(id, label); } - async run(): Promise { + async run(): Promise { const nextGroup = this.editorGroupService.findGroup({ location: GroupLocation.NEXT }, this.editorGroupService.activeGroup, true); nextGroup.focus(); } @@ -253,7 +253,7 @@ export class FocusActiveGroupAction extends Action { super(id, label); } - async run(): Promise { + async run(): Promise { this.editorGroupService.activeGroup.focus(); } } @@ -269,7 +269,7 @@ export abstract class BaseFocusGroupAction extends Action { super(id, label); } - async run(): Promise { + async run(): Promise { const group = this.editorGroupService.findGroup(this.scope, this.editorGroupService.activeGroup, true); if (group) { group.focus(); @@ -409,25 +409,26 @@ export class OpenToSideFromQuickOpenAction extends Action { this.class = (preferredDirection === GroupDirection.RIGHT) ? 'codicon-split-horizontal' : 'codicon-split-vertical'; } - async run(context: any): Promise { + async run(context: unknown): Promise { const entry = toEditorQuickOpenEntry(context); if (entry) { const input = entry.getInput(); if (input) { if (input instanceof EditorInput) { - return this.editorService.openEditor(input, entry.getOptions(), SIDE_GROUP); + await this.editorService.openEditor(input, entry.getOptions(), SIDE_GROUP); + return; } const resourceInput = input as IResourceInput; resourceInput.options = mixin(resourceInput.options, entry.getOptions()); - return this.editorService.openEditor(resourceInput, SIDE_GROUP); + await this.editorService.openEditor(resourceInput, SIDE_GROUP); } } } } -export function toEditorQuickOpenEntry(element: any): IEditorQuickOpenEntry | null { +export function toEditorQuickOpenEntry(element: unknown): IEditorQuickOpenEntry | null { // QuickOpenEntryGroup if (element instanceof QuickOpenEntryGroup) { @@ -458,7 +459,7 @@ export class CloseEditorAction extends Action { super(id, label, 'codicon-close'); } - run(context?: IEditorCommandsContext): Promise { + run(context?: IEditorCommandsContext): Promise { return this.commandService.executeCommand(CLOSE_EDITOR_COMMAND_ID, undefined, context); } } @@ -476,7 +477,7 @@ export class CloseOneEditorAction extends Action { super(id, label, 'codicon-close'); } - async run(context?: IEditorCommandsContext): Promise { + async run(context?: IEditorCommandsContext): Promise { let group: IEditorGroup | undefined; let editorIndex: number | undefined; if (context) { @@ -519,7 +520,7 @@ export class RevertAndCloseEditorAction extends Action { super(id, label); } - async run(): Promise { + async run(): Promise { const activeControl = this.editorService.activeControl; if (activeControl) { const editor = activeControl.input; @@ -555,7 +556,7 @@ export class CloseLeftEditorsInGroupAction extends Action { super(id, label); } - async run(context?: IEditorIdentifier): Promise { + async run(context?: IEditorIdentifier): Promise { const { group, editor } = getTarget(this.editorService, this.editorGroupService, context); if (group && editor) { return group.closeEditors({ direction: CloseDirection.LEFT, except: editor }); @@ -600,7 +601,7 @@ export abstract class BaseCloseAllAction extends Action { return groupsToClose; } - async run(): Promise { + async run(): Promise { // Just close all if there are no dirty editors if (!this.workingCopyService.hasDirty) { @@ -653,7 +654,7 @@ export abstract class BaseCloseAllAction extends Action { } } - protected abstract doCloseAll(): Promise; + protected abstract doCloseAll(): Promise; } export class CloseAllEditorsAction extends BaseCloseAllAction { @@ -672,8 +673,8 @@ export class CloseAllEditorsAction extends BaseCloseAllAction { super(id, label, 'codicon-close-all', workingCopyService, fileDialogService, editorGroupService, editorService); } - protected doCloseAll(): Promise { - return Promise.all(this.groupsToClose.map(g => g.closeAllEditors())); + protected async doCloseAll(): Promise { + await Promise.all(this.groupsToClose.map(g => g.closeAllEditors())); } } @@ -693,7 +694,7 @@ export class CloseAllEditorGroupsAction extends BaseCloseAllAction { super(id, label, undefined, workingCopyService, fileDialogService, editorGroupService, editorService); } - protected async doCloseAll(): Promise { + protected async doCloseAll(): Promise { await Promise.all(this.groupsToClose.map(group => group.closeAllEditors())); this.groupsToClose.forEach(group => this.editorGroupService.removeGroup(group)); @@ -713,9 +714,9 @@ export class CloseEditorsInOtherGroupsAction extends Action { super(id, label); } - run(context?: IEditorIdentifier): Promise { + async run(context?: IEditorIdentifier): Promise { const groupToSkip = context ? this.editorGroupService.getGroup(context.groupId) : this.editorGroupService.activeGroup; - return Promise.all(this.editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE).map(async g => { + await Promise.all(this.editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE).map(async g => { if (groupToSkip && g.id === groupToSkip.id) { return; } @@ -739,10 +740,10 @@ export class CloseEditorInAllGroupsAction extends Action { super(id, label); } - async run(): Promise { + async run(): Promise { const activeEditor = this.editorService.activeEditor; if (activeEditor) { - return Promise.all(this.editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE).map(g => g.closeEditor(activeEditor))); + await Promise.all(this.editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE).map(g => g.closeEditor(activeEditor))); } } } @@ -758,7 +759,7 @@ export class BaseMoveGroupAction extends Action { super(id, label); } - async run(context?: IEditorIdentifier): Promise { + async run(context?: IEditorIdentifier): Promise { let sourceGroup: IEditorGroup | undefined; if (context && typeof context.groupId === 'number') { sourceGroup = this.editorGroupService.getGroup(context.groupId); @@ -867,7 +868,7 @@ export class MinimizeOtherGroupsAction extends Action { super(id, label); } - async run(): Promise { + async run(): Promise { this.editorGroupService.arrangeGroups(GroupsArrangement.MINIMIZE_OTHERS); } } @@ -881,7 +882,7 @@ export class ResetGroupSizesAction extends Action { super(id, label); } - async run(): Promise { + async run(): Promise { this.editorGroupService.arrangeGroups(GroupsArrangement.EVEN); } } @@ -895,7 +896,7 @@ export class ToggleGroupSizesAction extends Action { super(id, label); } - async run(): Promise { + async run(): Promise { this.editorGroupService.arrangeGroups(GroupsArrangement.TOGGLE); } } @@ -915,7 +916,7 @@ export class MaximizeGroupAction extends Action { super(id, label); } - async run(): Promise { + async run(): Promise { if (this.editorService.activeEditor) { this.editorGroupService.arrangeGroups(GroupsArrangement.MINIMIZE_OTHERS); this.layoutService.setSideBarHidden(true); @@ -934,7 +935,7 @@ export abstract class BaseNavigateEditorAction extends Action { super(id, label); } - async run(): Promise { + async run(): Promise { const result = this.navigate(); if (!result) { return; @@ -947,7 +948,7 @@ export abstract class BaseNavigateEditorAction extends Action { const group = this.editorGroupService.getGroup(groupId); if (group) { - return group.openEditor(editor); + await group.openEditor(editor); } } @@ -1123,7 +1124,7 @@ export class NavigateForwardAction extends Action { super(id, label); } - async run(): Promise { + async run(): Promise { this.historyService.forward(); } } @@ -1137,7 +1138,7 @@ export class NavigateBackwardsAction extends Action { super(id, label); } - async run(): Promise { + async run(): Promise { this.historyService.back(); } } @@ -1151,7 +1152,7 @@ export class NavigateToLastEditLocationAction extends Action { super(id, label); } - async run(): Promise { + async run(): Promise { this.historyService.openLastEditLocation(); } } @@ -1165,7 +1166,7 @@ export class NavigateLastAction extends Action { super(id, label); } - async run(): Promise { + async run(): Promise { this.historyService.last(); } } @@ -1183,7 +1184,7 @@ export class ReopenClosedEditorAction extends Action { super(id, label); } - async run(): Promise { + async run(): Promise { this.historyService.reopenLastClosedEditor(); } } @@ -1202,7 +1203,7 @@ export class ClearRecentFilesAction extends Action { super(id, label); } - async run(): Promise { + async run(): Promise { // Clear global recently opened this.workspacesService.clearRecentlyOpened(); @@ -1266,7 +1267,7 @@ export class BaseQuickOpenEditorAction extends Action { super(id, label); } - async run(): Promise { + async run(): Promise { const keybindings = this.keybindingService.lookupKeybindings(this.id); this.quickOpenService.show(this.prefix, { quickNavigateConfiguration: { keybindings } }); @@ -1347,7 +1348,7 @@ export class QuickOpenPreviousEditorFromHistoryAction extends Action { super(id, label); } - async run(): Promise { + async run(): Promise { const keybindings = this.keybindingService.lookupKeybindings(this.id); this.quickOpenService.show(undefined, { quickNavigateConfiguration: { keybindings } }); @@ -1367,7 +1368,7 @@ export class OpenNextRecentlyUsedEditorAction extends Action { super(id, label); } - async run(): Promise { + async run(): Promise { this.historyService.openNextRecentlyUsedEditor(); } } @@ -1385,7 +1386,7 @@ export class OpenPreviousRecentlyUsedEditorAction extends Action { super(id, label); } - async run(): Promise { + async run(): Promise { this.historyService.openPreviouslyUsedEditor(); } } @@ -1404,7 +1405,7 @@ export class OpenNextRecentlyUsedEditorInGroupAction extends Action { super(id, label); } - async run(): Promise { + async run(): Promise { this.historyService.openNextRecentlyUsedEditor(this.editorGroupsService.activeGroup.id); } } @@ -1423,7 +1424,7 @@ export class OpenPreviousRecentlyUsedEditorInGroupAction extends Action { super(id, label); } - async run(): Promise { + async run(): Promise { this.historyService.openPreviouslyUsedEditor(this.editorGroupsService.activeGroup.id); } } @@ -1441,7 +1442,7 @@ export class ClearEditorHistoryAction extends Action { super(id, label); } - async run(): Promise { + async run(): Promise { // Editor history this.historyService.clear(); @@ -1711,7 +1712,7 @@ export class BaseCreateEditorGroupAction extends Action { super(id, label); } - async run(): Promise { + async run(): Promise { this.editorGroupService.addGroup(this.editorGroupService.activeGroup, this.direction, { activate: true }); } } diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index eeb80e7449b..924b5ce7682 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -7,8 +7,8 @@ import * as nls from 'vs/nls'; import * as types from 'vs/base/common/types'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { TextCompareEditorVisibleContext, EditorInput, IEditorIdentifier, IEditorCommandsContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, CloseDirection, IEditor, IEditorInput } from 'vs/workbench/common/editor'; -import { IEditorService, IVisibleEditor } from 'vs/workbench/services/editor/common/editorService'; +import { TextCompareEditorVisibleContext, EditorInput, IEditorIdentifier, IEditorCommandsContext, ActiveEditorGroupEmptyContext, MultipleEditorGroupsContext, CloseDirection, IEditor, IEditorInput, IVisibleEditor } from 'vs/workbench/common/editor'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { TextDiffEditor } from 'vs/workbench/browser/parts/editor/textDiffEditor'; import { KeyMod, KeyCode, KeyChord } from 'vs/base/common/keyCodes'; @@ -84,7 +84,7 @@ function registerActiveEditorMoveCommand(): void { weight: KeybindingWeight.WorkbenchContrib, when: EditorContextKeys.editorTextFocus, primary: 0, - handler: (accessor, args: any) => moveActiveEditor(args, accessor), + handler: (accessor, args) => moveActiveEditor(args, accessor), description: { description: nls.localize('editorCommand.activeEditorMove.description', "Move the active editor by tabs or groups"), args: [ diff --git a/src/vs/workbench/browser/parts/editor/editorControl.ts b/src/vs/workbench/browser/parts/editor/editorControl.ts index 1cbe3fbcd53..f9e57becfeb 100644 --- a/src/vs/workbench/browser/parts/editor/editorControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorControl.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { EditorInput, EditorOptions } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, IVisibleEditor } from 'vs/workbench/common/editor'; import { Dimension, show, hide, addClass } from 'vs/base/browser/dom'; import { Registry } from 'vs/platform/registry/common/platform'; import { IEditorRegistry, Extensions as EditorExtensions, IEditorDescriptor } from 'vs/workbench/browser/editor'; @@ -14,7 +14,6 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IEditorProgressService, LongRunningOperation } from 'vs/platform/progress/common/progress'; import { IEditorGroupView, DEFAULT_EDITOR_MIN_DIMENSIONS, DEFAULT_EDITOR_MAX_DIMENSIONS } from 'vs/workbench/browser/parts/editor/editor'; import { Emitter } from 'vs/base/common/event'; -import { IVisibleEditor } from 'vs/workbench/services/editor/common/editorService'; import { assertIsDefined } from 'vs/base/common/types'; export interface IOpenEditorResult { diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 39ab5198646..29fdbb4f0c5 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, SaveReason, IEditorPartOptionsChangeEvent, EditorsOrder } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, GroupIdentifier, SideBySideEditorInput, CloseDirection, IEditorCloseEvent, EditorGroupActiveEditorDirtyContext, IEditor, EditorGroupEditorsCountContext, SaveReason, IEditorPartOptionsChangeEvent, EditorsOrder, IVisibleEditor } 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'; @@ -25,7 +25,7 @@ import { EditorProgressIndicator } from 'vs/workbench/services/progress/browser/ import { localize } from 'vs/nls'; import { isPromiseCanceledError } from 'vs/base/common/errors'; import { dispose, MutableDisposable } from 'vs/base/common/lifecycle'; -import { Severity, INotificationService, INotificationActions } from 'vs/platform/notification/common/notification'; +import { Severity, INotificationService } from 'vs/platform/notification/common/notification'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { RunOnceWorker } from 'vs/base/common/async'; @@ -42,7 +42,7 @@ import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { isErrorWithActions, IErrorWithActions } from 'vs/base/common/errorsWithActions'; -import { IVisibleEditor, IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { withNullAsUndefined, withUndefinedAsNull } from 'vs/base/common/types'; import { hash } from 'vs/base/common/hash'; import { guessMimeTypes } from 'vs/base/common/mime'; @@ -980,7 +980,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Otherwise, show a background notification. else { - const actions: INotificationActions = { primary: [] }; + const actions = { primary: [] as readonly IAction[] }; if (Array.isArray(errorActions)) { actions.primary = errorActions; } @@ -1075,7 +1075,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Update model and make sure to continue to use the editor we get from // the model. It is possible that the editor was already opened and we // want to ensure that we use the existing instance in that case. - const editor = this.group.getEditorByIndex(currentIndex)!; + const editor = this._group.getEditorByIndex(currentIndex); + if (!editor) { + return; + } // Update model this._group.moveEditor(editor, moveToIndex); @@ -1278,7 +1281,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return false; // editor must be dirty and not saving } - if (editor instanceof SideBySideEditorInput && this.isOpened(editor.master)) { + if (editor instanceof SideBySideEditorInput && this._group.contains(editor.master)) { return false; // master-side of editor is still opened somewhere else } @@ -1329,25 +1332,24 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Otherwise, handle accordingly switch (res) { case ConfirmResult.SAVE: - const result = await editor.save(this.id, { reason: SaveReason.EXPLICIT }); + await editor.save(this.id, { reason: SaveReason.EXPLICIT }); - return !result; + return editor.isDirty(); // veto if still dirty case ConfirmResult.DONT_SAVE: - try { // first try a normal revert where the contents of the editor are restored - const result = await editor.revert(this.id); + await editor.revert(this.id); - return !result; + return editor.isDirty(); // veto if still dirty } catch (error) { // if that fails, since we are about to close the editor, we accept that // the editor cannot be reverted and instead do a soft revert that just // enables us to close the editor. With this, a user can always close a // dirty editor even when reverting fails. - const result = await editor.revert(this.id, { soft: true }); + await editor.revert(this.id, { soft: true }); - return !result; + return editor.isDirty(); // veto if still dirty } case ConfirmResult.CANCEL: return true; // veto diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index 18ba6d61dcb..574b6c7092c 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -13,7 +13,7 @@ import { URI } from 'vs/base/common/uri'; import { Action } from 'vs/base/common/actions'; import { Language } from 'vs/base/common/platform'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; -import { IFileEditorInput, EncodingMode, IEncodingSupport, toResource, SideBySideEditorInput, IEditor as IBaseEditor, IEditorInput, SideBySideEditor, IModeSupport } from 'vs/workbench/common/editor'; +import { IFileEditorInput, EncodingMode, IEncodingSupport, toResource, SideBySideEditorInput, IEditor, IEditorInput, SideBySideEditor, IModeSupport } from 'vs/workbench/common/editor'; import { Disposable, MutableDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IEditorAction } from 'vs/editor/common/editorCommon'; import { EndOfLineSequence } from 'vs/editor/common/model'; @@ -144,6 +144,7 @@ class StateChange { encoding: boolean = false; EOL: boolean = false; tabFocusMode: boolean = false; + columnSelectionMode: boolean = false; screenReaderMode: boolean = false; metadata: boolean = false; @@ -154,6 +155,7 @@ class StateChange { this.encoding = this.encoding || other.encoding; this.EOL = this.EOL || other.EOL; this.tabFocusMode = this.tabFocusMode || other.tabFocusMode; + this.columnSelectionMode = this.columnSelectionMode || other.columnSelectionMode; this.screenReaderMode = this.screenReaderMode || other.screenReaderMode; this.metadata = this.metadata || other.metadata; } @@ -165,21 +167,23 @@ class StateChange { || this.encoding || this.EOL || this.tabFocusMode + || this.columnSelectionMode || this.screenReaderMode || this.metadata; } } -interface StateDelta { - selectionStatus?: string; - mode?: string; - encoding?: string; - EOL?: string; - indentation?: string; - tabFocusMode?: boolean; - screenReaderMode?: boolean; - metadata?: string | undefined; -} +type StateDelta = ( + { type: 'selectionStatus'; selectionStatus: string | undefined; } + | { type: 'mode'; mode: string | undefined; } + | { type: 'encoding'; encoding: string | undefined; } + | { type: 'EOL'; EOL: string | undefined; } + | { type: 'indentation'; indentation: string | undefined; } + | { type: 'tabFocusMode'; tabFocusMode: boolean; } + | { type: 'columnSelectionMode'; columnSelectionMode: boolean; } + | { type: 'screenReaderMode'; screenReaderMode: boolean; } + | { type: 'metadata'; metadata: string | undefined; } +); class State { @@ -201,6 +205,9 @@ class State { private _tabFocusMode: boolean | undefined; get tabFocusMode(): boolean | undefined { return this._tabFocusMode; } + private _columnSelectionMode: boolean | undefined; + get columnSelectionMode(): boolean | undefined { return this._columnSelectionMode; } + private _screenReaderMode: boolean | undefined; get screenReaderMode(): boolean | undefined { return this._screenReaderMode; } @@ -210,56 +217,63 @@ class State { update(update: StateDelta): StateChange { const change = new StateChange(); - if ('selectionStatus' in update) { + if (update.type === 'selectionStatus') { if (this._selectionStatus !== update.selectionStatus) { this._selectionStatus = update.selectionStatus; change.selectionStatus = true; } } - if ('indentation' in update) { + if (update.type === 'indentation') { if (this._indentation !== update.indentation) { this._indentation = update.indentation; change.indentation = true; } } - if ('mode' in update) { + if (update.type === 'mode') { if (this._mode !== update.mode) { this._mode = update.mode; change.mode = true; } } - if ('encoding' in update) { + if (update.type === 'encoding') { if (this._encoding !== update.encoding) { this._encoding = update.encoding; change.encoding = true; } } - if ('EOL' in update) { + if (update.type === 'EOL') { if (this._EOL !== update.EOL) { this._EOL = update.EOL; change.EOL = true; } } - if ('tabFocusMode' in update) { + if (update.type === 'tabFocusMode') { if (this._tabFocusMode !== update.tabFocusMode) { this._tabFocusMode = update.tabFocusMode; change.tabFocusMode = true; } } - if ('screenReaderMode' in update) { + if (update.type === 'columnSelectionMode') { + if (this._columnSelectionMode !== update.columnSelectionMode) { + this._columnSelectionMode = update.columnSelectionMode; + change.columnSelectionMode = true; + } + } + + if (update.type === 'screenReaderMode') { if (this._screenReaderMode !== update.screenReaderMode) { this._screenReaderMode = update.screenReaderMode; change.screenReaderMode = true; } } - if ('metadata' in update) { + if (update.type === 'metadata') { if (this._metadata !== update.metadata) { this._metadata = update.metadata; change.metadata = true; @@ -279,6 +293,7 @@ const nlsEOLCRLF = nls.localize('endOfLineCarriageReturnLineFeed', "CRLF"); export class EditorStatus extends Disposable implements IWorkbenchContribution { private readonly tabFocusModeElement = this._register(new MutableDisposable()); + private readonly columnSelectionModeElement = this._register(new MutableDisposable()); private readonly screenRedearModeElement = this._register(new MutableDisposable()); private readonly indentationElement = this._register(new MutableDisposable()); private readonly selectionElement = this._register(new MutableDisposable()); @@ -399,6 +414,22 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { } } + private updateColumnSelectionModeElement(visible: boolean): void { + if (visible) { + if (!this.columnSelectionModeElement.value) { + this.columnSelectionModeElement.value = this.statusbarService.addEntry({ + text: nls.localize('columnSelectionModeEnabled', "Column Selection"), + tooltip: nls.localize('disableColumnSelectionMode', "Disable Column Selection Mode"), + command: 'editor.action.toggleColumnSelection', + backgroundColor: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_BACKGROUND), + color: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_FOREGROUND) + }, 'status.editor.columnSelectionMode', nls.localize('status.editor.columnSelectionMode', "Column Selection Mode"), StatusbarAlignment.RIGHT, 100.8); + } + } else { + this.columnSelectionModeElement.clear(); + } + } + private updateScreenReaderModeElement(visible: boolean): void { if (visible) { if (!this.screenRedearModeElement.value) { @@ -541,6 +572,7 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { private doRenderNow(changed: StateChange): void { this.updateTabFocusModeElement(!!this.state.tabFocusMode); + this.updateColumnSelectionModeElement(!!this.state.columnSelectionMode); this.updateScreenReaderModeElement(!!this.state.screenReaderMode); this.updateIndentationElement(this.state.indentation); this.updateSelectionElement(this.state.selectionStatus && !this.state.screenReaderMode ? this.state.selectionStatus : undefined); @@ -580,6 +612,7 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { const activeCodeEditor = activeControl ? withNullAsUndefined(getCodeEditor(activeControl.getControl())) : undefined; // Update all states + this.onColumnSelectionModeChange(activeCodeEditor); this.onScreenReaderModeChange(activeCodeEditor); this.onSelectionChange(activeCodeEditor); this.onModeChange(activeCodeEditor, activeInput); @@ -597,6 +630,9 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { // Hook Listener for Configuration changes this.activeEditorListeners.add(activeCodeEditor.onDidChangeConfiguration((event: ConfigurationChangedEvent) => { + if (event.hasChanged(EditorOption.columnSelection)) { + this.onColumnSelectionModeChange(activeCodeEditor); + } if (event.hasChanged(EditorOption.accessibilitySupport)) { this.onScreenReaderModeChange(activeCodeEditor); } @@ -665,14 +701,14 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { } private onModeChange(editorWidget: ICodeEditor | undefined, editorInput: IEditorInput | undefined): void { - let info: StateDelta = { mode: undefined }; + let info: StateDelta = { type: 'mode', mode: undefined }; // We only support text based editors if (editorWidget && editorInput && toEditorWithModeSupport(editorInput)) { const textModel = editorWidget.getModel(); if (textModel) { const modeId = textModel.getLanguageIdentifier().language; - info = { mode: withNullAsUndefined(this.modeService.getLanguageName(modeId)) }; + info.mode = withNullAsUndefined(this.modeService.getLanguageName(modeId)); } } @@ -680,7 +716,7 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { } private onIndentationChange(editorWidget: ICodeEditor | undefined): void { - const update: StateDelta = { indentation: undefined }; + const update: StateDelta = { type: 'indentation', indentation: undefined }; if (editorWidget) { const model = editorWidget.getModel(); @@ -697,8 +733,8 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { this.updateState(update); } - private onMetadataChange(editor: IBaseEditor | undefined): void { - const update: StateDelta = { metadata: undefined }; + private onMetadataChange(editor: IEditor | undefined): void { + const update: StateDelta = { type: 'metadata', metadata: undefined }; if (editor instanceof BaseBinaryResourceEditor || editor instanceof BinaryResourceDiffEditor) { update.metadata = editor.getMetadata(); @@ -707,6 +743,16 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { this.updateState(update); } + private onColumnSelectionModeChange(editorWidget: ICodeEditor | undefined): void { + const info: StateDelta = { type: 'columnSelectionMode', columnSelectionMode: false }; + + if (editorWidget && editorWidget.getOption(EditorOption.columnSelection)) { + info.columnSelectionMode = true; + } + + this.updateState(info); + } + private onScreenReaderModeChange(editorWidget: ICodeEditor | undefined): void { let screenReaderMode = false; @@ -730,7 +776,7 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { this.screenReaderNotification.close(); } - this.updateState({ screenReaderMode: screenReaderMode }); + this.updateState({ type: 'screenReaderMode', screenReaderMode: screenReaderMode }); } private onSelectionChange(editorWidget: ICodeEditor | undefined): void { @@ -770,11 +816,11 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { } } - this.updateState({ selectionStatus: this.getSelectionLabel(info) }); + this.updateState({ type: 'selectionStatus', selectionStatus: this.getSelectionLabel(info) }); } private onEOLChange(editorWidget: ICodeEditor | undefined): void { - const info: StateDelta = { EOL: undefined }; + const info: StateDelta = { type: 'EOL', EOL: undefined }; if (editorWidget && !editorWidget.getOption(EditorOption.readOnly)) { const codeEditorModel = editorWidget.getModel(); @@ -786,12 +832,12 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { this.updateState(info); } - private onEncodingChange(editor: IBaseEditor | undefined, editorWidget: ICodeEditor | undefined): void { + private onEncodingChange(editor: IEditor | undefined, editorWidget: ICodeEditor | undefined): void { if (editor && !this.isActiveEditor(editor)) { return; } - const info: StateDelta = { encoding: undefined }; + const info: StateDelta = { type: 'encoding', encoding: undefined }; // We only support text based editors that have a model associated // This ensures we do not show the encoding picker while an editor @@ -825,12 +871,12 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { } private onTabFocusModeChange(): void { - const info: StateDelta = { tabFocusMode: TabFocus.getTabFocusMode() }; + const info: StateDelta = { type: 'tabFocusMode', tabFocusMode: TabFocus.getTabFocusMode() }; this.updateState(info); } - private isActiveEditor(control: IBaseEditor): boolean { + private isActiveEditor(control: IEditor): boolean { const activeControl = this.editorService.activeControl; return !!activeControl && activeControl === control; @@ -1000,10 +1046,11 @@ export class ChangeModeAction extends Action { super(actionId, actionLabel); } - async run(): Promise { + async run(): Promise { const activeTextEditorWidget = getCodeEditor(this.editorService.activeTextEditorWidget); if (!activeTextEditorWidget) { - return this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]); + await this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]); + return; } const textModel = activeTextEditorWidget.getModel(); @@ -1200,14 +1247,16 @@ export class ChangeEOLAction extends Action { super(actionId, actionLabel); } - async run(): Promise { + async run(): Promise { const activeTextEditorWidget = getCodeEditor(this.editorService.activeTextEditorWidget); if (!activeTextEditorWidget) { - return this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]); + await this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]); + return; } if (this.editorService.activeEditor?.isReadonly()) { - return this.quickInputService.pick([{ label: nls.localize('noWritableCodeEditor', "The active code editor is read-only.") }]); + await this.quickInputService.pick([{ label: nls.localize('noWritableCodeEditor', "The active code editor is read-only.") }]); + return; } let textModel = activeTextEditorWidget.getModel(); @@ -1249,19 +1298,22 @@ export class ChangeEncodingAction extends Action { super(actionId, actionLabel); } - async run(): Promise { + async run(): Promise { if (!getCodeEditor(this.editorService.activeTextEditorWidget)) { - return this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]); + await this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]); + return; } const activeControl = this.editorService.activeControl; if (!activeControl) { - return this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]); + await this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]); + return; } const encodingSupport: IEncodingSupport | null = toEditorWithEncodingSupport(activeControl.input); if (!encodingSupport) { - return this.quickInputService.pick([{ label: nls.localize('noFileEditor', "No file active at this time") }]); + await this.quickInputService.pick([{ label: nls.localize('noFileEditor', "No file active at this time") }]); + return; } const saveWithEncodingPick: IQuickPickItem = { label: nls.localize('saveWithEncoding', "Save with Encoding") }; @@ -1296,7 +1348,7 @@ export class ChangeEncodingAction extends Action { const resource = toResource(activeControl.input, { supportSideBySide: SideBySideEditor.MASTER }); if (!resource || (!this.fileService.canHandleResource(resource) && resource.scheme !== Schemas.untitled)) { - return null; // encoding detection only possible for resources the file service can handle or that are untitled + return; // encoding detection only possible for resources the file service can handle or that are untitled } let guessedEncoding: string | undefined = undefined; diff --git a/src/vs/workbench/browser/parts/editor/editorsObserver.ts b/src/vs/workbench/browser/parts/editor/editorsObserver.ts index b370de392c8..47d62dc9351 100644 --- a/src/vs/workbench/browser/parts/editor/editorsObserver.ts +++ b/src/vs/workbench/browser/parts/editor/editorsObserver.ts @@ -3,15 +3,16 @@ * 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 { IEditorInput, IEditorInputFactoryRegistry, IEditorIdentifier, GroupIdentifier, Extensions, IEditorPartOptionsChangeEvent, EditorsOrder, toResource, SideBySideEditor } 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 { LinkedMap, Touch, ResourceMap } from 'vs/base/common/map'; import { equals } from 'vs/base/common/objects'; +import { URI } from 'vs/base/common/uri'; interface ISerializedEditorsList { entries: ISerializedEditorIdentifier[]; @@ -37,9 +38,10 @@ export class EditorsObserver extends Disposable { private readonly keyMap = new Map>(); private readonly mostRecentEditorsMap = new LinkedMap(); + private readonly editorResourcesMap = new ResourceMap(); - private readonly _onDidChange = this._register(new Emitter()); - readonly onDidChange = this._onDidChange.event; + private readonly _onDidMostRecentlyActiveEditorsChange = this._register(new Emitter()); + readonly onDidMostRecentlyActiveEditorsChange = this._onDidMostRecentlyActiveEditorsChange.event; get count(): number { return this.mostRecentEditorsMap.size; @@ -49,6 +51,10 @@ export class EditorsObserver extends Disposable { return this.mostRecentEditorsMap.values(); } + hasEditor(resource: URI): boolean { + return this.editorResourcesMap.has(resource); + } + constructor( @IEditorGroupsService private editorGroupsService: IEditorGroupsService, @IStorageService private readonly storageService: IStorageService @@ -72,12 +78,12 @@ export class EditorsObserver extends Disposable { // 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 */); + this.addMostRecentEditor(group, groupEditorsMru[i], false /* is not active */, true /* is new */); } // 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 */); + this.addMostRecentEditor(group, group.activeEditor, true /* is active */, false /* already added before */); } // Group Listeners @@ -92,7 +98,7 @@ export class EditorsObserver extends Disposable { // 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 */); + this.addMostRecentEditor(group, group.activeEditor, true /* is active */, false /* editor already opened */); } break; @@ -102,7 +108,7 @@ export class EditorsObserver extends Disposable { // if group is active, otherwise second most recent case GroupChangeKind.EDITOR_ACTIVE: { if (e.editor) { - this.addMostRecentEditor(group, e.editor, this.editorGroupsService.activeGroup === group); + this.addMostRecentEditor(group, e.editor, this.editorGroupsService.activeGroup === group, false /* editor already opened */); } break; @@ -114,7 +120,7 @@ export class EditorsObserver extends Disposable { // start to close oldest ones if needed. case GroupChangeKind.EDITOR_OPEN: { if (e.editor) { - this.addMostRecentEditor(group, e.editor, false /* is not active */); + this.addMostRecentEditor(group, e.editor, false /* is not active */, true /* is new */); this.ensureOpenedEditorsLimit({ groupId: group.id, editor: e.editor }, group.id); } @@ -148,7 +154,7 @@ export class EditorsObserver extends Disposable { } } - private addMostRecentEditor(group: IEditorGroup, editor: IEditorInput, isActive: boolean): void { + private addMostRecentEditor(group: IEditorGroup, editor: IEditorInput, isActive: boolean, isNew: boolean): void { const key = this.ensureKey(group, editor); const mostRecentEditor = this.mostRecentEditorsMap.first; @@ -169,11 +175,39 @@ export class EditorsObserver extends Disposable { this.mostRecentEditorsMap.set(mostRecentEditor, mostRecentEditor, Touch.AsOld /* make first */); } + // Update in resource map if this is a new editor + if (isNew) { + this.updateEditorResourcesMap(editor, true); + } + // Event - this._onDidChange.fire(); + this._onDidMostRecentlyActiveEditorsChange.fire(); + } + + private updateEditorResourcesMap(editor: IEditorInput, add: boolean): void { + const resource = toResource(editor, { supportSideBySide: SideBySideEditor.MASTER }); + if (!resource) { + return; // require a resource + } + + if (add) { + this.editorResourcesMap.set(resource, (this.editorResourcesMap.get(resource) ?? 0) + 1); + } else { + const counter = this.editorResourcesMap.get(resource) ?? 0; + if (counter > 1) { + this.editorResourcesMap.set(resource, counter - 1); + } else { + this.editorResourcesMap.delete(resource); + } + } } private removeMostRecentEditor(group: IEditorGroup, editor: IEditorInput): void { + + // Update in resource map + this.updateEditorResourcesMap(editor, false); + + // Update in MRU list const key = this.findKey(group, editor); if (key) { @@ -187,7 +221,7 @@ export class EditorsObserver extends Disposable { } // Event - this._onDidChange.fire(); + this._onDidMostRecentlyActiveEditorsChange.fire(); } } @@ -361,7 +395,7 @@ export class EditorsObserver extends Disposable { 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 */); + this.addMostRecentEditor(group, groupEditorsMru[i], true /* enforce as active to preserve order */, true /* is new */); } } } @@ -392,6 +426,9 @@ export class EditorsObserver extends Disposable { // Make sure key is registered as well const editorIdentifier = this.ensureKey(group, editor); mapValues.push([editorIdentifier, editorIdentifier]); + + // Update in resource map + this.updateEditorResourcesMap(editor, true); } // Fill map with deserialized values diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index 3509fe604e0..dffc8df1cf2 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -40,9 +40,9 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { BreadcrumbsControl } from 'vs/workbench/browser/parts/editor/breadcrumbsControl'; import { IFileService } from 'vs/platform/files/common/files'; import { withNullAsUndefined, assertAllDefined, assertIsDefined } from 'vs/base/common/types'; -import { ILabelService } from 'vs/platform/label/common/label'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { basenameOrAuthority } from 'vs/base/common/resources'; +import { RunOnceScheduler } from 'vs/base/common/async'; interface IEditorInputLabel { name?: string; @@ -85,10 +85,9 @@ export class TabsTitleControl extends TitleControl { @IExtensionService extensionService: IExtensionService, @IConfigurationService configurationService: IConfigurationService, @IFileService fileService: IFileService, - @ILabelService labelService: ILabelService, @IEditorService private readonly editorService: EditorServiceImpl ) { - super(parent, accessor, group, contextMenuService, instantiationService, contextKeyService, keybindingService, telemetryService, notificationService, menuService, quickOpenService, themeService, extensionService, configurationService, fileService, labelService); + super(parent, accessor, group, contextMenuService, instantiationService, contextKeyService, keybindingService, telemetryService, notificationService, menuService, quickOpenService, themeService, extensionService, configurationService, fileService); this.tabResourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, DEFAULT_LABELS_CONTAINER)); this.closeOneEditorAction = this._register(this.instantiationService.createInstance(CloseOneEditorAction, CloseOneEditorAction.ID, CloseOneEditorAction.LABEL)); @@ -392,10 +391,16 @@ export class TabsTitleControl extends TitleControl { this.layout(this.dimension); } + private updateEditorLabelAggregator = this._register(new RunOnceScheduler(() => this.updateEditorLabels(), 0)); + updateEditorLabel(editor: IEditorInput): void { // Update all labels to account for changes to tab labels - this.updateEditorLabels(); + // Since this method may be called a lot of times from + // individual editors, we collect all those requests and + // then run the update once because we have to update + // all opened tabs in the group at once. + this.updateEditorLabelAggregator.schedule(); } updateEditorLabels(): void { diff --git a/src/vs/workbench/browser/parts/editor/titleControl.ts b/src/vs/workbench/browser/parts/editor/titleControl.ts index 1077097edde..e191034f051 100644 --- a/src/vs/workbench/browser/parts/editor/titleControl.ts +++ b/src/vs/workbench/browser/parts/editor/titleControl.ts @@ -40,7 +40,6 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; import { IFileService } from 'vs/platform/files/common/files'; import { withNullAsUndefined, withUndefinedAsNull, assertIsDefined } from 'vs/base/common/types'; -import { ILabelService } from 'vs/platform/label/common/label'; import { isFirefox } from 'vs/base/browser/browser'; export interface IToolbarActions { @@ -82,8 +81,7 @@ export abstract class TitleControl extends Themable { @IThemeService themeService: IThemeService, @IExtensionService private readonly extensionService: IExtensionService, @IConfigurationService protected configurationService: IConfigurationService, - @IFileService private readonly fileService: IFileService, - @ILabelService private readonly labelService: ILabelService + @IFileService private readonly fileService: IFileService ) { super(themeService); @@ -97,8 +95,9 @@ export abstract class TitleControl extends Themable { } private registerListeners(): void { + + // Update actions toolbar when extension register that may contribute them this._register(this.extensionService.onDidRegisterExtensions(() => this.updateEditorActionsToolbar())); - this._register(this.labelService.onDidChangeFormatters(() => this.updateEditorLabels())); } protected abstract create(parent: HTMLElement): void; diff --git a/src/vs/workbench/browser/parts/notifications/notificationsActions.ts b/src/vs/workbench/browser/parts/notifications/notificationsActions.ts index f6e1ee43461..5318fd1ace0 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsActions.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsActions.ts @@ -26,10 +26,8 @@ export class ClearNotificationAction extends Action { super(id, label, 'codicon-close'); } - run(notification: INotificationViewItem): Promise { + async run(notification: INotificationViewItem): Promise { this.commandService.executeCommand(CLEAR_NOTIFICATION, notification); - - return Promise.resolve(); } } @@ -46,10 +44,8 @@ export class ClearAllNotificationsAction extends Action { super(id, label, 'codicon-clear-all'); } - run(notification: INotificationViewItem): Promise { + async run(): Promise { this.commandService.executeCommand(CLEAR_ALL_NOTIFICATIONS); - - return Promise.resolve(); } } @@ -63,13 +59,11 @@ export class HideNotificationsCenterAction extends Action { label: string, @ICommandService private readonly commandService: ICommandService ) { - super(id, label, 'codicon-close'); + super(id, label, 'codicon-chevron-down'); } - run(notification: INotificationViewItem): Promise { + async run(): Promise { this.commandService.executeCommand(HIDE_NOTIFICATIONS_CENTER); - - return Promise.resolve(); } } @@ -86,10 +80,8 @@ export class ExpandNotificationAction extends Action { super(id, label, 'codicon-chevron-up'); } - run(notification: INotificationViewItem): Promise { + async run(notification: INotificationViewItem): Promise { this.commandService.executeCommand(EXPAND_NOTIFICATION, notification); - - return Promise.resolve(); } } @@ -106,10 +98,8 @@ export class CollapseNotificationAction extends Action { super(id, label, 'codicon-chevron-down'); } - run(notification: INotificationViewItem): Promise { + async run(notification: INotificationViewItem): Promise { this.commandService.executeCommand(COLLAPSE_NOTIFICATION, notification); - - return Promise.resolve(); } } @@ -140,7 +130,7 @@ export class CopyNotificationMessageAction extends Action { super(id, label); } - run(notification: INotificationViewItem): Promise { + run(notification: INotificationViewItem): Promise { return this.clipboardService.writeText(notification.message.raw); } } @@ -154,7 +144,7 @@ export class NotificationActionRunner extends ActionRunner { super(); } - protected async runAction(action: IAction, context: INotificationViewItem): Promise { + protected async runAction(action: IAction, context: INotificationViewItem): Promise { this.telemetryService.publicLog2('workbenchActionExecuted', { id: action.id, from: 'message' }); // Run and make sure to notify on any error again diff --git a/src/vs/workbench/browser/parts/notifications/notificationsAlerts.ts b/src/vs/workbench/browser/parts/notifications/notificationsAlerts.ts index 22b683d8c68..86bf82ce942 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsAlerts.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsAlerts.ts @@ -5,7 +5,7 @@ import { alert } from 'vs/base/browser/ui/aria/aria'; import { localize } from 'vs/nls'; -import { INotificationViewItem, INotificationsModel, NotificationChangeType, INotificationChangeEvent, NotificationViewItemLabelKind } from 'vs/workbench/common/notifications'; +import { INotificationViewItem, INotificationsModel, NotificationChangeType, INotificationChangeEvent, NotificationViewItemContentChangeKind } from 'vs/workbench/common/notifications'; import { Disposable } from 'vs/base/common/lifecycle'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Severity } from 'vs/platform/notification/common/notification'; @@ -45,9 +45,9 @@ export class NotificationsAlerts extends Disposable { private triggerAriaAlert(notifiation: INotificationViewItem): void { - // Trigger the alert again whenever the label changes - const listener = notifiation.onDidChangeLabel(e => { - if (e.kind === NotificationViewItemLabelKind.MESSAGE) { + // Trigger the alert again whenever the message changes + const listener = notifiation.onDidChangeContent(e => { + if (e.kind === NotificationViewItemContentChangeKind.MESSAGE) { this.doTriggerAriaAlert(notifiation); } }); diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts index 094c9e1567f..c99f963b2af 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts @@ -7,7 +7,7 @@ import 'vs/css!./media/notificationsCenter'; import 'vs/css!./media/notificationsActions'; import { Themable, NOTIFICATIONS_BORDER, NOTIFICATIONS_CENTER_HEADER_FOREGROUND, NOTIFICATIONS_CENTER_HEADER_BACKGROUND, NOTIFICATIONS_CENTER_BORDER } from 'vs/workbench/common/theme'; import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; -import { INotificationsModel, INotificationChangeEvent, NotificationChangeType } from 'vs/workbench/common/notifications'; +import { INotificationsModel, INotificationChangeEvent, NotificationChangeType, NotificationViewItemContentChangeKind } from 'vs/workbench/common/notifications'; import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; import { Emitter } from 'vs/base/common/event'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -100,6 +100,9 @@ export class NotificationsCenter extends Themable implements INotificationsCente // Theming this.updateStyles(); + // Mark as visible + this.model.notifications.forEach(notification => notification.updateVisibility(true)); + // Context Key this.notificationsCenterVisibleContextKey.set(true); @@ -115,7 +118,7 @@ export class NotificationsCenter extends Themable implements INotificationsCente clearAllAction.enabled = false; } else { notificationsCenterTitle.textContent = localize('notifications', "Notifications"); - clearAllAction.enabled = true; + clearAllAction.enabled = this.model.notifications.some(notification => !notification.hasProgress); } } @@ -172,20 +175,38 @@ export class NotificationsCenter extends Themable implements INotificationsCente return; // only if visible } - let focusGroup = false; + let focusEditor = false; - // Update notifications list based on event + // Update notifications list based on event kind const [notificationsList, notificationsCenterContainer] = assertAllDefined(this.notificationsList, this.notificationsCenterContainer); switch (e.kind) { case NotificationChangeType.ADD: notificationsList.updateNotificationsList(e.index, 0, [e.item]); + e.item.updateVisibility(true); break; case NotificationChangeType.CHANGE: + // Handle content changes + // - actions: re-draw to properly show them + // - message: update notification height unless collapsed + switch (e.detail) { + case NotificationViewItemContentChangeKind.ACTIONS: + notificationsList.updateNotificationsList(e.index, 1, [e.item]); + break; + case NotificationViewItemContentChangeKind.MESSAGE: + if (e.item.expanded) { + notificationsList.updateNotificationHeight(e.item); + } + break; + } + break; + case NotificationChangeType.EXPAND_COLLAPSE: + // Re-draw entire item when expansion changes to reveal or hide details notificationsList.updateNotificationsList(e.index, 1, [e.item]); break; case NotificationChangeType.REMOVE: - focusGroup = isAncestor(document.activeElement, notificationsCenterContainer); + focusEditor = isAncestor(document.activeElement, notificationsCenterContainer); notificationsList.updateNotificationsList(e.index, 1); + e.item.updateVisibility(false); break; } @@ -197,7 +218,7 @@ export class NotificationsCenter extends Themable implements INotificationsCente this.hide(); // Restore focus to editor group if we had focus - if (focusGroup) { + if (focusEditor) { this.editorGroupService.activeGroup.focus(); } } @@ -208,13 +229,16 @@ export class NotificationsCenter extends Themable implements INotificationsCente return; // already hidden } - const focusGroup = isAncestor(document.activeElement, this.notificationsCenterContainer); + const focusEditor = isAncestor(document.activeElement, this.notificationsCenterContainer); // Hide this._isVisible = false; removeClass(this.notificationsCenterContainer, 'visible'); this.notificationsList.hide(); + // Mark as hidden + this.model.notifications.forEach(notification => notification.updateVisibility(false)); + // Context Key this.notificationsCenterVisibleContextKey.set(false); @@ -222,7 +246,7 @@ export class NotificationsCenter extends Themable implements INotificationsCente this._onDidChangeVisibility.fire(); // Restore focus to editor group if we had focus - if (focusGroup) { + if (focusEditor) { this.editorGroupService.activeGroup.focus(); } } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts index 413e700668d..e41c8692e57 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts @@ -75,6 +75,7 @@ export function registerNotificationCommands(center: INotificationsCenterControl // Show Notifications Cneter CommandsRegistry.registerCommand(SHOW_NOTIFICATIONS_CENTER, () => { + toasts.hide(); center.show(); }); @@ -92,6 +93,7 @@ export function registerNotificationCommands(center: INotificationsCenterControl if (center.isVisible) { center.hide(); } else { + toasts.hide(); center.show(); } }); @@ -105,7 +107,7 @@ export function registerNotificationCommands(center: INotificationsCenterControl mac: { primary: KeyMod.CtrlCmd | KeyCode.Backspace }, - handler: (accessor, args?: any) => { + handler: (accessor, args?) => { const notification = getNotificationFromContext(accessor.get(IListService), args); if (notification && !notification.hasProgress) { notification.close(); @@ -119,7 +121,7 @@ export function registerNotificationCommands(center: INotificationsCenterControl weight: KeybindingWeight.WorkbenchContrib, when: NotificationFocusedContext, primary: KeyCode.RightArrow, - handler: (accessor, args?: any) => { + handler: (accessor, args?) => { const notification = getNotificationFromContext(accessor.get(IListService), args); if (notification) { notification.expand(); @@ -133,7 +135,7 @@ export function registerNotificationCommands(center: INotificationsCenterControl weight: KeybindingWeight.WorkbenchContrib, when: NotificationFocusedContext, primary: KeyCode.LeftArrow, - handler: (accessor, args?: any) => { + handler: (accessor, args?) => { const notification = getNotificationFromContext(accessor.get(IListService), args); if (notification) { notification.collapse(); diff --git a/src/vs/workbench/browser/parts/notifications/notificationsList.ts b/src/vs/workbench/browser/parts/notifications/notificationsList.ts index 2d585698cb9..64085073bb4 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsList.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsList.ts @@ -21,6 +21,7 @@ import { assertIsDefined, assertAllDefined } from 'vs/base/common/types'; export class NotificationsList extends Themable { private listContainer: HTMLElement | undefined; private list: WorkbenchList | undefined; + private listDelegate: NotificationsListDelegate | undefined; private viewModel: INotificationViewItem[]; private isVisible: boolean | undefined; @@ -73,11 +74,12 @@ export class NotificationsList extends Themable { const renderer = this.instantiationService.createInstance(NotificationRenderer, actionRunner); // List + const listDelegate = this.listDelegate = new NotificationsListDelegate(this.listContainer); const list = this.list = >this._register(this.instantiationService.createInstance( WorkbenchList, 'NotificationsList', this.listContainer, - new NotificationsListDelegate(this.listContainer), + listDelegate, [renderer], { ...this.options, @@ -186,6 +188,17 @@ export class NotificationsList extends Themable { } } + updateNotificationHeight(item: INotificationViewItem): void { + const index = this.viewModel.indexOf(item); + if (index === -1) { + return; + } + + const [list, listDelegate] = assertAllDefined(this.list, this.listDelegate); + list.updateElementHeight(index, listDelegate.getHeight(item)); + list.layout(); + } + hide(): void { if (!this.isVisible || !this.list) { return; // already hidden diff --git a/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts b/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts index 71a82ef3aae..cf45117e80c 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts @@ -46,7 +46,7 @@ export class NotificationsStatus extends Disposable { if (!this.isNotificationsCenterVisible) { if (e.kind === NotificationChangeType.ADD) { this.newNotificationsCount++; - } else if (e.kind === NotificationChangeType.REMOVE) { + } else if (e.kind === NotificationChangeType.REMOVE && this.newNotificationsCount > 0) { this.newNotificationsCount--; } } @@ -69,8 +69,9 @@ export class NotificationsStatus extends Disposable { } } + // Show the bell with a dot if there are unread or in-progress notifications const statusProperties: IStatusbarEntry = { - text: `${this.newNotificationsCount === 0 ? '$(bell)' : '$(bell-dot)'}${notificationsInProgress > 0 ? ' $(sync~spin)' : ''}`, + text: `${notificationsInProgress > 0 || this.newNotificationsCount > 0 ? '$(bell-dot)' : '$(bell)'}`, command: this.isNotificationsCenterVisible ? HIDE_NOTIFICATIONS_CENTER : SHOW_NOTIFICATIONS_CENTER, tooltip: this.getTooltip(notificationsInProgress), showBeak: this.isNotificationsCenterVisible diff --git a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts index 95e287fbd49..090488b31b0 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsToasts.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/notificationsToasts'; -import { INotificationsModel, NotificationChangeType, INotificationChangeEvent, INotificationViewItem, NotificationViewItemLabelKind } from 'vs/workbench/common/notifications'; +import { INotificationsModel, NotificationChangeType, INotificationChangeEvent, INotificationViewItem, NotificationViewItemContentChangeKind } from 'vs/workbench/common/notifications'; import { IDisposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { addClass, removeClass, isAncestor, addDisposableListener, EventType, Dimension } from 'vs/base/browser/dom'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -179,11 +179,8 @@ export class NotificationsToasts extends Themable implements INotificationsToast const toast: INotificationToast = { item, list: notificationList, container: notificationToastContainer, toast: notificationToast, toDispose: itemDisposables }; this.mapNotificationToToast.set(item, toast); - itemDisposables.add(toDisposable(() => { - if (this.isToastVisible(toast) && notificationsToastsContainer) { - notificationsToastsContainer.removeChild(toast.container); - } - })); + // When disposed, remove as visible + itemDisposables.add(toDisposable(() => this.updateToastVisibility(toast, false))); // Make visible notificationList.show(); @@ -199,19 +196,24 @@ export class NotificationsToasts extends Themable implements INotificationsToast // the height computation takes the content of it into account! this.layoutContainer(maxDimensions.height); - // Update when item height changes due to expansion + // Re-draw entire item when expansion changes to reveal or hide details itemDisposables.add(item.onDidChangeExpansion(() => { notificationList.updateNotificationsList(0, 1, [item]); })); - // Update when item height potentially changes due to label changes - itemDisposables.add(item.onDidChangeLabel(e => { - if (!item.expanded) { - return; // dynamic height only applies to expanded notifications - } - - if (e.kind === NotificationViewItemLabelKind.ACTIONS || e.kind === NotificationViewItemLabelKind.MESSAGE) { - notificationList.updateNotificationsList(0, 1, [item]); + // Handle content changes + // - actions: re-draw to properly show them + // - message: update notification height unless collapsed + itemDisposables.add(item.onDidChangeContent(e => { + switch (e.kind) { + case NotificationViewItemContentChangeKind.ACTIONS: + notificationList.updateNotificationsList(0, 1, [item]); + break; + case NotificationViewItemContentChangeKind.MESSAGE: + if (item.expanded) { + notificationList.updateNotificationHeight(item); + } + break; } })); @@ -236,6 +238,9 @@ export class NotificationsToasts extends Themable implements INotificationsToast addClass(notificationToast, 'notification-fade-in-done'); })); + // Mark as visible + item.updateVisibility(true); + // Events if (!this._isVisible) { this._isVisible = true; @@ -292,12 +297,13 @@ export class NotificationsToasts extends Themable implements INotificationsToast } private removeToast(item: INotificationViewItem): void { + let focusEditor = false; + const notificationToast = this.mapNotificationToToast.get(item); - let focusGroup = false; if (notificationToast) { const toastHasDOMFocus = isAncestor(document.activeElement, notificationToast.container); if (toastHasDOMFocus) { - focusGroup = !(this.focusNext() || this.focusPrevious()); // focus next if any, otherwise focus editor + focusEditor = !(this.focusNext() || this.focusPrevious()); // focus next if any, otherwise focus editor } // Listeners @@ -317,7 +323,7 @@ export class NotificationsToasts extends Themable implements INotificationsToast this.doHide(); // Move focus back to editor group as needed - if (focusGroup) { + if (focusEditor) { this.editorGroupService.activeGroup.focus(); } } @@ -346,11 +352,11 @@ export class NotificationsToasts extends Themable implements INotificationsToast } hide(): void { - const focusGroup = this.notificationsToastsContainer ? isAncestor(document.activeElement, this.notificationsToastsContainer) : false; + const focusEditor = this.notificationsToastsContainer ? isAncestor(document.activeElement, this.notificationsToastsContainer) : false; this.removeToasts(); - if (focusGroup) { + if (focusEditor) { this.editorGroupService.activeGroup.focus(); } } @@ -459,12 +465,12 @@ export class NotificationsToasts extends Themable implements INotificationsToast notificationToasts.push(toast); break; case ToastVisibility.HIDDEN: - if (!this.isToastVisible(toast)) { + if (!this.isToastInDOM(toast)) { notificationToasts.push(toast); } break; case ToastVisibility.VISIBLE: - if (this.isToastVisible(toast)) { + if (this.isToastInDOM(toast)) { notificationToasts.push(toast); } break; @@ -530,7 +536,7 @@ export class NotificationsToasts extends Themable implements INotificationsToast // In order to measure the client height, the element cannot have display: none toast.container.style.opacity = '0'; - this.setVisibility(toast, true); + this.updateToastVisibility(toast, true); heightToGive -= toast.container.offsetHeight; @@ -542,7 +548,7 @@ export class NotificationsToasts extends Themable implements INotificationsToast } // Hide or show toast based on context - this.setVisibility(toast, makeVisible); + this.updateToastVisibility(toast, makeVisible); toast.container.style.opacity = ''; if (makeVisible) { @@ -551,20 +557,24 @@ export class NotificationsToasts extends Themable implements INotificationsToast }); } - private setVisibility(toast: INotificationToast, visible: boolean): void { - if (this.isToastVisible(toast) === visible) { + private updateToastVisibility(toast: INotificationToast, visible: boolean): void { + if (this.isToastInDOM(toast) === visible) { return; } + // Update visibility in DOM const notificationsToastsContainer = assertIsDefined(this.notificationsToastsContainer); if (visible) { notificationsToastsContainer.appendChild(toast.container); } else { notificationsToastsContainer.removeChild(toast.container); } + + // Update visibility in model + toast.item.updateVisibility(visible); } - private isToastVisible(toast: INotificationToast): boolean { + private isToastInDOM(toast: INotificationToast): boolean { return !!toast.container.parentElement; } } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts index 41063188c35..19cd1f14f2b 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts @@ -17,7 +17,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { dispose, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdown'; -import { INotificationViewItem, NotificationViewItem, NotificationViewItemLabelKind, INotificationMessage, ChoiceAction } from 'vs/workbench/common/notifications'; +import { INotificationViewItem, NotificationViewItem, NotificationViewItemContentChangeKind, INotificationMessage, ChoiceAction } from 'vs/workbench/common/notifications'; import { ClearNotificationAction, ExpandNotificationAction, CollapseNotificationAction, ConfigureNotificationAction } from 'vs/workbench/browser/parts/notifications/notificationsActions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; @@ -46,14 +46,13 @@ export class NotificationsListDelegate implements IListVirtualDelegate { + // Label Change Events that we can handle directly + // (changes to actions require an entire redraw of + // the notification because it has an impact on + // epxansion state) + this.inputDisposables.add(notification.onDidChangeContent(event => { switch (event.kind) { - case NotificationViewItemLabelKind.SEVERITY: + case NotificationViewItemContentChangeKind.SEVERITY: this.renderSeverity(notification); break; - case NotificationViewItemLabelKind.PROGRESS: + case NotificationViewItemContentChangeKind.PROGRESS: this.renderProgress(notification); break; - case NotificationViewItemLabelKind.MESSAGE: + case NotificationViewItemContentChangeKind.MESSAGE: this.renderMessage(notification); break; } diff --git a/src/vs/workbench/browser/parts/panel/media/panelpart.css b/src/vs/workbench/browser/parts/panel/media/panelpart.css index abca35a41a7..a5b9004ca72 100644 --- a/src/vs/workbench/browser/parts/panel/media/panelpart.css +++ b/src/vs/workbench/browser/parts/panel/media/panelpart.css @@ -62,6 +62,10 @@ } +.monaco-workbench .part.panel > .composite.title > .composite-bar-excess { + width: 100px; +} + .monaco-workbench .part.panel > .title > .panel-switcher-container > .monaco-action-bar { line-height: 27px; /* matches panel titles in settings */ height: 35px; diff --git a/src/vs/workbench/browser/parts/panel/panelActions.ts b/src/vs/workbench/browser/parts/panel/panelActions.ts index 8c3117a0b12..7cb981e9583 100644 --- a/src/vs/workbench/browser/parts/panel/panelActions.ts +++ b/src/vs/workbench/browser/parts/panel/panelActions.ts @@ -17,7 +17,7 @@ import { ActivityAction, ToggleCompositePinnedAction, ICompositeBar } from 'vs/w 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'; +import { ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; export class ClosePanelAction extends Action { @@ -32,9 +32,8 @@ export class ClosePanelAction extends Action { super(id, name, 'codicon-close'); } - run(): Promise { + async run(): Promise { this.layoutService.setPanelHidden(true); - return Promise.resolve(); } } @@ -51,9 +50,8 @@ export class TogglePanelAction extends Action { super(id, name, layoutService.isVisible(Parts.PANEL_PART) ? 'panel expanded' : 'panel'); } - run(): Promise { + async run(): Promise { this.layoutService.setPanelHidden(this.layoutService.isVisible(Parts.PANEL_PART)); - return Promise.resolve(); } } @@ -71,12 +69,12 @@ class FocusPanelAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { // Show panel if (!this.layoutService.isVisible(Parts.PANEL_PART)) { this.layoutService.setPanelHidden(false); - return Promise.resolve(); + return; } // Focus into active panel @@ -84,8 +82,6 @@ class FocusPanelAction extends Action { if (panel) { panel.focus(); } - - return Promise.resolve(); } } @@ -115,13 +111,12 @@ export class ToggleMaximizedPanelAction extends Action { })); } - run(): Promise { + async run(): Promise { if (!this.layoutService.isVisible(Parts.PANEL_PART)) { this.layoutService.setPanelHidden(false); } this.layoutService.toggleMaximizedPanel(); - return Promise.resolve(); } } @@ -133,7 +128,7 @@ const PositionPanelActionId = { interface PanelActionConfig { id: string; - when: ContextKeyExpr; + when: ContextKeyExpression; alias: string; label: string; value: T; @@ -166,10 +161,9 @@ export class SetPanelPositionAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { const position = positionByActionId.get(this.id); this.layoutService.setPanelPosition(position === undefined ? Position.BOTTOM : position); - return Promise.resolve(); } } @@ -182,7 +176,7 @@ export class PanelActivityAction extends ActivityAction { super(activity); } - async run(event: any): Promise { + async run(): Promise { await this.panelService.openPanel(this.activity.id, true); this.activate(); } @@ -224,7 +218,7 @@ export class SwitchPanelViewAction extends Action { super(id, name); } - async run(offset: number): Promise { + async run(offset: number): Promise { const pinnedPanels = this.panelService.getPinnedPanels(); const activePanel = this.panelService.getActivePanel(); if (!activePanel) { @@ -256,7 +250,7 @@ export class PreviousPanelViewAction extends SwitchPanelViewAction { super(id, name, panelService); } - run(): Promise { + run(): Promise { return super.run(-1); } } @@ -274,7 +268,7 @@ export class NextPanelViewAction extends SwitchPanelViewAction { super(id, name, panelService); } - run(): Promise { + run(): Promise { return super.run(1); } } diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index e8126ec13e1..a73f42b53f0 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -36,6 +36,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { ViewContainer, IViewContainersRegistry, Extensions as ViewContainerExtensions, IViewDescriptorService, IViewDescriptorCollection, ViewContainerLocation } from 'vs/workbench/common/views'; import { MenuId } from 'vs/platform/actions/common/actions'; import { ViewMenuActions } from 'vs/workbench/browser/parts/views/viewMenuActions'; +import { IPaneComposite } from 'vs/workbench/common/panecomposite'; interface ICachedPanel { id: string; @@ -143,7 +144,7 @@ export class PanelPart extends CompositePart implements IPanelService { getDefaultCompositeId: () => this.panelRegistry.getDefaultPanelId(), hidePart: () => this.layoutService.setPanelHidden(true), dndHandler: new CompositeDragAndDrop(this.viewDescriptorService, ViewContainerLocation.Panel, - (id: string, focus?: boolean) => this.openPanel(id, focus), + (id: string, focus?: boolean) => this.openPanel(id, focus) as Promise, (from: string, to: string) => this.compositeBar.move(from, to), () => this.getPinnedPanels().map(p => p.id) ), diff --git a/src/vs/workbench/browser/parts/quickopen/quickopen.ts b/src/vs/workbench/browser/parts/quickopen/quickopen.ts index a1e64954456..2e79e602673 100644 --- a/src/vs/workbench/browser/parts/quickopen/quickopen.ts +++ b/src/vs/workbench/browser/parts/quickopen/quickopen.ts @@ -60,14 +60,12 @@ export class BaseQuickOpenNavigateAction extends Action { super(id, label); } - run(event?: any): Promise { + async run(): Promise { const keys = this.keybindingService.lookupKeybindings(this.id); const quickNavigate = this.quickNavigate ? { keybindings: keys } : undefined; this.quickOpenService.navigate(this.next, quickNavigate); this.quickInputService.navigate(this.next, quickNavigate); - - return Promise.resolve(true); } } diff --git a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts index 02a5769f116..76e0dcce519 100644 --- a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts +++ b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts @@ -328,11 +328,12 @@ class FocusSideBarAction extends Action { super(id, label); } - run(): Promise { + async run(): Promise { // Show side bar if (!this.layoutService.isVisible(Parts.SIDEBAR_PART)) { - return Promise.resolve(this.layoutService.setSideBarHidden(false)); + this.layoutService.setSideBarHidden(false); + return; } // Focus into active viewlet @@ -340,8 +341,6 @@ class FocusSideBarAction extends Action { if (viewlet) { viewlet.focus(); } - - return Promise.resolve(true); } } diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index 9ec20f5c9c0..688534ce6fe 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -303,14 +303,12 @@ class ToggleStatusbarEntryVisibilityAction extends Action { this.checked = !model.isHidden(id); } - run(): Promise { + async run(): Promise { if (this.model.isHidden(this.id)) { this.model.show(this.id); } else { this.model.hide(this.id); } - - return Promise.resolve(true); } } @@ -320,10 +318,8 @@ class HideStatusbarEntryAction extends Action { super(id, nls.localize('hide', "Hide '{0}'", name), undefined, true); } - run(): Promise { + async run(): Promise { this.model.hide(this.id); - - return Promise.resolve(true); } } diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index f23aacde162..13dcddb9786 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -86,7 +86,7 @@ export class TitlebarPart extends Part implements ITitleService { private readonly properties: ITitleProperties = { isPure: true, isAdmin: false }; private readonly activeEditorListeners = this._register(new DisposableStore()); - private titleUpdater: RunOnceScheduler = this._register(new RunOnceScheduler(() => this.doUpdateTitle(), 0)); + private readonly titleUpdater = this._register(new RunOnceScheduler(() => this.doUpdateTitle(), 0)); private contextMenu: IMenu; diff --git a/src/vs/workbench/browser/parts/views/customView.ts b/src/vs/workbench/browser/parts/views/customView.ts index e4cfff2a63c..666a9e25f3f 100644 --- a/src/vs/workbench/browser/parts/views/customView.ts +++ b/src/vs/workbench/browser/parts/views/customView.ts @@ -881,7 +881,7 @@ class MultipleSelectionActionRunner extends ActionRunner { })); } - runAction(action: IAction, context: TreeViewItemHandleArg): Promise { + runAction(action: IAction, context: TreeViewItemHandleArg): Promise { const selection = this.getSelectedResources(); let selectionHandleArgs: TreeViewItemHandleArg[] | undefined = undefined; let actionInSelected: boolean = false; diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index 90554ca9351..21e312a5084 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -255,7 +255,10 @@ export abstract class ViewPane extends Pane implements IView { this._onDidFocus.fire(); })); this._register(focusTracker.onDidBlur(() => { - this.focusedViewContextKey.reset(); + if (this.focusedViewContextKey.get() === this.id) { + this.focusedViewContextKey.reset(); + } + this._onDidBlur.fire(); })); } @@ -1006,7 +1009,8 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { } if (!this.areExtensionsReady) { if (this.visibleViewsCountFromCache === undefined) { - return true; + // TODO @sbatten fix hack for #91367 + return this.viewDescriptorService.getViewContainerLocation(this.viewContainer) === ViewContainerLocation.Panel; } // Check in cache so that view do not jump. See #29609 return this.visibleViewsCountFromCache === 1; diff --git a/src/vs/workbench/browser/parts/views/views.ts b/src/vs/workbench/browser/parts/views/views.ts index 232a343d488..5480d538e6a 100644 --- a/src/vs/workbench/browser/parts/views/views.ts +++ b/src/vs/workbench/browser/parts/views/views.ts @@ -559,7 +559,7 @@ export class ViewsService extends Disposable implements IViewsService { } }); } - run(accessor: ServicesAccessor): any { + run(accessor: ServicesAccessor): void { accessor.get(IViewsService).openView(viewDescriptor.id, true); } })); @@ -589,7 +589,7 @@ export class ViewsService extends Disposable implements IViewsService { }], }); } - run(accessor: ServicesAccessor): any { + run(accessor: ServicesAccessor): void { accessor.get(IViewDescriptorService).moveViewToLocation(viewDescriptor, newLocation); accessor.get(IViewsService).openView(viewDescriptor.id, true); } diff --git a/src/vs/workbench/browser/viewlet.ts b/src/vs/workbench/browser/viewlet.ts index ddcb76a0a95..096974a38ae 100644 --- a/src/vs/workbench/browser/viewlet.ts +++ b/src/vs/workbench/browser/viewlet.ts @@ -165,17 +165,16 @@ export class ShowViewletAction extends Action { this.enabled = !!this.viewletService && !!this.editorGroupService; } - run(): Promise { + async run(): Promise { // Pass focus to viewlet if not open or focused if (this.otherViewletShowing() || !this.sidebarHasFocus()) { - return this.viewletService.openViewlet(this.viewletId, true); + await this.viewletService.openViewlet(this.viewletId, true); + return; } // Otherwise pass focus to editor group this.editorGroupService.activeGroup.focus(); - - return Promise.resolve(true); } private otherViewletShowing(): boolean { diff --git a/src/vs/workbench/common/actions.ts b/src/vs/workbench/common/actions.ts index 7f5a5cbdd14..0d75f14187f 100644 --- a/src/vs/workbench/common/actions.ts +++ b/src/vs/workbench/common/actions.ts @@ -11,7 +11,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; export const Extensions = { WorkbenchActions: 'workbench.contributions.actions' @@ -28,11 +28,11 @@ export interface IWorkbenchActionRegistry { Registry.add(Extensions.WorkbenchActions, new class implements IWorkbenchActionRegistry { - registerWorkbenchAction(descriptor: SyncActionDescriptor, alias: string, category?: string, when?: ContextKeyExpr): IDisposable { + registerWorkbenchAction(descriptor: SyncActionDescriptor, alias: string, category?: string, when?: ContextKeyExpression): IDisposable { return this.registerWorkbenchCommandFromAction(descriptor, alias, category, when); } - private registerWorkbenchCommandFromAction(descriptor: SyncActionDescriptor, alias: string, category?: string, when?: ContextKeyExpr): IDisposable { + private registerWorkbenchCommandFromAction(descriptor: SyncActionDescriptor, alias: string, category?: string, when?: ContextKeyExpression): IDisposable { const registrations = new DisposableStore(); // command @@ -98,7 +98,7 @@ Registry.add(Extensions.WorkbenchActions, new class implements IWorkbenchActionR }; } - private async triggerAndDisposeAction(instantiationService: IInstantiationService, lifecycleService: ILifecycleService, descriptor: SyncActionDescriptor, args: any): Promise { + private async triggerAndDisposeAction(instantiationService: IInstantiationService, lifecycleService: ILifecycleService, descriptor: SyncActionDescriptor, args: unknown): Promise { // run action when workbench is created await lifecycleService.when(LifecyclePhase.Ready); @@ -115,7 +115,7 @@ Registry.add(Extensions.WorkbenchActions, new class implements IWorkbenchActionR // otherwise run and dispose try { - const from = args?.from || 'keybinding'; + const from = (args as any)?.from || 'keybinding'; await actionInstance.run(undefined, { from }); } finally { actionInstance.dispose(); diff --git a/src/vs/workbench/common/contributions.ts b/src/vs/workbench/common/contributions.ts index 1817e91d841..7d2a421a910 100644 --- a/src/vs/workbench/common/contributions.ts +++ b/src/vs/workbench/common/contributions.ts @@ -38,12 +38,14 @@ export interface IWorkbenchContributionsRegistry { } class WorkbenchContributionsRegistry implements IWorkbenchContributionsRegistry { + private instantiationService: IInstantiationService | undefined; private lifecycleService: ILifecycleService | undefined; - private readonly toBeInstantiated: Map[]> = new Map[]>(); + private readonly toBeInstantiated = new Map[]>(); registerWorkbenchContribution(ctor: new (...services: Services) => IWorkbenchContribution, phase: LifecyclePhase = LifecyclePhase.Starting): void { + // Instantiate directly if we are already matching the provided phase if (this.instantiationService && this.lifecycleService && this.lifecycleService.phase >= phase) { this.instantiationService.createInstance(ctor); diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index b63812fdc96..68cb88a96c7 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -12,7 +12,7 @@ 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, ITextEditorSelection, TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; import { IInstantiationService, IConstructorSignature0, ServicesAccessor, BrandedService } from 'vs/platform/instantiation/common/instantiation'; -import { RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { Registry } from 'vs/platform/registry/common/platform'; import { ITextModel } from 'vs/editor/common/model'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -38,7 +38,7 @@ export const EditorsVisibleContext = new RawContextKey('editorIsOpen', export const EditorPinnedContext = new RawContextKey('editorPinned', false); export const EditorGroupActiveEditorDirtyContext = new RawContextKey('groupActiveEditorDirty', false); export const EditorGroupEditorsCountContext = new RawContextKey('groupEditorsCount', 0); -export const NoEditorsVisibleContext: ContextKeyExpr = EditorsVisibleContext.toNegated(); +export const NoEditorsVisibleContext = EditorsVisibleContext.toNegated(); export const TextCompareEditorVisibleContext = new RawContextKey('textCompareEditorVisible', false); export const TextCompareEditorActiveContext = new RawContextKey('textCompareEditorActive', false); export const ActiveEditorGroupEmptyContext = new RawContextKey('activeEditorGroupEmpty', false); @@ -66,17 +66,12 @@ export interface IEditor extends IPanel { /** * The assigned input of this editor. */ - input: IEditorInput | undefined; - - /** - * The assigned options of this editor. - */ - options: IEditorOptions | undefined; + readonly input: IEditorInput | undefined; /** * The assigned group this editor is showing in. */ - group: IEditorGroup | undefined; + readonly group: IEditorGroup | undefined; /** * The minimum width of this editor. @@ -114,6 +109,14 @@ export interface IEditor extends IPanel { isVisible(): boolean; } +/** + * Overrides `IEditor` where `input` and `group` are known to be set. + */ +export interface IVisibleEditor extends IEditor { + readonly input: IEditorInput; + readonly group: IEditorGroup; +} + export interface ITextEditor extends IEditor { /** @@ -449,7 +452,7 @@ export interface IEditorInput extends IDisposable { /** * Reverts this input from the provided group. */ - revert(group: GroupIdentifier, options?: IRevertOptions): Promise; + revert(group: GroupIdentifier, options?: IRevertOptions): Promise; /** * Called to determine how to handle a resource that is moved that matches @@ -557,9 +560,7 @@ export abstract class EditorInput extends Disposable implements IEditorInput { return this; } - async revert(group: GroupIdentifier, options?: IRevertOptions): Promise { - return true; - } + async revert(group: GroupIdentifier, options?: IRevertOptions): Promise { } move(group: GroupIdentifier, target: URI): IMoveResult | undefined { return undefined; @@ -611,8 +612,20 @@ export abstract class TextResourceEditorInput extends EditorInput { protected registerListeners(): void { // Clear label memoizer on certain events that have impact - this._register(this.labelService.onDidChangeFormatters(() => TextResourceEditorInput.MEMOIZER.clear())); - this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(() => TextResourceEditorInput.MEMOIZER.clear())); + this._register(this.labelService.onDidChangeFormatters(e => this.onLabelEvent(e.scheme))); + this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onLabelEvent(e.scheme))); + this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onLabelEvent(e.scheme))); + } + + private onLabelEvent(scheme: string): void { + if (scheme === this.resource.scheme) { + + // Clear any cached labels from before + TextResourceEditorInput.MEMOIZER.clear(); + + // Trigger recompute of label + this._onDidChangeLabel.fire(); + } } getName(): string { @@ -687,10 +700,6 @@ export abstract class TextResourceEditorInput extends EditorInput { return false; // untitled is never readonly } - if (!this.fileService.canHandleResource(this.resource)) { - return true; // resources without file support are always readonly - } - return this.fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly); } @@ -735,8 +744,8 @@ export abstract class TextResourceEditorInput extends EditorInput { return this; } - revert(group: GroupIdentifier, options?: IRevertOptions): Promise { - return this.textFileService.revert(this.resource, options); + async revert(group: GroupIdentifier, options?: IRevertOptions): Promise { + await this.textFileService.revert(this.resource, options); } } @@ -876,7 +885,7 @@ export class SideBySideEditorInput extends EditorInput { return this.master.saveAs(group, options); } - revert(group: GroupIdentifier, options?: IRevertOptions): Promise { + revert(group: GroupIdentifier, options?: IRevertOptions): Promise { return this.master.revert(group, options); } diff --git a/src/vs/workbench/common/memento.ts b/src/vs/workbench/common/memento.ts index 795b6ac8d90..6102751b8f5 100644 --- a/src/vs/workbench/common/memento.ts +++ b/src/vs/workbench/common/memento.ts @@ -87,4 +87,4 @@ class ScopedMemento { this.storageService.remove(this.id, this.scope); } } -} \ No newline at end of file +} diff --git a/src/vs/workbench/common/notifications.ts b/src/vs/workbench/common/notifications.ts index 353baa3bd6e..c5d3cfad158 100644 --- a/src/vs/workbench/common/notifications.ts +++ b/src/vs/workbench/common/notifications.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { INotification, INotificationHandle, INotificationActions, INotificationProgress, NoOpNotification, Severity, NotificationMessage, IPromptChoice, IStatusMessageOptions, NotificationsFilter } from 'vs/platform/notification/common/notification'; +import { INotification, INotificationHandle, INotificationActions, INotificationProgress, NoOpNotification, Severity, NotificationMessage, IPromptChoice, IStatusMessageOptions, NotificationsFilter, INotificationProgressProperties } from 'vs/platform/notification/common/notification'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Event, Emitter } from 'vs/base/common/event'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -41,8 +41,26 @@ export interface INotificationsModel { } export const enum NotificationChangeType { + + /** + * A notification was added. + */ ADD, + + /** + * A notification changed. Check `detail` property + * on the event for additional information. + */ CHANGE, + + /** + * A notification expanded or collapsed. + */ + EXPAND_COLLAPSE, + + /** + * A notification was removed. + */ REMOVE } @@ -62,6 +80,12 @@ export interface INotificationChangeEvent { * The kind of notification change. */ kind: NotificationChangeType; + + /** + * Additional detail about the item change. Only applies to + * `NotificationChangeType.CHANGE`. + */ + detail?: NotificationViewItemContentChangeKind } export const enum StatusMessageChangeType { @@ -92,6 +116,9 @@ export class NotificationHandle extends Disposable implements INotificationHandl private readonly _onDidClose = this._register(new Emitter()); readonly onDidClose = this._onDidClose.event; + private readonly _onDidChangeVisibility = this._register(new Emitter()); + readonly onDidChangeVisibility = this._onDidChangeVisibility.event; + constructor(private readonly item: INotificationViewItem, private readonly onClose: (item: INotificationViewItem) => void) { super(); @@ -99,6 +126,11 @@ export class NotificationHandle extends Disposable implements INotificationHandl } private registerListeners(): void { + + // Visibility + this._register(this.item.onDidChangeVisibility(visible => this._onDidChangeVisibility.fire(visible))); + + // Closing Event.once(this.item.onDidClose)(() => { this._onDidClose.fire(); @@ -198,26 +230,19 @@ export class NotificationsModel extends Disposable implements INotificationsMode } // Item Events - const onItemChangeEvent = () => { + const fireNotificationChangeEvent = (kind: NotificationChangeType, detail?: NotificationViewItemContentChangeKind) => { const index = this._notifications.indexOf(item); if (index >= 0) { - this._onDidChangeNotification.fire({ item, index, kind: NotificationChangeType.CHANGE }); + this._onDidChangeNotification.fire({ item, index, kind, detail }); } }; - const itemExpansionChangeListener = item.onDidChangeExpansion(() => onItemChangeEvent()); - - const itemLabelChangeListener = item.onDidChangeLabel(e => { - // a label change in the area of actions or the message is a change that potentially has an impact - // on the size of the notification and as such we emit a change event so that viewers can redraw - if (e.kind === NotificationViewItemLabelKind.ACTIONS || e.kind === NotificationViewItemLabelKind.MESSAGE) { - onItemChangeEvent(); - } - }); + const itemExpansionChangeListener = item.onDidChangeExpansion(() => fireNotificationChangeEvent(NotificationChangeType.EXPAND_COLLAPSE)); + const itemContentChangeListener = item.onDidChangeContent(e => fireNotificationChangeEvent(NotificationChangeType.CHANGE, e.kind)); Event.once(item.onDidClose)(() => { itemExpansionChangeListener.dispose(); - itemLabelChangeListener.dispose(); + itemContentChangeListener.dispose(); const index = this._notifications.indexOf(item); if (index >= 0) { @@ -264,8 +289,9 @@ export interface INotificationViewItem { readonly hasProgress: boolean; readonly onDidChangeExpansion: Event; + readonly onDidChangeVisibility: Event; + readonly onDidChangeContent: Event; readonly onDidClose: Event; - readonly onDidChangeLabel: Event; expand(): void; collapse(skipEvents?: boolean): void; @@ -275,6 +301,8 @@ export interface INotificationViewItem { updateMessage(message: NotificationMessage): void; updateActions(actions?: INotificationActions): void; + updateVisibility(visible: boolean): void; + close(): void; equals(item: INotificationViewItem): boolean; @@ -284,15 +312,15 @@ export function isNotificationViewItem(obj: unknown): obj is INotificationViewIt return obj instanceof NotificationViewItem; } -export const enum NotificationViewItemLabelKind { +export const enum NotificationViewItemContentChangeKind { SEVERITY, MESSAGE, ACTIONS, PROGRESS } -export interface INotificationViewItemLabelChangeEvent { - kind: NotificationViewItemLabelKind; +export interface INotificationViewItemContentChangeEvent { + kind: NotificationViewItemContentChangeKind; } export interface INotificationViewItemProgressState { @@ -398,6 +426,7 @@ export class NotificationViewItem extends Disposable implements INotificationVie private static readonly MAX_MESSAGE_LENGTH = 1000; private _expanded: boolean | undefined; + private _visible: boolean = false; private _actions: INotificationActions | undefined; private _progress: NotificationViewItemProgress | undefined; @@ -408,8 +437,11 @@ export class NotificationViewItem extends Disposable implements INotificationVie private readonly _onDidClose = this._register(new Emitter()); readonly onDidClose = this._onDidClose.event; - private readonly _onDidChangeLabel = this._register(new Emitter()); - readonly onDidChangeLabel = this._onDidChangeLabel.event; + private readonly _onDidChangeContent = this._register(new Emitter()); + readonly onDidChangeContent = this._onDidChangeContent.event; + + private readonly _onDidChangeVisibility = this._register(new Emitter()); + readonly onDidChangeVisibility = this._onDidChangeVisibility.event; static create(notification: INotification, filter: NotificationsFilter = NotificationsFilter.OFF): INotificationViewItem | undefined { if (!notification || !notification.message || isPromiseCanceledError(notification.message)) { @@ -435,7 +467,7 @@ export class NotificationViewItem extends Disposable implements INotificationVie actions = { primary: notification.message.actions }; } - return new NotificationViewItem(severity, notification.sticky, notification.silent || filter === NotificationsFilter.SILENT || (filter === NotificationsFilter.ERROR && notification.severity !== Severity.Error), message, notification.source, actions); + return new NotificationViewItem(severity, notification.sticky, notification.silent || filter === NotificationsFilter.SILENT || (filter === NotificationsFilter.ERROR && notification.severity !== Severity.Error), message, notification.source, notification.progress, actions); } private static parseNotificationMessage(input: NotificationMessage): INotificationMessage | undefined { @@ -472,28 +504,41 @@ export class NotificationViewItem extends Disposable implements INotificationVie private _silent: boolean | undefined, private _message: INotificationMessage, private _source: string | undefined, + progress: INotificationProgressProperties | undefined, actions?: INotificationActions ) { super(); + if (progress) { + this.setProgress(progress); + } + this.setActions(actions); } + private setProgress(progress: INotificationProgressProperties): void { + if (progress.infinite) { + this.progress.infinite(); + } else if (progress.total) { + this.progress.total(progress.total); + + if (progress.worked) { + this.progress.worked(progress.worked); + } + } + } + private setActions(actions: INotificationActions = { primary: [], secondary: [] }): void { - if (!Array.isArray(actions.primary)) { - actions.primary = []; - } + this._actions = { + primary: Array.isArray(actions.primary) ? actions.primary : [], + secondary: Array.isArray(actions.secondary) ? actions.secondary : [] + }; - if (!Array.isArray(actions.secondary)) { - actions.secondary = []; - } - - this._actions = actions; - this._expanded = actions.primary.length > 0; + this._expanded = actions.primary && actions.primary.length > 0; } get canCollapse(): boolean { - return !this.hasPrompt; + return !this.hasActions; } get expanded(): boolean { @@ -509,11 +554,11 @@ export class NotificationViewItem extends Disposable implements INotificationVie return true; // explicitly sticky } - const hasPrompt = this.hasPrompt; + const hasActions = this.hasActions; if ( - (hasPrompt && this._severity === Severity.Error) || // notification errors with actions are sticky - (!hasPrompt && this._expanded) || // notifications that got expanded are sticky - (this._progress && !this._progress.state.done) // notifications with running progress are sticky + (hasActions && this._severity === Severity.Error) || // notification errors with actions are sticky + (!hasActions && this._expanded) || // notifications that got expanded are sticky + (this._progress && !this._progress.state.done) // notifications with running progress are sticky ) { return true; } @@ -525,7 +570,7 @@ export class NotificationViewItem extends Disposable implements INotificationVie return !!this._silent; } - private get hasPrompt(): boolean { + private get hasActions(): boolean { if (!this._actions) { return false; } @@ -544,7 +589,7 @@ export class NotificationViewItem extends Disposable implements INotificationVie get progress(): INotificationViewItemProgress { if (!this._progress) { this._progress = this._register(new NotificationViewItemProgress()); - this._register(this._progress.onDidChange(() => this._onDidChangeLabel.fire({ kind: NotificationViewItemLabelKind.PROGRESS }))); + this._register(this._progress.onDidChange(() => this._onDidChangeContent.fire({ kind: NotificationViewItemContentChangeKind.PROGRESS }))); } return this._progress; @@ -564,7 +609,7 @@ export class NotificationViewItem extends Disposable implements INotificationVie updateSeverity(severity: Severity): void { this._severity = severity; - this._onDidChangeLabel.fire({ kind: NotificationViewItemLabelKind.SEVERITY }); + this._onDidChangeContent.fire({ kind: NotificationViewItemContentChangeKind.SEVERITY }); } updateMessage(input: NotificationMessage): void { @@ -574,13 +619,20 @@ export class NotificationViewItem extends Disposable implements INotificationVie } this._message = message; - this._onDidChangeLabel.fire({ kind: NotificationViewItemLabelKind.MESSAGE }); + this._onDidChangeContent.fire({ kind: NotificationViewItemContentChangeKind.MESSAGE }); } updateActions(actions?: INotificationActions): void { this.setActions(actions); + this._onDidChangeContent.fire({ kind: NotificationViewItemContentChangeKind.ACTIONS }); + } - this._onDidChangeLabel.fire({ kind: NotificationViewItemLabelKind.ACTIONS }); + updateVisibility(visible: boolean): void { + if (this._visible !== visible) { + this._visible = visible; + + this._onDidChangeVisibility.fire(visible); + } } expand(): void { diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index 1e141807590..8cf5db8c126 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -6,7 +6,7 @@ import { Command } from 'vs/editor/common/modes'; 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 { RawContextKey, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { localize } from 'vs/nls'; import { IViewlet } from 'vs/workbench/common/viewlet'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -17,7 +17,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IKeybindings } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IAction, IActionViewItem } from 'vs/base/common/actions'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { flatten } from 'vs/base/common/arrays'; +import { flatten, mergeSort } from 'vs/base/common/arrays'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { SetMap } from 'vs/base/common/collections'; @@ -53,6 +53,7 @@ export interface IViewContainerDescriptor { readonly extensionId?: ExtensionIdentifier; + readonly rejectAddedViews?: boolean; } export interface IViewContainersRegistry { @@ -177,7 +178,7 @@ export interface IViewDescriptor { readonly ctorDescriptor: SyncDescriptor; - readonly when?: ContextKeyExpr; + readonly when?: ContextKeyExpression; readonly order?: number; @@ -211,14 +212,21 @@ export interface IViewDescriptorCollection extends IDisposable { readonly allViewDescriptors: IViewDescriptor[]; } +export enum ViewContentPriority { + Normal = 0, + Low = 1, + Lowest = 2 +} + export interface IViewContentDescriptor { readonly content: string; - readonly when?: ContextKeyExpr | 'default'; + readonly when?: ContextKeyExpression | 'default'; + readonly priority?: ViewContentPriority; /** * ordered preconditions for each button in the content */ - readonly preconditions?: (ContextKeyExpr | undefined)[]; + readonly preconditions?: (ContextKeyExpression | undefined)[]; } export interface IViewsRegistry { @@ -247,6 +255,13 @@ export interface IViewsRegistry { } function compareViewContentDescriptors(a: IViewContentDescriptor, b: IViewContentDescriptor): number { + const aPriority = a.priority ?? ViewContentPriority.Normal; + const bPriority = b.priority ?? ViewContentPriority.Normal; + + if (aPriority !== bPriority) { + return aPriority - bPriority; + } + return a.content < b.content ? -1 : 1; } @@ -328,8 +343,8 @@ class ViewsRegistry extends Disposable implements IViewsRegistry { getViewWelcomeContent(id: string): IViewContentDescriptor[] { const result: IViewContentDescriptor[] = []; - result.sort(compareViewContentDescriptors); this._viewWelcomeContents.forEach(id, descriptor => result.push(descriptor)); + mergeSort(result, compareViewContentDescriptors); return result; } diff --git a/src/vs/workbench/contrib/backup/common/backupTracker.ts b/src/vs/workbench/contrib/backup/common/backupTracker.ts index 074a0c545aa..4121d8ae848 100644 --- a/src/vs/workbench/contrib/backup/common/backupTracker.ts +++ b/src/vs/workbench/contrib/backup/common/backupTracker.ts @@ -114,10 +114,6 @@ export abstract class BackupTracker extends Disposable { return; // skip if auto save is enabled with a short delay } - if (typeof workingCopy.backup !== 'function') { - return; // skip if working copy does not support backups - } - // Clear any running backup operation dispose(this.pendingBackups.get(workingCopy)); this.pendingBackups.delete(workingCopy); @@ -131,7 +127,7 @@ export abstract class BackupTracker extends Disposable { this.pendingBackups.delete(workingCopy); // Backup if dirty - if (workingCopy.isDirty() && typeof workingCopy.backup === 'function') { + if (workingCopy.isDirty()) { this.logService.trace(`[backup tracker] running backup`, workingCopy.resource.toString()); const backup = await workingCopy.backup(); diff --git a/src/vs/workbench/contrib/backup/electron-browser/backupTracker.ts b/src/vs/workbench/contrib/backup/electron-browser/backupTracker.ts index 1aba5636ccb..b3b65134298 100644 --- a/src/vs/workbench/contrib/backup/electron-browser/backupTracker.ts +++ b/src/vs/workbench/contrib/backup/electron-browser/backupTracker.ts @@ -161,12 +161,10 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont // Backup does not exist else { - if (typeof workingCopy.backup === 'function') { - const backup = await workingCopy.backup(); - await this.backupFileService.backup(workingCopy.resource, backup.content, contentVersion, backup.meta); + const backup = await workingCopy.backup(); + await this.backupFileService.backup(workingCopy.resource, backup.content, contentVersion, backup.meta); - backups.push(workingCopy); - } + backups.push(workingCopy); } })); } @@ -235,16 +233,13 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont const revertOptions = { soft: true }; // First revert through the editor service if we revert all - let result: boolean | undefined = undefined; if (workingCopies.length === this.workingCopyService.dirtyCount) { - result = await this.editorService.revertAll(revertOptions); + await this.editorService.revertAll(revertOptions); } // If we still have dirty working copies, revert those directly // unless the revert operation was not successful (e.g. cancelled) - if (result !== false) { - await Promise.all(workingCopies.map(workingCopy => workingCopy.isDirty() ? workingCopy.revert(revertOptions) : Promise.resolve(true))); - } + await Promise.all(workingCopies.map(workingCopy => workingCopy.isDirty() ? workingCopy.revert(revertOptions) : Promise.resolve())); } private noVeto(backupsToDiscard: IWorkingCopy[]): boolean | Promise { diff --git a/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts b/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts index 8420b62c436..b3d66ede005 100644 --- a/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts +++ b/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts @@ -40,6 +40,11 @@ import { BackupTracker } from 'vs/workbench/contrib/backup/common/backupTracker' import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/electron-browser/workbenchTestServices'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { TestFilesConfigurationService, TestEnvironmentService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; const userdataDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backuprestorer'); const backupHome = path.join(userdataDir, 'Backups'); @@ -113,11 +118,23 @@ suite('BackupTracker', () => { return pfs.rimraf(backupHome, pfs.RimRafMode.MOVE); }); - async function createTracker(): Promise<[TestServiceAccessor, EditorPart, BackupTracker, IInstantiationService]> { + async function createTracker(autoSaveEnabled = false): Promise<[TestServiceAccessor, EditorPart, BackupTracker, IInstantiationService]> { const backupFileService = new NodeTestBackupFileService(workspaceBackupPath); const instantiationService = workbenchInstantiationService(); instantiationService.stub(IBackupFileService, backupFileService); + const configurationService = new TestConfigurationService(); + if (autoSaveEnabled) { + configurationService.setUserConfiguration('files', { autoSave: 'afterDelay', autoSaveDelay: 1 }); + } + instantiationService.stub(IConfigurationService, configurationService); + + instantiationService.stub(IFilesConfigurationService, new TestFilesConfigurationService( + instantiationService.createInstance(MockContextKeyService), + configurationService, + TestEnvironmentService + )); + const part = instantiationService.createInstance(EditorPart); part.create(document.createElement('div')); part.layout(400, 300); @@ -198,7 +215,7 @@ suite('BackupTracker', () => { tracker.dispose(); }); - test('confirm onWillShutdown - no veto', async function () { + test('onWillShutdown - no veto if no dirty files', async function () { const [accessor, part, tracker] = await createTracker(); const resource = toResource.call(this, '/path/index.txt'); @@ -207,18 +224,14 @@ suite('BackupTracker', () => { const event = new BeforeShutdownEventImpl(); accessor.lifecycleService.fireWillShutdown(event); - const veto = event.value; - if (typeof veto === 'boolean') { - assert.ok(!veto); - } else { - assert.ok(!(await veto)); - } + const veto = await event.value; + assert.ok(!veto); part.dispose(); tracker.dispose(); }); - test('confirm onWillShutdown - veto if user cancels', async function () { + test('onWillShutdown - veto if user cancels (hot.exit: off)', async function () { const [accessor, part, tracker] = await createTracker(); const resource = toResource.call(this, '/path/index.txt'); @@ -227,6 +240,7 @@ suite('BackupTracker', () => { const model = accessor.textFileService.files.get(resource); accessor.fileDialogService.setConfirmResult(ConfirmResult.CANCEL); + accessor.filesConfigurationService.onFilesConfigurationChange({ files: { hotExit: 'off' } }); await model?.load(); model?.textEditorModel?.setValue('foo'); @@ -234,13 +248,39 @@ suite('BackupTracker', () => { const event = new BeforeShutdownEventImpl(); accessor.lifecycleService.fireWillShutdown(event); - assert.ok(event.value); + + const veto = await event.value; + assert.ok(veto); part.dispose(); tracker.dispose(); }); - test('confirm onWillShutdown - no veto and backups cleaned up if user does not want to save (hot.exit: off)', async function () { + test('onWillShutdown - no veto if auto save is on', async function () { + const [accessor, part, tracker] = await createTracker(true /* auto save enabled */); + + const resource = toResource.call(this, '/path/index.txt'); + await accessor.editorService.openEditor({ resource, options: { pinned: true } }); + + const model = accessor.textFileService.files.get(resource); + + await model?.load(); + model?.textEditorModel?.setValue('foo'); + assert.equal(accessor.workingCopyService.dirtyCount, 1); + + const event = new BeforeShutdownEventImpl(); + accessor.lifecycleService.fireWillShutdown(event); + + const veto = await event.value; + assert.ok(!veto); + + assert.equal(accessor.workingCopyService.dirtyCount, 0); + + part.dispose(); + tracker.dispose(); + }); + + test('onWillShutdown - no veto and backups cleaned up if user does not want to save (hot.exit: off)', async function () { const [accessor, part, tracker] = await createTracker(); const resource = toResource.call(this, '/path/index.txt'); @@ -257,21 +297,15 @@ suite('BackupTracker', () => { const event = new BeforeShutdownEventImpl(); accessor.lifecycleService.fireWillShutdown(event); - let veto = event.value; - if (typeof veto === 'boolean') { - assert.ok(accessor.backupFileService.discardedBackups.length > 0); - assert.ok(!veto); - } else { - veto = await veto; - assert.ok(accessor.backupFileService.discardedBackups.length > 0); - assert.ok(!veto); - } + const veto = await event.value; + assert.ok(!veto); + assert.ok(accessor.backupFileService.discardedBackups.length > 0); part.dispose(); tracker.dispose(); }); - test('confirm onWillShutdown - save (hot.exit: off)', async function () { + test('onWillShutdown - save (hot.exit: off)', async function () { const [accessor, part, tracker] = await createTracker(); const resource = toResource.call(this, '/path/index.txt'); @@ -288,7 +322,7 @@ suite('BackupTracker', () => { const event = new BeforeShutdownEventImpl(); accessor.lifecycleService.fireWillShutdown(event); - const veto = await (>event.value); + const veto = await event.value; assert.ok(!veto); assert.ok(!model?.isDirty()); @@ -431,7 +465,7 @@ suite('BackupTracker', () => { event.reason = shutdownReason; accessor.lifecycleService.fireWillShutdown(event); - const veto = await (>event.value); + const veto = await event.value; assert.equal(accessor.backupFileService.discardedBackups.length, 0); // When hot exit is set, backups should never be cleaned since the confirm result is cancel assert.equal(veto, shouldVeto); diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.contribution.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.contribution.ts index 66624cf7bb6..faf9b538534 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.contribution.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.contribution.ts @@ -111,6 +111,7 @@ class BulkEditPreviewContribution { private async _previewEdit(edit: WorkspaceEdit) { this._ctxEnabled.set(true); + const uxState = this._activeSession?.uxState ?? new UXState(this._panelService, this._editorGroupsService); const view = await getBulkEditPane(this._viewsService); if (!view) { this._ctxEnabled.set(false); @@ -136,9 +137,9 @@ class BulkEditPreviewContribution { let session: PreviewSession; if (this._activeSession) { this._activeSession.cts.dispose(true); - session = new PreviewSession(this._activeSession.uxState); + session = new PreviewSession(uxState); } else { - session = new PreviewSession(new UXState(this._panelService, this._editorGroupsService)); + session = new PreviewSession(uxState); } this._activeSession = session; diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPreview.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPreview.ts index 06bdb85376a..cfc36c26499 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPreview.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPreview.ts @@ -18,7 +18,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { Emitter, Event } from 'vs/base/common/event'; import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; import { ConflictDetector } from 'vs/workbench/services/bulkEdit/browser/conflicts'; -import { values, ResourceMap } from 'vs/base/common/map'; +import { ResourceMap } from 'vs/base/common/map'; import { localize } from 'vs/nls'; export class CheckedStates { @@ -89,7 +89,7 @@ export class BulkFileOperation { readonly parent: BulkFileOperations ) { } - addEdit(index: number, type: BulkFileOperationType, edit: WorkspaceTextEdit | WorkspaceFileEdit, ) { + addEdit(index: number, type: BulkFileOperationType, edit: WorkspaceTextEdit | WorkspaceFileEdit) { this.type |= type; this.originalEdits.set(index, edit); if (WorkspaceTextEdit.is(edit)) { @@ -126,8 +126,8 @@ export class BulkCategory { constructor(readonly metadata: WorkspaceEditMetadata = BulkCategory._defaultMetadata) { } - get fileOperations(): BulkFileOperation[] { - return values(this.operationByResource); + get fileOperations(): IterableIterator { + return this.operationByResource.values(); } } diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditTree.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditTree.ts index 0c55961052c..39b04d94db9 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditTree.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditTree.ts @@ -27,6 +27,7 @@ import { WorkspaceFileEdit } from 'vs/editor/common/modes'; import { compare } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; +import { Iterable } from 'vs/base/common/iterator'; // --- VIEW MODEL @@ -201,7 +202,7 @@ export class BulkEditDataSource implements IAsyncDataSource new FileElement(element, op)); + return [...Iterable.map(element.category.fileOperations, op => new FileElement(element, op))]; } // file: text edit @@ -283,7 +284,7 @@ export class BulkEditSorter implements ITreeSorter { } private static _needsConfirmation(a: BulkCategory): boolean { - return a.fileOperations.some(ops => ops.needsConfirmation()); + return Iterable.some(a.fileOperations, ops => ops.needsConfirmation()); } } diff --git a/src/vs/workbench/contrib/codeActions/common/documentationContribution.ts b/src/vs/workbench/contrib/codeActions/common/documentationContribution.ts index 9c2b75c277d..1b6507eed9d 100644 --- a/src/vs/workbench/contrib/codeActions/common/documentationContribution.ts +++ b/src/vs/workbench/contrib/codeActions/common/documentationContribution.ts @@ -10,7 +10,7 @@ import { Selection } from 'vs/editor/common/core/selection'; import { ITextModel } from 'vs/editor/common/model'; import * as modes from 'vs/editor/common/modes'; import { CodeActionKind } from 'vs/editor/contrib/codeAction/types'; -import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, IContextKeyService, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IExtensionPoint } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { DocumentationExtensionPoint } from './documentationExtensionPoint'; @@ -20,7 +20,7 @@ export class CodeActionDocumentationContribution extends Disposable implements I private contributions: { title: string; - when: ContextKeyExpr; + when: ContextKeyExpression; command: string; }[] = []; diff --git a/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts b/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts index 3386ae55dc6..32f7f4a1722 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts @@ -30,7 +30,7 @@ import { IWorkbenchContribution, Extensions as WorkbenchContributionsExtensions, import { Registry } from 'vs/platform/registry/common/platform'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -class TrimWhitespaceParticipant implements ITextFileSaveParticipant { +export class TrimWhitespaceParticipant implements ITextFileSaveParticipant { constructor( @IConfigurationService private readonly configurationService: IConfigurationService, diff --git a/src/vs/workbench/contrib/codeEditor/browser/toggleColumnSelection.ts b/src/vs/workbench/contrib/codeEditor/browser/toggleColumnSelection.ts index 6624e94c7bd..011918eb9b8 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/toggleColumnSelection.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/toggleColumnSelection.ts @@ -10,6 +10,13 @@ import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configur import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { CoreNavigationCommands } from 'vs/editor/browser/controller/coreCommands'; +import { Position } from 'vs/editor/common/core/position'; +import { Selection } from 'vs/editor/common/core/selection'; +import { CursorColumns } from 'vs/editor/common/controller/cursorCommon'; export class ToggleColumnSelectionAction extends Action { public static readonly ID = 'editor.action.toggleColumnSelection'; @@ -18,26 +25,68 @@ export class ToggleColumnSelectionAction extends Action { constructor( id: string, label: string, - @IConfigurationService private readonly _configurationService: IConfigurationService + @IConfigurationService private readonly _configurationService: IConfigurationService, + @ICodeEditorService private readonly _codeEditorService: ICodeEditorService ) { super(id, label); } - public run(): Promise { - const newValue = !this._configurationService.getValue('editor.columnSelection'); - return this._configurationService.updateValue('editor.columnSelection', newValue, ConfigurationTarget.USER); + private _getCodeEditor(): ICodeEditor | null { + const codeEditor = this._codeEditorService.getFocusedCodeEditor(); + if (codeEditor) { + return codeEditor; + } + return this._codeEditorService.getActiveCodeEditor(); + } + + public async run(): Promise { + const oldValue = this._configurationService.getValue('editor.columnSelection'); + const codeEditor = this._getCodeEditor(); + await this._configurationService.updateValue('editor.columnSelection', !oldValue, ConfigurationTarget.USER); + const newValue = this._configurationService.getValue('editor.columnSelection'); + if (!codeEditor || codeEditor !== this._getCodeEditor() || oldValue === newValue || !codeEditor.hasModel()) { + return; + } + const cursors = codeEditor._getCursors(); + if (codeEditor.getOption(EditorOption.columnSelection)) { + const selection = codeEditor.getSelection(); + const modelSelectionStart = new Position(selection.selectionStartLineNumber, selection.selectionStartColumn); + const viewSelectionStart = cursors.context.convertModelPositionToViewPosition(modelSelectionStart); + const modelPosition = new Position(selection.positionLineNumber, selection.positionColumn); + const viewPosition = cursors.context.convertModelPositionToViewPosition(modelPosition); + + CoreNavigationCommands.MoveTo.runCoreEditorCommand(cursors, { + position: modelSelectionStart, + viewPosition: viewSelectionStart + }); + const visibleColumn = CursorColumns.visibleColumnFromColumn2(cursors.context.config, cursors.context.viewModel, viewPosition); + CoreNavigationCommands.ColumnSelect.runCoreEditorCommand(cursors, { + position: modelPosition, + viewPosition: viewPosition, + doColumnSelect: true, + mouseColumn: visibleColumn + 1 + }); + } else { + const columnSelectData = cursors.getColumnSelectData(); + const fromViewColumn = CursorColumns.columnFromVisibleColumn2(cursors.context.config, cursors.context.viewModel, columnSelectData.fromViewLineNumber, columnSelectData.fromViewVisualColumn); + const fromPosition = cursors.context.convertViewPositionToModelPosition(columnSelectData.fromViewLineNumber, fromViewColumn); + const toViewColumn = CursorColumns.columnFromVisibleColumn2(cursors.context.config, cursors.context.viewModel, columnSelectData.toViewLineNumber, columnSelectData.toViewVisualColumn); + const toPosition = cursors.context.convertViewPositionToModelPosition(columnSelectData.toViewLineNumber, toViewColumn); + + codeEditor.setSelection(new Selection(fromPosition.lineNumber, fromPosition.column, toPosition.lineNumber, toPosition.column)); + } } } const registry = Registry.as(ActionExtensions.WorkbenchActions); -registry.registerWorkbenchAction(SyncActionDescriptor.create(ToggleColumnSelectionAction, ToggleColumnSelectionAction.ID, ToggleColumnSelectionAction.LABEL), 'View: Toggle Column Selection Mode', nls.localize('view', "View")); +registry.registerWorkbenchAction(SyncActionDescriptor.create(ToggleColumnSelectionAction, ToggleColumnSelectionAction.ID, ToggleColumnSelectionAction.LABEL), 'Toggle Column Selection Mode'); MenuRegistry.appendMenuItem(MenuId.MenubarSelectionMenu, { - group: '3_multi', + group: '4_config', command: { id: ToggleColumnSelectionAction.ID, title: nls.localize({ key: 'miColumnSelection', comment: ['&& denotes a mnemonic'] }, "Column &&Selection Mode"), toggled: ContextKeyExpr.equals('config.editor.columnSelection', true) }, - order: 1.5 + order: 2 }); diff --git a/src/vs/workbench/contrib/codeEditor/browser/toggleMultiCursorModifier.ts b/src/vs/workbench/contrib/codeEditor/browser/toggleMultiCursorModifier.ts index 05df06aa388..bc210c5a355 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/toggleMultiCursorModifier.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/toggleMultiCursorModifier.ts @@ -70,7 +70,7 @@ Registry.as(WorkbenchExtensions.Workbench).regi const registry = Registry.as(Extensions.WorkbenchActions); registry.registerWorkbenchAction(SyncActionDescriptor.create(ToggleMultiCursorModifierAction, ToggleMultiCursorModifierAction.ID, ToggleMultiCursorModifierAction.LABEL), 'Toggle Multi-Cursor Modifier'); MenuRegistry.appendMenuItem(MenuId.MenubarSelectionMenu, { - group: '3_multi', + group: '4_config', command: { id: ToggleMultiCursorModifierAction.ID, title: nls.localize('miMultiCursorAlt', "Switch to Alt+Click for Multi-Cursor") @@ -79,7 +79,7 @@ MenuRegistry.appendMenuItem(MenuId.MenubarSelectionMenu, { order: 1 }); MenuRegistry.appendMenuItem(MenuId.MenubarSelectionMenu, { - group: '3_multi', + group: '4_config', command: { id: ToggleMultiCursorModifierAction.ID, title: ( diff --git a/src/vs/workbench/contrib/codeEditor/electron-browser/selectionClipboard.ts b/src/vs/workbench/contrib/codeEditor/electron-browser/selectionClipboard.ts index 536d0f75c5c..592605c0759 100644 --- a/src/vs/workbench/contrib/codeEditor/electron-browser/selectionClipboard.ts +++ b/src/vs/workbench/contrib/codeEditor/electron-browser/selectionClipboard.ts @@ -20,10 +20,7 @@ import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; export class SelectionClipboard extends Disposable implements IEditorContribution { private static readonly SELECTION_LENGTH_LIMIT = 65536; @@ -119,15 +116,7 @@ class PasteSelectionClipboardAction extends EditorAction { id: 'editor.action.selectionClipboardPaste', label: nls.localize('actions.pasteSelectionClipboard', "Paste Selection Clipboard"), alias: 'Paste Selection Clipboard', - precondition: EditorContextKeys.writable, - kbOpts: { - kbExpr: ContextKeyExpr.and( - EditorContextKeys.editorTextFocus, - ContextKeyExpr.has('config.editor.selectionClipboard') - ), - primary: KeyMod.Shift | KeyCode.Insert, - weight: KeybindingWeight.EditorContrib - } + precondition: EditorContextKeys.writable }); } diff --git a/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts b/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts index 5c7917b7350..bed0805028b 100644 --- a/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts +++ b/src/vs/workbench/contrib/codeEditor/test/browser/saveParticipant.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { FinalNewLineParticipant, TrimFinalNewLinesParticipant } from 'vs/workbench/contrib/codeEditor/browser/saveParticipants'; +import { FinalNewLineParticipant, TrimFinalNewLinesParticipant, TrimWhitespaceParticipant } from 'vs/workbench/contrib/codeEditor/browser/saveParticipants'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; import { toResource } from 'vs/base/test/common/utils'; @@ -16,7 +16,7 @@ import { IResolvedTextFileEditorModel, snapshotToString } from 'vs/workbench/ser import { SaveReason } from 'vs/workbench/common/editor'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; -suite('MainThreadSaveParticipant', function () { +suite('Save Participants', function () { let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; @@ -151,4 +151,24 @@ suite('MainThreadSaveParticipant', function () { model.textEditorModel.redo(); assert.equal(snapshotToString(model.createSnapshot()!), `${textContent}${eol}`); }); + + test('trim whitespace', async function () { + const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/trim_final_new_line.txt'), 'utf8', undefined) as IResolvedTextFileEditorModel; + + await model.load(); + const configService = new TestConfigurationService(); + configService.setUserConfiguration('files', { 'trimTrailingWhitespace': true }); + const participant = new TrimWhitespaceParticipant(configService, undefined!); + const textContent = 'Test'; + let content = `${textContent} `; + model.textEditorModel.setValue(content); + + // save many times + for (let i = 0; i < 10; i++) { + await participant.participate(model, { reason: SaveReason.EXPLICIT }); + } + + // confirm trimming + assert.equal(snapshotToString(model.createSnapshot()!), `${textContent}`); + }); }); diff --git a/src/vs/workbench/contrib/customEditor/browser/commands.ts b/src/vs/workbench/contrib/customEditor/browser/commands.ts index 87c12f0e206..a6b3efb63be 100644 --- a/src/vs/workbench/contrib/customEditor/browser/commands.ts +++ b/src/vs/workbench/contrib/customEditor/browser/commands.ts @@ -17,7 +17,7 @@ import { EditorViewColumn, viewColumnToEditorGroup } from 'vs/workbench/api/comm import { IEditorCommandsContext } from 'vs/workbench/common/editor'; import { CustomEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput'; 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 { CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, CONTEXT_CUSTOM_EDITORS, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import type { ITextEditorOptions } from 'vs/platform/editor/common/editor'; @@ -80,7 +80,7 @@ MenuRegistry.appendMenuItem(MenuId.CommandPalette, { title: REOPEN_WITH_TITLE, category: viewCategory, }, - when: CONTEXT_HAS_CUSTOM_EDITORS, + when: CONTEXT_CUSTOM_EDITORS.notEqualsTo(''), }); MenuRegistry.appendMenuItem(MenuId.EditorTitle, { @@ -89,9 +89,9 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { title: REOPEN_WITH_TITLE, category: viewCategory, }, - group: '3_open', + group: '6_reopen', order: 20, - when: CONTEXT_HAS_CUSTOM_EDITORS, + when: CONTEXT_CUSTOM_EDITORS.notEqualsTo(''), }); // #endregion @@ -155,7 +155,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { constructor() { super({ id: ToggleCustomEditorCommand.ID, - precondition: CONTEXT_HAS_CUSTOM_EDITORS, + precondition: CONTEXT_CUSTOM_EDITORS, }); } diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index 82ee3c38afa..d55b558f42b 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -190,9 +190,9 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { return this.handleMove(groupId, target) || this.editorService.createInput({ resource: target, forceFile: true }); } - public async revert(group: GroupIdentifier, options?: IRevertOptions): Promise { + public async revert(group: GroupIdentifier, options?: IRevertOptions): Promise { if (!this._model) { - return false; + return; } switch (this._model.type) { diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts index 7ce0a933cb7..ca4581ede56 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts @@ -25,7 +25,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, CustomEditorInfoCollection, CustomEditorPriority, CustomEditorSelector, ICustomEditor, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; +import { CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, CONTEXT_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 { IWebviewService, webviewHasOwnEditFunctionsContext } from 'vs/workbench/contrib/webview/browser/webview'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -98,7 +98,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ private readonly _models: CustomEditorModelManager; - private readonly _hasCustomEditor: IContextKey; + private readonly _customEditorContextKey: IContextKey; private readonly _focusedCustomEditorIsEditable: IContextKey; private readonly _webviewHasOwnEditFunctions: IContextKey; @@ -118,7 +118,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ this._models = new CustomEditorModelManager(workingCopyService, labelService); - this._hasCustomEditor = CONTEXT_HAS_CUSTOM_EDITORS.bindTo(contextKeyService); + this._customEditorContextKey = CONTEXT_CUSTOM_EDITORS.bindTo(contextKeyService); this._focusedCustomEditorIsEditable = CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE.bindTo(contextKeyService); this._webviewHasOwnEditFunctions = webviewHasOwnEditFunctionsContext.bindTo(contextKeyService); @@ -310,7 +310,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ const activeControl = this.editorService.activeControl; const resource = activeControl?.input.resource; if (!resource) { - this._hasCustomEditor.reset(); + this._customEditorContextKey.reset(); this._focusedCustomEditorIsEditable.reset(); this._webviewHasOwnEditFunctions.reset(); return; @@ -320,7 +320,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ ...this.getContributedCustomEditors(resource).allEditors, ...this.getUserConfiguredCustomEditors(resource).allEditors, ]; - this._hasCustomEditor.set(possibleEditors.length > 0); + this._customEditorContextKey.set(possibleEditors.map(x => x.id).join(',')); this._focusedCustomEditorIsEditable.set(activeControl?.input instanceof CustomEditorInput); this._webviewHasOwnEditFunctions.set(possibleEditors.length > 0); } @@ -333,11 +333,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ } const editorInfo = this._editorInfoStore.get(editor.viewType); - if (!editorInfo) { - continue; - } - - if (!editorInfo.matches(newResource)) { + if (!editorInfo?.matches(newResource)) { continue; } diff --git a/src/vs/workbench/contrib/customEditor/common/customEditor.ts b/src/vs/workbench/contrib/customEditor/common/customEditor.ts index 96247937c20..277a153fd5a 100644 --- a/src/vs/workbench/contrib/customEditor/common/customEditor.ts +++ b/src/vs/workbench/contrib/customEditor/common/customEditor.ts @@ -18,7 +18,7 @@ import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCo export const ICustomEditorService = createDecorator('customEditorService'); -export const CONTEXT_HAS_CUSTOM_EDITORS = new RawContextKey('hasCustomEditors', false); +export const CONTEXT_CUSTOM_EDITORS = new RawContextKey('customEditors', ''); export const CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE = new RawContextKey('focusedCustomEditorIsEditable', false); export interface ICustomEditor { @@ -74,12 +74,12 @@ export interface ICustomEditorModel extends IWorkingCopy { readonly onWillSave: Event; readonly onWillSaveAs: Event; - onBackup(f: () => CancelablePromise): void; + onBackup(f: () => CancelablePromise): void; setDirty(dirty: boolean): void; undo(): void; redo(): void; - revert(options?: IRevertOptions): Promise; + revert(options?: IRevertOptions): Promise; save(options?: ISaveOptions): Promise; saveAs(resource: URI, targetResource: URI, currentOptions?: ISaveOptions): Promise; diff --git a/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts b/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts index 6adac4c6199..d961dec639e 100644 --- a/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts +++ b/src/vs/workbench/contrib/customEditor/common/customEditorModel.ts @@ -29,7 +29,7 @@ namespace HotExitState { readonly type = Type.Pending; constructor( - public readonly operation: CancelablePromise, + public readonly operation: CancelablePromise, ) { } } @@ -90,9 +90,9 @@ export class CustomEditorModel extends Disposable implements ICustomEditorModel private readonly _onWillSaveAs = this._register(new Emitter()); public readonly onWillSaveAs = this._onWillSaveAs.event; - private _onBackup: undefined | (() => CancelablePromise); + private _onBackup: undefined | (() => CancelablePromise); - public onBackup(f: () => CancelablePromise) { + public onBackup(f: () => CancelablePromise) { if (this._onBackup) { throw new Error('Backup already implemented'); } @@ -113,12 +113,9 @@ export class CustomEditorModel extends Disposable implements ICustomEditorModel } public async revert(_options?: IRevertOptions) { - if (!this._dirty) { - return true; + if (this._dirty) { + this._onRevert.fire(); } - - this._onRevert.fire(); - return true; } public undo() { @@ -182,7 +179,11 @@ export class CustomEditorModel extends Disposable implements ICustomEditorModel this._hotExitState = pendingState; try { - this._hotExitState = await pendingState.operation ? HotExitState.Allowed : HotExitState.NotAllowed; + await pendingState.operation; + // Make sure state has not changed in the meantime + if (this._hotExitState === pendingState) { + this._hotExitState = HotExitState.Allowed; + } } catch (e) { // Make sure state has not changed in the meantime if (this._hotExitState === pendingState) { diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 30ae2db8bbf..06f84bae24b 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -31,7 +31,7 @@ import { IQuickOpenRegistry, Extensions as QuickOpenExtensions, QuickOpenHandler import { StatusBarColorProvider } from 'vs/workbench/contrib/debug/browser/statusbarColorProvider'; import { IViewsRegistry, Extensions as ViewExtensions, IViewContainersRegistry, ViewContainerLocation, ViewContainer } from 'vs/workbench/common/views'; import { isMacintosh } from 'vs/base/common/platform'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { URI } from 'vs/base/common/uri'; import { DebugQuickOpenHandler } from 'vs/workbench/contrib/debug/browser/debugQuickOpen'; import { DebugStatusContribution } from 'vs/workbench/contrib/debug/browser/debugStatus'; @@ -135,7 +135,7 @@ registry.registerWorkbenchAction(SyncActionDescriptor.create(DisableAllBreakpoin registry.registerWorkbenchAction(SyncActionDescriptor.create(SelectAndStartAction, SelectAndStartAction.ID, SelectAndStartAction.LABEL), 'Debug: Select and Start Debugging', debugCategory); registry.registerWorkbenchAction(SyncActionDescriptor.create(ClearReplAction, ClearReplAction.ID, ClearReplAction.LABEL), 'Debug: Clear Console', debugCategory); -const registerDebugCommandPaletteItem = (id: string, title: string, when?: ContextKeyExpr, precondition?: ContextKeyExpr) => { +const registerDebugCommandPaletteItem = (id: string, title: string, when?: ContextKeyExpression, precondition?: ContextKeyExpression) => { MenuRegistry.appendMenuItem(MenuId.CommandPalette, { when, command: { @@ -267,7 +267,7 @@ configurationRegistry.registerConfiguration({ default: true }, 'debug.onTaskErrors': { - enum: ['debugAnyway', 'showErrors', 'prompt', 'cancel'], + enum: ['debugAnyway', 'showErrors', 'prompt', 'abort'], enumDescriptions: [nls.localize('debugAnyway', "Ignore task errors and start debugging."), nls.localize('showErrors', "Show the Problems view and do not start debugging."), nls.localize('prompt', "Prompt user."), nls.localize('cancel', "Cancel debugging.")], description: nls.localize('debug.onTaskErrors', "Controls what to do when errors are encountered after running a preLaunchTask."), default: 'prompt' @@ -290,7 +290,7 @@ Registry.as(WorkbenchExtensions.Workbench).regi // Debug toolbar -const registerDebugToolBarItem = (id: string, title: string, order: number, icon: { light?: URI, dark?: URI } | ThemeIcon, when?: ContextKeyExpr, precondition?: ContextKeyExpr) => { +const registerDebugToolBarItem = (id: string, title: string, order: number, icon: { light?: URI, dark?: URI } | ThemeIcon, when?: ContextKeyExpression, precondition?: ContextKeyExpression) => { MenuRegistry.appendMenuItem(MenuId.DebugToolBar, { group: 'navigation', when, @@ -316,7 +316,7 @@ registerDebugToolBarItem(STEP_BACK_ID, nls.localize('stepBackDebug', "Step Back" registerDebugToolBarItem(REVERSE_CONTINUE_ID, nls.localize('reverseContinue', "Reverse"), 60, { id: 'codicon/debug-reverse-continue' }, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); // Debug callstack context menu -const registerDebugCallstackItem = (id: string, title: string, order: number, when?: ContextKeyExpr, precondition?: ContextKeyExpr, group = 'navigation') => { +const registerDebugCallstackItem = (id: string, title: string, order: number, when?: ContextKeyExpression, precondition?: ContextKeyExpression, group = 'navigation') => { MenuRegistry.appendMenuItem(MenuId.DebugCallStackContext, { group, when, @@ -558,7 +558,7 @@ MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { // Touch Bar if (isMacintosh) { - const registerTouchBarEntry = (id: string, title: string, order: number, when: ContextKeyExpr | undefined, iconUri: URI) => { + const registerTouchBarEntry = (id: string, title: string, order: number, when: ContextKeyExpression | undefined, iconUri: URI) => { MenuRegistry.appendMenuItem(MenuId.TouchBarContext, { command: { id, diff --git a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts index 29dcf72e752..1a4ec6cf4d7 100644 --- a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts @@ -519,18 +519,17 @@ abstract class AbstractLaunch { if (!config || (!Array.isArray(config.configurations) && !Array.isArray(config.compounds))) { return []; } else { - const names: string[] = []; + const configurations: (IConfig | ICompound)[] = []; if (config.configurations) { - names.push(...config.configurations.filter(cfg => cfg && typeof cfg.name === 'string').map(cfg => cfg.name)); + configurations.push(...config.configurations.filter(cfg => cfg && typeof cfg.name === 'string')); } if (includeCompounds && config.compounds) { if (config.compounds) { - names.push(...config.compounds.filter(compound => typeof compound.name === 'string' && compound.configurations && compound.configurations.length) - .map(compound => compound.name)); + configurations.push(...config.compounds.filter(compound => typeof compound.name === 'string' && compound.configurations && compound.configurations.length)); } } - return names; + return getVisibleAndSorted(configurations).map(c => c.name); } } diff --git a/src/vs/workbench/contrib/debug/browser/debugHover.ts b/src/vs/workbench/contrib/debug/browser/debugHover.ts index 5de2c78eea2..0a9b8964e1a 100644 --- a/src/vs/workbench/contrib/debug/browser/debugHover.ts +++ b/src/vs/workbench/contrib/debug/browser/debugHover.ts @@ -209,7 +209,7 @@ export class DebugHoverWidget implements IContentWidget { if (!matchingExpression) { const lineContent = model.getLineContent(pos.lineNumber); - matchingExpression = lineContent.substring(rng.startColumn - 1, rng.endColumn); + matchingExpression = lineContent.substring(rng.startColumn - 1, rng.endColumn - 1); } } diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 3c9ba78f0d6..214d3763448 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -19,7 +19,7 @@ import { RawDebugSession } from 'vs/workbench/contrib/debug/browser/rawDebugSess import { IProductService } from 'vs/platform/product/common/productService'; import { IWorkspaceFolder, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { RunOnceScheduler } from 'vs/base/common/async'; +import { RunOnceScheduler, Queue } from 'vs/base/common/async'; import { generateUuid } from 'vs/base/common/uuid'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug'; @@ -807,62 +807,58 @@ export class DebugSession implements IDebugSession { this._onDidChangeState.fire(); })); - let outpuPromises: Promise[] = []; + const outputQueue = new Queue(); this.rawListeners.push(this.raw.onDidOutput(async event => { - if (!event.body || !this.raw) { - return; - } - - const outputSeverity = event.body.category === 'stderr' ? severity.Error : event.body.category === 'console' ? severity.Warning : severity.Info; - if (event.body.category === 'telemetry') { - // only log telemetry events from debug adapter if the debug extension provided the telemetry key - // and the user opted in telemetry - if (this.raw.customTelemetryService && this.telemetryService.isOptedIn) { - // __GDPR__TODO__ We're sending events in the name of the debug extension and we can not ensure that those are declared correctly. - this.raw.customTelemetryService.publicLog(event.body.output, event.body.data); - } - - return; - } - - // Make sure to append output in the correct order by properly waiting on preivous promises #33822 - const waitFor = outpuPromises.slice(); - const source = event.body.source && event.body.line ? { - lineNumber: event.body.line, - column: event.body.column ? event.body.column : 1, - source: this.getSource(event.body.source) - } : undefined; - - if (event.body.group === 'start' || event.body.group === 'startCollapsed') { - const expanded = event.body.group === 'start'; - this.repl.startGroup(event.body.output || '', expanded, source); - return; - } - if (event.body.group === 'end') { - this.repl.endGroup(); - if (!event.body.output) { - // Only return if the end event does not have additional output in it + outputQueue.queue(async () => { + if (!event.body || !this.raw) { return; } - } - if (event.body.variablesReference) { - const container = new ExpressionContainer(this, undefined, event.body.variablesReference, generateUuid()); - outpuPromises.push(container.getChildren().then(async children => { - await Promise.all(waitFor); - children.forEach(child => { - // Since we can not display multiple trees in a row, we are displaying these variables one after the other (ignoring their names) - (child).name = null; - this.appendToRepl(child, outputSeverity, source); + const outputSeverity = event.body.category === 'stderr' ? severity.Error : event.body.category === 'console' ? severity.Warning : severity.Info; + if (event.body.category === 'telemetry') { + // only log telemetry events from debug adapter if the debug extension provided the telemetry key + // and the user opted in telemetry + if (this.raw.customTelemetryService && this.telemetryService.isOptedIn) { + // __GDPR__TODO__ We're sending events in the name of the debug extension and we can not ensure that those are declared correctly. + this.raw.customTelemetryService.publicLog(event.body.output, event.body.data); + } + + return; + } + + // Make sure to append output in the correct order by properly waiting on preivous promises #33822 + const source = event.body.source && event.body.line ? { + lineNumber: event.body.line, + column: event.body.column ? event.body.column : 1, + source: this.getSource(event.body.source) + } : undefined; + + if (event.body.group === 'start' || event.body.group === 'startCollapsed') { + const expanded = event.body.group === 'start'; + this.repl.startGroup(event.body.output || '', expanded, source); + return; + } + if (event.body.group === 'end') { + this.repl.endGroup(); + if (!event.body.output) { + // Only return if the end event does not have additional output in it + return; + } + } + + if (event.body.variablesReference) { + const container = new ExpressionContainer(this, undefined, event.body.variablesReference, generateUuid()); + await container.getChildren().then(children => { + children.forEach(child => { + // Since we can not display multiple trees in a row, we are displaying these variables one after the other (ignoring their names) + (child).name = null; + this.appendToRepl(child, outputSeverity, source); + }); }); - })); - } else if (typeof event.body.output === 'string') { - await Promise.all(waitFor); - this.appendToRepl(event.body.output, outputSeverity, source); - } - - await Promise.all(outpuPromises); - outpuPromises = []; + } else if (typeof event.body.output === 'string') { + this.appendToRepl(event.body.output, outputSeverity, source); + } + }); })); this.rawListeners.push(this.raw.onDidBreakpoint(event => { diff --git a/src/vs/workbench/contrib/debug/browser/startView.ts b/src/vs/workbench/contrib/debug/browser/startView.ts index f6b3133dc28..38ef45e83b5 100644 --- a/src/vs/workbench/contrib/debug/browser/startView.ts +++ b/src/vs/workbench/contrib/debug/browser/startView.ts @@ -75,35 +75,35 @@ export class StartView extends ViewPane { }; this._register(editorService.onDidActiveEditorChange(setContextKey)); this._register(this.debugService.getConfigurationManager().onDidRegisterDebugger(setContextKey)); - this.registerViews(); + setContextKey(); + + const debugKeybinding = this.keybindingService.lookupKeybinding(StartAction.ID); + debugKeybindingLabel = debugKeybinding ? ` (${debugKeybinding.getLabel()})` : ''; } shouldShowWelcome(): boolean { return true; } - - private registerViews(): void { - const viewsRegistry = Registry.as(Extensions.ViewsRegistry); - viewsRegistry.registerViewWelcomeContent(StartView.ID, { - content: localize('openAFileWhichCanBeDebugged', "[Open a file](command:{0}) which can be debugged or run.", isMacintosh ? OpenFileFolderAction.ID : OpenFileAction.ID), - when: CONTEXT_DEBUGGER_INTERESTED_IN_ACTIVE_EDITOR.toNegated() - }); - - const debugKeybinding = this.keybindingService.lookupKeybinding(StartAction.ID); - const debugKeybindingLabel = debugKeybinding ? ` (${debugKeybinding.getLabel()})` : ''; - viewsRegistry.registerViewWelcomeContent(StartView.ID, { - content: localize('runAndDebugAction', "[Run and Debug{0}](command:{1})", debugKeybindingLabel, StartAction.ID), - preconditions: [CONTEXT_DEBUGGER_INTERESTED_IN_ACTIVE_EDITOR] - }); - - viewsRegistry.registerViewWelcomeContent(StartView.ID, { - content: localize('customizeRunAndDebug', "To customize Run and Debug [create a launch.json file](command:{0}).", ConfigureAction.ID), - when: WorkbenchStateContext.notEqualsTo('empty') - }); - - viewsRegistry.registerViewWelcomeContent(StartView.ID, { - content: localize('customizeRunAndDebugOpenFolder', "To customize Run and Debug, [open a folder](command:{0}) and create a launch.json file.", isMacintosh ? OpenFileFolderAction.ID : OpenFolderAction.ID), - when: WorkbenchStateContext.isEqualTo('empty') - }); - } } + +const viewsRegistry = Registry.as(Extensions.ViewsRegistry); +viewsRegistry.registerViewWelcomeContent(StartView.ID, { + content: localize('openAFileWhichCanBeDebugged', "[Open a file](command:{0}) which can be debugged or run.", isMacintosh ? OpenFileFolderAction.ID : OpenFileAction.ID), + when: CONTEXT_DEBUGGER_INTERESTED_IN_ACTIVE_EDITOR.toNegated() +}); + +let debugKeybindingLabel = ''; +viewsRegistry.registerViewWelcomeContent(StartView.ID, { + content: localize('runAndDebugAction', "[Run and Debug{0}](command:{1})", debugKeybindingLabel, StartAction.ID), + preconditions: [CONTEXT_DEBUGGER_INTERESTED_IN_ACTIVE_EDITOR] +}); + +viewsRegistry.registerViewWelcomeContent(StartView.ID, { + content: localize('customizeRunAndDebug', "To customize Run and Debug [create a launch.json file](command:{0}).", ConfigureAction.ID), + when: WorkbenchStateContext.notEqualsTo('empty') +}); + +viewsRegistry.registerViewWelcomeContent(StartView.ID, { + content: localize('customizeRunAndDebugOpenFolder', "To customize Run and Debug, [open a folder](command:{0}) and create a launch.json file.", isMacintosh ? OpenFileFolderAction.ID : OpenFolderAction.ID), + when: WorkbenchStateContext.isEqualTo('empty') +}); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index 39ada55824f..e28796d1000 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -507,6 +507,7 @@ export class ExtensionEditor extends BaseEditor { if (e.enabled === false) { hide(template.subtextContainer); } + this.layout(); })); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 8f0b1969062..cf17ac8d9c5 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -87,7 +87,8 @@ Registry.as(ViewContainerExtensions.ViewContainersRegis name: localize('extensions', "Extensions"), ctorDescriptor: new SyncDescriptor(ExtensionsViewPaneContainer), icon: 'codicon-extensions', - order: 4 + order: 4, + rejectAddedViews: true, }, ViewContainerLocation.Sidebar); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index dec8ffa2385..e032e135584 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -2669,7 +2669,7 @@ export class SystemDisabledWarningAction extends ExtensionAction { if (server) { this.tooltip = localize('Install in other server to enable', "Install the extension on '{0}' to enable.", server.label); } else { - this.tooltip = localize('disabled because of extension kind', "This extension cannot be enabled in the remote server."); + this.tooltip = localize('disabled because of extension kind', "This extension has defined that it cannot run on the remote server"); } return; } diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts index 52f71ee6229..b7d2136893e 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts @@ -39,7 +39,7 @@ import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browse import { ExtensionIdentifier, IExtensionContributions, ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { ILabelService } from 'vs/platform/label/common/label'; +import { ILabelService, IFormatterChangeEvent } from 'vs/platform/label/common/label'; import { ExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/electron-browser/extensionManagementServerService'; import { IProductService } from 'vs/platform/product/common/productService'; import { Schemas } from 'vs/base/common/network'; @@ -92,7 +92,7 @@ suite('ExtensionsActions Test', () => { }()); instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); - instantiationService.stub(ILabelService, { onDidChangeFormatters: new Emitter().event }); + instantiationService.stub(ILabelService, { onDidChangeFormatters: new Emitter().event }); instantiationService.set(IExtensionTipsService, instantiationService.createInstance(ExtensionTipsService)); instantiationService.stub(IURLService, URLService); diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts index 24ac77635c9..856a58552a8 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts @@ -31,6 +31,7 @@ import { IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; import { createErrorWithActions } from 'vs/base/common/errorsWithActions'; import { MutableDisposable } from 'vs/base/common/lifecycle'; import { EditorActivation, IEditorOptions } from 'vs/platform/editor/common/editor'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; /** * An implementation of editor for file system resources. @@ -49,14 +50,15 @@ export class TextFileEditor extends BaseTextEditor { @IInstantiationService instantiationService: IInstantiationService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IStorageService storageService: IStorageService, - @ITextResourceConfigurationService configurationService: ITextResourceConfigurationService, + @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, @IEditorService editorService: IEditorService, @IThemeService themeService: IThemeService, @IEditorGroupsService editorGroupService: IEditorGroupsService, @ITextFileService private readonly textFileService: ITextFileService, - @IExplorerService private readonly explorerService: IExplorerService + @IExplorerService private readonly explorerService: IExplorerService, + @IConfigurationService private readonly configurationService: IConfigurationService ) { - super(TextFileEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService, editorService, editorGroupService); + super(TextFileEditor.ID, telemetryService, instantiationService, storageService, textResourceConfigurationService, themeService, editorService, editorGroupService); this.updateRestoreViewStateConfiguration(); @@ -87,7 +89,7 @@ export class TextFileEditor extends BaseTextEditor { } private updateRestoreViewStateConfiguration(): void { - this.restoreViewState = this.textResourceConfigurationService.getValue(undefined, 'workbench.editor.restoreViewState'); + this.restoreViewState = this.configurationService.getValue('workbench.editor.restoreViewState') ?? true /* default */; } getTitle(): string { diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileEditorTracker.ts b/src/vs/workbench/contrib/files/browser/editors/textFileEditorTracker.ts index d15f6a57e81..9d6d291ea17 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileEditorTracker.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileEditorTracker.ts @@ -13,7 +13,6 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { RunOnceWorker } from 'vs/base/common/async'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { Schemas } from 'vs/base/common/network'; export class TextFileEditorTracker extends Disposable implements IWorkbenchContribution { @@ -45,7 +44,7 @@ export class TextFileEditorTracker extends Disposable implements IWorkbenchContr //#region Text File: Ensure every dirty text and untitled file is opened in an editor - private readonly ensureDirtyFilesAreOpenedWorker = this._register(new RunOnceWorker(units => this.ensureDirtyTextFilesAreOpened(units), 250)); + private readonly ensureDirtyFilesAreOpenedWorker = this._register(new RunOnceWorker(units => this.ensureDirtyTextFilesAreOpened(units), 50)); private ensureDirtyTextFilesAreOpened(resources: URI[]): void { this.doEnsureDirtyTextFilesAreOpened(distinct(resources.filter(resource => { @@ -58,7 +57,7 @@ export class TextFileEditorTracker extends Disposable implements IWorkbenchContr return false; // resource must not be pending to save } - if (this.editorService.isOpen(this.editorService.createInput({ resource, forceFile: resource.scheme !== Schemas.untitled, forceUntitled: resource.scheme === Schemas.untitled }))) { + if (this.editorService.isOpen({ resource })) { return false; // model must not be opened already as file } diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts b/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts index 2170391364a..c622bf0d5dd 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts @@ -100,7 +100,7 @@ export class TextFileSaveErrorHandler extends Disposable implements ISaveErrorHa } } - onSaveError(error: any, model: ITextFileEditorModel): void { + onSaveError(error: unknown, model: ITextFileEditorModel): void { const fileOperationError = error as FileOperationError; const resource = model.resource; @@ -208,8 +208,8 @@ class ResolveConflictLearnMoreAction extends Action { super('workbench.files.action.resolveConflictLearnMore', nls.localize('learnMore', "Learn More")); } - run(): Promise { - return this.openerService.open(URI.parse('https://go.microsoft.com/fwlink/?linkid=868264')); + async run(): Promise { + await this.openerService.open(URI.parse('https://go.microsoft.com/fwlink/?linkid=868264')); } } @@ -221,7 +221,7 @@ class DoNotShowResolveConflictLearnMoreAction extends Action { super('workbench.files.action.resolveConflictLearnMoreDoNotShowAgain', nls.localize('dontShowAgain', "Don't Show Again")); } - async run(notification: IDisposable): Promise { + async run(notification: IDisposable): Promise { this.storageService.store(LEARN_MORE_DIRTY_WRITE_IGNORE_KEY, true, StorageScope.GLOBAL); // Hide notification @@ -241,7 +241,7 @@ class ResolveSaveConflictAction extends Action { super('workbench.files.action.resolveConflict', nls.localize('compareChanges', "Compare")); } - async run(): Promise { + async run(): Promise { if (!this.model.isDisposed()) { const resource = this.model.resource; const name = basename(resource); @@ -272,7 +272,7 @@ class SaveElevatedAction extends Action { super('workbench.files.action.saveElevated', triedToMakeWriteable ? isWindows ? nls.localize('overwriteElevated', "Overwrite as Admin...") : nls.localize('overwriteElevatedSudo', "Overwrite as Sudo...") : isWindows ? nls.localize('saveElevated', "Retry as Admin...") : nls.localize('saveElevatedSudo', "Retry as Sudo...")); } - async run(): Promise { + async run(): Promise { if (!this.model.isDisposed()) { this.model.save({ writeElevated: true, @@ -291,7 +291,7 @@ class OverwriteReadonlyAction extends Action { super('workbench.files.action.overwrite', nls.localize('overwrite', "Overwrite")); } - async run(): Promise { + async run(): Promise { if (!this.model.isDisposed()) { this.model.save({ overwriteReadonly: true, reason: SaveReason.EXPLICIT }); } @@ -306,7 +306,7 @@ class SaveIgnoreModifiedSinceAction extends Action { super('workbench.files.action.saveIgnoreModifiedSince', nls.localize('overwrite', "Overwrite")); } - async run(): Promise { + async run(): Promise { if (!this.model.isDisposed()) { this.model.save({ ignoreModifiedSince: true, reason: SaveReason.EXPLICIT }); } @@ -321,7 +321,7 @@ class ConfigureSaveConflictAction extends Action { super('workbench.files.action.configureSaveConflict', nls.localize('configure', "Configure")); } - async run(): Promise { + async run(): Promise { this.preferencesService.openSettings(undefined, 'files.saveConflictResolution'); } } diff --git a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts index 720b01e66ed..dde38c2809c 100644 --- a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts +++ b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts @@ -34,7 +34,8 @@ 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 { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { WorkbenchStateContext, RemoteNameContext, IsWebContext } from 'vs/workbench/browser/contextkeys'; +import { WorkbenchStateContext, RemoteNameContext } from 'vs/workbench/browser/contextkeys'; +import { IsWebContext } from 'vs/platform/contextkey/common/contextkeys'; import { AddRootFolderAction, OpenFolderAction, OpenFileFolderAction } from 'vs/workbench/browser/actions/workspaceActions'; import { isMacintosh } from 'vs/base/common/platform'; @@ -65,21 +66,21 @@ export class ExplorerViewletViewsContribution extends Disposable implements IWor private registerViews(): void { const viewsRegistry = Registry.as(Extensions.ViewsRegistry); - viewsRegistry.registerViewWelcomeContent(EmptyView.ID, { + this._register(viewsRegistry.registerViewWelcomeContent(EmptyView.ID, { content: localize('noWorkspaceHelp', "You have not yet added a folder to the workspace.\n[Add Folder](command:{0})", AddRootFolderAction.ID), when: WorkbenchStateContext.isEqualTo('workspace') - }); + })); const commandId = isMacintosh ? OpenFileFolderAction.ID : OpenFolderAction.ID; - viewsRegistry.registerViewWelcomeContent(EmptyView.ID, { + this._register(viewsRegistry.registerViewWelcomeContent(EmptyView.ID, { content: localize('remoteNoFolderHelp', "Connected to remote.\n[Open Folder](command:{0})", commandId), when: ContextKeyExpr.and(WorkbenchStateContext.notEqualsTo('workspace'), RemoteNameContext.notEqualsTo(''), IsWebContext.toNegated()) - }); + })); - viewsRegistry.registerViewWelcomeContent(EmptyView.ID, { + this._register(viewsRegistry.registerViewWelcomeContent(EmptyView.ID, { content: localize('noFolderHelp', "You have not yet opened a folder.\n[Open Folder](command:{0})", commandId), when: ContextKeyExpr.or(ContextKeyExpr.and(WorkbenchStateContext.notEqualsTo('workspace'), RemoteNameContext.isEqualTo('')), ContextKeyExpr.and(WorkbenchStateContext.notEqualsTo('workspace'), IsWebContext)) - }); + })); const viewDescriptors = viewsRegistry.getViews(VIEW_CONTAINER); diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index 6876d783a34..4c21a261e15 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -12,7 +12,7 @@ import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/wor import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; import { openWindowCommand, COPY_PATH_COMMAND_ID, REVEAL_IN_EXPLORER_COMMAND_ID, OPEN_TO_SIDE_COMMAND_ID, REVERT_FILE_COMMAND_ID, SAVE_FILE_COMMAND_ID, SAVE_FILE_LABEL, SAVE_FILE_AS_COMMAND_ID, SAVE_FILE_AS_LABEL, SAVE_ALL_IN_GROUP_COMMAND_ID, OpenEditorsGroupContext, COMPARE_WITH_SAVED_COMMAND_ID, COMPARE_RESOURCE_COMMAND_ID, SELECT_FOR_COMPARE_COMMAND_ID, ResourceSelectedForCompareContext, DirtyEditorContext, COMPARE_SELECTED_COMMAND_ID, REMOVE_ROOT_FOLDER_COMMAND_ID, REMOVE_ROOT_FOLDER_LABEL, SAVE_FILES_COMMAND_ID, COPY_RELATIVE_PATH_COMMAND_ID, SAVE_FILE_WITHOUT_FORMATTING_COMMAND_ID, SAVE_FILE_WITHOUT_FORMATTING_LABEL, newWindowCommand, ReadonlyEditorContext } from 'vs/workbench/contrib/files/browser/fileCommands'; import { CommandsRegistry, ICommandHandler } from 'vs/platform/commands/common/commands'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { isMacintosh } from 'vs/base/common/platform'; import { FilesExplorerFocusCondition, ExplorerRootContext, ExplorerFolderContext, ExplorerResourceNotReadonlyContext, ExplorerResourceCut, IExplorerService, ExplorerResourceMoveableToTrash, ExplorerViewletVisibleContext } from 'vs/workbench/contrib/files/common/files'; @@ -22,7 +22,8 @@ import { AutoSaveAfterShortDelayContext } from 'vs/workbench/services/filesConfi import { ResourceContextKey } from 'vs/workbench/common/resources'; import { WorkbenchListDoubleSelection } from 'vs/platform/list/browser/listService'; import { Schemas } from 'vs/base/common/network'; -import { WorkspaceFolderCountContext, IsWebContext } from 'vs/workbench/browser/contextkeys'; +import { WorkspaceFolderCountContext } from 'vs/workbench/browser/contextkeys'; +import { IsWebContext } from 'vs/platform/contextkey/common/contextkeys'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { OpenFileFolderAction, OpenFileAction, OpenFolderAction, OpenWorkspaceAction } from 'vs/workbench/browser/actions/workspaceActions'; import { ActiveEditorIsReadonlyContext, DirtyWorkingCopiesContext, ActiveEditorContext } from 'vs/workbench/common/editor'; @@ -170,7 +171,7 @@ appendEditorTitleContextMenuItem(COPY_PATH_COMMAND_ID, copyPathCommand.title, Re appendEditorTitleContextMenuItem(COPY_RELATIVE_PATH_COMMAND_ID, copyRelativePathCommand.title, ResourceContextKey.IsFileSystemResource, '1_cutcopypaste'); appendEditorTitleContextMenuItem(REVEAL_IN_EXPLORER_COMMAND_ID, nls.localize('revealInSideBar', "Reveal in Side Bar"), ResourceContextKey.IsFileSystemResource); -export function appendEditorTitleContextMenuItem(id: string, title: string, when: ContextKeyExpr | undefined, group?: string): void { +export function appendEditorTitleContextMenuItem(id: string, title: string, when: ContextKeyExpression | undefined, group?: string): void { // Menu MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { @@ -200,7 +201,7 @@ function appendSaveConflictEditorTitleAction(id: string, title: string, icon: Th // Menu registration - command palette -export function appendToCommandPalette(id: string, title: ILocalizedString, category: ILocalizedString, when?: ContextKeyExpr): void { +export function appendToCommandPalette(id: string, title: ILocalizedString, category: ILocalizedString, when?: ContextKeyExpression): void { MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id, diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index 5c8be8905a0..2bc6d031192 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -100,7 +100,7 @@ export class NewFileAction extends Action { })); } - run(): Promise { + run(): Promise { return this.commandService.executeCommand(NEW_FILE_COMMAND_ID); } } @@ -122,7 +122,7 @@ export class NewFolderAction extends Action { })); } - run(): Promise { + run(): Promise { return this.commandService.executeCommand(NEW_FOLDER_COMMAND_ID); } } @@ -140,8 +140,8 @@ export class GlobalNewUntitledFileAction extends Action { super(id, label); } - run(): Promise { - return this.editorService.openEditor({ options: { pinned: true } }); // untitled are always pinned + async run(): Promise { + await this.editorService.openEditor({ options: { pinned: true } }); // untitled are always pinned } } @@ -436,7 +436,7 @@ export function incrementFileName(name: string, isFolder: boolean, incrementalNa // folder.1=>folder.2 if (isFolder && name.match(/(\d+)$/)) { - return name.replace(/(\d+)$/, (match: string, ...groups: any[]) => { + return name.replace(/(\d+)$/, (match, ...groups) => { let number = parseInt(groups[0]); return number < maxNumber ? strings.pad(number + 1, groups[0].length) @@ -446,7 +446,7 @@ export function incrementFileName(name: string, isFolder: boolean, incrementalNa // 1.folder=>2.folder if (isFolder && name.match(/^(\d+)/)) { - return name.replace(/^(\d+)(.*)$/, (match: string, ...groups: any[]) => { + return name.replace(/^(\d+)(.*)$/, (match, ...groups) => { let number = parseInt(groups[0]); return number < maxNumber ? strings.pad(number + 1, groups[0].length) + groups[1] @@ -474,7 +474,7 @@ export class GlobalCompareResourcesAction extends Action { super(id, label); } - async run(): Promise { + async run(): Promise { const activeInput = this.editorService.activeEditor; const activeResource = activeInput ? activeInput.resource : undefined; if (activeResource) { @@ -520,7 +520,7 @@ export class ToggleAutoSaveAction extends Action { super(id, label); } - run(): Promise { + run(): Promise { return this.filesConfigurationService.toggleAutoSave(); } } @@ -543,7 +543,7 @@ export abstract class BaseSaveAllAction extends Action { this.registerListeners(); } - protected abstract doRun(context: any): Promise; + protected abstract doRun(context: unknown): Promise; private registerListeners(): void { @@ -559,7 +559,7 @@ export abstract class BaseSaveAllAction extends Action { } } - async run(context?: any): Promise { + async run(context?: unknown): Promise { try { await this.doRun(context); } catch (error) { @@ -577,7 +577,7 @@ export class SaveAllAction extends BaseSaveAllAction { return 'explorer-action codicon-save-all'; } - protected doRun(context: any): Promise { + protected doRun(): Promise { return this.commandService.executeCommand(SAVE_ALL_COMMAND_ID); } } @@ -591,7 +591,7 @@ export class SaveAllInGroupAction extends BaseSaveAllAction { return 'explorer-action codicon-save-all'; } - protected doRun(context: any): Promise { + protected doRun(context: unknown): Promise { return this.commandService.executeCommand(SAVE_ALL_IN_GROUP_COMMAND_ID, {}, context); } } @@ -605,7 +605,7 @@ export class CloseGroupAction extends Action { super(id, label, 'codicon-close-all'); } - run(context?: any): Promise { + run(context?: unknown): Promise { return this.commandService.executeCommand(CLOSE_EDITORS_AND_GROUP_COMMAND_ID, {}, context); } } @@ -623,8 +623,8 @@ export class FocusFilesExplorer extends Action { super(id, label); } - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true); + async run(): Promise { + await this.viewletService.openViewlet(VIEWLET_ID, true); } } @@ -643,15 +643,13 @@ export class ShowActiveFileInExplorer extends Action { super(id, label); } - async run(): Promise { + async run(): Promise { const resource = toResource(this.editorService.activeEditor, { supportSideBySide: SideBySideEditor.MASTER }); if (resource) { this.commandService.executeCommand(REVEAL_IN_EXPLORER_COMMAND_ID, resource); } else { this.notificationService.info(nls.localize('openFileToShow', "Open a file first to show it in the explorer")); } - - return true; } } @@ -672,7 +670,7 @@ export class CollapseExplorerView extends Action { })); } - async run(): Promise { + async run(): Promise { const explorerViewlet = (await this.viewletService.openViewlet(VIEWLET_ID))?.getViewPaneContainer() as ExplorerViewPaneContainer; const explorerView = explorerViewlet.getExplorerView(); if (explorerView) { @@ -699,7 +697,7 @@ export class RefreshExplorerView extends Action { })); } - async run(): Promise { + async run(): Promise { await this.viewletService.openViewlet(VIEWLET_ID); this.explorerService.refresh(); } @@ -721,7 +719,7 @@ export class ShowOpenedFileInNewWindow extends Action { super(id, label); } - async run(): Promise { + async run(): Promise { const fileResource = toResource(this.editorService.activeEditor, { supportSideBySide: SideBySideEditor.MASTER }); if (fileResource) { if (this.fileService.canHandleResource(fileResource)) { @@ -732,8 +730,6 @@ export class ShowOpenedFileInNewWindow extends Action { } else { this.notificationService.info(nls.localize('openFileToShowInNewWindow.nofile', "Open a file first to open in new window")); } - - return true; } } @@ -817,7 +813,7 @@ export class CompareWithClipboardAction extends Action { this.enabled = true; } - async run(): Promise { + async run(): Promise { const resource = toResource(this.editorService.activeEditor, { supportSideBySide: SideBySideEditor.MASTER }); if (resource && (this.fileService.canHandleResource(resource) || resource.scheme === Schemas.untitled)) { if (!this.registrationDisposal) { @@ -828,13 +824,11 @@ export class CompareWithClipboardAction extends Action { const name = resources.basename(resource); const editorLabel = nls.localize('clipboardComparisonLabel', "Clipboard ↔ {0}", name); - return this.editorService.openEditor({ leftResource: resource.with({ scheme: CompareWithClipboardAction.SCHEME }), rightResource: resource, label: editorLabel }).finally(() => { + await this.editorService.openEditor({ leftResource: resource.with({ scheme: CompareWithClipboardAction.SCHEME }), rightResource: resource, label: editorLabel }).finally(() => { dispose(this.registrationDisposal); this.registrationDisposal = undefined; }); } - - return true; } dispose(): void { @@ -859,7 +853,7 @@ class ClipboardContentProvider implements ITextModelContentProvider { } } -function onErrorWithRetry(notificationService: INotificationService, error: any, retry: () => Promise): void { +function onErrorWithRetry(notificationService: INotificationService, error: unknown, retry: () => Promise): void { notificationService.prompt(Severity.Error, toErrorMessage(error, false), [{ label: nls.localize('retry', "Retry"), diff --git a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css index e7e32d6ff20..04e0bf24825 100644 --- a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css +++ b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css @@ -55,11 +55,11 @@ align-items: center; } -.explorer-viewlet .pane-header .monaco-count-badge.hidden { +.pane-header .dirty-count.monaco-count-badge.hidden { display: none; } -.explorer-viewlet .monaco-count-badge { +.dirty-count.monaco-count-badge { padding: 1px 6px 2px; margin-left: 6px; min-height: auto; diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index c388727aba4..ebf9f42d42f 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -185,7 +185,7 @@ export class OpenEditorsView extends ViewPane { super.renderHeaderTitle(container, this.title); const count = dom.append(container, $('.count')); - this.dirtyCountElement = dom.append(count, $('.monaco-count-badge')); + this.dirtyCountElement = dom.append(count, $('.dirty-count.monaco-count-badge')); this._register((attachStylerCallback(this.themeService, { badgeBackground, badgeForeground, contrastBorder }, colors => { const background = colors.badgeBackground ? colors.badgeBackground.toString() : ''; @@ -491,9 +491,9 @@ interface IEditorGroupTemplateData { class OpenEditorActionRunner extends ActionRunner { public editor: OpenEditor | undefined; - run(action: IAction, context?: any): Promise { + async run(action: IAction): Promise { if (!this.editor) { - return Promise.resolve(); + return; } return super.run(action, { groupId: this.editor.groupId, editorIndex: this.editor.editorIndex }); @@ -677,7 +677,7 @@ class OpenEditorsDragAndDrop implements IListDragAndDrop { disposables = []; }); - test('editor auto saves after short delay if configured', async function () { + async function createEditorAutoSave(autoSaveConfig: object): Promise<[TestServiceAccessor, EditorPart, EditorAutoSave]> { const instantiationService = workbenchInstantiationService(); const configurationService = new TestConfigurationService(); - configurationService.setUserConfiguration('files', { autoSave: 'afterDelay', autoSaveDelay: 1 }); + configurationService.setUserConfiguration('files', autoSaveConfig); instantiationService.stub(IConfigurationService, configurationService); instantiationService.stub(IFilesConfigurationService, new TestFilesConfigurationService( @@ -73,10 +73,15 @@ suite('EditorAutoSave', () => { const editorAutoSave = instantiationService.createInstance(EditorAutoSave); + return [accessor, part, editorAutoSave]; + } + + test('editor auto saves after short delay if configured', async function () { + const [accessor, part, editorAutoSave] = await createEditorAutoSave({ autoSave: 'afterDelay', autoSaveDelay: 1 }); + const resource = toResource.call(this, '/path/index.txt'); const model = await accessor.textFileService.files.resolve(resource) as IResolvedTextFileEditorModel; - model.textEditorModel.setValue('Super Good'); assert.ok(model.isDirty()); @@ -90,6 +95,28 @@ suite('EditorAutoSave', () => { (accessor.textFileService.files).dispose(); }); + test('editor auto saves on focus change if configured', async function () { + const [accessor, part, editorAutoSave] = await createEditorAutoSave({ autoSave: 'onFocusChange' }); + + const resource = toResource.call(this, '/path/index.txt'); + await accessor.editorService.openEditor({ resource, forceFile: true }); + + const model = await accessor.textFileService.files.resolve(resource) as IResolvedTextFileEditorModel; + model.textEditorModel.setValue('Super Good'); + + assert.ok(model.isDirty()); + + await accessor.editorService.openEditor({ resource: toResource.call(this, '/path/index_other.txt') }); + + await awaitModelSaved(model); + + assert.ok(!model.isDirty()); + + part.dispose(); + editorAutoSave.dispose(); + (accessor.textFileService.files).dispose(); + }); + function awaitModelSaved(model: ITextFileEditorModel): Promise { return new Promise(c => { Event.once(model.onDidChangeDirty)(c); 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 9aaea5c645b..5b78f85f045 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts @@ -16,6 +16,7 @@ import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textF import { timeout } from 'vs/base/common/async'; import { ModesRegistry, PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; suite('Files - FileEditorInput', () => { let instantiationService: IInstantiationService; @@ -148,7 +149,7 @@ suite('Files - FileEditorInput', () => { resolved.textEditorModel!.setValue('changed'); assert.ok(input.isDirty()); - assert.ok(await input.revert(0)); + await input.revert(0); assert.ok(!input.isDirty()); input.dispose(); @@ -196,4 +197,19 @@ suite('Files - FileEditorInput', () => { input.dispose(); listener.dispose(); }); + + test('force open text/binary', async function () { + const input = instantiationService.createInstance(FileEditorInput, toResource.call(this, '/foo/bar/updatefile.js'), undefined, undefined); + input.setForceOpenAsBinary(); + + let resolved = await input.resolve(); + assert.ok(resolved instanceof BinaryEditorModel); + + input.setForceOpenAsText(); + + resolved = await input.resolve(); + assert.ok(resolved instanceof TextFileEditorModel); + + resolved.dispose(); + }); }); diff --git a/src/vs/workbench/contrib/files/test/browser/textFileEditor.test.ts b/src/vs/workbench/contrib/files/test/browser/textFileEditor.test.ts new file mode 100644 index 00000000000..a58dd887f65 --- /dev/null +++ b/src/vs/workbench/contrib/files/test/browser/textFileEditor.test.ts @@ -0,0 +1,110 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { toResource } from 'vs/base/test/common/utils'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { workbenchInstantiationService, TestServiceAccessor, TestFilesConfigurationService, TestEnvironmentService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { TextFileEditor } from 'vs/workbench/contrib/files/browser/editors/textFileEditor'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { EditorInput } from 'vs/workbench/common/editor'; +import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; +import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; +import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { Selection } from 'vs/editor/common/core/selection'; +import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; + +suite('Files - TextFileEditor', () => { + + let disposables: IDisposable[] = []; + + setup(() => { + disposables.push(Registry.as(EditorExtensions.Editors).registerEditor( + EditorDescriptor.create( + TextFileEditor, + TextFileEditor.ID, + 'Text File Editor' + ), + [new SyncDescriptor(FileEditorInput)] + )); + }); + + teardown(() => { + dispose(disposables); + disposables = []; + }); + + async function createPart(restoreViewState: boolean): Promise<[EditorPart, TestServiceAccessor, IInstantiationService, IEditorService]> { + const instantiationService = workbenchInstantiationService(); + + const configurationService = new TestConfigurationService(); + configurationService.setUserConfiguration('workbench', { editor: { restoreViewState } }); + instantiationService.stub(IConfigurationService, configurationService); + + instantiationService.stub(IFilesConfigurationService, new TestFilesConfigurationService( + instantiationService.createInstance(MockContextKeyService), + configurationService, + TestEnvironmentService + )); + + 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); + + const accessor = instantiationService.createInstance(TestServiceAccessor); + + await part.whenRestored; + + return [part, accessor, instantiationService, editorService]; + } + + test('text file editor preserves viewstate', async function () { + return viewStateTest(this, true); + }); + + test('text file editor resets viewstate if configured as such', async function () { + return viewStateTest(this, false); + }); + + async function viewStateTest(context: Mocha.ITestCallbackContext, restoreViewState: boolean): Promise { + const [part, accessor] = await createPart(restoreViewState); + + let editor = await accessor.editorService.openEditor(accessor.editorService.createInput({ resource: toResource.call(context, '/path/index.txt'), forceFile: true })); + + let codeEditor = editor?.getControl() as CodeEditorWidget; + const selection = new Selection(1, 3, 1, 4); + codeEditor.setSelection(selection); + + editor = await accessor.editorService.openEditor(accessor.editorService.createInput({ resource: toResource.call(context, '/path/index-other.txt'), forceFile: true })); + editor = await accessor.editorService.openEditor(accessor.editorService.createInput({ resource: toResource.call(context, '/path/index.txt'), forceFile: true })); + + codeEditor = editor?.getControl() as CodeEditorWidget; + + if (restoreViewState) { + assert.ok(codeEditor.getSelection()?.equalsSelection(selection)); + } else { + assert.ok(!codeEditor.getSelection()?.equalsSelection(selection)); + } + + part.dispose(); + (accessor.textFileService.files).dispose(); + } +}); diff --git a/src/vs/workbench/contrib/files/test/browser/fileEditorTracker.test.ts b/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts similarity index 83% rename from src/vs/workbench/contrib/files/test/browser/fileEditorTracker.test.ts rename to src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts index d2b3385e6df..081e0d02d1a 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileEditorTracker.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts @@ -9,7 +9,7 @@ import { TextFileEditorTracker } from 'vs/workbench/contrib/files/browser/editor import { toResource } from 'vs/base/test/common/utils'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; -import { IResolvedTextFileEditorModel, snapshotToString } from 'vs/workbench/services/textfile/common/textfiles'; +import { IResolvedTextFileEditorModel, snapshotToString, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { FileChangesEvent, FileChangeType } from 'vs/platform/files/common/files'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { timeout } from 'vs/base/common/async'; @@ -25,6 +25,8 @@ import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; +import { isEqual } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; suite('Files - TextFileEditorTracker', () => { @@ -46,32 +48,6 @@ suite('Files - TextFileEditorTracker', () => { disposables = []; }); - test('file change event updates model', async function () { - const instantiationService = workbenchInstantiationService(); - const accessor = instantiationService.createInstance(TestServiceAccessor); - - const tracker = instantiationService.createInstance(TextFileEditorTracker); - - const resource = toResource.call(this, '/path/index.txt'); - - const model = await accessor.textFileService.files.resolve(resource) as IResolvedTextFileEditorModel; - - model.textEditorModel.setValue('Super Good'); - assert.equal(snapshotToString(model.createSnapshot()!), 'Super Good'); - - await model.save(); - - // change event (watcher) - accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.UPDATED }])); - - await timeout(0); // due to event updating model async - - assert.equal(snapshotToString(model.createSnapshot()!), 'Hello Html'); - - tracker.dispose(); - (accessor.textFileService.files).dispose(); - }); - async function createTracker(): Promise<[EditorPart, TestServiceAccessor, TextFileEditorTracker, IInstantiationService, IEditorService]> { const instantiationService = workbenchInstantiationService(); @@ -93,6 +69,29 @@ suite('Files - TextFileEditorTracker', () => { return [part, accessor, tracker, instantiationService, editorService]; } + test('file change event updates model', async function () { + const [, accessor, tracker] = await createTracker(); + + const resource = toResource.call(this, '/path/index.txt'); + + const model = await accessor.textFileService.files.resolve(resource) as IResolvedTextFileEditorModel; + + model.textEditorModel.setValue('Super Good'); + assert.equal(snapshotToString(model.createSnapshot()!), 'Super Good'); + + await model.save(); + + // change event (watcher) + accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.UPDATED }])); + + await timeout(0); // due to event updating model async + + assert.equal(snapshotToString(model.createSnapshot()!), 'Hello Html'); + + tracker.dispose(); + (accessor.textFileService.files).dispose(); + }); + test('dirty text file model opens as editor', async function () { const [part, accessor, tracker] = await createTracker(); @@ -135,4 +134,32 @@ suite('Files - TextFileEditorTracker', () => { Event.once(editorService.onDidActiveEditorChange)(c); }); } + + test('non-dirty files reload on window focus', async function () { + const [part, accessor, tracker] = await createTracker(); + + const resource = toResource.call(this, '/path/index.txt'); + + await accessor.editorService.openEditor(accessor.editorService.createInput({ resource, forceFile: true })); + + accessor.hostService.setFocus(false); + accessor.hostService.setFocus(true); + + await awaitModelLoadEvent(accessor.textFileService, resource); + + part.dispose(); + tracker.dispose(); + (accessor.textFileService.files).dispose(); + }); + + function awaitModelLoadEvent(textFileService: ITextFileService, resource: URI): Promise { + return new Promise(c => { + const listener = textFileService.files.onDidLoad(e => { + if (isEqual(e.model.resource, resource)) { + listener.dispose(); + c(); + } + }); + }); + } }); diff --git a/src/vs/workbench/contrib/issue/electron-browser/issueService.ts b/src/vs/workbench/contrib/issue/electron-browser/issueService.ts index 6b5b4068648..e184076085a 100644 --- a/src/vs/workbench/contrib/issue/electron-browser/issueService.ts +++ b/src/vs/workbench/contrib/issue/electron-browser/issueService.ts @@ -14,6 +14,7 @@ import { assign } from 'vs/base/common/objects'; import { IWorkbenchIssueService } from 'vs/workbench/contrib/issue/electron-browser/issue'; import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; export class WorkbenchIssueService implements IWorkbenchIssueService { _serviceBrand: undefined; @@ -23,7 +24,7 @@ export class WorkbenchIssueService implements IWorkbenchIssueService { @IThemeService private readonly themeService: IThemeService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService + @IWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService ) { } openReporter(dataOverrides: Partial = {}): Promise { diff --git a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts index abd5d1d00fe..b1ec26e5c46 100644 --- a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts +++ b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts @@ -856,6 +856,7 @@ export class ResourceDragAndDrop implements ITreeDragAndDrop { registerThemingParticipant((theme, collector) => { const linkFg = theme.getColor(textLinkForeground); if (linkFg) { - collector.addRule(`.markers-panel .markers-panel-container .tree-container .monaco-tl-contents .details-container a.code-link span:hover { color: ${linkFg}; }`); + collector.addRule(`.markers-panel .markers-panel-container .tree-container .monaco-tl-contents .details-container a.code-link .marker-code > span:hover { color: ${linkFg}; }`); + collector.addRule(`.markers-panel .markers-panel-container .tree-container .monaco-list:focus .monaco-tl-contents .details-container a.code-link .marker-code > span:hover { color: inherit; }`); } }); diff --git a/src/vs/workbench/contrib/markers/browser/markersView.ts b/src/vs/workbench/contrib/markers/browser/markersView.ts index 9b4453be046..bb4a5763759 100644 --- a/src/vs/workbench/contrib/markers/browser/markersView.ts +++ b/src/vs/workbench/contrib/markers/browser/markersView.ts @@ -349,7 +349,6 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { private createArialLabelElement(parent: HTMLElement): void { this.ariaLabelElement = dom.append(parent, dom.$('')); this.ariaLabelElement.setAttribute('id', 'markers-panel-arialabel'); - this.ariaLabelElement.setAttribute('aria-live', 'polite'); } private createTree(parent: HTMLElement): void { diff --git a/src/vs/workbench/contrib/output/browser/output.contribution.ts b/src/vs/workbench/contrib/output/browser/output.contribution.ts index 90a0493c1e3..fc1a2c61224 100644 --- a/src/vs/workbench/contrib/output/browser/output.contribution.ts +++ b/src/vs/workbench/contrib/output/browser/output.contribution.ts @@ -23,6 +23,7 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ViewContainer, IViewContainersRegistry, ViewContainerLocation, Extensions as ViewContainerExtensions, IViewsRegistry } from 'vs/workbench/common/views'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; // Register Service registerSingleton(IOutputService, OutputService); @@ -145,3 +146,19 @@ MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { }, order: 1 }); + +Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ + id: 'output', + order: 30, + title: nls.localize('output', "Output"), + type: 'object', + properties: { + 'output.smartScroll.enabled': { + type: 'boolean', + description: nls.localize('output.smartScroll.enabled', "Enable/disable the ability of smart scrolling in the output view. Smart scrolling allows you to lock scrolling automatically when you click in the output view and unlocks when you click in the last line."), + default: true, + scope: ConfigurationScope.APPLICATION, + tags: ['output'] + } + } +}); diff --git a/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index aebdb85e8b8..bcc70dcadcf 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -90,6 +90,10 @@ export class OutputViewPane extends ViewPane { return; } + if (!this.configurationService.getValue('output.smartScroll.enabled')) { + return; + } + const model = codeEditor.getModel(); if (model && this.actions) { const newPositionLine = e.position.lineNumber; diff --git a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index 8e94c2c2236..b450943ee2a 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts @@ -20,7 +20,8 @@ import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { IsMacNativeContext, RemoteNameContext, WorkbenchStateContext } from 'vs/workbench/browser/contextkeys'; +import { RemoteNameContext, WorkbenchStateContext } from 'vs/workbench/browser/contextkeys'; +import { IsMacNativeContext } from 'vs/platform/contextkey/common/contextkeys'; import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor'; import { Extensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts b/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts index 76786b81061..c1812459c6b 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts @@ -252,7 +252,7 @@ export class PreferencesEditor extends BaseEditor { if (this.editorService.activeControl !== this) { this.focus(); } - const promise: Promise = this.input && this.input.isDirty() ? this.editorService.save({ editor: this.input, groupId: this.group!.id }) : Promise.resolve(true); + const promise = this.input && this.input.isDirty() ? this.editorService.save({ editor: this.input, groupId: this.group!.id }) : Promise.resolve(true); promise.then(() => { if (target === ConfigurationTarget.USER_LOCAL) { this.preferencesService.switchSettings(ConfigurationTarget.USER_LOCAL, this.preferencesService.userSettingsResource, true); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts index fb75f474a2a..4763193df8c 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts @@ -165,6 +165,11 @@ export const tocData: ITOCEntry = { label: localize('problems', "Problems"), settings: ['problems.*'] }, + { + id: 'features/output', + label: localize('output', "Output"), + settings: ['output.*'] + }, { id: 'features/comments', label: localize('comments', "Comments"), diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index c1a674828f9..bae1aee1cd5 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -1238,7 +1238,10 @@ export class SettingTreeRenderers { private getActionsForSetting(setting: ISetting): IAction[] { const enableSync = this._userDataSyncEnablementService.isEnabled(); return enableSync && !setting.disallowSyncIgnore ? - [this._instantiationService.createInstance(SyncSettingAction, setting)] : + [ + new Separator(), + this._instantiationService.createInstance(SyncSettingAction, setting) + ] : []; } 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 eb03c3f76d7..67df9f76198 100644 --- a/src/vs/workbench/contrib/remote/electron-browser/remote.contribution.ts +++ b/src/vs/workbench/contrib/remote/electron-browser/remote.contribution.ts @@ -38,6 +38,7 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { RemoteConnectionState, Deprecated_RemoteAuthorityContext, RemoteFileDialogContext } from 'vs/workbench/browser/contextkeys'; import { IDownloadService } from 'vs/platform/download/common/download'; import { OpenLocalFileFolderCommand, OpenLocalFileCommand, OpenLocalFolderCommand, SaveLocalFileCommand } from 'vs/workbench/services/dialogs/browser/simpleFileDialog'; +import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; const WINDOW_ACTIONS_COMMAND_ID = 'workbench.action.remote.showMenu'; const CLOSE_REMOTE_COMMAND_ID = 'workbench.action.remote.close'; @@ -331,7 +332,7 @@ class RemoteTelemetryEnablementUpdater extends Disposable implements IWorkbenchC class RemoteEmptyWorkbenchPresentation extends Disposable implements IWorkbenchContribution { constructor( - @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IWorkbenchEnvironmentService environmentService: INativeWorkbenchEnvironmentService, @IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService, @IConfigurationService configurationService: IConfigurationService, @ICommandService commandService: ICommandService, diff --git a/src/vs/workbench/contrib/scm/browser/repositoryPane.ts b/src/vs/workbench/contrib/scm/browser/repositoryPane.ts index e13d4f79beb..9d926605116 100644 --- a/src/vs/workbench/contrib/scm/browser/repositoryPane.ts +++ b/src/vs/workbench/contrib/scm/browser/repositoryPane.ts @@ -67,6 +67,9 @@ import { SuggestController } from 'vs/editor/contrib/suggest/suggestController'; import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; import { Schemas } from 'vs/base/common/network'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { ModesHoverController } from 'vs/editor/contrib/hover/hover'; +import { ColorDetector } from 'vs/editor/contrib/colorPicker/colorDetector'; +import { LinkDetector } from 'vs/editor/contrib/links/links'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -747,6 +750,9 @@ export class RepositoryPane extends ViewPane { MenuPreventer.ID, SelectionClipboardContributionID, ContextMenuController.ID, + ColorDetector.ID, + ModesHoverController.ID, + LinkDetector.ID ]) }; diff --git a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts index b78b35610e7..1d3fdd35ad5 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts @@ -44,7 +44,7 @@ export interface ISpliceEvent { export class EmptyPane extends ViewPane { static readonly ID = 'workbench.scm'; - static readonly TITLE = localize('scm providers', "Source Control Providers"); + static readonly TITLE = localize('scm', "Source Control"); constructor( options: IViewPaneOptions, diff --git a/src/vs/workbench/contrib/search/browser/media/searchview.css b/src/vs/workbench/contrib/search/browser/media/searchview.css index c8f01bd98a3..75b34a17bdb 100644 --- a/src/vs/workbench/contrib/search/browser/media/searchview.css +++ b/src/vs/workbench/contrib/search/browser/media/searchview.css @@ -6,6 +6,7 @@ .search-view .search-widgets-container { margin: 0px 12px 0 2px; padding-top: 6px; + padding-bottom: 6px; } .search-view .search-widget .toggle-replace-button { @@ -132,7 +133,7 @@ } .search-view .query-details.more .file-types:last-child { - padding-bottom: 10px; + padding-bottom: 4px; } .search-view .query-details.more h4 { diff --git a/src/vs/workbench/contrib/search/browser/openFileHandler.ts b/src/vs/workbench/contrib/search/browser/openFileHandler.ts index e3c5e565472..f4a5f62c36a 100644 --- a/src/vs/workbench/contrib/search/browser/openFileHandler.ts +++ b/src/vs/workbench/contrib/search/browser/openFileHandler.ts @@ -167,7 +167,11 @@ export class OpenFileHandler extends QuickOpenHandler { } else { - complete = await this.searchService.fileSearch(this.queryBuilder.file(this.contextService.getWorkspace().folders.map(folder => folder.uri), queryOptions), token); + let fileQuery = this.queryBuilder.file( + this.contextService.getWorkspace().folders, + queryOptions + ); + complete = await this.searchService.fileSearch(fileQuery, token); } const results: QuickOpenEntry[] = []; @@ -238,10 +242,7 @@ export class OpenFileHandler extends QuickOpenHandler { sortByScore: true, }; - const folderResources = this.contextService.getWorkspace().folders.map(folder => folder.uri); - const query = this.queryBuilder.file(folderResources, options); - - return query; + return this.queryBuilder.file(this.contextService.getWorkspace().folders, options); } get isCacheLoaded(): boolean { diff --git a/src/vs/workbench/contrib/search/common/queryBuilder.ts b/src/vs/workbench/contrib/search/common/queryBuilder.ts index ee91d6edcf2..92c69e2691d 100644 --- a/src/vs/workbench/contrib/search/common/queryBuilder.ts +++ b/src/vs/workbench/contrib/search/common/queryBuilder.ts @@ -16,7 +16,7 @@ import { isMultilineRegexSource } from 'vs/editor/common/model/textModelSearch'; import * as nls from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService, WorkbenchState, toWorkspaceFolder, IWorkspaceFolderData } from 'vs/platform/workspace/common/workspace'; import { getExcludes, ICommonQueryProps, IFileQuery, IFolderQuery, IPatternInfo, ISearchConfiguration, ITextQuery, ITextSearchPreviewOptions, pathIncludedInQuery, QueryType } from 'vs/workbench/services/search/common/search'; import { Schemas } from 'vs/base/common/network'; @@ -94,7 +94,7 @@ export class QueryBuilder { return !folderConfig.search.useRipgrep; }); - const commonQuery = this.commonQuery(folderResources, options); + const commonQuery = this.commonQuery(folderResources?.map(toWorkspaceFolder), options); return { ...commonQuery, type: QueryType.Text, @@ -134,8 +134,8 @@ export class QueryBuilder { return newPattern; } - file(folderResources: uri[] | undefined, options: IFileQueryBuilderOptions = {}): IFileQuery { - const commonQuery = this.commonQuery(folderResources, options); + file(folders: IWorkspaceFolderData[], options: IFileQueryBuilderOptions = {}): IFileQuery { + const commonQuery = this.commonQuery(folders, options); return { ...commonQuery, type: QueryType.File, @@ -144,11 +144,11 @@ export class QueryBuilder { : options.filePattern, exists: options.exists, sortByScore: options.sortByScore, - cacheKey: options.cacheKey + cacheKey: options.cacheKey, }; } - private commonQuery(folderResources: uri[] = [], options: ICommonQueryBuilderOptions = {}): ICommonQueryProps { + private commonQuery(folderResources: IWorkspaceFolderData[] = [], options: ICommonQueryBuilderOptions = {}): ICommonQueryProps { let includeSearchPathsInfo: ISearchPathsInfo = {}; if (options.includePattern) { const includePattern = normalizeSlashes(options.includePattern); @@ -166,9 +166,10 @@ export class QueryBuilder { } // Build folderQueries from searchPaths, if given, otherwise folderResources + const includeFolderName = folderResources.length > 1; const folderQueries = (includeSearchPathsInfo.searchPaths && includeSearchPathsInfo.searchPaths.length ? includeSearchPathsInfo.searchPaths.map(searchPath => this.getFolderQueryForSearchPath(searchPath, options, excludeSearchPathsInfo)) : - folderResources.map(uri => this.getFolderQueryForRoot(uri, options, excludeSearchPathsInfo))) + folderResources.map(folder => this.getFolderQueryForRoot(folder, options, excludeSearchPathsInfo, includeFolderName))) .filter(query => !!query) as IFolderQuery[]; const queryProps: ICommonQueryProps = { @@ -403,7 +404,7 @@ export class QueryBuilder { } private getFolderQueryForSearchPath(searchPath: ISearchPathPattern, options: ICommonQueryBuilderOptions, searchPathExcludes: ISearchPathsInfo): IFolderQuery | null { - const rootConfig = this.getFolderQueryForRoot(searchPath.searchPath, options, searchPathExcludes); + const rootConfig = this.getFolderQueryForRoot(toWorkspaceFolder(searchPath.searchPath), options, searchPathExcludes, false); if (!rootConfig) { return null; } @@ -416,10 +417,10 @@ export class QueryBuilder { }; } - private getFolderQueryForRoot(folder: uri, options: ICommonQueryBuilderOptions, searchPathExcludes: ISearchPathsInfo): IFolderQuery | null { + private getFolderQueryForRoot(folder: IWorkspaceFolderData, options: ICommonQueryBuilderOptions, searchPathExcludes: ISearchPathsInfo, includeFolderName: boolean): IFolderQuery | null { let thisFolderExcludeSearchPathPattern: glob.IExpression | undefined; if (searchPathExcludes.searchPaths) { - const thisFolderExcludeSearchPath = searchPathExcludes.searchPaths.filter(sp => isEqual(sp.searchPath, folder))[0]; + const thisFolderExcludeSearchPath = searchPathExcludes.searchPaths.filter(sp => isEqual(sp.searchPath, folder.uri))[0]; if (thisFolderExcludeSearchPath && !thisFolderExcludeSearchPath.pattern) { // entire folder is excluded return null; @@ -428,7 +429,7 @@ export class QueryBuilder { } } - const folderConfig = this.configurationService.getValue({ resource: folder }); + const folderConfig = this.configurationService.getValue({ resource: folder.uri }); const settingExcludes = this.getExcludesForFolder(folderConfig, options); const excludePattern: glob.IExpression = { ...(settingExcludes || {}), @@ -436,7 +437,8 @@ export class QueryBuilder { }; return { - folder, + folder: folder.uri, + folderName: includeFolderName ? folder.name : undefined, excludePattern: Object.keys(excludePattern).length > 0 ? excludePattern : undefined, fileEncoding: folderConfig.files && folderConfig.files.encoding, disregardIgnoreFiles: typeof options.disregardIgnoreFiles === 'boolean' ? options.disregardIgnoreFiles : !folderConfig.search.useIgnoreFiles, diff --git a/src/vs/workbench/contrib/search/common/searchModel.ts b/src/vs/workbench/contrib/search/common/searchModel.ts index bea090b9701..1505579db92 100644 --- a/src/vs/workbench/contrib/search/common/searchModel.ts +++ b/src/vs/workbench/contrib/search/common/searchModel.ts @@ -141,6 +141,15 @@ export class Match { return thisMatchPreviewLines.join('\n'); } + rangeInPreview() { + // convert to editor's base 1 positions. + return { + ...this._fullPreviewRange, + startColumn: this._fullPreviewRange.startColumn + 1, + endColumn: this._fullPreviewRange.endColumn + 1 + }; + } + fullPreviewLines(): string[] { return this._fullPreviewLines.slice(this._fullPreviewRange.startLineNumber, this._fullPreviewRange.endLineNumber + 1); } diff --git a/src/vs/workbench/contrib/search/test/browser/queryBuilder.test.ts b/src/vs/workbench/contrib/search/test/browser/queryBuilder.test.ts index 26f6e129337..621cf435ac4 100644 --- a/src/vs/workbench/contrib/search/test/browser/queryBuilder.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/queryBuilder.test.ts @@ -25,6 +25,7 @@ suite('QueryBuilder', () => { const PATTERN_INFO: IPatternInfo = { pattern: 'a' }; const ROOT_1 = fixPath('/foo/root1'); const ROOT_1_URI = getUri(ROOT_1); + const ROOT_1_NAMED_FOLDER = toWorkspaceFolder(ROOT_1_URI); const WS_CONFIG_PATH = getUri('/bar/test.code-workspace'); // location of the workspace file (not important except that it is a file URI) let instantiationService: TestInstantiationService; @@ -89,7 +90,10 @@ suite('QueryBuilder', () => { test('does not split glob pattern when expandPatterns disabled', () => { assertEqualQueries( - queryBuilder.file([ROOT_1_URI], { includePattern: '**/foo, **/bar' }), + queryBuilder.file( + [ROOT_1_NAMED_FOLDER], + { includePattern: '**/foo, **/bar' }, + ), { folderQueries: [{ folder: ROOT_1_URI @@ -362,7 +366,7 @@ suite('QueryBuilder', () => { const content = 'content'; assertEqualQueries( queryBuilder.file( - undefined, + [], { filePattern: ` ${content} ` } ), { @@ -902,10 +906,13 @@ suite('QueryBuilder', () => { suite('file', () => { test('simple file query', () => { const cacheKey = 'asdf'; - const query = queryBuilder.file([ROOT_1_URI], { - cacheKey, - sortByScore: true - }); + const query = queryBuilder.file( + [ROOT_1_NAMED_FOLDER], + { + cacheKey, + sortByScore: true + }, + ); assert.equal(query.folderQueries.length, 1); assert.equal(query.cacheKey, cacheKey); diff --git a/src/vs/workbench/contrib/searchEditor/browser/constants.ts b/src/vs/workbench/contrib/searchEditor/browser/constants.ts index 7b1b1d34825..7de8978e745 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/constants.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/constants.ts @@ -19,5 +19,8 @@ export const SelectAllSearchEditorMatchesCommandId = 'selectAllSearchEditorMatch export const InSearchEditor = new RawContextKey('inSearchEditor', false); export const SearchEditorScheme = 'search-editor'; +export const SearchEditorBodyScheme = 'search-editor-body'; export const SearchEditorFindMatchClass = 'seaarchEditorFindMatch'; + +export const SearchEditorID = 'workbench.editor.searchEditor'; diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts index 1f10071bcfd..eb2646630b4 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts @@ -9,7 +9,7 @@ import { endsWith } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { ToggleCaseSensitiveKeybinding, ToggleRegexKeybinding, ToggleWholeWordKeybinding } from 'vs/editor/contrib/find/findModel'; import { localize } from 'vs/nls'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -20,16 +20,16 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor'; import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; -import { Extensions as EditorInputExtensions, IEditorInputFactory, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { Extensions as EditorInputExtensions, IEditorInputFactory, IEditorInputFactoryRegistry, ActiveEditorContext } from 'vs/workbench/common/editor'; import * as SearchConstants from 'vs/workbench/contrib/search/common/constants'; import * as SearchEditorConstants from 'vs/workbench/contrib/searchEditor/browser/constants'; import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditor'; -import { OpenResultsInEditorAction, OpenSearchEditorAction, toggleSearchEditorCaseSensitiveCommand, toggleSearchEditorContextLinesCommand, toggleSearchEditorRegexCommand, toggleSearchEditorWholeWordCommand, selectAllSearchEditorMatchesCommand } from 'vs/workbench/contrib/searchEditor/browser/searchEditorActions'; +import { OpenResultsInEditorAction, OpenSearchEditorAction, toggleSearchEditorCaseSensitiveCommand, toggleSearchEditorContextLinesCommand, toggleSearchEditorRegexCommand, toggleSearchEditorWholeWordCommand, selectAllSearchEditorMatchesCommand, RerunSearchEditorSearchAction } from 'vs/workbench/contrib/searchEditor/browser/searchEditorActions'; import { getOrMakeSearchEditorInput, SearchEditorInput } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; //#region Editor Descriptior Registry.as(EditorExtensions.Editors).registerEditor( @@ -54,10 +54,15 @@ class SearchEditorContribution implements IWorkbenchContribution { ) { this.editorService.overrideOpenEditor((editor, options, group) => { - const resource = editor.resource; - if (!resource || - !(endsWith(resource.path, '.code-search') || resource.scheme === SearchEditorConstants.SearchEditorScheme) || - !(editor instanceof FileEditorInput || (resource.scheme === SearchEditorConstants.SearchEditorScheme))) { + let resource = editor.resource; + if (!resource) { return undefined; } + + if (resource.scheme === SearchEditorConstants.SearchEditorBodyScheme) { + resource = resource.with({ scheme: SearchEditorConstants.SearchEditorScheme }); + } + + if (resource.scheme !== SearchEditorConstants.SearchEditorScheme + && !(endsWith(resource.path, '.code-search') && editor instanceof FileEditorInput)) { return undefined; } @@ -154,15 +159,6 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ handler: selectAllSearchEditorMatchesCommand }); -CommandsRegistry.registerCommand( - SearchEditorConstants.RerunSearchEditorSearchCommandId, - (accessor: ServicesAccessor) => { - const activeControl = accessor.get(IEditorService).activeControl; - if (activeControl instanceof SearchEditor) { - activeControl.triggerSearch({ resetCursor: false }); - } - }); - CommandsRegistry.registerCommand( SearchEditorConstants.CleanSearchEditorStateCommandId, (accessor: ServicesAccessor) => { @@ -186,4 +182,19 @@ registry.registerWorkbenchAction( registry.registerWorkbenchAction( SyncActionDescriptor.create(OpenSearchEditorAction, OpenSearchEditorAction.ID, OpenSearchEditorAction.LABEL), 'Search Editor: Open New Search Editor', category); + +registry.registerWorkbenchAction(SyncActionDescriptor.create(RerunSearchEditorSearchAction, RerunSearchEditorSearchAction.ID, RerunSearchEditorSearchAction.LABEL, + { mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_R } }, ContextKeyExpr.and(SearchEditorConstants.InSearchEditor)), + 'Search Editor: Rerun', category); //#endregion + + +MenuRegistry.appendMenuItem(MenuId.EditorTitle, { + command: { + id: RerunSearchEditorSearchAction.ID, + title: RerunSearchEditorSearchAction.LABEL, + icon: { id: 'codicon/refresh' }, + }, + group: 'navigation', + when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(SearchEditorConstants.SearchEditorID)) +}); diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts index 8f26a359018..523e97cdac5 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts @@ -35,7 +35,7 @@ import { InputBoxFocusedKey } from 'vs/workbench/contrib/search/common/constants import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder'; import { getOutOfWorkspaceEditorResources } from 'vs/workbench/contrib/search/common/search'; import { SearchModel } from 'vs/workbench/contrib/search/common/searchModel'; -import { InSearchEditor, SearchEditorFindMatchClass } from 'vs/workbench/contrib/searchEditor/browser/constants'; +import { InSearchEditor, SearchEditorFindMatchClass, SearchEditorID } from 'vs/workbench/contrib/searchEditor/browser/constants'; import type { SearchConfiguration, SearchEditorInput } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; import { extractSearchQuery, serializeSearchConfiguration, serializeSearchResultForEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditorSerialization'; import { IPatternInfo, ISearchConfigurationProperties, ITextQuery } from 'vs/workbench/services/search/common/search'; @@ -56,7 +56,7 @@ const FILE_LINE_REGEX = /^(\S.*):$/; type SearchEditorViewState = ICodeEditorViewState & { focused: 'input' | 'editor' }; export class SearchEditor extends BaseTextEditor { - static readonly ID: string = 'workbench.editor.searchEditor'; + static readonly ID: string = SearchEditorID; static readonly SEARCH_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'searchEditorViewState'; @@ -194,6 +194,7 @@ export class SearchEditor extends BaseTextEditor { const runAgainLink = DOM.append(this.messageBox, DOM.$('a.pointer.prominent.message', {}, localize('runSearch', "Run Search"))); this.messageDisposables.push(DOM.addDisposableListener(runAgainLink, DOM.EventType.CLICK, async () => { await this.triggerSearch(); + this.searchResultEditor.focus(); this.toggleRunAgainMessage(false); })); } diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts index 094acf25fd3..464d2bb6e51 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts @@ -116,6 +116,24 @@ export class OpenResultsInEditorAction extends Action { } } +export class RerunSearchEditorSearchAction extends Action { + static readonly ID: string = Constants.RerunSearchEditorSearchCommandId; + static readonly LABEL = localize('search.rerunSearchInEditor', "Search Again"); + + constructor(id: string, label: string, + @IEditorService private readonly editorService: IEditorService, + ) { + super(id, label, 'codicon-refresh'); + } + + async run() { + const input = this.editorService.activeEditor; + if (input instanceof SearchEditorInput) { + (this.editorService.activeControl as SearchEditor).triggerSearch({ resetCursor: false }); + } + } +} + const openNewSearchEditor = async (accessor: ServicesAccessor) => { const editorService = accessor.get(IEditorService); diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts index 389caf35844..698bb4c8f97 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts @@ -18,7 +18,7 @@ import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { EditorInput, GroupIdentifier, IEditorInput, IRevertOptions, ISaveOptions, IMoveResult } from 'vs/workbench/common/editor'; -import { SearchEditorFindMatchClass, SearchEditorScheme } from 'vs/workbench/contrib/searchEditor/browser/constants'; +import { SearchEditorFindMatchClass, SearchEditorScheme, SearchEditorBodyScheme } from 'vs/workbench/contrib/searchEditor/browser/constants'; import { extractSearchQuery, serializeSearchConfiguration } from 'vs/workbench/contrib/searchEditor/browser/searchEditorSerialization'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -53,7 +53,7 @@ export class SearchEditorInput extends EditorInput { private _cachedContentsModel: ITextModel | undefined; private _cachedConfig?: SearchConfiguration; - private readonly _onDidChangeContent = new Emitter(); + private readonly _onDidChangeContent = this._register(new Emitter()); readonly onDidChangeContent: Event = this._onDidChangeContent.event; private oldDecorationsIDs: string[] = []; @@ -112,7 +112,7 @@ export class SearchEditorInput extends EditorInput { isDirty(): boolean { return input.isDirty(); } backup(): Promise { return input.backup(); } save(options?: ISaveOptions): Promise { return input.save(0, options).then(editor => !!editor); } - revert(options?: IRevertOptions): Promise { return input.revert(0, options); } + revert(options?: IRevertOptions): Promise { return input.revert(0, options); } }; this.workingCopyService.registerWorkingCopy(workingCopyAdapter); @@ -261,7 +261,6 @@ export class SearchEditorInput extends EditorInput { // TODO: this should actually revert the contents. But it needs to set dirty false. super.revert(group, options); this.setDirty(false); - return true; } private async backup(): Promise { @@ -339,7 +338,7 @@ export const getOrMakeSearchEditorInput = ( } } - const contentsModelURI = uri.with({ scheme: 'search-editor-body' }); + const contentsModelURI = uri.with({ scheme: SearchEditorBodyScheme }); const headerModelURI = uri.with({ scheme: 'search-editor-header' }); const contentsModel = modelService.getModel(contentsModelURI) ?? modelService.createModel('', modeService.create('search-result'), contentsModelURI); const headerModel = modelService.getModel(headerModelURI) ?? modelService.createModel('', modeService.create('search-result'), headerModelURI); diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts index eca8524c25f..038519308e4 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts @@ -42,7 +42,7 @@ const matchToSearchResultFormat = (match: Match): { line: string, ranges: Range[ const rangeOnThisLine = ({ start, end }: { start?: number; end?: number; }) => new Range(1, (start ?? 1) + prefixOffset, 1, (end ?? sourceLine.length + 1) + prefixOffset); - const matchRange = match.range(); + const matchRange = match.rangeInPreview(); const matchIsSingleLine = matchRange.startLineNumber === matchRange.endLineNumber; let lineRange; @@ -211,9 +211,12 @@ export const serializeSearchResultForEditor = ? contentPatternToSearchResultHeader(searchResult.query, rawIncludePattern, rawExcludePattern, contextLines) : []; + const filecount = searchResult.fileCount() > 1 ? localize('numFiles', "{0} files", searchResult.fileCount()) : localize('oneFile', "1 file"); + const resultcount = searchResult.count() > 1 ? localize('numResults', "{0} results", searchResult.count()) : localize('oneResult', "1 result"); + const info = [ searchResult.count() - ? localize('resultCount', "{0} results in {1} files", searchResult.count(), searchResult.fileCount()) + ? `${resultcount} - ${filecount}` : localize('noResults', "No Results"), '']; diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 728fff5fac2..42386cce1ed 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -63,7 +63,7 @@ import { getTemplates as getTaskTemplates } from 'vs/workbench/contrib/tasks/com import * as TaskConfig from '../common/taskConfiguration'; import { TerminalTaskSystem } from './terminalTaskSystem'; -import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickInputService, IQuickPickItem, QuickPickInput, IQuickPick } from 'vs/platform/quickinput/common/quickInput'; import { TaskDefinitionRegistry } from 'vs/workbench/contrib/tasks/common/taskDefinitionRegistry'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -80,6 +80,7 @@ import { IPreferencesService } from 'vs/workbench/services/preferences/common/pr import { find } from 'vs/base/common/arrays'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { IViewsService } from 'vs/workbench/common/views'; +import { ProviderProgressMananger } from 'vs/workbench/contrib/tasks/browser/providerProgressManager'; const QUICKOPEN_HISTORY_LIMIT_CONFIG = 'task.quickOpen.history'; const QUICKOPEN_DETAIL_CONFIG = 'task.quickOpen.detail'; @@ -218,6 +219,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer private _providers: Map; private _providerTypes: Map; protected _taskSystemInfos: Map; + private _providerProgressManager: ProviderProgressMananger | undefined; protected _workspaceTasksPromise?: Promise>; protected _areJsonTasksSupportedPromise: Promise = Promise.resolve(false); @@ -560,6 +562,37 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer protected abstract versionAndEngineCompatible(filter?: TaskFilter): boolean; + private tasksAndGroupedTasks(filter?: TaskFilter): { tasks: Promise, grouped: Promise } { + if (!this.versionAndEngineCompatible(filter)) { + return { tasks: Promise.resolve([]), grouped: Promise.resolve(new TaskMap()) }; + } + const grouped = this.getGroupedTasks(filter ? filter.type : undefined); + const tasks = grouped.then((map) => { + if (!filter || !filter.type) { + return map.all(); + } + let result: Task[] = []; + map.forEach((tasks) => { + for (let task of tasks) { + if (ContributedTask.is(task) && task.defines.type === filter.type) { + result.push(task); + } else if (CustomTask.is(task)) { + if (task.type === filter.type) { + result.push(task); + } else { + let customizes = task.customizes(); + if (customizes && customizes.type === filter.type) { + result.push(task); + } + } + } + } + }); + return result; + }); + return { tasks, grouped }; + } + public tasks(filter?: TaskFilter): Promise { if (!this.versionAndEngineCompatible(filter)) { return Promise.resolve([]); @@ -703,11 +736,11 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }); } - public run(task: Task | undefined, options?: ProblemMatcherRunOptions, runSource: TaskRunSource = TaskRunSource.System): Promise { + public run(task: Task | undefined, options?: ProblemMatcherRunOptions, runSource: TaskRunSource = TaskRunSource.System, grouped?: Promise): Promise { if (!task) { throw new TaskError(Severity.Info, nls.localize('TaskServer.noTask', 'Task to execute is undefined'), TaskErrors.TaskNotFound); } - return this.getGroupedTasks().then((grouped) => { + return (grouped ?? this.getGroupedTasks()).then((grouped) => { let resolver = this.createResolver(grouped); if (options && options.attachProblemMatcher && this.shouldAttachProblemMatcher(task) && !InMemoryTask.is(task)) { return this.attachProblemMatcher(task).then((toExecute) => { @@ -1251,7 +1284,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer private executeTask(task: Task, resolver: ITaskResolver): Promise { return ProblemMatcherRegistry.onReady().then(() => { - return this.editorService.saveAll().then((value) => { // make sure all dirty editors are saved + return this.editorService.saveAll().then(() => { // make sure all dirty editors are saved let executeResult = this.getTaskSystem().run(task, resolver); return this.handleExecuteResult(executeResult); }); @@ -1345,11 +1378,24 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer protected abstract getTaskSystem(): ITaskSystem; - private async provideTasksWithWarning(provider: ITaskProvider, type: string, validTypes: IStringDictionary): Promise { + private async provideTasksWithWarning(provider: ITaskProvider, type: string, validTypes: IStringDictionary): Promise { return new Promise(async (resolve, reject) => { - provider.provideTasks(validTypes).then((value) => { + let isDone = false; + let disposable: IDisposable | undefined; + const providePromise = provider.provideTasks(validTypes); + this._providerProgressManager?.addProvider(type, providePromise); + disposable = this._providerProgressManager?.canceled.token.onCancellationRequested(() => { + if (!isDone) { + resolve(); + } + }); + providePromise.then((value) => { + isDone = true; + disposable?.dispose(); resolve(value); }, (e) => { + isDone = true; + disposable?.dispose(); reject(e); }); }); @@ -1361,10 +1407,11 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer TaskDefinitionRegistry.all().forEach(definition => validTypes[definition.taskType] = true); validTypes['shell'] = true; validTypes['process'] = true; + this._providerProgressManager = new ProviderProgressMananger(); return new Promise(resolve => { let result: TaskSet[] = []; let counter: number = 0; - let done = (value: TaskSet) => { + let done = (value: TaskSet | undefined) => { if (value) { result.push(value); } @@ -2060,30 +2107,69 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } return entries; }); - return this.quickInputService.pick(pickEntries, { - placeHolder, - matchOnDescription: true, - onDidTriggerItemButton: context => { - let task = context.item.task; - this.quickInputService.cancel(); - if (ContributedTask.is(task)) { - this.customize(task, undefined, true); - } else if (CustomTask.is(task)) { - this.openConfig(task); + + const picker: IQuickPick = this.quickInputService.createQuickPick(); + picker.placeholder = placeHolder; + picker.matchOnDescription = true; + picker.ignoreFocusOut = true; + + picker.onDidTriggerItemButton(context => { + let task = context.item.task; + this.quickInputService.cancel(); + if (ContributedTask.is(task)) { + this.customize(task, undefined, true); + } else if (CustomTask.is(task)) { + this.openConfig(task); + } + }); + picker.busy = true; + const progressManager = this._providerProgressManager; + const progressTimeout = setTimeout(() => { + if (progressManager) { + progressManager.showProgress = (stillProviding, total) => { + let message = undefined; + if (stillProviding.length > 0) { + message = nls.localize('pickProgressManager.description', 'Detecting tasks ({0} of {1}): {2} in progress', total - stillProviding.length, total, stillProviding.join(', ')); + } + picker.description = message; + }; + progressManager.addOnDoneListener(() => { + picker.focusOnInput(); + picker.customButton = false; + }); + if (!progressManager.isDone) { + picker.customLabel = nls.localize('taskQuickPick.cancel', "Stop detecting"); + picker.onDidCustom(() => { + this._providerProgressManager?.cancel(); + }); + picker.customButton = true; } } - }, cancellationToken).then(async (selection) => { - if (cancellationToken.isCancellationRequested) { - // canceled when there's only one task - const task = (await pickEntries)[0]; - if ((task).task) { - selection = task; + }, 1000); + pickEntries.then(entries => { + clearTimeout(progressTimeout); + progressManager?.dispose(); + picker.busy = false; + picker.items = entries; + }); + picker.show(); + + return new Promise(resolve => { + this._register(picker.onDidAccept(async () => { + let selection = picker.selectedItems ? picker.selectedItems[0] : undefined; + if (cancellationToken.isCancellationRequested) { + // canceled when there's only one task + const task = (await pickEntries)[0]; + if ((task).task) { + selection = task; + } } - } - if (!selection) { - return; - } - return selection; + picker.dispose(); + if (!selection) { + resolve(); + } + resolve(selection); + })); }); } @@ -2138,7 +2224,11 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer private doRunTaskCommand(tasks?: Task[]): void { this.showIgnoredFoldersMessage().then(() => { - this.showQuickPick(tasks ? tasks : this.tasks(), + let taskResult: { tasks: Promise, grouped: Promise } | undefined = undefined; + if (!tasks) { + taskResult = this.tasksAndGroupedTasks(); + } + this.showQuickPick(tasks ? tasks : taskResult!.tasks, nls.localize('TaskService.pickRunTask', 'Select the task to run'), { label: nls.localize('TaskService.noEntryToRun', 'No task to run found. Configure Tasks...'), @@ -2153,7 +2243,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (task === null) { this.runConfigureTasks(); } else { - this.run(task, { attachProblemMatcher: true }, TaskRunSource.User).then(undefined, reason => { + this.run(task, { attachProblemMatcher: true }, TaskRunSource.User, taskResult?.grouped).then(undefined, reason => { // eat the error, it has already been surfaced to the user and we don't care about it here }); } @@ -2167,7 +2257,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } ProblemMatcherRegistry.onReady().then(() => { - return this.editorService.saveAll().then((value) => { // make sure all dirty editors are saved + return this.editorService.saveAll().then(() => { // make sure all dirty editors are saved let executeResult = this.getTaskSystem().rerun(); if (executeResult) { return this.handleExecuteResult(executeResult); diff --git a/src/vs/workbench/contrib/tasks/browser/providerProgressManager.ts b/src/vs/workbench/contrib/tasks/browser/providerProgressManager.ts new file mode 100644 index 00000000000..7c48da7f73c --- /dev/null +++ b/src/vs/workbench/contrib/tasks/browser/providerProgressManager.ts @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TaskSet } from 'vs/workbench/contrib/tasks/common/tasks'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; + +export class ProviderProgressMananger extends Disposable { + private _onProviderComplete: Emitter = new Emitter(); + private _stillProviding: Set = new Set(); + private _totalProviders: number = 0; + private _onDone: Emitter = new Emitter(); + private _isDone: boolean = false; + private _showProgress: ((remaining: string[], total: number) => void) | undefined; + public canceled: CancellationTokenSource = new CancellationTokenSource(); + + constructor() { + super(); + this._register(this._onProviderComplete.event(taskType => { + this._stillProviding.delete(taskType); + if (this._stillProviding.size === 0) { + this._isDone = true; + this._onDone.fire(); + } + if (this._showProgress) { + this._showProgress(Array.from(this._stillProviding), this._totalProviders); + } + })); + } + + public addProvider(taskType: string, provider: Promise) { + this._totalProviders++; + this._stillProviding.add(taskType); + provider.then(() => this._onProviderComplete.fire(taskType)); + } + + public addOnDoneListener(onDoneListener: () => void) { + this._register(this._onDone.event(onDoneListener)); + } + + set showProgress(progressDisplayFunction: (remaining: string[], total: number) => void) { + this._showProgress = progressDisplayFunction; + this._showProgress(Array.from(this._stillProviding), this._totalProviders); + } + + get isDone(): boolean { + return this._isDone; + } + + public cancel() { + this._isDone = true; + if (this._showProgress) { + this._showProgress([], 0); + } + this._onDone.fire(); + this.canceled.cancel(); + } +} diff --git a/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts b/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts index 8b290a74f53..14666552147 100644 --- a/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts +++ b/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts @@ -681,6 +681,7 @@ export namespace RunOnOptions { } export namespace RunOptions { + const properties: MetaData[] = [{ property: 'reevaluateOnRerun' }, { property: 'runOn' }, { property: 'instanceLimit' }]; export function fromConfiguration(value: RunOptionsConfig | undefined): Tasks.RunOptions { return { reevaluateOnRerun: value ? value.reevaluateOnRerun : true, @@ -688,6 +689,14 @@ export namespace RunOptions { instanceLimit: value ? value.instanceLimit : 1 }; } + + export function assignProperties(target: Tasks.RunOptions, source: Tasks.RunOptions | undefined): Tasks.RunOptions { + return _assignProperties(target, source, properties)!; + } + + export function fillProperties(target: Tasks.RunOptions, source: Tasks.RunOptions | undefined): Tasks.RunOptions { + return _fillProperties(target, source, properties)!; + } } interface ParseContext { @@ -1609,6 +1618,7 @@ namespace CustomTask { result.command.presentation = CommandConfiguration.PresentationOptions.assignProperties( result.command.presentation!, configuredProps.configurationProperties.presentation)!; result.command.options = CommandOptions.assignProperties(result.command.options, configuredProps.configurationProperties.options); + result.runOptions = RunOptions.assignProperties(result.runOptions, configuredProps.runOptions); let contributedConfigProps: Tasks.ConfigurationProperties = contributedTask.configurationProperties; fillProperty(resultConfigProps, contributedConfigProps, 'group'); @@ -1621,6 +1631,7 @@ namespace CustomTask { result.command.presentation = CommandConfiguration.PresentationOptions.fillProperties( result.command.presentation!, contributedConfigProps.presentation)!; result.command.options = CommandOptions.fillProperties(result.command.options, contributedConfigProps.options); + result.runOptions = RunOptions.fillProperties(result.runOptions, contributedTask.runOptions); if (contributedTask.hasDefinedMatchers === true) { result.hasDefinedMatchers = true; diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index 9ad11415ffe..a355f69febd 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -282,6 +282,11 @@ configurationRegistry.registerConfiguration({ type: 'boolean', default: true }, + 'terminal.integrated.allowMenubarMnemonics': { + markdownDescription: nls.localize('terminal.integrated.allowMenubarMnemonics', "Whether to allow menubar mnemonics (eg. alt+f) to trigger the open the menubar. Note that this will cause all alt keystrokes will skip the shell when true."), + type: 'boolean', + default: false + }, 'terminal.integrated.inheritEnv': { markdownDescription: nls.localize('terminal.integrated.inheritEnv', "Whether new shells should inherit their environment from VS Code. This is not supported on Windows."), type: 'boolean', diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index cec072d72b3..b3375193099 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -595,19 +595,30 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { return false; } - // Skip processing by xterm.js of keyboard events that resolve to commands described - // within commandsToSkipShell const standardKeyboardEvent = new StandardKeyboardEvent(event); const resolveResult = this._keybindingService.softDispatch(standardKeyboardEvent, standardKeyboardEvent.target); + // Respect chords if the allowChords setting is set and it's not Escape. Escape is // handled specially for Zen Mode's Escape, Escape chord, plus it's important in // terminals generally - const allowChords = resolveResult && resolveResult.enterChord && this._configHelper.config.allowChords && event.key !== 'Escape'; - if (allowChords || resolveResult && this._skipTerminalCommands.some(k => k === resolveResult.commandId)) { + const isValidChord = resolveResult?.enterChord && this._configHelper.config.allowChords && event.key !== 'Escape'; + if (this._keybindingService.inChordMode || isValidChord) { event.preventDefault(); return false; } + // Skip processing by xterm.js of keyboard events that resolve to commands described + // within commandsToSkipShell + if (resolveResult && this._skipTerminalCommands.some(k => k === resolveResult.commandId)) { + event.preventDefault(); + return false; + } + + // Skip processing by xterm.js of keyboard events that match menu bar mnemonics + if (this._configHelper.config.allowMenubarMnemonics && event.altKey) { + return false; + } + // If tab focus mode is on, tab is not passed to the terminal if (TabFocus.getTabFocusMode() && event.keyCode === 9) { return false; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts index df405001f97..e714542e72c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts @@ -69,7 +69,7 @@ export class TerminalInstanceService implements ITerminalInstanceService { throw new Error('Not implemented'); } - public getDefaultShellAndArgs(useAutomationShell: boolean, ): Promise<{ shell: string, args: string[] | string | undefined }> { + public getDefaultShellAndArgs(useAutomationShell: boolean,): Promise<{ shell: string, args: string[] | string | undefined }> { return new Promise(r => this._onRequestDefaultShellAndArgs.fire({ useAutomationShell, callback: (shell, args) => r({ shell, args }) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index c0d8a8bc8f9..12bc4fab7ff 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -114,7 +114,7 @@ export class TerminalViewPane extends ViewPane { })); // Force another layout (first is setContainers) since config has changed - this.layoutBody(this._terminalContainer.offsetWidth, this._terminalContainer.offsetHeight); + this.layoutBody(this._terminalContainer.offsetHeight, this._terminalContainer.offsetWidth); } protected layoutBody(height: number, width: number): void { @@ -321,7 +321,7 @@ export class TerminalViewPane extends ViewPane { } // TODO: Can we support ligatures? // dom.toggleClass(this._parentDomElement, 'enable-ligatures', this._terminalService.configHelper.config.fontLigatures); - this.layoutBody(this._parentDomElement.offsetWidth, this._parentDomElement.offsetHeight); + this.layoutBody(this._parentDomElement.offsetHeight, this._parentDomElement.offsetWidth); } } diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index e58b63cc000..4b9abc01604 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; import { OperatingSystem } from 'vs/base/common/platform'; @@ -19,25 +19,25 @@ export const KEYBINDING_CONTEXT_TERMINAL_IS_OPEN = new RawContextKey('t /** A context key that is set when the integrated terminal has focus. */ export const KEYBINDING_CONTEXT_TERMINAL_FOCUS = new RawContextKey('terminalFocus', false); /** A context key that is set when the integrated terminal does not have focus. */ -export const KEYBINDING_CONTEXT_TERMINAL_NOT_FOCUSED: ContextKeyExpr = KEYBINDING_CONTEXT_TERMINAL_FOCUS.toNegated(); +export const KEYBINDING_CONTEXT_TERMINAL_NOT_FOCUSED = KEYBINDING_CONTEXT_TERMINAL_FOCUS.toNegated(); /** A context key that is set when the user is navigating the accessibility tree */ export const KEYBINDING_CONTEXT_TERMINAL_A11Y_TREE_FOCUS = new RawContextKey('terminalA11yTreeFocus', false); /** A keybinding context key that is set when the integrated terminal has text selected. */ export const KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED = new RawContextKey('terminalTextSelected', false); /** A keybinding context key that is set when the integrated terminal does not have text selected. */ -export const KEYBINDING_CONTEXT_TERMINAL_TEXT_NOT_SELECTED: ContextKeyExpr = KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED.toNegated(); +export const KEYBINDING_CONTEXT_TERMINAL_TEXT_NOT_SELECTED = KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED.toNegated(); /** A context key that is set when the find widget in integrated terminal is visible. */ export const KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE = new RawContextKey('terminalFindWidgetVisible', false); /** A context key that is set when the find widget in integrated terminal is not visible. */ -export const KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_NOT_VISIBLE: ContextKeyExpr = KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE.toNegated(); +export const KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_NOT_VISIBLE = KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE.toNegated(); /** A context key that is set when the find widget find input in integrated terminal is focused. */ export const KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_INPUT_FOCUSED = new RawContextKey('terminalFindWidgetInputFocused', false); /** A context key that is set when the find widget in integrated terminal is focused. */ export const KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_FOCUSED = new RawContextKey('terminalFindWidgetFocused', false); /** A context key that is set when the find widget find input in integrated terminal is not focused. */ -export const KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_INPUT_NOT_FOCUSED: ContextKeyExpr = KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_INPUT_FOCUSED.toNegated(); +export const KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_INPUT_NOT_FOCUSED = KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_INPUT_FOCUSED.toNegated(); export const IS_WORKSPACE_SHELL_ALLOWED_STORAGE_KEY = 'terminal.integrated.isWorkspaceShellAllowed'; export const NEVER_MEASURE_RENDER_TIME_STORAGE_KEY = 'terminal.integrated.neverMeasureRenderTime'; @@ -107,6 +107,7 @@ export interface ITerminalConfiguration { scrollback: number; commandsToSkipShell: string[]; allowChords: boolean; + allowMenubarMnemonics: boolean; cwd: string; confirmOnExit: boolean; enableBell: boolean; diff --git a/src/vs/workbench/contrib/timeline/browser/media/timelinePane.css b/src/vs/workbench/contrib/timeline/browser/media/timelinePane.css index 3b036147cfc..4e6b3c4972a 100644 --- a/src/vs/workbench/contrib/timeline/browser/media/timelinePane.css +++ b/src/vs/workbench/contrib/timeline/browser/media/timelinePane.css @@ -7,9 +7,25 @@ position: relative; } +.monaco-workbench .timeline-view.pane-header .description { + margin-left: 10px; + opacity: 0.6; + text-transform: none; + font-weight: normal; +} + +.monaco-workbench .timeline-view.pane-header:not(.expanded) .description { + display: none; +} + +.monaco-workbench .timeline-view.pane-header .description span.codicon { + font-size: 9px; + margin-left: 2px; +} + .monaco-workbench .timeline-tree-view .message.timeline-subtle { - padding: 10px 22px 0 22px; opacity: 0.5; + padding: 10px 22px 0 22px; position: absolute; pointer-events: none; z-index: 1; diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index d438a1b739c..afb28a1a2a7 100644 --- a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts +++ b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts @@ -80,8 +80,8 @@ interface TimelineActionContext { } interface TimelineCursors { - startCursors?: { before: any; after?: any }; - endCursors?: { before: any; after?: any }; + startCursors?: { before: string; after?: string }; + endCursors?: { before: string; after?: string }; more: boolean; } @@ -89,9 +89,10 @@ export class TimelinePane extends ViewPane { static readonly ID = 'timeline'; static readonly TITLE = localize('timeline', 'Timeline'); - private _container!: HTMLElement; - private _messageElement!: HTMLDivElement; - private _treeElement!: HTMLDivElement; + private _$container!: HTMLElement; + private _$message!: HTMLDivElement; + private _$titleDescription!: HTMLSpanElement; + private _$tree!: HTMLDivElement; private _tree!: WorkbenchObjectTree; private _treeRenderer: TimelineTreeRenderer | undefined; private _menus: TimelineMenus; @@ -100,7 +101,6 @@ export class TimelinePane extends ViewPane { private _excludedSources: Set; private _cursorsByProvider: Map = new Map(); private _items: { element: TreeElement }[] = []; - private _loadingMessageTimer: any | undefined; private _pendingRequests = new Map(); private _uri: URI | undefined; @@ -181,6 +181,17 @@ export class TimelinePane extends ViewPane { this.loadTimeline(true); } + + private _titleDescription: string | undefined; + get titleDescription(): string | undefined { + return this._titleDescription; + } + + set titleDescription(description: string | undefined) { + this._titleDescription = description; + this._$titleDescription.textContent = description ?? ''; + } + private _message: string | undefined; get message(): string | undefined { return this._message; @@ -192,7 +203,7 @@ export class TimelinePane extends ViewPane { } private updateMessage(): void { - if (this._message) { + if (this._message !== undefined) { this.showMessage(this._message); } else { this.hideMessage(); @@ -200,35 +211,32 @@ export class TimelinePane extends ViewPane { } private showMessage(message: string): void { - DOM.removeClass(this._messageElement, 'hide'); + DOM.removeClass(this._$message, 'hide'); this.resetMessageElement(); - this._messageElement.textContent = message; + this._$message.textContent = message; } private hideMessage(): void { this.resetMessageElement(); - DOM.addClass(this._messageElement, 'hide'); + DOM.addClass(this._$message, 'hide'); } private resetMessageElement(): void { - DOM.clearNode(this._messageElement); + DOM.clearNode(this._$message); } + private _pendingAnyResults: boolean = false; private async loadTimeline(reset: boolean, sources?: string[], options: TimelineOptions = {}) { const defaultPageSize = reset ? InitialPageSize : SubsequentPageSize; // If we have no source, we are reseting all sources, so cancel everything in flight and reset caches if (sources === undefined) { if (reset) { + this._pendingAnyResults = this._pendingAnyResults || this._items.length !== 0; this._items.length = 0; this._cursorsByProvider.clear(); - if (this._loadingMessageTimer) { - clearTimeout(this._loadingMessageTimer); - this._loadingMessageTimer = undefined; - } - for (const { tokenSource } of this._pendingRequests.values()) { tokenSource.dispose(true); } @@ -237,26 +245,23 @@ export class TimelinePane extends ViewPane { } // TODO[ECA]: Are these the right the list of schemes to exclude? Is there a better way? - if (this._uri && (this._uri.scheme === 'vscode-settings' || this._uri.scheme === 'webview-panel' || this._uri.scheme === 'walkThrough')) { - this.message = localize('timeline.editorCannotProvideTimeline', 'The active editor cannot provide timeline information.'); - this._tree.setChildren(null, undefined); + if (this._uri?.scheme === 'vscode-settings' || this._uri?.scheme === 'webview-panel' || this._uri?.scheme === 'walkThrough') { + this._uri = undefined; + this._items.length = 0; + this.refresh(); return; } - if (reset && this._uri !== undefined) { - this._loadingMessageTimer = setTimeout((uri: URI) => { - if (uri !== this._uri) { - return; - } - - this._tree.setChildren(null, undefined); - this.message = localize('timeline.loading', 'Loading timeline for ${0}...', basename(uri.fsPath)); - }, 500, this._uri); + if (!this._pendingAnyResults && this._uri !== undefined) { + this.setLoadingUriMessage(); } } if (this._uri === undefined) { + this._items.length = 0; + this.refresh(); + return; } @@ -284,6 +289,8 @@ export class TimelinePane extends ViewPane { } } + let noRequests = true; + for (const source of filteredSources) { let request = this._pendingRequests.get(source); @@ -291,7 +298,7 @@ export class TimelinePane extends ViewPane { if (!reset) { // TODO: Handle pending request - if (cursors?.more === false) { + if (cursors?.more !== true) { continue; } @@ -301,11 +308,18 @@ export class TimelinePane extends ViewPane { { cursor: options.before ? cursors?.startCursors?.before : (cursors?.endCursors ?? cursors?.startCursors)?.after, ...options, - limit: options.limit === 0 ? undefined : options.limit ?? defaultPageSize + limit: options.limit === 0 + ? undefined + : options.limit ?? defaultPageSize }, - request?.tokenSource ?? new CancellationTokenSource(), { cacheResults: true } + request?.tokenSource ?? new CancellationTokenSource(), { cacheResults: true, resetCache: false } )!; + if (request === undefined) { + continue; + } + + noRequests = false; this._pendingRequests.set(source, request); if (!reusingToken) { request.tokenSource.token.onCancellationRequested(() => this._pendingRequests.delete(source)); @@ -317,17 +331,32 @@ export class TimelinePane extends ViewPane { source, this._uri, { ...options, - limit: options.limit === 0 ? undefined : (reset ? cursors?.endCursors?.after : undefined) ?? options.limit ?? defaultPageSize + limit: options.limit === 0 + ? undefined + : (reset && cursors?.endCursors?.after !== undefined + ? { cursor: cursors.endCursors.after } + : undefined) ?? options.limit ?? defaultPageSize }, - new CancellationTokenSource(), { cacheResults: true } + new CancellationTokenSource(), { cacheResults: true, resetCache: true } )!; + if (request === undefined) { + continue; + } + + noRequests = false; this._pendingRequests.set(source, request); request.tokenSource.token.onCancellationRequested(() => this._pendingRequests.delete(source)); } this.handleRequest(request); } + + if (noRequests) { + this.refresh(); + } else if (this.message !== undefined) { + this.setLoadingUriMessage(); + } } private async handleRequest(request: TimelineRequest) { @@ -340,13 +369,20 @@ export class TimelinePane extends ViewPane { } if ( - timeline === undefined || request.tokenSource.token.isCancellationRequested || request.uri !== this._uri ) { return; } + if (timeline === undefined) { + if (this._pendingRequests.size === 0) { + this.refresh(); + } + + return; + } + let items: TreeElement[]; const source = request.source; @@ -424,8 +460,7 @@ export class TimelinePane extends ViewPane { // If we have items already and there are other pending requests, debounce for a bit to wait for other requests if (alreadyHadItems && this._pendingRequests.size !== 0) { this.refreshDebounced(); - } - else { + } else { this.refresh(); } } @@ -504,17 +539,22 @@ export class TimelinePane extends ViewPane { } private refresh() { - if (this._loadingMessageTimer) { - clearTimeout(this._loadingMessageTimer); - this._loadingMessageTimer = undefined; - } - - if (this._items.length === 0) { - this.message = localize('timeline.noTimelineInfo', 'No timeline information was provided.'); + if (this._uri === undefined) { + this.titleDescription = undefined; + this.message = localize('timeline.editorCannotProvideTimeline', 'The active editor cannot provide timeline information.'); + } else if (this._items.length === 0) { + if (this._pendingRequests.size !== 0) { + this.setLoadingUriMessage(); + } else { + this.titleDescription = basename(this._uri.fsPath); + this.message = localize('timeline.noTimelineInfo', 'No timeline information was provided.'); + } } else { + this.titleDescription = basename(this._uri.fsPath); this.message = undefined; } + this._pendingAnyResults = false; this._tree.setChildren(null, this._items); } @@ -547,23 +587,30 @@ export class TimelinePane extends ViewPane { this._tree.layout(height, width); } + protected renderHeaderTitle(container: HTMLElement): void { + super.renderHeaderTitle(container, this.title); + + DOM.addClass(container, 'timeline-view'); + this._$titleDescription = DOM.append(container, DOM.$('span.description', undefined, this.titleDescription ?? '')); + } + protected renderBody(container: HTMLElement): void { - this._container = container; + this._$container = container; DOM.addClasses(container, 'tree-explorer-viewlet-tree-view', 'timeline-tree-view'); - this._messageElement = DOM.append(this._container, DOM.$('.message')); - DOM.addClass(this._messageElement, 'timeline-subtle'); + this._$message = DOM.append(this._$container, DOM.$('.message')); + DOM.addClass(this._$message, 'timeline-subtle'); this.message = localize('timeline.editorCannotProvideTimeline', 'The active editor cannot provide timeline information.'); - this._treeElement = document.createElement('div'); - DOM.addClasses(this._treeElement, 'customview-tree', 'file-icon-themable-tree', 'hide-arrows'); + this._$tree = document.createElement('div'); + DOM.addClasses(this._$tree, 'customview-tree', 'file-icon-themable-tree', 'hide-arrows'); // DOM.addClass(this._treeElement, 'show-file-icons'); - container.appendChild(this._treeElement); + container.appendChild(this._$tree); this._treeRenderer = this.instantiationService.createInstance(TimelineTreeRenderer, this._menus); this._tree = >this.instantiationService.createInstance(WorkbenchObjectTree, 'TimelinePane', - this._treeElement, new TimelineListVirtualDelegate(), [this._treeRenderer], { + this._$tree, new TimelineListVirtualDelegate(), [this._treeRenderer], { identityProvider: new TimelineIdentityProvider(), keyboardNavigationLabelProvider: new TimelineKeyboardNavigationLabelProvider(), overrideStyles: { @@ -575,9 +622,10 @@ export class TimelinePane extends ViewPane { const customTreeNavigator = new TreeResourceNavigator(this._tree, { openOnFocus: false, openOnSelection: false }); this._register(customTreeNavigator); this._register(this._tree.onContextMenu(e => this.onContextMenu(this._menus, e))); + this._register(this._tree.onDidChangeSelection(e => this.ensureValidItems())); this._register( customTreeNavigator.onDidOpenResource(e => { - if (!e.browserEvent) { + if (!e.browserEvent || !this.ensureValidItems()) { return; } @@ -604,6 +652,24 @@ export class TimelinePane extends ViewPane { }) ); } + ensureValidItems() { + if (this._pendingAnyResults) { + this._tree.setChildren(null, undefined); + + this.setLoadingUriMessage(); + + this._pendingAnyResults = false; + return false; + } + + return true; + } + + setLoadingUriMessage() { + const file = this._uri && basename(this._uri.fsPath); + this.titleDescription = file ?? ''; + this.message = file ? localize('timeline.loading', 'Loading timeline for {0}...', file) : ''; + } private onContextMenu(menus: TimelineMenus, treeEvent: ITreeContextMenuEvent): void { const item = treeEvent.element; @@ -615,6 +681,10 @@ export class TimelinePane extends ViewPane { event.preventDefault(); event.stopPropagation(); + if (!this.ensureValidItems()) { + return; + } + this._tree.setFocus([item]); const actions = menus.getResourceContextActions(item); if (!actions.length) { diff --git a/src/vs/workbench/contrib/timeline/common/timeline.ts b/src/vs/workbench/contrib/timeline/common/timeline.ts index 3c5597d21fc..3f7cad3109e 100644 --- a/src/vs/workbench/contrib/timeline/common/timeline.ts +++ b/src/vs/workbench/contrib/timeline/common/timeline.ts @@ -40,7 +40,12 @@ export interface TimelineChangeEvent { export interface TimelineOptions { cursor?: string; before?: boolean; - limit?: number | string; + limit?: number | { cursor: string }; +} + +export interface InternalTimelineOptions { + cacheResults: boolean; + resetCache: boolean; } export interface Timeline { @@ -59,7 +64,7 @@ export interface Timeline { export interface TimelineProvider extends TimelineProviderDescriptor, IDisposable { onDidChange?: Event; - provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: { cacheResults?: boolean }): Promise; + provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: InternalTimelineOptions): Promise; } export interface TimelineProviderDescriptor { @@ -93,7 +98,7 @@ export interface ITimelineService { getSources(): string[]; - getTimeline(id: string, uri: URI, options: TimelineOptions, tokenSource: CancellationTokenSource, internalOptions?: { cacheResults?: boolean }): TimelineRequest | undefined; + getTimeline(id: string, uri: URI, options: TimelineOptions, tokenSource: CancellationTokenSource, internalOptions?: InternalTimelineOptions): TimelineRequest | undefined; // refresh(fetch?: 'all' | 'more'): void; reset(): void; diff --git a/src/vs/workbench/contrib/timeline/common/timelineService.ts b/src/vs/workbench/contrib/timeline/common/timelineService.ts index d017b15245d..4f61ad5ef20 100644 --- a/src/vs/workbench/contrib/timeline/common/timelineService.ts +++ b/src/vs/workbench/contrib/timeline/common/timelineService.ts @@ -9,7 +9,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; // import { basename } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; -import { ITimelineService, TimelineChangeEvent, TimelineOptions, TimelineProvidersChangeEvent, TimelineProvider } from './timeline'; +import { ITimelineService, TimelineChangeEvent, TimelineOptions, TimelineProvidersChangeEvent, TimelineProvider, InternalTimelineOptions } from './timeline'; export class TimelineService implements ITimelineService { _serviceBrand: undefined; @@ -27,54 +27,68 @@ export class TimelineService implements ITimelineService { private readonly _providerSubscriptions = new Map(); constructor(@ILogService private readonly logService: ILogService) { + // let source = 'slow-source'; // this.registerTimelineProvider({ - // id: 'local-history', - // label: 'Local History', - // provideTimeline(uri: URI, token: CancellationToken) { + // scheme: '*', + // id: source, + // label: 'Slow Source', + // provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: { cacheResults?: boolean | undefined; }) { // return new Promise(resolve => setTimeout(() => { - // resolve([ - // { - // id: '1', - // label: 'Slow Timeline1', - // description: basename(uri.fsPath), - // timestamp: Date.now(), - // source: 'local-history' - // }, - // { - // id: '2', - // label: 'Slow Timeline2', - // description: basename(uri.fsPath), - // timestamp: new Date(0).getTime(), - // source: 'local-history' - // } - // ]); - // }, 3000)); + // resolve({ + // source: source, + // items: [ + // { + // handle: `${source}|1`, + // id: '1', + // label: 'Slow Timeline1', + // description: basename(uri.fsPath), + // timestamp: Date.now(), + // source: source + // }, + // { + // handle: `${source}|2`, + // id: '2', + // label: 'Slow Timeline2', + // description: basename(uri.fsPath), + // timestamp: new Date(0).getTime(), + // source: source + // } + // ] + // }); + // }, 5000)); // }, // dispose() { } // }); + // source = 'very-slow-source'; // this.registerTimelineProvider({ - // id: 'slow-history', - // label: 'Slow History', - // provideTimeline(uri: URI, token: CancellationToken) { + // scheme: '*', + // id: source, + // label: 'Very Slow Source', + // provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: { cacheResults?: boolean | undefined; }) { // return new Promise(resolve => setTimeout(() => { - // resolve([ - // { - // id: '1', - // label: 'VERY Slow Timeline1', - // description: basename(uri.fsPath), - // timestamp: Date.now(), - // source: 'slow-history' - // }, - // { - // id: '2', - // label: 'VERY Slow Timeline2', - // description: basename(uri.fsPath), - // timestamp: new Date(0).getTime(), - // source: 'slow-history' - // } - // ]); - // }, 6000)); + // resolve({ + // source: source, + // items: [ + // { + // handle: `${source}|1`, + // id: '1', + // label: 'VERY Slow Timeline1', + // description: basename(uri.fsPath), + // timestamp: Date.now(), + // source: source + // }, + // { + // handle: `${source}|2`, + // id: '2', + // label: 'VERY Slow Timeline2', + // description: basename(uri.fsPath), + // timestamp: new Date(0).getTime(), + // source: source + // } + // ] + // }); + // }, 10000)); // }, // dispose() { } // }); @@ -84,7 +98,7 @@ export class TimelineService implements ITimelineService { return [...this._providers.keys()]; } - getTimeline(id: string, uri: URI, options: TimelineOptions, tokenSource: CancellationTokenSource, internalOptions?: { cacheResults?: boolean }) { + getTimeline(id: string, uri: URI, options: TimelineOptions, tokenSource: CancellationTokenSource, internalOptions?: InternalTimelineOptions) { this.logService.trace(`TimelineService#getTimeline(${id}): uri=${uri.toString(true)}`); const provider = this._providers.get(id); diff --git a/src/vs/workbench/contrib/update/browser/update.ts b/src/vs/workbench/contrib/update/browser/update.ts index 786014463b6..0e39da8602f 100644 --- a/src/vs/workbench/contrib/update/browser/update.ts +++ b/src/vs/workbench/contrib/update/browser/update.ts @@ -22,10 +22,9 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { ReleaseNotesManager } from './releaseNotesEditor'; import { isWindows } from 'vs/base/common/platform'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { RawContextKey, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { RawContextKey, IContextKey, IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { FalseContext } from 'vs/platform/contextkey/common/contextkeys'; import { ShowCurrentReleaseNotesActionId, CheckForVSCodeUpdateActionId } from 'vs/workbench/contrib/update/common/update'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -417,7 +416,7 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu command: { id: 'update.checking', title: nls.localize('checkingForUpdates', "Checking for Updates..."), - precondition: FalseContext + precondition: ContextKeyExpr.false() }, when: CONTEXT_UPDATE_STATE.isEqualTo(StateType.CheckingForUpdates) }); @@ -438,7 +437,7 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu command: { id: 'update.downloading', title: nls.localize('DownloadingUpdate', "Downloading Update..."), - precondition: FalseContext + precondition: ContextKeyExpr.false() }, when: CONTEXT_UPDATE_STATE.isEqualTo(StateType.Downloading) }); @@ -459,7 +458,7 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu command: { id: 'update.updating', title: nls.localize('installingUpdate', "Installing Update..."), - precondition: FalseContext + precondition: ContextKeyExpr.false() }, when: CONTEXT_UPDATE_STATE.isEqualTo(StateType.Updating) }); diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSyncService.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSyncService.ts index 3caeb60f404..7e035fddbce 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSyncService.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSyncService.ts @@ -10,6 +10,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { UserDataSyncTrigger } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; export class UserDataAutoSyncService extends BaseUserDataAutoSyncService { @@ -20,15 +21,15 @@ export class UserDataAutoSyncService extends BaseUserDataAutoSyncService { @IAuthenticationTokenService authTokenService: IAuthenticationTokenService, @IInstantiationService instantiationService: IInstantiationService, @IHostService hostService: IHostService, + @ITelemetryService telemetryService: ITelemetryService, ) { - super(userDataSyncEnablementService, userDataSyncService, logService, authTokenService); + super(userDataSyncEnablementService, userDataSyncService, logService, authTokenService, telemetryService); - // Sync immediately if there is a local change. - this._register(Event.debounce(Event.any( - userDataSyncService.onDidChangeLocal, + this._register(Event.debounce(Event.any( + Event.map(hostService.onDidChangeFocus, () => 'windowFocus'), instantiationService.createInstance(UserDataSyncTrigger).onDidTriggerSync, - hostService.onDidChangeFocus - ), () => undefined, 500)(() => this.triggerAutoSync())); + userDataSyncService.onDidChangeLocal, + ), (last, source) => last ? [...last, source] : [source], 1000)(sources => this.triggerAutoSync(sources))); } } diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 3af86359029..05c609878ac 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -23,7 +23,7 @@ import { localize } from 'vs/nls'; import { MenuId, MenuRegistry, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey, ContextKeyRegexExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -34,7 +34,7 @@ import { CONTEXT_SYNC_STATE, getSyncSourceFromRemoteContentResource, getUserData import { FloatingClickWidget } from 'vs/workbench/browser/parts/editor/editorWidgets'; import { GLOBAL_ACTIVITY_ID } from 'vs/workbench/common/activity'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import type { IEditorInput } from 'vs/workbench/common/editor'; +import { IEditorInput, toResource, SideBySideEditor } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import * as Constants from 'vs/workbench/contrib/logs/common/logConstants'; import { IOutputService } from 'vs/workbench/contrib/output/common/output'; @@ -49,6 +49,7 @@ import { fromNow } from 'vs/base/common/date'; import { IProductService } from 'vs/platform/product/common/productService'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { timeout } from 'vs/base/common/async'; const enum AuthStatus { Initializing = 'Initializing', @@ -64,7 +65,7 @@ type ConfigureSyncQuickPickItem = { id: ResourceKey, label: string, description? function getSyncAreaLabel(source: SyncSource): string { switch (source) { case SyncSource.Settings: return localize('settings', "Settings"); - case SyncSource.Keybindings: return localize('keybindings', "Keybindings"); + case SyncSource.Keybindings: return localize('keybindings', "Keyboard Shortcuts"); case SyncSource.Extensions: return localize('extensions', "Extensions"); case SyncSource.GlobalState: return localize('ui state label', "UI State"); } @@ -88,18 +89,18 @@ const getActivityTitle = (label: string, userDataSyncService: IUserDataSyncServi } return label; }; -const getIdentityTitle = (label: string, account?: AuthenticationSession): string => { - return account ? `${label} (${account.accountName})` : label; +const getIdentityTitle = (label: string, authenticationProviderId: string, account: AuthenticationSession | undefined, authenticationService: IAuthenticationService): string => { + return account ? `${label} (${authenticationService.getDisplayName(authenticationProviderId)}:${account.accountName})` : label; }; const turnOnSyncCommand = { id: 'workbench.userData.actions.syncStart', title: localize('turn on sync with category', "Sync: Turn on Sync") }; const signInCommand = { id: 'workbench.userData.actions.signin', title: localize('sign in', "Sync: Sign in to sync") }; -const stopSyncCommand = { id: 'workbench.userData.actions.stopSync', title(account?: AuthenticationSession) { return getIdentityTitle(localize('stop sync', "Sync: Turn off Sync"), account); } }; +const stopSyncCommand = { id: 'workbench.userData.actions.stopSync', title(authenticationProviderId: string, account: AuthenticationSession | undefined, authenticationService: IAuthenticationService) { return getIdentityTitle(localize('stop sync', "Sync: Turn off Sync"), authenticationProviderId, account, authenticationService); } }; const resolveSettingsConflictsCommand = { id: 'workbench.userData.actions.resolveSettingsConflicts', title: localize('showConflicts', "Sync: Show Settings Conflicts") }; const resolveKeybindingsConflictsCommand = { id: 'workbench.userData.actions.resolveKeybindingsConflicts', title: localize('showKeybindingsConflicts', "Sync: Show Keybindings Conflicts") }; const configureSyncCommand = { id: 'workbench.userData.actions.configureSync', title: localize('configure sync', "Sync: Configure") }; const showSyncActivityCommand = { id: 'workbench.userData.actions.showSyncActivity', title(userDataSyncService: IUserDataSyncService): string { - return getActivityTitle(localize('show sync log', "Sync: Show Activity"), userDataSyncService); + return getActivityTitle(localize('show sync log', "Sync: Show Log"), userDataSyncService); } }; const showSyncSettingsCommand = { id: 'workbench.userData.actions.syncSettings', title: localize('sync settings', "Sync: Settings"), }; @@ -154,7 +155,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo this._register(Event.debounce(userDataSyncService.onDidChangeStatus, () => undefined, 500)(() => this.onDidChangeSyncStatus(this.userDataSyncService.status))); this._register(userDataSyncService.onDidChangeConflicts(() => this.onDidChangeConflicts(this.userDataSyncService.conflictsSources))); this._register(userDataSyncService.onSyncErrors(errors => this.onSyncErrors(errors))); - this._register(this.authTokenService.onTokenFailed(_ => this.authenticationService.getSessions(this.userDataSyncStore!.authenticationProviderId))); + this._register(this.authTokenService.onTokenFailed(_ => this.onTokenFailed())); this._register(this.userDataSyncEnablementService.onDidChangeEnablement(enabled => this.onDidChangeEnablement(enabled))); this._register(this.authenticationService.onDidRegisterAuthenticationProvider(e => this.onDidRegisterAuthenticationProvider(e))); this._register(this.authenticationService.onDidUnregisterAuthenticationProvider(e => this.onDidUnregisterAuthenticationProvider(e))); @@ -163,7 +164,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo this.registerActions(); this.initializeActiveAccount().then(_ => { if (!isWeb) { - this._register(instantiationService.createInstance(UserDataSyncTrigger).onDidTriggerSync(() => userDataAutoSyncService.triggerAutoSync())); + this._register(instantiationService.createInstance(UserDataSyncTrigger).onDidTriggerSync(source => userDataAutoSyncService.triggerAutoSync([source]))); } }); @@ -226,7 +227,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo if (account) { try { - const token = await account.accessToken(); + const token = await account.getAccessToken(); this.authTokenService.setToken(token); this.authenticationState.set(AuthStatus.SignedIn); } catch (e) { @@ -254,6 +255,16 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } } + private async onTokenFailed(): Promise { + if (this.activeAccount) { + const accounts = (await this.authenticationService.getSessions(this.userDataSyncStore!.authenticationProviderId) || []); + const matchingAccount = accounts.filter(a => a.id === this.activeAccount?.id)[0]; + this.setActiveAccount(matchingAccount); + } else { + this.setActiveAccount(undefined); + } + } + private async onDidRegisterAuthenticationProvider(providerId: string) { if (providerId === this.userDataSyncStore!.authenticationProviderId) { await this.initializeActiveAccount(); @@ -290,7 +301,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo const conflictsEditorInput = this.getConflictsEditorInput(conflictsSource); if (!conflictsEditorInput && !this.conflictsDisposables.has(conflictsSource)) { const conflictsArea = getSyncAreaLabel(conflictsSource); - const handle = this.notificationService.prompt(Severity.Warning, localize('conflicts detected', "Unable to sync due to conflicts in {0}. Please resolve them to continue.", conflictsArea), + const handle = this.notificationService.prompt(Severity.Warning, localize('conflicts detected', "Unable to sync due to conflicts in {0}. Please resolve them to continue.", conflictsArea.toLowerCase()), [ { label: localize('accept remote', "Accept Remote"), @@ -409,9 +420,9 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo const sourceArea = getSyncAreaLabel(error.source); this.notificationService.notify({ severity: Severity.Error, - message: localize('too large', "Disabled syncing {0} because size of the {1} file to sync is larger than {2}. Please open the file and reduce the size and enable sync", sourceArea, sourceArea, '100kb'), + message: localize('too large', "Disabled syncing {0} because size of the {1} file to sync is larger than {2}. Please open the file and reduce the size and enable sync", sourceArea.toLowerCase(), sourceArea.toLowerCase(), '100kb'), actions: { - primary: [new Action('open sync file', localize('open file', "Open {0} file", sourceArea), undefined, true, + primary: [new Action('open sync file', localize('open file', "Open {0} File", sourceArea), undefined, true, () => error.source === SyncSource.Settings ? this.preferencesService.openGlobalSettings(true) : this.preferencesService.openGlobalKeybindingSettings(true))] } }); @@ -450,22 +461,31 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } private handleInvalidContentError(source: SyncSource): void { - if (!this.invalidContentErrorDisposables.has(source)) { - const errorArea = getSyncAreaLabel(source); - const handle = this.notificationService.notify({ - severity: Severity.Error, - message: localize('errorInvalidConfiguration', "Unable to sync {0} because there are some errors/warnings in the file. Please open the file to correct errors/warnings in it.", errorArea), - actions: { - primary: [new Action('open sync file', localize('open file', "Open {0} file", errorArea), undefined, true, - () => source === SyncSource.Settings ? this.preferencesService.openGlobalSettings(true) : this.preferencesService.openGlobalKeybindingSettings(true))] - } - }); - this.invalidContentErrorDisposables.set(source, toDisposable(() => { - // close the error warning notification - handle.close(); - this.invalidContentErrorDisposables.delete(source); - })); + if (this.invalidContentErrorDisposables.has(source)) { + return; } + if (source !== SyncSource.Settings && source !== SyncSource.Keybindings) { + return; + } + const resource = source === SyncSource.Settings ? this.workbenchEnvironmentService.settingsResource : this.workbenchEnvironmentService.keybindingsResource; + if (isEqual(resource, toResource(this.editorService.activeEditor, { supportSideBySide: SideBySideEditor.MASTER }))) { + // Do not show notification if the file in error is active + return; + } + const errorArea = getSyncAreaLabel(source); + const handle = this.notificationService.notify({ + severity: Severity.Error, + message: localize('errorInvalidConfiguration', "Unable to sync {0} because there are some errors/warnings in the file. Please open the file to correct errors/warnings in it.", errorArea.toLowerCase()), + actions: { + primary: [new Action('open sync file', localize('open file', "Open {0} File", errorArea), undefined, true, + () => source === SyncSource.Settings ? this.preferencesService.openGlobalSettings(true) : this.preferencesService.openGlobalKeybindingSettings(true))] + } + }); + this.invalidContentErrorDisposables.set(source, toDisposable(() => { + // close the error warning notification + handle.close(); + this.invalidContentErrorDisposables.delete(source); + })); } private async updateBadge(): Promise { @@ -493,7 +513,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo localize('sync preview message', "Synchronizing your preferences is a preview feature, please read the documentation before turning it on."), [ localize('open doc', "Open Documentation"), - localize('confirm', "Continue"), + localize('turn on sync', "Turn on Sync"), localize('cancel', "Cancel"), ], { @@ -501,7 +521,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } ); switch (result.choice) { - case 0: this.openerService.open(URI.parse('https://go.microsoft.com/fwlink/?LinkId=827846')); return; + case 0: this.openerService.open(URI.parse('https://aka.ms/vscode-settings-sync-help')); return; case 2: return; } } @@ -509,11 +529,11 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo const disposables: DisposableStore = new DisposableStore(); const quickPick = this.quickInputService.createQuickPick(); disposables.add(quickPick); - quickPick.title = localize('turn on sync', "Turn on Sync"); + quickPick.title = localize('turn on title', "Sync: Turn On"); quickPick.ok = false; quickPick.customButton = true; if (this.authenticationState.get() === AuthStatus.SignedIn) { - quickPick.customLabel = localize('turn on', "Turn on"); + quickPick.customLabel = localize('turn on', "Turn On"); } else { const displayName = this.authenticationService.getDisplayName(this.userDataSyncStore!.authenticationProviderId); quickPick.description = localize('sign in and turn on sync detail', "Sign in with your {0} account to synchronize your data across devices.", displayName); @@ -538,6 +558,39 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } private async doTurnOn(): Promise { + if (this.authenticationState.get() === AuthStatus.SignedIn) { + await new Promise((c, e) => { + const disposables: DisposableStore = new DisposableStore(); + const displayName = this.authenticationService.getDisplayName(this.userDataSyncStore!.authenticationProviderId); + const quickPick = this.quickInputService.createQuickPick<{ id: string, label: string, description?: string, detail?: string }>(); + disposables.add(quickPick); + const chooseAnotherItemId = 'chooseAnother'; + quickPick.title = localize('pick account', "{0}: Pick an account", displayName); + quickPick.ok = false; + quickPick.placeholder = localize('choose account placeholder', "Pick an account for syncing"); + quickPick.ignoreFocusOut = true; + quickPick.items = [{ + id: 'existing', + label: localize('existing', "{0}", this.activeAccount!.accountName), + detail: localize('signed in', "Signed in"), + }, { + id: chooseAnotherItemId, + label: localize('choose another', "Use another account") + }]; + disposables.add(quickPick.onDidAccept(async () => { + if (quickPick.selectedItems.length) { + if (quickPick.selectedItems[0].id === chooseAnotherItemId) { + await this.authenticationService.logout(this.userDataSyncStore!.authenticationProviderId, this.activeAccount!.id); + await this.setActiveAccount(undefined); + } + quickPick.hide(); + c(); + } + })); + disposables.add(quickPick.onDidHide(() => disposables.dispose())); + quickPick.show(); + }); + } if (this.authenticationState.get() === AuthStatus.SignedOut) { await this.signIn(); } @@ -608,7 +661,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } const result = await this.dialogService.show( Severity.Info, - localize('firs time sync', "First time Sync"), + localize('firs time sync', "Sync"), [ localize('merge', "Merge"), localize('cancel', "Cancel"), @@ -616,7 +669,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo ], { cancelId: 1, - detail: localize('first time sync detail', "Synchronizing from this device for the first time.\nWould you like to merge or replace with the data from the cloud?"), + detail: localize('first time sync detail', "It looks like this is the first time sync is set up.\nWould you like to merge or replace with the data from the cloud?"), } ); switch (result.choice) { @@ -638,7 +691,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo type: 'info', message: localize('turn off sync confirmation', "Turn off Sync"), detail: localize('turn off sync detail', "Your settings, keybindings, extensions and UI State will no longer be synced."), - primaryButton: localize('turn off', "Turn off"), + primaryButton: localize('turn off', "Turn Off"), checkbox: { label: localize('turn off sync everywhere', "Turn off sync on all your devices and clear the data from the cloud.") } @@ -792,7 +845,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } private registerShowSettingsConflictsAction(): void { - const resolveSettingsConflictsWhenContext = ContextKeyRegexExpr.create(CONTEXT_CONFLICTS_SOURCES.keys()[0], /.*settings.*/i); + const resolveSettingsConflictsWhenContext = ContextKeyExpr.regex(CONTEXT_CONFLICTS_SOURCES.keys()[0], /.*settings.*/i); CommandsRegistry.registerCommand(resolveSettingsConflictsCommand.id, () => this.handleConflicts(SyncSource.Settings)); MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { group: '5_sync', @@ -819,7 +872,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } private registerShowKeybindingsConflictsAction(): void { - const resolveKeybindingsConflictsWhenContext = ContextKeyRegexExpr.create(CONTEXT_CONFLICTS_SOURCES.keys()[0], /.*keybindings.*/i); + const resolveKeybindingsConflictsWhenContext = ContextKeyExpr.regex(CONTEXT_CONFLICTS_SOURCES.keys()[0], /.*keybindings.*/i); CommandsRegistry.registerCommand(resolveKeybindingsConflictsCommand.id, () => this.handleConflicts(SyncSource.Keybindings)); MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { group: '5_sync', @@ -874,7 +927,9 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo return new Promise((c, e) => { const quickInputService = accessor.get(IQuickInputService); const commandService = accessor.get(ICommandService); + const disposables = new DisposableStore(); const quickPick = quickInputService.createQuickPick(); + disposables.add(quickPick); const items: Array = []; if (that.userDataSyncService.conflictsSources.length) { for (const source of that.userDataSyncService.conflictsSources) { @@ -893,12 +948,12 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo items.push({ id: showSyncSettingsCommand.id, label: showSyncSettingsCommand.title }); items.push({ id: showSyncActivityCommand.id, label: showSyncActivityCommand.title(that.userDataSyncService) }); items.push({ type: 'separator' }); - items.push({ id: stopSyncCommand.id, label: stopSyncCommand.title(that.activeAccount), }); + items.push({ id: stopSyncCommand.id, label: stopSyncCommand.title(that.userDataSyncStore!.authenticationProviderId, that.activeAccount, that.authenticationService) }); quickPick.items = items; - const disposables = new DisposableStore(); disposables.add(quickPick.onDidAccept(() => { if (quickPick.selectedItems[0] && quickPick.selectedItems[0].id) { - commandService.executeCommand(quickPick.selectedItems[0].id); + // Introduce timeout as workaround - #91661 #91740 + timeout(0).then(() => commandService.executeCommand(quickPick.selectedItems[0].id!)); } quickPick.hide(); })); @@ -918,7 +973,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo constructor() { super({ id: stopSyncCommand.id, - title: stopSyncCommand.title(that.activeAccount), + title: stopSyncCommand.title(that.userDataSyncStore!.authenticationProviderId, that.activeAccount, that.authenticationService), menu: { id: MenuId.CommandPalette, when: ContextKeyExpr.and(CONTEXT_SYNC_STATE.notEqualsTo(SyncStatus.Uninitialized), CONTEXT_SYNC_ENABLEMENT), @@ -1091,8 +1146,8 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio ? localize('Sync accept remote', "Sync: {0}", acceptRemoteLabel) : localize('Sync accept local', "Sync: {0}", acceptLocalLabel), message: isRemote - ? localize('confirm replace and overwrite local', "Would you like to accept Remote {0} and replace Local {1}?", syncAreaLabel, syncAreaLabel) - : localize('confirm replace and overwrite remote', "Would you like to accept Local {0} and replace Remote {1}?", syncAreaLabel, syncAreaLabel), + ? localize('confirm replace and overwrite local', "Would you like to accept remote {0} and replace local {1}?", syncAreaLabel.toLowerCase(), syncAreaLabel.toLowerCase()) + : localize('confirm replace and overwrite remote', "Would you like to accept local {0} and replace remote {1}?", syncAreaLabel.toLowerCase(), syncAreaLabel.toLowerCase()), primaryButton: isRemote ? acceptRemoteLabel : acceptLocalLabel }); if (result.confirmed) { diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts index 43b93c20ab2..1642cd210a4 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts @@ -16,8 +16,8 @@ import { IViewlet } from 'vs/workbench/common/viewlet'; export class UserDataSyncTrigger extends Disposable { - private readonly _onDidTriggerSync: Emitter = this._register(new Emitter()); - readonly onDidTriggerSync: Event = this._onDidTriggerSync.event; + private readonly _onDidTriggerSync: Emitter = this._register(new Emitter()); + readonly onDidTriggerSync: Event = this._onDidTriggerSync.event; constructor( @IEditorService editorService: IEditorService, @@ -25,37 +25,44 @@ export class UserDataSyncTrigger extends Disposable { @IViewletService viewletService: IViewletService, ) { super(); - this._register(Event.debounce(Event.any( - Event.filter(editorService.onDidActiveEditorChange, () => this.isUserDataEditorInput(editorService.activeEditor)), - Event.filter(viewletService.onDidViewletOpen, viewlet => this.isUserDataViewlet(viewlet)) - ), () => undefined, 500)(() => this._onDidTriggerSync.fire())); + this._register(Event.any( + Event.map(editorService.onDidActiveEditorChange, () => this.getUserDataEditorInputSource(editorService.activeEditor)), + Event.map(viewletService.onDidViewletOpen, viewlet => this.getUserDataViewletSource(viewlet)) + )(source => { + if (source) { + this._onDidTriggerSync.fire(source); + } + })); } - private isUserDataViewlet(viewlet: IViewlet): boolean { - return viewlet.getId() === VIEWLET_ID; + private getUserDataViewletSource(viewlet: IViewlet): string | undefined { + if (viewlet.getId() === VIEWLET_ID) { + return 'extensionsViewlet'; + } + return undefined; } - private isUserDataEditorInput(editorInput: IEditorInput | undefined): boolean { + private getUserDataEditorInputSource(editorInput: IEditorInput | undefined): string | undefined { if (!editorInput) { - return false; + return undefined; } if (editorInput instanceof SettingsEditor2Input) { - return true; + return 'settingsEditor'; } if (editorInput instanceof PreferencesEditorInput) { - return true; + return 'settingsEditor'; } if (editorInput instanceof KeybindingsEditorInput) { - return true; + return 'keybindingsEditor'; } const resource = editorInput.resource; if (isEqual(resource, this.workbenchEnvironmentService.settingsResource)) { - return true; + return 'settingsEditor'; } if (isEqual(resource, this.workbenchEnvironmentService.keybindingsResource)) { - return true; + return 'keybindingsEditor'; } - return false; + return undefined; } } diff --git a/src/vs/workbench/contrib/webview/browser/webviewCommands.ts b/src/vs/workbench/contrib/webview/browser/webviewCommands.ts index 5f870d1d15e..66a2f7f9955 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewCommands.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewCommands.ts @@ -7,7 +7,7 @@ import { Action } from 'vs/base/common/actions'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import * as nls from 'vs/nls'; import { Action2 } from 'vs/platform/actions/common/actions'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE } from 'vs/workbench/contrib/webview/browser/webview'; @@ -19,7 +19,7 @@ export class ShowWebViewEditorFindWidgetAction extends Action2 { public static readonly ID = 'editor.action.webvieweditor.showFind'; public static readonly LABEL = nls.localize('editor.action.webvieweditor.showFind', "Show find"); - constructor(contextKeyExpr: ContextKeyExpr) { + constructor(contextKeyExpr: ContextKeyExpression) { super({ id: ShowWebViewEditorFindWidgetAction.ID, title: ShowWebViewEditorFindWidgetAction.LABEL, @@ -40,7 +40,7 @@ export class HideWebViewEditorFindCommand extends Action2 { public static readonly ID = 'editor.action.webvieweditor.hideFind'; public static readonly LABEL = nls.localize('editor.action.webvieweditor.hideFind', "Stop find"); - constructor(contextKeyExpr: ContextKeyExpr) { + constructor(contextKeyExpr: ContextKeyExpression) { super({ id: HideWebViewEditorFindCommand.ID, title: HideWebViewEditorFindCommand.LABEL, @@ -61,7 +61,7 @@ export class WebViewEditorFindNextCommand extends Action2 { public static readonly ID = 'editor.action.webvieweditor.findNext'; public static readonly LABEL = nls.localize('editor.action.webvieweditor.findNext', 'Find next'); - constructor(contextKeyExpr: ContextKeyExpr) { + constructor(contextKeyExpr: ContextKeyExpression) { super({ id: WebViewEditorFindNextCommand.ID, title: WebViewEditorFindNextCommand.LABEL, @@ -82,7 +82,7 @@ export class WebViewEditorFindPreviousCommand extends Action2 { public static readonly ID = 'editor.action.webvieweditor.findPrevious'; public static readonly LABEL = nls.localize('editor.action.webvieweditor.findPrevious', 'Find previous'); - constructor(contextKeyExpr: ContextKeyExpr) { + constructor(contextKeyExpr: ContextKeyExpression) { super({ id: WebViewEditorFindPreviousCommand.ID, title: WebViewEditorFindPreviousCommand.LABEL, @@ -103,7 +103,7 @@ export class SelectAllWebviewEditorCommand extends Action2 { public static readonly ID = 'editor.action.webvieweditor.selectAll'; public static readonly LABEL = nls.localize('editor.action.webvieweditor.selectAll', 'Select all'); - constructor(contextKeyExpr: ContextKeyExpr) { + constructor(contextKeyExpr: ContextKeyExpression) { const precondition = ContextKeyExpr.and(contextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)); super({ id: SelectAllWebviewEditorCommand.ID, diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewCommands.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewCommands.ts index ebbe40c7f42..43c0e653b7f 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewCommands.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewCommands.ts @@ -9,7 +9,7 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import * as nls from 'vs/nls'; import { Action2 } from 'vs/platform/actions/common/actions'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { WebviewEditorOverlay, webviewHasOwnEditFunctionsContextKey } from 'vs/workbench/contrib/webview/browser/webview'; @@ -42,7 +42,7 @@ export class CopyWebviewEditorCommand extends Action2 { public static readonly ID = 'editor.action.webvieweditor.copy'; public static readonly LABEL = nls.localize('editor.action.webvieweditor.copy', "Copy2"); - constructor(contextKeyExpr: ContextKeyExpr) { + constructor(contextKeyExpr: ContextKeyExpression) { super({ id: CopyWebviewEditorCommand.ID, title: CopyWebviewEditorCommand.LABEL, @@ -63,7 +63,7 @@ export class PasteWebviewEditorCommand extends Action2 { public static readonly ID = 'editor.action.webvieweditor.paste'; public static readonly LABEL = nls.localize('editor.action.webvieweditor.paste', 'Paste'); - constructor(contextKeyExpr: ContextKeyExpr) { + constructor(contextKeyExpr: ContextKeyExpression) { super({ id: PasteWebviewEditorCommand.ID, title: PasteWebviewEditorCommand.LABEL, @@ -84,7 +84,7 @@ export class CutWebviewEditorCommand extends Action2 { public static readonly ID = 'editor.action.webvieweditor.cut'; public static readonly LABEL = nls.localize('editor.action.webvieweditor.cut', 'Cut'); - constructor(contextKeyExpr: ContextKeyExpr) { + constructor(contextKeyExpr: ContextKeyExpression) { super({ id: CutWebviewEditorCommand.ID, title: CutWebviewEditorCommand.LABEL, @@ -105,7 +105,7 @@ export class UndoWebviewEditorCommand extends Action2 { public static readonly ID = 'editor.action.webvieweditor.undo'; public static readonly LABEL = nls.localize('editor.action.webvieweditor.undo', "Undo"); - constructor(contextKeyExpr: ContextKeyExpr) { + constructor(contextKeyExpr: ContextKeyExpression) { super({ id: UndoWebviewEditorCommand.ID, title: UndoWebviewEditorCommand.LABEL, @@ -126,7 +126,7 @@ export class RedoWebviewEditorCommand extends Action2 { public static readonly ID = 'editor.action.webvieweditor.redo'; public static readonly LABEL = nls.localize('editor.action.webvieweditor.redo', "Redo"); - constructor(contextKeyExpr: ContextKeyExpr) { + constructor(contextKeyExpr: ContextKeyExpression) { super({ id: RedoWebviewEditorCommand.ID, title: RedoWebviewEditorCommand.LABEL, diff --git a/src/vs/workbench/contrib/welcome/common/viewsWelcomeContribution.ts b/src/vs/workbench/contrib/welcome/common/viewsWelcomeContribution.ts index 51fb3194e95..e7e50346aa5 100644 --- a/src/vs/workbench/contrib/welcome/common/viewsWelcomeContribution.ts +++ b/src/vs/workbench/contrib/welcome/common/viewsWelcomeContribution.ts @@ -7,9 +7,9 @@ import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IExtensionPoint } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { ViewsWelcomeExtensionPoint, ViewWelcome, viewsWelcomeExtensionPointDescriptor } from './viewsWelcomeExtensionPoint'; +import { ViewsWelcomeExtensionPoint, ViewWelcome, viewsWelcomeExtensionPointDescriptor, ViewIdentifierMap } from './viewsWelcomeExtensionPoint'; import { Registry } from 'vs/platform/registry/common/platform'; -import { Extensions as ViewContainerExtensions, IViewsRegistry } from 'vs/workbench/common/views'; +import { Extensions as ViewContainerExtensions, IViewsRegistry, ViewContentPriority } from 'vs/workbench/common/views'; import { localize } from 'vs/nls'; const viewsRegistry = Registry.as(ViewContainerExtensions.ViewsRegistry); @@ -45,9 +45,11 @@ export class ViewsWelcomeContribution extends Disposable implements IWorkbenchCo } for (const welcome of contribution.value) { - const disposable = viewsRegistry.registerViewWelcomeContent(welcome.view, { + const id = ViewIdentifierMap[welcome.view] ?? welcome.view; + const disposable = viewsRegistry.registerViewWelcomeContent(id, { content: welcome.contents, - when: ContextKeyExpr.deserialize(welcome.when) + when: ContextKeyExpr.deserialize(welcome.when), + priority: contribution.description.isBuiltin ? ViewContentPriority.Low : ViewContentPriority.Lowest }); this.viewWelcomeContents.set(welcome, disposable); diff --git a/src/vs/workbench/contrib/welcome/common/viewsWelcomeExtensionPoint.ts b/src/vs/workbench/contrib/welcome/common/viewsWelcomeExtensionPoint.ts index 28a5bc02dfa..9a1fdbe078e 100644 --- a/src/vs/workbench/contrib/welcome/common/viewsWelcomeExtensionPoint.ts +++ b/src/vs/workbench/contrib/welcome/common/viewsWelcomeExtensionPoint.ts @@ -20,9 +20,15 @@ export interface ViewWelcome { export type ViewsWelcomeExtensionPoint = ViewWelcome[]; +export const ViewIdentifierMap: { [key: string]: string } = { + 'explorer': 'workbench.explorer.emptyView', + 'debug': 'workbench.debug.startView', + 'scm': 'workbench.scm', +}; + const viewsWelcomeExtensionPointSchema = Object.freeze({ type: 'array', - description: nls.localize('contributes.viewsWelcome', "Contributed views welcome content."), + description: nls.localize('contributes.viewsWelcome', "Contributed views welcome content. Welcome content will be rendered in views whenever they have no meaningful content to display, ie. the File Explorer when no folder is open. Such content is useful as in-product documentation to drive users to use certain features before they are available. A good example would be a `Clone Repository` button in the File Explorer welcome view."), items: { type: 'object', description: nls.localize('contributes.viewsWelcome.view', "Contributed welcome content for a specific view."), @@ -33,15 +39,16 @@ const viewsWelcomeExtensionPointSchema = Object.freeze { +export function main(configuration: INativeWindowConfiguration): Promise { const renderer = new DesktopMain(configuration); return renderer.open(); diff --git a/src/vs/workbench/electron-browser/window.ts b/src/vs/workbench/electron-browser/window.ts index d4c3246d98d..a528ed51106 100644 --- a/src/vs/workbench/electron-browser/window.ts +++ b/src/vs/workbench/electron-browser/window.ts @@ -62,6 +62,7 @@ import { IElectronEnvironmentService } from 'vs/workbench/services/electron/elec import { IWorkingCopyService, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { AutoSaveMode, IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { Event } from 'vs/base/common/event'; +import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; export class ElectronWindow extends Disposable { @@ -94,7 +95,7 @@ export class ElectronWindow extends Disposable { @IMenuService private readonly menuService: IMenuService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @IIntegrityService private readonly integrityService: IIntegrityService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService, @IAccessibilityService private readonly accessibilityService: IAccessibilityService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -613,6 +614,8 @@ export class ElectronWindow extends Disposable { } private trackClosedWaitFiles(waitMarkerFile: URI, resourcesToWaitFor: URI[]): IDisposable { + let remainingResourcesToWaitFor = resourcesToWaitFor.slice(0); + // In wait mode, listen to changes to the editors and wait until the files // are closed that the user wants to wait for. When this happens we delete // the wait marker file to signal to the outside that editing is done. @@ -622,7 +625,7 @@ export class ElectronWindow extends Disposable { // Remove from resources to wait for based on the // resources from editors that got closed - resourcesToWaitFor = resourcesToWaitFor.filter(resourceToWaitFor => { + remainingResourcesToWaitFor = remainingResourcesToWaitFor.filter(resourceToWaitFor => { if (isEqual(resourceToWaitFor, masterResource) || isEqual(resourceToWaitFor, detailsResource)) { return false; // remove - the closing editor matches this resource } @@ -630,7 +633,7 @@ export class ElectronWindow extends Disposable { return true; // keep - not yet closed }); - if (resourcesToWaitFor.length === 0) { + if (remainingResourcesToWaitFor.length === 0) { // If auto save is configured with the default delay (1s) it is possible // to close the editor while the save still continues in the background. As such // we have to also check if the files to wait for are dirty and if so wait diff --git a/src/vs/workbench/services/accessibility/node/accessibilityService.ts b/src/vs/workbench/services/accessibility/electron-browser/accessibilityService.ts similarity index 91% rename from src/vs/workbench/services/accessibility/node/accessibilityService.ts rename to src/vs/workbench/services/accessibility/electron-browser/accessibilityService.ts index 38d831833b1..19d44947f6b 100644 --- a/src/vs/workbench/services/accessibility/node/accessibilityService.ts +++ b/src/vs/workbench/services/accessibility/electron-browser/accessibilityService.ts @@ -16,6 +16,7 @@ import { IJSONEditingService } from 'vs/workbench/services/configuration/common/ import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; interface AccessibilityMetrics { enabled: boolean; @@ -24,14 +25,14 @@ type AccessibilityMetricsClassification = { enabled: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; }; -export class NodeAccessibilityService extends AccessibilityService implements IAccessibilityService { +export class NativeAccessibilityService extends AccessibilityService implements IAccessibilityService { _serviceBrand: undefined; private didSendTelemetry = false; constructor( - @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IWorkbenchEnvironmentService environmentService: INativeWorkbenchEnvironmentService, @IContextKeyService contextKeyService: IContextKeyService, @IConfigurationService configurationService: IConfigurationService, @ITelemetryService private readonly _telemetryService: ITelemetryService @@ -69,7 +70,7 @@ export class NodeAccessibilityService extends AccessibilityService implements IA } } -registerSingleton(IAccessibilityService, NodeAccessibilityService, true); +registerSingleton(IAccessibilityService, NativeAccessibilityService, true); // On linux we do not automatically detect that a screen reader is detected, thus we have to implicitly notify the renderer to enable accessibility when user configures it in settings class LinuxAccessibilityContribution implements IWorkbenchContribution { diff --git a/src/vs/workbench/services/activity/common/activity.ts b/src/vs/workbench/services/activity/common/activity.ts index 628590befc4..d8bdaca851b 100644 --- a/src/vs/workbench/services/activity/common/activity.ts +++ b/src/vs/workbench/services/activity/common/activity.ts @@ -6,14 +6,25 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +export const IActivityService = createDecorator('activityService'); + +export interface IActivityService { + + _serviceBrand: undefined; + + /** + * Show activity in the panel for the given panel or in the activitybar for the given viewlet or global action. + */ + showActivity(compositeOrActionId: string, badge: IBadge, clazz?: string, priority?: number): IDisposable; +} + export interface IBadge { getDescription(): string; } -export class BaseBadge implements IBadge { - descriptorFn: (args: any) => string; +class BaseBadge implements IBadge { - constructor(descriptorFn: (args: any) => string) { + constructor(public readonly descriptorFn: (arg: any) => string) { this.descriptorFn = descriptorFn; } @@ -23,9 +34,8 @@ export class BaseBadge implements IBadge { } export class NumberBadge extends BaseBadge { - number: number; - constructor(number: number, descriptorFn: (args: any) => string) { + constructor(public readonly number: number, descriptorFn: (num: number) => string) { super(descriptorFn); this.number = number; @@ -37,31 +47,17 @@ export class NumberBadge extends BaseBadge { } export class TextBadge extends BaseBadge { - text: string; - constructor(text: string, descriptorFn: (args: any) => string) { + constructor(public readonly text: string, descriptorFn: () => string) { super(descriptorFn); - - this.text = text; } } export class IconBadge extends BaseBadge { - constructor(descriptorFn: (args: any) => string) { + constructor(descriptorFn: () => string) { super(descriptorFn); } } export class ProgressBadge extends BaseBadge { } - -export const IActivityService = createDecorator('activityService'); - -export interface IActivityService { - _serviceBrand: undefined; - - /** - * Show activity in the panel for the given panel or in the activitybar for the given viewlet or global action. - */ - showActivity(compositeOrActionId: string, badge: IBadge, clazz?: string, priority?: number): IDisposable; -} diff --git a/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts b/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts index b7f29e36862..dcb5a2c3346 100644 --- a/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts +++ b/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts @@ -130,6 +130,7 @@ class BulkEditModel implements IDisposable { private _tasks: ModelEditTask[] | undefined; constructor( + private readonly _label: string | undefined, private readonly _editor: ICodeEditor | undefined, private readonly _progress: IProgress, edits: WorkspaceTextEdit[], @@ -232,7 +233,7 @@ class BulkEditModel implements IDisposable { } const multiModelEditStackElement = new MultiModelEditStackElement( - localize('workspaceEdit', "Workspace Edit"), + this._label || localize('workspaceEdit', "Workspace Edit"), tasks.map(t => new EditStackElement(t.model, t.getBeforeCursorState())) ); this._undoRedoService.pushElement(multiModelEditStackElement); @@ -250,11 +251,13 @@ type Edit = WorkspaceFileEdit | WorkspaceTextEdit; class BulkEdit { + private readonly _label: string | undefined; private readonly _edits: Edit[] = []; private readonly _editor: ICodeEditor | undefined; private readonly _progress: IProgress; constructor( + label: string | undefined, editor: ICodeEditor | undefined, progress: IProgress | undefined, edits: Edit[], @@ -265,6 +268,7 @@ class BulkEdit { @IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService, @IConfigurationService private readonly _configurationService: IConfigurationService ) { + this._label = label; this._editor = editor; this._progress = progress || Progress.None; this._edits = edits; @@ -361,7 +365,7 @@ class BulkEdit { private async _performTextEdits(edits: WorkspaceTextEdit[], progress: IProgress): Promise { this._logService.debug('_performTextEdits', JSON.stringify(edits)); - const model = this._instaService.createInstance(BulkEditModel, this._editor, progress, edits); + const model = this._instaService.createInstance(BulkEditModel, this._label, this._editor, progress, edits); await model.prepare(); @@ -439,7 +443,7 @@ export class BulkEditService implements IBulkEditService { // If the code editor is readonly still allow bulk edits to be applied #68549 codeEditor = undefined; } - const bulkEdit = this._instaService.createInstance(BulkEdit, codeEditor, options?.progress, edits); + const bulkEdit = this._instaService.createInstance(BulkEdit, options?.quotableLabel || options?.label, codeEditor, options?.progress, edits); return bulkEdit.perform().then(() => { return { ariaSummary: bulkEdit.ariaMessage() }; }).catch(err => { diff --git a/src/vs/workbench/services/configuration/common/configurationEditingService.ts b/src/vs/workbench/services/configuration/common/configurationEditingService.ts index 29f4c406cc6..6b15032cd5c 100644 --- a/src/vs/workbench/services/configuration/common/configurationEditingService.ts +++ b/src/vs/workbench/services/configuration/common/configurationEditingService.ts @@ -230,7 +230,7 @@ export class ConfigurationEditingService { } } - private onInvalidConfigurationError(error: ConfigurationEditingError, operation: IConfigurationEditOperation, ): void { + private onInvalidConfigurationError(error: ConfigurationEditingError, operation: IConfigurationEditOperation,): void { const openStandAloneConfigurationActionLabel = operation.workspaceStandAloneConfigurationKey === TASKS_CONFIGURATION_KEY ? nls.localize('openTasksConfiguration', "Open Tasks Configuration") : operation.workspaceStandAloneConfigurationKey === LAUNCH_CONFIGURATION_KEY ? nls.localize('openLaunchConfiguration', "Open Launch Configuration") : null; diff --git a/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts b/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts index e86e00ffc4e..80b0cd54c2a 100644 --- a/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts +++ b/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts @@ -340,7 +340,7 @@ export class ConfigurationResolverService extends BaseConfigurationResolverServi @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, @IQuickInputService quickInputService: IQuickInputService ) { - super(environmentService.configuration.userEnv, editorService, environmentService, configurationService, commandService, workspaceContextService, quickInputService); + super(Object.create(null), editorService, environmentService, configurationService, commandService, workspaceContextService, quickInputService); } } diff --git a/src/vs/workbench/services/configurationResolver/common/configurationResolverSchema.ts b/src/vs/workbench/services/configurationResolver/common/configurationResolverSchema.ts index 6350d247915..2fceba9e4ac 100644 --- a/src/vs/workbench/services/configurationResolver/common/configurationResolverSchema.ts +++ b/src/vs/workbench/services/configurationResolver/common/configurationResolverSchema.ts @@ -46,7 +46,7 @@ export const inputsSchema: IJSONSchema = { }, password: { type: 'boolean', - description: nls.localize('JsonSchema.input.password', "Set to true to show a password prompt that will not show the typed value."), + description: nls.localize('JsonSchema.input.password', "Controls if a password input is shown. Password input hides the typed text."), }, } }, diff --git a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts index 5f4b1cbb52a..277923ee5be 100644 --- a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts +++ b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts @@ -9,7 +9,7 @@ import * as platform from 'vs/base/common/platform'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; -import { ConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/configurationResolverService'; +import { BaseConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/configurationResolverService'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { TestEditorService, TestContextService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestWindowConfiguration } from 'vs/workbench/test/electron-browser/workbenchTestServices'; @@ -21,7 +21,6 @@ import * as Types from 'vs/base/common/types'; import { EditorType } from 'vs/editor/common/editorCommon'; import { Selection } from 'vs/editor/common/core/selection'; import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; const mockLineNumber = 10; class TestEditorServiceWithActiveEditor extends TestEditorService { @@ -37,10 +36,14 @@ class TestEditorServiceWithActiveEditor extends TestEditorService { } } +class TestConfigurationResolverService extends BaseConfigurationResolverService { + +} + suite('Configuration Resolver Service', () => { let configurationResolverService: IConfigurationResolverService | null; let envVariables: { [key: string]: string } = { key1: 'Value for key1', key2: 'Value for key2' }; - let environmentService: IWorkbenchEnvironmentService; + let environmentService: MockWorkbenchEnvironmentService; let mockCommandService: MockCommandService; let editorService: TestEditorServiceWithActiveEditor; let workspace: IWorkspaceFolder; @@ -57,7 +60,7 @@ suite('Configuration Resolver Service', () => { index: 0, toResource: (path: string) => uri.file(path) }; - configurationResolverService = new ConfigurationResolverService(editorService, environmentService, new MockInputsConfigurationService(), mockCommandService, new TestContextService(), quickInputService); + configurationResolverService = new TestConfigurationResolverService(environmentService.userEnv, editorService, environmentService, new MockInputsConfigurationService(), mockCommandService, new TestContextService(), quickInputService); }); teardown(() => { @@ -136,7 +139,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new ConfigurationResolverService(new TestEditorServiceWithActiveEditor(), environmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); + let service = new TestConfigurationResolverService(environmentService.userEnv, new TestEditorServiceWithActiveEditor(), environmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.fontFamily} xyz'), 'abc foo xyz'); }); @@ -153,7 +156,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new ConfigurationResolverService(new TestEditorServiceWithActiveEditor(), environmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); + let service = new TestConfigurationResolverService(environmentService.userEnv, new TestEditorServiceWithActiveEditor(), environmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.fontFamily} ${config:terminal.integrated.fontFamily} xyz'), 'abc foo bar xyz'); }); @@ -170,7 +173,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new ConfigurationResolverService(new TestEditorServiceWithActiveEditor(), environmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); + let service = new TestConfigurationResolverService(environmentService.userEnv, new TestEditorServiceWithActiveEditor(), environmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); if (platform.isWindows) { assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.fontFamily} ${workspaceFolder} ${env:key1} xyz'), 'abc foo \\VSCode\\workspaceLocation Value for key1 xyz'); } else { @@ -191,7 +194,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new ConfigurationResolverService(new TestEditorServiceWithActiveEditor(), environmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); + let service = new TestConfigurationResolverService(environmentService.userEnv, new TestEditorServiceWithActiveEditor(), environmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); if (platform.isWindows) { assert.strictEqual(service.resolve(workspace, '${config:editor.fontFamily} ${config:terminal.integrated.fontFamily} ${workspaceFolder} - ${workspaceFolder} ${env:key1} - ${env:key2}'), 'foo bar \\VSCode\\workspaceLocation - \\VSCode\\workspaceLocation Value for key1 - Value for key2'); } else { @@ -225,7 +228,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new ConfigurationResolverService(new TestEditorServiceWithActiveEditor(), environmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); + let service = new TestConfigurationResolverService(environmentService.userEnv, new TestEditorServiceWithActiveEditor(), environmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.fontFamily} ${config:editor.lineNumbers} ${config:editor.insertSpaces} xyz'), 'abc foo 123 false xyz'); }); @@ -235,7 +238,7 @@ suite('Configuration Resolver Service', () => { editor: {} }); - let service = new ConfigurationResolverService(new TestEditorServiceWithActiveEditor(), environmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); + let service = new TestConfigurationResolverService(environmentService.userEnv, new TestEditorServiceWithActiveEditor(), environmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); assert.strictEqual(service.resolve(workspace, 'abc ${unknownVariable} xyz'), 'abc ${unknownVariable} xyz'); assert.strictEqual(service.resolve(workspace, 'abc ${env:unknownVariable} xyz'), 'abc xyz'); }); @@ -248,7 +251,7 @@ suite('Configuration Resolver Service', () => { } }); - let service = new ConfigurationResolverService(new TestEditorServiceWithActiveEditor(), environmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); + let service = new TestConfigurationResolverService(environmentService.userEnv, new TestEditorServiceWithActiveEditor(), environmentService, configurationService, mockCommandService, new TestContextService(), quickInputService); assert.throws(() => service.resolve(workspace, 'abc ${env} xyz')); assert.throws(() => service.resolve(workspace, 'abc ${env:} xyz')); @@ -619,7 +622,7 @@ class MockInputsConfigurationService extends TestConfigurationService { class MockWorkbenchEnvironmentService extends NativeWorkbenchEnvironmentService { - constructor(userEnv: platform.IProcessEnvironment) { + constructor(public userEnv: platform.IProcessEnvironment) { super({ ...TestWindowConfiguration, userEnv }, TestWindowConfiguration.execPath, TestWindowConfiguration.windowId); } } diff --git a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts index d481f2ee6ff..bdcff69df4f 100644 --- a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts @@ -92,6 +92,10 @@ export abstract class AbstractFileDialogService implements IFileDialogService { return ConfirmResult.DONT_SAVE; // no veto when we are in extension dev mode because we cannot assume we run interactive (e.g. tests) } + return this.doShowSaveConfirm(fileNamesOrResources); + } + + protected async doShowSaveConfirm(fileNamesOrResources: (string | URI)[]): Promise { if (fileNamesOrResources.length === 0) { return ConfirmResult.DONT_SAVE; } diff --git a/src/vs/workbench/services/dialogs/browser/dialogService.ts b/src/vs/workbench/services/dialogs/browser/dialogService.ts index f67f9aa064c..6b42535bff5 100644 --- a/src/vs/workbench/services/dialogs/browser/dialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/dialogService.ts @@ -18,6 +18,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IProductService } from 'vs/platform/product/common/productService'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { fromNow } from 'vs/base/common/date'; export class DialogService implements IDialogService { @@ -121,18 +122,24 @@ export class DialogService implements IDialogService { } async about(): Promise { - const detail = nls.localize('aboutDetail', - "Version: {0}\nCommit: {1}\nDate: {2}\nBrowser: {3}", - this.productService.version || 'Unknown', - this.productService.commit || 'Unknown', - this.productService.date || 'Unknown', - navigator.userAgent - ); + const detailString = (useAgo: boolean): string => { + return nls.localize('aboutDetail', + "Version: {0}\nCommit: {1}\nDate: {2}\nBrowser: {3}", + this.productService.version || 'Unknown', + this.productService.commit || 'Unknown', + this.productService.date ? `${this.productService.date}${useAgo ? ' (' + fromNow(new Date(this.productService.date), true) + ')' : ''}` : 'Unknown', + navigator.userAgent + ); + }; + + const detail = detailString(true); + const detailToCopy = detailString(false); + const { choice } = await this.show(Severity.Info, this.productService.nameLong, [nls.localize('copy', "Copy"), nls.localize('ok', "OK")], { detail, cancelId: 1 }); if (choice === 0) { - this.clipboardService.writeText(detail); + this.clipboardService.writeText(detailToCopy); } } } diff --git a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts index e9e8c8fff36..a3f45c88cdb 100644 --- a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts +++ b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts @@ -156,7 +156,7 @@ export class SimpleFileDialog { public async showOpenDialog(options: IOpenDialogOptions = {}): Promise { this.scheme = this.getScheme(options.availableFileSystems, options.defaultUri); - this.userHome = await this.remotePathService.userHome; + this.userHome = await this.getUserHome(); const newOptions = this.getOptions(options); if (!newOptions) { return Promise.resolve(undefined); @@ -167,7 +167,7 @@ export class SimpleFileDialog { public async showSaveDialog(options: ISaveDialogOptions): Promise { this.scheme = this.getScheme(options.availableFileSystems, options.defaultUri); - this.userHome = await this.remotePathService.userHome; + this.userHome = await this.getUserHome(); this.requiresTrailing = true; const newOptions = this.getOptions(options, true); if (!newOptions) { @@ -231,6 +231,13 @@ export class SimpleFileDialog { return this.remoteAgentEnvironment; } + private async getUserHome(): Promise { + if (this.scheme !== Schemas.file) { + return this.remotePathService.userHome; + } + return URI.from({ scheme: this.scheme, path: this.environmentService.userHome }); + } + private async pickResource(isSave: boolean = false): Promise { this.allowFolderSelection = !!this.options.canSelectFolders; this.allowFileSelection = !!this.options.canSelectFiles; @@ -290,6 +297,8 @@ export class SimpleFileDialog { function doResolve(dialog: SimpleFileDialog, uri: URI | undefined) { if (uri) { + uri = resources.addTrailingPathSeparator(uri, dialog.separator); // Ensures that c: is c:/ since this comes from user input and can be incorrect. + // To be consistent, we should never have a trailing path separator on directories (or anything else). Will not remove from c:/. uri = resources.removeTrailingPathSeparator(uri); } resolve(uri); diff --git a/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts b/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts index 43caf27af2e..969a1e5a5dc 100644 --- a/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts +++ b/src/vs/workbench/services/dialogs/electron-browser/dialogService.ts @@ -23,6 +23,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IElectronService } from 'vs/platform/electron/node/electron'; import { MessageBoxOptions } from 'electron'; +import { fromNow } from 'vs/base/common/date'; interface IMassagedMessageBoxOptions { @@ -216,17 +217,23 @@ class NativeDialogService implements IDialogService { } const isSnap = process.platform === 'linux' && process.env.SNAP && process.env.SNAP_REVISION; - const detail = nls.localize('aboutDetail', - "Version: {0}\nCommit: {1}\nDate: {2}\nElectron: {3}\nChrome: {4}\nNode.js: {5}\nV8: {6}\nOS: {7}", - version, - product.commit || 'Unknown', - product.date || 'Unknown', - process.versions['electron'], - process.versions['chrome'], - process.versions['node'], - process.versions['v8'], - `${os.type()} ${os.arch()} ${os.release()}${isSnap ? ' snap' : ''}` - ); + + const detailString = (useAgo: boolean): string => { + return nls.localize('aboutDetail', + "Version: {0}\nCommit: {1}\nDate: {2}\nElectron: {3}\nChrome: {4}\nNode.js: {5}\nV8: {6}\nOS: {7}", + version, + product.commit || 'Unknown', + product.date ? `${product.date}${useAgo ? ' (' + fromNow(new Date(product.date), true) + ')' : ''}` : 'Unknown', + process.versions['electron'], + process.versions['chrome'], + process.versions['node'], + process.versions['v8'], + `${os.type()} ${os.arch()} ${os.release()}${isSnap ? ' snap' : ''}` + ); + }; + + const detail = detailString(true); + const detailToCopy = detailString(false); const ok = nls.localize('okButton', "OK"); const copy = mnemonicButtonLabel(nls.localize({ key: 'copy', comment: ['&& denotes a mnemonic'] }, "&&Copy")); @@ -249,7 +256,7 @@ class NativeDialogService implements IDialogService { }); if (buttons[result.response] === copy) { - this.clipboardService.writeText(detail); + this.clipboardService.writeText(detailToCopy); } } } diff --git a/src/vs/workbench/services/dialogs/electron-browser/fileDialogService.ts b/src/vs/workbench/services/dialogs/electron-browser/fileDialogService.ts index 14b4f05f932..36e903296cb 100644 --- a/src/vs/workbench/services/dialogs/electron-browser/fileDialogService.ts +++ b/src/vs/workbench/services/dialogs/electron-browser/fileDialogService.ts @@ -198,7 +198,7 @@ export class FileDialogService extends AbstractFileDialogService implements IFil } } - return super.showSaveConfirm(fileNamesOrResources); + return super.doShowSaveConfirm(fileNamesOrResources); } } diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index 2583458a30d..fdfc4cb6d8d 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 { SideBySideEditor as SideBySideEditorChoice, IEditorInput, IEditor, GroupIdentifier, IFileEditorInput, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, IEditorInputFactoryRegistry, Extensions as EditorExtensions, EditorInput, SideBySideEditorInput, IEditorInputWithOptions, isEditorInputWithOptions, EditorOptions, TextEditorOptions, IEditorIdentifier, IEditorCloseEvent, ITextEditor, ITextDiffEditor, ITextSideBySideEditor, IRevertOptions, SaveReason, EditorsOrder, isTextEditor, IWorkbenchEditorConfiguration, toResource } from 'vs/workbench/common/editor'; +import { SideBySideEditor as SideBySideEditorChoice, IEditorInput, IEditor, GroupIdentifier, IFileEditorInput, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, IEditorInputFactoryRegistry, Extensions as EditorExtensions, EditorInput, SideBySideEditorInput, IEditorInputWithOptions, isEditorInputWithOptions, EditorOptions, TextEditorOptions, IEditorIdentifier, IEditorCloseEvent, ITextEditor, ITextDiffEditor, ITextSideBySideEditor, IRevertOptions, SaveReason, EditorsOrder, isTextEditor, IWorkbenchEditorConfiguration, toResource, IVisibleEditor } 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, isEqualOrParent, joinPath } from 'vs/base/common/resources'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; 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 { IResourceEditor, SIDE_GROUP, IResourceEditorReplacement, IOpenEditorOverrideHandler, 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'; import { coalesce, distinct } from 'vs/base/common/arrays'; @@ -84,7 +84,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()); + this.editorsObserver.onDidMostRecentlyActiveEditorsChange(() => this._onDidMostRecentlyActiveEditorsChange.fire()); // Out of workspace file watchers this._register(this.onDidVisibleEditorsChange(() => this.handleVisibleEditorsChange())); @@ -705,8 +705,18 @@ export class EditorService extends Disposable implements EditorServiceImpl { //#region isOpen() - isOpen(editor: IEditorInput): boolean { - return this.editorGroupService.groups.some(group => group.isOpened(editor)); + isOpen(editor: IEditorInput): boolean; + isOpen(editor: IResourceInput): boolean; + isOpen(editor: IEditorInput | IResourceInput): boolean { + if (editor instanceof EditorInput) { + return this.editorGroupService.groups.some(group => group.isOpened(editor)); + } + + if (editor.resource) { + return this.editorsObserver.hasEditor(editor.resource); + } + + return false; } //#endregion @@ -953,6 +963,10 @@ export class EditorService extends Disposable implements EditorServiceImpl { editors = [editors]; } + // Make sure to not save the same editor multiple times + // by using the `matches()` method to find duplicates + const uniqueEditors = this.getUniqueEditors(editors); + // Split editors up into a bucket that is saved in parallel // and sequentially. Unless "Save As", all non-untitled editors // can be saved in parallel to speed up the operation. Remaining @@ -961,9 +975,9 @@ export class EditorService extends Disposable implements EditorServiceImpl { const editorsToSaveParallel: IEditorIdentifier[] = []; const editorsToSaveSequentially: IEditorIdentifier[] = []; if (options?.saveAs) { - editorsToSaveSequentially.push(...editors); + editorsToSaveSequentially.push(...uniqueEditors); } else { - for (const { groupId, editor } of editors) { + for (const { groupId, editor } of uniqueEditors) { if (editor.isUntitled()) { editorsToSaveSequentially.push({ groupId, editor }); } else { @@ -973,7 +987,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { } // Editors to save in parallel - await Promise.all(editorsToSaveParallel.map(({ groupId, editor }) => { + const saveResults = await Promise.all(editorsToSaveParallel.map(({ groupId, editor }) => { // Use save as a hint to pin the editor if used explicitly if (options?.reason === SaveReason.EXPLICIT) { @@ -1000,8 +1014,10 @@ export class EditorService extends Disposable implements EditorServiceImpl { } const result = options?.saveAs ? await editor.saveAs(groupId, options) : await editor.save(groupId, options); + saveResults.push(result); + if (!result) { - return false; // failed or cancelled, abort + break; // failed or cancelled, abort } // Replace editor preserving viewstate (either across all groups or @@ -1015,35 +1031,34 @@ export class EditorService extends Disposable implements EditorServiceImpl { } } - return true; + return saveResults.every(result => !!result); } saveAll(options?: ISaveAllEditorsOptions): Promise { return this.save(this.getAllDirtyEditors(options), options); } - async revert(editors: IEditorIdentifier | IEditorIdentifier[], options?: IRevertOptions): Promise { + async revert(editors: IEditorIdentifier | IEditorIdentifier[], options?: IRevertOptions): Promise { // Convert to array if (!Array.isArray(editors)) { editors = [editors]; } - const result = await Promise.all(editors.map(async ({ groupId, editor }) => { - if (editor.isDisposed()) { - return true; // might have been disposed from from the revert already - } + // Make sure to not revert the same editor multiple times + // by using the `matches()` method to find duplicates + const uniqueEditors = this.getUniqueEditors(editors); + + await Promise.all(uniqueEditors.map(async ({ groupId, editor }) => { // Use revert as a hint to pin the editor this.editorGroupService.getGroup(groupId)?.pinEditor(editor); return editor.revert(groupId, options); })); - - return result.every(success => !!success); } - async revertAll(options?: IRevertAllEditorsOptions): Promise { + async revertAll(options?: IRevertAllEditorsOptions): Promise { return this.revert(this.getAllDirtyEditors(options), options); } @@ -1061,6 +1076,19 @@ export class EditorService extends Disposable implements EditorServiceImpl { return editors; } + private getUniqueEditors(editors: IEditorIdentifier[]): IEditorIdentifier[] { + const uniqueEditors: IEditorIdentifier[] = []; + for (const { editor, groupId } of editors) { + if (uniqueEditors.some(uniqueEditor => uniqueEditor.editor.matches(editor))) { + continue; + } + + uniqueEditors.push({ editor, groupId }); + } + + return uniqueEditors; + } + //#endregion dispose(): void { @@ -1151,7 +1179,9 @@ export class DelegatingEditorService implements IEditorService { return this.editorService.replaceEditors(editors as IResourceEditorReplacement[] /* TS fail */, group); } - isOpen(editor: IEditorInput): boolean { return this.editorService.isOpen(editor); } + isOpen(editor: IEditorInput): boolean; + isOpen(editor: IResourceInput): boolean; + isOpen(editor: IEditorInput | IResourceInput): boolean { return this.editorService.isOpen(editor as IResourceInput /* TS fail */); } overrideOpenEditor(handler: IOpenEditorOverrideHandler): IDisposable { return this.editorService.overrideOpenEditor(handler); } @@ -1162,8 +1192,8 @@ export class DelegatingEditorService implements IEditorService { 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); } + revert(editors: IEditorIdentifier | IEditorIdentifier[], options?: IRevertOptions): Promise { return this.editorService.revert(editors, options); } + revertAll(options?: IRevertAllEditorsOptions): Promise { return this.editorService.revertAll(options); } //#endregion } diff --git a/src/vs/workbench/services/editor/common/editorGroupsService.ts b/src/vs/workbench/services/editor/common/editorGroupsService.ts index 20e6bfbb1ae..5202a0d00fe 100644 --- a/src/vs/workbench/services/editor/common/editorGroupsService.ts +++ b/src/vs/workbench/services/editor/common/editorGroupsService.ts @@ -5,10 +5,9 @@ import { Event } from 'vs/base/common/event'; import { createDecorator, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { IEditorInput, IEditor, GroupIdentifier, IEditorInputWithOptions, CloseDirection, IEditorPartOptions, IEditorPartOptionsChangeEvent, EditorsOrder } from 'vs/workbench/common/editor'; +import { IEditorInput, IEditor, GroupIdentifier, IEditorInputWithOptions, CloseDirection, IEditorPartOptions, IEditorPartOptionsChangeEvent, EditorsOrder, IVisibleEditor } from 'vs/workbench/common/editor'; import { IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IVisibleEditor } from 'vs/workbench/services/editor/common/editorService'; import { IDimension } from 'vs/editor/common/editorCommon'; import { IDisposable } from 'vs/base/common/lifecycle'; diff --git a/src/vs/workbench/services/editor/common/editorService.ts b/src/vs/workbench/services/editor/common/editorService.ts index 5f6bb85eff3..20f8d2ee047 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, EditorsOrder } from 'vs/workbench/common/editor'; +import { IEditorInput, IEditor, GroupIdentifier, IEditorInputWithOptions, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, ITextEditor, ITextDiffEditor, ITextSideBySideEditor, IEditorIdentifier, ISaveOptions, IRevertOptions, EditorsOrder, IVisibleEditor } 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'; @@ -16,8 +16,8 @@ export const IEditorService = createDecorator('editorService'); export type IResourceEditor = IResourceInput | IUntitledTextResourceInput | IResourceDiffInput | IResourceSideBySideInput; export interface IResourceEditorReplacement { - editor: IResourceEditor; - replacement: IResourceEditor; + readonly editor: IResourceEditor; + readonly replacement: IResourceEditor; } export const ACTIVE_GROUP = -1; @@ -39,17 +39,12 @@ export interface IOpenEditorOverride { override?: Promise; } -export interface IVisibleEditor extends IEditor { - input: IEditorInput; - group: IEditorGroup; -} - export interface ISaveEditorsOptions extends ISaveOptions { /** * If true, will ask for a location of the editor to save to. */ - saveAs?: boolean; + readonly saveAs?: boolean; } export interface IBaseSaveRevertAllEditorOptions { @@ -57,7 +52,7 @@ export interface IBaseSaveRevertAllEditorOptions { /** * Whether to include untitled editors as well. */ - includeUntitled?: boolean; + readonly includeUntitled?: boolean; } export interface ISaveAllEditorsOptions extends ISaveEditorsOptions, IBaseSaveRevertAllEditorOptions { } @@ -127,7 +122,7 @@ export interface IEditorService { * All text editor widgets that are currently visible across all editor groups. A text editor * widget is either a text or a diff editor. */ - readonly visibleTextEditorWidgets: ReadonlyArray; + readonly visibleTextEditorWidgets: ReadonlyArray; /** * All editors that are opened across all editor groups in sequential order @@ -196,7 +191,13 @@ export interface IEditorService { * Find out if the provided editor is opened in any editor group. * * Note: An editor can be opened but not actively visible. + * + * @param editor the editor to check for being opened. If a + * `IResourceInput` is passed in, the resource is checked on + * all opened editors. In case of a side by side editor, the + * right hand side resource is considered only. */ + isOpen(editor: IResourceInput): boolean; isOpen(editor: IEditorInput): boolean; /** @@ -218,21 +219,25 @@ export interface IEditorService { /** * Save the provided list of editors. + * + * @returns `true` if all editors saved and `false` otherwise. */ save(editors: IEditorIdentifier | IEditorIdentifier[], options?: ISaveEditorsOptions): Promise; /** * Save all editors. + * + * @returns `true` if all editors saved and `false` otherwise. */ saveAll(options?: ISaveAllEditorsOptions): Promise; /** * Reverts the provided list of editors. */ - revert(editors: IEditorIdentifier | IEditorIdentifier[], options?: IRevertOptions): Promise; + revert(editors: IEditorIdentifier | IEditorIdentifier[], options?: IRevertOptions): Promise; /** * Reverts all editors. */ - revertAll(options?: IRevertAllEditorsOptions): Promise; + revertAll(options?: IRevertAllEditorsOptions): Promise; } 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 4066348ec73..558339f6357 100644 --- a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; -import { workbenchInstantiationService, registerTestEditor, TestFileEditorInput } from 'vs/workbench/test/browser/workbenchTestServices'; +import { workbenchInstantiationService, registerTestEditor, TestFileEditorInput, TestEditorPart, ITestInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { GroupDirection, GroupsOrder, MergeGroupMode, GroupOrientation, GroupChangeKind, GroupLocation } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { EditorOptions, CloseDirection, IEditorPartOptions, EditorsOrder } from 'vs/workbench/common/editor'; @@ -29,18 +28,16 @@ suite('EditorGroupsService', () => { disposables = []; }); - function createPart(): EditorPart { - const instantiationService = workbenchInstantiationService(); - - const part = instantiationService.createInstance(EditorPart); + function createPart(instantiationService = workbenchInstantiationService()): [TestEditorPart, ITestInstantiationService] { + const part = instantiationService.createInstance(TestEditorPart); part.create(document.createElement('div')); part.layout(400, 300); - return part; + return [part, instantiationService]; } test('groups basics', async function () { - const part = createPart(); + const [part] = createPart(); let activeGroupChangeCounter = 0; const activeGroupChangeListener = part.onDidActiveGroupChange(() => { @@ -201,8 +198,37 @@ suite('EditorGroupsService', () => { part.dispose(); }); + test('save & restore state', async function () { + let [part, instantiationService] = createPart(); + + const rootGroup = part.groups[0]; + const rightGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); + const downGroup = part.addGroup(rightGroup, GroupDirection.DOWN); + + const rootGroupInput = new TestFileEditorInput(URI.file('foo/bar1'), TEST_EDITOR_INPUT_ID); + await rootGroup.openEditor(rootGroupInput, EditorOptions.create({ pinned: true })); + + const rightGroupInput = new TestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID); + await rightGroup.openEditor(rightGroupInput, EditorOptions.create({ pinned: true })); + + assert.equal(part.groups.length, 3); + + part.saveState(); + part.dispose(); + + let [restoredPart] = createPart(instantiationService); + + assert.equal(restoredPart.groups.length, 3); + assert.ok(restoredPart.getGroup(rootGroup.id)); + assert.ok(restoredPart.getGroup(rightGroup.id)); + assert.ok(restoredPart.getGroup(downGroup.id)); + + restoredPart.clearState(); + restoredPart.dispose(); + }); + test('groups index / labels', function () { - const part = createPart(); + const [part] = createPart(); const rootGroup = part.groups[0]; const rightGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); @@ -260,7 +286,7 @@ suite('EditorGroupsService', () => { }); test('copy/merge groups', async () => { - const part = createPart(); + const [part] = createPart(); let groupAddedCounter = 0; const groupAddedListener = part.onDidAddGroup(() => { @@ -301,7 +327,7 @@ suite('EditorGroupsService', () => { }); test('whenRestored', async () => { - const part = createPart(); + const [part] = createPart(); await part.whenRestored; assert.ok(true); @@ -309,7 +335,7 @@ suite('EditorGroupsService', () => { }); test('options', () => { - const part = createPart(); + const [part] = createPart(); let oldOptions!: IEditorPartOptions; let newOptions!: IEditorPartOptions; @@ -330,7 +356,7 @@ suite('EditorGroupsService', () => { }); test('editor basics', async function () { - const part = createPart(); + const [part] = createPart(); const group = part.activeGroup; assert.equal(group.isEmpty, true); @@ -428,7 +454,7 @@ suite('EditorGroupsService', () => { }); test('openEditors / closeEditors', async () => { - const part = createPart(); + const [part] = createPart(); const group = part.activeGroup; assert.equal(group.isEmpty, true); @@ -446,7 +472,7 @@ suite('EditorGroupsService', () => { }); test('closeEditors (except one)', async () => { - const part = createPart(); + const [part] = createPart(); const group = part.activeGroup; assert.equal(group.isEmpty, true); @@ -467,7 +493,7 @@ suite('EditorGroupsService', () => { }); test('closeEditors (saved only)', async () => { - const part = createPart(); + const [part] = createPart(); const group = part.activeGroup; assert.equal(group.isEmpty, true); @@ -487,7 +513,7 @@ suite('EditorGroupsService', () => { }); test('closeEditors (direction: right)', async () => { - const part = createPart(); + const [part] = createPart(); const group = part.activeGroup; assert.equal(group.isEmpty, true); @@ -509,7 +535,7 @@ suite('EditorGroupsService', () => { }); test('closeEditors (direction: left)', async () => { - const part = createPart(); + const [part] = createPart(); const group = part.activeGroup; assert.equal(group.isEmpty, true); @@ -531,7 +557,7 @@ suite('EditorGroupsService', () => { }); test('closeAllEditors', async () => { - const part = createPart(); + const [part] = createPart(); const group = part.activeGroup; assert.equal(group.isEmpty, true); @@ -549,7 +575,7 @@ suite('EditorGroupsService', () => { }); test('moveEditor (same group)', async () => { - const part = createPart(); + const [part] = createPart(); const group = part.activeGroup; assert.equal(group.isEmpty, true); @@ -577,7 +603,7 @@ suite('EditorGroupsService', () => { }); test('moveEditor (across groups)', async () => { - const part = createPart(); + const [part] = createPart(); const group = part.activeGroup; assert.equal(group.isEmpty, true); @@ -599,7 +625,7 @@ suite('EditorGroupsService', () => { }); test('copyEditor (across groups)', async () => { - const part = createPart(); + const [part] = createPart(); const group = part.activeGroup; assert.equal(group.isEmpty, true); @@ -622,7 +648,7 @@ suite('EditorGroupsService', () => { }); test('replaceEditors', async () => { - const part = createPart(); + const [part] = createPart(); const group = part.activeGroup; assert.equal(group.isEmpty, true); @@ -640,7 +666,7 @@ suite('EditorGroupsService', () => { }); test('find neighbour group (left/right)', function () { - const part = createPart(); + const [part] = createPart(); const rootGroup = part.activeGroup; const rightGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); @@ -651,7 +677,7 @@ suite('EditorGroupsService', () => { }); test('find neighbour group (up/down)', function () { - const part = createPart(); + const [part] = createPart(); const rootGroup = part.activeGroup; const downGroup = part.addGroup(rootGroup, GroupDirection.DOWN); @@ -662,7 +688,7 @@ suite('EditorGroupsService', () => { }); test('find group by location (left/right)', function () { - const part = createPart(); + const [part] = createPart(); const rootGroup = part.activeGroup; const rightGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); const downGroup = part.addGroup(rightGroup, GroupDirection.DOWN); @@ -678,4 +704,24 @@ suite('EditorGroupsService', () => { part.dispose(); }); + + test('applyLayout (2x2)', function () { + const [part] = createPart(); + + part.applyLayout({ groups: [{ groups: [{}, {}] }, { groups: [{}, {}] }], orientation: GroupOrientation.HORIZONTAL }); + + assert.equal(part.groups.length, 4); + + part.dispose(); + }); + + test('centeredLayout', function () { + const [part] = createPart(); + + part.centerLayout(true); + + assert.equal(part.isLayoutCentered(), true); + + part.dispose(); + }); }); 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 a57f108b8fb..c80aa1cd9f3 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -8,7 +8,7 @@ import { EditorActivation } from 'vs/platform/editor/common/editor'; import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { EditorInput, EditorsOrder } from 'vs/workbench/common/editor'; +import { EditorInput, EditorsOrder, SideBySideEditorInput } from 'vs/workbench/common/editor'; import { workbenchInstantiationService, TestStorageService, TestServiceAccessor, registerTestEditor, TestFileEditorInput } from 'vs/workbench/test/browser/workbenchTestServices'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; @@ -26,6 +26,7 @@ import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry'; import { UntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; import { NullFileSystemProvider } from 'vs/platform/files/test/common/nullFileSystemProvider'; +import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; const TEST_EDITOR_ID = 'MyTestEditorForEditorService'; const TEST_EDITOR_INPUT_ID = 'testEditorInputForEditorService'; @@ -104,6 +105,7 @@ suite('EditorService', () => { assert.ok(!service.activeTextEditorMode); assert.equal(service.visibleTextEditorWidgets.length, 0); assert.equal(service.isOpen(input), true); + assert.equal(service.isOpen({ resource: input.resource }), true); assert.equal(activeEditorChangeEventCounter, 1); assert.equal(visibleEditorChangeEventCounter, 1); @@ -136,7 +138,9 @@ suite('EditorService', () => { 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({ resource: input.resource }), true); assert.equal(service.isOpen(otherInput), true); + assert.equal(service.isOpen({ resource: otherInput.resource }), true); assert.equal(activeEditorChangeEventCounter, 4); assert.equal(visibleEditorChangeEventCounter, 4); @@ -148,6 +152,53 @@ suite('EditorService', () => { part.dispose(); }); + test('isOpen() with side by side editor', async () => { + const [part, service] = createEditorService(); + + const input = new TestFileEditorInput(URI.parse('my://resource-openEditors'), TEST_EDITOR_INPUT_ID); + const otherInput = new TestFileEditorInput(URI.parse('my://resource2-openEditors'), TEST_EDITOR_INPUT_ID); + const sideBySideInput = new SideBySideEditorInput('sideBySide', '', input, otherInput); + + await part.whenRestored; + + const editor1 = await service.openEditor(sideBySideInput, { pinned: true }); + assert.equal(part.activeGroup.count, 1); + + assert.equal(service.isOpen(input), false); + assert.equal(service.isOpen(otherInput), false); + assert.equal(service.isOpen(sideBySideInput), true); + assert.equal(service.isOpen({ resource: input.resource }), false); + assert.equal(service.isOpen({ resource: otherInput.resource }), true); + + const editor2 = await service.openEditor(input, { pinned: true }); + assert.equal(part.activeGroup.count, 2); + + assert.equal(service.isOpen(input), true); + assert.equal(service.isOpen(otherInput), false); + assert.equal(service.isOpen(sideBySideInput), true); + assert.equal(service.isOpen({ resource: input.resource }), true); + assert.equal(service.isOpen({ resource: otherInput.resource }), true); + + await editor2?.group?.closeEditor(input); + assert.equal(part.activeGroup.count, 1); + + assert.equal(service.isOpen(input), false); + assert.equal(service.isOpen(otherInput), false); + assert.equal(service.isOpen(sideBySideInput), true); + assert.equal(service.isOpen({ resource: input.resource }), false); + assert.equal(service.isOpen({ resource: otherInput.resource }), true); + + await editor1?.group?.closeEditor(sideBySideInput); + + assert.equal(service.isOpen(input), false); + assert.equal(service.isOpen(otherInput), false); + assert.equal(service.isOpen(sideBySideInput), false); + assert.equal(service.isOpen({ resource: input.resource }), false); + assert.equal(service.isOpen({ resource: otherInput.resource }), false); + + part.dispose(); + }); + test('openEditors() / replaceEditors()', async () => { const [part, service] = createEditorService(); @@ -233,6 +284,10 @@ suite('EditorService', () => { let contentInput = input; assert.strictEqual(contentInput.resource.fsPath, toResource.call(this, '/index.html').fsPath); + // Typed Input + assert.equal(service.createInput(input), input); + assert.equal(service.createInput({ editor: input }), input); + // Untyped Input (file, encoding) input = service.createInput({ resource: toResource.call(this, '/index.html'), encoding: 'utf16le', options: { selection: { startLineNumber: 1, startColumn: 1 } } }); assert(input instanceof FileEditorInput); @@ -289,6 +344,20 @@ suite('EditorService', () => { // Untyped Input (resource) input = service.createInput({ resource: URI.parse('custom:resource') }); assert(input instanceof ResourceEditorInput); + + // Untyped Input (side by side) + input = service.createInput({ + masterResource: toResource.call(this, '/master.html'), + detailResource: toResource.call(this, '/detail.html') + }); + assert(input instanceof SideBySideEditorInput); + + // Untyped Input (diff) + input = service.createInput({ + leftResource: toResource.call(this, '/master.html'), + rightResource: toResource.call(this, '/detail.html') + }); + assert(input instanceof DiffEditorInput); }); test('delegate', function (done) { @@ -306,7 +375,7 @@ suite('EditorService', () => { layout(): void { } - createEditor(): any { } + createEditor(): void { } } const ed = instantiationService.createInstance(MyEditor, 'my.editor'); @@ -710,10 +779,12 @@ suite('EditorService', () => { test('save, saveAll, revertAll', async function () { const [part, service] = createEditorService(); - const input1 = new TestFileEditorInput(URI.parse('my://resource1-openside'), TEST_EDITOR_INPUT_ID); + const input1 = new TestFileEditorInput(URI.parse('my://resource1'), TEST_EDITOR_INPUT_ID); input1.dirty = true; - const input2 = new TestFileEditorInput(URI.parse('my://resource2-openside'), TEST_EDITOR_INPUT_ID); + const input2 = new TestFileEditorInput(URI.parse('my://resource2'), TEST_EDITOR_INPUT_ID); input2.dirty = true; + const sameInput1 = new TestFileEditorInput(URI.parse('my://resource1'), TEST_EDITOR_INPUT_ID); + sameInput1.dirty = true; const rootGroup = part.activeGroup; @@ -721,24 +792,50 @@ suite('EditorService', () => { await service.openEditor(input1, { pinned: true }); await service.openEditor(input2, { pinned: true }); + await service.openEditor(sameInput1, { pinned: true }, SIDE_GROUP); await service.save({ groupId: rootGroup.id, editor: input1 }); assert.equal(input1.gotSaved, true); + input1.gotSaved = false; + input1.gotSavedAs = false; + input1.gotReverted = false; + await service.save({ groupId: rootGroup.id, editor: input1 }, { saveAs: true }); assert.equal(input1.gotSavedAs, true); + input1.gotSaved = false; + input1.gotSavedAs = false; + input1.gotReverted = false; + await service.revertAll(); assert.equal(input1.gotReverted, true); + input1.gotSaved = false; + input1.gotSavedAs = false; + input1.gotReverted = false; + await service.saveAll(); assert.equal(input1.gotSaved, true); assert.equal(input2.gotSaved, true); + input1.gotSaved = false; + input1.gotSavedAs = false; + input1.gotReverted = false; + input2.gotSaved = false; + input2.gotSavedAs = false; + input2.gotReverted = false; + await service.saveAll({ saveAs: true }); + assert.equal(input1.gotSavedAs, true); assert.equal(input2.gotSavedAs, true); + // services dedupes inputs automatically + assert.equal(sameInput1.gotSaved, false); + assert.equal(sameInput1.gotSavedAs, false); + assert.equal(sameInput1.gotReverted, false); + part.dispose(); }); @@ -753,9 +850,9 @@ suite('EditorService', () => { async function testFileDeleteEditorClose(dirty: boolean): Promise { const [part, service, accessor] = createEditorService(); - const input1 = new TestFileEditorInput(URI.parse('my://resource1-openside'), TEST_EDITOR_INPUT_ID); + const input1 = new TestFileEditorInput(URI.parse('my://resource1'), TEST_EDITOR_INPUT_ID); input1.dirty = dirty; - const input2 = new TestFileEditorInput(URI.parse('my://resource2-openside'), TEST_EDITOR_INPUT_ID); + const input2 = new TestFileEditorInput(URI.parse('my://resource2'), TEST_EDITOR_INPUT_ID); input2.dirty = dirty; const rootGroup = part.activeGroup; @@ -785,8 +882,8 @@ suite('EditorService', () => { test('file move asks input to move', async function () { const [part, service, accessor] = createEditorService(); - const input1 = new TestFileEditorInput(URI.parse('my://resource1-openside'), TEST_EDITOR_INPUT_ID); - const movedInput = new TestFileEditorInput(URI.parse('my://resource2-openside'), TEST_EDITOR_INPUT_ID); + const input1 = new TestFileEditorInput(URI.parse('my://resource1'), TEST_EDITOR_INPUT_ID); + const movedInput = new TestFileEditorInput(URI.parse('my://resource2'), TEST_EDITOR_INPUT_ID); input1.movedEditor = { editor: movedInput }; const rootGroup = part.activeGroup; @@ -803,7 +900,7 @@ suite('EditorService', () => { isDirectory: false, isFile: true, mtime: 0, - name: 'resource2-openside', + name: 'resource2', size: 0, isSymbolicLink: false })); @@ -823,8 +920,8 @@ suite('EditorService', () => { test('file watcher gets installed for out of workspace files', async function () { const [part, service, accessor] = createEditorService(); - const input1 = new TestFileEditorInput(URI.parse('file://resource1-openside'), TEST_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.parse('file://resource2-openside'), TEST_EDITOR_INPUT_ID); + const input1 = new TestFileEditorInput(URI.parse('file://resource1'), TEST_EDITOR_INPUT_ID); + const input2 = new TestFileEditorInput(URI.parse('file://resource2'), TEST_EDITOR_INPUT_ID); await part.whenRestored; @@ -841,4 +938,53 @@ suite('EditorService', () => { part.dispose(); }); + + test('invokeWithinEditorContext', async function () { + const [part, service] = createEditorService(); + + const input1 = new TestFileEditorInput(URI.parse('file://resource1'), TEST_EDITOR_INPUT_ID); + new TestFileEditorInput(URI.parse('file://resource2'), TEST_EDITOR_INPUT_ID); + + await part.whenRestored; + + await service.openEditor(input1, { pinned: true }); + + let hasAccessor = false; + service.invokeWithinEditorContext(accessor => { + hasAccessor = true; + }); + + assert.ok(hasAccessor); + + part.dispose(); + }); + + test('overrideOpenEditor', async function () { + const [part, service] = createEditorService(); + + const input1 = new TestFileEditorInput(URI.parse('file://resource1'), TEST_EDITOR_INPUT_ID); + const input2 = new TestFileEditorInput(URI.parse('file://resource2'), TEST_EDITOR_INPUT_ID); + + await part.whenRestored; + + let overrideCalled = false; + + const handler = service.overrideOpenEditor(editor => { + if (editor === input1) { + overrideCalled = true; + + return { override: service.openEditor(input2, { pinned: true }) }; + } + + return undefined; + }); + + await service.openEditor(input1, { pinned: true }); + + assert.ok(overrideCalled); + assert.equal(service.activeEditor, input2); + + handler.dispose(); + part.dispose(); + }); }); diff --git a/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts b/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts index 2f25fec289d..c0e0d8a1921 100644 --- a/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { EditorOptions, IEditorInputFactoryRegistry, Extensions as EditorExtensions } from 'vs/workbench/common/editor'; import { URI } from 'vs/base/common/uri'; -import { workbenchInstantiationService, TestStorageService, TestFileEditorInput, registerTestEditor } from 'vs/workbench/test/browser/workbenchTestServices'; +import { workbenchInstantiationService, TestStorageService, TestFileEditorInput, registerTestEditor, TestEditorPart } from 'vs/workbench/test/browser/workbenchTestServices'; import { Registry } from 'vs/platform/registry/common/platform'; import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; @@ -34,11 +34,11 @@ suite('EditorsObserver', function () { disposables = []; }); - async function createPart(): Promise { + async function createPart(): Promise { const instantiationService = workbenchInstantiationService(); instantiationService.invokeFunction(accessor => Registry.as(EditorExtensions.EditorInputFactories).start(accessor)); - const part = instantiationService.createInstance(EditorPart); + const part = instantiationService.createInstance(TestEditorPart); part.create(document.createElement('div')); part.layout(400, 300); @@ -58,14 +58,14 @@ suite('EditorsObserver', function () { test('basics (single group)', async () => { const [part, observer] = await createEditorObserver(); - let observerChangeListenerCalled = false; - const listener = observer.onDidChange(() => { - observerChangeListenerCalled = true; + let onDidMostRecentlyActiveEditorsChangeCalled = false; + const listener = observer.onDidMostRecentlyActiveEditorsChange(() => { + onDidMostRecentlyActiveEditorsChangeCalled = true; }); let currentEditorsMRU = observer.editors; assert.equal(currentEditorsMRU.length, 0); - assert.equal(observerChangeListenerCalled, false); + assert.equal(onDidMostRecentlyActiveEditorsChangeCalled, false); const input1 = new TestFileEditorInput(URI.parse('foo://bar1'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); @@ -75,7 +75,8 @@ suite('EditorsObserver', function () { assert.equal(currentEditorsMRU.length, 1); assert.equal(currentEditorsMRU[0].groupId, part.activeGroup.id); assert.equal(currentEditorsMRU[0].editor, input1); - assert.equal(observerChangeListenerCalled, true); + assert.equal(onDidMostRecentlyActiveEditorsChangeCalled, true); + assert.equal(observer.hasEditor(input1.resource), true); const input2 = new TestFileEditorInput(URI.parse('foo://bar2'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); const input3 = new TestFileEditorInput(URI.parse('foo://bar3'), TEST_SERIALIZABLE_EDITOR_INPUT_ID); @@ -91,6 +92,8 @@ suite('EditorsObserver', function () { assert.equal(currentEditorsMRU[1].editor, input2); assert.equal(currentEditorsMRU[2].groupId, part.activeGroup.id); assert.equal(currentEditorsMRU[2].editor, input1); + assert.equal(observer.hasEditor(input2.resource), true); + assert.equal(observer.hasEditor(input3.resource), true); await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true })); @@ -102,8 +105,11 @@ suite('EditorsObserver', function () { assert.equal(currentEditorsMRU[1].editor, input3); assert.equal(currentEditorsMRU[2].groupId, part.activeGroup.id); assert.equal(currentEditorsMRU[2].editor, input1); + assert.equal(observer.hasEditor(input1.resource), true); + assert.equal(observer.hasEditor(input2.resource), true); + assert.equal(observer.hasEditor(input3.resource), true); - observerChangeListenerCalled = false; + onDidMostRecentlyActiveEditorsChangeCalled = false; await part.activeGroup.closeEditor(input1); currentEditorsMRU = observer.editors; @@ -112,11 +118,17 @@ suite('EditorsObserver', function () { assert.equal(currentEditorsMRU[0].editor, input2); assert.equal(currentEditorsMRU[1].groupId, part.activeGroup.id); assert.equal(currentEditorsMRU[1].editor, input3); - assert.equal(observerChangeListenerCalled, true); + assert.equal(onDidMostRecentlyActiveEditorsChangeCalled, true); + assert.equal(observer.hasEditor(input1.resource), false); + assert.equal(observer.hasEditor(input2.resource), true); + assert.equal(observer.hasEditor(input3.resource), true); await part.activeGroup.closeAllEditors(); currentEditorsMRU = observer.editors; assert.equal(currentEditorsMRU.length, 0); + assert.equal(observer.hasEditor(input1.resource), false); + assert.equal(observer.hasEditor(input2.resource), false); + assert.equal(observer.hasEditor(input3.resource), false); part.dispose(); listener.dispose(); @@ -143,6 +155,7 @@ suite('EditorsObserver', function () { assert.equal(currentEditorsMRU[0].editor, input1); assert.equal(currentEditorsMRU[1].groupId, rootGroup.id); assert.equal(currentEditorsMRU[1].editor, input1); + assert.equal(observer.hasEditor(input1.resource), true); await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true, activation: EditorActivation.ACTIVATE })); @@ -152,6 +165,7 @@ suite('EditorsObserver', function () { assert.equal(currentEditorsMRU[0].editor, input1); assert.equal(currentEditorsMRU[1].groupId, sideGroup.id); assert.equal(currentEditorsMRU[1].editor, input1); + assert.equal(observer.hasEditor(input1.resource), true); // Opening an editor inactive should not change // the most recent editor, but rather put it behind @@ -167,6 +181,8 @@ suite('EditorsObserver', function () { assert.equal(currentEditorsMRU[1].editor, input2); assert.equal(currentEditorsMRU[2].groupId, sideGroup.id); assert.equal(currentEditorsMRU[2].editor, input1); + assert.equal(observer.hasEditor(input1.resource), true); + assert.equal(observer.hasEditor(input2.resource), true); await rootGroup.closeAllEditors(); @@ -174,11 +190,15 @@ suite('EditorsObserver', function () { assert.equal(currentEditorsMRU.length, 1); assert.equal(currentEditorsMRU[0].groupId, sideGroup.id); assert.equal(currentEditorsMRU[0].editor, input1); + assert.equal(observer.hasEditor(input1.resource), true); + assert.equal(observer.hasEditor(input2.resource), false); await sideGroup.closeAllEditors(); currentEditorsMRU = observer.editors; assert.equal(currentEditorsMRU.length, 0); + assert.equal(observer.hasEditor(input1.resource), false); + assert.equal(observer.hasEditor(input2.resource), false); part.dispose(); }); @@ -204,6 +224,9 @@ suite('EditorsObserver', function () { assert.equal(currentEditorsMRU[1].editor, input2); assert.equal(currentEditorsMRU[2].groupId, rootGroup.id); assert.equal(currentEditorsMRU[2].editor, input1); + assert.equal(observer.hasEditor(input1.resource), true); + assert.equal(observer.hasEditor(input2.resource), true); + assert.equal(observer.hasEditor(input3.resource), true); const copiedGroup = part.copyGroup(rootGroup, rootGroup, GroupDirection.RIGHT); copiedGroup.setActive(true); @@ -222,6 +245,21 @@ suite('EditorsObserver', function () { assert.equal(currentEditorsMRU[4].editor, input2); assert.equal(currentEditorsMRU[5].groupId, rootGroup.id); assert.equal(currentEditorsMRU[5].editor, input1); + assert.equal(observer.hasEditor(input1.resource), true); + assert.equal(observer.hasEditor(input2.resource), true); + assert.equal(observer.hasEditor(input3.resource), true); + + await rootGroup.closeAllEditors(); + + assert.equal(observer.hasEditor(input1.resource), true); + assert.equal(observer.hasEditor(input2.resource), true); + assert.equal(observer.hasEditor(input3.resource), true); + + await copiedGroup.closeAllEditors(); + + assert.equal(observer.hasEditor(input1.resource), false); + assert.equal(observer.hasEditor(input2.resource), false); + assert.equal(observer.hasEditor(input3.resource), false); part.dispose(); }); @@ -251,6 +289,9 @@ suite('EditorsObserver', function () { assert.equal(currentEditorsMRU[1].editor, input2); assert.equal(currentEditorsMRU[2].groupId, rootGroup.id); assert.equal(currentEditorsMRU[2].editor, input1); + assert.equal(observer.hasEditor(input1.resource), true); + assert.equal(observer.hasEditor(input2.resource), true); + assert.equal(observer.hasEditor(input3.resource), true); storage._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN }); @@ -265,7 +306,11 @@ suite('EditorsObserver', function () { assert.equal(currentEditorsMRU[1].editor, input2); assert.equal(currentEditorsMRU[2].groupId, rootGroup.id); assert.equal(currentEditorsMRU[2].editor, input1); + assert.equal(observer.hasEditor(input1.resource), true); + assert.equal(observer.hasEditor(input2.resource), true); + assert.equal(observer.hasEditor(input3.resource), true); + part.clearState(); part.dispose(); }); @@ -296,6 +341,9 @@ suite('EditorsObserver', function () { assert.equal(currentEditorsMRU[1].editor, input2); assert.equal(currentEditorsMRU[2].groupId, rootGroup.id); assert.equal(currentEditorsMRU[2].editor, input1); + assert.equal(observer.hasEditor(input1.resource), true); + assert.equal(observer.hasEditor(input2.resource), true); + assert.equal(observer.hasEditor(input3.resource), true); storage._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN }); @@ -310,7 +358,11 @@ suite('EditorsObserver', function () { assert.equal(currentEditorsMRU[1].editor, input2); assert.equal(currentEditorsMRU[2].groupId, rootGroup.id); assert.equal(currentEditorsMRU[2].editor, input1); + assert.equal(restoredObserver.hasEditor(input1.resource), true); + assert.equal(restoredObserver.hasEditor(input2.resource), true); + assert.equal(restoredObserver.hasEditor(input3.resource), true); + part.clearState(); part.dispose(); }); @@ -331,6 +383,7 @@ suite('EditorsObserver', function () { assert.equal(currentEditorsMRU.length, 1); assert.equal(currentEditorsMRU[0].groupId, rootGroup.id); assert.equal(currentEditorsMRU[0].editor, input1); + assert.equal(observer.hasEditor(input1.resource), true); storage._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN }); @@ -339,7 +392,9 @@ suite('EditorsObserver', function () { currentEditorsMRU = restoredObserver.editors; assert.equal(currentEditorsMRU.length, 0); + assert.equal(restoredObserver.hasEditor(input1.resource), false); + part.clearState(); part.dispose(); }); @@ -368,6 +423,10 @@ suite('EditorsObserver', function () { assert.equal(rootGroup.isOpened(input2), true); assert.equal(rootGroup.isOpened(input3), true); assert.equal(rootGroup.isOpened(input4), true); + assert.equal(observer.hasEditor(input1.resource), false); + assert.equal(observer.hasEditor(input2.resource), true); + assert.equal(observer.hasEditor(input3.resource), true); + assert.equal(observer.hasEditor(input4.resource), true); input2.setDirty(); part.enforcePartOptions({ limit: { enabled: true, value: 1 } }); @@ -379,6 +438,10 @@ suite('EditorsObserver', function () { assert.equal(rootGroup.isOpened(input2), true); // dirty assert.equal(rootGroup.isOpened(input3), false); assert.equal(rootGroup.isOpened(input4), true); + assert.equal(observer.hasEditor(input1.resource), false); + assert.equal(observer.hasEditor(input2.resource), true); + assert.equal(observer.hasEditor(input3.resource), false); + assert.equal(observer.hasEditor(input4.resource), true); const input5 = new TestFileEditorInput(URI.parse('foo://bar5'), TEST_EDITOR_INPUT_ID); await sideGroup.openEditor(input5, EditorOptions.create({ pinned: true })); @@ -388,8 +451,12 @@ suite('EditorsObserver', function () { 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); + assert.equal(observer.hasEditor(input1.resource), false); + assert.equal(observer.hasEditor(input2.resource), true); + assert.equal(observer.hasEditor(input3.resource), false); + assert.equal(observer.hasEditor(input4.resource), false); + assert.equal(observer.hasEditor(input5.resource), true); observer.dispose(); part.dispose(); @@ -415,11 +482,15 @@ suite('EditorsObserver', function () { await rootGroup.openEditor(input3, EditorOptions.create({ pinned: true })); await rootGroup.openEditor(input4, EditorOptions.create({ pinned: true })); - assert.equal(rootGroup.count, 3); + assert.equal(rootGroup.count, 3); // 1 editor got closed due to our limit! assert.equal(rootGroup.isOpened(input1), false); assert.equal(rootGroup.isOpened(input2), true); assert.equal(rootGroup.isOpened(input3), true); assert.equal(rootGroup.isOpened(input4), true); + assert.equal(observer.hasEditor(input1.resource), false); + assert.equal(observer.hasEditor(input2.resource), true); + assert.equal(observer.hasEditor(input3.resource), true); + assert.equal(observer.hasEditor(input4.resource), true); await sideGroup.openEditor(input1, EditorOptions.create({ pinned: true })); await sideGroup.openEditor(input2, EditorOptions.create({ pinned: true })); @@ -431,6 +502,10 @@ suite('EditorsObserver', function () { assert.equal(sideGroup.isOpened(input2), true); assert.equal(sideGroup.isOpened(input3), true); assert.equal(sideGroup.isOpened(input4), true); + assert.equal(observer.hasEditor(input1.resource), false); + assert.equal(observer.hasEditor(input2.resource), true); + assert.equal(observer.hasEditor(input3.resource), true); + assert.equal(observer.hasEditor(input4.resource), true); part.enforcePartOptions({ limit: { enabled: true, value: 1, perEditorGroup: true } }); @@ -448,6 +523,11 @@ suite('EditorsObserver', function () { assert.equal(sideGroup.isOpened(input3), false); assert.equal(sideGroup.isOpened(input4), true); + assert.equal(observer.hasEditor(input1.resource), false); + assert.equal(observer.hasEditor(input2.resource), false); + assert.equal(observer.hasEditor(input3.resource), false); + assert.equal(observer.hasEditor(input4.resource), true); + observer.dispose(); part.dispose(); }); diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index b7ee69dc594..fa59b3513ef 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -4,22 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import { Schemas } from 'vs/base/common/network'; -import { ExportData } from 'vs/base/common/performance'; -import { IProcessEnvironment } from 'vs/base/common/platform'; import { joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { BACKUPS, IExtensionHostDebugParams } from 'vs/platform/environment/common/environment'; -import { LogLevel } from 'vs/platform/log/common/log'; -import { IPath, IPathsToWaitFor, IWindowConfiguration } from 'vs/platform/windows/common/windows'; -import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { IPath, IWindowConfiguration } from 'vs/platform/windows/common/windows'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IWorkbenchConstructionOptions } from 'vs/workbench/workbench.web.api'; import product from 'vs/platform/product/common/product'; import { serializableToMap } from 'vs/base/common/map'; import { memoize } from 'vs/base/common/decorators'; -// TODO@ben remove properties that are node/electron only export class BrowserWindowConfiguration implements IWindowConfiguration { constructor( @@ -28,8 +23,6 @@ export class BrowserWindowConfiguration implements IWindowConfiguration { private readonly environment: IWorkbenchEnvironmentService ) { } - //#region PROPERLY CONFIGURED IN DESKTOP + WEB - @memoize get sessionId(): string { return generateUuid(); } @@ -54,44 +47,10 @@ export class BrowserWindowConfiguration implements IWindowConfiguration { return undefined; } - // Currently unsupported in web + // Currently unsupported in web but should look into support get filesToDiff(): IPath[] | undefined { return undefined; } - - //#endregion - - - //#region TODO MOVE TO NODE LAYER - - _!: any[]; - - windowId!: number; - mainPid!: number; - - logLevel!: LogLevel; - - appRoot!: string; - execPath!: string; - backupPath?: string; - nodeCachedDataDir?: string; - - userEnv!: IProcessEnvironment; - - workspace?: IWorkspaceIdentifier; - folderUri?: ISingleFolderWorkspaceIdentifier; - - zoomLevel?: number; - fullscreen?: boolean; - maximized?: boolean; - highContrast?: boolean; - accessibilitySupport?: boolean; - partsSplashPath?: string; - - isInitialStartup?: boolean; - perfEntries!: ExportData; - - filesToWait?: IPathsToWaitFor; - - //#endregion + highContrast = false; + _ = []; private getCookieValue(name: string): string | undefined { const m = document.cookie.match('(^|[^;]+)\\s*' + name + '\\s*=\\s*([^;]+)'); // See https://stackoverflow.com/a/25490531 @@ -116,7 +75,14 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment _serviceBrand: undefined; - //#region PROPERLY CONFIGURED IN DESKTOP + WEB + private _configuration: IWindowConfiguration | undefined = undefined; + get configuration(): IWindowConfiguration { + if (!this._configuration) { + this._configuration = new BrowserWindowConfiguration(this.options, this.payload, this); + } + + return this._configuration; + } @memoize get isBuilt(): boolean { return !!product.commit; } @@ -137,7 +103,7 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment get argvResource(): URI { return joinPath(this.userRoamingDataHome, 'argv.json'); } @memoize - get userDataSyncHome(): URI { return joinPath(this.userRoamingDataHome, '.sync'); } + get userDataSyncHome(): URI { return joinPath(this.userRoamingDataHome, 'sync'); } @memoize get settingsSyncPreviewResource(): URI { return joinPath(this.userDataSyncHome, 'settings.json'); } @@ -209,71 +175,14 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment return this.webviewExternalEndpoint.replace('{{uuid}}', '*'); } - // Currently not configurable in web + // Currently unsupported in web but should look into support get disableExtensions() { return false; } get extensionsPath(): string | undefined { return undefined; } get verbose(): boolean { return false; } get disableUpdates(): boolean { return false; } get logExtensionHostCommunication(): boolean { return false; } - - //#endregion - - - //#region TODO MOVE TO NODE LAYER - - private _configuration: IWindowConfiguration | undefined = undefined; - get configuration(): IWindowConfiguration { - if (!this._configuration) { - this._configuration = new BrowserWindowConfiguration(this.options, this.payload, this); - } - - return this._configuration; - } - - args = { _: [] }; - - wait!: boolean; - status!: boolean; - log?: string; - - mainIPCHandle!: string; - sharedIPCHandle!: string; - - nodeCachedDataDir?: string; - - disableCrashReporter!: boolean; - - driverHandle?: string; - driverVerbose!: boolean; - - installSourcePath!: string; - - builtinExtensionsPath!: string; - - globalStorageHome!: string; - workspaceStorageHome!: string; - - backupWorkspacesPath!: string; - - machineSettingsHome!: URI; - machineSettingsResource!: URI; - - userHome!: string; - userDataPath!: string; - appRoot!: string; - appSettingsHome!: URI; - execPath!: string; - cliPath!: string; - - //#endregion - - - //#region TODO ENABLE IN WEB - galleryMachineIdResource?: URI; - //#endregion - private payload: Map | undefined; constructor(readonly options: IBrowserWorkbenchEnvironmentConstructionOptions) { @@ -316,4 +225,35 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment return extensionHostDebugEnvironment; } + + //#region TODO MOVE TO NODE LAYER + + args = { _: [] }; + + mainIPCHandle!: string; + sharedIPCHandle!: string; + + nodeCachedDataDir?: string; + + driverHandle?: string; + driverVerbose!: boolean; + + installSourcePath!: string; + + builtinExtensionsPath!: string; + + globalStorageHome!: string; + workspaceStorageHome!: string; + + backupWorkspacesPath!: string; + + machineSettingsResource!: URI; + + userHome!: string; + userDataPath!: string; + appRoot!: string; + appSettingsHome!: URI; + execPath!: string; + + //#endregion } diff --git a/src/vs/workbench/services/environment/electron-browser/environmentService.ts b/src/vs/workbench/services/environment/electron-browser/environmentService.ts index 8d32aff4ee6..8adb181b7e9 100644 --- a/src/vs/workbench/services/environment/electron-browser/environmentService.ts +++ b/src/vs/workbench/services/environment/electron-browser/environmentService.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { EnvironmentService } from 'vs/platform/environment/node/environmentService'; -import { IWindowConfiguration } from 'vs/platform/windows/common/windows'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { memoize } from 'vs/base/common/decorators'; import { URI } from 'vs/base/common/uri'; @@ -12,8 +11,18 @@ import { Schemas } from 'vs/base/common/network'; import { toBackupWorkspaceResource } from 'vs/workbench/services/backup/electron-browser/backup'; import { join } from 'vs/base/common/path'; import product from 'vs/platform/product/common/product'; +import { INativeWindowConfiguration } from 'vs/platform/windows/node/window'; -export class NativeWorkbenchEnvironmentService extends EnvironmentService implements IWorkbenchEnvironmentService { +export interface INativeWorkbenchEnvironmentService extends IWorkbenchEnvironmentService { + + readonly configuration: INativeWindowConfiguration; + + log?: string; + cliPath: string; + disableCrashReporter: boolean; +} + +export class NativeWorkbenchEnvironmentService extends EnvironmentService implements INativeWorkbenchEnvironmentService { _serviceBrand: undefined; @@ -37,7 +46,7 @@ export class NativeWorkbenchEnvironmentService extends EnvironmentService implem get logFile(): URI { return URI.file(join(this.logsPath, `renderer${this.windowId}.log`)); } constructor( - readonly configuration: IWindowConfiguration, + readonly configuration: INativeWindowConfiguration, execPath: string, private readonly windowId: number ) { diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index 4a07843244d..9f8c6ac6f5e 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -209,7 +209,7 @@ export class ExtensionManagementService extends Disposable implements IExtension return Promise.reject(localize('Manifest is not found', "Installing Extension {0} failed: Manifest is not found.", gallery.displayName || gallery.name)); } if (!isLanguagePackExtension(manifest) && !canExecuteOnWorkspace(manifest, this.productService, this.configurationService)) { - const error = new Error(localize('cannot be installed', "Cannot install '{0}' extension since it cannot be enabled in the remote server.", gallery.displayName || gallery.name)); + const error = new Error(localize('cannot be installed', "Cannot install '{0}' because this extension has defined that it cannot run on the remote server.", gallery.displayName || gallery.name)); error.name = INSTALL_ERROR_NOT_SUPPORTED; return Promise.reject(error); } diff --git a/src/vs/workbench/services/extensions/common/extensionHostMain.ts b/src/vs/workbench/services/extensions/common/extensionHostMain.ts index caebe7634f3..93d9a1d107d 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostMain.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostMain.ts @@ -76,7 +76,7 @@ export class ExtensionHostMain { // error forwarding and stack trace scanning Error.stackTraceLimit = 100; // increase number of stack frames (from 10, https://github.com/v8/v8/wiki/Stack-Trace-API) - const extensionErrors = new WeakMap(); + const extensionErrors = new WeakMap(); this._extensionService.getExtensionPathIndex().then(map => { (Error).prepareStackTrace = (error: Error, stackTrace: errors.V8CallSite[]) => { let stackTraceMessage = ''; diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts index c45f7e5c3e3..b823db0ce9b 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts @@ -42,6 +42,7 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { joinPath } from 'vs/base/common/resources'; import { Registry } from 'vs/platform/registry/common/platform'; import { IOutputChannelRegistry, Extensions } from 'vs/workbench/services/output/common/output'; +import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; export class ExtensionHostProcessWorker implements IExtensionHostStarter { @@ -78,7 +79,7 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter { @INotificationService private readonly _notificationService: INotificationService, @IElectronService private readonly _electronService: IElectronService, @ILifecycleService private readonly _lifecycleService: ILifecycleService, - @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, + @IWorkbenchEnvironmentService private readonly _environmentService: INativeWorkbenchEnvironmentService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @ILogService private readonly _logService: ILogService, @ILabelService private readonly _labelService: ILabelService, diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index b4a88b0a363..75a7b78a09f 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -459,16 +459,13 @@ export class ExtensionService extends AbstractExtensionService implements IExten } catch (err) { const remoteName = getRemoteName(remoteAuthority); if (RemoteAuthorityResolverError.isNoResolverFound(err)) { - this._handleNoResolverFound(remoteName, allExtensions); + err.isHandled = await this._handleNoResolverFound(remoteName, allExtensions); } else { console.log(err); - if (RemoteAuthorityResolverError.isHandledNotAvailable(err)) { - console.log(`Not showing a notification for the error`); - } else { - this._notificationService.notify({ severity: Severity.Error, message: nls.localize('resolveAuthorityFailure', "Resolving the authority `{0}` failed", remoteName) }); + if (RemoteAuthorityResolverError.isHandled(err)) { + console.log(`Error handled: Not showing a notification for the error`); } } - this._remoteAuthorityResolverService.setResolvedAuthorityError(remoteAuthority, err); // Proceed with the local extension host @@ -584,10 +581,10 @@ export class ExtensionService extends AbstractExtensionService implements IExten } } - private async _handleNoResolverFound(remoteName: string, allExtensions: IExtensionDescription[]): Promise { + private async _handleNoResolverFound(remoteName: string, allExtensions: IExtensionDescription[]): Promise { const recommendation = this._productService.remoteExtensionTips?.[remoteName]; if (!recommendation) { - return; + return false; } const sendTelemetry = (userReaction: 'install' | 'enable' | 'cancel') => { /* __GDPR__ @@ -603,7 +600,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten const extension = allExtensions.filter(e => e.identifier.value === resolverExtensionId)[0]; if (extension) { if (this._isDisabled(extension)) { - const message = nls.localize('enableResolver', "Extension '{0}' is required to open the remote window.\nOk to enable?", recommendation.friendlyName); + const message = nls.localize('enableResolver', "Extension '{0}' is required to open the remote window.\nOK to enable?", recommendation.friendlyName); this._notificationService.prompt(Severity.Info, message, [{ label: nls.localize('enable', 'Enable and Reload'), @@ -618,7 +615,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten } } else { // Install the Extension and reload the window to handle. - const message = nls.localize('installResolver', "Extension '{0}' is required to open the remote window.\nOk to install?", recommendation.friendlyName); + const message = nls.localize('installResolver', "Extension '{0}' is required to open the remote window.\nnOK to install?", recommendation.friendlyName); this._notificationService.prompt(Severity.Info, message, [{ label: nls.localize('install', 'Install and Reload'), @@ -641,6 +638,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten ); } + return true; } } diff --git a/src/vs/workbench/services/keybinding/browser/keybindingService.ts b/src/vs/workbench/services/keybinding/browser/keybindingService.ts index 7db79b0c45a..42dfc300992 100644 --- a/src/vs/workbench/services/keybinding/browser/keybindingService.ts +++ b/src/vs/workbench/services/keybinding/browser/keybindingService.ts @@ -15,7 +15,7 @@ import { OS, OperatingSystem } from 'vs/base/common/platform'; import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { Extensions as ConfigExtensions, IConfigurationNode, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; -import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, IContextKeyService, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { Extensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { AbstractKeybindingService } from 'vs/platform/keybinding/common/abstractKeybindingService'; @@ -510,7 +510,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService { let commandAction = MenuRegistry.getCommand(command); let precondition = commandAction && commandAction.precondition; - let fullWhen: ContextKeyExpr | undefined; + let fullWhen: ContextKeyExpression | undefined; if (when && precondition) { fullWhen = ContextKeyExpr.and(precondition, ContextKeyExpr.deserialize(when)); } else if (when) { diff --git a/src/vs/workbench/services/keybinding/common/keybindingIO.ts b/src/vs/workbench/services/keybinding/common/keybindingIO.ts index f7c600c0a02..2d4da1d3ff3 100644 --- a/src/vs/workbench/services/keybinding/common/keybindingIO.ts +++ b/src/vs/workbench/services/keybinding/common/keybindingIO.ts @@ -6,7 +6,7 @@ import { SimpleKeybinding } from 'vs/base/common/keyCodes'; import { KeybindingParser } from 'vs/base/common/keybindingParser'; import { ScanCodeBinding } from 'vs/base/common/scanCode'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { IUserFriendlyKeybinding } from 'vs/platform/keybinding/common/keybinding'; import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem'; @@ -14,7 +14,7 @@ export interface IUserKeybindingItem { parts: (SimpleKeybinding | ScanCodeBinding)[]; command: string | null; commandArgs?: any; - when: ContextKeyExpr | undefined; + when: ContextKeyExpression | undefined; } export class KeybindingIO { diff --git a/src/vs/workbench/services/label/common/labelService.ts b/src/vs/workbench/services/label/common/labelService.ts index 8bc3fa0db09..0c2905e2708 100644 --- a/src/vs/workbench/services/label/common/labelService.ts +++ b/src/vs/workbench/services/label/common/labelService.ts @@ -5,9 +5,9 @@ import { localize } from 'vs/nls'; import { URI } from 'vs/base/common/uri'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; import * as paths from 'vs/base/common/path'; -import { Event, Emitter } from 'vs/base/common/event'; +import { Emitter } from 'vs/base/common/event'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { Registry } from 'vs/platform/registry/common/platform'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -16,7 +16,7 @@ import { isEqual, basenameOrAuthority, basename, joinPath, dirname } from 'vs/ba import { tildify, getPathLabel } from 'vs/base/common/labels'; import { ltrim, endsWith } from 'vs/base/common/strings'; import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, WORKSPACE_EXTENSION, toWorkspaceIdentifier, isWorkspaceIdentifier, isUntitledWorkspace } from 'vs/platform/workspaces/common/workspaces'; -import { ILabelService, ResourceLabelFormatter, ResourceLabelFormatting } from 'vs/platform/label/common/label'; +import { ILabelService, ResourceLabelFormatter, ResourceLabelFormatting, IFormatterChangeEvent } from 'vs/platform/label/common/label'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { match } from 'vs/base/common/glob'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; @@ -89,19 +89,20 @@ class ResourceLabelFormattersHandler implements IWorkbenchContribution { } Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ResourceLabelFormattersHandler, LifecyclePhase.Restored); -export class LabelService implements ILabelService { +export class LabelService extends Disposable implements ILabelService { + _serviceBrand: undefined; private formatters: ResourceLabelFormatter[] = []; - private readonly _onDidChangeFormatters = new Emitter(); + + private readonly _onDidChangeFormatters = this._register(new Emitter()); + readonly onDidChangeFormatters = this._onDidChangeFormatters.event; constructor( @IEnvironmentService private readonly environmentService: IEnvironmentService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - ) { } - - get onDidChangeFormatters(): Event { - return this._onDidChangeFormatters.event; + ) { + super(); } findFormatting(resource: URI): ResourceLabelFormatting | undefined { @@ -226,12 +227,12 @@ export class LabelService implements ILabelService { registerFormatter(formatter: ResourceLabelFormatter): IDisposable { this.formatters.push(formatter); - this._onDidChangeFormatters.fire(); + this._onDidChangeFormatters.fire({ scheme: formatter.scheme }); return { dispose: () => { this.formatters = this.formatters.filter(f => f !== formatter); - this._onDidChangeFormatters.fire(); + this._onDidChangeFormatters.fire({ scheme: formatter.scheme }); } }; } diff --git a/src/vs/workbench/services/notification/common/notificationService.ts b/src/vs/workbench/services/notification/common/notificationService.ts index 80a451c1589..0a9c6e448be 100644 --- a/src/vs/workbench/services/notification/common/notificationService.ts +++ b/src/vs/workbench/services/notification/common/notificationService.ts @@ -87,11 +87,14 @@ export class NotificationService extends Disposable implements INotificationServ })); // Insert as primary or secondary action - const actions = notification.actions || { primary: [], secondary: [] }; + const actions = { + primary: notification.actions?.primary || [], + secondary: notification.actions?.secondary || [] + }; if (!notification.neverShowAgain.isSecondary) { - actions.primary = [neverShowAgainAction, ...(actions.primary || [])]; // action comes first + actions.primary = [neverShowAgainAction, ...actions.primary]; // action comes first } else { - actions.secondary = [...(actions.secondary || []), neverShowAgainAction]; // actions comes last + actions.secondary = [...actions.secondary, neverShowAgainAction]; // actions comes last } notification.actions = actions; diff --git a/src/vs/workbench/services/progress/browser/progressIndicator.ts b/src/vs/workbench/services/progress/browser/progressIndicator.ts index ff1da4bab76..dc0b538df55 100644 --- a/src/vs/workbench/services/progress/browser/progressIndicator.ts +++ b/src/vs/workbench/services/progress/browser/progressIndicator.ts @@ -45,7 +45,7 @@ export class ProgressBarIndicator extends Disposable implements IProgressIndicat }; } - async showWhile(promise: Promise, delay?: number): Promise { + async showWhile(promise: Promise, delay?: number): Promise { try { this.progressbar.infinite().show(delay); @@ -92,7 +92,7 @@ export class EditorProgressIndicator extends ProgressBarIndicator { return super.show(infiniteOrTotal, delay); } - async showWhile(promise: Promise, delay?: number): Promise { + async showWhile(promise: Promise, delay?: number): Promise { // No editor open: ignore any progress reporting if (this.group.isEmpty) { @@ -125,7 +125,7 @@ namespace ProgressIndicatorState { readonly type = Type.While; constructor( - readonly whilePromise: Promise, + readonly whilePromise: Promise, readonly whileStart: number, readonly whileDelay: number, ) { } @@ -311,7 +311,7 @@ export class CompositeProgressIndicator extends CompositeScope implements IProgr }; } - async showWhile(promise: Promise, delay?: number): Promise { + async showWhile(promise: Promise, delay?: number): Promise { // Join with existing running promise to ensure progress is accurate if (this.progressState.type === ProgressIndicatorState.Type.While) { diff --git a/src/vs/workbench/services/progress/browser/progressService.ts b/src/vs/workbench/services/progress/browser/progressService.ts index 296e4f47bf5..b96b974ea2d 100644 --- a/src/vs/workbench/services/progress/browser/progressService.ts +++ b/src/vs/workbench/services/progress/browser/progressService.ts @@ -6,10 +6,10 @@ import 'vs/css!./media/progressService'; import { localize } from 'vs/nls'; -import { IDisposable, dispose, DisposableStore, MutableDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose, DisposableStore, Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IProgressService, IProgressOptions, IProgressStep, ProgressLocation, IProgress, Progress, IProgressCompositeOptions, IProgressNotificationOptions, IProgressRunner, IProgressIndicator, IProgressWindowOptions } from 'vs/platform/progress/common/progress'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { StatusbarAlignment, IStatusbarService } from 'vs/workbench/services/statusbar/common/statusbar'; +import { StatusbarAlignment, IStatusbarService, IStatusbarEntryAccessor, IStatusbarEntry } from 'vs/workbench/services/statusbar/common/statusbar'; import { timeout } from 'vs/base/common/async'; import { ProgressBadge, IActivityService } from 'vs/workbench/services/activity/common/activity'; import { INotificationService, Severity, INotificationHandle } from 'vs/platform/notification/common/notification'; @@ -24,14 +24,12 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { EventHelper } from 'vs/base/browser/dom'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; +import { parseLinkedText } from 'vs/base/common/linkedText'; export class ProgressService extends Disposable implements IProgressService { _serviceBrand: undefined; - private readonly stack: [IProgressOptions, Progress][] = []; - private readonly globalStatusEntry = this._register(new MutableDisposable()); - constructor( @IActivityService private readonly activityService: IActivityService, @IViewletService private readonly viewletService: IViewletService, @@ -77,6 +75,9 @@ export class ProgressService extends Disposable implements IProgressService { } } + private readonly windowProgressStack: [IProgressOptions, Progress][] = []; + private windowProgressStatusEntry: IStatusbarEntryAccessor | undefined = undefined; + private withWindowProgress(options: IProgressWindowOptions, callback: (progress: IProgress<{ message?: string }>) => Promise): Promise { const task: [IProgressWindowOptions, Progress] = [options, new Progress(() => this.updateWindowProgress())]; @@ -84,7 +85,7 @@ export class ProgressService extends Disposable implements IProgressService { let delayHandle: any = setTimeout(() => { delayHandle = undefined; - this.stack.unshift(task); + this.windowProgressStack.unshift(task); this.updateWindowProgress(); // show progress for at least 150ms @@ -92,8 +93,8 @@ export class ProgressService extends Disposable implements IProgressService { timeout(150), promise ]).finally(() => { - const idx = this.stack.indexOf(task); - this.stack.splice(idx, 1); + const idx = this.windowProgressStack.indexOf(task); + this.windowProgressStack.splice(idx, 1); this.updateWindowProgress(); }); }, 150); @@ -103,10 +104,10 @@ export class ProgressService extends Disposable implements IProgressService { } private updateWindowProgress(idx: number = 0) { - this.globalStatusEntry.clear(); - if (idx < this.stack.length) { - const [options, progress] = this.stack[idx]; + // We still have progress to show + if (idx < this.windowProgressStack.length) { + const [options, progress] = this.windowProgressStack[idx]; let progressTitle = options.title; let progressMessage = progress.value && progress.value.message; @@ -135,11 +136,23 @@ export class ProgressService extends Disposable implements IProgressService { return; } - this.globalStatusEntry.value = this.statusbarService.addEntry({ + const statusEntryProperties: IStatusbarEntry = { text: `$(sync~spin) ${text}`, tooltip: title, command: progressCommand - }, 'status.progress', localize('status.progress', "Progress Message"), StatusbarAlignment.LEFT); + }; + + if (this.windowProgressStatusEntry) { + this.windowProgressStatusEntry.update(statusEntryProperties); + } else { + this.windowProgressStatusEntry = this.statusbarService.addEntry(statusEntryProperties, 'status.progress', localize('status.progress', "Progress Message"), StatusbarAlignment.LEFT); + } + } + + // Progress is done so we remove the status entry + else { + this.windowProgressStatusEntry?.dispose(); + this.windowProgressStatusEntry = undefined; } } @@ -191,6 +204,46 @@ export class ProgressService extends Disposable implements IProgressService { } }; + const createWindowProgress = () => { + + // Create a promise that we can resolve as needed + // when the outside calls dispose on us + let promiseResolve: () => void; + const promise = new Promise(resolve => promiseResolve = resolve); + + this.withWindowProgress({ + location: ProgressLocation.Window, + title: options.title ? parseLinkedText(options.title).toString() : undefined, // convert markdown links => string + command: 'notifications.showList' + }, progress => { + + function reportProgress(step: IProgressStep) { + if (step.message) { + progress.report({ + message: parseLinkedText(step.message).toString() // convert markdown links => string + }); + } + } + + // Apply any progress that was made already + if (progressStateModel.step) { + reportProgress(progressStateModel.step); + } + + // Continue to report progress as it happens + const onDidReportListener = progressStateModel.onDidReport(step => reportProgress(step)); + promise.finally(() => onDidReportListener.dispose()); + + // When the progress model gets disposed, we are done as well + Event.once(progressStateModel.onDispose)(() => promiseResolve()); + + return promise; + }); + + // Dispose means completing our promise + return toDisposable(() => promiseResolve()); + }; + const createNotification = (message: string, increment?: number): INotificationHandle => { const notificationDisposables = new DisposableStore(); @@ -204,7 +257,7 @@ export class ProgressService extends Disposable implements IProgressService { super(`progress.button.${button}`, button, undefined, true); } - async run(): Promise { + async run(): Promise { progressStateModel.cancel(index); } }; @@ -220,7 +273,7 @@ export class ProgressService extends Disposable implements IProgressService { super('progress.cancel', localize('cancel', "Cancel"), undefined, true); } - async run(): Promise { + async run(): Promise { progressStateModel.cancel(); } }; @@ -229,19 +282,34 @@ export class ProgressService extends Disposable implements IProgressService { primaryActions.push(cancelAction); } - const handle = this.notificationService.notify({ + const notification = this.notificationService.notify({ severity: Severity.Info, message, source: options.source, - actions: { primary: primaryActions, secondary: secondaryActions } + actions: { primary: primaryActions, secondary: secondaryActions }, + progress: typeof increment === 'number' && increment >= 0 ? { total: 100, worked: increment } : { infinite: true } }); - updateProgress(handle, increment); + // Switch to window based progress once the notification + // changes visibility to hidden and is still ongoing. + // Remove that window based progress once the notification + // shows again. + let windowProgressDisposable: IDisposable | undefined = undefined; + notificationDisposables.add(notification.onDidChangeVisibility(visible => { + + // Clear any previous running window progress + dispose(windowProgressDisposable); + + // Create new window progress if notification got hidden + if (!visible && !progressStateModel.done) { + windowProgressDisposable = createWindowProgress(); + } + })); // Clear upon dispose - Event.once(handle.onDidClose)(() => notificationDisposables.dispose()); + Event.once(notification.onDidClose)(() => notificationDisposables.dispose()); - return handle; + return notification; }; const updateProgress = (notification: INotificationHandle, increment?: number): void => { diff --git a/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts b/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts index a97aaafe97d..bf8e93ea187 100644 --- a/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts +++ b/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts @@ -164,7 +164,7 @@ class RemoteConnectionFailureNotificationContribution implements IWorkbenchContr // Let's cover the case where connecting to fetch the remote extension info fails remoteAgentService.getEnvironment(true) .then(undefined, err => { - if (!RemoteAuthorityResolverError.isHandledNotAvailable(err)) { + if (!RemoteAuthorityResolverError.isHandled(err)) { notificationService.error(nls.localize('connectionError', "Failed to connect to the remote extension host server (Error: {0})", err ? err.message : '')); } }); diff --git a/src/vs/workbench/services/search/common/search.ts b/src/vs/workbench/services/search/common/search.ts index fa80c141398..df5cdda37dd 100644 --- a/src/vs/workbench/services/search/common/search.ts +++ b/src/vs/workbench/services/search/common/search.ts @@ -9,7 +9,7 @@ import * as glob from 'vs/base/common/glob'; import { IDisposable } from 'vs/base/common/lifecycle'; import * as objects from 'vs/base/common/objects'; import * as extpath from 'vs/base/common/extpath'; -import { getNLines } from 'vs/base/common/strings'; +import { fuzzyContains, getNLines } from 'vs/base/common/strings'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IFilesConfiguration } from 'vs/platform/files/common/files'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; @@ -50,6 +50,7 @@ export interface ISearchResultProvider { export interface IFolderQuery { folder: U; + folderName?: string; excludePattern?: glob.IExpression; includePattern?: glob.IExpression; fileEncoding?: string; @@ -437,7 +438,19 @@ export interface IRawSearchService { export interface IRawFileMatch { base?: string; + /** + * The path of the file relative to the containing `base` folder. + * This path is exactly as it appears on the filesystem. + */ relativePath: string; + /** + * This path is transformed for search purposes. For example, this could be + * the `relativePath` with the workspace folder name prepended. This way the + * search algorithm would also match against the name of the containing folder. + * + * If not given, the search algorithm should use `relativePath`. + */ + searchPath?: string; } export interface ISearchEngine { @@ -484,6 +497,11 @@ export function isSerializedFileMatch(arg: ISerializedSearchProgressItem): arg i return !!(arg).path; } +export function isFilePatternMatch(candidate: IRawFileMatch, normalizedFilePatternLowercase: string): boolean { + const pathToMatch = candidate.searchPath ? candidate.searchPath : candidate.relativePath; + return fuzzyContains(pathToMatch, normalizedFilePatternLowercase); +} + export interface ISerializedFileMatch { path: string; results?: ITextSearchResult[]; diff --git a/src/vs/workbench/services/search/common/searchService.ts b/src/vs/workbench/services/search/common/searchService.ts index 990d4637b07..f87b32b45a1 100644 --- a/src/vs/workbench/services/search/common/searchService.ts +++ b/src/vs/workbench/services/search/common/searchService.ts @@ -390,7 +390,7 @@ export class SearchService extends Disposable implements ISearchService { } // Skip files that are not opened as text file - if (!this.editorService.isOpen(this.editorService.createInput({ resource, forceFile: resource.scheme !== Schemas.untitled, forceUntitled: resource.scheme === Schemas.untitled }))) { + if (!this.editorService.isOpen({ resource })) { return; } diff --git a/src/vs/workbench/services/search/node/fileSearch.ts b/src/vs/workbench/services/search/node/fileSearch.ts index e3965113ca9..e0465326652 100644 --- a/src/vs/workbench/services/search/node/fileSearch.ts +++ b/src/vs/workbench/services/search/node/fileSearch.ts @@ -20,7 +20,7 @@ import * as strings from 'vs/base/common/strings'; import * as types from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { readdir } from 'vs/base/node/pfs'; -import { IFileQuery, IFolderQuery, IProgressMessage, ISearchEngineStats, IRawFileMatch, ISearchEngine, ISearchEngineSuccess } from 'vs/workbench/services/search/common/search'; +import { IFileQuery, IFolderQuery, IProgressMessage, ISearchEngineStats, IRawFileMatch, ISearchEngine, ISearchEngineSuccess, isFilePatternMatch } from 'vs/workbench/services/search/common/search'; import { spawnRipgrepCmd } from './ripgrepFileSearch'; import { prepareQuery } from 'vs/base/parts/quickopen/common/quickOpenScorer'; @@ -246,7 +246,7 @@ export class FileWalker { if (noSiblingsClauses) { for (const relativePath of relativeFiles) { - this.matchFile(onResult, { base: rootFolder, relativePath }); + this.matchFile(onResult, { base: rootFolder, relativePath, searchPath: this.getSearchPath(folderQuery, relativePath) }); if (this.isLimitHit) { killCmd(); break; @@ -538,7 +538,11 @@ export class FileWalker { return clb(null, undefined); // ignore file if max file size is hit } - this.matchFile(onResult, { base: rootFolder.fsPath, relativePath: currentRelativePath }); + this.matchFile(onResult, { + base: rootFolder.fsPath, + relativePath: currentRelativePath, + searchPath: this.getSearchPath(folderQuery, currentRelativePath), + }); } // Unwind @@ -552,7 +556,7 @@ export class FileWalker { } private matchFile(onResult: (result: IRawFileMatch) => void, candidate: IRawFileMatch): void { - if (this.isFilePatternMatch(candidate.relativePath) && (!this.includePattern || this.includePattern(candidate.relativePath, path.basename(candidate.relativePath)))) { + if (this.isFileMatch(candidate) && (!this.includePattern || this.includePattern(candidate.relativePath, path.basename(candidate.relativePath)))) { this.resultCount++; if (this.exists || (this.maxResults && this.resultCount > this.maxResults)) { @@ -565,8 +569,7 @@ export class FileWalker { } } - private isFilePatternMatch(path: string): boolean { - + private isFileMatch(candidate: IRawFileMatch): boolean { // Check for search pattern if (this.filePattern) { if (this.filePattern === '*') { @@ -574,7 +577,7 @@ export class FileWalker { } if (this.normalizedFilePatternLowercase) { - return strings.fuzzyContains(path, this.normalizedFilePatternLowercase); + return isFilePatternMatch(candidate, this.normalizedFilePatternLowercase); } } @@ -603,6 +606,19 @@ export class FileWalker { return clb(null, path); } + + /** + * If we're searching for files in multiple workspace folders, then better prepend the + * name of the workspace folder to the path of the file. This way we'll be able to + * better filter files that are all on the top of a workspace folder and have all the + * same name. A typical example are `package.json` or `README.md` files. + */ + private getSearchPath(folderQuery: IFolderQuery, relativePath: string): string { + if (folderQuery.folderName) { + return path.join(folderQuery.folderName, relativePath); + } + return relativePath; + } } export class Engine implements ISearchEngine { diff --git a/src/vs/workbench/services/search/node/rawSearchService.ts b/src/vs/workbench/services/search/node/rawSearchService.ts index 9b268ed6c1a..51f5440de19 100644 --- a/src/vs/workbench/services/search/node/rawSearchService.ts +++ b/src/vs/workbench/services/search/node/rawSearchService.ts @@ -17,7 +17,7 @@ import * as strings from 'vs/base/common/strings'; import { URI, UriComponents } from 'vs/base/common/uri'; import { compareItemsByScore, IItemAccessor, prepareQuery, ScorerCache } from 'vs/base/parts/quickopen/common/quickOpenScorer'; import { MAX_FILE_SIZE } from 'vs/base/node/pfs'; -import { ICachedSearchStats, IFileQuery, IFileSearchStats, IFolderQuery, IProgressMessage, IRawFileQuery, IRawQuery, IRawTextQuery, ITextQuery, IFileSearchProgressItem, IRawFileMatch, IRawSearchService, ISearchEngine, ISearchEngineSuccess, ISerializedFileMatch, ISerializedSearchComplete, ISerializedSearchProgressItem, ISerializedSearchSuccess } from 'vs/workbench/services/search/common/search'; +import { ICachedSearchStats, IFileQuery, IFileSearchStats, IFolderQuery, IProgressMessage, IRawFileQuery, IRawQuery, IRawTextQuery, ITextQuery, IFileSearchProgressItem, IRawFileMatch, IRawSearchService, ISearchEngine, ISearchEngineSuccess, ISerializedFileMatch, ISerializedSearchComplete, ISerializedSearchProgressItem, ISerializedSearchSuccess, isFilePatternMatch } from 'vs/workbench/services/search/common/search'; import { Engine as FileSearchEngine } from 'vs/workbench/services/search/node/fileSearch'; import { TextSearchEngineAdapter } from 'vs/workbench/services/search/node/textSearchAdapter'; @@ -316,7 +316,7 @@ export class SearchService implements IRawSearchService { for (const entry of cachedEntries) { // Check if this entry is a match for the search value - if (!strings.fuzzyContains(entry.relativePath, normalizedSearchValueLowercase)) { + if (!isFilePatternMatch(entry, normalizedSearchValueLowercase)) { continue; } diff --git a/src/vs/workbench/services/statusbar/common/statusbar.ts b/src/vs/workbench/services/statusbar/common/statusbar.ts index 91519f20cbd..31f46524c9c 100644 --- a/src/vs/workbench/services/statusbar/common/statusbar.ts +++ b/src/vs/workbench/services/statusbar/common/statusbar.ts @@ -50,7 +50,7 @@ export interface IStatusbarEntry { /** * Optional arguments for the command. */ - readonly arguments?: any[]; + readonly arguments?: unknown[]; /** * Whether to show a beak above the status bar entry. diff --git a/src/vs/workbench/services/telemetry/electron-browser/telemetryService.ts b/src/vs/workbench/services/telemetry/electron-browser/telemetryService.ts index c2900691c8e..b495fda6b19 100644 --- a/src/vs/workbench/services/telemetry/electron-browser/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/electron-browser/telemetryService.ts @@ -17,6 +17,7 @@ import { resolveWorkbenchCommonProperties } from 'vs/platform/telemetry/node/wor import { TelemetryService as BaseTelemetryService, ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ClassifiedEvent, StrictPropertyCheck, GDPRClassification } from 'vs/platform/telemetry/common/gdprTypings'; +import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; export class TelemetryService extends Disposable implements ITelemetryService { @@ -25,7 +26,7 @@ export class TelemetryService extends Disposable implements ITelemetryService { private impl: ITelemetryService; constructor( - @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IWorkbenchEnvironmentService environmentService: INativeWorkbenchEnvironmentService, @IProductService productService: IProductService, @ISharedProcessService sharedProcessService: ISharedProcessService, @ILogService logService: ILogService, @@ -38,7 +39,7 @@ export class TelemetryService extends Disposable implements ITelemetryService { const channel = sharedProcessService.getChannel('telemetryAppender'); const config: ITelemetryServiceConfig = { appender: combinedAppender(new TelemetryAppenderClient(channel), new LogAppender(logService)), - commonProperties: resolveWorkbenchCommonProperties(storageService, productService.commit, productService.version, environmentService.configuration.machineId!, productService.msftInternalDomains, environmentService.installSourcePath, environmentService.configuration.remoteAuthority), + commonProperties: resolveWorkbenchCommonProperties(storageService, productService.commit, productService.version, environmentService.configuration.machineId, productService.msftInternalDomains, environmentService.installSourcePath, environmentService.configuration.remoteAuthority), piiPaths: environmentService.extensionsPath ? [environmentService.appRoot, environmentService.extensionsPath] : [environmentService.appRoot] }; diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index f97ce3ccfb2..ef6a97ac6ec 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -442,7 +442,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex //#region revert - async revert(resource: URI, options?: IRevertOptions): Promise { + async revert(resource: URI, options?: IRevertOptions): Promise { // Untitled if (resource.scheme === Schemas.untitled) { @@ -450,17 +450,15 @@ export abstract class AbstractTextFileService extends Disposable implements ITex if (model) { return model.revert(options); } - - return false; } // File - const model = this.files.get(resource); - if (model && (model.isDirty() || options?.force)) { - return model.revert(options); + else { + const model = this.files.get(resource); + if (model && (model.isDirty() || options?.force)) { + return model.revert(options); + } } - - return false; } //#endregion diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index 7afb0645e24..17eadf382ca 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -206,9 +206,9 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil //#region Revert - async revert(options?: IRevertOptions): Promise { + async revert(options?: IRevertOptions): Promise { if (!this.isResolved()) { - return false; + return; } // Unset flags @@ -240,8 +240,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil if (wasDirty) { this._onDidChangeDirty.fire(); } - - return true; } //#endregion @@ -251,6 +249,13 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil async load(options?: ITextFileLoadOptions): Promise { this.logService.trace('[text file model] load() - enter', this.resource.toString(true)); + // Return early if we are disposed + if (this.isDisposed()) { + this.logService.trace('[text file model] load() - exit - without loading because model is disposed', this.resource.toString(true)); + + return this; + } + // It is very important to not reload the model when the model is dirty. // We also only want to reload the model from the disk if no save is pending // to avoid data loss. @@ -359,7 +364,14 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private loadFromContent(content: ITextFileStreamContent, options?: ITextFileLoadOptions, fromBackup?: boolean): TextFileEditorModel { - this.logService.trace('[text file model] load() - resolved content', this.resource.toString(true)); + this.logService.trace('[text file model] loadFromContent() - enter', this.resource.toString(true)); + + // Return early if we are disposed + if (this.isDisposed()) { + this.logService.trace('[text file model] loadFromContent() - exit - because model is disposed', this.resource.toString(true)); + + return this; + } // Update our resolved disk stat model this.updateLastResolvedFileStat({ @@ -405,7 +417,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private doCreateTextModel(resource: URI, value: ITextBufferFactory, fromBackup: boolean): void { - this.logService.trace('[text file model] load() - created text editor model', this.resource.toString(true)); + this.logService.trace('[text file model] doCreateTextModel()', this.resource.toString(true)); // Create model const textModel = this.createTextEditorModel(value, resource, this.preferredMode); @@ -420,7 +432,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private doUpdateTextModel(value: ITextBufferFactory): void { - this.logService.trace('[text file model] load() - updated text editor model', this.resource.toString(true)); + this.logService.trace('[text file model] doUpdateTextModel()', this.resource.toString(true)); // Update model value in a block that ignores content change events for dirty tracking this.ignoreDirtyOnModelContentChange = true; @@ -703,7 +715,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } private handleSaveSuccess(stat: IFileStatWithMetadata, versionId: number, options: ITextFileSaveOptions): void { - this.logService.trace(`[text file model] doSave(${versionId}) - after write()`, this.resource.toString(true)); // Updated resolved stat with updated stat this.updateLastResolvedFileStat(stat); @@ -906,6 +917,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } dispose(): void { + this.logService.trace('[text file model] dispose()', this.resource.toString(true)); + this.disposed = true; this.inConflictMode = false; this.inOrphanMode = false; diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index d3531ab7b7b..07956b419a5 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -78,7 +78,7 @@ export interface ITextFileService extends IDisposable { * @param resource the resource of the file to revert. * @param force to force revert even when the file is not dirty */ - revert(resource: URI, options?: IRevertOptions): Promise; + revert(resource: URI, options?: IRevertOptions): Promise; /** * Read the contents of a file identified by the resource. @@ -411,7 +411,7 @@ export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport updatePreferredEncoding(encoding: string | undefined): void; save(options?: ITextFileSaveOptions): Promise; - revert(options?: IRevertOptions): Promise; + revert(options?: IRevertOptions): Promise; load(options?: ITextFileLoadOptions): Promise; diff --git a/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts b/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts index aeff0a5f220..52d0c2f949a 100644 --- a/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts +++ b/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts @@ -39,6 +39,7 @@ import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IRemotePathService } from 'vs/workbench/services/path/common/remotePathService'; import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; +import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; export class NativeTextFileService extends AbstractTextFileService { @@ -48,7 +49,7 @@ export class NativeTextFileService extends AbstractTextFileService { @ILifecycleService lifecycleService: ILifecycleService, @IInstantiationService instantiationService: IInstantiationService, @IModelService modelService: IModelService, - @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IWorkbenchEnvironmentService protected environmentService: INativeWorkbenchEnvironmentService, @IDialogService dialogService: IDialogService, @IFileDialogService fileDialogService: IFileDialogService, @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, diff --git a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts index 7140b161af9..b61434bb1bc 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts @@ -80,6 +80,12 @@ suite('Files - TextFileEditorModel', () => { assert.equal(accessor.workingCopyService.dirtyCount, 0); + let savedEvent = false; + model.onDidSave(() => savedEvent = true); + + await model.save(); + assert.ok(!savedEvent); + model.updateTextEditorModel(createTextBufferFactory('bar')); assert.ok(getLastModifiedTime(model) <= Date.now()); assert.ok(model.hasState(TextFileEditorModelState.DIRTY)); @@ -87,9 +93,6 @@ suite('Files - TextFileEditorModel', () => { assert.equal(accessor.workingCopyService.dirtyCount, 1); assert.equal(accessor.workingCopyService.isDirty(model.resource), true); - let savedEvent = false; - model.onDidSave(() => savedEvent = true); - let workingCopyEvent = false; accessor.workingCopyService.onDidChangeDirty(e => { if (e.resource.toString() === model.resource.toString()) { @@ -110,6 +113,11 @@ suite('Files - TextFileEditorModel', () => { assert.equal(accessor.workingCopyService.dirtyCount, 0); assert.equal(accessor.workingCopyService.isDirty(model.resource), false); + savedEvent = false; + + await model.save({ force: true }); + assert.ok(savedEvent); + model.dispose(); assert.ok(!accessor.modelService.getModel(model.resource)); }); @@ -404,7 +412,7 @@ suite('Files - TextFileEditorModel', () => { model.dispose(); }); - test('No Dirty for readonly models', async function () { + test('No Dirty or saving for readonly models', async function () { let workingCopyEvent = false; accessor.workingCopyService.onDidChangeDirty(e => { if (e.resource.toString() === model.resource.toString()) { @@ -414,10 +422,18 @@ suite('Files - TextFileEditorModel', () => { const model = instantiationService.createInstance(TestReadonlyTextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + let saveEvent = false; + model.onDidSave(() => { + saveEvent = true; + }); + await model.load(); model.updateTextEditorModel(createTextBufferFactory('foo')); assert.ok(!model.isDirty()); + await model.save({ force: true }); + assert.equal(saveEvent, false); + await model.revert({ soft: true }); assert.ok(!model.isDirty()); diff --git a/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts index d90c11ca710..ab4d13394e2 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts @@ -97,8 +97,7 @@ suite('Files - TextFileService', () => { model!.textEditorModel!.setValue('foo'); assert.ok(accessor.textFileService.isDirty(model.resource)); - const res = await accessor.textFileService.revert(model.resource); - assert.ok(res); + await accessor.textFileService.revert(model.resource); assert.ok(!accessor.textFileService.isDirty(model.resource)); }); diff --git a/src/vs/workbench/services/timer/electron-browser/timerService.ts b/src/vs/workbench/services/timer/electron-browser/timerService.ts index 3b3d71f9eb0..47fd9783039 100644 --- a/src/vs/workbench/services/timer/electron-browser/timerService.ts +++ b/src/vs/workbench/services/timer/electron-browser/timerService.ts @@ -18,6 +18,7 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; /* __GDPR__FRAGMENT__ "IMemoryInfo" : { @@ -303,7 +304,7 @@ class TimerService implements ITimerService { constructor( @IElectronService private readonly _electronService: IElectronService, - @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, + @IWorkbenchEnvironmentService private readonly _environmentService: INativeWorkbenchEnvironmentService, @ILifecycleService private readonly _lifecycleService: ILifecycleService, @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, @IExtensionService private readonly _extensionService: IExtensionService, diff --git a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts index e42c09b2e33..bd4f8afbc99 100644 --- a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts +++ b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts @@ -253,7 +253,7 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt return !!target; } - async revert(): Promise { + async revert(): Promise { this.setDirty(false); // Emit as event @@ -263,8 +263,6 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt // no actual source on disk to revert to. As such we // dispose the model. this.dispose(); - - return true; } async backup(): Promise { diff --git a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts index 9b2b8015372..cf3ce106c2a 100644 --- a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts +++ b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts @@ -82,7 +82,7 @@ suite('Untitled text editors', () => { assert.ok(!workingCopyService.isDirty(input2.resource)); assert.equal(workingCopyService.dirtyCount, 0); - assert.equal(await input1.revert(0), false); + await input1.revert(0); assert.ok(input1.isDisposed()); assert.ok(!service.get(input1.resource)); diff --git a/src/vs/workbench/services/userDataSync/electron-browser/userDataAutoSyncService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataAutoSyncService.ts index 4dc5de8bc46..88ee0dcb78f 100644 --- a/src/vs/workbench/services/userDataSync/electron-browser/userDataAutoSyncService.ts +++ b/src/vs/workbench/services/userDataSync/electron-browser/userDataAutoSyncService.ts @@ -24,8 +24,8 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto this.channel = sharedProcessService.getChannel('userDataAutoSync'); } - triggerAutoSync(): Promise { - return this.channel.call('triggerAutoSync'); + triggerAutoSync(sources: string[]): Promise { + return this.channel.call('triggerAutoSync', [sources]); } } diff --git a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts index 2b495a43dc9..b3ec8ffeb95 100644 --- a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts +++ b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts @@ -22,7 +22,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ private _onDidChangeStatus: Emitter = this._register(new Emitter()); readonly onDidChangeStatus: Event = this._onDidChangeStatus.event; - get onDidChangeLocal(): Event { return this.channel.listen('onDidChangeLocal'); } + get onDidChangeLocal(): Event { return this.channel.listen('onDidChangeLocal'); } private _conflictsSources: SyncSource[] = []; get conflictsSources(): SyncSource[] { return this._conflictsSources; } diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts index 7be4f335d23..628ca136d64 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts @@ -8,7 +8,7 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { Event, Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { Disposable, IDisposable, toDisposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; -import { TernarySearchTree, values } from 'vs/base/common/map'; +import { values, ResourceMap } from 'vs/base/common/map'; import { ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; import { ITextSnapshot } from 'vs/editor/common/model'; @@ -42,10 +42,20 @@ export interface IWorkingCopyBackup { export interface IWorkingCopy { + /** + * The unique resource of the working copy. There can only be one + * working copy in the system with the same URI. + */ readonly resource: URI; + /** + * Human readable name of the working copy. + */ readonly name: string; + /** + * The capabilities of the working copy. + */ readonly capabilities: WorkingCopyCapabilities; @@ -83,15 +93,22 @@ export interface IWorkingCopy { * * Providers of working copies should use `IBackupFileService.resolve(workingCopy.resource)` * to retrieve the backup metadata associated when loading the working copy. - * - * Not providing this method from the working copy will disable any - * backups and hot-exit functionality for those working copies. */ - backup?(): Promise; + backup(): Promise; + /** + * Asks the working copy to save. If the working copy was dirty, it is + * expected to be non-dirty after this operation has finished. + * + * @returns `true` if the operation was successful and `false` otherwise. + */ save(options?: ISaveOptions): Promise; - revert(options?: IRevertOptions): Promise; + /** + * Asks the working copy to revert. If the working copy was dirty, it is + * expected to be non-dirty after this operation has finished. + */ + revert(options?: IRevertOptions): Promise; //#endregion } @@ -133,8 +150,12 @@ export interface IWorkingCopyService { readonly workingCopies: IWorkingCopy[]; - getWorkingCopies(resource: URI): IWorkingCopy[]; - + /** + * Register a new working copy with the service. This method will + * throw if you try to register a working copy with a resource + * that was already registered before. There can only be 1 working + * copy per resource registered to the service. + */ registerWorkingCopy(workingCopy: IWorkingCopy): IDisposable; //#endregion @@ -163,30 +184,21 @@ export class WorkingCopyService extends Disposable implements IWorkingCopyServic //#region Registry - private readonly mapResourceToWorkingCopy = TernarySearchTree.forPaths>(); - get workingCopies(): IWorkingCopy[] { return values(this._workingCopies); } private _workingCopies = new Set(); - getWorkingCopies(resource: URI): IWorkingCopy[] { - const workingCopies = this.mapResourceToWorkingCopy.get(resource.toString()); - - return workingCopies ? values(workingCopies) : []; - } + private readonly mapResourceToWorkingCopy = new ResourceMap(); registerWorkingCopy(workingCopy: IWorkingCopy): IDisposable { + if (this.mapResourceToWorkingCopy.has(workingCopy.resource)) { + throw new Error(`Cannot register more than one working copy with the same resource ${workingCopy.resource.toString()}.`); + } + const disposables = new DisposableStore(); // Registry - let workingCopiesForResource = this.mapResourceToWorkingCopy.get(workingCopy.resource.toString()); - if (!workingCopiesForResource) { - workingCopiesForResource = new Set(); - this.mapResourceToWorkingCopy.set(workingCopy.resource.toString(), workingCopiesForResource); - } - - workingCopiesForResource.add(workingCopy); - this._workingCopies.add(workingCopy); + this.mapResourceToWorkingCopy.set(workingCopy.resource, workingCopy); // Wire in Events disposables.add(workingCopy.onDidChangeContent(() => this._onDidChangeContent.fire(workingCopy))); @@ -210,12 +222,8 @@ export class WorkingCopyService extends Disposable implements IWorkingCopyServic private unregisterWorkingCopy(workingCopy: IWorkingCopy): void { // Remove from registry - const workingCopiesForResource = this.mapResourceToWorkingCopy.get(workingCopy.resource.toString()); - if (workingCopiesForResource && workingCopiesForResource.delete(workingCopy) && workingCopiesForResource.size === 0) { - this.mapResourceToWorkingCopy.delete(workingCopy.resource.toString()); - } - this._workingCopies.delete(workingCopy); + this.mapResourceToWorkingCopy.delete(workingCopy.resource); // If copy is dirty, ensure to fire an event to signal the dirty change // (a disposed working copy cannot account for being dirty in our model) @@ -256,13 +264,9 @@ export class WorkingCopyService extends Disposable implements IWorkingCopyServic } isDirty(resource: URI): boolean { - const workingCopies = this.mapResourceToWorkingCopy.get(resource.toString()); - if (workingCopies) { - for (const workingCopy of workingCopies) { - if (workingCopy.isDirty()) { - return true; - } - } + const workingCopy = this.mapResourceToWorkingCopy.get(resource); + if (workingCopy) { + return workingCopy.isDirty(); } return false; diff --git a/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts b/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts index 4e189be6867..5c31dd4fb89 100644 --- a/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts @@ -56,10 +56,8 @@ suite('WorkingCopyService', () => { return true; } - async revert(options?: IRevertOptions): Promise { + async revert(options?: IRevertOptions): Promise { this.setDirty(false); - - return true; } async backup(): Promise { @@ -112,8 +110,8 @@ suite('WorkingCopyService', () => { assert.equal(service.dirtyCount, 1); assert.equal(service.dirtyWorkingCopies.length, 1); assert.equal(service.dirtyWorkingCopies[0], copy1); - assert.equal(service.getWorkingCopies(copy1.resource).length, 1); - assert.equal(service.getWorkingCopies(copy1.resource)[0], copy1); + assert.equal(service.workingCopies.length, 1); + assert.equal(service.workingCopies[0], copy1); assert.equal(service.isDirty(resource1), true); assert.equal(service.hasDirty, true); assert.equal(onDidChangeDirty.length, 1); @@ -167,7 +165,7 @@ suite('WorkingCopyService', () => { assert.equal(onDidChangeDirty[3], copy2); }); - test('registry - multiple copies on same resource', () => { + test('registry - multiple copies on same resource throws', () => { const service = new TestWorkingCopyService(); const onDidChangeDirty: IWorkingCopy[] = []; @@ -176,37 +174,10 @@ suite('WorkingCopyService', () => { const resource = URI.parse('custom://some/folder/custom.txt'); const copy1 = new TestWorkingCopy(resource); - const unregister1 = service.registerWorkingCopy(copy1); + service.registerWorkingCopy(copy1); const copy2 = new TestWorkingCopy(resource); - const unregister2 = service.registerWorkingCopy(copy2); - assert.equal(service.getWorkingCopies(copy1.resource).length, 2); - assert.equal(service.getWorkingCopies(copy1.resource)[0], copy1); - assert.equal(service.getWorkingCopies(copy1.resource)[1], copy2); - - copy1.setDirty(true); - - assert.equal(service.dirtyCount, 1); - assert.equal(onDidChangeDirty.length, 1); - assert.equal(service.isDirty(resource), true); - - copy2.setDirty(true); - - assert.equal(service.dirtyCount, 2); - assert.equal(onDidChangeDirty.length, 2); - assert.equal(service.isDirty(resource), true); - - unregister1.dispose(); - - assert.equal(service.dirtyCount, 1); - assert.equal(onDidChangeDirty.length, 3); - assert.equal(service.isDirty(resource), true); - - unregister2.dispose(); - - assert.equal(service.dirtyCount, 0); - assert.equal(onDidChangeDirty.length, 4); - assert.equal(service.isDirty(resource), false); + assert.throws(() => service.registerWorkingCopy(copy2)); }); }); diff --git a/src/vs/workbench/services/workspaces/electron-browser/workspaceEditingService.ts b/src/vs/workbench/services/workspaces/electron-browser/workspaceEditingService.ts index d65ea2c055e..e3dc86a88db 100644 --- a/src/vs/workbench/services/workspaces/electron-browser/workspaceEditingService.ts +++ b/src/vs/workbench/services/workspaces/electron-browser/workspaceEditingService.ts @@ -31,6 +31,7 @@ import { IElectronService } from 'vs/platform/electron/node/electron'; import { isMacintosh } from 'vs/base/common/platform'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { BackupFileService } from 'vs/workbench/services/backup/common/backupFileService'; +import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; export class NativeWorkspaceEditingService extends AbstractWorkspaceEditingService { @@ -49,7 +50,7 @@ export class NativeWorkspaceEditingService extends AbstractWorkspaceEditingServi @IFileService fileService: IFileService, @ITextFileService textFileService: ITextFileService, @IWorkspacesService workspacesService: IWorkspacesService, - @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IWorkbenchEnvironmentService protected environmentService: INativeWorkbenchEnvironmentService, @IFileDialogService fileDialogService: IFileDialogService, @IDialogService protected dialogService: IDialogService, @ILifecycleService private readonly lifecycleService: ILifecycleService, diff --git a/src/vs/workbench/test/browser/api/extHostApiCommands.test.ts b/src/vs/workbench/test/browser/api/extHostApiCommands.test.ts index ccdf0740f5b..60ee0ec0087 100644 --- a/src/vs/workbench/test/browser/api/extHostApiCommands.test.ts +++ b/src/vs/workbench/test/browser/api/extHostApiCommands.test.ts @@ -895,7 +895,7 @@ suite('ExtHostLanguageFeatureCommands', function () { disposables.push(extHost.registerCallHierarchyProvider(nullExtensionDescription, defaultSelector, new class implements vscode.CallHierarchyProvider { - prepareCallHierarchy(document: vscode.TextDocument, position: vscode.Position, ): vscode.ProviderResult { + prepareCallHierarchy(document: vscode.TextDocument, position: vscode.Position,): vscode.ProviderResult { return new types.CallHierarchyItem(types.SymbolKind.Constant, 'ROOT', 'ROOT', document.uri, new types.Range(0, 0, 0, 0), new types.Range(0, 0, 0, 0)); } 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 19ba3f55d7c..e76d0813009 100644 --- a/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts @@ -103,11 +103,9 @@ suite('Workbench base editor', () => { assert(!e.isVisible()); assert(!e.input); - assert(!e.options); await e.setInput(input, options, CancellationToken.None); assert.strictEqual(input, e.input); - assert.strictEqual(options, e.options); const group = new TestEditorGroupView(1); e.setVisible(true, group); assert(e.isVisible()); @@ -120,7 +118,6 @@ suite('Workbench base editor', () => { e.setVisible(false, group); assert(!e.isVisible()); assert(!e.input); - assert(!e.options); assert(!e.getControl()); }); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 7a7f9f4ee7e..e164075d474 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -10,7 +10,7 @@ import * as resources from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { IEditorInputWithOptions, CloseDirection, IEditorIdentifier, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, IEditorInput, IEditor, IEditorCloseEvent, IEditorPartOptions, IRevertOptions, GroupIdentifier, EditorInput, EditorOptions, EditorsOrder, IFileEditorInput, IEditorInputFactoryRegistry, IEditorInputFactory, Extensions as EditorExtensions, ISaveOptions, IMoveResult } from 'vs/workbench/common/editor'; +import { IEditorInputWithOptions, CloseDirection, IEditorIdentifier, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, IEditorInput, IEditor, IEditorCloseEvent, IEditorPartOptions, IRevertOptions, GroupIdentifier, EditorInput, EditorOptions, EditorsOrder, IFileEditorInput, IEditorInputFactoryRegistry, IEditorInputFactory, Extensions as EditorExtensions, ISaveOptions, IMoveResult, ITextEditor, ITextDiffEditor, ITextSideBySideEditor, IVisibleEditor } from 'vs/workbench/common/editor'; import { IEditorOpeningEvent, EditorServiceImpl, IEditorGroupView, IEditorGroupsAccessor } from 'vs/workbench/browser/parts/editor/editor'; import { Event, Emitter } from 'vs/base/common/event'; import { IBackupFileService, IResolvedBackup } from 'vs/workbench/services/backup/common/backup'; @@ -18,7 +18,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, IEditorModel, ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { IUntitledTextEditorService, UntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ILifecycleService, BeforeShutdownEvent, ShutdownReason, StartupKind, LifecyclePhase, WillShutdownEvent } from 'vs/platform/lifecycle/common/lifecycle'; @@ -42,7 +42,7 @@ import { IPosition, Position as EditorPosition } from 'vs/editor/common/core/pos import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { MockContextKeyService, MockKeybindingService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; -import { ITextBufferFactory, DefaultEndOfLine, EndOfLinePreference, IModelDecorationOptions, ITextModel, ITextSnapshot } from 'vs/editor/common/model'; +import { ITextBufferFactory, DefaultEndOfLine, EndOfLinePreference, ITextSnapshot } from 'vs/editor/common/model'; import { Range } from 'vs/editor/common/core/range'; import { IDialogService, IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, IFileDialogService, ConfirmResult } from 'vs/platform/dialogs/common/dialogs'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -51,12 +51,10 @@ 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, DisposableStore } from 'vs/base/common/lifecycle'; -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 { IEditorGroupsService, IEditorGroup, GroupsOrder, GroupsArrangement, GroupDirection, IAddGroupOptions, IMergeGroupOptions, IMoveEditorOptions, ICopyEditorOptions, IEditorReplacement, IGroupChangeEvent, IFindGroupScope, EditorGroupLayout, ICloseEditorOptions, GroupOrientation } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorService, IOpenEditorOverrideHandler, ISaveEditorsOptions, IRevertAllEditorsOptions, IResourceEditor, SIDE_GROUP_TYPE, ACTIVE_GROUP_TYPE } from 'vs/workbench/services/editor/common/editorService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorRegistry, EditorDescriptor, Extensions } from 'vs/workbench/browser/editor'; -import { IDecorationRenderOptions } from 'vs/editor/common/editorCommon'; import { EditorGroup } from 'vs/workbench/common/editor/editorGroup'; import { Dimension } from 'vs/base/browser/dom'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; @@ -65,7 +63,7 @@ import { timeout } from 'vs/base/common/async'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { ViewletDescriptor, Viewlet } from 'vs/workbench/browser/viewlet'; import { IViewlet } from 'vs/workbench/common/viewlet'; -import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { isLinux } from 'vs/base/common/platform'; import { LabelService } from 'vs/workbench/services/label/common/labelService'; import { IDimension } from 'vs/platform/layout/browser/layoutService'; @@ -99,11 +97,15 @@ import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { CancellationToken } from 'vs/base/common/cancellation'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; +import { CodeEditorService } from 'vs/workbench/services/editor/browser/codeEditorService'; +import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; export import TestTextResourcePropertiesService = CommonWorkbenchTestServices.TestTextResourcePropertiesService; export import TestContextService = CommonWorkbenchTestServices.TestContextService; export import TestStorageService = CommonWorkbenchTestServices.TestStorageService; export import TestWorkingCopyService = CommonWorkbenchTestServices.TestWorkingCopyService; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IDiffEditor } from 'vs/editor/common/editorCommon'; export function createFileInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { return instantiationService.createInstance(FileEditorInput, resource, undefined, undefined); @@ -151,14 +153,15 @@ export function workbenchInstantiationService(overrides?: { textFileService?: (i instantiationService.stub(ITextFileService, overrides?.textFileService ? overrides.textFileService(instantiationService) : instantiationService.createInstance(TestTextFileService)); instantiationService.stub(IHostService, instantiationService.createInstance(TestHostService)); instantiationService.stub(ITextModelService, instantiationService.createInstance(TextModelResolverService)); - instantiationService.stub(IThemeService, new TestThemeService()); + const themeService = new TestThemeService(); + instantiationService.stub(IThemeService, themeService); instantiationService.stub(ILogService, new NullLogService()); const editorGroupService = new TestEditorGroupsService([new TestEditorGroupView(0)]); instantiationService.stub(IEditorGroupsService, editorGroupService); instantiationService.stub(ILabelService, instantiationService.createInstance(LabelService)); const editorService = new TestEditorService(editorGroupService); instantiationService.stub(IEditorService, editorService); - instantiationService.stub(ICodeEditorService, new TestCodeEditorService()); + instantiationService.stub(ICodeEditorService, new CodeEditorService(editorService, themeService)); instantiationService.stub(IViewletService, new TestViewletService()); return instantiationService; @@ -181,7 +184,8 @@ export class TestServiceAccessor { @ITextModelService public textModelResolverService: ITextModelService, @IUntitledTextEditorService public untitledTextEditorService: UntitledTextEditorService, @IConfigurationService public testConfigurationService: TestConfigurationService, - @IBackupFileService public backupFileService: TestBackupFileService + @IBackupFileService public backupFileService: TestBackupFileService, + @IHostService public hostService: TestHostService ) { } } @@ -467,7 +471,7 @@ export class TestEditorGroupsService implements IEditorGroupsService { onDidLayout: Event = Event.None; onDidEditorPartOptionsChange = Event.None; - orientation: any; + orientation = GroupOrientation.HORIZONTAL; whenRestored: Promise = Promise.resolve(undefined); willRestoreEditors = false; @@ -486,7 +490,7 @@ export class TestEditorGroupsService implements IEditorGroupsService { setSize(_group: number | IEditorGroup, _size: { width: number, height: number }): void { } arrangeGroups(_arrangement: GroupsArrangement): void { } applyLayout(_layout: EditorGroupLayout): void { } - setGroupOrientation(_orientation: any): void { } + setGroupOrientation(_orientation: GroupOrientation): void { } addGroup(_location: number | IEditorGroup, _direction: GroupDirection, _options?: IAddGroupOptions): IEditorGroup { throw new Error('not implemented'); } removeGroup(_group: number | IEditorGroup): void { } moveGroup(_group: number | IEditorGroup, _location: number | IEditorGroup, _direction: GroupDirection): IEditorGroup { throw new Error('not implemented'); } @@ -589,10 +593,10 @@ export class TestEditorService implements EditorServiceImpl { onDidOpenEditorFail: Event = Event.None; onDidMostRecentlyActiveEditorsChange: Event = Event.None; - activeControl!: IVisibleEditor; - activeTextEditorWidget: any; - activeTextEditorMode: any; - activeEditor!: IEditorInput; + activeControl: IVisibleEditor | undefined; + activeTextEditorWidget: ICodeEditor | IDiffEditor | undefined; + activeTextEditorMode: string | undefined; + activeEditor: IEditorInput | undefined; editors: ReadonlyArray = []; mostRecentlyActiveEditors: ReadonlyArray = []; visibleControls: ReadonlyArray = []; @@ -604,7 +608,13 @@ export class TestEditorService implements EditorServiceImpl { getEditors() { return []; } overrideOpenEditor(_handler: IOpenEditorOverrideHandler): IDisposable { return toDisposable(() => undefined); } - openEditor(_editor: any, _options?: any, _group?: any): Promise { throw new Error('not implemented'); } + openEditor(editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; + openEditor(editor: IResourceInput | IUntitledTextResourceInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; + openEditor(editor: IResourceDiffInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; + openEditor(editor: IResourceSideBySideInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise; + async openEditor(editor: IEditorInput | IResourceEditor, optionsOrGroup?: IEditorOptions | ITextEditorOptions | IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise { + throw new Error('not implemented'); + } doResolveEditorOpenRequest(editor: IEditorInput | IResourceEditor): [IEditorGroup, EditorInput, EditorOptions | undefined] | undefined { if (!this.editorGroupService) { return undefined; @@ -613,15 +623,14 @@ export class TestEditorService implements EditorServiceImpl { return [this.editorGroupService.activeGroup, editor as EditorInput, undefined]; } openEditors(_editors: any, _group?: any): Promise { throw new Error('not implemented'); } - isOpen(_editor: IEditorInput | IResourceInput | IUntitledTextResourceInput): boolean { return false; } - getOpened(_editor: IEditorInput | IResourceInput | IUntitledTextResourceInput): IEditorInput { throw new Error('not implemented'); } + isOpen(_editor: IEditorInput | IResourceInput): boolean { return false; } replaceEditors(_editors: any, _group: any) { return Promise.resolve(undefined); } invokeWithinEditorContext(fn: (accessor: ServicesAccessor) => T): T { throw new Error('not implemented'); } createInput(_input: IResourceInput | IUntitledTextResourceInput | IResourceDiffInput | IResourceSideBySideInput): EditorInput { throw new Error('not implemented'); } save(editors: IEditorIdentifier[], options?: ISaveEditorsOptions): Promise { throw new Error('Method not implemented.'); } saveAll(options?: ISaveEditorsOptions): Promise { throw new Error('Method not implemented.'); } - revert(editors: IEditorIdentifier[], options?: IRevertOptions): Promise { throw new Error('Method not implemented.'); } - revertAll(options?: IRevertAllEditorsOptions): Promise { throw new Error('Method not implemented.'); } + revert(editors: IEditorIdentifier[], options?: IRevertOptions): Promise { throw new Error('Method not implemented.'); } + revertAll(options?: IRevertAllEditorsOptions): Promise { throw new Error('Method not implemented.'); } } export class TestFileService implements IFileService { @@ -791,32 +800,6 @@ export class TestBackupFileService implements IBackupFileService { } } -export class TestCodeEditorService implements ICodeEditorService { - _serviceBrand: undefined; - - onCodeEditorAdd: Event = Event.None; - onCodeEditorRemove: Event = Event.None; - onDiffEditorAdd: Event = Event.None; - onDiffEditorRemove: Event = Event.None; - onDidChangeTransientModelProperty: Event = Event.None; - - addCodeEditor(_editor: ICodeEditor): void { } - removeCodeEditor(_editor: ICodeEditor): void { } - listCodeEditors(): ICodeEditor[] { return []; } - addDiffEditor(_editor: IDiffEditor): void { } - removeDiffEditor(_editor: IDiffEditor): void { } - listDiffEditors(): IDiffEditor[] { return []; } - getFocusedCodeEditor(): ICodeEditor | null { return null; } - registerDecorationType(_key: string, _options: IDecorationRenderOptions, _parentTypeKey?: string): void { } - removeDecorationType(_key: string): void { } - resolveDecorationOptions(_typeKey: string, _writable: boolean): IModelDecorationOptions { return Object.create(null); } - setTransientModelProperty(_model: ITextModel, _key: string, _value: any): void { } - getTransientModelProperty(_model: ITextModel, _key: string) { } - getTransientModelProperties(_model: ITextModel) { return undefined; } - getActiveCodeEditor(): ICodeEditor | null { return null; } - openCodeEditor(_input: IResourceInput, _source: ICodeEditor, _sideBySide?: boolean): Promise { return Promise.resolve(null); } -} - export class TestLifecycleService implements ILifecycleService { _serviceBrand: undefined; @@ -906,9 +889,17 @@ export class TestHostService implements IHostService { _serviceBrand: undefined; - readonly hasFocus: boolean = true; - async hadLastFocus(): Promise { return true; } - readonly onDidChangeFocus: Event = Event.None; + private _hasFocus = true; + get hasFocus() { return this._hasFocus; } + async hadLastFocus(): Promise { return this._hasFocus; } + + private _onDidChangeFocus = new Emitter(); + readonly onDidChangeFocus = this._onDidChangeFocus.event; + + setFocus(focus: boolean) { + this._hasFocus = focus; + this._onDidChangeFocus.fire(this._hasFocus); + } async restart(): Promise { } async reload(): Promise { } @@ -962,7 +953,7 @@ export function registerTestEditor(id: string, inputs: SyncDescriptor { + async revert(group: GroupIdentifier, options?: IRevertOptions): Promise { this.gotReverted = true; this.gotSaved = false; this.gotSavedAs = false; - return true; } setDirty(): void { this.dirty = true; } isDirty(): boolean { @@ -1059,3 +1049,22 @@ export class TestFileEditorInput extends EditorInput implements IFileEditorInput movedEditor: IMoveResult | undefined = undefined; move(): IMoveResult | undefined { return this.movedEditor; } } + +export class TestEditorPart extends EditorPart { + + saveState(): void { + return super.saveState(); + } + + clearState(): void { + const workspaceMemento = this.getMemento(StorageScope.WORKSPACE); + for (const key of Object.keys(workspaceMemento)) { + delete workspaceMemento[key]; + } + + const globalMemento = this.getMemento(StorageScope.GLOBAL); + for (const key of Object.keys(globalMemento)) { + delete globalMemento[key]; + } + } +} diff --git a/src/vs/workbench/test/common/notifications.test.ts b/src/vs/workbench/test/common/notifications.test.ts index 8984e0ab869..765c140dc60 100644 --- a/src/vs/workbench/test/common/notifications.test.ts +++ b/src/vs/workbench/test/common/notifications.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { NotificationsModel, NotificationViewItem, INotificationChangeEvent, NotificationChangeType, NotificationViewItemLabelKind, IStatusMessageChangeEvent, StatusMessageChangeType } from 'vs/workbench/common/notifications'; +import { NotificationsModel, NotificationViewItem, INotificationChangeEvent, NotificationChangeType, NotificationViewItemContentChangeKind, IStatusMessageChangeEvent, StatusMessageChangeType } from 'vs/workbench/common/notifications'; import { Action } from 'vs/base/common/actions'; import { INotification, Severity, NotificationsFilter } from 'vs/platform/notification/common/notification'; import { createErrorWithActions } from 'vs/base/common/errorsWithActions'; @@ -23,6 +23,7 @@ suite('Notifications', () => { let item3 = NotificationViewItem.create({ severity: Severity.Info, message: 'Info Message' })!; let item4 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message', source: 'Source' })!; let item5 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message', actions: { primary: [new Action('id', 'label')] } })!; + let item6 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message', actions: { primary: [new Action('id', 'label')] }, progress: { infinite: true } })!; assert.equal(item1.equals(item1), true); assert.equal(item2.equals(item2), true); @@ -35,6 +36,10 @@ suite('Notifications', () => { assert.equal(item1.equals(item4), false); assert.equal(item1.equals(item5), false); + // Progress + assert.equal(item1.hasProgress, false); + assert.equal(item6.hasProgress, true); + // Message Box assert.equal(item5.canCollapse, false); assert.equal(item5.expanded, true); @@ -53,8 +58,8 @@ suite('Notifications', () => { assert.equal(called, 2); called = 0; - item1.onDidChangeLabel(e => { - if (e.kind === NotificationViewItemLabelKind.PROGRESS) { + item1.onDidChangeContent(e => { + if (e.kind === NotificationViewItemContentChangeKind.PROGRESS) { called++; } }); @@ -65,8 +70,8 @@ suite('Notifications', () => { assert.equal(called, 2); called = 0; - item1.onDidChangeLabel(e => { - if (e.kind === NotificationViewItemLabelKind.MESSAGE) { + item1.onDidChangeContent(e => { + if (e.kind === NotificationViewItemContentChangeKind.MESSAGE) { called++; } }); @@ -74,8 +79,8 @@ suite('Notifications', () => { item1.updateMessage('message update'); called = 0; - item1.onDidChangeLabel(e => { - if (e.kind === NotificationViewItemLabelKind.SEVERITY) { + item1.onDidChangeContent(e => { + if (e.kind === NotificationViewItemContentChangeKind.SEVERITY) { called++; } }); @@ -83,8 +88,8 @@ suite('Notifications', () => { item1.updateSeverity(Severity.Error); called = 0; - item1.onDidChangeLabel(e => { - if (e.kind === NotificationViewItemLabelKind.ACTIONS) { + item1.onDidChangeContent(e => { + if (e.kind === NotificationViewItemContentChangeKind.ACTIONS) { called++; } }); @@ -93,6 +98,17 @@ suite('Notifications', () => { assert.equal(called, 1); + called = 0; + item1.onDidChangeVisibility(e => { + called++; + }); + + item1.updateVisibility(true); + item1.updateVisibility(false); + item1.updateVisibility(false); + + assert.equal(called, 2); + called = 0; item1.onDidClose(() => { called++; @@ -102,8 +118,8 @@ suite('Notifications', () => { assert.equal(called, 1); // Error with Action - let item6 = NotificationViewItem.create({ severity: Severity.Error, message: createErrorWithActions('Hello Error', { actions: [new Action('id', 'label')] }) })!; - assert.equal(item6.actions!.primary!.length, 1); + let item7 = NotificationViewItem.create({ severity: Severity.Error, message: createErrorWithActions('Hello Error', { actions: [new Action('id', 'label')] }) })!; + assert.equal(item7.actions!.primary!.length, 1); // Filter let item8 = NotificationViewItem.create({ severity: Severity.Error, message: 'Error Message' }, NotificationsFilter.SILENT)!; @@ -143,6 +159,22 @@ suite('Notifications', () => { assert.equal(lastNotificationEvent.index, 0); assert.equal(lastNotificationEvent.kind, NotificationChangeType.ADD); + item1Handle.updateMessage('Error Message'); + assert.equal(lastNotificationEvent.kind, NotificationChangeType.CHANGE); + assert.equal(lastNotificationEvent.detail, NotificationViewItemContentChangeKind.MESSAGE); + + item1Handle.updateSeverity(Severity.Error); + assert.equal(lastNotificationEvent.kind, NotificationChangeType.CHANGE); + assert.equal(lastNotificationEvent.detail, NotificationViewItemContentChangeKind.SEVERITY); + + item1Handle.updateActions({ primary: [], secondary: [] }); + assert.equal(lastNotificationEvent.kind, NotificationChangeType.CHANGE); + assert.equal(lastNotificationEvent.detail, NotificationViewItemContentChangeKind.ACTIONS); + + item1Handle.progress.infinite(); + assert.equal(lastNotificationEvent.kind, NotificationChangeType.CHANGE); + assert.equal(lastNotificationEvent.detail, NotificationViewItemContentChangeKind.PROGRESS); + let item2Handle = model.addNotification(item2); assert.equal(lastNotificationEvent.item.severity, item2.severity); assert.equal(lastNotificationEvent.item.message.linkedText.toString(), item2.message); @@ -188,7 +220,7 @@ suite('Notifications', () => { assert.equal(lastNotificationEvent.item.severity, item3.severity); assert.equal(lastNotificationEvent.item.message.linkedText.toString(), item3.message); assert.equal(lastNotificationEvent.index, 0); - assert.equal(lastNotificationEvent.kind, NotificationChangeType.CHANGE); + assert.equal(lastNotificationEvent.kind, NotificationChangeType.EXPAND_COLLAPSE); const disposable = model.showStatusMessage('Hello World'); assert.equal(model.statusMessage!.message, 'Hello World'); diff --git a/src/vs/workbench/test/common/workbenchTestServices.ts b/src/vs/workbench/test/common/workbenchTestServices.ts index dfe948f1797..82de69c0655 100644 --- a/src/vs/workbench/test/common/workbenchTestServices.ts +++ b/src/vs/workbench/test/common/workbenchTestServices.ts @@ -35,14 +35,20 @@ export class TestTextResourcePropertiesService implements ITextResourcePropertie } export class TestContextService implements IWorkspaceContextService { + _serviceBrand: undefined; private workspace: Workspace; private options: any; private readonly _onDidChangeWorkspaceName: Emitter; + get onDidChangeWorkspaceName(): Event { return this._onDidChangeWorkspaceName.event; } + private readonly _onDidChangeWorkspaceFolders: Emitter; + get onDidChangeWorkspaceFolders(): Event { return this._onDidChangeWorkspaceFolders.event; } + private readonly _onDidChangeWorkbenchState: Emitter; + get onDidChangeWorkbenchState(): Event { return this._onDidChangeWorkbenchState.event; } constructor(workspace: any = TestWorkspace, options: any = null) { this.workspace = workspace; @@ -52,18 +58,6 @@ export class TestContextService implements IWorkspaceContextService { this._onDidChangeWorkbenchState = new Emitter(); } - get onDidChangeWorkspaceName(): Event { - return this._onDidChangeWorkspaceName.event; - } - - get onDidChangeWorkspaceFolders(): Event { - return this._onDidChangeWorkspaceFolders.event; - } - - get onDidChangeWorkbenchState(): Event { - return this._onDidChangeWorkbenchState.event; - } - getFolders(): IWorkspaceFolder[] { return this.workspace ? this.workspace.folders : []; } @@ -100,9 +94,7 @@ export class TestContextService implements IWorkspaceContextService { return this.options; } - updateOptions() { - - } + updateOptions() { } isInsideWorkspace(resource: URI): boolean { if (resource && this.workspace) { diff --git a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts index 929d1d748ad..c08bf8098f9 100644 --- a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts @@ -6,7 +6,7 @@ import { workbenchInstantiationService as browserWorkbenchInstantiationService, ITestInstantiationService, TestLifecycleService, TestFilesConfigurationService, TestContextService, TestFileService, TestFileDialogService } from 'vs/workbench/test/browser/workbenchTestServices'; import { Event } from 'vs/base/common/event'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; -import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; +import { NativeWorkbenchEnvironmentService, INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; import { NativeTextFileService, EncodingOracle, IEncodingOverride } from 'vs/workbench/services/textfile/electron-browser/nativeTextFileService'; import { IElectronService } from 'vs/platform/electron/node/electron'; import { INativeOpenDialogOptions } from 'vs/platform/dialogs/node/dialogs'; @@ -25,7 +25,7 @@ import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService import { URI } from 'vs/base/common/uri'; import { IReadTextFileOptions, ITextFileStreamContent, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel'; -import { IOpenedWindow, IOpenEmptyWindowOptions, IWindowOpenable, IOpenWindowOptions, IWindowConfiguration } from 'vs/platform/windows/common/windows'; +import { IOpenedWindow, IOpenEmptyWindowOptions, IWindowOpenable, IOpenWindowOptions } from 'vs/platform/windows/common/windows'; import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv'; import { LogLevel } from 'vs/platform/log/common/log'; import { IRemotePathService } from 'vs/workbench/services/path/common/remotePathService'; @@ -37,12 +37,15 @@ import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { NodeTestBackupFileService } from 'vs/workbench/services/backup/test/electron-browser/backupFileService.test'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { INativeWindowConfiguration } from 'vs/platform/windows/node/window'; -export const TestWindowConfiguration: IWindowConfiguration = { +export const TestWindowConfiguration: INativeWindowConfiguration = { windowId: 0, + machineId: 'testMachineId', sessionId: 'testSessionId', logLevel: LogLevel.Error, mainPid: 0, + partsSplashPath: '', appRoot: '', userEnv: {}, execPath: process.execPath, @@ -61,7 +64,7 @@ export class TestTextFileService extends NativeTextFileService { @ILifecycleService lifecycleService: ILifecycleService, @IInstantiationService instantiationService: IInstantiationService, @IModelService modelService: IModelService, - @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IWorkbenchEnvironmentService environmentService: INativeWorkbenchEnvironmentService, @IDialogService dialogService: IDialogService, @IFileDialogService fileDialogService: IFileDialogService, @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index fdf7e6ae5db..8393a631a59 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -43,7 +43,7 @@ import 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl'; import 'vs/workbench/services/telemetry/electron-browser/telemetryService'; import 'vs/workbench/services/configurationResolver/electron-browser/configurationResolverService'; import 'vs/workbench/services/extensionManagement/node/extensionManagementService'; -import 'vs/workbench/services/accessibility/node/accessibilityService'; +import 'vs/workbench/services/accessibility/electron-browser/accessibilityService'; import 'vs/workbench/services/remote/node/tunnelService'; import 'vs/workbench/services/backup/node/backupFileService'; import 'vs/workbench/services/url/electron-browser/urlService'; diff --git a/src/vs/workbench/workbench.web.api.ts b/src/vs/workbench/workbench.web.api.ts index 140711b300e..9f18e7bf59d 100644 --- a/src/vs/workbench/workbench.web.api.ts +++ b/src/vs/workbench/workbench.web.api.ts @@ -238,7 +238,7 @@ async function create(domElement: HTMLElement, options: IWorkbenchConstructionOp // Register commands if any if (Array.isArray(options.commands)) { for (const command of options.commands) { - CommandsRegistry.registerCommand(command.id, (accessor, ...args: any[]) => { + CommandsRegistry.registerCommand(command.id, (accessor, ...args) => { // we currently only pass on the arguments but not the accessor // to the command to reduce our exposure of internal API. command.handler(...args); diff --git a/test/automation/package.json b/test/automation/package.json index a3f3baf2f40..8fced3ba824 100644 --- a/test/automation/package.json +++ b/test/automation/package.json @@ -28,7 +28,8 @@ "concurrently": "^3.5.1", "cpx": "^1.5.0", "typescript": "3.7.5", - "watch": "^1.0.2" + "watch": "^1.0.2", + "tree-kill": "1.2.2" }, "dependencies": { "mkdirp": "^0.5.1", diff --git a/test/automation/src/code.ts b/test/automation/src/code.ts index 3b5a667f81f..34bd09b739e 100644 --- a/test/automation/src/code.ts +++ b/test/automation/src/code.ts @@ -115,9 +115,6 @@ async function createDriverHandle(): Promise { } export async function spawn(options: SpawnOptions): Promise { - const codePath = options.codePath; - const electronPath = codePath ? getBuildElectronPath(codePath) : getDevElectronPath(); - const outPath = codePath ? getBuildOutPath(codePath) : getDevOutPath(); const handle = await createDriverHandle(); let child: cp.ChildProcess | undefined; @@ -126,63 +123,67 @@ export async function spawn(options: SpawnOptions): Promise { if (options.web) { await launch(options.userDataDir, options.workspacePath, options.codePath); connectDriver = connectPlaywrightDriver.bind(connectPlaywrightDriver, options.browser); - } else { - const env = process.env; - - const args = [ - options.workspacePath, - '--skip-getting-started', - '--skip-release-notes', - '--sticky-quickopen', - '--disable-telemetry', - '--disable-updates', - '--disable-crash-reporter', - `--extensions-dir=${options.extensionsPath}`, - `--user-data-dir=${options.userDataDir}`, - '--driver', handle - ]; - - if (options.remote) { - // Replace workspace path with URI - args[0] = `--${options.workspacePath.endsWith('.code-workspace') ? 'file' : 'folder'}-uri=vscode-remote://test+test/${URI.file(options.workspacePath).path}`; - - if (codePath) { - // running against a build: copy the test resolver extension - const testResolverExtPath = path.join(options.extensionsPath, 'vscode-test-resolver'); - if (!fs.existsSync(testResolverExtPath)) { - const orig = path.join(repoPath, 'extensions', 'vscode-test-resolver'); - await new Promise((c, e) => ncp(orig, testResolverExtPath, err => err ? e(err) : c())); - } - } - args.push('--enable-proposed-api=vscode.vscode-test-resolver'); - const remoteDataDir = `${options.userDataDir}-server`; - mkdirp.sync(remoteDataDir); - env['TESTRESOLVER_DATA_FOLDER'] = remoteDataDir; - } - - if (!codePath) { - args.unshift(repoPath); - } - - if (options.verbose) { - args.push('--driver-verbose'); - } - - if (options.log) { - args.push('--log', options.log); - } - - if (options.extraArgs) { - args.push(...options.extraArgs); - } - - const spawnOptions: cp.SpawnOptions = { env }; - child = cp.spawn(electronPath, args, spawnOptions); - instances.add(child); - child.once('exit', () => instances.delete(child!)); - connectDriver = connectElectronDriver; + return connect(connectDriver, child, '', handle, options.logger); } + const env = process.env; + const codePath = options.codePath; + const outPath = codePath ? getBuildOutPath(codePath) : getDevOutPath(); + + const args = [ + options.workspacePath, + '--skip-getting-started', + '--skip-release-notes', + '--sticky-quickopen', + '--disable-telemetry', + '--disable-updates', + '--disable-crash-reporter', + `--extensions-dir=${options.extensionsPath}`, + `--user-data-dir=${options.userDataDir}`, + `--disable-restore-windows`, + '--driver', handle + ]; + + if (options.remote) { + // Replace workspace path with URI + args[0] = `--${options.workspacePath.endsWith('.code-workspace') ? 'file' : 'folder'}-uri=vscode-remote://test+test/${URI.file(options.workspacePath).path}`; + + if (codePath) { + // running against a build: copy the test resolver extension + const testResolverExtPath = path.join(options.extensionsPath, 'vscode-test-resolver'); + if (!fs.existsSync(testResolverExtPath)) { + const orig = path.join(repoPath, 'extensions', 'vscode-test-resolver'); + await new Promise((c, e) => ncp(orig, testResolverExtPath, err => err ? e(err) : c())); + } + } + args.push('--enable-proposed-api=vscode.vscode-test-resolver'); + const remoteDataDir = `${options.userDataDir}-server`; + mkdirp.sync(remoteDataDir); + env['TESTRESOLVER_DATA_FOLDER'] = remoteDataDir; + } + + if (!codePath) { + args.unshift(repoPath); + } + + if (options.verbose) { + args.push('--driver-verbose'); + } + + if (options.log) { + args.push('--log', options.log); + } + + if (options.extraArgs) { + args.push(...options.extraArgs); + } + + const electronPath = codePath ? getBuildElectronPath(codePath) : getDevElectronPath(); + const spawnOptions: cp.SpawnOptions = { env }; + child = cp.spawn(electronPath, args, spawnOptions); + instances.add(child); + child.once('exit', () => instances.delete(child!)); + connectDriver = connectElectronDriver; return connect(connectDriver, child, outPath, handle, options.logger); } diff --git a/test/automation/src/playwrightDriver.ts b/test/automation/src/playwrightDriver.ts index 40ee4c82851..cc0c5547db3 100644 --- a/test/automation/src/playwrightDriver.ts +++ b/test/automation/src/playwrightDriver.ts @@ -10,6 +10,7 @@ import { mkdir } from 'fs'; import { promisify } from 'util'; import { IDriver, IDisposable } from './driver'; import { URI } from 'vscode-uri'; +import * as kill from 'tree-kill'; const width = 1200; const height = 800; @@ -93,6 +94,7 @@ let workspacePath: string | undefined; export async function launch(userDataDir: string, _workspacePath: string, codeServerPath = process.env.VSCODE_REMOTE_SERVER_PATH): Promise { workspacePath = _workspacePath; + const agentFolder = userDataDir; await promisify(mkdir)(agentFolder); const env = { @@ -121,7 +123,7 @@ export async function launch(userDataDir: string, _workspacePath: string, codeSe function teardown(): void { if (server) { - server.kill(); + kill(server.pid); server = undefined; } } @@ -137,13 +139,9 @@ function waitForEndpoint(): Promise { }); } -export function connect(engine: 'chromium' | 'webkit' | 'firefox' = 'chromium'): Promise<{ client: IDisposable, driver: IDriver }> { +export function connect(browserType: 'chromium' | 'webkit' | 'firefox' = 'chromium'): Promise<{ client: IDisposable, driver: IDriver }> { return new Promise(async (c) => { - const browser = await playwright[engine].launch({ - // Run in Edge dev on macOS - // executablePath: '/Applications/Microsoft\ Edge\ Dev.app/Contents/MacOS/Microsoft\ Edge\ Dev', - headless: false - }); + const browser = await playwright[browserType].launch({ headless: false, dumpio: true }); const context = await browser.newContext(); const page = await context.newPage(); await page.setViewportSize({ width, height }); diff --git a/test/automation/yarn.lock b/test/automation/yarn.lock index 98c63c91e0d..11785373d40 100644 --- a/test/automation/yarn.lock +++ b/test/automation/yarn.lock @@ -1634,6 +1634,11 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" +tree-kill@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" + integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== + tree-kill@^1.1.0: version "1.2.1" resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.1.tgz#5398f374e2f292b9dcc7b2e71e30a5c3bb6c743a" diff --git a/test/smoke/README.md b/test/smoke/README.md index 8b41d05ed08..7bec799f89d 100644 --- a/test/smoke/README.md +++ b/test/smoke/README.md @@ -13,13 +13,13 @@ yarn --cwd test/automation yarn smoketest # Dev (Web) -yarn smoketest --web --browser [chromium|firefox|webkit] +yarn smoketest --web --browser [chromium|webkit] # Build (Electron) yarn smoketest --build --stable-build # Build (Web - read instructions below) -yarn smoketest --build --web --browser [chromium|firefox|webkit] +yarn smoketest --build --web --browser [chromium|webkit] # Remote (Electron) yarn smoketest --build --remote diff --git a/test/smoke/src/areas/css/css.test.ts b/test/smoke/src/areas/languages/languages.test.ts similarity index 89% rename from test/smoke/src/areas/css/css.test.ts rename to test/smoke/src/areas/languages/languages.test.ts index aa6ccb9590b..0985a614a9e 100644 --- a/test/smoke/src/areas/css/css.test.ts +++ b/test/smoke/src/areas/languages/languages.test.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Application, ProblemSeverity, Problems } from '../../../../automation'; +import { Application, ProblemSeverity, Problems } from '../../../../automation/out'; export function setup() { - describe('CSS', () => { + describe('Language Features', () => { it('verifies quick outline', async function () { const app = this.app as Application; await app.workbench.quickopen.openFile('style.css'); @@ -15,7 +15,7 @@ export function setup() { await app.workbench.quickopen.waitForQuickOpenElements(names => names.length === 2); }); - it('verifies warnings for the empty rule', async function () { + it('verifies problems view', async function () { const app = this.app as Application; await app.workbench.quickopen.openFile('style.css'); await app.workbench.editor.waitForTypeInEditor('style.css', '.foo{}'); @@ -27,7 +27,7 @@ export function setup() { await app.workbench.problems.hideProblemsView(); }); - it('verifies that warning becomes an error once setting changed', async function () { + it('verifies settings', async function () { const app = this.app as Application; await app.workbench.settingsEditor.addUserSetting('css.lint.emptyRules', '"error"'); await app.workbench.quickopen.openFile('style.css'); diff --git a/test/smoke/src/areas/workbench/localization.test.ts b/test/smoke/src/areas/workbench/localization.test.ts index 5db5fe2b74a..07e9199080e 100644 --- a/test/smoke/src/areas/workbench/localization.test.ts +++ b/test/smoke/src/areas/workbench/localization.test.ts @@ -37,10 +37,10 @@ export function setup() { await app.workbench.scm.waitForTitle(title => /quellcodeverwaltung/i.test(title)); await app.workbench.debug.openDebugViewlet(); - await app.workbench.debug.waitForTitle(title => /debug/i.test(title)); + await app.workbench.debug.waitForTitle(title => /starten/i.test(title)); - // await app.workbench.extensions.openExtensionsViewlet(); - // await app.workbench.extensions.waitForTitle(title => /erweiterungen/i.test(title)); + await app.workbench.extensions.openExtensionsViewlet(); + await app.workbench.extensions.waitForTitle(title => /extensions/i.test(title)); }); }); } diff --git a/test/smoke/src/main.ts b/test/smoke/src/main.ts index 8122d235289..5a1dd9f11cc 100644 --- a/test/smoke/src/main.ts +++ b/test/smoke/src/main.ts @@ -25,7 +25,7 @@ import { setup as setupDataMigrationTests } from './areas/workbench/data-migrati import { setup as setupDataLossTests } from './areas/workbench/data-loss.test'; import { setup as setupDataPreferencesTests } from './areas/preferences/preferences.test'; import { setup as setupDataSearchTests } from './areas/search/search.test'; -import { setup as setupDataCSSTests } from './areas/css/css.test'; +import { setup as setupDataLanguagesTests } from './areas/languages/languages.test'; import { setup as setupDataEditorTests } from './areas/editor/editor.test'; import { setup as setupDataStatusbarTests } from './areas/statusbar/statusbar.test'; import { setup as setupDataExtensionTests } from './areas/extensions/extensions.test'; @@ -57,8 +57,7 @@ const opts = minimist(args, { boolean: [ 'verbose', 'remote', - 'web', - 'ci' + 'web' ], default: { verbose: false @@ -299,24 +298,16 @@ describe(`VSCode Smoke Tests (${opts.web ? 'Web' : 'Electron'})`, () => { }); } - // CI only tests (must be reliable) - if (opts.ci) { - // TODO@Ben figure out tests that can run continously and reliably - } - - // Non-CI execution (all tests) - else { - if (!opts.web) { setupDataMigrationTests(opts['stable-build'], testDataPath); } - if (!opts.web) { setupDataLossTests(); } - if (!opts.web) { setupDataPreferencesTests(); } - setupDataSearchTests(); - setupDataCSSTests(); - setupDataEditorTests(); - setupDataStatusbarTests(!!opts.web); - if (!opts.web) { setupDataExtensionTests(); } - setupTerminalTests(); - if (!opts.web) { setupDataMultirootTests(); } - if (!opts.web) { setupDataLocalizationTests(); } - if (!opts.web) { setupLaunchTests(); } - } + if (!opts.web) { setupDataMigrationTests(opts['stable-build'], testDataPath); } + if (!opts.web) { setupDataLossTests(); } + if (!opts.web) { setupDataPreferencesTests(); } + setupDataSearchTests(); + setupDataLanguagesTests(); + setupDataEditorTests(); + setupDataStatusbarTests(!!opts.web); + if (!opts.web) { setupDataExtensionTests(); } + setupTerminalTests(); + if (!opts.web) { setupDataMultirootTests(); } + if (!opts.web) { setupDataLocalizationTests(); } + if (!opts.web) { setupLaunchTests(); } }); diff --git a/test/unit/README.md b/test/unit/README.md index 4172285834f..f471ebb7d8e 100644 --- a/test/unit/README.md +++ b/test/unit/README.md @@ -9,6 +9,8 @@ All unit tests are run inside a electron-browser environment which access to DOM - use the `--debug` to see an electron window with dev tools which allows for debugging - to run only a subset of tests use the `--run` or `--glob` options +For instance, `./scripts/test.sh --debug --glob **/extHost*.test.js` runs all tests from `extHost`-files and enables you to debug them. + ## Run (inside browser) yarn test-browser --browser webkit --browser chromium @@ -24,11 +26,6 @@ Unit tests from layers `common` and `browser` are run inside `chromium`, `webkit yarn run mocha --run src/vs/editor/test/browser/controller/cursor.test.ts -## Debug - -To debug tests use `--debug` when running the test script. Also, the set of tests can be reduced with the `--run` and `--runGlob` flags. Both require a file path/pattern. Like so: - - ./scripts/test.sh --debug --runGrep **/extHost*.test.js ## Coverage diff --git a/yarn.lock b/yarn.lock index 9050b677d92..a3f8ad9006b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9220,16 +9220,16 @@ typescript-formatter@7.1.0: commandpost "^1.0.0" editorconfig "^0.15.0" -typescript@3.8.2: - version "3.8.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.2.tgz#91d6868aaead7da74f493c553aeff76c0c0b1d5a" - integrity sha512-EgOVgL/4xfVrCMbhYKUQTdF37SQn4Iw73H5BgCrF1Abdun7Kwy/QZsE/ssAy0y4LxBbvua3PIbFsbRczWWnDdQ== - typescript@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.2.tgz#3c5b6fd7f6de0914269027f03c0946758f7673a4" integrity sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q= +typescript@^3.9.0-dev.20200229: + version "3.9.0-dev.20200229" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.0-dev.20200229.tgz#45f0821d5c420a4c7d6d894c64531e1301dfa9bd" + integrity sha512-DtSLzxoiUir0qRc3+JJBxiAe6NvTEM3uDxnPxVWJU6sRDhUi8Ssx6DBjGWCZAQJlLk5A+jk2ptf3JvvZrQlLNQ== + uc.micro@^1.0.1, uc.micro@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.3.tgz#7ed50d5e0f9a9fb0a573379259f2a77458d50192" @@ -9638,10 +9638,10 @@ vsce@1.48.0: yauzl "^2.3.1" yazl "^2.2.2" -vscode-debugprotocol@1.39.0-pre.0: - version "1.39.0-pre.0" - resolved "https://registry.yarnpkg.com/vscode-debugprotocol/-/vscode-debugprotocol-1.39.0-pre.0.tgz#67843631a3c53f2d5282f75ab5996e1408c3958c" - integrity sha512-VpoD8m0gOo2Ag5dEpNT9sAI6BBxIyCxEk2dhGIBegxnlOuiB1SVxMgo1tmsvNYcRCpf9eng27kZ6d6FGoPpIAg== +vscode-debugprotocol@1.39.0: + version "1.39.0" + resolved "https://registry.yarnpkg.com/vscode-debugprotocol/-/vscode-debugprotocol-1.39.0.tgz#0c639178d0d5ea7de7903b6478b53d2bc0d77461" + integrity sha512-Wkvgtuz90vjtQBcvw9Z+BYa4dA6W+sHwHMpqvJVNmwWSuT3JZdl0XDhZNLqtMXkVF4okxtAe0MmbupPSt+gnAQ== vscode-minimist@^1.2.2: version "1.2.2"