2020-09-01 11:53:07 -05:00
|
|
|
import * as lc from "vscode-languageclient/node";
|
2020-02-13 14:48:20 -06:00
|
|
|
import * as vscode from "vscode";
|
2020-06-02 15:21:48 -05:00
|
|
|
import * as ra from "../src/lsp_ext";
|
2020-09-01 11:53:07 -05:00
|
|
|
import * as Is from "vscode-languageclient/lib/common/utils/is";
|
2020-06-02 15:21:48 -05:00
|
|
|
import { assert } from "./util";
|
2020-11-10 11:20:01 -06:00
|
|
|
import { WorkspaceEdit } from "vscode";
|
2021-05-23 08:22:13 -05:00
|
|
|
import { Workspace } from "./ctx";
|
2022-10-16 10:11:24 -05:00
|
|
|
import { substituteVariablesInEnv, substituteVSCodeVariables } from "./config";
|
2022-06-05 06:59:49 -05:00
|
|
|
import { outputChannel, traceOutputChannel } from "./main";
|
2022-05-16 13:53:00 -05:00
|
|
|
import { randomUUID } from "crypto";
|
2019-12-31 11:14:00 -06:00
|
|
|
|
2020-12-30 03:17:25 -06:00
|
|
|
export interface Env {
|
|
|
|
[name: string]: string;
|
|
|
|
}
|
|
|
|
|
2022-05-16 13:53:00 -05:00
|
|
|
// Command URIs have a form of command:command-name?arguments, where
|
|
|
|
// arguments is a percent-encoded array of data we want to pass along to
|
|
|
|
// the command function. For "Show References" this is a list of all file
|
|
|
|
// URIs with locations of every reference, and it can get quite long.
|
|
|
|
//
|
|
|
|
// To work around it we use an intermediary linkToCommand command. When
|
|
|
|
// we render a command link, a reference to a command with all its arguments
|
|
|
|
// is stored in a map, and instead a linkToCommand link is rendered
|
|
|
|
// with the key to that map.
|
|
|
|
export const LINKED_COMMANDS = new Map<string, ra.CommandLink>();
|
|
|
|
|
|
|
|
// For now the map is cleaned up periodically (I've set it to every
|
|
|
|
// 10 minutes). In general case we'll probably need to introduce TTLs or
|
|
|
|
// flags to denote ephemeral links (like these in hover popups) and
|
|
|
|
// persistent links and clean those separately. But for now simply keeping
|
|
|
|
// the last few links in the map should be good enough. Likewise, we could
|
|
|
|
// add code to remove a target command from the map after the link is
|
|
|
|
// clicked, but assuming most links in hover sheets won't be clicked anyway
|
|
|
|
// this code won't change the overall memory use much.
|
|
|
|
setInterval(function cleanupOlderCommandLinks() {
|
|
|
|
// keys are returned in insertion order, we'll keep a few
|
|
|
|
// of recent keys available, and clean the rest
|
|
|
|
const keys = [...LINKED_COMMANDS.keys()];
|
|
|
|
const keysToRemove = keys.slice(0, keys.length - 10);
|
|
|
|
for (const key of keysToRemove) {
|
|
|
|
LINKED_COMMANDS.delete(key);
|
|
|
|
}
|
|
|
|
}, 10 * 60 * 1000);
|
|
|
|
|
|
|
|
function renderCommand(cmd: ra.CommandLink): string {
|
|
|
|
const commandId = randomUUID();
|
|
|
|
LINKED_COMMANDS.set(commandId, cmd);
|
|
|
|
return `[${cmd.title}](command:rust-analyzer.linkToCommand?${encodeURIComponent(
|
|
|
|
JSON.stringify([commandId])
|
2021-02-07 11:45:13 -06:00
|
|
|
)} '${cmd.tooltip}')`;
|
2020-06-03 06:15:54 -05:00
|
|
|
}
|
|
|
|
|
2020-06-05 07:25:01 -05:00
|
|
|
function renderHoverActions(actions: ra.CommandLinkGroup[]): vscode.MarkdownString {
|
2020-06-03 06:15:54 -05:00
|
|
|
const text = actions
|
|
|
|
.map(
|
|
|
|
(group) =>
|
|
|
|
(group.title ? group.title + " " : "") +
|
|
|
|
group.commands.map(renderCommand).join(" | ")
|
|
|
|
)
|
|
|
|
.join("___");
|
|
|
|
|
|
|
|
const result = new vscode.MarkdownString(text);
|
|
|
|
result.isTrusted = true;
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2022-04-29 06:34:03 -05:00
|
|
|
export async function createClient(
|
|
|
|
serverPath: string,
|
|
|
|
workspace: Workspace,
|
|
|
|
extraEnv: Env
|
|
|
|
): Promise<lc.LanguageClient> {
|
2019-12-31 11:14:00 -06:00
|
|
|
// '.' Is the fallback if no folder is open
|
2020-02-04 16:13:46 -06:00
|
|
|
// TODO?: Workspace folders support Uri's (eg: file://test.txt).
|
|
|
|
// It might be a good idea to test if the uri points to a file.
|
2019-12-31 11:14:00 -06:00
|
|
|
|
2022-05-11 07:37:40 -05:00
|
|
|
const newEnv = substituteVariablesInEnv(Object.assign({}, process.env, extraEnv));
|
2019-12-31 11:14:00 -06:00
|
|
|
const run: lc.Executable = {
|
2020-02-14 16:42:32 -06:00
|
|
|
command: serverPath,
|
2021-05-25 17:11:52 -05:00
|
|
|
options: { env: newEnv },
|
2019-12-31 11:14:00 -06:00
|
|
|
};
|
|
|
|
const serverOptions: lc.ServerOptions = {
|
|
|
|
run,
|
|
|
|
debug: run,
|
|
|
|
};
|
2020-02-13 14:48:20 -06:00
|
|
|
|
2022-10-16 10:11:24 -05:00
|
|
|
let rawInitializationOptions = vscode.workspace.getConfiguration("rust-analyzer");
|
2022-04-29 06:34:03 -05:00
|
|
|
|
2021-05-23 15:47:58 -05:00
|
|
|
if (workspace.kind === "Detached Files") {
|
2022-10-16 10:11:24 -05:00
|
|
|
rawInitializationOptions = {
|
2021-05-23 08:22:13 -05:00
|
|
|
detachedFiles: workspace.files.map((file) => file.uri.fsPath),
|
2022-10-16 10:11:24 -05:00
|
|
|
...rawInitializationOptions,
|
2021-05-23 08:22:13 -05:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-10-16 10:11:24 -05:00
|
|
|
const initializationOptions = substituteVSCodeVariables(rawInitializationOptions);
|
|
|
|
|
2019-12-31 11:14:00 -06:00
|
|
|
const clientOptions: lc.LanguageClientOptions = {
|
|
|
|
documentSelector: [{ scheme: "file", language: "rust" }],
|
2021-05-23 08:22:13 -05:00
|
|
|
initializationOptions,
|
2020-07-23 00:32:54 -05:00
|
|
|
diagnosticCollectionName: "rustc",
|
2022-06-05 06:59:49 -05:00
|
|
|
traceOutputChannel: traceOutputChannel(),
|
|
|
|
outputChannel: outputChannel(),
|
2020-02-27 03:19:56 -06:00
|
|
|
middleware: {
|
2022-10-16 10:11:24 -05:00
|
|
|
workspace: {
|
|
|
|
async configuration(
|
|
|
|
params: lc.ConfigurationParams,
|
|
|
|
token: vscode.CancellationToken,
|
|
|
|
next: lc.ConfigurationRequest.HandlerSignature
|
|
|
|
) {
|
|
|
|
const resp = await next(params, token);
|
|
|
|
if (resp && Array.isArray(resp)) {
|
|
|
|
return resp.map((val) => {
|
|
|
|
return substituteVSCodeVariables(val);
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
return resp;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
2020-06-03 06:15:54 -05:00
|
|
|
async provideHover(
|
|
|
|
document: vscode.TextDocument,
|
|
|
|
position: vscode.Position,
|
|
|
|
token: vscode.CancellationToken,
|
|
|
|
_next: lc.ProvideHoverSignature
|
|
|
|
) {
|
2021-07-25 16:26:54 -05:00
|
|
|
const editor = vscode.window.activeTextEditor;
|
2021-07-26 11:14:14 -05:00
|
|
|
const positionOrRange = editor?.selection?.contains(position)
|
|
|
|
? client.code2ProtocolConverter.asRange(editor.selection)
|
|
|
|
: client.code2ProtocolConverter.asPosition(position);
|
|
|
|
return client
|
|
|
|
.sendRequest(
|
|
|
|
ra.hover,
|
|
|
|
{
|
|
|
|
textDocument:
|
|
|
|
client.code2ProtocolConverter.asTextDocumentIdentifier(document),
|
|
|
|
position: positionOrRange,
|
|
|
|
},
|
|
|
|
token
|
2022-05-17 12:15:06 -05:00
|
|
|
)
|
2021-07-26 11:14:14 -05:00
|
|
|
.then(
|
2021-07-26 16:33:21 -05:00
|
|
|
(result) => {
|
2021-07-25 16:26:54 -05:00
|
|
|
const hover = client.protocol2CodeConverter.asHover(result);
|
2021-07-26 16:33:21 -05:00
|
|
|
if (hover) {
|
2020-06-03 06:15:54 -05:00
|
|
|
const actions = (<any>result).actions;
|
|
|
|
if (actions) {
|
2021-07-26 16:33:21 -05:00
|
|
|
hover.contents.push(renderHoverActions(actions));
|
2022-05-17 12:15:06 -05:00
|
|
|
}
|
2020-06-03 06:15:54 -05:00
|
|
|
}
|
2021-07-26 16:33:21 -05:00
|
|
|
return hover;
|
2022-05-17 12:15:06 -05:00
|
|
|
},
|
2021-07-26 16:33:21 -05:00
|
|
|
(error) => {
|
2022-04-08 06:24:28 -05:00
|
|
|
client.handleFailedRequest(lc.HoverRequest.type, token, error, null);
|
2021-07-26 16:33:21 -05:00
|
|
|
return Promise.resolve(null);
|
|
|
|
}
|
|
|
|
);
|
2020-06-03 06:15:54 -05:00
|
|
|
},
|
2020-11-10 11:20:01 -06:00
|
|
|
// Using custom handling of CodeActions to support action groups and snippet edits.
|
|
|
|
// Note that this means we have to re-implement lazy edit resolving ourselves as well.
|
2020-05-17 18:53:55 -05:00
|
|
|
async provideCodeActions(
|
|
|
|
document: vscode.TextDocument,
|
|
|
|
range: vscode.Range,
|
|
|
|
context: vscode.CodeActionContext,
|
|
|
|
token: vscode.CancellationToken,
|
|
|
|
_next: lc.ProvideCodeActionsSignature
|
|
|
|
) {
|
|
|
|
const params: lc.CodeActionParams = {
|
|
|
|
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document),
|
|
|
|
range: client.code2ProtocolConverter.asRange(range),
|
2022-04-08 06:24:28 -05:00
|
|
|
context: await client.code2ProtocolConverter.asCodeActionContext(
|
|
|
|
context,
|
|
|
|
token
|
2022-05-17 12:15:06 -05:00
|
|
|
),
|
2020-05-17 18:53:55 -05:00
|
|
|
};
|
2022-04-08 06:24:28 -05:00
|
|
|
return client.sendRequest(lc.CodeActionRequest.type, params, token).then(
|
|
|
|
async (values) => {
|
2020-05-17 18:53:55 -05:00
|
|
|
if (values === null) return undefined;
|
|
|
|
const result: (vscode.CodeAction | vscode.Command)[] = [];
|
2020-05-22 10:29:55 -05:00
|
|
|
const groups = new Map<
|
|
|
|
string,
|
|
|
|
{ index: number; items: vscode.CodeAction[] }
|
|
|
|
>();
|
2020-05-17 18:53:55 -05:00
|
|
|
for (const item of values) {
|
2020-06-02 15:21:48 -05:00
|
|
|
// In our case we expect to get code edits only from diagnostics
|
2020-05-17 18:53:55 -05:00
|
|
|
if (lc.CodeAction.is(item)) {
|
2020-06-02 15:21:48 -05:00
|
|
|
assert(
|
|
|
|
!item.command,
|
|
|
|
"We don't expect to receive commands in CodeActions"
|
2020-06-28 18:59:39 -05:00
|
|
|
);
|
|
|
|
const action = await client.protocol2CodeConverter.asCodeAction(
|
2020-06-02 15:21:48 -05:00
|
|
|
item,
|
|
|
|
token
|
|
|
|
);
|
2020-05-22 10:29:55 -05:00
|
|
|
result.push(action);
|
2020-06-02 15:21:48 -05:00
|
|
|
continue;
|
2020-05-22 10:29:55 -05:00
|
|
|
}
|
2022-05-17 12:15:06 -05:00
|
|
|
assert(
|
2020-06-02 15:21:48 -05:00
|
|
|
isCodeActionWithoutEditsAndCommands(item),
|
2020-05-22 10:29:55 -05:00
|
|
|
"We don't expect edits or commands here"
|
2022-05-17 12:15:06 -05:00
|
|
|
);
|
2020-05-22 10:29:55 -05:00
|
|
|
const kind = client.protocol2CodeConverter.asCodeActionKind(
|
|
|
|
(item as any).kind
|
|
|
|
);
|
|
|
|
const action = new vscode.CodeAction(item.title, kind);
|
2020-06-28 18:59:39 -05:00
|
|
|
const group = (item as any).group;
|
2020-05-22 10:29:55 -05:00
|
|
|
action.command = {
|
|
|
|
command: "rust-analyzer.resolveCodeAction",
|
2021-02-07 12:36:16 -06:00
|
|
|
title: item.title,
|
|
|
|
arguments: [item],
|
2020-05-22 10:29:55 -05:00
|
|
|
};
|
2020-11-10 11:20:01 -06:00
|
|
|
|
|
|
|
// Set a dummy edit, so that VS Code doesn't try to resolve this.
|
|
|
|
action.edit = new WorkspaceEdit();
|
|
|
|
|
2020-06-02 15:21:48 -05:00
|
|
|
if (group) {
|
|
|
|
let entry = groups.get(group);
|
|
|
|
if (!entry) {
|
2020-05-22 10:29:55 -05:00
|
|
|
entry = { index: result.length, items: [] };
|
2020-06-02 15:21:48 -05:00
|
|
|
groups.set(group, entry);
|
2020-05-22 10:29:55 -05:00
|
|
|
result.push(action);
|
2022-05-17 12:15:06 -05:00
|
|
|
}
|
2020-06-02 15:21:48 -05:00
|
|
|
entry.items.push(action);
|
2022-05-17 12:15:06 -05:00
|
|
|
} else {
|
2020-06-02 15:21:48 -05:00
|
|
|
result.push(action);
|
2022-05-17 12:15:06 -05:00
|
|
|
}
|
2020-05-22 10:29:55 -05:00
|
|
|
}
|
|
|
|
for (const [group, { index, items }] of groups) {
|
|
|
|
if (items.length === 1) {
|
|
|
|
result[index] = items[0];
|
|
|
|
} else {
|
2022-04-08 06:24:28 -05:00
|
|
|
const action = new vscode.CodeAction(group);
|
2020-06-28 18:59:39 -05:00
|
|
|
action.kind = items[0].kind;
|
2020-05-22 10:29:55 -05:00
|
|
|
action.command = {
|
2020-06-02 15:21:48 -05:00
|
|
|
command: "rust-analyzer.applyActionGroup",
|
2020-05-22 10:29:55 -05:00
|
|
|
title: "",
|
|
|
|
arguments: [
|
|
|
|
items.map((item) => {
|
2022-05-17 12:15:06 -05:00
|
|
|
return {
|
2020-06-02 15:21:48 -05:00
|
|
|
label: item.title,
|
2021-02-07 12:36:16 -06:00
|
|
|
arguments: item.command!.arguments![0],
|
2022-05-17 12:15:06 -05:00
|
|
|
};
|
|
|
|
}),
|
|
|
|
],
|
|
|
|
};
|
|
|
|
|
2020-11-10 11:20:01 -06:00
|
|
|
// Set a dummy edit, so that VS Code doesn't try to resolve this.
|
|
|
|
action.edit = new WorkspaceEdit();
|
2022-05-17 12:15:06 -05:00
|
|
|
|
2020-05-22 10:29:55 -05:00
|
|
|
result[index] = action;
|
2022-05-17 12:15:06 -05:00
|
|
|
}
|
|
|
|
}
|
2020-05-17 18:53:55 -05:00
|
|
|
return result;
|
|
|
|
},
|
|
|
|
(_error) => undefined
|
|
|
|
);
|
2020-02-27 03:19:56 -06:00
|
|
|
},
|
2022-01-14 17:20:35 -06:00
|
|
|
},
|
|
|
|
markdown: {
|
|
|
|
supportHtml: true,
|
2020-08-09 17:09:27 -05:00
|
|
|
},
|
2019-12-31 11:14:00 -06:00
|
|
|
};
|
|
|
|
|
2020-05-17 14:24:33 -05:00
|
|
|
const client = new lc.LanguageClient(
|
2019-12-31 11:14:00 -06:00
|
|
|
"rust-analyzer",
|
|
|
|
"Rust Analyzer Language Server",
|
|
|
|
serverOptions,
|
|
|
|
clientOptions
|
|
|
|
);
|
|
|
|
|
2020-05-17 18:53:55 -05:00
|
|
|
// To turn on all proposed features use: client.registerProposedFeatures();
|
2020-05-22 10:29:55 -05:00
|
|
|
client.registerFeature(new ExperimentalFeatures());
|
2020-02-26 07:42:26 -06:00
|
|
|
|
2020-05-17 14:24:33 -05:00
|
|
|
return client;
|
|
|
|
}
|
|
|
|
|
2020-05-22 10:29:55 -05:00
|
|
|
class ExperimentalFeatures implements lc.StaticFeature {
|
2020-05-17 14:24:33 -05:00
|
|
|
fillClientCapabilities(capabilities: lc.ClientCapabilities): void {
|
|
|
|
const caps: any = capabilities.experimental ?? {};
|
|
|
|
caps.snippetTextEdit = true;
|
2020-05-22 10:29:55 -05:00
|
|
|
caps.codeActionGroup = true;
|
2020-06-03 06:15:54 -05:00
|
|
|
caps.hoverActions = true;
|
2021-04-06 06:16:35 -05:00
|
|
|
caps.serverStatusNotification = true;
|
feat: gate custom clint-side commands behind capabilities
Some features of rust-analyzer requires support for custom commands on
the client side. Specifically, hover & code lens need this.
Stock LSP doesn't have a way for the server to know which client-side
commands are available. For that reason, we historically were just
sending the commands, not worrying whether the client supports then or
not.
That's not really great though, so in this PR we add infrastructure for
the client to explicitly opt-into custom commands, via `extensions`
field of the ClientCapabilities.
To preserve backwards compatability, if the client doesn't set the
field, we assume that it does support all custom commands. In the
future, we'll start treating that case as if the client doesn't support
commands.
So, if you maintain a rust-analyzer client and implement
`rust-analyzer/runSingle` and such, please also advertise this via a
capability.
2021-07-30 11:16:33 -05:00
|
|
|
caps.commands = {
|
|
|
|
commands: [
|
|
|
|
"rust-analyzer.runSingle",
|
|
|
|
"rust-analyzer.debugSingle",
|
|
|
|
"rust-analyzer.showReferences",
|
|
|
|
"rust-analyzer.gotoLocation",
|
|
|
|
"editor.action.triggerParameterHints",
|
|
|
|
],
|
|
|
|
};
|
2020-05-17 18:53:55 -05:00
|
|
|
capabilities.experimental = caps;
|
2020-05-17 14:24:33 -05:00
|
|
|
}
|
|
|
|
initialize(
|
|
|
|
_capabilities: lc.ServerCapabilities<any>,
|
|
|
|
_documentSelector: lc.DocumentSelector | undefined
|
|
|
|
): void {}
|
2020-11-17 09:10:34 -06:00
|
|
|
dispose(): void {}
|
2019-12-31 11:14:00 -06:00
|
|
|
}
|
2020-05-17 18:53:55 -05:00
|
|
|
|
2020-06-02 15:21:48 -05:00
|
|
|
function isCodeActionWithoutEditsAndCommands(value: any): boolean {
|
|
|
|
const candidate: lc.CodeAction = value;
|
|
|
|
return (
|
|
|
|
candidate &&
|
|
|
|
Is.string(candidate.title) &&
|
|
|
|
(candidate.diagnostics === void 0 ||
|
|
|
|
Is.typedArray(candidate.diagnostics, lc.Diagnostic.is)) &&
|
|
|
|
(candidate.kind === void 0 || Is.string(candidate.kind)) &&
|
|
|
|
candidate.edit === void 0 &&
|
|
|
|
candidate.command === void 0
|
|
|
|
);
|
2020-05-22 10:29:55 -05:00
|
|
|
}
|