Path: blob/main/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.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 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}4142public getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void {4344const getLanguageId = () => {45return model.getLanguageId();46};47const getLanguageIdAtPosition = (lineNumber: number, column: number) => {48return model.getLanguageIdAtPosition(lineNumber, column);49};5051const modelLineCount = model.getLineCount();5253if (this._isMovingDown && this._selection.endLineNumber === modelLineCount) {54this._selectionId = builder.trackSelection(this._selection);55return;56}57if (!this._isMovingDown && this._selection.startLineNumber === 1) {58this._selectionId = builder.trackSelection(this._selection);59return;60}6162this._moveEndPositionDown = false;63let s = this._selection;6465if (s.startLineNumber < s.endLineNumber && s.endColumn === 1) {66this._moveEndPositionDown = true;67s = s.setEndPosition(s.endLineNumber - 1, model.getLineMaxColumn(s.endLineNumber - 1));68}6970const { tabSize, indentSize, insertSpaces } = model.getOptions();71const indentConverter = this.buildIndentConverter(tabSize, indentSize, insertSpaces);7273if (s.startLineNumber === s.endLineNumber && model.getLineMaxColumn(s.startLineNumber) === 1) {74// Current line is empty75const lineNumber = s.startLineNumber;76const otherLineNumber = (this._isMovingDown ? lineNumber + 1 : lineNumber - 1);7778if (model.getLineMaxColumn(otherLineNumber) === 1) {79// Other line number is empty too, so no editing is needed80// Add a no-op to force running by the model81builder.addEditOperation(new Range(1, 1, 1, 1), null);82} else {83// Type content from other line number on line number84builder.addEditOperation(new Range(lineNumber, 1, lineNumber, 1), model.getLineContent(otherLineNumber));8586// Remove content from other line number87builder.addEditOperation(new Range(otherLineNumber, 1, otherLineNumber, model.getLineMaxColumn(otherLineNumber)), null);88}89// Track selection at the other line number90s = new Selection(otherLineNumber, 1, otherLineNumber, 1);9192} else {9394let movingLineNumber: number;95let movingLineText: string;9697if (this._isMovingDown) {98movingLineNumber = s.endLineNumber + 1;99movingLineText = model.getLineContent(movingLineNumber);100// Delete line that needs to be moved101builder.addEditOperation(new Range(movingLineNumber - 1, model.getLineMaxColumn(movingLineNumber - 1), movingLineNumber, model.getLineMaxColumn(movingLineNumber)), null);102103let insertingText = movingLineText;104105if (this.shouldAutoIndent(model, s)) {106const movingLineMatchResult = this.matchEnterRule(model, indentConverter, tabSize, movingLineNumber, s.startLineNumber - 1);107// if s.startLineNumber - 1 matches onEnter rule, we still honor that.108if (movingLineMatchResult !== null) {109const oldIndentation = strings.getLeadingWhitespace(model.getLineContent(movingLineNumber));110const newSpaceCnt = movingLineMatchResult + indentUtils.getSpaceCnt(oldIndentation, tabSize);111const newIndentation = indentUtils.generateIndent(newSpaceCnt, tabSize, insertSpaces);112insertingText = newIndentation + this.trimStart(movingLineText);113} else {114// no enter rule matches, let's check indentatin rules then.115const virtualModel: IVirtualModel = {116tokenization: {117getLineTokens: (lineNumber: number) => {118if (lineNumber === s.startLineNumber) {119return model.tokenization.getLineTokens(movingLineNumber);120} else {121return model.tokenization.getLineTokens(lineNumber);122}123},124getLanguageId,125getLanguageIdAtPosition,126},127getLineContent: (lineNumber: number) => {128if (lineNumber === s.startLineNumber) {129return model.getLineContent(movingLineNumber);130} else {131return model.getLineContent(lineNumber);132}133},134};135const indentOfMovingLine = getGoodIndentForLine(136this._autoIndent,137virtualModel,138model.getLanguageIdAtPosition(movingLineNumber, 1),139s.startLineNumber,140indentConverter,141this._languageConfigurationService142);143if (indentOfMovingLine !== null) {144const oldIndentation = strings.getLeadingWhitespace(model.getLineContent(movingLineNumber));145const newSpaceCnt = indentUtils.getSpaceCnt(indentOfMovingLine, tabSize);146const oldSpaceCnt = indentUtils.getSpaceCnt(oldIndentation, tabSize);147if (newSpaceCnt !== oldSpaceCnt) {148const newIndentation = indentUtils.generateIndent(newSpaceCnt, tabSize, insertSpaces);149insertingText = newIndentation + this.trimStart(movingLineText);150}151}152}153154// add edit operations for moving line first to make sure it's executed after we make indentation change155// to s.startLineNumber156builder.addEditOperation(new Range(s.startLineNumber, 1, s.startLineNumber, 1), insertingText + '\n');157158const ret = this.matchEnterRuleMovingDown(model, indentConverter, tabSize, s.startLineNumber, movingLineNumber, insertingText);159160// check if the line being moved before matches onEnter rules, if so let's adjust the indentation by onEnter rules.161if (ret !== null) {162if (ret !== 0) {163this.getIndentEditsOfMovingBlock(model, builder, s, tabSize, insertSpaces, ret);164}165} else {166// it doesn't match onEnter rules, let's check indentation rules then.167const virtualModel: IVirtualModel = {168tokenization: {169getLineTokens: (lineNumber: number) => {170if (lineNumber === s.startLineNumber) {171// TODO@aiday-mar: the tokens here don't correspond exactly to the corresponding content (after indentation adjustment), have to fix this.172return model.tokenization.getLineTokens(movingLineNumber);173} else if (lineNumber >= s.startLineNumber + 1 && lineNumber <= s.endLineNumber + 1) {174return model.tokenization.getLineTokens(lineNumber - 1);175} else {176return model.tokenization.getLineTokens(lineNumber);177}178},179getLanguageId,180getLanguageIdAtPosition,181},182getLineContent: (lineNumber: number) => {183if (lineNumber === s.startLineNumber) {184return insertingText;185} else if (lineNumber >= s.startLineNumber + 1 && lineNumber <= s.endLineNumber + 1) {186return model.getLineContent(lineNumber - 1);187} else {188return model.getLineContent(lineNumber);189}190},191};192193const newIndentatOfMovingBlock = getGoodIndentForLine(194this._autoIndent,195virtualModel,196model.getLanguageIdAtPosition(movingLineNumber, 1),197s.startLineNumber + 1,198indentConverter,199this._languageConfigurationService200);201202if (newIndentatOfMovingBlock !== null) {203const oldIndentation = strings.getLeadingWhitespace(model.getLineContent(s.startLineNumber));204const newSpaceCnt = indentUtils.getSpaceCnt(newIndentatOfMovingBlock, tabSize);205const oldSpaceCnt = indentUtils.getSpaceCnt(oldIndentation, tabSize);206if (newSpaceCnt !== oldSpaceCnt) {207const spaceCntOffset = newSpaceCnt - oldSpaceCnt;208209this.getIndentEditsOfMovingBlock(model, builder, s, tabSize, insertSpaces, spaceCntOffset);210}211}212}213} else {214// Insert line that needs to be moved before215builder.addEditOperation(new Range(s.startLineNumber, 1, s.startLineNumber, 1), insertingText + '\n');216}217} else {218movingLineNumber = s.startLineNumber - 1;219movingLineText = model.getLineContent(movingLineNumber);220221// Delete line that needs to be moved222builder.addEditOperation(new Range(movingLineNumber, 1, movingLineNumber + 1, 1), null);223224// Insert line that needs to be moved after225builder.addEditOperation(new Range(s.endLineNumber, model.getLineMaxColumn(s.endLineNumber), s.endLineNumber, model.getLineMaxColumn(s.endLineNumber)), '\n' + movingLineText);226227if (this.shouldAutoIndent(model, s)) {228const virtualModel: IVirtualModel = {229tokenization: {230getLineTokens: (lineNumber: number) => {231if (lineNumber === movingLineNumber) {232return model.tokenization.getLineTokens(s.startLineNumber);233} else {234return model.tokenization.getLineTokens(lineNumber);235}236},237getLanguageId,238getLanguageIdAtPosition,239},240getLineContent: (lineNumber: number) => {241if (lineNumber === movingLineNumber) {242return model.getLineContent(s.startLineNumber);243} else {244return model.getLineContent(lineNumber);245}246},247};248249const ret = this.matchEnterRule(model, indentConverter, tabSize, s.startLineNumber, s.startLineNumber - 2);250// check if s.startLineNumber - 2 matches onEnter rules, if so adjust the moving block by onEnter rules.251if (ret !== null) {252if (ret !== 0) {253this.getIndentEditsOfMovingBlock(model, builder, s, tabSize, insertSpaces, ret);254}255} else {256// it doesn't match any onEnter rule, let's check indentation rules then.257const indentOfFirstLine = getGoodIndentForLine(258this._autoIndent,259virtualModel,260model.getLanguageIdAtPosition(s.startLineNumber, 1),261movingLineNumber,262indentConverter,263this._languageConfigurationService264);265if (indentOfFirstLine !== null) {266// adjust the indentation of the moving block267const oldIndent = strings.getLeadingWhitespace(model.getLineContent(s.startLineNumber));268const newSpaceCnt = indentUtils.getSpaceCnt(indentOfFirstLine, tabSize);269const oldSpaceCnt = indentUtils.getSpaceCnt(oldIndent, tabSize);270if (newSpaceCnt !== oldSpaceCnt) {271const spaceCntOffset = newSpaceCnt - oldSpaceCnt;272273this.getIndentEditsOfMovingBlock(model, builder, s, tabSize, insertSpaces, spaceCntOffset);274}275}276}277}278}279}280281this._selectionId = builder.trackSelection(s);282}283284private buildIndentConverter(tabSize: number, indentSize: number, insertSpaces: boolean): IIndentConverter {285return {286shiftIndent: (indentation) => {287return ShiftCommand.shiftIndent(indentation, indentation.length + 1, tabSize, indentSize, insertSpaces);288},289unshiftIndent: (indentation) => {290return ShiftCommand.unshiftIndent(indentation, indentation.length + 1, tabSize, indentSize, insertSpaces);291}292};293}294295private parseEnterResult(model: ITextModel, indentConverter: IIndentConverter, tabSize: number, line: number, enter: CompleteEnterAction | null) {296if (enter) {297let enterPrefix = enter.indentation;298299if (enter.indentAction === IndentAction.None) {300enterPrefix = enter.indentation + enter.appendText;301} else if (enter.indentAction === IndentAction.Indent) {302enterPrefix = enter.indentation + enter.appendText;303} else if (enter.indentAction === IndentAction.IndentOutdent) {304enterPrefix = enter.indentation;305} else if (enter.indentAction === IndentAction.Outdent) {306enterPrefix = indentConverter.unshiftIndent(enter.indentation) + enter.appendText;307}308const movingLineText = model.getLineContent(line);309if (this.trimStart(movingLineText).indexOf(this.trimStart(enterPrefix)) >= 0) {310const oldIndentation = strings.getLeadingWhitespace(model.getLineContent(line));311let newIndentation = strings.getLeadingWhitespace(enterPrefix);312const indentMetadataOfMovelingLine = getIndentMetadata(model, line, this._languageConfigurationService);313if (indentMetadataOfMovelingLine !== null && indentMetadataOfMovelingLine & IndentConsts.DECREASE_MASK) {314newIndentation = indentConverter.unshiftIndent(newIndentation);315}316const newSpaceCnt = indentUtils.getSpaceCnt(newIndentation, tabSize);317const oldSpaceCnt = indentUtils.getSpaceCnt(oldIndentation, tabSize);318return newSpaceCnt - oldSpaceCnt;319}320}321322return null;323}324325/**326*327* @param model328* @param indentConverter329* @param tabSize330* @param line the line moving down331* @param futureAboveLineNumber the line which will be at the `line` position332* @param futureAboveLineText333*/334private matchEnterRuleMovingDown(model: ITextModel, indentConverter: IIndentConverter, tabSize: number, line: number, futureAboveLineNumber: number, futureAboveLineText: string) {335if (strings.lastNonWhitespaceIndex(futureAboveLineText) >= 0) {336// break337const maxColumn = model.getLineMaxColumn(futureAboveLineNumber);338const enter = getEnterAction(this._autoIndent, model, new Range(futureAboveLineNumber, maxColumn, futureAboveLineNumber, maxColumn), this._languageConfigurationService);339return this.parseEnterResult(model, indentConverter, tabSize, line, enter);340} else {341// go upwards, starting from `line - 1`342let validPrecedingLine = line - 1;343while (validPrecedingLine >= 1) {344const lineContent = model.getLineContent(validPrecedingLine);345const nonWhitespaceIdx = strings.lastNonWhitespaceIndex(lineContent);346347if (nonWhitespaceIdx >= 0) {348break;349}350351validPrecedingLine--;352}353354if (validPrecedingLine < 1 || line > model.getLineCount()) {355return null;356}357358const maxColumn = model.getLineMaxColumn(validPrecedingLine);359const enter = getEnterAction(this._autoIndent, model, new Range(validPrecedingLine, maxColumn, validPrecedingLine, maxColumn), this._languageConfigurationService);360return this.parseEnterResult(model, indentConverter, tabSize, line, enter);361}362}363364private matchEnterRule(model: ITextModel, indentConverter: IIndentConverter, tabSize: number, line: number, oneLineAbove: number, previousLineText?: string) {365let validPrecedingLine = oneLineAbove;366while (validPrecedingLine >= 1) {367// ship empty lines as empty lines just inherit indentation368let lineContent;369if (validPrecedingLine === oneLineAbove && previousLineText !== undefined) {370lineContent = previousLineText;371} else {372lineContent = model.getLineContent(validPrecedingLine);373}374375const nonWhitespaceIdx = strings.lastNonWhitespaceIndex(lineContent);376if (nonWhitespaceIdx >= 0) {377break;378}379validPrecedingLine--;380}381382if (validPrecedingLine < 1 || line > model.getLineCount()) {383return null;384}385386const maxColumn = model.getLineMaxColumn(validPrecedingLine);387const enter = getEnterAction(this._autoIndent, model, new Range(validPrecedingLine, maxColumn, validPrecedingLine, maxColumn), this._languageConfigurationService);388return this.parseEnterResult(model, indentConverter, tabSize, line, enter);389}390391private trimStart(str: string) {392return str.replace(/^\s+/, '');393}394395private shouldAutoIndent(model: ITextModel, selection: Selection) {396if (this._autoIndent < EditorAutoIndentStrategy.Full) {397return false;398}399// if it's not easy to tokenize, we stop auto indent.400if (!model.tokenization.isCheapToTokenize(selection.startLineNumber)) {401return false;402}403const languageAtSelectionStart = model.getLanguageIdAtPosition(selection.startLineNumber, 1);404const languageAtSelectionEnd = model.getLanguageIdAtPosition(selection.endLineNumber, 1);405406if (languageAtSelectionStart !== languageAtSelectionEnd) {407return false;408}409410if (this._languageConfigurationService.getLanguageConfiguration(languageAtSelectionStart).indentRulesSupport === null) {411return false;412}413414return true;415}416417private getIndentEditsOfMovingBlock(model: ITextModel, builder: IEditOperationBuilder, s: Selection, tabSize: number, insertSpaces: boolean, offset: number) {418for (let i = s.startLineNumber; i <= s.endLineNumber; i++) {419const lineContent = model.getLineContent(i);420const originalIndent = strings.getLeadingWhitespace(lineContent);421const originalSpacesCnt = indentUtils.getSpaceCnt(originalIndent, tabSize);422const newSpacesCnt = originalSpacesCnt + offset;423const newIndent = indentUtils.generateIndent(newSpacesCnt, tabSize, insertSpaces);424425if (newIndent !== originalIndent) {426builder.addEditOperation(new Range(i, 1, i, originalIndent.length + 1), newIndent);427428if (i === s.endLineNumber && s.endColumn <= originalIndent.length + 1 && newIndent === '') {429// as users select part of the original indent white spaces430// when we adjust the indentation of endLine, we should adjust the cursor position as well.431this._moveEndLineSelectionShrink = true;432}433}434435}436}437438public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection {439let result = helper.getTrackedSelection(this._selectionId!);440441if (this._moveEndPositionDown) {442result = result.setEndPosition(result.endLineNumber + 1, 1);443}444445if (this._moveEndLineSelectionShrink && result.startLineNumber < result.endLineNumber) {446result = result.setEndPosition(result.endLineNumber, 2);447}448449return result;450}451}452453454