diff --git a/build/gulpfile.hygiene.js b/build/gulpfile.hygiene.js index 0ca8ea54b9d..230bdfe8a82 100644 --- a/build/gulpfile.hygiene.js +++ b/build/gulpfile.hygiene.js @@ -189,11 +189,12 @@ const hygiene = exports.hygiene = (some, options) => { }); const tsl = es.through(function (file) { - const configuration = tslint.findConfiguration(null, '.'); - const options = { configuration, formatter: 'json', rulesDirectory: 'build/lib/tslint' }; + const configuration = tslint.Configuration.findConfiguration(null, '.'); + const options = { formatter: 'json', rulesDirectory: 'build/lib/tslint' }; const contents = file.contents.toString('utf8'); - const linter = new tslint(file.relative, contents, options); - const result = linter.lint(); + const linter = new tslint.Linter(options); + linter.lint(file.relative, contents, configuration.results); + const result = linter.getResult(); if (result.failureCount > 0) { reportFailures(result.failures); @@ -229,6 +230,11 @@ gulp.task('hygiene', () => hygiene()); if (require.main === module) { const cp = require('child_process'); + process.on('unhandledRejection', (reason, p) => { + console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); + process.exit(1); + }); + cp.exec('git config core.autocrlf', (err, out) => { const skipEOL = out.trim() === 'true'; diff --git a/build/gulpfile.mixin.js b/build/gulpfile.mixin.js index a3d2391938d..387839f8b44 100644 --- a/build/gulpfile.mixin.js +++ b/build/gulpfile.mixin.js @@ -51,8 +51,18 @@ gulp.task('mixin', function () { const build = all.pipe(filter('build/**')); const productJsonFilter = filter('product.json', { restore: true }); + const vsdaFilter = (function() { + const filter = []; + if (process.platform !== 'win32') { filter.push('!**/vsda_win32.node'); } + if (process.platform !== 'darwin') { filter.push('!**/vsda_darwin.node'); } + if (process.platform !== 'linux' || process.arch !== 'x64') { filter.push('!**/vsda_linux64.node'); } + if (process.platform !== 'linux' || process.arch === 'x64') { filter.push('!**/vsda_linux32.node'); } + + return filter; + })(); + const mixin = all - .pipe(filter('quality/' + quality + '/**')) + .pipe(filter(['quality/' + quality + '/**'].concat(vsdaFilter))) .pipe(util.rebase(2)) .pipe(productJsonFilter) .pipe(buffer()) @@ -71,4 +81,4 @@ gulp.task('mixin', function () { return f; })) .pipe(gulp.dest('.')); -}); +}); \ No newline at end of file diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index fcc9d25a0a4..8e028190a1a 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -30,7 +30,8 @@ const product = require('../product.json'); const shrinkwrap = require('../npm-shrinkwrap.json'); const crypto = require('crypto'); -const dependencies = Object.keys(shrinkwrap.dependencies); +const dependencies = Object.keys(shrinkwrap.dependencies) + .concat(Array.isArray(product.extraNodeModules) ? product.extraNodeModules : []); // additional dependencies from our product configuration const baseModules = Object.keys(process.binding('natives')).filter(n => !/^_|\//.test(n)); const nodeModules = ['electron', 'original-fs'] .concat(dependencies) @@ -39,8 +40,8 @@ const nodeModules = ['electron', 'original-fs'] // Build const builtInExtensions = [ - { name: 'ms-vscode.node-debug', version: '1.9.5' }, - { name: 'ms-vscode.node-debug2', version: '1.9.2' } + { name: 'ms-vscode.node-debug', version: '1.9.6' }, + { name: 'ms-vscode.node-debug2', version: '1.9.4' } ]; const vscodeEntryPoints = _.flatten([ @@ -93,11 +94,11 @@ gulp.task('optimize-vscode', ['clean-optimized-vscode', 'compile-build', 'compil gulp.task('optimize-index-js', ['optimize-vscode'], () => { - const fullpath = path.join(process.cwd(), 'out-vscode/vs/workbench/electron-browser/bootstrap/index.js') + const fullpath = path.join(process.cwd(), 'out-vscode/vs/workbench/electron-browser/bootstrap/index.js'); const contents = fs.readFileSync(fullpath).toString(); const newContents = contents.replace('[/*BUILD->INSERT_NODE_MODULES*/]', JSON.stringify(nodeModules)); fs.writeFileSync(fullpath, newContents); -}) +}); const baseUrl = `https://ticino.blob.core.windows.net/sourcemaps/${commit}/core`; gulp.task('clean-minified-vscode', util.rimraf('out-vscode-min')); diff --git a/build/lib/compilation.js b/build/lib/compilation.js index 2171ffa1ae9..54e640c2b15 100644 --- a/build/lib/compilation.js +++ b/build/lib/compilation.js @@ -23,8 +23,6 @@ options.verbose = false; options.sourceMap = true; options.rootDir = rootDir; options.sourceRoot = util.toFileUri(rootDir); -var smSourceRootPath = path.resolve(path.dirname(rootDir)); -var smSourceRoot = util.toFileUri(smSourceRootPath); function createCompile(build, emitError) { var opts = _.clone(options); opts.inlineSources = !!build; @@ -48,7 +46,7 @@ function createCompile(build, emitError) { .pipe(sourcemaps.write('.', { addComment: false, includeContent: !!build, - sourceRoot: smSourceRoot + sourceRoot: options.sourceRoot })) .pipe(tsFilter.restore) .pipe(reporter.end(emitError)); diff --git a/build/lib/compilation.ts b/build/lib/compilation.ts index ac367a573b0..3b33a6bd7ed 100644 --- a/build/lib/compilation.ts +++ b/build/lib/compilation.ts @@ -28,9 +28,6 @@ options.sourceMap = true; options.rootDir = rootDir; options.sourceRoot = util.toFileUri(rootDir); -const smSourceRootPath = path.resolve(path.dirname(rootDir)); -const smSourceRoot = util.toFileUri(smSourceRootPath); - function createCompile(build: boolean, emitError?: boolean): (token?: util.ICancellationToken) => NodeJS.ReadWriteStream { const opts = _.clone(options); opts.inlineSources = !!build; @@ -57,7 +54,7 @@ function createCompile(build: boolean, emitError?: boolean): (token?: util.ICanc .pipe(sourcemaps.write('.', { addComment: false, includeContent: !!build, - sourceRoot: smSourceRoot + sourceRoot: options.sourceRoot })) .pipe(tsFilter.restore) .pipe(reporter.end(emitError)); diff --git a/build/lib/tslint/noUnexternalizedStringsRule.js b/build/lib/tslint/noUnexternalizedStringsRule.js index 711b80925b2..a8193d9b1d3 100644 --- a/build/lib/tslint/noUnexternalizedStringsRule.js +++ b/build/lib/tslint/noUnexternalizedStringsRule.js @@ -90,8 +90,8 @@ var NoUnexternalizedStringsRuleWalker = (function (_super) { return; } if (doubleQuoted && (!callInfo || callInfo.argIndex === -1 || !this.signatures[functionName])) { - var s = node.getText(); - var replacement = new Lint.Replacement(node.getStart(), node.getWidth(), "nls.localize('KEY-" + s.substring(1, s.length - 1) + "', " + s + ")"); + var s_1 = node.getText(); + var replacement = new Lint.Replacement(node.getStart(), node.getWidth(), "nls.localize('KEY-" + s_1.substring(1, s_1.length - 1) + "', " + s_1 + ")"); var fix = new Lint.Fix("Unexternalitzed string", [replacement]); this.addFailure(this.createFailure(node.getStart(), node.getWidth(), "Unexternalized string found: " + node.getText(), fix)); return; diff --git a/build/monaco/api.ts b/build/monaco/api.ts index b399d1b1d81..ff227fd0156 100644 --- a/build/monaco/api.ts +++ b/build/monaco/api.ts @@ -195,7 +195,7 @@ function format(text:string): string { // Apply the edits on the input code return applyEdits(text, edits); - function getRuleProvider(options: ts.FormatCodeOptions) { + function getRuleProvider(options: ts.FormatCodeSettings) { // Share this between multiple formatters using the same options. // This represents the bulk of the space the formatter uses. let ruleProvider = new (ts).formatting.RulesProvider(); @@ -215,7 +215,7 @@ function format(text:string): string { return result; } - function getDefaultOptions(): ts.FormatCodeOptions { + function getDefaultOptions(): ts.FormatCodeSettings { return { indentSize: 4, tabSize: 4, diff --git a/build/monaco/package.json b/build/monaco/package.json index 8bd8ea80925..87ddd11dc01 100644 --- a/build/monaco/package.json +++ b/build/monaco/package.json @@ -43,7 +43,7 @@ "sinon": "^1.17.2", "source-map": "^0.4.4", "tslint": "^3.3.0", - "typescript": "^2.0.3", + "typescript": "^2.1.4", "typescript-formatter": "3.1.0", "underscore": "^1.8.2", "vinyl": "^0.4.5", diff --git a/extensions/markdown/media/markdown.css b/extensions/markdown/media/markdown.css index 2c16c282843..9ca2c0bbf86 100644 --- a/extensions/markdown/media/markdown.css +++ b/extensions/markdown/media/markdown.css @@ -6,8 +6,9 @@ body { font-family: "Segoe WPC", "Segoe UI", "SFUIText-Light", "HelveticaNeue-Light", sans-serif, "Droid Sans Fallback"; font-size: 14px; - padding-left: 12px; + padding: 0 12px; line-height: 22px; + word-wrap: break-word; } body.scrollBeyondLastLine { @@ -96,6 +97,10 @@ code { line-height: 19px; } +body.wordWrap pre { + white-space: pre-wrap; +} + .mac code { font-size: 12px; line-height: 18px; diff --git a/extensions/markdown/src/extension.ts b/extensions/markdown/src/extension.ts index 8dad9e2ec24..274dea90195 100644 --- a/extensions/markdown/src/extension.ts +++ b/extensions/markdown/src/extension.ts @@ -256,6 +256,8 @@ class MDDocumentContentProvider implements vscode.TextDocumentContentProvider { public provideTextDocumentContent(uri: vscode.Uri): Thenable { return vscode.workspace.openTextDocument(vscode.Uri.parse(uri.query)).then(document => { const scrollBeyondLastLine = vscode.workspace.getConfiguration('editor')['scrollBeyondLastLine']; + const wordWrap = vscode.workspace.getConfiguration('editor')['wordWrap']; + const head = ([] as Array).concat( '', '', @@ -267,7 +269,7 @@ class MDDocumentContentProvider implements vscode.TextDocumentContentProvider { this.computeCustomStyleSheetIncludes(uri), ``, '', - `` + `` ).join('\n'); const body = this._renderer.render(this.getDocumentContentForPreview(document)); diff --git a/extensions/typescript/package.json b/extensions/typescript/package.json index ce5a0ceb1f7..a74697b8026 100644 --- a/extensions/typescript/package.json +++ b/extensions/typescript/package.json @@ -256,11 +256,6 @@ } } }, - "keybindings": { - "key": ".", - "command": "^acceptSelectedSuggestion", - "when": "editorTextFocus && suggestWidgetVisible && editorLangId == 'typescript' && suggestionSupportsAcceptOnKey" - }, "commands": [ { "command": "typescript.reloadProjects", diff --git a/extensions/typescript/src/features/completionItemProvider.ts b/extensions/typescript/src/features/completionItemProvider.ts index 71cb92ffd94..0f1c282d850 100644 --- a/extensions/typescript/src/features/completionItemProvider.ts +++ b/extensions/typescript/src/features/completionItemProvider.ts @@ -18,16 +18,17 @@ import * as nls from 'vscode-nls'; let localize = nls.loadMessageBundle(); class MyCompletionItem extends CompletionItem { - - document: TextDocument; - position: Position; - - constructor(position: Position, document: TextDocument, entry: CompletionEntry) { + constructor( + public position: Position, + public document: TextDocument, + entry: CompletionEntry, + enableDotCompletions: boolean + ) { super(entry.name); this.sortText = entry.sortText; this.kind = MyCompletionItem.convertKind(entry.kind); this.position = position; - this.document = document; + this.commitCharacters = MyCompletionItem.getCommitCharacters(enableDotCompletions, entry.kind); if (entry.replacementSpan) { let span: protocol.TextSpan = entry.replacementSpan; // The indexing for the range returned by the server uses 1-based indexing. @@ -85,6 +86,38 @@ class MyCompletionItem extends CompletionItem { return CompletionItemKind.Property; } + + private static getCommitCharacters(enableDotCompletions: boolean, kind: string): string[] | undefined { + switch (kind) { + case PConst.Kind.externalModuleName: + return ['"', '\'']; + + case PConst.Kind.file: + case PConst.Kind.directory: + return ['/', '"', '\'']; + + case PConst.Kind.memberGetAccessor: + case PConst.Kind.memberSetAccessor: + case PConst.Kind.constructSignature: + case PConst.Kind.callSignature: + case PConst.Kind.indexSignature: + case PConst.Kind.enum: + case PConst.Kind.interface: + return enableDotCompletions ? ['.'] : undefined; + + case PConst.Kind.module: + case PConst.Kind.alias: + case PConst.Kind.variable: + case PConst.Kind.localVariable: + case PConst.Kind.memberVariable: + case PConst.Kind.class: + case PConst.Kind.function: + case PConst.Kind.memberFunction: + return enableDotCompletions ? ['.', '('] : undefined; + } + + return undefined; + } } interface Configuration { @@ -97,15 +130,12 @@ namespace Configuration { export default class TypeScriptCompletionItemProvider implements CompletionItemProvider { - public triggerCharacters = ['.']; - public excludeTokens = ['string', 'comment', 'numeric']; - public sortBy = [{ type: 'reference', partSeparator: '/' }]; - - private client: ITypescriptServiceClient; - private typingsStatus: TypingsStatus; private config: Configuration; - constructor(client: ITypescriptServiceClient, typingsStatus: TypingsStatus) { + constructor( + private client: ITypescriptServiceClient, + private typingsStatus: TypingsStatus + ) { this.client = client; this.typingsStatus = typingsStatus; this.config = { useCodeSnippetsOnMethodSuggest: false }; @@ -158,9 +188,22 @@ export default class TypeScriptCompletionItemProvider implements CompletionItemP let completionItems: CompletionItem[] = []; let body = msg.body; if (body) { + // Only enable dot completions in TS files for now + let enableDotCompletions = document && (document.languageId === 'typescript' || document.languageId === 'typescriptreact'); + + // TODO: Workaround for https://github.com/Microsoft/TypeScript/issues/13456 + // Only enable dot completions when previous character is an identifier. + // Prevents incorrectly completing while typing spread operators. + if (position.character > 0) { + const preText = document.getText(new Range( + new Position(position.line, 0), + new Position(position.line, position.character - 1))); + enableDotCompletions = preText.match(/[a-z_$]\s*$/ig) !== null; + } + for (let i = 0; i < body.length; i++) { let element = body[i]; - let item = new MyCompletionItem(position, document, element); + let item = new MyCompletionItem(position, document, element, enableDotCompletions); completionItems.push(item); } } diff --git a/extensions/typescript/src/features/definitionProvider.ts b/extensions/typescript/src/features/definitionProvider.ts index 73d87374783..ee364a63614 100644 --- a/extensions/typescript/src/features/definitionProvider.ts +++ b/extensions/typescript/src/features/definitionProvider.ts @@ -5,50 +5,18 @@ 'use strict'; -import { DefinitionProvider, TextDocument, Position, Range, CancellationToken, Definition, Location } from 'vscode'; +import { DefinitionProvider, TextDocument, Position, CancellationToken, Definition } from 'vscode'; -import * as Proto from '../protocol'; import { ITypescriptServiceClient } from '../typescriptService'; +import DefinitionProviderBase from './definitionProviderBase'; -export default class TypeScriptDefinitionProvider implements DefinitionProvider { - - private client: ITypescriptServiceClient; - - public tokens: string[] = []; +export default class TypeScriptDefinitionProvider extends DefinitionProviderBase implements DefinitionProvider { constructor(client: ITypescriptServiceClient) { - this.client = client; + super(client); } - public provideDefinition(document: TextDocument, position: Position, token: CancellationToken): Promise { - const filepath = this.client.asAbsolutePath(document.uri); - if (!filepath) { - return Promise.resolve(null); - } - let args: Proto.FileLocationRequestArgs = { - file: filepath, - line: position.line + 1, - offset: position.character + 1 - }; - if (!args.file) { - return Promise.resolve(null); - } - return this.client.execute('definition', args, token).then(response => { - let locations: Proto.FileSpan[] = response.body || []; - if (!locations || locations.length === 0) { - return [] as Definition; - } - return locations.map(location => { - let resource = this.client.asUrl(location.file); - if (resource === null) { - return null; - } else { - return new Location(resource, new Range(location.start.line - 1, location.start.offset - 1, location.end.line - 1, location.end.offset - 1)); - } - }).filter(x => x !== null) as Location[]; - }, (error) => { - this.client.error(`'definition' request failed with error.`, error); - return [] as Definition; - }); + public provideDefinition(document: TextDocument, position: Position, token: CancellationToken | boolean): Promise { + return this.getSymbolLocations('definition', document, position, token); } } \ No newline at end of file diff --git a/extensions/typescript/src/features/definitionProviderBase.ts b/extensions/typescript/src/features/definitionProviderBase.ts new file mode 100644 index 00000000000..94977e55076 --- /dev/null +++ b/extensions/typescript/src/features/definitionProviderBase.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { TextDocument, Position, Range, CancellationToken, Location } from 'vscode'; + +import * as Proto from '../protocol'; +import { ITypescriptServiceClient } from '../typescriptService'; + +export default class TypeScriptDefinitionProviderBase { + + private client: ITypescriptServiceClient; + + public tokens: string[] = []; + + constructor(client: ITypescriptServiceClient) { + this.client = client; + } + + protected getSymbolLocations(definitionType: 'definition' | 'implementation', document: TextDocument, position: Position, token: CancellationToken | boolean): Promise { + const filepath = this.client.asAbsolutePath(document.uri); + if (!filepath) { + return Promise.resolve(null); + } + let args: Proto.FileLocationRequestArgs = { + file: filepath, + line: position.line + 1, + offset: position.character + 1 + }; + if (!args.file) { + return Promise.resolve(null); + } + return this.client.execute(definitionType, args, token).then(response => { + let locations: Proto.FileSpan[] = (response && response.body) || []; + if (!locations || locations.length === 0) { + return []; + } + return locations.map(location => { + let resource = this.client.asUrl(location.file); + if (resource === null) { + return null; + } else { + return new Location(resource, new Range(location.start.line - 1, location.start.offset - 1, location.end.line - 1, location.end.offset - 1)); + } + }).filter(x => x !== null) as Location[]; + }, (error) => { + this.client.error(`'${definitionType}' request failed with error.`, error); + return []; + }); + } +} \ No newline at end of file diff --git a/extensions/typescript/src/features/referencesCodeLensProvider.ts b/extensions/typescript/src/features/referencesCodeLensProvider.ts index 26d3160db60..fa4197bbb64 100644 --- a/extensions/typescript/src/features/referencesCodeLensProvider.ts +++ b/extensions/typescript/src/features/referencesCodeLensProvider.ts @@ -73,14 +73,13 @@ export default class TypeScriptReferencesCodeLensProvider implements CodeLensPro // Exclude original definition from references const locations = response.body.refs .filter(reference => - reference.start.line !== codeLens.range.start.line + 1 - && reference.start.offset !== codeLens.range.start.character + 1) + !(reference.start.line === codeLens.range.start.line + 1 + && reference.start.offset === codeLens.range.start.character + 1)) .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: locations.length + ' ' + (locations.length === 1 ? localize('oneReferenceLabel', 'reference') : localize('manyReferenceLabel', 'references')), command: 'editor.action.showReferences', @@ -124,12 +123,17 @@ export default class TypeScriptReferencesCodeLensProvider implements CodeLensPro } // fallthrough + case PConst.Kind.class: + if (item.text === '') { + 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: diff --git a/extensions/typescript/src/features/typeDefinitionProvider.ts b/extensions/typescript/src/features/typeDefinitionProvider.ts new file mode 100644 index 00000000000..49c45283689 --- /dev/null +++ b/extensions/typescript/src/features/typeDefinitionProvider.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { TypeDefinitionProvider, TextDocument, Position, CancellationToken, Definition } from 'vscode'; + +import { ITypescriptServiceClient } from '../typescriptService'; +import DefinitionProviderBase from './definitionProviderBase'; + +export default class TypeScriptTypeDefinitionProvider extends DefinitionProviderBase implements TypeDefinitionProvider { + + constructor(client: ITypescriptServiceClient) { + super(client); + } + + public provideTypeDefinition(document: TextDocument, position: Position, token: CancellationToken | boolean): Promise { + return this.getSymbolLocations('implementation', document, position, token); + } +} \ No newline at end of file diff --git a/extensions/typescript/src/typescriptMain.ts b/extensions/typescript/src/typescriptMain.ts index 9b53a6bd255..97c8e8b3140 100644 --- a/extensions/typescript/src/typescriptMain.ts +++ b/extensions/typescript/src/typescriptMain.ts @@ -25,6 +25,7 @@ import { ITypescriptServiceClientHost } from './typescriptService'; import HoverProvider from './features/hoverProvider'; import DefinitionProvider from './features/definitionProvider'; +import TypeDefinitionProvider from './features/TypeDefinitionProvider'; import DocumentHighlightProvider from './features/documentHighlightProvider'; import ReferenceProvider from './features/referenceProvider'; import DocumentSymbolProvider from './features/documentSymbolProvider'; @@ -147,6 +148,7 @@ class LanguageProvider { let hoverProvider = new HoverProvider(client); let definitionProvider = new DefinitionProvider(client); + let typeDefinitionProvider = new TypeDefinitionProvider(client); let documentHighlightProvider = new DocumentHighlightProvider(client); let referenceProvider = new ReferenceProvider(client); let documentSymbolProvider = new DocumentSymbolProvider(client); @@ -169,6 +171,7 @@ class LanguageProvider { languages.registerCompletionItemProvider(selector, this.completionItemProvider, '.'); languages.registerHoverProvider(selector, hoverProvider); languages.registerDefinitionProvider(selector, definitionProvider); + languages.registerTypeDefinitionProvider(selector, typeDefinitionProvider); languages.registerDocumentHighlightProvider(selector, documentHighlightProvider); languages.registerReferenceProvider(selector, referenceProvider); languages.registerDocumentSymbolProvider(selector, documentSymbolProvider); diff --git a/extensions/typescript/src/typescriptService.ts b/extensions/typescript/src/typescriptService.ts index 14a8b65f86a..8f05acaaa6e 100644 --- a/extensions/typescript/src/typescriptService.ts +++ b/extensions/typescript/src/typescriptService.ts @@ -87,6 +87,7 @@ export interface ITypescriptServiceClient { execute(commant: 'completionEntryDetails', args: Proto.CompletionDetailsRequestArgs, token?: CancellationToken): Promise; execute(commant: 'signatureHelp', args: Proto.SignatureHelpRequestArgs, token?: CancellationToken): Promise; execute(command: 'definition', args: Proto.FileLocationRequestArgs, token?: CancellationToken): Promise; + execute(command: 'implementation', args: Proto.FileLocationRequestArgs, token?: CancellationToken): Promise; execute(command: 'references', args: Proto.FileLocationRequestArgs, token?: CancellationToken): Promise; execute(command: 'navto', args: Proto.NavtoRequestArgs, token?: CancellationToken): Promise; execute(command: 'navbar', args: Proto.FileRequestArgs, token?: CancellationToken): Promise; diff --git a/gulpfile.js b/gulpfile.js index f82bdbf2bb5..70aab71c296 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -58,8 +58,14 @@ var ALL_EDITOR_TASKS = [ 'tslint', 'hygiene', ]; + var runningEditorTasks = process.argv.length > 2 && process.argv.slice(2).every(function (arg) { return (ALL_EDITOR_TASKS.indexOf(arg) !== -1); }); +process.on('unhandledRejection', (reason, p) => { + console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); + process.exit(1); +}); + if (runningEditorTasks) { require(`./build/gulpfile.editor`); require(`./build/gulpfile.hygiene`); diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 671e5031fe0..8d541e33c20 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -432,7 +432,7 @@ "xterm": { "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#8fb0947bf7d2e506b16b5425c71726c25a64475b" + "resolved": "git+https://github.com/Tyriar/xterm.js.git#36c63323c3f940636e799ae6e0168b2dfd7a3d21" }, "yauzl": { "version": "2.3.1", diff --git a/package.json b/package.json index 918583b8c91..3b4dca5b90f 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "code-oss-dev", "version": "1.9.0", "electronVersion": "1.4.6", - "distro": "ef07477c3bbf2aa2f274b13093cbe0d96fa59fdd", + "distro": "07e460da0e2854e12c183abaa5acbcd7b8c48d7f", "author": { "name": "Microsoft Corporation" }, @@ -67,7 +67,7 @@ "gulp-rename": "^1.2.0", "gulp-replace": "^0.5.4", "gulp-shell": "^0.5.2", - "gulp-sourcemaps": "^1.6.0", + "gulp-sourcemaps": "^1.11.0", "gulp-tsb": "^2.0.3", "gulp-tslint": "^7.0.1", "gulp-uglify": "^2.0.0", diff --git a/src/vs/base/browser/ui/selectBox/selectBox.ts b/src/vs/base/browser/ui/selectBox/selectBox.ts index f793b135d48..72c68596089 100644 --- a/src/vs/base/browser/ui/selectBox/selectBox.ts +++ b/src/vs/base/browser/ui/selectBox/selectBox.ts @@ -40,13 +40,14 @@ export class SelectBox extends Widget { return this._onDidSelect.event; } - public setOptions(options: string[], selected?: number): void { + public setOptions(options: string[], selected?: number, disabled?: number): void { if (!this.options || !arrays.equals(this.options, options)) { this.options = options; this.selectElement.options.length = 0; + let i = 0; this.options.forEach((option) => { - this.selectElement.add(this.createOption(option)); + this.selectElement.add(this.createOption(option, disabled === i++)); }); } this.select(selected); @@ -91,10 +92,11 @@ export class SelectBox extends Widget { this.setOptions(this.options, this.selected); } - private createOption(value: string): HTMLOptionElement { + private createOption(value: string, disabled?: boolean): HTMLOptionElement { let option = document.createElement('option'); option.value = value; option.text = value; + option.disabled = disabled; return option; } diff --git a/src/vs/editor/browser/controller/keyboardHandler.ts b/src/vs/editor/browser/controller/keyboardHandler.ts index 070e69cb887..89ceea1c90a 100644 --- a/src/vs/editor/browser/controller/keyboardHandler.ts +++ b/src/vs/editor/browser/controller/keyboardHandler.ts @@ -157,7 +157,7 @@ export class KeyboardHandler extends ViewEventHandler implements IDisposable { } public focusTextArea(): void { - this.textAreaHandler.writePlaceholderAndSelectTextAreaSync(); + this.textAreaHandler.focusTextArea(); } public onConfigurationChanged(e: editorCommon.IConfigurationChangedEvent): boolean { diff --git a/src/vs/editor/browser/controller/mouseHandler.ts b/src/vs/editor/browser/controller/mouseHandler.ts index 2066f45128b..f6762fa1ddd 100644 --- a/src/vs/editor/browser/controller/mouseHandler.ts +++ b/src/vs/editor/browser/controller/mouseHandler.ts @@ -17,7 +17,7 @@ import * as editorBrowser from 'vs/editor/browser/editorBrowser'; import { TimeoutTimer, RunOnceScheduler } from 'vs/base/common/async'; import { ViewContext } from 'vs/editor/common/view/viewContext'; import { VisibleRange } from 'vs/editor/common/view/renderingContext'; -import { EditorMouseEventFactory, GlobalEditorMouseMoveMonitor, EditorMouseEvent } from 'vs/editor/browser/editorDom'; +import { EditorMouseEventFactory, GlobalEditorMouseMoveMonitor, EditorMouseEvent, createEditorPagePosition, ClientCoordinates } from 'vs/editor/browser/editorDom'; import { StandardMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { EditorZoom } from 'vs/editor/common/config/editorZoom'; import { IViewCursorRenderData } from 'vs/editor/browser/viewParts/viewCursors/viewCursor'; @@ -204,9 +204,7 @@ export class MouseHandler extends ViewEventHandler implements IDisposable { } // --- begin event handlers - _layoutInfo: editorCommon.EditorLayoutInfo; public onLayoutChanged(layoutInfo: editorCommon.EditorLayoutInfo): boolean { - this._layoutInfo = layoutInfo; return false; } public onScrollChanged(e: editorCommon.IScrollEvent): boolean { @@ -224,13 +222,26 @@ export class MouseHandler extends ViewEventHandler implements IDisposable { } // --- end event handlers + public getTargetAtClientPoint(clientX: number, clientY: number): editorBrowser.IMouseTarget { + let clientPos = new ClientCoordinates(clientX, clientY); + let pos = clientPos.toPageCoordinates(); + let editorPos = createEditorPagePosition(this.viewHelper.viewDomNode); + + if (pos.y < editorPos.y || pos.y > editorPos.y + editorPos.height || pos.x < editorPos.x || pos.x > editorPos.x + editorPos.width) { + return null; + } + + let lastViewCursorsRenderData = this.viewHelper.getLastViewCursorsRenderData(); + return this.mouseTargetFactory.createMouseTarget(lastViewCursorsRenderData, editorPos, pos, null); + } + protected _createMouseTarget(e: EditorMouseEvent, testEventTarget: boolean): editorBrowser.IMouseTarget { let lastViewCursorsRenderData = this.viewHelper.getLastViewCursorsRenderData(); - return this.mouseTargetFactory.createMouseTarget(this._layoutInfo, lastViewCursorsRenderData, e, testEventTarget); + return this.mouseTargetFactory.createMouseTarget(lastViewCursorsRenderData, e.editorPos, e.pos, testEventTarget ? e.target : null); } private _getMouseColumn(e: EditorMouseEvent): number { - return this.mouseTargetFactory.getMouseColumn(this._layoutInfo, e); + return this.mouseTargetFactory.getMouseColumn(e.editorPos, e.pos); } protected _onContextMenu(e: EditorMouseEvent, testEventTarget: boolean): void { @@ -453,23 +464,23 @@ class MouseDownOperation extends Disposable { let mouseColumn = this._getMouseColumn(e); - if (e.posy < editorContent.top) { - let aboveLineNumber = this._viewHelper.getLineNumberAtVerticalOffset(Math.max(this._viewHelper.getScrollTop() - (editorContent.top - e.posy), 0)); + if (e.posy < editorContent.y) { + let aboveLineNumber = this._viewHelper.getLineNumberAtVerticalOffset(Math.max(this._viewHelper.getScrollTop() - (editorContent.y - e.posy), 0)); return new MousePosition(new Position(aboveLineNumber, 1), mouseColumn); } - if (e.posy > editorContent.top + editorContent.height) { - let belowLineNumber = this._viewHelper.getLineNumberAtVerticalOffset(this._viewHelper.getScrollTop() + (e.posy - editorContent.top)); + if (e.posy > editorContent.y + editorContent.height) { + let belowLineNumber = this._viewHelper.getLineNumberAtVerticalOffset(this._viewHelper.getScrollTop() + (e.posy - editorContent.y)); return new MousePosition(new Position(belowLineNumber, this._context.model.getLineMaxColumn(belowLineNumber)), mouseColumn); } - let possibleLineNumber = this._viewHelper.getLineNumberAtVerticalOffset(this._viewHelper.getScrollTop() + (e.posy - editorContent.top)); + let possibleLineNumber = this._viewHelper.getLineNumberAtVerticalOffset(this._viewHelper.getScrollTop() + (e.posy - editorContent.y)); - if (e.posx < editorContent.left) { + if (e.posx < editorContent.x) { return new MousePosition(new Position(possibleLineNumber, 1), mouseColumn); } - if (e.posx > editorContent.left + editorContent.width) { + if (e.posx > editorContent.x + editorContent.width) { return new MousePosition(new Position(possibleLineNumber, this._context.model.getLineMaxColumn(possibleLineNumber)), mouseColumn); } diff --git a/src/vs/editor/browser/controller/mouseTarget.ts b/src/vs/editor/browser/controller/mouseTarget.ts index c0610bd7d92..c1fefa4ce52 100644 --- a/src/vs/editor/browser/controller/mouseTarget.ts +++ b/src/vs/editor/browser/controller/mouseTarget.ts @@ -10,11 +10,11 @@ import { EditorLayoutInfo, MouseTargetType } from 'vs/editor/common/editorCommon import { ClassNames, IMouseTarget, IViewZoneData } from 'vs/editor/browser/editorBrowser'; import { ViewContext } from 'vs/editor/common/view/viewContext'; import { IPointerHandlerHelper } from 'vs/editor/browser/controller/mouseHandler'; -import { EditorMouseEvent } from 'vs/editor/browser/editorDom'; -import * as dom from 'vs/base/browser/dom'; +import { EditorMouseEvent, PageCoordinates, ClientCoordinates, EditorPagePosition } from 'vs/editor/browser/editorDom'; import * as browser from 'vs/base/browser/browser'; import { IViewCursorRenderData } from 'vs/editor/browser/viewParts/viewCursors/viewCursor'; import { PartFingerprint, PartFingerprints } from 'vs/editor/browser/view/viewPart'; +import { IViewModel } from 'vs/editor/common/viewModel/viewModel'; interface IETextRange { boundingHeight: number; @@ -68,12 +68,12 @@ interface IHitTestResult { class MouseTarget implements IMouseTarget { - public element: Element; - public type: MouseTargetType; - public mouseColumn: number; - public position: Position; - public range: EditorRange; - public detail: any; + public readonly element: Element; + public readonly type: MouseTargetType; + public readonly mouseColumn: number; + public readonly position: Position; + public readonly range: EditorRange; + public readonly detail: any; constructor(element: Element, type: MouseTargetType, mouseColumn: number = 0, position: Position = null, range: EditorRange = null, detail: any = null) { this.element = element; @@ -133,13 +133,6 @@ class MouseTarget implements IMouseTarget { } class ElementPath { - public static isTextAreaCover(path: Uint8Array): boolean { - return ( - path.length === 2 - && path[0] === PartFingerprint.OverflowGuard - && path[1] === PartFingerprint.TextAreaCover - ); - } public static isTextArea(path: Uint8Array): boolean { return ( @@ -149,14 +142,6 @@ class ElementPath { ); } - public static isViewLines(path: Uint8Array): boolean { - return ( - path.length === 4 - && path[0] === PartFingerprint.OverflowGuard - && path[3] === PartFingerprint.ViewLines - ); - } - public static isChildOfViewLines(path: Uint8Array): boolean { return ( path.length >= 4 @@ -165,14 +150,6 @@ class ElementPath { ); } - public static isCursorsLayer(path: Uint8Array): boolean { - return ( - path.length === 4 - && path[0] === PartFingerprint.OverflowGuard - && path[3] === PartFingerprint.ViewCursorsLayer - ); - } - public static isChildOfScrollableElement(path: Uint8Array): boolean { return ( path.length >= 2 @@ -203,439 +180,32 @@ class ElementPath { && path[1] === PartFingerprint.OverlayWidgets ); } - - public static isChildOfMargin(path: Uint8Array): boolean { - return ( - path.length >= 2 - && path[0] === PartFingerprint.OverflowGuard - && path[1] === PartFingerprint.Margin - ); - } - - public static isChildOfViewZones(path: Uint8Array): boolean { - return ( - path.length >= 4 - && path[0] === PartFingerprint.OverflowGuard - && path[3] === PartFingerprint.ViewZones - ); - } - - public static isOverviewRuler(path: Uint8Array): boolean { - return ( - path.length === 3 - && path[0] === PartFingerprint.OverflowGuard - && path[2] === PartFingerprint.OverviewRuler - ); - } } -export class MouseTargetFactory { +class HitTestContext { - private _context: ViewContext; - private _viewHelper: IPointerHandlerHelper; + public readonly model: IViewModel; + public readonly layoutInfo: EditorLayoutInfo; + public readonly viewDomNode: HTMLElement; + public readonly lineHeight: number; + public readonly typicalHalfwidthCharacterWidth: number; + public readonly lastViewCursorsRenderData: IViewCursorRenderData[]; - constructor(context: ViewContext, viewHelper: IPointerHandlerHelper) { + private readonly _context: ViewContext; + private readonly _viewHelper: IPointerHandlerHelper; + + constructor(context: ViewContext, viewHelper: IPointerHandlerHelper, lastViewCursorsRenderData: IViewCursorRenderData[]) { + this.model = context.model; + this.layoutInfo = context.configuration.editor.layoutInfo; + this.viewDomNode = viewHelper.viewDomNode; + this.lineHeight = context.configuration.editor.lineHeight; + this.typicalHalfwidthCharacterWidth = context.configuration.editor.fontInfo.typicalHalfwidthCharacterWidth; + this.lastViewCursorsRenderData = lastViewCursorsRenderData; this._context = context; this._viewHelper = viewHelper; } - public mouseTargetIsWidget(e: EditorMouseEvent): boolean { - let t = e.target; - let path = PartFingerprints.collect(t, this._viewHelper.viewDomNode); - - // Is it a content widget? - if (ElementPath.isChildOfContentWidgets(path) || ElementPath.isChildOfOverflowingContentWidgets(path)) { - return true; - } - - // Is it an overlay widget? - if (ElementPath.isChildOfOverlayWidgets(path)) { - return true; - } - - return false; - } - - public createMouseTarget(layoutInfo: EditorLayoutInfo, lastViewCursorsRenderData: IViewCursorRenderData[], e: EditorMouseEvent, testEventTarget: boolean): IMouseTarget { - try { - let r = this._unsafeCreateMouseTarget(layoutInfo, lastViewCursorsRenderData, e, testEventTarget); - return r; - } catch (e) { - return this.createMouseTargetFromUnknownTarget(e.target); - } - } - - private _unsafeCreateMouseTarget(layoutInfo: EditorLayoutInfo, lastViewCursorsRenderData: IViewCursorRenderData[], e: EditorMouseEvent, testEventTarget: boolean): IMouseTarget { - let mouseVerticalOffset = Math.max(0, this._viewHelper.getScrollTop() + (e.posy - e.editorPos.top)); - let mouseContentHorizontalOffset = this._viewHelper.getScrollLeft() + (e.posx - e.editorPos.left) - layoutInfo.contentLeft; - let mouseColumn = this._getMouseColumn(mouseContentHorizontalOffset); - // console.log(`mouseVerticalOffset: ${mouseVerticalOffset}, mouseContentHorizontalOffset: ${mouseContentHorizontalOffset}, mouseColumn: ${mouseColumn}`) - - let t = e.target; - - // Edge has a bug when hit-testing the exact position of a cursor, - // instead of returning the correct dom node, it returns the - // first or last rendered view line dom node, therefore help it out - // and first check if we are on top of a cursor - for (let i = 0, len = lastViewCursorsRenderData.length; i < len; i++) { - let d = lastViewCursorsRenderData[i]; - - if ( - d.contentLeft <= mouseContentHorizontalOffset - && mouseContentHorizontalOffset <= d.contentLeft + d.width - && d.contentTop <= mouseVerticalOffset - && mouseVerticalOffset <= d.contentTop + d.height - ) { - return this.createMouseTargetFromViewCursor(t, d.position.lineNumber, d.position.column, mouseColumn); - } - } - - let path = PartFingerprints.collect(t, this._viewHelper.viewDomNode); - - // Is it a content widget? - if (ElementPath.isChildOfContentWidgets(path) || ElementPath.isChildOfOverflowingContentWidgets(path)) { - return this.createMouseTargetFromContentWidgetsChild(t, mouseColumn); - } - - // Is it an overlay widget? - if (ElementPath.isChildOfOverlayWidgets(path)) { - return this.createMouseTargetFromOverlayWidgetsChild(t, mouseColumn); - } - - // Is it a cursor ? - let lineNumberAttribute = t.hasAttribute && t.hasAttribute('lineNumber') ? t.getAttribute('lineNumber') : null; - let columnAttribute = t.hasAttribute && t.hasAttribute('column') ? t.getAttribute('column') : null; - if (lineNumberAttribute && columnAttribute) { - return this.createMouseTargetFromViewCursor(t, parseInt(lineNumberAttribute, 10), parseInt(columnAttribute, 10), mouseColumn); - } - - // Is it the textarea cover? - if (ElementPath.isTextAreaCover(path)) { - if (this._context.configuration.editor.viewInfo.glyphMargin) { - return this.createMouseTargetFromGlyphMargin(t, mouseVerticalOffset, mouseColumn); - } else if (this._context.configuration.editor.viewInfo.renderLineNumbers) { - return this.createMouseTargetFromLineNumbers(t, mouseVerticalOffset, mouseColumn); - } else { - return this.createMouseTargetFromLinesDecorationsChild(t, mouseVerticalOffset, mouseColumn); - } - } - - // Is it the textarea? - if (ElementPath.isTextArea(path)) { - return new MouseTarget(t, MouseTargetType.TEXTAREA); - } - - // Is it a view zone? - if (ElementPath.isChildOfViewZones(path)) { - // Check if it is at a view zone - let viewZoneData = this._getZoneAtCoord(mouseVerticalOffset); - if (viewZoneData) { - return new MouseTarget(t, MouseTargetType.CONTENT_VIEW_ZONE, mouseColumn, viewZoneData.position, null, viewZoneData); - } - return this.createMouseTargetFromUnknownTarget(t); - } - - // Is it the view lines container? - if (ElementPath.isViewLines(path)) { - // Sometimes, IE returns this target when right clicking on top of text - // -> See Bug #12990: [F12] Context menu shows incorrect position while doing a resize - - // Check if it is below any lines and any view zones - if (this._viewHelper.isAfterLines(mouseVerticalOffset)) { - return this.createMouseTargetFromViewLines(t, mouseVerticalOffset, mouseColumn); - } - - // Check if it is at a view zone - let viewZoneData = this._getZoneAtCoord(mouseVerticalOffset); - if (viewZoneData) { - return new MouseTarget(t, MouseTargetType.CONTENT_VIEW_ZONE, mouseColumn, viewZoneData.position, null, viewZoneData); - } - - // Check if it hits a position - let hitTestResult = this._doHitTest(e, mouseVerticalOffset); - if (hitTestResult.position) { - return this.createMouseTargetFromHitTestPosition(t, hitTestResult.position.lineNumber, hitTestResult.position.column, mouseContentHorizontalOffset, mouseColumn); - } - - // Fall back to view lines - return this.createMouseTargetFromViewLines(t, mouseVerticalOffset, mouseColumn); - } - - // Is it a child of the view lines container? - if (!testEventTarget || ElementPath.isChildOfViewLines(path)) { - let hitTestResult = this._doHitTest(e, mouseVerticalOffset); - if (hitTestResult.position) { - return this.createMouseTargetFromHitTestPosition(t, hitTestResult.position.lineNumber, hitTestResult.position.column, mouseContentHorizontalOffset, mouseColumn); - } else if (hitTestResult.hitTarget) { - t = hitTestResult.hitTarget; - path = PartFingerprints.collect(t, this._viewHelper.viewDomNode); - - // TODO@Alex: try again with this different target, but guard against recursion. - // Is it a cursor ? - let lineNumberAttribute = t.hasAttribute && t.hasAttribute('lineNumber') ? t.getAttribute('lineNumber') : null; - let columnAttribute = t.hasAttribute && t.hasAttribute('column') ? t.getAttribute('column') : null; - if (lineNumberAttribute && columnAttribute) { - return this.createMouseTargetFromViewCursor(t, parseInt(lineNumberAttribute, 10), parseInt(columnAttribute, 10), mouseColumn); - } - } else { - // Hit testing completely failed... - let possibleLineNumber = this._viewHelper.getLineNumberAtVerticalOffset(mouseVerticalOffset); - let maxColumn = this._context.model.getLineMaxColumn(possibleLineNumber); - return new MouseTarget(t, MouseTargetType.CONTENT_EMPTY, mouseColumn, new Position(possibleLineNumber, maxColumn)); - } - } - - // Is it the cursors layer? - if (ElementPath.isCursorsLayer(path)) { - return new MouseTarget(t, MouseTargetType.UNKNOWN); - } - - // Is it a child of the scrollable element? - if (ElementPath.isChildOfScrollableElement(path)) { - return this.createMouseTargetFromScrollbar(t, mouseVerticalOffset, mouseColumn); - } - - if (ElementPath.isChildOfMargin(path)) { - let offset = Math.abs(e.posx - e.editorPos.left); - - if (offset <= layoutInfo.glyphMarginWidth) { - // On the glyph margin - return this.createMouseTargetFromGlyphMargin(t, mouseVerticalOffset, mouseColumn); - } - offset -= layoutInfo.glyphMarginWidth; - - if (offset <= layoutInfo.lineNumbersWidth) { - // On the line numbers - return this.createMouseTargetFromLineNumbers(t, mouseVerticalOffset, mouseColumn); - } - offset -= layoutInfo.lineNumbersWidth; - - // On the line decorations - return this.createMouseTargetFromLinesDecorationsChild(t, mouseVerticalOffset, mouseColumn); - } - - if (ElementPath.isOverviewRuler(path)) { - return this.createMouseTargetFromScrollbar(t, mouseVerticalOffset, mouseColumn); - } - - return this.createMouseTargetFromUnknownTarget(t); - } - - private _isChild(testChild: Node, testAncestor: Node, stopAt: Node): boolean { - while (testChild && testChild !== document.body) { - if (testChild === testAncestor) { - return true; - } - if (testChild === stopAt) { - return false; - } - testChild = testChild.parentNode; - } - return false; - } - - private _findAttribute(element: Element, attr: string, stopAt: Element): string { - while (element && element !== document.body) { - if (element.hasAttribute && element.hasAttribute(attr)) { - return element.getAttribute(attr); - } - if (element === stopAt) { - return null; - } - element = element.parentNode; - } - return null; - } - - /** - * Most probably WebKit browsers and Edge - */ - private _doHitTestWithCaretRangeFromPoint(e: EditorMouseEvent, mouseVerticalOffset: number): IHitTestResult { - - // In Chrome, especially on Linux it is possible to click between lines, - // so try to adjust the `hity` below so that it lands in the center of a line - let lineNumber = this._viewHelper.getLineNumberAtVerticalOffset(mouseVerticalOffset); - let lineVerticalOffset = this._viewHelper.getVerticalOffsetForLineNumber(lineNumber); - let centeredVerticalOffset = lineVerticalOffset + Math.floor(this._context.configuration.editor.lineHeight / 2); - let adjustedPosy = e.posy + (centeredVerticalOffset - mouseVerticalOffset); - - if (adjustedPosy <= e.editorPos.top) { - adjustedPosy = e.editorPos.top + 1; - } - if (adjustedPosy >= e.editorPos.top + this._context.configuration.editor.layoutInfo.height) { - adjustedPosy = e.editorPos.top + this._context.configuration.editor.layoutInfo.height - 1; - } - - let r = this._actualDoHitTestWithCaretRangeFromPoint(e.viewportx, adjustedPosy - dom.StandardWindow.scrollY); - if (r.position) { - return r; - } - - // Also try to hit test without the adjustment (for the edge cases that we are near the top or bottom) - return this._actualDoHitTestWithCaretRangeFromPoint(e.viewportx, e.viewporty); - } - - private _actualDoHitTestWithCaretRangeFromPoint(hitx: number, hity: number): IHitTestResult { - - let range: Range = (document).caretRangeFromPoint(hitx, hity); - - if (!range || !range.startContainer) { - return { - position: null, - hitTarget: null - }; - } - - // Chrome always hits a TEXT_NODE, while Edge sometimes hits a token span - let startContainer = range.startContainer; - let hitTarget: HTMLElement; - - if (startContainer.nodeType === startContainer.TEXT_NODE) { - // startContainer is expected to be the token text - let parent1 = startContainer.parentNode; // expected to be the token span - let parent2 = parent1 ? parent1.parentNode : null; // expected to be the view line container span - let parent3 = parent2 ? parent2.parentNode : null; // expected to be the view line div - let parent3ClassName = parent3 && parent3.nodeType === parent3.ELEMENT_NODE ? (parent3).className : null; - - if (parent3ClassName === ClassNames.VIEW_LINE) { - let p = this._viewHelper.getPositionFromDOMInfo(parent1, range.startOffset); - return { - position: p, - hitTarget: null - }; - } else { - hitTarget = startContainer.parentNode; - } - } else if (startContainer.nodeType === startContainer.ELEMENT_NODE) { - // startContainer is expected to be the token span - let parent1 = startContainer.parentNode; // expected to be the view line container span - let parent2 = parent1 ? parent1.parentNode : null; // expected to be the view line div - let parent2ClassName = parent2 && parent2.nodeType === parent2.ELEMENT_NODE ? (parent2).className : null; - - if (parent2ClassName === ClassNames.VIEW_LINE) { - let p = this._viewHelper.getPositionFromDOMInfo(startContainer, (startContainer).textContent.length); - return { - position: p, - hitTarget: null - }; - } else { - hitTarget = startContainer; - } - } - - return { - position: null, - hitTarget: hitTarget - }; - } - - /** - * Most probably Gecko - */ - private _doHitTestWithCaretPositionFromPoint(e: EditorMouseEvent): IHitTestResult { - let hitResult: { offsetNode: Node; offset: number; } = (document).caretPositionFromPoint(e.viewportx, e.viewporty); - - let range = document.createRange(); - range.setStart(hitResult.offsetNode, hitResult.offset); - range.collapse(true); - let resultPosition = this._viewHelper.getPositionFromDOMInfo(range.startContainer.parentNode, range.startOffset); - range.detach(); - - return { - position: resultPosition, - hitTarget: null - }; - } - - /** - * Most probably IE - */ - private _doHitTestWithMoveToPoint(e: EditorMouseEvent): IHitTestResult { - let resultPosition: Position = null; - let resultHitTarget: Element = null; - - let textRange: IETextRange = (document.body).createTextRange(); - try { - textRange.moveToPoint(e.viewportx, e.viewporty); - } catch (err) { - return { - position: null, - hitTarget: null - }; - } - - textRange.collapse(true); - - // Now, let's do our best to figure out what we hit :) - let parentElement = textRange ? textRange.parentElement() : null; - let parent1 = parentElement ? parentElement.parentNode : null; - let parent2 = parent1 ? parent1.parentNode : null; - - let parent2ClassName = parent2 && parent2.nodeType === parent2.ELEMENT_NODE ? (parent2).className : ''; - - if (parent2ClassName === ClassNames.VIEW_LINE) { - let rangeToContainEntireSpan = textRange.duplicate(); - rangeToContainEntireSpan.moveToElementText(parentElement); - rangeToContainEntireSpan.setEndPoint('EndToStart', textRange); - - resultPosition = this._viewHelper.getPositionFromDOMInfo(parentElement, rangeToContainEntireSpan.text.length); - // Move range out of the span node, IE doesn't like having many ranges in - // the same spot and will act badly for lines containing dashes ('-') - rangeToContainEntireSpan.moveToElementText(this._viewHelper.viewDomNode); - } else { - // Looks like we've hit the hover or something foreign - resultHitTarget = parentElement; - } - - // Move range out of the span node, IE doesn't like having many ranges in - // the same spot and will act badly for lines containing dashes ('-') - textRange.moveToElementText(this._viewHelper.viewDomNode); - - return { - position: resultPosition, - hitTarget: resultHitTarget - }; - } - - private _doHitTest(e: EditorMouseEvent, mouseVerticalOffset: number): IHitTestResult { - // State of the art (18.10.2012): - // The spec says browsers should support document.caretPositionFromPoint, but nobody implemented it (http://dev.w3.org/csswg/cssom-view/) - // Gecko: - // - they tried to implement it once, but failed: https://bugzilla.mozilla.org/show_bug.cgi?id=654352 - // - however, they do give out rangeParent/rangeOffset properties on mouse events - // Webkit: - // - they have implemented a previous version of the spec which was using document.caretRangeFromPoint - // IE: - // - they have a proprietary method on ranges, moveToPoint: https://msdn.microsoft.com/en-us/library/ie/ms536632(v=vs.85).aspx - - // 24.08.2016: Edge has added WebKit's document.caretRangeFromPoint, but it is quite buggy - // - when hit testing the cursor it returns the first or the last line in the viewport - // - it inconsistently hits text nodes or span nodes, while WebKit only hits text nodes - // - when toggling render whitespace on, and hit testing in the empty content after a line, it always hits offset 0 of the first span of the line - - // Thank you browsers for making this so 'easy' :) - - if ((document).caretRangeFromPoint) { - - return this._doHitTestWithCaretRangeFromPoint(e, mouseVerticalOffset); - - } else if ((document).caretPositionFromPoint) { - - return this._doHitTestWithCaretPositionFromPoint(e); - - } else if ((document.body).createTextRange) { - - return this._doHitTestWithMoveToPoint(e); - - } - - return { - position: null, - hitTarget: null - }; - } - - private _getZoneAtCoord(mouseVerticalOffset: number): IViewZoneData { + public getZoneAtCoord(mouseVerticalOffset: number): IViewZoneData { // The target is either a view zone or the empty space after the last view-line let viewZoneWhitespace = this._viewHelper.getWhitespaceAtVerticalOffset(mouseVerticalOffset); @@ -676,7 +246,7 @@ export class MouseTargetFactory { return null; } - private _getFullLineRangeAtCoord(mouseVerticalOffset: number): { range: EditorRange; isAfterLines: boolean; } { + public getFullLineRangeAtCoord(mouseVerticalOffset: number): { range: EditorRange; isAfterLines: boolean; } { if (this._viewHelper.isAfterLines(mouseVerticalOffset)) { // Below the last line let lineNumber = this._context.model.getLineCount(); @@ -695,153 +265,604 @@ export class MouseTargetFactory { }; } - public getMouseColumn(layoutInfo: EditorLayoutInfo, e: EditorMouseEvent): number { - let mouseContentHorizontalOffset = this._viewHelper.getScrollLeft() + (e.posx - e.editorPos.left) - layoutInfo.contentLeft; - return this._getMouseColumn(mouseContentHorizontalOffset); + public getLineNumberAtVerticalOffset(mouseVerticalOffset: number): number { + return this._viewHelper.getLineNumberAtVerticalOffset(mouseVerticalOffset); } - private _getMouseColumn(mouseContentHorizontalOffset: number): number { + public isAfterLines(mouseVerticalOffset: number): boolean { + return this._viewHelper.isAfterLines(mouseVerticalOffset); + } + + public getVerticalOffsetForLineNumber(lineNumber: number): number { + return this._viewHelper.getVerticalOffsetForLineNumber(lineNumber); + } + + public findAttribute(element: Element, attr: string): string { + return HitTestContext._findAttribute(element, attr, this._viewHelper.viewDomNode); + } + + private static _findAttribute(element: Element, attr: string, stopAt: Element): string { + while (element && element !== document.body) { + if (element.hasAttribute && element.hasAttribute(attr)) { + return element.getAttribute(attr); + } + if (element === stopAt) { + return null; + } + element = element.parentNode; + } + return null; + } + + public getLineWidth(lineNumber: number): number { + return this._viewHelper.getLineWidth(lineNumber); + } + + public visibleRangeForPosition2(lineNumber: number, column: number) { + return this._viewHelper.visibleRangeForPosition2(lineNumber, column); + } + + public getPositionFromDOMInfo(spanNode: HTMLElement, offset: number): Position { + return this._viewHelper.getPositionFromDOMInfo(spanNode, offset); + } + + public getScrollTop(): number { + return this._viewHelper.getScrollTop(); + } + + public getScrollLeft(): number { + return this._viewHelper.getScrollLeft(); + } +} + +abstract class BareHitTestRequest { + + public readonly editorPos: EditorPagePosition; + public readonly pos: PageCoordinates; + public readonly mouseVerticalOffset: number; + public readonly isInMarginArea: boolean; + public readonly isInContentArea: boolean; + public readonly mouseContentHorizontalOffset: number; + + protected readonly mouseColumn: number; + + constructor(ctx: HitTestContext, editorPos: EditorPagePosition, pos: PageCoordinates) { + this.editorPos = editorPos; + this.pos = pos; + + this.mouseVerticalOffset = Math.max(0, ctx.getScrollTop() + pos.y - editorPos.y); + this.mouseContentHorizontalOffset = ctx.getScrollLeft() + pos.x - editorPos.x - ctx.layoutInfo.contentLeft; + this.isInMarginArea = (pos.x - editorPos.x < ctx.layoutInfo.contentLeft); + this.isInContentArea = !this.isInMarginArea; + this.mouseColumn = Math.max(0, MouseTargetFactory._getMouseColumn(this.mouseContentHorizontalOffset, ctx.typicalHalfwidthCharacterWidth)); + } +} + +class HitTestRequest extends BareHitTestRequest { + private readonly _ctx: HitTestContext; + public readonly target: Element; + public readonly targetPath: Uint8Array; + + constructor(ctx: HitTestContext, editorPos: EditorPagePosition, pos: PageCoordinates, target: Element) { + super(ctx, editorPos, pos); + this._ctx = ctx; + + if (target) { + this.target = target; + this.targetPath = PartFingerprints.collect(target, ctx.viewDomNode); + } else { + this.target = null; + this.targetPath = new Uint8Array(0); + } + } + + public toString(): string { + return `pos(${this.pos.x},${this.pos.y}), editorPos(${this.editorPos.x},${this.editorPos.y}), mouseVerticalOffset: ${this.mouseVerticalOffset}, mouseContentHorizontalOffset: ${this.mouseContentHorizontalOffset}\n\ttarget: ${this.target ? (this.target).outerHTML : null}`; + } + + public fulfill(type: MouseTargetType, position: Position = null, range: EditorRange = null, detail: any = null): MouseTarget { + return new MouseTarget(this.target, type, this.mouseColumn, position, range, detail); + } + + public withTarget(target: Element): HitTestRequest { + return new HitTestRequest(this._ctx, this.editorPos, this.pos, target); + } +} + +export class MouseTargetFactory { + + private _context: ViewContext; + private _viewHelper: IPointerHandlerHelper; + + constructor(context: ViewContext, viewHelper: IPointerHandlerHelper) { + this._context = context; + this._viewHelper = viewHelper; + } + + public mouseTargetIsWidget(e: EditorMouseEvent): boolean { + let t = e.target; + let path = PartFingerprints.collect(t, this._viewHelper.viewDomNode); + + // Is it a content widget? + if (ElementPath.isChildOfContentWidgets(path) || ElementPath.isChildOfOverflowingContentWidgets(path)) { + return true; + } + + // Is it an overlay widget? + if (ElementPath.isChildOfOverlayWidgets(path)) { + return true; + } + + return false; + } + + public createMouseTarget(lastViewCursorsRenderData: IViewCursorRenderData[], editorPos: EditorPagePosition, pos: PageCoordinates, target: HTMLElement): IMouseTarget { + const ctx = new HitTestContext(this._context, this._viewHelper, lastViewCursorsRenderData); + const request = new HitTestRequest(ctx, editorPos, pos, target); + try { + let r = MouseTargetFactory._createMouseTarget(ctx, request, false); + // console.log(r.toString()); + return r; + } catch (err) { + // console.log(err); + return request.fulfill(MouseTargetType.UNKNOWN); + } + } + + private static _createMouseTarget(ctx: HitTestContext, request: HitTestRequest, domHitTestExecuted: boolean): MouseTarget { + + // console.log(`${domHitTestExecuted ? '=>' : ''}CAME IN REQUEST: ${request}`); + + // First ensure the request has a target + if (request.target === null) { + if (domHitTestExecuted) { + // Still no target... and we have already executed hit test... + return request.fulfill(MouseTargetType.UNKNOWN); + } + + const hitTestResult = MouseTargetFactory._doHitTest(ctx, request); + + if (hitTestResult.position) { + return MouseTargetFactory.createMouseTargetFromHitTestPosition(ctx, request, hitTestResult.position.lineNumber, hitTestResult.position.column); + } + + return this._createMouseTarget(ctx, request.withTarget(hitTestResult.hitTarget), true); + } + + let result: MouseTarget = null; + + result = result || MouseTargetFactory._hitTestContentWidget(ctx, request); + result = result || MouseTargetFactory._hitTestOverlayWidget(ctx, request); + result = result || MouseTargetFactory._hitTestViewZone(ctx, request); + result = result || MouseTargetFactory._hitTestMargin(ctx, request); + result = result || MouseTargetFactory._hitTestViewCursor(ctx, request); + result = result || MouseTargetFactory._hitTestTextArea(ctx, request); + result = result || MouseTargetFactory._hitTestViewLines(ctx, request, domHitTestExecuted); + result = result || MouseTargetFactory._hitTestScrollbar(ctx, request); + + return (result || request.fulfill(MouseTargetType.UNKNOWN)); + } + + private static _hitTestContentWidget(ctx: HitTestContext, request: HitTestRequest): MouseTarget { + // Is it a content widget? + if (ElementPath.isChildOfContentWidgets(request.targetPath) || ElementPath.isChildOfOverflowingContentWidgets(request.targetPath)) { + let widgetId = ctx.findAttribute(request.target, 'widgetId'); + if (widgetId) { + return request.fulfill(MouseTargetType.CONTENT_WIDGET, null, null, widgetId); + } else { + return request.fulfill(MouseTargetType.UNKNOWN); + } + } + return null; + } + + private static _hitTestOverlayWidget(ctx: HitTestContext, request: HitTestRequest): MouseTarget { + // Is it an overlay widget? + if (ElementPath.isChildOfOverlayWidgets(request.targetPath)) { + let widgetId = ctx.findAttribute(request.target, 'widgetId'); + if (widgetId) { + return request.fulfill(MouseTargetType.OVERLAY_WIDGET, null, null, widgetId); + } else { + return request.fulfill(MouseTargetType.UNKNOWN); + } + } + return null; + } + + private static _hitTestViewCursor(ctx: HitTestContext, request: HitTestRequest): MouseTarget { + + if (request.isInContentArea) { + // Edge has a bug when hit-testing the exact position of a cursor, + // instead of returning the correct dom node, it returns the + // first or last rendered view line dom node, therefore help it out + // and first check if we are on top of a cursor + + const lastViewCursorsRenderData = ctx.lastViewCursorsRenderData; + const mouseContentHorizontalOffset = request.mouseContentHorizontalOffset; + const mouseVerticalOffset = request.mouseVerticalOffset; + + for (let i = 0, len = lastViewCursorsRenderData.length; i < len; i++) { + const d = lastViewCursorsRenderData[i]; + + if ( + d.contentLeft <= mouseContentHorizontalOffset + && mouseContentHorizontalOffset <= d.contentLeft + d.width + && d.contentTop <= mouseVerticalOffset + && mouseVerticalOffset <= d.contentTop + d.height + ) { + return request.fulfill(MouseTargetType.CONTENT_TEXT, d.position); + } + } + } + + // Is it a cursor ? + if (request.target.getAttribute) { + // Target is an Element + const lineNumberAttribute = request.target.getAttribute('lineNumber'); + if (lineNumberAttribute) { + const columnAttribute = request.target.getAttribute('column'); + if (columnAttribute) { + const position = new Position(parseInt(lineNumberAttribute, 10), parseInt(columnAttribute, 10)); + return request.fulfill(MouseTargetType.CONTENT_TEXT, position); + } + } + } + + return null; + } + + private static _hitTestViewZone(ctx: HitTestContext, request: HitTestRequest): MouseTarget { + let viewZoneData = ctx.getZoneAtCoord(request.mouseVerticalOffset); + if (viewZoneData) { + let mouseTargetType = (request.isInContentArea ? MouseTargetType.CONTENT_VIEW_ZONE : MouseTargetType.GUTTER_VIEW_ZONE); + return request.fulfill(mouseTargetType, viewZoneData.position, null, viewZoneData); + } + + return null; + } + + private static _hitTestTextArea(ctx: HitTestContext, request: HitTestRequest): MouseTarget { + // Is it the textarea? + if (ElementPath.isTextArea(request.targetPath)) { + return request.fulfill(MouseTargetType.TEXTAREA); + } + return null; + } + + private static _hitTestMargin(ctx: HitTestContext, request: HitTestRequest): MouseTarget { + if (request.isInMarginArea) { + let res = ctx.getFullLineRangeAtCoord(request.mouseVerticalOffset); + let pos = res.range.getStartPosition(); + + let offset = Math.abs(request.pos.x - request.editorPos.x); + if (offset <= ctx.layoutInfo.glyphMarginWidth) { + // On the glyph margin + return request.fulfill(MouseTargetType.GUTTER_GLYPH_MARGIN, pos, res.range, res.isAfterLines); + } + offset -= ctx.layoutInfo.glyphMarginWidth; + + if (offset <= ctx.layoutInfo.lineNumbersWidth) { + // On the line numbers + return request.fulfill(MouseTargetType.GUTTER_LINE_NUMBERS, pos, res.range, res.isAfterLines); + } + offset -= ctx.layoutInfo.lineNumbersWidth; + + // On the line decorations + return request.fulfill(MouseTargetType.GUTTER_LINE_DECORATIONS, pos, res.range, res.isAfterLines); + } + return null; + } + + private static _hitTestViewLines(ctx: HitTestContext, request: HitTestRequest, domHitTestExecuted: boolean): MouseTarget { + if (!ElementPath.isChildOfViewLines(request.targetPath)) { + return null; + } + + // Check if it is below any lines and any view zones + if (ctx.isAfterLines(request.mouseVerticalOffset)) { + // This most likely indicates it happened after the last view-line + const lineCount = ctx.model.getLineCount(); + const maxLineColumn = ctx.model.getLineMaxColumn(lineCount); + return request.fulfill(MouseTargetType.CONTENT_EMPTY, new Position(lineCount, maxLineColumn)); + } + + if (domHitTestExecuted) { + // We have already executed hit test... + return request.fulfill(MouseTargetType.UNKNOWN); + } + + const hitTestResult = MouseTargetFactory._doHitTest(ctx, request); + + if (hitTestResult.position) { + return MouseTargetFactory.createMouseTargetFromHitTestPosition(ctx, request, hitTestResult.position.lineNumber, hitTestResult.position.column); + } + + return this._createMouseTarget(ctx, request.withTarget(hitTestResult.hitTarget), true); + } + + private static _hitTestScrollbar(ctx: HitTestContext, request: HitTestRequest): MouseTarget { + // Is it the overview ruler? + // Is it a child of the scrollable element? + if (ElementPath.isChildOfScrollableElement(request.targetPath)) { + const possibleLineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset); + const maxColumn = ctx.model.getLineMaxColumn(possibleLineNumber); + return request.fulfill(MouseTargetType.SCROLLBAR, new Position(possibleLineNumber, maxColumn)); + } + + return null; + } + + public getMouseColumn(editorPos: EditorPagePosition, pos: PageCoordinates): number { + let layoutInfo = this._context.configuration.editor.layoutInfo; + let mouseContentHorizontalOffset = this._viewHelper.getScrollLeft() + pos.x - editorPos.x - layoutInfo.contentLeft; + return MouseTargetFactory._getMouseColumn(mouseContentHorizontalOffset, this._context.configuration.editor.fontInfo.typicalHalfwidthCharacterWidth); + } + + public static _getMouseColumn(mouseContentHorizontalOffset: number, typicalHalfwidthCharacterWidth: number): number { if (mouseContentHorizontalOffset < 0) { return 1; } - let charWidth = this._context.configuration.editor.fontInfo.typicalHalfwidthCharacterWidth; - let chars = Math.round(mouseContentHorizontalOffset / charWidth); + let chars = Math.round(mouseContentHorizontalOffset / typicalHalfwidthCharacterWidth); return (chars + 1); } - private createMouseTargetFromViewCursor(target: Element, lineNumber: number, column: number, mouseColumn: number): MouseTarget { - return new MouseTarget(target, MouseTargetType.CONTENT_TEXT, mouseColumn, new Position(lineNumber, column)); - } - - private createMouseTargetFromViewLines(target: Element, mouseVerticalOffset: number, mouseColumn: number): MouseTarget { - // This most likely indicates it happened after the last view-line - let lineCount = this._context.model.getLineCount(); - let maxLineColumn = this._context.model.getLineMaxColumn(lineCount); - return new MouseTarget(target, MouseTargetType.CONTENT_EMPTY, mouseColumn, new Position(lineCount, maxLineColumn)); - } - - private createMouseTargetFromHitTestPosition(target: Element, lineNumber: number, column: number, mouseHorizontalOffset: number, mouseColumn: number): MouseTarget { + private static createMouseTargetFromHitTestPosition(ctx: HitTestContext, request: HitTestRequest, lineNumber: number, column: number): MouseTarget { let pos = new Position(lineNumber, column); - let lineWidth = this._viewHelper.getLineWidth(lineNumber); + let lineWidth = ctx.getLineWidth(lineNumber); - if (mouseHorizontalOffset > lineWidth) { + if (request.mouseContentHorizontalOffset > lineWidth) { if (browser.isEdge && pos.column === 1) { // See https://github.com/Microsoft/vscode/issues/10875 - return new MouseTarget(target, MouseTargetType.CONTENT_EMPTY, mouseColumn, new Position(lineNumber, this._context.model.getLineMaxColumn(lineNumber))); + return request.fulfill(MouseTargetType.CONTENT_EMPTY, new Position(lineNumber, ctx.model.getLineMaxColumn(lineNumber))); } - return new MouseTarget(target, MouseTargetType.CONTENT_EMPTY, mouseColumn, pos); + return request.fulfill(MouseTargetType.CONTENT_EMPTY, pos); } - let visibleRange = this._viewHelper.visibleRangeForPosition2(lineNumber, column); + let visibleRange = ctx.visibleRangeForPosition2(lineNumber, column); if (!visibleRange) { - return new MouseTarget(target, MouseTargetType.UNKNOWN, mouseColumn, pos); + return request.fulfill(MouseTargetType.UNKNOWN, pos); } let columnHorizontalOffset = visibleRange.left; - if (mouseHorizontalOffset === columnHorizontalOffset) { - return new MouseTarget(target, MouseTargetType.CONTENT_TEXT, mouseColumn, pos); + if (request.mouseContentHorizontalOffset === columnHorizontalOffset) { + return request.fulfill(MouseTargetType.CONTENT_TEXT, pos); } let mouseIsBetween: boolean; if (column > 1) { let prevColumnHorizontalOffset = visibleRange.left; mouseIsBetween = false; - mouseIsBetween = mouseIsBetween || (prevColumnHorizontalOffset < mouseHorizontalOffset && mouseHorizontalOffset < columnHorizontalOffset); // LTR case - mouseIsBetween = mouseIsBetween || (columnHorizontalOffset < mouseHorizontalOffset && mouseHorizontalOffset < prevColumnHorizontalOffset); // RTL case + mouseIsBetween = mouseIsBetween || (prevColumnHorizontalOffset < request.mouseContentHorizontalOffset && request.mouseContentHorizontalOffset < columnHorizontalOffset); // LTR case + mouseIsBetween = mouseIsBetween || (columnHorizontalOffset < request.mouseContentHorizontalOffset && request.mouseContentHorizontalOffset < prevColumnHorizontalOffset); // RTL case if (mouseIsBetween) { let rng = new EditorRange(lineNumber, column, lineNumber, column - 1); - return new MouseTarget(target, MouseTargetType.CONTENT_TEXT, mouseColumn, pos, rng); + return request.fulfill(MouseTargetType.CONTENT_TEXT, pos, rng); } } - let lineMaxColumn = this._context.model.getLineMaxColumn(lineNumber); + let lineMaxColumn = ctx.model.getLineMaxColumn(lineNumber); if (column < lineMaxColumn) { - let nextColumnVisibleRange = this._viewHelper.visibleRangeForPosition2(lineNumber, column + 1); + let nextColumnVisibleRange = ctx.visibleRangeForPosition2(lineNumber, column + 1); if (nextColumnVisibleRange) { let nextColumnHorizontalOffset = nextColumnVisibleRange.left; mouseIsBetween = false; - mouseIsBetween = mouseIsBetween || (columnHorizontalOffset < mouseHorizontalOffset && mouseHorizontalOffset < nextColumnHorizontalOffset); // LTR case - mouseIsBetween = mouseIsBetween || (nextColumnHorizontalOffset < mouseHorizontalOffset && mouseHorizontalOffset < columnHorizontalOffset); // RTL case + mouseIsBetween = mouseIsBetween || (columnHorizontalOffset < request.mouseContentHorizontalOffset && request.mouseContentHorizontalOffset < nextColumnHorizontalOffset); // LTR case + mouseIsBetween = mouseIsBetween || (nextColumnHorizontalOffset < request.mouseContentHorizontalOffset && request.mouseContentHorizontalOffset < columnHorizontalOffset); // RTL case if (mouseIsBetween) { let rng = new EditorRange(lineNumber, column, lineNumber, column + 1); - return new MouseTarget(target, MouseTargetType.CONTENT_TEXT, mouseColumn, pos, rng); + return request.fulfill(MouseTargetType.CONTENT_TEXT, pos, rng); } } } - return new MouseTarget(target, MouseTargetType.CONTENT_TEXT, mouseColumn, pos); + return request.fulfill(MouseTargetType.CONTENT_TEXT, pos); } - private createMouseTargetFromContentWidgetsChild(target: Element, mouseColumn: number): MouseTarget { - let widgetId = this._findAttribute(target, 'widgetId', this._viewHelper.viewDomNode); + /** + * Most probably WebKit browsers and Edge + */ + private static _doHitTestWithCaretRangeFromPoint(ctx: HitTestContext, request: BareHitTestRequest): IHitTestResult { - if (widgetId) { - return new MouseTarget(target, MouseTargetType.CONTENT_WIDGET, mouseColumn, null, null, widgetId); + // In Chrome, especially on Linux it is possible to click between lines, + // so try to adjust the `hity` below so that it lands in the center of a line + let lineNumber = ctx.getLineNumberAtVerticalOffset(request.mouseVerticalOffset); + let lineVerticalOffset = ctx.getVerticalOffsetForLineNumber(lineNumber); + let lineCenteredVerticalOffset = lineVerticalOffset + Math.floor(ctx.lineHeight / 2); + let adjustedPageY = request.pos.y + (lineCenteredVerticalOffset - request.mouseVerticalOffset); + + if (adjustedPageY <= request.editorPos.y) { + adjustedPageY = request.editorPos.y + 1; + } + if (adjustedPageY >= request.editorPos.y + ctx.layoutInfo.height) { + adjustedPageY = request.editorPos.y + ctx.layoutInfo.height - 1; + } + + let adjustedPage = new PageCoordinates(request.pos.x, adjustedPageY); + + let r = this._actualDoHitTestWithCaretRangeFromPoint(ctx, adjustedPage.toClientCoordinates()); + if (r.position) { + return r; + } + + // Also try to hit test without the adjustment (for the edge cases that we are near the top or bottom) + return this._actualDoHitTestWithCaretRangeFromPoint(ctx, request.pos.toClientCoordinates()); + } + + private static _actualDoHitTestWithCaretRangeFromPoint(ctx: HitTestContext, coords: ClientCoordinates): IHitTestResult { + + let range: Range = document.caretRangeFromPoint(coords.clientX, coords.clientY); + + if (!range || !range.startContainer) { + return { + position: null, + hitTarget: null + }; + } + + // Chrome always hits a TEXT_NODE, while Edge sometimes hits a token span + let startContainer = range.startContainer; + let hitTarget: HTMLElement; + + if (startContainer.nodeType === startContainer.TEXT_NODE) { + // startContainer is expected to be the token text + let parent1 = startContainer.parentNode; // expected to be the token span + let parent2 = parent1 ? parent1.parentNode : null; // expected to be the view line container span + let parent3 = parent2 ? parent2.parentNode : null; // expected to be the view line div + let parent3ClassName = parent3 && parent3.nodeType === parent3.ELEMENT_NODE ? (parent3).className : null; + + if (parent3ClassName === ClassNames.VIEW_LINE) { + let p = ctx.getPositionFromDOMInfo(parent1, range.startOffset); + return { + position: p, + hitTarget: null + }; + } else { + hitTarget = startContainer.parentNode; + } + } else if (startContainer.nodeType === startContainer.ELEMENT_NODE) { + // startContainer is expected to be the token span + let parent1 = startContainer.parentNode; // expected to be the view line container span + let parent2 = parent1 ? parent1.parentNode : null; // expected to be the view line div + let parent2ClassName = parent2 && parent2.nodeType === parent2.ELEMENT_NODE ? (parent2).className : null; + + if (parent2ClassName === ClassNames.VIEW_LINE) { + let p = ctx.getPositionFromDOMInfo(startContainer, (startContainer).textContent.length); + return { + position: p, + hitTarget: null + }; + } else { + hitTarget = startContainer; + } + } + + return { + position: null, + hitTarget: hitTarget + }; + } + + /** + * Most probably Gecko + */ + private static _doHitTestWithCaretPositionFromPoint(ctx: HitTestContext, coords: ClientCoordinates): IHitTestResult { + let hitResult: { offsetNode: Node; offset: number; } = (document).caretPositionFromPoint(coords.clientX, coords.clientY); + + if (hitResult.offsetNode.nodeType === hitResult.offsetNode.TEXT_NODE) { + // offsetNode is expected to be the token text + let parent1 = hitResult.offsetNode.parentNode; // expected to be the token span + let parent2 = parent1 ? parent1.parentNode : null; // expected to be the view line container span + let parent3 = parent2 ? parent2.parentNode : null; // expected to be the view line div + let parent3ClassName = parent3 && parent3.nodeType === parent3.ELEMENT_NODE ? (parent3).className : null; + + if (parent3ClassName === ClassNames.VIEW_LINE) { + let p = ctx.getPositionFromDOMInfo(hitResult.offsetNode.parentNode, hitResult.offset); + return { + position: p, + hitTarget: null + }; + } else { + return { + position: null, + hitTarget: hitResult.offsetNode.parentNode + }; + } + } + + return { + position: null, + hitTarget: hitResult.offsetNode + }; + } + + /** + * Most probably IE + */ + private static _doHitTestWithMoveToPoint(ctx: HitTestContext, coords: ClientCoordinates): IHitTestResult { + let resultPosition: Position = null; + let resultHitTarget: Element = null; + + let textRange: IETextRange = (document.body).createTextRange(); + try { + textRange.moveToPoint(coords.clientX, coords.clientY); + } catch (err) { + return { + position: null, + hitTarget: null + }; + } + + textRange.collapse(true); + + // Now, let's do our best to figure out what we hit :) + let parentElement = textRange ? textRange.parentElement() : null; + let parent1 = parentElement ? parentElement.parentNode : null; + let parent2 = parent1 ? parent1.parentNode : null; + + let parent2ClassName = parent2 && parent2.nodeType === parent2.ELEMENT_NODE ? (parent2).className : ''; + + if (parent2ClassName === ClassNames.VIEW_LINE) { + let rangeToContainEntireSpan = textRange.duplicate(); + rangeToContainEntireSpan.moveToElementText(parentElement); + rangeToContainEntireSpan.setEndPoint('EndToStart', textRange); + + resultPosition = ctx.getPositionFromDOMInfo(parentElement, rangeToContainEntireSpan.text.length); + // Move range out of the span node, IE doesn't like having many ranges in + // the same spot and will act badly for lines containing dashes ('-') + rangeToContainEntireSpan.moveToElementText(ctx.viewDomNode); } else { - return new MouseTarget(target, MouseTargetType.UNKNOWN); + // Looks like we've hit the hover or something foreign + resultHitTarget = parentElement; } + + // Move range out of the span node, IE doesn't like having many ranges in + // the same spot and will act badly for lines containing dashes ('-') + textRange.moveToElementText(ctx.viewDomNode); + + return { + position: resultPosition, + hitTarget: resultHitTarget + }; } - private createMouseTargetFromOverlayWidgetsChild(target: Element, mouseColumn: number): MouseTarget { - let widgetId = this._findAttribute(target, 'widgetId', this._viewHelper.viewDomNode); + private static _doHitTest(ctx: HitTestContext, request: BareHitTestRequest): IHitTestResult { + // State of the art (18.10.2012): + // The spec says browsers should support document.caretPositionFromPoint, but nobody implemented it (http://dev.w3.org/csswg/cssom-view/) + // Gecko: + // - they tried to implement it once, but failed: https://bugzilla.mozilla.org/show_bug.cgi?id=654352 + // - however, they do give out rangeParent/rangeOffset properties on mouse events + // Webkit: + // - they have implemented a previous version of the spec which was using document.caretRangeFromPoint + // IE: + // - they have a proprietary method on ranges, moveToPoint: https://msdn.microsoft.com/en-us/library/ie/ms536632(v=vs.85).aspx - if (widgetId) { - return new MouseTarget(target, MouseTargetType.OVERLAY_WIDGET, mouseColumn, null, null, widgetId); - } else { - return new MouseTarget(target, MouseTargetType.UNKNOWN); - } - } + // 24.08.2016: Edge has added WebKit's document.caretRangeFromPoint, but it is quite buggy + // - when hit testing the cursor it returns the first or the last line in the viewport + // - it inconsistently hits text nodes or span nodes, while WebKit only hits text nodes + // - when toggling render whitespace on, and hit testing in the empty content after a line, it always hits offset 0 of the first span of the line + + // Thank you browsers for making this so 'easy' :) + + if (document.caretRangeFromPoint) { + + return this._doHitTestWithCaretRangeFromPoint(ctx, request); + + } else if ((document).caretPositionFromPoint) { + + return this._doHitTestWithCaretPositionFromPoint(ctx, request.pos.toClientCoordinates()); + + } else if ((document.body).createTextRange) { + + return this._doHitTestWithMoveToPoint(ctx, request.pos.toClientCoordinates()); - private createMouseTargetFromLinesDecorationsChild(target: Element, mouseVerticalOffset: number, mouseColumn: number): MouseTarget { - let viewZoneData = this._getZoneAtCoord(mouseVerticalOffset); - if (viewZoneData) { - return new MouseTarget(target, MouseTargetType.GUTTER_VIEW_ZONE, mouseColumn, viewZoneData.position, null, viewZoneData); } - let res = this._getFullLineRangeAtCoord(mouseVerticalOffset); - return new MouseTarget(target, MouseTargetType.GUTTER_LINE_DECORATIONS, mouseColumn, new Position(res.range.startLineNumber, res.range.startColumn), res.range, res.isAfterLines); - } - - private createMouseTargetFromLineNumbers(target: Element, mouseVerticalOffset: number, mouseColumn: number): MouseTarget { - let viewZoneData = this._getZoneAtCoord(mouseVerticalOffset); - if (viewZoneData) { - return new MouseTarget(target, MouseTargetType.GUTTER_VIEW_ZONE, mouseColumn, viewZoneData.position, null, viewZoneData); - } - - let res = this._getFullLineRangeAtCoord(mouseVerticalOffset); - return new MouseTarget(target, MouseTargetType.GUTTER_LINE_NUMBERS, mouseColumn, new Position(res.range.startLineNumber, res.range.startColumn), res.range, res.isAfterLines); - } - - private createMouseTargetFromGlyphMargin(target: Element, mouseVerticalOffset: number, mouseColumn: number): MouseTarget { - let viewZoneData = this._getZoneAtCoord(mouseVerticalOffset); - if (viewZoneData) { - return new MouseTarget(target, MouseTargetType.GUTTER_VIEW_ZONE, mouseColumn, viewZoneData.position, null, viewZoneData); - } - - let res = this._getFullLineRangeAtCoord(mouseVerticalOffset); - return new MouseTarget(target, MouseTargetType.GUTTER_GLYPH_MARGIN, mouseColumn, new Position(res.range.startLineNumber, res.range.startColumn), res.range, res.isAfterLines); - } - - private createMouseTargetFromScrollbar(target: Element, mouseVerticalOffset: number, mouseColumn: number): MouseTarget { - let possibleLineNumber = this._viewHelper.getLineNumberAtVerticalOffset(mouseVerticalOffset); - let maxColumn = this._context.model.getLineMaxColumn(possibleLineNumber); - return new MouseTarget(target, MouseTargetType.SCROLLBAR, mouseColumn, new Position(possibleLineNumber, maxColumn)); - } - - private createMouseTargetFromUnknownTarget(target: Element): MouseTarget { - let isInView = this._isChild(target, this._viewHelper.viewDomNode, this._viewHelper.viewDomNode); - let widgetId = null; - if (isInView) { - widgetId = this._findAttribute(target, 'widgetId', this._viewHelper.viewDomNode); - } - - if (widgetId) { - return new MouseTarget(target, MouseTargetType.OVERLAY_WIDGET, null, null, widgetId); - } else { - return new MouseTarget(target, MouseTargetType.UNKNOWN); - } + return { + position: null, + hitTarget: null + }; } } \ No newline at end of file diff --git a/src/vs/editor/browser/controller/pointerHandler.ts b/src/vs/editor/browser/controller/pointerHandler.ts index c3cf0812122..02bbeb3c3a1 100644 --- a/src/vs/editor/browser/controller/pointerHandler.ts +++ b/src/vs/editor/browser/controller/pointerHandler.ts @@ -7,9 +7,8 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import * as dom from 'vs/base/browser/dom'; import { EventType, Gesture, GestureEvent } from 'vs/base/browser/touch'; -import { IScrollEvent } from 'vs/editor/common/editorCommon'; import { MouseHandler, IPointerHandlerHelper } from 'vs/editor/browser/controller/mouseHandler'; -import { IViewController } from 'vs/editor/browser/editorBrowser'; +import { IViewController, IMouseTarget } from 'vs/editor/browser/editorBrowser'; import { ViewContext } from 'vs/editor/common/view/viewContext'; import { EditorMouseEvent } from 'vs/editor/browser/editorDom'; @@ -247,8 +246,8 @@ export class PointerHandler implements IDisposable { } } - public onScrollChanged(e: IScrollEvent): void { - this.handler.onScrollChanged(e); + public getTargetAtClientPoint(clientX: number, clientY: number): IMouseTarget { + return this.handler.getTargetAtClientPoint(clientX, clientY); } public dispose(): void { diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index 64478ca22a4..34096c5e735 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -44,6 +44,7 @@ export interface ICodeEditorHelper { getVerticalOffsetForPosition(lineNumber: number, column: number): number; delegateVerticalScrollbarMouseDown(browserEvent: MouseEvent): void; getOffsetForColumn(lineNumber: number, column: number): number; + getTargetAtClientPoint(clientX: number, clientY: number): IMouseTarget; } /** @@ -522,6 +523,14 @@ export interface ICodeEditor extends editorCommon.ICommonCodeEditor { */ getTopForPosition(lineNumber: number, column: number): number; + /** + * Get the hit test target at coordinates `clientX` and `clientY`. + * The coordinates are relative to the top-left of the viewport. + * + * @returns Hit test target or null if the coordinates fall outside the editor or the editor has no model. + */ + getTargetAtClientPoint(clientX: number, clientY: number): IMouseTarget; + /** * Get the visible position for `position`. * The result position takes scrolling into account and is relative to the top left corner of the editor. diff --git a/src/vs/editor/browser/editorDom.ts b/src/vs/editor/browser/editorDom.ts index 2a227addb95..78427527605 100644 --- a/src/vs/editor/browser/editorDom.ts +++ b/src/vs/editor/browser/editorDom.ts @@ -9,27 +9,88 @@ import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import * as dom from 'vs/base/browser/dom'; import { GlobalMouseMoveMonitor } from 'vs/base/browser/globalMouseMoveMonitor'; +/** + * Coordinates relative to the whole document (e.g. mouse event's pageX and pageY) + */ +export class PageCoordinates { + _pageCoordinatesBrand: void; + public readonly x: number; + public readonly y: number; + + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } + + public toClientCoordinates(): ClientCoordinates { + return new ClientCoordinates(this.x - dom.StandardWindow.scrollX, this.y - dom.StandardWindow.scrollY); + } +} + +/** + * Coordinates within the application's client area (i.e. origin is document's scroll position). + * + * For example, clicking in the top-left corner of the client area will + * always result in a mouse event with a client.x value of 0, regardless + * of whether the page is scrolled horizontally. + */ +export class ClientCoordinates { + _clientCoordinatesBrand: void; + + public readonly clientX: number; + public readonly clientY: number; + + constructor(clientX: number, clientY: number) { + this.clientX = clientX; + this.clientY = clientY; + } + + public toPageCoordinates(): PageCoordinates { + return new PageCoordinates(this.clientX + dom.StandardWindow.scrollX, this.clientY + dom.StandardWindow.scrollY); + } +} + +/** + * The position of the editor in the page. + */ +export class EditorPagePosition { + _editorPagePositionBrand: void; + + public readonly x: number; + public readonly y: number; + public readonly width: number; + public readonly height: number; + + constructor(x: number, y: number, width: number, height: number) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } +} + +export function createEditorPagePosition(editorViewDomNode: HTMLElement): EditorPagePosition { + let editorPos = dom.getDomNodePagePosition(editorViewDomNode); + return new EditorPagePosition(editorPos.left, editorPos.top, editorPos.width, editorPos.height); +} + export class EditorMouseEvent extends StandardMouseEvent { _editorMouseEventBrand: void; - editorPos: dom.IDomNodePagePosition; + /** + * Coordinates relative to the whole document. + */ + public readonly pos: PageCoordinates; /** - * The horizontal position of the cursor relative to the viewport (i.e. scrolled). + * Editor's coordinates relative to the whole document. */ - viewportx: number; - - /** - * The vertical position of the cursor relative to the viewport (i.e. scrolled). - */ - viewporty: number; + public readonly editorPos: EditorPagePosition; constructor(e: MouseEvent, editorViewDomNode: HTMLElement) { super(e); - this.editorPos = dom.getDomNodePagePosition(editorViewDomNode); - - this.viewportx = this.posx - dom.StandardWindow.scrollX; - this.viewporty = this.posy - dom.StandardWindow.scrollY; + this.pos = new PageCoordinates(this.posx, this.posy); + this.editorPos = createEditorPagePosition(editorViewDomNode); } } diff --git a/src/vs/editor/browser/standalone/standaloneLanguages.ts b/src/vs/editor/browser/standalone/standaloneLanguages.ts index 0a9810e6b05..14750f8b094 100644 --- a/src/vs/editor/browser/standalone/standaloneLanguages.ts +++ b/src/vs/editor/browser/standalone/standaloneLanguages.ts @@ -276,6 +276,13 @@ export function registerDefinitionProvider(languageId: string, provider: modes.D return modes.DefinitionProviderRegistry.register(languageId, provider); } +/** + * Register a type definition provider (used by e.g. go to implementation). + */ +export function registerTypeDefinitionProvider(languageId: string, provider: modes.TypeDefinitionProvider): IDisposable { + return modes.TypeDefinitionProviderRegistry.register(languageId, provider); +} + /** * Register a code lens provider (used by e.g. inline code lenses). */ @@ -633,6 +640,7 @@ export function createMonacoLanguagesAPI(): typeof monaco.languages { registerDocumentSymbolProvider: registerDocumentSymbolProvider, registerDocumentHighlightProvider: registerDocumentHighlightProvider, registerDefinitionProvider: registerDefinitionProvider, + registerTypeDefinitionProvider: registerTypeDefinitionProvider, registerCodeLensProvider: registerCodeLensProvider, registerCodeActionProvider: registerCodeActionProvider, registerDocumentFormattingEditProvider: registerDocumentFormattingEditProvider, diff --git a/src/vs/editor/browser/view/viewImpl.ts b/src/vs/editor/browser/view/viewImpl.ts index 87d6cdaaf4e..cd57262869d 100644 --- a/src/vs/editor/browser/view/viewImpl.ts +++ b/src/vs/editor/browser/view/viewImpl.ts @@ -197,7 +197,6 @@ export class View extends ViewEventHandler implements editorBrowser.IView, IDisp // (there have been reports of tiny blinking cursors) // (in WebKit the textarea is 1px by 1px because it cannot handle input to a 0x0 textarea) this.textAreaCover = document.createElement('div'); - PartFingerprints.write(this.textAreaCover, PartFingerprint.TextAreaCover); if (this._context.configuration.editor.viewInfo.glyphMargin) { this.textAreaCover.className = 'monaco-editor-background ' + editorBrowser.ClassNames.GLYPH_MARGIN + ' ' + editorBrowser.ClassNames.TEXTAREA_COVER; } else { @@ -609,7 +608,15 @@ export class View extends ViewEventHandler implements editorBrowser.IView, IDisp return -1; } return visibleRanges[0].left; + }, + + getTargetAtClientPoint: (clientX: number, clientY: number): editorBrowser.IMouseTarget => { + if (this._isDisposed) { + throw new Error('ViewImpl.codeEditorHelper.getTargetAtClientPoint: View is disposed'); + } + return this.pointerHandler.getTargetAtClientPoint(clientX, clientY); } + }; } return this.codeEditorHelper; @@ -666,7 +673,9 @@ export class View extends ViewEventHandler implements editorBrowser.IView, IDisp this.keyboardHandler.focusTextArea(); // IE does not trigger the focus event immediately, so we must help it a little bit - this._setHasFocus(true); + if (document.activeElement === this.textArea) { + this._setHasFocus(true); + } } public isFocused(): boolean { diff --git a/src/vs/editor/browser/view/viewPart.ts b/src/vs/editor/browser/view/viewPart.ts index d1b4cb83f43..8ca7825c45b 100644 --- a/src/vs/editor/browser/view/viewPart.ts +++ b/src/vs/editor/browser/view/viewPart.ts @@ -30,17 +30,12 @@ export abstract class ViewPart extends ViewEventHandler { export const enum PartFingerprint { None, ContentWidgets, - Margin, OverflowingContentWidgets, OverflowGuard, OverlayWidgets, - OverviewRuler, ScrollableElement, TextArea, - TextAreaCover, - ViewCursorsLayer, ViewLines, - ViewZones } export class PartFingerprints { diff --git a/src/vs/editor/browser/viewParts/margin/margin.ts b/src/vs/editor/browser/viewParts/margin/margin.ts index 8cdd197bd67..3ff433c51a0 100644 --- a/src/vs/editor/browser/viewParts/margin/margin.ts +++ b/src/vs/editor/browser/viewParts/margin/margin.ts @@ -8,7 +8,7 @@ import { StyleMutator, FastDomNode, createFastDomNode } from 'vs/base/browser/styleMutator'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { ClassNames } from 'vs/editor/browser/editorBrowser'; -import { ViewPart, PartFingerprint, PartFingerprints } from 'vs/editor/browser/view/viewPart'; +import { ViewPart } from 'vs/editor/browser/view/viewPart'; import { ViewContext } from 'vs/editor/common/view/viewContext'; import { IRenderingContext, IRestrictedRenderingContext } from 'vs/editor/common/view/renderingContext'; import { ILayoutProvider } from 'vs/editor/browser/viewLayout/layoutProvider'; @@ -39,7 +39,6 @@ export class Margin extends ViewPart { public _createDomNode(): HTMLElement { let domNode = document.createElement('div'); - PartFingerprints.write(domNode, PartFingerprint.Margin); domNode.className = ClassNames.MARGIN + ' monaco-editor-background'; domNode.style.position = 'absolute'; domNode.setAttribute('role', 'presentation'); diff --git a/src/vs/editor/browser/viewParts/overviewRuler/overviewRulerImpl.ts b/src/vs/editor/browser/viewParts/overviewRuler/overviewRulerImpl.ts index f9bd58f54a6..888e5be0164 100644 --- a/src/vs/editor/browser/viewParts/overviewRuler/overviewRulerImpl.ts +++ b/src/vs/editor/browser/viewParts/overviewRuler/overviewRulerImpl.ts @@ -9,7 +9,6 @@ import { OverviewRulerPosition, OverviewRulerLane, OverviewRulerZone, ColorZone import { IDisposable } from 'vs/base/common/lifecycle'; import * as browser from 'vs/base/browser/browser'; import { OverviewZoneManager } from 'vs/editor/common/view/overviewZoneManager'; -import { PartFingerprint, PartFingerprints } from 'vs/editor/browser/view/viewPart'; export class OverviewRulerImpl { @@ -25,7 +24,6 @@ export class OverviewRulerImpl { this._canvasLeftOffset = canvasLeftOffset; this._domNode = document.createElement('canvas'); - PartFingerprints.write(this._domNode, PartFingerprint.OverviewRuler); this._domNode.className = cssClassName; this._domNode.style.position = 'absolute'; diff --git a/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts b/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts index a1e001ed0a1..f1c7688436b 100644 --- a/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts +++ b/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts @@ -8,7 +8,7 @@ import 'vs/css!./viewCursors'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { ClassNames } from 'vs/editor/browser/editorBrowser'; -import { ViewPart, PartFingerprint, PartFingerprints } from 'vs/editor/browser/view/viewPart'; +import { ViewPart } from 'vs/editor/browser/view/viewPart'; import { Position } from 'vs/editor/common/core/position'; import { IViewCursorRenderData, ViewCursor } from 'vs/editor/browser/viewParts/viewCursors/viewCursor'; import { ViewContext } from 'vs/editor/common/view/viewContext'; @@ -49,7 +49,6 @@ export class ViewCursors extends ViewPart { this._renderData = []; this._domNode = createFastDomNode(document.createElement('div')); - PartFingerprints.write(this._domNode.domNode, PartFingerprint.ViewCursorsLayer); this._updateDomClassName(); this._domNode.domNode.appendChild(this._primaryCursor.getDomNode()); diff --git a/src/vs/editor/browser/viewParts/viewZones/viewZones.ts b/src/vs/editor/browser/viewParts/viewZones/viewZones.ts index 387e6e4c62a..f4f1619f2dc 100644 --- a/src/vs/editor/browser/viewParts/viewZones/viewZones.ts +++ b/src/vs/editor/browser/viewParts/viewZones/viewZones.ts @@ -8,7 +8,7 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { StyleMutator } from 'vs/base/browser/styleMutator'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { ClassNames, IViewZone } from 'vs/editor/browser/editorBrowser'; -import { ViewPart, PartFingerprint, PartFingerprints } from 'vs/editor/browser/view/viewPart'; +import { ViewPart } from 'vs/editor/browser/view/viewPart'; import { ViewContext } from 'vs/editor/common/view/viewContext'; import { Position } from 'vs/editor/common/core/position'; import { IRenderingContext, IRestrictedRenderingContext } from 'vs/editor/common/view/renderingContext'; @@ -49,7 +49,6 @@ export class ViewZones extends ViewPart { this._whitespaceManager = whitespaceManager; this.domNode = document.createElement('div'); - PartFingerprints.write(this.domNode, PartFingerprint.ViewZones); this.domNode.className = ClassNames.VIEW_ZONES; this.domNode.style.position = 'absolute'; this.domNode.setAttribute('role', 'presentation'); diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index ed472911443..e02011125a7 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -404,6 +404,13 @@ export abstract class CodeEditorWidget extends CommonCodeEditor implements edito return this._view.getCodeEditorHelper().getVerticalOffsetForPosition(lineNumber, column); } + public getTargetAtClientPoint(clientX: number, clientY: number): editorBrowser.IMouseTarget { + if (!this.hasView) { + return null; + } + return this._view.getCodeEditorHelper().getTargetAtClientPoint(clientX, clientY); + } + public getScrolledVisiblePosition(rawPosition: editorCommon.IPosition): { top: number; left: number; height: number; } { if (!this.hasView) { return null; diff --git a/src/vs/editor/browser/widget/media/tokens.css b/src/vs/editor/browser/widget/media/tokens.css index 590c1574452..20e1a6e40f4 100644 --- a/src/vs/editor/browser/widget/media/tokens.css +++ b/src/vs/editor/browser/widget/media/tokens.css @@ -5,6 +5,7 @@ .monaco-editor .vs-whitespace { display:inline-block; + font-weight: normal !important; } .monaco-editor.hc-black .view-line { mix-blend-mode: difference; diff --git a/src/vs/editor/common/controller/textAreaHandler.ts b/src/vs/editor/common/controller/textAreaHandler.ts index 1015da7d3fb..a385e1c96a8 100644 --- a/src/vs/editor/common/controller/textAreaHandler.ts +++ b/src/vs/editor/common/controller/textAreaHandler.ts @@ -118,7 +118,7 @@ export class TextAreaHandler extends Disposable { // In IE we cannot set .value when handling 'compositionstart' because the entire composition will get canceled. if (!this.Browser.isEdgeOrIE) { - this.setTextAreaState('compositionstart', this.textAreaState.toEmpty()); + this.setTextAreaState('compositionstart', this.textAreaState.toEmpty(), false); } this._onCompositionStart.fire({ @@ -200,13 +200,13 @@ export class TextAreaHandler extends Disposable { } else { if (this.textArea.getSelectionStart() !== this.textArea.getSelectionEnd()) { // Clean up the textarea, to get a clean paste - this.setTextAreaState('paste', this.textAreaState.toEmpty()); + this.setTextAreaState('paste', this.textAreaState.toEmpty(), false); } this._nextCommand = ReadFromTextArea.Paste; } })); - this._writePlaceholderAndSelectTextArea('ctor'); + this._writePlaceholderAndSelectTextArea('ctor', false); } public dispose(): void { @@ -227,24 +227,24 @@ export class TextAreaHandler extends Disposable { } this.hasFocus = isFocused; if (this.hasFocus) { - this._writePlaceholderAndSelectTextArea('focusgain'); + this._writePlaceholderAndSelectTextArea('focusgain', false); } } public setCursorSelections(primary: Range, secondary: Range[]): void { this.selection = primary; this.selections = [primary].concat(secondary); - this._writePlaceholderAndSelectTextArea('selection changed'); + this._writePlaceholderAndSelectTextArea('selection changed', false); } // --- end event handlers - private setTextAreaState(reason: string, textAreaState: TextAreaState): void { + private setTextAreaState(reason: string, textAreaState: TextAreaState, forceFocus: boolean): void { if (!this.hasFocus) { textAreaState = textAreaState.resetSelection(); } - textAreaState.applyToTextArea(reason, this.textArea, this.hasFocus); + textAreaState.applyToTextArea(reason, this.textArea, this.hasFocus || forceFocus); this.textAreaState = textAreaState; } @@ -281,18 +281,18 @@ export class TextAreaHandler extends Disposable { }); } - public writePlaceholderAndSelectTextAreaSync(): void { - this._writePlaceholderAndSelectTextArea('focusTextArea'); + public focusTextArea(): void { + this._writePlaceholderAndSelectTextArea('focusTextArea', true); } - private _writePlaceholderAndSelectTextArea(reason: string): void { + private _writePlaceholderAndSelectTextArea(reason: string, forceFocus: boolean): void { if (!this.textareaIsShownAtCursor) { // Do not write to the textarea if it is visible. if (this.Browser.isIPad) { // Do not place anything in the textarea for the iPad - this.setTextAreaState(reason, this.textAreaState.toEmpty()); + this.setTextAreaState(reason, this.textAreaState.toEmpty(), forceFocus); } else { - this.setTextAreaState(reason, this.textAreaState.fromEditorSelection(this.model, this.selection)); + this.setTextAreaState(reason, this.textAreaState.fromEditorSelection(this.model, this.selection), forceFocus); } } } @@ -304,7 +304,7 @@ export class TextAreaHandler extends Disposable { if (e.canUseTextData()) { e.setTextData(whatToCopy); } else { - this.setTextAreaState('copy or cut', this.textAreaState.fromText(whatToCopy)); + this.setTextAreaState('copy or cut', this.textAreaState.fromText(whatToCopy), false); } if (this.Browser.enableEmptySelectionClipboard) { diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index 1932b5795ac..5a95b9a7335 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/src/vs/editor/common/editorCommon.ts @@ -243,10 +243,9 @@ export interface IEditorOptions { */ roundedSelection?: boolean; /** - * Theme to be used for rendering. Consists of two parts, the UI theme and the syntax theme, - * separated by a space. - * The current available UI themes are: 'vs' (default), 'vs-dark', 'hc-black' - * The syntax themes are contributed. The default is 'default-theme' + * Theme to be used for rendering. + * The current out-of-the-box available themes are: 'vs' (default), 'vs-dark', 'hc-black'. + * You can create custom themes via `monaco.editor.defineTheme`. */ theme?: string; /** @@ -3064,6 +3063,10 @@ export namespace ModeContextKeys { * @internal */ export const hasDefinitionProvider = new RawContextKey('editorHasDefinitionProvider', undefined); + /** + * @internal + */ + export const hasTypeDefinitionProvider = new RawContextKey('editorHasTypeDefinitionProvider', undefined); /** * @internal */ diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 9e4f4bbf394..8bee5352016 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -418,6 +418,7 @@ export interface Location { * defined. */ export type Definition = Location | Location[]; + /** * The definition provider interface defines the contract between extensions and * the [go to definition](https://code.visualstudio.com/docs/editor/editingevolved#_go-to-definition) @@ -430,6 +431,16 @@ export interface DefinitionProvider { provideDefinition(model: editorCommon.IReadOnlyModel, position: Position, token: CancellationToken): Definition | Thenable; } +/** + * The type definition provider interface defines the contract between extensions and + * the go to implementation feature. + */ +export interface TypeDefinitionProvider { + /** + * Provide the implementation of the symbol at the given position and document. + */ + provideTypeDefinition(model: editorCommon.IReadOnlyModel, position: Position, token: CancellationToken): Definition | Thenable; +} /** * A symbol kind. @@ -738,6 +749,11 @@ export const DocumentHighlightProviderRegistry = new LanguageFeatureRegistry(); +/** + * @internal + */ +export const TypeDefinitionProviderRegistry = new LanguageFeatureRegistry(); + /** * @internal */ diff --git a/src/vs/editor/common/modes/editorModeContext.ts b/src/vs/editor/common/modes/editorModeContext.ts index 1bcac6ebf21..b406ca7c8d0 100644 --- a/src/vs/editor/common/modes/editorModeContext.ts +++ b/src/vs/editor/common/modes/editorModeContext.ts @@ -19,6 +19,7 @@ export class EditorModeContext { private _hasCodeActionsProvider: IContextKey; private _hasCodeLensProvider: IContextKey; private _hasDefinitionProvider: IContextKey; + private _hasTypeDefinitionProvider: IContextKey; private _hasHoverProvider: IContextKey; private _hasDocumentHighlightProvider: IContextKey; private _hasDocumentSymbolProvider: IContextKey; @@ -39,6 +40,7 @@ export class EditorModeContext { this._hasCodeActionsProvider = ModeContextKeys.hasCodeActionsProvider.bindTo(contextKeyService); this._hasCodeLensProvider = ModeContextKeys.hasCodeLensProvider.bindTo(contextKeyService); this._hasDefinitionProvider = ModeContextKeys.hasDefinitionProvider.bindTo(contextKeyService); + this._hasTypeDefinitionProvider = ModeContextKeys.hasTypeDefinitionProvider.bindTo(contextKeyService); this._hasHoverProvider = ModeContextKeys.hasHoverProvider.bindTo(contextKeyService); this._hasDocumentHighlightProvider = ModeContextKeys.hasDocumentHighlightProvider.bindTo(contextKeyService); this._hasDocumentSymbolProvider = ModeContextKeys.hasDocumentSymbolProvider.bindTo(contextKeyService); @@ -57,6 +59,7 @@ export class EditorModeContext { modes.CodeActionProviderRegistry.onDidChange(this._update, this, this._disposables); modes.CodeLensProviderRegistry.onDidChange(this._update, this, this._disposables); modes.DefinitionProviderRegistry.onDidChange(this._update, this, this._disposables); + modes.TypeDefinitionProviderRegistry.onDidChange(this._update, this, this._disposables); modes.HoverProviderRegistry.onDidChange(this._update, this, this._disposables); modes.DocumentHighlightProviderRegistry.onDidChange(this._update, this, this._disposables); modes.DocumentSymbolProviderRegistry.onDidChange(this._update, this, this._disposables); @@ -79,6 +82,7 @@ export class EditorModeContext { this._hasCodeActionsProvider.reset(); this._hasCodeLensProvider.reset(); this._hasDefinitionProvider.reset(); + this._hasTypeDefinitionProvider.reset(); this._hasHoverProvider.reset(); this._hasDocumentHighlightProvider.reset(); this._hasDocumentSymbolProvider.reset(); @@ -100,6 +104,7 @@ export class EditorModeContext { this._hasCodeActionsProvider.set(modes.CodeActionProviderRegistry.has(model)); this._hasCodeLensProvider.set(modes.CodeLensProviderRegistry.has(model)); this._hasDefinitionProvider.set(modes.DefinitionProviderRegistry.has(model)); + this._hasTypeDefinitionProvider.set(modes.TypeDefinitionProviderRegistry.has(model)); this._hasHoverProvider.set(modes.HoverProviderRegistry.has(model)); this._hasDocumentHighlightProvider.set(modes.DocumentHighlightProviderRegistry.has(model)); this._hasDocumentSymbolProvider.set(modes.DocumentSymbolProviderRegistry.has(model)); diff --git a/src/vs/editor/contrib/find/common/findModel.ts b/src/vs/editor/contrib/find/common/findModel.ts index 337a5a0fbb1..b99cd6865cf 100644 --- a/src/vs/editor/contrib/find/common/findModel.ts +++ b/src/vs/editor/contrib/find/common/findModel.ts @@ -17,7 +17,6 @@ import { ReplaceAllCommand } from './replaceAllCommand'; import { Selection } from 'vs/editor/common/core/selection'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { IKeybindings } from 'vs/platform/keybinding/common/keybinding'; -import { SearchParams } from 'vs/editor/common/model/textModelSearch'; export const ToggleCaseSensitiveKeybinding: IKeybindings = { primary: KeyMod.Alt | KeyCode.KEY_C, diff --git a/src/vs/editor/contrib/find/common/replacePattern.ts b/src/vs/editor/contrib/find/common/replacePattern.ts index 5d355a4e126..125e363a768 100644 --- a/src/vs/editor/contrib/find/common/replacePattern.ts +++ b/src/vs/editor/contrib/find/common/replacePattern.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as strings from 'vs/base/common/strings'; -import { IPatternInfo } from 'vs/platform/search/common/search'; import { CharCode } from 'vs/base/common/charCode'; export class ReplacePattern { @@ -40,7 +38,7 @@ export class ReplacePattern { } } - public buildReplaceString(matches:string[]): string { + public buildReplaceString(matches: string[]): string { if (this._staticValue) { return this._staticValue; } @@ -61,7 +59,7 @@ export class ReplacePattern { return result; } - private static _substitute(matchIndex: number, matches:string[]): string { + private static _substitute(matchIndex: number, matches: string[]): string { if (matchIndex === 0) { return matches[0]; } @@ -83,11 +81,11 @@ export class ReplacePattern { */ export class ReplacePiece { - public static staticValue(value:string): ReplacePiece { + public static staticValue(value: string): ReplacePiece { return new ReplacePiece(value, -1); } - public static matchIndex(index:number): ReplacePiece { + public static matchIndex(index: number): ReplacePiece { return new ReplacePiece(null, index); } diff --git a/src/vs/editor/contrib/find/test/common/replacePattern.test.ts b/src/vs/editor/contrib/find/test/common/replacePattern.test.ts index fb56e9aa5f3..88fcaeeb3aa 100644 --- a/src/vs/editor/contrib/find/test/common/replacePattern.test.ts +++ b/src/vs/editor/contrib/find/test/common/replacePattern.test.ts @@ -99,7 +99,7 @@ suite('Replace Pattern test', () => { }); test('get replace string if given text is a complete match', () => { - function assertReplace(target:string, search: RegExp, replaceString: string, expected: string): void { + function assertReplace(target: string, search: RegExp, replaceString: string, expected: string): void { let replacePattern = parseReplaceString(replaceString); let m = search.exec(target); let actual = replacePattern.buildReplaceString(m); @@ -125,7 +125,7 @@ suite('Replace Pattern test', () => { }); test('get replace string if match is sub-string of the text', () => { - function assertReplace(target:string, search: RegExp, replaceString: string, expected: string): void { + function assertReplace(target: string, search: RegExp, replaceString: string, expected: string): void { let replacePattern = parseReplaceString(replaceString); let m = search.exec(target); let actual = replacePattern.buildReplaceString(m); diff --git a/src/vs/editor/contrib/goToDeclaration/browser/goToDeclaration.ts b/src/vs/editor/contrib/goToDeclaration/browser/goToDeclaration.ts index 10e0304f3a2..3e3f8e96961 100644 --- a/src/vs/editor/contrib/goToDeclaration/browser/goToDeclaration.ts +++ b/src/vs/editor/contrib/goToDeclaration/browser/goToDeclaration.ts @@ -26,13 +26,14 @@ import { editorAction, IActionOptions, ServicesAccessor, EditorAction } from 'vs import { Location, DefinitionProviderRegistry } from 'vs/editor/common/modes'; import { ICodeEditor, IEditorMouseEvent, IMouseTarget } from 'vs/editor/browser/editorBrowser'; import { editorContribution } from 'vs/editor/browser/editorBrowserExtensions'; -import { getDeclarationsAtPosition } from 'vs/editor/contrib/goToDeclaration/common/goToDeclaration'; +import { getDeclarationsAtPosition, getTypeDefinitionAtPosition } from 'vs/editor/contrib/goToDeclaration/common/goToDeclaration'; import { ReferencesController } from 'vs/editor/contrib/referenceSearch/browser/referencesController'; import { ReferencesModel } from 'vs/editor/contrib/referenceSearch/browser/referencesModel'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { PeekContext } from 'vs/editor/contrib/zoneWidget/browser/peekViewWidget'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ITextModelResolverService } from 'vs/editor/common/services/resolverService'; +import * as corePosition from 'vs/editor/common/core/position'; import ModeContextKeys = editorCommon.ModeContextKeys; import EditorContextKeys = editorCommon.EditorContextKeys; @@ -64,7 +65,7 @@ export class DefinitionAction extends EditorAction { let model = editor.getModel(); let pos = editor.getPosition(); - return getDeclarationsAtPosition(model, pos).then(references => { + return this.getDeclarationsAtPosition(model, pos).then(references => { if (!references) { return; @@ -104,6 +105,10 @@ export class DefinitionAction extends EditorAction { }); } + protected getDeclarationsAtPosition(model: editorCommon.IModel, position: corePosition.Position): TPromise { + return getDeclarationsAtPosition(model, position); + } + private _onResult(editorService: IEditorService, editor: editorCommon.ICommonCodeEditor, model: ReferencesModel) { if (this._configuration.openInPeek) { this._openInPeek(editorService, editor, model); @@ -217,6 +222,61 @@ export class PeekDefinitionAction extends DefinitionAction { } } + +@editorAction +export class GoToImplementationAction extends DefinitionAction { + + public static ID = 'editor.action.goToImplementation'; + + constructor() { + super(new DefinitionActionConfig(), { + id: GoToImplementationAction.ID, + label: nls.localize('actions.goToImplementation.label', "Go to Implementation"), + alias: 'Go to Implementation', + precondition: ModeContextKeys.hasTypeDefinitionProvider, + kbOpts: { + kbExpr: EditorContextKeys.TextFocus, + primary: KeyMod.CtrlCmd | KeyCode.F12 + }, + menuOpts: { + group: 'navigation', + order: 1.3 + } + }); + } + + protected getDeclarationsAtPosition(model: editorCommon.IModel, position: corePosition.Position): TPromise { + return getTypeDefinitionAtPosition(model, position); + } +} + +@editorAction +export class PeekImplementationAction extends DefinitionAction { + + public static ID = 'editor.action.peekImplementation'; + + constructor() { + super(new DefinitionActionConfig(false, true, false), { + id: PeekImplementationAction.ID, + label: nls.localize('actions.peekImplementation.label', "Peek Implementation"), + alias: 'Peek Implementation', + precondition: ModeContextKeys.hasTypeDefinitionProvider, + kbOpts: { + kbExpr: EditorContextKeys.TextFocus, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.F12 + }, + menuOpts: { + group: 'navigation', + order: 1.3 + } + }); + } + + protected getDeclarationsAtPosition(model: editorCommon.IModel, position: corePosition.Position): TPromise { + return getTypeDefinitionAtPosition(model, position); + } +} + // --- Editor Contribution to goto definition using the mouse and a modifier key @editorContribution diff --git a/src/vs/editor/contrib/goToDeclaration/common/goToDeclaration.ts b/src/vs/editor/contrib/goToDeclaration/common/goToDeclaration.ts index 0d076deae36..11037d438da 100644 --- a/src/vs/editor/contrib/goToDeclaration/common/goToDeclaration.ts +++ b/src/vs/editor/contrib/goToDeclaration/common/goToDeclaration.ts @@ -9,10 +9,24 @@ import { onUnexpectedExternalError } from 'vs/base/common/errors'; import { TPromise } from 'vs/base/common/winjs.base'; import { IReadOnlyModel } from 'vs/editor/common/editorCommon'; import { CommonEditorRegistry } from 'vs/editor/common/editorCommonExtensions'; -import { DefinitionProviderRegistry, Location } from 'vs/editor/common/modes'; +import { DefinitionProviderRegistry, TypeDefinitionProviderRegistry, Location } from 'vs/editor/common/modes'; import { asWinJsPromise } from 'vs/base/common/async'; import { Position } from 'vs/editor/common/core/position'; +function outputResults(promises: TPromise[]) { + return TPromise.join(promises).then(allReferences => { + let result: Location[] = []; + for (let references of allReferences) { + if (Array.isArray(references)) { + result.push(...references); + } else if (references) { + result.push(references); + } + } + return result; + }); +} + export function getDeclarationsAtPosition(model: IReadOnlyModel, position: Position): TPromise { const provider = DefinitionProviderRegistry.ordered(model); @@ -27,18 +41,25 @@ export function getDeclarationsAtPosition(model: IReadOnlyModel, position: Posit onUnexpectedExternalError(err); }); }); - - return TPromise.join(promises).then(allReferences => { - let result: Location[] = []; - for (let references of allReferences) { - if (Array.isArray(references)) { - result.push(...references); - } else if (references) { - result.push(references); - } - } - return result; - }); + return outputResults(promises); } -CommonEditorRegistry.registerDefaultLanguageCommand('_executeDefinitionProvider', getDeclarationsAtPosition); \ No newline at end of file +export function getTypeDefinitionAtPosition(model: IReadOnlyModel, position: Position): TPromise { + + const provider = TypeDefinitionProviderRegistry.ordered(model); + + // get results + const promises = provider.map((provider, idx) => { + return asWinJsPromise((token) => { + return provider.provideTypeDefinition(model, position, token); + }).then(result => { + return result; + }, err => { + onUnexpectedExternalError(err); + }); + }); + return outputResults(promises); +} + +CommonEditorRegistry.registerDefaultLanguageCommand('_executeDefinitionProvider', getDeclarationsAtPosition); +CommonEditorRegistry.registerDefaultLanguageCommand('_executeTypeDefinitionProvider', getTypeDefinitionAtPosition); \ No newline at end of file diff --git a/src/vs/editor/contrib/suggest/browser/suggestController.ts b/src/vs/editor/contrib/suggest/browser/suggestController.ts index 1b9332f32aa..5cbf0f0c32a 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestController.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestController.ts @@ -105,9 +105,14 @@ export class SuggestController implements IEditorContribution { // Wire up logic to accept a suggestion on certain characters const autoAcceptOracle = new AcceptOnCharacterOracle(editor, this.widget, item => this.onDidSelectItem(item)); this.toDispose.push( + autoAcceptOracle, this.model.onDidCancel(autoAcceptOracle.reset, autoAcceptOracle), this.model.onDidTrigger(autoAcceptOracle.reset, autoAcceptOracle), - autoAcceptOracle + this.model.onDidSuggest(e => { + if (e.completionModel.items.length === 0) { + autoAcceptOracle.reset(); + } + }) ); } diff --git a/src/vs/editor/test/browser/controller/imeTester.ts b/src/vs/editor/test/browser/controller/imeTester.ts index 906f40bf5d4..2abbab025a6 100644 --- a/src/vs/editor/test/browser/controller/imeTester.ts +++ b/src/vs/editor/test/browser/controller/imeTester.ts @@ -124,7 +124,7 @@ function doCreateTest(strategy: TextAreaStrategy, description: string, inputStr: cursorOffset = off; cursorLength = len; handler.setCursorSelections(new Range(1, 1 + cursorOffset, 1, 1 + cursorOffset + cursorLength), []); - handler.writePlaceholderAndSelectTextAreaSync(); + handler.focusTextArea(); }; let updateModelAndPosition = (text: string, off: number, len: number) => { diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 7f0769ad789..3dad2cce583 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -1154,10 +1154,9 @@ declare module monaco.editor { */ roundedSelection?: boolean; /** - * Theme to be used for rendering. Consists of two parts, the UI theme and the syntax theme, - * separated by a space. - * The current available UI themes are: 'vs' (default), 'vs-dark', 'hc-black' - * The syntax themes are contributed. The default is 'default-theme' + * Theme to be used for rendering. + * The current out-of-the-box available themes are: 'vs' (default), 'vs-dark', 'hc-black'. + * You can create custom themes via `monaco.editor.defineTheme`. */ theme?: string; /** @@ -3803,6 +3802,13 @@ declare module monaco.editor { * Get the vertical position (top offset) for the position w.r.t. to the first line. */ getTopForPosition(lineNumber: number, column: number): number; + /** + * Get the hit test target at coordinates `clientX` and `clientY`. + * The coordinates are relative to the top-left of the viewport. + * + * @returns Hit test target or null if the coordinates fall outside the editor or the editor has no model. + */ + getTargetAtClientPoint(clientX: number, clientY: number): IMouseTarget; /** * Get the visible position for `position`. * The result position takes scrolling into account and is relative to the top left corner of the editor. @@ -3953,6 +3959,11 @@ declare module monaco.languages { */ export function registerDefinitionProvider(languageId: string, provider: DefinitionProvider): IDisposable; + /** + * Register a type definition provider (used by e.g. go to implementation). + */ + export function registerTypeDefinitionProvider(languageId: string, provider: TypeDefinitionProvider): IDisposable; + /** * Register a code lens provider (used by e.g. inline code lenses). */ @@ -4532,6 +4543,17 @@ declare module monaco.languages { provideDefinition(model: editor.IReadOnlyModel, position: Position, token: CancellationToken): Definition | Thenable; } + /** + * The type definition provider interface defines the contract between extensions and + * the go to implementation feature. + */ + export interface TypeDefinitionProvider { + /** + * Provide the implementation of the symbol at the given position and document. + */ + provideTypeDefinition(model: editor.IReadOnlyModel, position: Position, token: CancellationToken): Definition | Thenable; + } + /** * A symbol kind. */ diff --git a/src/vs/platform/node/product.ts b/src/vs/platform/node/product.ts index d7b5a292652..96928924813 100644 --- a/src/vs/platform/node/product.ts +++ b/src/vs/platform/node/product.ts @@ -52,6 +52,7 @@ export interface IProductConfiguration { npsSurveyUrl: string; checksums: { [path: string]: string; }; checksumFailMoreInfoUrl: string; + extraNodeModules: string[]; } const rootPath = path.dirname(uri.parse(require.toUrl('')).fsPath); diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index d16ac7769a9..f415b76bc13 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -1632,6 +1632,24 @@ declare module 'vscode' { provideDefinition(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; } + /** + * The type definition provider interface defines the contract between extensions and + * the go to implementation feature. + */ + export interface TypeDefinitionProvider { + + /** + * Provide the implementations of the symbol at the given position and document. + * + * @param document The document in which the command was invoked. + * @param position The position at which the command was invoked. + * @param token A cancellation token. + * @return A definition or a thenable that resolves to such. The lack of a result can be + * signaled by returning `undefined` or `null`. + */ + provideTypeDefinition(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; + } + /** * MarkedString can be used to render human readable text. It is either a markdown string * or a code-block that provides a language and a code snippet. Note that @@ -3628,15 +3646,6 @@ declare module 'vscode' { */ export function createOutputChannel(name: string): OutputChannel; - /** - * Set a message to the status bar. This is a short hand for the more powerful - * status bar [items](#window.createStatusBarItem). - * - * @param text The message to show, support icon subtitution as in status bar [items](#StatusBarItem.text). - * @return A disposable which hides the status bar message. - */ - export function setStatusBarMessage(text: string): Disposable; - /** * Set a message to the status bar. This is a short hand for the more powerful * status bar [items](#window.createStatusBarItem). @@ -3657,6 +3666,18 @@ declare module 'vscode' { */ export function setStatusBarMessage(text: string, hideWhenDone: Thenable): Disposable; + /** + * Set a message to the status bar. This is a short hand for the more powerful + * status bar [items](#window.createStatusBarItem). + * + * *Note* that status bar messages stack and that they must be disposed when no + * longer used. + * + * @param text The message to show, support icon subtitution as in status bar [items](#StatusBarItem.text). + * @return A disposable which hides the status bar message. + */ + export function setStatusBarMessage(text: string): Disposable; + /** * Creates a status bar [item](#StatusBarItem). * @@ -4117,6 +4138,18 @@ declare module 'vscode' { */ export function registerDefinitionProvider(selector: DocumentSelector, provider: DefinitionProvider): Disposable; + /** + * Register an type definition provider. + * + * Multiple providers can be registered for a language. In that case providers are sorted + * by their [score](#languages.match) and the best-matching provider is used. + * + * @param selector A selector that defines the documents this provider is applicable to. + * @param provider An implementation provider. + * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + */ + export function registerTypeDefinitionProvider(selector: DocumentSelector, provider: TypeDefinitionProvider): Disposable; + /** * Register a hover provider. * diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index b07c897e685..4a9eb26034e 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -182,6 +182,9 @@ export function createApiFactory(initData: IInitData, threadService: IThreadServ registerDefinitionProvider(selector: vscode.DocumentSelector, provider: vscode.DefinitionProvider): vscode.Disposable { return languageFeatures.registerDefinitionProvider(selector, provider); }, + registerTypeDefinitionProvider(selector: vscode.DocumentSelector, provider: vscode.TypeDefinitionProvider): vscode.Disposable { + return languageFeatures.registerTypeDefinitionProvider(selector, provider); + }, registerHoverProvider(selector: vscode.DocumentSelector, provider: vscode.HoverProvider): vscode.Disposable { return languageFeatures.registerHoverProvider(selector, provider); }, diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 468e8644c93..84a0d86f0a8 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -153,6 +153,7 @@ export abstract class MainThreadLanguageFeaturesShape { $registerCodeLensSupport(handle: number, selector: vscode.DocumentSelector, eventHandle: number): TPromise { throw ni(); } $emitCodeLensEvent(eventHandle: number, event?: any): TPromise { throw ni(); } $registerDeclaractionSupport(handle: number, selector: vscode.DocumentSelector): TPromise { throw ni(); } + $registerImplementationSupport(handle: number, selector: vscode.DocumentSelector): TPromise { throw ni(); } $registerHoverProvider(handle: number, selector: vscode.DocumentSelector): TPromise { throw ni(); } $registerDocumentHighlightProvider(handle: number, selector: vscode.DocumentSelector): TPromise { throw ni(); } $registerReferenceSupport(handle: number, selector: vscode.DocumentSelector): TPromise { throw ni(); } @@ -335,6 +336,7 @@ export abstract class ExtHostLanguageFeaturesShape { $provideCodeLenses(handle: number, resource: URI): TPromise { throw ni(); } $resolveCodeLens(handle: number, resource: URI, symbol: modes.ICodeLensSymbol): TPromise { throw ni(); } $provideDefinition(handle: number, resource: URI, position: editorCommon.IPosition): TPromise { throw ni(); } + $provideTypeDefinition(handle: number, resource: URI, position: editorCommon.IPosition): TPromise { throw ni(); } $provideHover(handle: number, resource: URI, position: editorCommon.IPosition): TPromise { throw ni(); } $provideDocumentHighlights(handle: number, resource: URI, position: editorCommon.IPosition): TPromise { throw ni(); } $provideReferences(handle: number, resource: URI, position: editorCommon.IPosition, context: modes.ReferenceContext): TPromise { throw ni(); } diff --git a/src/vs/workbench/api/node/extHostApiCommands.ts b/src/vs/workbench/api/node/extHostApiCommands.ts index 5fca8b0a243..31b63b2fe0e 100644 --- a/src/vs/workbench/api/node/extHostApiCommands.ts +++ b/src/vs/workbench/api/node/extHostApiCommands.ts @@ -46,6 +46,14 @@ export class ExtHostApiCommands { ], returns: 'A promise that resolves to an array of Location-instances.' }); + this._register('vscode.executeTypeDefinitionProvider', this._executeTypeDefinitionProvider, { + description: 'Execute all implementation providers.', + args: [ + { name: 'uri', description: 'Uri of a text document', constraint: URI }, + { name: 'position', description: 'Position of a symbol', constraint: types.Position } + ], + returns: 'A promise that resolves to an array of Location-instance.' + }); this._register('vscode.executeHoverProvider', this._executeHoverProvider, { description: 'Execute all hover provider.', args: [ @@ -265,6 +273,18 @@ export class ExtHostApiCommands { }); } + private _executeTypeDefinitionProvider(resource: URI, position: types.Position): Thenable { + const args = { + resource, + position: position && typeConverters.fromPosition(position) + }; + return this._commands.executeCommand('_executeTypeDefinitionProvider', args).then(value => { + if (Array.isArray(value)) { + return value.map(typeConverters.location.to); + } + }); + } + private _executeHoverProvider(resource: URI, position: types.Position): Thenable { const args = { resource, diff --git a/src/vs/workbench/api/node/extHostLanguageFeatures.ts b/src/vs/workbench/api/node/extHostLanguageFeatures.ts index 6128e8165c2..ad4b7245e45 100644 --- a/src/vs/workbench/api/node/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/node/extHostLanguageFeatures.ts @@ -122,6 +122,29 @@ class DefinitionAdapter { } } +class ImplementationAdapter { + + private _documents: ExtHostDocuments; + private _provider: vscode.TypeDefinitionProvider; + + constructor(documents: ExtHostDocuments, provider: vscode.TypeDefinitionProvider) { + this._documents = documents; + this._provider = provider; + } + + provideTypeDefinition(resource: URI, position: IPosition): TPromise { + let doc = this._documents.getDocumentData(resource).document; + let pos = TypeConverters.toPosition(position); + return asWinJsPromise(token => this._provider.provideTypeDefinition(doc, pos, token)).then(value => { + if (Array.isArray(value)) { + return value.map(TypeConverters.location.from); + } else if (value) { + return TypeConverters.location.from(value); + } + }); + } +} + class HoverAdapter { private _documents: ExtHostDocuments; @@ -614,7 +637,7 @@ class LinkProviderAdapter { type Adapter = OutlineAdapter | CodeLensAdapter | DefinitionAdapter | HoverAdapter | DocumentHighlightAdapter | ReferenceAdapter | QuickFixAdapter | DocumentFormattingAdapter | RangeFormattingAdapter | OnTypeFormattingAdapter | NavigateTypeAdapter | RenameAdapter - | SuggestAdapter | SignatureHelpAdapter | LinkProviderAdapter; + | SuggestAdapter | SignatureHelpAdapter | LinkProviderAdapter | ImplementationAdapter; export class ExtHostLanguageFeatures extends ExtHostLanguageFeaturesShape { @@ -713,6 +736,17 @@ export class ExtHostLanguageFeatures extends ExtHostLanguageFeaturesShape { return this._withAdapter(handle, DefinitionAdapter, adapter => adapter.provideDefinition(resource, position)); } + registerTypeDefinitionProvider(selector: vscode.DocumentSelector, provider: vscode.TypeDefinitionProvider): vscode.Disposable { + const handle = this._nextHandle(); + this._adapter.set(handle, new ImplementationAdapter(this._documents, provider)); + this._proxy.$registerImplementationSupport(handle, selector); + return this._createDisposable(handle); + } + + $provideTypeDefinition(handle: number, resource: URI, position: IPosition): TPromise { + return this._withAdapter(handle, ImplementationAdapter, adapter => adapter.provideTypeDefinition(resource, position)); + } + // --- extra info registerHoverProvider(selector: vscode.DocumentSelector, provider: vscode.HoverProvider): vscode.Disposable { diff --git a/src/vs/workbench/api/node/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/node/mainThreadLanguageFeatures.ts index c1cc2c93809..5b12ce8f47d 100644 --- a/src/vs/workbench/api/node/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/node/mainThreadLanguageFeatures.ts @@ -102,6 +102,15 @@ export class MainThreadLanguageFeatures extends MainThreadLanguageFeaturesShape return undefined; } + $registerImplementationSupport(handle: number, selector: vscode.DocumentSelector): TPromise { + this._registrations[handle] = modes.TypeDefinitionProviderRegistry.register(selector, { + provideTypeDefinition: (model, position, token): Thenable => { + return wireCancellationToken(token, this._proxy.$provideTypeDefinition(handle, model.uri, position)); + } + }); + return undefined; + } + // --- extra info $registerHoverProvider(handle: number, selector: vscode.DocumentSelector): TPromise { diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index baac9cb8275..4095d49b320 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -95,7 +95,8 @@ Registry.as(EditorExtensions.Editors).registerEditor( ); interface ISerializedUntitledEditorInput { - resource: any | string; // TODO@Ben migration + resource: string; + resourceJSON: any; modeId: string; } @@ -120,7 +121,11 @@ class UntitledEditorInputFactory implements IEditorInputFactory { resource = URI.file(resource.fsPath); // untitled with associated file path use the file schema } - const serialized: ISerializedUntitledEditorInput = { resource: resource.toJSON(), modeId: untitledEditorInput.getModeId() }; + const serialized: ISerializedUntitledEditorInput = { + resource: resource.toString(), // Keep for backwards compatibility + resourceJSON: resource.toJSON(), + modeId: untitledEditorInput.getModeId() + }; return JSON.stringify(serialized); } @@ -128,7 +133,7 @@ class UntitledEditorInputFactory implements IEditorInputFactory { public deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): EditorInput { const deserialized: ISerializedUntitledEditorInput = JSON.parse(serializedEditorInput); - return this.untitledEditorService.createOrGet(typeof deserialized.resource === 'string' ? URI.parse(deserialized.resource) : URI.revive(deserialized.resource), deserialized.modeId); + return this.untitledEditorService.createOrGet(!!deserialized.resourceJSON ? URI.revive(deserialized.resourceJSON) : URI.parse(deserialized.resource), deserialized.modeId); } } diff --git a/src/vs/workbench/parts/debug/browser/debugActionItems.ts b/src/vs/workbench/parts/debug/browser/debugActionItems.ts index 9bd68c10517..5fee998bf96 100644 --- a/src/vs/workbench/parts/debug/browser/debugActionItems.ts +++ b/src/vs/workbench/parts/debug/browser/debugActionItems.ts @@ -22,6 +22,7 @@ const $ = dom.$; export class StartDebugActionItem extends EventEmitter implements IActionItem { private static ADD_CONFIGURATION = nls.localize('addConfiguration', "Add Configuration..."); + private static SEPARATOR = '─────────'; public actionRunner: IActionRunner; private container: HTMLElement; @@ -132,8 +133,9 @@ export class StartDebugActionItem extends EventEmitter implements IActionItem { } else { this.setEnabled(true); const selected = options.indexOf(this.debugService.getViewModel().selectedConfigurationName); + options.push(StartDebugActionItem.SEPARATOR); options.push(StartDebugActionItem.ADD_CONFIGURATION); - this.selectBox.setOptions(options, selected); + this.selectBox.setOptions(options, selected, options.length - 2); } } } diff --git a/src/vs/workbench/parts/debug/browser/debugContentProvider.ts b/src/vs/workbench/parts/debug/browser/debugContentProvider.ts index ceb3ae21200..e30d3cc43a5 100644 --- a/src/vs/workbench/parts/debug/browser/debugContentProvider.ts +++ b/src/vs/workbench/parts/debug/browser/debugContentProvider.ts @@ -6,14 +6,13 @@ import * as lifecycle from 'vs/base/common/lifecycle'; import uri from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; -import { guessMimeTypes } from 'vs/base/common/mime'; +import { guessMimeTypes, MIME_TEXT } from 'vs/base/common/mime'; import { IModel } from 'vs/editor/common/editorCommon'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { ITextModelResolverService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { DEBUG_SCHEME, IDebugService, State } from 'vs/workbench/parts/debug/common/debug'; -import { Model } from 'vs/workbench/parts/debug/common/debugModel'; import { Source } from 'vs/workbench/parts/debug/common/debugSource'; export class DebugContentProvider implements IWorkbenchContribution, ITextModelContentProvider { @@ -52,9 +51,12 @@ export class DebugContentProvider implements IWorkbenchContribution, ITextModelC this.modelsToDispose.push(model); return model; - }, err => { - (this.debugService.getModel()).sourceIsUnavailable(resource); - return err; + }, (err: DebugProtocol.ErrorResponse) => { + this.debugService.deemphasizeSource(resource); + const modePromise = this.modeService.getOrCreateMode(MIME_TEXT); + const model = this.modelService.createModel(err.message, modePromise, resource); + + return model; }); } } diff --git a/src/vs/workbench/parts/debug/common/debug.ts b/src/vs/workbench/parts/debug/common/debug.ts index e20e4ea4011..ea687355b40 100644 --- a/src/vs/workbench/parts/debug/common/debug.ts +++ b/src/vs/workbench/parts/debug/common/debug.ts @@ -476,6 +476,11 @@ export interface IDebugService { */ restartProcess(process: IProcess): TPromise; + /** + * Deemphasizes all sources with the passed uri. Source will appear as grayed out in callstack view. + */ + deemphasizeSource(uri: uri): void; + /** * Gets the current debug model. */ diff --git a/src/vs/workbench/parts/debug/common/debugModel.ts b/src/vs/workbench/parts/debug/common/debugModel.ts index ddd0be8e6d7..f06e036463c 100644 --- a/src/vs/workbench/parts/debug/common/debugModel.ts +++ b/src/vs/workbench/parts/debug/common/debugModel.ts @@ -381,7 +381,7 @@ export class Thread implements debug.IThread { } public getId(): string { - return `thread:${this.process.getId()}:${this.name}:${this.threadId}`; + return `thread:${this.process.getId()}:${this.threadId}`; } public clearCallStack(): void { @@ -506,6 +506,9 @@ export class Process implements debug.IProcess { if (data.thread && !this.threads.has(data.threadId)) { // A new thread came in, initialize it. this.threads.set(data.threadId, new Thread(this, data.thread.name, data.thread.id)); + } else if (data.thread && data.thread.name) { + // Just the thread name got updated #18244 + this.threads.get(data.threadId).name = data.thread.name; } if (data.stoppedDetails) { @@ -556,7 +559,7 @@ export class Process implements debug.IProcess { } } - public sourceIsUnavailable(uri: uri): void { + public deemphasizeSource(uri: uri): void { this.threads.forEach(thread => { thread.getCallStack().forEach(stackFrame => { if (stackFrame.source.uri.toString() === uri.toString()) { @@ -926,8 +929,8 @@ export class Model implements debug.IModel { this._onDidChangeWatchExpressions.fire(); } - public sourceIsUnavailable(uri: uri): void { - this.processes.forEach(p => p.sourceIsUnavailable(uri)); + public deemphasizeSource(uri: uri): void { + this.processes.forEach(p => p.deemphasizeSource(uri)); this._onDidChangeCallStack.fire(); } diff --git a/src/vs/workbench/parts/debug/electron-browser/debugHover.ts b/src/vs/workbench/parts/debug/electron-browser/debugHover.ts index 344ef9cf51a..4a939965429 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugHover.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugHover.ts @@ -213,7 +213,7 @@ export class DebugHoverWidget implements IContentWidget { } // Find the most specific scope containing the range #16632 - return [scopes.filter(scope => Range.containsRange(scope.range, expressionRange)) + return [scopes.filter(scope => scope.range && Range.containsRange(scope.range, expressionRange)) .sort((first, second) => (first.range.endLineNumber - first.range.startLineNumber) - (second.range.endLineNumber - second.range.startLineNumber)).shift()]; }) .then(scopes => TPromise.join(scopes.map(scope => this.doFindExpression(scope, namesToFind)))) diff --git a/src/vs/workbench/parts/debug/electron-browser/debugService.ts b/src/vs/workbench/parts/debug/electron-browser/debugService.ts index 09f24ecf255..893db6fdc9f 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugService.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugService.ts @@ -310,7 +310,7 @@ export class DebugService implements debug.IDebugService { })); this.toDisposeOnSessionEnd.get(session.getId()).push(session.onDidContinued(event => { - const threadId = event.body.allThreadsContinued ? undefined : event.body.threadId; + const threadId = event.body.allThreadsContinued !== false ? undefined : event.body.threadId; this.model.clearThreads(session.getId(), false, threadId); if (this.viewModel.focusedProcess.getId() === session.getId()) { this.focusStackFrameAndEvaluate(null, this.viewModel.focusedProcess).done(null, errors.onUnexpectedError); @@ -779,7 +779,7 @@ export class DebugService implements debug.IDebugService { this.lastTaskEvent = null; }); - if (filteredTasks[0].isWatching) { + if (filteredTasks[0].isBackground) { return new TPromise((c, e) => this.taskService.addOneTimeDisposableListener(TaskServiceEvents.Inactive, () => c(null))); } @@ -801,6 +801,10 @@ export class DebugService implements debug.IDebugService { }); } + public deemphasizeSource(uri: uri): void { + this.model.deemphasizeSource(uri); + } + public restartProcess(process: debug.IProcess): TPromise { if (!process) { return this.createProcess(this.viewModel.selectedConfigurationName); diff --git a/src/vs/workbench/parts/debug/electron-browser/debugViewer.ts b/src/vs/workbench/parts/debug/electron-browser/debugViewer.ts index 4ba2c467049..ab9ea647226 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugViewer.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugViewer.ts @@ -49,6 +49,11 @@ export interface IRenderValueOptions { showHover?: boolean; } +function replaceWhitespace(value: string): string { + const map = { '\n': '\\n', '\r': '\\r', '\t': '\\t' }; + return value.replace(/[\n\r\t]/g, char => map[char]); +} + export function renderExpressionValue(expressionOrValue: debug.IExpression | string, container: HTMLElement, options: IRenderValueOptions): void { let value = typeof expressionOrValue === 'string' ? expressionOrValue : expressionOrValue.value; @@ -77,8 +82,7 @@ export function renderExpressionValue(expressionOrValue: debug.IExpression | str value = value.substr(0, options.maxValueLength) + '...'; } if (value && !options.preserveWhitespace) { - const map = { '\n': '\\n', '\r': '\\r', '\t': '\\t' }; - container.textContent = value.replace(/[\n\r\t]/g, char => map[char]); + container.textContent = replaceWhitespace(value); } else { container.textContent = value; } @@ -89,7 +93,7 @@ export function renderExpressionValue(expressionOrValue: debug.IExpression | str export function renderVariable(tree: ITree, variable: Variable, data: IVariableTemplateData, showChanged: boolean): void { if (variable.available) { - data.name.textContent = variable.name; + data.name.textContent = replaceWhitespace(variable.name); data.name.title = variable.type ? variable.type : ''; } @@ -556,6 +560,9 @@ export class CallStackRenderer implements IRenderer { private renderStackFrame(stackFrame: debug.IStackFrame, data: IStackFrameTemplateData): void { stackFrame.source.deemphasize ? dom.addClass(data.stackFrame, 'disabled') : dom.removeClass(data.stackFrame, 'disabled'); data.file.title = stackFrame.source.raw.path || stackFrame.source.name; + if (stackFrame.source.raw.origin) { + data.file.title += `\n${stackFrame.source.raw.origin}`; + } data.label.textContent = stackFrame.name; data.label.title = stackFrame.name; data.fileName.textContent = getSourceName(stackFrame.source, this.contextService); diff --git a/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts b/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts index b3c2e259b8c..04be8868b7e 100644 --- a/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts +++ b/src/vs/workbench/parts/debug/electron-browser/rawDebugSession.ts @@ -54,6 +54,7 @@ export class RawDebugSession extends v8.V8Protocol implements debug.ISession { public disconnected: boolean; private sentPromises: TPromise[]; private capabilities: DebugProtocol.Capabilities; + private allThreadsContinued: boolean; private _onDidInitialize: Emitter; private _onDidStop: Emitter; @@ -80,6 +81,7 @@ export class RawDebugSession extends v8.V8Protocol implements debug.ISession { super(id); this.emittedStopped = false; this.readyForBreakpoints = false; + this.allThreadsContinued = false; this.sentPromises = []; this._onDidInitialize = new Emitter(); @@ -202,6 +204,7 @@ export class RawDebugSession extends v8.V8Protocol implements debug.ISession { this.emittedStopped = true; this._onDidStop.fire(event); } else if (event.event === 'continued') { + this.allThreadsContinued = (event).body.allThreadsContinued = false ? false : true; this._onDidContinued.fire(event); } else if (event.event === 'thread') { this._onDidThread.fire(event); @@ -270,7 +273,7 @@ export class RawDebugSession extends v8.V8Protocol implements debug.ISession { public continue(args: DebugProtocol.ContinueArguments): TPromise { return this.send('continue', args).then(response => { - this.fireFakeContinued(args.threadId); + this.fireFakeContinued(args.threadId, this.allThreadsContinued); return response; }); } @@ -402,12 +405,13 @@ export class RawDebugSession extends v8.V8Protocol implements debug.ISession { } } - private fireFakeContinued(threadId: number): void { + private fireFakeContinued(threadId: number, allThreadsContinued = false): void { this._onDidContinued.fire({ type: 'event', event: 'continued', body: { - threadId + threadId, + allThreadsContinued }, seq: undefined }); diff --git a/src/vs/workbench/parts/debug/test/common/mockDebug.ts b/src/vs/workbench/parts/debug/test/common/mockDebug.ts index 5a14dd02372..7e8acab6248 100644 --- a/src/vs/workbench/parts/debug/test/common/mockDebug.ts +++ b/src/vs/workbench/parts/debug/test/common/mockDebug.ts @@ -86,6 +86,8 @@ export class MockDebugService implements debug.IDebugService { public getViewModel(): debug.IViewModel { return null; } + + public deemphasizeSource(uri: uri): void { } } export class MockSession implements debug.ISession { diff --git a/src/vs/workbench/parts/files/browser/files.contribution.ts b/src/vs/workbench/parts/files/browser/files.contribution.ts index bf68449f9d5..891d5796745 100644 --- a/src/vs/workbench/parts/files/browser/files.contribution.ts +++ b/src/vs/workbench/parts/files/browser/files.contribution.ts @@ -105,7 +105,8 @@ const descriptor = new AsyncDescriptor('vs/workbench/parts/fil Registry.as(EditorExtensions.Editors).registerDefaultFileInput(descriptor); interface ISerializedFileInput { - resource: any | string; // TODO@Ben migration + resource: string; + resourceJSON: any; encoding?: string; } @@ -131,9 +132,10 @@ class FileEditorInputFactory implements IEditorInputFactory { public serialize(editorInput: EditorInput): string { const fileEditorInput = editorInput; - + const resource = fileEditorInput.getResource(); const fileInput: ISerializedFileInput = { - resource: fileEditorInput.getResource().toJSON() + resource: resource.toString(), // Keep for backwards compatibility + resourceJSON: resource.toJSON() }; const encoding = fileEditorInput.getPreferredEncoding(); @@ -147,7 +149,7 @@ class FileEditorInputFactory implements IEditorInputFactory { public deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): EditorInput { const fileInput: ISerializedFileInput = JSON.parse(serializedEditorInput); - return instantiationService.createInstance(FileEditorInput, typeof fileInput.resource === 'string' ? URI.parse(fileInput.resource) : URI.revive(fileInput.resource), fileInput.encoding); + return instantiationService.createInstance(FileEditorInput, !!fileInput.resourceJSON ? URI.revive(fileInput.resourceJSON) : URI.parse(fileInput.resource), fileInput.encoding); } } diff --git a/src/vs/workbench/parts/files/browser/views/openEditorsView.ts b/src/vs/workbench/parts/files/browser/views/openEditorsView.ts index cd6d46ba6cb..01ba9c4143f 100644 --- a/src/vs/workbench/parts/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/parts/files/browser/views/openEditorsView.ts @@ -105,7 +105,7 @@ export class OpenEditorsView extends AdaptiveCollapsibleViewletView { }, { indentPixels: 0, twistiePixels: 20, - ariaLabel: nls.localize({ key: 'treeAriaLabel', comment: ['Open is an adjective'] }, "Open Editors") + ariaLabel: nls.localize({ key: 'treeAriaLabel', comment: ['Open is an adjective'] }, "Open Editors: List of Active Files") }); this.fullRefreshNeeded = true; diff --git a/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts b/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts index 167f097a8d6..fe00a431112 100644 --- a/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts +++ b/src/vs/workbench/parts/files/common/editors/fileEditorInput.ts @@ -71,10 +71,6 @@ export class FileEditorInput extends EditorInput implements IFileEditorInput { } public setResource(resource: URI): void { - if (resource.scheme !== 'file') { - throw new Error('FileEditorInput can only handle file:// resources.'); - } - this.resource = resource; // Reset resource dependent properties diff --git a/src/vs/workbench/parts/output/browser/outputServices.ts b/src/vs/workbench/parts/output/browser/outputServices.ts index cd5d58d457a..67b45331611 100644 --- a/src/vs/workbench/parts/output/browser/outputServices.ts +++ b/src/vs/workbench/parts/output/browser/outputServices.ts @@ -243,6 +243,7 @@ class OutputContentProvider implements ITextModelContentProvider { } const bufferedOutput = this.bufferedOutput[channel]; + this.bufferedOutput[channel] = ''; if (!bufferedOutput) { return; // return if nothing to append } diff --git a/src/vs/workbench/parts/search/browser/searchViewlet.ts b/src/vs/workbench/parts/search/browser/searchViewlet.ts index 050e2799129..62ba4873eeb 100644 --- a/src/vs/workbench/parts/search/browser/searchViewlet.ts +++ b/src/vs/workbench/parts/search/browser/searchViewlet.ts @@ -999,7 +999,7 @@ export class SearchViewlet extends Viewlet { // Fake progress up to 90%, or when actual progress beats it const fakeMax = 900; - const fakeMultiplier = 15; + const fakeMultiplier = 12; if (fakeProgress && progressWorked < fakeMax) { // Linearly decrease the rate of fake progress. // 1 is the smallest allowed amount of progress. diff --git a/src/vs/workbench/parts/tasks/common/languageServiceTaskSystem.ts b/src/vs/workbench/parts/tasks/common/languageServiceTaskSystem.ts deleted file mode 100644 index 3fb0a2240df..00000000000 --- a/src/vs/workbench/parts/tasks/common/languageServiceTaskSystem.ts +++ /dev/null @@ -1,115 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 { TPromise, Promise } from 'vs/base/common/winjs.base'; -import { TerminateResponse } from 'vs/base/common/processes'; - -import { IMode } from 'vs/editor/common/modes'; -import { EventEmitter } from 'vs/base/common/eventEmitter'; - -import { ITaskSystem, ITaskSummary, TaskDescription, TelemetryEvent, Triggers, TaskConfiguration, ITaskExecuteResult, TaskExecuteKind } from 'vs/workbench/parts/tasks/common/taskSystem'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IModeService } from 'vs/editor/common/services/modeService'; - -export interface LanguageServiceTaskConfiguration extends TaskConfiguration { - modes: string[]; -} - -export class LanguageServiceTaskSystem extends EventEmitter implements ITaskSystem { - - public static TelemetryEventName: string = 'taskService'; - - private configuration: LanguageServiceTaskConfiguration; - private telemetryService: ITelemetryService; - private modeService: IModeService; - - constructor(configuration: LanguageServiceTaskConfiguration, telemetryService: ITelemetryService, modeService: IModeService) { - super(); - this.configuration = configuration; - this.telemetryService = telemetryService; - this.modeService = modeService; - } - - public build(): ITaskExecuteResult { - return this.processMode((mode) => { - return null; - }, 'build', Triggers.shortcut); - } - - public rebuild(): ITaskExecuteResult { - return this.processMode((mode) => { - return null; - }, 'rebuild', Triggers.shortcut); - } - - public clean(): ITaskExecuteResult { - return this.processMode((mode) => { - return null; - }, 'clean', Triggers.shortcut); - } - - public runTest(): ITaskExecuteResult { - return { kind: TaskExecuteKind.Started, promise: TPromise.wrapError('Not implemented yet.') }; - } - - public run(taskIdentifier: string): ITaskExecuteResult { - return { kind: TaskExecuteKind.Started, promise: TPromise.wrapError('Not implemented yet.') }; - } - - public isActive(): TPromise { - return TPromise.as(false); - } - - public isActiveSync(): boolean { - return false; - } - - public canAutoTerminate(): boolean { - return false; - } - - public terminate(): TPromise { - return TPromise.as({ success: true }); - } - - public terminateSync(): TerminateResponse { - return { success: true }; - } - - public tasks(): TPromise { - let result: TaskDescription[] = []; - return TPromise.as(result); - } - - private processMode(fn: (mode: IMode) => Promise, taskName: string, trigger: string): ITaskExecuteResult { - let telemetryEvent: TelemetryEvent = { - trigger: trigger, - command: 'languageService', - success: true - }; - return { - kind: TaskExecuteKind.Started, started: {}, promise: Promise.join(this.configuration.modes.map((mode) => { - return this.modeService.getOrCreateMode(mode); - })).then((modes: IMode[]) => { - let promises: Promise[] = []; - modes.forEach((mode) => { - let promise = fn(mode); - if (promise) { - promises.push(promise); - } - }); - return Promise.join(promises); - }).then((value) => { - this.telemetryService.publicLog(LanguageServiceTaskSystem.TelemetryEventName, telemetryEvent); - return value; - }, (err) => { - telemetryEvent.success = false; - this.telemetryService.publicLog(LanguageServiceTaskSystem.TelemetryEventName, telemetryEvent); - return Promise.wrapError(err); - }) - }; - } -} diff --git a/src/vs/workbench/parts/tasks/common/taskSystem.ts b/src/vs/workbench/parts/tasks/common/taskSystem.ts index 420f753685c..186e190fbe8 100644 --- a/src/vs/workbench/parts/tasks/common/taskSystem.ts +++ b/src/vs/workbench/parts/tasks/common/taskSystem.ts @@ -98,9 +98,9 @@ export interface TaskDescription { args?: string[]; /** - * Whether the task is running in watching mode or not. + * Whether the task is a background task or not. */ - isWatching?: boolean; + isBackground?: boolean; /** * Whether the task should prompt on close for confirmation if running. @@ -201,7 +201,7 @@ export interface ITaskExecuteResult { }; active?: { same: boolean; - watching: boolean; + background: boolean; }; } @@ -242,5 +242,5 @@ export interface TaskConfiguration { /** * The build system to use. If omitted program is used. */ - buildSystem?: string; + _runner?: string; } \ No newline at end of file diff --git a/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts b/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts index 57bbc37b561..282718bc334 100644 --- a/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts +++ b/src/vs/workbench/parts/tasks/electron-browser/task.contribution.ts @@ -59,13 +59,15 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IOutputService, IOutputChannelRegistry, Extensions as OutputExt, IOutputChannel } from 'vs/workbench/parts/output/common/output'; +import { ITerminalService } from 'vs/workbench/parts/terminal/common/terminal'; + import { ITaskSystem, ITaskSummary, ITaskExecuteResult, TaskExecuteKind, TaskError, TaskErrors, TaskConfiguration, TaskDescription, TaskSystemEvents } from 'vs/workbench/parts/tasks/common/taskSystem'; import { ITaskService, TaskServiceEvents } from 'vs/workbench/parts/tasks/common/taskService'; import { templates as taskTemplates } from 'vs/workbench/parts/tasks/common/taskTemplates'; -import { LanguageServiceTaskSystem, LanguageServiceTaskConfiguration } from 'vs/workbench/parts/tasks/common/languageServiceTaskSystem'; import * as FileConfig from 'vs/workbench/parts/tasks/node/processRunnerConfiguration'; import { ProcessRunnerSystem } from 'vs/workbench/parts/tasks/node/processRunnerSystem'; +import { TerminalTaskSystem } from './terminalTaskSystem'; import { ProcessRunnerDetector } from 'vs/workbench/parts/tasks/node/processRunnerDetector'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -631,7 +633,9 @@ class TaskService extends EventEmitter implements ITaskService { @IModelService modelService: IModelService, @IExtensionService extensionService: IExtensionService, @IQuickOpenService quickOpenService: IQuickOpenService, @IEnvironmentService private environmentService: IEnvironmentService, - @IConfigurationResolverService private configurationResolverService: IConfigurationResolverService) { + @IConfigurationResolverService private configurationResolverService: IConfigurationResolverService, + @ITerminalService private terminalService: ITerminalService + ) { super(); this.modeService = modeService; @@ -740,10 +744,15 @@ class TaskService extends EventEmitter implements ITaskService { throw new TaskError(Severity.Info, nls.localize('TaskSystem.noConfiguration', 'No task runner configured.'), TaskErrors.NotConfigured); } let result: ITaskSystem = null; - if (config.buildSystem === 'service') { - result = new LanguageServiceTaskSystem(config, this.telemetryService, this.modeService); - } else if (this.isRunnerConfig(config)) { + if (this.isRunnerConfig(config)) { result = new ProcessRunnerSystem(config, this.markerService, this.modelService, this.telemetryService, this.outputService, this.configurationResolverService, TaskService.OutputChannelId, clearOutput); + } else if (this.isTerminalConfig(config)) { + result = new TerminalTaskSystem( + config, + this.terminalService, this.outputService, this.markerService, + this.modelService, this.configurationResolverService, this.telemetryService, + TaskService.OutputChannelId + ); } if (result === null) { this._taskSystemPromise = null; @@ -776,7 +785,11 @@ class TaskService extends EventEmitter implements ITaskService { } private isRunnerConfig(config: TaskConfiguration): boolean { - return !config.buildSystem || config.buildSystem === 'program'; + return !config._runner || config._runner === 'program'; + } + + private isTerminalConfig(config: TaskConfiguration): boolean { + return config._runner === 'terminal'; } private hasDetectorSupport(config: FileConfig.ExternalTaskRunnerConfiguration): boolean { @@ -819,14 +832,14 @@ class TaskService extends EventEmitter implements ITaskService { } private executeTarget(fn: (taskSystem: ITaskSystem) => ITaskExecuteResult): TPromise { - return this.textFileService.saveAll().then((value) => { // make sure all dirty files are saved - return this.configurationService.reloadConfiguration().then(() => { // make sure configuration is up to date + return this.textFileService.saveAll().then((value) => { // make sure all dirty files are saved + return this.configurationService.reloadConfiguration().then(() => { // make sure configuration is up to date return this.taskSystemPromise. then((taskSystem) => { let executeResult = fn(taskSystem); if (executeResult.kind === TaskExecuteKind.Active) { let active = executeResult.active; - if (active.same && active.watching) { + if (active.same && active.background) { this.messageService.show(Severity.Info, nls.localize('TaskSystem.activeSame', 'The task is already active and in watch mode. To terminate the task use `F1 > terminate task`')); } else { throw new TaskError(Severity.Warning, nls.localize('TaskSystem.active', 'There is an active running task right now. Terminate it first before executing another task.'), TaskErrors.RunningTask); diff --git a/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts b/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts new file mode 100644 index 00000000000..f76e7114a1a --- /dev/null +++ b/src/vs/workbench/parts/tasks/electron-browser/terminalTaskSystem.ts @@ -0,0 +1,440 @@ +/*--------------------------------------------------------------------------------------------- + * 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 path = require('path'); + +import * as nls from 'vs/nls'; +import * as Objects from 'vs/base/common/objects'; +import { CharCode } from 'vs/base/common/charCode'; +import * as Platform from 'vs/base/common/platform'; +import * as Async from 'vs/base/common/async'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IStringDictionary } from 'vs/base/common/collections'; +import Severity from 'vs/base/common/severity'; +import { EventEmitter } from 'vs/base/common/eventEmitter'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { TerminateResponse } from 'vs/base/common/processes'; + +import { IMarkerService } from 'vs/platform/markers/common/markers'; +import { ValidationStatus } from 'vs/base/common/parsers'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { ProblemMatcher } from 'vs/platform/markers/common/problemMatcher'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; + +import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; +import { ITerminalService, ITerminalInstance } from 'vs/workbench/parts/terminal/common/terminal'; +import { TerminalConfigHelper } from 'vs/workbench/parts/terminal/electron-browser/terminalConfigHelper'; +import { IOutputService, IOutputChannel } from 'vs/workbench/parts/output/common/output'; +import { StartStopProblemCollector, WatchingProblemCollector, ProblemCollectorEvents } from 'vs/workbench/parts/tasks/common/problemCollectors'; +import { ITaskSystem, ITaskSummary, ITaskExecuteResult, TaskExecuteKind, TaskError, TaskErrors, TaskRunnerConfiguration, TaskDescription, ShowOutput, TelemetryEvent, Triggers, TaskSystemEvents, TaskEvent, TaskType } from 'vs/workbench/parts/tasks/common/taskSystem'; +import * as FileConfig from '../node/processRunnerConfiguration'; + +interface TerminalData { + terminal: ITerminalInstance; + promise: TPromise; +} + +class TerminalDecoder { + // See https://en.wikipedia.org/wiki/ANSI_escape_code & http://stackoverflow.com/questions/25189651/how-to-remove-ansi-control-chars-vt100-from-a-java-string & + // https://www.npmjs.com/package/strip-ansi + private static ANSI_CONTROL_SEQUENCE: RegExp = /\x1b[[()#;?]*(?:\d{1,4}(?:;\d{0,4})*)?[0-9A-ORZcf-nqry=><]/g; + + private remaining: string; + + public write(data: string): string[] { + let result: string[] = []; + let value = this.remaining + ? this.remaining + data.replace(TerminalDecoder.ANSI_CONTROL_SEQUENCE, '') + : data.replace(TerminalDecoder.ANSI_CONTROL_SEQUENCE, ''); + + if (value.length < 1) { + return result; + } + let start = 0; + let ch: number; + while (start < value.length && ((ch = value.charCodeAt(start)) === CharCode.CarriageReturn || ch === CharCode.LineFeed)) { + start++; + } + let idx = start; + while (idx < value.length) { + ch = value.charCodeAt(idx); + if (ch === CharCode.CarriageReturn || ch === CharCode.LineFeed) { + result.push(value.substring(start, idx)); + idx++; + while (idx < value.length && ((ch = value.charCodeAt(idx)) === CharCode.CarriageReturn || ch === CharCode.LineFeed)) { + idx++; + } + start = idx; + } else { + idx++; + } + } + this.remaining = start < value.length ? value.substr(start) : null; + return result; + } + + public end(): string { + return this.remaining; + } +} +export class TerminalTaskSystem extends EventEmitter implements ITaskSystem { + + public static TelemetryEventName: string = 'taskService'; + + private validationStatus: ValidationStatus; + private buildTaskIdentifier: string; + private testTaskIdentifier: string; + private configuration: TaskRunnerConfiguration; + + private outputChannel: IOutputChannel; + private activeTasks: IStringDictionary; + + constructor(private fileConfig: FileConfig.ExternalTaskRunnerConfiguration, private terminalService: ITerminalService, private outputService: IOutputService, + private markerService: IMarkerService, private modelService: IModelService, private configurationResolverService: IConfigurationResolverService, + private telemetryService: ITelemetryService, outputChannelId: string) { + super(); + + this.outputChannel = this.outputService.getChannel(outputChannelId); + this.clearOutput(); + this.activeTasks = Object.create(null); + + let parseResult = FileConfig.parse(fileConfig, this); + this.validationStatus = parseResult.validationStatus; + this.configuration = parseResult.configuration; + this.buildTaskIdentifier = parseResult.defaultBuildTaskIdentifier; + this.testTaskIdentifier = parseResult.defaultTestTaskIdentifier; + + if (!this.validationStatus.isOK()) { + this.showOutput(); + } + } + + public log(value: string): void { + this.outputChannel.append(value + '\n'); + } + + private showOutput(): void { + this.outputChannel.show(true); + } + + private clearOutput(): void { + this.outputChannel.clear(); + } + + public build(): ITaskExecuteResult { + if (!this.buildTaskIdentifier) { + throw new TaskError(Severity.Info, nls.localize('TerminalTaskSystem.noBuildTask', 'No build task defined in tasks.json'), TaskErrors.NoBuildTask); + } + return this.run(this.buildTaskIdentifier, Triggers.shortcut); + } + + public rebuild(): ITaskExecuteResult { + return null; + } + + public clean(): ITaskExecuteResult { + return null; + } + + public runTest(): ITaskExecuteResult { + return null; + } + + public run(taskIdentifier: string, trigger: string = Triggers.command): ITaskExecuteResult { + let task = this.configuration.tasks[taskIdentifier]; + if (!task) { + throw new TaskError(Severity.Info, nls.localize('TerminalTaskSystem.noTask', 'Task \'{0}\' not found', taskIdentifier), TaskErrors.TaskNotFound); + } + let telemetryEvent: TelemetryEvent = { + trigger: trigger, + command: 'other', + success: true + }; + try { + let result = this.executeTask(task, telemetryEvent); + result.promise = result.promise.then((summary) => { + this.telemetryService.publicLog(TerminalTaskSystem.TelemetryEventName, telemetryEvent); + return summary; + }, (error) => { + telemetryEvent.success = false; + this.telemetryService.publicLog(TerminalTaskSystem.TelemetryEventName, telemetryEvent); + return TPromise.wrapError(error); + }); + return result; + } catch (error) { + telemetryEvent.success = false; + this.telemetryService.publicLog(TerminalTaskSystem.TelemetryEventName, telemetryEvent); + if (error instanceof TaskError) { + throw error; + } else if (error instanceof Error) { + this.log(error.message); + throw new TaskError(Severity.Error, error.message, TaskErrors.UnknownError); + } else { + this.log(error.toString()); + throw new TaskError(Severity.Error, nls.localize('TerminalTaskSystem.unknownError', 'A unknown error has occurred while executing a task. See task output log for details.'), TaskErrors.UnknownError); + } + } + } + + public isActive(): TPromise { + return TPromise.as(false); + } + + public isActiveSync(): boolean { + return false; + } + + public canAutoTerminate(): boolean { + return false; + } + + public terminate(): TPromise { + return null; + } + + public tasks(): TPromise { + let result: TaskDescription[]; + if (!this.configuration || !this.configuration.tasks) { + result = []; + } else { + result = Object.keys(this.configuration.tasks).map(key => this.configuration.tasks[key]); + } + return TPromise.as(result); + } + + private executeTask(task: TaskDescription, telemetryEvent: TelemetryEvent): ITaskExecuteResult { + let terminalData = this.activeTasks[task.id]; + if (terminalData && terminalData.promise) { + if (task.showOutput === ShowOutput.Always) { + terminalData.terminal.setVisible(true); + } + return { kind: TaskExecuteKind.Active, active: { same: true, background: task.isBackground }, promise: terminalData.promise }; + } else { + let terminal: ITerminalInstance = undefined; + let promise: TPromise = undefined; + if (task.isBackground) { + promise = new TPromise((resolve, reject) => { + let watchingProblemMatcher = new WatchingProblemCollector(this.resolveMatchers(task.problemMatchers), this.markerService, this.modelService); + let toUnbind: IDisposable[] = []; + let event: TaskEvent = { taskId: task.id, taskName: task.name, type: TaskType.Watching }; + let eventCounter: number = 0; + toUnbind.push(watchingProblemMatcher.addListener2(ProblemCollectorEvents.WatchingBeginDetected, () => { + eventCounter++; + this.emit(TaskSystemEvents.Active, event); + })); + toUnbind.push(watchingProblemMatcher.addListener2(ProblemCollectorEvents.WatchingEndDetected, () => { + eventCounter--; + this.emit(TaskSystemEvents.Inactive, event); + })); + watchingProblemMatcher.aboutToStart(); + let delayer: Async.Delayer = null; + let decoder = new TerminalDecoder(); + terminal = this.createTerminal(task); + terminal.onData((data: string) => { + decoder.write(data).forEach(line => { + watchingProblemMatcher.processLine(line); + if (delayer === null) { + delayer = new Async.Delayer(3000); + } + delayer.trigger(() => { + watchingProblemMatcher.forceDelivery(); + delayer = null; + }); + }); + }); + terminal.onExit((exitCode) => { + watchingProblemMatcher.dispose(); + toUnbind = dispose(toUnbind); + toUnbind = null; + for (let i = 0; i < eventCounter; i++) { + this.emit(TaskSystemEvents.Inactive, event); + } + eventCounter = 0; + if (exitCode && exitCode === 1 && watchingProblemMatcher.numberOfMatches === 0 && task.showOutput !== ShowOutput.Never) { + this.terminalService.setActiveInstance(terminal); + this.terminalService.showPanel(false); + } + resolve({ exitCode }); + }); + }); + } else { + promise = new TPromise((resolve, reject) => { + terminal = this.createTerminal(task); + this.emit(TaskSystemEvents.Active, event); + let decoder = new TerminalDecoder(); + let startStopProblemMatcher = new StartStopProblemCollector(this.resolveMatchers(task.problemMatchers), this.markerService, this.modelService); + terminal.onData((data: string) => { + decoder.write(data).forEach((line) => { + startStopProblemMatcher.processLine(line); + }); + }); + terminal.onExit((exitCode) => { + startStopProblemMatcher.processLine(decoder.end()); + startStopProblemMatcher.done(); + startStopProblemMatcher.dispose(); + this.emit(TaskSystemEvents.Inactive, event); + delete this.activeTasks[task.id]; + resolve({ exitCode }); + }); + this.terminalService.setActiveInstance(terminal); + if (task.showOutput === ShowOutput.Always) { + this.terminalService.showPanel(false); + } + }); + } + this.terminalService.setActiveInstance(terminal); + if (task.showOutput === ShowOutput.Always) { + this.terminalService.showPanel(false); + } + this.activeTasks[task.id] = { terminal, promise }; + return { kind: TaskExecuteKind.Started, started: {}, promise: promise }; + } + } + + private createTerminal(task: TaskDescription): ITerminalInstance { + let { command, args } = this.resolveCommandAndArgs(task); + let terminalName = nls.localize('TerminalTaskSystem.terminalName', 'Task - {0}', task.name); + if (this.configuration.isShellCommand) { + let shellConfig = (this.terminalService.configHelper as TerminalConfigHelper).getShell(); + let shellArgs = shellConfig.args.slice(0); + let toAdd: string[] = []; + let commandLine: string; + if (Platform.isWindows) { + toAdd.push('/d', '/c'); + let quotedCommand: boolean = false; + let quotedArg: boolean = false; + let quoted = this.ensureDoubleQuotes(command); + let commandPieces: string[] = []; + commandPieces.push(quoted.value); + quotedCommand = quoted.quoted; + if (args) { + args.forEach((arg) => { + quoted = this.ensureDoubleQuotes(arg); + commandPieces.push(quoted.value); + quotedArg = quotedArg && quoted.quoted; + }); + } + if (quotedCommand) { + if (quotedArg) { + commandLine = '"' + commandPieces.join(' ') + '"'; + } else { + if (commandPieces.length > 1) { + commandLine = '"' + commandPieces[0] + '"' + ' ' + commandPieces.slice(1).join(' '); + } else { + commandLine = '"' + commandPieces[0] + '"'; + } + } + } else { + commandLine = commandPieces.join(' '); + } + } else { + toAdd.push('-c'); + commandLine = `${command} ${args.join(' ')}`; + } + toAdd.forEach(element => { + if (!shellArgs.some(arg => arg.toLowerCase() === element)) { + shellArgs.push(element); + } + }); + shellArgs.push(commandLine); + return this.terminalService.createInstance(terminalName, shellConfig.executable, shellArgs, true); + } else { + return this.terminalService.createInstance(terminalName, command, args, true); + } + } + + private resolveCommandAndArgs(task: TaskDescription): { command: string, args: string[] } { + let args: string[] = this.configuration.args ? this.configuration.args.slice() : []; + // We need to first pass the task name + if (!task.suppressTaskName) { + if (this.fileConfig.taskSelector) { + args.push(this.fileConfig.taskSelector + task.name); + } else { + args.push(task.name); + } + } + // And then additional arguments + if (task.args) { + args = args.concat(task.args); + } + args = this.resolveVariables(args); + let command: string = this.resolveVariable(this.configuration.command); + return { command, args }; + } + + + private resolveVariables(value: string[]): string[] { + return value.map(s => this.resolveVariable(s)); + } + + private resolveMatchers(values: T[]): T[] { + if (values.length === 0) { + return values; + } + let result: T[] = []; + values.forEach((matcher) => { + if (!matcher.filePrefix) { + result.push(matcher); + } else { + let copy = Objects.clone(matcher); + copy.filePrefix = this.resolveVariable(copy.filePrefix); + result.push(copy); + } + }); + return result; + } + + private resolveVariable(value: string): string { + return this.configurationResolverService.resolve(value); + } + + private static doubleQuotes = /^[^"].* .*[^"]$/; + private ensureDoubleQuotes(value: string) { + if (TerminalTaskSystem.doubleQuotes.test(value)) { + return { + value: '"' + value + '"', + quoted: true + }; + } else { + return { + value: value, + quoted: value.length > 0 && value[0] === '"' && value[value.length - 1] === '"' + }; + } + } + + private static WellKnowCommands: IStringDictionary = { + 'ant': true, + 'cmake': true, + 'eslint': true, + 'gradle': true, + 'grunt': true, + 'gulp': true, + 'jake': true, + 'jenkins': true, + 'jshint': true, + 'make': true, + 'maven': true, + 'msbuild': true, + 'msc': true, + 'nmake': true, + 'npm': true, + 'rake': true, + 'tsc': true, + 'xbuild': true + }; + public getSanitizedCommand(cmd: string): string { + let result = cmd.toLowerCase(); + let index = result.lastIndexOf(path.sep); + if (index !== -1) { + result = result.substring(index + 1); + } + if (TerminalTaskSystem.WellKnowCommands[result]) { + return result; + } + return 'other'; + } +} \ No newline at end of file diff --git a/src/vs/workbench/parts/tasks/node/processRunnerConfiguration.ts b/src/vs/workbench/parts/tasks/node/processRunnerConfiguration.ts index 54ed1f79ee5..6634b8455fe 100644 --- a/src/vs/workbench/parts/tasks/node/processRunnerConfiguration.ts +++ b/src/vs/workbench/parts/tasks/node/processRunnerConfiguration.ts @@ -451,7 +451,7 @@ class ConfigurationParser { name: globals.command, showOutput: globals.showOutput, suppressTaskName: true, - isWatching: isWatching, + isBackground: isWatching, promptOnClose: promptOnClose, echoCommand: globals.echoCommand, }; @@ -539,15 +539,15 @@ class ConfigurationParser { if (Types.isStringArray(externalTask.args)) { task.args = externalTask.args.slice(); } - task.isWatching = false; + task.isBackground = false; if (!Types.isUndefined(externalTask.isWatching)) { - task.isWatching = !!externalTask.isWatching; + task.isBackground = !!externalTask.isWatching; } task.promptOnClose = true; if (!Types.isUndefined(externalTask.promptOnClose)) { task.promptOnClose = !!externalTask.promptOnClose; } else { - task.promptOnClose = !task.isWatching; + task.promptOnClose = !task.isBackground; } if (Types.isString(externalTask.showOutput)) { task.showOutput = TaskSystem.ShowOutput.fromString(externalTask.showOutput); diff --git a/src/vs/workbench/parts/tasks/node/processRunnerSystem.ts b/src/vs/workbench/parts/tasks/node/processRunnerSystem.ts index 9a90c03d842..643d5bcc4b5 100644 --- a/src/vs/workbench/parts/tasks/node/processRunnerSystem.ts +++ b/src/vs/workbench/parts/tasks/node/processRunnerSystem.ts @@ -90,7 +90,7 @@ export class ProcessRunnerSystem extends EventEmitter implements ITaskSystem { public build(): ITaskExecuteResult { if (this.activeTaskIdentifier) { let task = this.configuration.tasks[this.activeTaskIdentifier]; - return { kind: TaskExecuteKind.Active, active: { same: this.activeTaskIdentifier === this.defaultBuildTaskIdentifier, watching: task.isWatching }, promise: this.activeTaskPromise }; + return { kind: TaskExecuteKind.Active, active: { same: this.activeTaskIdentifier === this.defaultBuildTaskIdentifier, background: task.isBackground }, promise: this.activeTaskPromise }; } if (!this.defaultBuildTaskIdentifier) { throw new TaskError(Severity.Info, nls.localize('TaskRunnerSystem.noBuildTask', 'No task is marked as a build task in the tasks.json. Mark a task with \'isBuildCommand\'.'), TaskErrors.NoBuildTask); @@ -109,7 +109,7 @@ export class ProcessRunnerSystem extends EventEmitter implements ITaskSystem { public runTest(): ITaskExecuteResult { if (this.activeTaskIdentifier) { let task = this.configuration.tasks[this.activeTaskIdentifier]; - return { kind: TaskExecuteKind.Active, active: { same: this.activeTaskIdentifier === this.defaultTestTaskIdentifier, watching: task.isWatching }, promise: this.activeTaskPromise }; + return { kind: TaskExecuteKind.Active, active: { same: this.activeTaskIdentifier === this.defaultTestTaskIdentifier, background: task.isBackground }, promise: this.activeTaskPromise }; } if (!this.defaultTestTaskIdentifier) { throw new TaskError(Severity.Info, nls.localize('TaskRunnerSystem.noTestTask', 'No test task configured.'), TaskErrors.NoTestTask); @@ -120,7 +120,7 @@ export class ProcessRunnerSystem extends EventEmitter implements ITaskSystem { public run(taskIdentifier: string): ITaskExecuteResult { if (this.activeTaskIdentifier) { let task = this.configuration.tasks[this.activeTaskIdentifier]; - return { kind: TaskExecuteKind.Active, active: { same: this.activeTaskIdentifier === taskIdentifier, watching: task.isWatching }, promise: this.activeTaskPromise }; + return { kind: TaskExecuteKind.Active, active: { same: this.activeTaskIdentifier === taskIdentifier, background: task.isBackground }, promise: this.activeTaskPromise }; } return this.executeTask(taskIdentifier); } @@ -239,7 +239,7 @@ export class ProcessRunnerSystem extends EventEmitter implements ITaskSystem { let prompt: string = Platform.isWindows ? '>' : '$'; this.log(`running command${prompt} ${command} ${args.join(' ')}`); } - if (task.isWatching) { + if (task.isBackground) { let watchingProblemMatcher = new WatchingProblemCollector(this.resolveMatchers(task.problemMatchers), this.markerService, this.modelService); let toUnbind: IDisposable[] = []; let event: TaskEvent = { taskId: task.id, taskName: task.name, type: TaskType.Watching }; 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 2cd2709a8d8..4b83891a7b0 100644 --- a/src/vs/workbench/parts/tasks/test/node/configuration.test.ts +++ b/src/vs/workbench/parts/tasks/test/node/configuration.test.ts @@ -73,7 +73,7 @@ class TaskBuilder { showOutput: TaskSystem.ShowOutput.Always, suppressTaskName: false, echoCommand: false, - isWatching: false, + isBackground: false, promptOnClose: true, problemMatchers: [] }; @@ -99,8 +99,8 @@ class TaskBuilder { return this; } - public isWatching(value: boolean): TaskBuilder { - this.result.isWatching = value; + public isBackground(value: boolean): TaskBuilder { + this.result.isBackground = value; return this; } @@ -307,7 +307,7 @@ suite('Tasks Configuration parsing tests', () => { let builder = new ConfiguationBuilder('tsc'); builder.task('tsc'). suppressTaskName(true). - isWatching(true). + isBackground(true). promptOnClose(false); testGobalCommand( { @@ -627,7 +627,7 @@ suite('Tasks Configuration parsing tests', () => { showOutput(TaskSystem.ShowOutput.Never). echoCommand(true). args(['--p']). - isWatching(true). + isBackground(true). promptOnClose(false); let result = testConfiguration(external, builder); @@ -833,7 +833,7 @@ suite('Tasks Configuration parsing tests', () => { ] }; let builder = new ConfiguationBuilder('tsc'); - builder.task('taskName').isWatching(true).promptOnClose(false); + builder.task('taskName').isBackground(true).promptOnClose(false); testConfiguration(external, builder); }); @@ -943,7 +943,7 @@ suite('Tasks Configuration parsing tests', () => { assert.strictEqual(actual.showOutput, expected.showOutput, 'showOutput'); assert.strictEqual(actual.suppressTaskName, expected.suppressTaskName, 'suppressTaskName'); assert.strictEqual(actual.echoCommand, expected.echoCommand, 'echoCommand'); - assert.strictEqual(actual.isWatching, expected.isWatching, 'isWatching'); + assert.strictEqual(actual.isBackground, expected.isBackground, 'isBackground'); assert.strictEqual(actual.promptOnClose, expected.promptOnClose, 'promptOnClose'); assert.strictEqual(typeof actual.problemMatchers, typeof expected.problemMatchers); if (actual.problemMatchers && expected.problemMatchers) { diff --git a/src/vs/workbench/services/history/browser/history.ts b/src/vs/workbench/services/history/browser/history.ts index d5f080a2628..cef3f762a40 100644 --- a/src/vs/workbench/services/history/browser/history.ts +++ b/src/vs/workbench/services/history/browser/history.ts @@ -71,7 +71,8 @@ export class EditorState { } interface ISerializedFileHistoryEntry { - resource: any | string; // TODO@Ben migration + resource?: string; + resourceJSON: any; } export abstract class BaseHistoryService { @@ -708,7 +709,7 @@ export class HistoryService extends BaseHistoryService implements IHistoryServic return void 0; // only file resource inputs are serializable currently } - return { resource: (input as IResourceInput).resource.toJSON() }; + return { resourceJSON: (input as IResourceInput).resource.toJSON() }; }).filter(serialized => !!serialized); this.storageService.store(HistoryService.STORAGE_KEY, JSON.stringify(entries), StorageScope.WORKSPACE); @@ -724,8 +725,8 @@ export class HistoryService extends BaseHistoryService implements IHistoryServic this.history = entries.map(entry => { const serializedFileInput = entry as ISerializedFileHistoryEntry; - if (serializedFileInput.resource) { - return { resource: typeof serializedFileInput.resource === 'string' ? URI.parse(serializedFileInput.resource) : URI.revive(serializedFileInput.resource) } as IResourceInput; + if (serializedFileInput.resource || serializedFileInput.resourceJSON) { + return { resource: !!serializedFileInput.resourceJSON ? URI.revive(serializedFileInput.resourceJSON) : URI.parse(serializedFileInput.resource) } as IResourceInput; } return void 0; diff --git a/src/vs/workbench/services/themes/common/themeService.ts b/src/vs/workbench/services/themes/common/themeService.ts index 27c417cbceb..0ea894b9d09 100644 --- a/src/vs/workbench/services/themes/common/themeService.ts +++ b/src/vs/workbench/services/themes/common/themeService.ts @@ -48,4 +48,5 @@ export interface IThemeSetting { export interface IThemeSettingStyle { foreground?: string; + background?: string; } \ No newline at end of file diff --git a/src/vs/workbench/services/themes/electron-browser/themeService.ts b/src/vs/workbench/services/themes/electron-browser/themeService.ts index 423f92fa1b5..0e76f4f994d 100644 --- a/src/vs/workbench/services/themes/electron-browser/themeService.ts +++ b/src/vs/workbench/services/themes/electron-browser/themeService.ts @@ -170,7 +170,24 @@ export class ThemeService implements IThemeService { @ITelemetryService private telemetryService: ITelemetryService) { this.knownColorThemes = []; - this.currentColorThemeDocument = null; + + // In order to avoid paint flashing for tokens, because + // themes are loaded asynchronously, we need to initialize + // a color theme document with good defaults until the theme is loaded + let isLightTheme = (Array.prototype.indexOf.call(document.body.classList, 'vs') >= 0); + let foreground = isLightTheme ? '#000000' : '#D4D4D4'; + let background = isLightTheme ? '#ffffff' : '#1E1E1E'; + this.currentColorThemeDocument = { + name: null, + include: null, + settings: [{ + settings: { + foreground: foreground, + background: background + } + }] + }; + this.onColorThemeChange = new Emitter(); this.knownIconThemes = []; this.currentIconTheme = ''; diff --git a/src/vs/workbench/test/common/editor/rangeDecorations.test.ts b/src/vs/workbench/test/common/editor/rangeDecorations.test.ts index bde6f79405e..eee0aa2c901 100644 --- a/src/vs/workbench/test/common/editor/rangeDecorations.test.ts +++ b/src/vs/workbench/test/common/editor/rangeDecorations.test.ts @@ -151,7 +151,7 @@ suite('Editor - Range decorations', () => { } function mockEditorService(editorInput: IEditorInput); - function mockEditorService(resource: URI) + function mockEditorService(resource: URI); function mockEditorService(arg: any) { let editorInput: IEditorInput = arg instanceof URI ? instantiationService.createInstance(FileEditorInput, arg, void 0) : arg; instantiationService.stub(WorkbenchEditorService.IWorkbenchEditorService, 'getActiveEditorInput', editorInput); diff --git a/src/vs/workbench/test/node/api/extHostLanguageFeatures.test.ts b/src/vs/workbench/test/node/api/extHostLanguageFeatures.test.ts index de736d04d87..a915bf3b3c4 100644 --- a/src/vs/workbench/test/node/api/extHostLanguageFeatures.test.ts +++ b/src/vs/workbench/test/node/api/extHostLanguageFeatures.test.ts @@ -27,7 +27,7 @@ import { ExtHostDocuments } from 'vs/workbench/api/node/extHostDocuments'; import { getDocumentSymbols } from 'vs/editor/contrib/quickOpen/common/quickOpen'; import { DocumentSymbolProviderRegistry, DocumentHighlightKind } from 'vs/editor/common/modes'; import { getCodeLensData } from 'vs/editor/contrib/codelens/common/codelens'; -import { getDeclarationsAtPosition } from 'vs/editor/contrib/goToDeclaration/common/goToDeclaration'; +import { getDeclarationsAtPosition, getTypeDefinitionAtPosition } from 'vs/editor/contrib/goToDeclaration/common/goToDeclaration'; import { getHover } from 'vs/editor/contrib/hover/common/hover'; import { getOccurrencesAtPosition } from 'vs/editor/contrib/wordHighlighter/common/wordHighlighter'; import { provideReferences } from 'vs/editor/contrib/referenceSearch/common/referenceSearch'; @@ -351,6 +351,26 @@ suite('ExtHostLanguageFeatures', function () { }); }); + // --- type definition + + test('TypeDefinition, data conversion', function () { + + disposables.push(extHost.registerTypeDefinitionProvider(defaultSelector, { + provideTypeDefinition(): any { + return [new types.Location(model.uri, new types.Range(1, 2, 3, 4))]; + } + })); + + return threadService.sync().then(() => { + return getTypeDefinitionAtPosition(model, new EditorPosition(1, 1)).then(value => { + assert.equal(value.length, 1); + let [entry] = value; + assert.deepEqual(entry.range, { startLineNumber: 2, startColumn: 3, endLineNumber: 4, endColumn: 5 }); + assert.equal(entry.uri.toString(), model.uri.toString()); + }); + }); + }); + // --- extra info test('HoverProvider, word range at pos', function () {