Jump to source and preview

This commit is contained in:
Laurenz 2023-03-11 23:28:35 +01:00
parent 1a390deaea
commit ca6edf5283
10 changed files with 245 additions and 66 deletions

View File

@ -145,12 +145,12 @@ impl ParNode {
let children = par.children(); let children = par.children();
// Collect all text into one string for BiDi analysis. // 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 // Perform BiDi analysis and then prepare paragraph layout by building a
// representation on which we can do line breaking without layouting // representation on which we can do line breaking without layouting
// each and every line from scratch. // 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. // Break the paragraph into lines.
let lines = linebreak(&vt, &p, region.x); let lines = linebreak(&vt, &p, region.x);
@ -264,6 +264,8 @@ struct Preparation<'a> {
bidi: BidiInfo<'a>, bidi: BidiInfo<'a>,
/// Text runs, spacing and layouted nodes. /// Text runs, spacing and layouted nodes.
items: Vec<Item<'a>>, items: Vec<Item<'a>>,
/// The span mapper.
spans: SpanMapper,
/// The styles shared by all children. /// The styles shared by all children.
styles: StyleChain<'a>, styles: StyleChain<'a>,
/// Whether to hyphenate if it's the same for all children. /// 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 /// A layouted line, consisting of a sequence of layouted paragraph items that
/// are mostly borrowed from the preparation phase. This type enables you to /// 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 /// measure the size of a line in a range before comitting to building the
@ -485,10 +516,11 @@ fn collect<'a>(
children: &'a [Content], children: &'a [Content],
styles: &'a StyleChain<'a>, styles: &'a StyleChain<'a>,
consecutive: bool, consecutive: bool,
) -> SourceResult<(String, Vec<(Segment<'a>, StyleChain<'a>)>)> { ) -> SourceResult<(String, Vec<(Segment<'a>, StyleChain<'a>)>, SpanMapper)> {
let mut full = String::new(); let mut full = String::new();
let mut quoter = Quoter::new(); let mut quoter = Quoter::new();
let mut segments = vec![]; let mut segments = vec![];
let mut spans = SpanMapper::new();
let mut iter = children.iter().peekable(); let mut iter = children.iter().peekable();
if consecutive { if consecutive {
@ -578,6 +610,8 @@ fn collect<'a>(
quoter.last(last); quoter.last(last);
} }
spans.push(segment.len(), child.span());
if let (Some((Segment::Text(last_len), last_styles)), Segment::Text(len)) = if let (Some((Segment::Text(last_len), last_styles)), Segment::Text(len)) =
(segments.last_mut(), segment) (segments.last_mut(), segment)
{ {
@ -590,7 +624,7 @@ fn collect<'a>(
segments.push((segment, styles)); segments.push((segment, styles));
} }
Ok((full, segments)) Ok((full, segments, spans))
} }
/// Prepare paragraph layout by shaping the whole paragraph and layouting all /// Prepare paragraph layout by shaping the whole paragraph and layouting all
@ -600,6 +634,7 @@ fn prepare<'a>(
children: &'a [Content], children: &'a [Content],
text: &'a str, text: &'a str,
segments: Vec<(Segment<'a>, StyleChain<'a>)>, segments: Vec<(Segment<'a>, StyleChain<'a>)>,
spans: SpanMapper,
styles: StyleChain<'a>, styles: StyleChain<'a>,
region: Size, region: Size,
) -> SourceResult<Preparation<'a>> { ) -> SourceResult<Preparation<'a>> {
@ -620,7 +655,7 @@ fn prepare<'a>(
let end = cursor + segment.len(); let end = cursor + segment.len();
match segment { match segment {
Segment::Text(_) => { 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 { Segment::Spacing(spacing) => match spacing {
Spacing::Rel(v) => { Spacing::Rel(v) => {
@ -655,6 +690,7 @@ fn prepare<'a>(
Ok(Preparation { Ok(Preparation {
bidi, bidi,
items, items,
spans,
styles, styles,
hyphenate: shared_get(styles, children, TextNode::hyphenate_in), hyphenate: shared_get(styles, children, TextNode::hyphenate_in),
lang: shared_get(styles, children, TextNode::lang_in), lang: shared_get(styles, children, TextNode::lang_in),
@ -670,11 +706,12 @@ fn shape_range<'a>(
vt: &Vt, vt: &Vt,
bidi: &BidiInfo<'a>, bidi: &BidiInfo<'a>,
range: Range, range: Range,
spans: &SpanMapper,
styles: StyleChain<'a>, 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 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)); items.push(Item::Text(shaped));
}; };
@ -694,7 +731,7 @@ fn shape_range<'a>(
if level != prev_level || !is_compatible(script, prev_script) { if level != prev_level || !is_compatible(script, prev_script) {
if cursor < i { if cursor < i {
process(&bidi.text[cursor..i], prev_level); process(cursor..i, prev_level);
} }
cursor = i; cursor = i;
prev_level = level; 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. /// 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 + shaped.text.len() > range.end {
if hyphen || start < range.end || before.is_empty() { if hyphen || start < range.end || before.is_empty() {
let shifted = start - base..range.end - base; 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 { if hyphen || shy {
reshaped.push_hyphen(vt); reshaped.push_hyphen(vt);
} }
@ -1096,7 +1133,7 @@ fn line<'a>(
if range.start + shaped.text.len() > end { if range.start + shaped.text.len() > end {
if range.start < end { if range.start < end {
let shifted = range.start - base..end - base; let shifted = range.start - base..end - base;
let reshaped = shaped.reshape(vt, shifted); let reshaped = shaped.reshape(vt, &p.spans, shifted);
width += reshaped.width; width += reshaped.width;
first = Some(Item::Text(reshaped)); first = Some(Item::Text(reshaped));
} }

View File

@ -222,6 +222,8 @@ impl GlyphFragment {
c: self.c, c: self.c,
x_advance: Em::from_length(self.width, self.font_size), x_advance: Em::from_length(self.width, self.font_size),
x_offset: Em::zero(), x_offset: Em::zero(),
span: Span::detached(),
offset: 0,
}], }],
}; };
let size = Size::new(self.width, self.ascent + self.descent); let size = Size::new(self.width, self.ascent + self.descent);

View File

@ -6,6 +6,7 @@ use typst::font::{Font, FontVariant};
use typst::util::SliceExt; use typst::util::SliceExt;
use super::*; use super::*;
use crate::layout::SpanMapper;
use crate::prelude::*; use crate::prelude::*;
/// The result of shaping text. /// The result of shaping text.
@ -14,6 +15,8 @@ use crate::prelude::*;
/// measured, used to reshape substrings more quickly and converted into a /// measured, used to reshape substrings more quickly and converted into a
/// frame. /// frame.
pub struct ShapedText<'a> { pub struct ShapedText<'a> {
/// The start of the text in the full paragraph.
pub base: usize,
/// The text that was shaped. /// The text that was shaped.
pub text: &'a str, pub text: &'a str,
/// The text direction. /// The text direction.
@ -53,6 +56,10 @@ pub struct ShapedGlyph {
pub safe_to_break: bool, pub safe_to_break: bool,
/// The first char in this glyph's cluster. /// The first char in this glyph's cluster.
pub c: char, pub c: char,
/// The source code location of the text.
pub span: Span,
/// The offset within the spanned text.
pub offset: u16,
} }
impl ShapedGlyph { impl ShapedGlyph {
@ -110,6 +117,8 @@ impl<'a> ShapedText<'a> {
}, },
x_offset: glyph.x_offset, x_offset: glyph.x_offset,
c: glyph.c, c: glyph.c,
span: glyph.span,
offset: glyph.offset,
}) })
.collect(); .collect();
@ -187,9 +196,15 @@ impl<'a> ShapedText<'a> {
/// Reshape a range of the shaped text, reusing information from this /// Reshape a range of the shaped text, reusing information from this
/// shaping process if possible. /// 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()) { if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) {
Self { Self {
base: self.base + text_range.start,
text: &self.text[text_range], text: &self.text[text_range],
dir: self.dir, dir: self.dir,
styles: self.styles, styles: self.styles,
@ -199,7 +214,14 @@ impl<'a> ShapedText<'a> {
glyphs: Cow::Borrowed(glyphs), glyphs: Cow::Borrowed(glyphs),
} }
} else { } 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, cluster,
safe_to_break: true, safe_to_break: true,
c: '-', c: '-',
span: Span::detached(),
offset: 0,
}); });
Some(()) Some(())
}); });
@ -298,6 +322,8 @@ impl Debug for ShapedText<'_> {
/// Holds shaping results and metadata common to all shaped segments. /// Holds shaping results and metadata common to all shaped segments.
struct ShapingContext<'a> { struct ShapingContext<'a> {
vt: &'a Vt<'a>, vt: &'a Vt<'a>,
base: usize,
spans: &'a SpanMapper,
glyphs: Vec<ShapedGlyph>, glyphs: Vec<ShapedGlyph>,
used: Vec<Font>, used: Vec<Font>,
styles: StyleChain<'a>, styles: StyleChain<'a>,
@ -311,13 +337,17 @@ struct ShapingContext<'a> {
/// Shape text into [`ShapedText`]. /// Shape text into [`ShapedText`].
pub fn shape<'a>( pub fn shape<'a>(
vt: &Vt, vt: &Vt,
base: usize,
text: &'a str, text: &'a str,
spans: &SpanMapper,
styles: StyleChain<'a>, styles: StyleChain<'a>,
dir: Dir, dir: Dir,
) -> ShapedText<'a> { ) -> ShapedText<'a> {
let size = TextNode::size_in(styles); let size = TextNode::size_in(styles);
let mut ctx = ShapingContext { let mut ctx = ShapingContext {
vt, vt,
base,
spans,
size, size,
glyphs: vec![], glyphs: vec![],
used: vec![], used: vec![],
@ -335,6 +365,7 @@ pub fn shape<'a>(
track_and_space(&mut ctx); track_and_space(&mut ctx);
ShapedText { ShapedText {
base,
text, text,
dir, dir,
styles, styles,
@ -410,6 +441,7 @@ fn shape_segment<'a>(
if info.glyph_id != 0 { if info.glyph_id != 0 {
// Add the glyph to the shaped output. // Add the glyph to the shaped output.
// TODO: Don't ignore y_advance. // TODO: Don't ignore y_advance.
let (span, offset) = ctx.spans.span_at(ctx.base + cluster);
ctx.glyphs.push(ShapedGlyph { ctx.glyphs.push(ShapedGlyph {
font: font.clone(), font: font.clone(),
glyph_id: info.glyph_id as u16, glyph_id: info.glyph_id as u16,
@ -419,6 +451,8 @@ fn shape_segment<'a>(
cluster: base + cluster, cluster: base + cluster,
safe_to_break: !info.unsafe_to_break(), safe_to_break: !info.unsafe_to_break(),
c: text[cluster..].chars().next().unwrap(), c: text[cluster..].chars().next().unwrap(),
span,
offset,
}); });
} else { } else {
// Determine the source text range for the tofu sequence. // 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) { fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, font: Font) {
let x_advance = font.advance(0).unwrap_or_default(); let x_advance = font.advance(0).unwrap_or_default();
for (cluster, c) in text.char_indices() { 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 { ctx.glyphs.push(ShapedGlyph {
font: font.clone(), font: font.clone(),
glyph_id: 0, glyph_id: 0,
x_advance, x_advance,
x_offset: Em::zero(), x_offset: Em::zero(),
y_offset: Em::zero(), y_offset: Em::zero(),
cluster: base + cluster, cluster,
safe_to_break: true, safe_to_break: true,
c, c,
span,
offset,
}); });
} }
} }

View File

@ -15,6 +15,7 @@ use crate::geom::{
}; };
use crate::image::Image; use crate::image::Image;
use crate::model::{node, Content, Fold, StableId, StyleChain}; use crate::model::{node, Content, Fold, StableId, StyleChain};
use crate::syntax::Span;
/// A finished document with metadata and page frames. /// A finished document with metadata and page frames.
#[derive(Debug, Default, Clone, Hash)] #[derive(Debug, Default, Clone, Hash)]
@ -119,8 +120,8 @@ impl Frame {
let mut text = EcoString::new(); let mut text = EcoString::new();
for (_, element) in self.elements() { for (_, element) in self.elements() {
match element { match element {
Element::Text(content) => { Element::Text(element) => {
for glyph in &content.glyphs { for glyph in &element.glyphs {
text.push(glyph.c); text.push(glyph.c);
} }
} }
@ -499,6 +500,10 @@ pub struct Glyph {
pub x_offset: Em, pub x_offset: Em,
/// The first character of the glyph's cluster. /// The first character of the glyph's cluster.
pub c: char, 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. /// An identifier for a natural language.

View File

@ -62,9 +62,11 @@ impl Func {
self.1 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 { pub fn spanned(mut self, span: Span) -> Self {
self.1 = span; if self.1.is_detached() {
self.1 = span;
}
self self
} }

93
src/ide/jump.rs Normal file
View File

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

View File

@ -3,10 +3,12 @@
mod analyze; mod analyze;
mod complete; mod complete;
mod highlight; mod highlight;
mod jump;
mod tooltip; mod tooltip;
pub use self::complete::*; pub use self::complete::*;
pub use self::highlight::*; pub use self::highlight::*;
pub use self::jump::*;
pub use self::tooltip::*; pub use self::tooltip::*;
use std::fmt::Write; use std::fmt::Write;

View File

@ -106,9 +106,11 @@ impl Content {
self.span 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 { pub fn spanned(mut self, span: Span) -> Self {
self.span = span; if self.span.is_detached() {
self.span = span;
}
self self
} }

View File

@ -204,14 +204,6 @@ impl SyntaxNode {
Ok(()) 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. /// Whether this is a leaf node.
pub(super) fn is_leaf(&self) -> bool { pub(super) fn is_leaf(&self) -> bool {
matches!(self.0, Repr::Leaf(_)) matches!(self.0, Repr::Leaf(_))
@ -429,40 +421,6 @@ impl InnerNode {
Ok(()) 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. /// Replaces a range of children with a replacement.
/// ///
/// May have mutated the children if it returns `Err(_)`. /// May have mutated the children if it returns `Err(_)`.
@ -669,6 +627,39 @@ impl<'a> LinkedNode<'a> {
back: self.offset + self.len(), 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. /// Access to parents and siblings.

View File

@ -9,7 +9,7 @@ use comemo::Prehashed;
use unscanny::Scanner; use unscanny::Scanner;
use super::ast::Markup; 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::diag::SourceResult;
use crate::util::{PathExt, StrExt}; use crate::util::{PathExt, StrExt};
@ -149,13 +149,20 @@ impl Source {
self.lines.len() 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. /// Map a span that points into this source file to a byte range.
/// ///
/// Panics if the span does not point into this source file. /// Panics if the span does not point into this source file.
pub fn range(&self, span: Span) -> Range<usize> { pub fn range(&self, span: Span) -> Range<usize> {
self.root self.find(span).range()
.range(span, 0)
.expect("span does not point into this source file")
} }
/// Return the index of the UTF-16 code unit at the byte index. /// Return the index of the UTF-16 code unit at the byte index.