Skip to content

Commit

Permalink
feat: jump to neareast position in preview from cursor (#997)
Browse files Browse the repository at this point in the history
* return multiple positions from jump_to_cursor

* format

* remove an alert

* add a simple doc for jump test

* feat: resolve currentPosition

---------

Co-authored-by: Myriad-Dreamin <camiyoru@gmail.com>
  • Loading branch information
Eric-Song-Nop and Myriad-Dreamin authored Dec 14, 2024
1 parent cb3b648 commit c54d1d3
Show file tree
Hide file tree
Showing 8 changed files with 160 additions and 54 deletions.
60 changes: 31 additions & 29 deletions crates/tinymist/src/tool/preview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,22 +61,32 @@ impl CompileHandler {
async fn resolve_document_position(
snap: &SucceededArtifact<LspCompilerFeat>,
loc: Location,
) -> Option<Position> {
) -> Vec<Position> {
let Location::Src(src_loc) = loc;

let path = Path::new(&src_loc.filepath).to_owned();
let line = src_loc.pos.line;
let column = src_loc.pos.column;

let doc = snap.success_doc();
let doc = doc.as_deref()?;
let Some(doc) = doc.as_deref() else {
return vec![];
};
let world = snap.world();

let relative_path = path.strip_prefix(&world.workspace_root()?).ok()?;
let Some(root) = world.workspace_root() else {
return vec![];
};
let Some(relative_path) = path.strip_prefix(root).ok() else {
return vec![];
};

let source_id = TypstFileId::new(None, VirtualPath::new(relative_path));
let source = world.source(source_id).ok()?;
let cursor = source.line_column_to_byte(line, column)?;
let Some(source) = world.source(source_id).ok() else {
return vec![];
};
let Some(cursor) = source.line_column_to_byte(line, column) else {
return vec![];
};

jump_from_cursor(doc, &source, cursor)
}
Expand Down Expand Up @@ -115,7 +125,7 @@ impl SourceFileServer for CompileHandler {

/// fixme: character is 0-based, UTF-16 code unit.
/// We treat it as UTF-8 now.
async fn resolve_document_position(&self, loc: Location) -> Result<Option<Position>, Error> {
async fn resolve_document_position(&self, loc: Location) -> Result<Vec<Position>, Error> {
let snap = self.artifact()?.receive().await?;
Ok(Self::resolve_document_position(&snap, loc).await)
}
Expand Down Expand Up @@ -675,38 +685,30 @@ impl Notification for NotifDocumentOutline {
}

/// Find the output location in the document for a cursor position.
fn jump_from_cursor(document: &TypstDocument, source: &Source, cursor: usize) -> Option<Position> {
let node = LinkedNode::new(source.root()).leaf_at_compat(cursor)?;
if node.kind() != SyntaxKind::Text {
return None;
}
fn jump_from_cursor(document: &TypstDocument, source: &Source, cursor: usize) -> Vec<Position> {
let Some(node) = LinkedNode::new(source.root())
.leaf_at_compat(cursor)
.filter(|node| node.kind() == SyntaxKind::Text)
else {
return vec![];
};

let mut min_dis = u64::MAX;
let mut p = Point::default();
let mut ppage = 0usize;

let span = node.span();
let mut positions: Vec<Position> = vec![];
for (i, page) in document.pages.iter().enumerate() {
let t_dis = min_dis;
let mut min_dis = u64::MAX;
if let Some(pos) = find_in_frame(&page.frame, span, &mut min_dis, &mut p) {
return Some(Position {
page: NonZeroUsize::new(i + 1)?,
point: pos,
});
}
if t_dis != min_dis {
ppage = i;
if let Some(page) = NonZeroUsize::new(i + 1) {
positions.push(Position { page, point: pos });
}
}
}

if min_dis == u64::MAX {
return None;
}
log::info!("jump_from_cursor: {positions:#?}");

Some(Position {
page: NonZeroUsize::new(ppage + 1)?,
point: p,
})
positions
}

/// Find the position of a span in a frame.
Expand Down
26 changes: 10 additions & 16 deletions crates/typst-preview/src/actor/typst.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,26 +116,20 @@ impl<T: SourceFileServer + EditorServer> TypstActor<T> {
.map_err(|err| {
error!("TypstActor: failed to resolve src to doc jump: {:#}", err);
})
.ok()
.flatten();
// impl From<TypstPosition> for DocumentPosition {
// fn from(position: TypstPosition) -> Self {
// Self {
// page_no: position.page.into(),
// x: position.point.x.to_pt() as f32,
// y: position.point.y.to_pt() as f32,
// }
// }
// }
.ok();

if let Some(info) = res {
let _ = self
.webview_conn_sender
.send(WebviewActorRequest::SrcToDocJump(DocumentPosition {
page_no: info.page.into(),
x: info.point.x.to_pt() as f32,
y: info.point.y.to_pt() as f32,
}));
.send(WebviewActorRequest::SrcToDocJump(
info.into_iter()
.map(|info| DocumentPosition {
page_no: info.page.into(),
x: info.point.x.to_pt() as f32,
y: info.point.y.to_pt() as f32,
})
.collect(),
));
}
}
TypstActorRequest::SyncMemoryFiles(m) => {
Expand Down
13 changes: 11 additions & 2 deletions crates/typst-preview/src/actor/webview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub type SrcToDocJumpInfo = DocumentPosition;
#[derive(Debug, Clone)]
pub enum WebviewActorRequest {
ViewportPosition(DocumentPosition),
SrcToDocJump(SrcToDocJumpInfo),
SrcToDocJump(Vec<SrcToDocJumpInfo>),
// CursorPosition(CursorPosition),
CursorPaths(Vec<Vec<ElementPoint>>),
}
Expand All @@ -29,6 +29,15 @@ fn position_req(
format!("{event},{page_no} {x} {y}")
}

fn positions_req(event: &'static str, positions: Vec<DocumentPosition>) -> String {
format!("{event},")
+ &positions
.iter()
.map(|DocumentPosition { page_no, x, y }| format!("{page_no} {x} {y}"))
.collect::<Vec<_>>()
.join(",")
}

pub struct WebviewActor<
'a,
C: futures::Sink<Message, Error = WsError> + futures::Stream<Item = Result<Message, WsError>>,
Expand Down Expand Up @@ -84,7 +93,7 @@ impl<
trace!("WebviewActor: received message from mailbox: {:?}", msg);
match msg {
WebviewActorRequest::SrcToDocJump(jump_info) => {
let msg = position_req("jump", jump_info);
let msg = positions_req("jump", jump_info);
self.webview_websocket_conn.send(Message::Binary(msg.into_bytes()))
.await.unwrap();
}
Expand Down
4 changes: 2 additions & 2 deletions crates/typst-preview/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -325,8 +325,8 @@ pub trait SourceFileServer {
fn resolve_document_position(
&self,
_by: Location,
) -> impl Future<Output = Result<Option<Position>, Error>> + Send {
async { Ok(None) }
) -> impl Future<Output = Result<Vec<Position>, Error>> + Send {
async { Ok(vec![]) }
}

fn resolve_source_location(
Expand Down
21 changes: 21 additions & 0 deletions editors/vscode/e2e-workspaces/simple-docs/jump.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#outline()
#pagebreak()
Some text
More text
#pagebreak()
Some text
More text
#pagebreak()
Some text
More text
#pagebreak()
Some text
More text
#pagebreak()
= Title
Some text
More text
#pagebreak()
more text
even more
#pagebreak()
7 changes: 7 additions & 0 deletions tools/typst-preview-frontend/src/global.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
interface TypstPosition {
page: number;
x: number;
y: number;
}

interface Window {
initTypstSvg(docRoot: SVGElement): void;
currentPosition(elem: Element): TypstPosition | undefined;
handleTypstLocation(elem: Element, page: number, x: number, y: number);
typstWebsocket: WebSocket;
}
Expand Down
56 changes: 56 additions & 0 deletions tools/typst-preview-frontend/src/typst.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,62 @@ function layoutText(svg: SVGElement) {
console.log(`layoutText used time ${performance.now() - layoutBegin} ms`);
}

window.currentPosition = function (elem: Element) {
const docRoot = findAncestor(elem, "typst-doc");
if (!docRoot) {
console.warn("no typst-doc found", elem);
return;
}

interface TypstPosition {
page: number;
x: number;
y: number;
distance: number;
}

let result: TypstPosition | undefined = undefined;
const windowX = window.innerWidth / 2;
const windowY = window.innerHeight / 2;
type ScrollRect = Pick<DOMRect, "left" | "top" | "width" | "height">;
const handlePage = (pageBBox: ScrollRect, page: number) => {
const x = pageBBox.left;
const y = pageBBox.top + pageBBox.height / 2;

const distance = Math.hypot(x - windowX, y - windowY);
if (result === undefined || distance < result.distance) {
result = { page, x, y, distance };
}
};

const renderMode = docRoot.getAttribute("data-render-mode");
if (renderMode === "canvas") {
const pages = docRoot.querySelectorAll<HTMLDivElement>(".typst-page");

for (const page of pages) {
const pageNumber = Number.parseInt(
page.getAttribute("data-page-number")!
);

const bbox = page.getBoundingClientRect();
handlePage(bbox, pageNumber);
}
return result;
}

const children = docRoot.children;
let nthPage = 0;
for (let i = 0; i < children.length; i++) {
if (children[i].tagName === "g") {
nthPage++;
}
const page = children[i] as SVGGElement;
const bbox = page.getBoundingClientRect();
handlePage(bbox, nthPage);
}
return result;
};

window.handleTypstLocation = function (
elem: Element,
pageNo: number,
Expand Down
27 changes: 22 additions & 5 deletions tools/typst-preview-frontend/src/ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,11 +305,30 @@ export async function wsMain({ url, previewMode, isContentPreview }: WsArgs) {
}

if (message[0] === "jump" || message[0] === "viewport") {
const rootElem =
document.getElementById("typst-app")?.firstElementChild;

// todo: aware height padding
const [page, x, y] = dec
let currentPageNumber = 1;
if (previewMode === PreviewMode.Slide) {
currentPageNumber = svgDoc.getPartialPageNumber();
} else if (rootElem) {
currentPageNumber = window.currentPosition(rootElem)?.page || 1;
}

let positions = dec
.decode((message[1] as any).buffer)
.split(" ")
.map(Number);
.split(",")

// choose the page, x, y closest to the current page
const [page, x, y] = positions.reduce((acc, cur) => {
const [page, x, y] = cur.split(" ").map(Number);
const current_page = currentPageNumber;
if (Math.abs(page - current_page) < Math.abs(acc[0] - current_page)) {
return [page, x, y];
}
return acc;
}, [Number.MAX_SAFE_INTEGER, 0, 0]);

let pageToJump = page;

Expand All @@ -327,8 +346,6 @@ export async function wsMain({ url, previewMode, isContentPreview }: WsArgs) {
}
}

const rootElem =
document.getElementById("typst-app")?.firstElementChild;
if (rootElem) {
/// Note: when it is really scrolled, it will trigger `svgDoc.addViewportChange`
/// via `window.onscroll` event
Expand Down

0 comments on commit c54d1d3

Please sign in to comment.