mirror of
https://github.com/denoland/deno.git
synced 2025-08-04 10:59:13 +00:00
224 lines
5.9 KiB
Rust
224 lines
5.9 KiB
Rust
// Copyright 2018-2025 the Deno authors. MIT license.
|
|
|
|
use std::borrow::Cow;
|
|
use std::path::Component;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
use std::str::FromStr;
|
|
|
|
use deno_config::UrlToFilePathError;
|
|
use deno_core::error::AnyError;
|
|
use deno_core::url::Url;
|
|
use deno_path_util::url_to_file_path;
|
|
use lsp_types::Uri;
|
|
|
|
use super::logging::lsp_warn;
|
|
|
|
/// Matches the `encodeURIComponent()` encoding from JavaScript, which matches
|
|
/// the component percent encoding set.
|
|
///
|
|
/// See: <https://url.spec.whatwg.org/#component-percent-encode-set>
|
|
pub const COMPONENT: &percent_encoding::AsciiSet = &percent_encoding::CONTROLS
|
|
.add(b' ')
|
|
.add(b'"')
|
|
.add(b'#')
|
|
.add(b'<')
|
|
.add(b'>')
|
|
.add(b'?')
|
|
.add(b'`')
|
|
.add(b'{')
|
|
.add(b'}')
|
|
.add(b'/')
|
|
.add(b':')
|
|
.add(b';')
|
|
.add(b'=')
|
|
.add(b'@')
|
|
.add(b'[')
|
|
.add(b'\\')
|
|
.add(b']')
|
|
.add(b'^')
|
|
.add(b'|')
|
|
.add(b'$')
|
|
.add(b'%')
|
|
.add(b'&')
|
|
.add(b'+')
|
|
.add(b',');
|
|
|
|
/// Characters that are left unencoded in a `Url` path but will be encoded in a
|
|
/// VSCode URI.
|
|
const URL_TO_URI_PATH: &percent_encoding::AsciiSet =
|
|
&percent_encoding::CONTROLS
|
|
.add(b' ')
|
|
.add(b'!')
|
|
.add(b'$')
|
|
.add(b'&')
|
|
.add(b'\'')
|
|
.add(b'(')
|
|
.add(b')')
|
|
.add(b'*')
|
|
.add(b'+')
|
|
.add(b',')
|
|
.add(b':')
|
|
.add(b';')
|
|
.add(b'=')
|
|
.add(b'@')
|
|
.add(b'[')
|
|
.add(b']')
|
|
.add(b'^')
|
|
.add(b'|');
|
|
|
|
/// Characters that may be left unencoded in a `Url` query but not valid in a
|
|
/// `Uri` query.
|
|
const URL_TO_URI_QUERY: &percent_encoding::AsciiSet =
|
|
&URL_TO_URI_PATH.add(b'\\').add(b'`').add(b'{').add(b'}');
|
|
|
|
/// Characters that may be left unencoded in a `Url` fragment but not valid in
|
|
/// a `Uri` fragment.
|
|
const URL_TO_URI_FRAGMENT: &percent_encoding::AsciiSet =
|
|
&URL_TO_URI_PATH.add(b'#').add(b'\\').add(b'{').add(b'}');
|
|
|
|
pub fn uri_parse_unencoded(s: &str) -> Result<Uri, AnyError> {
|
|
url_to_uri(&Url::parse(s)?)
|
|
}
|
|
|
|
pub fn normalize_uri(uri: &Uri) -> Cow<'_, Uri> {
|
|
if !uri.scheme().is_some_and(|s| s.eq_lowercase("file")) {
|
|
return Cow::Borrowed(uri);
|
|
}
|
|
let url = normalize_url(Url::parse(uri.as_str()).unwrap());
|
|
let Ok(normalized_uri) = url_to_uri(&url) else {
|
|
return Cow::Borrowed(uri);
|
|
};
|
|
Cow::Owned(normalized_uri)
|
|
}
|
|
|
|
pub fn url_to_uri(url: &Url) -> Result<Uri, AnyError> {
|
|
let components = deno_core::url::quirks::internal_components(url);
|
|
let mut input = String::with_capacity(url.as_str().len());
|
|
input.push_str(&url.as_str()[..components.path_start as usize]);
|
|
let path = url.path();
|
|
let mut chars = path.chars();
|
|
let has_drive_letter = chars.next().is_some_and(|c| c == '/')
|
|
&& chars.next().is_some_and(|c| c.is_ascii_alphabetic())
|
|
&& chars.next().is_some_and(|c| c == ':')
|
|
&& chars.next().is_none_or(|c| c == '/');
|
|
if has_drive_letter {
|
|
let (dl_part, rest) = path.split_at(2);
|
|
input.push_str(&dl_part.to_ascii_lowercase());
|
|
input.push_str(
|
|
&percent_encoding::utf8_percent_encode(rest, URL_TO_URI_PATH).to_string(),
|
|
);
|
|
} else {
|
|
input.push_str(
|
|
&percent_encoding::utf8_percent_encode(path, URL_TO_URI_PATH).to_string(),
|
|
);
|
|
}
|
|
if let Some(query) = url.query() {
|
|
input.push('?');
|
|
input.push_str(
|
|
&percent_encoding::utf8_percent_encode(query, URL_TO_URI_QUERY)
|
|
.to_string(),
|
|
);
|
|
}
|
|
if let Some(fragment) = url.fragment() {
|
|
input.push('#');
|
|
input.push_str(
|
|
&percent_encoding::utf8_percent_encode(fragment, URL_TO_URI_FRAGMENT)
|
|
.to_string(),
|
|
);
|
|
}
|
|
Ok(Uri::from_str(&input).inspect_err(|err| {
|
|
lsp_warn!("Could not convert URL \"{url}\" to URI: {err}")
|
|
})?)
|
|
}
|
|
|
|
pub fn uri_to_url(uri: &Uri) -> Url {
|
|
(|| {
|
|
let scheme = uri.scheme()?;
|
|
if !scheme.eq_lowercase("untitled")
|
|
&& !scheme.eq_lowercase("vscode-notebook-cell")
|
|
&& !scheme.eq_lowercase("deno-notebook-cell")
|
|
&& !scheme.eq_lowercase("vscode-userdata")
|
|
{
|
|
return None;
|
|
}
|
|
Url::parse(&format!(
|
|
"file:///{}",
|
|
&uri.as_str()[uri.path_bounds.0 as usize..].trim_start_matches('/'),
|
|
))
|
|
.ok()
|
|
.map(normalize_url)
|
|
})()
|
|
.unwrap_or_else(|| normalize_url(Url::parse(uri.as_str()).unwrap()))
|
|
}
|
|
|
|
pub fn uri_to_file_path(uri: &Uri) -> Result<PathBuf, UrlToFilePathError> {
|
|
url_to_file_path(&uri_to_url(uri))
|
|
}
|
|
|
|
pub fn uri_is_file_like(uri: &Uri) -> bool {
|
|
let Some(scheme) = uri.scheme() else {
|
|
return false;
|
|
};
|
|
scheme.eq_lowercase("file")
|
|
|| scheme.eq_lowercase("untitled")
|
|
|| scheme.eq_lowercase("vscode-notebook-cell")
|
|
|| scheme.eq_lowercase("deno-notebook-cell")
|
|
|| scheme.eq_lowercase("vscode-userdata")
|
|
}
|
|
|
|
fn normalize_url(url: Url) -> Url {
|
|
let Ok(path) = url_to_file_path(&url) else {
|
|
return url;
|
|
};
|
|
let normalized_path = normalize_path(&path);
|
|
let Ok(mut normalized_url) = Url::from_file_path(&normalized_path) else {
|
|
return url;
|
|
};
|
|
if let Some(query) = url.query() {
|
|
normalized_url.set_query(Some(query));
|
|
}
|
|
if let Some(fragment) = url.fragment() {
|
|
normalized_url.set_fragment(Some(fragment));
|
|
}
|
|
normalized_url
|
|
}
|
|
|
|
// TODO(nayeemrmn): Change the version of this in deno_path_util to force
|
|
// uppercase on drive letters. Then remove this.
|
|
fn normalize_path<P: AsRef<Path>>(path: P) -> PathBuf {
|
|
fn inner(path: &Path) -> PathBuf {
|
|
let mut components = path.components().peekable();
|
|
let mut ret =
|
|
if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
|
|
components.next();
|
|
let s = c.as_os_str();
|
|
if s.len() == 2 {
|
|
PathBuf::from(s.to_ascii_uppercase())
|
|
} else {
|
|
PathBuf::from(s)
|
|
}
|
|
} else {
|
|
PathBuf::new()
|
|
};
|
|
|
|
for component in components {
|
|
match component {
|
|
Component::Prefix(..) => unreachable!(),
|
|
Component::RootDir => {
|
|
ret.push(component.as_os_str());
|
|
}
|
|
Component::CurDir => {}
|
|
Component::ParentDir => {
|
|
ret.pop();
|
|
}
|
|
Component::Normal(c) => {
|
|
ret.push(c);
|
|
}
|
|
}
|
|
}
|
|
ret
|
|
}
|
|
|
|
inner(path.as_ref())
|
|
}
|