From a4850d52db4d9837a3a98d7cbfd3b6063cf41888 Mon Sep 17 00:00:00 2001 From: n-gist Date: Wed, 26 Aug 2020 22:15:09 +0600 Subject: [PATCH] Implementation TM_SELECTED_TEXT snippets variable for working with overtyped text --- src/vs/editor/common/model.ts | 6 ++++ src/vs/editor/common/model/editStack.ts | 18 +++++++++++ src/vs/editor/common/model/textModel.ts | 31 +++++++++++++++++-- .../editor/contrib/snippet/snippetSession.ts | 3 +- .../contrib/snippet/snippetVariables.ts | 20 +++++++++--- .../snippet/test/snippetVariables.test.ts | 10 +++--- 6 files changed, 76 insertions(+), 12 deletions(-) diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index d15e79eab7d..6eead87c480 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -1137,6 +1137,12 @@ export interface ITextModel { */ _applyRedo(changes: TextChange[], eol: EndOfLineSequence, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): void; + /** + * Returns the text that has just been overtyped + * @internal + */ + getOvertypedText(overtypeIdx: number, undoSearchLimit: number, editSizeLimit: number): string | undefined; + /** * Undo edit operations until the first previous stop point created by `pushStackElement`. * The inverse edit operations will be pushed on the redo stack. diff --git a/src/vs/editor/common/model/editStack.ts b/src/vs/editor/common/model/editStack.ts index 5316fce9e95..a1d3b051d4e 100644 --- a/src/vs/editor/common/model/editStack.ts +++ b/src/vs/editor/common/model/editStack.ts @@ -198,6 +198,24 @@ export class SingleModelEditStackElement implements IResourceUndoRedoElement { } } + public getOvertypedTextAtEditingEnd(overtypeIdx: number, editingEnd: number): { text: string | undefined, continuousEditing: boolean, previousEditingEnd: number } { + let continuousEditing = false; + let overtypedText: string | undefined; + const data = (this._data instanceof SingleModelEditStackData ? this._data : SingleModelEditStackData.deserialize(this._data)); + const change = data.changes[overtypeIdx >= data.changes.length ? 0 : overtypeIdx]; + const inserted = change.oldLength === 0; + const deleted = change.newLength === 0; + if (inserted !== deleted) { + // If change is insert, and starts with new line, break the search + if (!(inserted && /^[\r\n]/.test(change.newText))) { + continuousEditing = editingEnd < 0 ? true : change.newEnd === editingEnd; + } + } else if (editingEnd < 0 || change.newEnd === editingEnd) { + overtypedText = change.oldText; + } + return { text: overtypedText, continuousEditing: continuousEditing, previousEditingEnd: change.oldEnd }; + } + public undo(): void { if (URI.isUri(this.model)) { // don't have a model diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index cb7b34b3451..58801ac093e 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -16,7 +16,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 model from 'vs/editor/common/model'; -import { EditStack } from 'vs/editor/common/model/editStack'; +import { EditStack, SingleModelEditStackElement } from 'vs/editor/common/model/editStack'; import { guessIndentation } from 'vs/editor/common/model/indentationGuesser'; import { IntervalNode, IntervalTree, getNodeIsInOverviewRuler, recomputeMaxEnd } from 'vs/editor/common/model/intervalTree'; import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; @@ -34,7 +34,7 @@ import { VSBufferReadableStream, VSBuffer } from 'vs/base/common/buffer'; import { TokensStore, MultilineTokens, countEOL, MultilineTokens2, TokensStore2 } from 'vs/editor/common/model/tokensStore'; import { Color } from 'vs/base/common/color'; import { EditorTheme } from 'vs/editor/common/view/viewContext'; -import { IUndoRedoService, ResourceEditStackSnapshot } from 'vs/platform/undoRedo/common/undoRedo'; +import { IUndoRedoService, ResourceEditStackSnapshot, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo'; import { TextChange } from 'vs/editor/common/model/textChange'; import { Constants } from 'vs/base/common/uint'; @@ -1491,6 +1491,33 @@ export class TextModel extends Disposable implements model.ITextModel { return (result.reverseEdits === null ? undefined : result.reverseEdits); } + public getOvertypedText(overtypeIdx: number, undoSearchLimit: number, editSizeLimit: number): string | undefined { + const elements = this._undoRedoService.getElements(this.uri).past; + if (elements.length === 0) { + return; + } + + // Cycle through undo elements to find one containing text that was overtyped + let editingEnd = -1; + let elementIndex = elements.length; + do { + const element = elements[--elementIndex]; + if (!(element.type === UndoRedoElementType.Resource && element instanceof SingleModelEditStackElement)) { + return; + } + const searchResult = element.getOvertypedTextAtEditingEnd(overtypeIdx, editingEnd); + if (searchResult.text) { + return searchResult.text; + } + if (!searchResult.continuousEditing || (editingEnd >= 0 && Math.abs(editingEnd - searchResult.previousEditingEnd) > editSizeLimit)) { + return; + } + editingEnd = searchResult.previousEditingEnd; + } while (elementIndex > 0 && --undoSearchLimit > 0); + + return; + } + public undo(): void { this._undoRedoService.undo(this.uri); } diff --git a/src/vs/editor/contrib/snippet/snippetSession.ts b/src/vs/editor/contrib/snippet/snippetSession.ts index 717b223cb4a..465b5401496 100644 --- a/src/vs/editor/contrib/snippet/snippetSession.ts +++ b/src/vs/editor/contrib/snippet/snippetSession.ts @@ -415,6 +415,7 @@ export class SnippetSession { .map((selection, idx) => ({ selection, idx })) .sort((a, b) => Range.compareRangesUsingStarts(a.selection, b.selection)); + let overtypedIdx = 0; // Makes overtyping snippets working with multiselections for (const { selection, idx } of indexedSelections) { // extend selection with the `overwriteBefore` and `overwriteAfter` and then @@ -449,7 +450,7 @@ export class SnippetSession { snippet.resolveVariables(new CompositeSnippetVariableResolver([ modelBasedVariableResolver, new ClipboardBasedVariableResolver(readClipboardText, idx, indexedSelections.length, editor.getOption(EditorOption.multiCursorPaste) === 'spread'), - new SelectionBasedVariableResolver(model, selection), + new SelectionBasedVariableResolver(model, selection, overtypedIdx++), new CommentBasedVariableResolver(model, selection), new TimeBasedVariableResolver, new WorkspaceBasedVariableResolver(workspaceService), diff --git a/src/vs/editor/contrib/snippet/snippetVariables.ts b/src/vs/editor/contrib/snippet/snippetVariables.ts index 3253898bdee..a788ca3b47f 100644 --- a/src/vs/editor/contrib/snippet/snippetVariables.ts +++ b/src/vs/editor/contrib/snippet/snippetVariables.ts @@ -71,7 +71,8 @@ export class SelectionBasedVariableResolver implements VariableResolver { constructor( private readonly _model: ITextModel, - private readonly _selection: Selection + private readonly _selection: Selection, + private readonly _overtypeIdx: number ) { // } @@ -82,14 +83,25 @@ export class SelectionBasedVariableResolver implements VariableResolver { if (name === 'SELECTION' || name === 'TM_SELECTED_TEXT') { let value = this._model.getValueInRange(this._selection) || undefined; - if (value && this._selection.startLineNumber !== this._selection.endLineNumber && variable.snippet) { + + // If there is no selected text, try to get overtyped text + let overtyped = false; + if (!value) { + const maxTypos = 10; // Allows the user to make 10 typos (delete+insert) when typing the snippet prefix + const maxEditSize = 50; // Limits the searching by edit size less than 50 symbols + value = this._model.getOvertypedText(this._overtypeIdx, 1 + 2 * maxTypos, maxEditSize); + if (value) { + overtyped = true; + } + } + + if (value && (overtyped || (this._selection.startLineNumber !== this._selection.endLineNumber)) && variable.snippet) { // Selection is a multiline string which we indentation we now // need to adjust. We compare the indentation of this variable // with the indentation at the editor position and add potential // extra indentation to the value - const line = this._model.getLineContent(this._selection.startLineNumber); - const lineLeadingWhitespace = getLeadingWhitespace(line, 0, this._selection.startColumn - 1); + const lineLeadingWhitespace = overtyped ? '' : getLeadingWhitespace(this._model.getLineContent(this._selection.startLineNumber), 0, this._selection.startColumn - 1); let varLeadingWhitespace = lineLeadingWhitespace; variable.snippet.walk(marker => { diff --git a/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts b/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts index a7c03dca8e0..3ac1d023c21 100644 --- a/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts +++ b/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts @@ -34,7 +34,7 @@ suite('Snippet Variables Resolver', function () { resolver = new CompositeSnippetVariableResolver([ new ModelBasedVariableResolver(labelService, model), - new SelectionBasedVariableResolver(model, new Selection(1, 1, 1, 1)), + new SelectionBasedVariableResolver(model, new Selection(1, 1, 1, 1), 0), ]); }); @@ -102,24 +102,24 @@ suite('Snippet Variables Resolver', function () { test('editor variables, selection', function () { - resolver = new SelectionBasedVariableResolver(model, new Selection(1, 2, 2, 3)); + resolver = new SelectionBasedVariableResolver(model, new Selection(1, 2, 2, 3), 0); assertVariableResolve(resolver, 'TM_SELECTED_TEXT', 'his is line one\nth'); assertVariableResolve(resolver, 'TM_CURRENT_LINE', 'this is line two'); assertVariableResolve(resolver, 'TM_LINE_INDEX', '1'); assertVariableResolve(resolver, 'TM_LINE_NUMBER', '2'); - resolver = new SelectionBasedVariableResolver(model, new Selection(2, 3, 1, 2)); + resolver = new SelectionBasedVariableResolver(model, new Selection(2, 3, 1, 2), 0); assertVariableResolve(resolver, 'TM_SELECTED_TEXT', 'his is line one\nth'); assertVariableResolve(resolver, 'TM_CURRENT_LINE', 'this is line one'); assertVariableResolve(resolver, 'TM_LINE_INDEX', '0'); assertVariableResolve(resolver, 'TM_LINE_NUMBER', '1'); - resolver = new SelectionBasedVariableResolver(model, new Selection(1, 2, 1, 2)); + resolver = new SelectionBasedVariableResolver(model, new Selection(1, 2, 1, 2), 0); assertVariableResolve(resolver, 'TM_SELECTED_TEXT', undefined); assertVariableResolve(resolver, 'TM_CURRENT_WORD', 'this'); - resolver = new SelectionBasedVariableResolver(model, new Selection(3, 1, 3, 1)); + resolver = new SelectionBasedVariableResolver(model, new Selection(3, 1, 3, 1), 0); assertVariableResolve(resolver, 'TM_CURRENT_WORD', undefined); });