use tower_lsp::jsonrpc::Result;
use tower_lsp::lsp_types::*;
use tower_lsp::{Client, LanguageServer, LspService, Server};
use tracing::info;
mod completion;
mod diagnostics;
mod includes;
use includes::{IncludedWord, LocalWord};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::RwLock;
struct DocumentState {
content: String,
file_path: Option<PathBuf>,
included_words: Vec<IncludedWord>,
local_words: Vec<LocalWord>,
}
struct SeqLanguageServer {
client: Client,
documents: RwLock<HashMap<String, DocumentState>>,
stdlib_path: Option<PathBuf>,
}
impl SeqLanguageServer {
fn new(client: Client) -> Self {
let stdlib_path = includes::find_stdlib_path();
if let Some(ref path) = stdlib_path {
info!("Found stdlib at: {}", path.display());
} else {
info!("Stdlib not found - include completions will be limited");
}
Self {
client,
documents: RwLock::new(HashMap::new()),
stdlib_path,
}
}
fn get_included_words(&self, uri: &str) -> Vec<IncludedWord> {
if let Ok(docs) = self.documents.read()
&& let Some(state) = docs.get(uri)
{
return state.included_words.clone();
}
Vec::new()
}
fn update_document(&self, uri: &str, content: String, file_path: Option<PathBuf>) {
let (includes, local_words) = includes::parse_document(&content);
info!(
"Parsed document: {} includes, {} local words, stdlib_path={:?}, file_path={:?}",
includes.len(),
local_words.len(),
self.stdlib_path,
file_path
);
let included_words = includes::resolve_includes(
&includes,
file_path.as_deref(),
self.stdlib_path.as_deref(),
);
info!(
"Document has {} local words, {} included words from {} includes",
local_words.len(),
included_words.len(),
includes.len()
);
let state = DocumentState {
content,
file_path,
included_words,
local_words,
};
if let Ok(mut docs) = self.documents.write() {
docs.insert(uri.to_string(), state);
}
}
}
#[tower_lsp::async_trait]
impl LanguageServer for SeqLanguageServer {
async fn initialize(&self, _: InitializeParams) -> Result<InitializeResult> {
info!("Seq LSP server initializing");
Ok(InitializeResult {
capabilities: ServerCapabilities {
text_document_sync: Some(TextDocumentSyncCapability::Kind(
TextDocumentSyncKind::FULL,
)),
completion_provider: Some(CompletionOptions {
trigger_characters: Some(vec![
" ".to_string(),
"\n".to_string(),
":".to_string(),
]),
..Default::default()
}),
hover_provider: Some(HoverProviderCapability::Simple(true)),
definition_provider: Some(OneOf::Left(true)),
..Default::default()
},
server_info: Some(ServerInfo {
name: "seq-lsp".to_string(),
version: Some(env!("CARGO_PKG_VERSION").to_string()),
}),
})
}
async fn initialized(&self, _: InitializedParams) {
info!("Seq LSP server initialized");
self.client
.log_message(MessageType::INFO, "Seq LSP server ready")
.await;
}
async fn shutdown(&self) -> Result<()> {
info!("Seq LSP server shutting down");
Ok(())
}
async fn did_open(&self, params: DidOpenTextDocumentParams) {
let uri = params.text_document.uri;
let text = params.text_document.text;
info!("Document opened: {}", uri);
let file_path = includes::uri_to_path(uri.as_str());
info!("File path: {:?}", file_path);
self.update_document(uri.as_str(), text.clone(), file_path);
let included_words = self.get_included_words(uri.as_str());
info!(
"Got {} included words for diagnostics",
included_words.len()
);
let diagnostics = diagnostics::check_document_with_includes(&text, &included_words);
self.client
.publish_diagnostics(uri, diagnostics, None)
.await;
}
async fn did_change(&self, params: DidChangeTextDocumentParams) {
let uri = params.text_document.uri;
if let Some(change) = params.content_changes.into_iter().next() {
let text = change.text;
info!("Document changed: {}", uri);
let file_path = if let Ok(docs) = self.documents.read() {
docs.get(uri.as_str()).and_then(|s| s.file_path.clone())
} else {
None
};
self.update_document(uri.as_str(), text.clone(), file_path);
let included_words = self.get_included_words(uri.as_str());
let diagnostics = diagnostics::check_document_with_includes(&text, &included_words);
self.client
.publish_diagnostics(uri, diagnostics, None)
.await;
}
}
async fn did_close(&self, params: DidCloseTextDocumentParams) {
let uri = params.text_document.uri;
info!("Document closed: {}", uri);
if let Ok(mut docs) = self.documents.write() {
docs.remove(uri.as_str());
}
self.client.publish_diagnostics(uri, vec![], None).await;
}
async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
let _uri = params.text_document_position_params.text_document.uri;
let _position = params.text_document_position_params.position;
Ok(None)
}
async fn goto_definition(
&self,
params: GotoDefinitionParams,
) -> Result<Option<GotoDefinitionResponse>> {
let _uri = params.text_document_position_params.text_document.uri;
let _position = params.text_document_position_params.position;
Ok(None)
}
async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
let uri = params.text_document_position.text_document.uri;
let position = params.text_document_position.position;
let (line_prefix, included_words, local_words) = if let Ok(docs) = self.documents.read() {
if let Some(state) = docs.get(uri.as_str()) {
let prefix = state
.content
.lines()
.nth(position.line as usize)
.map(|line| {
let end = (position.character as usize).min(line.len());
line[..end].to_string()
});
(
prefix,
state.included_words.clone(),
state.local_words.clone(),
)
} else {
(None, Vec::new(), Vec::new())
}
} else {
(None, Vec::new(), Vec::new())
};
let context = line_prefix
.as_ref()
.map(|prefix| completion::CompletionContext {
line_prefix: prefix,
included_words: &included_words,
local_words: &local_words,
});
let items = completion::get_completions(context);
Ok(Some(CompletionResponse::Array(items)))
}
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::from_default_env()
.add_directive("seq_lsp=info".parse().unwrap()),
)
.with_writer(std::io::stderr)
.init();
info!("Starting Seq LSP server");
let stdin = tokio::io::stdin();
let stdout = tokio::io::stdout();
let (service, socket) = LspService::new(SeqLanguageServer::new);
Server::new(stdin, stdout, socket).serve(service).await;
}