mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
Merge VS Code 1.31.1 (#4283)
This commit is contained in:
@@ -3,47 +3,146 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { SelectionRangeProvider } from 'vs/editor/common/modes';
|
||||
import { SelectionRangeProvider, SelectionRange } from 'vs/editor/common/modes';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { LinkedList } from 'vs/base/common/linkedList';
|
||||
|
||||
export class BracketSelectionRangeProvider implements SelectionRangeProvider {
|
||||
|
||||
provideSelectionRanges(model: ITextModel, position: Position): Range[] {
|
||||
provideSelectionRanges(model: ITextModel, position: Position): Promise<SelectionRange[]> {
|
||||
const bucket: SelectionRange[] = [];
|
||||
const ranges = new Map<string, LinkedList<Range>>();
|
||||
return new Promise(resolve => BracketSelectionRangeProvider._bracketsRightYield(resolve, 0, model, position, ranges))
|
||||
.then(() => new Promise(resolve => BracketSelectionRangeProvider._bracketsLeftYield(resolve, 0, model, position, ranges, bucket)))
|
||||
.then(() => bucket);
|
||||
}
|
||||
|
||||
let result: Range[] = [];
|
||||
let last: Range | undefined;
|
||||
let pos = position;
|
||||
let i = 0;
|
||||
for (; i < 1750; i++) {
|
||||
private static readonly _maxDuration = 30;
|
||||
private static readonly _maxRounds = 2;
|
||||
|
||||
private static _bracketsRightYield(resolve: () => void, round: number, model: ITextModel, pos: Position, ranges: Map<string, LinkedList<Range>>): void {
|
||||
const counts = new Map<string, number>();
|
||||
const t1 = Date.now();
|
||||
while (true) {
|
||||
if (round >= BracketSelectionRangeProvider._maxRounds) {
|
||||
resolve();
|
||||
break;
|
||||
}
|
||||
if (!pos) {
|
||||
resolve();
|
||||
break;
|
||||
}
|
||||
let bracket = model.findNextBracket(pos);
|
||||
if (!bracket) {
|
||||
// no more brackets
|
||||
resolve();
|
||||
break;
|
||||
} else if (bracket.isOpen) {
|
||||
// skip past the closing bracket
|
||||
let matching = model.matchBracket(bracket.range.getEndPosition());
|
||||
if (!matching) {
|
||||
break;
|
||||
}
|
||||
pos = model.getPositionAt(model.getOffsetAt(matching[1].getEndPosition()) + 1);
|
||||
|
||||
}
|
||||
let d = Date.now() - t1;
|
||||
if (d > BracketSelectionRangeProvider._maxDuration) {
|
||||
setTimeout(() => BracketSelectionRangeProvider._bracketsRightYield(resolve, round + 1, model, pos, ranges));
|
||||
break;
|
||||
}
|
||||
const key = bracket.close;
|
||||
if (bracket.isOpen) {
|
||||
// wait for closing
|
||||
let val = counts.has(key) ? counts.get(key)! : 0;
|
||||
counts.set(key, val + 1);
|
||||
} else {
|
||||
// find matching, opening bracket
|
||||
let range = model.findMatchingBracketUp(bracket.close, bracket.range.getStartPosition());
|
||||
if (!range) {
|
||||
break;
|
||||
// process closing
|
||||
let val = counts.has(key) ? counts.get(key)! : 0;
|
||||
val -= 1;
|
||||
counts.set(key, Math.max(0, val));
|
||||
if (val < 0) {
|
||||
let list = ranges.get(key);
|
||||
if (!list) {
|
||||
list = new LinkedList();
|
||||
ranges.set(key, list);
|
||||
}
|
||||
list.push(bracket.range);
|
||||
}
|
||||
if (!last || range.getStartPosition().isBefore(last.getStartPosition())) {
|
||||
const inner = Range.fromPositions(range.getStartPosition(), bracket.range.getEndPosition());
|
||||
const outer = Range.fromPositions(range.getEndPosition(), bracket.range.getStartPosition());
|
||||
result.push(inner, outer);
|
||||
last = outer;
|
||||
}
|
||||
pos = bracket.range.getEndPosition();
|
||||
}
|
||||
}
|
||||
|
||||
private static _bracketsLeftYield(resolve: () => void, round: number, model: ITextModel, pos: Position, ranges: Map<string, LinkedList<Range>>, bucket: SelectionRange[]): void {
|
||||
const counts = new Map<string, number>();
|
||||
const t1 = Date.now();
|
||||
while (true) {
|
||||
if (round >= BracketSelectionRangeProvider._maxRounds && ranges.size === 0) {
|
||||
resolve();
|
||||
break;
|
||||
}
|
||||
if (!pos) {
|
||||
resolve();
|
||||
break;
|
||||
}
|
||||
let bracket = model.findPrevBracket(pos);
|
||||
if (!bracket) {
|
||||
resolve();
|
||||
break;
|
||||
}
|
||||
let d = Date.now() - t1;
|
||||
if (d > BracketSelectionRangeProvider._maxDuration) {
|
||||
setTimeout(() => BracketSelectionRangeProvider._bracketsLeftYield(resolve, round + 1, model, pos, ranges, bucket));
|
||||
break;
|
||||
}
|
||||
const key = bracket.close;
|
||||
if (!bracket.isOpen) {
|
||||
// wait for opening
|
||||
let val = counts.has(key) ? counts.get(key)! : 0;
|
||||
counts.set(key, val + 1);
|
||||
} else {
|
||||
// opening
|
||||
let val = counts.has(key) ? counts.get(key)! : 0;
|
||||
val -= 1;
|
||||
counts.set(key, Math.max(0, val));
|
||||
if (val < 0) {
|
||||
let list = ranges.get(key);
|
||||
if (list) {
|
||||
let closing = list.shift();
|
||||
if (list.size === 0) {
|
||||
ranges.delete(key);
|
||||
}
|
||||
const innerBracket = Range.fromPositions(bracket.range.getEndPosition(), closing!.getStartPosition());
|
||||
const outerBracket = Range.fromPositions(bracket.range.getStartPosition(), closing!.getEndPosition());
|
||||
bucket.push({ range: innerBracket, kind: 'statement.brackets' });
|
||||
bucket.push({ range: outerBracket, kind: 'statement.brackets.full' });
|
||||
BracketSelectionRangeProvider._addBracketLeading(model, outerBracket, bucket);
|
||||
}
|
||||
}
|
||||
pos = model.getPositionAt(model.getOffsetAt(bracket.range.getEndPosition()) + 1);
|
||||
}
|
||||
pos = bracket.range.getStartPosition();
|
||||
}
|
||||
}
|
||||
|
||||
private static _addBracketLeading(model: ITextModel, bracket: Range, bucket: SelectionRange[]): void {
|
||||
if (bracket.startLineNumber === bracket.endLineNumber) {
|
||||
return;
|
||||
}
|
||||
// xxxxxxxx {
|
||||
//
|
||||
// }
|
||||
const startLine = bracket.startLineNumber;
|
||||
const column = model.getLineFirstNonWhitespaceColumn(startLine);
|
||||
if (column !== 0 && column !== bracket.startColumn) {
|
||||
bucket.push({ range: Range.fromPositions(new Position(startLine, column), bracket.getEndPosition()), kind: 'statement.brackets.leading' });
|
||||
bucket.push({ range: Range.fromPositions(new Position(startLine, 1), bracket.getEndPosition()), kind: 'statement.brackets.leading.full' });
|
||||
}
|
||||
|
||||
// xxxxxxxx
|
||||
// {
|
||||
//
|
||||
// }
|
||||
const aboveLine = startLine - 1;
|
||||
if (aboveLine > 0) {
|
||||
const column = model.getLineFirstNonWhitespaceColumn(aboveLine);
|
||||
if (column === bracket.startColumn && column !== model.getLineLastNonWhitespaceColumn(aboveLine)) {
|
||||
bucket.push({ range: Range.fromPositions(new Position(aboveLine, column), bracket.getEndPosition()), kind: 'statement.brackets.leading' });
|
||||
bucket.push({ range: Range.fromPositions(new Position(aboveLine, 1), bracket.getEndPosition()), kind: 'statement.brackets.leading.full' });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,9 +18,9 @@ import * as nls from 'vs/nls';
|
||||
import { MenuId } from 'vs/platform/actions/common/actions';
|
||||
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { TokenTreeSelectionRangeProvider } from 'vs/editor/contrib/smartSelect/tokenTree';
|
||||
import { WordSelectionRangeProvider } from 'vs/editor/contrib/smartSelect/wordSelections';
|
||||
import { BracketSelectionRangeProvider } from 'vs/editor/contrib/smartSelect/bracketSelections';
|
||||
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
|
||||
|
||||
class SelectionRanges {
|
||||
|
||||
@@ -82,10 +82,10 @@ class SmartSelectController implements IEditorContribution {
|
||||
}
|
||||
|
||||
|
||||
let promise: Promise<void> = Promise.resolve(void 0);
|
||||
let promise: Promise<void> = Promise.resolve(undefined);
|
||||
|
||||
if (!this._state) {
|
||||
promise = provideSelectionRanges(model, selection.getStartPosition(), CancellationToken.None).then(ranges => {
|
||||
promise = provideSelectionRanges(model, selection.getPosition(), CancellationToken.None).then(ranges => {
|
||||
if (!arrays.isNonEmptyArray(ranges)) {
|
||||
// invalid result
|
||||
return;
|
||||
@@ -154,9 +154,9 @@ abstract class AbstractSmartSelect extends EditorAction {
|
||||
class GrowSelectionAction extends AbstractSmartSelect {
|
||||
constructor() {
|
||||
super(true, {
|
||||
id: 'editor.action.smartSelect.grow',
|
||||
label: nls.localize('smartSelect.grow', "Expand Select"),
|
||||
alias: 'Expand Select',
|
||||
id: 'editor.action.smartSelect.expand',
|
||||
label: nls.localize('smartSelect.expand', "Expand Selection"),
|
||||
alias: 'Expand Selection',
|
||||
precondition: null,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.editorTextFocus,
|
||||
@@ -174,12 +174,15 @@ class GrowSelectionAction extends AbstractSmartSelect {
|
||||
}
|
||||
}
|
||||
|
||||
// renamed command id
|
||||
CommandsRegistry.registerCommandAlias('editor.action.smartSelect.grow', 'editor.action.smartSelect.expand');
|
||||
|
||||
class ShrinkSelectionAction extends AbstractSmartSelect {
|
||||
constructor() {
|
||||
super(false, {
|
||||
id: 'editor.action.smartSelect.shrink',
|
||||
label: nls.localize('smartSelect.shrink', "Shrink Select"),
|
||||
alias: 'Shrink Select',
|
||||
label: nls.localize('smartSelect.shrink', "Shrink Selection"),
|
||||
alias: 'Shrink Selection',
|
||||
precondition: null,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.editorTextFocus,
|
||||
@@ -201,14 +204,18 @@ registerEditorContribution(SmartSelectController);
|
||||
registerEditorAction(GrowSelectionAction);
|
||||
registerEditorAction(ShrinkSelectionAction);
|
||||
|
||||
// word selection
|
||||
modes.SelectionRangeRegistry.register('*', new WordSelectionRangeProvider());
|
||||
modes.SelectionRangeRegistry.register('*', new BracketSelectionRangeProvider());
|
||||
modes.SelectionRangeRegistry.register('*', new TokenTreeSelectionRangeProvider());
|
||||
|
||||
export function provideSelectionRanges(model: ITextModel, position: Position, token: CancellationToken): Promise<Range[] | undefined | null> {
|
||||
|
||||
const provider = modes.SelectionRangeRegistry.orderedGroups(model);
|
||||
|
||||
if (provider.length === 1) {
|
||||
// add word selection and bracket selection when no provider exists
|
||||
provider.unshift([new BracketSelectionRangeProvider()]);
|
||||
}
|
||||
|
||||
interface RankedRange {
|
||||
rank: number;
|
||||
range: Range;
|
||||
@@ -221,11 +228,11 @@ export function provideSelectionRanges(model: ITextModel, position: Position, to
|
||||
for (const group of provider) {
|
||||
rank += 1;
|
||||
for (const prov of group) {
|
||||
work.push(Promise.resolve(prov.provideSelectionRanges(model, position, token)).then(res => {
|
||||
if (arrays.isNonEmptyArray(res)) {
|
||||
for (const range of res) {
|
||||
if (Range.isIRange(range) && Range.containsPosition(range, position)) {
|
||||
ranges.push({ range: Range.lift(range), rank });
|
||||
work.push(Promise.resolve(prov.provideSelectionRanges(model, position, token)).then(selectionRanges => {
|
||||
if (arrays.isNonEmptyArray(selectionRanges)) {
|
||||
for (const sel of selectionRanges) {
|
||||
if (Range.isIRange(sel.range) && Range.containsPosition(sel.range, position)) {
|
||||
ranges.push({ range: Range.lift(sel.range), rank });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -234,6 +241,11 @@ export function provideSelectionRanges(model: ITextModel, position: Position, to
|
||||
}
|
||||
|
||||
return Promise.all(work).then(() => {
|
||||
|
||||
if (ranges.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
ranges.sort((a, b) => {
|
||||
if (Position.isBefore(a.range.getStartPosition(), b.range.getStartPosition())) {
|
||||
return 1;
|
||||
@@ -248,16 +260,35 @@ export function provideSelectionRanges(model: ITextModel, position: Position, to
|
||||
}
|
||||
});
|
||||
|
||||
// ranges.sort((a, b) => Range.compareRangesUsingStarts(b.range, a.range));
|
||||
let result: Range[] = [];
|
||||
let last: Range | undefined;
|
||||
for (const { range } of ranges) {
|
||||
if (!last || Range.containsRange(range, last)) {
|
||||
if (!last || (Range.containsRange(range, last) && !Range.equalsRange(range, last))) {
|
||||
result.push(range);
|
||||
last = range;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
||||
let result2: Range[] = [result[0]];
|
||||
for (let i = 1; i < result.length; i++) {
|
||||
const prev = result[i - 1];
|
||||
const cur = result[i];
|
||||
if (cur.startLineNumber !== prev.startLineNumber || cur.endLineNumber !== prev.endLineNumber) {
|
||||
// add line/block range without leading/failing whitespace
|
||||
const rangeNoWhitespace = new Range(prev.startLineNumber, model.getLineFirstNonWhitespaceColumn(prev.startLineNumber), prev.endLineNumber, model.getLineLastNonWhitespaceColumn(prev.endLineNumber));
|
||||
if (rangeNoWhitespace.containsRange(prev) && !rangeNoWhitespace.equalsRange(prev)) {
|
||||
result2.push(rangeNoWhitespace);
|
||||
}
|
||||
// add line/block range
|
||||
const rangeFull = new Range(prev.startLineNumber, 1, prev.endLineNumber, model.getLineMaxColumn(prev.endLineNumber));
|
||||
if (rangeFull.containsRange(prev) && !rangeFull.equalsRange(rangeNoWhitespace)) {
|
||||
result2.push(rangeFull);
|
||||
}
|
||||
}
|
||||
result2.push(cur);
|
||||
}
|
||||
|
||||
return result2;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
321
src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts
Normal file
321
src/vs/editor/contrib/smartSelect/test/smartSelect.test.ts
Normal file
@@ -0,0 +1,321 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as assert from 'assert';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Range, IRange } from 'vs/editor/common/core/range';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { LanguageIdentifier, SelectionRangeProvider } from 'vs/editor/common/modes';
|
||||
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';
|
||||
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
|
||||
import { javascriptOnEnterRules } from 'vs/editor/test/common/modes/supports/javascriptOnEnterRules';
|
||||
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 { BracketSelectionRangeProvider } from 'vs/editor/contrib/smartSelect/bracketSelections';
|
||||
import { provideSelectionRanges } from 'vs/editor/contrib/smartSelect/smartSelect';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { WordSelectionRangeProvider } from 'vs/editor/contrib/smartSelect/wordSelections';
|
||||
|
||||
class TestTextResourcePropertiesService implements ITextResourcePropertiesService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
constructor(
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
) {
|
||||
}
|
||||
|
||||
getEOL(resource: URI | undefined): string {
|
||||
const filesConfiguration = this.configurationService.getValue<{ eol: string }>('files');
|
||||
if (filesConfiguration && filesConfiguration.eol) {
|
||||
if (filesConfiguration.eol !== 'auto') {
|
||||
return filesConfiguration.eol;
|
||||
}
|
||||
}
|
||||
return (isLinux || isMacintosh) ? '\n' : '\r\n';
|
||||
}
|
||||
}
|
||||
|
||||
class MockJSMode extends MockMode {
|
||||
|
||||
private static readonly _id = new LanguageIdentifier('mockJSMode', 3);
|
||||
|
||||
constructor() {
|
||||
super(MockJSMode._id);
|
||||
|
||||
this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), {
|
||||
brackets: [
|
||||
['(', ')'],
|
||||
['{', '}'],
|
||||
['[', ']']
|
||||
],
|
||||
|
||||
onEnterRules: javascriptOnEnterRules,
|
||||
wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\#\$\%\^\&\*\(\)\=\+\[\{\]\}\\\;\:\'\"\,\.\<\>\/\?\s]+)/g
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
suite('SmartSelect', () => {
|
||||
|
||||
let modelService: ModelServiceImpl;
|
||||
let mode: MockJSMode;
|
||||
|
||||
setup(() => {
|
||||
const configurationService = new TestConfigurationService();
|
||||
modelService = new ModelServiceImpl(configurationService, new TestTextResourcePropertiesService(configurationService));
|
||||
mode = new MockJSMode();
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
modelService.dispose();
|
||||
mode.dispose();
|
||||
});
|
||||
|
||||
async function assertGetRangesToPosition(text: string[], lineNumber: number, column: number, ranges: Range[]): Promise<void> {
|
||||
let uri = URI.file('test.js');
|
||||
let model = modelService.createModel(text.join('\n'), new StaticLanguageSelector(mode.getLanguageIdentifier()), uri);
|
||||
let actual = await provideSelectionRanges(model, new Position(lineNumber, column), CancellationToken.None);
|
||||
let actualStr = actual!.map(r => new Range(r.startLineNumber, r.startColumn, r.endLineNumber, r.endColumn).toString());
|
||||
let desiredStr = ranges.reverse().map(r => String(r));
|
||||
|
||||
assert.deepEqual(actualStr, desiredStr, `\nA: ${actualStr} VS \nE: ${desiredStr}`);
|
||||
modelService.destroyModel(uri);
|
||||
}
|
||||
|
||||
test('getRangesToPosition #1', () => {
|
||||
|
||||
return assertGetRangesToPosition([
|
||||
'function a(bar, foo){',
|
||||
'\tif (bar) {',
|
||||
'\t\treturn (bar + (2 * foo))',
|
||||
'\t}',
|
||||
'}'
|
||||
], 3, 20, [
|
||||
new Range(1, 1, 5, 2), // all
|
||||
new Range(1, 21, 5, 2), // {} outside
|
||||
new Range(1, 22, 5, 1), // {} inside
|
||||
new Range(2, 1, 4, 3), // block
|
||||
new Range(2, 1, 4, 3),
|
||||
new Range(2, 2, 4, 3),
|
||||
new Range(2, 11, 4, 3),
|
||||
new Range(2, 12, 4, 2),
|
||||
new Range(3, 1, 3, 27), // line w/ triva
|
||||
new Range(3, 3, 3, 27), // line w/o triva
|
||||
new Range(3, 10, 3, 27), // () outside
|
||||
new Range(3, 11, 3, 26), // () inside
|
||||
new Range(3, 17, 3, 26), // () outside
|
||||
new Range(3, 18, 3, 25), // () inside
|
||||
]);
|
||||
});
|
||||
|
||||
test('getRangesToPosition #56886. Skip empty lines correctly.', () => {
|
||||
|
||||
return assertGetRangesToPosition([
|
||||
'function a(bar, foo){',
|
||||
'\tif (bar) {',
|
||||
'',
|
||||
'\t}',
|
||||
'}'
|
||||
], 3, 1, [
|
||||
new Range(1, 1, 5, 2),
|
||||
new Range(1, 21, 5, 2),
|
||||
new Range(1, 22, 5, 1),
|
||||
new Range(2, 1, 4, 3),
|
||||
new Range(2, 1, 4, 3),
|
||||
new Range(2, 2, 4, 3),
|
||||
new Range(2, 11, 4, 3),
|
||||
new Range(2, 12, 4, 2),
|
||||
]);
|
||||
});
|
||||
|
||||
test('getRangesToPosition #56886. Do not skip lines with only whitespaces.', () => {
|
||||
|
||||
return assertGetRangesToPosition([
|
||||
'function a(bar, foo){',
|
||||
'\tif (bar) {',
|
||||
' ',
|
||||
'\t}',
|
||||
'}'
|
||||
], 3, 1, [
|
||||
new Range(1, 1, 5, 2), // all
|
||||
new Range(1, 21, 5, 2), // {} outside
|
||||
new Range(1, 22, 5, 1), // {} inside
|
||||
new Range(2, 1, 4, 3),
|
||||
new Range(2, 1, 4, 3),
|
||||
new Range(2, 2, 4, 3),
|
||||
new Range(2, 11, 4, 3),
|
||||
new Range(2, 12, 4, 2),
|
||||
new Range(3, 1, 3, 2), // block
|
||||
new Range(3, 1, 3, 2) // empty line
|
||||
]);
|
||||
});
|
||||
|
||||
test('getRangesToPosition #40658. Cursor at first position inside brackets should select line inside.', () => {
|
||||
|
||||
return assertGetRangesToPosition([
|
||||
' [ ]',
|
||||
' { } ',
|
||||
'( ) '
|
||||
], 2, 3, [
|
||||
new Range(1, 1, 3, 5),
|
||||
new Range(2, 1, 2, 6), // line w/ triava
|
||||
new Range(2, 2, 2, 5), // {} inside, line w/o triva
|
||||
new Range(2, 3, 2, 4) // {} inside
|
||||
]);
|
||||
});
|
||||
|
||||
test('getRangesToPosition #40658. Cursor in empty brackets should reveal brackets first.', () => {
|
||||
|
||||
return assertGetRangesToPosition([
|
||||
' [] ',
|
||||
' { } ',
|
||||
' ( ) '
|
||||
], 1, 3, [
|
||||
new Range(1, 1, 3, 7), // all
|
||||
new Range(1, 1, 1, 5), // line w/ trival
|
||||
new Range(1, 2, 1, 4), // [] outside, line w/o trival
|
||||
new Range(1, 3, 1, 3), // [] inside
|
||||
]);
|
||||
});
|
||||
|
||||
test('getRangesToPosition #40658. Tokens before bracket will be revealed first.', () => {
|
||||
|
||||
return assertGetRangesToPosition([
|
||||
' [] ',
|
||||
' { } ',
|
||||
'selectthis( ) '
|
||||
], 3, 11, [
|
||||
new Range(1, 1, 3, 15), // all
|
||||
new Range(3, 1, 3, 15), // line w/ trivia
|
||||
new Range(3, 1, 3, 14), // line w/o trivia
|
||||
new Range(3, 1, 3, 11) // word
|
||||
]);
|
||||
});
|
||||
|
||||
// -- bracket selections
|
||||
|
||||
async function assertRanges(provider: SelectionRangeProvider, value: string, ...expected: IRange[]): Promise<void> {
|
||||
|
||||
let model = modelService.createModel(value, new StaticLanguageSelector(mode.getLanguageIdentifier()), URI.parse('fake:lang'));
|
||||
let pos = model.getPositionAt(value.indexOf('|'));
|
||||
let ranges = await provider.provideSelectionRanges(model, pos, CancellationToken.None);
|
||||
modelService.destroyModel(model.uri);
|
||||
|
||||
assert.equal(expected.length, ranges!.length);
|
||||
for (const range of ranges!) {
|
||||
let exp = expected.shift() || null;
|
||||
assert.ok(Range.equalsRange(range.range, exp), `A=${range.range} <> E=${exp}`);
|
||||
}
|
||||
}
|
||||
|
||||
test('bracket selection', async () => {
|
||||
await assertRanges(new BracketSelectionRangeProvider(), '(|)',
|
||||
new Range(1, 2, 1, 3), new Range(1, 1, 1, 4)
|
||||
);
|
||||
|
||||
await assertRanges(new BracketSelectionRangeProvider(), '[[[](|)]]',
|
||||
new Range(1, 6, 1, 7), new Range(1, 5, 1, 8), // ()
|
||||
new Range(1, 3, 1, 8), new Range(1, 2, 1, 9), // [[]()]
|
||||
new Range(1, 2, 1, 9), new Range(1, 1, 1, 10), // [[[]()]]
|
||||
);
|
||||
|
||||
await assertRanges(new BracketSelectionRangeProvider(), '[a[](|)a]',
|
||||
new Range(1, 6, 1, 7), new Range(1, 5, 1, 8),
|
||||
new Range(1, 2, 1, 9), new Range(1, 1, 1, 10),
|
||||
);
|
||||
|
||||
// no bracket
|
||||
await assertRanges(new BracketSelectionRangeProvider(), 'fofof|fofo');
|
||||
|
||||
// empty
|
||||
await assertRanges(new BracketSelectionRangeProvider(), '[[[]()]]|');
|
||||
await assertRanges(new BracketSelectionRangeProvider(), '|[[[]()]]');
|
||||
|
||||
// edge
|
||||
await assertRanges(new BracketSelectionRangeProvider(), '[|[[]()]]', new Range(1, 2, 1, 9), new Range(1, 1, 1, 10));
|
||||
await assertRanges(new BracketSelectionRangeProvider(), '[[[]()]|]', new Range(1, 2, 1, 9), new Range(1, 1, 1, 10));
|
||||
|
||||
await assertRanges(new BracketSelectionRangeProvider(), 'aaa(aaa)bbb(b|b)ccc(ccc)', new Range(1, 13, 1, 16), new Range(1, 12, 1, 17));
|
||||
await assertRanges(new BracketSelectionRangeProvider(), '(aaa(aaa)bbb(b|b)ccc(ccc))', new Range(1, 14, 1, 17), new Range(1, 13, 1, 18), new Range(1, 2, 1, 26), new Range(1, 1, 1, 27));
|
||||
});
|
||||
|
||||
test('bracket with leading/trailing', async () => {
|
||||
|
||||
await assertRanges(new BracketSelectionRangeProvider(), 'for(a of b){\n foo(|);\n}',
|
||||
new Range(2, 7, 2, 8), new Range(2, 6, 2, 9),
|
||||
new Range(1, 13, 3, 1), new Range(1, 12, 3, 2),
|
||||
new Range(1, 1, 3, 2), new Range(1, 1, 3, 2),
|
||||
);
|
||||
|
||||
await assertRanges(new BracketSelectionRangeProvider(), 'for(a of b)\n{\n foo(|);\n}',
|
||||
new Range(3, 7, 3, 8), new Range(3, 6, 3, 9),
|
||||
new Range(2, 2, 4, 1), new Range(2, 1, 4, 2),
|
||||
new Range(1, 1, 4, 2), new Range(1, 1, 4, 2),
|
||||
);
|
||||
});
|
||||
|
||||
test('in-word ranges', async () => {
|
||||
|
||||
await assertRanges(new WordSelectionRangeProvider(), 'f|ooBar',
|
||||
new Range(1, 1, 1, 5), // foo
|
||||
new Range(1, 1, 1, 8), // fooBar
|
||||
new Range(1, 1, 1, 8), // doc
|
||||
);
|
||||
|
||||
await assertRanges(new WordSelectionRangeProvider(), 'f|oo_Ba',
|
||||
new Range(1, 1, 1, 5),
|
||||
new Range(1, 1, 1, 8),
|
||||
new Range(1, 1, 1, 8),
|
||||
);
|
||||
|
||||
await assertRanges(new WordSelectionRangeProvider(), 'f|oo-Ba',
|
||||
new Range(1, 1, 1, 5),
|
||||
new Range(1, 1, 1, 8),
|
||||
new Range(1, 1, 1, 8),
|
||||
);
|
||||
});
|
||||
|
||||
test('Default selection should select current word/hump first in camelCase #67493', async function () {
|
||||
|
||||
await assertRanges(new WordSelectionRangeProvider(), 'Abs|tractSmartSelect',
|
||||
new Range(1, 1, 1, 10),
|
||||
new Range(1, 1, 1, 21),
|
||||
new Range(1, 1, 1, 21),
|
||||
);
|
||||
|
||||
await assertRanges(new WordSelectionRangeProvider(), 'AbstractSma|rtSelect',
|
||||
new Range(1, 9, 1, 15),
|
||||
new Range(1, 1, 1, 21),
|
||||
new Range(1, 1, 1, 21),
|
||||
);
|
||||
|
||||
await assertRanges(new WordSelectionRangeProvider(), 'Abstrac-Sma|rt-elect',
|
||||
new Range(1, 9, 1, 15),
|
||||
new Range(1, 1, 1, 21),
|
||||
new Range(1, 1, 1, 21),
|
||||
);
|
||||
|
||||
await assertRanges(new WordSelectionRangeProvider(), 'Abstrac_Sma|rt_elect',
|
||||
new Range(1, 9, 1, 15),
|
||||
new Range(1, 1, 1, 21),
|
||||
new Range(1, 1, 1, 21),
|
||||
);
|
||||
|
||||
await assertRanges(new WordSelectionRangeProvider(), 'Abstrac_Sma|rt-elect',
|
||||
new Range(1, 9, 1, 15),
|
||||
new Range(1, 1, 1, 21),
|
||||
new Range(1, 1, 1, 21),
|
||||
);
|
||||
|
||||
await assertRanges(new WordSelectionRangeProvider(), 'Abstrac_Sma|rtSelect',
|
||||
new Range(1, 9, 1, 15),
|
||||
new Range(1, 1, 1, 21),
|
||||
new Range(1, 1, 1, 21),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,188 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as assert from 'assert';
|
||||
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 { 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';
|
||||
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
|
||||
import { javascriptOnEnterRules } from 'vs/editor/test/common/modes/supports/javascriptOnEnterRules';
|
||||
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 { TokenTreeSelectionRangeProvider } from 'vs/editor/contrib/smartSelect/tokenTree';
|
||||
import { MarkerService } from 'vs/platform/markers/common/markerService';
|
||||
|
||||
class MockJSMode extends MockMode {
|
||||
|
||||
private static readonly _id = new LanguageIdentifier('mockJSMode', 3);
|
||||
|
||||
constructor() {
|
||||
super(MockJSMode._id);
|
||||
|
||||
this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), {
|
||||
brackets: [
|
||||
['(', ')'],
|
||||
['{', '}'],
|
||||
['[', ']']
|
||||
],
|
||||
|
||||
onEnterRules: javascriptOnEnterRules
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
suite('TokenSelectionSupport', () => {
|
||||
|
||||
let modelService: ModelServiceImpl;
|
||||
let mode: MockJSMode;
|
||||
|
||||
setup(() => {
|
||||
const configurationService = new TestConfigurationService();
|
||||
modelService = new ModelServiceImpl(new MarkerService(), configurationService, new TestTextResourcePropertiesService(configurationService));
|
||||
mode = new MockJSMode();
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
modelService.dispose();
|
||||
mode.dispose();
|
||||
});
|
||||
|
||||
function assertGetRangesToPosition(text: string[], lineNumber: number, column: number, ranges: Range[]): void {
|
||||
let uri = URI.file('test.js');
|
||||
let model = modelService.createModel(text.join('\n'), new StaticLanguageSelector(mode.getLanguageIdentifier()), uri);
|
||||
|
||||
let actual = new TokenTreeSelectionRangeProvider().provideSelectionRanges(model, new Position(lineNumber, column));
|
||||
|
||||
|
||||
let actualStr = actual.map(r => new Range(r.startLineNumber, r.startColumn, r.endLineNumber, r.endColumn).toString());
|
||||
let desiredStr = ranges.reverse().map(r => String(r));
|
||||
|
||||
assert.deepEqual(actualStr, desiredStr);
|
||||
|
||||
modelService.destroyModel(uri);
|
||||
}
|
||||
|
||||
test('getRangesToPosition #1', () => {
|
||||
|
||||
return assertGetRangesToPosition([
|
||||
'function a(bar, foo){',
|
||||
'\tif (bar) {',
|
||||
'\t\treturn (bar + (2 * foo))',
|
||||
'\t}',
|
||||
'}'
|
||||
], 3, 20, [
|
||||
new Range(1, 1, 5, 2),
|
||||
new Range(1, 21, 5, 2),
|
||||
new Range(2, 1, 4, 3),
|
||||
new Range(2, 11, 4, 3),
|
||||
new Range(3, 1, 4, 2),
|
||||
new Range(3, 1, 3, 27),
|
||||
new Range(3, 10, 3, 27),
|
||||
new Range(3, 11, 3, 26),
|
||||
new Range(3, 17, 3, 26),
|
||||
new Range(3, 18, 3, 25),
|
||||
// new Range(3, 19, 3, 20)
|
||||
]);
|
||||
});
|
||||
|
||||
test('getRangesToPosition #56886. Skip empty lines correctly.', () => {
|
||||
|
||||
return assertGetRangesToPosition([
|
||||
'function a(bar, foo){',
|
||||
'\tif (bar) {',
|
||||
'',
|
||||
'\t}',
|
||||
'}'
|
||||
], 3, 1, [
|
||||
new Range(1, 1, 5, 2),
|
||||
new Range(1, 21, 5, 2),
|
||||
new Range(2, 1, 4, 3),
|
||||
new Range(2, 11, 4, 3)
|
||||
]);
|
||||
});
|
||||
|
||||
test('getRangesToPosition #56886. Do not skip lines with only whitespaces.', () => {
|
||||
|
||||
return assertGetRangesToPosition([
|
||||
'function a(bar, foo){',
|
||||
'\tif (bar) {',
|
||||
' ',
|
||||
'\t}',
|
||||
'}'
|
||||
], 3, 1, [
|
||||
new Range(1, 1, 5, 2),
|
||||
new Range(1, 21, 5, 2),
|
||||
new Range(2, 1, 4, 3),
|
||||
new Range(2, 11, 4, 3),
|
||||
new Range(3, 1, 4, 2),
|
||||
new Range(3, 1, 3, 2)
|
||||
]);
|
||||
});
|
||||
|
||||
test('getRangesToPosition #40658. Cursor at first position inside brackets should select line inside.', () => {
|
||||
|
||||
return assertGetRangesToPosition([
|
||||
' [ ]',
|
||||
' { } ',
|
||||
'( ) '
|
||||
], 2, 3, [
|
||||
new Range(1, 1, 3, 5),
|
||||
new Range(2, 1, 2, 6),
|
||||
new Range(2, 2, 2, 5),
|
||||
new Range(2, 3, 2, 4)
|
||||
]);
|
||||
});
|
||||
|
||||
test('getRangesToPosition #40658. Cursor in empty brackets should reveal brackets first.', () => {
|
||||
|
||||
return assertGetRangesToPosition([
|
||||
' [] ',
|
||||
' { } ',
|
||||
' ( ) '
|
||||
], 1, 3, [
|
||||
new Range(1, 1, 3, 7),
|
||||
new Range(1, 1, 1, 5),
|
||||
new Range(1, 2, 1, 4)
|
||||
]);
|
||||
});
|
||||
|
||||
test('getRangesToPosition #40658. Tokens before bracket will be revealed first.', () => {
|
||||
|
||||
return assertGetRangesToPosition([
|
||||
' [] ',
|
||||
' { } ',
|
||||
'selectthis( ) '
|
||||
], 3, 11, [
|
||||
new Range(1, 1, 3, 15),
|
||||
new Range(3, 1, 3, 15),
|
||||
new Range(3, 1, 3, 11)
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
class TestTextResourcePropertiesService implements ITextResourcePropertiesService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
constructor(
|
||||
@IConfigurationService private configurationService: IConfigurationService,
|
||||
) {
|
||||
}
|
||||
|
||||
getEOL(resource: URI): string {
|
||||
const filesConfiguration = this.configurationService.getValue<{ eol: string }>('files');
|
||||
if (filesConfiguration && filesConfiguration.eol) {
|
||||
if (filesConfiguration.eol !== 'auto') {
|
||||
return filesConfiguration.eol;
|
||||
}
|
||||
}
|
||||
return (isLinux || isMacintosh) ? '\n' : '\r\n';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,452 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { LineTokens } from 'vs/editor/common/core/lineTokens';
|
||||
import { ignoreBracketsInToken } from 'vs/editor/common/modes/supports';
|
||||
import { BracketsUtils, RichEditBrackets } from 'vs/editor/common/modes/supports/richEditBrackets';
|
||||
import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
|
||||
import { LanguageId, StandardTokenType, SelectionRangeProvider } from 'vs/editor/common/modes';
|
||||
|
||||
export class TokenTreeSelectionRangeProvider implements SelectionRangeProvider {
|
||||
|
||||
provideSelectionRanges(model: ITextModel, position: Position): 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;
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
}
|
||||
|
||||
export const enum TokenTreeBracket {
|
||||
None = 0,
|
||||
Open = 1,
|
||||
Close = -1
|
||||
}
|
||||
|
||||
export class Node {
|
||||
|
||||
start: Position;
|
||||
|
||||
end: Position;
|
||||
|
||||
get range(): Range {
|
||||
return new Range(
|
||||
this.start.lineNumber,
|
||||
this.start.column,
|
||||
this.end.lineNumber,
|
||||
this.end.column
|
||||
);
|
||||
}
|
||||
|
||||
parent: Node;
|
||||
}
|
||||
|
||||
export class NodeList extends Node {
|
||||
|
||||
children: Node[];
|
||||
|
||||
get start(): Position {
|
||||
return this.hasChildren
|
||||
? this.children[0].start
|
||||
: this.parent.start;
|
||||
}
|
||||
|
||||
get end(): Position {
|
||||
return this.hasChildren
|
||||
? this.children[this.children.length - 1].end
|
||||
: this.parent.end;
|
||||
}
|
||||
|
||||
get hasChildren() {
|
||||
return this.children && this.children.length > 0;
|
||||
}
|
||||
|
||||
get isEmpty() {
|
||||
return !this.hasChildren && !this.parent;
|
||||
}
|
||||
|
||||
public append(node: Node | null): boolean {
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
node.parent = this;
|
||||
if (!this.children) {
|
||||
this.children = [];
|
||||
}
|
||||
if (node instanceof NodeList) {
|
||||
if (node.children) {
|
||||
this.children.push.apply(this.children, node.children);
|
||||
}
|
||||
} else {
|
||||
this.children.push(node);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class Block extends Node {
|
||||
|
||||
open: Node;
|
||||
close: Node;
|
||||
elements: NodeList;
|
||||
|
||||
get start(): Position {
|
||||
return this.open.start;
|
||||
}
|
||||
|
||||
get end(): Position {
|
||||
return this.close.end;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.elements = new NodeList();
|
||||
this.elements.parent = this;
|
||||
}
|
||||
}
|
||||
|
||||
class Token {
|
||||
_tokenBrand: void;
|
||||
|
||||
readonly range: Range;
|
||||
readonly bracket: TokenTreeBracket;
|
||||
readonly bracketType: string | null;
|
||||
|
||||
constructor(range: Range, bracket: TokenTreeBracket, bracketType: string | null) {
|
||||
this.range = range;
|
||||
this.bracket = bracket;
|
||||
this.bracketType = bracketType;
|
||||
}
|
||||
}
|
||||
|
||||
function newNode(token: Token): Node {
|
||||
let node = new Node();
|
||||
node.start = token.range.getStartPosition();
|
||||
node.end = token.range.getEndPosition();
|
||||
return node;
|
||||
}
|
||||
|
||||
class RawToken {
|
||||
_basicTokenBrand: void;
|
||||
|
||||
public lineNumber: number;
|
||||
public lineText: string;
|
||||
public startOffset: number;
|
||||
public endOffset: number;
|
||||
public type: StandardTokenType;
|
||||
public languageId: LanguageId;
|
||||
|
||||
constructor(source: LineTokens, tokenIndex: number, lineNumber: number) {
|
||||
this.lineNumber = lineNumber;
|
||||
this.lineText = source.getLineContent();
|
||||
this.startOffset = source.getStartOffset(tokenIndex);
|
||||
this.endOffset = source.getEndOffset(tokenIndex);
|
||||
this.type = source.getStandardTokenType(tokenIndex);
|
||||
this.languageId = source.getLanguageId(tokenIndex);
|
||||
}
|
||||
}
|
||||
|
||||
class ModelRawTokenScanner {
|
||||
|
||||
private _model: ITextModel;
|
||||
private _lineCount: number;
|
||||
private _versionId: number;
|
||||
private _lineNumber: number;
|
||||
private _tokenIndex: number;
|
||||
private _lineTokens: LineTokens | null;
|
||||
|
||||
constructor(model: ITextModel) {
|
||||
this._model = model;
|
||||
this._lineCount = this._model.getLineCount();
|
||||
this._versionId = this._model.getVersionId();
|
||||
this._lineNumber = 0;
|
||||
this._tokenIndex = 0;
|
||||
this._lineTokens = null;
|
||||
this._advance();
|
||||
}
|
||||
|
||||
private _advance(): void {
|
||||
if (this._lineTokens) {
|
||||
this._tokenIndex++;
|
||||
if (this._tokenIndex >= this._lineTokens.getCount()) {
|
||||
this._lineTokens = null;
|
||||
}
|
||||
}
|
||||
|
||||
while (this._lineNumber < this._lineCount && !this._lineTokens) {
|
||||
this._lineNumber++;
|
||||
this._model.forceTokenization(this._lineNumber);
|
||||
this._lineTokens = this._model.getLineTokens(this._lineNumber);
|
||||
this._tokenIndex = 0;
|
||||
if (this._lineTokens.getLineContent().length === 0) {
|
||||
// Skip empty lines
|
||||
this._lineTokens = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public next(): RawToken | null {
|
||||
if (!this._lineTokens) {
|
||||
return null;
|
||||
}
|
||||
if (this._model.getVersionId() !== this._versionId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let result = new RawToken(this._lineTokens, this._tokenIndex, this._lineNumber);
|
||||
this._advance();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class TokenScanner {
|
||||
|
||||
private _rawTokenScanner: ModelRawTokenScanner;
|
||||
private _nextBuff: Token[];
|
||||
|
||||
private _cachedLanguageBrackets: RichEditBrackets | null;
|
||||
private _cachedLanguageId: LanguageId;
|
||||
|
||||
constructor(model: ITextModel) {
|
||||
this._rawTokenScanner = new ModelRawTokenScanner(model);
|
||||
this._nextBuff = [];
|
||||
this._cachedLanguageBrackets = null;
|
||||
this._cachedLanguageId = -1;
|
||||
}
|
||||
|
||||
next(): Token | null {
|
||||
if (this._nextBuff.length > 0) {
|
||||
return this._nextBuff.shift()!;
|
||||
}
|
||||
|
||||
const token = this._rawTokenScanner.next();
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
const lineNumber = token.lineNumber;
|
||||
const lineText = token.lineText;
|
||||
const tokenType = token.type;
|
||||
let startOffset = token.startOffset;
|
||||
const endOffset = token.endOffset;
|
||||
|
||||
if (this._cachedLanguageId !== token.languageId) {
|
||||
this._cachedLanguageId = token.languageId;
|
||||
this._cachedLanguageBrackets = LanguageConfigurationRegistry.getBracketsSupport(this._cachedLanguageId);
|
||||
}
|
||||
const modeBrackets = this._cachedLanguageBrackets;
|
||||
|
||||
if (!modeBrackets || ignoreBracketsInToken(tokenType)) {
|
||||
return new Token(
|
||||
new Range(lineNumber, startOffset + 1, lineNumber, endOffset + 1),
|
||||
TokenTreeBracket.None,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
let foundBracket: Range | null;
|
||||
do {
|
||||
foundBracket = BracketsUtils.findNextBracketInToken(modeBrackets.forwardRegex, lineNumber, lineText, startOffset, endOffset);
|
||||
if (foundBracket) {
|
||||
const foundBracketStartOffset = foundBracket.startColumn - 1;
|
||||
const foundBracketEndOffset = foundBracket.endColumn - 1;
|
||||
|
||||
if (startOffset < foundBracketStartOffset) {
|
||||
// there is some text before this bracket in this token
|
||||
this._nextBuff.push(new Token(
|
||||
new Range(lineNumber, startOffset + 1, lineNumber, foundBracketStartOffset + 1),
|
||||
TokenTreeBracket.None,
|
||||
null
|
||||
));
|
||||
}
|
||||
|
||||
let bracketText = lineText.substring(foundBracketStartOffset, foundBracketEndOffset);
|
||||
bracketText = bracketText.toLowerCase();
|
||||
|
||||
const bracketData = modeBrackets.textIsBracket[bracketText];
|
||||
const bracketIsOpen = modeBrackets.textIsOpenBracket[bracketText];
|
||||
|
||||
this._nextBuff.push(new Token(
|
||||
new Range(lineNumber, foundBracketStartOffset + 1, lineNumber, foundBracketEndOffset + 1),
|
||||
bracketIsOpen ? TokenTreeBracket.Open : TokenTreeBracket.Close,
|
||||
`${bracketData.languageIdentifier.language};${bracketData.open};${bracketData.close}`
|
||||
));
|
||||
|
||||
startOffset = foundBracketEndOffset;
|
||||
}
|
||||
} while (foundBracket);
|
||||
|
||||
if (startOffset < endOffset) {
|
||||
// there is some remaining none-bracket text in this token
|
||||
this._nextBuff.push(new Token(
|
||||
new Range(lineNumber, startOffset + 1, lineNumber, endOffset + 1),
|
||||
TokenTreeBracket.None,
|
||||
null
|
||||
));
|
||||
}
|
||||
|
||||
return this._nextBuff.shift() || null;
|
||||
}
|
||||
}
|
||||
|
||||
class TokenTreeBuilder {
|
||||
|
||||
private _scanner: TokenScanner;
|
||||
private _stack: Token[] = [];
|
||||
private _currentToken: Token;
|
||||
|
||||
constructor(model: ITextModel) {
|
||||
this._scanner = new TokenScanner(model);
|
||||
}
|
||||
|
||||
public build(): Node {
|
||||
let node = new NodeList();
|
||||
while (node.append(this._line() || this._any())) {
|
||||
// accept all
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
private _accept(condt: (info: Token) => boolean): boolean {
|
||||
let token = this._stack.pop() || this._scanner.next();
|
||||
if (!token) {
|
||||
return false;
|
||||
}
|
||||
let accepted = condt(token);
|
||||
if (!accepted) {
|
||||
this._stack.push(token);
|
||||
// this._currentToken = null;
|
||||
} else {
|
||||
this._currentToken = token;
|
||||
// console.log('accepted: ' + token.__debugContent);
|
||||
}
|
||||
return accepted;
|
||||
}
|
||||
|
||||
private _peek(condt: (info: Token) => boolean): boolean {
|
||||
let ret = false;
|
||||
this._accept(info => {
|
||||
ret = condt(info);
|
||||
return false;
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
private _line(): Node | null {
|
||||
let node = new NodeList();
|
||||
let lineNumber: number;
|
||||
|
||||
// capture current linenumber
|
||||
this._peek(info => {
|
||||
lineNumber = info.range.startLineNumber;
|
||||
return false;
|
||||
});
|
||||
|
||||
while (this._peek(info => info.range.startLineNumber === lineNumber)
|
||||
&& node.append(this._token() || this._block())) {
|
||||
|
||||
// all children that started on this line
|
||||
}
|
||||
|
||||
if (!node.children || node.children.length === 0) {
|
||||
return null;
|
||||
} else if (node.children.length === 1) {
|
||||
return node.children[0];
|
||||
} else {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
private _token(): Node | null {
|
||||
if (!this._accept(token => token.bracket === TokenTreeBracket.None)) {
|
||||
return null;
|
||||
}
|
||||
return newNode(this._currentToken);
|
||||
}
|
||||
|
||||
private _block(): Node | null {
|
||||
|
||||
let bracketType: string | null;
|
||||
let accepted: boolean;
|
||||
|
||||
accepted = this._accept(token => {
|
||||
bracketType = token.bracketType;
|
||||
return token.bracket === TokenTreeBracket.Open;
|
||||
});
|
||||
if (!accepted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let bracket = new Block();
|
||||
bracket.open = newNode(this._currentToken);
|
||||
while (bracket.elements.append(this._line())) {
|
||||
// inside brackets
|
||||
}
|
||||
|
||||
if (!this._accept(token => token.bracket === TokenTreeBracket.Close && token.bracketType === bracketType)) {
|
||||
// missing closing bracket -> return just a node list
|
||||
let nodelist = new NodeList();
|
||||
nodelist.append(bracket.open);
|
||||
nodelist.append(bracket.elements);
|
||||
return nodelist;
|
||||
}
|
||||
|
||||
bracket.close = newNode(this._currentToken);
|
||||
return bracket;
|
||||
}
|
||||
|
||||
private _any(): Node | null {
|
||||
if (!this._accept(_ => true)) {
|
||||
return null;
|
||||
}
|
||||
return newNode(this._currentToken);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses this grammar:
|
||||
* grammer = { line }
|
||||
* line = { block | "token" }
|
||||
* block = "open_bracket" { line } "close_bracket"
|
||||
*/
|
||||
export function build(model: ITextModel): Node {
|
||||
let node = new TokenTreeBuilder(model).build();
|
||||
return node;
|
||||
}
|
||||
|
||||
export function find(node: Node, position: Position): Node | null {
|
||||
if (node instanceof NodeList && node.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!Range.containsPosition(node.range, position)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let result: Node | null = null;
|
||||
|
||||
if (node instanceof NodeList) {
|
||||
if (node.hasChildren) {
|
||||
for (let i = 0, len = node.children.length; i < len && !result; i++) {
|
||||
result = find(node.children[i], position);
|
||||
}
|
||||
}
|
||||
|
||||
} else if (node instanceof Block) {
|
||||
result = find(node.elements, position) || find(node.open, position) || find(node.close, position);
|
||||
}
|
||||
|
||||
return result || node;
|
||||
}
|
||||
@@ -3,29 +3,81 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { SelectionRangeProvider } from 'vs/editor/common/modes';
|
||||
import { SelectionRangeProvider, SelectionRange } from 'vs/editor/common/modes';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { CharCode } from 'vs/base/common/charCode';
|
||||
import { isUpperAsciiLetter, isLowerAsciiLetter } from 'vs/base/common/strings';
|
||||
|
||||
export class WordSelectionRangeProvider implements SelectionRangeProvider {
|
||||
|
||||
provideSelectionRanges(model: ITextModel, position: Position): Range[] {
|
||||
let result: Range[] = [];
|
||||
provideSelectionRanges(model: ITextModel, position: Position): SelectionRange[] {
|
||||
let result: SelectionRange[] = [];
|
||||
this._addInWordRanges(result, model, position);
|
||||
this._addWordRanges(result, model, position);
|
||||
this._addLineRanges(result, model, position);
|
||||
this._addWhitespaceLine(result, model, position);
|
||||
result.push({ range: model.getFullModelRange(), kind: 'statement.all' });
|
||||
return result;
|
||||
}
|
||||
|
||||
private _addWordRanges(bucket: Range[], model: ITextModel, pos: Position): void {
|
||||
const word = model.getWordAtPosition(pos);
|
||||
if (word) {
|
||||
bucket.push(new Range(pos.lineNumber, word.startColumn, pos.lineNumber, word.endColumn));
|
||||
private _addInWordRanges(bucket: SelectionRange[], model: ITextModel, pos: Position): void {
|
||||
const obj = model.getWordAtPosition(pos);
|
||||
if (!obj) {
|
||||
return;
|
||||
}
|
||||
|
||||
let { word, startColumn } = obj;
|
||||
let offset = pos.column - startColumn;
|
||||
let start = offset;
|
||||
let end = offset;
|
||||
let lastCh: number = 0;
|
||||
|
||||
// LEFT anchor (start)
|
||||
for (; start >= 0; start--) {
|
||||
let ch = word.charCodeAt(start);
|
||||
if (ch === CharCode.Underline || ch === CharCode.Dash) {
|
||||
// foo-bar OR foo_bar
|
||||
break;
|
||||
} else if (isLowerAsciiLetter(ch) && isUpperAsciiLetter(lastCh)) {
|
||||
// fooBar
|
||||
break;
|
||||
}
|
||||
lastCh = ch;
|
||||
}
|
||||
start += 1;
|
||||
|
||||
// RIGHT anchor (end)
|
||||
for (; end < word.length; end++) {
|
||||
let ch = word.charCodeAt(end);
|
||||
if (isUpperAsciiLetter(ch) && isLowerAsciiLetter(lastCh)) {
|
||||
// fooBar
|
||||
break;
|
||||
} else if (ch === CharCode.Underline || ch === CharCode.Dash) {
|
||||
// foo-bar OR foo_bar
|
||||
break;
|
||||
}
|
||||
lastCh = ch;
|
||||
}
|
||||
|
||||
if (start < end) {
|
||||
bucket.push({ range: new Range(pos.lineNumber, startColumn + start, pos.lineNumber, startColumn + end), kind: 'statement.word.part' });
|
||||
}
|
||||
}
|
||||
|
||||
private _addLineRanges(bucket: Range[], model: ITextModel, pos: Position): void {
|
||||
bucket.push(new Range(pos.lineNumber, model.getLineFirstNonWhitespaceColumn(pos.lineNumber), pos.lineNumber, model.getLineLastNonWhitespaceColumn(pos.lineNumber)));
|
||||
bucket.push(new Range(pos.lineNumber, model.getLineMinColumn(pos.lineNumber), pos.lineNumber, model.getLineMaxColumn(pos.lineNumber)));
|
||||
private _addWordRanges(bucket: SelectionRange[], model: ITextModel, pos: Position): void {
|
||||
const word = model.getWordAtPosition(pos);
|
||||
if (word) {
|
||||
bucket.push({ range: new Range(pos.lineNumber, word.startColumn, pos.lineNumber, word.endColumn), kind: 'statement.word' });
|
||||
}
|
||||
}
|
||||
|
||||
private _addWhitespaceLine(bucket: SelectionRange[], model: ITextModel, pos: Position): void {
|
||||
if (model.getLineLength(pos.lineNumber) > 0
|
||||
&& model.getLineFirstNonWhitespaceColumn(pos.lineNumber) === 0
|
||||
&& model.getLineLastNonWhitespaceColumn(pos.lineNumber) === 0
|
||||
) {
|
||||
bucket.push({ range: new Range(pos.lineNumber, 1, pos.lineNumber, model.getLineMaxColumn(pos.lineNumber)), kind: 'statement.line' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user