on master help sign in
Add LSP workspace command picker (#3140)

* Add workspace command picker * Make command typable * Add optional argument to lsp-workspace-command

commitdee5b2a983f6a334753be48730868e8dd651b505 authorMatthias Deiml <matthias@deiml.net> dateWed 9 Nov 2022, 10:17 (4 years ago) parent3e84434
5 files changed+129
book/src/generated/typable-cmd.md+1 helix-lsp/src/client.rs+3 helix-term/src/commands/lsp.rs+8 helix-term/src/commands/typed.rs+78 helix-term/src/ui/mod.rs+39
book/src/generated/typable-cmd.md+1
@@ -45,6 +45,7 @@
4545| `:encoding` | Set encoding. Based on `https://encoding.spec.whatwg.org`. |
4646| `:reload` | Discard changes and reload from the source file. |
4747| `:update` | Write changes only if the file has been modified. |
48+| `:lsp-workspace-command` | Open workspace command picker |
4849| `:lsp-restart` | Restarts the Language Server that is in use by the current doc |
4950| `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. |
5051| `:debug-start`, `:dbg` | Start a debug session from a given template with given parameters. |
helix-lsp/src/client.rs+3
@@ -298,6 +298,9 @@ impl Client {
298298 dynamic_registration: Some(false),
299299 ..Default::default()
300300 }),
301+ execute_command: Some(lsp::DynamicRegistrationClientCapabilities {
302+ dynamic_registration: Some(false),
303+ }),
301304 ..Default::default()
302305 }),
303306 text_document: Some(lsp::TextDocumentClientCapabilities {
helix-term/src/commands/lsp.rs+8
@@ -603,6 +603,14 @@ pub fn code_action(cx: &mut Context) {
603603 },
604604 )
605605}
606+
607+impl ui::menu::Item for lsp::Command {
608+ type Data = ();
609+ fn label(&self, _data: &Self::Data) -> Spans {
610+ self.title.as_str().into()
611+ }
612+}
613+
606614pub fn execute_lsp_command(editor: &mut Editor, cmd: lsp::Command) {
607615 let doc = doc!(editor);
608616 let language_server = language_server!(editor, doc);
helix-term/src/commands/typed.rs+78
@@ -1052,6 +1052,77 @@ fn update(
10521052 }
10531053}
10541054
1055+fn lsp_workspace_command(
1056+ cx: &mut compositor::Context,
1057+ args: &[Cow<str>],
1058+ event: PromptEvent,
1059+) -> anyhow::Result<()> {
1060+ if event != PromptEvent::Validate {
1061+ return Ok(());
1062+ }
1063+
1064+ let (_, doc) = current!(cx.editor);
1065+
1066+ let language_server = match doc.language_server() {
1067+ Some(language_server) => language_server,
1068+ None => {
1069+ cx.editor
1070+ .set_status("Language server not active for current buffer");
1071+ return Ok(());
1072+ }
1073+ };
1074+
1075+ let options = match &language_server.capabilities().execute_command_provider {
1076+ Some(options) => options,
1077+ None => {
1078+ cx.editor
1079+ .set_status("Workspace commands are not supported for this language server");
1080+ return Ok(());
1081+ }
1082+ };
1083+ if args.is_empty() {
1084+ let commands = options
1085+ .commands
1086+ .iter()
1087+ .map(|command| helix_lsp::lsp::Command {
1088+ title: command.clone(),
1089+ command: command.clone(),
1090+ arguments: None,
1091+ })
1092+ .collect::<Vec<_>>();
1093+ let callback = async move {
1094+ let call: job::Callback = Callback::EditorCompositor(Box::new(
1095+ move |_editor: &mut Editor, compositor: &mut Compositor| {
1096+ let picker = ui::Picker::new(commands, (), |cx, command, _action| {
1097+ execute_lsp_command(cx.editor, command.clone());
1098+ });
1099+ compositor.push(Box::new(overlayed(picker)))
1100+ },
1101+ ));
1102+ Ok(call)
1103+ };
1104+ cx.jobs.callback(callback);
1105+ } else {
1106+ let command = args.join(" ");
1107+ if options.commands.iter().any(|c| c == &command) {
1108+ execute_lsp_command(
1109+ cx.editor,
1110+ helix_lsp::lsp::Command {
1111+ title: command.clone(),
1112+ arguments: None,
1113+ command,
1114+ },
1115+ );
1116+ } else {
1117+ cx.editor.set_status(format!(
1118+ "`{command}` is not supported for this language server"
1119+ ));
1120+ return Ok(());
1121+ }
1122+ }
1123+ Ok(())
1124+}
1125+
10551126fn lsp_restart(
10561127 cx: &mut compositor::Context,
10571128 _args: &[Cow<str>],
@@ -1987,6 +2058,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
19872058 fun: update,
19882059 completer: None,
19892060 },
2061+ TypableCommand {
2062+ name: "lsp-workspace-command",
2063+ aliases: &[],
2064+ doc: "Open workspace command picker",
2065+ fun: lsp_workspace_command,
2066+ completer: Some(completers::lsp_workspace_command),
2067+ },
19902068 TypableCommand {
19912069 name: "lsp-restart",
19922070 aliases: &[],
helix-term/src/ui/mod.rs+39
@@ -390,6 +390,45 @@ pub mod completers {
390390 .collect()
391391 }
392392
393+ pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec<Completion> {
394+ let matcher = Matcher::default();
395+
396+ let (_, doc) = current_ref!(editor);
397+
398+ let language_server = match doc.language_server() {
399+ Some(language_server) => language_server,
400+ None => {
401+ return vec![];
402+ }
403+ };
404+
405+ let options = match &language_server.capabilities().execute_command_provider {
406+ Some(options) => options,
407+ None => {
408+ return vec![];
409+ }
410+ };
411+
412+ let mut matches: Vec<_> = options
413+ .commands
414+ .iter()
415+ .filter_map(|command| {
416+ matcher
417+ .fuzzy_match(command, input)
418+ .map(|score| (command, score))
419+ })
420+ .collect();
421+
422+ matches.sort_unstable_by(|(command1, score1), (command2, score2)| {
423+ (Reverse(*score1), command1).cmp(&(Reverse(*score2), command2))
424+ });
425+
426+ matches
427+ .into_iter()
428+ .map(|(command, _score)| ((0..), command.clone().into()))
429+ .collect()
430+ }
431+
393432 pub fn directory(editor: &Editor, input: &str) -> Vec<Completion> {
394433 filename_impl(editor, input, |entry| {
395434 let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());