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

basic support for nested snippets, #24855

上级 4ad0fd04
......@@ -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');
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');
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);
});
});
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册