diff --git a/.travis.yml b/.travis.yml index 6bedf740e15..02d0fb00dcc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -42,8 +42,8 @@ script: - gulp electron --silent - gulp compile --silent - gulp optimize-vscode --silent - - if [[ "$TRAVIS_OS_NAME" == "linux_off" ]]; then ./scripts/test.sh --reporter dot --coverage; else ./scripts/test.sh --reporter dot; fi + - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then ./scripts/test.sh --reporter dot --coverage; else ./scripts/test.sh --reporter dot; fi - ./scripts/test-integration.sh after_success: - - if [[ "$TRAVIS_OS_NAME" == "linux_off" ]]; then node_modules/.bin/coveralls < .build/coverage/lcov.info; fi \ No newline at end of file + - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then node_modules/.bin/coveralls < .build/coverage/lcov.info; fi \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 34475138c36..0179fbe0cf5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,7 +5,8 @@ "files.exclude": { ".git": true, ".build": true, - "**/.DS_Store": true + "**/.DS_Store": true, + "build/**/*.js": { "when": "$(basename).ts" } }, "search.exclude": { "**/node_modules": true, diff --git a/LICENSE.txt b/LICENSE.txt index 3615b7decc1..9afc63d4a1d 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,17 +1,23 @@ -Copyright (c) Microsoft Corporation +MIT License + +Copyright (c) 2015 - present Microsoft Corporation All rights reserved. -MIT License +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, -modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS -BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT -OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/build/gulpfile.hygiene.js b/build/gulpfile.hygiene.js index 0ca8ea54b9d..f53a1c1bd26 100644 --- a/build/gulpfile.hygiene.js +++ b/build/gulpfile.hygiene.js @@ -229,6 +229,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.vscode.js b/build/gulpfile.vscode.js index 575635e629d..ab0267d9142 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -30,7 +30,7 @@ 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(['vsda' /* vsda can come in from distro build, do not remove */]); const baseModules = Object.keys(process.binding('natives')).filter(n => !/^_|\//.test(n)); const nodeModules = ['electron', 'original-fs'] .concat(dependencies) @@ -39,8 +39,8 @@ const nodeModules = ['electron', 'original-fs'] // Build const builtInExtensions = [ - { name: 'ms-vscode.node-debug', version: '1.9.2' }, - { name: 'ms-vscode.node-debug2', version: '1.9.3' } + { name: 'ms-vscode.node-debug', version: '1.9.6' }, + { name: 'ms-vscode.node-debug2', version: '1.9.4' } ]; const vscodeEntryPoints = _.flatten([ @@ -93,11 +93,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')); @@ -269,7 +269,13 @@ function packageTask(platform, arch, opts) { .pipe(util.cleanNodeModule('native-keymap', ['binding.gyp', 'build/**', 'src/**', 'deps/**'], ['**/*.node'])) .pipe(util.cleanNodeModule('windows-foreground-love', ['binding.gyp', 'build/**', 'src/**'], ['**/*.node'])) .pipe(util.cleanNodeModule('gc-signals', ['binding.gyp', 'build/**', 'src/**', 'deps/**'], ['**/*.node', 'src/index.js'])) - .pipe(util.cleanNodeModule('node-pty', ['binding.gyp', 'build/**', 'src/**', 'deps/**'], ['build/Release/**'])); + .pipe(util.cleanNodeModule('node-pty', ['binding.gyp', 'build/**', 'src/**', 'deps/**'], ['build/Release/**'])) + // vsda can come in from distro build, do not remove + .pipe(util.cleanNodeModule('vsda', ['**'], [(function () { + if (process.platform === 'win32') { return 'build/Release/vsda_win32.node'; } + if (process.platform === 'darwin') { return 'build/Release/vsda_darwin.node'; } + if (process.platform === 'linux') { return process.arch === 'x64' ? 'build/Release/vsda_linux64.node' : 'build/Release/vsda_linux32.node'; } + })(), 'index.js'])); let all = es.merge( packageJsonStream, 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/npm-shrinkwrap.json b/extensions/typescript/npm-shrinkwrap.json index f68512bf919..cc50e3da5e7 100644 --- a/extensions/typescript/npm-shrinkwrap.json +++ b/extensions/typescript/npm-shrinkwrap.json @@ -13,9 +13,9 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz" }, "typescript": { - "version": "2.1.5-insiders.20161229", - "from": "typescript@2.1.5-insiders.20161229", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.1.5-insiders.20161229.tgz" + "version": "2.1.5", + "from": "typescript@2.1.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.1.5.tgz" }, "vscode-extension-telemetry": { "version": "0.0.5", diff --git a/extensions/typescript/package.json b/extensions/typescript/package.json index d4d56aba5fb..a74697b8026 100644 --- a/extensions/typescript/package.json +++ b/extensions/typescript/package.json @@ -14,7 +14,7 @@ "semver": "4.3.6", "vscode-extension-telemetry": "^0.0.5", "vscode-nls": "^2.0.1", - "typescript": "2.1.5-insiders.20161229" + "typescript": "2.1.5" }, "devDependencies": { "@types/semver": "^5.3.30" @@ -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 da7a9c55919..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,16 +123,21 @@ 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: - const identifierMatch = new RegExp(`^(.*?(\\b|\\W))${item.text}`, 'gm'); + const identifierMatch = new RegExp(`^(.*?(\\b|\\W))${item.text}\\b`, 'gm'); const match = identifierMatch.exec(text); const start = match ? match.index + match[1].length : 0; results.push(new Range( 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/issue_template.md b/issue_template.md index 3b8bc8506cf..81a8470fc44 100644 --- a/issue_template.md +++ b/issue_template.md @@ -1,7 +1,9 @@ + + - VSCode Version: - OS Version: Steps to Reproduce: -1. -2. +1. +2. 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 208cbfa69b6..2ccabda0404 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": "47280b25a68a86e8edb9ce69c649ba529136bcaf", "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", @@ -91,7 +91,7 @@ "sinon": "^1.17.2", "source-map": "^0.4.4", "tslint": "^4.3.1", - "typescript": "^2.1.4", + "typescript": "2.1.5", "typescript-formatter": "4.0.1", "uglify-js": "2.4.8", "underscore": "^1.8.2", 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/base/common/uri.ts b/src/vs/base/common/uri.ts index c8b2c628d49..9b4a3e07890 100644 --- a/src/vs/base/common/uri.ts +++ b/src/vs/base/common/uri.ts @@ -378,25 +378,42 @@ export default class URI { } public toJSON(): any { - return { - scheme: this.scheme, - authority: this.authority, - path: this.path, + const res = { fsPath: this.fsPath, - query: this.query, - fragment: this.fragment, external: this.toString(), $mid: 1 }; + + if (this.path) { + res.path = this.path; + } + + if (this.scheme) { + res.scheme = this.scheme; + } + + if (this.authority) { + res.authority = this.authority; + } + + if (this.query) { + res.query = this.query; + } + + if (this.fragment) { + res.fragment = this.fragment; + } + + return res; } static revive(data: any): URI { let result = new URI(); - result._scheme = (data).scheme; - result._authority = (data).authority; - result._path = (data).path; - result._query = (data).query; - result._fragment = (data).fragment; + result._scheme = (data).scheme || URI._empty; + result._authority = (data).authority || URI._empty; + result._path = (data).path || URI._empty; + result._query = (data).query || URI._empty; + result._fragment = (data).fragment || URI._empty; result._fsPath = (data).fsPath; result._formatted = (data).external; URI._validate(result); 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 d9313b22935..c1fefa4ce52 100644 --- a/src/vs/editor/browser/controller/mouseTarget.ts +++ b/src/vs/editor/browser/controller/mouseTarget.ts @@ -10,10 +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; @@ -67,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; @@ -131,473 +132,80 @@ class MouseTarget implements IMouseTarget { } } +class ElementPath { -// e.g. of paths: -// - overflow-guard/monaco-scrollable-element editor-scrollable vs/lines-content/view-lines/view-line -// - overflow-guard/monaco-scrollable-element editor-scrollable vs/lines-content/view-lines/view-line/token comment js -// etc. -let REGEX = (function () { - - function nodeWithClass(className: string): string { - return '[^/]*' + className + '[^/]*'; + public static isTextArea(path: Uint8Array): boolean { + return ( + path.length === 2 + && path[0] === PartFingerprint.OverflowGuard + && path[1] === PartFingerprint.TextArea + ); } - function anyNode(): string { - return '[^/]+'; + public static isChildOfViewLines(path: Uint8Array): boolean { + return ( + path.length >= 4 + && path[0] === PartFingerprint.OverflowGuard + && path[3] === PartFingerprint.ViewLines + ); } - let ANCHOR = '^' + ClassNames.OVERFLOW_GUARD + '\\/'; - - function createRegExp(...pieces: string[]): RegExp { - let forceEndMatch = false; - if (pieces[pieces.length - 1] === '$') { - forceEndMatch = true; - pieces.pop(); - } - return new RegExp(ANCHOR + pieces.join('\\/') + (forceEndMatch ? '$' : '')); + public static isChildOfScrollableElement(path: Uint8Array): boolean { + return ( + path.length >= 2 + && path[0] === PartFingerprint.OverflowGuard + && path[1] === PartFingerprint.ScrollableElement + ); } - return { - IS_TEXTAREA_COVER: createRegExp(nodeWithClass(ClassNames.TEXTAREA_COVER), '$'), - IS_TEXTAREA: createRegExp(ClassNames.TEXTAREA, '$'), - IS_VIEW_LINES: createRegExp(anyNode(), anyNode(), ClassNames.VIEW_LINES, '$'), - IS_CURSORS_LAYER: createRegExp(anyNode(), anyNode(), nodeWithClass(ClassNames.VIEW_CURSORS_LAYER), '$'), - IS_CHILD_OF_VIEW_LINES: createRegExp(anyNode(), anyNode(), ClassNames.VIEW_LINES), - IS_CHILD_OF_SCROLLABLE_ELEMENT: createRegExp(nodeWithClass(ClassNames.SCROLLABLE_ELEMENT)), - IS_CHILD_OF_CONTENT_WIDGETS: createRegExp(anyNode(), anyNode(), ClassNames.CONTENT_WIDGETS), - IS_CHILD_OF_OVERFLOWING_CONTENT_WIDGETS: new RegExp('^' + ClassNames.OVERFLOWING_CONTENT_WIDGETS + '\\/'), - IS_CHILD_OF_OVERLAY_WIDGETS: createRegExp(ClassNames.OVERLAY_WIDGETS), - IS_CHILD_OF_MARGIN: createRegExp(ClassNames.MARGIN), - IS_CHILD_OF_VIEW_ZONES: createRegExp(anyNode(), anyNode(), ClassNames.VIEW_ZONES), - }; -})(); + public static isChildOfContentWidgets(path: Uint8Array): boolean { + return ( + path.length >= 4 + && path[0] === PartFingerprint.OverflowGuard + && path[3] === PartFingerprint.ContentWidgets + ); + } -export class MouseTargetFactory { + public static isChildOfOverflowingContentWidgets(path: Uint8Array): boolean { + return ( + path.length >= 1 + && path[0] === PartFingerprint.OverflowingContentWidgets + ); + } - private _context: ViewContext; - private _viewHelper: IPointerHandlerHelper; + public static isChildOfOverlayWidgets(path: Uint8Array): boolean { + return ( + path.length >= 2 + && path[0] === PartFingerprint.OverflowGuard + && path[1] === PartFingerprint.OverlayWidgets + ); + } +} - constructor(context: ViewContext, viewHelper: IPointerHandlerHelper) { +class HitTestContext { + + public readonly model: IViewModel; + public readonly layoutInfo: EditorLayoutInfo; + public readonly viewDomNode: HTMLElement; + public readonly lineHeight: number; + public readonly typicalHalfwidthCharacterWidth: number; + public readonly lastViewCursorsRenderData: IViewCursorRenderData[]; + + 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; } - private getClassNamePathTo(child: Node, stopAt: Node): string { - let path: string[] = [], - className: string; - - while (child && child !== document.body) { - if (child === stopAt) { - break; - } - if (child.nodeType === child.ELEMENT_NODE) { - className = (child).className; - if (className) { - path.unshift(className); - } - } - child = child.parentNode; - } - - return path.join('/'); - } - - public mouseTargetIsWidget(e: EditorMouseEvent): boolean { - let t = e.target; - let path = this.getClassNamePathTo(t, this._viewHelper.viewDomNode); - - // Is it a content widget? - if (REGEX.IS_CHILD_OF_CONTENT_WIDGETS.test(path) || REGEX.IS_CHILD_OF_OVERFLOWING_CONTENT_WIDGETS.test(path)) { - return true; - } - - // Is it an overlay widget? - if (REGEX.IS_CHILD_OF_OVERLAY_WIDGETS.test(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); - - 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 = this.getClassNamePathTo(t, this._viewHelper.viewDomNode); - - // Is it a content widget? - if (REGEX.IS_CHILD_OF_CONTENT_WIDGETS.test(path) || REGEX.IS_CHILD_OF_OVERFLOWING_CONTENT_WIDGETS.test(path)) { - return this.createMouseTargetFromContentWidgetsChild(t, mouseColumn); - } - - // Is it an overlay widget? - if (REGEX.IS_CHILD_OF_OVERLAY_WIDGETS.test(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 (REGEX.IS_TEXTAREA_COVER.test(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 (REGEX.IS_TEXTAREA.test(path)) { - return new MouseTarget(t, MouseTargetType.TEXTAREA); - } - - // Is it a view zone? - if (REGEX.IS_CHILD_OF_VIEW_ZONES.test(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 (REGEX.IS_VIEW_LINES.test(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 || REGEX.IS_CHILD_OF_VIEW_LINES.test(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 = this.getClassNamePathTo(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 (REGEX.IS_CURSORS_LAYER.test(path)) { - return new MouseTarget(t, MouseTargetType.UNKNOWN); - } - - // Is it a child of the scrollable element? - if (REGEX.IS_CHILD_OF_SCROLLABLE_ELEMENT.test(path)) { - return this.createMouseTargetFromScrollbar(t, mouseVerticalOffset, mouseColumn); - } - - if (REGEX.IS_CHILD_OF_MARGIN.test(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 (/OverviewRuler/i.test(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); @@ -638,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(); @@ -657,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/editor.all.ts b/src/vs/editor/browser/editor.all.ts index 2cd0530de45..18a65b277a0 100644 --- a/src/vs/editor/browser/editor.all.ts +++ b/src/vs/editor/browser/editor.all.ts @@ -8,6 +8,8 @@ import 'vs/editor/browser/widget/codeEditorWidget'; import 'vs/editor/browser/widget/diffEditorWidget'; +import 'vs/editor/contrib/bracketMatching/common/bracketMatching'; +import 'vs/css!vs/editor/contrib/bracketMatching/browser/bracketMatching'; import 'vs/editor/contrib/clipboard/browser/clipboard'; import 'vs/editor/contrib/codelens/browser/codelens'; import 'vs/editor/contrib/comment/common/comment'; @@ -31,7 +33,6 @@ import 'vs/editor/contrib/quickFix/browser/quickFix'; import 'vs/editor/contrib/referenceSearch/browser/referenceSearch'; import 'vs/editor/contrib/rename/browser/rename'; import 'vs/editor/contrib/smartSelect/common/smartSelect'; -import 'vs/editor/contrib/smartSelect/common/jumpToBracket'; import 'vs/editor/contrib/snippet/common/snippet'; import 'vs/editor/contrib/snippet/browser/snippet'; import 'vs/editor/contrib/suggest/common/snippetCompletion'; 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 4633a53a146..2766ef2f051 100644 --- a/src/vs/editor/browser/view/viewImpl.ts +++ b/src/vs/editor/browser/view/viewImpl.ts @@ -42,7 +42,7 @@ import { ScrollDecorationViewPart } from 'vs/editor/browser/viewParts/scrollDeco import { SelectionsOverlay } from 'vs/editor/browser/viewParts/selections/selections'; import { ViewCursors } from 'vs/editor/browser/viewParts/viewCursors/viewCursors'; import { ViewZones } from 'vs/editor/browser/viewParts/viewZones/viewZones'; -import { ViewPart } from 'vs/editor/browser/view/viewPart'; +import { ViewPart, PartFingerprint, PartFingerprints } from 'vs/editor/browser/view/viewPart'; import { ViewContext, IViewEventHandler } from 'vs/editor/common/view/viewContext'; import { IViewModel } from 'vs/editor/common/viewModel/viewModel'; import { ViewLinesViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; @@ -118,6 +118,7 @@ export class View extends ViewEventHandler implements editorBrowser.IView, IDisp this.domNode.className = configuration.editor.viewInfo.editorClassName; this.overflowGuardContainer = document.createElement('div'); + PartFingerprints.write(this.overflowGuardContainer, PartFingerprint.OverflowGuard); this.overflowGuardContainer.className = editorBrowser.ClassNames.OVERFLOW_GUARD; // The layout provider has such responsibilities as: @@ -174,6 +175,7 @@ export class View extends ViewEventHandler implements editorBrowser.IView, IDisp private createTextArea(): void { // Text Area (The focus will always be in the textarea when the cursor is blinking) this.textArea = document.createElement('textarea'); + PartFingerprints.write(this.textArea, PartFingerprint.TextArea); this.textArea.className = editorBrowser.ClassNames.TEXTAREA; this.textArea.setAttribute('wrap', 'off'); this.textArea.setAttribute('autocorrect', 'off'); @@ -606,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; diff --git a/src/vs/editor/browser/view/viewPart.ts b/src/vs/editor/browser/view/viewPart.ts index dee65b4ab53..8ca7825c45b 100644 --- a/src/vs/editor/browser/view/viewPart.ts +++ b/src/vs/editor/browser/view/viewPart.ts @@ -26,3 +26,49 @@ export abstract class ViewPart extends ViewEventHandler { public abstract prepareRender(ctx: IRenderingContext): void; public abstract render(ctx: IRestrictedRenderingContext): void; } + +export const enum PartFingerprint { + None, + ContentWidgets, + OverflowingContentWidgets, + OverflowGuard, + OverlayWidgets, + ScrollableElement, + TextArea, + ViewLines, +} + +export class PartFingerprints { + + public static write(target: Element, partId: PartFingerprint) { + target.setAttribute('data-mprt', String(partId)); + } + + public static read(target: Element): PartFingerprint { + let r = target.getAttribute('data-mprt'); + if (r === null) { + return PartFingerprint.None; + } + return parseInt(r, 10); + } + + public static collect(child: Element, stopAt: Element): Uint8Array { + let result: PartFingerprint[] = [], resultLen = 0; + + while (child && child !== document.body) { + if (child === stopAt) { + break; + } + if (child.nodeType === child.ELEMENT_NODE) { + result[resultLen++] = this.read(child); + } + child = child.parentElement; + } + + let r = new Uint8Array(resultLen); + for (let i = 0; i < resultLen; i++) { + r[i] = result[resultLen - i - 1]; + } + return r; + } +} diff --git a/src/vs/editor/browser/viewLayout/scrollManager.ts b/src/vs/editor/browser/viewLayout/scrollManager.ts index 741b8593846..cfa38268ad3 100644 --- a/src/vs/editor/browser/viewLayout/scrollManager.ts +++ b/src/vs/editor/browser/viewLayout/scrollManager.ts @@ -11,6 +11,7 @@ import { IOverviewRulerLayoutInfo, ScrollableElement } from 'vs/base/browser/ui/ import { EventType, IConfiguration, IConfigurationChangedEvent, IScrollEvent, INewScrollPosition } from 'vs/editor/common/editorCommon'; import { ClassNames } from 'vs/editor/browser/editorBrowser'; import { IViewEventBus } from 'vs/editor/common/view/viewContext'; +import { PartFingerprint, PartFingerprints } from 'vs/editor/browser/view/viewPart'; function addPropertyIfPresent(src: any, dst: any, prop: string): void { if (src.hasOwnProperty(prop)) { @@ -56,6 +57,8 @@ export class ScrollManager implements IDisposable { addPropertyIfPresent(configScrollbarOpts, scrollbarOptions, 'mouseWheelScrollSensitivity'); this.scrollbar = new ScrollableElement(linesContent, scrollbarOptions); + PartFingerprints.write(this.scrollbar.getDomNode(), PartFingerprint.ScrollableElement); + this.onLayoutInfoChanged(); this.toDispose.push(this.scrollbar); this.toDispose.push(this.scrollbar.onScroll((e: IScrollEvent) => { diff --git a/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts b/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts index a2a0d00ab5c..b45f04ae32b 100644 --- a/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts +++ b/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts @@ -9,7 +9,7 @@ import * as dom from 'vs/base/browser/dom'; import { StyleMutator } from 'vs/base/browser/styleMutator'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { ClassNames, ContentWidgetPositionPreference, IContentWidget } from 'vs/editor/browser/editorBrowser'; -import { ViewPart } from 'vs/editor/browser/view/viewPart'; +import { ViewPart, PartFingerprint, PartFingerprints } from 'vs/editor/browser/view/viewPart'; import { ViewContext } from 'vs/editor/common/view/viewContext'; import { IRenderingContext, IRestrictedRenderingContext } from 'vs/editor/common/view/renderingContext'; import { Position } from 'vs/editor/common/core/position'; @@ -66,11 +66,13 @@ export class ViewContentWidgets extends ViewPart { this._renderData = {}; this.domNode = document.createElement('div'); + PartFingerprints.write(this.domNode, PartFingerprint.ContentWidgets); this.domNode.className = ClassNames.CONTENT_WIDGETS; this.domNode.style.position = 'absolute'; this.domNode.style.top = '0'; this.overflowingContentWidgetsDomNode = document.createElement('div'); + PartFingerprints.write(this.overflowingContentWidgetsDomNode, PartFingerprint.OverflowingContentWidgets); this.overflowingContentWidgetsDomNode.className = ClassNames.OVERFLOWING_CONTENT_WIDGETS; } diff --git a/src/vs/editor/browser/viewParts/lines/viewLine.ts b/src/vs/editor/browser/viewParts/lines/viewLine.ts index 6be70ae1026..7f278afa2f0 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLine.ts @@ -7,15 +7,14 @@ import * as browser from 'vs/base/browser/browser'; import { FastDomNode, createFastDomNode } from 'vs/base/browser/styleMutator'; import { IConfigurationChangedEvent } from 'vs/editor/common/editorCommon'; -import { createLineParts, getColumnOfLinePartOffset } from 'vs/editor/common/viewLayout/viewLineParts'; -import { renderLine, RenderLineInput, RenderLineOutput } from 'vs/editor/common/viewLayout/viewLineRenderer'; +import { createLineParts } from 'vs/editor/common/viewLayout/viewLineParts'; +import { renderLine, RenderLineInput, RenderLineOutput, CharacterMapping } from 'vs/editor/common/viewLayout/viewLineRenderer'; import { ClassNames } from 'vs/editor/browser/editorBrowser'; import { IVisibleLineData } from 'vs/editor/browser/view/viewLayer'; import { RangeUtil } from 'vs/editor/browser/viewParts/lines/rangeUtil'; import { ViewContext } from 'vs/editor/common/view/viewContext'; import { HorizontalRange } from 'vs/editor/common/view/renderingContext'; import { InlineDecoration } from 'vs/editor/common/viewModel/viewModel'; -import { LineParts } from 'vs/editor/common/core/lineParts'; export class ViewLine implements IVisibleLineData { @@ -175,8 +174,7 @@ class RenderedViewLine { public readonly input: RenderLineInput; public readonly html: string; - protected readonly _charOffsetInPart: number[]; - private readonly _lastRenderedPartIndex: number; + protected readonly _characterMapping: CharacterMapping; private readonly _isWhitespaceOnly: boolean; private _cachedWidth: number; @@ -189,8 +187,7 @@ class RenderedViewLine { this.domNode = domNode; this.input = renderLineInput; this.html = renderLineOutput.output; - this._charOffsetInPart = renderLineOutput.charOffsetInPart; - this._lastRenderedPartIndex = renderLineOutput.lastRenderedPartIndex; + this._characterMapping = renderLineOutput.characterMapping; this._isWhitespaceOnly = renderLineOutput.isWhitespaceOnly; this._cachedWidth = -1; @@ -291,18 +288,19 @@ class RenderedViewLine { private _actualReadPixelOffset(column: number, clientRectDeltaLeft: number, endNode: HTMLElement): number { - if (this._charOffsetInPart.length === 0) { + if (this._characterMapping.length === 0) { // This line is empty return 0; } - if (column === this._charOffsetInPart.length && this._isWhitespaceOnly) { + if (column === this._characterMapping.length && this._isWhitespaceOnly) { // This branch helps in the case of whitespace only lines which have a width set return this.getWidth(); } - let partIndex = findIndexInArrayWithMax(this.input.lineParts, column - 1, this._lastRenderedPartIndex); - let charOffsetInPart = this._charOffsetInPart[column - 1]; + let partData = this._characterMapping.charOffsetToPartData(column - 1); + let partIndex = CharacterMapping.getPartIndex(partData); + let charOffsetInPart = CharacterMapping.getCharIndex(partData); let r = RangeUtil.readHorizontalRanges(this._getReadingTarget(), partIndex, charOffsetInPart, partIndex, charOffsetInPart, clientRectDeltaLeft, endNode); if (!r || r.length === 0) { @@ -313,16 +311,19 @@ class RenderedViewLine { private _readRawVisibleRangesForRange(startColumn: number, endColumn: number, clientRectDeltaLeft: number, endNode: HTMLElement): HorizontalRange[] { - if (startColumn === 1 && endColumn === this._charOffsetInPart.length) { + if (startColumn === 1 && endColumn === this._characterMapping.length) { // This branch helps IE with bidi text & gives a performance boost to other browsers when reading visible ranges for an entire line return [new HorizontalRange(0, this.getWidth())]; } - let startPartIndex = findIndexInArrayWithMax(this.input.lineParts, startColumn - 1, this._lastRenderedPartIndex); - let startCharOffsetInPart = this._charOffsetInPart[startColumn - 1]; - let endPartIndex = findIndexInArrayWithMax(this.input.lineParts, endColumn - 1, this._lastRenderedPartIndex); - let endCharOffsetInPart = this._charOffsetInPart[endColumn - 1]; + let startPartData = this._characterMapping.charOffsetToPartData(startColumn - 1); + let startPartIndex = CharacterMapping.getPartIndex(startPartData); + let startCharOffsetInPart = CharacterMapping.getCharIndex(startPartData); + + let endPartData = this._characterMapping.charOffsetToPartData(endColumn - 1); + let endPartIndex = CharacterMapping.getPartIndex(endPartData); + let endCharOffsetInPart = CharacterMapping.getCharIndex(endPartData); return RangeUtil.readHorizontalRanges(this._getReadingTarget(), startPartIndex, startCharOffsetInPart, endPartIndex, endCharOffsetInPart, clientRectDeltaLeft, endNode); } @@ -338,17 +339,9 @@ class RenderedViewLine { spanNode = spanNode.previousSibling; spanIndex++; } - let lineParts = this.input.lineParts.parts; - return getColumnOfLinePartOffset( - this.input.stopRenderingLineAfter, - lineParts, - this.input.lineParts.maxLineColumn, - this._charOffsetInPart, - spanIndex, - spanNodeTextContentLength, - offset - ); + let charOffset = this._characterMapping.partDataToCharOffset(spanIndex, spanNodeTextContentLength, offset); + return charOffset + 1; } } @@ -356,7 +349,7 @@ class WebKitRenderedViewLine extends RenderedViewLine { protected _readVisibleRangesForRange(startColumn: number, endColumn: number, clientRectDeltaLeft: number, endNode: HTMLElement): HorizontalRange[] { let output = super._readVisibleRangesForRange(startColumn, endColumn, clientRectDeltaLeft, endNode); - if (!output || output.length === 0 || startColumn === endColumn || (startColumn === 1 && endColumn === this._charOffsetInPart.length)) { + if (!output || output.length === 0 || startColumn === endColumn || (startColumn === 1 && endColumn === this._characterMapping.length)) { return output; } @@ -383,11 +376,6 @@ class WebKitRenderedViewLine extends RenderedViewLine { } } -function findIndexInArrayWithMax(lineParts: LineParts, desiredIndex: number, maxResult: number): number { - let r = lineParts.findIndexOfOffset(desiredIndex); - return r <= maxResult ? r : maxResult; -} - const createRenderedLine: (domNode: FastDomNode, renderLineInput: RenderLineInput, modelContainsRTL: boolean, renderLineOutput: RenderLineOutput) => RenderedViewLine = (function () { if (browser.isWebKit) { return createWebKitRenderedLine; diff --git a/src/vs/editor/browser/viewParts/lines/viewLines.ts b/src/vs/editor/browser/viewParts/lines/viewLines.ts index d64d0fc08c7..75a7a789f5b 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLines.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLines.ts @@ -18,6 +18,7 @@ import { ViewContext } from 'vs/editor/common/view/viewContext'; import { ViewLinesViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; import { VisibleRange, LineVisibleRanges } from 'vs/editor/common/view/renderingContext'; import { ILayoutProvider } from 'vs/editor/browser/viewLayout/layoutProvider'; +import { PartFingerprint, PartFingerprints } from 'vs/editor/browser/view/viewPart'; class LastRenderedData { @@ -80,6 +81,8 @@ export class ViewLines extends ViewLayer { this._revealHorizontalRightPadding = this._context.configuration.editor.viewInfo.revealHorizontalRightPadding; this._canUseTranslate3d = context.configuration.editor.viewInfo.canUseTranslate3d; this._layoutProvider = layoutProvider; + + PartFingerprints.write(this.domNode.domNode, PartFingerprint.ViewLines); this.domNode.setClassName(ClassNames.VIEW_LINES); Configuration.applyFontInfo(this.domNode, this._context.configuration.editor.fontInfo); diff --git a/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts b/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts index d07217245b5..9537b73a257 100644 --- a/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts +++ b/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts @@ -9,7 +9,7 @@ import 'vs/css!./overlayWidgets'; import { StyleMutator } from 'vs/base/browser/styleMutator'; import { EditorLayoutInfo } from 'vs/editor/common/editorCommon'; import { ClassNames, IOverlayWidget, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; -import { ViewPart } from 'vs/editor/browser/view/viewPart'; +import { ViewPart, PartFingerprint, PartFingerprints } from 'vs/editor/browser/view/viewPart'; import { ViewContext } from 'vs/editor/common/view/viewContext'; import { IRenderingContext, IRestrictedRenderingContext } from 'vs/editor/common/view/renderingContext'; @@ -42,6 +42,7 @@ export class ViewOverlayWidgets extends ViewPart { this._editorWidth = 0; this.domNode = document.createElement('div'); + PartFingerprints.write(this.domNode, PartFingerprint.OverlayWidgets); this.domNode.className = ClassNames.OVERLAY_WIDGETS; } diff --git a/src/vs/editor/browser/viewParts/overviewRuler/overviewRulerImpl.ts b/src/vs/editor/browser/viewParts/overviewRuler/overviewRulerImpl.ts index 1020e203a99..888e5be0164 100644 --- a/src/vs/editor/browser/viewParts/overviewRuler/overviewRulerImpl.ts +++ b/src/vs/editor/browser/viewParts/overviewRuler/overviewRulerImpl.ts @@ -24,6 +24,7 @@ export class OverviewRulerImpl { this._canvasLeftOffset = canvasLeftOffset; this._domNode = document.createElement('canvas'); + this._domNode.className = cssClassName; this._domNode.style.position = 'absolute'; 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/editor.css b/src/vs/editor/browser/widget/media/editor.css index b812108a658..b665708cea7 100644 --- a/src/vs/editor/browser/widget/media/editor.css +++ b/src/vs/editor/browser/widget/media/editor.css @@ -129,14 +129,3 @@ .monaco-editor.vs .greensquiggly, .monaco-editor.vs-dark .greensquiggly { background: url("green-squiggly.svg") repeat-x bottom left; } .monaco-editor.hc-black .greensquiggly { border-bottom: 4px double #71B771; opacity: 0.8; } - -/* -------------------- Bracket Match -------------------- */ - -.monaco-editor .bracket-match { - box-sizing: border-box; - background-color: rgba(0, 100, 0, 0.1); -} -.monaco-editor.vs .bracket-match { border: 1px solid #B9B9B9; } -.monaco-editor.vs-dark .bracket-match { border: 1px solid #888; } -.monaco-editor.hc-black .bracket-match { border: 1px solid #fff; } - 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/commonCodeEditor.ts b/src/vs/editor/common/commonCodeEditor.ts index c67207a4b8f..3bdb7b417d2 100644 --- a/src/vs/editor/common/commonCodeEditor.ts +++ b/src/vs/editor/common/commonCodeEditor.ts @@ -799,7 +799,6 @@ export abstract class CommonCodeEditor extends EventEmitter implements editorCom }; this.cursor = new Cursor( - this.id, this._configuration, this.model, viewModelHelper, diff --git a/src/vs/editor/common/controller/cursor.ts b/src/vs/editor/common/controller/cursor.ts index 5d75593dbc9..0f0b58842d9 100644 --- a/src/vs/editor/common/controller/cursor.ts +++ b/src/vs/editor/common/controller/cursor.ts @@ -69,7 +69,6 @@ interface ICommandsData { export class Cursor extends EventEmitter { - private editorId: number; private configuration: editorCommon.IConfiguration; private model: editorCommon.IModel; @@ -87,19 +86,18 @@ export class Cursor extends EventEmitter { [key: string]: (ctx: IMultipleCursorOperationContext) => boolean; }; - constructor(editorId: number, configuration: editorCommon.IConfiguration, model: editorCommon.IModel, viewModelHelper: IViewModelHelper, enableEmptySelectionClipboard: boolean) { + constructor(configuration: editorCommon.IConfiguration, model: editorCommon.IModel, viewModelHelper: IViewModelHelper, enableEmptySelectionClipboard: boolean) { super([ editorCommon.EventType.CursorPositionChanged, editorCommon.EventType.CursorSelectionChanged, editorCommon.EventType.CursorRevealRange, editorCommon.EventType.CursorScrollRequest ]); - this.editorId = editorId; this.configuration = configuration; this.model = model; this.viewModelHelper = viewModelHelper; this.enableEmptySelectionClipboard = enableEmptySelectionClipboard; - this.cursors = new CursorCollection(this.editorId, this.model, this.configuration, this.viewModelHelper); + this.cursors = new CursorCollection(this.model, this.configuration, this.viewModelHelper); this.cursorUndoStack = []; this._isHandling = false; @@ -207,7 +205,7 @@ export class Cursor extends EventEmitter { // a model.setValue() was called this.cursors.dispose(); - this.cursors = new CursorCollection(this.editorId, this.model, this.configuration, this.viewModelHelper); + this.cursors = new CursorCollection(this.model, this.configuration, this.viewModelHelper); this.emitCursorPositionChanged('model', editorCommon.CursorChangeReason.ContentFlush); this.emitCursorSelectionChanged('model', editorCommon.CursorChangeReason.ContentFlush); @@ -852,8 +850,6 @@ export class Cursor extends EventEmitter { private _registerHandlers(): void { let H = editorCommon.Handler; - this._handlers[H.JumpToBracket] = (ctx) => this._jumpToBracket(ctx); - this._handlers[H.CursorMove] = (ctx) => this._cursorMove(ctx); this._handlers[H.MoveTo] = (ctx) => this._moveTo(false, ctx); this._handlers[H.MoveToSelect] = (ctx) => this._moveTo(true, ctx); @@ -1025,11 +1021,6 @@ export class Cursor extends EventEmitter { return result; } - private _jumpToBracket(ctx: IMultipleCursorOperationContext): boolean { - this.cursors.killSecondaryCursors(); - return this._invokeForAll(ctx, (cursorIndex: number, oneCursor: OneCursor, oneCtx: IOneCursorOperationContext) => OneCursorOp.jumpToBracket(oneCursor, oneCtx)); - } - private _moveTo(inSelectionMode: boolean, ctx: IMultipleCursorOperationContext): boolean { this.cursors.killSecondaryCursors(); return this._invokeForAll(ctx, (cursorIndex: number, oneCursor: OneCursor, oneCtx: IOneCursorOperationContext) => OneCursorOp.moveTo(oneCursor, inSelectionMode, ctx.eventData.position, ctx.eventData.viewPosition, ctx.eventSource, oneCtx)); diff --git a/src/vs/editor/common/controller/cursorCollection.ts b/src/vs/editor/common/controller/cursorCollection.ts index 736dd0bba15..55e1f13b37c 100644 --- a/src/vs/editor/common/controller/cursorCollection.ts +++ b/src/vs/editor/common/controller/cursorCollection.ts @@ -19,7 +19,6 @@ export interface ICursorCollectionState { export class CursorCollection { - private editorId: number; private model: IModel; private configuration: IConfiguration; private modeConfiguration: IModeConfiguration; @@ -32,14 +31,13 @@ export class CursorCollection { private viewModelHelper: IViewModelHelper; - constructor(editorId: number, model: IModel, configuration: IConfiguration, viewModelHelper: IViewModelHelper) { - this.editorId = editorId; + constructor(model: IModel, configuration: IConfiguration, viewModelHelper: IViewModelHelper) { this.model = model; this.configuration = configuration; this.viewModelHelper = viewModelHelper; this.modeConfiguration = this.getModeConfiguration(); - this.primaryCursor = new OneCursor(this.editorId, this.model, this.configuration, this.modeConfiguration, this.viewModelHelper); + this.primaryCursor = new OneCursor(this.model, this.configuration, this.modeConfiguration, this.viewModelHelper); this.secondaryCursors = []; this.lastAddedCursorIndex = 0; } @@ -158,15 +156,10 @@ export class CursorCollection { public normalize(): void { this._mergeCursorsIfNecessary(); - - this.primaryCursor.adjustBracketDecorations(); - for (var i = 0, len = this.secondaryCursors.length; i < len; i++) { - this.secondaryCursors[i].adjustBracketDecorations(); - } } public addSecondaryCursor(selection: ISelection): void { - var newCursor = new OneCursor(this.editorId, this.model, this.configuration, this.modeConfiguration, this.viewModelHelper); + var newCursor = new OneCursor(this.model, this.configuration, this.modeConfiguration, this.viewModelHelper); if (selection) { newCursor.setSelection(selection); } diff --git a/src/vs/editor/common/controller/oneCursor.ts b/src/vs/editor/common/controller/oneCursor.ts index 68f6a16de0d..3df8a61504e 100644 --- a/src/vs/editor/common/controller/oneCursor.ts +++ b/src/vs/editor/common/controller/oneCursor.ts @@ -107,7 +107,6 @@ export class MoveOperationResult { export class OneCursor implements IOneCursor { // --- contextual state - private readonly editorId: number; public readonly model: editorCommon.IModel; public readonly viewModel: ICursorSimpleModel; private readonly configuration: editorCommon.IConfiguration; @@ -122,21 +121,16 @@ export class OneCursor implements IOneCursor { public modelState: SingleCursorState; public viewState: SingleCursorState; - // --- bracket match decorations - private bracketDecorations: string[]; - // --- computed properties private _selStartMarker: string; private _selEndMarker: string; constructor( - editorId: number, model: editorCommon.IModel, configuration: editorCommon.IConfiguration, modeConfiguration: IModeConfiguration, viewModelHelper: IViewModelHelper ) { - this.editorId = editorId; this.model = model; this.configuration = configuration; this.modeConfiguration = modeConfiguration; @@ -153,8 +147,6 @@ export class OneCursor implements IOneCursor { } }); - this.bracketDecorations = []; - this._setState( new SingleCursorState(new Range(1, 1, 1, 1), 0, new Position(1, 1), 0), new SingleCursorState(new Range(1, 1, 1, 1), 0, new Position(1, 1), 0), @@ -270,7 +262,7 @@ export class OneCursor implements IOneCursor { } public duplicate(): OneCursor { - let result = new OneCursor(this.editorId, this.model, this.configuration, this.modeConfiguration, this.viewModelHelper); + let result = new OneCursor(this.model, this.configuration, this.modeConfiguration, this.viewModelHelper); result._setState( this.modelState, this.viewState, @@ -284,31 +276,8 @@ export class OneCursor implements IOneCursor { this._configChangeListener.dispose(); this.model._removeMarker(this._selStartMarker); this.model._removeMarker(this._selEndMarker); - this.bracketDecorations = this.model.deltaDecorations(this.bracketDecorations, [], this.editorId); } - public adjustBracketDecorations(): void { - let bracketMatch: [Range, Range] = null; - let selection = this.modelState.selection; - if (selection.isEmpty()) { - bracketMatch = this.model.matchBracket(this.modelState.position); - } - - let newDecorations: editorCommon.IModelDeltaDecoration[] = []; - if (bracketMatch) { - let options: editorCommon.IModelDecorationOptions = { - stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, - className: 'bracket-match' - }; - newDecorations.push({ range: bracketMatch[0], options: options }); - newDecorations.push({ range: bracketMatch[1], options: options }); - } - - this.bracketDecorations = this.model.deltaDecorations(this.bracketDecorations, newDecorations, this.editorId); - } - - - public setSelection(selection: editorCommon.ISelection, viewSelection: editorCommon.ISelection = null): void { let position = this.model.validatePosition({ lineNumber: selection.positionLineNumber, @@ -414,9 +383,6 @@ export class OneCursor implements IOneCursor { // -------------------- START reading API - public getBracketsDecorations(): string[] { - return this.bracketDecorations; - } public setSelectionStartLeftoverVisibleColumns(value: number): void { this._setState( this.modelState.withSelectionStartLeftoverVisibleColumns(value), @@ -531,30 +497,6 @@ export class OneCursor implements IOneCursor { export class OneCursorOp { // -------------------- START handlers that simply change cursor state - public static jumpToBracket(cursor: OneCursor, ctx: IOneCursorOperationContext): boolean { - let bracketDecorations = cursor.getBracketsDecorations(); - - if (bracketDecorations.length !== 2) { - return false; - } - - let firstBracket = cursor.model.getDecorationRange(bracketDecorations[0]); - let secondBracket = cursor.model.getDecorationRange(bracketDecorations[1]); - - let position = cursor.modelState.position; - - if (Utils.isPositionAtRangeEdges(position, firstBracket) || Utils.isPositionInsideRange(position, firstBracket)) { - cursor.moveModelPosition(false, secondBracket.endLineNumber, secondBracket.endColumn, 0, false); - return true; - } - - if (Utils.isPositionAtRangeEdges(position, secondBracket) || Utils.isPositionInsideRange(position, secondBracket)) { - cursor.moveModelPosition(false, firstBracket.endLineNumber, firstBracket.endColumn, 0, false); - return true; - } - - return false; - } public static moveTo(cursor: OneCursor, inSelectionMode: boolean, position: editorCommon.IPosition, viewPosition: editorCommon.IPosition, eventSource: string, ctx: IOneCursorOperationContext): boolean { let validatedPosition = cursor.model.validatePosition(position); @@ -925,36 +867,3 @@ export class OneCursorOp { // -------------------- STOP handlers that simply change cursor state } - -class Utils { - - /** - * Tests if position is contained inside range. - * If position is either the starting or ending of a range, false is returned. - */ - static isPositionInsideRange(position: Position, range: Range): boolean { - if (position.lineNumber < range.startLineNumber) { - return false; - } - if (position.lineNumber > range.endLineNumber) { - return false; - } - if (position.lineNumber === range.startLineNumber && position.column < range.startColumn) { - return false; - } - if (position.lineNumber === range.endLineNumber && position.column > range.endColumn) { - return false; - } - return true; - } - - static isPositionAtRangeEdges(position: Position, range: Range): boolean { - if (position.lineNumber === range.startLineNumber && position.column === range.startColumn) { - return true; - } - if (position.lineNumber === range.endLineNumber && position.column === range.endColumn) { - return true; - } - return false; - } -} diff --git a/src/vs/editor/common/core/arrays.ts b/src/vs/editor/common/core/arrays.ts deleted file mode 100644 index 6118b3b6900..00000000000 --- a/src/vs/editor/common/core/arrays.ts +++ /dev/null @@ -1,43 +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'; - -export namespace Arrays { - - /** - * Given a sorted array of natural number segments, find the segment containing a natural number. - * For example, the segments [0, 5), [5, 9), [9, infinity) will be represented in the following manner: - * [{ startIndex: 0 }, { startIndex: 5 }, { startIndex: 9 }] - * Searching for 0, 1, 2, 3 or 4 will return 0. - * Searching for 5, 6, 7 or 8 will return 1. - * Searching for 9, 10, 11, ... will return 2. - * @param arr A sorted array representing natural number segments - * @param desiredIndex The search - * @return The index of the containing segment in the array. - */ - export function findIndexInSegmentsArray(arr: { readonly startIndex: number; }[], desiredIndex: number): number { - - let low = 0; - let high = arr.length - 1; - - if (high <= 0) { - return 0; - } - - while (low < high) { - - let mid = low + Math.ceil((high - low) / 2); - - if (arr[mid].startIndex > desiredIndex) { - high = mid - 1; - } else { - low = mid; - } - } - - return low; - } - -} diff --git a/src/vs/editor/common/core/lineParts.ts b/src/vs/editor/common/core/lineParts.ts index e3378304cf7..784049905bc 100644 --- a/src/vs/editor/common/core/lineParts.ts +++ b/src/vs/editor/common/core/lineParts.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { Arrays } from 'vs/editor/common/core/arrays'; import { ViewLineToken } from 'vs/editor/common/core/viewLineToken'; export class LineParts { @@ -24,8 +23,4 @@ export class LineParts { && ViewLineToken.equalsArray(this.parts, other.parts) ); } - - public findIndexOfOffset(offset: number): number { - return Arrays.findIndexInSegmentsArray(this.parts, offset); - } } diff --git a/src/vs/editor/common/core/position.ts b/src/vs/editor/common/core/position.ts index 306e4d36dd8..2e16fc22223 100644 --- a/src/vs/editor/common/core/position.ts +++ b/src/vs/editor/common/core/position.ts @@ -90,6 +90,22 @@ export class Position { return a.column <= b.column; } + /** + * A function that compares positions, useful for sorting + */ + public static compare(a: IPosition, b: IPosition): number { + let aLineNumber = a.lineNumber | 0; + let bLineNumber = b.lineNumber | 0; + + if (aLineNumber === bLineNumber) { + let aColumn = a.column | 0; + let bColumn = b.column | 0; + return aColumn - bColumn; + } + + return aLineNumber - bLineNumber; + } + /** * Clone this position. */ diff --git a/src/vs/editor/common/core/range.ts b/src/vs/editor/common/core/range.ts index 39aa0512112..b7f475d89f9 100644 --- a/src/vs/editor/common/core/range.ts +++ b/src/vs/editor/common/core/range.ts @@ -317,16 +317,18 @@ export class Range { public static compareRangesUsingStarts(a: IRange, b: IRange): number { let aStartLineNumber = a.startLineNumber | 0; let bStartLineNumber = b.startLineNumber | 0; - let aStartColumn = a.startColumn | 0; - let bStartColumn = b.startColumn | 0; - let aEndLineNumber = a.endLineNumber | 0; - let bEndLineNumber = b.endLineNumber | 0; - let aEndColumn = a.endColumn | 0; - let bEndColumn = b.endColumn | 0; if (aStartLineNumber === bStartLineNumber) { + let aStartColumn = a.startColumn | 0; + let bStartColumn = b.startColumn | 0; + if (aStartColumn === bStartColumn) { + let aEndLineNumber = a.endLineNumber | 0; + let bEndLineNumber = b.endLineNumber | 0; + if (aEndLineNumber === bEndLineNumber) { + let aEndColumn = a.endColumn | 0; + let bEndColumn = b.endColumn | 0; return aEndColumn - bEndColumn; } return aEndLineNumber - bEndLineNumber; diff --git a/src/vs/editor/common/core/viewLineToken.ts b/src/vs/editor/common/core/viewLineToken.ts index 8ebf372c76f..661483207df 100644 --- a/src/vs/editor/common/core/viewLineToken.ts +++ b/src/vs/editor/common/core/viewLineToken.ts @@ -4,8 +4,6 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { Arrays } from 'vs/editor/common/core/arrays'; - /** * A token on a line. */ @@ -27,10 +25,6 @@ export class ViewLineToken { ); } - public static findIndexInSegmentsArray(arr: ViewLineToken[], desiredIndex: number): number { - return Arrays.findIndexInSegmentsArray(arr, desiredIndex); - } - public static equalsArray(a: ViewLineToken[], b: ViewLineToken[]): boolean { let aLen = a.length; let bLen = b.length; @@ -78,8 +72,4 @@ export class ViewLineTokens { && ViewLineToken.equalsArray(this._lineTokens, other._lineTokens) ); } - - public findIndexOfOffset(offset: number): number { - return ViewLineToken.findIndexInSegmentsArray(this._lineTokens, offset); - } } diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index d80f06c6b7e..1bc532e7c83 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/src/vs/editor/common/editorCommon.ts @@ -1774,10 +1774,11 @@ export interface ITextModel { * @param isRegex Used to indicate that `searchString` is a regular expression. * @param matchCase Force the matching to match lower/upper case exactly. * @param wholeWord Force the matching to match entire words only. + * @param captureMatches The result will contain the captured groups. * @param limitResultCount Limit the number of results * @return The ranges where the matches are. It is empty if not matches have been found. */ - findMatches(searchString: string, searchOnlyEditableRange: boolean, isRegex: boolean, matchCase: boolean, wholeWord: boolean, limitResultCount?: number): Range[]; + findMatches(searchString: string, searchOnlyEditableRange: boolean, isRegex: boolean, matchCase: boolean, wholeWord: boolean, captureMatches: boolean, limitResultCount?: number): FindMatch[]; /** * Search the model. * @param searchString The string used to search. If it is a regular expression, set `isRegex` to true. @@ -1785,10 +1786,11 @@ export interface ITextModel { * @param isRegex Used to indicate that `searchString` is a regular expression. * @param matchCase Force the matching to match lower/upper case exactly. * @param wholeWord Force the matching to match entire words only. + * @param captureMatches The result will contain the captured groups. * @param limitResultCount Limit the number of results * @return The ranges where the matches are. It is empty if no matches have been found. */ - findMatches(searchString: string, searchScope: IRange, isRegex: boolean, matchCase: boolean, wholeWord: boolean, limitResultCount?: number): Range[]; + findMatches(searchString: string, searchScope: IRange, isRegex: boolean, matchCase: boolean, wholeWord: boolean, captureMatches: boolean, limitResultCount?: number): FindMatch[]; /** * Search the model for the next match. Loops to the beginning of the model if needed. * @param searchString The string used to search. If it is a regular expression, set `isRegex` to true. @@ -1796,9 +1798,10 @@ export interface ITextModel { * @param isRegex Used to indicate that `searchString` is a regular expression. * @param matchCase Force the matching to match lower/upper case exactly. * @param wholeWord Force the matching to match entire words only. + * @param captureMatches The result will contain the captured groups. * @return The range where the next match is. It is null if no next match has been found. */ - findNextMatch(searchString: string, searchStart: IPosition, isRegex: boolean, matchCase: boolean, wholeWord: boolean): Range; + findNextMatch(searchString: string, searchStart: IPosition, isRegex: boolean, matchCase: boolean, wholeWord: boolean, captureMatches: boolean): FindMatch; /** * Search the model for the previous match. Loops to the end of the model if needed. * @param searchString The string used to search. If it is a regular expression, set `isRegex` to true. @@ -1806,9 +1809,10 @@ export interface ITextModel { * @param isRegex Used to indicate that `searchString` is a regular expression. * @param matchCase Force the matching to match lower/upper case exactly. * @param wholeWord Force the matching to match entire words only. + * @param captureMatches The result will contain the captured groups. * @return The range where the previous match is. It is null if no previous match has been found. */ - findPreviousMatch(searchString: string, searchStart: IPosition, isRegex: boolean, matchCase: boolean, wholeWord: boolean): Range; + findPreviousMatch(searchString: string, searchStart: IPosition, isRegex: boolean, matchCase: boolean, wholeWord: boolean, captureMatches: boolean): FindMatch; } export class FindMatch { @@ -3060,6 +3064,10 @@ export namespace ModeContextKeys { * @internal */ export const hasDefinitionProvider = new RawContextKey('editorHasDefinitionProvider', undefined); + /** + * @internal + */ + export const hasTypeDefinitionProvider = new RawContextKey('editorHasTypeDefinitionProvider', undefined); /** * @internal */ @@ -4399,8 +4407,6 @@ export var Handler = { CreateCursor: 'createCursor', LastCursorMoveToSelect: 'lastCursorMoveToSelect', - JumpToBracket: 'jumpToBracket', - Type: 'type', ReplacePreviousChar: 'replacePreviousChar', CompositionStart: 'compositionStart', diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 17aaaff0633..fe322cc4bb9 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -832,7 +832,7 @@ export class TextModel extends OrderGuaranteeEventEmitter implements editorCommo throw new Error('Unknown EOL preference'); } - public findMatches(searchString: string, rawSearchScope: any, isRegex: boolean, matchCase: boolean, wholeWord: boolean, limitResultCount: number = LIMIT_FIND_COUNT): Range[] { + public findMatches(searchString: string, rawSearchScope: any, isRegex: boolean, matchCase: boolean, wholeWord: boolean, captureMatches: boolean, limitResultCount: number = LIMIT_FIND_COUNT): editorCommon.FindMatch[] { this._assertNotDisposed(); let searchRange: Range; @@ -842,19 +842,19 @@ export class TextModel extends OrderGuaranteeEventEmitter implements editorCommo searchRange = this.getFullModelRange(); } - return TextModelSearch.findMatches(this, new SearchParams(searchString, isRegex, matchCase, wholeWord), searchRange, false, limitResultCount).map(e => e.range); + return TextModelSearch.findMatches(this, new SearchParams(searchString, isRegex, matchCase, wholeWord), searchRange, captureMatches, limitResultCount); } - public findNextMatch(searchString: string, rawSearchStart: editorCommon.IPosition, isRegex: boolean, matchCase: boolean, wholeWord: boolean): Range { + public findNextMatch(searchString: string, rawSearchStart: editorCommon.IPosition, isRegex: boolean, matchCase: boolean, wholeWord: boolean, captureMatches: boolean): editorCommon.FindMatch { this._assertNotDisposed(); - let r = TextModelSearch.findNextMatch(this, new SearchParams(searchString, isRegex, matchCase, wholeWord), rawSearchStart, false); - return r ? r.range : null; + const searchStart = this.validatePosition(rawSearchStart); + return TextModelSearch.findNextMatch(this, new SearchParams(searchString, isRegex, matchCase, wholeWord), searchStart, captureMatches); } - public findPreviousMatch(searchString: string, rawSearchStart: editorCommon.IPosition, isRegex: boolean, matchCase: boolean, wholeWord: boolean): Range { + public findPreviousMatch(searchString: string, rawSearchStart: editorCommon.IPosition, isRegex: boolean, matchCase: boolean, wholeWord: boolean, captureMatches: boolean): editorCommon.FindMatch { this._assertNotDisposed(); - let r = TextModelSearch.findPreviousMatch(this, new SearchParams(searchString, isRegex, matchCase, wholeWord), rawSearchStart, false); - return r ? r.range : null; + const searchStart = this.validatePosition(rawSearchStart); + return TextModelSearch.findPreviousMatch(this, new SearchParams(searchString, isRegex, matchCase, wholeWord), searchStart, captureMatches); } } diff --git a/src/vs/editor/common/model/textModelSearch.ts b/src/vs/editor/common/model/textModelSearch.ts index bc516faa852..93f1eb9cc40 100644 --- a/src/vs/editor/common/model/textModelSearch.ts +++ b/src/vs/editor/common/model/textModelSearch.ts @@ -7,7 +7,7 @@ import * as strings from 'vs/base/common/strings'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { IPosition, FindMatch } from 'vs/editor/common/editorCommon'; +import { FindMatch } from 'vs/editor/common/editorCommon'; import { CharCode } from 'vs/base/common/charCode'; import { TextModel } from 'vs/editor/common/model/textModel'; @@ -205,13 +205,12 @@ export class TextModelSearch { return counter; } - public static findNextMatch(model: TextModel, searchParams: SearchParams, rawSearchStart: IPosition, captureMatches: boolean): FindMatch { + public static findNextMatch(model: TextModel, searchParams: SearchParams, searchStart: Position, captureMatches: boolean): FindMatch { const regex = searchParams.parseSearchRequest(); if (!regex) { return null; } - const searchStart = model.validatePosition(rawSearchStart); if (regex.multiline) { return this._doFindNextMatchMultiline(model, searchStart, regex, captureMatches); } @@ -219,7 +218,7 @@ export class TextModelSearch { } private static _doFindNextMatchMultiline(model: TextModel, searchStart: Position, searchRegex: RegExp, captureMatches: boolean): FindMatch { - const searchTextStart: IPosition = { lineNumber: searchStart.lineNumber, column: 1 }; + const searchTextStart = new Position(searchStart.lineNumber, 1); const deltaOffset = model.getOffsetAt(searchTextStart); const lineCount = model.getLineCount(); const text = model.getValueInRange(new Range(searchTextStart.lineNumber, searchTextStart.column, lineCount, model.getLineMaxColumn(lineCount))); @@ -282,13 +281,12 @@ export class TextModelSearch { return null; } - public static findPreviousMatch(model: TextModel, searchParams: SearchParams, rawSearchStart: IPosition, captureMatches: boolean): FindMatch { + public static findPreviousMatch(model: TextModel, searchParams: SearchParams, searchStart: Position, captureMatches: boolean): FindMatch { const regex = searchParams.parseSearchRequest(); if (!regex) { return null; } - const searchStart = model.validatePosition(rawSearchStart); if (regex.multiline) { return this._doFindPreviousMatchMultiline(model, searchStart, regex, captureMatches); } 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/common/services/editorWorkerServiceImpl.ts b/src/vs/editor/common/services/editorWorkerServiceImpl.ts index aa9d5fdb4ae..8de07aa6a2b 100644 --- a/src/vs/editor/common/services/editorWorkerServiceImpl.ts +++ b/src/vs/editor/common/services/editorWorkerServiceImpl.ts @@ -31,8 +31,8 @@ const STOP_WORKER_DELTA_TIME_MS = 5 * 60 * 1000; export class EditorWorkerServiceImpl implements IEditorWorkerService { public _serviceBrand: any; - private _workerManager: WorkerManager; - private _registrations: IDisposable[]; + private readonly _workerManager: WorkerManager; + private readonly _registrations: IDisposable[]; constructor( @IModelService modelService: IModelService, @@ -187,14 +187,12 @@ class EditorModelManager extends Disposable { } private _beginModelSync(resource: URI): void { - let modelUrl = resource.toString(); let model = this._modelService.getModel(resource); if (!model) { return; } - if (model.isTooLargeForHavingARichMode()) { - return; - } + + let modelUrl = resource.toString(); this._proxy.acceptNewModel({ url: model.uri.toString(), diff --git a/src/vs/editor/common/viewLayout/viewLineParts.ts b/src/vs/editor/common/viewLayout/viewLineParts.ts index cbf7394f819..aa7694aa5e5 100644 --- a/src/vs/editor/common/viewLayout/viewLineParts.ts +++ b/src/vs/editor/common/viewLayout/viewLineParts.ts @@ -38,56 +38,6 @@ export function createLineParts(lineNumber: number, minLineColumn: number, lineC } } -export function getColumnOfLinePartOffset(stopRenderingLineAfter: number, lineParts: ViewLineToken[], lineMaxColumn: number, charOffsetInPart: number[], partIndex: number, partLength: number, offset: number): number { - if (partIndex >= lineParts.length) { - return stopRenderingLineAfter; - } - - if (offset === 0) { - return lineParts[partIndex].startIndex + 1; - } - - if (offset === partLength) { - return (partIndex + 1 < lineParts.length ? lineParts[partIndex + 1].startIndex + 1 : lineMaxColumn); - } - - let originalMin = lineParts[partIndex].startIndex; - let originalMax = (partIndex + 1 < lineParts.length ? lineParts[partIndex + 1].startIndex : lineMaxColumn - 1); - - let min = originalMin; - let max = originalMax; - - // invariant: offsetOf(min) <= offset <= offsetOf(max) - while (min + 1 < max) { - let mid = ((min + max) >>> 1); - let midOffset = charOffsetInPart[mid]; - - if (midOffset === offset) { - return mid + 1; - } else if (midOffset > offset) { - max = mid; - } else { - min = mid; - } - } - - if (min === max) { - return min + 1; - } - - let minOffset = charOffsetInPart[min]; - let maxOffset = (max < originalMax ? charOffsetInPart[max] : partLength); - - let distanceToMin = offset - minOffset; - let distanceToMax = maxOffset - offset; - - if (distanceToMin <= distanceToMax) { - return min + 1; - } else { - return max + 1; - } -} - function trimEmptyTrailingPart(parts: ViewLineToken[], lineContent: string): ViewLineToken[] { if (parts.length <= 1) { return parts; diff --git a/src/vs/editor/common/viewLayout/viewLineRenderer.ts b/src/vs/editor/common/viewLayout/viewLineRenderer.ts index 8bcf450c58d..58621061fbc 100644 --- a/src/vs/editor/common/viewLayout/viewLineRenderer.ts +++ b/src/vs/editor/common/viewLayout/viewLineRenderer.ts @@ -50,17 +50,126 @@ export class RenderLineInput { } } +export const enum CharacterMappingConstants { + PART_INDEX_MASK = 0b11111111111111110000000000000000, + CHAR_INDEX_MASK = 0b00000000000000001111111111111111, + + CHAR_INDEX_OFFSET = 0, + PART_INDEX_OFFSET = 16 +} + +/** + * Provides a both direction mapping between a line's character and its rendered position. + */ +export class CharacterMapping { + + public static getPartIndex(partData: number): number { + return (partData & CharacterMappingConstants.PART_INDEX_MASK) >>> CharacterMappingConstants.PART_INDEX_OFFSET; + } + + public static getCharIndex(partData: number): number { + return (partData & CharacterMappingConstants.CHAR_INDEX_MASK) >>> CharacterMappingConstants.CHAR_INDEX_OFFSET; + } + + private readonly _data: Uint32Array; + public readonly length: number; + + constructor(length: number) { + this.length = length; + this._data = new Uint32Array(this.length); + } + + public setPartData(charOffset: number, partIndex: number, charIndex: number): void { + let partData = ( + (partIndex << CharacterMappingConstants.PART_INDEX_OFFSET) + | (charIndex << CharacterMappingConstants.CHAR_INDEX_OFFSET) + ) >>> 0; + this._data[charOffset] = partData; + } + + public charOffsetToPartData(charOffset: number): number { + if (this.length === 0) { + return 0; + } + if (charOffset < 0) { + return this._data[0]; + } + if (charOffset >= this.length) { + return this._data[this.length - 1]; + } + return this._data[charOffset]; + } + + public partDataToCharOffset(partIndex: number, partLength: number, charIndex: number): number { + if (this.length === 0) { + return 0; + } + + let searchEntry = ( + (partIndex << CharacterMappingConstants.PART_INDEX_OFFSET) + | (charIndex << CharacterMappingConstants.CHAR_INDEX_OFFSET) + ) >>> 0; + + let min = 0; + let max = this.length - 1; + while (min + 1 < max) { + let mid = ((min + max) >>> 1); + let midEntry = this._data[mid]; + if (midEntry === searchEntry) { + return mid; + } else if (midEntry > searchEntry) { + max = mid; + } else { + min = mid; + } + } + + if (min === max) { + return min; + } + + let minEntry = this._data[min]; + let maxEntry = this._data[max]; + + if (minEntry === searchEntry) { + return min; + } + if (maxEntry === searchEntry) { + return max; + } + + let minPartIndex = CharacterMapping.getPartIndex(minEntry); + let minCharIndex = CharacterMapping.getCharIndex(minEntry); + + let maxPartIndex = CharacterMapping.getPartIndex(maxEntry); + let maxCharIndex: number; + + if (minPartIndex !== maxPartIndex) { + // sitting between parts + maxCharIndex = partLength; + } else { + maxCharIndex = CharacterMapping.getCharIndex(maxEntry); + } + + let minEntryDistance = charIndex - minCharIndex; + let maxEntryDistance = maxCharIndex - charIndex; + + if (minEntryDistance <= maxEntryDistance) { + return min; + } + return max; + } +} + export class RenderLineOutput { _renderLineOutputBrand: void; - readonly charOffsetInPart: number[]; - readonly lastRenderedPartIndex: number; + readonly characterMapping: CharacterMapping; readonly output: string; readonly isWhitespaceOnly: boolean; - constructor(charOffsetInPart: number[], lastRenderedPartIndex: number, output: string, isWhitespaceOnly: boolean) { - this.charOffsetInPart = charOffsetInPart; - this.lastRenderedPartIndex = lastRenderedPartIndex; + constructor(characterMapping: CharacterMapping, output: string, isWhitespaceOnly: boolean) { + this.characterMapping = characterMapping; this.output = output; this.isWhitespaceOnly = isWhitespaceOnly; } @@ -78,8 +187,7 @@ export function renderLine(input: RenderLineInput): RenderLineOutput { if (lineTextLength === 0) { return new RenderLineOutput( - [], - 0, + new CharacterMapping(0), // This is basically for IE's hit test to work ' ', true @@ -113,11 +221,12 @@ function renderLineActual(lineText: string, lineTextLength: number, tabSize: num let charIndex = 0; let out = ''; - let charOffsetInPartArr: number[] = []; let charOffsetInPart = 0; let tabsCharDelta = 0; let isWhitespaceOnly = /^\s*$/.test(lineText); + let characterMapping = new CharacterMapping(Math.min(lineTextLength, charBreakIndex) + 1); + out += ''; for (let partIndex = 0, partIndexLen = actualLineParts.length; partIndex < partIndexLen; partIndex++) { let part = actualLineParts[partIndex]; @@ -136,7 +245,7 @@ function renderLineActual(lineText: string, lineTextLength: number, tabSize: num let partContentCnt = 0; let partContent = ''; for (; charIndex < toCharIndex; charIndex++) { - charOffsetInPartArr[charIndex] = charOffsetInPart; + characterMapping.setPartData(charIndex, partIndex, charOffsetInPart); let charCode = lineText.charCodeAt(charIndex); if (charCode === CharCode.Tab) { @@ -163,10 +272,9 @@ function renderLineActual(lineText: string, lineTextLength: number, tabSize: num if (charIndex >= charBreakIndex) { out += `${partContent}…`; - charOffsetInPartArr[charIndex] = charOffsetInPart; + characterMapping.setPartData(charIndex, partIndex, charOffsetInPart); return new RenderLineOutput( - charOffsetInPartArr, - partIndex, + characterMapping, out, isWhitespaceOnly ); @@ -177,7 +285,7 @@ function renderLineActual(lineText: string, lineTextLength: number, tabSize: num out += ``; for (; charIndex < toCharIndex; charIndex++) { - charOffsetInPartArr[charIndex] = charOffsetInPart; + characterMapping.setPartData(charIndex, partIndex, charOffsetInPart); let charCode = lineText.charCodeAt(charIndex); switch (charCode) { @@ -233,10 +341,9 @@ function renderLineActual(lineText: string, lineTextLength: number, tabSize: num if (charIndex >= charBreakIndex) { out += '…'; - charOffsetInPartArr[charIndex] = charOffsetInPart; + characterMapping.setPartData(charIndex, partIndex, charOffsetInPart); return new RenderLineOutput( - charOffsetInPartArr, - partIndex, + characterMapping, out, isWhitespaceOnly ); @@ -251,11 +358,10 @@ function renderLineActual(lineText: string, lineTextLength: number, tabSize: num // When getting client rects for the last character, we will position the // text range at the end of the span, insteaf of at the beginning of next span - charOffsetInPartArr.push(charOffsetInPart); + characterMapping.setPartData(lineTextLength, actualLineParts.length - 1, charOffsetInPart); return new RenderLineOutput( - charOffsetInPartArr, - actualLineParts.length - 1, + characterMapping, out, isWhitespaceOnly ); diff --git a/src/vs/editor/contrib/bracketMatching/browser/bracketMatching.css b/src/vs/editor/contrib/bracketMatching/browser/bracketMatching.css new file mode 100644 index 00000000000..161b88a13d9 --- /dev/null +++ b/src/vs/editor/contrib/bracketMatching/browser/bracketMatching.css @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-editor .bracket-match { + box-sizing: border-box; + background-color: rgba(0, 100, 0, 0.1); +} +.monaco-editor.vs .bracket-match { border: 1px solid #B9B9B9; } +.monaco-editor.vs-dark .bracket-match { border: 1px solid #888; } +.monaco-editor.hc-black .bracket-match { border: 1px solid #fff; } diff --git a/src/vs/editor/contrib/bracketMatching/common/bracketMatching.ts b/src/vs/editor/contrib/bracketMatching/common/bracketMatching.ts new file mode 100644 index 00000000000..2601f55f2f4 --- /dev/null +++ b/src/vs/editor/contrib/bracketMatching/common/bracketMatching.ts @@ -0,0 +1,188 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as nls from 'vs/nls'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Range } from 'vs/editor/common/core/range'; +import { Position } from 'vs/editor/common/core/position'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import * as editorCommon from 'vs/editor/common/editorCommon'; +import { editorAction, commonEditorContribution, ServicesAccessor, EditorAction } from 'vs/editor/common/editorCommonExtensions'; + +import EditorContextKeys = editorCommon.EditorContextKeys; + +@editorAction +class SelectBracketAction extends EditorAction { + constructor() { + super({ + id: 'editor.action.jumpToBracket', + label: nls.localize('smartSelect.jumpBracket', "Go to Bracket"), + alias: 'Go to Bracket', + precondition: null, + kbOpts: { + kbExpr: EditorContextKeys.TextFocus, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_BACKSLASH + } + }); + } + + public run(accessor: ServicesAccessor, editor: editorCommon.ICommonCodeEditor): void { + let controller = BracketMatchingController.get(editor); + if (!controller) { + return; + } + controller.jumpToBracket(); + } +} + +type Brackets = [Range, Range]; + +class BracketsData { + public readonly position: Position; + public readonly brackets: Brackets; + + constructor(position: Position, brackets: Brackets) { + this.position = position; + this.brackets = brackets; + } +} + +@commonEditorContribution +export class BracketMatchingController extends Disposable implements editorCommon.IEditorContribution { + private static ID = 'editor.contrib.bracketMatchingController'; + + public static get(editor: editorCommon.ICommonCodeEditor): BracketMatchingController { + return editor.getContribution(BracketMatchingController.ID); + } + + private readonly _editor: editorCommon.ICommonCodeEditor; + + private _lastBracketsData: BracketsData[]; + private _lastVersionId: number; + private _decorations: string[]; + private _updateBracketsSoon: RunOnceScheduler; + + constructor(editor: editorCommon.ICommonCodeEditor) { + super(); + this._editor = editor; + this._lastBracketsData = []; + this._lastVersionId = 0; + this._decorations = []; + this._updateBracketsSoon = this._register(new RunOnceScheduler(() => this._updateBrackets(), 50)); + + this._updateBracketsSoon.schedule(); + this._register(editor.onDidChangeCursorPosition((e) => this._updateBracketsSoon.schedule())); + this._register(editor.onDidChangeModel((e) => { this._decorations = []; this._updateBracketsSoon.schedule(); })); + } + + public getId(): string { + return BracketMatchingController.ID; + } + + public jumpToBracket(): void { + const model = this._editor.getModel(); + if (!model) { + return; + } + + const selection = this._editor.getSelection(); + if (!selection.isEmpty()) { + return; + } + + const position = selection.getStartPosition(); + const brackets = model.matchBracket(position); + if (!brackets) { + return; + } + + if (brackets[0].containsPosition(position)) { + this._editor.setPosition(brackets[1].getStartPosition()); + return; + } + + if (brackets[1].containsPosition(position)) { + this._editor.setPosition(brackets[0].getStartPosition()); + return; + } + } + + private static _DECORATION_OPTIONS: editorCommon.IModelDecorationOptions = { + stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + className: 'bracket-match' + }; + + private _updateBrackets(): void { + this._recomputeBrackets(); + + let newDecorations: editorCommon.IModelDeltaDecoration[] = [], newDecorationsLen = 0; + for (let i = 0, len = this._lastBracketsData.length; i < len; i++) { + let brackets = this._lastBracketsData[i].brackets; + if (brackets) { + newDecorations[newDecorationsLen++] = { range: brackets[0], options: BracketMatchingController._DECORATION_OPTIONS }; + newDecorations[newDecorationsLen++] = { range: brackets[1], options: BracketMatchingController._DECORATION_OPTIONS }; + } + } + + this._decorations = this._editor.deltaDecorations(this._decorations, newDecorations); + } + + private _recomputeBrackets(): void { + const model = this._editor.getModel(); + if (!model) { + // no model => no brackets! + this._lastBracketsData = []; + this._lastVersionId = 0; + return; + } + + const versionId = model.getVersionId(); + let previousData: BracketsData[] = []; + if (this._lastVersionId === versionId) { + // use the previous data only if the model is at the same version id + previousData = this._lastBracketsData; + } + + const selections = this._editor.getSelections(); + + let positions: Position[] = [], positionsLen = 0; + for (let i = 0, len = selections.length; i < len; i++) { + let selection = selections[i]; + + if (selection.isEmpty()) { + // will bracket match a cursor only if the selection is collapsed + positions[positionsLen++] = selection.getStartPosition(); + } + } + + // sort positions for `previousData` cache hits + if (positions.length > 1) { + positions.sort(Position.compare); + } + + let newData: BracketsData[] = [], newDataLen = 0; + let previousIndex = 0, previousLen = previousData.length; + for (let i = 0, len = positions.length; i < len; i++) { + let position = positions[i]; + + while (previousIndex < previousLen && previousData[previousIndex].position.isBefore(position)) { + previousIndex++; + } + + if (previousIndex < previousLen && previousData[previousIndex].position.equals(position)) { + newData[newDataLen++] = previousData[previousIndex]; + } else { + let brackets = model.matchBracket(position); + newData[newDataLen++] = new BracketsData(position, brackets); + } + } + + this._lastBracketsData = newData; + this._lastVersionId = versionId; + } +} diff --git a/src/vs/editor/contrib/bracketMatching/test/common/bracketMatching.test.ts b/src/vs/editor/contrib/bracketMatching/test/common/bracketMatching.test.ts new file mode 100644 index 00000000000..56c6b20b5ff --- /dev/null +++ b/src/vs/editor/contrib/bracketMatching/test/common/bracketMatching.test.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as assert from 'assert'; +import { withMockCodeEditor } from 'vs/editor/test/common/mocks/mockCodeEditor'; +import { Position } from 'vs/editor/common/core/position'; +import { Model } from 'vs/editor/common/model/model'; +import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; +import { MockMode } from 'vs/editor/test/common/mocks/mockMode'; +import { LanguageIdentifier } from 'vs/editor/common/modes'; +import { BracketMatchingController } from 'vs/editor/contrib/bracketMatching/common/bracketMatching'; + +suite('bracket matching', () => { + test('issue #183: jump to matching bracket position', () => { + class BracketMode extends MockMode { + + private static _id = new LanguageIdentifier('bracketMode', 3); + + constructor() { + super(BracketMode._id); + this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'], + ] + })); + } + } + + let mode = new BracketMode(); + let model = Model.createFromString('var x = (3 + (5-7)) + ((5+3)+5);', undefined, mode.getLanguageIdentifier()); + + withMockCodeEditor(null, { model: model }, (editor, cursor) => { + let bracketMatchingController = editor.registerAndInstantiateContribution(BracketMatchingController); + + editor.setPosition(new Position(1, 20)); + bracketMatchingController.jumpToBracket(); + assert.deepEqual(editor.getPosition(), new Position(1, 9)); + bracketMatchingController.jumpToBracket(); + assert.deepEqual(editor.getPosition(), new Position(1, 19)); + bracketMatchingController.jumpToBracket(); + assert.deepEqual(editor.getPosition(), new Position(1, 9)); + + editor.setPosition(new Position(1, 23)); + bracketMatchingController.jumpToBracket(); + assert.deepEqual(editor.getPosition(), new Position(1, 31)); + bracketMatchingController.jumpToBracket(); + assert.deepEqual(editor.getPosition(), new Position(1, 23)); + bracketMatchingController.jumpToBracket(); + assert.deepEqual(editor.getPosition(), new Position(1, 31)); + + bracketMatchingController.dispose(); + }); + + model.dispose(); + mode.dispose(); + }); +}); diff --git a/src/vs/editor/contrib/defineKeybinding/browser/defineKeybinding.ts b/src/vs/editor/contrib/defineKeybinding/browser/defineKeybinding.ts index a66f37afa16..2078b0a0418 100644 --- a/src/vs/editor/contrib/defineKeybinding/browser/defineKeybinding.ts +++ b/src/vs/editor/contrib/defineKeybinding/browser/defineKeybinding.ts @@ -150,7 +150,7 @@ export class DefineKeybindingController implements editorCommon.IEditorContribut let model = this._editor.getModel(); let regex = KeybindingLabels.getUserSettingsKeybindingRegex(); - var m = model.findMatches(regex, false, true, false, false); + var m = model.findMatches(regex, false, true, false, false, false).map(m => m.range); let data = m.map((range) => { let text = model.getValueInRange(range); diff --git a/src/vs/editor/contrib/find/common/findController.ts b/src/vs/editor/contrib/find/common/findController.ts index e71aa8063e4..a4128a58a47 100644 --- a/src/vs/editor/contrib/find/common/findController.ts +++ b/src/vs/editor/contrib/find/common/findController.ts @@ -540,13 +540,13 @@ export abstract class SelectNextFindMatchAction extends EditorAction { let allSelections = editor.getSelections(); let lastAddedSelection = allSelections[allSelections.length - 1]; - let nextMatch = editor.getModel().findNextMatch(r.searchText, lastAddedSelection.getEndPosition(), false, r.matchCase, r.wholeWord); + let nextMatch = editor.getModel().findNextMatch(r.searchText, lastAddedSelection.getEndPosition(), false, r.matchCase, r.wholeWord, false); if (!nextMatch) { return null; } - return new Selection(nextMatch.startLineNumber, nextMatch.startColumn, nextMatch.endLineNumber, nextMatch.endColumn); + return new Selection(nextMatch.range.startLineNumber, nextMatch.range.startColumn, nextMatch.range.endLineNumber, nextMatch.range.endColumn); } } @@ -563,13 +563,13 @@ export abstract class SelectPreviousFindMatchAction extends EditorAction { let allSelections = editor.getSelections(); let lastAddedSelection = allSelections[allSelections.length - 1]; - let previousMatch = editor.getModel().findPreviousMatch(r.searchText, lastAddedSelection.getStartPosition(), false, r.matchCase, r.wholeWord); + let previousMatch = editor.getModel().findPreviousMatch(r.searchText, lastAddedSelection.getStartPosition(), false, r.matchCase, r.wholeWord, false); if (!previousMatch) { return null; } - return new Selection(previousMatch.startLineNumber, previousMatch.startColumn, previousMatch.endLineNumber, previousMatch.endColumn); + return new Selection(previousMatch.range.startLineNumber, previousMatch.range.startColumn, previousMatch.range.endLineNumber, previousMatch.range.endColumn); } } @@ -688,7 +688,7 @@ export abstract class AbstractSelectHighlightsAction extends EditorAction { return; } - let matches = editor.getModel().findMatches(r.searchText, true, false, r.matchCase, r.wholeWord); + let matches = editor.getModel().findMatches(r.searchText, true, false, r.matchCase, r.wholeWord, false).map(m => m.range); if (matches.length > 0) { let editorSelection = editor.getSelection(); @@ -844,7 +844,7 @@ export class SelectionHighlighter extends Disposable implements editorCommon.IEd } - let allMatches = model.findMatches(r.searchText, true, false, r.matchCase, r.wholeWord); + let allMatches = model.findMatches(r.searchText, true, false, r.matchCase, r.wholeWord, false).map(m => m.range); allMatches.sort(Range.compareRangesUsingStarts); selections.sort(Range.compareRangesUsingStarts); diff --git a/src/vs/editor/contrib/find/common/findModel.ts b/src/vs/editor/contrib/find/common/findModel.ts index 3f16d1c9fb6..337a5a0fbb1 100644 --- a/src/vs/editor/contrib/find/common/findModel.ts +++ b/src/vs/editor/contrib/find/common/findModel.ts @@ -6,7 +6,7 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { ReplacePattern } from 'vs/platform/search/common/replace'; +import { ReplacePattern, parseReplaceString } from 'vs/editor/contrib/find/common/replacePattern'; import { ReplaceCommand } from 'vs/editor/common/commands/replaceCommand'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; @@ -160,8 +160,8 @@ export class FindModelBoundToEditorModel { findScope = new Range(findScope.startLineNumber, 1, findScope.endLineNumber, this._editor.getModel().getLineMaxColumn(findScope.endLineNumber)); } - let findMatches = this._findMatches(findScope, MATCHES_LIMIT); - this._decorations.set(findMatches, findScope); + let findMatches = this._findMatches(findScope, false, MATCHES_LIMIT); + this._decorations.set(findMatches.map(match => match.range), findScope); this._state.changeMatchInfo( this._decorations.getCurrentMatchesPosition(this._editor.getSelection()), @@ -225,9 +225,9 @@ export class FindModelBoundToEditorModel { let position = new Position(lineNumber, column); - let prevMatch = model.findPreviousMatch(this._state.searchString, position, this._state.isRegex, this._state.matchCase, this._state.wholeWord); + let prevMatch = model.findPreviousMatch(this._state.searchString, position, this._state.isRegex, this._state.matchCase, this._state.wholeWord, false); - if (prevMatch && prevMatch.isEmpty() && prevMatch.getStartPosition().equals(position)) { + if (prevMatch && prevMatch.range.isEmpty() && prevMatch.range.getStartPosition().equals(position)) { // Looks like we're stuck at this position, unacceptable! let isUsingLineStops = this._state.isRegex && ( @@ -247,19 +247,19 @@ export class FindModelBoundToEditorModel { } position = new Position(lineNumber, column); - prevMatch = model.findPreviousMatch(this._state.searchString, position, this._state.isRegex, this._state.matchCase, this._state.wholeWord); + prevMatch = model.findPreviousMatch(this._state.searchString, position, this._state.isRegex, this._state.matchCase, this._state.wholeWord, false); } if (!prevMatch) { // there is precisely one match and selection is on top of it - return; + return null; } - if (!isRecursed && !searchRange.containsRange(prevMatch)) { - return this._moveToPrevMatch(prevMatch.getStartPosition(), true); + if (!isRecursed && !searchRange.containsRange(prevMatch.range)) { + return this._moveToPrevMatch(prevMatch.range.getStartPosition(), true); } - this._setCurrentFindMatch(prevMatch); + this._setCurrentFindMatch(prevMatch.range); } public moveToPrevMatch(): void { @@ -267,13 +267,13 @@ export class FindModelBoundToEditorModel { } private _moveToNextMatch(after: Position): void { - let nextMatch = this._getNextMatch(after); + let nextMatch = this._getNextMatch(after, false); if (nextMatch) { - this._setCurrentFindMatch(nextMatch as Range); + this._setCurrentFindMatch(nextMatch.range); } } - private _getNextMatch(after: Position, isRecursed: boolean = false): Range { + private _getNextMatch(after: Position, captureMatches: boolean, isRecursed: boolean = false): editorCommon.FindMatch { if (this._cannotFind()) { return null; } @@ -296,9 +296,9 @@ export class FindModelBoundToEditorModel { let position = new Position(lineNumber, column); - let nextMatch = model.findNextMatch(this._state.searchString, position, this._state.isRegex, this._state.matchCase, this._state.wholeWord); + let nextMatch = model.findNextMatch(this._state.searchString, position, this._state.isRegex, this._state.matchCase, this._state.wholeWord, captureMatches); - if (nextMatch && nextMatch.isEmpty() && nextMatch.getStartPosition().equals(position)) { + if (nextMatch && nextMatch.range.isEmpty() && nextMatch.range.getStartPosition().equals(position)) { // Looks like we're stuck at this position, unacceptable! let isUsingLineStops = this._state.isRegex && ( @@ -318,7 +318,7 @@ export class FindModelBoundToEditorModel { } position = new Position(lineNumber, column); - nextMatch = model.findNextMatch(this._state.searchString, position, this._state.isRegex, this._state.matchCase, this._state.wholeWord); + nextMatch = model.findNextMatch(this._state.searchString, position, this._state.isRegex, this._state.matchCase, this._state.wholeWord, captureMatches); } if (!nextMatch) { @@ -326,8 +326,8 @@ export class FindModelBoundToEditorModel { return null; } - if (!isRecursed && !searchRange.containsRange(nextMatch)) { - return this._getNextMatch(nextMatch.getEndPosition(), true); + if (!isRecursed && !searchRange.containsRange(nextMatch.range)) { + return this._getNextMatch(nextMatch.range.getEndPosition(), captureMatches, true); } return nextMatch; @@ -337,32 +337,11 @@ export class FindModelBoundToEditorModel { this._moveToNextMatch(this._editor.getSelection().getEndPosition()); } - private getReplaceString(matchRange: Range): string { + private _getReplacePattern(): ReplacePattern { if (this._state.isRegex) { - let searchParams = new SearchParams(this._state.searchString, this._state.isRegex, this._state.matchCase, this._state.wholeWord); - let regExp = searchParams.parseSearchRequest(); - let replacePattern = new ReplacePattern(this._state.replaceString, true, regExp); - let model = this._editor.getModel(); - let matchedString = model.getValueInRange(matchRange); - let replacedString = replacePattern.getReplaceString(matchedString); - // If matched string is not matching then regex pattern has a lookahead expression - if (replacedString === null) { - replacedString = replacePattern.getReplaceString(this._getTextToMatch(matchRange, regExp)); - } - return replacedString; + return parseReplaceString(this._state.replaceString); } - return this._state.replaceString; - } - - private _getTextToMatch(matchRange: Range, regExp: RegExp): string { - let model = this._editor.getModel(); - // If regex is multiline, then return the text from starting of the matching range till end of the model. - if (regExp.multiline) { - let lineCount = model.getLineCount(); - return model.getValueInRange(new Range(matchRange.startLineNumber, matchRange.startColumn, lineCount, model.getLineMaxColumn(lineCount))); - } - // If regex is not multiline, then return the text from starting of the matching range till end of the line. - return model.getValueInRange(new Range(matchRange.startLineNumber, matchRange.startColumn, matchRange.endLineNumber, model.getLineMaxColumn(matchRange.endLineNumber))); + return ReplacePattern.fromStaticValue(this._state.replaceString); } public replace(): void { @@ -370,12 +349,13 @@ export class FindModelBoundToEditorModel { return; } + let replacePattern = this._getReplacePattern(); let selection = this._editor.getSelection(); - let nextMatch = this._getNextMatch(selection.getStartPosition()); + let nextMatch = this._getNextMatch(selection.getStartPosition(), replacePattern.hasReplacementPatterns); if (nextMatch) { - if (selection.equalsRange(nextMatch)) { + if (selection.equalsRange(nextMatch.range)) { // selection sits on a find match => replace it! - let replaceString = this.getReplaceString(selection); + let replaceString = replacePattern.buildReplaceString(nextMatch.matches); let command = new ReplaceCommand(selection, replaceString); @@ -385,14 +365,14 @@ export class FindModelBoundToEditorModel { this.research(true); } else { this._decorations.setStartPosition(this._editor.getPosition()); - this._setCurrentFindMatch(nextMatch); + this._setCurrentFindMatch(nextMatch.range); } } } - private _findMatches(findScope: Range, limitResultCount: number): Range[] { + private _findMatches(findScope: Range, captureMatches: boolean, limitResultCount: number): editorCommon.FindMatch[] { let searchRange = FindModelBoundToEditorModel._getSearchRange(this._editor.getModel(), this._state.isReplaceRevealed, findScope); - return this._editor.getModel().findMatches(this._state.searchString, searchRange, this._state.isRegex, this._state.matchCase, this._state.wholeWord, limitResultCount); + return this._editor.getModel().findMatches(this._state.searchString, searchRange, this._state.isRegex, this._state.matchCase, this._state.wholeWord, captureMatches, limitResultCount); } public replaceAll(): void { @@ -401,16 +381,16 @@ export class FindModelBoundToEditorModel { } let findScope = this._decorations.getFindScope(); - + let replacePattern = this._getReplacePattern(); // Get all the ranges (even more than the highlighted ones) - let ranges = this._findMatches(findScope, Number.MAX_VALUE); + let matches = this._findMatches(findScope, replacePattern.hasReplacementPatterns, Number.MAX_VALUE); let replaceStrings: string[] = []; - for (let i = 0, len = ranges.length; i < len; i++) { - replaceStrings.push(this.getReplaceString(ranges[i])); + for (let i = 0, len = matches.length; i < len; i++) { + replaceStrings[i] = replacePattern.buildReplaceString(matches[i].matches); } - let command = new ReplaceAllCommand(this._editor.getSelection(), ranges, replaceStrings); + let command = new ReplaceAllCommand(this._editor.getSelection(), matches.map(m => m.range), replaceStrings); this._executeEditorCommand('replaceAll', command); this.research(false); @@ -424,8 +404,8 @@ export class FindModelBoundToEditorModel { let findScope = this._decorations.getFindScope(); // Get all the ranges (even more than the highlighted ones) - let ranges = this._findMatches(findScope, Number.MAX_VALUE); - let selections = ranges.map(r => new Selection(r.startLineNumber, r.startColumn, r.endLineNumber, r.endColumn)); + let matches = this._findMatches(findScope, false, Number.MAX_VALUE); + let selections = matches.map(m => new Selection(m.range.startLineNumber, m.range.startColumn, m.range.endLineNumber, m.range.endColumn)); // If one of the ranges is the editor selection, then maintain it as primary let editorSelection = this._editor.getSelection(); diff --git a/src/vs/editor/contrib/find/common/replacePattern.ts b/src/vs/editor/contrib/find/common/replacePattern.ts new file mode 100644 index 00000000000..1a0bba7527e --- /dev/null +++ b/src/vs/editor/contrib/find/common/replacePattern.ts @@ -0,0 +1,266 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * 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 { + + public static fromStaticValue(value: string): ReplacePattern { + return new ReplacePattern([ReplacePiece.staticValue(value)]); + } + + /** + * Assigned when the replace pattern is entirely static. + */ + private readonly _staticValue: string; + + public get hasReplacementPatterns(): boolean { + return this._staticValue === null; + } + + /** + * Assigned when the replace pattern has replacemend patterns. + */ + private readonly _pieces: ReplacePiece[]; + + constructor(pieces: ReplacePiece[]) { + if (!pieces || pieces.length === 0) { + this._staticValue = ''; + this._pieces = null; + } else if (pieces.length === 1 && pieces[0].staticValue !== null) { + this._staticValue = pieces[0].staticValue; + this._pieces = null; + } else { + this._staticValue = null; + this._pieces = pieces; + } + } + + public buildReplaceString(matches: string[]): string { + if (this._staticValue) { + return this._staticValue; + } + + let result = ''; + for (let i = 0, len = this._pieces.length; i < len; i++) { + let piece = this._pieces[i]; + if (piece.staticValue !== null) { + // static value ReplacePiece + result += piece.staticValue; + continue; + } + + // match index ReplacePiece + result += ReplacePattern._substitute(piece.matchIndex, matches); + } + + return result; + } + + private static _substitute(matchIndex: number, matches: string[]): string { + if (matchIndex === 0) { + return matches[0]; + } + + let remainder = ''; + while (matchIndex > 0) { + if (matchIndex < matches.length) { + return matches[matchIndex] + remainder; + } + remainder = String(matchIndex % 10) + remainder; + matchIndex = Math.floor(matchIndex / 10); + } + return '$' + remainder; + } +} + +/** + * A replace piece can either be a static string or an index to a specific match. + */ +export class ReplacePiece { + + public static staticValue(value: string): ReplacePiece { + return new ReplacePiece(value, -1); + } + + public static matchIndex(index: number): ReplacePiece { + return new ReplacePiece(null, index); + } + + public readonly staticValue: string; + public readonly matchIndex: number; + + private constructor(staticValue: string, matchIndex: number) { + this.staticValue = staticValue; + this.matchIndex = matchIndex; + } +} + +class ReplacePieceBuilder { + + private readonly _source: string; + private _lastCharIndex: number; + private readonly _result: ReplacePiece[]; + private _resultLen: number; + private _currentStaticPiece: string; + + constructor(source: string) { + this._source = source; + this._lastCharIndex = 0; + this._result = []; + this._resultLen = 0; + this._currentStaticPiece = ''; + } + + public emitUnchanged(toCharIndex: number): void { + this._emitStatic(this._source.substring(this._lastCharIndex, toCharIndex)); + this._lastCharIndex = toCharIndex; + } + + public emitStatic(value: string, toCharIndex: number): void { + this._emitStatic(value); + this._lastCharIndex = toCharIndex; + } + + private _emitStatic(value: string): void { + if (value.length === 0) { + return; + } + this._currentStaticPiece += value; + } + + public emitMatchIndex(index: number, toCharIndex: number): void { + if (this._currentStaticPiece.length !== 0) { + this._result[this._resultLen++] = ReplacePiece.staticValue(this._currentStaticPiece); + this._currentStaticPiece = ''; + } + this._result[this._resultLen++] = ReplacePiece.matchIndex(index); + this._lastCharIndex = toCharIndex; + } + + + public finalize(): ReplacePattern { + this.emitUnchanged(this._source.length); + if (this._currentStaticPiece.length !== 0) { + this._result[this._resultLen++] = ReplacePiece.staticValue(this._currentStaticPiece); + this._currentStaticPiece = ''; + } + return new ReplacePattern(this._result); + } +} + +/** + * \n => inserts a LF + * \t => inserts a TAB + * \\ => inserts a "\". + * $$ => inserts a "$". + * $& and $0 => inserts the matched substring. + * $n => Where n is a non-negative integer lesser than 100, inserts the nth parenthesized submatch string + * everything else stays untouched + * + * Also see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#Specifying_a_string_as_a_parameter + */ +export function parseReplaceString(replaceString: string): ReplacePattern { + if (!replaceString || replaceString.length === 0) { + return new ReplacePattern(null); + } + + let result = new ReplacePieceBuilder(replaceString); + + for (let i = 0, len = replaceString.length; i < len; i++) { + let chCode = replaceString.charCodeAt(i); + + if (chCode === CharCode.Backslash) { + + // move to next char + i++; + + if (i >= len) { + // string ends with a \ + break; + } + + let nextChCode = replaceString.charCodeAt(i); + // let replaceWithCharacter: string = null; + + switch (nextChCode) { + case CharCode.Backslash: + // \\ => inserts a "\" + result.emitUnchanged(i - 1); + result.emitStatic('\\', i + 1); + break; + case CharCode.n: + // \n => inserts a LF + result.emitUnchanged(i - 1); + result.emitStatic('\n', i + 1); + break; + case CharCode.t: + // \t => inserts a TAB + result.emitUnchanged(i - 1); + result.emitStatic('\t', i + 1); + break; + } + + continue; + } + + if (chCode === CharCode.DollarSign) { + + // move to next char + i++; + + if (i >= len) { + // string ends with a $ + break; + } + + let nextChCode = replaceString.charCodeAt(i); + + if (nextChCode === CharCode.DollarSign) { + // $$ => inserts a "$" + result.emitUnchanged(i - 1); + result.emitStatic('$', i + 1); + continue; + } + + if (nextChCode === CharCode.Digit0 || nextChCode === CharCode.Ampersand) { + // $& and $0 => inserts the matched substring. + result.emitUnchanged(i - 1); + result.emitMatchIndex(0, i + 1); + continue; + } + + if (CharCode.Digit1 <= nextChCode && nextChCode <= CharCode.Digit9) { + // $n + + let matchIndex = nextChCode - CharCode.Digit0; + + // peek next char to probe for $nn + if (i + 1 < len) { + let nextNextChCode = replaceString.charCodeAt(i + 1); + if (CharCode.Digit0 <= nextNextChCode && nextNextChCode <= CharCode.Digit9) { + // $nn + + // move to next char + i++; + matchIndex = matchIndex * 10 + (nextNextChCode - CharCode.Digit0); + + result.emitUnchanged(i - 2); + result.emitMatchIndex(matchIndex, i + 1); + continue; + } + } + + result.emitUnchanged(i - 1); + result.emitMatchIndex(matchIndex, i + 1); + continue; + } + } + } + + return result.finalize(); +} diff --git a/src/vs/editor/contrib/find/test/common/findController.test.ts b/src/vs/editor/contrib/find/test/common/findController.test.ts index 3345b1bd131..21de8afbb8d 100644 --- a/src/vs/editor/contrib/find/test/common/findController.test.ts +++ b/src/vs/editor/contrib/find/test/common/findController.test.ts @@ -350,6 +350,31 @@ suite('FindController', () => { }); }); + test('issue #18111: Regex replace with single space replaces with no space', () => { + withMockCodeEditor([ + 'HRESULT OnAmbientPropertyChange(DISPID dispid);' + ], {}, (editor, cursor) => { + + let findController = editor.registerAndInstantiateContribution(TestFindController); + + let startFindAction = new StartFindAction(); + startFindAction.run(null, editor); + + findController.getState().change({ searchString: '\\b\\s{3}\\b', replaceString: ' ', isRegex: true }, false); + findController.moveToNextMatch(); + + assert.deepEqual(editor.getSelections().map(fromRange), [ + [1, 39, 1, 42] + ]); + + findController.replace(); + + assert.deepEqual(editor.getValue(), 'HRESULT OnAmbientPropertyChange(DISPID dispid);'); + + findController.dispose(); + }); + }); + function toArray(historyNavigator: HistoryNavigator): string[] { let result = []; historyNavigator.first(); diff --git a/src/vs/editor/contrib/find/test/common/replacePattern.test.ts b/src/vs/editor/contrib/find/test/common/replacePattern.test.ts new file mode 100644 index 00000000000..88fcaeeb3aa --- /dev/null +++ b/src/vs/editor/contrib/find/test/common/replacePattern.test.ts @@ -0,0 +1,150 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as assert from 'assert'; +import { parseReplaceString, ReplacePattern, ReplacePiece } from 'vs/editor/contrib/find/common/replacePattern'; + +suite('Replace Pattern test', () => { + + test('parse replace string', () => { + let testParse = (input: string, expectedPieces: ReplacePiece[]) => { + let actual = parseReplaceString(input); + let expected = new ReplacePattern(expectedPieces); + assert.deepEqual(actual, expected, 'Parsing ' + input); + }; + + // no backslash => no treatment + testParse('hello', [ReplacePiece.staticValue('hello')]); + + // \t => TAB + testParse('\\thello', [ReplacePiece.staticValue('\thello')]); + testParse('h\\tello', [ReplacePiece.staticValue('h\tello')]); + testParse('hello\\t', [ReplacePiece.staticValue('hello\t')]); + + // \n => LF + testParse('\\nhello', [ReplacePiece.staticValue('\nhello')]); + + // \\t => \t + testParse('\\\\thello', [ReplacePiece.staticValue('\\thello')]); + testParse('h\\\\tello', [ReplacePiece.staticValue('h\\tello')]); + testParse('hello\\\\t', [ReplacePiece.staticValue('hello\\t')]); + + // \\\t => \TAB + testParse('\\\\\\thello', [ReplacePiece.staticValue('\\\thello')]); + + // \\\\t => \\t + testParse('\\\\\\\\thello', [ReplacePiece.staticValue('\\\\thello')]); + + // \ at the end => no treatment + testParse('hello\\', [ReplacePiece.staticValue('hello\\')]); + + // \ with unknown char => no treatment + testParse('hello\\x', [ReplacePiece.staticValue('hello\\x')]); + + // \ with back reference => no treatment + testParse('hello\\0', [ReplacePiece.staticValue('hello\\0')]); + + testParse('hello$&', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(0)]); + testParse('hello$0', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(0)]); + testParse('hello$02', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(0), ReplacePiece.staticValue('2')]); + testParse('hello$1', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(1)]); + testParse('hello$2', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(2)]); + testParse('hello$9', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(9)]); + testParse('$9hello', [ReplacePiece.matchIndex(9), ReplacePiece.staticValue('hello')]); + + testParse('hello$12', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(12)]); + testParse('hello$99', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(99)]); + testParse('hello$99a', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(99), ReplacePiece.staticValue('a')]); + testParse('hello$1a', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(1), ReplacePiece.staticValue('a')]); + testParse('hello$100', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(10), ReplacePiece.staticValue('0')]); + testParse('hello$100a', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(10), ReplacePiece.staticValue('0a')]); + testParse('hello$10a0', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(10), ReplacePiece.staticValue('a0')]); + testParse('hello$$', [ReplacePiece.staticValue('hello$')]); + testParse('hello$$0', [ReplacePiece.staticValue('hello$0')]); + + testParse('hello$`', [ReplacePiece.staticValue('hello$`')]); + testParse('hello$\'', [ReplacePiece.staticValue('hello$\'')]); + }); + + test('replace has JavaScript semantics', () => { + let testJSReplaceSemantics = (target: string, search: RegExp, replaceString: string, expected: string) => { + let replacePattern = parseReplaceString(replaceString); + let m = search.exec(target); + let actual = replacePattern.buildReplaceString(m); + + assert.deepEqual(actual, expected, `${target}.replace(${search}, ${replaceString})`); + }; + + testJSReplaceSemantics('hi', /hi/, 'hello', 'hi'.replace(/hi/, 'hello')); + testJSReplaceSemantics('hi', /hi/, '\\t', 'hi'.replace(/hi/, '\t')); + testJSReplaceSemantics('hi', /hi/, '\\n', 'hi'.replace(/hi/, '\n')); + testJSReplaceSemantics('hi', /hi/, '\\\\t', 'hi'.replace(/hi/, '\\t')); + testJSReplaceSemantics('hi', /hi/, '\\\\n', 'hi'.replace(/hi/, '\\n')); + + // implicit capture group 0 + testJSReplaceSemantics('hi', /hi/, 'hello$&', 'hi'.replace(/hi/, 'hello$&')); + testJSReplaceSemantics('hi', /hi/, 'hello$0', 'hi'.replace(/hi/, 'hello$&')); + testJSReplaceSemantics('hi', /hi/, 'hello$&1', 'hi'.replace(/hi/, 'hello$&1')); + testJSReplaceSemantics('hi', /hi/, 'hello$01', 'hi'.replace(/hi/, 'hello$&1')); + + // capture groups have funny semantics in replace strings + // the replace string interprets $nn as a captured group only if it exists in the search regex + testJSReplaceSemantics('hi', /(hi)/, 'hello$10', 'hi'.replace(/(hi)/, 'hello$10')); + testJSReplaceSemantics('hi', /(hi)()()()()()()()()()/, 'hello$10', 'hi'.replace(/(hi)()()()()()()()()()/, 'hello$10')); + testJSReplaceSemantics('hi', /(hi)/, 'hello$100', 'hi'.replace(/(hi)/, 'hello$100')); + testJSReplaceSemantics('hi', /(hi)/, 'hello$20', 'hi'.replace(/(hi)/, 'hello$20')); + }); + + test('get replace string if given text is a complete match', () => { + 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); + + assert.equal(actual, expected, `${target}.replace(${search}, ${replaceString}) === ${expected}`); + } + + assertReplace('bla', /bla/, 'hello', 'hello'); + assertReplace('bla', /(bla)/, 'hello', 'hello'); + assertReplace('bla', /(bla)/, 'hello$0', 'hellobla'); + + let searchRegex = /let\s+(\w+)\s*=\s*require\s*\(\s*['"]([\w\.\-/]+)\s*['"]\s*\)\s*/; + assertReplace('let fs = require(\'fs\')', searchRegex, 'import * as $1 from \'$2\';', 'import * as fs from \'fs\';'); + assertReplace('let something = require(\'fs\')', searchRegex, 'import * as $1 from \'$2\';', 'import * as something from \'fs\';'); + assertReplace('let something = require(\'fs\')', searchRegex, 'import * as $1 from \'$1\';', 'import * as something from \'something\';'); + assertReplace('let something = require(\'fs\')', searchRegex, 'import * as $2 from \'$1\';', 'import * as fs from \'something\';'); + assertReplace('let something = require(\'fs\')', searchRegex, 'import * as $0 from \'$0\';', 'import * as let something = require(\'fs\') from \'let something = require(\'fs\')\';'); + assertReplace('let fs = require(\'fs\')', searchRegex, 'import * as $1 from \'$2\';', 'import * as fs from \'fs\';'); + assertReplace('for ()', /for(.*)/, 'cat$1', 'cat ()'); + + // issue #18111 + assertReplace('HRESULT OnAmbientPropertyChange(DISPID dispid);', /\b\s{3}\b/, ' ', ' '); + }); + + test('get replace string if match is sub-string of the text', () => { + 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); + + assert.equal(actual, expected, `${target}.replace(${search}, ${replaceString}) === ${expected}`); + } + assertReplace('this is a bla text', /bla/, 'hello', 'hello'); + assertReplace('this is a bla text', /this(?=.*bla)/, 'that', 'that'); + assertReplace('this is a bla text', /(th)is(?=.*bla)/, '$1at', 'that'); + assertReplace('this is a bla text', /(th)is(?=.*bla)/, '$1e', 'the'); + assertReplace('this is a bla text', /(th)is(?=.*bla)/, '$1ere', 'there'); + assertReplace('this is a bla text', /(th)is(?=.*bla)/, '$1', 'th'); + assertReplace('this is a bla text', /(th)is(?=.*bla)/, 'ma$1', 'math'); + assertReplace('this is a bla text', /(th)is(?=.*bla)/, 'ma$1s', 'maths'); + assertReplace('this is a bla text', /(th)is(?=.*bla)/, '$0', 'this'); + assertReplace('this is a bla text', /(th)is(?=.*bla)/, '$0$1', 'thisth'); + assertReplace('this is a bla text', /bla(?=\stext$)/, 'foo', 'foo'); + assertReplace('this is a bla text', /b(la)(?=\stext$)/, 'f$1', 'fla'); + assertReplace('this is a bla text', /b(la)(?=\stext$)/, 'f$0', 'fbla'); + assertReplace('this is a bla text', /b(la)(?=\stext$)/, '$0ah', 'blaah'); + }); +}); 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/smartSelect/common/jumpToBracket.ts b/src/vs/editor/contrib/smartSelect/common/jumpToBracket.ts deleted file mode 100644 index 6d080e3d84c..00000000000 --- a/src/vs/editor/contrib/smartSelect/common/jumpToBracket.ts +++ /dev/null @@ -1,27 +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 * as nls from 'vs/nls'; -import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { Handler, EditorContextKeys } from 'vs/editor/common/editorCommon'; -import { editorAction, HandlerEditorAction } from 'vs/editor/common/editorCommonExtensions'; - -@editorAction -class SelectBracketAction extends HandlerEditorAction { - constructor() { - super({ - id: 'editor.action.jumpToBracket', - label: nls.localize('smartSelect.jumpBracket', "Go to Bracket"), - alias: 'Go to Bracket', - precondition: null, - handlerId: Handler.JumpToBracket, - kbOpts: { - kbExpr: EditorContextKeys.TextFocus, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_BACKSLASH - } - }); - } -} diff --git a/src/vs/editor/test/common/commands/commandTestUtils.ts b/src/vs/editor/test/common/commands/commandTestUtils.ts index 0d2af66fd57..16ce62a0937 100644 --- a/src/vs/editor/test/common/commands/commandTestUtils.ts +++ b/src/vs/editor/test/common/commands/commandTestUtils.ts @@ -25,7 +25,7 @@ export function testCommand( let model = Model.createFromString(lines.join('\n'), undefined, languageIdentifier); let config = new MockConfiguration(null); - let cursor = new Cursor(0, config, model, viewModelHelper(model), false); + let cursor = new Cursor(config, model, viewModelHelper(model), false); cursor.setSelections('tests', [selection]); diff --git a/src/vs/editor/test/common/commands/sideEditing.test.ts b/src/vs/editor/test/common/commands/sideEditing.test.ts index 2423f5ff0d2..2ff2eb083b8 100644 --- a/src/vs/editor/test/common/commands/sideEditing.test.ts +++ b/src/vs/editor/test/common/commands/sideEditing.test.ts @@ -21,7 +21,7 @@ const NO_TAB_SIZE = 0; function testCommand(lines: string[], selections: Selection[], edits: IIdentifiedSingleEditOperation[], expectedLines: string[], expectedSelections: Selection[]): void { let model = Model.createFromString(lines.join('\n')); let config = new MockConfiguration(null); - let cursor = new Cursor(0, config, model, viewModelHelper(model), false); + let cursor = new Cursor(config, model, viewModelHelper(model), false); cursor.setSelections('tests', selections); diff --git a/src/vs/editor/test/common/controller/cursor.test.ts b/src/vs/editor/test/common/controller/cursor.test.ts index 9ca9b5d1e0b..ef34c8223fb 100644 --- a/src/vs/editor/test/common/controller/cursor.test.ts +++ b/src/vs/editor/test/common/controller/cursor.test.ts @@ -134,7 +134,7 @@ function assertCursor(cursor: Cursor, what: Position | Selection | Selection[]): suite('Editor Controller - Cursor', () => { const LINE1 = ' \tMy First Line\t '; const LINE2 = '\tMy Second Line'; - const LINE3 = ' Third Line💩'; + const LINE3 = ' Third Line🐶'; const LINE4 = ''; const LINE5 = '1'; @@ -152,7 +152,7 @@ suite('Editor Controller - Cursor', () => { thisModel = Model.createFromString(text); thisConfiguration = new MockConfiguration(null); - thisCursor = new Cursor(1, thisConfiguration, thisModel, viewModelHelper(thisModel), false); + thisCursor = new Cursor(thisConfiguration, thisModel, viewModelHelper(thisModel), false); }); teardown(() => { @@ -667,7 +667,7 @@ suite('Editor Controller - Cursor', () => { moveRight(thisCursor, true); moveRight(thisCursor, true); deleteWordLeft(thisCursor); - assert.equal(thisModel.getLineContent(3), ' Thd Line💩'); + assert.equal(thisModel.getLineContent(3), ' Thd Line🐶'); assertCursor(thisCursor, new Position(3, 7)); }); @@ -681,7 +681,7 @@ suite('Editor Controller - Cursor', () => { test('delete word left for caret at end of whitespace', () => { moveTo(thisCursor, 3, 11); deleteWordLeft(thisCursor); - assert.equal(thisModel.getLineContent(3), ' Line💩'); + assert.equal(thisModel.getLineContent(3), ' Line🐶'); assertCursor(thisCursor, new Position(3, 5)); }); @@ -704,7 +704,7 @@ suite('Editor Controller - Cursor', () => { moveRight(thisCursor, true); moveRight(thisCursor, true); deleteWordRight(thisCursor); - assert.equal(thisModel.getLineContent(3), ' Thd Line💩'); + assert.equal(thisModel.getLineContent(3), ' Thd Line🐶'); assertCursor(thisCursor, new Position(3, 7)); }); @@ -718,7 +718,7 @@ suite('Editor Controller - Cursor', () => { test('delete word right for caret at beggining of whitespace', () => { moveTo(thisCursor, 3, 1); deleteWordRight(thisCursor); - assert.equal(thisModel.getLineContent(3), 'Third Line💩'); + assert.equal(thisModel.getLineContent(3), 'Third Line🐶'); assertCursor(thisCursor, new Position(3, 1)); }); @@ -854,7 +854,7 @@ suite('Editor Controller - Cursor', () => { '\t\t}', '\t}' ].join('\n')); - let cursor = new Cursor(1, new MockConfiguration(null), model, viewModelHelper(model), true); + let cursor = new Cursor(new MockConfiguration(null), model, viewModelHelper(model), true); moveTo(cursor, 1, 7, false); assertCursor(cursor, new Position(1, 7)); @@ -888,7 +888,7 @@ suite('Editor Controller - Cursor', () => { 'var concat = require("gulp-concat");', 'var newer = require("gulp-newer");', ].join('\n')); - let cursor = new Cursor(1, new MockConfiguration(null), model, viewModelHelper(model), true); + let cursor = new Cursor(new MockConfiguration(null), model, viewModelHelper(model), true); moveTo(cursor, 1, 4, false); assertCursor(cursor, new Position(1, 4)); @@ -920,7 +920,7 @@ suite('Editor Controller - Cursor', () => { 'var concat = require("gulp-concat");', 'var newer = require("gulp-newer");', ].join('\n')); - let cursor = new Cursor(1, new MockConfiguration(null), model, viewModelHelper(model), true); + let cursor = new Cursor(new MockConfiguration(null), model, viewModelHelper(model), true); moveTo(cursor, 1, 4, false); assertCursor(cursor, new Position(1, 4)); @@ -1211,46 +1211,7 @@ suite('Editor Controller - Regression tests', () => { }); }); - test('issue #183: jump to matching bracket position', () => { - class BracketMode extends MockMode { - private static _id = new LanguageIdentifier('bracketMode', 3); - - constructor() { - super(BracketMode._id); - this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), { - brackets: [ - ['{', '}'], - ['[', ']'], - ['(', ')'], - ] - })); - } - } - - let mode = new BracketMode(); - usingCursor({ - text: [ - 'var x = (3 + (5-7));' - ], - languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { - // ensure is tokenized - model.getLineTokens(1, false); - - moveTo(cursor, 1, 20); - - cursorCommand(cursor, H.JumpToBracket, null, 'keyboard'); - assertCursor(cursor, new Position(1, 10)); - - cursorCommand(cursor, H.JumpToBracket, null, 'keyboard'); - assertCursor(cursor, new Position(1, 20)); - - cursorCommand(cursor, H.JumpToBracket, null, 'keyboard'); - assertCursor(cursor, new Position(1, 10)); - }); - mode.dispose(); - }); test('bug #16543: Tab should indent to correct indentation spot immediately', () => { let mode = new OnEnterMode(IndentAction.Indent); @@ -1493,7 +1454,7 @@ suite('Editor Controller - Regression tests', () => { 'qwerty' ]; let model = Model.createFromString(text.join('\n')); - let cursor = new Cursor(1, new MockConfiguration(null), model, viewModelHelper(model), true); + let cursor = new Cursor(new MockConfiguration(null), model, viewModelHelper(model), true); moveTo(cursor, 2, 1, false); assertCursor(cursor, new Selection(2, 1, 2, 1)); @@ -1511,7 +1472,7 @@ suite('Editor Controller - Regression tests', () => { '' ]; model = Model.createFromString(text.join('\n')); - cursor = new Cursor(1, new MockConfiguration(null), model, viewModelHelper(model), true); + cursor = new Cursor(new MockConfiguration(null), model, viewModelHelper(model), true); moveTo(cursor, 2, 1, false); assertCursor(cursor, new Selection(2, 1, 2, 1)); @@ -2835,7 +2796,7 @@ interface ICursorOpts { function usingCursor(opts: ICursorOpts, callback: (model: Model, cursor: Cursor) => void): void { let model = Model.createFromString(opts.text.join('\n'), opts.modelOpts, opts.languageIdentifier); let config = new MockConfiguration(opts.editorOpts); - let cursor = new Cursor(1, config, model, viewModelHelper(model), false); + let cursor = new Cursor(config, model, viewModelHelper(model), false); callback(model, cursor); diff --git a/src/vs/editor/test/common/controller/cursorMoveCommand.test.ts b/src/vs/editor/test/common/controller/cursorMoveCommand.test.ts index 37372302e53..152e52b1099 100644 --- a/src/vs/editor/test/common/controller/cursorMoveCommand.test.ts +++ b/src/vs/editor/test/common/controller/cursorMoveCommand.test.ts @@ -20,7 +20,7 @@ let H = Handler; suite('Cursor move command test', () => { const LINE1 = ' \tMy First Line\t '; const LINE2 = '\tMy Second Line'; - const LINE3 = ' Third Line💩'; + const LINE3 = ' Third Line🐶'; const LINE4 = ''; const LINE5 = '1'; @@ -456,7 +456,7 @@ suite('Cursor move command test', () => { }); function aCursor(viewModelHelper?: IViewModelHelper): Cursor { - return new Cursor(1, thisConfiguration, thisModel, viewModelHelper || aViewModelHelper(thisModel), false); + return new Cursor(thisConfiguration, thisModel, viewModelHelper || aViewModelHelper(thisModel), false); } }); diff --git a/src/vs/editor/test/common/model/textModelSearch.test.ts b/src/vs/editor/test/common/model/textModelSearch.test.ts index 0b87c282253..e4aaea8e273 100644 --- a/src/vs/editor/test/common/model/textModelSearch.test.ts +++ b/src/vs/editor/test/common/model/textModelSearch.test.ts @@ -330,13 +330,13 @@ suite('TextModelSearch', () => { let searchParams = new SearchParams('line', false, false, false); - let actual = TextModelSearch.findNextMatch(model, searchParams, { lineNumber: 1, column: 1 }, false); + let actual = TextModelSearch.findNextMatch(model, searchParams, new Position(1, 1), false); assertFindMatch(actual, new Range(1, 1, 1, 5)); actual = TextModelSearch.findNextMatch(model, searchParams, actual.range.getEndPosition(), false); assertFindMatch(actual, new Range(1, 6, 1, 10)); - actual = TextModelSearch.findNextMatch(model, searchParams, { lineNumber: 1, column: 3 }, false); + actual = TextModelSearch.findNextMatch(model, searchParams, new Position(1, 3), false); assertFindMatch(actual, new Range(1, 6, 1, 10)); actual = TextModelSearch.findNextMatch(model, searchParams, actual.range.getEndPosition(), false); @@ -353,13 +353,13 @@ suite('TextModelSearch', () => { let searchParams = new SearchParams('^line', true, false, false); - let actual = TextModelSearch.findNextMatch(model, searchParams, { lineNumber: 1, column: 1 }, false); + let actual = TextModelSearch.findNextMatch(model, searchParams, new Position(1, 1), false); assertFindMatch(actual, new Range(1, 1, 1, 5)); actual = TextModelSearch.findNextMatch(model, searchParams, actual.range.getEndPosition(), false); assertFindMatch(actual, new Range(2, 1, 2, 5)); - actual = TextModelSearch.findNextMatch(model, searchParams, { lineNumber: 1, column: 3 }, false); + actual = TextModelSearch.findNextMatch(model, searchParams, new Position(1, 3), false); assertFindMatch(actual, new Range(2, 1, 2, 5)); actual = TextModelSearch.findNextMatch(model, searchParams, actual.range.getEndPosition(), false); @@ -373,13 +373,13 @@ suite('TextModelSearch', () => { let searchParams = new SearchParams('^line', true, false, false); - let actual = TextModelSearch.findNextMatch(model, searchParams, { lineNumber: 1, column: 1 }, false); + let actual = TextModelSearch.findNextMatch(model, searchParams, new Position(1, 1), false); assertFindMatch(actual, new Range(1, 1, 1, 5)); actual = TextModelSearch.findNextMatch(model, searchParams, actual.range.getEndPosition(), false); assertFindMatch(actual, new Range(2, 1, 2, 5)); - actual = TextModelSearch.findNextMatch(model, searchParams, { lineNumber: 1, column: 3 }, false); + actual = TextModelSearch.findNextMatch(model, searchParams, new Position(1, 3), false); assertFindMatch(actual, new Range(2, 1, 2, 5)); actual = TextModelSearch.findNextMatch(model, searchParams, actual.range.getEndPosition(), false); @@ -393,13 +393,13 @@ suite('TextModelSearch', () => { let searchParams = new SearchParams('^line.*\\nline', true, false, false); - let actual = TextModelSearch.findNextMatch(model, searchParams, { lineNumber: 1, column: 1 }, false); + let actual = TextModelSearch.findNextMatch(model, searchParams, new Position(1, 1), false); assertFindMatch(actual, new Range(1, 1, 2, 5)); actual = TextModelSearch.findNextMatch(model, searchParams, actual.range.getEndPosition(), false); assertFindMatch(actual, new Range(3, 1, 4, 5)); - actual = TextModelSearch.findNextMatch(model, searchParams, { lineNumber: 2, column: 1 }, false); + actual = TextModelSearch.findNextMatch(model, searchParams, new Position(2, 1), false); assertFindMatch(actual, new Range(2, 1, 3, 5)); model.dispose(); @@ -410,10 +410,10 @@ suite('TextModelSearch', () => { let searchParams = new SearchParams('line$', true, false, false); - let actual = TextModelSearch.findNextMatch(model, searchParams, { lineNumber: 1, column: 1 }, false); + let actual = TextModelSearch.findNextMatch(model, searchParams, new Position(1, 1), false); assertFindMatch(actual, new Range(1, 10, 1, 14)); - actual = TextModelSearch.findNextMatch(model, searchParams, { lineNumber: 1, column: 4 }, false); + actual = TextModelSearch.findNextMatch(model, searchParams, new Position(1, 4), false); assertFindMatch(actual, new Range(1, 10, 1, 14)); actual = TextModelSearch.findNextMatch(model, searchParams, actual.range.getEndPosition(), false); diff --git a/src/vs/editor/test/common/viewLayout/viewLineParts.test.ts b/src/vs/editor/test/common/viewLayout/viewLineParts.test.ts index 2127f78fe10..e782cd87527 100644 --- a/src/vs/editor/test/common/viewLayout/viewLineParts.test.ts +++ b/src/vs/editor/test/common/viewLayout/viewLineParts.test.ts @@ -5,7 +5,7 @@ 'use strict'; import * as assert from 'assert'; -import { DecorationSegment, LineDecorationsNormalizer, getColumnOfLinePartOffset, createLineParts } from 'vs/editor/common/viewLayout/viewLineParts'; +import { DecorationSegment, LineDecorationsNormalizer, createLineParts } from 'vs/editor/common/viewLayout/viewLineParts'; import { Range } from 'vs/editor/common/core/range'; import { RenderLineInput, renderLine } from 'vs/editor/common/viewLayout/viewLineRenderer'; import { ViewLineToken, ViewLineTokens } from 'vs/editor/common/core/viewLineToken'; @@ -342,7 +342,8 @@ suite('Editor ViewLayout - ViewLineParts', () => { let renderLineOutput = renderLine(new RenderLineInput(lineContent, tabSize, 10, -1, 'none', false, new LineParts(parts, lineContent.length + 1))); return (partIndex: number, partLength: number, offset: number, expected: number) => { - let actual = getColumnOfLinePartOffset(-1, parts, lineContent.length + 1, renderLineOutput.charOffsetInPart, partIndex, partLength, offset); + let charOffset = renderLineOutput.characterMapping.partDataToCharOffset(partIndex, partLength, offset); + let actual = charOffset + 1; assert.equal(actual, expected, 'getColumnOfLinePartOffset for ' + partIndex + ' @ ' + offset); }; } diff --git a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts index 7c8c856ab5c..65333a15943 100644 --- a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts +++ b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts @@ -5,7 +5,7 @@ 'use strict'; import * as assert from 'assert'; -import { renderLine, RenderLineInput } from 'vs/editor/common/viewLayout/viewLineRenderer'; +import { renderLine, RenderLineInput, CharacterMapping } from 'vs/editor/common/viewLayout/viewLineRenderer'; import { ViewLineToken } from 'vs/editor/common/core/viewLineToken'; import { CharCode } from 'vs/base/common/charCode'; import { LineParts } from 'vs/editor/common/core/lineParts'; @@ -16,7 +16,7 @@ suite('viewLineRenderer.renderLine', () => { return new ViewLineToken(startIndex, type); } - function assertCharacterReplacement(lineContent: string, tabSize: number, expected: string, expectedCharOffsetInPart: number[]): void { + function assertCharacterReplacement(lineContent: string, tabSize: number, expected: string, expectedCharOffsetInPart: number[][]): void { let _actual = renderLine(new RenderLineInput( lineContent, tabSize, @@ -28,37 +28,37 @@ suite('viewLineRenderer.renderLine', () => { )); assert.equal(_actual.output, '' + expected + ''); - assert.deepEqual(_actual.charOffsetInPart, expectedCharOffsetInPart); + assertCharacterMapping(_actual.characterMapping, expectedCharOffsetInPart); } test('replaces spaces', () => { - assertCharacterReplacement(' ', 4, ' ', [0, 1]); - assertCharacterReplacement(' ', 4, '  ', [0, 1, 2]); - assertCharacterReplacement('a b', 4, 'a  b', [0, 1, 2, 3, 4]); + assertCharacterReplacement(' ', 4, ' ', [[0, 1]]); + assertCharacterReplacement(' ', 4, '  ', [[0, 1, 2]]); + assertCharacterReplacement('a b', 4, 'a  b', [[0, 1, 2, 3, 4]]); }); test('escapes HTML markup', () => { - assertCharacterReplacement('ab', 4, 'a>b', [0, 1, 2, 3]); - assertCharacterReplacement('a&b', 4, 'a&b', [0, 1, 2, 3]); + assertCharacterReplacement('ab', 4, 'a>b', [[0, 1, 2, 3]]); + assertCharacterReplacement('a&b', 4, 'a&b', [[0, 1, 2, 3]]); }); test('replaces some bad characters', () => { - assertCharacterReplacement('a\0b', 4, 'a�b', [0, 1, 2, 3]); - assertCharacterReplacement('a' + String.fromCharCode(CharCode.UTF8_BOM) + 'b', 4, 'a\ufffdb', [0, 1, 2, 3]); - assertCharacterReplacement('a\u2028b', 4, 'a\ufffdb', [0, 1, 2, 3]); - assertCharacterReplacement('a\rb', 4, 'a​b', [0, 1, 2, 3]); + assertCharacterReplacement('a\0b', 4, 'a�b', [[0, 1, 2, 3]]); + assertCharacterReplacement('a' + String.fromCharCode(CharCode.UTF8_BOM) + 'b', 4, 'a\ufffdb', [[0, 1, 2, 3]]); + assertCharacterReplacement('a\u2028b', 4, 'a\ufffdb', [[0, 1, 2, 3]]); + assertCharacterReplacement('a\rb', 4, 'a​b', [[0, 1, 2, 3]]); }); test('handles tabs', () => { - assertCharacterReplacement('\t', 4, '    ', [0, 4]); - assertCharacterReplacement('x\t', 4, 'x   ', [0, 1, 4]); - assertCharacterReplacement('xx\t', 4, 'xx  ', [0, 1, 2, 4]); - assertCharacterReplacement('xxx\t', 4, 'xxx ', [0, 1, 2, 3, 4]); - assertCharacterReplacement('xxxx\t', 4, 'xxxx    ', [0, 1, 2, 3, 4, 8]); + assertCharacterReplacement('\t', 4, '    ', [[0, 4]]); + assertCharacterReplacement('x\t', 4, 'x   ', [[0, 1, 4]]); + assertCharacterReplacement('xx\t', 4, 'xx  ', [[0, 1, 2, 4]]); + assertCharacterReplacement('xxx\t', 4, 'xxx ', [[0, 1, 2, 3, 4]]); + assertCharacterReplacement('xxxx\t', 4, 'xxxx    ', [[0, 1, 2, 3, 4, 8]]); }); - function assertParts(lineContent: string, tabSize: number, parts: ViewLineToken[], expected: string, expectedCharOffsetInPart: number[]): void { + function assertParts(lineContent: string, tabSize: number, parts: ViewLineToken[], expected: string, expectedCharOffsetInPart: number[][]): void { let _actual = renderLine(new RenderLineInput( lineContent, tabSize, @@ -70,7 +70,7 @@ suite('viewLineRenderer.renderLine', () => { )); assert.equal(_actual.output, '' + expected + ''); - assert.deepEqual(_actual.charOffsetInPart, expectedCharOffsetInPart); + assertCharacterMapping(_actual.characterMapping, expectedCharOffsetInPart); } test('empty line', () => { @@ -78,15 +78,15 @@ suite('viewLineRenderer.renderLine', () => { }); test('uses part type', () => { - assertParts('x', 4, [createPart(0, 'y')], 'x', [0, 1]); - assertParts('x', 4, [createPart(0, 'aAbBzZ0123456789-cC')], 'x', [0, 1]); - assertParts('x', 4, [createPart(0, ' ')], 'x', [0, 1]); + assertParts('x', 4, [createPart(0, 'y')], 'x', [[0, 1]]); + assertParts('x', 4, [createPart(0, 'aAbBzZ0123456789-cC')], 'x', [[0, 1]]); + assertParts('x', 4, [createPart(0, ' ')], 'x', [[0, 1]]); }); test('two parts', () => { - assertParts('xy', 4, [createPart(0, 'a'), createPart(1, 'b')], 'xy', [0, 0, 1]); - assertParts('xyz', 4, [createPart(0, 'a'), createPart(1, 'b')], 'xyz', [0, 0, 1, 2]); - assertParts('xyz', 4, [createPart(0, 'a'), createPart(2, 'b')], 'xyz', [0, 1, 0, 1]); + assertParts('xy', 4, [createPart(0, 'a'), createPart(1, 'b')], 'xy', [[0], [0, 1]]); + assertParts('xyz', 4, [createPart(0, 'a'), createPart(1, 'b')], 'xyz', [[0], [0, 1, 2]]); + assertParts('xyz', 4, [createPart(0, 'a'), createPart(2, 'b')], 'xyz', [[0, 1], [0, 1]]); }); test('overflow', () => { @@ -126,13 +126,13 @@ suite('viewLineRenderer.renderLine', () => { ].join(''); assert.equal(_actual.output, '' + expectedOutput + ''); - assert.deepEqual(_actual.charOffsetInPart, [ - 0, - 0, - 0, - 0, - 0, - 1 + assertCharacterMapping(_actual.characterMapping, [ + [0], + [0], + [0], + [0], + [0], + [1], ]); }); @@ -180,7 +180,6 @@ suite('viewLineRenderer.renderLine', () => { [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], [0, 1, 2, 3, 4, 5], ]; - let expectedOffsets = expectedOffsetsArr.reduce((prev, curr) => prev.concat(curr), []); let _actual = renderLine(new RenderLineInput( lineText, @@ -193,7 +192,7 @@ suite('viewLineRenderer.renderLine', () => { )); assert.equal(_actual.output, '' + expectedOutput + ''); - assert.deepEqual(_actual.charOffsetInPart, expectedOffsets); + assertCharacterMapping(_actual.characterMapping, expectedOffsetsArr); }); test('issue #2255: Weird line rendering part 1', () => { @@ -235,7 +234,6 @@ suite('viewLineRenderer.renderLine', () => { [0], // 1 char [0, 1] // 2 chars ]; - let expectedOffsets = expectedOffsetsArr.reduce((prev, curr) => prev.concat(curr), []); let _actual = renderLine(new RenderLineInput( lineText, @@ -248,7 +246,7 @@ suite('viewLineRenderer.renderLine', () => { )); assert.equal(_actual.output, '' + expectedOutput + ''); - assert.deepEqual(_actual.charOffsetInPart, expectedOffsets); + assertCharacterMapping(_actual.characterMapping, expectedOffsetsArr); }); test('issue #2255: Weird line rendering part 2', () => { @@ -290,7 +288,6 @@ suite('viewLineRenderer.renderLine', () => { [0], // 1 char [0, 1] // 2 chars ]; - let expectedOffsets = expectedOffsetsArr.reduce((prev, curr) => prev.concat(curr), []); let _actual = renderLine(new RenderLineInput( lineText, @@ -303,8 +300,38 @@ suite('viewLineRenderer.renderLine', () => { )); assert.equal(_actual.output, '' + expectedOutput + ''); - assert.deepEqual(_actual.charOffsetInPart, expectedOffsets); + assertCharacterMapping(_actual.characterMapping, expectedOffsetsArr); }); + function assertCharacterMapping(actual: CharacterMapping, expected: number[][]): void { + let charOffset = 0; + for (let partIndex = 0; partIndex < expected.length; partIndex++) { + let part = expected[partIndex]; + for (let i = 0; i < part.length; i++) { + let charIndex = part[i]; + let _actualPartData = actual.charOffsetToPartData(charOffset); + let actualPartIndex = CharacterMapping.getPartIndex(_actualPartData); + let actualCharIndex = CharacterMapping.getCharIndex(_actualPartData); + + assert.deepEqual( + { partIndex: actualPartIndex, charIndex: actualCharIndex }, + { partIndex: partIndex, charIndex: charIndex }, + `character mapping for offset ${charOffset}` + ); + + let actualOffset = actual.partDataToCharOffset(partIndex, part[part.length - 1] + 1, charIndex); + + assert.equal( + actualOffset, + charOffset, + `character mapping for part ${partIndex}, ${charIndex}` + ); + + charOffset++; + } + } + + assert.equal(actual.length, charOffset); + } }); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index f4f46ba3155..b236973e1a6 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -521,6 +521,10 @@ declare module monaco { * If the two positions are equal, the result will be true. */ static isBeforeOrEqual(a: IPosition, b: IPosition): boolean; + /** + * A function that compares positions, useful for sorting + */ + static compare(a: IPosition, b: IPosition): number; /** * Clone this position. */ @@ -2052,10 +2056,11 @@ declare module monaco.editor { * @param isRegex Used to indicate that `searchString` is a regular expression. * @param matchCase Force the matching to match lower/upper case exactly. * @param wholeWord Force the matching to match entire words only. + * @param captureMatches The result will contain the captured groups. * @param limitResultCount Limit the number of results * @return The ranges where the matches are. It is empty if not matches have been found. */ - findMatches(searchString: string, searchOnlyEditableRange: boolean, isRegex: boolean, matchCase: boolean, wholeWord: boolean, limitResultCount?: number): Range[]; + findMatches(searchString: string, searchOnlyEditableRange: boolean, isRegex: boolean, matchCase: boolean, wholeWord: boolean, captureMatches: boolean, limitResultCount?: number): FindMatch[]; /** * Search the model. * @param searchString The string used to search. If it is a regular expression, set `isRegex` to true. @@ -2063,10 +2068,11 @@ declare module monaco.editor { * @param isRegex Used to indicate that `searchString` is a regular expression. * @param matchCase Force the matching to match lower/upper case exactly. * @param wholeWord Force the matching to match entire words only. + * @param captureMatches The result will contain the captured groups. * @param limitResultCount Limit the number of results * @return The ranges where the matches are. It is empty if no matches have been found. */ - findMatches(searchString: string, searchScope: IRange, isRegex: boolean, matchCase: boolean, wholeWord: boolean, limitResultCount?: number): Range[]; + findMatches(searchString: string, searchScope: IRange, isRegex: boolean, matchCase: boolean, wholeWord: boolean, captureMatches: boolean, limitResultCount?: number): FindMatch[]; /** * Search the model for the next match. Loops to the beginning of the model if needed. * @param searchString The string used to search. If it is a regular expression, set `isRegex` to true. @@ -2074,9 +2080,10 @@ declare module monaco.editor { * @param isRegex Used to indicate that `searchString` is a regular expression. * @param matchCase Force the matching to match lower/upper case exactly. * @param wholeWord Force the matching to match entire words only. + * @param captureMatches The result will contain the captured groups. * @return The range where the next match is. It is null if no next match has been found. */ - findNextMatch(searchString: string, searchStart: IPosition, isRegex: boolean, matchCase: boolean, wholeWord: boolean): Range; + findNextMatch(searchString: string, searchStart: IPosition, isRegex: boolean, matchCase: boolean, wholeWord: boolean, captureMatches: boolean): FindMatch; /** * Search the model for the previous match. Loops to the end of the model if needed. * @param searchString The string used to search. If it is a regular expression, set `isRegex` to true. @@ -2084,9 +2091,10 @@ declare module monaco.editor { * @param isRegex Used to indicate that `searchString` is a regular expression. * @param matchCase Force the matching to match lower/upper case exactly. * @param wholeWord Force the matching to match entire words only. + * @param captureMatches The result will contain the captured groups. * @return The range where the previous match is. It is null if no previous match has been found. */ - findPreviousMatch(searchString: string, searchStart: IPosition, isRegex: boolean, matchCase: boolean, wholeWord: boolean): Range; + findPreviousMatch(searchString: string, searchStart: IPosition, isRegex: boolean, matchCase: boolean, wholeWord: boolean, captureMatches: boolean): FindMatch; } export class FindMatch { @@ -3387,7 +3395,6 @@ declare module monaco.editor { ColumnSelect: string; CreateCursor: string; LastCursorMoveToSelect: string; - JumpToBracket: string; Type: string; ReplacePreviousChar: string; CompositionStart: string; @@ -3796,6 +3803,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. @@ -3946,6 +3960,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). */ @@ -4525,6 +4544,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/vscode.d.ts b/src/vs/vscode.d.ts index cbd1ad7f30c..82be13ee78d 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 @@ -4117,6 +4135,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 7a81523517c..d6312342ca5 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -209,6 +209,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 571f357ec09..f16240664ef 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(); } @@ -350,6 +351,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/api/node/mainThreadSaveParticipant.ts b/src/vs/workbench/api/node/mainThreadSaveParticipant.ts index 2a59a90558e..161a59cc558 100644 --- a/src/vs/workbench/api/node/mainThreadSaveParticipant.ts +++ b/src/vs/workbench/api/node/mainThreadSaveParticipant.ts @@ -70,23 +70,21 @@ class TrimWhitespaceParticipant implements INamedSaveParticpant { } function findEditor(model: IModel, codeEditorService: ICodeEditorService): ICommonCodeEditor { + let candidate: ICommonCodeEditor = null; + if (model.isAttachedToEditor()) { - const allEditors = codeEditorService.listCodeEditors(); - for (let i = 0, len = allEditors.length; i < len; i++) { - const editor = allEditors[i]; - const editorModel = editor.getModel(); + for (const editor of codeEditorService.listCodeEditors()) { + if (editor.getModel() === model) { + if (editor.isFocused()) { + return editor; // favour focussed editor if there are multiple + } - if (!editorModel) { - continue; // empty editor - } - - if (model === editorModel) { - return editor; + candidate = editor; } } } - return null; + return candidate; } export class FinalNewLineParticipant implements INamedSaveParticpant { @@ -158,7 +156,7 @@ class FormatOnSaveParticipant implements INamedSaveParticpant { }).then(edits => { if (edits && versionNow === model.getVersionId()) { - const editor = this._findEditor(model); + const editor = findEditor(model, this._editorService); if (editor) { this._editsWithEditor(editor, edits); } else { @@ -194,24 +192,6 @@ class FormatOnSaveParticipant implements INamedSaveParticpant { forceMoveMarkers: true }; } - - private _findEditor(model: IModel) { - if (!model.isAttachedToEditor()) { - return; - } - - let candidate: ICommonCodeEditor; - for (const editor of this._editorService.listCodeEditors()) { - if (editor.getModel() === model) { - if (editor.isFocused()) { - return editor; - } else { - candidate = editor; - } - } - } - return candidate; - } } class ExtHostSaveParticipant implements INamedSaveParticpant { diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index bccd08207a4..4095d49b320 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -96,6 +96,7 @@ Registry.as(EditorExtensions.Editors).registerEditor( interface ISerializedUntitledEditorInput { 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.toString(), 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(URI.parse(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/browser/parts/editor/textDiffEditor.ts b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts index 9fc75180eca..aae84a02f95 100644 --- a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts @@ -32,7 +32,6 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IThemeService } from 'vs/workbench/services/themes/common/themeService'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; /** @@ -53,10 +52,9 @@ export class TextDiffEditor extends BaseTextEditor { @IConfigurationService configurationService: IConfigurationService, @IWorkbenchEditorService private editorService: IWorkbenchEditorService, @IThemeService themeService: IThemeService, - @IEditorGroupService private editorGroupService: IEditorGroupService, - @ITextFileService textFileService: ITextFileService + @IEditorGroupService private editorGroupService: IEditorGroupService ) { - super(TextDiffEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService, textFileService); + super(TextDiffEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService); } public getTitle(): string { diff --git a/src/vs/workbench/browser/parts/editor/textEditor.ts b/src/vs/workbench/browser/parts/editor/textEditor.ts index 090500a91c1..9d62c5667d8 100644 --- a/src/vs/workbench/browser/parts/editor/textEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textEditor.ts @@ -8,21 +8,17 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { Dimension, Builder } from 'vs/base/browser/builder'; import objects = require('vs/base/common/objects'); -import errors = require('vs/base/common/errors'); import types = require('vs/base/common/types'); -import DOM = require('vs/base/browser/dom'); import { CodeEditor } from 'vs/editor/browser/codeEditor'; import { EditorInput, EditorOptions } from 'vs/workbench/common/editor'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { IEditorViewState, IEditor, IEditorOptions, EventType as EditorEventType } from 'vs/editor/common/editorCommon'; +import { IEditorViewState, IEditor, IEditorOptions } from 'vs/editor/common/editorCommon'; import { Position } from 'vs/platform/editor/common/editor'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/workbench/services/themes/common/themeService'; -import { ITextFileService, SaveReason, AutoSaveMode } from 'vs/workbench/services/textfile/common/textfiles'; -import { EventEmitter } from 'vs/base/common/eventEmitter'; import { Scope } from 'vs/workbench/common/memento'; const TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'textEditorViewState'; @@ -46,7 +42,6 @@ export abstract class BaseTextEditor extends BaseEditor { private editorControl: IEditor; private _editorContainer: Builder; private hasPendingConfigurationChange: boolean; - private pendingAutoSave: TPromise; constructor( id: string, @@ -54,8 +49,7 @@ export abstract class BaseTextEditor extends BaseEditor { @IInstantiationService private _instantiationService: IInstantiationService, @IStorageService private storageService: IStorageService, @IConfigurationService private configurationService: IConfigurationService, - @IThemeService private themeService: IThemeService, - @ITextFileService private textFileService: ITextFileService + @IThemeService private themeService: IThemeService ) { super(id, telemetryService); @@ -116,12 +110,6 @@ export abstract class BaseTextEditor extends BaseEditor { // Editor for Text this._editorContainer = parent; this.editorControl = this.createEditorControl(parent, this.computeConfiguration(this.configurationService.getConfiguration())); - - // Application & Editor focus change - if (this.editorControl instanceof EventEmitter) { - this.toUnbind.push(this.editorControl.addListener2(EditorEventType.EditorBlur, () => this.onEditorFocusLost())); - } - this.toUnbind.push(DOM.addDisposableListener(window, DOM.EventType.BLUR, () => this.onWindowFocusLost())); } /** @@ -136,39 +124,6 @@ export abstract class BaseTextEditor extends BaseEditor { return this.instantiationService.createInstance(CodeEditor, parent.getHTMLElement(), configuration); } - private onEditorFocusLost(): void { - if (this.pendingAutoSave) { - return; // save is already triggered - } - - if (this.textFileService.getAutoSaveMode() === AutoSaveMode.ON_FOCUS_CHANGE && this.textFileService.isDirty()) { - this.saveAll(SaveReason.FOCUS_CHANGE); - } - } - - private onWindowFocusLost(): void { - if (this.pendingAutoSave) { - return; // save is already triggered - } - - if (this.textFileService.getAutoSaveMode() === AutoSaveMode.ON_WINDOW_CHANGE && this.textFileService.isDirty()) { - this.saveAll(SaveReason.WINDOW_CHANGE); - } - } - - private saveAll(reason: SaveReason): void { - this.pendingAutoSave = this.textFileService.saveAll(void 0, reason).then(() => { - this.pendingAutoSave = void 0; - - return void 0; - }, error => { - this.pendingAutoSave = void 0; - errors.onUnexpectedError(error); - - return void 0; - }); - } - public setInput(input: EditorInput, options?: EditorOptions): TPromise { return super.setInput(input, options).then(() => { diff --git a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts index 25299ae5adb..851072441d4 100644 --- a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts @@ -20,7 +20,6 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/workbench/services/themes/common/themeService'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; /** @@ -38,10 +37,9 @@ export class TextResourceEditor extends BaseTextEditor { @IConfigurationService configurationService: IConfigurationService, @IThemeService themeService: IThemeService, @IUntitledEditorService private untitledEditorService: IUntitledEditorService, - @IEditorGroupService private editorGroupService: IEditorGroupService, - @ITextFileService textFileService: ITextFileService + @IEditorGroupService private editorGroupService: IEditorGroupService ) { - super(TextResourceEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService, textFileService); + super(TextResourceEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService); this.toUnbind.push(this.untitledEditorService.onDidChangeDirty(e => this.onUntitledDirtyChange(e))); } diff --git a/src/vs/workbench/parts/codeEditor/electron-browser/toggleWordWrap.ts b/src/vs/workbench/parts/codeEditor/electron-browser/toggleWordWrap.ts index b0f5b03817f..a27a7cb5aa8 100644 --- a/src/vs/workbench/parts/codeEditor/electron-browser/toggleWordWrap.ts +++ b/src/vs/workbench/parts/codeEditor/electron-browser/toggleWordWrap.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { ICommonCodeEditor, EditorContextKeys } from 'vs/editor/common/editorCommon'; +import { ICommonCodeEditor } from 'vs/editor/common/editorCommon'; import { editorAction, ServicesAccessor, EditorAction } from 'vs/editor/common/editorCommonExtensions'; import { IConfigurationEditingService, ConfigurationTarget } from 'vs/workbench/services/configuration/common/configurationEditing'; import { IMessageService, Severity } from 'vs/platform/message/common/message'; @@ -21,7 +21,7 @@ class ToggleWordWrapAction extends EditorAction { alias: 'View: Toggle Word Wrap', precondition: null, kbOpts: { - kbExpr: EditorContextKeys.TextFocus, + kbExpr: null, primary: KeyMod.Alt | KeyCode.KEY_Z } }); diff --git a/src/vs/workbench/parts/debug/browser/debugActionItems.ts b/src/vs/workbench/parts/debug/browser/debugActionItems.ts index 473d5697b1f..5fee998bf96 100644 --- a/src/vs/workbench/parts/debug/browser/debugActionItems.ts +++ b/src/vs/workbench/parts/debug/browser/debugActionItems.ts @@ -13,13 +13,17 @@ import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { SelectBox } from 'vs/base/browser/ui/selectBox/selectBox'; import { SelectActionItem, IActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { EventEmitter } from 'vs/base/common/eventEmitter'; +import { ICommonCodeEditor } from 'vs/editor/common/editorCommon'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IDebugService } from 'vs/workbench/parts/debug/common/debug'; +import { IDebugService, EDITOR_CONTRIBUTION_ID, IDebugEditorContribution } from 'vs/workbench/parts/debug/common/debug'; 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; private start: HTMLElement; @@ -45,7 +49,18 @@ export class StartDebugActionItem extends EventEmitter implements IActionItem { } })); this.toDispose.push(this.selectBox.onDidSelect(configurationName => { - this.debugService.getViewModel().setSelectedConfigurationName(configurationName); + if (configurationName === StartDebugActionItem.ADD_CONFIGURATION) { + const manager = this.debugService.getConfigurationManager(); + this.selectBox.select(manager.getConfigurationNames().indexOf(this.debugService.getViewModel().selectedConfigurationName)); + manager.openConfigFile(false).then(editor => { + if (editor) { + const codeEditor = editor.getControl(); + return codeEditor.getContribution(EDITOR_CONTRIBUTION_ID).addLaunchConfiguration(); + } + }); + } else { + this.debugService.getViewModel().setSelectedConfigurationName(configurationName); + } })); } @@ -118,7 +133,9 @@ export class StartDebugActionItem extends EventEmitter implements IActionItem { } else { this.setEnabled(true); const selected = options.indexOf(this.debugService.getViewModel().selectedConfigurationName); - this.selectBox.setOptions(options, selected); + options.push(StartDebugActionItem.SEPARATOR); + options.push(StartDebugActionItem.ADD_CONFIGURATION); + this.selectBox.setOptions(options, selected, options.length - 2); } } } diff --git a/src/vs/workbench/parts/debug/browser/debugActions.ts b/src/vs/workbench/parts/debug/browser/debugActions.ts index 997807ba6e9..2ccd39b54a7 100644 --- a/src/vs/workbench/parts/debug/browser/debugActions.ts +++ b/src/vs/workbench/parts/debug/browser/debugActions.ts @@ -10,7 +10,8 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IDebugService, State, IProcess, SessionRequestType, IThread, IEnablement, IBreakpoint, IStackFrame, IFunctionBreakpoint, IDebugEditorContribution, EDITOR_CONTRIBUTION_ID, IExpression, REPL_ID } +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IDebugService, State, IProcess, SessionRequestType, IThread, IEnablement, IBreakpoint, IStackFrame, IFunctionBreakpoint, IDebugEditorContribution, EDITOR_CONTRIBUTION_ID, IExpression, REPL_ID, IConfig } from 'vs/workbench/parts/debug/common/debug'; import { Variable, Expression, Thread, Breakpoint, Process } from 'vs/workbench/parts/debug/common/debugModel'; import { IPartService } from 'vs/workbench/services/part/common/partService'; @@ -18,7 +19,7 @@ import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { TogglePanelAction } from 'vs/workbench/browser/panel'; -export class AbstractDebugAction extends Action { +export abstract class AbstractDebugAction extends Action { protected toDispose: lifecycle.IDisposable[]; @@ -56,7 +57,7 @@ export class AbstractDebugAction extends Action { } protected isEnabled(state: State): boolean { - return state !== State.Disabled; + return true; } public dispose(): void { @@ -69,7 +70,11 @@ export class ConfigureAction extends AbstractDebugAction { static ID = 'workbench.action.debug.configure'; static LABEL = nls.localize('openLaunchJson', "Open {0}", 'launch.json'); - constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { + constructor(id: string, label: string, + @IDebugService debugService: IDebugService, + @IKeybindingService keybindingService: IKeybindingService, + @IWorkspaceContextService private contextService: IWorkspaceContextService + ) { super(id, label, 'debug-action configure', debugService, keybindingService); this.toDispose.push(debugService.getViewModel().onDidSelectConfiguration(configurationName => this.updateClass())); this.updateClass(); @@ -88,6 +93,10 @@ export class ConfigureAction extends AbstractDebugAction { } public run(event?: any): TPromise { + if (!this.contextService.getWorkspace()) { + return TPromise.as(null); + } + const sideBySide = !!(event && (event.ctrlKey || event.metaKey)); return this.debugService.getConfigurationManager().openConfigFile(sideBySide); } @@ -97,7 +106,12 @@ export class StartAction extends AbstractDebugAction { static ID = 'workbench.action.debug.start'; static LABEL = nls.localize('startDebug', "Start Debugging"); - constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService, @ICommandService private commandService: ICommandService) { + constructor(id: string, label: string, + @IDebugService debugService: IDebugService, + @IKeybindingService keybindingService: IKeybindingService, + @ICommandService private commandService: ICommandService, + @IWorkspaceContextService private contextService: IWorkspaceContextService + ) { super(id, label, 'debug-action start', debugService, keybindingService); this.debugService.getViewModel().onDidSelectConfiguration(() => { this.updateEnablement(); @@ -105,7 +119,19 @@ export class StartAction extends AbstractDebugAction { } public run(): TPromise { - return this.commandService.executeCommand('_workbench.startDebug', this.debugService.getViewModel().selectedConfigurationName); + const manager = this.debugService.getConfigurationManager(); + const configName = this.debugService.getViewModel().selectedConfigurationName; + const configurationPromise: TPromise = configName && this.contextService.getWorkspace() ? + manager.getConfiguration(configName) : TPromise.as(null); + + return configurationPromise.then(configuration => { + const command = manager.getStartSessionCommand(configuration ? configuration.type : undefined); + if (command) { + return this.commandService.executeCommand(command, configuration || {}); + } + + return this.commandService.executeCommand('_workbench.startDebug', configName); + }); } // Disabled if the launch drop down shows the launch config that is already running. @@ -440,7 +466,7 @@ export class ReapplyBreakpointsAction extends AbstractDebugAction { protected isEnabled(state: State): boolean { const model = this.debugService.getModel(); - return super.isEnabled(state) && state !== State.Disabled && state !== State.Inactive && + return super.isEnabled(state) && state !== State.Inactive && (model.getFunctionBreakpoints().length + model.getBreakpoints().length + model.getExceptionBreakpoints().length > 0); } } @@ -682,15 +708,30 @@ export class RunAction extends AbstractDebugAction { static ID = 'workbench.action.debug.run'; static LABEL = nls.localize('startWithoutDebugging', "Start Without Debugging"); - constructor(id: string, label: string, @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService) { + constructor(id: string, label: string, + @IDebugService debugService: IDebugService, + @IKeybindingService keybindingService: IKeybindingService, + @ICommandService private commandService: ICommandService, + @IWorkspaceContextService private contextService: IWorkspaceContextService + ) { super(id, label, null, debugService, keybindingService); } public run(): TPromise { - return this.debugService.getConfigurationManager().getConfiguration(this.debugService.getViewModel().selectedConfigurationName).then(configuration => { + const manager = this.debugService.getConfigurationManager(); + const configName = this.debugService.getViewModel().selectedConfigurationName; + const configurationPromise: TPromise = configName && this.contextService.getWorkspace() ? + manager.getConfiguration(configName) : TPromise.as(null); + + return configurationPromise.then(configuration => { + const command = manager.getStartSessionCommand(configuration ? configuration.type : undefined); + if (command) { + return this.commandService.executeCommand(command, configuration || { noDebug: true }); + } + if (configuration) { configuration.noDebug = true; - return this.debugService.createProcess(configuration); + return this.commandService.executeCommand('_workbench.startDebug', configuration); } }); } diff --git a/src/vs/workbench/parts/debug/browser/debugActionsWidget.ts b/src/vs/workbench/parts/debug/browser/debugActionsWidget.ts index 1bf5bca7589..3ef3fcc3dde 100644 --- a/src/vs/workbench/parts/debug/browser/debugActionsWidget.ts +++ b/src/vs/workbench/parts/debug/browser/debugActionsWidget.ts @@ -156,7 +156,7 @@ export class DebugActionsWidget implements IWorkbenchContribution { private update(): void { const state = this.debugService.state; - if (state === debug.State.Disabled || state === debug.State.Inactive) { + if (state === debug.State.Inactive) { return this.hide(); } 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/browser/debugEditorModelManager.ts b/src/vs/workbench/parts/debug/browser/debugEditorModelManager.ts index 7e2ace18c1f..a93c08da733 100644 --- a/src/vs/workbench/parts/debug/browser/debugEditorModelManager.ts +++ b/src/vs/workbench/parts/debug/browser/debugEditorModelManager.ts @@ -202,7 +202,7 @@ export class DebugEditorModelManager implements IWorkbenchContribution { }); } } - modelData.dirty = this.debugService.state !== State.Inactive && this.debugService.state !== State.Disabled; + modelData.dirty = this.debugService.state !== State.Inactive; const toRemove = this.debugService.getModel().getBreakpoints() .filter(bp => bp.uri.toString() === modelUri.toString()); diff --git a/src/vs/workbench/parts/debug/browser/debugViewlet.ts b/src/vs/workbench/parts/debug/browser/debugViewlet.ts index 08fd97d0b1c..d6976eb653c 100644 --- a/src/vs/workbench/parts/debug/browser/debugViewlet.ts +++ b/src/vs/workbench/parts/debug/browser/debugViewlet.ts @@ -4,9 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/debugViewlet'; -import * as nls from 'vs/nls'; -import * as errors from 'vs/base/common/errors'; -import { $, Builder, Dimension } from 'vs/base/browser/builder'; +import { Builder, Dimension } from 'vs/base/browser/builder'; import { TPromise } from 'vs/base/common/winjs.base'; import * as lifecycle from 'vs/base/common/lifecycle'; import { IAction } from 'vs/base/common/actions'; @@ -23,9 +21,7 @@ import { IProgressService, IProgressRunner } from 'vs/platform/progress/common/p import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import env = require('vs/base/common/platform'); import { Button } from 'vs/base/browser/ui/button/button'; -import { OpenFolderAction, OpenFileFolderAction } from 'vs/workbench/browser/actions/fileActions'; export class DebugViewlet extends Viewlet { @@ -65,40 +61,16 @@ export class DebugViewlet extends Viewlet { super.create(parent); this.$el = parent.div().addClass('debug-viewlet'); - if (this.contextService.hasWorkspace()) { - const actionRunner = this.getActionRunner(); - this.views = DebugViewRegistry.getDebugViews().map(viewConstructor => this.instantiationService.createInstance( - viewConstructor, - actionRunner, - this.viewletSettings) - ); + const actionRunner = this.getActionRunner(); + this.views = DebugViewRegistry.getDebugViews().map(viewConstructor => this.instantiationService.createInstance( + viewConstructor, + actionRunner, + this.viewletSettings) + ); - this.splitView = new SplitView(this.$el.getHTMLElement()); - this.toDispose.push(this.splitView); - this.views.forEach(v => this.splitView.addView(v)); - } else { - const noworkspace = $([ - '
', - '

', nls.localize('noWorkspaceHelp', "You have not yet opened a folder."), '

', - '

', nls.localize('pleaseRestartToDebug', "Open a folder in order to start debugging."), '

', - '
' - ].join('')); - - this.openFolderButton = new Button(noworkspace); - this.openFolderButton.label = nls.localize('openFolder', "Open Folder"); - this.openFolderButton.addListener2('click', () => { - const actionClass = env.isMacintosh ? OpenFileFolderAction : OpenFolderAction; - const action = this.instantiationService.createInstance(actionClass, actionClass.ID, actionClass.LABEL); - this.actionRunner.run(action).done(() => { - action.dispose(); - }, err => { - action.dispose(); - errors.onUnexpectedError(err); - }); - }); - - this.$el.append(noworkspace); - } + this.splitView = new SplitView(this.$el.getHTMLElement()); + this.toDispose.push(this.splitView); + this.views.forEach(v => this.splitView.addView(v)); return TPromise.as(null); } @@ -129,16 +101,13 @@ export class DebugViewlet extends Viewlet { } public getActions(): IAction[] { - if (this.debugService.state === State.Disabled) { - return []; - } - if (!this.actions) { - this.actions = [ - this.instantiationService.createInstance(StartAction, StartAction.ID, StartAction.LABEL), - this.instantiationService.createInstance(ConfigureAction, ConfigureAction.ID, ConfigureAction.LABEL), - this.instantiationService.createInstance(ToggleReplAction, ToggleReplAction.ID, ToggleReplAction.LABEL) - ]; + this.actions = []; + this.actions.push(this.instantiationService.createInstance(StartAction, StartAction.ID, StartAction.LABEL)); + if (this.contextService.getWorkspace()) { + this.actions.push(this.instantiationService.createInstance(ConfigureAction, ConfigureAction.ID, ConfigureAction.LABEL)); + } + this.actions.push(this.instantiationService.createInstance(ToggleReplAction, ToggleReplAction.ID, ToggleReplAction.LABEL)); this.actions.forEach(a => { this.toDispose.push(a); @@ -149,7 +118,7 @@ export class DebugViewlet extends Viewlet { } public getActionItem(action: IAction): IActionItem { - if (action.id === StartAction.ID) { + if (action.id === StartAction.ID && this.contextService.getWorkspace()) { if (!this.startDebugActionItem) { this.startDebugActionItem = this.instantiationService.createInstance(StartDebugActionItem, null, action); } diff --git a/src/vs/workbench/parts/debug/browser/media/debugViewlet.css b/src/vs/workbench/parts/debug/browser/media/debugViewlet.css index 955673c2dab..cff9d97eb53 100644 --- a/src/vs/workbench/parts/debug/browser/media/debugViewlet.css +++ b/src/vs/workbench/parts/debug/browser/media/debugViewlet.css @@ -15,6 +15,10 @@ background: url('configure.svg') center center no-repeat; } +.monaco-workbench .debug-action.start { + background: url('continue.svg') center center no-repeat; +} + .monaco-workbench .debug-action.toggle-repl { background: url('repl.svg') center center no-repeat; } @@ -31,6 +35,11 @@ border: 1px solid white; } +.vs-dark .monaco-workbench .debug-action.start, +.hc-black .monaco-workbench .debug-action.start { + background: url('continue-inverse.svg') center center no-repeat; +} + .vs-dark .monaco-workbench .debug-action.configure, .hc-black .monaco-workbench .debug-action.configure { background: url('configure-inverse.svg') center center no-repeat; diff --git a/src/vs/workbench/parts/debug/common/debug.ts b/src/vs/workbench/parts/debug/common/debug.ts index c665252b3e5..ea687355b40 100644 --- a/src/vs/workbench/parts/debug/common/debug.ts +++ b/src/vs/workbench/parts/debug/common/debug.ts @@ -9,6 +9,7 @@ import Event from 'vs/base/common/event'; import { IJSONSchemaSnippet } from 'vs/base/common/jsonSchema'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IModel as EditorIModel, IEditorContribution, IRange } from 'vs/editor/common/editorCommon'; +import { IEditor } from 'vs/platform/editor/common/editor'; import { Position } from 'vs/editor/common/core/position'; import { ISuggestion } from 'vs/editor/common/modes'; import { Source } from 'vs/workbench/parts/debug/common/debugSource'; @@ -263,7 +264,6 @@ export interface IModel extends ITreeElement { // Debug enums export enum State { - Disabled, Inactive, Initializing, Stopped, @@ -320,6 +320,8 @@ export interface IRawAdapter extends IRawEnvAdapter { configurationAttributes?: any; configurationSnippets?: IJSONSchemaSnippet[]; initialConfigurations?: any[] | string; + startSessionCommand?: string; + languages?: string[]; variables?: { [key: string]: string }; aiKey?: string; win?: IRawEnvAdapter; @@ -352,12 +354,19 @@ export interface IConfigurationManager { /** * Opens the launch.json file */ - openConfigFile(sideBySide: boolean): TPromise; + openConfigFile(sideBySide: boolean): TPromise; /** * Returns true if breakpoints can be set for a given editor model. Depends on mode. */ canSetBreakpointsIn(model: EditorIModel): boolean; + + /** + * Returns a "startSessionCommand" contribution for an adapter with the passed type. + * If no type is specified will try to automatically pick an adapter by looking at + * the active editor language and matching it against the "languages" contribution of an adapter. + */ + getStartSessionCommand(type?: string): string; } // Debug service interfaces @@ -467,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. */ @@ -483,6 +497,7 @@ export interface IDebugEditorContribution extends IEditorContribution { showHover(range: Range, focus: boolean): TPromise; showBreakpointWidget(lineNumber: number): void; closeBreakpointWidget(): void; + addLaunchConfiguration(): TPromise; } // utils diff --git a/src/vs/workbench/parts/debug/common/debugModel.ts b/src/vs/workbench/parts/debug/common/debugModel.ts index 5b97853ef25..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 { @@ -432,10 +432,10 @@ export class Thread implements debug.IThread { return response.body.stackFrames.map((rsf, level) => { if (!rsf) { - return new StackFrame(this, 0, new Source({ name: UNKNOWN_SOURCE_LABEL }, false), nls.localize('unknownStack', "Unknown stack location"), null, null); + return new StackFrame(this, 0, new Source({ name: UNKNOWN_SOURCE_LABEL }, true), nls.localize('unknownStack', "Unknown stack location"), null, null); } - return new StackFrame(this, rsf.id, rsf.source ? new Source(rsf.source) : new Source({ name: UNKNOWN_SOURCE_LABEL }, false), rsf.name, rsf.line, rsf.column); + return new StackFrame(this, rsf.id, rsf.source ? new Source(rsf.source, rsf.source.presentationHint === 'deemphasize') : new Source({ name: UNKNOWN_SOURCE_LABEL }, true), rsf.name, rsf.line, rsf.column); }); }, (err: Error) => { if (this.stoppedDetails) { @@ -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,11 +559,11 @@ 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()) { - stackFrame.source.available = false; + stackFrame.source.deemphasize = true; } }); }); @@ -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/common/debugSource.ts b/src/vs/workbench/parts/debug/common/debugSource.ts index e51a24cfefb..f616063b268 100644 --- a/src/vs/workbench/parts/debug/common/debugSource.ts +++ b/src/vs/workbench/parts/debug/common/debugSource.ts @@ -9,14 +9,12 @@ import { DEBUG_SCHEME } from 'vs/workbench/parts/debug/common/debug'; export class Source { public uri: uri; - public available: boolean; private static INTERNAL_URI_PREFIX = `${DEBUG_SCHEME}://internal/`; - constructor(public raw: DebugProtocol.Source, available = true) { + constructor(public raw: DebugProtocol.Source, public deemphasize: boolean) { const path = raw.path || raw.name; this.uri = raw.sourceReference > 0 ? uri.parse(Source.INTERNAL_URI_PREFIX + raw.sourceReference + '/' + path) : uri.file(path); - this.available = available; } public get name() { diff --git a/src/vs/workbench/parts/debug/electron-browser/debug.contribution.ts b/src/vs/workbench/parts/debug/electron-browser/debug.contribution.ts index 828364cbc40..055036a7108 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debug.contribution.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debug.contribution.ts @@ -134,12 +134,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ handler(accessor: ServicesAccessor, configurationOrName: any) { const debugService = accessor.get(IDebugService); if (!configurationOrName) { - const viewModel = debugService.getViewModel(); - if (!viewModel.selectedConfigurationName) { - const name = debugService.getConfigurationManager().getConfigurationNames().shift(); - viewModel.setSelectedConfigurationName(name); - } - configurationOrName = viewModel.selectedConfigurationName; + configurationOrName = debugService.getViewModel().selectedConfigurationName; } return debugService.createProcess(configurationOrName); @@ -148,6 +143,19 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ primary: undefined }); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'workbench.customDebugRequest', + weight: KeybindingsRegistry.WEIGHT.workbenchContrib(0), + handler(accessor: ServicesAccessor, request: string, requestArgs: any) { + const process = accessor.get(IDebugService).getViewModel().focusedProcess; + if (process) { + return process.session.custom(request, requestArgs); + } + }, + when: CONTEXT_IN_DEBUG_MODE, + primary: undefined +}); + // register service registerSingleton(IDebugService, service.DebugService); diff --git a/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts b/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts index 539ed2b4206..8df36f4bab6 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugConfigurationManager.ts @@ -13,7 +13,8 @@ import uri from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import * as paths from 'vs/base/common/paths'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; -import { IModel } from 'vs/editor/common/editorCommon'; +import { IModel, ICommonCodeEditor } from 'vs/editor/common/editorCommon'; +import { IEditor } from 'vs/platform/editor/common/editor'; import * as extensionsRegistry from 'vs/platform/extensions/common/extensionsRegistry'; import { Registry } from 'vs/platform/platform'; import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; @@ -71,6 +72,14 @@ export const debuggersExtPoint = extensionsRegistry.ExtensionsRegistry.registerE description: nls.localize('vscode.extension.contributes.debuggers.initialConfigurations', "Configurations for generating the initial \'launch.json\'."), type: ['array', 'string'], }, + languages: { + description: nls.localize('vscode.extension.contributes.debuggers.languages', "List of languages for which the debug extension could be considered the \"default debugger\"."), + type: 'array' + }, + startSessionCommand: { + description: nls.localize('vscode.extension.contributes.debuggers.startSessionCommand', "If specified VS Code will call this command for the \"debug\" or \"run\" actions targeted for this extension."), + type: 'string' + }, configurationSnippets: { description: nls.localize('vscode.extension.contributes.debuggers.configurationSnippets', "Snippets for adding new configurations in \'launch.json\'."), type: 'array' @@ -307,6 +316,10 @@ export class ConfigurationManager implements debug.IConfigurationManager { result = objects.deepClone(filtered[0]); } + if (!this.contextService.getWorkspace()) { + return TPromise.as(result); + } + // Set operating system specific properties #1873 const setOSProperties = (flag: boolean, osConfig: debug.IEnvConfig) => { if (flag && osConfig) { @@ -328,7 +341,7 @@ export class ConfigurationManager implements debug.IConfigurationManager { return this.configurationResolverService.resolveInteractiveVariables(result, adapter ? adapter.variables : null); } - public openConfigFile(sideBySide: boolean): TPromise { + public openConfigFile(sideBySide: boolean): TPromise { const resource = uri.file(paths.join(this.contextService.getWorkspace().resource.fsPath, '/.vscode/launch.json')); let configFileCreated = false; @@ -358,7 +371,7 @@ export class ConfigurationManager implements debug.IConfigurationManager { })) .then(errorFree => { if (!errorFree) { - return false; + return undefined; } this.telemetryService.publicLog('debugConfigure'); @@ -369,12 +382,33 @@ export class ConfigurationManager implements debug.IConfigurationManager { pinned: configFileCreated, // pin only if config file is created #8727 revealIfVisible: true }, - }, sideBySide).then(() => true); + }, sideBySide); }, (error) => { throw new Error(nls.localize('DebugConfig.failed', "Unable to create 'launch.json' file inside the '.vscode' folder ({0}).", error)); }); } + public getStartSessionCommand(type?: string): string { + if (type) { + const adapter = this.adapters.filter(a => a.type === type).pop(); + if (adapter) { + return adapter.startSessionCommand; + } + } else { + const editor = this.editorService.getActiveEditor(); + if (editor) { + const model = (editor.getControl()).getModel(); + const language = model ? model.getLanguageIdentifier().language : undefined; + const adapter = this.adapters.filter(a => a.languages && a.languages.indexOf(language) >= 0).pop(); + if (adapter) { + return adapter.startSessionCommand; + } + } + } + + return undefined; + } + public canSetBreakpointsIn(model: IModel): boolean { if (model.uri.scheme === Schemas.inMemory) { return false; diff --git a/src/vs/workbench/parts/debug/electron-browser/debugEditorContribution.ts b/src/vs/workbench/parts/debug/electron-browser/debugEditorContribution.ts index 074af2646d1..824e53dc115 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugEditorContribution.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugEditorContribution.ts @@ -20,6 +20,7 @@ import { IModelDecorationOptions, MouseTargetType, IModelDeltaDecoration, Tracke import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; @@ -53,7 +54,8 @@ export class DebugEditorContribution implements IDebugEditorContribution { @IContextMenuService private contextMenuService: IContextMenuService, @IInstantiationService private instantiationService: IInstantiationService, @IContextKeyService contextKeyService: IContextKeyService, - @ICommandService private commandService: ICommandService + @ICommandService private commandService: ICommandService, + @ITelemetryService private telemetryService: ITelemetryService ) { this.breakpointHintDecoration = []; this.hoverWidget = new DebugHoverWidget(this.editor, this.debugService, this.instantiationService); @@ -281,6 +283,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { } public addLaunchConfiguration(): TPromise { + this.telemetryService.publicLog('debug/addLaunchConfiguration'); let configurationsPosition: IPosition; const model = this.editor.getModel(); let depthInArray = 0; diff --git a/src/vs/workbench/parts/debug/electron-browser/debugService.ts b/src/vs/workbench/parts/debug/electron-browser/debugService.ts index 75c04ff7cde..947db833ebc 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugService.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugService.ts @@ -131,7 +131,7 @@ export class DebugService implements debug.IDebugService { this.toDispose.push(this.windowService.onBroadcast(this.onBroadcast, this)); this.toDispose.push(this.configurationService.onDidUpdateConfiguration((event) => { - if (event.sourceConfig.launch) { + if (event.sourceConfig) { const names = this.configurationManager.getConfigurationNames(); if (names.every(name => name !== this.viewModel.selectedConfigurationName)) { // Current selected configuration no longer exists - take the first configuration instead. @@ -278,7 +278,7 @@ export class DebugService implements debug.IDebugService { thread.fetchCallStack().then(callStack => { if (callStack.length > 0 && !this.viewModel.focusedStackFrame) { // focus first stack frame from top that has source location if no other stack frame is focussed - const stackFrameToFocus = first(callStack, sf => sf.source && sf.source.available, callStack[0]); + const stackFrameToFocus = first(callStack, sf => sf.source && !sf.source.deemphasize, callStack[0]); this.focusStackFrameAndEvaluate(stackFrameToFocus).done(null, errors.onUnexpectedError); this.windowService.getWindow().focus(); aria.alert(nls.localize('debuggingPaused', "Debugging paused, reason {0}, {1} {2}", event.body.reason, stackFrameToFocus.source ? stackFrameToFocus.source.name : '', stackFrameToFocus.lineNumber)); @@ -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); @@ -359,7 +359,7 @@ export class DebugService implements debug.IDebugService { this.toDisposeOnSessionEnd.get(session.getId()).push(session.onDidExitAdapter(event => { // 'Run without debugging' mode VSCode must terminate the extension host. More details: #3905 - if (session && session.configuration.type === 'extensionHost' && this.sessionStates.get(session.getId()) === debug.State.RunningNoDebug) { + if (session && session.configuration.type === 'extensionHost' && this.sessionStates.get(session.getId()) === debug.State.RunningNoDebug && this.contextService.getWorkspace()) { this.windowsService.closeExtensionHostWindow(this.contextService.getWorkspace().resource.fsPath); } if (session && session.getId() === event.body.sessionId) { @@ -426,10 +426,6 @@ export class DebugService implements debug.IDebugService { } public get state(): debug.State { - if (!this.contextService.hasWorkspace()) { - return debug.State.Disabled; - } - const focusedProcess = this.viewModel.focusedProcess; if (focusedProcess) { return this.sessionStates.get(focusedProcess.getId()); @@ -614,6 +610,10 @@ export class DebugService implements debug.IDebugService { }); }); }, err => { + if (!this.contextService.getWorkspace()) { + return this.messageService.show(severity.Error, nls.localize('noFolderWorkspaceDebugError', "The active file can not be debugged. Make sure it is saved on disk and that you have a debug extension installed for that file type.")); + } + return this.configurationManager.openConfigFile(false).then(openend => { if (openend) { this.messageService.show(severity.Info, nls.localize('NewLaunchConfig', "Please set up the launch configuration file for your application. {0}", err.message)); @@ -694,7 +694,7 @@ export class DebugService implements debug.IDebugService { this.panelService.openPanel(debug.REPL_ID, false).done(undefined, errors.onUnexpectedError); } - if (!this.viewModel.changedWorkbenchViewState && this.partService.isVisible(Parts.SIDEBAR_PART)) { + if (!this.viewModel.changedWorkbenchViewState && (this.partService.isVisible(Parts.SIDEBAR_PART) || !this.contextService.getWorkspace())) { // We only want to change the workbench view state on the first debug session #5738 and if the side bar is not hidden this.viewModel.changedWorkbenchViewState = true; this.viewletService.openViewlet(debug.VIEWLET_ID); @@ -738,7 +738,7 @@ export class DebugService implements debug.IDebugService { const configureAction = this.instantiationService.createInstance(debugactions.ConfigureAction, debugactions.ConfigureAction.ID, debugactions.ConfigureAction.LABEL); const actions = (error.actions && error.actions.length) ? error.actions.concat([configureAction]) : [CloseAction, configureAction]; - return TPromise.wrapError(errors.create(error.message, { actions })); + this.messageService.show(severity.Error, { message: errorMessage, actions }); }); }); } @@ -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); @@ -920,7 +924,7 @@ export class DebugService implements debug.IDebugService { return session.setBreakpoints({ source: rawSource, lines: breakpointsToSend.map(bp => bp.lineNumber), - breakpoints: breakpointsToSend.map(bp => ({ line: bp.lineNumber, condition: bp.condition, hitCondition: bp.hitCondition, column: bp.column })), + breakpoints: breakpointsToSend.map(bp => ({ line: bp.lineNumber, condition: bp.condition, hitCondition: bp.hitCondition })), sourceModified }).then(response => { if (!response || !response.body) { diff --git a/src/vs/workbench/parts/debug/electron-browser/debugViewer.ts b/src/vs/workbench/parts/debug/electron-browser/debugViewer.ts index bc4659c5892..4cba885380f 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugViewer.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugViewer.ts @@ -48,6 +48,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; @@ -76,8 +81,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; } @@ -88,7 +92,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 : ''; } @@ -553,7 +557,7 @@ export class CallStackRenderer implements IRenderer { } private renderStackFrame(stackFrame: debug.IStackFrame, data: IStackFrameTemplateData): void { - stackFrame.source.available ? dom.removeClass(data.stackFrame, 'disabled') : dom.addClass(data.stackFrame, 'disabled'); + stackFrame.source.deemphasize ? dom.addClass(data.stackFrame, 'disabled') : dom.removeClass(data.stackFrame, 'disabled'); data.file.title = stackFrame.source.raw.path || stackFrame.source.name; data.label.textContent = stackFrame.name; data.label.title = stackFrame.name; 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/node/debugAdapter.ts b/src/vs/workbench/parts/debug/node/debugAdapter.ts index 13bef462821..dbedd8d6574 100644 --- a/src/vs/workbench/parts/debug/node/debugAdapter.ts +++ b/src/vs/workbench/parts/debug/node/debugAdapter.ts @@ -76,6 +76,14 @@ export class Adapter { return this.rawAdapter.configurationSnippets; } + public get languages(): string[] { + return this.rawAdapter.languages; + } + + public get startSessionCommand(): string { + return this.rawAdapter.startSessionCommand; + } + public merge(secondRawAdapter: IRawAdapter, extensionDescription: IExtensionDescription): void { // Give priority to built in debug adapters if (extensionDescription.isBuiltin) { diff --git a/src/vs/workbench/parts/debug/test/common/debugSource.test.ts b/src/vs/workbench/parts/debug/test/common/debugSource.test.ts index ae95f3710e2..890b93aa851 100644 --- a/src/vs/workbench/parts/debug/test/common/debugSource.test.ts +++ b/src/vs/workbench/parts/debug/test/common/debugSource.test.ts @@ -15,9 +15,9 @@ suite('Debug - Source', () => { path: '/xx/yy/zz', sourceReference: 0 }; - const source = new Source(rawSource); + const source = new Source(rawSource, false); - assert.equal(source.available, true); + assert.equal(source.deemphasize, false); assert.equal(source.name, rawSource.name); assert.equal(source.inMemory, false); assert.equal(source.reference, rawSource.sourceReference); @@ -30,9 +30,9 @@ suite('Debug - Source', () => { name: 'internalModule.js', sourceReference: 11 }; - const source = new Source(rawSource); + const source = new Source(rawSource, true); - assert.equal(source.available, true); + assert.equal(source.deemphasize, true); assert.equal(source.name, rawSource.name); assert.equal(source.inMemory, true); assert.equal(source.reference, rawSource.sourceReference); 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/editors/textFileEditor.ts b/src/vs/workbench/parts/files/browser/editors/textFileEditor.ts index 954cea1054f..aaf7058fc30 100644 --- a/src/vs/workbench/parts/files/browser/editors/textFileEditor.ts +++ b/src/vs/workbench/parts/files/browser/editors/textFileEditor.ts @@ -13,7 +13,7 @@ import paths = require('vs/base/common/paths'); import { IEditorOptions } from 'vs/editor/common/editorCommon'; import { Action } from 'vs/base/common/actions'; import { VIEWLET_ID, TEXT_FILE_EDITOR_ID } from 'vs/workbench/parts/files/common/files'; -import { ITextFileEditorModel, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; import { EditorOptions, TextEditorOptions } from 'vs/workbench/common/editor'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; @@ -50,10 +50,9 @@ export class TextFileEditor extends BaseTextEditor { @IConfigurationService configurationService: IConfigurationService, @IWorkbenchEditorService private editorService: IWorkbenchEditorService, @IThemeService themeService: IThemeService, - @IEditorGroupService private editorGroupService: IEditorGroupService, - @ITextFileService textFileService: ITextFileService + @IEditorGroupService private editorGroupService: IEditorGroupService ) { - super(TextFileEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService, textFileService); + super(TextFileEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService); // Clear view state for deleted files this.toUnbind.push(this.fileService.onFileChanges(e => this.onFilesChanged(e))); diff --git a/src/vs/workbench/parts/files/browser/files.contribution.ts b/src/vs/workbench/parts/files/browser/files.contribution.ts index 30350bebb0a..891d5796745 100644 --- a/src/vs/workbench/parts/files/browser/files.contribution.ts +++ b/src/vs/workbench/parts/files/browser/files.contribution.ts @@ -106,6 +106,7 @@ Registry.as(EditorExtensions.Editors).registerDefaultFileInput( interface ISerializedFileInput { 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().toString() + 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, URI.parse(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/saveErrorHandler.ts b/src/vs/workbench/parts/files/browser/saveErrorHandler.ts index 1e716709f59..514bd7a8b3a 100644 --- a/src/vs/workbench/parts/files/browser/saveErrorHandler.ts +++ b/src/vs/workbench/parts/files/browser/saveErrorHandler.ts @@ -201,7 +201,7 @@ class ResolveSaveConflictMessage implements IMessageWithAction { const name = paths.basename(resource.fsPath); const editorLabel = nls.localize('saveConflictDiffLabel', "{0} (on disk) ↔ {1} (in {2}) - Resolve save conflict", name, name, this.environmentService.appNameLong); - return this.editorService.openEditor({ leftResource: URI.from({ scheme: CONFLICT_RESOLUTION_SCHEME, path: resource.fsPath }), rightResource: resource, label: editorLabel }).then(() => { + return this.editorService.openEditor({ leftResource: URI.from({ scheme: CONFLICT_RESOLUTION_SCHEME, path: resource.fsPath }), rightResource: resource, label: editorLabel, options: { pinned: true } }).then(() => { // We have to bring the model into conflict resolution mode to prevent subsequent save erros when the user makes edits this.model.setConflictResolutionMode(); 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/outputPanel.ts b/src/vs/workbench/parts/output/browser/outputPanel.ts index 9a1b48d3f51..8ccda82ca42 100644 --- a/src/vs/workbench/parts/output/browser/outputPanel.ts +++ b/src/vs/workbench/parts/output/browser/outputPanel.ts @@ -22,7 +22,6 @@ import { OutputEditors, OUTPUT_PANEL_ID, IOutputService, CONTEXT_IN_OUTPUT } fro import { SwitchOutputAction, SwitchOutputActionItem, ClearOutputAction } from 'vs/workbench/parts/output/browser/outputActions'; import { IThemeService } from 'vs/workbench/services/themes/common/themeService'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; export class OutputPanel extends TextResourceEditor { @@ -39,10 +38,9 @@ export class OutputPanel extends TextResourceEditor { @IOutputService private outputService: IOutputService, @IUntitledEditorService untitledEditorService: IUntitledEditorService, @IContextKeyService private contextKeyService: IContextKeyService, - @IEditorGroupService editorGroupService: IEditorGroupService, - @ITextFileService textFileService: ITextFileService + @IEditorGroupService editorGroupService: IEditorGroupService ) { - super(telemetryService, instantiationService, storageService, configurationService, themeService, untitledEditorService, editorGroupService, textFileService); + super(telemetryService, instantiationService, storageService, configurationService, themeService, untitledEditorService, editorGroupService); this.scopedInstantiationService = instantiationService; this.toDispose = []; 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/preferences/browser/preferencesEditor.ts b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts index 3b1e00ae280..79362c54d96 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts @@ -43,7 +43,6 @@ import { IThemeService } from 'vs/workbench/services/themes/common/themeService' import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IMessageService, Severity } from 'vs/platform/message/common/message'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -113,10 +112,9 @@ export class DefaultPreferencesEditor extends BaseTextEditor { @IUntitledEditorService private untitledEditorService: IUntitledEditorService, @IPreferencesService private preferencesService: IPreferencesService, @IModelService private modelService: IModelService, - @IModeService private modeService: IModeService, - @ITextFileService textFileService: ITextFileService + @IModeService private modeService: IModeService ) { - super(DefaultPreferencesEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService, textFileService); + super(DefaultPreferencesEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService); this.delayedFilterLogging = new Delayer(1000); } 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/search/common/searchModel.ts b/src/vs/workbench/parts/search/common/searchModel.ts index e2ba6cde2ed..64a37ed9bf1 100644 --- a/src/vs/workbench/parts/search/common/searchModel.ts +++ b/src/vs/workbench/parts/search/common/searchModel.ts @@ -18,7 +18,7 @@ import { ISearchService, ISearchProgressItem, ISearchComplete, ISearchQuery, IPa import { ReplacePattern } from 'vs/platform/search/common/replace'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { Range } from 'vs/editor/common/core/range'; -import { IModel, IModelDeltaDecoration, OverviewRulerLane, TrackedRangeStickiness, IModelDecorationOptions } from 'vs/editor/common/editorCommon'; +import { IModel, IModelDeltaDecoration, OverviewRulerLane, TrackedRangeStickiness, IModelDecorationOptions, FindMatch } from 'vs/editor/common/editorCommon'; import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IReplaceService } from 'vs/workbench/parts/search/common/replace'; @@ -180,7 +180,7 @@ export class FileMatch extends Disposable { } this._matches = new LinkedMap(); let matches = this._model - .findMatches(this._query.pattern, this._model.getFullModelRange(), this._query.isRegExp, this._query.isCaseSensitive, this._query.isWordMatch); + .findMatches(this._query.pattern, this._model.getFullModelRange(), this._query.isRegExp, this._query.isCaseSensitive, this._query.isWordMatch, false); this.updateMatches(matches); } @@ -195,13 +195,13 @@ export class FileMatch extends Disposable { const oldMatches = this._matches.values().filter(match => match.range().startLineNumber === lineNumber); oldMatches.forEach(match => this._matches.delete(match.id())); - const matches = this._model.findMatches(this._query.pattern, range, this._query.isRegExp, this._query.isCaseSensitive, this._query.isWordMatch); + const matches = this._model.findMatches(this._query.pattern, range, this._query.isRegExp, this._query.isCaseSensitive, this._query.isWordMatch, false); this.updateMatches(matches); } - private updateMatches(matches: Range[]) { - matches.forEach(range => { - let match = new Match(this, this._model.getLineContent(range.startLineNumber), range.startLineNumber - 1, range.startColumn - 1, range.endColumn - range.startColumn); + private updateMatches(matches: FindMatch[]) { + matches.forEach(m => { + let match = new Match(this, this._model.getLineContent(m.range.startLineNumber), m.range.startLineNumber - 1, m.range.startColumn - 1, m.range.endColumn - m.range.startColumn); if (!this._removedMatches.contains(match.id())) { this.add(match); if (this.isMatchSelected(match)) { diff --git a/src/vs/workbench/parts/snippets/electron-browser/snippetsTracker.ts b/src/vs/workbench/parts/snippets/electron-browser/snippetsTracker.ts index 55ccb3ff1a3..7474861a1a7 100644 --- a/src/vs/workbench/parts/snippets/electron-browser/snippetsTracker.ts +++ b/src/vs/workbench/parts/snippets/electron-browser/snippetsTracker.ts @@ -16,6 +16,7 @@ import lifecycle = require('vs/base/common/lifecycle'); import { readAndRegisterSnippets } from 'vs/editor/node/textMate/TMSnippets'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IExtensionService } from 'vs/platform/extensions/common/extensions'; import { watch, FSWatcher } from 'fs'; import { IModeService } from 'vs/editor/common/services/modeService'; @@ -30,14 +31,16 @@ export class SnippetsTracker implements workbenchExt.IWorkbenchContribution { constructor( @ILifecycleService private lifecycleService: ILifecycleService, @IModeService private modeService: IModeService, - @IEnvironmentService environmentService: IEnvironmentService + @IEnvironmentService environmentService: IEnvironmentService, + @IExtensionService extensionService: IExtensionService ) { this.snippetFolder = paths.join(environmentService.appSettingsHome, 'snippets'); this.toDispose = []; this.fileWatchDelayer = new async.ThrottledDelayer(SnippetsTracker.FILE_WATCH_DELAY); - mkdirp(this.snippetFolder) + extensionService.onReady() + .then(() => mkdirp(this.snippetFolder)) .then(() => this.scanUserSnippets()) .then(() => this.registerListeners()) .done(undefined, onUnexpectedError); diff --git a/src/vs/workbench/parts/terminal/common/terminal.ts b/src/vs/workbench/parts/terminal/common/terminal.ts index 6be089c31a7..e03abc7ba4d 100644 --- a/src/vs/workbench/parts/terminal/common/terminal.ts +++ b/src/vs/workbench/parts/terminal/common/terminal.ts @@ -234,4 +234,20 @@ export interface ITerminalInstance { * @param visible Whether the element is visible. */ setVisible(visible: boolean): void; + + /** + * Attach a listener to the data stream from the terminal's pty process. + * + * @param listener The listener function which takes the processes' data stream (including + * ANSI escape sequences). + */ + onData(listener: (data: string) => void): void; + + /** + * Attach a listener that fires when the terminal's pty process exits. + * + * @param listener The listener function which takes the processes' exit code, an exit code of + * null means the process was killed as a result of the ITerminalInstance being disposed. + */ + onExit(listener: (exitCode: number) => void): void; } diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts index d97c72fc963..de119ecdc7f 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts @@ -426,6 +426,18 @@ export class TerminalInstance implements ITerminalInstance { return env; } + public onData(listener: (data: string) => void): void { + this._process.on('message', (message) => { + if (message.type === 'data') { + listener(message.content); + } + }); + } + + public onExit(listener: (exitCode: number) => void): void { + this._process.on('exit', listener); + } + private static _sanitizeCwd(cwd: string) { // Make the drive letter uppercase on Windows (see #9448) if (platform.platform === platform.Platform.Windows && cwd && cwd[1] === ':') { diff --git a/src/vs/workbench/parts/update/electron-browser/update.ts b/src/vs/workbench/parts/update/electron-browser/update.ts index c537d235a44..93644682b12 100644 --- a/src/vs/workbench/parts/update/electron-browser/update.ts +++ b/src/vs/workbench/parts/update/electron-browser/update.ts @@ -137,7 +137,7 @@ export abstract class AbstractShowReleaseNotesAction extends Action { this.enabled = false; return this.instantiationService.invokeFunction(loadReleaseNotes, this.version) - .then(text => this.editorService.openEditor(this.instantiationService.createInstance(ReleaseNotesInput, this.version, text))) + .then(text => this.editorService.openEditor(this.instantiationService.createInstance(ReleaseNotesInput, this.version, text), { pinned: true })) .then(() => true) .then(null, () => { const action = this.instantiationService.createInstance(OpenLatestReleaseNotesInBrowserAction); @@ -207,7 +207,7 @@ export class UpdateContribution implements IWorkbenchContribution { if (product.releaseNotesUrl && lastVersion && pkg.version !== lastVersion) { instantiationService.invokeFunction(loadReleaseNotes, pkg.version) .then( - text => editorService.openEditor(instantiationService.createInstance(ReleaseNotesInput, pkg.version, text)), + text => editorService.openEditor(instantiationService.createInstance(ReleaseNotesInput, pkg.version, text), { pinned: true }), () => { messageService.show(Severity.Info, { message: nls.localize('read the release notes', "Welcome to {0} v{1}! Would you like to read the Release Notes?", product.nameLong, pkg.version), diff --git a/src/vs/workbench/services/configuration/test/node/configurationEditingService.test.ts b/src/vs/workbench/services/configuration/test/node/configurationEditingService.test.ts index 193e17db95b..2d45970844f 100644 --- a/src/vs/workbench/services/configuration/test/node/configurationEditingService.test.ts +++ b/src/vs/workbench/services/configuration/test/node/configurationEditingService.test.ts @@ -36,6 +36,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IMessageService } from 'vs/platform/message/common/message'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { IWindowsService } from 'vs/platform/windows/common/windows'; +import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; class SettingsTestEnvironmentService extends EnvironmentService { @@ -60,9 +61,10 @@ class TestDirtyTextFileService extends TestTextFileService { @IInstantiationService instantiationService: IInstantiationService, @IMessageService messageService: IMessageService, @IBackupFileService backupFileService: IBackupFileService, - @IWindowsService windowsService: IWindowsService + @IWindowsService windowsService: IWindowsService, + @IEditorGroupService editorGroupService: IEditorGroupService ) { - super(lifecycleService, contextService, configurationService, telemetryService, editorService, fileService, untitledEditorService, instantiationService, messageService, backupFileService, windowsService); + super(lifecycleService, contextService, configurationService, telemetryService, editorService, fileService, untitledEditorService, instantiationService, messageService, backupFileService, windowsService, editorGroupService); } public isDirty(resource?: URI): boolean { diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts index d9172fd481d..9632a7a7985 100644 --- a/src/vs/workbench/services/files/node/fileService.ts +++ b/src/vs/workbench/services/files/node/fileService.ts @@ -11,7 +11,7 @@ import os = require('os'); import crypto = require('crypto'); import assert = require('assert'); -import { FileOperation, FileOperationEvent, IContent, IFileService, IResolveFileOptions, IResolveContentOptions, IFileStat, IStreamContent, IFileOperationResult, FileOperationResult, IBaseStat, IUpdateContentOptions, FileChangeType, IImportResult, MAX_FILE_SIZE, FileChangesEvent } from 'vs/platform/files/common/files'; +import { isEqual, isParent, FileOperation, FileOperationEvent, IContent, IFileService, IResolveFileOptions, IResolveContentOptions, IFileStat, IStreamContent, IFileOperationResult, FileOperationResult, IBaseStat, IUpdateContentOptions, FileChangeType, IImportResult, MAX_FILE_SIZE, FileChangesEvent } from 'vs/platform/files/common/files'; import strings = require('vs/base/common/strings'); import arrays = require('vs/base/common/arrays'); import baseMime = require('vs/base/common/mime'); @@ -865,7 +865,7 @@ export class StatResolver { let resolveFolderChildren = false; if (files.length === 1 && resolveSingleChildDescendants) { resolveFolderChildren = true; - } else if (childCount > 0 && absoluteTargetPaths && absoluteTargetPaths.some(targetPath => basePaths.isEqualOrParent(targetPath, fileResource.fsPath))) { + } else if (childCount > 0 && absoluteTargetPaths && absoluteTargetPaths.some(targetPath => isEqual(targetPath, fileResource.fsPath) || isParent(targetPath, fileResource.fsPath))) { resolveFolderChildren = true; } diff --git a/src/vs/workbench/services/files/node/watcher/unix/chokidarWatcherService.ts b/src/vs/workbench/services/files/node/watcher/unix/chokidarWatcherService.ts index 27c93f4e80b..376f9541f74 100644 --- a/src/vs/workbench/services/files/node/watcher/unix/chokidarWatcherService.ts +++ b/src/vs/workbench/services/files/node/watcher/unix/chokidarWatcherService.ts @@ -56,26 +56,17 @@ export class ChokidarWatcherService implements IWatcherService { // Change if (type === 'change') { - event = { - type: 0, - path: path - }; + event = { type: 0, path }; } // Add else if (type === 'add' || type === 'addDir') { - event = { - type: 1, - path: path - }; + event = { type: 1, path }; } // Delete else if (type === 'unlink' || type === 'unlinkDir') { - event = { - type: 2, - path: path - }; + event = { type: 2, path }; } if (event) { diff --git a/src/vs/workbench/services/history/browser/history.ts b/src/vs/workbench/services/history/browser/history.ts index 2a7d7a09c24..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: string; + 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.toString() }; + 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: URI.parse(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/search/node/searchService.ts b/src/vs/workbench/services/search/node/searchService.ts index 123d2bc5497..9ea10c7de73 100644 --- a/src/vs/workbench/services/search/node/searchService.ts +++ b/src/vs/workbench/services/search/node/searchService.ts @@ -139,13 +139,13 @@ export class SearchService implements ISearchService { } // Use editor API to find matches - let ranges = model.findMatches(query.contentPattern.pattern, false, query.contentPattern.isRegExp, query.contentPattern.isCaseSensitive, query.contentPattern.isWordMatch); - if (ranges.length) { + let matches = model.findMatches(query.contentPattern.pattern, false, query.contentPattern.isRegExp, query.contentPattern.isCaseSensitive, query.contentPattern.isWordMatch, false); + if (matches.length) { let fileMatch = new FileMatch(resource); localResults[resource.toString()] = fileMatch; - ranges.forEach((range) => { - fileMatch.lineMatches.push(new LineMatch(model.getLineContent(range.startLineNumber), range.startLineNumber - 1, [[range.startColumn - 1, range.endColumn - range.startColumn]])); + matches.forEach((match) => { + fileMatch.lineMatches.push(new LineMatch(model.getLineContent(match.range.startLineNumber), match.range.startLineNumber - 1, [[match.range.startColumn - 1, match.range.endColumn - match.range.startColumn]])); }); } else { localResults[resource.toString()] = false; // flag as empty result diff --git a/src/vs/workbench/services/textfile/common/textFileService.ts b/src/vs/workbench/services/textfile/common/textFileService.ts index d622ec34313..eac56436670 100644 --- a/src/vs/workbench/services/textfile/common/textFileService.ts +++ b/src/vs/workbench/services/textfile/common/textFileService.ts @@ -10,12 +10,14 @@ import URI from 'vs/base/common/uri'; import paths = require('vs/base/common/paths'); import errors = require('vs/base/common/errors'); import objects = require('vs/base/common/objects'); +import DOM = require('vs/base/browser/dom'); import Event, { Emitter } from 'vs/base/common/event'; import platform = require('vs/base/common/platform'); import { IWindowsService } from 'vs/platform/windows/common/windows'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { IRevertOptions, IResult, ITextFileOperationResult, ITextFileService, IRawTextContent, IAutoSaveConfiguration, AutoSaveMode, SaveReason, ITextFileEditorModelManager, ITextFileEditorModel, ISaveOptions } from 'vs/workbench/services/textfile/common/textfiles'; import { ConfirmResult } from 'vs/workbench/common/editor'; +import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; import { ILifecycleService, ShutdownReason } from 'vs/platform/lifecycle/common/lifecycle'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IFileService, IResolveContentOptions, IFilesConfiguration, IFileOperationResult, FileOperationResult, AutoSaveConfiguration } from 'vs/platform/files/common/files'; @@ -66,6 +68,7 @@ export abstract class TextFileService implements ITextFileService { @IMessageService private messageService: IMessageService, @IEnvironmentService protected environmentService: IEnvironmentService, @IBackupFileService private backupFileService: IBackupFileService, + @IEditorGroupService private editorGroupService: IEditorGroupService, @IWindowsService private windowsService: IWindowsService ) { this.toUnbind = []; @@ -116,6 +119,11 @@ export abstract class TextFileService implements ITextFileService { // Configuration changes this.toUnbind.push(this.configurationService.onDidUpdateConfiguration(e => this.onConfigurationChange(e.config))); + + // Application & Editor focus change + this.toUnbind.push(DOM.addDisposableListener(window, DOM.EventType.BLUR, () => this.onWindowFocusLost())); + this.toUnbind.push(DOM.addDisposableListener(window, DOM.EventType.BLUR, () => this.onEditorFocusChanged(), true)); + this.toUnbind.push(this.editorGroupService.onEditorsChanged(() => this.onEditorFocusChanged())); } private beforeShutdown(reason: ShutdownReason): boolean | TPromise { @@ -287,6 +295,18 @@ export abstract class TextFileService implements ITextFileService { return this.backupFileService.discardAllWorkspaceBackups(); } + private onWindowFocusLost(): void { + if (this.configuredAutoSaveOnWindowChange && this.isDirty()) { + this.saveAll(void 0, SaveReason.WINDOW_CHANGE).done(null, errors.onUnexpectedError); + } + } + + private onEditorFocusChanged(): void { + if (this.configuredAutoSaveOnFocusChange && this.isDirty()) { + this.saveAll(void 0, SaveReason.FOCUS_CHANGE).done(null, errors.onUnexpectedError); + } + } + private onConfigurationChange(configuration: IFilesConfiguration): void { const wasAutoSaveEnabled = (this.getAutoSaveMode() !== AutoSaveMode.OFF); diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index 26de327ac49..2ee2efeca34 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -261,8 +261,8 @@ export interface ITextFileService extends IDisposable { * @param resources can be null to save all. * @param includeUntitled to save all resources and optionally exclude untitled ones. */ - saveAll(includeUntitled?: boolean, reason?: SaveReason): TPromise; - saveAll(resources: URI[], reason?: SaveReason): TPromise; + saveAll(includeUntitled?: boolean): TPromise; + saveAll(resources: URI[]): TPromise; /** * Reverts the provided resource. diff --git a/src/vs/workbench/services/textfile/electron-browser/textFileService.ts b/src/vs/workbench/services/textfile/electron-browser/textFileService.ts index 31689440954..69595481ff2 100644 --- a/src/vs/workbench/services/textfile/electron-browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/electron-browser/textFileService.ts @@ -24,6 +24,7 @@ import { IModeService } from 'vs/editor/common/services/modeService'; import { IWindowIPCService } from 'vs/workbench/services/window/electron-browser/windowService'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ModelBuilder } from 'vs/editor/node/model/modelBuilder'; +import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; import product from 'vs/platform/node/product'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -51,9 +52,10 @@ export class TextFileService extends AbstractTextFileService { @IMessageService messageService: IMessageService, @IBackupFileService backupFileService: IBackupFileService, @IStorageService private storageService: IStorageService, - @IWindowsService windowsService: IWindowsService + @IWindowsService windowsService: IWindowsService, + @IEditorGroupService editorGroupService: IEditorGroupService ) { - super(lifecycleService, contextService, configurationService, telemetryService, fileService, untitledEditorService, instantiationService, messageService, environmentService, backupFileService, windowsService); + super(lifecycleService, contextService, configurationService, telemetryService, fileService, untitledEditorService, instantiationService, messageService, environmentService, backupFileService, editorGroupService, windowsService); } public resolveTextContent(resource: URI, options?: IResolveContentOptions): TPromise { 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/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 () { diff --git a/src/vs/workbench/test/node/api/extHostTypes.test.ts b/src/vs/workbench/test/node/api/extHostTypes.test.ts index 87c9dc78186..b65d6bddaf3 100644 --- a/src/vs/workbench/test/node/api/extHostTypes.test.ts +++ b/src/vs/workbench/test/node/api/extHostTypes.test.ts @@ -25,11 +25,8 @@ suite('ExtHostTypes', function () { assert.deepEqual(data, { $mid: 1, scheme: 'file', - authority: '', path: '/path/test.file', fsPath: '/path/test.file'.replace(/\//g, isWindows ? '\\' : '/'), - query: '', - fragment: '', external: 'file:///path/test.file' }); }); diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index a336de3b3e7..ed4025be1e4 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -115,9 +115,10 @@ export class TestTextFileService extends TextFileService { @IInstantiationService instantiationService: IInstantiationService, @IMessageService messageService: IMessageService, @IBackupFileService backupFileService: IBackupFileService, - @IWindowsService windowsService: IWindowsService + @IWindowsService windowsService: IWindowsService, + @IEditorGroupService editorGroupService: IEditorGroupService ) { - super(lifecycleService, contextService, configurationService, telemetryService, fileService, untitledEditorService, instantiationService, messageService, TestEnvironmentService, backupFileService, windowsService); + super(lifecycleService, contextService, configurationService, telemetryService, fileService, untitledEditorService, instantiationService, messageService, TestEnvironmentService, backupFileService, editorGroupService, windowsService); } public setPromptPath(path: string): void {