Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-lsp-types/src/lib.rs')
| -rw-r--r-- | helix-lsp-types/src/lib.rs | 156 |
1 files changed, 142 insertions, 14 deletions
diff --git a/helix-lsp-types/src/lib.rs b/helix-lsp-types/src/lib.rs index 41c483f4..5024f6e7 100644 --- a/helix-lsp-types/src/lib.rs +++ b/helix-lsp-types/src/lib.rs @@ -3,27 +3,155 @@ Language Server Protocol types for Rust. Based on: <https://microsoft.github.io/language-server-protocol/specification> - -This library uses the URL crate for parsing URIs. Note that there is -some confusion on the meaning of URLs vs URIs: -<http://stackoverflow.com/a/28865728/393898>. 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)] use bitflags::bitflags; -use std::{collections::HashMap, fmt::Debug}; +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); + +// <https://datatracker.ietf.org/doc/html/rfc3986#section-2.2>, also see +// <https://github.com/microsoft/vscode-uri/blob/6dec22d7dcc6c63c30343d3a8d56050d0078cb6a/src/uri.ts#L454-L477> +const RESERVED: &percent_encoding::AsciiSet = &percent_encoding::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'='); + +impl Url { + #[allow(clippy::result_unit_err)] + #[cfg(any(unix, target_os = "redox", target_os = "wasi"))] + pub fn from_file_path<P: AsRef<Path>>(path: P) -> Result<Self, ()> { + #[cfg(any(unix, target_os = "redox"))] + use std::os::unix::prelude::OsStrExt; + #[cfg(target_os = "wasi")] + use std::os::wasi::prelude::OsStrExt; + + let mut serialization = String::from("file://"); + // skip the root component + for component in path.as_ref().components().skip(1) { + serialization.push('/'); + serialization.extend(percent_encoding::percent_encode( + component.as_os_str().as_bytes(), + RESERVED, + )); + } + if &serialization == "file://" { + // An URL's path must not be empty. + serialization.push('/'); + } + Ok(Self(serialization)) + } + + #[allow(clippy::result_unit_err)] + #[cfg(windows)] + pub fn from_file_path<P: AsRef<Path>>(path: P) -> Result<Self, ()> { + from_file_path_windows(path.as_ref()) + } + + #[allow(clippy::result_unit_err)] + #[cfg_attr(not(windows), allow(dead_code))] + fn from_file_path_windows(path: &Path) -> Result<Self, ()> { + use std::path::{Component, Prefix}; + + fn is_windows_drive_letter(segment: &str) -> bool { + segment.len() == 2 + && (segment.as_bytes()[0] as char).is_ascii_alphabetic() + && matches!(segment.as_bytes()[1], b':' | b'|') + } + + assert!(path.is_absolute()); + let mut serialization = String::from("file://"); + let mut components = path.components(); + let host_start = serialization.len() + 1; + + match components.next() { + Some(Component::Prefix(ref p)) => match p.kind() { + Prefix::Disk(letter) | Prefix::VerbatimDisk(letter) => { + serialization.push('/'); + serialization.push(letter as char); + serialization.push(':'); + } + // TODO: Prefix::UNC | Prefix::VerbatimUNC + _ => todo!("support UNC drives"), + }, + _ => unreachable!("absolute windows paths must start with a prefix"), + } + + let mut path_only_has_prefix = true; + for component in components { + if component == Component::RootDir { + continue; + } + + path_only_has_prefix = false; + + serialization.push('/'); + serialization.extend(percent_encoding::percent_encode( + component.as_os_str().as_encoded_bytes(), + RESERVED, + )); + } + + if serialization.len() > host_start + && is_windows_drive_letter(&serialization[host_start..]) + && path_only_has_prefix + { + serialization.push('/'); + } + + Ok(Self(serialization)) + } + + #[allow(clippy::result_unit_err)] + pub fn from_directory_path<P: AsRef<Path>>(path: P) -> Result<Self, ()> { + let Self(mut serialization) = Self::from_file_path(path)?; + if !serialization.ends_with('/') { + serialization.push('/'); + } + Ok(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 +2971,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":[]}}"#, ); } |