Merge VS Code 1.31.1 (#4283)

This commit is contained in:
Matt Irvine
2019-03-15 13:09:45 -07:00
committed by GitHub
parent 7d31575149
commit 86bac90001
1716 changed files with 53308 additions and 48375 deletions

View File

@@ -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;
}
}

View File

@@ -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;
});
}

View 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),
);
});
});

View File

@@ -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';
}
}

View File

@@ -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;
}

View File

@@ -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' });
}
}
}