Path: blob/main/src/vs/editor/common/languages/autoIndent.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 * as strings from '../../../base/common/strings.js';6import { Range } from '../core/range.js';7import { ITextModel } from '../model.js';8import { IndentAction } from './languageConfiguration.js';9import { IndentConsts } from './supports/indentRules.js';10import { EditorAutoIndentStrategy } from '../config/editorOptions.js';11import { ILanguageConfigurationService } from './languageConfigurationRegistry.js';12import { IViewLineTokens } from '../tokens/lineTokens.js';13import { IndentationContextProcessor, isLanguageDifferentFromLineStart, ProcessedIndentRulesSupport } from './supports/indentationLineProcessor.js';14import { CursorConfiguration } from '../cursorCommon.js';1516export interface IVirtualModel {17tokenization: {18getLineTokens(lineNumber: number): IViewLineTokens;19getLanguageId(): string;20getLanguageIdAtPosition(lineNumber: number, column: number): string;21forceTokenization?(lineNumber: number): void;22};23getLineContent(lineNumber: number): string;24}2526export interface IIndentConverter {27shiftIndent(indentation: string): string;28unshiftIndent(indentation: string): string;29normalizeIndentation?(indentation: string): string;30}3132/**33* Get nearest preceding line which doesn't match unIndentPattern or contains all whitespace.34* Result:35* -1: run into the boundary of embedded languages36* 0: every line above are invalid37* else: nearest preceding line of the same language38*/39function getPrecedingValidLine(model: IVirtualModel, lineNumber: number, processedIndentRulesSupport: ProcessedIndentRulesSupport) {40const languageId = model.tokenization.getLanguageIdAtPosition(lineNumber, 0);41if (lineNumber > 1) {42let lastLineNumber: number;43let resultLineNumber = -1;4445for (lastLineNumber = lineNumber - 1; lastLineNumber >= 1; lastLineNumber--) {46if (model.tokenization.getLanguageIdAtPosition(lastLineNumber, 0) !== languageId) {47return resultLineNumber;48}49const text = model.getLineContent(lastLineNumber);50if (processedIndentRulesSupport.shouldIgnore(lastLineNumber) || /^\s+$/.test(text) || text === '') {51resultLineNumber = lastLineNumber;52continue;53}5455return lastLineNumber;56}57}5859return -1;60}6162/**63* Get inherited indentation from above lines.64* 1. Find the nearest preceding line which doesn't match unIndentedLinePattern.65* 2. If this line matches indentNextLinePattern or increaseIndentPattern, it means that the indent level of `lineNumber` should be 1 greater than this line.66* 3. If this line doesn't match any indent rules67* a. check whether the line above it matches indentNextLinePattern68* b. If not, the indent level of this line is the result69* c. If so, it means the indent of this line is *temporary*, go upward utill we find a line whose indent is not temporary (the same workflow a -> b -> c).70* 4. Otherwise, we fail to get an inherited indent from aboves. Return null and we should not touch the indent of `lineNumber`71*72* This function only return the inherited indent based on above lines, it doesn't check whether current line should decrease or not.73*/74export function getInheritIndentForLine(75autoIndent: EditorAutoIndentStrategy,76model: IVirtualModel,77lineNumber: number,78honorIntentialIndent: boolean = true,79languageConfigurationService: ILanguageConfigurationService80): { indentation: string; action: IndentAction | null; line?: number } | null {81if (autoIndent < EditorAutoIndentStrategy.Full) {82return null;83}8485const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(model.tokenization.getLanguageId()).indentRulesSupport;86if (!indentRulesSupport) {87return null;88}89const processedIndentRulesSupport = new ProcessedIndentRulesSupport(model, indentRulesSupport, languageConfigurationService);9091if (lineNumber <= 1) {92return {93indentation: '',94action: null95};96}9798// Use no indent if this is the first non-blank line99for (let priorLineNumber = lineNumber - 1; priorLineNumber > 0; priorLineNumber--) {100if (model.getLineContent(priorLineNumber) !== '') {101break;102}103if (priorLineNumber === 1) {104return {105indentation: '',106action: null107};108}109}110111const precedingUnIgnoredLine = getPrecedingValidLine(model, lineNumber, processedIndentRulesSupport);112if (precedingUnIgnoredLine < 0) {113return null;114} else if (precedingUnIgnoredLine < 1) {115return {116indentation: '',117action: null118};119}120121if (processedIndentRulesSupport.shouldIncrease(precedingUnIgnoredLine) || processedIndentRulesSupport.shouldIndentNextLine(precedingUnIgnoredLine)) {122const precedingUnIgnoredLineContent = model.getLineContent(precedingUnIgnoredLine);123return {124indentation: strings.getLeadingWhitespace(precedingUnIgnoredLineContent),125action: IndentAction.Indent,126line: precedingUnIgnoredLine127};128} else if (processedIndentRulesSupport.shouldDecrease(precedingUnIgnoredLine)) {129const precedingUnIgnoredLineContent = model.getLineContent(precedingUnIgnoredLine);130return {131indentation: strings.getLeadingWhitespace(precedingUnIgnoredLineContent),132action: null,133line: precedingUnIgnoredLine134};135} else {136// precedingUnIgnoredLine can not be ignored.137// it doesn't increase indent of following lines138// it doesn't increase just next line139// so current line is not affect by precedingUnIgnoredLine140// and then we should get a correct inheritted indentation from above lines141if (precedingUnIgnoredLine === 1) {142return {143indentation: strings.getLeadingWhitespace(model.getLineContent(precedingUnIgnoredLine)),144action: null,145line: precedingUnIgnoredLine146};147}148149const previousLine = precedingUnIgnoredLine - 1;150151const previousLineIndentMetadata = indentRulesSupport.getIndentMetadata(model.getLineContent(previousLine));152if (!(previousLineIndentMetadata & (IndentConsts.INCREASE_MASK | IndentConsts.DECREASE_MASK)) &&153(previousLineIndentMetadata & IndentConsts.INDENT_NEXTLINE_MASK)) {154let stopLine = 0;155for (let i = previousLine - 1; i > 0; i--) {156if (processedIndentRulesSupport.shouldIndentNextLine(i)) {157continue;158}159stopLine = i;160break;161}162163return {164indentation: strings.getLeadingWhitespace(model.getLineContent(stopLine + 1)),165action: null,166line: stopLine + 1167};168}169170if (honorIntentialIndent) {171return {172indentation: strings.getLeadingWhitespace(model.getLineContent(precedingUnIgnoredLine)),173action: null,174line: precedingUnIgnoredLine175};176} else {177// search from precedingUnIgnoredLine until we find one whose indent is not temporary178for (let i = precedingUnIgnoredLine; i > 0; i--) {179if (processedIndentRulesSupport.shouldIncrease(i)) {180return {181indentation: strings.getLeadingWhitespace(model.getLineContent(i)),182action: IndentAction.Indent,183line: i184};185} else if (processedIndentRulesSupport.shouldIndentNextLine(i)) {186let stopLine = 0;187for (let j = i - 1; j > 0; j--) {188if (processedIndentRulesSupport.shouldIndentNextLine(i)) {189continue;190}191stopLine = j;192break;193}194195return {196indentation: strings.getLeadingWhitespace(model.getLineContent(stopLine + 1)),197action: null,198line: stopLine + 1199};200} else if (processedIndentRulesSupport.shouldDecrease(i)) {201return {202indentation: strings.getLeadingWhitespace(model.getLineContent(i)),203action: null,204line: i205};206}207}208209return {210indentation: strings.getLeadingWhitespace(model.getLineContent(1)),211action: null,212line: 1213};214}215}216}217218export function getGoodIndentForLine(219autoIndent: EditorAutoIndentStrategy,220virtualModel: IVirtualModel,221languageId: string,222lineNumber: number,223indentConverter: IIndentConverter,224languageConfigurationService: ILanguageConfigurationService225): string | null {226if (autoIndent < EditorAutoIndentStrategy.Full) {227return null;228}229230const richEditSupport = languageConfigurationService.getLanguageConfiguration(languageId);231if (!richEditSupport) {232return null;233}234235const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(languageId).indentRulesSupport;236if (!indentRulesSupport) {237return null;238}239240const processedIndentRulesSupport = new ProcessedIndentRulesSupport(virtualModel, indentRulesSupport, languageConfigurationService);241const indent = getInheritIndentForLine(autoIndent, virtualModel, lineNumber, undefined, languageConfigurationService);242243if (indent) {244const inheritLine = indent.line;245if (inheritLine !== undefined) {246// Apply enter action as long as there are only whitespace lines between inherited line and this line.247let shouldApplyEnterRules = true;248for (let inBetweenLine = inheritLine; inBetweenLine < lineNumber - 1; inBetweenLine++) {249if (!/^\s*$/.test(virtualModel.getLineContent(inBetweenLine))) {250shouldApplyEnterRules = false;251break;252}253}254if (shouldApplyEnterRules) {255const enterResult = richEditSupport.onEnter(autoIndent, '', virtualModel.getLineContent(inheritLine), '');256257if (enterResult) {258let indentation = strings.getLeadingWhitespace(virtualModel.getLineContent(inheritLine));259260if (enterResult.removeText) {261indentation = indentation.substring(0, indentation.length - enterResult.removeText);262}263264if (265(enterResult.indentAction === IndentAction.Indent) ||266(enterResult.indentAction === IndentAction.IndentOutdent)267) {268indentation = indentConverter.shiftIndent(indentation);269} else if (enterResult.indentAction === IndentAction.Outdent) {270indentation = indentConverter.unshiftIndent(indentation);271}272273if (processedIndentRulesSupport.shouldDecrease(lineNumber)) {274indentation = indentConverter.unshiftIndent(indentation);275}276277if (enterResult.appendText) {278indentation += enterResult.appendText;279}280281return strings.getLeadingWhitespace(indentation);282}283}284}285286if (processedIndentRulesSupport.shouldDecrease(lineNumber)) {287if (indent.action === IndentAction.Indent) {288return indent.indentation;289} else {290return indentConverter.unshiftIndent(indent.indentation);291}292} else {293if (indent.action === IndentAction.Indent) {294return indentConverter.shiftIndent(indent.indentation);295} else {296return indent.indentation;297}298}299}300return null;301}302303export function getIndentForEnter(304autoIndent: EditorAutoIndentStrategy,305model: ITextModel,306range: Range,307indentConverter: IIndentConverter,308languageConfigurationService: ILanguageConfigurationService309): { beforeEnter: string; afterEnter: string } | null {310if (autoIndent < EditorAutoIndentStrategy.Full) {311return null;312}313const languageId = model.getLanguageIdAtPosition(range.startLineNumber, range.startColumn);314const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(languageId).indentRulesSupport;315if (!indentRulesSupport) {316return null;317}318319model.tokenization.forceTokenization(range.startLineNumber);320const indentationContextProcessor = new IndentationContextProcessor(model, languageConfigurationService);321const processedContextTokens = indentationContextProcessor.getProcessedTokenContextAroundRange(range);322const afterEnterProcessedTokens = processedContextTokens.afterRangeProcessedTokens;323const beforeEnterProcessedTokens = processedContextTokens.beforeRangeProcessedTokens;324const beforeEnterIndent = strings.getLeadingWhitespace(beforeEnterProcessedTokens.getLineContent());325326const virtualModel = createVirtualModelWithModifiedTokensAtLine(model, range.startLineNumber, beforeEnterProcessedTokens);327const languageIsDifferentFromLineStart = isLanguageDifferentFromLineStart(model, range.getStartPosition());328const currentLine = model.getLineContent(range.startLineNumber);329const currentLineIndent = strings.getLeadingWhitespace(currentLine);330const afterEnterAction = getInheritIndentForLine(autoIndent, virtualModel, range.startLineNumber + 1, undefined, languageConfigurationService);331if (!afterEnterAction) {332const beforeEnter = languageIsDifferentFromLineStart ? currentLineIndent : beforeEnterIndent;333return {334beforeEnter: beforeEnter,335afterEnter: beforeEnter336};337}338339let afterEnterIndent = languageIsDifferentFromLineStart ? currentLineIndent : afterEnterAction.indentation;340341if (afterEnterAction.action === IndentAction.Indent) {342afterEnterIndent = indentConverter.shiftIndent(afterEnterIndent);343}344345if (indentRulesSupport.shouldDecrease(afterEnterProcessedTokens.getLineContent())) {346afterEnterIndent = indentConverter.unshiftIndent(afterEnterIndent);347}348349return {350beforeEnter: languageIsDifferentFromLineStart ? currentLineIndent : beforeEnterIndent,351afterEnter: afterEnterIndent352};353}354355/**356* We should always allow intentional indentation. It means, if users change the indentation of `lineNumber` and the content of357* this line doesn't match decreaseIndentPattern, we should not adjust the indentation.358*/359export function getIndentActionForType(360cursorConfig: CursorConfiguration,361model: ITextModel,362range: Range,363ch: string,364indentConverter: IIndentConverter,365languageConfigurationService: ILanguageConfigurationService366): string | null {367const autoIndent = cursorConfig.autoIndent;368if (autoIndent < EditorAutoIndentStrategy.Full) {369return null;370}371const languageIsDifferentFromLineStart = isLanguageDifferentFromLineStart(model, range.getStartPosition());372if (languageIsDifferentFromLineStart) {373// this line has mixed languages and indentation rules will not work374return null;375}376377const languageId = model.getLanguageIdAtPosition(range.startLineNumber, range.startColumn);378const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(languageId).indentRulesSupport;379if (!indentRulesSupport) {380return null;381}382383const indentationContextProcessor = new IndentationContextProcessor(model, languageConfigurationService);384const processedContextTokens = indentationContextProcessor.getProcessedTokenContextAroundRange(range);385const beforeRangeText = processedContextTokens.beforeRangeProcessedTokens.getLineContent();386const afterRangeText = processedContextTokens.afterRangeProcessedTokens.getLineContent();387const textAroundRange = beforeRangeText + afterRangeText;388const textAroundRangeWithCharacter = beforeRangeText + ch + afterRangeText;389390// If previous content already matches decreaseIndentPattern, it means indentation of this line should already be adjusted391// Users might change the indentation by purpose and we should honor that instead of readjusting.392if (!indentRulesSupport.shouldDecrease(textAroundRange) && indentRulesSupport.shouldDecrease(textAroundRangeWithCharacter)) {393// after typing `ch`, the content matches decreaseIndentPattern, we should adjust the indent to a good manner.394// 1. Get inherited indent action395const r = getInheritIndentForLine(autoIndent, model, range.startLineNumber, false, languageConfigurationService);396if (!r) {397return null;398}399400let indentation = r.indentation;401if (r.action !== IndentAction.Indent) {402indentation = indentConverter.unshiftIndent(indentation);403}404405return indentation;406}407408const previousLineNumber = range.startLineNumber - 1;409if (previousLineNumber > 0) {410const previousLine = model.getLineContent(previousLineNumber);411if (indentRulesSupport.shouldIndentNextLine(previousLine) && indentRulesSupport.shouldIncrease(textAroundRangeWithCharacter)) {412const inheritedIndentationData = getInheritIndentForLine(autoIndent, model, range.startLineNumber, false, languageConfigurationService);413const inheritedIndentation = inheritedIndentationData?.indentation;414if (inheritedIndentation !== undefined) {415const currentLine = model.getLineContent(range.startLineNumber);416const actualCurrentIndentation = strings.getLeadingWhitespace(currentLine);417const inferredCurrentIndentation = indentConverter.shiftIndent(inheritedIndentation);418// If the inferred current indentation is not equal to the actual current indentation, then the indentation has been intentionally changed, in that case keep it419const inferredIndentationEqualsActual = inferredCurrentIndentation === actualCurrentIndentation;420const textAroundRangeContainsOnlyWhitespace = /^\s*$/.test(textAroundRange);421const autoClosingPairs = cursorConfig.autoClosingPairs.autoClosingPairsOpenByEnd.get(ch);422const autoClosingPairExists = autoClosingPairs && autoClosingPairs.length > 0;423const isChFirstNonWhitespaceCharacterAndInAutoClosingPair = autoClosingPairExists && textAroundRangeContainsOnlyWhitespace;424if (inferredIndentationEqualsActual && isChFirstNonWhitespaceCharacterAndInAutoClosingPair) {425return inheritedIndentation;426}427}428}429}430431return null;432}433434export function getIndentMetadata(435model: ITextModel,436lineNumber: number,437languageConfigurationService: ILanguageConfigurationService438): number | null {439const indentRulesSupport = languageConfigurationService.getLanguageConfiguration(model.getLanguageId()).indentRulesSupport;440if (!indentRulesSupport) {441return null;442}443if (lineNumber < 1 || lineNumber > model.getLineCount()) {444return null;445}446return indentRulesSupport.getIndentMetadata(model.getLineContent(lineNumber));447}448449function createVirtualModelWithModifiedTokensAtLine(model: ITextModel, modifiedLineNumber: number, modifiedTokens: IViewLineTokens): IVirtualModel {450const virtualModel: IVirtualModel = {451tokenization: {452getLineTokens: (lineNumber: number): IViewLineTokens => {453if (lineNumber === modifiedLineNumber) {454return modifiedTokens;455} else {456return model.tokenization.getLineTokens(lineNumber);457}458},459getLanguageId: (): string => {460return model.getLanguageId();461},462getLanguageIdAtPosition: (lineNumber: number, column: number): string => {463return model.getLanguageIdAtPosition(lineNumber, column);464},465},466getLineContent: (lineNumber: number): string => {467if (lineNumber === modifiedLineNumber) {468return modifiedTokens.getLineContent();469} else {470return model.getLineContent(lineNumber);471}472}473};474return virtualModel;475}476477478479