From 1ef21739b752314c33fba5584f4c61b3c662095e Mon Sep 17 00:00:00 2001
From: Myriad-Dreamin <camiyoru@gmail.com>
Date: Tue, 12 Mar 2024 03:02:56 +0800
Subject: [PATCH] feat: analyze lexical hierarchy for def-use relations

---
 crates/tinymist-query/src/analysis.rs         |  33 ++
 crates/tinymist-query/src/analysis/def_use.rs |   7 +
 .../src/analysis/lexical_hierarchy.rs         | 420 +++++++++++++-----
 crates/tinymist-query/src/analysis/mod.rs     |  10 -
 .../src/fixtures/document_symbols/func.typ    |   1 +
 .../document_symbols/test@func.typ.snap       |  13 +
 .../folding_range/test@paren_folding.typ.snap |  10 +-
 .../src/fixtures/lexical_hierarchy/base.typ   |   3 +
 .../lexical_hierarchy/destructing.typ         |   2 +
 .../src/fixtures/lexical_hierarchy/dict.typ   |   5 +
 .../src/fixtures/lexical_hierarchy/func.typ   |   2 +
 .../fixtures/lexical_hierarchy/redefine.typ   |   2 +
 .../lexical_hierarchy/scope@base.typ.snap     |  17 +
 .../scope@destructing.typ.snap                |  37 ++
 .../lexical_hierarchy/scope@dict.typ.snap     |  27 ++
 .../lexical_hierarchy/scope@func.typ.snap     |  34 ++
 .../lexical_hierarchy/scope@redefine.typ.snap |  22 +
 crates/tinymist-query/src/lib.rs              |  16 +-
 18 files changed, 534 insertions(+), 127 deletions(-)
 create mode 100644 crates/tinymist-query/src/analysis.rs
 create mode 100644 crates/tinymist-query/src/analysis/def_use.rs
 delete mode 100644 crates/tinymist-query/src/analysis/mod.rs
 create mode 100644 crates/tinymist-query/src/fixtures/document_symbols/func.typ
 create mode 100644 crates/tinymist-query/src/fixtures/document_symbols/test@func.typ.snap
 create mode 100644 crates/tinymist-query/src/fixtures/lexical_hierarchy/base.typ
 create mode 100644 crates/tinymist-query/src/fixtures/lexical_hierarchy/destructing.typ
 create mode 100644 crates/tinymist-query/src/fixtures/lexical_hierarchy/dict.typ
 create mode 100644 crates/tinymist-query/src/fixtures/lexical_hierarchy/func.typ
 create mode 100644 crates/tinymist-query/src/fixtures/lexical_hierarchy/redefine.typ
 create mode 100644 crates/tinymist-query/src/fixtures/lexical_hierarchy/scope@base.typ.snap
 create mode 100644 crates/tinymist-query/src/fixtures/lexical_hierarchy/scope@destructing.typ.snap
 create mode 100644 crates/tinymist-query/src/fixtures/lexical_hierarchy/scope@dict.typ.snap
 create mode 100644 crates/tinymist-query/src/fixtures/lexical_hierarchy/scope@func.typ.snap
 create mode 100644 crates/tinymist-query/src/fixtures/lexical_hierarchy/scope@redefine.typ.snap

diff --git a/crates/tinymist-query/src/analysis.rs b/crates/tinymist-query/src/analysis.rs
new file mode 100644
index 000000000..1df325660
--- /dev/null
+++ b/crates/tinymist-query/src/analysis.rs
@@ -0,0 +1,33 @@
+pub mod track_values;
+pub use track_values::*;
+pub mod lexical_hierarchy;
+pub(crate) use lexical_hierarchy::*;
+pub mod definition;
+pub use definition::*;
+pub mod import;
+pub use import::*;
+pub mod reference;
+pub use reference::*;
+pub mod def_use;
+pub use def_use::*;
+
+#[cfg(test)]
+mod lexical_hierarchy_tests {
+    use crate::analysis::lexical_hierarchy;
+    use crate::prelude::*;
+    use crate::tests::*;
+
+    #[test]
+    fn scope() {
+        snapshot_testing("lexical_hierarchy", &|world, path| {
+            let source = get_suitable_source_in_workspace(world, &path).unwrap();
+
+            let result = lexical_hierarchy::get_lexical_hierarchy(
+                source,
+                lexical_hierarchy::LexicalScopeKind::DefUse,
+            );
+
+            assert_snapshot!(JsonRepr::new_redacted(result, &REDACT_LOC));
+        });
+    }
+}
diff --git a/crates/tinymist-query/src/analysis/def_use.rs b/crates/tinymist-query/src/analysis/def_use.rs
new file mode 100644
index 000000000..2b1c0381a
--- /dev/null
+++ b/crates/tinymist-query/src/analysis/def_use.rs
@@ -0,0 +1,7 @@
+use typst::syntax::Source;
+
+use super::{get_lexical_hierarchy, LexicalScopeKind};
+
+pub fn get_def_use(source: Source) {
+    let _ = get_lexical_hierarchy(source, LexicalScopeKind::DefUse);
+}
diff --git a/crates/tinymist-query/src/analysis/lexical_hierarchy.rs b/crates/tinymist-query/src/analysis/lexical_hierarchy.rs
index 42b66af05..d8f6c84b8 100644
--- a/crates/tinymist-query/src/analysis/lexical_hierarchy.rs
+++ b/crates/tinymist-query/src/analysis/lexical_hierarchy.rs
@@ -1,17 +1,53 @@
-use std::ops::Range;
+use std::ops::{Deref, Range};
 
 use anyhow::anyhow;
 use log::info;
 use lsp_types::SymbolKind;
-use typst::syntax::{ast, LinkedNode, Source, SyntaxKind};
+use serde::{Deserialize, Serialize};
+use typst::{
+    syntax::{ast, LinkedNode, Source, SyntaxKind},
+    util::LazyHash,
+};
 use typst_ts_core::typst::prelude::{eco_vec, EcoVec};
 
-#[derive(Debug, Clone, Copy, Hash)]
+pub(crate) fn get_lexical_hierarchy(
+    source: Source,
+    g: LexicalScopeKind,
+) -> Option<EcoVec<LexicalHierarchy>> {
+    let b = std::time::Instant::now();
+    let root = LinkedNode::new(source.root());
+
+    let mut worker = LexicalHierarchyWorker {
+        g,
+        ..LexicalHierarchyWorker::default()
+    };
+    worker.stack.push((
+        LexicalInfo {
+            name: "deadbeef".to_string(),
+            kind: LexicalKind::Namespace(-1),
+            range: 0..0,
+        },
+        eco_vec![],
+    ));
+    let res = worker.get_symbols(root).ok();
+
+    while worker.stack.len() > 1 {
+        worker.symbreak();
+    }
+
+    let e = std::time::Instant::now();
+    info!("lexical hierarchy analysis took {:?}", e - b);
+    res.map(|_| worker.stack.pop().unwrap().1)
+}
+
+#[derive(Debug, Clone, Copy, Hash, Serialize, Deserialize)]
 pub(crate) enum LexicalKind {
     Namespace(i16),
+    ValRef,
+    LabelRef,
     Variable,
     Function,
-    Constant,
+    Label,
     Block,
 }
 
@@ -23,8 +59,8 @@ impl TryFrom<LexicalKind> for SymbolKind {
             LexicalKind::Namespace(..) => Ok(SymbolKind::NAMESPACE),
             LexicalKind::Variable => Ok(SymbolKind::VARIABLE),
             LexicalKind::Function => Ok(SymbolKind::FUNCTION),
-            LexicalKind::Constant => Ok(SymbolKind::CONSTANT),
-            LexicalKind::Block => Err(()),
+            LexicalKind::Label => Ok(SymbolKind::CONSTANT),
+            LexicalKind::ValRef | LexicalKind::LabelRef | LexicalKind::Block => Err(()),
         }
     }
 }
@@ -34,19 +70,32 @@ pub(crate) enum LexicalScopeKind {
     #[default]
     Symbol,
     Braced,
+    DefUse,
 }
 
 impl LexicalScopeKind {
     fn affect_symbol(&self) -> bool {
-        matches!(self, LexicalScopeKind::Symbol)
+        matches!(self, Self::DefUse | Self::Symbol)
+    }
+
+    fn affect_ref(&self) -> bool {
+        matches!(self, Self::DefUse)
+    }
+
+    fn affect_markup(&self) -> bool {
+        matches!(self, Self::Braced)
     }
 
     fn affect_block(&self) -> bool {
-        matches!(self, LexicalScopeKind::Braced)
+        matches!(self, Self::DefUse | Self::Braced)
     }
 
     fn affect_expr(&self) -> bool {
-        matches!(self, LexicalScopeKind::Braced)
+        matches!(self, Self::Braced)
+    }
+
+    fn affect_heading(&self) -> bool {
+        matches!(self, Self::Symbol | Self::Braced)
     }
 }
 
@@ -60,138 +109,301 @@ pub(crate) struct LexicalInfo {
 #[derive(Debug, Clone, Hash)]
 pub(crate) struct LexicalHierarchy {
     pub info: LexicalInfo,
-    pub children: Option<comemo::Prehashed<EcoVec<LexicalHierarchy>>>,
+    pub children: Option<LazyHash<EcoVec<LexicalHierarchy>>>,
 }
 
-pub(crate) fn get_lexical_hierarchy(
-    source: Source,
-    g: LexicalScopeKind,
-) -> Option<EcoVec<LexicalHierarchy>> {
-    fn symbreak(sym: LexicalInfo, curr: EcoVec<LexicalHierarchy>) -> LexicalHierarchy {
-        LexicalHierarchy {
-            info: sym,
-            children: if curr.is_empty() {
-                None
-            } else {
-                Some(comemo::Prehashed::new(curr))
-            },
+impl Serialize for LexicalHierarchy {
+    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        use serde::ser::SerializeStruct;
+        let mut state = serializer.serialize_struct("LexicalHierarchy", 2)?;
+        state.serialize_field("name", &self.info.name)?;
+        state.serialize_field("kind", &self.info.kind)?;
+        state.serialize_field("range", &self.info.range)?;
+        if let Some(children) = &self.children {
+            state.serialize_field("children", children.deref())?;
         }
+        state.end()
     }
+}
+
+impl<'de> Deserialize<'de> for LexicalHierarchy {
+    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
+        use serde::de::MapAccess;
+        struct LexicalHierarchyVisitor;
+        impl<'de> serde::de::Visitor<'de> for LexicalHierarchyVisitor {
+            type Value = LexicalHierarchy;
 
-    #[derive(Default)]
-    struct LexicalHierarchyWorker {
-        g: LexicalScopeKind,
-        stack: Vec<(LexicalInfo, EcoVec<LexicalHierarchy>)>,
+            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
+                formatter.write_str("a struct")
+            }
+
+            fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
+                let mut name = None;
+                let mut kind = None;
+                let mut range = None;
+                let mut children = None;
+                while let Some(key) = map.next_key()? {
+                    match key {
+                        "name" => name = Some(map.next_value()?),
+                        "kind" => kind = Some(map.next_value()?),
+                        "range" => range = Some(map.next_value()?),
+                        "children" => children = Some(map.next_value()?),
+                        _ => {}
+                    }
+                }
+                let name = name.ok_or_else(|| serde::de::Error::missing_field("name"))?;
+                let kind = kind.ok_or_else(|| serde::de::Error::missing_field("kind"))?;
+                let range = range.ok_or_else(|| serde::de::Error::missing_field("range"))?;
+                Ok(LexicalHierarchy {
+                    info: LexicalInfo { name, kind, range },
+                    children: children.map(LazyHash::new),
+                })
+            }
+        }
+
+        deserializer.deserialize_struct(
+            "LexicalHierarchy",
+            &["name", "kind", "range", "children"],
+            LexicalHierarchyVisitor,
+        )
     }
+}
 
-    impl LexicalHierarchyWorker {
-        fn symbreak(&mut self) {
-            let (symbol, children) = self.stack.pop().unwrap();
-            let current = &mut self.stack.last_mut().unwrap().1;
+#[derive(Debug, Clone, Copy, Hash, Default, PartialEq, Eq)]
+enum IdentContext {
+    #[default]
+    Ref,
+    Func,
+    Var,
+    Params,
+}
+
+#[derive(Default)]
+struct LexicalHierarchyWorker {
+    g: LexicalScopeKind,
+    stack: Vec<(LexicalInfo, EcoVec<LexicalHierarchy>)>,
+    ident_context: IdentContext,
+}
+
+impl LexicalHierarchyWorker {
+    fn symbreak(&mut self) {
+        let (symbol, children) = self.stack.pop().unwrap();
+        let current = &mut self.stack.last_mut().unwrap().1;
+
+        current.push(symbreak(symbol, children));
+    }
 
-            current.push(symbreak(symbol, children));
+    fn enter_symbol_context(&mut self, node: &LinkedNode) -> anyhow::Result<IdentContext> {
+        let checkpoint = self.ident_context;
+        match node.kind() {
+            SyntaxKind::RefMarker => self.ident_context = IdentContext::Ref,
+            SyntaxKind::LetBinding => self.ident_context = IdentContext::Ref,
+            SyntaxKind::Closure => self.ident_context = IdentContext::Func,
+            SyntaxKind::Params => self.ident_context = IdentContext::Params,
+            _ => {}
         }
 
-        /// Get all symbols for a node recursively.
-        fn get_symbols(&mut self, node: LinkedNode) -> anyhow::Result<()> {
-            let own_symbol = get_ident(&node, self.g)?;
-
-            if let Some(symbol) = own_symbol {
-                if let LexicalKind::Namespace(level) = symbol.kind {
-                    'heading_break: while let Some((w, _)) = self.stack.last() {
-                        match w.kind {
-                            LexicalKind::Namespace(l) if l < level => break 'heading_break,
-                            LexicalKind::Block => break 'heading_break,
-                            _ if self.stack.len() <= 1 => break 'heading_break,
-                            _ => {}
-                        }
+        Ok(checkpoint)
+    }
+
+    fn exit_symbol_context(&mut self, checkpoint: IdentContext) -> anyhow::Result<()> {
+        self.ident_context = checkpoint;
+        Ok(())
+    }
+
+    /// Get all symbols for a node recursively.
+    fn get_symbols(&mut self, node: LinkedNode) -> anyhow::Result<()> {
+        let own_symbol = self.get_ident(&node)?;
 
-                        self.symbreak();
+        let checkpoint = self.enter_symbol_context(&node)?;
+
+        if let Some(symbol) = own_symbol {
+            if let LexicalKind::Namespace(level) = symbol.kind {
+                'heading_break: while let Some((w, _)) = self.stack.last() {
+                    match w.kind {
+                        LexicalKind::Namespace(l) if l < level => break 'heading_break,
+                        LexicalKind::Block => break 'heading_break,
+                        _ if self.stack.len() <= 1 => break 'heading_break,
+                        _ => {}
                     }
+
+                    self.symbreak();
                 }
-                let is_heading = matches!(symbol.kind, LexicalKind::Namespace(..));
+            }
+            let is_heading = matches!(symbol.kind, LexicalKind::Namespace(..));
+
+            self.stack.push((symbol, eco_vec![]));
+            let stack_height = self.stack.len();
 
-                self.stack.push((symbol, eco_vec![]));
-                let stack_height = self.stack.len();
+            for child in node.children() {
+                self.get_symbols(child)?;
+            }
 
-                for child in node.children() {
-                    self.get_symbols(child)?;
+            if is_heading {
+                while stack_height < self.stack.len() {
+                    self.symbreak();
+                }
+            } else {
+                while stack_height <= self.stack.len() {
+                    self.symbreak();
                 }
+            }
+        } else {
+            match node.kind() {
+                SyntaxKind::LetBinding => 'let_binding: {
+                    let name = node.children().find(|n| n.cast::<ast::Pattern>().is_some());
+
+                    if let Some(name) = &name {
+                        let p = name.cast::<ast::Pattern>().unwrap();
 
-                if is_heading {
-                    while stack_height < self.stack.len() {
-                        self.symbreak();
+                        // special case
+                        if matches!(p, ast::Pattern::Normal(ast::Expr::Closure(..))) {
+                            self.get_symbols_with(name.clone(), IdentContext::Ref)?;
+                            break 'let_binding;
+                        }
                     }
-                } else {
-                    while stack_height <= self.stack.len() {
-                        self.symbreak();
+
+                    // reverse order for correct symbol affection
+                    if self.g == LexicalScopeKind::DefUse {
+                        self.get_symbols_in_first_expr(node.children().rev())?;
+                        if let Some(name) = name {
+                            self.get_symbols_with(name, IdentContext::Var)?;
+                        }
+                    } else {
+                        if let Some(name) = name {
+                            self.get_symbols_with(name, IdentContext::Var)?;
+                        }
+                        self.get_symbols_in_first_expr(node.children().rev())?;
                     }
                 }
-            } else {
-                for child in node.children() {
-                    self.get_symbols(child)?;
+                SyntaxKind::Closure => {
+                    let n = node.children().next();
+                    if let Some(n) = n {
+                        if n.kind() == SyntaxKind::Ident {
+                            self.get_symbols_with(n, IdentContext::Func)?;
+                        }
+                    }
+                    if self.g == LexicalScopeKind::DefUse {
+                        let param = node.children().find(|n| n.kind() == SyntaxKind::Params);
+                        if let Some(param) = param {
+                            self.get_symbols_with(param, IdentContext::Params)?;
+                        }
+                    }
+                    let body = node
+                        .children()
+                        .rev()
+                        .find(|n| n.cast::<ast::Expr>().is_some());
+                    if let Some(body) = body {
+                        if self.g == LexicalScopeKind::DefUse {
+                            let symbol = LexicalInfo {
+                                name: String::new(),
+                                kind: LexicalKind::Block,
+                                range: body.range(),
+                            };
+                            self.stack.push((symbol, eco_vec![]));
+                            let stack_height = self.stack.len();
+                            self.get_symbols_with(body, IdentContext::Ref)?;
+                            while stack_height <= self.stack.len() {
+                                self.symbreak();
+                            }
+                        } else {
+                            self.get_symbols_with(body, IdentContext::Ref)?;
+                        }
+                    }
+                }
+                SyntaxKind::FieldAccess => {
+                    self.get_symbols_in_first_expr(node.children())?;
+                }
+                SyntaxKind::Named => {
+                    if self.ident_context == IdentContext::Params {
+                        let ident = node.children().find(|n| n.kind() == SyntaxKind::Ident);
+                        if let Some(ident) = ident {
+                            self.get_symbols_with(ident, IdentContext::Var)?;
+                        }
+                    }
+
+                    self.get_symbols_in_first_expr(node.children().rev())?;
+                }
+                _ => {
+                    for child in node.children() {
+                        self.get_symbols(child)?;
+                    }
                 }
             }
+        }
+
+        self.exit_symbol_context(checkpoint)?;
+
+        Ok(())
+    }
 
-            Ok(())
+    fn get_symbols_in_first_expr<'a>(
+        &mut self,
+        mut nodes: impl Iterator<Item = LinkedNode<'a>>,
+    ) -> anyhow::Result<()> {
+        let body = nodes.find(|n| n.cast::<ast::Expr>().is_some());
+        if let Some(body) = body {
+            self.get_symbols_with(body, IdentContext::Ref)?;
         }
+
+        Ok(())
+    }
+
+    fn get_symbols_with(&mut self, node: LinkedNode, context: IdentContext) -> anyhow::Result<()> {
+        let c = self.ident_context;
+        self.ident_context = context;
+
+        let res = self.get_symbols(node);
+
+        self.ident_context = c;
+        res
     }
 
     /// Get symbol for a leaf node of a valid type, or `None` if the node is an
     /// invalid type.
     #[allow(deprecated)]
-    fn get_ident(node: &LinkedNode, g: LexicalScopeKind) -> anyhow::Result<Option<LexicalInfo>> {
+    fn get_ident(&self, node: &LinkedNode) -> anyhow::Result<Option<LexicalInfo>> {
         let (name, kind) = match node.kind() {
-            SyntaxKind::Label if g.affect_symbol() => {
+            SyntaxKind::Label if self.g.affect_symbol() => {
                 let ast_node = node
                     .cast::<ast::Label>()
                     .ok_or_else(|| anyhow!("cast to ast node failed: {:?}", node))?;
                 let name = ast_node.get().to_string();
 
-                (name, LexicalKind::Constant)
+                (name, LexicalKind::Label)
+            }
+            SyntaxKind::RefMarker if self.g.affect_ref() => {
+                let name = node.text().trim_start_matches('@').to_owned();
+                (name, LexicalKind::LabelRef)
             }
-            SyntaxKind::Ident if g.affect_symbol() => {
+            SyntaxKind::Ident if self.g.affect_symbol() => {
                 let ast_node = node
                     .cast::<ast::Ident>()
                     .ok_or_else(|| anyhow!("cast to ast node failed: {:?}", node))?;
                 let name = ast_node.get().to_string();
-                let Some(parent) = node.parent() else {
-                    return Ok(None);
-                };
-                let kind = match parent.kind() {
-                    // for variable definitions, the Let binding holds an Ident
-                    SyntaxKind::LetBinding => LexicalKind::Variable,
-                    // for function definitions, the Let binding holds a Closure which holds the
-                    // Ident
-                    SyntaxKind::Closure => {
-                        let Some(grand_parent) = parent.parent() else {
-                            return Ok(None);
-                        };
-                        match grand_parent.kind() {
-                            SyntaxKind::LetBinding => LexicalKind::Function,
-                            _ => return Ok(None),
-                        }
-                    }
+                let kind = match self.ident_context {
+                    IdentContext::Ref if self.g.affect_ref() => LexicalKind::ValRef,
+                    IdentContext::Func => LexicalKind::Function,
+                    IdentContext::Var | IdentContext::Params => LexicalKind::Variable,
                     _ => return Ok(None),
                 };
 
                 (name, kind)
             }
-            SyntaxKind::Equation
-            | SyntaxKind::Raw
-            | SyntaxKind::CodeBlock
-            | SyntaxKind::ContentBlock
-            | SyntaxKind::BlockComment
-                if g.affect_block() =>
+            SyntaxKind::Equation | SyntaxKind::Raw | SyntaxKind::BlockComment
+                if self.g.affect_markup() =>
             {
                 (String::new(), LexicalKind::Block)
             }
+            SyntaxKind::CodeBlock | SyntaxKind::ContentBlock if self.g.affect_block() => {
+                (String::new(), LexicalKind::Block)
+            }
             SyntaxKind::Parenthesized
             | SyntaxKind::Destructuring
             | SyntaxKind::Args
             | SyntaxKind::Array
             | SyntaxKind::Dict
-                if g.affect_expr() =>
+                if self.g.affect_expr() =>
             {
                 (String::new(), LexicalKind::Block)
             }
@@ -204,7 +416,7 @@ pub(crate) fn get_lexical_hierarchy(
                     return Ok(None);
                 };
                 let kind = match parent.kind() {
-                    SyntaxKind::Heading => LexicalKind::Namespace(
+                    SyntaxKind::Heading if self.g.affect_heading() => LexicalKind::Namespace(
                         parent.cast::<ast::Heading>().unwrap().depth().get() as i16,
                     ),
                     _ => return Ok(None),
@@ -221,29 +433,15 @@ pub(crate) fn get_lexical_hierarchy(
             range: node.range(),
         }))
     }
+}
 
-    let b = std::time::Instant::now();
-    let root = LinkedNode::new(source.root());
-
-    let mut worker = LexicalHierarchyWorker {
-        g,
-        ..LexicalHierarchyWorker::default()
-    };
-    worker.stack.push((
-        LexicalInfo {
-            name: "deadbeef".to_string(),
-            kind: LexicalKind::Namespace(-1),
-            range: 0..0,
+fn symbreak(sym: LexicalInfo, curr: EcoVec<LexicalHierarchy>) -> LexicalHierarchy {
+    LexicalHierarchy {
+        info: sym,
+        children: if curr.is_empty() {
+            None
+        } else {
+            Some(LazyHash::new(curr))
         },
-        eco_vec![],
-    ));
-    let res = worker.get_symbols(root).ok();
-
-    while worker.stack.len() > 1 {
-        worker.symbreak();
     }
-
-    let e = std::time::Instant::now();
-    info!("lexical hierarchy analysis took {:?}", e - b);
-    res.map(|_| worker.stack.pop().unwrap().1)
 }
diff --git a/crates/tinymist-query/src/analysis/mod.rs b/crates/tinymist-query/src/analysis/mod.rs
deleted file mode 100644
index 4bf52d6cd..000000000
--- a/crates/tinymist-query/src/analysis/mod.rs
+++ /dev/null
@@ -1,10 +0,0 @@
-pub mod track_values;
-pub use track_values::*;
-pub mod lexical_hierarchy;
-pub(crate) use lexical_hierarchy::*;
-pub mod definition;
-pub use definition::*;
-pub mod import;
-pub use import::*;
-pub mod reference;
-pub use reference::*;
diff --git a/crates/tinymist-query/src/fixtures/document_symbols/func.typ b/crates/tinymist-query/src/fixtures/document_symbols/func.typ
new file mode 100644
index 000000000..47a753137
--- /dev/null
+++ b/crates/tinymist-query/src/fixtures/document_symbols/func.typ
@@ -0,0 +1 @@
+#let f(a) = a;
\ No newline at end of file
diff --git a/crates/tinymist-query/src/fixtures/document_symbols/test@func.typ.snap b/crates/tinymist-query/src/fixtures/document_symbols/test@func.typ.snap
new file mode 100644
index 000000000..2f62641f8
--- /dev/null
+++ b/crates/tinymist-query/src/fixtures/document_symbols/test@func.typ.snap
@@ -0,0 +1,13 @@
+---
+source: crates/tinymist-query/src/document_symbol.rs
+expression: "JsonRepr::new_redacted(result.unwrap(), &REDACT_LOC)"
+input_file: crates/tinymist-query/src/fixtures/document_symbols/func.typ
+---
+[
+ {
+  "kind": 12,
+  "name": "f",
+  "range": "0:5:0:6",
+  "selectionRange": "0:5:0:6"
+ }
+]
diff --git a/crates/tinymist-query/src/fixtures/folding_range/test@paren_folding.typ.snap b/crates/tinymist-query/src/fixtures/folding_range/test@paren_folding.typ.snap
index df324b33a..80d1bd067 100644
--- a/crates/tinymist-query/src/fixtures/folding_range/test@paren_folding.typ.snap
+++ b/crates/tinymist-query/src/fixtures/folding_range/test@paren_folding.typ.snap
@@ -3,4 +3,12 @@ source: crates/tinymist-query/src/folding_range.rs
 expression: "JsonRepr::new_pure(result.unwrap())"
 input_file: crates/tinymist-query/src/fixtures/folding_range/paren_folding.typ
 ---
-[]
+[
+ {
+  "collapsedText": "",
+  "endCharacter": 1,
+  "endLine": 2,
+  "startCharacter": 1,
+  "startLine": 0
+ }
+]
diff --git a/crates/tinymist-query/src/fixtures/lexical_hierarchy/base.typ b/crates/tinymist-query/src/fixtures/lexical_hierarchy/base.typ
new file mode 100644
index 000000000..b93f9586b
--- /dev/null
+++ b/crates/tinymist-query/src/fixtures/lexical_hierarchy/base.typ
@@ -0,0 +1,3 @@
+// most simple def-use case
+#let x = 1;
+#x
\ No newline at end of file
diff --git a/crates/tinymist-query/src/fixtures/lexical_hierarchy/destructing.typ b/crates/tinymist-query/src/fixtures/lexical_hierarchy/destructing.typ
new file mode 100644
index 000000000..f52a413a4
--- /dev/null
+++ b/crates/tinymist-query/src/fixtures/lexical_hierarchy/destructing.typ
@@ -0,0 +1,2 @@
+#let (a, b) = (1, 1);
+#let (a, b) = (b, a);
\ No newline at end of file
diff --git a/crates/tinymist-query/src/fixtures/lexical_hierarchy/dict.typ b/crates/tinymist-query/src/fixtures/lexical_hierarchy/dict.typ
new file mode 100644
index 000000000..eda7ed9c6
--- /dev/null
+++ b/crates/tinymist-query/src/fixtures/lexical_hierarchy/dict.typ
@@ -0,0 +1,5 @@
+#let z = 1;
+#let x = (
+  y: z,
+  "1 2": z,
+)
\ No newline at end of file
diff --git a/crates/tinymist-query/src/fixtures/lexical_hierarchy/func.typ b/crates/tinymist-query/src/fixtures/lexical_hierarchy/func.typ
new file mode 100644
index 000000000..a2140d57a
--- /dev/null
+++ b/crates/tinymist-query/src/fixtures/lexical_hierarchy/func.typ
@@ -0,0 +1,2 @@
+#let x = 1;
+#let f(a) = a;
\ No newline at end of file
diff --git a/crates/tinymist-query/src/fixtures/lexical_hierarchy/redefine.typ b/crates/tinymist-query/src/fixtures/lexical_hierarchy/redefine.typ
new file mode 100644
index 000000000..7e0744d35
--- /dev/null
+++ b/crates/tinymist-query/src/fixtures/lexical_hierarchy/redefine.typ
@@ -0,0 +1,2 @@
+#let x = 1;
+#let x = x;
\ No newline at end of file
diff --git a/crates/tinymist-query/src/fixtures/lexical_hierarchy/scope@base.typ.snap b/crates/tinymist-query/src/fixtures/lexical_hierarchy/scope@base.typ.snap
new file mode 100644
index 000000000..42e2b0716
--- /dev/null
+++ b/crates/tinymist-query/src/fixtures/lexical_hierarchy/scope@base.typ.snap
@@ -0,0 +1,17 @@
+---
+source: crates/tinymist-query/src/analysis.rs
+expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
+input_file: crates/tinymist-query/src/fixtures/lexical_hierarchy/base.typ
+---
+[
+ {
+  "kind": "Variable",
+  "name": "x",
+  "range": "5:6"
+ },
+ {
+  "kind": "ValRef",
+  "name": "x",
+  "range": "14:15"
+ }
+]
diff --git a/crates/tinymist-query/src/fixtures/lexical_hierarchy/scope@destructing.typ.snap b/crates/tinymist-query/src/fixtures/lexical_hierarchy/scope@destructing.typ.snap
new file mode 100644
index 000000000..5e142da41
--- /dev/null
+++ b/crates/tinymist-query/src/fixtures/lexical_hierarchy/scope@destructing.typ.snap
@@ -0,0 +1,37 @@
+---
+source: crates/tinymist-query/src/analysis.rs
+expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
+input_file: crates/tinymist-query/src/fixtures/lexical_hierarchy/destructing.typ
+---
+[
+ {
+  "kind": "Variable",
+  "name": "a",
+  "range": "6:7"
+ },
+ {
+  "kind": "Variable",
+  "name": "b",
+  "range": "9:10"
+ },
+ {
+  "kind": "ValRef",
+  "name": "b",
+  "range": "38:39"
+ },
+ {
+  "kind": "ValRef",
+  "name": "a",
+  "range": "41:42"
+ },
+ {
+  "kind": "Variable",
+  "name": "a",
+  "range": "29:30"
+ },
+ {
+  "kind": "Variable",
+  "name": "b",
+  "range": "32:33"
+ }
+]
diff --git a/crates/tinymist-query/src/fixtures/lexical_hierarchy/scope@dict.typ.snap b/crates/tinymist-query/src/fixtures/lexical_hierarchy/scope@dict.typ.snap
new file mode 100644
index 000000000..af5aaed17
--- /dev/null
+++ b/crates/tinymist-query/src/fixtures/lexical_hierarchy/scope@dict.typ.snap
@@ -0,0 +1,27 @@
+---
+source: crates/tinymist-query/src/analysis.rs
+expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
+input_file: crates/tinymist-query/src/fixtures/lexical_hierarchy/dict.typ
+---
+[
+ {
+  "kind": "Variable",
+  "name": "z",
+  "range": "5:6"
+ },
+ {
+  "kind": "ValRef",
+  "name": "z",
+  "range": "30:31"
+ },
+ {
+  "kind": "ValRef",
+  "name": "z",
+  "range": "43:44"
+ },
+ {
+  "kind": "Variable",
+  "name": "x",
+  "range": "18:19"
+ }
+]
diff --git a/crates/tinymist-query/src/fixtures/lexical_hierarchy/scope@func.typ.snap b/crates/tinymist-query/src/fixtures/lexical_hierarchy/scope@func.typ.snap
new file mode 100644
index 000000000..371e9070b
--- /dev/null
+++ b/crates/tinymist-query/src/fixtures/lexical_hierarchy/scope@func.typ.snap
@@ -0,0 +1,34 @@
+---
+source: crates/tinymist-query/src/analysis.rs
+expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
+input_file: crates/tinymist-query/src/fixtures/lexical_hierarchy/func.typ
+---
+[
+ {
+  "kind": "Variable",
+  "name": "x",
+  "range": "5:6"
+ },
+ {
+  "kind": "Function",
+  "name": "f",
+  "range": "18:19"
+ },
+ {
+  "kind": "Variable",
+  "name": "a",
+  "range": "20:21"
+ },
+ {
+  "children": [
+   {
+    "kind": "ValRef",
+    "name": "a",
+    "range": "25:26"
+   }
+  ],
+  "kind": "Block",
+  "name": "",
+  "range": "25:26"
+ }
+]
diff --git a/crates/tinymist-query/src/fixtures/lexical_hierarchy/scope@redefine.typ.snap b/crates/tinymist-query/src/fixtures/lexical_hierarchy/scope@redefine.typ.snap
new file mode 100644
index 000000000..7bb763fbc
--- /dev/null
+++ b/crates/tinymist-query/src/fixtures/lexical_hierarchy/scope@redefine.typ.snap
@@ -0,0 +1,22 @@
+---
+source: crates/tinymist-query/src/analysis.rs
+expression: "JsonRepr::new_redacted(result, &REDACT_LOC)"
+input_file: crates/tinymist-query/src/fixtures/lexical_hierarchy/redefine.typ
+---
+[
+ {
+  "kind": "Variable",
+  "name": "x",
+  "range": "5:6"
+ },
+ {
+  "kind": "ValRef",
+  "name": "x",
+  "range": "22:23"
+ },
+ {
+  "kind": "Variable",
+  "name": "x",
+  "range": "18:19"
+ }
+]
diff --git a/crates/tinymist-query/src/lib.rs b/crates/tinymist-query/src/lib.rs
index 022478bc3..0d64bb028 100644
--- a/crates/tinymist-query/src/lib.rs
+++ b/crates/tinymist-query/src/lib.rs
@@ -209,16 +209,18 @@ mod tests {
         for (i, source) in sources.enumerate() {
             // find prelude
             let mut source = source.trim();
-            let path = if source.starts_with("//") {
+            let mut path = None;
+
+            if source.starts_with("//") {
                 let first_line = source.lines().next().unwrap();
                 source = source.strip_prefix(first_line).unwrap().trim();
 
                 let content = first_line.strip_prefix("//").unwrap().trim();
-                content.strip_prefix("path:").unwrap().trim().to_owned()
-            } else {
-                format!("/source{i}.typ")
+                path = content.strip_prefix("path:").map(|e| e.trim().to_owned())
             };
 
+            let path = path.unwrap_or_else(|| format!("/source{i}.typ"));
+
             let pw = root.join(Path::new(&path));
             world
                 .map_shadow(&pw, Bytes::from(source.as_bytes()))
@@ -316,7 +318,11 @@ mod tests {
     }
 
     fn pos(v: &Value) -> String {
-        format!("{}:{}", v["line"], v["character"])
+        match v {
+            Value::Object(v) => format!("{}:{}", v["line"], v["character"]),
+            Value::Number(v) => v.to_string(),
+            _ => "<null>".to_owned(),
+        }
     }
 
     impl Redact for RedactFields {