Implementation TM_SELECTED_TEXT snippets variable for working with overtyped text

This commit is contained in:
n-gist
2020-08-26 22:15:09 +06:00
parent 6be16f9a16
commit a4850d52db
6 changed files with 76 additions and 12 deletions
+6
View File
@@ -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.
+18
View File
@@ -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
+29 -2
View File
@@ -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);
}
@@ -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),
@@ -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 => {
@@ -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);
});