Path: blob/main/src/vs/editor/contrib/smartSelect/browser/smartSelect.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as arrays from '../../../../base/common/arrays.js';6import { CancellationToken } from '../../../../base/common/cancellation.js';7import { onUnexpectedExternalError } from '../../../../base/common/errors.js';8import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';9import { IDisposable } from '../../../../base/common/lifecycle.js';10import { ICodeEditor } from '../../../browser/editorBrowser.js';11import { EditorAction, EditorContributionInstantiation, IActionOptions, registerEditorAction, registerEditorContribution, ServicesAccessor } from '../../../browser/editorExtensions.js';12import { EditorOption } from '../../../common/config/editorOptions.js';13import { Position } from '../../../common/core/position.js';14import { Range } from '../../../common/core/range.js';15import { Selection } from '../../../common/core/selection.js';16import { IEditorContribution } from '../../../common/editorCommon.js';17import { EditorContextKeys } from '../../../common/editorContextKeys.js';18import { ITextModel } from '../../../common/model.js';19import * as languages from '../../../common/languages.js';20import { BracketSelectionRangeProvider } from './bracketSelections.js';21import { WordSelectionRangeProvider } from './wordSelections.js';22import * as nls from '../../../../nls.js';23import { MenuId } from '../../../../platform/actions/common/actions.js';24import { CommandsRegistry } from '../../../../platform/commands/common/commands.js';25import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';26import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';27import { LanguageFeatureRegistry } from '../../../common/languageFeatureRegistry.js';28import { ITextModelService } from '../../../common/services/resolverService.js';29import { assertType } from '../../../../base/common/types.js';30import { URI } from '../../../../base/common/uri.js';3132class SelectionRanges {3334constructor(35readonly index: number,36readonly ranges: Range[]37) { }3839mov(fwd: boolean): SelectionRanges {40const index = this.index + (fwd ? 1 : -1);41if (index < 0 || index >= this.ranges.length) {42return this;43}44const res = new SelectionRanges(index, this.ranges);45if (res.ranges[index].equalsRange(this.ranges[this.index])) {46// next range equals this range, retry with next-next47return res.mov(fwd);48}49return res;50}51}5253export class SmartSelectController implements IEditorContribution {5455static readonly ID = 'editor.contrib.smartSelectController';5657static get(editor: ICodeEditor): SmartSelectController | null {58return editor.getContribution<SmartSelectController>(SmartSelectController.ID);59}6061private _state?: SelectionRanges[];62private _selectionListener?: IDisposable;63private _ignoreSelection: boolean = false;6465constructor(66private readonly _editor: ICodeEditor,67@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,68) { }6970dispose(): void {71this._selectionListener?.dispose();72}7374async run(forward: boolean): Promise<void> {75if (!this._editor.hasModel()) {76return;77}7879const selections = this._editor.getSelections();80const model = this._editor.getModel();8182if (!this._state) {8384await provideSelectionRanges(this._languageFeaturesService.selectionRangeProvider, model, selections.map(s => s.getPosition()), this._editor.getOption(EditorOption.smartSelect), CancellationToken.None).then(ranges => {85if (!arrays.isNonEmptyArray(ranges) || ranges.length !== selections.length) {86// invalid result87return;88}89if (!this._editor.hasModel() || !arrays.equals(this._editor.getSelections(), selections, (a, b) => a.equalsSelection(b))) {90// invalid editor state91return;92}9394for (let i = 0; i < ranges.length; i++) {95ranges[i] = ranges[i].filter(range => {96// filter ranges inside the selection97return range.containsPosition(selections[i].getStartPosition()) && range.containsPosition(selections[i].getEndPosition());98});99// prepend current selection100ranges[i].unshift(selections[i]);101}102103104this._state = ranges.map(ranges => new SelectionRanges(0, ranges));105106// listen to caret move and forget about state107this._selectionListener?.dispose();108this._selectionListener = this._editor.onDidChangeCursorPosition(() => {109if (!this._ignoreSelection) {110this._selectionListener?.dispose();111this._state = undefined;112}113});114});115}116117if (!this._state) {118// no state119return;120}121this._state = this._state.map(state => state.mov(forward));122const newSelections = this._state.map(state => Selection.fromPositions(state.ranges[state.index].getStartPosition(), state.ranges[state.index].getEndPosition()));123this._ignoreSelection = true;124try {125this._editor.setSelections(newSelections);126} finally {127this._ignoreSelection = false;128}129}130}131132abstract class AbstractSmartSelect extends EditorAction {133134private readonly _forward: boolean;135136constructor(forward: boolean, opts: IActionOptions) {137super(opts);138this._forward = forward;139}140141async run(_accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {142const controller = SmartSelectController.get(editor);143if (controller) {144await controller.run(this._forward);145}146}147}148149class GrowSelectionAction extends AbstractSmartSelect {150constructor() {151super(true, {152id: 'editor.action.smartSelect.expand',153label: nls.localize2('smartSelect.expand', "Expand Selection"),154precondition: undefined,155kbOpts: {156kbExpr: EditorContextKeys.editorTextFocus,157primary: KeyMod.Shift | KeyMod.Alt | KeyCode.RightArrow,158mac: {159primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyMod.Shift | KeyCode.RightArrow,160secondary: [KeyMod.WinCtrl | KeyMod.Shift | KeyCode.RightArrow],161},162weight: KeybindingWeight.EditorContrib163},164menuOpts: {165menuId: MenuId.MenubarSelectionMenu,166group: '1_basic',167title: nls.localize({ key: 'miSmartSelectGrow', comment: ['&& denotes a mnemonic'] }, "&&Expand Selection"),168order: 2169}170});171}172}173174// renamed command id175CommandsRegistry.registerCommandAlias('editor.action.smartSelect.grow', 'editor.action.smartSelect.expand');176177class ShrinkSelectionAction extends AbstractSmartSelect {178constructor() {179super(false, {180id: 'editor.action.smartSelect.shrink',181label: nls.localize2('smartSelect.shrink', "Shrink Selection"),182precondition: undefined,183kbOpts: {184kbExpr: EditorContextKeys.editorTextFocus,185primary: KeyMod.Shift | KeyMod.Alt | KeyCode.LeftArrow,186mac: {187primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyMod.Shift | KeyCode.LeftArrow,188secondary: [KeyMod.WinCtrl | KeyMod.Shift | KeyCode.LeftArrow],189},190weight: KeybindingWeight.EditorContrib191},192menuOpts: {193menuId: MenuId.MenubarSelectionMenu,194group: '1_basic',195title: nls.localize({ key: 'miSmartSelectShrink', comment: ['&& denotes a mnemonic'] }, "&&Shrink Selection"),196order: 3197}198});199}200}201202registerEditorContribution(SmartSelectController.ID, SmartSelectController, EditorContributionInstantiation.Lazy);203registerEditorAction(GrowSelectionAction);204registerEditorAction(ShrinkSelectionAction);205206export interface SelectionRangesOptions {207selectLeadingAndTrailingWhitespace: boolean;208selectSubwords: boolean;209}210211export async function provideSelectionRanges(registry: LanguageFeatureRegistry<languages.SelectionRangeProvider>, model: ITextModel, positions: Position[], options: SelectionRangesOptions, token: CancellationToken): Promise<Range[][]> {212213const providers = registry.all(model)214.concat(new WordSelectionRangeProvider(options.selectSubwords)); // ALWAYS have word based selection range215216if (providers.length === 1) {217// add word selection and bracket selection when no provider exists218providers.unshift(new BracketSelectionRangeProvider());219}220221const work: Promise<any>[] = [];222const allRawRanges: Range[][] = [];223224for (const provider of providers) {225226work.push(Promise.resolve(provider.provideSelectionRanges(model, positions, token)).then(allProviderRanges => {227if (arrays.isNonEmptyArray(allProviderRanges) && allProviderRanges.length === positions.length) {228for (let i = 0; i < positions.length; i++) {229if (!allRawRanges[i]) {230allRawRanges[i] = [];231}232for (const oneProviderRanges of allProviderRanges[i]) {233if (Range.isIRange(oneProviderRanges.range) && Range.containsPosition(oneProviderRanges.range, positions[i])) {234allRawRanges[i].push(Range.lift(oneProviderRanges.range));235}236}237}238}239}, onUnexpectedExternalError));240}241242await Promise.all(work);243244return allRawRanges.map(oneRawRanges => {245246if (oneRawRanges.length === 0) {247return [];248}249250// sort all by start/end position251oneRawRanges.sort((a, b) => {252if (Position.isBefore(a.getStartPosition(), b.getStartPosition())) {253return 1;254} else if (Position.isBefore(b.getStartPosition(), a.getStartPosition())) {255return -1;256} else if (Position.isBefore(a.getEndPosition(), b.getEndPosition())) {257return -1;258} else if (Position.isBefore(b.getEndPosition(), a.getEndPosition())) {259return 1;260} else {261return 0;262}263});264265// remove ranges that don't contain the former range or that are equal to the266// former range267const oneRanges: Range[] = [];268let last: Range | undefined;269for (const range of oneRawRanges) {270if (!last || (Range.containsRange(range, last) && !Range.equalsRange(range, last))) {271oneRanges.push(range);272last = range;273}274}275276if (!options.selectLeadingAndTrailingWhitespace) {277return oneRanges;278}279280// add ranges that expand trivia at line starts and ends whenever a range281// wraps onto the a new line282const oneRangesWithTrivia: Range[] = [oneRanges[0]];283for (let i = 1; i < oneRanges.length; i++) {284const prev = oneRanges[i - 1];285const cur = oneRanges[i];286if (cur.startLineNumber !== prev.startLineNumber || cur.endLineNumber !== prev.endLineNumber) {287// add line/block range without leading/failing whitespace288const rangeNoWhitespace = new Range(prev.startLineNumber, model.getLineFirstNonWhitespaceColumn(prev.startLineNumber), prev.endLineNumber, model.getLineLastNonWhitespaceColumn(prev.endLineNumber));289if (rangeNoWhitespace.containsRange(prev) && !rangeNoWhitespace.equalsRange(prev) && cur.containsRange(rangeNoWhitespace) && !cur.equalsRange(rangeNoWhitespace)) {290oneRangesWithTrivia.push(rangeNoWhitespace);291}292// add line/block range293const rangeFull = new Range(prev.startLineNumber, 1, prev.endLineNumber, model.getLineMaxColumn(prev.endLineNumber));294if (rangeFull.containsRange(prev) && !rangeFull.equalsRange(rangeNoWhitespace) && cur.containsRange(rangeFull) && !cur.equalsRange(rangeFull)) {295oneRangesWithTrivia.push(rangeFull);296}297}298oneRangesWithTrivia.push(cur);299}300return oneRangesWithTrivia;301});302}303304305CommandsRegistry.registerCommand('_executeSelectionRangeProvider', async function (accessor, ...args) {306307const [resource, positions] = args;308assertType(URI.isUri(resource));309310const registry = accessor.get(ILanguageFeaturesService).selectionRangeProvider;311const reference = await accessor.get(ITextModelService).createModelReference(resource);312313try {314return provideSelectionRanges(registry, reference.object.textEditorModel, positions, { selectLeadingAndTrailingWhitespace: true, selectSubwords: true }, CancellationToken.None);315} finally {316reference.dispose();317}318});319320321