diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index 91e6ea3864b278649e64d699fdb33df3a5ed4748..a5e31344d3c2d0013ddc7f6cb230b6f902467e53 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -13,7 +13,7 @@ import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import * as editorCommon from 'vs/editor/common/editorCommon'; -import { IIdentifiedSingleEditOperation, IModelDecoration, IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; +import { IIdentifiedSingleEditOperation, IModelDecoration, IModelDeltaDecoration, ITextModel, ICursorStateComputer } from 'vs/editor/common/model'; import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent } from 'vs/editor/common/model/textModelEvents'; import { OverviewRulerZone } from 'vs/editor/common/view/overviewZoneManager'; import { IEditorWhitespace } from 'vs/editor/common/viewLayout/whitespaceComputer'; @@ -612,7 +612,7 @@ export interface ICodeEditor extends editorCommon.IEditor { * @param edits The edits to execute. * @param endCursorState Cursor state after the edits were applied. */ - executeEdits(source: string, edits: IIdentifiedSingleEditOperation[], endCursorState?: Selection[]): boolean; + executeEdits(source: string, edits: IIdentifiedSingleEditOperation[], endCursorState?: ICursorStateComputer | Selection[]): boolean; /** * Execute multiple (concomitant) commands on the editor. diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index e6a4fd0597778e22fff695fb5b1f3121bcce8ace..0ac879ee78a5979f4a8997a1d06a1b5262b1b1b4 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -33,7 +33,7 @@ import { ISelection, Selection } from 'vs/editor/common/core/selection'; import { InternalEditorAction } from 'vs/editor/common/editorAction'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { EndOfLinePreference, IIdentifiedSingleEditOperation, IModelDecoration, IModelDecorationOptions, IModelDecorationsChangeAccessor, IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; +import { EndOfLinePreference, IIdentifiedSingleEditOperation, IModelDecoration, IModelDecorationOptions, IModelDecorationsChangeAccessor, IModelDeltaDecoration, ITextModel, ICursorStateComputer } from 'vs/editor/common/model'; import { ClassName } from 'vs/editor/common/model/intervalTree'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelOptionsChangedEvent } from 'vs/editor/common/model/textModelEvents'; @@ -980,7 +980,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE return true; } - public executeEdits(source: string, edits: IIdentifiedSingleEditOperation[], endCursorState?: Selection[]): boolean { + public executeEdits(source: string, edits: IIdentifiedSingleEditOperation[], endCursorState?: ICursorStateComputer | Selection[]): boolean { if (!this._modelData) { return false; } @@ -989,14 +989,16 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE return false; } - this._modelData.model.pushEditOperations(this._modelData.cursor.getSelections(), edits, () => { - return endCursorState ? endCursorState : null; - }); - - if (endCursorState) { - this._modelData.cursor.setSelections(source, endCursorState); + let cursorStateComputer: ICursorStateComputer; + if (!endCursorState) { + cursorStateComputer = () => null; + } else if (Array.isArray(endCursorState)) { + cursorStateComputer = () => endCursorState; + } else { + cursorStateComputer = endCursorState; } + this._modelData.cursor.executeEdits(source, edits, cursorStateComputer); return true; } diff --git a/src/vs/editor/common/controller/cursor.ts b/src/vs/editor/common/controller/cursor.ts index 789d3a97b021c343774c16a0e2c7b3319374af62..fefca3cd9b55f80ba45eda55f67320a355fca9c8 100644 --- a/src/vs/editor/common/controller/cursor.ts +++ b/src/vs/editor/common/controller/cursor.ts @@ -15,7 +15,7 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { ISelection, Selection, SelectionDirection } from 'vs/editor/common/core/selection'; import * as editorCommon from 'vs/editor/common/editorCommon'; -import { IIdentifiedSingleEditOperation, ITextModel, TrackedRangeStickiness, IModelDeltaDecoration } from 'vs/editor/common/model'; +import { IIdentifiedSingleEditOperation, ITextModel, TrackedRangeStickiness, IModelDeltaDecoration, ICursorStateComputer } from 'vs/editor/common/model'; import { RawContentChangedType } from 'vs/editor/common/model/textModelEvents'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { IViewModel } from 'vs/editor/common/viewModel/viewModel'; @@ -429,6 +429,31 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { // ------ auxiliary handling logic + private _pushAutoClosedAction(autoClosedCharactersRanges: Range[], autoClosedEnclosingRanges: Range[]): void { + let autoClosedCharactersDeltaDecorations: IModelDeltaDecoration[] = []; + let autoClosedEnclosingDeltaDecorations: IModelDeltaDecoration[] = []; + + for (let i = 0, len = autoClosedCharactersRanges.length; i < len; i++) { + autoClosedCharactersDeltaDecorations.push({ + range: autoClosedCharactersRanges[i], + options: { + inlineClassName: 'auto-closed-character', + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges + } + }); + autoClosedEnclosingDeltaDecorations.push({ + range: autoClosedEnclosingRanges[i], + options: { + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges + } + }); + } + + const autoClosedCharactersDecorations = this._model.deltaDecorations([], autoClosedCharactersDeltaDecorations); + const autoClosedEnclosingDecorations = this._model.deltaDecorations([], autoClosedEnclosingDeltaDecorations); + this._autoClosedActions.push(new AutoClosedAction(this._model, autoClosedCharactersDecorations, autoClosedEnclosingDecorations)); + } + private _executeEditOperation(opResult: EditOperationResult | null): void { if (!opResult) { @@ -446,32 +471,19 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { this._interpretCommandResult(result); // Check for auto-closing closed characters - let autoClosedCharactersRanges: IModelDeltaDecoration[] = []; - let autoClosedEnclosingRanges: IModelDeltaDecoration[] = []; + let autoClosedCharactersRanges: Range[] = []; + let autoClosedEnclosingRanges: Range[] = []; for (let i = 0; i < opResult.commands.length; i++) { const command = opResult.commands[i]; if (command instanceof TypeWithAutoClosingCommand && command.enclosingRange && command.closeCharacterRange) { - autoClosedCharactersRanges.push({ - range: command.closeCharacterRange, - options: { - inlineClassName: 'auto-closed-character', - stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges - } - }); - autoClosedEnclosingRanges.push({ - range: command.enclosingRange, - options: { - stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges - } - }); + autoClosedCharactersRanges.push(command.closeCharacterRange); + autoClosedEnclosingRanges.push(command.enclosingRange); } } if (autoClosedCharactersRanges.length > 0) { - const autoClosedCharactersDecorations = this._model.deltaDecorations([], autoClosedCharactersRanges); - const autoClosedEnclosingDecorations = this._model.deltaDecorations([], autoClosedEnclosingRanges); - this._autoClosedActions.push(new AutoClosedAction(this._model, autoClosedCharactersDecorations, autoClosedEnclosingDecorations)); + this._pushAutoClosedAction(autoClosedCharactersRanges, autoClosedEnclosingRanges); } this._prevEditOperationType = opResult.type; @@ -563,6 +575,75 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { // ----------------------------------------------------------------------------------------------------------- // ----- handlers beyond this point + private _findAutoClosingPairs(edits: IIdentifiedSingleEditOperation[]): [number, number][] | null { + if (!edits.length) { + return null; + } + + let indices: [number, number][] = []; + for (let i = 0, len = edits.length; i < len; i++) { + const edit = edits[i]; + if (!edit.text || edit.text.indexOf('\n') >= 0) { + return null; + } + + const m = edit.text.match(/([)\]}>'"`])([^)\]}>'"`]*)$/); + if (!m) { + return null; + } + const closeChar = m[1]; + + const openChar = this.context.config.autoClosingPairsClose[closeChar]; + if (!openChar) { + return null; + } + + const closeCharIndex = edit.text.length - m[2].length - 1; + const openCharIndex = edit.text.lastIndexOf(openChar, closeCharIndex - 1); + if (openCharIndex === -1) { + return null; + } + + indices.push([openCharIndex, closeCharIndex]); + } + + return indices; + } + + public executeEdits(source: string, edits: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer): void { + let autoClosingIndices: [number, number][] | null = null; + if (source === 'snippet') { + autoClosingIndices = this._findAutoClosingPairs(edits); + } + + if (autoClosingIndices) { + edits[0]._isTracked = true; + } + let autoClosedCharactersRanges: Range[] = []; + let autoClosedEnclosingRanges: Range[] = []; + const selections = this._model.pushEditOperations(this.getSelections(), edits, (undoEdits) => { + if (autoClosingIndices) { + for (let i = 0, len = autoClosingIndices.length; i < len; i++) { + const [openCharInnerIndex, closeCharInnerIndex] = autoClosingIndices[i]; + const undoEdit = undoEdits[i]; + const lineNumber = undoEdit.range.startLineNumber; + const openCharIndex = undoEdit.range.startColumn - 1 + openCharInnerIndex; + const closeCharIndex = undoEdit.range.startColumn - 1 + closeCharInnerIndex; + + autoClosedCharactersRanges.push(new Range(lineNumber, closeCharIndex + 1, lineNumber, closeCharIndex + 2)); + autoClosedEnclosingRanges.push(new Range(lineNumber, openCharIndex + 1, lineNumber, closeCharIndex + 2)); + } + } + return cursorStateComputer(undoEdits); + }); + if (selections) { + this.setSelections(source, selections); + } + if (autoClosedCharactersRanges.length > 0) { + this._pushAutoClosedAction(autoClosedCharactersRanges, autoClosedEnclosingRanges); + } + } + public trigger(source: string, handlerId: string, payload: any): void { const H = editorCommon.Handler; diff --git a/src/vs/editor/contrib/snippet/snippetSession.ts b/src/vs/editor/contrib/snippet/snippetSession.ts index 689b5567ed17b5f0a380683eeee00f07b88176bd..63aa8a230eb92d04a7e2a1de241dec6886146c0c 100644 --- a/src/vs/editor/contrib/snippet/snippetSession.ts +++ b/src/vs/editor/contrib/snippet/snippetSession.ts @@ -489,21 +489,18 @@ export class SnippetSession { return; } - const model = this._editor.getModel(); - // make insert edit and start with first selections const { edits, snippets } = SnippetSession.createEditsAndSnippets(this._editor, this._template, this._options.overwriteBefore, this._options.overwriteAfter, false, this._options.adjustWhitespace, this._options.clipboardText); this._snippets = snippets; - const selections = model.pushEditOperations(this._editor.getSelections(), edits, undoEdits => { + this._editor.executeEdits('snippet', edits, undoEdits => { if (this._snippets[0].hasPlaceholder) { return this._move(true); } else { return undoEdits.map(edit => Selection.fromPositions(edit.range.getEndPosition())); } - })!; - this._editor.setSelections(selections); - this._editor.revealRange(selections[0]); + }); + this._editor.revealRange(this._editor.getSelections()[0]); } merge(template: string, options: ISnippetSessionInsertOptions = _defaultOptions): void { @@ -513,8 +510,7 @@ export class SnippetSession { this._templateMerges.push([this._snippets[0]._nestingLevel, this._snippets[0]._placeholderGroupsIdx, template]); const { edits, snippets } = SnippetSession.createEditsAndSnippets(this._editor, template, options.overwriteBefore, options.overwriteAfter, true, options.adjustWhitespace, options.clipboardText); - this._editor.setSelections(this._editor.getModel().pushEditOperations(this._editor.getSelections(), edits, undoEdits => { - + this._editor.executeEdits('snippet', edits, undoEdits => { for (const snippet of this._snippets) { snippet.merge(snippets); } @@ -525,7 +521,7 @@ export class SnippetSession { } else { return undoEdits.map(edit => Selection.fromPositions(edit.range.getEndPosition())); } - })!); + }); } next(): void { diff --git a/src/vs/editor/test/browser/controller/cursor.test.ts b/src/vs/editor/test/browser/controller/cursor.test.ts index a42bd6d06c303c84c8b56e72efe1534d09b31fe8..e925406c1b4d7ca1295aabb50ee7344e6d4bbaa7 100644 --- a/src/vs/editor/test/browser/controller/cursor.test.ts +++ b/src/vs/editor/test/browser/controller/cursor.test.ts @@ -4692,6 +4692,28 @@ suite('autoClosingPairs', () => { mode.dispose(); }); + test('issue #78975 - Parentheses swallowing does not work when parentheses are inserted by autocomplete', () => { + let mode = new AutoClosingMode(); + usingCursor({ + text: [ + '
{ + cursor.setSelections('test', [new Selection(1, 8, 1, 8)]); + + cursor.executeEdits('snippet', [{ range: new Range(1, 6, 1, 8), text: 'id=""' }], () => [new Selection(1, 10, 1, 10)]); + assert.strictEqual(model.getLineContent(1), '
{ let mode = new AutoClosingMode(); usingCursor({ diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index b44b3ae08ad3bc675c7c8b97e96f101fa0019b31..222b9e84666407870bbcd04e4a96b54400efe811 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -4054,7 +4054,7 @@ declare namespace monaco.editor { * @param edits The edits to execute. * @param endCursorState Cursor state after the edits were applied. */ - executeEdits(source: string, edits: IIdentifiedSingleEditOperation[], endCursorState?: Selection[]): boolean; + executeEdits(source: string, edits: IIdentifiedSingleEditOperation[], endCursorState?: ICursorStateComputer | Selection[]): boolean; /** * Execute multiple (concomitant) commands on the editor. * @param source The source of the call.