From ca6edf5283c258d8410134d678347977cb273cdd Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sat, 11 Mar 2023 23:28:35 +0100 Subject: [PATCH] Jump to source and preview --- library/src/layout/par.rs | 59 ++++++++++++++++++----- library/src/math/fragment.rs | 2 + library/src/text/shaping.rs | 44 +++++++++++++++-- src/doc.rs | 9 +++- src/eval/func.rs | 6 ++- src/ide/jump.rs | 93 ++++++++++++++++++++++++++++++++++++ src/ide/mod.rs | 2 + src/model/content.rs | 6 ++- src/syntax/node.rs | 75 +++++++++++++---------------- src/syntax/source.rs | 15 ++++-- 10 files changed, 245 insertions(+), 66 deletions(-) create mode 100644 src/ide/jump.rs diff --git a/library/src/layout/par.rs b/library/src/layout/par.rs index 8dd81d29b..6178c0599 100644 --- a/library/src/layout/par.rs +++ b/library/src/layout/par.rs @@ -145,12 +145,12 @@ impl ParNode { let children = par.children(); // Collect all text into one string for BiDi analysis. - let (text, segments) = collect(&children, &styles, consecutive)?; + let (text, segments, spans) = collect(&children, &styles, consecutive)?; // Perform BiDi analysis and then prepare paragraph layout by building a // representation on which we can do line breaking without layouting // each and every line from scratch. - let p = prepare(&mut vt, &children, &text, segments, styles, region)?; + let p = prepare(&mut vt, &children, &text, segments, spans, styles, region)?; // Break the paragraph into lines. let lines = linebreak(&vt, &p, region.x); @@ -264,6 +264,8 @@ struct Preparation<'a> { bidi: BidiInfo<'a>, /// Text runs, spacing and layouted nodes. items: Vec>, + /// The span mapper. + spans: SpanMapper, /// The styles shared by all children. styles: StyleChain<'a>, /// Whether to hyphenate if it's the same for all children. @@ -388,6 +390,35 @@ impl<'a> Item<'a> { } } +/// Maps byte offsets back to spans. +pub struct SpanMapper(Vec<(usize, Span)>); + +impl SpanMapper { + /// Create a new span mapper. + pub fn new() -> Self { + Self(vec![]) + } + + /// Push a span for a segment with the given length. + pub fn push(&mut self, len: usize, span: Span) { + self.0.push((len, span)); + } + + /// Determine the span at the given byte offset. + /// + /// May return a detached span. + pub fn span_at(&self, offset: usize) -> (Span, u16) { + let mut cursor = 0; + for &(len, span) in &self.0 { + if (cursor..=cursor + len).contains(&offset) { + return (span, u16::try_from(offset - cursor).unwrap_or(0)); + } + cursor += len; + } + (Span::detached(), 0) + } +} + /// A layouted line, consisting of a sequence of layouted paragraph items that /// are mostly borrowed from the preparation phase. This type enables you to /// measure the size of a line in a range before comitting to building the @@ -485,10 +516,11 @@ fn collect<'a>( children: &'a [Content], styles: &'a StyleChain<'a>, consecutive: bool, -) -> SourceResult<(String, Vec<(Segment<'a>, StyleChain<'a>)>)> { +) -> SourceResult<(String, Vec<(Segment<'a>, StyleChain<'a>)>, SpanMapper)> { let mut full = String::new(); let mut quoter = Quoter::new(); let mut segments = vec![]; + let mut spans = SpanMapper::new(); let mut iter = children.iter().peekable(); if consecutive { @@ -578,6 +610,8 @@ fn collect<'a>( quoter.last(last); } + spans.push(segment.len(), child.span()); + if let (Some((Segment::Text(last_len), last_styles)), Segment::Text(len)) = (segments.last_mut(), segment) { @@ -590,7 +624,7 @@ fn collect<'a>( segments.push((segment, styles)); } - Ok((full, segments)) + Ok((full, segments, spans)) } /// Prepare paragraph layout by shaping the whole paragraph and layouting all @@ -600,6 +634,7 @@ fn prepare<'a>( children: &'a [Content], text: &'a str, segments: Vec<(Segment<'a>, StyleChain<'a>)>, + spans: SpanMapper, styles: StyleChain<'a>, region: Size, ) -> SourceResult> { @@ -620,7 +655,7 @@ fn prepare<'a>( let end = cursor + segment.len(); match segment { Segment::Text(_) => { - shape_range(&mut items, vt, &bidi, cursor..end, styles); + shape_range(&mut items, vt, &bidi, cursor..end, &spans, styles); } Segment::Spacing(spacing) => match spacing { Spacing::Rel(v) => { @@ -655,6 +690,7 @@ fn prepare<'a>( Ok(Preparation { bidi, items, + spans, styles, hyphenate: shared_get(styles, children, TextNode::hyphenate_in), lang: shared_get(styles, children, TextNode::lang_in), @@ -670,11 +706,12 @@ fn shape_range<'a>( vt: &Vt, bidi: &BidiInfo<'a>, range: Range, + spans: &SpanMapper, styles: StyleChain<'a>, ) { - let mut process = |text, level: BidiLevel| { + let mut process = |range: Range, level: BidiLevel| { let dir = if level.is_ltr() { Dir::LTR } else { Dir::RTL }; - let shaped = shape(vt, text, styles, dir); + let shaped = shape(vt, range.start, &bidi.text[range], spans, styles, dir); items.push(Item::Text(shaped)); }; @@ -694,7 +731,7 @@ fn shape_range<'a>( if level != prev_level || !is_compatible(script, prev_script) { if cursor < i { - process(&bidi.text[cursor..i], prev_level); + process(cursor..i, prev_level); } cursor = i; prev_level = level; @@ -704,7 +741,7 @@ fn shape_range<'a>( } } - process(&bidi.text[cursor..range.end], prev_level); + process(cursor..range.end, prev_level); } /// Whether this is not a specific script. @@ -1073,7 +1110,7 @@ fn line<'a>( if hyphen || start + shaped.text.len() > range.end { if hyphen || start < range.end || before.is_empty() { let shifted = start - base..range.end - base; - let mut reshaped = shaped.reshape(vt, shifted); + let mut reshaped = shaped.reshape(vt, &p.spans, shifted); if hyphen || shy { reshaped.push_hyphen(vt); } @@ -1096,7 +1133,7 @@ fn line<'a>( if range.start + shaped.text.len() > end { if range.start < end { let shifted = range.start - base..end - base; - let reshaped = shaped.reshape(vt, shifted); + let reshaped = shaped.reshape(vt, &p.spans, shifted); width += reshaped.width; first = Some(Item::Text(reshaped)); } diff --git a/library/src/math/fragment.rs b/library/src/math/fragment.rs index 73daa4b2c..de456b82f 100644 --- a/library/src/math/fragment.rs +++ b/library/src/math/fragment.rs @@ -222,6 +222,8 @@ impl GlyphFragment { c: self.c, x_advance: Em::from_length(self.width, self.font_size), x_offset: Em::zero(), + span: Span::detached(), + offset: 0, }], }; let size = Size::new(self.width, self.ascent + self.descent); diff --git a/library/src/text/shaping.rs b/library/src/text/shaping.rs index e7ce4027a..a96238d95 100644 --- a/library/src/text/shaping.rs +++ b/library/src/text/shaping.rs @@ -6,6 +6,7 @@ use typst::font::{Font, FontVariant}; use typst::util::SliceExt; use super::*; +use crate::layout::SpanMapper; use crate::prelude::*; /// The result of shaping text. @@ -14,6 +15,8 @@ use crate::prelude::*; /// measured, used to reshape substrings more quickly and converted into a /// frame. pub struct ShapedText<'a> { + /// The start of the text in the full paragraph. + pub base: usize, /// The text that was shaped. pub text: &'a str, /// The text direction. @@ -53,6 +56,10 @@ pub struct ShapedGlyph { pub safe_to_break: bool, /// The first char in this glyph's cluster. pub c: char, + /// The source code location of the text. + pub span: Span, + /// The offset within the spanned text. + pub offset: u16, } impl ShapedGlyph { @@ -110,6 +117,8 @@ impl<'a> ShapedText<'a> { }, x_offset: glyph.x_offset, c: glyph.c, + span: glyph.span, + offset: glyph.offset, }) .collect(); @@ -187,9 +196,15 @@ impl<'a> ShapedText<'a> { /// Reshape a range of the shaped text, reusing information from this /// shaping process if possible. - pub fn reshape(&'a self, vt: &Vt, text_range: Range) -> ShapedText<'a> { + pub fn reshape( + &'a self, + vt: &Vt, + spans: &SpanMapper, + text_range: Range, + ) -> ShapedText<'a> { if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) { Self { + base: self.base + text_range.start, text: &self.text[text_range], dir: self.dir, styles: self.styles, @@ -199,7 +214,14 @@ impl<'a> ShapedText<'a> { glyphs: Cow::Borrowed(glyphs), } } else { - shape(vt, &self.text[text_range], self.styles, self.dir) + shape( + vt, + self.base + text_range.start, + &self.text[text_range], + spans, + self.styles, + self.dir, + ) } } @@ -225,6 +247,8 @@ impl<'a> ShapedText<'a> { cluster, safe_to_break: true, c: '-', + span: Span::detached(), + offset: 0, }); Some(()) }); @@ -298,6 +322,8 @@ impl Debug for ShapedText<'_> { /// Holds shaping results and metadata common to all shaped segments. struct ShapingContext<'a> { vt: &'a Vt<'a>, + base: usize, + spans: &'a SpanMapper, glyphs: Vec, used: Vec, styles: StyleChain<'a>, @@ -311,13 +337,17 @@ struct ShapingContext<'a> { /// Shape text into [`ShapedText`]. pub fn shape<'a>( vt: &Vt, + base: usize, text: &'a str, + spans: &SpanMapper, styles: StyleChain<'a>, dir: Dir, ) -> ShapedText<'a> { let size = TextNode::size_in(styles); let mut ctx = ShapingContext { vt, + base, + spans, size, glyphs: vec![], used: vec![], @@ -335,6 +365,7 @@ pub fn shape<'a>( track_and_space(&mut ctx); ShapedText { + base, text, dir, styles, @@ -410,6 +441,7 @@ fn shape_segment<'a>( if info.glyph_id != 0 { // Add the glyph to the shaped output. // TODO: Don't ignore y_advance. + let (span, offset) = ctx.spans.span_at(ctx.base + cluster); ctx.glyphs.push(ShapedGlyph { font: font.clone(), glyph_id: info.glyph_id as u16, @@ -419,6 +451,8 @@ fn shape_segment<'a>( cluster: base + cluster, safe_to_break: !info.unsafe_to_break(), c: text[cluster..].chars().next().unwrap(), + span, + offset, }); } else { // Determine the source text range for the tofu sequence. @@ -478,15 +512,19 @@ fn shape_segment<'a>( fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, font: Font) { let x_advance = font.advance(0).unwrap_or_default(); for (cluster, c) in text.char_indices() { + let cluster = base + cluster; + let (span, offset) = ctx.spans.span_at(ctx.base + cluster); ctx.glyphs.push(ShapedGlyph { font: font.clone(), glyph_id: 0, x_advance, x_offset: Em::zero(), y_offset: Em::zero(), - cluster: base + cluster, + cluster, safe_to_break: true, c, + span, + offset, }); } } diff --git a/src/doc.rs b/src/doc.rs index 67e13bc84..76a456066 100644 --- a/src/doc.rs +++ b/src/doc.rs @@ -15,6 +15,7 @@ use crate::geom::{ }; use crate::image::Image; use crate::model::{node, Content, Fold, StableId, StyleChain}; +use crate::syntax::Span; /// A finished document with metadata and page frames. #[derive(Debug, Default, Clone, Hash)] @@ -119,8 +120,8 @@ impl Frame { let mut text = EcoString::new(); for (_, element) in self.elements() { match element { - Element::Text(content) => { - for glyph in &content.glyphs { + Element::Text(element) => { + for glyph in &element.glyphs { text.push(glyph.c); } } @@ -499,6 +500,10 @@ pub struct Glyph { pub x_offset: Em, /// The first character of the glyph's cluster. pub c: char, + /// The source code location of the text. + pub span: Span, + /// The offset within the spanned text. + pub offset: u16, } /// An identifier for a natural language. diff --git a/src/eval/func.rs b/src/eval/func.rs index 268542402..a5fa6fa1c 100644 --- a/src/eval/func.rs +++ b/src/eval/func.rs @@ -62,9 +62,11 @@ impl Func { self.1 } - /// Attach a span to the function. + /// Attach a span to this function if it doesn't already have one. pub fn spanned(mut self, span: Span) -> Self { - self.1 = span; + if self.1.is_detached() { + self.1 = span; + } self } diff --git a/src/ide/jump.rs b/src/ide/jump.rs new file mode 100644 index 000000000..1a96fbbed --- /dev/null +++ b/src/ide/jump.rs @@ -0,0 +1,93 @@ +use std::num::NonZeroUsize; + +use crate::doc::{Element, Frame, Location}; +use crate::geom::Point; +use crate::syntax::{LinkedNode, Source, Span, SyntaxKind}; +use crate::World; + +/// Find the source file and byte offset for a click position. +pub fn jump_to_source<'a>( + world: &'a dyn World, + frame: &Frame, + click: Point, +) -> Option<(&'a Source, usize)> { + for (mut pos, element) in frame.elements() { + if let Element::Text(text) = element { + for glyph in &text.glyphs { + if glyph.span.is_detached() { + continue; + } + + let width = glyph.x_advance.at(text.size); + if pos.x <= click.x + && pos.x + width >= click.x + && pos.y >= click.y + && pos.y - text.size <= click.y + { + let source = world.source(glyph.span.source()); + let node = source.find(glyph.span); + let pos = if node.kind() == SyntaxKind::Text { + let range = node.range(); + (range.start + usize::from(glyph.offset)).min(range.end) + } else { + node.offset() + }; + return Some((source, pos)); + } + + pos.x += width; + } + } + + if let Element::Group(group) = element { + if let Some(span) = jump_to_source(world, &group.frame, click - pos) { + return Some(span); + } + } + } + + None +} + +/// Find the output location for a cursor position. +pub fn jump_to_preview( + frames: &[Frame], + source: &Source, + cursor: usize, +) -> Option { + let node = LinkedNode::new(source.root()).leaf_at(cursor)?; + if node.kind() != SyntaxKind::Text { + return None; + } + + let span = node.span(); + for (i, frame) in frames.iter().enumerate() { + if let Some(pos) = find_in_frame(frame, span) { + return Some(Location { page: NonZeroUsize::new(i + 1).unwrap(), pos }); + } + } + + None +} + +/// Find the position of a span in a frame. +fn find_in_frame(frame: &Frame, span: Span) -> Option { + for (mut pos, element) in frame.elements() { + if let Element::Text(text) = element { + for glyph in &text.glyphs { + if glyph.span == span { + return Some(pos); + } + pos.x += glyph.x_advance.at(text.size); + } + } + + if let Element::Group(group) = element { + if let Some(point) = find_in_frame(&group.frame, span) { + return Some(point + pos); + } + } + } + + None +} diff --git a/src/ide/mod.rs b/src/ide/mod.rs index 4999da523..bee959cdf 100644 --- a/src/ide/mod.rs +++ b/src/ide/mod.rs @@ -3,10 +3,12 @@ mod analyze; mod complete; mod highlight; +mod jump; mod tooltip; pub use self::complete::*; pub use self::highlight::*; +pub use self::jump::*; pub use self::tooltip::*; use std::fmt::Write; diff --git a/src/model/content.rs b/src/model/content.rs index 012ad05f9..071a58622 100644 --- a/src/model/content.rs +++ b/src/model/content.rs @@ -106,9 +106,11 @@ impl Content { self.span } - /// Attach a span to the content. + /// Attach a span to the content if it doesn't already have one. pub fn spanned(mut self, span: Span) -> Self { - self.span = span; + if self.span.is_detached() { + self.span = span; + } self } diff --git a/src/syntax/node.rs b/src/syntax/node.rs index 392633f60..afbebe97f 100644 --- a/src/syntax/node.rs +++ b/src/syntax/node.rs @@ -204,14 +204,6 @@ impl SyntaxNode { Ok(()) } - /// If the span points into this node, convert it to a byte range. - pub(super) fn range(&self, span: Span, offset: usize) -> Option> { - match &self.0 { - Repr::Inner(inner) => inner.range(span, offset), - _ => (self.span() == span).then(|| offset..offset + self.len()), - } - } - /// Whether this is a leaf node. pub(super) fn is_leaf(&self) -> bool { matches!(self.0, Repr::Leaf(_)) @@ -429,40 +421,6 @@ impl InnerNode { Ok(()) } - /// If the span points into this node, convert it to a byte range. - fn range(&self, span: Span, mut offset: usize) -> Option> { - // Check whether we found it. - if span == self.span { - return Some(offset..offset + self.len); - } - - // The parent of a subtree has a smaller span number than all of its - // descendants. Therefore, we can bail out early if the target span's - // number is smaller than our number. - if span.number() < self.span.number() { - return None; - } - - let mut children = self.children.iter().peekable(); - while let Some(child) = children.next() { - // Every node in this child's subtree has a smaller span number than - // the next sibling. Therefore we only need to recurse if the next - // sibling's span number is larger than the target span's number. - if children - .peek() - .map_or(true, |next| next.span().number() > span.number()) - { - if let Some(range) = child.range(span, offset) { - return Some(range); - } - } - - offset += child.len(); - } - - None - } - /// Replaces a range of children with a replacement. /// /// May have mutated the children if it returns `Err(_)`. @@ -669,6 +627,39 @@ impl<'a> LinkedNode<'a> { back: self.offset + self.len(), } } + + /// Find a descendant with the given span. + pub fn find(&self, span: Span) -> Option> { + if self.span() == span { + return Some(self.clone()); + } + + if let Repr::Inner(inner) = &self.0 { + // The parent of a subtree has a smaller span number than all of its + // descendants. Therefore, we can bail out early if the target span's + // number is smaller than our number. + if span.number() < inner.span.number() { + return None; + } + + let mut children = self.children().peekable(); + while let Some(child) = children.next() { + // Every node in this child's subtree has a smaller span number than + // the next sibling. Therefore we only need to recurse if the next + // sibling's span number is larger than the target span's number. + if children + .peek() + .map_or(true, |next| next.span().number() > span.number()) + { + if let Some(found) = child.find(span) { + return Some(found); + } + } + } + } + + None + } } /// Access to parents and siblings. diff --git a/src/syntax/source.rs b/src/syntax/source.rs index f00d779b9..607a26033 100644 --- a/src/syntax/source.rs +++ b/src/syntax/source.rs @@ -9,7 +9,7 @@ use comemo::Prehashed; use unscanny::Scanner; use super::ast::Markup; -use super::{is_newline, parse, reparse, Span, SyntaxNode}; +use super::{is_newline, parse, reparse, LinkedNode, Span, SyntaxNode}; use crate::diag::SourceResult; use crate::util::{PathExt, StrExt}; @@ -149,13 +149,20 @@ impl Source { self.lines.len() } + /// Find the node with the given span. + /// + /// Panics if the span does not point into this source file. + pub fn find(&self, span: Span) -> LinkedNode<'_> { + LinkedNode::new(&self.root) + .find(span) + .expect("span does not point into this source file") + } + /// Map a span that points into this source file to a byte range. /// /// Panics if the span does not point into this source file. pub fn range(&self, span: Span) -> Range { - self.root - .range(span, 0) - .expect("span does not point into this source file") + self.find(span).range() } /// Return the index of the UTF-16 code unit at the byte index.