Unnamed repository; edit this file 'description' to name the repository.
Handle conversion to/from new LSP URL type
Michael Davis 2024-12-21
parent b84c9a8 · commit a36806e
-rw-r--r--Cargo.lock6
-rw-r--r--Cargo.toml1
-rw-r--r--helix-core/Cargo.toml2
-rw-r--r--helix-core/src/uri.rs116
-rw-r--r--helix-lsp/src/client.rs7
-rw-r--r--helix-term/src/application.rs7
-rw-r--r--helix-term/src/commands.rs10
-rw-r--r--helix-term/src/commands/lsp.rs6
-rw-r--r--helix-term/src/lib.rs7
-rw-r--r--helix-view/Cargo.toml2
-rw-r--r--helix-view/src/document.rs7
-rw-r--r--helix-view/src/handlers/lsp.rs16
12 files changed, 97 insertions, 90 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 559e9eb8..679b87f3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1237,6 +1237,7 @@ dependencies = [
"nucleo",
"once_cell",
"parking_lot",
+ "percent-encoding",
"quickcheck",
"regex",
"regex-cursor",
@@ -1252,7 +1253,6 @@ dependencies = [
"unicode-general-category",
"unicode-segmentation",
"unicode-width",
- "url",
]
[[package]]
@@ -1332,10 +1332,10 @@ name = "helix-lsp-types"
version = "0.95.1"
dependencies = [
"bitflags",
+ "percent-encoding",
"serde",
"serde_json",
"serde_repr",
- "url",
]
[[package]]
@@ -1468,7 +1468,6 @@ dependencies = [
"tokio",
"tokio-stream",
"toml",
- "url",
]
[[package]]
@@ -2622,7 +2621,6 @@ dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
- "serde",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 753be4b4..35bba77a 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 = "2.0"
+percent-encoding = "2.3"
[workspace.package]
version = "24.7.0"
diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml
index d245ec13..485a4155 100644
--- a/helix-core/Cargo.toml
+++ b/helix-core/Cargo.toml
@@ -40,7 +40,7 @@ bitflags = "2.6"
ahash = "0.8.11"
hashbrown = { version = "0.14.5", features = ["raw"] }
dunce = "1.0"
-url = "2.5.4"
+percent-encoding.workspace = true
log = "0.4"
anyhow = "1.0"
diff --git a/helix-core/src/uri.rs b/helix-core/src/uri.rs
index cbe0fadd..0b6f4e0b 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<url::Url, ()> {
- 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,96 @@ 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,
- UnableToConvert,
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum UriParseErrorKind {
+ UnsupportedScheme(String),
+ MalformedUri,
}
-impl fmt::Display for UrlConversionError {
+impl fmt::Display for UriParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self.kind {
- UrlConversionErrorKind::UnsupportedScheme => {
+ match &self.kind {
+ UriParseErrorKind::UnsupportedScheme(scheme) => {
+ write!(f, "unsupported scheme '{scheme}' in URI {}", self.source)
+ }
+ UriParseErrorKind::MalformedUri => {
write!(
f,
- "unsupported scheme '{}' in URL {}",
- self.source.scheme(),
+ "unable to convert malformed URI to file path: {}",
self.source
)
}
- UrlConversionErrorKind::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<Uri, UrlConversionErrorKind> {
- 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<Self, Self::Err> {
+ 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::MalformedUri,
+ });
+ };
+
+ if scheme != "file" {
+ return Err(Self::Err {
+ source: s.to_string(),
+ kind: UriParseErrorKind::UnsupportedScheme(scheme.to_string()),
+ });
+ }
-impl TryFrom<url::Url> 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::MalformedUri,
+ });
+ }
- fn try_from(url: url::Url) -> Result<Self, Self::Error> {
- 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<Self, Self::Error> {
- convert_url_to_uri(url).map_err(|kind| Self::Error {
- source: url.clone(),
- kind,
- })
+ fn try_from(s: &str) -> Result<Self, Self::Error> {
+ 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::<Uri>(),
+ Err(UriParseError {
+ source: uri.to_string(),
+ kind: UriParseErrorKind::UnsupportedScheme("csharp".to_string()),
})
- ));
+ );
}
}
diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs
index 48b7a879..e8efa613 100644
--- a/helix-lsp/src/client.rs
+++ b/helix-lsp/src/client.rs
@@ -42,7 +42,8 @@ fn workspace_for_path(path: &Path) -> WorkspaceFolder {
lsp::WorkspaceFolder {
name,
- uri: lsp::Url::from_file_path(path).expect("absolute paths can be converted to `Url`s"),
+ uri: lsp::Url::from_directory_path(path)
+ .expect("absolute paths can be converted to `Url`s"),
}
}
@@ -742,7 +743,7 @@ impl Client {
} else {
Url::from_file_path(path)
};
- Some(url.ok()?.to_string())
+ Some(url.ok()?.into_string())
};
let files = vec![lsp::FileRename {
old_uri: url_from_path(old_path)?,
@@ -776,7 +777,7 @@ impl Client {
} else {
Url::from_file_path(path)
};
- Some(url.ok()?.to_string())
+ Some(url.ok()?.into_string())
};
let files = vec![lsp::FileRename {
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs
index 36cb295c..28fcd8fc 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -744,7 +744,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}");
@@ -1143,7 +1143,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 };
};
@@ -1154,7 +1155,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 0bfb12ad..30eb3798 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -1347,7 +1347,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| {
@@ -1360,9 +1362,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 fcc0333e..4fd957c8 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<Location> {
- 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 cf4fbd9f..71fb3b4f 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<U: AsRef<std::ffi::OsStr>>(
+ url: U,
) -> impl Future<Output = Result<job::Callback, anyhow::Error>> + 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 6f71fa05..0b58a86b 100644
--- a/helix-view/Cargo.toml
+++ b/helix-view/Cargo.toml
@@ -30,9 +30,7 @@ crossterm = { version = "0.28", optional = true }
tempfile = "3.14"
-# Conversion traits
once_cell = "1.20"
-url = "2.5.4"
arc-swap = { version = "1.7.1" }
diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs
index dcdc8dc2..5a17c5b7 100644
--- a/helix-view/src/document.rs
+++ b/helix-view/src/document.rs
@@ -642,7 +642,6 @@ where
}
use helix_lsp::{lsp, Client, LanguageServerId, LanguageServerName};
-use url::Url;
impl Document {
pub fn from(
@@ -1822,8 +1821,8 @@ impl Document {
}
/// File path as a URL.
- pub fn url(&self) -> Option<Url> {
- Url::from_file_path(self.path()?).ok()
+ pub fn url(&self) -> Option<lsp::Url> {
+ lsp::Url::from_file_path(self.path()?).ok()
}
pub fn uri(&self) -> Option<helix_core::Uri> {
@@ -1909,7 +1908,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<Diagnostic> {
diff --git a/helix-view/src/handlers/lsp.rs b/helix-view/src/handlers/lsp.rs
index 1fd2289d..77f87696 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<std::io::Error> for ApplyEditErrorKind {
}
}
-impl From<helix_core::uri::UrlConversionError> for ApplyEditErrorKind {
- fn from(err: helix_core::uri::UrlConversionError) -> Self {
+impl From<helix_core::uri::UriParseError> for ApplyEditErrorKind {
+ fn from(err: helix_core::uri::UriParseError) -> Self {
ApplyEditErrorKind::InvalidUrl(err)
}
}
@@ -94,7 +94,7 @@ impl Editor {
text_edits: Vec<lsp::TextEdit>,
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)