diff --git a/build/gulpfile.hygiene.js b/build/gulpfile.hygiene.js index b49bc069366..f6594804322 100644 --- a/build/gulpfile.hygiene.js +++ b/build/gulpfile.hygiene.js @@ -119,7 +119,8 @@ const copyrightFilter = [ '!resources/completions/**', '!extensions/markdown-language-features/media/highlight.css', '!extensions/html-language-features/server/src/modes/typescript/*', - '!extensions/*/server/bin/*' + '!extensions/*/server/bin/*', + '!src/vs/editor/test/node/classification/typescript-test.ts', ]; const eslintFilter = [ diff --git a/build/gulpfile.reh.js b/build/gulpfile.reh.js index 8784602db3e..7085c4f8f50 100644 --- a/build/gulpfile.reh.js +++ b/build/gulpfile.reh.js @@ -17,7 +17,7 @@ const gunzip = require('gulp-gunzip'); const untar = require('gulp-untar'); const File = require('vinyl'); const fs = require('fs'); -const remote = require('gulp-remote-src'); +const remote = require('gulp-remote-retry-src'); const rename = require('gulp-rename'); const filter = require('gulp-filter'); const cp = require('child_process'); diff --git a/build/lib/extensions.js b/build/lib/extensions.js index a73f8cf5864..73d2c7acef3 100644 --- a/build/lib/extensions.js +++ b/build/lib/extensions.js @@ -13,7 +13,7 @@ const File = require("vinyl"); const vsce = require("vsce"); const stats_1 = require("./stats"); const util2 = require("./util"); -const remote = require("gulp-remote-src"); +const remote = require("gulp-remote-retry-src"); const vzip = require('gulp-vinyl-zip'); const filter = require("gulp-filter"); const rename = require("gulp-rename"); diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index 94bc81c15df..4b185aff681 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -13,7 +13,7 @@ import * as File from 'vinyl'; import * as vsce from 'vsce'; import { createStatsStream } from './stats'; import * as util2 from './util'; -import remote = require('gulp-remote-src'); +import remote = require('gulp-remote-retry-src'); const vzip = require('gulp-vinyl-zip'); import filter = require('gulp-filter'); import rename = require('gulp-rename'); diff --git a/build/lib/typings/gulp-remote-src.d.ts b/build/lib/typings/gulp-remote-src.d.ts index 6ea57f84fe5..ff9026b79bb 100644 --- a/build/lib/typings/gulp-remote-src.d.ts +++ b/build/lib/typings/gulp-remote-src.d.ts @@ -1,4 +1,4 @@ -declare module 'gulp-remote-src' { +declare module 'gulp-remote-retry-src' { import stream = require("stream"); @@ -20,4 +20,4 @@ declare module 'gulp-remote-src' { } export = remote; -} \ No newline at end of file +} diff --git a/extensions/cpp/language-configuration.json b/extensions/cpp/language-configuration.json index fcf971bc11f..d430c4d1518 100644 --- a/extensions/cpp/language-configuration.json +++ b/extensions/cpp/language-configuration.json @@ -13,8 +13,7 @@ { "open": "{", "close": "}" }, { "open": "(", "close": ")" }, { "open": "'", "close": "'", "notIn": ["string", "comment"] }, - { "open": "\"", "close": "\"", "notIn": ["string"] }, - { "open": "/**", "close": " */", "notIn": ["string", "comment"] } + { "open": "\"", "close": "\"", "notIn": ["string"] } ], "surroundingPairs": [ ["{", "}"], @@ -30,4 +29,4 @@ "end": "^\\s*#pragma\\s+endregion\\b" } } -} \ No newline at end of file +} diff --git a/extensions/css-language-features/server/package.json b/extensions/css-language-features/server/package.json index d1913a26acf..c6e8600d2ae 100644 --- a/extensions/css-language-features/server/package.json +++ b/extensions/css-language-features/server/package.json @@ -9,7 +9,7 @@ }, "main": "./out/cssServerMain", "dependencies": { - "vscode-css-languageservice": "^4.0.3-next.0", + "vscode-css-languageservice": "^4.0.3-next.1", "vscode-languageserver": "^5.3.0-next.8" }, "devDependencies": { diff --git a/extensions/css-language-features/server/src/cssServerMain.ts b/extensions/css-language-features/server/src/cssServerMain.ts index f762bde09c4..0bfd886c310 100644 --- a/extensions/css-language-features/server/src/cssServerMain.ts +++ b/extensions/css-language-features/server/src/cssServerMain.ts @@ -121,9 +121,9 @@ connection.onInitialize((params: InitializeParams): InitializeResult => { scopedSettingsSupport = !!getClientCapability('workspace.configuration', false); foldingRangeLimit = getClientCapability('textDocument.foldingRange.rangeLimit', Number.MAX_VALUE); - languageServices.css = getCSSLanguageService({ customDataProviders, fileSystemProvider }); - languageServices.scss = getSCSSLanguageService({ customDataProviders, fileSystemProvider }); - languageServices.less = getLESSLanguageService({ customDataProviders, fileSystemProvider }); + languageServices.css = getCSSLanguageService({ customDataProviders, fileSystemProvider, clientCapabilities: params.capabilities }); + languageServices.scss = getSCSSLanguageService({ customDataProviders, fileSystemProvider, clientCapabilities: params.capabilities }); + languageServices.less = getLESSLanguageService({ customDataProviders, fileSystemProvider, clientCapabilities: params.capabilities }); const capabilities: ServerCapabilities = { // Tell the client that the server works in FULL text document sync mode diff --git a/extensions/css-language-features/server/yarn.lock b/extensions/css-language-features/server/yarn.lock index 3537a3d2ecd..d93eec392c7 100644 --- a/extensions/css-language-features/server/yarn.lock +++ b/extensions/css-language-features/server/yarn.lock @@ -781,10 +781,10 @@ supports-color@^5.3.0: dependencies: has-flag "^3.0.0" -vscode-css-languageservice@^4.0.3-next.0: - version "4.0.3-next.0" - resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-4.0.3-next.0.tgz#ba96894cf2a0c86c744a1274590f27e55ea60f58" - integrity sha512-ku58Y5jDFNfDicv2AAhgu1edgfGcRZPwlKu6EBK2ck/O/Vco7Zy64FDoClJghcYBhJiDs7sy2q/UtQD0IoGbRw== +vscode-css-languageservice@^4.0.3-next.1: + version "4.0.3-next.1" + resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-4.0.3-next.1.tgz#e89d01ce0d79b3e6c2642f5e3ad73cb8160d38d9" + integrity sha512-Zrm5TeraVUJ8vRikWhFt259dQu+WK+Ie3K5UA8BB4kqcanoM+1mcnIt8fPkTXlZLbiEWElrkJ9yuYbDNkufeBg== dependencies: vscode-languageserver-types "^3.15.0-next.2" vscode-nls "^4.1.1" diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 36366ad7d09..196a1bddade 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -519,8 +519,8 @@ class DotGitWatcher implements IFileWatcher { this.transientDisposables.push(upstreamWatcher); upstreamWatcher.event(this.emitter.fire, this.emitter, this.transientDisposables); } catch (err) { - if (env.logLevel <= LogLevel.Info) { - this.outputChannel.appendLine(`Failed to watch ref '${upstreamPath}'. Ref is most likely packed.`); + if (env.logLevel <= LogLevel.Error) { + this.outputChannel.appendLine(`Failed to watch ref '${upstreamPath}', is most likely packed.\n${err.stack || err}`); } } } @@ -651,19 +651,30 @@ export class Repository implements Disposable { const onWorkspaceRepositoryFileChange = filterEvent(onWorkspaceFileChange, uri => isDescendant(repository.root, uri.fsPath)); const onWorkspaceWorkingTreeFileChange = filterEvent(onWorkspaceRepositoryFileChange, uri => !/\/\.git($|\/)/.test(uri.path)); - const dotGitFileWatcher = new DotGitWatcher(this, outputChannel); - this.disposables.push(dotGitFileWatcher); + let onDotGitFileChange: Event; + + try { + const dotGitFileWatcher = new DotGitWatcher(this, outputChannel); + onDotGitFileChange = dotGitFileWatcher.event; + this.disposables.push(dotGitFileWatcher); + } catch (err) { + if (env.logLevel <= LogLevel.Error) { + outputChannel.appendLine(`Failed to watch '${this.dotGit}', reverting to legacy API file watched. Some events might be lost.\n${err.stack || err}`); + } + + onDotGitFileChange = filterEvent(onWorkspaceRepositoryFileChange, uri => /\/\.git($|\/)/.test(uri.path)); + } // FS changes should trigger `git status`: // - any change inside the repository working tree // - any change whithin the first level of the `.git` folder, except the folder itself and `index.lock` - const onFileChange = anyEvent(onWorkspaceWorkingTreeFileChange, dotGitFileWatcher.event); + const onFileChange = anyEvent(onWorkspaceWorkingTreeFileChange, onDotGitFileChange); onFileChange(this.onFileChange, this, this.disposables); // Relevate repository changes should trigger virtual document change events - dotGitFileWatcher.event(this._onDidChangeRepository.fire, this._onDidChangeRepository, this.disposables); + onDotGitFileChange(this._onDidChangeRepository.fire, this._onDidChangeRepository, this.disposables); - this.disposables.push(new FileEventLogger(onWorkspaceWorkingTreeFileChange, dotGitFileWatcher.event, outputChannel)); + this.disposables.push(new FileEventLogger(onWorkspaceWorkingTreeFileChange, onDotGitFileChange, outputChannel)); const root = Uri.file(repository.root); this._sourceControl = scm.createSourceControl('git', 'Git', root); diff --git a/extensions/html-language-features/server/package.json b/extensions/html-language-features/server/package.json index 087fbaec82b..95a97f1110f 100644 --- a/extensions/html-language-features/server/package.json +++ b/extensions/html-language-features/server/package.json @@ -9,8 +9,8 @@ }, "main": "./out/htmlServerMain", "dependencies": { - "vscode-css-languageservice": "^4.0.3-next.0", - "vscode-html-languageservice": "^3.0.3", + "vscode-css-languageservice": "^4.0.3-next.1", + "vscode-html-languageservice": "^3.0.4-next.0", "vscode-languageserver": "^5.3.0-next.8", "vscode-languageserver-types": "3.15.0-next.2", "vscode-nls": "^4.1.1", diff --git a/extensions/html-language-features/server/src/htmlServerMain.ts b/extensions/html-language-features/server/src/htmlServerMain.ts index 7ca71d16760..01b43d9a9ba 100644 --- a/extensions/html-language-features/server/src/htmlServerMain.ts +++ b/extensions/html-language-features/server/src/htmlServerMain.ts @@ -96,7 +96,7 @@ connection.onInitialize((params: InitializeParams): InitializeResult => { get folders() { return workspaceFolders; } }; - languageModes = getLanguageModes(initializationOptions ? initializationOptions.embeddedLanguages : { css: true, javascript: true }, workspace, providers); + languageModes = getLanguageModes(initializationOptions ? initializationOptions.embeddedLanguages : { css: true, javascript: true }, workspace, params.capabilities, providers); documents.onDidClose(e => { languageModes.onDocumentRemoved(e.document); diff --git a/extensions/html-language-features/server/src/modes/cssMode.ts b/extensions/html-language-features/server/src/modes/cssMode.ts index 168c7ceffa0..6e60c32d87c 100644 --- a/extensions/html-language-features/server/src/modes/cssMode.ts +++ b/extensions/html-language-features/server/src/modes/cssMode.ts @@ -5,13 +5,12 @@ import { LanguageModelCache, getLanguageModelCache } from '../languageModelCache'; import { TextDocument, Position, Range, CompletionList } from 'vscode-languageserver-types'; -import { getCSSLanguageService, Stylesheet, FoldingRange } from 'vscode-css-languageservice'; +import { Stylesheet, FoldingRange, LanguageService as CSSLanguageService } from 'vscode-css-languageservice'; import { LanguageMode, Workspace } from './languageModes'; import { HTMLDocumentRegions, CSS_STYLE_RULE } from './embeddedSupport'; import { Color } from 'vscode-languageserver'; -export function getCSSMode(documentRegions: LanguageModelCache, workspace: Workspace): LanguageMode { - let cssLanguageService = getCSSLanguageService(); +export function getCSSMode(cssLanguageService: CSSLanguageService, documentRegions: LanguageModelCache, workspace: Workspace): LanguageMode { let embeddedCSSDocuments = getLanguageModelCache(10, 60, document => documentRegions.get(document).getEmbeddedDocument('css')); let cssStylesheets = getLanguageModelCache(10, 60, document => cssLanguageService.parseStylesheet(document)); diff --git a/extensions/html-language-features/server/src/modes/languageModes.ts b/extensions/html-language-features/server/src/modes/languageModes.ts index b44b2442420..d07e0bd80f6 100644 --- a/extensions/html-language-features/server/src/modes/languageModes.ts +++ b/extensions/html-language-features/server/src/modes/languageModes.ts @@ -3,18 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getLanguageService as getHTMLLanguageService, DocumentContext, IHTMLDataProvider, SelectionRange } from 'vscode-html-languageservice'; -import { - CompletionItem, Location, SignatureHelp, Definition, TextEdit, TextDocument, Diagnostic, DocumentLink, Range, - Hover, DocumentHighlight, CompletionList, Position, FormattingOptions, SymbolInformation, FoldingRange -} from 'vscode-languageserver-types'; -import { ColorInformation, ColorPresentation, Color, WorkspaceFolder } from 'vscode-languageserver'; - +import { getCSSLanguageService } from 'vscode-css-languageservice'; +import { ClientCapabilities, DocumentContext, getLanguageService as getHTMLLanguageService, IHTMLDataProvider, SelectionRange } from 'vscode-html-languageservice'; +import { Color, ColorInformation, ColorPresentation, WorkspaceFolder } from 'vscode-languageserver'; +import { CompletionItem, CompletionList, Definition, Diagnostic, DocumentHighlight, DocumentLink, FoldingRange, FormattingOptions, Hover, Location, Position, Range, SignatureHelp, SymbolInformation, TextDocument, TextEdit } from 'vscode-languageserver-types'; import { getLanguageModelCache, LanguageModelCache } from '../languageModelCache'; -import { getDocumentRegions, HTMLDocumentRegions } from './embeddedSupport'; import { getCSSMode } from './cssMode'; -import { getJavaScriptMode } from './javascriptMode'; +import { getDocumentRegions, HTMLDocumentRegions } from './embeddedSupport'; import { getHTMLMode } from './htmlMode'; +import { getJavaScriptMode } from './javascriptMode'; export { ColorInformation, ColorPresentation, Color }; @@ -66,8 +63,9 @@ export interface LanguageModeRange extends Range { attributeValue?: boolean; } -export function getLanguageModes(supportedLanguages: { [languageId: string]: boolean; }, workspace: Workspace, customDataProviders?: IHTMLDataProvider[]): LanguageModes { - const htmlLanguageService = getHTMLLanguageService({ customDataProviders }); +export function getLanguageModes(supportedLanguages: { [languageId: string]: boolean; }, workspace: Workspace, clientCapabilities: ClientCapabilities, customDataProviders?: IHTMLDataProvider[]): LanguageModes { + const htmlLanguageService = getHTMLLanguageService({ customDataProviders, clientCapabilities }); + const cssLanguageService = getCSSLanguageService({ clientCapabilities }); let documentRegions = getLanguageModelCache(10, 60, document => getDocumentRegions(htmlLanguageService, document)); @@ -77,7 +75,7 @@ export function getLanguageModes(supportedLanguages: { [languageId: string]: boo let modes = Object.create(null); modes['html'] = getHTMLMode(htmlLanguageService, workspace); if (supportedLanguages['css']) { - modes['css'] = getCSSMode(documentRegions, workspace); + modes['css'] = getCSSMode(cssLanguageService, documentRegions, workspace); } if (supportedLanguages['javascript']) { modes['javascript'] = getJavaScriptMode(documentRegions); diff --git a/extensions/html-language-features/server/src/test/completions.test.ts b/extensions/html-language-features/server/src/test/completions.test.ts index de056ed3c1e..aaba72add6d 100644 --- a/extensions/html-language-features/server/src/test/completions.test.ts +++ b/extensions/html-language-features/server/src/test/completions.test.ts @@ -9,6 +9,7 @@ import { URI } from 'vscode-uri'; import { TextDocument, CompletionList, CompletionItemKind } from 'vscode-languageserver-types'; import { getLanguageModes } from '../modes/languageModes'; import { WorkspaceFolder } from 'vscode-languageserver'; +import { ClientCapabilities } from 'vscode-html-languageservice'; export interface ItemDescription { label: string; @@ -58,7 +59,7 @@ export function testCompletionFor(value: string, expected: { count?: number, ite let document = TextDocument.create(uri, 'html', 0, value); let position = document.positionAt(offset); - const languageModes = getLanguageModes({ css: true, javascript: true }, workspace); + const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST); const mode = languageModes.getModeAtPosition(document, position)!; let list = mode.doComplete!(document, position); diff --git a/extensions/html-language-features/server/src/test/folding.test.ts b/extensions/html-language-features/server/src/test/folding.test.ts index 3ce2816185b..e19555d568d 100644 --- a/extensions/html-language-features/server/src/test/folding.test.ts +++ b/extensions/html-language-features/server/src/test/folding.test.ts @@ -8,6 +8,7 @@ import * as assert from 'assert'; import { TextDocument } from 'vscode-languageserver'; import { getFoldingRanges } from '../modes/htmlFolding'; import { getLanguageModes } from '../modes/languageModes'; +import { ClientCapabilities } from 'vscode-css-languageservice'; interface ExpectedIndentRange { startLine: number; @@ -21,7 +22,7 @@ function assertRanges(lines: string[], expected: ExpectedIndentRange[], message? settings: {}, folders: [{ name: 'foo', uri: 'test://foo' }] }; - let languageModes = getLanguageModes({ css: true, javascript: true }, workspace); + let languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST); let actual = getFoldingRanges(languageModes, document, nRanges, null); let actualRanges = []; diff --git a/extensions/html-language-features/server/src/test/formatting.test.ts b/extensions/html-language-features/server/src/test/formatting.test.ts index 702aa76da6c..8c59c75e017 100644 --- a/extensions/html-language-features/server/src/test/formatting.test.ts +++ b/extensions/html-language-features/server/src/test/formatting.test.ts @@ -11,6 +11,7 @@ import { getLanguageModes } from '../modes/languageModes'; import { TextDocument, Range, FormattingOptions } from 'vscode-languageserver-types'; import { format } from '../modes/formatting'; +import { ClientCapabilities } from 'vscode-html-languageservice'; suite('HTML Embedded Formatting', () => { @@ -19,7 +20,7 @@ suite('HTML Embedded Formatting', () => { settings: options, folders: [{ name: 'foo', uri: 'test://foo' }] }; - var languageModes = getLanguageModes({ css: true, javascript: true }, workspace); + var languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST); let rangeStartOffset = value.indexOf('|'); let rangeEndOffset; diff --git a/extensions/html-language-features/server/src/utils/documentContext.ts b/extensions/html-language-features/server/src/utils/documentContext.ts index 71f9a3591be..6c391064fae 100644 --- a/extensions/html-language-features/server/src/utils/documentContext.ts +++ b/extensions/html-language-features/server/src/utils/documentContext.ts @@ -35,9 +35,8 @@ export function getDocumentContext(documentUri: string, workspaceFolders: Worksp try { return url.resolve(base, ref); } catch { - return undefined; + return ''; } - }, }; } diff --git a/extensions/html-language-features/server/yarn.lock b/extensions/html-language-features/server/yarn.lock index c386ce0bbb9..9ea59b6077c 100644 --- a/extensions/html-language-features/server/yarn.lock +++ b/extensions/html-language-features/server/yarn.lock @@ -229,19 +229,19 @@ supports-color@5.4.0: dependencies: has-flag "^3.0.0" -vscode-css-languageservice@^4.0.3-next.0: - version "4.0.3-next.0" - resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-4.0.3-next.0.tgz#ba96894cf2a0c86c744a1274590f27e55ea60f58" - integrity sha512-ku58Y5jDFNfDicv2AAhgu1edgfGcRZPwlKu6EBK2ck/O/Vco7Zy64FDoClJghcYBhJiDs7sy2q/UtQD0IoGbRw== +vscode-css-languageservice@^4.0.3-next.1: + version "4.0.3-next.1" + resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-4.0.3-next.1.tgz#e89d01ce0d79b3e6c2642f5e3ad73cb8160d38d9" + integrity sha512-Zrm5TeraVUJ8vRikWhFt259dQu+WK+Ie3K5UA8BB4kqcanoM+1mcnIt8fPkTXlZLbiEWElrkJ9yuYbDNkufeBg== dependencies: vscode-languageserver-types "^3.15.0-next.2" vscode-nls "^4.1.1" vscode-uri "^2.0.3" -vscode-html-languageservice@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-3.0.3.tgz#0aeae18a59000e317447ea34965f72680a2140ef" - integrity sha512-U+upM3gHp3HaF3wXAnUduA6IDKcz6frWS/dTAju3cZVIyZwOLBBFElQVlLH0ycHyMzqUFrjvdv+kEyPAEWfQ/g== +vscode-html-languageservice@^3.0.4-next.0: + version "3.0.4-next.0" + resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-3.0.4-next.0.tgz#d4f5a103b94753a19b374158212fe734dbe670e8" + integrity sha512-5Z5ITtokWt/zuPKemKEXfC+4XHoQryBAZVAcTwpAel2qqueUwGqjd5ZrVy/2x5GZAdZAipl0BvsTTMkOBS1BFQ== dependencies: vscode-languageserver-types "^3.15.0-next.2" vscode-nls "^4.1.1" diff --git a/extensions/json-language-features/server/src/jsonServerMain.ts b/extensions/json-language-features/server/src/jsonServerMain.ts index 27e29a96764..ba1c7a18518 100644 --- a/extensions/json-language-features/server/src/jsonServerMain.ts +++ b/extensions/json-language-features/server/src/jsonServerMain.ts @@ -14,7 +14,7 @@ import * as fs from 'fs'; import { URI } from 'vscode-uri'; import * as URL from 'url'; import { formatError, runSafe, runSafeAsync } from './utils/runner'; -import { JSONDocument, JSONSchema, getLanguageService, DocumentLanguageSettings, SchemaConfiguration } from 'vscode-json-languageservice'; +import { JSONDocument, JSONSchema, getLanguageService, DocumentLanguageSettings, SchemaConfiguration, ClientCapabilities } from 'vscode-json-languageservice'; import { getLanguageModelCache } from './languageModelCache'; interface ISchemaAssociations { @@ -103,6 +103,7 @@ function getSchemaRequestService(handledSchemas: { [schema: string]: boolean }) let languageService = getLanguageService({ workspaceContext, contributions: [], + clientCapabilities: ClientCapabilities.LATEST }); // Create a text document manager. diff --git a/extensions/npm/package.json b/extensions/npm/package.json index c5b1a45a31e..843255c2141 100644 --- a/extensions/npm/package.json +++ b/extensions/npm/package.json @@ -31,6 +31,7 @@ "activationEvents": [ "onCommand:workbench.action.tasks.runTask", "onLanguage:json", + "workspaceContains:package.json", "onView:npm" ], "contributes": { @@ -47,7 +48,7 @@ { "id": "npm", "name": "%view.name%", - "when": "config.npm.enableScriptExplorer" + "when": "npm:showScriptExplorer || config.npm.enableScriptExplorer" } ] }, diff --git a/extensions/npm/src/main.ts b/extensions/npm/src/main.ts index 640c349334a..d32d245b0ed 100644 --- a/extensions/npm/src/main.ts +++ b/extensions/npm/src/main.ts @@ -7,7 +7,7 @@ import * as httpRequest from 'request-light'; import * as vscode from 'vscode'; import { addJSONProviders } from './features/jsonContributions'; import { NpmScriptsTreeDataProvider } from './npmView'; -import { invalidateTasksCache, NpmTaskProvider } from './tasks'; +import { invalidateTasksCache, NpmTaskProvider, hasPackageJson } from './tasks'; import { invalidateHoverScriptsCache, NpmScriptHoverProvider } from './scriptHover'; import { runSelectedScript } from './commands'; @@ -41,6 +41,10 @@ export async function activate(context: vscode.ExtensionContext): Promise context.subscriptions.push(d); context.subscriptions.push(vscode.commands.registerCommand('npm.runSelectedScript', runSelectedScript)); context.subscriptions.push(addJSONProviders(httpRequest.xhr)); + + if (await hasPackageJson()) { + vscode.commands.executeCommand('setContext', 'npm:showScriptExplorer', true); + } } function registerTaskProvider(context: vscode.ExtensionContext): vscode.Disposable | undefined { diff --git a/extensions/npm/src/tasks.ts b/extensions/npm/src/tasks.ts index bfe9d8cd698..f60f7531516 100644 --- a/extensions/npm/src/tasks.ts +++ b/extensions/npm/src/tasks.ts @@ -262,6 +262,22 @@ export function getPackageJsonUriFromTask(task: Task): Uri | null { return null; } +export async function hasPackageJson(): Promise { + let folders = workspace.workspaceFolders; + if (!folders) { + return false; + } + for (const folder of folders) { + if (folder.uri.scheme === 'file') { + let packageJson = path.join(folder.uri.fsPath, 'package.json'); + if (await exists(packageJson)) { + return true; + } + } + } + return false; +} + async function exists(file: string): Promise { return new Promise((resolve, _reject) => { fs.exists(file, (value) => { diff --git a/extensions/typescript-basics/package.json b/extensions/typescript-basics/package.json index f5532d15dd3..185a8a2bc71 100644 --- a/extensions/typescript-basics/package.json +++ b/extensions/typescript-basics/package.json @@ -130,4 +130,4 @@ } ] } -} \ No newline at end of file +} diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/editor.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/editor.test.ts index 80ad28ddf57..9b6c6d82897 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/editor.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/editor.test.ts @@ -195,4 +195,27 @@ suite('editor tests', () => { ); }); }); + + test('throw when using invalid edit', async function () { + + await withRandomFileEditor('foo', editor => { + + return new Promise((resolve, reject) => { + + editor.edit(edit => { + edit.insert(new Position(0, 0), 'bar'); + setTimeout(() => { + try { + edit.insert(new Position(0, 0), 'bar'); + reject(new Error('expected error')); + } catch (err) { + assert.ok(true); + resolve(); + } + }, 0); + }); + }); + }); + + }); }); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts index 63e39fdd2e4..dc281a06fd5 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { window, Terminal, TerminalVirtualProcess, EventEmitter, TerminalDimensions, workspace, ConfigurationTarget } from 'vscode'; +import { window, Terminal, Pseudoterminal, EventEmitter, TerminalDimensions, workspace, ConfigurationTarget } from 'vscode'; import { doesNotThrow, equal, ok } from 'assert'; suite('window namespace tests', () => { @@ -264,12 +264,12 @@ suite('window namespace tests', () => { }); term.dispose(); }); - const virtualProcess: TerminalVirtualProcess = { + const pty: Pseudoterminal = { onDidWrite: new EventEmitter().event, - start: () => {}, - shutdown: () => {} + open: () => {}, + close: () => {} }; - window.createTerminal({ name: 'c', virtualProcess }); + window.createTerminal({ name: 'c', pty }); }); test('should fire Terminal.onData on write', (done) => { @@ -291,12 +291,12 @@ suite('window namespace tests', () => { let startResolve: () => void; const startPromise: Promise = new Promise(r => startResolve = r); const writeEmitter = new EventEmitter(); - const virtualProcess: TerminalVirtualProcess = { + const pty: Pseudoterminal = { onDidWrite: writeEmitter.event, - start: () => startResolve(), - shutdown: () => {} + open: () => startResolve(), + close: () => {} }; - const terminal = window.createTerminal({ name: 'foo', virtualProcess }); + const terminal = window.createTerminal({ name: 'foo', pty }); }); test('should fire provide dimensions on start as the terminal has been shown', (done) => { @@ -304,9 +304,9 @@ suite('window namespace tests', () => { equal(terminal, term); reg1.dispose(); }); - const virtualProcess: TerminalVirtualProcess = { + const pty: Pseudoterminal = { onDidWrite: new EventEmitter().event, - start: (dimensions) => { + open: (dimensions) => { ok(dimensions!.columns > 0); ok(dimensions!.rows > 0); const reg3 = window.onDidCloseTerminal(() => { @@ -315,9 +315,9 @@ suite('window namespace tests', () => { }); terminal.dispose(); }, - shutdown: () => {} + close: () => {} }; - const terminal = window.createTerminal({ name: 'foo', virtualProcess }); + const terminal = window.createTerminal({ name: 'foo', pty }); }); test('should respect dimension overrides', (done) => { @@ -340,13 +340,13 @@ suite('window namespace tests', () => { }); const writeEmitter = new EventEmitter(); const overrideDimensionsEmitter = new EventEmitter(); - const virtualProcess: TerminalVirtualProcess = { + const pty: Pseudoterminal = { onDidWrite: writeEmitter.event, onDidOverrideDimensions: overrideDimensionsEmitter.event, - start: () => {}, - shutdown: () => {} + open: () => {}, + close: () => {} }; - const terminal = window.createTerminal({ name: 'foo', virtualProcess }); + const terminal = window.createTerminal({ name: 'foo', pty }); }); }); }); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts index c839127a255..daef120762c 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts @@ -35,17 +35,18 @@ suite('workspace-namespace', () => { customProp1: 'testing task one' }; const writeEmitter = new vscode.EventEmitter(); - const execution = new vscode.CustomExecution2((): Thenable => { - return Promise.resolve({ + const execution = new vscode.CustomExecution2((): Thenable => { + const pty: vscode.Pseudoterminal = { onDidWrite: writeEmitter.event, - start: () => { + open: () => { writeEmitter.fire('testing\r\n'); }, - shutdown: () => { + close: () => { taskProvider.dispose(); done(); } - }); + }; + return Promise.resolve(pty); }); const task = new vscode.Task2(kind, vscode.TaskScope.Workspace, taskName, taskType, execution); result.push(task); diff --git a/package.json b/package.json index 0bdff88bdee..8ba1690dde2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.37.0", - "distro": "ecf90e8c66d243c7ac15e30c24b3cc437d0700dc", + "distro": "40921be4a9cdf7467495a9898ecd8c03565c489f", "author": { "name": "Microsoft Corporation" }, @@ -94,7 +94,7 @@ "gulp-gunzip": "^1.0.0", "gulp-json-editor": "^2.5.0", "gulp-plumber": "^1.2.0", - "gulp-remote-src": "^0.4.4", + "gulp-remote-retry-src": "^0.6.0", "gulp-rename": "^1.2.0", "gulp-replace": "^0.5.4", "gulp-shell": "^0.6.5", diff --git a/src/vs/base/browser/ui/centered/centeredViewLayout.ts b/src/vs/base/browser/ui/centered/centeredViewLayout.ts index b40384018f1..299b6534873 100644 --- a/src/vs/base/browser/ui/centered/centeredViewLayout.ts +++ b/src/vs/base/browser/ui/centered/centeredViewLayout.ts @@ -6,7 +6,7 @@ import { SplitView, Orientation, ISplitViewStyles, IView as ISplitViewView } from 'vs/base/browser/ui/splitview/splitview'; import { $ } from 'vs/base/browser/dom'; import { Event } from 'vs/base/common/event'; -import { IView, IViewSize } from 'vs/base/browser/ui/grid/gridview'; +import { IView, IViewSize } from 'vs/base/browser/ui/grid/grid'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Color } from 'vs/base/common/color'; diff --git a/src/vs/base/browser/ui/checkbox/checkbox.ts b/src/vs/base/browser/ui/checkbox/checkbox.ts index 8ec96513dd2..723f64dc4fa 100644 --- a/src/vs/base/browser/ui/checkbox/checkbox.ts +++ b/src/vs/base/browser/ui/checkbox/checkbox.ts @@ -65,7 +65,7 @@ export class CheckboxActionViewItem extends BaseActionViewItem { } } - dipsose(): void { + dispose(): void { this.disposables.dispose(); super.dispose(); } diff --git a/src/vs/base/browser/ui/grid/grid.ts b/src/vs/base/browser/ui/grid/grid.ts index 08090875642..eb8476e1b8b 100644 --- a/src/vs/base/browser/ui/grid/grid.ts +++ b/src/vs/base/browser/ui/grid/grid.ts @@ -7,11 +7,11 @@ import 'vs/css!./gridview'; import { Orientation } from 'vs/base/browser/ui/sash/sash'; import { Disposable } from 'vs/base/common/lifecycle'; import { tail2 as tail, equals } from 'vs/base/common/arrays'; -import { orthogonal, IView, GridView, Sizing as GridViewSizing, Box, IGridViewStyles, IViewSize } from './gridview'; +import { orthogonal, IView as IGridViewView, GridView, Sizing as GridViewSizing, Box, IGridViewStyles, IViewSize, ILayoutController, LayoutController } from './gridview'; import { Event } from 'vs/base/common/event'; import { InvisibleSizing } from 'vs/base/browser/ui/splitview/splitview'; -export { Orientation, Sizing as GridViewSizing } from './gridview'; +export { Orientation, Sizing as GridViewSizing, IViewSize, orthogonal, LayoutPriority } from './gridview'; export const enum Direction { Up, @@ -29,6 +29,11 @@ function oppositeDirection(direction: Direction): Direction { } } +export interface IView extends IGridViewView { + readonly preferredHeight?: number; + readonly preferredWidth?: number; +} + export interface GridLeafNode { readonly view: T; readonly box: Box; @@ -192,6 +197,7 @@ export interface IGridOptions { readonly styles?: IGridStyles; readonly proportionalLayout?: boolean; readonly firstViewVisibleCachedSize?: number; + readonly layoutController?: ILayoutController; } export class Grid extends Disposable { @@ -212,12 +218,14 @@ export class Grid extends Disposable { get element(): HTMLElement { return this.gridview.element; } + private didLayout = false; + constructor(view: T, options: IGridOptions = {}) { super(); this.gridview = new GridView(options); this._register(this.gridview); - this._register(this.gridview.onDidSashReset(this.doResetViewSize, this)); + this._register(this.gridview.onDidSashReset(this.onDidSashReset, this)); const size: number | GridViewSizing = typeof options.firstViewVisibleCachedSize === 'number' ? GridViewSizing.Invisible(options.firstViewVisibleCachedSize) @@ -232,6 +240,7 @@ export class Grid extends Disposable { layout(width: number, height: number): void { this.gridview.layout(width, height); + this.didLayout = true; } hasView(view: T): boolean { @@ -335,10 +344,14 @@ export class Grid extends Disposable { } getViews(): GridBranchNode { - return this.gridview.getViews() as GridBranchNode; + return this.gridview.getView() as GridBranchNode; } getNeighborViews(view: T, direction: Direction, wrap: boolean = false): T[] { + if (!this.didLayout) { + throw new Error('Can\'t call getNeighborViews before first layout'); + } + const location = this.getViewLocation(view); const root = this.getViews(); const node = getGridNode(root, location); @@ -370,8 +383,36 @@ export class Grid extends Disposable { return getGridLocation(element); } - private doResetViewSize(location: number[]): void { - const [parentLocation,] = tail(location); + private onDidSashReset(location: number[]): void { + const resizeToPreferredSize = (location: number[]): boolean => { + const node = this.gridview.getView(location) as GridNode; + + if (isGridBranchNode(node)) { + return false; + } + + const direction = getLocationOrientation(this.orientation, location); + const size = direction === Orientation.HORIZONTAL ? node.view.preferredWidth : node.view.preferredHeight; + + if (typeof size !== 'number') { + return false; + } + + const viewSize = direction === Orientation.HORIZONTAL ? { width: Math.round(size) } : { height: Math.round(size) }; + this.gridview.resizeView(location, viewSize); + return true; + }; + + if (resizeToPreferredSize(location)) { + return; + } + + const [parentLocation, index] = tail(location); + + if (resizeToPreferredSize([...parentLocation, index + 1])) { + return; + } + this.gridview.distributeViewSizes(parentLocation); } } @@ -494,6 +535,9 @@ export class SerializableGrid extends Grid { throw new Error('Invalid serialized state, first leaf not found'); } + const layoutController = new LayoutController(false); + options = { ...options, layoutController }; + if (typeof firstLeaf.cachedVisibleSize === 'number') { options = { ...options, firstViewVisibleCachedSize: firstLeaf.cachedVisibleSize }; } @@ -503,6 +547,7 @@ export class SerializableGrid extends Grid { result.restoreViews(firstLeaf.view, orientation, root); result.initialLayoutContext = { width, height, root }; + layoutController.isLayoutEnabled = true; return result; } diff --git a/src/vs/base/browser/ui/grid/gridview.ts b/src/vs/base/browser/ui/grid/gridview.ts index 3e05e4a9499..81a6950a65b 100644 --- a/src/vs/base/browser/ui/grid/gridview.ts +++ b/src/vs/base/browser/ui/grid/gridview.ts @@ -67,9 +67,18 @@ const defaultStyles: IGridViewStyles = { separatorBorder: Color.transparent }; +export interface ILayoutController { + readonly isLayoutEnabled: boolean; +} + +export class LayoutController implements ILayoutController { + constructor(public isLayoutEnabled: boolean) { } +} + export interface IGridViewOptions { - styles?: IGridViewStyles; - proportionalLayout?: boolean; // default true + readonly styles?: IGridViewStyles; + readonly proportionalLayout?: boolean; // default true + readonly layoutController?: ILayoutController; } class BranchNode implements ISplitView, IDisposable { @@ -466,7 +475,8 @@ class LeafNode implements ISplitView, IDisposable { constructor( readonly view: IView, readonly orientation: Orientation, - orthogonalSize: number = 0 + readonly layoutController: ILayoutController, + orthogonalSize: number ) { this._orthogonalSize = orthogonalSize; @@ -536,7 +546,10 @@ class LeafNode implements ISplitView, IDisposable { layout(size: number): void { this._size = size; - return this.view.layout(this.width, this.height, orthogonal(this.orientation)); + + if (this.layoutController.isLayoutEnabled) { + this.view.layout(this.width, this.height, orthogonal(this.orientation)); + } } setVisible(visible: boolean): void { @@ -553,7 +566,10 @@ class LeafNode implements ISplitView, IDisposable { orthogonalLayout(size: number): void { this._orthogonalSize = size; - return this.view.layout(this.width, this.height, orthogonal(this.orientation)); + + if (this.layoutController.isLayoutEnabled) { + this.view.layout(this.width, this.height, orthogonal(this.orientation)); + } } dispose(): void { } @@ -584,7 +600,7 @@ function flipNode(node: T, size: number, orthogonalSize: number) return result as T; } else { - return new LeafNode((node as LeafNode).view, orthogonal(node.orientation), orthogonalSize) as T; + return new LeafNode((node as LeafNode).view, orthogonal(node.orientation), (node as LeafNode).layoutController, orthogonalSize) as T; } } @@ -644,11 +660,14 @@ export class GridView implements IDisposable { private _onDidChange = new Relay(); readonly onDidChange = this._onDidChange.event; + private layoutController: LayoutController; + constructor(options: IGridViewOptions = {}) { this.element = $('.monaco-grid-view'); this.styles = options.styles || defaultStyles; this.proportionalLayout = typeof options.proportionalLayout !== 'undefined' ? !!options.proportionalLayout : true; this.root = new BranchNode(Orientation.VERTICAL, this.styles, this.proportionalLayout); + this.layoutController = options.layoutController || new LayoutController(true); } style(styles: IGridViewStyles): void { @@ -670,7 +689,7 @@ export class GridView implements IDisposable { const [pathToParent, parent] = this.getNode(rest); if (parent instanceof BranchNode) { - const node = new LeafNode(view, orthogonal(parent.orientation), parent.orthogonalSize); + const node = new LeafNode(view, orthogonal(parent.orientation), this.layoutController, parent.orthogonalSize); parent.addChild(node, size, index); } else { @@ -690,14 +709,14 @@ export class GridView implements IDisposable { grandParent.addChild(newParent, parent.size, parentIndex); newParent.orthogonalLayout(parent.orthogonalSize); - const newSibling = new LeafNode(parent.view, grandParent.orientation, parent.size); + const newSibling = new LeafNode(parent.view, grandParent.orientation, this.layoutController, parent.size); newParent.addChild(newSibling, newSiblingSize, 0); if (typeof size !== 'number' && size.type === 'split') { size = Sizing.Split(0); } - const node = new LeafNode(view, grandParent.orientation, parent.size); + const node = new LeafNode(view, grandParent.orientation, this.layoutController, parent.size); newParent.addChild(node, size, index); } } @@ -759,7 +778,7 @@ export class GridView implements IDisposable { grandParent.addChild(child, child.size, parentIndex + i); } } else { - const newSibling = new LeafNode(sibling.view, orthogonal(sibling.orientation), sibling.size); + const newSibling = new LeafNode(sibling.view, orthogonal(sibling.orientation), this.layoutController, sibling.size); grandParent.addChild(newSibling, sibling.orthogonalSize, parentIndex); } @@ -903,8 +922,11 @@ export class GridView implements IDisposable { parent.setChildVisible(index, visible); } - getViews(): GridBranchNode { - return this._getViews(this.root, this.orientation, { top: 0, left: 0, width: this.width, height: this.height }) as GridBranchNode; + getView(): GridBranchNode; + getView(location?: number[]): GridNode; + getView(location?: number[]): GridNode { + const node = location ? this.getNode(location)[1] : this._root; + return this._getViews(node, this.orientation, { top: 0, left: 0, width: this.width, height: this.height }); } private _getViews(node: Node, orientation: Orientation, box: Box): GridNode { diff --git a/src/vs/base/browser/ui/splitview/splitview.ts b/src/vs/base/browser/ui/splitview/splitview.ts index 171a44837c4..6f031dac570 100644 --- a/src/vs/base/browser/ui/splitview/splitview.ts +++ b/src/vs/base/browser/ui/splitview/splitview.ts @@ -336,7 +336,24 @@ export class SplitView extends Disposable { const onChangeDisposable = onChange(this.onSashChange, this); const onEnd = Event.map(sash.onDidEnd, () => firstIndex(this.sashItems, item => item.sash === sash)); const onEndDisposable = onEnd(this.onSashEnd, this); - const onDidResetDisposable = sash.onDidReset(() => this._onDidSashReset.fire(firstIndex(this.sashItems, item => item.sash === sash))); + + const onDidResetDisposable = sash.onDidReset(() => { + const index = firstIndex(this.sashItems, item => item.sash === sash); + const upIndexes = range(index, -1); + const downIndexes = range(index + 1, this.viewItems.length); + const snapBeforeIndex = this.findFirstSnapIndex(upIndexes); + const snapAfterIndex = this.findFirstSnapIndex(downIndexes); + + if (typeof snapBeforeIndex === 'number' && !this.viewItems[snapBeforeIndex].visible) { + return; + } + + if (typeof snapAfterIndex === 'number' && !this.viewItems[snapAfterIndex].visible) { + return; + } + + this._onDidSashReset.fire(index); + }); const disposable = combinedDisposable(onStartDisposable, onChangeDisposable, onEndDisposable, onDidResetDisposable, sash); const sashItem: ISashItem = { sash, disposable }; @@ -630,10 +647,19 @@ export class SplitView extends Disposable { } distributeViewSizes(): void { - const size = Math.floor(this.size / this.viewItems.length); + const flexibleViewItems: ViewItem[] = []; + let flexibleSize = 0; - for (let i = 0; i < this.viewItems.length; i++) { - const item = this.viewItems[i]; + for (const item of this.viewItems) { + if (item.maximumSize - item.minimumSize > 0) { + flexibleViewItems.push(item); + flexibleSize += item.size; + } + } + + const size = Math.floor(flexibleSize / flexibleViewItems.length); + + for (const item of flexibleViewItems) { item.size = clamp(size, item.minimumSize, item.maximumSize); } diff --git a/src/vs/base/common/buffer.ts b/src/vs/base/common/buffer.ts index 05a22bd3d1e..99d5e5274e1 100644 --- a/src/vs/base/common/buffer.ts +++ b/src/vs/base/common/buffer.ts @@ -105,7 +105,7 @@ export class VSBuffer { } } -function readUInt32BE(source: Uint8Array, offset: number): number { +export function readUInt32BE(source: Uint8Array, offset: number): number { return ( source[offset] * 2 ** 24 + source[offset + 1] * 2 ** 16 @@ -114,7 +114,7 @@ function readUInt32BE(source: Uint8Array, offset: number): number { ); } -function writeUInt32BE(destination: Uint8Array, value: number, offset: number): void { +export function writeUInt32BE(destination: Uint8Array, value: number, offset: number): void { destination[offset + 3] = value; value = value >>> 8; destination[offset + 2] = value; diff --git a/src/vs/base/common/worker/simpleWorker.ts b/src/vs/base/common/worker/simpleWorker.ts index ca6e174ce33..2cc1b188cac 100644 --- a/src/vs/base/common/worker/simpleWorker.ts +++ b/src/vs/base/common/worker/simpleWorker.ts @@ -12,7 +12,7 @@ const INITIALIZE = '$initialize'; export interface IWorker extends IDisposable { getId(): number; - postMessage(message: string): void; + postMessage(message: any, transfer: Transferable[]): void; } export interface IWorkerCallback { @@ -60,7 +60,7 @@ interface IMessageReply { } interface IMessageHandler { - sendMessage(msg: string): void; + sendMessage(msg: any, transfer?: ArrayBuffer[]): void; handleMessage(method: string, args: any[]): Promise; } @@ -98,14 +98,7 @@ class SimpleWorkerProtocol { }); } - public handleMessage(serializedMessage: string): void { - let message: IMessage; - try { - message = JSON.parse(serializedMessage); - } catch (e) { - // nothing - return; - } + public handleMessage(message: IMessage): void { if (!message || !message.vsWorker) { return; } @@ -167,9 +160,21 @@ class SimpleWorkerProtocol { } private _send(msg: IRequestMessage | IReplyMessage): void { - let strMsg = JSON.stringify(msg); - // console.log('SENDING: ' + strMsg); - this._handler.sendMessage(strMsg); + let transfer: ArrayBuffer[] = []; + if (msg.req) { + const m = msg; + for (let i = 0; i < m.args.length; i++) { + if (m.args[i] instanceof ArrayBuffer) { + transfer.push(m.args[i]); + } + } + } else { + const m = msg; + if (m.res instanceof ArrayBuffer) { + transfer.push(m.res); + } + } + this._handler.sendMessage(msg, transfer); } } @@ -195,7 +200,7 @@ export class SimpleWorkerClient extends Disp this._worker = this._register(workerFactory.create( 'vs/base/common/worker/simpleWorker', - (msg: string) => { + (msg: any) => { this._protocol.handleMessage(msg); }, (err: any) => { @@ -208,8 +213,8 @@ export class SimpleWorkerClient extends Disp )); this._protocol = new SimpleWorkerProtocol({ - sendMessage: (msg: string): void => { - this._worker.postMessage(msg); + sendMessage: (msg: any, transfer: ArrayBuffer[]): void => { + this._worker.postMessage(msg, transfer); }, handleMessage: (method: string, args: any[]): Promise => { if (typeof (host as any)[method] !== 'function') { @@ -240,7 +245,7 @@ export class SimpleWorkerClient extends Disp // Send initialize message this._onModuleLoaded = this._protocol.sendMessage(INITIALIZE, [ this._worker.getId(), - loaderConfiguration, + JSON.parse(JSON.stringify(loaderConfiguration)), moduleId, hostMethods, ]); @@ -297,18 +302,18 @@ export class SimpleWorkerServer { private _requestHandler: IRequestHandler | null; private _protocol: SimpleWorkerProtocol; - constructor(postSerializedMessage: (msg: string) => void, requestHandlerFactory: IRequestHandlerFactory | null) { + constructor(postMessage: (msg: any, transfer?: Transferable[]) => void, requestHandlerFactory: IRequestHandlerFactory | null) { this._requestHandlerFactory = requestHandlerFactory; this._requestHandler = null; this._protocol = new SimpleWorkerProtocol({ - sendMessage: (msg: string): void => { - postSerializedMessage(msg); + sendMessage: (msg: any, transfer: ArrayBuffer[]): void => { + postMessage(msg, transfer); }, handleMessage: (method: string, args: any[]): Promise => this._handleMessage(method, args) }); } - public onmessage(msg: string): void { + public onmessage(msg: any): void { this._protocol.handleMessage(msg); } diff --git a/src/vs/base/test/browser/ui/grid/grid.test.ts b/src/vs/base/test/browser/ui/grid/grid.test.ts index 092f3007082..3c2b1b4ec44 100644 --- a/src/vs/base/test/browser/ui/grid/grid.test.ts +++ b/src/vs/base/test/browser/ui/grid/grid.test.ts @@ -634,12 +634,6 @@ suite('SerializableGrid', function () { assert.deepEqual(nodesToArrays(grid2.getViews()), [[view4Copy, view2Copy], [[view1Copy, view5Copy], view3Copy]]); - assert.deepEqual(view1Copy.size, [50, 50]); - assert.deepEqual(view2Copy.size, [50, 50]); - assert.deepEqual(view3Copy.size, [50, 100]); - assert.deepEqual(view4Copy.size, [50, 50]); - assert.deepEqual(view5Copy.size, [50, 50]); - grid2.layout(800, 600); assert.deepEqual(view1Copy.size, [600, 300]); diff --git a/src/vs/base/test/browser/ui/grid/gridview.test.ts b/src/vs/base/test/browser/ui/grid/gridview.test.ts index 20b7626b184..78cc44cc76a 100644 --- a/src/vs/base/test/browser/ui/grid/gridview.test.ts +++ b/src/vs/base/test/browser/ui/grid/gridview.test.ts @@ -22,7 +22,7 @@ suite('Gridview', function () { }); test('empty gridview is empty', function () { - assert.deepEqual(nodesToArrays(gridview.getViews()), []); + assert.deepEqual(nodesToArrays(gridview.getView()), []); gridview.dispose(); }); @@ -43,7 +43,7 @@ suite('Gridview', function () { gridview.addView(views[1], 200, [1]); gridview.addView(views[2], 200, [2]); - assert.deepEqual(nodesToArrays(gridview.getViews()), views); + assert.deepEqual(nodesToArrays(gridview.getView()), views); gridview.dispose(); }); @@ -62,7 +62,7 @@ suite('Gridview', function () { gridview.addView((views[1] as TestView[])[0] as IView, 200, [1]); gridview.addView((views[1] as TestView[])[1] as IView, 200, [1, 1]); - assert.deepEqual(nodesToArrays(gridview.getViews()), views); + assert.deepEqual(nodesToArrays(gridview.getView()), views); gridview.dispose(); }); @@ -71,35 +71,35 @@ suite('Gridview', function () { const view1 = new TestView(20, 20, 20, 20); gridview.addView(view1 as IView, 200, [0]); - assert.deepEqual(nodesToArrays(gridview.getViews()), [view1]); + assert.deepEqual(nodesToArrays(gridview.getView()), [view1]); const view2 = new TestView(20, 20, 20, 20); gridview.addView(view2 as IView, 200, [1]); - assert.deepEqual(nodesToArrays(gridview.getViews()), [view1, view2]); + assert.deepEqual(nodesToArrays(gridview.getView()), [view1, view2]); const view3 = new TestView(20, 20, 20, 20); gridview.addView(view3 as IView, 200, [1, 0]); - assert.deepEqual(nodesToArrays(gridview.getViews()), [view1, [view3, view2]]); + assert.deepEqual(nodesToArrays(gridview.getView()), [view1, [view3, view2]]); const view4 = new TestView(20, 20, 20, 20); gridview.addView(view4 as IView, 200, [1, 0, 0]); - assert.deepEqual(nodesToArrays(gridview.getViews()), [view1, [[view4, view3], view2]]); + assert.deepEqual(nodesToArrays(gridview.getView()), [view1, [[view4, view3], view2]]); const view5 = new TestView(20, 20, 20, 20); gridview.addView(view5 as IView, 200, [1, 0]); - assert.deepEqual(nodesToArrays(gridview.getViews()), [view1, [view5, [view4, view3], view2]]); + assert.deepEqual(nodesToArrays(gridview.getView()), [view1, [view5, [view4, view3], view2]]); const view6 = new TestView(20, 20, 20, 20); gridview.addView(view6 as IView, 200, [2]); - assert.deepEqual(nodesToArrays(gridview.getViews()), [view1, [view5, [view4, view3], view2], view6]); + assert.deepEqual(nodesToArrays(gridview.getView()), [view1, [view5, [view4, view3], view2], view6]); const view7 = new TestView(20, 20, 20, 20); gridview.addView(view7 as IView, 200, [1, 1]); - assert.deepEqual(nodesToArrays(gridview.getViews()), [view1, [view5, view7, [view4, view3], view2], view6]); + assert.deepEqual(nodesToArrays(gridview.getView()), [view1, [view5, view7, [view4, view3], view2], view6]); const view8 = new TestView(20, 20, 20, 20); gridview.addView(view8 as IView, 200, [1, 1, 0]); - assert.deepEqual(nodesToArrays(gridview.getViews()), [view1, [view5, [view8, view7], [view4, view3], view2], view6]); + assert.deepEqual(nodesToArrays(gridview.getView()), [view1, [view5, [view8, view7], [view4, view3], view2], view6]); gridview.dispose(); }); diff --git a/src/vs/base/test/browser/ui/grid/util.ts b/src/vs/base/test/browser/ui/grid/util.ts index 0efdc44851a..39a35736ddd 100644 --- a/src/vs/base/test/browser/ui/grid/util.ts +++ b/src/vs/base/test/browser/ui/grid/util.ts @@ -5,7 +5,8 @@ import * as assert from 'assert'; import { Emitter, Event } from 'vs/base/common/event'; -import { IView, GridNode, isGridBranchNode, } from 'vs/base/browser/ui/grid/gridview'; +import { GridNode, isGridBranchNode } from 'vs/base/browser/ui/grid/gridview'; +import { IView } from 'vs/base/browser/ui/grid/grid'; export class TestView implements IView { @@ -78,4 +79,4 @@ export function nodesToArrays(node: GridNode): any { } else { return node.view; } -} \ No newline at end of file +} diff --git a/src/vs/base/worker/defaultWorkerFactory.ts b/src/vs/base/worker/defaultWorkerFactory.ts index eccaa81f139..c2a7ddc54e6 100644 --- a/src/vs/base/worker/defaultWorkerFactory.ts +++ b/src/vs/base/worker/defaultWorkerFactory.ts @@ -63,7 +63,7 @@ class WebWorker implements IWorker { } else { this.worker = Promise.resolve(workerOrPromise); } - this.postMessage(moduleId); + this.postMessage(moduleId, []); this.worker.then((w) => { w.onmessage = function (ev: any) { onMessageCallback(ev.data); @@ -79,9 +79,9 @@ class WebWorker implements IWorker { return this.id; } - public postMessage(msg: string): void { + public postMessage(message: any, transfer: Transferable[]): void { if (this.worker) { - this.worker.then(w => w.postMessage(msg)); + this.worker.then(w => w.postMessage(message, transfer)); } } diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index e571b75dd3b..c3be18fe35d 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -1848,4 +1848,6 @@ registerThemingParticipant((theme, collector) => { if (unnecessaryBorder) { collector.addRule(`.${SHOW_UNUSED_ENABLED_CLASS} .monaco-editor .${ClassName.EditorUnnecessaryDecoration} { border-bottom: 2px dashed ${unnecessaryBorder}; }`); } + + collector.addRule(`.monaco-editor .${ClassName.EditorDeprecatedInlineDecoration} { text-decoration: line-through; }`); }); diff --git a/src/vs/editor/browser/widget/media/editor.css b/src/vs/editor/browser/widget/media/editor.css index 83c0859bd29..fb9e5f5e8b8 100644 --- a/src/vs/editor/browser/widget/media/editor.css +++ b/src/vs/editor/browser/widget/media/editor.css @@ -41,6 +41,8 @@ top: 0; } +/* .monaco-editor .auto-closed-character { opacity: 0.3; } +*/ diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 3de16ed77a4..3037589c764 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -14,6 +14,7 @@ import { IModelContentChange, IModelContentChangedEvent, IModelDecorationsChange import { SearchData } from 'vs/editor/common/model/textModelSearch'; import { LanguageId, LanguageIdentifier, FormattingOptions } from 'vs/editor/common/modes'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; +import { MultilineTokens } from 'vs/editor/common/model/tokensStore'; /** * Vertical Lane in the overview ruler of the editor. @@ -779,6 +780,11 @@ export interface ITextModel { */ findPreviousMatch(searchString: string, searchStart: IPosition, isRegex: boolean, matchCase: boolean, wordSeparators: string | null, captureMatches: boolean): FindMatch | null; + /** + * @internal + */ + setTokens(tokens: MultilineTokens[]): void; + /** * Flush all tokenization state. * @internal diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 3b7bb2d1365..e73183b0bcc 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -22,7 +22,7 @@ import { IntervalNode, IntervalTree, getNodeIsInOverviewRuler, recomputeMaxEnd } import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent, IModelTokensChangedEvent, InternalModelContentChangeEvent, ModelRawChange, ModelRawContentChangedEvent, ModelRawEOLChanged, ModelRawFlush, ModelRawLineChanged, ModelRawLinesDeleted, ModelRawLinesInserted } from 'vs/editor/common/model/textModelEvents'; import { SearchData, SearchParams, TextModelSearch } from 'vs/editor/common/model/textModelSearch'; -import { TextModelTokenization, countEOL } from 'vs/editor/common/model/textModelTokens'; +import { TextModelTokenization } from 'vs/editor/common/model/textModelTokens'; import { getWordAtText } from 'vs/editor/common/model/wordHelper'; import { LanguageId, LanguageIdentifier, FormattingOptions } from 'vs/editor/common/modes'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; @@ -32,7 +32,7 @@ import { BracketsUtils, RichEditBracket, RichEditBrackets } from 'vs/editor/comm import { ITheme, ThemeColor } from 'vs/platform/theme/common/themeService'; import { withUndefinedAsNull } from 'vs/base/common/types'; import { VSBufferReadableStream, VSBuffer } from 'vs/base/common/buffer'; -import { TokensStore, MultilineTokens } from 'vs/editor/common/model/tokensStore'; +import { TokensStore, MultilineTokens, countEOL } from 'vs/editor/common/model/tokensStore'; import { Color } from 'vs/base/common/color'; function createTextBufferBuilder() { @@ -1279,7 +1279,7 @@ export class TextModel extends Disposable implements model.ITextModel { for (let i = 0, len = contentChanges.length; i < len; i++) { const change = contentChanges[i]; const [eolCount, firstLineLength] = countEOL(change.text); - this._tokens.applyEdits(change.range, eolCount, firstLineLength); + this._tokens.acceptEdit(change.range, eolCount, firstLineLength); this._onDidChangeDecorations.fire(); this._decorationsTree.acceptReplace(change.rangeOffset, change.rangeLength, change.text.length, change.forceMoveMarkers); @@ -1704,7 +1704,7 @@ export class TextModel extends Disposable implements model.ITextModel { //#region Tokenization - public setLineTokens(lineNumber: number, tokens: Uint32Array): void { + public setLineTokens(lineNumber: number, tokens: Uint32Array | ArrayBuffer | null): void { if (lineNumber < 1 || lineNumber > this.getLineCount()) { throw new Error('Illegal value for lineNumber'); } diff --git a/src/vs/editor/common/model/textModelTokens.ts b/src/vs/editor/common/model/textModelTokens.ts index b959ba7a1b6..076b142db79 100644 --- a/src/vs/editor/common/model/textModelTokens.ts +++ b/src/vs/editor/common/model/textModelTokens.ts @@ -15,38 +15,7 @@ import { nullTokenize2 } from 'vs/editor/common/modes/nullMode'; import { TextModel } from 'vs/editor/common/model/textModel'; import { Disposable } from 'vs/base/common/lifecycle'; import { StopWatch } from 'vs/base/common/stopwatch'; -import { CharCode } from 'vs/base/common/charCode'; -import { MultilineTokensBuilder } from 'vs/editor/common/model/tokensStore'; - -export function countEOL(text: string): [number, number] { - let eolCount = 0; - let firstLineLength = 0; - for (let i = 0, len = text.length; i < len; i++) { - const chr = text.charCodeAt(i); - - if (chr === CharCode.CarriageReturn) { - if (eolCount === 0) { - firstLineLength = i; - } - eolCount++; - if (i + 1 < len && text.charCodeAt(i + 1) === CharCode.LineFeed) { - // \r\n... case - i++; // skip \n - } else { - // \r... case - } - } else if (chr === CharCode.LineFeed) { - if (eolCount === 0) { - firstLineLength = i; - } - eolCount++; - } - } - if (eolCount === 0) { - firstLineLength = text.length; - } - return [eolCount, firstLineLength]; -} +import { MultilineTokensBuilder, countEOL } from 'vs/editor/common/model/tokensStore'; const enum Constants { CHEAP_TOKENIZATION_LENGTH_LIMIT = 2048 @@ -117,6 +86,9 @@ export class TokenizationStateStore { if (deleteCount === 0) { return; } + if (start + deleteCount > this._len) { + deleteCount = this._len - start; + } this._beginState.splice(start, deleteCount); this._valid.splice(start, deleteCount); this._len -= deleteCount; diff --git a/src/vs/editor/common/model/tokensStore.ts b/src/vs/editor/common/model/tokensStore.ts index 3ee6ab6ced3..d965bcf8937 100644 --- a/src/vs/editor/common/model/tokensStore.ts +++ b/src/vs/editor/common/model/tokensStore.ts @@ -8,6 +8,38 @@ import { LineTokens } from 'vs/editor/common/core/lineTokens'; import { Position } from 'vs/editor/common/core/position'; import { IRange } from 'vs/editor/common/core/range'; import { ColorId, FontStyle, LanguageId, MetadataConsts, StandardTokenType, TokenMetadata } from 'vs/editor/common/modes'; +import { writeUInt32BE, readUInt32BE } from 'vs/base/common/buffer'; +import { CharCode } from 'vs/base/common/charCode'; + +export function countEOL(text: string): [number, number] { + let eolCount = 0; + let firstLineLength = 0; + for (let i = 0, len = text.length; i < len; i++) { + const chr = text.charCodeAt(i); + + if (chr === CharCode.CarriageReturn) { + if (eolCount === 0) { + firstLineLength = i; + } + eolCount++; + if (i + 1 < len && text.charCodeAt(i + 1) === CharCode.LineFeed) { + // \r\n... case + i++; // skip \n + } else { + // \r... case + } + } else if (chr === CharCode.LineFeed) { + if (eolCount === 0) { + firstLineLength = i; + } + eolCount++; + } + } + if (eolCount === 0) { + firstLineLength = text.length; + } + return [eolCount, firstLineLength]; +} function getDefaultMetadata(topLevelLanguageId: LanguageId): number { return ( @@ -39,23 +71,226 @@ export class MultilineTokensBuilder { return; } } - this.tokens.push(new MultilineTokens(lineNumber, lineTokens)); + this.tokens.push(new MultilineTokens(lineNumber, [lineTokens])); + } + + public static deserialize(buff: Uint8Array): MultilineTokens[] { + let offset = 0; + const count = readUInt32BE(buff, offset); offset += 4; + let result: MultilineTokens[] = []; + for (let i = 0; i < count; i++) { + offset = MultilineTokens.deserialize(buff, offset, result); + } + return result; + } + + public serialize(): Uint8Array { + const size = this._serializeSize(); + const result = new Uint8Array(size); + this._serialize(result); + return result; + } + + private _serializeSize(): number { + let result = 0; + result += 4; // 4 bytes for the count + for (let i = 0; i < this.tokens.length; i++) { + result += this.tokens[i].serializeSize(); + } + return result; + } + + private _serialize(destination: Uint8Array): void { + let offset = 0; + writeUInt32BE(destination, this.tokens.length, offset); offset += 4; + for (let i = 0; i < this.tokens.length; i++) { + offset = this.tokens[i].serialize(destination, offset); + } } } export class MultilineTokens { - public readonly startLineNumber: number; - public readonly tokens: Uint32Array[]; + public startLineNumber: number; + public tokens: (Uint32Array | ArrayBuffer | null)[]; - constructor(lineNumber: number, tokens: Uint32Array) { - this.startLineNumber = lineNumber; - this.tokens = [tokens]; + constructor(startLineNumber: number, tokens: Uint32Array[]) { + this.startLineNumber = startLineNumber; + this.tokens = tokens; + } + + public static deserialize(buff: Uint8Array, offset: number, result: MultilineTokens[]): number { + const view32 = new Uint32Array(buff.buffer); + const startLineNumber = readUInt32BE(buff, offset); offset += 4; + const count = readUInt32BE(buff, offset); offset += 4; + let tokens: Uint32Array[] = []; + for (let i = 0; i < count; i++) { + const byteCount = readUInt32BE(buff, offset); offset += 4; + tokens.push(view32.subarray(offset / 4, offset / 4 + byteCount / 4)); + offset += byteCount; + } + result.push(new MultilineTokens(startLineNumber, tokens)); + return offset; + } + + public serializeSize(): number { + let result = 0; + result += 4; // 4 bytes for the start line number + result += 4; // 4 bytes for the line count + for (let i = 0; i < this.tokens.length; i++) { + const lineTokens = this.tokens[i]; + if (!(lineTokens instanceof Uint32Array)) { + throw new Error(`Not supported!`); + } + result += 4; // 4 bytes for the byte count + result += lineTokens.byteLength; + } + return result; + } + + public serialize(destination: Uint8Array, offset: number): number { + writeUInt32BE(destination, this.startLineNumber, offset); offset += 4; + writeUInt32BE(destination, this.tokens.length, offset); offset += 4; + for (let i = 0; i < this.tokens.length; i++) { + const lineTokens = this.tokens[i]; + if (!(lineTokens instanceof Uint32Array)) { + throw new Error(`Not supported!`); + } + writeUInt32BE(destination, lineTokens.byteLength, offset); offset += 4; + destination.set(new Uint8Array(lineTokens.buffer), offset); offset += lineTokens.byteLength; + } + return offset; + } + + public applyEdit(range: IRange, text: string): void { + const [eolCount, firstLineLength] = countEOL(text); + this._acceptDeleteRange(range); + this._acceptInsertText(new Position(range.startLineNumber, range.startColumn), eolCount, firstLineLength); + } + + private _acceptDeleteRange(range: IRange): void { + if (range.startLineNumber === range.endLineNumber && range.startColumn === range.endColumn) { + // Nothing to delete + return; + } + + const firstLineIndex = range.startLineNumber - this.startLineNumber; + const lastLineIndex = range.endLineNumber - this.startLineNumber; + + if (lastLineIndex < 0) { + // this deletion occurs entirely before this block, so we only need to adjust line numbers + const deletedLinesCount = lastLineIndex - firstLineIndex; + this.startLineNumber -= deletedLinesCount; + return; + } + + if (firstLineIndex >= this.tokens.length) { + // this deletion occurs entirely after this block, so there is nothing to do + return; + } + + if (firstLineIndex < 0 && lastLineIndex >= this.tokens.length) { + // this deletion completely encompasses this block + this.startLineNumber = 0; + this.tokens = []; + } + + if (firstLineIndex === lastLineIndex) { + // a delete on a single line + this.tokens[firstLineIndex] = TokensStore._delete(this.tokens[firstLineIndex], range.startColumn - 1, range.endColumn - 1); + return; + } + + if (firstLineIndex >= 0) { + // The first line survives + this.tokens[firstLineIndex] = TokensStore._deleteEnding(this.tokens[firstLineIndex], range.startColumn - 1); + + if (lastLineIndex < this.tokens.length) { + // The last line survives + const lastLineTokens = TokensStore._deleteBeginning(this.tokens[lastLineIndex], range.endColumn - 1); + + // Take remaining text on last line and append it to remaining text on first line + this.tokens[firstLineIndex] = TokensStore._append(this.tokens[firstLineIndex], lastLineTokens); + + // Delete middle lines + this.tokens.splice(firstLineIndex + 1, lastLineIndex - firstLineIndex); + } else { + // The last line does not survive + + // Take remaining text on last line and append it to remaining text on first line + this.tokens[firstLineIndex] = TokensStore._append(this.tokens[firstLineIndex], null); + + // Delete lines + this.tokens = this.tokens.slice(0, firstLineIndex + 1); + } + } else { + // The first line does not survive + + const deletedBefore = -firstLineIndex; + this.startLineNumber -= deletedBefore; + + // Remove beginning from last line + this.tokens[lastLineIndex] = TokensStore._deleteBeginning(this.tokens[lastLineIndex], range.endColumn - 1); + + // Delete lines + this.tokens = this.tokens.slice(lastLineIndex); + } + } + + private _acceptInsertText(position: Position, eolCount: number, firstLineLength: number): void { + + if (eolCount === 0 && firstLineLength === 0) { + // Nothing to insert + return; + } + + const lineIndex = position.lineNumber - this.startLineNumber; + + if (lineIndex < 0) { + // this insertion occurs before this block, so we only need to adjust line numbers + this.startLineNumber += eolCount; + return; + } + + if (lineIndex >= this.tokens.length) { + // this insertion occurs after this block, so there is nothing to do + return; + } + + if (eolCount === 0) { + // Inserting text on one line + this.tokens[lineIndex] = TokensStore._insert(this.tokens[lineIndex], position.column - 1, firstLineLength); + return; + } + + this.tokens[lineIndex] = TokensStore._deleteEnding(this.tokens[lineIndex], position.column - 1); + this.tokens[lineIndex] = TokensStore._insert(this.tokens[lineIndex], position.column - 1, firstLineLength); + + this._insertLines(position.lineNumber, eolCount); + } + + private _insertLines(insertIndex: number, insertCount: number): void { + if (insertCount === 0) { + return; + } + let lineTokens: (Uint32Array | ArrayBuffer | null)[] = []; + for (let i = 0; i < insertCount; i++) { + lineTokens[i] = null; + } + this.tokens = arrays.arrayInsert(this.tokens, insertIndex, lineTokens); + } +} + +function toUint32Array(arr: Uint32Array | ArrayBuffer): Uint32Array { + if (arr instanceof Uint32Array) { + return arr; + } else { + return new Uint32Array(arr); } } export class TokensStore { - private _lineTokens: (ArrayBuffer | null)[]; + private _lineTokens: (Uint32Array | ArrayBuffer | null)[]; private _len: number; constructor() { @@ -69,13 +304,13 @@ export class TokensStore { } public getTokens(topLevelLanguageId: LanguageId, lineIndex: number, lineText: string): LineTokens { - let rawLineTokens: ArrayBuffer | null = null; + let rawLineTokens: Uint32Array | ArrayBuffer | null = null; if (lineIndex < this._len) { rawLineTokens = this._lineTokens[lineIndex]; } if (rawLineTokens !== null && rawLineTokens !== EMPTY_LINE_TOKENS) { - return new LineTokens(new Uint32Array(rawLineTokens), lineText); + return new LineTokens(toUint32Array(rawLineTokens), lineText); } let lineTokens = new Uint32Array(2); @@ -84,7 +319,10 @@ export class TokensStore { return new LineTokens(lineTokens, lineText); } - private static _massageTokens(topLevelLanguageId: LanguageId, lineTextLength: number, tokens: Uint32Array): ArrayBuffer { + private static _massageTokens(topLevelLanguageId: LanguageId, lineTextLength: number, _tokens: Uint32Array | ArrayBuffer | null): Uint32Array | ArrayBuffer { + + const tokens = _tokens ? toUint32Array(_tokens) : null; + if (lineTextLength === 0) { let hasDifferentLanguageId = false; if (tokens && tokens.length > 1) { @@ -97,12 +335,20 @@ export class TokensStore { } if (!tokens || tokens.length === 0) { - tokens = new Uint32Array(2); + const tokens = new Uint32Array(2); tokens[0] = lineTextLength; tokens[1] = getDefaultMetadata(topLevelLanguageId); + return tokens.buffer; } - return tokens.buffer; + // Ensure the last token covers the end of the text + tokens[tokens.length - 2] = lineTextLength; + + if (tokens.byteOffset === 0 && tokens.byteLength === tokens.buffer.byteLength) { + // Store directly the ArrayBuffer pointer to save an object + return tokens.buffer; + } + return tokens; } private _ensureLine(lineIndex: number): void { @@ -116,6 +362,9 @@ export class TokensStore { if (deleteCount === 0) { return; } + if (start + deleteCount > this._len) { + deleteCount = this._len - start; + } this._lineTokens.splice(start, deleteCount); this._len -= deleteCount; } @@ -124,7 +373,7 @@ export class TokensStore { if (insertCount === 0) { return; } - let lineTokens: (ArrayBuffer | null)[] = []; + let lineTokens: (Uint32Array | ArrayBuffer | null)[] = []; for (let i = 0; i < insertCount; i++) { lineTokens[i] = null; } @@ -132,7 +381,7 @@ export class TokensStore { this._len += insertCount; } - public setTokens(topLevelLanguageId: LanguageId, lineIndex: number, lineTextLength: number, _tokens: Uint32Array): void { + public setTokens(topLevelLanguageId: LanguageId, lineIndex: number, lineTextLength: number, _tokens: Uint32Array | ArrayBuffer | null): void { const tokens = TokensStore._massageTokens(topLevelLanguageId, lineTextLength, _tokens); this._ensureLine(lineIndex); this._lineTokens[lineIndex] = tokens; @@ -140,7 +389,7 @@ export class TokensStore { //#region Editing - public applyEdits(range: IRange, eolCount: number, firstLineLength: number): void { + public acceptEdit(range: IRange, eolCount: number, firstLineLength: number): void { this._acceptDeleteRange(range); this._acceptInsertText(new Position(range.startLineNumber, range.startColumn), eolCount, firstLineLength); } @@ -165,7 +414,7 @@ export class TokensStore { this._lineTokens[firstLineIndex] = TokensStore._deleteEnding(this._lineTokens[firstLineIndex], range.startColumn - 1); const lastLineIndex = range.endLineNumber - 1; - let lastLineTokens: ArrayBuffer | null = null; + let lastLineTokens: Uint32Array | ArrayBuffer | null = null; if (lastLineIndex < this._len) { lastLineTokens = TokensStore._deleteBeginning(this._lineTokens[lastLineIndex], range.endColumn - 1); } @@ -201,29 +450,29 @@ export class TokensStore { this._insertLines(position.lineNumber, eolCount); } - private static _deleteBeginning(lineTokens: ArrayBuffer | null, toChIndex: number): ArrayBuffer | null { + public static _deleteBeginning(lineTokens: Uint32Array | ArrayBuffer | null, toChIndex: number): Uint32Array | ArrayBuffer | null { if (lineTokens === null || lineTokens === EMPTY_LINE_TOKENS) { return lineTokens; } return TokensStore._delete(lineTokens, 0, toChIndex); } - private static _deleteEnding(lineTokens: ArrayBuffer | null, fromChIndex: number): ArrayBuffer | null { + public static _deleteEnding(lineTokens: Uint32Array | ArrayBuffer | null, fromChIndex: number): Uint32Array | ArrayBuffer | null { if (lineTokens === null || lineTokens === EMPTY_LINE_TOKENS) { return lineTokens; } - const tokens = new Uint32Array(lineTokens); + const tokens = toUint32Array(lineTokens); const lineTextLength = tokens[tokens.length - 2]; return TokensStore._delete(lineTokens, fromChIndex, lineTextLength); } - private static _delete(lineTokens: ArrayBuffer | null, fromChIndex: number, toChIndex: number): ArrayBuffer | null { + public static _delete(lineTokens: Uint32Array | ArrayBuffer | null, fromChIndex: number, toChIndex: number): Uint32Array | ArrayBuffer | null { if (lineTokens === null || lineTokens === EMPTY_LINE_TOKENS || fromChIndex === toChIndex) { return lineTokens; } - const tokens = new Uint32Array(lineTokens); + const tokens = toUint32Array(lineTokens); const tokensCount = (tokens.length >>> 1); // special case: deleting everything @@ -275,7 +524,7 @@ export class TokensStore { return tmp.buffer; } - private static _append(lineTokens: ArrayBuffer | null, _otherTokens: ArrayBuffer | null): ArrayBuffer | null { + public static _append(lineTokens: Uint32Array | ArrayBuffer | null, _otherTokens: Uint32Array | ArrayBuffer | null): Uint32Array | ArrayBuffer | null { if (_otherTokens === EMPTY_LINE_TOKENS) { return lineTokens; } @@ -289,8 +538,8 @@ export class TokensStore { // cannot determine combined line length... return null; } - const myTokens = new Uint32Array(lineTokens); - const otherTokens = new Uint32Array(_otherTokens); + const myTokens = toUint32Array(lineTokens); + const otherTokens = toUint32Array(_otherTokens); const otherTokensCount = (otherTokens.length >>> 1); let result = new Uint32Array(myTokens.length + otherTokens.length); @@ -304,13 +553,13 @@ export class TokensStore { return result.buffer; } - private static _insert(lineTokens: ArrayBuffer | null, chIndex: number, textLength: number): ArrayBuffer | null { + public static _insert(lineTokens: Uint32Array | ArrayBuffer | null, chIndex: number, textLength: number): Uint32Array | ArrayBuffer | null { if (lineTokens === null || lineTokens === EMPTY_LINE_TOKENS) { // nothing to do return lineTokens; } - const tokens = new Uint32Array(lineTokens); + const tokens = toUint32Array(lineTokens); const tokensCount = (tokens.length >>> 1); let fromTokenIndex = LineTokens.findIndexInTokensArray(tokens, chIndex); diff --git a/src/vs/editor/common/modes/tokenization/typescript.ts b/src/vs/editor/common/modes/tokenization/typescript.ts new file mode 100644 index 00000000000..207e8f492ee --- /dev/null +++ b/src/vs/editor/common/modes/tokenization/typescript.ts @@ -0,0 +1,304 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { StandardTokenType } from 'vs/editor/common/modes'; +import { CharCode } from 'vs/base/common/charCode'; + +class ParserContext { + public readonly text: string; + public readonly len: number; + public readonly tokens: number[]; + public pos: number; + + private currentTokenStartOffset: number; + private currentTokenType: StandardTokenType; + + constructor(text: string) { + this.text = text; + this.len = this.text.length; + this.tokens = []; + this.pos = 0; + this.currentTokenStartOffset = 0; + this.currentTokenType = StandardTokenType.Other; + } + + private _safeCharCodeAt(index: number): number { + if (index >= this.len) { + return CharCode.Null; + } + return this.text.charCodeAt(index); + } + + peek(distance: number = 0): number { + return this._safeCharCodeAt(this.pos + distance); + } + + next(): number { + const result = this._safeCharCodeAt(this.pos); + this.pos++; + return result; + } + + advance(distance: number): void { + this.pos += distance; + } + + eof(): boolean { + return this.pos >= this.len; + } + + beginToken(tokenType: StandardTokenType, deltaPos: number = 0): void { + this.currentTokenStartOffset = this.pos + deltaPos; + this.currentTokenType = tokenType; + } + + endToken(deltaPos: number = 0): void { + const length = this.pos + deltaPos - this.currentTokenStartOffset; + // check if it is touching previous token + if (this.tokens.length > 0) { + const previousStartOffset = this.tokens[this.tokens.length - 3]; + const previousLength = this.tokens[this.tokens.length - 2]; + const previousTokenType = this.tokens[this.tokens.length - 1]; + const previousEndOffset = previousStartOffset + previousLength; + if (this.currentTokenStartOffset === previousEndOffset && previousTokenType === this.currentTokenType) { + // extend previous token + this.tokens[this.tokens.length - 2] += length; + return; + } + } + this.tokens.push(this.currentTokenStartOffset, length, this.currentTokenType); + } +} + +export function parse(text: string): number[] { + const ctx = new ParserContext(text); + while (!ctx.eof()) { + parseRoot(ctx); + } + return ctx.tokens; +} + +function parseRoot(ctx: ParserContext): void { + let curlyCount = 0; + while (!ctx.eof()) { + const ch = ctx.peek(); + + switch (ch) { + case CharCode.SingleQuote: + parseSimpleString(ctx, CharCode.SingleQuote); + break; + case CharCode.DoubleQuote: + parseSimpleString(ctx, CharCode.DoubleQuote); + break; + case CharCode.BackTick: + parseInterpolatedString(ctx); + break; + case CharCode.Slash: + parseSlash(ctx); + break; + case CharCode.OpenCurlyBrace: + ctx.advance(1); + curlyCount++; + break; + case CharCode.CloseCurlyBrace: + ctx.advance(1); + curlyCount--; + if (curlyCount < 0) { + return; + } + break; + default: + ctx.advance(1); + } + } + +} + +function parseSimpleString(ctx: ParserContext, closingQuote: number): void { + ctx.beginToken(StandardTokenType.String); + + // skip the opening quote + ctx.advance(1); + + while (!ctx.eof()) { + const ch = ctx.next(); + if (ch === CharCode.Backslash) { + // skip \r\n or any other character following a backslash + const advanceCount = (ctx.peek() === CharCode.CarriageReturn && ctx.peek(1) === CharCode.LineFeed ? 2 : 1); + ctx.advance(advanceCount); + } else if (ch === closingQuote) { + // hit end quote, so stop + break; + } + } + + ctx.endToken(); +} + +function parseInterpolatedString(ctx: ParserContext): void { + ctx.beginToken(StandardTokenType.String); + + // skip the opening quote + ctx.advance(1); + + while (!ctx.eof()) { + const ch = ctx.next(); + if (ch === CharCode.Backslash) { + // skip \r\n or any other character following a backslash + const advanceCount = (ctx.peek() === CharCode.CarriageReturn && ctx.peek(1) === CharCode.LineFeed ? 2 : 1); + ctx.advance(advanceCount); + } else if (ch === CharCode.BackTick) { + // hit end quote, so stop + break; + } else if (ch === CharCode.DollarSign) { + if (ctx.peek() === CharCode.OpenCurlyBrace) { + ctx.advance(1); + ctx.endToken(); + parseRoot(ctx); + ctx.beginToken(StandardTokenType.String, -1); + } + } + } + + ctx.endToken(); +} + +function parseSlash(ctx: ParserContext): void { + + const nextCh = ctx.peek(1); + if (nextCh === CharCode.Asterisk) { + parseMultiLineComment(ctx); + return; + } + + if (nextCh === CharCode.Slash) { + parseSingleLineComment(ctx); + return; + } + + if (tryParseRegex(ctx)) { + return; + } + + ctx.advance(1); +} + +function tryParseRegex(ctx: ParserContext): boolean { + // See https://www.ecma-international.org/ecma-262/10.0/index.html#prod-RegularExpressionLiteral + + // TODO: avoid regex... + let contentBefore = ctx.text.substr(ctx.pos - 100, 100); + if (/[a-zA-Z0-9](\s*)$/.test(contentBefore)) { + // Cannot start after an identifier + return false; + } + + let pos = 0; + let len = ctx.len - ctx.pos; + let inClass = false; + + // skip / + pos++; + + while (pos < len) { + const ch = ctx.peek(pos++); + + if (ch === CharCode.CarriageReturn || ch === CharCode.LineFeed) { + return false; + } + + if (ch === CharCode.Backslash) { + const nextCh = ctx.peek(); + if (nextCh === CharCode.CarriageReturn || nextCh === CharCode.LineFeed) { + return false; + } + // skip next character + pos++; + continue; + } + + if (inClass) { + + if (ch === CharCode.CloseSquareBracket) { + inClass = false; + continue; + } + + } else { + + if (ch === CharCode.Slash) { + // cannot be directly followed by a / + if (ctx.peek(pos) === CharCode.Slash) { + return false; + } + + // consume flags + do { + let nextCh = ctx.peek(pos); + if (nextCh >= CharCode.a && nextCh <= CharCode.z) { + pos++; + continue; + } else { + break; + } + } while (true); + + // TODO: avoid regex... + if (/^(\s*)(\.|;|\/|,|\)|\]|\}|$)/.test(ctx.text.substr(ctx.pos + pos))) { + // Must be followed by an operator of kinds + ctx.beginToken(StandardTokenType.RegEx); + ctx.advance(pos); + ctx.endToken(); + return true; + } + + return false; + } + + if (ch === CharCode.OpenSquareBracket) { + inClass = true; + continue; + } + + } + } + + return false; +} + +function parseMultiLineComment(ctx: ParserContext): void { + ctx.beginToken(StandardTokenType.Comment); + + // skip the /* + ctx.advance(2); + + while (!ctx.eof()) { + const ch = ctx.next(); + if (ch === CharCode.Asterisk) { + if (ctx.peek() === CharCode.Slash) { + ctx.advance(1); + break; + } + } + } + + ctx.endToken(); +} + +function parseSingleLineComment(ctx: ParserContext): void { + ctx.beginToken(StandardTokenType.Comment); + + // skip the // + ctx.advance(2); + + while (!ctx.eof()) { + const ch = ctx.next(); + if (ch === CharCode.CarriageReturn || ch === CharCode.LineFeed) { + break; + } + } + + ctx.endToken(); +} diff --git a/src/vs/editor/common/services/markerDecorationsServiceImpl.ts b/src/vs/editor/common/services/markerDecorationsServiceImpl.ts index a319e8004b6..3da62fed16f 100644 --- a/src/vs/editor/common/services/markerDecorationsServiceImpl.ts +++ b/src/vs/editor/common/services/markerDecorationsServiceImpl.ts @@ -221,6 +221,9 @@ export class MarkerDecorationsService extends Disposable implements IMarkerDecor if (marker.tags.indexOf(MarkerTag.Unnecessary) !== -1) { inlineClassName = ClassName.EditorUnnecessaryInlineDecoration; } + if (marker.tags.indexOf(MarkerTag.Deprecated) !== -1) { + inlineClassName = ClassName.EditorDeprecatedInlineDecoration; + } } return { diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index df80861b2a8..46d295d2398 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -7,7 +7,8 @@ export enum MarkerTag { - Unnecessary = 1 + Unnecessary = 1, + Deprecated = 2 } export enum MarkerSeverity { diff --git a/src/vs/editor/contrib/referenceSearch/referenceSearch.ts b/src/vs/editor/contrib/referenceSearch/referenceSearch.ts index 22fb4882f5d..97d8aa5dbf0 100644 --- a/src/vs/editor/contrib/referenceSearch/referenceSearch.ts +++ b/src/vs/editor/contrib/referenceSearch/referenceSearch.ts @@ -27,6 +27,7 @@ import { CommandsRegistry, ICommandHandler } from 'vs/platform/commands/common/c import { URI } from 'vs/base/common/uri'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { coalesce, flatten } from 'vs/base/common/arrays'; export const defaultReferenceSearchOptions: RequestOptions = { getMetaTitle(model) { @@ -287,15 +288,7 @@ export function provideReferences(model: ITextModel, position: Position, token: }); }); - return Promise.all(promises).then(references => { - let result: Location[] = []; - for (let ref of references) { - if (ref) { - result.push(...ref); - } - } - return result; - }); + return Promise.all(promises).then(references => flatten(coalesce(references))); } registerDefaultLanguageCommand('_executeReferenceProvider', (model, position) => provideReferences(model, position, CancellationToken.None)); diff --git a/src/vs/editor/contrib/tokenization/tokenization.ts b/src/vs/editor/contrib/tokenization/tokenization.ts index d24591b4eb9..87e8ab76019 100644 --- a/src/vs/editor/contrib/tokenization/tokenization.ts +++ b/src/vs/editor/contrib/tokenization/tokenization.ts @@ -7,8 +7,6 @@ import * as nls from 'vs/nls'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, ServicesAccessor, registerEditorAction } from 'vs/editor/browser/editorExtensions'; import { StopWatch } from 'vs/base/common/stopwatch'; -import { StandardTokenType } from 'vs/editor/common/modes'; -import { ITextModel } from 'vs/editor/common/model'; class ForceRetokenizeAction extends EditorAction { constructor() { @@ -31,51 +29,6 @@ class ForceRetokenizeAction extends EditorAction { sw.stop(); console.log(`tokenization took ${sw.elapsed()}`); - if (!true) { - extractTokenTypes(model); - } - } -} - -function extractTokenTypes(model: ITextModel): void { - const eolLength = model.getEOL().length; - let result: number[] = []; - let resultLen: number = 0; - let lastTokenType: StandardTokenType = StandardTokenType.Other; - let lastEndOffset: number = 0; - let offset = 0; - for (let lineNumber = 1, lineCount = model.getLineCount(); lineNumber <= lineCount; lineNumber++) { - const lineTokens = model.getLineTokens(lineNumber); - - for (let i = 0, len = lineTokens.getCount(); i < len; i++) { - const tokenType = lineTokens.getStandardTokenType(i); - if (tokenType === StandardTokenType.Other) { - continue; - } - - const startOffset = offset + lineTokens.getStartOffset(i); - const endOffset = offset + lineTokens.getEndOffset(i); - const length = endOffset - startOffset; - - if (length === 0) { - continue; - } - - if (lastTokenType === tokenType && lastEndOffset === startOffset) { - result[resultLen - 2] += length; - lastEndOffset += length; - continue; - } - - result[resultLen++] = startOffset; // - lastEndOffset - result[resultLen++] = length; - result[resultLen++] = tokenType; - - lastTokenType = tokenType; - lastEndOffset = endOffset; - } - - offset += lineTokens.getLineContent().length + eolLength; } } diff --git a/src/vs/editor/standalone/browser/simpleServices.ts b/src/vs/editor/standalone/browser/simpleServices.ts index 576092c9865..d333ce03746 100644 --- a/src/vs/editor/standalone/browser/simpleServices.ts +++ b/src/vs/editor/standalone/browser/simpleServices.ts @@ -356,7 +356,7 @@ export class StandaloneKeybindingService extends AbstractKeybindingService { private _toNormalizedKeybindingItems(items: IKeybindingItem[], isDefault: boolean): ResolvedKeybindingItem[] { let result: ResolvedKeybindingItem[] = [], resultLen = 0; for (const item of items) { - const when = (item.when ? item.when.normalize() : undefined); + const when = item.when || undefined; const keybinding = item.keybinding; if (!keybinding) { diff --git a/src/vs/editor/standalone/common/monarch/monarchLexer.ts b/src/vs/editor/standalone/common/monarch/monarchLexer.ts index 2567c561f7c..a5c97ad6f28 100644 --- a/src/vs/editor/standalone/common/monarch/monarchLexer.ts +++ b/src/vs/editor/standalone/common/monarch/monarchLexer.ts @@ -857,6 +857,11 @@ export class MonarchTokenizer implements modes.ITokenizationSupport { return null; } + if (mimetypeOrModeId === this._modeId) { + // embedding myself... + return mimetypeOrModeId; + } + let modeId = this._modeService.getModeId(mimetypeOrModeId); if (modeId) { diff --git a/src/vs/editor/test/node/classification/typescript-test.ts b/src/vs/editor/test/node/classification/typescript-test.ts new file mode 100644 index 00000000000..f8c68e4ee85 --- /dev/null +++ b/src/vs/editor/test/node/classification/typescript-test.ts @@ -0,0 +1,71 @@ +/// +/* tslint:disable */ +const x01 = "string"; +/// ^^^^^^^^ string + +const x02 = '\''; +/// ^^^^ string + +const x03 = '\n\'\t'; +/// ^^^^^^^^ string + +const x04 = 'this is\ +/// ^^^^^^^^^ string\ +a multiline string'; +/// <------------------- string + +const x05 = x01;// just some text +/// ^^^^^^^^^^^^^^^^^ comment + +const x06 = x05;/* multi +/// ^^^^^^^^ comment +line *comment */ +/// <---------------- comment + +const x07 = 4 / 5; + +const x08 = `howdy`; +/// ^^^^^^^ string + +const x09 = `\'\"\``; +/// ^^^^^^^^ string + +const x10 = `$[]`; +/// ^^^^^ string + +const x11 = `${x07 +/**/3}px`; +/// ^^^ string +/// ^^^^ comment +/// ^^^^ string + +const x12 = `${x07 + (function () { return 5; })()/**/}px`; +/// ^^^ string +/// ^^^^ comment +/// ^^^^ string + +const x13 = /([\w\-]+)?(#([\w\-]+))?((.([\w\-]+))*)/; +/// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ regex + +const x14 = /\./g; +/// ^^^^^ regex + + +const x15 = Math.abs(x07) / x07; // speed +/// ^^^^^^^^ comment + +const x16 = / x07; /.test('3'); +/// ^^^^^^^^ regex +/// ^^^ string + +const x17 = `.dialog-modal-block${true ? '.dimmed' : ''}`; +/// ^^^^^^^^^^^^^^^^^^^^^^ string +/// ^^^^^^^^^ string +/// ^^^^ string + +const x18 = Math.min((14 <= 0.5 ? 123 / (2 * 1) : ''.length / (2 - (2 * 1))), 1); +/// ^^ string + +const x19 = `${3 / '5'.length} km/h)`; +/// ^^^ string +/// ^^^ string +/// ^^^^^^^ string diff --git a/src/vs/editor/test/node/classification/typescript.test.ts b/src/vs/editor/test/node/classification/typescript.test.ts new file mode 100644 index 00000000000..0817a93198d --- /dev/null +++ b/src/vs/editor/test/node/classification/typescript.test.ts @@ -0,0 +1,139 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { StandardTokenType } from 'vs/editor/common/modes'; +import * as fs from 'fs'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; +import { parse } from 'vs/editor/common/modes/tokenization/typescript'; +import { toStandardTokenType } from 'vs/editor/common/modes/supports/tokenization'; + +interface IParseFunc { + (text: string): number[]; +} + +interface IAssertion { + testLineNumber: number; + startOffset: number; + length: number; + tokenType: StandardTokenType; +} + +interface ITest { + content: string; + assertions: IAssertion[]; +} + +function parseTest(fileName: string): ITest { + interface ILineWithAssertions { + line: string; + assertions: ILineAssertion[]; + } + + interface ILineAssertion { + testLineNumber: number; + startOffset: number; + length: number; + expectedTokenType: StandardTokenType; + } + + const testContents = fs.readFileSync(fileName).toString(); + const lines = testContents.split(/\r\n|\n/); + const magicToken = lines[0]; + + let currentElement: ILineWithAssertions = { + line: lines[1], + assertions: [] + }; + + let parsedTest: ILineWithAssertions[] = []; + for (let i = 2; i < lines.length; i++) { + let line = lines[i]; + if (line.substr(0, magicToken.length) === magicToken) { + // this is an assertion line + let m1 = line.substr(magicToken.length).match(/^( +)([\^]+) (\w+)\\?$/); + if (m1) { + currentElement.assertions.push({ + testLineNumber: i + 1, + startOffset: magicToken.length + m1[1].length, + length: m1[2].length, + expectedTokenType: toStandardTokenType(m1[3]) + }); + } else { + let m2 = line.substr(magicToken.length).match(/^( +)<(-+) (\w+)\\?$/); + if (m2) { + currentElement.assertions.push({ + testLineNumber: i + 1, + startOffset: 0, + length: m2[2].length, + expectedTokenType: toStandardTokenType(m2[3]) + }); + } else { + throw new Error(`Invalid test line at line number ${i + 1}.`); + } + } + } else { + // this is a line to be parsed + parsedTest.push(currentElement); + currentElement = { + line: line, + assertions: [] + }; + } + } + parsedTest.push(currentElement); + + let assertions: IAssertion[] = []; + + let offset = 0; + for (let i = 0; i < parsedTest.length; i++) { + const parsedTestLine = parsedTest[i]; + for (let j = 0; j < parsedTestLine.assertions.length; j++) { + const assertion = parsedTestLine.assertions[j]; + assertions.push({ + testLineNumber: assertion.testLineNumber, + startOffset: offset + assertion.startOffset, + length: assertion.length, + tokenType: assertion.expectedTokenType + }); + } + offset += parsedTestLine.line.length + 1; + } + + let content: string = parsedTest.map(parsedTestLine => parsedTestLine.line).join('\n'); + + return { content, assertions }; +} + +function executeTest(fileName: string, parseFunc: IParseFunc): void { + const { content, assertions } = parseTest(fileName); + const actual = parseFunc(content); + + let actualIndex = 0, actualCount = actual.length / 3; + for (let i = 0; i < assertions.length; i++) { + const assertion = assertions[i]; + while (actualIndex < actualCount && actual[3 * actualIndex] + actual[3 * actualIndex + 1] <= assertion.startOffset) { + actualIndex++; + } + assert.ok( + actual[3 * actualIndex] <= assertion.startOffset, + `Line ${assertion.testLineNumber} : startOffset : ${actual[3 * actualIndex]} <= ${assertion.startOffset}` + ); + assert.ok( + actual[3 * actualIndex] + actual[3 * actualIndex + 1] >= assertion.startOffset + assertion.length, + `Line ${assertion.testLineNumber} : length : ${actual[3 * actualIndex]} + ${actual[3 * actualIndex + 1]} >= ${assertion.startOffset} + ${assertion.length}.` + ); + assert.equal( + actual[3 * actualIndex + 2], + assertion.tokenType, + `Line ${assertion.testLineNumber} : tokenType`); + } +} + +suite('Classification', () => { + test('TypeScript', () => { + executeTest(getPathFromAmdModule(require, 'vs/editor/test/node/classification/typescript-test.ts').replace(/\bout\b/, 'src'), parse); + }); +}); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index b64d6701d72..c6bc300217c 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -29,7 +29,8 @@ declare namespace monaco { export enum MarkerTag { - Unnecessary = 1 + Unnecessary = 1, + Deprecated = 2 } export enum MarkerSeverity { diff --git a/src/vs/platform/browser/contextScopedHistoryWidget.ts b/src/vs/platform/browser/contextScopedHistoryWidget.ts index 94d4de658b5..5e5e8603683 100644 --- a/src/vs/platform/browser/contextScopedHistoryWidget.ts +++ b/src/vs/platform/browser/contextScopedHistoryWidget.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IContextKeyService, ContextKeyDefinedExpr, ContextKeyExpr, ContextKeyAndExpr, ContextKeyEqualsExpr, RawContextKey, IContextKey, IContextKeyServiceTarget } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, ContextKeyExpr, RawContextKey, IContextKey, IContextKeyServiceTarget } from 'vs/platform/contextkey/common/contextkey'; import { HistoryInputBox, IHistoryInputOptions } from 'vs/base/browser/ui/inputbox/inputBox'; import { FindInput, IFindInputOptions } from 'vs/base/browser/ui/findinput/findInput'; import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; @@ -66,7 +66,7 @@ export class ContextScopedFindInput extends FindInput { KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'history.showPrevious', weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(new ContextKeyDefinedExpr(HistoryNavigationWidgetContext), new ContextKeyEqualsExpr(HistoryNavigationEnablementContext, true)), + when: ContextKeyExpr.and(ContextKeyExpr.has(HistoryNavigationWidgetContext), ContextKeyExpr.equals(HistoryNavigationEnablementContext, true)), primary: KeyCode.UpArrow, secondary: [KeyMod.Alt | KeyCode.UpArrow], handler: (accessor, arg2) => { @@ -81,7 +81,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'history.showNext', weight: KeybindingWeight.WorkbenchContrib, - when: new ContextKeyAndExpr([new ContextKeyDefinedExpr(HistoryNavigationWidgetContext), new ContextKeyEqualsExpr(HistoryNavigationEnablementContext, true)]), + when: ContextKeyExpr.and(ContextKeyExpr.has(HistoryNavigationWidgetContext), ContextKeyExpr.equals(HistoryNavigationEnablementContext, true)), primary: KeyCode.DownArrow, secondary: [KeyMod.Alt | KeyCode.DownArrow], handler: (accessor, arg2) => { diff --git a/src/vs/platform/contextkey/common/contextkey.ts b/src/vs/platform/contextkey/common/contextkey.ts index 289ba8668ce..64025a4249f 100644 --- a/src/vs/platform/contextkey/common/contextkey.ts +++ b/src/vs/platform/contextkey/common/contextkey.ts @@ -13,41 +13,47 @@ export const enum ContextKeyExprType { Equals = 3, NotEquals = 4, And = 5, - Regex = 6 + Regex = 6, + NotRegex = 7, + Or = 8 } export interface IContextKeyExprMapper { - mapDefined(key: string): ContextKeyDefinedExpr; - mapNot(key: string): ContextKeyNotExpr; - mapEquals(key: string, value: any): ContextKeyEqualsExpr; - mapNotEquals(key: string, value: any): ContextKeyNotEqualsExpr; + mapDefined(key: string): ContextKeyExpr; + mapNot(key: string): ContextKeyExpr; + mapEquals(key: string, value: any): ContextKeyExpr; + mapNotEquals(key: string, value: any): ContextKeyExpr; mapRegex(key: string, regexp: RegExp | null): ContextKeyRegexExpr; } export abstract class ContextKeyExpr { public static has(key: string): ContextKeyExpr { - return new ContextKeyDefinedExpr(key); + return ContextKeyDefinedExpr.create(key); } public static equals(key: string, value: any): ContextKeyExpr { - return new ContextKeyEqualsExpr(key, value); + return ContextKeyEqualsExpr.create(key, value); } public static notEquals(key: string, value: any): ContextKeyExpr { - return new ContextKeyNotEqualsExpr(key, value); + return ContextKeyNotEqualsExpr.create(key, value); } public static regex(key: string, value: RegExp): ContextKeyExpr { - return new ContextKeyRegexExpr(key, value); + return ContextKeyRegexExpr.create(key, value); } public static not(key: string): ContextKeyExpr { - return new ContextKeyNotExpr(key); + return ContextKeyNotExpr.create(key); } - public static and(...expr: Array): ContextKeyExpr { - return new ContextKeyAndExpr(expr); + public static and(...expr: Array): ContextKeyExpr | undefined { + return ContextKeyAndExpr.create(expr); + } + + public static or(...expr: Array): ContextKeyExpr | undefined { + return ContextKeyOrExpr.create(expr); } public static deserialize(serialized: string | null | undefined, strict: boolean = false): ContextKeyExpr | undefined { @@ -55,9 +61,17 @@ export abstract class ContextKeyExpr { return undefined; } + return this._deserializeOrExpression(serialized, strict); + } + + private static _deserializeOrExpression(serialized: string, strict: boolean): ContextKeyExpr | undefined { + let pieces = serialized.split('||'); + return ContextKeyOrExpr.create(pieces.map(p => this._deserializeAndExpression(p, strict))); + } + + private static _deserializeAndExpression(serialized: string, strict: boolean): ContextKeyExpr | undefined { let pieces = serialized.split('&&'); - let result = new ContextKeyAndExpr(pieces.map(p => this._deserializeOne(p, strict))); - return result.normalize(); + return ContextKeyAndExpr.create(pieces.map(p => this._deserializeOne(p, strict))); } private static _deserializeOne(serializedOne: string, strict: boolean): ContextKeyExpr { @@ -65,24 +79,24 @@ export abstract class ContextKeyExpr { if (serializedOne.indexOf('!=') >= 0) { let pieces = serializedOne.split('!='); - return new ContextKeyNotEqualsExpr(pieces[0].trim(), this._deserializeValue(pieces[1], strict)); + return ContextKeyNotEqualsExpr.create(pieces[0].trim(), this._deserializeValue(pieces[1], strict)); } if (serializedOne.indexOf('==') >= 0) { let pieces = serializedOne.split('=='); - return new ContextKeyEqualsExpr(pieces[0].trim(), this._deserializeValue(pieces[1], strict)); + return ContextKeyEqualsExpr.create(pieces[0].trim(), this._deserializeValue(pieces[1], strict)); } if (serializedOne.indexOf('=~') >= 0) { let pieces = serializedOne.split('=~'); - return new ContextKeyRegexExpr(pieces[0].trim(), this._deserializeRegexValue(pieces[1], strict)); + return ContextKeyRegexExpr.create(pieces[0].trim(), this._deserializeRegexValue(pieces[1], strict)); } if (/^\!\s*/.test(serializedOne)) { - return new ContextKeyNotExpr(serializedOne.substr(1).trim()); + return ContextKeyNotExpr.create(serializedOne.substr(1).trim()); } - return new ContextKeyDefinedExpr(serializedOne); + return ContextKeyDefinedExpr.create(serializedOne); } private static _deserializeValue(serializedValue: string, strict: boolean): any { @@ -143,10 +157,10 @@ export abstract class ContextKeyExpr { public abstract getType(): ContextKeyExprType; public abstract equals(other: ContextKeyExpr): boolean; public abstract evaluate(context: IContext): boolean; - public abstract normalize(): ContextKeyExpr | undefined; 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 { @@ -166,13 +180,21 @@ function cmp(a: ContextKeyExpr, b: ContextKeyExpr): number { 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!'); } } export class ContextKeyDefinedExpr implements ContextKeyExpr { - constructor(protected key: string) { + public static create(key: string): ContextKeyDefinedExpr { + return new ContextKeyDefinedExpr(key); + } + + protected constructor(protected key: string) { } public getType(): ContextKeyExprType { @@ -200,10 +222,6 @@ export class ContextKeyDefinedExpr implements ContextKeyExpr { return (!!context.getValue(this.key)); } - public normalize(): ContextKeyExpr { - return this; - } - public serialize(): string { return this.key; } @@ -215,10 +233,25 @@ export class ContextKeyDefinedExpr implements ContextKeyExpr { public map(mapFnc: IContextKeyExprMapper): ContextKeyExpr { return mapFnc.mapDefined(this.key); } + + public negate(): ContextKeyExpr { + return ContextKeyNotExpr.create(this.key); + } } export class ContextKeyEqualsExpr implements ContextKeyExpr { - constructor(private readonly key: string, private readonly value: any) { + + public static create(key: string, value: any): ContextKeyExpr { + if (typeof value === 'boolean') { + if (value) { + return ContextKeyDefinedExpr.create(key); + } + return ContextKeyNotExpr.create(key); + } + return new ContextKeyEqualsExpr(key, value); + } + + private constructor(private readonly key: string, private readonly value: any) { } public getType(): ContextKeyExprType { @@ -255,21 +288,7 @@ export class ContextKeyEqualsExpr implements ContextKeyExpr { /* tslint:enable:triple-equals */ } - public normalize(): ContextKeyExpr { - if (typeof this.value === 'boolean') { - if (this.value) { - return new ContextKeyDefinedExpr(this.key); - } - return new ContextKeyNotExpr(this.key); - } - return this; - } - public serialize(): string { - if (typeof this.value === 'boolean') { - return this.normalize().serialize(); - } - return this.key + ' == \'' + this.value + '\''; } @@ -280,10 +299,25 @@ export class ContextKeyEqualsExpr implements ContextKeyExpr { public map(mapFnc: IContextKeyExprMapper): ContextKeyExpr { return mapFnc.mapEquals(this.key, this.value); } + + public negate(): ContextKeyExpr { + return ContextKeyNotEqualsExpr.create(this.key, this.value); + } } export class ContextKeyNotEqualsExpr implements ContextKeyExpr { - constructor(private key: string, private value: any) { + + public static create(key: string, value: any): ContextKeyExpr { + if (typeof value === 'boolean') { + if (value) { + return ContextKeyNotExpr.create(key); + } + return ContextKeyDefinedExpr.create(key); + } + return new ContextKeyNotEqualsExpr(key, value); + } + + private constructor(private key: string, private value: any) { } public getType(): ContextKeyExprType { @@ -320,21 +354,7 @@ export class ContextKeyNotEqualsExpr implements ContextKeyExpr { /* tslint:enable:triple-equals */ } - public normalize(): ContextKeyExpr { - if (typeof this.value === 'boolean') { - if (this.value) { - return new ContextKeyNotExpr(this.key); - } - return new ContextKeyDefinedExpr(this.key); - } - return this; - } - public serialize(): string { - if (typeof this.value === 'boolean') { - return this.normalize().serialize(); - } - return this.key + ' != \'' + this.value + '\''; } @@ -345,10 +365,19 @@ export class ContextKeyNotEqualsExpr implements ContextKeyExpr { public map(mapFnc: IContextKeyExprMapper): ContextKeyExpr { return mapFnc.mapNotEquals(this.key, this.value); } + + public negate(): ContextKeyExpr { + return ContextKeyEqualsExpr.create(this.key, this.value); + } } export class ContextKeyNotExpr implements ContextKeyExpr { - constructor(private key: string) { + + public static create(key: string): ContextKeyExpr { + return new ContextKeyNotExpr(key); + } + + private constructor(private key: string) { } public getType(): ContextKeyExprType { @@ -376,10 +405,6 @@ export class ContextKeyNotExpr implements ContextKeyExpr { return (!context.getValue(this.key)); } - public normalize(): ContextKeyExpr { - return this; - } - public serialize(): string { return '!' + this.key; } @@ -391,11 +416,19 @@ export class ContextKeyNotExpr implements ContextKeyExpr { public map(mapFnc: IContextKeyExprMapper): ContextKeyExpr { return mapFnc.mapNot(this.key); } + + public negate(): ContextKeyExpr { + return ContextKeyDefinedExpr.create(this.key); + } } export class ContextKeyRegexExpr implements ContextKeyExpr { - constructor(private key: string, private regexp: RegExp | null) { + public static create(key: string, regexp: RegExp | null): ContextKeyRegexExpr { + return new ContextKeyRegexExpr(key, regexp); + } + + private constructor(private key: string, private regexp: RegExp | null) { // } @@ -435,10 +468,6 @@ export class ContextKeyRegexExpr implements ContextKeyExpr { return this.regexp ? this.regexp.test(value) : false; } - public normalize(): ContextKeyExpr { - return this; - } - public serialize(): string { const value = this.regexp ? `/${this.regexp.source}/${this.regexp.ignoreCase ? 'i' : ''}` @@ -450,22 +479,99 @@ export class ContextKeyRegexExpr implements ContextKeyExpr { return [this.key]; } - public map(mapFnc: IContextKeyExprMapper): ContextKeyExpr { + public map(mapFnc: IContextKeyExprMapper): ContextKeyRegexExpr { return mapFnc.mapRegex(this.key, this.regexp); } + + public negate(): ContextKeyExpr { + return ContextKeyNotRegexExpr.create(this); + } +} + +export class ContextKeyNotRegexExpr implements ContextKeyExpr { + + public static create(actual: ContextKeyRegexExpr): ContextKeyExpr { + return new ContextKeyNotRegexExpr(actual); + } + + private constructor(private readonly _actual: ContextKeyRegexExpr) { + // + } + + public getType(): ContextKeyExprType { + return ContextKeyExprType.NotRegex; + } + + public cmp(other: ContextKeyNotRegexExpr): number { + return this._actual.cmp(other._actual); + } + + public equals(other: ContextKeyExpr): boolean { + if (other instanceof ContextKeyNotRegexExpr) { + return this._actual.equals(other._actual); + } + return false; + } + + public evaluate(context: IContext): boolean { + return !this._actual.evaluate(context); + } + + public serialize(): string { + throw new Error('Method not implemented.'); + } + + public keys(): string[] { + return this._actual.keys(); + } + + public map(mapFnc: IContextKeyExprMapper): ContextKeyExpr { + return new ContextKeyNotRegexExpr(this._actual.map(mapFnc)); + } + + public negate(): ContextKeyExpr { + return this._actual; + } } export class ContextKeyAndExpr implements ContextKeyExpr { - public readonly expr: ContextKeyExpr[]; - constructor(expr: Array) { - this.expr = ContextKeyAndExpr._normalizeArr(expr); + public static create(_expr: Array): ContextKeyExpr | undefined { + const expr = ContextKeyAndExpr._normalizeArr(_expr); + if (expr.length === 0) { + return undefined; + } + + if (expr.length === 1) { + return expr[0]; + } + + return new ContextKeyAndExpr(expr); + } + + private constructor(public readonly expr: ContextKeyExpr[]) { } public getType(): ContextKeyExprType { return ContextKeyExprType.And; } + public cmp(other: ContextKeyAndExpr): number { + 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 ContextKeyAndExpr) { if (this.expr.length !== other.expr.length) { @@ -500,16 +606,16 @@ export class ContextKeyAndExpr implements ContextKeyExpr { continue; } - e = e.normalize(); - if (!e) { - continue; - } - if (e instanceof ContextKeyAndExpr) { expr = expr.concat(e.expr); continue; } + if (e instanceof ContextKeyOrExpr) { + // 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!`); + } + expr.push(e); } @@ -519,29 +625,7 @@ export class ContextKeyAndExpr implements ContextKeyExpr { return expr; } - public normalize(): ContextKeyExpr | undefined { - if (this.expr.length === 0) { - return undefined; - } - - if (this.expr.length === 1) { - return this.expr[0]; - } - - return this; - } - public serialize(): string { - if (this.expr.length === 0) { - return ''; - } - if (this.expr.length === 1) { - const normalized = this.normalize(); - if (!normalized) { - return ''; - } - return normalized.serialize(); - } return this.expr.map(e => e.serialize()).join(' && '); } @@ -556,6 +640,132 @@ export class ContextKeyAndExpr implements ContextKeyExpr { public map(mapFnc: IContextKeyExprMapper): ContextKeyExpr { return new ContextKeyAndExpr(this.expr.map(expr => expr.map(mapFnc))); } + + public negate(): ContextKeyExpr { + let result: ContextKeyExpr[] = []; + for (let expr of this.expr) { + result.push(expr.negate()); + } + return ContextKeyOrExpr.create(result)!; + } +} + +export class ContextKeyOrExpr implements ContextKeyExpr { + + public static create(_expr: Array): ContextKeyExpr | undefined { + const expr = ContextKeyOrExpr._normalizeArr(_expr); + if (expr.length === 0) { + return undefined; + } + + if (expr.length === 1) { + return expr[0]; + } + + return new ContextKeyOrExpr(expr); + } + + private constructor(public readonly expr: ContextKeyExpr[]) { + } + + public getType(): ContextKeyExprType { + return ContextKeyExprType.Or; + } + + public equals(other: ContextKeyExpr): boolean { + if (other instanceof ContextKeyOrExpr) { + if (this.expr.length !== other.expr.length) { + return false; + } + for (let i = 0, len = this.expr.length; i < len; i++) { + if (!this.expr[i].equals(other.expr[i])) { + return false; + } + } + return true; + } + return false; + } + + public evaluate(context: IContext): boolean { + for (let i = 0, len = this.expr.length; i < len; i++) { + if (this.expr[i].evaluate(context)) { + return true; + } + } + return false; + } + + private static _normalizeArr(arr: Array): ContextKeyExpr[] { + let expr: ContextKeyExpr[] = []; + + if (arr) { + for (let i = 0, len = arr.length; i < len; i++) { + let e: ContextKeyExpr | null | undefined = arr[i]; + if (!e) { + continue; + } + + if (e instanceof ContextKeyOrExpr) { + expr = expr.concat(e.expr); + continue; + } + + expr.push(e); + } + + expr.sort(cmp); + } + + return expr; + } + + public serialize(): string { + return this.expr.map(e => e.serialize()).join(' || '); + } + + public keys(): string[] { + const result: string[] = []; + for (let expr of this.expr) { + result.push(...expr.keys()); + } + return result; + } + + public map(mapFnc: IContextKeyExprMapper): ContextKeyExpr { + return new ContextKeyOrExpr(this.expr.map(expr => expr.map(mapFnc))); + } + + public negate(): ContextKeyExpr { + let result: ContextKeyExpr[] = []; + for (let expr of this.expr) { + result.push(expr.negate()); + } + + const terminals = (node: ContextKeyExpr) => { + if (node instanceof ContextKeyOrExpr) { + return node.expr; + } + return [node]; + }; + + // We don't support parens, so here we distribute the AND over the OR terminals + // We always take the first 2 AND pairs and distribute them + while (result.length > 1) { + const LEFT = result.shift()!; + const RIGHT = result.shift()!; + + const all: ContextKeyExpr[] = []; + for (const left of terminals(LEFT)) { + for (const right of terminals(RIGHT)) { + all.push(ContextKeyExpr.and(left, right)!); + } + } + result.unshift(ContextKeyExpr.or(...all)!); + } + + return result[0]; + } } export class RawContextKey extends ContextKeyDefinedExpr { diff --git a/src/vs/platform/contextkey/test/common/contextkey.test.ts b/src/vs/platform/contextkey/test/common/contextkey.test.ts index cb12470e3c2..9db3782da31 100644 --- a/src/vs/platform/contextkey/test/common/contextkey.test.ts +++ b/src/vs/platform/contextkey/test/common/contextkey.test.ts @@ -27,7 +27,7 @@ suite('ContextKeyExpr', () => { ContextKeyExpr.notEquals('c2', 'cc2'), ContextKeyExpr.not('d1'), ContextKeyExpr.not('d2') - ); + )!; let b = ContextKeyExpr.and( ContextKeyExpr.equals('b2', 'bb2'), ContextKeyExpr.notEquals('c1', 'cc1'), @@ -40,7 +40,7 @@ suite('ContextKeyExpr', () => { ContextKeyExpr.has('a1'), ContextKeyExpr.and(ContextKeyExpr.equals('and.a', true)), ContextKeyExpr.not('d2') - ); + )!; assert(a.equals(b), 'expressions should be equal'); }); @@ -50,10 +50,10 @@ suite('ContextKeyExpr', () => { let key1IsFalse = ContextKeyExpr.equals('key1', false); let key1IsNotTrue = ContextKeyExpr.notEquals('key1', true); - assert.ok(key1IsTrue.normalize()!.equals(ContextKeyExpr.has('key1'))); - assert.ok(key1IsNotFalse.normalize()!.equals(ContextKeyExpr.has('key1'))); - assert.ok(key1IsFalse.normalize()!.equals(ContextKeyExpr.not('key1'))); - assert.ok(key1IsNotTrue.normalize()!.equals(ContextKeyExpr.not('key1'))); + assert.ok(key1IsTrue.equals(ContextKeyExpr.has('key1'))); + assert.ok(key1IsNotFalse.equals(ContextKeyExpr.has('key1'))); + assert.ok(key1IsFalse.equals(ContextKeyExpr.not('key1'))); + assert.ok(key1IsNotTrue.equals(ContextKeyExpr.not('key1'))); }); test('evaluate', () => { @@ -93,5 +93,24 @@ suite('ContextKeyExpr', () => { testExpression('a && !b && c == 5', true && !false && '5' == '5'); testExpression('d =~ /e.*/', false); /* tslint:enable:triple-equals */ + + // precedence test: false && true || true === true because && is evaluated first + testExpression('b && a || a', true); + + testExpression('a || b', true); + testExpression('b || b', false); + testExpression('b && a || a && b', false); + }); + + test('negate', () => { + function testNegate(expr: string, expected: string): void { + const actual = ContextKeyExpr.deserialize(expr)!.negate().serialize(); + assert.strictEqual(actual, expected); + } + 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'); }); }); diff --git a/src/vs/platform/keybinding/common/keybindingResolver.ts b/src/vs/platform/keybinding/common/keybindingResolver.ts index 1436cfa6604..29d559eab4d 100644 --- a/src/vs/platform/keybinding/common/keybindingResolver.ts +++ b/src/vs/platform/keybinding/common/keybindingResolver.ts @@ -6,7 +6,7 @@ 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 { ContextKeyAndExpr, ContextKeyExpr, IContext } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, IContext, ContextKeyOrExpr } from 'vs/platform/contextkey/common/contextkey'; import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem'; import { keys } from 'vs/base/common/map'; @@ -171,7 +171,6 @@ export class KeybindingResolver { /** * Returns true if it is provable `a` implies `b`. - * **Precondition**: Assumes `a` and `b` are normalized! */ public static whenIsEntirelyIncluded(a: ContextKeyExpr | null | undefined, b: ContextKeyExpr | null | undefined): boolean { if (!b) { @@ -181,26 +180,35 @@ export class KeybindingResolver { return false; } - const aExpressions: ContextKeyExpr[] = ((a instanceof ContextKeyAndExpr) ? a.expr : [a]); - const bExpressions: ContextKeyExpr[] = ((b instanceof ContextKeyAndExpr) ? b.expr : [b]); + return this._implies(a, b); + } - let aIndex = 0; - for (const bExpr of bExpressions) { - let bExprMatched = false; - while (!bExprMatched && aIndex < aExpressions.length) { - let aExpr = aExpressions[aIndex]; - if (aExpr.equals(bExpr)) { - bExprMatched = true; - } - aIndex++; + /** + * Returns true if it is provable `p` implies `q`. + */ + private static _implies(p: ContextKeyExpr, q: ContextKeyExpr): boolean { + const notP = p.negate(); + + const terminals = (node: ContextKeyExpr) => { + if (node instanceof ContextKeyOrExpr) { + return node.expr; } + return [node]; + }; - if (!bExprMatched) { - return false; + let expr = terminals(notP).concat(terminals(q)); + for (let i = 0; i < expr.length; i++) { + const a = expr[i]; + const notA = a.negate(); + for (let j = i + 1; j < expr.length; j++) { + const b = expr[j]; + if (notA.equals(b)) { + return true; + } } } - return true; + return false; } public getDefaultBoundCommands(): Map { diff --git a/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts b/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts index a4488cffb17..c85003be1ac 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 { ContextKeyAndExpr, ContextKeyExpr, IContext } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, IContext } 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,13 +20,13 @@ function createContext(ctx: any) { suite('KeybindingResolver', () => { - function kbItem(keybinding: number, command: string, commandArgs: any, when: ContextKeyExpr, isDefault: boolean): ResolvedKeybindingItem { + function kbItem(keybinding: number, command: string, commandArgs: any, when: ContextKeyExpr | undefined, isDefault: boolean): ResolvedKeybindingItem { const resolvedKeybinding = (keybinding !== 0 ? new USLayoutResolvedKeybinding(createKeybinding(keybinding, OS)!, OS) : undefined); return new ResolvedKeybindingItem( resolvedKeybinding, command, commandArgs, - when ? when.normalize() : undefined, + when, isDefault ); } @@ -191,64 +191,41 @@ suite('KeybindingResolver', () => { }); test('contextIsEntirelyIncluded', () => { - let assertIsIncluded = (a: ContextKeyExpr[], b: ContextKeyExpr[]) => { - let tmpA = new ContextKeyAndExpr(a).normalize(); - let tmpB = new ContextKeyAndExpr(b).normalize(); - assert.equal(KeybindingResolver.whenIsEntirelyIncluded(tmpA, tmpB), true); + const assertIsIncluded = (a: string | null, b: string | null) => { + assert.equal(KeybindingResolver.whenIsEntirelyIncluded(ContextKeyExpr.deserialize(a), ContextKeyExpr.deserialize(b)), true); }; - let assertIsNotIncluded = (a: ContextKeyExpr[], b: ContextKeyExpr[]) => { - let tmpA = new ContextKeyAndExpr(a).normalize(); - let tmpB = new ContextKeyAndExpr(b).normalize(); - assert.equal(KeybindingResolver.whenIsEntirelyIncluded(tmpA, tmpB), false); + const assertIsNotIncluded = (a: string | null, b: string | null) => { + assert.equal(KeybindingResolver.whenIsEntirelyIncluded(ContextKeyExpr.deserialize(a), ContextKeyExpr.deserialize(b)), false); }; - let key1IsTrue = ContextKeyExpr.equals('key1', true); - let key1IsNotFalse = ContextKeyExpr.notEquals('key1', false); - let key1IsFalse = ContextKeyExpr.equals('key1', false); - let key1IsNotTrue = ContextKeyExpr.notEquals('key1', true); - let key2IsTrue = ContextKeyExpr.equals('key2', true); - let key2IsNotFalse = ContextKeyExpr.notEquals('key2', false); - let key3IsTrue = ContextKeyExpr.equals('key3', true); - let key4IsTrue = ContextKeyExpr.equals('key4', true); - assertIsIncluded([key1IsTrue], null!); - assertIsIncluded([key1IsTrue], []); - assertIsIncluded([key1IsTrue], [key1IsTrue]); - assertIsIncluded([key1IsTrue], [key1IsNotFalse]); + assertIsIncluded('key1', null); + assertIsIncluded('key1', ''); + assertIsIncluded('key1', 'key1'); + assertIsIncluded('!key1', ''); + assertIsIncluded('!key1', '!key1'); + assertIsIncluded('key2', ''); + assertIsIncluded('key2', 'key2'); + assertIsIncluded('key1 && key1 && key2 && key2', 'key2'); + assertIsIncluded('key1 && key2', 'key2'); + assertIsIncluded('key1 && key2', 'key1'); + assertIsIncluded('key1 && key2', ''); + assertIsIncluded('key1', 'key1 || key2'); + assertIsIncluded('key1 || !key1', 'key2 || !key2'); + assertIsIncluded('key1', 'key1 || key2 && key3'); - assertIsIncluded([key1IsFalse], []); - assertIsIncluded([key1IsFalse], [key1IsFalse]); - assertIsIncluded([key1IsFalse], [key1IsNotTrue]); - - assertIsIncluded([key2IsNotFalse], []); - assertIsIncluded([key2IsNotFalse], [key2IsNotFalse]); - assertIsIncluded([key2IsNotFalse], [key2IsTrue]); - - assertIsIncluded([key1IsTrue, key2IsNotFalse], [key2IsTrue]); - assertIsIncluded([key1IsTrue, key2IsNotFalse], [key2IsNotFalse]); - assertIsIncluded([key1IsTrue, key2IsNotFalse], [key1IsTrue]); - assertIsIncluded([key1IsTrue, key2IsNotFalse], [key1IsNotFalse]); - assertIsIncluded([key1IsTrue, key2IsNotFalse], []); - - assertIsNotIncluded([key1IsTrue], [key1IsFalse]); - assertIsNotIncluded([key1IsTrue], [key1IsNotTrue]); - assertIsNotIncluded([key1IsNotFalse], [key1IsFalse]); - assertIsNotIncluded([key1IsNotFalse], [key1IsNotTrue]); - - assertIsNotIncluded([key1IsFalse], [key1IsTrue]); - assertIsNotIncluded([key1IsFalse], [key1IsNotFalse]); - assertIsNotIncluded([key1IsNotTrue], [key1IsTrue]); - assertIsNotIncluded([key1IsNotTrue], [key1IsNotFalse]); - - assertIsNotIncluded([key1IsTrue, key2IsNotFalse], [key3IsTrue]); - assertIsNotIncluded([key1IsTrue, key2IsNotFalse], [key4IsTrue]); - assertIsNotIncluded([key1IsTrue], [key2IsTrue]); - assertIsNotIncluded([], [key2IsTrue]); - assertIsNotIncluded(null!, [key2IsTrue]); + assertIsNotIncluded('key1', '!key1'); + assertIsNotIncluded('!key1', 'key1'); + assertIsNotIncluded('key1 && key2', 'key3'); + assertIsNotIncluded('key1 && key2', 'key4'); + assertIsNotIncluded('key1', 'key2'); + assertIsNotIncluded('key1 || key2', 'key2'); + assertIsNotIncluded('', 'key2'); + assertIsNotIncluded(null, 'key2'); }); test('resolve command', function () { - function _kbItem(keybinding: number, command: string, when: ContextKeyExpr): ResolvedKeybindingItem { + function _kbItem(keybinding: number, command: string, when: ContextKeyExpr | undefined): ResolvedKeybindingItem { return kbItem(keybinding, command, null, when, true); } diff --git a/src/vs/platform/markers/common/markers.ts b/src/vs/platform/markers/common/markers.ts index 9461b506c21..235c4b12f36 100644 --- a/src/vs/platform/markers/common/markers.ts +++ b/src/vs/platform/markers/common/markers.ts @@ -39,6 +39,7 @@ export interface IRelatedInformation { export const enum MarkerTag { Unnecessary = 1, + Deprecated = 2 } export enum MarkerSeverity { diff --git a/src/vs/platform/product/common/product.ts b/src/vs/platform/product/common/product.ts index 45c31ca0f3d..d39dd65c08f 100644 --- a/src/vs/platform/product/common/product.ts +++ b/src/vs/platform/product/common/product.ts @@ -72,8 +72,8 @@ export interface IProductConfiguration { readonly recommendationsUrl: string; }; extensionTips: { [id: string]: string; }; - extensionImportantTips: { [id: string]: { name: string; pattern: string; }; }; - readonly exeBasedExtensionTips: { [id: string]: { friendlyName: string, windowsPath?: string, recommendations: readonly string[] }; }; + extensionImportantTips: { [id: string]: { name: string; pattern: string; isExtensionPack?: boolean }; }; + readonly exeBasedExtensionTips: { [id: string]: IExeBasedExtensionTip; }; readonly extensionKeywords: { [extension: string]: readonly string[]; }; readonly extensionAllowedBadgeProviders: readonly string[]; readonly extensionAllowedProposedApi: readonly string[]; @@ -120,6 +120,14 @@ export interface IProductConfiguration { readonly uiExtensions?: readonly string[]; } +export interface IExeBasedExtensionTip { + friendlyName: string; + windowsPath?: string; + recommendations: readonly string[]; + important?: boolean; + exeFriendlyName?: string; +} + export interface ISurveyData { surveyId: string; surveyUrl: string; diff --git a/src/vs/platform/request/common/request.ts b/src/vs/platform/request/common/request.ts index ed6c2611166..31e3c314242 100644 --- a/src/vs/platform/request/common/request.ts +++ b/src/vs/platform/request/common/request.ts @@ -90,7 +90,7 @@ Registry.as(Extensions.Configuration) 'http.proxy': { type: 'string', pattern: '^https?://([^:]*(:[^@]*)?@)?([^:]+)(:\\d+)?/?$|^$', - description: localize('proxy', "The proxy setting to use. If not set will be taken from the http_proxy and https_proxy environment variables.") + markdownDescription: localize('proxy', "The proxy setting to use. If not set, will be inherited from the `http_proxy` and `https_proxy` environment variables.") }, 'http.proxyStrictSSL': { type: 'boolean', @@ -100,7 +100,7 @@ Registry.as(Extensions.Configuration) 'http.proxyAuthorization': { type: ['null', 'string'], default: null, - description: localize('proxyAuthorization', "The value to send as the 'Proxy-Authorization' header for every network request.") + markdownDescription: localize('proxyAuthorization', "The value to send as the `Proxy-Authorization` header for every network request.") }, 'http.proxySupport': { type: 'string', diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index 9af1792f70c..0339be71c36 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -156,7 +156,7 @@ export interface IWindowsService { openExtensionDevelopmentHostWindow(args: ParsedArgs, env: IProcessEnvironment): Promise; getWindows(): Promise<{ id: number; workspace?: IWorkspaceIdentifier; folderUri?: ISingleFolderWorkspaceIdentifier; title: string; filename?: string; }[]>; getWindowCount(): Promise; - log(severity: string, ...messages: string[]): Promise; + log(severity: string, args: string[]): Promise; showItemInFolder(path: URI): Promise; getActiveWindowId(): Promise; diff --git a/src/vs/platform/windows/electron-browser/windowsService.ts b/src/vs/platform/windows/electron-browser/windowsService.ts index 34ec58fc49a..228dfd98ba2 100644 --- a/src/vs/platform/windows/electron-browser/windowsService.ts +++ b/src/vs/platform/windows/electron-browser/windowsService.ts @@ -226,8 +226,8 @@ export class WindowsService implements IWindowsService { return this.channel.call('getWindowCount'); } - log(severity: string, ...messages: string[]): Promise { - return this.channel.call('log', [severity, messages]); + log(severity: string, args: string[]): Promise { + return this.channel.call('log', [severity, args]); } showItemInFolder(path: URI): Promise { @@ -257,4 +257,4 @@ export class WindowsService implements IWindowsService { resolveProxy(windowId: number, url: string): Promise { return Promise.resolve(this.channel.call('resolveProxy', [windowId, url])); } -} \ No newline at end of file +} diff --git a/src/vs/platform/windows/electron-main/windowsService.ts b/src/vs/platform/windows/electron-main/windowsService.ts index 910c4e8d594..c33bbb42b8a 100644 --- a/src/vs/platform/windows/electron-main/windowsService.ts +++ b/src/vs/platform/windows/electron-main/windowsService.ts @@ -334,7 +334,7 @@ export class WindowsService extends Disposable implements IWindowsService, IURLH return this.windowsMainService.getWindows().length; } - async log(severity: string, ...messages: string[]): Promise { + async log(severity: string, args: string[]): Promise { let consoleFn = console.log; switch (severity) { @@ -349,7 +349,7 @@ export class WindowsService extends Disposable implements IWindowsService, IURLH break; } - consoleFn(...messages); + consoleFn.call(console, ...args); } async showItemInFolder(resource: URI): Promise { diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 610c340fd55..68e0b1e6e5d 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -816,7 +816,7 @@ declare module 'vscode' { * [Terminal.sendText](#Terminal.sendText) is triggered that will fire the * [TerminalRenderer.onDidAcceptInput](#TerminalRenderer.onDidAcceptInput) event. * - * @deprecated Use [virtual processes](#TerminalVirtualProcess) instead. + * @deprecated Use [ExtensionTerminalOptions](#ExtensionTerminalOptions) instead. * * **Example:** Create a terminal renderer, show it and write hello world in red * ```typescript @@ -828,7 +828,7 @@ declare module 'vscode' { export interface TerminalRenderer { /** * The name of the terminal, this will appear in the terminal selector. - * @deprecated Use [virtual processes](#TerminalVirtualProcess) instead. + * @deprecated Use [ExtensionTerminalOptions](#ExtensionTerminalOptions) instead. */ name: string; @@ -837,7 +837,7 @@ declare module 'vscode' { * a value smaller than the maximum value, if this is undefined the terminal will auto fit * to the maximum value [maximumDimensions](TerminalRenderer.maximumDimensions). * - * @deprecated Use [virtual processes](#TerminalVirtualProcess) instead. + * @deprecated Use [ExtensionTerminalOptions](#ExtensionTerminalOptions) instead. * * **Example:** Override the dimensions of a TerminalRenderer to 20 columns and 10 rows * ```typescript @@ -855,14 +855,14 @@ declare module 'vscode' { * Listen to [onDidChangeMaximumDimensions](TerminalRenderer.onDidChangeMaximumDimensions) * to get notified when this value changes. * - * @deprecated Use [virtual processes](#TerminalVirtualProcess) instead. + * @deprecated Use [ExtensionTerminalOptions](#ExtensionTerminalOptions) instead. */ readonly maximumDimensions: TerminalDimensions | undefined; /** * The corresponding [Terminal](#Terminal) for this TerminalRenderer. * - * @deprecated Use [virtual processes](#TerminalVirtualProcess) instead. + * @deprecated Use [ExtensionTerminalOptions](#ExtensionTerminalOptions) instead. */ readonly terminal: Terminal; @@ -871,7 +871,7 @@ declare module 'vscode' { * text to the underlying _process_, this will write the text to the terminal itself. * * @param text The text to write. - * @deprecated Use [virtual processes](#TerminalVirtualProcess) instead. + * @deprecated Use [ExtensionTerminalOptions](#ExtensionTerminalOptions) instead. * * **Example:** Write red text to the terminal * ```typescript @@ -890,7 +890,7 @@ declare module 'vscode' { * [Terminal.sendText](#Terminal.sendText). Keystrokes are converted into their * corresponding VT sequence representation. * - * @deprecated Use [virtual processes](#TerminalVirtualProcess) instead. + * @deprecated Use [ExtensionTerminalOptions](#ExtensionTerminalOptions) instead. * * **Example:** Simulate interaction with the terminal from an outside extension or a * workbench command such as `workbench.action.terminal.runSelectedText` @@ -908,7 +908,7 @@ declare module 'vscode' { * An event which fires when the [maximum dimensions](#TerminalRenderer.maximumDimensions) of * the terminal renderer change. * - * @deprecated Use [virtual processes](#TerminalVirtualProcess) instead. + * @deprecated Use [ExtensionTerminalOptions](#ExtensionTerminalOptions) instead. */ readonly onDidChangeMaximumDimensions: Event; } @@ -918,60 +918,60 @@ declare module 'vscode' { * Create a [TerminalRenderer](#TerminalRenderer). * * @param name The name of the terminal renderer, this shows up in the terminal selector. - * @deprecated Use [virtual processes](#TerminalVirtualProcess) instead. + * @deprecated Use [ExtensionTerminalOptions](#ExtensionTerminalOptions) instead. */ export function createTerminalRenderer(name: string): TerminalRenderer; } //#endregion - //#region Terminal extension pty + //#region Extension terminals export namespace window { /** - * Creates a [Terminal](#Terminal) where an extension acts as the process. + * Creates a [Terminal](#Terminal) where an extension controls the teerminal. * - * @param options A [TerminalVirtualProcessOptions](#TerminalVirtualProcessOptions) object describing the - * characteristics of the new terminal. + * @param options An [ExtensionTerminalOptions](#ExtensionTerminalOptions) object describing + * the characteristics of the new terminal. * @return A new Terminal. */ - export function createTerminal(options: TerminalVirtualProcessOptions): Terminal; + export function createTerminal(options: ExtensionTerminalOptions): Terminal; } /** * Value-object describing what options a virtual process terminal should use. */ - export interface TerminalVirtualProcessOptions { + export interface ExtensionTerminalOptions { /** * A human-readable string which will be used to represent the terminal in the UI. */ name: string; /** - * An implementation of [TerminalVirtualProcess](#TerminalVirtualProcess) that allows an - * extension to act as a terminal's backing process. + * An implementation of [Pseudoterminal](#Pseudoterminal) that allows an extension to + * control a terminal. */ - virtualProcess: TerminalVirtualProcess; + pty: Pseudoterminal; } /** - * Defines the interface of a terminal virtual process, enabling extensions to act as a process - * in the terminal. + * Defines the interface of a terminal pty, enabling extensions to control a terminal. */ - interface TerminalVirtualProcess { + interface Pseudoterminal { /** * An event that when fired will write data to the terminal. Unlike - * [Terminal.sendText](#Terminal.sendText) which sends text to the underlying _process_, - * this will write the text to the terminal itself. + * [Terminal.sendText](#Terminal.sendText) which sends text to the underlying _process_ + * (the pty "slave"), this will write the text to the terminal itself (the pty "master"). * * **Example:** Write red text to the terminal * ```typescript * const writeEmitter = new vscode.EventEmitter(); - * const virtualProcess: TerminalVirtualProcess = { - * onDidWrite: writeEmitter.event + * const pty: vscode.Pseudoterminal = { + * onDidWrite: writeEmitter.event, + * open: () => writeEmitter.fire('\x1b[31mHello world\x1b[0m'), + * close: () => {} * }; - * vscode.window.createTerminal({ name: 'My terminal', virtualProcess }); - * writeEmitter.fire('\x1b[31mHello world\x1b[0m'); + * vscode.window.createTerminal({ name: 'My terminal', pty }); * ``` * * **Example:** Move the cursor to the 10th row and 20th column and write an asterisk @@ -985,71 +985,82 @@ declare module 'vscode' { * An event that when fired allows overriding the [dimensions](#Terminal.dimensions) of the * terminal. Note that when set the overridden dimensions will only take effect when they * are lower than the actual dimensions of the terminal (ie. there will never be a scroll - * bar). Set to `undefined` for the terminal to go back to the regular dimensions. + * bar). Set to `undefined` for the terminal to go back to the regular dimensions (fit to + * the size of the panel). * * **Example:** Override the dimensions of a terminal to 20 columns and 10 rows * ```typescript * const dimensionsEmitter = new vscode.EventEmitter(); - * const virtualProcess: TerminalVirtualProcess = { + * const pty: vscode.Pseudoterminal = { * onDidWrite: writeEmitter.event, - * onDidOverrideDimensions: dimensionsEmitter.event + * onDidOverrideDimensions: dimensionsEmitter.event, + * open: () => { + * dimensionsEmitter.fire({ + * columns: 20, + * rows: 10 + * }); + * }, + * close: () => {} * }; - * vscode.window.createTerminal({ name: 'My terminal', virtualProcess }); - * dimensionsEmitter.fire({ - * columns: 20, - * rows: 10 - * }); + * vscode.window.createTerminal({ name: 'My terminal', pty }); * ``` */ onDidOverrideDimensions?: Event; /** - * An event that when fired will exit the process with an exit code, this will behave the - * same for a virtual process as when a regular process exits with an exit code. Note that - * exit codes must be positive numbers, when negative the exit code will be forced to `1`. + * An event that when fired will signal that the pty is closed and dispose of the terminal. * - * **Example:** Exit with an exit code of `0` if the y key is pressed, otherwise `1`. + * **Example:** Exit the terminal when "y" is pressed, otherwise show a notification. * ```typescript * const writeEmitter = new vscode.EventEmitter(); - * const exitEmitter = new vscode.EventEmitter(); - * const virtualProcess: TerminalVirtualProcess = { + * const closeEmitter = new vscode.EventEmitter(); + * const pty: vscode.Pseudoterminal = { * onDidWrite: writeEmitter.event, - * input: data => exitEmitter.fire(data === 'y' ? 0 : 1) + * onDidClose: closeEmitter.event, + * open: () => writeEmitter.fire('Press y to exit successfully'), + * close: () => {} + * handleInput: { + * if (data !== 'y') { + * vscode.window.showInformationMessage('Something went wrong'); + * } + * data => closeEmitter.fire(); + * } * }; - * vscode.window.createTerminal({ name: 'Exit example', virtualProcess }); - * writeEmitter.fire('Press y to exit successfully'); + * vscode.window.createTerminal({ name: 'Exit example', pty }); */ - onDidExit?: Event; + onDidClose?: Event; /** - * Implement to handle when the terminal is ready to start firing events. + * Implement to handle when the pty is open and ready to start firing events. * * @param initialDimensions The dimensions of the terminal, this will be undefined if the * terminal panel has not been opened before this is called. */ - start(initialDimensions: TerminalDimensions | undefined): void; + open(initialDimensions: TerminalDimensions | undefined): void; /** - * Implement to handle when the terminal shuts down by an act of the user. + * Implement to handle when the terminal is closed by an act of the user. */ - shutdown(): void; + close(): void; /** - * Implement to handle keystrokes in the terminal or when an extension calls - * [Terminal.sendText](#Terminal.sendText). Keystrokes are converted into their - * corresponding VT sequence representation. + * Implement to handle incoming keystrokes in the terminal or when an extension calls + * [Terminal.sendText](#Terminal.sendText). `data` contains the keystrokes/text serialized into + * their corresponding VT sequence representation. * - * @param data The sent data. + * @param data The incoming data. * * **Example:** Echo input in the terminal. The sequence for enter (`\r`) is translated to * CRLF to go to a new line and move the cursor to the start of the line. * ```typescript * const writeEmitter = new vscode.EventEmitter(); - * const virtualProcess: TerminalVirtualProcess = { + * const pty: vscode.Pseudoterminal = { * onDidWrite: writeEmitter.event, + * open: () => {}, + * close: () => {}, * handleInput: data => writeEmitter.fire(data === '\r' ? '\r\n' : data) * }; - * vscode.window.createTerminal({ name: 'Local echo', virtualProcess }); + * vscode.window.createTerminal({ name: 'Local echo', pty }); * ``` */ handleInput?(data: string): void; @@ -1145,6 +1156,7 @@ declare module 'vscode' { } //#endregion + //#region CustomExecution /** * Class used to execute an extension callback as a task. */ @@ -1168,16 +1180,17 @@ declare module 'vscode' { */ export class CustomExecution2 { /** - * @param process The [TerminalVirtualProcess](#TerminalVirtualProcess) to be used by the task to display output. + * @param process The [Pseudotrminal](#Pseudoterminal) to be used by the task to display output. * @param callback The callback that will be called when the task is started by a user. */ - constructor(callback: (thisArg?: any) => Thenable); + constructor(callback: (thisArg?: any) => Thenable); /** - * The callback used to execute the task. Cancellation should be handled using the shutdown method of [TerminalVirtualProcess](#TerminalVirtualProcess). - * When the task is complete, onDidExit should be fired on the TerminalVirtualProcess with the exit code with '0' for success and a non-zero value for failure. + * The callback used to execute the task. Cancellation should be handled using + * [Pseudoterminal.close](#Pseudoterminal.close). When the task is complete fire + * [Pseudoterminal.onDidClose](#Pseudoterminal.onDidClose). */ - callback: (thisArg?: any) => Thenable; + callback: (thisArg?: any) => Thenable; } /** @@ -1203,6 +1216,7 @@ declare module 'vscode' { */ execution2?: ProcessExecution | ShellExecution | CustomExecution | CustomExecution2; } + //#endregion //#region Tasks export interface TaskPresentationOptions { @@ -1304,8 +1318,7 @@ declare module 'vscode' { /** * Deprecated or obsolete code * - * Can be used to style with strikeout or other "obsolete" styling. See: - * https://github.com/microsoft/vscode/issues/50972 + * Can be used to style with strikeout or other "obsolete" styling. */ Deprecated = 2, } diff --git a/src/vs/workbench/api/browser/mainThreadConsole.ts b/src/vs/workbench/api/browser/mainThreadConsole.ts index 160f9f0b773..1d7a7723404 100644 --- a/src/vs/workbench/api/browser/mainThreadConsole.ts +++ b/src/vs/workbench/api/browser/mainThreadConsole.ts @@ -40,7 +40,7 @@ export class MainThreadConsole implements MainThreadConsoleShape { // Log on main side if running tests from cli if (this._isExtensionDevTestFromCli) { - this._windowsService.log(entry.severity, ...parse(entry).args); + this._windowsService.log(entry.severity, parse(entry).args); } // Broadcast to other windows if we are in development mode diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index 13f4fad4663..f23b625d140 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { ITerminalService, ITerminalInstance, IShellLaunchConfig, ITerminalProcessExtHostProxy, ITerminalProcessExtHostRequest, ITerminalDimensions, EXT_HOST_CREATION_DELAY, IAvailableShellsRequest, IDefaultShellAndArgsRequest, ITerminalVirtualProcessRequest } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ITerminalService, ITerminalInstance, IShellLaunchConfig, ITerminalProcessExtHostProxy, ISpawnExtHostProcessRequest, ITerminalDimensions, EXT_HOST_CREATION_DELAY, IAvailableShellsRequest, IDefaultShellAndArgsRequest, IStartExtensionTerminalRequest } from 'vs/workbench/contrib/terminal/common/terminal'; import { ExtHostContext, ExtHostTerminalServiceShape, MainThreadTerminalServiceShape, MainContext, IExtHostContext, ShellLaunchConfigDto, TerminalLaunchConfig, ITerminalDimensionsDto } from 'vs/workbench/api/common/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { URI } from 'vs/base/common/uri'; @@ -47,8 +47,8 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._toDispose.add(_terminalService.onInstanceProcessIdReady(instance => this._onTerminalProcessIdReady(instance))); this._toDispose.add(_terminalService.onInstanceDimensionsChanged(instance => this._onInstanceDimensionsChanged(instance))); this._toDispose.add(_terminalService.onInstanceMaximumDimensionsChanged(instance => this._onInstanceMaximumDimensionsChanged(instance))); - this._toDispose.add(_terminalService.onInstanceRequestExtHostProcess(request => this._onTerminalRequestExtHostProcess(request))); - this._toDispose.add(_terminalService.onInstanceRequestVirtualProcess(e => this._onTerminalRequestVirtualProcess(e))); + this._toDispose.add(_terminalService.onInstanceRequestSpawnExtHostProcess(request => this._onRequestSpawnExtHostProcess(request))); + this._toDispose.add(_terminalService.onInstanceRequestStartExtensionTerminal(e => this._onRequestStartExtensionTerminal(e))); this._toDispose.add(_terminalService.onActiveInstanceChanged(instance => this._onActiveTerminalChanged(instance ? instance.id : null))); this._toDispose.add(_terminalService.onInstanceTitleChanged(instance => this._onTitleChanged(instance.id, instance.title))); this._toDispose.add(_terminalService.configHelper.onWorkspacePermissionsChanged(isAllowed => this._onWorkspacePermissionsChanged(isAllowed))); @@ -90,7 +90,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape env: launchConfig.env, strictEnv: launchConfig.strictEnv, hideFromUser: launchConfig.hideFromUser, - isVirtualProcess: launchConfig.isVirtualProcess + isExtensionTerminal: launchConfig.isExtensionTerminal }; const terminal = this._terminalService.createTerminal(shellLaunchConfig); this._terminalProcesses.set(terminal.id, new Promise(r => this._terminalProcessesReady.set(terminal.id, r))); @@ -240,7 +240,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._proxy.$acceptTerminalMaximumDimensions(instance.id, instance.maxCols, instance.maxRows); } - private _onTerminalRequestExtHostProcess(request: ITerminalProcessExtHostRequest): void { + private _onRequestSpawnExtHostProcess(request: ISpawnExtHostProcessRequest): void { // Only allow processes on remote ext hosts if (!this._remoteAuthority) { return; @@ -261,7 +261,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape cwd: request.shellLaunchConfig.cwd, env: request.shellLaunchConfig.env }; - this._proxy.$createProcess(proxy.terminalId, shellLaunchConfigDto, request.activeWorkspaceRootUri, request.cols, request.rows, request.isWorkspaceShellAllowed); + this._proxy.$spawnExtHostProcess(proxy.terminalId, shellLaunchConfigDto, request.activeWorkspaceRootUri, request.cols, request.rows, request.isWorkspaceShellAllowed); proxy.onInput(data => this._proxy.$acceptProcessInput(proxy.terminalId, data)); proxy.onResize(dimensions => this._proxy.$acceptProcessResize(proxy.terminalId, dimensions.cols, dimensions.rows)); proxy.onShutdown(immediate => this._proxy.$acceptProcessShutdown(proxy.terminalId, immediate)); @@ -270,7 +270,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape proxy.onRequestLatency(() => this._onRequestLatency(proxy.terminalId)); } - private _onTerminalRequestVirtualProcess(request: ITerminalVirtualProcessRequest): void { + private _onRequestStartExtensionTerminal(request: IStartExtensionTerminalRequest): void { const proxy = request.proxy; const ready = this._terminalProcessesReady.get(proxy.terminalId); if (!ready) { @@ -286,7 +286,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape columns: request.cols, rows: request.rows } : undefined; - this._proxy.$startVirtualProcess(proxy.terminalId, initialDimensions); + this._proxy.$startExtensionTerminal(proxy.terminalId, initialDimensions); proxy.onInput(data => this._proxy.$acceptProcessInput(proxy.terminalId, data)); proxy.onShutdown(immediate => this._proxy.$acceptProcessShutdown(proxy.terminalId, immediate)); proxy.onRequestCwd(() => this._proxy.$acceptProcessRequestCwd(proxy.terminalId)); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 94db70c62ff..b8f3ff3a9df 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -390,7 +390,7 @@ export interface TerminalLaunchConfig { waitOnExit?: boolean; strictEnv?: boolean; hideFromUser?: boolean; - isVirtualProcess?: boolean; + isExtensionTerminal?: boolean; } export interface MainThreadTerminalServiceShape extends IDisposable { @@ -1162,8 +1162,8 @@ export interface ExtHostTerminalServiceShape { $acceptTerminalTitleChange(id: number, name: string): void; $acceptTerminalDimensions(id: number, cols: number, rows: number): void; $acceptTerminalMaximumDimensions(id: number, cols: number, rows: number): void; - $createProcess(id: number, shellLaunchConfig: ShellLaunchConfigDto, activeWorkspaceRootUri: UriComponents, cols: number, rows: number, isWorkspaceShellAllowed: boolean): void; - $startVirtualProcess(id: number, initialDimensions: ITerminalDimensionsDto | undefined): void; + $spawnExtHostProcess(id: number, shellLaunchConfig: ShellLaunchConfigDto, activeWorkspaceRootUri: UriComponents, cols: number, rows: number, isWorkspaceShellAllowed: boolean): void; + $startExtensionTerminal(id: number, initialDimensions: ITerminalDimensionsDto | undefined): void; $acceptProcessInput(id: number, data: string): void; $acceptProcessResize(id: number, cols: number, rows: number): void; $acceptProcessShutdown(id: number, immediate: boolean): void; diff --git a/src/vs/workbench/api/common/extHostTextEditor.ts b/src/vs/workbench/api/common/extHostTextEditor.ts index f97da57cf53..fa10d30014b 100644 --- a/src/vs/workbench/api/common/extHostTextEditor.ts +++ b/src/vs/workbench/api/common/extHostTextEditor.ts @@ -51,21 +51,21 @@ export class TextEditorEdit { private readonly _document: vscode.TextDocument; private readonly _documentVersionId: number; - private _collectedEdits: ITextEditOperation[]; - private _setEndOfLine: EndOfLine | undefined; private readonly _undoStopBefore: boolean; private readonly _undoStopAfter: boolean; + private _collectedEdits: ITextEditOperation[] = []; + private _setEndOfLine: EndOfLine | undefined = undefined; + private _finalized: boolean = false; constructor(document: vscode.TextDocument, options: { undoStopBefore: boolean; undoStopAfter: boolean; }) { this._document = document; this._documentVersionId = document.version; - this._collectedEdits = []; - this._setEndOfLine = undefined; this._undoStopBefore = options.undoStopBefore; this._undoStopAfter = options.undoStopAfter; } finalize(): IEditData { + this._finalized = true; return { documentVersionId: this._documentVersionId, edits: this._collectedEdits, @@ -75,7 +75,14 @@ export class TextEditorEdit { }; } + private _throwIfFinalized() { + if (this._finalized) { + throw new Error('Edit is only valid while callback runs'); + } + } + replace(location: Position | Range | Selection, value: string): void { + this._throwIfFinalized(); let range: Range | null = null; if (location instanceof Position) { @@ -90,10 +97,12 @@ export class TextEditorEdit { } insert(location: Position, value: string): void { + this._throwIfFinalized(); this._pushEdit(new Range(location, location), value, true); } delete(location: Range | Selection): void { + this._throwIfFinalized(); let range: Range | null = null; if (location instanceof Range) { @@ -115,6 +124,7 @@ export class TextEditorEdit { } setEndOfLine(endOfLine: EndOfLine): void { + this._throwIfFinalized(); if (endOfLine !== EndOfLine.LF && endOfLine !== EndOfLine.CRLF) { throw illegalArgument('endOfLine'); } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index de31343f036..39b2cda9188 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -108,6 +108,8 @@ export namespace DiagnosticTag { switch (value) { case types.DiagnosticTag.Unnecessary: return MarkerTag.Unnecessary; + case types.DiagnosticTag.Deprecated: + return MarkerTag.Deprecated; } return undefined; } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index b8aebf92b45..0a0ce14f00e 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -773,6 +773,7 @@ export class SnippetString { export enum DiagnosticTag { Unnecessary = 1, + Deprecated = 2 } export enum DiagnosticSeverity { @@ -1773,19 +1774,19 @@ export class CustomExecution implements vscode.CustomExecution { } export class CustomExecution2 implements vscode.CustomExecution2 { - private _callback: () => Thenable; - constructor(callback: () => Thenable) { + private _callback: () => Thenable; + constructor(callback: () => Thenable) { this._callback = callback; } public computeId(): string { return 'customExecution' + generateUuid(); } - public set callback(value: () => Thenable) { + public set callback(value: () => Thenable) { this._callback = value; } - public get callback(): (() => Thenable) { + public get callback(): (() => Thenable) { return this._callback; } } diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 27fb09b2abd..f4df88c11ba 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -528,10 +528,10 @@ export function createApiFactory( checkProposedApiEnabled(extension); return extHostEditorInsets.createWebviewEditorInset(editor, line, height, options, extension); }, - createTerminal(nameOrOptions?: vscode.TerminalOptions | vscode.TerminalVirtualProcessOptions | string, shellPath?: string, shellArgs?: string[] | string): vscode.Terminal { + createTerminal(nameOrOptions?: vscode.TerminalOptions | vscode.ExtensionTerminalOptions | string, shellPath?: string, shellArgs?: string[] | string): vscode.Terminal { if (typeof nameOrOptions === 'object') { - if ('virtualProcess' in nameOrOptions) { - return extHostTerminalService.createVirtualProcessTerminal(nameOrOptions); + if ('pty' in nameOrOptions) { + return extHostTerminalService.createExtensionTerminal(nameOrOptions); } else { nameOrOptions.hideFromUser = nameOrOptions.hideFromUser || (nameOrOptions.runInBackground && extension.enableProposedApi); return extHostTerminalService.createTerminalFromOptions(nameOrOptions); diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index d9172447d73..be353ca5bde 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -341,19 +341,29 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { } else { resolve(true); } - }).then(needNewTerminal => { + }).then(async needNewTerminal => { + + const configProvider = await this._configurationService.getConfigProvider(); + const shell = this._terminalService.getDefaultShell(configProvider); if (needNewTerminal || !this._integratedTerminalInstance) { - this._integratedTerminalInstance = this._terminalService.createTerminal(args.title || nls.localize('debug.terminal.title', "debuggee")); + const options: vscode.TerminalOptions = { + shellPath: shell, + // shellArgs: this._terminalService._getDefaultShellArgs(configProvider), + cwd: args.cwd, + name: args.title || nls.localize('debug.terminal.title', "debuggee"), + env: args.env + }; + delete args.cwd; + delete args.env; + this._integratedTerminalInstance = this._terminalService.createTerminalFromOptions(options); } const terminal: vscode.Terminal = this._integratedTerminalInstance; terminal.show(); - return this._integratedTerminalInstance.processId.then(async shellProcessId => { + return this._integratedTerminalInstance.processId.then(shellProcessId => { - const configProvider = await this._configurationService.getConfigProvider(); - const shell = this._terminalService.getDefaultShell(configProvider); const command = prepareCommand(args, shell, configProvider); terminal.sendText(command, true); diff --git a/src/vs/workbench/api/node/extHostTask.ts b/src/vs/workbench/api/node/extHostTask.ts index 0e7f5f9dd7a..9191b53aa47 100644 --- a/src/vs/workbench/api/node/extHostTask.ts +++ b/src/vs/workbench/api/node/extHostTask.ts @@ -588,7 +588,7 @@ export class ExtHostTask implements ExtHostTaskShape { // Clone the custom execution to keep the original untouched. This is important for multiple runs of the same task. this._activeCustomExecutions2.set(execution.id, execution2); - await this._terminalService.attachVirtualProcessToTerminal(terminalId, await execution2.callback()); + await this._terminalService.attachPtyToTerminal(terminalId, await execution2.callback()); } // Once a terminal is spun up for the custom execution task this event will be fired. diff --git a/src/vs/workbench/api/node/extHostTerminalService.ts b/src/vs/workbench/api/node/extHostTerminalService.ts index 4aa39f91127..58734d0827d 100644 --- a/src/vs/workbench/api/node/extHostTerminalService.ts +++ b/src/vs/workbench/api/node/extHostTerminalService.ts @@ -124,8 +124,8 @@ export class ExtHostTerminal extends BaseExtHostTerminal implements vscode.Termi this._runQueuedRequests(terminal.id); } - public async createVirtualProcess(): Promise { - const terminal = await this._proxy.$createTerminal({ name: this._name, isVirtualProcess: true }); + public async createExtensionTerminal(): Promise { + const terminal = await this._proxy.$createTerminal({ name: this._name, isExtensionTerminal: true }); this._name = terminal.name; this._runQueuedRequests(terminal.id); } @@ -310,9 +310,9 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { private _logService: ILogService ) { this._proxy = mainContext.getProxy(MainContext.MainThreadTerminalService); - this.updateLastActiveWorkspace(); - this.updateVariableResolver(); - this.registerListeners(); + this._updateLastActiveWorkspace(); + this._updateVariableResolver(); + this._registerListeners(); } public createTerminal(name?: string, shellPath?: string, shellArgs?: string[] | string): vscode.Terminal { @@ -329,20 +329,20 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { return terminal; } - public createVirtualProcessTerminal(options: vscode.TerminalVirtualProcessOptions): vscode.Terminal { + public createExtensionTerminal(options: vscode.ExtensionTerminalOptions): vscode.Terminal { const terminal = new ExtHostTerminal(this._proxy, options.name); - const p = new ExtHostVirtualProcess(options.virtualProcess); - terminal.createVirtualProcess().then(() => this._setupExtHostProcessListeners(terminal._id, p)); + const p = new ExtHostPseudoterminal(options.pty); + terminal.createExtensionTerminal().then(() => this._setupExtHostProcessListeners(terminal._id, p)); this._terminals.push(terminal); return terminal; } - public async attachVirtualProcessToTerminal(id: number, virtualProcess: vscode.TerminalVirtualProcess): Promise { + public async attachPtyToTerminal(id: number, pty: vscode.Pseudoterminal): Promise { const terminal = this._getTerminalByIdEventually(id); if (!terminal) { throw new Error(`Cannot resolve terminal with id ${id} for virtual process`); } - const p = new ExtHostVirtualProcess(virtualProcess); + const p = new ExtHostPseudoterminal(pty); this._setupExtHostProcessListeners(id, p); } @@ -532,25 +532,25 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { return env; } - private registerListeners(): void { - this._extHostDocumentsAndEditors.onDidChangeActiveTextEditor(() => this.updateLastActiveWorkspace()); - this._extHostWorkspace.onDidChangeWorkspace(() => this.updateVariableResolver()); + private _registerListeners(): void { + this._extHostDocumentsAndEditors.onDidChangeActiveTextEditor(() => this._updateLastActiveWorkspace()); + this._extHostWorkspace.onDidChangeWorkspace(() => this._updateVariableResolver()); } - private updateLastActiveWorkspace(): void { + private _updateLastActiveWorkspace(): void { const activeEditor = this._extHostDocumentsAndEditors.activeEditor(); if (activeEditor) { this._lastActiveWorkspace = this._extHostWorkspace.getWorkspaceFolder(activeEditor.document.uri) as IWorkspaceFolder; } } - private async updateVariableResolver(): Promise { + private async _updateVariableResolver(): Promise { const configProvider = await this._extHostConfiguration.getConfigProvider(); const workspaceFolders = await this._extHostWorkspace.getWorkspaceFolders2(); this._variableResolver = new ExtHostVariableResolverService(workspaceFolders || [], this._extHostDocumentsAndEditors, configProvider); } - public async $createProcess(id: number, shellLaunchConfigDto: ShellLaunchConfigDto, activeWorkspaceRootUriComponents: UriComponents, cols: number, rows: number, isWorkspaceShellAllowed: boolean): Promise { + public async $spawnExtHostProcess(id: number, shellLaunchConfigDto: ShellLaunchConfigDto, activeWorkspaceRootUriComponents: UriComponents, cols: number, rows: number, isWorkspaceShellAllowed: boolean): Promise { const shellLaunchConfig: IShellLaunchConfig = { name: shellLaunchConfigDto.name, executable: shellLaunchConfigDto.executable, @@ -619,9 +619,9 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { this._setupExtHostProcessListeners(id, new TerminalProcess(shellLaunchConfig, initialCwd, cols, rows, env, enableConpty, this._logService)); } - public async $startVirtualProcess(id: number, initialDimensions: ITerminalDimensionsDto | undefined): Promise { + public async $startExtensionTerminal(id: number, initialDimensions: ITerminalDimensionsDto | undefined): Promise { // Make sure the ExtHostTerminal exists so onDidOpenTerminal has fired before we call - // TerminalVirtualProcess.start + // Pseudoterminal.start await this._getTerminalByIdEventually(id); // Processes should be initialized here for normal virtual process terminals, however for @@ -630,7 +630,7 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { let retries = 5; while (retries-- > 0) { if (this._terminalProcesses[id]) { - (this._terminalProcesses[id] as ExtHostVirtualProcess).startSendingEvents(initialDimensions); + (this._terminalProcesses[id] as ExtHostPseudoterminal).startSendingEvents(initialDimensions); return; } await timeout(50); @@ -773,7 +773,7 @@ class ApiRequest { } } -class ExtHostVirtualProcess implements ITerminalChildProcess { +class ExtHostPseudoterminal implements ITerminalChildProcess { private _queuedEvents: (IQueuedEvent | IQueuedEvent | IQueuedEvent<{ pid: number, cwd: string }> | IQueuedEvent)[] = []; private _queueDisposables: IDisposable[] | undefined; @@ -789,33 +789,33 @@ class ExtHostVirtualProcess implements ITerminalChildProcess { public get onProcessOverrideDimensions(): Event { return this._onProcessOverrideDimensions.event; } constructor( - private readonly _virtualProcess: vscode.TerminalVirtualProcess + private readonly _pty: vscode.Pseudoterminal ) { this._queueDisposables = []; - this._queueDisposables.push(this._virtualProcess.onDidWrite(e => this._queuedEvents.push({ emitter: this._onProcessData, data: e }))); - if (this._virtualProcess.onDidExit) { - this._queueDisposables.push(this._virtualProcess.onDidExit(e => this._queuedEvents.push({ emitter: this._onProcessExit, data: e }))); + this._queueDisposables.push(this._pty.onDidWrite(e => this._queuedEvents.push({ emitter: this._onProcessData, data: e }))); + if (this._pty.onDidClose) { + this._queueDisposables.push(this._pty.onDidClose(e => this._queuedEvents.push({ emitter: this._onProcessExit, data: 0 }))); } - if (this._virtualProcess.onDidOverrideDimensions) { - this._queueDisposables.push(this._virtualProcess.onDidOverrideDimensions(e => this._queuedEvents.push({ emitter: this._onProcessOverrideDimensions, data: e ? { cols: e.columns, rows: e.rows } : undefined }))); + if (this._pty.onDidOverrideDimensions) { + this._queueDisposables.push(this._pty.onDidOverrideDimensions(e => this._queuedEvents.push({ emitter: this._onProcessOverrideDimensions, data: e ? { cols: e.columns, rows: e.rows } : undefined }))); } } shutdown(): void { - if (this._virtualProcess.shutdown) { - this._virtualProcess.shutdown(); + if (this._pty.close) { + this._pty.close(); } } input(data: string): void { - if (this._virtualProcess.handleInput) { - this._virtualProcess.handleInput(data); + if (this._pty.handleInput) { + this._pty.handleInput(data); } } resize(cols: number, rows: number): void { - if (this._virtualProcess.setDimensions) { - this._virtualProcess.setDimensions({ columns: cols, rows }); + if (this._pty.setDimensions) { + this._pty.setDimensions({ columns: cols, rows }); } } @@ -838,19 +838,16 @@ class ExtHostVirtualProcess implements ITerminalChildProcess { this._queueDisposables = undefined; // Attach the real listeners - this._virtualProcess.onDidWrite(e => this._onProcessData.fire(e)); - if (this._virtualProcess.onDidExit) { - this._virtualProcess.onDidExit(e => { - // Ensure only positive exit codes are returned - this._onProcessExit.fire(e >= 0 ? e : 1); - }); + this._pty.onDidWrite(e => this._onProcessData.fire(e)); + if (this._pty.onDidClose) { + this._pty.onDidClose(e => this._onProcessExit.fire(0)); } - if (this._virtualProcess.onDidOverrideDimensions) { - this._virtualProcess.onDidOverrideDimensions(e => this._onProcessOverrideDimensions.fire(e ? { cols: e.columns, rows: e.rows } : e)); + if (this._pty.onDidOverrideDimensions) { + this._pty.onDidOverrideDimensions(e => this._onProcessOverrideDimensions.fire(e ? { cols: e.columns, rows: e.rows } : e)); } - if (this._virtualProcess.start) { - this._virtualProcess.start(initialDimensions); + if (this._pty.open) { + this._pty.open(initialDimensions); } } } diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index c2e011c5370..cfbafeba3f6 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -5,7 +5,7 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; -import { EventType, addDisposableListener, addClass, removeClass, isAncestor, getClientArea, position, size, EventHelper } from 'vs/base/browser/dom'; +import { EventType, addDisposableListener, addClass, removeClass, isAncestor, getClientArea, position, size, EventHelper, Dimension } from 'vs/base/browser/dom'; import { onDidChangeFullscreen, isFullscreen, getZoomFactor } from 'vs/base/browser/browser'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -27,7 +27,7 @@ import { IWindowService, MenuBarVisibility, getTitleBarStyle } from 'vs/platform import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IEditorService, IResourceEditor } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { Sizing, Direction, Grid, SerializableGrid, ISerializableView, ISerializedGrid } from 'vs/base/browser/ui/grid/grid'; +import { Sizing, Direction, Grid, SerializableGrid, ISerializableView, ISerializedGrid, GridBranchNode, GridLeafNode, isGridBranchNode } from 'vs/base/browser/ui/grid/grid'; import { WorkbenchLegacyLayout } from 'vs/workbench/browser/legacyLayout'; import { IDimension } from 'vs/platform/layout/browser/layoutService'; import { Part } from 'vs/workbench/browser/part'; @@ -545,6 +545,10 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi return true; // any other part cannot be hidden } + getDimension(part: Parts): Dimension { + return this.getPart(part).dimension; + } + getTitleBarOffset(): number { let offset = 0; if (this.isVisible(Parts.TITLEBAR_PART)) { @@ -745,20 +749,10 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi try { workbenchGrid = SerializableGrid.deserialize(parsedGrid, { fromJSON }, { proportionalLayout: false }); - // Set some layout state - this.state.sideBar.position = Position.LEFT; - for (let view of workbenchGrid.getNeighborViews(this.sideBarPartView, Direction.Right)) { - if (view === this.activityBarPartView) { - this.state.sideBar.position = Position.RIGHT; - } - } - - this.state.panel.position = Position.BOTTOM; - for (let view of workbenchGrid.getNeighborViews(this.panelPartView, Direction.Left)) { - if (view === this.editorPartView) { - this.state.panel.position = Position.RIGHT; - } - } + const root = workbenchGrid.getViews(); + const middleSection = root.children[1] as GridBranchNode; + this.state.sideBar.position = (middleSection.children[0] as GridLeafNode).view === this.activityBarPartView ? Position.LEFT : Position.RIGHT; + this.state.panel.position = isGridBranchNode(middleSection.children[2]) ? Position.BOTTOM : Position.RIGHT; } catch (err) { console.error(err); } @@ -779,6 +773,10 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.setPanelHidden(!visible, true); })); + this._register((this.editorPartView as PanelPart).onDidVisibilityChange((visible) => { + this.setEditorHidden(!visible, true); + })); + this._register(this.lifecycleService.onBeforeShutdown(beforeShutdownEvent => { beforeShutdownEvent.veto(new Promise((resolve) => { const grid = this.workbenchGrid as SerializableGrid; @@ -845,8 +843,9 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi statusBarInGrid = true; } - if (!titlebarInGrid && getTitleBarStyle(this.configurationService, this.environmentService) === 'custom') { + if (!titlebarInGrid) { this.workbenchGrid.addView(this.titleBarPartView, Sizing.Split, this.editorPartView, Direction.Up); + titlebarInGrid = true; } @@ -1153,6 +1152,11 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } else { size.width = this.state.panel.sizeBeforeMaximize; } + + // Unhide the editor if needed + if (this.state.editor.hidden) { + this.setEditorHidden(false); + } } this.workbenchGrid.resizeView(this.panelPartView, size); diff --git a/src/vs/workbench/browser/legacyLayout.ts b/src/vs/workbench/browser/legacyLayout.ts index d936e13f1b2..8800afc09e8 100644 --- a/src/vs/workbench/browser/legacyLayout.ts +++ b/src/vs/workbench/browser/legacyLayout.ts @@ -626,11 +626,11 @@ export class WorkbenchLegacyLayout extends Disposable implements IVerticalSashLa } // Propagate to Part Layouts - this.parts.titlebar.layout(this.workbenchSize.width, this.titlebarHeight, -1); - this.parts.editor.layout(editorSize.width, editorSize.height, -1); - this.parts.sidebar.layout(sidebarSize.width, sidebarSize.height, -1); - this.parts.panel.layout(panelDimension.width, panelDimension.height, -1); - this.parts.activitybar.layout(activityBarSize.width, activityBarSize.height, -1); + this.parts.titlebar.layout(this.workbenchSize.width, this.titlebarHeight); + this.parts.editor.layout(editorSize.width, editorSize.height); + this.parts.sidebar.layout(sidebarSize.width, sidebarSize.height); + this.parts.panel.layout(panelDimension.width, panelDimension.height); + this.parts.activitybar.layout(activityBarSize.width, activityBarSize.height); // Propagate to Context View this.contextViewService.layout(); diff --git a/src/vs/workbench/browser/part.ts b/src/vs/workbench/browser/part.ts index f80e4545957..c78ad168cb7 100644 --- a/src/vs/workbench/browser/part.ts +++ b/src/vs/workbench/browser/part.ts @@ -9,10 +9,9 @@ import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService'; import { Dimension, size } from 'vs/base/browser/dom'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IDimension } from 'vs/platform/layout/browser/layoutService'; -import { ISerializableView, Orientation } from 'vs/base/browser/ui/grid/grid'; +import { ISerializableView, IViewSize } from 'vs/base/browser/ui/grid/grid'; import { Event, Emitter } from 'vs/base/common/event'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; -import { IViewSize } from 'vs/base/browser/ui/grid/gridview'; export interface IPartOptions { hasTitle?: boolean; @@ -29,6 +28,10 @@ export interface ILayoutContentResult { * arranges an optional title and mandatory content area to show content. */ export abstract class Part extends Component implements ISerializableView { + + private _dimension: Dimension; + get dimension(): Dimension { return this._dimension; } + private parent: HTMLElement; private titleArea: HTMLElement | null; private contentArea: HTMLElement | null; @@ -128,7 +131,10 @@ export abstract class Part extends Component implements ISerializableView { abstract minimumHeight: number; abstract maximumHeight: number; - abstract layout(width: number, height: number, orientation: Orientation): void; + layout(width: number, height: number): void { + this._dimension = new Dimension(width, height); + } + abstract toJSON(): object; //#endregion @@ -164,4 +170,4 @@ class PartLayout { return { titleSize, contentSize }; } -} \ No newline at end of file +} diff --git a/src/vs/workbench/browser/parts/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index d09cf93901b..e5d7cfbe7c8 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -466,6 +466,7 @@ export abstract class CompositePart extends Part { } layout(width: number, height: number): void { + super.layout(width, height); // Layout contents this.contentAreaSize = super.layoutContents(width, height).contentSize; diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index 53629222ca3..354ef330025 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -448,7 +448,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: editorCommands. interface IEditorToolItem { id: string; title: string; iconDark: string; iconLight: string; } -function appendEditorToolItem(primary: IEditorToolItem, when: ContextKeyExpr, order: number, alternative?: IEditorToolItem): void { +function appendEditorToolItem(primary: IEditorToolItem, when: ContextKeyExpr | undefined, order: number, alternative?: IEditorToolItem): void { const item: IMenuItem = { command: { id: primary.id, diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index a42cf0aed2f..3d456d54ecc 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -11,7 +11,7 @@ import { Event, Emitter, Relay } from 'vs/base/common/event'; import { contrastBorder, editorBackground } from 'vs/platform/theme/common/colorRegistry'; import { GroupDirection, IAddGroupOptions, GroupsArrangement, GroupOrientation, IMergeGroupOptions, MergeGroupMode, ICopyEditorOptions, GroupsOrder, GroupChangeKind, GroupLocation, IFindGroupScope, EditorGroupLayout, GroupLayoutArgument, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IInstantiationService, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; -import { Direction, SerializableGrid, Sizing, ISerializedGrid, Orientation, GridBranchNode, isGridBranchNode, GridNode, createSerializedGrid, Grid } from 'vs/base/browser/ui/grid/grid'; +import { IView, orthogonal, LayoutPriority, IViewSize, Direction, SerializableGrid, Sizing, ISerializedGrid, Orientation, GridBranchNode, isGridBranchNode, GridNode, createSerializedGrid, Grid } from 'vs/base/browser/ui/grid/grid'; import { GroupIdentifier, IWorkbenchEditorConfiguration, IEditorPartOptions } from 'vs/workbench/common/editor'; import { values } from 'vs/base/common/map'; import { EDITOR_GROUP_BORDER, EDITOR_PANE_BACKGROUND } from 'vs/workbench/common/theme'; @@ -27,7 +27,6 @@ import { EditorDropTarget } from 'vs/workbench/browser/parts/editor/editorDropTa import { localize } from 'vs/nls'; import { Color } from 'vs/base/common/color'; import { CenteredViewLayout } from 'vs/base/browser/ui/centered/centeredViewLayout'; -import { IView, orthogonal, LayoutPriority, IViewSize } from 'vs/base/browser/ui/grid/gridview'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Parts, IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -112,13 +111,11 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro private _onDidSizeConstraintsChange = this._register(new Relay<{ width: number; height: number; } | undefined>()); get onDidSizeConstraintsChange(): Event<{ width: number; height: number; } | undefined> { return Event.any(this.onDidSetGridWidget.event, this._onDidSizeConstraintsChange.event); } - private readonly _onDidPreferredSizeChange: Emitter = this._register(new Emitter()); - readonly onDidPreferredSizeChange: Event = this._onDidPreferredSizeChange.event; + private _onDidVisibilityChange = this._register(new Emitter()); + readonly onDidVisibilityChange: Event = this._onDidVisibilityChange.event; //#endregion - private _preferredSize: Dimension | undefined; - private readonly workspaceMemento: MementoObject; private readonly globalMemento: MementoObject; @@ -205,8 +202,8 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro //#region IEditorGroupsService - private _dimension: Dimension; - get dimension(): Dimension { return this._dimension; } + private _contentDimension: Dimension; + get contentDimension(): Dimension { return this._contentDimension; } get activeGroup(): IEditorGroupView { return this._activeGroup; @@ -366,9 +363,6 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro const newOrientation = (orientation === GroupOrientation.HORIZONTAL) ? Orientation.HORIZONTAL : Orientation.VERTICAL; if (this.gridWidget.orientation !== newOrientation) { this.gridWidget.orientation = newOrientation; - - // Mark preferred size as changed - this.resetPreferredSize(); } } @@ -418,14 +412,11 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro this.doCreateGridControlWithState(gridDescriptor, activeGroup.id, currentGroupViews); // Layout - this.doLayout(this._dimension); + this.doLayout(this._contentDimension); // Update container this.updateContainer(); - // Mark preferred size as changed - this.resetPreferredSize(); - // Events for groups that got added this.getGroups(GroupsOrder.GRID_APPEARANCE).forEach(groupView => { if (currentGroupViews.indexOf(groupView) === -1) { @@ -490,9 +481,6 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro // Update container this.updateContainer(); - // Mark preferred size as changed - this.resetPreferredSize(); - // Event this._onDidAddGroup.fire(newGroupView); @@ -661,9 +649,6 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro // Update container this.updateContainer(); - // Mark preferred size as changed - this.resetPreferredSize(); - // Event this._onDidRemoveGroup.fire(groupView); } @@ -761,26 +746,11 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro get minimumHeight(): number { return this.centeredLayoutWidget.minimumHeight; } get maximumHeight(): number { return this.centeredLayoutWidget.maximumHeight; } + readonly snap = true; + get onDidChange(): Event { return this.centeredLayoutWidget.onDidChange; } readonly priority: LayoutPriority = LayoutPriority.High; - get preferredSize(): Dimension { - if (!this._preferredSize) { - this._preferredSize = new Dimension(this.gridWidget.minimumWidth, this.gridWidget.minimumHeight); - } - - return this._preferredSize; - } - - private resetPreferredSize(): void { - - // Reset (will be computed upon next access) - this._preferredSize = undefined; - - // Event - this._onDidPreferredSizeChange.fire(); - } - private get gridSeparatorBorder(): Color { return this.theme.getColor(EDITOR_GROUP_BORDER) || this.theme.getColor(contrastBorder) || Color.transparent; } @@ -968,10 +938,10 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro } private doLayout(dimension: Dimension): void { - this._dimension = dimension; + this._contentDimension = dimension; // Layout Grid - this.centeredLayoutWidget.layout(this._dimension.width, this._dimension.height); + this.centeredLayoutWidget.layout(this._contentDimension.width, this._contentDimension.height); // Event this._onDidLayout.fire(dimension); @@ -1021,6 +991,10 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro //#endregion + setVisible(visible: boolean): void { + this._onDidVisibilityChange.fire(visible); + } + toJSON(): object { return { type: Parts.EDITOR_PART @@ -1028,4 +1002,4 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro } } -registerSingleton(IEditorGroupsService, EditorPart); \ No newline at end of file +registerSingleton(IEditorGroupsService, EditorPart); diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index 874d81baddb..1189df45c45 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -59,6 +59,16 @@ export class PanelPart extends CompositePart implements IPanelService { readonly snap = true; + get preferredHeight(): number | undefined { + const sidebarDimension = this.layoutService.getDimension(Parts.SIDEBAR_PART); + return sidebarDimension.height * 0.4; + } + + get preferredWidth(): number | undefined { + const statusbarPart = this.layoutService.getDimension(Parts.STATUSBAR_PART); + return statusbarPart.width * 0.4; + } + //#endregion get onDidPanelOpen(): Event<{ panel: IPanel, focus: boolean }> { return Event.map(this.onDidCompositeOpen.event, compositeOpen => ({ panel: compositeOpen.composite, focus: compositeOpen.focus })); } @@ -74,7 +84,7 @@ export class PanelPart extends CompositePart implements IPanelService { private compositeActions: Map = new Map(); private blockOpeningPanel: boolean; - private dimension: Dimension; + private _contentDimension: Dimension; constructor( @INotificationService notificationService: INotificationService, @@ -293,21 +303,21 @@ export class PanelPart extends CompositePart implements IPanelService { } if (this.layoutService.getPanelPosition() === Position.RIGHT) { - this.dimension = new Dimension(width - 1, height!); // Take into account the 1px border when layouting + this._contentDimension = new Dimension(width - 1, height!); // Take into account the 1px border when layouting } else { - this.dimension = new Dimension(width, height!); + this._contentDimension = new Dimension(width, height!); } // Layout contents - super.layout(this.dimension.width, this.dimension.height); + super.layout(this._contentDimension.width, this._contentDimension.height); // Layout composite bar this.layoutCompositeBar(); } private layoutCompositeBar(): void { - if (this.dimension) { - let availableWidth = this.dimension.width - 40; // take padding into account + if (this._contentDimension) { + let availableWidth = this._contentDimension.width - 40; // take padding into account if (this.toolBar) { availableWidth = Math.max(PanelPart.MIN_COMPOSITE_BAR_WIDTH, availableWidth - this.getToolbarWidth()); // adjust height for global actions showing } @@ -522,4 +532,4 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { } }); -registerSingleton(IPanelService, PanelPart); \ No newline at end of file +registerSingleton(IPanelService, PanelPart); diff --git a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts index 88c40e827ae..8b50a07b662 100644 --- a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts +++ b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts @@ -47,6 +47,22 @@ export class SidebarPart extends CompositePart implements IViewletServi readonly snap = true; + get preferredWidth(): number | undefined { + const viewlet = this.getActiveViewlet(); + + if (!viewlet) { + return; + } + + const width = viewlet.getOptimalWidth(); + + if (typeof width !== 'number') { + return; + } + + return width; + } + //#endregion get onDidViewletRegister(): Event { return >this.viewletRegistry.onDidRegister; } @@ -303,4 +319,4 @@ registry.registerWorkbenchAction(new SyncActionDescriptor(FocusSideBarAction, Fo primary: KeyMod.CtrlCmd | KeyCode.KEY_0 }), 'View: Focus into Side Bar', nls.localize('viewCategory', "View")); -registerSingleton(IViewletService, SidebarPart); \ No newline at end of file +registerSingleton(IViewletService, SidebarPart); diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index 00fe6c45bc4..089cbd3003e 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -611,6 +611,7 @@ export class StatusbarPart extends Part implements IStatusbarService { } layout(width: number, height: number): void { + super.layout(width, height); super.layoutContents(width, height); } @@ -816,4 +817,4 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { } }); -registerSingleton(IStatusbarService, StatusbarPart); \ No newline at end of file +registerSingleton(IStatusbarService, StatusbarPart); diff --git a/src/vs/workbench/browser/parts/views/customView.ts b/src/vs/workbench/browser/parts/views/customView.ts index ab1ad41a50b..53a104e4273 100644 --- a/src/vs/workbench/browser/parts/views/customView.ts +++ b/src/vs/workbench/browser/parts/views/customView.ts @@ -13,7 +13,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; import { ContextAwareMenuEntryActionViewItem, createAndFillInActionBarActions, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IViewsService, ITreeView, ITreeItem, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeViewDescriptor, IViewsRegistry, ViewContainer, ITreeItemLabel, Extensions } from 'vs/workbench/common/views'; +import { ITreeView, ITreeItem, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeViewDescriptor, IViewsRegistry, ViewContainer, ITreeItemLabel, Extensions } from 'vs/workbench/common/views'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -56,9 +56,9 @@ export class CustomTreeViewPanel extends ViewletPanel { @IKeybindingService keybindingService: IKeybindingService, @IContextMenuService contextMenuService: IContextMenuService, @IConfigurationService configurationService: IConfigurationService, - @IViewsService viewsService: IViewsService, + @IContextKeyService contextKeyService: IContextKeyService, ) { - super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: options.title }, keybindingService, contextMenuService, configurationService); + super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: options.title }, keybindingService, contextMenuService, configurationService, contextKeyService); const { treeView } = (Registry.as(Extensions.ViewsRegistry).getView(options.id)); this.treeView = treeView; this._register(this.treeView.onDidChangeActions(() => this.updateActions(), this)); diff --git a/src/vs/workbench/browser/parts/views/panelViewlet.ts b/src/vs/workbench/browser/parts/views/panelViewlet.ts index 6c2671e227f..ef9dbe0d75f 100644 --- a/src/vs/workbench/browser/parts/views/panelViewlet.ts +++ b/src/vs/workbench/browser/parts/views/panelViewlet.ts @@ -26,8 +26,9 @@ import { PanelView, IPanelViewOptions, IPanelOptions, Panel } from 'vs/base/brow import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; -import { IView } from 'vs/workbench/common/views'; +import { IView, FocusedViewContext } from 'vs/workbench/common/views'; import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; export interface IPanelColors extends IColorMapping { dropBackground?: ColorIdentifier; @@ -58,6 +59,8 @@ export abstract class ViewletPanel extends Panel implements IView { protected _onDidChangeTitleArea = this._register(new Emitter()); readonly onDidChangeTitleArea: Event = this._onDidChangeTitleArea.event; + private focusedViewContextKey: IContextKey; + private _isVisible: boolean = false; readonly id: string; readonly title: string; @@ -71,13 +74,15 @@ export abstract class ViewletPanel extends Panel implements IView { options: IViewletPanelOptions, @IKeybindingService protected keybindingService: IKeybindingService, @IContextMenuService protected contextMenuService: IContextMenuService, - @IConfigurationService protected readonly configurationService: IConfigurationService + @IConfigurationService protected readonly configurationService: IConfigurationService, + @IContextKeyService contextKeyService: IContextKeyService ) { super(options); this.id = options.id; this.title = options.title; this.actionRunner = options.actionRunner; + this.focusedViewContextKey = FocusedViewContext.bindTo(contextKeyService); } setVisible(visible: boolean): void { @@ -112,8 +117,14 @@ export abstract class ViewletPanel extends Panel implements IView { const focusTracker = trackFocus(this.element); this._register(focusTracker); - this._register(focusTracker.onDidFocus(() => this._onDidFocus.fire())); - this._register(focusTracker.onDidBlur(() => this._onDidBlur.fire())); + this._register(focusTracker.onDidFocus(() => { + this.focusedViewContextKey.set(this.id); + this._onDidFocus.fire(); + })); + this._register(focusTracker.onDidBlur(() => { + this.focusedViewContextKey.reset(); + this._onDidBlur.fire(); + })); } protected renderHeader(container: HTMLElement): void { diff --git a/src/vs/workbench/browser/parts/views/views.ts b/src/vs/workbench/browser/parts/views/views.ts index ddf297c0264..ca709d3bd91 100644 --- a/src/vs/workbench/browser/parts/views/views.ts +++ b/src/vs/workbench/browser/parts/views/views.ts @@ -12,7 +12,7 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IContextKeyService, IContextKeyChangeEvent, IReadableSet, IContextKey, RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { Event, Emitter } from 'vs/base/common/event'; import { sortedDiff, firstIndex, move, isNonEmptyArray } from 'vs/base/common/arrays'; -import { isUndefinedOrNull } from 'vs/base/common/types'; +import { isUndefinedOrNull, isUndefined } from 'vs/base/common/types'; import { MenuId, MenuRegistry, ICommandAction } from 'vs/platform/actions/common/actions'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { localize } from 'vs/nls'; @@ -205,9 +205,9 @@ class ViewDescriptorCollection extends Disposable implements IViewDescriptorColl } export interface IViewState { - visibleGlobal: boolean; - visibleWorkspace: boolean; - collapsed: boolean; + visibleGlobal: boolean | undefined; + visibleWorkspace: boolean | undefined; + collapsed: boolean | undefined; order?: number; size?: number; } @@ -238,6 +238,9 @@ export class ContributableViewsModel extends Disposable { private _onDidMove = this._register(new Emitter<{ from: IViewDescriptorRef; to: IViewDescriptorRef; }>()); readonly onDidMove: Event<{ from: IViewDescriptorRef; to: IViewDescriptorRef; }> = this._onDidMove.event; + private _onDidChangeViewState = this._register(new Emitter()); + protected readonly onDidChangeViewState: Event = this._onDidChangeViewState.event; + constructor( container: ViewContainer, viewsService: IViewsService, @@ -284,7 +287,7 @@ export class ContributableViewsModel extends Disposable { } if (visible) { - this._onDidAdd.fire([{ index: visibleIndex, viewDescriptor, size: state.size, collapsed: state.collapsed }]); + this._onDidAdd.fire([{ index: visibleIndex, viewDescriptor, size: state.size, collapsed: !!state.collapsed }]); } else { this._onDidRemove.fire([{ index: visibleIndex, viewDescriptor }]); } @@ -297,12 +300,15 @@ export class ContributableViewsModel extends Disposable { throw new Error(`Unknown view ${id}`); } - return state.collapsed; + return !!state.collapsed; } setCollapsed(id: string, collapsed: boolean): void { - const { state } = this.find(id); - state.collapsed = collapsed; + const { index, state, viewDescriptor } = this.find(id); + if (state.collapsed !== collapsed) { + state.collapsed = collapsed; + this._onDidChangeViewState.fire({ viewDescriptor, index }); + } } getSize(id: string): number | undefined { @@ -316,8 +322,11 @@ export class ContributableViewsModel extends Disposable { } setSize(id: string, size: number): void { - const { state } = this.find(id); - state.size = size; + const { index, state, viewDescriptor } = this.find(id); + if (state.size !== size) { + state.size = size; + this._onDidChangeViewState.fire({ viewDescriptor, index }); + } } move(from: string, to: string): void { @@ -345,7 +354,7 @@ export class ContributableViewsModel extends Disposable { if (!viewState) { throw new Error(`Unknown view ${viewDescriptor.id}`); } - return viewDescriptor.workspace ? viewState.visibleWorkspace : viewState.visibleGlobal; + return viewDescriptor.workspace ? !!viewState.visibleWorkspace : !!viewState.visibleGlobal; } private find(id: string): { index: number, visibleIndex: number, viewDescriptor: IViewDescriptor, state: IViewState } { @@ -435,7 +444,7 @@ export class ContributableViewsModel extends Disposable { const state = this.viewStates.get(viewDescriptor.id)!; if (this.isViewDescriptorVisible(viewDescriptor)) { - toAdd.push({ index: startIndex++, viewDescriptor, size: state.size, collapsed: state.collapsed }); + toAdd.push({ index: startIndex++, viewDescriptor, size: state.size, collapsed: !!state.collapsed }); } } } @@ -452,10 +461,23 @@ export class ContributableViewsModel extends Disposable { } } +interface IStoredWorkspaceViewState { + collapsed: boolean; + isHidden: boolean; + size?: number; + order?: number; +} + +interface IStoredGlobalViewState { + id: string; + isHidden: boolean; + order?: number; +} + export class PersistentContributableViewsModel extends ContributableViewsModel { - private viewletStateStorageId: string; - private readonly hiddenViewsStorageId: string; + private readonly workspaceViewsStateStorageId: string; + private readonly globalViewsStateStorageId: string; private storageService: IStorageService; @@ -465,97 +487,124 @@ export class PersistentContributableViewsModel extends ContributableViewsModel { @IViewsService viewsService: IViewsService, @IStorageService storageService: IStorageService, ) { - const hiddenViewsStorageId = `${viewletStateStorageId}.hidden`; - const viewStates = PersistentContributableViewsModel.loadViewsStates(viewletStateStorageId, hiddenViewsStorageId, storageService); + const globalViewsStateStorageId = `${viewletStateStorageId}.hidden`; + const viewStates = PersistentContributableViewsModel.loadViewsStates(viewletStateStorageId, globalViewsStateStorageId, storageService); super(container, viewsService, viewStates); - this.viewletStateStorageId = viewletStateStorageId; - this.hiddenViewsStorageId = hiddenViewsStorageId; + this.workspaceViewsStateStorageId = viewletStateStorageId; + this.globalViewsStateStorageId = globalViewsStateStorageId; this.storageService = storageService; - this._register(this.onDidAdd(viewDescriptorRefs => this.saveVisibilityStates(viewDescriptorRefs.map(r => r.viewDescriptor)))); - this._register(this.onDidRemove(viewDescriptorRefs => this.saveVisibilityStates(viewDescriptorRefs.map(r => r.viewDescriptor)))); - this._register(this.storageService.onWillSaveState(() => this.saveViewsStates())); + this._register(Event.any( + this.onDidAdd, + this.onDidRemove, + Event.map(this.onDidMove, ({ from, to }) => [from, to]), + Event.map(this.onDidChangeViewState, viewDescriptorRef => [viewDescriptorRef])) + (viewDescriptorRefs => this.saveViewsStates(viewDescriptorRefs.map(r => r.viewDescriptor)))); } - private saveViewsStates(): void { - const storedViewsStates: { [id: string]: { collapsed: boolean, size?: number, order?: number } } = {}; + private saveViewsStates(viewDescriptors: IViewDescriptor[]): void { + this.saveWorkspaceViewsStates(); + this.saveGlobalViewsStates(); + } + + private saveWorkspaceViewsStates(): void { + const storedViewsStates: { [id: string]: IStoredWorkspaceViewState } = {}; let hasState = false; for (const viewDescriptor of this.viewDescriptors) { const viewState = this.viewStates.get(viewDescriptor.id); if (viewState) { - storedViewsStates[viewDescriptor.id] = { collapsed: viewState.collapsed, size: viewState.size, order: viewState.order }; + storedViewsStates[viewDescriptor.id] = { + collapsed: !!viewState.collapsed, + isHidden: !viewState.visibleWorkspace, + size: viewState.size, + order: viewDescriptor.workspace && viewState ? viewState.order : undefined + }; hasState = true; } } if (hasState) { - this.storageService.store(this.viewletStateStorageId, JSON.stringify(storedViewsStates), StorageScope.WORKSPACE); + this.storageService.store(this.workspaceViewsStateStorageId, JSON.stringify(storedViewsStates), StorageScope.WORKSPACE); } else { - this.storageService.remove(this.viewletStateStorageId, StorageScope.WORKSPACE); + this.storageService.remove(this.workspaceViewsStateStorageId, StorageScope.WORKSPACE); } } - private saveVisibilityStates(viewDescriptors: IViewDescriptor[]): void { - const globalViews: IViewDescriptor[] = viewDescriptors.filter(v => !v.workspace); - const workspaceViews: IViewDescriptor[] = viewDescriptors.filter(v => v.workspace); - if (globalViews.length) { - this.saveVisibilityStatesInScope(globalViews, StorageScope.GLOBAL); - } - if (workspaceViews.length) { - this.saveVisibilityStatesInScope(workspaceViews, StorageScope.WORKSPACE); + private saveGlobalViewsStates(): void { + const storedViewsVisibilityStates = PersistentContributableViewsModel.loadGlobalViewsState(this.globalViewsStateStorageId, this.storageService, StorageScope.GLOBAL); + for (const viewDescriptor of this.viewDescriptors) { + const viewState = this.viewStates.get(viewDescriptor.id); + storedViewsVisibilityStates.set(viewDescriptor.id, { + id: viewDescriptor.id, + isHidden: viewState && viewDescriptor.canToggleVisibility ? !viewState.visibleGlobal : false, + order: !viewDescriptor.workspace && viewState ? viewState.order : undefined + }); } + this.storageService.store(this.globalViewsStateStorageId, JSON.stringify(values(storedViewsVisibilityStates)), StorageScope.GLOBAL); } - private saveVisibilityStatesInScope(viewDescriptors: IViewDescriptor[], scope: StorageScope): void { - const storedViewsVisibilityStates = PersistentContributableViewsModel.loadViewsVisibilityState(this.hiddenViewsStorageId, this.storageService, scope); - for (const viewDescriptor of viewDescriptors) { - if (viewDescriptor.canToggleVisibility) { - const viewState = this.viewStates.get(viewDescriptor.id); - storedViewsVisibilityStates.set(viewDescriptor.id, { id: viewDescriptor.id, isHidden: viewState ? (scope === StorageScope.GLOBAL ? !viewState.visibleGlobal : !viewState.visibleWorkspace) : false }); - } - } - this.storageService.store(this.hiddenViewsStorageId, JSON.stringify(values(storedViewsVisibilityStates)), scope); - } - private static loadViewsStates(viewletStateStorageId: string, hiddenViewsStorageId: string, storageService: IStorageService): Map { + private static loadViewsStates(workspaceViewsStateStorageId: string, globalViewsStateStorageId: string, storageService: IStorageService): Map { const viewStates = new Map(); - const storedViewsStates = JSON.parse(storageService.get(viewletStateStorageId, StorageScope.WORKSPACE, '{}')); - const globalVisibilityStates = this.loadViewsVisibilityState(hiddenViewsStorageId, storageService, StorageScope.GLOBAL); - const workspaceVisibilityStates = this.loadViewsVisibilityState(hiddenViewsStorageId, storageService, StorageScope.WORKSPACE); + const workspaceViewsStates = <{ [id: string]: IStoredWorkspaceViewState }>JSON.parse(storageService.get(workspaceViewsStateStorageId, StorageScope.WORKSPACE, '{}')); + for (const id of Object.keys(workspaceViewsStates)) { + const workspaceViewState = workspaceViewsStates[id]; + viewStates.set(id, { + visibleGlobal: undefined, + visibleWorkspace: isUndefined(workspaceViewState.isHidden) ? undefined : !workspaceViewState.isHidden, + collapsed: workspaceViewState.collapsed, + order: workspaceViewState.order + }); + } - for (const { id, isHidden } of values(globalVisibilityStates)) { - const viewState = storedViewsStates[id]; - if (viewState) { - viewStates.set(id, { ...viewState, ...{ visibleGlobal: !isHidden } }); - } else { - // New workspace - viewStates.set(id, { ...{ visibleGlobal: !isHidden } }); + // Migrate to `viewletStateStorageId` + const workspaceVisibilityStates = this.loadGlobalViewsState(globalViewsStateStorageId, storageService, StorageScope.WORKSPACE); + if (workspaceVisibilityStates.size > 0) { + for (const { id, isHidden } of values(workspaceVisibilityStates)) { + let viewState = viewStates.get(id); + // Not migrated to `viewletStateStorageId` + if (viewState) { + if (isUndefined(viewState.visibleWorkspace)) { + viewState.visibleWorkspace = !isHidden; + } + } else { + viewStates.set(id, { + collapsed: undefined, + visibleGlobal: undefined, + visibleWorkspace: !isHidden, + }); + } } + storageService.remove(globalViewsStateStorageId, StorageScope.WORKSPACE); } - for (const { id, isHidden } of values(workspaceVisibilityStates)) { - const viewState = storedViewsStates[id]; + + const globalViewsStates = this.loadGlobalViewsState(globalViewsStateStorageId, storageService, StorageScope.GLOBAL); + for (const { id, isHidden, order } of values(globalViewsStates)) { + let viewState = viewStates.get(id); if (viewState) { - viewStates.set(id, { ...viewState, ...{ visibleWorkspace: !isHidden } }); + viewState.visibleGlobal = !isHidden; + if (!isUndefined(order)) { + viewState.order = order; + } } else { - // New workspace - viewStates.set(id, { ...{ visibleWorkspace: !isHidden } }); - } - } - for (const id of Object.keys(storedViewsStates)) { - if (!viewStates.has(id)) { - viewStates.set(id, { ...storedViewsStates[id] }); + viewStates.set(id, { + visibleGlobal: !isHidden, + order, + collapsed: undefined, + visibleWorkspace: undefined, + }); } } return viewStates; } - private static loadViewsVisibilityState(hiddenViewsStorageId: string, storageService: IStorageService, scope: StorageScope): Map { - const storedVisibilityStates = >JSON.parse(storageService.get(hiddenViewsStorageId, scope, '[]')); + private static loadGlobalViewsState(globalViewsStateStorageId: string, storageService: IStorageService, scope: StorageScope): Map { + const storedValue = >JSON.parse(storageService.get(globalViewsStateStorageId, scope, '[]')); let hasDuplicates = false; - const storedViewsVisibilityStates = storedVisibilityStates.reduce((result, storedState) => { + const storedGlobalViewsState = storedValue.reduce((result, storedState) => { if (typeof storedState === 'string' /* migration */) { hasDuplicates = hasDuplicates || result.has(storedState); result.set(storedState, { id: storedState, isHidden: true }); @@ -564,13 +613,13 @@ export class PersistentContributableViewsModel extends ContributableViewsModel { result.set(storedState.id, storedState); } return result; - }, new Map()); + }, new Map()); if (hasDuplicates) { - storageService.store(hiddenViewsStorageId, JSON.stringify(values(storedViewsVisibilityStates)), scope); + storageService.store(globalViewsStateStorageId, JSON.stringify(values(storedGlobalViewsState)), scope); } - return storedViewsVisibilityStates; + return storedGlobalViewsState; } } @@ -725,4 +774,4 @@ export function createFileIconThemableTreeContainerScope(container: HTMLElement, return themeService.onDidFileIconThemeChange(onDidChangeFileIconTheme); } -registerSingleton(IViewsService, ViewsService); \ No newline at end of file +registerSingleton(IViewsService, ViewsService); diff --git a/src/vs/workbench/browser/web.simpleservices.ts b/src/vs/workbench/browser/web.simpleservices.ts index d450e7da4f3..3a5be568517 100644 --- a/src/vs/workbench/browser/web.simpleservices.ts +++ b/src/vs/workbench/browser/web.simpleservices.ts @@ -791,7 +791,7 @@ export class SimpleWindowsService implements IWindowsService { return Promise.resolve(this.windowCount); } - log(_severity: string, ..._messages: string[]): Promise { + log(_severity: string, _args: string[]): Promise { return Promise.resolve(); } diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index 51139cefce6..6f5386ee4a8 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 } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ITreeViewDataProvider } from 'vs/workbench/common/views'; import { localize } from 'vs/nls'; import { IViewlet } from 'vs/workbench/common/viewlet'; @@ -21,6 +21,7 @@ import { IMarkdownString } from 'vs/base/common/htmlContent'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; export const TEST_VIEW_CONTAINER_ID = 'workbench.view.extension.test'; +export const FocusedViewContext = new RawContextKey('focusedView', ''); export namespace Extensions { export const ViewContainersRegistry = 'workbench.registry.view.containers'; diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index f5a80eea68b..e8d7d850123 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -181,7 +181,7 @@ export class CommentNode extends Disposable { let hasReactionHandler = this.commentService.hasReactionHandler(this.owner); if (hasReactionHandler) { - let toggleReactionAction = this.createReactionPicker2(this.comment.commentReactions || []); + let toggleReactionAction = this.createReactionPicker(this.comment.commentReactions || []); actions.push(toggleReactionAction); } @@ -208,7 +208,7 @@ export class CommentNode extends Disposable { actionViewItemProvider(action: Action) { let options = {}; - if (action.id === 'comment.delete' || action.id === 'comment.edit' || action.id === ToggleReactionsAction.ID) { + if (action.id === ToggleReactionsAction.ID) { options = { label: false, icon: true }; } else { options = { label: false, icon: true }; @@ -226,7 +226,7 @@ export class CommentNode extends Disposable { } } - private createReactionPicker2(reactionGroup: modes.CommentReaction[]): ToggleReactionsAction { + private createReactionPicker(reactionGroup: modes.CommentReaction[]): ToggleReactionsAction { let toggleReactionActionViewItem: DropdownMenuActionViewItem; let toggleReactionAction = this._register(new ToggleReactionsAction(() => { if (toggleReactionActionViewItem) { @@ -321,14 +321,8 @@ export class CommentNode extends Disposable { }); if (hasReactionHandler) { - let toggleReactionAction = this.createReactionPicker2(this.comment.commentReactions || []); + let toggleReactionAction = this.createReactionPicker(this.comment.commentReactions || []); this._reactionsActionBar.push(toggleReactionAction, { label: false, icon: true }); - } else { - // let reactionGroup = this.commentService.getReactionGroup(this.owner); - // if (reactionGroup && reactionGroup.length) { - // let toggleReactionAction = this.createReactionPicker2(reactionGroup || []); - // this._reactionsActionBar.push(toggleReactionAction, { label: false, icon: true }); - // } } } @@ -526,4 +520,4 @@ function fillInActions(groups: [string, Array local.dispose()); this._onDidClose.fire(undefined); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/contrib/comments/browser/media/review.css b/src/vs/workbench/contrib/comments/browser/media/review.css index 767e4c97057..47f8feb5fc9 100644 --- a/src/vs/workbench/contrib/comments/browser/media/review.css +++ b/src/vs/workbench/contrib/comments/browser/media/review.css @@ -400,7 +400,7 @@ .monaco-editor .review-widget .action-item { min-width: 18px; - min-height: 18px; + min-height: 20px; margin-left: 4px; } diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index 9114575d433..87094b93ebc 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -30,6 +30,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet'; import { ILabelService } from 'vs/platform/label/common/label'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; const $ = dom.$; @@ -56,9 +57,10 @@ export class BreakpointsView extends ViewletPanel { @IThemeService private readonly themeService: IThemeService, @IEditorService private readonly editorService: IEditorService, @IContextViewService private readonly contextViewService: IContextViewService, - @IConfigurationService configurationService: IConfigurationService + @IConfigurationService configurationService: IConfigurationService, + @IContextKeyService contextKeyService: IContextKeyService, ) { - super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: nls.localize('breakpointsSection', "Breakpoints Section") }, keybindingService, contextMenuService, configurationService); + super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: nls.localize('breakpointsSection', "Breakpoints Section") }, keybindingService, contextMenuService, configurationService, contextKeyService); this.minimumBodySize = this.maximumBodySize = this.getExpandedBodySize(); this._register(this.debugService.getModel().onDidChangeBreakpoints(() => this.onBreakpointsChange())); diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index e8eac6f7d23..142460a504c 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -60,7 +60,7 @@ export class CallStackView extends ViewletPanel { @IMenuService menuService: IMenuService, @IContextKeyService readonly contextKeyService: IContextKeyService, ) { - super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: nls.localize('callstackSection', "Call Stack Section") }, keybindingService, contextMenuService, configurationService); + super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: nls.localize('callstackSection', "Call Stack Section") }, keybindingService, contextMenuService, configurationService, contextKeyService); this.callStackItemType = CONTEXT_CALLSTACK_ITEM_TYPE.bindTo(contextKeyService); this.contributedContextMenu = menuService.createMenu(MenuId.DebugCallStackContext, contextKeyService); diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 84afaa1d2f1..9a1431da67e 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -534,7 +534,7 @@ MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { // Touch Bar if (isMacintosh) { - const registerTouchBarEntry = (id: string, title: string, order: number, when: ContextKeyExpr, icon: string) => { + const registerTouchBarEntry = (id: string, title: string, order: number, when: ContextKeyExpr | undefined, icon: string) => { MenuRegistry.appendMenuItem(MenuId.TouchBarContext, { command: { id, diff --git a/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts b/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts index ef9f3cb5d37..70a962e46ec 100644 --- a/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts +++ b/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts @@ -402,7 +402,7 @@ export class LoadedScriptsView extends ViewletPanel { @IDebugService private readonly debugService: IDebugService, @ILabelService private readonly labelService: ILabelService ) { - super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: nls.localize('loadedScriptsSection', "Loaded Scripts Section") }, keybindingService, contextMenuService, configurationService); + super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: nls.localize('loadedScriptsSection', "Loaded Scripts Section") }, keybindingService, contextMenuService, configurationService, contextKeyService); this.loadedScriptsItemType = CONTEXT_LOADED_SCRIPTS_ITEM_TYPE.bindTo(contextKeyService); } diff --git a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts index b67785c3d98..a0f2120b920 100644 --- a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts @@ -587,7 +587,13 @@ export class RawDebugSession { } } else { - args._.push(a2); + const match = /^--(.+)$/.exec(a2); + if (match && match.length === 2) { + const key = match[1]; + (args)[key] = true; + } else { + args._.push(a2); + } } } } diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index 581042b38cc..71559c50d3c 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -29,6 +29,7 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; const $ = dom.$; let forgetScopes = true; @@ -49,9 +50,10 @@ export class VariablesView extends ViewletPanel { @IKeybindingService keybindingService: IKeybindingService, @IConfigurationService configurationService: IConfigurationService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IClipboardService private readonly clipboardService: IClipboardService + @IClipboardService private readonly clipboardService: IClipboardService, + @IContextKeyService contextKeyService: IContextKeyService ) { - super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: nls.localize('variablesSection', "Variables Section") }, keybindingService, contextMenuService, configurationService); + super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: nls.localize('variablesSection', "Variables Section") }, keybindingService, contextMenuService, configurationService, contextKeyService); // Use scheduler to prevent unnecessary flashing this.onFocusStackFrameScheduler = new RunOnceScheduler(() => { diff --git a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts index 970b8abfa7d..7b0430d2504 100644 --- a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts +++ b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts @@ -29,6 +29,7 @@ import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; import { FuzzyScore } from 'vs/base/common/filters'; import { IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { variableSetEmitter, VariablesRenderer } from 'vs/workbench/contrib/debug/browser/variablesView'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024; @@ -45,8 +46,9 @@ export class WatchExpressionsView extends ViewletPanel { @IKeybindingService keybindingService: IKeybindingService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IConfigurationService configurationService: IConfigurationService, + @IContextKeyService contextKeyService: IContextKeyService, ) { - super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: nls.localize('watchExpressionsSection', "Watch Expressions Section") }, keybindingService, contextMenuService, configurationService); + super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: nls.localize('watchExpressionsSection', "Watch Expressions Section") }, keybindingService, contextMenuService, configurationService, contextKeyService); this.onWatchExpressionsUpdatedScheduler = new RunOnceScheduler(() => { this.needsRefresh = false; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index df4be0957a3..fdb0056d228 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -1694,7 +1694,7 @@ export class InstallRecommendedExtensionAction extends Action { return this.viewletService.openViewlet(VIEWLET_ID, true) .then(viewlet => viewlet as IExtensionsViewlet) .then(viewlet => { - viewlet.search('@recommended '); + viewlet.search(`@id:${this.extensionId}`); viewlet.focus(); return this.extensionWorkbenchService.queryGallery({ names: [this.extensionId], source: 'install-recommendation', pageSize: 1 }, CancellationToken.None) .then(pager => { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 2e889a62b7f..f9bf9ad4458 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -47,6 +47,7 @@ import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async import { isUIExtension } from 'vs/workbench/services/extensions/common/extensionsUtil'; import { IProductService } from 'vs/platform/product/common/product'; import { SeverityIcon } from 'vs/platform/severityIcon/common/severityIcon'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; class ExtensionsViewState extends Disposable implements IExtensionsViewState { @@ -101,8 +102,9 @@ export class ExtensionsListView extends ViewletPanel { @IWorkbenchThemeService private readonly workbenchThemeService: IWorkbenchThemeService, @IExtensionManagementServerService protected readonly extensionManagementServerService: IExtensionManagementServerService, @IProductService protected readonly productService: IProductService, + @IContextKeyService contextKeyService: IContextKeyService, ) { - super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: options.title }, keybindingService, contextMenuService, configurationService); + super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: options.title }, keybindingService, contextMenuService, configurationService, contextKeyService); this.server = options.server; } @@ -849,9 +851,10 @@ export class ServerExtensionsView extends ExtensionsListView { @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, @IExtensionManagementServerService extensionManagementServerService: IExtensionManagementServerService, @IProductService productService: IProductService, + @IContextKeyService contextKeyService: IContextKeyService, ) { options.server = server; - super(options, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, editorService, tipsService, telemetryService, configurationService, contextService, experimentService, workbenchThemeService, extensionManagementServerService, productService); + super(options, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, editorService, tipsService, telemetryService, configurationService, contextService, experimentService, workbenchThemeService, extensionManagementServerService, productService, contextKeyService); this._register(onDidChangeTitle(title => this.updateTitle(title))); } diff --git a/src/vs/workbench/contrib/extensions/common/extensionQuery.ts b/src/vs/workbench/contrib/extensions/common/extensionQuery.ts index 445e23db188..6c5a2505331 100644 --- a/src/vs/workbench/contrib/extensions/common/extensionQuery.ts +++ b/src/vs/workbench/contrib/extensions/common/extensionQuery.ts @@ -12,12 +12,13 @@ export class Query { } static suggestions(query: string): string[] { - const commands = ['installed', 'outdated', 'enabled', 'disabled', 'builtin', 'recommended', 'sort', 'category', 'tag', 'ext']; + const commands = ['installed', 'outdated', 'enabled', 'disabled', 'builtin', 'recommended', 'sort', 'category', 'tag', 'ext', 'id']; const subcommands = { 'sort': ['installs', 'rating', 'name'], 'category': ['"programming languages"', 'snippets', 'linters', 'themes', 'debuggers', 'formatters', 'keymaps', '"scm providers"', 'other', '"extension packs"', '"language packs"'], 'tag': [''], - 'ext': [''] + 'ext': [''], + 'id': [''] }; let queryContains = (substr: string) => query.indexOf(substr) > -1; @@ -77,4 +78,4 @@ export class Query { equals(other: Query): boolean { return this.value === other.value && this.sortBy === other.sortBy; } -} \ No newline at end of file +} diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionTipsService.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionTipsService.ts index f4af7a1c9b6..b64e6d66977 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionTipsService.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionTipsService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { join } from 'vs/base/common/path'; +import { join, basename } from 'vs/base/common/path'; import { forEach } from 'vs/base/common/collections'; import { Disposable } from 'vs/base/common/lifecycle'; import { match } from 'vs/base/common/glob'; @@ -43,6 +43,8 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { extname } from 'vs/base/common/resources'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IExeBasedExtensionTip } from 'vs/platform/product/common/product'; +import { timeout } from 'vs/base/common/async'; const milliSecondsInADay = 1000 * 60 * 60 * 24; const choiceNever = localize('neverShowAgain', "Don't Show Again"); @@ -71,7 +73,8 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe _serviceBrand: any; private _fileBasedRecommendations: { [id: string]: { recommendedTime: number, sources: ExtensionRecommendationSource[] }; } = Object.create(null); - private _exeBasedRecommendations: { [id: string]: string; } = Object.create(null); + private _exeBasedRecommendations: { [id: string]: IExeBasedExtensionTip; } = Object.create(null); + private _importantExeBasedRecommendations: { [id: string]: IExeBasedExtensionTip; } = Object.create(null); private _availableRecommendations: { [pattern: string]: string[] } = Object.create(null); private _allWorkspaceRecommendedExtensions: IExtensionRecommendation[] = []; private _dynamicWorkspaceRecommendations: string[] = []; @@ -187,7 +190,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe forEach(this._exeBasedRecommendations, entry => output[entry.key.toLowerCase()] = { reasonId: ExtensionRecommendationReason.Executable, - reasonText: localize('exeBasedRecommendation', "This extension is recommended because you have {0} installed.", entry.value) + reasonText: localize('exeBasedRecommendation', "This extension is recommended because you have {0} installed.", entry.value.friendlyName) }); forEach(this._fileBasedRecommendations, entry => output[entry.key.toLowerCase()] = { @@ -496,6 +499,100 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe //#endregion + //#region important exe based extension + + private async promptForImportantExeBasedExtension(): Promise { + + const storageKey = 'extensionsAssistant/workspaceRecommendationsIgnore'; + const config = this.configurationService.getValue(ConfigurationKey); + + if (config.ignoreRecommendations + || config.showRecommendationsOnlyOnDemand + || this.storageService.getBoolean(storageKey, StorageScope.WORKSPACE, false)) { + return false; + } + + const installed = await this.extensionManagementService.getInstalled(ExtensionType.User); + let recommendationsToSuggest = Object.keys(this._importantExeBasedRecommendations); + recommendationsToSuggest = this.filterAllIgnoredInstalledAndNotAllowed(recommendationsToSuggest, installed); + if (recommendationsToSuggest.length === 0) { + return false; + } + + const id = recommendationsToSuggest[0]; + const tip = this._importantExeBasedRecommendations[id]; + const message = localize('exeRecommended', "The '{0}' extension is recommended as you have {1} installed on your system.", tip.friendlyName!, tip.exeFriendlyName || basename(tip.windowsPath!)); + + this.notificationService.prompt(Severity.Info, message, + [{ + label: localize('install', 'Install'), + run: () => { + /* __GDPR__ + "exeExtensionRecommendations:popup" : { + "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('exeExtensionRecommendations:popup', { userReaction: 'install', extensionId: name }); + this.instantiationService.createInstance(InstallRecommendedExtensionAction, id).run(); + } + }, { + label: localize('showRecommendations', "Show Recommendations"), + run: () => { + /* __GDPR__ + "exeExtensionRecommendations:popup" : { + "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('exeExtensionRecommendations:popup', { userReaction: 'show', extensionId: name }); + + const recommendationsAction = this.instantiationService.createInstance(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, localize('showRecommendations', "Show Recommendations")); + recommendationsAction.run(); + recommendationsAction.dispose(); + } + }, { + label: choiceNever, + isSecondary: true, + run: () => { + this.addToImportantRecommendationsIgnore(id); + /* __GDPR__ + "exeExtensionRecommendations:popup" : { + "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('exeExtensionRecommendations:popup', { userReaction: 'neverShowAgain', extensionId: name }); + this.notificationService.prompt( + Severity.Info, + localize('ignoreExtensionRecommendations', "Do you want to ignore all extension recommendations?"), + [{ + label: localize('ignoreAll', "Yes, Ignore All"), + run: () => this.setIgnoreRecommendationsConfig(true) + }, { + label: localize('no', "No"), + run: () => this.setIgnoreRecommendationsConfig(false) + }] + ); + } + }], + { + sticky: true, + onCancel: () => { + /* __GDPR__ + "exeExtensionRecommendations:popup" : { + "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('exeExtensionRecommendations:popup', { userReaction: 'cancelled', extensionId: name }); + } + } + ); + + return true; + } + //#region fileBasedRecommendations getFileBasedRecommendations(): IExtensionRecommendation[] { @@ -597,7 +694,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe const now = Date.now(); forEach(this._availableRecommendations, entry => { let { key: pattern, value: ids } = entry; - if (match(pattern, model.uri.path)) { + if (match(pattern, model.uri.toString())) { for (let id of ids) { if (caseInsensitiveGet(product.extensionImportantTips, id)) { recommendationsToSuggest.push(id); @@ -646,21 +743,8 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe } private async promptRecommendedExtensionForFileType(recommendationsToSuggest: string[], installed: ILocalExtension[]): Promise { - const importantRecommendationsIgnoreList = JSON.parse(this.storageService.get('extensionsAssistant/importantRecommendationsIgnore', StorageScope.GLOBAL, '[]')); - const installedExtensionsIds = installed.reduce((result, i) => { result.add(i.identifier.id.toLowerCase()); return result; }, new Set()); - recommendationsToSuggest = recommendationsToSuggest.filter(id => { - if (importantRecommendationsIgnoreList.indexOf(id) !== -1) { - return false; - } - if (!this.isExtensionAllowedToBeRecommended(id)) { - return false; - } - if (installedExtensionsIds.has(id.toLowerCase())) { - return false; - } - return true; - }); + recommendationsToSuggest = this.filterAllIgnoredInstalledAndNotAllowed(recommendationsToSuggest, installed); if (recommendationsToSuggest.length === 0) { return false; } @@ -670,22 +754,12 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe if (!entry) { return false; } - const name = entry['name']; - + const name = entry.name; let message = localize('reallyRecommended2', "The '{0}' extension is recommended for this file type.", name); - // Temporary fix for the only extension pack we recommend. See https://github.com/Microsoft/vscode/issues/35364 - if (id === 'vscjava.vscode-java-pack') { + if (entry.isExtensionPack) { message = localize('reallyRecommendedExtensionPack', "The '{0}' extension pack is recommended for this file type.", name); } - const setIgnoreRecommendationsConfig = (configVal: boolean) => { - this.configurationService.updateValue('extensions.ignoreRecommendations', configVal, ConfigurationTarget.USER); - if (configVal) { - const ignoreWorkspaceRecommendationsStorageKey = 'extensionsAssistant/workspaceRecommendationsIgnore'; - this.storageService.store(ignoreWorkspaceRecommendationsStorageKey, true, StorageScope.WORKSPACE); - } - }; - this.notificationService.prompt(Severity.Info, message, [{ label: localize('install', 'Install'), @@ -718,12 +792,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe label: choiceNever, isSecondary: true, run: () => { - importantRecommendationsIgnoreList.push(id); - this.storageService.store( - 'extensionsAssistant/importantRecommendationsIgnore', - JSON.stringify(importantRecommendationsIgnoreList), - StorageScope.GLOBAL - ); + this.addToImportantRecommendationsIgnore(id); /* __GDPR__ "extensionRecommendations:popup" : { "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, @@ -736,10 +805,10 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe localize('ignoreExtensionRecommendations', "Do you want to ignore all extension recommendations?"), [{ label: localize('ignoreAll', "Yes, Ignore All"), - run: () => setIgnoreRecommendationsConfig(true) + run: () => this.setIgnoreRecommendationsConfig(true) }, { label: localize('no', "No"), - run: () => setIgnoreRecommendationsConfig(false) + run: () => this.setIgnoreRecommendationsConfig(false) }] ); } @@ -831,6 +900,42 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe ); } + private filterAllIgnoredInstalledAndNotAllowed(recommendationsToSuggest: string[], installed: ILocalExtension[]): string[] { + + const importantRecommendationsIgnoreList = JSON.parse(this.storageService.get('extensionsAssistant/importantRecommendationsIgnore', StorageScope.GLOBAL, '[]')); + const installedExtensionsIds = installed.reduce((result, i) => { result.add(i.identifier.id.toLowerCase()); return result; }, new Set()); + return recommendationsToSuggest.filter(id => { + if (importantRecommendationsIgnoreList.indexOf(id) !== -1) { + return false; + } + if (!this.isExtensionAllowedToBeRecommended(id)) { + return false; + } + if (installedExtensionsIds.has(id.toLowerCase())) { + return false; + } + return true; + }); + } + + private addToImportantRecommendationsIgnore(id: string) { + const importantRecommendationsIgnoreList = JSON.parse(this.storageService.get('extensionsAssistant/importantRecommendationsIgnore', StorageScope.GLOBAL, '[]')); + importantRecommendationsIgnoreList.push(id); + this.storageService.store( + 'extensionsAssistant/importantRecommendationsIgnore', + JSON.stringify(importantRecommendationsIgnoreList), + StorageScope.GLOBAL + ); + } + + private setIgnoreRecommendationsConfig(configVal: boolean) { + this.configurationService.updateValue('extensions.ignoreRecommendations', configVal, ConfigurationTarget.USER); + if (configVal) { + const ignoreWorkspaceRecommendationsStorageKey = 'extensionsAssistant/workspaceRecommendationsIgnore'; + this.storageService.store(ignoreWorkspaceRecommendationsStorageKey, true, StorageScope.WORKSPACE); + } + } + //#endregion //#region otherRecommendations @@ -857,18 +962,18 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe } private fetchProactiveRecommendations(calledDuringStartup?: boolean): Promise { - let fetchPromise = Promise.resolve(undefined); + let fetchPromise = Promise.resolve(undefined); if (!this.proactiveRecommendationsFetched) { this.proactiveRecommendationsFetched = true; - // Executable based recommendations carry out a lot of file stats, so run them after 10 secs - // So that the startup is not affected + // Executable based recommendations carry out a lot of file stats, delay the resolution so that the startup is not affected + // 10 sec for regular extensions + // 3 secs for important - fetchPromise = new Promise((c, e) => { - setTimeout(() => { - Promise.all([this.fetchExecutableRecommendations(), this.fetchDynamicWorkspaceRecommendations()]).then(() => c(undefined)); - }, calledDuringStartup ? 10000 : 0); - }); + const importantExeBasedRecommendations = timeout(calledDuringStartup ? 3000 : 0).then(_ => this.fetchExecutableRecommendations(true)); + importantExeBasedRecommendations.then(_ => this.promptForImportantExeBasedExtension()); + + fetchPromise = timeout(calledDuringStartup ? 10000 : 0).then(_ => Promise.all([this.fetchDynamicWorkspaceRecommendations(), this.fetchExecutableRecommendations(false), importantExeBasedRecommendations])); } return fetchPromise; @@ -877,20 +982,22 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe /** * If user has any of the tools listed in product.exeBasedExtensionTips, fetch corresponding recommendations */ - private fetchExecutableRecommendations(): Promise { + private fetchExecutableRecommendations(important: boolean): Promise { const homeDir = os.homedir(); let foundExecutables: Set = new Set(); - let findExecutable = (exeName: string, path: string) => { + let findExecutable = (exeName: string, tip: IExeBasedExtensionTip, path: string) => { return pfs.fileExists(path).then(exists => { if (exists && !foundExecutables.has(exeName)) { foundExecutables.add(exeName); - (product.exeBasedExtensionTips[exeName]['recommendations'] || []) - .forEach(extensionId => { - if (product.exeBasedExtensionTips[exeName]['friendlyName']) { - this._exeBasedRecommendations[extensionId.toLowerCase()] = product.exeBasedExtensionTips[exeName]['friendlyName']; + (tip['recommendations'] || []).forEach(extensionId => { + if (tip.friendlyName) { + if (important) { + this._importantExeBasedRecommendations[extensionId.toLowerCase()] = tip; } - }); + this._exeBasedRecommendations[extensionId.toLowerCase()] = tip; + } + }); } }); }; @@ -901,8 +1008,10 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe if (typeof entry.value !== 'object' || !Array.isArray(entry.value['recommendations'])) { return; } - - let exeName = entry.key; + if (important !== !!entry.value.important) { + return; + } + const exeName = entry.key; if (process.platform === 'win32') { let windowsPath = entry.value['windowsPath']; if (!windowsPath || typeof windowsPath !== 'string') { @@ -913,10 +1022,10 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe .replace('%ProgramFiles%', process.env['ProgramFiles']!) .replace('%APPDATA%', process.env['APPDATA']!) .replace('%WINDIR%', process.env['WINDIR']!); - promises.push(findExecutable(exeName, windowsPath)); + promises.push(findExecutable(exeName, entry.value, windowsPath)); } else { - promises.push(findExecutable(exeName, join('/usr/local/bin', exeName))); - promises.push(findExecutable(exeName, join(homeDir, exeName))); + promises.push(findExecutable(exeName, entry.value, join('/usr/local/bin', exeName))); + promises.push(findExecutable(exeName, entry.value, join(homeDir, exeName))); } }); diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts index 90db9e85e98..cf2b6c79522 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts @@ -44,6 +44,8 @@ import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedPr import { ExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/electron-browser/extensionManagementServerService'; import { IProductService } from 'vs/platform/product/common/product'; import { ILabelService } from 'vs/platform/label/common/label'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; suite('ExtensionsListView Tests', () => { @@ -93,6 +95,7 @@ suite('ExtensionsListView Tests', () => { instantiationService.stub(IExtensionManagementService, 'onUninstallExtension', uninstallEvent.event); instantiationService.stub(IExtensionManagementService, 'onDidUninstallExtension', didUninstallEvent.event); instantiationService.stub(IRemoteAgentService, RemoteAgentService); + instantiationService.stub(IContextKeyService, MockContextKeyService); instantiationService.stub(IExtensionManagementServerService, new class extends ExtensionManagementServerService { private _localExtensionManagementServer: IExtensionManagementServer = { extensionManagementService: instantiationService.get(IExtensionManagementService), label: 'local', authority: 'vscode-local' }; diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index 044c0db7733..8b29304e715 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -203,7 +203,7 @@ appendEditorTitleContextMenuItem(REVEAL_IN_OS_COMMAND_ID, REVEAL_IN_OS_LABEL, Re appendEditorTitleContextMenuItem(REVEAL_IN_OS_COMMAND_ID, REVEAL_IN_OS_LABEL, ContextKeyExpr.and(IsWebContext.toNegated(), ResourceContextKey.Scheme.isEqualTo(Schemas.userData))); appendEditorTitleContextMenuItem(REVEAL_IN_EXPLORER_COMMAND_ID, nls.localize('revealInSideBar', "Reveal in Side Bar"), ResourceContextKey.IsFileSystemResource); -function appendEditorTitleContextMenuItem(id: string, title: string, when: ContextKeyExpr, group?: string): void { +function appendEditorTitleContextMenuItem(id: string, title: string, when: ContextKeyExpr | undefined, group?: string): void { // Menu MenuRegistry.appendMenuItem(MenuId.EditorTitleContext, { diff --git a/src/vs/workbench/contrib/files/browser/views/emptyView.ts b/src/vs/workbench/contrib/files/browser/views/emptyView.ts index 66b56e6c22a..fddb247aa63 100644 --- a/src/vs/workbench/contrib/files/browser/views/emptyView.ts +++ b/src/vs/workbench/contrib/files/browser/views/emptyView.ts @@ -25,6 +25,7 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { ILabelService } from 'vs/platform/label/common/label'; import { Schemas } from 'vs/base/common/network'; import { isWeb } from 'vs/base/common/platform'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; export class EmptyView extends ViewletPanel { @@ -44,9 +45,10 @@ export class EmptyView extends ViewletPanel { @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IConfigurationService configurationService: IConfigurationService, @IWorkbenchEnvironmentService private environmentService: IWorkbenchEnvironmentService, - @ILabelService private labelService: ILabelService + @ILabelService private labelService: ILabelService, + @IContextKeyService contextKeyService: IContextKeyService ) { - super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: nls.localize('explorerSection', "Files Explorer Section") }, keybindingService, contextMenuService, configurationService); + super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: nls.localize('explorerSection', "Files Explorer Section") }, keybindingService, contextMenuService, configurationService, contextKeyService); this._register(this.contextService.onDidChangeWorkbenchState(() => this.setLabels())); this._register(this.labelService.onDidChangeFormatters(() => this.setLabels())); } diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index fe6751e85db..9b464d453c9 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -90,7 +90,7 @@ export class ExplorerView extends ViewletPanel { @IClipboardService private clipboardService: IClipboardService, @IFileService private readonly fileService: IFileService ) { - super({ ...(options as IViewletPanelOptions), id: ExplorerView.ID, ariaHeaderLabel: nls.localize('explorerSection', "Files Explorer Section") }, keybindingService, contextMenuService, configurationService); + super({ ...(options as IViewletPanelOptions), id: ExplorerView.ID, ariaHeaderLabel: nls.localize('explorerSection', "Files Explorer Section") }, keybindingService, contextMenuService, configurationService, contextKeyService); this.resourceContext = instantiationService.createInstance(ResourceContextKey); this._register(this.resourceContext); diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index 5ed1a09eb84..893ea55f5be 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -80,7 +80,7 @@ export class OpenEditorsView extends ViewletPanel { super({ ...(options as IViewletPanelOptions), ariaHeaderLabel: nls.localize({ key: 'openEditosrSection', comment: ['Open is an adjective'] }, "Open Editors Section"), - }, keybindingService, contextMenuService, configurationService); + }, keybindingService, contextMenuService, configurationService, contextKeyService); this.structuralRefreshDelay = 0; this.listRefreshScheduler = new RunOnceScheduler(() => { diff --git a/src/vs/workbench/contrib/markers/browser/markersPanelActions.ts b/src/vs/workbench/contrib/markers/browser/markersPanelActions.ts index 92fd3b3c925..f2747c02b15 100644 --- a/src/vs/workbench/contrib/markers/browser/markersPanelActions.ts +++ b/src/vs/workbench/contrib/markers/browser/markersPanelActions.ts @@ -159,6 +159,7 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { toggleLayout(small: boolean) { if (this.container) { DOM.toggleClass(this.container, 'small', small); + this.adjustInputBox(); } } @@ -247,7 +248,7 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { } private adjustInputBox(): void { - this.filterInputBox.inputElement.style.paddingRight = (DOM.getTotalWidth(this.controlsContainer) || 20) + 'px'; + this.filterInputBox.inputElement.style.paddingRight = DOM.hasClass(this.container, 'small') || DOM.hasClass(this.filterBadge, 'hidden') ? '25px' : '150px'; } // Action toolbar is swallowing some keys for action items which should not be for an input box diff --git a/src/vs/workbench/contrib/outline/browser/outlinePanel.ts b/src/vs/workbench/contrib/outline/browser/outlinePanel.ts index 74c1efc7e98..fd070d880eb 100644 --- a/src/vs/workbench/contrib/outline/browser/outlinePanel.ts +++ b/src/vs/workbench/contrib/outline/browser/outlinePanel.ts @@ -266,7 +266,7 @@ export class OutlinePanel extends ViewletPanel { @IContextKeyService contextKeyService: IContextKeyService, @IContextMenuService contextMenuService: IContextMenuService, ) { - super(options, keybindingService, contextMenuService, configurationService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService); this._outlineViewState.restore(this._storageService); this._contextKeyFocused = OutlineViewFocused.bindTo(contextKeyService); this._contextKeyFiltered = OutlineViewFiltered.bindTo(contextKeyService); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 70f8cfc7188..0d080e7d577 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -132,6 +132,7 @@ export class SettingsEditor2 extends BaseEditor { private tocFocusedElement: SettingsTreeGroupElement | null; private settingsTreeScrollTop = 0; + private dimension: DOM.Dimension; constructor( @ITelemetryService telemetryService: ITelemetryService, @@ -279,10 +280,11 @@ export class SettingsEditor2 extends BaseEditor { } layout(dimension: DOM.Dimension): void { + this.dimension = dimension; this.layoutTrees(dimension); - const innerWidth = dimension.width - 24 * 2; // 24px padding on left and right - const monacoWidth = (innerWidth > 1000 ? 1000 : innerWidth) - 10; + const innerWidth = Math.min(1000, dimension.width) - 24 * 2; // 24px padding on left and right; + const monacoWidth = innerWidth - 10 - this.countElement.clientWidth - 12; // minus padding inside inputbox, countElement width, extra padding before countElement this.searchWidget.layout({ height: 20, width: monacoWidth }); DOM.toggleClass(this.rootElement, 'mid-width', dimension.width < 1000 && dimension.width >= 600); @@ -1231,7 +1233,11 @@ export class SettingsEditor2 extends BaseEditor { : 'none'; if (!this.searchResultModel) { - this.countElement.style.display = 'none'; + if (this.countElement.style.display !== 'none') { + this.countElement.style.display = 'none'; + this.layout(this.dimension); + } + DOM.removeClass(this.rootElement, 'no-results'); return; } @@ -1244,7 +1250,10 @@ export class SettingsEditor2 extends BaseEditor { default: this.countElement.innerText = localize('moreThanOneResult', "{0} Settings Found", count); } - this.countElement.style.display = 'block'; + if (this.countElement.style.display !== 'block') { + this.countElement.style.display = 'block'; + this.layout(this.dimension); + } DOM.toggleClass(this.rootElement, 'no-results', count === 0); } } diff --git a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index db9568a6eb4..beef3cdc9fb 100644 --- a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +++ b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts @@ -18,7 +18,7 @@ import { SCMViewlet } from 'vs/workbench/contrib/scm/browser/scmViewlet'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { ContextKeyDefinedExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; @@ -115,7 +115,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'scm.acceptInput', description: { description: localize('scm accept', "SCM: Accept Input"), args: [] }, weight: KeybindingWeight.WorkbenchContrib, - when: new ContextKeyDefinedExpr('scmRepository'), + when: ContextKeyExpr.has('scmRepository'), primary: KeyMod.CtrlCmd | KeyCode.Enter, handler: accessor => { const contextKeyService = accessor.get(IContextKeyService); @@ -134,4 +134,4 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); -registerSingleton(ISCMService, SCMService); \ No newline at end of file +registerSingleton(ISCMService, SCMService); diff --git a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts index e3d7a922c78..00e6d9356ce 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts @@ -234,7 +234,7 @@ export class MainPanel extends ViewletPanel { @IMenuService private readonly menuService: IMenuService, @IConfigurationService configurationService: IConfigurationService ) { - super(options, keybindingService, contextMenuService, configurationService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService); } protected renderBody(container: HTMLElement): void { @@ -324,7 +324,7 @@ export class MainPanel extends ViewletPanel { } private onListSelectionChange(e: IListEvent): void { - if (e.elements.length > 0 && e.browserEvent) { + if (e.elements.length > 0) { const scrollTop = this.list.scrollTop; this.viewModel.setVisibleRepositories(e.elements); this.list.scrollTop = scrollTop; @@ -495,6 +495,7 @@ class ResourceRenderer implements IListRenderer const icon = theme.type === LIGHT ? resource.decorations.icon : resource.decorations.iconDark; template.fileLabel.setFile(resource.sourceUri, { fileDecorations: { colors: false, badges: !icon, data: resource.decorations } }); + template.actionBar.clear(); template.actionBar.context = resource; const disposables = new DisposableStore(); @@ -733,7 +734,7 @@ export class RepositoryPanel extends ViewletPanel { @IContextKeyService contextKeyService: IContextKeyService, @IMenuService protected menuService: IMenuService ) { - super(options, keybindingService, contextMenuService, configurationService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService); this.menus = instantiationService.createInstance(SCMMenus, this.repository.provider); this._register(this.menus); diff --git a/src/vs/workbench/contrib/search/browser/replaceService.ts b/src/vs/workbench/contrib/search/browser/replaceService.ts index 71d38344f4c..ff9c9bacaf1 100644 --- a/src/vs/workbench/contrib/search/browser/replaceService.ts +++ b/src/vs/workbench/contrib/search/browser/replaceService.ts @@ -67,7 +67,7 @@ class ReplacePreviewModel extends Disposable { resolve(replacePreviewUri: URI): Promise { const fileResource = toFileResource(replacePreviewUri); - const fileMatch = this.searchWorkbenchService.searchModel.searchResult.matches().filter(match => match.resource().toString() === fileResource.toString())[0]; + const fileMatch = this.searchWorkbenchService.searchModel.searchResult.matches().filter(match => match.resource.toString() === fileResource.toString())[0]; return this.textModelResolverService.createModelReference(fileResource).then(ref => { ref = this._register(ref); const sourceModel = ref.object.textEditorModel; @@ -112,8 +112,8 @@ export class ReplaceService implements IReplaceService { const fileMatch = element instanceof Match ? element.parent() : element; return this.editorService.openEditor({ - leftResource: fileMatch.resource(), - rightResource: toReplaceResource(fileMatch.resource()), + leftResource: fileMatch.resource, + rightResource: toReplaceResource(fileMatch.resource), label: nls.localize('fileReplaceChanges', "{0} ↔ {1} (Replace Preview)", fileMatch.name(), fileMatch.name()), options: { preserveFocus, @@ -139,8 +139,8 @@ export class ReplaceService implements IReplaceService { } updateReplacePreview(fileMatch: FileMatch, override: boolean = false): Promise { - const replacePreviewUri = toReplaceResource(fileMatch.resource()); - return Promise.all([this.textModelResolverService.createModelReference(fileMatch.resource()), this.textModelResolverService.createModelReference(replacePreviewUri)]) + const replacePreviewUri = toReplaceResource(fileMatch.resource); + return Promise.all([this.textModelResolverService.createModelReference(fileMatch.resource), this.textModelResolverService.createModelReference(replacePreviewUri)]) .then(([sourceModelRef, replaceModelRef]) => { const sourceModel = sourceModelRef.object.textEditorModel; const replaceModel = replaceModelRef.object.textEditorModel; @@ -200,7 +200,7 @@ export class ReplaceService implements IReplaceService { private createEdit(match: Match, text: string, resource: URI | null = null): ResourceTextEdit { const fileMatch: FileMatch = match.parent(); const resourceEdit: ResourceTextEdit = { - resource: resource !== null ? resource : fileMatch.resource(), + resource: resource !== null ? resource : fileMatch.resource, edits: [{ range: match.range(), text: text diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index 49878f159b1..8d827d437d3 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -219,7 +219,7 @@ CommandsRegistry.registerCommand({ const viewletService = accessor.get(IViewletService); const explorerService = accessor.get(IExplorerService); const contextService = accessor.get(IWorkspaceContextService); - const uri = fileMatch.resource(); + const uri = fileMatch.resource; viewletService.openViewlet(VIEWLET_ID_FILES, false).then((viewlet: ExplorerViewlet) => { if (uri && contextService.isInsideWorkspace(uri)) { diff --git a/src/vs/workbench/contrib/search/browser/searchActions.ts b/src/vs/workbench/contrib/search/browser/searchActions.ts index 0d2d44cb5a1..ca96a4cd1ee 100644 --- a/src/vs/workbench/contrib/search/browser/searchActions.ts +++ b/src/vs/workbench/contrib/search/browser/searchActions.ts @@ -654,14 +654,14 @@ export class ReplaceAction extends AbstractSearchAndReplaceAction { } private hasSameParent(element: RenderableMatch): boolean { - return element && element instanceof Match && element.parent().resource() === this.element.parent().resource(); + return element && element instanceof Match && element.parent().resource === this.element.parent().resource; } private hasToOpenFile(): boolean { const activeEditor = this.editorService.activeEditor; const file = activeEditor ? activeEditor.getResource() : undefined; if (file) { - return file.toString() === this.element.parent().resource().toString(); + return file.toString() === this.element.parent().resource.toString(); } return false; } @@ -674,7 +674,7 @@ function uriToClipboardString(resource: URI): string { export const copyPathCommand: ICommandHandler = async (accessor, fileMatch: FileMatch | FolderMatch) => { const clipboardService = accessor.get(IClipboardService); - const text = uriToClipboardString(fileMatch.resource()); + const text = uriToClipboardString(fileMatch.resource); await clipboardService.writeText(text); }; @@ -712,7 +712,7 @@ function fileMatchToString(fileMatch: FileMatch, maxMatches: number): { text: st .slice(0, maxMatches) .map(match => matchToString(match, 2)); return { - text: `${uriToClipboardString(fileMatch.resource())}${lineDelimiter}${matchTextRows.join(lineDelimiter)}`, + text: `${uriToClipboardString(fileMatch.resource)}${lineDelimiter}${matchTextRows.join(lineDelimiter)}`, count: matchTextRows.length }; } diff --git a/src/vs/workbench/contrib/search/browser/searchResultsView.ts b/src/vs/workbench/contrib/search/browser/searchResultsView.ts index 4e79b687d46..7635f1eac29 100644 --- a/src/vs/workbench/contrib/search/browser/searchResultsView.ts +++ b/src/vs/workbench/contrib/search/browser/searchResultsView.ts @@ -115,11 +115,11 @@ export class FolderMatchRenderer extends Disposable implements ITreeRenderer, index: number, templateData: IFolderMatchTemplate): void { const folderMatch = node.element; if (folderMatch.hasResource()) { - const workspaceFolder = this.contextService.getWorkspaceFolder(folderMatch.resource()); - if (workspaceFolder && resources.isEqual(workspaceFolder.uri, folderMatch.resource())) { - templateData.label.setFile(folderMatch.resource(), { fileKind: FileKind.ROOT_FOLDER, hidePath: true }); + const workspaceFolder = this.contextService.getWorkspaceFolder(folderMatch.resource); + if (workspaceFolder && resources.isEqual(workspaceFolder.uri, folderMatch.resource)) { + templateData.label.setFile(folderMatch.resource, { fileKind: FileKind.ROOT_FOLDER, hidePath: true }); } else { - templateData.label.setFile(folderMatch.resource(), { fileKind: FileKind.FOLDER }); + templateData.label.setFile(folderMatch.resource, { fileKind: FileKind.FOLDER }); } } else { templateData.label.setLabel(nls.localize('searchFolderMatch.other.label', "Other files")); @@ -185,8 +185,8 @@ export class FileMatchRenderer extends Disposable implements ITreeRenderer, index: number, templateData: IFileMatchTemplate): void { const fileMatch = node.element; - templateData.el.setAttribute('data-resource', fileMatch.resource().toString()); - templateData.label.setFile(fileMatch.resource(), { hideIcon: false }); + templateData.el.setAttribute('data-resource', fileMatch.resource.toString()); + templateData.label.setFile(fileMatch.resource, { hideIcon: false }); const count = fileMatch.count(); templateData.badge.setCount(count); templateData.badge.setTitleFormat(count > 1 ? nls.localize('searchMatches', "{0} matches found", count) : nls.localize('searchMatch', "{0} match found", count)); @@ -316,7 +316,7 @@ export class SearchAccessibilityProvider implements IAccessibilityProvider { const element = elements[0]; return element instanceof FileMatch ? - resources.basename(element.resource()) : + resources.basename(element.resource) : undefined; } @@ -370,7 +370,7 @@ export class SearchDND implements ITreeDragAndDrop { const elements = (data as ElementsDragAndDropData).elements; const resources: URI[] = elements .filter(e => e instanceof FileMatch) - .map((fm: FileMatch) => fm.resource()); + .map((fm: FileMatch) => fm.resource); if (resources.length) { // Apply some datatransfer types to allow for dragging the element outside of the application diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index c175024c0eb..2daad95ca68 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -154,7 +154,7 @@ export class SearchView extends ViewletPanel { @IKeybindingService keybindingService: IKeybindingService, @IStorageService storageService: IStorageService, ) { - super({ ...(options as IViewletPanelOptions), id: VIEW_ID, ariaHeaderLabel: nls.localize('searchView', "Search") }, keybindingService, contextMenuService, configurationService); + super({ ...(options as IViewletPanelOptions), id: VIEW_ID, ariaHeaderLabel: nls.localize('searchView', "Search") }, keybindingService, contextMenuService, configurationService, contextKeyService); this.viewletVisible = Constants.SearchViewVisibleKey.bindTo(contextKeyService); this.viewletFocused = Constants.SearchViewFocusedKey.bindTo(contextKeyService); @@ -896,13 +896,13 @@ export class SearchView extends ViewletPanel { 0 : dom.getTotalHeight(this.messagesElement); - const searchResultContainerSize = this.size.height - + const searchResultContainerHeight = this.size.height - messagesSize - dom.getTotalHeight(this.searchWidgetsContainerElement); - this.resultsElement.style.height = searchResultContainerSize + 'px'; + this.resultsElement.style.height = searchResultContainerHeight + 'px'; - this.tree.layout(searchResultContainerSize, this.size.width); + this.tree.layout(searchResultContainerHeight, this.size.width); } protected layoutBody(height: number, width: number): void { @@ -1542,7 +1542,7 @@ export class SearchView extends ViewletPanel { open(element: FileMatchOrMatch, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): Promise { const selection = this.getSelectionFrom(element); - const resource = element instanceof Match ? element.parent().resource() : (element).resource(); + const resource = element instanceof Match ? element.parent().resource : (element).resource; return this.editorService.openEditor({ resource: resource, options: { @@ -1600,7 +1600,7 @@ export class SearchView extends ViewletPanel { if (!this.untitledEditorService.isDirty(resource)) { const matches = this.viewModel.searchResult.matches(); for (let i = 0, len = matches.length; i < len; i++) { - if (resource.toString() === matches[i].resource().toString()) { + if (resource.toString() === matches[i].resource.toString()) { this.viewModel.searchResult.remove(matches[i]); } } @@ -1614,11 +1614,8 @@ export class SearchView extends ViewletPanel { const matches = this.viewModel.searchResult.matches(); - for (let i = 0, len = matches.length; i < len; i++) { - if (e.contains(matches[i].resource(), FileChangeType.DELETED)) { - this.viewModel.searchResult.remove(matches[i]); - } - } + const changedMatches = matches.filter(m => e.contains(m.resource, FileChangeType.DELETED)); + this.viewModel.searchResult.remove(changedMatches); } getActions(): IAction[] { diff --git a/src/vs/workbench/contrib/search/common/searchModel.ts b/src/vs/workbench/contrib/search/common/searchModel.ts index 5f8ba902bd9..9c4d170c1d5 100644 --- a/src/vs/workbench/contrib/search/common/searchModel.ts +++ b/src/vs/workbench/contrib/search/common/searchModel.ts @@ -146,7 +146,7 @@ export class Match { } } -export class FileMatch extends Disposable { +export class FileMatch extends Disposable implements IFileMatch { private static readonly _CURRENT_FIND_MATCH = ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, @@ -309,7 +309,7 @@ export class FileMatch extends Disposable { } id(): string { - return this.resource().toString(); + return this.resource.toString(); } parent(): BaseFolderMatch { @@ -357,12 +357,12 @@ export class FileMatch extends Disposable { return this.matches().length; } - resource(): URI { + get resource(): URI { return this._resource; } name(): string { - return getBaseLabel(this.resource()); + return getBaseLabel(this.resource); } add(match: Match, trigger?: boolean) { @@ -432,7 +432,7 @@ export class BaseFolderMatch extends Disposable { return this._id; } - resource(): URI | null { + get resource(): URI | null { return this._resource; } @@ -441,7 +441,7 @@ export class BaseFolderMatch extends Disposable { } name(): string { - return getBaseLabel(withNullAsUndefined(this.resource())) || ''; + return getBaseLabel(withNullAsUndefined(this.resource)) || ''; } parent(): SearchResult { @@ -494,8 +494,8 @@ export class BaseFolderMatch extends Disposable { this._onChange.fire({ elements: changed, removed: true }); } - remove(match: FileMatch): void { - this.doRemove(match); + remove(matches: FileMatch | FileMatch[]): void { + this.doRemove(matches); } replace(match: FileMatch): Promise { @@ -530,7 +530,7 @@ export class BaseFolderMatch extends Disposable { private onFileChange(fileMatch: FileMatch): void { let added: boolean = false; let removed: boolean = false; - if (!this._fileMatches.has(fileMatch.resource())) { + if (!this._fileMatches.has(fileMatch.resource)) { this.doAdd(fileMatch); added = true; } @@ -545,22 +545,28 @@ export class BaseFolderMatch extends Disposable { } private doAdd(fileMatch: FileMatch): void { - this._fileMatches.set(fileMatch.resource(), fileMatch); - if (this._unDisposedFileMatches.has(fileMatch.resource())) { - this._unDisposedFileMatches.delete(fileMatch.resource()); + this._fileMatches.set(fileMatch.resource, fileMatch); + if (this._unDisposedFileMatches.has(fileMatch.resource)) { + this._unDisposedFileMatches.delete(fileMatch.resource); } } - private doRemove(fileMatch: FileMatch, dispose: boolean = true, trigger: boolean = true): void { - this._fileMatches.delete(fileMatch.resource()); - if (dispose) { - fileMatch.dispose(); - } else { - this._unDisposedFileMatches.set(fileMatch.resource(), fileMatch); + private doRemove(fileMatches: FileMatch | FileMatch[], dispose: boolean = true, trigger: boolean = true): void { + if (!Array.isArray(fileMatches)) { + fileMatches = [fileMatches]; + } + + for (let match of fileMatches) { + this._fileMatches.delete(match.resource); + if (dispose) { + match.dispose(); + } else { + this._unDisposedFileMatches.set(match.resource, match); + } } if (trigger) { - this._onChange.fire({ elements: [fileMatch], removed: true }); + this._onChange.fire({ elements: fileMatches, removed: true }); } } @@ -590,7 +596,7 @@ export class FolderMatch extends BaseFolderMatch { super(_resource, _id, _index, _query, _parent, _searchModel, replaceService, instantiationService); } - resource(): URI { + get resource(): URI { return this._resource!; } } @@ -605,7 +611,7 @@ export function searchMatchComparer(elementA: RenderableMatch, elementB: Rendera } if (elementA instanceof FileMatch && elementB instanceof FileMatch) { - return elementA.resource().fsPath.localeCompare(elementB.resource().fsPath) || elementA.name().localeCompare(elementB.name()); + return elementA.resource.fsPath.localeCompare(elementB.resource.fsPath) || elementA.name().localeCompare(elementB.name()); } if (elementA instanceof Match && elementB instanceof Match) { @@ -652,7 +658,7 @@ export class SearchResult extends Disposable { .map(fq => fq.folder) .map((resource, index) => this.createFolderMatch(resource, resource.toString(), index, query)); - this._folderMatches.forEach(fm => this._folderMatchesMap.set(fm.resource().toString(), fm)); + this._folderMatches.forEach(fm => this._folderMatchesMap.set(fm.resource.toString(), fm)); this._otherFilesMatch = this.createOtherFilesFolderMatch('otherFiles', this._folderMatches.length + 1, query); this._query = query; @@ -686,26 +692,9 @@ export class SearchResult extends Disposable { add(allRaw: IFileMatch[], silent: boolean = false): void { // Split up raw into a list per folder so we can do a batch add per folder. - const rawPerFolder = new ResourceMap(); - const otherFileMatches: IFileMatch[] = []; - this._folderMatches.forEach(fm => rawPerFolder.set(fm.resource(), [])); - allRaw.forEach(rawFileMatch => { - const folderMatch = this.getFolderMatch(rawFileMatch.resource); - if (!folderMatch) { - // foldermatch was previously removed by user or disposed for some reason - return; - } - - const resource = folderMatch.resource(); - if (resource) { - rawPerFolder.get(resource)!.push(rawFileMatch); - } else { - otherFileMatches.push(rawFileMatch); - } - }); - - rawPerFolder.forEach((raw) => { + const { byFolder, other } = this.groupFilesByFolder(allRaw); + byFolder.forEach(raw => { if (!raw.length) { return; } @@ -716,7 +705,7 @@ export class SearchResult extends Disposable { } }); - this._otherFilesMatch!.add(otherFileMatches, silent); + this._otherFilesMatch!.add(other, silent); } clear(): void { @@ -724,16 +713,31 @@ export class SearchResult extends Disposable { this.disposeMatches(); } - remove(match: FileMatch | FolderMatch): void { - if (match instanceof FileMatch) { - this.getFolderMatch(match.resource()).remove(match); - } else { - match.clear(); + remove(matches: FileMatch | FileMatch[]): void { + if (!Array.isArray(matches)) { + matches = [matches]; + } + + const { byFolder, other } = this.groupFilesByFolder(matches); + byFolder.forEach(matches => { + if (!matches.length) { + return; + } + + this.getFolderMatch(matches[0].resource).remove(matches); + }); + + if (other.length) { + this.getFolderMatch(other[0].resource).remove(other); } } + removeFolder(match: FolderMatch): void { + match.clear(); + } + replace(match: FileMatch): Promise { - return this.getFolderMatch(match.resource()).replace(match); + return this.getFolderMatch(match.resource).replace(match); } replaceAll(progress: IProgress): Promise { @@ -807,7 +811,7 @@ export class SearchResult extends Disposable { if (this._showHighlights && selectedMatch) { // TS? this._rangeHighlightDecorations.highlightRange( - (selectedMatch).parent().resource(), + (selectedMatch).parent().resource, (selectedMatch).range() ); } else { @@ -830,6 +834,32 @@ export class SearchResult extends Disposable { }); } + private groupFilesByFolder(fileMatches: IFileMatch[]): { byFolder: ResourceMap, other: IFileMatch[] } { + const rawPerFolder = new ResourceMap(); + const otherFileMatches: IFileMatch[] = []; + this._folderMatches.forEach(fm => rawPerFolder.set(fm.resource, [])); + + fileMatches.forEach(rawFileMatch => { + const folderMatch = this.getFolderMatch(rawFileMatch.resource); + if (!folderMatch) { + // foldermatch was previously removed by user or disposed for some reason + return; + } + + const resource = folderMatch.resource; + if (resource) { + rawPerFolder.get(resource)!.push(rawFileMatch); + } else { + otherFileMatches.push(rawFileMatch); + } + }); + + return { + byFolder: rawPerFolder, + other: otherFileMatches + }; + } + private disposeMatches(): void { this.folderMatches().forEach(folderMatch => folderMatch.dispose()); this._folderMatches = []; diff --git a/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts b/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts index 8f63fa9eb07..a2eb0ba8458 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts @@ -155,4 +155,4 @@ suite('Search Actions', () => { instantiationService.stub(IConfigurationService, new TestConfigurationService()); return instantiationService.createInstance(ModelServiceImpl); } -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/contrib/search/test/common/searchModel.test.ts b/src/vs/workbench/contrib/search/test/common/searchModel.test.ts index d1438b530b7..ba00d3b20a1 100644 --- a/src/vs/workbench/contrib/search/test/common/searchModel.test.ts +++ b/src/vs/workbench/contrib/search/test/common/searchModel.test.ts @@ -130,7 +130,7 @@ suite('SearchModel', () => { const actual = testObject.searchResult.matches(); assert.equal(2, actual.length); - assert.equal('file://c:/1', actual[0].resource().toString()); + assert.equal('file://c:/1', actual[0].resource.toString()); let actuaMatches = actual[0].matches(); assert.equal(2, actuaMatches.length); diff --git a/src/vs/workbench/contrib/search/test/common/searchResult.test.ts b/src/vs/workbench/contrib/search/test/common/searchResult.test.ts index 64f09791842..b1a771cf4d0 100644 --- a/src/vs/workbench/contrib/search/test/common/searchResult.test.ts +++ b/src/vs/workbench/contrib/search/test/common/searchResult.test.ts @@ -55,12 +55,12 @@ suite('SearchResult', () => { test('File Match', function () { let fileMatch = aFileMatch('folder/file.txt'); assert.equal(fileMatch.matches(), 0); - assert.equal(fileMatch.resource().toString(), 'file:///folder/file.txt'); + assert.equal(fileMatch.resource.toString(), 'file:///folder/file.txt'); assert.equal(fileMatch.name(), 'file.txt'); fileMatch = aFileMatch('file.txt'); assert.equal(fileMatch.matches(), 0); - assert.equal(fileMatch.resource().toString(), 'file:///file.txt'); + assert.equal(fileMatch.resource.toString(), 'file:///file.txt'); assert.equal(fileMatch.name(), 'file.txt'); }); @@ -156,7 +156,7 @@ suite('SearchResult', () => { const actual = testObject.matches(); assert.equal(1, actual.length); - assert.equal('file://c:/', actual[0].resource().toString()); + assert.equal('file://c:/', actual[0].resource.toString()); const actuaMatches = actual[0].matches(); assert.equal(3, actuaMatches.length); @@ -186,7 +186,7 @@ suite('SearchResult', () => { const actual = testObject.matches(); assert.equal(2, actual.length); - assert.equal('file://c:/1', actual[0].resource().toString()); + assert.equal('file://c:/1', actual[0].resource.toString()); let actuaMatches = actual[0].matches(); assert.equal(2, actuaMatches.length); @@ -228,13 +228,30 @@ suite('SearchResult', () => { testObject.add([ aRawMatch('file://c:/1', new TextSearchMatch('preview 1', lineOneRange))]); - const objectRoRemove = testObject.matches()[0]; + const objectToRemove = testObject.matches()[0]; testObject.onChange(target); - testObject.remove(objectRoRemove); + testObject.remove(objectToRemove); assert.ok(target.calledOnce); - assert.deepEqual([{ elements: [objectRoRemove], removed: true }], target.args[0]); + assert.deepEqual([{ elements: [objectToRemove], removed: true }], target.args[0]); + }); + + test('remove array triggers change event', function () { + const target = sinon.spy(); + const testObject = aSearchResult(); + testObject.add([ + aRawMatch('file://c:/1', + new TextSearchMatch('preview 1', lineOneRange)), + aRawMatch('file://c:/2', + new TextSearchMatch('preview 2', lineOneRange))]); + const arrayToRemove = testObject.matches(); + testObject.onChange(target); + + testObject.remove(arrayToRemove); + + assert.ok(target.calledOnce); + assert.deepEqual([{ elements: arrayToRemove, removed: true }], target.args[0]); }); test('remove triggers change event', function () { @@ -243,13 +260,13 @@ suite('SearchResult', () => { testObject.add([ aRawMatch('file://c:/1', new TextSearchMatch('preview 1', lineOneRange))]); - const objectRoRemove = testObject.matches()[0]; + const objectToRemove = testObject.matches()[0]; testObject.onChange(target); - testObject.remove(objectRoRemove); + testObject.remove(objectToRemove); assert.ok(target.calledOnce); - assert.deepEqual([{ elements: [objectRoRemove], removed: true }], target.args[0]); + assert.deepEqual([{ elements: [objectToRemove], removed: true }], target.args[0]); }); test('Removing all line matches and adding back will add file back to result', function () { @@ -290,13 +307,13 @@ suite('SearchResult', () => { aRawMatch('file://c:/1', new TextSearchMatch('preview 1', lineOneRange))]); testObject.onChange(target); - const objectRoRemove = testObject.matches()[0]; + const objectToRemove = testObject.matches()[0]; - testObject.replace(objectRoRemove); + testObject.replace(objectToRemove); return voidPromise.then(() => { assert.ok(target.calledOnce); - assert.deepEqual([{ elements: [objectRoRemove], removed: true }], target.args[0]); + assert.deepEqual([{ elements: [objectToRemove], removed: true }], target.args[0]); }); }); diff --git a/src/vs/workbench/contrib/stats/electron-browser/workspaceStatsService.ts b/src/vs/workbench/contrib/stats/electron-browser/workspaceStatsService.ts index 5041cb4be1e..ef0dc08081c 100644 --- a/src/vs/workbench/contrib/stats/electron-browser/workspaceStatsService.ts +++ b/src/vs/workbench/contrib/stats/electron-browser/workspaceStatsService.ts @@ -47,7 +47,22 @@ const ModulesToLookFor = [ 'azure-storage', 'firebase', '@google-cloud/common', - 'heroku-cli' + 'heroku-cli', + //Office and Sharepoint packages + '@microsoft/office-js', + '@microsoft/office-js-helpers', + '@types/office-js', + '@types/office-runtime', + 'office-ui-fabric-react', + '@uifabric/icons', + '@uifabric/merge-styles', + '@uifabric/styling', + '@uifabric/experiments', + '@uifabric/utilities', + '@microsoft/rush', + 'lerna', + 'just-task', + 'beachball' ]; const PyModulesToLookFor = [ 'azure', @@ -143,6 +158,20 @@ export class WorkspaceStatsService implements IWorkspaceStatsService { "workspace.npm.@google-cloud/common" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.firebase" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.npm.heroku-cli" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@microsoft/office-js" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@microsoft/office-js-helpers" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@types/office-js" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@types/office-runtime" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.office-ui-fabric-react" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@uifabric/icons" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@uifabric/merge-styles" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@uifabric/styling" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@uifabric/experiments" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@uifabric/utilities" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.@microsoft/rush" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.lerna" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.just-task" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.npm.beachball" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.bower" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.yeoman.code.ext" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.cordova.high" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, @@ -188,7 +217,6 @@ export class WorkspaceStatsService implements IWorkspaceStatsService { "workspace.py.botbuilder-core" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.botbuilder-schema" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.botframework-connector" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } - } */ private resolveWorkspaceTags(configuration: IWindowConfiguration, participant?: (rootFiles: string[]) => void): Promise { @@ -474,4 +502,4 @@ export class WorkspaceStatsService implements IWorkspaceStatsService { private searchArray(arr: string[], regEx: RegExp): boolean | undefined { return arr.some(v => v.search(regEx) > -1) || undefined; } -} \ No newline at end of file +} diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index 21491081db7..efef7a28135 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -926,7 +926,7 @@ export class TerminalTaskSystem implements ITaskSystem { }; } else if (task.command.runtime === RuntimeType.CustomExecution2) { this.currentTask.shellLaunchConfig = launchConfigs = { - isVirtualProcess: true, + isExtensionTerminal: true, waitOnExit, name: this.createTerminalName(task, workspaceFolder), initialText: task.command.presentation && task.command.presentation.echo ? `\x1b[1m> Executing task: ${task._label} <\x1b[0m\n` : undefined diff --git a/src/vs/workbench/contrib/tasks/common/taskTemplates.ts b/src/vs/workbench/contrib/tasks/common/taskTemplates.ts index 1a1d1ee1217..a0412d55268 100644 --- a/src/vs/workbench/contrib/tasks/common/taskTemplates.ts +++ b/src/vs/workbench/contrib/tasks/common/taskTemplates.ts @@ -29,6 +29,12 @@ const dotnetBuild: TaskEntry = { '\t\t\t"label": "build",', '\t\t\t"command": "dotnet build",', '\t\t\t"type": "shell",', + '\t\t\t"args": [', + '\t\t\t\t// Ask dotnet build to generate full paths for file names.', + '\t\t\t\t"/property:GenerateFullPaths=true",', + '\t\t\t\t// Do not generate summary otherwise it leads to duplicate errors in Problems panel', + '\t\t\t\t"/consoleloggerparameters:NoSummary"', + '\t\t\t],', '\t\t\t"group": "build",', '\t\t\t"presentation": {', '\t\t\t\t"reveal": "silent"', @@ -58,7 +64,9 @@ const msbuild: TaskEntry = { '\t\t\t"args": [', '\t\t\t\t// Ask msbuild to generate full paths for file names.', '\t\t\t\t"/property:GenerateFullPaths=true",', - '\t\t\t\t"/t:build"', + '\t\t\t\t"/t:build",', + '\t\t\t\t// Do not generate summary otherwise it leads to duplicate errors in Problems panel', + '\t\t\t\t"/consoleloggerparameters:NoSummary"', '\t\t\t],', '\t\t\t"group": "build",', '\t\t\t"presentation": {', diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index a391164f1f2..348b93e3307 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -31,7 +31,7 @@ const LATENCY_MEASURING_INTERVAL = 1000; enum ProcessType { Process, - VirtualProcess + ExtensionTerminal } /** @@ -113,8 +113,8 @@ export class TerminalProcessManager implements ITerminalProcessManager { rows: number, isScreenReaderModeEnabled: boolean ): Promise { - if (shellLaunchConfig.isVirtualProcess) { - this._processType = ProcessType.VirtualProcess; + if (shellLaunchConfig.isExtensionTerminal) { + this._processType = ProcessType.ExtensionTerminal; this._process = this._instantiationService.createInstance(TerminalProcessExtHostProxy, this._terminalId, shellLaunchConfig, undefined, cols, rows, this._configHelper); } else { const forceExtHostProcess = (this._configHelper.config as any).extHostProcess; @@ -239,7 +239,7 @@ export class TerminalProcessManager implements ITerminalProcessManager { } public write(data: string): void { - if (this.shellProcessId || this._processType === ProcessType.VirtualProcess) { + if (this.shellProcessId || this._processType === ProcessType.ExtensionTerminal) { if (this._process) { // Send data if the pty is ready this._process.input(data); @@ -292,4 +292,4 @@ export class TerminalProcessManager implements ITerminalProcessManager { this._onProcessExit.fire(exitCode); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index f427fd3ef0e..f6413d9857e 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -188,14 +188,14 @@ export interface IShellLaunchConfig { initialText?: string; /** - * @deprecated use `isVirtualProcess` + * @deprecated use `isExtensionTerminal` */ isRendererOnly?: boolean; /** - * When true an extension is acting as the terminal's process. + * Whether an extension is controlling the terminal via a `vscode.Pseudoterminal`. */ - isVirtualProcess?: boolean; + isExtensionTerminal?: boolean; /** * Whether the terminal process environment should be exactly as provided in @@ -231,8 +231,8 @@ export interface ITerminalService { onInstanceProcessIdReady: Event; onInstanceDimensionsChanged: Event; onInstanceMaximumDimensionsChanged: Event; - onInstanceRequestExtHostProcess: Event; - onInstanceRequestVirtualProcess: Event; + onInstanceRequestSpawnExtHostProcess: Event; + onInstanceRequestStartExtensionTerminal: Event; onInstancesChanged: Event; onInstanceTitleChanged: Event; onActiveInstanceChanged: Event; @@ -299,8 +299,8 @@ export interface ITerminalService { preparePathForTerminalAsync(path: string, executable: string | undefined, title: string): Promise; extHostReady(remoteAuthority: string): void; - requestExtHostProcess(proxy: ITerminalProcessExtHostProxy, shellLaunchConfig: IShellLaunchConfig, activeWorkspaceRootUri: URI, cols: number, rows: number, isWorkspaceShellAllowed: boolean): void; - requestVirtualProcess(proxy: ITerminalProcessExtHostProxy, cols: number, rows: number): void; + requestSpawnExtHostProcess(proxy: ITerminalProcessExtHostProxy, shellLaunchConfig: IShellLaunchConfig, activeWorkspaceRootUri: URI, cols: number, rows: number, isWorkspaceShellAllowed: boolean): void; + requestStartExtensionTerminal(proxy: ITerminalProcessExtHostProxy, cols: number, rows: number): void; } /** @@ -760,7 +760,7 @@ export interface ITerminalProcessExtHostProxy extends IDisposable { onRequestLatency: Event; } -export interface ITerminalProcessExtHostRequest { +export interface ISpawnExtHostProcessRequest { proxy: ITerminalProcessExtHostProxy; shellLaunchConfig: IShellLaunchConfig; activeWorkspaceRootUri: URI; @@ -769,7 +769,7 @@ export interface ITerminalProcessExtHostRequest { isWorkspaceShellAllowed: boolean; } -export interface ITerminalVirtualProcessRequest { +export interface IStartExtensionTerminalRequest { proxy: ITerminalProcessExtHostProxy; cols: number; rows: number; diff --git a/src/vs/workbench/contrib/terminal/common/terminalProcessExtHostProxy.ts b/src/vs/workbench/contrib/terminal/common/terminalProcessExtHostProxy.ts index e611bce74a6..80923d84486 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalProcessExtHostProxy.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalProcessExtHostProxy.ts @@ -58,14 +58,14 @@ export class TerminalProcessExtHostProxy extends Disposable implements ITerminal // Request a process if needed, if this is a virtual process this step can be skipped as // there is no real "process" and we know it's ready on the ext host already. - if (shellLaunchConfig.isVirtualProcess) { - this._terminalService.requestVirtualProcess(this, cols, rows); + if (shellLaunchConfig.isExtensionTerminal) { + this._terminalService.requestStartExtensionTerminal(this, cols, rows); } else { remoteAgentService.getEnvironment().then(env => { if (!env) { throw new Error('Could not fetch environment'); } - this._terminalService.requestExtHostProcess(this, shellLaunchConfig, activeWorkspaceRootUri, cols, rows, configHelper.checkWorkspaceShellPermissions(env.os)); + this._terminalService.requestSpawnExtHostProcess(this, shellLaunchConfig, activeWorkspaceRootUri, cols, rows, configHelper.checkWorkspaceShellPermissions(env.os)); }); if (!hasReceivedResponse) { setTimeout(() => this._onProcessTitleChanged.fire(nls.localize('terminal.integrated.starting', "Starting...")), 0); @@ -149,4 +149,4 @@ export class TerminalProcessExtHostProxy extends Disposable implements ITerminal this._pendingLatencyRequests.push(resolve); }); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/contrib/terminal/common/terminalService.ts b/src/vs/workbench/contrib/terminal/common/terminalService.ts index 558be1e44ea..8bf6a5bfa5b 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalService.ts @@ -8,7 +8,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; -import { ITerminalService, ITerminalInstance, IShellLaunchConfig, ITerminalConfigHelper, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE, TERMINAL_PANEL_ID, ITerminalTab, ITerminalProcessExtHostProxy, ITerminalProcessExtHostRequest, KEYBINDING_CONTEXT_TERMINAL_IS_OPEN, ITerminalNativeService, IShellDefinition, IAvailableShellsRequest, ITerminalVirtualProcessRequest } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ITerminalService, ITerminalInstance, IShellLaunchConfig, ITerminalConfigHelper, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_WIDGET_VISIBLE, TERMINAL_PANEL_ID, ITerminalTab, ITerminalProcessExtHostProxy, ISpawnExtHostProcessRequest, KEYBINDING_CONTEXT_TERMINAL_IS_OPEN, ITerminalNativeService, IShellDefinition, IAvailableShellsRequest, IStartExtensionTerminalRequest } from 'vs/workbench/contrib/terminal/common/terminal'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { URI } from 'vs/base/common/uri'; import { FindReplaceState } from 'vs/editor/contrib/find/findState'; @@ -20,11 +20,15 @@ import { escapeNonWindowsPath } from 'vs/workbench/contrib/terminal/common/termi import { isWindows, isMacintosh, OperatingSystem } from 'vs/base/common/platform'; import { basename } from 'vs/base/common/path'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { timeout } from 'vs/base/common/async'; import { IOpenFileRequest } from 'vs/platform/windows/common/windows'; import { IPickOptions, IQuickPickItem, IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; +interface IExtHostReadyEntry { + promise: Promise; + resolve: () => void; +} + export abstract class TerminalService implements ITerminalService { public _serviceBrand: any; @@ -38,7 +42,7 @@ export abstract class TerminalService implements ITerminalService { return this._terminalTabs.reduce((p, c) => p.concat(c.terminalInstances), []); } private _findState: FindReplaceState; - private _extHostsReady: { [authority: string]: boolean } = {}; + private _extHostsReady: { [authority: string]: IExtHostReadyEntry | undefined } = {}; private _activeTabIndex: number; public get activeTabIndex(): number { return this._activeTabIndex; } @@ -53,10 +57,10 @@ export abstract class TerminalService implements ITerminalService { public get onInstanceDisposed(): Event { return this._onInstanceDisposed.event; } protected readonly _onInstanceProcessIdReady = new Emitter(); public get onInstanceProcessIdReady(): Event { return this._onInstanceProcessIdReady.event; } - protected readonly _onInstanceRequestExtHostProcess = new Emitter(); - public get onInstanceRequestExtHostProcess(): Event { return this._onInstanceRequestExtHostProcess.event; } - protected readonly _onInstanceRequestVirtualProcess = new Emitter(); - public get onInstanceRequestVirtualProcess(): Event { return this._onInstanceRequestVirtualProcess.event; } + protected readonly _onInstanceRequestSpawnExtHostProcess = new Emitter(); + public get onInstanceRequestSpawnExtHostProcess(): Event { return this._onInstanceRequestSpawnExtHostProcess.event; } + protected readonly _onInstanceRequestStartExtensionTerminal = new Emitter(); + public get onInstanceRequestStartExtensionTerminal(): Event { return this._onInstanceRequestStartExtensionTerminal.event; } protected readonly _onInstanceDimensionsChanged = new Emitter(); public get onInstanceDimensionsChanged(): Event { return this._onInstanceDimensionsChanged.event; } protected readonly _onInstanceMaximumDimensionsChanged = new Emitter(); @@ -131,25 +135,39 @@ export abstract class TerminalService implements ITerminalService { return activeInstance ? activeInstance : this.createTerminal(undefined, wasNewTerminalAction); } - public requestExtHostProcess(proxy: ITerminalProcessExtHostProxy, shellLaunchConfig: IShellLaunchConfig, activeWorkspaceRootUri: URI, cols: number, rows: number, isWorkspaceShellAllowed: boolean): void { + public requestSpawnExtHostProcess(proxy: ITerminalProcessExtHostProxy, shellLaunchConfig: IShellLaunchConfig, activeWorkspaceRootUri: URI, cols: number, rows: number, isWorkspaceShellAllowed: boolean): void { this._extensionService.whenInstalledExtensionsRegistered().then(async () => { - // Wait for the remoteAuthority to be ready (and listening for events) before proceeding + // Wait for the remoteAuthority to be ready (and listening for events) before firing + // the event to spawn the ext host process const conn = this._remoteAgentService.getConnection(); const remoteAuthority = conn ? conn.remoteAuthority : 'null'; - let retries = 0; - while (!this._extHostsReady[remoteAuthority] && ++retries < 50) { - await timeout(100); - } - this._onInstanceRequestExtHostProcess.fire({ proxy, shellLaunchConfig, activeWorkspaceRootUri, cols, rows, isWorkspaceShellAllowed }); + await this._whenExtHostReady(remoteAuthority); + this._onInstanceRequestSpawnExtHostProcess.fire({ proxy, shellLaunchConfig, activeWorkspaceRootUri, cols, rows, isWorkspaceShellAllowed }); }); } - public requestVirtualProcess(proxy: ITerminalProcessExtHostProxy, cols: number, rows: number): void { - this._onInstanceRequestVirtualProcess.fire({ proxy, cols, rows }); + public requestStartExtensionTerminal(proxy: ITerminalProcessExtHostProxy, cols: number, rows: number): void { + this._onInstanceRequestStartExtensionTerminal.fire({ proxy, cols, rows }); } - public extHostReady(remoteAuthority: string): void { - this._extHostsReady[remoteAuthority] = true; + public async extHostReady(remoteAuthority: string): Promise { + this._createExtHostReadyEntry(remoteAuthority); + this._extHostsReady[remoteAuthority]!.resolve(); + } + + private async _whenExtHostReady(remoteAuthority: string): Promise { + this._createExtHostReadyEntry(remoteAuthority); + return this._extHostsReady[remoteAuthority]!.promise; + } + + private _createExtHostReadyEntry(remoteAuthority: string): void { + if (this._extHostsReady[remoteAuthority]) { + return; + } + + let resolve!: () => void; + const promise = new Promise(r => resolve = r); + this._extHostsReady[remoteAuthority] = { promise, resolve }; } private _onBeforeShutdown(): boolean | Promise { diff --git a/src/vs/workbench/contrib/watermark/browser/watermark.ts b/src/vs/workbench/contrib/watermark/browser/watermark.ts index 7788f6e36a0..54c9b8620f5 100644 --- a/src/vs/workbench/contrib/watermark/browser/watermark.ts +++ b/src/vs/workbench/contrib/watermark/browser/watermark.ts @@ -173,7 +173,7 @@ export class WatermarkContribution extends Disposable implements IWorkbenchContr dom.prepend(container.firstElementChild as HTMLElement, this.watermark); this._register(this.keybindingService.onDidUpdateKeybindings(update)); this._register(this.editorGroupsService.onDidLayout(dimension => this.handleEditorPartSize(container, dimension))); - this.handleEditorPartSize(container, this.editorGroupsService.dimension); + this.handleEditorPartSize(container, this.editorGroupsService.contentDimension); } private handleEditorPartSize(container: HTMLElement, dimension: IDimension): void { @@ -214,4 +214,4 @@ Registry.as(ConfigurationExtensions.Configuration) 'description': nls.localize('tips.enabled', "When enabled, will show the watermark tips when no editor is open.") }, } - }); \ No newline at end of file + }); diff --git a/src/vs/workbench/services/editor/common/editorGroupsService.ts b/src/vs/workbench/services/editor/common/editorGroupsService.ts index c91173e5514..a9ab498cc98 100644 --- a/src/vs/workbench/services/editor/common/editorGroupsService.ts +++ b/src/vs/workbench/services/editor/common/editorGroupsService.ts @@ -179,7 +179,7 @@ export interface IEditorGroupsService { /** * The size of the editor groups area. */ - readonly dimension: IDimension; + readonly contentDimension: IDimension; /** * An active group is the default location for new editors to open. 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 b577d40f181..477fdfc5bc7 100644 --- a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts @@ -118,11 +118,6 @@ suite('EditorGroupsService', () => { groupMovedCounter++; }); - let preferredSizeChangeCounter = 0; - const preferredSizeChangeListener = part.onDidPreferredSizeChange(() => { - preferredSizeChangeCounter++; - }); - // always a root group const rootGroup = part.groups[0]; assert.equal(part.groups.length, 1); @@ -141,7 +136,6 @@ suite('EditorGroupsService', () => { assert.equal(part.groups.length, 2); assert.equal(part.count, 2); assert.ok(part.activeGroup === rootGroup); - assert.equal(preferredSizeChangeCounter, 1); assert.equal(rootGroup.label, 'Group 1'); assert.equal(rightGroup.label, 'Group 2'); @@ -189,7 +183,6 @@ suite('EditorGroupsService', () => { assert.equal(part.groups.length, 3); assert.ok(part.activeGroup === rightGroup); assert.ok(!downGroup.activeControl); - assert.equal(preferredSizeChangeCounter, 2); assert.equal(rootGroup.label, 'Group 1'); assert.equal(rightGroup.label, 'Group 2'); assert.equal(downGroup.label, 'Group 3'); @@ -208,13 +201,11 @@ suite('EditorGroupsService', () => { part.moveGroup(downGroup, rightGroup, GroupDirection.DOWN); assert.equal(groupMovedCounter, 1); - assert.equal(preferredSizeChangeCounter, 2); part.removeGroup(downGroup); assert.ok(!part.getGroup(downGroup.id)); assert.equal(didDispose, true); assert.equal(groupRemovedCounter, 1); - assert.equal(preferredSizeChangeCounter, 3); assert.equal(part.groups.length, 2); assert.ok(part.activeGroup === rightGroup); assert.equal(rootGroup.label, 'Group 1'); @@ -254,13 +245,11 @@ suite('EditorGroupsService', () => { assert.ok(part.activeGroup === rootGroup); part.setGroupOrientation(part.orientation === GroupOrientation.HORIZONTAL ? GroupOrientation.VERTICAL : GroupOrientation.HORIZONTAL); - assert.equal(preferredSizeChangeCounter, 5); activeGroupChangeListener.dispose(); groupAddedListener.dispose(); groupRemovedListener.dispose(); groupMovedListener.dispose(); - preferredSizeChangeListener.dispose(); part.dispose(); }); diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts index 4d11f6a9459..923130c9ee3 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts @@ -434,7 +434,7 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter { // Log on main side if running tests from cli if (this._isExtensionDevTestFromCli) { - this._windowsService.log(entry.severity, ...parse(entry).args); + this._windowsService.log(entry.severity, parse(entry).args); } // Broadcast to other windows if we are in development mode diff --git a/src/vs/workbench/services/keybinding/browser/keybindingService.ts b/src/vs/workbench/services/keybinding/browser/keybindingService.ts index 4e64ebdc0c6..dec59cfca2c 100644 --- a/src/vs/workbench/services/keybinding/browser/keybindingService.ts +++ b/src/vs/workbench/services/keybinding/browser/keybindingService.ts @@ -300,7 +300,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService { private _resolveKeybindingItems(items: IKeybindingItem[], isDefault: boolean): ResolvedKeybindingItem[] { let result: ResolvedKeybindingItem[] = [], resultLen = 0; for (const item of items) { - const when = (item.when ? item.when.normalize() : undefined); + const when = item.when || undefined; const keybinding = item.keybinding; if (!keybinding) { // This might be a removal keybinding item in user settings => accept it @@ -323,7 +323,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService { private _resolveUserKeybindingItems(items: IUserKeybindingItem[], isDefault: boolean): ResolvedKeybindingItem[] { let result: ResolvedKeybindingItem[] = [], resultLen = 0; for (const item of items) { - const when = (item.when ? item.when.normalize() : undefined); + const when = item.when || undefined; const parts = item.parts; if (parts.length === 0) { // This might be a removal keybinding item in user settings => accept it diff --git a/src/vs/workbench/services/layout/browser/layoutService.ts b/src/vs/workbench/services/layout/browser/layoutService.ts index d0bf836984f..344c7657de5 100644 --- a/src/vs/workbench/services/layout/browser/layoutService.ts +++ b/src/vs/workbench/services/layout/browser/layoutService.ts @@ -8,6 +8,7 @@ import { Event } from 'vs/base/common/event'; import { MenuBarVisibility } from 'vs/platform/windows/common/windows'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { Part } from 'vs/workbench/browser/part'; +import { Dimension } from 'vs/base/browser/dom'; export const IWorkbenchLayoutService = createDecorator('layoutService'); @@ -81,6 +82,11 @@ export interface IWorkbenchLayoutService extends ILayoutService { */ isVisible(part: Parts): boolean; + /** + * Returns if the part is visible. + */ + getDimension(part: Parts): Dimension; + /** * Set activity bar hidden or not */ diff --git a/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts b/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts index 9bdf3506f81..d73ffc2fade 100644 --- a/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts +++ b/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts @@ -43,7 +43,7 @@ export abstract class AbstractTextMateService extends Disposable implements ITex private _grammarDefinitions: IValidGrammarDefinition[] | null; private _grammarFactory: TMGrammarFactory | null; private _tokenizersRegistrations: IDisposable[]; - private _currentTokenColors: ITokenColorizationRule[] | null; + protected _currentTheme: IRawTheme | null; constructor( @IModeService private readonly _modeService: IModeService, @@ -64,6 +64,8 @@ export abstract class AbstractTextMateService extends Disposable implements ITex this._grammarFactory = null; this._tokenizersRegistrations = []; + this._currentTheme = null; + grammarsExtPoint.setHandler((extensions) => { this._grammarDefinitions = null; if (this._grammarFactory) { @@ -242,11 +244,11 @@ export abstract class AbstractTextMateService extends Disposable implements ITex } private _updateTheme(grammarFactory: TMGrammarFactory, colorTheme: IColorTheme, forceUpdate: boolean): void { - if (!forceUpdate && AbstractTextMateService.equalsTokenRules(this._currentTokenColors, colorTheme.tokenColors)) { + if (!forceUpdate && this._currentTheme && AbstractTextMateService.equalsTokenRules(this._currentTheme.settings, colorTheme.tokenColors)) { return; } - this._currentTokenColors = colorTheme.tokenColors; - this._doUpdateTheme(grammarFactory, { name: colorTheme.label, settings: colorTheme.tokenColors }); + this._currentTheme = { name: colorTheme.label, settings: colorTheme.tokenColors }; + this._doUpdateTheme(grammarFactory, this._currentTheme); } protected _doUpdateTheme(grammarFactory: TMGrammarFactory, theme: IRawTheme): void { diff --git a/src/vs/workbench/services/textMate/electron-browser/textMateService.ts b/src/vs/workbench/services/textMate/electron-browser/textMateService.ts index abc60f9424d..dc35917e961 100644 --- a/src/vs/workbench/services/textMate/electron-browser/textMateService.ts +++ b/src/vs/workbench/services/textMate/electron-browser/textMateService.ts @@ -14,12 +14,15 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { createWebWorker, MonacoWebWorker } from 'vs/editor/common/services/webWorker'; import { IModelService } from 'vs/editor/common/services/modelService'; -import { IOnigLib } from 'vscode-textmate'; +import { IOnigLib, IRawTheme } from 'vscode-textmate'; import { IValidGrammarDefinition } from 'vs/workbench/services/textMate/common/TMScopeRegistry'; import { TextMateWorker } from 'vs/workbench/services/textMate/electron-browser/textMateWorker'; import { ITextModel } from 'vs/editor/common/model'; import { Disposable } from 'vs/base/common/lifecycle'; import { UriComponents, URI } from 'vs/base/common/uri'; +import { MultilineTokensBuilder } from 'vs/editor/common/model/tokensStore'; +import { TMGrammarFactory } from 'vs/workbench/services/textMate/common/TMGrammarFactory'; +import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents'; import { IStorageService } from 'vs/platform/storage/common/storage'; const RUN_TEXTMATE_IN_WORKER = false; @@ -29,6 +32,7 @@ class ModelWorkerTextMateTokenizer extends Disposable { private readonly _worker: TextMateWorker; private readonly _model: ITextModel; private _isSynced: boolean; + private _pendingChanges: IModelContentChangedEvent[] = []; constructor(worker: TextMateWorker, model: ITextModel) { super(); @@ -42,6 +46,7 @@ class ModelWorkerTextMateTokenizer extends Disposable { this._register(this._model.onDidChangeContent((e) => { if (this._isSynced) { this._worker.acceptModelChanged(this._model.uri.toString(), e); + this._pendingChanges.push(e); } })); @@ -84,11 +89,36 @@ class ModelWorkerTextMateTokenizer extends Disposable { super.dispose(); this._endSync(); } + + private _confirm(versionId: number): void { + while (this._pendingChanges.length > 0 && this._pendingChanges[0].versionId <= versionId) { + this._pendingChanges.shift(); + } + } + + public setTokens(versionId: number, rawTokens: ArrayBuffer): void { + this._confirm(versionId); + const tokens = MultilineTokensBuilder.deserialize(new Uint8Array(rawTokens)); + + for (let i = 0; i < this._pendingChanges.length; i++) { + const change = this._pendingChanges[i]; + for (let j = 0; j < tokens.length; j++) { + for (let k = 0; k < change.changes.length; k++) { + tokens[j].applyEdit(change.changes[k].range, change.changes[k].text); + } + } + } + + this._model.setTokens(tokens); + } } export class TextMateWorkerHost { - constructor(@IFileService private readonly _fileService: IFileService) { + constructor( + private readonly textMateService: TextMateService, + @IFileService private readonly _fileService: IFileService + ) { } async readFile(_resource: UriComponents): Promise { @@ -96,6 +126,11 @@ export class TextMateWorkerHost { const content = await this._fileService.readFile(resource); return content.value.toString(); } + + async setTokens(_resource: UriComponents, versionId: number, tokens: Uint8Array): Promise { + const resource = URI.revive(_resource); + this.textMateService.setTokens(resource, versionId, tokens); + } } export class TextMateService extends AbstractTextMateService { @@ -155,7 +190,7 @@ export class TextMateService extends AbstractTextMateService { this._killWorker(); if (RUN_TEXTMATE_IN_WORKER) { - const workerHost = new TextMateWorkerHost(this._fileService); + const workerHost = new TextMateWorkerHost(this, this._fileService); const worker = createWebWorker(this._modelService, { createData: { grammarDefinitions @@ -172,11 +207,21 @@ export class TextMateService extends AbstractTextMateService { return; } this._workerProxy = proxy; + if (this._currentTheme) { + this._workerProxy.acceptTheme(this._currentTheme); + } this._modelService.getModels().forEach((model) => this._onModelAdded(model)); }); } } + protected _doUpdateTheme(grammarFactory: TMGrammarFactory, theme: IRawTheme): void { + super._doUpdateTheme(grammarFactory, theme); + if (this._currentTheme && this._workerProxy) { + this._workerProxy.acceptTheme(this._currentTheme); + } + } + protected _onDidDisposeGrammarFactory(): void { this._killWorker(); } @@ -193,6 +238,14 @@ export class TextMateService extends AbstractTextMateService { } this._workerProxy = null; } + + setTokens(resource: URI, versionId: number, tokens: ArrayBuffer): void { + const key = resource.toString(); + if (!this._tokenizers[key]) { + return; + } + this._tokenizers[key].setTokens(versionId, tokens); + } } -registerSingleton(ITextMateService, TextMateService); \ No newline at end of file +registerSingleton(ITextMateService, TextMateService); diff --git a/src/vs/workbench/services/textMate/electron-browser/textMateWorker.ts b/src/vs/workbench/services/textMate/electron-browser/textMateWorker.ts index 27361f4835e..a5c24f1d7f7 100644 --- a/src/vs/workbench/services/textMate/electron-browser/textMateWorker.ts +++ b/src/vs/workbench/services/textMate/electron-browser/textMateWorker.ts @@ -10,6 +10,10 @@ import { IValidEmbeddedLanguagesMap, IValidTokenTypeMap, IValidGrammarDefinition import { TMGrammarFactory, ICreateGrammarResult } from 'vs/workbench/services/textMate/common/TMGrammarFactory'; import { IModelChangedEvent, MirrorTextModel } from 'vs/editor/common/model/mirrorTextModel'; import { TextMateWorkerHost } from 'vs/workbench/services/textMate/electron-browser/textMateService'; +import { TokenizationStateStore } from 'vs/editor/common/model/textModelTokens'; +import { IGrammar, StackElement, IRawTheme } from 'vscode-textmate'; +import { MultilineTokensBuilder, countEOL } from 'vs/editor/common/model/tokensStore'; +import { LineTokens } from 'vs/editor/common/core/lineTokens'; export interface IValidGrammarDefinitionDTO { location: UriComponents; @@ -34,26 +38,79 @@ export interface IRawModelData { class TextMateWorkerModel extends MirrorTextModel { + private readonly _tokenizationStateStore: TokenizationStateStore; private readonly _worker: TextMateWorker; private _languageId: LanguageId; + private _grammar: IGrammar | null; + private _isDisposed: boolean; constructor(uri: URI, lines: string[], eol: string, versionId: number, worker: TextMateWorker, languageId: LanguageId) { super(uri, lines, eol, versionId); + this._tokenizationStateStore = new TokenizationStateStore(); this._worker = worker; + this._languageId = languageId; + this._isDisposed = false; + this._grammar = null; + this._resetTokenization(); + } + + public dispose(): void { + this._isDisposed = true; + super.dispose(); + } + + public onLanguageId(languageId: LanguageId): void { this._languageId = languageId; this._resetTokenization(); } - onLanguageId(languageId: LanguageId): void { - this._languageId = languageId; - this._resetTokenization(); + onEvents(e: IModelChangedEvent): void { + super.onEvents(e); + for (let i = 0; i < e.changes.length; i++) { + const change = e.changes[i]; + const [eolCount] = countEOL(change.text); + this._tokenizationStateStore.applyEdits(change.range, eolCount); + } + this._ensureTokens(); } private _resetTokenization(): void { - this._worker.getOrCreateGrammar(this._languageId).then((r) => { - console.log(r); + this._grammar = null; + this._tokenizationStateStore.flush(null); + + const languageId = this._languageId; + this._worker.getOrCreateGrammar(languageId).then((r) => { + if (this._isDisposed || languageId !== this._languageId) { + return; + } + + this._grammar = r.grammar; + this._tokenizationStateStore.flush(r.initialState); + this._ensureTokens(); }); } + + private _ensureTokens(): void { + if (!this._grammar) { + return; + } + const builder = new MultilineTokensBuilder(); + const lineCount = this._lines.length; + + // Validate all states up to and including endLineIndex + for (let lineIndex = this._tokenizationStateStore.invalidLineStartIndex; lineIndex < lineCount; lineIndex++) { + const text = this._lines[lineIndex]; + const lineStartState = this._tokenizationStateStore.getBeginState(lineIndex); + + const r = this._grammar.tokenizeLine2(text, lineStartState!); + LineTokens.convertToEndOffset(r.tokens, text.length); + builder.add(lineIndex + 1, r.tokens); + this._tokenizationStateStore.setEndState(lineCount, lineIndex, r.ruleStack); + lineIndex = this._tokenizationStateStore.invalidLineStartIndex - 1; // -1 because the outer loop increments it + } + + this._worker._setTokens(this._uri, this._versionId, builder.serialize()); + } } export class TextMateWorker { @@ -91,7 +148,7 @@ export class TextMateWorker { } this._grammarFactory = new TMGrammarFactory({ - logTrace: (msg: string) => console.log(msg), + logTrace: (msg: string) => {/* console.log(msg) */ }, logError: (msg: string, err: any) => console.error(msg, err), readFile: (resource: URI) => this._host.readFile(resource) }, grammarDefinitions, vscodeTextmate, undefined); @@ -112,7 +169,10 @@ export class TextMateWorker { } public acceptRemovedModel(strURL: string): void { - delete this._models[strURL]; + if (this._models[strURL]) { + this._models[strURL].dispose(); + delete this._models[strURL]; + } } public getOrCreateGrammar(languageId: LanguageId): Promise { @@ -121,6 +181,14 @@ export class TextMateWorker { } return this._grammarCache[languageId]; } + + public acceptTheme(theme: IRawTheme): void { + this._grammarFactory.setTheme(theme); + } + + public _setTokens(resource: URI, versionId: number, tokens: Uint8Array): void { + this._host.setTokens(resource, versionId, tokens); + } } export function create(ctx: IWorkerContext, createData: ICreateData): TextMateWorker { diff --git a/src/vs/workbench/test/browser/part.test.ts b/src/vs/workbench/test/browser/part.test.ts index 73878b4e603..ed151b3aced 100644 --- a/src/vs/workbench/test/browser/part.test.ts +++ b/src/vs/workbench/test/browser/part.test.ts @@ -10,7 +10,6 @@ import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService import { append, $, hide } from 'vs/base/browser/dom'; import { TestStorageService, TestLayoutService } from 'vs/workbench/test/workbenchTestServices'; import { StorageScope } from 'vs/platform/storage/common/storage'; -import { Orientation } from 'vs/base/browser/ui/grid/grid'; class SimplePart extends Part { @@ -19,7 +18,7 @@ class SimplePart extends Part { minimumHeight: number; maximumHeight: number; - layout(width: number, height: number, orientation: Orientation): void { + layout(width: number, height: number): void { throw new Error('Method not implemented.'); } @@ -172,4 +171,4 @@ suite('Workbench parts', () => { assert(!document.getElementById('myPart.title')); assert(document.getElementById('myPart.content')); }); -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index 9e9e0d739e8..48ebaf8e296 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -474,6 +474,10 @@ export class TestLayoutService implements IWorkbenchLayoutService { return true; } + getDimension(_part: Parts): Dimension { + return new Dimension(0, 0); + } + public getContainer(_part: Parts): HTMLElement { return null!; } @@ -667,7 +671,7 @@ export class TestEditorGroupsService implements IEditorGroupsService { whenRestored: Promise = Promise.resolve(undefined); willRestoreEditors = false; - dimension = { width: 800, height: 600 }; + contentDimension = { width: 800, height: 600 }; get activeGroup(): IEditorGroup { return this.groups[0]; @@ -1473,7 +1477,7 @@ export class TestWindowsService implements IWindowsService { return Promise.resolve(this.windowCount); } - log(_severity: string, ..._messages: string[]): Promise { + log(_severity: string, _args: string[]): Promise { return Promise.resolve(); } @@ -1622,4 +1626,4 @@ export class RemoteFileSystemProvider implements IFileSystemProvider { write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise { return this.diskFileSystemProvider.write!(fd, pos, data, offset, length); } private toFileResource(resource: URI): URI { return resource.with({ scheme: Schemas.file, authority: '' }); } -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index 6c206d6fa00..f1c162556e3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3907,14 +3907,15 @@ gulp-plumber@^1.2.0: plugin-error "^0.1.2" through2 "^2.0.3" -gulp-remote-src@^0.4.4: - version "0.4.4" - resolved "https://registry.yarnpkg.com/gulp-remote-src/-/gulp-remote-src-0.4.4.tgz#4a4d18fac0ffedde94a7855953de90db00a1d1b1" - integrity sha512-mo7lGgZmNXyTbcUzfjSnUVkx1pnqqiwv/pPaIrYdTO77hq0WNTxXLAzQdoYOnyJ0mfVLNmNl9AGqWLiAzTPMMA== +gulp-remote-retry-src@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/gulp-remote-retry-src/-/gulp-remote-retry-src-0.6.0.tgz#fdcb5d5c9e67c31ae378a2a886ddad3d47913bb1" + integrity sha512-lFxpwwbM/GEIdYiNumxiUcPHZUROFJaF1zTBne1H8b3Pwx6Te6O9uEYp++JZPP62jdheOWcHUTBREiMkpdbm4Q== dependencies: event-stream "3.3.4" node.extend "~1.1.2" request "^2.88.0" + requestretry "^4.0.0" through2 "~2.0.3" vinyl "~2.0.1" @@ -4161,6 +4162,13 @@ has@^1.0.1: dependencies: function-bind "^1.0.2" +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + hash-base@^3.0.0: version "3.0.4" resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" @@ -6088,11 +6096,12 @@ node-pty@0.9.0-beta19: nan "^2.13.2" node.extend@~1.1.2: - version "1.1.6" - resolved "https://registry.yarnpkg.com/node.extend/-/node.extend-1.1.6.tgz#a7b882c82d6c93a4863a5504bd5de8ec86258b96" - integrity sha1-p7iCyC1sk6SGOlUEvV3o7IYli5Y= + version "1.1.8" + resolved "https://registry.yarnpkg.com/node.extend/-/node.extend-1.1.8.tgz#0aab3e63789f4e6d68b42bc00073ad1881243cf0" + integrity sha512-L/dvEBwyg3UowwqOUTyDsGBU6kjBQOpOhshio9V3i3BMPv5YUb9+mWNN8MK0IbWqT0AqaTSONZf0aTuMMahWgA== dependencies: - is "^3.1.0" + has "^1.0.3" + is "^3.2.1" noop-logger@^0.1.1: version "0.1.1" @@ -7737,6 +7746,15 @@ request@^2.86.0, request@^2.88.0: tunnel-agent "^0.6.0" uuid "^3.3.2" +requestretry@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/requestretry/-/requestretry-4.0.0.tgz#4e9e7280a7d8561bf33e9925264cf026e2be3e89" + integrity sha512-ST8m0+5FQH2FA+gbzUQyOQjUwHf22kbPQnd6TexveR0p+2UV1YYBg+Roe7BnKQ1Bb/+LtJwwm0QzxK2NA20Cug== + dependencies: + extend "^3.0.2" + lodash "^4.17.10" + when "^3.7.7" + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -9684,6 +9702,11 @@ webpack@^4.16.5, webpack@^4.7.0: watchpack "^1.5.0" webpack-sources "^1.0.1" +when@^3.7.7: + version "3.7.8" + resolved "https://registry.yarnpkg.com/when/-/when-3.7.8.tgz#c7130b6a7ea04693e842cdc9e7a1f2aa39a39f82" + integrity sha1-xxMLan6gRpPoQs3J56Hyqjmjn4I= + whet.extend@~0.9.9: version "0.9.9" resolved "https://registry.yarnpkg.com/whet.extend/-/whet.extend-0.9.9.tgz#f877d5bf648c97e5aa542fadc16d6a259b9c11a1"