diff --git a/extensions/bat/package.json b/extensions/bat/package.json index 8363d58ed04..d9f1cba592b 100644 --- a/extensions/bat/package.json +++ b/extensions/bat/package.json @@ -17,6 +17,10 @@ "language": "bat", "scopeName": "source.batchfile", "path": "./syntaxes/batchfile.tmLanguage.json" + }], + "snippets": [{ + "language": "bat", + "path": "./snippets/batchfile.snippets.json" }] } } \ No newline at end of file diff --git a/extensions/bat/snippets/batchfile.snippets.json b/extensions/bat/snippets/batchfile.snippets.json new file mode 100644 index 00000000000..3759e25eacd --- /dev/null +++ b/extensions/bat/snippets/batchfile.snippets.json @@ -0,0 +1,16 @@ +{ + "Region Start": { + "prefix": "#region", + "body": [ + "::#region" + ], + "description": "Folding Region Start" + }, + "Region End": { + "prefix": "#endregion", + "body": [ + "::#endregion" + ], + "description": "Folding Region End" + } +} diff --git a/extensions/coffeescript/language-configuration.json b/extensions/coffeescript/language-configuration.json index 8c7fbd458df..01fc34db790 100644 --- a/extensions/coffeescript/language-configuration.json +++ b/extensions/coffeescript/language-configuration.json @@ -23,6 +23,10 @@ ["'", "'"] ], "folding": { - "offSide": true + "offSide": true, + "markers": { + "start": "^\\s*#region\\b", + "end": "^\\s*#endregion\\b" + } } -} \ No newline at end of file +} diff --git a/extensions/coffeescript/package.json b/extensions/coffeescript/package.json index 5c8f11b7206..c688f7b0836 100644 --- a/extensions/coffeescript/package.json +++ b/extensions/coffeescript/package.json @@ -22,6 +22,10 @@ { "language": "coffeescript" } - ] + ], + "snippets": [{ + "language": "coffeescript", + "path": "./snippets/coffeescript.snippets.json" + }] } } \ No newline at end of file diff --git a/extensions/coffeescript/snippets/coffeescript.snippets.json b/extensions/coffeescript/snippets/coffeescript.snippets.json new file mode 100644 index 00000000000..49d6a927993 --- /dev/null +++ b/extensions/coffeescript/snippets/coffeescript.snippets.json @@ -0,0 +1,16 @@ +{ + "Region Start": { + "prefix": "#region", + "body": [ + "#region" + ], + "description": "Folding Region Start" + }, + "Region End": { + "prefix": "#endregion", + "body": [ + "#endregion" + ], + "description": "Folding Region End" + } +} diff --git a/extensions/css/package.json b/extensions/css/package.json index e404748578a..51f35887735 100644 --- a/extensions/css/package.json +++ b/extensions/css/package.json @@ -43,6 +43,10 @@ "path": "./syntaxes/css.tmLanguage.json" } ], + "snippets": [{ + "language": "css", + "path": "./snippets/css.snippets.json" + }], "configuration": [ { "order": 22, diff --git a/extensions/css/snippets/css.snippets.json b/extensions/css/snippets/css.snippets.json new file mode 100644 index 00000000000..30207dbb525 --- /dev/null +++ b/extensions/css/snippets/css.snippets.json @@ -0,0 +1,16 @@ +{ + "Region Start": { + "prefix": "#region", + "body": [ + "/*#region $0*/" + ], + "description": "Folding Region Start" + }, + "Region End": { + "prefix": "#endregion", + "body": [ + "/*#endregion $0*/" + ], + "description": "Folding Region End" + } +} diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 93f50de36e0..09140870137 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -171,10 +171,8 @@ export class Resource implements SourceControlResourceState { } get decorations(): SourceControlResourceDecorations { - // TODO@joh, still requires restart/redraw in the SCM viewlet - const decorations = workspace.getConfiguration().get('git.decorations.enabled'); - const light = !decorations ? { iconPath: this.getIconPath('light') } : undefined; - const dark = !decorations ? { iconPath: this.getIconPath('dark') } : undefined; + const light = this._useIcons ? { iconPath: this.getIconPath('light') } : undefined; + const dark = this._useIcons ? { iconPath: this.getIconPath('dark') } : undefined; const tooltip = this.tooltip; const strikeThrough = this.strikeThrough; const faded = this.faded; @@ -275,6 +273,7 @@ export class Resource implements SourceControlResourceState { private _resourceGroupType: ResourceGroupType, private _resourceUri: Uri, private _type: Status, + private _useIcons: boolean, private _renameResourceUri?: Uri ) { } } @@ -863,6 +862,7 @@ export class Repository implements Disposable { const { status, didHitLimit } = await this.repository.getStatus(); const config = workspace.getConfiguration('git'); const shouldIgnore = config.get('ignoreLimitWarning') === true; + const useIcons = config.get('decorations.enabled', true); this.isRepositoryHuge = didHitLimit; @@ -910,30 +910,30 @@ export class Repository implements Disposable { const renameUri = raw.rename ? Uri.file(path.join(this.repository.root, raw.rename)) : undefined; switch (raw.x + raw.y) { - case '??': return workingTree.push(new Resource(ResourceGroupType.WorkingTree, uri, Status.UNTRACKED)); - case '!!': return workingTree.push(new Resource(ResourceGroupType.WorkingTree, uri, Status.IGNORED)); - case 'DD': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.BOTH_DELETED)); - case 'AU': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.ADDED_BY_US)); - case 'UD': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.DELETED_BY_THEM)); - case 'UA': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.ADDED_BY_THEM)); - case 'DU': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.DELETED_BY_US)); - case 'AA': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.BOTH_ADDED)); - case 'UU': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.BOTH_MODIFIED)); + case '??': return workingTree.push(new Resource(ResourceGroupType.WorkingTree, uri, Status.UNTRACKED, useIcons)); + case '!!': return workingTree.push(new Resource(ResourceGroupType.WorkingTree, uri, Status.IGNORED, useIcons)); + case 'DD': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.BOTH_DELETED, useIcons)); + case 'AU': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.ADDED_BY_US, useIcons)); + case 'UD': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.DELETED_BY_THEM, useIcons)); + case 'UA': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.ADDED_BY_THEM, useIcons)); + case 'DU': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.DELETED_BY_US, useIcons)); + case 'AA': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.BOTH_ADDED, useIcons)); + case 'UU': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.BOTH_MODIFIED, useIcons)); } let isModifiedInIndex = false; switch (raw.x) { - case 'M': index.push(new Resource(ResourceGroupType.Index, uri, Status.INDEX_MODIFIED)); isModifiedInIndex = true; break; - case 'A': index.push(new Resource(ResourceGroupType.Index, uri, Status.INDEX_ADDED)); break; - case 'D': index.push(new Resource(ResourceGroupType.Index, uri, Status.INDEX_DELETED)); break; - case 'R': index.push(new Resource(ResourceGroupType.Index, uri, Status.INDEX_RENAMED, renameUri)); break; - case 'C': index.push(new Resource(ResourceGroupType.Index, uri, Status.INDEX_COPIED, renameUri)); break; + case 'M': index.push(new Resource(ResourceGroupType.Index, uri, Status.INDEX_MODIFIED, useIcons)); isModifiedInIndex = true; break; + case 'A': index.push(new Resource(ResourceGroupType.Index, uri, Status.INDEX_ADDED, useIcons)); break; + case 'D': index.push(new Resource(ResourceGroupType.Index, uri, Status.INDEX_DELETED, useIcons)); break; + case 'R': index.push(new Resource(ResourceGroupType.Index, uri, Status.INDEX_RENAMED, useIcons, renameUri)); break; + case 'C': index.push(new Resource(ResourceGroupType.Index, uri, Status.INDEX_COPIED, useIcons, renameUri)); break; } switch (raw.y) { - case 'M': workingTree.push(new Resource(ResourceGroupType.WorkingTree, uri, Status.MODIFIED, renameUri)); break; - case 'D': workingTree.push(new Resource(ResourceGroupType.WorkingTree, uri, Status.DELETED, renameUri)); break; + case 'M': workingTree.push(new Resource(ResourceGroupType.WorkingTree, uri, Status.MODIFIED, useIcons, renameUri)); break; + case 'D': workingTree.push(new Resource(ResourceGroupType.WorkingTree, uri, Status.DELETED, useIcons, renameUri)); break; } }); diff --git a/extensions/html/server/src/htmlServerMain.ts b/extensions/html/server/src/htmlServerMain.ts index 1719e612d2a..2593ab4b147 100644 --- a/extensions/html/server/src/htmlServerMain.ts +++ b/extensions/html/server/src/htmlServerMain.ts @@ -103,7 +103,7 @@ connection.onInitialize((params: InitializeParams): InitializeResult => { let capabilities: ServerCapabilities & CPServerCapabilities = { // Tell the client that the server works in FULL text document sync mode textDocumentSync: documents.syncKind, - completionProvider: clientSnippetSupport ? { resolveProvider: true, triggerCharacters: ['.', ':', '<', '"', '=', '/', '>'] } : undefined, + completionProvider: clientSnippetSupport ? { resolveProvider: true, triggerCharacters: ['.', ':', '<', '"', '=', '/'] } : undefined, hoverProvider: true, documentHighlightProvider: true, documentRangeFormattingProvider: false, diff --git a/extensions/java/package.json b/extensions/java/package.json index d8c9375f0c3..6493004d0a8 100644 --- a/extensions/java/package.json +++ b/extensions/java/package.json @@ -17,6 +17,10 @@ "language": "java", "scopeName": "source.java", "path": "./syntaxes/java.tmLanguage.json" + }], + "snippets": [{ + "language": "java", + "path": "./snippets/java.snippets.json" }] } } \ No newline at end of file diff --git a/extensions/java/snippets/java.snippets.json b/extensions/java/snippets/java.snippets.json new file mode 100644 index 00000000000..9a2300b18f2 --- /dev/null +++ b/extensions/java/snippets/java.snippets.json @@ -0,0 +1,16 @@ +{ + "Region Start": { + "prefix": "#region", + "body": [ + "//#region" + ], + "description": "Folding Region Start" + }, + "Region End": { + "prefix": "#endregion", + "body": [ + "//#endregion" + ], + "description": "Folding Region End" + } +} diff --git a/extensions/json/package.json b/extensions/json/package.json index 43908f3c681..ec79d0ea37a 100644 --- a/extensions/json/package.json +++ b/extensions/json/package.json @@ -59,7 +59,9 @@ "settings.json", "launch.json", "tasks.json", - "keybindings.json" + "keybindings.json", + "tsconfig.json", + "jsconfig.json" ], "configuration": "./language-configuration.json" } diff --git a/extensions/less/language-configuration.json b/extensions/less/language-configuration.json index f60130d9d46..181954633b0 100644 --- a/extensions/less/language-configuration.json +++ b/extensions/less/language-configuration.json @@ -25,5 +25,11 @@ "indentationRules": { "increaseIndentPattern": "(^.*\\{[^}]*$)", "decreaseIndentPattern": "^\\s*\\}" + }, + "folding": { + "markers": { + "start": "^\\s*\\/\\*\\s*#region\\b\\s*(.*?)\\s*\\*\\/", + "end": "^\\s*\\/\\*\\s*#endregion\\b.*\\*\\/" + } } } \ No newline at end of file diff --git a/extensions/less/package.json b/extensions/less/package.json index b23dfe356b9..d9aef5866b7 100644 --- a/extensions/less/package.json +++ b/extensions/less/package.json @@ -19,6 +19,10 @@ "scopeName": "source.css.less", "path": "./syntaxes/less.tmLanguage.json" }], + "snippets": [{ + "language": "less", + "path": "./snippets/less.snippets.json" + }], "problemMatchers": [ { "name": "lessc", diff --git a/extensions/less/snippets/less.snippets.json b/extensions/less/snippets/less.snippets.json new file mode 100644 index 00000000000..30207dbb525 --- /dev/null +++ b/extensions/less/snippets/less.snippets.json @@ -0,0 +1,16 @@ +{ + "Region Start": { + "prefix": "#region", + "body": [ + "/*#region $0*/" + ], + "description": "Folding Region Start" + }, + "Region End": { + "prefix": "#endregion", + "body": [ + "/*#endregion $0*/" + ], + "description": "Folding Region End" + } +} diff --git a/extensions/php/package.json b/extensions/php/package.json index 7d4eb5204e3..6954ae5c661 100644 --- a/extensions/php/package.json +++ b/extensions/php/package.json @@ -53,7 +53,7 @@ "snippets": [ { "language": "php", - "path": "./snippets/php.json" + "path": "./snippets/php.snippets.json" } ], "configuration": { diff --git a/extensions/php/snippets/php.json b/extensions/php/snippets/php.snippets.json similarity index 95% rename from extensions/php/snippets/php.json rename to extensions/php/snippets/php.snippets.json index 24a4426a54a..4ae37b3a6e0 100644 --- a/extensions/php/snippets/php.json +++ b/extensions/php/snippets/php.snippets.json @@ -234,5 +234,19 @@ "$0" ], "description": "Throw exception" + }, + "Region Start": { + "prefix": "#region", + "body": [ + "#region" + ], + "description": "Folding Region Start" + }, + "Region End": { + "prefix": "#endregion", + "body": [ + "#endregion" + ], + "description": "Folding Region End" } } \ No newline at end of file diff --git a/extensions/php/src/features/completionItemProvider.ts b/extensions/php/src/features/completionItemProvider.ts index c948c8357ad..601944f277c 100644 --- a/extensions/php/src/features/completionItemProvider.ts +++ b/extensions/php/src/features/completionItemProvider.ts @@ -5,12 +5,12 @@ 'use strict'; -import { CompletionItemProvider, CompletionItem, CompletionItemKind, CancellationToken, TextDocument, Position, Range, TextEdit, workspace } from 'vscode'; +import { CompletionItemProvider, CompletionItem, CompletionItemKind, CancellationToken, TextDocument, Position, Range, TextEdit, workspace, CompletionContext } from 'vscode'; import phpGlobals = require('./phpGlobals'); export default class PHPCompletionItemProvider implements CompletionItemProvider { - public provideCompletionItems(document: TextDocument, position: Position, _token: CancellationToken): Promise { + public provideCompletionItems(document: TextDocument, position: Position, _token: CancellationToken, context: CompletionContext): Promise { let result: CompletionItem[] = []; let shouldProvideCompletionItems = workspace.getConfiguration('php').get('suggest.basic', true); @@ -24,6 +24,14 @@ export default class PHPCompletionItemProvider implements CompletionItemProvider range = new Range(position, position); } + if (context.triggerCharacter === '>') { + const twoBeforeCursor = new Position(position.line, Math.max(0, position.character - 2)); + const previousTwoChars = document.getText(new Range(twoBeforeCursor, position)); + if (previousTwoChars !== '->') { + return Promise.resolve(result); + } + } + var added: any = {}; var createNewProposal = function (kind: CompletionItemKind, name: string, entry: phpGlobals.IEntry | null): CompletionItem { var proposal: CompletionItem = new CompletionItem(name); diff --git a/extensions/python/language-configuration.json b/extensions/python/language-configuration.json index 14ad98220b4..736124425d7 100644 --- a/extensions/python/language-configuration.json +++ b/extensions/python/language-configuration.json @@ -13,7 +13,23 @@ { "open": "[", "close": "]" }, { "open": "(", "close": ")" }, { "open": "\"", "close": "\"", "notIn": ["string"] }, - { "open": "'", "close": "'", "notIn": ["string", "comment"] } + { "open": "r\"", "close": "\"", "notIn": ["string", "comment"] }, + { "open": "R\"", "close": "\"", "notIn": ["string", "comment"] }, + { "open": "u\"", "close": "\"", "notIn": ["string", "comment"] }, + { "open": "U\"", "close": "\"", "notIn": ["string", "comment"] }, + { "open": "f\"", "close": "\"", "notIn": ["string", "comment"] }, + { "open": "F\"", "close": "\"", "notIn": ["string", "comment"] }, + { "open": "b\"", "close": "\"", "notIn": ["string", "comment"] }, + { "open": "B\"", "close": "\"", "notIn": ["string", "comment"] }, + { "open": "'", "close": "'", "notIn": ["string", "comment"] }, + { "open": "r'", "close": "'", "notIn": ["string", "comment"] }, + { "open": "R'", "close": "'", "notIn": ["string", "comment"] }, + { "open": "u'", "close": "'", "notIn": ["string", "comment"] }, + { "open": "U'", "close": "'", "notIn": ["string", "comment"] }, + { "open": "f'", "close": "'", "notIn": ["string", "comment"] }, + { "open": "F'", "close": "'", "notIn": ["string", "comment"] }, + { "open": "b'", "close": "'", "notIn": ["string", "comment"] }, + { "open": "B'", "close": "'", "notIn": ["string", "comment"] } ], "surroundingPairs": [ ["{", "}"], diff --git a/extensions/scss/language-configuration.json b/extensions/scss/language-configuration.json index 36a9cccd688..bdf0984ec18 100644 --- a/extensions/scss/language-configuration.json +++ b/extensions/scss/language-configuration.json @@ -21,5 +21,11 @@ ["(", ")"], ["\"", "\""], ["'", "'"] - ] + ], + "folding": { + "markers": { + "start": "^\\s*\\/\\*\\s*#region\\b\\s*(.*?)\\s*\\*\\/", + "end": "^\\s*\\/\\*\\s*#endregion\\b.*\\*\\/" + } + } } \ No newline at end of file diff --git a/extensions/scss/package.json b/extensions/scss/package.json index 37e3abc67c5..74ae7f718fc 100644 --- a/extensions/scss/package.json +++ b/extensions/scss/package.json @@ -19,6 +19,10 @@ "scopeName": "source.css.scss", "path": "./syntaxes/scss.json" }], + "snippets": [{ + "language": "scss", + "path": "./snippets/scss.snippets.json" + }], "problemMatchers": [{ "name": "node-sass", "label": "Node Sass Compiler", diff --git a/extensions/scss/snippets/scss.snippets.json b/extensions/scss/snippets/scss.snippets.json new file mode 100644 index 00000000000..30207dbb525 --- /dev/null +++ b/extensions/scss/snippets/scss.snippets.json @@ -0,0 +1,16 @@ +{ + "Region Start": { + "prefix": "#region", + "body": [ + "/*#region $0*/" + ], + "description": "Folding Region Start" + }, + "Region End": { + "prefix": "#endregion", + "body": [ + "/*#endregion $0*/" + ], + "description": "Folding Region End" + } +} diff --git a/extensions/typescript/src/typescriptMain.ts b/extensions/typescript/src/typescriptMain.ts index 3c620dc95cf..64093b4c85f 100644 --- a/extensions/typescript/src/typescriptMain.ts +++ b/extensions/typescript/src/typescriptMain.ts @@ -430,6 +430,7 @@ class TypeScriptServiceClientHost implements ITypeScriptServiceClientHost { private readonly languagePerId = new Map(); private readonly disposables: Disposable[] = []; private readonly versionStatus: VersionStatus; + private reportStyleCheckAsWarnings: boolean = true; constructor( descriptions: LanguageDescription[], @@ -496,6 +497,9 @@ class TypeScriptServiceClientHost implements ITypeScriptServiceClientHost { this.client.onTsServerStarted(() => { this.triggerAllDiagnostics(); }); + + workspace.onDidChangeConfiguration(this.configurationChanged, this, this.disposables); + this.configurationChanged(); } public dispose(): void { @@ -603,6 +607,11 @@ class TypeScriptServiceClientHost implements ITypeScriptServiceClientHost { } } + private configurationChanged(): void { + const config = workspace.getConfiguration('typescript'); + this.reportStyleCheckAsWarnings = config.get('reportStyleChecksAsWarnings', true); + } + private async findLanguage(file: string): Promise { try { const doc = await workspace.openTextDocument(this.client.asUrl(file)); @@ -717,8 +726,7 @@ class TypeScriptServiceClientHost implements ITypeScriptServiceClientHost { } private getDiagnosticSeverity(diagnostic: Proto.Diagnostic): DiagnosticSeverity { - - if (this.reportStyleCheckAsWarnings() && this.isStyleCheckDiagnostic(diagnostic.code)) { + if (this.reportStyleCheckAsWarnings && this.isStyleCheckDiagnostic(diagnostic.code)) { return DiagnosticSeverity.Warning; } @@ -737,9 +745,4 @@ class TypeScriptServiceClientHost implements ITypeScriptServiceClientHost { private isStyleCheckDiagnostic(code: number | undefined): boolean { return code ? styleCheckDiagnostics.indexOf(code) !== -1 : false; } - - private reportStyleCheckAsWarnings() { - const config = workspace.getConfiguration('typescript'); - return config.get('reportStyleChecksAsWarnings', true); - } } \ No newline at end of file diff --git a/i18n/deu/extensions/git/package.i18n.json b/i18n/deu/extensions/git/package.i18n.json index 88964f86644..29c8acd65d3 100644 --- a/i18n/deu/extensions/git/package.i18n.json +++ b/i18n/deu/extensions/git/package.i18n.json @@ -47,7 +47,7 @@ "command.publish": "Branch veröffentlichen", "command.showOutput": "Git-Ausgabe anzeigen", "command.ignore": "Datei zu .gitignore hinzufügen", - "command.stash": " Stash ausführen", + "command.stash": "Stash ausführen", "command.stashPop": "Pop für Stash ausführen...", "command.stashPopLatest": "Pop für letzten Stash ausführen", "config.enabled": "Gibt an, ob Git aktiviert ist.", @@ -70,4 +70,4 @@ "colors.untracked": "Farbe für nicht verfolgte Ressourcen.", "colors.ignored": "Farbe für ignorierte Ressourcen.", "colors.conflict": "Farbe für Ressourcen mit Konflikten." -} \ No newline at end of file +} diff --git a/resources/linux/bin/code.sh b/resources/linux/bin/code.sh index bb146af861f..088bcf8b2ab 100755 --- a/resources/linux/bin/code.sh +++ b/resources/linux/bin/code.sh @@ -12,7 +12,7 @@ if [ "$(id -u)" = "0" ]; then fi done if [ -z $DATA_DIR_SET ]; then - echo "It is recommended to start vscode as a normal user. To run as root, you must specify an alternate user data directory with the --user-data-dir argument." 1>&2 + echo "You are trying to start vscode as a super user which is not recommended. If you really want to, you must specify an alternate user data directory using the --user-data-dir argument." 1>&2 exit 1 fi fi diff --git a/src/vs/base/common/color.ts b/src/vs/base/common/color.ts index d650dc0d92d..f18a87dc15e 100644 --- a/src/vs/base/common/color.ts +++ b/src/vs/base/common/color.ts @@ -214,7 +214,7 @@ export class HSVA { m = ((r - g) / delta) + 4; } - return new HSVA(m * 60, s, cmax, rgba.a); + return new HSVA(Math.round(m * 60), s, cmax, rgba.a); } // from http://www.rapidtables.com/convert/color/hsv-to-rgb.htm diff --git a/src/vs/base/node/ps.ts b/src/vs/base/node/ps.ts new file mode 100644 index 00000000000..17f1b02500b --- /dev/null +++ b/src/vs/base/node/ps.ts @@ -0,0 +1,163 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { spawn, exec } from 'child_process'; + +export interface ProcessItem { + name: string; + cmd: string; + pid: number; + ppid: number; + load: number; + mem: number; + + children?: ProcessItem[]; +} + +export function listProcesses(rootPid: number): Promise { + + return new Promise((resolve, reject) => { + + let rootItem: ProcessItem; + const map = new Map(); + + function addToTree(pid: number, ppid: number, cmd: string, load: number, mem: number) { + + const parent = map.get(ppid); + if (pid === rootPid || parent) { + + const item: ProcessItem = { + name: findName(cmd), + cmd, + pid, + ppid, + load, + mem + }; + map.set(pid, item); + + if (pid === rootPid) { + rootItem = item; + } + + if (parent) { + if (!parent.children) { + parent.children = []; + } + parent.children.push(item); + if (parent.children.length > 1) { + parent.children = parent.children.sort((a, b) => a.pid - b.pid); + } + } + } + } + + function findName(cmd: string): string { + + const RENDERER_PROCESS_HINT = /--disable-blink-features=Auxclick/; + const WINDOWS_WATCHER_HINT = /\\watcher\\win32\\CodeHelper.exe/; + const TYPE = /--type=([a-zA-Z-]+)/; + + // find windows file watcher + if (WINDOWS_WATCHER_HINT.exec(cmd)) { + return 'watcherService'; + } + + // find "--type=xxxx" + let matches = TYPE.exec(cmd); + if (matches && matches.length === 2) { + if (matches[1] === 'renderer') { + if (!RENDERER_PROCESS_HINT.exec(cmd)) { + return 'shared-process'; + } + + return `renderer`; + } + return matches[1]; + } + + // find all xxxx.js + const JS = /[a-zA-Z-]+\.js/g; + let result = ''; + do { + matches = JS.exec(cmd); + if (matches) { + result += matches + ' '; + } + } while (matches); + + if (result) { + if (cmd.indexOf('node ') !== 0) { + return `electron_node ${result}`; + } + } + return cmd; + } + + if (process.platform === 'win32') { + + const CMD = 'wmic process get ProcessId,ParentProcessId,CommandLine \n'; + const CMD_PID = /^(.+)\s+([0-9]+)\s+([0-9]+)$/; + + let stdout = ''; + let stderr = ''; + + const cmd = spawn('cmd'); + + cmd.stdout.on('data', data => { + stdout += data.toString(); + }); + cmd.stderr.on('data', data => { + stderr += data.toString(); + }); + + cmd.on('exit', () => { + + if (stderr.length > 0) { + reject(stderr); + } else { + + const lines = stdout.split('\r\n'); + for (const line of lines) { + let matches = CMD_PID.exec(line.trim()); + if (matches && matches.length === 4) { + addToTree(parseInt(matches[3]), parseInt(matches[2]), matches[1].trim(), 0.0, 0.0); + } + } + + resolve(rootItem); + } + }); + + cmd.stdin.write(CMD); + cmd.stdin.end(); + + } else { // OS X & Linux + + const CMD = 'ps -ax -o pid=,ppid=,pcpu=,pmem=,command='; + const PID_CMD = /^\s*([0-9]+)\s+([0-9]+)\s+([0-9]+\.[0-9]+)\s+([0-9]+\.[0-9]+)\s+(.+)$/; + + exec(CMD, { maxBuffer: 1000 * 1024 }, (err, stdout, stderr) => { + + if (err || stderr) { + reject(err || stderr.toString()); + } else { + + const lines = stdout.toString().split('\n'); + for (const line of lines) { + let matches = PID_CMD.exec(line.trim()); + if (matches && matches.length === 6) { + addToTree(parseInt(matches[1]), parseInt(matches[2]), matches[5], parseFloat(matches[3]), parseFloat(matches[4])); + } + } + + resolve(rootItem); + } + }); + } + }); +} diff --git a/src/vs/base/node/stats.ts b/src/vs/base/node/stats.ts new file mode 100644 index 00000000000..def7b94207d --- /dev/null +++ b/src/vs/base/node/stats.ts @@ -0,0 +1,100 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { readdirSync, statSync } from 'fs'; + +export interface WorkspaceStatItem { + name: string; + value: number; +} +export interface WorkspaceStats { + fileTypes: WorkspaceStatItem[]; + configFiles: WorkspaceStatItem[]; +} + +export function collectWorkspaceStats(folder: string, filter: string[]): WorkspaceStats { + const configFilePatterns = [ + { 'tag': 'grunt.js', 'pattern': /^gruntfile\.js$/i }, + { 'tag': 'gulp.js', 'pattern': /^gulpfile\.js$/i }, + { 'tag': 'tsconfig.json', 'pattern': /^tsconfig\.json$/i }, + { 'tag': 'package.json', 'pattern': /^package\.json$/i }, + { 'tag': 'jsconfig.json', 'pattern': /^jsconfig\.json$/i }, + { 'tag': 'tslint.json', 'pattern': /^tslint\.json$/i }, + { 'tag': 'eslint.json', 'pattern': /^eslint\.json$/i }, + { 'tag': 'tasks.json', 'pattern': /^tasks\.json$/i }, + { 'tag': 'launch.json', 'pattern': /^launch\.json$/i }, + { 'tag': 'settings.json', 'pattern': /^settings\.json$/i }, + { 'tag': 'webpack.config.js', 'pattern': /^webpack\.config\.js$/i }, + { 'tag': 'project.json', 'pattern': /^project\.json$/i }, + { 'tag': 'makefile', 'pattern': /^makefile$/i }, + { 'tag': 'sln', 'pattern': /^.+\.sln$/i }, + { 'tag': 'csproj', 'pattern': /^.+\.csproj$/i }, + { 'tag': 'cmake', 'pattern': /^.+\.cmake$/i } + ]; + + let fileTypes = new Map(); + let configFiles = new Map(); + + let walkSync = (dir: string, acceptFile: (fileName: string) => void, filter: string[]) => { + let files = readdirSync(dir); + for (const file of files) { + if (statSync(dir + '/' + file).isDirectory()) { + if (filter.indexOf(file) === -1) { + walkSync(dir + '/' + file, acceptFile, filter); + } + } + else { + acceptFile(file); + } + } + }; + + let addFileType = (fileType: string) => { + if (fileTypes.has(fileType)) { + fileTypes.set(fileType, fileTypes.get(fileType) + 1); + } + else { + fileTypes.set(fileType, 1); + } + }; + + let addConfigFiles = (fileName: string) => { + for (const each of configFilePatterns) { + if (each.pattern.test(fileName)) { + if (configFiles.has(each.tag)) { + configFiles.set(each.tag, configFiles.get(each.tag) + 1); + } else { + configFiles.set(each.tag, 1); + } + } + } + }; + + let acceptFile = (name: string) => { + if (name.lastIndexOf('.') >= 0) { + let suffix: string | undefined = name.split('.').pop(); + if (suffix) { + addFileType(suffix); + } + } + addConfigFiles(name); + }; + + let asSortedItems = (map: Map): WorkspaceStatItem[] => { + let a: WorkspaceStatItem[] = []; + map.forEach((value, index) => a.push({ name: index, value: value })); + return a.sort((a, b) => b.value - a.value); + }; + + walkSync(folder, acceptFile, filter); + + let result = { + 'configFiles': asSortedItems(configFiles), + 'fileTypes': asSortedItems(fileTypes) + }; + return result; +} \ No newline at end of file diff --git a/src/vs/base/test/common/color.test.ts b/src/vs/base/test/common/color.test.ts index 7ccee7a4d51..359bc6c398e 100644 --- a/src/vs/base/test/common/color.test.ts +++ b/src/vs/base/test/common/color.test.ts @@ -153,6 +153,7 @@ suite('Color', () => { assert.deepEqual(HSVA.toRGBA(new HSVA(300, 1, 0.502, 1)), new RGBA(128, 0, 128, 1)); assert.deepEqual(HSVA.toRGBA(new HSVA(180, 1, 0.502, 1)), new RGBA(0, 128, 128, 1)); assert.deepEqual(HSVA.toRGBA(new HSVA(240, 1, 0.502, 1)), new RGBA(0, 0, 128, 1)); + }); test('HSVA.fromRGBA', () => { @@ -185,6 +186,11 @@ suite('Color', () => { assert.deepEqual(new Color(new HSVA(10, 0, 0, 0)).rgba, new Color(new HSVA(20, 0, 0, 0)).rgba); assert.notDeepEqual(new Color(new HSVA(10, 0, 0, 0)).hsva, new Color(new HSVA(20, 0, 0, 0)).hsva); }); + + test('bug#36240', () => { + assert.deepEqual(HSVA.fromRGBA(new RGBA(92, 106, 196, 1)), new HSVA(232, .531, .769, 1)); + assert.deepEqual(HSVA.toRGBA(HSVA.fromRGBA(new RGBA(92, 106, 196, 1))), new RGBA(92, 106, 196, 1)); + }); }); suite('Format', () => { diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 186ab3198ae..f455c77a8f5 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -109,7 +109,7 @@ export class CodeApplication { }); app.on('will-quit', () => { - this.logService.log('App#will-quit: disposing resources'); + this.logService.info('App#will-quit: disposing resources'); this.dispose(); }); @@ -121,7 +121,7 @@ export class CodeApplication { }); app.on('activate', (event: Event, hasVisibleWindows: boolean) => { - this.logService.log('App#activate'); + this.logService.info('App#activate'); // Mac only event: open new window when we get activated if (!hasVisibleWindows && this.windowsMainService) { @@ -156,7 +156,7 @@ export class CodeApplication { let macOpenFiles: string[] = []; let runningTimeout: number = null; app.on('open-file', (event: Event, path: string) => { - this.logService.log('App#open-file: ', path); + this.logService.info('App#open-file: ', path); event.preventDefault(); // Keep in array because more might come! @@ -188,7 +188,7 @@ export class CodeApplication { }); ipc.on('vscode:exit', (_event: any, code: number) => { - this.logService.log('IPC#vscode:exit', code); + this.logService.info('IPC#vscode:exit', code); this.dispose(); this.lifecycleService.kill(code); @@ -211,7 +211,7 @@ export class CodeApplication { ipc.on('vscode:broadcast', (_event: any, windowId: number, broadcast: { channel: string; payload: any; }) => { if (this.windowsMainService && broadcast.channel && !isUndefinedOrNull(broadcast.payload)) { - this.logService.log('IPC#vscode:broadcast', broadcast.channel, broadcast.payload); + this.logService.info('IPC#vscode:broadcast', broadcast.channel, broadcast.payload); // Handle specific events on main side this.onBroadcast(broadcast.channel, broadcast.payload); @@ -241,9 +241,9 @@ export class CodeApplication { } public startup(): TPromise { - this.logService.log('Starting VS Code in verbose mode'); - this.logService.log(`from: ${this.environmentService.appRoot}`); - this.logService.log('args:', this.environmentService.args); + this.logService.info('Starting VS Code in verbose mode'); + this.logService.info(`from: ${this.environmentService.appRoot}`); + this.logService.info('args:', this.environmentService.args); // Make sure we associate the program with the app user model id // This will help Windows to associate the running program with @@ -257,9 +257,9 @@ export class CodeApplication { this.electronIpcServer = new ElectronIPCServer(); // Resolve unique machine ID - this.logService.log('Resolving machine identifier...'); + this.logService.info('Resolving machine identifier...'); return this.resolveMachineId().then(machineId => { - this.logService.log(`Resolved machine identifier: ${machineId}`); + this.logService.info(`Resolved machine identifier: ${machineId}`); // Spawn shared process this.sharedProcess = new SharedProcess(this.environmentService, machineId, this.userEnv); diff --git a/src/vs/code/electron-main/diagnostics.ts b/src/vs/code/electron-main/diagnostics.ts new file mode 100644 index 00000000000..7731c1c23f3 --- /dev/null +++ b/src/vs/code/electron-main/diagnostics.ts @@ -0,0 +1,159 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { WorkspaceStats, collectWorkspaceStats } from 'vs/base/node/stats'; +import { IMainProcessInfo } from 'vs/code/electron-main/launch'; +import { ProcessItem, listProcesses } from 'vs/base/node/ps'; +import product from 'vs/platform/node/product'; +import pkg from 'vs/platform/node/package'; +import * as os from 'os'; +import { virtualMachineHint } from 'vs/base/node/id'; +import { repeat, pad } from 'vs/base/common/strings'; +import { isWindows } from 'vs/base/common/platform'; +import { app } from 'electron'; +import { basename } from 'path'; + +export function printDiagnostics(info: IMainProcessInfo): Promise { + return listProcesses(info.mainPID).then(rootProcess => { + + // Environment Info + console.log(''); + console.log(formatEnvironment(info)); + + // Process List + console.log(''); + console.log(formatProcessList(info, rootProcess)); + + // Workspace Stats + if (info.windows.some(window => window.folders.length > 0)) { + console.log(''); + console.log('Workspace Stats: '); + info.windows.forEach(window => { + if (window.folders.length === 0) { + return; + } + + console.log(`| Renderer (${window.title})`); + + window.folders.forEach(folder => { + console.log(`| Folder (${basename(folder)})`); + const stats = collectWorkspaceStats(folder, ['node_modules', '.git']); + console.log(formatWorkspaceStats(stats)); + }); + }); + } + console.log(''); + console.log(''); + }); +} + +function formatWorkspaceStats(workspaceStats: WorkspaceStats): string { + const output: string[] = []; + const lineLength = 60; + let col = 0; + + const appendAndWrap = (index: string, value: number) => { + const item = ` ${index}(${value})`; + if (col + item.length > lineLength) { + output.push(line); + line = '| '; + col = line.length; + } + else { + col += item.length; + } + line += item; + }; + + + // File Types + let line = '| File types:'; + let hasFileTypes = false; + workspaceStats.fileTypes.forEach((item) => { + if (item.value > 20) { + hasFileTypes = true; + appendAndWrap(item.name, item.value); + } + }); + if (!hasFileTypes) { + line = `${line} `; + } + output.push(line); + + // Conf Files + line = '| Conf files:'; + col = 0; + let hasConfFiles = false; + workspaceStats.configFiles.forEach((item) => { + hasConfFiles = true; + appendAndWrap(item.name, item.value); + }); + if (!hasConfFiles) { + line = `${line} `; + } + output.push(line); + + return output.join('\n'); +} + +function formatEnvironment(info: IMainProcessInfo): string { + const MB = 1024 * 1024; + const GB = 1024 * MB; + + const output: string[] = []; + output.push(`Version: ${pkg.name} ${pkg.version} (${product.commit || 'Commit unknown'}, ${product.date || 'Date unknown'})`); + output.push(`OS Version: ${os.type()} ${os.arch()} ${os.release()})`); + const cpus = os.cpus(); + if (cpus && cpus.length > 0) { + output.push(`CPUs: ${cpus[0].model} (${cpus.length} x ${cpus[0].speed})`); + } + output.push(`Memory (System): ${(os.totalmem() / GB).toFixed(2)}GB (${(os.freemem() / GB).toFixed(2)}GB free)`); + if (!isWindows) { + output.push(`Load (avg): ${os.loadavg().map(l => Math.round(l)).join(', ')}`); // only provided on Linux/macOS + } + output.push(`VM: ${Math.round((virtualMachineHint.value() * 100))}%`); + output.push(`Screen Reader: ${app.isAccessibilitySupportEnabled() ? 'yes' : 'no'}`); + + return output.join('\n'); +} + +function formatProcessList(info: IMainProcessInfo, rootProcess: ProcessItem): string { + const mapPidToWindowTitle = new Map(); + info.windows.forEach(window => mapPidToWindowTitle.set(window.pid, window.title)); + + const output: string[] = []; + + output.push('CPU %\tMem MB\tProcess'); + + formatProcessItem(mapPidToWindowTitle, output, rootProcess, 0); + + return output.join('\n'); +} + +function formatProcessItem(mapPidToWindowTitle: Map, output: string[], item: ProcessItem, indent: number): void { + const isRoot = (indent === 0); + + const MB = 1024 * 1024; + + // Format name with indent + let name: string; + if (isRoot) { + name = `${product.applicationName} main`; + } else { + name = `${repeat(' ', indent)} ${item.name}`; + + if (item.name === 'renderer') { + name = `${name} (${mapPidToWindowTitle.get(item.pid)})`; + } + } + output.push(`${pad(Number(item.load.toFixed(0)), 5, ' ')}\t${pad(Number(((os.totalmem() * (item.mem / 100)) / MB).toFixed(0)), 6, ' ')}\t${name}`); + + // Recurse into children if any + if (Array.isArray(item.children)) { + item.children.forEach(child => formatProcessItem(mapPidToWindowTitle, output, child, indent + 1)); + } +} \ No newline at end of file diff --git a/src/vs/code/electron-main/launch.ts b/src/vs/code/electron-main/launch.ts index f613a445f81..692af0700cb 100644 --- a/src/vs/code/electron-main/launch.ts +++ b/src/vs/code/electron-main/launch.ts @@ -15,6 +15,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { OpenContext } from 'vs/platform/windows/common/windows'; import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows'; import { whenDeleted } from 'vs/base/node/pfs'; +import { IWorkspacesMainService } from 'vs/platform/workspaces/common/workspaces'; export const ID = 'launchService'; export const ILaunchService = createDecorator(ID); @@ -24,15 +25,28 @@ export interface IStartArguments { userEnv: IProcessEnvironment; } +export interface IWindowInfo { + pid: number; + title: string; + folders: string[]; +} + +export interface IMainProcessInfo { + mainPID: number; + windows: IWindowInfo[]; +} + export interface ILaunchService { _serviceBrand: any; start(args: ParsedArgs, userEnv: IProcessEnvironment): TPromise; getMainProcessId(): TPromise; + getMainProcessInfo(): TPromise; } export interface ILaunchChannel extends IChannel { call(command: 'start', arg: IStartArguments): TPromise; call(command: 'get-main-process-id', arg: null): TPromise; + call(command: 'get-main-process-info', arg: null): TPromise; call(command: string, arg: any): TPromise; } @@ -48,6 +62,9 @@ export class LaunchChannel implements ILaunchChannel { case 'get-main-process-id': return this.service.getMainProcessId(); + + case 'get-main-process-info': + return this.service.getMainProcessInfo(); } return undefined; @@ -67,6 +84,10 @@ export class LaunchChannelClient implements ILaunchService { public getMainProcessId(): TPromise { return this.channel.call('get-main-process-id', null); } + + public getMainProcessInfo(): TPromise { + return this.channel.call('get-main-process-info', null); + } } export class LaunchService implements ILaunchService { @@ -76,11 +97,12 @@ export class LaunchService implements ILaunchService { constructor( @ILogService private logService: ILogService, @IWindowsMainService private windowsMainService: IWindowsMainService, - @IURLService private urlService: IURLService + @IURLService private urlService: IURLService, + @IWorkspacesMainService private workspacesMainService: IWorkspacesMainService ) { } public start(args: ParsedArgs, userEnv: IProcessEnvironment): TPromise { - this.logService.log('Received data from other instance: ', args, userEnv); + this.logService.info('Received data from other instance: ', args, userEnv); // Check early for open-url which is handled in URL service const openUrlArg = args['open-url'] || []; @@ -127,8 +149,40 @@ export class LaunchService implements ILaunchService { } public getMainProcessId(): TPromise { - this.logService.log('Received request for process ID from other instance.'); + this.logService.info('Received request for process ID from other instance.'); return TPromise.as(process.pid); } + + public getMainProcessInfo(): TPromise { + this.logService.info('Received request for main process info from other instance.'); + + return TPromise.wrap({ + mainPID: process.pid, + windows: this.windowsMainService.getWindows().map(window => { + return this.getWindowInfo(window); + }) + } as IMainProcessInfo); + } + + private getWindowInfo(window: ICodeWindow): IWindowInfo { + const folders: string[] = []; + + if (window.openedFolderPath) { + folders.push(window.openedFolderPath); + } else if (window.openedWorkspace) { + const rootFolders = this.workspacesMainService.resolveWorkspaceSync(window.openedWorkspace.configPath).folders; + rootFolders.forEach(root => { + if (root.uri.scheme === 'file') { + folders.push(root.uri.fsPath); + } + }); + } + + return { + pid: window.win.webContents.getOSProcessId(), + title: window.win.getTitle(), + folders + } as IWindowInfo; + } } \ No newline at end of file diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index d8e0a76c457..472cb0ce925 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -15,12 +15,12 @@ import { validatePaths } from 'vs/code/node/paths'; import { LifecycleService, ILifecycleService } from 'vs/platform/lifecycle/electron-main/lifecycleMain'; import { Server, serve, connect } from 'vs/base/parts/ipc/node/ipc.net'; import { TPromise } from 'vs/base/common/winjs.base'; -import { ILaunchChannel, LaunchChannelClient } from './launch'; +import { ILaunchChannel, LaunchChannelClient } from 'vs/code/electron-main/launch'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { ILogService, LogMainService } from 'vs/platform/log/common/log'; +import { ILogService, LegacyLogMainService } from 'vs/platform/log/common/log'; import { StateService } from 'vs/platform/state/node/stateService'; import { IStateService } from 'vs/platform/state/common/state'; import { IBackupMainService } from 'vs/platform/backup/common/backup'; @@ -41,12 +41,13 @@ import { WorkspacesMainService } from 'vs/platform/workspaces/electron-main/work import { IWorkspacesMainService } from 'vs/platform/workspaces/common/workspaces'; import { localize } from 'vs/nls'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; +import { printDiagnostics } from 'vs/code/electron-main/diagnostics'; function createServices(args: ParsedArgs): IInstantiationService { const services = new ServiceCollection(); services.set(IEnvironmentService, new SyncDescriptor(EnvironmentService, args, process.execPath)); - services.set(ILogService, new SyncDescriptor(LogMainService)); + services.set(ILogService, new SyncDescriptor(LegacyLogMainService, 'main')); services.set(IWorkspacesMainService, new SyncDescriptor(WorkspacesMainService)); services.set(IHistoryMainService, new SyncDescriptor(HistoryMainService)); services.set(ILifecycleService, new SyncDescriptor(LifecycleService)); @@ -81,7 +82,7 @@ function setupIPC(accessor: ServicesAccessor): TPromise { if (platform.isWindows) { promise = service.getMainProcessId() .then(processId => { - logService.log('Sending some foreground love to the running instance:', processId); + logService.info('Sending some foreground love to the running instance:', processId); try { const { allowSetForegroundWindow } = require.__$__nodeRequire('windows-foreground-love'); @@ -101,6 +102,11 @@ function setupIPC(accessor: ServicesAccessor): TPromise { app.dock.show(); // dock might be hidden at this case due to a retry } + // Print --ps usage info + if (environmentService.args.ps) { + console.log('Warning: The --ps argument can only be used if Code is already running. Please run it again after Code has started.'); + } + return server; }, err => { if (err.code !== 'EADDRINUSE') { @@ -125,8 +131,6 @@ function setupIPC(accessor: ServicesAccessor): TPromise { return TPromise.wrapError(new Error(msg)); } - logService.log('Sending env to running instance...'); - // Show a warning dialog after some timeout if it takes long to talk to the other instance // Skip this if we are running with --wait where it is expected that we wait for a while let startupWarningDialogHandle: number; @@ -142,6 +146,15 @@ function setupIPC(accessor: ServicesAccessor): TPromise { const channel = client.getChannel('launch'); const service = new LaunchChannelClient(channel); + // Process Info + if (environmentService.args.ps) { + return service.getMainProcessInfo().then(info => { + return printDiagnostics(info).then(() => TPromise.wrapError(new ExpectedError())); + }); + } + + logService.info('Sending env to running instance...'); + return allowSetForegroundWindow(service) .then(() => service.start(environmentService.args, process.env)) .then(() => client.dispose()) @@ -173,7 +186,7 @@ function setupIPC(accessor: ServicesAccessor): TPromise { try { fs.unlinkSync(environmentService.mainIPCHandle); } catch (e) { - logService.log('Fatal error deleting obsolete instance handle', e); + logService.info('Fatal error deleting obsolete instance handle', e); return TPromise.wrapError(e); } @@ -205,7 +218,7 @@ function quit(accessor: ServicesAccessor, reason?: ExpectedError | Error): void if (reason) { if ((reason as ExpectedError).isExpected) { - logService.log(reason.message); + logService.info(reason.message); } else { exitCode = 1; // signal error to the outside diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index 86e1240889b..d124479b68d 100644 --- a/src/vs/code/electron-main/window.ts +++ b/src/vs/code/electron-main/window.ts @@ -202,7 +202,7 @@ export class CodeWindow implements ICodeWindow { } } } catch (err) { - this.logService.log(`Unexpected error fixing window position on windows with multiple windows: ${err}\n${err.stack}`); + this.logService.info(`Unexpected error fixing window position on windows with multiple windows: ${err}\n${err.stack}`); } } @@ -671,7 +671,7 @@ export class CodeWindow implements ICodeWindow { try { state = this.validateWindowState(state); } catch (err) { - this.logService.log(`Unexpected error validating window state: ${err}\n${err.stack}`); // somehow display API can be picky about the state to validate + this.logService.info(`Unexpected error validating window state: ${err}\n${err.stack}`); // somehow display API can be picky about the state to validate } } diff --git a/src/vs/code/electron-main/windows.ts b/src/vs/code/electron-main/windows.ts index 7bf059f20bf..21e46fe7b39 100644 --- a/src/vs/code/electron-main/windows.ts +++ b/src/vs/code/electron-main/windows.ts @@ -203,7 +203,7 @@ export class WindowsManager implements IWindowsMainService { // React to workbench loaded events from windows ipc.on('vscode:workbenchLoaded', (_event: any, windowId: number) => { - this.logService.log('IPC#vscode-workbenchLoaded'); + this.logService.info('IPC#vscode-workbenchLoaded'); const win = this.getWindowById(windowId); if (win) { diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index b0f83200890..293855d91bc 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -37,14 +37,24 @@ export async function main(argv: string[]): TPromise { return TPromise.as(null); } + // Help if (args.help) { console.log(buildHelpMessage(product.nameLong, product.applicationName, pkg.version)); - } else if (args.version) { + } + + // Version Info + else if (args.version) { console.log(`${pkg.version}\n${product.commit}\n${process.arch}`); - } else if (shouldSpawnCliProcess(args)) { + } + + // Extensions Management + else if (shouldSpawnCliProcess(args)) { const mainCli = new TPromise(c => require(['vs/code/node/cliProcessMain'], c)); return mainCli.then(cli => cli.main(args)); - } else { + } + + // Just Code + else { const env = assign({}, process.env, { // this will signal Code that it was spawned from this module 'VSCODE_CLI': '1', @@ -55,7 +65,9 @@ export async function main(argv: string[]): TPromise { let processCallbacks: ((child: ChildProcess) => Thenable)[] = []; - if (args.verbose) { + const verbose = args.verbose || args.ps; + + if (verbose) { env['ELECTRON_ENABLE_LOGGING'] = '1'; processCallbacks.push(child => { @@ -68,7 +80,7 @@ export async function main(argv: string[]): TPromise { // If we are running with input from stdin, pipe that into a file and // open this file via arguments. Ignore this when we are passed with - // paths to open. + // paths to open. let isReadingFromStdin: boolean; try { isReadingFromStdin = args._.length === 0 && !process.stdin.isTTY; // Via https://twitter.com/MylesBorins/status/782009479382626304 @@ -97,7 +109,7 @@ export async function main(argv: string[]): TPromise { stdinFileError = error; } - if (args.verbose) { + if (verbose) { if (stdinFileError) { console.error(`Failed to create file to read via stdin: ${stdinFileError.toString()}`); } else { @@ -122,7 +134,7 @@ export async function main(argv: string[]): TPromise { waitMarkerError = error; } - if (args.verbose) { + if (verbose) { if (waitMarkerError) { console.error(`Failed to create marker file for --wait: ${waitMarkerError.toString()}`); } else { @@ -195,7 +207,7 @@ export async function main(argv: string[]): TPromise { env }; - if (!args.verbose) { + if (!verbose) { options['stdio'] = 'ignore'; } diff --git a/src/vs/code/node/code-ps.ps1 b/src/vs/code/node/code-ps.ps1 new file mode 100644 index 00000000000..21705c7ef4a --- /dev/null +++ b/src/vs/code/node/code-ps.ps1 @@ -0,0 +1,185 @@ +################################################################################################ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +################################################################################################ + +Param( + [bool]$Insider = $false, + [int]$MaxSamples = 10 +) + +$processName = "code.exe" +if ($Insider) { + $processName = "code - insiders.exe" +} +$processLength = "process(".Length + +function Get-MachineInfo { + $model = (Get-WmiObject -Class Win32_Processor).Name + $memory = (Get-WmiObject -Class Win32_PhysicalMemory | Measure-Object -Property Capacity -Sum).Sum / 1MB + $wmi_cs = Get-WmiObject -Class Win32_ComputerSystem + return @{ + "type" = "machineInfo" + "model" = $model + "processors" = $wmi_cs.NumberOfProcessors + "logicalProcessors" = $wmi_cs.NumberOfLogicalProcessors + "totalMemory" = $memory + + } +} +$machineInfo = Get-MachineInfo + +function Get-MachineState { + $proc = Get-WmiObject Win32_Processor + $os = Get-WmiObject win32_OperatingSystem + return @{ + "type" = 'machineState' + "cpuLoad" = $proc.LoadPercentage + "handles" = (Get-Process | Measure-Object Handles -Sum).Sum + "memory" = @{ + "total" = $os.TotalVisibleMemorySize + "free" = $os.FreePhysicalMemory + "swapTotal" = $os.TotalVirtualMemorySize + "swapFree" = $os.FreeVirtualMemory + } + } +} +$machineState = Get-MachineState + +$processId2CpuLoad = @{} +function Get-PerformanceCounters ($logicalProcessors) { + $counterError + # In a first round we get the performance counters and the process ids. + $counters = (Get-Counter ("\Process(*)\% Processor Time", "\Process(*)\ID Process")).CounterSamples + $processKey2Id = @{} + foreach ($counter in $counters) { + if ($counter.Status -ne 0) { + continue + } + $path = $counter.path; + $segments = $path.Split("\"); + $kind = $segments[4]; + $processKey = $segments[3].Substring($processLength, $segments[3].Length - $processLength - 1) + if ($kind -eq "id process") { + $processKey2Id[$processKey] = [uint32]$counter.CookedValue + } + } + foreach ($counter in $counters) { + if ($counter.Status -ne 0) { + continue + } + $path = $counter.path; + $segments = $path.Split("\"); + $kind = $segments[4]; + $processKey = $segments[3].Substring($processLength, $segments[3].Length - $processLength - 1) + if ($kind -eq "% processor time") { + $array = New-Object double[] ($MaxSamples + 1) + $array[0] = ($counter.CookedValue / $logicalProcessors) + $processId = $processKey2Id[$processKey] + if ($processId) { + $processId2CpuLoad[$processId] = $array + } + } + } + # Now lets sample another 10 times but only the processor time + $samples = Get-Counter "\Process(*)\% Processor Time" -SampleInterval 1 -MaxSamples $MaxSamples + for ($s = 0; $s -lt $samples.Count; $s++) { + $counters = $samples[$s].CounterSamples; + foreach ($counter in $counters) { + if ($counter.Status -ne 0) { + continue + } + $path = $counter.path; + $segments = $path.Split("\"); + $processKey = $segments[3].Substring($processLength, $segments[3].Length - $processLength - 1) + $processKey = $processKey2Id[$processKey]; + if ($processKey) { + $processId2CpuLoad[$processKey][$s + 1] = ($counter.CookedValue / $logicalProcessors) + } + } + } +} +Get-PerformanceCounters -logicalProcessors $machineInfo.logicalProcessors + +$topElements = New-Object PSObject[] $processId2CpuLoad.Keys.Count; +$index = 0; +foreach ($key in $processId2CpuLoad.Keys) { + $obj = [PSCustomObject]@{ + ProcessId = $key + Load = ($processId2CpuLoad[$key] | Measure-Object -Sum).Sum / ($MaxSamples + 1) + } + $topElements[$index] = $obj + $index++ +} +$topElements = $topElements | Sort-Object Load -Descending + +# Get all code processes +$codeProcesses = @{} +foreach ($item in Get-WmiObject Win32_Process -Filter "name = '$processName'") { + $codeProcesses[$item.ProcessId] = $item +} +foreach ($item in Get-WmiObject Win32_Process -Filter "name = 'codeHelper.exe'") { + $codeProcesses[$item.ProcessId] = $item +} +$otherProcesses = @{} +foreach ($item in Get-WmiObject Win32_Process -Filter "name Like '%'") { + if (!($codeProcesses.Contains($item.ProcessId))) { + $otherProcesses[$item.ProcessId] = $item + } +} +$modified = $false +do { + $toDelete = @() + $modified = $false + foreach ($item in $otherProcesses.Values) { + if ($codeProcesses.Contains([uint32]$item.ParentProcessId)) { + $codeProcesses[$item.ProcessId] = $item; + $toDelete += $item + } + } + foreach ($item in $toDelete) { + $otherProcesses.Remove([uint32]$item.ProcessId) + $modified = $true + } +} while ($modified) + +$result = New-Object PSObject[] (2 + [math]::Min(5, $topElements.Count) + $codeProcesses.Count) +$result[0] = $machineInfo +$result[1] = $machineState +$index = 2; +for($i = 0; $i -lt 5 -and $i -lt $topElements.Count; $i++) { + $element = $topElements[$i] + $item = $codeProcesses[[uint32]$element.ProcessId] + if (!$item) { + $item = $otherProcesses[[uint32]$element.ProcessId] + } + if ($item) { + $cpuLoad = $processId2CpuLoad[[uint32]$item.ProcessId] | % { [pscustomobject] $_ } + $result[$index] = [pscustomobject]@{ + "type" = "topProcess" + "name" = $item.Name + "processId" = $item.ProcessId + "parentProcessId" = $item.ParentProcessId + "commandLine" = $item.CommandLine + "handles" = $item.HandleCount + "cpuLoad" = $cpuLoad + } + $index++ + } +} +foreach ($item in $codeProcesses.Values) { + # we need to convert this otherwise to JSON with create a value, count object and not an inline array + $cpuLoad = $processId2CpuLoad[[uint32]$item.ProcessId] | % { [pscustomobject] $_ } + $result[$index] = [pscustomobject]@{ + "type" = "processInfo" + "name" = $item.Name + "processId" = $item.ProcessId + "parentProcessId" = $item.ParentProcessId + "commandLine" = $item.CommandLine + "handles" = $item.HandleCount + "cpuLoad" = $cpuLoad + } + $index++ +} + +$result | ConvertTo-Json -Depth 99 diff --git a/src/vs/editor/contrib/folding/folding.ts b/src/vs/editor/contrib/folding/folding.ts index 56c2fcec5da..6396a3551e7 100644 --- a/src/vs/editor/contrib/folding/folding.ts +++ b/src/vs/editor/contrib/folding/folding.ts @@ -181,7 +181,10 @@ export class FoldingController implements IEditorContribution { if (this.updateScheduler) { this.foldingModelPromise = this.updateScheduler.trigger(() => { if (this.foldingModel) { // null if editor has been disposed, or folding turned off - this.foldingModel.update(this.computeRanges(this.foldingModel.textModel)); + // some cursors might have moved into hidden regions, make sure they are in expanded regions + let selections = this.editor.getSelections(); + let selectionLineNumbers = selections ? selections.map(s => s.startLineNumber) : []; + this.foldingModel.update(this.computeRanges(this.foldingModel.textModel), selectionLineNumbers); } return this.foldingModel; }); @@ -533,6 +536,54 @@ class FoldAllBlockCommentsAction extends FoldingAction { } } +class FoldAllRegionsAction extends FoldingAction { + + constructor() { + super({ + id: 'editor.foldAllMarkerRegions', + label: nls.localize('foldAllMarkerRegions.label', "Fold All Marker Regions"), + alias: 'Fold All Marker Regions', + precondition: null, + kbOpts: { + kbExpr: EditorContextKeys.textFocus, + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_8) + } + }); + } + + invoke(foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor): void { + let foldingRules = LanguageConfigurationRegistry.getFoldingRules(editor.getModel().getLanguageIdentifier().id); + if (foldingRules && foldingRules.markers && foldingRules.markers.start) { + let regExp = new RegExp(foldingRules.markers.start); + setCollapseStateForMatchingLines(foldingModel, regExp, true); + } + } +} + +class UnfoldAllRegionsAction extends FoldingAction { + + constructor() { + super({ + id: 'editor.unfoldAllMarkerRegions', + label: nls.localize('unfoldAllMarkerRegions.label', "Unfold All Marker Regions"), + alias: 'Unfold All Marker Regions', + precondition: null, + kbOpts: { + kbExpr: EditorContextKeys.textFocus, + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_9) + } + }); + } + + invoke(foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor): void { + let foldingRules = LanguageConfigurationRegistry.getFoldingRules(editor.getModel().getLanguageIdentifier().id); + if (foldingRules && foldingRules.markers && foldingRules.markers.start) { + let regExp = new RegExp(foldingRules.markers.start); + setCollapseStateForMatchingLines(foldingModel, regExp, false); + } + } +} + class FoldAllAction extends FoldingAction { constructor() { @@ -594,8 +645,10 @@ registerEditorAction(FoldRecursivelyAction); registerEditorAction(FoldAllAction); registerEditorAction(UnfoldAllAction); registerEditorAction(FoldAllBlockCommentsAction); +registerEditorAction(FoldAllRegionsAction); +registerEditorAction(UnfoldAllRegionsAction); -for (let i = 1; i <= 9; i++) { +for (let i = 1; i <= 7; i++) { registerInstantiatedEditorAction( new FoldLevelAction({ id: FoldLevelAction.ID(i), diff --git a/src/vs/editor/contrib/folding/foldingModel.ts b/src/vs/editor/contrib/folding/foldingModel.ts index ea3d547a526..8c39fd86c30 100644 --- a/src/vs/editor/contrib/folding/foldingModel.ts +++ b/src/vs/editor/contrib/folding/foldingModel.ts @@ -60,12 +60,24 @@ export class FoldingModel { this._updateEventEmitter.fire({ model: this, collapseStateChanged: regions }); } - public update(newRanges: FoldingRanges): void { + public update(newRanges: FoldingRanges, blockedLineNumers: number[] = []): void { let newEditorDecorations = []; + let isBlocked = (startLineNumber, endLineNumber) => { + for (let blockedLineNumber of blockedLineNumers) { + if (startLineNumber < blockedLineNumber && blockedLineNumber <= endLineNumber) { // first line is visible + return true; + } + } + return false; + }; + let initRange = (index: number, isCollapsed: boolean) => { - newRanges.setCollapsed(index, isCollapsed); let startLineNumber = newRanges.getStartLineNumber(index); + if (isCollapsed && isBlocked(startLineNumber, newRanges.getEndLineNumber(index))) { + isCollapsed = false; + } + newRanges.setCollapsed(index, isCollapsed); let maxColumn = this._textModel.getLineMaxColumn(startLineNumber); let decorationRange = { startLineNumber: startLineNumber, diff --git a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts index ae7618cefd3..6472c256330 100644 --- a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts +++ b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts @@ -19,7 +19,7 @@ import { BackupMainService } from 'vs/platform/backup/electron-main/backupMainSe import { IBackupWorkspacesFormat } from 'vs/platform/backup/common/backup'; import { HotExitConfiguration } from 'vs/platform/files/common/files'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { LogMainService } from 'vs/platform/log/common/log'; +import { LegacyLogMainService } from 'vs/platform/log/common/log'; import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { createHash } from 'crypto'; import { getRandomTestPath } from 'vs/workbench/test/workbenchTestServices'; @@ -34,7 +34,7 @@ suite('BackupMainService', () => { class TestBackupMainService extends BackupMainService { constructor(backupHome: string, backupWorkspacesPath: string, configService: TestConfigurationService) { - super(environmentService, configService, new LogMainService(environmentService)); + super(environmentService, configService, new LegacyLogMainService('test', environmentService)); this.backupHome = backupHome; this.workspacesJsonPath = backupWorkspacesPath; diff --git a/src/vs/platform/commands/common/commandService.ts b/src/vs/platform/commands/common/commandService.ts index 570d8e6df19..a8fc43ad4da 100644 --- a/src/vs/platform/commands/common/commandService.ts +++ b/src/vs/platform/commands/common/commandService.ts @@ -11,6 +11,7 @@ import { IExtensionService } from 'vs/platform/extensions/common/extensions'; import Event, { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { log, LogLevel, ILogService } from 'vs/platform/log/common/log'; export class CommandService extends Disposable implements ICommandService { @@ -24,12 +25,15 @@ export class CommandService extends Disposable implements ICommandService { constructor( @IInstantiationService private _instantiationService: IInstantiationService, @IExtensionService private _extensionService: IExtensionService, - @IContextKeyService private _contextKeyService: IContextKeyService + @IContextKeyService private _contextKeyService: IContextKeyService, + // @ts-ignore + @ILogService private logService: ILogService ) { super(); this._extensionService.whenInstalledExtensionsRegistered().then(value => this._extensionHostIsReady = value); } + @log(LogLevel.INFO, 'CommandService', (msg, id) => `${msg}(${id})`) executeCommand(id: string, ...args: any[]): TPromise { // we always send an activation event, but // we don't wait for it when the extension diff --git a/src/vs/platform/commands/test/commandService.test.ts b/src/vs/platform/commands/test/commandService.test.ts index 27d613f04bd..78207993cb0 100644 --- a/src/vs/platform/commands/test/commandService.test.ts +++ b/src/vs/platform/commands/test/commandService.test.ts @@ -16,6 +16,7 @@ import { ContextKeyService } from 'vs/platform/contextkey/browser/contextKeyServ import { SimpleConfigurationService } from 'vs/editor/standalone/browser/simpleServices'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import Event, { Emitter } from 'vs/base/common/event'; +import { NoopLogService } from 'vs/platform/log/common/log'; class SimpleExtensionService implements IExtensionService { _serviceBrand: any; @@ -74,7 +75,7 @@ suite('CommandService', function () { lastEvent = activationEvent; return super.activateByEvent(activationEvent); } - }, new ContextKeyService(new SimpleConfigurationService())); + }, new ContextKeyService(new SimpleConfigurationService()), new NoopLogService()); return service.executeCommand('foo').then(() => { assert.ok(lastEvent, 'onCommand:foo'); @@ -92,7 +93,7 @@ suite('CommandService', function () { activateByEvent(activationEvent: string): TPromise { return TPromise.wrapError(new Error('bad_activate')); } - }, new ContextKeyService(new SimpleConfigurationService())); + }, new ContextKeyService(new SimpleConfigurationService()), new NoopLogService()); return service.executeCommand('foo').then(() => assert.ok(false), err => { assert.equal(err.message, 'bad_activate'); @@ -108,7 +109,7 @@ suite('CommandService', function () { whenInstalledExtensionsRegistered() { return new TPromise(_resolve => { /*ignore*/ }); } - }, new ContextKeyService(new SimpleConfigurationService())); + }, new ContextKeyService(new SimpleConfigurationService()), new NoopLogService()); service.executeCommand('bar'); assert.equal(callCounter, 1); @@ -125,7 +126,7 @@ suite('CommandService', function () { whenInstalledExtensionsRegistered() { return new TPromise(_resolve => { resolveFunc = _resolve; }); } - }, new ContextKeyService(new SimpleConfigurationService())); + }, new ContextKeyService(new SimpleConfigurationService()), new NoopLogService()); let r = service.executeCommand('bar'); assert.equal(callCounter, 0); @@ -144,7 +145,8 @@ suite('CommandService', function () { let commandService = new CommandService( new InstantiationService(), new SimpleExtensionService(), - contextKeyService + contextKeyService, + new NoopLogService() ); let counter = 0; diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index e32a25f3362..ef5353ab958 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -48,6 +48,7 @@ export interface ParsedArgs { 'disable-updates'?: string; 'disable-crash-reporter'?: string; 'skip-add-to-recently-opened'?: boolean; + 'ps'?: boolean; } export const IEnvironmentService = createDecorator('environmentService'); diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 2a961e11f27..1f8755477f5 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -52,7 +52,8 @@ const options: minimist.Opts = { 'disable-telemetry', 'disable-updates', 'disable-crash-reporter', - 'skip-add-to-recently-opened' + 'skip-add-to-recently-opened', + 'ps' ], alias: { add: 'a', @@ -146,6 +147,7 @@ export const optionsHelp: { [name: string]: string; } = { '--enable-proposed-api ': localize('experimentalApis', "Enables proposed api features for an extension."), '--disable-extensions': localize('disableExtensions', "Disable all installed extensions."), '--disable-gpu': localize('disableGPU', "Disable GPU hardware acceleration."), + '--ps': localize('ps', "Print process usage and diagnostics information."), '-v, --version': localize('version', "Print version."), '-h, --help': localize('help', "Print usage.") }; diff --git a/src/vs/platform/history/electron-main/historyMainService.ts b/src/vs/platform/history/electron-main/historyMainService.ts index 11520755190..7c278100bed 100644 --- a/src/vs/platform/history/electron-main/historyMainService.ts +++ b/src/vs/platform/history/electron-main/historyMainService.ts @@ -281,7 +281,7 @@ export class HistoryMainService implements IHistoryMainService { try { app.setJumpList(jumpList); } catch (error) { - this.logService.log('#setJumpList', error); // since setJumpList is relatively new API, make sure to guard for errors + this.logService.info('#setJumpList', error); // since setJumpList is relatively new API, make sure to guard for errors } } } \ No newline at end of file diff --git a/src/vs/platform/lifecycle/electron-main/lifecycleMain.ts b/src/vs/platform/lifecycle/electron-main/lifecycleMain.ts index 2f989fe0214..e6688612075 100644 --- a/src/vs/platform/lifecycle/electron-main/lifecycleMain.ts +++ b/src/vs/platform/lifecycle/electron-main/lifecycleMain.ts @@ -124,7 +124,7 @@ export class LifecycleService implements ILifecycleService { // before-quit app.on('before-quit', (e) => { - this.logService.log('Lifecycle#before-quit'); + this.logService.info('Lifecycle#before-quit'); if (!this.quitRequested) { this._onBeforeQuit.fire(); // only send this if this is the first quit request we have @@ -135,7 +135,7 @@ export class LifecycleService implements ILifecycleService { // window-all-closed app.on('window-all-closed', () => { - this.logService.log('Lifecycle#window-all-closed'); + this.logService.info('Lifecycle#window-all-closed'); // Windows/Linux: we quit when all windows have closed // Mac: we only quit when quit was requested @@ -150,11 +150,11 @@ export class LifecycleService implements ILifecycleService { // Window Before Closing: Main -> Renderer window.win.on('close', e => { const windowId = window.id; - this.logService.log('Lifecycle#window-before-close', windowId); + this.logService.info('Lifecycle#window-before-close', windowId); // The window already acknowledged to be closed if (this.windowToCloseRequest[windowId]) { - this.logService.log('Lifecycle#window-close', windowId); + this.logService.info('Lifecycle#window-close', windowId); delete this.windowToCloseRequest[windowId]; @@ -183,7 +183,7 @@ export class LifecycleService implements ILifecycleService { return TPromise.as(false); } - this.logService.log('Lifecycle#unload()', window.id); + this.logService.info('Lifecycle#unload()', window.id); const windowUnloadReason = this.quitRequested ? UnloadReason.QUIT : reason; @@ -247,7 +247,7 @@ export class LifecycleService implements ILifecycleService { * by the user or not. */ public quit(fromUpdate?: boolean): TPromise { - this.logService.log('Lifecycle#quit()'); + this.logService.info('Lifecycle#quit()'); if (!this.pendingQuitPromise) { this.pendingQuitPromise = new TPromise(c => { diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index 6afae3b00f4..b28316f0b5b 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -7,13 +7,15 @@ import { ITree, ITreeConfiguration, ITreeOptions } from 'vs/base/parts/tree/browser/tree'; import { List, IListOptions } from 'vs/base/browser/ui/list/listWidget'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IDisposable, toDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, toDisposable, combinedDisposable, dispose } from 'vs/base/common/lifecycle'; import { IContextKeyService, IContextKey, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { PagedList, IPagedRenderer } from 'vs/base/browser/ui/list/listPaging'; import { IDelegate, IRenderer } from 'vs/base/browser/ui/list/list'; import { Tree } from 'vs/base/parts/tree/browser/treeImpl'; import { attachListStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { debounce } from 'vs/base/common/decorators'; +import Event, { Emitter } from 'vs/base/common/event'; export type ListWidget = List | PagedList | ITree; @@ -141,8 +143,12 @@ export class WorkbenchPagedList extends PagedList { export class WorkbenchTree extends Tree { + private _onFocusChange = new Emitter(); + readonly onFocusChange: Event = this._onFocusChange.event; + readonly contextKeyService: IContextKeyService; - private disposable: IDisposable; + private workbenchListFocusContextKey: IContextKey; + private disposables: IDisposable[] = []; constructor( container: HTMLElement, @@ -153,16 +159,30 @@ export class WorkbenchTree extends Tree { @IThemeService themeService: IThemeService ) { super(container, configuration, options); - this.contextKeyService = createScopedContextKeyService(contextKeyService, this); - this.disposable = combinedDisposable([ + this.contextKeyService = contextKeyService.createScoped(this.getHTMLElement()); + this.workbenchListFocusContextKey = WorkbenchListFocusContextKey.bindTo(this.contextKeyService); + + this.disposables.push( this.contextKeyService, (listService as ListService).register(this), attachListStyler(this, themeService) - ]); + ); + + this.onDidFocus(this.updateContextKey, this, this.disposables); + this.onDidBlur(this.updateContextKey, this, this.disposables); + this.onDidChangeHighlight(this.updateContextKey, this, this.disposables); + } + + @debounce(50) + private updateContextKey(): void { + const isFocused = document.activeElement === this.getHTMLElement(); + + this.workbenchListFocusContextKey.set(isFocused); + this._onFocusChange.fire(isFocused); } dispose(): void { - this.disposable.dispose(); + this.disposables = dispose(this.disposables); } } diff --git a/src/vs/platform/log/common/log.ts b/src/vs/platform/log/common/log.ts index 6860e33cf4e..7b12d9563e7 100644 --- a/src/vs/platform/log/common/log.ts +++ b/src/vs/platform/log/common/log.ts @@ -5,37 +5,99 @@ 'use strict'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { createDecorator as createServiceDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { createDecorator } from 'vs/base/common/decorators'; -export const ILogService = createDecorator('logService'); +export const ILogService = createServiceDecorator('logService'); + +export enum LogLevel { + TRACE, + DEBUG, + INFO, + WARN, + ERROR, + CRITICAL +} export interface ILogService { _serviceBrand: any; - log(...args: any[]): void; - warn(...args: any[]): void; - error(...args: any[]): void; + trace(message: string, ...args: any[]): void; + debug(message: string, ...args: any[]): void; + info(message: string, ...args: any[]): void; + warn(message: string, ...args: any[]): void; + error(message: string | Error, ...args: any[]): void; + critical(message: string | Error, ...args: any[]): void; } -export class LogMainService implements ILogService { +export class LegacyLogMainService implements ILogService { _serviceBrand: any; - constructor( @IEnvironmentService private environmentService: IEnvironmentService) { + constructor( + processName: string, + @IEnvironmentService private environmentService: IEnvironmentService + ) { } + + trace(message: string, ...args: any[]): void { + // console.log(`\x1b[90m[main ${new Date().toLocaleTimeString()}]\x1b[0m`, ...args); } - public log(...args: any[]): void { + debug(message: string, ...args: any[]): void { + // console.log(`\x1b[90m[main ${new Date().toLocaleTimeString()}]\x1b[0m`, ...args); + } + + info(message: string, ...args: any[]): void { if (this.environmentService.verbose) { console.log(`\x1b[90m[main ${new Date().toLocaleTimeString()}]\x1b[0m`, ...args); } } - public error(...args: any[]): void { + error(message: string, ...args: any[]): void { console.error(`\x1b[91m[main ${new Date().toLocaleTimeString()}]\x1b[0m`, ...args); } - public warn(...args: any[]): void { + warn(message: string | Error, ...args: any[]): void { console.warn(`\x1b[93m[main ${new Date().toLocaleTimeString()}]\x1b[0m`, ...args); } + + critical(message: string, ...args: any[]): void { + // console.log(`\x1b[90m[main ${new Date().toLocaleTimeString()}]\x1b[0m`, ...args); + } +} + +export function log(level: LogLevel, prefix: string, logFn?: (message: string, ...args: any[]) => string): Function { + return createDecorator((fn, key) => { + // TODO@Joao: load-time log level? return fn; + + return function (this: any, ...args: any[]) { + let message = `${prefix} - ${key}`; + + if (logFn) { + message = logFn(message, ...args); + } + + switch (level) { + case LogLevel.TRACE: this.logService.trace(message); break; + case LogLevel.DEBUG: this.logService.debug(message); break; + case LogLevel.INFO: this.logService.info(message); break; + case LogLevel.WARN: this.logService.warn(message); break; + case LogLevel.ERROR: this.logService.error(message); break; + case LogLevel.CRITICAL: this.logService.critical(message); break; + } + + return fn.apply(this, args); + }; + }); +} + +export class NoopLogService implements ILogService { + _serviceBrand: any; + trace(message: string, ...args: any[]): void { } + debug(message: string, ...args: any[]): void { } + info(message: string, ...args: any[]): void { } + warn(message: string, ...args: any[]): void { } + error(message: string | Error, ...args: any[]): void { } + critical(message: string | Error, ...args: any[]): void { } } \ No newline at end of file diff --git a/src/vs/platform/log/node/spdlogService.ts b/src/vs/platform/log/node/spdlogService.ts new file mode 100644 index 00000000000..5fdb306f82f --- /dev/null +++ b/src/vs/platform/log/node/spdlogService.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { ILogService } from 'vs/platform/log/common/log'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; + +export class SpdLogService implements ILogService { + + _serviceBrand: any; + + constructor( + processName: string, + @IEnvironmentService environmentService: IEnvironmentService + ) { + // TODO create logger + } + + trace(message: string, ...args: any[]): void { + // console.log('TRACE', message, ...args); + } + + debug(message: string, ...args: any[]): void { + // console.log('DEBUG', message, ...args); + } + + info(message: string, ...args: any[]): void { + // console.log('INFO', message, ...args); + } + + warn(message: string, ...args: any[]): void { + // console.warn('WARN', message, ...args); + } + + error(message: string | Error, ...args: any[]): void { + // console.error('ERROR', message, ...args); + } + + critical(message: string, ...args: any[]): void { + // console.error('CRITICAL', message, ...args); + } +} \ No newline at end of file diff --git a/src/vs/platform/update/electron-main/updateService.ts b/src/vs/platform/update/electron-main/updateService.ts index cc86289ccd5..2d5e9e17c78 100644 --- a/src/vs/platform/update/electron-main/updateService.ts +++ b/src/vs/platform/update/electron-main/updateService.ts @@ -270,10 +270,10 @@ export class UpdateService implements IUpdateService { return TPromise.as(null); } - this.logService.log('update#quitAndInstall(): before lifecycle quit()'); + this.logService.info('update#quitAndInstall(): before lifecycle quit()'); this.lifecycleService.quit(true /* from update */).done(vetod => { - this.logService.log(`update#quitAndInstall(): after lifecycle quit() with veto: ${vetod}`); + this.logService.info(`update#quitAndInstall(): after lifecycle quit() with veto: ${vetod}`); if (vetod) { return; } @@ -282,11 +282,11 @@ export class UpdateService implements IUpdateService { // we workaround this issue by forcing an explicit flush of the storage data. // see also https://github.com/Microsoft/vscode/issues/172 if (process.platform === 'darwin') { - this.logService.log('update#quitAndInstall(): calling flushStorageData()'); + this.logService.info('update#quitAndInstall(): calling flushStorageData()'); electron.session.defaultSession.flushStorageData(); } - this.logService.log('update#quitAndInstall(): running raw#quitAndInstall()'); + this.logService.info('update#quitAndInstall(): running raw#quitAndInstall()'); this.raw.quitAndInstall(); }); diff --git a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts index c550fe7b46e..1d1823c0ccd 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts @@ -86,7 +86,7 @@ export class WorkspacesMainService implements IWorkspacesMainService { folders: toWorkspaceFolders(workspace.folders, URI.file(dirname(path))) }; } catch (error) { - this.logService.log(error.toString()); + this.logService.info(error.toString()); } return null; @@ -262,7 +262,7 @@ export class WorkspacesMainService implements IWorkspacesMainService { try { delSync(dirname(configPath)); } catch (error) { - this.logService.log(`Unable to delete untitled workspace ${configPath} (${error}).`); + this.logService.info(`Unable to delete untitled workspace ${configPath} (${error}).`); } } @@ -271,7 +271,7 @@ export class WorkspacesMainService implements IWorkspacesMainService { try { untitledWorkspacePaths = readdirSync(this.workspacesHome).map(folder => join(this.workspacesHome, folder, UNTITLED_WORKSPACE_NAME)); } catch (error) { - this.logService.log(`Unable to read folders in ${this.workspacesHome} (${error}).`); + this.logService.info(`Unable to read folders in ${this.workspacesHome} (${error}).`); } const untitledWorkspaces: IWorkspaceIdentifier[] = coalesce(untitledWorkspacePaths.map(untitledWorkspacePath => { diff --git a/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts b/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts index f112ef8f9eb..dfc6044522c 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts @@ -15,7 +15,7 @@ import { EnvironmentService } from 'vs/platform/environment/node/environmentServ import { parseArgs } from 'vs/platform/environment/node/argv'; import { WorkspacesMainService, IStoredWorkspace } from 'vs/platform/workspaces/electron-main/workspacesMainService'; import { WORKSPACE_EXTENSION, IWorkspaceSavedEvent, IWorkspaceIdentifier, IRawFileWorkspaceFolder, IWorkspaceFolderCreationData, IRawUriWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; -import { LogMainService } from 'vs/platform/log/common/log'; +import { LegacyLogMainService } from 'vs/platform/log/common/log'; import URI from 'vs/base/common/uri'; import { getRandomTestPath } from 'vs/workbench/test/workbenchTestServices'; @@ -48,7 +48,7 @@ suite('WorkspacesMainService', () => { } const environmentService = new TestEnvironmentService(parseArgs(process.argv), process.execPath); - const logService = new LogMainService(environmentService); + const logService = new LegacyLogMainService('test', environmentService); let service: TestWorkspacesMainService; diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index d95b2c764ca..7790f17773d 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -1714,13 +1714,13 @@ declare module 'vscode' { /** * A file glob pattern to match file paths against. This can either be a glob pattern string - * (like `**\*.{ts,js}` or `*.{ts,js}`) or a [relative pattern](#RelativePattern). + * (like `**​/*.{ts,js}` or `*.{ts,js}`) or a [relative pattern](#RelativePattern). * * Glob patterns can have the following syntax: * * `*` to match one or more characters in a path segment * * `?` to match on one character in a path segment * * `**` to match any number of path segments, including none - * * `{}` to group conditions (e.g. `**\*.{ts,js}` matches all TypeScript and JavaScript files) + * * `{}` to group conditions (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files) * * `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) * * `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`) */ @@ -1732,7 +1732,7 @@ declare module 'vscode' { * its resource, or a glob-pattern that is applied to the [path](#TextDocument.fileName). * * @sample A language filter that applies to typescript files on disk: `{ language: 'typescript', scheme: 'file' }` - * @sample A language filter that applies to all package.json paths: `{ language: 'json', pattern: '**\package.json' }` + * @sample A language filter that applies to all package.json paths: `{ language: 'json', pattern: '**​/package.json' }` */ export interface DocumentFilter { @@ -1758,7 +1758,7 @@ declare module 'vscode' { * and [language filters](#DocumentFilter). * * @sample `let sel:DocumentSelector = 'typescript'`; - * @sample `let sel:DocumentSelector = ['typescript', { language: 'json', pattern: '**\tsconfig.json' }]`; + * @sample `let sel:DocumentSelector = ['typescript', { language: 'json', pattern: '**​/tsconfig.json' }]`; */ export type DocumentSelector = string | DocumentFilter | (string | DocumentFilter)[]; @@ -5260,7 +5260,7 @@ declare module 'vscode' { /** * Find files across all [workspace folders](#workspace.workspaceFolders) in the workspace. * - * @sample `findFiles('**\*.js', '**\node_modules\**', 10)` + * @sample `findFiles('**​/*.js', '**​/node_modules/**', 10)` * @param include A [glob pattern](#GlobPattern) that defines the files to search for. The glob pattern * will be matched against the file paths of resulting matches relative to their workspace. Use a [relative pattern](#RelativePattern) * to restrict the search results to a [workspace folder](#WorkspaceFolder). diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index f9d35e7f3eb..2fbe3f0ec19 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -57,6 +57,7 @@ import { FileChangeType, FileType } from 'vs/platform/files/common/files'; import { ExtHostDecorations } from 'vs/workbench/api/node/extHostDecorations'; import { toGlobPattern, toLanguageSelector } from 'vs/workbench/api/node/extHostTypeConverters'; import { ExtensionActivatedByAPI } from 'vs/workbench/api/node/extHostExtensionActivator'; +import { ILogService } from 'vs/platform/log/common/log'; export interface IExtensionApiFactory { (extension: IExtensionDescription): typeof vscode; @@ -80,7 +81,8 @@ export function createApiFactory( threadService: ExtHostThreadService, extHostWorkspace: ExtHostWorkspace, extHostConfiguration: ExtHostConfiguration, - extensionService: ExtHostExtensionService + extensionService: ExtHostExtensionService, + logService: ILogService ): IExtensionApiFactory { // Addressable instances @@ -91,7 +93,7 @@ export function createApiFactory( const extHostDocumentContentProviders = threadService.set(ExtHostContext.ExtHostDocumentContentProviders, new ExtHostDocumentContentProvider(threadService, extHostDocumentsAndEditors)); const extHostDocumentSaveParticipant = threadService.set(ExtHostContext.ExtHostDocumentSaveParticipant, new ExtHostDocumentSaveParticipant(extHostDocuments, threadService.get(MainContext.MainThreadEditors))); const extHostEditors = threadService.set(ExtHostContext.ExtHostEditors, new ExtHostEditors(threadService, extHostDocumentsAndEditors)); - const extHostCommands = threadService.set(ExtHostContext.ExtHostCommands, new ExtHostCommands(threadService, extHostHeapService)); + const extHostCommands = threadService.set(ExtHostContext.ExtHostCommands, new ExtHostCommands(threadService, extHostHeapService, logService)); const extHostTreeViews = threadService.set(ExtHostContext.ExtHostTreeViews, new ExtHostTreeViews(threadService.get(MainContext.MainThreadTreeViews), extHostCommands)); threadService.set(ExtHostContext.ExtHostWorkspace, extHostWorkspace); const extHostDebugService = threadService.set(ExtHostContext.ExtHostDebugService, new ExtHostDebugService(threadService, extHostWorkspace)); @@ -102,7 +104,7 @@ export function createApiFactory( const extHostFileSystemEvent = threadService.set(ExtHostContext.ExtHostFileSystemEventService, new ExtHostFileSystemEventService()); const extHostQuickOpen = threadService.set(ExtHostContext.ExtHostQuickOpen, new ExtHostQuickOpen(threadService, extHostWorkspace, extHostCommands)); const extHostTerminalService = threadService.set(ExtHostContext.ExtHostTerminalService, new ExtHostTerminalService(threadService)); - const extHostSCM = threadService.set(ExtHostContext.ExtHostSCM, new ExtHostSCM(threadService, extHostCommands)); + const extHostSCM = threadService.set(ExtHostContext.ExtHostSCM, new ExtHostSCM(threadService, extHostCommands, logService)); const extHostTask = threadService.set(ExtHostContext.ExtHostTask, new ExtHostTask(threadService, extHostWorkspace)); const extHostWindow = threadService.set(ExtHostContext.ExtHostWindow, new ExtHostWindow(threadService)); threadService.set(ExtHostContext.ExtHostExtensionService, extensionService); diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 81db78184f5..0e352960a6c 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -49,6 +49,7 @@ import { SerializedError } from 'vs/base/common/errors'; import { IWorkspaceFolderData } from 'vs/platform/workspace/common/workspace'; import { IStat, IFileChange } from 'vs/platform/files/common/files'; import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; +import { ParsedArgs } from 'vs/platform/environment/common/environment'; export interface IEnvironment { isExtensionDevelopmentDebug: boolean; @@ -75,6 +76,8 @@ export interface IInitData { extensions: IExtensionDescription[]; configuration: IConfigurationInitData; telemetryInfo: ITelemetryInfo; + args: ParsedArgs; + execPath: string; } export interface IConfigurationInitData extends IConfigurationData { diff --git a/src/vs/workbench/api/node/extHostCommands.ts b/src/vs/workbench/api/node/extHostCommands.ts index 774d71f8e26..abeaf711612 100644 --- a/src/vs/workbench/api/node/extHostCommands.ts +++ b/src/vs/workbench/api/node/extHostCommands.ts @@ -15,6 +15,7 @@ import { ExtHostHeapService } from 'vs/workbench/api/node/extHostHeapService'; import { isFalsyOrEmpty } from 'vs/base/common/arrays'; import * as modes from 'vs/editor/common/modes'; import * as vscode from 'vscode'; +import { ILogService, log, LogLevel } from 'vs/platform/log/common/log'; interface CommandHandler { callback: Function; @@ -35,7 +36,9 @@ export class ExtHostCommands implements ExtHostCommandsShape { constructor( mainContext: IMainContext, - heapService: ExtHostHeapService + heapService: ExtHostHeapService, + // @ts-ignore + @ILogService private logService: ILogService ) { this._proxy = mainContext.get(MainContext.MainThreadCommands); this._converter = new CommandsConverter(this, heapService); @@ -49,6 +52,7 @@ export class ExtHostCommands implements ExtHostCommandsShape { this._argumentProcessors.push(processor); } + @log(LogLevel.TRACE, 'ExtHostCommands', (msg, id) => `${msg}(${id})`) registerCommand(id: string, callback: (...args: any[]) => T | Thenable, thisArg?: any, description?: ICommandHandlerDescription): extHostTypes.Disposable { if (!id.trim().length) { @@ -69,6 +73,7 @@ export class ExtHostCommands implements ExtHostCommandsShape { }); } + @log(LogLevel.TRACE, 'ExtHostCommands', (msg, id) => `${msg}(${id})`) executeCommand(id: string, ...args: any[]): Thenable { if (this._commands.has(id)) { @@ -133,6 +138,7 @@ export class ExtHostCommands implements ExtHostCommandsShape { } } + @log(LogLevel.TRACE, 'ExtHostCommands', (msg, filterUnderscoreCommands) => `${msg}(${filterUnderscoreCommands})`) getCommands(filterUnderscoreCommands: boolean = false): Thenable { return this._proxy.$getCommands().then(result => { if (filterUnderscoreCommands) { diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index 0d5d3c3207f..c6d96bf299c 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -21,6 +21,7 @@ import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace'; import { realpath } from 'fs'; import { TernarySearchTree } from 'vs/base/common/map'; import { Barrier } from 'vs/base/common/async'; +import { ILogService } from 'vs/platform/log/common/log'; class ExtensionMemento implements IExtensionMemento { @@ -125,7 +126,8 @@ export class ExtHostExtensionService implements ExtHostExtensionServiceShape { constructor(initData: IInitData, threadService: ExtHostThreadService, extHostWorkspace: ExtHostWorkspace, - extHostConfiguration: ExtHostConfiguration + extHostConfiguration: ExtHostConfiguration, + logService: ILogService ) { this._barrier = new Barrier(); this._registry = new ExtensionDescriptionRegistry(initData.extensions); @@ -137,7 +139,7 @@ export class ExtHostExtensionService implements ExtHostExtensionServiceShape { this._activator = null; // initialize API first (i.e. do not release barrier until the API is initialized) - const apiFactory = createApiFactory(initData, threadService, extHostWorkspace, extHostConfiguration, this); + const apiFactory = createApiFactory(initData, threadService, extHostWorkspace, extHostConfiguration, this, logService); initializeExtensionApi(this, apiFactory).then(() => { diff --git a/src/vs/workbench/api/node/extHostSCM.ts b/src/vs/workbench/api/node/extHostSCM.ts index 9c3637a8c4c..5159a2e37df 100644 --- a/src/vs/workbench/api/node/extHostSCM.ts +++ b/src/vs/workbench/api/node/extHostSCM.ts @@ -17,6 +17,7 @@ import { sortedDiff } from 'vs/base/common/arrays'; import { comparePaths } from 'vs/base/common/comparers'; import * as vscode from 'vscode'; import { ISplice } from 'vs/base/common/sequence'; +import { log, LogLevel, ILogService } from 'vs/platform/log/common/log'; type ProviderHandle = number; type GroupHandle = number; @@ -443,7 +444,9 @@ export class ExtHostSCM { constructor( mainContext: IMainContext, - private _commands: ExtHostCommands + private _commands: ExtHostCommands, + // @ts-ignore + @ILogService private logService: ILogService ) { this._proxy = mainContext.get(MainContext.MainThreadSCM); @@ -486,6 +489,7 @@ export class ExtHostSCM { }); } + @log(LogLevel.TRACE, 'ExtHostSCM', (msg, extension, id, label, rootUri) => `${msg}(${extension.id}, ${id}, ${label}, ${rootUri})`) createSourceControl(extension: IExtensionDescription, id: string, label: string, rootUri: vscode.Uri | undefined): vscode.SourceControl { const handle = ExtHostSCM._handlePool++; const sourceControl = new ExtHostSourceControl(this._proxy, this._commands, id, label, rootUri); @@ -499,6 +503,7 @@ export class ExtHostSCM { } // Deprecated + @log(LogLevel.TRACE, 'ExtHostSCM', (msg, extension) => `${msg}(${extension.id})`) getLastInputBox(extension: IExtensionDescription): ExtHostSCMInputBox { const sourceControls = this._sourceControlsByExtension.get(extension.id); const sourceControl = sourceControls && sourceControls[sourceControls.length - 1]; @@ -507,6 +512,7 @@ export class ExtHostSCM { return inputBox; } + @log(LogLevel.TRACE, 'ExtHostSCM', (msg, handle, uri) => `${msg}(${handle}, ${uri})`) $provideOriginalResource(sourceControlHandle: number, uri: URI): TPromise { const sourceControl = this._sourceControls.get(sourceControlHandle); @@ -520,6 +526,7 @@ export class ExtHostSCM { }); } + @log(LogLevel.TRACE, 'ExtHostSCM', (msg, handle) => `${msg}(${handle})`) $onInputBoxValueChange(sourceControlHandle: number, value: string): TPromise { const sourceControl = this._sourceControls.get(sourceControlHandle); @@ -531,6 +538,7 @@ export class ExtHostSCM { return TPromise.as(null); } + @log(LogLevel.TRACE, 'ExtHostSCM', (msg, h1, h2, h3) => `${msg}(${h1}, ${h2}, ${h3})`) async $executeResourceCommand(sourceControlHandle: number, groupHandle: number, handle: number): TPromise { const sourceControl = this._sourceControls.get(sourceControlHandle); diff --git a/src/vs/workbench/browser/parts/editor/media/tabstitle.css b/src/vs/workbench/browser/parts/editor/media/tabstitle.css index e10f3451c3f..75fc44ad7a2 100644 --- a/src/vs/workbench/browser/parts/editor/media/tabstitle.css +++ b/src/vs/workbench/browser/parts/editor/media/tabstitle.css @@ -51,14 +51,14 @@ } .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.sizing-shrink { - min-width: 80px; + min-width: 60px; flex-basis: 0; /* all tabs are even */ flex-grow: 1; /* all tabs grow even */ max-width: fit-content; } -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.sizing-shrink.tab.close-button-off { - min-width: 60px; +.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.sizing-shrink.close-button-left { + min-width: 80px; /* make more room for close button when it shows to the left */ } .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dragged { @@ -99,6 +99,15 @@ width: 28px; } +.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.close-button-right.sizing-shrink > .tab-close { + flex: 0; + overflow: hidden; /* let the close button be pushed out of view when sizing is set to shrink to make more room... */ +} + +.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.close-button-right.sizing-shrink:hover > .tab-close { + overflow: visible; /* ...but still show the close button on hover */ +} + .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.close-button-off > .tab-close { display: none; /* hide the close action bar when we are configured to hide it */ } @@ -150,8 +159,11 @@ /* No Tab Close Button */ .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.close-button-off { - padding-right: 12px; - transition: padding-right ease-in-out 100ms; + padding-right: 12px; /* give a little bit more room if close button is off... */ +} + +.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.close-button-off.sizing-shrink { + padding-right: 0; /* ...but not when shrinking is enabled */ } .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.close-button-off.dirty { diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index fecab9d2d85..e7a1c82fbe3 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -314,7 +314,7 @@ export class TabsTitleControl extends TitleControl { const tabOptions = this.editorGroupService.getTabOptions(); - ['off', 'left'].forEach(option => { + ['off', 'left', 'right'].forEach(option => { const domAction = tabOptions.tabCloseButton === option ? DOM.addClass : DOM.removeClass; domAction(tabContainer, `close-button-${option}`); }); diff --git a/src/vs/workbench/electron-browser/actions.ts b/src/vs/workbench/electron-browser/actions.ts index db7264d1255..d49a9374bff 100644 --- a/src/vs/workbench/electron-browser/actions.ts +++ b/src/vs/workbench/electron-browser/actions.ts @@ -885,7 +885,7 @@ export class CloseMessagesAction extends Action { export class ReportIssueAction extends Action { public static readonly ID = 'workbench.action.reportIssues'; - public static readonly LABEL = nls.localize('reportIssues', "Report Issues"); + public static readonly LABEL = nls.localize({ key: 'reportIssueInEnglish', comment: ['Translate this to "Report Issue in English" in all languages please!'] }, "Report Issue"); constructor( id: string, diff --git a/src/vs/workbench/electron-browser/main.ts b/src/vs/workbench/electron-browser/main.ts index 577105b0535..047788587f2 100644 --- a/src/vs/workbench/electron-browser/main.ts +++ b/src/vs/workbench/electron-browser/main.ts @@ -41,6 +41,7 @@ import { WorkspacesChannelClient } from 'vs/platform/workspaces/common/workspace import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; import fs = require('fs'); +import { SpdLogService } from 'vs/platform/log/node/spdlogService'; gracefulFs.gracefulify(fs); // enable gracefulFs const currentWindowId = remote.getCurrentWindow().id; @@ -72,6 +73,8 @@ function openWorkbench(configuration: IWindowConfiguration): TPromise { const mainServices = createMainProcessServices(mainProcessClient); const environmentService = new EnvironmentService(configuration, configuration.execPath); + const logService = new SpdLogService('renderer', environmentService); + logService.info('openWorkbench', JSON.stringify(configuration)); // Since the configuration service is one of the core services that is used in so many places, we initialize it // right before startup of the workbench shell to have its data ready for consumers @@ -90,6 +93,7 @@ function openWorkbench(configuration: IWindowConfiguration): TPromise { contextService: workspaceService, configurationService: workspaceService, environmentService, + logService, timerService, storageService }, mainServices, configuration); diff --git a/src/vs/workbench/electron-browser/shell.ts b/src/vs/workbench/electron-browser/shell.ts index 54f4b656d50..063e7965089 100644 --- a/src/vs/workbench/electron-browser/shell.ts +++ b/src/vs/workbench/electron-browser/shell.ts @@ -88,6 +88,7 @@ import { ITextMateService } from 'vs/workbench/services/textMate/electron-browse import { IBroadcastService, BroadcastService } from 'vs/platform/broadcast/electron-browser/broadcastService'; import { HashService } from 'vs/workbench/services/hash/node/hashService'; import { IHashService } from 'vs/workbench/services/hash/common/hashService'; +import { ILogService } from 'vs/platform/log/common/log'; /** * Services that we require for the Shell @@ -96,6 +97,7 @@ export interface ICoreServices { contextService: IWorkspaceContextService; configurationService: IConfigurationService; environmentService: IEnvironmentService; + logService: ILogService; timerService: ITimerService; storageService: IStorageService; } @@ -110,6 +112,7 @@ export class WorkbenchShell { private storageService: IStorageService; private messageService: MessageService; private environmentService: IEnvironmentService; + private logService: ILogService; private contextViewService: ContextViewService; private configurationService: IConfigurationService; private contextService: IWorkspaceContextService; @@ -140,6 +143,7 @@ export class WorkbenchShell { this.contextService = coreServices.contextService; this.configurationService = coreServices.configurationService; this.environmentService = coreServices.environmentService; + this.logService = coreServices.logService; this.timerService = coreServices.timerService; this.storageService = coreServices.storageService; @@ -279,6 +283,7 @@ export class WorkbenchShell { serviceCollection.set(IWorkspaceContextService, this.contextService); serviceCollection.set(IConfigurationService, this.configurationService); serviceCollection.set(IEnvironmentService, this.environmentService); + serviceCollection.set(ILogService, this.logService); serviceCollection.set(ITimerService, this.timerService); serviceCollection.set(IStorageService, this.storageService); this.mainProcessServices.forEach((serviceIdentifier, serviceInstance) => { diff --git a/src/vs/workbench/electron-browser/workbench.ts b/src/vs/workbench/electron-browser/workbench.ts index 787f63d147a..ac666a905aa 100644 --- a/src/vs/workbench/electron-browser/workbench.ts +++ b/src/vs/workbench/electron-browser/workbench.ts @@ -135,6 +135,14 @@ const Identifiers = { STATUSBAR_PART: 'workbench.parts.statusbar' }; +function getWorkbenchStateString(state: WorkbenchState): string { + switch (state) { + case WorkbenchState.EMPTY: return 'empty'; + case WorkbenchState.FOLDER: return 'folder'; + case WorkbenchState.WORKSPACE: return 'workspace'; + } +} + /** * The workbench creates and lays out all parts that make up the workbench. */ @@ -269,6 +277,11 @@ export class Workbench implements IPartService { this.inZenMode = InZenModeContext.bindTo(this.contextKeyService); this.sideBarVisibleContext = SidebarVisibleContext.bindTo(this.contextKeyService); + // Set workbench state context + const WorkbenchStateContext = new RawContextKey('workbenchState', getWorkbenchStateString(this.configurationService.getWorkbenchState())); + const workbenchStateContext = WorkbenchStateContext.bindTo(this.contextKeyService); + this.toDispose.push(this.configurationService.onDidChangeWorkbenchState(() => workbenchStateContext.set(getWorkbenchStateString(this.configurationService.getWorkbenchState())))); + // Register Listeners this.registerListeners(); diff --git a/src/vs/workbench/node/extensionHostMain.ts b/src/vs/workbench/node/extensionHostMain.ts index 551e81121ed..25f3c221cb5 100644 --- a/src/vs/workbench/node/extensionHostMain.ts +++ b/src/vs/workbench/node/extensionHostMain.ts @@ -22,9 +22,11 @@ import * as errors from 'vs/base/common/errors'; import * as watchdog from 'native-watchdog'; import * as glob from 'vs/base/common/glob'; import { ExtensionActivatedByEvent } from 'vs/workbench/api/node/extHostExtensionActivator'; +import { EnvironmentService } from 'vs/platform/environment/node/environmentService'; +import { SpdLogService } from 'vs/platform/log/node/spdlogService'; // const nativeExit = process.exit.bind(process); -function patchExit(allowExit: boolean) { +function patchProcess(allowExit: boolean) { process.exit = function (code) { if (allowExit) { exit(code); @@ -33,6 +35,11 @@ function patchExit(allowExit: boolean) { console.warn(err.stack); } }; + + process.crash = function () { + const err = new Error('An extension called process.crash() and this was prevented.'); + console.warn(err.stack); + }; } export function exit(code?: number) { //nativeExit(code); @@ -74,13 +81,16 @@ export class ExtensionHostMain { this._workspace = initData.workspace; const allowExit = !!this._environment.extensionTestsPath; // to support other test frameworks like Jasmin that use process.exit (https://github.com/Microsoft/vscode/issues/37708) - patchExit(allowExit); + patchProcess(allowExit); // services const threadService = new ExtHostThreadService(rpcProtocol); const extHostWorkspace = new ExtHostWorkspace(threadService, initData.workspace); + const environmentService = new EnvironmentService(initData.args, initData.execPath); + const logService = new SpdLogService('exthost', environmentService); + this._extHostConfiguration = new ExtHostConfiguration(threadService.get(MainContext.MainThreadConfiguration), extHostWorkspace, initData.configuration); - this._extensionService = new ExtHostExtensionService(initData, threadService, extHostWorkspace, this._extHostConfiguration); + this._extensionService = new ExtHostExtensionService(initData, threadService, extHostWorkspace, this._extHostConfiguration, logService); // error forwarding and stack trace scanning const extensionErrors = new WeakMap(); diff --git a/src/vs/workbench/parts/debug/electron-browser/breakpointsView.ts b/src/vs/workbench/parts/debug/electron-browser/breakpointsView.ts index 4c0b7cf9950..5d16986e205 100644 --- a/src/vs/workbench/parts/debug/electron-browser/breakpointsView.ts +++ b/src/vs/workbench/parts/debug/electron-browser/breakpointsView.ts @@ -108,7 +108,9 @@ export class BreakpointsView extends ViewsViewletPanel { } protected layoutBody(size: number): void { - this.list.layout(size); + if (this.list) { + this.list.layout(size); + } } private onListContextMenu(e: IListContextMenuEvent): void { diff --git a/src/vs/workbench/parts/extensions/browser/extensionsWidgets.ts b/src/vs/workbench/parts/extensions/browser/extensionsWidgets.ts index 12d0a66aa7a..065ad00c0df 100644 --- a/src/vs/workbench/parts/extensions/browser/extensionsWidgets.ts +++ b/src/vs/workbench/parts/extensions/browser/extensionsWidgets.ts @@ -10,6 +10,7 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IExtension, IExtensionsWorkbenchService } from '../common/extensions'; import { append, $, addClass } from 'vs/base/browser/dom'; import * as platform from 'vs/base/common/platform'; +import { localize } from 'vs/nls'; export interface IOptions { extension?: IExtension; @@ -142,6 +143,7 @@ export class RatingsWidget implements IDisposable { const count = append(this.container, $('span.count')); count.textContent = String(rating); + this.container.title = localize('ratedByUsers', "Rated by {0} users", this.extension.ratingCount); } else { for (let i = 1; i <= 5; i++) { if (rating >= i) { diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts index 3de767e604b..ec1a3d56ead 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts @@ -353,7 +353,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe this.extensionsService.getInstalled(LocalExtensionType.User).done(local => { const recommendations = allRecommendations - .filter(id => local.every(local => `${local.manifest.publisher}.${local.manifest.name}` !== id)); + .filter(id => local.every(local => `${local.manifest.publisher.toLowerCase()}.${local.manifest.name.toLowerCase()}` !== id)); if (!recommendations.length) { return; diff --git a/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts b/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts index d8a17340dbc..6d1389d9a28 100644 --- a/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts +++ b/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts @@ -424,14 +424,6 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService { } open(extension: IExtension, sideByside: boolean = false): TPromise { - /* __GDPR__ - "extensionGallery:open" : { - "${include}": [ - "${GalleryExtensionTelemetryData}" - ] - } - */ - this.telemetryService.publicLog('extensionGallery:open', extension.telemetryData); return this.editorService.openEditor(this.instantiationService.createInstance(ExtensionsInput, extension), null, sideByside); } diff --git a/src/vs/workbench/parts/files/electron-browser/explorerViewlet.ts b/src/vs/workbench/parts/files/electron-browser/explorerViewlet.ts index 619a0e36a4e..b7b9b3cd8d8 100644 --- a/src/vs/workbench/parts/files/electron-browser/explorerViewlet.ts +++ b/src/vs/workbench/parts/files/electron-browser/explorerViewlet.ts @@ -271,7 +271,7 @@ export class ExplorerViewlet extends PersistentViewsViewlet implements IExplorer return false; } - if (view instanceof ExplorerView || view instanceof OpenEditorsView) { + if (view instanceof ExplorerView) { const viewer = view.getViewer(); if (!viewer) { return false; @@ -280,6 +280,9 @@ export class ExplorerViewlet extends PersistentViewsViewlet implements IExplorer return !!viewer.getFocus() || (viewer.getSelection() && viewer.getSelection().length > 0); } + if (view instanceof OpenEditorsView && !!view.getList()) { + return view.getList().isDOMFocused(); + } return false; } diff --git a/src/vs/workbench/parts/files/electron-browser/fileActions.ts b/src/vs/workbench/parts/files/electron-browser/fileActions.ts index 5dbf38e4515..959f0644dc2 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileActions.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileActions.ts @@ -1141,7 +1141,9 @@ export class OpenToSideAction extends Action { public run(): TPromise { // Remove highlight - this.tree.clearHighlight(); + if (this.tree) { + this.tree.clearHighlight(); + } // Set side input return this.editorService.openEditor({ @@ -1713,7 +1715,7 @@ export class FocusOpenEditorsView extends Action { const openEditorsView = viewlet.getOpenEditorsView(); if (openEditorsView) { openEditorsView.setExpanded(true); - openEditorsView.getViewer().DOMFocus(); + openEditorsView.getList().domFocus(); } }); } diff --git a/src/vs/workbench/parts/files/electron-browser/fileCommands.ts b/src/vs/workbench/parts/files/electron-browser/fileCommands.ts index b2eeab111dd..40455bd3cb2 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileCommands.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileCommands.ts @@ -174,21 +174,22 @@ export function withFocusedFilesExplorer(accessor: ServicesAccessor): TPromise<{ }); } -function withFocusedOpenEditorsViewItem(accessor: ServicesAccessor): TPromise<{ explorer: ExplorerViewlet, tree: ITree, item: OpenEditor }> { +function withFocusedOpenEditorsViewItem(accessor: ServicesAccessor): TPromise<{ explorer: ExplorerViewlet, item: OpenEditor }> { return withVisibleExplorer(accessor).then(explorer => { - if (!explorer || !explorer.getOpenEditorsView()) { + if (!explorer || !explorer.getOpenEditorsView() || !explorer.getOpenEditorsView().getList()) { return void 0; // empty folder or hidden explorer } - const tree = explorer.getOpenEditorsView().getViewer(); + const list = explorer.getOpenEditorsView().getList(); // Ignore if in highlight mode or not focused - const focus = tree.getFocus(); - if (tree.getHighlight() || !tree.isDOMFocused() || !(focus instanceof OpenEditor)) { + const focused = list.getFocusedElements(); + const focus = focused.length ? focused[0] : undefined; + if (!list.isDOMFocused() || !(focus instanceof OpenEditor)) { return void 0; } - return { explorer, tree, item: focus }; + return { explorer, item: focus }; }); } diff --git a/src/vs/workbench/parts/files/electron-browser/media/explorerviewlet.css b/src/vs/workbench/parts/files/electron-browser/media/explorerviewlet.css index 07ff1c0ca4b..ef3c88021fd 100644 --- a/src/vs/workbench/parts/files/electron-browser/media/explorerviewlet.css +++ b/src/vs/workbench/parts/files/electron-browser/media/explorerviewlet.css @@ -41,11 +41,12 @@ flex: 0; /* do not steal space when label is hidden because we are in edit mode */ } -.explorer-viewlet .explorer-open-editors .monaco-tree .monaco-tree-row > .content { +.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row { + padding-left: 22px; display: flex; } -.explorer-viewlet .explorer-open-editors .monaco-tree .monaco-tree-row > .content > .monaco-action-bar { +.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row > .monaco-action-bar { visibility: hidden; } @@ -106,32 +107,32 @@ display: none; } -.explorer-viewlet .explorer-open-editors .monaco-tree .monaco-tree-row:hover > .content .monaco-action-bar, -.explorer-viewlet .explorer-open-editors .monaco-tree.focused .monaco-tree-row.focused > .content .monaco-action-bar, -.explorer-viewlet .explorer-open-editors .monaco-tree .monaco-tree-row > .content.dirty > .monaco-action-bar { +.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row:hover > .monaco-action-bar, +.explorer-viewlet .explorer-open-editors .monaco-list.focused .monaco-list-row.focused > .monaco-action-bar, +.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row.dirty > .monaco-action-bar { visibility: visible; } -.explorer-viewlet .explorer-open-editors .monaco-tree .monaco-tree-row > .content .monaco-action-bar .action-label { +.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row > .monaco-action-bar .action-label { display: block; } -.explorer-viewlet .explorer-open-editors .monaco-tree .monaco-tree-row > .content .monaco-action-bar .close-editor-action { +.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row > .monaco-action-bar .close-editor-action { width: 8px; height: 22px; } -.explorer-viewlet .explorer-open-editors .monaco-tree .monaco-tree-row > .content .monaco-action-bar .action-close-all-files, -.explorer-viewlet .explorer-open-editors .monaco-tree .monaco-tree-row > .content .monaco-action-bar .save-all { +.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row > .monaco-action-bar .action-close-all-files, +.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row > .monaco-action-bar .save-all { width: 23px; height: 22px; } -.explorer-viewlet .explorer-open-editors .monaco-tree .monaco-tree-row > .content > .open-editor { +.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row > .open-editor { flex: 1; } -.explorer-viewlet .explorer-open-editors .monaco-tree .monaco-tree-row > .content > .editor-group { +.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row > .editor-group { flex: 1; } @@ -169,7 +170,7 @@ height: 20px; } -.explorer-viewlet .explorer-open-editors .monaco-tree .monaco-tree-row .editor-group { +.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row .editor-group { font-size: 11px; font-weight: bold; text-transform: uppercase; @@ -177,10 +178,10 @@ } /* Bold font style does not go well with CJK fonts */ -.explorer-viewlet:lang(zh-Hans) .explorer-open-editors .monaco-tree .monaco-tree-row .editor-group, -.explorer-viewlet:lang(zh-Hant) .explorer-open-editors .monaco-tree .monaco-tree-row .editor-group, -.explorer-viewlet:lang(ja) .explorer-open-editors .monaco-tree .monaco-tree-row .editor-group, -.explorer-viewlet:lang(ko) .explorer-open-editors .monaco-tree .monaco-tree-row .editor-group { +.explorer-viewlet:lang(zh-Hans) .explorer-open-editors .monaco-list .monaco-list-row .editor-group, +.explorer-viewlet:lang(zh-Hant) .explorer-open-editors .monaco-list .monaco-list-row .editor-group, +.explorer-viewlet:lang(ja) .explorer-open-editors .monaco-list .monaco-list-row .editor-group, +.explorer-viewlet:lang(ko) .explorer-open-editors .monaco-list .monaco-list-row .editor-group { font-weight: normal; } diff --git a/src/vs/workbench/parts/files/electron-browser/media/fileactions.css b/src/vs/workbench/parts/files/electron-browser/media/fileactions.css index 0954b8841ce..a073640bc8f 100644 --- a/src/vs/workbench/parts/files/electron-browser/media/fileactions.css +++ b/src/vs/workbench/parts/files/electron-browser/media/fileactions.css @@ -104,20 +104,20 @@ background: url("action-close.svg") center center no-repeat; } -.explorer-viewlet .explorer-open-editors .focused .monaco-tree-row.selected:not(.highlighted) > .content .close-editor-action { +.explorer-viewlet .explorer-open-editors .focused .monaco-list-row.selected:not(.highlighted) .close-editor-action { background: url("action-close-focus.svg") center center no-repeat; } -.explorer-viewlet .explorer-open-editors .monaco-tree .monaco-tree-row:not(:hover) > .content.dirty > .monaco-action-bar .close-editor-action { +.explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row.dirty:not(:hover) > .monaco-action-bar .close-editor-action { background: url("action-close-dirty.svg") center center no-repeat; } -.vs-dark .explorer-viewlet .explorer-open-editors .monaco-tree .monaco-tree-row:not(:hover) > .content.dirty > .monaco-action-bar .close-editor-action, -.hc-black .monaco-workbench .explorer-viewlet .explorer-open-editors .monaco-tree .monaco-tree-row:not(:hover) > .content.dirty > .monaco-action-bar .close-editor-action { +.vs-dark .explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row.dirty:not(:hover) > .monaco-action-bar .close-editor-action, +.hc-black .monaco-workbench .explorer-viewlet .explorer-open-editors .monaco-list .monaco-list-row.dirty:not(:hover) > .monaco-action-bar .close-editor-action { background: url("action-close-dirty-dark.svg") center center no-repeat; } -.explorer-viewlet .explorer-open-editors .monaco-tree.focused .monaco-tree-row.selected:not(:hover) > .content.dirty > .monaco-action-bar .close-editor-action { +.explorer-viewlet .explorer-open-editors .monaco-list.focused .monaco-list-row.selected.dirty:not(:hover) > .monaco-action-bar .close-editor-action { background: url("action-close-dirty-focus.svg") center center no-repeat; } diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts index 4470a9b770a..74b55c0a819 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerView.ts @@ -425,8 +425,14 @@ export class ExplorerView extends TreeViewsViewletPanel implements IExplorerView }, this.contextKeyService, this.listService, this.themeService); // Bind context keys - FilesExplorerFocusedContext.bindTo(this.explorerViewer.contextKeyService); - ExplorerFocusedContext.bindTo(this.explorerViewer.contextKeyService); + const filesExplorerFocusedContextKey = FilesExplorerFocusedContext.bindTo(this.explorerViewer.contextKeyService); + const explorerFocusedContextKey = ExplorerFocusedContext.bindTo(this.explorerViewer.contextKeyService); + + // Update context keys + this.disposables.push(this.explorerViewer.onFocusChange(focused => { + filesExplorerFocusedContextKey.set(focused); + explorerFocusedContextKey.set(focused); + })); // Update Viewer based on File Change Events this.disposables.push(this.fileService.onAfterOperation(e => this.onFileOperation(e))); diff --git a/src/vs/workbench/parts/files/electron-browser/views/openEditorsView.ts b/src/vs/workbench/parts/files/electron-browser/views/openEditorsView.ts index 2621f9fe4d9..008d376c544 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/openEditorsView.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/openEditorsView.ts @@ -3,39 +3,47 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import nls = require('vs/nls'); -import errors = require('vs/base/common/errors'); +import * as nls from 'vs/nls'; +import * as errors from 'vs/base/common/errors'; import { RunOnceScheduler } from 'vs/base/common/async'; -import { TPromise } from 'vs/base/common/winjs.base'; import { IAction } from 'vs/base/common/actions'; -import dom = require('vs/base/browser/dom'); -import { IItemCollapseEvent } from 'vs/base/parts/tree/browser/treeModel'; +import * as dom from 'vs/base/browser/dom'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { Position, IEditorInput } from 'vs/platform/editor/common/editor'; import { IEditorStacksModel, IStacksModelChangeEvent, IEditorGroup } from 'vs/workbench/common/editor'; -import { SaveAllAction } from 'vs/workbench/parts/files/electron-browser/fileActions'; -import { TreeViewsViewletPanel, IViewletViewOptions, IViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; -import { VIEWLET_ID, OpenEditorsFocusedContext, ExplorerFocusedContext } from 'vs/workbench/parts/files/common/files'; +import { SaveAllAction, SaveAllInGroupAction, OpenToSideAction, SaveFileAction, RevertFileAction, SaveFileAsAction, CompareWithSavedAction, CompareResourcesAction, SelectResourceForCompareAction } from 'vs/workbench/parts/files/electron-browser/fileActions'; +import { IViewletViewOptions, IViewOptions, ViewsViewletPanel } from 'vs/workbench/browser/parts/views/viewsViewlet'; +import { OpenEditorsFocusedContext, ExplorerFocusedContext, IFilesConfiguration } from 'vs/workbench/parts/files/common/files'; import { ITextFileService, AutoSaveMode } from 'vs/workbench/services/textfile/common/textfiles'; -import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { OpenEditor } from 'vs/workbench/parts/files/common/explorerModel'; -import { Renderer, DataSource, Controller, AccessibilityProvider, ActionProvider, DragAndDrop } from 'vs/workbench/parts/files/electron-browser/views/openEditorsViewer'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; -import { CloseAllEditorsAction } from 'vs/workbench/browser/parts/editor/editorActions'; +import { CloseAllEditorsAction, CloseUnmodifiedEditorsInGroupAction, CloseEditorsInGroupAction, CloseOtherEditorsInGroupAction, CloseEditorAction } from 'vs/workbench/browser/parts/editor/editorActions'; import { ToggleEditorLayoutAction } from 'vs/workbench/browser/actions/toggleEditorLayout'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { EditorGroup } from 'vs/workbench/common/editor/editorStacksModel'; import { attachStylerCallback } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { badgeBackground, badgeForeground, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; -import { WorkbenchTree, IListService } from 'vs/platform/list/browser/listService'; +import { IListService, WorkbenchList } from 'vs/platform/list/browser/listService'; +import { IDelegate, IRenderer, IListContextMenuEvent, IListMouseEvent } from 'vs/base/browser/ui/list/list'; +import { EditorLabel } from 'vs/workbench/browser/labels'; +import { ActionBar, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { ContributableActionProvider } from 'vs/workbench/browser/actions'; +import { memoize } from 'vs/base/common/decorators'; const $ = dom.$; -export class OpenEditorsView extends TreeViewsViewletPanel { +export class OpenEditorsView extends ViewsViewletPanel { private static readonly DEFAULT_VISIBLE_OPEN_EDITORS = 9; private static readonly DEFAULT_DYNAMIC_HEIGHT = true; @@ -44,24 +52,25 @@ export class OpenEditorsView extends TreeViewsViewletPanel { private model: IEditorStacksModel; private dirtyCountElement: HTMLElement; - private structuralTreeRefreshScheduler: RunOnceScheduler; + private listRefreshScheduler: RunOnceScheduler; private structuralRefreshDelay: number; - private groupToRefresh: IEditorGroup; - private fullRefreshNeeded: boolean; + private list: WorkbenchList; + private needsRefresh: boolean; constructor( options: IViewletViewOptions, @IInstantiationService private instantiationService: IInstantiationService, @IContextMenuService contextMenuService: IContextMenuService, @ITextFileService private textFileService: ITextFileService, - @IEditorGroupService editorGroupService: IEditorGroupService, + @IWorkbenchEditorService private editorService: IWorkbenchEditorService, + @IEditorGroupService private editorGroupService: IEditorGroupService, @IConfigurationService private configurationService: IConfigurationService, @IKeybindingService keybindingService: IKeybindingService, @IListService private listService: IListService, @IUntitledEditorService private untitledEditorService: IUntitledEditorService, @IContextKeyService private contextKeyService: IContextKeyService, - @IViewletService private viewletService: IViewletService, - @IThemeService private themeService: IThemeService + @IThemeService private themeService: IThemeService, + @ITelemetryService private telemetryService: ITelemetryService ) { super({ ...(options as IViewOptions), @@ -71,7 +80,25 @@ export class OpenEditorsView extends TreeViewsViewletPanel { this.model = editorGroupService.getStacksModel(); this.structuralRefreshDelay = 0; - this.structuralTreeRefreshScheduler = new RunOnceScheduler(() => this.structuralTreeUpdate(), this.structuralRefreshDelay); + this.listRefreshScheduler = new RunOnceScheduler(() => { + this.list.splice(0, this.list.length, this.elements); + this.focusActiveEditor(); + this.updateSize(); + this.needsRefresh = false; + }, this.structuralRefreshDelay); + + // update on model changes + this.disposables.push(this.model.onModelChanged(e => this.onEditorStacksModelChanged(e))); + + // Also handle configuration updates + this.disposables.push(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationChange(e))); + + // Handle dirty counter + this.disposables.push(this.untitledEditorService.onDidChangeDirty(e => this.updateDirtyIndicator())); + this.disposables.push(this.textFileService.models.onModelsDirty(e => this.updateDirtyIndicator())); + this.disposables.push(this.textFileService.models.onModelsSaved(e => this.updateDirtyIndicator())); + this.disposables.push(this.textFileService.models.onModelsSaveError(e => this.updateDirtyIndicator())); + this.disposables.push(this.textFileService.models.onModelsReverted(e => this.updateDirtyIndicator())); } protected renderHeaderTitle(container: HTMLElement): void { @@ -97,6 +124,43 @@ export class OpenEditorsView extends TreeViewsViewletPanel { this.updateDirtyIndicator(); } + public renderBody(container: HTMLElement): void { + dom.addClass(container, 'explorer-open-editors'); + dom.addClass(container, 'show-file-icons'); + + const delegate = new OpenEditorsDelegate(); + this.updateSize(); + this.list = new WorkbenchList(container, delegate, [ + new EditorGroupRenderer(this.keybindingService, this.instantiationService), + new OpenEditorRenderer(this.instantiationService, this.keybindingService, this.configurationService) + ], { + identityProvider: element => element instanceof OpenEditor ? element.getId() : element.id.toString(), + multipleSelectionSupport: false + }, this.contextKeyService, this.listService, this.themeService); + + // Bind context keys + OpenEditorsFocusedContext.bindTo(this.list.contextKeyService); + ExplorerFocusedContext.bindTo(this.list.contextKeyService); + + this.disposables.push(this.list.onContextMenu(e => this.onListContextMenu(e))); + + // Open when selecting via keyboard + this.disposables.push(this.list.onMouseClick(e => this.onMouseClick(e, false))); + this.disposables.push(this.list.onMouseDblClick(e => this.onMouseClick(e, true))); + this.disposables.push(this.list.onKeyDown(e => { + const event = new StandardKeyboardEvent(e); + if (event.keyCode === KeyCode.Enter) { + const focused = this.list.getFocusedElements(); + const element = focused.length ? focused[0] : undefined; + if (element instanceof OpenEditor) { + this.openEditor(element, { pinned: false, sideBySide: !!event.ctrlKey, preserveFocus: false }); + } + } + })); + + this.listRefreshScheduler.schedule(0); + } + public getActions(): IAction[] { return [ this.instantiationService.createInstance(ToggleEditorLayoutAction, ToggleEditorLayoutAction.ID, ToggleEditorLayoutAction.LABEL), @@ -105,159 +169,149 @@ export class OpenEditorsView extends TreeViewsViewletPanel { ]; } - public renderBody(container: HTMLElement): void { - this.treeContainer = super.renderViewTree(container); - dom.addClass(this.treeContainer, 'explorer-open-editors'); - dom.addClass(this.treeContainer, 'show-file-icons'); - - const dataSource = this.instantiationService.createInstance(DataSource); - const actionProvider = this.instantiationService.createInstance(ActionProvider, this.model); - const renderer = this.instantiationService.createInstance(Renderer, actionProvider); - const controller = this.instantiationService.createInstance(Controller, actionProvider, this.model); - const accessibilityProvider = this.instantiationService.createInstance(AccessibilityProvider); - const dnd = this.instantiationService.createInstance(DragAndDrop); - - this.tree = new WorkbenchTree(this.treeContainer, { - dataSource, - renderer, - controller, - accessibilityProvider, - dnd - }, { - indentPixels: 0, - twistiePixels: 22, - ariaLabel: nls.localize({ key: 'treeAriaLabel', comment: ['Open is an adjective'] }, "Open Editors: List of Active Files"), - showTwistie: false, - keyboardSupport: false - }, this.contextKeyService, this.listService, this.themeService); - - // Bind context keys - OpenEditorsFocusedContext.bindTo(this.tree.contextKeyService); - ExplorerFocusedContext.bindTo(this.tree.contextKeyService); - - // Open when selecting via keyboard - this.disposables.push(this.tree.onDidChangeSelection(event => { - if (event && event.payload && event.payload.origin === 'keyboard') { - controller.openEditor(this.tree.getFocus(), { pinned: false, sideBySide: false, preserveFocus: false }); - } - })); - - // Prevent collapsing of editor groups - this.disposables.push(this.tree.onDidCollapseItem((event: IItemCollapseEvent) => { - if (event.item && event.item.getElement() instanceof EditorGroup) { - setTimeout(() => this.tree.expand(event.item.getElement())); // unwind from callback - } - })); - - this.fullRefreshNeeded = true; - this.structuralTreeUpdate(); + public setExpanded(expanded: boolean): void { + super.setExpanded(expanded); + if (expanded && this.needsRefresh) { + this.listRefreshScheduler.schedule(0); + } } - public create(): TPromise { - - // Load Config - this.updateSize(); - - // listeners - this.registerListeners(); - - return super.create(); + public setVisible(visible: boolean): TPromise { + return super.setVisible(visible).then(() => { + if (visible && this.needsRefresh) { + this.listRefreshScheduler.schedule(0); + } + }); } - private registerListeners(): void { + public getList(): WorkbenchList { + return this.list; + } - // update on model changes - this.disposables.push(this.model.onModelChanged(e => this.onEditorStacksModelChanged(e))); + protected layoutBody(size: number): void { + if (this.list) { + this.list.layout(size); + } + } - // Also handle configuration updates - this.disposables.push(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationChange(e))); + @memoize + private get actionProvider(): ActionProvider { + return new ActionProvider(this.instantiationService, this.textFileService, this.untitledEditorService); + } - // Handle dirty counter - this.disposables.push(this.untitledEditorService.onDidChangeDirty(e => this.updateDirtyIndicator())); - this.disposables.push(this.textFileService.models.onModelsDirty(e => this.updateDirtyIndicator())); - this.disposables.push(this.textFileService.models.onModelsSaved(e => this.updateDirtyIndicator())); - this.disposables.push(this.textFileService.models.onModelsSaveError(e => this.updateDirtyIndicator())); - this.disposables.push(this.textFileService.models.onModelsReverted(e => this.updateDirtyIndicator())); - - // We are not updating the tree while the viewlet is not visible. Thus refresh when viewlet becomes visible #6702 - this.disposables.push(this.viewletService.onDidViewletOpen(viewlet => { - if (viewlet.getId() === VIEWLET_ID) { - this.fullRefreshNeeded = true; - this.structuralTreeUpdate(); - this.updateDirtyIndicator(); + private get elements(): (IEditorGroup | OpenEditor)[] { + const result: (IEditorGroup | OpenEditor)[] = []; + this.model.groups.forEach(g => { + if (this.model.groups.length > 1) { + result.push(g); } - })); + result.push(...g.getEditors().map(ei => new OpenEditor(ei, g))); + }); + + return result; + } + + private getIndex(group: IEditorGroup, editor: IEditorInput): number { + let index = 0; + for (let g of this.model.groups) { + if (this.model.groups.length > 1) { + index++; + } + if (g.id !== group.id) { + index += g.getEditors().length; + } else { + if (!editor) { + return index - 1; + } + for (let e of g.getEditors()) { + if (e.getResource().toString() !== editor.getResource().toString()) { + index++; + } else { + return index; + } + } + } + } + + return -1; + } + + private onMouseClick(event: IListMouseEvent, isDoubleClick: boolean): void { + const element = event.element; + if (!(element instanceof OpenEditor)) { + return; + } + + if (event.browserEvent && event.browserEvent.button === 1 /* Middle Button */) { + const position = this.model.positionOfGroup(element.editorGroup); + this.editorService.closeEditor(position, element.editorInput).done(null, errors.onUnexpectedError); + } else { + this.openEditor(element, { preserveFocus: !isDoubleClick, pinned: isDoubleClick, sideBySide: event.browserEvent.ctrlKey || event.browserEvent.metaKey }); + } + } + + private openEditor(element: OpenEditor, options: { preserveFocus: boolean; pinned: boolean; sideBySide: boolean; }): void { + if (element) { + /* __GDPR__ + "workbenchActionExecuted" : { + "id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('workbenchActionExecuted', { id: 'workbench.files.openFile', from: 'openEditors' }); + let position = this.model.positionOfGroup(element.editorGroup); + if (options.sideBySide && position !== Position.THREE) { + position++; + } + this.editorGroupService.activateGroup(this.model.groupAt(position)); + this.editorService.openEditor(element.editorInput, options, position) + .done(() => this.editorGroupService.activateGroup(this.model.groupAt(position)), errors.onUnexpectedError); + } + } + + private onListContextMenu(e: IListContextMenuEvent): void { + this.contextMenuService.showContextMenu({ + getAnchor: () => e.anchor, + getActions: () => this.actionProvider.getSecondaryActions(e.element), + getActionsContext: () => e.element + }); } private onEditorStacksModelChanged(e: IStacksModelChangeEvent): void { - if (this.isDisposed || !this.isVisible() || !this.tree) { + if (!this.isVisible() || !this.list || !this.isExpanded()) { + this.needsRefresh = true; return; } // Do a minimal tree update based on if the change is structural or not #6670 if (e.structural) { - // If an editor changed structurally it is enough to refresh the group, otherwise a group changed structurally and we need the full refresh. - // If there are multiple groups to refresh - refresh the whole tree. - if (e.editor && !this.groupToRefresh) { - this.groupToRefresh = e.group; - } else { - this.fullRefreshNeeded = true; - } - this.structuralTreeRefreshScheduler.schedule(this.structuralRefreshDelay); + this.listRefreshScheduler.schedule(this.structuralRefreshDelay); } else { - const toRefresh = e.editor ? new OpenEditor(e.editor, e.group) : e.group; - this.tree.refresh(toRefresh, false).done(() => this.highlightActiveEditor(), errors.onUnexpectedError); + + const newElement = e.editor ? new OpenEditor(e.editor, e.group) : e.group; + const index = this.getIndex(e.group, e.editor); + this.list.splice(index, 1, [newElement]); + this.focusActiveEditor(); } } - private structuralTreeUpdate(): void { - // View size - this.minimumBodySize = this.maximumBodySize = this.getExpandedBodySize(this.model); - // Show groups only if there is more than 1 group - const treeInput = this.model.groups.length === 1 ? this.model.groups[0] : this.model; - // TODO@Isidor temporary workaround due to a partial tree refresh issue - this.fullRefreshNeeded = true; - const toRefresh = this.fullRefreshNeeded ? null : this.groupToRefresh; - - (treeInput !== this.tree.getInput() ? this.tree.setInput(treeInput) : this.tree.refresh(toRefresh)).done(() => { - this.fullRefreshNeeded = false; - this.groupToRefresh = null; - - // Always expand all the groups as they are unclickable - return this.tree.expandAll(this.model.groups).then(() => this.highlightActiveEditor()); - }, errors.onUnexpectedError); - } - - private highlightActiveEditor(): void { + private focusActiveEditor(): void { if (this.model.activeGroup && this.model.activeGroup.activeEditor /* could be empty */) { - const openEditor = new OpenEditor(this.model.activeGroup.activeEditor, this.model.activeGroup); - this.tree.clearFocus(); - this.tree.clearSelection(); - - if (openEditor) { - this.tree.setFocus(openEditor); - this.tree.setSelection([openEditor]); - const relativeTop = this.tree.getRelativeTop(openEditor); - if (relativeTop <= 0 || relativeTop >= 1) { - // Only reveal the element if it is not visible #8279 - this.tree.reveal(openEditor).done(null, errors.onUnexpectedError); - } - } + const index = this.getIndex(this.model.activeGroup, this.model.activeGroup.activeEditor); + this.list.setFocus([index]); + this.list.setSelection([index]); + this.list.reveal(index); } } private onConfigurationChange(event: IConfigurationChangeEvent): void { - if (this.isDisposed) { - return; // guard against possible race condition when config change causes recreate of views - } - if (event.affectsConfiguration('explorer.openEditors')) { this.updateSize(); } // Trigger a 'repaint' when decoration settings change if (event.affectsConfiguration('explorer.decorations')) { - this.tree.refresh(); + this.listRefreshScheduler.schedule(); } } @@ -304,7 +358,7 @@ export class OpenEditorsView extends TreeViewsViewletPanel { itemsToShow = Math.max(visibleOpenEditors, 1); } - return itemsToShow * Renderer.ITEM_HEIGHT; + return itemsToShow * OpenEditorsDelegate.ITEM_HEIGHT; } public setStructuralRefreshDelay(delay: number): void { @@ -312,9 +366,307 @@ export class OpenEditorsView extends TreeViewsViewletPanel { } public getOptimalWidth(): number { - let parentNode = this.tree.getHTMLElement(); + let parentNode = this.list.getHTMLElement(); let childNodes = [].slice.call(parentNode.querySelectorAll('.open-editor > a')); return dom.getLargestChildWidth(parentNode, childNodes); } } + +interface IOpenEditorTemplateData { + container: HTMLElement; + root: EditorLabel; + actionBar: ActionBar; + toDispose: IDisposable[]; +} + +interface IEditorGroupTemplateData { + root: HTMLElement; + name: HTMLSpanElement; + actionBar: ActionBar; +} + +class OpenEditorsDelegate implements IDelegate { + + public static readonly ITEM_HEIGHT = 22; + + getHeight(element: OpenEditor | IEditorGroup): number { + return OpenEditorsDelegate.ITEM_HEIGHT; + } + + getTemplateId(element: OpenEditor | IEditorGroup): string { + if (element instanceof EditorGroup) { + return EditorGroupRenderer.ID; + } + + return OpenEditorRenderer.ID; + } +} + +class EditorGroupRenderer implements IRenderer { + static ID = 'editorgroup'; + + constructor( + private keybindingService: IKeybindingService, + private instantiationService: IInstantiationService + ) { + // noop + } + + get templateId() { + return EditorGroupRenderer.ID; + } + + renderTemplate(container: HTMLElement): IEditorGroupTemplateData { + const editorGroupTemplate: IEditorGroupTemplateData = Object.create(null); + editorGroupTemplate.root = dom.append(container, $('.editor-group')); + editorGroupTemplate.name = dom.append(editorGroupTemplate.root, $('span.name')); + editorGroupTemplate.actionBar = new ActionBar(container); + + const editorGroupActions = [ + this.instantiationService.createInstance(SaveAllInGroupAction, SaveAllInGroupAction.ID, SaveAllInGroupAction.LABEL), + this.instantiationService.createInstance(CloseUnmodifiedEditorsInGroupAction, CloseUnmodifiedEditorsInGroupAction.ID, CloseUnmodifiedEditorsInGroupAction.LABEL), + this.instantiationService.createInstance(CloseEditorsInGroupAction, CloseEditorsInGroupAction.ID, CloseEditorsInGroupAction.LABEL) + ]; + editorGroupActions.forEach(a => { + const key = this.keybindingService.lookupKeybinding(a.id); + editorGroupTemplate.actionBar.push(a, { icon: true, label: false, keybinding: key ? key.getLabel() : void 0 }); + }); + + return editorGroupTemplate; + } + + renderElement(editorGroup: IEditorGroup, index: number, templateData: IEditorGroupTemplateData): void { + templateData.name.textContent = editorGroup.label; + templateData.actionBar.context = { group: editorGroup }; + } + + disposeTemplate(templateData: IEditorGroupTemplateData): void { + templateData.actionBar.dispose(); + } +} + +class OpenEditorRenderer implements IRenderer { + static ID = 'openeditor'; + + constructor( + private instantiationService: IInstantiationService, + private keybindingService: IKeybindingService, + private configurationService: IConfigurationService + ) { + // noop + } + + get templateId() { + return OpenEditorRenderer.ID; + } + + renderTemplate(container: HTMLElement): IOpenEditorTemplateData { + const editorTemplate: IOpenEditorTemplateData = Object.create(null); + editorTemplate.container = container; + editorTemplate.actionBar = new ActionBar(container); + + const closeEditorAction = this.instantiationService.createInstance(CloseEditorAction, CloseEditorAction.ID, CloseEditorAction.LABEL); + const key = this.keybindingService.lookupKeybinding(closeEditorAction.id); + editorTemplate.actionBar.push(closeEditorAction, { icon: true, label: false, keybinding: key ? key.getLabel() : void 0 }); + + editorTemplate.root = this.instantiationService.createInstance(EditorLabel, container, void 0); + + editorTemplate.toDispose = []; + + return editorTemplate; + } + + renderElement(editor: OpenEditor, index: number, templateData: IOpenEditorTemplateData): void { + editor.isDirty() ? dom.addClass(templateData.container, 'dirty') : dom.removeClass(templateData.container, 'dirty'); + templateData.root.setEditor(editor.editorInput, { + italic: editor.isPreview(), + extraClasses: ['open-editor'], + fileDecorations: this.configurationService.getValue().explorer.decorations + }); + templateData.actionBar.context = { group: editor.editorGroup, editor: editor.editorInput }; + } + + disposeTemplate(templateData: IOpenEditorTemplateData): void { + templateData.actionBar.dispose(); + templateData.root.dispose(); + dispose(templateData.toDispose); + } +} + +export class ActionProvider extends ContributableActionProvider { + + constructor( + @IInstantiationService private instantiationService: IInstantiationService, + @ITextFileService private textFileService: ITextFileService, + @IUntitledEditorService private untitledEditorService: IUntitledEditorService + ) { + super(); + } + + public getSecondaryActions(element: any): TPromise { + return super.getSecondaryActions(undefined, element).then(result => { + const autoSaveEnabled = this.textFileService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY; + + if (element instanceof EditorGroup) { + if (!autoSaveEnabled) { + result.push(this.instantiationService.createInstance(SaveAllInGroupAction, SaveAllInGroupAction.ID, nls.localize('saveAll', "Save All"))); + result.push(new Separator()); + } + + result.push(this.instantiationService.createInstance(CloseUnmodifiedEditorsInGroupAction, CloseUnmodifiedEditorsInGroupAction.ID, nls.localize('closeAllUnmodified', "Close Unmodified"))); + result.push(this.instantiationService.createInstance(CloseEditorsInGroupAction, CloseEditorsInGroupAction.ID, nls.localize('closeAll', "Close All"))); + } else { + const openEditor = element; + const resource = openEditor.getResource(); + if (resource) { + // Open to side + result.unshift(this.instantiationService.createInstance(OpenToSideAction, undefined, resource, false)); + + if (!openEditor.isUntitled()) { + + // Files: Save / Revert + if (!autoSaveEnabled) { + result.push(new Separator()); + + const saveAction = this.instantiationService.createInstance(SaveFileAction, SaveFileAction.ID, SaveFileAction.LABEL); + saveAction.setResource(resource); + saveAction.enabled = openEditor.isDirty(); + result.push(saveAction); + + const revertAction = this.instantiationService.createInstance(RevertFileAction, RevertFileAction.ID, RevertFileAction.LABEL); + revertAction.setResource(resource); + revertAction.enabled = openEditor.isDirty(); + result.push(revertAction); + } + } + + // Untitled: Save / Save As + if (openEditor.isUntitled()) { + result.push(new Separator()); + + if (this.untitledEditorService.hasAssociatedFilePath(resource)) { + let saveUntitledAction = this.instantiationService.createInstance(SaveFileAction, SaveFileAction.ID, SaveFileAction.LABEL); + saveUntitledAction.setResource(resource); + result.push(saveUntitledAction); + } + + let saveAsAction = this.instantiationService.createInstance(SaveFileAsAction, SaveFileAsAction.ID, SaveFileAsAction.LABEL); + saveAsAction.setResource(resource); + result.push(saveAsAction); + } + + // Compare Actions + result.push(new Separator()); + + if (!openEditor.isUntitled()) { + const compareWithSavedAction = this.instantiationService.createInstance(CompareWithSavedAction, CompareWithSavedAction.ID, nls.localize('compareWithSaved', "Compare with Saved")); + compareWithSavedAction.setResource(resource); + compareWithSavedAction.enabled = openEditor.isDirty(); + result.push(compareWithSavedAction); + } + + const runCompareAction = this.instantiationService.createInstance(CompareResourcesAction, resource, undefined); + if (runCompareAction._isEnabled()) { + result.push(runCompareAction); + } + result.push(this.instantiationService.createInstance(SelectResourceForCompareAction, resource, undefined)); + + result.push(new Separator()); + } + + result.push(this.instantiationService.createInstance(CloseEditorAction, CloseEditorAction.ID, nls.localize('close', "Close"))); + const closeOtherEditorsInGroupAction = this.instantiationService.createInstance(CloseOtherEditorsInGroupAction, CloseOtherEditorsInGroupAction.ID, nls.localize('closeOthers', "Close Others")); + closeOtherEditorsInGroupAction.enabled = openEditor.editorGroup.count > 1; + result.push(closeOtherEditorsInGroupAction); + result.push(this.instantiationService.createInstance(CloseUnmodifiedEditorsInGroupAction, CloseUnmodifiedEditorsInGroupAction.ID, nls.localize('closeAllUnmodified', "Close Unmodified"))); + result.push(this.instantiationService.createInstance(CloseEditorsInGroupAction, CloseEditorsInGroupAction.ID, nls.localize('closeAll', "Close All"))); + } + + return result; + }); + } +} + +// export class DragAndDrop extends DefaultDragAndDrop { + +// constructor( +// @IWorkbenchEditorService private editorService: IWorkbenchEditorService, +// @IEditorGroupService private editorGroupService: IEditorGroupService +// ) { +// super(); +// } + +// public getDragURI(tree: ITree, element: OpenEditor): string { +// if (!(element instanceof OpenEditor)) { +// return null; +// } + +// const resource = element.getResource(); +// // Some open editors do not have a resource so use the name as drag identifier instead #7021 +// return resource ? resource.toString() : element.editorInput.getName(); +// } + +// public getDragLabel(tree: ITree, elements: OpenEditor[]): string { +// if (elements.length > 1) { +// return String(elements.length); +// } + +// return elements[0].editorInput.getName(); +// } + +// public onDragOver(tree: ITree, data: IDragAndDropData, target: OpenEditor | EditorGroup, originalEvent: DragMouseEvent): IDragOverReaction { +// if (!(target instanceof OpenEditor) && !(target instanceof EditorGroup)) { +// return DRAG_OVER_REJECT; +// } + +// if (data instanceof ExternalElementsDragAndDropData) { +// let resource = explorerItemToFileResource(data.getData()[0]); + +// if (!resource) { +// return DRAG_OVER_REJECT; +// } + +// return resource.isDirectory ? DRAG_OVER_REJECT : DRAG_OVER_ACCEPT; +// } + +// if (data instanceof DesktopDragAndDropData) { +// return DRAG_OVER_REJECT; +// } + +// if (!(data instanceof ElementsDragAndDropData)) { +// return DRAG_OVER_REJECT; +// } + +// return DRAG_OVER_ACCEPT; +// } + +// public drop(tree: ITree, data: IDragAndDropData, target: OpenEditor | EditorGroup, originalEvent: DragMouseEvent): void { +// let draggedElement: OpenEditor | EditorGroup; +// const model = this.editorGroupService.getStacksModel(); +// const positionOfTargetGroup = model.positionOfGroup(target instanceof EditorGroup ? target : target.editorGroup); +// const index = target instanceof OpenEditor ? target.editorGroup.indexOf(target.editorInput) : undefined; +// // Support drop from explorer viewer +// if (data instanceof ExternalElementsDragAndDropData) { +// let resource = explorerItemToFileResource(data.getData()[0]); +// (resource as IResourceInput).options = { index, pinned: true }; +// this.editorService.openEditor(resource, positionOfTargetGroup).done(null, errors.onUnexpectedError); +// } + +// // Drop within viewer +// else { +// let source: OpenEditor | EditorGroup[] = data.getData(); +// if (Array.isArray(source)) { +// draggedElement = source[0]; +// } +// } + +// if (draggedElement) { +// if (draggedElement instanceof OpenEditor) { +// this.editorGroupService.moveEditor(draggedElement.editorInput, model.positionOfGroup(draggedElement.editorGroup), positionOfTargetGroup, { index }); +// } else { +// this.editorGroupService.moveGroup(model.positionOfGroup(draggedElement), positionOfTargetGroup); +// } +// } +// } +// } diff --git a/src/vs/workbench/parts/files/electron-browser/views/openEditorsViewer.ts b/src/vs/workbench/parts/files/electron-browser/views/openEditorsViewer.ts deleted file mode 100644 index 6c0ac532f9a..00000000000 --- a/src/vs/workbench/parts/files/electron-browser/views/openEditorsViewer.ts +++ /dev/null @@ -1,521 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import nls = require('vs/nls'); -import errors = require('vs/base/common/errors'); -import { TPromise } from 'vs/base/common/winjs.base'; -import { IAction } from 'vs/base/common/actions'; -import { EditorLabel } from 'vs/workbench/browser/labels'; -import { DefaultController, ClickBehavior, DefaultDragAndDrop } from 'vs/base/parts/tree/browser/treeDefaults'; -import { IDataSource, ITree, IAccessibilityProvider, IDragAndDropData, IDragOverReaction, DRAG_OVER_ACCEPT, DRAG_OVER_REJECT, ContextMenuEvent, IRenderer } from 'vs/base/parts/tree/browser/tree'; -import { ExternalElementsDragAndDropData, ElementsDragAndDropData, DesktopDragAndDropData } from 'vs/base/parts/tree/browser/treeDnd'; -import { ActionBar, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; -import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import dom = require('vs/base/browser/dom'); -import { IMouseEvent, DragMouseEvent } from 'vs/base/browser/mouseEvent'; -import { IResourceInput, Position } from 'vs/platform/editor/common/editor'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IEditorGroup, IEditorStacksModel } from 'vs/workbench/common/editor'; -import { OpenEditor } from 'vs/workbench/parts/files/common/explorerModel'; -import { ContributableActionProvider } from 'vs/workbench/browser/actions'; -import { explorerItemToFileResource, IFilesConfiguration } from 'vs/workbench/parts/files/common/files'; -import { ITextFileService, AutoSaveMode } from 'vs/workbench/services/textfile/common/textfiles'; -import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { EditorStacksModel, EditorGroup } from 'vs/workbench/common/editor/editorStacksModel'; -import { SaveFileAction, RevertFileAction, SaveFileAsAction, OpenToSideAction, SelectResourceForCompareAction, CompareResourcesAction, SaveAllInGroupAction, CompareWithSavedAction } from 'vs/workbench/parts/files/electron-browser/fileActions'; -import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; -import { CloseOtherEditorsInGroupAction, CloseEditorAction, CloseEditorsInGroupAction, CloseUnmodifiedEditorsInGroupAction } from 'vs/workbench/browser/parts/editor/editorActions'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; - -const $ = dom.$; - -export class DataSource implements IDataSource { - - public getId(tree: ITree, element: any): string { - if (element instanceof EditorStacksModel) { - return 'root'; - } - if (element instanceof EditorGroup) { - return (element).id.toString(); - } - - return (element).getId(); - } - - public hasChildren(tree: ITree, element: any): boolean { - return element instanceof EditorStacksModel || element instanceof EditorGroup; - } - - public getChildren(tree: ITree, element: any): TPromise { - if (element instanceof EditorStacksModel) { - return TPromise.as((element).groups); - } - - const editorGroup = element; - return TPromise.as(editorGroup.getEditors().map(ei => new OpenEditor(ei, editorGroup))); - } - - public getParent(tree: ITree, element: any): TPromise { - return TPromise.as(null); - } -} - -interface IOpenEditorTemplateData { - container: HTMLElement; - root: EditorLabel; - actionBar: ActionBar; -} - -interface IEditorGroupTemplateData { - root: HTMLElement; - name: HTMLSpanElement; - actionBar: ActionBar; -} - -export class Renderer implements IRenderer { - - public static readonly ITEM_HEIGHT = 22; - private static readonly EDITOR_GROUP_TEMPLATE_ID = 'editorgroup'; - private static readonly OPEN_EDITOR_TEMPLATE_ID = 'openeditor'; - - constructor( - private actionProvider: ActionProvider, - @IInstantiationService private instantiationService: IInstantiationService, - @IKeybindingService private keybindingService: IKeybindingService, - @IConfigurationService private configurationService: IConfigurationService - ) { - // noop - } - - public getHeight(tree: ITree, element: any): number { - return Renderer.ITEM_HEIGHT; - } - - public getTemplateId(tree: ITree, element: any): string { - if (element instanceof EditorGroup) { - return Renderer.EDITOR_GROUP_TEMPLATE_ID; - } - - return Renderer.OPEN_EDITOR_TEMPLATE_ID; - } - - public renderTemplate(tree: ITree, templateId: string, container: HTMLElement): any { - if (templateId === Renderer.EDITOR_GROUP_TEMPLATE_ID) { - const editorGroupTemplate: IEditorGroupTemplateData = Object.create(null); - editorGroupTemplate.root = dom.append(container, $('.editor-group')); - editorGroupTemplate.name = dom.append(editorGroupTemplate.root, $('span.name')); - editorGroupTemplate.actionBar = new ActionBar(container); - - const editorGroupActions = this.actionProvider.getEditorGroupActions(); - editorGroupActions.forEach(a => { - const key = this.keybindingService.lookupKeybinding(a.id); - editorGroupTemplate.actionBar.push(a, { icon: true, label: false, keybinding: key ? key.getLabel() : void 0 }); - }); - - return editorGroupTemplate; - } - - const editorTemplate: IOpenEditorTemplateData = Object.create(null); - editorTemplate.container = container; - editorTemplate.actionBar = new ActionBar(container); - - const openEditorActions = this.actionProvider.getOpenEditorActions(); - openEditorActions.forEach(a => { - const key = this.keybindingService.lookupKeybinding(a.id); - editorTemplate.actionBar.push(a, { icon: true, label: false, keybinding: key ? key.getLabel() : void 0 }); - }); - - editorTemplate.root = this.instantiationService.createInstance(EditorLabel, container, void 0); - - return editorTemplate; - } - - public renderElement(tree: ITree, element: any, templateId: string, templateData: any): void { - if (templateId === Renderer.EDITOR_GROUP_TEMPLATE_ID) { - this.renderEditorGroup(tree, element, templateData); - } else { - this.renderOpenEditor(tree, element, templateData); - } - } - - private renderEditorGroup(tree: ITree, editorGroup: IEditorGroup, templateData: IEditorGroupTemplateData): void { - templateData.name.textContent = editorGroup.label; - templateData.actionBar.context = { group: editorGroup }; - } - - private renderOpenEditor(tree: ITree, editor: OpenEditor, templateData: IOpenEditorTemplateData): void { - editor.isDirty() ? dom.addClass(templateData.container, 'dirty') : dom.removeClass(templateData.container, 'dirty'); - templateData.root.setEditor(editor.editorInput, { - italic: editor.isPreview(), - extraClasses: ['open-editor'], - fileDecorations: this.configurationService.getValue().explorer.decorations - }); - templateData.actionBar.context = { group: editor.editorGroup, editor: editor.editorInput }; - } - - public disposeTemplate(tree: ITree, templateId: string, templateData: any): void { - if (templateId === Renderer.OPEN_EDITOR_TEMPLATE_ID) { - (templateData).actionBar.dispose(); - (templateData).root.dispose(); - } - if (templateId === Renderer.EDITOR_GROUP_TEMPLATE_ID) { - (templateData).actionBar.dispose(); - } - } -} - -export class Controller extends DefaultController { - - constructor(private actionProvider: ActionProvider, private model: IEditorStacksModel, - @IWorkbenchEditorService private editorService: IWorkbenchEditorService, - @IEditorGroupService private editorGroupService: IEditorGroupService, - @IContextMenuService private contextMenuService: IContextMenuService, - @ITelemetryService private telemetryService: ITelemetryService - ) { - super({ clickBehavior: ClickBehavior.ON_MOUSE_DOWN, keyboardSupport: false }); - } - - public onClick(tree: ITree, element: any, event: IMouseEvent): boolean { - - // Close opened editor on middle mouse click - if (element instanceof OpenEditor && event.browserEvent && event.browserEvent.button === 1 /* Middle Button */) { - const position = this.model.positionOfGroup(element.editorGroup); - - this.editorService.closeEditor(position, element.editorInput).done(null, errors.onUnexpectedError); - - return true; - } - - return super.onClick(tree, element, event); - } - - protected onLeftClick(tree: ITree, element: any, event: IMouseEvent, origin: string = 'mouse'): boolean { - const payload = { origin: origin }; - const isDoubleClick = (origin === 'mouse' && event.detail === 2); - - // Cancel Event - const isMouseDown = event && event.browserEvent && event.browserEvent.type === 'mousedown'; - if (!isMouseDown) { - event.preventDefault(); // we cannot preventDefault onMouseDown because this would break DND otherwise - } - event.stopPropagation(); - - // Status group should never get selected nor expanded/collapsed - if (!(element instanceof OpenEditor)) { - return true; - } - - // Set DOM focus - tree.DOMFocus(); - - // Allow to unselect - if (event.shiftKey) { - const selection = tree.getSelection(); - if (selection && selection.length > 0 && selection[0] === element) { - tree.clearSelection(payload); - } - } - - // Select, Focus and open files - else { - tree.setFocus(element, payload); - - if (isDoubleClick) { - event.preventDefault(); // focus moves to editor, we need to prevent default - } - - tree.setSelection([element], payload); - this.openEditor(element, { preserveFocus: !isDoubleClick, pinned: isDoubleClick, sideBySide: event.ctrlKey || event.metaKey }); - } - - return true; - } - - // Do not allow left / right to expand and collapse groups #7848 - protected onLeft(tree: ITree, event: IKeyboardEvent): boolean { - return true; - } - - protected onRight(tree: ITree, event: IKeyboardEvent): boolean { - return true; - } - - public onContextMenu(tree: ITree, element: any, event: ContextMenuEvent): boolean { - if (event.target && event.target.tagName && event.target.tagName.toLowerCase() === 'input') { - return false; - } - // Check if clicked on some element - if (element === tree.getInput()) { - return false; - } - - event.preventDefault(); - event.stopPropagation(); - - tree.setFocus(element); - const group = element instanceof EditorGroup ? element : (element).editorGroup; - const editor = element instanceof OpenEditor ? (element).editorInput : undefined; - - let anchor = { x: event.posx, y: event.posy }; - this.contextMenuService.showContextMenu({ - getAnchor: () => anchor, - getActions: () => this.actionProvider.getSecondaryActions(tree, element), - onHide: (wasCancelled?: boolean) => { - if (wasCancelled) { - tree.DOMFocus(); - } - }, - getActionsContext: () => ({ group, editor }) - }); - - return true; - } - - public openEditor(element: OpenEditor, options: { preserveFocus: boolean; pinned: boolean; sideBySide: boolean; }): void { - if (element) { - /* __GDPR__ - "workbenchActionExecuted" : { - "id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetryService.publicLog('workbenchActionExecuted', { id: 'workbench.files.openFile', from: 'openEditors' }); - let position = this.model.positionOfGroup(element.editorGroup); - if (options.sideBySide && position !== Position.THREE) { - position++; - } - this.editorGroupService.activateGroup(this.model.groupAt(position)); - this.editorService.openEditor(element.editorInput, options, position) - .done(() => this.editorGroupService.activateGroup(this.model.groupAt(position)), errors.onUnexpectedError); - } - } -} - -export class AccessibilityProvider implements IAccessibilityProvider { - - getAriaLabel(tree: ITree, element: any): string { - if (element instanceof EditorGroup) { - return nls.localize('editorGroupAriaLabel', "{0}, Editor Group", (element).label); - } - - return nls.localize('openEditorAriaLabel', "{0}, Open Editor", (element).editorInput.getName()); - } -} - -export class ActionProvider extends ContributableActionProvider { - - constructor( - private model: IEditorStacksModel, - @IInstantiationService private instantiationService: IInstantiationService, - @ITextFileService private textFileService: ITextFileService, - @IUntitledEditorService private untitledEditorService: IUntitledEditorService - ) { - super(); - } - - public hasActions(tree: ITree, element: any): boolean { - const multipleGroups = this.model.groups.length > 1; - return element instanceof OpenEditor || (element instanceof EditorGroup && multipleGroups); - } - - public getActions(tree: ITree, element: any): TPromise { - if (element instanceof OpenEditor) { - return TPromise.as(this.getOpenEditorActions()); - } - if (element instanceof EditorGroup) { - return TPromise.as(this.getEditorGroupActions()); - } - - return TPromise.as([]); - } - - public getOpenEditorActions(): IAction[] { - return [this.instantiationService.createInstance(CloseEditorAction, CloseEditorAction.ID, CloseEditorAction.LABEL)]; - } - - public getEditorGroupActions(): IAction[] { - const saveAllAction = this.instantiationService.createInstance(SaveAllInGroupAction, SaveAllInGroupAction.ID, SaveAllInGroupAction.LABEL); - - return [ - saveAllAction, - this.instantiationService.createInstance(CloseUnmodifiedEditorsInGroupAction, CloseUnmodifiedEditorsInGroupAction.ID, CloseUnmodifiedEditorsInGroupAction.LABEL), - this.instantiationService.createInstance(CloseEditorsInGroupAction, CloseEditorsInGroupAction.ID, CloseEditorsInGroupAction.LABEL) - ]; - } - - public hasSecondaryActions(tree: ITree, element: any): boolean { - return element instanceof OpenEditor || element instanceof EditorGroup; - } - - public getSecondaryActions(tree: ITree, element: any): TPromise { - return super.getSecondaryActions(tree, element).then(result => { - const autoSaveEnabled = this.textFileService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY; - - if (element instanceof EditorGroup) { - if (!autoSaveEnabled) { - result.push(this.instantiationService.createInstance(SaveAllInGroupAction, SaveAllInGroupAction.ID, nls.localize('saveAll', "Save All"))); - result.push(new Separator()); - } - - result.push(this.instantiationService.createInstance(CloseUnmodifiedEditorsInGroupAction, CloseUnmodifiedEditorsInGroupAction.ID, nls.localize('closeAllUnmodified', "Close Unmodified"))); - result.push(this.instantiationService.createInstance(CloseEditorsInGroupAction, CloseEditorsInGroupAction.ID, nls.localize('closeAll', "Close All"))); - } else { - const openEditor = element; - const resource = openEditor.getResource(); - if (resource) { - // Open to side - result.unshift(this.instantiationService.createInstance(OpenToSideAction, tree, resource, false)); - - if (!openEditor.isUntitled()) { - - // Files: Save / Revert - if (!autoSaveEnabled) { - result.push(new Separator()); - - const saveAction = this.instantiationService.createInstance(SaveFileAction, SaveFileAction.ID, SaveFileAction.LABEL); - saveAction.setResource(resource); - saveAction.enabled = openEditor.isDirty(); - result.push(saveAction); - - const revertAction = this.instantiationService.createInstance(RevertFileAction, RevertFileAction.ID, RevertFileAction.LABEL); - revertAction.setResource(resource); - revertAction.enabled = openEditor.isDirty(); - result.push(revertAction); - } - } - - // Untitled: Save / Save As - if (openEditor.isUntitled()) { - result.push(new Separator()); - - if (this.untitledEditorService.hasAssociatedFilePath(resource)) { - let saveUntitledAction = this.instantiationService.createInstance(SaveFileAction, SaveFileAction.ID, SaveFileAction.LABEL); - saveUntitledAction.setResource(resource); - result.push(saveUntitledAction); - } - - let saveAsAction = this.instantiationService.createInstance(SaveFileAsAction, SaveFileAsAction.ID, SaveFileAsAction.LABEL); - saveAsAction.setResource(resource); - result.push(saveAsAction); - } - - // Compare Actions - result.push(new Separator()); - - if (!openEditor.isUntitled()) { - const compareWithSavedAction = this.instantiationService.createInstance(CompareWithSavedAction, CompareWithSavedAction.ID, nls.localize('compareWithSaved', "Compare with Saved")); - compareWithSavedAction.setResource(resource); - compareWithSavedAction.enabled = openEditor.isDirty(); - result.push(compareWithSavedAction); - } - - const runCompareAction = this.instantiationService.createInstance(CompareResourcesAction, resource, tree); - if (runCompareAction._isEnabled()) { - result.push(runCompareAction); - } - result.push(this.instantiationService.createInstance(SelectResourceForCompareAction, resource, tree)); - - result.push(new Separator()); - } - - result.push(this.instantiationService.createInstance(CloseEditorAction, CloseEditorAction.ID, nls.localize('close', "Close"))); - const closeOtherEditorsInGroupAction = this.instantiationService.createInstance(CloseOtherEditorsInGroupAction, CloseOtherEditorsInGroupAction.ID, nls.localize('closeOthers', "Close Others")); - closeOtherEditorsInGroupAction.enabled = openEditor.editorGroup.count > 1; - result.push(closeOtherEditorsInGroupAction); - result.push(this.instantiationService.createInstance(CloseUnmodifiedEditorsInGroupAction, CloseUnmodifiedEditorsInGroupAction.ID, nls.localize('closeAllUnmodified', "Close Unmodified"))); - result.push(this.instantiationService.createInstance(CloseEditorsInGroupAction, CloseEditorsInGroupAction.ID, nls.localize('closeAll', "Close All"))); - } - - return result; - }); - } -} - -export class DragAndDrop extends DefaultDragAndDrop { - - constructor( - @IWorkbenchEditorService private editorService: IWorkbenchEditorService, - @IEditorGroupService private editorGroupService: IEditorGroupService - ) { - super(); - } - - public getDragURI(tree: ITree, element: OpenEditor): string { - if (!(element instanceof OpenEditor)) { - return null; - } - - const resource = element.getResource(); - // Some open editors do not have a resource so use the name as drag identifier instead #7021 - return resource ? resource.toString() : element.editorInput.getName(); - } - - public getDragLabel(tree: ITree, elements: OpenEditor[]): string { - if (elements.length > 1) { - return String(elements.length); - } - - return elements[0].editorInput.getName(); - } - - public onDragOver(tree: ITree, data: IDragAndDropData, target: OpenEditor | EditorGroup, originalEvent: DragMouseEvent): IDragOverReaction { - if (!(target instanceof OpenEditor) && !(target instanceof EditorGroup)) { - return DRAG_OVER_REJECT; - } - - if (data instanceof ExternalElementsDragAndDropData) { - let resource = explorerItemToFileResource(data.getData()[0]); - - if (!resource) { - return DRAG_OVER_REJECT; - } - - return resource.isDirectory ? DRAG_OVER_REJECT : DRAG_OVER_ACCEPT; - } - - if (data instanceof DesktopDragAndDropData) { - return DRAG_OVER_REJECT; - } - - if (!(data instanceof ElementsDragAndDropData)) { - return DRAG_OVER_REJECT; - } - - return DRAG_OVER_ACCEPT; - } - - public drop(tree: ITree, data: IDragAndDropData, target: OpenEditor | EditorGroup, originalEvent: DragMouseEvent): void { - let draggedElement: OpenEditor | EditorGroup; - const model = this.editorGroupService.getStacksModel(); - const positionOfTargetGroup = model.positionOfGroup(target instanceof EditorGroup ? target : target.editorGroup); - const index = target instanceof OpenEditor ? target.editorGroup.indexOf(target.editorInput) : undefined; - // Support drop from explorer viewer - if (data instanceof ExternalElementsDragAndDropData) { - let resource = explorerItemToFileResource(data.getData()[0]); - (resource as IResourceInput).options = { index, pinned: true }; - this.editorService.openEditor(resource, positionOfTargetGroup).done(null, errors.onUnexpectedError); - } - - // Drop within viewer - else { - let source: OpenEditor | EditorGroup[] = data.getData(); - if (Array.isArray(source)) { - draggedElement = source[0]; - } - } - - if (draggedElement) { - if (draggedElement instanceof OpenEditor) { - this.editorGroupService.moveEditor(draggedElement.editorInput, model.positionOfGroup(draggedElement.editorGroup), positionOfTargetGroup, { index }); - } else { - this.editorGroupService.moveGroup(model.positionOfGroup(draggedElement), positionOfTargetGroup); - } - } - } -} diff --git a/src/vs/workbench/parts/stats/node/workspaceStats.ts b/src/vs/workbench/parts/stats/node/workspaceStats.ts index a937112e7ad..6ea817f0a70 100644 --- a/src/vs/workbench/parts/stats/node/workspaceStats.ts +++ b/src/vs/workbench/parts/stats/node/workspaceStats.ts @@ -210,34 +210,35 @@ export class WorkspaceStats implements IWorkbenchContribution { if (folders && folders.length && this.fileService) { return this.fileService.resolveFiles(folders.map(resource => ({ resource }))).then(results => { const names = ([]).concat(...results.map(result => result.success ? (result.stat.children || []) : [])).map(c => c.name); + const nameSet = names.reduce((s, n) => s.add(n.toLowerCase()), new Set()); - tags['workspace.grunt'] = this.searchArray(names, /^gruntfile\.js$/i); - tags['workspace.gulp'] = this.searchArray(names, /^gulpfile\.js$/i); - tags['workspace.jake'] = this.searchArray(names, /^jakefile\.js$/i); + tags['workspace.grunt'] = nameSet.has('gruntfile.js'); + tags['workspace.gulp'] = nameSet.has('gulpfile.js'); + tags['workspace.jake'] = nameSet.has('jakefile.js'); - tags['workspace.tsconfig'] = this.searchArray(names, /^tsconfig\.json$/i); - tags['workspace.jsconfig'] = this.searchArray(names, /^jsconfig\.json$/i); - tags['workspace.config.xml'] = this.searchArray(names, /^config\.xml/i); - tags['workspace.vsc.extension'] = this.searchArray(names, /^vsc-extension-quickstart\.md/i); + tags['workspace.tsconfig'] = nameSet.has('tsconfig.json'); + tags['workspace.jsconfig'] = nameSet.has('jsconfig.json'); + tags['workspace.config.xml'] = nameSet.has('config.xml'); + tags['workspace.vsc.extension'] = nameSet.has('vsc-extension-quickstart.md'); - tags['workspace.ASP5'] = this.searchArray(names, /^project\.json$/i) && this.searchArray(names, /^.+\.cs$/i); + tags['workspace.ASP5'] = nameSet.has('project.json') && this.searchArray(names, /^.+\.cs$/i); tags['workspace.sln'] = this.searchArray(names, /^.+\.sln$|^.+\.csproj$/i); - tags['workspace.unity'] = this.searchArray(names, /^Assets$/i) && this.searchArray(names, /^Library$/i) && this.searchArray(names, /^ProjectSettings/i); - tags['workspace.npm'] = this.searchArray(names, /^package\.json$|^node_modules$/i); - tags['workspace.bower'] = this.searchArray(names, /^bower\.json$|^bower_components$/i); + tags['workspace.unity'] = nameSet.has('assets') && nameSet.has('library') && nameSet.has('projectsettings'); + tags['workspace.npm'] = nameSet.has('package.json') || nameSet.has('node_modules'); + tags['workspace.bower'] = nameSet.has('bower.json') || nameSet.has('bower_components'); - tags['workspace.yeoman.code.ext'] = this.searchArray(names, /^vsc-extension-quickstart\.md$/i); + tags['workspace.yeoman.code.ext'] = nameSet.has('vsc-extension-quickstart.md'); - let mainActivity = this.searchArray(names, /^MainActivity\.cs$/i) || this.searchArray(names, /^MainActivity\.fs$/i); - let appDelegate = this.searchArray(names, /^AppDelegate\.cs$/i) || this.searchArray(names, /^AppDelegate\.fs$/i); - let androidManifest = this.searchArray(names, /^AndroidManifest\.xml$/i); + let mainActivity = nameSet.has('mainactivity.cs') || nameSet.has('mainactivity.fs'); + let appDelegate = nameSet.has('appdelegate.cs') || nameSet.has('appdelegate.fs'); + let androidManifest = nameSet.has('androidmanifest.xml'); - let platforms = this.searchArray(names, /^platforms$/i); - let plugins = this.searchArray(names, /^plugins$/i); - let www = this.searchArray(names, /^www$/i); - let properties = this.searchArray(names, /^Properties/i); - let resources = this.searchArray(names, /^Resources/i); - let jni = this.searchArray(names, /^JNI/i); + let platforms = nameSet.has('platforms'); + let plugins = nameSet.has('plugins'); + let www = nameSet.has('www'); + let properties = nameSet.has('properties'); + let resources = nameSet.has('resources'); + let jni = nameSet.has('jni'); if (tags['workspace.config.xml'] && !tags['workspace.language.cs'] && !tags['workspace.language.vb'] && !tags['workspace.language.aspx']) { @@ -260,8 +261,8 @@ export class WorkspaceStats implements IWorkbenchContribution { tags['workspace.android.cpp'] = true; } - tags['workspace.reactNative'] = this.searchArray(names, /^android$/i) && this.searchArray(names, /^ios$/i) && - this.searchArray(names, /^index\.android\.js$/i) && this.searchArray(names, /^index\.ios\.js$/i); + tags['workspace.reactNative'] = nameSet.has('android') && nameSet.has('ios') && + nameSet.has('index.android.js') && nameSet.has('index.ios.js'); return tags; }, error => { onUnexpectedError(error); return null; }); diff --git a/src/vs/workbench/services/configuration/node/configuration.ts b/src/vs/workbench/services/configuration/node/configuration.ts index 79e24da0d55..983bf2b902f 100644 --- a/src/vs/workbench/services/configuration/node/configuration.ts +++ b/src/vs/workbench/services/configuration/node/configuration.ts @@ -24,6 +24,7 @@ import { JSONEditingService } from 'vs/workbench/services/configuration/node/jso import { WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { relative } from 'path'; +import { equals } from 'vs/base/common/objects'; // node.hs helper functions @@ -189,10 +190,9 @@ export class FolderConfiguration extends Disposable { } reprocess(): ConfigurationModel { - const oldKeys = this.getUnsupportedKeys(); + const oldContents = this._folderSettingsModelParser.folderSettingsModel.contents; this._folderSettingsModelParser.reprocess(); - const newKeys = this.getUnsupportedKeys(); - if (this.hasKeysChanged(oldKeys, newKeys)) { + if (!equals(oldContents, this._folderSettingsModelParser.folderSettingsModel.contents)) { this.consolidate(); } return this._cache; @@ -202,18 +202,6 @@ export class FolderConfiguration extends Disposable { return this._folderSettingsModelParser.folderSettingsModel.unsupportedKeys; } - private hasKeysChanged(oldKeys: string[], newKeys: string[]): boolean { - if (oldKeys.length !== newKeys.length) { - return true; - } - for (const key of oldKeys) { - if (newKeys.indexOf(key) === -1) { - return true; - } - } - return false; - } - private consolidate(): void { this._cache = this._folderSettingsModelParser.folderSettingsModel.merge(...this._standAloneConfigurations); } diff --git a/src/vs/workbench/services/configuration/test/node/configurationService.test.ts b/src/vs/workbench/services/configuration/test/node/configurationService.test.ts index eaa5fa0c337..d41ac767a69 100644 --- a/src/vs/workbench/services/configuration/test/node/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/node/configurationService.test.ts @@ -968,6 +968,26 @@ suite('WorkspaceConfigurationService - Multiroot', () => { }); }); + test('resource setting in folder is read after it is registered later', () => { + fs.writeFileSync(workspaceContextService.getWorkspace().folders[0].toResource('.vscode/settings.json').fsPath, '{ "configurationService.workspace.testNewResourceSetting2": "workspaceFolderValue" }'); + return jsonEditingServce.write(workspaceContextService.getWorkspace().configuration, { key: 'settings', value: { 'configurationService.workspace.testNewResourceSetting2': 'workspaceValue' } }, true) + .then(() => testObject.reloadConfiguration()) + .then(() => { + configurationRegistry.registerConfiguration({ + 'id': '_test', + 'type': 'object', + 'properties': { + 'configurationService.workspace.testNewResourceSetting2': { + 'type': 'string', + 'default': 'isSet', + scope: ConfigurationScope.RESOURCE + } + } + }); + assert.equal(testObject.getValue('configurationService.workspace.testNewResourceSetting2', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'workspaceFolderValue'); + }); + }); + test('inspect', () => { let actual = testObject.inspect('something.missing'); assert.equal(actual.default, void 0); diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts b/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts index db41c548c95..d350d4a01e2 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionHost.ts @@ -365,7 +365,9 @@ export class ExtensionHostProcessWorker { extensions: extensionDescriptions, // Send configurations scopes only in development mode. configuration: !this._environmentService.isBuilt || this._environmentService.isExtensionDevelopment ? { ...configurationData, configurationScopes: getScopes(this._configurationService.keys().default) } : configurationData, - telemetryInfo + telemetryInfo, + args: this._environmentService.args, + execPath: this._environmentService.execPath }; return r; }); diff --git a/src/vs/workbench/services/scm/common/scmService.ts b/src/vs/workbench/services/scm/common/scmService.ts index dadee567ffa..614c0a4d664 100644 --- a/src/vs/workbench/services/scm/common/scmService.ts +++ b/src/vs/workbench/services/scm/common/scmService.ts @@ -8,6 +8,7 @@ import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import Event, { Emitter } from 'vs/base/common/event'; import { ISCMService, ISCMProvider, ISCMInput, ISCMRepository } from './scm'; +import { log, LogLevel, ILogService } from 'vs/platform/log/common/log'; class SCMInput implements ISCMInput { @@ -76,8 +77,12 @@ export class SCMService implements ISCMService { private _onDidRemoveProvider = new Emitter(); get onDidRemoveRepository(): Event { return this._onDidRemoveProvider.event; } - constructor() { } + constructor( + // @ts-ignore + @ILogService private logService: ILogService + ) { } + @log(LogLevel.INFO, 'SCMService') registerSCMProvider(provider: ISCMProvider): ISCMRepository { if (this._providerIds.has(provider.id)) { throw new Error(`SCM Provider ${provider.id} already exists.`); diff --git a/src/vs/workbench/test/electron-browser/api/extHostApiCommands.test.ts b/src/vs/workbench/test/electron-browser/api/extHostApiCommands.test.ts index e8c7a63c6fb..3c1d07155f2 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostApiCommands.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostApiCommands.test.ts @@ -32,6 +32,7 @@ import { ExtHostDiagnostics } from 'vs/workbench/api/node/extHostDiagnostics'; import * as vscode from 'vscode'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import 'vs/workbench/parts/search/electron-browser/search.contribution'; +import { NoopLogService } from 'vs/platform/log/common/log'; const defaultSelector = { scheme: 'far' }; const model: EditorCommon.IModel = EditorModel.createFromString( @@ -112,8 +113,9 @@ suite('ExtHostLanguageFeatureCommands', function () { threadService.set(ExtHostContext.ExtHostDocuments, extHostDocuments); const heapService = new ExtHostHeapService(); + const logService = new NoopLogService(); - commands = new ExtHostCommands(threadService, heapService); + commands = new ExtHostCommands(threadService, heapService, logService); threadService.set(ExtHostContext.ExtHostCommands, commands); threadService.setTestInstance(MainContext.MainThreadCommands, inst.createInstance(MainThreadCommands, threadService)); ExtHostApiCommands.register(commands); diff --git a/src/vs/workbench/test/electron-browser/api/extHostCommands.test.ts b/src/vs/workbench/test/electron-browser/api/extHostCommands.test.ts index 69759480fc2..9ea21cf75fa 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostCommands.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostCommands.test.ts @@ -12,6 +12,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { OneGetThreadService } from './testThreadService'; import { mock } from 'vs/workbench/test/electron-browser/api/mock'; +import { NoopLogService } from 'vs/platform/log/common/log'; suite('ExtHostCommands', function () { @@ -29,7 +30,7 @@ suite('ExtHostCommands', function () { } }; - const commands = new ExtHostCommands(OneGetThreadService(shape), undefined); + const commands = new ExtHostCommands(OneGetThreadService(shape), undefined, new NoopLogService()); commands.registerCommand('foo', (): any => { }).dispose(); assert.equal(lastUnregister, 'foo'); assert.equal(CommandsRegistry.getCommand('foo'), undefined); @@ -50,7 +51,7 @@ suite('ExtHostCommands', function () { } }; - const commands = new ExtHostCommands(OneGetThreadService(shape), undefined); + const commands = new ExtHostCommands(OneGetThreadService(shape), undefined, new NoopLogService()); const reg = commands.registerCommand('foo', (): any => { }); reg.dispose(); reg.dispose(); diff --git a/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts b/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts index d6f2f03381b..e1fb7ffbb5d 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts @@ -44,6 +44,7 @@ import { ExtHostDiagnostics } from 'vs/workbench/api/node/extHostDiagnostics'; import { ExtHostHeapService } from 'vs/workbench/api/node/extHostHeapService'; import * as vscode from 'vscode'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { NoopLogService } from 'vs/platform/log/common/log'; const defaultSelector = { scheme: 'far' }; const model: EditorCommon.IModel = EditorModel.createFromString( @@ -102,7 +103,7 @@ suite('ExtHostLanguageFeatures', function () { const heapService = new ExtHostHeapService(); - const commands = new ExtHostCommands(threadService, heapService); + const commands = new ExtHostCommands(threadService, heapService, new NoopLogService()); threadService.set(ExtHostContext.ExtHostCommands, commands); threadService.setTestInstance(MainContext.MainThreadCommands, inst.createInstance(MainThreadCommands, threadService)); diff --git a/src/vs/workbench/test/electron-browser/api/extHostTreeViews.test.ts b/src/vs/workbench/test/electron-browser/api/extHostTreeViews.test.ts index 1eaf46831e4..85c50190733 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostTreeViews.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostTreeViews.test.ts @@ -17,6 +17,7 @@ import { TestInstantiationService } from 'vs/platform/instantiation/test/common/ import { MainThreadCommands } from 'vs/workbench/api/electron-browser/mainThreadCommands'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { mock } from 'vs/workbench/test/electron-browser/api/mock'; +import { NoopLogService } from 'vs/platform/log/common/log'; suite('ExtHostConfiguration', function () { @@ -48,7 +49,7 @@ suite('ExtHostConfiguration', function () { threadService.setTestInstance(MainContext.MainThreadCommands, inst.createInstance(MainThreadCommands, threadService)); target = new RecordingShape(); - testObject = new ExtHostTreeViews(target, new ExtHostCommands(threadService, new ExtHostHeapService())); + testObject = new ExtHostTreeViews(target, new ExtHostCommands(threadService, new ExtHostHeapService(), new NoopLogService())); onDidChangeTreeData = new Emitter(); testObject.registerTreeDataProvider('testDataProvider', aTreeDataProvider());