Path: blob/main/src/vs/editor/contrib/indentation/browser/indentation.ts
5332 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 { DisposableStore } from '../../../../base/common/lifecycle.js';6import * as strings from '../../../../base/common/strings.js';7import * as nls from '../../../../nls.js';8import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js';9import { ICodeEditor } from '../../../browser/editorBrowser.js';10import { EditorAction, EditorContributionInstantiation, IActionOptions, registerEditorAction, registerEditorContribution, ServicesAccessor } from '../../../browser/editorExtensions.js';11import { ShiftCommand } from '../../../common/commands/shiftCommand.js';12import { EditorAutoIndentStrategy, EditorOption } from '../../../common/config/editorOptions.js';13import { ISingleEditOperation } from '../../../common/core/editOperation.js';14import { Position } from '../../../common/core/position.js';15import { IRange, Range } from '../../../common/core/range.js';16import { Selection } from '../../../common/core/selection.js';17import { ICommand, ICursorStateComputerData, IEditOperationBuilder, IEditorContribution } from '../../../common/editorCommon.js';18import { EditorContextKeys } from '../../../common/editorContextKeys.js';19import { StandardTokenType } from '../../../common/encodedTokenAttributes.js';20import { TextEdit } from '../../../common/languages.js';21import { getGoodIndentForLine, getIndentMetadata } from '../../../common/languages/autoIndent.js';22import { ILanguageConfigurationService } from '../../../common/languages/languageConfigurationRegistry.js';23import { IndentConsts } from '../../../common/languages/supports/indentRules.js';24import { EndOfLineSequence, ITextModel } from '../../../common/model.js';25import { IModelService } from '../../../common/services/model.js';26import { getStandardTokenTypeAtPosition } from '../../../common/tokens/lineTokens.js';27import { getReindentEditOperations } from '../common/indentation.js';28import * as indentUtils from '../common/indentUtils.js';2930export class IndentationToSpacesAction extends EditorAction {31public static readonly ID = 'editor.action.indentationToSpaces';3233constructor() {34super({35id: IndentationToSpacesAction.ID,36label: nls.localize2('indentationToSpaces', "Convert Indentation to Spaces"),37precondition: EditorContextKeys.writable,38metadata: {39description: nls.localize2('indentationToSpacesDescription', "Convert the tab indentation to spaces."),40}41});42}4344public run(accessor: ServicesAccessor, editor: ICodeEditor): void {45const model = editor.getModel();46if (!model) {47return;48}49const modelOpts = model.getOptions();50const selection = editor.getSelection();51if (!selection) {52return;53}54const command = new IndentationToSpacesCommand(selection, modelOpts.tabSize);5556editor.pushUndoStop();57editor.executeCommands(this.id, [command]);58editor.pushUndoStop();5960model.updateOptions({61insertSpaces: true62});63}64}6566export class IndentationToTabsAction extends EditorAction {67public static readonly ID = 'editor.action.indentationToTabs';6869constructor() {70super({71id: IndentationToTabsAction.ID,72label: nls.localize2('indentationToTabs', "Convert Indentation to Tabs"),73precondition: EditorContextKeys.writable,74metadata: {75description: nls.localize2('indentationToTabsDescription', "Convert the spaces indentation to tabs."),76}77});78}7980public run(accessor: ServicesAccessor, editor: ICodeEditor): void {81const model = editor.getModel();82if (!model) {83return;84}85const modelOpts = model.getOptions();86const selection = editor.getSelection();87if (!selection) {88return;89}90const command = new IndentationToTabsCommand(selection, modelOpts.tabSize);9192editor.pushUndoStop();93editor.executeCommands(this.id, [command]);94editor.pushUndoStop();9596model.updateOptions({97insertSpaces: false98});99}100}101102export class ChangeIndentationSizeAction extends EditorAction {103104constructor(private readonly insertSpaces: boolean, private readonly displaySizeOnly: boolean, opts: IActionOptions) {105super(opts);106}107108public run(accessor: ServicesAccessor, editor: ICodeEditor): void {109const quickInputService = accessor.get(IQuickInputService);110const modelService = accessor.get(IModelService);111112const model = editor.getModel();113if (!model) {114return;115}116117const creationOpts = modelService.getCreationOptions(model.getLanguageId(), model.uri, model.isForSimpleWidget);118const modelOpts = model.getOptions();119const picks = [1, 2, 3, 4, 5, 6, 7, 8].map(n => ({120id: n.toString(),121label: n.toString(),122// add description for tabSize value set in the configuration123description: (124n === creationOpts.tabSize && n === modelOpts.tabSize125? nls.localize('configuredTabSize', "Configured Tab Size")126: n === creationOpts.tabSize127? nls.localize('defaultTabSize', "Default Tab Size")128: n === modelOpts.tabSize129? nls.localize('currentTabSize', "Current Tab Size")130: undefined131)132}));133134// auto focus the tabSize set for the current editor135const autoFocusIndex = Math.min(model.getOptions().tabSize - 1, 7);136137setTimeout(() => {138quickInputService.pick(picks, { placeHolder: nls.localize({ key: 'selectTabWidth', comment: ['Tab corresponds to the tab key'] }, "Select Tab Size for Current File"), activeItem: picks[autoFocusIndex] }).then(pick => {139if (pick) {140if (model && !model.isDisposed()) {141const pickedVal = parseInt(pick.label, 10);142if (this.displaySizeOnly) {143model.updateOptions({144tabSize: pickedVal145});146} else {147model.updateOptions({148tabSize: pickedVal,149indentSize: pickedVal,150insertSpaces: this.insertSpaces151});152}153}154}155});156}, 50/* quick input is sensitive to being opened so soon after another */);157}158}159160export class IndentUsingTabs extends ChangeIndentationSizeAction {161162public static readonly ID = 'editor.action.indentUsingTabs';163164constructor() {165super(false, false, {166id: IndentUsingTabs.ID,167label: nls.localize2('indentUsingTabs', "Indent Using Tabs"),168precondition: undefined,169metadata: {170description: nls.localize2('indentUsingTabsDescription', "Use indentation with tabs."),171}172});173}174}175176export class IndentUsingSpaces extends ChangeIndentationSizeAction {177178public static readonly ID = 'editor.action.indentUsingSpaces';179180constructor() {181super(true, false, {182id: IndentUsingSpaces.ID,183label: nls.localize2('indentUsingSpaces', "Indent Using Spaces"),184precondition: undefined,185metadata: {186description: nls.localize2('indentUsingSpacesDescription', "Use indentation with spaces."),187}188});189}190}191192export class ChangeTabDisplaySize extends ChangeIndentationSizeAction {193194public static readonly ID = 'editor.action.changeTabDisplaySize';195196constructor() {197super(true, true, {198id: ChangeTabDisplaySize.ID,199label: nls.localize2('changeTabDisplaySize', "Change Tab Display Size"),200precondition: undefined,201metadata: {202description: nls.localize2('changeTabDisplaySizeDescription', "Change the space size equivalent of the tab."),203}204});205}206}207208export class DetectIndentation extends EditorAction {209210public static readonly ID = 'editor.action.detectIndentation';211212constructor() {213super({214id: DetectIndentation.ID,215label: nls.localize2('detectIndentation', "Detect Indentation from Content"),216precondition: undefined,217metadata: {218description: nls.localize2('detectIndentationDescription', "Detect the indentation from content."),219}220});221}222223public run(accessor: ServicesAccessor, editor: ICodeEditor): void {224const modelService = accessor.get(IModelService);225226const model = editor.getModel();227if (!model) {228return;229}230231const creationOpts = modelService.getCreationOptions(model.getLanguageId(), model.uri, model.isForSimpleWidget);232model.detectIndentation(creationOpts.insertSpaces, creationOpts.tabSize);233}234}235236export class ReindentLinesAction extends EditorAction {237constructor() {238super({239id: 'editor.action.reindentlines',240label: nls.localize2('editor.reindentlines', "Reindent Lines"),241precondition: EditorContextKeys.writable,242metadata: {243description: nls.localize2('editor.reindentlinesDescription', "Reindent the lines of the editor."),244},245canTriggerInlineEdits: true,246});247}248249public run(accessor: ServicesAccessor, editor: ICodeEditor): void {250const languageConfigurationService = accessor.get(ILanguageConfigurationService);251252const model = editor.getModel();253if (!model) {254return;255}256const edits = getReindentEditOperations(model, languageConfigurationService, 1, model.getLineCount());257if (edits.length > 0) {258editor.pushUndoStop();259editor.executeEdits(this.id, edits);260editor.pushUndoStop();261}262}263}264265export class ReindentSelectedLinesAction extends EditorAction {266constructor() {267super({268id: 'editor.action.reindentselectedlines',269label: nls.localize2('editor.reindentselectedlines', "Reindent Selected Lines"),270precondition: EditorContextKeys.writable,271metadata: {272description: nls.localize2('editor.reindentselectedlinesDescription', "Reindent the selected lines of the editor."),273},274canTriggerInlineEdits: true,275});276}277278public run(accessor: ServicesAccessor, editor: ICodeEditor): void {279const languageConfigurationService = accessor.get(ILanguageConfigurationService);280281const model = editor.getModel();282if (!model) {283return;284}285286const selections = editor.getSelections();287if (selections === null) {288return;289}290291const edits: ISingleEditOperation[] = [];292293for (const selection of selections) {294let startLineNumber = selection.startLineNumber;295let endLineNumber = selection.endLineNumber;296297if (startLineNumber !== endLineNumber && selection.endColumn === 1) {298endLineNumber--;299}300301if (startLineNumber === 1) {302if (startLineNumber === endLineNumber) {303continue;304}305} else {306startLineNumber--;307}308309const editOperations = getReindentEditOperations(model, languageConfigurationService, startLineNumber, endLineNumber);310edits.push(...editOperations);311}312313if (edits.length > 0) {314editor.pushUndoStop();315editor.executeEdits(this.id, edits);316editor.pushUndoStop();317}318}319}320321export class AutoIndentOnPasteCommand implements ICommand {322323private readonly _edits: { range: IRange; text: string; eol?: EndOfLineSequence }[];324325private readonly _initialSelection: Selection;326private _selectionId: string | null;327328constructor(edits: TextEdit[], initialSelection: Selection) {329this._initialSelection = initialSelection;330this._edits = [];331this._selectionId = null;332333for (const edit of edits) {334if (edit.range && typeof edit.text === 'string') {335this._edits.push(edit as { range: IRange; text: string; eol?: EndOfLineSequence });336}337}338}339340public getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void {341for (const edit of this._edits) {342builder.addEditOperation(Range.lift(edit.range), edit.text);343}344345let selectionIsSet = false;346if (Array.isArray(this._edits) && this._edits.length === 1 && this._initialSelection.isEmpty()) {347if (this._edits[0].range.startColumn === this._initialSelection.endColumn &&348this._edits[0].range.startLineNumber === this._initialSelection.endLineNumber) {349selectionIsSet = true;350this._selectionId = builder.trackSelection(this._initialSelection, true);351} else if (this._edits[0].range.endColumn === this._initialSelection.startColumn &&352this._edits[0].range.endLineNumber === this._initialSelection.startLineNumber) {353selectionIsSet = true;354this._selectionId = builder.trackSelection(this._initialSelection, false);355}356}357358if (!selectionIsSet) {359this._selectionId = builder.trackSelection(this._initialSelection);360}361}362363public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection {364return helper.getTrackedSelection(this._selectionId!);365}366}367368export class AutoIndentOnPaste implements IEditorContribution {369public static readonly ID = 'editor.contrib.autoIndentOnPaste';370371private readonly callOnDispose = new DisposableStore();372private readonly callOnModel = new DisposableStore();373374constructor(375private readonly editor: ICodeEditor,376@ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService377) {378379this.callOnDispose.add(editor.onDidChangeConfiguration(() => this.update()));380this.callOnDispose.add(editor.onDidChangeModel(() => this.update()));381this.callOnDispose.add(editor.onDidChangeModelLanguage(() => this.update()));382}383384private update(): void {385386// clean up387this.callOnModel.clear();388389// we are disabled390if (!this.editor.getOption(EditorOption.autoIndentOnPaste) || this.editor.getOption(EditorOption.autoIndent) < EditorAutoIndentStrategy.Full) {391return;392}393394// no model395if (!this.editor.hasModel()) {396return;397}398399this.callOnModel.add(this.editor.onDidPaste(({ range }) => {400this.trigger(range);401}));402}403404public trigger(range: Range): void {405const selections = this.editor.getSelections();406if (selections === null || selections.length > 1) {407return;408}409410const model = this.editor.getModel();411if (!model) {412return;413}414const containsOnlyWhitespace = this.rangeContainsOnlyWhitespaceCharacters(model, range);415if (containsOnlyWhitespace) {416return;417}418if (!this.editor.getOption(EditorOption.autoIndentOnPasteWithinString) && isStartOrEndInString(model, range)) {419return;420}421if (!model.tokenization.isCheapToTokenize(range.getStartPosition().lineNumber)) {422return;423}424const autoIndent = this.editor.getOption(EditorOption.autoIndent);425const { tabSize, indentSize, insertSpaces } = model.getOptions();426const textEdits: TextEdit[] = [];427428const indentConverter = {429shiftIndent: (indentation: string) => {430return ShiftCommand.shiftIndent(indentation, indentation.length + 1, tabSize, indentSize, insertSpaces);431},432unshiftIndent: (indentation: string) => {433return ShiftCommand.unshiftIndent(indentation, indentation.length + 1, tabSize, indentSize, insertSpaces);434}435};436437let startLineNumber = range.startLineNumber;438439let firstLineText = model.getLineContent(startLineNumber);440if (!/\S/.test(firstLineText.substring(0, range.startColumn - 1))) {441const indentOfFirstLine = getGoodIndentForLine(autoIndent, model, model.getLanguageId(), startLineNumber, indentConverter, this._languageConfigurationService);442443if (indentOfFirstLine !== null) {444const oldIndentation = strings.getLeadingWhitespace(firstLineText);445const newSpaceCnt = indentUtils.getSpaceCnt(indentOfFirstLine, tabSize);446const oldSpaceCnt = indentUtils.getSpaceCnt(oldIndentation, tabSize);447448if (newSpaceCnt !== oldSpaceCnt) {449const newIndent = indentUtils.generateIndent(newSpaceCnt, tabSize, insertSpaces);450textEdits.push({451range: new Range(startLineNumber, 1, startLineNumber, oldIndentation.length + 1),452text: newIndent453});454firstLineText = newIndent + firstLineText.substring(oldIndentation.length);455} else {456const indentMetadata = getIndentMetadata(model, startLineNumber, this._languageConfigurationService);457458if (indentMetadata === 0 || indentMetadata === IndentConsts.UNINDENT_MASK) {459// we paste content into a line where only contains whitespaces460// after pasting, the indentation of the first line is already correct461// the first line doesn't match any indentation rule462// then no-op.463return;464}465}466}467}468469const firstLineNumber = startLineNumber;470471// ignore empty or ignored lines472while (startLineNumber < range.endLineNumber) {473if (!/\S/.test(model.getLineContent(startLineNumber + 1))) {474startLineNumber++;475continue;476}477break;478}479480if (startLineNumber !== range.endLineNumber) {481const virtualModel = {482tokenization: {483getLineTokens: (lineNumber: number) => {484return model.tokenization.getLineTokens(lineNumber);485},486getLanguageId: () => {487return model.getLanguageId();488},489getLanguageIdAtPosition: (lineNumber: number, column: number) => {490return model.getLanguageIdAtPosition(lineNumber, column);491},492},493getLineContent: (lineNumber: number) => {494if (lineNumber === firstLineNumber) {495return firstLineText;496} else {497return model.getLineContent(lineNumber);498}499}500};501const indentOfSecondLine = getGoodIndentForLine(autoIndent, virtualModel, model.getLanguageId(), startLineNumber + 1, indentConverter, this._languageConfigurationService);502if (indentOfSecondLine !== null) {503const newSpaceCntOfSecondLine = indentUtils.getSpaceCnt(indentOfSecondLine, tabSize);504const oldSpaceCntOfSecondLine = indentUtils.getSpaceCnt(strings.getLeadingWhitespace(model.getLineContent(startLineNumber + 1)), tabSize);505506if (newSpaceCntOfSecondLine !== oldSpaceCntOfSecondLine) {507const spaceCntOffset = newSpaceCntOfSecondLine - oldSpaceCntOfSecondLine;508for (let i = startLineNumber + 1; i <= range.endLineNumber; i++) {509const lineContent = model.getLineContent(i);510const originalIndent = strings.getLeadingWhitespace(lineContent);511const originalSpacesCnt = indentUtils.getSpaceCnt(originalIndent, tabSize);512const newSpacesCnt = originalSpacesCnt + spaceCntOffset;513const newIndent = indentUtils.generateIndent(newSpacesCnt, tabSize, insertSpaces);514515if (newIndent !== originalIndent) {516textEdits.push({517range: new Range(i, 1, i, originalIndent.length + 1),518text: newIndent519});520}521}522}523}524}525526if (textEdits.length > 0) {527this.editor.pushUndoStop();528const cmd = new AutoIndentOnPasteCommand(textEdits, this.editor.getSelection()!);529this.editor.executeCommand('autoIndentOnPaste', cmd);530this.editor.pushUndoStop();531}532}533534private rangeContainsOnlyWhitespaceCharacters(model: ITextModel, range: Range): boolean {535const lineContainsOnlyWhitespace = (content: string): boolean => {536return content.trim().length === 0;537};538let containsOnlyWhitespace: boolean = true;539if (range.startLineNumber === range.endLineNumber) {540const lineContent = model.getLineContent(range.startLineNumber);541const linePart = lineContent.substring(range.startColumn - 1, range.endColumn - 1);542containsOnlyWhitespace = lineContainsOnlyWhitespace(linePart);543} else {544for (let i = range.startLineNumber; i <= range.endLineNumber; i++) {545const lineContent = model.getLineContent(i);546if (i === range.startLineNumber) {547const linePart = lineContent.substring(range.startColumn - 1);548containsOnlyWhitespace = lineContainsOnlyWhitespace(linePart);549} else if (i === range.endLineNumber) {550const linePart = lineContent.substring(0, range.endColumn - 1);551containsOnlyWhitespace = lineContainsOnlyWhitespace(linePart);552} else {553containsOnlyWhitespace = model.getLineFirstNonWhitespaceColumn(i) === 0;554}555if (!containsOnlyWhitespace) {556break;557}558}559}560return containsOnlyWhitespace;561}562563public dispose(): void {564this.callOnDispose.dispose();565this.callOnModel.dispose();566}567}568569function isStartOrEndInString(model: ITextModel, range: Range): boolean {570const isPositionInString = (position: Position): boolean => {571const tokenType = getStandardTokenTypeAtPosition(model, position);572return tokenType === StandardTokenType.String;573};574return isPositionInString(range.getStartPosition()) || isPositionInString(range.getEndPosition());575}576577function getIndentationEditOperations(model: ITextModel, builder: IEditOperationBuilder, tabSize: number, tabsToSpaces: boolean): void {578if (model.getLineCount() === 1 && model.getLineMaxColumn(1) === 1) {579// Model is empty580return;581}582583let spaces = '';584for (let i = 0; i < tabSize; i++) {585spaces += ' ';586}587588const spacesRegExp = new RegExp(spaces, 'gi');589590for (let lineNumber = 1, lineCount = model.getLineCount(); lineNumber <= lineCount; lineNumber++) {591let lastIndentationColumn = model.getLineFirstNonWhitespaceColumn(lineNumber);592if (lastIndentationColumn === 0) {593lastIndentationColumn = model.getLineMaxColumn(lineNumber);594}595596if (lastIndentationColumn === 1) {597continue;598}599600const originalIndentationRange = new Range(lineNumber, 1, lineNumber, lastIndentationColumn);601const originalIndentation = model.getValueInRange(originalIndentationRange);602const newIndentation = (603tabsToSpaces604? originalIndentation.replace(/\t/ig, spaces)605: originalIndentation.replace(spacesRegExp, '\t')606);607608builder.addEditOperation(originalIndentationRange, newIndentation);609}610}611612export class IndentationToSpacesCommand implements ICommand {613614private selectionId: string | null = null;615616constructor(private readonly selection: Selection, private tabSize: number) { }617618public getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void {619this.selectionId = builder.trackSelection(this.selection);620getIndentationEditOperations(model, builder, this.tabSize, true);621}622623public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection {624return helper.getTrackedSelection(this.selectionId!);625}626}627628export class IndentationToTabsCommand implements ICommand {629630private selectionId: string | null = null;631632constructor(private readonly selection: Selection, private tabSize: number) { }633634public getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void {635this.selectionId = builder.trackSelection(this.selection);636getIndentationEditOperations(model, builder, this.tabSize, false);637}638639public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection {640return helper.getTrackedSelection(this.selectionId!);641}642}643644registerEditorContribution(AutoIndentOnPaste.ID, AutoIndentOnPaste, EditorContributionInstantiation.BeforeFirstInteraction);645registerEditorAction(IndentationToSpacesAction);646registerEditorAction(IndentationToTabsAction);647registerEditorAction(IndentUsingTabs);648registerEditorAction(IndentUsingSpaces);649registerEditorAction(ChangeTabDisplaySize);650registerEditorAction(DetectIndentation);651registerEditorAction(ReindentLinesAction);652registerEditorAction(ReindentSelectedLinesAction);653654655