summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--library/src/layout/par.rs59
-rw-r--r--library/src/math/fragment.rs2
-rw-r--r--library/src/text/shaping.rs44
-rw-r--r--src/doc.rs9
-rw-r--r--src/eval/func.rs6
-rw-r--r--src/ide/jump.rs93
-rw-r--r--src/ide/mod.rs2
-rw-r--r--src/model/content.rs6
-rw-r--r--src/syntax/node.rs75
-rw-r--r--src/syntax/source.rs15
10 files changed, 245 insertions, 66 deletions
diff --git a/library/src/layout/par.rs b/library/src/layout/par.rs
index 8dd81d29..6178c059 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<Item<'a>>,
+ /// 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<Preparation<'a>> {
@@ -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 73daa4b2..de456b82 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 e7ce4027..a96238d9 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<usize>) -> ShapedText<'a> {
+ pub fn reshape(
+ &'a self,
+ vt: &Vt,
+ spans: &SpanMapper,
+ text_range: Range<usize>,
+ ) -> 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<ShapedGlyph>,
used: Vec<Font>,
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 67e13bc8..76a45606 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 26854240..a5fa6fa1 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 00000000..1a96fbbe
--- /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<Location> {
+ 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<Point> {
+ 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 4999da52..bee959cd 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 012ad05f..071a5862 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 392633f6..afbebe97 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<Range<usize>> {
- 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<Range<usize>> {
- // 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<LinkedNode<'a>> {
+ 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 f00d779b..607a2603 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<usize> {
- 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.