Skip to content

Commit

Permalink
Merge pull request #185 from Schottkyc137/auto-completions
Browse files Browse the repository at this point in the history
Enable initial support for completions
  • Loading branch information
kraigher committed Sep 10, 2023
2 parents d54bd40 + d8dd938 commit 8b717f0
Show file tree
Hide file tree
Showing 9 changed files with 321 additions and 4 deletions.
2 changes: 2 additions & 0 deletions vhdl_lang/src/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ mod static_expression;
mod target;
mod visibility;

mod completion;

#[cfg(test)]
mod tests;

Expand Down
246 changes: 246 additions & 0 deletions vhdl_lang/src/analysis/completion.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
use crate::analysis::DesignRoot;
use crate::ast::{AnyDesignUnit, AnyPrimaryUnit, Declaration, UnitKey};
use crate::data::{ContentReader, Symbol};
use crate::syntax::Kind::*;
use crate::syntax::{Symbols, Token, Tokenizer, Value};
use crate::{Position, Source};
use itertools::Itertools;
use std::default::Default;

macro_rules! kind {
($kind: pat) => {
Token { kind: $kind, .. }
};
}

macro_rules! ident {
($bind: pat) => {
Token {
kind: Identifier,
value: Value::Identifier($bind),
..
}
};
}

/// Returns the completable string representation of a declaration
/// for example:
/// `let alias = parse_vhdl("alias my_alias is ...")`
/// `declaration_to_string(Declaration::Alias(alias)) == "my_alias"`
/// Returns `None` if the declaration has no string representation that can be used for completion
/// purposes.
fn declaration_to_string(decl: &Declaration) -> Option<String> {
match decl {
Declaration::Object(o) => Some(o.ident.tree.item.to_string()),
Declaration::File(f) => Some(f.ident.tree.item.to_string()),
Declaration::Type(t) => Some(t.ident.tree.item.to_string()),
Declaration::Component(c) => Some(c.ident.tree.item.to_string()),
Declaration::Attribute(a) => match a {
crate::ast::Attribute::Specification(spec) => Some(spec.ident.item.to_string()),
crate::ast::Attribute::Declaration(decl) => Some(decl.ident.tree.item.to_string()),
},
Declaration::Alias(a) => Some(a.designator.to_string()),
Declaration::SubprogramDeclaration(decl) => Some(decl.subpgm_designator().to_string()),
Declaration::SubprogramBody(_) => None,
Declaration::Use(_) => None,
Declaration::Package(p) => Some(p.ident.to_string()),
Declaration::Configuration(_) => None,
}
}

/// Tokenizes `source` up to `cursor` but no further. The last token returned is the token
/// where the cursor currently resides or the token right before the cursor.
///
/// Examples:
///
/// input = "use ieee.std_logic_1|164.a"
/// ^ cursor position
/// `tokenize_input(input)` -> {USE, ieee, DOT, std_logic_1164}
///
/// input = "use ieee.std_logic_1164|.a"
/// ^ cursor position
/// `tokenize_input(input)` -> {USE, ieee, DOT, std_logic_1164}
///
/// input = "use ieee.std_logic_1164.|a"
/// ^ cursor position
/// `tokenize_input(input)` -> {USE, ieee, DOT, std_logic_1164, DOT}
/// input = "use ieee.std_logic_1164.a|"
/// ^ cursor position
/// `tokenize_input(input)` -> {USE, ieee, DOT, std_logic_1164, DOT, a}
///
/// On error, or if the source is empty, returns an empty vector.
fn tokenize_input(symbols: &Symbols, source: &Source, cursor: Position) -> Vec<Token> {
let contents = source.contents();
let mut tokenizer = Tokenizer::new(symbols, source, ContentReader::new(&contents));
let mut tokens = Vec::new();
loop {
match tokenizer.pop() {
Ok(Some(token)) => {
if token.pos.start() >= cursor {
break;
}
tokens.push(token);
}
Ok(None) => break,
Err(_) => return vec![],
}
}
tokens
}

impl DesignRoot {
/// helper function to list the name of all available libraries
fn list_all_libraries(&self) -> Vec<String> {
self.available_libraries()
.map(|k| k.name().to_string())
.collect()
}

/// List the name of all primary units for a given library.
/// If the library is non-resolvable, list an empty vector
fn list_primaries_for_lib(&self, lib: &Symbol) -> Vec<String> {
let Some(lib) = self.get_library_units(lib) else {
return vec![];
};
lib.keys()
.filter_map(|key| match key {
UnitKey::Primary(prim) => Some(prim.name().to_string()),
UnitKey::Secondary(_, _) => None,
})
.collect()
}

/// Lists all available declarations for a primary unit inside a given library
/// If the library does not exist or there is no primary unit with the given name for that library,
/// return an empty vector
fn list_available_declarations(&self, lib: &Symbol, primary_unit: &Symbol) -> Vec<String> {
let Some(lib) = self.get_library_units(lib) else {
return vec![];
};
let Some(unit) = lib.get(&UnitKey::Primary(primary_unit.clone())) else {
return vec![];
};
let unit = unit.unit.get();
match unit.unwrap().to_owned() {
AnyDesignUnit::Primary(AnyPrimaryUnit::Package(pkg)) => pkg
.decl
.iter()
.filter_map(declaration_to_string)
.unique()
.chain(vec!["all".to_string()])
.collect_vec(),
_ => Vec::default(),
}
}

pub fn list_completion_options(&self, source: &Source, cursor: Position) -> Vec<String> {
let tokens = tokenize_input(&self.symbols, source, cursor);
match &tokens[..] {
[.., kind!(Library)] | [.., kind!(Use)] | [.., kind!(Use), kind!(Identifier)] => {
self.list_all_libraries()
}
[.., kind!(Use), ident!(library), kind!(Dot)]
| [.., kind!(Use), ident!(library), kind!(Dot), kind!(Identifier)] => {
self.list_primaries_for_lib(library)
}
[.., kind!(Use), ident!(library), kind!(Dot), ident!(selected), kind!(Dot)]
| [.., kind!(Use), ident!(library), kind!(Dot), ident!(selected), kind!(Dot), kind!(StringLiteral | Identifier)] => {
self.list_available_declarations(library, selected)
}
_ => vec![],
}
}
}

#[cfg(test)]
mod test {
use super::*;
use crate::analysis::completion::tokenize_input;
use crate::analysis::tests::LibraryBuilder;
use crate::syntax::test::Code;
use assert_matches::assert_matches;

#[test]
fn tokenizing_an_empty_input() {
let input = Code::new("");
let tokens = tokenize_input(&input.symbols, input.source(), Position::new(0, 0));
assert_eq!(tokens.len(), 0);
}

#[test]
fn tokenizing_stops_at_the_cursors_position() {
let input = Code::new("use ieee.std_logic_1164.all");
let mut cursor = input.s1("std_logic_11").pos().end();
let tokens = tokenize_input(&input.symbols, input.source(), cursor);
assert_matches!(
tokens[..],
[kind!(Use), kind!(Identifier), kind!(Dot), kind!(Identifier)]
);
cursor = input.s1("std_logic_1164").pos().end();
let tokens = tokenize_input(&input.symbols, input.source(), cursor);
assert_matches!(
tokens[..],
[kind!(Use), kind!(Identifier), kind!(Dot), kind!(Identifier)]
);
cursor = input.s1("std_logic_1164.").pos().end();
let tokens = tokenize_input(&input.symbols, input.source(), cursor);
assert_matches!(
tokens[..],
[
kind!(Use),
kind!(Identifier),
kind!(Dot),
kind!(Identifier),
kind!(Dot)
]
);
cursor = input.s1("std_logic_1164.all").pos().end();
let tokens = tokenize_input(&input.symbols, input.source(), cursor);
assert_matches!(
tokens[..],
[
kind!(Use),
kind!(Identifier),
kind!(Dot),
kind!(Identifier),
kind!(Dot),
kind!(All)
]
);
}

#[test]
pub fn completing_libraries() {
let input = LibraryBuilder::new();
let code = Code::new("library ");
let (root, _) = input.get_analyzed_root();
let cursor = code.s1("library ").pos().end();
let options = root.list_completion_options(code.source(), cursor);
assert_eq!(options, root.list_all_libraries())
}

#[test]
pub fn completing_primaries() {
let (root, _) = LibraryBuilder::new().get_analyzed_root();
let code = Code::new("use std.");
let cursor = code.pos().end();
let options = root.list_completion_options(code.source(), cursor);
assert_eq!(options, vec!["textio", "standard", "env"]);

let code = Code::new("use std.t");
let cursor = code.pos().end();
let options = root.list_completion_options(code.source(), cursor);
// Note that the filtering only happens at client side
assert_eq!(options, vec!["textio", "standard", "env"]);
}

#[test]
pub fn completing_declarations() {
let input = LibraryBuilder::new();
let code = Code::new("use std.env.");
let (root, _) = input.get_analyzed_root();
let cursor = code.pos().end();
let options = root.list_completion_options(code.source(), cursor);
assert_eq!(options, vec!["stop", "finish", "resolution_limit", "all"])
}
}
5 changes: 5 additions & 0 deletions vhdl_lang/src/analysis/root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,11 @@ impl DesignRoot {
.map(|library| &library.units)
}

/// Iterates over all available library symbols.
pub fn available_libraries(&self) -> impl Iterator<Item = &Symbol> {
self.libraries.keys()
}

pub(crate) fn get_design_entity<'a>(
&'a self,
library_name: &Symbol,
Expand Down
4 changes: 4 additions & 0 deletions vhdl_lang/src/data/source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,10 @@ impl Range {
pub fn new(start: Position, end: Position) -> Range {
Range { start, end }
}

pub fn contains(&self, position: Position) -> bool {
self.start <= position && self.end >= position
}
}

/// A lexical range within a specific source file.
Expand Down
4 changes: 4 additions & 0 deletions vhdl_lang/src/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,10 @@ impl Project {
pub fn files(&self) -> impl Iterator<Item = &SourceFile> {
self.files.values()
}

pub fn list_completion_options(&self, source: &Source, cursor: Position) -> Vec<String> {
self.root.list_completion_options(source, cursor)
}
}

/// Multiply clonable value by cloning
Expand Down
2 changes: 1 addition & 1 deletion vhdl_lang/src/syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ mod waveform;
pub mod test;

pub use parser::{ParserResult, VHDLParser};
pub use tokens::Symbols;
pub use tokens::*;
10 changes: 7 additions & 3 deletions vhdl_lang/src/syntax/sequential_statement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,13 @@ pub fn parse_labeled_sequential_statements(
End | Else | Elsif | When => {
break Ok(statements);
}
_ => {
statements.push(parse_sequential_statement(stream, diagnostics)?);
}
_ => match parse_sequential_statement(stream, diagnostics) {
Ok(stmt) => statements.push(stmt),
Err(diag) => {
diagnostics.push(diag);
let _ = stream.skip_until(|kind| matches!(kind, End | Else | Elsif | When));
}
},
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions vhdl_ls/src/stdio_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,14 @@ impl ConnectionRpcChannel {
}
Err(request) => request,
};
let request = match extract::<request::Completion>(request) {
Ok((id, params)) => {
let res = server.request_completion(&params);
self.send_response(lsp_server::Response::new_ok(id, res));
return;
}
Err(request) => request,
};

debug!("Unhandled request: {:?}", request);
self.send_response(lsp_server::Response::new_err(
Expand Down
44 changes: 44 additions & 0 deletions vhdl_ls/src/vhdl_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ impl VHDLServer {
let config = self.load_config();
self.project = Project::from_config(&config, &mut self.message_filter());
self.init_params = Some(init_params);
let trigger_chars: Vec<String> = r".".chars().map(|ch| ch.to_string()).collect();

let capabilities = ServerCapabilities {
text_document_sync: Some(TextDocumentSyncCapability::Kind(
Expand All @@ -128,6 +129,13 @@ impl VHDLServer {
})),
workspace_symbol_provider: Some(OneOf::Left(true)),
document_symbol_provider: Some(OneOf::Left(true)),
completion_provider: Some(CompletionOptions {
resolve_provider: Some(false),
trigger_characters: Some(trigger_chars),
all_commit_characters: None,
work_done_progress_options: Default::default(),
completion_item: Default::default(),
}),
..Default::default()
};

Expand Down Expand Up @@ -252,6 +260,42 @@ impl VHDLServer {
}
}

/// Called when the client requests a completion.
/// This function looks in the source code to find suitable options and then returns them
pub fn request_completion(&mut self, params: &CompletionParams) -> CompletionList {
let binding = uri_to_file_name(&params.text_document_position.text_document.uri);
let file = binding.as_path();
// 1) get source position, and source file
let Some(source) = self.project.get_source(file) else {
// Do not enable completions for files that are not part of the project
return CompletionList {
..Default::default()
};
};
let cursor = from_lsp_pos(params.text_document_position.position);
// 2) Optimization chance: go to last recognizable token before the cursor. For example:
// - Any primary unit (e.g. entity declaration, package declaration, ...)
// => keyword `entity`, `package`, ...
// - Any secondary unit (e.g. package body, architecture)
// => keyword `architecture`, ...

// 3) Run the parser until the point of the cursor. Then exit with possible completions
let options = self
.project
.list_completion_options(&source, cursor)
.into_iter()
.map(|option| CompletionItem {
label: option,
..Default::default()
})
.collect();

CompletionList {
items: options,
is_incomplete: true,
}
}

fn client_supports_related_information(&self) -> bool {
let try_fun = || {
self.init_params
Expand Down

0 comments on commit 8b717f0

Please sign in to comment.