diff --git a/crates/tinymist-query/src/lsp_typst_boundary.rs b/crates/tinymist-query/src/lsp_typst_boundary.rs index e7aaaec5..0d30f2e0 100644 --- a/crates/tinymist-query/src/lsp_typst_boundary.rs +++ b/crates/tinymist-query/src/lsp_typst_boundary.rs @@ -6,6 +6,7 @@ use std::path::{Path, PathBuf}; use lsp_types::{self, Url}; +use once_cell::sync::Lazy; use reflexo::path::PathClean; pub type LspPosition = lsp_types::Position; @@ -68,21 +69,35 @@ pub type TypstCompletion = crate::upstream::Completion; pub type TypstCompletionKind = crate::upstream::CompletionKind; const UNTITLED_ROOT: &str = "/untitled"; +static EMPTY_URL: Lazy = Lazy::new(|| Url::parse("file://").unwrap()); pub fn path_to_url(path: &Path) -> anyhow::Result { if let Ok(untitled) = path.strip_prefix(UNTITLED_ROOT) { + // rust-url will panic on converting an empty path. + if untitled == Path::new("nEoViM-BuG") { + return Ok(EMPTY_URL.clone()); + } + return Ok(Url::parse(&format!("untitled:{}", untitled.display()))?); } - Url::from_file_path(path).map_err(|e| { + Url::from_file_path(path).or_else(|e| { let _: () = e; - anyhow::anyhow!("could not convert path to URI: path: {path:?}",) + + anyhow::bail!("could not convert path to URI: path: {path:?}",) }) } pub fn url_to_path(uri: Url) -> PathBuf { if uri.scheme() == "file" { - return uri.to_file_path().unwrap(); + // typst converts an empty path to `Path::new("/")`, which is undesirable. + if !uri.has_host() && uri.path() == "/" { + return PathBuf::from("/untitled/nEoViM-BuG"); + } + + return uri + .to_file_path() + .unwrap_or_else(|_| panic!("could not convert URI to path: URI: {uri:?}",)); } if uri.scheme() == "untitled" { @@ -375,6 +390,17 @@ mod test { assert_eq!(path, Path::new("/untitled/test").clean()); } + #[test] + fn unnamed_buffer() { + // https://github.com/neovim/nvim-lspconfig/pull/2226 + let uri = EMPTY_URL.clone(); + let path = url_to_path(uri); + assert_eq!(path, Path::new("/untitled/nEoViM-BuG")); + + let uri2 = path_to_url(&path).unwrap(); + assert_eq!(EMPTY_URL.clone(), uri2); + } + const ENCODING_TEST_STRING: &str = "test 🥺 test"; #[test] diff --git a/scripts/e2e.ps1 b/scripts/e2e.ps1 new file mode 100644 index 00000000..620b1446 --- /dev/null +++ b/scripts/e2e.ps1 @@ -0,0 +1,4 @@ + +cargo build --release --bin tinymist +Copy-Item -Path ".\target\release\tinymist.exe" -Destination ".\editors\vscode\out\tinymist.exe" -Force +cargo insta test -p tests --accept diff --git a/scripts/e2e.sh b/scripts/e2e.sh new file mode 100755 index 00000000..1f0d8ee6 --- /dev/null +++ b/scripts/e2e.sh @@ -0,0 +1,3 @@ +cargo build --release --bin tinymist +cp target/release/tinymist editors/vscode/out/tinymist +cargo insta test -p tests --accept diff --git a/tests/e2e/main.rs b/tests/e2e/main.rs index b00ffe54..5b1307fa 100644 --- a/tests/e2e/main.rs +++ b/tests/e2e/main.rs @@ -134,40 +134,32 @@ fn messages(output: Vec) -> Vec { messages } -#[test] -fn e2e() { - std::env::set_var("RUST_BACKTRACE", "full"); +struct SmokeArgs { + root: PathBuf, + init: String, + log: String, +} - let cwd = find_git_root().unwrap(); - if cfg!(target_os = "...") { - let w = Command::new("cargo") - .args(["build", "--release", "--bin", "tinymist"]) - .status(); - assert!(handle_io(w).success()); - handle_io(std::fs::copy( - cwd.join("target/release/tinymist.exe"), - cwd.join("editors/vscode/out/tinymist.exe"), - )); - } - let root = cwd.join("target/e2e/tinymist"); - gen(&root.join("vscode"), |srv| { - use lsp_types::notification::*; - use lsp_types::request::*; - use lsp_types::*; - let wp = root.join("vscode"); - let wp_url = lsp_types::Url::from_directory_path(&wp).unwrap(); - srv.request::(fixture("initialization/vscode", |v| { - v["rootUri"] = json!(wp_url); - v["rootPath"] = json!(wp); +fn gen_smoke(args: SmokeArgs) { + use lsp_types::notification::*; + use lsp_types::request::*; + use lsp_types::*; + + let SmokeArgs { root, init, log } = args; + gen(&root, |srv| { + let root_uri = lsp_types::Url::from_directory_path(&root).unwrap(); + srv.request::(fixture(&init, |v| { + v["rootUri"] = json!(root_uri); + v["rootPath"] = json!(root); v["workspaceFolders"] = json!([{ - "uri": wp_url, + "uri": root_uri, "name": "tinymist", }]); })); srv.notify::(json!({})); // open editions/base.log and readlines - let log = std::fs::read_to_string("tests/fixtures/editions/base.log").unwrap(); + let log = std::fs::read_to_string(&log).unwrap(); let log = log.trim().split('\n').collect::>(); let mut uri_set = HashSet::new(); let mut uris = Vec::new(); @@ -191,9 +183,12 @@ fn e2e() { } let uri_name = v["params"]["textDocument"]["uri"].as_str().unwrap(); - let url_v = wp_url.join(uri_name).unwrap(); - let uri = json!(url_v); - v["params"]["textDocument"]["uri"] = uri; + let url_v = if uri_name.starts_with("file:") || uri_name.starts_with("untitled:") { + lsp_types::Url::parse(uri_name).unwrap() + } else { + root_uri.join(uri_name).unwrap() + }; + v["params"]["textDocument"]["uri"] = json!(url_v); let method = v["method"].as_str().unwrap(); srv.notify_("textDocument/".to_owned() + method, v["params"].clone()); @@ -330,15 +325,12 @@ fn e2e() { } } }); +} - let tinymist_binary = if cfg!(windows) { - cwd.join("editors/vscode/out/tinymist.exe") - } else { - cwd.join("editors/vscode/out/tinymist") - }; +fn replay_log(tinymist_binary: &Path, root: &Path) -> String { let tinymist_binary = tinymist_binary.to_str().unwrap(); - let log_file = root.join("vscode/mirror.log").to_str().unwrap().to_owned(); + let log_file = root.join("mirror.log").to_str().unwrap().to_owned(); let mut res = messages(exec_output(tinymist_binary, ["lsp", "--replay", &log_file])); // retain not notification res.retain(|msg| matches!(msg, lsp_server::Message::Response(_))); @@ -351,15 +343,51 @@ fn e2e() { // print to result.log let res = serde_json::to_value(&res).unwrap(); let c = serde_json::to_string_pretty(&res).unwrap(); - std::fs::write(root.join("vscode/result.json"), c).unwrap(); + std::fs::write(root.join("result.json"), c).unwrap(); // let sorted_res let sorted_res = sort_and_redact_value(res); let c = serde_json::to_string_pretty(&sorted_res).unwrap(); let hash = reflexo::hash::hash128(&c); - std::fs::write(root.join("vscode/result_sorted.json"), c).unwrap(); - let hash = format!("siphash128_13:{:x}", hash); + std::fs::write(root.join("result_sorted.json"), c).unwrap(); - insta::assert_snapshot!(hash, @"siphash128_13:db9523369516f3a16997fc1913381d6e"); + format!("siphash128_13:{:x}", hash) +} + +#[test] +fn e2e() { + std::env::set_var("RUST_BACKTRACE", "full"); + + let cwd = find_git_root().unwrap(); + + let tinymist_binary = if cfg!(windows) { + cwd.join("editors/vscode/out/tinymist.exe") + } else { + cwd.join("editors/vscode/out/tinymist") + }; + + let root = cwd.join("target/e2e/tinymist"); + + { + gen_smoke(SmokeArgs { + root: root.join("neovim"), + init: "initialization/neovim-0.9.4".to_owned(), + log: "tests/fixtures/editions/neovim_unnamed_buffer.log".to_owned(), + }); + + let hash = replay_log(&tinymist_binary, &root.join("neovim")); + insta::assert_snapshot!(hash, @"siphash128_13:ff13445227b3b86b70905bba912bcd0a"); + } + + { + gen_smoke(SmokeArgs { + root: root.join("vscode"), + init: "initialization/vscode-1.87.2".to_owned(), + log: "tests/fixtures/editions/base.log".to_owned(), + }); + + let hash = replay_log(&tinymist_binary, &root.join("vscode")); + insta::assert_snapshot!(hash, @"siphash128_13:db9523369516f3a16997fc1913381d6e"); + } } struct StableHash<'a>(&'a Value); @@ -424,10 +452,19 @@ fn sort_and_redact_value(v: Value) -> Value { if k == "uri" || k == "targetUri" { // get uri and set as file name let uri = v.as_str().unwrap(); - let uri = lsp_types::Url::parse(uri).unwrap(); - let path = uri.to_file_path().unwrap(); - let path = path.file_name().unwrap().to_str().unwrap(); - Value::String(path.to_owned()) + if uri == "file://" || uri == "file:///" { + Value::String("".to_owned()) + } else { + let uri = lsp_types::Url::parse(uri).unwrap(); + + match uri.to_file_path() { + Ok(path) => { + let path = path.file_name().unwrap().to_str().unwrap(); + Value::String(path.to_owned()) + } + Err(_) => Value::String(uri.to_string()), + } + } } else { sort_and_redact_value(v.clone()) } diff --git a/tests/fixtures/editions/neovim_unnamed_buffer.log b/tests/fixtures/editions/neovim_unnamed_buffer.log new file mode 100644 index 00000000..f729d0dc --- /dev/null +++ b/tests/fixtures/editions/neovim_unnamed_buffer.log @@ -0,0 +1,21 @@ +{"method":"didOpen","params":{"textDocument":{"text":"\n","uri":"file://","version":0,"languageId":"typst"}}} +{"method":"didChange","params":{"contentChanges":[{"range":{"end":{"line":0,"character":0},"start":{"line":0,"character":0}},"rangeLength":0,"text":"#"}],"textDocument":{"version":3,"uri":"file://"}}} +{"method":"didChange","params":{"contentChanges":[{"range":{"end":{"line":0,"character":1},"start":{"line":0,"character":1}},"rangeLength":0,"text":"l"}],"textDocument":{"version":4,"uri":"file://"}}} +{"method":"didChange","params":{"contentChanges":[{"range":{"end":{"line":0,"character":2},"start":{"line":0,"character":2}},"rangeLength":0,"text":"e"},{"range":{"end":{"line":0,"character":3},"start":{"line":0,"character":3}},"rangeLength":0,"text":"t"}],"textDocument":{"version":6,"uri":"file://"}}} +{"method":"didChange","params":{"contentChanges":[{"range":{"end":{"line":0,"character":4},"start":{"line":0,"character":4}},"rangeLength":0,"text":" "}],"textDocument":{"version":7,"uri":"file://"}}} +{"method":"didChange","params":{"contentChanges":[{"range":{"end":{"line":0,"character":5},"start":{"line":0,"character":5}},"rangeLength":0,"text":"a"}],"textDocument":{"version":8,"uri":"file://"}}} +{"method":"didChange","params":{"contentChanges":[{"range":{"end":{"line":0,"character":6},"start":{"line":0,"character":6}},"rangeLength":0,"text":" "}],"textDocument":{"version":9,"uri":"file://"}}} +{"method":"didChange","params":{"contentChanges":[{"range":{"end":{"line":0,"character":7},"start":{"line":0,"character":7}},"rangeLength":0,"text":"="}],"textDocument":{"version":10,"uri":"file://"}}} +{"method":"didChange","params":{"contentChanges":[{"range":{"end":{"line":0,"character":8},"start":{"line":0,"character":8}},"rangeLength":0,"text":" "}],"textDocument":{"version":11,"uri":"file://"}}} +{"method":"didChange","params":{"contentChanges":[{"range":{"end":{"line":0,"character":9},"start":{"line":0,"character":9}},"rangeLength":0,"text":"5"}],"textDocument":{"version":12,"uri":"file://"}}} +{"method":"didChange","params":{"contentChanges":[{"range":{"end":{"line":0,"character":10},"start":{"line":0,"character":10}},"rangeLength":0,"text":"\n"}],"textDocument":{"version":13,"uri":"file://"}}} +{"method":"didChange","params":{"contentChanges":[{"range":{"end":{"line":1,"character":0},"start":{"line":1,"character":0}},"rangeLength":0,"text":"#"}],"textDocument":{"version":14,"uri":"file://"}}} +{"method":"didChange","params":{"contentChanges":[{"range":{"end":{"line":1,"character":1},"start":{"line":1,"character":1}},"rangeLength":0,"text":"l"}],"textDocument":{"version":15,"uri":"file://"}}} +{"method":"didChange","params":{"contentChanges":[{"range":{"end":{"line":1,"character":2},"start":{"line":1,"character":2}},"rangeLength":0,"text":"e"},{"range":{"end":{"line":1,"character":3},"start":{"line":1,"character":3}},"rangeLength":0,"text":"t"}],"textDocument":{"version":17,"uri":"file://"}}} +{"method":"didChange","params":{"contentChanges":[{"range":{"end":{"line":1,"character":4},"start":{"line":1,"character":4}},"rangeLength":0,"text":" "}],"textDocument":{"version":18,"uri":"file://"}}} +{"method":"didChange","params":{"contentChanges":[{"range":{"end":{"line":1,"character":5},"start":{"line":1,"character":5}},"rangeLength":0,"text":"f"},{"range":{"end":{"line":1,"character":6},"start":{"line":1,"character":6}},"rangeLength":0,"text":"u"}],"textDocument":{"version":20,"uri":"file://"}}} +{"method":"didChange","params":{"contentChanges":[{"range":{"end":{"line":1,"character":7},"start":{"line":1,"character":7}},"rangeLength":0,"text":"n"}],"textDocument":{"version":21,"uri":"file://"}}} +{"method":"didChange","params":{"contentChanges":[{"range":{"end":{"line":1,"character":8},"start":{"line":1,"character":8}},"rangeLength":0,"text":"c"}],"textDocument":{"version":22,"uri":"file://"}}} +{"method":"didChange","params":{"contentChanges":[{"range":{"end":{"line":1,"character":9},"start":{"line":1,"character":9}},"rangeLength":0,"text":"("}],"textDocument":{"version":23,"uri":"file://"}}} +{"method":"didChange","params":{"contentChanges":[{"range":{"end":{"line":1,"character":10},"start":{"line":1,"character":10}},"rangeLength":0,"text":")"}],"textDocument":{"version":24,"uri":"file://"}}} +{"method":"didChange","params":{"contentChanges":[{"range":{"end":{"line":1,"character":10},"start":{"line":1,"character":10}},"rangeLength":0,"text":"a"}],"textDocument":{"version":25,"uri":"file://"}}} \ No newline at end of file diff --git a/tests/fixtures/initialization/neovim-0.9.4.json b/tests/fixtures/initialization/neovim-0.9.4.json new file mode 100644 index 00000000..49b8eccb --- /dev/null +++ b/tests/fixtures/initialization/neovim-0.9.4.json @@ -0,0 +1,174 @@ +{ + "processId": 3414686, + "clientInfo": { "version": "0.9.4", "name": "Neovim" }, + "initializationOptions": {}, + "capabilities": { + "window": { + "showMessage": { + "messageActionItem": { "additionalPropertiesSupport": false } + }, + "workDoneProgress": true, + "showDocument": { "support": true } + }, + "workspace": { + "applyEdit": true, + "workspaceFolders": true, + "semanticTokens": { "refreshSupport": true }, + "didChangeWatchedFiles": { + "dynamicRegistration": true, + "relativePatternSupport": true + }, + "workspaceEdit": { + "resourceOperations": ["rename", "create", "delete"] + }, + "symbol": { + "dynamicRegistration": false, + "symbolKind": { + "valueSet": [ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 26 + ] + }, + "hierarchicalWorkspaceSymbolSupport": true + }, + "configuration": true + }, + "textDocument": { + "rename": { "prepareSupport": true, "dynamicRegistration": false }, + "documentHighlight": { "dynamicRegistration": false }, + "documentSymbol": { + "dynamicRegistration": false, + "symbolKind": { + "valueSet": [ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 26 + ] + }, + "hierarchicalDocumentSymbolSupport": true + }, + "foldingRange": { + "lineFoldingOnly": true, + "dynamicRegistration": false + }, + "semanticTokens": { + "formats": ["relative"], + "overlappingTokenSupport": true, + "requests": { "range": false, "full": { "delta": true } }, + "serverCancelSupport": false, + "augmentsSyntaxTokens": true, + "dynamicRegistration": false, + "tokenModifiers": [ + "declaration", + "definition", + "readonly", + "static", + "deprecated", + "abstract", + "async", + "modification", + "documentation", + "defaultLibrary" + ], + "tokenTypes": [ + "namespace", + "type", + "class", + "enum", + "interface", + "struct", + "typeParameter", + "parameter", + "variable", + "property", + "enumMember", + "event", + "function", + "method", + "macro", + "keyword", + "modifier", + "comment", + "string", + "number", + "regexp", + "operator", + "decorator" + ], + "multilineTokenSupport": false + }, + "publishDiagnostics": { + "relatedInformation": true, + "tagSupport": { "valueSet": [1, 2] } + }, + "codeAction": { + "dataSupport": true, + "dynamicRegistration": false, + "codeActionLiteralSupport": { + "codeActionKind": { + "valueSet": [ + "", + "quickfix", + "refactor", + "refactor.extract", + "refactor.inline", + "refactor.rewrite", + "source", + "source.organizeImports" + ] + } + }, + "resolveSupport": { "properties": ["edit"] }, + "isPreferredSupport": true + }, + "callHierarchy": { "dynamicRegistration": false }, + "completion": { + "completionItem": { + "resolveSupport": { + "properties": ["documentation", "detail", "additionalTextEdits"] + }, + "snippetSupport": true, + "commitCharactersSupport": true, + "preselectSupport": true, + "deprecatedSupport": true, + "documentationFormat": ["markdown", "plaintext"], + "tagSupport": { "valueSet": [1] }, + "insertReplaceSupport": true, + "labelDetailsSupport": true + }, + "dynamicRegistration": false, + "completionItemKind": { + "valueSet": [ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25 + ] + }, + "contextSupport": false + }, + "declaration": { "linkSupport": true }, + "definition": { "linkSupport": true }, + "signatureHelp": { + "signatureInformation": { + "documentationFormat": ["markdown", "plaintext"], + "parameterInformation": { "labelOffsetSupport": true }, + "activeParameterSupport": true + }, + "dynamicRegistration": false + }, + "synchronization": { + "didSave": true, + "dynamicRegistration": false, + "willSaveWaitUntil": true, + "willSave": true + }, + "hover": { + "contentFormat": ["markdown", "plaintext"], + "dynamicRegistration": false + }, + "typeDefinition": { "linkSupport": true }, + "references": { "dynamicRegistration": false }, + "implementation": { "linkSupport": true } + } + }, + "trace": "off", + "workspaceFolders": [] +} diff --git a/tests/fixtures/initialization/vscode.json b/tests/fixtures/initialization/vscode-1.87.2.json similarity index 100% rename from tests/fixtures/initialization/vscode.json rename to tests/fixtures/initialization/vscode-1.87.2.json