use super::*;
pub(super) enum Req {
Init,
Goto,
Hover,
Symbol { name: String, then: Option<&'static str> },
}
fn req_for(method: &str) -> Req {
if method.ends_with("hover") { Req::Hover } else { Req::Goto }
}
impl App {
pub(super) fn lsp_start(&mut self) {
match Lsp::start(&self.root) {
Ok((l, id)) => {
self.lsp = Some(l);
self.inflight.insert(id, Req::Init);
}
Err(e) => self.msg = format!("{e:#}"),
}
}
fn lsp_revive(&mut self) {
if std::mem::take(&mut self.lsp_lost) {
self.lsp_start();
}
}
fn lsp_usable(&mut self) -> bool {
self.lsp_revive();
match &self.lsp {
None => self.msg = "rust-analyzer is not running".into(),
Some(_) if !self.lsp_ready => self.msg = "rust-analyzer is starting…".into(),
Some(_) => return true,
}
false
}
pub(super) fn lsp_request(&mut self, method: &'static str) {
let row = &self.rows[self.head.row];
if matches!(row.line.kind, Kind::Comment | Kind::Thread) || row.file == INFO {
return self.lsp_by_name(method);
}
let (Some(n), Some(f)) = (row.line.new, self.files.get(row.file)) else {
self.msg = "not on a new-side line".into();
return;
};
let (path, character) = (self.root.join(&f.path), lsp::utf16_col(&row.line.text, self.head.col));
self.ask_at(method, &path, n - 1, character);
}
pub(super) fn ask_at(&mut self, method: &'static str, path: &std::path::Path, line: u32, character: u32) {
if !self.lsp_usable() {
return;
}
let Some(l) = &mut self.lsp else { return };
let mut params = json!({
"textDocument": { "uri": lsp::uri(path) },
"position": { "line": line, "character": character },
});
if method.ends_with("references") {
params["context"] = json!({ "includeDeclaration": true });
}
match l.request(method, params) {
Ok(id) => {
let what = match method.rsplit('/').next() {
Some("definition") => "the definition",
Some("typeDefinition") => "the type",
Some("implementation") => "implementations",
Some("references") => "references",
_ => "",
};
self.msg = match what {
"" => "asking rust-analyzer…".into(),
w => format!("finding {w}…"),
};
self.inflight.insert(id, req_for(method));
}
Err(e) => self.msg = format!("rust-analyzer: {e}"),
}
}
fn lsp_by_name(&mut self, method: &'static str) {
let Some(name) = self.name_at_cursor() else { return self.msg = "no name under the cursor".into() };
let then = match method.rsplit('/').next() {
Some("references" | "implementation" | "hover") => Some(method),
_ => None, };
self.lsp_revive();
match &mut self.lsp {
Some(l) if self.lsp_ready => match l.request("workspace/symbol", json!({ "query": name })) {
Ok(id) => {
self.msg = format!("finding {name}…");
self.inflight.insert(id, Req::Symbol { name, then });
}
Err(e) => self.msg = format!("rust-analyzer: {e}"),
},
_ if then.is_none() => self.goto_definition(&name),
None => self.msg = "rust-analyzer is not running (Enter finds the definition by text)".into(),
Some(_) => self.msg = "rust-analyzer is starting… (Enter finds the definition by text meanwhile)".into(),
}
}
pub(super) fn lsp_poll(&mut self) {
let Some(l) = &self.lsp else { return };
let mut msgs = vec![];
let gone = loop {
match l.rx.try_recv() {
Ok(m) => msgs.push(m),
Err(std::sync::mpsc::TryRecvError::Empty) => break false,
Err(std::sync::mpsc::TryRecvError::Disconnected) => break true,
}
};
for m in msgs {
self.lsp_msg(m);
}
if gone && self.lsp.is_some() {
self.lsp_gone("rust-analyzer exited");
}
}
fn lsp_gone(&mut self, why: &str) {
let again = self.lsp_ready;
(self.lsp, self.lsp_ready, self.lsp_lost) = (None, false, again);
self.inflight.clear();
self.progress.clear();
self.msg = if again { format!("{why} (asked again, it starts again)") } else { why.into() };
}
pub(super) fn lsp_msg(&mut self, m: Value) {
let Some(l) = &mut self.lsp else { return };
match (m.get("id"), m["method"].as_str()) {
(Some(id), Some(_)) => _ = l.reply(id.clone()),
(None, Some("$/progress")) => {
let (tok, v) = (m["params"]["token"].to_string(), &m["params"]["value"]);
if v["kind"] == "end" {
self.progress.remove(&tok);
} else {
let e = self.progress.entry(tok).or_default();
if let Some(t) = v["title"].as_str() {
e.0 = t.to_owned();
}
e.1 = v["message"].as_str().unwrap_or("").to_owned();
}
}
(Some(id), None) => {
let Some(req) = id.as_u64().and_then(|id| self.inflight.remove(&id)) else { return };
if let Some(err) = m.get("error") {
let why = format!("rust-analyzer: {}", err["message"].as_str().unwrap_or("error"));
return match req {
Req::Init => self.lsp_gone(&why),
_ => self.msg = why,
};
}
let res = &m["result"];
if !matches!(req, Req::Init) {
self.msg.clear(); }
match req {
Req::Init => {
self.lsp_ready = true;
_ = l.notify("initialized", json!({}));
}
Req::Hover => match lsp::hover_text(res).trim().to_owned() {
t if t.is_empty() => self.msg = "no hover info".into(),
t => self.popup = Some((" hover j/k scroll ".into(), self.hover_lines(&t), 0)),
},
Req::Goto => self.show_locations(lsp::locations(res), None),
Req::Symbol { name, then } => {
let found: Vec<Value> = res.as_array().into_iter().flatten().filter(|s| s["name"] == name.as_str()).map(|s| s["location"].clone()).collect();
let locs = lsp::locations(&Value::Array(found));
match (then, locs.first()) {
(None, None) => self.goto_definition(&name),
(Some(_), None) => self.msg = format!("rust-analyzer doesn't know {name}"),
(then, _) => self.show_locations(locs, then),
}
}
}
}
_ => {}
}
}
pub(super) fn show_locations(&mut self, locs: Vec<Loc>, then: Option<&'static str>) {
match locs.as_slice() {
[] => self.msg = "no results".into(),
[l] => self.at_loc(l, then),
_ => {
let mut cache = HashMap::new();
let items = locs
.into_iter()
.map(|l| {
let mark = if self.loc_row(&l).is_some() { "●" } else { " " };
let shown = l.path.strip_prefix(&self.root).unwrap_or(&l.path).display().to_string();
let text = disk_line(&mut cache, &l.path, l.line);
let label = format!("{mark} {shown}:{} {}", l.line + 1, text.trim());
Item::new(label, Target::Loc(l, then))
})
.collect();
let title = match then.and_then(|m| m.rsplit('/').next()) {
Some("references") => " which one's references? ● = in diff ",
Some("implementation") => " which one's implementations? ● = in diff ",
Some(_) => " which one's hover? ● = in diff ",
None => " locations ● = in diff ",
};
self.picker = Some(Picker::new(title, items));
}
}
}
pub(super) fn at_loc(&mut self, l: &Loc, then: Option<&'static str>) {
match then {
Some(m) => self.ask_at(m, &l.path, l.line, l.character),
None => self.jump_loc(l),
}
}
pub(super) fn loc_row(&self, l: &Loc) -> Option<usize> {
let rel = l.path.strip_prefix(&self.root).ok()?.to_str()?;
let fi = self.files.iter().position(|f| f.path == rel)?;
self.rows.iter().position(|r| r.file == fi && r.line.new == Some(l.line + 1))
}
pub(super) fn jump_loc(&mut self, l: &Loc) {
let rel = l.path.strip_prefix(&self.root).ok().and_then(|p| p.to_str()).map(str::to_owned);
if self.loc_row(l).is_none()
&& let Some(p) = rel.filter(|p| self.files.iter().any(|f| &f.path == p))
{
self.show_file(&p, |a| a.loc_row(l).is_some());
}
match self.loc_row(l) {
Some(r) => {
let col = lsp::char_col(&self.rows[r].line.text, l.character);
self.jump(r, col);
}
None => {
let col = lsp::char_col(&disk_line(&mut HashMap::new(), &l.path, l.line), l.character);
self.action = Some(Action::Open(format!("{}:{}:{}", l.path.display(), l.line + 1, col + 1)));
}
}
}
}