Schuyler Mortimer 5 months ago
parent 348c0ebeb4
commit 877c332439

@ -665,6 +665,11 @@ impl Client {
..Default::default()
}),
window: Some(lsp::WindowClientCapabilities {
show_message: Some(lsp::ShowMessageRequestClientCapabilities {
message_action_item: Some(lsp::MessageActionItemCapabilities {
additional_properties_support: Some(true),
}),
}),
work_done_progress: Some(true),
..Default::default()
}),

@ -567,6 +567,7 @@ pub enum MethodCall {
RegisterCapability(lsp::RegistrationParams),
UnregisterCapability(lsp::UnregistrationParams),
ShowDocument(lsp::ShowDocumentParams),
ShowMessageRequest(lsp::ShowMessageRequestParams),
}
impl MethodCall {
@ -598,6 +599,9 @@ impl MethodCall {
let params: lsp::ShowDocumentParams = params.parse()?;
Self::ShowDocument(params)
}
lsp::request::ShowMessageRequest::METHOD => {
Self::ShowMessageRequest(params.parse::<lsp::ShowMessageRequestParams>()?)
}
_ => {
return Err(Error::Unhandled);
}

@ -1,8 +1,8 @@
use arc_swap::{access::Map, ArcSwap};
use futures_util::Stream;
use helix_core::{diagnostic::Severity, pos_at_coords, syntax, Selection};
use helix_core::{diagnostic::Severity, pos_at_coords, syntax, Rope, Selection};
use helix_lsp::{
lsp::{self, notification::Notification},
lsp::{self, notification::Notification, MessageType},
util::lsp_range_to_range,
LanguageServerId, LspProgressMap,
};
@ -26,7 +26,7 @@ use crate::{
handlers,
job::Jobs,
keymap::Keymaps,
ui::{self, overlay::overlaid},
ui::{self, overlay::overlaid, FileLocation, Picker, Popup},
};
use log::{debug, error, info, warn};
@ -960,11 +960,11 @@ impl Application {
"Language Server: Method {} not found in request {}",
method, id
);
Err(helix_lsp::jsonrpc::Error {
Some(Err(helix_lsp::jsonrpc::Error {
code: helix_lsp::jsonrpc::ErrorCode::MethodNotFound,
message: format!("Method not found: {}", method),
data: None,
})
}))
}
Err(err) => {
log::error!(
@ -973,11 +973,11 @@ impl Application {
id,
err
);
Err(helix_lsp::jsonrpc::Error {
Some(Err(helix_lsp::jsonrpc::Error {
code: helix_lsp::jsonrpc::ErrorCode::ParseError,
message: format!("Malformed method call: {}", method),
data: None,
})
}))
}
Ok(MethodCall::WorkDoneProgressCreate(params)) => {
self.lsp_progress.create(server_id, params.token);
@ -991,7 +991,7 @@ impl Application {
spinner.start();
}
Ok(serde_json::Value::Null)
Some(Ok(serde_json::Value::Null))
}
Ok(MethodCall::ApplyWorkspaceEdit(params)) => {
let language_server = language_server!();
@ -1001,25 +1001,73 @@ impl Application {
.editor
.apply_workspace_edit(offset_encoding, &params.edit);
Ok(json!(lsp::ApplyWorkspaceEditResponse {
Some(Ok(json!(lsp::ApplyWorkspaceEditResponse {
applied: res.is_ok(),
failure_reason: res.as_ref().err().map(|err| err.kind.to_string()),
failed_change: res
.as_ref()
.err()
.map(|err| err.failed_change_idx as u32),
}))
})))
} else {
Err(helix_lsp::jsonrpc::Error {
Some(Err(helix_lsp::jsonrpc::Error {
code: helix_lsp::jsonrpc::ErrorCode::InvalidRequest,
message: "Server must be initialized to request workspace edits"
.to_string(),
data: None,
})
}))
}
}
Ok(MethodCall::WorkspaceFolders) => {
Ok(json!(&*language_server!().workspace_folders().await))
Some(Ok(json!(&*language_server!().workspace_folders().await)))
}
Ok(MethodCall::ShowMessageRequest(params)) => {
if let Some(actions) = params.actions {
let call_id = id.clone();
let message_type = match params.typ {
MessageType::ERROR => "ERROR",
MessageType::WARNING => "WARNING",
MessageType::INFO => "INFO",
_ => "LOG",
};
let message = format!("{}: {}", message_type, &params.message);
let rope = Rope::from(message);
let picker =
Picker::new(actions, (), move |ctx, message_action, _event| {
let server_from_id =
ctx.editor.language_servers.get_by_id(server_id);
let language_server = match server_from_id {
Some(language_server) => language_server,
None => {
warn!(
"can't find language server with id `{server_id}`"
);
return;
}
};
let response = match message_action {
Some(item) => json!(item),
None => serde_json::Value::Null,
};
tokio::spawn(
language_server.reply(call_id.clone(), Ok(response)),
);
})
.with_preview(
move |_editor, _value| {
let file_location: FileLocation =
(rope.clone().into(), None);
Some(file_location)
},
);
let popup_id = "show-message-request";
let popup = Popup::new(popup_id, picker);
self.compositor.replace_or_push(popup_id, popup);
// do not send a reply just yet
None
} else {
Some(Ok(serde_json::Value::Null))
}
}
Ok(MethodCall::WorkspaceConfiguration(params)) => {
let language_server = language_server!();
@ -1039,7 +1087,7 @@ impl Application {
Some(config)
})
.collect();
Ok(json!(result))
Some(Ok(json!(result)))
}
Ok(MethodCall::RegisterCapability(params)) => {
if let Some(client) = self.editor.language_servers.get_by_id(server_id) {
@ -1077,7 +1125,7 @@ impl Application {
}
}
Ok(serde_json::Value::Null)
Some(Ok(serde_json::Value::Null))
}
Ok(MethodCall::UnregisterCapability(params)) => {
for unreg in params.unregisterations {
@ -1093,18 +1141,26 @@ impl Application {
}
}
}
Ok(serde_json::Value::Null)
Some(Ok(serde_json::Value::Null))
}
Ok(MethodCall::ShowDocument(params)) => {
let language_server = language_server!();
let offset_encoding = language_server.offset_encoding();
let result = self.handle_show_document(params, offset_encoding);
Ok(json!(result))
Some(Ok(json!(result)))
}
};
tokio::spawn(language_server!().reply(id, reply));
if let Some(reply) = reply {
let lanaguage_server = match self.editor.language_servers.get_by_id(server_id) {
Some(language_server) => language_server,
None => {
warn!("can't find language server with id `{}`", server_id);
return;
}
};
tokio::spawn(lanaguage_server.reply(id, reply));
}
}
Call::Invalid { id } => log::error!("LSP invalid method call id={:?}", id),
}

@ -2425,7 +2425,10 @@ fn global_search(cx: &mut Context) {
let picker = Picker::with_stream(
picker,
injector,
move |cx, FileResult { path, line_num }, action| {
move |cx, file_result, action| {
let Some(FileResult { path, line_num }) = file_result else {
return;
};
let doc = match cx.editor.open(path, action) {
Ok(id) => doc_mut!(cx.editor, &id),
Err(e) => {
@ -2941,7 +2944,8 @@ fn buffer_picker(cx: &mut Context) {
// mru
items.sort_unstable_by_key(|item| std::cmp::Reverse(item.focused_at));
let picker = Picker::new(items, (), |cx, meta, action| {
let picker = Picker::new(items, (), |cx, buffer_meta, action| {
let Some(meta) = buffer_meta else { return };
cx.editor.switch(meta.id, action);
})
.with_preview(|editor, meta| {
@ -3030,7 +3034,8 @@ fn jumplist_picker(cx: &mut Context) {
})
.collect(),
(),
|cx, meta, action| {
|cx, jump_meta, action| {
let Some(meta) = jump_meta else { return };
cx.editor.switch(meta.id, action);
let config = cx.editor.config();
let (view, doc) = (view_mut!(cx.editor), doc_mut!(cx.editor, &meta.id));
@ -3108,7 +3113,8 @@ fn changed_file_picker(cx: &mut Context) {
style_deleted: deleted,
style_renamed: renamed,
},
|cx, meta: &FileChange, action| {
|cx, file_meta: Option<&FileChange>, action| {
let Some(meta) = file_meta else { return };
let path_to_open = meta.path();
if let Err(e) = cx.editor.open(path_to_open, action) {
let err = if let Some(err) = e.source() {
@ -3184,7 +3190,10 @@ pub fn command_palette(cx: &mut Context) {
}
}));
let picker = Picker::new(commands, keymap, move |cx, command, _action| {
let picker = Picker::new(commands, keymap, move |cx, mappable_command, _action| {
let Some(command) = mappable_command else {
return;
};
let mut ctx = Context {
register,
count,

@ -74,7 +74,8 @@ fn thread_picker(
let thread_states = debugger.thread_states.clone();
let picker = Picker::new(threads, thread_states, move |cx, thread, _action| {
callback_fn(cx.editor, thread)
let Some(t) = thread else { return };
callback_fn(cx.editor, t)
})
.with_preview(move |editor, thread| {
let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?;
@ -271,7 +272,10 @@ pub fn dap_launch(cx: &mut Context) {
cx.push_layer(Box::new(overlaid(Picker::new(
templates,
(),
|cx, template, _action| {
|cx, debug_template, _action| {
let Some(template) = debug_template else {
return;
};
if template.completion.is_empty() {
if let Err(err) = dap_start_impl(cx, Some(&template.name), None, None) {
cx.editor.set_error(err.to_string());
@ -736,7 +740,8 @@ pub fn dap_switch_stack_frame(cx: &mut Context) {
let frames = debugger.stack_frames[&thread_id].clone();
let picker = Picker::new(frames, (), move |cx, frame, _action| {
let picker = Picker::new(frames, (), move |cx, stack_frame, _action| {
let Some(frame) = stack_frame else { return };
let debugger = debugger!(cx.editor);
// TODO: this should be simpler to find
let pos = debugger.stack_frames[&thread_id]

@ -245,7 +245,8 @@ type SymbolPicker = Picker<SymbolInformationItem>;
fn sym_picker(symbols: Vec<SymbolInformationItem>, current_path: Option<lsp::Url>) -> SymbolPicker {
// TODO: drop current_path comparison and instead use workspace: bool flag?
Picker::new(symbols, current_path, move |cx, item, action| {
Picker::new(symbols, current_path, move |cx, symbol_item, action| {
let Some(item) = symbol_item else { return };
jump_to_location(
cx.editor,
&item.symbol.location,
@ -296,13 +297,15 @@ fn diag_picker(
Picker::new(
flat_diag,
(styles, format),
move |cx,
PickerDiagnostic {
move |cx, picker_diagnostic, action| {
let Some(PickerDiagnostic {
path,
diag,
offset_encoding,
},
action| {
}) = picker_diagnostic
else {
return;
};
jump_to_position(cx.editor, path, diag.range, *offset_encoding, action)
},
)
@ -509,6 +512,13 @@ impl ui::menu::Item for CodeActionOrCommandItem {
}
}
impl ui::menu::Item for lsp::MessageActionItem {
type Data = ();
fn format(&self, _data: &Self::Data) -> Row {
self.title.as_str().into()
}
}
/// Determines the category of the `CodeAction` using the `CodeAction::kind` field.
/// Returns a number that represent these categories.
/// Categories with a lower number should be displayed first.
@ -818,7 +828,8 @@ fn goto_impl(
[] => unreachable!("`locations` should be non-empty for `goto_impl`"),
_locations => {
let picker = Picker::new(locations, cwdir, move |cx, location, action| {
jump_to_location(cx.editor, location, offset_encoding, action)
let Some(l) = location else { return };
jump_to_location(cx.editor, l, offset_encoding, action)
})
.with_preview(move |_editor, location| Some(location_to_file_location(location)));
compositor.push(Box::new(overlaid(picker)));

@ -1413,7 +1413,8 @@ fn lsp_workspace_command(
commands,
(),
move |cx, LsIdCommand(ls_id, command), _action| {
execute_lsp_command(cx.editor, *ls_id, command.clone());
let Some(c) = command else { return };
execute_lsp_command(cx.editor, *ls_id, c.clone());
},
);
compositor.push(Box::new(overlaid(picker)))

@ -217,7 +217,11 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker
});
log::debug!("file_picker init {:?}", Instant::now().duration_since(now));
let picker = Picker::new(Vec::new(), root, move |cx, path: &PathBuf, action| {
let picker = Picker::new(
Vec::new(),
root,
move |cx, path_buff: Option<&PathBuf>, action| {
let Some(path) = path_buff else { return };
if let Err(e) = cx.editor.open(path, action) {
let err = if let Some(err) = e.source() {
format!("{}", err)
@ -226,7 +230,8 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker
};
cx.editor.set_error(err);
}
})
},
)
.with_preview(|_editor, path| Some((path.clone().into(), None)));
let injector = picker.injector();
let timeout = std::time::Instant::now() + std::time::Duration::from_millis(30);

@ -35,7 +35,7 @@ use std::{
use crate::ui::{Prompt, PromptEvent};
use helix_core::{
char_idx_at_visual_offset, fuzzy::MATCHER, movement::Direction,
text_annotations::TextAnnotations, unicode::segmentation::UnicodeSegmentation, Position,
text_annotations::TextAnnotations, unicode::segmentation::UnicodeSegmentation, Position, Rope,
Syntax,
};
use helix_view::{
@ -57,6 +57,7 @@ pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024;
pub enum PathOrId {
Id(DocumentId),
Path(PathBuf),
Text(Rope),
}
impl PathOrId {
@ -65,10 +66,17 @@ impl PathOrId {
match self {
Path(path) => Path(helix_stdx::path::canonicalize(path)),
Id(id) => Id(id),
Text(rope) => Text(rope),
}
}
}
impl From<Rope> for PathOrId {
fn from(text: Rope) -> Self {
Self::Text(text)
}
}
impl From<PathBuf> for PathOrId {
fn from(v: PathBuf) -> Self {
Self::Path(v)
@ -98,6 +106,7 @@ pub enum CachedPreview {
pub enum Preview<'picker, 'editor> {
Cached(&'picker CachedPreview),
EditorDocument(&'editor Document),
Text(Box<Document>),
}
impl Preview<'_, '_> {
@ -105,6 +114,7 @@ impl Preview<'_, '_> {
match self {
Preview::EditorDocument(doc) => Some(doc),
Preview::Cached(CachedPreview::Document(doc)) => Some(doc),
Preview::Text(doc) => Some(doc.as_ref()),
_ => None,
}
}
@ -112,6 +122,7 @@ impl Preview<'_, '_> {
/// Alternate text to show for the preview.
fn placeholder(&self) -> &str {
match *self {
Self::Text(_) => "<Text>",
Self::EditorDocument(_) => "<Invalid file location>",
Self::Cached(preview) => match preview {
CachedPreview::Document(_) => "<Invalid file location>",
@ -222,7 +233,7 @@ impl<T: Item + 'static> Picker<T> {
pub fn new(
options: Vec<T>,
editor_data: T::Data,
callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
callback_fn: impl Fn(&mut Context, Option<&T>, Action) + 'static,
) -> Self {
let matcher = Nucleo::new(
Config::DEFAULT,
@ -247,7 +258,7 @@ impl<T: Item + 'static> Picker<T> {
pub fn with_stream(
matcher: Nucleo<T>,
injector: Injector<T>,
callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
callback_fn: impl Fn(&mut Context, Option<&T>, Action) + 'static,
) -> Self {
Self::with(matcher, injector.editor_data, injector.shutown, callback_fn)
}
@ -256,7 +267,7 @@ impl<T: Item + 'static> Picker<T> {
matcher: Nucleo<T>,
editor_data: Arc<T::Data>,
shutdown: Arc<AtomicBool>,
callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
callback_fn: impl Fn(&mut Context, Option<&T>, Action) + 'static,
) -> Self {
let prompt = Prompt::new(
"".into(),
@ -440,6 +451,10 @@ impl<T: Item + 'static> Picker<T> {
let doc = editor.documents.get(&id).unwrap();
Preview::EditorDocument(doc)
}
PathOrId::Text(rope) => {
let doc = Document::from(rope, None, editor.config.clone());
Preview::Text(Box::new(doc))
}
}
}
@ -455,6 +470,7 @@ impl<T: Item + 'static> Picker<T> {
Some(CachedPreview::Document(ref mut doc)) => doc,
_ => return EventResult::Consumed(None),
},
PathOrId::Text(_) => return EventResult::Consumed(None),
};
let mut callback: Option<compositor::Callback> = None;
@ -502,6 +518,7 @@ impl<T: Item + 'static> Picker<T> {
}
_ => return,
},
PathOrId::Text(_rope) => return,
};
doc.syntax = Some(syntax);
};
@ -885,26 +902,18 @@ impl<T: Item + 'static + Send + Sync> Component for Picker<T> {
}
key!(Esc) | ctrl!('c') => return close_fn(self),
alt!(Enter) => {
if let Some(option) = self.selection() {
(self.callback_fn)(ctx, option, Action::Load);
}
(self.callback_fn)(ctx, self.selection(), Action::Load);
}
key!(Enter) => {
if let Some(option) = self.selection() {
(self.callback_fn)(ctx, option, Action::Replace);
}
(self.callback_fn)(ctx, self.selection(), Action::Replace);
return close_fn(self);
}
ctrl!('s') => {
if let Some(option) = self.selection() {
(self.callback_fn)(ctx, option, Action::HorizontalSplit);
}
(self.callback_fn)(ctx, self.selection(), Action::HorizontalSplit);
return close_fn(self);
}
ctrl!('v') => {
if let Some(option) = self.selection() {
(self.callback_fn)(ctx, option, Action::VerticalSplit);
}
(self.callback_fn)(ctx, self.selection(), Action::VerticalSplit);
return close_fn(self);
}
ctrl!('t') => {
@ -945,7 +954,7 @@ impl<T: Item> Drop for Picker<T> {
}
}
type PickerCallback<T> = Box<dyn Fn(&mut Context, &T, Action)>;
type PickerCallback<T> = Box<dyn Fn(&mut Context, Option<&T>, Action)>;
/// Returns a new list of options to replace the contents of the picker
/// when called with the current picker query,

Loading…
Cancel
Save