From 3e827012a0f27550ad07cd40ab7d43a0b62efaae Mon Sep 17 00:00:00 2001 From: Yifan Song Date: Fri, 13 Dec 2024 20:08:34 +0100 Subject: [PATCH 1/5] return multiple positions from jump_to_cursor --- crates/tinymist/src/tool/preview.rs | 60 ++++++++++++----------- crates/typst-preview/src/actor/typst.rs | 16 ++---- crates/typst-preview/src/actor/webview.rs | 16 +++++- crates/typst-preview/src/lib.rs | 4 +- tools/typst-preview-frontend/src/ws.ts | 19 +++++-- 5 files changed, 66 insertions(+), 49 deletions(-) diff --git a/crates/tinymist/src/tool/preview.rs b/crates/tinymist/src/tool/preview.rs index fba636112..ee52911ef 100644 --- a/crates/tinymist/src/tool/preview.rs +++ b/crates/tinymist/src/tool/preview.rs @@ -61,7 +61,7 @@ impl CompileHandler { async fn resolve_document_position( snap: &SucceededArtifact, loc: Location, - ) -> Option { + ) -> Vec { let Location::Src(src_loc) = loc; let path = Path::new(&src_loc.filepath).to_owned(); @@ -69,14 +69,24 @@ impl CompileHandler { 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) } @@ -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, Error> { + async fn resolve_document_position(&self, loc: Location) -> Result, Error> { let snap = self.artifact()?.receive().await?; Ok(Self::resolve_document_position(&snap, loc).await) } @@ -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 { - 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 { + 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 = 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. diff --git a/crates/typst-preview/src/actor/typst.rs b/crates/typst-preview/src/actor/typst.rs index ac3701e48..ef68840ac 100644 --- a/crates/typst-preview/src/actor/typst.rs +++ b/crates/typst-preview/src/actor/typst.rs @@ -116,26 +116,16 @@ impl TypstActor { .map_err(|err| { error!("TypstActor: failed to resolve src to doc jump: {:#}", err); }) - .ok() - .flatten(); - // impl From 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 { + .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) => { diff --git a/crates/typst-preview/src/actor/webview.rs b/crates/typst-preview/src/actor/webview.rs index ed97d4f1a..ea8d9064e 100644 --- a/crates/typst-preview/src/actor/webview.rs +++ b/crates/typst-preview/src/actor/webview.rs @@ -17,7 +17,7 @@ pub type SrcToDocJumpInfo = DocumentPosition; #[derive(Debug, Clone)] pub enum WebviewActorRequest { ViewportPosition(DocumentPosition), - SrcToDocJump(SrcToDocJumpInfo), + SrcToDocJump(Vec), // CursorPosition(CursorPosition), CursorPaths(Vec>), } @@ -29,6 +29,18 @@ fn position_req( format!("{event},{page_no} {x} {y}") } +fn positions_req( + event: &'static str, + positions: Vec, +) -> String { + format!("{event},") + + &positions + .iter() + .map(|DocumentPosition { page_no, x, y }| format!("{page_no} {x} {y}")) + .collect::>() + .join(",") +} + pub struct WebviewActor< 'a, C: futures::Sink + futures::Stream>, @@ -84,7 +96,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(); } diff --git a/crates/typst-preview/src/lib.rs b/crates/typst-preview/src/lib.rs index 8e673cc9d..15ac432de 100644 --- a/crates/typst-preview/src/lib.rs +++ b/crates/typst-preview/src/lib.rs @@ -325,8 +325,8 @@ pub trait SourceFileServer { fn resolve_document_position( &self, _by: Location, - ) -> impl Future, Error>> + Send { - async { Ok(None) } + ) -> impl Future, Error>> + Send { + async { Ok(vec![]) } } fn resolve_source_location( diff --git a/tools/typst-preview-frontend/src/ws.ts b/tools/typst-preview-frontend/src/ws.ts index ac2a9a866..9e015987b 100644 --- a/tools/typst-preview-frontend/src/ws.ts +++ b/tools/typst-preview-frontend/src/ws.ts @@ -306,10 +306,23 @@ export async function wsMain({ url, previewMode, isContentPreview }: WsArgs) { if (message[0] === "jump" || message[0] === "viewport") { // todo: aware height padding - const [page, x, y] = dec + const current_page_number = svgDoc.getPartialPageNumber(); + + 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); + console.log("jump", page, x, y); + alert("jump " + page + " " + x + " " + y); + const current_page = current_page_number; + 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; From 16c4c5b0b08f1929650eb086726b511d2a136422 Mon Sep 17 00:00:00 2001 From: Yifan Song Date: Fri, 13 Dec 2024 20:17:25 +0100 Subject: [PATCH 2/5] format --- crates/typst-preview/src/actor/typst.rs | 14 +++++++++----- crates/typst-preview/src/actor/webview.rs | 5 +---- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/crates/typst-preview/src/actor/typst.rs b/crates/typst-preview/src/actor/typst.rs index ef68840ac..95bf09f40 100644 --- a/crates/typst-preview/src/actor/typst.rs +++ b/crates/typst-preview/src/actor/typst.rs @@ -121,11 +121,15 @@ impl TypstActor { if let Some(info) = res { let _ = self .webview_conn_sender - .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())); + .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) => { diff --git a/crates/typst-preview/src/actor/webview.rs b/crates/typst-preview/src/actor/webview.rs index ea8d9064e..4f4bc47b7 100644 --- a/crates/typst-preview/src/actor/webview.rs +++ b/crates/typst-preview/src/actor/webview.rs @@ -29,10 +29,7 @@ fn position_req( format!("{event},{page_no} {x} {y}") } -fn positions_req( - event: &'static str, - positions: Vec, -) -> String { +fn positions_req(event: &'static str, positions: Vec) -> String { format!("{event},") + &positions .iter() From f8c0314de41014206f90c0dad257f437a9f4b043 Mon Sep 17 00:00:00 2001 From: Yifan Song Date: Sat, 14 Dec 2024 12:29:51 +0100 Subject: [PATCH 3/5] remove an alert --- tools/typst-preview-frontend/src/ws.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/tools/typst-preview-frontend/src/ws.ts b/tools/typst-preview-frontend/src/ws.ts index 9e015987b..53160cc54 100644 --- a/tools/typst-preview-frontend/src/ws.ts +++ b/tools/typst-preview-frontend/src/ws.ts @@ -315,8 +315,6 @@ export async function wsMain({ url, previewMode, isContentPreview }: WsArgs) { // 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); - console.log("jump", page, x, y); - alert("jump " + page + " " + x + " " + y); const current_page = current_page_number; if (Math.abs(page - current_page) < Math.abs(acc[0] - current_page)) { return [page, x, y]; From 90d6d56b510bc7e826cd8ec6db239a022ca19c8d Mon Sep 17 00:00:00 2001 From: Yifan Song Date: Sat, 14 Dec 2024 14:40:27 +0100 Subject: [PATCH 4/5] add a simple doc for jump test --- .../e2e-workspaces/simple-docs/jump.typ | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 editors/vscode/e2e-workspaces/simple-docs/jump.typ diff --git a/editors/vscode/e2e-workspaces/simple-docs/jump.typ b/editors/vscode/e2e-workspaces/simple-docs/jump.typ new file mode 100644 index 000000000..696af958e --- /dev/null +++ b/editors/vscode/e2e-workspaces/simple-docs/jump.typ @@ -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() \ No newline at end of file From bf1da5d608ae41ec13997c2bf89f629c8ba1c45c Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Sat, 14 Dec 2024 22:00:41 +0800 Subject: [PATCH 5/5] feat: resolve currentPosition --- tools/typst-preview-frontend/src/global.d.ts | 7 +++ tools/typst-preview-frontend/src/typst.ts | 56 ++++++++++++++++++++ tools/typst-preview-frontend/src/ws.ts | 14 +++-- 3 files changed, 73 insertions(+), 4 deletions(-) diff --git a/tools/typst-preview-frontend/src/global.d.ts b/tools/typst-preview-frontend/src/global.d.ts index 048a4298a..48e11dab2 100644 --- a/tools/typst-preview-frontend/src/global.d.ts +++ b/tools/typst-preview-frontend/src/global.d.ts @@ -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; } diff --git a/tools/typst-preview-frontend/src/typst.ts b/tools/typst-preview-frontend/src/typst.ts index 781148d76..97656aa7d 100644 --- a/tools/typst-preview-frontend/src/typst.ts +++ b/tools/typst-preview-frontend/src/typst.ts @@ -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; + 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(".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, diff --git a/tools/typst-preview-frontend/src/ws.ts b/tools/typst-preview-frontend/src/ws.ts index 53160cc54..65c1d9996 100644 --- a/tools/typst-preview-frontend/src/ws.ts +++ b/tools/typst-preview-frontend/src/ws.ts @@ -305,8 +305,16 @@ 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 current_page_number = svgDoc.getPartialPageNumber(); + 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) @@ -315,7 +323,7 @@ export async function wsMain({ url, previewMode, isContentPreview }: WsArgs) { // 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 = current_page_number; + const current_page = currentPageNumber; if (Math.abs(page - current_page) < Math.abs(acc[0] - current_page)) { return [page, x, y]; } @@ -338,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