Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>("enabled");

return enabled === true;
},
};

const vsCodeVariableRegex = new RegExp(/\$\{(.+?)\}/g);
Expand Down
43 changes: 43 additions & 0 deletions src/sourcekit-lsp/LanguageClientConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: I'd like to confirm an existing command should be used when one is already defined on item.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, if we ever have a command on an item we keep that rather than overriding it with triggering signature help. This shouldn't matter as of now since SourceKit-LSP already doesn't set commands for completion items.

};
default:
return item;
}
});
}

export function lspClientOptions(
swiftVersion: Version,
workspaceContext: WorkspaceContext,
Expand All @@ -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 => {
Expand Down
208 changes: 208 additions & 0 deletions test/unit-tests/sourcekit-lsp/LanguageClientManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
DidChangeWorkspaceFoldersNotification,
DidChangeWorkspaceFoldersParams,
LanguageClient,
Middleware,
State,
StateChangeEvent,
} from "vscode-languageclient/node";
Expand Down Expand Up @@ -597,6 +598,213 @@ suite("LanguageClientManager Suite", () => {
]);
});

suite("provideCompletionItem middleware", () => {
const mockParameterHintsEnabled = mockGlobalValue(configuration, "parameterHintsEnabled");
let document: MockedObject<vscode.TextDocument>;
let middleware: Middleware;

setup(async () => {
mockParameterHintsEnabled.setValue(() => true);

document = mockObject<vscode.TextDocument>({
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<vscode.CompletionItem[]> => {
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<vscode.CompletionItem[]> => {
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<vscode.CompletionList> => {
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<null> => {
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");

Expand Down