mirror of
https://github.com/microsoft/vscode.git
synced 2026-09-30 06:58:55 +01:00
basic support for nested snippets, #24855
This commit is contained in:
@@ -13,6 +13,62 @@ import { SnippetSession } from './snippetSession';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
|
||||
class SnippetSessions {
|
||||
|
||||
private _stack: SnippetSession[] = [];
|
||||
|
||||
add(session: SnippetSession): number {
|
||||
return this._stack.push(session);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
dispose(this._stack);
|
||||
this._stack.length = 0;
|
||||
}
|
||||
|
||||
get empty(): boolean {
|
||||
return this._stack.length === 0;
|
||||
}
|
||||
|
||||
get hasPlaceholder(): boolean {
|
||||
return this._stack.some(s => s.hasPlaceholder);
|
||||
}
|
||||
|
||||
get isAtFirstPlaceholder(): boolean {
|
||||
// return !this.empty && this._stack[0].isAtFirstPlaceholder;
|
||||
return this._stack.every(s => s.isAtFirstPlaceholder);
|
||||
}
|
||||
|
||||
get isAtFinalPlaceholder(): boolean {
|
||||
// return !this.empty && this._stack[0].isAtFinalPlaceholder;
|
||||
return this._stack.every(s => s.isAtFinalPlaceholder);
|
||||
}
|
||||
|
||||
get isSelectionWithinPlaceholders(): boolean {
|
||||
return this._stack.some(s => s.isSelectionWithinPlaceholders());
|
||||
}
|
||||
|
||||
prev(): void {
|
||||
for (let i = this._stack.length - 1; i >= 0; i--) {
|
||||
const snippet = this._stack[i];
|
||||
if (!snippet.isAtFirstPlaceholder) {
|
||||
snippet.prev();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
next(): void {
|
||||
for (let i = this._stack.length - 1; i >= 0; i--) {
|
||||
const snippet = this._stack[i];
|
||||
if (!snippet.isAtFinalPlaceholder) {
|
||||
snippet.next();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@commonEditorContribution
|
||||
export class SnippetController2 {
|
||||
|
||||
@@ -28,7 +84,8 @@ export class SnippetController2 {
|
||||
private readonly _hasNextTabstop: IContextKey<boolean>;
|
||||
private readonly _hasPrevTabstop: IContextKey<boolean>;
|
||||
|
||||
private _snippet: SnippetSession;
|
||||
// private _snippet: SnippetSession;
|
||||
private _sessions = new SnippetSessions();
|
||||
private _snippetListener: IDisposable[] = [];
|
||||
|
||||
constructor(
|
||||
@@ -44,7 +101,7 @@ export class SnippetController2 {
|
||||
this._inSnippet.reset();
|
||||
this._hasPrevTabstop.reset();
|
||||
this._hasNextTabstop.reset();
|
||||
dispose(this._snippet);
|
||||
this._sessions.clear();
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
@@ -56,47 +113,49 @@ export class SnippetController2 {
|
||||
overwriteBefore: number = 0, overwriteAfter: number = 0,
|
||||
undoStopBefore: boolean = true, undoStopAfter: boolean = true
|
||||
): void {
|
||||
|
||||
// don't listen while inserting the snippet
|
||||
// as that is the inflight state causing cancelation
|
||||
this._snippetListener = dispose(this._snippetListener);
|
||||
|
||||
if (undoStopBefore) {
|
||||
this._editor.getModel().pushStackElement();
|
||||
}
|
||||
if (!this._snippet) {
|
||||
// create a new session
|
||||
this._snippet = new SnippetSession(this._editor);
|
||||
this._snippet.insert(template, overwriteBefore, overwriteAfter);
|
||||
this._snippetListener = [
|
||||
this._editor.onDidChangeModel(() => this.cancel()),
|
||||
this._editor.onDidChangeCursorSelection(() => this._updateState())
|
||||
];
|
||||
} else {
|
||||
// only insert the snippet when a session
|
||||
// is already active
|
||||
this._snippet.insert(template, overwriteBefore, overwriteAfter);
|
||||
}
|
||||
|
||||
const snippet = new SnippetSession(this._editor);
|
||||
snippet.insert(template, overwriteBefore, overwriteAfter);
|
||||
this._sessions.add(snippet);
|
||||
|
||||
if (undoStopAfter) {
|
||||
this._editor.getModel().pushStackElement();
|
||||
}
|
||||
|
||||
this._snippetListener = [
|
||||
this._editor.onDidChangeModel(() => this.cancel()),
|
||||
this._editor.onDidChangeCursorSelection(() => this._updateState())
|
||||
];
|
||||
this._updateState();
|
||||
}
|
||||
|
||||
private _updateState(): void {
|
||||
if (!this._snippet) {
|
||||
if (this._sessions.empty) {
|
||||
// canceled in the meanwhile
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._snippet.hasPlaceholder) {
|
||||
if (!this._sessions.hasPlaceholder) {
|
||||
// don't listen for selection changes and don't
|
||||
// update context keys when the snippet is plain text
|
||||
return this.cancel();
|
||||
}
|
||||
|
||||
if (this._snippet.isAtFinalPlaceholder || !this._snippet.isSelectionWithinPlaceholders()) {
|
||||
if (this._sessions.isAtFinalPlaceholder || !this._sessions.isSelectionWithinPlaceholders) {
|
||||
return this.cancel();
|
||||
}
|
||||
|
||||
this._inSnippet.set(true);
|
||||
this._hasPrevTabstop.set(!this._snippet.isAtFirstPlaceholder);
|
||||
this._hasNextTabstop.set(!this._snippet.isAtFinalPlaceholder);
|
||||
this._hasPrevTabstop.set(!this._sessions.isAtFirstPlaceholder);
|
||||
this._hasNextTabstop.set(!this._sessions.isAtFinalPlaceholder);
|
||||
}
|
||||
|
||||
finish(): void {
|
||||
@@ -106,28 +165,21 @@ export class SnippetController2 {
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
if (this._snippet) {
|
||||
this._inSnippet.reset();
|
||||
this._hasPrevTabstop.reset();
|
||||
this._hasNextTabstop.reset();
|
||||
dispose(this._snippetListener);
|
||||
dispose(this._snippet);
|
||||
this._snippet = undefined;
|
||||
}
|
||||
this._inSnippet.reset();
|
||||
this._hasPrevTabstop.reset();
|
||||
this._hasNextTabstop.reset();
|
||||
this._sessions.clear();
|
||||
dispose(this._snippetListener);
|
||||
}
|
||||
|
||||
prev(): void {
|
||||
if (this._snippet) {
|
||||
this._snippet.prev();
|
||||
this._updateState();
|
||||
}
|
||||
this._sessions.prev();
|
||||
this._updateState();
|
||||
}
|
||||
|
||||
next(): void {
|
||||
if (this._snippet) {
|
||||
this._snippet.next();
|
||||
this._updateState();
|
||||
}
|
||||
this._sessions.next();
|
||||
this._updateState();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -130,11 +130,13 @@ export class OneSnippet {
|
||||
}
|
||||
|
||||
get isAtFirstPlaceholder() {
|
||||
return this._placeholderGroupsIdx === 0;
|
||||
return this._placeholderGroupsIdx === 0 || this._placeholderGroups.length === 0;
|
||||
}
|
||||
|
||||
get isAtFinalPlaceholder() {
|
||||
if (this._placeholderGroupsIdx < 0) {
|
||||
if (this._placeholderGroups.length === 0) {
|
||||
return true;
|
||||
} else if (this._placeholderGroupsIdx < 0) {
|
||||
return false;
|
||||
} else {
|
||||
return this._placeholderGroups[this._placeholderGroupsIdx][0].isFinalTabstop;
|
||||
@@ -196,24 +198,35 @@ export class SnippetSession {
|
||||
return selection;
|
||||
}
|
||||
|
||||
static makeInsertEditsAndSnippets(editor: ICommonCodeEditor, template: string, overwriteBefore: number = 0, overwriteAfter: number = 0): { edits: IIdentifiedSingleEditOperation[], snippets: OneSnippet[] } {
|
||||
private readonly _editor: ICommonCodeEditor;
|
||||
private _snippets: OneSnippet[] = [];
|
||||
|
||||
constructor(editor: ICommonCodeEditor) {
|
||||
this._editor = editor;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
dispose(this._snippets);
|
||||
}
|
||||
|
||||
insert(template: string, overwriteBefore: number = 0, overwriteAfter: number = 0): void {
|
||||
|
||||
const model = this._editor.getModel();
|
||||
const edits: IIdentifiedSingleEditOperation[] = [];
|
||||
|
||||
let delta = 0;
|
||||
let edits: IIdentifiedSingleEditOperation[] = [];
|
||||
let snippets: OneSnippet[] = [];
|
||||
let model = editor.getModel();
|
||||
|
||||
// know what text the overwrite[Before|After] extensions
|
||||
// of the primary curser have selected because only when
|
||||
// secondary selections extend to the same text we can grow them
|
||||
let firstBeforeText = model.getValueInRange(SnippetSession.adjustSelection(model, editor.getSelection(), overwriteBefore, 0));
|
||||
let firstAfterText = model.getValueInRange(SnippetSession.adjustSelection(model, editor.getSelection(), 0, overwriteAfter));
|
||||
let firstBeforeText = model.getValueInRange(SnippetSession.adjustSelection(model, this._editor.getSelection(), overwriteBefore, 0));
|
||||
let firstAfterText = model.getValueInRange(SnippetSession.adjustSelection(model, this._editor.getSelection(), 0, overwriteAfter));
|
||||
|
||||
// sort selections by their start position but remeber
|
||||
// the original index. that allows you to create correct
|
||||
// offset-based selection logic without changing the
|
||||
// primary selection
|
||||
const indexedSelection = editor.getSelections()
|
||||
const indexedSelection = this._editor.getSelections()
|
||||
.map((selection, idx) => ({ selection, idx }))
|
||||
.sort((a, b) => Range.compareRangesUsingStarts(a.selection, b.selection));
|
||||
|
||||
@@ -248,46 +261,18 @@ export class SnippetSession {
|
||||
// that ensures the primiary cursor stays primary despite not being
|
||||
// the one with lowest start position
|
||||
edits[idx] = EditOperation.replaceMove(snippetSelection, snippet.text);
|
||||
snippets[idx] = new OneSnippet(editor, snippet, offset);
|
||||
}
|
||||
|
||||
return { edits, snippets };
|
||||
}
|
||||
|
||||
private readonly _editor: ICommonCodeEditor;
|
||||
private _snippets: OneSnippet[];
|
||||
|
||||
constructor(editor: ICommonCodeEditor) {
|
||||
this._editor = editor;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
dispose(this._snippets);
|
||||
}
|
||||
|
||||
insert(template: string, overwriteBefore: number = 0, overwriteAfter: number = 0): void {
|
||||
|
||||
const model = this._editor.getModel();
|
||||
const { edits, snippets } = SnippetSession.makeInsertEditsAndSnippets(
|
||||
this._editor, template, overwriteBefore, overwriteAfter
|
||||
);
|
||||
|
||||
let isNestedInsert = true;
|
||||
if (!this._snippets) {
|
||||
// keep snippets around
|
||||
this._snippets = snippets;
|
||||
isNestedInsert = false;
|
||||
this._snippets[idx] = new OneSnippet(this._editor, snippet, offset);
|
||||
}
|
||||
|
||||
// make insert edit and start with first selections
|
||||
const newSelections = model.pushEditOperations(this._editor.getSelections(), edits, undoEdits => {
|
||||
if (!isNestedInsert && this._snippets[0].hasPlaceholder) {
|
||||
|
||||
this._editor.setSelections(model.pushEditOperations(this._editor.getSelections(), edits, undoEdits => {
|
||||
if (this._snippets[0].hasPlaceholder) {
|
||||
return this._move(true);
|
||||
} else {
|
||||
return undoEdits.map(edit => Selection.fromPositions(edit.range.getEndPosition()));
|
||||
}
|
||||
});
|
||||
this._editor.setSelections(newSelections);
|
||||
}));
|
||||
}
|
||||
|
||||
next(): void {
|
||||
@@ -322,6 +307,11 @@ export class SnippetSession {
|
||||
}
|
||||
|
||||
isSelectionWithinPlaceholders(): boolean {
|
||||
|
||||
if (!this.hasPlaceholder) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const selections = this._editor.getSelections();
|
||||
if (selections.length < this._snippets.length) {
|
||||
// this means we started snippet mode with N
|
||||
|
||||
@@ -148,17 +148,37 @@ suite('SnippetController2', function () {
|
||||
// assertContextKeys(contextKeys, false, false, false);
|
||||
});
|
||||
|
||||
test('insert, insert nested', function () {
|
||||
test('insert, nested snippet', function () {
|
||||
const ctrl = new SnippetController2(editor, contextKeys);
|
||||
ctrl.insert('${1:foobar}$0');
|
||||
assertContextKeys(contextKeys, true, false, true);
|
||||
assertSelections(editor, new Selection(1, 1, 1, 7), new Selection(2, 5, 2, 11));
|
||||
|
||||
ctrl.insert('farboo$1$0');
|
||||
assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(2, 11, 2, 11));
|
||||
assertContextKeys(contextKeys, true, false, true);
|
||||
|
||||
ctrl.next();
|
||||
assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(2, 11, 2, 11));
|
||||
assertContextKeys(contextKeys, true, true, true);
|
||||
|
||||
ctrl.next();
|
||||
assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(2, 11, 2, 11));
|
||||
assertContextKeys(contextKeys, false, false, false);
|
||||
});
|
||||
|
||||
test('insert, nested plain text', function () {
|
||||
const ctrl = new SnippetController2(editor, contextKeys);
|
||||
ctrl.insert('${1:foobar}$0');
|
||||
assertContextKeys(contextKeys, true, false, true);
|
||||
assertSelections(editor, new Selection(1, 1, 1, 7), new Selection(2, 5, 2, 11));
|
||||
|
||||
ctrl.insert('farboo');
|
||||
assertContextKeys(contextKeys, true, false, true);
|
||||
assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(2, 11, 2, 11));
|
||||
assertContextKeys(contextKeys, true, false, true);
|
||||
|
||||
ctrl.next();
|
||||
assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(2, 11, 2, 11));
|
||||
assertContextKeys(contextKeys, false, false, false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -403,20 +403,5 @@ suite('SnippetSession', function () {
|
||||
assert.equal(model.getValue(), '@line=1function foo() {\n @line=2console.log(a);\n}');
|
||||
assertSelections(editor, new Selection(1, 8, 1, 8), new Selection(2, 12, 2, 12));
|
||||
});
|
||||
|
||||
test('snippet, insert-nested', function () {
|
||||
const session = new SnippetSession(editor);
|
||||
session.insert('foo$1foo$0');
|
||||
|
||||
assertSelections(editor, new Selection(1, 4, 1, 4), new Selection(2, 8, 2, 8));
|
||||
session.insert('bar');
|
||||
assert.ok(session.isSelectionWithinPlaceholders());
|
||||
assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(2, 11, 2, 11));
|
||||
|
||||
session.next();
|
||||
assertSelections(editor, new Selection(1, 10, 1, 10), new Selection(2, 14, 2, 14));
|
||||
assert.ok(session.isAtFinalPlaceholder);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user