Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: jump to neareast position in preview from cursor #997

Merged
merged 5 commits into from
Dec 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading