提交 d50b568d 编写于 作者: J Johannes Rieken

multiple cursor in internal api of smart select, #41838

上级 8bbe6acd
......@@ -1064,7 +1064,7 @@ export interface SelectionRangeProvider {
/**
* Provide ranges that should be selected from the given position.
*/
provideSelectionRanges(model: model.ITextModel, position: Position, token: CancellationToken): ProviderResult<SelectionRange[]>;
provideSelectionRanges(model: model.ITextModel, positions: Position[], token: CancellationToken): ProviderResult<SelectionRange[][]>;
}
export interface FoldingContext {
......
......@@ -11,12 +11,19 @@ import { LinkedList } from 'vs/base/common/linkedList';
export class BracketSelectionRangeProvider implements SelectionRangeProvider {
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);
async provideSelectionRanges(model: ITextModel, positions: Position[]): Promise<SelectionRange[][]> {
const result: SelectionRange[][] = [];
for (const position of positions) {
const bucket: SelectionRange[] = [];
result.push(bucket);
const ranges = new Map<string, LinkedList<Range>>();
await new Promise(resolve => BracketSelectionRangeProvider._bracketsRightYield(resolve, 0, model, position, ranges));
await new Promise(resolve => BracketSelectionRangeProvider._bracketsLeftYield(resolve, 0, model, position, ranges, bucket));
}
return result;
}
private static readonly _maxDuration = 30;
......
......@@ -86,7 +86,7 @@ class SmartSelectController implements IEditorContribution {
let promise: Promise<void> = Promise.resolve(undefined);
if (!this._state) {
promise = provideSelectionRangesN(model, selections.map(s => s.getPosition()), CancellationToken.None).then(ranges => {
promise = provideSelectionRanges(model, selections.map(s => s.getPosition()), CancellationToken.None).then(ranges => {
if (!arrays.isNonEmptyArray(ranges) || ranges.length !== selections.length) {
// invalid result
return;
......@@ -210,99 +210,93 @@ registerEditorAction(ShrinkSelectionAction);
// word selection
modes.SelectionRangeRegistry.register('*', new WordSelectionRangeProvider());
export function provideSelectionRanges(model: ITextModel, position: Position, token: CancellationToken): Promise<Range[] | undefined | null> {
export function provideSelectionRanges(model: ITextModel, positions: Position[], token: CancellationToken): Promise<Range[][]> {
const provider = modes.SelectionRangeRegistry.orderedGroups(model);
const providers = modes.SelectionRangeRegistry.all(model);
if (provider.length === 1) {
if (providers.length === 1) {
// add word selection and bracket selection when no provider exists
provider.unshift([new BracketSelectionRangeProvider()]);
}
interface RankedRange {
rank: number;
range: Range;
providers.unshift(new BracketSelectionRangeProvider());
}
let work: Promise<any>[] = [];
let ranges: RankedRange[] = [];
let rank = 0;
for (const group of provider) {
rank += 1;
for (const prov of group) {
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 });
let allRawRanges: Range[][] = [];
arrays.fill(positions.length, [], allRawRanges);
for (const provider of providers) {
work.push(Promise.resolve(provider.provideSelectionRanges(model, positions, token)).then(allProviderRanges => {
if (arrays.isNonEmptyArray(allProviderRanges) && allProviderRanges.length === positions.length) {
for (let i = 0; i < positions.length; i++) {
for (const oneProviderRanges of allProviderRanges[i]) {
if (Range.isIRange(oneProviderRanges.range) && Range.containsPosition(oneProviderRanges.range, positions[i])) {
allRawRanges[i].push(Range.lift(oneProviderRanges.range));
}
}
}
}));
}
}
}));
}
return Promise.all(work).then(() => {
if (ranges.length === 0) {
return [];
}
return allRawRanges.map(oneRawRanges => {
ranges.sort((a, b) => {
if (Position.isBefore(a.range.getStartPosition(), b.range.getStartPosition())) {
return 1;
} else if (Position.isBefore(b.range.getStartPosition(), a.range.getStartPosition())) {
return -1;
} else if (Position.isBefore(a.range.getEndPosition(), b.range.getEndPosition())) {
return -1;
} else if (Position.isBefore(b.range.getEndPosition(), a.range.getEndPosition())) {
return 1;
} else {
return b.rank - a.rank;
if (oneRawRanges.length === 0) {
return [];
}
});
let result: Range[] = [];
let last: Range | undefined;
for (const { range } of ranges) {
if (!last || (Range.containsRange(range, last) && !Range.equalsRange(range, last))) {
result.push(range);
last = range;
}
}
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);
// sort all by start/end position
oneRawRanges.sort((a, b) => {
if (Position.isBefore(a.getStartPosition(), b.getStartPosition())) {
return 1;
} else if (Position.isBefore(b.getStartPosition(), a.getStartPosition())) {
return -1;
} else if (Position.isBefore(a.getEndPosition(), b.getEndPosition())) {
return -1;
} else if (Position.isBefore(b.getEndPosition(), a.getEndPosition())) {
return 1;
} else {
return 0;
}
// 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);
});
// remove ranges that don't contain the former range or that are equal to the
// former range
let oneRanges: Range[] = [];
let last: Range | undefined;
for (const range of oneRawRanges) {
if (!last || (Range.containsRange(range, last) && !Range.equalsRange(range, last))) {
oneRanges.push(range);
last = range;
}
}
result2.push(cur);
}
return result2;
});
}
export function provideSelectionRangesN(model: ITextModel, position: Position[], token: CancellationToken): Promise<Range[][]> {
return Promise.all(position.map(pos => {
return provideSelectionRanges(model, pos, token).then(value => {
return value || [];
// add ranges that expand trivia at line starts and ends whenever a range
// wraps onto the a new line
let oneRangesWithTrivia: Range[] = [oneRanges[0]];
for (let i = 1; i < oneRanges.length; i++) {
const prev = oneRanges[i - 1];
const cur = oneRanges[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)) {
oneRangesWithTrivia.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)) {
oneRangesWithTrivia.push(rangeFull);
}
}
oneRangesWithTrivia.push(cur);
}
return oneRangesWithTrivia;
});
}));
});
}
registerDefaultLanguageCommand('_executeSelectionRangeProvider', function (model, position) {
return provideSelectionRanges(model, position, CancellationToken.None);
registerDefaultLanguageCommand('_executeSelectionRangeProvider', function (model, _position, args) {
return provideSelectionRanges(model, args.positions, CancellationToken.None);
});
......@@ -79,7 +79,7 @@ suite('SmartSelect', () => {
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 [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));
......@@ -203,7 +203,9 @@ suite('SmartSelect', () => {
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);
let all = await provider.provideSelectionRanges(model, [pos], CancellationToken.None);
let ranges = all![0];
modelService.destroyModel(model.uri);
assert.equal(expected.length, ranges!.length);
......
......@@ -12,12 +12,16 @@ import { isUpperAsciiLetter, isLowerAsciiLetter } from 'vs/base/common/strings';
export class WordSelectionRangeProvider implements SelectionRangeProvider {
provideSelectionRanges(model: ITextModel, position: Position): SelectionRange[] {
let result: SelectionRange[] = [];
this._addInWordRanges(result, model, position);
this._addWordRanges(result, model, position);
this._addWhitespaceLine(result, model, position);
result.push({ range: model.getFullModelRange(), kind: 'statement.all' });
provideSelectionRanges(model: ITextModel, positions: Position[]): SelectionRange[][] {
const result: SelectionRange[][] = [];
for (const position of positions) {
const bucket: SelectionRange[] = [];
result.push(bucket);
this._addInWordRanges(bucket, model, position);
this._addWordRanges(bucket, model, position);
this._addWhitespaceLine(bucket, model, position);
bucket.push({ range: model.getFullModelRange(), kind: 'statement.all' });
}
return result;
}
......
......@@ -34,11 +34,11 @@ export abstract class WordDistance {
return Promise.resolve(WordDistance.None);
}
return new BracketSelectionRangeProvider().provideSelectionRanges(model, position).then(ranges => {
if (!ranges || ranges.length === 0) {
return new BracketSelectionRangeProvider().provideSelectionRanges(model, [position]).then(ranges => {
if (!ranges || ranges.length === 0 || ranges[0].length === 0) {
return WordDistance.None;
}
return service.computeWordRanges(model.uri, ranges[0].range).then(wordRanges => {
return service.computeWordRanges(model.uri, ranges[0][0].range).then(wordRanges => {
return new class extends WordDistance {
distance(anchor: IPosition, suggestion: CompletionItem) {
if (!wordRanges || !position.equals(editor.getPosition())) {
......@@ -55,7 +55,7 @@ export abstract class WordDistance {
let idx = binarySearch(wordLines, Range.fromPositions(anchor), Range.compareRangesUsingStarts);
let bestWordRange = idx >= 0 ? wordLines[idx] : wordLines[Math.max(0, ~idx - 1)];
let blockDistance = ranges.length;
for (const range of ranges) {
for (const range of ranges[0]) {
if (!Range.containsRange(range.range, bestWordRange)) {
break;
}
......
......@@ -5303,7 +5303,7 @@ declare namespace monaco.languages {
/**
* Provide ranges that should be selected from the given position.
*/
provideSelectionRanges(model: editor.ITextModel, position: Position, token: CancellationToken): ProviderResult<SelectionRange[]>;
provideSelectionRanges(model: editor.ITextModel, positions: Position[], token: CancellationToken): ProviderResult<SelectionRange[][]>;
}
export interface FoldingContext {
......
......@@ -408,8 +408,8 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha
$registerSelectionRangeProvider(handle: number, selector: ISerializedDocumentFilter[]): void {
this._registrations[handle] = modes.SelectionRangeRegistry.register(typeConverters.LanguageSelector.from(selector), {
provideSelectionRanges: (model, position, token) => {
return this._proxy.$provideSelectionRanges(handle, model.uri, position, token);
provideSelectionRanges: (model, positions, token) => {
return this._proxy.$provideSelectionRanges(handle, model.uri, positions, token);
}
});
}
......
......@@ -921,7 +921,7 @@ export interface ExtHostLanguageFeaturesShape {
$provideDocumentColors(handle: number, resource: UriComponents, token: CancellationToken): Promise<IRawColorInfo[]>;
$provideColorPresentations(handle: number, resource: UriComponents, colorInfo: IRawColorInfo, token: CancellationToken): Promise<modes.IColorPresentation[]>;
$provideFoldingRanges(handle: number, resource: UriComponents, context: modes.FoldingContext, token: CancellationToken): Promise<modes.FoldingRange[]>;
$provideSelectionRanges(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise<modes.SelectionRange[]>;
$provideSelectionRanges(handle: number, resource: UriComponents, positions: IPosition[], token: CancellationToken): Promise<modes.SelectionRange[][]>;
}
export interface ExtHostQuickOpenShape {
......
......@@ -17,7 +17,7 @@ import { ExtHostCommands } from 'vs/workbench/api/node/extHostCommands';
import { CustomCodeAction } from 'vs/workbench/api/node/extHostLanguageFeatures';
import { ICommandsExecutor, PreviewHTMLAPICommand, OpenFolderAPICommand, DiffAPICommand, OpenAPICommand, RemoveFromRecentlyOpenedAPICommand, SetEditorLayoutAPICommand } from './apiCommands';
import { EditorGroupLayout } from 'vs/workbench/services/editor/common/editorGroupsService';
import { isFalsyOrEmpty, isNonEmptyArray } from 'vs/base/common/arrays';
import { isFalsyOrEmpty } from 'vs/base/common/arrays';
export class ExtHostApiCommands {
......@@ -199,7 +199,7 @@ export class ExtHostApiCommands {
description: 'Execute selection range provider.',
args: [
{ name: 'uri', description: 'Uri of a text document', constraint: URI },
{ name: 'position', description: 'Position in a text document', constraint: types.Position }
{ name: 'positions', description: 'Positions in a text document', constraint: a => Array.isArray(a) }
],
returns: 'A promise that resolves to an array of ranges.'
});
......@@ -420,16 +420,15 @@ export class ExtHostApiCommands {
});
}
private _executeSelectionRangeProvider(resource: URI, position: types.Position): Promise<vscode.SelectionRange[]> {
private _executeSelectionRangeProvider(resource: URI, positions: types.Position[]): Promise<vscode.SelectionRange[][]> {
let pos = positions.map(typeConverters.Position.from);
const args = {
resource,
position: position && typeConverters.Position.from(position)
position: pos[0],
positions: pos
};
return this._commands.executeCommand<modes.SelectionRange[]>('_executeSelectionRangeProvider', args).then(result => {
if (isNonEmptyArray(result)) {
return result.map(typeConverters.SelectionRange.to);
}
return [];
return this._commands.executeCommand<modes.SelectionRange[][]>('_executeSelectionRangeProvider', args).then(result => {
return result.map(oneResult => oneResult.map(typeConverters.SelectionRange.to));
});
}
......
......@@ -876,24 +876,26 @@ class SelectionRangeAdapter {
private readonly _provider: vscode.SelectionRangeProvider
) { }
provideSelectionRanges(resource: URI, position: IPosition, token: CancellationToken): Promise<modes.SelectionRange[]> {
provideSelectionRanges(resource: URI, positions: IPosition[], token: CancellationToken): Promise<modes.SelectionRange[][]> {
const { document } = this._documents.getDocumentData(resource);
const pos = typeConvert.Position.to(position);
return asPromise(() => this._provider.provideSelectionRanges(document, pos, token)).then(selectionRanges => {
if (isFalsyOrEmpty(selectionRanges)) {
return undefined;
}
let result: modes.SelectionRange[] = [];
let last: vscode.Position | vscode.Range = pos;
for (const sel of selectionRanges) {
if (!sel.range.contains(last)) {
throw new Error('INVALID selection range, must contain the previous range');
return Promise.all(positions.map(position => {
const pos = typeConvert.Position.to(position);
return asPromise(() => this._provider.provideSelectionRanges(document, pos, token)).then(selectionRanges => {
if (isFalsyOrEmpty(selectionRanges)) {
return undefined;
}
result.push(typeConvert.SelectionRange.from(sel));
last = sel.range;
}
return result;
});
let oneResult: modes.SelectionRange[] = [];
let last: vscode.Position | vscode.Range = pos;
for (const sel of selectionRanges) {
if (!sel.range.contains(last)) {
throw new Error('INVALID selection range, must contain the previous range');
}
oneResult.push(typeConvert.SelectionRange.from(sel));
last = sel.range;
}
return oneResult;
});
}));
}
}
......@@ -1307,8 +1309,8 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape {
return this._createDisposable(handle);
}
$provideSelectionRanges(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise<modes.SelectionRange[]> {
return this._withAdapter(handle, SelectionRangeAdapter, adapter => adapter.provideSelectionRanges(URI.revive(resource), position, token));
$provideSelectionRanges(handle: number, resource: UriComponents, positions: IPosition[], token: CancellationToken): Promise<modes.SelectionRange[][]> {
return this._withAdapter(handle, SelectionRangeAdapter, adapter => adapter.provideSelectionRanges(URI.revive(resource), positions, token));
}
// --- configuration
......
......@@ -796,7 +796,7 @@ suite('ExtHostLanguageFeatureCommands', function () {
// --- selection ranges
test('Links, back and forth', async function () {
test('Selection Range, back and forth', async function () {
disposables.push(extHost.registerSelectionRangeProvider(nullExtensionDescription, defaultSelector, <vscode.SelectionRangeProvider>{
provideSelectionRanges() {
......@@ -808,8 +808,9 @@ suite('ExtHostLanguageFeatureCommands', function () {
}));
await rpcProtocol.sync();
let value = await commands.executeCommand<vscode.DocumentLink[]>('vscode.executeSelectionRangeProvider', model.uri, new types.Position(0, 10));
assert.ok(value.length >= 2);
let value = await commands.executeCommand<vscode.SelectionRange[][]>('vscode.executeSelectionRangeProvider', model.uri, [new types.Position(0, 10)]);
assert.equal(value.length, 1);
assert.ok(value[0].length >= 2);
});
});
......@@ -1103,8 +1103,9 @@ suite('ExtHostLanguageFeatures', function () {
await rpcProtocol.sync();
provideSelectionRanges(model, new Position(1, 17), CancellationToken.None).then(ranges => {
assert.ok(ranges.length >= 2);
provideSelectionRanges(model, [new Position(1, 17)], CancellationToken.None).then(ranges => {
assert.equal(ranges.length, 1);
assert.ok(ranges[0].length >= 2);
});
});
});
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册