Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-term/src/commands/lsp.rs')
-rw-r--r--helix-term/src/commands/lsp.rs274
1 files changed, 148 insertions, 126 deletions
diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs
index bf9747a4..2292569b 100644
--- a/helix-term/src/commands/lsp.rs
+++ b/helix-term/src/commands/lsp.rs
@@ -9,10 +9,7 @@ use helix_lsp::{
Client, LanguageServerId, OffsetEncoding,
};
use tokio_stream::StreamExt;
-use tui::{
- text::{Span, Spans},
- widgets::Row,
-};
+use tui::{text::Span, widgets::Row};
use super::{align_view, push_jump, Align, Context, Editor};
@@ -62,69 +59,11 @@ macro_rules! language_server_with_feature {
}};
}
-impl ui::menu::Item for lsp::Location {
- /// Current working directory.
- type Data = PathBuf;
-
- fn format(&self, cwdir: &Self::Data) -> Row {
- // The preallocation here will overallocate a few characters since it will account for the
- // URL's scheme, which is not used most of the time since that scheme will be "file://".
- // Those extra chars will be used to avoid allocating when writing the line number (in the
- // common case where it has 5 digits or less, which should be enough for a cast majority
- // of usages).
- let mut res = String::with_capacity(self.uri.as_str().len());
-
- if self.uri.scheme() == "file" {
- // With the preallocation above and UTF-8 paths already, this closure will do one (1)
- // allocation, for `to_file_path`, else there will be two (2), with `to_string_lossy`.
- let mut write_path_to_res = || -> Option<()> {
- let path = self.uri.to_file_path().ok()?;
- res.push_str(&path.strip_prefix(cwdir).unwrap_or(&path).to_string_lossy());
- Some(())
- };
- write_path_to_res();
- } else {
- // Never allocates since we declared the string with this capacity already.
- res.push_str(self.uri.as_str());
- }
-
- // Most commonly, this will not allocate, especially on Unix systems where the root prefix
- // is a simple `/` and not `C:\` (with whatever drive letter)
- write!(&mut res, ":{}", self.range.start.line + 1)
- .expect("Will only failed if allocating fail");
- res.into()
- }
-}
-
struct SymbolInformationItem {
symbol: lsp::SymbolInformation,
offset_encoding: OffsetEncoding,
}
-impl ui::menu::Item for SymbolInformationItem {
- /// Path to currently focussed document
- type Data = Option<lsp::Url>;
-
- fn format(&self, current_doc_path: &Self::Data) -> Row {
- if current_doc_path.as_ref() == Some(&self.symbol.location.uri) {
- self.symbol.name.as_str().into()
- } else {
- match self.symbol.location.uri.to_file_path() {
- Ok(path) => {
- let get_relative_path = path::get_relative_path(path.as_path());
- format!(
- "{} ({})",
- &self.symbol.name,
- get_relative_path.to_string_lossy()
- )
- .into()
- }
- Err(_) => format!("{} ({})", &self.symbol.name, &self.symbol.location.uri).into(),
- }
- }
- }
-}
-
struct DiagnosticStyles {
hint: Style,
info: Style,
@@ -138,48 +77,6 @@ struct PickerDiagnostic {
offset_encoding: OffsetEncoding,
}
-impl ui::menu::Item for PickerDiagnostic {
- type Data = (DiagnosticStyles, DiagnosticsFormat);
-
- fn format(&self, (styles, format): &Self::Data) -> Row {
- let mut style = self
- .diag
- .severity
- .map(|s| match s {
- DiagnosticSeverity::HINT => styles.hint,
- DiagnosticSeverity::INFORMATION => styles.info,
- DiagnosticSeverity::WARNING => styles.warning,
- DiagnosticSeverity::ERROR => styles.error,
- _ => Style::default(),
- })
- .unwrap_or_default();
-
- // remove background as it is distracting in the picker list
- style.bg = None;
-
- let code = match self.diag.code.as_ref() {
- Some(NumberOrString::Number(n)) => format!(" ({n})"),
- Some(NumberOrString::String(s)) => format!(" ({s})"),
- None => String::new(),
- };
-
- let path = match format {
- DiagnosticsFormat::HideSourcePath => String::new(),
- DiagnosticsFormat::ShowSourcePath => {
- let path = path::get_truncated_path(&self.path);
- format!("{}: ", path.to_string_lossy())
- }
- };
-
- Spans::from(vec![
- Span::raw(path),
- Span::styled(&self.diag.message, style),
- Span::styled(code, style),
- ])
- .into()
- }
-}
-
fn location_to_file_location(location: &lsp::Location) -> FileLocation {
let path = location.uri.to_file_path().unwrap();
let line = Some((
@@ -241,16 +138,82 @@ fn jump_to_position(
}
}
-type SymbolPicker = Picker<SymbolInformationItem, Option<lsp::Url>>;
+fn display_symbol_kind(kind: lsp::SymbolKind) -> &'static str {
+ match kind {
+ lsp::SymbolKind::FILE => "file",
+ lsp::SymbolKind::MODULE => "module",
+ lsp::SymbolKind::NAMESPACE => "namespace",
+ lsp::SymbolKind::PACKAGE => "package",
+ lsp::SymbolKind::CLASS => "class",
+ lsp::SymbolKind::METHOD => "method",
+ lsp::SymbolKind::PROPERTY => "property",
+ lsp::SymbolKind::FIELD => "field",
+ lsp::SymbolKind::CONSTRUCTOR => "construct",
+ lsp::SymbolKind::ENUM => "enum",
+ lsp::SymbolKind::INTERFACE => "interface",
+ lsp::SymbolKind::FUNCTION => "function",
+ lsp::SymbolKind::VARIABLE => "variable",
+ lsp::SymbolKind::CONSTANT => "constant",
+ lsp::SymbolKind::STRING => "string",
+ lsp::SymbolKind::NUMBER => "number",
+ lsp::SymbolKind::BOOLEAN => "boolean",
+ lsp::SymbolKind::ARRAY => "array",
+ lsp::SymbolKind::OBJECT => "object",
+ lsp::SymbolKind::KEY => "key",
+ lsp::SymbolKind::NULL => "null",
+ lsp::SymbolKind::ENUM_MEMBER => "enummem",
+ lsp::SymbolKind::STRUCT => "struct",
+ lsp::SymbolKind::EVENT => "event",
+ lsp::SymbolKind::OPERATOR => "operator",
+ lsp::SymbolKind::TYPE_PARAMETER => "typeparam",
+ _ => {
+ log::warn!("Unknown symbol kind: {:?}", kind);
+ ""
+ }
+ }
+}
+
+type SymbolPicker = Picker<SymbolInformationItem, ()>;
+
+fn sym_picker(symbols: Vec<SymbolInformationItem>, workspace: bool) -> SymbolPicker {
+ let symbol_kind_column = ui::PickerColumn::new("kind", |item: &SymbolInformationItem, _| {
+ display_symbol_kind(item.symbol.kind).into()
+ });
+
+ let columns = if workspace {
+ vec![
+ symbol_kind_column,
+ ui::PickerColumn::new("name", |item: &SymbolInformationItem, _| {
+ item.symbol.name.as_str().into()
+ })
+ .without_filtering(),
+ ui::PickerColumn::new("path", |item: &SymbolInformationItem, _| {
+ match item.symbol.location.uri.to_file_path() {
+ Ok(path) => path::get_relative_path(path.as_path())
+ .to_string_lossy()
+ .to_string()
+ .into(),
+ Err(_) => item.symbol.location.uri.to_string().into(),
+ }
+ }),
+ ]
+ } else {
+ vec![
+ symbol_kind_column,
+ // Some symbols in the document symbol picker may have a URI that isn't
+ // the current file. It should be rare though, so we concatenate that
+ // URI in with the symbol name in this picker.
+ ui::PickerColumn::new("name", |item: &SymbolInformationItem, _| {
+ item.symbol.name.as_str().into()
+ }),
+ ]
+ };
-fn sym_picker(symbols: Vec<SymbolInformationItem>, current_path: Option<lsp::Url>) -> SymbolPicker {
- // TODO: drop current_path comparison and instead use workspace: bool flag?
- let columns = vec![];
Picker::new(
columns,
- 0,
+ 1, // name column
symbols,
- current_path,
+ (),
move |cx, item, action| {
jump_to_location(
cx.editor,
@@ -270,7 +233,7 @@ enum DiagnosticsFormat {
HideSourcePath,
}
-type DiagnosticsPicker = Picker<PickerDiagnostic, (DiagnosticStyles, DiagnosticsFormat)>;
+type DiagnosticsPicker = Picker<PickerDiagnostic, DiagnosticStyles>;
fn diag_picker(
cx: &Context,
@@ -302,12 +265,50 @@ fn diag_picker(
error: cx.editor.theme.get("error"),
};
- let columns = vec![];
+ let mut columns = vec![
+ ui::PickerColumn::new(
+ "severity",
+ |item: &PickerDiagnostic, styles: &DiagnosticStyles| {
+ match item.diag.severity {
+ Some(DiagnosticSeverity::HINT) => Span::styled("HINT", styles.hint),
+ Some(DiagnosticSeverity::INFORMATION) => Span::styled("INFO", styles.info),
+ Some(DiagnosticSeverity::WARNING) => Span::styled("WARN", styles.warning),
+ Some(DiagnosticSeverity::ERROR) => Span::styled("ERROR", styles.error),
+ _ => Span::raw(""),
+ }
+ .into()
+ },
+ ),
+ ui::PickerColumn::new("code", |item: &PickerDiagnostic, _| {
+ match item.diag.code.as_ref() {
+ Some(NumberOrString::Number(n)) => n.to_string().into(),
+ Some(NumberOrString::String(s)) => s.as_str().into(),
+ None => "".into(),
+ }
+ }),
+ ui::PickerColumn::new("message", |item: &PickerDiagnostic, _| {
+ item.diag.message.as_str().into()
+ }),
+ ];
+ let mut primary_column = 2; // message
+
+ if format == DiagnosticsFormat::ShowSourcePath {
+ columns.insert(
+ // between message code and message
+ 2,
+ ui::PickerColumn::new("path", |item: &PickerDiagnostic, _| {
+ let path = path::get_truncated_path(&item.path);
+ path.to_string_lossy().to_string().into()
+ }),
+ );
+ primary_column += 1;
+ }
+
Picker::new(
columns,
- 0,
+ primary_column,
flat_diag,
- (styles, format),
+ styles,
move |cx,
PickerDiagnostic {
path,
@@ -389,7 +390,6 @@ pub fn symbol_picker(cx: &mut Context) {
}
})
.collect();
- let current_url = doc.url();
if futures.is_empty() {
cx.editor
@@ -404,7 +404,7 @@ pub fn symbol_picker(cx: &mut Context) {
symbols.append(&mut lsp_items);
}
let call = move |_editor: &mut Editor, compositor: &mut Compositor| {
- let picker = sym_picker(symbols, current_url);
+ let picker = sym_picker(symbols, false);
compositor.push(Box::new(overlaid(picker)))
};
@@ -466,13 +466,12 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
.boxed()
};
- let current_url = doc.url();
let initial_symbols = get_symbols("".to_owned(), cx.editor);
cx.jobs.callback(async move {
let symbols = initial_symbols.await?;
let call = move |_editor: &mut Editor, compositor: &mut Compositor| {
- let picker = sym_picker(symbols, current_url);
+ let picker = sym_picker(symbols, true);
let dyn_picker = DynamicPicker::new(picker, Box::new(get_symbols));
compositor.push(Box::new(overlaid(dyn_picker)))
};
@@ -753,13 +752,6 @@ pub fn code_action(cx: &mut Context) {
});
}
-impl ui::menu::Item for lsp::Command {
- type Data = ();
- fn format(&self, _data: &Self::Data) -> Row {
- self.title.as_str().into()
- }
-}
-
pub fn execute_lsp_command(
editor: &mut Editor,
language_server_id: LanguageServerId,
@@ -829,7 +821,37 @@ fn goto_impl(
}
[] => unreachable!("`locations` should be non-empty for `goto_impl`"),
_locations => {
- let columns = vec![];
+ let columns = vec![ui::PickerColumn::new(
+ "location",
+ |item: &lsp::Location, cwdir: &std::path::PathBuf| {
+ // The preallocation here will overallocate a few characters since it will account for the
+ // URL's scheme, which is not used most of the time since that scheme will be "file://".
+ // Those extra chars will be used to avoid allocating when writing the line number (in the
+ // common case where it has 5 digits or less, which should be enough for a cast majority
+ // of usages).
+ let mut res = String::with_capacity(item.uri.as_str().len());
+
+ if item.uri.scheme() == "file" {
+ // With the preallocation above and UTF-8 paths already, this closure will do one (1)
+ // allocation, for `to_file_path`, else there will be two (2), with `to_string_lossy`.
+ if let Ok(path) = item.uri.to_file_path() {
+ res.push_str(
+ &path.strip_prefix(cwdir).unwrap_or(&path).to_string_lossy(),
+ );
+ }
+ } else {
+ // Never allocates since we declared the string with this capacity already.
+ res.push_str(item.uri.as_str());
+ }
+
+ // Most commonly, this will not allocate, especially on Unix systems where the root prefix
+ // is a simple `/` and not `C:\` (with whatever drive letter)
+ write!(&mut res, ":{}", item.range.start.line + 1)
+ .expect("Will only failed if allocating fail");
+ res.into()
+ },
+ )];
+
let picker = Picker::new(columns, 0, locations, cwdir, move |cx, location, action| {
jump_to_location(cx.editor, location, offset_encoding, action)
})