use std::collections::HashMap;
use std::io::{BufRead, BufReader, Write};
use std::process::{ChildStdin, Command, Stdio};
use std::sync::{Arc, Mutex};
use std::thread;
use glacier_dialect::{Mode, SourceMap, transpile_as};
use serde_json::{Value, json};
struct Doc {
rust_uri: String,
map: SourceMap,
}
#[derive(Default)]
struct State {
docs: HashMap<String, Doc>,
twins: HashMap<String, String>,
texts: HashMap<String, String>,
pending: HashMap<String, (String, String)>,
initialize_id: Option<String>,
warned_stale: bool,
root: Option<std::path::PathBuf>,
}
type Shared<T> = Arc<Mutex<T>>;
pub fn run() -> Result<(), String> {
let binary = std::env::var("GLACIER_RUST_ANALYZER").unwrap_or_else(|_| "rust-analyzer".into());
let mut child = Command::new(&binary)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.map_err(|e| format!("starting {binary}: {e}"))?;
let state: Shared<State> = Arc::default();
let server_in: Shared<ChildStdin> = Arc::new(Mutex::new(child.stdin.take().expect("piped")));
let client_out: Shared<std::io::Stdout> = Arc::new(Mutex::new(std::io::stdout()));
let server_out = child.stdout.take().expect("piped");
let (s, out) = (state.clone(), client_out.clone());
let from_server = thread::spawn(move || {
let mut reader = BufReader::new(server_out);
while let Some(mut message) = read_message(&mut reader) {
log(&json!({ "from_rust_analyzer": &message }));
from_rust_analyzer(&s, &mut message);
send(&out, &message);
}
});
let mut reader = BufReader::new(std::io::stdin());
while let Some(message) = read_message(&mut reader) {
let exit = message["method"] == "exit";
for forward in from_editor(&state, &client_out, message) {
send_to(&server_in, &forward);
}
if exit {
break;
}
}
drop(server_in);
let _ = child.wait();
let _ = from_server.join();
Ok(())
}
fn read_message(reader: &mut impl BufRead) -> Option<Value> {
let mut length = None;
loop {
let mut header = String::new();
if reader.read_line(&mut header).ok()? == 0 {
return None;
}
let header = header.trim_end();
if header.is_empty() {
break;
}
if let Some(value) = header.strip_prefix("Content-Length:") {
length = value.trim().parse::<usize>().ok();
}
}
let mut body = vec![0; length?];
reader.read_exact(&mut body).ok()?;
serde_json::from_slice(&body).ok()
}
fn frame(message: &Value) -> Vec<u8> {
let body = message.to_string();
let mut out = format!("Content-Length: {}\r\n\r\n", body.len()).into_bytes();
out.extend(body.into_bytes());
out
}
fn send(out: &Shared<std::io::Stdout>, message: &Value) {
let mut out = out.lock().unwrap();
let _ = out.write_all(&frame(message));
let _ = out.flush();
}
fn log(message: &Value) {
if let Ok(path) = std::env::var("GLACIER_LSP_LOG")
&& let Ok(mut file) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
{
let _ = writeln!(file, "{message}");
}
}
fn send_to(input: &Shared<ChildStdin>, message: &Value) {
log(message);
let mut input = input.lock().unwrap();
let _ = input.write_all(&frame(message));
let _ = input.flush();
}
fn is_glacier(uri: &str) -> bool {
uri.ends_with(".glacier")
}
fn rust_twin(uri: &str) -> String {
let path = uri.strip_prefix("file://").unwrap_or(uri);
match glacier_dialect::build::output_path(std::path::Path::new(path)) {
Some(output) => format!("file://{}", output.display()),
None => format!("{uri}.rs"),
}
}
fn closed_source(uri: &str) -> Option<(String, SourceMap)> {
let path = std::path::Path::new(uri.strip_prefix("file://")?);
let source = glacier_dialect::build::source_path(path)?;
let text = std::fs::read_to_string(&source).ok()?;
let name = source.file_name()?.to_string_lossy().into_owned();
let output = transpile_as(&text, &name, Mode::for_path(&source)).ok()?;
Some((format!("file://{}", source.display()), output.map))
}
fn id_key(id: &Value) -> String {
id.to_string()
}
fn from_editor(
state: &Shared<State>,
client: &Shared<std::io::Stdout>,
mut message: Value,
) -> Vec<Value> {
let method = message["method"].as_str().unwrap_or_default().to_owned();
let uri = message["params"]["textDocument"]["uri"]
.as_str()
.unwrap_or_default()
.to_owned();
if method == "initialize" {
let mut s = state.lock().unwrap();
s.initialize_id = Some(id_key(&message["id"]));
let root = message["params"]["rootUri"]
.as_str()
.or_else(|| message["params"]["workspaceFolders"][0]["uri"].as_str());
s.root = root.and_then(path_of);
return vec![message];
}
if method == "workspace/symbol" {
let root = state.lock().unwrap().root.clone();
let query = message["params"]["query"].as_str().unwrap_or_default();
send(
client,
&workspace_symbols(&message["id"], root.as_deref(), query),
);
return Vec::new();
}
if method.ends_with("/resolve") && !message["id"].is_null() {
let s = &mut *state.lock().unwrap();
if let Some(glacier_uri) = twin_in(&message["params"], &s.twins) {
s.pending
.insert(id_key(&message["id"]), (glacier_uri, method));
}
return vec![message];
}
if !is_glacier(&uri) {
return vec![message];
}
match method.as_str() {
"textDocument/didOpen" | "textDocument/didChange" => {
if method == "textDocument/didOpen" {
warn_if_stale(state, client, &uri);
}
let text = if method == "textDocument/didOpen" {
message["params"]["textDocument"]["text"].as_str()
} else {
message["params"]["contentChanges"]
.as_array()
.and_then(|c| c.last())
.and_then(|c| c["text"].as_str())
}
.unwrap_or_default()
.to_owned();
let version = message["params"]["textDocument"]["version"].clone();
let name = uri.rsplit('/').next().unwrap_or_default().to_owned();
let rust_uri = rust_twin(&uri);
let mode = Mode::for_path(std::path::Path::new(&uri));
state
.lock()
.unwrap()
.texts
.insert(uri.clone(), text.clone());
match transpile_as(&text, &name, mode) {
Ok(output) => {
let opening = {
let mut s = state.lock().unwrap();
let opening = !s.docs.contains_key(&uri);
s.twins.insert(rust_uri.clone(), uri.clone());
s.docs.insert(
uri.clone(),
Doc {
rust_uri: rust_uri.clone(),
map: output.map,
},
);
opening
};
let forward = if opening {
json!({"jsonrpc": "2.0", "method": "textDocument/didOpen", "params": {
"textDocument": {"uri": rust_uri, "languageId": "rust", "version": version, "text": output.rust}
}})
} else {
json!({"jsonrpc": "2.0", "method": "textDocument/didChange", "params": {
"textDocument": {"uri": rust_uri, "version": version},
"contentChanges": [{"text": output.rust}]
}})
};
vec![forward]
}
Err(error) => {
send(
client,
&json!({"jsonrpc": "2.0", "method": "textDocument/publishDiagnostics", "params": {
"uri": uri,
"diagnostics": [{
"range": {"start": {"line": error.line, "character": error.col},
"end": {"line": error.line, "character": error.col + 1}},
"severity": 1,
"source": "glacier",
"message": error.message,
"data": crate::analysis::glacier_error(&uri, &error),
}]
}}),
);
Vec::new()
}
}
}
"textDocument/didClose" | "textDocument/didSave" => {
let mut s = state.lock().unwrap();
let known = s.docs.contains_key(&uri);
if method == "textDocument/didClose" {
s.docs.remove(&uri);
s.texts.remove(&uri);
s.twins.remove(&rust_twin(&uri));
}
if !known {
return Vec::new();
}
message["params"]["textDocument"]["uri"] = Value::String(rust_twin(&uri));
vec![message]
}
"textDocument/documentSymbol" => {
let text = state
.lock()
.unwrap()
.texts
.get(&uri)
.cloned()
.unwrap_or_default();
let symbols = document_symbols(&crate::symbols::glacier_symbols(&text, &uri));
send(
client,
&json!({"jsonrpc": "2.0", "id": message["id"], "result": symbols}),
);
Vec::new()
}
"textDocument/formatting" => {
let text = state.lock().unwrap().texts.get(&uri).cloned();
send(client, &format_response(&message["id"], &uri, text));
Vec::new()
}
"textDocument/rangeFormatting" | "textDocument/onTypeFormatting" => {
send(
client,
&json!({"jsonrpc": "2.0", "id": message["id"], "result": null}),
);
Vec::new()
}
_ => {
let s = &mut *state.lock().unwrap();
let Some(doc) = s.docs.get(&uri) else {
if !message["id"].is_null() {
send(client, &unknown_document_response(&message["id"], &method));
}
return Vec::new();
};
message["params"]["textDocument"]["uri"] = Value::String(doc.rust_uri.clone());
if let Some(position) = message["params"].get_mut("position") {
map_position(position, &doc.map, true);
}
if let Some(range) = message["params"].get_mut("range") {
map_range(range, &doc.map, true);
}
if !message["id"].is_null() {
s.pending.insert(id_key(&message["id"]), (uri, method));
}
vec![message]
}
}
}
fn path_of(uri: &str) -> Option<std::path::PathBuf> {
let path = uri.strip_prefix("file://")?;
let decoded = path.replace("%20", " ");
Some(std::path::PathBuf::from(decoded))
}
fn symbol_information(symbol: &crate::symbols::Symbol, uri: &str) -> Value {
let kind = match symbol.kind {
"model" | "controller" | "serializer" | "mailer" | "client" | "middleware" => 5,
"module" => 2,
"service" => 2,
"field" => 8,
"association" => 7,
"callback" => 24,
"method" | "action" | "scope" | "changeset" => 6,
"job" | "struct" => 23,
"factory" => 9,
_ => 12,
};
let start = json!({"line": symbol.line - 1, "character": symbol.column - 1});
let end_line = symbol.end_line.unwrap_or(symbol.line) - 1;
let mut out = json!({
"name": symbol.qualified(),
"kind": kind,
"location": {"uri": uri, "range": {"start": start, "end": {"line": end_line, "character": 0}}},
});
if let Some(parent) = &symbol.parent {
out["containerName"] = parent.clone().into();
}
out
}
fn document_symbols(symbols: &[crate::symbols::Symbol]) -> Vec<Value> {
let item = |symbol: &crate::symbols::Symbol| {
let indent = symbol.column - 1;
let name_at = symbol
.text
.find(symbol.name.as_str())
.map_or(indent, |at| indent + at);
let line = symbol.line - 1;
let end_line = symbol.end_line.unwrap_or(symbol.line) - 1;
let information = symbol_information(symbol, "");
json!({
"name": symbol.name,
"kind": information["kind"],
"range": {
"start": {"line": line, "character": indent},
"end": {"line": end_line, "character": indent + 3},
},
"selectionRange": {
"start": {"line": line, "character": name_at},
"end": {"line": line, "character": name_at + symbol.name.len()},
},
"children": [],
})
};
let mut out: Vec<Value> = Vec::new();
for symbol in symbols {
let parent = symbol
.parent
.as_ref()
.and_then(|parent| out.iter_mut().find(|o| o["name"] == parent.as_str()));
match parent {
Some(parent) => parent["children"]
.as_array_mut()
.expect("children")
.push(item(symbol)),
None => out.push(item(symbol)),
}
}
out
}
fn workspace_symbols(id: &Value, root: Option<&std::path::Path>, query: &str) -> Value {
let query = query.to_lowercase();
let found: Vec<Value> = root
.map(crate::symbols::index)
.unwrap_or_default()
.iter()
.filter(|s| s.qualified().to_lowercase().contains(&query))
.take(500)
.map(|s| {
let uri = format!(
"file://{}",
root.unwrap_or(std::path::Path::new("."))
.join(&s.file)
.display()
);
symbol_information(s, &uri)
})
.collect();
json!({"jsonrpc": "2.0", "id": id, "result": found})
}
fn unknown_document_response(id: &Value, method: &str) -> Value {
let result = if method == "textDocument/diagnostic" {
json!({"kind": "full", "items": []})
} else {
Value::Null
};
json!({"jsonrpc": "2.0", "id": id, "result": result})
}
fn warn_if_stale(state: &Shared<State>, client: &Shared<std::io::Stdout>, uri: &str) {
let path = std::path::Path::new(uri.strip_prefix("file://").unwrap_or(uri));
let Some(project) = glacier_dialect::build::project_fingerprint(path) else {
return;
};
if project == glacier_dialect::FINGERPRINT {
return;
}
{
let mut s = state.lock().unwrap();
if s.warned_stale {
return;
}
s.warned_stale = true;
}
send(client, &stale_warning());
}
fn stale_warning() -> Value {
json!({"jsonrpc": "2.0", "method": "window/showMessage", "params": {
"type": 2,
"message": "This project was compiled by a different version of the .glacier dialect \
than the installed `glacier`, so the editor may not understand its syntax. \
Reinstall the CLI (`cargo install glacier-web-cli`, or \
`cargo install --path crates/glacier-cli` in a checkout) to match."
}})
}
fn format_response(id: &Value, uri: &str, text: Option<String>) -> Value {
let Some(text) = text else {
return json!({"jsonrpc": "2.0", "id": id, "result": null});
};
let mode = Mode::for_path(std::path::Path::new(uri));
match glacier_dialect::format::format(&text, mode) {
Ok(formatted) if formatted == text => json!({"jsonrpc": "2.0", "id": id, "result": []}),
Ok(formatted) => {
let lines = text.split('\n').count();
json!({"jsonrpc": "2.0", "id": id, "result": [{
"range": {"start": {"line": 0, "character": 0},
"end": {"line": lines, "character": 0}},
"newText": formatted,
}]})
}
Err(error) => json!({"jsonrpc": "2.0", "id": id, "error": {
"code": -32603,
"message": format!("can't format this file: {error}"),
}}),
}
}
fn from_rust_analyzer(state: &Shared<State>, message: &mut Value) {
let s = &mut *state.lock().unwrap();
if message.get("id").is_some() && message.get("method").is_none() {
let key = id_key(&message["id"]);
if s.initialize_id.as_deref() == Some(key.as_str()) {
let sync = &mut message["result"]["capabilities"]["textDocumentSync"];
if sync.is_object() {
sync["change"] = json!(1);
} else {
*sync = json!(1);
}
return;
}
if let Some((uri, method)) = s.pending.remove(&key) {
let context = s.docs.get(&uri).map(|d| &d.map);
tidy(&method, &mut message["result"], context);
remap(&mut message["result"], context, s);
}
return;
}
if message["method"] == "textDocument/publishDiagnostics" {
let rust_uri = message["params"]["uri"]
.as_str()
.unwrap_or_default()
.to_owned();
if let Some(uri) = s.twins.get(&rust_uri).cloned() {
message["params"]["uri"] = Value::String(uri.clone());
let map = s.docs.get(&uri).map(|d| &d.map);
remap(&mut message["params"]["diagnostics"], map, s);
}
}
}
fn twin_in(value: &Value, twins: &HashMap<String, String>) -> Option<String> {
match value {
Value::String(text) => twins.get(text).cloned(),
Value::Array(items) => items.iter().find_map(|v| twin_in(v, twins)),
Value::Object(object) => object.values().find_map(|v| twin_in(v, twins)),
_ => None,
}
}
fn tidy(method: &str, result: &mut Value, map: Option<&SourceMap>) {
match method {
"textDocument/codeAction" => *result = json!([]),
"textDocument/completion" | "completionItem/resolve" => {
let items = match result {
Value::Object(list) if list.contains_key("items") => &mut list["items"],
other => other,
};
match items {
Value::Array(items) => items.iter_mut().for_each(drop_imports),
item => drop_imports(item),
}
}
"textDocument/inlayHint" => {
if let (Value::Array(hints), Some(map)) = (result, map) {
hints.retain(|hint| {
let (Some(line), Some(col)) = (
hint["position"]["line"].as_u64(),
hint["position"]["character"].as_u64(),
) else {
return false;
};
map.to_glacier_exact(line as usize, col as usize).is_some()
});
}
}
_ => {}
}
}
fn drop_imports(item: &mut Value) {
if let Value::Object(item) = item {
item.remove("additionalTextEdits");
}
}
fn remap(value: &mut Value, context: Option<&SourceMap>, s: &State) {
match value {
Value::Object(object) => {
let mut context = context;
let mut closed: Option<SourceMap> = None;
for key in ["uri", "targetUri"] {
let Some(uri) = object.get(key).and_then(Value::as_str).map(str::to_owned) else {
continue;
};
match s.twins.get(&uri) {
Some(glacier_uri) => {
context = s.docs.get(glacier_uri).map(|d| &d.map);
object.insert(key.into(), Value::String(glacier_uri.clone()));
}
None => match closed_source(&uri) {
Some((glacier_uri, map)) => {
closed = Some(map);
object.insert(key.into(), Value::String(glacier_uri));
}
None => context = None,
},
}
}
if closed.is_some() {
context = closed.as_ref();
}
if is_range(object) {
if let Some(map) = context {
let mut range = Value::Object(std::mem::take(object));
map_range(&mut range, map, false);
if let Value::Object(mapped) = range {
*object = mapped;
}
}
return;
}
for (key, child) in object.iter_mut() {
match key.as_str() {
"data" => {}
"position" if is_position(child) => {
if let Some(map) = context {
map_position(child, map, false);
}
}
_ => remap(child, context, s),
}
}
if context.is_some() {
contain_selection(object);
}
}
Value::Array(items) => {
for item in items {
remap(item, context, s);
}
}
_ => {}
}
}
fn contain_selection(object: &mut serde_json::Map<String, Value>) {
let (Some(range), Some(selection)) = (object.get("range"), object.get("selectionRange")) else {
return;
};
let (Some(start), Some(end), Some(sel_start), Some(sel_end)) = (
point(&range["start"]),
point(&range["end"]),
point(&selection["start"]),
point(&selection["end"]),
) else {
return;
};
let (start, end) = (start.min(sel_start), end.max(sel_end));
object.insert(
"range".into(),
json!({"start": {"line": start.0, "character": start.1},
"end": {"line": end.0, "character": end.1}}),
);
}
fn point(position: &Value) -> Option<(u64, u64)> {
Some((position["line"].as_u64()?, position["character"].as_u64()?))
}
fn is_range(object: &serde_json::Map<String, Value>) -> bool {
object.len() == 2
&& object.get("start").is_some_and(is_position)
&& object.get("end").is_some_and(is_position)
}
fn is_position(value: &Value) -> bool {
value["line"].is_u64() && value["character"].is_u64()
}
fn map_range(range: &mut Value, map: &SourceMap, to_rust: bool) {
if !range.is_object() {
return;
}
map_position(&mut range["start"], map, to_rust);
map_position(&mut range["end"], map, to_rust);
}
fn map_position(position: &mut Value, map: &SourceMap, to_rust: bool) {
let (Some(line), Some(col)) = (position["line"].as_u64(), position["character"].as_u64())
else {
return;
};
let (line, col) = (line as usize, col as usize);
let mapped = if to_rust {
map.to_rust(line, col)
} else {
map.to_glacier(line, col)
};
position["character"] = json!(mapped);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn symbols_come_from_the_glacier_index() {
let dir = std::env::temp_dir().join(format!("glacier-lsp-symbols-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("src/models")).unwrap();
std::fs::write(
dir.join("src/models/post.glacier"),
"model Post do\n field title: String\n\n before_save do\n end\nend\n",
)
.unwrap();
let reply = workspace_symbols(&json!(7), Some(&dir), "post.TIT");
assert_eq!(reply["id"], 7);
let found = reply["result"].as_array().unwrap();
assert_eq!(found.len(), 1);
assert_eq!(found[0]["name"], "Post.title");
assert_eq!(found[0]["kind"], 8, "a field");
assert_eq!(found[0]["containerName"], "Post");
assert_eq!(
found[0]["location"]["range"]["start"],
json!({"line": 1, "character": 2})
);
assert!(
found[0]["location"]["uri"]
.as_str()
.unwrap()
.ends_with("src/models/post.glacier")
);
let all = workspace_symbols(&json!(8), Some(&dir), "");
let names: Vec<&str> = all["result"]
.as_array()
.unwrap()
.iter()
.map(|s| s["name"].as_str().unwrap())
.collect();
assert_eq!(names, ["Post", "Post.title", "Post.before_save"]);
assert_eq!(
path_of("file:///a%20b/c").unwrap(),
std::path::PathBuf::from("/a b/c")
);
std::fs::remove_dir_all(&dir).ok();
}
fn state_with(glacier: &str) -> State {
let uri = "file:///app/src/controllers/a.glacier".to_owned();
let rust_uri = "file:///app/.glacier/controllers/a.rs".to_owned();
let map = glacier_dialect::transpile(glacier, "a.glacier")
.unwrap()
.map;
let mut state = State::default();
state.twins.insert(rust_uri.clone(), uri.clone());
state.docs.insert(uri, Doc { rust_uri, map });
state
}
#[test]
fn inlay_hints_land_after_source_tokens() {
let glacier = "controller A do\n def x do\n posts = Post.all()!\n end\nend\n";
let s = state_with(glacier);
let map = &s.docs.values().next().unwrap().map;
let mut hints = json!([
{"position": {"line": 2, "character": 17}, "label": ": Vec<Post>"},
{"position": {"line": 2, "character": 5}, "label": "nonsense"},
]);
tidy("textDocument/inlayHint", &mut hints, Some(map));
remap(&mut hints, Some(map), &s);
assert_eq!(
hints,
json!([{"position": {"line": 2, "character": 9}, "label": ": Vec<Post>"}])
);
}
#[test]
fn opaque_data_and_rust_edits_stay_out_of_glacier_files() {
let s = state_with("controller A do\nend\n");
let twin = "file:///app/.glacier/controllers/a.rs";
let mut completion = json!({"items": [{
"label": "find",
"additionalTextEdits": [{"range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 0}}, "newText": "use x;\n"}],
"data": {"position": {"textDocument": {"uri": twin}, "position": {"line": 0, "character": 3}}},
}]});
tidy("textDocument/completion", &mut completion, None);
remap(&mut completion, None, &s);
assert!(completion["items"][0].get("additionalTextEdits").is_none());
assert_eq!(
completion["items"][0]["data"]["position"]["textDocument"]["uri"], twin,
"rust-analyzer needs its own data back to resolve the item"
);
assert_eq!(
twin_in(&completion["items"][0], &s.twins).as_deref(),
Some("file:///app/src/controllers/a.glacier")
);
let mut actions = json!([{"title": "Fill match arms", "data": {}}]);
tidy("textDocument/codeAction", &mut actions, None);
assert_eq!(actions, json!([]));
}
#[test]
fn symbol_names_stay_inside_their_ranges() {
let mut symbol = json!({
"name": "impl Posts",
"range": {"start": {"line": 2, "character": 20}, "end": {"line": 9, "character": 3}},
"selectionRange": {"start": {"line": 2, "character": 11}, "end": {"line": 2, "character": 16}},
});
contain_selection(symbol.as_object_mut().unwrap());
assert_eq!(
symbol["range"]["start"],
json!({"line": 2, "character": 11})
);
assert_eq!(symbol["range"]["end"], json!({"line": 9, "character": 3}));
let nested = json!({
"range": {"start": {"line": 1, "character": 0}, "end": {"line": 3, "character": 5}},
"selectionRange": {"start": {"line": 1, "character": 4}, "end": {"line": 1, "character": 8}},
});
let mut copy = nested.clone();
contain_selection(copy.as_object_mut().unwrap());
assert_eq!(copy, nested);
}
#[test]
fn documents_that_never_transpiled_stay_away_from_rust_analyzer() {
let state: Shared<State> = Arc::default();
let client = Arc::new(Mutex::new(std::io::stdout()));
let uri = "file:///app/src/controllers/broken.glacier";
let open = json!({"jsonrpc": "2.0", "method": "textDocument/didOpen", "params": {
"textDocument": {"uri": uri, "languageId": "glacier", "version": 1, "text": "def (\n"}
}});
assert!(from_editor(&state, &client, open).is_empty());
let hover = json!({"jsonrpc": "2.0", "id": 7, "method": "textDocument/hover", "params": {
"textDocument": {"uri": uri}, "position": {"line": 0, "character": 1}
}});
assert!(from_editor(&state, &client, hover).is_empty());
let close = json!({"jsonrpc": "2.0", "method": "textDocument/didClose", "params": {
"textDocument": {"uri": uri}
}});
assert!(from_editor(&state, &client, close).is_empty());
assert_eq!(
unknown_document_response(&json!(3), "textDocument/diagnostic")["result"]["kind"],
"full"
);
}
#[test]
fn warns_once_when_the_project_was_compiled_by_another_dialect() {
let app = std::env::temp_dir().join(format!("glacier-lsp-stale-{}", std::process::id()));
std::fs::create_dir_all(app.join("src/models")).unwrap();
std::fs::create_dir_all(app.join(".glacier")).unwrap();
std::fs::write(app.join(".glacier/.dialect"), "0000000000000000").unwrap();
let uri = format!("file://{}/src/models/post.glacier", app.display());
let state: Shared<State> = Arc::default();
let client = Arc::new(Mutex::new(std::io::stdout()));
warn_if_stale(&state, &client, &uri);
assert!(state.lock().unwrap().warned_stale);
std::fs::write(app.join(".glacier/.dialect"), glacier_dialect::FINGERPRINT).unwrap();
let fresh: Shared<State> = Arc::default();
warn_if_stale(&fresh, &client, &uri);
assert!(
!fresh.lock().unwrap().warned_stale,
"same dialect: no warning"
);
assert_eq!(stale_warning()["params"]["type"], 2);
std::fs::remove_dir_all(app).ok();
}
#[test]
fn formats_glacier_documents_whole() {
let id = json!(7);
let uri = "file:///app/src/controllers/a.glacier";
let messy = "controller A do\ndef x do\n1\nend\nend\n".to_owned();
let reply = format_response(&id, uri, Some(messy));
assert_eq!(reply["result"][0]["range"]["end"]["line"], 6);
assert_eq!(
reply["result"][0]["newText"],
"controller A do\n def x do\n 1\n end\nend\n"
);
let tidy = "controller A do\nend\n".to_owned();
assert_eq!(format_response(&id, uri, Some(tidy))["result"], json!([]));
let broken = "controller A do\n".to_owned();
let reply = format_response(&id, uri, Some(broken));
assert!(
reply["error"]["message"]
.as_str()
.unwrap()
.contains("missing its `end`")
);
let test = "test \"a\" do\nassert 1 == 1\nend\n".to_owned();
let reply = format_response(&id, "file:///app/tests/a.test.glacier", Some(test));
assert_eq!(
reply["result"][0]["newText"],
"test \"a\" do\n assert 1 == 1\nend\n"
);
}
}