first commit
This commit is contained in:
commit
1b91a7dad9
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
debug/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
|
|
||||||
|
tests_rtl
|
||||||
|
log_files
|
||||||
|
test.txt
|
||||||
|
|
||||||
|
.vscode/
|
1705
Cargo.lock
generated
Normal file
1705
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
Cargo.toml
Normal file
27
Cargo.toml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
[package]
|
||||||
|
name = "veridian"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Vivek Malneedi <vivekmalneedi@gmail.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
sv-parser = "0.8.2"
|
||||||
|
log = "0.4.19"
|
||||||
|
tower-lsp = "0.20.0"
|
||||||
|
flexi_logger = "0.27.4"
|
||||||
|
ropey = "1.6.0"
|
||||||
|
tokio = { version = "1.29.1", features = ["macros", "io-std", "rt-multi-thread"] }
|
||||||
|
path-clean = "1.0.1"
|
||||||
|
pathdiff = "0.2.1"
|
||||||
|
walkdir = "2.3.3"
|
||||||
|
serde_yaml = "0.9.25"
|
||||||
|
anyhow = "1.0.72"
|
||||||
|
serde = "1.0.179"
|
||||||
|
which = "6.0.0"
|
||||||
|
regex = "1.9.1"
|
||||||
|
structopt = "0.3.26"
|
||||||
|
strum = "0.26.1"
|
||||||
|
strum_macros = "0.26.1"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempdir = "0.3.7"
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021 Vivek Malneedi
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
2
rustfmt.toml
Normal file
2
rustfmt.toml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
edition = "2018"
|
||||||
|
max_width = 100
|
781
src/completion.rs
Normal file
781
src/completion.rs
Normal file
@ -0,0 +1,781 @@
|
|||||||
|
use crate::server::LSPServer;
|
||||||
|
use crate::sources::LSPSupport;
|
||||||
|
use log::{debug, trace};
|
||||||
|
use ropey::{Rope, RopeSlice};
|
||||||
|
use std::time::Instant;
|
||||||
|
use tower_lsp::lsp_types::*;
|
||||||
|
|
||||||
|
pub mod keyword;
|
||||||
|
|
||||||
|
impl LSPServer {
|
||||||
|
pub fn completion(&self, params: CompletionParams) -> Option<CompletionResponse> {
|
||||||
|
debug!("completion requested");
|
||||||
|
trace!("{:#?}", ¶ms);
|
||||||
|
let now = Instant::now();
|
||||||
|
let doc = params.text_document_position;
|
||||||
|
let file_id = self.srcs.get_id(&doc.text_document.uri).to_owned();
|
||||||
|
self.srcs.wait_parse_ready(file_id, false);
|
||||||
|
trace!("comp wait parse: {}", now.elapsed().as_millis());
|
||||||
|
let file = self.srcs.get_file(file_id)?;
|
||||||
|
let file = file.read().ok()?;
|
||||||
|
trace!("comp read: {}", now.elapsed().as_millis());
|
||||||
|
let token = get_completion_token(
|
||||||
|
&file.text,
|
||||||
|
file.text.line(doc.position.line as usize),
|
||||||
|
doc.position,
|
||||||
|
);
|
||||||
|
let response = match params.context {
|
||||||
|
Some(context) => match context.trigger_kind {
|
||||||
|
CompletionTriggerKind::TRIGGER_CHARACTER => {
|
||||||
|
debug!(
|
||||||
|
"trigger char completion: {}",
|
||||||
|
context.trigger_character.clone()?.as_str()
|
||||||
|
);
|
||||||
|
match context.trigger_character?.as_str() {
|
||||||
|
"." => Some(self.srcs.get_dot_completions(
|
||||||
|
token.trim_end_matches('.'),
|
||||||
|
file.text.pos_to_byte(&doc.position),
|
||||||
|
&doc.text_document.uri,
|
||||||
|
)?),
|
||||||
|
"$" => Some(CompletionList {
|
||||||
|
is_incomplete: false,
|
||||||
|
items: self.sys_tasks.clone(),
|
||||||
|
}),
|
||||||
|
"`" => Some(CompletionList {
|
||||||
|
is_incomplete: false,
|
||||||
|
items: self.directives.clone(),
|
||||||
|
}),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CompletionTriggerKind::TRIGGER_FOR_INCOMPLETE_COMPLETIONS => None,
|
||||||
|
CompletionTriggerKind::INVOKED => {
|
||||||
|
debug!("Invoked Completion");
|
||||||
|
let mut comps = self.srcs.get_completions(
|
||||||
|
&token,
|
||||||
|
file.text.pos_to_byte(&doc.position),
|
||||||
|
&doc.text_document.uri,
|
||||||
|
)?;
|
||||||
|
// complete keywords
|
||||||
|
comps.items.extend::<Vec<CompletionItem>>(
|
||||||
|
self.key_comps
|
||||||
|
.iter()
|
||||||
|
.filter(|x| x.label.starts_with(&token))
|
||||||
|
.cloned()
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
Some(comps)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
let trigger = prev_char(&file.text, &doc.position);
|
||||||
|
match trigger {
|
||||||
|
'.' => Some(self.srcs.get_dot_completions(
|
||||||
|
token.trim_end_matches('.'),
|
||||||
|
file.text.pos_to_byte(&doc.position),
|
||||||
|
&doc.text_document.uri,
|
||||||
|
)?),
|
||||||
|
'$' => Some(CompletionList {
|
||||||
|
is_incomplete: false,
|
||||||
|
items: self.sys_tasks.clone(),
|
||||||
|
}),
|
||||||
|
'`' => Some(CompletionList {
|
||||||
|
is_incomplete: false,
|
||||||
|
items: self.directives.clone(),
|
||||||
|
}),
|
||||||
|
_ => {
|
||||||
|
let mut comps = self.srcs.get_completions(
|
||||||
|
&token,
|
||||||
|
file.text.pos_to_byte(&doc.position),
|
||||||
|
&doc.text_document.uri,
|
||||||
|
)?;
|
||||||
|
comps.items.extend::<Vec<CompletionItem>>(
|
||||||
|
self.key_comps
|
||||||
|
.iter()
|
||||||
|
.filter(|x| x.label.starts_with(&token))
|
||||||
|
.cloned()
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
Some(comps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// eprintln!("comp response: {}", now.elapsed().as_millis());
|
||||||
|
Some(CompletionResponse::List(response?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get the previous non-whitespace character
|
||||||
|
fn prev_char(text: &Rope, pos: &Position) -> char {
|
||||||
|
let char_idx = text.pos_to_char(pos);
|
||||||
|
if char_idx > 0 {
|
||||||
|
for i in (0..char_idx).rev() {
|
||||||
|
let res = text.char(i);
|
||||||
|
if !res.is_whitespace() {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
' '
|
||||||
|
} else {
|
||||||
|
' '
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// attempt to get the token the user was trying to complete, by
|
||||||
|
/// filtering out characters unneeded for name resolution
|
||||||
|
fn get_completion_token(text: &Rope, line: RopeSlice, pos: Position) -> String {
|
||||||
|
let mut token = String::new();
|
||||||
|
let mut line_iter = line.chars();
|
||||||
|
for _ in 0..(line.utf16_cu_to_char(pos.character as usize)) {
|
||||||
|
line_iter.next();
|
||||||
|
}
|
||||||
|
let mut c = line_iter.prev();
|
||||||
|
//TODO: make this a regex
|
||||||
|
while c.is_some()
|
||||||
|
&& (c.unwrap().is_alphanumeric()
|
||||||
|
|| c.unwrap() == '_'
|
||||||
|
|| c.unwrap() == '.'
|
||||||
|
|| c.unwrap() == '['
|
||||||
|
|| c.unwrap() == ']')
|
||||||
|
{
|
||||||
|
token.push(c.unwrap());
|
||||||
|
c = line_iter.prev();
|
||||||
|
}
|
||||||
|
let mut result: String = token.chars().rev().collect();
|
||||||
|
if result.contains('[') {
|
||||||
|
let l_bracket_offset = result.find('[').unwrap_or(result.len());
|
||||||
|
result.replace_range(l_bracket_offset.., "");
|
||||||
|
}
|
||||||
|
if &result == "." {
|
||||||
|
// probably a instantiation, the token should be what we're instatiating
|
||||||
|
let mut char_iter = text.chars();
|
||||||
|
let mut token = String::new();
|
||||||
|
for _ in 0..text.pos_to_char(&pos) {
|
||||||
|
char_iter.next();
|
||||||
|
}
|
||||||
|
let mut c = char_iter.prev();
|
||||||
|
|
||||||
|
// go to the last semicolon
|
||||||
|
while c.is_some() && (c.unwrap() != ';') {
|
||||||
|
c = char_iter.prev();
|
||||||
|
}
|
||||||
|
// go the the start of the next symbol
|
||||||
|
while c.is_some() && !(c.unwrap().is_alphanumeric() || c.unwrap() == '_') {
|
||||||
|
c = char_iter.next();
|
||||||
|
}
|
||||||
|
// then extract the next symbol
|
||||||
|
while c.is_some() && (c.unwrap().is_alphanumeric() || c.unwrap() == '_') {
|
||||||
|
token.push(c.unwrap());
|
||||||
|
c = char_iter.next();
|
||||||
|
}
|
||||||
|
token
|
||||||
|
} else {
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::definition::def_types::Scope;
|
||||||
|
use crate::definition::get_scopes;
|
||||||
|
use crate::sources::{parse, LSPSupport};
|
||||||
|
use crate::support::test_init;
|
||||||
|
use ropey::Rope;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_completion_token() {
|
||||||
|
test_init();
|
||||||
|
let text = Rope::from_str("abc abc.cba de_fg cde[4]");
|
||||||
|
let mut result = get_completion_token(
|
||||||
|
&text,
|
||||||
|
text.line(0),
|
||||||
|
Position {
|
||||||
|
line: 0,
|
||||||
|
character: 3,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert_eq!(&result, "abc");
|
||||||
|
result = get_completion_token(
|
||||||
|
&text,
|
||||||
|
text.line(0),
|
||||||
|
Position {
|
||||||
|
line: 0,
|
||||||
|
character: 11,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert_eq!(&result, "abc.cba");
|
||||||
|
result = get_completion_token(
|
||||||
|
&text,
|
||||||
|
text.line(0),
|
||||||
|
Position {
|
||||||
|
line: 0,
|
||||||
|
character: 16,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert_eq!(&result, "de_f");
|
||||||
|
result = get_completion_token(
|
||||||
|
&text,
|
||||||
|
text.line(0),
|
||||||
|
Position {
|
||||||
|
line: 0,
|
||||||
|
character: 23,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert_eq!(&result, "cde");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_completion() {
|
||||||
|
test_init();
|
||||||
|
let server = LSPServer::new(None);
|
||||||
|
let uri = Url::parse("file:///test.sv").unwrap();
|
||||||
|
let text = r#"module test;
|
||||||
|
logic abc;
|
||||||
|
logic abcd;
|
||||||
|
|
||||||
|
endmodule
|
||||||
|
"#;
|
||||||
|
let open_params = DidOpenTextDocumentParams {
|
||||||
|
text_document: TextDocumentItem {
|
||||||
|
uri: uri.clone(),
|
||||||
|
language_id: "systemverilog".to_owned(),
|
||||||
|
version: 0,
|
||||||
|
text: text.to_owned(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
server.did_open(open_params);
|
||||||
|
let fid = server.srcs.get_id(&uri);
|
||||||
|
server.srcs.wait_parse_ready(fid, true);
|
||||||
|
|
||||||
|
let change_params = DidChangeTextDocumentParams {
|
||||||
|
text_document: VersionedTextDocumentIdentifier {
|
||||||
|
uri: uri.clone(),
|
||||||
|
version: 3,
|
||||||
|
},
|
||||||
|
content_changes: vec![
|
||||||
|
TextDocumentContentChangeEvent {
|
||||||
|
range: Some(Range {
|
||||||
|
start: Position {
|
||||||
|
line: 3,
|
||||||
|
character: 0,
|
||||||
|
},
|
||||||
|
end: Position {
|
||||||
|
line: 3,
|
||||||
|
character: 0,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
range_length: None,
|
||||||
|
text: "\n".to_owned(),
|
||||||
|
},
|
||||||
|
TextDocumentContentChangeEvent {
|
||||||
|
range: Some(Range {
|
||||||
|
start: Position {
|
||||||
|
line: 4,
|
||||||
|
character: 0,
|
||||||
|
},
|
||||||
|
end: Position {
|
||||||
|
line: 4,
|
||||||
|
character: 0,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
range_length: None,
|
||||||
|
text: " ".to_owned(),
|
||||||
|
},
|
||||||
|
TextDocumentContentChangeEvent {
|
||||||
|
range: Some(Range {
|
||||||
|
start: Position {
|
||||||
|
line: 4,
|
||||||
|
character: 2,
|
||||||
|
},
|
||||||
|
end: Position {
|
||||||
|
line: 4,
|
||||||
|
character: 2,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
range_length: None,
|
||||||
|
text: "a".to_owned(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
server.did_change(change_params);
|
||||||
|
server.srcs.wait_parse_ready(fid, true);
|
||||||
|
|
||||||
|
let completion_params = CompletionParams {
|
||||||
|
text_document_position: TextDocumentPositionParams {
|
||||||
|
text_document: TextDocumentIdentifier { uri },
|
||||||
|
position: Position {
|
||||||
|
line: 4,
|
||||||
|
character: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
work_done_progress_params: WorkDoneProgressParams::default(),
|
||||||
|
partial_result_params: PartialResultParams::default(),
|
||||||
|
context: Some(CompletionContext {
|
||||||
|
trigger_kind: CompletionTriggerKind::INVOKED,
|
||||||
|
trigger_character: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
let response: CompletionResponse = server.completion(completion_params).unwrap();
|
||||||
|
let item1 = CompletionItem {
|
||||||
|
label: "abc".to_owned(),
|
||||||
|
kind: Some(CompletionItemKind::VARIABLE),
|
||||||
|
detail: Some("logic".to_string()),
|
||||||
|
..CompletionItem::default()
|
||||||
|
};
|
||||||
|
let item2 = CompletionItem {
|
||||||
|
label: "abcd".to_owned(),
|
||||||
|
kind: Some(CompletionItemKind::VARIABLE),
|
||||||
|
detail: Some("logic".to_string()),
|
||||||
|
..CompletionItem::default()
|
||||||
|
};
|
||||||
|
if let CompletionResponse::List(item) = response {
|
||||||
|
assert!(item.items.contains(&item1));
|
||||||
|
assert!(item.items.contains(&item2));
|
||||||
|
} else {
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_nested_completion() {
|
||||||
|
test_init();
|
||||||
|
let server = LSPServer::new(None);
|
||||||
|
let uri = Url::parse("file:///test.sv").unwrap();
|
||||||
|
let text = r#"module test;
|
||||||
|
logic aouter;
|
||||||
|
function func1();
|
||||||
|
logic abc;
|
||||||
|
func1 = abc;
|
||||||
|
endfunction
|
||||||
|
function func2();
|
||||||
|
logic abcd;
|
||||||
|
func2 = abcd;
|
||||||
|
endfunction
|
||||||
|
endmodule
|
||||||
|
"#;
|
||||||
|
let open_params = DidOpenTextDocumentParams {
|
||||||
|
text_document: TextDocumentItem {
|
||||||
|
uri: uri.clone(),
|
||||||
|
language_id: "systemverilog".to_owned(),
|
||||||
|
version: 0,
|
||||||
|
text: text.to_owned(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
server.did_open(open_params);
|
||||||
|
let fid = server.srcs.get_id(&uri);
|
||||||
|
server.srcs.wait_parse_ready(fid, true);
|
||||||
|
|
||||||
|
let change_params = DidChangeTextDocumentParams {
|
||||||
|
text_document: VersionedTextDocumentIdentifier {
|
||||||
|
uri: uri.clone(),
|
||||||
|
version: 3,
|
||||||
|
},
|
||||||
|
content_changes: vec![
|
||||||
|
TextDocumentContentChangeEvent {
|
||||||
|
range: Some(Range {
|
||||||
|
start: Position {
|
||||||
|
line: 4,
|
||||||
|
character: 0,
|
||||||
|
},
|
||||||
|
end: Position {
|
||||||
|
line: 4,
|
||||||
|
character: 0,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
range_length: None,
|
||||||
|
text: "\n".to_owned(),
|
||||||
|
},
|
||||||
|
TextDocumentContentChangeEvent {
|
||||||
|
range: Some(Range {
|
||||||
|
start: Position {
|
||||||
|
line: 4,
|
||||||
|
character: 0,
|
||||||
|
},
|
||||||
|
end: Position {
|
||||||
|
line: 4,
|
||||||
|
character: 0,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
range_length: None,
|
||||||
|
text: " ".to_owned(),
|
||||||
|
},
|
||||||
|
TextDocumentContentChangeEvent {
|
||||||
|
range: Some(Range {
|
||||||
|
start: Position {
|
||||||
|
line: 4,
|
||||||
|
character: 2,
|
||||||
|
},
|
||||||
|
end: Position {
|
||||||
|
line: 4,
|
||||||
|
character: 2,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
range_length: None,
|
||||||
|
text: "a".to_owned(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
server.did_change(change_params);
|
||||||
|
server.srcs.wait_parse_ready(fid, true);
|
||||||
|
|
||||||
|
let completion_params = CompletionParams {
|
||||||
|
text_document_position: TextDocumentPositionParams {
|
||||||
|
text_document: TextDocumentIdentifier { uri },
|
||||||
|
position: Position {
|
||||||
|
line: 4,
|
||||||
|
character: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
work_done_progress_params: WorkDoneProgressParams::default(),
|
||||||
|
partial_result_params: PartialResultParams::default(),
|
||||||
|
context: Some(CompletionContext {
|
||||||
|
trigger_kind: CompletionTriggerKind::INVOKED,
|
||||||
|
trigger_character: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
let response: CompletionResponse = server.completion(completion_params).unwrap();
|
||||||
|
let item1 = CompletionItem {
|
||||||
|
label: "abc".to_owned(),
|
||||||
|
kind: Some(CompletionItemKind::VARIABLE),
|
||||||
|
detail: Some("logic".to_string()),
|
||||||
|
..CompletionItem::default()
|
||||||
|
};
|
||||||
|
let item3 = CompletionItem {
|
||||||
|
label: "aouter".to_owned(),
|
||||||
|
kind: Some(CompletionItemKind::VARIABLE),
|
||||||
|
detail: Some("logic".to_string()),
|
||||||
|
..CompletionItem::default()
|
||||||
|
};
|
||||||
|
if let CompletionResponse::List(item) = response {
|
||||||
|
eprintln!("{:#?}", item);
|
||||||
|
assert!(item.items.contains(&item1));
|
||||||
|
for comp in &item.items {
|
||||||
|
assert!(comp.label != "abcd");
|
||||||
|
}
|
||||||
|
assert!(item.items.contains(&item3));
|
||||||
|
} else {
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_dot_completion() {
|
||||||
|
test_init();
|
||||||
|
let server = LSPServer::new(None);
|
||||||
|
let uri = Url::parse("file:///test.sv").unwrap();
|
||||||
|
let text = r#"interface test_inter;
|
||||||
|
wire abcd;
|
||||||
|
endinterface
|
||||||
|
module test(
|
||||||
|
test_inter abc
|
||||||
|
);
|
||||||
|
abc.
|
||||||
|
test_inter.
|
||||||
|
endmodule
|
||||||
|
"#;
|
||||||
|
let open_params = DidOpenTextDocumentParams {
|
||||||
|
text_document: TextDocumentItem {
|
||||||
|
uri: uri.clone(),
|
||||||
|
language_id: "systemverilog".to_owned(),
|
||||||
|
version: 0,
|
||||||
|
text: text.to_owned(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
server.did_open(open_params);
|
||||||
|
let fid = server.srcs.get_id(&uri);
|
||||||
|
server.srcs.wait_parse_ready(fid, true);
|
||||||
|
let file = server.srcs.get_file(fid).unwrap();
|
||||||
|
let file = file.read().unwrap();
|
||||||
|
eprintln!("{}", file.syntax_tree.as_ref().unwrap());
|
||||||
|
eprintln!(
|
||||||
|
"{:#?}",
|
||||||
|
server.srcs.scope_tree.read().unwrap().as_ref().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
let completion_params = CompletionParams {
|
||||||
|
text_document_position: TextDocumentPositionParams {
|
||||||
|
text_document: TextDocumentIdentifier { uri: uri.clone() },
|
||||||
|
position: Position {
|
||||||
|
line: 6,
|
||||||
|
character: 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
work_done_progress_params: WorkDoneProgressParams::default(),
|
||||||
|
partial_result_params: PartialResultParams::default(),
|
||||||
|
context: Some(CompletionContext {
|
||||||
|
trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
|
||||||
|
trigger_character: Some(".".to_string()),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
let response: CompletionResponse = server.completion(completion_params).unwrap();
|
||||||
|
dbg!(&response);
|
||||||
|
let item1 = CompletionItem {
|
||||||
|
label: "abcd".to_owned(),
|
||||||
|
kind: Some(CompletionItemKind::VARIABLE),
|
||||||
|
detail: Some("wire".to_string()),
|
||||||
|
..CompletionItem::default()
|
||||||
|
};
|
||||||
|
if let CompletionResponse::List(item) = response {
|
||||||
|
eprintln!("{:#?}", item);
|
||||||
|
assert!(item.items.contains(&item1));
|
||||||
|
assert!(item.items.len() == 1);
|
||||||
|
} else {
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
let completion_params = CompletionParams {
|
||||||
|
text_document_position: TextDocumentPositionParams {
|
||||||
|
text_document: TextDocumentIdentifier { uri },
|
||||||
|
position: Position {
|
||||||
|
line: 7,
|
||||||
|
character: 14,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
work_done_progress_params: WorkDoneProgressParams::default(),
|
||||||
|
partial_result_params: PartialResultParams::default(),
|
||||||
|
context: Some(CompletionContext {
|
||||||
|
trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
|
||||||
|
trigger_character: Some(".".to_string()),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
let response: CompletionResponse = server.completion(completion_params).unwrap();
|
||||||
|
if let CompletionResponse::List(item) = response {
|
||||||
|
eprintln!("{:#?}", item);
|
||||||
|
assert!(item.items.contains(&item1));
|
||||||
|
assert!(item.items.len() == 1);
|
||||||
|
} else {
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_trigger_dot_nocontext() {
|
||||||
|
test_init();
|
||||||
|
let server = LSPServer::new(None);
|
||||||
|
let uri = Url::parse("file:///test.sv").unwrap();
|
||||||
|
let text = r#"interface test_inter;
|
||||||
|
wire abcd;
|
||||||
|
endinterface
|
||||||
|
module test(
|
||||||
|
test_inter abc
|
||||||
|
);
|
||||||
|
abc.
|
||||||
|
test_inter.
|
||||||
|
endmodule
|
||||||
|
"#;
|
||||||
|
let open_params = DidOpenTextDocumentParams {
|
||||||
|
text_document: TextDocumentItem {
|
||||||
|
uri: uri.clone(),
|
||||||
|
language_id: "systemverilog".to_owned(),
|
||||||
|
version: 0,
|
||||||
|
text: text.to_owned(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
server.did_open(open_params);
|
||||||
|
let fid = server.srcs.get_id(&uri);
|
||||||
|
server.srcs.wait_parse_ready(fid, true);
|
||||||
|
let file = server.srcs.get_file(fid).unwrap();
|
||||||
|
let file = file.read().unwrap();
|
||||||
|
eprintln!("{}", file.syntax_tree.as_ref().unwrap());
|
||||||
|
eprintln!(
|
||||||
|
"{:#?}",
|
||||||
|
server.srcs.scope_tree.read().unwrap().as_ref().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
let completion_params = CompletionParams {
|
||||||
|
text_document_position: TextDocumentPositionParams {
|
||||||
|
text_document: TextDocumentIdentifier { uri: uri.clone() },
|
||||||
|
position: Position {
|
||||||
|
line: 6,
|
||||||
|
character: 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
work_done_progress_params: WorkDoneProgressParams::default(),
|
||||||
|
partial_result_params: PartialResultParams::default(),
|
||||||
|
context: None,
|
||||||
|
};
|
||||||
|
let response: CompletionResponse = server.completion(completion_params).unwrap();
|
||||||
|
dbg!(&response);
|
||||||
|
let item1 = CompletionItem {
|
||||||
|
label: "abcd".to_owned(),
|
||||||
|
kind: Some(CompletionItemKind::VARIABLE),
|
||||||
|
detail: Some("wire".to_string()),
|
||||||
|
..CompletionItem::default()
|
||||||
|
};
|
||||||
|
if let CompletionResponse::List(item) = response {
|
||||||
|
eprintln!("{:#?}", item);
|
||||||
|
assert!(item.items.contains(&item1));
|
||||||
|
assert!(item.items.len() == 1);
|
||||||
|
} else {
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
let completion_params = CompletionParams {
|
||||||
|
text_document_position: TextDocumentPositionParams {
|
||||||
|
text_document: TextDocumentIdentifier { uri },
|
||||||
|
position: Position {
|
||||||
|
line: 7,
|
||||||
|
character: 14,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
work_done_progress_params: WorkDoneProgressParams::default(),
|
||||||
|
partial_result_params: PartialResultParams::default(),
|
||||||
|
context: Some(CompletionContext {
|
||||||
|
trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
|
||||||
|
trigger_character: Some(".".to_string()),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
let response: CompletionResponse = server.completion(completion_params).unwrap();
|
||||||
|
if let CompletionResponse::List(item) = response {
|
||||||
|
eprintln!("{:#?}", item);
|
||||||
|
assert!(item.items.contains(&item1));
|
||||||
|
assert!(item.items.len() == 1);
|
||||||
|
} else {
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_dot_completion_instantiation() {
|
||||||
|
test_init();
|
||||||
|
let text = r#"interface test_inter;
|
||||||
|
wire wrong;
|
||||||
|
logic clk;
|
||||||
|
endinterface
|
||||||
|
module test;
|
||||||
|
logic clk;
|
||||||
|
test_inter2 t (
|
||||||
|
.clk(clk),
|
||||||
|
.
|
||||||
|
)
|
||||||
|
endmodule
|
||||||
|
interface test_inter2;
|
||||||
|
wire abcd;
|
||||||
|
logic clk;
|
||||||
|
endinterface
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let doc = Rope::from_str(&text);
|
||||||
|
let url = Url::parse("file:///test.sv").unwrap();
|
||||||
|
let syntax_tree = parse(&doc, &url, &None, &Vec::new()).unwrap();
|
||||||
|
let scope_tree = get_scopes(&syntax_tree, &url).unwrap();
|
||||||
|
let pos = Position::new(8, 9);
|
||||||
|
let token = get_completion_token(&doc, doc.line(pos.line as usize), pos);
|
||||||
|
let completions = scope_tree.get_dot_completion(
|
||||||
|
token.trim_end_matches('.'),
|
||||||
|
doc.pos_to_byte(&pos),
|
||||||
|
&url,
|
||||||
|
&scope_tree,
|
||||||
|
);
|
||||||
|
let labels: Vec<String> = completions.iter().map(|x| x.label.clone()).collect();
|
||||||
|
assert_eq!(labels, vec!["abcd", "clk"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
#[test]
|
||||||
|
fn test_package_completion() {
|
||||||
|
test_init();
|
||||||
|
let text = r#"package p;
|
||||||
|
struct {int x;} s1;
|
||||||
|
struct {int x;} s2;
|
||||||
|
function void f();
|
||||||
|
int x;
|
||||||
|
endfunction
|
||||||
|
endpackage
|
||||||
|
module m;
|
||||||
|
import p::*;
|
||||||
|
if (1) begin : s1
|
||||||
|
initial begin
|
||||||
|
s1.x = 1;
|
||||||
|
f.x = 1;
|
||||||
|
end
|
||||||
|
int x;
|
||||||
|
end
|
||||||
|
endmodule
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let doc = Rope::from_str(&text);
|
||||||
|
let url = Url::parse("file:///test.sv").unwrap();
|
||||||
|
let syntax_tree = parse(&doc, &url, &None, &Vec::new()).unwrap();
|
||||||
|
let scope_tree = get_scopes(&syntax_tree, &url).unwrap();
|
||||||
|
dbg!(&scope_tree);
|
||||||
|
/*
|
||||||
|
let pos = Position::new(8, 9);
|
||||||
|
let token = get_completion_token(&doc, doc.line(pos.line as usize), pos);
|
||||||
|
let completions = scope_tree.get_dot_completion(
|
||||||
|
token.trim_end_matches('.'),
|
||||||
|
doc.pos_to_byte(&pos),
|
||||||
|
&url,
|
||||||
|
&scope_tree,
|
||||||
|
);
|
||||||
|
let labels: Vec<String> = completions.iter().map(|x| x.label.clone()).collect();
|
||||||
|
assert_eq!(labels, vec!["abcd", "clk"]);
|
||||||
|
*/
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_inter_file_completion() {
|
||||||
|
test_init();
|
||||||
|
let server = LSPServer::new(None);
|
||||||
|
let uri = Url::parse("file:///test.sv").unwrap();
|
||||||
|
let uri2 = Url::parse("file:///test2.sv").unwrap();
|
||||||
|
let text = r#"module test;
|
||||||
|
s
|
||||||
|
endmodule
|
||||||
|
"#;
|
||||||
|
let text2 = r#"interface simple_bus;
|
||||||
|
logic clk;
|
||||||
|
endinterface"#;
|
||||||
|
let open_params = DidOpenTextDocumentParams {
|
||||||
|
text_document: TextDocumentItem {
|
||||||
|
uri: uri.clone(),
|
||||||
|
language_id: "systemverilog".to_owned(),
|
||||||
|
version: 0,
|
||||||
|
text: text.to_owned(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let open_params2 = DidOpenTextDocumentParams {
|
||||||
|
text_document: TextDocumentItem {
|
||||||
|
uri: uri2.clone(),
|
||||||
|
language_id: "systemverilog".to_owned(),
|
||||||
|
version: 0,
|
||||||
|
text: text2.to_owned(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
server.did_open(open_params);
|
||||||
|
server.did_open(open_params2);
|
||||||
|
let fid = server.srcs.get_id(&uri);
|
||||||
|
let fid2 = server.srcs.get_id(&uri2);
|
||||||
|
server.srcs.wait_parse_ready(fid, true);
|
||||||
|
server.srcs.wait_parse_ready(fid2, true);
|
||||||
|
|
||||||
|
let completion_params = CompletionParams {
|
||||||
|
text_document_position: TextDocumentPositionParams {
|
||||||
|
text_document: TextDocumentIdentifier { uri },
|
||||||
|
position: Position {
|
||||||
|
line: 1,
|
||||||
|
character: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
work_done_progress_params: WorkDoneProgressParams::default(),
|
||||||
|
partial_result_params: PartialResultParams::default(),
|
||||||
|
context: Some(CompletionContext {
|
||||||
|
trigger_kind: CompletionTriggerKind::INVOKED,
|
||||||
|
trigger_character: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
let response: CompletionResponse = server.completion(completion_params).unwrap();
|
||||||
|
let scope_tree = server.srcs.scope_tree.read().unwrap();
|
||||||
|
dbg!(scope_tree.as_ref().unwrap());
|
||||||
|
if let CompletionResponse::List(item) = response {
|
||||||
|
// eprintln!("{:#?}", item);
|
||||||
|
let names: Vec<&String> = item.items.iter().map(|x| &x.label).collect();
|
||||||
|
assert!(names.contains(&&"simple_bus".to_string()));
|
||||||
|
} else {
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
466
src/completion/keyword.rs
Normal file
466
src/completion/keyword.rs
Normal file
@ -0,0 +1,466 @@
|
|||||||
|
use tower_lsp::lsp_types::*;
|
||||||
|
|
||||||
|
pub fn keyword_completions(keywords: &[(&str, &str)]) -> Vec<CompletionItem> {
|
||||||
|
let mut items: Vec<CompletionItem> = Vec::new();
|
||||||
|
for key in keywords {
|
||||||
|
if key.1.is_empty() {
|
||||||
|
items.push(CompletionItem {
|
||||||
|
label: key.0.to_string(),
|
||||||
|
kind: Some(CompletionItemKind::KEYWORD),
|
||||||
|
..CompletionItem::default()
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
items.push(CompletionItem {
|
||||||
|
label: key.0.to_string(),
|
||||||
|
kind: Some(CompletionItemKind::KEYWORD),
|
||||||
|
insert_text: Some(key.1.to_string()),
|
||||||
|
insert_text_format: Some(InsertTextFormat::SNIPPET),
|
||||||
|
..CompletionItem::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn other_completions(tasks: &[&str]) -> Vec<CompletionItem> {
|
||||||
|
tasks
|
||||||
|
.iter()
|
||||||
|
.map(|x| CompletionItem {
|
||||||
|
label: x.to_string(),
|
||||||
|
kind: Some(CompletionItemKind::FUNCTION),
|
||||||
|
..CompletionItem::default()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const KEYWORDS: &[(&str, &str)] = &[
|
||||||
|
("accept_on", ""),
|
||||||
|
("alias", ""),
|
||||||
|
("always", "always @($1) begin\nend"),
|
||||||
|
("always_comb", "always_comb begin\n\t$1\nend"),
|
||||||
|
("always_ff", "always_ff @($1) begin\nend"),
|
||||||
|
("always_latch", "always_latch begin\n\t$1\nend"),
|
||||||
|
("and", ""),
|
||||||
|
("assert", ""),
|
||||||
|
("assign", ""),
|
||||||
|
("assume", ""),
|
||||||
|
("automatic", ""),
|
||||||
|
("before", ""),
|
||||||
|
("begin", "begin\n\t$1\nend"),
|
||||||
|
("bind", ""),
|
||||||
|
("bins", ""),
|
||||||
|
("binsof", ""),
|
||||||
|
("bit", ""),
|
||||||
|
("break", ""),
|
||||||
|
("buf", ""),
|
||||||
|
("bufif0", ""),
|
||||||
|
("bufif1", ""),
|
||||||
|
("byte", ""),
|
||||||
|
("case", "case $1;\nendcase"),
|
||||||
|
("casex", "casex $1;\nendcase"),
|
||||||
|
("casez", "casez $1;\nendcase"),
|
||||||
|
("cell", ""),
|
||||||
|
("chandle", ""),
|
||||||
|
("checker", "checker $1;\nendchecker"),
|
||||||
|
("class", "class $1;\nendclass"),
|
||||||
|
("clocking", "clocking $1;\nendclocking"),
|
||||||
|
("cmos", ""),
|
||||||
|
("config", "config $1;\nendconfig"),
|
||||||
|
("const", ""),
|
||||||
|
("constraint", ""),
|
||||||
|
("context", ""),
|
||||||
|
("continue", ""),
|
||||||
|
("cover", ""),
|
||||||
|
("covergroup", ""),
|
||||||
|
("coverpoint", ""),
|
||||||
|
("cross", ""),
|
||||||
|
("deassign", ""),
|
||||||
|
("default", ""),
|
||||||
|
("defparam", ""),
|
||||||
|
("design", ""),
|
||||||
|
("disable", ""),
|
||||||
|
("dist", ""),
|
||||||
|
("do", ""),
|
||||||
|
("edge", ""),
|
||||||
|
("else", ""),
|
||||||
|
("end", ""),
|
||||||
|
("endcase", ""),
|
||||||
|
("endchecker", ""),
|
||||||
|
("endclass", ""),
|
||||||
|
("endclocking", ""),
|
||||||
|
("endconfig", ""),
|
||||||
|
("endfunction", ""),
|
||||||
|
("endgenerate", ""),
|
||||||
|
("endgroup", ""),
|
||||||
|
("endinterface", ""),
|
||||||
|
("endmodule", ""),
|
||||||
|
("endpackage", ""),
|
||||||
|
("endprimitive", ""),
|
||||||
|
("endprogram", ""),
|
||||||
|
("endproperty", ""),
|
||||||
|
("endspecify", ""),
|
||||||
|
("endsequence", ""),
|
||||||
|
("endtable", ""),
|
||||||
|
("endtask", ""),
|
||||||
|
("enum", ""),
|
||||||
|
("event", ""),
|
||||||
|
("eventually", ""),
|
||||||
|
("expect", ""),
|
||||||
|
("export", ""),
|
||||||
|
("extends", ""),
|
||||||
|
("extern", ""),
|
||||||
|
("final", ""),
|
||||||
|
("first_match", ""),
|
||||||
|
("for", ""),
|
||||||
|
("force", ""),
|
||||||
|
("foreach", ""),
|
||||||
|
("forever", ""),
|
||||||
|
("fork", ""),
|
||||||
|
("forkjoin", ""),
|
||||||
|
("function", "function $1;\nendfunction"),
|
||||||
|
("generate", "generate\n\t$1\nendgenerate"),
|
||||||
|
("genvar", ""),
|
||||||
|
("global", ""),
|
||||||
|
("highz0", ""),
|
||||||
|
("highz1", ""),
|
||||||
|
("if", ""),
|
||||||
|
("iff", ""),
|
||||||
|
("ifnone", ""),
|
||||||
|
("ignore_bins", ""),
|
||||||
|
("illegal_bins", ""),
|
||||||
|
("implements", ""),
|
||||||
|
("implies", ""),
|
||||||
|
("import", ""),
|
||||||
|
("incdir", ""),
|
||||||
|
("include", ""),
|
||||||
|
("initial", ""),
|
||||||
|
("inout", ""),
|
||||||
|
("input", ""),
|
||||||
|
("inside", ""),
|
||||||
|
("instance", ""),
|
||||||
|
("int", ""),
|
||||||
|
("integer", ""),
|
||||||
|
("interconnect", ""),
|
||||||
|
("interface", "interface $1;\nendinterface"),
|
||||||
|
("intersect", ""),
|
||||||
|
("join", ""),
|
||||||
|
("join_any", ""),
|
||||||
|
("join_none", ""),
|
||||||
|
("large", ""),
|
||||||
|
("let", ""),
|
||||||
|
("liblist", ""),
|
||||||
|
("library", ""),
|
||||||
|
("local", ""),
|
||||||
|
("localparam", ""),
|
||||||
|
("logic", ""),
|
||||||
|
("longint", ""),
|
||||||
|
("macromodule", ""),
|
||||||
|
("matches", ""),
|
||||||
|
("medium", ""),
|
||||||
|
("modport", ""),
|
||||||
|
("module", "module $1 ($2);\nendmodule"),
|
||||||
|
("nand", ""),
|
||||||
|
("negedge", ""),
|
||||||
|
("nettype", ""),
|
||||||
|
("new", ""),
|
||||||
|
("nexttime", ""),
|
||||||
|
("nmos", ""),
|
||||||
|
("nor", ""),
|
||||||
|
("noshowcancelled", ""),
|
||||||
|
("not", ""),
|
||||||
|
("notif0", ""),
|
||||||
|
("notif1", ""),
|
||||||
|
("null", ""),
|
||||||
|
("or", ""),
|
||||||
|
("output", ""),
|
||||||
|
("package", "package $1;\nendpackage"),
|
||||||
|
("packed", ""),
|
||||||
|
("parameter", ""),
|
||||||
|
("pmos", ""),
|
||||||
|
("posedge", ""),
|
||||||
|
("primitive", "primitive $1;\nendprimitive"),
|
||||||
|
("priority", ""),
|
||||||
|
("program", "program $1;\nendprogram"),
|
||||||
|
("property", "property $1;\nendproperty"),
|
||||||
|
("protected", ""),
|
||||||
|
("pull0", ""),
|
||||||
|
("pull1", ""),
|
||||||
|
("pulldown", ""),
|
||||||
|
("pullup", ""),
|
||||||
|
("pulsestyle_ondetect", ""),
|
||||||
|
("pulsestyle_onevent", ""),
|
||||||
|
("pure", ""),
|
||||||
|
("rand", ""),
|
||||||
|
("randc", ""),
|
||||||
|
("randcase", ""),
|
||||||
|
("randsequence", ""),
|
||||||
|
("rcmos", ""),
|
||||||
|
("real", ""),
|
||||||
|
("realtime", ""),
|
||||||
|
("ref", ""),
|
||||||
|
("reg", ""),
|
||||||
|
("reject_on", ""),
|
||||||
|
("release", ""),
|
||||||
|
("repeat", ""),
|
||||||
|
("restrict", ""),
|
||||||
|
("return", ""),
|
||||||
|
("rnmos", ""),
|
||||||
|
("rpmos", ""),
|
||||||
|
("rtran", ""),
|
||||||
|
("rtranif0", ""),
|
||||||
|
("rtranif1", ""),
|
||||||
|
("s_always", ""),
|
||||||
|
("s_eventually", ""),
|
||||||
|
("s_nexttime", ""),
|
||||||
|
("s_until", ""),
|
||||||
|
("s_until_with", ""),
|
||||||
|
("scalared", ""),
|
||||||
|
("sequence", "sequence $1;\nendsequence"),
|
||||||
|
("shortint", ""),
|
||||||
|
("shortreal", ""),
|
||||||
|
("showcancelled", ""),
|
||||||
|
("signed", ""),
|
||||||
|
("small", ""),
|
||||||
|
("soft", ""),
|
||||||
|
("solve", ""),
|
||||||
|
("specify", "specify $1;\nendspecify"),
|
||||||
|
("specparam", ""),
|
||||||
|
("static", ""),
|
||||||
|
("string", ""),
|
||||||
|
("strong", ""),
|
||||||
|
("strong0", ""),
|
||||||
|
("strong1", ""),
|
||||||
|
("struct", ""),
|
||||||
|
("super", ""),
|
||||||
|
("supply0", ""),
|
||||||
|
("supply1", ""),
|
||||||
|
("sync_accept_on", ""),
|
||||||
|
("sync_reject_on", ""),
|
||||||
|
("table", "table $1;\nendtable"),
|
||||||
|
("tagged", ""),
|
||||||
|
("task", "task $1;\nendtask"),
|
||||||
|
("this", ""),
|
||||||
|
("throughout", ""),
|
||||||
|
("time", ""),
|
||||||
|
("timeprecision", ""),
|
||||||
|
("timeunit", ""),
|
||||||
|
("tran", ""),
|
||||||
|
("tranif0", ""),
|
||||||
|
("tranif1", ""),
|
||||||
|
("tri", ""),
|
||||||
|
("tri0", ""),
|
||||||
|
("tri1", ""),
|
||||||
|
("triand", ""),
|
||||||
|
("trior", ""),
|
||||||
|
("trireg", ""),
|
||||||
|
("type", ""),
|
||||||
|
("typedef", ""),
|
||||||
|
("union", ""),
|
||||||
|
("unique", ""),
|
||||||
|
("unique0", ""),
|
||||||
|
("unsigned", ""),
|
||||||
|
("until", ""),
|
||||||
|
("until_with", ""),
|
||||||
|
("untyped", ""),
|
||||||
|
("use", ""),
|
||||||
|
("uwire", ""),
|
||||||
|
("var", ""),
|
||||||
|
("vectored", ""),
|
||||||
|
("virtual", ""),
|
||||||
|
("void", ""),
|
||||||
|
("wait", ""),
|
||||||
|
("wait_order", ""),
|
||||||
|
("wand", ""),
|
||||||
|
("weak", ""),
|
||||||
|
("weak0", ""),
|
||||||
|
("weak1", ""),
|
||||||
|
("while", ""),
|
||||||
|
("wildcard", ""),
|
||||||
|
("wire", ""),
|
||||||
|
("with", ""),
|
||||||
|
("within", ""),
|
||||||
|
("wor", ""),
|
||||||
|
("xnor", ""),
|
||||||
|
("xor", ""),
|
||||||
|
];
|
||||||
|
|
||||||
|
pub const SYS_TASKS: &[&str] = &[
|
||||||
|
"finish",
|
||||||
|
"exit",
|
||||||
|
"fatal",
|
||||||
|
"warning",
|
||||||
|
"stop",
|
||||||
|
"error",
|
||||||
|
"info",
|
||||||
|
"realtime",
|
||||||
|
"time",
|
||||||
|
"asserton",
|
||||||
|
"assertkill",
|
||||||
|
"assertpasson",
|
||||||
|
"assertfailon",
|
||||||
|
"assertnonvacuouson",
|
||||||
|
"stime",
|
||||||
|
"printtimescale",
|
||||||
|
"timeformat",
|
||||||
|
"bitstoreal",
|
||||||
|
"bitstoshortreal",
|
||||||
|
"itor",
|
||||||
|
"signed",
|
||||||
|
"cast",
|
||||||
|
"realtobits",
|
||||||
|
"shortrealtobits",
|
||||||
|
"rtoi",
|
||||||
|
"unsigned",
|
||||||
|
"sampled",
|
||||||
|
"fell",
|
||||||
|
"changed",
|
||||||
|
"past_gclk",
|
||||||
|
"fell_gclk",
|
||||||
|
"changed_gclk",
|
||||||
|
"rising_gclk",
|
||||||
|
"steady_gclk",
|
||||||
|
"bits",
|
||||||
|
"typename",
|
||||||
|
"isunbounded",
|
||||||
|
"coverage_control",
|
||||||
|
"coverage_get",
|
||||||
|
"coverage_save",
|
||||||
|
"set_coverage_db_name",
|
||||||
|
"dimensions",
|
||||||
|
"right",
|
||||||
|
"high",
|
||||||
|
"size",
|
||||||
|
"random",
|
||||||
|
"dist_erlang",
|
||||||
|
"dist_normal",
|
||||||
|
"dist_t",
|
||||||
|
"asin",
|
||||||
|
"acos",
|
||||||
|
"atan",
|
||||||
|
"atan2",
|
||||||
|
"hypot",
|
||||||
|
"sinh",
|
||||||
|
"cosh",
|
||||||
|
"tanh",
|
||||||
|
"asinh",
|
||||||
|
"acosh",
|
||||||
|
"atanh",
|
||||||
|
"q_initialize",
|
||||||
|
"q_remove",
|
||||||
|
"q_exam",
|
||||||
|
"q_add",
|
||||||
|
"q_full",
|
||||||
|
"async$and$array",
|
||||||
|
"async$nand$array",
|
||||||
|
"async$or$array",
|
||||||
|
"async$nor$array",
|
||||||
|
"sync$and$array",
|
||||||
|
"sync$nand$array",
|
||||||
|
"sync$or$array",
|
||||||
|
"sync$nor$array",
|
||||||
|
"countones",
|
||||||
|
"onehot0",
|
||||||
|
"fatal",
|
||||||
|
"warning",
|
||||||
|
"dist_chi_square",
|
||||||
|
"dist_exponential",
|
||||||
|
"dist_poisson",
|
||||||
|
"dist_uniform",
|
||||||
|
"countbits",
|
||||||
|
"onehot",
|
||||||
|
"isunknown",
|
||||||
|
"coverage_get_max",
|
||||||
|
"coverage_merge",
|
||||||
|
"get_coverage",
|
||||||
|
"load_coverage_db",
|
||||||
|
"clog2",
|
||||||
|
"ln",
|
||||||
|
"log10",
|
||||||
|
"exp",
|
||||||
|
"sqrt",
|
||||||
|
"pow",
|
||||||
|
"floor",
|
||||||
|
"ceil",
|
||||||
|
"sin",
|
||||||
|
"cos",
|
||||||
|
"tan",
|
||||||
|
"rose",
|
||||||
|
"stable",
|
||||||
|
"past",
|
||||||
|
"rose_gclk",
|
||||||
|
"stable_gclk",
|
||||||
|
"future_gclk",
|
||||||
|
"falling_gclk",
|
||||||
|
"changing_gclk",
|
||||||
|
"unpacked_dimensions",
|
||||||
|
"left",
|
||||||
|
"low",
|
||||||
|
"increment",
|
||||||
|
"assertoff",
|
||||||
|
"assertcontrol",
|
||||||
|
"assertpassoff",
|
||||||
|
"assertfailoff",
|
||||||
|
"assertvacuousoff",
|
||||||
|
"error",
|
||||||
|
"info",
|
||||||
|
"async$and$plane",
|
||||||
|
"async$nand$plane",
|
||||||
|
"async$or$plane",
|
||||||
|
"async$nor$plane",
|
||||||
|
"sync$and$plane",
|
||||||
|
"sync$nand$plane",
|
||||||
|
"sync$or$plane",
|
||||||
|
"sync$nor$plane",
|
||||||
|
"system",
|
||||||
|
"countdrivers",
|
||||||
|
"getpattern",
|
||||||
|
"incsave",
|
||||||
|
"input",
|
||||||
|
"key",
|
||||||
|
"list",
|
||||||
|
"log",
|
||||||
|
"nokey",
|
||||||
|
"nolog",
|
||||||
|
"reset",
|
||||||
|
"reset_count",
|
||||||
|
"reset_value",
|
||||||
|
"restart",
|
||||||
|
"save",
|
||||||
|
"scale",
|
||||||
|
"scope",
|
||||||
|
"showscopes",
|
||||||
|
"showvars",
|
||||||
|
"sreadmemb",
|
||||||
|
"sreadmemh",
|
||||||
|
];
|
||||||
|
|
||||||
|
pub const DIRECTIVES: &[&str] = &[
|
||||||
|
"__FILE__",
|
||||||
|
"__LINE__",
|
||||||
|
"begin_keywords",
|
||||||
|
"celldefine",
|
||||||
|
"default_nettype",
|
||||||
|
"define",
|
||||||
|
"else",
|
||||||
|
"elsif",
|
||||||
|
"end_keywords",
|
||||||
|
"endcelldefine",
|
||||||
|
"endif",
|
||||||
|
"ifdef",
|
||||||
|
"ifndef",
|
||||||
|
"include",
|
||||||
|
"line",
|
||||||
|
"nounconnected_drive",
|
||||||
|
"pragma",
|
||||||
|
"resetall",
|
||||||
|
"timescale",
|
||||||
|
"unconnected_drive",
|
||||||
|
"undef",
|
||||||
|
"undefineall",
|
||||||
|
"default_decay_time",
|
||||||
|
"default_trireg_strength",
|
||||||
|
"delay_mode_distributed",
|
||||||
|
"delay_mode_path",
|
||||||
|
"delay_mode_unit",
|
||||||
|
"delay_mode_zero",
|
||||||
|
];
|
499
src/definition.rs
Normal file
499
src/definition.rs
Normal file
@ -0,0 +1,499 @@
|
|||||||
|
use crate::definition::extract_defs::get_ident;
|
||||||
|
use crate::server::LSPServer;
|
||||||
|
use crate::sources::LSPSupport;
|
||||||
|
use log::{debug, trace};
|
||||||
|
use ropey::{Rope, RopeSlice};
|
||||||
|
use sv_parser::*;
|
||||||
|
use tower_lsp::lsp_types::*;
|
||||||
|
|
||||||
|
pub mod def_types;
|
||||||
|
pub use def_types::*;
|
||||||
|
|
||||||
|
mod extract_defs;
|
||||||
|
use extract_defs::*;
|
||||||
|
|
||||||
|
impl LSPServer {
|
||||||
|
pub fn goto_definition(&self, params: GotoDefinitionParams) -> Option<GotoDefinitionResponse> {
|
||||||
|
let doc = params.text_document_position_params.text_document.uri;
|
||||||
|
let pos = params.text_document_position_params.position;
|
||||||
|
let file_id = self.srcs.get_id(&doc).to_owned();
|
||||||
|
self.srcs.wait_parse_ready(file_id, false);
|
||||||
|
let file = self.srcs.get_file(file_id)?;
|
||||||
|
let file = file.read().ok()?;
|
||||||
|
let token = get_definition_token(file.text.line(pos.line as usize), pos);
|
||||||
|
debug!("goto definition, token: {}", &token);
|
||||||
|
let scope_tree = self.srcs.scope_tree.read().ok()?;
|
||||||
|
trace!("{:#?}", scope_tree.as_ref()?);
|
||||||
|
let def = scope_tree
|
||||||
|
.as_ref()?
|
||||||
|
.get_definition(&token, file.text.pos_to_byte(&pos), &doc)?;
|
||||||
|
let def_pos = file.text.byte_to_pos(def.byte_idx());
|
||||||
|
debug!("def: {:?}", def_pos);
|
||||||
|
Some(GotoDefinitionResponse::Scalar(Location::new(
|
||||||
|
def.url(),
|
||||||
|
Range::new(def_pos, def_pos),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hover(&self, params: HoverParams) -> Option<Hover> {
|
||||||
|
let doc = params.text_document_position_params.text_document.uri;
|
||||||
|
let pos = params.text_document_position_params.position;
|
||||||
|
let file_id = self.srcs.get_id(&doc).to_owned();
|
||||||
|
self.srcs.wait_parse_ready(file_id, false);
|
||||||
|
let file = self.srcs.get_file(file_id)?;
|
||||||
|
let file = file.read().ok()?;
|
||||||
|
let token = get_definition_token(file.text.line(pos.line as usize), pos);
|
||||||
|
debug!("hover, token: {}", &token);
|
||||||
|
let scope_tree = self.srcs.scope_tree.read().ok()?;
|
||||||
|
let def = scope_tree
|
||||||
|
.as_ref()?
|
||||||
|
.get_definition(&token, file.text.pos_to_byte(&pos), &doc)?;
|
||||||
|
let def_line = file.text.byte_to_line(def.byte_idx());
|
||||||
|
Some(Hover {
|
||||||
|
contents: HoverContents::Scalar(MarkedString::LanguageString(LanguageString {
|
||||||
|
language: "systemverilog".to_owned(),
|
||||||
|
value: get_hover(&file.text, def_line),
|
||||||
|
})),
|
||||||
|
range: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn document_symbol(&self, params: DocumentSymbolParams) -> Option<DocumentSymbolResponse> {
|
||||||
|
let uri = params.text_document.uri;
|
||||||
|
let file_id = self.srcs.get_id(&uri).to_owned();
|
||||||
|
self.srcs.wait_parse_ready(file_id, false);
|
||||||
|
let file = self.srcs.get_file(file_id)?;
|
||||||
|
let file = file.read().ok()?;
|
||||||
|
let scope_tree = self.srcs.scope_tree.read().ok()?;
|
||||||
|
Some(DocumentSymbolResponse::Nested(
|
||||||
|
scope_tree.as_ref()?.document_symbols(&uri, &file.text),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn document_highlight(
|
||||||
|
&self,
|
||||||
|
params: DocumentHighlightParams,
|
||||||
|
) -> Option<Vec<DocumentHighlight>> {
|
||||||
|
let uri = params.text_document_position_params.text_document.uri;
|
||||||
|
let pos = params.text_document_position_params.position;
|
||||||
|
let file_id = self.srcs.get_id(&uri).to_owned();
|
||||||
|
self.srcs.wait_parse_ready(file_id, false);
|
||||||
|
let file = self.srcs.get_file(file_id)?;
|
||||||
|
let file = file.read().ok()?;
|
||||||
|
let token = get_definition_token(file.text.line(pos.line as usize), pos);
|
||||||
|
let scope_tree = self.srcs.scope_tree.read().ok()?;
|
||||||
|
// use the byte_idx of the definition if possible, otherwise use the cursor
|
||||||
|
let byte_idx =
|
||||||
|
match scope_tree
|
||||||
|
.as_ref()?
|
||||||
|
.get_definition(&token, file.text.pos_to_byte(&pos), &uri)
|
||||||
|
{
|
||||||
|
Some(def) => def.byte_idx,
|
||||||
|
None => file.text.pos_to_byte(&pos),
|
||||||
|
};
|
||||||
|
let syntax_tree = file.syntax_tree.as_ref()?;
|
||||||
|
let references = all_identifiers(syntax_tree, &token);
|
||||||
|
Some(
|
||||||
|
scope_tree
|
||||||
|
.as_ref()?
|
||||||
|
.document_highlights(&uri, &file.text, references, byte_idx),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// return all identifiers in a syntax tree matching a given token
|
||||||
|
fn all_identifiers(syntax_tree: &SyntaxTree, token: &str) -> Vec<(String, usize)> {
|
||||||
|
let mut idents: Vec<(String, usize)> = Vec::new();
|
||||||
|
for node in syntax_tree {
|
||||||
|
if let RefNode::Identifier(_) = node {
|
||||||
|
let (ident, byte_idx) = get_ident(syntax_tree, node);
|
||||||
|
if ident == token {
|
||||||
|
idents.push((ident, byte_idx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
idents
|
||||||
|
}
|
||||||
|
|
||||||
|
/// retrieve the token the user invoked goto definition or hover on
|
||||||
|
fn get_definition_token(line: RopeSlice, pos: Position) -> String {
|
||||||
|
let mut token = String::new();
|
||||||
|
let mut line_iter = line.chars();
|
||||||
|
for _ in 0..(line.utf16_cu_to_char(pos.character as usize)) {
|
||||||
|
line_iter.next();
|
||||||
|
}
|
||||||
|
let mut c = line_iter.prev();
|
||||||
|
while c.is_some() && (c.unwrap().is_alphanumeric() || c.unwrap() == '_') {
|
||||||
|
token.push(c.unwrap());
|
||||||
|
c = line_iter.prev();
|
||||||
|
}
|
||||||
|
token = token.chars().rev().collect();
|
||||||
|
line_iter = line.chars();
|
||||||
|
for _ in 0..(line.utf16_cu_to_char(pos.character as usize)) {
|
||||||
|
line_iter.next();
|
||||||
|
}
|
||||||
|
let mut c = line_iter.next();
|
||||||
|
while c.is_some() && (c.unwrap().is_alphanumeric() || c.unwrap() == '_') {
|
||||||
|
token.push(c.unwrap());
|
||||||
|
c = line_iter.next();
|
||||||
|
}
|
||||||
|
token
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScopesAndDefs = Option<(Vec<Box<dyn Scope>>, Vec<Box<dyn Definition>>)>;
|
||||||
|
|
||||||
|
/// Take a given syntax node from a sv-parser syntax tree and extract out the definition/scope at
|
||||||
|
/// that point.
|
||||||
|
pub fn match_definitions(
|
||||||
|
syntax_tree: &SyntaxTree,
|
||||||
|
event_iter: &mut EventIter,
|
||||||
|
node: RefNode,
|
||||||
|
url: &Url,
|
||||||
|
) -> ScopesAndDefs {
|
||||||
|
let mut definitions: Vec<Box<dyn Definition>> = Vec::new();
|
||||||
|
let mut scopes: Vec<Box<dyn Scope>> = Vec::new();
|
||||||
|
match node {
|
||||||
|
RefNode::ModuleDeclaration(n) => {
|
||||||
|
let module = module_dec(syntax_tree, n, event_iter, url);
|
||||||
|
if module.is_some() {
|
||||||
|
scopes.push(Box::new(module?));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RefNode::InterfaceDeclaration(n) => {
|
||||||
|
let interface = interface_dec(syntax_tree, n, event_iter, url);
|
||||||
|
if interface.is_some() {
|
||||||
|
scopes.push(Box::new(interface?));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RefNode::UdpDeclaration(n) => {
|
||||||
|
let dec = udp_dec(syntax_tree, n, event_iter, url);
|
||||||
|
if dec.is_some() {
|
||||||
|
scopes.push(Box::new(dec?));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RefNode::ProgramDeclaration(n) => {
|
||||||
|
let dec = program_dec(syntax_tree, n, event_iter, url);
|
||||||
|
if dec.is_some() {
|
||||||
|
scopes.push(Box::new(dec?));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RefNode::PackageDeclaration(n) => {
|
||||||
|
let dec = package_dec(syntax_tree, n, event_iter, url);
|
||||||
|
if dec.is_some() {
|
||||||
|
scopes.push(Box::new(dec?));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RefNode::ConfigDeclaration(n) => {
|
||||||
|
let dec = config_dec(syntax_tree, n, event_iter, url);
|
||||||
|
if dec.is_some() {
|
||||||
|
scopes.push(Box::new(dec?));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RefNode::ClassDeclaration(n) => {
|
||||||
|
let dec = class_dec(syntax_tree, n, event_iter, url);
|
||||||
|
if dec.is_some() {
|
||||||
|
scopes.push(Box::new(dec?));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RefNode::PortDeclaration(n) => {
|
||||||
|
let ports = port_dec_non_ansi(syntax_tree, n, event_iter, url);
|
||||||
|
if ports.is_some() {
|
||||||
|
for port in ports? {
|
||||||
|
definitions.push(Box::new(port));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RefNode::NetDeclaration(n) => {
|
||||||
|
let nets = net_dec(syntax_tree, n, event_iter, url);
|
||||||
|
if nets.is_some() {
|
||||||
|
for net in nets? {
|
||||||
|
definitions.push(Box::new(net));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RefNode::DataDeclaration(n) => {
|
||||||
|
let vars = data_dec(syntax_tree, n, event_iter, url);
|
||||||
|
if let Some(vars) = vars {
|
||||||
|
for var in vars {
|
||||||
|
match var {
|
||||||
|
Declaration::Dec(dec) => definitions.push(Box::new(dec)),
|
||||||
|
Declaration::Import(dec) => definitions.push(Box::new(dec)),
|
||||||
|
Declaration::Scope(scope) => scopes.push(Box::new(scope)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RefNode::ParameterDeclaration(n) => {
|
||||||
|
let vars = param_dec(syntax_tree, n, event_iter, url);
|
||||||
|
if vars.is_some() {
|
||||||
|
for var in vars? {
|
||||||
|
definitions.push(Box::new(var));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RefNode::LocalParameterDeclaration(n) => {
|
||||||
|
let vars = localparam_dec(syntax_tree, n, event_iter, url);
|
||||||
|
if vars.is_some() {
|
||||||
|
for var in vars? {
|
||||||
|
definitions.push(Box::new(var));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RefNode::FunctionDeclaration(n) => {
|
||||||
|
let dec = function_dec(syntax_tree, n, event_iter, url);
|
||||||
|
if dec.is_some() {
|
||||||
|
scopes.push(Box::new(dec?));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RefNode::TaskDeclaration(n) => {
|
||||||
|
let dec = task_dec(syntax_tree, n, event_iter, url);
|
||||||
|
if dec.is_some() {
|
||||||
|
scopes.push(Box::new(dec?));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RefNode::ModportDeclaration(n) => {
|
||||||
|
let decs = modport_dec(syntax_tree, n, event_iter, url);
|
||||||
|
if decs.is_some() {
|
||||||
|
for dec in decs? {
|
||||||
|
definitions.push(Box::new(dec));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RefNode::ModuleInstantiation(n) => {
|
||||||
|
let decs = module_inst(syntax_tree, n, event_iter, url);
|
||||||
|
if decs.is_some() {
|
||||||
|
for dec in decs? {
|
||||||
|
definitions.push(Box::new(dec));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RefNode::TextMacroDefinition(n) => {
|
||||||
|
let dec = text_macro_def(syntax_tree, n, event_iter, url);
|
||||||
|
if dec.is_some() {
|
||||||
|
definitions.push(Box::new(dec?));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
Some((scopes, definitions))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// convert the syntax tree to a scope tree
|
||||||
|
/// the root node is the global scope
|
||||||
|
pub fn get_scopes(syntax_tree: &SyntaxTree, url: &Url) -> Option<GenericScope> {
|
||||||
|
trace!("{}", syntax_tree);
|
||||||
|
let mut scopes: Vec<Box<dyn Scope>> = Vec::new();
|
||||||
|
let mut global_scope: GenericScope = GenericScope::new(url);
|
||||||
|
global_scope.ident = "global".to_string();
|
||||||
|
let mut event_iter = syntax_tree.into_iter().event();
|
||||||
|
// iterate over each enter event and extract out any scopes or definitions
|
||||||
|
// match_definitions is recursively called so we get a tree in the end
|
||||||
|
while let Some(event) = event_iter.next() {
|
||||||
|
match event {
|
||||||
|
NodeEvent::Enter(node) => {
|
||||||
|
let mut result = match_definitions(syntax_tree, &mut event_iter, node, url)?;
|
||||||
|
global_scope.defs.append(&mut result.1);
|
||||||
|
scopes.append(&mut result.0);
|
||||||
|
}
|
||||||
|
NodeEvent::Leave(_) => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
global_scope.scopes.append(&mut scopes);
|
||||||
|
Some(global_scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get the hover information
|
||||||
|
fn get_hover(doc: &Rope, line: usize) -> String {
|
||||||
|
if line == 0 {
|
||||||
|
return doc.line(line).to_string();
|
||||||
|
}
|
||||||
|
let mut hover: Vec<String> = Vec::new();
|
||||||
|
let mut multiline: bool = false;
|
||||||
|
let mut valid: bool = true;
|
||||||
|
let mut current: String = doc.line(line).to_string();
|
||||||
|
let ltrim: String = " ".repeat(current.len() - current.trim_start().len());
|
||||||
|
let mut line_idx = line;
|
||||||
|
|
||||||
|
// iterate upwards from the definition, and grab the comments
|
||||||
|
while valid {
|
||||||
|
hover.push(current.clone());
|
||||||
|
line_idx -= 1;
|
||||||
|
valid = false;
|
||||||
|
if line_idx > 0 {
|
||||||
|
current = doc.line(line_idx).to_string();
|
||||||
|
let currentl = current.clone().trim_start().to_owned();
|
||||||
|
let currentr = current.clone().trim_end().to_owned();
|
||||||
|
if currentl.starts_with("/*") && currentr.ends_with("*/") {
|
||||||
|
valid = true;
|
||||||
|
} else if currentr.ends_with("*/") {
|
||||||
|
multiline = true;
|
||||||
|
valid = true;
|
||||||
|
} else if currentl.starts_with("/*") {
|
||||||
|
multiline = false;
|
||||||
|
valid = true;
|
||||||
|
} else {
|
||||||
|
valid = currentl.starts_with("//") || multiline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hover.reverse();
|
||||||
|
let mut result: Vec<String> = Vec::new();
|
||||||
|
for i in hover {
|
||||||
|
if let Some(stripped) = i.strip_prefix(<rim) {
|
||||||
|
result.push(stripped.to_owned());
|
||||||
|
} else {
|
||||||
|
result.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.join("").trim_end().to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::sources::{parse, LSPSupport};
|
||||||
|
use crate::support::test_init;
|
||||||
|
use ropey::Rope;
|
||||||
|
use std::fs::read_to_string;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_definition_token() {
|
||||||
|
test_init();
|
||||||
|
let line = Rope::from_str("assign ab_c[2:0] = 3'b000;");
|
||||||
|
let token = get_definition_token(line.line(0), Position::new(0, 10));
|
||||||
|
assert_eq!(token, "ab_c".to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_definition() {
|
||||||
|
test_init();
|
||||||
|
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
d.push("test_data/definition_test.sv");
|
||||||
|
let text = read_to_string(d).unwrap();
|
||||||
|
let doc = Rope::from_str(&text);
|
||||||
|
let url = Url::parse("file:///test_data/definition_test.sv").unwrap();
|
||||||
|
let syntax_tree = parse(&doc, &url, &None, &Vec::new()).unwrap();
|
||||||
|
trace!("{}", &syntax_tree);
|
||||||
|
let scope_tree = get_scopes(&syntax_tree, &url).unwrap();
|
||||||
|
trace!("{:#?}", &scope_tree);
|
||||||
|
for def in &scope_tree.defs {
|
||||||
|
trace!("{:?} {:?}", def, doc.byte_to_pos(def.byte_idx()));
|
||||||
|
}
|
||||||
|
let token = get_definition_token(doc.line(3), Position::new(3, 13));
|
||||||
|
for def in scope_tree.defs {
|
||||||
|
if token == def.ident() {
|
||||||
|
assert_eq!(doc.byte_to_pos(def.byte_idx()), Position::new(3, 9))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_hover() {
|
||||||
|
test_init();
|
||||||
|
let text = r#"
|
||||||
|
// module test
|
||||||
|
// test module
|
||||||
|
module test;
|
||||||
|
/* a */
|
||||||
|
logic a;
|
||||||
|
/**
|
||||||
|
* b
|
||||||
|
*/
|
||||||
|
logic b;
|
||||||
|
endmodule"#;
|
||||||
|
let doc = Rope::from_str(text);
|
||||||
|
eprintln!("{}", get_hover(&doc, 2));
|
||||||
|
assert_eq!(
|
||||||
|
get_hover(&doc, 3),
|
||||||
|
r#"// module test
|
||||||
|
// test module
|
||||||
|
module test;"#
|
||||||
|
.to_owned()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
get_hover(&doc, 9),
|
||||||
|
r#"/**
|
||||||
|
* b
|
||||||
|
*/
|
||||||
|
logic b;"#
|
||||||
|
.to_owned()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_symbols() {
|
||||||
|
test_init();
|
||||||
|
let text = r#"
|
||||||
|
module test;
|
||||||
|
logic a;
|
||||||
|
logic b;
|
||||||
|
endmodule"#;
|
||||||
|
let doc = Rope::from_str(&text);
|
||||||
|
let url = Url::parse("file:///test.sv").unwrap();
|
||||||
|
let syntax_tree = parse(&doc, &url, &None, &Vec::new()).unwrap();
|
||||||
|
let scope_tree = get_scopes(&syntax_tree, &url).unwrap();
|
||||||
|
let symbol = scope_tree.document_symbols(&url, &doc);
|
||||||
|
let symbol = symbol.get(0).unwrap();
|
||||||
|
assert_eq!(&symbol.name, "test");
|
||||||
|
let names: Vec<String> = symbol
|
||||||
|
.children
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.map(|x| x.name.clone())
|
||||||
|
.collect();
|
||||||
|
assert!(names.contains(&"a".to_string()));
|
||||||
|
assert!(names.contains(&"b".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_highlight() {
|
||||||
|
test_init();
|
||||||
|
let text = r#"
|
||||||
|
module test;
|
||||||
|
logic clk;
|
||||||
|
assign clk = 1'b1;
|
||||||
|
endmodule"#;
|
||||||
|
let doc = Rope::from_str(&text);
|
||||||
|
let url = Url::parse("file:///test.sv").unwrap();
|
||||||
|
let syntax_tree = parse(&doc, &url, &None, &Vec::new()).unwrap();
|
||||||
|
let scope_tree = get_scopes(&syntax_tree, &url).unwrap();
|
||||||
|
let references = all_identifiers(&syntax_tree, "clk");
|
||||||
|
let highlights = scope_tree.document_highlights(
|
||||||
|
&url,
|
||||||
|
&doc,
|
||||||
|
references,
|
||||||
|
doc.pos_to_byte(&Position::new(2, 8)),
|
||||||
|
);
|
||||||
|
let expected = vec![
|
||||||
|
DocumentHighlight {
|
||||||
|
range: Range {
|
||||||
|
start: Position {
|
||||||
|
line: 2,
|
||||||
|
character: 8,
|
||||||
|
},
|
||||||
|
end: Position {
|
||||||
|
line: 2,
|
||||||
|
character: 11,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
kind: None,
|
||||||
|
},
|
||||||
|
DocumentHighlight {
|
||||||
|
range: Range {
|
||||||
|
start: Position {
|
||||||
|
line: 3,
|
||||||
|
character: 9,
|
||||||
|
},
|
||||||
|
end: Position {
|
||||||
|
line: 3,
|
||||||
|
character: 12,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
kind: None,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
assert_eq!(highlights, expected)
|
||||||
|
}
|
||||||
|
}
|
965
src/definition/def_types.rs
Normal file
965
src/definition/def_types.rs
Normal file
@ -0,0 +1,965 @@
|
|||||||
|
use crate::sources::LSPSupport;
|
||||||
|
use log::trace;
|
||||||
|
use ropey::Rope;
|
||||||
|
use tower_lsp::lsp_types::*;
|
||||||
|
|
||||||
|
/// cleanup the text of a definition so it can be included in completions
|
||||||
|
pub fn clean_type_str(type_str: &str, ident: &str) -> String {
|
||||||
|
let endings: &[_] = &[';', ','];
|
||||||
|
// remove anything after an equals sign
|
||||||
|
let eq_offset = type_str.find('=').unwrap_or(type_str.len());
|
||||||
|
let mut result = type_str.to_string();
|
||||||
|
result.replace_range(eq_offset.., "");
|
||||||
|
result
|
||||||
|
.trim_start()
|
||||||
|
.trim_end()
|
||||||
|
.trim_end_matches(endings)
|
||||||
|
.trim_end_matches(ident)
|
||||||
|
.split_whitespace()
|
||||||
|
.collect::<Vec<&str>>()
|
||||||
|
.join(" ")
|
||||||
|
.replace("[ ", "[")
|
||||||
|
.replace(" ]", "]")
|
||||||
|
.replace(" : ", ":")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn copy_defs(defs: &[Box<dyn Definition>]) -> Vec<Box<dyn Definition>> {
|
||||||
|
let mut decs: Vec<Box<dyn Definition>> = Vec::new();
|
||||||
|
for def in defs {
|
||||||
|
decs.push(Box::new(GenericDec {
|
||||||
|
ident: def.ident(),
|
||||||
|
byte_idx: def.byte_idx(),
|
||||||
|
url: def.url(),
|
||||||
|
type_str: def.type_str(),
|
||||||
|
completion_kind: def.completion_kind(),
|
||||||
|
symbol_kind: def.symbol_kind(),
|
||||||
|
def_type: def.def_type(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
decs
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn copy_scopes(scopes: &[Box<dyn Scope>]) -> Vec<Box<dyn Scope>> {
|
||||||
|
let mut scope_decs: Vec<Box<dyn Scope>> = Vec::new();
|
||||||
|
for scope in scopes {
|
||||||
|
let mut scope_copy = GenericScope {
|
||||||
|
ident: scope.ident(),
|
||||||
|
byte_idx: scope.byte_idx(),
|
||||||
|
start: scope.start(),
|
||||||
|
end: scope.end(),
|
||||||
|
url: scope.url(),
|
||||||
|
type_str: scope.type_str(),
|
||||||
|
completion_kind: scope.completion_kind(),
|
||||||
|
symbol_kind: scope.symbol_kind(),
|
||||||
|
def_type: scope.def_type(),
|
||||||
|
defs: Vec::new(),
|
||||||
|
scopes: Vec::new(),
|
||||||
|
};
|
||||||
|
scope_copy.defs.extend(copy_defs(scope.defs()));
|
||||||
|
scope_copy.scopes.extend(copy_scopes(scope.scopes()));
|
||||||
|
scope_decs.push(Box::new(scope_copy))
|
||||||
|
}
|
||||||
|
scope_decs
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A definition of any SystemVerilog variable or construct
|
||||||
|
pub trait Definition: std::fmt::Debug + Sync + Send {
|
||||||
|
// identifier
|
||||||
|
fn ident(&self) -> String;
|
||||||
|
// byte index in file of definition
|
||||||
|
fn byte_idx(&self) -> usize;
|
||||||
|
// url pointing to the file the definition is in
|
||||||
|
fn url(&self) -> Url;
|
||||||
|
// cleaned up text of the definition
|
||||||
|
fn type_str(&self) -> String;
|
||||||
|
// the kind of this definition, for use in completions
|
||||||
|
fn completion_kind(&self) -> CompletionItemKind;
|
||||||
|
// the kind of this definition, for use in showing document symbols
|
||||||
|
// for some reason this kind is different than CompletionItemKind
|
||||||
|
fn symbol_kind(&self) -> SymbolKind;
|
||||||
|
// the kind of this definition, simplified for internal use
|
||||||
|
fn def_type(&self) -> DefinitionType;
|
||||||
|
// whether the definition identifier starts with the given token
|
||||||
|
fn starts_with(&self, token: &str) -> bool;
|
||||||
|
// constructs the completion for this definition
|
||||||
|
fn completion(&self) -> CompletionItem;
|
||||||
|
fn dot_completion(&self, scope_tree: &GenericScope) -> Vec<CompletionItem>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Scope: std::fmt::Debug + Definition + Sync + Send {
|
||||||
|
// the start byte of this scope
|
||||||
|
fn start(&self) -> usize;
|
||||||
|
// the end byte of this scope
|
||||||
|
fn end(&self) -> usize;
|
||||||
|
// all the within this scope
|
||||||
|
fn defs(&self) -> &Vec<Box<dyn Definition>>;
|
||||||
|
// all the scopes within this scope, ex. task inside a module
|
||||||
|
fn scopes(&self) -> &Vec<Box<dyn Scope>>;
|
||||||
|
// the definition of this scope
|
||||||
|
fn definition(&self) -> GenericDec {
|
||||||
|
GenericDec {
|
||||||
|
ident: self.ident(),
|
||||||
|
byte_idx: self.byte_idx(),
|
||||||
|
url: self.url(),
|
||||||
|
type_str: self.type_str(),
|
||||||
|
completion_kind: self.completion_kind(),
|
||||||
|
symbol_kind: self.symbol_kind(),
|
||||||
|
def_type: DefinitionType::GenericScope,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// return a completion from the scope tree, this function should be called on the global scope
|
||||||
|
fn get_completion(&self, token: &str, byte_idx: usize, url: &Url) -> Vec<CompletionItem> {
|
||||||
|
let mut completions: Vec<CompletionItem> = Vec::new();
|
||||||
|
// first we need to go down the scope tree, to the scope the user is invoking a completion
|
||||||
|
// in
|
||||||
|
for scope in self.scopes() {
|
||||||
|
if &scope.url() == url && scope.start() <= byte_idx && byte_idx <= scope.end() {
|
||||||
|
completions = scope.get_completion(token, byte_idx, url);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// now that we are in the users scope, we can attempt to find a relevant completion
|
||||||
|
// we proceed back upwards through the scope tree, adding any definitions that match
|
||||||
|
// the users token
|
||||||
|
let completion_idents: Vec<String> = completions.iter().map(|x| x.label.clone()).collect();
|
||||||
|
for def in self.defs() {
|
||||||
|
if !completion_idents.contains(&def.ident()) && def.starts_with(token) {
|
||||||
|
completions.push(def.completion());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for scope in self.scopes() {
|
||||||
|
if scope.starts_with(token) {
|
||||||
|
completions.push(scope.completion());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
completions
|
||||||
|
}
|
||||||
|
|
||||||
|
/// return a dot completion from the scope tree, this function should be called on the global
|
||||||
|
/// scope
|
||||||
|
fn get_dot_completion(
|
||||||
|
&self,
|
||||||
|
token: &str,
|
||||||
|
byte_idx: usize,
|
||||||
|
url: &Url,
|
||||||
|
scope_tree: &GenericScope,
|
||||||
|
) -> Vec<CompletionItem> {
|
||||||
|
trace!("dot entering: {}, token: {}", self.ident(), token);
|
||||||
|
trace!("{:?}", self.scopes());
|
||||||
|
// first we need to go down the scope tree, to the scope the user is invoking a completion
|
||||||
|
// in
|
||||||
|
for scope in self.scopes() {
|
||||||
|
trace!(
|
||||||
|
"{}, {}, {}, {}",
|
||||||
|
scope.ident(),
|
||||||
|
byte_idx,
|
||||||
|
scope.start(),
|
||||||
|
scope.end()
|
||||||
|
);
|
||||||
|
if &scope.url() == url && scope.start() <= byte_idx && byte_idx <= scope.end() {
|
||||||
|
eprintln!("checking dot completion: {}", scope.ident());
|
||||||
|
let result = scope.get_dot_completion(token, byte_idx, url, scope_tree);
|
||||||
|
if !result.is_empty() {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// now that we are in the users scope, we can attempt to find the relevant definition
|
||||||
|
// we proceed back upwards through the scope tree, and if a definition matches our token,
|
||||||
|
// we invoke dot completion on that definition and pass it the syntax tree
|
||||||
|
for def in self.defs() {
|
||||||
|
trace!("def: {:?}", def);
|
||||||
|
if def.starts_with(token) {
|
||||||
|
trace!("complete def: {:?}", def);
|
||||||
|
return def.dot_completion(scope_tree);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for scope in self.scopes() {
|
||||||
|
if scope.starts_with(token) {
|
||||||
|
trace!("found dot-completion scope: {}", scope.ident());
|
||||||
|
return scope.dot_completion(scope_tree);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// return a definition from the scope tree, this function should be called on the global
|
||||||
|
/// scope
|
||||||
|
fn get_definition(&self, token: &str, byte_idx: usize, url: &Url) -> Option<GenericDec> {
|
||||||
|
let mut definition: Option<GenericDec> = None;
|
||||||
|
for scope in self.scopes() {
|
||||||
|
if &scope.url() == url && scope.start() <= byte_idx && byte_idx <= scope.end() {
|
||||||
|
definition = scope.get_definition(token, byte_idx, url);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if definition.is_none() {
|
||||||
|
for def in self.defs() {
|
||||||
|
if def.ident() == token {
|
||||||
|
return Some(GenericDec {
|
||||||
|
ident: def.ident(),
|
||||||
|
byte_idx: def.byte_idx(),
|
||||||
|
url: def.url(),
|
||||||
|
type_str: def.type_str(),
|
||||||
|
completion_kind: def.completion_kind(),
|
||||||
|
symbol_kind: def.symbol_kind(),
|
||||||
|
def_type: DefinitionType::Net,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for scope in self.scopes() {
|
||||||
|
if scope.ident() == token {
|
||||||
|
return Some(scope.definition());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
definition
|
||||||
|
}
|
||||||
|
/// returns all symbols in a document
|
||||||
|
fn document_symbols(&self, uri: &Url, doc: &Rope) -> Vec<DocumentSymbol> {
|
||||||
|
let mut symbols: Vec<DocumentSymbol> = Vec::new();
|
||||||
|
for scope in self.scopes() {
|
||||||
|
if &scope.url() == uri {
|
||||||
|
#[allow(deprecated)]
|
||||||
|
symbols.push(DocumentSymbol {
|
||||||
|
name: scope.ident(),
|
||||||
|
detail: Some(scope.type_str()),
|
||||||
|
kind: scope.symbol_kind(),
|
||||||
|
deprecated: None,
|
||||||
|
range: Range::new(doc.byte_to_pos(scope.start()), doc.byte_to_pos(scope.end())),
|
||||||
|
selection_range: Range::new(
|
||||||
|
doc.byte_to_pos(scope.byte_idx()),
|
||||||
|
doc.byte_to_pos(scope.byte_idx() + scope.ident().len()),
|
||||||
|
),
|
||||||
|
children: Some(scope.document_symbols(uri, doc)),
|
||||||
|
tags: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for def in self.defs() {
|
||||||
|
#[allow(deprecated)]
|
||||||
|
symbols.push(DocumentSymbol {
|
||||||
|
name: def.ident(),
|
||||||
|
detail: Some(def.type_str()),
|
||||||
|
kind: def.symbol_kind(),
|
||||||
|
deprecated: None,
|
||||||
|
range: Range::new(
|
||||||
|
doc.byte_to_pos(def.byte_idx()),
|
||||||
|
doc.byte_to_pos(def.byte_idx() + def.ident().len()),
|
||||||
|
),
|
||||||
|
selection_range: Range::new(
|
||||||
|
doc.byte_to_pos(def.byte_idx()),
|
||||||
|
doc.byte_to_pos(def.byte_idx() + def.ident().len()),
|
||||||
|
),
|
||||||
|
children: None,
|
||||||
|
tags: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
symbols
|
||||||
|
}
|
||||||
|
|
||||||
|
/// highlight all references of a symbol
|
||||||
|
fn document_highlights(
|
||||||
|
&self,
|
||||||
|
uri: &Url,
|
||||||
|
doc: &Rope,
|
||||||
|
// all references in the doc's syntax tree
|
||||||
|
references: Vec<(String, usize)>,
|
||||||
|
// byte_idx of symbol definition
|
||||||
|
byte_idx: usize,
|
||||||
|
) -> Vec<DocumentHighlight> {
|
||||||
|
// to find references we need to grab references from locations downward from the
|
||||||
|
// definition
|
||||||
|
for scope in self.scopes() {
|
||||||
|
if &scope.url() == uri && scope.start() <= byte_idx && byte_idx <= scope.end() {
|
||||||
|
return scope.document_highlights(uri, doc, references, byte_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// we should now be in the scope of the definition, so we can grab all references
|
||||||
|
// in this scope. This also grabs references below this scope.
|
||||||
|
references
|
||||||
|
.iter()
|
||||||
|
.filter(|x| self.start() <= x.1 && x.1 <= self.end())
|
||||||
|
.map(|x| DocumentHighlight {
|
||||||
|
range: Range::new(doc.byte_to_pos(x.1), doc.byte_to_pos(x.1 + x.0.len())),
|
||||||
|
kind: None,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum DefinitionType {
|
||||||
|
Port,
|
||||||
|
Net,
|
||||||
|
Data,
|
||||||
|
Modport,
|
||||||
|
Subroutine,
|
||||||
|
ModuleInstantiation,
|
||||||
|
GenericScope,
|
||||||
|
Class,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct PortDec {
|
||||||
|
pub ident: String,
|
||||||
|
pub byte_idx: usize,
|
||||||
|
pub url: Url,
|
||||||
|
pub type_str: String,
|
||||||
|
pub completion_kind: CompletionItemKind,
|
||||||
|
pub symbol_kind: SymbolKind,
|
||||||
|
pub def_type: DefinitionType,
|
||||||
|
pub interface: Option<String>,
|
||||||
|
pub modport: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PortDec {
|
||||||
|
pub fn new(url: &Url) -> Self {
|
||||||
|
Self {
|
||||||
|
ident: String::new(),
|
||||||
|
byte_idx: 0,
|
||||||
|
type_str: String::new(),
|
||||||
|
completion_kind: CompletionItemKind::PROPERTY,
|
||||||
|
symbol_kind: SymbolKind::PROPERTY,
|
||||||
|
def_type: DefinitionType::Port,
|
||||||
|
interface: None,
|
||||||
|
modport: None,
|
||||||
|
url: url.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Definition for PortDec {
|
||||||
|
fn ident(&self) -> String {
|
||||||
|
self.ident.clone()
|
||||||
|
}
|
||||||
|
fn byte_idx(&self) -> usize {
|
||||||
|
self.byte_idx
|
||||||
|
}
|
||||||
|
fn url(&self) -> Url {
|
||||||
|
self.url.clone()
|
||||||
|
}
|
||||||
|
fn type_str(&self) -> String {
|
||||||
|
self.type_str.clone()
|
||||||
|
}
|
||||||
|
fn completion_kind(&self) -> CompletionItemKind {
|
||||||
|
self.completion_kind
|
||||||
|
}
|
||||||
|
fn symbol_kind(&self) -> SymbolKind {
|
||||||
|
self.symbol_kind
|
||||||
|
}
|
||||||
|
fn def_type(&self) -> DefinitionType {
|
||||||
|
self.def_type
|
||||||
|
}
|
||||||
|
fn starts_with(&self, token: &str) -> bool {
|
||||||
|
self.ident.starts_with(token)
|
||||||
|
}
|
||||||
|
fn completion(&self) -> CompletionItem {
|
||||||
|
CompletionItem {
|
||||||
|
label: self.ident.clone(),
|
||||||
|
detail: Some(clean_type_str(&self.type_str, &self.ident)),
|
||||||
|
kind: Some(self.completion_kind),
|
||||||
|
..CompletionItem::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn dot_completion(&self, scope_tree: &GenericScope) -> Vec<CompletionItem> {
|
||||||
|
for scope in &scope_tree.scopes {
|
||||||
|
if let Some(interface) = &self.interface {
|
||||||
|
if &scope.ident() == interface {
|
||||||
|
return match &self.modport {
|
||||||
|
Some(modport) => {
|
||||||
|
for def in scope.defs() {
|
||||||
|
if def.starts_with(modport) {
|
||||||
|
return def.dot_completion(scope_tree);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
None => scope
|
||||||
|
.defs()
|
||||||
|
.iter()
|
||||||
|
.filter(|x| !x.starts_with(&scope.ident()))
|
||||||
|
.map(|x| x.completion())
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct GenericDec {
|
||||||
|
pub ident: String,
|
||||||
|
pub byte_idx: usize,
|
||||||
|
pub url: Url,
|
||||||
|
pub type_str: String,
|
||||||
|
pub completion_kind: CompletionItemKind,
|
||||||
|
pub symbol_kind: SymbolKind,
|
||||||
|
pub def_type: DefinitionType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GenericDec {
|
||||||
|
pub fn new(url: &Url) -> Self {
|
||||||
|
Self {
|
||||||
|
ident: String::new(),
|
||||||
|
byte_idx: 0,
|
||||||
|
url: url.clone(),
|
||||||
|
type_str: String::new(),
|
||||||
|
completion_kind: CompletionItemKind::VARIABLE,
|
||||||
|
// FIXME: check if this replacement is correct
|
||||||
|
symbol_kind: SymbolKind::NULL,
|
||||||
|
def_type: DefinitionType::Net,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Definition for GenericDec {
|
||||||
|
fn ident(&self) -> String {
|
||||||
|
self.ident.clone()
|
||||||
|
}
|
||||||
|
fn byte_idx(&self) -> usize {
|
||||||
|
self.byte_idx
|
||||||
|
}
|
||||||
|
fn url(&self) -> Url {
|
||||||
|
self.url.clone()
|
||||||
|
}
|
||||||
|
fn type_str(&self) -> String {
|
||||||
|
self.type_str.clone()
|
||||||
|
}
|
||||||
|
fn completion_kind(&self) -> CompletionItemKind {
|
||||||
|
self.completion_kind
|
||||||
|
}
|
||||||
|
fn symbol_kind(&self) -> SymbolKind {
|
||||||
|
self.symbol_kind
|
||||||
|
}
|
||||||
|
fn def_type(&self) -> DefinitionType {
|
||||||
|
self.def_type
|
||||||
|
}
|
||||||
|
fn starts_with(&self, token: &str) -> bool {
|
||||||
|
self.ident.starts_with(token)
|
||||||
|
}
|
||||||
|
fn completion(&self) -> CompletionItem {
|
||||||
|
CompletionItem {
|
||||||
|
label: self.ident.clone(),
|
||||||
|
detail: Some(clean_type_str(&self.type_str, &self.ident)),
|
||||||
|
kind: Some(self.completion_kind),
|
||||||
|
..CompletionItem::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn dot_completion(&self, _: &GenericScope) -> Vec<CompletionItem> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct PackageImport {
|
||||||
|
pub ident: String,
|
||||||
|
pub byte_idx: usize,
|
||||||
|
pub url: Url,
|
||||||
|
pub type_str: String,
|
||||||
|
pub completion_kind: CompletionItemKind,
|
||||||
|
pub symbol_kind: SymbolKind,
|
||||||
|
pub def_type: DefinitionType,
|
||||||
|
pub asterisk: bool,
|
||||||
|
pub import_ident: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PackageImport {
|
||||||
|
pub fn new(url: &Url) -> Self {
|
||||||
|
Self {
|
||||||
|
ident: String::new(),
|
||||||
|
byte_idx: 0,
|
||||||
|
url: url.clone(),
|
||||||
|
type_str: String::new(),
|
||||||
|
completion_kind: CompletionItemKind::TEXT,
|
||||||
|
symbol_kind: SymbolKind::NAMESPACE,
|
||||||
|
def_type: DefinitionType::Data,
|
||||||
|
asterisk: false,
|
||||||
|
import_ident: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Definition for PackageImport {
|
||||||
|
fn ident(&self) -> String {
|
||||||
|
self.ident.clone()
|
||||||
|
}
|
||||||
|
fn byte_idx(&self) -> usize {
|
||||||
|
self.byte_idx
|
||||||
|
}
|
||||||
|
fn url(&self) -> Url {
|
||||||
|
self.url.clone()
|
||||||
|
}
|
||||||
|
fn type_str(&self) -> String {
|
||||||
|
self.type_str.clone()
|
||||||
|
}
|
||||||
|
fn completion_kind(&self) -> CompletionItemKind {
|
||||||
|
self.completion_kind
|
||||||
|
}
|
||||||
|
fn symbol_kind(&self) -> SymbolKind {
|
||||||
|
self.symbol_kind
|
||||||
|
}
|
||||||
|
fn def_type(&self) -> DefinitionType {
|
||||||
|
self.def_type
|
||||||
|
}
|
||||||
|
fn starts_with(&self, token: &str) -> bool {
|
||||||
|
self.ident.starts_with(token)
|
||||||
|
}
|
||||||
|
fn completion(&self) -> CompletionItem {
|
||||||
|
CompletionItem {
|
||||||
|
label: self.ident.clone(),
|
||||||
|
detail: Some(clean_type_str(&self.type_str, &self.ident.clone())),
|
||||||
|
kind: Some(self.completion_kind),
|
||||||
|
..CompletionItem::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn dot_completion(&self, _: &GenericScope) -> Vec<CompletionItem> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SubDec {
|
||||||
|
pub ident: String,
|
||||||
|
pub byte_idx: usize,
|
||||||
|
pub url: Url,
|
||||||
|
pub type_str: String,
|
||||||
|
pub completion_kind: CompletionItemKind,
|
||||||
|
pub symbol_kind: SymbolKind,
|
||||||
|
pub def_type: DefinitionType,
|
||||||
|
pub start: usize,
|
||||||
|
pub end: usize,
|
||||||
|
pub defs: Vec<Box<dyn Definition>>,
|
||||||
|
pub scopes: Vec<Box<dyn Scope>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SubDec {
|
||||||
|
pub fn new(url: &Url) -> Self {
|
||||||
|
Self {
|
||||||
|
ident: String::new(),
|
||||||
|
byte_idx: 0,
|
||||||
|
url: url.clone(),
|
||||||
|
type_str: String::new(),
|
||||||
|
completion_kind: CompletionItemKind::FUNCTION,
|
||||||
|
symbol_kind: SymbolKind::FUNCTION,
|
||||||
|
def_type: DefinitionType::Subroutine,
|
||||||
|
start: 0,
|
||||||
|
end: 0,
|
||||||
|
defs: Vec::new(),
|
||||||
|
scopes: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Definition for SubDec {
|
||||||
|
fn ident(&self) -> String {
|
||||||
|
self.ident.clone()
|
||||||
|
}
|
||||||
|
fn byte_idx(&self) -> usize {
|
||||||
|
self.byte_idx
|
||||||
|
}
|
||||||
|
fn url(&self) -> Url {
|
||||||
|
self.url.clone()
|
||||||
|
}
|
||||||
|
fn type_str(&self) -> String {
|
||||||
|
self.type_str.clone()
|
||||||
|
}
|
||||||
|
fn completion_kind(&self) -> CompletionItemKind {
|
||||||
|
self.completion_kind
|
||||||
|
}
|
||||||
|
fn symbol_kind(&self) -> SymbolKind {
|
||||||
|
self.symbol_kind
|
||||||
|
}
|
||||||
|
fn def_type(&self) -> DefinitionType {
|
||||||
|
self.def_type
|
||||||
|
}
|
||||||
|
fn starts_with(&self, token: &str) -> bool {
|
||||||
|
self.ident.starts_with(token)
|
||||||
|
}
|
||||||
|
fn completion(&self) -> CompletionItem {
|
||||||
|
CompletionItem {
|
||||||
|
label: self.ident.clone(),
|
||||||
|
detail: Some(clean_type_str(&self.type_str, &self.ident)),
|
||||||
|
kind: Some(self.completion_kind),
|
||||||
|
..CompletionItem::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn dot_completion(&self, _: &GenericScope) -> Vec<CompletionItem> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Scope for SubDec {
|
||||||
|
fn start(&self) -> usize {
|
||||||
|
self.start
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end(&self) -> usize {
|
||||||
|
self.end
|
||||||
|
}
|
||||||
|
fn defs(&self) -> &Vec<Box<dyn Definition>> {
|
||||||
|
&self.defs
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scopes(&self) -> &Vec<Box<dyn Scope>> {
|
||||||
|
&self.scopes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ModportDec {
|
||||||
|
pub ident: String,
|
||||||
|
pub byte_idx: usize,
|
||||||
|
pub url: Url,
|
||||||
|
pub type_str: String,
|
||||||
|
pub completion_kind: CompletionItemKind,
|
||||||
|
pub symbol_kind: SymbolKind,
|
||||||
|
pub def_type: DefinitionType,
|
||||||
|
pub ports: Vec<Box<dyn Definition>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModportDec {
|
||||||
|
pub fn new(url: &Url) -> Self {
|
||||||
|
Self {
|
||||||
|
ident: String::new(),
|
||||||
|
byte_idx: 0,
|
||||||
|
url: url.clone(),
|
||||||
|
type_str: String::new(),
|
||||||
|
completion_kind: CompletionItemKind::INTERFACE,
|
||||||
|
symbol_kind: SymbolKind::INTERFACE,
|
||||||
|
def_type: DefinitionType::Modport,
|
||||||
|
ports: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Definition for ModportDec {
|
||||||
|
fn ident(&self) -> String {
|
||||||
|
self.ident.clone()
|
||||||
|
}
|
||||||
|
fn byte_idx(&self) -> usize {
|
||||||
|
self.byte_idx
|
||||||
|
}
|
||||||
|
fn url(&self) -> Url {
|
||||||
|
self.url.clone()
|
||||||
|
}
|
||||||
|
fn type_str(&self) -> String {
|
||||||
|
self.type_str.clone()
|
||||||
|
}
|
||||||
|
fn completion_kind(&self) -> CompletionItemKind {
|
||||||
|
self.completion_kind
|
||||||
|
}
|
||||||
|
fn symbol_kind(&self) -> SymbolKind {
|
||||||
|
self.symbol_kind
|
||||||
|
}
|
||||||
|
fn def_type(&self) -> DefinitionType {
|
||||||
|
self.def_type
|
||||||
|
}
|
||||||
|
fn starts_with(&self, token: &str) -> bool {
|
||||||
|
self.ident.starts_with(token)
|
||||||
|
}
|
||||||
|
fn completion(&self) -> CompletionItem {
|
||||||
|
CompletionItem {
|
||||||
|
label: self.ident.clone(),
|
||||||
|
detail: Some(clean_type_str(&self.type_str, &self.ident)),
|
||||||
|
kind: Some(self.completion_kind),
|
||||||
|
..CompletionItem::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn dot_completion(&self, _: &GenericScope) -> Vec<CompletionItem> {
|
||||||
|
self.ports.iter().map(|x| x.completion()).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ModInst {
|
||||||
|
pub ident: String,
|
||||||
|
pub byte_idx: usize,
|
||||||
|
pub url: Url,
|
||||||
|
pub type_str: String,
|
||||||
|
pub completion_kind: CompletionItemKind,
|
||||||
|
pub symbol_kind: SymbolKind,
|
||||||
|
pub def_type: DefinitionType,
|
||||||
|
pub mod_ident: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModInst {
|
||||||
|
pub fn new(url: &Url) -> Self {
|
||||||
|
Self {
|
||||||
|
ident: String::new(),
|
||||||
|
byte_idx: 0,
|
||||||
|
url: url.clone(),
|
||||||
|
type_str: String::new(),
|
||||||
|
completion_kind: CompletionItemKind::MODULE,
|
||||||
|
symbol_kind: SymbolKind::MODULE,
|
||||||
|
def_type: DefinitionType::ModuleInstantiation,
|
||||||
|
mod_ident: String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Definition for ModInst {
|
||||||
|
fn ident(&self) -> String {
|
||||||
|
self.ident.clone()
|
||||||
|
}
|
||||||
|
fn byte_idx(&self) -> usize {
|
||||||
|
self.byte_idx
|
||||||
|
}
|
||||||
|
fn url(&self) -> Url {
|
||||||
|
self.url.clone()
|
||||||
|
}
|
||||||
|
fn type_str(&self) -> String {
|
||||||
|
self.type_str.clone()
|
||||||
|
}
|
||||||
|
fn completion_kind(&self) -> CompletionItemKind {
|
||||||
|
self.completion_kind
|
||||||
|
}
|
||||||
|
fn symbol_kind(&self) -> SymbolKind {
|
||||||
|
self.symbol_kind
|
||||||
|
}
|
||||||
|
fn def_type(&self) -> DefinitionType {
|
||||||
|
self.def_type
|
||||||
|
}
|
||||||
|
fn starts_with(&self, token: &str) -> bool {
|
||||||
|
self.ident.starts_with(token)
|
||||||
|
}
|
||||||
|
fn completion(&self) -> CompletionItem {
|
||||||
|
CompletionItem {
|
||||||
|
label: self.ident.clone(),
|
||||||
|
detail: Some(clean_type_str(&self.type_str, &self.ident)),
|
||||||
|
kind: Some(self.completion_kind),
|
||||||
|
..CompletionItem::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn dot_completion(&self, scope_tree: &GenericScope) -> Vec<CompletionItem> {
|
||||||
|
for scope in &scope_tree.scopes {
|
||||||
|
if scope.ident() == self.mod_ident {
|
||||||
|
return scope
|
||||||
|
.defs()
|
||||||
|
.iter()
|
||||||
|
.filter(|x| !x.starts_with(&scope.ident()))
|
||||||
|
.map(|x| x.completion())
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct GenericScope {
|
||||||
|
pub ident: String,
|
||||||
|
pub byte_idx: usize,
|
||||||
|
pub start: usize,
|
||||||
|
pub end: usize,
|
||||||
|
pub url: Url,
|
||||||
|
pub type_str: String,
|
||||||
|
pub completion_kind: CompletionItemKind,
|
||||||
|
pub symbol_kind: SymbolKind,
|
||||||
|
pub def_type: DefinitionType,
|
||||||
|
pub defs: Vec<Box<dyn Definition>>,
|
||||||
|
pub scopes: Vec<Box<dyn Scope>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GenericScope {
|
||||||
|
pub fn new(url: &Url) -> Self {
|
||||||
|
Self {
|
||||||
|
ident: String::new(),
|
||||||
|
byte_idx: 0,
|
||||||
|
start: 0,
|
||||||
|
end: 0,
|
||||||
|
url: url.clone(),
|
||||||
|
type_str: String::new(),
|
||||||
|
completion_kind: CompletionItemKind::MODULE,
|
||||||
|
symbol_kind: SymbolKind::MODULE,
|
||||||
|
def_type: DefinitionType::GenericScope,
|
||||||
|
defs: Vec::new(),
|
||||||
|
scopes: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn contains_scope(&self, scope_ident: &str) -> bool {
|
||||||
|
for scope in &self.scopes {
|
||||||
|
if scope.starts_with(scope_ident) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Definition for GenericScope {
|
||||||
|
fn ident(&self) -> String {
|
||||||
|
self.ident.clone()
|
||||||
|
}
|
||||||
|
fn byte_idx(&self) -> usize {
|
||||||
|
self.byte_idx
|
||||||
|
}
|
||||||
|
fn url(&self) -> Url {
|
||||||
|
self.url.clone()
|
||||||
|
}
|
||||||
|
fn type_str(&self) -> String {
|
||||||
|
self.type_str.clone()
|
||||||
|
}
|
||||||
|
fn completion_kind(&self) -> CompletionItemKind {
|
||||||
|
self.completion_kind
|
||||||
|
}
|
||||||
|
fn symbol_kind(&self) -> SymbolKind {
|
||||||
|
self.symbol_kind
|
||||||
|
}
|
||||||
|
fn def_type(&self) -> DefinitionType {
|
||||||
|
self.def_type
|
||||||
|
}
|
||||||
|
fn starts_with(&self, token: &str) -> bool {
|
||||||
|
self.ident.starts_with(token)
|
||||||
|
}
|
||||||
|
fn completion(&self) -> CompletionItem {
|
||||||
|
CompletionItem {
|
||||||
|
label: self.ident.clone(),
|
||||||
|
detail: Some(clean_type_str(&self.type_str, &self.ident)),
|
||||||
|
kind: Some(self.completion_kind),
|
||||||
|
..CompletionItem::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn dot_completion(&self, scope_tree: &GenericScope) -> Vec<CompletionItem> {
|
||||||
|
for scope in scope_tree.scopes() {
|
||||||
|
if scope.ident() == self.ident {
|
||||||
|
return scope
|
||||||
|
.defs()
|
||||||
|
.iter()
|
||||||
|
.filter(|x| !x.starts_with(&scope.ident()))
|
||||||
|
.map(|x| x.completion())
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Scope for GenericScope {
|
||||||
|
fn start(&self) -> usize {
|
||||||
|
self.start
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end(&self) -> usize {
|
||||||
|
self.end
|
||||||
|
}
|
||||||
|
|
||||||
|
fn defs(&self) -> &Vec<Box<dyn Definition>> {
|
||||||
|
&self.defs
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scopes(&self) -> &Vec<Box<dyn Scope>> {
|
||||||
|
&self.scopes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ClassDec {
|
||||||
|
pub ident: String,
|
||||||
|
pub byte_idx: usize,
|
||||||
|
pub start: usize,
|
||||||
|
pub end: usize,
|
||||||
|
pub url: Url,
|
||||||
|
pub type_str: String,
|
||||||
|
pub completion_kind: CompletionItemKind,
|
||||||
|
pub symbol_kind: SymbolKind,
|
||||||
|
pub def_type: DefinitionType,
|
||||||
|
pub defs: Vec<Box<dyn Definition>>,
|
||||||
|
pub scopes: Vec<Box<dyn Scope>>,
|
||||||
|
// class, package
|
||||||
|
pub extends: (Vec<String>, Option<String>),
|
||||||
|
// class, package
|
||||||
|
pub implements: Vec<(String, Option<String>)>,
|
||||||
|
pub interface: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClassDec {
|
||||||
|
pub fn new(url: &Url) -> Self {
|
||||||
|
Self {
|
||||||
|
ident: String::new(),
|
||||||
|
byte_idx: 0,
|
||||||
|
start: 0,
|
||||||
|
end: 0,
|
||||||
|
url: url.clone(),
|
||||||
|
type_str: String::new(),
|
||||||
|
completion_kind: CompletionItemKind::CLASS,
|
||||||
|
symbol_kind: SymbolKind::CLASS,
|
||||||
|
def_type: DefinitionType::Class,
|
||||||
|
defs: Vec::new(),
|
||||||
|
scopes: Vec::new(),
|
||||||
|
extends: (Vec::new(), None),
|
||||||
|
implements: Vec::new(),
|
||||||
|
interface: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Definition for ClassDec {
|
||||||
|
fn ident(&self) -> String {
|
||||||
|
self.ident.clone()
|
||||||
|
}
|
||||||
|
fn byte_idx(&self) -> usize {
|
||||||
|
self.byte_idx
|
||||||
|
}
|
||||||
|
fn url(&self) -> Url {
|
||||||
|
self.url.clone()
|
||||||
|
}
|
||||||
|
fn type_str(&self) -> String {
|
||||||
|
self.type_str.clone()
|
||||||
|
}
|
||||||
|
fn completion_kind(&self) -> CompletionItemKind {
|
||||||
|
self.completion_kind
|
||||||
|
}
|
||||||
|
fn symbol_kind(&self) -> SymbolKind {
|
||||||
|
self.symbol_kind
|
||||||
|
}
|
||||||
|
fn def_type(&self) -> DefinitionType {
|
||||||
|
self.def_type
|
||||||
|
}
|
||||||
|
fn starts_with(&self, token: &str) -> bool {
|
||||||
|
self.ident.starts_with(token)
|
||||||
|
}
|
||||||
|
fn completion(&self) -> CompletionItem {
|
||||||
|
CompletionItem {
|
||||||
|
label: self.ident.clone(),
|
||||||
|
detail: Some(clean_type_str(&self.type_str, &self.ident)),
|
||||||
|
kind: Some(self.completion_kind),
|
||||||
|
..CompletionItem::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn dot_completion(&self, scope_tree: &GenericScope) -> Vec<CompletionItem> {
|
||||||
|
for scope in scope_tree.scopes() {
|
||||||
|
if scope.ident() == self.ident {
|
||||||
|
return scope
|
||||||
|
.defs()
|
||||||
|
.iter()
|
||||||
|
.filter(|x| !x.starts_with(&scope.ident()))
|
||||||
|
.map(|x| x.completion())
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Scope for ClassDec {
|
||||||
|
fn start(&self) -> usize {
|
||||||
|
self.start
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end(&self) -> usize {
|
||||||
|
self.end
|
||||||
|
}
|
||||||
|
|
||||||
|
fn defs(&self) -> &Vec<Box<dyn Definition>> {
|
||||||
|
&self.defs
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scopes(&self) -> &Vec<Box<dyn Scope>> {
|
||||||
|
&self.scopes
|
||||||
|
}
|
||||||
|
}
|
2314
src/definition/extract_defs.rs
Normal file
2314
src/definition/extract_defs.rs
Normal file
File diff suppressed because it is too large
Load Diff
496
src/diagnostics.rs
Normal file
496
src/diagnostics.rs
Normal file
@ -0,0 +1,496 @@
|
|||||||
|
use crate::server::ProjectConfig;
|
||||||
|
#[cfg(feature = "slang")]
|
||||||
|
use path_clean::PathClean;
|
||||||
|
use regex::Regex;
|
||||||
|
use ropey::Rope;
|
||||||
|
#[cfg(feature = "slang")]
|
||||||
|
use std::env::current_dir;
|
||||||
|
#[cfg(feature = "slang")]
|
||||||
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
use tower_lsp::lsp_types::*;
|
||||||
|
#[cfg(feature = "slang")]
|
||||||
|
use veridian_slang::slang_compile;
|
||||||
|
use walkdir::DirEntry;
|
||||||
|
#[cfg(feature = "slang")]
|
||||||
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
|
#[cfg(feature = "slang")]
|
||||||
|
pub fn get_diagnostics(
|
||||||
|
uri: Url,
|
||||||
|
rope: &Rope,
|
||||||
|
files: Vec<Url>,
|
||||||
|
conf: &ProjectConfig,
|
||||||
|
) -> PublishDiagnosticsParams {
|
||||||
|
if !(cfg!(test) && (uri.to_string().starts_with("file:///test"))) {
|
||||||
|
let paths = get_paths(files, conf.auto_search_workdir);
|
||||||
|
let mut diagnostics = {
|
||||||
|
if conf.verilator.syntax.enabled {
|
||||||
|
if let Ok(path) = uri.to_file_path() {
|
||||||
|
match verilator_syntax(
|
||||||
|
rope,
|
||||||
|
path,
|
||||||
|
&conf.verilator.syntax.path,
|
||||||
|
&conf.verilator.syntax.args,
|
||||||
|
) {
|
||||||
|
Some(diags) => diags,
|
||||||
|
None => Vec::new(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
} else if conf.verible.syntax.enabled {
|
||||||
|
match verible_syntax(rope, &conf.verible.syntax.path, &conf.verible.syntax.args) {
|
||||||
|
Some(diags) => diags,
|
||||||
|
None => Vec::new(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
diagnostics.append(&mut parse_report(
|
||||||
|
uri.clone(),
|
||||||
|
slang_compile(paths).unwrap(),
|
||||||
|
));
|
||||||
|
PublishDiagnosticsParams {
|
||||||
|
uri,
|
||||||
|
diagnostics,
|
||||||
|
version: None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
PublishDiagnosticsParams {
|
||||||
|
uri,
|
||||||
|
diagnostics: Vec::new(),
|
||||||
|
version: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "slang"))]
|
||||||
|
pub fn get_diagnostics(
|
||||||
|
uri: Url,
|
||||||
|
rope: &Rope,
|
||||||
|
#[allow(unused_variables)] files: Vec<Url>,
|
||||||
|
conf: &ProjectConfig,
|
||||||
|
) -> PublishDiagnosticsParams {
|
||||||
|
if !(cfg!(test) && (uri.to_string().starts_with("file:///test"))) {
|
||||||
|
let diagnostics = {
|
||||||
|
if conf.verilator.syntax.enabled {
|
||||||
|
if let Ok(path) = uri.to_file_path() {
|
||||||
|
match verilator_syntax(
|
||||||
|
rope,
|
||||||
|
path,
|
||||||
|
&conf.verilator.syntax.path,
|
||||||
|
&conf.verilator.syntax.args,
|
||||||
|
) {
|
||||||
|
Some(diags) => diags,
|
||||||
|
None => Vec::new(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
} else if conf.verible.syntax.enabled {
|
||||||
|
match verible_syntax(rope, &conf.verible.syntax.path, &conf.verible.syntax.args) {
|
||||||
|
Some(diags) => diags,
|
||||||
|
None => Vec::new(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
PublishDiagnosticsParams {
|
||||||
|
uri,
|
||||||
|
diagnostics,
|
||||||
|
version: None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
PublishDiagnosticsParams {
|
||||||
|
uri,
|
||||||
|
diagnostics: Vec::new(),
|
||||||
|
version: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// recursively find source file paths from working directory
|
||||||
|
/// and open files
|
||||||
|
#[cfg(feature = "slang")]
|
||||||
|
fn get_paths(files: Vec<Url>, search_workdir: bool) -> Vec<PathBuf> {
|
||||||
|
// check recursively from working dir for source files
|
||||||
|
let mut paths: Vec<PathBuf> = Vec::new();
|
||||||
|
if search_workdir {
|
||||||
|
let walker = WalkDir::new(".").into_iter();
|
||||||
|
for entry in walker.filter_entry(|e| !is_hidden(e)) {
|
||||||
|
let entry = entry.unwrap();
|
||||||
|
if entry.file_type().is_file() {
|
||||||
|
let extension = entry.path().extension().unwrap();
|
||||||
|
|
||||||
|
if extension == "sv" || extension == "svh" || extension == "v" || extension == "vh"
|
||||||
|
{
|
||||||
|
paths.push(entry.path().to_path_buf());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check recursively from opened files for source files
|
||||||
|
for file in files {
|
||||||
|
if let Ok(path) = file.to_file_path() {
|
||||||
|
if !paths.contains(&path) {
|
||||||
|
let walker = WalkDir::new(path.parent().unwrap()).into_iter();
|
||||||
|
for entry in walker.filter_entry(|e| !is_hidden(e)).flatten() {
|
||||||
|
if entry.file_type().is_file() && entry.path().extension().is_some() {
|
||||||
|
let extension = entry.path().extension().unwrap();
|
||||||
|
|
||||||
|
if extension == "sv"
|
||||||
|
|| extension == "svh"
|
||||||
|
|| extension == "v"
|
||||||
|
|| extension == "vh"
|
||||||
|
{
|
||||||
|
let entry_path = entry.path().to_path_buf();
|
||||||
|
if !paths.contains(&entry_path) {
|
||||||
|
paths.push(entry_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
paths
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_hidden(entry: &DirEntry) -> bool {
|
||||||
|
entry
|
||||||
|
.file_name()
|
||||||
|
.to_str()
|
||||||
|
.map(|s| s.starts_with('.'))
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "slang")]
|
||||||
|
/// parse a report from slang
|
||||||
|
fn parse_report(uri: Url, report: String) -> Vec<Diagnostic> {
|
||||||
|
let mut diagnostics: Vec<Diagnostic> = Vec::new();
|
||||||
|
for line in report.lines() {
|
||||||
|
let diag: Vec<&str> = line.splitn(5, ':').collect();
|
||||||
|
if absolute_path(diag.first().unwrap()) == uri.to_file_path().unwrap().as_os_str() {
|
||||||
|
let pos = Position::new(
|
||||||
|
diag.get(1).unwrap().parse::<u32>().unwrap() - 1,
|
||||||
|
diag.get(2).unwrap().parse::<u32>().unwrap() - 1,
|
||||||
|
);
|
||||||
|
diagnostics.push(Diagnostic::new(
|
||||||
|
Range::new(pos, pos),
|
||||||
|
slang_severity(diag.get(3).unwrap()),
|
||||||
|
None,
|
||||||
|
Some("slang".to_owned()),
|
||||||
|
(*diag.get(4).unwrap()).to_owned(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "slang")]
|
||||||
|
fn slang_severity(severity: &str) -> Option<DiagnosticSeverity> {
|
||||||
|
match severity {
|
||||||
|
" error" => Some(DiagnosticSeverity::ERROR),
|
||||||
|
" warning" => Some(DiagnosticSeverity::WARNING),
|
||||||
|
" note" => Some(DiagnosticSeverity::INFORMATION),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "slang")]
|
||||||
|
// convert relative path to absolute
|
||||||
|
fn absolute_path(path_str: &str) -> PathBuf {
|
||||||
|
let path = Path::new(path_str);
|
||||||
|
current_dir().unwrap().join(path).clean()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// convert captured severity string to DiagnosticSeverity
|
||||||
|
fn verilator_severity(severity: &str) -> Option<DiagnosticSeverity> {
|
||||||
|
match severity {
|
||||||
|
"Error" => Some(DiagnosticSeverity::ERROR),
|
||||||
|
s if s.starts_with("Warning") => Some(DiagnosticSeverity::WARNING),
|
||||||
|
// NOTE: afaik, verilator doesn't have an info or hint severity
|
||||||
|
_ => Some(DiagnosticSeverity::INFORMATION),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// syntax checking using verilator --lint-only
|
||||||
|
fn verilator_syntax(
|
||||||
|
rope: &Rope,
|
||||||
|
file_path: PathBuf,
|
||||||
|
verilator_syntax_path: &str,
|
||||||
|
verilator_syntax_args: &[String],
|
||||||
|
) -> Option<Vec<Diagnostic>> {
|
||||||
|
let mut child = Command::new(verilator_syntax_path)
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.args(verilator_syntax_args)
|
||||||
|
.arg(file_path.to_str()?)
|
||||||
|
.spawn()
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
static RE: std::sync::OnceLock<Regex> = std::sync::OnceLock::new();
|
||||||
|
let re = RE.get_or_init(|| {
|
||||||
|
Regex::new(
|
||||||
|
r"%(?P<severity>Error|Warning)(-(?P<warning_type>[A-Z0-9_]+))?: (?P<filepath>[^:]+):(?P<line>\d+):((?P<col>\d+):)? ?(?P<message>.*)",
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
// write file to stdin, read output from stdout
|
||||||
|
rope.write_to(child.stdin.as_mut()?).ok()?;
|
||||||
|
let output = child.wait_with_output().ok()?;
|
||||||
|
if !output.status.success() {
|
||||||
|
let mut diags: Vec<Diagnostic> = Vec::new();
|
||||||
|
let raw_output = String::from_utf8(output.stderr).ok()?;
|
||||||
|
let filtered_output = raw_output
|
||||||
|
.lines()
|
||||||
|
.filter(|line| line.starts_with('%'))
|
||||||
|
.collect::<Vec<&str>>();
|
||||||
|
for error in filtered_output {
|
||||||
|
let caps = match re.captures(error) {
|
||||||
|
Some(caps) => caps,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
// check if diagnostic is for this file, since verilator can provide diagnostics for
|
||||||
|
// included files
|
||||||
|
if caps.name("filepath")?.as_str() != file_path.to_str().unwrap_or("") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let severity = verilator_severity(caps.name("severity")?.as_str());
|
||||||
|
let line: u32 = caps.name("line")?.as_str().to_string().parse().ok()?;
|
||||||
|
let col: u32 = caps.name("col").map_or("1", |m| m.as_str()).parse().ok()?;
|
||||||
|
let pos = Position::new(line - 1, col - 1);
|
||||||
|
let msg = match severity {
|
||||||
|
Some(DiagnosticSeverity::ERROR) => caps.name("message")?.as_str().to_string(),
|
||||||
|
Some(DiagnosticSeverity::WARNING) => format!(
|
||||||
|
"{}: {}",
|
||||||
|
caps.name("warning_type")?.as_str(),
|
||||||
|
caps.name("message")?.as_str()
|
||||||
|
),
|
||||||
|
_ => "".to_string(),
|
||||||
|
};
|
||||||
|
diags.push(Diagnostic::new(
|
||||||
|
Range::new(pos, pos),
|
||||||
|
severity,
|
||||||
|
None,
|
||||||
|
Some("verilator".to_string()),
|
||||||
|
msg,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Some(diags)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// syntax checking using verible-verilog-syntax
|
||||||
|
fn verible_syntax(
|
||||||
|
rope: &Rope,
|
||||||
|
verible_syntax_path: &str,
|
||||||
|
verible_syntax_args: &[String],
|
||||||
|
) -> Option<Vec<Diagnostic>> {
|
||||||
|
let mut child = Command::new(verible_syntax_path)
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.args(verible_syntax_args)
|
||||||
|
.arg("-")
|
||||||
|
.spawn()
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
static RE: std::sync::OnceLock<Regex> = std::sync::OnceLock::new();
|
||||||
|
let re = RE.get_or_init(|| {
|
||||||
|
Regex::new(
|
||||||
|
r"^.+:(?P<line>\d*):(?P<startcol>\d*)(?:-(?P<endcol>\d*))?:\s(?P<message>.*)\s.*$",
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
// write file to stdin, read output from stdout
|
||||||
|
rope.write_to(child.stdin.as_mut()?).ok()?;
|
||||||
|
let output = child.wait_with_output().ok()?;
|
||||||
|
if !output.status.success() {
|
||||||
|
let mut diags: Vec<Diagnostic> = Vec::new();
|
||||||
|
let raw_output = String::from_utf8(output.stdout).ok()?;
|
||||||
|
for error in raw_output.lines() {
|
||||||
|
let caps = re.captures(error)?;
|
||||||
|
let line: u32 = caps.name("line")?.as_str().parse().ok()?;
|
||||||
|
let startcol: u32 = caps.name("startcol")?.as_str().parse().ok()?;
|
||||||
|
let endcol: Option<u32> = match caps.name("endcol").map(|e| e.as_str().parse()) {
|
||||||
|
Some(Ok(e)) => Some(e),
|
||||||
|
None => None,
|
||||||
|
Some(Err(_)) => return None,
|
||||||
|
};
|
||||||
|
let start_pos = Position::new(line - 1, startcol - 1);
|
||||||
|
let end_pos = Position::new(line - 1, endcol.unwrap_or(startcol) - 1);
|
||||||
|
diags.push(Diagnostic::new(
|
||||||
|
Range::new(start_pos, end_pos),
|
||||||
|
Some(DiagnosticSeverity::ERROR),
|
||||||
|
None,
|
||||||
|
Some("verible".to_string()),
|
||||||
|
caps.name("message")?.as_str().to_string(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Some(diags)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::support::test_init;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
use tempdir::TempDir;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(feature = "slang")]
|
||||||
|
fn test_diagnostics() {
|
||||||
|
test_init();
|
||||||
|
let uri = Url::from_file_path(absolute_path("test_data/diag/diag_test.sv")).unwrap();
|
||||||
|
let expected = PublishDiagnosticsParams::new(
|
||||||
|
uri.clone(),
|
||||||
|
vec![Diagnostic::new(
|
||||||
|
Range::new(Position::new(3, 13), Position::new(3, 13)),
|
||||||
|
Some(DiagnosticSeverity::WARNING),
|
||||||
|
None,
|
||||||
|
Some("slang".to_owned()),
|
||||||
|
" cannot refer to element 2 of \'logic[1:0]\' [-Windex-oob]".to_owned(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)],
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
let diag = get_diagnostics(
|
||||||
|
uri.clone(),
|
||||||
|
&Rope::default(),
|
||||||
|
vec![uri],
|
||||||
|
&ProjectConfig::default(),
|
||||||
|
);
|
||||||
|
assert_eq!(diag.uri, expected.uri);
|
||||||
|
assert_eq!(diag.version, expected.version);
|
||||||
|
assert_eq!(diag.diagnostics.last(), expected.diagnostics.last());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unsaved_file() {
|
||||||
|
test_init();
|
||||||
|
let uri = Url::parse("file://test.sv").unwrap();
|
||||||
|
get_diagnostics(
|
||||||
|
uri.clone(),
|
||||||
|
&Rope::default(),
|
||||||
|
vec![uri],
|
||||||
|
&ProjectConfig::default(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_verible_syntax() {
|
||||||
|
let text = r#"module test;
|
||||||
|
logic abc;
|
||||||
|
logic abcd;
|
||||||
|
|
||||||
|
a
|
||||||
|
endmodule
|
||||||
|
"#;
|
||||||
|
let doc = Rope::from_str(text);
|
||||||
|
let errors = verible_syntax(&doc, "verible-verilog-syntax", &[])
|
||||||
|
.expect("verible-verilog-syntax not found, test can not run");
|
||||||
|
let expected: Vec<Diagnostic> = vec![Diagnostic {
|
||||||
|
range: Range {
|
||||||
|
start: Position {
|
||||||
|
line: 5,
|
||||||
|
character: 0,
|
||||||
|
},
|
||||||
|
end: Position {
|
||||||
|
line: 5,
|
||||||
|
character: 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
severity: Some(DiagnosticSeverity::ERROR),
|
||||||
|
code: None,
|
||||||
|
source: Some("verible".to_string()),
|
||||||
|
message: "syntax error at token".to_string(),
|
||||||
|
related_information: None,
|
||||||
|
tags: None,
|
||||||
|
code_description: None,
|
||||||
|
data: None,
|
||||||
|
}];
|
||||||
|
assert_eq!(errors, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_verilator_syntax() {
|
||||||
|
let text = r#"module test;
|
||||||
|
logic abc;
|
||||||
|
logic abcd;
|
||||||
|
|
||||||
|
a
|
||||||
|
endmodule
|
||||||
|
"#;
|
||||||
|
let doc = Rope::from_str(text);
|
||||||
|
|
||||||
|
// verilator can't read from stdin so we must create a temp dir to place our
|
||||||
|
// test file
|
||||||
|
let dir = TempDir::new("verilator_test").unwrap();
|
||||||
|
let file_path_1 = dir.path().join("test.sv");
|
||||||
|
let mut f = File::create(&file_path_1).unwrap();
|
||||||
|
f.write_all(text.as_bytes()).unwrap();
|
||||||
|
f.sync_all().unwrap();
|
||||||
|
|
||||||
|
let errors = verilator_syntax(
|
||||||
|
&doc,
|
||||||
|
file_path_1,
|
||||||
|
"verilator",
|
||||||
|
&[
|
||||||
|
"--lint-only".to_string(),
|
||||||
|
"--sv".to_string(),
|
||||||
|
"-Wall".to_string(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.expect("verilator not found, test can not run");
|
||||||
|
|
||||||
|
drop(f);
|
||||||
|
dir.close().unwrap();
|
||||||
|
|
||||||
|
let expected: Vec<Diagnostic> = vec![Diagnostic {
|
||||||
|
range: Range {
|
||||||
|
start: Position {
|
||||||
|
line: 5,
|
||||||
|
character: 0,
|
||||||
|
},
|
||||||
|
end: Position {
|
||||||
|
line: 5,
|
||||||
|
character: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
severity: Some(DiagnosticSeverity::ERROR),
|
||||||
|
code: None,
|
||||||
|
source: Some("verilator".to_string()),
|
||||||
|
message: "syntax error, unexpected endmodule, expecting IDENTIFIER or randomize"
|
||||||
|
.to_string(),
|
||||||
|
related_information: None,
|
||||||
|
tags: None,
|
||||||
|
code_description: None,
|
||||||
|
data: None,
|
||||||
|
}];
|
||||||
|
assert_eq!(errors[0].severity, expected[0].severity);
|
||||||
|
assert_eq!(errors[0].range.start.line, expected[0].range.start.line);
|
||||||
|
assert_eq!(errors[0].range.end.line, expected[0].range.end.line);
|
||||||
|
assert!(errors[0].message.contains("syntax error"));
|
||||||
|
}
|
||||||
|
}
|
173
src/format.rs
Normal file
173
src/format.rs
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
use crate::server::LSPServer;
|
||||||
|
use crate::sources::LSPSupport;
|
||||||
|
use log::info;
|
||||||
|
use ropey::Rope;
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
use tower_lsp::lsp_types::*;
|
||||||
|
|
||||||
|
impl LSPServer {
|
||||||
|
pub fn formatting(&self, params: DocumentFormattingParams) -> Option<Vec<TextEdit>> {
|
||||||
|
let uri = params.text_document.uri;
|
||||||
|
info!("formatting {}", &uri);
|
||||||
|
let file_id = self.srcs.get_id(&uri).to_owned();
|
||||||
|
self.srcs.wait_parse_ready(file_id, false);
|
||||||
|
let file = self.srcs.get_file(file_id)?;
|
||||||
|
let file = file.read().ok()?;
|
||||||
|
|
||||||
|
let conf = self.conf.read().unwrap();
|
||||||
|
if conf.verible.format.enabled {
|
||||||
|
Some(vec![TextEdit::new(
|
||||||
|
Range::new(
|
||||||
|
file.text.char_to_pos(0),
|
||||||
|
file.text.char_to_pos(file.text.len_chars()),
|
||||||
|
),
|
||||||
|
format_document(
|
||||||
|
&file.text,
|
||||||
|
None,
|
||||||
|
&conf.verible.format.path,
|
||||||
|
&conf.verible.format.args,
|
||||||
|
)?,
|
||||||
|
)])
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn range_formatting(&self, params: DocumentRangeFormattingParams) -> Option<Vec<TextEdit>> {
|
||||||
|
let uri = params.text_document.uri;
|
||||||
|
info!("range formatting {}", &uri);
|
||||||
|
let file_id = self.srcs.get_id(&uri).to_owned();
|
||||||
|
self.srcs.wait_parse_ready(file_id, false);
|
||||||
|
let file = self.srcs.get_file(file_id)?;
|
||||||
|
let file = file.read().ok()?;
|
||||||
|
|
||||||
|
let conf = self.conf.read().unwrap();
|
||||||
|
if conf.verible.format.enabled {
|
||||||
|
Some(vec![TextEdit::new(
|
||||||
|
file.text.char_range_to_range(0..file.text.len_chars()),
|
||||||
|
format_document(
|
||||||
|
&file.text,
|
||||||
|
Some(params.range),
|
||||||
|
&conf.verible.format.path,
|
||||||
|
&conf.verible.format.args,
|
||||||
|
)?,
|
||||||
|
)])
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// format the document using verible-verilog-format
|
||||||
|
pub fn format_document(
|
||||||
|
rope: &Rope,
|
||||||
|
range: Option<Range>,
|
||||||
|
verible_format_path: &str,
|
||||||
|
verible_format_args: &[String],
|
||||||
|
) -> Option<String> {
|
||||||
|
let mut child = Command::new(verible_format_path);
|
||||||
|
child
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.args(verible_format_args);
|
||||||
|
// rangeFormatting
|
||||||
|
if let Some(r) = range {
|
||||||
|
child
|
||||||
|
.arg("--lines")
|
||||||
|
.arg(format!("{}-{}", r.start.line + 1, r.end.line + 1));
|
||||||
|
}
|
||||||
|
let mut child = child.arg("-").spawn().ok()?;
|
||||||
|
|
||||||
|
// write file to stdin, read output from stdout
|
||||||
|
rope.write_to(child.stdin.as_mut()?).ok()?;
|
||||||
|
let output = child.wait_with_output().ok()?;
|
||||||
|
if output.status.success() {
|
||||||
|
info!("formatting succeeded");
|
||||||
|
let raw_output = String::from_utf8(output.stdout).ok()?;
|
||||||
|
Some(raw_output)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::server::ProjectConfig;
|
||||||
|
use crate::support::test_init;
|
||||||
|
use which::which;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_formatting() {
|
||||||
|
test_init();
|
||||||
|
let text = r#"
|
||||||
|
module test;
|
||||||
|
logic a;
|
||||||
|
logic b;
|
||||||
|
endmodule"#;
|
||||||
|
let text_fixed = r#"
|
||||||
|
module test;
|
||||||
|
logic a;
|
||||||
|
logic b;
|
||||||
|
endmodule
|
||||||
|
"#;
|
||||||
|
let doc = Rope::from_str(&text);
|
||||||
|
if which("verible-verilog-format").is_ok() {
|
||||||
|
assert_eq!(
|
||||||
|
format_document(
|
||||||
|
&doc,
|
||||||
|
None,
|
||||||
|
&ProjectConfig::default().verible.format.path,
|
||||||
|
&[]
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
text_fixed.to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_range_formatting() {
|
||||||
|
test_init();
|
||||||
|
let text = r#"module t1;
|
||||||
|
logic a;
|
||||||
|
logic b;
|
||||||
|
logic c;
|
||||||
|
endmodule
|
||||||
|
|
||||||
|
|
||||||
|
module t2;
|
||||||
|
logic a;
|
||||||
|
logic b;
|
||||||
|
logic c;
|
||||||
|
endmodule"#;
|
||||||
|
|
||||||
|
let text_fixed = r#"module t1;
|
||||||
|
logic a;
|
||||||
|
logic b;
|
||||||
|
logic c;
|
||||||
|
endmodule
|
||||||
|
|
||||||
|
|
||||||
|
module t2;
|
||||||
|
logic a;
|
||||||
|
logic b;
|
||||||
|
logic c;
|
||||||
|
endmodule
|
||||||
|
"#;
|
||||||
|
let doc = Rope::from_str(&text);
|
||||||
|
if which("verible-verilog-format").is_ok() {
|
||||||
|
assert_eq!(
|
||||||
|
format_document(
|
||||||
|
&doc,
|
||||||
|
Some(Range::new(Position::new(0, 0), Position::new(4, 9))),
|
||||||
|
&ProjectConfig::default().verible.format.path,
|
||||||
|
&[]
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
text_fixed.to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
src/lib.rs
Normal file
9
src/lib.rs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
#![recursion_limit = "256"]
|
||||||
|
|
||||||
|
pub mod completion;
|
||||||
|
pub mod definition;
|
||||||
|
pub mod diagnostics;
|
||||||
|
pub mod format;
|
||||||
|
pub mod server;
|
||||||
|
pub mod sources;
|
||||||
|
pub mod support;
|
35
src/main.rs
Normal file
35
src/main.rs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
#![recursion_limit = "256"]
|
||||||
|
|
||||||
|
use log::info;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use structopt::StructOpt;
|
||||||
|
use tower_lsp::{LspService, Server};
|
||||||
|
|
||||||
|
mod completion;
|
||||||
|
mod definition;
|
||||||
|
mod diagnostics;
|
||||||
|
mod format;
|
||||||
|
mod server;
|
||||||
|
mod sources;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod support;
|
||||||
|
use server::Backend;
|
||||||
|
|
||||||
|
#[derive(StructOpt, Debug)]
|
||||||
|
#[structopt(name = "veridian", about = "A SystemVerilog/Verilog Language Server")]
|
||||||
|
struct Opt {}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let _ = Opt::from_args();
|
||||||
|
let log_handle = flexi_logger::Logger::with(flexi_logger::LogSpecification::info())
|
||||||
|
.start()
|
||||||
|
.unwrap();
|
||||||
|
info!("starting veridian...");
|
||||||
|
|
||||||
|
let stdin = tokio::io::stdin();
|
||||||
|
let stdout = tokio::io::stdout();
|
||||||
|
|
||||||
|
let (service, messages) = LspService::new(|client| Arc::new(Backend::new(client, log_handle)));
|
||||||
|
Server::new(stdin, stdout, messages).serve(service).await;
|
||||||
|
}
|
385
src/server.rs
Normal file
385
src/server.rs
Normal file
@ -0,0 +1,385 @@
|
|||||||
|
use crate::sources::*;
|
||||||
|
|
||||||
|
use crate::completion::keyword::*;
|
||||||
|
use flexi_logger::LoggerHandle;
|
||||||
|
use log::{debug, info, warn};
|
||||||
|
use path_clean::PathClean;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::env::current_dir;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Read;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::string::ToString;
|
||||||
|
use std::sync::{Mutex, RwLock};
|
||||||
|
use tower_lsp::jsonrpc::{Error, ErrorCode, Result};
|
||||||
|
use tower_lsp::lsp_types::*;
|
||||||
|
use tower_lsp::{Client, LanguageServer};
|
||||||
|
use which::which;
|
||||||
|
|
||||||
|
pub struct LSPServer {
|
||||||
|
pub srcs: Sources,
|
||||||
|
pub key_comps: Vec<CompletionItem>,
|
||||||
|
pub sys_tasks: Vec<CompletionItem>,
|
||||||
|
pub directives: Vec<CompletionItem>,
|
||||||
|
pub conf: RwLock<ProjectConfig>,
|
||||||
|
pub log_handle: Mutex<Option<LoggerHandle>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LSPServer {
|
||||||
|
pub fn new(log_handle: Option<LoggerHandle>) -> LSPServer {
|
||||||
|
LSPServer {
|
||||||
|
srcs: Sources::new(),
|
||||||
|
key_comps: keyword_completions(KEYWORDS),
|
||||||
|
sys_tasks: other_completions(SYS_TASKS),
|
||||||
|
directives: other_completions(DIRECTIVES),
|
||||||
|
conf: RwLock::new(ProjectConfig::default()),
|
||||||
|
log_handle: Mutex::new(log_handle),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Backend {
|
||||||
|
client: Client,
|
||||||
|
server: LSPServer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Backend {
|
||||||
|
pub fn new(client: Client, log_handle: LoggerHandle) -> Backend {
|
||||||
|
Backend {
|
||||||
|
client,
|
||||||
|
server: LSPServer::new(Some(log_handle)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(strum_macros::Display, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum LogLevel {
|
||||||
|
#[strum(serialize = "error")]
|
||||||
|
Error,
|
||||||
|
#[strum(serialize = "warn")]
|
||||||
|
Warn,
|
||||||
|
#[strum(serialize = "info")]
|
||||||
|
Info,
|
||||||
|
#[strum(serialize = "debug")]
|
||||||
|
Debug,
|
||||||
|
#[strum(serialize = "trace")]
|
||||||
|
Trace,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct ProjectConfig {
|
||||||
|
// if true, recursively search the working directory for files to run diagnostics on
|
||||||
|
pub auto_search_workdir: bool,
|
||||||
|
// list of directories with header files
|
||||||
|
pub include_dirs: Vec<String>,
|
||||||
|
// list of directories to recursively search for SystemVerilog/Verilog sources
|
||||||
|
pub source_dirs: Vec<String>,
|
||||||
|
// config options for verible tools
|
||||||
|
pub verible: Verible,
|
||||||
|
// config options for verilator tools
|
||||||
|
pub verilator: Verilator,
|
||||||
|
// log level
|
||||||
|
pub log_level: LogLevel,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ProjectConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
ProjectConfig {
|
||||||
|
auto_search_workdir: true,
|
||||||
|
include_dirs: Vec::new(),
|
||||||
|
source_dirs: Vec::new(),
|
||||||
|
verible: Verible::default(),
|
||||||
|
verilator: Verilator::default(),
|
||||||
|
log_level: LogLevel::Info,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct Verible {
|
||||||
|
pub syntax: VeribleSyntax,
|
||||||
|
pub format: VeribleFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct VeribleSyntax {
|
||||||
|
pub enabled: bool,
|
||||||
|
pub path: String,
|
||||||
|
pub args: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for VeribleSyntax {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
enabled: true,
|
||||||
|
path: "verible-verilog-syntax".to_string(),
|
||||||
|
args: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct Verilator {
|
||||||
|
pub syntax: VerilatorSyntax,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct VerilatorSyntax {
|
||||||
|
pub enabled: bool,
|
||||||
|
pub path: String,
|
||||||
|
pub args: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for VerilatorSyntax {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
enabled: true,
|
||||||
|
path: "verilator".to_string(),
|
||||||
|
args: vec![
|
||||||
|
"--lint-only".to_string(),
|
||||||
|
"--sv".to_string(),
|
||||||
|
"-Wall".to_string(),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct VeribleFormat {
|
||||||
|
pub enabled: bool,
|
||||||
|
pub path: String,
|
||||||
|
pub args: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for VeribleFormat {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
enabled: true,
|
||||||
|
path: "verible-verilog-format".to_string(),
|
||||||
|
args: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_config(root_uri: Option<Url>) -> anyhow::Result<ProjectConfig> {
|
||||||
|
let path = root_uri
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("couldn't resolve workdir path"))?
|
||||||
|
.to_file_path()
|
||||||
|
.map_err(|_| anyhow::anyhow!("couldn't resolve workdir path"))?;
|
||||||
|
let mut config: Option<PathBuf> = None;
|
||||||
|
for dir in path.ancestors() {
|
||||||
|
let config_path = dir.join("veridian.yaml");
|
||||||
|
if config_path.exists() {
|
||||||
|
info!("found config: veridian.yaml");
|
||||||
|
config = Some(config_path);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let config_path = dir.join("veridian.yml");
|
||||||
|
if config_path.exists() {
|
||||||
|
info!("found config: veridian.yml");
|
||||||
|
config = Some(config_path);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut contents = String::new();
|
||||||
|
File::open(config.ok_or_else(|| anyhow::anyhow!("unable to read config file"))?)?
|
||||||
|
.read_to_string(&mut contents)?;
|
||||||
|
info!("reading config file");
|
||||||
|
Ok(serde_yaml::from_str(&contents)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert string path to absolute path
|
||||||
|
fn absolute_path(path_str: &str) -> Option<PathBuf> {
|
||||||
|
let path = PathBuf::from(path_str);
|
||||||
|
if !path.exists() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if !path.has_root() {
|
||||||
|
Some(current_dir().unwrap().join(path).clean())
|
||||||
|
} else {
|
||||||
|
Some(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tower_lsp::async_trait]
|
||||||
|
impl LanguageServer for Backend {
|
||||||
|
async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> {
|
||||||
|
// grab include dirs and source dirs from config, and convert to abs path
|
||||||
|
let mut inc_dirs = self.server.srcs.include_dirs.write().unwrap();
|
||||||
|
let mut src_dirs = self.server.srcs.source_dirs.write().unwrap();
|
||||||
|
match read_config(params.root_uri) {
|
||||||
|
Ok(conf) => {
|
||||||
|
inc_dirs.extend(conf.include_dirs.iter().filter_map(|x| absolute_path(x)));
|
||||||
|
debug!("{:#?}", inc_dirs);
|
||||||
|
src_dirs.extend(conf.source_dirs.iter().filter_map(|x| absolute_path(x)));
|
||||||
|
debug!("{:#?}", src_dirs);
|
||||||
|
let mut log_handle = self.server.log_handle.lock().unwrap();
|
||||||
|
let log_handle = log_handle.as_mut();
|
||||||
|
if let Some(handle) = log_handle {
|
||||||
|
handle
|
||||||
|
.parse_and_push_temp_spec(&conf.log_level.to_string())
|
||||||
|
.map_err(|e| Error {
|
||||||
|
code: ErrorCode::InvalidParams,
|
||||||
|
message: e.to_string().into(),
|
||||||
|
data: None,
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
*self.server.conf.write().unwrap() = conf;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("found errors in config file: {:#?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut conf = self.server.conf.write().unwrap();
|
||||||
|
conf.verible.syntax.enabled = which(&conf.verible.syntax.path).is_ok();
|
||||||
|
if cfg!(feature = "slang") {
|
||||||
|
info!("enabled linting with slang");
|
||||||
|
}
|
||||||
|
if conf.verilator.syntax.enabled {
|
||||||
|
info!("enabled linting with verilator")
|
||||||
|
} else if conf.verible.syntax.enabled {
|
||||||
|
info!("enabled linting with verible-verilog-syntax")
|
||||||
|
}
|
||||||
|
conf.verible.format.enabled = which(&conf.verible.format.path).is_ok();
|
||||||
|
if conf.verible.format.enabled {
|
||||||
|
info!("enabled formatting with verible-verilog-format");
|
||||||
|
} else {
|
||||||
|
info!("formatting unavailable");
|
||||||
|
}
|
||||||
|
drop(inc_dirs);
|
||||||
|
drop(src_dirs);
|
||||||
|
// parse all source files found from walking source dirs and include dirs
|
||||||
|
self.server.srcs.init();
|
||||||
|
Ok(InitializeResult {
|
||||||
|
server_info: None,
|
||||||
|
capabilities: ServerCapabilities {
|
||||||
|
text_document_sync: Some(TextDocumentSyncCapability::Options(
|
||||||
|
TextDocumentSyncOptions {
|
||||||
|
open_close: Some(true),
|
||||||
|
change: Some(TextDocumentSyncKind::INCREMENTAL),
|
||||||
|
will_save: None,
|
||||||
|
will_save_wait_until: None,
|
||||||
|
save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions {
|
||||||
|
include_text: None,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
completion_provider: Some(CompletionOptions {
|
||||||
|
resolve_provider: Some(false),
|
||||||
|
trigger_characters: Some(vec![
|
||||||
|
".".to_string(),
|
||||||
|
"$".to_string(),
|
||||||
|
"`".to_string(),
|
||||||
|
]),
|
||||||
|
work_done_progress_options: WorkDoneProgressOptions {
|
||||||
|
work_done_progress: None,
|
||||||
|
},
|
||||||
|
all_commit_characters: None,
|
||||||
|
//TODO: check if corect
|
||||||
|
completion_item: None,
|
||||||
|
}),
|
||||||
|
definition_provider: Some(OneOf::Left(true)),
|
||||||
|
hover_provider: Some(HoverProviderCapability::Simple(true)),
|
||||||
|
document_symbol_provider: Some(OneOf::Left(true)),
|
||||||
|
document_formatting_provider: Some(OneOf::Left(conf.verible.format.enabled)),
|
||||||
|
document_range_formatting_provider: Some(OneOf::Left(conf.verible.format.enabled)),
|
||||||
|
document_highlight_provider: Some(OneOf::Left(true)),
|
||||||
|
..ServerCapabilities::default()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async fn initialized(&self, _: InitializedParams) {
|
||||||
|
self.client
|
||||||
|
.log_message(MessageType::INFO, "veridian initialized!")
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
async fn shutdown(&self) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn did_open(&self, params: DidOpenTextDocumentParams) {
|
||||||
|
let diagnostics = self.server.did_open(params);
|
||||||
|
self.client
|
||||||
|
.publish_diagnostics(
|
||||||
|
diagnostics.uri,
|
||||||
|
diagnostics.diagnostics,
|
||||||
|
diagnostics.version,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
async fn did_change(&self, params: DidChangeTextDocumentParams) {
|
||||||
|
self.server.did_change(params);
|
||||||
|
}
|
||||||
|
async fn did_save(&self, params: DidSaveTextDocumentParams) {
|
||||||
|
let diagnostics = self.server.did_save(params);
|
||||||
|
self.client
|
||||||
|
.publish_diagnostics(
|
||||||
|
diagnostics.uri,
|
||||||
|
diagnostics.diagnostics,
|
||||||
|
diagnostics.version,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
|
||||||
|
Ok(self.server.completion(params))
|
||||||
|
}
|
||||||
|
async fn goto_definition(
|
||||||
|
&self,
|
||||||
|
params: GotoDefinitionParams,
|
||||||
|
) -> Result<Option<GotoDefinitionResponse>> {
|
||||||
|
Ok(self.server.goto_definition(params))
|
||||||
|
}
|
||||||
|
async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
|
||||||
|
Ok(self.server.hover(params))
|
||||||
|
}
|
||||||
|
async fn document_symbol(
|
||||||
|
&self,
|
||||||
|
params: DocumentSymbolParams,
|
||||||
|
) -> Result<Option<DocumentSymbolResponse>> {
|
||||||
|
Ok(self.server.document_symbol(params))
|
||||||
|
}
|
||||||
|
async fn formatting(&self, params: DocumentFormattingParams) -> Result<Option<Vec<TextEdit>>> {
|
||||||
|
Ok(self.server.formatting(params))
|
||||||
|
}
|
||||||
|
async fn range_formatting(
|
||||||
|
&self,
|
||||||
|
params: DocumentRangeFormattingParams,
|
||||||
|
) -> Result<Option<Vec<TextEdit>>> {
|
||||||
|
Ok(self.server.range_formatting(params))
|
||||||
|
}
|
||||||
|
async fn document_highlight(
|
||||||
|
&self,
|
||||||
|
params: DocumentHighlightParams,
|
||||||
|
) -> Result<Option<Vec<DocumentHighlight>>> {
|
||||||
|
Ok(self.server.document_highlight(params))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_config() {
|
||||||
|
let config = r#"
|
||||||
|
auto_search_workdir: false
|
||||||
|
format: true
|
||||||
|
verible:
|
||||||
|
syntax:
|
||||||
|
enabled: true
|
||||||
|
path: "verible-verilog-syntax"
|
||||||
|
format:
|
||||||
|
args:
|
||||||
|
- --net_variable_alignment=align
|
||||||
|
log_level: Info
|
||||||
|
"#;
|
||||||
|
let config = serde_yaml::from_str::<ProjectConfig>(config);
|
||||||
|
dbg!(&config);
|
||||||
|
assert!(config.is_ok());
|
||||||
|
}
|
||||||
|
}
|
629
src/sources.rs
Normal file
629
src/sources.rs
Normal file
@ -0,0 +1,629 @@
|
|||||||
|
use crate::definition::def_types::*;
|
||||||
|
use crate::definition::get_scopes;
|
||||||
|
use crate::diagnostics::{get_diagnostics, is_hidden};
|
||||||
|
use crate::server::LSPServer;
|
||||||
|
use log::{debug, error, trace};
|
||||||
|
use pathdiff::diff_paths;
|
||||||
|
use ropey::{Rope, RopeSlice};
|
||||||
|
use std::cmp::min;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::env::current_dir;
|
||||||
|
use std::fs;
|
||||||
|
use std::ops::Range as StdRange;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::{Arc, Condvar, Mutex, RwLock};
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Instant;
|
||||||
|
use sv_parser::*;
|
||||||
|
use thread::JoinHandle;
|
||||||
|
use tower_lsp::lsp_types::*;
|
||||||
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
|
impl LSPServer {
|
||||||
|
pub fn did_open(&self, params: DidOpenTextDocumentParams) -> PublishDiagnosticsParams {
|
||||||
|
let document: TextDocumentItem = params.text_document;
|
||||||
|
let uri = document.uri.clone();
|
||||||
|
debug!("did_open: {}", &uri);
|
||||||
|
// check if doc is already added
|
||||||
|
if self.srcs.names.read().unwrap().contains_key(&document.uri) {
|
||||||
|
// convert to a did_change that replace the entire text
|
||||||
|
self.did_change(DidChangeTextDocumentParams {
|
||||||
|
text_document: VersionedTextDocumentIdentifier::new(document.uri, document.version),
|
||||||
|
content_changes: vec![TextDocumentContentChangeEvent {
|
||||||
|
range: None,
|
||||||
|
range_length: None,
|
||||||
|
text: document.text,
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.srcs.add(document);
|
||||||
|
}
|
||||||
|
// diagnostics
|
||||||
|
let urls = self.srcs.names.read().unwrap().keys().cloned().collect();
|
||||||
|
let file_id = self.srcs.get_id(&uri);
|
||||||
|
let file = self.srcs.get_file(file_id).unwrap();
|
||||||
|
let file = file.read().unwrap();
|
||||||
|
get_diagnostics(uri, &file.text, urls, &self.conf.read().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn did_change(&self, params: DidChangeTextDocumentParams) {
|
||||||
|
debug!("did_change: {}", ¶ms.text_document.uri);
|
||||||
|
let file_id = self.srcs.get_id(¶ms.text_document.uri);
|
||||||
|
let file = self.srcs.get_file(file_id).unwrap();
|
||||||
|
let mut file = file.write().unwrap();
|
||||||
|
// loop through changes and apply
|
||||||
|
for change in params.content_changes {
|
||||||
|
if change.range.is_none() {
|
||||||
|
file.text = Rope::from_str(&change.text);
|
||||||
|
} else {
|
||||||
|
file.text.apply_change(&change);
|
||||||
|
}
|
||||||
|
file.last_change_range = change.range;
|
||||||
|
}
|
||||||
|
file.version = params.text_document.version;
|
||||||
|
drop(file);
|
||||||
|
|
||||||
|
// invalidate syntaxtree and wake parse thread
|
||||||
|
let meta_data = self.srcs.get_meta_data(file_id).unwrap();
|
||||||
|
let (lock, cvar) = &*meta_data.read().unwrap().valid_parse;
|
||||||
|
let mut valid = lock.lock().unwrap();
|
||||||
|
*valid = false;
|
||||||
|
cvar.notify_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn did_save(&self, params: DidSaveTextDocumentParams) -> PublishDiagnosticsParams {
|
||||||
|
let urls = self.srcs.names.read().unwrap().keys().cloned().collect();
|
||||||
|
let file_id = self.srcs.get_id(¶ms.text_document.uri);
|
||||||
|
let file = self.srcs.get_file(file_id).unwrap();
|
||||||
|
let file = file.read().unwrap();
|
||||||
|
get_diagnostics(
|
||||||
|
params.text_document.uri,
|
||||||
|
&file.text,
|
||||||
|
urls,
|
||||||
|
&self.conf.read().unwrap(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The Source struct holds all file specific information
|
||||||
|
pub struct Source {
|
||||||
|
pub id: usize,
|
||||||
|
pub uri: Url,
|
||||||
|
pub text: Rope,
|
||||||
|
pub version: i32,
|
||||||
|
pub syntax_tree: Option<SyntaxTree>,
|
||||||
|
// if there is a parse error, we can remove the last change
|
||||||
|
pub last_change_range: Option<Range>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// file metadata, including whether or not the syntax tree is up to date
|
||||||
|
pub struct SourceMeta {
|
||||||
|
pub id: usize,
|
||||||
|
pub valid_parse: Arc<(Mutex<bool>, Condvar)>,
|
||||||
|
pub parse_handle: JoinHandle<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// find SystemVerilog/Verilog sources recursively from opened files
|
||||||
|
fn find_src_paths(dirs: &[PathBuf]) -> Vec<PathBuf> {
|
||||||
|
let mut paths: Vec<PathBuf> = Vec::new();
|
||||||
|
|
||||||
|
for dir in dirs {
|
||||||
|
let walker = WalkDir::new(dir).into_iter();
|
||||||
|
for entry in walker.filter_entry(|e| !is_hidden(e)) {
|
||||||
|
let entry = entry.unwrap();
|
||||||
|
if entry.file_type().is_file() && entry.path().extension().is_some() {
|
||||||
|
let extension = entry.path().extension().unwrap();
|
||||||
|
|
||||||
|
if extension == "sv" || extension == "svh" || extension == "v" || extension == "vh"
|
||||||
|
{
|
||||||
|
let entry_path = entry.path().to_path_buf();
|
||||||
|
if !paths.contains(&entry_path) {
|
||||||
|
paths.push(entry_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
paths
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The Sources struct manages all source files
|
||||||
|
pub struct Sources {
|
||||||
|
// all files
|
||||||
|
pub files: Arc<RwLock<Vec<Arc<RwLock<Source>>>>>,
|
||||||
|
// map file urls to id
|
||||||
|
pub names: Arc<RwLock<HashMap<Url, usize>>>,
|
||||||
|
// file metadata
|
||||||
|
pub meta: Arc<RwLock<Vec<Arc<RwLock<SourceMeta>>>>>,
|
||||||
|
// all source files are indexed into this tree, which can then
|
||||||
|
// be used for completion, name resolution
|
||||||
|
pub scope_tree: Arc<RwLock<Option<GenericScope>>>,
|
||||||
|
// include directories, passed to parser to resolve `include
|
||||||
|
pub include_dirs: Arc<RwLock<Vec<PathBuf>>>,
|
||||||
|
// source directories
|
||||||
|
pub source_dirs: Arc<RwLock<Vec<PathBuf>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::default::Default for Sources {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sources {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
files: Arc::new(RwLock::new(Vec::new())),
|
||||||
|
names: Arc::new(RwLock::new(HashMap::new())),
|
||||||
|
meta: Arc::new(RwLock::new(Vec::new())),
|
||||||
|
scope_tree: Arc::new(RwLock::new(None)),
|
||||||
|
include_dirs: Arc::new(RwLock::new(Vec::new())),
|
||||||
|
source_dirs: Arc::new(RwLock::new(Vec::new())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn init(&self) {
|
||||||
|
let mut paths: Vec<PathBuf> = Vec::new();
|
||||||
|
for path in &*self.include_dirs.read().unwrap() {
|
||||||
|
paths.push(path.clone());
|
||||||
|
}
|
||||||
|
for path in &*self.source_dirs.read().unwrap() {
|
||||||
|
paths.push(path.clone());
|
||||||
|
}
|
||||||
|
// find and add all source/header files recursively from configured include and source directories
|
||||||
|
let src_paths = find_src_paths(&paths);
|
||||||
|
for path in src_paths {
|
||||||
|
if let Ok(url) = Url::from_file_path(&path) {
|
||||||
|
if let Ok(text) = fs::read_to_string(&path) {
|
||||||
|
self.add(TextDocumentItem::new(
|
||||||
|
url,
|
||||||
|
"systemverilog".to_string(),
|
||||||
|
-1,
|
||||||
|
text,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// add a source file, creating a parse thread for that file
|
||||||
|
pub fn add(&self, doc: TextDocumentItem) {
|
||||||
|
// use a condvar to synchronize the parse thread
|
||||||
|
// the valid bool decides whether or not the file
|
||||||
|
// needs to be re-parsed
|
||||||
|
#[allow(clippy::mutex_atomic)] // https://github.com/rust-lang/rust-clippy/issues/1516
|
||||||
|
let valid_parse = Arc::new((Mutex::new(false), Condvar::new()));
|
||||||
|
let valid_parse2 = valid_parse.clone();
|
||||||
|
let mut files = self.files.write().unwrap();
|
||||||
|
let source = Arc::new(RwLock::new(Source {
|
||||||
|
id: files.len(),
|
||||||
|
uri: doc.uri.clone(),
|
||||||
|
text: Rope::from_str(&doc.text),
|
||||||
|
version: doc.version,
|
||||||
|
syntax_tree: None,
|
||||||
|
last_change_range: None,
|
||||||
|
}));
|
||||||
|
let source_handle = source.clone();
|
||||||
|
let scope_handle = self.scope_tree.clone();
|
||||||
|
let inc_dirs = self.include_dirs.clone();
|
||||||
|
|
||||||
|
// spawn parse thread
|
||||||
|
let parse_handle = thread::spawn(move || {
|
||||||
|
let (lock, cvar) = &*valid_parse2;
|
||||||
|
loop {
|
||||||
|
let now = Instant::now();
|
||||||
|
let file = source_handle.read().unwrap();
|
||||||
|
let text = file.text.clone();
|
||||||
|
let uri = &file.uri.clone();
|
||||||
|
let range = &file.last_change_range.clone();
|
||||||
|
drop(file);
|
||||||
|
trace!("{}, parse read: {}", uri, now.elapsed().as_millis());
|
||||||
|
let syntax_tree = parse(&text, uri, range, &inc_dirs.read().unwrap());
|
||||||
|
let mut scope_tree = match &syntax_tree {
|
||||||
|
Some(tree) => get_scopes(tree, uri),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
trace!(
|
||||||
|
"{}, parse read complete: {}",
|
||||||
|
uri,
|
||||||
|
now.elapsed().as_millis()
|
||||||
|
);
|
||||||
|
let mut file = source_handle.write().unwrap();
|
||||||
|
trace!("{}, parse write: {}", uri, now.elapsed().as_millis());
|
||||||
|
file.syntax_tree = syntax_tree;
|
||||||
|
drop(file);
|
||||||
|
debug!("try write global scope");
|
||||||
|
let mut global_scope = scope_handle.write().unwrap();
|
||||||
|
match &mut *global_scope {
|
||||||
|
Some(scope) => match &mut scope_tree {
|
||||||
|
Some(tree) => {
|
||||||
|
scope.defs.retain(|x| &x.url() != uri);
|
||||||
|
scope.scopes.retain(|x| &x.url() != uri);
|
||||||
|
scope.defs.append(&mut tree.defs);
|
||||||
|
scope.scopes.append(&mut tree.scopes);
|
||||||
|
}
|
||||||
|
None => (),
|
||||||
|
},
|
||||||
|
None => *global_scope = scope_tree,
|
||||||
|
}
|
||||||
|
// eprintln!("{:#?}", *global_scope);
|
||||||
|
drop(global_scope);
|
||||||
|
trace!("{}, write global scope", uri);
|
||||||
|
trace!(
|
||||||
|
"{}, parse write complete: {}",
|
||||||
|
uri,
|
||||||
|
now.elapsed().as_millis()
|
||||||
|
);
|
||||||
|
let mut valid = lock.lock().unwrap();
|
||||||
|
*valid = true;
|
||||||
|
cvar.notify_all();
|
||||||
|
while *valid {
|
||||||
|
valid = cvar.wait(valid).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
files.push(source);
|
||||||
|
let fid = files.len() - 1;
|
||||||
|
self.meta
|
||||||
|
.write()
|
||||||
|
.unwrap()
|
||||||
|
.push(Arc::new(RwLock::new(SourceMeta {
|
||||||
|
id: fid,
|
||||||
|
valid_parse,
|
||||||
|
parse_handle,
|
||||||
|
})));
|
||||||
|
debug!("added {}", &doc.uri);
|
||||||
|
self.names.write().unwrap().insert(doc.uri, fid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get file by id
|
||||||
|
pub fn get_file(&self, id: usize) -> Option<Arc<RwLock<Source>>> {
|
||||||
|
let files = self.files.read().ok()?;
|
||||||
|
for file in files.iter() {
|
||||||
|
let source = file.read().ok()?;
|
||||||
|
if source.id == id {
|
||||||
|
return Some(file.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get metadata by file id
|
||||||
|
pub fn get_meta_data(&self, id: usize) -> Option<Arc<RwLock<SourceMeta>>> {
|
||||||
|
let meta = self.meta.read().ok()?;
|
||||||
|
for data in meta.iter() {
|
||||||
|
let i = data.read().ok()?;
|
||||||
|
if i.id == id {
|
||||||
|
return Some(data.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// wait for a valid parse
|
||||||
|
pub fn wait_parse_ready(&self, id: usize, wait_valid: bool) {
|
||||||
|
let file = self.get_file(id).unwrap();
|
||||||
|
let file = file.read().unwrap();
|
||||||
|
if file.syntax_tree.is_none() || wait_valid {
|
||||||
|
drop(file);
|
||||||
|
let meta_data = self.get_meta_data(id).unwrap();
|
||||||
|
let (lock, cvar) = &*meta_data.read().unwrap().valid_parse;
|
||||||
|
let mut valid = lock.lock().unwrap();
|
||||||
|
while !*valid {
|
||||||
|
valid = cvar.wait(valid).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get file id from url
|
||||||
|
pub fn get_id(&self, uri: &Url) -> usize {
|
||||||
|
*self.names.read().unwrap().get(uri).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// compute identifier completions
|
||||||
|
pub fn get_completions(
|
||||||
|
&self,
|
||||||
|
token: &str,
|
||||||
|
byte_idx: usize,
|
||||||
|
url: &Url,
|
||||||
|
) -> Option<CompletionList> {
|
||||||
|
debug!("retrieving identifier completion for token: {}", &token);
|
||||||
|
Some(CompletionList {
|
||||||
|
is_incomplete: false,
|
||||||
|
items: self
|
||||||
|
.scope_tree
|
||||||
|
.read()
|
||||||
|
.ok()?
|
||||||
|
.as_ref()?
|
||||||
|
.get_completion(token, byte_idx, url),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// compute dot completions
|
||||||
|
pub fn get_dot_completions(
|
||||||
|
&self,
|
||||||
|
token: &str,
|
||||||
|
byte_idx: usize,
|
||||||
|
url: &Url,
|
||||||
|
) -> Option<CompletionList> {
|
||||||
|
debug!("retrieving dot completion for token: {}", &token);
|
||||||
|
let tree = self.scope_tree.read().ok()?;
|
||||||
|
Some(CompletionList {
|
||||||
|
is_incomplete: false,
|
||||||
|
items: tree
|
||||||
|
.as_ref()?
|
||||||
|
.get_dot_completion(token, byte_idx, url, tree.as_ref()?),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: show all unrecoverable parse errors to user
|
||||||
|
/// parse the file using sv-parser, attempt to recover if the parse fails
|
||||||
|
pub fn parse(
|
||||||
|
doc: &Rope,
|
||||||
|
uri: &Url,
|
||||||
|
last_change_range: &Option<Range>,
|
||||||
|
inc_paths: &[PathBuf],
|
||||||
|
) -> Option<SyntaxTree> {
|
||||||
|
let mut parse_iterations = 1;
|
||||||
|
let mut i = 0;
|
||||||
|
let mut includes: Vec<PathBuf> = inc_paths.to_vec();
|
||||||
|
let mut reverted_change = false;
|
||||||
|
let mut text = doc.clone();
|
||||||
|
|
||||||
|
while i < parse_iterations {
|
||||||
|
i += 1;
|
||||||
|
match parse_sv_str(
|
||||||
|
&text.to_string(),
|
||||||
|
uri.to_file_path().unwrap(),
|
||||||
|
&HashMap::new(),
|
||||||
|
&includes,
|
||||||
|
false,
|
||||||
|
) {
|
||||||
|
Ok((syntax_tree, _)) => {
|
||||||
|
debug!("parse complete of {}", uri);
|
||||||
|
trace!("{}", syntax_tree.to_string());
|
||||||
|
return Some(syntax_tree);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
match err {
|
||||||
|
// syntax error
|
||||||
|
sv_parser::Error::Parse(trace) => match trace {
|
||||||
|
Some((_, bpos)) => {
|
||||||
|
let mut line_start = text.byte_to_line(bpos);
|
||||||
|
let mut line_end = text.byte_to_line(bpos) + 1;
|
||||||
|
if !reverted_change {
|
||||||
|
if let Some(range) = last_change_range {
|
||||||
|
line_start = range.start.line as usize;
|
||||||
|
line_end = range.end.line as usize + 1;
|
||||||
|
reverted_change = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for line_idx in line_start..line_end {
|
||||||
|
let line = text.line(line_idx);
|
||||||
|
let start_char = text.line_to_char(line_idx);
|
||||||
|
let line_length = line.len_chars();
|
||||||
|
text.remove(start_char..(start_char + line_length - 1));
|
||||||
|
text.insert(start_char, &" ".to_owned().repeat(line_length));
|
||||||
|
}
|
||||||
|
parse_iterations += 1;
|
||||||
|
}
|
||||||
|
None => return None,
|
||||||
|
},
|
||||||
|
// include error, take the include path from the error message and
|
||||||
|
// add it as an include dir for the next parser invocation
|
||||||
|
sv_parser::Error::Include { source: x } => {
|
||||||
|
if let sv_parser::Error::File { source: _, path: z } = *x {
|
||||||
|
// Include paths have to be relative to the working directory
|
||||||
|
// so we have to convert a source file relative path to a working directory
|
||||||
|
// relative path. This should have been handled by sv-parser
|
||||||
|
let mut inc_path_given = z.clone();
|
||||||
|
let mut uri_path = uri.to_file_path().unwrap();
|
||||||
|
uri_path.pop();
|
||||||
|
let rel_path = diff_paths(uri_path, current_dir().unwrap()).unwrap();
|
||||||
|
inc_path_given.pop();
|
||||||
|
let inc_path = rel_path.join(inc_path_given);
|
||||||
|
if !includes.contains(&inc_path) {
|
||||||
|
includes.push(inc_path);
|
||||||
|
} else {
|
||||||
|
error!("parser: include error: {:?}", z);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
parse_iterations += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => error!("parse error, {:?}", err),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: add bounds checking for utf8<->utf16 conversions
|
||||||
|
/// This trait defines some helper functions to convert between lsp types
|
||||||
|
/// and char/byte positions
|
||||||
|
pub trait LSPSupport {
|
||||||
|
fn pos_to_byte(&self, pos: &Position) -> usize;
|
||||||
|
fn pos_to_char(&self, pos: &Position) -> usize;
|
||||||
|
fn byte_to_pos(&self, byte_idx: usize) -> Position;
|
||||||
|
fn char_to_pos(&self, char_idx: usize) -> Position;
|
||||||
|
fn range_to_char_range(&self, range: &Range) -> StdRange<usize>;
|
||||||
|
fn char_range_to_range(&self, range: StdRange<usize>) -> Range;
|
||||||
|
fn apply_change(&mut self, change: &TextDocumentContentChangeEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extend ropey's Rope type with lsp convenience functions
|
||||||
|
impl LSPSupport for Rope {
|
||||||
|
fn pos_to_byte(&self, pos: &Position) -> usize {
|
||||||
|
self.char_to_byte(self.pos_to_char(pos))
|
||||||
|
}
|
||||||
|
fn pos_to_char(&self, pos: &Position) -> usize {
|
||||||
|
let line_slice = self.line(pos.line as usize);
|
||||||
|
self.line_to_char(pos.line as usize) + line_slice.utf16_cu_to_char(pos.character as usize)
|
||||||
|
}
|
||||||
|
fn byte_to_pos(&self, byte_idx: usize) -> Position {
|
||||||
|
self.char_to_pos(self.byte_to_char(min(byte_idx, self.len_bytes() - 1)))
|
||||||
|
}
|
||||||
|
fn char_to_pos(&self, char_idx: usize) -> Position {
|
||||||
|
let line = self.char_to_line(char_idx);
|
||||||
|
let line_slice = self.line(line);
|
||||||
|
Position {
|
||||||
|
line: line as u32,
|
||||||
|
character: line_slice.char_to_utf16_cu(char_idx - self.line_to_char(line)) as u32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn range_to_char_range(&self, range: &Range) -> StdRange<usize> {
|
||||||
|
self.pos_to_char(&range.start)..self.pos_to_char(&range.end)
|
||||||
|
}
|
||||||
|
fn char_range_to_range(&self, range: StdRange<usize>) -> Range {
|
||||||
|
Range {
|
||||||
|
start: self.char_to_pos(range.start),
|
||||||
|
end: self.char_to_pos(range.end),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn apply_change(&mut self, change: &TextDocumentContentChangeEvent) {
|
||||||
|
if let Some(range) = change.range {
|
||||||
|
let char_range = self.range_to_char_range(&range);
|
||||||
|
self.remove(char_range.clone());
|
||||||
|
if !change.text.is_empty() {
|
||||||
|
self.insert(char_range.start, &change.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> LSPSupport for RopeSlice<'a> {
|
||||||
|
fn pos_to_byte(&self, pos: &Position) -> usize {
|
||||||
|
self.char_to_byte(self.pos_to_char(pos))
|
||||||
|
}
|
||||||
|
fn pos_to_char(&self, pos: &Position) -> usize {
|
||||||
|
let line_slice = self.line(pos.line as usize);
|
||||||
|
self.line_to_char(pos.line as usize) + line_slice.utf16_cu_to_char(pos.character as usize)
|
||||||
|
}
|
||||||
|
fn byte_to_pos(&self, byte_idx: usize) -> Position {
|
||||||
|
self.char_to_pos(self.byte_to_char(min(byte_idx, self.len_bytes() - 1)))
|
||||||
|
}
|
||||||
|
fn char_to_pos(&self, char_idx: usize) -> Position {
|
||||||
|
let line = self.char_to_line(char_idx);
|
||||||
|
let line_slice = self.line(line);
|
||||||
|
Position {
|
||||||
|
line: line as u32,
|
||||||
|
character: line_slice.char_to_utf16_cu(char_idx - self.line_to_char(line)) as u32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn range_to_char_range(&self, range: &Range) -> StdRange<usize> {
|
||||||
|
self.pos_to_char(&range.start)..self.pos_to_char(&range.end)
|
||||||
|
}
|
||||||
|
fn char_range_to_range(&self, range: StdRange<usize>) -> Range {
|
||||||
|
Range {
|
||||||
|
start: self.char_to_pos(range.start),
|
||||||
|
end: self.char_to_pos(range.end),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn apply_change(&mut self, _: &TextDocumentContentChangeEvent) {
|
||||||
|
panic!("can't edit a rope slice");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::support::test_init;
|
||||||
|
use std::fs::read_to_string;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_open_and_change() {
|
||||||
|
test_init();
|
||||||
|
let server = LSPServer::new(None);
|
||||||
|
let uri = Url::parse("file:///test.sv").unwrap();
|
||||||
|
let text = r#"module test;
|
||||||
|
logic abc;
|
||||||
|
endmodule"#;
|
||||||
|
|
||||||
|
let open_params = DidOpenTextDocumentParams {
|
||||||
|
text_document: TextDocumentItem {
|
||||||
|
uri: uri.clone(),
|
||||||
|
language_id: "systemverilog".to_owned(),
|
||||||
|
version: 0,
|
||||||
|
text: text.to_owned(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
server.did_open(open_params);
|
||||||
|
let fid = server.srcs.get_id(&uri);
|
||||||
|
let file = server.srcs.get_file(fid).unwrap();
|
||||||
|
let file = file.read().unwrap();
|
||||||
|
assert_eq!(file.text.to_string(), text.to_owned());
|
||||||
|
drop(file);
|
||||||
|
|
||||||
|
let change_params = DidChangeTextDocumentParams {
|
||||||
|
text_document: VersionedTextDocumentIdentifier { uri, version: 1 },
|
||||||
|
content_changes: vec![TextDocumentContentChangeEvent {
|
||||||
|
range: Some(Range {
|
||||||
|
start: Position {
|
||||||
|
line: 1,
|
||||||
|
character: 8,
|
||||||
|
},
|
||||||
|
end: Position {
|
||||||
|
line: 1,
|
||||||
|
character: 11,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
range_length: None,
|
||||||
|
text: "var1".to_owned(),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
server.did_change(change_params);
|
||||||
|
let file = server.srcs.get_file(fid).unwrap();
|
||||||
|
let file = file.read().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
file.text.to_string(),
|
||||||
|
r#"module test;
|
||||||
|
logic var1;
|
||||||
|
endmodule"#
|
||||||
|
.to_owned()
|
||||||
|
);
|
||||||
|
assert_eq!(file.version, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fault_tolerance() {
|
||||||
|
test_init();
|
||||||
|
let server = LSPServer::new(None);
|
||||||
|
let uri = Url::parse("file:///test.sv").unwrap();
|
||||||
|
let text = r#"module test;
|
||||||
|
logic abc
|
||||||
|
endmodule"#;
|
||||||
|
let open_params = DidOpenTextDocumentParams {
|
||||||
|
text_document: TextDocumentItem {
|
||||||
|
uri: uri.clone(),
|
||||||
|
language_id: "systemverilog".to_owned(),
|
||||||
|
version: 0,
|
||||||
|
text: text.to_owned(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
server.did_open(open_params);
|
||||||
|
let fid = server.srcs.get_id(&uri);
|
||||||
|
|
||||||
|
server.srcs.wait_parse_ready(fid, true);
|
||||||
|
|
||||||
|
assert!(server
|
||||||
|
.srcs
|
||||||
|
.scope_tree
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.contains_scope("test"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_header() {
|
||||||
|
test_init();
|
||||||
|
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
d.push("test_data/top_inc.sv");
|
||||||
|
let text = read_to_string(&d).unwrap();
|
||||||
|
let doc = Rope::from_str(&text);
|
||||||
|
assert!(parse(&doc, &Url::from_file_path(d).unwrap(), &None, &Vec::new()).is_some(),);
|
||||||
|
// TODO: add missing header test
|
||||||
|
}
|
||||||
|
}
|
3
src/support.rs
Normal file
3
src/support.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pub fn test_init() {
|
||||||
|
let _ = flexi_logger::Logger::with(flexi_logger::LogSpecification::info()).start();
|
||||||
|
}
|
4
test/client/src/.gitignore
vendored
Normal file
4
test/client/src/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
out
|
||||||
|
node_modules
|
||||||
|
.vscode-test
|
||||||
|
*.vsix
|
8
test/client/src/.vscodeignore
Normal file
8
test/client/src/.vscodeignore
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
.vscode/**
|
||||||
|
**/*.ts
|
||||||
|
**/*.map
|
||||||
|
.gitignore
|
||||||
|
**/tsconfig.json
|
||||||
|
**/tsconfig.base.json
|
||||||
|
contributing.md
|
||||||
|
.travis.yml
|
6
test/client/src/README.md
Normal file
6
test/client/src/README.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# veridian
|
||||||
|
|
||||||
|
a vscode client extension for the veridian language server.
|
||||||
|
|
||||||
|
- veridian must be installed seperately, see https://github.com/vivekmalneedi/veridian for details
|
||||||
|
- provides syntax highlighting, the grammar for which is borrowed from https://github.com/TheClams/SystemVerilog and is under the Apache 2.0 License
|
56
test/client/src/extension.ts
Normal file
56
test/client/src/extension.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
/* --------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||||
|
* ------------------------------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
import { workspace, ExtensionContext } from "vscode";
|
||||||
|
|
||||||
|
import {
|
||||||
|
LanguageClient,
|
||||||
|
LanguageClientOptions,
|
||||||
|
ServerOptions,
|
||||||
|
Executable,
|
||||||
|
} from "vscode-languageclient/node";
|
||||||
|
|
||||||
|
let client: LanguageClient;
|
||||||
|
const workSpaceFolder = workspace.workspaceFolders?.[0];
|
||||||
|
let cwd: string = workSpaceFolder.uri.fsPath;
|
||||||
|
const serverPath: string = workspace.getConfiguration().get("veridian.serverPath");
|
||||||
|
|
||||||
|
export function activate(context: ExtensionContext) {
|
||||||
|
const run: Executable = {
|
||||||
|
command: serverPath,
|
||||||
|
// options: { cwd },
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the extension is launched in debug mode then the debug server options are used
|
||||||
|
// Otherwise the run options are used
|
||||||
|
let serverOptions: ServerOptions = {
|
||||||
|
run,
|
||||||
|
debug: run,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Options to control the language client
|
||||||
|
let clientOptions: LanguageClientOptions = {
|
||||||
|
// Register the server for plain text documents
|
||||||
|
documentSelector: [{ scheme: "file", language: "systemverilog" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the language client and start the client.
|
||||||
|
client = new LanguageClient(
|
||||||
|
"veridian",
|
||||||
|
"veridian",
|
||||||
|
serverOptions,
|
||||||
|
clientOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
// Start the client. This will also launch the server
|
||||||
|
client.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deactivate(): Thenable<void> | undefined {
|
||||||
|
if (!client) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return client.stop();
|
||||||
|
}
|
4368
test/client/src/package-lock.json
generated
Normal file
4368
test/client/src/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
96
test/client/src/package.json
Normal file
96
test/client/src/package.json
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
{
|
||||||
|
"name": "veridian",
|
||||||
|
"description": "A client for the Veridian Language Server for SystemVerilog/Verilog",
|
||||||
|
"author": "Vivek Malneedi",
|
||||||
|
"publisher": "vivekmalneedi",
|
||||||
|
"license": "MIT",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"categories": [
|
||||||
|
"Programming Languages",
|
||||||
|
"Snippets",
|
||||||
|
"Linters"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"SystemVerilog",
|
||||||
|
"Verilog"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/vivekmalneedi/veridian"
|
||||||
|
},
|
||||||
|
"activationEvents": [
|
||||||
|
"onLanguage:systemverilog",
|
||||||
|
"onLanguage:verilog"
|
||||||
|
],
|
||||||
|
"main": "./out/extension",
|
||||||
|
"scripts": {
|
||||||
|
"vscode:prepublish": "npm run compile",
|
||||||
|
"compile": "tsc -b",
|
||||||
|
"watch": "tsc -b -w"
|
||||||
|
},
|
||||||
|
"contributes": {
|
||||||
|
"languages": [
|
||||||
|
{
|
||||||
|
"id": "systemverilog",
|
||||||
|
"extensions": [
|
||||||
|
".sv",
|
||||||
|
".svh",
|
||||||
|
".v",
|
||||||
|
".vh",
|
||||||
|
".verilog"
|
||||||
|
],
|
||||||
|
"aliases": [
|
||||||
|
"SystemVerilog",
|
||||||
|
"verilog",
|
||||||
|
"Verilog"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grammars": [
|
||||||
|
{
|
||||||
|
"language": "systemverilog",
|
||||||
|
"scopeName": "source.systemverilog",
|
||||||
|
"path": "./syntaxes/systemverilog.tmLanguage.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configuration": {
|
||||||
|
"type": "object",
|
||||||
|
"title": "veridian",
|
||||||
|
"properties": {
|
||||||
|
"veridian.serverPath": {
|
||||||
|
"scope": "window",
|
||||||
|
"type": "string",
|
||||||
|
"default": "veridian",
|
||||||
|
"description": "path of the veridian binary"
|
||||||
|
},
|
||||||
|
"veridian.trace.server": {
|
||||||
|
"scope": "window",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"off",
|
||||||
|
"messages",
|
||||||
|
"verbose"
|
||||||
|
],
|
||||||
|
"default": "off",
|
||||||
|
"description": "Traces the communication between VS Code and the language server."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"vscode": "^1.56.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"vscode-languageclient": "^7.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/vscode": "^1.56.0",
|
||||||
|
"vscode-test": "^1.5.2",
|
||||||
|
"@types/mocha": "^8.2.2",
|
||||||
|
"mocha": "^8.4.0",
|
||||||
|
"@types/node": "^15.12.1",
|
||||||
|
"eslint": "^7.28.0",
|
||||||
|
"@typescript-eslint/parser": "^4.26.0",
|
||||||
|
"typescript": "^4.3.2"
|
||||||
|
}
|
||||||
|
}
|
12
test/client/src/tsconfig.json
Normal file
12
test/client/src/tsconfig.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es2019",
|
||||||
|
"lib": ["ES2019"],
|
||||||
|
"outDir": "out",
|
||||||
|
"rootDir": "src",
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules", ".vscode-test"]
|
||||||
|
}
|
1051
test/client/syntaxes/systemverilog.tmLanguage.json
Normal file
1051
test/client/syntaxes/systemverilog.tmLanguage.json
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user