#![allow(clippy::mutable_key_type)]
use std::collections::{HashMap, HashSet, VecDeque};
use std::path::{Path as FsPath, PathBuf};
use lsp_server::{Connection, Message, RequestId, Response, ResponseError};
use lsp_types::{
notification::{
DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument, DidSaveTextDocument, Notification as _,
PublishDiagnostics,
},
request::{Completion, GotoDefinition, HoverRequest, Request as _, SemanticTokensFullRequest},
CompletionItem, CompletionItemKind, CompletionOptions, CompletionParams, CompletionResponse, Diagnostic,
DiagnosticSeverity, GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams,
HoverProviderCapability, InitializeParams, Location, MarkedString, OneOf, Position, PublishDiagnosticsParams,
Range, SemanticToken, SemanticTokenType, SemanticTokens, SemanticTokensFullOptions, SemanticTokensLegend,
SemanticTokensOptions, SemanticTokensParams, SemanticTokensServerCapabilities, ServerCapabilities,
TextDocumentSyncCapability, TextDocumentSyncKind, Uri, WorkDoneProgressOptions,
};
use typelisp::check::core;
use typelisp::check::semantic::{encode, file_type_tokens, TypeKind, TypeToken};
use typelisp::project::{find_src_root, module_segs_for, Loader};
use typelisp::*;
fn main() {
let (connection, io_threads) = Connection::stdio();
run(connection);
io_threads.join().expect("LSP I/O threads panicked");
}
const SEMANTIC_TOKEN_TYPES: [SemanticTokenType; 3] =
[SemanticTokenType::STRUCT, SemanticTokenType::ENUM, SemanticTokenType::INTERFACE];
fn semantic_token_index(kind: TypeKind) -> u32 {
match kind {
TypeKind::Struct => 0,
TypeKind::Enum => 1,
TypeKind::Trait => 2,
}
}
fn run(connection: Connection) {
let capabilities = ServerCapabilities {
text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL)),
hover_provider: Some(HoverProviderCapability::Simple(true)),
definition_provider: Some(OneOf::Left(true)),
completion_provider: Some(CompletionOptions { trigger_characters: Some(vec![":".to_string()]), ..Default::default() }),
semantic_tokens_provider: Some(SemanticTokensServerCapabilities::SemanticTokensOptions(
SemanticTokensOptions {
work_done_progress_options: WorkDoneProgressOptions::default(),
legend: SemanticTokensLegend {
token_types: SEMANTIC_TOKEN_TYPES.to_vec(),
token_modifiers: Vec::new(),
},
range: Some(false),
full: Some(SemanticTokensFullOptions::Bool(true)),
},
)),
..Default::default()
};
let init_params = connection
.initialize(serde_json::to_value(&capabilities).expect("ServerCapabilities always serializes"))
.expect("LSP initialize handshake failed");
let _params: InitializeParams =
serde_json::from_value(init_params).unwrap_or_else(|_| InitializeParams::default());
let mut docs: HashMap<Uri, String> = HashMap::new();
let mut deps: HashMap<Uri, HashSet<PathBuf>> = HashMap::new();
let mut analyses: HashMap<Uri, Analysis> = HashMap::new();
for msg in &connection.receiver {
match msg {
Message::Request(req) => {
if connection.handle_shutdown(&req).unwrap_or(true) {
break;
}
let resp = if req.method == HoverRequest::METHOD {
handle_hover(req.id, req.params, &analyses)
} else if req.method == GotoDefinition::METHOD {
handle_goto_definition(req.id, req.params, &analyses)
} else if req.method == Completion::METHOD {
handle_completion(req.id, req.params, &docs)
} else if req.method == SemanticTokensFullRequest::METHOD {
handle_semantic_tokens(req.id, req.params, &analyses)
} else {
Response {
id: req.id,
result: None,
error: Some(ResponseError {
code: lsp_server::ErrorCode::MethodNotFound as i32,
message: format!("unsupported request: {}", req.method),
data: None,
}),
}
};
if connection.sender.send(Message::Response(resp)).is_err() {
break;
}
}
Message::Notification(not) => match not.method.as_str() {
m if m == DidOpenTextDocument::METHOD => {
if let Ok(p) = serde_json::from_value::<lsp_types::DidOpenTextDocumentParams>(not.params) {
let uri = p.text_document.uri;
docs.insert(uri.clone(), p.text_document.text);
publish(&connection, &uri, &docs, &mut deps, &mut analyses);
}
}
m if m == DidChangeTextDocument::METHOD => {
if let Ok(mut p) = serde_json::from_value::<lsp_types::DidChangeTextDocumentParams>(not.params) {
if let Some(change) = p.content_changes.pop() {
let uri = p.text_document.uri;
docs.insert(uri.clone(), change.text);
publish(&connection, &uri, &docs, &mut deps, &mut analyses);
}
}
}
m if m == DidSaveTextDocument::METHOD => {
if let Ok(p) = serde_json::from_value::<lsp_types::DidSaveTextDocumentParams>(not.params) {
let uri = p.text_document.uri;
if docs.contains_key(&uri) {
publish(&connection, &uri, &docs, &mut deps, &mut analyses);
}
}
}
m if m == DidCloseTextDocument::METHOD => {
if let Ok(p) = serde_json::from_value::<lsp_types::DidCloseTextDocumentParams>(not.params) {
let uri = p.text_document.uri;
docs.remove(&uri);
deps.remove(&uri);
analyses.remove(&uri);
let params = PublishDiagnosticsParams { uri, diagnostics: Vec::new(), version: None };
let n = lsp_server::Notification::new(PublishDiagnostics::METHOD.into(), params);
let _ = connection.sender.send(Message::Notification(n));
}
}
"exit" => break,
_ => {}
},
Message::Response(_) => {}
}
}
}
fn publish(
connection: &Connection,
uri: &Uri,
docs: &HashMap<Uri, String>,
deps: &mut HashMap<Uri, HashSet<PathBuf>>,
analyses: &mut HashMap<Uri, Analysis>,
) {
let mut visited: HashSet<Uri> = HashSet::new();
let mut queue: VecDeque<Uri> = VecDeque::new();
visited.insert(uri.clone());
queue.push_back(uri.clone());
while let Some(cur) = queue.pop_front() {
publish_one(connection, &cur, docs, deps, analyses);
let cur_path = PathBuf::from(cur.path().as_str());
for other in docs.keys() {
if visited.contains(other) {
continue;
}
if deps.get(other).is_some_and(|d| d.contains(&cur_path)) {
visited.insert(other.clone());
queue.push_back(other.clone());
}
}
}
}
fn build_overlay(docs: &HashMap<Uri, String>, exclude: &Uri) -> HashMap<PathBuf, String> {
docs.iter()
.filter(|(u, _)| *u != exclude)
.map(|(u, text)| (PathBuf::from(u.path().as_str()), text.clone()))
.collect()
}
fn publish_one(
connection: &Connection,
uri: &Uri,
docs: &HashMap<Uri, String>,
deps: &mut HashMap<Uri, HashSet<PathBuf>>,
analyses: &mut HashMap<Uri, Analysis>,
) {
let overlay = build_overlay(docs, uri);
let text = &docs[uri];
let (diagnostics, loaded, analysis) = diagnostics_for(uri.path().as_str(), text, overlay);
deps.insert(uri.clone(), loaded);
if let Some(a) = analysis {
analyses.insert(uri.clone(), a);
}
let params = PublishDiagnosticsParams { uri: uri.clone(), diagnostics, version: None };
let n = lsp_server::Notification::new(PublishDiagnostics::METHOD.into(), params);
let _ = connection.sender.send(Message::Notification(n));
}
fn diagnostics_for(
file: &str,
text: &str,
overlay: HashMap<PathBuf, String>,
) -> (Vec<Diagnostic>, HashSet<PathBuf>, Option<Analysis>) {
let CheckedDocument { heap, checker, mut loader, result, .. } = check_document(file, text, overlay);
let mut diagnostics = Vec::new();
for w in checker.take_warnings() {
diagnostics.push(warning_diagnostic(w));
}
for e in checker.take_errors() {
diagnostics.push(error_diagnostic(&e, file));
}
let analysis = if result.is_ok() {
let body = module_body(&heap, loader.take_pending().pop());
let def_locs = checker.registry().def_locs.clone();
let docs = checker.registry().docs.clone();
let tokens = file_type_tokens(&checker.take_type_uses(), file);
Some(Analysis { file: file.to_string(), heap, body, def_locs, docs, tokens })
} else {
None
};
if let Err(e) = result {
diagnostics.push(error_diagnostic(&e, file));
}
(diagnostics, loader.loaded_files().clone(), analysis)
}
struct CheckedDocument {
heap: Heap,
checker: Checker,
loader: Loader,
src_root: PathBuf,
result: Result<(), Error>,
}
fn check_document(file: &str, text: &str, overlay: HashMap<PathBuf, String>) -> CheckedDocument {
let mut heap = Heap::with_capacity(1 << 16);
let reader = Reader::new();
let mut checker = Checker::new();
let mut interp = Interp::new();
load_prelude(&mut heap, &mut checker, &mut interp);
checker.set_recover(true);
let fs_file = FsPath::new(file);
let dir = fs_file.parent().filter(|p| !p.as_os_str().is_empty()).map(FsPath::to_path_buf).unwrap_or_else(|| PathBuf::from("."));
let src_root = find_src_root(&dir).unwrap_or(dir);
let mut loader = Loader::new(src_root.clone());
loader.set_overlay(overlay);
let result = loader.load_entry_src(&mut heap, &reader, &mut checker, &mut interp, fs_file, text);
CheckedDocument { heap, checker, loader, src_root, result }
}
struct Analysis {
file: String,
heap: Heap,
body: Vec<TopLevelForm>,
def_locs: DefLocs,
docs: Docs,
tokens: Vec<TypeToken>,
}
fn handle_semantic_tokens(
id: RequestId,
params: serde_json::Value,
analyses: &HashMap<Uri, Analysis>,
) -> Response {
let result = (|| {
let p: SemanticTokensParams = serde_json::from_value(params).ok()?;
let uri = p.text_document.uri;
let analysis = analyses.get(&uri)?;
let data = encode(&analysis.tokens, semantic_token_index)
.chunks_exact(5)
.map(|c| SemanticToken {
delta_line: c[0],
delta_start: c[1],
length: c[2],
token_type: c[3],
token_modifiers_bitset: c[4],
})
.collect();
let result = SemanticTokens { result_id: None, data };
Some(serde_json::to_value(result).expect("SemanticTokens always serializes"))
})();
Response { id, result, error: None }
}
fn handle_hover(id: RequestId, params: serde_json::Value, analyses: &HashMap<Uri, Analysis>) -> Response {
let result = (|| {
let p: HoverParams = serde_json::from_value(params).ok()?;
let uri = p.text_document_position_params.text_document.uri;
let pos = p.text_document_position_params.position;
let analysis = analyses.get(&uri)?;
let heap = &analysis.heap;
let node = locate_node(heap, &analysis.body, &analysis.file, pos.line + 1, pos.character + 1)?;
let range = heap.cons_loc(node).filter(|l| !l.is_degenerate()).map(|l| loc_to_range(&l));
let text = hover_text(heap, node, &analysis.def_locs, &analysis.docs);
let hover = Hover { contents: HoverContents::Scalar(MarkedString::String(text)), range };
Some(serde_json::to_value(hover).expect("Hover always serializes"))
})();
Response { id, result, error: None }
}
fn handle_goto_definition(id: RequestId, params: serde_json::Value, analyses: &HashMap<Uri, Analysis>) -> Response {
let result = (|| {
let p: GotoDefinitionParams = serde_json::from_value(params).ok()?;
let uri = p.text_document_position_params.text_document.uri;
let pos = p.text_document_position_params.position;
let analysis = analyses.get(&uri)?;
let heap = &analysis.heap;
let node = locate_node(heap, &analysis.body, &analysis.file, pos.line + 1, pos.character + 1)?;
let target = definition_target(heap, node, &analysis.def_locs)?;
let target_uri = if target.file.as_ref() == analysis.file.as_str() {
uri
} else {
format!("file://{}", target.file).parse::<Uri>().ok()?
};
let resp = GotoDefinitionResponse::Scalar(Location::new(target_uri, loc_to_range(&target)));
Some(serde_json::to_value(resp).expect("GotoDefinitionResponse always serializes"))
})();
Response { id, result, error: None }
}
fn handle_completion(
id: RequestId,
params: serde_json::Value,
docs: &HashMap<Uri, String>,
) -> Response {
let result = (|| {
let p: CompletionParams = serde_json::from_value(params).ok()?;
let uri = p.text_document_position.text_document.uri.clone();
let pos = p.text_document_position.position;
let text = docs.get(&uri)?;
let chars: Vec<char> = text.chars().collect();
let offset = char_offset(&chars, pos);
let prefix_start = prefix_start(&chars, offset);
let prefix: String = chars[prefix_start..offset].iter().collect::<String>().to_lowercase();
let mut truncated: String = chars[..prefix_start].iter().collect();
let (line, col) = line_col_at(&chars, prefix_start);
if needs_completion_placeholder(&truncated) {
truncated.push_str("(panic \"\")");
}
let patched = heuristically_close(&truncated);
let overlay = build_overlay(docs, &uri);
let candidates = candidates_for(uri.path().as_str(), &patched, overlay, line, col);
let items: Vec<CompletionItem> = candidates
.into_iter()
.filter(|c| prefix.is_empty() || c.name.starts_with(&prefix))
.map(|c| CompletionItem {
label: c.name,
kind: Some(completion_item_kind(c.kind)),
detail: Some(c.detail),
..Default::default()
})
.collect();
Some(serde_json::to_value(CompletionResponse::Array(items)).expect("CompletionResponse always serializes"))
})();
Response { id, result, error: None }
}
fn candidates_for(
file: &str,
patched_text: &str,
overlay: HashMap<PathBuf, String>,
line: u32,
col: u32,
) -> Vec<CompletionCandidate> {
let CheckedDocument { heap, checker, mut loader, src_root, .. } = check_document(file, patched_text, overlay);
let module_path = module_segs_for(FsPath::new(file), &src_root).unwrap_or_default();
let mut candidates = completion_candidates(checker.registry(), &module_path);
let body = module_body(&heap, loader.take_pending().pop());
for name in completion_locals(&heap, &body, file, line, col) {
candidates.push(CompletionCandidate { name, kind: CompletionKind::Variable, detail: "local".to_string() });
}
candidates
}
fn module_body(heap: &Heap, last: Option<TopLevelForm>) -> Vec<TopLevelForm> {
match last {
Some(tl) if core::op_is(heap, tl, typelisp::wk::MODULE) => {
core::fields(heap, tl).map(|f| f[1..].to_vec()).unwrap_or_default()
}
Some(other) => vec![other],
None => Vec::new(),
}
}
fn line_col_at(chars: &[char], offset: usize) -> (u32, u32) {
let mut line = 1u32;
let mut col = 1u32;
for &c in &chars[..offset.min(chars.len())] {
if c == '\n' {
line += 1;
col = 1;
} else {
col += 1;
}
}
(line, col)
}
fn completion_item_kind(kind: CompletionKind) -> CompletionItemKind {
match kind {
CompletionKind::Function => CompletionItemKind::FUNCTION,
CompletionKind::Method => CompletionItemKind::METHOD,
CompletionKind::Type => CompletionItemKind::CLASS,
CompletionKind::Variable => CompletionItemKind::VARIABLE,
CompletionKind::Macro => CompletionItemKind::KEYWORD,
CompletionKind::Trait => CompletionItemKind::INTERFACE,
CompletionKind::Module => CompletionItemKind::MODULE,
}
}
fn char_offset(chars: &[char], pos: Position) -> usize {
let mut offset = 0usize;
let mut line = 0u32;
let mut col = 0u32;
for (i, &c) in chars.iter().enumerate() {
if line == pos.line {
offset = i + (pos.character.saturating_sub(col)) as usize;
break;
}
if c == '\n' {
line += 1;
col = 0;
} else {
col += 1;
}
offset = i + 1;
}
offset.min(chars.len())
}
fn prefix_start(chars: &[char], offset: usize) -> usize {
let mut i = offset;
while i > 0 {
let c = chars[i - 1];
if c.is_whitespace() || matches!(c, '(' | ')' | '"' | '\'' | '`' | ';') {
break;
}
i -= 1;
}
i
}
fn needs_completion_placeholder(truncated: &str) -> bool {
!matches!(truncated.trim_end().chars().last(), None | Some('('))
}
fn heuristically_close(text: &str) -> String {
let chars: Vec<char> = text.chars().collect();
let mut i = 0usize;
let mut depth: i32 = 0;
while i < chars.len() {
match chars[i] {
';' => {
while i < chars.len() && chars[i] != '\n' {
i += 1;
}
}
'"' => {
i += 1;
while i < chars.len() && chars[i] != '"' {
if chars[i] == '\\' {
i += 1;
}
i += 1;
}
i += 1;
}
'#' if chars.get(i + 1) == Some(&'|') => {
i += 2;
let mut nesting = 1;
while i < chars.len() && nesting > 0 {
if chars[i] == '#' && chars.get(i + 1) == Some(&'|') {
nesting += 1;
i += 2;
} else if chars[i] == '|' && chars.get(i + 1) == Some(&'#') {
nesting -= 1;
i += 2;
} else {
i += 1;
}
}
}
'#' if chars.get(i + 1) == Some(&'\\') => {
i += 2;
if i < chars.len() {
let first = chars[i];
i += 1;
if first.is_alphabetic() {
while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '-') {
i += 1;
}
}
}
}
'(' => {
depth += 1;
i += 1;
}
')' => {
depth -= 1;
i += 1;
}
_ => i += 1,
}
}
let mut out = text.to_string();
for _ in 0..depth.max(0) {
out.push(')');
}
out
}
fn error_diagnostic(e: &Error, current_file: &str) -> Diagnostic {
match e.loc() {
Some(l) if &*l.file == current_file => {
diagnostic(format!("{}", e.kind()), loc_to_range(l), DiagnosticSeverity::ERROR)
}
Some(_) => diagnostic(format!("{}", e), doc_start_range(), DiagnosticSeverity::ERROR),
None => diagnostic(format!("{}", e.kind()), doc_start_range(), DiagnosticSeverity::ERROR),
}
}
fn warning_diagnostic(message: String) -> Diagnostic {
diagnostic(message, doc_start_range(), DiagnosticSeverity::WARNING)
}
fn diagnostic(message: String, range: Range, severity: DiagnosticSeverity) -> Diagnostic {
Diagnostic { range, severity: Some(severity), source: Some("typelisp".into()), message, ..Default::default() }
}
fn loc_to_range(loc: &Loc) -> Range {
let line = loc.line.saturating_sub(1);
let col = loc.col.saturating_sub(1);
if loc.is_degenerate() {
return Range::new(Position::new(line, col), Position::new(line, col + 1));
}
Range::new(
Position::new(line, col),
Position::new(loc.end_line.saturating_sub(1), loc.end_col.saturating_sub(1)),
)
}
fn doc_start_range() -> Range {
Range::new(Position::new(0, 0), Position::new(0, 1))
}
#[cfg(test)]
mod completion_helper_tests {
use super::*;
#[test]
fn heuristically_close_appends_missing_parens() {
assert_eq!(heuristically_close("(defun f () i32 (+ 1 "), "(defun f () i32 (+ 1 ))");
assert_eq!(heuristically_close("(defun f () i32 1)"), "(defun f () i32 1)");
}
#[test]
fn heuristically_close_ignores_parens_inside_a_string() {
assert_eq!(heuristically_close(r#"(defun f () str (str-new "("#), "(defun f () str (str-new \"(".to_string() + "))");
}
#[test]
fn heuristically_close_ignores_parens_inside_a_char_literal() {
assert_eq!(heuristically_close(r"(defun f () char #\("), r"(defun f () char #\()");
}
#[test]
fn heuristically_close_ignores_parens_inside_a_line_comment() {
assert_eq!(heuristically_close("(defun f () i32 ; unclosed (\n 1"), "(defun f () i32 ; unclosed (\n 1)");
}
#[test]
fn heuristically_close_does_not_go_negative_on_an_extra_close_paren() {
assert_eq!(heuristically_close("(defun f () i32 1))"), "(defun f () i32 1))");
}
#[test]
fn prefix_start_finds_the_identifier_ending_at_offset() {
let chars: Vec<char> = "(add my-va".chars().collect();
assert_eq!(prefix_start(&chars, chars.len()), 5);
}
#[test]
fn prefix_start_is_the_offset_itself_right_after_a_delimiter() {
let chars: Vec<char> = "(add ".chars().collect();
assert_eq!(prefix_start(&chars, chars.len()), chars.len());
}
#[test]
fn char_offset_finds_a_position_on_a_later_line() {
let chars: Vec<char> = "(a)\n(b c".chars().collect();
assert_eq!(char_offset(&chars, Position::new(1, 4)), chars.len());
}
#[test]
fn char_offset_finds_a_position_on_the_first_line() {
let chars: Vec<char> = "(add 1 2)".chars().collect();
assert_eq!(char_offset(&chars, Position::new(0, 4)), 4);
}
#[test]
fn line_col_at_finds_a_position_on_the_first_line() {
let chars: Vec<char> = "(add 1 2)".chars().collect();
assert_eq!(line_col_at(&chars, 4), (1, 5));
}
#[test]
fn line_col_at_finds_a_position_on_a_later_line() {
let chars: Vec<char> = "(a)\n(b c)".chars().collect();
assert_eq!(line_col_at(&chars, 7), (2, 4));
}
#[test]
fn needs_completion_placeholder_is_false_right_after_a_fresh_open_paren() {
assert!(!needs_completion_placeholder("(defun f () i32 ("));
}
#[test]
fn needs_completion_placeholder_is_true_after_a_prior_sibling() {
assert!(needs_completion_placeholder("(defun f () i32 (let ((n 1)) "));
assert!(needs_completion_placeholder("(+ mylocal "));
}
#[test]
fn needs_completion_placeholder_is_false_at_the_very_start_of_the_document() {
assert!(!needs_completion_placeholder(""));
assert!(!needs_completion_placeholder(" "));
}
#[test]
fn candidates_for_offers_locals_inside_a_truncated_non_catchall_match_arm() {
let file = "/tmp/typelisp-lsp-recover-test/f.typl";
let patched = "(defun f ((o Option<i32>)) i32 (match o ((Some x) (panic \"\"))))";
let candidates = candidates_for(file, patched, HashMap::new(), 1, 51);
let locals: Vec<&str> = candidates
.iter()
.filter(|c| c.kind == CompletionKind::Variable)
.map(|c| c.name.as_str())
.collect();
assert!(locals.contains(&"o"), "parameter offered, got {:?}", locals);
assert!(locals.contains(&"x"), "match-arm binding offered, got {:?}", locals);
}
}