diff --git a/extensions/vscode-api-tests/src/editor.test.ts b/extensions/vscode-api-tests/src/editor.test.ts index 81d9565520a..1eafbc23530 100644 --- a/extensions/vscode-api-tests/src/editor.test.ts +++ b/extensions/vscode-api-tests/src/editor.test.ts @@ -6,7 +6,7 @@ 'use strict'; import * as assert from 'assert'; -import { workspace, window, Position, Range, commands, TextEditor, TextDocument, TextEditorCursorStyle, TextEditorLineNumbersStyle } from 'vscode'; +import { workspace, window, Position, Range, commands, TextEditor, TextDocument, TextEditorCursorStyle, TextEditorLineNumbersStyle, SnippetString, Selection } from 'vscode'; import { createRandomFile, deleteFile, cleanUp } from './utils'; suite('editor tests', () => { @@ -33,6 +33,40 @@ suite('editor tests', () => { }); } + test('insert snippet', () => { + const snippetString = new SnippetString() + .appendText('This is a ') + .appendTabstop() + .appendPlaceholder('placeholder') + .appendText(' snippet'); + + return withRandomFileEditor('', (editor, doc) => { + return editor.edit(snippetString).then(inserted => { + assert.ok(inserted); + assert.equal(doc.getText(), 'This is a placeholder snippet'); + assert.ok(doc.isDirty); + }); + }); + }); + + test('insert snippet with replacement', () => { + const snippetString = new SnippetString() + .appendText('has been'); + + return withRandomFileEditor('This will be replaced', (editor, doc) => { + editor.selection = new Selection( + new Position(0, 5), + new Position(0, 12) + ); + + return editor.edit(snippetString).then(inserted => { + assert.ok(inserted); + assert.equal(doc.getText(), 'This has been replaced'); + assert.ok(doc.isDirty); + }); + }); + }); + test('make edit', () => { return withRandomFileEditor('', (editor, doc) => { return editor.edit((builder) => { diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index f3b6312c5d0..e51b03adac2 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -936,6 +936,15 @@ declare module 'vscode' { */ edit(callback: (editBuilder: TextEditorEdit) => void, options?: { undoStopBefore: boolean; undoStopAfter: boolean; }): Thenable; + /** + * Enters snippet mode in the editor with the specified snippet. + * + * @param snippet The snippet to insert in this edit. + * @param options The undo/redo behaviour around this edit. By default, undo stops will be created before and after this edit. + * @return A promise that resolves with a value indicating if the snippet could be inserted. + */ + edit(snippet: SnippetString, options?: { undoStopBefore: boolean; undoStopAfter: boolean; }): Thenable; + /** * Adds a set of decorations to the text editor. If a set of decorations already exists with * the given [decoration type](#TextEditorDecorationType), they will be replaced. diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 7af57e7eb0e..dbacd611ed6 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -34,7 +34,7 @@ import { IWorkspaceConfigurationValues } from 'vs/workbench/services/configurati import { IPickOpenEntry, IPickOptions } from 'vs/platform/quickOpen/common/quickOpen'; import { SaveReason } from 'vs/workbench/services/textfile/common/textfiles'; import { IWorkspaceSymbol } from 'vs/workbench/parts/search/common/search'; -import { IApplyEditsOptions, TextEditorRevealType, ITextEditorConfigurationUpdate, IResolvedTextEditorConfiguration, ISelectionChangeEvent } from './mainThreadEditorsTracker'; +import { IApplyEditsOptions, IUndoStopOptions, TextEditorRevealType, ITextEditorConfigurationUpdate, IResolvedTextEditorConfiguration, ISelectionChangeEvent } from './mainThreadEditorsTracker'; import { InternalTreeExplorerNodeContent } from 'vs/workbench/parts/explorers/common/treeExplorerViewModel'; @@ -138,6 +138,7 @@ export abstract class MainThreadEditorsShape { $tryRevealRange(id: string, range: editorCommon.IRange, revealType: TextEditorRevealType): TPromise { throw ni(); } $trySetSelections(id: string, selections: editorCommon.ISelection[]): TPromise { throw ni(); } $tryApplyEdits(id: string, modelVersionId: number, edits: editorCommon.ISingleEditOperation[], opts: IApplyEditsOptions): TPromise { throw ni(); } + $tryInsertSnippet(id: string, template: string, opts: IUndoStopOptions): TPromise { throw ni(); } } export abstract class MainThreadTreeExplorersShape { diff --git a/src/vs/workbench/api/node/extHostEditors.ts b/src/vs/workbench/api/node/extHostEditors.ts index 7f1b0e57cd5..3a65e8da293 100644 --- a/src/vs/workbench/api/node/extHostEditors.ts +++ b/src/vs/workbench/api/node/extHostEditors.ts @@ -12,7 +12,7 @@ import Event, { Emitter } from 'vs/base/common/event'; import { TPromise } from 'vs/base/common/winjs.base'; import { IThreadService } from 'vs/workbench/services/thread/common/threadService'; import { ExtHostDocuments, ExtHostDocumentData } from 'vs/workbench/api/node/extHostDocuments'; -import { Selection, Range, Position, EndOfLine, TextEditorRevealType, TextEditorSelectionChangeKind, TextEditorLineNumbersStyle } from './extHostTypes'; +import { Selection, Range, Position, EndOfLine, TextEditorRevealType, TextEditorSelectionChangeKind, TextEditorLineNumbersStyle, SnippetString } from './extHostTypes'; import { ISingleEditOperation, TextEditorCursorStyle } from 'vs/editor/common/editorCommon'; import { IResolvedTextEditorConfiguration, ISelectionChangeEvent, ITextEditorConfigurationUpdate } from 'vs/workbench/api/node/mainThreadEditorsTracker'; import * as TypeConverters from './extHostTypeConverters'; @@ -595,10 +595,17 @@ class ExtHostTextEditor implements vscode.TextEditor { // ---- editing - edit(callback: (edit: TextEditorEdit) => void, options: { undoStopBefore: boolean; undoStopAfter: boolean; } = { undoStopBefore: true, undoStopAfter: true }): Thenable { - let edit = new TextEditorEdit(this._documentData.document, options); - callback(edit); - return this._applyEdit(edit); + edit(callback: (edit: TextEditorEdit) => void, options: { undoStopBefore: boolean; undoStopAfter: boolean; }): Thenable; + edit(snippet: SnippetString, options: { undoStopBefore: boolean; undoStopAfter: boolean; }): Thenable; + + edit(callbackOrSnippet: ((edit: TextEditorEdit) => void) | SnippetString, options: { undoStopBefore: boolean; undoStopAfter: boolean; } = { undoStopBefore: true, undoStopAfter: true }): Thenable { + if (SnippetString.isSnippetString(callbackOrSnippet)) { + return this._proxy.$tryInsertSnippet(this._id, callbackOrSnippet.value, options); + } else { + let edit = new TextEditorEdit(this._documentData.document, options); + callbackOrSnippet(edit); + return this._applyEdit(edit); + } } _applyEdit(editBuilder: TextEditorEdit): TPromise { diff --git a/src/vs/workbench/api/node/extHostTypes.ts b/src/vs/workbench/api/node/extHostTypes.ts index e7b26461e60..b74137ff36e 100644 --- a/src/vs/workbench/api/node/extHostTypes.ts +++ b/src/vs/workbench/api/node/extHostTypes.ts @@ -519,6 +519,16 @@ export class WorkspaceEdit { export class SnippetString { + static isSnippetString(thing: any): thing is SnippetString { + if (thing instanceof SnippetString) { + return true; + } + if (!thing) { + return false; + } + return typeof (thing).value === 'string'; + } + private static _escape(value: string): string { return value.replace(/\$|}|\\/g, '\\$&'); } diff --git a/src/vs/workbench/api/node/mainThreadEditors.ts b/src/vs/workbench/api/node/mainThreadEditors.ts index 631c1454e78..7061f854f3a 100644 --- a/src/vs/workbench/api/node/mainThreadEditors.ts +++ b/src/vs/workbench/api/node/mainThreadEditors.ts @@ -14,7 +14,7 @@ import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/edi import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; import { Position as EditorPosition } from 'vs/platform/editor/common/editor'; import { IModelService } from 'vs/editor/common/services/modelService'; -import { MainThreadEditorsTracker, TextEditorRevealType, MainThreadTextEditor, IApplyEditsOptions, ITextEditorConfigurationUpdate } from 'vs/workbench/api/node/mainThreadEditorsTracker'; +import { MainThreadEditorsTracker, TextEditorRevealType, MainThreadTextEditor, IApplyEditsOptions, IUndoStopOptions, ITextEditorConfigurationUpdate } from 'vs/workbench/api/node/mainThreadEditorsTracker'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { equals as arrayEquals } from 'vs/base/common/arrays'; import { equals as objectEquals } from 'vs/base/common/objects'; @@ -293,6 +293,13 @@ export class MainThreadEditors extends MainThreadEditorsShape { return TPromise.as(this._textEditorsMap[id].applyEdits(modelVersionId, edits, opts)); } + $tryInsertSnippet(id: string, template: string, opts: IUndoStopOptions): TPromise { + if (!this._textEditorsMap[id]) { + return TPromise.wrapError('TextEditor disposed'); + } + return TPromise.as(this._textEditorsMap[id].insertSnippet(template, opts)); + } + $registerTextEditorDecorationType(key: string, options: IDecorationRenderOptions): void { this._editorTracker.registerTextEditorDecorationType(key, options); } diff --git a/src/vs/workbench/api/node/mainThreadEditorsTracker.ts b/src/vs/workbench/api/node/mainThreadEditorsTracker.ts index f742f730926..127ff1c9373 100644 --- a/src/vs/workbench/api/node/mainThreadEditorsTracker.ts +++ b/src/vs/workbench/api/node/mainThreadEditorsTracker.ts @@ -14,6 +14,7 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { IdGenerator } from 'vs/base/common/idGenerator'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; +import { SnippetController } from 'vs/editor/contrib/snippet/common/snippetController'; import { EndOfLine, TextEditorLineNumbersStyle } from 'vs/workbench/api/node/extHostTypes'; export interface ITextEditorConfigurationUpdate { @@ -58,9 +59,12 @@ export enum TextEditorRevealType { InCenterIfOutsideViewport = 2 } -export interface IApplyEditsOptions { +export interface IUndoStopOptions { undoStopBefore: boolean; undoStopAfter: boolean; +} + +export interface IApplyEditsOptions extends IUndoStopOptions { setEndOfLine: EndOfLine; } @@ -383,6 +387,28 @@ export class MainThreadTextEditor { console.warn('applyEdits on invisible editor'); return false; } + + insertSnippet(template: string, opts: IUndoStopOptions) { + const snippetController = SnippetController.get(this._codeEditor); + + if (!this._codeEditor) { + return false; + } + + this._codeEditor.focus(); + + if (opts.undoStopBefore) { + this._codeEditor.pushUndoStop(); + } + + snippetController.insertSnippet(template, 0, 0); + + if (opts.undoStopAfter) { + this._codeEditor.pushUndoStop(); + } + + return true; + } } /** diff --git a/src/vs/workbench/test/node/api/extHostEditors.test.ts b/src/vs/workbench/test/node/api/extHostEditors.test.ts index 84de2253a89..05490a666f9 100644 --- a/src/vs/workbench/test/node/api/extHostEditors.test.ts +++ b/src/vs/workbench/test/node/api/extHostEditors.test.ts @@ -34,6 +34,7 @@ suite('ExtHostTextEditorOptions', () => { $tryRevealRange: undefined, $trySetSelections: undefined, $tryApplyEdits: undefined, + $tryInsertSnippet: undefined }; opts = new ExtHostTextEditorOptions(mockProxy, '1', { tabSize: 4,