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

Adding an overload to TextEditor.edit for insertion of a SnippetString #17628

Merged
merged 13 commits into from
Jan 20, 2017
Merged
31 changes: 30 additions & 1 deletion extensions/vscode-api-tests/src/editor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } from 'vscode';
import { createRandomFile, deleteFile, cleanUp } from './utils';

suite('editor tests', () => {
Expand All @@ -33,6 +33,35 @@ 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.insertSnippet(snippetString.value, new Position(0, 0)).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) => {
return editor.insertSnippet(snippetString.value, new Range(0, 5, 0, 12)).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) => {
Expand Down
1 change: 1 addition & 0 deletions extensions/vscode-api-tests/src/typings/ref.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

/// <reference path='../../../../src/vs/vscode.proposed.d.ts'/>
/// <reference path='../../../../src/typings/mocha.d.ts'/>
/// <reference path='../../../../extensions/declares.d.ts'/>
/// <reference path='../../../../extensions/node.d.ts'/>
Expand Down
9 changes: 9 additions & 0 deletions src/vs/editor/contrib/snippet/common/snippetController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,11 +466,20 @@ export class SnippetController {
return SnippetController.ID;
}

public get inSnippetMode() {
return this._inSnippetMode.get();
}

public insertSnippet(template: string, overwriteBefore: number, overwriteAfter: number): void {
const snippet = CodeSnippet.fromTextmate(template, this._variableResolver);
this.run(snippet, overwriteBefore, overwriteAfter);
}

public insertSnippetWithReplaceRange(template: string, replaceRange: Range): void {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should try to get away with just using insertSnippet. The controller is already complex

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that this could be consolidated.

const snippet = CodeSnippet.fromTextmate(template, this._variableResolver);
this.runWithReplaceRange(snippet, replaceRange);
}

public run(snippet: CodeSnippet, overwriteBefore: number, overwriteAfter: number, stripPrefix?: boolean): void {
this._runAndRestoreController(() => {
if (snippet.isInsertOnly || snippet.isSingleTabstopOnly) {
Expand Down
11 changes: 11 additions & 0 deletions src/vs/vscode.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -967,6 +967,17 @@ declare module 'vscode' {
*/
edit(callback: (editBuilder: TextEditorEdit) => void, options?: { undoStopBefore: boolean; undoStopAfter: boolean; }): Thenable<boolean>;

/**
* Inserts the given snippet template and enters snippet mode.
*
* If the editor is already in snippet mode, insertion fails and the returned promise resolves to false.
*
* @param template The snippet template to insert
* @param posOrRange The position or replacement range representing the location of the insertion.
* @return A promise that resolves with a value indicating if the snippet could be inserted.
*/
insertSnippet(template: string, posOrRange: Position | Range): Thenable<boolean>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • template should be of type SnippetString
  • Unsure about posOrRange - we should consider using the current editor selection/selections. That would allow to insert snippets in multiple locations at the same time. Also inserting snippets is highly interactive, so it's fair to use the current selection for that (or change the selection first).
  • We should consider making this an overload of the edit method

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that you should be able to call this without being concerned with location. Sharing an argument for both position or range seems a little awkward as well. So long as I can alter the selection before starting. How should this work for multiple selections?

In the case of overloading TextEditor.edit, if that involves changes to TextEditorEdit, how does entering snippet mode fit in with subsequent insertions, replacements, etc.? It seems like this might have more in common with CompletionItem than editing.

Another thought: Should we supply a callback to know when snippet mode was exited?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How should this work for multiple selections?
As with a single selection, the editor API allows to set one or many selections. The snippet logic tho isn't yet smart enough to always honour multiple cursors when snippets are inserted. We have a issue for this and it will be implemented in the near future

how does entering snippet mode fit in with subsequent insertions, replacements, etc.? It seems like this might have more in common with CompletionItem than editing.

Sure, I was just wondering if reusing the name edit makes sense because that is sort of what we do. It doesn't have to fit in the builder but could just reuse the name, like so:

edit(snippet: SnippetString, options?:{/*undo stops control*/}): void;

edit(callback: (editBuilder: TextEditorEdit) => void, options?: { undoStopBefore: boolean; undoStopAfter: boolean; }): Thenable<boolean>;

Another thought: Should we supply a callback to know when snippet mode was exited?

Not a fan because it really depends on the user tabbing through things to exit snippet mode.


/**
* 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.
Expand Down
12 changes: 12 additions & 0 deletions src/vs/vscode.proposed.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,16 @@ declare module 'vscode' {
*/
getClickCommand?(node: T): string;
}
export interface TextEditor {
/**
* Inserts the given snippet template and enters snippet mode.
*
* If the editor is already in snippet mode, insertion fails and the returned promise resolves to false.
*
* @param template The snippet template to insert
* @param posOrRange The position or replacement range representing the location of the insertion.
* @return A promise that resolves with a value indicating if the snippet could be inserted.
*/
insertSnippet(template: string, posOrRange: Position | Range): Thenable<boolean>;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this isn't needed as we make the change directly in vscode.d.ts

Copy link
Contributor Author

@joelday joelday Jan 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't think so, but the extensions project pulls from the publicly published vscode package instead of the vscode.d.ts in the main project. That's why I got the first build failure in the PR.

}
1 change: 1 addition & 0 deletions src/vs/workbench/api/node/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export abstract class MainThreadEditorsShape {
$tryRevealRange(id: string, range: editorCommon.IRange, revealType: TextEditorRevealType): TPromise<any> { throw ni(); }
$trySetSelections(id: string, selections: editorCommon.ISelection[]): TPromise<any> { throw ni(); }
$tryApplyEdits(id: string, modelVersionId: number, edits: editorCommon.ISingleEditOperation[], opts: IApplyEditsOptions): TPromise<boolean> { throw ni(); }
$tryInsertSnippet(id: string, template: string, posOrRange: editorCommon.IPosition | editorCommon.IRange): TPromise<boolean> { throw ni(); }
}

export abstract class MainThreadTreeExplorersShape {
Expand Down
18 changes: 17 additions & 1 deletion src/vs/workbench/api/node/extHostEditors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ 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 { ISingleEditOperation, TextEditorCursorStyle } from 'vs/editor/common/editorCommon';
import { ISingleEditOperation, TextEditorCursorStyle, IPosition, IRange } from 'vs/editor/common/editorCommon';
import { IResolvedTextEditorConfiguration, ISelectionChangeEvent, ITextEditorConfigurationUpdate } from 'vs/workbench/api/node/mainThreadEditorsTracker';
import * as TypeConverters from './extHostTypeConverters';
import { MainContext, MainThreadEditorsShape, ExtHostEditorsShape, ITextEditorAddData, ITextEditorPositionData } from './extHost.protocol';
Expand Down Expand Up @@ -620,6 +620,22 @@ class ExtHostTextEditor implements vscode.TextEditor {
});
}

insertSnippet(template: string, posOrRange: Position | Range) {
let convertedPosOrRange: IPosition | IRange;

if (Position.isPosition(posOrRange)) {
convertedPosOrRange = TypeConverters.fromPosition(posOrRange);
}
else if (Range.isRange(posOrRange)) {
convertedPosOrRange = TypeConverters.fromRange(posOrRange);
}
else {
return TPromise.wrapError(new Error('Unrecognized value for posOrRange'));
}

return this._proxy.$tryInsertSnippet(this._id, template, convertedPosOrRange);
}

// ---- util

private _runOnProxy(callback: () => TPromise<any>, silent: boolean): TPromise<ExtHostTextEditor> {
Expand Down
9 changes: 8 additions & 1 deletion src/vs/workbench/api/node/mainThreadEditors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import URI from 'vs/base/common/uri';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { TPromise } from 'vs/base/common/winjs.base';
import { IThreadService } from 'vs/workbench/services/thread/common/threadService';
import { ISingleEditOperation, ISelection, IRange, IEditor, EditorType, ICommonCodeEditor, ICommonDiffEditor, IDecorationRenderOptions, IDecorationOptions } from 'vs/editor/common/editorCommon';
import { ISingleEditOperation, ISelection, IPosition, IRange, IEditor, EditorType, ICommonCodeEditor, ICommonDiffEditor, IDecorationRenderOptions, IDecorationOptions } from 'vs/editor/common/editorCommon';
import { ICodeEditorService } from 'vs/editor/common/services/codeEditorService';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
Expand Down Expand Up @@ -293,6 +293,13 @@ export class MainThreadEditors extends MainThreadEditorsShape {
return TPromise.as(this._textEditorsMap[id].applyEdits(modelVersionId, edits, opts));
}

$tryInsertSnippet(id: string, template: string, posOrRange: IPosition | IRange): TPromise<boolean> {
if (!this._textEditorsMap[id]) {
return TPromise.wrapError('TextEditor disposed');
}
return TPromise.as(this._textEditorsMap[id].insertSnippet(template, posOrRange));
}

$registerTextEditorDecorationType(key: string, options: IDecorationRenderOptions): void {
this._editorTracker.registerTextEditorDecorationType(key, options);
}
Expand Down
25 changes: 25 additions & 0 deletions src/vs/workbench/api/node/mainThreadEditorsTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import { IModelService } from 'vs/editor/common/services/modelService';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { RunOnceScheduler } from 'vs/base/common/async';
import { IdGenerator } from 'vs/base/common/idGenerator';
import { Position } from 'vs/editor/common/core/position';
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 {
Expand Down Expand Up @@ -383,6 +385,29 @@ export class MainThreadTextEditor {
console.warn('applyEdits on invisible editor');
return false;
}

insertSnippet(template: string, posOrRange: EditorCommon.IPosition | EditorCommon.IRange) {
const snippetController = SnippetController.get(this._codeEditor);
if (snippetController.inSnippetMode) {
return false;
}

const range = Range.isIRange(posOrRange) ? Range.lift(posOrRange) : null;
const position = Position.isIPosition(posOrRange) ? Position.lift(posOrRange) : range.getStartPosition();

this._codeEditor.setPosition(position);
this._codeEditor.revealLine(position.lineNumber);
this._codeEditor.focus();

if (range) {
snippetController.insertSnippetWithReplaceRange(template, range);
}
else {
snippetController.insertSnippet(template, 0, 0);
}

return true;
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/vs/workbench/test/node/api/extHostEditors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ suite('ExtHostTextEditorOptions', () => {
$tryRevealRange: undefined,
$trySetSelections: undefined,
$tryApplyEdits: undefined,
$tryInsertSnippet: undefined
};
opts = new ExtHostTextEditorOptions(mockProxy, '1', {
tabSize: 4,
Expand Down