diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/editor.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/editor.test.ts index 20bf80b0b05..f9a94ea4d7c 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/editor.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/editor.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { workspace, window, Position, Range, commands, TextEditor, TextDocument, TextEditorCursorStyle, TextEditorLineNumbersStyle, SnippetString, Selection, Uri } from 'vscode'; +import { workspace, window, Position, Range, commands, TextEditor, TextDocument, TextEditorCursorStyle, TextEditorLineNumbersStyle, SnippetString, Selection, Uri, env } from 'vscode'; import { createRandomFile, deleteFile, closeAllEditors } from '../utils'; suite('vscode API - editors', () => { @@ -47,6 +47,20 @@ suite('vscode API - editors', () => { }); }); + test('insert snippet with clipboard variables', async () => { + + await env.clipboard.writeText('INTEGRATION-TESTS'); + + const snippetString = new SnippetString('running: $CLIPBOARD'); + + return withRandomFileEditor('', async (editor, doc) => { + const inserted = await editor.insertSnippet(snippetString); + assert.ok(inserted); + assert.equal(doc.getText(), 'running: INTEGRATION-TESTS'); + assert.ok(doc.isDirty); + }); + }); + test('insert snippet with replacement, editor selection', () => { const snippetString = new SnippetString() .appendText('has been'); diff --git a/src/vs/editor/contrib/snippet/snippetController2.ts b/src/vs/editor/contrib/snippet/snippetController2.ts index 8005cf5badf..608c33074ed 100644 --- a/src/vs/editor/contrib/snippet/snippetController2.ts +++ b/src/vs/editor/contrib/snippet/snippetController2.ts @@ -45,6 +45,10 @@ export class SnippetController2 implements IEditorContribution { return editor.getContribution(SnippetController2.ID); } + static guessNeedsClipboard(template: string): boolean { + return /\${?CLIPBOARD/.test(template); + } + static readonly InSnippetMode = new RawContextKey('inSnippetMode', false); static readonly HasNextTabstop = new RawContextKey('hasNextTabstop', false); static readonly HasPrevTabstop = new RawContextKey('hasPrevTabstop', false); diff --git a/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts b/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts index a8cd6ddfee8..a14b9720272 100644 --- a/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts @@ -29,6 +29,7 @@ import { ITextFileService } from 'vs/workbench/services/textfile/common/textfile import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; namespace delta { @@ -331,6 +332,7 @@ export class MainThreadDocumentsAndEditors { @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService, @IUriIdentityService uriIdentityService: IUriIdentityService, + @IClipboardService private readonly _clipboardService: IClipboardService, ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDocumentsAndEditors); @@ -365,7 +367,7 @@ export class MainThreadDocumentsAndEditors { // added editors for (const apiEditor of delta.addedEditors) { const mainThreadEditor = new MainThreadTextEditor(apiEditor.id, apiEditor.editor.getModel(), - apiEditor.editor, { onGainedFocus() { }, onLostFocus() { } }, this._modelService); + apiEditor.editor, { onGainedFocus() { }, onLostFocus() { } }, this._modelService, this._clipboardService); this._textEditors.set(apiEditor.id, mainThreadEditor); addedEditors.push(mainThreadEditor); diff --git a/src/vs/workbench/api/browser/mainThreadEditor.ts b/src/vs/workbench/api/browser/mainThreadEditor.ts index 1cb9c614eb4..f6b8b3ccad0 100644 --- a/src/vs/workbench/api/browser/mainThreadEditor.ts +++ b/src/vs/workbench/api/browser/mainThreadEditor.ts @@ -17,6 +17,8 @@ import { IApplyEditsOptions, IEditorPropertiesChangeData, IResolvedTextEditorCon import { IEditorPane } from 'vs/workbench/common/editor'; import { withNullAsUndefined } from 'vs/base/common/types'; import { equals } from 'vs/base/common/arrays'; +import { CodeEditorStateFlag, EditorState } from 'vs/editor/browser/core/editorState'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; export interface IFocusTracker { onGainedFocus(): void; @@ -159,6 +161,7 @@ export class MainThreadTextEditor { private readonly _id: string; private _model: ITextModel; private readonly _modelService: IModelService; + private readonly _clipboardService: IClipboardService; private readonly _modelListeners = new DisposableStore(); private _codeEditor: ICodeEditor | null; private readonly _focusTracker: IFocusTracker; @@ -172,7 +175,8 @@ export class MainThreadTextEditor { model: ITextModel, codeEditor: ICodeEditor, focusTracker: IFocusTracker, - modelService: IModelService + modelService: IModelService, + clipboardService: IClipboardService, ) { this._id = id; this._model = model; @@ -180,6 +184,7 @@ export class MainThreadTextEditor { this._properties = null; this._focusTracker = focusTracker; this._modelService = modelService; + this._clipboardService = clipboardService; this._onPropertiesChanged = new Emitter(); @@ -454,12 +459,23 @@ export class MainThreadTextEditor { return true; } - insertSnippet(template: string, ranges: readonly IRange[], opts: IUndoStopOptions) { + async insertSnippet(template: string, ranges: readonly IRange[], opts: IUndoStopOptions) { - if (!this._codeEditor) { + if (!this._codeEditor || !this._codeEditor.hasModel()) { return false; } + // check if clipboard is required and only iff read it (async) + let clipboardText: string | undefined; + const needsTemplate = SnippetController2.guessNeedsClipboard(template); + if (needsTemplate) { + const state = new EditorState(this._codeEditor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Position); + clipboardText = await this._clipboardService.readText(); + if (!state.validate(this._codeEditor)) { + return false; + } + } + const snippetController = SnippetController2.get(this._codeEditor); // // cancel previous snippet mode @@ -471,7 +487,11 @@ export class MainThreadTextEditor { this._codeEditor.focus(); // make modifications - snippetController.insert(template, { overwriteBefore: 0, overwriteAfter: 0, undoStopBefore: opts.undoStopBefore, undoStopAfter: opts.undoStopAfter }); + snippetController.insert(template, { + overwriteBefore: 0, overwriteAfter: 0, + undoStopBefore: opts.undoStopBefore, undoStopAfter: opts.undoStopAfter, + clipboardText + }); return true; } diff --git a/src/vs/workbench/contrib/snippets/test/browser/snippetFile.test.ts b/src/vs/workbench/contrib/snippets/test/browser/snippetFile.test.ts index a804f261f37..b94ee44d3a0 100644 --- a/src/vs/workbench/contrib/snippets/test/browser/snippetFile.test.ts +++ b/src/vs/workbench/contrib/snippets/test/browser/snippetFile.test.ts @@ -6,6 +6,7 @@ import * as assert from 'assert'; import { SnippetFile, Snippet, SnippetSource } from 'vs/workbench/contrib/snippets/browser/snippetsFile'; import { URI } from 'vs/base/common/uri'; +import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; suite('Snippets', function () { @@ -70,6 +71,8 @@ suite('Snippets', function () { function assertNeedsClipboard(body: string, expected: boolean): void { let snippet = new Snippet(['foo'], 'FooSnippet1', 'foo', '', body, 'test', SnippetSource.User); assert.equal(snippet.needsClipboard, expected); + + assert.equal(SnippetController2.guessNeedsClipboard(body), expected); } assertNeedsClipboard('foo$CLIPBOARD', true); diff --git a/src/vs/workbench/test/browser/api/mainThreadDocumentsAndEditors.test.ts b/src/vs/workbench/test/browser/api/mainThreadDocumentsAndEditors.test.ts index 48b2da2c996..62362bba2da 100644 --- a/src/vs/workbench/test/browser/api/mainThreadDocumentsAndEditors.test.ts +++ b/src/vs/workbench/test/browser/api/mainThreadDocumentsAndEditors.test.ts @@ -27,6 +27,7 @@ import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogSer import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { TestTextResourcePropertiesService, TestWorkingCopyFileService } from 'vs/workbench/test/common/workbenchTestServices'; import { UriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentityService'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; suite('MainThreadDocumentsAndEditors', () => { @@ -92,6 +93,11 @@ suite('MainThreadDocumentsAndEditors', () => { TestEnvironmentService, new TestWorkingCopyFileService(), new UriIdentityService(fileService), + new class extends mock() { + readText() { + return Promise.resolve('clipboard_contents'); + } + } ); });