A small write-up on hover diagnostics for the language I’m working on.
To start off with, let’s look at what the LSP gives us to use when you hover.
I’m using TowerLSP in Rust, so I have abstracted JSONRPC calls, like this:
async fn hover(&self, params: HoverParams) -> Result<Option<Hover>, Error> {
todo!()
}
Now, $\text{HoverParams}$ is a structure given by Tower in the $\text{LanguageServer}$ trait for the $hover$ function. What does it contain? Well…
pub struct HoverParams {
pub text_document_position_params: TextDocumentPositionParams,
pub work_done_progress_params: WorkDoneProgressParams,
}
For now, we can ignore $\text{work\_done\_progress\_params }$. $\text{text\_document\_position\_params}$ is what we actually need. It contains:
pub struct TextDocumentPositionParams {
pub text_document: TextDocumentIdentifier,
pub position: Position,
}
So, how do we use just the URI of a file and the position where you hovered to accurately find the type of the thing that was hovered and display it??? It seems like a preposterous question at first, but it’s actually very possible.
First, let’s look at how I would report diagnostics at all. I wrote a basic “fake-it-till-you-make-it” approach, where i call my compiler recursively from within the LSP with some special flags, then the compiler sees those flags and throws an error early with the diagnostics we want. Finally, I just read the $stderr$ from the command, parse it, and display it!
Observe:
use anyhow::Result;
use std::{env::set_current_dir, path::Path};
use tokio::process::Command;
use crate::misc::constants::SHORT_EXTENSION;
pub async fn get_file_output(path: &Path, extra_args: Option<Vec<&str>>) -> Result<String> {
// Don't call the compiler if the file isn't a .le file
if path
.extension()
.is_none_or(|ext| format!(".{}", ext.display()) != SHORT_EXTENSION)
{
return Err(anyhow::anyhow!("File must have the '.le' extension").into());
}
// Set the current dir to the file path before running the compiler
set_current_dir(path.parent().unwrap())
.unwrap_or_else(|err| panic!("Failed to set the current dir: {}", err));
// -c = compile only, don't generate an executable, allows for a `main` function to be omitted
// -x = diagnostics only, aka only generates diagnostics in the form of errors (if any) instead of compiling
// -Wall = enable all warnings
// --noclr = disable all colors
let mut args = vec!["-c", "-x", "-Wall", "--noclr"];
// This is necessary because we're gonna put more args here for the hover LSP
if let Some(extra_args) = extra_args {
args.extend(extra_args);
}
args.push(path.to_str().unwrap());
let output = Command::new("ellec")
.args(args)
.stderr(std::process::Stdio::piped())
.output()
.await?;
dbg!(&output);
// Simply take the stderr from `ellec`
Ok(String::from_utf8_lossy(&output.stderr).to_string())
}
Now, if we take that position that the LSP gives us and we parse it to a format like “<row>:<col>”, we can accept it into the compiler via a flag and do something with it.
So, let’s do that:
pub async fn try_report_hover(&self, uri: &Url, pos: Position) -> Option<Hover> {
let path = uri.to_file_path().ok()?;
let output = get_file_output(
&path,
// Pass along the position where the user hovered.
Some(vec!["-i", &format!("{}:{}", pos.line, pos.character)]),
)
.await
.ok()?;
let hover_info = get_hover_info(&output);
dbg!(&hover_info);
hover_info
}
Alright, now on the compiler side: