use std::collections::HashMap;
use std::sync::Mutex;
use tower_lsp_server::jsonrpc::Result;
use tower_lsp_server::ls_types::*;
use tower_lsp_server::{Client, LanguageServer};
use crate::completion;
use crate::definition;
use crate::diagnostics;
use crate::hover as hover_mod;
use crate::lenses;
use crate::modules;
use crate::position::utf16_col_to_byte_idx;
use crate::signature;
use crate::symbols;
pub struct AverBackend {
client: Client,
documents: Mutex<HashMap<String, String>>,
root_dir: Mutex<Option<String>>,
}
impl AverBackend {
pub fn new(client: Client) -> Self {
AverBackend {
client,
documents: Mutex::new(HashMap::new()),
root_dir: Mutex::new(None),
}
}
fn get_base_dir(&self, uri: &Uri) -> Option<String> {
let root = self.root_dir.lock().unwrap().clone();
root.or_else(|| modules::base_dir_from_uri(uri))
}
async fn publish_diagnostics_for(&self, uri: &Uri, source: &str) {
let base_dir = self.get_base_dir(uri);
let diags = diagnostics::diagnose(source, base_dir.as_deref(), Some(uri));
self.client
.publish_diagnostics(uri.clone(), diags, None)
.await;
}
}
impl LanguageServer for AverBackend {
async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> {
let root = params
.workspace_folders
.as_ref()
.and_then(|folders| folders.first())
.and_then(|folder| folder.uri.to_file_path())
.map(|p| p.to_string_lossy().to_string())
.or_else(|| {
#[allow(deprecated)]
params
.root_uri
.as_ref()
.and_then(|uri| uri.to_file_path())
.map(|p| p.to_string_lossy().to_string())
});
if let Some(ref dir) = root {
*self.root_dir.lock().unwrap() = Some(dir.clone());
}
Ok(InitializeResult {
capabilities: ServerCapabilities {
text_document_sync: Some(TextDocumentSyncCapability::Kind(
TextDocumentSyncKind::FULL,
)),
document_symbol_provider: Some(OneOf::Left(true)),
document_formatting_provider: Some(OneOf::Left(true)),
code_lens_provider: Some(CodeLensOptions {
resolve_provider: Some(false),
}),
completion_provider: Some(CompletionOptions {
trigger_characters: Some(vec![".".to_string()]),
..Default::default()
}),
hover_provider: Some(HoverProviderCapability::Simple(true)),
definition_provider: Some(OneOf::Left(true)),
signature_help_provider: Some(SignatureHelpOptions {
trigger_characters: Some(vec!["(".to_string(), ",".to_string()]),
retrigger_characters: None,
work_done_progress_options: Default::default(),
}),
..Default::default()
},
..Default::default()
})
}
async fn initialized(&self, _: InitializedParams) {
let root = self.root_dir.lock().unwrap().clone();
self.client
.log_message(
MessageType::INFO,
format!("Aver LSP initialized, root_dir={:?}", root),
)
.await;
}
async fn shutdown(&self) -> Result<()> {
Ok(())
}
async fn did_open(&self, params: DidOpenTextDocumentParams) {
let uri = params.text_document.uri;
let text = params.text_document.text;
{
let mut docs = self.documents.lock().unwrap();
docs.insert(uri.to_string(), text.clone());
}
self.publish_diagnostics_for(&uri, &text).await;
}
async fn did_change(&self, params: DidChangeTextDocumentParams) {
let uri = params.text_document.uri;
if let Some(change) = params.content_changes.into_iter().last() {
let text = change.text;
{
let mut docs = self.documents.lock().unwrap();
docs.insert(uri.to_string(), text.clone());
}
self.publish_diagnostics_for(&uri, &text).await;
}
}
async fn did_close(&self, params: DidCloseTextDocumentParams) {
let uri = params.text_document.uri;
{
let mut docs = self.documents.lock().unwrap();
docs.remove(&uri.to_string());
}
self.client.publish_diagnostics(uri, Vec::new(), None).await;
}
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 source = {
let docs = self.documents.lock().unwrap();
docs.get(&uri.to_string()).cloned().unwrap_or_default()
};
let base_dir = self.get_base_dir(&uri);
let line_text = source.lines().nth(position.line as usize).unwrap_or("");
let byte_col = utf16_col_to_byte_idx(line_text, position.character);
let before_cursor = &line_text[..byte_col];
if let Some(dot_pos) = before_cursor.rfind('.') {
let before_dot = &before_cursor[..dot_pos];
let namespace = before_dot
.rsplit(|c: char| !c.is_alphanumeric() && c != '_')
.next()
.unwrap_or("");
if !namespace.is_empty() {
let items = completion::namespace_completions(namespace);
if !items.is_empty() {
return Ok(Some(CompletionResponse::Array(items)));
}
let items = completion::user_type_completions(&source, namespace);
if !items.is_empty() {
return Ok(Some(CompletionResponse::Array(items)));
}
if let Some(base) = &base_dir {
let items = completion::module_completions(&source, namespace, base);
if !items.is_empty() {
return Ok(Some(CompletionResponse::Array(items)));
}
}
}
}
if inside_effect_list(&source, position.line as usize, position.character) {
return Ok(Some(CompletionResponse::Array(
completion::effect_completions(),
)));
}
let mut items = completion::all_namespaces();
items.extend(completion::user_fn_completions(&source));
if let Some(base) = &base_dir {
items.extend(completion::depends_module_completions(&source, base));
}
let keywords = [
"fn", "match", "module", "depends", "exposes", "opaque", "intent", "verify",
"decision", "type", "record", "law", "given", "when", "true", "false",
];
for kw in &keywords {
items.push(CompletionItem {
label: kw.to_string(),
kind: Some(CompletionItemKind::KEYWORD),
..Default::default()
});
}
Ok(Some(CompletionResponse::Array(items)))
}
async fn document_symbol(
&self,
params: DocumentSymbolParams,
) -> Result<Option<DocumentSymbolResponse>> {
let uri = params.text_document.uri;
let source = {
let docs = self.documents.lock().unwrap();
docs.get(&uri.to_string()).cloned().unwrap_or_default()
};
Ok(symbols::document_symbols(&source))
}
async fn formatting(&self, params: DocumentFormattingParams) -> Result<Option<Vec<TextEdit>>> {
let uri = params.text_document.uri;
let source = {
let docs = self.documents.lock().unwrap();
docs.get(&uri.to_string()).cloned().unwrap_or_default()
};
Ok(format_document_edits(&source))
}
async fn code_lens(&self, params: CodeLensParams) -> Result<Option<Vec<CodeLens>>> {
let uri = params.text_document.uri;
let source = {
let docs = self.documents.lock().unwrap();
docs.get(&uri.to_string()).cloned().unwrap_or_default()
};
let base_dir = self.get_base_dir(&uri);
Ok(Some(lenses::code_lenses(
&source,
base_dir.as_deref(),
&uri.to_string(),
)))
}
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;
let source = {
let docs = self.documents.lock().unwrap();
docs.get(&uri.to_string()).cloned().unwrap_or_default()
};
let word = match hover_mod::word_at_position(
&source,
position.line as usize,
position.character,
) {
Some(w) => w,
None => return Ok(None),
};
let base_dir = self.get_base_dir(&uri);
Ok(definition::goto_definition(
&word,
&source,
&uri,
base_dir.as_deref(),
))
}
async fn signature_help(&self, params: SignatureHelpParams) -> Result<Option<SignatureHelp>> {
let uri = params.text_document_position_params.text_document.uri;
let position = params.text_document_position_params.position;
let source = {
let docs = self.documents.lock().unwrap();
docs.get(&uri.to_string()).cloned().unwrap_or_default()
};
let base_dir = self.get_base_dir(&uri);
Ok(signature::signature_help(
&source,
position.line as usize,
position.character,
base_dir.as_deref(),
))
}
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;
let source = {
let docs = self.documents.lock().unwrap();
docs.get(&uri.to_string()).cloned().unwrap_or_default()
};
let word = match hover_mod::word_at_position(
&source,
position.line as usize,
position.character,
) {
Some(w) => w,
None => return Ok(None),
};
let base_dir = self.get_base_dir(&uri);
Ok(hover_mod::hover_for_word(
&word,
&source,
base_dir.as_deref(),
))
}
}
fn format_document_edits(source: &str) -> Option<Vec<TextEdit>> {
let (formatted, _violations) = aver::format::try_format_source(source).ok()?;
if formatted == source {
return None;
}
Some(vec![TextEdit {
range: full_document_range(source),
new_text: formatted,
}])
}
fn full_document_range(source: &str) -> Range {
let lines: Vec<&str> = source.split('\n').collect();
let end_line = lines.len().saturating_sub(1) as u32;
let end_character = lines
.last()
.map(|line| line.encode_utf16().count() as u32)
.unwrap_or(0);
Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: end_line,
character: end_character,
},
}
}
fn inside_effect_list(source: &str, line: usize, character: u32) -> bool {
let mut prefix = String::new();
for (idx, current_line) in source.lines().enumerate() {
if idx < line {
prefix.push_str(current_line);
prefix.push('\n');
continue;
}
if idx == line {
let byte_col = utf16_col_to_byte_idx(current_line, character);
prefix.push_str(¤t_line[..byte_col]);
}
break;
}
let bytes = prefix.as_bytes();
let mut stack: Vec<bool> = Vec::new();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'!' {
let mut j = i + 1;
while j < bytes.len() && bytes[j].is_ascii_whitespace() {
j += 1;
}
if j < bytes.len() && bytes[j] == b'[' {
stack.push(true);
i = j + 1;
continue;
}
}
match bytes[i] {
b'[' => stack.push(false),
b']' => {
let _ = stack.pop();
}
_ => {}
}
i += 1;
}
stack.last().copied().unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::{format_document_edits, full_document_range, inside_effect_list};
#[test]
fn detects_effect_list_context_across_lines() {
let src = "fn log() -> Unit\n ! [\n Console.print,\n Time.now\n";
assert!(inside_effect_list(src, 2, 15));
}
#[test]
fn ignores_normal_list_literals() {
let src = "fn demo() -> List<Int>\n xs = [1, 2, 3]\n xs\n";
assert!(!inside_effect_list(src, 1, 10));
}
#[test]
fn formatting_returns_full_document_edit() {
let src = "module Demo\r\nfn x() -> Int\r\n 1\t \r\n";
let edits = format_document_edits(src).expect("expected formatting edits");
assert_eq!(edits.len(), 1);
assert_eq!(edits[0].new_text, "module Demo\n\nfn x() -> Int\n 1\n");
}
#[test]
fn full_document_range_handles_trailing_newline() {
let range = full_document_range("a\n");
assert_eq!(range.start.line, 0);
assert_eq!(range.end.line, 1);
assert_eq!(range.end.character, 0);
}
}