Skip to content

Commit

Permalink
Support SnippetTextEdit vscode API (#12047)
Browse files Browse the repository at this point in the history
Signed-off-by: Jonah Iden <jonah.iden@typefox.io>
Co-authored-by: Mark Sujew <mark.sujew@typefox.io>
  • Loading branch information
jonah-iden and msujew authored Jan 16, 2023
1 parent 5062766 commit 7c90401
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 20 deletions.
7 changes: 7 additions & 0 deletions packages/core/src/common/array-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@ export namespace ArrayUtils {
return -(low + 1);
}

export function partition<T>(array: T[], filter: (e: T, idx: number, arr: T[]) => boolean | undefined): [T[], T[]] {
const pass: T[] = [];
const fail: T[] = [];
array.forEach((e, idx, arr) => (filter(e, idx, arr) ? pass : fail).push(e));
return [pass, fail];
}

/**
* @returns New array with all falsy values removed. The original array IS NOT modified.
*/
Expand Down
30 changes: 28 additions & 2 deletions packages/monaco/src/browser/monaco-workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { MonacoTextModelService } from './monaco-text-model-service';
import { WillSaveMonacoModelEvent, MonacoEditorModel, MonacoModelContentChangedEvent } from './monaco-editor-model';
import { MonacoEditor } from './monaco-editor';
import { ProblemManager } from '@theia/markers/lib/browser';
import { ArrayUtils } from '@theia/core/lib/common/types';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { FileSystemProviderCapabilities } from '@theia/filesystem/lib/common/files';
import * as monaco from '@theia/monaco-editor-core';
Expand All @@ -36,6 +37,9 @@ import {
import { IEditorWorkerService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/editorWorker';
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
import { EndOfLineSequence } from '@theia/monaco-editor-core/esm/vs/editor/common/model';
import { SnippetParser } from '@theia/monaco-editor-core/esm/vs/editor/contrib/snippet/browser/snippetParser';
import { TextEdit } from '@theia/monaco-editor-core/esm/vs/editor/common/languages';
import { SnippetController2 } from '@theia/monaco-editor-core/esm/vs/editor/contrib/snippet/browser/snippetController2';
import { isObject, MaybePromise } from '@theia/core/lib/common';

export namespace WorkspaceFileEdit {
Expand Down Expand Up @@ -227,7 +231,8 @@ export class MonacoWorkspace {
let totalEdits = 0;
let totalFiles = 0;
const fileEdits = edits.filter(edit => edit instanceof MonacoResourceFileEdit);
const textEdits = edits.filter(edit => edit instanceof MonacoResourceTextEdit);
const [snippetEdits, textEdits] = ArrayUtils.partition(edits.filter(edit => edit instanceof MonacoResourceTextEdit) as MonacoResourceTextEdit[],
edit => edit.textEdit.insertAsSnippet && (edit.resource.toString() === this.editorManager.activeEditor?.getResourceUri()?.toString()));

if (fileEdits.length > 0) {
await this.performFileEdits(<MonacoResourceFileEdit[]>fileEdits);
Expand All @@ -239,6 +244,10 @@ export class MonacoWorkspace {
totalFiles += result.totalFiles;
}

if (snippetEdits.length > 0) {
await this.performSnippetEdits(<MonacoResourceTextEdit[]>snippetEdits);
}

const ariaSummary = this.getAriaSummary(totalEdits, totalFiles);
return { ariaSummary, success: true };
} catch (e) {
Expand Down Expand Up @@ -288,7 +297,8 @@ export class MonacoWorkspace {
const uri = monaco.Uri.parse(key);
let eol: EndOfLineSequence | undefined;
const editOperations: monaco.editor.IIdentifiedSingleEditOperation[] = [];
const minimalEdits = await StandaloneServices.get(IEditorWorkerService).computeMoreMinimalEdits(uri, value.map(v => v.textEdit));
const minimalEdits = await StandaloneServices.get(IEditorWorkerService)
.computeMoreMinimalEdits(uri, value.map(edit => this.transformSnippetStringToInsertText(edit)));
if (minimalEdits) {
for (const textEdit of minimalEdits) {
if (typeof textEdit.eol === 'number') {
Expand Down Expand Up @@ -364,4 +374,20 @@ export class MonacoWorkspace {
}
}
}

protected async performSnippetEdits(edits: MonacoResourceTextEdit[]): Promise<void> {
const activeEditor = MonacoEditor.getActive(this.editorManager)?.getControl();
if (activeEditor) {
const snippetController: SnippetController2 = activeEditor.getContribution('snippetController2')!;
snippetController.apply(edits.map(edit => ({ range: monaco.Range.lift(edit.textEdit.range), template: edit.textEdit.text })));
}
}

protected transformSnippetStringToInsertText(resourceEdit: MonacoResourceTextEdit): TextEdit & { insertAsSnippet?: boolean } {
if (resourceEdit.textEdit.insertAsSnippet) {
return { ...resourceEdit.textEdit, insertAsSnippet: false, text: SnippetParser.asInsertText(resourceEdit.textEdit.text) };
} else {
return resourceEdit.textEdit;
}
}
}
5 changes: 2 additions & 3 deletions packages/plugin-ext/src/common/plugin-api-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1407,7 +1407,7 @@ export interface WorkspaceEditMetadataDto {
} | {
light: UriComponents;
dark: UriComponents;
};
} | ThemeIcon;
}

export interface WorkspaceFileEditDto {
Expand All @@ -1420,9 +1420,8 @@ export interface WorkspaceFileEditDto {
export interface WorkspaceTextEditDto {
resource: UriComponents;
modelVersionId?: number;
textEdit: TextEdit;
textEdit: TextEdit & { insertAsSnippet?: boolean };
metadata?: WorkspaceEditMetadataDto;

}
export namespace WorkspaceTextEditDto {
export function is(arg: WorkspaceTextEditDto | WorkspaceFileEditDto): arg is WorkspaceTextEditDto {
Expand Down
2 changes: 2 additions & 0 deletions packages/plugin-ext/src/plugin/plugin-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import {
CompletionItemKind,
CompletionList,
TextEdit,
SnippetTextEdit,
CompletionTriggerKind,
Diagnostic,
DiagnosticRelatedInformation,
Expand Down Expand Up @@ -1170,6 +1171,7 @@ export function createAPIFactory(
Diagnostic,
CompletionTriggerKind,
TextEdit,
SnippetTextEdit,
ProgressLocation,
ProgressOptions,
Progress,
Expand Down
10 changes: 9 additions & 1 deletion packages/plugin-ext/src/plugin/type-converters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,14 @@ export function fromTextEdit(edit: theia.TextEdit): model.TextEdit {
};
}

function fromSnippetTextEdit(edit: theia.SnippetTextEdit): model.TextEdit & { insertAsSnippet?: boolean } {
return {
text: edit.snippet.value,
range: fromRange(edit.range),
insertAsSnippet: true
};
}

export function convertDiagnosticToMarkerData(diagnostic: theia.Diagnostic): model.MarkerData {
return {
code: convertCode(diagnostic.code),
Expand Down Expand Up @@ -570,7 +578,7 @@ export function fromWorkspaceEdit(value: theia.WorkspaceEdit, documents?: any):
const workspaceTextEditDto: WorkspaceTextEditDto = {
resource: uri,
modelVersionId: doc?.version,
textEdit: uriOrEdits.map(fromTextEdit)[0],
textEdit: uriOrEdits.map(edit => (edit instanceof types.TextEdit) ? fromTextEdit(edit) : fromSnippetTextEdit(edit))[0],
metadata: entry[2] as types.WorkspaceEditMetadata
};
result.edits.push(workspaceTextEditDto);
Expand Down
86 changes: 73 additions & 13 deletions packages/plugin-ext/src/plugin/types-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1279,6 +1279,30 @@ export class NotebookRange implements theia.NotebookRange {

}

export class SnippetTextEdit implements theia.SnippetTextEdit {
range: Range;
snippet: SnippetString;

static isSnippetTextEdit(thing: unknown): thing is SnippetTextEdit {
return thing instanceof SnippetTextEdit || isObject<SnippetTextEdit>(thing)
&& Range.isRange((<SnippetTextEdit>thing).range)
&& SnippetString.isSnippetString((<SnippetTextEdit>thing).snippet);
}

static replace(range: Range, snippet: SnippetString): SnippetTextEdit {
return new SnippetTextEdit(range, snippet);
}

static insert(position: Position, snippet: SnippetString): SnippetTextEdit {
return SnippetTextEdit.replace(new Range(position, position), snippet);
}

constructor(range: Range, snippet: SnippetString) {
this.range = range;
this.snippet = snippet;
}
}

@es5ClassCompat
export class NotebookEdit implements theia.NotebookEdit {
range: theia.NotebookRange;
Expand Down Expand Up @@ -1623,28 +1647,44 @@ export interface WorkspaceEditMetadata {
} | {
light: URI;
dark: URI;
};
} | ThemeIcon;
}

export const enum FileEditType {
File = 1,
Text = 2,
Snippet = 6,
}

export interface FileOperation {
_type: 1;
_type: FileEditType.File;
from: URI | undefined;
to: URI | undefined;
options?: FileOperationOptions;
metadata?: WorkspaceEditMetadata;
}

export interface FileTextEdit {
_type: 2;
_type: FileEditType.Text;
uri: URI;
edit: TextEdit;
metadata?: WorkspaceEditMetadata;
}

export interface FileSnippetTextEdit {
readonly _type: FileEditType.Snippet;
readonly uri: URI;
readonly range: Range;
readonly edit: SnippetTextEdit;
readonly metadata?: theia.WorkspaceEditEntryMetadata;
}

type WorkspaceEditEntry = FileOperation | FileTextEdit | FileSnippetTextEdit | undefined;

@es5ClassCompat
export class WorkspaceEdit implements theia.WorkspaceEdit {

private _edits = new Array<FileOperation | FileTextEdit | undefined>();
private _edits = new Array<WorkspaceEditEntry>();

renameFile(from: theia.Uri, to: theia.Uri, options?: { overwrite?: boolean, ignoreIfExists?: boolean }, metadata?: WorkspaceEditMetadata): void {
this._edits.push({ _type: 1, from, to, options, metadata });
Expand Down Expand Up @@ -1679,21 +1719,41 @@ export class WorkspaceEdit implements theia.WorkspaceEdit {
return false;
}

set(uri: URI, edits: TextEdit[]): void {
set(uri: URI, edits: ReadonlyArray<TextEdit | SnippetTextEdit>): void;
set(uri: URI, edits: ReadonlyArray<[TextEdit | SnippetTextEdit, theia.WorkspaceEditEntryMetadata]>): void;

set(uri: URI, edits: ReadonlyArray<TextEdit | SnippetTextEdit | [TextEdit | SnippetTextEdit, theia.WorkspaceEditEntryMetadata]>): void {
if (!edits) {
// remove all text edits for `uri`
for (let i = 0; i < this._edits.length; i++) {
const element = this._edits[i];
if (element && element._type === 2 && element.uri.toString() === uri.toString()) {
if (element &&
(element._type === FileEditType.Text || element._type === FileEditType.Snippet) &&
element.uri.toString() === uri.toString()) {
this._edits[i] = undefined;
}
}
this._edits = this._edits.filter(e => !!e);
} else {
// append edit to the end
for (const edit of edits) {
if (edit) {
this._edits.push({ _type: 2, uri, edit });
for (const editOrTuple of edits) {
if (!editOrTuple) {
continue;
}

let edit: TextEdit | SnippetTextEdit;
let metadata: theia.WorkspaceEditEntryMetadata | undefined;
if (Array.isArray(editOrTuple)) {
edit = editOrTuple[0];
metadata = editOrTuple[1];
} else {
edit = editOrTuple;
}

if (SnippetTextEdit.isSnippetTextEdit(edit)) {
this._edits.push({ _type: FileEditType.Snippet, uri, range: edit.range, edit, metadata });
} else {
this._edits.push({ _type: FileEditType.Text, uri, edit });
}
}
}
Expand All @@ -1715,7 +1775,7 @@ export class WorkspaceEdit implements theia.WorkspaceEdit {
entries(): [URI, TextEdit[]][] {
const textEdits = new Map<string, [URI, TextEdit[]]>();
for (const candidate of this._edits) {
if (candidate && candidate._type === 2) {
if (candidate && candidate._type === FileEditType.Text) {
let textEdit = textEdits.get(candidate.uri.toString());
if (!textEdit) {
textEdit = [candidate.uri, []];
Expand All @@ -1729,13 +1789,13 @@ export class WorkspaceEdit implements theia.WorkspaceEdit {
return result;
}

_allEntries(): ([URI, TextEdit[], WorkspaceEditMetadata] | [URI, URI, FileOperationOptions, WorkspaceEditMetadata])[] {
const res: ([URI, TextEdit[], WorkspaceEditMetadata] | [URI, URI, FileOperationOptions, WorkspaceEditMetadata])[] = [];
_allEntries(): ([URI, Array<TextEdit | SnippetTextEdit>, theia.WorkspaceEditEntryMetadata] | [URI, URI, FileOperationOptions, WorkspaceEditMetadata])[] {
const res: ([URI, Array<TextEdit | SnippetTextEdit>, theia.WorkspaceEditEntryMetadata] | [URI, URI, FileOperationOptions, WorkspaceEditMetadata])[] = [];
for (const edit of this._edits) {
if (!edit) {
continue;
}
if (edit._type === 1) {
if (edit._type === FileEditType.File) {
res.push([edit.from!, edit.to!, edit.options!, edit.metadata!]);
} else {
res.push([edit.uri, [edit.edit], edit.metadata!]);
Expand Down
58 changes: 57 additions & 1 deletion packages/plugin/src/theia.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9501,7 +9501,15 @@ export module '@theia/plugin' {
* @param uri A resource identifier.
* @param edits An array of text edits.
*/
set(uri: Uri, edits: TextEdit[]): void;
set(uri: Uri, edits: ReadonlyArray<TextEdit | SnippetTextEdit>): void;

/**
* Set (and replace) text edits or snippet edits with metadata for a resource.
*
* @param uri A resource identifier.
* @param edits An array of edits.
*/
set(uri: Uri, edits: ReadonlyArray<[TextEdit | SnippetTextEdit, WorkspaceEditEntryMetadata]>): void;

/**
* Get the text edits for a resource.
Expand Down Expand Up @@ -14309,6 +14317,54 @@ export module '@theia/plugin' {
readonly selections?: readonly NotebookRange[];
}

/**
* A snippet edit represents an interactive edit that is performed by
* the editor.
*
* *Note* that a snippet edit can always be performed as a normal {@link TextEdit text edit}.
* This will happen when no matching editor is open or when a {@link WorkspaceEdit workspace edit}
* contains snippet edits for multiple files. In that case only those that match the active editor
* will be performed as snippet edits and the others as normal text edits.
*/
export class SnippetTextEdit {

/**
* Utility to create a replace snippet edit.
*
* @param range A range.
* @param snippet A snippet string.
* @return A new snippet edit object.
*/
static replace(range: Range, snippet: SnippetString): SnippetTextEdit;

/**
* Utility to create an insert snippet edit.
*
* @param position A position, will become an empty range.
* @param snippet A snippet string.
* @return A new snippet edit object.
*/
static insert(position: Position, snippet: SnippetString): SnippetTextEdit;

/**
* The range this edit applies to.
*/
range: Range;

/**
* The {@link SnippetString snippet} this edit will perform.
*/
snippet: SnippetString;

/**
* Create a new snippet edit.
*
* @param range A range.
* @param snippet A snippet string.
*/
constructor(range: Range, snippet: SnippetString);
}

/**
* A notebook edit represents edits that should be applied to the contents of a notebook.
*/
Expand Down

0 comments on commit 7c90401

Please sign in to comment.