Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/smartSelect/browser/smartSelect.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as arrays from '../../../../base/common/arrays.js';
7
import { CancellationToken } from '../../../../base/common/cancellation.js';
8
import { onUnexpectedExternalError } from '../../../../base/common/errors.js';
9
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
10
import { IDisposable } from '../../../../base/common/lifecycle.js';
11
import { ICodeEditor } from '../../../browser/editorBrowser.js';
12
import { EditorAction, EditorContributionInstantiation, IActionOptions, registerEditorAction, registerEditorContribution, ServicesAccessor } from '../../../browser/editorExtensions.js';
13
import { EditorOption } from '../../../common/config/editorOptions.js';
14
import { Position } from '../../../common/core/position.js';
15
import { Range } from '../../../common/core/range.js';
16
import { Selection } from '../../../common/core/selection.js';
17
import { IEditorContribution } from '../../../common/editorCommon.js';
18
import { EditorContextKeys } from '../../../common/editorContextKeys.js';
19
import { ITextModel } from '../../../common/model.js';
20
import * as languages from '../../../common/languages.js';
21
import { BracketSelectionRangeProvider } from './bracketSelections.js';
22
import { WordSelectionRangeProvider } from './wordSelections.js';
23
import * as nls from '../../../../nls.js';
24
import { MenuId } from '../../../../platform/actions/common/actions.js';
25
import { CommandsRegistry } from '../../../../platform/commands/common/commands.js';
26
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
27
import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';
28
import { LanguageFeatureRegistry } from '../../../common/languageFeatureRegistry.js';
29
import { ITextModelService } from '../../../common/services/resolverService.js';
30
import { assertType } from '../../../../base/common/types.js';
31
import { URI } from '../../../../base/common/uri.js';
32
33
class SelectionRanges {
34
35
constructor(
36
readonly index: number,
37
readonly ranges: Range[]
38
) { }
39
40
mov(fwd: boolean): SelectionRanges {
41
const index = this.index + (fwd ? 1 : -1);
42
if (index < 0 || index >= this.ranges.length) {
43
return this;
44
}
45
const res = new SelectionRanges(index, this.ranges);
46
if (res.ranges[index].equalsRange(this.ranges[this.index])) {
47
// next range equals this range, retry with next-next
48
return res.mov(fwd);
49
}
50
return res;
51
}
52
}
53
54
export class SmartSelectController implements IEditorContribution {
55
56
static readonly ID = 'editor.contrib.smartSelectController';
57
58
static get(editor: ICodeEditor): SmartSelectController | null {
59
return editor.getContribution<SmartSelectController>(SmartSelectController.ID);
60
}
61
62
private _state?: SelectionRanges[];
63
private _selectionListener?: IDisposable;
64
private _ignoreSelection: boolean = false;
65
66
constructor(
67
private readonly _editor: ICodeEditor,
68
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
69
) { }
70
71
dispose(): void {
72
this._selectionListener?.dispose();
73
}
74
75
async run(forward: boolean): Promise<void> {
76
if (!this._editor.hasModel()) {
77
return;
78
}
79
80
const selections = this._editor.getSelections();
81
const model = this._editor.getModel();
82
83
if (!this._state) {
84
85
await provideSelectionRanges(this._languageFeaturesService.selectionRangeProvider, model, selections.map(s => s.getPosition()), this._editor.getOption(EditorOption.smartSelect), CancellationToken.None).then(ranges => {
86
if (!arrays.isNonEmptyArray(ranges) || ranges.length !== selections.length) {
87
// invalid result
88
return;
89
}
90
if (!this._editor.hasModel() || !arrays.equals(this._editor.getSelections(), selections, (a, b) => a.equalsSelection(b))) {
91
// invalid editor state
92
return;
93
}
94
95
for (let i = 0; i < ranges.length; i++) {
96
ranges[i] = ranges[i].filter(range => {
97
// filter ranges inside the selection
98
return range.containsPosition(selections[i].getStartPosition()) && range.containsPosition(selections[i].getEndPosition());
99
});
100
// prepend current selection
101
ranges[i].unshift(selections[i]);
102
}
103
104
105
this._state = ranges.map(ranges => new SelectionRanges(0, ranges));
106
107
// listen to caret move and forget about state
108
this._selectionListener?.dispose();
109
this._selectionListener = this._editor.onDidChangeCursorPosition(() => {
110
if (!this._ignoreSelection) {
111
this._selectionListener?.dispose();
112
this._state = undefined;
113
}
114
});
115
});
116
}
117
118
if (!this._state) {
119
// no state
120
return;
121
}
122
this._state = this._state.map(state => state.mov(forward));
123
const newSelections = this._state.map(state => Selection.fromPositions(state.ranges[state.index].getStartPosition(), state.ranges[state.index].getEndPosition()));
124
this._ignoreSelection = true;
125
try {
126
this._editor.setSelections(newSelections);
127
} finally {
128
this._ignoreSelection = false;
129
}
130
}
131
}
132
133
abstract class AbstractSmartSelect extends EditorAction {
134
135
private readonly _forward: boolean;
136
137
constructor(forward: boolean, opts: IActionOptions) {
138
super(opts);
139
this._forward = forward;
140
}
141
142
async run(_accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {
143
const controller = SmartSelectController.get(editor);
144
if (controller) {
145
await controller.run(this._forward);
146
}
147
}
148
}
149
150
class GrowSelectionAction extends AbstractSmartSelect {
151
constructor() {
152
super(true, {
153
id: 'editor.action.smartSelect.expand',
154
label: nls.localize2('smartSelect.expand', "Expand Selection"),
155
precondition: undefined,
156
kbOpts: {
157
kbExpr: EditorContextKeys.editorTextFocus,
158
primary: KeyMod.Shift | KeyMod.Alt | KeyCode.RightArrow,
159
mac: {
160
primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyMod.Shift | KeyCode.RightArrow,
161
secondary: [KeyMod.WinCtrl | KeyMod.Shift | KeyCode.RightArrow],
162
},
163
weight: KeybindingWeight.EditorContrib
164
},
165
menuOpts: {
166
menuId: MenuId.MenubarSelectionMenu,
167
group: '1_basic',
168
title: nls.localize({ key: 'miSmartSelectGrow', comment: ['&& denotes a mnemonic'] }, "&&Expand Selection"),
169
order: 2
170
}
171
});
172
}
173
}
174
175
// renamed command id
176
CommandsRegistry.registerCommandAlias('editor.action.smartSelect.grow', 'editor.action.smartSelect.expand');
177
178
class ShrinkSelectionAction extends AbstractSmartSelect {
179
constructor() {
180
super(false, {
181
id: 'editor.action.smartSelect.shrink',
182
label: nls.localize2('smartSelect.shrink', "Shrink Selection"),
183
precondition: undefined,
184
kbOpts: {
185
kbExpr: EditorContextKeys.editorTextFocus,
186
primary: KeyMod.Shift | KeyMod.Alt | KeyCode.LeftArrow,
187
mac: {
188
primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyMod.Shift | KeyCode.LeftArrow,
189
secondary: [KeyMod.WinCtrl | KeyMod.Shift | KeyCode.LeftArrow],
190
},
191
weight: KeybindingWeight.EditorContrib
192
},
193
menuOpts: {
194
menuId: MenuId.MenubarSelectionMenu,
195
group: '1_basic',
196
title: nls.localize({ key: 'miSmartSelectShrink', comment: ['&& denotes a mnemonic'] }, "&&Shrink Selection"),
197
order: 3
198
}
199
});
200
}
201
}
202
203
registerEditorContribution(SmartSelectController.ID, SmartSelectController, EditorContributionInstantiation.Lazy);
204
registerEditorAction(GrowSelectionAction);
205
registerEditorAction(ShrinkSelectionAction);
206
207
export interface SelectionRangesOptions {
208
selectLeadingAndTrailingWhitespace: boolean;
209
selectSubwords: boolean;
210
}
211
212
export async function provideSelectionRanges(registry: LanguageFeatureRegistry<languages.SelectionRangeProvider>, model: ITextModel, positions: Position[], options: SelectionRangesOptions, token: CancellationToken): Promise<Range[][]> {
213
214
const providers = registry.all(model)
215
.concat(new WordSelectionRangeProvider(options.selectSubwords)); // ALWAYS have word based selection range
216
217
if (providers.length === 1) {
218
// add word selection and bracket selection when no provider exists
219
providers.unshift(new BracketSelectionRangeProvider());
220
}
221
222
const work: Promise<any>[] = [];
223
const allRawRanges: Range[][] = [];
224
225
for (const provider of providers) {
226
227
work.push(Promise.resolve(provider.provideSelectionRanges(model, positions, token)).then(allProviderRanges => {
228
if (arrays.isNonEmptyArray(allProviderRanges) && allProviderRanges.length === positions.length) {
229
for (let i = 0; i < positions.length; i++) {
230
if (!allRawRanges[i]) {
231
allRawRanges[i] = [];
232
}
233
for (const oneProviderRanges of allProviderRanges[i]) {
234
if (Range.isIRange(oneProviderRanges.range) && Range.containsPosition(oneProviderRanges.range, positions[i])) {
235
allRawRanges[i].push(Range.lift(oneProviderRanges.range));
236
}
237
}
238
}
239
}
240
}, onUnexpectedExternalError));
241
}
242
243
await Promise.all(work);
244
245
return allRawRanges.map(oneRawRanges => {
246
247
if (oneRawRanges.length === 0) {
248
return [];
249
}
250
251
// sort all by start/end position
252
oneRawRanges.sort((a, b) => {
253
if (Position.isBefore(a.getStartPosition(), b.getStartPosition())) {
254
return 1;
255
} else if (Position.isBefore(b.getStartPosition(), a.getStartPosition())) {
256
return -1;
257
} else if (Position.isBefore(a.getEndPosition(), b.getEndPosition())) {
258
return -1;
259
} else if (Position.isBefore(b.getEndPosition(), a.getEndPosition())) {
260
return 1;
261
} else {
262
return 0;
263
}
264
});
265
266
// remove ranges that don't contain the former range or that are equal to the
267
// former range
268
const oneRanges: Range[] = [];
269
let last: Range | undefined;
270
for (const range of oneRawRanges) {
271
if (!last || (Range.containsRange(range, last) && !Range.equalsRange(range, last))) {
272
oneRanges.push(range);
273
last = range;
274
}
275
}
276
277
if (!options.selectLeadingAndTrailingWhitespace) {
278
return oneRanges;
279
}
280
281
// add ranges that expand trivia at line starts and ends whenever a range
282
// wraps onto the a new line
283
const oneRangesWithTrivia: Range[] = [oneRanges[0]];
284
for (let i = 1; i < oneRanges.length; i++) {
285
const prev = oneRanges[i - 1];
286
const cur = oneRanges[i];
287
if (cur.startLineNumber !== prev.startLineNumber || cur.endLineNumber !== prev.endLineNumber) {
288
// add line/block range without leading/failing whitespace
289
const rangeNoWhitespace = new Range(prev.startLineNumber, model.getLineFirstNonWhitespaceColumn(prev.startLineNumber), prev.endLineNumber, model.getLineLastNonWhitespaceColumn(prev.endLineNumber));
290
if (rangeNoWhitespace.containsRange(prev) && !rangeNoWhitespace.equalsRange(prev) && cur.containsRange(rangeNoWhitespace) && !cur.equalsRange(rangeNoWhitespace)) {
291
oneRangesWithTrivia.push(rangeNoWhitespace);
292
}
293
// add line/block range
294
const rangeFull = new Range(prev.startLineNumber, 1, prev.endLineNumber, model.getLineMaxColumn(prev.endLineNumber));
295
if (rangeFull.containsRange(prev) && !rangeFull.equalsRange(rangeNoWhitespace) && cur.containsRange(rangeFull) && !cur.equalsRange(rangeFull)) {
296
oneRangesWithTrivia.push(rangeFull);
297
}
298
}
299
oneRangesWithTrivia.push(cur);
300
}
301
return oneRangesWithTrivia;
302
});
303
}
304
305
306
CommandsRegistry.registerCommand('_executeSelectionRangeProvider', async function (accessor, ...args) {
307
308
const [resource, positions] = args;
309
assertType(URI.isUri(resource));
310
311
const registry = accessor.get(ILanguageFeaturesService).selectionRangeProvider;
312
const reference = await accessor.get(ITextModelService).createModelReference(resource);
313
314
try {
315
return provideSelectionRanges(registry, reference.object.textEditorModel, positions, { selectLeadingAndTrailingWhitespace: true, selectSubwords: true }, CancellationToken.None);
316
} finally {
317
reference.dispose();
318
}
319
});
320
321