scope-based copmletions on original file

This commit is contained in:
Aleksey Kladov 2018-12-22 00:52:02 +03:00
parent 2136e75c0b
commit ccca5aae43
5 changed files with 223 additions and 322 deletions

View file

@ -1,10 +1,10 @@
mod completion_item;
mod reference_completion;
mod complete_fn_param;
mod complete_keyword;
mod complete_snippet;
mod complete_path;
mod complete_scope;
use ra_editor::find_node_at_offset;
use ra_text_edit::AtomTextEdit;
@ -33,26 +33,16 @@ pub(crate) fn completions(
position: FilePosition,
) -> Cancelable<Option<Completions>> {
let original_file = db.source_file(position.file_id);
// Insert a fake ident to get a valid parse tree
let file = {
let edit = AtomTextEdit::insert(position.offset, "intellijRulezz".to_string());
original_file.reparse(&edit)
};
let module = ctry!(source_binder::module_from_position(db, position)?);
let ctx = ctry!(SyntaxContext::new(db, &original_file, position)?);
let mut acc = Completions::default();
// First, let's try to complete a reference to some declaration.
if let Some(name_ref) = find_node_at_offset::<ast::NameRef>(file.syntax(), position.offset) {
reference_completion::completions(&mut acc, db, &module, &file, name_ref)?;
}
let ctx = ctry!(SyntaxContext::new(db, &original_file, position)?);
complete_fn_param::complete_fn_param(&mut acc, &ctx);
complete_keyword::complete_expr_keyword(&mut acc, &ctx);
complete_snippet::complete_expr_snippet(&mut acc, &ctx);
complete_snippet::complete_item_snippet(&mut acc, &ctx);
complete_path::complete_path(&mut acc, &ctx)?;
complete_scope::complete_scope(&mut acc, &ctx)?;
Ok(Some(acc))
}
@ -62,6 +52,7 @@ pub(crate) fn completions(
#[derive(Debug)]
pub(super) struct SyntaxContext<'a> {
db: &'a db::RootDatabase,
offset: TextUnit,
leaf: SyntaxNodeRef<'a>,
module: Option<hir::Module>,
enclosing_fn: Option<ast::FnDef<'a>>,
@ -88,6 +79,7 @@ impl<'a> SyntaxContext<'a> {
let mut ctx = SyntaxContext {
db,
leaf,
offset: position.offset,
module,
enclosing_fn: None,
is_param: false,

View file

@ -9,8 +9,8 @@ pub(super) fn complete_path(acc: &mut Completions, ctx: &SyntaxContext) -> Cance
_ => return Ok(()),
};
let def_id = match module.resolve_path(ctx.db, path)? {
None => return Ok(()),
Some(it) => it,
None => return Ok(()),
};
let target_module = match def_id.resolve(ctx.db)? {
hir::Def::Module(it) => it,

View file

@ -0,0 +1,171 @@
use rustc_hash::FxHashSet;
use ra_syntax::TextUnit;
use crate::{
completion::{CompletionItem, Completions, CompletionKind::*, SyntaxContext},
Cancelable
};
pub(super) fn complete_scope(acc: &mut Completions, ctx: &SyntaxContext) -> Cancelable<()> {
if !ctx.is_trivial_path {
return Ok(());
}
if let Some(fn_def) = ctx.enclosing_fn {
let scopes = hir::FnScopes::new(fn_def);
complete_fn(acc, &scopes, ctx.offset);
}
if let Some(module) = &ctx.module {
let module_scope = module.scope(ctx.db)?;
module_scope
.entries()
.filter(|(_name, res)| {
// Don't expose this item
match res.import {
None => true,
Some(import) => {
let range = import.range(ctx.db, module.source().file_id());
!range.is_subrange(&ctx.leaf.range())
}
}
})
.for_each(|(name, _res)| {
CompletionItem::new(name.to_string())
.kind(Reference)
.add_to(acc)
});
}
Ok(())
}
fn complete_fn(acc: &mut Completions, scopes: &hir::FnScopes, offset: TextUnit) {
let mut shadowed = FxHashSet::default();
scopes
.scope_chain_for_offset(offset)
.flat_map(|scope| scopes.entries(scope).iter())
.filter(|entry| shadowed.insert(entry.name()))
.for_each(|entry| {
CompletionItem::new(entry.name().to_string())
.kind(Reference)
.add_to(acc)
});
if scopes.self_param.is_some() {
CompletionItem::new("self").kind(Reference).add_to(acc);
}
}
#[cfg(test)]
mod tests {
use crate::completion::{CompletionKind, check_completion};
fn check_reference_completion(code: &str, expected_completions: &str) {
check_completion(code, expected_completions, CompletionKind::Reference);
}
#[test]
fn test_completion_let_scope() {
check_reference_completion(
r"
fn quux(x: i32) {
let y = 92;
1 + <|>;
let z = ();
}
",
"y;x;quux",
);
}
#[test]
fn test_completion_if_let_scope() {
check_reference_completion(
r"
fn quux() {
if let Some(x) = foo() {
let y = 92;
};
if let Some(a) = bar() {
let b = 62;
1 + <|>
}
}
",
"b;a;quux",
);
}
#[test]
fn test_completion_for_scope() {
check_reference_completion(
r"
fn quux() {
for x in &[1, 2, 3] {
<|>
}
}
",
"x;quux",
);
}
#[test]
fn test_completion_mod_scope() {
check_reference_completion(
r"
struct Foo;
enum Baz {}
fn quux() {
<|>
}
",
"quux;Foo;Baz",
);
}
#[test]
fn test_completion_mod_scope_nested() {
check_reference_completion(
r"
struct Foo;
mod m {
struct Bar;
fn quux() { <|> }
}
",
"quux;Bar",
);
}
#[test]
fn test_complete_type() {
check_reference_completion(
r"
struct Foo;
fn x() -> <|>
",
"Foo;x",
)
}
#[test]
fn test_complete_shadowing() {
check_reference_completion(
r"
fn foo() -> {
let bar = 92;
{
let bar = 62;
<|>
}
}
",
"bar;foo",
)
}
#[test]
fn test_complete_self() {
check_reference_completion(r"impl S { fn foo(&self) { <|> } }", "self")
}
}

View file

@ -1,307 +0,0 @@
use rustc_hash::FxHashSet;
use ra_syntax::{
SourceFileNode, AstNode,
ast,
SyntaxKind::*,
};
use hir::{
self,
FnScopes, Path
};
use crate::{
db::RootDatabase,
completion::{CompletionItem, Completions, CompletionKind::*},
Cancelable
};
pub(super) fn completions(
acc: &mut Completions,
db: &RootDatabase,
module: &hir::Module,
_file: &SourceFileNode,
name_ref: ast::NameRef,
) -> Cancelable<()> {
let kind = match classify_name_ref(name_ref) {
Some(it) => it,
None => return Ok(()),
};
match kind {
NameRefKind::LocalRef { enclosing_fn } => {
if let Some(fn_def) = enclosing_fn {
let scopes = FnScopes::new(fn_def);
complete_fn(name_ref, &scopes, acc);
}
let module_scope = module.scope(db)?;
module_scope
.entries()
.filter(|(_name, res)| {
// Don't expose this item
match res.import {
None => true,
Some(import) => {
let range = import.range(db, module.source().file_id());
!range.is_subrange(&name_ref.syntax().range())
}
}
})
.for_each(|(name, _res)| {
CompletionItem::new(name.to_string())
.kind(Reference)
.add_to(acc)
});
}
NameRefKind::Path(_) => (),
NameRefKind::BareIdentInMod => (),
}
Ok(())
}
enum NameRefKind<'a> {
/// NameRef is a part of single-segment path, for example, a refernece to a
/// local variable.
LocalRef {
enclosing_fn: Option<ast::FnDef<'a>>,
},
/// NameRef is the last segment in some path
Path(Path),
/// NameRef is bare identifier at the module's root.
/// Used for keyword completion
BareIdentInMod,
}
fn classify_name_ref(name_ref: ast::NameRef) -> Option<NameRefKind> {
let name_range = name_ref.syntax().range();
let top_node = name_ref
.syntax()
.ancestors()
.take_while(|it| it.range() == name_range)
.last()
.unwrap();
match top_node.parent().map(|it| it.kind()) {
Some(SOURCE_FILE) | Some(ITEM_LIST) => return Some(NameRefKind::BareIdentInMod),
_ => (),
}
let parent = name_ref.syntax().parent()?;
if let Some(segment) = ast::PathSegment::cast(parent) {
let path = segment.parent_path();
if let Some(path) = Path::from_ast(path) {
if !path.is_ident() {
return Some(NameRefKind::Path(path));
}
}
if path.qualifier().is_none() {
let enclosing_fn = name_ref
.syntax()
.ancestors()
.take_while(|it| it.kind() != SOURCE_FILE && it.kind() != MODULE)
.find_map(ast::FnDef::cast);
return Some(NameRefKind::LocalRef { enclosing_fn });
}
}
None
}
fn complete_fn(name_ref: ast::NameRef, scopes: &FnScopes, acc: &mut Completions) {
let mut shadowed = FxHashSet::default();
scopes
.scope_chain(name_ref.syntax())
.flat_map(|scope| scopes.entries(scope).iter())
.filter(|entry| shadowed.insert(entry.name()))
.for_each(|entry| {
CompletionItem::new(entry.name().to_string())
.kind(Reference)
.add_to(acc)
});
if scopes.self_param.is_some() {
CompletionItem::new("self").kind(Reference).add_to(acc);
}
}
#[cfg(test)]
mod tests {
use crate::completion::{CompletionKind, check_completion};
fn check_reference_completion(code: &str, expected_completions: &str) {
check_completion(code, expected_completions, CompletionKind::Reference);
}
#[test]
fn test_completion_let_scope() {
check_reference_completion(
r"
fn quux(x: i32) {
let y = 92;
1 + <|>;
let z = ();
}
",
"y;x;quux",
);
}
#[test]
fn test_completion_if_let_scope() {
check_reference_completion(
r"
fn quux() {
if let Some(x) = foo() {
let y = 92;
};
if let Some(a) = bar() {
let b = 62;
1 + <|>
}
}
",
"b;a;quux",
);
}
#[test]
fn test_completion_for_scope() {
check_reference_completion(
r"
fn quux() {
for x in &[1, 2, 3] {
<|>
}
}
",
"x;quux",
);
}
#[test]
fn test_completion_mod_scope() {
check_reference_completion(
r"
struct Foo;
enum Baz {}
fn quux() {
<|>
}
",
"quux;Foo;Baz",
);
}
#[test]
fn test_completion_mod_scope_no_self_use() {
check_reference_completion(
r"
use foo<|>;
",
"",
);
}
#[test]
fn test_completion_self_path() {
check_reference_completion(
r"
use self::m::<|>;
mod m {
struct Bar;
}
",
"Bar",
);
}
#[test]
fn test_completion_mod_scope_nested() {
check_reference_completion(
r"
struct Foo;
mod m {
struct Bar;
fn quux() { <|> }
}
",
"quux;Bar",
);
}
#[test]
fn test_complete_type() {
check_reference_completion(
r"
struct Foo;
fn x() -> <|>
",
"Foo;x",
)
}
#[test]
fn test_complete_shadowing() {
check_reference_completion(
r"
fn foo() -> {
let bar = 92;
{
let bar = 62;
<|>
}
}
",
"bar;foo",
)
}
#[test]
fn test_complete_self() {
check_reference_completion(r"impl S { fn foo(&self) { <|> } }", "self")
}
#[test]
fn test_complete_crate_path() {
check_reference_completion(
"
//- /lib.rs
mod foo;
struct Spam;
//- /foo.rs
use crate::Sp<|>
",
"Spam;foo",
);
}
#[test]
fn test_complete_crate_path_with_braces() {
check_reference_completion(
"
//- /lib.rs
mod foo;
struct Spam;
//- /foo.rs
use crate::{Sp<|>};
",
"Spam;foo",
);
}
#[test]
fn test_complete_crate_path_in_nested_tree() {
check_reference_completion(
"
//- /lib.rs
mod foo;
pub mod bar {
pub mod baz {
pub struct Spam;
}
}
//- /foo.rs
use crate::{bar::{baz::Sp<|>}};
",
"Spam",
);
}
}

View file

@ -1,7 +1,7 @@
use rustc_hash::{FxHashMap, FxHashSet};
use ra_syntax::{
AstNode, SmolStr, SyntaxNodeRef, TextRange,
AstNode, SmolStr, SyntaxNodeRef, TextUnit, TextRange,
algo::generate,
ast::{self, ArgListOwner, LoopBodyOwner, NameOwner},
};
@ -57,6 +57,48 @@ impl FnScopes {
self.scopes[scope].parent
})
}
pub fn scope_chain_for_offset<'a>(
&'a self,
offset: TextUnit,
) -> impl Iterator<Item = ScopeId> + 'a {
let scope = self
.scope_for
.iter()
// find containin scope
.min_by_key(|(ptr, _scope)| {
(
!(ptr.range().start() <= offset && offset <= ptr.range().end()),
ptr.range().len(),
)
})
.map(|(ptr, scope)| self.adjust(*ptr, *scope, offset));
generate(scope, move |&scope| self.scopes[scope].parent)
}
// XXX: during completion, cursor might be outside of any particular
// expression. Try to figure out the correct scope...
fn adjust(&self, ptr: LocalSyntaxPtr, original_scope: ScopeId, offset: TextUnit) -> ScopeId {
let r = ptr.range();
let child_scopes = self
.scope_for
.iter()
.map(|(ptr, scope)| (ptr.range(), scope))
.filter(|(range, _)| range.start() <= offset && range.is_subrange(&r) && *range != r);
child_scopes
.max_by(|(r1, _), (r2, _)| {
if r2.is_subrange(&r1) {
std::cmp::Ordering::Greater
} else if r1.is_subrange(&r2) {
std::cmp::Ordering::Less
} else {
r1.start().cmp(&r2.start())
}
})
.map(|(ptr, scope)| *scope)
.unwrap_or(original_scope)
}
pub fn resolve_local_name<'a>(&'a self, name_ref: ast::NameRef) -> Option<&'a ScopeEntry> {
let mut shadowed = FxHashSet::default();
let ret = self
@ -144,6 +186,8 @@ impl ScopeEntry {
}
fn compute_block_scopes(block: ast::Block, scopes: &mut FnScopes, mut scope: ScopeId) {
// A hack for completion :(
scopes.set_scope(block.syntax(), scope);
for stmt in block.statements() {
match stmt {
ast::Stmt::LetStmt(stmt) => {
@ -165,6 +209,7 @@ fn compute_block_scopes(block: ast::Block, scopes: &mut FnScopes, mut scope: Sco
}
}
if let Some(expr) = block.expr() {
eprintln!("{:?}", expr);
scopes.set_scope(expr.syntax(), scope);
compute_expr_scopes(expr, scopes, scope);
}