提交 52cc722d 编写于 作者: J Johannes Rieken

basic support for nested snippets, #24855

上级 4ad0fd04
...@@ -13,6 +13,62 @@ import { SnippetSession } from './snippetSession'; ...@@ -13,6 +13,62 @@ import { SnippetSession } from './snippetSession';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; 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 @commonEditorContribution
export class SnippetController2 { export class SnippetController2 {
...@@ -28,7 +84,8 @@ export class SnippetController2 { ...@@ -28,7 +84,8 @@ export class SnippetController2 {
private readonly _hasNextTabstop: IContextKey<boolean>; private readonly _hasNextTabstop: IContextKey<boolean>;
private readonly _hasPrevTabstop: IContextKey<boolean>; private readonly _hasPrevTabstop: IContextKey<boolean>;
private _snippet: SnippetSession; // private _snippet: SnippetSession;
private _sessions = new SnippetSessions();
private _snippetListener: IDisposable[] = []; private _snippetListener: IDisposable[] = [];
constructor( constructor(
...@@ -44,7 +101,7 @@ export class SnippetController2 { ...@@ -44,7 +101,7 @@ export class SnippetController2 {
this._inSnippet.reset(); this._inSnippet.reset();
this._hasPrevTabstop.reset(); this._hasPrevTabstop.reset();
this._hasNextTabstop.reset(); this._hasNextTabstop.reset();
dispose(this._snippet); this._sessions.clear();
} }
getId(): string { getId(): string {
...@@ -56,47 +113,49 @@ export class SnippetController2 { ...@@ -56,47 +113,49 @@ export class SnippetController2 {
overwriteBefore: number = 0, overwriteAfter: number = 0, overwriteBefore: number = 0, overwriteAfter: number = 0,
undoStopBefore: boolean = true, undoStopAfter: boolean = true undoStopBefore: boolean = true, undoStopAfter: boolean = true
): void { ): void {
// don't listen while inserting the snippet
// as that is the inflight state causing cancelation
this._snippetListener = dispose(this._snippetListener);
if (undoStopBefore) { if (undoStopBefore) {
this._editor.getModel().pushStackElement(); this._editor.getModel().pushStackElement();
} }
if (!this._snippet) {
// create a new session const snippet = new SnippetSession(this._editor);
this._snippet = new SnippetSession(this._editor); snippet.insert(template, overwriteBefore, overwriteAfter);
this._snippet.insert(template, overwriteBefore, overwriteAfter); this._sessions.add(snippet);
if (undoStopAfter) {
this._editor.getModel().pushStackElement();
}
this._snippetListener = [ this._snippetListener = [
this._editor.onDidChangeModel(() => this.cancel()), this._editor.onDidChangeModel(() => this.cancel()),
this._editor.onDidChangeCursorSelection(() => this._updateState()) this._editor.onDidChangeCursorSelection(() => this._updateState())
]; ];
} else {
// only insert the snippet when a session
// is already active
this._snippet.insert(template, overwriteBefore, overwriteAfter);
}
if (undoStopAfter) {
this._editor.getModel().pushStackElement();
}
this._updateState(); this._updateState();
} }
private _updateState(): void { private _updateState(): void {
if (!this._snippet) { if (this._sessions.empty) {
// canceled in the meanwhile // canceled in the meanwhile
return; return;
} }
if (!this._snippet.hasPlaceholder) { if (!this._sessions.hasPlaceholder) {
// don't listen for selection changes and don't // don't listen for selection changes and don't
// update context keys when the snippet is plain text // update context keys when the snippet is plain text
return this.cancel(); return this.cancel();
} }
if (this._snippet.isAtFinalPlaceholder || !this._snippet.isSelectionWithinPlaceholders()) { if (this._sessions.isAtFinalPlaceholder || !this._sessions.isSelectionWithinPlaceholders) {
return this.cancel(); return this.cancel();
} }
this._inSnippet.set(true); this._inSnippet.set(true);
this._hasPrevTabstop.set(!this._snippet.isAtFirstPlaceholder); this._hasPrevTabstop.set(!this._sessions.isAtFirstPlaceholder);
this._hasNextTabstop.set(!this._snippet.isAtFinalPlaceholder); this._hasNextTabstop.set(!this._sessions.isAtFinalPlaceholder);
} }
finish(): void { finish(): void {
...@@ -106,29 +165,22 @@ export class SnippetController2 { ...@@ -106,29 +165,22 @@ export class SnippetController2 {
} }
cancel(): void { cancel(): void {
if (this._snippet) {
this._inSnippet.reset(); this._inSnippet.reset();
this._hasPrevTabstop.reset(); this._hasPrevTabstop.reset();
this._hasNextTabstop.reset(); this._hasNextTabstop.reset();
this._sessions.clear();
dispose(this._snippetListener); dispose(this._snippetListener);
dispose(this._snippet);
this._snippet = undefined;
}
} }
prev(): void { prev(): void {
if (this._snippet) { this._sessions.prev();
this._snippet.prev();
this._updateState(); this._updateState();
} }
}
next(): void { next(): void {
if (this._snippet) { this._sessions.next();
this._snippet.next();
this._updateState(); this._updateState();
} }
}
} }
......
...@@ -130,11 +130,13 @@ export class OneSnippet { ...@@ -130,11 +130,13 @@ export class OneSnippet {
} }
get isAtFirstPlaceholder() { get isAtFirstPlaceholder() {
return this._placeholderGroupsIdx === 0; return this._placeholderGroupsIdx === 0 || this._placeholderGroups.length === 0;
} }
get isAtFinalPlaceholder() { get isAtFinalPlaceholder() {
if (this._placeholderGroupsIdx < 0) { if (this._placeholderGroups.length === 0) {
return true;
} else if (this._placeholderGroupsIdx < 0) {
return false; return false;
} else { } else {
return this._placeholderGroups[this._placeholderGroupsIdx][0].isFinalTabstop; return this._placeholderGroups[this._placeholderGroupsIdx][0].isFinalTabstop;
...@@ -196,24 +198,35 @@ export class SnippetSession { ...@@ -196,24 +198,35 @@ export class SnippetSession {
return selection; 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 delta = 0;
let edits: IIdentifiedSingleEditOperation[] = [];
let snippets: OneSnippet[] = [];
let model = editor.getModel();
// know what text the overwrite[Before|After] extensions // know what text the overwrite[Before|After] extensions
// of the primary curser have selected because only when // of the primary curser have selected because only when
// secondary selections extend to the same text we can grow them // secondary selections extend to the same text we can grow them
let firstBeforeText = model.getValueInRange(SnippetSession.adjustSelection(model, editor.getSelection(), overwriteBefore, 0)); let firstBeforeText = model.getValueInRange(SnippetSession.adjustSelection(model, this._editor.getSelection(), overwriteBefore, 0));
let firstAfterText = model.getValueInRange(SnippetSession.adjustSelection(model, editor.getSelection(), 0, overwriteAfter)); let firstAfterText = model.getValueInRange(SnippetSession.adjustSelection(model, this._editor.getSelection(), 0, overwriteAfter));
// sort selections by their start position but remeber // sort selections by their start position but remeber
// the original index. that allows you to create correct // the original index. that allows you to create correct
// offset-based selection logic without changing the // offset-based selection logic without changing the
// primary selection // primary selection
const indexedSelection = editor.getSelections() const indexedSelection = this._editor.getSelections()
.map((selection, idx) => ({ selection, idx })) .map((selection, idx) => ({ selection, idx }))
.sort((a, b) => Range.compareRangesUsingStarts(a.selection, b.selection)); .sort((a, b) => Range.compareRangesUsingStarts(a.selection, b.selection));
...@@ -248,46 +261,18 @@ export class SnippetSession { ...@@ -248,46 +261,18 @@ export class SnippetSession {
// that ensures the primiary cursor stays primary despite not being // that ensures the primiary cursor stays primary despite not being
// the one with lowest start position // the one with lowest start position
edits[idx] = EditOperation.replaceMove(snippetSelection, snippet.text); edits[idx] = EditOperation.replaceMove(snippetSelection, snippet.text);
snippets[idx] = new OneSnippet(editor, snippet, offset); this._snippets[idx] = new OneSnippet(this._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;
} }
// make insert edit and start with first selections // 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); return this._move(true);
} else { } else {
return undoEdits.map(edit => Selection.fromPositions(edit.range.getEndPosition())); return undoEdits.map(edit => Selection.fromPositions(edit.range.getEndPosition()));
} }
}); }));
this._editor.setSelections(newSelections);
} }
next(): void { next(): void {
...@@ -322,6 +307,11 @@ export class SnippetSession { ...@@ -322,6 +307,11 @@ export class SnippetSession {
} }
isSelectionWithinPlaceholders(): boolean { isSelectionWithinPlaceholders(): boolean {
if (!this.hasPlaceholder) {
return false;
}
const selections = this._editor.getSelections(); const selections = this._editor.getSelections();
if (selections.length < this._snippets.length) { if (selections.length < this._snippets.length) {
// this means we started snippet mode with N // this means we started snippet mode with N
......
...@@ -148,17 +148,37 @@ suite('SnippetController2', function () { ...@@ -148,17 +148,37 @@ suite('SnippetController2', function () {
// assertContextKeys(contextKeys, false, false, false); // assertContextKeys(contextKeys, false, false, false);
}); });
test('insert, insert nested', function () { test('insert, nested snippet', function () {
const ctrl = new SnippetController2(editor, contextKeys); const ctrl = new SnippetController2(editor, contextKeys);
ctrl.insert('${1:foobar}$0'); ctrl.insert('${1:foobar}$0');
assertContextKeys(contextKeys, true, false, true); assertContextKeys(contextKeys, true, false, true);
assertSelections(editor, new Selection(1, 1, 1, 7), new Selection(2, 5, 2, 11)); assertSelections(editor, new Selection(1, 1, 1, 7), new Selection(2, 5, 2, 11));
ctrl.insert('farboo'); 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); assertContextKeys(contextKeys, true, false, true);
assertSelections(editor, new Selection(1, 1, 1, 7), new Selection(2, 5, 2, 11));
ctrl.insert('farboo');
assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(2, 11, 2, 11)); assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(2, 11, 2, 11));
assertContextKeys(contextKeys, true, false, true);
ctrl.next(); ctrl.next();
assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(2, 11, 2, 11));
assertContextKeys(contextKeys, false, false, false); assertContextKeys(contextKeys, false, false, false);
}); });
}); });
...@@ -403,20 +403,5 @@ suite('SnippetSession', function () { ...@@ -403,20 +403,5 @@ suite('SnippetSession', function () {
assert.equal(model.getValue(), '@line=1function foo() {\n @line=2console.log(a);\n}'); 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)); 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);
});
}); });
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册