From d81a1bcd5888fd6d3b37c90501edbf7c01dd4cea Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 28 Nov 2018 17:50:58 +0100 Subject: [PATCH] add internal api for selection ranges provider, #63935 --- src/vs/editor/common/modes.ts | 13 +++ .../contrib/smartSelect/defaultProvider.ts | 30 +++++++ .../editor/contrib/smartSelect/smartSelect.ts | 88 +++++++++++-------- .../test/tokenSelectionSupport.test.ts | 25 +++--- .../smartSelect/tokenSelectionSupport.ts | 63 ------------- src/vs/monaco.d.ts | 7 ++ 6 files changed, 111 insertions(+), 115 deletions(-) create mode 100644 src/vs/editor/contrib/smartSelect/defaultProvider.ts delete mode 100644 src/vs/editor/contrib/smartSelect/tokenSelectionSupport.ts diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index bb1af92f90d..e5676281b29 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -1031,6 +1031,14 @@ export interface DocumentColorProvider { */ provideColorPresentations(model: model.ITextModel, colorInfo: IColorInformation, token: CancellationToken): ProviderResult; } + +export interface SelectionRangeProvider { + /** + * Provide ranges that should be selected from the given position. + */ + provideSelectionRanges(model: model.ITextModel, position: Position, token: CancellationToken): ProviderResult; +} + export interface FoldingContext { } /** @@ -1337,6 +1345,11 @@ export const LinkProviderRegistry = new LanguageFeatureRegistry(); */ export const ColorProviderRegistry = new LanguageFeatureRegistry(); +/** + * @internal + */ +export const SelectionRangeRegistry = new LanguageFeatureRegistry(); + /** * @internal */ diff --git a/src/vs/editor/contrib/smartSelect/defaultProvider.ts b/src/vs/editor/contrib/smartSelect/defaultProvider.ts new file mode 100644 index 00000000000..3298c357c74 --- /dev/null +++ b/src/vs/editor/contrib/smartSelect/defaultProvider.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { SelectionRangeProvider } from 'vs/editor/common/modes'; +import { Range } from 'vs/editor/common/core/range'; +import { Position } from 'vs/editor/common/core/position'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { ITextModel } from 'vs/editor/common/model'; +import { build, find } from 'vs/editor/contrib/smartSelect/tokenTree'; + +export class DefaultSelectionRangeProvider implements SelectionRangeProvider { + + provideSelectionRanges(model: ITextModel, position: Position, token: CancellationToken): Range[] { + let tree = build(model); + let node = find(tree, position); + let ranges: Range[] = []; + let lastRange: Range | undefined; + while (node) { + if (!lastRange || !Range.equalsRange(lastRange, node.range)) { + ranges.push(node.range); + } + lastRange = node.range; + node = node.parent; + } + ranges = ranges.reverse(); + return ranges; + } +} diff --git a/src/vs/editor/contrib/smartSelect/smartSelect.ts b/src/vs/editor/contrib/smartSelect/smartSelect.ts index d7c9b6002be..4ed98b13b0b 100644 --- a/src/vs/editor/contrib/smartSelect/smartSelect.ts +++ b/src/vs/editor/contrib/smartSelect/smartSelect.ts @@ -3,19 +3,23 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import * as arrays from 'vs/base/common/arrays'; +import { asThenable, first } from 'vs/base/common/async'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorAction, IActionOptions, registerEditorAction, registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; +import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { registerEditorAction, ServicesAccessor, IActionOptions, EditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; -import { TokenSelectionSupport, ILogicalSelectionEntry } from './tokenSelectionSupport'; -import { ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { ITextModel } from 'vs/editor/common/model'; +import * as modes from 'vs/editor/common/modes'; +import { DefaultSelectionRangeProvider } from 'vs/editor/contrib/smartSelect/defaultProvider'; +import * as nls from 'vs/nls'; import { MenuId } from 'vs/platform/actions/common/actions'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; // --- selection state machine @@ -42,62 +46,62 @@ class SmartSelectController implements IEditorContribution { return editor.getContribution(SmartSelectController.ID); } - private _tokenSelectionSupport: TokenSelectionSupport; + private _editor: ICodeEditor; private _state?: State; private _ignoreSelection: boolean; - constructor( - private editor: ICodeEditor, - @IInstantiationService instantiationService: IInstantiationService - ) { - this._tokenSelectionSupport = instantiationService.createInstance(TokenSelectionSupport); + constructor(editor: ICodeEditor) { + this._editor = editor; this._ignoreSelection = false; } - public dispose(): void { + dispose(): void { } - public getId(): string { + getId(): string { return SmartSelectController.ID; } - public run(forward: boolean): Promise { - if (!this.editor.hasModel()) { - return Promise.resolve(void 0); + run(forward: boolean): Promise | void { + if (!this._editor.hasModel()) { + return; } - const selection = this.editor.getSelection(); - const model = this.editor.getModel(); + const selection = this._editor.getSelection(); + const model = this._editor.getModel(); + + if (!modes.SelectionRangeRegistry.has(model)) { + return; + } // forget about current state if (this._state) { - if (this._state.editor !== this.editor) { + if (this._state.editor !== this._editor) { this._state = undefined; } } let promise: Promise = Promise.resolve(void 0); - if (!this._state) { - promise = Promise.resolve(this._tokenSelectionSupport.getRangesToPositionSync(model.uri, selection.getStartPosition())).then((elements: ILogicalSelectionEntry[]) => { - if (arrays.isFalsyOrEmpty(elements)) { + if (!this._state) { + promise = provideSelectionRanges(model, selection.getStartPosition(), CancellationToken.None).then(ranges => { + if (!arrays.isNonEmptyArray(ranges)) { + // invalid result + return; + } + if (!this._editor.hasModel() || !this._editor.getSelection().equalsSelection(selection)) { + // invalid editor state return; } let lastState: State | undefined; - elements.filter((element) => { - if (!this.editor.hasModel()) { - return false; - } + ranges.filter(range => { // filter ranges inside the selection - const selection = this.editor.getSelection(); - const range = new Range(element.range.startLineNumber, element.range.startColumn, element.range.endLineNumber, element.range.endColumn); return range.containsPosition(selection.getStartPosition()) && range.containsPosition(selection.getEndPosition()); - }).forEach((element) => { + }).forEach(range => { // create ranges - const range = element.range; - const state = new State(this.editor); + const state = new State(this._editor); state.selection = new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn); if (lastState) { state.next = lastState; @@ -107,7 +111,7 @@ class SmartSelectController implements IEditorContribution { }); // insert current selection - const editorState = new State(this.editor); + const editorState = new State(this._editor); editorState.next = lastState; if (lastState) { lastState.previous = editorState; @@ -115,7 +119,7 @@ class SmartSelectController implements IEditorContribution { this._state = editorState; // listen to caret move and forget about state - const unhook = this.editor.onDidChangeCursorPosition((e: ICursorPositionChangedEvent) => { + const unhook = this._editor.onDidChangeCursorPosition((e: ICursorPositionChangedEvent) => { if (this._ignoreSelection) { return; } @@ -139,7 +143,7 @@ class SmartSelectController implements IEditorContribution { this._ignoreSelection = true; try { if (this._state.selection) { - this.editor.setSelection(this._state.selection); + this._editor.setSelection(this._state.selection); } } finally { this._ignoreSelection = false; @@ -159,12 +163,11 @@ abstract class AbstractSmartSelect extends EditorAction { this._forward = forward; } - public run(accessor: ServicesAccessor, editor: ICodeEditor): Promise | undefined { + async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { let controller = SmartSelectController.get(editor); if (controller) { - return controller.run(this._forward); + await controller.run(this._forward); } - return undefined; } } @@ -217,3 +220,10 @@ class ShrinkSelectionAction extends AbstractSmartSelect { registerEditorContribution(SmartSelectController); registerEditorAction(GrowSelectionAction); registerEditorAction(ShrinkSelectionAction); + +export function provideSelectionRanges(model: ITextModel, position: Position, token: CancellationToken): Promise { + const provider = modes.SelectionRangeRegistry.ordered(model); + return first(provider.map(pro => () => asThenable(() => pro.provideSelectionRanges(model, position, token))), arrays.isNonEmptyArray); +} + +modes.SelectionRangeRegistry.register('*', new DefaultSelectionRangeProvider()); diff --git a/src/vs/editor/contrib/smartSelect/test/tokenSelectionSupport.test.ts b/src/vs/editor/contrib/smartSelect/test/tokenSelectionSupport.test.ts index bab374e26c7..1aa1edfab0b 100644 --- a/src/vs/editor/contrib/smartSelect/test/tokenSelectionSupport.test.ts +++ b/src/vs/editor/contrib/smartSelect/test/tokenSelectionSupport.test.ts @@ -7,7 +7,6 @@ import { URI } from 'vs/base/common/uri'; import { Range } from 'vs/editor/common/core/range'; import { Position } from 'vs/editor/common/core/position'; import { LanguageIdentifier } from 'vs/editor/common/modes'; -import { TokenSelectionSupport } from 'vs/editor/contrib/smartSelect/tokenSelectionSupport'; import { MockMode, StaticLanguageSelector } from 'vs/editor/test/common/mocks/mockMode'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; @@ -16,6 +15,8 @@ import { javascriptOnEnterRules } from 'vs/editor/test/common/modes/supports/jav import { ITextResourcePropertiesService } from 'vs/editor/common/services/resourceConfiguration'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { isLinux, isMacintosh } from 'vs/base/common/platform'; +import { provideSelectionRanges } from 'vs/editor/contrib/smartSelect/smartSelect'; +import { CancellationToken } from 'vs/base/common/cancellation'; class MockJSMode extends MockMode { @@ -39,13 +40,11 @@ class MockJSMode extends MockMode { suite('TokenSelectionSupport', () => { let modelService: ModelServiceImpl | null = null; - let tokenSelectionSupport: TokenSelectionSupport; let mode: MockJSMode | null = null; setup(() => { const configurationService = new TestConfigurationService(); modelService = new ModelServiceImpl(null, configurationService, new TestTextResourcePropertiesService(configurationService)); - tokenSelectionSupport = new TokenSelectionSupport(modelService); mode = new MockJSMode(); }); @@ -54,13 +53,13 @@ suite('TokenSelectionSupport', () => { mode.dispose(); }); - function assertGetRangesToPosition(text: string[], lineNumber: number, column: number, ranges: Range[]): void { + async function assertGetRangesToPosition(text: string[], lineNumber: number, column: number, ranges: Range[]): Promise { let uri = URI.file('test.js'); - modelService.createModel(text.join('\n'), new StaticLanguageSelector(mode.getLanguageIdentifier()), uri); + let model = modelService.createModel(text.join('\n'), new StaticLanguageSelector(mode.getLanguageIdentifier()), uri); - let actual = tokenSelectionSupport.getRangesToPositionSync(uri, new Position(lineNumber, column)); + let actual = await provideSelectionRanges(model, new Position(lineNumber, column), CancellationToken.None); - let actualStr = actual.map(r => new Range(r.range.startLineNumber, r.range.startColumn, r.range.endLineNumber, r.range.endColumn).toString()); + let actualStr = actual.map(r => new Range(r.startLineNumber, r.startColumn, r.endLineNumber, r.endColumn).toString()); let desiredStr = ranges.map(r => String(r)); assert.deepEqual(actualStr, desiredStr); @@ -70,7 +69,7 @@ suite('TokenSelectionSupport', () => { test('getRangesToPosition #1', () => { - assertGetRangesToPosition([ + return assertGetRangesToPosition([ 'function a(bar, foo){', '\tif (bar) {', '\t\treturn (bar + (2 * foo))', @@ -93,7 +92,7 @@ suite('TokenSelectionSupport', () => { test('getRangesToPosition #56886. Skip empty lines correctly.', () => { - assertGetRangesToPosition([ + return assertGetRangesToPosition([ 'function a(bar, foo){', '\tif (bar) {', '', @@ -109,7 +108,7 @@ suite('TokenSelectionSupport', () => { test('getRangesToPosition #56886. Do not skip lines with only whitespaces.', () => { - assertGetRangesToPosition([ + return assertGetRangesToPosition([ 'function a(bar, foo){', '\tif (bar) {', ' ', @@ -127,7 +126,7 @@ suite('TokenSelectionSupport', () => { test('getRangesToPosition #40658. Cursor at first position inside brackets should select line inside.', () => { - assertGetRangesToPosition([ + return assertGetRangesToPosition([ ' [ ]', ' { } ', '( ) ' @@ -141,7 +140,7 @@ suite('TokenSelectionSupport', () => { test('getRangesToPosition #40658. Cursor in empty brackets should reveal brackets first.', () => { - assertGetRangesToPosition([ + return assertGetRangesToPosition([ ' [] ', ' { } ', ' ( ) ' @@ -154,7 +153,7 @@ suite('TokenSelectionSupport', () => { test('getRangesToPosition #40658. Tokens before bracket will be revealed first.', () => { - assertGetRangesToPosition([ + return assertGetRangesToPosition([ ' [] ', ' { } ', 'selectthis( ) ' diff --git a/src/vs/editor/contrib/smartSelect/tokenSelectionSupport.ts b/src/vs/editor/contrib/smartSelect/tokenSelectionSupport.ts deleted file mode 100644 index 04400c56929..00000000000 --- a/src/vs/editor/contrib/smartSelect/tokenSelectionSupport.ts +++ /dev/null @@ -1,63 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { URI } from 'vs/base/common/uri'; -import { Range } from 'vs/editor/common/core/range'; -import { ITextModel } from 'vs/editor/common/model'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { build, find } from './tokenTree'; -import { Position } from 'vs/editor/common/core/position'; - -/** - * Interface used to compute a hierachry of logical ranges. - */ -export interface ILogicalSelectionEntry { - type?: string; - range: Range; -} - -export class TokenSelectionSupport { - - private _modelService: IModelService; - - constructor(@IModelService modelService: IModelService) { - this._modelService = modelService; - } - - public getRangesToPositionSync(resource: URI, position: Position): ILogicalSelectionEntry[] { - const model = this._modelService.getModel(resource); - let entries: ILogicalSelectionEntry[] = []; - - if (model) { - this._doGetRangesToPosition(model, position).forEach(range => { - entries.push({ - type: void 0, - range - }); - }); - } - - return entries; - } - - private _doGetRangesToPosition(model: ITextModel, position: Position): Range[] { - - let tree = build(model); - let lastRange: Range | undefined; - - let node = find(tree, position); - let ranges: Range[] = []; - while (node) { - if (!lastRange || !Range.equalsRange(lastRange, node.range)) { - ranges.push(node.range); - } - lastRange = node.range; - node = node.parent; - } - ranges = ranges.reverse(); - return ranges; - } - -} diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index de93f9b87ee..843e97c19cb 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -5294,6 +5294,13 @@ declare namespace monaco.languages { provideColorPresentations(model: editor.ITextModel, colorInfo: IColorInformation, token: CancellationToken): ProviderResult; } + export interface SelectionRangeProvider { + /** + * Provide ranges that should be selected from the given position. + */ + provideSelectionRanges(model: editor.ITextModel, position: Position, token: CancellationToken): ProviderResult; + } + export interface FoldingContext { } -- GitLab