Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

joh/theoretical quokka #154157

Merged
merged 2 commits into from
Jul 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions extensions/vscode-api-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"scmActionButton",
"scmSelectedProvider",
"scmValidation",
"snippetWorkspaceEdit",
"taskPresentationGroup",
"terminalDataWriteEvent",
"terminalDimensions",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1147,4 +1147,27 @@ suite('vscode API - workspace', () => {
assert.strictEqual(document.isDirty, false);
}
});

test('SnippetString in WorkspaceEdit', async function (): Promise<any> {
const file = await createRandomFile('hello\nworld');

const document = await vscode.workspace.openTextDocument(file);
const edt = await vscode.window.showTextDocument(document);

assert.ok(edt === vscode.window.activeTextEditor);

const we = new vscode.WorkspaceEdit();
we.set(document.uri, [{ range: new vscode.Range(0, 0, 0, 0), newText: '', newText2: new vscode.SnippetString('${1:foo}${2:bar}') }]);
const success = await vscode.workspace.applyEdit(we);


if (edt !== vscode.window.activeTextEditor) {
return this.skip();
}

assert.ok(success);
assert.strictEqual(document.getText(), 'foobarhello\nworld');
assert.deepStrictEqual(edt.selections, [new vscode.Selection(0, 0, 0, 3)]);

});
});
79 changes: 30 additions & 49 deletions src/vs/editor/contrib/snippet/browser/snippetController2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,26 @@

import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { assertType } from 'vs/base/common/types';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { EditorCommand, registerEditorCommand, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { ISelection } from 'vs/editor/common/core/selection';
import { ISelection, Selection } from 'vs/editor/common/core/selection';
import { IEditorContribution } from 'vs/editor/common/editorCommon';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { CompletionItem, CompletionItemKind, CompletionItemProvider } from 'vs/editor/common/languages';
import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry';
import { ITextModel } from 'vs/editor/common/model';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { Choice, SnippetParser } from 'vs/editor/contrib/snippet/browser/snippetParser';
import { Choice } from 'vs/editor/contrib/snippet/browser/snippetParser';
import { showSimpleSuggestions } from 'vs/editor/contrib/suggest/browser/suggest';
import { OvertypingCapturer } from 'vs/editor/contrib/suggest/browser/suggestOvertypingCapturer';
import { localize } from 'vs/nls';
import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { ILogService } from 'vs/platform/log/common/log';
import { SnippetSession } from './snippetSession';
import { ISnippetEdit, SnippetSession } from './snippetSession';

export interface ISnippetInsertOptions {
overwriteBefore: number;
Expand Down Expand Up @@ -88,6 +89,19 @@ export class SnippetController2 implements IEditorContribution {
this._snippetListener.dispose();
}

apply(edits: ISnippetEdit[], opts?: Partial<ISnippetInsertOptions>) {
try {
this._doInsert(edits, typeof opts === 'undefined' ? _defaultOptions : { ..._defaultOptions, ...opts });

} catch (e) {
this.cancel();
this._logService.error(e);
this._logService.error('snippet_error');
this._logService.error('insert_edits=', edits);
this._logService.error('existing_template=', this._session ? this._session._logInfo() : '<no_session>');
}
}

insert(
template: string,
opts?: Partial<ISnippetInsertOptions>
Expand All @@ -108,7 +122,7 @@ export class SnippetController2 implements IEditorContribution {
}

private _doInsert(
template: string,
template: string | ISnippetEdit[],
opts: ISnippetInsertOptions
): void {
if (!this._editor.hasModel()) {
Expand All @@ -123,11 +137,17 @@ export class SnippetController2 implements IEditorContribution {
this._editor.getModel().pushStackElement();
}

// don't merge
if (this._session && typeof template !== 'string') {
this.cancel();
}

if (!this._session) {
this._modelVersionId = this._editor.getModel().getAlternativeVersionId();
this._session = new SnippetSession(this._editor, template, opts, this._languageConfigurationService);
this._session.insert();
} else {
assertType(typeof template === 'string');
this._session.merge(template, opts);
}

Expand Down Expand Up @@ -342,50 +362,11 @@ export function performSnippetEdit(editor: ICodeEditor, snippet: string, selecti
return false;
}
editor.focus();
editor.setSelections(selections ?? []);
controller.insert(snippet);
return controller.isInSnippet();
}


export type ISnippetEdit = {
range: Range;
snippet: string;
};

// ---

export function performSnippetEdits(editor: ICodeEditor, edits: ISnippetEdit[]) {

if (!editor.hasModel()) {
return false;
}
if (edits.length === 0) {
return false;
}

const model = editor.getModel();
let newText = '';
let last: ISnippetEdit | undefined;
edits.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range));

for (const item of edits) {
if (last) {
const between = Range.fromPositions(last.range.getEndPosition(), item.range.getStartPosition());
const text = model.getValueInRange(between);
newText += SnippetParser.escape(text);
}
newText += item.snippet;
last = item;
}

const controller = SnippetController2.get(editor);
if (!controller) {
return false;
}
model.pushStackElement();
const range = Range.plusRange(edits[0].range, edits[edits.length - 1].range);
editor.setSelection(range);
controller.insert(newText, { undoStopBefore: false });
controller.apply(selections.map(selection => {
return {
range: Selection.liftSelection(selection),
template: snippet
};
}));
return controller.isInSnippet();
}
32 changes: 20 additions & 12 deletions src/vs/editor/contrib/snippet/browser/snippetParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -613,11 +613,17 @@ export class SnippetParser {
}

parse(value: string, insertFinalTabstop?: boolean, enforceFinalTabstop?: boolean): TextmateSnippet {
const snippet = new TextmateSnippet();
this.parseFragment(value, snippet);
this.ensureFinalTabstop(snippet, enforceFinalTabstop ?? false, insertFinalTabstop ?? false);
return snippet;
}

parseFragment(value: string, snippet: TextmateSnippet): readonly Marker[] {

const offset = snippet.children.length;
this._scanner.text(value);
this._token = this._scanner.next();

const snippet = new TextmateSnippet();
while (this._parse(snippet)) {
// nothing
}
Expand All @@ -626,10 +632,8 @@ export class SnippetParser {
// that has a value defines the value for all placeholders with that index
const placeholderDefaultValues = new Map<number, Marker[] | undefined>();
const incompletePlaceholders: Placeholder[] = [];
let placeholderCount = 0;
snippet.walk(marker => {
if (marker instanceof Placeholder) {
placeholderCount += 1;
if (marker.isFinalTabstop) {
placeholderDefaultValues.set(0, undefined);
} else if (!placeholderDefaultValues.has(marker.index) && marker.children.length > 0) {
Expand All @@ -640,6 +644,7 @@ export class SnippetParser {
}
return true;
});

for (const placeholder of incompletePlaceholders) {
const defaultValues = placeholderDefaultValues.get(placeholder.index);
if (defaultValues) {
Expand All @@ -652,17 +657,20 @@ export class SnippetParser {
}
}

if (!enforceFinalTabstop) {
enforceFinalTabstop = placeholderCount > 0 && insertFinalTabstop;
}
return snippet.children.slice(offset);
}

if (!placeholderDefaultValues.has(0) && enforceFinalTabstop) {
// the snippet uses placeholders but has no
// final tabstop defined -> insert at the end
snippet.appendChild(new Placeholder(0));
ensureFinalTabstop(snippet: TextmateSnippet, enforceFinalTabstop: boolean, insertFinalTabstop: boolean) {

if (enforceFinalTabstop || insertFinalTabstop && snippet.placeholders.length > 0) {
const finalTabstop = snippet.placeholders.find(p => p.index === 0);
if (!finalTabstop) {
// the snippet uses placeholders but has no
// final tabstop defined -> insert at the end
snippet.appendChild(new Placeholder(0));
}
}

return snippet;
}

private _accept(type?: TokenType): boolean;
Expand Down
95 changes: 80 additions & 15 deletions src/vs/editor/contrib/snippet/browser/snippetSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,11 @@ const _defaultOptions: ISnippetSessionInsertOptions = {
overtypingCapturer: undefined
};

export interface ISnippetEdit {
range: Range;
template: string;
}

export class SnippetSession {

static adjustWhitespace(model: ITextModel, position: IPosition, snippet: TextmateSnippet, adjustIndentation: boolean, adjustNewlines: boolean): string {
Expand Down Expand Up @@ -434,7 +439,7 @@ export class SnippetSession {
return selection;
}

static createEditsAndSnippets(editor: IActiveCodeEditor, template: string, overwriteBefore: number, overwriteAfter: number, enforceFinalTabstop: boolean, adjustWhitespace: boolean, clipboardText: string | undefined, overtypingCapturer: OvertypingCapturer | undefined, languageConfigurationService: ILanguageConfigurationService): { edits: IIdentifiedSingleEditOperation[]; snippets: OneSnippet[] } {
static createEditsAndSnippetsFromSelections(editor: IActiveCodeEditor, template: string, overwriteBefore: number, overwriteAfter: number, enforceFinalTabstop: boolean, adjustWhitespace: boolean, clipboardText: string | undefined, overtypingCapturer: OvertypingCapturer | undefined, languageConfigurationService: ILanguageConfigurationService): { edits: IIdentifiedSingleEditOperation[]; snippets: OneSnippet[] } {
const edits: IIdentifiedSingleEditOperation[] = [];
const snippets: OneSnippet[] = [];

Expand Down Expand Up @@ -518,22 +523,79 @@ export class SnippetSession {
return { edits, snippets };
}

private readonly _editor: IActiveCodeEditor;
private readonly _template: string;
private readonly _templateMerges: [number, number, string][] = [];
private readonly _options: ISnippetSessionInsertOptions;
static createEditsAndSnippetsFromEdits(editor: IActiveCodeEditor, snippetEdits: ISnippetEdit[], enforceFinalTabstop: boolean, adjustWhitespace: boolean, clipboardText: string | undefined, overtypingCapturer: OvertypingCapturer | undefined, languageConfigurationService: ILanguageConfigurationService): { edits: IIdentifiedSingleEditOperation[]; snippets: OneSnippet[] } {

if (!editor.hasModel() || snippetEdits.length === 0) {
return { edits: [], snippets: [] };
}

const edits: IIdentifiedSingleEditOperation[] = [];
const model = editor.getModel();

const parser = new SnippetParser();
const snippet = new TextmateSnippet();

//
snippetEdits = snippetEdits.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range));
let offset = 0;
for (let i = 0; i < snippetEdits.length; i++) {

const { range, template } = snippetEdits[i];

// gaps between snippet edits are appended as text nodes. this
// ensures placeholder-offsets are later correct
if (i > 0) {
const lastRange = snippetEdits[i - 1].range;
const textRange = Range.fromPositions(lastRange.getEndPosition(), range.getStartPosition());
const textNode = new Text(model.getValueInRange(textRange));
snippet.appendChild(textNode);
offset += textNode.value.length;
}

parser.parseFragment(template, snippet);

const snippetText = snippet.toString();
const snippetFragmentText = snippetText.slice(offset);
offset = snippetText.length;

// make edit
const edit: IIdentifiedSingleEditOperation = EditOperation.replace(range, snippetFragmentText);
edit.identifier = { major: i, minor: 0 }; // mark the edit so only our undo edits will be used to generate end cursors
edit._isTracked = true;
edits.push(edit);
}

//
parser.ensureFinalTabstop(snippet, enforceFinalTabstop, true);

// snippet variables resolver
const resolver = new CompositeSnippetVariableResolver([
editor.invokeWithinContext(accessor => new ModelBasedVariableResolver(accessor.get(ILabelService), model)),
new ClipboardBasedVariableResolver(() => clipboardText, 0, editor.getSelections().length, editor.getOption(EditorOption.multiCursorPaste) === 'spread'),
new SelectionBasedVariableResolver(model, editor.getSelection(), 0, overtypingCapturer),
new CommentBasedVariableResolver(model, editor.getSelection(), languageConfigurationService),
new TimeBasedVariableResolver,
new WorkspaceBasedVariableResolver(editor.invokeWithinContext(accessor => accessor.get(IWorkspaceContextService))),
new RandomBasedVariableResolver,
]);
snippet.resolveVariables(resolver);


return {
edits,
snippets: [new OneSnippet(editor, snippet, '')]
};
}

private readonly _templateMerges: [number, number, string | ISnippetEdit[]][] = [];
private _snippets: OneSnippet[] = [];

constructor(
editor: IActiveCodeEditor,
template: string,
options: ISnippetSessionInsertOptions = _defaultOptions,
private readonly _editor: IActiveCodeEditor,
private readonly _template: string | ISnippetEdit[],
private readonly _options: ISnippetSessionInsertOptions = _defaultOptions,
@ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService
) {
this._editor = editor;
this._template = template;
this._options = options;
}
) { }

dispose(): void {
dispose(this._snippets);
Expand All @@ -549,7 +611,10 @@ export class SnippetSession {
}

// make insert edit and start with first selections
const { edits, snippets } = SnippetSession.createEditsAndSnippets(this._editor, this._template, this._options.overwriteBefore, this._options.overwriteAfter, false, this._options.adjustWhitespace, this._options.clipboardText, this._options.overtypingCapturer, this._languageConfigurationService);
const { edits, snippets } = typeof this._template === 'string'
? SnippetSession.createEditsAndSnippetsFromSelections(this._editor, this._template, this._options.overwriteBefore, this._options.overwriteAfter, false, this._options.adjustWhitespace, this._options.clipboardText, this._options.overtypingCapturer, this._languageConfigurationService)
: SnippetSession.createEditsAndSnippetsFromEdits(this._editor, this._template, false, this._options.adjustWhitespace, this._options.clipboardText, this._options.overtypingCapturer, this._languageConfigurationService);

this._snippets = snippets;

this._editor.executeEdits('snippet', edits, _undoEdits => {
Expand All @@ -576,7 +641,7 @@ export class SnippetSession {
return;
}
this._templateMerges.push([this._snippets[0]._nestingLevel, this._snippets[0]._placeholderGroupsIdx, template]);
const { edits, snippets } = SnippetSession.createEditsAndSnippets(this._editor, template, options.overwriteBefore, options.overwriteAfter, true, options.adjustWhitespace, options.clipboardText, options.overtypingCapturer, this._languageConfigurationService);
const { edits, snippets } = SnippetSession.createEditsAndSnippetsFromSelections(this._editor, template, options.overwriteBefore, options.overwriteAfter, true, options.adjustWhitespace, options.clipboardText, options.overtypingCapturer, this._languageConfigurationService);

this._editor.executeEdits('snippet', edits, _undoEdits => {
// Sometimes, the text buffer will remove automatic whitespace when doing any edits,
Expand Down
Loading