Handle conversion to/from new LSP URL type

pull/11889/head
Michael Davis 3 months ago
parent b56f09a6fc
commit 636902ea3e
No known key found for this signature in database

6
Cargo.lock generated

@ -1218,6 +1218,7 @@ dependencies = [
"nucleo", "nucleo",
"once_cell", "once_cell",
"parking_lot", "parking_lot",
"percent-encoding",
"quickcheck", "quickcheck",
"regex", "regex",
"ropey", "ropey",
@ -1232,7 +1233,6 @@ dependencies = [
"unicode-general-category", "unicode-general-category",
"unicode-segmentation", "unicode-segmentation",
"unicode-width", "unicode-width",
"url",
] ]
[[package]] [[package]]
@ -1312,10 +1312,10 @@ name = "helix-lsp-types"
version = "0.95.1" version = "0.95.1"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"percent-encoding",
"serde", "serde",
"serde_json", "serde_json",
"serde_repr", "serde_repr",
"url",
] ]
[[package]] [[package]]
@ -1446,7 +1446,6 @@ dependencies = [
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"toml", "toml",
"url",
] ]
[[package]] [[package]]
@ -2420,7 +2419,6 @@ dependencies = [
"form_urlencoded", "form_urlencoded",
"idna", "idna",
"percent-encoding", "percent-encoding",
"serde",
] ]
[[package]] [[package]]

@ -42,6 +42,7 @@ tree-sitter = { version = "0.22" }
nucleo = "0.5.0" nucleo = "0.5.0"
slotmap = "1.0.7" slotmap = "1.0.7"
thiserror = "1.0" thiserror = "1.0"
percent-encoding = "2.3"
[workspace.package] [workspace.package]
version = "24.7.0" version = "24.7.0"

@ -39,7 +39,7 @@ bitflags = "2.6"
ahash = "0.8.11" ahash = "0.8.11"
hashbrown = { version = "0.14.5", features = ["raw"] } hashbrown = { version = "0.14.5", features = ["raw"] }
dunce = "1.0" dunce = "1.0"
url = "2.5.0" percent-encoding.workspace = true
log = "0.4" log = "0.4"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }

@ -1,6 +1,7 @@
use std::{ use std::{
fmt, fmt,
path::{Path, PathBuf}, path::{Path, PathBuf},
str::FromStr,
sync::Arc, sync::Arc,
}; };
@ -16,14 +17,6 @@ pub enum Uri {
} }
impl 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> { pub fn as_path(&self) -> Option<&Path> {
match self { match self {
Self::File(path) => Some(path), Self::File(path) => Some(path),
@ -45,81 +38,92 @@ impl fmt::Display for Uri {
} }
} }
#[derive(Debug)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct UrlConversionError { pub struct UriParseError {
source: url::Url, source: String,
kind: UrlConversionErrorKind, kind: UriParseErrorKind,
} }
#[derive(Debug)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum UrlConversionErrorKind { pub enum UriParseErrorKind {
UnsupportedScheme, UnsupportedScheme(String),
UnableToConvert, UnableToConvert,
} }
impl fmt::Display for UrlConversionError { impl fmt::Display for UriParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.kind { match &self.kind {
UrlConversionErrorKind::UnsupportedScheme => { UriParseErrorKind::UnsupportedScheme(scheme) => {
write!( write!(f, "unsupported scheme '{scheme}' in URL {}", self.source)
f,
"unsupported scheme '{}' in URL {}",
self.source.scheme(),
self.source
)
} }
UrlConversionErrorKind::UnableToConvert => { UriParseErrorKind::UnableToConvert => {
write!(f, "unable to convert URL to file path: {}", self.source) write!(f, "unable to convert URL to file path: {}", self.source)
} }
} }
} }
} }
impl std::error::Error for UrlConversionError {} impl std::error::Error for UriParseError {}
fn convert_url_to_uri(url: &url::Url) -> Result<Uri, UrlConversionErrorKind> { impl FromStr for Uri {
if url.scheme() == "file" { type Err = UriParseError;
url.to_file_path()
.map(|path| Uri::File(helix_stdx::path::normalize(path).into())) fn from_str(s: &str) -> Result<Self, Self::Err> {
.map_err(|_| UrlConversionErrorKind::UnableToConvert) use std::ffi::OsStr;
} else { #[cfg(any(unix, target_os = "redox"))]
Err(UrlConversionErrorKind::UnsupportedScheme) 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<url::Url> for Uri { // Assert there is no query or fragment in the URI.
type Error = UrlConversionError; if s.find(['?', '#']).is_some() {
return Err(Self::Err {
source: s.to_string(),
kind: UriParseErrorKind::UnableToConvert,
});
}
fn try_from(url: url::Url) -> Result<Self, Self::Error> { let mut bytes = Vec::new();
convert_url_to_uri(&url).map_err(|kind| Self::Error { source: url, kind }) bytes.extend(percent_encoding::percent_decode(rest.as_bytes()));
Ok(PathBuf::from(OsStr::from_bytes(&bytes)).into())
} }
} }
impl TryFrom<&url::Url> for Uri { impl TryFrom<&str> for Uri {
type Error = UrlConversionError; type Error = UriParseError;
fn try_from(url: &url::Url) -> Result<Self, Self::Error> { fn try_from(s: &str) -> Result<Self, Self::Error> {
convert_url_to_uri(url).map_err(|kind| Self::Error { s.parse()
source: url.clone(),
kind,
})
} }
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use url::Url;
#[test] #[test]
fn unknown_scheme() { fn unknown_scheme() {
let url = Url::parse("csharp:/metadata/foo/bar/Baz.cs").unwrap(); let uri = "csharp://metadata/foo/barBaz.cs";
assert!(matches!( assert_eq!(
Uri::try_from(url), uri.parse::<Uri>(),
Err(UrlConversionError { Err(UriParseError {
kind: UrlConversionErrorKind::UnsupportedScheme, source: uri.to_string(),
.. kind: UriParseErrorKind::UnsupportedScheme("csharp".to_string()),
}) })
)); );
} }
} }

@ -42,7 +42,8 @@ fn workspace_for_path(path: &Path) -> WorkspaceFolder {
lsp::WorkspaceFolder { lsp::WorkspaceFolder {
name, 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 { } else {
Url::from_file_path(path) Url::from_file_path(path)
}; };
Some(url.ok()?.to_string()) Some(url.ok()?.into_string())
}; };
let files = vec![lsp::FileRename { let files = vec![lsp::FileRename {
old_uri: url_from_path(old_path)?, old_uri: url_from_path(old_path)?,
@ -776,7 +777,7 @@ impl Client {
} else { } else {
Url::from_file_path(path) Url::from_file_path(path)
}; };
Some(url.ok()?.to_string()) Some(url.ok()?.into_string())
}; };
let files = vec![lsp::FileRename { let files = vec![lsp::FileRename {

@ -738,7 +738,7 @@ impl Application {
} }
} }
Notification::PublishDiagnostics(mut params) => { 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, Ok(uri) => uri,
Err(err) => { Err(err) => {
log::error!("{err}"); log::error!("{err}");
@ -1137,7 +1137,8 @@ impl Application {
.. ..
} = params } = 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 }; return lsp::ShowDocumentResult { success: true };
}; };
@ -1148,7 +1149,7 @@ impl Application {
.. ..
} = params; } = params;
let uri = match helix_core::Uri::try_from(uri) { let uri = match helix_core::Uri::try_from(uri.as_str()) {
Ok(uri) => uri, Ok(uri) => uri,
Err(err) => { Err(err) => {
log::error!("{err}"); log::error!("{err}");

@ -1350,7 +1350,9 @@ fn open_url(cx: &mut Context, url: Url, action: Action) {
.unwrap_or_default(); .unwrap_or_default();
if url.scheme() != "file" { 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| { 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 // 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 // program as well, e.g. pdf files or images
match content_type { match content_type {
Ok(content_inspector::ContentType::BINARY) => { Ok(content_inspector::ContentType::BINARY) => cx
cx.jobs.callback(crate::open_external_url_callback(url)) .jobs
} .callback(crate::open_external_url_callback(url.as_str())),
Ok(_) | Err(_) => { Ok(_) | Err(_) => {
let path = &rel_path.join(url.path()); let path = &rel_path.join(url.path());
if path.is_dir() { if path.is_dir() {

@ -69,7 +69,7 @@ struct Location {
} }
fn lsp_location_to_location(location: lsp::Location) -> Option<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, Ok(uri) => uri,
Err(err) => { Err(err) => {
log::warn!("discarding invalid or unsupported URI: {err}"); log::warn!("discarding invalid or unsupported URI: {err}");
@ -456,7 +456,7 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
.unwrap_or_default() .unwrap_or_default()
.into_iter() .into_iter()
.filter_map(|symbol| { .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, Ok(uri) => uri,
Err(err) => { Err(err) => {
log::warn!("discarding symbol with invalid URI: {err}"); log::warn!("discarding symbol with invalid URI: {err}");
@ -510,7 +510,7 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
.to_string() .to_string()
.into() .into()
} else { } else {
item.symbol.location.uri.to_string().into() item.symbol.location.uri.as_str().into()
} }
}), }),
]; ];

@ -18,7 +18,6 @@ use futures_util::Future;
mod handlers; mod handlers;
use ignore::DirEntry; use ignore::DirEntry;
use url::Url;
#[cfg(windows)] #[cfg(windows)]
fn true_color() -> bool { 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. /// Opens URL in external program.
fn open_external_url_callback( fn open_external_url_callback<U: AsRef<std::ffi::OsStr>>(
url: Url, url: U,
) -> impl Future<Output = Result<job::Callback, anyhow::Error>> + Send + 'static { ) -> impl Future<Output = Result<job::Callback, anyhow::Error>> + Send + 'static {
let commands = open::commands(url.as_str()); let commands = open::commands(url);
async { async {
for cmd in commands { for cmd in commands {
let mut command = tokio::process::Command::new(cmd.get_program()); let mut command = tokio::process::Command::new(cmd.get_program());

@ -30,9 +30,7 @@ crossterm = { version = "0.28", optional = true }
tempfile = "3.13" tempfile = "3.13"
# Conversion traits
once_cell = "1.20" once_cell = "1.20"
url = "2.5.2"
arc-swap = { version = "1.7.1" } arc-swap = { version = "1.7.1" }

@ -640,7 +640,6 @@ where
} }
use helix_lsp::{lsp, Client, LanguageServerId, LanguageServerName}; use helix_lsp::{lsp, Client, LanguageServerId, LanguageServerName};
use url::Url;
impl Document { impl Document {
pub fn from( pub fn from(
@ -1811,8 +1810,8 @@ impl Document {
} }
/// File path as a URL. /// File path as a URL.
pub fn url(&self) -> Option<Url> { pub fn url(&self) -> Option<lsp::Url> {
Url::from_file_path(self.path()?).ok() lsp::Url::from_file_path(self.path()?).ok()
} }
pub fn uri(&self) -> Option<helix_core::Uri> { pub fn uri(&self) -> Option<helix_core::Uri> {
@ -1898,7 +1897,7 @@ impl Document {
pub fn lsp_diagnostic_to_diagnostic( pub fn lsp_diagnostic_to_diagnostic(
text: &Rope, text: &Rope,
language_config: Option<&LanguageConfiguration>, language_config: Option<&LanguageConfiguration>,
diagnostic: &helix_lsp::lsp::Diagnostic, diagnostic: &lsp::Diagnostic,
language_server_id: LanguageServerId, language_server_id: LanguageServerId,
offset_encoding: helix_lsp::OffsetEncoding, offset_encoding: helix_lsp::OffsetEncoding,
) -> Option<Diagnostic> { ) -> Option<Diagnostic> {

@ -57,7 +57,7 @@ pub struct ApplyEditError {
pub enum ApplyEditErrorKind { pub enum ApplyEditErrorKind {
DocumentChanged, DocumentChanged,
FileNotFound, FileNotFound,
InvalidUrl(helix_core::uri::UrlConversionError), InvalidUrl(helix_core::uri::UriParseError),
IoError(std::io::Error), IoError(std::io::Error),
// TODO: check edits before applying and propagate failure // TODO: check edits before applying and propagate failure
// InvalidEdit, // InvalidEdit,
@ -69,8 +69,8 @@ impl From<std::io::Error> for ApplyEditErrorKind {
} }
} }
impl From<helix_core::uri::UrlConversionError> for ApplyEditErrorKind { impl From<helix_core::uri::UriParseError> for ApplyEditErrorKind {
fn from(err: helix_core::uri::UrlConversionError) -> Self { fn from(err: helix_core::uri::UriParseError) -> Self {
ApplyEditErrorKind::InvalidUrl(err) ApplyEditErrorKind::InvalidUrl(err)
} }
} }
@ -94,7 +94,7 @@ impl Editor {
text_edits: Vec<lsp::TextEdit>, text_edits: Vec<lsp::TextEdit>,
offset_encoding: OffsetEncoding, offset_encoding: OffsetEncoding,
) -> Result<(), ApplyEditErrorKind> { ) -> Result<(), ApplyEditErrorKind> {
let uri = match Uri::try_from(url) { let uri = match Uri::try_from(url.as_str()) {
Ok(uri) => uri, Ok(uri) => uri,
Err(err) => { Err(err) => {
log::error!("{err}"); log::error!("{err}");
@ -242,7 +242,7 @@ impl Editor {
// may no longer be valid. // may no longer be valid.
match op { match op {
ResourceOp::Create(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 path = uri.as_path().expect("URIs are valid paths");
let ignore_if_exists = op.options.as_ref().map_or(false, |options| { let ignore_if_exists = op.options.as_ref().map_or(false, |options| {
!options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false) !options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
@ -262,7 +262,7 @@ impl Editor {
} }
} }
ResourceOp::Delete(op) => { 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"); let path = uri.as_path().expect("URIs are valid paths");
if path.is_dir() { if path.is_dir() {
let recursive = op let recursive = op
@ -284,9 +284,9 @@ impl Editor {
} }
} }
ResourceOp::Rename(op) => { 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 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 to = to_uri.as_path().expect("URIs are valid paths");
let ignore_if_exists = op.options.as_ref().map_or(false, |options| { let ignore_if_exists = op.options.as_ref().map_or(false, |options| {
!options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false) !options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)

Loading…
Cancel
Save