提交 46ae36f9 编写于 作者: J Johannes Rieken

more correct snippet session

上级 b3f11cd4
......@@ -6,44 +6,56 @@
'use strict';
import { getLeadingWhitespace, compare } from 'vs/base/common/strings';
import { ICommonCodeEditor, TrackedRangeStickiness } from 'vs/editor/common/editorCommon';
import { ICommonCodeEditor, IModel, TrackedRangeStickiness, IIdentifiedSingleEditOperation } from 'vs/editor/common/editorCommon';
import { EditOperation } from 'vs/editor/common/core/editOperation';
import { TextmateSnippet, Placeholder } from '../common/snippetParser';
import { TextmateSnippet, Placeholder, SnippetParser } from '../common/snippetParser';
import { Selection } from 'vs/editor/common/core/selection';
import { Range } from 'vs/editor/common/core/range';
import { IPosition } from 'vs/editor/common/core/position';
import { dispose } from 'vs/base/common/lifecycle';
class OneSnippet {
private readonly _editor: ICommonCodeEditor;
private readonly _placeholderDecoration = new Map<Placeholder, string>();
private readonly _placeholderGroups: Placeholder[][];
private readonly _snippet: TextmateSnippet;
private readonly _offset: number;
private _placeholderDecorations: Map<Placeholder, string>;
private _placeholderGroups: Placeholder[][];
private _placeholderGroupsIdx: number;
constructor(editor: ICommonCodeEditor, selection: Selection, snippet: TextmateSnippet) {
constructor(editor: ICommonCodeEditor, snippet: TextmateSnippet, offset: number) {
this._editor = editor;
this._snippet = snippet;
this._offset = offset;
}
// for each selection get the leading 'reference'-whitespace and adjust the snippet accordingly.
const model = editor.getModel();
const line = model.getLineContent(selection.startLineNumber);
const leadingWhitespace = getLeadingWhitespace(line, 0, selection.startColumn - 1);
snippet = snippet.withIndentation(whitespace => model.normalizeIndentation(leadingWhitespace + whitespace));
dispose(): void {
if (this._placeholderDecorations) {
this._editor.changeDecorations(accessor => this._placeholderDecorations.forEach(handle => accessor.removeDecoration(handle)));
}
}
const offset = model.getOffsetAt(selection.getStartPosition());
private _init(): void {
this._editor.executeEdits('onesnieppt', [EditOperation.replaceMove(selection, snippet.value)]);
if (this._placeholderDecorations) {
// already initialized
return;
}
this._placeholderDecorations = new Map<Placeholder, string>();
const model = this._editor.getModel();
// create a decoration (tracked range) for each placeholder
this._editor.changeDecorations(accessor => {
let lastRange: Range;
for (const placeholder of snippet.getPlaceholders()) {
const placeholderOffset = snippet.offset(placeholder);
const placeholderLen = snippet.len(placeholder);
const start = model.getPositionAt(offset + placeholderOffset);
const end = model.getPositionAt(offset + placeholderOffset + placeholderLen);
for (const placeholder of this._snippet.getPlaceholders()) {
const placeholderOffset = this._snippet.offset(placeholder);
const placeholderLen = this._snippet.len(placeholder);
const start = model.getPositionAt(this._offset + placeholderOffset);
const end = model.getPositionAt(this._offset + placeholderOffset + placeholderLen);
const range = new Range(start.lineNumber, start.column, end.lineNumber, end.column);
let stickiness: TrackedRangeStickiness;
......@@ -54,7 +66,7 @@ class OneSnippet {
}
const handle = accessor.addDecoration(range, { stickiness });
this._placeholderDecoration.set(placeholder, handle);
this._placeholderDecorations.set(placeholder, handle);
lastRange = range;
}
......@@ -63,7 +75,7 @@ class OneSnippet {
this._placeholderGroupsIdx = -1;
this._placeholderGroups = [];
let lastBucket: Placeholder[];
snippet.getPlaceholders().sort((a, b) => compare(a.name, b.name)).reverse().forEach(a => {
this._snippet.getPlaceholders().sort((a, b) => compare(a.name, b.name)).reverse().forEach(a => {
if (!lastBucket || lastBucket[0].name !== a.name) {
lastBucket = [a];
this._placeholderGroups.push(lastBucket);
......@@ -73,47 +85,107 @@ class OneSnippet {
});
}
dispose(): void {
this._editor.changeDecorations(accessor => this._placeholderDecoration.forEach(handle => accessor.removeDecoration(handle)));
}
move(fwd: boolean): Selection[] {
next(): Selection[] {
this._placeholderGroupsIdx += 1;
if (this._placeholderGroupsIdx >= this._placeholderGroups.length) {
this._init();
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;
}
const ranges: Selection[] = [];
}
private _getCurrentPlaceholderSelections(): Selection[] {
const selections: Selection[] = [];
for (const placeholder of this._placeholderGroups[this._placeholderGroupsIdx]) {
const handle = this._placeholderDecoration.get(placeholder);
const handle = this._placeholderDecorations.get(placeholder);
const range = this._editor.getModel().getDecorationRange(handle);
ranges.push(new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn));
selections.push(new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn));
}
return ranges;
return selections;
}
}
export class SnippetSession {
static normalizeWhitespace(model: IModel, position: IPosition, template: string): string {
const line = model.getLineContent(position.lineNumber);
const lineLeadingWhitespace = getLeadingWhitespace(line, 0, position.column - 1);
const templateLines = template.split(/\r\n|\r|\n/);
for (let i = 0; i < templateLines.length; i++) {
let templateLeadingWhitespace = getLeadingWhitespace(templateLines[i]);
if (templateLeadingWhitespace.length > 0) {
templateLines[i] = model.normalizeIndentation(lineLeadingWhitespace + templateLeadingWhitespace) + templateLines[i].substr(templateLeadingWhitespace.length);
}
}
return templateLines.join(model.getEOL());
}
private readonly _editor: ICommonCodeEditor;
private readonly _snippets: OneSnippet[] = [];
private readonly _snippets: OneSnippet[];
constructor(editor: ICommonCodeEditor, snippet: TextmateSnippet) {
constructor(editor: ICommonCodeEditor, template: string) {
this._editor = editor;
this._editor.pushUndoStop();
this._snippets = [];
let delta = 0;
let edits: IIdentifiedSingleEditOperation[] = [];
let model = editor.getModel();
for (const selection of editor.getSelections()) {
const oneSnippet = new OneSnippet(editor, selection, snippet);
this._snippets.push(oneSnippet);
const start = selection.getStartPosition();
const adjustedTemplate = SnippetSession.normalizeWhitespace(model, start, template);
const snippet = SnippetParser.parse(adjustedTemplate);
const offset = model.getOffsetAt(start) + delta;
edits.push(EditOperation.replaceMove(selection, snippet.value));
this._snippets.push(new OneSnippet(editor, snippet, offset));
delta += snippet.value.length - model.getValueLengthInRange(selection);
}
this._editor.pushUndoStop();
this.next();
// make insert edit and start with first selections
const newSelections = model.pushEditOperations(editor.getSelections(), edits, () => this._move(true));
this._editor.setSelections(newSelections);
}
next(): void {
const newSelections = this._move(true);
this._editor.setSelections(newSelections);
}
prev(): void {
const newSelections = this._move(false);
this._editor.setSelections(newSelections);
}
private _move(fwd: boolean): Selection[] {
const selections: Selection[] = [];
for (const snippet of this._snippets) {
const sel = snippet.next();
selections.push(...sel);
const oneSelection = snippet.move(fwd);
if (!oneSelection) {
if (fwd) {
this.stop();
}
return this._editor.getSelections();
}
selections.push(...oneSelection);
}
this._editor.setSelections(selections);
return selections;
}
stop(): void {
dispose(this._snippets);
}
}
......@@ -6,49 +6,110 @@
import * as assert from 'assert';
import { Selection } from 'vs/editor/common/core/selection';
import { IPosition, Position } from 'vs/editor/common/core/position';
import { SnippetSession } from 'vs/editor/contrib/snippet/browser/editorSnippets';
import { SnippetParser } from 'vs/editor/contrib/snippet/common/snippetParser';
import { ICommonCodeEditor } from 'vs/editor/common/editorCommon';
import { mockCodeEditor } from 'vs/editor/test/common/mocks/mockCodeEditor';
import { Model } from "vs/editor/common/model/model";
suite('Editor Contrib - Snippets', () => {
suite('SnippetInsertion', function () {
let editor: ICommonCodeEditor;
let model: Model;
function assertSelections(editor: ICommonCodeEditor, ...s: Selection[]) {
for (const selection of editor.getSelections()) {
const actual = s.shift();
assert.ok(selection.equalsSelection(actual), `actual=${selection.toString()} <> expected=${actual.toString()}`);
}
assert.equal(s.length, 0);
}
setup(() => {
editor = mockCodeEditor([
'function foo() {',
'\tconsole.log(a)',
'}'
], {});
setup(function () {
model = Model.createFromString('function foo() {\n console.log(a);\n}');
editor = mockCodeEditor([], { model });
editor.setSelections([new Selection(1, 1, 1, 1), new Selection(2, 5, 2, 5)]);
assert.equal(model.getEOL(), '\n');
});
teardown(() => {
editor.dispose();
teardown(function () {
model.dispose();
});
test('snippets, selections', () => {
test('normalize whitespace', function () {
editor.setSelections([
new Selection(1, 1, 1, 1),
new Selection(2, 2, 2, 2),
]);
function assertNormalized(position: IPosition, input: string, expected: string): void {
const actual = SnippetSession.normalizeWhitespace(model, position, input);
assert.equal(actual, expected);
}
assertNormalized(new Position(1, 1), 'foo', 'foo');
assertNormalized(new Position(1, 1), 'foo\rbar', 'foo\nbar');
assertNormalized(new Position(1, 1), 'foo\rbar', 'foo\nbar');
assertNormalized(new Position(2, 5), 'foo\r\tbar', 'foo\n bar');
assertNormalized(new Position(2, 3), 'foo\r\tbar', 'foo\n bar');
});
const snippet = SnippetParser.parse('foo${1:bar}foo$0');
const session = new SnippetSession(editor, snippet);
test('text edits & selection', function () {
const session = new SnippetSession(editor, 'foo${1:bar}foo$0');
assert.equal(editor.getModel().getValue(), 'foobarfoofunction foo() {\n foobarfooconsole.log(a);\n}');
assertSelections(editor, new Selection(1, 4, 1, 7), new Selection(2, 8, 2, 11));
session.next();
assertSelections(editor, new Selection(1, 10, 1, 10), new Selection(2, 14, 2, 14));
});
assert.equal(editor.getModel().getLineContent(1), 'foobarfoofunction foo() {');
assert.equal(editor.getModel().getLineContent(2), '\tfoobarfooconsole.log(a)');
test('snippets, selections and new text with newlines', () => {
assert.equal(editor.getSelections().length, 2);
assert.ok(editor.getSelections()[0].equalsSelection(new Selection(1, 4, 1, 7)));
assert.ok(editor.getSelections()[1].equalsSelection(new Selection(2, 5, 2, 8)));
const session = new SnippetSession(editor, 'foo\n\t${1:bar}\n$0');
assert.equal(editor.getModel().getValue(), 'foo\n bar\nfunction foo() {\n foo\n bar\nconsole.log(a);\n}');
assertSelections(editor, new Selection(2, 5, 2, 8), new Selection(5, 9, 5, 12));
session.next();
assert.equal(editor.getSelections().length, 2);
assert.ok(editor.getSelections()[0].equalsSelection(new Selection(1, 10, 1, 10)));
assert.ok(editor.getSelections()[1].equalsSelection(new Selection(2, 11, 2, 11)));
assertSelections(editor, new Selection(3, 1, 3, 1), new Selection(6, 1, 6, 1));
});
test('snippets, selections -> next/prev', () => {
const session = new SnippetSession(editor, 'f$2oo${1:bar}foo$0');
// @ $2
assertSelections(editor, new Selection(1, 2, 1, 2), new Selection(2, 6, 2, 6));
// @ $1
session.next();
assertSelections(editor, new Selection(1, 4, 1, 7), new Selection(2, 8, 2, 11));
// @ $2
session.prev();
assertSelections(editor, new Selection(1, 2, 1, 2), new Selection(2, 6, 2, 6));
// @ $1
session.next();
assertSelections(editor, new Selection(1, 4, 1, 7), new Selection(2, 8, 2, 11));
// @ $0
session.next();
assertSelections(editor, new Selection(1, 10, 1, 10), new Selection(2, 14, 2, 14));
});
test('snippest, selections & typing', function () {
const session = new SnippetSession(editor, 'f${2:oo}_$1_$0');
editor.trigger('test', 'type', { text: 'X' });
session.next();
editor.trigger('test', 'type', { text: 'bar' });
// go back to ${2:oo} which is now just 'X'
session.prev();
assertSelections(editor, new Selection(1, 2, 1, 3), new Selection(2, 6, 2, 7));
// go forward to $1 which is now 'bar'
session.next();
assertSelections(editor, new Selection(1, 4, 1, 7), new Selection(2, 8, 2, 11));
// go to final tabstop
session.next();
assert.equal(model.getValue(), 'fX_bar_function foo() {\n fX_bar_console.log(a);\n}');
assertSelections(editor, new Selection(1, 8, 1, 8), new Selection(2, 12, 2, 12));
});
});
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册