Merge pull request #6569 from faldor20/docs

Add docs to completions and hover
This commit is contained in:
Richard Feldman 2024-03-16 11:22:58 -04:00 committed by GitHub
commit 14ba398b5d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 954 additions and 597 deletions

39
crates/lang_srv/TODO.md Normal file
View file

@ -0,0 +1,39 @@
# Improvements to the language server.
## Performance
- [ ] Implement some performance logging for actions like completion goto def hover etc
### Completion
Currently the way we handle documentation and type info for completion requires us to prform all the computation up front and has no caching. Documentation is also quite inneficient and likely requires a lot of repeated computation which could be slow in files with lots of doc comments. The language server allows us to defer getting the info for a completion until the item is actually selected in the editor, this could speed up completion requests.
We would need to profile this to see how performant it really is.
## Features
- [ ] Rename refactoring #HighPriority
- [ ] Show references #HighPriority
Initially this could just be within the current file and it could be expanded to multi file
Should have a lot in commmon with rename refactoring
- [ ] Completion within the import section
### Code Actions
- [ ] Create cases of when is block
- [ ] Destructure record
- [ ] Extract selection into it's own function (This one seems hard)
- [ ] Add function to exposed list
### Completion
- [ ] Completion of Tags #HighPriority
- [ ] Completion of Types inside signatures
- [ ] Completion of when is cases
- [ ] Completion of record fields
- [ ] During destructuring
- [ ] When creating records
- [ ] When describing records inside function params
- [ ] Completion of unimported vars that are exposed by modules within the project (will need to have appropriate indicator and ranking so as not to be annoying)

View file

@ -8,8 +8,8 @@ use bumpalo::Bump;
use parking_lot::Mutex;
use roc_can::{abilities::AbilitiesStore, expr::Declarations};
use roc_collections::{MutMap, MutSet};
use roc_load::{CheckedModule, LoadedModule};
use roc_collections::{MutMap, MutSet, VecMap};
use roc_load::{docs::ModuleDocumentation, CheckedModule, LoadedModule};
use roc_module::symbol::{Interns, ModuleId, Symbol};
use roc_packaging::cache::{self, RocCacheDir};
use roc_region::all::LineInfo;
@ -38,22 +38,22 @@ pub const HIGHLIGHT_TOKENS_LEGEND: &[SemanticTokenType] = Token::LEGEND;
pub(super) struct ModulesInfo {
subs: Mutex<HashMap<ModuleId, Subs>>,
exposed: HashMap<ModuleId, Arc<Vec<(Symbol, Variable)>>>,
docs: VecMap<ModuleId, ModuleDocumentation>,
}
impl ModulesInfo {
/// Apply function to subs
fn with_subs<F, A>(&self, mod_id: &ModuleId, f: F) -> Option<A>
where
F: FnOnce(&mut Subs) -> A,
{
self.subs.lock().get_mut(mod_id).map(f)
}
/// Transforms some of the raw data from the analysis into a state that is
/// more useful during processes like completion.
fn from_analysis(
exposes: MutMap<ModuleId, Vec<(Symbol, Variable)>>,
typechecked: &MutMap<ModuleId, CheckedModule>,
docs_by_module: VecMap<ModuleId, ModuleDocumentation>,
) -> ModulesInfo {
// We wrap this in Arc because later we will go through each module's imports and
// store the full list of symbols that each imported module exposes.
@ -76,6 +76,7 @@ impl ModulesInfo {
ModulesInfo {
subs: all_subs,
exposed,
docs: docs_by_module,
}
}
}
@ -149,9 +150,10 @@ pub(crate) fn global_analysis(doc_info: DocInfo) -> Vec<AnalyzedDocument> {
mut typechecked,
solved,
abilities_store,
mut imports,
exposed_imports,
mut imports,
exposes,
docs_by_module,
..
} = module;
@ -162,7 +164,11 @@ pub(crate) fn global_analysis(doc_info: DocInfo) -> Vec<AnalyzedDocument> {
let exposed_imports = resolve_exposed_imports(exposed_imports, &exposes);
let modules_info = Arc::new(ModulesInfo::from_analysis(exposes, &typechecked));
let modules_info = Arc::new(ModulesInfo::from_analysis(
exposes,
&typechecked,
docs_by_module,
));
let mut builder = AnalyzedDocumentBuilder {
interns: &interns,

View file

@ -169,19 +169,33 @@ impl AnalyzedDocument {
declarations,
module_id,
interns,
modules_info,
..
} = self.module()?;
let (region, var) = roc_can::traverse::find_closest_type_at(pos, declarations)?;
//TODO:Can this be integrated into find closest type? is it even worth it?
let docs_opt = self
.symbol_at(position)
.and_then(|symb| modules_info.docs.get(module_id)?.get_doc_for_symbol(&symb));
let type_str = format_var_type(var, &mut subs.clone(), module_id, interns);
let range = region.to_range(self.line_info());
let type_content = MarkedString::LanguageString(LanguageString {
language: "roc".to_string(),
value: type_str,
});
let content = vec![Some(type_content), docs_opt.map(MarkedString::String)]
.into_iter()
.flatten()
.collect::<Vec<_>>();
Some(Hover {
contents: HoverContents::Scalar(MarkedString::LanguageString(LanguageString {
language: "roc".to_string(),
value: type_str,
})),
contents: HoverContents::Array(content),
range: Some(range),
})
}
@ -285,6 +299,7 @@ impl AnalyzedDocument {
&mut subs.clone(),
module_id,
interns,
modules_info.docs.get(module_id),
exposed_imports,
);
Some(completions)

View file

@ -1,254 +1,23 @@
use std::{collections::HashMap, sync::Arc};
use log::{debug, trace, warn};
use log::{debug, warn};
use roc_can::{
def::Def,
expr::{ClosureData, Declarations, Expr, WhenBranch},
pattern::{ListPatterns, Pattern, RecordDestruct, TupleDestruct},
traverse::{walk_decl, walk_def, walk_expr, DeclarationInfo, Visitor},
};
use roc_can::{expr::Declarations, traverse::Visitor};
use roc_collections::MutMap;
use roc_load::docs::{DocDef, ModuleDocumentation};
use roc_module::symbol::{Interns, ModuleId, Symbol};
use roc_region::all::{Loc, Position, Region};
use roc_region::all::Position;
use roc_types::{
subs::{Subs, Variable},
types::Alias,
};
use tower_lsp::lsp_types::{CompletionItem, CompletionItemKind};
use tower_lsp::lsp_types::{self, CompletionItem, CompletionItemKind};
use self::visitor::CompletionVisitor;
use super::{utils::format_var_type, ModulesInfo};
mod formatting;
pub struct CompletionVisitor<'a> {
position: Position,
found_decls: Vec<(Symbol, Variable)>,
pub interns: &'a Interns,
pub prefix: String,
}
impl Visitor for CompletionVisitor<'_> {
fn should_visit(&mut self, region: Region) -> bool {
region.contains_pos(self.position)
}
fn visit_expr(&mut self, expr: &Expr, region: Region, var: Variable) {
if region.contains_pos(self.position) {
let mut res = self.expression_defs(expr);
self.found_decls.append(&mut res);
walk_expr(self, expr, var);
}
}
fn visit_decl(&mut self, decl: DeclarationInfo<'_>) {
match decl {
DeclarationInfo::Value { loc_expr, .. }
| DeclarationInfo::Function {
loc_body: loc_expr, ..
}
| DeclarationInfo::Destructure { loc_expr, .. } => {
let res = self.decl_to_completion_item(&decl);
self.found_decls.extend(res);
if loc_expr.region.contains_pos(self.position) {
walk_decl(self, decl);
};
}
_ => {
walk_decl(self, decl);
}
}
}
fn visit_def(&mut self, def: &Def) {
let res = self.extract_defs(def);
self.found_decls.extend(res);
walk_def(self, def);
}
}
impl CompletionVisitor<'_> {
fn extract_defs(&mut self, def: &Def) -> Vec<(Symbol, Variable)> {
trace!("Completion begin");
def.pattern_vars
.iter()
.map(|(symbol, var)| (*symbol, *var))
.collect()
}
fn expression_defs(&self, expr: &Expr) -> Vec<(Symbol, Variable)> {
match expr {
Expr::When {
expr_var, branches, ..
} => self.when_is_expr(branches, expr_var),
Expr::Closure(ClosureData {
arguments,
loc_body,
..
}) => {
//if we are inside the closure complete it's vars
if loc_body.region.contains_pos(self.position) {
arguments
.iter()
.flat_map(|(var, _, pat)| self.patterns(&pat.value, var))
.collect()
} else {
vec![]
}
}
_ => vec![],
}
}
///Extract any variables made available by the branch of a when_is expression that contains `self.position`
fn when_is_expr(
&self,
branches: &[WhenBranch],
expr_var: &Variable,
) -> Vec<(Symbol, Variable)> {
branches
.iter()
.flat_map(
|WhenBranch {
patterns, value, ..
}| {
if value.region.contains_pos(self.position) {
patterns
.iter()
.flat_map(|pattern| self.patterns(&pattern.pattern.value, expr_var))
.collect()
} else {
vec![]
}
},
)
.collect()
}
fn record_destructure(&self, destructs: &[Loc<RecordDestruct>]) -> Vec<(Symbol, Variable)> {
destructs
.iter()
.flat_map(|a| match &a.value.typ {
roc_can::pattern::DestructType::Required
| roc_can::pattern::DestructType::Optional(_, _) => {
vec![(a.value.symbol, a.value.var)]
}
roc_can::pattern::DestructType::Guard(var, pat) => self.patterns(&pat.value, var),
})
.collect()
}
fn tuple_destructure(&self, destructs: &[Loc<TupleDestruct>]) -> Vec<(Symbol, Variable)> {
destructs
.iter()
.flat_map(|a| {
let (var, pattern) = &a.value.typ;
self.patterns(&pattern.value, var)
})
.collect()
}
fn list_pattern(&self, list_elems: &ListPatterns, var: &Variable) -> Vec<(Symbol, Variable)> {
list_elems
.patterns
.iter()
.flat_map(|a| self.patterns(&a.value, var))
.collect()
}
fn tag_pattern(&self, arguments: &[(Variable, Loc<Pattern>)]) -> Vec<(Symbol, Variable)> {
arguments
.iter()
.flat_map(|(var, pat)| self.patterns(&pat.value, var))
.collect()
}
fn as_pattern(
&self,
as_pat: &Pattern,
as_symbol: Symbol,
var: &Variable,
) -> Vec<(Symbol, Variable)> {
//Get the variables introduced within the pattern
let mut patterns = self.patterns(as_pat, var);
//Add the "as" that wraps the whole pattern
patterns.push((as_symbol, *var));
patterns
}
///Returns a list of symbols defined by this pattern.
///`pattern_var`: Variable type of the entire pattern. This will be returned if the pattern turns out to be an identifier
fn patterns(
&self,
pattern: &roc_can::pattern::Pattern,
pattern_var: &Variable,
) -> Vec<(Symbol, Variable)> {
match pattern {
roc_can::pattern::Pattern::Identifier(symbol) => {
if self.is_match(symbol) {
vec![(*symbol, *pattern_var)]
} else {
vec![]
}
}
Pattern::AppliedTag { arguments, .. } => self.tag_pattern(arguments),
Pattern::UnwrappedOpaque { argument, .. } => {
self.patterns(&argument.1.value, &argument.0)
}
Pattern::List {
elem_var, patterns, ..
} => self.list_pattern(patterns, elem_var),
roc_can::pattern::Pattern::As(pat, symbol) => {
self.as_pattern(&pat.value, *symbol, pattern_var)
}
roc_can::pattern::Pattern::RecordDestructure { destructs, .. } => {
self.record_destructure(destructs)
}
roc_can::pattern::Pattern::TupleDestructure { destructs, .. } => {
self.tuple_destructure(destructs)
}
_ => vec![],
}
}
fn is_match(&self, symbol: &Symbol) -> bool {
symbol.as_str(self.interns).starts_with(&self.prefix)
}
fn decl_to_completion_item(&self, decl: &DeclarationInfo) -> Vec<(Symbol, Variable)> {
match decl {
DeclarationInfo::Value {
expr_var, pattern, ..
} => self.patterns(pattern, expr_var),
DeclarationInfo::Function {
expr_var,
pattern,
function,
loc_body,
..
} => {
let mut out = vec![];
//Append the function declaration itself for recursive calls
out.extend(self.patterns(pattern, expr_var));
if loc_body.region.contains_pos(self.position) {
//also add the arguments if we are inside the function
let args = function
.value
.arguments
.iter()
.flat_map(|(var, _, pat)| self.patterns(&pat.value, var));
//We add in the pattern for the function declaration
out.extend(args);
trace!("Added function args to completion output =:{:#?}", out);
}
out
}
DeclarationInfo::Destructure {
loc_pattern,
expr_var,
..
} => self.patterns(&loc_pattern.value, expr_var),
DeclarationInfo::Expectation { .. } => vec![],
}
}
}
mod visitor;
fn get_completions(
position: Position,
@ -258,49 +27,17 @@ fn get_completions(
) -> Vec<(Symbol, Variable)> {
let mut visitor = CompletionVisitor {
position,
found_decls: Vec::new(),
found_declarations: Vec::new(),
interns,
prefix,
};
visitor.visit_decls(decls);
visitor.found_decls
}
fn make_completion_item(
subs: &mut Subs,
module_id: &ModuleId,
interns: &Interns,
str: String,
var: Variable,
) -> CompletionItem {
let type_str = format_var_type(var, subs, module_id, interns);
let typ = match subs.get(var).content {
roc_types::subs::Content::Structure(var) => match var {
roc_types::subs::FlatType::Apply(_, _) => CompletionItemKind::FUNCTION,
roc_types::subs::FlatType::Func(_, _, _) => CompletionItemKind::FUNCTION,
roc_types::subs::FlatType::EmptyTagUnion
| roc_types::subs::FlatType::TagUnion(_, _) => CompletionItemKind::ENUM,
_ => CompletionItemKind::VARIABLE,
},
a => {
debug!(
"No specific completionKind for variable type: {:?} defaulting to 'Variable'",
a
);
CompletionItemKind::VARIABLE
}
};
CompletionItem {
label: str,
detail: Some(type_str),
kind: Some(typ),
..Default::default()
}
visitor.found_declarations
}
#[allow(clippy::too_many_arguments)]
/// Walks through declarations that would be accessible from the provided
/// position, adding them to a list of completion items until all accessible
/// position adding them to a list of completion items until all accessible
/// declarations have been fully explored.
pub fn get_completion_items(
position: Position,
@ -309,22 +46,13 @@ pub fn get_completion_items(
subs: &mut Subs,
module_id: &ModuleId,
interns: &Interns,
docs: Option<&ModuleDocumentation>,
exposed_imports: &[(Symbol, Variable)],
) -> Vec<CompletionItem> {
let mut completions = get_completions(position, decls, prefix, interns);
completions.extend(exposed_imports);
debug!("extended with:{:#?}", exposed_imports);
make_completion_items(
subs,
module_id,
interns,
completions
.into_iter()
.map(|(symb, var)| (symb.as_str(interns).to_string(), var))
.collect(),
)
make_completion_items(subs, module_id, interns, docs, completions)
}
pub(super) fn get_module_completion_items(
@ -339,6 +67,7 @@ pub(super) fn get_module_completion_items(
.flat_map(|(mod_id, exposed_symbols)| {
let mod_name = mod_id.to_ident_str(interns).to_string();
// Completion for modules themselves
if mod_name.starts_with(&prefix) {
let item = CompletionItem {
label: mod_name.clone(),
@ -348,15 +77,22 @@ pub(super) fn get_module_completion_items(
mod_id,
interns,
exposed_symbols,
modules_info.docs.get(mod_id),
modules_info,
)),
..Default::default()
};
vec![item]
// Complete dot completions
vec![item]
// Complete dot completions for module exports
} else if prefix.starts_with(&(mod_name + ".")) {
get_module_exposed_completion(exposed_symbols, modules_info, mod_id, interns)
get_module_exposed_completion(
exposed_symbols,
modules_info,
mod_id,
modules_info.docs.get(mod_id),
interns,
)
} else {
vec![]
}
@ -373,20 +109,26 @@ fn get_module_exposed_completion(
exposed_symbols: &[(Symbol, Variable)],
modules_info: &ModulesInfo,
mod_id: &ModuleId,
docs: Option<&ModuleDocumentation>,
interns: &Interns,
) -> Vec<CompletionItem> {
let mut completion_docs = docs.map_or(Default::default(), |docs| {
get_completion_docs(exposed_symbols, docs)
});
exposed_symbols
.iter()
.map(|(sym, var)| {
.map(|(symbol, var)| {
// We need to fetch the subs for the module that is exposing what we
// are trying to complete, because that will have the type info we need.
// are trying to complete because that will have the type info we need.
modules_info
.with_subs(mod_id, |subs| {
make_completion_item(
subs,
mod_id,
interns,
sym.as_str(interns).to_string(),
completion_docs.remove(symbol),
symbol.as_str(interns).to_string(),
*var,
)
})
@ -395,8 +137,38 @@ fn get_module_exposed_completion(
.collect::<Vec<_>>()
}
/// Efficiently walks the list of docs collecting the docs for completions as we go.
/// Should be faster than re-walking for every completion.
fn get_completion_docs(
completions: &[(Symbol, Variable)],
docs: &ModuleDocumentation,
) -> HashMap<Symbol, String> {
let mut symbols = completions
.iter()
.map(|(symbol, _)| symbol)
.collect::<Vec<_>>();
docs.entries
.iter()
.filter_map(|doc| match doc {
roc_load::docs::DocEntry::DocDef(DocDef { docs, symbol, .. }) => {
let docs_str = docs.as_ref().map(|str| str.trim().to_string())?;
let (index, _symbol) = symbols
.iter()
.enumerate()
.find(|(_index, symb)| symb == &&symbol)?;
symbols.swap_remove(index);
Some((*symbol, docs_str))
}
_ => None,
})
.collect()
}
/// Provides a list of completions for Type aliases within the scope.
/// TODO: Use this when we know we are within a type definition.
///TODO: Use this when we know we are within a type definition
fn _alias_completions(
aliases: &MutMap<Symbol, (bool, Alias)>,
module_id: &ModuleId,
@ -407,17 +179,43 @@ fn _alias_completions(
.filter(|(symbol, (_exposed, _alias))| &symbol.module_id() == module_id)
.map(|(symbol, (_exposed, _alias))| {
let name = symbol.as_str(interns).to_string();
CompletionItem {
label: name.clone(),
detail: Some(name + " we don't know how to print types."),
detail: Some(name + "we don't know how to print types "),
kind: Some(CompletionItemKind::CLASS),
..Default::default()
}
})
.collect()
}
fn make_completion_items(
subs: &mut Subs,
module_id: &ModuleId,
interns: &Interns,
docs: Option<&ModuleDocumentation>,
completions: Vec<(Symbol, Variable)>,
) -> Vec<CompletionItem> {
let mut completion_docs = docs.map_or(Default::default(), |mod_docs| {
get_completion_docs(&completions, mod_docs)
});
completions
.into_iter()
.map(|(symbol, var)| {
make_completion_item(
subs,
module_id,
interns,
completion_docs.remove(&symbol),
symbol.as_str(interns).to_string(),
var,
)
})
.collect()
}
fn make_completion_items_string(
subs: &mut Subs,
module_id: &ModuleId,
interns: &Interns,
@ -425,12 +223,60 @@ fn make_completion_items(
) -> Vec<CompletionItem> {
completions
.into_iter()
.map(|(symbol, var)| make_completion_item(subs, module_id, interns, symbol, var))
.map(|(symbol, var)| make_completion_item(subs, module_id, interns, None, symbol, var))
.collect()
}
///Finds the types of and names of all the fields of a record
///`var` should be a `Variable` that you know is a record's type or else it will return an empty list
fn make_completion_item(
subs: &mut Subs,
module_id: &ModuleId,
interns: &Interns,
docs_opt: Option<String>,
symbol_str: String,
var: Variable,
) -> CompletionItem {
let type_str = format_var_type(var, subs, module_id, interns);
let typ = match subs.get(var).content {
roc_types::subs::Content::Structure(var) => match var {
roc_types::subs::FlatType::Apply(_, _) => CompletionItemKind::FUNCTION,
roc_types::subs::FlatType::Func(_, _, _) => CompletionItemKind::FUNCTION,
roc_types::subs::FlatType::EmptyTagUnion
| roc_types::subs::FlatType::TagUnion(_, _) => CompletionItemKind::ENUM,
_ => CompletionItemKind::VARIABLE,
},
other => {
debug!(
"No specific completionKind for variable type: {:?} defaulting to 'Variable'",
other
);
CompletionItemKind::VARIABLE
}
};
CompletionItem {
label: symbol_str,
detail: Some(type_str),
kind: Some(typ),
documentation: docs_opt.map(|docs| {
lsp_types::Documentation::MarkupContent(lsp_types::MarkupContent {
kind: lsp_types::MarkupKind::Markdown,
value: docs,
})
}),
..Default::default()
}
}
/// E.g. a.b.c.d->{variable_name:"a", field:"d", middle_fields:["b","c"]}
struct RecFieldCompletion {
/// name of variable that is a record
variable_name: String,
field: String,
middle_fields: Vec<String>,
}
/// Finds the types of and names of all the fields of a record.
/// `var` should be a `Variable` that you know is of type record or else it will return an empty list.
fn find_record_fields(var: Variable, subs: &mut Subs) -> Vec<(String, Variable)> {
let content = subs.get(var);
match content.content {
@ -484,22 +330,17 @@ fn find_record_fields(var: Variable, subs: &mut Subs) -> Vec<(String, Variable)>
}
}
struct FieldCompletion {
var: String,
field: String,
middle_fields: Vec<String>,
}
///Splits a completion prefix for a field into its components
///E.g. a.b.c.d->{var:"a",middle_fields:["b","c"],field:"d"}
fn get_field_completion_parts(symbol_prefix: &str) -> Option<FieldCompletion> {
/// Splits a completion prefix for a field into its components.
/// E.g. a.b.c.d->{variable_name:"a",middle_fields:["b","c"],field:"d"}
fn get_field_completion_parts(symbol_prefix: &str) -> Option<RecFieldCompletion> {
let mut parts = symbol_prefix.split('.').collect::<Vec<_>>();
let field = parts.pop().unwrap_or("").to_string();
let var = parts.remove(0);
//Now that we have the head and tail removed this is all the intermediate fields
let variable_name = parts.remove(0).to_string();
// Now that we have the head and tail removed this is all the intermediate fields.
let middle_fields = parts.into_iter().map(ToString::to_string).collect();
Some(FieldCompletion {
var: var.to_string(),
Some(RecFieldCompletion {
variable_name,
field,
middle_fields,
})
@ -512,25 +353,30 @@ pub fn field_completion(
subs: &mut Subs,
module_id: &ModuleId,
) -> Option<Vec<CompletionItem>> {
let FieldCompletion {
var,
let RecFieldCompletion {
variable_name,
field,
middle_fields,
} = get_field_completion_parts(&symbol_prefix)?;
debug!(
"Getting record field completions: variable: {:?} field: {:?} middle: {:?} ",
var, field, middle_fields
variable_name, field, middle_fields
);
let completion = get_completions(position, declarations, var.to_string(), interns)
// We get completions here, but all we really want is the info about the variable that
// is the first part of our record completion.
// We are completing the full name of the variable so we should only have one match.
let completion = get_completions(position, declarations, variable_name, interns)
.into_iter()
.map(|a| (a.0.as_str(interns).to_string(), a.1))
.map(|(symbol, var)| (symbol.as_str(interns).to_string(), var))
.next()?;
//If we have a type that has nested records we could have a completion prefix like: "var.field1.field2.fi"
//If the document isn't fully typechecked we won't know what the type of field2 is for us to offer completions based on it's fields
//Instead we get the type of "var" and then the type of "field1" within var's type and then "field2" within field1's type etc etc, until we have the type of the record we are actually looking for field completions for.
// If we have a type that has nested records we could have a completion prefix like: "var.field1.field2.fi".
// If the document isn't fully typechecked we won't know what the type of field2 is for us to offer
// completions based on it's fields. Instead we get the type of "var" and then the type of "field1" within
// var's type and then "field2" within field1's type etc etc, until we have the type of the record we are
// actually looking for field completions for.
let completion_record = middle_fields.iter().fold(completion, |state, chain_field| {
let fields_vars = find_record_fields(state.1, subs);
fields_vars
@ -544,6 +390,8 @@ pub fn field_completion(
.filter(|(str, _)| str.starts_with(&field.to_string()))
.collect();
let field_completions = make_completion_items(subs, module_id, interns, field_completions);
let field_completions =
make_completion_items_string(subs, module_id, interns, field_completions);
Some(field_completions)
}

View file

@ -1,3 +1,4 @@
use roc_load::docs::ModuleDocumentation;
use roc_module::symbol::{Interns, ModuleId, Symbol};
use roc_types::subs::Variable;
@ -40,12 +41,26 @@ pub(super) fn module_documentation(
module_id: &ModuleId,
interns: &Interns,
exposed: &[(Symbol, Variable)],
module_docs: Option<&ModuleDocumentation>,
modules_info: &ModulesInfo,
) -> Documentation {
let exposed_string =
get_module_exposed_list(module_id, interns, modules_info, exposed).unwrap_or_default();
let module_doc = module_docs
.and_then(|docs| {
docs.entries.first().and_then(|first_doc| match first_doc {
roc_load::docs::DocEntry::ModuleDoc(str) => Some(str.clone().trim().to_string()),
_ => None,
})
})
.unwrap_or_default();
match description_type {
DescriptionsType::Exposes => md_doc(format!("```roc\n{0}\n```", exposed_string)),
DescriptionsType::Exposes => md_doc(format!(
"{0}```roc\n{1}\n```",
module_doc + "\n",
exposed_string
)),
}
}

View file

@ -0,0 +1,254 @@
use log::trace;
use roc_can::{
def::Def,
expr::{ClosureData, Expr, WhenBranch},
pattern::{ListPatterns, Pattern, RecordDestruct, TupleDestruct},
traverse::{walk_decl, walk_def, walk_expr, DeclarationInfo, Visitor},
};
use roc_module::symbol::{Interns, Symbol};
use roc_region::all::{Loc, Position, Region};
use roc_types::subs::Variable;
pub(crate) struct CompletionVisitor<'a> {
pub(crate) position: Position,
pub(crate) found_declarations: Vec<(Symbol, Variable)>,
pub(crate) interns: &'a Interns,
pub(crate) prefix: String,
}
impl Visitor for CompletionVisitor<'_> {
fn should_visit(&mut self, region: Region) -> bool {
region.contains_pos(self.position)
}
fn visit_expr(&mut self, expr: &Expr, region: Region, var: Variable) {
if region.contains_pos(self.position) {
let mut res = self.expression_defs(expr);
self.found_declarations.append(&mut res);
walk_expr(self, expr, var);
}
}
fn visit_decl(&mut self, decl: DeclarationInfo<'_>) {
match decl {
DeclarationInfo::Value { loc_expr, .. }
| DeclarationInfo::Function {
loc_body: loc_expr, ..
}
| DeclarationInfo::Destructure { loc_expr, .. } => {
let res = self.decl_to_completion_item(&decl);
self.found_declarations.extend(res);
if loc_expr.region.contains_pos(self.position) {
walk_decl(self, decl);
};
}
_ => {
walk_decl(self, decl);
}
}
}
fn visit_def(&mut self, def: &Def) {
let sym_var_vec = self.extract_defs(def);
self.found_declarations.extend(sym_var_vec);
walk_def(self, def);
}
}
impl CompletionVisitor<'_> {
fn extract_defs(&mut self, def: &Def) -> Vec<(Symbol, Variable)> {
trace!("Completion begin");
def.pattern_vars
.iter()
.map(|(symbol, var)| (*symbol, *var))
.collect()
}
fn expression_defs(&self, expr: &Expr) -> Vec<(Symbol, Variable)> {
match expr {
Expr::When {
expr_var, branches, ..
} => self.when_is_expr(branches, expr_var),
Expr::Closure(ClosureData {
arguments,
loc_body,
..
}) => {
// if we are inside the closure complete it's vars
if loc_body.region.contains_pos(self.position) {
arguments
.iter()
.flat_map(|(var, _, pat)| self.patterns(&pat.value, var))
.collect()
} else {
vec![]
}
}
_ => vec![],
}
}
/// Extract any variables made available by the branch of a when_is expression that contains `self.position`
fn when_is_expr(
&self,
branches: &[WhenBranch],
expr_var: &Variable,
) -> Vec<(Symbol, Variable)> {
branches
.iter()
.flat_map(
|WhenBranch {
patterns, value, ..
}| {
if value.region.contains_pos(self.position) {
patterns
.iter()
.flat_map(|pattern| self.patterns(&pattern.pattern.value, expr_var))
.collect()
} else {
vec![]
}
},
)
.collect()
}
fn record_destructure(&self, destructs: &[Loc<RecordDestruct>]) -> Vec<(Symbol, Variable)> {
destructs
.iter()
.flat_map(|loc| match &loc.value.typ {
roc_can::pattern::DestructType::Required
| roc_can::pattern::DestructType::Optional(_, _) => {
vec![(loc.value.symbol, loc.value.var)]
}
roc_can::pattern::DestructType::Guard(var, pat) => self.patterns(&pat.value, var),
})
.collect()
}
fn tuple_destructure(&self, destructs: &[Loc<TupleDestruct>]) -> Vec<(Symbol, Variable)> {
destructs
.iter()
.flat_map(|loc| {
let (var, pattern) = &loc.value.typ;
self.patterns(&pattern.value, var)
})
.collect()
}
fn list_pattern(&self, list_elems: &ListPatterns, var: &Variable) -> Vec<(Symbol, Variable)> {
list_elems
.patterns
.iter()
.flat_map(|loc| self.patterns(&loc.value, var))
.collect()
}
fn tag_pattern(&self, arguments: &[(Variable, Loc<Pattern>)]) -> Vec<(Symbol, Variable)> {
arguments
.iter()
.flat_map(|(var, pat)| self.patterns(&pat.value, var))
.collect()
}
fn as_pattern(
&self,
as_pat: &Pattern,
as_symbol: Symbol,
var: &Variable,
) -> Vec<(Symbol, Variable)> {
// get the variables introduced within the pattern
let mut patterns = self.patterns(as_pat, var);
// add the "as" that wraps the whole pattern
patterns.push((as_symbol, *var));
patterns
}
/// Returns a list of symbols defined by this pattern.
/// `pattern_var`: Variable type of the entire pattern. This will be returned if
/// the pattern turns out to be an identifier.
fn patterns(
&self,
pattern: &roc_can::pattern::Pattern,
pattern_var: &Variable,
) -> Vec<(Symbol, Variable)> {
match pattern {
roc_can::pattern::Pattern::Identifier(symbol) => {
if self.is_match(symbol) {
vec![(*symbol, *pattern_var)]
} else {
vec![]
}
}
Pattern::AppliedTag { arguments, .. } => self.tag_pattern(arguments),
Pattern::UnwrappedOpaque { argument, .. } => {
self.patterns(&argument.1.value, &argument.0)
}
Pattern::List {
elem_var, patterns, ..
} => self.list_pattern(patterns, elem_var),
roc_can::pattern::Pattern::As(pat, symbol) => {
self.as_pattern(&pat.value, *symbol, pattern_var)
}
roc_can::pattern::Pattern::RecordDestructure { destructs, .. } => {
self.record_destructure(destructs)
}
roc_can::pattern::Pattern::TupleDestructure { destructs, .. } => {
self.tuple_destructure(destructs)
}
_ => vec![],
}
}
fn is_match(&self, symbol: &Symbol) -> bool {
symbol.as_str(self.interns).starts_with(&self.prefix)
}
fn decl_to_completion_item(&self, decl: &DeclarationInfo) -> Vec<(Symbol, Variable)> {
match decl {
DeclarationInfo::Value {
expr_var, pattern, ..
} => self.patterns(pattern, expr_var),
DeclarationInfo::Function {
expr_var,
pattern,
function,
loc_body,
..
} => {
let mut sym_var_vec = vec![];
// append the function declaration itself for recursive calls
sym_var_vec.extend(self.patterns(pattern, expr_var));
if loc_body.region.contains_pos(self.position) {
// also add the arguments if we are inside the function
let args = function
.value
.arguments
.iter()
.flat_map(|(var, _, pat)| self.patterns(&pat.value, var));
// we add in the pattern for the function declaration
sym_var_vec.extend(args);
trace!(
"Added function args to completion output =:{:#?}",
sym_var_vec
);
}
sym_var_vec
}
DeclarationInfo::Destructure {
loc_pattern,
expr_var,
..
} => self.patterns(&loc_pattern.value, expr_var),
DeclarationInfo::Expectation { .. } => vec![],
}
}
}

View file

@ -354,24 +354,33 @@ mod tests {
use super::*;
fn completion_resp_to_labels(resp: CompletionResponse) -> Vec<String> {
fn completion_resp_to_strings(
resp: CompletionResponse,
) -> Vec<(String, Option<Documentation>)> {
match resp {
CompletionResponse::Array(list) => list.into_iter(),
CompletionResponse::List(list) => list.items.into_iter(),
}
.map(|item| item.label)
.map(|item| (item.label, item.documentation))
.collect::<Vec<_>>()
}
/// Gets completion and returns only the label for each completion
async fn get_completion_labels(
/// gets completion and returns only the label and docs for each completion
async fn get_basic_completion_info(
reg: &Registry,
url: &Url,
position: Position,
) -> Option<Vec<String>> {
) -> Option<Vec<(String, Option<Documentation>)>> {
reg.completion_items(url, position)
.await
.map(completion_resp_to_labels)
.map(completion_resp_to_strings)
}
/// gets completion and returns only the label for each completion
fn comp_labels(
completions: Option<Vec<(String, Option<Documentation>)>>,
) -> Option<Vec<String>> {
completions.map(|list| list.into_iter().map(|(labels, _)| labels).collect())
}
const DOC_LIT: &str = indoc! {r#"
@ -403,7 +412,7 @@ mod tests {
initial: &str,
addition: &str,
position: Position,
) -> Option<std::vec::Vec<std::string::String>> {
) -> Option<Vec<(String, Option<Documentation>)>> {
let doc = DOC_LIT.to_string() + initial;
let (inner, url) = test_setup(doc.clone()).await;
let registry = &inner.registry;
@ -413,10 +422,18 @@ mod tests {
inner.change(&url, change, 1).await.unwrap();
get_completion_labels(registry, &url, position).await
get_basic_completion_info(registry, &url, position).await
}
/// Tests that completion works properly when we apply an "as" pattern to an identifier.
async fn completion_test_labels(
initial: &str,
addition: &str,
position: Position,
) -> Option<Vec<String>> {
comp_labels(completion_test(initial, addition, position).await)
}
/// Test that completion works properly when we apply an "as" pattern to an identifier
#[tokio::test]
async fn test_completion_as_identifier() {
let suffix = DOC_LIT.to_string()
@ -428,15 +445,15 @@ mod tests {
let (inner, url) = test_setup(suffix.clone()).await;
let position = Position::new(6, 7);
let reg = &inner.registry;
let registry = &inner.registry;
let change = suffix.clone() + "o";
inner.change(&url, change, 1).await.unwrap();
let comp1 = get_completion_labels(reg, &url, position).await;
let comp1 = comp_labels(get_basic_completion_info(registry, &url, position).await);
let c = suffix.clone() + "i";
inner.change(&url, c, 2).await.unwrap();
let comp2 = get_completion_labels(reg, &url, position).await;
let comp2 = comp_labels(get_basic_completion_info(registry, &url, position).await);
let actual = [comp1, comp2];
@ -474,11 +491,11 @@ mod tests {
let change = doc.clone() + "o";
inner.change(&url, change, 1).await.unwrap();
let comp1 = get_completion_labels(reg, &url, position).await;
let comp1 = comp_labels(get_basic_completion_info(reg, &url, position).await);
let c = doc.clone() + "t";
inner.change(&url, c, 2).await.unwrap();
let comp2 = get_completion_labels(reg, &url, position).await;
let comp2 = comp_labels(get_basic_completion_info(reg, &url, position).await);
let actual = [comp1, comp2];
expect![[r#"
@ -502,10 +519,10 @@ mod tests {
.assert_debug_eq(&actual);
}
/// Tests that completion of function args in scope works properly.
/// Test that completion works properly when we apply an "as" pattern to a record
#[tokio::test]
async fn test_completion_fun_params() {
let actual = completion_test(
let actual = completion_test_labels(
indoc! {r"
main = \param1, param2 ->
"},
@ -526,10 +543,10 @@ mod tests {
}
#[tokio::test]
async fn test_completion_fun_params_map() {
let actual = completion_test(
async fn test_completion_closure() {
let actual = completion_test_labels(
indoc! {r"
main = [] |> List.map \param1 , param2->
main = [] |> List.map \ param1 , param2->
"},
"par",
Position::new(4, 3),
@ -545,4 +562,36 @@ mod tests {
"#]]
.assert_debug_eq(&actual);
}
#[tokio::test]
async fn test_completion_with_docs() {
let actual = completion_test(
indoc! {r"
## This is the main function
main = mai
"},
"par",
Position::new(4, 10),
)
.await;
expect![[r#"
Some(
[
(
"main",
Some(
MarkupContent(
MarkupContent {
kind: Markdown,
value: "This is the main function",
},
),
),
),
],
)
"#]]
.assert_debug_eq(&actual);
}
}