diff --git a/Cargo.lock b/Cargo.lock index e622fbe59615..db27baa678e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1218,6 +1218,7 @@ dependencies = [ "nucleo", "once_cell", "parking_lot", + "percent-encoding", "quickcheck", "regex", "ropey", @@ -1232,7 +1233,6 @@ dependencies = [ "unicode-general-category", "unicode-segmentation", "unicode-width", - "url", ] [[package]] @@ -1312,10 +1312,10 @@ name = "helix-lsp-types" version = "0.95.1" dependencies = [ "bitflags", + "percent-encoding", "serde", "serde_json", "serde_repr", - "url", ] [[package]] @@ -1446,7 +1446,6 @@ dependencies = [ "tokio", "tokio-stream", "toml", - "url", ] [[package]] @@ -2420,7 +2419,6 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", - "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 763992480176..6f686984e3e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ tree-sitter = { version = "0.22" } nucleo = "0.5.0" slotmap = "1.0.7" thiserror = "1.0" +percent-encoding = "2.3" [workspace.package] version = "24.7.0" diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 4cd516268b0d..1c0fe33683c5 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -39,7 +39,7 @@ bitflags = "2.6" ahash = "0.8.11" hashbrown = { version = "0.14.5", features = ["raw"] } dunce = "1.0" -url = "2.5.0" +percent-encoding.workspace = true log = "0.4" serde = { version = "1.0", features = ["derive"] } diff --git a/helix-core/src/uri.rs b/helix-core/src/uri.rs index cbe0fadda67d..cb852e808091 100644 --- a/helix-core/src/uri.rs +++ b/helix-core/src/uri.rs @@ -1,6 +1,7 @@ use std::{ fmt, path::{Path, PathBuf}, + str::FromStr, sync::Arc, }; @@ -16,14 +17,6 @@ pub enum Uri { } impl Uri { - // This clippy allow mirrors url::Url::from_file_path - #[allow(clippy::result_unit_err)] - pub fn to_url(&self) -> Result { - match self { - Uri::File(path) => url::Url::from_file_path(path), - } - } - pub fn as_path(&self) -> Option<&Path> { match self { Self::File(path) => Some(path), @@ -45,81 +38,92 @@ impl fmt::Display for Uri { } } -#[derive(Debug)] -pub struct UrlConversionError { - source: url::Url, - kind: UrlConversionErrorKind, +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UriParseError { + source: String, + kind: UriParseErrorKind, } -#[derive(Debug)] -pub enum UrlConversionErrorKind { - UnsupportedScheme, +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum UriParseErrorKind { + UnsupportedScheme(String), UnableToConvert, } -impl fmt::Display for UrlConversionError { +impl fmt::Display for UriParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self.kind { - UrlConversionErrorKind::UnsupportedScheme => { - write!( - f, - "unsupported scheme '{}' in URL {}", - self.source.scheme(), - self.source - ) + match &self.kind { + UriParseErrorKind::UnsupportedScheme(scheme) => { + write!(f, "unsupported scheme '{scheme}' in URL {}", self.source) } - UrlConversionErrorKind::UnableToConvert => { + UriParseErrorKind::UnableToConvert => { write!(f, "unable to convert URL to file path: {}", self.source) } } } } -impl std::error::Error for UrlConversionError {} - -fn convert_url_to_uri(url: &url::Url) -> Result { - if url.scheme() == "file" { - url.to_file_path() - .map(|path| Uri::File(helix_stdx::path::normalize(path).into())) - .map_err(|_| UrlConversionErrorKind::UnableToConvert) - } else { - Err(UrlConversionErrorKind::UnsupportedScheme) - } -} +impl std::error::Error for UriParseError {} + +impl FromStr for Uri { + type Err = UriParseError; + + fn from_str(s: &str) -> Result { + use std::ffi::OsStr; + #[cfg(any(unix, target_os = "redox"))] + use std::os::unix::prelude::OsStrExt; + #[cfg(target_os = "wasi")] + use std::os::wasi::prelude::OsStrExt; + + let Some((scheme, rest)) = s.split_once("://") else { + return Err(Self::Err { + source: s.to_string(), + kind: UriParseErrorKind::UnableToConvert, + }); + }; + + if scheme != "file" { + return Err(Self::Err { + source: s.to_string(), + kind: UriParseErrorKind::UnsupportedScheme(scheme.to_string()), + }); + } -impl TryFrom for Uri { - type Error = UrlConversionError; + // Assert there is no query or fragment in the URI. + if s.find(['?', '#']).is_some() { + return Err(Self::Err { + source: s.to_string(), + kind: UriParseErrorKind::UnableToConvert, + }); + } - fn try_from(url: url::Url) -> Result { - convert_url_to_uri(&url).map_err(|kind| Self::Error { source: url, kind }) + let mut bytes = Vec::new(); + bytes.extend(percent_encoding::percent_decode(rest.as_bytes())); + Ok(PathBuf::from(OsStr::from_bytes(&bytes)).into()) } } -impl TryFrom<&url::Url> for Uri { - type Error = UrlConversionError; +impl TryFrom<&str> for Uri { + type Error = UriParseError; - fn try_from(url: &url::Url) -> Result { - convert_url_to_uri(url).map_err(|kind| Self::Error { - source: url.clone(), - kind, - }) + fn try_from(s: &str) -> Result { + s.parse() } } #[cfg(test)] mod test { use super::*; - use url::Url; #[test] fn unknown_scheme() { - let url = Url::parse("csharp:/metadata/foo/bar/Baz.cs").unwrap(); - assert!(matches!( - Uri::try_from(url), - Err(UrlConversionError { - kind: UrlConversionErrorKind::UnsupportedScheme, - .. + let uri = "csharp://metadata/foo/barBaz.cs"; + assert_eq!( + uri.parse::(), + Err(UriParseError { + source: uri.to_string(), + kind: UriParseErrorKind::UnsupportedScheme("csharp".to_string()), }) - )); + ); } } diff --git a/helix-lsp-types/Cargo.toml b/helix-lsp-types/Cargo.toml index 1ecb3d810cad..f6ddf41c82f1 100644 --- a/helix-lsp-types/Cargo.toml +++ b/helix-lsp-types/Cargo.toml @@ -25,7 +25,7 @@ bitflags = "2.6.0" serde = { version = "1.0.209", features = ["derive"] } serde_json = "1.0.127" serde_repr = "0.1" -url = {version = "2.0.0", features = ["serde"]} +percent-encoding.workspace = true [features] default = [] diff --git a/helix-lsp-types/README.md b/helix-lsp-types/README.md index 01803be17092..716a4b7cf088 100644 --- a/helix-lsp-types/README.md +++ b/helix-lsp-types/README.md @@ -1,3 +1,5 @@ # Helix's `lsp-types` -This is a fork of the [`lsp-types`](https://crates.io/crates/lsp-types) crate ([`gluon-lang/lsp-types`](https://github.com/gluon-lang/lsp-types)) taken at version v0.95.1 (commit [3e6daee](https://github.com/gluon-lang/lsp-types/commit/3e6daee771d14db4094a554b8d03e29c310dfcbe)). This fork focuses usability improvements that make the types easier to work with for the Helix codebase. For example the URL type - the `uri` crate at this version of `lsp-types` - will be replaced with a wrapper around a string. +This is a fork of the [`lsp-types`](https://crates.io/crates/lsp-types) crate ([`gluon-lang/lsp-types`](https://github.com/gluon-lang/lsp-types)) taken at version v0.95.1 (commit [3e6daee](https://github.com/gluon-lang/lsp-types/commit/3e6daee771d14db4094a554b8d03e29c310dfcbe)). This fork focuses usability improvements that make the types easier to work with for the Helix codebase. + +The URL type has been replaced with a newtype wrapper of a `String`. The `lsp-types` crate at the forked version used [`url::Url`](https://docs.rs/url/2.5.0/url/struct.Url.html) which provides conveniences for using URLs according to [the WHATWG URL spec](https://url.spec.whatwg.org). Helix supports a subset of valid URLs, namely the `file://` scheme, so a wrapper around a normal `String` is sufficient. Plus the LSP spec requires URLs to be in [RFC3986](https://tools.ietf.org/html/rfc3986) format instead. diff --git a/helix-lsp-types/src/call_hierarchy.rs b/helix-lsp-types/src/call_hierarchy.rs index dea78803f588..2669e08eee81 100644 --- a/helix-lsp-types/src/call_hierarchy.rs +++ b/helix-lsp-types/src/call_hierarchy.rs @@ -1,10 +1,9 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; -use url::Url; use crate::{ DynamicRegistrationClientCapabilities, PartialResultParams, Range, SymbolKind, SymbolTag, - TextDocumentPositionParams, WorkDoneProgressOptions, WorkDoneProgressParams, + TextDocumentPositionParams, Url, WorkDoneProgressOptions, WorkDoneProgressParams, }; pub type CallHierarchyClientCapabilities = DynamicRegistrationClientCapabilities; diff --git a/helix-lsp-types/src/document_diagnostic.rs b/helix-lsp-types/src/document_diagnostic.rs index a2b5c41fab9c..5a1c6b35a3a3 100644 --- a/helix-lsp-types/src/document_diagnostic.rs +++ b/helix-lsp-types/src/document_diagnostic.rs @@ -1,11 +1,10 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; -use url::Url; use crate::{ Diagnostic, PartialResultParams, StaticRegistrationOptions, TextDocumentIdentifier, - TextDocumentRegistrationOptions, WorkDoneProgressOptions, WorkDoneProgressParams, + TextDocumentRegistrationOptions, Url, WorkDoneProgressOptions, WorkDoneProgressParams, }; /// Client capabilities specific to diagnostic pull requests. diff --git a/helix-lsp-types/src/document_link.rs b/helix-lsp-types/src/document_link.rs index 1400dd96b6e0..dde0e2011d66 100644 --- a/helix-lsp-types/src/document_link.rs +++ b/helix-lsp-types/src/document_link.rs @@ -1,10 +1,9 @@ use crate::{ - PartialResultParams, Range, TextDocumentIdentifier, WorkDoneProgressOptions, + PartialResultParams, Range, TextDocumentIdentifier, Url, WorkDoneProgressOptions, WorkDoneProgressParams, }; use serde::{Deserialize, Serialize}; use serde_json::Value; -use url::Url; #[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] diff --git a/helix-lsp-types/src/lib.rs b/helix-lsp-types/src/lib.rs index 3ea1c0cd0ba7..01bd1eb8f8a6 100644 --- a/helix-lsp-types/src/lib.rs +++ b/helix-lsp-types/src/lib.rs @@ -3,27 +3,90 @@ Language Server Protocol types for Rust. Based on: - -This library uses the URL crate for parsing URIs. Note that there is -some confusion on the meaning of URLs vs URIs: -. According to that -information, on the classical sense of "URLs", "URLs" are a subset of -URIs, But on the modern/new meaning of URLs, they are the same as -URIs. The important take-away aspect is that the URL crate should be -able to parse any URI, such as `urn:isbn:0451450523`. - - */ #![allow(non_upper_case_globals)] #![forbid(unsafe_code)] -#[macro_use] -extern crate bitflags; -use std::{collections::HashMap, fmt::Debug}; +use bitflags::bitflags; + +use std::{collections::HashMap, fmt::Debug, path::Path}; use serde::{de, de::Error as Error_, Deserialize, Serialize}; use serde_json::Value; -pub use url::Url; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)] +pub struct Url(String); + +impl Url { + pub fn from_file_path>(path: P) -> Self { + use percent_encoding::{percent_encode, AsciiSet, CONTROLS}; + #[cfg(any(unix, target_os = "redox"))] + use std::os::unix::prelude::OsStrExt; + #[cfg(target_os = "wasi")] + use std::os::wasi::prelude::OsStrExt; + + // , also see + // + const RESERVED: &AsciiSet = &CONTROLS + // GEN_DELIMS + .add(b':') + .add(b'/') + .add(b'?') + .add(b'#') + .add(b'[') + .add(b']') + .add(b'@') + // SUB_DELIMS + .add(b'!') + .add(b'$') + .add(b'&') + .add(b'\'') + .add(b'(') + .add(b')') + .add(b'*') + .add(b'+') + .add(b',') + .add(b';') + .add(b'='); + + let mut serialization = String::from("file://"); + // skip the root component + for component in path.as_ref().components().skip(1) { + serialization.push('/'); + serialization.extend(percent_encode(component.as_os_str().as_bytes(), RESERVED)); + } + if &serialization == "file://" { + // An URL's path must not be empty. + serialization.push('/'); + } + Self(serialization) + } + + pub fn from_directory_path>(path: P) -> Self { + let Self(mut serialization) = Self::from_file_path(path); + if !serialization.ends_with('/') { + serialization.push('/'); + } + Self(serialization) + } + + /// Returns the serialized representation of the URL as a `&str` + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Consumes the URL, converting into a `String`. + /// Note that the string is the serialized representation of the URL. + pub fn into_string(self) -> String { + self.0 + } +} + +impl From<&str> for Url { + fn from(value: &str) -> Self { + Self(value.to_string()) + } +} // Large enough to contain any enumeration name defined in this crate type PascalCaseBuf = [u8; 32]; @@ -2843,14 +2906,14 @@ mod tests { test_serialization( &WorkspaceEdit { changes: Some( - vec![(Url::parse("file://test").unwrap(), vec![])] + vec![(Url::from("file://test"), vec![])] .into_iter() .collect(), ), document_changes: None, ..Default::default() }, - r#"{"changes":{"file://test/":[]}}"#, + r#"{"changes":{"file://test":[]}}"#, ); } diff --git a/helix-lsp-types/src/window.rs b/helix-lsp-types/src/window.rs index ac45e6083a08..0cfffa0cefd0 100644 --- a/helix-lsp-types/src/window.rs +++ b/helix-lsp-types/src/window.rs @@ -4,9 +4,7 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; -use url::Url; - -use crate::Range; +use crate::{Range, Url}; #[derive(Eq, PartialEq, Clone, Copy, Deserialize, Serialize)] #[serde(transparent)] diff --git a/helix-lsp-types/src/workspace_diagnostic.rs b/helix-lsp-types/src/workspace_diagnostic.rs index e8a7646b0286..485dcc2bb41c 100644 --- a/helix-lsp-types/src/workspace_diagnostic.rs +++ b/helix-lsp-types/src/workspace_diagnostic.rs @@ -1,8 +1,7 @@ use serde::{Deserialize, Serialize}; -use url::Url; use crate::{ - FullDocumentDiagnosticReport, PartialResultParams, UnchangedDocumentDiagnosticReport, + FullDocumentDiagnosticReport, PartialResultParams, UnchangedDocumentDiagnosticReport, Url, WorkDoneProgressParams, }; diff --git a/helix-lsp-types/src/workspace_folders.rs b/helix-lsp-types/src/workspace_folders.rs index aeca89ffe81c..2169cade0798 100644 --- a/helix-lsp-types/src/workspace_folders.rs +++ b/helix-lsp-types/src/workspace_folders.rs @@ -1,7 +1,6 @@ use serde::{Deserialize, Serialize}; -use url::Url; -use crate::OneOf; +use crate::{OneOf, Url}; #[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index cc1c4ce8fe67..8c0337e981cf 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -32,14 +32,17 @@ use tokio::{ }, }; -fn workspace_for_uri(uri: lsp::Url) -> WorkspaceFolder { +fn workspace_for_path(path: &Path) -> WorkspaceFolder { + let name = path + .iter() + .last() + .expect("workspace paths should be non-empty") + .to_string_lossy() + .to_string(); + lsp::WorkspaceFolder { - name: uri - .path_segments() - .and_then(|segments| segments.last()) - .map(|basename| basename.to_string()) - .unwrap_or_default(), - uri, + name, + uri: lsp::Url::from_directory_path(path), } } @@ -55,7 +58,7 @@ pub struct Client { config: Option, root_path: std::path::PathBuf, root_uri: Option, - workspace_folders: Mutex>, + workspace_folders: Mutex>, initialize_notify: Arc, /// workspace folders added while the server is still initializing req_timeout: u64, @@ -80,16 +83,13 @@ impl Client { &workspace, workspace_is_cwd, ); - let root_uri = root - .as_ref() - .and_then(|root| lsp::Url::from_file_path(root).ok()); - if self.root_path == root.unwrap_or(workspace) - || root_uri.as_ref().map_or(false, |root_uri| { + if &self.root_path == root.as_ref().unwrap_or(&workspace) + || root.as_ref().map_or(false, |root| { self.workspace_folders .lock() .iter() - .any(|workspace| &workspace.uri == root_uri) + .any(|workspace| workspace == root) }) { // workspace URI is already registered so we can use this client @@ -113,15 +113,16 @@ impl Client { // wait and see if anyone ever runs into it. tokio::spawn(async move { client.initialize_notify.notified().await; - if let Some(workspace_folders_caps) = client + if let Some((workspace_folders_caps, root)) = client .capabilities() .workspace .as_ref() .and_then(|cap| cap.workspace_folders.as_ref()) .filter(|cap| cap.supported.unwrap_or(false)) + .zip(root) { client.add_workspace_folder( - root_uri, + root, workspace_folders_caps.change_notifications.as_ref(), ); } @@ -129,16 +130,14 @@ impl Client { return true; }; - if let Some(workspace_folders_caps) = capabilities + if let Some((workspace_folders_caps, root)) = capabilities .workspace .as_ref() .and_then(|cap| cap.workspace_folders.as_ref()) .filter(|cap| cap.supported.unwrap_or(false)) + .zip(root) { - self.add_workspace_folder( - root_uri, - workspace_folders_caps.change_notifications.as_ref(), - ); + self.add_workspace_folder(root, workspace_folders_caps.change_notifications.as_ref()); true } else { // the server doesn't support multi workspaces, we need a new client @@ -148,29 +147,19 @@ impl Client { fn add_workspace_folder( &self, - root_uri: Option, + root: PathBuf, change_notifications: Option<&OneOf>, ) { - // root_uri is None just means that there isn't really any LSP workspace - // associated with this file. For servers that support multiple workspaces - // there is just one server so we can always just use that shared instance. - // No need to add a new workspace root here as there is no logical root for this file - // let the server deal with this - let Some(root_uri) = root_uri else { - return; - }; - + let workspace = workspace_for_path(&root); // server supports workspace folders, let's add the new root to the list - self.workspace_folders - .lock() - .push(workspace_for_uri(root_uri.clone())); + self.workspace_folders.lock().push(root); if Some(&OneOf::Left(false)) == change_notifications { // server specifically opted out of DidWorkspaceChange notifications // let's assume the server will request the workspace folders itself // and that we can therefore reuse the client (but are done now) return; } - tokio::spawn(self.did_change_workspace(vec![workspace_for_uri(root_uri)], Vec::new())); + tokio::spawn(self.did_change_workspace(vec![workspace], Vec::new())); } #[allow(clippy::type_complexity, clippy::too_many_arguments)] @@ -179,8 +168,8 @@ impl Client { args: &[String], config: Option, server_environment: HashMap, - root_path: PathBuf, - root_uri: Option, + root: Option, + workspace: PathBuf, id: LanguageServerId, name: String, req_timeout: u64, @@ -212,10 +201,11 @@ impl Client { let (server_rx, server_tx, initialize_notify) = Transport::start(reader, writer, stderr, id, name.clone()); - let workspace_folders = root_uri - .clone() - .map(|root| vec![workspace_for_uri(root)]) - .unwrap_or_default(); + let workspace_folders = root.clone().into_iter().collect(); + let root_uri = root.clone().map(lsp::Url::from_file_path); + // `root_uri` and `workspace_folder` can be empty in case there is no workspace + // `root_url` can not, use `workspace` as a fallback + let root_path = root.unwrap_or(workspace); let client = Self { id, @@ -376,10 +366,12 @@ impl Client { self.config.as_ref() } - pub async fn workspace_folders( - &self, - ) -> parking_lot::MutexGuard<'_, Vec> { - self.workspace_folders.lock() + pub async fn workspace_folders(&self) -> Vec { + self.workspace_folders + .lock() + .iter() + .map(|path| workspace_for_path(path)) + .collect() } /// Execute a RPC request on the language server. @@ -526,7 +518,7 @@ impl Client { #[allow(deprecated)] let params = lsp::InitializeParams { process_id: Some(std::process::id()), - workspace_folders: Some(self.workspace_folders.lock().clone()), + workspace_folders: Some(self.workspace_folders().await), // root_path is obsolete, but some clients like pyright still use it so we specify both. // clients will prefer _uri if possible root_path: self.root_path.to_str().map(|path| path.to_owned()), @@ -748,11 +740,11 @@ impl Client { } else { Url::from_file_path(path) }; - Some(url.ok()?.to_string()) + url.into_string() }; let files = vec![lsp::FileRename { - old_uri: url_from_path(old_path)?, - new_uri: url_from_path(new_path)?, + old_uri: url_from_path(old_path), + new_uri: url_from_path(new_path), }]; let request = self.call_with_timeout::( &lsp::RenameFilesParams { files }, @@ -782,12 +774,12 @@ impl Client { } else { Url::from_file_path(path) }; - Some(url.ok()?.to_string()) + url.into_string() }; let files = vec![lsp::FileRename { - old_uri: url_from_path(old_path)?, - new_uri: url_from_path(new_path)?, + old_uri: url_from_path(old_path), + new_uri: url_from_path(new_path), }]; Some(self.notify::(lsp::RenameFilesParams { files })) } diff --git a/helix-lsp/src/file_event.rs b/helix-lsp/src/file_event.rs index c7297d67fc11..d7ca446d281c 100644 --- a/helix-lsp/src/file_event.rs +++ b/helix-lsp/src/file_event.rs @@ -106,16 +106,13 @@ impl Handler { log::warn!("LSP client was dropped: {id}"); return false; }; - let Ok(uri) = lsp::Url::from_file_path(&path) else { - return true; - }; log::debug!( "Sending didChangeWatchedFiles notification to client '{}'", client.name() ); if let Err(err) = crate::block_on(client .did_change_watched_files(vec![lsp::FileEvent { - uri, + uri: lsp::Url::from_file_path(&path), // We currently always send the CHANGED state // since we don't actually have more context at // the moment. diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 47f38bcf2ef1..5f4c71965e8f 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -955,12 +955,8 @@ fn start_client( workspace_is_cwd, ); - // `root_uri` and `workspace_folder` can be empty in case there is no workspace - // `root_url` can not, use `workspace` as a fallback - let root_path = root.clone().unwrap_or_else(|| workspace.clone()); - let root_uri = root.and_then(|root| lsp::Url::from_file_path(root).ok()); - if let Some(globset) = &ls_config.required_root_patterns { + let root_path = root.as_ref().unwrap_or(&workspace); if !root_path .read_dir()? .flatten() @@ -976,8 +972,8 @@ fn start_client( &ls_config.args, ls_config.config.clone(), ls_config.environment.clone(), - root_path, - root_uri, + root, + workspace, id, name, ls_config.timeout, diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index a567815fcaa6..471da2af8d48 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -738,7 +738,7 @@ impl Application { } } Notification::PublishDiagnostics(mut params) => { - let uri = match helix_core::Uri::try_from(params.uri) { + let uri = match helix_core::Uri::try_from(params.uri.as_str()) { Ok(uri) => uri, Err(err) => { log::error!("{err}"); @@ -1137,7 +1137,8 @@ impl Application { .. } = params { - self.jobs.callback(crate::open_external_url_callback(uri)); + self.jobs + .callback(crate::open_external_url_callback(uri.as_str())); return lsp::ShowDocumentResult { success: true }; }; @@ -1148,7 +1149,7 @@ impl Application { .. } = params; - let uri = match helix_core::Uri::try_from(uri) { + let uri = match helix_core::Uri::try_from(uri.as_str()) { Ok(uri) => uri, Err(err) => { log::error!("{err}"); diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index b1c29378dec6..4a0c89028ce6 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1350,7 +1350,9 @@ fn open_url(cx: &mut Context, url: Url, action: Action) { .unwrap_or_default(); if url.scheme() != "file" { - return cx.jobs.callback(crate::open_external_url_callback(url)); + return cx + .jobs + .callback(crate::open_external_url_callback(url.as_str())); } let content_type = std::fs::File::open(url.path()).and_then(|file| { @@ -1363,9 +1365,9 @@ fn open_url(cx: &mut Context, url: Url, action: Action) { // we attempt to open binary files - files that can't be open in helix - using external // program as well, e.g. pdf files or images match content_type { - Ok(content_inspector::ContentType::BINARY) => { - cx.jobs.callback(crate::open_external_url_callback(url)) - } + Ok(content_inspector::ContentType::BINARY) => cx + .jobs + .callback(crate::open_external_url_callback(url.as_str())), Ok(_) | Err(_) => { let path = &rel_path.join(url.path()); if path.is_dir() { diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index fcc0333e8cd8..4fd957c872ad 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -69,7 +69,7 @@ struct Location { } fn lsp_location_to_location(location: lsp::Location) -> Option { - let uri = match location.uri.try_into() { + let uri = match location.uri.as_str().try_into() { Ok(uri) => uri, Err(err) => { log::warn!("discarding invalid or unsupported URI: {err}"); @@ -456,7 +456,7 @@ pub fn workspace_symbol_picker(cx: &mut Context) { .unwrap_or_default() .into_iter() .filter_map(|symbol| { - let uri = match Uri::try_from(&symbol.location.uri) { + let uri = match Uri::try_from(symbol.location.uri.as_str()) { Ok(uri) => uri, Err(err) => { log::warn!("discarding symbol with invalid URI: {err}"); @@ -510,7 +510,7 @@ pub fn workspace_symbol_picker(cx: &mut Context) { .to_string() .into() } else { - item.symbol.location.uri.to_string().into() + item.symbol.location.uri.as_str().into() } }), ]; diff --git a/helix-term/src/lib.rs b/helix-term/src/lib.rs index cf4fbd9fa7ae..71fb3b4f5c0c 100644 --- a/helix-term/src/lib.rs +++ b/helix-term/src/lib.rs @@ -18,7 +18,6 @@ use futures_util::Future; mod handlers; use ignore::DirEntry; -use url::Url; #[cfg(windows)] fn true_color() -> bool { @@ -70,10 +69,10 @@ fn filter_picker_entry(entry: &DirEntry, root: &Path, dedup_symlinks: bool) -> b } /// Opens URL in external program. -fn open_external_url_callback( - url: Url, +fn open_external_url_callback>( + url: U, ) -> impl Future> + Send + 'static { - let commands = open::commands(url.as_str()); + let commands = open::commands(url); async { for cmd in commands { let mut command = tokio::process::Command::new(cmd.get_program()); diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index 725a77547cf9..f1fe5e3781e5 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -30,9 +30,7 @@ crossterm = { version = "0.28", optional = true } tempfile = "3.13" -# Conversion traits once_cell = "1.20" -url = "2.5.2" arc-swap = { version = "1.7.1" } diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 91ec27874853..74a1a935168d 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -640,7 +640,6 @@ where } use helix_lsp::{lsp, Client, LanguageServerId, LanguageServerName}; -use url::Url; impl Document { pub fn from( @@ -1811,8 +1810,8 @@ impl Document { } /// File path as a URL. - pub fn url(&self) -> Option { - Url::from_file_path(self.path()?).ok() + pub fn url(&self) -> Option { + self.path().map(lsp::Url::from_file_path) } pub fn uri(&self) -> Option { @@ -1898,7 +1897,7 @@ impl Document { pub fn lsp_diagnostic_to_diagnostic( text: &Rope, language_config: Option<&LanguageConfiguration>, - diagnostic: &helix_lsp::lsp::Diagnostic, + diagnostic: &lsp::Diagnostic, language_server_id: LanguageServerId, offset_encoding: helix_lsp::OffsetEncoding, ) -> Option { diff --git a/helix-view/src/handlers/lsp.rs b/helix-view/src/handlers/lsp.rs index 1fd2289db5d8..77f8769651f8 100644 --- a/helix-view/src/handlers/lsp.rs +++ b/helix-view/src/handlers/lsp.rs @@ -57,7 +57,7 @@ pub struct ApplyEditError { pub enum ApplyEditErrorKind { DocumentChanged, FileNotFound, - InvalidUrl(helix_core::uri::UrlConversionError), + InvalidUrl(helix_core::uri::UriParseError), IoError(std::io::Error), // TODO: check edits before applying and propagate failure // InvalidEdit, @@ -69,8 +69,8 @@ impl From for ApplyEditErrorKind { } } -impl From for ApplyEditErrorKind { - fn from(err: helix_core::uri::UrlConversionError) -> Self { +impl From for ApplyEditErrorKind { + fn from(err: helix_core::uri::UriParseError) -> Self { ApplyEditErrorKind::InvalidUrl(err) } } @@ -94,7 +94,7 @@ impl Editor { text_edits: Vec, offset_encoding: OffsetEncoding, ) -> Result<(), ApplyEditErrorKind> { - let uri = match Uri::try_from(url) { + let uri = match Uri::try_from(url.as_str()) { Ok(uri) => uri, Err(err) => { log::error!("{err}"); @@ -242,7 +242,7 @@ impl Editor { // may no longer be valid. match op { ResourceOp::Create(op) => { - let uri = Uri::try_from(&op.uri)?; + let uri = Uri::try_from(op.uri.as_str())?; 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) @@ -262,7 +262,7 @@ impl Editor { } } ResourceOp::Delete(op) => { - let uri = Uri::try_from(&op.uri)?; + let uri = Uri::try_from(op.uri.as_str())?; let path = uri.as_path().expect("URIs are valid paths"); if path.is_dir() { let recursive = op @@ -284,9 +284,9 @@ impl Editor { } } ResourceOp::Rename(op) => { - let from_uri = Uri::try_from(&op.old_uri)?; + let from_uri = Uri::try_from(op.old_uri.as_str())?; let from = from_uri.as_path().expect("URIs are valid paths"); - let to_uri = Uri::try_from(&op.new_uri)?; + let to_uri = Uri::try_from(op.new_uri.as_str())?; let to = to_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)