提交 37981d8f 编写于 作者: A Alex Dima

Fixes #79430: Bring all auto-closing logic in one place which can now also...

Fixes #79430: Bring all auto-closing logic in one place which can now also handle multi-character auto-closing pairs
上级 1271e25f
......@@ -86,6 +86,14 @@ export class CursorModelState {
class AutoClosedAction {
public static getAllAutoClosedCharacters(autoClosedActions: AutoClosedAction[]): Range[] {
let autoClosedCharacters: Range[] = [];
for (const autoClosedAction of autoClosedActions) {
autoClosedCharacters = autoClosedCharacters.concat(autoClosedAction.getAutoClosedCharactersRanges());
}
return autoClosedCharacters;
}
private readonly _model: ITextModel;
private _autoClosedCharactersDecorations: string[];
......@@ -593,11 +601,12 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors {
}
const closeChar = m[1];
const openChar = this.context.config.autoClosingPairsClose[closeChar];
if (!openChar) {
const autoClosingPairsCandidates = this.context.config.autoClosingPairsClose2.get(closeChar);
if (!autoClosingPairsCandidates || autoClosingPairsCandidates.length !== 1) {
return null;
}
const openChar = autoClosingPairsCandidates[0].open;
const closeCharIndex = edit.text.length - m[2].length - 1;
const openCharIndex = edit.text.lastIndexOf(openChar, closeCharIndex - 1);
if (openCharIndex === -1) {
......@@ -738,7 +747,8 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors {
private _interpretCompositionEnd(source: string) {
if (!this._isDoingComposition && source === 'keyboard') {
// composition finishes, let's check if we need to auto complete if necessary.
this._executeEditOperation(TypeOperations.compositionEndWithInterceptors(this._prevEditOperationType, this.context.config, this.context.model, this.getSelections()));
const autoClosedCharacters = AutoClosedAction.getAllAutoClosedCharacters(this._autoClosedActions);
this._executeEditOperation(TypeOperations.compositionEndWithInterceptors(this._prevEditOperationType, this.context.config, this.context.model, this.getSelections(), autoClosedCharacters));
}
}
......@@ -756,14 +766,8 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors {
chr = text.charAt(i);
}
let autoClosedCharacters: Range[] = [];
if (this._autoClosedActions.length > 0) {
for (let i = 0, len = this._autoClosedActions.length; i < len; i++) {
autoClosedCharacters = autoClosedCharacters.concat(this._autoClosedActions[i].getAutoClosedCharactersRanges());
}
}
// Here we must interpret each typed character individually, that's why we create a new context
// Here we must interpret each typed character individually
const autoClosedCharacters = AutoClosedAction.getAllAutoClosedCharacters(this._autoClosedActions);
this._executeEditOperation(TypeOperations.typeWithInterceptors(this._prevEditOperationType, this.context.config, this.context.model, this.getSelections(), autoClosedCharacters, chr));
}
......
......@@ -15,7 +15,7 @@ import { ICommand, IConfiguration, ScrollType } from 'vs/editor/common/editorCom
import { ITextModel, TextModelResolvedOptions } from 'vs/editor/common/model';
import { TextModel } from 'vs/editor/common/model/textModel';
import { LanguageIdentifier } from 'vs/editor/common/modes';
import { IAutoClosingPair } from 'vs/editor/common/modes/languageConfiguration';
import { IAutoClosingPair, StandardAutoClosingPairConditional } from 'vs/editor/common/modes/languageConfiguration';
import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
import { VerticalRevealType } from 'vs/editor/common/view/viewEvents';
import { IViewModel } from 'vs/editor/common/viewModel/viewModel';
......@@ -67,11 +67,22 @@ export interface ICursors {
export interface CharacterMap {
[char: string]: string;
}
export interface MultipleCharacterMap {
[char: string]: string[];
}
const autoCloseAlways = () => true;
const autoCloseNever = () => false;
const autoCloseBeforeWhitespace = (chr: string) => (chr === ' ' || chr === '\t');
function appendEntry<K, V>(target: Map<K, V[]>, key: K, value: V): void {
if (target.has(key)) {
target.get(key)!.push(value);
} else {
target.set(key, [value]);
}
}
export class CursorConfiguration {
_cursorMoveConfigurationBrand: void;
......@@ -90,8 +101,8 @@ export class CursorConfiguration {
public readonly autoClosingQuotes: EditorAutoClosingStrategy;
public readonly autoSurround: EditorAutoSurroundStrategy;
public readonly autoIndent: boolean;
public readonly autoClosingPairsOpen: CharacterMap;
public readonly autoClosingPairsClose: CharacterMap;
public readonly autoClosingPairsOpen2: Map<string, StandardAutoClosingPairConditional[]>;
public readonly autoClosingPairsClose2: Map<string, StandardAutoClosingPairConditional[]>;
public readonly surroundingPairs: CharacterMap;
public readonly shouldAutoCloseBefore: { quote: (ch: string) => boolean, bracket: (ch: string) => boolean };
......@@ -138,8 +149,8 @@ export class CursorConfiguration {
this.autoSurround = c.autoSurround;
this.autoIndent = c.autoIndent;
this.autoClosingPairsOpen = {};
this.autoClosingPairsClose = {};
this.autoClosingPairsOpen2 = new Map<string, StandardAutoClosingPairConditional[]>();
this.autoClosingPairsClose2 = new Map<string, StandardAutoClosingPairConditional[]>();
this.surroundingPairs = {};
this._electricChars = null;
......@@ -151,8 +162,10 @@ export class CursorConfiguration {
let autoClosingPairs = CursorConfiguration._getAutoClosingPairs(languageIdentifier);
if (autoClosingPairs) {
for (const pair of autoClosingPairs) {
this.autoClosingPairsOpen[pair.open] = pair.close;
this.autoClosingPairsClose[pair.close] = pair.open;
appendEntry(this.autoClosingPairsOpen2, pair.open.charAt(pair.open.length - 1), pair);
if (pair.close.length === 1) {
appendEntry(this.autoClosingPairsClose2, pair.close, pair);
}
}
}
......@@ -190,7 +203,7 @@ export class CursorConfiguration {
}
}
private static _getAutoClosingPairs(languageIdentifier: LanguageIdentifier): IAutoClosingPair[] | null {
private static _getAutoClosingPairs(languageIdentifier: LanguageIdentifier): StandardAutoClosingPairConditional[] | null {
try {
return LanguageConfigurationRegistry.getAutoClosingPairs(languageIdentifier.id);
} catch (e) {
......
......@@ -63,7 +63,8 @@ export class DeleteOperations {
const lineText = model.getLineContent(position.lineNumber);
const character = lineText[position.column - 2];
if (!config.autoClosingPairsOpen.hasOwnProperty(character)) {
const autoClosingPairCandidates = config.autoClosingPairsOpen2.get(character);
if (!autoClosingPairCandidates) {
return false;
}
......@@ -78,9 +79,14 @@ export class DeleteOperations {
}
const afterCharacter = lineText[position.column - 1];
const closeCharacter = config.autoClosingPairsOpen[character];
if (afterCharacter !== closeCharacter) {
let foundAutoClosingPair = false;
for (const autoClosingPairCandidate of autoClosingPairCandidates) {
if (autoClosingPairCandidate.open === character && autoClosingPairCandidate.close === afterCharacter) {
foundAutoClosingPair = true;
}
}
if (!foundAutoClosingPair) {
return false;
}
}
......
......@@ -13,9 +13,10 @@ import { CursorColumns, CursorConfiguration, EditOperationResult, EditOperationT
import { WordCharacterClass, getMapForWordSeparators } from 'vs/editor/common/controller/wordCharacterClassifier';
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 { ITextModel } from 'vs/editor/common/model';
import { EnterAction, IndentAction } from 'vs/editor/common/modes/languageConfiguration';
import { EnterAction, IndentAction, StandardAutoClosingPairConditional } from 'vs/editor/common/modes/languageConfiguration';
import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
import { IElectricAction } from 'vs/editor/common/modes/supports/electricCharacter';
......@@ -433,7 +434,11 @@ export class TypeOperations {
private static _isAutoClosingCloseCharType(config: CursorConfiguration, model: ITextModel, selections: Selection[], autoClosedCharacters: Range[], ch: string): boolean {
const autoCloseConfig = isQuote(ch) ? config.autoClosingQuotes : config.autoClosingBrackets;
if (autoCloseConfig === 'never' || !config.autoClosingPairsClose.hasOwnProperty(ch)) {
if (autoCloseConfig === 'never') {
return false;
}
if (!config.autoClosingPairsClose2.has(ch)) {
return false;
}
......@@ -469,15 +474,6 @@ export class TypeOperations {
return true;
}
private static _countNeedlesInHaystack(haystack: string, needle: string): number {
let cnt = 0;
let lastIndex = -1;
while ((lastIndex = haystack.indexOf(needle, lastIndex + 1)) !== -1) {
cnt++;
}
return cnt;
}
private static _runAutoClosingCloseCharType(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string): EditOperationResult {
let commands: ICommand[] = [];
for (let i = 0, len = selections.length; i < len; i++) {
......@@ -492,65 +488,98 @@ export class TypeOperations {
});
}
private static _isBeforeClosingBrace(config: CursorConfiguration, ch: string, characterAfter: string) {
const thisBraceIsSymmetric = (config.autoClosingPairsOpen[ch] === ch);
let isBeforeCloseBrace = false;
for (let otherCloseBrace in config.autoClosingPairsClose) {
const otherBraceIsSymmetric = (config.autoClosingPairsOpen[otherCloseBrace] === otherCloseBrace);
private static _isBeforeClosingBrace(config: CursorConfiguration, autoClosingPair: StandardAutoClosingPairConditional, characterAfter: string) {
const otherAutoClosingPairs = config.autoClosingPairsClose2.get(characterAfter);
if (!otherAutoClosingPairs) {
return false;
}
const thisBraceIsSymmetric = (autoClosingPair.open === autoClosingPair.close);
for (const otherAutoClosingPair of otherAutoClosingPairs) {
const otherBraceIsSymmetric = (otherAutoClosingPair.open === otherAutoClosingPair.close);
if (!thisBraceIsSymmetric && otherBraceIsSymmetric) {
continue;
}
if (characterAfter === otherCloseBrace) {
isBeforeCloseBrace = true;
break;
}
return true;
}
return isBeforeCloseBrace;
return false;
}
private static _isAutoClosingOpenCharType(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string): boolean {
private static _findAutoClosingPairOpen(config: CursorConfiguration, model: ITextModel, positions: Position[], ch: string): StandardAutoClosingPairConditional | null {
const autoClosingPairCandidates = config.autoClosingPairsOpen2.get(ch);
if (!autoClosingPairCandidates) {
return null;
}
// Determine which auto-closing pair it is
let autoClosingPair: StandardAutoClosingPairConditional | null = null;
for (const autoClosingPairCandidate of autoClosingPairCandidates) {
if (autoClosingPair === null || autoClosingPairCandidate.open.length > autoClosingPair.open.length) {
let candidateIsMatch = true;
for (const position of positions) {
const relevantText = model.getValueInRange(new Range(position.lineNumber, position.column - autoClosingPairCandidate.open.length + 1, position.lineNumber, position.column));
if (relevantText + ch !== autoClosingPairCandidate.open) {
candidateIsMatch = false;
break;
}
}
if (candidateIsMatch) {
autoClosingPair = autoClosingPairCandidate;
}
}
}
return autoClosingPair;
}
private static _isAutoClosingOpenCharType(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, insertOpenCharacter: boolean): StandardAutoClosingPairConditional | null {
const chIsQuote = isQuote(ch);
const autoCloseConfig = chIsQuote ? config.autoClosingQuotes : config.autoClosingBrackets;
if (autoCloseConfig === 'never') {
return null;
}
if (autoCloseConfig === 'never' || !config.autoClosingPairsOpen.hasOwnProperty(ch)) {
return false;
const autoClosingPair = this._findAutoClosingPairOpen(config, model, selections.map(s => s.getPosition()), ch);
if (!autoClosingPair) {
return null;
}
let shouldAutoCloseBefore = chIsQuote ? config.shouldAutoCloseBefore.quote : config.shouldAutoCloseBefore.bracket;
const shouldAutoCloseBefore = chIsQuote ? config.shouldAutoCloseBefore.quote : config.shouldAutoCloseBefore.bracket;
for (let i = 0, len = selections.length; i < len; i++) {
const selection = selections[i];
if (!selection.isEmpty()) {
return false;
return null;
}
const position = selection.getPosition();
const lineText = model.getLineContent(position.lineNumber);
// Do not auto-close ' or " after a word character
if ((chIsQuote && position.column > 1) && autoCloseConfig !== 'always') {
const wordSeparators = getMapForWordSeparators(config.wordSeparators);
const characterBeforeCode = lineText.charCodeAt(position.column - 2);
const characterBeforeType = wordSeparators.get(characterBeforeCode);
if (characterBeforeType === WordCharacterClass.Regular) {
return false;
}
}
// Only consider auto closing the pair if a space follows or if another autoclosed pair follows
const characterAfter = lineText.charAt(position.column - 1);
if (characterAfter) {
let isBeforeCloseBrace = TypeOperations._isBeforeClosingBrace(config, ch, characterAfter);
if (lineText.length > position.column - 1) {
const characterAfter = lineText.charAt(position.column - 1);
const isBeforeCloseBrace = TypeOperations._isBeforeClosingBrace(config, autoClosingPair, characterAfter);
if (!isBeforeCloseBrace && !shouldAutoCloseBefore(characterAfter)) {
return false;
return null;
}
}
if (!model.isCheapToTokenize(position.lineNumber)) {
// Do not force tokenization
return false;
return null;
}
// Do not auto-close ' or " after a word character
if (autoClosingPair.open.length === 1 && chIsQuote && autoCloseConfig !== 'always') {
const wordSeparators = getMapForWordSeparators(config.wordSeparators);
if (insertOpenCharacter && position.column > 1 && wordSeparators.get(lineText.charCodeAt(position.column - 2)) === WordCharacterClass.Regular) {
return null;
}
if (!insertOpenCharacter && position.column > 2 && wordSeparators.get(lineText.charCodeAt(position.column - 3)) === WordCharacterClass.Regular) {
return null;
}
}
model.forceTokenization(position.lineNumber);
......@@ -558,25 +587,24 @@ export class TypeOperations {
let shouldAutoClosePair = false;
try {
shouldAutoClosePair = LanguageConfigurationRegistry.shouldAutoClosePair(ch, lineTokens, position.column);
shouldAutoClosePair = LanguageConfigurationRegistry.shouldAutoClosePair(autoClosingPair, lineTokens, insertOpenCharacter ? position.column : position.column - 1);
} catch (e) {
onUnexpectedError(e);
}
if (!shouldAutoClosePair) {
return false;
return null;
}
}
return true;
return autoClosingPair;
}
private static _runAutoClosingOpenCharType(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string): EditOperationResult {
private static _runAutoClosingOpenCharType(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, insertOpenCharacter: boolean, autoClosingPair: StandardAutoClosingPairConditional): EditOperationResult {
let commands: ICommand[] = [];
for (let i = 0, len = selections.length; i < len; i++) {
const selection = selections[i];
const closeCharacter = config.autoClosingPairsOpen[ch];
commands[i] = new TypeWithAutoClosingCommand(selection, ch, closeCharacter);
commands[i] = new TypeWithAutoClosingCommand(selection, ch, insertOpenCharacter, autoClosingPair.close);
}
return new EditOperationResult(EditOperationType.Typing, commands, {
shouldPushStackElementBefore: true,
......@@ -679,14 +707,6 @@ export class TypeOperations {
return null;
}
if (electricAction.appendText) {
const command = new ReplaceCommandWithOffsetCursorState(selection, ch + electricAction.appendText, 0, -electricAction.appendText.length);
return new EditOperationResult(EditOperationType.Typing, [command], {
shouldPushStackElementBefore: false,
shouldPushStackElementAfter: true
});
}
if (electricAction.matchOpenBracket) {
let endColumn = (lineTokens.getLineContent() + ch).lastIndexOf(electricAction.matchOpenBracket) + 1;
let match = model.findMatchingBracketUp(electricAction.matchOpenBracket, {
......@@ -722,87 +742,44 @@ export class TypeOperations {
return null;
}
public static compositionEndWithInterceptors(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[]): EditOperationResult | null {
if (config.autoClosingQuotes === 'never') {
return null;
}
let commands: ICommand[] = [];
for (let i = 0; i < selections.length; i++) {
if (!selections[i].isEmpty()) {
continue;
/**
* This is very similar with typing, but the character is already in the text buffer!
*/
public static compositionEndWithInterceptors(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], autoClosedCharacters: Range[]): EditOperationResult | null {
let ch: string | null = null;
// extract last typed character
for (const selection of selections) {
if (!selection.isEmpty()) {
return null;
}
const position = selections[i].getPosition();
const lineText = model.getLineContent(position.lineNumber);
const ch = lineText.charAt(position.column - 2);
if (config.autoClosingPairsClose.hasOwnProperty(ch)) { // first of all, it's a closing tag
if (ch === config.autoClosingPairsClose[ch] /** isEqualPair */) {
const lineTextBeforeCursor = lineText.substr(0, position.column - 2);
const chCntBefore = this._countNeedlesInHaystack(lineTextBeforeCursor, ch);
if (chCntBefore % 2 === 1) {
continue; // it pairs with the opening tag.
}
}
const position = selection.getPosition();
const currentChar = model.getValueInRange(new Range(position.lineNumber, position.column - 1, position.lineNumber, position.column));
if (ch === null) {
ch = currentChar;
} else if (ch !== currentChar) {
return null;
}
}
// As we are not typing in a new character, so we don't need to run `_runAutoClosingCloseCharType`
// Next step, let's try to check if it's an open char.
if (config.autoClosingPairsOpen.hasOwnProperty(ch)) {
if (isQuote(ch) && position.column > 2) {
const wordSeparators = getMapForWordSeparators(config.wordSeparators);
const characterBeforeCode = lineText.charCodeAt(position.column - 3);
const characterBeforeType = wordSeparators.get(characterBeforeCode);
if (characterBeforeType === WordCharacterClass.Regular) {
continue;
}
}
const characterAfter = lineText.charAt(position.column - 1);
if (characterAfter) {
let isBeforeCloseBrace = TypeOperations._isBeforeClosingBrace(config, ch, characterAfter);
let shouldAutoCloseBefore = isQuote(ch) ? config.shouldAutoCloseBefore.quote : config.shouldAutoCloseBefore.bracket;
if (isBeforeCloseBrace) {
// In normal auto closing logic, we will auto close if the cursor is even before a closing brace intentionally.
// However for composition mode, we do nothing here as users might clear all the characters for composition and we don't want to do a unnecessary auto close.
// Related: microsoft/vscode#57250.
continue;
}
if (!shouldAutoCloseBefore(characterAfter)) {
continue;
}
}
if (!model.isCheapToTokenize(position.lineNumber)) {
// Do not force tokenization
continue;
}
model.forceTokenization(position.lineNumber);
const lineTokens = model.getLineTokens(position.lineNumber);
let shouldAutoClosePair = false;
if (!ch) {
return null;
}
try {
shouldAutoClosePair = LanguageConfigurationRegistry.shouldAutoClosePair(ch, lineTokens, position.column - 1);
} catch (e) {
onUnexpectedError(e);
}
if (this._isAutoClosingCloseCharType(config, model, selections, autoClosedCharacters, ch)) {
// Unfortunately, the close character is at this point "doubled", so we need to delete it...
const commands = selections.map(s => new ReplaceCommand(new Range(s.positionLineNumber, s.positionColumn, s.positionLineNumber, s.positionColumn + 1), '', false));
return new EditOperationResult(EditOperationType.Typing, commands, {
shouldPushStackElementBefore: true,
shouldPushStackElementAfter: false
});
}
if (shouldAutoClosePair) {
const closeCharacter = config.autoClosingPairsOpen[ch];
commands[i] = new ReplaceCommandWithOffsetCursorState(selections[i], closeCharacter, 0, -closeCharacter.length);
}
}
const autoClosingPairOpenCharType = this._isAutoClosingOpenCharType(config, model, selections, ch, false);
if (autoClosingPairOpenCharType) {
return this._runAutoClosingOpenCharType(prevEditOperationType, config, model, selections, ch, false, autoClosingPairOpenCharType);
}
return new EditOperationResult(EditOperationType.Typing, commands, {
shouldPushStackElementBefore: true,
shouldPushStackElementAfter: false
});
return null;
}
public static typeWithInterceptors(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], autoClosedCharacters: Range[], ch: string): EditOperationResult {
......@@ -840,8 +817,9 @@ export class TypeOperations {
return this._runAutoClosingCloseCharType(prevEditOperationType, config, model, selections, ch);
}
if (this._isAutoClosingOpenCharType(config, model, selections, ch)) {
return this._runAutoClosingOpenCharType(prevEditOperationType, config, model, selections, ch);
const autoClosingPairOpenCharType = this._isAutoClosingOpenCharType(config, model, selections, ch, true);
if (autoClosingPairOpenCharType) {
return this._runAutoClosingOpenCharType(prevEditOperationType, config, model, selections, ch, true, autoClosingPairOpenCharType);
}
if (this._isSurroundSelectionType(config, model, selections, ch)) {
......@@ -929,12 +907,14 @@ export class TypeOperations {
export class TypeWithAutoClosingCommand extends ReplaceCommandWithOffsetCursorState {
private _closeCharacter: string;
private readonly _openCharacter: string;
private readonly _closeCharacter: string;
public closeCharacterRange: Range | null;
public enclosingRange: Range | null;
constructor(selection: Selection, openCharacter: string, closeCharacter: string) {
super(selection, openCharacter + closeCharacter, 0, -closeCharacter.length);
constructor(selection: Selection, openCharacter: string, insertOpenCharacter: boolean, closeCharacter: string) {
super(selection, (insertOpenCharacter ? openCharacter : '') + closeCharacter, 0, -closeCharacter.length);
this._openCharacter = openCharacter;
this._closeCharacter = closeCharacter;
this.closeCharacterRange = null;
this.enclosingRange = null;
......@@ -944,7 +924,7 @@ export class TypeWithAutoClosingCommand extends ReplaceCommandWithOffsetCursorSt
let inverseEditOperations = helper.getInverseEditOperations();
let range = inverseEditOperations[0].range;
this.closeCharacterRange = new Range(range.startLineNumber, range.endColumn - this._closeCharacter.length, range.endLineNumber, range.endColumn);
this.enclosingRange = range;
this.enclosingRange = new Range(range.startLineNumber, range.endColumn - this._openCharacter.length - this._closeCharacter.length, range.endLineNumber, range.endColumn);
return super.computeCursorState(model, helper);
}
}
......@@ -78,7 +78,9 @@ export interface LanguageConfiguration {
*
* @deprecated Will be replaced by a better API soon.
*/
__electricCharacterSupport?: IBracketElectricCharacterContribution;
__electricCharacterSupport?: {
docComment?: IDocComment;
};
}
/**
......@@ -155,10 +157,6 @@ export interface OnEnterRule {
action: EnterAction;
}
export interface IBracketElectricCharacterContribution {
docComment?: IDocComment;
}
/**
* Definition of documentation comments (e.g. Javadoc/JSdoc)
*/
......
......@@ -12,7 +12,7 @@ import { Range } from 'vs/editor/common/core/range';
import { ITextModel } from 'vs/editor/common/model';
import { DEFAULT_WORD_REGEXP, ensureValidWordDefinition } from 'vs/editor/common/model/wordHelper';
import { LanguageId, LanguageIdentifier } from 'vs/editor/common/modes';
import { EnterAction, FoldingRules, IAutoClosingPair, IAutoClosingPairConditional, IndentAction, IndentationRule, LanguageConfiguration } from 'vs/editor/common/modes/languageConfiguration';
import { EnterAction, FoldingRules, IAutoClosingPair, IndentAction, IndentationRule, LanguageConfiguration, StandardAutoClosingPairConditional } from 'vs/editor/common/modes/languageConfiguration';
import { createScopedLineTokens } from 'vs/editor/common/modes/supports';
import { CharacterPairSupport } from 'vs/editor/common/modes/supports/characterPair';
import { BracketElectricCharacterSupport, IElectricAction } from 'vs/editor/common/modes/supports/electricCharacter';
......@@ -97,16 +97,7 @@ export class RichEditSupport {
public get electricCharacter(): BracketElectricCharacterSupport | null {
if (!this._electricCharacter) {
let autoClosingPairs: IAutoClosingPairConditional[] = [];
if (this._conf.autoClosingPairs) {
autoClosingPairs = this._conf.autoClosingPairs;
} else if (this._conf.brackets) {
autoClosingPairs = this._conf.brackets.map(b => {
return { open: b[0], close: b[1] };
});
}
this._electricCharacter = new BracketElectricCharacterSupport(this.brackets, autoClosingPairs, this._conf.__electricCharacterSupport);
this._electricCharacter = new BracketElectricCharacterSupport(this.brackets);
}
return this._electricCharacter;
}
......@@ -261,7 +252,7 @@ export class LanguageConfigurationRegistryImpl {
return value.characterPair || null;
}
public getAutoClosingPairs(languageId: LanguageId): IAutoClosingPair[] {
public getAutoClosingPairs(languageId: LanguageId): StandardAutoClosingPairConditional[] {
let characterPairSupport = this._getCharacterPairSupport(languageId);
if (!characterPairSupport) {
return [];
......@@ -285,13 +276,9 @@ export class LanguageConfigurationRegistryImpl {
return characterPairSupport.getSurroundingPairs();
}
public shouldAutoClosePair(character: string, context: LineTokens, column: number): boolean {
let scopedLineTokens = createScopedLineTokens(context, column - 1);
let characterPairSupport = this._getCharacterPairSupport(scopedLineTokens.languageId);
if (!characterPairSupport) {
return false;
}
return characterPairSupport.shouldAutoClosePair(character, scopedLineTokens, column - scopedLineTokens.firstCharOffset);
public shouldAutoClosePair(autoClosingPair: StandardAutoClosingPairConditional, context: LineTokens, column: number): boolean {
const scopedLineTokens = createScopedLineTokens(context, column - 1);
return CharacterPairSupport.shouldAutoClosePair(autoClosingPair, scopedLineTokens, column - scopedLineTokens.firstCharOffset);
}
// end characterPair
......
......@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CharacterPair, IAutoClosingPair, IAutoClosingPairConditional, StandardAutoClosingPairConditional } from 'vs/editor/common/modes/languageConfiguration';
import { IAutoClosingPair, StandardAutoClosingPairConditional, LanguageConfiguration } from 'vs/editor/common/modes/languageConfiguration';
import { ScopedLineTokens } from 'vs/editor/common/modes/supports';
export class CharacterPairSupport {
......@@ -15,7 +15,7 @@ export class CharacterPairSupport {
private readonly _surroundingPairs: IAutoClosingPair[];
private readonly _autoCloseBefore: string;
constructor(config: { brackets?: CharacterPair[]; autoClosingPairs?: IAutoClosingPairConditional[], surroundingPairs?: IAutoClosingPair[], autoCloseBefore?: string }) {
constructor(config: LanguageConfiguration) {
if (config.autoClosingPairs) {
this._autoClosingPairs = config.autoClosingPairs.map(el => new StandardAutoClosingPairConditional(el));
} else if (config.brackets) {
......@@ -24,12 +24,18 @@ export class CharacterPairSupport {
this._autoClosingPairs = [];
}
if (config.__electricCharacterSupport && config.__electricCharacterSupport.docComment) {
const docComment = config.__electricCharacterSupport.docComment;
// IDocComment is legacy, only partially supported
this._autoClosingPairs.push(new StandardAutoClosingPairConditional({ open: docComment.open, close: docComment.close || '' }));
}
this._autoCloseBefore = typeof config.autoCloseBefore === 'string' ? config.autoCloseBefore : CharacterPairSupport.DEFAULT_AUTOCLOSE_BEFORE_LANGUAGE_DEFINED;
this._surroundingPairs = config.surroundingPairs || this._autoClosingPairs;
}
public getAutoClosingPairs(): IAutoClosingPair[] {
public getAutoClosingPairs(): StandardAutoClosingPairConditional[] {
return this._autoClosingPairs;
}
......@@ -37,22 +43,15 @@ export class CharacterPairSupport {
return this._autoCloseBefore;
}
public shouldAutoClosePair(character: string, context: ScopedLineTokens, column: number): boolean {
public static shouldAutoClosePair(autoClosingPair: StandardAutoClosingPairConditional, context: ScopedLineTokens, column: number): boolean {
// Always complete on empty line
if (context.getTokenCount() === 0) {
return true;
}
let tokenIndex = context.findTokenIndexAtOffset(column - 2);
let standardTokenType = context.getStandardTokenType(tokenIndex);
for (const autoClosingPair of this._autoClosingPairs) {
if (autoClosingPair.open === character) {
return autoClosingPair.isOK(standardTokenType);
}
}
return false;
const tokenIndex = context.findTokenIndexAtOffset(column - 2);
const standardTokenType = context.getStandardTokenType(tokenIndex);
return autoClosingPair.isOK(standardTokenType);
}
public getSurroundingPairs(): IAutoClosingPair[] {
......
......@@ -3,7 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IAutoClosingPairConditional, IBracketElectricCharacterContribution, StandardAutoClosingPairConditional } from 'vs/editor/common/modes/languageConfiguration';
import { ScopedLineTokens, ignoreBracketsInToken } from 'vs/editor/common/modes/supports';
import { BracketsUtils, RichEditBrackets } from 'vs/editor/common/modes/supports/richEditBrackets';
......@@ -12,29 +11,17 @@ import { BracketsUtils, RichEditBrackets } from 'vs/editor/common/modes/supports
* @internal
*/
export interface IElectricAction {
// Only one of the following properties should be defined:
// The line will be indented at the same level of the line
// which contains the matching given bracket type.
matchOpenBracket?: string;
// The text will be appended after the electric character.
appendText?: string;
matchOpenBracket: string;
}
export class BracketElectricCharacterSupport {
private readonly _richEditBrackets: RichEditBrackets | null;
private readonly _complexAutoClosePairs: StandardAutoClosingPairConditional[];
constructor(richEditBrackets: RichEditBrackets | null, autoClosePairs: IAutoClosingPairConditional[], contribution: IBracketElectricCharacterContribution | null | undefined) {
contribution = contribution || {};
constructor(richEditBrackets: RichEditBrackets | null) {
this._richEditBrackets = richEditBrackets;
this._complexAutoClosePairs = autoClosePairs.filter(pair => pair.open.length > 1 && !!pair.close).map(el => new StandardAutoClosingPairConditional(el));
if (contribution.docComment) {
// IDocComment is legacy, only partially supported
this._complexAutoClosePairs.push(new StandardAutoClosingPairConditional({ open: contribution.docComment.open, close: contribution.docComment.close || '' }));
}
}
public getElectricCharacters(): string[] {
......@@ -48,11 +35,6 @@ export class BracketElectricCharacterSupport {
}
}
// auto close
for (let pair of this._complexAutoClosePairs) {
result.push(pair.open.charAt(pair.open.length - 1));
}
// Filter duplicate entries
result = result.filter((item, pos, array) => {
return array.indexOf(item) === pos;
......@@ -62,12 +44,6 @@ export class BracketElectricCharacterSupport {
}
public onElectricCharacter(character: string, context: ScopedLineTokens, column: number): IElectricAction | null {
return (this._onElectricAutoClose(character, context, column) ||
this._onElectricAutoIndent(character, context, column));
}
private _onElectricAutoIndent(character: string, context: ScopedLineTokens, column: number): IElectricAction | null {
if (!this._richEditBrackets || this._richEditBrackets.brackets.length === 0) {
return null;
}
......@@ -103,44 +79,4 @@ export class BracketElectricCharacterSupport {
matchOpenBracket: bracketText
};
}
private _onElectricAutoClose(character: string, context: ScopedLineTokens, column: number): IElectricAction | null {
if (!this._complexAutoClosePairs.length) {
return null;
}
let line = context.getLineContent();
for (let i = 0, len = this._complexAutoClosePairs.length; i < len; i++) {
let pair = this._complexAutoClosePairs[i];
// See if the right electric character was pressed
if (character !== pair.open.charAt(pair.open.length - 1)) {
continue;
}
// check if the full open bracket matches
let start = column - pair.open.length + 1;
let actual = line.substring(start - 1, column - 1) + character;
if (actual !== pair.open) {
continue;
}
let lastTokenIndex = context.findTokenIndexAtOffset(column - 1);
let lastTokenStandardType = context.getStandardTokenType(lastTokenIndex);
// If we're in a scope listed in 'notIn', do nothing
if (!pair.isOK(lastTokenStandardType)) {
continue;
}
// If this line already contains the closing tag, do nothing.
if (line.indexOf(pair.close, column - 1) >= 0) {
continue;
}
return { appendText: pair.close };
}
return null;
}
}
......@@ -3998,8 +3998,12 @@ suite('autoClosingPairs', () => {
{ open: '\'', close: '\'', notIn: ['string', 'comment'] },
{ open: '\"', close: '\"', notIn: ['string'] },
{ open: '`', close: '`', notIn: ['string', 'comment'] },
{ open: '/**', close: ' */', notIn: ['string'] }
{ open: '/**', close: ' */', notIn: ['string'] },
{ open: 'begin', close: 'end', notIn: ['string'] }
],
__electricCharacterSupport: {
docComment: { open: '/**', close: ' */' }
}
}));
}
......@@ -4439,6 +4443,28 @@ suite('autoClosingPairs', () => {
mode.dispose();
});
test('multi-character autoclose', () => {
let mode = new AutoClosingMode();
usingCursor({
text: [
'',
],
languageIdentifier: mode.getLanguageIdentifier()
}, (model, cursor) => {
model.setValue('begi');
cursor.setSelections('test', [new Selection(1, 5, 1, 5)]);
cursorCommand(cursor, H.Type, { text: 'n' }, 'keyboard');
assert.strictEqual(model.getLineContent(1), 'beginend');
model.setValue('/*');
cursor.setSelections('test', [new Selection(1, 3, 1, 3)]);
cursorCommand(cursor, H.Type, { text: '*' }, 'keyboard');
assert.strictEqual(model.getLineContent(1), '/** */');
});
mode.dispose();
});
test('issue #55314: Do not auto-close when ending with open', () => {
const languageId = new LanguageIdentifier('myElectricMode', 5);
class ElectricMode extends MockMode {
......@@ -4477,7 +4503,7 @@ suite('autoClosingPairs', () => {
model.forceTokenization(model.getLineCount());
assertType(model, cursor, 3, 4, '"', '"', `does not double quote when ending with open`);
model.forceTokenization(model.getLineCount());
assertType(model, cursor, 4, 2, '"', '""', `double quote when ending with open`);
assertType(model, cursor, 4, 2, '"', '"', `does not double quote when ending with open`);
model.forceTokenization(model.getLineCount());
assertType(model, cursor, 4, 3, '"', '"', `does not double quote when ending with open`);
});
......@@ -4772,31 +4798,18 @@ suite('autoClosingPairs', () => {
// on the mac US intl kb layout
// Typing ` + space
// Typing ' + space
cursorCommand(cursor, H.CompositionStart, null, 'keyboard');
cursorCommand(cursor, H.Type, { text: '\'' }, 'keyboard');
cursorCommand(cursor, H.ReplacePreviousChar, { replaceCharCnt: 1, text: '\'' }, 'keyboard');
cursorCommand(cursor, H.CompositionEnd, null, 'keyboard');
assert.equal(model.getValue(), '\'\'');
// Typing " + space within string
cursor.setSelections('test', [new Selection(1, 2, 1, 2)]);
cursorCommand(cursor, H.CompositionStart, null, 'keyboard');
cursorCommand(cursor, H.Type, { text: '"' }, 'keyboard');
cursorCommand(cursor, H.ReplacePreviousChar, { replaceCharCnt: 1, text: '"' }, 'keyboard');
cursorCommand(cursor, H.CompositionEnd, null, 'keyboard');
assert.equal(model.getValue(), '\'"\'');
// Typing ' + space after '
model.setValue('\'');
cursor.setSelections('test', [new Selection(1, 2, 1, 2)]);
// Typing one more ' + space
cursorCommand(cursor, H.CompositionStart, null, 'keyboard');
cursorCommand(cursor, H.Type, { text: '\'' }, 'keyboard');
cursorCommand(cursor, H.ReplacePreviousChar, { replaceCharCnt: 1, text: '\'' }, 'keyboard');
cursorCommand(cursor, H.CompositionEnd, null, 'keyboard');
assert.equal(model.getValue(), '\'\'');
// Typing ' as a closing tag
......
......@@ -7,6 +7,7 @@ import * as assert from 'assert';
import { StandardTokenType } from 'vs/editor/common/modes';
import { CharacterPairSupport } from 'vs/editor/common/modes/supports/characterPair';
import { TokenText, createFakeScopedLineTokens } from 'vs/editor/test/common/modesTestUtils';
import { StandardAutoClosingPairConditional } from 'vs/editor/common/modes/languageConfiguration';
suite('CharacterPairSupport', () => {
......@@ -52,8 +53,21 @@ suite('CharacterPairSupport', () => {
assert.deepEqual(characaterPairSupport.getSurroundingPairs(), []);
});
function findAutoClosingPair(characterPairSupport: CharacterPairSupport, character: string): StandardAutoClosingPairConditional | null {
for (const autoClosingPair of characterPairSupport.getAutoClosingPairs()) {
if (autoClosingPair.open === character) {
return autoClosingPair;
}
}
return null;
}
function testShouldAutoClose(characterPairSupport: CharacterPairSupport, line: TokenText[], character: string, column: number): boolean {
return characterPairSupport.shouldAutoClosePair(character, createFakeScopedLineTokens(line), column);
const autoClosingPair = findAutoClosingPair(characterPairSupport, character);
if (!autoClosingPair) {
return false;
}
return CharacterPairSupport.shouldAutoClosePair(autoClosingPair, createFakeScopedLineTokens(line), column);
}
test('shouldAutoClosePair in empty line', () => {
......
......@@ -21,86 +21,20 @@ suite('Editor Modes - Auto Indentation', () => {
assert.deepEqual(actual, null);
}
function testAppends(electricCharacterSupport: BracketElectricCharacterSupport, line: TokenText[], character: string, offset: number, appendText: string): void {
let actual = _testOnElectricCharacter(electricCharacterSupport, line, character, offset);
assert.deepEqual(actual, { appendText: appendText });
}
function testMatchBracket(electricCharacterSupport: BracketElectricCharacterSupport, line: TokenText[], character: string, offset: number, matchOpenBracket: string): void {
let actual = _testOnElectricCharacter(electricCharacterSupport, line, character, offset);
assert.deepEqual(actual, { matchOpenBracket: matchOpenBracket });
}
test('Doc comments', () => {
let brackets = new BracketElectricCharacterSupport(null, [{ open: '/**', close: ' */' }], null);
testAppends(brackets, [
{ text: '/*', type: StandardTokenType.Other },
], '*', 3, ' */');
testDoesNothing(brackets, [
{ text: '/*', type: StandardTokenType.Other },
{ text: ' ', type: StandardTokenType.Other },
{ text: '*/', type: StandardTokenType.Other },
], '*', 3);
});
test('getElectricCharacters uses all sources and dedups', () => {
let sup = new BracketElectricCharacterSupport(
new RichEditBrackets(fakeLanguageIdentifier, [
['{', '}'],
['(', ')']
]), [
{ open: '{', close: '}', notIn: ['string', 'comment'] },
{ open: '"', close: '"', notIn: ['string', 'comment'] },
{ open: 'begin', close: 'end', notIn: ['string'] }
],
{ docComment: { open: '/**', close: ' */' } }
);
assert.deepEqual(sup.getElectricCharacters(), ['}', ')', 'n', '*']);
});
test('auto-close', () => {
let sup = new BracketElectricCharacterSupport(
new RichEditBrackets(fakeLanguageIdentifier, [
['{', '}'],
['(', ')']
]), [
{ open: '{', close: '}', notIn: ['string', 'comment'] },
{ open: '"', close: '"', notIn: ['string', 'comment'] },
{ open: 'begin', close: 'end', notIn: ['string'] }
],
{ docComment: { open: '/**', close: ' */' } }
])
);
testDoesNothing(sup, [], 'a', 0);
testDoesNothing(sup, [{ text: 'egi', type: StandardTokenType.Other }], 'b', 1);
testDoesNothing(sup, [{ text: 'bgi', type: StandardTokenType.Other }], 'e', 2);
testDoesNothing(sup, [{ text: 'bei', type: StandardTokenType.Other }], 'g', 3);
testDoesNothing(sup, [{ text: 'beg', type: StandardTokenType.Other }], 'i', 4);
testDoesNothing(sup, [{ text: 'egin', type: StandardTokenType.Other }], 'b', 1);
testDoesNothing(sup, [{ text: 'bgin', type: StandardTokenType.Other }], 'e', 2);
testDoesNothing(sup, [{ text: 'bein', type: StandardTokenType.Other }], 'g', 3);
testDoesNothing(sup, [{ text: 'begn', type: StandardTokenType.Other }], 'i', 4);
testAppends(sup, [{ text: 'begi', type: StandardTokenType.Other }], 'n', 5, 'end');
testDoesNothing(sup, [{ text: '3gin', type: StandardTokenType.Other }], 'b', 1);
testDoesNothing(sup, [{ text: 'bgin', type: StandardTokenType.Other }], '3', 2);
testDoesNothing(sup, [{ text: 'b3in', type: StandardTokenType.Other }], 'g', 3);
testDoesNothing(sup, [{ text: 'b3gn', type: StandardTokenType.Other }], 'i', 4);
testDoesNothing(sup, [{ text: 'b3gi', type: StandardTokenType.Other }], 'n', 5);
testDoesNothing(sup, [{ text: 'begi', type: StandardTokenType.String }], 'n', 5);
testAppends(sup, [{ text: '"', type: StandardTokenType.String }, { text: 'begi', type: StandardTokenType.Other }], 'n', 6, 'end');
testDoesNothing(sup, [{ text: '"', type: StandardTokenType.String }, { text: 'begi', type: StandardTokenType.String }], 'n', 6);
testAppends(sup, [{ text: '/*', type: StandardTokenType.String }], '*', 3, ' */');
testDoesNothing(sup, [{ text: 'begi', type: StandardTokenType.Other }, { text: 'end', type: StandardTokenType.Other }], 'n', 5);
assert.deepEqual(sup.getElectricCharacters(), ['}', ')']);
});
test('matchOpenBracket', () => {
......@@ -108,12 +42,7 @@ suite('Editor Modes - Auto Indentation', () => {
new RichEditBrackets(fakeLanguageIdentifier, [
['{', '}'],
['(', ')']
]), [
{ open: '{', close: '}', notIn: ['string', 'comment'] },
{ open: '"', close: '"', notIn: ['string', 'comment'] },
{ open: 'begin', close: 'end', notIn: ['string'] }
],
{ docComment: { open: '/**', close: ' */' } }
])
);
testDoesNothing(sup, [{ text: '\t{', type: StandardTokenType.Other }], '\t', 1);
......
......@@ -4561,7 +4561,9 @@ declare namespace monaco.languages {
*
* @deprecated Will be replaced by a better API soon.
*/
__electricCharacterSupport?: IBracketElectricCharacterContribution;
__electricCharacterSupport?: {
docComment?: IDocComment;
};
}
/**
......@@ -4636,10 +4638,6 @@ declare namespace monaco.languages {
action: EnterAction;
}
export interface IBracketElectricCharacterContribution {
docComment?: IDocComment;
}
/**
* Definition of documentation comments (e.g. Javadoc/JSdoc)
*/
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册