Unnamed repository; edit this file 'description' to name the repository.
Merge pull request #11486 from helix-editor/lsp-location-refactor
Replace uses of `lsp::Location` with a custom Location type
Pascal Kuthe 2024-10-04
parent 02b6f14 · parent da2b0a7 · commit 162028d
-rw-r--r--helix-core/src/uri.rs41
-rw-r--r--helix-term/src/commands/lsp.rs196
-rw-r--r--helix-term/src/ui/picker.rs21
-rw-r--r--helix-view/src/handlers/lsp.rs18
4 files changed, 121 insertions, 155 deletions
diff --git a/helix-core/src/uri.rs b/helix-core/src/uri.rs
index 4e03c58b..cbe0fadd 100644
--- a/helix-core/src/uri.rs
+++ b/helix-core/src/uri.rs
@@ -1,12 +1,18 @@
-use std::path::{Path, PathBuf};
+use std::{
+ fmt,
+ path::{Path, PathBuf},
+ sync::Arc,
+};
/// A generic pointer to a file location.
///
/// Currently this type only supports paths to local files.
+///
+/// Cloning this type is cheap: the internal representation uses an Arc.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[non_exhaustive]
pub enum Uri {
- File(PathBuf),
+ File(Arc<Path>),
}
impl Uri {
@@ -23,26 +29,18 @@ impl Uri {
Self::File(path) => Some(path),
}
}
-
- pub fn as_path_buf(self) -> Option<PathBuf> {
- match self {
- Self::File(path) => Some(path),
- }
- }
}
impl From<PathBuf> for Uri {
fn from(path: PathBuf) -> Self {
- Self::File(path)
+ Self::File(path.into())
}
}
-impl TryFrom<Uri> for PathBuf {
- type Error = ();
-
- fn try_from(uri: Uri) -> Result<Self, Self::Error> {
- match uri {
- Uri::File(path) => Ok(path),
+impl fmt::Display for Uri {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::File(path) => write!(f, "{}", path.display()),
}
}
}
@@ -59,11 +57,16 @@ pub enum UrlConversionErrorKind {
UnableToConvert,
}
-impl std::fmt::Display for UrlConversionError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+impl fmt::Display for UrlConversionError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.kind {
UrlConversionErrorKind::UnsupportedScheme => {
- write!(f, "unsupported scheme in URL: {}", self.source.scheme())
+ write!(
+ f,
+ "unsupported scheme '{}' in URL {}",
+ self.source.scheme(),
+ self.source
+ )
}
UrlConversionErrorKind::UnableToConvert => {
write!(f, "unable to convert URL to file path: {}", self.source)
@@ -77,7 +80,7 @@ impl std::error::Error for UrlConversionError {}
fn convert_url_to_uri(url: &url::Url) -> Result<Uri, UrlConversionErrorKind> {
if url.scheme() == "file" {
url.to_file_path()
- .map(|path| Uri::File(helix_stdx::path::normalize(path)))
+ .map(|path| Uri::File(helix_stdx::path::normalize(path).into()))
.map_err(|_| UrlConversionErrorKind::UnableToConvert)
} else {
Err(UrlConversionErrorKind::UnsupportedScheme)
diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs
index 93ac2a84..fcc0333e 100644
--- a/helix-term/src/commands/lsp.rs
+++ b/helix-term/src/commands/lsp.rs
@@ -34,7 +34,7 @@ use crate::{
use std::{
cmp::Ordering,
collections::{BTreeMap, HashSet},
- fmt::{Display, Write},
+ fmt::Display,
future::Future,
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 {
+ location: Location,
symbol: lsp::SymbolInformation,
offset_encoding: OffsetEncoding,
- uri: Uri,
}
struct DiagnosticStyles {
@@ -75,35 +96,35 @@ struct DiagnosticStyles {
}
struct PickerDiagnostic {
- uri: Uri,
+ location: Location,
diag: lsp::Diagnostic,
offset_encoding: OffsetEncoding,
}
-fn uri_to_file_location<'a>(uri: &'a Uri, range: &lsp::Range) -> Option<FileLocation<'a>> {
- let path = uri.as_path()?;
- let line = Some((range.start.line as usize, range.end.line as usize));
+fn location_to_file_location(location: &Location) -> Option<FileLocation> {
+ let path = location.uri.as_path()?;
+ let line = Some((
+ location.range.start.line as usize,
+ location.range.end.line as usize,
+ ));
Some((path.into(), line))
}
fn jump_to_location(
editor: &mut Editor,
- location: &lsp::Location,
+ location: &Location,
offset_encoding: OffsetEncoding,
action: Action,
) {
let (view, doc) = current!(editor);
push_jump(view, doc);
- let path = match location.uri.to_file_path() {
- Ok(path) => path,
- Err(_) => {
- let err = format!("unable to convert URI to filepath: {}", location.uri);
- editor.set_error(err);
- return;
- }
+ let Some(path) = location.uri.as_path() else {
+ let err = format!("unable to convert URI to filepath: {:?}", location.uri);
+ 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(
@@ -196,7 +217,10 @@ fn diag_picker(
for (diag, ls) in diags {
if let Some(ls) = cx.editor.language_server_by_id(ls) {
flat_diag.push(PickerDiagnostic {
- uri: uri.clone(),
+ location: Location {
+ uri: uri.clone(),
+ range: diag.range,
+ },
diag,
offset_encoding: ls.offset_encoding(),
});
@@ -243,7 +267,7 @@ fn diag_picker(
// between message code and message
2,
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)
.to_string_lossy()
.to_string()
@@ -261,26 +285,14 @@ fn diag_picker(
primary_column,
flat_diag,
styles,
- move |cx,
- PickerDiagnostic {
- uri,
- diag,
- offset_encoding,
- },
- action| {
- let Some(path) = uri.as_path() else {
- return;
- };
- jump_to_position(cx.editor, path, diag.range, *offset_encoding, action);
+ move |cx, diag, action| {
+ jump_to_location(cx.editor, &diag.location, diag.offset_encoding, action);
let (view, doc) = current!(cx.editor);
view.diagnostics_handler
.immediately_show_diagnostic(doc, view.id);
},
)
- .with_preview(move |_editor, PickerDiagnostic { uri, diag, .. }| {
- let line = Some((diag.range.start.line as usize, diag.range.end.line as usize));
- Some((uri.as_path()?.into(), line))
- })
+ .with_preview(move |_editor, diag| location_to_file_location(&diag.location))
.truncate_start(false)
}
@@ -303,7 +315,10 @@ pub fn symbol_picker(cx: &mut Context) {
container_name: None,
},
offset_encoding,
- uri: uri.clone(),
+ location: Location {
+ uri: uri.clone(),
+ range: symbol.selection_range,
+ },
});
for child in symbol.children.into_iter().flatten() {
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
.into_iter()
.map(|symbol| SymbolInformationItem {
- uri: doc_uri.clone(),
+ location: Location {
+ uri: doc_uri.clone(),
+ range: symbol.location.range,
+ },
symbol,
offset_encoding,
})
@@ -392,17 +410,10 @@ pub fn symbol_picker(cx: &mut Context) {
symbols,
(),
move |cx, item, action| {
- jump_to_location(
- cx.editor,
- &item.symbol.location,
- item.offset_encoding,
- action,
- );
+ jump_to_location(cx.editor, &item.location, item.offset_encoding, action);
},
)
- .with_preview(move |_editor, item| {
- uri_to_file_location(&item.uri, &item.symbol.location.range)
- })
+ .with_preview(move |_editor, item| location_to_file_location(&item.location))
.truncate_start(false);
compositor.push(Box::new(overlaid(picker)))
@@ -453,8 +464,11 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
}
};
Some(SymbolInformationItem {
+ location: Location {
+ uri,
+ range: symbol.location.range,
+ },
symbol,
- uri,
offset_encoding,
})
})
@@ -490,7 +504,7 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
})
.without_filtering(),
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)
.to_string_lossy()
.to_string()
@@ -507,15 +521,10 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
[],
(),
move |cx, item, action| {
- jump_to_location(
- cx.editor,
- &item.symbol.location,
- item.offset_encoding,
- action,
- );
+ jump_to_location(cx.editor, &item.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)
.truncate_start(false);
@@ -847,7 +856,7 @@ impl Display for ApplyEditErrorKind {
fn goto_impl(
editor: &mut Editor,
compositor: &mut Compositor,
- locations: Vec<lsp::Location>,
+ locations: Vec<Location>,
offset_encoding: OffsetEncoding,
) {
let cwdir = helix_stdx::env::current_working_dir();
@@ -860,80 +869,41 @@ fn goto_impl(
_locations => {
let columns = [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() {
- // 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(),
- );
- }
+ |item: &Location, cwdir: &std::path::PathBuf| {
+ let path = if let Some(path) = item.uri.as_path() {
+ 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());
- }
+ item.uri.to_string().into()
+ };
- // 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()
+ format!("{path}:{}", item.range.start.line + 1).into()
},
)];
let picker = Picker::new(columns, 0, locations, cwdir, move |cx, location, action| {
jump_to_location(cx.editor, location, offset_encoding, action)
})
- .with_preview(move |_editor, 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))
- });
+ .with_preview(move |_editor, location| location_to_file_location(location));
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 {
- Some(lsp::GotoDefinitionResponse::Scalar(location)) => vec![location],
- Some(lsp::GotoDefinitionResponse::Array(locations)) => locations,
+ Some(lsp::GotoDefinitionResponse::Scalar(location)) => {
+ 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
.into_iter()
- .map(|location_link| lsp::Location {
- uri: location_link.target_uri,
- range: location_link.target_range,
+ .map(|location_link| {
+ lsp::Location::new(location_link.target_uri, location_link.target_range)
})
+ .flat_map(lsp_location_to_location)
.collect(),
None => Vec::new(),
}
@@ -1018,7 +988,11 @@ pub fn goto_reference(cx: &mut Context) {
cx.callback(
future,
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() {
editor.set_error("No references found.");
} else {
diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs
index 82fe9689..ecf8111a 100644
--- a/helix-term/src/ui/picker.rs
+++ b/helix-term/src/ui/picker.rs
@@ -32,7 +32,7 @@ use std::{
borrow::Cow,
collections::HashMap,
io::Read,
- path::{Path, PathBuf},
+ path::Path,
sync::{
atomic::{self, AtomicUsize},
Arc,
@@ -63,26 +63,12 @@ pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024;
#[derive(PartialEq, Eq, Hash)]
pub enum PathOrId<'a> {
Id(DocumentId),
- // See [PathOrId::from_path_buf]: this will eventually become `Path(&Path)`.
- Path(Cow<'a, Path>),
-}
-
-impl<'a> PathOrId<'a> {
- /// Creates a [PathOrId] from a PathBuf
- ///
- /// # Deprecated
- /// The owned version of PathOrId will be removed in a future refactor
- /// and replaced with `&'a Path`. See the caller of this function for
- /// more details on its removal.
- #[deprecated]
- pub fn from_path_buf(path_buf: PathBuf) -> Self {
- Self::Path(Cow::Owned(path_buf))
- }
+ Path(&'a Path),
}
impl<'a> From<&'a Path> for PathOrId<'a> {
fn from(path: &'a Path) -> Self {
- Self::Path(Cow::Borrowed(path))
+ Self::Path(path)
}
}
@@ -581,7 +567,6 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
match path_or_id {
PathOrId::Path(path) => {
- let path = path.as_ref();
if let Some(doc) = editor.document_by_path(path) {
return Some((Preview::EditorDocument(doc), range));
}
diff --git a/helix-view/src/handlers/lsp.rs b/helix-view/src/handlers/lsp.rs
index 6aff2e50..1fd2289d 100644
--- a/helix-view/src/handlers/lsp.rs
+++ b/helix-view/src/handlers/lsp.rs
@@ -243,7 +243,7 @@ impl Editor {
match op {
ResourceOp::Create(op) => {
let uri = Uri::try_from(&op.uri)?;
- let path = uri.as_path_buf().expect("URIs are valid paths");
+ let path = uri.as_path().expect("URIs are valid paths");
let ignore_if_exists = op.options.as_ref().map_or(false, |options| {
!options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
});
@@ -255,13 +255,15 @@ impl Editor {
}
}
- fs::write(&path, [])?;
- self.language_servers.file_event_handler.file_changed(path);
+ fs::write(path, [])?;
+ self.language_servers
+ .file_event_handler
+ .file_changed(path.to_path_buf());
}
}
ResourceOp::Delete(op) => {
let uri = Uri::try_from(&op.uri)?;
- let path = uri.as_path_buf().expect("URIs are valid paths");
+ let path = uri.as_path().expect("URIs are valid paths");
if path.is_dir() {
let recursive = op
.options
@@ -270,11 +272,13 @@ impl Editor {
.unwrap_or(false);
if recursive {
- fs::remove_dir_all(&path)?
+ fs::remove_dir_all(path)?
} else {
- fs::remove_dir(&path)?
+ fs::remove_dir(path)?
}
- self.language_servers.file_event_handler.file_changed(path);
+ self.language_servers
+ .file_event_handler
+ .file_changed(path.to_path_buf());
} else if path.is_file() {
fs::remove_file(path)?;
}