Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keepins same indentation if increaseIndentPattern is satisifed on line n and increaseNextLinePattern is satisfied on line n-1 #216500

Merged
merged 2 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/vs/editor/common/cursor/cursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { CursorContext } from 'vs/editor/common/cursor/cursorContext';
import { DeleteOperations } from 'vs/editor/common/cursor/cursorDeleteOperations';
import { CursorChangeReason } from 'vs/editor/common/cursorEvents';
import { CompositionOutcome, TypeOperations } from 'vs/editor/common/cursor/cursorTypeOperations';
import { BaseTypeWithAutoClosingCommand } from 'vs/editor/common/cursor/cursorTypeEditOperations';
import { Position } from 'vs/editor/common/core/position';
import { Range, IRange } from 'vs/editor/common/core/range';
import { ISelection, Selection, SelectionDirection } from 'vs/editor/common/core/selection';
Expand All @@ -21,7 +22,6 @@ import { VerticalRevealType, ViewCursorStateChangedEvent, ViewRevealRangeRequest
import { dispose, Disposable } from 'vs/base/common/lifecycle';
import { ICoordinatesConverter } from 'vs/editor/common/viewModel';
import { CursorStateChangedEvent, ViewModelEventsCollector } from 'vs/editor/common/viewModelEventDispatcher';
import { TypeWithAutoClosingCommand } from 'vs/editor/common/cursor/cursorTypeEditOperations';

export class CursorsController extends Disposable {

Expand Down Expand Up @@ -368,7 +368,7 @@ export class CursorsController extends Disposable {

for (let i = 0; i < opResult.commands.length; i++) {
const command = opResult.commands[i];
if (command instanceof TypeWithAutoClosingCommand && command.enclosingRange && command.closeCharacterRange) {
if (command instanceof BaseTypeWithAutoClosingCommand && command.enclosingRange && command.closeCharacterRange) {
autoClosedCharactersRanges.push(command.closeCharacterRange);
autoClosedEnclosingRanges.push(command.enclosingRange);
}
Expand Down
136 changes: 96 additions & 40 deletions src/vs/editor/common/cursor/cursorTypeEditOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { WordCharacterClass, getMapForWordSeparators } from 'vs/editor/common/co
import { Range } from 'vs/editor/common/core/range';
import { Selection } from 'vs/editor/common/core/selection';
import { Position } from 'vs/editor/common/core/position';
import { ICommand, ICursorStateComputerData } from 'vs/editor/common/editorCommon';
import { ICommand, ICursorStateComputerData, IEditOperationBuilder } from 'vs/editor/common/editorCommon';
import { ITextModel } from 'vs/editor/common/model';
import { EnterAction, IndentAction, StandardAutoClosingPairConditional } from 'vs/editor/common/languages/languageConfiguration';
import { getIndentationAtPosition } from 'vs/editor/common/languages/languageConfigurationRegistry';
Expand All @@ -28,21 +28,17 @@ export class AutoIndentOperation {

public static getEdits(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, isDoingComposition: boolean): EditOperationResult | undefined {
if (!isDoingComposition && this._isAutoIndentType(config, model, selections)) {
const commands: Array<ICommand | null> = [];
let autoIndentFails = false;
for (let i = 0, len = selections.length; i < len; i++) {
commands[i] = this._runAutoIndentType(config, model, selections[i], ch);
if (!commands[i]) {
autoIndentFails = true;
break;
const indentationForSelections: { selection: Selection; indentation: string }[] = [];
for (const selection of selections) {
const indentation = this._findActualIndentationForSelection(config, model, selection, ch);
if (indentation === null) {
// Auto indentation failed
return;
}
indentationForSelections.push({ selection, indentation });
}
if (!autoIndentFails) {
return new EditOperationResult(EditOperationType.TypingOther, commands, {
shouldPushStackElementBefore: true,
shouldPushStackElementAfter: false,
});
}
const autoClosingPairClose = AutoClosingOpenCharTypeOperation.getAutoClosingPairClose(config, model, selections, ch, false);
return this._getIndentationAndAutoClosingPairEdits(config, model, indentationForSelections, ch, autoClosingPairClose);
}
return;
}
Expand All @@ -59,9 +55,8 @@ export class AutoIndentOperation {
return true;
}

private static _runAutoIndentType(config: CursorConfiguration, model: ITextModel, range: Range, ch: string): ICommand | null {
const currentIndentation = getIndentationAtPosition(model, range.startLineNumber, range.startColumn);
const actualIndentation = getIndentActionForType(config.autoIndent, model, range, ch, {
private static _findActualIndentationForSelection(config: CursorConfiguration, model: ITextModel, selection: Selection, ch: string): string | null {
const actualIndentation = getIndentActionForType(config, model, selection, ch, {
shiftIndent: (indentation) => {
return shiftIndent(config, indentation);
},
Expand All @@ -74,24 +69,40 @@ export class AutoIndentOperation {
return null;
}

if (actualIndentation !== config.normalizeIndentation(currentIndentation)) {
const firstNonWhitespace = model.getLineFirstNonWhitespaceColumn(range.startLineNumber);
if (firstNonWhitespace === 0) {
return typeCommand(
new Range(range.startLineNumber, 1, range.endLineNumber, range.endColumn),
config.normalizeIndentation(actualIndentation) + ch,
false
);
const currentIndentation = getIndentationAtPosition(model, selection.startLineNumber, selection.startColumn);
if (actualIndentation === config.normalizeIndentation(currentIndentation)) {
return null;
}
return actualIndentation;
}

private static _getIndentationAndAutoClosingPairEdits(config: CursorConfiguration, model: ITextModel, indentationForSelections: { selection: Selection; indentation: string }[], ch: string, autoClosingPairClose: string | null): EditOperationResult {
const commands: ICommand[] = indentationForSelections.map(({ selection, indentation }) => {
if (autoClosingPairClose !== null) {
// Apply both auto closing pair edits and auto indentation edits
const indentationEdit = this._getEditFromIndentationAndSelection(config, model, indentation, selection, ch, false);
return new TypeWithIndentationAndAutoClosingCommand(indentationEdit, selection, ch, autoClosingPairClose);
} else {
return typeCommand(
new Range(range.startLineNumber, 1, range.endLineNumber, range.endColumn),
config.normalizeIndentation(actualIndentation) +
model.getLineContent(range.startLineNumber).substring(firstNonWhitespace - 1, range.startColumn - 1) + ch,
false
);
// Apply only auto indentation edits
const indentationEdit = this._getEditFromIndentationAndSelection(config, model, indentation, selection, ch, true);
return typeCommand(indentationEdit.range, indentationEdit.text, false);
}
});
const editOptions = { shouldPushStackElementBefore: true, shouldPushStackElementAfter: false };
return new EditOperationResult(EditOperationType.TypingOther, commands, editOptions);
}

private static _getEditFromIndentationAndSelection(config: CursorConfiguration, model: ITextModel, indentation: string, selection: Selection, ch: string, includeChInEdit: boolean = true): { range: Range; text: string } {
const startLineNumber = selection.startLineNumber;
const firstNonWhitespaceColumn = model.getLineFirstNonWhitespaceColumn(startLineNumber);
let text: string = config.normalizeIndentation(indentation);
if (firstNonWhitespaceColumn !== 0) {
const startLine = model.getLineContent(startLineNumber);
text += startLine.substring(firstNonWhitespaceColumn - 1, selection.startColumn - 1);
}
return null;
text += includeChInEdit ? ch : '';
const range = new Range(startLineNumber, 1, selection.endLineNumber, selection.endColumn);
return { range, text };
}
}

Expand Down Expand Up @@ -138,7 +149,7 @@ export class AutoClosingOpenCharTypeOperation {

public static getEdits(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, chIsAlreadyTyped: boolean, isDoingComposition: boolean): EditOperationResult | undefined {
if (!isDoingComposition) {
const autoClosingPairClose = this._getAutoClosingPairClose(config, model, selections, ch, chIsAlreadyTyped);
const autoClosingPairClose = this.getAutoClosingPairClose(config, model, selections, ch, chIsAlreadyTyped);
if (autoClosingPairClose !== null) {
return this._runAutoClosingOpenCharType(selections, ch, chIsAlreadyTyped, autoClosingPairClose);
}
Expand All @@ -158,7 +169,7 @@ export class AutoClosingOpenCharTypeOperation {
});
}

private static _getAutoClosingPairClose(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, chIsAlreadyTyped: boolean): string | null {
public static getAutoClosingPairClose(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, chIsAlreadyTyped: boolean): string | null {
for (const selection of selections) {
if (!selection.isEmpty()) {
return null;
Expand Down Expand Up @@ -843,30 +854,75 @@ export class TabOperation {
}
}

export class TypeWithAutoClosingCommand extends ReplaceCommandWithOffsetCursorState {
export class BaseTypeWithAutoClosingCommand extends ReplaceCommandWithOffsetCursorState {

private readonly _openCharacter: string;
private readonly _closeCharacter: string;
public closeCharacterRange: Range | null;
public enclosingRange: Range | null;

constructor(selection: Selection, openCharacter: string, insertOpenCharacter: boolean, closeCharacter: string) {
super(selection, (insertOpenCharacter ? openCharacter : '') + closeCharacter, 0, -closeCharacter.length);
constructor(selection: Selection, text: string, lineNumberDeltaOffset: number, columnDeltaOffset: number, openCharacter: string, closeCharacter: string) {
super(selection, text, lineNumberDeltaOffset, columnDeltaOffset);
this._openCharacter = openCharacter;
this._closeCharacter = closeCharacter;
this.closeCharacterRange = null;
this.enclosingRange = null;
}

public override computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection {
const inverseEditOperations = helper.getInverseEditOperations();
const range = inverseEditOperations[0].range;
protected _computeCursorStateWithRange(model: ITextModel, range: Range, helper: ICursorStateComputerData): Selection {
this.closeCharacterRange = new Range(range.startLineNumber, range.endColumn - this._closeCharacter.length, range.endLineNumber, range.endColumn);
this.enclosingRange = new Range(range.startLineNumber, range.endColumn - this._openCharacter.length - this._closeCharacter.length, range.endLineNumber, range.endColumn);
return super.computeCursorState(model, helper);
}
}

class TypeWithAutoClosingCommand extends BaseTypeWithAutoClosingCommand {

constructor(selection: Selection, openCharacter: string, insertOpenCharacter: boolean, closeCharacter: string) {
const text = (insertOpenCharacter ? openCharacter : '') + closeCharacter;
const lineNumberDeltaOffset = 0;
const columnDeltaOffset = -closeCharacter.length;
super(selection, text, lineNumberDeltaOffset, columnDeltaOffset, openCharacter, closeCharacter);
}

public override computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection {
const inverseEditOperations = helper.getInverseEditOperations();
const range = inverseEditOperations[0].range;
return this._computeCursorStateWithRange(model, range, helper);
}
}

class TypeWithIndentationAndAutoClosingCommand extends BaseTypeWithAutoClosingCommand {

private readonly _autoIndentationEdit: { range: Range; text: string };
private readonly _autoClosingEdit: { range: Range; text: string };

constructor(autoIndentationEdit: { range: Range; text: string }, selection: Selection, openCharacter: string, closeCharacter: string) {
const text = openCharacter + closeCharacter;
const lineNumberDeltaOffset = 0;
const columnDeltaOffset = openCharacter.length;
super(selection, text, lineNumberDeltaOffset, columnDeltaOffset, openCharacter, closeCharacter);
this._autoIndentationEdit = autoIndentationEdit;
this._autoClosingEdit = { range: selection, text };
}

public override getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void {
builder.addTrackedEditOperation(this._autoIndentationEdit.range, this._autoIndentationEdit.text);
builder.addTrackedEditOperation(this._autoClosingEdit.range, this._autoClosingEdit.text);
}

public override computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection {
const inverseEditOperations = helper.getInverseEditOperations();
if (inverseEditOperations.length !== 2) {
throw new Error('There should be two inverse edit operations!');
}
const range1 = inverseEditOperations[0].range;
const range2 = inverseEditOperations[1].range;
const range = range1.plusRange(range2);
return this._computeCursorStateWithRange(model, range, helper);
}
}

function getTypingOperation(typedText: string, previousTypingOperation: EditOperationType): EditOperationType {
if (typedText === ' ') {
return previousTypingOperation === EditOperationType.TypingFirstSpace
Expand Down
27 changes: 26 additions & 1 deletion src/vs/editor/common/languages/autoIndent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { EditorAutoIndentStrategy } from 'vs/editor/common/config/editorOptions'
import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry';
import { IViewLineTokens } from 'vs/editor/common/tokens/lineTokens';
import { IndentationContextProcessor, isLanguageDifferentFromLineStart, ProcessedIndentRulesSupport } from 'vs/editor/common/languages/supports/indentationLineProcessor';
import { CursorConfiguration } from 'vs/editor/common/cursorCommon';

export interface IVirtualModel {
tokenization: {
Expand Down Expand Up @@ -357,13 +358,14 @@ export function getIndentForEnter(
* this line doesn't match decreaseIndentPattern, we should not adjust the indentation.
*/
export function getIndentActionForType(
autoIndent: EditorAutoIndentStrategy,
cursorConfig: CursorConfiguration,
model: ITextModel,
range: Range,
ch: string,
indentConverter: IIndentConverter,
languageConfigurationService: ILanguageConfigurationService
): string | null {
const autoIndent = cursorConfig.autoIndent;
if (autoIndent < EditorAutoIndentStrategy.Full) {
return null;
}
Expand Down Expand Up @@ -404,6 +406,29 @@ export function getIndentActionForType(
return indentation;
}

const previousLineNumber = range.startLineNumber - 1;
if (previousLineNumber > 0) {
const previousLine = model.getLineContent(previousLineNumber);
if (indentRulesSupport.shouldIndentNextLine(previousLine) && indentRulesSupport.shouldIncrease(textAroundRangeWithCharacter)) {
const inheritedIndentationData = getInheritIndentForLine(autoIndent, model, range.startLineNumber, false, languageConfigurationService);
const inheritedIndentation = inheritedIndentationData?.indentation;
if (inheritedIndentation !== undefined) {
const currentLine = model.getLineContent(range.startLineNumber);
const actualCurrentIndentation = strings.getLeadingWhitespace(currentLine);
const inferredCurrentIndentation = indentConverter.shiftIndent(inheritedIndentation);
// If the inferred current indentation is not equal to the actual current indentation, then the indentation has been intentionally changed, in that case keep it
const inferredIndentationEqualsActual = inferredCurrentIndentation === actualCurrentIndentation;
const textAroundRangeContainsOnlyWhitespace = /^\s*$/.test(textAroundRange);
const autoClosingPairs = cursorConfig.autoClosingPairs.autoClosingPairsOpenByEnd.get(ch);
const autoClosingPairExists = autoClosingPairs && autoClosingPairs.length > 0;
const isChFirstNonWhitespaceCharacterAndInAutoClosingPair = autoClosingPairExists && textAroundRangeContainsOnlyWhitespace;
if (inferredIndentationEqualsActual && isChFirstNonWhitespaceCharacterAndInAutoClosingPair) {
return inheritedIndentation;
}
}
}
}

return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { goIndentationRules, htmlIndentationRules, javascriptIndentationRules, l
import { cppOnEnterRules, htmlOnEnterRules, javascriptOnEnterRules, phpOnEnterRules } from 'vs/editor/test/common/modes/supports/onEnterRules';
import { TypeOperations } from 'vs/editor/common/cursor/cursorTypeOperations';
import { cppBracketRules, goBracketRules, htmlBracketRules, latexBracketRules, luaBracketRules, phpBracketRules, rubyBracketRules, typescriptBracketRules, vbBracketRules } from 'vs/editor/test/common/modes/supports/bracketRules';
import { latexAutoClosingPairsRules } from 'vs/editor/test/common/modes/supports/autoClosingPairsRules';
import { javascriptAutoClosingPairsRules, latexAutoClosingPairsRules } from 'vs/editor/test/common/modes/supports/autoClosingPairsRules';
import { LanguageService } from 'vs/editor/common/services/languageService';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService';
Expand Down Expand Up @@ -60,6 +60,7 @@ export function registerLanguageConfiguration(languageConfigurationService: ILan
lineComment: '//',
blockComment: ['/*', '*/']
},
autoClosingPairs: javascriptAutoClosingPairsRules,
indentationRules: javascriptIndentationRules,
onEnterRules: javascriptOnEnterRules
});
Expand Down Expand Up @@ -1076,6 +1077,38 @@ suite('Auto Indent On Type - TypeScript/JavaScript', () => {
});
});

test('issue #209802: allman style braces in JavaScript', () => {

// https://github.com/microsoft/vscode/issues/209802

const model = createTextModel([
'if (/*condition*/)',
].join('\n'), languageId, {});
disposables.add(model);

withTestCodeEditor(model, { autoIndent: "full", serviceCollection }, (editor, viewModel) => {
editor.setSelection(new Selection(1, 19, 1, 19));
viewModel.type("\n", 'keyboard');
assert.strictEqual(model.getValue(), [
'if (/*condition*/)',
' '
].join('\n'));
viewModel.type("{", 'keyboard');
assert.strictEqual(model.getValue(), [
'if (/*condition*/)',
'{}'
].join('\n'));
editor.setSelection(new Selection(2, 2, 2, 2));
viewModel.type("\n", 'keyboard');
assert.strictEqual(model.getValue(), [
'if (/*condition*/)',
'{',
' ',
'}'
].join('\n'));
});
});

// Failing tests...

test.skip('issue #43244: indent after equal sign is detected', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,17 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { IAutoClosingPair } from 'vs/editor/common/languages/languageConfiguration';
import { IAutoClosingPair, IAutoClosingPairConditional } from 'vs/editor/common/languages/languageConfiguration';

export const javascriptAutoClosingPairsRules: IAutoClosingPairConditional[] = [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '\'', close: '\'', notIn: ['string', 'comment'] },
{ open: '"', close: '"', notIn: ['string'] },
{ open: '`', close: '`', notIn: ['string', 'comment'] },
{ open: '/**', close: ' */', notIn: ['string'] }
];

export const latexAutoClosingPairsRules: IAutoClosingPair[] = [
{ open: '\\left(', close: '\\right)' },
Expand Down
Loading