diff --git a/src/configuration.ts b/src/configuration.ts index db85a2adf..145435980 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -525,6 +525,16 @@ const configuration = { get outputChannelLogLevel(): string { return vscode.workspace.getConfiguration("swift").get("outputChannelLogLevel", "info"); }, + parameterHintsEnabled(documentUri: vscode.Uri): boolean { + const enabled = vscode.workspace + .getConfiguration("editor.parameterHints", { + uri: documentUri, + languageId: "swift", + }) + .get("enabled"); + + return enabled === true; + }, }; const vsCodeVariableRegex = new RegExp(/\$\{(.+?)\}/g); diff --git a/src/sourcekit-lsp/LanguageClientConfiguration.ts b/src/sourcekit-lsp/LanguageClientConfiguration.ts index a083bf01b..6a36cfe09 100644 --- a/src/sourcekit-lsp/LanguageClientConfiguration.ts +++ b/src/sourcekit-lsp/LanguageClientConfiguration.ts @@ -176,6 +176,33 @@ export class LanguagerClientDocumentSelectors { } } +function addParameterHintsCommandsIfNeeded( + items: vscode.CompletionItem[], + documentUri: vscode.Uri +): vscode.CompletionItem[] { + if (!configuration.parameterHintsEnabled(documentUri)) { + return items; + } + + return items.map(item => { + switch (item.kind) { + case vscode.CompletionItemKind.Function: + case vscode.CompletionItemKind.Method: + case vscode.CompletionItemKind.Constructor: + case vscode.CompletionItemKind.EnumMember: + return { + command: { + title: "Trigger Parameter Hints", + command: "editor.action.triggerParameterHints", + }, + ...item, + }; + default: + return item; + } + }); +} + export function lspClientOptions( swiftVersion: Version, workspaceContext: WorkspaceContext, @@ -199,6 +226,22 @@ export function lspClientOptions( middleware: { didOpen: activeDocumentManager.didOpen.bind(activeDocumentManager), didClose: activeDocumentManager.didClose.bind(activeDocumentManager), + provideCompletionItem: async (document, position, context, token, next) => { + const result = await next(document, position, context, token); + + if (!result) { + return result; + } + + if (Array.isArray(result)) { + return addParameterHintsCommandsIfNeeded(result, document.uri); + } + + return { + ...result, + items: addParameterHintsCommandsIfNeeded(result.items, document.uri), + }; + }, provideCodeLenses: async (document, token, next) => { const result = await next(document, token); return result?.map(codelens => { diff --git a/test/unit-tests/sourcekit-lsp/LanguageClientManager.test.ts b/test/unit-tests/sourcekit-lsp/LanguageClientManager.test.ts index 407e55066..7ed6c2bd9 100644 --- a/test/unit-tests/sourcekit-lsp/LanguageClientManager.test.ts +++ b/test/unit-tests/sourcekit-lsp/LanguageClientManager.test.ts @@ -36,6 +36,7 @@ import { DidChangeWorkspaceFoldersNotification, DidChangeWorkspaceFoldersParams, LanguageClient, + Middleware, State, StateChangeEvent, } from "vscode-languageclient/node"; @@ -597,6 +598,213 @@ suite("LanguageClientManager Suite", () => { ]); }); + suite("provideCompletionItem middleware", () => { + const mockParameterHintsEnabled = mockGlobalValue(configuration, "parameterHintsEnabled"); + let document: MockedObject; + let middleware: Middleware; + + setup(async () => { + mockParameterHintsEnabled.setValue(() => true); + + document = mockObject({ + uri: vscode.Uri.file("/test/file.swift"), + }); + + new LanguageClientToolchainCoordinator( + instance(mockedWorkspace), + {}, + languageClientFactoryMock + ); + + await waitForReturnedPromises(languageClientMock.start); + + middleware = languageClientFactoryMock.createLanguageClient.args[0][3].middleware!; + }); + + test("adds parameter hints command to function completion items when enabled", async () => { + const completionItemsFromLSP = async (): Promise => { + return [ + { + label: "post(endpoint: String, body: [String : Any]?)", + detail: "NetworkRequest", + kind: vscode.CompletionItemKind.EnumMember, + }, + { + label: "defaultHeaders", + detail: "[String : String]", + kind: vscode.CompletionItemKind.Property, + }, + { + label: "makeRequest(for: NetworkRequest)", + detail: "String", + kind: vscode.CompletionItemKind.Function, + }, + { + label: "[endpoint: String]", + detail: "NetworkRequest", + kind: vscode.CompletionItemKind.Method, + }, + { + label: "(endpoint: String, method: String)", + detail: "NetworkRequest", + kind: vscode.CompletionItemKind.Constructor, + }, + ]; + }; + + expect(middleware).to.have.property("provideCompletionItem"); + + const result = await middleware.provideCompletionItem!( + instance(document), + new vscode.Position(0, 0), + {} as any, + {} as any, + completionItemsFromLSP + ); + + expect(result).to.deep.equal([ + { + label: "post(endpoint: String, body: [String : Any]?)", + detail: "NetworkRequest", + kind: vscode.CompletionItemKind.EnumMember, + command: { + title: "Trigger Parameter Hints", + command: "editor.action.triggerParameterHints", + }, + }, + { + label: "defaultHeaders", + detail: "[String : String]", + kind: vscode.CompletionItemKind.Property, + }, + { + label: "makeRequest(for: NetworkRequest)", + detail: "String", + kind: vscode.CompletionItemKind.Function, + command: { + title: "Trigger Parameter Hints", + command: "editor.action.triggerParameterHints", + }, + }, + { + label: "[endpoint: String]", + detail: "NetworkRequest", + kind: vscode.CompletionItemKind.Method, + command: { + title: "Trigger Parameter Hints", + command: "editor.action.triggerParameterHints", + }, + }, + { + label: "(endpoint: String, method: String)", + detail: "NetworkRequest", + kind: vscode.CompletionItemKind.Constructor, + command: { + title: "Trigger Parameter Hints", + command: "editor.action.triggerParameterHints", + }, + }, + ]); + }); + + test("does not add parameter hints command when disabled", async () => { + mockParameterHintsEnabled.setValue(() => false); + + const completionItems = [ + { + label: "makeRequest(for: NetworkRequest)", + detail: "String", + kind: vscode.CompletionItemKind.Function, + }, + { + label: "[endpoint: String]", + detail: "NetworkRequest", + kind: vscode.CompletionItemKind.Method, + }, + ]; + + const completionItemsFromLSP = async (): Promise => { + return completionItems; + }; + + const result = await middleware.provideCompletionItem!( + instance(document), + new vscode.Position(0, 0), + {} as any, + {} as any, + completionItemsFromLSP + ); + + expect(result).to.deep.equal(completionItems); + }); + + test("handles CompletionList result format", async () => { + const completionListFromLSP = async (): Promise => { + return { + isIncomplete: false, + items: [ + { + label: "defaultHeaders", + detail: "[String : String]", + kind: vscode.CompletionItemKind.Property, + }, + { + label: "makeRequest(for: NetworkRequest)", + detail: "String", + kind: vscode.CompletionItemKind.Function, + }, + ], + }; + }; + + const result = await middleware.provideCompletionItem!( + instance(document), + new vscode.Position(0, 0), + {} as any, + {} as any, + completionListFromLSP + ); + + expect(result).to.deep.equal({ + isIncomplete: false, + items: [ + { + label: "defaultHeaders", + detail: "[String : String]", + kind: vscode.CompletionItemKind.Property, + }, + { + label: "makeRequest(for: NetworkRequest)", + detail: "String", + kind: vscode.CompletionItemKind.Function, + command: { + title: "Trigger Parameter Hints", + command: "editor.action.triggerParameterHints", + }, + }, + ], + }); + }); + + test("handles null/undefined result from next middleware", async () => { + mockParameterHintsEnabled.setValue(() => true); + + const nullCompletionResult = async (): Promise => { + return null; + }; + + const result = await middleware.provideCompletionItem!( + instance(document), + new vscode.Position(0, 0), + {} as any, + {} as any, + nullCompletionResult + ); + + expect(result).to.be.null; + }); + }); + suite("active document changes", () => { const mockWindow = mockGlobalObject(vscode, "window");