fix: empty url sent from neovim (#130)

* fix: empty url sent from neovim

* dev: move e2e build script

* dev: make stronger name for empty urls

* fix: work around empty url

* fix: convert empty link on unix system
This commit is contained in:
Myriad-Dreamin 2024-03-30 20:04:03 +08:00 committed by GitHub
parent b76e80bad3
commit edbb7bc1af
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 311 additions and 46 deletions

View file

@ -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<Url> = Lazy::new(|| Url::parse("file://").unwrap());
pub fn path_to_url(path: &Path) -> anyhow::Result<Url> {
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]

4
scripts/e2e.ps1 Normal file
View file

@ -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

3
scripts/e2e.sh Executable file
View file

@ -0,0 +1,3 @@
cargo build --release --bin tinymist
cp target/release/tinymist editors/vscode/out/tinymist
cargo insta test -p tests --accept

View file

@ -134,40 +134,32 @@ fn messages(output: Vec<u8>) -> Vec<lsp_server::Message> {
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::<Initialize>(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::<Initialize>(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::<Initialized>(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::<Vec<_>>();
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())
}

View file

@ -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://"}}}

View file

@ -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": []
}