diff --git a/crates/tinymist-query/src/analysis/definition.rs b/crates/tinymist-query/src/analysis/definition.rs index 40274b88..26efb1ee 100644 --- a/crates/tinymist-query/src/analysis/definition.rs +++ b/crates/tinymist-query/src/analysis/definition.rs @@ -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 { 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, source: &Source, - use_site: LinkedNode, + use_site: VarClass, ) -> Option { // Lexical reference let ident_store = use_site.clone(); - let ident_ref = match ident_store.cast::()? { + let ident_ref = match ident_store.node().cast::()? { 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() } }; diff --git a/crates/tinymist-query/src/analysis/post_tyck.rs b/crates/tinymist-query/src/analysis/post_tyck.rs index 1dfbc1bb..71b68b31 100644 --- a/crates/tinymist-query/src/analysis/post_tyck.rs +++ b/crates/tinymist-query/src/analysis/post_tyck.rs @@ -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, } } diff --git a/crates/tinymist-query/src/completion.rs b/crates/tinymist-query/src/completion.rs index 63ac4ff0..e939fdd9 100644 --- a/crates/tinymist-query/src/completion.rs +++ b/crates/tinymist-query/src/completion.rs @@ -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) => { diff --git a/crates/tinymist-query/src/fixtures/completion/snaps/test@func_pos_arg_field_call.typ.snap b/crates/tinymist-query/src/fixtures/completion/snaps/test@func_pos_arg_field_call.typ.snap index 575992b7..99156dd1 100644 --- a/crates/tinymist-query/src/fixtures/completion/snaps/test@func_pos_arg_field_call.typ.snap +++ b/crates/tinymist-query/src/fixtures/completion/snaps/test@func_pos_arg_field_call.typ.snap @@ -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": [] } ] diff --git a/crates/tinymist-query/src/fixtures/completion/snaps/test@func_pos_arg_field_middle.typ.snap b/crates/tinymist-query/src/fixtures/completion/snaps/test@func_pos_arg_field_middle.typ.snap index bce6709a..b8f5ece0 100644 --- a/crates/tinymist-query/src/fixtures/completion/snaps/test@func_pos_arg_field_middle.typ.snap +++ b/crates/tinymist-query/src/fixtures/completion/snaps/test@func_pos_arg_field_middle.typ.snap @@ -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": [] } ] diff --git a/crates/tinymist-query/src/signature_help.rs b/crates/tinymist-query/src/signature_help.rs index 48bf2631..28028a21 100644 --- a/crates/tinymist-query/src/signature_help.rs +++ b/crates/tinymist-query/src/signature_help.rs @@ -33,7 +33,7 @@ impl SemanticRequest for SignatureHelpRequest { target, is_set, .. - } = classify_cursor(ast_node)? + } = classify_cursor(ast_node, Some(cursor))? else { return None; }; diff --git a/crates/tinymist-query/src/syntax/matcher.rs b/crates/tinymist-query/src/syntax/matcher.rs index 475f6f5a..fce3a602 100644 --- a/crates/tinymist-query/src/syntax/matcher.rs +++ b/crates/tinymist-query/src/syntax/matcher.rs @@ -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 }) } +/// 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 { + 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> { + Some(match self { + Self::Ident(node) => node.clone(), + Self::FieldAccess(node) => { + let field_access = node.cast::()?; + 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> { + 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 Option 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> { +pub fn classify_cursor(node: LinkedNode, cursor: Option) -> Option> { 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> { } } - 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> { 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> { 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 { + 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"); + } } diff --git a/crates/tinymist-query/src/upstream/complete/ext.rs b/crates/tinymist-query/src/upstream/complete/ext.rs index 4c971b5b..d142392a 100644 --- a/crates/tinymist-query/src/upstream/complete/ext.rs +++ b/crates/tinymist-query/src/upstream/complete/ext.rs @@ -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 => {} } diff --git a/tests/e2e/main.rs b/tests/e2e/main.rs index 1b9cf8ee..aeef20fa 100644 --- a/tests/e2e/main.rs +++ b/tests/e2e/main.rs @@ -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"); } }