Merge pull request #110094 from olivercoad/conflicting-autoclose-pairs

Handle conflicting multi-char auto closing pairs
This commit is contained in:
Alexandru Dima 2020-11-13 22:25:31 +01:00 committed by GitHub
commit 7dde16206f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 136 additions and 56 deletions

View file

@ -531,7 +531,7 @@ export class Cursor extends Disposable {
}
const closeChar = m[1];
const autoClosingPairsCandidates = this.context.cursorConfig.autoClosingPairsClose2.get(closeChar);
const autoClosingPairsCandidates = this.context.cursorConfig.autoClosingPairs.autoClosingPairsCloseSingleChar.get(closeChar);
if (!autoClosingPairsCandidates || autoClosingPairsCandidates.length !== 1) {
return null;
}

View file

@ -14,7 +14,7 @@ import { ICommand, IConfiguration } from 'vs/editor/common/editorCommon';
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, StandardAutoClosingPairConditional } from 'vs/editor/common/modes/languageConfiguration';
import { AutoClosingPairs, IAutoClosingPair } from 'vs/editor/common/modes/languageConfiguration';
import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
import { ICoordinatesConverter } from 'vs/editor/common/viewModel/viewModel';
import { Constants } from 'vs/base/common/uint';
@ -75,8 +75,7 @@ export class CursorConfiguration {
public readonly autoClosingOvertype: EditorAutoClosingOvertypeStrategy;
public readonly autoSurround: EditorAutoSurroundStrategy;
public readonly autoIndent: EditorAutoIndentStrategy;
public readonly autoClosingPairsOpen2: Map<string, StandardAutoClosingPairConditional[]>;
public readonly autoClosingPairsClose2: Map<string, StandardAutoClosingPairConditional[]>;
public readonly autoClosingPairs: AutoClosingPairs;
public readonly surroundingPairs: CharacterMap;
public readonly shouldAutoCloseBefore: { quote: (ch: string) => boolean, bracket: (ch: string) => boolean };
@ -136,9 +135,7 @@ export class CursorConfiguration {
bracket: CursorConfiguration._getShouldAutoClose(languageIdentifier, this.autoClosingBrackets)
};
const autoClosingPairs = LanguageConfigurationRegistry.getAutoClosingPairs(languageIdentifier.id);
this.autoClosingPairsOpen2 = autoClosingPairs.autoClosingPairsOpen;
this.autoClosingPairsClose2 = autoClosingPairs.autoClosingPairsClose;
this.autoClosingPairs = LanguageConfigurationRegistry.getAutoClosingPairs(languageIdentifier.id);
let surroundingPairs = CursorConfiguration._getSurroundingPairs(languageIdentifier);
if (surroundingPairs) {

View file

@ -122,7 +122,7 @@ export class DeleteOperations {
public static deleteLeft(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ICursorSimpleModel, selections: Selection[]): [boolean, Array<ICommand | null>] {
if (this.isAutoClosingPairDelete(config.autoClosingBrackets, config.autoClosingQuotes, config.autoClosingPairsOpen2, model, selections)) {
if (this.isAutoClosingPairDelete(config.autoClosingBrackets, config.autoClosingQuotes, config.autoClosingPairs.autoClosingPairsOpenByEnd, model, selections)) {
return this._runAutoClosingPairDelete(config, model, selections);
}

View file

@ -439,7 +439,7 @@ export class TypeOperations {
return false;
}
if (!config.autoClosingPairsClose2.has(ch)) {
if (!config.autoClosingPairs.autoClosingPairsCloseSingleChar.has(ch)) {
return false;
}
@ -498,31 +498,20 @@ export class TypeOperations {
});
}
private static _autoClosingPairIsSymmetric(autoClosingPair: StandardAutoClosingPairConditional): boolean {
const { open, close } = autoClosingPair;
return (open.indexOf(close) >= 0 || close.indexOf(open) >= 0);
}
private static _isBeforeClosingBrace(config: CursorConfiguration, lineAfter: string) {
// If the start of lineAfter can be interpretted as both a starting or ending brace, default to returning false
const nextChar = lineAfter.charAt(0);
const potentialStartingBraces = config.autoClosingPairs.autoClosingPairsOpenByStart.get(nextChar) || [];
const potentialClosingBraces = config.autoClosingPairs.autoClosingPairsCloseByStart.get(nextChar) || [];
private static _isBeforeClosingBrace(config: CursorConfiguration, autoClosingPair: StandardAutoClosingPairConditional, characterAfter: string) {
const otherAutoClosingPairs = config.autoClosingPairsClose2.get(characterAfter);
if (!otherAutoClosingPairs) {
return false;
}
const isBeforeStartingBrace = potentialStartingBraces.some(x => lineAfter.startsWith(x.open));
const isBeforeClosingBrace = potentialClosingBraces.some(x => lineAfter.startsWith(x.close));
const thisBraceIsSymmetric = TypeOperations._autoClosingPairIsSymmetric(autoClosingPair);
for (const otherAutoClosingPair of otherAutoClosingPairs) {
const otherBraceIsSymmetric = TypeOperations._autoClosingPairIsSymmetric(otherAutoClosingPair);
if (!thisBraceIsSymmetric && otherBraceIsSymmetric) {
continue;
}
return true;
}
return false;
return !isBeforeStartingBrace && isBeforeClosingBrace;
}
private static _findAutoClosingPairOpen(config: CursorConfiguration, model: ITextModel, positions: Position[], ch: string): StandardAutoClosingPairConditional | null {
const autoClosingPairCandidates = config.autoClosingPairsOpen2.get(ch);
const autoClosingPairCandidates = config.autoClosingPairs.autoClosingPairsOpenByEnd.get(ch);
if (!autoClosingPairCandidates) {
return null;
}
@ -548,7 +537,29 @@ export class TypeOperations {
return autoClosingPair;
}
private static _isAutoClosingOpenCharType(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, insertOpenCharacter: boolean): StandardAutoClosingPairConditional | null {
private static _findSubAutoClosingPairClose(config: CursorConfiguration, autoClosingPair: StandardAutoClosingPairConditional): string {
if (autoClosingPair.open.length <= 1) {
return '';
}
const lastChar = autoClosingPair.close.charAt(autoClosingPair.close.length - 1);
// get candidates with the same last character as close
const subPairCandidates = config.autoClosingPairs.autoClosingPairsCloseByEnd.get(lastChar) || [];
let subPairMatch: StandardAutoClosingPairConditional | null = null;
for (const x of subPairCandidates) {
if (x.open !== autoClosingPair.open && autoClosingPair.open.includes(x.open) && autoClosingPair.close.endsWith(x.close)) {
if (!subPairMatch || x.open.length > subPairMatch.open.length) {
subPairMatch = x;
}
}
}
if (subPairMatch) {
return subPairMatch.close;
} else {
return '';
}
}
private static _getAutoClosingPairClose(config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, insertOpenCharacter: boolean): string | null {
const chIsQuote = isQuote(ch);
const autoCloseConfig = chIsQuote ? config.autoClosingQuotes : config.autoClosingBrackets;
if (autoCloseConfig === 'never') {
@ -560,6 +571,9 @@ export class TypeOperations {
return null;
}
const subAutoClosingPairClose = this._findSubAutoClosingPairClose(config, autoClosingPair);
let isSubAutoClosingPairPresent = true;
const shouldAutoCloseBefore = chIsQuote ? config.shouldAutoCloseBefore.quote : config.shouldAutoCloseBefore.bracket;
for (let i = 0, len = selections.length; i < len; i++) {
@ -570,11 +584,16 @@ export class TypeOperations {
const position = selection.getPosition();
const lineText = model.getLineContent(position.lineNumber);
const lineAfter = lineText.substring(position.column - 1);
// Only consider auto closing the pair if a space follows or if another autoclosed pair follows
if (!lineAfter.startsWith(subAutoClosingPairClose)) {
isSubAutoClosingPairPresent = false;
}
// Only consider auto closing the pair if an allowed character follows or if another autoclosed pair closing brace follows
if (lineText.length > position.column - 1) {
const characterAfter = lineText.charAt(position.column - 1);
const isBeforeCloseBrace = TypeOperations._isBeforeClosingBrace(config, autoClosingPair, characterAfter);
const isBeforeCloseBrace = TypeOperations._isBeforeClosingBrace(config, lineAfter);
if (!isBeforeCloseBrace && !shouldAutoCloseBefore(characterAfter)) {
return null;
@ -612,14 +631,18 @@ export class TypeOperations {
}
}
return autoClosingPair;
if (isSubAutoClosingPairPresent) {
return autoClosingPair.close.substring(0, autoClosingPair.close.length - subAutoClosingPairClose.length);
} else {
return autoClosingPair.close;
}
}
private static _runAutoClosingOpenCharType(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, insertOpenCharacter: boolean, autoClosingPair: StandardAutoClosingPairConditional): EditOperationResult {
private static _runAutoClosingOpenCharType(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], ch: string, insertOpenCharacter: boolean, autoClosingPairClose: string): EditOperationResult {
let commands: ICommand[] = [];
for (let i = 0, len = selections.length; i < len; i++) {
const selection = selections[i];
commands[i] = new TypeWithAutoClosingCommand(selection, ch, insertOpenCharacter, autoClosingPair.close);
commands[i] = new TypeWithAutoClosingCommand(selection, ch, insertOpenCharacter, autoClosingPairClose);
}
return new EditOperationResult(EditOperationType.Typing, commands, {
shouldPushStackElementBefore: true,
@ -794,9 +817,9 @@ export class TypeOperations {
});
}
const autoClosingPairOpenCharType = this._isAutoClosingOpenCharType(config, model, selections, ch, false);
if (autoClosingPairOpenCharType) {
return this._runAutoClosingOpenCharType(prevEditOperationType, config, model, selections, ch, false, autoClosingPairOpenCharType);
const autoClosingPairClose = this._getAutoClosingPairClose(config, model, selections, ch, false);
if (autoClosingPairClose !== null) {
return this._runAutoClosingOpenCharType(prevEditOperationType, config, model, selections, ch, false, autoClosingPairClose);
}
return null;
@ -838,9 +861,9 @@ export class TypeOperations {
}
if (!isDoingComposition) {
const autoClosingPairOpenCharType = this._isAutoClosingOpenCharType(config, model, selections, ch, true);
if (autoClosingPairOpenCharType) {
return this._runAutoClosingOpenCharType(prevEditOperationType, config, model, selections, ch, true, autoClosingPairOpenCharType);
const autoClosingPairClose = this._getAutoClosingPairClose(config, model, selections, ch, true);
if (autoClosingPairClose) {
return this._runAutoClosingOpenCharType(prevEditOperationType, config, model, selections, ch, true, autoClosingPairClose);
}
}

View file

@ -384,7 +384,7 @@ export class WordOperations {
return selection;
}
if (DeleteOperations.isAutoClosingPairDelete(ctx.autoClosingBrackets, ctx.autoClosingQuotes, ctx.autoClosingPairs.autoClosingPairsOpen, ctx.model, [ctx.selection])) {
if (DeleteOperations.isAutoClosingPairDelete(ctx.autoClosingBrackets, ctx.autoClosingQuotes, ctx.autoClosingPairs.autoClosingPairsOpenByEnd, ctx.model, [ctx.selection])) {
const position = ctx.selection.getPosition();
return new Range(position.lineNumber, position.column - 1, position.lineNumber, position.column + 1);
}

View file

@ -294,17 +294,32 @@ export class StandardAutoClosingPairConditional {
* @internal
*/
export class AutoClosingPairs {
// it is useful to be able to get pairs using either end of open and close
public readonly autoClosingPairsOpen: Map<string, StandardAutoClosingPairConditional[]>;
public readonly autoClosingPairsClose: Map<string, StandardAutoClosingPairConditional[]>;
/** Key is first character of open */
public readonly autoClosingPairsOpenByStart: Map<string, StandardAutoClosingPairConditional[]>;
/** Key is last character of open */
public readonly autoClosingPairsOpenByEnd: Map<string, StandardAutoClosingPairConditional[]>;
/** Key is first character of close */
public readonly autoClosingPairsCloseByStart: Map<string, StandardAutoClosingPairConditional[]>;
/** Key is last character of close */
public readonly autoClosingPairsCloseByEnd: Map<string, StandardAutoClosingPairConditional[]>;
/** Key is close. Only has pairs that are a single character */
public readonly autoClosingPairsCloseSingleChar: Map<string, StandardAutoClosingPairConditional[]>;
constructor(autoClosingPairs: StandardAutoClosingPairConditional[]) {
this.autoClosingPairsOpen = new Map<string, StandardAutoClosingPairConditional[]>();
this.autoClosingPairsClose = new Map<string, StandardAutoClosingPairConditional[]>();
this.autoClosingPairsOpenByStart = new Map<string, StandardAutoClosingPairConditional[]>();
this.autoClosingPairsOpenByEnd = new Map<string, StandardAutoClosingPairConditional[]>();
this.autoClosingPairsCloseByStart = new Map<string, StandardAutoClosingPairConditional[]>();
this.autoClosingPairsCloseByEnd = new Map<string, StandardAutoClosingPairConditional[]>();
this.autoClosingPairsCloseSingleChar = new Map<string, StandardAutoClosingPairConditional[]>();
for (const pair of autoClosingPairs) {
appendEntry(this.autoClosingPairsOpen, pair.open.charAt(pair.open.length - 1), pair);
if (pair.close.length === 1) {
appendEntry(this.autoClosingPairsClose, pair.close, pair);
appendEntry(this.autoClosingPairsOpenByStart, pair.open.charAt(0), pair);
appendEntry(this.autoClosingPairsOpenByEnd, pair.open.charAt(pair.open.length - 1), pair);
appendEntry(this.autoClosingPairsCloseByStart, pair.close.charAt(0), pair);
appendEntry(this.autoClosingPairsCloseByEnd, pair.close.charAt(pair.close.length - 1), pair);
if (pair.close.length === 1 && pair.open.length === 1) {
appendEntry(this.autoClosingPairsCloseSingleChar, pair.close, pair);
}
}
}

View file

@ -4660,7 +4660,7 @@ suite('autoClosingPairs', () => {
'v|ar |c = \'|asd\';|',
'v|ar d = "|asd";|',
'v|ar e = /*3*/ 3;|',
'v|ar f = /** 3 */3;|',
'v|ar f = /** 3| */3;|',
'v|ar g = (3+5|);|',
'v|ar h = { |a: \'v|alue\' |};|',
];
@ -4841,13 +4841,13 @@ suite('autoClosingPairs', () => {
let autoClosePositions = [
'var a |=| [|]|;|',
'var b |=| |`asd`|;|',
'var c |=| |\'asd\'|;|',
'var d |=| |"asd"|;|',
'var b |=| `asd`|;|',
'var c |=| \'asd\'|;|',
'var d |=| "asd"|;|',
'var e |=| /*3*/| 3;|',
'var f |=| /**| 3 */3;|',
'var g |=| (3+5)|;|',
'var h |=| {| a:| |\'value\'| |}|;|',
'var h |=| {| a:| \'value\'| |}|;|',
];
for (let i = 0, len = autoClosePositions.length; i < len; i++) {
const lineNumber = i + 1;
@ -4890,6 +4890,51 @@ suite('autoClosingPairs', () => {
mode.dispose();
});
test('issue #72177: multi-character autoclose with conflicting patterns', () => {
const languageId = new LanguageIdentifier('autoClosingModeMultiChar', 5);
class AutoClosingModeMultiChar extends MockMode {
constructor() {
super(languageId);
this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), {
autoClosingPairs: [
{ open: '(', close: ')' },
{ open: '(*', close: '*)' },
{ open: '<@', close: '@>' },
{ open: '<@@', close: '@@>' },
],
}));
}
}
const mode = new AutoClosingModeMultiChar();
usingCursor({
text: [
'',
],
languageIdentifier: mode.getLanguageIdentifier()
}, (editor, model, viewModel) => {
viewModel.type('(', 'keyboard');
assert.strictEqual(model.getLineContent(1), '()');
viewModel.type('*', 'keyboard');
assert.strictEqual(model.getLineContent(1), '(**)', `doesn't add entire close when already closed substring is there`);
model.setValue('(');
viewModel.setSelections('test', [new Selection(1, 2, 1, 2)]);
viewModel.type('*', 'keyboard');
assert.strictEqual(model.getLineContent(1), '(**)', `does add entire close if not already there`);
model.setValue('');
viewModel.type('<@', 'keyboard');
assert.strictEqual(model.getLineContent(1), '<@@>');
viewModel.type('@', 'keyboard');
assert.strictEqual(model.getLineContent(1), '<@@@@>', `autocloses when before multi-character closing brace`);
viewModel.type('(', 'keyboard');
assert.strictEqual(model.getLineContent(1), '<@@()@@>', `autocloses when before multi-character closing brace`);
});
mode.dispose();
});
test('issue #55314: Do not auto-close when ending with open', () => {
const languageId = new LanguageIdentifier('myElectricMode', 5);
class ElectricMode extends MockMode {
@ -4943,7 +4988,7 @@ suite('autoClosingPairs', () => {
],
languageIdentifier: mode.getLanguageIdentifier()
}, (editor, model, viewModel) => {
assertType(editor, model, viewModel, 1, 12, '"', '""', `does not over type and will auto close`);
assertType(editor, model, viewModel, 1, 12, '"', '"', `does not over type and will not auto close`);
});
mode.dispose();
});
@ -5304,7 +5349,7 @@ suite('autoClosingPairs', () => {
assert.equal(model.getValue(), 'console.log(\'it\\\');');
viewModel.type('\'', 'keyboard');
assert.equal(model.getValue(), 'console.log(\'it\\\'\'\');');
assert.equal(model.getValue(), 'console.log(\'it\\\'\');');
});
mode.dispose();
});