mirror of
https://github.com/microsoft/vscode.git
synced 2026-09-12 11:39:57 +01:00
Initial work on signature help context (#58135)
* Initial work on signature help context Fixes #54972 Adds `SignatureHelpContext`. This tells providers why signature help was requested TODO: - [ ] Better understand semantics of retrigger. Should `retrigger` be an flag instead of a `triggerReason`? - [ ] Fix skipped test - [ ] Add more tests for trigger reasons / trigger characters * Fix unit test * Make sure we retrigger sig help if it is already showing * Add test for dismiss and re-invoke * Extract some constants * Extract createMockEditor
This commit is contained in:
@@ -20,13 +20,17 @@ class TypeScriptSignatureHelpProvider implements vscode.SignatureHelpProvider {
|
||||
public async provideSignatureHelp(
|
||||
document: vscode.TextDocument,
|
||||
position: vscode.Position,
|
||||
token: vscode.CancellationToken
|
||||
token: vscode.CancellationToken,
|
||||
context?: vscode.SignatureHelpContext,
|
||||
): Promise<vscode.SignatureHelp | undefined> {
|
||||
const filepath = this.client.toPath(document.uri);
|
||||
if (!filepath) {
|
||||
return undefined;
|
||||
}
|
||||
const args: Proto.SignatureHelpRequestArgs = typeConverters.Position.toFileLocationRequestArgs(filepath, position);
|
||||
const args: Proto.SignatureHelpRequestArgs = {
|
||||
...typeConverters.Position.toFileLocationRequestArgs(filepath, position),
|
||||
triggerReason: toTsTriggerReason(context!)
|
||||
};
|
||||
|
||||
let info: Proto.SignatureHelpItems;
|
||||
try {
|
||||
@@ -71,6 +75,23 @@ class TypeScriptSignatureHelpProvider implements vscode.SignatureHelpProvider {
|
||||
}
|
||||
}
|
||||
|
||||
function toTsTriggerReason(context: vscode.SignatureHelpContext): Proto.SignatureHelpTriggerReason {
|
||||
switch (context.triggerReason) {
|
||||
case vscode.SignatureHelpTriggerReason.Retrigger:
|
||||
return { kind: 'retrigger' };
|
||||
|
||||
case vscode.SignatureHelpTriggerReason.TriggerCharacter:
|
||||
if (context.triggerCharacter) {
|
||||
return { kind: 'characterTyped', triggerCharacter: context.triggerCharacter as any };
|
||||
} else {
|
||||
return { kind: 'invoked' };
|
||||
}
|
||||
|
||||
case vscode.SignatureHelpTriggerReason.Invoke:
|
||||
default:
|
||||
return { kind: 'invoked' };
|
||||
}
|
||||
}
|
||||
export function register(
|
||||
selector: vscode.DocumentSelector,
|
||||
client: ITypeScriptServiceClient,
|
||||
|
||||
@@ -447,6 +447,18 @@ export interface SignatureHelp {
|
||||
*/
|
||||
activeParameter: number;
|
||||
}
|
||||
|
||||
export enum SignatureHelpTriggerReason {
|
||||
Invoke = 1,
|
||||
TriggerCharacter = 2,
|
||||
Retrigger = 3,
|
||||
}
|
||||
|
||||
export interface SignatureHelpContext {
|
||||
triggerReason: SignatureHelpTriggerReason;
|
||||
triggerCharacter?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The signature help provider interface defines the contract between extensions and
|
||||
* the [parameter hints](https://code.visualstudio.com/docs/editor/intellisense)-feature.
|
||||
@@ -458,7 +470,7 @@ export interface SignatureHelpProvider {
|
||||
/**
|
||||
* Provide help for the signature at the given position and document.
|
||||
*/
|
||||
provideSignatureHelp(model: model.ITextModel, position: Position, token: CancellationToken): ProviderResult<SignatureHelp>;
|
||||
provideSignatureHelp(model: model.ITextModel, position: Position, token: CancellationToken, context: SignatureHelpContext): ProviderResult<SignatureHelp>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,6 +16,7 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { ParameterHintsWidget } from './parameterHintsWidget';
|
||||
import { Context } from 'vs/editor/contrib/parameterHints/provideSignatureHelp';
|
||||
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import * as modes from 'vs/editor/common/modes';
|
||||
|
||||
class ParameterHintsController implements IEditorContribution {
|
||||
|
||||
@@ -49,8 +50,8 @@ class ParameterHintsController implements IEditorContribution {
|
||||
this.widget.next();
|
||||
}
|
||||
|
||||
trigger(): void {
|
||||
this.widget.trigger();
|
||||
trigger(context: modes.SignatureHelpContext): void {
|
||||
this.widget.trigger(context);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
@@ -77,7 +78,7 @@ export class TriggerParameterHintsAction extends EditorAction {
|
||||
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
|
||||
let controller = ParameterHintsController.get(editor);
|
||||
if (controller) {
|
||||
controller.trigger();
|
||||
controller.trigger({ triggerReason: modes.SignatureHelpTriggerReason.Invoke });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import * as nls from 'vs/nls';
|
||||
import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import * as aria from 'vs/base/browser/ui/aria/aria';
|
||||
import { SignatureHelp, SignatureInformation, SignatureHelpProviderRegistry } from 'vs/editor/common/modes';
|
||||
import * as modes from 'vs/editor/common/modes';
|
||||
import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser';
|
||||
import { RunOnceScheduler, createCancelablePromise, CancelablePromise } from 'vs/base/common/async';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
@@ -31,12 +31,12 @@ import { MarkdownRenderer } from 'vs/editor/contrib/markdown/markdownRenderer';
|
||||
const $ = dom.$;
|
||||
|
||||
export interface IHintEvent {
|
||||
hints: SignatureHelp;
|
||||
hints: modes.SignatureHelp;
|
||||
}
|
||||
|
||||
export class ParameterHintsModel extends Disposable {
|
||||
|
||||
static DELAY = 120; // ms
|
||||
private static readonly DEFAULT_DELAY = 120; // ms
|
||||
|
||||
private readonly _onHint = this._register(new Emitter<IHintEvent>());
|
||||
public readonly onHint: Event<IHintEvent> = this._onHint.event;
|
||||
@@ -47,27 +47,33 @@ export class ParameterHintsModel extends Disposable {
|
||||
private editor: ICodeEditor;
|
||||
private enabled: boolean;
|
||||
private triggerCharactersListeners: IDisposable[];
|
||||
private active: boolean;
|
||||
private throttledDelayer: RunOnceScheduler;
|
||||
private provideSignatureHelpRequest?: CancelablePromise<SignatureHelp>;
|
||||
private active: boolean = false;
|
||||
private pending: boolean = false;
|
||||
private triggerChars = new CharacterSet();
|
||||
|
||||
constructor(editor: ICodeEditor) {
|
||||
private triggerContext: modes.SignatureHelpContext | undefined;
|
||||
private throttledDelayer: RunOnceScheduler;
|
||||
private provideSignatureHelpRequest?: CancelablePromise<modes.SignatureHelp>;
|
||||
|
||||
constructor(
|
||||
editor: ICodeEditor,
|
||||
delay: number = ParameterHintsModel.DEFAULT_DELAY
|
||||
) {
|
||||
super();
|
||||
|
||||
this.editor = editor;
|
||||
this.enabled = false;
|
||||
this.triggerCharactersListeners = [];
|
||||
|
||||
this.throttledDelayer = new RunOnceScheduler(() => this.doTrigger(), ParameterHintsModel.DELAY);
|
||||
|
||||
this.active = false;
|
||||
this.throttledDelayer = new RunOnceScheduler(() => this.doTrigger(), delay);
|
||||
|
||||
this._register(this.editor.onDidChangeConfiguration(() => this.onEditorConfigurationChange()));
|
||||
this._register(this.editor.onDidChangeModel(e => this.onModelChanged()));
|
||||
this._register(this.editor.onDidChangeModelLanguage(_ => this.onModelChanged()));
|
||||
this._register(this.editor.onDidChangeCursorSelection(e => this.onCursorChange(e)));
|
||||
this._register(this.editor.onDidChangeModelContent(e => this.onModelContentChange()));
|
||||
this._register(SignatureHelpProviderRegistry.onDidChange(this.onModelChanged, this));
|
||||
this._register(modes.SignatureHelpProviderRegistry.onDidChange(this.onModelChanged, this));
|
||||
this._register(this.editor.onDidType(text => this.onDidType(text)));
|
||||
|
||||
this.onEditorConfigurationChange();
|
||||
this.onModelChanged();
|
||||
@@ -75,6 +81,8 @@ export class ParameterHintsModel extends Disposable {
|
||||
|
||||
cancel(silent: boolean = false): void {
|
||||
this.active = false;
|
||||
this.pending = false;
|
||||
this.triggerContext = undefined;
|
||||
|
||||
this.throttledDelayer.cancel();
|
||||
|
||||
@@ -88,12 +96,13 @@ export class ParameterHintsModel extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
trigger(delay = ParameterHintsModel.DELAY): void {
|
||||
if (!SignatureHelpProviderRegistry.has(this.editor.getModel())) {
|
||||
trigger(context: modes.SignatureHelpContext, delay?: number): void {
|
||||
if (!modes.SignatureHelpProviderRegistry.has(this.editor.getModel())) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.cancel(true);
|
||||
this.triggerContext = context;
|
||||
return this.throttledDelayer.schedule(delay);
|
||||
}
|
||||
|
||||
@@ -102,9 +111,17 @@ export class ParameterHintsModel extends Disposable {
|
||||
this.provideSignatureHelpRequest.cancel();
|
||||
}
|
||||
|
||||
this.provideSignatureHelpRequest = createCancelablePromise(token => provideSignatureHelp(this.editor.getModel(), this.editor.getPosition(), token));
|
||||
this.pending = true;
|
||||
|
||||
const triggerContext = this.triggerContext || { triggerReason: modes.SignatureHelpTriggerReason.Invoke };
|
||||
this.triggerContext = undefined;
|
||||
|
||||
this.provideSignatureHelpRequest = createCancelablePromise(token =>
|
||||
provideSignatureHelp(this.editor.getModel(), this.editor.getPosition(), triggerContext, token));
|
||||
|
||||
this.provideSignatureHelpRequest.then(result => {
|
||||
this.pending = false;
|
||||
|
||||
if (!result || !result.signatures || result.signatures.length === 0) {
|
||||
this.cancel();
|
||||
this._onCancel.fire(void 0);
|
||||
@@ -116,54 +133,63 @@ export class ParameterHintsModel extends Disposable {
|
||||
this._onHint.fire(event);
|
||||
return true;
|
||||
|
||||
}).catch(onUnexpectedError);
|
||||
}).catch(error => {
|
||||
this.pending = false;
|
||||
onUnexpectedError(error);
|
||||
});
|
||||
}
|
||||
|
||||
isTriggered(): boolean {
|
||||
return this.active || this.throttledDelayer.isScheduled();
|
||||
private get isTriggered(): boolean {
|
||||
return this.active || this.pending || this.throttledDelayer.isScheduled();
|
||||
}
|
||||
|
||||
private onModelChanged(): void {
|
||||
this.cancel();
|
||||
|
||||
this.triggerCharactersListeners = dispose(this.triggerCharactersListeners);
|
||||
// Update trigger characters
|
||||
this.triggerChars = new CharacterSet();
|
||||
|
||||
const model = this.editor.getModel();
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
|
||||
const triggerChars = new CharacterSet();
|
||||
for (const support of SignatureHelpProviderRegistry.ordered(model)) {
|
||||
for (const support of modes.SignatureHelpProviderRegistry.ordered(model)) {
|
||||
if (Array.isArray(support.signatureHelpTriggerCharacters)) {
|
||||
for (const ch of support.signatureHelpTriggerCharacters) {
|
||||
triggerChars.add(ch.charCodeAt(0));
|
||||
this.triggerChars.add(ch.charCodeAt(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.triggerCharactersListeners.push(this.editor.onDidType((text: string) => {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
private onDidType(text: string) {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (triggerChars.has(text.charCodeAt(text.length - 1))) {
|
||||
this.trigger();
|
||||
}
|
||||
}));
|
||||
const lastCharIndex = text.length - 1;
|
||||
if (this.triggerChars.has(text.charCodeAt(lastCharIndex))) {
|
||||
this.trigger({
|
||||
triggerReason: this.isTriggered
|
||||
? modes.SignatureHelpTriggerReason.Retrigger
|
||||
: modes.SignatureHelpTriggerReason.TriggerCharacter,
|
||||
triggerCharacter: text.charAt(lastCharIndex)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private onCursorChange(e: ICursorSelectionChangedEvent): void {
|
||||
if (e.source === 'mouse') {
|
||||
this.cancel();
|
||||
} else if (this.isTriggered()) {
|
||||
this.trigger();
|
||||
} else if (this.isTriggered) {
|
||||
this.trigger({ triggerReason: modes.SignatureHelpTriggerReason.Retrigger });
|
||||
}
|
||||
}
|
||||
|
||||
private onModelContentChange(): void {
|
||||
if (this.isTriggered()) {
|
||||
this.trigger();
|
||||
if (this.isTriggered) {
|
||||
this.trigger({ triggerReason: modes.SignatureHelpTriggerReason.Retrigger });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +224,7 @@ export class ParameterHintsWidget implements IContentWidget, IDisposable {
|
||||
private overloads: HTMLElement;
|
||||
private currentSignature: number;
|
||||
private visible: boolean;
|
||||
private hints: SignatureHelp;
|
||||
private hints: modes.SignatureHelp;
|
||||
private announcedLabel: string;
|
||||
private scrollbar: DomScrollableElement;
|
||||
private disposables: IDisposable[];
|
||||
@@ -405,7 +431,7 @@ export class ParameterHintsWidget implements IContentWidget, IDisposable {
|
||||
this.scrollbar.scanDomNode();
|
||||
}
|
||||
|
||||
private renderParameters(parent: HTMLElement, signature: SignatureInformation, currentParameter: number): void {
|
||||
private renderParameters(parent: HTMLElement, signature: modes.SignatureInformation, currentParameter: number): void {
|
||||
let end = signature.label.length;
|
||||
let idx = 0;
|
||||
let element: HTMLSpanElement;
|
||||
@@ -526,8 +552,8 @@ export class ParameterHintsWidget implements IContentWidget, IDisposable {
|
||||
return ParameterHintsWidget.ID;
|
||||
}
|
||||
|
||||
trigger(): void {
|
||||
this.model.trigger(0);
|
||||
trigger(context: modes.SignatureHelpContext): void {
|
||||
this.model.trigger(context, 0);
|
||||
}
|
||||
|
||||
private updateMaxHeight(): void {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { onUnexpectedExternalError } from 'vs/base/common/errors';
|
||||
import { registerDefaultLanguageCommand } from 'vs/editor/browser/editorExtensions';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { SignatureHelp, SignatureHelpProviderRegistry } from 'vs/editor/common/modes';
|
||||
import * as modes from 'vs/editor/common/modes';
|
||||
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
|
||||
@@ -17,13 +17,14 @@ export const Context = {
|
||||
MultipleSignatures: new RawContextKey<boolean>('parameterHintsMultipleSignatures', false),
|
||||
};
|
||||
|
||||
export function provideSignatureHelp(model: ITextModel, position: Position, token: CancellationToken): Promise<SignatureHelp> {
|
||||
export function provideSignatureHelp(model: ITextModel, position: Position, context: modes.SignatureHelpContext, token: CancellationToken): Promise<modes.SignatureHelp> {
|
||||
|
||||
const supports = SignatureHelpProviderRegistry.ordered(model);
|
||||
const supports = modes.SignatureHelpProviderRegistry.ordered(model);
|
||||
|
||||
return first2(supports.map(support => () => {
|
||||
return Promise.resolve(support.provideSignatureHelp(model, position, token)).catch(onUnexpectedExternalError);
|
||||
return Promise.resolve(support.provideSignatureHelp(model, position, token, context)).catch(onUnexpectedExternalError);
|
||||
}));
|
||||
}
|
||||
|
||||
registerDefaultLanguageCommand('_executeSignatureHelpProvider', (model, position) => provideSignatureHelp(model, position, CancellationToken.None));
|
||||
registerDefaultLanguageCommand('_executeSignatureHelpProvider', (model, position) =>
|
||||
provideSignatureHelp(model, position, { triggerReason: modes.SignatureHelpTriggerReason.Invoke }, CancellationToken.None));
|
||||
|
||||
@@ -5,60 +5,214 @@
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Handler } from 'vs/editor/common/editorCommon';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { TextModel } from 'vs/editor/common/model/textModel';
|
||||
import { SignatureHelp, SignatureHelpProvider, SignatureHelpProviderRegistry } from 'vs/editor/common/modes';
|
||||
import { TestCodeEditor, createTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor';
|
||||
import * as modes from 'vs/editor/common/modes';
|
||||
import { createTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor';
|
||||
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
|
||||
import { IStorageService, NullStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
|
||||
import { ParameterHintsModel } from '../parameterHintsWidget';
|
||||
|
||||
function createMockEditor(model: TextModel): TestCodeEditor {
|
||||
return createTestCodeEditor({
|
||||
model: model,
|
||||
serviceCollection: new ServiceCollection(
|
||||
[ITelemetryService, NullTelemetryService],
|
||||
[IStorageService, NullStorageService]
|
||||
)
|
||||
});
|
||||
}
|
||||
const mockFile = URI.parse('test:somefile.ttt');
|
||||
const mockFileSelector = { scheme: 'test' };
|
||||
|
||||
|
||||
const emptySigHelpResult = {
|
||||
signatures: [{
|
||||
label: 'none',
|
||||
parameters: []
|
||||
}],
|
||||
activeParameter: 0,
|
||||
activeSignature: 0
|
||||
};
|
||||
suite('ParameterHintsModel', () => {
|
||||
let disposables: IDisposable[] = [];
|
||||
|
||||
|
||||
setup(function () {
|
||||
disposables = dispose(disposables);
|
||||
});
|
||||
|
||||
test('Should cancel existing request when new request comes in', () => {
|
||||
const textModel = TextModel.createFromString('abc def', undefined, undefined, URI.parse('test:somefile.ttt'));
|
||||
function createMockEditor(fileContents: string) {
|
||||
const textModel = TextModel.createFromString(fileContents, undefined, undefined, mockFile);
|
||||
const editor = createTestCodeEditor({
|
||||
model: textModel,
|
||||
serviceCollection: new ServiceCollection(
|
||||
[ITelemetryService, NullTelemetryService],
|
||||
[IStorageService, NullStorageService]
|
||||
)
|
||||
});
|
||||
disposables.push(textModel);
|
||||
disposables.push(editor);
|
||||
return editor;
|
||||
}
|
||||
|
||||
const editor = createMockEditor(textModel);
|
||||
test('Provider should get trigger character on type', (done) => {
|
||||
const triggerChar = '(';
|
||||
|
||||
const editor = createMockEditor('');
|
||||
disposables.push(new ParameterHintsModel(editor));
|
||||
|
||||
disposables.push(modes.SignatureHelpProviderRegistry.register(mockFileSelector, new class implements modes.SignatureHelpProvider {
|
||||
signatureHelpTriggerCharacters = [triggerChar];
|
||||
|
||||
provideSignatureHelp(_model: ITextModel, _position: Position, _token: CancellationToken, context: modes.SignatureHelpContext): modes.SignatureHelp | Thenable<modes.SignatureHelp> {
|
||||
assert.strictEqual(context.triggerReason, modes.SignatureHelpTriggerReason.TriggerCharacter);
|
||||
assert.strictEqual(context.triggerCharacter, triggerChar);
|
||||
done();
|
||||
return undefined;
|
||||
}
|
||||
}));
|
||||
|
||||
editor.trigger('keyboard', Handler.Type, { text: triggerChar });
|
||||
});
|
||||
|
||||
test('Provider should be retriggered if already active', (done) => {
|
||||
const triggerChar = '(';
|
||||
|
||||
const editor = createMockEditor('');
|
||||
disposables.push(new ParameterHintsModel(editor));
|
||||
|
||||
let invokeCount = 0;
|
||||
disposables.push(modes.SignatureHelpProviderRegistry.register(mockFileSelector, new class implements modes.SignatureHelpProvider {
|
||||
signatureHelpTriggerCharacters = [triggerChar];
|
||||
|
||||
provideSignatureHelp(_model: ITextModel, _position: Position, _token: CancellationToken, context: modes.SignatureHelpContext): modes.SignatureHelp | Thenable<modes.SignatureHelp> {
|
||||
++invokeCount;
|
||||
if (invokeCount === 1) {
|
||||
assert.strictEqual(context.triggerReason, modes.SignatureHelpTriggerReason.TriggerCharacter);
|
||||
assert.strictEqual(context.triggerCharacter, triggerChar);
|
||||
// Retrigger
|
||||
editor.trigger('keyboard', Handler.Type, { text: triggerChar });
|
||||
} else {
|
||||
assert.strictEqual(invokeCount, 2);
|
||||
assert.strictEqual(context.triggerReason, modes.SignatureHelpTriggerReason.Retrigger);
|
||||
assert.strictEqual(context.triggerCharacter, triggerChar);
|
||||
done();
|
||||
}
|
||||
return emptySigHelpResult;
|
||||
}
|
||||
}));
|
||||
|
||||
editor.trigger('keyboard', Handler.Type, { text: triggerChar });
|
||||
});
|
||||
|
||||
test('Provider should not be retriggered if previous help is canceled first', (done) => {
|
||||
const triggerChar = '(';
|
||||
|
||||
const editor = createMockEditor('');
|
||||
const hintModel = new ParameterHintsModel(editor);
|
||||
disposables.push(hintModel);
|
||||
|
||||
let invokeCount = 0;
|
||||
disposables.push(modes.SignatureHelpProviderRegistry.register(mockFileSelector, new class implements modes.SignatureHelpProvider {
|
||||
signatureHelpTriggerCharacters = [triggerChar];
|
||||
|
||||
provideSignatureHelp(_model: ITextModel, _position: Position, _token: CancellationToken, context: modes.SignatureHelpContext): modes.SignatureHelp | Thenable<modes.SignatureHelp> {
|
||||
++invokeCount;
|
||||
if (invokeCount === 1) {
|
||||
assert.strictEqual(context.triggerReason, modes.SignatureHelpTriggerReason.TriggerCharacter);
|
||||
assert.strictEqual(context.triggerCharacter, triggerChar);
|
||||
|
||||
// Cancel and retrigger
|
||||
hintModel.cancel();
|
||||
editor.trigger('keyboard', Handler.Type, { text: triggerChar });
|
||||
} else {
|
||||
assert.strictEqual(invokeCount, 2);
|
||||
assert.strictEqual(context.triggerReason, modes.SignatureHelpTriggerReason.TriggerCharacter);
|
||||
assert.strictEqual(context.triggerCharacter, triggerChar);
|
||||
done();
|
||||
}
|
||||
return emptySigHelpResult;
|
||||
}
|
||||
}));
|
||||
|
||||
editor.trigger('keyboard', Handler.Type, { text: triggerChar });
|
||||
});
|
||||
|
||||
test('Provider should get last trigger character when triggered multiple times and only be invoked once', (done) => {
|
||||
const editor = createMockEditor('');
|
||||
disposables.push(new ParameterHintsModel(editor, 5));
|
||||
|
||||
let invokeCount = 0;
|
||||
disposables.push(modes.SignatureHelpProviderRegistry.register(mockFileSelector, new class implements modes.SignatureHelpProvider {
|
||||
signatureHelpTriggerCharacters = ['a', 'b', 'c'];
|
||||
|
||||
provideSignatureHelp(_model: ITextModel, _position: Position, _token: CancellationToken, context: modes.SignatureHelpContext): modes.SignatureHelp | Thenable<modes.SignatureHelp> {
|
||||
++invokeCount;
|
||||
assert.strictEqual(context.triggerReason, modes.SignatureHelpTriggerReason.Retrigger);
|
||||
assert.strictEqual(context.triggerCharacter, 'c');
|
||||
|
||||
// Give some time to allow for later triggers
|
||||
setTimeout(() => {
|
||||
assert.strictEqual(invokeCount, 1);
|
||||
|
||||
done();
|
||||
}, 50);
|
||||
return undefined;
|
||||
}
|
||||
}));
|
||||
|
||||
editor.trigger('keyboard', Handler.Type, { text: 'a' });
|
||||
editor.trigger('keyboard', Handler.Type, { text: 'b' });
|
||||
editor.trigger('keyboard', Handler.Type, { text: 'c' });
|
||||
});
|
||||
|
||||
test('Provider should be retriggered if already active', (done) => {
|
||||
const editor = createMockEditor('');
|
||||
disposables.push(new ParameterHintsModel(editor, 5));
|
||||
|
||||
let invokeCount = 0;
|
||||
disposables.push(modes.SignatureHelpProviderRegistry.register(mockFileSelector, new class implements modes.SignatureHelpProvider {
|
||||
signatureHelpTriggerCharacters = ['a', 'b'];
|
||||
|
||||
provideSignatureHelp(_model: ITextModel, _position: Position, _token: CancellationToken, context: modes.SignatureHelpContext): modes.SignatureHelp | Thenable<modes.SignatureHelp> {
|
||||
++invokeCount;
|
||||
if (invokeCount === 1) {
|
||||
assert.strictEqual(context.triggerReason, modes.SignatureHelpTriggerReason.TriggerCharacter);
|
||||
assert.strictEqual(context.triggerCharacter, 'a');
|
||||
|
||||
// retrigger after delay for widget to show up
|
||||
setTimeout(() => editor.trigger('keyboard', Handler.Type, { text: 'b' }), 50);
|
||||
} else if (invokeCount === 2) {
|
||||
assert.strictEqual(context.triggerReason, modes.SignatureHelpTriggerReason.Retrigger);
|
||||
assert.strictEqual(context.triggerCharacter, 'b');
|
||||
done();
|
||||
} else {
|
||||
assert.fail('Unexpected invoke');
|
||||
}
|
||||
|
||||
return emptySigHelpResult;
|
||||
}
|
||||
}));
|
||||
|
||||
editor.trigger('keyboard', Handler.Type, { text: 'a' });
|
||||
});
|
||||
|
||||
test('Should cancel existing request when new request comes in', () => {
|
||||
const editor = createMockEditor('abc def');
|
||||
const hintsModel = new ParameterHintsModel(editor);
|
||||
|
||||
let didRequestCancellationOf = -1;
|
||||
let invokeCount = 0;
|
||||
const longRunningProvider = new class implements SignatureHelpProvider {
|
||||
signatureHelpTriggerCharacters: string[] = [];
|
||||
const longRunningProvider = new class implements modes.SignatureHelpProvider {
|
||||
signatureHelpTriggerCharacters = [];
|
||||
|
||||
provideSignatureHelp(model: ITextModel, position: Position, token: CancellationToken): SignatureHelp | Thenable<SignatureHelp> {
|
||||
provideSignatureHelp(_model: ITextModel, _position: Position, token: CancellationToken): modes.SignatureHelp | Thenable<modes.SignatureHelp> {
|
||||
const count = invokeCount++;
|
||||
token.onCancellationRequested(() => { didRequestCancellationOf = count; });
|
||||
|
||||
// retrigger on first request
|
||||
if (count === 0) {
|
||||
hintsModel.trigger(0);
|
||||
hintsModel.trigger({ triggerReason: modes.SignatureHelpTriggerReason.Invoke }, 0);
|
||||
}
|
||||
|
||||
return new Promise<SignatureHelp>(resolve => {
|
||||
return new Promise<modes.SignatureHelp>(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
signatures: [{
|
||||
@@ -73,9 +227,9 @@ suite('ParameterHintsModel', () => {
|
||||
}
|
||||
};
|
||||
|
||||
disposables.push(SignatureHelpProviderRegistry.register({ scheme: 'test' }, longRunningProvider));
|
||||
disposables.push(modes.SignatureHelpProviderRegistry.register(mockFileSelector, longRunningProvider));
|
||||
|
||||
hintsModel.trigger(0);
|
||||
hintsModel.trigger({ triggerReason: modes.SignatureHelpTriggerReason.Invoke }, 0);
|
||||
assert.strictEqual(-1, didRequestCancellationOf);
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
|
||||
@@ -880,6 +880,7 @@ export function createMonacoLanguagesAPI(): typeof monaco.languages {
|
||||
SymbolKind: modes.SymbolKind,
|
||||
IndentAction: IndentAction,
|
||||
SuggestTriggerKind: modes.SuggestTriggerKind,
|
||||
FoldingRangeKind: modes.FoldingRangeKind
|
||||
FoldingRangeKind: modes.FoldingRangeKind,
|
||||
SignatureHelpTriggerReason: modes.SignatureHelpTriggerReason,
|
||||
};
|
||||
}
|
||||
|
||||
Vendored
+12
-1
@@ -4884,6 +4884,17 @@ declare namespace monaco.languages {
|
||||
activeParameter: number;
|
||||
}
|
||||
|
||||
export enum SignatureHelpTriggerReason {
|
||||
Invoke = 1,
|
||||
TriggerCharacter = 2,
|
||||
Retrigger = 3
|
||||
}
|
||||
|
||||
export interface SignatureHelpContext {
|
||||
triggerReason: SignatureHelpTriggerReason;
|
||||
triggerCharacter?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The signature help provider interface defines the contract between extensions and
|
||||
* the [parameter hints](https://code.visualstudio.com/docs/editor/intellisense)-feature.
|
||||
@@ -4893,7 +4904,7 @@ declare namespace monaco.languages {
|
||||
/**
|
||||
* Provide help for the signature at the given position and document.
|
||||
*/
|
||||
provideSignatureHelp(model: editor.ITextModel, position: Position, token: CancellationToken): ProviderResult<SignatureHelp>;
|
||||
provideSignatureHelp(model: editor.ITextModel, position: Position, token: CancellationToken, context: SignatureHelpContext): ProviderResult<SignatureHelp>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Vendored
+48
@@ -808,4 +808,52 @@ declare module 'vscode' {
|
||||
export const onDidRenameFile: Event<FileRenameEvent>;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Signature Help
|
||||
/**
|
||||
* How a [Signature provider](#SignatureHelpProvider) was triggered
|
||||
*/
|
||||
export enum SignatureHelpTriggerReason {
|
||||
/**
|
||||
* Signature help was invoked manually by the user or by a command.
|
||||
*/
|
||||
Invoke = 1,
|
||||
|
||||
/**
|
||||
* Signature help was triggered by a trigger character.
|
||||
*/
|
||||
TriggerCharacter = 2,
|
||||
|
||||
/**
|
||||
* Signature help was retriggered.
|
||||
*
|
||||
* Retriggers occur when the signature help is already active and can be caused by typing a trigger character
|
||||
* or by a cursor move.
|
||||
*/
|
||||
Retrigger = 3,
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains additional information about the context in which a
|
||||
* [signature help provider](#SignatureHelpProvider.provideSignatureHelp) is triggered.
|
||||
*/
|
||||
export interface SignatureHelpContext {
|
||||
/**
|
||||
* Action that caused signature help to be requested.
|
||||
*/
|
||||
readonly triggerReason: SignatureHelpTriggerReason;
|
||||
|
||||
/**
|
||||
* Character that caused signature help to be requested.
|
||||
*
|
||||
* This is `undefined` for manual triggers or retriggers for a cursor move.
|
||||
*/
|
||||
readonly triggerCharacter?: string;
|
||||
}
|
||||
|
||||
export interface SignatureHelpProvider {
|
||||
provideSignatureHelp(document: TextDocument, position: Position, token: CancellationToken, context: SignatureHelpContext): ProviderResult<SignatureHelp>;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
@@ -307,10 +307,9 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha
|
||||
|
||||
signatureHelpTriggerCharacters: triggerCharacter,
|
||||
|
||||
provideSignatureHelp: (model: ITextModel, position: EditorPosition, token: CancellationToken): Thenable<modes.SignatureHelp> => {
|
||||
return this._proxy.$provideSignatureHelp(handle, model.uri, position, token);
|
||||
provideSignatureHelp: (model: ITextModel, position: EditorPosition, token: CancellationToken, context: modes.SignatureHelpContext): Thenable<modes.SignatureHelp> => {
|
||||
return this._proxy.$provideSignatureHelp(handle, model.uri, position, context, token);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -759,6 +759,7 @@ export function createApiFactory(
|
||||
Selection: extHostTypes.Selection,
|
||||
ShellExecution: extHostTypes.ShellExecution,
|
||||
ShellQuoting: extHostTypes.ShellQuoting,
|
||||
SignatureHelpTriggerReason: extHostTypes.SignatureHelpTriggerReason,
|
||||
SignatureHelp: extHostTypes.SignatureHelp,
|
||||
SignatureInformation: extHostTypes.SignatureInformation,
|
||||
SnippetString: extHostTypes.SnippetString,
|
||||
|
||||
@@ -843,7 +843,7 @@ export interface ExtHostLanguageFeaturesShape {
|
||||
$provideCompletionItems(handle: number, resource: UriComponents, position: IPosition, context: modes.SuggestContext, token: CancellationToken): Thenable<SuggestResultDto>;
|
||||
$resolveCompletionItem(handle: number, resource: UriComponents, position: IPosition, suggestion: modes.ISuggestion, token: CancellationToken): Thenable<modes.ISuggestion>;
|
||||
$releaseCompletionItems(handle: number, id: number): void;
|
||||
$provideSignatureHelp(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Thenable<modes.SignatureHelp>;
|
||||
$provideSignatureHelp(handle: number, resource: UriComponents, position: IPosition, context: modes.SignatureHelpContext, token: CancellationToken): Thenable<modes.SignatureHelp>;
|
||||
$provideDocumentLinks(handle: number, resource: UriComponents, token: CancellationToken): Thenable<modes.ILink[]>;
|
||||
$resolveDocumentLink(handle: number, link: modes.ILink, token: CancellationToken): Thenable<modes.ILink>;
|
||||
$provideDocumentColors(handle: number, resource: UriComponents, token: CancellationToken): Thenable<IRawColorInfo[]>;
|
||||
|
||||
@@ -714,12 +714,12 @@ class SignatureHelpAdapter {
|
||||
private readonly _provider: vscode.SignatureHelpProvider
|
||||
) { }
|
||||
|
||||
provideSignatureHelp(resource: URI, position: IPosition, token: CancellationToken): Thenable<modes.SignatureHelp> {
|
||||
provideSignatureHelp(resource: URI, position: IPosition, context: modes.SignatureHelpContext, token: CancellationToken): Thenable<modes.SignatureHelp> {
|
||||
|
||||
const doc = this._documents.getDocumentData(resource).document;
|
||||
const pos = typeConvert.Position.to(position);
|
||||
|
||||
return asThenable(() => this._provider.provideSignatureHelp(doc, pos, token)).then(value => {
|
||||
return asThenable(() => this._provider.provideSignatureHelp(doc, pos, token, context)).then(value => {
|
||||
if (value) {
|
||||
return typeConvert.SignatureHelp.from(value);
|
||||
}
|
||||
@@ -1144,8 +1144,8 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape {
|
||||
return this._createDisposable(handle);
|
||||
}
|
||||
|
||||
$provideSignatureHelp(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Thenable<modes.SignatureHelp> {
|
||||
return this._withAdapter(handle, SignatureHelpAdapter, adapter => adapter.provideSignatureHelp(URI.revive(resource), position, token));
|
||||
$provideSignatureHelp(handle: number, resource: UriComponents, position: IPosition, context: modes.SignatureHelpContext, token: CancellationToken): Thenable<modes.SignatureHelp> {
|
||||
return this._withAdapter(handle, SignatureHelpAdapter, adapter => adapter.provideSignatureHelp(URI.revive(resource), position, context, token));
|
||||
}
|
||||
|
||||
// --- links
|
||||
|
||||
@@ -1064,6 +1064,12 @@ export class SignatureHelp {
|
||||
}
|
||||
}
|
||||
|
||||
export enum SignatureHelpTriggerReason {
|
||||
Invoke = 1,
|
||||
TriggerCharacter = 2,
|
||||
Retrigger = 3,
|
||||
}
|
||||
|
||||
export enum CompletionTriggerKind {
|
||||
Invoke = 0,
|
||||
TriggerCharacter = 1,
|
||||
|
||||
@@ -24,7 +24,7 @@ import { IHeapService } from 'vs/workbench/api/electron-browser/mainThreadHeapSe
|
||||
import { ExtHostDocuments } from 'vs/workbench/api/node/extHostDocuments';
|
||||
import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors';
|
||||
import { getDocumentSymbols } from 'vs/editor/contrib/quickOpen/quickOpen';
|
||||
import { DocumentSymbolProviderRegistry, DocumentHighlightKind, Hover, ResourceTextEdit } from 'vs/editor/common/modes';
|
||||
import * as modes from 'vs/editor/common/modes';
|
||||
import { getCodeLensData } from 'vs/editor/contrib/codelens/codelens';
|
||||
import { getDefinitionsAtPosition, getImplementationsAtPosition, getTypeDefinitionsAtPosition } from 'vs/editor/contrib/goToDefinition/goToDefinition';
|
||||
import { getHover } from 'vs/editor/contrib/hover/getHover';
|
||||
@@ -133,7 +133,7 @@ suite('ExtHostLanguageFeatures', function () {
|
||||
// --- outline
|
||||
|
||||
test('DocumentSymbols, register/deregister', function () {
|
||||
assert.equal(DocumentSymbolProviderRegistry.all(model).length, 0);
|
||||
assert.equal(modes.DocumentSymbolProviderRegistry.all(model).length, 0);
|
||||
let d1 = extHost.registerDocumentSymbolProvider(defaultSelector, <vscode.DocumentSymbolProvider>{
|
||||
provideDocumentSymbols() {
|
||||
return <vscode.SymbolInformation[]>[];
|
||||
@@ -141,7 +141,7 @@ suite('ExtHostLanguageFeatures', function () {
|
||||
});
|
||||
|
||||
return rpcProtocol.sync().then(() => {
|
||||
assert.equal(DocumentSymbolProviderRegistry.all(model).length, 1);
|
||||
assert.equal(modes.DocumentSymbolProviderRegistry.all(model).length, 1);
|
||||
d1.dispose();
|
||||
return rpcProtocol.sync();
|
||||
});
|
||||
@@ -443,7 +443,7 @@ suite('ExtHostLanguageFeatures', function () {
|
||||
return rpcProtocol.sync().then(() => {
|
||||
return getHover(model, new EditorPosition(1, 1), CancellationToken.None).then(value => {
|
||||
assert.equal(value.length, 2);
|
||||
let [first, second] = value as Hover[];
|
||||
let [first, second] = value as modes.Hover[];
|
||||
assert.equal(first.contents[0].value, 'registered second');
|
||||
assert.equal(second.contents[0].value, 'registered first');
|
||||
});
|
||||
@@ -489,7 +489,7 @@ suite('ExtHostLanguageFeatures', function () {
|
||||
assert.equal(value.length, 1);
|
||||
let [entry] = value;
|
||||
assert.deepEqual(entry.range, { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 5 });
|
||||
assert.equal(entry.kind, DocumentHighlightKind.Text);
|
||||
assert.equal(entry.kind, modes.DocumentHighlightKind.Text);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -513,7 +513,7 @@ suite('ExtHostLanguageFeatures', function () {
|
||||
assert.equal(value.length, 1);
|
||||
let [entry] = value;
|
||||
assert.deepEqual(entry.range, { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 5 });
|
||||
assert.equal(entry.kind, DocumentHighlightKind.Text);
|
||||
assert.equal(entry.kind, modes.DocumentHighlightKind.Text);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -537,7 +537,7 @@ suite('ExtHostLanguageFeatures', function () {
|
||||
assert.equal(value.length, 1);
|
||||
let [entry] = value;
|
||||
assert.deepEqual(entry.range, { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 3 });
|
||||
assert.equal(entry.kind, DocumentHighlightKind.Text);
|
||||
assert.equal(entry.kind, modes.DocumentHighlightKind.Text);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -838,8 +838,8 @@ suite('ExtHostLanguageFeatures', function () {
|
||||
return rename(model, new EditorPosition(1, 1), 'newName').then(value => {
|
||||
// least relevant rename provider
|
||||
assert.equal(value.edits.length, 2);
|
||||
assert.equal((<ResourceTextEdit>value.edits[0]).edits.length, 1);
|
||||
assert.equal((<ResourceTextEdit>value.edits[1]).edits.length, 1);
|
||||
assert.equal((<modes.ResourceTextEdit>value.edits[0]).edits.length, 1);
|
||||
assert.equal((<modes.ResourceTextEdit>value.edits[1]).edits.length, 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -866,7 +866,7 @@ suite('ExtHostLanguageFeatures', function () {
|
||||
|
||||
return rpcProtocol.sync().then(() => {
|
||||
|
||||
return provideSignatureHelp(model, new EditorPosition(1, 1), CancellationToken.None).then(value => {
|
||||
return provideSignatureHelp(model, new EditorPosition(1, 1), { triggerReason: modes.SignatureHelpTriggerReason.Invoke }, CancellationToken.None).then(value => {
|
||||
assert.ok(value);
|
||||
});
|
||||
});
|
||||
@@ -881,7 +881,7 @@ suite('ExtHostLanguageFeatures', function () {
|
||||
|
||||
return rpcProtocol.sync().then(() => {
|
||||
|
||||
return provideSignatureHelp(model, new EditorPosition(1, 1), CancellationToken.None).then(value => {
|
||||
return provideSignatureHelp(model, new EditorPosition(1, 1), { triggerReason: modes.SignatureHelpTriggerReason.Invoke }, CancellationToken.None).then(value => {
|
||||
assert.equal(value, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user