Skip to content

Commit

Permalink
feat: lsp autocomplete
Browse files Browse the repository at this point in the history
  • Loading branch information
viddrobnic committed Oct 10, 2024
1 parent 5204f01 commit 5386b3b
Show file tree
Hide file tree
Showing 9 changed files with 340 additions and 57 deletions.
47 changes: 37 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
42 changes: 33 additions & 9 deletions language_server/src/analyze/document_info.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand All @@ -19,14 +21,6 @@ pub struct DocumentInfo {

pub documentation: LocationData<String>,

// 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<DocumentSymbol>,
}

Expand Down Expand Up @@ -56,4 +50,34 @@ impl DocumentInfo {
.get(&pos)
.map(|entry| entry.entry.as_ref())
}

pub fn get_completion_items(&self, position: &Position) -> Vec<CompletionItem> {
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<CompletionItem>,
) {
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)
}
}
}
}
}
17 changes: 17 additions & 0 deletions language_server/src/analyze/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,19 +111,27 @@ 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.
// We add it on top of the current frame as anonymous symbol.
self.symbols.last_mut().unwrap().push(DocumentSymbol {
name: None,
kind: DocumentSymbolKind::Function,
parameters: Some(parameters),
name_range: node.range,
range: node.range,
children,
Expand Down Expand Up @@ -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![],
Expand Down Expand Up @@ -452,26 +461,30 @@ 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![],
},
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![],
},
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![],
Expand All @@ -481,26 +494,30 @@ 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![],
},
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![],
},
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![],
Expand Down
2 changes: 2 additions & 0 deletions language_server/src/analyze/symbol_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ pub enum DocumentSymbolKind {
pub struct DocumentSymbol {
pub name: Option<String>,
pub kind: DocumentSymbolKind,
/// If symbol is a function, these are the parameters that the function takes
pub parameters: Option<Vec<String>>,
/// Range of the symbol name (ie. range of function ident)
pub name_range: Range,
/// Range of the symbol scope (ie function name + body)
Expand Down
Loading

0 comments on commit 5386b3b

Please sign in to comment.