Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable initial support for completions #185

Merged
merged 35 commits into from
Sep 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
edde255
Register completion provider
Schottkyc137 Jun 24, 2023
9d836de
clippy and fmt
Schottkyc137 Jun 24, 2023
2bbfdaa
Add function to request completion
Schottkyc137 Jun 24, 2023
3177bc8
Add dummy implementation of request_completion function
Schottkyc137 Jun 24, 2023
93ec3f0
Naive implementation
Schottkyc137 Jun 24, 2023
25ea8b4
Merge branch 'master' into auto-completions
Schottkyc137 Jul 2, 2023
dc23807
Refactor: TokenStream is now a trait
Schottkyc137 Jul 22, 2023
32a01e3
Merge DiagnosticTokenStream and TokenStream
Schottkyc137 Jul 22, 2023
c274a6b
Rename TokenStream to BaseTokenStream
Schottkyc137 Jul 22, 2023
a6741de
Rename _TokenStream to TokenStream
Schottkyc137 Jul 22, 2023
f169464
Refactor BaseTokenStream to TokenStream
Schottkyc137 Jul 22, 2023
017fc4a
Implement completion_tokenstream
Schottkyc137 Jul 25, 2023
72a1fc8
Merge branch 'master' into auto-completions
Schottkyc137 Aug 9, 2023
18cf4cb
Use alternative strategy to enable completions
Schottkyc137 Aug 9, 2023
cc456ec
Revert breaking changes
Schottkyc137 Aug 9, 2023
9dc5ab5
fmt
Schottkyc137 Aug 9, 2023
76f72f8
Allow double deref
Schottkyc137 Aug 9, 2023
54a6e70
Refactor: Use entity header instead of generic and port clause
Schottkyc137 Aug 10, 2023
7b103f1
Refactor: Architecture declarative part is a region
Schottkyc137 Aug 10, 2023
9261b87
Implement capabilities for regions
Schottkyc137 Aug 10, 2023
d8bda3d
Only search throught the current source when looking at completions
Schottkyc137 Aug 10, 2023
3765cfd
fmt
Schottkyc137 Aug 10, 2023
085c9bd
Add documentation
Schottkyc137 Aug 10, 2023
10eaef2
fmt
Schottkyc137 Aug 10, 2023
d040f7f
make sequential statements recoverable
Schottkyc137 Aug 10, 2023
1527b1f
Merge branch 'master' into auto-completions
Schottkyc137 Sep 9, 2023
b77b93b
clippy, fmt
Schottkyc137 Sep 9, 2023
9c3580f
restore Cargo.lock
Schottkyc137 Sep 9, 2023
21bc454
Don't complete simple items
Schottkyc137 Sep 9, 2023
708102f
Remove hardcoded positions
Schottkyc137 Sep 9, 2023
fe888fa
clippy
Schottkyc137 Sep 9, 2023
4588cd0
remove unused tests
Schottkyc137 Sep 9, 2023
64cb893
Merge branch 'master' into auto-completions
Schottkyc137 Sep 9, 2023
87b80d5
remove region related code
Schottkyc137 Sep 10, 2023
d8dd938
Add testcases; use substring instead of hardcoded positions
Schottkyc137 Sep 10, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading