Skip to content

Commit 00685fb

Browse files
MDeimlFrederik Vestre
authored andcommitted
Add LSP workspace command picker (helix-editor#3140)
* Add workspace command picker * Make command typable * Add optional argument to lsp-workspace-command
1 parent 29a91b8 commit 00685fb

File tree

5 files changed

+129
-0
lines changed

5 files changed

+129
-0
lines changed

book/src/generated/typable-cmd.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
606614
pub 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

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
10551126
fn 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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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());

0 commit comments

Comments
 (0)