Replace uses of lsp::Location with a custom Location type

The lsp location type has the lsp's URI type and a range. We can replace
that with a custom type private to the lsp commands module that uses the
core URI type instead.

We can't entirely replace the type with a new Location type in core.
That type might look like:

    pub struct Location {
        uri: crate::Uri,
        range: crate::Range,
    }

But we can't convert every `lsp::Location` to this type because for
definitions, references and diagnostics language servers send documents
which we haven't opened yet, so we don't have the information to convert
an `lsp::Range` (line+col) to a `helix_core::Range` (char indexing).

This cleans up the picker definitions in this file so that they can all
use helpers like `jump_to_location` and `location_to_file_location` for
the picker preview. It also removes the only use of the deprecated
`PathOrId::from_path_buf` function, allowing us to drop the owned
variant of that type in the child commit.
This commit is contained in:
Michael Davis 2024-08-08 11:03:29 -04:00
parent ff33b07756
commit 606b957172
No known key found for this signature in database
2 changed files with 105 additions and 115 deletions

View file

@ -1,4 +1,7 @@
use std::path::{Path, PathBuf}; use std::{
fmt,
path::{Path, PathBuf},
};
/// A generic pointer to a file location. /// A generic pointer to a file location.
/// ///
@ -47,6 +50,14 @@ impl TryFrom<Uri> for PathBuf {
} }
} }
impl fmt::Display for Uri {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::File(path) => write!(f, "{}", path.display()),
}
}
}
#[derive(Debug)] #[derive(Debug)]
pub struct UrlConversionError { pub struct UrlConversionError {
source: url::Url, source: url::Url,
@ -59,11 +70,16 @@ pub enum UrlConversionErrorKind {
UnableToConvert, UnableToConvert,
} }
impl std::fmt::Display for UrlConversionError { impl fmt::Display for UrlConversionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.kind { match self.kind {
UrlConversionErrorKind::UnsupportedScheme => { UrlConversionErrorKind::UnsupportedScheme => {
write!(f, "unsupported scheme in URL: {}", self.source.scheme()) write!(
f,
"unsupported scheme '{}' in URL {}",
self.source.scheme(),
self.source
)
} }
UrlConversionErrorKind::UnableToConvert => { UrlConversionErrorKind::UnableToConvert => {
write!(f, "unable to convert URL to file path: {}", self.source) write!(f, "unable to convert URL to file path: {}", self.source)

View file

@ -34,7 +34,7 @@ use crate::{
use std::{ use std::{
cmp::Ordering, cmp::Ordering,
collections::{BTreeMap, HashSet}, collections::{BTreeMap, HashSet},
fmt::{Display, Write}, fmt::Display,
future::Future, future::Future,
path::Path, path::Path,
}; };
@ -61,10 +61,31 @@ macro_rules! language_server_with_feature {
}}; }};
} }
/// A wrapper around `lsp::Location` that swaps out the LSP URI for `helix_core::Uri`.
#[derive(Debug, Clone, PartialEq, Eq)]
struct Location {
uri: Uri,
range: lsp::Range,
}
fn lsp_location_to_location(location: lsp::Location) -> Option<Location> {
let uri = match location.uri.try_into() {
Ok(uri) => uri,
Err(err) => {
log::warn!("discarding invalid or unsupported URI: {err}");
return None;
}
};
Some(Location {
uri,
range: location.range,
})
}
struct SymbolInformationItem { struct SymbolInformationItem {
location: Location,
symbol: lsp::SymbolInformation, symbol: lsp::SymbolInformation,
offset_encoding: OffsetEncoding, offset_encoding: OffsetEncoding,
uri: Uri,
} }
struct DiagnosticStyles { struct DiagnosticStyles {
@ -75,35 +96,35 @@ struct DiagnosticStyles {
} }
struct PickerDiagnostic { struct PickerDiagnostic {
uri: Uri, location: Location,
diag: lsp::Diagnostic, diag: lsp::Diagnostic,
offset_encoding: OffsetEncoding, offset_encoding: OffsetEncoding,
} }
fn uri_to_file_location<'a>(uri: &'a Uri, range: &lsp::Range) -> Option<FileLocation<'a>> { fn location_to_file_location(location: &Location) -> Option<FileLocation> {
let path = uri.as_path()?; let path = location.uri.as_path()?;
let line = Some((range.start.line as usize, range.end.line as usize)); let line = Some((
location.range.start.line as usize,
location.range.end.line as usize,
));
Some((path.into(), line)) Some((path.into(), line))
} }
fn jump_to_location( fn jump_to_location(
editor: &mut Editor, editor: &mut Editor,
location: &lsp::Location, location: &Location,
offset_encoding: OffsetEncoding, offset_encoding: OffsetEncoding,
action: Action, action: Action,
) { ) {
let (view, doc) = current!(editor); let (view, doc) = current!(editor);
push_jump(view, doc); push_jump(view, doc);
let path = match location.uri.to_file_path() { let Some(path) = location.uri.as_path() else {
Ok(path) => path, let err = format!("unable to convert URI to filepath: {:?}", location.uri);
Err(_) => { editor.set_error(err);
let err = format!("unable to convert URI to filepath: {}", location.uri); return;
editor.set_error(err);
return;
}
}; };
jump_to_position(editor, &path, location.range, offset_encoding, action); jump_to_position(editor, path, location.range, offset_encoding, action);
} }
fn jump_to_position( fn jump_to_position(
@ -196,7 +217,10 @@ fn diag_picker(
for (diag, ls) in diags { for (diag, ls) in diags {
if let Some(ls) = cx.editor.language_server_by_id(ls) { if let Some(ls) = cx.editor.language_server_by_id(ls) {
flat_diag.push(PickerDiagnostic { flat_diag.push(PickerDiagnostic {
uri: uri.clone(), location: Location {
uri: uri.clone(),
range: diag.range,
},
diag, diag,
offset_encoding: ls.offset_encoding(), offset_encoding: ls.offset_encoding(),
}); });
@ -243,7 +267,7 @@ fn diag_picker(
// between message code and message // between message code and message
2, 2,
ui::PickerColumn::new("path", |item: &PickerDiagnostic, _| { ui::PickerColumn::new("path", |item: &PickerDiagnostic, _| {
if let Some(path) = item.uri.as_path() { if let Some(path) = item.location.uri.as_path() {
path::get_truncated_path(path) path::get_truncated_path(path)
.to_string_lossy() .to_string_lossy()
.to_string() .to_string()
@ -261,26 +285,14 @@ fn diag_picker(
primary_column, primary_column,
flat_diag, flat_diag,
styles, styles,
move |cx, move |cx, diag, action| {
PickerDiagnostic { jump_to_location(cx.editor, &diag.location, diag.offset_encoding, action);
uri,
diag,
offset_encoding,
},
action| {
let Some(path) = uri.as_path() else {
return;
};
jump_to_position(cx.editor, path, diag.range, *offset_encoding, action);
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
view.diagnostics_handler view.diagnostics_handler
.immediately_show_diagnostic(doc, view.id); .immediately_show_diagnostic(doc, view.id);
}, },
) )
.with_preview(move |_editor, PickerDiagnostic { uri, diag, .. }| { .with_preview(move |_editor, diag| location_to_file_location(&diag.location))
let line = Some((diag.range.start.line as usize, diag.range.end.line as usize));
Some((uri.as_path()?.into(), line))
})
.truncate_start(false) .truncate_start(false)
} }
@ -303,7 +315,10 @@ pub fn symbol_picker(cx: &mut Context) {
container_name: None, container_name: None,
}, },
offset_encoding, offset_encoding,
uri: uri.clone(), location: Location {
uri: uri.clone(),
range: symbol.selection_range,
},
}); });
for child in symbol.children.into_iter().flatten() { for child in symbol.children.into_iter().flatten() {
nested_to_flat(list, file, uri, child, offset_encoding); nested_to_flat(list, file, uri, child, offset_encoding);
@ -337,7 +352,10 @@ pub fn symbol_picker(cx: &mut Context) {
lsp::DocumentSymbolResponse::Flat(symbols) => symbols lsp::DocumentSymbolResponse::Flat(symbols) => symbols
.into_iter() .into_iter()
.map(|symbol| SymbolInformationItem { .map(|symbol| SymbolInformationItem {
uri: doc_uri.clone(), location: Location {
uri: doc_uri.clone(),
range: symbol.location.range,
},
symbol, symbol,
offset_encoding, offset_encoding,
}) })
@ -392,17 +410,10 @@ pub fn symbol_picker(cx: &mut Context) {
symbols, symbols,
(), (),
move |cx, item, action| { move |cx, item, action| {
jump_to_location( jump_to_location(cx.editor, &item.location, item.offset_encoding, action);
cx.editor,
&item.symbol.location,
item.offset_encoding,
action,
);
}, },
) )
.with_preview(move |_editor, item| { .with_preview(move |_editor, item| location_to_file_location(&item.location))
uri_to_file_location(&item.uri, &item.symbol.location.range)
})
.truncate_start(false); .truncate_start(false);
compositor.push(Box::new(overlaid(picker))) compositor.push(Box::new(overlaid(picker)))
@ -453,8 +464,11 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
} }
}; };
Some(SymbolInformationItem { Some(SymbolInformationItem {
location: Location {
uri,
range: symbol.location.range,
},
symbol, symbol,
uri,
offset_encoding, offset_encoding,
}) })
}) })
@ -490,7 +504,7 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
}) })
.without_filtering(), .without_filtering(),
ui::PickerColumn::new("path", |item: &SymbolInformationItem, _| { ui::PickerColumn::new("path", |item: &SymbolInformationItem, _| {
if let Some(path) = item.uri.as_path() { if let Some(path) = item.location.uri.as_path() {
path::get_relative_path(path) path::get_relative_path(path)
.to_string_lossy() .to_string_lossy()
.to_string() .to_string()
@ -507,15 +521,10 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
[], [],
(), (),
move |cx, item, action| { move |cx, item, action| {
jump_to_location( jump_to_location(cx.editor, &item.location, item.offset_encoding, action);
cx.editor,
&item.symbol.location,
item.offset_encoding,
action,
);
}, },
) )
.with_preview(|_editor, item| uri_to_file_location(&item.uri, &item.symbol.location.range)) .with_preview(|_editor, item| location_to_file_location(&item.location))
.with_dynamic_query(get_symbols, None) .with_dynamic_query(get_symbols, None)
.truncate_start(false); .truncate_start(false);
@ -847,7 +856,7 @@ impl Display for ApplyEditErrorKind {
fn goto_impl( fn goto_impl(
editor: &mut Editor, editor: &mut Editor,
compositor: &mut Compositor, compositor: &mut Compositor,
locations: Vec<lsp::Location>, locations: Vec<Location>,
offset_encoding: OffsetEncoding, offset_encoding: OffsetEncoding,
) { ) {
let cwdir = helix_stdx::env::current_working_dir(); let cwdir = helix_stdx::env::current_working_dir();
@ -860,80 +869,41 @@ fn goto_impl(
_locations => { _locations => {
let columns = [ui::PickerColumn::new( let columns = [ui::PickerColumn::new(
"location", "location",
|item: &lsp::Location, cwdir: &std::path::PathBuf| { |item: &Location, cwdir: &std::path::PathBuf| {
// The preallocation here will overallocate a few characters since it will account for the let path = if let Some(path) = item.uri.as_path() {
// URL's scheme, which is not used most of the time since that scheme will be "file://". path.strip_prefix(cwdir).unwrap_or(path).to_string_lossy()
// 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() {
// We don't convert to a `helix_core::Uri` here because we've already checked the scheme.
// This path won't be normalized but it's only used for display.
res.push_str(
&path.strip_prefix(cwdir).unwrap_or(&path).to_string_lossy(),
);
}
} else { } else {
// Never allocates since we declared the string with this capacity already. item.uri.to_string().into()
res.push_str(item.uri.as_str()); };
}
// Most commonly, this will not allocate, especially on Unix systems where the root prefix format!("{path}:{}", item.range.start.line + 1).into()
// 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| { let picker = Picker::new(columns, 0, locations, cwdir, move |cx, location, action| {
jump_to_location(cx.editor, location, offset_encoding, action) jump_to_location(cx.editor, location, offset_encoding, action)
}) })
.with_preview(move |_editor, location| { .with_preview(move |_editor, location| location_to_file_location(location));
use crate::ui::picker::PathOrId;
let lines = Some((
location.range.start.line as usize,
location.range.end.line as usize,
));
// TODO: we should avoid allocating by doing the Uri conversion ahead of time.
//
// To do this, introduce a `Location` type in `helix-core` that reuses the core
// `Uri` type instead of the LSP `Url` type and replaces the LSP `Range` type.
// Refactor the callers of `goto_impl` to pass iterators that translate the
// LSP location type to the custom one in core, or have them collect and pass
// `Vec<Location>`s. Replace the `uri_to_file_location` function with
// `location_to_file_location` that takes only `&helix_core::Location` as
// parameters.
//
// By doing this we can also eliminate the duplicated URI info in the
// `SymbolInformationItem` type and introduce a custom Symbol type in `helix-core`
// which will be reused in the future for tree-sitter based symbol pickers.
let path = Uri::try_from(&location.uri).ok()?.as_path_buf()?;
#[allow(deprecated)]
Some((PathOrId::from_path_buf(path), lines))
});
compositor.push(Box::new(overlaid(picker))); compositor.push(Box::new(overlaid(picker)));
} }
} }
} }
fn to_locations(definitions: Option<lsp::GotoDefinitionResponse>) -> Vec<lsp::Location> { fn to_locations(definitions: Option<lsp::GotoDefinitionResponse>) -> Vec<Location> {
match definitions { match definitions {
Some(lsp::GotoDefinitionResponse::Scalar(location)) => vec![location], Some(lsp::GotoDefinitionResponse::Scalar(location)) => {
Some(lsp::GotoDefinitionResponse::Array(locations)) => locations, lsp_location_to_location(location).into_iter().collect()
}
Some(lsp::GotoDefinitionResponse::Array(locations)) => locations
.into_iter()
.flat_map(lsp_location_to_location)
.collect(),
Some(lsp::GotoDefinitionResponse::Link(locations)) => locations Some(lsp::GotoDefinitionResponse::Link(locations)) => locations
.into_iter() .into_iter()
.map(|location_link| lsp::Location { .map(|location_link| {
uri: location_link.target_uri, lsp::Location::new(location_link.target_uri, location_link.target_range)
range: location_link.target_range,
}) })
.flat_map(lsp_location_to_location)
.collect(), .collect(),
None => Vec::new(), None => Vec::new(),
} }
@ -1018,7 +988,11 @@ pub fn goto_reference(cx: &mut Context) {
cx.callback( cx.callback(
future, future,
move |editor, compositor, response: Option<Vec<lsp::Location>>| { move |editor, compositor, response: Option<Vec<lsp::Location>>| {
let items = response.unwrap_or_default(); let items: Vec<Location> = response
.into_iter()
.flatten()
.flat_map(lsp_location_to_location)
.collect();
if items.is_empty() { if items.is_empty() {
editor.set_error("No references found."); editor.set_error("No references found.");
} else { } else {