mirror of
https://github.com/denoland/deno.git
synced 2024-11-25 15:29:32 -05:00
fix(lsp): decoding percent-encoding(non-ASCII) file path correctly (#22582)
This commit is contained in:
parent
3462248571
commit
feb744cebd
5 changed files with 162 additions and 19 deletions
|
@ -21,6 +21,7 @@ use crate::graph_util::enhanced_resolution_error_message;
|
||||||
use crate::lsp::lsp_custom::DiagnosticBatchNotificationParams;
|
use crate::lsp::lsp_custom::DiagnosticBatchNotificationParams;
|
||||||
use crate::resolver::SloppyImportsResolution;
|
use crate::resolver::SloppyImportsResolution;
|
||||||
use crate::resolver::SloppyImportsResolver;
|
use crate::resolver::SloppyImportsResolver;
|
||||||
|
use crate::util::path::to_percent_decoded_str;
|
||||||
|
|
||||||
use deno_ast::MediaType;
|
use deno_ast::MediaType;
|
||||||
use deno_core::anyhow::anyhow;
|
use deno_core::anyhow::anyhow;
|
||||||
|
@ -1212,8 +1213,10 @@ impl DenoDiagnostic {
|
||||||
specifier: &ModuleSpecifier,
|
specifier: &ModuleSpecifier,
|
||||||
sloppy_resolution: SloppyImportsResolution,
|
sloppy_resolution: SloppyImportsResolution,
|
||||||
) -> String {
|
) -> String {
|
||||||
let mut message =
|
let mut message = format!(
|
||||||
format!("Unable to load a local module: {}\n", specifier);
|
"Unable to load a local module: {}\n",
|
||||||
|
to_percent_decoded_str(specifier.as_ref())
|
||||||
|
);
|
||||||
if let Some(additional_message) =
|
if let Some(additional_message) =
|
||||||
sloppy_resolution.as_suggestion_message()
|
sloppy_resolution.as_suggestion_message()
|
||||||
{
|
{
|
||||||
|
@ -1971,6 +1974,50 @@ let c: number = "a";
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn unable_to_load_a_local_module() {
|
||||||
|
let temp_dir = TempDir::new();
|
||||||
|
let (snapshot, _) = setup(
|
||||||
|
&temp_dir,
|
||||||
|
&[(
|
||||||
|
"file:///a.ts",
|
||||||
|
r#"
|
||||||
|
import { 東京 } from "./🦕.ts";
|
||||||
|
"#,
|
||||||
|
1,
|
||||||
|
LanguageId::TypeScript,
|
||||||
|
)],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let config = mock_config();
|
||||||
|
let token = CancellationToken::new();
|
||||||
|
let actual = generate_deno_diagnostics(&snapshot, &config, token);
|
||||||
|
assert_eq!(actual.len(), 1);
|
||||||
|
let record = actual.first().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
json!(record.versioned.diagnostics),
|
||||||
|
json!([
|
||||||
|
{
|
||||||
|
"range": {
|
||||||
|
"start": {
|
||||||
|
"line": 1,
|
||||||
|
"character": 27
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"line": 1,
|
||||||
|
"character": 35
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"severity": 1,
|
||||||
|
"code": "no-local",
|
||||||
|
"source": "deno",
|
||||||
|
"message": "Unable to load a local module: file:///🦕.ts\nPlease check the file path.",
|
||||||
|
}
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_specifier_text_for_redirected() {
|
fn test_specifier_text_for_redirected() {
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
|
|
|
@ -122,6 +122,7 @@ use crate::tools::upgrade::upgrade_check_enabled;
|
||||||
use crate::util::fs::remove_dir_all_if_exists;
|
use crate::util::fs::remove_dir_all_if_exists;
|
||||||
use crate::util::path::is_importable_ext;
|
use crate::util::path::is_importable_ext;
|
||||||
use crate::util::path::specifier_to_file_path;
|
use crate::util::path::specifier_to_file_path;
|
||||||
|
use crate::util::path::to_percent_decoded_str;
|
||||||
use crate::util::progress_bar::ProgressBar;
|
use crate::util::progress_bar::ProgressBar;
|
||||||
use crate::util::progress_bar::ProgressBarStyle;
|
use crate::util::progress_bar::ProgressBarStyle;
|
||||||
|
|
||||||
|
@ -1738,16 +1739,21 @@ impl Inner {
|
||||||
match resolution {
|
match resolution {
|
||||||
Resolution::Ok(resolved) => {
|
Resolution::Ok(resolved) => {
|
||||||
let specifier = &resolved.specifier;
|
let specifier = &resolved.specifier;
|
||||||
|
let format = |scheme: &str, rest: &str| -> String {
|
||||||
|
format!("{}​{}", scheme, rest).replace('@', "​@")
|
||||||
|
};
|
||||||
match specifier.scheme() {
|
match specifier.scheme() {
|
||||||
"data" => "_(a data url)_".to_string(),
|
"data" => "_(a data url)_".to_string(),
|
||||||
"blob" => "_(a blob url)_".to_string(),
|
"blob" => "_(a blob url)_".to_string(),
|
||||||
|
"file" => format(
|
||||||
|
&specifier[..url::Position::AfterScheme],
|
||||||
|
&to_percent_decoded_str(&specifier[url::Position::AfterScheme..]),
|
||||||
|
),
|
||||||
_ => {
|
_ => {
|
||||||
let mut result = format!(
|
let mut result = format(
|
||||||
"{}​{}",
|
|
||||||
&specifier[..url::Position::AfterScheme],
|
&specifier[..url::Position::AfterScheme],
|
||||||
&specifier[url::Position::AfterScheme..],
|
&specifier[url::Position::AfterScheme..],
|
||||||
)
|
);
|
||||||
.replace('@', "​@");
|
|
||||||
if let Ok(jsr_req_ref) =
|
if let Ok(jsr_req_ref) =
|
||||||
JsrPackageReqReference::from_specifier(specifier)
|
JsrPackageReqReference::from_specifier(specifier)
|
||||||
{
|
{
|
||||||
|
|
|
@ -31,6 +31,7 @@ use crate::tsc;
|
||||||
use crate::tsc::ResolveArgs;
|
use crate::tsc::ResolveArgs;
|
||||||
use crate::util::path::relative_specifier;
|
use crate::util::path::relative_specifier;
|
||||||
use crate::util::path::specifier_to_file_path;
|
use crate::util::path::specifier_to_file_path;
|
||||||
|
use crate::util::path::to_percent_decoded_str;
|
||||||
|
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use deno_ast::MediaType;
|
use deno_ast::MediaType;
|
||||||
|
@ -543,6 +544,10 @@ impl TsServer {
|
||||||
.and_then(|mut changes| {
|
.and_then(|mut changes| {
|
||||||
for changes in &mut changes {
|
for changes in &mut changes {
|
||||||
changes.normalize(&self.specifier_map)?;
|
changes.normalize(&self.specifier_map)?;
|
||||||
|
for text_changes in &mut changes.text_changes {
|
||||||
|
text_changes.new_text =
|
||||||
|
to_percent_decoded_str(&text_changes.new_text);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(changes)
|
Ok(changes)
|
||||||
})
|
})
|
||||||
|
@ -1611,7 +1616,41 @@ fn display_parts_to_string(
|
||||||
link.name = Some(part.text.clone());
|
link.name = Some(part.text.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => out.push(part.text.clone()),
|
_ => out.push(
|
||||||
|
// should decode percent-encoding string when hovering over the right edge of module specifier like below
|
||||||
|
// module "file:///path/to/🦕"
|
||||||
|
to_percent_decoded_str(&part.text),
|
||||||
|
// NOTE: The reason why an example above that lacks `.ts` extension is caused by the implementation of tsc itself.
|
||||||
|
// The request `tsc.request.getQuickInfoAtPosition` receives the payload from tsc host as follows.
|
||||||
|
// {
|
||||||
|
// text_span: {
|
||||||
|
// start: 19,
|
||||||
|
// length: 9,
|
||||||
|
// },
|
||||||
|
// displayParts:
|
||||||
|
// [
|
||||||
|
// {
|
||||||
|
// text: "module",
|
||||||
|
// kind: "keyword",
|
||||||
|
// target: null,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// text: " ",
|
||||||
|
// kind: "space",
|
||||||
|
// target: null,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// text: "\"file:///path/to/%F0%9F%A6%95\"",
|
||||||
|
// kind: "stringLiteral",
|
||||||
|
// target: null,
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// documentation: [],
|
||||||
|
// tags: null,
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// related issue: https://github.com/denoland/deno/issues/16058
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5424,7 +5463,7 @@ mod tests {
|
||||||
.get_edits_for_file_rename(
|
.get_edits_for_file_rename(
|
||||||
snapshot,
|
snapshot,
|
||||||
resolve_url("file:///b.ts").unwrap(),
|
resolve_url("file:///b.ts").unwrap(),
|
||||||
resolve_url("file:///c.ts").unwrap(),
|
resolve_url("file:///🦕.ts").unwrap(),
|
||||||
FormatCodeSettings::default(),
|
FormatCodeSettings::default(),
|
||||||
UserPreferences::default(),
|
UserPreferences::default(),
|
||||||
)
|
)
|
||||||
|
@ -5439,7 +5478,7 @@ mod tests {
|
||||||
start: 8,
|
start: 8,
|
||||||
length: 6,
|
length: 6,
|
||||||
},
|
},
|
||||||
new_text: "./c.ts".to_string(),
|
new_text: "./🦕.ts".to_string(),
|
||||||
}],
|
}],
|
||||||
is_new_file: None,
|
is_new_file: None,
|
||||||
}]
|
}]
|
||||||
|
|
|
@ -156,11 +156,12 @@ pub fn relative_specifier(
|
||||||
text.push('/');
|
text.push('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(if text.starts_with("../") || text.starts_with("./") {
|
let text = if text.starts_with("../") || text.starts_with("./") {
|
||||||
text
|
text
|
||||||
} else {
|
} else {
|
||||||
format!("./{text}")
|
format!("./{text}")
|
||||||
})
|
};
|
||||||
|
Some(to_percent_decoded_str(&text))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets a path with the specified file stem suffix.
|
/// Gets a path with the specified file stem suffix.
|
||||||
|
@ -265,6 +266,24 @@ pub fn matches_pattern_or_exact_path(
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// For decoding percent-encodeing string
|
||||||
|
/// could be used for module specifier string literal of local modules,
|
||||||
|
/// or local file path to display `non-ASCII` characters correctly
|
||||||
|
/// # Examples
|
||||||
|
/// ```
|
||||||
|
/// use crate::util::path::to_percent_decoded_str;
|
||||||
|
///
|
||||||
|
/// let str = to_percent_decoded_str("file:///Users/path/to/%F0%9F%A6%95.ts");
|
||||||
|
/// assert_eq!(str, "file:///Users/path/to/🦕.ts");
|
||||||
|
/// ```
|
||||||
|
pub fn to_percent_decoded_str(s: &str) -> String {
|
||||||
|
match percent_encoding::percent_decode_str(s).decode_utf8() {
|
||||||
|
Ok(s) => s.to_string(),
|
||||||
|
// when failed to decode, return the original string
|
||||||
|
Err(_) => s.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -457,4 +476,10 @@ mod test {
|
||||||
PathBuf::from("/test_2.d.cts")
|
PathBuf::from("/test_2.d.cts")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_to_percent_decoded_str() {
|
||||||
|
let str = to_percent_decoded_str("%F0%9F%A6%95");
|
||||||
|
assert_eq!(str, "🦕");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2266,7 +2266,7 @@ fn lsp_hover_dependency() {
|
||||||
"uri": "file:///a/file.ts",
|
"uri": "file:///a/file.ts",
|
||||||
"languageId": "typescript",
|
"languageId": "typescript",
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"text": "import * as a from \"http://127.0.0.1:4545/xTypeScriptTypes.js\";\n// @deno-types=\"http://127.0.0.1:4545/type_definitions/foo.d.ts\"\nimport * as b from \"http://127.0.0.1:4545/type_definitions/foo.js\";\nimport * as c from \"http://127.0.0.1:4545/subdir/type_reference.js\";\nimport * as d from \"http://127.0.0.1:4545/subdir/mod1.ts\";\nimport * as e from \"data:application/typescript;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=\";\nimport * as f from \"./file_01.ts\";\nimport * as g from \"http://localhost:4545/x/a/mod.ts\";\n\nconsole.log(a, b, c, d, e, f, g);\n"
|
"text": "import * as a from \"http://127.0.0.1:4545/xTypeScriptTypes.js\";\n// @deno-types=\"http://127.0.0.1:4545/type_definitions/foo.d.ts\"\nimport * as b from \"http://127.0.0.1:4545/type_definitions/foo.js\";\nimport * as c from \"http://127.0.0.1:4545/subdir/type_reference.js\";\nimport * as d from \"http://127.0.0.1:4545/subdir/mod1.ts\";\nimport * as e from \"data:application/typescript;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=\";\nimport * as f from \"./file_01.ts\";\nimport * as g from \"http://localhost:4545/x/a/mod.ts\";\nimport * as h from \"./mod🦕.ts\";\n\nconsole.log(a, b, c, d, e, f, g, h);\n"
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
@ -2387,6 +2387,28 @@ fn lsp_hover_dependency() {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
let res = client.write_request(
|
||||||
|
"textDocument/hover",
|
||||||
|
json!({
|
||||||
|
"textDocument": {
|
||||||
|
"uri": "file:///a/file.ts",
|
||||||
|
},
|
||||||
|
"position": { "line": 8, "character": 28 }
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
res,
|
||||||
|
json!({
|
||||||
|
"contents": {
|
||||||
|
"kind": "markdown",
|
||||||
|
"value": "**Resolved Dependency**\n\n**Code**: file​:///a/mod🦕.ts\n"
|
||||||
|
},
|
||||||
|
"range": {
|
||||||
|
"start": { "line": 8, "character": 19 },
|
||||||
|
"end":{ "line": 8, "character": 30 }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This tests for a regression covered by denoland/deno#12753 where the lsp was
|
// This tests for a regression covered by denoland/deno#12753 where the lsp was
|
||||||
|
@ -6637,7 +6659,7 @@ fn lsp_completions_auto_import() {
|
||||||
client.initialize_default();
|
client.initialize_default();
|
||||||
client.did_open(json!({
|
client.did_open(json!({
|
||||||
"textDocument": {
|
"textDocument": {
|
||||||
"uri": "file:///a/b.ts",
|
"uri": "file:///a/🦕.ts",
|
||||||
"languageId": "typescript",
|
"languageId": "typescript",
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"text": "export const foo = \"foo\";\n",
|
"text": "export const foo = \"foo\";\n",
|
||||||
|
@ -6668,7 +6690,7 @@ fn lsp_completions_auto_import() {
|
||||||
let req = json!({
|
let req = json!({
|
||||||
"label": "foo",
|
"label": "foo",
|
||||||
"labelDetails": {
|
"labelDetails": {
|
||||||
"description": "./b.ts",
|
"description": "./🦕.ts",
|
||||||
},
|
},
|
||||||
"kind": 6,
|
"kind": 6,
|
||||||
"sortText": "16_0",
|
"sortText": "16_0",
|
||||||
|
@ -6683,12 +6705,16 @@ fn lsp_completions_auto_import() {
|
||||||
"specifier": "file:///a/file.ts",
|
"specifier": "file:///a/file.ts",
|
||||||
"position": 12,
|
"position": 12,
|
||||||
"name": "foo",
|
"name": "foo",
|
||||||
"source": "./b.ts",
|
"source": "./%F0%9F%A6%95.ts",
|
||||||
|
"specifierRewrite": [
|
||||||
|
"./%F0%9F%A6%95.ts",
|
||||||
|
"./🦕.ts",
|
||||||
|
],
|
||||||
"data": {
|
"data": {
|
||||||
"exportName": "foo",
|
"exportName": "foo",
|
||||||
"exportMapKey": "",
|
"exportMapKey": "",
|
||||||
"moduleSpecifier": "./b.ts",
|
"moduleSpecifier": "./%F0%9F%A6%95.ts",
|
||||||
"fileName": "file:///a/b.ts"
|
"fileName": "file:///a/%F0%9F%A6%95.ts"
|
||||||
},
|
},
|
||||||
"useCodeSnippet": false
|
"useCodeSnippet": false
|
||||||
}
|
}
|
||||||
|
@ -6702,7 +6728,7 @@ fn lsp_completions_auto_import() {
|
||||||
json!({
|
json!({
|
||||||
"label": "foo",
|
"label": "foo",
|
||||||
"labelDetails": {
|
"labelDetails": {
|
||||||
"description": "./b.ts",
|
"description": "./🦕.ts",
|
||||||
},
|
},
|
||||||
"kind": 6,
|
"kind": 6,
|
||||||
"detail": "const foo: \"foo\"",
|
"detail": "const foo: \"foo\"",
|
||||||
|
@ -6717,7 +6743,7 @@ fn lsp_completions_auto_import() {
|
||||||
"start": { "line": 0, "character": 0 },
|
"start": { "line": 0, "character": 0 },
|
||||||
"end": { "line": 0, "character": 0 }
|
"end": { "line": 0, "character": 0 }
|
||||||
},
|
},
|
||||||
"newText": "import { foo } from \"./b.ts\";\n\n"
|
"newText": "import { foo } from \"./🦕.ts\";\n\n"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue