mod analysis;
mod diagnostics;
mod schema_index;
mod text;
use std::collections::HashMap;
use anyhow::Result;
use lsp_server::{Connection, Message, Notification, Request, Response};
use lsp_types::notification::{
DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument, Notification as _,
};
use lsp_types::request::{Completion, HoverRequest, Request as _};
use lsp_types::{
CompletionOptions, CompletionParams, CompletionResponse, HoverParams, HoverProviderCapability,
PublishDiagnosticsParams, ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind,
Uri,
};
use serde_json::Value;
use self::schema_index::SchemaIndex;
use self::text::LineIndex;
pub(crate) fn run() -> Result<i32> {
let (connection, io_threads) = Connection::stdio();
let capabilities = serde_json::to_value(server_capabilities())?;
let _initialize_params = connection.initialize(capabilities)?;
let mut server = Server {
documents: HashMap::new(),
schema: SchemaIndex::build(),
};
server.serve(&connection)?;
drop(connection);
io_threads.join()?;
Ok(0)
}
fn server_capabilities() -> ServerCapabilities {
ServerCapabilities {
text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL)),
hover_provider: Some(HoverProviderCapability::Simple(true)),
completion_provider: Some(CompletionOptions {
trigger_characters: Some(
["[", "=", "\"", " ", ".", ","]
.iter()
.map(|s| (*s).to_string())
.collect(),
),
..CompletionOptions::default()
}),
..ServerCapabilities::default()
}
}
struct Server {
documents: HashMap<Uri, String>,
schema: SchemaIndex,
}
impl Server {
fn serve(&mut self, connection: &Connection) -> Result<()> {
for message in &connection.receiver {
match message {
Message::Request(request) => {
if connection.handle_shutdown(&request)? {
return Ok(());
}
self.handle_request(connection, request)?;
}
Message::Notification(notification) => {
self.handle_notification(connection, notification)?;
}
Message::Response(_) => {}
}
}
Ok(())
}
fn handle_request(&self, connection: &Connection, request: Request) -> Result<()> {
let method = request.method.clone();
let response = match method.as_str() {
HoverRequest::METHOD => {
let (id, params) = request.extract::<HoverParams>(HoverRequest::METHOD)?;
Response::new_ok(id, self.hover(¶ms))
}
Completion::METHOD => {
let (id, params) = request.extract::<CompletionParams>(Completion::METHOD)?;
Response::new_ok(id, CompletionResponse::Array(self.completion(¶ms)))
}
_ => Response::new_ok(request.id, Value::Null),
};
connection.sender.send(Message::Response(response))?;
Ok(())
}
fn handle_notification(
&mut self,
connection: &Connection,
notification: Notification,
) -> Result<()> {
let method = notification.method.clone();
match method.as_str() {
DidOpenTextDocument::METHOD => {
let params = notification
.extract::<lsp_types::DidOpenTextDocumentParams>(DidOpenTextDocument::METHOD)?;
let uri = params.text_document.uri;
self.documents
.insert(uri.clone(), params.text_document.text);
self.publish_diagnostics(connection, &uri);
}
DidChangeTextDocument::METHOD => {
let mut params = notification.extract::<lsp_types::DidChangeTextDocumentParams>(
DidChangeTextDocument::METHOD,
)?;
if let Some(change) = params.content_changes.pop() {
let uri = params.text_document.uri;
self.documents.insert(uri.clone(), change.text);
self.publish_diagnostics(connection, &uri);
}
}
DidCloseTextDocument::METHOD => {
let params = notification.extract::<lsp_types::DidCloseTextDocumentParams>(
DidCloseTextDocument::METHOD,
)?;
let uri = params.text_document.uri;
self.documents.remove(&uri);
send_diagnostics(connection, uri, Vec::new());
}
_ => {}
}
Ok(())
}
fn publish_diagnostics(&self, connection: &Connection, uri: &Uri) {
let diagnostics = self
.documents
.get(uri)
.filter(|_| is_runner_toml(uri))
.map(|text| diagnostics::compute(text, &LineIndex::new(text)))
.unwrap_or_default();
send_diagnostics(connection, uri.clone(), diagnostics);
}
fn hover(&self, params: &HoverParams) -> Option<lsp_types::Hover> {
let pos = params.text_document_position_params.position;
let uri = ¶ms.text_document_position_params.text_document.uri;
let text = self.documents.get(uri).filter(|_| is_runner_toml(uri))?;
analysis::hover(&LineIndex::new(text), &self.schema, text, pos)
}
fn completion(&self, params: &CompletionParams) -> Vec<lsp_types::CompletionItem> {
let pos = params.text_document_position.position;
let uri = ¶ms.text_document_position.text_document.uri;
let Some(text) = self.documents.get(uri).filter(|_| is_runner_toml(uri)) else {
return Vec::new();
};
analysis::completion(&LineIndex::new(text), &self.schema, text, pos)
}
}
fn send_diagnostics(connection: &Connection, uri: Uri, diagnostics: Vec<lsp_types::Diagnostic>) {
let params = PublishDiagnosticsParams {
uri,
diagnostics,
version: None,
};
if let Ok(params) = serde_json::to_value(params) {
let _ = connection.sender.send(Message::Notification(Notification {
method: lsp_types::notification::PublishDiagnostics::METHOD.to_string(),
params,
}));
}
}
fn is_runner_toml(uri: &Uri) -> bool {
uri.path()
.as_str()
.rsplit('/')
.next()
.is_some_and(|name| name == "runner.toml")
}