Path: blob/main/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts
5251 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 strings from '../../../../base/common/strings.js';6import { ShiftCommand } from '../../../common/commands/shiftCommand.js';7import { EditorAutoIndentStrategy } from '../../../common/config/editorOptions.js';8import { Range } from '../../../common/core/range.js';9import { Selection } from '../../../common/core/selection.js';10import { ICommand, ICursorStateComputerData, IEditOperationBuilder } from '../../../common/editorCommon.js';11import { ITextModel } from '../../../common/model.js';12import { CompleteEnterAction, IndentAction } from '../../../common/languages/languageConfiguration.js';13import { ILanguageConfigurationService } from '../../../common/languages/languageConfigurationRegistry.js';14import { IndentConsts } from '../../../common/languages/supports/indentRules.js';15import * as indentUtils from '../../indentation/common/indentUtils.js';16import { getGoodIndentForLine, getIndentMetadata, IIndentConverter, IVirtualModel } from '../../../common/languages/autoIndent.js';17import { getEnterAction } from '../../../common/languages/enterAction.js';1819export class MoveLinesCommand implements ICommand {2021private readonly _selection: Selection;22private readonly _isMovingDown: boolean;23private readonly _autoIndent: EditorAutoIndentStrategy;2425private _selectionId: string | null;26private _moveEndPositionDown?: boolean;27private _moveEndLineSelectionShrink: boolean;2829constructor(30selection: Selection,31isMovingDown: boolean,32autoIndent: EditorAutoIndentStrategy,33@ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService34) {35this._selection = selection;36this._isMovingDown = isMovingDown;37this._autoIndent = autoIndent;38this._selectionId = null;39this._moveEndLineSelectionShrink = false;40}4142private createVirtualModel(43model: ITextModel,44lineNumberMapper: (lineNumber: number) => number,45contentOverride?: (lineNumber: number) => string | undefined46): IVirtualModel {47return {48tokenization: {49getLineTokens: (lineNumber) => model.tokenization.getLineTokens(lineNumberMapper(lineNumber)),50getLanguageId: () => model.getLanguageId(),51getLanguageIdAtPosition: (lineNumber, column) => model.getLanguageIdAtPosition(lineNumber, column)52},53getLineContent: (lineNumber) => {54const customContent = contentOverride?.(lineNumber);55if (customContent !== undefined) {56return customContent;57}58return model.getLineContent(lineNumberMapper(lineNumber));59}60};61}6263public getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void {6465const modelLineCount = model.getLineCount();6667if (this._isMovingDown && this._selection.endLineNumber === modelLineCount) {68this._selectionId = builder.trackSelection(this._selection);69return;70}71if (!this._isMovingDown && this._selection.startLineNumber === 1) {72this._selectionId = builder.trackSelection(this._selection);73return;74}7576this._moveEndPositionDown = false;77let s = this._selection;7879if (s.startLineNumber < s.endLineNumber && s.endColumn === 1) {80this._moveEndPositionDown = true;81s = s.setEndPosition(s.endLineNumber - 1, model.getLineMaxColumn(s.endLineNumber - 1));82}8384const { tabSize, indentSize, insertSpaces } = model.getOptions();85const indentConverter = this.buildIndentConverter(tabSize, indentSize, insertSpaces);8687if (s.startLineNumber === s.endLineNumber && model.getLineMaxColumn(s.startLineNumber) === 1) {88// Current line is empty89const lineNumber = s.startLineNumber;90const otherLineNumber = (this._isMovingDown ? lineNumber + 1 : lineNumber - 1);9192if (model.getLineMaxColumn(otherLineNumber) === 1) {93// Other line number is empty too, so no editing is needed94// Add a no-op to force running by the model95builder.addEditOperation(new Range(1, 1, 1, 1), null);96} else {97// Type content from other line number on line number98builder.addEditOperation(new Range(lineNumber, 1, lineNumber, 1), model.getLineContent(otherLineNumber));99100// Remove content from other line number101builder.addEditOperation(new Range(otherLineNumber, 1, otherLineNumber, model.getLineMaxColumn(otherLineNumber)), null);102}103// Track selection at the other line number104s = new Selection(otherLineNumber, 1, otherLineNumber, 1);105106} else {107108let movingLineNumber: number;109let movingLineText: string;110111if (this._isMovingDown) {112movingLineNumber = s.endLineNumber + 1;113movingLineText = model.getLineContent(movingLineNumber);114// Delete line that needs to be moved115builder.addEditOperation(new Range(movingLineNumber - 1, model.getLineMaxColumn(movingLineNumber - 1), movingLineNumber, model.getLineMaxColumn(movingLineNumber)), null);116117let insertingText = movingLineText;118119if (this.shouldAutoIndent(model, s)) {120const movingLineMatchResult = this.matchEnterRule(model, indentConverter, tabSize, movingLineNumber, s.startLineNumber - 1);121// if s.startLineNumber - 1 matches onEnter rule, we still honor that.122if (movingLineMatchResult !== null) {123const oldIndentation = strings.getLeadingWhitespace(model.getLineContent(movingLineNumber));124const newSpaceCnt = movingLineMatchResult + indentUtils.getSpaceCnt(oldIndentation, tabSize);125const newIndentation = indentUtils.generateIndent(newSpaceCnt, tabSize, insertSpaces);126insertingText = newIndentation + this.trimStart(movingLineText);127} else {128// no enter rule matches, let's check indentatin rules then.129const virtualModel = this.createVirtualModel(130model,131(lineNumber) => lineNumber === s.startLineNumber ? movingLineNumber : lineNumber132);133const indentOfMovingLine = getGoodIndentForLine(134this._autoIndent,135virtualModel,136model.getLanguageIdAtPosition(movingLineNumber, 1),137s.startLineNumber,138indentConverter,139this._languageConfigurationService140);141if (indentOfMovingLine !== null) {142const oldIndentation = strings.getLeadingWhitespace(model.getLineContent(movingLineNumber));143const newSpaceCnt = indentUtils.getSpaceCnt(indentOfMovingLine, tabSize);144const oldSpaceCnt = indentUtils.getSpaceCnt(oldIndentation, tabSize);145if (newSpaceCnt !== oldSpaceCnt) {146const newIndentation = indentUtils.generateIndent(newSpaceCnt, tabSize, insertSpaces);147insertingText = newIndentation + this.trimStart(movingLineText);148}149}150}151152// add edit operations for moving line first to make sure it's executed after we make indentation change153// to s.startLineNumber154builder.addEditOperation(new Range(s.startLineNumber, 1, s.startLineNumber, 1), insertingText + '\n');155156const ret = this.matchEnterRuleMovingDown(model, indentConverter, tabSize, s.startLineNumber, movingLineNumber, insertingText);157158// check if the line being moved before matches onEnter rules, if so let's adjust the indentation by onEnter rules.159if (ret !== null) {160if (ret !== 0) {161this.getIndentEditsOfMovingBlock(model, builder, s, tabSize, insertSpaces, ret);162}163} else {164// it doesn't match onEnter rules, let's check indentation rules then.165const virtualModel = this.createVirtualModel(166model,167(lineNumber) => {168if (lineNumber === s.startLineNumber) {169// TODO@aiday-mar: the tokens here don't correspond exactly to the corresponding content (after indentation adjustment), have to fix this.170return movingLineNumber;171} else if (lineNumber >= s.startLineNumber + 1 && lineNumber <= s.endLineNumber + 1) {172return lineNumber - 1;173} else {174return lineNumber;175}176},177(lineNumber) => lineNumber === s.startLineNumber ? insertingText : undefined178);179180const newIndentatOfMovingBlock = getGoodIndentForLine(181this._autoIndent,182virtualModel,183model.getLanguageIdAtPosition(movingLineNumber, 1),184s.startLineNumber + 1,185indentConverter,186this._languageConfigurationService187);188189if (newIndentatOfMovingBlock !== null) {190const oldIndentation = strings.getLeadingWhitespace(model.getLineContent(s.startLineNumber));191const newSpaceCnt = indentUtils.getSpaceCnt(newIndentatOfMovingBlock, tabSize);192const oldSpaceCnt = indentUtils.getSpaceCnt(oldIndentation, tabSize);193if (newSpaceCnt !== oldSpaceCnt) {194const spaceCntOffset = newSpaceCnt - oldSpaceCnt;195196this.getIndentEditsOfMovingBlock(model, builder, s, tabSize, insertSpaces, spaceCntOffset);197}198}199}200} else {201// Insert line that needs to be moved before202builder.addEditOperation(new Range(s.startLineNumber, 1, s.startLineNumber, 1), insertingText + '\n');203}204} else {205movingLineNumber = s.startLineNumber - 1;206movingLineText = model.getLineContent(movingLineNumber);207208// Delete line that needs to be moved209builder.addEditOperation(new Range(movingLineNumber, 1, movingLineNumber + 1, 1), null);210211// Insert line that needs to be moved after212builder.addEditOperation(new Range(s.endLineNumber, model.getLineMaxColumn(s.endLineNumber), s.endLineNumber, model.getLineMaxColumn(s.endLineNumber)), '\n' + movingLineText);213214if (this.shouldAutoIndent(model, s)) {215const virtualModel = this.createVirtualModel(216model,217(lineNumber) => lineNumber === movingLineNumber ? s.startLineNumber : lineNumber218);219220const ret = this.matchEnterRule(model, indentConverter, tabSize, s.startLineNumber, s.startLineNumber - 2);221// check if s.startLineNumber - 2 matches onEnter rules, if so adjust the moving block by onEnter rules.222if (ret !== null) {223if (ret !== 0) {224this.getIndentEditsOfMovingBlock(model, builder, s, tabSize, insertSpaces, ret);225}226} else {227// it doesn't match any onEnter rule, let's check indentation rules then.228const indentOfFirstLine = getGoodIndentForLine(229this._autoIndent,230virtualModel,231model.getLanguageIdAtPosition(s.startLineNumber, 1),232movingLineNumber,233indentConverter,234this._languageConfigurationService235);236if (indentOfFirstLine !== null) {237// adjust the indentation of the moving block238const oldIndent = strings.getLeadingWhitespace(model.getLineContent(s.startLineNumber));239const newSpaceCnt = indentUtils.getSpaceCnt(indentOfFirstLine, tabSize);240const oldSpaceCnt = indentUtils.getSpaceCnt(oldIndent, tabSize);241if (newSpaceCnt !== oldSpaceCnt) {242const spaceCntOffset = newSpaceCnt - oldSpaceCnt;243244this.getIndentEditsOfMovingBlock(model, builder, s, tabSize, insertSpaces, spaceCntOffset);245}246}247}248}249}250}251252this._selectionId = builder.trackSelection(s);253}254255private buildIndentConverter(tabSize: number, indentSize: number, insertSpaces: boolean): IIndentConverter {256return {257shiftIndent: (indentation) => {258return ShiftCommand.shiftIndent(indentation, indentation.length + 1, tabSize, indentSize, insertSpaces);259},260unshiftIndent: (indentation) => {261return ShiftCommand.unshiftIndent(indentation, indentation.length + 1, tabSize, indentSize, insertSpaces);262}263};264}265266private parseEnterResult(model: ITextModel, indentConverter: IIndentConverter, tabSize: number, line: number, enter: CompleteEnterAction | null) {267if (enter) {268let enterPrefix = enter.indentation;269270if (enter.indentAction === IndentAction.None) {271enterPrefix = enter.indentation + enter.appendText;272} else if (enter.indentAction === IndentAction.Indent) {273enterPrefix = enter.indentation + enter.appendText;274} else if (enter.indentAction === IndentAction.IndentOutdent) {275enterPrefix = enter.indentation;276} else if (enter.indentAction === IndentAction.Outdent) {277enterPrefix = indentConverter.unshiftIndent(enter.indentation) + enter.appendText;278}279const movingLineText = model.getLineContent(line);280if (this.trimStart(movingLineText).indexOf(this.trimStart(enterPrefix)) >= 0) {281const oldIndentation = strings.getLeadingWhitespace(model.getLineContent(line));282let newIndentation = strings.getLeadingWhitespace(enterPrefix);283const indentMetadataOfMovelingLine = getIndentMetadata(model, line, this._languageConfigurationService);284if (indentMetadataOfMovelingLine !== null && indentMetadataOfMovelingLine & IndentConsts.DECREASE_MASK) {285newIndentation = indentConverter.unshiftIndent(newIndentation);286}287const newSpaceCnt = indentUtils.getSpaceCnt(newIndentation, tabSize);288const oldSpaceCnt = indentUtils.getSpaceCnt(oldIndentation, tabSize);289return newSpaceCnt - oldSpaceCnt;290}291}292293return null;294}295296/**297*298* @param model299* @param indentConverter300* @param tabSize301* @param line the line moving down302* @param futureAboveLineNumber the line which will be at the `line` position303* @param futureAboveLineText304*/305private matchEnterRuleMovingDown(model: ITextModel, indentConverter: IIndentConverter, tabSize: number, line: number, futureAboveLineNumber: number, futureAboveLineText: string) {306if (strings.lastNonWhitespaceIndex(futureAboveLineText) >= 0) {307// break308const maxColumn = model.getLineMaxColumn(futureAboveLineNumber);309const enter = getEnterAction(this._autoIndent, model, new Range(futureAboveLineNumber, maxColumn, futureAboveLineNumber, maxColumn), this._languageConfigurationService);310return this.parseEnterResult(model, indentConverter, tabSize, line, enter);311} else {312// go upwards, starting from `line - 1`313let validPrecedingLine = line - 1;314while (validPrecedingLine >= 1) {315const lineContent = model.getLineContent(validPrecedingLine);316const nonWhitespaceIdx = strings.lastNonWhitespaceIndex(lineContent);317318if (nonWhitespaceIdx >= 0) {319break;320}321322validPrecedingLine--;323}324325if (validPrecedingLine < 1 || line > model.getLineCount()) {326return null;327}328329const maxColumn = model.getLineMaxColumn(validPrecedingLine);330const enter = getEnterAction(this._autoIndent, model, new Range(validPrecedingLine, maxColumn, validPrecedingLine, maxColumn), this._languageConfigurationService);331return this.parseEnterResult(model, indentConverter, tabSize, line, enter);332}333}334335private matchEnterRule(model: ITextModel, indentConverter: IIndentConverter, tabSize: number, line: number, oneLineAbove: number, previousLineText?: string) {336let validPrecedingLine = oneLineAbove;337while (validPrecedingLine >= 1) {338// ship empty lines as empty lines just inherit indentation339let lineContent;340if (validPrecedingLine === oneLineAbove && previousLineText !== undefined) {341lineContent = previousLineText;342} else {343lineContent = model.getLineContent(validPrecedingLine);344}345346const nonWhitespaceIdx = strings.lastNonWhitespaceIndex(lineContent);347if (nonWhitespaceIdx >= 0) {348break;349}350validPrecedingLine--;351}352353if (validPrecedingLine < 1 || line > model.getLineCount()) {354return null;355}356357const maxColumn = model.getLineMaxColumn(validPrecedingLine);358const enter = getEnterAction(this._autoIndent, model, new Range(validPrecedingLine, maxColumn, validPrecedingLine, maxColumn), this._languageConfigurationService);359return this.parseEnterResult(model, indentConverter, tabSize, line, enter);360}361362private trimStart(str: string) {363return str.replace(/^\s+/, '');364}365366private shouldAutoIndent(model: ITextModel, selection: Selection) {367if (this._autoIndent < EditorAutoIndentStrategy.Full) {368return false;369}370// if it's not easy to tokenize, we stop auto indent.371if (!model.tokenization.isCheapToTokenize(selection.startLineNumber)) {372return false;373}374const languageAtSelectionStart = model.getLanguageIdAtPosition(selection.startLineNumber, 1);375const languageAtSelectionEnd = model.getLanguageIdAtPosition(selection.endLineNumber, 1);376377if (languageAtSelectionStart !== languageAtSelectionEnd) {378return false;379}380381if (this._languageConfigurationService.getLanguageConfiguration(languageAtSelectionStart).indentRulesSupport === null) {382return false;383}384385return true;386}387388private getIndentEditsOfMovingBlock(model: ITextModel, builder: IEditOperationBuilder, s: Selection, tabSize: number, insertSpaces: boolean, offset: number) {389for (let i = s.startLineNumber; i <= s.endLineNumber; i++) {390const lineContent = model.getLineContent(i);391const originalIndent = strings.getLeadingWhitespace(lineContent);392const originalSpacesCnt = indentUtils.getSpaceCnt(originalIndent, tabSize);393const newSpacesCnt = originalSpacesCnt + offset;394const newIndent = indentUtils.generateIndent(newSpacesCnt, tabSize, insertSpaces);395396if (newIndent !== originalIndent) {397builder.addEditOperation(new Range(i, 1, i, originalIndent.length + 1), newIndent);398399if (i === s.endLineNumber && s.endColumn <= originalIndent.length + 1 && newIndent === '') {400// as users select part of the original indent white spaces401// when we adjust the indentation of endLine, we should adjust the cursor position as well.402this._moveEndLineSelectionShrink = true;403}404}405406}407}408409public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection {410let result = helper.getTrackedSelection(this._selectionId!);411412if (this._moveEndPositionDown) {413result = result.setEndPosition(result.endLineNumber + 1, 1);414}415416if (this._moveEndLineSelectionShrink && result.startLineNumber < result.endLineNumber) {417result = result.setEndPosition(result.endLineNumber, 2);418}419420return result;421}422}423424425