Path: blob/main/src/vs/editor/common/commands/shiftCommand.ts
3294 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 { CharCode } from '../../../base/common/charCode.js';6import * as strings from '../../../base/common/strings.js';7import { CursorColumns } from '../core/cursorColumns.js';8import { Range } from '../core/range.js';9import { Selection, SelectionDirection } from '../core/selection.js';10import { ICommand, ICursorStateComputerData, IEditOperationBuilder } from '../editorCommon.js';11import { ITextModel } from '../model.js';12import { EditorAutoIndentStrategy } from '../config/editorOptions.js';13import { getEnterAction } from '../languages/enterAction.js';14import { ILanguageConfigurationService } from '../languages/languageConfigurationRegistry.js';1516export interface IShiftCommandOpts {17isUnshift: boolean;18tabSize: number;19indentSize: number;20insertSpaces: boolean;21useTabStops: boolean;22autoIndent: EditorAutoIndentStrategy;23}2425const repeatCache: { [str: string]: string[] } = Object.create(null);26function cachedStringRepeat(str: string, count: number): string {27if (count <= 0) {28return '';29}30if (!repeatCache[str]) {31repeatCache[str] = ['', str];32}33const cache = repeatCache[str];34for (let i = cache.length; i <= count; i++) {35cache[i] = cache[i - 1] + str;36}37return cache[count];38}3940export class ShiftCommand implements ICommand {4142public static unshiftIndent(line: string, column: number, tabSize: number, indentSize: number, insertSpaces: boolean): string {43// Determine the visible column where the content starts44const contentStartVisibleColumn = CursorColumns.visibleColumnFromColumn(line, column, tabSize);4546if (insertSpaces) {47const indent = cachedStringRepeat(' ', indentSize);48const desiredTabStop = CursorColumns.prevIndentTabStop(contentStartVisibleColumn, indentSize);49const indentCount = desiredTabStop / indentSize; // will be an integer50return cachedStringRepeat(indent, indentCount);51} else {52const indent = '\t';53const desiredTabStop = CursorColumns.prevRenderTabStop(contentStartVisibleColumn, tabSize);54const indentCount = desiredTabStop / tabSize; // will be an integer55return cachedStringRepeat(indent, indentCount);56}57}5859public static shiftIndent(line: string, column: number, tabSize: number, indentSize: number, insertSpaces: boolean): string {60// Determine the visible column where the content starts61const contentStartVisibleColumn = CursorColumns.visibleColumnFromColumn(line, column, tabSize);6263if (insertSpaces) {64const indent = cachedStringRepeat(' ', indentSize);65const desiredTabStop = CursorColumns.nextIndentTabStop(contentStartVisibleColumn, indentSize);66const indentCount = desiredTabStop / indentSize; // will be an integer67return cachedStringRepeat(indent, indentCount);68} else {69const indent = '\t';70const desiredTabStop = CursorColumns.nextRenderTabStop(contentStartVisibleColumn, tabSize);71const indentCount = desiredTabStop / tabSize; // will be an integer72return cachedStringRepeat(indent, indentCount);73}74}7576private readonly _opts: IShiftCommandOpts;77private readonly _selection: Selection;78private _selectionId: string | null;79private _useLastEditRangeForCursorEndPosition: boolean;80private _selectionStartColumnStaysPut: boolean;8182constructor(83range: Selection,84opts: IShiftCommandOpts,85@ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService86) {87this._opts = opts;88this._selection = range;89this._selectionId = null;90this._useLastEditRangeForCursorEndPosition = false;91this._selectionStartColumnStaysPut = false;92}9394private _addEditOperation(builder: IEditOperationBuilder, range: Range, text: string) {95if (this._useLastEditRangeForCursorEndPosition) {96builder.addTrackedEditOperation(range, text);97} else {98builder.addEditOperation(range, text);99}100}101102public getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void {103const startLine = this._selection.startLineNumber;104105let endLine = this._selection.endLineNumber;106if (this._selection.endColumn === 1 && startLine !== endLine) {107endLine = endLine - 1;108}109110const { tabSize, indentSize, insertSpaces } = this._opts;111const shouldIndentEmptyLines = (startLine === endLine);112113if (this._opts.useTabStops) {114// if indenting or outdenting on a whitespace only line115if (this._selection.isEmpty()) {116if (/^\s*$/.test(model.getLineContent(startLine))) {117this._useLastEditRangeForCursorEndPosition = true;118}119}120121// keep track of previous line's "miss-alignment"122let previousLineExtraSpaces = 0, extraSpaces = 0;123for (let lineNumber = startLine; lineNumber <= endLine; lineNumber++, previousLineExtraSpaces = extraSpaces) {124extraSpaces = 0;125const lineText = model.getLineContent(lineNumber);126let indentationEndIndex = strings.firstNonWhitespaceIndex(lineText);127128if (this._opts.isUnshift && (lineText.length === 0 || indentationEndIndex === 0)) {129// empty line or line with no leading whitespace => nothing to do130continue;131}132133if (!shouldIndentEmptyLines && !this._opts.isUnshift && lineText.length === 0) {134// do not indent empty lines => nothing to do135continue;136}137138if (indentationEndIndex === -1) {139// the entire line is whitespace140indentationEndIndex = lineText.length;141}142143if (lineNumber > 1) {144const contentStartVisibleColumn = CursorColumns.visibleColumnFromColumn(lineText, indentationEndIndex + 1, tabSize);145if (contentStartVisibleColumn % indentSize !== 0) {146// The current line is "miss-aligned", so let's see if this is expected...147// This can only happen when it has trailing commas in the indent148if (model.tokenization.isCheapToTokenize(lineNumber - 1)) {149const enterAction = getEnterAction(this._opts.autoIndent, model, new Range(lineNumber - 1, model.getLineMaxColumn(lineNumber - 1), lineNumber - 1, model.getLineMaxColumn(lineNumber - 1)), this._languageConfigurationService);150if (enterAction) {151extraSpaces = previousLineExtraSpaces;152if (enterAction.appendText) {153for (let j = 0, lenJ = enterAction.appendText.length; j < lenJ && extraSpaces < indentSize; j++) {154if (enterAction.appendText.charCodeAt(j) === CharCode.Space) {155extraSpaces++;156} else {157break;158}159}160}161if (enterAction.removeText) {162extraSpaces = Math.max(0, extraSpaces - enterAction.removeText);163}164165// Act as if `prefixSpaces` is not part of the indentation166for (let j = 0; j < extraSpaces; j++) {167if (indentationEndIndex === 0 || lineText.charCodeAt(indentationEndIndex - 1) !== CharCode.Space) {168break;169}170indentationEndIndex--;171}172}173}174}175}176177178if (this._opts.isUnshift && indentationEndIndex === 0) {179// line with no leading whitespace => nothing to do180continue;181}182183let desiredIndent: string;184if (this._opts.isUnshift) {185desiredIndent = ShiftCommand.unshiftIndent(lineText, indentationEndIndex + 1, tabSize, indentSize, insertSpaces);186} else {187desiredIndent = ShiftCommand.shiftIndent(lineText, indentationEndIndex + 1, tabSize, indentSize, insertSpaces);188}189190this._addEditOperation(builder, new Range(lineNumber, 1, lineNumber, indentationEndIndex + 1), desiredIndent);191if (lineNumber === startLine && !this._selection.isEmpty()) {192// Force the startColumn to stay put because we're inserting after it193this._selectionStartColumnStaysPut = (this._selection.startColumn <= indentationEndIndex + 1);194}195}196} else {197198// if indenting or outdenting on a whitespace only line199if (!this._opts.isUnshift && this._selection.isEmpty() && model.getLineLength(startLine) === 0) {200this._useLastEditRangeForCursorEndPosition = true;201}202203const oneIndent = (insertSpaces ? cachedStringRepeat(' ', indentSize) : '\t');204205for (let lineNumber = startLine; lineNumber <= endLine; lineNumber++) {206const lineText = model.getLineContent(lineNumber);207let indentationEndIndex = strings.firstNonWhitespaceIndex(lineText);208209if (this._opts.isUnshift && (lineText.length === 0 || indentationEndIndex === 0)) {210// empty line or line with no leading whitespace => nothing to do211continue;212}213214if (!shouldIndentEmptyLines && !this._opts.isUnshift && lineText.length === 0) {215// do not indent empty lines => nothing to do216continue;217}218219if (indentationEndIndex === -1) {220// the entire line is whitespace221indentationEndIndex = lineText.length;222}223224if (this._opts.isUnshift && indentationEndIndex === 0) {225// line with no leading whitespace => nothing to do226continue;227}228229if (this._opts.isUnshift) {230231indentationEndIndex = Math.min(indentationEndIndex, indentSize);232for (let i = 0; i < indentationEndIndex; i++) {233const chr = lineText.charCodeAt(i);234if (chr === CharCode.Tab) {235indentationEndIndex = i + 1;236break;237}238}239240this._addEditOperation(builder, new Range(lineNumber, 1, lineNumber, indentationEndIndex + 1), '');241} else {242this._addEditOperation(builder, new Range(lineNumber, 1, lineNumber, 1), oneIndent);243if (lineNumber === startLine && !this._selection.isEmpty()) {244// Force the startColumn to stay put because we're inserting after it245this._selectionStartColumnStaysPut = (this._selection.startColumn === 1);246}247}248}249}250251this._selectionId = builder.trackSelection(this._selection);252}253254public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection {255if (this._useLastEditRangeForCursorEndPosition) {256const lastOp = helper.getInverseEditOperations()[0];257return new Selection(lastOp.range.endLineNumber, lastOp.range.endColumn, lastOp.range.endLineNumber, lastOp.range.endColumn);258}259const result = helper.getTrackedSelection(this._selectionId!);260261if (this._selectionStartColumnStaysPut) {262// The selection start should not move263const initialStartColumn = this._selection.startColumn;264const resultStartColumn = result.startColumn;265if (resultStartColumn <= initialStartColumn) {266return result;267}268269if (result.getDirection() === SelectionDirection.LTR) {270return new Selection(result.startLineNumber, initialStartColumn, result.endLineNumber, result.endColumn);271}272return new Selection(result.endLineNumber, result.endColumn, result.startLineNumber, initialStartColumn);273}274275return result;276}277}278279280