feat: classify field accesses for ide functions (#1034)

* feat: classify field accesses for ide functions

* test: update snapshot
This commit is contained in:
Myriad-Dreamin 2024-12-20 18:47:44 +08:00 committed by GitHub
parent 7dbaca8851
commit 39243ba626
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 216 additions and 81 deletions

View file

@ -5,7 +5,7 @@ use typst::introspection::Introspector;
use typst::model::BibliographyElem;
use super::{prelude::*, InsTy, SharedContext};
use crate::syntax::{Decl, DeclExpr, Expr, ExprInfo, SyntaxClass};
use crate::syntax::{Decl, DeclExpr, Expr, ExprInfo, SyntaxClass, VarClass};
use crate::ty::DocSource;
use crate::VersionedDocument;
@ -63,10 +63,9 @@ pub fn definition(
syntax: SyntaxClass,
) -> Option<Definition> {
match syntax {
// todi: field access
SyntaxClass::VarAccess(node) | SyntaxClass::Callee(node) => {
find_ident_definition(ctx, source, node)
}
// todo: field access
SyntaxClass::VarAccess(node) => find_ident_definition(ctx, source, node),
SyntaxClass::Callee(node) => find_ident_definition(ctx, source, VarClass::Ident(node)),
SyntaxClass::ImportPath(path) | SyntaxClass::IncludePath(path) => {
DefResolver::new(ctx, source)?.of_span(path.span())
}
@ -97,16 +96,16 @@ pub fn definition(
fn find_ident_definition(
ctx: &Arc<SharedContext>,
source: &Source,
use_site: LinkedNode,
use_site: VarClass,
) -> Option<Definition> {
// Lexical reference
let ident_store = use_site.clone();
let ident_ref = match ident_store.cast::<ast::Expr>()? {
let ident_ref = match ident_store.node().cast::<ast::Expr>()? {
ast::Expr::Ident(ident) => ident.span(),
ast::Expr::MathIdent(ident) => ident.span(),
ast::Expr::FieldAccess(field_access) => return field_definition(ctx, field_access),
_ => {
crate::log_debug_ct!("unsupported kind {kind:?}", kind = use_site.kind());
crate::log_debug_ct!("unsupported kind {kind:?}", kind = use_site.node().kind());
Span::detached()
}
};

View file

@ -8,7 +8,7 @@ use super::{
ArgsTy, Sig, SigChecker, SigShape, SigSurfaceKind, SigTy, Ty, TyCtx, TyCtxMut, TypeBounds,
TypeInfo, TypeVar,
};
use crate::syntax::{classify_cursor, classify_cursor_by_context, ArgClass, CursorClass};
use crate::syntax::{classify_cursor, classify_cursor_by_context, ArgClass, CursorClass, VarClass};
use crate::ty::BuiltinTy;
/// With given type information, check the type of a literal expression again by
@ -182,7 +182,7 @@ impl<'a> PostTypeChecker<'a> {
None
};
let contextual_self_ty = self.check_cursor(classify_cursor(node.clone()), context_ty);
let contextual_self_ty = self.check_cursor(classify_cursor(node.clone(), None), context_ty);
crate::log_debug_ct!(
"post check(res): {:?}::{:?} -> {self_ty:?}, {contextual_self_ty:?}",
context.kind(),
@ -303,10 +303,14 @@ impl<'a> PostTypeChecker<'a> {
allow_package: true,
}),
)),
CursorClass::Label { node: target, .. } | CursorClass::Normal(target) => {
CursorClass::VarAccess(VarClass::Ident(node))
| CursorClass::VarAccess(VarClass::FieldAccess(node))
| CursorClass::VarAccess(VarClass::DotAccess(node))
| CursorClass::Label { node, .. }
| CursorClass::Normal(node) => {
let label_ty = matches!(cursor, CursorClass::Label { is_error: true, .. })
.then_some(Ty::Builtin(BuiltinTy::Label));
let ty = self.check_or(target, context_ty);
let ty = self.check_or(node, context_ty);
crate::log_debug_ct!("post check target normal: {ty:?} {label_ty:?}");
ty.or(label_ty)
}
@ -335,7 +339,7 @@ impl<'a> PostTypeChecker<'a> {
None,
),
// todo: constraint node
SyntaxKind::Named => self.check_cursor(classify_cursor(context.clone()), None),
SyntaxKind::Named => self.check_cursor(classify_cursor(context.clone(), None), None),
_ => None,
}
}

View file

@ -89,7 +89,8 @@ impl StatefulRequest for CompletionRequest {
let explicit = false;
// Skip if is the let binding item *directly*
if let Some(SyntaxClass::VarAccess(node)) = &syntax {
if let Some(SyntaxClass::VarAccess(var)) = &syntax {
let node = var.node();
match node.parent_kind() {
// complete the init part of the let binding
Some(SyntaxKind::LetBinding) => {

View file

@ -8,28 +8,6 @@ snapshot_kind: text
[
{
"isIncomplete": false,
"items": [
{
"kind": 5,
"label": "mode",
"labelDetails": {
"description": "\"code\" | \"markup\" | \"math\""
},
"sortText": "000",
"textEdit": {
"newText": "mode: ${1:}",
"range": {
"end": {
"character": 69,
"line": 2
},
"start": {
"character": 69,
"line": 2
}
}
}
}
]
"items": []
}
]

View file

@ -8,28 +8,6 @@ snapshot_kind: text
[
{
"isIncomplete": false,
"items": [
{
"kind": 5,
"label": "mode",
"labelDetails": {
"description": "\"code\" | \"markup\" | \"math\""
},
"sortText": "000",
"textEdit": {
"newText": "mode: ${1:}",
"range": {
"end": {
"character": 69,
"line": 2
},
"start": {
"character": 69,
"line": 2
}
}
}
}
]
"items": []
}
]

View file

@ -33,7 +33,7 @@ impl SemanticRequest for SignatureHelpRequest {
target,
is_set,
..
} = classify_cursor(ast_node)?
} = classify_cursor(ast_node, Some(cursor))?
else {
return None;
};

View file

@ -1,3 +1,4 @@
use reflexo_typst::debug_loc::SourceSpanOffset;
use serde::{Deserialize, Serialize};
use crate::prelude::*;
@ -255,13 +256,109 @@ pub(crate) fn interpret_mode_at_kind(kind: SyntaxKind) -> Option<InterpretMode>
})
}
/// Classes of field syntax that can be operated on by IDE functionality.
#[derive(Debug, Clone)]
pub enum FieldClass<'a> {
Field(LinkedNode<'a>),
DotSuffix(SourceSpanOffset),
}
impl FieldClass<'_> {
/// Gets the node of the field class.
pub fn offset(&self, source: &Source) -> Option<usize> {
Some(match self {
Self::Field(node) => node.offset(),
Self::DotSuffix(span_offset) => {
source.find(span_offset.span)?.offset() + span_offset.offset
}
})
}
}
/// Classes of variable (access) syntax that can be operated on by IDE
/// functionality.
#[derive(Debug, Clone)]
pub enum VarClass<'a> {
/// An identifier expression.
Ident(LinkedNode<'a>),
/// A field access expression.
FieldAccess(LinkedNode<'a>),
/// A dot access expression, for example, `#a.|`, `$a.|$`, or `x.|.y`.
/// Note the cursor of the last example is on the middle of the spread
/// operator.
DotAccess(LinkedNode<'a>),
}
impl<'a> VarClass<'a> {
/// Gets the node of the var (access) class.
pub fn node(&self) -> &LinkedNode<'a> {
match self {
Self::Ident(node) | Self::FieldAccess(node) | Self::DotAccess(node) => node,
}
}
/// Gets the accessed node of the var (access) class.
pub fn accessed_node(&self) -> Option<LinkedNode<'a>> {
Some(match self {
Self::Ident(node) => node.clone(),
Self::FieldAccess(node) => {
let field_access = node.cast::<ast::FieldAccess>()?;
node.find(field_access.target().span())?
}
Self::DotAccess(node) => node.clone(),
})
}
/// Gets the accessing field of the var (access) class.
pub fn accessing_field(&self) -> Option<FieldClass<'a>> {
match self {
Self::FieldAccess(node) => {
let dot = node
.children()
.find(|n| matches!(n.kind(), SyntaxKind::Dot))?;
let mut iter_after_dot =
node.children().skip_while(|n| n.kind() != SyntaxKind::Dot);
let ident = iter_after_dot.find(|n| {
matches!(
n.kind(),
SyntaxKind::Ident | SyntaxKind::MathIdent | SyntaxKind::Error
)
});
let ident_case = ident.map(|ident| {
if ident.text().is_empty() {
FieldClass::DotSuffix(SourceSpanOffset {
span: ident.span(),
offset: 0,
})
} else {
FieldClass::Field(ident)
}
});
ident_case.or_else(|| {
Some(FieldClass::DotSuffix(SourceSpanOffset {
span: dot.span(),
offset: 1,
}))
})
}
Self::DotAccess(node) => Some(FieldClass::DotSuffix(SourceSpanOffset {
span: node.span(),
offset: node.range().len() + 1,
})),
Self::Ident(_) => None,
}
}
}
/// Classes of syntax that can be operated on by IDE functionality.
#[derive(Debug, Clone)]
pub enum SyntaxClass<'a> {
/// A variable access expression.
///
/// It can be either an identifier or a field access.
VarAccess(LinkedNode<'a>),
VarAccess(VarClass<'a>),
/// A (content) label expression.
Label {
node: LinkedNode<'a>,
@ -299,9 +396,9 @@ impl<'a> SyntaxClass<'a> {
/// Gets the node of the syntax class.
pub fn node(&self) -> &LinkedNode<'a> {
match self {
SyntaxClass::VarAccess(cls) => cls.node(),
SyntaxClass::Label { node, .. }
| SyntaxClass::Ref(node)
| SyntaxClass::VarAccess(node)
| SyntaxClass::Callee(node)
| SyntaxClass::ImportPath(node)
| SyntaxClass::IncludePath(node)
@ -344,6 +441,28 @@ pub fn classify_syntax(node: LinkedNode, cursor: usize) -> Option<SyntaxClass<'_
node = node.prev_sibling()?;
}
let is_dot = matches!(node.kind(), SyntaxKind::Dot)
|| (matches!(node.kind(), SyntaxKind::Text | SyntaxKind::Error) && node.text() == ".");
if is_dot && node.offset() + 1 == cursor {
let dot_target = node.clone().prev_leaf().and_then(first_ancestor_expr);
if let Some(dots_target) = dot_target {
return Some(SyntaxClass::VarAccess(VarClass::DotAccess(dots_target)));
}
}
if matches!(node.kind(), SyntaxKind::Dots) && node.offset() + 1 == cursor {
let dot_target = node.parent()?;
if dot_target.kind() == SyntaxKind::Spread {
let dot_target = dot_target.prev_leaf().and_then(first_ancestor_expr);
if let Some(dot_target) = dot_target {
return Some(SyntaxClass::VarAccess(VarClass::DotAccess(dot_target)));
}
}
}
// Move to the first ancestor that is an expression.
let ancestor = first_ancestor_expr(node)?;
crate::log_debug_ct!("first_ancestor_expr: {ancestor:?}");
@ -359,9 +478,10 @@ pub fn classify_syntax(node: LinkedNode, cursor: usize) -> Option<SyntaxClass<'_
ast::Expr::Ref(..) => SyntaxClass::Ref(adjusted),
ast::Expr::FuncCall(call) => SyntaxClass::Callee(adjusted.find(call.callee().span())?),
ast::Expr::Set(set) => SyntaxClass::Callee(adjusted.find(set.target().span())?),
ast::Expr::Ident(..) | ast::Expr::MathIdent(..) | ast::Expr::FieldAccess(..) => {
SyntaxClass::VarAccess(adjusted)
ast::Expr::Ident(..) | ast::Expr::MathIdent(..) => {
SyntaxClass::VarAccess(VarClass::Ident(adjusted))
}
ast::Expr::FieldAccess(..) => SyntaxClass::VarAccess(VarClass::FieldAccess(adjusted)),
ast::Expr::Str(..) => {
let parent = adjusted.parent()?;
if parent.kind() == SyntaxKind::ModuleImport {
@ -571,6 +691,10 @@ pub enum CursorClass<'a> {
container: LinkedNode<'a>,
is_before: bool,
},
/// A variable access expression.
///
/// It can be either an identifier or a field access.
VarAccess(VarClass<'a>),
/// A cursor on an import path.
ImportPath(LinkedNode<'a>),
/// A cursor on an include path.
@ -592,6 +716,7 @@ impl<'a> CursorClass<'a> {
ArgClass::Positional { .. } => return None,
ArgClass::Named(node) => node.clone(),
},
CursorClass::VarAccess(cls) => cls.node().clone(),
CursorClass::Paren { container, .. } => container.clone(),
CursorClass::Label { node, .. }
| CursorClass::ImportPath(node)
@ -648,7 +773,7 @@ pub fn classify_cursor_by_context<'a>(
}
/// Classifies a cursor syntax that are preferred by type checking.
pub fn classify_cursor(node: LinkedNode) -> Option<CursorClass<'_>> {
pub fn classify_cursor(node: LinkedNode, cursor: Option<usize>) -> Option<CursorClass<'_>> {
let mut node = node;
if node.kind().is_trivia() && node.parent_kind().is_some_and(possible_in_code_trivia) {
loop {
@ -660,7 +785,8 @@ pub fn classify_cursor(node: LinkedNode) -> Option<CursorClass<'_>> {
}
}
let syntax = classify_syntax(node.clone(), node.offset())?;
let cursor = cursor.unwrap_or_else(|| node.offset());
let syntax = classify_syntax(node.clone(), cursor)?;
let normal_syntax = match syntax {
SyntaxClass::Callee(callee) => {
@ -675,7 +801,7 @@ pub fn classify_cursor(node: LinkedNode) -> Option<CursorClass<'_>> {
SyntaxClass::IncludePath(node) => {
return Some(CursorClass::IncludePath(node));
}
syntax => syntax.node().clone(),
syntax => syntax,
};
let Some(mut node_parent) = node.parent().cloned() else {
@ -739,7 +865,10 @@ pub fn classify_cursor(node: LinkedNode) -> Option<CursorClass<'_>> {
is_before,
})
}
_ => Some(CursorClass::Normal(normal_syntax)),
_ => Some(match normal_syntax {
SyntaxClass::VarAccess(v) => CursorClass::VarAccess(v),
normal_syntax => CursorClass::Normal(normal_syntax.node().clone()),
}),
}
}
@ -884,11 +1013,12 @@ mod tests {
fn map_cursor(source: &str) -> String {
map_node(source, |root, cursor| {
let node = root.leaf_at_compat(cursor);
let kind = node.and_then(|node| classify_cursor(node));
let kind = node.and_then(|node| classify_cursor(node, Some(cursor)));
match kind {
Some(CursorClass::Arg { .. }) => 'p',
Some(CursorClass::Element { .. }) => 'e',
Some(CursorClass::Paren { .. }) => 'P',
Some(CursorClass::VarAccess { .. }) => 'v',
Some(CursorClass::ImportPath(..)) => 'i',
Some(CursorClass::IncludePath(..)) => 'I',
Some(CursorClass::Label { .. }) => 'l',
@ -935,20 +1065,20 @@ Text
= Heading #let y = 2;
== Heading"#).trim(), @r"
#let x = 1
nnnnnnnnn
nnnnvvnnn
Text
= Heading #let y = 2;
nnnnnnnnn
nnnnvvnnn
== Heading
");
assert_snapshot!(map_cursor(r#"#let f(x);"#).trim(), @r"
#let f(x);
nnnnn n
nnnnv v
");
assert_snapshot!(map_cursor(r#"#f(1, 2) Test"#).trim(), @r"
#f(1, 2) Test
npppppp
vpppppp
");
assert_snapshot!(map_cursor(r#"#() Test"#).trim(), @r"
#() Test
@ -973,4 +1103,42 @@ Text
Test
");
}
#[test]
fn test_access_field() {
fn test_fn(s: &str, cursor: i32) -> String {
test_fn_(s, cursor).unwrap_or_default()
}
fn test_fn_(s: &str, cursor: i32) -> Option<String> {
let cursor = if cursor < 0 {
s.len() as i32 + cursor
} else {
cursor
};
let source = Source::detached(s.to_owned());
let root = LinkedNode::new(source.root());
let node = root.leaf_at_compat(cursor as usize)?;
let syntax = classify_syntax(node, cursor as usize)?;
let SyntaxClass::VarAccess(var) = syntax else {
return None;
};
let field = var.accessing_field()?;
Some(match field {
FieldClass::Field(ident) => format!("Field: {}", ident.text()),
FieldClass::DotSuffix(span_offset) => {
let offset = source.find(span_offset.span)?.offset() + span_offset.offset;
format!("DotSuffix: {offset:?}")
}
})
}
assert_snapshot!(test_fn("#(a.b)", 5), @r"Field: b");
assert_snapshot!(test_fn("#a.", 3), @"DotSuffix: 3");
assert_snapshot!(test_fn("$a.$", 3), @"DotSuffix: 3");
assert_snapshot!(test_fn("#(a.)", 4), @"DotSuffix: 4");
assert_snapshot!(test_fn("#(a..b)", 4), @"DotSuffix: 4");
assert_snapshot!(test_fn("#(a..b())", 4), @"DotSuffix: 4");
}
}

View file

@ -22,6 +22,7 @@ use crate::snippet::{
};
use crate::syntax::{
interpret_mode_at, is_ident_like, previous_decls, CursorClass, InterpretMode, PreviousDecl,
VarClass,
};
use crate::ty::{DynTypeBounds, Iface, IfaceChecker, InsTy, SigTy, TyCtx, TypeInfo, TypeVar};
use crate::upstream::complete::complete_code;
@ -1373,7 +1374,7 @@ pub(crate) fn complete_type_and_syntax(ctx: &mut CompletionContext) -> Option<()
use crate::syntax::classify_cursor;
use SurroundingSyntax::*;
let cursor_class = classify_cursor(ctx.leaf.clone());
let cursor_class = classify_cursor(ctx.leaf.clone(), Some(ctx.cursor));
crate::log_debug_ct!("complete_type: pos {:?} -> {cursor_class:#?}", ctx.leaf);
let mut args_node = None;
@ -1396,6 +1397,11 @@ pub(crate) fn complete_type_and_syntax(ctx: &mut CompletionContext) -> Option<()
}
args_node = Some(args.to_untyped().clone());
}
// todo: complete field by types
Some(CursorClass::VarAccess(VarClass::FieldAccess { .. }))
| Some(CursorClass::VarAccess(VarClass::DotAccess { .. })) => {
return None;
}
Some(CursorClass::ImportPath(path) | CursorClass::IncludePath(path)) => {
let Some(ast::Expr::Str(str)) = path.cast() else {
return None;
@ -1429,11 +1435,12 @@ pub(crate) fn complete_type_and_syntax(ctx: &mut CompletionContext) -> Option<()
{
args_node = node.parent().map(|s| s.get().clone());
}
// todo: complete type field
Some(CursorClass::Normal(node)) if matches!(node.kind(), SyntaxKind::FieldAccess) => {
return None;
}
Some(CursorClass::Paren { .. } | CursorClass::Label { .. } | CursorClass::Normal(..))
Some(
CursorClass::VarAccess(VarClass::Ident { .. })
| CursorClass::Paren { .. }
| CursorClass::Label { .. }
| CursorClass::Normal(..),
)
| None => {}
}

View file

@ -385,7 +385,7 @@ fn e2e() {
});
let hash = replay_log(&tinymist_binary, &root.join("vscode"));
insta::assert_snapshot!(hash, @"siphash128_13:ac449ba75867cd79a6135b0285c0cf47");
insta::assert_snapshot!(hash, @"siphash128_13:12bc49a33793e415352373ce601c715f");
}
}