Skip to content

Commit e6519cb

Browse files
committed
Implementation TM_SELECTED_TEXT snippets variable for working with overtyped text
1 parent ad40702 commit e6519cb

6 files changed

Lines changed: 76 additions & 12 deletions

File tree

src/vs/editor/common/model.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1137,6 +1137,12 @@ export interface ITextModel {
11371137
*/
11381138
_applyRedo(changes: TextChange[], eol: EndOfLineSequence, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): void;
11391139

1140+
/**
1141+
* Returns the text that has just been overtyped
1142+
* @internal
1143+
*/
1144+
getOvertypedText(overtypeIdx: number, undoSearchLimit: number, editSizeLimit: number): string | undefined;
1145+
11401146
/**
11411147
* Undo edit operations until the first previous stop point created by `pushStackElement`.
11421148
* The inverse edit operations will be pushed on the redo stack.

src/vs/editor/common/model/editStack.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,24 @@ export class SingleModelEditStackElement implements IResourceUndoRedoElement {
198198
}
199199
}
200200

201+
public getOvertypedTextAtEditingEnd(overtypeIdx: number, editingEnd: number): { text: string | undefined, continuousEditing: boolean, previousEditingEnd: number } {
202+
let continuousEditing = false;
203+
let overtypedText: string | undefined;
204+
const data = (this._data instanceof SingleModelEditStackData ? this._data : SingleModelEditStackData.deserialize(this._data));
205+
const change = data.changes[overtypeIdx >= data.changes.length ? 0 : overtypeIdx];
206+
const inserted = change.oldLength === 0;
207+
const deleted = change.newLength === 0;
208+
if (inserted !== deleted) {
209+
// If change is insert, and starts with new line, break the search
210+
if (!(inserted && /^[\r\n]/.test(change.newText))) {
211+
continuousEditing = editingEnd < 0 ? true : change.newEnd === editingEnd;
212+
}
213+
} else if (editingEnd < 0 || change.newEnd === editingEnd) {
214+
overtypedText = change.oldText;
215+
}
216+
return { text: overtypedText, continuousEditing: continuousEditing, previousEditingEnd: change.oldEnd };
217+
}
218+
201219
public undo(): void {
202220
if (URI.isUri(this.model)) {
203221
// don't have a model

src/vs/editor/common/model/textModel.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { IPosition, Position } from 'vs/editor/common/core/position';
1616
import { IRange, Range } from 'vs/editor/common/core/range';
1717
import { Selection } from 'vs/editor/common/core/selection';
1818
import * as model from 'vs/editor/common/model';
19-
import { EditStack } from 'vs/editor/common/model/editStack';
19+
import { EditStack, SingleModelEditStackElement } from 'vs/editor/common/model/editStack';
2020
import { guessIndentation } from 'vs/editor/common/model/indentationGuesser';
2121
import { IntervalNode, IntervalTree, getNodeIsInOverviewRuler, recomputeMaxEnd } from 'vs/editor/common/model/intervalTree';
2222
import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder';
@@ -34,7 +34,7 @@ import { VSBufferReadableStream, VSBuffer } from 'vs/base/common/buffer';
3434
import { TokensStore, MultilineTokens, countEOL, MultilineTokens2, TokensStore2 } from 'vs/editor/common/model/tokensStore';
3535
import { Color } from 'vs/base/common/color';
3636
import { EditorTheme } from 'vs/editor/common/view/viewContext';
37-
import { IUndoRedoService, ResourceEditStackSnapshot } from 'vs/platform/undoRedo/common/undoRedo';
37+
import { IUndoRedoService, ResourceEditStackSnapshot, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo';
3838
import { TextChange } from 'vs/editor/common/model/textChange';
3939
import { Constants } from 'vs/base/common/uint';
4040

@@ -1491,6 +1491,33 @@ export class TextModel extends Disposable implements model.ITextModel {
14911491
return (result.reverseEdits === null ? undefined : result.reverseEdits);
14921492
}
14931493

1494+
public getOvertypedText(overtypeIdx: number, undoSearchLimit: number, editSizeLimit: number): string | undefined {
1495+
const elements = this._undoRedoService.getElements(this.uri).past;
1496+
if (elements.length === 0) {
1497+
return;
1498+
}
1499+
1500+
// Cycle through undo elements to find one containing text that was overtyped
1501+
let editingEnd = -1;
1502+
let elementIndex = elements.length;
1503+
do {
1504+
const element = elements[--elementIndex];
1505+
if (!(element.type === UndoRedoElementType.Resource && element instanceof SingleModelEditStackElement)) {
1506+
return;
1507+
}
1508+
const searchResult = element.getOvertypedTextAtEditingEnd(overtypeIdx, editingEnd);
1509+
if (searchResult.text) {
1510+
return searchResult.text;
1511+
}
1512+
if (!searchResult.continuousEditing || (editingEnd >= 0 && Math.abs(editingEnd - searchResult.previousEditingEnd) > editSizeLimit)) {
1513+
return;
1514+
}
1515+
editingEnd = searchResult.previousEditingEnd;
1516+
} while (elementIndex > 0 && --undoSearchLimit > 0);
1517+
1518+
return;
1519+
}
1520+
14941521
public undo(): void {
14951522
this._undoRedoService.undo(this.uri);
14961523
}

src/vs/editor/contrib/snippet/snippetSession.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,7 @@ export class SnippetSession {
415415
.map((selection, idx) => ({ selection, idx }))
416416
.sort((a, b) => Range.compareRangesUsingStarts(a.selection, b.selection));
417417

418+
let overtypedIdx = 0; // Makes overtyping snippets working with multiselections
418419
for (const { selection, idx } of indexedSelections) {
419420

420421
// extend selection with the `overwriteBefore` and `overwriteAfter` and then
@@ -449,7 +450,7 @@ export class SnippetSession {
449450
snippet.resolveVariables(new CompositeSnippetVariableResolver([
450451
modelBasedVariableResolver,
451452
new ClipboardBasedVariableResolver(readClipboardText, idx, indexedSelections.length, editor.getOption(EditorOption.multiCursorPaste) === 'spread'),
452-
new SelectionBasedVariableResolver(model, selection),
453+
new SelectionBasedVariableResolver(model, selection, overtypedIdx++),
453454
new CommentBasedVariableResolver(model, selection),
454455
new TimeBasedVariableResolver,
455456
new WorkspaceBasedVariableResolver(workspaceService),

src/vs/editor/contrib/snippet/snippetVariables.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ export class SelectionBasedVariableResolver implements VariableResolver {
7171

7272
constructor(
7373
private readonly _model: ITextModel,
74-
private readonly _selection: Selection
74+
private readonly _selection: Selection,
75+
private readonly _overtypeIdx: number
7576
) {
7677
//
7778
}
@@ -82,14 +83,25 @@ export class SelectionBasedVariableResolver implements VariableResolver {
8283

8384
if (name === 'SELECTION' || name === 'TM_SELECTED_TEXT') {
8485
let value = this._model.getValueInRange(this._selection) || undefined;
85-
if (value && this._selection.startLineNumber !== this._selection.endLineNumber && variable.snippet) {
86+
87+
// If there is no selected text, try to get overtyped text
88+
let overtyped = false;
89+
if (!value) {
90+
const maxTypos = 10; // Allows the user to make 10 typos (delete+insert) when typing the snippet prefix
91+
const maxEditSize = 50; // Limits the searching by edit size less than 50 symbols
92+
value = this._model.getOvertypedText(this._overtypeIdx, 1 + 2 * maxTypos, maxEditSize);
93+
if (value) {
94+
overtyped = true;
95+
}
96+
}
97+
98+
if (value && (overtyped || (this._selection.startLineNumber !== this._selection.endLineNumber)) && variable.snippet) {
8699
// Selection is a multiline string which we indentation we now
87100
// need to adjust. We compare the indentation of this variable
88101
// with the indentation at the editor position and add potential
89102
// extra indentation to the value
90103

91-
const line = this._model.getLineContent(this._selection.startLineNumber);
92-
const lineLeadingWhitespace = getLeadingWhitespace(line, 0, this._selection.startColumn - 1);
104+
const lineLeadingWhitespace = overtyped ? '' : getLeadingWhitespace(this._model.getLineContent(this._selection.startLineNumber), 0, this._selection.startColumn - 1);
93105

94106
let varLeadingWhitespace = lineLeadingWhitespace;
95107
variable.snippet.walk(marker => {

src/vs/editor/contrib/snippet/test/snippetVariables.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ suite('Snippet Variables Resolver', function () {
3434

3535
resolver = new CompositeSnippetVariableResolver([
3636
new ModelBasedVariableResolver(labelService, model),
37-
new SelectionBasedVariableResolver(model, new Selection(1, 1, 1, 1)),
37+
new SelectionBasedVariableResolver(model, new Selection(1, 1, 1, 1), 0),
3838
]);
3939
});
4040

@@ -102,24 +102,24 @@ suite('Snippet Variables Resolver', function () {
102102

103103
test('editor variables, selection', function () {
104104

105-
resolver = new SelectionBasedVariableResolver(model, new Selection(1, 2, 2, 3));
105+
resolver = new SelectionBasedVariableResolver(model, new Selection(1, 2, 2, 3), 0);
106106
assertVariableResolve(resolver, 'TM_SELECTED_TEXT', 'his is line one\nth');
107107
assertVariableResolve(resolver, 'TM_CURRENT_LINE', 'this is line two');
108108
assertVariableResolve(resolver, 'TM_LINE_INDEX', '1');
109109
assertVariableResolve(resolver, 'TM_LINE_NUMBER', '2');
110110

111-
resolver = new SelectionBasedVariableResolver(model, new Selection(2, 3, 1, 2));
111+
resolver = new SelectionBasedVariableResolver(model, new Selection(2, 3, 1, 2), 0);
112112
assertVariableResolve(resolver, 'TM_SELECTED_TEXT', 'his is line one\nth');
113113
assertVariableResolve(resolver, 'TM_CURRENT_LINE', 'this is line one');
114114
assertVariableResolve(resolver, 'TM_LINE_INDEX', '0');
115115
assertVariableResolve(resolver, 'TM_LINE_NUMBER', '1');
116116

117-
resolver = new SelectionBasedVariableResolver(model, new Selection(1, 2, 1, 2));
117+
resolver = new SelectionBasedVariableResolver(model, new Selection(1, 2, 1, 2), 0);
118118
assertVariableResolve(resolver, 'TM_SELECTED_TEXT', undefined);
119119

120120
assertVariableResolve(resolver, 'TM_CURRENT_WORD', 'this');
121121

122-
resolver = new SelectionBasedVariableResolver(model, new Selection(3, 1, 3, 1));
122+
resolver = new SelectionBasedVariableResolver(model, new Selection(3, 1, 3, 1), 0);
123123
assertVariableResolve(resolver, 'TM_CURRENT_WORD', undefined);
124124

125125
});

0 commit comments

Comments
 (0)