diff --git a/README.md b/README.md index 4be80e0..33f5dc4 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ For the language to be almost operational it should have: - [x] an interpreter, - [x] syntax highlighting, -- [ ] basic LSP. +- [x] basic LSP. ## Usage @@ -67,6 +67,33 @@ If you are using some other editor, you are on your own... ``` 5. Restart neovim and open an `.aoc` file :) +### LSP + +Language server can be started by running `aoc-lang lsp`. If you are using +neovim with [lspconfig](https://github.com/neovim/nvim-lspconfig), you can +configure the LSP by adding the following configuration: + +```lua +local configs = require 'lspconfig.configs' + +if not configs.aoc then + configs.aoc = { + default_config = { + name = 'AOC LSP', + cmd = { '/path/to/aoc-lang', 'lsp' }, + root_dir = lspconfig.util.root_pattern('.git'), + filetypes = { 'aoc' }, + }, + } +end +lspconfig.aoc.setup { + capabilities = capabilities, + on_attach = on_attach, +} +``` + +The language server should get started automatically when you open a `.aoc` file. + ## Language features AoC language supports the following features: @@ -98,14 +125,14 @@ AoC language supports the following features: For more detailed overview of the syntax, see `examples` directory, which contains examples of code with comments. -## Wishlist: - -### LSP +## Language server features -I didn't look into how LSP implementation is done yet, so I don't know how hard -it will be. But I can still write a wishlist :) +AoC LSP has the following features: -- [ ] diagnostics -- [ ] go to definition -- [ ] basic autocomplete -- [ ] formatting +- diagnostics +- go to definition +- list references +- highlight +- hover +- list document symbols +- auto-complete suggestions diff --git a/language_server/src/analyze/document_info.rs b/language_server/src/analyze/document_info.rs index 4f269f4..51dc6e7 100644 --- a/language_server/src/analyze/document_info.rs +++ b/language_server/src/analyze/document_info.rs @@ -1,4 +1,6 @@ -use parser::position::{Position, Range}; +use parser::position::{Position, PositionOrdering, Range}; + +use crate::message::completion::CompletionItem; use super::{location::LocationData, symbol_info::DocumentSymbol}; @@ -19,14 +21,6 @@ pub struct DocumentInfo { pub documentation: LocationData, - // TODO NOTE: - // Document symbols hierarchy are gotten by just recursively mapping the vec of document symbols + filtering out not named symbols. - // - // Autocomplete is implemented by iterating the vec of symbols: - // - symbol is before current position: add the name to possible autocomplete values - // - current position is inside symbol: add the name of the possible autocomplete values and - // recursively call autocomplete on children of the symbol - // - symbol is after current position: exit the loop pub symbol_tree: Vec, } @@ -56,4 +50,34 @@ impl DocumentInfo { .get(&pos) .map(|entry| entry.entry.as_ref()) } + + pub fn get_completion_items(&self, position: &Position) -> Vec { + let mut items = vec![]; + get_completion_items(position, &self.symbol_tree, &mut items); + items + } +} + +fn get_completion_items( + position: &Position, + symbol_tree: &[DocumentSymbol], + items: &mut Vec, +) { + for symbol in symbol_tree { + match position.cmp_range(&symbol.range) { + PositionOrdering::Before => return, + PositionOrdering::Inside => { + if let Some(it) = symbol.into() { + items.push(it) + } + + get_completion_items(position, &symbol.children, items); + } + PositionOrdering::After => { + if let Some(it) = symbol.into() { + items.push(it) + } + } + } + } } diff --git a/language_server/src/analyze/mod.rs b/language_server/src/analyze/mod.rs index 24d9ca1..355c82b 100644 --- a/language_server/src/analyze/mod.rs +++ b/language_server/src/analyze/mod.rs @@ -111,12 +111,19 @@ impl Analyzer { self.symbol_table.leave_scope(); let children = self.symbols.pop().unwrap(); + let parameters: Vec<_> = fn_lit + .parameters + .iter() + .map(|param| param.name.clone()) + .collect(); + if fn_lit.name.is_some() { // The function is named, which means that the last symbol on top of the // frame is the name of the function. We have to update the range, kind and children. let fn_sym = self.symbols.last_mut().unwrap().last_mut().unwrap(); fn_sym.range.end = node.range.end; fn_sym.kind = DocumentSymbolKind::Function; + fn_sym.parameters = Some(parameters); fn_sym.children = children; } else { // The function is not named => it is anonymous. @@ -124,6 +131,7 @@ impl Analyzer { self.symbols.last_mut().unwrap().push(DocumentSymbol { name: None, kind: DocumentSymbolKind::Function, + parameters: Some(parameters), name_range: node.range, range: node.range, children, @@ -249,6 +257,7 @@ impl Analyzer { self.symbols.last_mut().unwrap().push(DocumentSymbol { name: Some(ident), kind: DocumentSymbolKind::Variable, + parameters: None, name_range: location, range: location, children: vec![], @@ -452,6 +461,7 @@ mod test { DocumentSymbol { name: Some("a".to_string()), kind: DocumentSymbolKind::Variable, + parameters: None, name_range: Range::new(Position::new(1, 12), Position::new(1, 13)), range: Range::new(Position::new(1, 12), Position::new(1, 13)), children: vec![], @@ -459,12 +469,14 @@ mod test { DocumentSymbol { name: None, kind: DocumentSymbolKind::Function, + parameters: Some(vec![]), name_range: Range::new(Position::new(3, 12), Position::new(6, 13)), range: Range::new(Position::new(3, 12), Position::new(6, 13)), children: vec![ DocumentSymbol { name: Some("a".to_string()), kind: DocumentSymbolKind::Variable, + parameters: None, name_range: Range::new(Position::new(4, 16), Position::new(4, 17)), range: Range::new(Position::new(4, 16), Position::new(4, 17)), children: vec![], @@ -472,6 +484,7 @@ mod test { DocumentSymbol { name: Some("b".to_string()), kind: DocumentSymbolKind::Variable, + parameters: None, name_range: Range::new(Position::new(5, 16), Position::new(5, 17)), range: Range::new(Position::new(5, 16), Position::new(5, 17)), children: vec![], @@ -481,12 +494,14 @@ mod test { DocumentSymbol { name: Some("foo".to_string()), kind: DocumentSymbolKind::Function, + parameters: Some(vec!["bar".to_string()]), name_range: Range::new(Position::new(10, 12), Position::new(10, 15)), range: Range::new(Position::new(10, 12), Position::new(12, 13)), children: vec![ DocumentSymbol { name: Some("bar".to_string()), kind: DocumentSymbolKind::Variable, + parameters: None, name_range: Range::new(Position::new(10, 21), Position::new(10, 24)), range: Range::new(Position::new(10, 21), Position::new(10, 24)), children: vec![], @@ -494,6 +509,7 @@ mod test { DocumentSymbol { name: Some("a".to_string()), kind: DocumentSymbolKind::Variable, + parameters: None, name_range: Range::new(Position::new(11, 17), Position::new(11, 18)), range: Range::new(Position::new(11, 17), Position::new(11, 18)), children: vec![], @@ -501,6 +517,7 @@ mod test { DocumentSymbol { name: Some("b".to_string()), kind: DocumentSymbolKind::Variable, + parameters: None, name_range: Range::new(Position::new(11, 20), Position::new(11, 21)), range: Range::new(Position::new(11, 20), Position::new(11, 21)), children: vec![], diff --git a/language_server/src/analyze/symbol_info.rs b/language_server/src/analyze/symbol_info.rs index 62fbdee..e80425c 100644 --- a/language_server/src/analyze/symbol_info.rs +++ b/language_server/src/analyze/symbol_info.rs @@ -10,6 +10,8 @@ pub enum DocumentSymbolKind { pub struct DocumentSymbol { pub name: Option, pub kind: DocumentSymbolKind, + /// If symbol is a function, these are the parameters that the function takes + pub parameters: Option>, /// Range of the symbol name (ie. range of function ident) pub name_range: Range, /// Range of the symbol scope (ie function name + body) diff --git a/language_server/src/completion.rs b/language_server/src/completion.rs new file mode 100644 index 0000000..7d5629d --- /dev/null +++ b/language_server/src/completion.rs @@ -0,0 +1,182 @@ +use runtime::builtin::Builtin; + +use crate::{ + hover::MarkupContent, + message::completion::{CompletionItem, CompletionItemKind, InsertTextFormat}, +}; + +pub fn extend_snippets(completions: &mut Vec) { + completions.push(CompletionItem { + label: "for".to_string(), + kind: Some(CompletionItemKind::Snippet as i32), + insert_text: Some("for ($1; $2; $3) {\n $4\n}$0".to_string()), + insert_text_format: Some(InsertTextFormat::Snippet as i32), + documentation: None, + }); + + completions.push(CompletionItem { + label: "if".to_string(), + kind: Some(CompletionItemKind::Snippet as i32), + insert_text: Some("if ($1) {\n $2\n}$0".to_string()), + insert_text_format: Some(InsertTextFormat::Snippet as i32), + documentation: None, + }); + + completions.push(CompletionItem { + label: "ifelse".to_string(), + kind: Some(CompletionItemKind::Snippet as i32), + insert_text: Some("if ($1) {\n $2\n} else {\n $3\n}$0".to_string()), + insert_text_format: Some(InsertTextFormat::Snippet as i32), + documentation: None, + }); + + completions.push(CompletionItem { + label: "while".to_string(), + kind: Some(CompletionItemKind::Snippet as i32), + insert_text: Some("while ($1) {\n $2\n}$0".to_string()), + insert_text_format: Some(InsertTextFormat::Snippet as i32), + documentation: None, + }); +} + +pub fn extend_builtin(completions: &mut Vec) { + completions.push(CompletionItem { + label: "len".to_string(), + kind: Some(CompletionItemKind::Function as i32), + insert_text: Some("len($1)$0".to_string()), + insert_text_format: Some(InsertTextFormat::Snippet as i32), + documentation: Some(MarkupContent::from_markdown(Builtin::Len.documentation())), + }); + completions.push(CompletionItem { + label: "str".to_string(), + kind: Some(CompletionItemKind::Function as i32), + insert_text: Some("str($1)$0".to_string()), + insert_text_format: Some(InsertTextFormat::Snippet as i32), + documentation: Some(MarkupContent::from_markdown(Builtin::Str.documentation())), + }); + completions.push(CompletionItem { + label: "int".to_string(), + kind: Some(CompletionItemKind::Function as i32), + insert_text: Some("int($1)$0".to_string()), + insert_text_format: Some(InsertTextFormat::Snippet as i32), + documentation: Some(MarkupContent::from_markdown(Builtin::Int.documentation())), + }); + completions.push(CompletionItem { + label: "char".to_string(), + kind: Some(CompletionItemKind::Function as i32), + insert_text: Some("char($1)$0".to_string()), + insert_text_format: Some(InsertTextFormat::Snippet as i32), + documentation: Some(MarkupContent::from_markdown(Builtin::Char.documentation())), + }); + completions.push(CompletionItem { + label: "float".to_string(), + kind: Some(CompletionItemKind::Function as i32), + insert_text: Some("float($1)$0".to_string()), + insert_text_format: Some(InsertTextFormat::Snippet as i32), + documentation: Some(MarkupContent::from_markdown(Builtin::Float.documentation())), + }); + completions.push(CompletionItem { + label: "bool".to_string(), + kind: Some(CompletionItemKind::Function as i32), + insert_text: Some("bool($1)$0".to_string()), + insert_text_format: Some(InsertTextFormat::Snippet as i32), + documentation: Some(MarkupContent::from_markdown(Builtin::Bool.documentation())), + }); + completions.push(CompletionItem { + label: "is_null".to_string(), + kind: Some(CompletionItemKind::Function as i32), + insert_text: Some("is_null($1)$0".to_string()), + insert_text_format: Some(InsertTextFormat::Snippet as i32), + documentation: Some(MarkupContent::from_markdown( + Builtin::IsNull.documentation(), + )), + }); + completions.push(CompletionItem { + label: "floor".to_string(), + kind: Some(CompletionItemKind::Function as i32), + insert_text: Some("floor($1)$0".to_string()), + insert_text_format: Some(InsertTextFormat::Snippet as i32), + documentation: Some(MarkupContent::from_markdown(Builtin::Floor.documentation())), + }); + completions.push(CompletionItem { + label: "ceil".to_string(), + kind: Some(CompletionItemKind::Function as i32), + insert_text: Some("ceil($1)$0".to_string()), + insert_text_format: Some(InsertTextFormat::Snippet as i32), + documentation: Some(MarkupContent::from_markdown(Builtin::Ceil.documentation())), + }); + completions.push(CompletionItem { + label: "round".to_string(), + kind: Some(CompletionItemKind::Function as i32), + insert_text: Some("round($1)$0".to_string()), + insert_text_format: Some(InsertTextFormat::Snippet as i32), + documentation: Some(MarkupContent::from_markdown(Builtin::Round.documentation())), + }); + completions.push(CompletionItem { + label: "trim_start".to_string(), + kind: Some(CompletionItemKind::Function as i32), + insert_text: Some("trim_start($1)$0".to_string()), + insert_text_format: Some(InsertTextFormat::Snippet as i32), + documentation: Some(MarkupContent::from_markdown( + Builtin::TrimStart.documentation(), + )), + }); + completions.push(CompletionItem { + label: "trim_end".to_string(), + kind: Some(CompletionItemKind::Function as i32), + insert_text: Some("trim_end($1)$0".to_string()), + insert_text_format: Some(InsertTextFormat::Snippet as i32), + documentation: Some(MarkupContent::from_markdown( + Builtin::TrimEnd.documentation(), + )), + }); + completions.push(CompletionItem { + label: "trim".to_string(), + kind: Some(CompletionItemKind::Function as i32), + insert_text: Some("trim($1)$0".to_string()), + insert_text_format: Some(InsertTextFormat::Snippet as i32), + documentation: Some(MarkupContent::from_markdown(Builtin::Trim.documentation())), + }); + completions.push(CompletionItem { + label: "split".to_string(), + kind: Some(CompletionItemKind::Function as i32), + insert_text: Some("split(${1:str}, ${2:delim})$0".to_string()), + insert_text_format: Some(InsertTextFormat::Snippet as i32), + documentation: Some(MarkupContent::from_markdown(Builtin::Split.documentation())), + }); + completions.push(CompletionItem { + label: "push".to_string(), + kind: Some(CompletionItemKind::Function as i32), + insert_text: Some("push(${1:arr}, ${2:value})$0".to_string()), + insert_text_format: Some(InsertTextFormat::Snippet as i32), + documentation: Some(MarkupContent::from_markdown(Builtin::Push.documentation())), + }); + completions.push(CompletionItem { + label: "pop".to_string(), + kind: Some(CompletionItemKind::Function as i32), + insert_text: Some("pop(${1:arr})$0".to_string()), + insert_text_format: Some(InsertTextFormat::Snippet as i32), + documentation: Some(MarkupContent::from_markdown(Builtin::Pop.documentation())), + }); + completions.push(CompletionItem { + label: "del".to_string(), + kind: Some(CompletionItemKind::Function as i32), + insert_text: Some("del(${1:dict}, ${2:key})$0".to_string()), + insert_text_format: Some(InsertTextFormat::Snippet as i32), + documentation: Some(MarkupContent::from_markdown(Builtin::Del.documentation())), + }); + completions.push(CompletionItem { + label: "print".to_string(), + kind: Some(CompletionItemKind::Function as i32), + insert_text: Some("print(${1:value})$0".to_string()), + insert_text_format: Some(InsertTextFormat::Snippet as i32), + documentation: Some(MarkupContent::from_markdown(Builtin::Print.documentation())), + }); + completions.push(CompletionItem { + label: "input".to_string(), + kind: Some(CompletionItemKind::Function as i32), + insert_text: Some("input()$0".to_string()), + insert_text_format: Some(InsertTextFormat::Snippet as i32), + documentation: Some(MarkupContent::from_markdown(Builtin::Input.documentation())), + }); +} diff --git a/language_server/src/lib.rs b/language_server/src/lib.rs index 2989b4c..4357ee0 100644 --- a/language_server/src/lib.rs +++ b/language_server/src/lib.rs @@ -7,23 +7,21 @@ use std::{ }; use analyze::{analyze, document_info::DocumentInfo}; -use completion::CompletionList; use diagnostics::{Diagnostic, DiagnosticSeverity, PublishDiagnosticsParams}; use document_symbol::{DocumentSymbol, DocumentSymbolParams}; use error::{Error, ErrorKind}; use hover::{Hover, MarkupContent, MarkupKind}; -use message::{initialize::*, *}; +use message::{completion::CompletionList, initialize::*, *}; use parser::position::PositionOrdering; use reference::ReferenceParams; use runtime::compiler; -use snippet::extend_completions; use text::*; pub mod error; mod analyze; +mod completion; mod message; -mod snippet; #[derive(Clone, Copy)] #[allow(dead_code)] @@ -285,12 +283,16 @@ impl Server { "textDocument/completion" => { let (req_id, params) = req.extract::()?; + let doc_name = params.text_document.uri.clone(); - self.log(LogLevel::Debug, &format!("Params: {:?}", params)); - + let doc_info = self.documents.get(&doc_name); let mut completions = vec![]; - extend_completions(&mut completions); + if let Some(doc_info) = doc_info { + completions = doc_info.get_completion_items(¶ms.position); + } + completion::extend_snippets(&mut completions); + completion::extend_builtin(&mut completions); let res = CompletionList { is_incomplete: false, items: completions, diff --git a/language_server/src/message/completion.rs b/language_server/src/message/completion.rs index fa4f0ac..66265f4 100644 --- a/language_server/src/message/completion.rs +++ b/language_server/src/message/completion.rs @@ -1,5 +1,10 @@ use serde::{Deserialize, Serialize}; +use crate::{ + analyze::symbol_info::{DocumentSymbol, DocumentSymbolKind}, + hover::MarkupContent, +}; + #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CompletionList { @@ -20,6 +25,9 @@ pub struct CompletionItem { #[serde(skip_serializing_if = "Option::is_none")] pub insert_text_format: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub documentation: Option, } #[derive(Clone, Copy, Debug)] @@ -58,3 +66,46 @@ pub enum InsertTextFormat { PlainText = 1, Snippet = 2, } + +impl From<&DocumentSymbol> for Option { + fn from(sym: &DocumentSymbol) -> Self { + let Some(name) = &sym.name else { return None }; + + let kind = match sym.kind { + DocumentSymbolKind::Function => CompletionItemKind::Function, + DocumentSymbolKind::Variable => CompletionItemKind::Variable, + }; + + let (text, format) = match sym.kind { + DocumentSymbolKind::Function => { + let mut text = format!("{name}("); + + if let Some(params) = &sym.parameters { + let params_str = params + .iter() + .enumerate() + .map(|(i, param)| format!("${{{}:{}}}", i + 1, param)) + .collect::>() + .join(", "); + + text.push_str(¶ms_str); + } else { + text.push_str("$1"); + } + + text.push_str(")$0"); + + (text, InsertTextFormat::Snippet) + } + DocumentSymbolKind::Variable => (name.clone(), InsertTextFormat::PlainText), + }; + + Some(CompletionItem { + label: name.clone(), + kind: Some(kind as i32), + insert_text: Some(text), + insert_text_format: Some(format as i32), + documentation: None, + }) + } +} diff --git a/language_server/src/message/hover.rs b/language_server/src/message/hover.rs index c65a884..8035bc2 100644 --- a/language_server/src/message/hover.rs +++ b/language_server/src/message/hover.rs @@ -17,3 +17,12 @@ pub enum MarkupKind { PlainText, Markdown, } + +impl MarkupContent { + pub fn from_markdown(md: String) -> Self { + Self { + kind: MarkupKind::Markdown, + value: md, + } + } +} diff --git a/language_server/src/snippet.rs b/language_server/src/snippet.rs deleted file mode 100644 index 8d37e0d..0000000 --- a/language_server/src/snippet.rs +++ /dev/null @@ -1,31 +0,0 @@ -use crate::completion::{CompletionItem, CompletionItemKind, InsertTextFormat}; - -pub fn extend_completions(completions: &mut Vec) { - completions.push(CompletionItem { - label: "for".to_string(), - kind: Some(CompletionItemKind::Snippet as i32), - insert_text: Some("for ($1; $2; $3) {\n $4\n}$0".to_string()), - insert_text_format: Some(InsertTextFormat::Snippet as i32), - }); - - completions.push(CompletionItem { - label: "if".to_string(), - kind: Some(CompletionItemKind::Snippet as i32), - insert_text: Some("if ($1) {\n $2\n}$0".to_string()), - insert_text_format: Some(InsertTextFormat::Snippet as i32), - }); - - completions.push(CompletionItem { - label: "ifelse".to_string(), - kind: Some(CompletionItemKind::Snippet as i32), - insert_text: Some("if ($1) {\n $2\n} else {\n $3\n}$0".to_string()), - insert_text_format: Some(InsertTextFormat::Snippet as i32), - }); - - completions.push(CompletionItem { - label: "while".to_string(), - kind: Some(CompletionItemKind::Snippet as i32), - insert_text: Some("while ($1) {\n $2\n}$0".to_string()), - insert_text_format: Some(InsertTextFormat::Snippet as i32), - }); -}