From 6819433c055b1116f4191397697b099c0af0fdda Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 9 May 2017 11:15:21 +0200 Subject: [PATCH] better decoration management, more tests, finish at final tab stop --- .../contrib/snippet/browser/editorSnippets.ts | 81 +++++++++++------- .../test/browser/editorSnippets.test.ts | 84 +++++++++++++++++++ 2 files changed, 134 insertions(+), 31 deletions(-) diff --git a/src/vs/editor/contrib/snippet/browser/editorSnippets.ts b/src/vs/editor/contrib/snippet/browser/editorSnippets.ts index f0c26cfa1b2..e77d8defd54 100644 --- a/src/vs/editor/contrib/snippet/browser/editorSnippets.ts +++ b/src/vs/editor/contrib/snippet/browser/editorSnippets.ts @@ -6,7 +6,7 @@ 'use strict'; import { getLeadingWhitespace } from 'vs/base/common/strings'; -import { ICommonCodeEditor, IModel, TrackedRangeStickiness, IIdentifiedSingleEditOperation } from 'vs/editor/common/editorCommon'; +import { ICommonCodeEditor, IModel, IModelDecorationOptions, TrackedRangeStickiness, IIdentifiedSingleEditOperation } from 'vs/editor/common/editorCommon'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { TextmateSnippet, Placeholder, SnippetParser } from '../common/snippetParser'; import { Selection } from 'vs/editor/common/core/selection'; @@ -24,6 +24,9 @@ class OneSnippet { private _placeholderGroups: Placeholder[][]; private _placeholderGroupsIdx: number; + private static readonly _growingDecoration: IModelDecorationOptions = { stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges }; + private static readonly _fixedDecoration: IModelDecorationOptions = { stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges }; + constructor(editor: ICommonCodeEditor, snippet: TextmateSnippet, offset: number) { this._editor = editor; this._snippet = snippet; @@ -58,16 +61,7 @@ class OneSnippet { const end = model.getPositionAt(this._offset + placeholderOffset + placeholderLen); const range = new Range(start.lineNumber, start.column, end.lineNumber, end.column); - let stickiness: TrackedRangeStickiness; - if (placeholder.isFinalTabstop) { - stickiness = TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges; - } else if (lastRange && lastRange.getEndPosition().equals(range.getStartPosition())) { - stickiness = TrackedRangeStickiness.GrowsOnlyWhenTypingAfter; - } else { - stickiness = TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges; - } - - const handle = accessor.addDecoration(range, { stickiness }); + const handle = accessor.addDecoration(range, OneSnippet._fixedDecoration); this._placeholderDecorations.set(placeholder, handle); lastRange = range; @@ -91,27 +85,50 @@ class OneSnippet { this._init(); + const prevGroupsIdx = this._placeholderGroupsIdx; + if (fwd && this._placeholderGroupsIdx < this._placeholderGroups.length - 1) { this._placeholderGroupsIdx += 1; - return this._getCurrentPlaceholderSelections(); } else if (!fwd && this._placeholderGroupsIdx > 0) { this._placeholderGroupsIdx -= 1; - return this._getCurrentPlaceholderSelections(); } else { - return undefined; + return []; } + + return this._editor.getModel().changeDecorations(accessor => { + + // change stickness to never grow when typing at its edges + // so that in-active tabstops never grow + if (prevGroupsIdx !== -1) { + for (const placeholder of this._placeholderGroups[prevGroupsIdx]) { + const id = this._placeholderDecorations.get(placeholder); + accessor.changeDecorationOptions(id, OneSnippet._fixedDecoration); + } + } + + // change stickiness to always grow when typing at its edges + // because these decorations represent the currently active + // tabstop. Special case: reaching the final tab stop + const selections: Selection[] = []; + for (const placeholder of this._placeholderGroups[this._placeholderGroupsIdx]) { + const id = this._placeholderDecorations.get(placeholder); + const range = this._editor.getModel().getDecorationRange(id); + selections.push(new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn)); + + accessor.changeDecorationOptions(id, placeholder.isFinalTabstop ? OneSnippet._fixedDecoration : OneSnippet._growingDecoration); + } + return selections; + }); } - private _getCurrentPlaceholderSelections(): Selection[] { - const selections: Selection[] = []; - for (const placeholder of this._placeholderGroups[this._placeholderGroupsIdx]) { - const handle = this._placeholderDecorations.get(placeholder); - const range = this._editor.getModel().getDecorationRange(handle); - selections.push(new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn)); + get isAtFinalPlaceholder() { + if (this._placeholderGroupsIdx < 0) { + return false; + } else { + return this._placeholderGroups[this._placeholderGroupsIdx][0].isFinalTabstop; } - return selections; } } @@ -161,32 +178,34 @@ export class SnippetSession { this._editor.setSelections(newSelections); } + dispose(): void { + dispose(this._snippets); + } + next(): void { const newSelections = this._move(true); - this._editor.setSelections(newSelections); + if (newSelections.length > 0) { + this._editor.setSelections(newSelections); + } } prev(): void { const newSelections = this._move(false); - this._editor.setSelections(newSelections); + if (newSelections.length > 0) { + this._editor.setSelections(newSelections); + } } private _move(fwd: boolean): Selection[] { const selections: Selection[] = []; for (const snippet of this._snippets) { const oneSelection = snippet.move(fwd); - if (!oneSelection) { - if (fwd) { - this.stop(); - } - return this._editor.getSelections(); - } selections.push(...oneSelection); } return selections; } - stop(): void { - dispose(this._snippets); + get isAtFinalPlaceholder() { + return this._snippets[0].isAtFinalPlaceholder; } } diff --git a/src/vs/editor/contrib/snippet/test/browser/editorSnippets.test.ts b/src/vs/editor/contrib/snippet/test/browser/editorSnippets.test.ts index 9d49e4e13d3..ed0c472107a 100644 --- a/src/vs/editor/contrib/snippet/test/browser/editorSnippets.test.ts +++ b/src/vs/editor/contrib/snippet/test/browser/editorSnippets.test.ts @@ -59,6 +59,19 @@ suite('SnippetInsertion', function () { assertSelections(editor, new Selection(1, 10, 1, 10), new Selection(2, 14, 2, 14)); }); + test('snippets, repeated tabstops', function () { + const session = new SnippetSession(editor, '${1:abc}foo${1:abc}$0'); + assertSelections(editor, + new Selection(1, 1, 1, 4), new Selection(1, 7, 1, 10), + new Selection(2, 5, 2, 8), new Selection(2, 11, 2, 14), + ); + session.next(); + assertSelections(editor, + new Selection(1, 10, 1, 10), + new Selection(2, 14, 2, 14), + ); + }); + test('snippets, selections and new text with newlines', () => { const session = new SnippetSession(editor, 'foo\n\t${1:bar}\n$0'); @@ -141,5 +154,76 @@ suite('SnippetInsertion', function () { assertSelections(editor, new Selection(1, 15, 1, 15)); }); + test('snippets, don\'t merge touching tabstops 1/2', function () { + + const session = new SnippetSession(editor, '$1$2$3$0'); + assertSelections(editor, new Selection(1, 1, 1, 1), new Selection(2, 5, 2, 5)); + + session.next(); + assertSelections(editor, new Selection(1, 1, 1, 1), new Selection(2, 5, 2, 5)); + + session.next(); + assertSelections(editor, new Selection(1, 1, 1, 1), new Selection(2, 5, 2, 5)); + + session.next(); + assertSelections(editor, new Selection(1, 1, 1, 1), new Selection(2, 5, 2, 5)); + + session.prev(); + session.prev(); + session.prev(); + assertSelections(editor, new Selection(1, 1, 1, 1), new Selection(2, 5, 2, 5)); + editor.trigger('test', 'type', { text: '111' }); + + session.next(); + editor.trigger('test', 'type', { text: '222' }); + + session.next(); + editor.trigger('test', 'type', { text: '333' }); + + session.next(); + assert.equal(model.getValue(), '111222333function foo() {\n 111222333console.log(a);\n}'); + assertSelections(editor, new Selection(1, 10, 1, 10), new Selection(2, 14, 2, 14)); + + session.prev(); + assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(2, 11, 2, 11)); + session.prev(); + assertSelections(editor, new Selection(1, 4, 1, 4), new Selection(2, 8, 2, 8)); + session.prev(); + assertSelections(editor, new Selection(1, 1, 1, 4), new Selection(2, 5, 2, 8)); + }); + test('snippets, don\'t merge touching tabstops 2/2', function () { + + const session = new SnippetSession(editor, '$1$2$3$0'); + assertSelections(editor, new Selection(1, 1, 1, 1), new Selection(2, 5, 2, 5)); + + editor.trigger('test', 'type', { text: '111' }); + + session.next(); + assertSelections(editor, new Selection(1, 4, 1, 4), new Selection(2, 8, 2, 8)); + editor.trigger('test', 'type', { text: '222' }); + + session.next(); + assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(2, 11, 2, 11)); + editor.trigger('test', 'type', { text: '333' }); + + session.next(); + assert.equal(session.isAtFinalPlaceholder, true); + }); + + test('snippets, gracefully move over final tabstop', function () { + const session = new SnippetSession(editor, '${1}bar$0'); + + assert.equal(session.isAtFinalPlaceholder, false); + assertSelections(editor, new Selection(1, 1, 1, 1), new Selection(2, 5, 2, 5)); + + session.next(); + assert.equal(session.isAtFinalPlaceholder, true); + assertSelections(editor, new Selection(1, 4, 1, 4), new Selection(2, 8, 2, 8)); + + session.next(); + assert.equal(session.isAtFinalPlaceholder, true); + assertSelections(editor, new Selection(1, 4, 1, 4), new Selection(2, 8, 2, 8)); + }); + }); -- GitLab