diff --git a/Cargo.lock b/Cargo.lock index 6f38f0034edf..c1eab5d79051 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1346,6 +1346,8 @@ dependencies = [ "etcetera", "ropey", "tempfile", + "thiserror", + "url", "which", ] diff --git a/helix-stdx/Cargo.toml b/helix-stdx/Cargo.toml index 540a1b99a6cc..715371bad40f 100644 --- a/helix-stdx/Cargo.toml +++ b/helix-stdx/Cargo.toml @@ -15,6 +15,8 @@ homepage.workspace = true dunce = "1.0" etcetera = "0.8" ropey = { version = "1.6.1", default-features = false } +thiserror = "1.0.57" +url = "2.5.0" which = "6.0" [dev-dependencies] diff --git a/helix-stdx/src/lib.rs b/helix-stdx/src/lib.rs index 68fe3ec37702..7cd7ffd8e844 100644 --- a/helix-stdx/src/lib.rs +++ b/helix-stdx/src/lib.rs @@ -1,3 +1,4 @@ pub mod env; pub mod path; pub mod rope; +pub mod uri; diff --git a/helix-stdx/src/uri.rs b/helix-stdx/src/uri.rs new file mode 100644 index 000000000000..999d71dbf188 --- /dev/null +++ b/helix-stdx/src/uri.rs @@ -0,0 +1,25 @@ +use std::path::PathBuf; + +use thiserror::Error; +use url::Url; + +#[derive(Debug, Error)] +pub enum FilePathError<'a> { + #[error("unsupported scheme in URI: {0}")] + UnsupportedScheme(&'a Url), + #[error("unable to convert URI to file path: {0}")] + UnableToConvert(&'a Url), +} + +/// Converts a [`Url`] into a [`PathBuf`]. +/// +/// Unlike [`Url::to_file_path`], this method respects the uri's scheme +/// and returns `Ok(None)` if the scheme was not "file". +pub fn uri_to_file_path(uri: &Url) -> Result { + if uri.scheme() == "file" { + uri.to_file_path() + .map_err(|_| FilePathError::UnableToConvert(uri)) + } else { + Err(FilePathError::UnsupportedScheme(uri)) + } +} diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 30df3981c896..0fe06937cb12 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -6,7 +6,7 @@ use helix_lsp::{ util::lsp_range_to_range, LspProgressMap, }; -use helix_stdx::path::get_relative_path; +use helix_stdx::{path::get_relative_path, uri::uri_to_file_path}; use helix_view::{ align_view, document::DocumentSavedEventResult, @@ -723,10 +723,10 @@ impl Application { } } Notification::PublishDiagnostics(mut params) => { - let path = match params.uri.to_file_path() { + let path = match uri_to_file_path(¶ms.uri) { Ok(path) => path, - Err(_) => { - log::error!("Unsupported file URI: {}", params.uri); + Err(err) => { + log::error!("{err}"); return; } }; @@ -1127,10 +1127,10 @@ impl Application { .. } = params; - let path = match uri.to_file_path() { + let path = match uri_to_file_path(&uri) { Ok(path) => path, Err(err) => { - log::error!("unsupported file URI: {}: {:?}", uri, err); + log::error!("{err}"); return lsp::ShowDocumentResult { success: false }; } }; diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index a1f7bf17dc88..a4611b223cdd 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -17,7 +17,7 @@ use tui::{ use super::{align_view, push_jump, Align, Context, Editor}; use helix_core::{syntax::LanguageServerFeature, text_annotations::InlineAnnotation, Selection}; -use helix_stdx::path; +use helix_stdx::{path, uri::uri_to_file_path}; use helix_view::{ document::{DocumentInlayHints, DocumentInlayHintsId}, editor::Action, @@ -79,6 +79,7 @@ impl ui::menu::Item for lsp::Location { // 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<()> { + // We don't use `uri_to_file_path` here, since we've already checked the scheme. let path = self.uri.to_file_path().ok()?; res.push_str(&path.strip_prefix(cwdir).unwrap_or(&path).to_string_lossy()); Some(()) @@ -110,7 +111,7 @@ impl ui::menu::Item for SymbolInformationItem { 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() { + match uri_to_file_path(&self.symbol.location.uri) { Ok(path) => { let get_relative_path = path::get_relative_path(path.as_path()); format!( @@ -120,7 +121,7 @@ impl ui::menu::Item for SymbolInformationItem { ) .into() } - Err(_) => format!("{} ({})", &self.symbol.name, &self.symbol.location.uri).into(), + _ => format!("{} ({})", &self.symbol.name, &self.symbol.location.uri).into(), } } } @@ -182,13 +183,20 @@ impl ui::menu::Item for PickerDiagnostic { } } -fn location_to_file_location(location: &lsp::Location) -> FileLocation { - let path = location.uri.to_file_path().unwrap(); - let line = Some(( - location.range.start.line as usize, - location.range.end.line as usize, - )); - (path.into(), line) +/// Creates a [`FileLocation`] from an [`lsp::Location`], for use with picker previews. +/// +/// This will return [`None`] if the location uri's scheme is not "file". +fn location_to_file_location(location: &lsp::Location) -> Option { + match uri_to_file_path(&location.uri) { + Ok(path) => { + let line = Some(( + location.range.start.line as usize, + location.range.end.line as usize, + )); + Some((path.into(), line)) + } + Err(_) => None, + } } fn jump_to_location( @@ -200,11 +208,10 @@ fn jump_to_location( let (view, doc) = current!(editor); push_jump(view, doc); - let path = match location.uri.to_file_path() { + let path = match uri_to_file_path(&location.uri) { Ok(path) => path, - Err(_) => { - let err = format!("unable to convert URI to filepath: {}", location.uri); - editor.set_error(err); + Err(conversion_err) => { + editor.set_error(conversion_err.to_string()); return; } }; @@ -246,7 +253,7 @@ fn sym_picker(symbols: Vec, current_path: Option for ApplyEditErrorKind { + fn from(err: std::io::Error) -> Self { + ApplyEditErrorKind::IoError(err) + } +} + impl ToString for ApplyEditErrorKind { fn to_string(&self) -> String { match self { @@ -72,6 +81,17 @@ impl ToString for ApplyEditErrorKind { } impl Editor { + fn uri_to_file_path(&mut self, uri: &helix_lsp::Url) -> Result { + match uri_to_file_path(uri) { + Ok(path) => Ok(path), + Err(err) => { + log::error!("{err}"); + self.set_error(err.to_string()); + Err(ApplyEditErrorKind::UnknownURISchema) + } + } + } + fn apply_text_edits( &mut self, uri: &helix_lsp::Url, @@ -79,15 +99,7 @@ impl Editor { text_edits: Vec, offset_encoding: OffsetEncoding, ) -> Result<(), ApplyEditErrorKind> { - let path = match uri.to_file_path() { - Ok(path) => path, - Err(_) => { - let err = format!("unable to convert URI to filepath: {}", uri); - log::error!("{}", err); - self.set_error(err); - return Err(ApplyEditErrorKind::UnknownURISchema); - } - }; + let path = self.uri_to_file_path(uri)?; let doc_id = match self.open(&path, Action::Load) { Ok(doc_id) => doc_id, @@ -158,9 +170,9 @@ impl Editor { for (i, operation) in operations.iter().enumerate() { match operation { lsp::DocumentChangeOperation::Op(op) => { - self.apply_document_resource_op(op).map_err(|io| { + self.apply_document_resource_op(op).map_err(|err| { ApplyEditError { - kind: ApplyEditErrorKind::IoError(io), + kind: err, failed_change_idx: i, } })?; @@ -214,12 +226,15 @@ impl Editor { Ok(()) } - fn apply_document_resource_op(&mut self, op: &lsp::ResourceOp) -> std::io::Result<()> { + fn apply_document_resource_op( + &mut self, + op: &lsp::ResourceOp, + ) -> Result<(), ApplyEditErrorKind> { use lsp::ResourceOp; use std::fs; match op { ResourceOp::Create(op) => { - let path = op.uri.to_file_path().unwrap(); + let path = self.uri_to_file_path(&op.uri)?; let ignore_if_exists = op.options.as_ref().map_or(false, |options| { !options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false) }); @@ -236,7 +251,7 @@ impl Editor { } } ResourceOp::Delete(op) => { - let path = op.uri.to_file_path().unwrap(); + let path = self.uri_to_file_path(&op.uri)?; if path.is_dir() { let recursive = op .options @@ -255,8 +270,8 @@ impl Editor { } } ResourceOp::Rename(op) => { - let from = op.old_uri.to_file_path().unwrap(); - let to = op.new_uri.to_file_path().unwrap(); + let from = self.uri_to_file_path(&op.old_uri)?; + let to = self.uri_to_file_path(&op.new_uri)?; let ignore_if_exists = op.options.as_ref().map_or(false, |options| { !options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false) });