413 lines
15 KiB
Rust
413 lines
15 KiB
Rust
use std::collections::HashSet;
|
|
|
|
use crate::{completion::feature::{get_dot_completion, include_path_completion}, core, hover::feature::make_module_profile_code, server::LSPServer, sources::LSPSupport, utils::get_language_id_by_uri};
|
|
use log::info;
|
|
use ropey::{Rope, RopeSlice};
|
|
use tower_lsp::lsp_types::*;
|
|
|
|
|
|
pub fn completion(server: &LSPServer, params: &CompletionParams) -> Option<CompletionResponse> {
|
|
let doc = ¶ms.text_document_position;
|
|
let uri = ¶ms.text_document_position.text_document.uri;
|
|
let pos = doc.position;
|
|
let language_id = get_language_id_by_uri(uri);
|
|
|
|
let file_id = server.srcs.get_id(uri).to_owned();
|
|
server.srcs.wait_parse_ready(file_id, false);
|
|
let file = server.srcs.get_file(file_id)?;
|
|
let file = file.read().ok()?;
|
|
let line_text = file.text.line(doc.position.line as usize);
|
|
let token = get_completion_token(
|
|
&file.text,
|
|
line_text.clone(),
|
|
doc.position,
|
|
);
|
|
|
|
// info!("trigger completion token: {}", token);
|
|
let line_text = file.text.line(pos.line as usize);
|
|
|
|
let response = match ¶ms.context {
|
|
Some(context) => match context.trigger_kind {
|
|
// 特殊字符触发
|
|
CompletionTriggerKind::TRIGGER_CHARACTER => {
|
|
// info!("trigger character");
|
|
let trigger_character = context.trigger_character.clone().unwrap();
|
|
match trigger_character.as_str() {
|
|
"." => {
|
|
info!("trigger dot completion");
|
|
get_dot_completion(server, &line_text, uri, &pos, &language_id)
|
|
},
|
|
"$" => Some(CompletionList {
|
|
is_incomplete: false,
|
|
items: server.vlog_sys_tasks_completion_items.clone(),
|
|
}),
|
|
"`" => Some(CompletionList {
|
|
is_incomplete: false,
|
|
items: server.vlog_directives.clone(),
|
|
}),
|
|
"/" => {
|
|
info!("trigger include");
|
|
include_path_completion(&doc.text_document.uri, &line_text, pos)
|
|
},
|
|
"\"" => {
|
|
info!("trigger include");
|
|
include_path_completion(&doc.text_document.uri, &line_text, pos)
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
CompletionTriggerKind::TRIGGER_FOR_INCOMPLETE_COMPLETIONS => None,
|
|
// 常规触发
|
|
CompletionTriggerKind::INVOKED => {
|
|
// 1. 先根据 AST 获取上下文补全项
|
|
let mut completion_items = server.srcs.get_completions(
|
|
&token,
|
|
file.text.pos_to_byte(&doc.position),
|
|
&doc.text_document.uri,
|
|
)?;
|
|
|
|
info!("current completion token: {}", token);
|
|
|
|
// 2. 根据 token 加入关键词
|
|
// TODO: 考虑使用前缀树进行优化
|
|
completion_items.items.extend::<Vec<CompletionItem>>(
|
|
server.vlog_keyword_completion_items
|
|
.iter()
|
|
.filter(|x| x.label.starts_with(&token))
|
|
.cloned()
|
|
.collect(),
|
|
);
|
|
|
|
// 3. 根据 token 加入系统函数
|
|
// TODO: 考虑使用前缀树进行优化
|
|
completion_items.items.extend::<Vec<CompletionItem>>(
|
|
server.vlog_sys_tasks_completion_items
|
|
.iter()
|
|
.filter(|x| x.label.starts_with(&token))
|
|
.cloned()
|
|
.collect(),
|
|
);
|
|
|
|
// 4. 加入例化自动补全的
|
|
completion_items.items.extend::<Vec<CompletionItem>>(
|
|
make_module_completions(server, &token, &language_id)
|
|
);
|
|
|
|
completion_items.items.dedup_by_key(|i| i.label.clone());
|
|
|
|
|
|
// info!("invoked return completion_items {:?}", completion_items);
|
|
Some(completion_items)
|
|
}
|
|
_ => None,
|
|
},
|
|
None => {
|
|
let trigger = prev_char(&file.text, &doc.position);
|
|
match trigger {
|
|
'.' => Some(server.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: server.vlog_sys_tasks_completion_items.clone(),
|
|
}),
|
|
'`' => Some(CompletionList {
|
|
is_incomplete: false,
|
|
items: server.vlog_directives.clone(),
|
|
}),
|
|
_ => {
|
|
let mut completion_items = server.srcs.get_completions(
|
|
&token,
|
|
file.text.pos_to_byte(&doc.position),
|
|
&doc.text_document.uri,
|
|
)?;
|
|
// info!("current completion token: {}", token);
|
|
|
|
completion_items.items.extend::<Vec<CompletionItem>>(
|
|
server.vlog_keyword_completion_items
|
|
.iter()
|
|
.filter(|x| x.label.starts_with(&token))
|
|
.cloned()
|
|
.collect(),
|
|
);
|
|
completion_items.items.dedup_by_key(|i| i.label.clone());
|
|
|
|
Some(completion_items)
|
|
}
|
|
}
|
|
}
|
|
};
|
|
// 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
|
|
}
|
|
}
|
|
|
|
fn make_primitives_instantiation_code(text: &str) -> String {
|
|
let mut instantiations = text.lines()
|
|
.filter(|line| !line.trim().is_empty() && !line.trim().starts_with("//"))
|
|
.collect::<Vec<&str>>();
|
|
// remove fake module and endmodule
|
|
instantiations.remove(0);
|
|
instantiations.pop();
|
|
|
|
instantiations.iter().map(|line| {
|
|
let trimmed = line.trim_start();
|
|
if trimmed.contains('.') {
|
|
format!("\t{}", trimmed)
|
|
} else {
|
|
trimmed.to_string()
|
|
}
|
|
}).collect::<Vec<String>>().join("\n")
|
|
}
|
|
|
|
fn make_primitives_module_profile_code(text: &str) -> String {
|
|
let mut lines: Vec<&str> = text.split_inclusive('\n').collect();
|
|
|
|
if lines.len() > 1 {
|
|
lines.remove(0);
|
|
lines.pop();
|
|
}
|
|
|
|
lines.join("")
|
|
}
|
|
|
|
|
|
fn make_instantiation_code(module: &crate::core::hdlparam::Module) -> String {
|
|
// makeVlogParamAssignments 参数信息列表
|
|
let mut snippet_codes = Vec::<String>::new();
|
|
let mut placeholder_id: u32 = 1;
|
|
|
|
snippet_codes.push(format!("{} ", module.name));
|
|
|
|
let params_length = module.params.len();
|
|
let ports_length = module.ports.len();
|
|
|
|
if params_length > 0 {
|
|
snippet_codes.push("#(\n".to_string());
|
|
|
|
let max_name_length = module.params.iter().map(|param| param.name.len()).max().unwrap_or(0);
|
|
let max_init_length = module.params.iter().map(|param| param.init.len()).max().unwrap_or(0);
|
|
let mut i: usize = 0;
|
|
for param in &module.params {
|
|
// 写入 .NAME ( INIT ),
|
|
let padding_name = format!("{}{}", param.name, " ".repeat(max_name_length - param.name.len() + 1));
|
|
snippet_codes.push(format!("\t.{}\t(", padding_name));
|
|
snippet_codes.push(format!("${{{}:{}}}", placeholder_id, param.init));
|
|
placeholder_id += 1;
|
|
snippet_codes.push(format!("{} )", " ".repeat(max_init_length - param.init.len() + 1)));
|
|
|
|
if i < params_length - 1 {
|
|
snippet_codes.push(",\n".to_string());
|
|
}
|
|
i += 1;
|
|
}
|
|
|
|
snippet_codes.push(")\n".to_string());
|
|
}
|
|
|
|
snippet_codes.push(format!("u_{}", module.name));
|
|
|
|
if ports_length > 0 {
|
|
let max_name_length = module.ports.iter().map(|port| port.name.len()).max().unwrap_or(0);
|
|
snippet_codes.push("(\n".to_string());
|
|
let mut i: usize = 0;
|
|
for port in &module.ports {
|
|
let padding_name = format!("{}{}", port.name, " ".repeat(max_name_length - port.name.len() + 1));
|
|
snippet_codes.push(format!("\t.{}\t(", padding_name));
|
|
snippet_codes.push(format!("${{{}:{}}}", placeholder_id, port.name));
|
|
placeholder_id += 1;
|
|
snippet_codes.push(format!("{} )", " ".repeat(max_name_length - port.name.len() + 1)));
|
|
|
|
if i < ports_length - 1 {
|
|
snippet_codes.push(",".to_string());
|
|
}
|
|
snippet_codes.push("\n".to_string());
|
|
|
|
i += 1;
|
|
}
|
|
|
|
snippet_codes.push(");\n".to_string());
|
|
}
|
|
|
|
snippet_codes.join("")
|
|
}
|
|
|
|
/// 自动补全例化模块
|
|
fn make_module_completions(
|
|
server: &LSPServer,
|
|
token: &str,
|
|
language_id: &str
|
|
) -> Vec<CompletionItem> {
|
|
let mut module_completioms = Vec::<CompletionItem>::new();
|
|
let hdl_param = server.srcs.hdl_param.clone();
|
|
|
|
let prefix = token.to_string().to_lowercase();
|
|
let module_name_to_path = hdl_param.module_name_to_path.read().unwrap();
|
|
|
|
// 获取和自动补全相关的配置
|
|
let auto_add_output_declaration = server.srcs.get_lsp_configuration_bool_value("digital-ide.function.lsp.completion.vlog.auto-add-output-declaration").unwrap_or(true);
|
|
|
|
// 遍历 hdlparam 中所有的 modules
|
|
for module_name in module_name_to_path.keys() {
|
|
if !module_name.to_string().to_lowercase().starts_with(&prefix) {
|
|
continue;
|
|
}
|
|
if let Some((module, file_type, def_path)) = hdl_param.find_module_context_by_name(&module_name) {
|
|
let mut insert_text = Vec::<String>::new();
|
|
if auto_add_output_declaration {
|
|
if let Some(declaration_string) = make_output_declaration(&module) {
|
|
insert_text.push(declaration_string);
|
|
}
|
|
}
|
|
|
|
let (insert_text, module_profile, define_info) = if file_type == "primitives" {
|
|
let primitive_map = server.srcs.primitive_text.name_to_text.read().unwrap();
|
|
if let Some(text) = primitive_map.get(&module.name) {
|
|
insert_text.push(make_primitives_instantiation_code(text));
|
|
(
|
|
insert_text.join("\n"),
|
|
make_primitives_module_profile_code(text),
|
|
format!("[Definition] Primitive module: {}", module.name)
|
|
)
|
|
} else {
|
|
continue;
|
|
}
|
|
} else {
|
|
insert_text.push(make_instantiation_code(&module));
|
|
let path_uri = Url::from_file_path(def_path).unwrap().to_string();
|
|
let def_row = module.range.start.line;
|
|
let def_col = module.range.start.character;
|
|
|
|
(
|
|
insert_text.join("\n"),
|
|
make_module_profile_code(&module),
|
|
format!("Go to [Definition]({path_uri}#L{def_row}:{def_col})")
|
|
)
|
|
};
|
|
|
|
let module_profile = MarkupContent {
|
|
kind: MarkupKind::Markdown,
|
|
value: format!("```{}\n{}\n```\n{}", language_id, module_profile, define_info)
|
|
};
|
|
|
|
let detail = format!("module instantiation ({})", file_type);
|
|
|
|
let item = CompletionItem {
|
|
label: module.name.to_string(),
|
|
detail: Some(detail),
|
|
documentation: Some(Documentation::MarkupContent(module_profile)),
|
|
kind: Some(CompletionItemKind::CLASS),
|
|
insert_text: Some(insert_text),
|
|
// 给模块例化自动补全附上最高权重
|
|
sort_text: Some("0001".to_string()),
|
|
insert_text_format: Some(InsertTextFormat::SNIPPET),
|
|
insert_text_mode: Some(InsertTextMode::ADJUST_INDENTATION),
|
|
..CompletionItem::default()
|
|
};
|
|
module_completioms.push(item);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
module_completioms
|
|
}
|
|
|
|
fn make_output_declaration(
|
|
module: &core::hdlparam::Module
|
|
) -> Option<String> {
|
|
let mut output_declaration = Vec::<String>::new();
|
|
for port in &module.ports {
|
|
if port.dir_type == "output" || port.dir_type == "out" {
|
|
let mut declaration = Vec::<String>::new();
|
|
if port.net_type == "reg" || port.net_type == "wire" {
|
|
declaration.push(port.net_type.to_string());
|
|
} else {
|
|
declaration.push("wire".to_string());
|
|
}
|
|
if port.width != "1" {
|
|
declaration.push(port.width.to_string());
|
|
}
|
|
declaration.push(port.name.to_string());
|
|
output_declaration.push(format!("{};", declaration.join(" ")));
|
|
}
|
|
}
|
|
|
|
if output_declaration.len() > 0 {
|
|
let output_declaration = format!("// output declaration of module {}\n{}\n", module.name, output_declaration.join("\n"));
|
|
Some(output_declaration)
|
|
} else {
|
|
None
|
|
}
|
|
} |