diff --git a/README.md b/README.md index 91566a493db..5d5ee55c63a 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ a code editor with what developers need for their core edit-build-debug cycle. Code provides comprehensive editing and debugging support, an extensibility model, and lightweight integration with existing tools. +VS Code is updated monthly with new features and bug fixes. You can download it for Windows, Mac and Linux on [VS Code's website](https://code.visualstudio.com/Download). To get the latest releases everyday, you can install the [Insiders version of VS Code](https://code.visualstudio.com/insiders). This builds from the master branch and is updated at least daily. +

VS Code in action

diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 5ecb9a1f315..404b81bfff5 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -269,7 +269,7 @@ function packageTask(platform, arch, opts) { .pipe(util.cleanNodeModule('native-keymap', ['binding.gyp', 'build/**', 'src/**', 'deps/**'], ['**/*.node'])) .pipe(util.cleanNodeModule('windows-foreground-love', ['binding.gyp', 'build/**', 'src/**'], ['**/*.node'])) .pipe(util.cleanNodeModule('gc-signals', ['binding.gyp', 'build/**', 'src/**', 'deps/**'], ['**/*.node', 'src/index.js'])) - .pipe(util.cleanNodeModule('pty.js', ['binding.gyp', 'build/**', 'src/**', 'deps/**'], ['build/Release/**'])); + .pipe(util.cleanNodeModule('node-pty', ['binding.gyp', 'build/**', 'src/**', 'deps/**'], ['build/Release/**'])); let all = es.merge( packageJsonStream, diff --git a/extensions/html/OSSREADME.json b/extensions/html/OSSREADME.json index 9949448fbcb..238e37e068f 100644 --- a/extensions/html/OSSREADME.json +++ b/extensions/html/OSSREADME.json @@ -1,7 +1,7 @@ // ATTENTION - THIS DIRECTORY CONTAINS THIRD PARTY OPEN SOURCE MATERIALS: [{ "name": "js-beautify", - "version": "1.6.4", + "version": "1.6.8", "license": "MIT", "repositoryURL": "https://github.com/beautify-web/js-beautify" },{ diff --git a/extensions/html/package.json b/extensions/html/package.json index b7d34877f26..db0bb0c3a0c 100644 --- a/extensions/html/package.json +++ b/extensions/html/package.json @@ -87,9 +87,17 @@ "string", "null" ], - "default": "a, abbr, acronym, b, bdo, big, br, button, cite, code, dfn, em, i, img, input, kbd, label, map, object, pre, q, samp, select, small, span, strong, sub, sup, textarea, tt, var", + "default": "a, abbr, acronym, b, bdo, big, br, button, cite, code, dfn, em, i, img, input, kbd, label, map, object, q, samp, select, small, span, strong, sub, sup, textarea, tt, var", "description": "%html.format.unformatted.desc%" }, + "html.format.contentUnformatted": { + "type": [ + "string", + "null" + ], + "default": "pre", + "description": "%html.format.contentUnformatted.desc%" + }, "html.format.indentInnerHtml": { "type": "boolean", "default": false, @@ -126,6 +134,13 @@ "default": "head, body, /html", "description": "%html.format.extraLiners.desc%" }, + "html.format.wrapAttributes": { + "type": "string", + "default": "auto", + "enum": [ "auto", "force", "force-align", "force-expand-multiline" ], + "enumDescriptions": ["%html.format.wrapAttributes.auto%", "%html.format.wrapAttributes.force%", "%html.format.wrapAttributes.forcealign%", "%html.format.wrapAttributes.forcemultiline%"], + "description": "%html.format.wrapAttributes.desc%" + }, "html.suggest.angular1": { "type": "boolean", "default": true, diff --git a/extensions/html/package.nls.json b/extensions/html/package.nls.json index f66e083890f..b27892fe954 100644 --- a/extensions/html/package.nls.json +++ b/extensions/html/package.nls.json @@ -2,12 +2,18 @@ "html.format.enable.desc": "Enable/disable default HTML formatter (requires restart)", "html.format.wrapLineLength.desc": "Maximum amount of characters per line (0 = disable).", "html.format.unformatted.desc": "List of tags, comma separated, that shouldn't be reformatted. 'null' defaults to all tags listed at https://www.w3.org/TR/html5/dom.html#phrasing-content.", + "html.format.contentUnformatted.desc": "List of tags, comma separated, where the content shouldn't be reformatted. 'null' defaults to the 'pre' tag.", "html.format.indentInnerHtml.desc": "Indent and sections.", "html.format.preserveNewLines.desc": "Whether existing line breaks before elements should be preserved. Only works before elements, not inside tags or for text.", "html.format.maxPreserveNewLines.desc": "Maximum number of line breaks to be preserved in one chunk. Use 'null' for unlimited.", "html.format.indentHandlebars.desc": "Format and indent {{#foo}} and {{/foo}}.", "html.format.endWithNewline.desc": "End with a newline.", "html.format.extraLiners.desc": "List of tags, comma separated, that should have an extra newline before them. 'null' defaults to \"head, body, /html\".", + "html.format.wrapAttributes.desc": "Wrap attributes.", + "html.format.wrapAttributes.auto": "Wrap attributes only when line length is exceeded.", + "html.format.wrapAttributes.force": "Wrap each attribute except first.", + "html.format.wrapAttributes.forcealign": "Wrap each attribute except first and keep aligned.", + "html.format.wrapAttributes.forcemultiline": "Wrap each attribute.", "html.suggest.angular1.desc": "Configures if the built-in HTML language support suggests Angular V1 tags and properties.", "html.suggest.ionic.desc": "Configures if the built-in HTML language support suggests Ionic tags, properties and values.", "html.suggest.html5.desc":"Configures if the built-in HTML language support suggests HTML5 tags, properties and values.", diff --git a/extensions/html/server/npm-shrinkwrap.json b/extensions/html/server/npm-shrinkwrap.json index be8c7759112..598012b6d8b 100644 --- a/extensions/html/server/npm-shrinkwrap.json +++ b/extensions/html/server/npm-shrinkwrap.json @@ -2,15 +2,20 @@ "name": "vscode-html-languageserver", "version": "1.0.0", "dependencies": { + "@types/node": { + "version": "6.0.58", + "from": "@types/node@>=6.0.51 <7.0.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.58.tgz" + }, "vscode-css-languageservice": { "version": "2.0.0-next.6", "from": "vscode-css-languageservice@next", "resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-2.0.0-next.6.tgz" }, "vscode-html-languageservice": { - "version": "2.0.0-next.3", + "version": "2.0.0-next.5", "from": "vscode-html-languageservice@next", - "resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-2.0.0-next.3.tgz" + "resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-2.0.0-next.5.tgz" }, "vscode-jsonrpc": { "version": "3.0.1-alpha.2", diff --git a/extensions/html/server/package.json b/extensions/html/server/package.json index e6d4b8cf772..fc6d4a05e98 100644 --- a/extensions/html/server/package.json +++ b/extensions/html/server/package.json @@ -9,7 +9,7 @@ }, "dependencies": { "vscode-css-languageservice": "^2.0.0-next.6", - "vscode-html-languageservice": "^2.0.0-next.3", + "vscode-html-languageservice": "^2.0.0-next.5", "vscode-languageserver": "3.0.1-alpha.2", "vscode-nls": "^1.0.7", "vscode-uri": "^1.0.0" diff --git a/extensions/javascript/language-configuration.json b/extensions/javascript/javascript-language-configuration.json similarity index 100% rename from extensions/javascript/language-configuration.json rename to extensions/javascript/javascript-language-configuration.json diff --git a/extensions/javascript/package.json b/extensions/javascript/package.json index b08729bbe12..8385c006d33 100644 --- a/extensions/javascript/package.json +++ b/extensions/javascript/package.json @@ -27,7 +27,7 @@ "extensions": [ ".jsx" ], - "configuration": "./language-configuration.json" + "configuration": "./javascript-language-configuration.json" }, { "id": "javascript", @@ -47,19 +47,31 @@ "mimetypes": [ "text/javascript" ], - "configuration": "./language-configuration.json" + "configuration": "./javascript-language-configuration.json" + }, + { + "id": "jsx-tags", + "configuration": "./tags-language-configuration.json" } ], "grammars": [ { "language": "javascriptreact", "scopeName": "source.js", - "path": "./syntaxes/JavaScript.tmLanguage.json" + "path": "./syntaxes/JavaScript.tmLanguage.json", + "embeddedLanguages": { + "meta.tag.js": "jsx-tags", + "meta.tag.without-attributes.js": "jsx-tags" + } }, { "language": "javascript", "scopeName": "source.js", - "path": "./syntaxes/JavaScript.tmLanguage.json" + "path": "./syntaxes/JavaScript.tmLanguage.json", + "embeddedLanguages": { + "meta.tag.js": "jsx-tags", + "meta.tag.without-attributes.js": "jsx-tags" + } }, { "scopeName": "source.js.regexp", diff --git a/extensions/javascript/tags-language-configuration.json b/extensions/javascript/tags-language-configuration.json new file mode 100644 index 00000000000..fa04cf1756f --- /dev/null +++ b/extensions/javascript/tags-language-configuration.json @@ -0,0 +1,27 @@ +{ + "comments": { + "blockComment": [ "{/*", "*/}" ] + }, + "brackets": [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + ["<", ">"] + ], + "autoClosingPairs": [ + { "open": "{", "close": "}" }, + { "open": "[", "close": "]" }, + { "open": "(", "close": ")" }, + { "open": "'", "close": "'", "notIn": ["string", "comment"] }, + { "open": "\"", "close": "\"", "notIn": ["string"] }, + { "open": "/**", "close": " */", "notIn": ["string"] } + ], + "surroundingPairs": [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + ["<", ">"], + ["'", "'"], + ["\"", "\""] + ] +} \ No newline at end of file diff --git a/extensions/json/server/npm-shrinkwrap.json b/extensions/json/server/npm-shrinkwrap.json index f65223e7872..c5d6a6aa9cf 100644 --- a/extensions/json/server/npm-shrinkwrap.json +++ b/extensions/json/server/npm-shrinkwrap.json @@ -43,9 +43,9 @@ "resolved": "https://registry.npmjs.org/request-light/-/request-light-0.1.0.tgz" }, "vscode-json-languageservice": { - "version": "2.0.0-next.8", + "version": "2.0.0-next.9", "from": "vscode-json-languageservice@next", - "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-2.0.0-next.8.tgz" + "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-2.0.0-next.9.tgz" }, "vscode-jsonrpc": { "version": "3.0.1-alpha.2", diff --git a/extensions/json/server/package.json b/extensions/json/server/package.json index 3b4d77f71e3..2ec6d081bca 100644 --- a/extensions/json/server/package.json +++ b/extensions/json/server/package.json @@ -9,7 +9,7 @@ }, "dependencies": { "request-light": "^0.1.0", - "vscode-json-languageservice": "^2.0.0-next.8", + "vscode-json-languageservice": "^2.0.0-next.9", "vscode-languageserver": "3.0.1-alpha.3", "vscode-nls": "^1.0.7" }, diff --git a/extensions/typescript/package.json b/extensions/typescript/package.json index 201ddf039de..ae1c645c03e 100644 --- a/extensions/typescript/package.json +++ b/extensions/typescript/package.json @@ -68,7 +68,11 @@ { "language": "typescriptreact", "scopeName": "source.tsx", - "path": "./syntaxes/TypeScriptReact.tmLanguage.json" + "path": "./syntaxes/TypeScriptReact.tmLanguage.json", + "embeddedLanguages": { + "meta.tag.tsx": "jsx-tags", + "meta.tag.without-attributes.tsx": "jsx-tags" + } } ], "configuration": { @@ -99,6 +103,11 @@ "default": true, "description": "%typescript.check.tscVersion%" }, + "typescript.referencesCodeLens.enabled": { + "type": "boolean", + "default": false, + "description": "%typescript.referencesCodeLens.enabled%" + }, "typescript.tsserver.trace": { "type": "string", "enum": [ diff --git a/extensions/typescript/package.nls.json b/extensions/typescript/package.nls.json index 51b9649ab91..1834876a6d9 100644 --- a/extensions/typescript/package.nls.json +++ b/extensions/typescript/package.nls.json @@ -24,5 +24,6 @@ "format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": "Defines space handling after opening and before closing JSX expression braces. Requires TypeScript >= 2.0.6.", "format.placeOpenBraceOnNewLineForFunctions": "Defines whether an open brace is put onto a new line for functions or not.", "format.placeOpenBraceOnNewLineForControlBlocks": "Defines whether an open brace is put onto a new line for control blocks or not.", - "javascript.validate.enable": "Enable/disable JavaScript validation." + "javascript.validate.enable": "Enable/disable JavaScript validation.", + "typescript.referencesCodeLens.enabled": "Enable/disable the references code lens" } \ No newline at end of file diff --git a/extensions/typescript/src/features/referencesCodeLensProvider.ts b/extensions/typescript/src/features/referencesCodeLensProvider.ts new file mode 100644 index 00000000000..145859cecb2 --- /dev/null +++ b/extensions/typescript/src/features/referencesCodeLensProvider.ts @@ -0,0 +1,144 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { CodeLensProvider, CodeLens, CancellationToken, TextDocument, Range, Uri, Location, Position, workspace, WorkspaceConfiguration } from 'vscode'; +import * as Proto from '../protocol'; +import * as PConst from '../protocol.const'; + +import { ITypescriptServiceClient } from '../typescriptService'; + +import * as nls from 'vscode-nls'; +let localize = nls.loadMessageBundle(); + + +class ReferencesCodeLens extends CodeLens { + public document: Uri; + public file: string; + + constructor(document: Uri, file: string, range: Range) { + super(range); + this.document = document; + this.file = file; + } +} + +export default class TypeScriptReferencesCodeLensProvider implements CodeLensProvider { + private client: ITypescriptServiceClient; + private enabled = false; + + constructor(client: ITypescriptServiceClient) { + this.client = client; + } + + public updateConfiguration(config: WorkspaceConfiguration): void { + let typeScriptConfig = workspace.getConfiguration('typescript'); + this.enabled = typeScriptConfig.get('referencesCodeLens.enabled', false); + } + + provideCodeLenses(document: TextDocument, token: CancellationToken): Promise { + if (!this.enabled) { + return Promise.resolve([]); + } + + const filepath = this.client.asAbsolutePath(document.uri); + if (!filepath) { + return Promise.resolve([]); + } + return this.client.execute('navtree', { file: filepath }, token).then(response => { + const tree = response.body; + const referenceableSpans: Range[] = []; + if (tree && tree.childItems) { + tree.childItems.forEach(item => this.extractReferenceableSymbols(document, item, referenceableSpans)); + } + return Promise.resolve(referenceableSpans.map(span => new ReferencesCodeLens(document.uri, filepath, span))); + }); + } + + resolveCodeLens(inputCodeLens: CodeLens, token: CancellationToken): Promise { + const codeLens = inputCodeLens as ReferencesCodeLens; + if (!codeLens.document) { + return Promise.reject(codeLens); + } + const args: Proto.FileLocationRequestArgs = { + file: codeLens.file, + line: codeLens.range.start.line + 1, + offset: codeLens.range.start.character + 1 + }; + return this.client.execute('references', args, token).then(response => { + if (response && response.body) { + const referenceCount = Math.max(0, response.body.refs.length - 1); + const locations = response.body.refs.map(reference => + new Location(Uri.file(reference.file), + new Range( + new Position(reference.start.line - 1, reference.start.offset - 1), + new Position(reference.end.line - 1, reference.end.offset - 1)))); + + codeLens.command = { + title: referenceCount + ' ' + (referenceCount === 1 ? localize('oneReferenceLabel', 'reference') : localize('manyReferenceLabel', 'references')), + command: 'editor.action.showReferences', + arguments: [codeLens.document, codeLens.range.start, locations] + }; + return Promise.resolve(codeLens); + } + return Promise.reject(codeLens); + }).catch(() => { + codeLens.command = { + title: localize('referenceErrorLabel', 'Could not determine references'), + command: '' + }; + return Promise.resolve(codeLens); + }); + } + + private extractReferenceableSymbols(document: TextDocument, item: Proto.NavigationTree, results: Range[]) { + if (!item) { + return; + } + + const span = item.spans && item.spans[0]; + if (span) { + const range = new Range( + new Position(span.start.line - 1, span.start.offset - 1), + new Position(span.end.line - 1, span.end.offset - 1)); + + // TODO: TS currently requires the position for 'references 'to be inside of the identifer + // Massage the range to make sure this is the case + const text = document.getText(range); + + switch (item.kind) { + case PConst.Kind.const: + case PConst.Kind.let: + case PConst.Kind.variable: + case PConst.Kind.function: + // Only show references for exported variables + if (!item.kindModifiers.match(/\bexport\b/)) { + break; + } + // fallthrough + + case PConst.Kind.memberFunction: + case PConst.Kind.memberVariable: + case PConst.Kind.memberGetAccessor: + case PConst.Kind.memberSetAccessor: + case PConst.Kind.constructorImplementation: + case PConst.Kind.class: + case PConst.Kind.interface: + case PConst.Kind.type: + case PConst.Kind.enum: + const identifierMatch = new RegExp(`^(.*?(\\b|\\W))${item.text}`, 'g'); + const match = identifierMatch.exec(text); + const start = match ? match.index + match[1].length : 0; + results.push(new Range( + new Position(range.start.line, range.start.character + start), + new Position(range.start.line, range.start.character + start + item.text.length))); + break; + } + } + + (item.childItems || []).forEach(item => this.extractReferenceableSymbols(document, item, results)); + } +}; \ No newline at end of file diff --git a/extensions/typescript/src/typescriptMain.ts b/extensions/typescript/src/typescriptMain.ts index 16080631a3d..02891af3f94 100644 --- a/extensions/typescript/src/typescriptMain.ts +++ b/extensions/typescript/src/typescriptMain.ts @@ -35,6 +35,7 @@ import BufferSyncSupport from './features/bufferSyncSupport'; import CompletionItemProvider from './features/completionItemProvider'; import WorkspaceSymbolProvider from './features/workspaceSymbolProvider'; import CodeActionProvider from './features/codeActionProvider'; +import ReferenceCodeLensProvider from './features/referencesCodeLensProvider'; import * as BuildStatus from './utils/buildStatus'; import * as ProjectStatus from './utils/projectStatus'; @@ -107,6 +108,7 @@ class LanguageProvider { private formattingProvider: FormattingProvider; private formattingProviderRegistration: Disposable | null; private typingsStatus: TypingsStatus; + private referenceCodeLensProvider: ReferenceCodeLensProvider; private _validate: boolean; @@ -156,6 +158,12 @@ class LanguageProvider { this.formattingProviderRegistration = languages.registerDocumentRangeFormattingEditProvider(this.description.modeIds, this.formattingProvider); } + this.referenceCodeLensProvider = new ReferenceCodeLensProvider(client); + this.referenceCodeLensProvider.updateConfiguration(config); + if (client.apiVersion.has206Features()) { + languages.registerCodeLensProvider(this.description.modeIds, this.referenceCodeLensProvider); + } + this.description.modeIds.forEach(modeId => { let selector: DocumentFilter = { scheme: 'file', language: modeId }; languages.registerCompletionItemProvider(selector, this.completionItemProvider, '.'); @@ -171,6 +179,7 @@ class LanguageProvider { if (client.apiVersion.has213Features()) { languages.registerCodeActionsProvider(selector, new CodeActionProvider(client, modeId)); } + languages.setLanguageConfiguration(modeId, { indentationRules: { // ^(.*\*/)?\s*\}.*$ @@ -217,6 +226,9 @@ class LanguageProvider { if (this.completionItemProvider) { this.completionItemProvider.updateConfiguration(config); } + if (this.referenceCodeLensProvider) { + this.referenceCodeLensProvider.updateConfiguration(config); + } if (this.formattingProvider) { this.formattingProvider.updateConfiguration(config); if (!this.formattingProvider.isEnabled() && this.formattingProviderRegistration) { diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index cc2ddaa5560..66e8c0861dd 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -332,10 +332,10 @@ "from": "process-nextick-args@>=1.0.6 <1.1.0", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz" }, - "pty.js": { - "version": "0.3.0", - "from": "https://github.com/Tyriar/pty.js/tarball/c75c2dcb6dcad83b0cb3ef2ae42d0448fb912642", - "resolved": "https://github.com/Tyriar/pty.js/tarball/c75c2dcb6dcad83b0cb3ef2ae42d0448fb912642", + "node-pty": { + "version": "0.4.1", + "from": "node-pty@0.4.1", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-0.4.1.tgz", "dependencies": { "extend": { "version": "1.2.1", @@ -430,9 +430,9 @@ "resolved": "https://registry.npmjs.org/winreg/-/winreg-1.2.0.tgz" }, "xterm": { - "version": "2.2.0", + "version": "2.2.3", "from": "git+https://github.com/Tyriar/xterm.js.git#vscode-release/1.9", - "resolved": "git+https://github.com/Tyriar/xterm.js.git#618e6bbd0a0ebaabc4b06ab10bea89768ded62b5" + "resolved": "git+https://github.com/Tyriar/xterm.js.git#01dd436a56ee2370fa9b5aa5bc2e138b22799eda" }, "yauzl": { "version": "2.3.1", diff --git a/package.json b/package.json index 529cb07f99c..f3f4fb2a155 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "iconv-lite": "0.4.15", "minimist": "1.2.0", "native-keymap": "0.3.0", - "pty.js": "https://github.com/Tyriar/pty.js/tarball/c75c2dcb6dcad83b0cb3ef2ae42d0448fb912642", + "node-pty": "0.4.1", "semver": "4.3.6", "vscode-debugprotocol": "1.15.0", "vscode-textmate": "3.1.0", diff --git a/src/typings/pty.js.d.ts b/src/typings/node-pty.d.ts similarity index 96% rename from src/typings/pty.js.d.ts rename to src/typings/node-pty.d.ts index cc726238aa2..ef417f1156e 100644 --- a/src/typings/pty.js.d.ts +++ b/src/typings/node-pty.d.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -declare module 'pty.js' { +declare module 'node-pty' { export function fork(file: string, args: string[], options: any): Terminal; export function spawn(file: string, args: string[], options: any): Terminal; export function createTerminal(file: string, args: string[], options: any): Terminal; diff --git a/src/vs/base/common/json.ts b/src/vs/base/common/json.ts index a22159dfc85..f8f7a40075b 100644 --- a/src/vs/base/common/json.ts +++ b/src/vs/base/common/json.ts @@ -1043,9 +1043,6 @@ export function visit(text: string, visitor: JSONVisitor, options?: ParseOptions } function parseString(isValue: boolean): boolean { - if (_scanner.getToken() !== SyntaxKind.StringLiteral) { - return false; - } let value = _scanner.getTokenValue(); if (isValue) { onLiteralValue(value); @@ -1088,10 +1085,11 @@ export function visit(text: string, visitor: JSONVisitor, options?: ParseOptions } function parseProperty(): boolean { - if (!parseString(false)) { + if (_scanner.getToken() !== SyntaxKind.StringLiteral) { handleError(ParseErrorCode.PropertyNameExpected, [], [SyntaxKind.CloseBraceToken, SyntaxKind.CommaToken]); return false; } + parseString(false); if (_scanner.getToken() === SyntaxKind.ColonToken) { onSeparator(':'); scanNext(); // consume colon @@ -1106,9 +1104,6 @@ export function visit(text: string, visitor: JSONVisitor, options?: ParseOptions } function parseObject(): boolean { - if (_scanner.getToken() !== SyntaxKind.OpenBraceToken) { - return false; - } onObjectBegin(); scanNext(); // consume open brace @@ -1138,9 +1133,6 @@ export function visit(text: string, visitor: JSONVisitor, options?: ParseOptions } function parseArray(): boolean { - if (_scanner.getToken() !== SyntaxKind.OpenBracketToken) { - return false; - } onArrayBegin(); scanNext(); // consume open bracket @@ -1170,7 +1162,16 @@ export function visit(text: string, visitor: JSONVisitor, options?: ParseOptions } function parseValue(): boolean { - return parseArray() || parseObject() || parseString(true) || parseLiteral(); + switch (_scanner.getToken()) { + case SyntaxKind.OpenBracketToken: + return parseArray(); + case SyntaxKind.OpenBraceToken: + return parseObject(); + case SyntaxKind.StringLiteral: + return parseString(true); + default: + return parseLiteral(); + } } scanNext(); diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 5eb0da6e884..ef6a310c847 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -261,8 +261,7 @@ function main(accessor: ServicesAccessor, mainIpcServer: Server, userEnv: platfo } // Install Menu - const menu = instantiationService2.createInstance(VSCodeMenu); - menu.ready(); + instantiationService2.createInstance(VSCodeMenu); }); } diff --git a/src/vs/code/electron-main/menus.ts b/src/vs/code/electron-main/menus.ts index 7bfc3bc7ec3..df488646937 100644 --- a/src/vs/code/electron-main/menus.ts +++ b/src/vs/code/electron-main/menus.ts @@ -21,6 +21,10 @@ import { Keybinding } from 'vs/base/common/keyCodes'; import { KeybindingLabels } from 'vs/base/common/keybinding'; import product from 'vs/platform/node/product'; import { RunOnceScheduler } from 'vs/base/common/async'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import Event, { Emitter, once } from 'vs/base/common/event'; +import { ConfigWatcher } from 'vs/base/node/config'; +import { IUserFriendlyKeybinding } from 'vs/platform/keybinding/common/keybinding'; interface IResolvedKeybinding { id: string; @@ -46,10 +50,97 @@ interface IConfiguration extends IFilesConfiguration { }; } -export class VSCodeMenu { +class KeybindingsResolver { private static lastKnownKeybindingsMapStorageKey = 'lastKnownKeybindings'; + private commandIds: Set; + private keybindings: { [commandId: string]: string }; + private keybindingsWatcher: ConfigWatcher; + + private _onKeybindingsChanged = new Emitter(); + onKeybindingsChanged: Event = this._onKeybindingsChanged.event; + + constructor( + @IStorageService private storageService: IStorageService, + @IEnvironmentService environmentService: IEnvironmentService, + @IWindowsMainService private windowsService: IWindowsMainService + ) { + this.commandIds = new Set(); + this.keybindings = this.storageService.getItem<{ [id: string]: string; }>(KeybindingsResolver.lastKnownKeybindingsMapStorageKey) || Object.create(null); + this.keybindingsWatcher = new ConfigWatcher(environmentService.appKeybindingsPath, { changeBufferDelay: 1000 /* update after 1s */ }); + + this.registerListeners(); + } + + private registerListeners(): void { + + // Resolve keybindings when any first window is loaded + const onceOnWindowReady = once(this.windowsService.onWindowReady); + onceOnWindowReady(win => this.resolveKeybindings(win)); + + // Listen to resolved keybindings from window + ipc.on('vscode:keybindingsResolved', (event, rawKeybindings: string) => { + let keybindings: IResolvedKeybinding[] = []; + try { + keybindings = JSON.parse(rawKeybindings); + } catch (error) { + // Should not happen + } + + // Fill hash map of resolved keybindings and check for changes + let keybindingsChanged = false; + let keybindingsCount = 0; + keybindings.forEach(keybinding => { + const accelerator = KeybindingLabels._toElectronAccelerator(new Keybinding(keybinding.binding)); + if (accelerator) { + keybindingsCount++; + + if (accelerator !== this.keybindings[keybinding.id]) { + this.keybindings[keybinding.id] = accelerator; + keybindingsChanged = true; + } + } + }); + + // A keybinding might have been unassigned, so we have to account for that too + if (Object.keys(this.keybindings).length !== keybindingsCount) { + keybindingsChanged = true; + } + + if (keybindingsChanged) { + this.storageService.setItem(KeybindingsResolver.lastKnownKeybindingsMapStorageKey, this.keybindings); // keep to restore instantly after restart + + this._onKeybindingsChanged.fire(); + } + }); + + // Resolve keybindings again when keybindings.json changes + this.keybindingsWatcher.onDidUpdateConfiguration(() => this.resolveKeybindings()); + + // Resolve keybindings when window reloads because an installed extension could have an impact + this.windowsService.onWindowReload(() => this.resolveKeybindings()); + } + + private resolveKeybindings(win: VSCodeWindow = this.windowsService.getLastActiveWindow()): void { + if (this.commandIds.size && win) { + const commandIds = []; + this.commandIds.forEach(id => commandIds.push(id)); + win.sendWhenReady('vscode:resolveKeybindings', JSON.stringify(commandIds)); + } + } + + public getKeybinding(commandId: string): string { + if (!this.commandIds.has(commandId)) { + this.commandIds.add(commandId); + } + + return this.keybindings[commandId]; + } +} + +export class VSCodeMenu { + private static MAX_MENU_RECENT_ENTRIES = 10; private currentAutoSaveSetting: string; @@ -62,35 +153,28 @@ export class VSCodeMenu { private menuUpdater: RunOnceScheduler; - private actionIdKeybindingRequests: string[]; - private mapLastKnownKeybindingToActionId: { [id: string]: string; }; - private mapResolvedKeybindingToActionId: { [id: string]: string; }; - private keybindingsResolved: boolean; + private keybindingsResolver: KeybindingsResolver; private extensionViewlets: IExtensionViewlet[]; constructor( - @IStorageService private storageService: IStorageService, @IUpdateService private updateService: IUpdateService, + @IInstantiationService instantiationService: IInstantiationService, @IConfigurationService private configurationService: IConfigurationService, @IWindowsMainService private windowsService: IWindowsMainService, @IEnvironmentService private environmentService: IEnvironmentService, @ITelemetryService private telemetryService: ITelemetryService ) { - this.actionIdKeybindingRequests = []; this.extensionViewlets = []; - this.mapResolvedKeybindingToActionId = Object.create(null); - this.mapLastKnownKeybindingToActionId = this.storageService.getItem<{ [id: string]: string; }>(VSCodeMenu.lastKnownKeybindingsMapStorageKey) || Object.create(null); - this.menuUpdater = new RunOnceScheduler(() => this.doUpdateMenu(), 0); + this.keybindingsResolver = instantiationService.createInstance(KeybindingsResolver); this.onConfigurationUpdated(this.configurationService.getConfiguration()); - } - public ready(): void { - this.registerListeners(); this.install(); + + this.registerListeners(); } private registerListeners(): void { @@ -105,43 +189,6 @@ export class VSCodeMenu { this.windowsService.onRecentPathsChange(paths => this.updateMenu()); this.windowsService.onWindowClose(_ => this.onClose(this.windowsService.getWindowCount())); - // Resolve keybindings when any first workbench is loaded - this.windowsService.onWindowReady(win => this.resolveKeybindings(win)); - - // Listen to resolved keybindings - ipc.on('vscode:keybindingsResolved', (event, rawKeybindings) => { - let keybindings: IResolvedKeybinding[] = []; - try { - keybindings = JSON.parse(rawKeybindings); - } catch (error) { - // Should not happen - } - - // Fill hash map of resolved keybindings - let needsMenuUpdate = false; - keybindings.forEach(keybinding => { - const accelerator = KeybindingLabels._toElectronAccelerator(new Keybinding(keybinding.binding)); - if (accelerator) { - this.mapResolvedKeybindingToActionId[keybinding.id] = accelerator; - if (this.mapLastKnownKeybindingToActionId[keybinding.id] !== accelerator) { - needsMenuUpdate = true; // we only need to update when something changed! - } - } - }); - - // A keybinding might have been unassigned, so we have to account for that too - if (Object.keys(this.mapLastKnownKeybindingToActionId).length !== Object.keys(this.mapResolvedKeybindingToActionId).length) { - needsMenuUpdate = true; - } - - if (needsMenuUpdate) { - this.storageService.setItem(VSCodeMenu.lastKnownKeybindingsMapStorageKey, this.mapResolvedKeybindingToActionId); // keep to restore instantly after restart - this.mapLastKnownKeybindingToActionId = this.mapResolvedKeybindingToActionId; // update our last known map - - this.updateMenu(); - } - }); - // Listen to extension viewlets ipc.on('vscode:extensionViewlets', (event, rawExtensionViewlets) => { let extensionViewlets: IExtensionViewlet[] = []; @@ -162,6 +209,9 @@ export class VSCodeMenu { // Listen to update service this.updateService.onStateChange(() => this.updateMenu()); + + // Listen to keybindings change + this.keybindingsResolver.onKeybindingsChanged(() => this.updateMenu()); } private onConfigurationUpdated(config: IConfiguration, handleMenu?: boolean): void { @@ -201,19 +251,6 @@ export class VSCodeMenu { } } - private resolveKeybindings(win: VSCodeWindow): void { - if (this.keybindingsResolved) { - return; // only resolve once - } - - this.keybindingsResolved = true; - - // Resolve keybindings when workbench window is up - if (this.actionIdKeybindingRequests.length) { - win.send('vscode:resolveKeybindings', JSON.stringify(this.actionIdKeybindingRequests)); - } - } - private updateMenu(): void { this.menuUpdater.schedule(); // buffer multiple attempts to update the menu } @@ -970,19 +1007,7 @@ export class VSCodeMenu { private getAccelerator(actionId: string, fallback?: string): string { if (actionId) { - const resolvedKeybinding = this.mapResolvedKeybindingToActionId[actionId]; - if (resolvedKeybinding) { - return resolvedKeybinding; // keybinding is fully resolved - } - - if (!this.keybindingsResolved) { - this.actionIdKeybindingRequests.push(actionId); // keybinding needs to be resolved - } - - const lastKnownKeybinding = this.mapLastKnownKeybindingToActionId[actionId]; - if (lastKnownKeybinding) { - return lastKnownKeybinding; // return the last known keybining (chance of mismatch is very low unless it changed) - } + return this.keybindingsResolver.getKeybinding(actionId); } return fallback; diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index dd967a6e14c..67e2d66ac62 100644 --- a/src/vs/code/electron-main/window.ts +++ b/src/vs/code/electron-main/window.ts @@ -8,6 +8,7 @@ import * as path from 'path'; import * as platform from 'vs/base/common/platform'; import * as objects from 'vs/base/common/objects'; +import nls = require('vs/nls'); import { IStorageService } from 'vs/code/electron-main/storage'; import { shell, screen, BrowserWindow, systemPreferences, app } from 'electron'; import { TPromise, TValueCallback } from 'vs/base/common/winjs.base'; @@ -117,6 +118,12 @@ export enum ReadyState { READY } +interface IConfiguration { + window: { + menuBarVisibility: 'visible' | 'toggle' | 'hidden'; + }; +} + export interface IVSCodeWindow { id: number; readyState: ReadyState; @@ -127,7 +134,6 @@ export interface IVSCodeWindow { export class VSCodeWindow implements IVSCodeWindow { - public static menuBarHiddenKey = 'menuBarHidden'; public static colorThemeStorageKey = 'theme'; private static MIN_WIDTH = 200; @@ -143,6 +149,7 @@ export class VSCodeWindow implements IVSCodeWindow { private _extensionDevelopmentPath: string; private _isExtensionTestHost: boolean; private windowState: IWindowState; + private currentMenuBarVisibility: 'visible' | 'toggle' | 'hidden'; private currentWindowMode: WindowMode; private whenReadyCallbacks: TValueCallback[]; @@ -220,15 +227,14 @@ export class VSCodeWindow implements IVSCodeWindow { this._lastFocusTime = Date.now(); // since we show directly, we need to set the last focus time too - if (this.storageService.getItem(VSCodeWindow.menuBarHiddenKey, false)) { - this.setMenuBarVisibility(false); // respect configured menu bar visibility - } + // respect configured menu bar visibility + this.onConfigurationUpdated(this.configurationService.getConfiguration()); - // TODO@joao: hook this up to some initialization routine - // this causes a race between setting the headers and doing + // TODO@joao: hook this up to some initialization routine this causes a race between setting the headers and doing // a request that needs them. chances are low this.setCommonHTTPHeaders(); + // Eventing this.registerListeners(); } @@ -407,8 +413,19 @@ export class VSCodeWindow implements IVSCodeWindow { } }); } + + // Handle configuration changes + this.configurationService.onDidUpdateConfiguration(e => this.onConfigurationUpdated(e.config)); } + private onConfigurationUpdated(config: IConfiguration): void { + const newMenuBarVisibility = this.getMenuBarVisibility(config); + if (newMenuBarVisibility !== this.currentMenuBarVisibility) { + this.currentMenuBarVisibility = newMenuBarVisibility; + this.setMenuBarVisibility(newMenuBarVisibility); + } + }; + public load(config: IWindowConfiguration): void { // If this is the first time the window is loaded, we associate the paths @@ -656,21 +673,53 @@ export class VSCodeWindow implements IVSCodeWindow { public toggleFullScreen(): void { const willBeFullScreen = !this.win.isFullScreen(); + // set fullscreen flag on window this.win.setFullScreen(willBeFullScreen); - // Windows & Linux: Hide the menu bar but still allow to bring it up by pressing the Alt key - if (platform.isWindows || platform.isLinux) { - if (willBeFullScreen) { - this.setMenuBarVisibility(false); - } else { - this.setMenuBarVisibility(!this.storageService.getItem(VSCodeWindow.menuBarHiddenKey, false)); // restore as configured - } - } + // respect configured menu bar visibility or default to toggle if not set + this.setMenuBarVisibility(this.getMenuBarVisibility(this.configurationService.getConfiguration(), willBeFullScreen ? 'toggle' : 'visible'), false); } - public setMenuBarVisibility(visible: boolean): void { - this.win.setMenuBarVisibility(visible); - this.win.setAutoHideMenuBar(!visible); + private getMenuBarVisibility(configuration: IConfiguration, fallback: 'visible' | 'toggle' | 'hidden' = 'visible'): 'visible' | 'toggle' | 'hidden' { + const windowConfig = this.configurationService.getConfiguration('window'); + + if (!windowConfig || !windowConfig.menuBarVisibility) { + return fallback; + } + + let menuBarVisibility = windowConfig.menuBarVisibility; + if (['visible', 'toggle', 'hidden'].indexOf(menuBarVisibility) < 0) { + menuBarVisibility = fallback; + } + + return menuBarVisibility; + } + + public setMenuBarVisibility(visibility: 'visible' | 'toggle' | 'hidden', notify: boolean = true): void { + if (platform.isMacintosh) { + return; // ignore for macOS platform + } + + switch (visibility) { + case ('visible'): + this.win.setMenuBarVisibility(true); + this.win.setAutoHideMenuBar(false); + break; + + case ('toggle'): + this.win.setMenuBarVisibility(false); + this.win.setAutoHideMenuBar(true); + + if (notify) { + this.send('vscode:showInfoMessage', nls.localize('hiddenMenuBar', "You can still access the menu bar by pressing the **Alt** key.")); + }; + break; + + case ('hidden'): + this.win.setMenuBarVisibility(false); + this.win.setAutoHideMenuBar(false); + break; + }; } public sendWhenReady(channel: string, ...args: any[]): void { diff --git a/src/vs/code/electron-main/windows.ts b/src/vs/code/electron-main/windows.ts index 7c442c543d9..f9084d15803 100644 --- a/src/vs/code/electron-main/windows.ts +++ b/src/vs/code/electron-main/windows.ts @@ -104,6 +104,7 @@ export interface IWindowsMainService { // events onWindowReady: CommonEvent; onWindowClose: CommonEvent; + onWindowReload: CommonEvent; onPathsOpen: CommonEvent; onRecentPathsChange: CommonEvent; @@ -131,7 +132,6 @@ export interface IWindowsMainService { removeFromRecentPathsList(path: string): void; removeFromRecentPathsList(paths: string[]): void; clearRecentPathsList(): void; - toggleMenuBar(windowId: number): void; quit(): void; } @@ -159,6 +159,9 @@ export class WindowsManager implements IWindowsMainService { private _onWindowClose = new Emitter(); onWindowClose: CommonEvent = this._onWindowClose.event; + private _onWindowReload = new Emitter(); + onWindowReload: CommonEvent = this._onWindowReload.event; + private _onPathsOpen = new Emitter(); onPathsOpen: CommonEvent = this._onPathsOpen.event; @@ -292,6 +295,9 @@ export class WindowsManager implements IWindowsMainService { this.lifecycleService.unload(win, UnloadReason.RELOAD).done(veto => { if (!veto) { win.reload(cli); + + // Emit + this._onWindowReload.fire(win.id); } }); } @@ -1176,24 +1182,6 @@ export class WindowsManager implements IWindowsMainService { return pathA === pathB; } - public toggleMenuBar(windowId: number): void { - // Update in settings - const menuBarHidden = this.storageService.getItem(VSCodeWindow.menuBarHiddenKey, false); - const newMenuBarHidden = !menuBarHidden; - this.storageService.setItem(VSCodeWindow.menuBarHiddenKey, newMenuBarHidden); - - // Update across windows - WindowsManager.WINDOWS.forEach(w => w.setMenuBarVisibility(!newMenuBarHidden)); - - // Inform user if menu bar is now hidden - if (newMenuBarHidden) { - const vscodeWindow = this.getWindowById(windowId); - if (vscodeWindow) { - vscodeWindow.send('vscode:showInfoMessage', nls.localize('hiddenMenuBar', "You can still access the menu bar by pressing the **Alt** key.")); - } - } - } - private updateWindowsJumpList(): void { if (!platform.isWindows) { return; // only on windows diff --git a/src/vs/editor/common/model/wordHelper.ts b/src/vs/editor/common/model/wordHelper.ts index a2e4e65ddbd..8ced8f8c91f 100644 --- a/src/vs/editor/common/model/wordHelper.ts +++ b/src/vs/editor/common/model/wordHelper.ts @@ -54,38 +54,69 @@ export function ensureValidWordDefinition(wordDefinition?: RegExp): RegExp { return result; } -export function getWordAtText(column: number, wordDefinition: RegExp, text: string, textOffset: number): IWordAtPosition { +function getWordAtPosFast(column: number, wordDefinition: RegExp, text: string, textOffset: number): IWordAtPosition { + // find whitespace enclosed text around column and match from there - // console.log('_getWordAtText: ', column, text, textOffset); + if (wordDefinition.test(' ')) { + return getWordAtPosSlow(column, wordDefinition, text, textOffset); + } - var words = text.match(wordDefinition), - k: number, - startWord: number, - endWord: number, - startColumn: number, - endColumn: number, - word: string; + let pos = column - 1 - textOffset; + let start = text.lastIndexOf(' ', pos - 1) + 1; + let end = text.indexOf(' ', pos); + if (end === -1) { + end = text.length; + } - if (words) { - for (k = 0; k < words.length; k++) { - word = words[k].trim(); - if (word.length > 0) { - startWord = text.indexOf(word, endWord); - endWord = startWord + word.length; - - startColumn = textOffset + startWord + 1; - endColumn = textOffset + endWord + 1; - - if (startColumn <= column && column <= endColumn) { - return { - word: word, - startColumn: startColumn, - endColumn: endColumn - }; - } - } + wordDefinition.lastIndex = start; + let match: RegExpMatchArray; + while (match = wordDefinition.exec(text)) { + if (match.index <= pos && wordDefinition.lastIndex >= pos) { + return { + word: match[0], + startColumn: textOffset + 1 + match.index, + endColumn: textOffset + 1 + wordDefinition.lastIndex + }; } } return null; } + + +function getWordAtPosSlow(column: number, wordDefinition: RegExp, text: string, textOffset: number): IWordAtPosition { + // matches all words starting at the beginning + // of the input until it finds a match that encloses + // the desired column. slow but correct + + let pos = column - 1 - textOffset; + wordDefinition.lastIndex = 0; + + let match: RegExpMatchArray; + while (match = wordDefinition.exec(text)) { + + if (match.index > pos) { + // |nW -> matched only after the pos + return null; + + } else if (wordDefinition.lastIndex >= pos) { + // W|W -> match encloses pos + return { + word: match[0], + startColumn: textOffset + 1 + match.index, + endColumn: textOffset + 1 + wordDefinition.lastIndex + }; + } + } + + return null; +} + +export function getWordAtText(column: number, wordDefinition: RegExp, text: string, textOffset: number): IWordAtPosition { + const result = getWordAtPosFast(column, wordDefinition, text, textOffset); + // both (getWordAtPosFast and getWordAtPosSlow) leave the wordDefinition-RegExp + // in an undefined state and to not confuse other users of the wordDefinition + // we reset the lastIndex + wordDefinition.lastIndex = 0; + return result; +} diff --git a/src/vs/editor/common/services/editorSimpleWorker.ts b/src/vs/editor/common/services/editorSimpleWorker.ts index b6824caa556..a9388a93a3b 100644 --- a/src/vs/editor/common/services/editorSimpleWorker.ts +++ b/src/vs/editor/common/services/editorSimpleWorker.ts @@ -148,8 +148,11 @@ class MirrorModel extends MirrorModel2 implements ICommonModel { // TODO@Joh, TODO@Alex - remove these and make sure the super-things work private _wordenize(content: string, wordDefinition: RegExp): editorCommon.IWordRange[] { - var result: editorCommon.IWordRange[] = []; - var match: RegExpExecArray; + const result: editorCommon.IWordRange[] = []; + let match: RegExpExecArray; + + wordDefinition.lastIndex = 0; // reset lastIndex just to be sure + while (match = wordDefinition.exec(content)) { if (match[0].length === 0) { // it did match the empty string diff --git a/src/vs/editor/contrib/suggest/common/snippetCompletion.ts b/src/vs/editor/contrib/suggest/common/snippetCompletion.ts index 61ef6a693fb..8fb21f6bb21 100644 --- a/src/vs/editor/contrib/suggest/common/snippetCompletion.ts +++ b/src/vs/editor/contrib/suggest/common/snippetCompletion.ts @@ -22,7 +22,7 @@ interface ISnippetPick extends IPickOpenEntry { class Args { static fromUser(arg: any): Args { - if (typeof arg !== 'object') { + if (!arg || typeof arg !== 'object') { return Args._empty; } let {snippet, name, langId} = arg; diff --git a/src/vs/platform/markers/common/problemMatcher.ts b/src/vs/platform/markers/common/problemMatcher.ts index 5be9dde216a..05c47eb7164 100644 --- a/src/vs/platform/markers/common/problemMatcher.ts +++ b/src/vs/platform/markers/common/problemMatcher.ts @@ -62,12 +62,10 @@ export interface ProblemPattern { loop?: boolean; - mostSignifikant?: boolean; - [key: string]: any; } -export let problemPatternProperties = ['file', 'message', 'location', 'line', 'column', 'endLine', 'endColumn', 'code', 'severity', 'loop', 'mostSignifikant']; +export let problemPatternProperties = ['file', 'message', 'location', 'line', 'column', 'endLine', 'endColumn', 'code', 'severity', 'loop']; export interface WatchingPattern { regexp: RegExp; diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index 51aee630f63..1436678648e 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -37,7 +37,6 @@ export interface IWindowsService { maximizeWindow(windowId: number): TPromise; unmaximizeWindow(windowId: number): TPromise; setDocumentEdited(windowId: number, flag: boolean): TPromise; - toggleMenuBar(windowId: number): TPromise; quit(): TPromise; // Global methods @@ -80,7 +79,6 @@ export interface IWindowService { getRecentlyOpen(): TPromise<{ files: string[]; folders: string[]; }>; focusWindow(): TPromise; setDocumentEdited(flag: boolean): TPromise; - toggleMenuBar(): TPromise; isMaximized(): TPromise; maximizeWindow(): TPromise; unmaximizeWindow(): TPromise; @@ -94,4 +92,5 @@ export interface IWindowSettings { zoomLevel: number; titleBarStyle: 'native' | 'custom'; autoDetectHighContrast: boolean; + menuBarVisibility: 'visible' | 'toggle' | 'hidden'; } diff --git a/src/vs/platform/windows/common/windowsIpc.ts b/src/vs/platform/windows/common/windowsIpc.ts index a02689a28ef..44f477c0ce1 100644 --- a/src/vs/platform/windows/common/windowsIpc.ts +++ b/src/vs/platform/windows/common/windowsIpc.ts @@ -29,7 +29,6 @@ export interface IWindowsChannel extends IChannel { call(command: 'maximizeWindow', arg: number): TPromise; call(command: 'unmaximizeWindow', arg: number): TPromise; call(command: 'setDocumentEdited', arg: [number, boolean]): TPromise; - call(command: 'toggleMenuBar', arg: number): TPromise; call(command: 'quit'): TPromise; call(command: 'openWindow', arg: [string[], { forceNewWindow?: boolean, forceReuseWindow?: boolean }]): TPromise; call(command: 'openNewWindow'): TPromise; @@ -75,7 +74,6 @@ export class WindowsChannel implements IWindowsChannel { case 'maximizeWindow': return this.service.maximizeWindow(arg); case 'unmaximizeWindow': return this.service.unmaximizeWindow(arg); case 'setDocumentEdited': return this.service.setDocumentEdited(arg[0], arg[1]); - case 'toggleMenuBar': return this.service.toggleMenuBar(arg); case 'openWindow': return this.service.openWindow(arg[0], arg[1]); case 'openNewWindow': return this.service.openNewWindow(); case 'showWindow': return this.service.showWindow(arg); @@ -171,10 +169,6 @@ export class WindowsChannelClient implements IWindowsService { return this.channel.call('setDocumentEdited', [windowId, flag]); } - toggleMenuBar(windowId: number): TPromise { - return this.channel.call('toggleMenuBar', windowId); - } - quit(): TPromise { return this.channel.call('quit'); } @@ -218,4 +212,4 @@ export class WindowsChannelClient implements IWindowsService { startCrashReporter(config: Electron.CrashReporterStartOptions): TPromise { return this.channel.call('startCrashReporter', config); } -} \ No newline at end of file +} diff --git a/src/vs/platform/windows/electron-browser/windowService.ts b/src/vs/platform/windows/electron-browser/windowService.ts index fb55d01df3f..4a6704becb8 100644 --- a/src/vs/platform/windows/electron-browser/windowService.ts +++ b/src/vs/platform/windows/electron-browser/windowService.ts @@ -89,7 +89,4 @@ export class WindowService implements IWindowService { return this.windowsService.setDocumentEdited(this.windowId, flag); } - toggleMenuBar(): TPromise { - return this.windowsService.toggleMenuBar(this.windowId); - } -} \ No newline at end of file +} diff --git a/src/vs/platform/windows/electron-main/windowsService.ts b/src/vs/platform/windows/electron-main/windowsService.ts index 7b0e6d47e4b..3e870dab516 100644 --- a/src/vs/platform/windows/electron-main/windowsService.ts +++ b/src/vs/platform/windows/electron-main/windowsService.ts @@ -193,11 +193,6 @@ export class WindowsService implements IWindowsService, IDisposable { return TPromise.as(null); } - toggleMenuBar(windowId: number): TPromise { - this.windowsMainService.toggleMenuBar(windowId); - return TPromise.as(null); - } - openWindow(paths: string[], options?: { forceNewWindow?: boolean, forceReuseWindow?: boolean }): TPromise { if (!paths || !paths.length) { return TPromise.as(null); @@ -278,4 +273,4 @@ export class WindowsService implements IWindowsService, IDisposable { dispose(): void { this.disposables = dispose(this.disposables); } -} \ No newline at end of file +} diff --git a/src/vs/workbench/electron-browser/actions.ts b/src/vs/workbench/electron-browser/actions.ts index 3c1e03ac623..56ef9e955ce 100644 --- a/src/vs/workbench/electron-browser/actions.ts +++ b/src/vs/workbench/electron-browser/actions.ts @@ -19,6 +19,7 @@ import { IMessageService, Severity } from 'vs/platform/message/common/message'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IConfigurationEditingService, ConfigurationTarget } from 'vs/workbench/services/configuration/common/configurationEditing'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IExtensionManagementService, LocalExtensionType, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; @@ -169,12 +170,36 @@ export class ToggleMenuBarAction extends Action { static ID = 'workbench.action.toggleMenuBar'; static LABEL = nls.localize('toggleMenuBar', "Toggle Menu Bar"); - constructor(id: string, label: string, @IWindowService private windowService: IWindowService) { + private static menuBarVisibilityKey = 'window.menuBarVisibility'; + + constructor( + id: string, + label: string, + @IMessageService private messageService: IMessageService, + @IConfigurationService private configurationService: IConfigurationService, + @IConfigurationEditingService private configurationEditingService: IConfigurationEditingService + ) { super(id, label); } - run(): TPromise { - return this.windowService.toggleMenuBar(); + public run(): TPromise { + let currentVisibilityValue = this.configurationService.lookup<'visible' | 'toggle' | 'hidden'>(ToggleMenuBarAction.menuBarVisibilityKey).value; + if (typeof (currentVisibilityValue) !== 'string') { + currentVisibilityValue = 'visible'; + } + + let newVisibilityValue: string; + if (currentVisibilityValue === 'visible') { + newVisibilityValue = 'toggle'; + } else { + newVisibilityValue = 'visible'; + } + + this.configurationEditingService.writeConfiguration(ConfigurationTarget.USER, { key: ToggleMenuBarAction.menuBarVisibilityKey, value: newVisibilityValue }).then(null, error => { + this.messageService.show(Severity.Error, error); + }); + + return TPromise.as(null); } } diff --git a/src/vs/workbench/electron-browser/main.contribution.ts b/src/vs/workbench/electron-browser/main.contribution.ts index 8be72f39727..4ac436381e3 100644 --- a/src/vs/workbench/electron-browser/main.contribution.ts +++ b/src/vs/workbench/electron-browser/main.contribution.ts @@ -225,6 +225,15 @@ Note that there can still be cases where this setting is ignored (e.g. when usin } }; +if (platform.isWindows || platform.isLinux) { + properties['window.menuBarVisibility'] = { + 'type': 'string', + 'enum': ['visible', 'toggle', 'hidden'], + 'default': 'visible', + 'description': nls.localize('menuBarVisibility', "Control the visibility of the menu bar. A setting of 'toggle' means that a single press of the alt key will show and hide the menu bar.") + }; +} + if (platform.isWindows) { properties['window.autoDetectHighContrast'] = { 'type': 'boolean', @@ -273,4 +282,4 @@ configurationRegistry.registerConfiguration({ 'description': nls.localize('zenMode.hideStatusBar', "Controls if turning on Zen Mode also hides the status bar at the bottom of the workbench.") } } -}); +}); \ No newline at end of file diff --git a/src/vs/workbench/node/extensionPoints.ts b/src/vs/workbench/node/extensionPoints.ts index 6957846b25d..4da349b2266 100644 --- a/src/vs/workbench/node/extensionPoints.ts +++ b/src/vs/workbench/node/extensionPoints.ts @@ -84,15 +84,11 @@ class ExtensionManifestParser extends ExtensionManifestHandler { public parse(): TPromise { return pfs.readFile(this._absoluteManifestPath).then((manifestContents) => { - let errors: json.ParseError[] = []; - const extensionDescription = json.parse(manifestContents.toString(), errors); - if (errors.length > 0) { - errors.forEach((error) => { - this._collector.error(this._absoluteFolderPath, nls.localize('jsonParseFail', "Failed to parse {0}: {1}.", this._absoluteManifestPath, json.getParseErrorMessage(error.error))); - }); - return null; + try { + return JSON.parse(manifestContents.toString()); + } catch (e) { + this._collector.error(this._absoluteFolderPath, nls.localize('jsonParseFail', "Failed to parse {0}: {1}.", this._absoluteManifestPath, json.getParseErrorMessage(e.message))); } - return extensionDescription; }, (err) => { if (err.code === 'ENOENT') { return null; @@ -165,40 +161,44 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler { /** * This routine make the following assumptions: * The root element is a object literal - * Strings to replace are one values of a key. So for example string[] are ignored. - * This is done to speed things up. */ private static _replaceNLStrings(literal: T, messages: { [key: string]: string; }, collector: MessagesCollector, messageScope: string): void { - Object.keys(literal).forEach(key => { - if (literal.hasOwnProperty(key)) { - let value = literal[key]; - if (Types.isString(value)) { - let str = value; - let length = str.length; - if (length > 1 && str[0] === '%' && str[length - 1] === '%') { - let messageKey = str.substr(1, length - 2); - let message = messages[messageKey]; - if (message) { - if (nlsConfig.pseudo) { - // FF3B and FF3D is the Unicode zenkaku representation for [ and ] - message = '\uFF3B' + message.replace(/[aouei]/g, '$&$&') + '\uFF3D'; - } - literal[key] = message; - } else { - collector.warn(messageScope, nls.localize('missingNLSKey', "Couldn't find message for key {0}.", messageKey)); + function processEntry(obj: any, key: string | number) { + let value = obj[key]; + if (Types.isString(value)) { + let str = value; + let length = str.length; + if (length > 1 && str[0] === '%' && str[length - 1] === '%') { + let messageKey = str.substr(1, length - 2); + let message = messages[messageKey]; + if (message) { + if (nlsConfig.pseudo) { + // FF3B and FF3D is the Unicode zenkaku representation for [ and ] + message = '\uFF3B' + message.replace(/[aouei]/g, '$&$&') + '\uFF3D'; } + obj[key] = message; + } else { + collector.warn(messageScope, nls.localize('missingNLSKey', "Couldn't find message for key {0}.", messageKey)); } - } else if (Types.isObject(value)) { - ExtensionManifestNLSReplacer._replaceNLStrings(value, messages, collector, messageScope); - } else if (Types.isArray(value)) { - (value).forEach(element => { - if (Types.isObject(element)) { - ExtensionManifestNLSReplacer._replaceNLStrings(element, messages, collector, messageScope); - } - }); + } + } else if (Types.isObject(value)) { + for (let k in value) { + if (value.hasOwnProperty(k)) { + processEntry(value, k); + } + } + } else if (Types.isArray(value)) { + for (let i = 0; i < value.length; i++) { + processEntry(value, i); } } - }); + } + + for (let key in literal) { + if (literal.hasOwnProperty(key)) { + processEntry(literal, key); + } + }; } } diff --git a/src/vs/workbench/parts/debug/common/debugModel.ts b/src/vs/workbench/parts/debug/common/debugModel.ts index 9a6b23de05b..5b97853ef25 100644 --- a/src/vs/workbench/parts/debug/common/debugModel.ts +++ b/src/vs/workbench/parts/debug/common/debugModel.ts @@ -520,7 +520,7 @@ export class Process implements debug.IProcess { thread.stopped = true; thread.clearCallStack(); }); - } else { + } else if (this.threads.has(data.threadId)) { // One thread is stopped, only update that thread. const thread = this.threads.get(data.threadId); thread.stoppedDetails = data.stoppedDetails; diff --git a/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts b/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts index 2cda12aa0b4..64e21cd4d8d 100644 --- a/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts +++ b/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts @@ -580,11 +580,10 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService { .then(null, onUnexpectedError); } } - } - - if (extension.gallery) { - // Report telemetry only for gallery extensions - this.reportTelemetry(installing, !error); + if (extension.gallery) { + // Report telemetry only for gallery extensions + this.reportTelemetry(installing, !error); + } } this._onChange.fire(); } diff --git a/src/vs/workbench/parts/files/browser/fileActions.ts b/src/vs/workbench/parts/files/browser/fileActions.ts index 623df36d1cb..8ded8efe06a 100644 --- a/src/vs/workbench/parts/files/browser/fileActions.ts +++ b/src/vs/workbench/parts/files/browser/fileActions.ts @@ -46,6 +46,8 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { Keybinding, KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { getCodeEditor } from 'vs/editor/common/services/codeEditorService'; import { IEditorViewState } from 'vs/editor/common/editorCommon'; +import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { ITextModelResolverService } from 'vs/editor/common/services/resolverService'; export interface IEditableData { action: IAction; @@ -271,7 +273,9 @@ class RenameFileAction extends BaseRenameAction { element: FileStat, @IFileService fileService: IFileService, @IMessageService messageService: IMessageService, - @ITextFileService textFileService: ITextFileService + @ITextFileService textFileService: ITextFileService, + @ITextModelResolverService private textModelResolverService: ITextModelResolverService, + @IBackupFileService private backupFileService: IBackupFileService ) { super(RenameFileAction.ID, nls.localize('rename', "Rename"), element, fileService, messageService, textFileService); @@ -280,40 +284,44 @@ class RenameFileAction extends BaseRenameAction { public runAction(newName: string): TPromise { - // Handle dirty - let revertPromise: TPromise = TPromise.as(null); + // 1. check for dirty files that are being moved and backup to new target const dirty = this.textFileService.getDirty().filter(d => paths.isEqualOrParent(d.fsPath, this.element.resource.fsPath)); - if (dirty.length) { - let message: string; - if (this.element.isDirectory) { - if (dirty.length === 1) { - message = nls.localize('dirtyMessageFolderOne', "You are renaming a folder with unsaved changes in 1 file. Do you want to continue?"); - } else { - message = nls.localize('dirtyMessageFolder', "You are renaming a folder with unsaved changes in {0} files. Do you want to continue?", dirty.length); - } - } else { - message = nls.localize('dirtyMessageFile', "You are renaming a file with unsaved changes. Do you want to continue?"); + const dirtyRenamed: URI[] = []; + return TPromise.join(dirty.map(d => { + const targetPath = paths.join(this.element.parent.resource.fsPath, newName); + let renamed: URI; + + // If the dirty file itself got moved, just reparent it to the target folder + if (this.element.resource.fsPath === d.fsPath) { + renamed = URI.file(targetPath); } - const res = this.messageService.confirm({ - message, - type: 'warning', - detail: nls.localize('dirtyWarning', "Your changes will be lost if you don't save them."), - primaryButton: nls.localize({ key: 'renameLabel', comment: ['&& denotes a mnemonic'] }, "&&Rename") - }); - - if (!res) { - return TPromise.as(null); + // Otherwise, a parent of the dirty resource got moved, so we have to reparent more complicated. Example: + else { + renamed = URI.file(paths.join(targetPath, d.fsPath.substr(this.element.resource.fsPath.length + 1))); } - revertPromise = this.textFileService.revertAll(dirty); - } + dirtyRenamed.push(renamed); - return revertPromise.then(() => { - return this.fileService.rename(this.element.resource, newName).then(null, (error: Error) => { - this.onErrorWithRetry(error, () => this.runAction(newName)); + const model = this.textFileService.models.get(d); + + return this.backupFileService.backupResource(renamed, model.getValue(), model.getVersionId()); + })) + + // 2. soft revert all dirty since we have backed up their contents + .then(() => this.textFileService.revertAll(dirty, { soft: true /* do not attempt to load content from disk */ })) + + // 3.) run the rename operation + .then(() => this.fileService.rename(this.element.resource, newName).then(null, (error: Error) => { + return TPromise.join(dirtyRenamed.map(d => this.backupFileService.discardResourceBackup(d))).then(() => { + this.onErrorWithRetry(error, () => this.runAction(newName)); + }); + })) + + // 4.) resolve those that were dirty to load their previous dirty contents from disk + .then(() => { + return TPromise.join(dirtyRenamed.map(t => this.textModelResolverService.createModelReference(t))); }); - }); } } @@ -821,12 +829,23 @@ export class ImportFileAction extends BaseFileAction { // Run import in sequence const importPromisesFactory: ITask>[] = []; - filesArray.forEach((file) => { + filesArray.forEach(file => { importPromisesFactory.push(() => { const sourceFile = URI.file(file.path); + const targetFile = URI.file(paths.join(targetElement.resource.fsPath, paths.basename(file.path))); - return this.fileService.importFile(sourceFile, targetElement.resource).then(null, (error: any) => { - this.messageService.show(Severity.Error, error); + // if the target exists and is dirty, make sure to revert it. otherwise the dirty contents + // of the target file would replace the contents of the imported file. since we already + // confirmed the overwrite before, this is OK. + let revertPromise = TPromise.as(null); + if (this.textFileService.isDirty(targetFile)) { + revertPromise = this.textFileService.revertAll([targetFile], { soft: true }); + } + + return revertPromise.then(() => { + return this.fileService.importFile(sourceFile, targetElement.resource).then(null, (error: any) => { + this.messageService.show(Severity.Error, error); + }); }); }); }); diff --git a/src/vs/workbench/parts/files/browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/browser/views/explorerViewer.ts index 896f5c17413..c7bc7fd1155 100644 --- a/src/vs/workbench/parts/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/browser/views/explorerViewer.ts @@ -49,6 +49,8 @@ import { Keybinding, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IMenuService, IMenu, MenuId } from 'vs/platform/actions/common/actions'; import { fillInActions } from 'vs/platform/actions/browser/menuItemActionItem'; +import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { ITextModelResolverService } from 'vs/editor/common/services/resolverService'; export class FileDataSource implements IDataSource { constructor( @@ -723,7 +725,9 @@ export class FileDragAndDrop implements IDragAndDrop { @IFileService private fileService: IFileService, @IConfigurationService private configurationService: IConfigurationService, @IInstantiationService private instantiationService: IInstantiationService, - @ITextFileService private textFileService: ITextFileService + @ITextFileService private textFileService: ITextFileService, + @ITextModelResolverService private textModelResolverService: ITextModelResolverService, + @IBackupFileService private backupFileService: IBackupFileService ) { this.toDispose = []; @@ -868,70 +872,87 @@ export class FileDragAndDrop implements IDragAndDrop { promise = tree.expand(target).then(() => { - // Reuse action if user copies + // Reuse duplicate action if user copies if (isCopy) { - const copyAction = this.instantiationService.createInstance(DuplicateFileAction, tree, source, target); - return copyAction.run(); + return this.instantiationService.createInstance(DuplicateFileAction, tree, source, target).run(); } - // Handle dirty (in file or inside the folder if any) - let revertPromise: TPromise = TPromise.as(null); + const dirtyMoved: URI[] = []; + + // Success: load all files that are dirty again to restore their dirty contents + // Error: discard any backups created during the process + const onSuccess = () => TPromise.join(dirtyMoved.map(t => this.textModelResolverService.createModelReference(t))); + const onError = (error?: Error, showError?: boolean) => { + if (showError) { + this.messageService.show(Severity.Error, error); + } + + return TPromise.join(dirtyMoved.map(d => this.backupFileService.discardResourceBackup(d))); + }; + + // 1. check for dirty files that are being moved and backup to new target const dirty = this.textFileService.getDirty().filter(d => paths.isEqualOrParent(d.fsPath, source.resource.fsPath)); - if (dirty.length) { - let message: string; - if (source.isDirectory) { - if (dirty.length === 1) { - message = nls.localize('dirtyMessageFolderOne', "You are moving a folder with unsaved changes in 1 file. Do you want to continue?"); - } else { - message = nls.localize('dirtyMessageFolder', "You are moving a folder with unsaved changes in {0} files. Do you want to continue?", dirty.length); - } - } else { - message = nls.localize('dirtyMessageFile', "You are moving a file with unsaved changes. Do you want to continue?"); + return TPromise.join(dirty.map(d => { + let moved: URI; + + // If the dirty file itself got moved, just reparent it to the target folder + if (source.resource.fsPath === d.fsPath) { + moved = URI.file(paths.join(target.resource.fsPath, source.name)); } - const res = this.messageService.confirm({ - message, - type: 'warning', - detail: nls.localize('dirtyWarning', "Your changes will be lost if you don't save them."), - primaryButton: nls.localize({ key: 'moveLabel', comment: ['&& denotes a mnemonic'] }, "&&Move") - }); - - if (!res) { - return TPromise.as(null); + // Otherwise, a parent of the dirty resource got moved, so we have to reparent more complicated. Example: + else { + moved = URI.file(paths.join(target.resource.fsPath, d.fsPath.substr(source.parent.resource.fsPath.length + 1))); } - revertPromise = this.textFileService.revertAll(dirty); - } + dirtyMoved.push(moved); - return revertPromise.then(() => { - const targetResource = URI.file(paths.join(target.resource.fsPath, source.name)); - let didHandleConflict = false; + const model = this.textFileService.models.get(d); - // Move File/Folder - return this.fileService.moveFile(source.resource, targetResource).then(null, error => { + return this.backupFileService.backupResource(moved, model.getValue(), model.getVersionId()); + })) - // Conflict - if ((error).fileOperationResult === FileOperationResult.FILE_MOVE_CONFLICT) { - didHandleConflict = true; + // 2. soft revert all dirty since we have backed up their contents + .then(() => this.textFileService.revertAll(dirty, { soft: true /* do not attempt to load content from disk */ })) - const confirm: IConfirmation = { - message: nls.localize('confirmOverwriteMessage', "'{0}' already exists in the destination folder. Do you want to replace it?", source.name), - detail: nls.localize('irreversible', "This action is irreversible!"), - primaryButton: nls.localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace") - }; + // 3.) run the move operation + .then(() => { + const targetResource = URI.file(paths.join(target.resource.fsPath, source.name)); + let didHandleConflict = false; - if (this.messageService.confirm(confirm)) { - return this.fileService.moveFile(source.resource, targetResource, true).then(null, (error) => { - this.messageService.show(Severity.Error, error); - }); + return this.fileService.moveFile(source.resource, targetResource).then(null, error => { + + // Conflict + if ((error).fileOperationResult === FileOperationResult.FILE_MOVE_CONFLICT) { + didHandleConflict = true; + + const confirm: IConfirmation = { + message: nls.localize('confirmOverwriteMessage', "'{0}' already exists in the destination folder. Do you want to replace it?", source.name), + detail: nls.localize('irreversible', "This action is irreversible!"), + primaryButton: nls.localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace") + }; + + // Move with overwrite if the user confirms + if (this.messageService.confirm(confirm)) { + const targetDirty = this.textFileService.getDirty().filter(d => paths.isEqualOrParent(d.fsPath, targetResource.fsPath)); + + // Make sure to revert all dirty in target first to be able to overwrite properly + return this.textFileService.revertAll(targetDirty, { soft: true /* do not attempt to load content from disk */ }).then(() => { + + // Then continue to do the move operation + return this.fileService.moveFile(source.resource, targetResource, true).then(onSuccess, error => onError(error, true)); + }); + } + + return onError(); } - return; - } + return onError(error, true); + }); + }) - this.messageService.show(Severity.Error, error); - }); - }); + // 4.) resolve those that were dirty to load their previous dirty contents from disk + .then(onSuccess, onError); }, errors.onUnexpectedError); } diff --git a/src/vs/workbench/parts/files/common/editors/fileEditorTracker.ts b/src/vs/workbench/parts/files/common/editors/fileEditorTracker.ts index 2e07718f129..7e5a6325049 100644 --- a/src/vs/workbench/parts/files/common/editors/fileEditorTracker.ts +++ b/src/vs/workbench/parts/files/common/editors/fileEditorTracker.ts @@ -8,9 +8,9 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import errors = require('vs/base/common/errors'); import URI from 'vs/base/common/uri'; import paths = require('vs/base/common/paths'); -import { IEditor } from 'vs/editor/common/editorCommon'; +import { IEditor, IEditorViewState, isCommonCodeEditor } from 'vs/editor/common/editorCommon'; import { IEditor as IBaseEditor } from 'vs/platform/editor/common/editor'; -import { EditorInput, IEditorStacksModel, SideBySideEditorInput } from 'vs/workbench/common/editor'; +import { toResource, EditorInput, IEditorStacksModel, SideBySideEditorInput, IEditorGroup } from 'vs/workbench/common/editor'; import { BINARY_FILE_EDITOR_ID } from 'vs/workbench/parts/files/common/files'; import { ITextFileService, ModelState } from 'vs/workbench/services/textfile/common/textfiles'; import { FileOperationEvent, FileOperation, IFileService, FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files'; @@ -157,13 +157,42 @@ export class FileEditorTracker implements IWorkbenchContribution { } // Reopen - this.editorService.openEditor({ resource: reopenFileResource, options: { preserveFocus: true, pinned: group.isPinned(input), index: group.indexOf(input), inactive: !group.isActive(input) } }, stacks.positionOfGroup(group)).done(null, errors.onUnexpectedError); + this.editorService.openEditor({ + resource: reopenFileResource, + options: { + preserveFocus: true, + pinned: group.isPinned(input), + index: group.indexOf(input), + inactive: !group.isActive(input), + viewState: this.getViewStateFor(oldResource, group) + } + }, stacks.positionOfGroup(group)).done(null, errors.onUnexpectedError); } } }); }); } + private getViewStateFor(resource: URI, group: IEditorGroup): IEditorViewState { + const stacks = this.editorGroupService.getStacksModel(); + const editors = this.editorService.getVisibleEditors(); + + for (let i = 0; i < editors.length; i++) { + const editor = editors[i]; + if (editor && editor.position === stacks.positionOfGroup(group)) { + const resource = toResource(editor.input, { filter: 'file' }); + if (resource && resource.fsPath === resource.fsPath) { + const control = editor.getControl(); + if (isCommonCodeEditor(control)) { + return control.saveViewState(); + } + } + } + } + + return void 0; + } + private handleUpdatesToVisibleEditors(e: FileChangesEvent) { const editors = this.editorService.getVisibleEditors(); editors.forEach(editor => { diff --git a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts index bc7a744c14f..96c0a7da7ba 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts @@ -142,7 +142,6 @@ export class DefaultPreferencesEditor extends BaseTextEditor { options.folding = false; options.renderWhitespace = 'none'; options.wrappingColumn = 0; - options.overviewRulerLanes = 0; options.renderIndentGuides = false; options.rulers = []; } diff --git a/src/vs/workbench/parts/preferences/browser/preferencesService.ts b/src/vs/workbench/parts/preferences/browser/preferencesService.ts index f3e53ff1119..24d871f3a58 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesService.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesService.ts @@ -125,7 +125,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic openWorkspaceSettings(): TPromise { if (!this.contextService.hasWorkspace()) { this.messageService.show(Severity.Info, nls.localize('openFolderFirst', "Open a folder first to create workspace settings")); - return; + return TPromise.as(null); } return this.openSettings(ConfigurationTarget.WORKSPACE); } diff --git a/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts b/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts index 8bbd3b3911d..a6260610aec 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesWidgets.ts @@ -91,8 +91,11 @@ export class SettingsGroupTitleWidget extends Widget implements IViewZone { } private layout(): void { - this.titleContainer.style.lineHeight = this.editor.getConfiguration().lineHeight + 3 + 'px'; - this.titleContainer.style.fontSize = this.editor.getConfiguration().fontInfo.fontSize + 'px'; + const configuration = this.editor.getConfiguration(); + const layoutInfo = this.editor.getLayoutInfo(); + this.titleContainer.style.width = layoutInfo.contentWidth - layoutInfo.verticalScrollbarWidth + 'px'; + this.titleContainer.style.lineHeight = configuration.lineHeight + 3 + 'px'; + this.titleContainer.style.fontSize = configuration.fontInfo.fontSize + 'px'; const iconSize = this.getIconSize(); this.icon.style.height = `${iconSize}px`; this.icon.style.width = `${iconSize}px`; diff --git a/src/vs/workbench/parts/search/browser/searchViewlet.ts b/src/vs/workbench/parts/search/browser/searchViewlet.ts index 748b1904ff2..1fcd97fd773 100644 --- a/src/vs/workbench/parts/search/browser/searchViewlet.ts +++ b/src/vs/workbench/parts/search/browser/searchViewlet.ts @@ -470,6 +470,7 @@ export class SearchViewlet extends Viewlet { } // Reveal the newly selected element + this.tree.setFocus(next, eventPayload); this.tree.setSelection([next], eventPayload); this.tree.reveal(next); } @@ -497,7 +498,9 @@ export class SearchViewlet extends Viewlet { } // Reveal the newly selected element + this.tree.setFocus(prev, eventPayload); this.tree.setSelection([prev], eventPayload); + this.tree.reveal(prev); } public setVisible(visible: boolean): TPromise { diff --git a/src/vs/workbench/parts/tasks/test/node/configuration.test.ts b/src/vs/workbench/parts/tasks/test/node/configuration.test.ts index 90ec0957c87..2cd2709a8d8 100644 --- a/src/vs/workbench/parts/tasks/test/node/configuration.test.ts +++ b/src/vs/workbench/parts/tasks/test/node/configuration.test.ts @@ -229,11 +229,6 @@ class PatternBuilder { this.result.loop = value; return this; } - - public mostSignifikant(value: boolean): PatternBuilder { - this.result.mostSignifikant = value; - return this; - } } @@ -1007,6 +1002,5 @@ suite('Tasks Configuration parsing tests', () => { assert.strictEqual(actual.code, expected.code); assert.strictEqual(actual.severity, expected.severity); assert.strictEqual(actual.loop, expected.loop); - assert.strictEqual(actual.mostSignifikant, expected.mostSignifikant); } }); \ No newline at end of file diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalConfigHelper.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalConfigHelper.ts index 113024a23db..7d05f7e29aa 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalConfigHelper.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalConfigHelper.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IConfiguration, DefaultConfig } from 'vs/editor/common/config/defaultConfig'; +import { IConfiguration as IEditorConfiguration, DefaultConfig } from 'vs/editor/common/config/defaultConfig'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITerminalConfiguration, ITerminalConfigHelper, ITerminalFont, IShell } from 'vs/workbench/parts/terminal/common/terminal'; import { Platform } from 'vs/base/common/platform'; @@ -91,16 +91,16 @@ export class TerminalConfigHelper implements ITerminalConfigHelper { this._charMeasureElement = document.createElement('div'); this.panelContainer.appendChild(this._charMeasureElement); } - let style = this._charMeasureElement.style; + const style = this._charMeasureElement.style; style.display = 'block'; style.fontFamily = fontFamily; style.fontSize = fontSize + 'px'; - style.height = Math.floor(lineHeight * fontSize) + 'px'; + style.lineHeight = lineHeight.toString(10); this._charMeasureElement.innerText = 'X'; - let rect = this._charMeasureElement.getBoundingClientRect(); + const rect = this._charMeasureElement.getBoundingClientRect(); style.display = 'none'; - let charWidth = Math.ceil(rect.width); - let charHeight = Math.ceil(rect.height); + const charWidth = rect.width; + const charHeight = rect.height; return { fontFamily, fontSize: fontSize + 'px', @@ -115,37 +115,46 @@ export class TerminalConfigHelper implements ITerminalConfigHelper { * terminal.integrated.fontSize, terminal.integrated.lineHeight configuration properties */ public getFont(): ITerminalFont { - let terminalConfig = this._configurationService.getConfiguration().terminal.integrated; - let editorConfig = this._configurationService.getConfiguration(); + const config = this._configurationService.getConfiguration(); + const editorConfig = (config).editor; + const terminalConfig = (config).terminal.integrated; - let fontFamily = terminalConfig.fontFamily || editorConfig.editor.fontFamily; + const fontFamily = terminalConfig.fontFamily || editorConfig.fontFamily; let fontSize = this._toInteger(terminalConfig.fontSize, 0); if (fontSize <= 0) { fontSize = DefaultConfig.editor.fontSize; } let lineHeight = terminalConfig.lineHeight <= 0 ? DEFAULT_LINE_HEIGHT : terminalConfig.lineHeight; + if (!lineHeight) { + lineHeight = DEFAULT_LINE_HEIGHT; + } return this._measureFont(fontFamily, fontSize, lineHeight); } public getFontLigaturesEnabled(): boolean { - let terminalConfig = this._configurationService.getConfiguration().terminal.integrated; - return terminalConfig.fontLigatures; + const terminalConfig = this._configurationService.getConfiguration(); + return terminalConfig.terminal.integrated.fontLigatures; } public getCursorBlink(): boolean { - let terminalConfig = this._configurationService.getConfiguration().terminal.integrated; - return terminalConfig.cursorBlinking; + const terminalConfig = this._configurationService.getConfiguration(); + return terminalConfig.terminal.integrated.cursorBlinking; } public getRightClickCopyPaste(): boolean { - let config = this._configurationService.getConfiguration(); + const config = this._configurationService.getConfiguration(); return config.terminal.integrated.rightClickCopyPaste; } + public getCommandsToSkipShell(): string[] { + const config = this._configurationService.getConfiguration(); + return config.terminal.integrated.commandsToSkipShell; + } + public getShell(): IShell { - let config = this._configurationService.getConfiguration(); - let shell: IShell = { + const config = this._configurationService.getConfiguration(); + const shell: IShell = { executable: '', args: [] }; @@ -190,9 +199,4 @@ export class TerminalConfigHelper implements ITerminalConfigHelper { } return r; } - - public getCommandsToSkipShell(): string[] { - let config = this._configurationService.getConfiguration(); - return config.terminal.integrated.commandsToSkipShell; - } } \ No newline at end of file diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts index 2e56d1240c9..f2b76b3f41c 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts @@ -156,8 +156,8 @@ export class TerminalInstance implements ITerminalInstance { }, 0); }); - let xtermHelper: HTMLElement = this._xterm.element.querySelector('.xterm-helpers'); - let focusTrap: HTMLElement = document.createElement('div'); + const xtermHelper: HTMLElement = this._xterm.element.querySelector('.xterm-helpers'); + const focusTrap: HTMLElement = document.createElement('div'); focusTrap.setAttribute('tabindex', '0'); DOM.addClass(focusTrap, 'focus-trap'); focusTrap.addEventListener('focus', function (event: FocusEvent) { @@ -165,7 +165,7 @@ export class TerminalInstance implements ITerminalInstance { while (!DOM.hasClass(currentElement, 'part')) { currentElement = currentElement.parentElement; } - let hidePanelElement = currentElement.querySelector('.hide-panel-action'); + const hidePanelElement = currentElement.querySelector('.hide-panel-action'); hidePanelElement.focus(); }); xtermHelper.insertBefore(focusTrap, this._xterm.textarea); @@ -240,7 +240,7 @@ export class TerminalInstance implements ITerminalInstance { if (!this._xterm) { return; } - let text = window.getSelection().toString(); + const text = window.getSelection().toString(); if (!text || force) { this._xterm.focus(); } @@ -331,11 +331,11 @@ export class TerminalInstance implements ITerminalInstance { } protected _createProcess(workspace: IWorkspace, name: string, shell: IShell) { - let locale = this._configHelper.isSetLocaleVariables() ? platform.locale : undefined; + const locale = this._configHelper.isSetLocaleVariables() ? platform.locale : undefined; if (!shell.executable) { shell = this._configHelper.getShell(); } - let env = TerminalInstance.createTerminalEnv(process.env, shell, this._getCwd(workspace, shell.ignoreCustomCwd), locale); + const env = TerminalInstance.createTerminalEnv(process.env, shell, this._getCwd(workspace, shell.ignoreCustomCwd), locale); this._title = name ? name : ''; this._process = cp.fork('./terminalProcess', [], { env: env, @@ -378,7 +378,7 @@ export class TerminalInstance implements ITerminalInstance { // TODO: This should be private/protected // TODO: locale should not be optional public static createTerminalEnv(parentEnv: IStringDictionary, shell: IShell, cwd: string, locale?: string): IStringDictionary { - let env = TerminalInstance._cloneEnv(parentEnv); + const env = TerminalInstance._cloneEnv(parentEnv); env['PTYPID'] = process.pid.toString(); env['PTYSHELL'] = shell.executable; if (shell.args) { @@ -402,7 +402,7 @@ export class TerminalInstance implements ITerminalInstance { } private static _cloneEnv(env: IStringDictionary): IStringDictionary { - let newEnv: IStringDictionary = Object.create(null); + const newEnv: IStringDictionary = Object.create(null); Object.keys(env).forEach((key) => { newEnv[key] = env[key]; }); @@ -442,7 +442,7 @@ export class TerminalInstance implements ITerminalInstance { } public layout(dimension: { width: number, height: number }): void { - let font = this._configHelper.getFont(); + const font = this._configHelper.getFont(); if (!font || !font.charWidth || !font.charHeight) { return; } @@ -455,10 +455,12 @@ export class TerminalInstance implements ITerminalInstance { // Upstream issue: https://github.com/sourcelair/xterm.js/issues/291 this._xterm.emit('scroll', this._xterm.ydisp); } - let leftPadding = parseInt(getComputedStyle(document.querySelector('.terminal-outer-container')).paddingLeft.split('px')[0], 10); - let innerWidth = dimension.width - leftPadding; - let cols = Math.floor(innerWidth / font.charWidth); - let rows = Math.floor(dimension.height / font.charHeight); + const padding = parseInt(getComputedStyle(document.querySelector('.terminal-outer-container')).paddingLeft.split('px')[0], 10); + // Use left padding as right padding, right padding is not defined in CSS just in case + // xterm.js causes an unexpected overflow. + const innerWidth = dimension.width - padding * 2; + const cols = Math.floor(innerWidth / font.charWidth); + const rows = Math.floor(dimension.height / font.charHeight); if (this._xterm) { this._xterm.resize(cols, rows); this._xterm.element.style.width = innerWidth + 'px'; diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalProcess.js b/src/vs/workbench/parts/terminal/electron-browser/terminalProcess.js index c6188f790b9..c1cadb0e30d 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalProcess.js +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalProcess.js @@ -6,7 +6,7 @@ var fs = require('fs'); var os = require('os'); var path = require('path'); -var ptyJs = require('pty.js'); +var ptyJs = require('node-pty'); // The pty process needs to be run in its own child process to get around maxing out CPU on Mac, // see https://github.com/electron/electron/issues/38 diff --git a/src/vs/workbench/test/node/api/extHostDocuments.test.ts b/src/vs/workbench/test/node/api/extHostDocuments.test.ts index b20a73865ef..5f66d6a8947 100644 --- a/src/vs/workbench/test/node/api/extHostDocuments.test.ts +++ b/src/vs/workbench/test/node/api/extHostDocuments.test.ts @@ -207,7 +207,7 @@ suite('ExtHostDocument', () => { test('getWordRangeAtPosition', function () { data = new ExtHostDocumentData(undefined, URI.file(''), [ - 'aaaa bbbb cccc abc' + 'aaaa bbbb+cccc abc' ], '\n', 'text', 1, false); let range = data.getWordRangeAtPosition(new Position(0, 2)); @@ -216,19 +216,20 @@ suite('ExtHostDocument', () => { assert.equal(range.end.line, 0); assert.equal(range.end.character, 4); + // ignore bad regular expresson /.*/ range = data.getWordRangeAtPosition(new Position(0, 2), /.*/); assert.equal(range.start.line, 0); assert.equal(range.start.character, 0); assert.equal(range.end.line, 0); assert.equal(range.end.character, 4); - range = data.getWordRangeAtPosition(new Position(0, 2), /a+.+?c/); + range = data.getWordRangeAtPosition(new Position(0, 5), /[a-z+]+/); assert.equal(range.start.line, 0); - assert.equal(range.start.character, 0); + assert.equal(range.start.character, 5); assert.equal(range.end.line, 0); - assert.equal(range.end.character, 11); + assert.equal(range.end.character, 14); - range = data.getWordRangeAtPosition(new Position(0, 17), /a+.+?c/); + range = data.getWordRangeAtPosition(new Position(0, 17), /[a-z+]+/); assert.equal(range.start.line, 0); assert.equal(range.start.character, 15); assert.equal(range.end.line, 0); diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index 4321cd6487b..22652887145 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -774,10 +774,6 @@ export class TestWindowService implements IWindowService { return TPromise.as(void 0); } - toggleMenuBar(): TPromise { - return TPromise.as(void 0); - } - isMaximized(): TPromise { return TPromise.as(void 0); } @@ -879,9 +875,6 @@ export class TestWindowsService implements IWindowsService { setDocumentEdited(windowId: number, flag: boolean): TPromise { return TPromise.as(void 0); } - toggleMenuBar(windowId: number): TPromise { - return TPromise.as(void 0); - } quit(): TPromise { return TPromise.as(void 0); } @@ -923,4 +916,4 @@ export class TestWindowsService implements IWindowsService { startCrashReporter(config: Electron.CrashReporterStartOptions): TPromise { return TPromise.as(void 0); } -} \ No newline at end of file +}