From 19a881b72fbdd0b34cb769bc978d3fbadd6a40de Mon Sep 17 00:00:00 2001 From: Alex Dima Date: Sat, 25 Mar 2017 10:06:35 +0100 Subject: [PATCH] Fixes #20891: All cursors should do the same thing when typing --- src/vs/editor/common/controller/cursor.ts | 6 +- .../common/controller/cursorTypeOperations.ts | 209 ++++++++++-------- .../test/common/controller/cursor.test.ts | 22 ++ 3 files changed, 149 insertions(+), 88 deletions(-) diff --git a/src/vs/editor/common/controller/cursor.ts b/src/vs/editor/common/controller/cursor.ts index f43f25098ad..80710728d6d 100644 --- a/src/vs/editor/common/controller/cursor.ts +++ b/src/vs/editor/common/controller/cursor.ts @@ -1399,7 +1399,11 @@ export class Cursor extends EventEmitter { // Here we must interpret each typed character individually, that's why we create a new context ctx.hasExecutedCommands = this._createAndInterpretHandlerCtx(ctx.eventSource, ctx.eventData, (charHandlerCtx: IMultipleCursorOperationContext) => { - this._applyEditForAll(charHandlerCtx, (cursor) => TypeOperations.typeWithInterceptors(cursor.config, cursor.model, cursor.modelState, chr)); + // Decide what all cursors will do up-front + const cursors = this.cursors.getAll(); + const states = cursors.map(cursor => cursor.modelState); + const editOperations = TypeOperations.typeWithInterceptors(cursors[0].config, cursors[0].model, states, chr); + this._applyEditForAll(charHandlerCtx, (cursor, cursorIndex) => editOperations[cursorIndex]); // The last typed character gets to win ctx.cursorPositionChangeReason = charHandlerCtx.cursorPositionChangeReason; diff --git a/src/vs/editor/common/controller/cursorTypeOperations.ts b/src/vs/editor/common/controller/cursorTypeOperations.ts index b3dc27c0705..e270c7c2ae8 100644 --- a/src/vs/editor/common/controller/cursorTypeOperations.ts +++ b/src/vs/editor/common/controller/cursorTypeOperations.ts @@ -273,37 +273,32 @@ export class TypeOperations { }); } - private static _typeInterceptorEnter(config: CursorConfiguration, model: ITokenizedModel, cursor: SingleCursorState, ch: string): EditOperationResult { - if (ch !== '\n') { - return null; - } - - return TypeOperations._enter(config, model, false, cursor.selection); - } - - private static isAutoClosingCloseCharType(config: CursorConfiguration, model: ITokenizedModel, cursor: SingleCursorState, ch: string): boolean { - if (!config.autoClosingBrackets) { + private static _isAutoClosingCloseCharType(config: CursorConfiguration, model: ITokenizedModel, cursors: SingleCursorState[], ch: string): boolean { + if (!config.autoClosingBrackets || !config.autoClosingPairsClose.hasOwnProperty(ch)) { return false; } - const selection = cursor.selection; - const position = cursor.position; + for (let i = 0, len = cursors.length; i < len; i++) { + const cursor = cursors[i]; + const selection = cursor.selection; - if (!selection.isEmpty() || !config.autoClosingPairsClose.hasOwnProperty(ch)) { - return false; - } + if (!selection.isEmpty()) { + return false; + } - const lineText = model.getLineContent(position.lineNumber); - const afterCharacter = lineText.charAt(position.column - 1); + const position = cursor.position; + const lineText = model.getLineContent(position.lineNumber); + const afterCharacter = lineText.charAt(position.column - 1); - if (afterCharacter !== ch) { - return false; + if (afterCharacter !== ch) { + return false; + } } return true; } - private static runAutoClosingCloseCharType(config: CursorConfiguration, model: ITokenizedModel, cursor: SingleCursorState, ch: string): EditOperationResult { + private static _runAutoClosingCloseCharType(config: CursorConfiguration, model: ITokenizedModel, cursor: SingleCursorState, ch: string): EditOperationResult { const position = cursor.position; const typeSelection = new Range(position.lineNumber, position.column, position.lineNumber, position.column + 1); return new EditOperationResult(new ReplaceCommand(typeSelection, ch), { @@ -312,55 +307,61 @@ export class TypeOperations { }); } - private static isAutoClosingOpenCharType(config: CursorConfiguration, model: ITokenizedModel, cursor: SingleCursorState, ch: string): boolean { - if (!config.autoClosingBrackets) { - return false; - } - - const selection = cursor.selection; - const position = cursor.position; - - if (!selection.isEmpty() || !config.autoClosingPairsOpen.hasOwnProperty(ch)) { + private static _isAutoClosingOpenCharType(config: CursorConfiguration, model: ITokenizedModel, cursors: SingleCursorState[], ch: string): boolean { + if (!config.autoClosingBrackets || !config.autoClosingPairsOpen.hasOwnProperty(ch)) { return false; } - const lineText = model.getLineContent(position.lineNumber); - const afterCharacter = lineText.charAt(position.column - 1); - - // Only consider auto closing the pair if a space follows or if another autoclosed pair follows - if (afterCharacter) { - const thisBraceIsSymmetric = (config.autoClosingPairsOpen[ch] === ch); + for (let i = 0, len = cursors.length; i < len; i++) { + const cursor = cursors[i]; + const selection = cursor.selection; + if (!selection.isEmpty()) { + return false; + } - let isBeforeCloseBrace = false; - for (let otherCloseBrace in config.autoClosingPairsClose) { - const otherBraceIsSymmetric = (config.autoClosingPairsOpen[otherCloseBrace] === otherCloseBrace); - if (!thisBraceIsSymmetric && otherBraceIsSymmetric) { - continue; + const position = cursor.position; + const lineText = model.getLineContent(position.lineNumber); + const afterCharacter = lineText.charAt(position.column - 1); + + // Only consider auto closing the pair if a space follows or if another autoclosed pair follows + if (afterCharacter) { + const thisBraceIsSymmetric = (config.autoClosingPairsOpen[ch] === ch); + + let isBeforeCloseBrace = false; + for (let otherCloseBrace in config.autoClosingPairsClose) { + const otherBraceIsSymmetric = (config.autoClosingPairsOpen[otherCloseBrace] === otherCloseBrace); + if (!thisBraceIsSymmetric && otherBraceIsSymmetric) { + continue; + } + if (afterCharacter === otherCloseBrace) { + isBeforeCloseBrace = true; + break; + } } - if (afterCharacter === otherCloseBrace) { - isBeforeCloseBrace = true; - break; + if (!isBeforeCloseBrace && !/\s/.test(afterCharacter)) { + return false; } } - if (!isBeforeCloseBrace && !/\s/.test(afterCharacter)) { - return false; - } - } - model.forceTokenization(position.lineNumber); - const lineTokens = model.getLineTokens(position.lineNumber); + model.forceTokenization(position.lineNumber); + const lineTokens = model.getLineTokens(position.lineNumber); - let shouldAutoClosePair = false; - try { - shouldAutoClosePair = LanguageConfigurationRegistry.shouldAutoClosePair(ch, lineTokens, position.column); - } catch (e) { - onUnexpectedError(e); + let shouldAutoClosePair = false; + try { + shouldAutoClosePair = LanguageConfigurationRegistry.shouldAutoClosePair(ch, lineTokens, position.column); + } catch (e) { + onUnexpectedError(e); + } + + if (!shouldAutoClosePair) { + return false; + } } - return shouldAutoClosePair; + return true; } - private static runAutoClosingOpenCharType(config: CursorConfiguration, model: ITokenizedModel, cursor: SingleCursorState, ch: string): EditOperationResult { + private static _runAutoClosingOpenCharType(config: CursorConfiguration, model: ITokenizedModel, cursor: SingleCursorState, ch: string): EditOperationResult { const selection = cursor.selection; const closeCharacter = config.autoClosingPairsOpen[ch]; return new EditOperationResult(new ReplaceCommandWithOffsetCursorState(selection, ch + closeCharacter, 0, -closeCharacter.length), { @@ -369,35 +370,42 @@ export class TypeOperations { }); } - private static isSurroundSelectionType(config: CursorConfiguration, model: ITokenizedModel, cursor: SingleCursorState, ch: string): boolean { - if (!config.autoClosingBrackets) { + private static _isSurroundSelectionType(config: CursorConfiguration, model: ITokenizedModel, cursors: SingleCursorState[], ch: string): boolean { + if (!config.autoClosingBrackets || !config.surroundingPairs.hasOwnProperty(ch)) { return false; } - const selection = cursor.selection; + for (let i = 0, len = cursors.length; i < len; i++) { + const cursor = cursors[i]; + const selection = cursor.selection; - if (selection.isEmpty() || !config.surroundingPairs.hasOwnProperty(ch)) { - return false; - } + if (selection.isEmpty()) { + return false; + } + + let selectionContainsOnlyWhitespace = true; - let selectionContainsOnlyWhitespace = true; - - for (let lineNumber = selection.startLineNumber; lineNumber <= selection.endLineNumber; lineNumber++) { - const lineText = model.getLineContent(lineNumber); - const startIndex = (lineNumber === selection.startLineNumber ? selection.startColumn - 1 : 0); - const endIndex = (lineNumber === selection.endLineNumber ? selection.endColumn - 1 : lineText.length); - const selectedText = lineText.substring(startIndex, endIndex); - if (/[^ \t]/.test(selectedText)) { - // this selected text contains something other than whitespace - selectionContainsOnlyWhitespace = false; - break; + for (let lineNumber = selection.startLineNumber; lineNumber <= selection.endLineNumber; lineNumber++) { + const lineText = model.getLineContent(lineNumber); + const startIndex = (lineNumber === selection.startLineNumber ? selection.startColumn - 1 : 0); + const endIndex = (lineNumber === selection.endLineNumber ? selection.endColumn - 1 : lineText.length); + const selectedText = lineText.substring(startIndex, endIndex); + if (/[^ \t]/.test(selectedText)) { + // this selected text contains something other than whitespace + selectionContainsOnlyWhitespace = false; + break; + } + } + + if (selectionContainsOnlyWhitespace) { + return false; } } - return (!selectionContainsOnlyWhitespace); + return true; } - private static runSurroundSelectionType(config: CursorConfiguration, model: ITokenizedModel, cursor: SingleCursorState, ch: string): EditOperationResult { + private static _runSurroundSelectionType(config: CursorConfiguration, model: ITokenizedModel, cursor: SingleCursorState, ch: string): EditOperationResult { const selection = cursor.selection; const closeCharacter = config.surroundingPairs[ch]; @@ -468,24 +476,51 @@ export class TypeOperations { return null; } - public static typeWithInterceptors(config: CursorConfiguration, model: ITokenizedModel, cursor: SingleCursorState, ch: string): EditOperationResult { - let r: EditOperationResult = null; + public static typeWithInterceptors(config: CursorConfiguration, model: ITokenizedModel, cursors: SingleCursorState[], ch: string): EditOperationResult[] { + let r2: EditOperationResult[] = []; - r = r || this._typeInterceptorEnter(config, model, cursor, ch); + if (ch === '\n') { + for (let i = 0, len = cursors.length; i < len; i++) { + r2[i] = TypeOperations._enter(config, model, false, cursors[i].selection); + } + return r2; + } + + if (this._isAutoClosingCloseCharType(config, model, cursors, ch)) { + for (let i = 0, len = cursors.length; i < len; i++) { + r2[i] = this._runAutoClosingCloseCharType(config, model, cursors[i], ch); + } + return r2; + } - if (!r && this.isAutoClosingCloseCharType(config, model, cursor, ch)) { - r = this.runAutoClosingCloseCharType(config, model, cursor, ch); + if (this._isAutoClosingOpenCharType(config, model, cursors, ch)) { + for (let i = 0, len = cursors.length; i < len; i++) { + r2[i] = this._runAutoClosingOpenCharType(config, model, cursors[i], ch); + } + return r2; } - if (!r && this.isAutoClosingOpenCharType(config, model, cursor, ch)) { - r = this.runAutoClosingOpenCharType(config, model, cursor, ch); + + if (this._isSurroundSelectionType(config, model, cursors, ch)) { + for (let i = 0, len = cursors.length; i < len; i++) { + r2[i] = this._runSurroundSelectionType(config, model, cursors[i], ch); + } + return r2; } - if (!r && this.isSurroundSelectionType(config, model, cursor, ch)) { - r = this.runSurroundSelectionType(config, model, cursor, ch); + + // Electric characters make sense only when dealing with a single cursor, + // as multiple cursors typing brackets for example would interfer with bracket matching + if (cursors.length === 1) { + const r = this._typeInterceptorElectricChar(config, model, cursors[0], ch); + if (r) { + r2[0] = r; + return r2; + } } - r = r || this._typeInterceptorElectricChar(config, model, cursor, ch); - r = r || this.typeWithoutInterceptors(config, model, cursor, ch); - return r; + for (let i = 0, len = cursors.length; i < len; i++) { + r2[i] = this.typeWithoutInterceptors(config, model, cursors[i], ch); + } + return r2; } public static typeWithoutInterceptors(config: CursorConfiguration, model: ITokenizedModel, cursor: SingleCursorState, str: string): EditOperationResult { diff --git a/src/vs/editor/test/common/controller/cursor.test.ts b/src/vs/editor/test/common/controller/cursor.test.ts index a05727bef9e..3a1dcf0e6a0 100644 --- a/src/vs/editor/test/common/controller/cursor.test.ts +++ b/src/vs/editor/test/common/controller/cursor.test.ts @@ -3627,4 +3627,26 @@ suite('autoClosingPairs', () => { }); mode.dispose(); }); + + test('issue #20891: All cursors should do the same thing', () => { + let mode = new AutoClosingMode(); + usingCursor({ + text: [ + 'var a = asd' + ], + languageIdentifier: mode.getLanguageIdentifier() + }, (model, cursor) => { + + cursor.setSelections('test', [ + new Selection(1, 9, 1, 9), + new Selection(1, 12, 1, 12), + ]); + + // type a ` + cursorCommand(cursor, H.Type, { text: '`' }, 'keyboard'); + + assert.equal(model.getValue(), 'var a = `asd`'); + }); + mode.dispose(); + }); }); -- GitLab