/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
cli/lsp/urls.rs
299 строк
10 KB
em
fix(lsp): spurious diagnostics in Jupyter notebook cells (#34734)
03 июн 2026, 04:36
Не верифицирован
03 июн 2026, 04:36
85cb7ae
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. use std::path::Component; use std::path::Path; use std::path::PathBuf; use std::path::Prefix; use std::str::FromStr; use deno_config::UrlToFilePathError; use deno_core::error::AnyError; use deno_core::url::Position; use deno_core::url::Url; use deno_path_util::url_to_file_path; use lsp_types::Uri; use super::logging::lsp_warn; pub fn uri_parse_unencoded(s: &str) -> Result<Uri, AnyError> { url_to_uri(&Url::parse(s)?) } pub fn normalize_uri(uri: &Uri) -> Uri { if !uri.scheme().as_str().eq_ignore_ascii_case("file") { return normalize_non_file_uri(uri); } let Some(path) = uri.to_file_path() else { return normalize_non_file_uri(uri); }; let normalized_path = normalize_path(path); let mut encoded_path = fluent_uri::pct_enc::EString::<fluent_uri::pct_enc::encoder::Path>::new(); let mut path_only_has_prefix = false; for component in normalized_path.components() { match component { Component::Prefix(prefix) => { path_only_has_prefix = true; match prefix.kind() { Prefix::Disk(mut letter) | Prefix::VerbatimDisk(mut letter) => { encoded_path.encode_str::<fluent_uri::pct_enc::encoder::Path>("/"); letter.make_ascii_uppercase(); let b = [letter]; // SAFETY: Drive letter is ascii. let s = unsafe { str::from_utf8_unchecked(&b) }; encoded_path.encode_str::<fluent_uri::pct_enc::encoder::Path>(s); encoded_path.encode_str::<fluent_uri::pct_enc::encoder::Path>(":"); } Prefix::UNC(..) | Prefix::VerbatimUNC(..) => { // These should be carried in `uri.authority()`. } Prefix::Verbatim(_) | Prefix::DeviceNS(_) => { // Not a local path, abort. return normalize_non_file_uri(uri); } } } Component::RootDir => {} component => { path_only_has_prefix = false; encoded_path.encode_str::<fluent_uri::pct_enc::encoder::Path>("/"); encoded_path.encode_str::<fluent_uri::pct_enc::encoder::Path>( &component.as_os_str().to_string_lossy(), ); } } } if encoded_path.is_empty() || path_only_has_prefix { encoded_path.encode_str::<fluent_uri::pct_enc::encoder::Path>("/"); } fluent_uri::Uri::builder() .scheme(fluent_uri::component::Scheme::new_or_panic("file")) .optional(fluent_uri::build::Builder::authority, uri.authority()) .path(encoded_path.as_ref()) .optional(fluent_uri::build::Builder::query, uri.query()) .optional(fluent_uri::build::Builder::fragment, uri.fragment()) .build() .expect("component constraints should be met by the above") .normalize() .into() } // TODO(nayeemrmn): Remove whe `Uri::normalize()` is fixed: // https://github.com/yescallop/fluent-uri-rs/issues/45 pub fn normalize_non_file_uri(uri: &Uri) -> Uri { let mut encoded_path = fluent_uri::pct_enc::EString::<fluent_uri::pct_enc::encoder::Path>::new(); encoded_path.encode_str::<fluent_uri::pct_enc::encoder::Path>( &percent_encoding::percent_decode_str(uri.path().as_str()) .decode_utf8_lossy(), ); fluent_uri::Uri::builder() .scheme(uri.scheme()) .optional(fluent_uri::build::Builder::authority, uri.authority()) .path(encoded_path.as_ref()) .optional(fluent_uri::build::Builder::query, uri.query()) .optional(fluent_uri::build::Builder::fragment, uri.fragment()) .build() .expect("component constraints should be met by the above") .into() } pub fn url_to_uri(url: &Url) -> Result<Uri, AnyError> { let uri_before_path = Uri::from_str(&url[..Position::BeforePath]) .inspect_err(|err| { lsp_warn!("Could not convert URL \"{url}\" to URI: {err}") })?; let mut encoded_path = fluent_uri::pct_enc::EString::<fluent_uri::pct_enc::encoder::Path>::new(); encoded_path.encode_str::<fluent_uri::pct_enc::encoder::Path>( &percent_encoding::percent_decode_str(url.path()).decode_utf8_lossy(), ); let encoded_query = url.query().map(|query| { let mut encoded_query = fluent_uri::pct_enc::EString::< fluent_uri::pct_enc::encoder::Query, >::new(); encoded_query.encode_str::<fluent_uri::pct_enc::encoder::Query>(query); encoded_query }); let encoded_fragment = url.fragment().map(|fragment| { let mut encoded_fragment = fluent_uri::pct_enc::EString::< fluent_uri::pct_enc::encoder::Fragment, >::new(); encoded_fragment .encode_str::<fluent_uri::pct_enc::encoder::Fragment>(fragment); encoded_fragment }); let uri = fluent_uri::Uri::builder() .scheme(uri_before_path.scheme()) .optional( fluent_uri::build::Builder::authority, uri_before_path.authority(), ) .path(encoded_path.as_ref()) .optional(fluent_uri::build::Builder::query, encoded_query.as_deref()) .optional( fluent_uri::build::Builder::fragment, encoded_fragment.as_deref(), ) .build() .expect("component constraints should be met by the above") .into(); Ok(normalize_uri(&uri)) } pub fn uri_to_url(uri: &Uri) -> Url { (|| { let scheme = uri.scheme(); if !scheme.as_str().eq_ignore_ascii_case("untitled") && !scheme.as_str().eq_ignore_ascii_case("vscode-notebook-cell") && !scheme.as_str().eq_ignore_ascii_case("deno-notebook-cell") && !scheme.as_str().eq_ignore_ascii_case("vscode-userdata") { return None; } let mut s = String::with_capacity(uri.as_str().len()); s.push_str("file:///"); s.push_str(uri.path().as_str().trim_start_matches('/')); if let Some(query) = uri.query() { s.push('?'); s.push_str(query.as_str()); } if let Some(fragment) = uri.fragment() { s.push('#'); s.push_str(fragment.as_str()); } Url::parse(&s).ok().map(normalize_url) })() .unwrap_or_else(|| { normalize_url( Url::parse(uri.as_str()) .inspect_err(|err| { lsp_warn!("Couldn't parse uri \"{}\" as url: {err}", 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 scheme = uri.scheme(); scheme.as_str().eq_ignore_ascii_case("file") || scheme.as_str().eq_ignore_ascii_case("untitled") || scheme.as_str().eq_ignore_ascii_case("vscode-notebook-cell") || scheme.as_str().eq_ignore_ascii_case("deno-notebook-cell") || scheme.as_str().eq_ignore_ascii_case("vscode-userdata") } /// Whether the URI refers to an in-memory editor document that isn't backed by /// a file on disk yet: untitled files and unsaved (untitled) notebook cells. /// These convert to a `file:` URL at the filesystem root via [`uri_to_url`], so /// they match no workspace scope and need to be associated with the workspace /// root instead. pub fn uri_is_in_memory(uri: &Uri) -> bool { let scheme = uri.scheme(); scheme.as_str().eq_ignore_ascii_case("untitled") || scheme.as_str().eq_ignore_ascii_case("vscode-notebook-cell") || scheme.as_str().eq_ignore_ascii_case("deno-notebook-cell") } 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. pub 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()) } #[cfg(test)] mod tests { use super::*; #[test] fn test_normalize_non_file_uri_no_double_encode_fragment() { // Notebook cell URIs have percent-encoded fragments (base64 padding). let uri: Uri = "vscode-notebook-cell:/Users/test/scratch.ipynb#W1sZmlsZQ%3D%3D" .parse() .unwrap(); let normalized = normalize_non_file_uri(&uri); assert_eq!( normalized.as_str(), "vscode-notebook-cell:/Users/test/scratch.ipynb#W1sZmlsZQ%3D%3D" ); // Ensure idempotency. let normalized2 = normalize_non_file_uri(&normalized); assert_eq!(normalized.as_str(), normalized2.as_str()); } #[test] fn test_normalize_uri_notebook_cell_idempotent() { let uri: Uri = "vscode-notebook-cell:/Users/test/scratch.ipynb#W0sZmlsZQ%3D%3D.ts" .parse() .unwrap(); let n1 = normalize_uri(&uri); let n2 = normalize_uri(&n1); assert_eq!(n1.as_str(), n2.as_str()); } #[test] fn test_normalize_non_file_uri_no_double_encode_query() { let uri: Uri = "custom-scheme:/path?key=val%20ue".parse().unwrap(); let normalized = normalize_non_file_uri(&uri); assert_eq!(normalized.as_str(), "custom-scheme:/path?key=val%20ue"); let normalized2 = normalize_non_file_uri(&normalized); assert_eq!(normalized.as_str(), normalized2.as_str()); } }