use holocron::{compile, HolocronError, Span};
use tokio::io::{stdin, stdout};
use tower_lsp::jsonrpc::Result;
use tower_lsp::lsp_types::{Diagnostic, OneOf};
use tower_lsp::lsp_types::{
DiagnosticSeverity, DidChangeTextDocumentParams, DidCloseTextDocumentParams,
DidOpenTextDocumentParams, InitializeParams, InitializeResult, InitializedParams, MessageType,
Position, Range, ServerCapabilities, ServerInfo, TextDocumentSyncCapability,
TextDocumentSyncKind, Url,
};
use tower_lsp::{Client, LanguageServer, LspService, Server};
const SERVER_NAME: &str = "holocron-lsp";
const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION");
const DIAGNOSTIC_SOURCE: &str = "holocron";
struct Backend {
client: Client,
}
#[tower_lsp::async_trait]
impl LanguageServer for Backend {
async fn initialize(&self, _: InitializeParams) -> Result<InitializeResult> {
Ok(InitializeResult {
server_info: Some(ServerInfo {
name: SERVER_NAME.to_string(),
version: Some(SERVER_VERSION.to_string()),
}),
capabilities: ServerCapabilities {
text_document_sync: Some(TextDocumentSyncCapability::Kind(
TextDocumentSyncKind::FULL,
)),
definition_provider: Some(OneOf::Left(false)),
..Default::default()
},
})
}
async fn initialized(&self, _: InitializedParams) {
self.client
.log_message(MessageType::INFO, format!("{SERVER_NAME} ready"))
.await;
}
async fn did_open(&self, params: DidOpenTextDocumentParams) {
self.client
.log_message(
MessageType::INFO,
format!(
"didOpen: {} (languageId={}, {} bytes)",
params.text_document.uri,
params.text_document.language_id,
params.text_document.text.len(),
),
)
.await;
self.publish(params.text_document.uri, ¶ms.text_document.text)
.await;
}
async fn did_change(&self, mut params: DidChangeTextDocumentParams) {
let Some(change) = params.content_changes.pop() else {
return;
};
self.client
.log_message(
MessageType::INFO,
format!(
"didChange: {} ({} bytes)",
params.text_document.uri,
change.text.len()
),
)
.await;
self.publish(params.text_document.uri, &change.text).await;
}
async fn did_close(&self, params: DidCloseTextDocumentParams) {
self.client
.log_message(
MessageType::INFO,
format!("didClose: {}", params.text_document.uri),
)
.await;
self.client
.publish_diagnostics(params.text_document.uri, Vec::new(), None)
.await;
}
async fn shutdown(&self) -> Result<()> {
Ok(())
}
}
impl Backend {
async fn publish(&self, uri: Url, source: &str) {
let diagnostics = match compile(source) {
Ok(_) => Vec::new(),
Err(error) => vec![diagnostic_for(&error, source)],
};
self.client
.log_message(
MessageType::INFO,
format!("publish: {} ({} diagnostic(s))", uri, diagnostics.len()),
)
.await;
self.client
.publish_diagnostics(uri, diagnostics, None)
.await;
}
}
fn diagnostic_for(error: &HolocronError, source: &str) -> Diagnostic {
let range = error
.span()
.map(|span| span_to_range(span, source))
.unwrap_or_else(|| Range::new(Position::new(0, 0), Position::new(0, 1)));
Diagnostic {
range,
severity: Some(DiagnosticSeverity::ERROR),
source: Some(DIAGNOSTIC_SOURCE.to_string()),
message: error.to_string(),
..Default::default()
}
}
fn span_to_range(span: Span, source: &str) -> Range {
Range {
start: offset_to_position(span.start, source),
end: offset_to_position(span.end, source),
}
}
fn offset_to_position(offset: usize, source: &str) -> Position {
let offset = offset.min(source.len());
let mut line: u32 = 0;
let mut line_start: usize = 0;
for (index, byte) in source.bytes().enumerate() {
if index >= offset {
break;
}
if byte == b'\n' {
line += 1;
line_start = index + 1;
}
}
let character = (offset - line_start) as u32;
Position { line, character }
}
#[tokio::main]
async fn main() {
let (service, socket) = LspService::new(|client| Backend { client });
Server::new(stdin(), stdout(), socket).serve(service).await;
}