feat: analyze lexical hierarchy for def-use relations (#17)

This commit is contained in:
Myriad-Dreamin 2024-03-12 12:24:53 +08:00 committed by GitHub
parent f8194c76b0
commit ee131ac68a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 637 additions and 230 deletions

View file

@ -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));
});
}
}

View file

@ -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);
}

View file

@ -1,227 +1,19 @@
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) enum LexicalKind {
Namespace(i16),
Variable,
Function,
Constant,
Block,
}
impl TryFrom<LexicalKind> for SymbolKind {
type Error = ();
fn try_from(value: LexicalKind) -> Result<Self, Self::Error> {
match value {
LexicalKind::Namespace(..) => Ok(SymbolKind::NAMESPACE),
LexicalKind::Variable => Ok(SymbolKind::VARIABLE),
LexicalKind::Function => Ok(SymbolKind::FUNCTION),
LexicalKind::Constant => Ok(SymbolKind::CONSTANT),
LexicalKind::Block => Err(()),
}
}
}
#[derive(Debug, Clone, Copy, Hash, Default, PartialEq, Eq)]
pub(crate) enum LexicalScopeKind {
#[default]
Symbol,
Braced,
}
impl LexicalScopeKind {
fn affect_symbol(&self) -> bool {
matches!(self, LexicalScopeKind::Symbol)
}
fn affect_block(&self) -> bool {
matches!(self, LexicalScopeKind::Braced)
}
fn affect_expr(&self) -> bool {
matches!(self, LexicalScopeKind::Braced)
}
}
#[derive(Debug, Clone, Hash)]
pub(crate) struct LexicalInfo {
pub name: String,
pub kind: LexicalKind,
pub range: Range<usize>,
}
#[derive(Debug, Clone, Hash)]
pub(crate) struct LexicalHierarchy {
pub info: LexicalInfo,
pub children: Option<comemo::Prehashed<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))
},
}
}
#[derive(Default)]
struct LexicalHierarchyWorker {
g: LexicalScopeKind,
stack: Vec<(LexicalInfo, EcoVec<LexicalHierarchy>)>,
}
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));
}
/// 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,
_ => {}
}
self.symbreak();
}
}
let is_heading = matches!(symbol.kind, LexicalKind::Namespace(..));
self.stack.push((symbol, eco_vec![]));
let stack_height = self.stack.len();
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 {
for child in node.children() {
self.get_symbols(child)?;
}
}
Ok(())
}
}
/// 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>> {
let (name, kind) = match node.kind() {
SyntaxKind::Label if 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)
}
SyntaxKind::Ident if 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),
}
}
_ => return Ok(None),
};
(name, kind)
}
SyntaxKind::Equation
| SyntaxKind::Raw
| SyntaxKind::CodeBlock
| SyntaxKind::ContentBlock
| SyntaxKind::BlockComment
if g.affect_block() =>
{
(String::new(), LexicalKind::Block)
}
SyntaxKind::Parenthesized
| SyntaxKind::Destructuring
| SyntaxKind::Args
| SyntaxKind::Array
| SyntaxKind::Dict
if g.affect_expr() =>
{
(String::new(), LexicalKind::Block)
}
SyntaxKind::Markup => {
let name = node.get().to_owned().into_text().to_string();
if name.is_empty() {
return Ok(None);
}
let Some(parent) = node.parent() else {
return Ok(None);
};
let kind = match parent.kind() {
SyntaxKind::Heading => LexicalKind::Namespace(
parent.cast::<ast::Heading>().unwrap().depth().get() as i16,
),
_ => return Ok(None),
};
(name, kind)
}
_ => return Ok(None),
};
Ok(Some(LexicalInfo {
name,
kind,
range: node.range(),
}))
}
let b = std::time::Instant::now();
let root = LinkedNode::new(source.root());
@ -247,3 +39,409 @@ pub(crate) fn get_lexical_hierarchy(
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,
Label,
Block,
}
impl TryFrom<LexicalKind> for SymbolKind {
type Error = ();
fn try_from(value: LexicalKind) -> Result<Self, Self::Error> {
match value {
LexicalKind::Namespace(..) => Ok(SymbolKind::NAMESPACE),
LexicalKind::Variable => Ok(SymbolKind::VARIABLE),
LexicalKind::Function => Ok(SymbolKind::FUNCTION),
LexicalKind::Label => Ok(SymbolKind::CONSTANT),
LexicalKind::ValRef | LexicalKind::LabelRef | LexicalKind::Block => Err(()),
}
}
}
#[derive(Debug, Clone, Copy, Hash, Default, PartialEq, Eq)]
pub(crate) enum LexicalScopeKind {
#[default]
Symbol,
Braced,
DefUse,
}
impl LexicalScopeKind {
fn affect_symbol(&self) -> bool {
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, Self::DefUse | Self::Braced)
}
fn affect_expr(&self) -> bool {
matches!(self, Self::Braced)
}
fn affect_heading(&self) -> bool {
matches!(self, Self::Symbol | Self::Braced)
}
}
#[derive(Debug, Clone, Hash)]
pub(crate) struct LexicalInfo {
pub name: String,
pub kind: LexicalKind,
pub range: Range<usize>,
}
#[derive(Debug, Clone, Hash)]
pub(crate) struct LexicalHierarchy {
pub info: LexicalInfo,
pub children: Option<LazyHash<EcoVec<LexicalHierarchy>>>,
}
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;
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,
)
}
}
#[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));
}
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,
_ => {}
}
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)?;
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(..));
self.stack.push((symbol, eco_vec![]));
let stack_height = self.stack.len();
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();
// special case
if matches!(p, ast::Pattern::Normal(ast::Expr::Closure(..))) {
self.get_symbols_with(name.clone(), IdentContext::Ref)?;
break 'let_binding;
}
}
// 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())?;
}
}
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(())
}
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(&self, node: &LinkedNode) -> anyhow::Result<Option<LexicalInfo>> {
let (name, kind) = match node.kind() {
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::Label)
}
SyntaxKind::RefMarker if self.g.affect_ref() => {
let name = node.text().trim_start_matches('@').to_owned();
(name, LexicalKind::LabelRef)
}
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 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::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 self.g.affect_expr() =>
{
(String::new(), LexicalKind::Block)
}
SyntaxKind::Markup => {
let name = node.get().to_owned().into_text().to_string();
if name.is_empty() {
return Ok(None);
}
let Some(parent) = node.parent() else {
return Ok(None);
};
let kind = match parent.kind() {
SyntaxKind::Heading if self.g.affect_heading() => LexicalKind::Namespace(
parent.cast::<ast::Heading>().unwrap().depth().get() as i16,
),
_ => return Ok(None),
};
(name, kind)
}
_ => return Ok(None),
};
Ok(Some(LexicalInfo {
name,
kind,
range: node.range(),
}))
}
}
fn symbreak(sym: LexicalInfo, curr: EcoVec<LexicalHierarchy>) -> LexicalHierarchy {
LexicalHierarchy {
info: sym,
children: if curr.is_empty() {
None
} else {
Some(LazyHash::new(curr))
},
}
}

View file

@ -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::*;

View file

@ -0,0 +1 @@
#let f(a) = a;

View file

@ -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"
}
]

View file

@ -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
}
]

View file

@ -0,0 +1,3 @@
// most simple def-use case
#let x = 1;
#x

View file

@ -0,0 +1,2 @@
#let (a, b) = (1, 1);
#let (a, b) = (b, a);

View file

@ -0,0 +1,5 @@
#let z = 1;
#let x = (
y: z,
"1 2": z,
)

View file

@ -0,0 +1,2 @@
#let x = 1;
#let f(a) = a;

View file

@ -0,0 +1,2 @@
#let x = 1;
#let x = x;

View file

@ -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"
}
]

View file

@ -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"
}
]

View file

@ -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"
}
]

View file

@ -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"
}
]

View file

@ -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"
}
]

View file

@ -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 {