use std::io::{BufRead, BufReader, Read, Write};
use std::path::{Path, PathBuf};
use std::process::{Child, ChildStdin, Command, Stdio};
use std::sync::mpsc::{Receiver, RecvTimeoutError, channel};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use serde_json::{Value, json};
const TIMEOUT: Duration = Duration::from_secs(30);
const STD_LIBRARY_TIMEOUT: Duration = Duration::from_secs(180);
const WIDGET_H: &str = "struct Widget { int size; };\n";
const MAIN_CPP: &str = "#include \"widget.h\"\n\nvoid f() {\n Widget w;\n w.size = 1;\n}\nstruct Unclosed { int x;\n";
#[test]
fn a_pulling_client_gets_a_report_and_not_null() {
let project = Project::new("pull-diagnostics");
project.write("widget.h", WIDGET_H);
project.write("main.cpp", MAIN_CPP);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
let initialized = server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"textDocument": { "diagnostic": { "dynamicRegistration": false } },
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
},
}),
);
assert_eq!(
initialized["result"]["capabilities"]["diagnosticProvider"]["interFileDependencies"],
json!(true),
"the server advertises the provider a pulling client looks for: {initialized}"
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": MAIN_CPP }
}),
);
let report = server.ask_until(100, |id| {
json!({
"id": id,
"method": "textDocument/diagnostic",
"params": { "textDocument": { "uri": main_uri } },
})
});
assert!(
!report["result"].is_null(),
"`null` is what a client cannot act on, and it is what this answered: {report}"
);
let items = report["result"]["items"].as_array().unwrap_or_else(|| {
panic!("a full document diagnostic report has an `items` array: {report}")
});
assert!(
!items.is_empty(),
"the file has an unclosed class, so a report with no items is \"clean\" and wrong: {report}"
);
assert!(
items.iter().any(|item| item["message"]
.as_str()
.is_some_and(|message| message.contains(';'))),
"and the parse error is the one the push test also expects: {report}"
);
let again = server.ask_until(200, |id| {
json!({
"id": id,
"method": "textDocument/diagnostic",
"params": { "textDocument": { "uri": main_uri } },
})
});
assert_eq!(
again["result"]["items"].as_array().map(Vec::len),
Some(items.len()),
"the same text has to give the same answer twice: first {report}, then {again}"
);
server.request(999, "shutdown", Value::Null);
server.notify("exit", Value::Null);
}
#[test]
fn a_client_can_open_a_file_and_get_diagnostics_a_definition_and_a_hover() {
let project = Project::new("handshake");
project.write("widget.h", WIDGET_H);
project.write("main.cpp", MAIN_CPP);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
let initialized = server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
},
}),
);
let capabilities = &initialized["result"]["capabilities"];
assert_eq!(capabilities["definitionProvider"], json!(true));
assert_eq!(capabilities["hoverProvider"], json!(true));
assert_eq!(
capabilities["diagnosticProvider"]["workspaceDiagnostics"],
json!(false),
"a file's parse errors are the whole answer, so there is nothing a workspace request would add"
);
assert!(
capabilities["textDocumentSync"].is_object(),
"the document lifecycle is what every other capability needs: {capabilities}"
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": MAIN_CPP }
}),
);
let reported = server.pull_diagnostics(90, &main_uri);
assert!(
!reported.is_empty(),
"the last class is never closed, so a parse error is expected: {reported:?}"
);
assert_eq!(
reported[0]["severity"],
json!(1),
"a parse error is an error, not a warning"
);
assert_eq!(
reported[0]["range"]["start"]["line"],
json!(6),
"the error is on the last line: {reported:?}"
);
assert!(
reported[0]["message"]
.as_str()
.is_some_and(|message| message.contains(';')),
"and it is the parser's own message about the missing `;`: {reported:?}"
);
let location = server.ask_until(100, |id| {
json!({
"id": id,
"method": "textDocument/definition",
"params": {
"textDocument": { "uri": main_uri },
"position": { "line": 4, "character": 6 }, },
})
});
let found_uri = location["result"]["uri"]
.as_str()
.unwrap_or_else(|| panic!("a definition was expected, got {location}"));
assert!(
found_uri.ends_with("widget.h"),
"`size` is declared in the header: {location}"
);
assert_eq!(
location["result"]["range"]["start"]["line"],
json!(0),
"and the range is the name in that file, not the top of it: {location}"
);
assert_eq!(location["result"]["range"]["start"]["character"], json!(20));
let header = server.ask_until(150, |id| {
json!({
"id": id,
"method": "textDocument/definition",
"params": {
"textDocument": { "uri": main_uri },
"position": { "line": 0, "character": 12 }, },
})
});
let header_uri = header["result"]["uri"]
.as_str()
.unwrap_or_else(|| panic!("a header was expected, got {header}"));
assert!(
header_uri.ends_with("widget.h"),
"the include names widget.h: {header}"
);
assert_eq!(
header["result"]["range"]["start"],
json!({ "line": 0, "character": 0 }),
"from the top of the header: {header}"
);
assert_eq!(
header["result"]["range"]["end"],
json!({ "line": 1, "character": 0 }),
"to the start of the second line, which is where the header's first line ends: {header}"
);
let about_the_header = server.ask_until(170, |id| {
json!({
"id": id,
"method": "textDocument/hover",
"params": {
"textDocument": { "uri": main_uri },
"position": { "line": 0, "character": 12 },
},
})
});
let markdown = about_the_header["result"]["contents"]["value"]
.as_str()
.unwrap_or_else(|| panic!("a hover was expected, got {about_the_header}"));
assert!(
markdown.contains("#include \"widget.h\""),
"the line as the file writes it: {markdown}"
);
let resolved = markdown.to_lowercase();
assert!(
resolved.contains("widget.h") && resolved.contains("cppls-handshake-tests"),
"and where it resolved to: {markdown}"
);
let on_the_hash = server.request(
160,
"textDocument/definition",
json!({
"textDocument": { "uri": main_uri },
"position": { "line": 0, "character": 0 },
}),
);
assert_eq!(
on_the_hash["result"],
Value::Null,
"the `#` is not the header's name: {on_the_hash}"
);
let hover = server.ask_until(200, |id| {
json!({
"id": id,
"method": "textDocument/hover",
"params": {
"textDocument": { "uri": main_uri },
"position": { "line": 3, "character": 6 }, },
})
});
let markdown = hover["result"]["contents"]["value"]
.as_str()
.unwrap_or_else(|| panic!("a hover was expected, got {hover}"));
assert!(
markdown.contains("struct Widget"),
"the declaration as the file writes it: {markdown}"
);
assert!(markdown.contains("widget.h"), "{markdown}");
let member = server.ask_until(300, |id| {
json!({
"id": id,
"method": "textDocument/hover",
"params": {
"textDocument": { "uri": main_uri },
"position": { "line": 4, "character": 7 }, },
})
});
let markdown = member["result"]["contents"]["value"]
.as_str()
.unwrap_or_else(|| panic!("a hover for the member was expected, got {member}"));
assert!(markdown.contains("size"), "{markdown}");
assert!(markdown.contains("int"), "its type is shown: {markdown}");
let nothing = server.request(
900,
"textDocument/hover",
json!({
"textDocument": { "uri": main_uri },
"position": { "line": 1, "character": 0 }, }),
);
assert_eq!(
nothing["result"],
Value::Null,
"there is nothing at the position, and that is what the client is told"
);
let shutdown = server.request(901, "shutdown", Value::Null);
assert_eq!(shutdown["result"], Value::Null);
server.notify("exit", Value::Null);
assert!(
server.wait_for_exit(),
"the server exits when the client says exit"
);
}
#[test]
fn a_shutdown_ends_the_process_while_the_client_keeps_the_pipe() {
let project = Project::new("shutdown-without-exit");
project.write("main.cpp", "int main() { return 0; }\n");
let mut server = Server::start(project.root());
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
},
}),
);
server.notify("initialized", json!({}));
server.pull_diagnostics(90, &uri_of(&project.root().join("main.cpp")));
let shutdown = server.request(901, "shutdown", Value::Null);
assert_eq!(shutdown["result"], Value::Null);
assert!(
server.wait_for_exit(),
"the process must end after shutdown, not wait for an exit notification"
);
}
#[test]
fn a_formatting_request_returns_edits_for_the_whole_file() {
let project = Project::new("formatting");
project.write("main.cpp", "int x = 1;\n int y = 2;\n");
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
let initialized = server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": { "textDocument": { "formatting": { "dynamicRegistration": false } } },
}),
);
let capabilities = &initialized["result"]["capabilities"];
assert_eq!(
capabilities["documentFormattingProvider"],
json!(true),
"the server advertises whole-file formatting: {capabilities}"
);
assert_eq!(capabilities["documentRangeFormattingProvider"], json!(true));
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": {
"uri": main_uri,
"languageId": "cpp",
"version": 1,
"text": "int x = 1;\n int y = 2;\n",
}
}),
);
let response = server.request(
2,
"textDocument/formatting",
json!({
"textDocument": { "uri": main_uri },
"options": { "tabSize": 4, "insertSpaces": true },
}),
);
let edits = response["result"]
.as_array()
.unwrap_or_else(|| panic!("edits were expected, got {response}"));
assert!(
!edits.is_empty(),
"the file is not formatted, so there is work: {response}"
);
let text = edits
.iter()
.filter_map(|edit| edit["newText"].as_str())
.collect::<String>();
assert!(
text.contains("int x = 1;"),
"the double space between `int` and `x` is closed up: {response}"
);
assert!(
!text.contains(" int y"),
"and the three spaces before `int y` are gone: {response}"
);
let response = server.request(
3,
"textDocument/rangeFormatting",
json!({
"textDocument": { "uri": main_uri },
"range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 5 } },
"options": { "tabSize": 4, "insertSpaces": true },
}),
);
let ranged = response["result"]
.as_array()
.unwrap_or_else(|| panic!("a range answer is a list, possibly empty: {response}"));
assert!(
ranged.len() <= edits.len(),
"a range never returns more than the whole file did: {response}"
);
let ranged_text = ranged
.iter()
.filter_map(|edit| edit["newText"].as_str())
.collect::<String>();
assert!(
!ranged_text.contains("int y"),
"the range formatted only line 0: {ranged_text:?}"
);
let response = server.request(
5,
"textDocument/willSaveWaitUntil",
json!({
"textDocument": { "uri": main_uri },
"reason": 1,
}),
);
let edits = response["result"]
.as_array()
.unwrap_or_else(|| panic!("the save-time edits are a list: {response}"));
assert!(
!edits.is_empty(),
"the file is still unformatted, so saving must offer the edits: {response}"
);
}
#[test]
fn a_project_configuration_reaches_the_servers_behaviour() {
let project = Project::new("configured");
project.write("widget.h", WIDGET_H);
project.write("main.cpp", MAIN_CPP);
project.write(".cppls.toml", "[hover]\nenable = false\n");
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true },
"window": { "workDoneProgress": true },
},
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": MAIN_CPP }
}),
);
let hover = server.request(
2,
"textDocument/hover",
json!({
"textDocument": { "uri": main_uri },
"position": { "line": 3, "character": 6 },
}),
);
assert_eq!(
hover["result"],
Value::Null,
"the project turned the popup off, and the server read the project's file to find that out: {hover}"
);
let shutdown = server.request(3, "shutdown", Value::Null);
assert_eq!(shutdown["result"], Value::Null);
server.notify("exit", Value::Null);
assert!(server.wait_for_exit());
}
struct Project {
root: PathBuf,
}
impl Project {
fn new(name: &str) -> Project {
let root = std::env::temp_dir()
.join("cppls-handshake-tests")
.join(name);
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).expect("the fixture directory is created");
Project { root }
}
fn root(&self) -> &Path {
&self.root
}
fn write(&self, name: &str, text: &str) {
std::fs::write(self.root.join(name), text).expect("the fixture writes");
}
}
impl Drop for Project {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.root);
}
}
fn uri_of(path: &Path) -> String {
let text = path
.to_string_lossy()
.replace('\\', "/")
.replace(' ', "%20");
format!("file:///{}", text.trim_start_matches('/'))
}
fn position_of(source: &str, needle: &str) -> (u32, u32) {
let at = source
.find(needle)
.unwrap_or_else(|| panic!("{needle:?} is not in the fixture"))
+ needle.len();
let before = &source[..at];
let line = before.matches('\n').count();
let character = before
.rsplit('\n')
.next()
.expect("rsplit always yields one piece")
.chars()
.count();
(line as u32, character as u32)
}
struct Server {
child: Child,
stdin: ChildStdin,
messages: Receiver<Value>,
log: Arc<Mutex<String>>,
}
impl Server {
fn start(root: &Path) -> Server {
let mut command = Command::new(env!("CARGO_BIN_EXE_cpp_ls"));
command
.arg("--log-path")
.arg(std::env::var("CPPLS_TEST_LOG_PATH").unwrap_or_else(|_| "none".to_string()));
if let Ok(level) = std::env::var("CPPLS_TEST_LOG_LEVEL") {
command.arg("--log-level").arg(level);
}
let mut child = command
.current_dir(root)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("the server starts");
let stdin = child.stdin.take().expect("stdin is piped");
let stdout = child.stdout.take().expect("stdout is piped");
let stderr = child.stderr.take().expect("stderr is piped");
let (tx, messages) = channel();
std::thread::spawn(move || read_messages(stdout, tx));
let log = Arc::new(Mutex::new(String::new()));
let sink = log.clone();
std::thread::spawn(move || {
let mut reader = BufReader::new(stderr);
let mut line = String::new();
while reader.read_line(&mut line).unwrap_or(0) > 0 {
sink.lock()
.expect("the log is not poisoned")
.push_str(&line);
line.clear();
}
});
Server {
child,
stdin,
messages,
log,
}
}
fn send(&mut self, message: Value) {
let body = serde_json::to_string(&message).expect("the message serializes");
write!(self.stdin, "Content-Length: {}\r\n\r\n{body}", body.len())
.expect("the message is written");
self.stdin.flush().expect("the message is flushed");
}
fn notify(&mut self, method: &str, params: Value) {
self.send(json!({ "jsonrpc": "2.0", "method": method, "params": params }));
}
fn request(&mut self, id: i64, method: &str, params: Value) -> Value {
self.send(json!({ "jsonrpc": "2.0", "id": id, "method": method, "params": params }));
self.wait_for(|message| message["id"] == json!(id))
}
fn ask_until(&mut self, first_id: i64, build: impl Fn(i64) -> Value) -> Value {
let deadline = Instant::now() + TIMEOUT;
let mut id = first_id;
loop {
let request = build(id);
let method = request["method"]
.as_str()
.expect("the closure builds a request")
.to_string();
let response = self.request(id, &method, request["params"].clone());
if let Some(error) = response.get("error") {
panic!("the server answered with an error: {error}");
}
if !response["result"].is_null() {
return response;
}
if Instant::now() >= deadline {
panic!("no answer within {TIMEOUT:?}, only `null`: {response}");
}
id += 1;
std::thread::sleep(Duration::from_millis(50));
}
}
fn pull_diagnostics(&mut self, first_id: i64, uri: &str) -> Vec<Value> {
let report = self.ask_until(first_id, |id| {
json!({
"id": id,
"method": "textDocument/diagnostic",
"params": { "textDocument": { "uri": uri } },
})
});
report["result"]["items"]
.as_array()
.unwrap_or_else(|| {
panic!("a pull request is answered with a report, not `null`: {report}")
})
.clone()
}
fn ask_until_it(
&mut self,
first_id: i64,
build: impl Fn(i64) -> Value,
accept: impl Fn(&Value) -> bool,
) -> Value {
self.ask_until_it_within(first_id, TIMEOUT, build, accept)
}
fn ask_until_it_within(
&mut self,
first_id: i64,
within: Duration,
build: impl Fn(i64) -> Value,
accept: impl Fn(&Value) -> bool,
) -> Value {
let deadline = Instant::now() + within;
let mut id = first_id;
loop {
let request = build(id);
let method = request["method"]
.as_str()
.expect("the closure builds a request")
.to_string();
let response = self.request(id, &method, request["params"].clone());
if let Some(error) = response.get("error") {
panic!("the server answered with an error: {error}");
}
if accept(&response) {
return response;
}
if Instant::now() >= deadline {
panic!(
"no answer the test accepts within {within:?}; the last one was: {response}"
);
}
id += 1;
std::thread::sleep(Duration::from_millis(50));
}
}
fn wait_for(&mut self, accept: impl Fn(&Value) -> bool) -> Value {
let deadline = Instant::now() + TIMEOUT;
loop {
let remaining = deadline
.checked_duration_since(Instant::now())
.unwrap_or_else(|| panic!("the server did not answer within {TIMEOUT:?}"));
let message = match self.messages.recv_timeout(remaining) {
Ok(message) => message,
Err(RecvTimeoutError::Timeout) => {
panic!("the server did not answer within {TIMEOUT:?}")
}
Err(RecvTimeoutError::Disconnected) => panic!("the server closed its stdout"),
};
if message.get("method").is_some() && message.get("id").is_some() {
self.answer(&message);
continue;
}
if accept(&message) {
return message;
}
}
}
fn answer(&mut self, request: &Value) {
let id = request["id"].clone();
let method = request["method"].as_str().unwrap_or_default();
let result = match method {
"workspace/configuration" => json!([Value::Null]),
_ => Value::Null,
};
self.send(json!({ "jsonrpc": "2.0", "id": id, "result": result }));
}
fn wait_for_exit(&mut self) -> bool {
let deadline = Instant::now() + Duration::from_secs(10);
while Instant::now() < deadline {
match self.child.try_wait() {
Ok(Some(_)) => return true,
Ok(None) => std::thread::sleep(Duration::from_millis(20)),
Err(_) => return false,
}
}
false
}
}
impl Drop for Server {
fn drop(&mut self) {
let log = self.log.lock().expect("the log is not poisoned");
if !log.trim().is_empty() {
eprintln!("--- the server logged:\n{log}");
}
drop(log);
let _ = self.child.kill();
let _ = self.child.wait();
}
}
fn read_messages(stdout: impl Read, tx: std::sync::mpsc::Sender<Value>) {
let mut reader = BufReader::new(stdout);
loop {
let mut length = None;
loop {
let mut header = String::new();
match reader.read_line(&mut header) {
Ok(0) => return,
Ok(_) => {}
Err(_) => return,
}
let header = header.trim_end();
if header.is_empty() {
break;
}
if let Some(value) = header.strip_prefix("Content-Length: ") {
length = value.parse::<usize>().ok();
}
}
let Some(length) = length else {
return;
};
let mut body = vec![0u8; length];
if reader.read_exact(&mut body).is_err() {
return;
}
let Ok(message) = serde_json::from_slice::<Value>(&body) else {
return;
};
if tx.send(message).is_err() {
return;
}
}
}
const HANDLE_H: &str = "#define DECLARE_HANDLE(name) struct name##__ { int unused; }; \
typedef struct name##__ *name\n";
const API_H: &str = "#include \"handle.h\"\nDECLARE_HANDLE(HWND);\n";
const API_CPP: &str = "#include \"api.h\"\nHWND h;\n";
#[test]
fn a_declaration_a_macro_makes_is_found_over_the_wire() {
let project = Project::new("macro-declared");
project.write("handle.h", HANDLE_H);
project.write("api.h", API_H);
project.write("main.cpp", API_CPP);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
},
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": API_CPP }
}),
);
let location = server.ask_until(100, |id| {
json!({
"id": id,
"method": "textDocument/definition",
"params": {
"textDocument": { "uri": main_uri },
"position": { "line": 1, "character": 1 },
},
})
});
let found = &location["result"];
let found_uri = found["uri"]
.as_str()
.unwrap_or_else(|| panic!("a definition was expected, got {location}"));
assert!(
found_uri.ends_with("api.h"),
"the declaration the macro made belongs to the file that invoked it, which nobody opened: {location}"
);
assert_eq!(
found["range"]["start"]["line"],
json!(1),
"and it is reported at the invocation: {location}"
);
server.request(999, "shutdown", Value::Null);
server.notify("exit", Value::Null);
}
const BRANCH_CPP: &str = "#include \"cfg.h\"\n#if OFF\nstruct Unclosed { int x;\n#endif\nint ok;\n";
const CFG_H: &str = "#define OFF 0\n";
#[test]
fn a_branch_nobody_takes_is_answered_as_clean() {
let project = Project::new("cooked-diagnostics");
project.write("cfg.h", CFG_H);
project.write("main.cpp", BRANCH_CPP);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
},
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": BRANCH_CPP }
}),
);
let clean = server.pull_diagnostics(90, &main_uri);
assert!(
clean.is_empty(),
"the branch is not compiled, so there is nothing to report: {clean:?}"
);
server.request(999, "shutdown", Value::Null);
server.notify("exit", Value::Null);
}
#[test]
fn an_error_typed_into_an_open_file_is_answered() {
let project = Project::new("diagnostics-after-an-edit");
project.write("main.cpp", CLEAN_CPP);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
},
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": CLEAN_CPP }
}),
);
server.notify(
"textDocument/didChange",
json!({
"textDocument": { "uri": main_uri, "version": 2 },
"contentChanges": [{ "text": EDITED_CPP }],
}),
);
let diagnostics = server.pull_diagnostics(300, &main_uri);
assert!(
diagnostics.iter().any(|diagnostic| diagnostic["message"]
.as_str()
.is_some_and(|message| message.contains(';'))),
"the missing `;` is the only difference from the clean file: {diagnostics:?}"
);
server.request(999, "shutdown", Value::Null);
server.notify("exit", Value::Null);
}
#[test]
fn a_diagnostic_about_deleted_text_is_gone_by_the_next_pull() {
let project = Project::new("diagnostics-clear-after-a-fix");
project.write("main.cpp", CLEAN_CPP);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
},
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": CLEAN_CPP }
}),
);
server.pull_diagnostics(100, &main_uri);
server.notify(
"textDocument/didChange",
json!({
"textDocument": { "uri": main_uri, "version": 2 },
"contentChanges": [{ "text": EDITED_CPP }],
}),
);
let broken = server.pull_diagnostics(200, &main_uri);
assert!(
broken
.iter()
.any(|diagnostic| diagnostic["message"].is_string()),
"the missing `;` is found: {broken:?}"
);
server.notify(
"textDocument/didChange",
json!({
"textDocument": { "uri": main_uri, "version": 3 },
"contentChanges": [{ "text": CLEAN_CPP }],
}),
);
let fixed = server.pull_diagnostics(300, &main_uri);
assert!(
fixed.is_empty(),
"the file is byte-for-byte the one that was clean before the edit, so nothing may be reported: {fixed:?}"
);
server.request(999, "shutdown", Value::Null);
server.notify("exit", Value::Null);
}
#[test]
fn a_finding_about_deleted_text_does_not_survive_a_busy_index() {
let project = Project::new("diagnostics-while-busy");
for index in 0..80 {
let next = if index + 1 < 80 {
format!("#include \"h{}.h\"\n", index + 1)
} else {
String::new()
};
project.write(
&format!("h{index}.h"),
&format!("#pragma once\n{next}struct S{index} {{ int f; }};\n"),
);
}
let clean = "#include \"h0.h\"\nint main() { return 0; }\n";
let broken = "#include \"h0.h\"\nint main() { return 0 }\n";
project.write("main.cpp", clean);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
},
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": clean }
}),
);
server.pull_diagnostics(100, &main_uri);
server.notify(
"textDocument/didChange",
json!({
"textDocument": { "uri": main_uri, "version": 2 },
"contentChanges": [{ "text": broken }],
}),
);
let found = server.pull_diagnostics(200, &main_uri);
assert!(
found
.iter()
.any(|diagnostic| diagnostic["message"].is_string()),
"the missing `;` is a parse error: {found:?}"
);
server.notify(
"textDocument/didChange",
json!({
"textDocument": { "uri": main_uri, "version": 3 },
"contentChanges": [{ "text": clean }],
}),
);
let fixed = server.pull_diagnostics(300, &main_uri);
assert!(
fixed.is_empty(),
"the `;` is back and the file is the one that was clean before the edit, so nothing may be reported: \
{fixed:?}"
);
server.request(999, "shutdown", Value::Null);
server.notify("exit", Value::Null);
}
const CLEAN_CPP: &str = "int g();\nstruct Name {\n int middleName;\n void print(int i) {\n \
middleName = i;\n }\n};\n";
const EDITED_CPP: &str = "int g();\nstruct Name {\n int middleName;\n void print(int i) {\n \
middleName = i\n }\n};\n";
#[test]
fn the_reported_file_is_answered() {
let project = Project::new("reported-file");
project.write("main.cpp", REPORTED_CPP);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
},
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": REPORTED_CPP }
}),
);
let diagnostics = server.pull_diagnostics(90, &main_uri);
eprintln!(
"the server answered with {} diagnostic(s): {diagnostics:?}",
diagnostics.len()
);
assert!(
diagnostics
.iter()
.all(|diagnostic| diagnostic["code"] != json!("syntax-error")),
"the file parses, and `return` reporting a missing `;` is the defect this fixture was built from: \
{diagnostics:?}"
);
server.request(999, "shutdown", Value::Null);
server.notify("exit", Value::Null);
}
const REPORTED_CPP: &str = r#"#include <iostream>
#include <string>
#include <cstdio>
int main() {
int a = 1, b = 2;
int sum = a + b;
std::cout << "The sum of " << a << " and " << b << " is: " << sum << std::endl;
std::string full;
std::cout << "Full name: " << full << std::endl;
std::cout << "hello world " << std::endl;
full.size();
full.length();
full.size();
printf("Full name Length: %zu\n", full.size());
// printf("Full name Length using length(): %zu\n", full.length());
return 0;
}
"#;
const NAMESPACE_H: &str = "#define BEGIN_NS namespace one {\n#define END_NS }\n";
const NAMESPACED_WIDGET_H: &str =
"#include \"namespace.h\"\nBEGIN_NS struct Widget { int size; }; END_NS\n";
const HWND_CPP: &str = "#include \"api.h\"\nvoid f() {\n HWND\n}\n";
const MEMBER_CPP: &str = "#include \"widget.h\"\nvoid f() {\n one::Widget w;\n w.\n}\n";
#[test]
fn a_name_only_the_cooked_reading_declares_is_offered_over_the_wire() {
let project = Project::new("completion-macro-declared");
project.write("handle.h", HANDLE_H);
project.write("api.h", API_H);
project.write("main.cpp", HWND_CPP);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
let capabilities = server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
},
}),
);
assert!(
capabilities["result"]["capabilities"]["completionProvider"].is_object(),
"the client is told the server completes: {capabilities}"
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": HWND_CPP }
}),
);
let answer = server.ask_until_it(
100,
|id| {
json!({
"id": id,
"method": "textDocument/completion",
"params": {
"textDocument": { "uri": main_uri },
"position": { "line": 2, "character": 8 },
},
})
},
|response| {
response["result"]["items"]
.as_array()
.is_some_and(|items| items.iter().any(|item| item["label"] == json!("HWND__")))
},
);
let items = answer["result"]["items"]
.as_array()
.unwrap_or_else(|| panic!("a completion list was expected, got {answer}"));
assert!(
items.iter().any(|item| item["label"] == json!("HWND")),
"and the typedef the same macro made is offered beside it: {answer}"
);
let generated = items
.iter()
.find(|item| item["label"] == json!("HWND__"))
.expect("the closure accepted this");
assert_eq!(generated["kind"], json!(7), "a class: {generated}");
assert_eq!(
generated["textEdit"]["range"]["start"],
json!({ "line": 2, "character": 4 }),
"the edit replaces the half-written name from where it starts: {generated}"
);
assert_eq!(
generated["textEdit"]["range"]["end"],
json!({ "line": 2, "character": 8 }),
"…to where the cursor is: {generated}"
);
assert_eq!(
generated["textEdit"]["newText"],
json!("HWND__"),
"{generated}"
);
server.request(999, "shutdown", Value::Null);
server.notify("exit", Value::Null);
}
const STD_STRING_CPP: &str = "\
#include <string>
int main() {
std::string full;
full.
}
";
#[test]
fn a_members_members_are_offered_over_the_wire() {
let project = Project::new("completion-member");
project.write("namespace.h", NAMESPACE_H);
project.write("widget.h", NAMESPACED_WIDGET_H);
project.write("main.cpp", MEMBER_CPP);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
},
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": MEMBER_CPP }
}),
);
let answer = server.ask_until_it(
100,
|id| {
json!({
"id": id,
"method": "textDocument/completion",
"params": {
"textDocument": { "uri": main_uri },
"position": { "line": 3, "character": 6 },
},
})
},
|response| {
response["result"]["items"]
.as_array()
.is_some_and(|items| items.iter().any(|item| item["label"] == json!("size")))
},
);
let items = answer["result"]["items"]
.as_array()
.unwrap_or_else(|| panic!("a completion list was expected, got {answer}"));
let size = items
.iter()
.find(|item| item["label"] == json!("size"))
.unwrap_or_else(|| panic!("`Widget` has a `size` member: {answer}"));
assert_eq!(
size["kind"],
json!(5),
"a **field** rather than a variable, because it is reached through an object — the completion layer \
distinguishes the two positions, and a client draws them with different icons: {size}"
);
assert_eq!(
size["detail"],
Value::Null,
"the detail is left for `completionItem/resolve`: {size}"
);
let resolved = server.request(4, "completionItem/resolve", size.clone());
assert_eq!(
resolved["result"]["detail"],
json!("int"),
"and resolve supplies the type the declaration was written with: {resolved}"
);
assert_eq!(
resolved["result"]["labelDetails"]["detail"],
json!(" int"),
"as the label's second half too, which is where 3.17 draws it: {resolved}"
);
assert_eq!(
size["textEdit"]["range"]["start"],
json!({ "line": 3, "character": 6 }),
"the edit is an empty range just past the dot: {size}"
);
server.request(999, "shutdown", Value::Null);
server.notify("exit", Value::Null);
}
#[test]
#[ignore = "needs the release profile: the standard library closure takes minutes to index in debug"]
fn a_standard_library_member_list_has_each_spelling_once() {
let project = Project::new("completion-std-string");
project.write("main.cpp", STD_STRING_CPP);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
},
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": STD_STRING_CPP }
}),
);
let (line, character) = position_of(STD_STRING_CPP, "full.");
assert_eq!(
(line, character),
(3, 9),
"the operator ends the fourth line and the cursor is just past it — written out here once, so that a \
fixture edited above this line fails loudly instead of silently asking about the wrong place"
);
let answer = server.ask_until_it_within(
100,
STD_LIBRARY_TIMEOUT,
|id| {
json!({
"id": id,
"method": "textDocument/completion",
"params": {
"textDocument": { "uri": main_uri },
"position": { "line": line, "character": character },
},
})
},
|response| {
response["result"]["items"]
.as_array()
.is_some_and(|items| items.iter().any(|item| item["label"] == json!("append")))
},
);
let items = answer["result"]["items"]
.as_array()
.unwrap_or_else(|| panic!("a completion list was expected, got {answer}"));
let labels: Vec<&str> = items
.iter()
.filter_map(|item| item["label"].as_str())
.collect();
assert!(
labels.contains(&"append") && labels.contains(&"size") && labels.contains(&"substr"),
"the members of the standard library's string: {labels:?}"
);
assert!(
!labels.contains(&"printf") && !labels.contains(&"main"),
"and not the names in scope, which cannot follow the operator: {labels:?}"
);
let mut unique = labels.clone();
unique.sort_unstable();
unique.dedup();
assert_eq!(
unique.len(),
labels.len(),
"every spelling appears once — {} of {} rows are repeats: {:?}",
labels.len() - unique.len(),
labels.len(),
labels
.iter()
.filter(|label| labels.iter().filter(|other| other == label).count() > 1)
.collect::<Vec<_>>()
);
let append = items
.iter()
.find(|item| item["label"] == json!("append"))
.expect("asserted above");
assert_eq!(
append["textEdit"]["range"]["start"],
json!({ "line": line, "character": character }),
"just past the dot: {append}"
);
assert_eq!(append["textEdit"]["newText"], json!("append"), "{append}");
server.request(999, "shutdown", Value::Null);
server.notify("exit", Value::Null);
}
const FRESH_TYPE_H: &str = "struct Widget { int size; };\n";
const FRESH_TYPE_CPP: &str = "\
#include \"widget.h\"
int main() {
Widget w;
w.
}
";
#[test]
fn a_completion_sees_a_change_to_an_included_header() {
let project = Project::new("completion-after-header-change");
project.write("widget.h", FRESH_TYPE_H);
project.write("main.cpp", FRESH_TYPE_CPP);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
let header_uri = uri_of(&project.root().join("widget.h"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
},
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": FRESH_TYPE_CPP }
}),
);
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": header_uri, "languageId": "cpp", "version": 1, "text": FRESH_TYPE_H }
}),
);
let (line, character) = position_of(FRESH_TYPE_CPP, "w.");
server.ask_until_it(
100,
|id| {
json!({
"id": id,
"method": "textDocument/completion",
"params": {
"textDocument": { "uri": main_uri },
"position": { "line": line, "character": character },
},
})
},
|response| {
response["result"]["items"]
.as_array()
.is_some_and(|items| items.iter().any(|item| item["label"] == json!("size")))
},
);
server.notify(
"textDocument/didChange",
json!({
"textDocument": { "uri": header_uri, "version": 2 },
"contentChanges": [{ "text": "struct Widget { int size; int added_by_the_edit; };\n" }],
}),
);
let answer = server.request(
200,
"textDocument/completion",
json!({
"textDocument": { "uri": main_uri },
"position": { "line": line, "character": character },
}),
);
let items = answer["result"]["items"]
.as_array()
.unwrap_or_else(|| panic!("a completion list was expected, got {answer}"));
let labels: Vec<&str> = items
.iter()
.filter_map(|item| item["label"].as_str())
.collect();
assert!(
labels.contains(&"added_by_the_edit"),
"the member the edit added, which only a rebuilt summary can express — asked once, right after the change, \
with no retry to hide a stale answer: {labels:?}"
);
assert!(
!labels.contains(&"main"),
"and not the names in scope, which is what a stale summary produces: {labels:?}"
);
server.request(999, "shutdown", Value::Null);
server.notify("exit", Value::Null);
}
#[test]
#[ignore = "needs the release profile: the answer comes from the standard library's include closure (156 files)"]
fn a_member_access_on_the_last_line_of_a_body_offers_the_members() {
let project = Project::new("completion-last-line");
project.write("main.cpp", LAST_LINE_CPP);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
},
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": LAST_LINE_CPP }
}),
);
let (line, ends_at) = position_of(LAST_LINE_CPP, "full2.");
for character in [ends_at, ends_at - 1] {
let answer = server.ask_until_it_within(
100,
STD_LIBRARY_TIMEOUT,
|id| {
json!({
"id": id,
"method": "textDocument/completion",
"params": {
"textDocument": { "uri": main_uri },
"position": { "line": line, "character": character },
},
})
},
|response| {
response["result"]["items"]
.as_array()
.is_some_and(|items| items.iter().any(|item| item["label"] == json!("append")))
},
);
let items = answer["result"]["items"]
.as_array()
.unwrap_or_else(|| panic!("a completion list was expected, got {answer}"));
let labels: Vec<&str> = items
.iter()
.filter_map(|item| item["label"].as_str())
.collect();
assert!(
labels.contains(&"append") && labels.contains(&"size"),
"the members of the standard library's string, at line {line} character {character}: {labels:?}"
);
assert!(
!labels.contains(&"full2") && !labels.contains(&"myName"),
"and not the names in scope, at line {line} character {character}: {labels:?}"
);
}
server.request(999, "shutdown", Value::Null);
server.notify("exit", Value::Null);
}
const LAST_LINE_CPP: &str = "\
#include <iostream>
#include <string>
struct Name {
std::string firstName;
std::string lastName;
std::string getFullName() const { return firstName + lastName; }
};
int main() {
Name myName;
std::string full2;
full2.
}
";
#[test]
fn a_completion_right_after_a_change_sees_the_change() {
const LINES: usize = 1200;
const ROUNDS: usize = 24;
let text = |round: usize, dot: bool| {
let mut text =
String::from("struct Widget { int size; int weight; };\nvoid f() {\n Widget w;\n");
for index in 0..LINES {
text.push_str(&format!(" w.size = {};\n", index + round));
}
text.push_str(if dot { " w.\n}\n" } else { " \n}\n" });
text
};
let project = Project::new("completion-after-change");
project.write("main.cpp", &text(0, false));
let mut server = Server::start(project.root());
let uri = uri_of(&project.root().join("main.cpp"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
},
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": uri, "languageId": "cpp", "version": 1, "text": text(0, false) }
}),
);
let sent = Instant::now();
for round in 1..=ROUNDS {
server.notify(
"textDocument/didChange",
json!({
"textDocument": { "uri": uri, "version": round + 1 },
"contentChanges": [{ "text": text(round, round == ROUNDS) }],
}),
);
}
let burst_written = Instant::now();
let answer = server.request(
100,
"textDocument/completion",
json!({
"textDocument": { "uri": uri },
"position": { "line": 3 + LINES, "character": 6 },
}),
);
let served = Instant::now();
println!(
"a {ROUNDS}-change burst sent in {:?}, then a completion answered {:?} later",
burst_written.elapsed(),
served - sent
);
let items = answer["result"]["items"]
.as_array()
.unwrap_or_else(|| panic!("`w.` in the changed text must complete, got {answer}"));
assert!(
items.iter().any(|item| item["label"] == json!("weight")),
"and the list is the *changed* class's members — `weight` is the member the change added: {answer}"
);
server.request(999, "shutdown", Value::Null);
server.notify("exit", Value::Null);
}
const LIMITS_H: &str = "#define MAX_ITEMS 64\n";
const ITEMS_H: &str = "#include \"limits.h\"\nint items[MAX_ITEMS];\n";
const ITEMS_CPP: &str = "#include \"api.h\"\nvoid f() {\n int more[MAX_ITEMS];\n}\n";
#[test]
fn a_macro_is_renamed_everywhere_and_an_ordinary_name_is_refused() {
let project = Project::new("rename");
project.write("limits.h", LIMITS_H);
project.write("api.h", ITEMS_H);
project.write("main.cpp", ITEMS_CPP);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
let capabilities = server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
},
}),
);
let capabilities = &capabilities["result"]["capabilities"];
assert_eq!(
capabilities["referencesProvider"],
json!(true),
"{capabilities}"
);
assert_eq!(
capabilities["renameProvider"]["prepareProvider"],
json!(true),
"the rename box is offered only where a rename can happen: {capabilities}"
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": ITEMS_CPP }
}),
);
let at_the_use = json!({ "line": 2, "character": 16 });
let mut references = |include_declaration: bool| {
server.ask_until(100, |id| {
json!({
"id": id,
"method": "textDocument/references",
"params": {
"textDocument": { "uri": main_uri },
"position": at_the_use,
"context": { "includeDeclaration": include_declaration },
},
})
})
};
let with_the_definition = references(true);
let locations = with_the_definition["result"]
.as_array()
.unwrap_or_else(|| panic!("a location list was expected, got {with_the_definition}"));
assert_eq!(
locations.len(),
3,
"the `#define` and both uses: {with_the_definition}"
);
let files: Vec<&str> = locations
.iter()
.filter_map(|location| location["uri"].as_str())
.collect();
assert!(
files.iter().any(|file| file.ends_with("limits.h")),
"including the file that defines it, which nobody opened: {with_the_definition}"
);
assert!(
files.iter().any(|file| file.ends_with("api.h")),
"and the header that uses it: {with_the_definition}"
);
let without = references(false);
assert_eq!(
without["result"].as_array().map(Vec::len),
Some(2),
"`includeDeclaration: false` leaves the `#define` out: {without}"
);
let prepared = server.request(
200,
"textDocument/prepareRename",
json!({ "textDocument": { "uri": main_uri }, "position": at_the_use }),
);
assert_eq!(
prepared["result"]["range"]["start"],
json!({ "line": 2, "character": 13 }),
"the box starts around the name itself: {prepared}"
);
assert_eq!(
prepared["result"]["range"]["end"],
json!({ "line": 2, "character": 22 }),
"{prepared}"
);
assert_eq!(
prepared["result"]["placeholder"], "MAX_ITEMS",
"and it starts with the name in it: {prepared}"
);
let renamed = server.request(
300,
"textDocument/rename",
json!({
"textDocument": { "uri": main_uri },
"position": at_the_use,
"newName": "MAX_ENTRIES",
}),
);
let changes = renamed["result"]["changes"]
.as_object()
.unwrap_or_else(|| panic!("a workspace edit was expected, got {renamed}"));
assert_eq!(changes.len(), 3, "three files are edited: {renamed}");
let edits: Vec<&Value> = changes
.values()
.flat_map(|edits| edits.as_array().into_iter().flatten())
.collect();
assert_eq!(edits.len(), 3, "and one edit in each: {renamed}");
assert!(
edits
.iter()
.all(|edit| edit["newText"] == json!("MAX_ENTRIES")),
"every edit writes the new name: {renamed}"
);
let definition_edit = changes
.iter()
.find(|(uri, _)| uri.ends_with("limits.h"))
.map(|(_, edits)| &edits[0])
.unwrap_or_else(|| panic!("the `#define` itself is edited: {renamed}"));
assert_eq!(
definition_edit["range"],
json!({
"start": { "line": 0, "character": 8 },
"end": { "line": 0, "character": 17 },
}),
"and the definition's edit covers its name: {definition_edit}"
);
let on_a_local = json!({ "line": 2, "character": 9 });
let prepared = server.request(
400,
"textDocument/prepareRename",
json!({ "textDocument": { "uri": main_uri }, "position": on_a_local }),
);
assert_eq!(
prepared["result"]["placeholder"], "more",
"a local has a rename box too: {prepared}"
);
let renamed = server.request(
500,
"textDocument/rename",
json!({
"textDocument": { "uri": main_uri },
"position": on_a_local,
"newName": "renamed",
}),
);
let changes = renamed["result"]["changes"]
.as_object()
.unwrap_or_else(|| panic!("the local is renamed: {renamed}"));
println!("local rename: {renamed}");
assert_eq!(
changes.len(),
1,
"one file — the one the local is in: {renamed}"
);
assert_eq!(
changes
.values()
.next()
.and_then(|edits| edits.as_array())
.map(Vec::len),
Some(1),
"one edit: the declaration, the only occurrence in this fixture: {renamed}"
);
for not_a_name in ["two words", "\"quoted\"", ""] {
let refused = server.request(
600,
"textDocument/rename",
json!({
"textDocument": { "uri": main_uri },
"position": at_the_use,
"newName": not_a_name,
}),
);
assert!(
refused["result"].is_null(),
"{not_a_name:?} is not a name: {refused}"
);
}
server.request(999, "shutdown", Value::Null);
server.notify("exit", Value::Null);
}
const SELECTION_CPP: &str = "int f() {\n return w.size;\n}\n";
#[test]
fn a_selection_expands_one_rung_at_a_time() {
let project = Project::new("selection-range");
project.write("main.cpp", SELECTION_CPP);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
let capabilities = server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
},
}),
);
assert_eq!(
capabilities["result"]["capabilities"]["selectionRangeProvider"],
json!(true),
"the client is told the server can expand a selection: {capabilities}"
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": SELECTION_CPP }
}),
);
let answer = server.ask_until(100, |id| {
json!({
"id": id,
"method": "textDocument/selectionRange",
"params": {
"textDocument": { "uri": main_uri },
"positions": [{ "line": 1, "character": 13 }],
},
})
});
let answers = answer["result"]
.as_array()
.unwrap_or_else(|| panic!("one answer per position was expected, got {answer}"));
assert_eq!(answers.len(), 1, "one position, one chain: {answer}");
let mut rungs = Vec::new();
let mut current = Some(&answers[0]);
while let Some(range) = current {
rungs.push((
range["range"]["start"]["line"].as_u64().unwrap_or_default(),
range["range"]["start"]["character"]
.as_u64()
.unwrap_or_default(),
range["range"]["end"]["line"].as_u64().unwrap_or_default(),
range["range"]["end"]["character"]
.as_u64()
.unwrap_or_default(),
));
current = range.get("parent").filter(|parent| !parent.is_null());
}
assert_eq!(
rungs,
vec![
(1, 13, 1, 17), (1, 11, 1, 17), (1, 4, 1, 18), (0, 7, 3, 0), (0, 0, 3, 0), ],
"the chain, innermost first: {answer}"
);
server.request(999, "shutdown", Value::Null);
server.notify("exit", Value::Null);
}
const SEARCH_H: &str =
"namespace ns {\nstruct Widget { int size; };\n}\nstruct WidgetRegistry { int count; };\n";
const SEARCH_CPP: &str = "#include \"search.h\"\nvoid f() { ns::Widget w; }\n";
#[test]
fn a_workspace_search_finds_a_symbol_in_a_file_nobody_opened() {
let project = Project::new("workspace-symbol");
project.write("search.h", SEARCH_H);
project.write("main.cpp", SEARCH_CPP);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
let capabilities = server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
},
}),
);
assert_eq!(
capabilities["result"]["capabilities"]["workspaceSymbolProvider"],
json!(true),
"the client is told the server searches: {capabilities}"
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": SEARCH_CPP }
}),
);
let answer = server.ask_until_it(
100,
|id| json!({ "id": id, "method": "workspace/symbol", "params": { "query": "widget" } }),
|response| {
response["result"]
.as_array()
.is_some_and(|symbols| !symbols.is_empty())
},
);
let found = answer["result"]
.as_array()
.unwrap_or_else(|| panic!("a symbol list was expected, got {answer}"));
let names: Vec<&str> = found
.iter()
.filter_map(|symbol| symbol["name"].as_str())
.collect();
assert_eq!(
names,
vec!["Widget", "WidgetRegistry"],
"the name first, then the one that contains it: {answer}"
);
let widget = &found[0];
assert!(
widget["location"]["uri"]
.as_str()
.is_some_and(|uri| uri.ends_with("search.h")),
"the class is declared in a file the client never opened: {widget}"
);
assert_eq!(
widget["containerName"],
json!("ns"),
"and the namespace it is in travels with it: {widget}"
);
assert_eq!(widget["kind"], json!(5), "a class: {widget}");
let qualified = server.ask_until(
200,
|id| json!({ "id": id, "method": "workspace/symbol", "params": { "query": "Widget::si" } }),
);
let names: Vec<&str> = qualified["result"]
.as_array()
.into_iter()
.flatten()
.filter_map(|symbol| symbol["name"].as_str())
.collect();
assert_eq!(
names,
vec!["size"],
"`Widget::si` asks for the member: {qualified}"
);
let class_only = server.ask_until(
300,
|id| json!({ "id": id, "method": "workspace/symbol", "params": { "query": "ns::widget" } }),
);
let names: Vec<&str> = class_only["result"]
.as_array()
.into_iter()
.flatten()
.filter_map(|symbol| symbol["name"].as_str())
.collect();
assert_eq!(
names,
vec!["Widget"],
"and a class query does not drag its members in: {class_only}"
);
server.request(999, "shutdown", Value::Null);
server.notify("exit", Value::Null);
}
const FOLDS_CPP: &str = "#include <string>\n\
#include <vector>\n\
\n\
// a note\n\
// and another\n\
\n\
struct Widget {\n\
int size;\n\
};\n\
\n\
#if defined(_WIN32)\n\
int win;\n\
#endif\n";
#[test]
fn the_folds_of_a_file_are_the_regions_a_reader_hides() {
let project = Project::new("folding");
project.write("main.cpp", FOLDS_CPP);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
let capabilities = server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
},
}),
);
assert_eq!(
capabilities["result"]["capabilities"]["foldingRangeProvider"],
json!(true),
"the client is told the server folds: {capabilities}"
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": FOLDS_CPP }
}),
);
let answer = server.ask_until(100, |id| {
json!({
"id": id,
"method": "textDocument/foldingRange",
"params": { "textDocument": { "uri": main_uri } },
})
});
let ranges = answer["result"]
.as_array()
.unwrap_or_else(|| panic!("a folding range list was expected, got {answer}"));
let seen: Vec<(u64, u64, Option<&str>)> = ranges
.iter()
.map(|range| {
(
range["startLine"].as_u64().unwrap_or_default(),
range["endLine"].as_u64().unwrap_or_default(),
range["kind"].as_str(),
)
})
.collect();
assert_eq!(
seen,
vec![
(0, 1, Some("imports")),
(3, 4, Some("comment")),
(6, 8, None),
(10, 12, Some("region")),
],
"the include run, the comment block, the class (no kind: the protocol calls that code), the conditional"
);
server.request(999, "shutdown", Value::Null);
server.notify("exit", Value::Null);
}
const OUTLINE_CPP: &str = "#include \"cfg.h\"\n\
#include \"handle.h\"\n\
#if OFF\n\
struct Dead { int x; };\n\
#endif\n\
struct Live { int y; };\n\
DECLARE_HANDLE(HWND);\n";
#[test]
fn the_outline_shows_the_file_and_not_the_compiler() {
let project = Project::new("outline");
project.write("cfg.h", CFG_H);
project.write("handle.h", HANDLE_H);
project.write("main.cpp", OUTLINE_CPP);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
let capabilities = server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
},
}),
);
assert_eq!(
capabilities["result"]["capabilities"]["documentSymbolProvider"],
json!(true),
"the client is told the server outlines: {capabilities}"
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": OUTLINE_CPP }
}),
);
let answer = server.ask_until(100, |id| {
json!({
"id": id,
"method": "textDocument/documentSymbol",
"params": { "textDocument": { "uri": main_uri } },
})
});
let symbols = answer["result"]
.as_array()
.unwrap_or_else(|| panic!("a nested outline was expected, got {answer}"));
fn all_names(symbols: &[Value], into: &mut Vec<String>) {
for symbol in symbols {
into.push(symbol["name"].as_str().unwrap_or_default().to_string());
if let Some(children) = symbol["children"].as_array() {
all_names(children, into);
}
}
}
let mut names = Vec::new();
all_names(symbols, &mut names);
assert_eq!(
names,
vec!["Dead", "x", "Live", "y"],
"the branch nobody takes is in the outline, the file's own class, and nothing else: {answer}"
);
assert_eq!(symbols[0]["kind"], json!(5), "a class: {}", symbols[0]);
assert_eq!(
symbols[0]["children"][0]["kind"],
json!(13),
"a variable: {}",
symbols[0]
);
assert_eq!(
symbols[0]["selectionRange"],
json!({ "start": { "line": 3, "character": 7 }, "end": { "line": 3, "character": 11 } }),
"`Dead` is selected by its own name: {}",
symbols[0]
);
assert_eq!(
symbols[0]["range"]["start"]["line"],
json!(3),
"and the declaration starts on the same line: {}",
symbols[0]
);
server.request(999, "shutdown", Value::Null);
server.notify("exit", Value::Null);
}
const COLOURS_CPP: &str = "\
#define LIMIT 8
namespace shapes {
struct Widget {
int size;
int scaled(int factor) const;
};
}
";
#[test]
fn semantic_colours_are_the_kinds_the_declarations_have() {
let project = Project::new("semantic-tokens");
project.write("main.cpp", COLOURS_CPP);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
let capabilities = server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
"textDocument": {
"semanticTokens": {
"requests": { "full": true },
"tokenTypes": ["namespace", "type", "typeParameter", "enumMember", "function",
"method", "variable", "parameter", "macro"],
"tokenModifiers": ["declaration"],
"formats": ["relative"],
},
},
},
}),
);
let legend = capabilities["result"]["capabilities"]["semanticTokensProvider"]["legend"].clone();
let index_of = |name: &str| -> u64 {
legend["tokenTypes"]
.as_array()
.expect("a legend")
.iter()
.position(|kind| kind == name)
.unwrap_or_else(|| panic!("the legend has no `{name}`: {legend}")) as u64
};
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": COLOURS_CPP }
}),
);
let answer = server.ask_until(100, |id| {
json!({
"id": id,
"method": "textDocument/semanticTokens/full",
"params": { "textDocument": { "uri": main_uri } },
})
});
let data = answer["result"]["data"]
.as_array()
.unwrap_or_else(|| panic!("a token array was expected, got {answer}"));
let numbers: Vec<u64> = data
.iter()
.map(|value| value.as_u64().expect("the encoding is numbers"))
.collect();
assert_eq!(numbers.len() % 5, 0, "five numbers per token: {numbers:?}");
let mut line = 0u64;
let mut character = 0u64;
let mut tokens: Vec<(u64, u64, u64, u64, u64)> = Vec::new();
for token in numbers.chunks(5) {
if token[0] == 0 {
character += token[1];
} else {
line += token[0];
character = token[1];
}
tokens.push((line, character, token[2], token[3], token[4]));
}
let at = |needle: &str| -> (u64, u64) {
let line = COLOURS_CPP
.lines()
.position(|text| text.contains(needle))
.expect("the fixture writes it") as u64;
let character = COLOURS_CPP
.lines()
.nth(line as usize)
.and_then(|text| text.find(needle))
.expect("the fixture writes it") as u64;
(line, character)
};
let coloured = |needle: &str| -> Option<(u64, u64, u64)> {
let (line, character) = at(needle);
tokens
.iter()
.find(|token| token.0 == line && token.1 == character)
.map(|token| (token.2, token.3, token.4))
};
assert_eq!(
coloured("LIMIT").map(|(_, kind, _)| kind),
Some(index_of("macro")),
"a macro this file defines: {tokens:?}"
);
assert_eq!(
coloured("shapes").map(|(_, kind, _)| kind),
Some(index_of("namespace")),
"{tokens:?}"
);
assert_eq!(
coloured("Widget").map(|(_, kind, _)| kind),
Some(index_of("type")),
"a class is a type here: {tokens:?}"
);
assert_eq!(
coloured("size").map(|(_, kind, _)| kind),
Some(index_of("variable")),
"a field is a variable: {tokens:?}"
);
assert_eq!(
coloured("scaled").map(|(_, kind, _)| kind),
Some(index_of("method")),
"a function in a class is a method: {tokens:?}"
);
assert_eq!(
coloured("factor").map(|(_, kind, _)| kind),
Some(index_of("parameter")),
"and a parameter is not a variable: {tokens:?}"
);
let (length, _, modifiers) = coloured("LIMIT").expect("the macro is coloured");
assert_eq!(length, "LIMIT".len() as u64);
assert_eq!(modifiers, 1, "the declaration modifier: {tokens:?}");
server.request(999, "shutdown", Value::Null);
server.notify("exit", Value::Null);
}
const SIGNATURE_CPP: &str = "\
/// Scales a count.
/// @param count the count
int scale(int count, double factor);
int f() {
return scale(1, );
}
";
#[test]
fn a_signature_is_shown_while_the_arguments_are_typed() {
let project = Project::new("signature-help");
project.write("main.cpp", SIGNATURE_CPP);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
},
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": SIGNATURE_CPP }
}),
);
let call_line = SIGNATURE_CPP
.lines()
.position(|line| line.contains("scale(1, )"))
.expect("the fixture writes the call") as u64;
let after_the_comma = SIGNATURE_CPP
.lines()
.nth(call_line as usize)
.and_then(|line| line.find("scale(1, "))
.expect("the fixture writes the call")
+ "scale(1, ".len();
let answer = server.ask_until(100, |id| {
json!({
"id": id,
"method": "textDocument/signatureHelp",
"params": {
"textDocument": { "uri": main_uri },
"position": { "line": call_line, "character": after_the_comma },
},
})
});
let signature = &answer["result"]["signatures"][0];
assert_eq!(
signature["label"], "scale(int count, double factor)",
"the declaration's own text: {answer}"
);
assert_eq!(answer["result"]["activeSignature"], json!(0));
assert_eq!(
answer["result"]["activeParameter"],
json!(1),
"the cursor is past the first comma, so the second parameter is active: {answer}"
);
let label = signature["label"].as_str().expect("a label");
let spans: Vec<&str> = signature["parameters"]
.as_array()
.expect("the parameters are sent")
.iter()
.map(|parameter| {
let offsets = parameter["label"].as_array().expect("offsets");
let start = offsets[0].as_u64().expect("a start") as usize;
let end = offsets[1].as_u64().expect("an end") as usize;
&label[start..end]
})
.collect();
assert_eq!(spans, vec!["int count", "double factor"]);
let documentation = signature["documentation"]["value"]
.as_str()
.unwrap_or_else(|| panic!("the documentation above the declaration: {answer}"));
assert!(documentation.contains("Scales a count."), "{documentation}");
assert!(
!documentation.contains("@param"),
"the parameter's own sentence is not repeated above the signature: {documentation}"
);
let parameter_documentation = signature["parameters"][0]["documentation"]["value"]
.as_str()
.unwrap_or_else(|| {
panic!("`@param count the count` belongs to the first parameter: {answer}")
});
assert_eq!(parameter_documentation, "the count");
assert_eq!(
signature["parameters"][1]["documentation"],
Value::Null,
"a parameter with no `@param` line has no documentation rather than somebody else's: {answer}"
);
server.request(999, "shutdown", Value::Null);
server.notify("exit", Value::Null);
}
const RESOLVE_CPP: &str = "\
struct Widget {
/// The widget's size in cells.
int size;
};
int f() {
Widget w;
w.
}
";
#[test]
fn a_completion_item_resolves_to_its_declarations_documentation() {
let project = Project::new("completion-resolve");
project.write("main.cpp", RESOLVE_CPP);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
},
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": RESOLVE_CPP }
}),
);
let line = RESOLVE_CPP
.lines()
.position(|text| text.trim() == "w.")
.expect("the fixture writes the access") as u64;
let character = RESOLVE_CPP
.lines()
.nth(line as usize)
.and_then(|text| text.find("w."))
.expect("the fixture writes the access")
+ 2;
let listed = server.ask_until_it(
100,
|id| {
json!({
"id": id,
"method": "textDocument/completion",
"params": {
"textDocument": { "uri": main_uri },
"position": { "line": line, "character": character },
},
})
},
|answer| {
answer["result"]["items"]
.as_array()
.is_some_and(|items| items.iter().any(|item| item["label"] == json!("size")))
},
);
let item = listed["result"]["items"]
.as_array()
.expect("an item list")
.iter()
.find(|item| item["label"] == json!("size"))
.expect("the member is offered")
.clone();
assert!(
item["data"]["file"].is_string() && item["data"]["offset"].is_number(),
"the item carries its declaration's identity: {item}"
);
assert!(
item["documentation"].is_null(),
"the list itself carries no comments: {item}"
);
let resolved = server.ask_until_it(
200,
|id| {
json!({
"id": id,
"method": "completionItem/resolve",
"params": item.clone(),
})
},
|answer| !answer["result"]["documentation"].is_null(),
);
let documentation = resolved["result"]["documentation"]["value"]
.as_str()
.unwrap_or_else(|| panic!("markdown documentation was expected, got {resolved}"));
assert!(
documentation.contains("The widget's size in cells."),
"the member's own comment: {documentation}"
);
assert_eq!(
resolved["result"]["label"], "size",
"and the item is the one that was asked about: {resolved}"
);
server.request(999, "shutdown", Value::Null);
server.notify("exit", Value::Null);
}
#[test]
fn a_hover_names_the_instantiation_and_never_invents_a_void() {
let project = Project::new("hover-instantiation");
project.write("map.h", INSTANTIATION_HEADER);
project.write("main.cpp", INSTANTIATION_CPP);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
},
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": INSTANTIATION_CPP }
}),
);
let instantiation = hover_over(
&mut server,
100,
&main_uri,
INSTANTIATION_CPP,
"map<const char*, int, Less>",
);
let text = instantiation["result"]["contents"]["value"]
.as_str()
.unwrap_or_else(|| panic!("a markdown answer was expected, got {instantiation}"));
assert!(
text.contains("std::map<const char*, int, Less>"),
"the use's own spelling, not the declaration's parameter names: {text}"
);
assert!(
!text.contains("map<_Kty, _Ty"),
"and not the shape the header declares: {text}"
);
let anonymous = hover_over(&mut server, 200, &main_uri, INSTANTIATION_CPP, "priority");
let text = anonymous["result"]["contents"]["value"]
.as_str()
.unwrap_or_else(|| panic!("a markdown answer was expected, got {anonymous}"));
assert!(
!text.contains("void"),
"an anonymous struct's variable is not `void`: {text}"
);
assert!(
!text.contains("unsigned char"),
"the card does not echo the struct's body: {text}"
);
assert!(
text.contains("priority"),
"and it is still about the name the cursor is on: {text}"
);
server.request(999, "shutdown", Value::Null);
server.notify("exit", Value::Null);
}
const INSTANTIATION_HEADER: &str = "\
#pragma once
namespace std {
template <class _Kty, class _Ty, class _Less>
class map {
public:
void insert(const _Kty& key, const _Ty& value);
};
}
";
const INSTANTIATION_CPP: &str = "\
#include \"map.h\"
std::map<const char*, int, Less> table;
static const struct
{
unsigned char left;
unsigned char right;
} priority[] = { { 1, 2 } };
";
const HOVER_CPP: &str = "\
/// Adds two counts.
/// @param a the first
/// @param b the second
int add(int a, int b);
// A note to the reader, which is not documentation.
int undocumented(int c);
struct Widget {
int size;
int scaled(int factor) const { return size * factor; }
int go() const { return this->scaled(2); }
};
";
#[test]
fn hover_shows_the_documentation_and_refuses_a_plain_comment() {
let project = Project::new("hover");
project.write("main.cpp", HOVER_CPP);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
},
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": HOVER_CPP }
}),
);
let documented = hover_over(&mut server, 100, &main_uri, HOVER_CPP, "add(int a, int b);");
let text = documented["result"]["contents"]["value"]
.as_str()
.unwrap_or_else(|| panic!("a markdown answer was expected, got {documented}"));
assert!(
text.contains("Adds two counts."),
"the comment above the declaration is the documentation: {text}"
);
assert!(
text.contains("**`a`** — the first") && text.contains("**`b`** — the second"),
"each parameter is a list item, named and described: {text}"
);
assert!(
!text.contains("@param"),
"and the markers are no longer in the text: {text}"
);
assert!(
text.contains("add(int a, int b)"),
"with the declaration itself: {text}"
);
let plain = hover_over(
&mut server,
200,
&main_uri,
HOVER_CPP,
"undocumented(int c);",
);
let text = plain["result"]["contents"]["value"]
.as_str()
.unwrap_or_else(|| panic!("a markdown answer was expected, got {plain}"));
assert!(
!text.contains("A note to the reader"),
"a plain `//` comment is not documentation: {text}"
);
assert!(
text.contains("undocumented(int c)"),
"the declaration is still what the hover is about: {text}"
);
let this = hover_over(&mut server, 300, &main_uri, HOVER_CPP, "this->scaled(2)");
let text = this["result"]["contents"]["value"]
.as_str()
.unwrap_or_else(|| panic!("a markdown answer was expected for `this`, got {this}"));
assert!(
text.contains("enclosing class `Widget`"),
"`this` names no declaration, so the expression question answers: {text}"
);
server.request(999, "shutdown", Value::Null);
server.notify("exit", Value::Null);
}
#[test]
fn a_hover_on_the_last_member_of_a_chain_reads_the_chain() {
const FIXTURE: &str = "\
typedef struct { int fnum; } buf_T;
typedef struct { buf_T *w_buffer; int other; } win_T;
int f(win_T *win) { return win->w_buffer->fnum; }
";
let project = Project::new("hover-chain");
project.write("main.c", FIXTURE);
let mut server = Server::start(project.root());
let uri = uri_of(&project.root().join("main.c"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
},
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": uri, "languageId": "c", "version": 1, "text": FIXTURE }
}),
);
let line = line_of(FIXTURE, "->w_buffer->fnum");
let character = FIXTURE
.lines()
.nth(line as usize)
.and_then(|text| text.find("fnum"))
.expect("the fixture writes it")
+ 2;
let answer = server.ask_until(100, |id| {
json!({
"id": id,
"method": "textDocument/hover",
"params": {
"textDocument": { "uri": uri },
"position": { "line": line, "character": character },
},
})
});
let text = answer["result"]["contents"]["value"]
.as_str()
.unwrap_or_else(|| panic!("a markdown answer was expected, got {answer}"));
assert!(
!text.contains("has no member"),
"`fnum` is a member of the chain's type, so nothing may say otherwise: {text}"
);
assert!(
text.contains("int"),
"and the popup says what the member is: {text}"
);
server.request(999, "shutdown", Value::Null);
server.notify("exit", Value::Null);
}
#[test]
fn a_hover_after_an_arrow_reads_the_pointees_members() {
const FIXTURE: &str = "\
struct Config { void load(); };
template <class T>
struct Handle {
T* operator->();
T* get() const;
void reset();
};
void f() {
Handle<Config> h;
h->load();
h.get();
}
";
let project = Project::new("hover_arrow");
project.write("main.cpp", FIXTURE);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true },
"window": { "workDoneProgress": true },
},
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": FIXTURE }
}),
);
let pointee = hover_at(&mut server, 100, &main_uri, FIXTURE, "h->load", 3);
let text = pointee["result"]["contents"]["value"]
.as_str()
.unwrap_or_else(|| panic!("a markdown answer was expected, got {pointee}"));
assert!(
text.contains("Config") && text.contains("load"),
"`h->load` is `Config::load`, through the handle's `operator->`: {text}"
);
let handle = hover_at(&mut server, 200, &main_uri, FIXTURE, "h.get", 2);
let text = handle["result"]["contents"]["value"]
.as_str()
.unwrap_or_else(|| panic!("a markdown answer was expected, got {handle}"));
assert!(
text.contains("get") && text.contains("Handle"),
"`h.get` is the handle's own member, asked second: {text}"
);
server.request(300, "shutdown", Value::Null);
server.notify("exit", Value::Null);
}
#[test]
fn hover_shows_the_unreal_reflection_annotation() {
let project = Project::new("unreal-hover");
project.write(".cppls.toml", "[unreal]\nenable = true\n");
project.write(
"engine.h",
"#pragma once\n\n#define UCLASS(...)\n#define UPROPERTY(...)\n#define GENERATED_BODY()\n",
);
const ACTOR_H: &str = "\
#include \"engine.h\"
UCLASS(Blueprintable)
class AMyActor
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, Category = \"Game\")
bool bIsReady = false;
};
";
project.write("actor.h", ACTOR_H);
let mut server = Server::start(project.root());
let uri = uri_of(&project.root().join("actor.h"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {},
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({ "textDocument": { "uri": uri, "languageId": "cpp", "version": 1, "text": ACTOR_H } }),
);
let hover = hover_at(&mut server, 100, &uri, ACTOR_H, "AMyActor", 1);
let text = hover["result"]["contents"]["value"]
.as_str()
.unwrap_or_else(|| panic!("a markdown answer was expected, got {hover}"));
assert!(
text.contains("UCLASS(Blueprintable)"),
"the annotation, as the file wrote it: {text}"
);
assert!(
text.contains("AMyActor"),
"and the declaration is still what the card is about: {text}"
);
server.request(200, "shutdown", Value::Null);
server.notify("exit", Value::Null);
}
fn hover_at(
server: &mut Server,
first_id: i64,
uri: &str,
fixture: &str,
needle: &str,
into: usize,
) -> Value {
let at = fixture.rfind(needle).expect("the fixture writes it") + into;
let before = &fixture[..at];
let line: u64 = before
.matches('\n')
.count()
.try_into()
.expect("a fixture this size fits in a u32");
let character: u64 = before
.rsplit('\n')
.next()
.unwrap_or_default()
.chars()
.count()
.try_into()
.expect("a line this size fits in a u32");
server.ask_until(first_id, |id| {
json!({
"id": id,
"method": "textDocument/hover",
"params": {
"textDocument": { "uri": uri },
"position": { "line": line, "character": character },
},
})
})
}
fn hover_over(server: &mut Server, first_id: i64, uri: &str, fixture: &str, needle: &str) -> Value {
let line = line_of(fixture, needle);
let character = fixture
.lines()
.nth(line as usize)
.and_then(|text| text.find(needle))
.expect("the fixture writes it");
server.ask_until(first_id, |id| {
json!({
"id": id,
"method": "textDocument/hover",
"params": {
"textDocument": { "uri": uri },
"position": { "line": line, "character": character },
},
})
})
}
fn line_of(text: &str, needle: &str) -> u64 {
text.lines()
.position(|line| line.contains(needle))
.unwrap_or_else(|| panic!("{needle:?} is not in the fixture"))
.try_into()
.expect("a fixture this size fits in a u32")
}
const INLAY_HEADER: &str = "\
#pragma once
double scale(int count, double factor);
";
const INLAY_ACROSS_FILES: &str = "\
#include \"widget.h\"
double f(int count) {
return scale(count, 1);
}
";
const INLAY_CPP: &str = "\
double scale(int count, double factor);
struct Widget {
int size_of(int scale) const;
};
double f(Widget& w, int count) {
return scale(count, 1) + w.size_of(2);
}
";
#[test]
fn the_hints_survive_the_edit_that_asks_for_them() {
let project = Project::new("inlay-hints-after-an-edit");
project.write("widget.h", INLAY_HEADER);
project.write("main.cpp", INLAY_ACROSS_FILES);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
let whole = json!({
"start": { "line": 0, "character": 0 },
"end": { "line": INLAY_ACROSS_FILES.lines().count(), "character": 0 },
});
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": { "textDocument": { "inlayHint": {} } },
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": {
"uri": main_uri,
"languageId": "cpp",
"version": 1,
"text": INLAY_ACROSS_FILES,
}
}),
);
let settled = server.ask_until_it(
100,
|id| {
json!({
"id": id,
"method": "textDocument/inlayHint",
"params": { "textDocument": { "uri": main_uri }, "range": whole },
})
},
|answer| {
answer["result"]
.as_array()
.is_some_and(|hints| !hints.is_empty())
},
);
assert!(
settled["result"]
.as_array()
.is_some_and(|hints| hints.len() == 1),
"one argument that does not spell its parameter: {settled}"
);
let edited = INLAY_ACROSS_FILES.replace("return scale(count, 1);", "return scale(count, 1) ;");
server.notify(
"textDocument/didChange",
json!({
"textDocument": { "uri": main_uri, "version": 2 },
"contentChanges": [{ "text": edited }],
}),
);
let after = server.request(
200,
"textDocument/inlayHint",
json!({ "textDocument": { "uri": main_uri }, "range": whole }),
);
let hints = after["result"]
.as_array()
.unwrap_or_else(|| panic!("**the hints were refused outright after an edit**: {after}"));
assert_eq!(
hints.len(),
1,
"an edit that moves no directive changes nothing a hint depends on, and the callee is in a header: {after}"
);
}
#[test]
fn parameter_names_are_drawn_at_the_arguments() {
let project = Project::new("inlay-hints");
project.write("main.cpp", INLAY_CPP);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
let capabilities = server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
},
}),
);
assert_eq!(
capabilities["result"]["capabilities"]["inlayHintProvider"]["resolveProvider"],
json!(false),
"the client is told the server hints, and that there is nothing to resolve: {capabilities}"
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": INLAY_CPP }
}),
);
let call_line = line_of(INLAY_CPP, "return scale(count, 1)");
let answer = server.ask_until(100, |id| {
json!({
"id": id,
"method": "textDocument/inlayHint",
"params": {
"textDocument": { "uri": main_uri },
"range": {
"start": { "line": 0, "character": 0 },
"end": { "line": INLAY_CPP.lines().count(), "character": 0 },
},
},
})
});
let hints = answer["result"]
.as_array()
.unwrap_or_else(|| panic!("an inlay hint list was expected, got {answer}"));
let seen: Vec<(u64, String)> = hints
.iter()
.map(|hint| {
(
hint["position"]["line"].as_u64().expect("a line"),
hint["label"].as_str().expect("a label").to_string(),
)
})
.collect();
assert_eq!(
seen,
vec![
(call_line, "factor:".to_string()),
(call_line, "scale:".to_string()),
],
"`count` spells its parameter and gets no hint; the other two do: {answer}"
);
let factor = &hints[0];
let argument = INLAY_CPP
.lines()
.nth(call_line as usize)
.and_then(|line| line.find("scale(count, 1)"))
.expect("the fixture writes the call")
+ "scale(count, ".len();
assert_eq!(
factor["position"]["character"],
json!(argument),
"the hint sits on the argument: {factor}"
);
assert_eq!(factor["paddingRight"], json!(true), "{factor}");
assert_eq!(factor["kind"], json!(2), "a parameter hint: {factor}");
server.request(999, "shutdown", Value::Null);
server.notify("exit", Value::Null);
}
#[test]
fn the_chosen_template_overload_is_shown_instantiated() {
const TAKE_H: &str =
"template <typename T> void take(T* value);\ntemplate <typename T> void take(T value);\n";
const TAKE_CPP: &str =
"#include \"take.h\"\nint g() { int n = 0; int* p = &n; take(p); return 0; }\n";
let project = Project::new("hover-template-overloads");
project.write("take.h", TAKE_H);
project.write("main.cpp", TAKE_CPP);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
},
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": TAKE_CPP }
}),
);
let hovered = hover_over(&mut server, 100, &main_uri, TAKE_CPP, "take(p)");
let text = hovered["result"]["contents"]["value"]
.as_str()
.unwrap_or_else(|| panic!("a markdown answer was expected, got {hovered}"));
assert!(
text.contains("→ "),
"an overload is marked as what the call means: {text}"
);
assert!(
text.lines()
.any(|line| line.trim_start().starts_with("→ ") && line.contains("int* value")),
"and it is shown instantiated — `take(int* value)` rather than `take(T* value)`: {text}"
);
}
#[test]
fn a_hover_on_a_call_shows_only_the_declarations_it_can_reach() {
const SHAPES_H: &str = "\
struct S {
void f(int a);
void f(int a, int b = 0);
void f(int a, int b, int c);
void g(int a);
void g(long a);
void g(int a, int b);
};
";
const SHAPES_CPP: &str = "\
#include \"shapes.h\"
void h(S s) {
s.f(1);
s.g(unknown_name);
}
";
let project = Project::new("hover-viable-overloads");
project.write("shapes.h", SHAPES_H);
project.write("main.cpp", SHAPES_CPP);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
},
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": SHAPES_CPP }
}),
);
let hovered = hover_over(&mut server, 100, &main_uri, SHAPES_CPP, "f(1)");
let text = hovered["result"]["contents"]["value"]
.as_str()
.unwrap_or_else(|| panic!("a markdown answer was expected, got {hovered}"));
assert!(
text.contains("void f(int a)") && text.contains("S::f"),
"the call selects the one-parameter overload, and the card is that member's own: {text}"
);
assert!(
!text.contains("int b"),
"and the overloads that need more arguments are not shown: {text}"
);
let hovered = hover_over(&mut server, 200, &main_uri, SHAPES_CPP, "g(unknown_name)");
let text = hovered["result"]["contents"]["value"]
.as_str()
.unwrap_or_else(|| panic!("a markdown answer was expected, got {hovered}"));
assert!(
text.contains("void g(int a)") && text.contains("void g(long a)"),
"both one-parameter overloads accept an unknown argument: {text}"
);
assert!(
!text.contains("int a, int b"),
"the two-parameter overload cannot take one argument: {text}"
);
}
#[test]
fn the_definition_of_a_call_leads_with_the_overload_it_means() {
const OVERLOADS_H: &str =
"int parse(int value);\nint parse(const char* text);\nint parse(double value);\n";
const OVERLOADS_CPP: &str = "#include \"overloads.h\"\nint f() { return parse(1); }\n";
let project = Project::new("definition-overloads-order");
project.write("overloads.h", OVERLOADS_H);
project.write("main.cpp", OVERLOADS_CPP);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
},
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": OVERLOADS_CPP }
}),
);
let line = OVERLOADS_CPP
.lines()
.position(|line| line.contains("parse(1)"))
.unwrap_or_default();
let character = OVERLOADS_CPP
.lines()
.nth(line)
.and_then(|text| text.find("parse"))
.unwrap_or_default();
let mut first_line = None;
for attempt in 0..40 {
let answer = server.request(
100 + attempt,
"textDocument/definition",
json!({
"textDocument": { "uri": main_uri },
"position": { "line": line, "character": character },
}),
);
let locations = answer["result"].as_array().cloned().unwrap_or_default();
if locations.len() > 1 {
first_line = locations[0]["range"]["start"]["line"].as_u64();
break;
}
}
assert_eq!(
first_line,
Some(0),
"an `int` argument leads with `int parse(int value);`, which is line 0 of the header"
);
}
#[test]
fn the_overload_a_call_means_is_marked_in_the_hover_list() {
const OVERLOADS_H: &str = "int parse(int value);\nint parse(const char* text);\n";
const OVERLOADS_CPP: &str = "#include \"overloads.h\"\nint f() { return parse(1); }\n";
let project = Project::new("hover-overloads");
project.write("overloads.h", OVERLOADS_H);
project.write("main.cpp", OVERLOADS_CPP);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
},
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": OVERLOADS_CPP }
}),
);
let hovered = hover_over(&mut server, 100, &main_uri, OVERLOADS_CPP, "parse(1)");
let text = hovered["result"]["contents"]["value"]
.as_str()
.unwrap_or_else(|| panic!("a markdown answer was expected, got {hovered}"));
assert!(
text.contains("→ "),
"the overload this call means is marked: {text}"
);
let marked = text
.lines()
.find(|line| line.trim_start().starts_with("→ "))
.unwrap_or_default()
.to_string();
assert!(
marked.contains("int value"),
"and it is the `int` one, because the argument is an `int`: {marked:?} of\n{text}"
);
}
#[test]
fn an_overload_set_answers_with_every_location() {
const OVERLOADS_H: &str =
"int parse(int value);\nint parse(const char* text);\nint parse(double value);\n";
const OVERLOADS_CPP: &str = "#include \"overloads.h\"\nint f() { return parse(1); }\n";
let project = Project::new("definition-overloads");
project.write("overloads.h", OVERLOADS_H);
project.write("main.cpp", OVERLOADS_CPP);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
let header_uri = uri_of(&project.root().join("overloads.h"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
},
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": OVERLOADS_CPP }
}),
);
let answer = server.ask_until_it(
100,
|id| {
json!({
"id": id,
"method": "textDocument/definition",
"params": {
"textDocument": { "uri": main_uri },
"position": { "line": 1, "character": 20 },
},
})
},
|response| {
response["result"]
.as_array()
.is_some_and(|locations| locations.len() == 3)
},
);
let locations = answer["result"]
.as_array()
.unwrap_or_else(|| panic!("three declarations, three locations: {answer}"));
assert_eq!(locations.len(), 3);
for (index, location) in locations.iter().enumerate() {
assert_eq!(
location["uri"],
json!(header_uri),
"all three are in the header: {location}"
);
assert_eq!(
location["range"]["start"]["line"],
json!(index),
"one per line, in the order the header writes them: {answer}"
);
}
let single = server.request(
200,
"textDocument/definition",
json!({
"textDocument": { "uri": main_uri },
"position": { "line": 1, "character": 4 },
}),
);
assert!(
single["result"]["uri"].is_string(),
"`f` is declared on this line, and one declaration is one location: {single}"
);
server.request(999, "shutdown", Value::Null);
server.notify("exit", Value::Null);
}
const MACRO_ARGUMENT_CPP: &str = "\
struct alpha { int one; };
struct beta { int gamma; };
#define PICK(first, second) pick_impl((first), (second))
beta bet;
int f() {
return PICK(1, bet.);
}
";
#[test]
fn a_completion_inside_a_macros_argument_is_about_that_argument() {
let project = Project::new("completion-in-a-macro-argument");
project.write("main.cpp", MACRO_ARGUMENT_CPP);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": { "textDocument": { "completion": {} } },
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": MACRO_ARGUMENT_CPP }
}),
);
let answer = server.ask_until(100, |id| {
json!({
"id": id,
"method": "textDocument/completion",
"params": {
"textDocument": { "uri": main_uri },
"position": { "line": 8, "character": 23 },
},
})
});
let items = answer["result"]["items"]
.as_array()
.or_else(|| answer["result"].as_array())
.unwrap_or_else(|| panic!("a completion list was expected, got {answer}"));
let labels: Vec<&str> = items
.iter()
.filter_map(|item| item["label"].as_str())
.collect();
assert!(
labels.contains(&"gamma"),
"the members of the argument's own type are offered: {labels:?}"
);
assert!(
!labels.contains(&"one"),
"and not the members of the other type in the same file: {labels:?}"
);
}
const SIGNATURE_IN_A_MACRO_CPP: &str = "\
#define PICK(first, second) pick_impl((first), (second))
/// Scales a count.
int scale(int count, double factor);
int f() {
return PICK(1, scale(2, ));
}
";
#[test]
fn a_signature_survives_being_inside_a_macro_argument() {
let project = Project::new("signature-in-a-macro");
project.write("main.cpp", SIGNATURE_IN_A_MACRO_CPP);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": { "textDocument": { "signatureHelp": {} } },
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": SIGNATURE_IN_A_MACRO_CPP }
}),
);
let answer = server.ask_until_it(
100,
|id| {
json!({
"id": id,
"method": "textDocument/signatureHelp",
"params": {
"textDocument": { "uri": main_uri },
"position": { "line": 6, "character": 28 },
},
})
},
|answer| !answer["result"].is_null(),
);
let signatures = answer["result"]["signatures"]
.as_array()
.unwrap_or_else(|| panic!("a signature list was expected, got {answer}"));
let labels: Vec<&str> = signatures
.iter()
.filter_map(|signature| signature["label"].as_str())
.collect();
assert!(
labels.iter().any(|label| label.contains("scale")),
"the signature of the call the cursor is in, not of the macro around it: {labels:?}"
);
assert!(
!labels.iter().any(|label| label.contains("PICK")),
"and not the macro's own name: {labels:?}"
);
assert_eq!(
answer["result"]["activeParameter"],
json!(1),
"the second argument is the one being typed: {answer}"
);
}
#[test]
#[ignore = "a reading aid: it prints popups rather than checking them"]
fn what_the_popups_look_like() {
let project = Project::new("hover-shapes");
project.write(
"box.h",
"#pragma once\n\
/// A box that holds one `T`.\n\
///\n\
/// The documentation is the file's own words.\n\
template <class T>\n\
class Box {\n\
public:\n\
/// What is in the box.\n\
T get() const { return v; }\n\
T v;\n\
};\n",
);
project.write(
"main.cpp",
"#include \"box.h\"\n\
\n\
int main() {\n\
auto n = 1;\n\
Box<int> b;\n\
b.v = n;\n\
return 0;\n\
}\n",
);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
let main_text = "#include \"box.h\"\n\nint main() {\n auto n = 1;\n Box<int> b;\n b.v = n;\n \
return 0;\n}\n";
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": { "textDocument": { "hover": {} } },
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": main_text }
}),
);
let asked: [(u64, u64, &str); 5] = [
(
3,
9,
"the local `n`, whose type is an `auto` the file wrote",
),
(
4,
4,
"`Box` in `Box<int> b;` — a class template from a header",
),
(5, 6, "the member `v` of that class template"),
(4, 13, "the variable `b`, whose type is that class template"),
(1, 4, "`main` itself, a function this file defines"),
];
for (line, character, what) in asked {
let mut answer = server.ask_until_it(
100,
|id| {
json!({
"id": id,
"method": "textDocument/hover",
"params": {
"textDocument": { "uri": main_uri },
"position": { "line": line, "character": character },
},
})
},
|answer| !answer["result"].is_null(),
);
for _ in 0..4 {
answer = server.ask_until_it(
100,
|id| {
json!({
"id": id,
"method": "textDocument/hover",
"params": {
"textDocument": { "uri": main_uri },
"position": { "line": line, "character": character },
},
})
},
|answer| !answer["result"].is_null(),
);
}
let markdown = answer["result"]["contents"]["value"]
.as_str()
.unwrap_or("<no popup>")
.to_string();
println!("=== {line}:{character} — {what} ===\n{markdown}\n");
}
}
#[test]
fn a_declaration_in_the_viewed_file_is_reported_on_its_own_line() {
let project = Project::new("hover-coordinates");
project.write(
"main.cpp",
"#include <vector>\n\
#include <string>\n\
#include <format>\n\
#include <iostream>\n\
\n\
class Sux {\n\
public:\n\
void print() {}\n\
};\n\
\n\
int main() {\n\
Sux sux;\n\
return 0;\n\
}\n",
);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": { "textDocument": { "hover": {} } },
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": {
"uri": main_uri,
"languageId": "cpp",
"version": 1,
"text": "#include <vector>\n#include <string>\n#include <format>\n#include <iostream>\n\n\
class Sux {\npublic:\n void print() {}\n};\n\nint main() {\n Sux sux;\n \
return 0;\n}\n",
}
}),
);
let mut answer = server.ask_until_it(
100,
|id| {
json!({
"id": id,
"method": "textDocument/hover",
"params": {
"textDocument": { "uri": main_uri },
"position": { "line": 11, "character": 4 },
},
})
},
|answer| !answer["result"].is_null(),
);
for _ in 0..4 {
answer = server.ask_until_it(
100,
|id| {
json!({
"id": id,
"method": "textDocument/hover",
"params": {
"textDocument": { "uri": main_uri },
"position": { "line": 11, "character": 4 },
},
})
},
|answer| !answer["result"].is_null(),
);
}
let markdown = answer["result"]["contents"]["value"]
.as_str()
.unwrap_or_else(|| panic!("a hover was expected, got {answer}"));
println!("--- the popup about `Sux` ---\n{markdown}\n---");
assert!(
markdown.contains("main.cpp:6:7"),
"the class is written on line 6, column 7 — not on a line measured in the rendering: {markdown}"
);
assert!(
markdown.contains("class Sux"),
"the declaration as it is written, not the bytes at that offset in another document: {markdown}"
);
assert!(
!markdown.contains("tream>"),
"and no fragment of an `#include` line: {markdown}"
);
}
#[test]
#[ignore = "a reading aid for a reported file"]
fn what_the_completion_answers_here() {
let Ok(named) = std::env::var("CPPLS_REPORTED_FILE") else {
println!("skipped: no CPPLS_REPORTED_FILE (the file a completion report was about)");
return;
};
let Ok(source) = std::fs::read_to_string(&named) else {
println!("skipped: {named} is not on this machine");
return;
};
let project = Project::new("completion-report");
project.write("main.cpp", &source);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": { "textDocument": { "completion": {} } },
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": source }
}),
);
let mut asked: Vec<(u64, u64)> = Vec::new();
for (number, line) in source.lines().enumerate() {
let line = line.trim_end();
for (column, _) in line.char_indices() {
let at = &line[..column];
let ends = at.chars().last().is_some_and(|last| {
last.is_alphanumeric() || last == '_' || last == ':' || last == '.'
});
if ends {
asked.push((number as u64, column as u64));
}
}
}
for (number, character) in asked {
let number = number as usize;
let body = source.lines().nth(number).unwrap_or("").trim_end();
let answer = server.request(
100 + number as i64,
"textDocument/completion",
json!({
"textDocument": { "uri": main_uri },
"position": { "line": number, "character": character },
}),
);
let items = match &answer["result"] {
Value::Array(items) => items.len(),
Value::Object(object) => object["items"].as_array().map(Vec::len).unwrap_or(0),
_ => 0,
};
let first: Vec<&str> = match &answer["result"] {
serde_json::Value::Array(items) => items
.iter()
.filter_map(|i| i["label"].as_str())
.take(8)
.collect(),
serde_json::Value::Object(o) => o["items"]
.as_array()
.map(|a| {
a.iter()
.filter_map(|i| i["label"].as_str())
.take(8)
.collect()
})
.unwrap_or_default(),
_ => Vec::new(),
};
println!(
"{number:>3}: {:<40} -> {items} item(s): {first:?}",
body.trim()
);
}
}
#[test]
#[ignore = "a probe, run by hand: four minutes on the real project, and it asserts nothing"]
fn probe_the_cpu_shape_of_the_real_project() {
let root = std::path::Path::new(r"E:\EmmyLuaCodeStyle");
let mut server = Server::start(root);
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(root),
"capabilities": {
"textDocument": { "diagnostic": {}, "hover": {} },
"window": { "workDoneProgress": true },
},
}),
);
server.notify("initialized", json!({}));
let started = Instant::now();
let mut answered = 0;
while started.elapsed() < Duration::from_secs(240) {
while let Ok(message) = server.messages.try_recv() {
let is_a_request = message.get("method").is_some() && message.get("id").is_some();
if is_a_request {
server.answer(&message);
answered += 1;
}
}
std::thread::sleep(Duration::from_millis(50));
}
println!("the client answered {answered} request(s) from the server while it worked");
let log = server.log.lock().expect("the log is not poisoned").clone();
let lines: Vec<&str> = log.lines().collect();
println!("the server logged {} line(s); the last 30:", lines.len());
for line in lines.iter().rev().take(30).rev() {
println!(" {line}");
}
let mut repeated: std::collections::BTreeMap<String, usize> = std::collections::BTreeMap::new();
for line in &lines {
let shape = line
.split_whitespace()
.take(8)
.collect::<Vec<_>>()
.join(" ");
*repeated.entry(shape).or_insert(0) += 1;
}
let mut worst: Vec<(String, usize)> = repeated.into_iter().collect();
worst.sort_by_key(|entry| std::cmp::Reverse(entry.1));
println!("the most repeated line shapes:");
for (shape, count) in worst.iter().take(8) {
println!(" {count:>7} x {shape}");
}
}
#[test]
fn a_member_hover_is_resolved_through_the_objects_type() {
let project = Project::new("member-hover");
let source = "\
namespace std {
template <class T>
class vector {
public:
void push_back(T value);
};
}
void f() {
auto z = std::vector<int>();
z.push_back(1);
z.append(2);
}
";
project.write("a.cpp", source);
let mut server = Server::start(project.root());
let a_uri = uri_of(&project.root().join("a.cpp"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {},
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": a_uri, "languageId": "cpp", "version": 1, "text": source }
}),
);
let line_of = |needle: &str| -> u32 {
source
.lines()
.position(|line| line.contains(needle))
.expect("the fixture has the line") as u32
};
let mut hover_on = |id: i64, line: u32, character: u32| -> serde_json::Value {
let uri = a_uri.clone();
server.ask_until_it(
id,
move |_asked| {
json!({
"method": "textDocument/hover",
"params": {
"textDocument": { "uri": uri },
"position": { "line": line, "character": character },
},
})
},
|response| !response["result"].is_null(),
)
};
let member = hover_on(2, line_of("z.push_back"), 6);
let text = member["result"]["contents"]["value"]
.as_str()
.unwrap_or_else(|| panic!("a hover is expected: {member}"))
.to_string();
assert!(
text.contains("push_back"),
"the member is the answer: {text}"
);
let absent = hover_on(3, line_of("z.append"), 6);
let text = absent["result"]["contents"]["value"]
.as_str()
.unwrap_or_else(|| panic!("a hover is expected: {absent}"))
.to_string();
assert!(
text.contains("no member `append`") || text.contains("not known here"),
"an absent member is answered honestly: {text}"
);
assert!(
!text.contains("basic_string"),
"and never with another class's members: {text}"
);
}
#[test]
fn a_macro_hover_shows_its_own_define_line() {
let project = Project::new("macro-hover");
let source = "#define FULL_NAME_FORMAT \"Full name: {}\"\n#define STD std::\n\nint main() {\n STD c;\n return 0;\n}\n";
project.write("main.cpp", source);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": { "textDocument": { "hover": {} } },
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": source }
}),
);
let hovered = server.ask_until(100, |id| {
json!({
"id": id,
"method": "textDocument/hover",
"params": {
"textDocument": { "uri": main_uri },
"position": { "line": 4, "character": 5 },
},
})
});
let value = hovered["result"]["contents"]["value"]
.as_str()
.unwrap_or_else(|| panic!("the macro has a popup: {hovered}"));
println!("hover: {value}");
assert!(
value.contains("#define STD std::"),
"the popup is the macro's own definition line: {value}"
);
assert!(
!value.contains("int main"),
"and not a slice of the program starting somewhere else: {value}"
);
server.request(999, "shutdown", Value::Null);
server.notify("exit", Value::Null);
}
#[test]
fn the_call_hierarchy_answers_over_the_wire() {
let project = Project::new("call_hierarchy");
project.write(
"base.h",
"int add(int a, int b);\nint subtract(int a, int b);\n",
);
let main = "#include \"base.h\"\n\
int both(int a, int b) {\n return add(a, b) + subtract(a, b);\n}\n";
project.write("main.cpp", main);
let mut server = Server::start(project.root());
let main_uri = uri_of(&project.root().join("main.cpp"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": { "textDocument": { "callHierarchy": {} } },
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": main_uri, "languageId": "cpp", "version": 1, "text": main },
}),
);
let (line, character) = position_of(main, "both(");
let prepared = server.request(
2,
"textDocument/prepareCallHierarchy",
json!({
"textDocument": { "uri": main_uri },
"position": { "line": line, "character": character - 2 },
}),
);
println!("prepare: {prepared}");
let item = prepared["result"]
.as_array()
.and_then(|items| items.first())
.cloned()
.unwrap_or_else(|| panic!("the function is a call hierarchy item: {prepared}"));
assert_eq!(item["name"], "both");
let incoming = server.request(3, "callHierarchy/incomingCalls", json!({ "item": item }));
println!("incoming: {incoming}");
assert!(
!incoming["result"].is_null(),
"the request is answered: {incoming}"
);
let outgoing = server.request(4, "callHierarchy/outgoingCalls", json!({ "item": item }));
println!("outgoing: {outgoing}");
let names: Vec<&str> = outgoing["result"]
.as_array()
.map(|calls| {
calls
.iter()
.filter_map(|call| call["to"]["name"].as_str())
.collect()
})
.unwrap_or_default();
assert!(names.contains(&"add"), "{names:?}");
assert!(names.contains(&"subtract"), "{names:?}");
}
#[test]
fn a_request_with_unreadable_params_is_answered_with_invalid_params() {
let project = Project::new("invalid_params");
project.write("main.cpp", "int main() { return 0; }\n");
let mut server = Server::start(project.root());
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {},
}),
);
server.notify("initialized", json!({}));
let uri = uri_of(&project.root().join("main.cpp"));
let answer = server.request(
2,
"textDocument/prepareCallHierarchy",
json!({
"textDocument": { "uri": uri },
"position": { "position": { "line": 0, "character": 4 } },
}),
);
println!("answer: {answer}");
assert_eq!(
answer["error"]["code"].as_i64().map(|code| code as i32),
Some(lsp_server::ErrorCode::InvalidParams as i32),
"an unreadable request is InvalidParams, not silence: {answer}"
);
}
#[test]
fn a_cuda_project_completes_its_vocabulary_and_explains_it() {
let project = Project::new("cuda-vocabulary");
project.write(".cppls.toml", "[compile.cuda]\nenable = true\n");
let source = "__global__ void add(float *out) {\n unsigned i = threadIdx.x;\n __dev\n}\n";
project.write("kernels.cu", source);
let uri = uri_of(&project.root().join("kernels.cu"));
let mut server = Server::start(project.root());
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": { "window": { "workDoneProgress": true } },
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": uri, "languageId": "cuda-cpp", "version": 1, "text": source }
}),
);
let (line, character) = position_of(source, "threadId");
let hover = server.ask_until(100, |id| {
json!({
"id": id,
"method": "textDocument/hover",
"params": { "textDocument": { "uri": uri }, "position": { "line": line, "character": character } }
})
});
let text = hover["result"]["contents"]["value"]
.as_str()
.unwrap_or_default();
assert!(
text.contains("threadIdx"),
"the popup says what `threadIdx` is: {text:?}"
);
let (line, character) = position_of(source, "__dev");
let completion = server.ask_until(200, |id| {
json!({
"id": id,
"method": "textDocument/completion",
"params": {
"textDocument": { "uri": uri },
"position": { "line": line, "character": character },
"context": { "triggerKind": 1 }
}
})
});
let items = completion["result"]["items"]
.as_array()
.or_else(|| completion["result"].as_array())
.expect("a completion list");
let labels: Vec<&str> = items
.iter()
.filter_map(|item| item["label"].as_str())
.collect();
assert!(
labels.contains(&"__device__"),
"a CUDA project completes `__device__`: {labels:?}"
);
let shutdown = server.request(500, "shutdown", Value::Null);
assert_eq!(shutdown["result"], Value::Null);
server.notify("exit", Value::Null);
assert!(server.wait_for_exit());
}
#[test]
fn a_cpp_project_is_not_offered_cuda_vocabulary() {
let project = Project::new("cuda-vocabulary-off");
let source = "__global__ void add(float *out) { }\n";
project.write("kernels.cu", source);
let uri = uri_of(&project.root().join("kernels.cu"));
let mut server = Server::start(project.root());
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": { "window": { "workDoneProgress": true } },
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": uri, "languageId": "cpp", "version": 1, "text": source }
}),
);
let (line, character) = position_of(source, "__glo");
let completion = server.ask_until(100, |id| {
json!({
"id": id,
"method": "textDocument/completion",
"params": {
"textDocument": { "uri": uri },
"position": { "line": line, "character": character },
"context": { "triggerKind": 1 }
}
})
});
let items = completion["result"]["items"]
.as_array()
.cloned()
.or_else(|| completion["result"].as_array().cloned())
.expect("a completion list");
assert!(
!items
.iter()
.any(|item| item["label"].as_str() == Some("__global__")),
"a project that is not CUDA is not offered CUDA's vocabulary"
);
let shutdown = server.request(500, "shutdown", Value::Null);
assert_eq!(shutdown["result"], Value::Null);
server.notify("exit", Value::Null);
assert!(server.wait_for_exit());
}
#[test]
fn a_hover_inside_a_format_string_explains_the_part_it_is_on() {
const FIXTURE: &str = "\
#include <cstdio>
struct width { int value; };
void f(double d, int n) {
std::printf(\"%-8.2f|%zu\", d, n);
const char *plain = \"not a format string %d\";
(void)plain;
}
";
let project = Project::new("hover-format");
project.write("main.cpp", FIXTURE);
let mut server = Server::start(project.root());
let uri = uri_of(&project.root().join("main.cpp"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
},
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": uri, "languageId": "cpp", "version": 1, "text": FIXTURE }
}),
);
let at = |needle: &str, within: usize| -> (u32, u32) {
let line = line_of(FIXTURE, needle);
let offset = FIXTURE
.lines()
.nth(line as usize)
.and_then(|text| text.find(needle))
.unwrap_or_else(|| panic!("the fixture writes {needle:?}"))
+ within;
(line as u32, offset as u32)
};
let ask = |server: &mut Server, id: i64, (line, character): (u32, u32)| -> String {
let answer = server.ask_until(id, |id| {
json!({
"id": id,
"method": "textDocument/hover",
"params": {
"textDocument": { "uri": uri },
"position": { "line": line, "character": character },
},
})
});
answer["result"]["contents"]["value"]
.as_str()
.unwrap_or_default()
.to_string()
};
let text = ask(&mut server, 100, at("%-8.2f", 5));
assert!(
text.contains("fixed-point decimal"),
"the conversion's meaning: {text:?}"
);
let text = ask(&mut server, 101, at("%-8.2f", 2));
assert!(
text.contains("width") && text.contains('8'),
"the width: {text:?}"
);
assert!(
text.contains("left-aligned"),
"and the flag before it: {text:?}"
);
let text = ask(&mut server, 102, at("not a format string", 17));
assert!(
!text.contains("conversion"),
"a string nothing formats is left to the reader: {text:?}"
);
server.request(999, "shutdown", Value::Null);
server.notify("exit", Value::Null);
}
#[test]
fn a_completion_inside_a_format_string_is_the_grammar_s_own() {
const FIXTURE: &str = "\
#include <cstdio>
struct width { int value; };
void f(double d, int n) {
std::printf(\"%5.\", d);
}
";
let project = Project::new("completion-format");
project.write("main.cpp", FIXTURE);
let mut server = Server::start(project.root());
let uri = uri_of(&project.root().join("main.cpp"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
"textDocument": { "completion": { "completionItem": { "snippetSupport": true } } },
},
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": uri, "languageId": "cpp", "version": 1, "text": FIXTURE }
}),
);
let line = line_of(FIXTURE, "%5.");
let character = FIXTURE
.lines()
.nth(line as usize)
.and_then(|text| text.find("%5."))
.expect("the fixture writes it")
+ 3;
let ask = |server: &mut Server, id: i64| -> Vec<Value> {
let answer = server.ask_until(id, |id| {
json!({
"id": id,
"method": "textDocument/completion",
"params": {
"textDocument": { "uri": uri },
"position": { "line": line, "character": character },
},
})
});
answer["result"]["items"]
.as_array()
.or_else(|| answer["result"].as_array())
.cloned()
.unwrap_or_else(|| panic!("a completion list was expected, got {answer}"))
};
let items = ask(&mut server, 100);
assert!(!items.is_empty(), "a conversion list was expected");
let labels: Vec<&str> = items
.iter()
.filter_map(|item| item["label"].as_str())
.collect();
for conversion in ["d", "f", "s", "x", "p"] {
assert!(
labels.contains(&conversion),
"`{conversion}` is a conversion: {labels:?}"
);
}
assert!(
!labels.contains(&"printf"),
"and the names in scope are not offered here: {labels:?}"
);
let fixed = items
.iter()
.find(|item| item["label"] == json!("f"))
.expect("`f` is in the list");
assert!(
fixed["detail"]
.as_str()
.is_some_and(|detail| detail.contains("fixed-point")),
"the one-line meaning: {fixed}"
);
server.request(999, "shutdown", Value::Null);
server.notify("exit", Value::Null);
}
#[test]
fn the_c_macro_questions_the_editor_asks_are_answered() {
let project = Project::new("macro-questions");
project.write("macros.c", MACROS_C);
let mut server = Server::start(project.root());
let uri = uri_of(&project.root().join("macros.c"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
},
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": uri, "languageId": "c", "version": 1, "text": MACROS_C }
}),
);
let hover_at = |server: &mut Server, id: i64, needle: &str| -> Value {
let line = line_of(MACROS_C, needle);
let character = MACROS_C
.lines()
.nth(line as usize)
.and_then(|text| text.find(needle))
.expect("the fixture writes it");
server.request(
id,
"textDocument/hover",
json!({
"textDocument": { "uri": uri },
"position": { "line": line, "character": character },
}),
)
};
let predefined = hover_at(&mut server, 100, "__STDC_VERSION__");
let text = predefined["result"]["contents"]["value"]
.as_str()
.unwrap_or_else(|| panic!("`__STDC_VERSION__` has a hover: {predefined}"));
assert!(
text.contains("predefined") && text.contains("__STDC_VERSION__"),
"the compiler defines it, and the card says so: {text}"
);
let definition = hover_at(&mut server, 200, "CORPUS_STANDARD");
let text = definition["result"]["contents"]["value"]
.as_str()
.unwrap_or_else(|| panic!("`CORPUS_STANDARD` has a hover at its definition: {definition}"));
assert!(
text.contains("CORPUS_STANDARD"),
"the card names the macro: {text}"
);
let assertion = hover_at(&mut server, 300, "_Static_assert");
let text = assertion["result"]["contents"]["value"]
.as_str()
.unwrap_or_else(|| panic!("`_Static_assert` has a hover: {assertion}"));
assert!(
text.contains("_Static_assert") && text.contains("compile time"),
"the card explains the construct: {text}"
);
let line = line_of(MACROS_C, "SUM(selected, 1, 2, 3)");
let character = MACROS_C
.lines()
.nth(line as usize)
.and_then(|text| text.find("SUM"))
.expect("the fixture writes the call");
let jumped = server.ask_until(400, |id| {
json!({
"id": id,
"method": "textDocument/definition",
"params": {
"textDocument": { "uri": uri },
"position": { "line": line, "character": character },
},
})
});
let location = if jumped["result"].is_array() {
jumped["result"][0].clone()
} else {
jumped["result"].clone()
};
assert_eq!(
location["uri"].as_str().map(|found| found == uri),
Some(true),
"a macro's definition is the `#define` in this file: {jumped}"
);
assert_eq!(
location["range"]["start"]["line"].as_u64(),
Some(line_of(MACROS_C, "#define SUM(")),
"and the line it names is the directive's: {jumped}"
);
server.request(999, "shutdown", Value::Null);
server.notify("exit", Value::Null);
}
const MACROS_C: &str = "\
#ifdef __STDC_VERSION__
#define CORPUS_STANDARD __STDC_VERSION__
#else
#define CORPUS_STANDARD 0L
#endif
#define SUM(count, ...) (impl)(count __VA_OPT__(, ) __VA_ARGS__)
int impl(int count, ...);
int use(void) {
int total = 0;
int selected = 2;
int n = SUM(selected, 1, 2, 3);
_Static_assert(sizeof(int) >= 2, \"an int is at least two bytes\");
return n + total;
}
";
#[test]
fn a_c_reader_gets_completion_and_signature_help_for_what_the_file_defines() {
let project = Project::new("c-completion");
project.write("main.c", C_COMPLETION_C);
let mut server = Server::start(project.root());
let uri = uri_of(&project.root().join("main.c"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
},
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": uri, "languageId": "c", "version": 1, "text": C_COMPLETION_C }
}),
);
let at = |needle: &str, after: usize| -> (u64, u64) {
let line = line_of(C_COMPLETION_C, needle);
let character = C_COMPLETION_C
.lines()
.nth(line as usize)
.and_then(|text| text.find(needle))
.expect("the fixture writes it")
+ after;
(line, character as u64)
};
let (line, character) = at("local_helper(1)", 7);
let asked = server.ask_until(100, |id| {
json!({
"id": id,
"method": "textDocument/completion",
"params": {
"textDocument": { "uri": uri },
"position": { "line": line, "character": character },
},
})
});
let items = completion_items(&asked);
let labels: Vec<&str> = items
.iter()
.filter_map(|item| item["label"].as_str())
.collect();
assert!(
labels.contains(&"local_helper"),
"a function the file defines completes: {} item(s), first {:?}",
labels.len(),
labels.iter().take(10).collect::<Vec<_>>()
);
let (line, character) = at("LOCAL_MACRO(value)", 6);
let asked = server.ask_until(200, |id| {
json!({
"id": id,
"method": "textDocument/completion",
"params": {
"textDocument": { "uri": uri },
"position": { "line": line, "character": character },
},
})
});
let items = completion_items(&asked);
let labels: Vec<&str> = items
.iter()
.filter_map(|item| item["label"].as_str())
.collect();
assert!(
labels.contains(&"LOCAL_MACRO"),
"a macro the file defines completes: {} item(s), first {:?}",
labels.len(),
labels.iter().take(10).collect::<Vec<_>>()
);
let (line, character) = at("fprintf(stdout", 8);
let asked = server.ask_until(250, |id| {
json!({
"id": id,
"method": "textDocument/completion",
"params": {
"textDocument": { "uri": uri },
"position": { "line": line, "character": character },
},
})
});
let items = completion_items(&asked);
assert!(
items
.iter()
.any(|item| item["label"].as_str() == Some("fprintf")),
"with nothing typed, the budget still reaches `fprintf`: {} item(s)",
items.len()
);
let (line, character) = at("fprintf(stdout", 8);
let asked = server.ask_until(300, |id| {
json!({
"id": id,
"method": "textDocument/signatureHelp",
"params": {
"textDocument": { "uri": uri },
"position": { "line": line, "character": character },
},
})
});
let signatures = asked["result"]["signatures"]
.as_array()
.cloned()
.unwrap_or_default();
assert!(
!signatures.is_empty(),
"`fprintf` has a signature to show: {asked}"
);
let label = signatures[0]["label"].as_str().unwrap_or_default();
assert!(
label.contains("fprintf") && label.contains("format"),
"and it is `fprintf`'s own: {label}"
);
server.request(999, "shutdown", Value::Null);
server.notify("exit", Value::Null);
}
fn completion_items(answer: &Value) -> Vec<Value> {
if let Some(items) = answer["result"]["items"].as_array() {
return items.clone();
}
answer["result"].as_array().cloned().unwrap_or_default()
}
const C_COMPLETION_C: &str = "\
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#define LOCAL_MACRO(n) ((n) + 1)
static int local_helper(int n) { return n; }
int main(void) {
int value = local_helper(1);
fprintf(stdout, \"%d\\n\", value);
return LOCAL_MACRO(value);
}
";
#[test]
fn the_first_completion_after_an_open_answers_the_same_as_the_second() {
let project = Project::new("c-first-completion");
project.write("main.c", C_COMPLETION_C);
let mut server = Server::start(project.root());
let uri = uri_of(&project.root().join("main.c"));
server.request(
1,
"initialize",
json!({
"processId": std::process::id(),
"rootUri": uri_of(project.root()),
"capabilities": {
"workspace": { "configuration": true, "didChangeWatchedFiles": { "dynamicRegistration": true } },
"window": { "workDoneProgress": true },
},
}),
);
server.notify("initialized", json!({}));
server.notify(
"textDocument/didOpen",
json!({
"textDocument": { "uri": uri, "languageId": "c", "version": 1, "text": C_COMPLETION_C }
}),
);
let line = line_of(C_COMPLETION_C, "fprintf(stdout");
let labels_at = |server: &mut Server, id: i64, width: u32| -> Vec<String> {
let character = C_COMPLETION_C
.lines()
.nth(line as usize)
.and_then(|text| text.find("fprintf"))
.expect("the fixture writes it") as u32
+ width;
let answer = server.request(
id,
"textDocument/completion",
json!({
"textDocument": { "uri": uri },
"position": { "line": line, "character": character },
}),
);
let items = if let Some(items) = answer["result"]["items"].as_array() {
items.clone()
} else {
answer["result"].as_array().cloned().unwrap_or_default()
};
items
.iter()
.filter_map(|item| item["label"].as_str().map(str::to_string))
.collect()
};
let one = labels_at(&mut server, 100, 1);
let two = labels_at(&mut server, 200, 2);
assert!(
one.iter().any(|label| label == "fprintf"),
"the answer to `f` holds `fprintf` — {} item(s), first {:?}",
one.len(),
one.iter().take(8).collect::<Vec<_>>()
);
assert!(
two.iter().any(|label| label == "fprintf"),
"and so does the answer to `fp` — {} item(s)",
two.len()
);
server.request(999, "shutdown", Value::Null);
server.notify("exit", Value::Null);
}