From 48c724212f4237871c5f10298c09f0f352154835 Mon Sep 17 00:00:00 2001 From: Henok Date: Sun, 21 Jun 2026 12:14:00 +0300 Subject: [PATCH 01/10] Centralize path normalization and source-file recovery in `DocToSource` `BuildOpenFileCmd` used to call `path::NormalizeTemp` unconditionally before building the inverse-search command line, and OnInverseSearch's source-file recovery fallback called `file::Exists` on the resolved source path. Both assume a Windows path and misbehave on anything else. Moved both into `DocToSource` (`Pdfsync::DocToSource` and `SyncTex::DocToSource`), the one place that already knows the resolved path's origin and format. `BuildOpenFileCmd` and `OnInverseSearch` no longer reason about path format at all; they just use whatever `DocToSource` returns. `DocToSource` is now the single place that decides how a path should be resolved, depending on its format. This is just preparation for WSL SyncTex support, no behavior change. --- src/AppTools.cpp | 3 +-- src/PdfSync.cpp | 32 ++++++++++++++++++++++++++------ src/PdfSync.h | 3 ++- src/SearchAndDDE.cpp | 10 ---------- 4 files changed, 29 insertions(+), 19 deletions(-) diff --git a/src/AppTools.cpp b/src/AppTools.cpp index ad0357fc02ff..af2b237cc08f 100644 --- a/src/AppTools.cpp +++ b/src/AppTools.cpp @@ -421,8 +421,7 @@ char* BuildOpenFileCmd(const char* pattern, const char* path, int line, int col) perc++; if (*perc == 'f') { - char* fname = path::NormalizeTemp(path); - cmdline.Append(fname); + cmdline.Append(path); } else if (*perc == 'l') { cmdline.AppendFmt("%d", line); } else if (*perc == 'c') { diff --git a/src/PdfSync.cpp b/src/PdfSync.cpp index 7364dafcef5b..dcc480132ce1 100644 --- a/src/PdfSync.cpp +++ b/src/PdfSync.cpp @@ -43,7 +43,8 @@ struct PdfsyncPoint { // Synchronizer based on .pdfsync file generated with the pdfsync tex package class Pdfsync : public Synchronizer { public: - Pdfsync(const char* syncfilename, EngineBase* engine) : Synchronizer(syncfilename), engine(engine) { + Pdfsync(const char* syncfilename, const char* pdffilename, EngineBase* engine) + : Synchronizer(syncfilename, pdffilename), engine(engine) { ReportIf(!str::EndsWithI(syncfilename, ".pdfsync")); } @@ -65,7 +66,8 @@ class Pdfsync : public Synchronizer { // Synchronizer based on .synctex file generated with SyncTex class SyncTex : public Synchronizer { public: - SyncTex(const char* syncfilename, EngineBase* engineIn) : Synchronizer(syncfilename) { + SyncTex(const char* syncfilename, const char* pdffilename, EngineBase* engineIn) + : Synchronizer(syncfilename, pdffilename) { engine = engineIn; scanner = nullptr; ReportIf(!str::EndsWithI(syncfilename, ".synctex")); @@ -83,8 +85,9 @@ class SyncTex : public Synchronizer { synctex_scanner_p scanner; }; -Synchronizer::Synchronizer(const char* syncFilePathIn) { +Synchronizer::Synchronizer(const char* syncFilePathIn, const char* pdfPathIn) { syncFilePath = str::Dup(syncFilePathIn); + pdfPath = str::Dup(pdfPathIn); WCHAR* path = ToWStrTemp(syncFilePathIn); _wstat(path, &syncfileTimestamp); } @@ -136,7 +139,7 @@ int Synchronizer::Create(const char* path, EngineBase* engine, Synchronizer** sy // Check if a PDFSYNC file is present char* syncFile = str::JoinTemp(basePath, ".pdfsync"); if (file::Exists(syncFile)) { - *sync = new Pdfsync(syncFile, engine); + *sync = new Pdfsync(syncFile, path, engine); return *sync ? PDFSYNCERR_SUCCESS : PDFSYNCERR_OUTOFMEMORY; } @@ -147,7 +150,7 @@ int Synchronizer::Create(const char* path, EngineBase* engine, Synchronizer** sy if (file::Exists(texGzFile) || file::Exists(texFile)) { // due to a bug with synctex_parser.c, this must always be // the path to the .synctex file (even if a .synctex.gz file is used instead) - *sync = new SyncTex(texFile, engine); + *sync = new SyncTex(texFile, path, engine); return *sync ? PDFSYNCERR_SUCCESS : PDFSYNCERR_OUTOFMEMORY; } @@ -307,6 +310,19 @@ static int cmpLineRecords(const void* a, const void* b) { return ((PdfsyncLine*)a)->record - ((PdfsyncLine*)b)->record; } +// If `srcfilepath` doesn't exist on disk, checks whether it's been moved to sit +// next to the PDF document (which happens if all files are moved together) +static void TryRecoverMovedSourceFile(AutoFreeStr& srcfilepath, const char* pdfPath) { + if (file::Exists(srcfilepath)) { + return; + } + TempStr altsrcpath = path::GetDirTemp(pdfPath); + altsrcpath = path::JoinTemp(altsrcpath, path::GetBaseNameTemp(srcfilepath)); + if (!str::Eq(altsrcpath, srcfilepath) && file::Exists(altsrcpath)) { + srcfilepath.SetCopy(altsrcpath); + } +} + int Pdfsync::DocToSource(int pageNo, Point pt, AutoFreeStr& filename, int* line, int* col) { int res = RebuildIndexIfNeeded(); if (res != PDFSYNCERR_SUCCESS) { @@ -368,7 +384,9 @@ int Pdfsync::DocToSource(int pageNo, Point pt, AutoFreeStr& filename, int* line, } char* path = srcfiles[found->file]; - filename.SetCopy(path); + filename.SetCopy(path::NormalizeTemp(path)); + TryRecoverMovedSourceFile(filename, pdfPath); + *line = (int)found->line; *col = (int)found->column; if (*col < 0) { @@ -780,6 +798,8 @@ int SyncTex::DocToSource(int pageNo, Point pt, AutoFreeStr& filename, int* line, if (!path::IsAbsolute(filename)) { filename.Set(PrependDir(filename)); } + filename.SetCopy(path::NormalizeTemp(filename.Get())); + TryRecoverMovedSourceFile(filename, pdfPath); *line = synctex_node_line(node); *col = synctex_node_column(node); diff --git a/src/PdfSync.h b/src/PdfSync.h index cc9c67ab70ad..0c83dbdc0cbe 100644 --- a/src/PdfSync.h +++ b/src/PdfSync.h @@ -22,7 +22,7 @@ class EngineBase; class Synchronizer { public: - explicit Synchronizer(const char* syncfilepath); + explicit Synchronizer(const char* syncfilepath, const char* pdffilename); virtual ~Synchronizer() = default; // Inverse-search: @@ -51,6 +51,7 @@ class Synchronizer { char* PrependDir(const char* filename) const; AutoFreeStr syncFilePath; // path to the synchronization file + AutoFreeStr pdfPath; public: static int Create(const char* pdffilename, EngineBase* engine, Synchronizer** sync); diff --git a/src/SearchAndDDE.cpp b/src/SearchAndDDE.cpp index 05796fd516fa..90ecf19af76c 100644 --- a/src/SearchAndDDE.cpp +++ b/src/SearchAndDDE.cpp @@ -583,16 +583,6 @@ bool OnInverseSearch(MainWindow* win, int x, int y) { return true; } - if (!file::Exists(srcfilepath)) { - // if the source file is missing, check if it's been moved to the same place as - // the PDF document (which happens if all files are moved together) - TempStr altsrcpath = path::GetDirTemp(tab->filePath); - altsrcpath = path::JoinTemp(altsrcpath, path::GetBaseNameTemp(srcfilepath)); - if (!str::Eq(altsrcpath, srcfilepath) && file::Exists(altsrcpath)) { - srcfilepath.SetCopy(altsrcpath); - } - } - char* inverseSearch = gGlobalPrefs->inverseSearchCmdLine; if (!inverseSearch) { Vec editors; From 62c7d6ba942d12cf8213378f9439496d77ae9e04 Mon Sep 17 00:00:00 2001 From: Henok Date: Sun, 21 Jun 2026 12:24:55 +0300 Subject: [PATCH 02/10] Add tests for WSL SyncTeX forward/inverse search Adds tests/issue-5702.ts, covering forward and inverse search across two WSL-related workflows that don't work yet: 1. files on the WSL filesystem, compiled from inside WSL 2. files on the Windows filesystem, compiled from inside WSL (e.g. via wsl.exe -d Ubuntu -- tectonic against a /mnt/c/... path) Uses Tectonic (run via WSL) to produce a real PDF + .synctex.gz pair for each workflow, then drives the control-pipe TestSynctex and TestInverseSearch commands to query SourceToDoc/DocToSource directly. All four scenarios fail at this commit, as expected, the fixes land in the following commits, one scenario at a time. --- cmd/control.ts | 1 + src/SumatraControl.cpp | 12 ++ src/SumatraTest.cpp | 40 +++- src/SumatraTest.h | 1 + tests/all.ts | 2 + tests/issue-5702-data/.gitignore | 2 + tests/issue-5702-data/test.tex | 11 ++ tests/issue-5702.ts | 322 +++++++++++++++++++++++++++++++ 8 files changed, 390 insertions(+), 1 deletion(-) create mode 100644 tests/issue-5702-data/.gitignore create mode 100644 tests/issue-5702-data/test.tex create mode 100644 tests/issue-5702.ts diff --git a/cmd/control.ts b/cmd/control.ts index 3140a61f54ad..b3e5b6198ace 100644 --- a/cmd/control.ts +++ b/cmd/control.ts @@ -10,6 +10,7 @@ export enum ControlCommand { TestChm = 14, TestSelectionTranslate = 15, TestTripleClickLineSelect = 16, + TestInverseSearch = 22, } export type ControlArg = number | string | Uint8Array | ControlArg[]; diff --git a/src/SumatraControl.cpp b/src/SumatraControl.cpp index 68a62c267792..8e015669d35a 100644 --- a/src/SumatraControl.cpp +++ b/src/SumatraControl.cpp @@ -28,6 +28,7 @@ enum class ControlCmd : u16 { TestChm = 14, TestSelectionTranslate = 15, TestTripleClickLineSelect = 16, + TestInverseSearch = 22, }; enum class ControlArgType : u16 { @@ -291,6 +292,17 @@ static void ExecuteControlRequest(ControlRequest* req) { break; } + case ControlCmd::TestInverseSearch: { + i32 page = 0, x = 0, y = 0; + const char* pdf = StringArg(req, 0); + if (!pdf || !IntArg(req, 1, page) || !IntArg(req, 2, x) || !IntArg(req, 3, y)) { + AppendError(req, "TestInverseSearch expects string pdf, int page, int x, int y"); + break; + } + AppendTestResult(req, 0, TestInverseSearchResult(pdf, page, x, y)); + break; + } + case ControlCmd::TestSearch: { const char* pdf = StringArg(req, 0); const char* needle = StringArg(req, 1); diff --git a/src/SumatraTest.cpp b/src/SumatraTest.cpp index 5d7ad6dcb15b..159dade75c02 100644 --- a/src/SumatraTest.cpp +++ b/src/SumatraTest.cpp @@ -54,7 +54,45 @@ char* TestSynctexResult(const char* pdfPath, const char* srcPath, int line) { int page = 0; Vec rects; int ret = sync->SourceToDoc(srcPath, line, 0, &page, rects); - out.AppendFmt("ret=%d page=%d nrects=%d src=%s line=%d\n", ret, page, rects.Size(), srcPath, line); + out.AppendFmt("ret=%d page=%d nrects=%d src=%s line=%d", ret, page, rects.Size(), srcPath, line); + if (rects.Size() > 0) { + Rect r = rects.at(0); + out.AppendFmt(" rect_x=%d rect_y=%d rect_dx=%d rect_dy=%d\n", r.x, r.y, r.dx, r.dy); + } + delete sync; + } + SafeEngineRelease(&engine); + } + + return out.StealData(); +} + +// Headless inverse-search test for issue #5702. Loads the pdf, creates a +// Synchronizer, and resolves (page, point) -> (srcfile, line, col) via +// DocToSource, returning a machine-readable result line. +char* TestInverseSearchResult(const char* pdfPath, int pageNo, int x, int y) { + ScopedGdiPlus gdiPlus; + EnsureTestGlobalPrefs(); + + StrBuilder out; + EngineBase* engine = CreateEngineFromFile(pdfPath, nullptr, false); + if (!engine) { + out.AppendFmt("ERROR engine-create-failed pdf=%s\n", pdfPath); + } else { + Synchronizer* sync = nullptr; + int err = Synchronizer::Create(pdfPath, engine, &sync); + if (err != PDFSYNCERR_SUCCESS || !sync) { + out.AppendFmt("ERROR sync-create-failed err=%d\n", err); + } else { + AutoFreeStr srcfilepath; + int line = 0, col = 0; + Point pt(x, y); + int ret = sync->DocToSource(pageNo, pt, srcfilepath, &line, &col); + if (ret != PDFSYNCERR_SUCCESS) { + out.AppendFmt("ERROR doctosource-failed err=%d\n", ret); + } else { + out.AppendFmt("ret=%d srcfile=%s line=%d col=%d\n", ret, srcfilepath.Get(), line, col); + } delete sync; } SafeEngineRelease(&engine); diff --git a/src/SumatraTest.h b/src/SumatraTest.h index 81a011d2c4a3..790544bc27b4 100644 --- a/src/SumatraTest.h +++ b/src/SumatraTest.h @@ -2,6 +2,7 @@ License: GPLv3 */ char* TestSynctexResult(const char* pdfPath, const char* srcPath, int line); +char* TestInverseSearchResult(const char* pdfPath, int pageNo, int x, int y); char* TestSearchResult(const char* pdfPath, const char* needle, const char* password = nullptr); char* TestDestResult(const char* pdfPath, int destNo); char* TestNamedDestResult(const char* pdfPath, const char* destName); diff --git a/tests/all.ts b/tests/all.ts index c592cd486ff9..eaf7cc9f3b9b 100644 --- a/tests/all.ts +++ b/tests/all.ts @@ -24,6 +24,7 @@ import { testit as issue5642 } from "./issue-5642.ts"; import { testit as issue5665 } from "./issue-5665.ts"; import { testit as issue5677 } from "./issue-5677.ts"; import { testit as issue5681 } from "./issue-5681.ts"; +import { testit as issue5702 } from "./issue-5702.ts"; const tests: [string, () => void | Promise][] = [ ["cmd-start-autoscroll", cmdStartAutoScroll], @@ -40,6 +41,7 @@ const tests: [string, () => void | Promise][] = [ ["issue-5665", issue5665], ["issue-5677", issue5677], ["issue-5681", issue5681], + ["issue-5702", issue5702], ]; export type AllTestOptions = { diff --git a/tests/issue-5702-data/.gitignore b/tests/issue-5702-data/.gitignore new file mode 100644 index 000000000000..fa14b6f9c822 --- /dev/null +++ b/tests/issue-5702-data/.gitignore @@ -0,0 +1,2 @@ +# scratch dir created at runtime by tests/issue-5702.ts for tectonic output and test results +.work/ diff --git a/tests/issue-5702-data/test.tex b/tests/issue-5702-data/test.tex new file mode 100644 index 000000000000..f39175085d76 --- /dev/null +++ b/tests/issue-5702-data/test.tex @@ -0,0 +1,11 @@ +\documentclass{article} +\begin{document} + +First paragraph of text on line four. It contains enough words that the +typesetter places it into a paragraph and synctex records a position that +maps back to this source line. + +Second paragraph of text on line eight, also with plenty of words so that +synctex emits boxes that can be mapped back to this exact source line. + +\end{document} diff --git a/tests/issue-5702.ts b/tests/issue-5702.ts new file mode 100644 index 000000000000..afbc6c0094ad --- /dev/null +++ b/tests/issue-5702.ts @@ -0,0 +1,322 @@ +// Test for https://github.com/sumatrapdfreader/sumatrapdf/issues/5702 +// +// Verifies that SyncTeX forward and inverse search work across two +// previously-unsupported WSL workflows: +// 1. files on the WSL filesystem, compiled from inside WSL +// 2. files on the Windows filesystem, compiled from inside WSL +// (e.g. via `wsl.exe -d Ubuntu -- tectonic ...` against /mnt/c/...) +// +// Uses Tectonic (run via WSL) to produce a real PDF + .synctex.gz pair for +// each workflow, then runs the control pipe TestSynctex/TestInverseSearch +// commands, which query SourceToDoc/DocToSource directly and check the +// results. +// +// Run: bun tests/issue-5702.ts [--no-build] (or via tests/all.ts) + +import { existsSync, mkdirSync, rmSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { EXE, runStandalone } from "./util.ts"; +import { ControlClient, ControlCommand, withControlledSumatra } from "../cmd/control.ts"; + +const DATA = join(import.meta.dir, "issue-5702-data"); +const WORK = join(DATA, ".work"); + +const TEX_NAME = "test.tex"; +const TEX_SRC = join(DATA, TEX_NAME); +const PDF_NAME = TEX_NAME.replace(/\.tex$/, ".pdf"); +const SYNCTEX_NAME = TEX_NAME.replace(/\.tex$/, ".synctex.gz"); + +const WSL_DISTRO = "Ubuntu"; +const WSL_UNC_ROOT = `\\\\wsl.localhost\\${WSL_DISTRO}`; +const WSL_TEST_DIR_UNIX = "/tmp/sumatra-test-5702"; +const WSL_TEST_DIR_UNC = join(WSL_UNC_ROOT, "tmp", "sumatra-test-5702"); + +// line 4 of issue-5702-data/test.tex is a body paragraph that synctex maps to a position +const TARGET_LINE = 4; + +type Rect = { + x: number, + y: number, + dx: number, + dy: number, +} + +type FwdSearchResult = { + ret: number; + page: number; + nrects: number; + rect?: Rect, + raw: string; +}; + +type FwdSearchPoint = { page: number; x: number; y: number }; + +type InvSearchResult = { + ret: number; + srcfile: string; + line: number; + col: number; + raw: string; +}; + + +function run(cmd: string[]): { ok: boolean; stdout: string; stderr: string } { + const p = Bun.spawnSync({ cmd, stdout: "pipe", stderr: "pipe" }); + return { + ok: p.exitCode === 0, + stdout: p.stdout.toString(), + stderr: p.stderr.toString(), + }; +} + +function findWslTectonic(): boolean { + const r = run(["wsl.exe", "-d", WSL_DISTRO, "--", "tectonic", "--version"]); + if (!r.ok) { + console.log( + `\nSKIP issue-5702: WSL distro '${WSL_DISTRO}' with tectonic not available, skipping.\n` + + `To run this test, install a WSL distro and tectonic inside it, e.g.:\n` + + ` wsl --install -d ${WSL_DISTRO}\n` + + ` wsl -d ${WSL_DISTRO} -- bash -lc "curl --proto '=https' --tlsv1.2 -fsSL https://drop-sh.fullyjustified.net |sh"`, + ); + } + return r.ok; +} + +// converts a Windows absolute path (e.g. "C:\foo\bar.tex") to its WSL +// mount-path equivalent (e.g. "/mnt/c/foo/bar.tex"), for invoking tectonic +// from inside WSL and for computing expected DocToSource results. +function windowsPathToWslMountPath(winPath: string): string { + const m = winPath.match(/^([A-Za-z]):[\\/](.*)$/); + if (!m) { + throw new Error(`not a Windows absolute path: ${winPath}`); + } + const drive = m[1].toLowerCase(); + const rest = m[2].replace(/\\/g, "/"); + return `/mnt/${drive}/${rest}`; +} + +// runs the app's control-pipe synctex forward-search for the given pdf and +// returns the parsed result (ret is a PDFSYNCERR_* code; 0 == PDFSYNCERR_SUCCESS), +// including the first rect's coordinates. +async function forwardSearch( + client: ControlClient, + pdfPath: string, + srcPath: string, +): Promise { + const [, rawArg] = await client.request(ControlCommand.TestSynctex, [pdfPath, srcPath, TARGET_LINE]); + const raw = String(rawArg).trim(); + + const m = raw.match(/ret=(-?\d+)\s+page=(-?\d+)\s+nrects=(-?\d+)/); + if (!m) throw Error(`(forwardSearch: parse error) raw: ${raw}`); + const result: FwdSearchResult = { + ret: parseInt(m[1]), + page: parseInt(m[2]), + nrects: parseInt(m[3]), + raw, + }; + const n = raw.match( + /rect_x=(-?\d+)\s+rect_y=(-?\d+)\s+rect_dx=(-?\d+)\s+rect_dy=(-?\d+)/, + ); + if (n) { + result["rect"] = { + x: parseInt(n[1]), + y: parseInt(n[2]), + dx: parseInt(n[3]), + dy: parseInt(n[4]), + }; + } + + return result; +} + +// runs the app's control-pipe synctex inverse-search for the given pdf and +// returns the parsed result (ret is a PDFSYNCERR_* code; 0 == PDFSYNCERR_SUCCESS) +async function inverseSearch( + client: ControlClient, + pdfPath: string, + page: number, + x: number, + y: number, +): Promise { + const [, rawArg] = await client.request(ControlCommand.TestInverseSearch, [pdfPath, page, x, y]); + const raw = String(rawArg).trim(); + + const m = raw.match(/ret=(-?\d+)\s+srcfile=(.*)\s+line=(-?\d+)\s+col=(-?\d+)/); + if (!m) { + throw Error(`(inverseSearch: parse error) raw: ${raw}`); + } + return { ret: parseInt(m[1]), srcfile: m[2], line: parseInt(m[3]), col: parseInt(m[4]), raw }; +} + +// derives a (page, x, y) point from a forward-search result, taking the +// center of the rect TARGET_LINE mapped to. Returns null if the result +// wasn't a usable match (e.g. the forward search itself failed). +function pointFromFwdSearchResult(res: FwdSearchResult): FwdSearchPoint | null { + if (res.ret !== 0 || res.page < 1 || res.nrects < 1 || !res.rect) { + return null; + } + return { + page: res.page, + x: Math.round(res.rect.x + res.rect.dx / 2), + y: Math.round(res.rect.y + res.rect.dy / 2), + }; +} + +// verifies a tectonic compile produced both the pdf and synctex.gz; throws +// with captured stdout/stderr otherwise. +function verifyCompileOutput( + compile: { ok: boolean; stdout: string; stderr: string }, + pdfPath: string, + synctexPath: string, + label: string, +): void { + if (!compile.ok || !existsSync(pdfPath) || !existsSync(synctexPath)) { + console.error(compile.stdout); + console.error(compile.stderr); + throw new Error(`tectonic did not produce ${pdfPath} + ${synctexPath} (${label})`); + } +} + +// compiles issue-5702-data/test.tex on the Windows filesystem via +// `wsl.exe -d Ubuntu -- tectonic ...` (workflow: Windows files, WSL compile). +// Returns the Windows paths to the resulting pdf and source file. +function compileWinFiles(): { pdfPath: string; srcPath: string } { + rmSync(WORK, { recursive: true, force: true }); + mkdirSync(WORK, { recursive: true }); + + const wslTexPath = windowsPathToWslMountPath(TEX_SRC); + const wslOutDir = windowsPathToWslMountPath(WORK); + + console.log(`• compiling ${TEX_SRC} via wsl -d ${WSL_DISTRO} -- tectonic ...`); + const compile = run([ + "wsl.exe", "-d", WSL_DISTRO, "--", + "tectonic", "-X", "compile", wslTexPath, "--synctex", "--outdir", wslOutDir, + ]); + + const pdfPath = join(WORK, PDF_NAME); + const synctexPath = join(WORK, SYNCTEX_NAME); + verifyCompileOutput(compile, pdfPath, synctexPath, "win files"); + + return { pdfPath, srcPath: TEX_SRC }; +} + +// copies issue-5702-data/test.tex onto the WSL filesystem (via its UNC path) +// and compiles it there (workflow: WSL files, WSL compile). Returns the WSL +// UNC paths to the resulting pdf and source file. +function compileWslFiles(): { pdfPath: string; srcPath: string } { + rmSync(WSL_TEST_DIR_UNC, { recursive: true, force: true }); + mkdirSync(WSL_TEST_DIR_UNC, { recursive: true }); + + const texContent = readFileSync(TEX_SRC); + const srcPath = join(WSL_TEST_DIR_UNC, TEX_NAME); + writeFileSync(srcPath, texContent); + + console.log(`• compiling ${WSL_TEST_DIR_UNIX}/${TEX_NAME} via wsl -d ${WSL_DISTRO} -- tectonic ...`); + const compile = run([ + "wsl.exe", "-d", WSL_DISTRO, "--", + "tectonic", "-X", "compile", `${WSL_TEST_DIR_UNIX}/${TEX_NAME}`, "--synctex", + ]); + + const pdfPath = join(WSL_TEST_DIR_UNC, PDF_NAME); + const synctexPath = join(WSL_TEST_DIR_UNC, SYNCTEX_NAME); + verifyCompileOutput(compile, pdfPath, synctexPath, "wsl files"); + + return { pdfPath, srcPath }; +} + +// forward search resolves for a .tex file on the Windows filesystem, +// compiled via wsl.exe -d Ubuntu -- tectonic against its /mnt/c/... path. +async function testForwardSearchWinFiles( + client: ControlClient, pdfPath: string, srcPath: string): Promise<{ ok: boolean, result: FwdSearchResult }> { + const res = await forwardSearch(client, pdfPath, srcPath); + const pass = res.ret === 0 && res.page >= 1 && res.nrects >= 1; + console.log(`${pass ? "PASS" : "FAIL"} forward search (Windows file) -> ${res.raw}`); + return { ok: pass, result: res }; +} + +// forward search resolves for a .tex file living entirely on the WSL +// filesystem, compiled from inside WSL. +async function testForwardSearchWslFiles( + client: ControlClient, pdfPath: string, srcPath: string): Promise<{ ok: boolean, result: FwdSearchResult }> { + const res = await forwardSearch(client, pdfPath, srcPath); + const pass = res.ret === 0 && res.page >= 1 && res.nrects >= 1; + console.log(`${pass ? "PASS" : "FAIL"} forward search (WSL file) -> ${res.raw}`); + return { ok: pass, result: res }; +} + +// inverse search resolves for a .tex file on the Windows filesystem, +// compiled via WSL, DocToSource should return the /mnt/c/... path +// recorded in the synctex file. +async function testInverseSearchWinFiles( + client: ControlClient, pdfPath: string, srcPath: string, fwdResult: FwdSearchResult): Promise<{ ok: boolean }> { + const pt = pointFromFwdSearchResult(fwdResult); + if (!pt) { + console.log(`FAIL inverse search (Windows file) -> could not discover point: ${fwdResult.raw}`); + return { ok: false }; + } + const expectedSrcFile = windowsPathToWslMountPath(srcPath); + const res = await inverseSearch(client, pdfPath, pt.page, pt.x, pt.y); + const pass = res.ret === 0 && res.srcfile === expectedSrcFile && res.line === TARGET_LINE; + console.log(`${pass ? "PASS" : "FAIL"} inverse search (Windows file) -> ${res.raw}`); + return { ok: pass }; +} + +// inverse search resolves for a .tex file living entirely on the WSL +// filesystem, DocToSource should return the plain Unix path recorded in +// the synctex file. +async function testInverseSearchWslFiles( + client: ControlClient, pdfPath: string, srcPath: string, fwdResult: FwdSearchResult): Promise<{ ok: boolean }> { + const pt = pointFromFwdSearchResult(fwdResult); + if (!pt) { + console.log(`FAIL inverse search (WSL file) -> could not discover point: ${fwdResult.raw}`); + return { ok: false }; + } + const expectedSrcFile = `${WSL_TEST_DIR_UNIX}/${TEX_NAME}`; + const res = await inverseSearch(client, pdfPath, pt.page, pt.x, pt.y); + const pass = res.ret === 0 && res.srcfile === expectedSrcFile && res.line === TARGET_LINE; + console.log(`${pass ? "PASS" : "FAIL"} inverse search (WSL file) -> ${res.raw}`); + return { ok: pass }; +} + +// --------------------------------------------------------------------------- + +export async function testit(): Promise { + if (!existsSync(EXE)) { + throw new Error(`app not found: ${EXE} (build first)`); + } + if (!existsSync(TEX_SRC)) { + throw new Error(`missing test fixture: ${TEX_SRC}`); + } + if (!findWslTectonic()) { + return; + } + + // compile the fixture for Windows and WSL before running any test + const winFiles = compileWinFiles(); + const wslFiles = compileWslFiles(); + + const results = await withControlledSumatra(EXE, async (client) => { + const fwdWin = await testForwardSearchWinFiles(client, winFiles.pdfPath, winFiles.srcPath); + const fwdWsl = await testForwardSearchWslFiles(client, wslFiles.pdfPath, wslFiles.srcPath); + const invWin = await testInverseSearchWinFiles(client, winFiles.pdfPath, winFiles.srcPath, fwdWin.result); + const invWsl = await testInverseSearchWslFiles(client, wslFiles.pdfPath, wslFiles.srcPath, fwdWsl.result); + return [ + { name: "forward search(win files)", ok: fwdWin.ok }, + { name: "forward search(wsl files)", ok: fwdWsl.ok }, + { name: "inverse search(win files)", ok: invWin.ok }, + { name: "inverse search(wsl files)", ok: invWsl.ok }, + ]; + }); + + const failed = results.filter((r) => !r.ok); + console.log(""); + if (failed.length > 0) { + throw new Error(`${failed.length}/${results.length} scenarios failed: ${failed.map((f) => f.name).join(", ")}`); + } + + console.log(`PASS issue-5702: all ${results.length} WSL synctex scenarios resolved correctly`); +} + +if (import.meta.main) { + await runStandalone(testit); +} From e9499292e0ddff76db7f3620bd218990521dc6a8 Mon Sep 17 00:00:00 2001 From: Henok Date: Sun, 21 Jun 2026 12:36:21 +0300 Subject: [PATCH 03/10] Fix SyncTeX forward search for files on the WSL filesystem `SyncTex::SourceToDoc` now converts a WSL UNC source path (`\\wsl.localhost\Ubuntu\...`) to its Unix equivalent before querying SyncTeX, since the `.synctex.gz` file (compiled inside WSL) records the plain Unix path, not the UNC form Sumatra receives when the user opens the file. 1/4 scenarios in tests/issue-5702.ts now pass (forward search, WSL files). --- src/PdfSync.cpp | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/PdfSync.cpp b/src/PdfSync.cpp index dcc480132ce1..cc1826ff5aca 100644 --- a/src/PdfSync.cpp +++ b/src/PdfSync.cpp @@ -762,6 +762,38 @@ int SyncTex::RebuildIndexIfNeeded() { return MarkIndexWasRebuilt(); } +// Converts a WSL UNC path to its equivalent Unix path by stripping the +// \\wsl.localhost\\ or \\wsl$\\ prefix. +// e.g. "\\wsl.localhost\Ubuntu\home\user\file.tex" -> "/home/user/file.tex" +static TempStr WslUncPathToUnixPathTemp(const char* srcfilepath) { + if (!srcfilepath) { + return nullptr; + } + + const char* p = nullptr; + + // Strip the WSL UNC prefix + if (str::StartsWithI(srcfilepath, "\\\\wsl.localhost\\")) { + p = srcfilepath + str::Len("\\\\wsl.localhost\\"); + } else if (str::StartsWithI(srcfilepath, "\\\\wsl$\\")) { + p = srcfilepath + str::Len("\\\\wsl$\\"); + } else { + return nullptr; + } + + // Skip the distribution name, e.g. "Ubuntu" in "\\wsl.localhost\Ubuntu\home\..." + while (*p && !path::IsSep(*p)) { + p++; + } + if (!path::IsSep(*p) || !p[1]) { + return nullptr; + } + + TempStr unixPath = str::JoinTemp("/", p + 1); + str::TransCharsInPlace(unixPath, "\\", "/"); + return unixPath; +} + int SyncTex::DocToSource(int pageNo, Point pt, AutoFreeStr& filename, int* line, int* col) { logfa("SyncTex::DocToSource: '%s', pageNo: %d\n", syncFilePath.Get(), pageNo); int res = RebuildIndexIfNeeded(); @@ -829,6 +861,10 @@ int SyncTex::SourceToDoc(const char* srcfilename, int line, int col, int* page, return PDFSYNCERR_OUTOFMEMORY; } + if (TempStr unixSrcFilePath = WslUncPathToUnixPathTemp(srcfilepath)) { + srcfilepath = unixSrcFilePath; + } + // dealed in SyncTex::RebuildIndexIfNeeded() int ret = synctex_display_query(this->scanner, srcfilepath, line, col, 0); From 65f4e0510c12436bd6068a370a1bcbedc7825b27 Mon Sep 17 00:00:00 2001 From: Henok Date: Sun, 21 Jun 2026 12:50:05 +0300 Subject: [PATCH 04/10] Fix SyncTeX inverse search for files on the WSL filesystem `SyncTex::DocToSource` now detects when the sync file itself lives on WSL and, in that case, treats the recorded source path as a Unix path: resolves relative paths against the sync file's own directory instead of treating it as a Windows path, and skips Windows-specific normalization (`path::NormalizeTemp`) and the missing-source-file recovery fallback, neither of which are meaningful for a Unix path. 2/4 scenarios in tests/issue-5702.ts now pass (forward + inverse search, WSL files). --- src/PdfSync.cpp | 42 +++++++++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/src/PdfSync.cpp b/src/PdfSync.cpp index cc1826ff5aca..d6e37c12fe05 100644 --- a/src/PdfSync.cpp +++ b/src/PdfSync.cpp @@ -762,6 +762,19 @@ int SyncTex::RebuildIndexIfNeeded() { return MarkIndexWasRebuilt(); } +// Returns true if the sync file itself lives on WSL +static bool IsUnixSourcePath(const char* syncFilePath) { + if (!syncFilePath) { + return false; + } + + if (str::StartsWithI(syncFilePath, "\\\\wsl.localhost\\") || str::StartsWithI(syncFilePath, "\\\\wsl$\\")) { + return true; + } + + return false; +} + // Converts a WSL UNC path to its equivalent Unix path by stripping the // \\wsl.localhost\\ or \\wsl$\\ prefix. // e.g. "\\wsl.localhost\Ubuntu\home\user\file.tex" -> "/home/user/file.tex" @@ -824,14 +837,29 @@ int SyncTex::DocToSource(int pageNo, Point pt, AutoFreeStr& filename, int* line, return PDFSYNCERR_OUTOFMEMORY; } - // undecorate the filepath: replace * by space and / by \ (backslash) - str::TransCharsInPlace(filename, "*/", " \\"); - // Convert the source filepath to an absolute path - if (!path::IsAbsolute(filename)) { - filename.Set(PrependDir(filename)); + // Unescape SyncTeX's space encoding: * represents a space in filenames + str::TransCharsInPlace(filename, "*", " "); + + if (IsUnixSourcePath(syncFilePath.Get())) { + // Treat filename as unix path + + // Resolve relative Unix paths relative to the sync file's directory + if (filename[0] != '/') { + TempStr unixSyncFilePath = WslUncPathToUnixPathTemp(syncFilePath.Get()); + TempStr dir = path::GetDirTemp(unixSyncFilePath); + filename.Set(path::Join(dir, filename)); + } + } else { + // Treat filename as Windows path + + str::TransCharsInPlace(filename, "/", "\\"); + // Convert the source filepath to an absolute path + if (!path::IsAbsolute(filename)) { + filename.Set(PrependDir(filename)); + } + filename.SetCopy(path::NormalizeTemp(filename.Get())); + TryRecoverMovedSourceFile(filename, pdfPath); } - filename.SetCopy(path::NormalizeTemp(filename.Get())); - TryRecoverMovedSourceFile(filename, pdfPath); *line = synctex_node_line(node); *col = synctex_node_column(node); From 356d84ceab8667a504b2991418c9cdae128ae2b5 Mon Sep 17 00:00:00 2001 From: Henok Date: Sun, 21 Jun 2026 13:01:42 +0300 Subject: [PATCH 05/10] Fix SyncTeX forward search for Windows files compiled via WSL If a `.tex` file lives on a Windows drive but was compiled from inside WSL (e.g. wsl.exe -d Ubuntu -- tectonic against a /mnt/c/... path), the resulting `.synctex.gz` records Unix-style `/mnt/c/...` paths even though Sumatra receives a native Windows path when the user opens the file. `SyncTex::SourceToDoc` now retries with the path translated to its WSL mount-path equivalent if the initial query (using the Windows path as-is) fails. 3/4 scenarios in tests/issue-5702.ts now pass (forward search for both workflows, inverse search for WSL files). --- src/PdfSync.cpp | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/PdfSync.cpp b/src/PdfSync.cpp index d6e37c12fe05..af81bf448942 100644 --- a/src/PdfSync.cpp +++ b/src/PdfSync.cpp @@ -870,6 +870,24 @@ int SyncTex::DocToSource(int pageNo, Point pt, AutoFreeStr& filename, int* line, return PDFSYNCERR_SUCCESS; } +// Converts a Windows absolute path to its WSL mount-path equivalent. +// e.g. "C:\project\file.tex" -> "/mnt/c/project/file.tex" +static TempStr WindowsPathToWslMountPathTemp(const char* path) { + if (!path) { + return nullptr; + } + + // Require an absolute Windows path (e.g. "C:\") + if (!(std::isalpha((unsigned char)path[0]) && path[1] == ':' && path[2] == '\\')) { + return nullptr; + } + + char drive = (char)tolower((unsigned char)path[0]); + TempStr rest = str::DupTemp(path + 3); // Skip "[drive]:\" + str::TransCharsInPlace(rest, "\\", "/"); + return str::FormatTemp("/mnt/%c/%s", drive, rest); +} + int SyncTex::SourceToDoc(const char* srcfilename, int line, int col, int* page, Vec& rects) { logfa("SyncTex::SourceToDoc: '%s', line: %d, col: %d\n", srcfilename, line, col); int res = RebuildIndexIfNeeded(); @@ -896,6 +914,16 @@ int SyncTex::SourceToDoc(const char* srcfilename, int line, int col, int* page, // dealed in SyncTex::RebuildIndexIfNeeded() int ret = synctex_display_query(this->scanner, srcfilepath, line, col, 0); + if (ret <= 0) { + if (TempStr wslMountSrcFilePath = WindowsPathToWslMountPathTemp(srcfilepath)) { + srcfilepath = wslMountSrcFilePath; + logfa("SyncTex::SourceToDoc: retrying with WSL mount path '%s'\n", srcfilepath); + int ret2 = synctex_display_query(this->scanner, srcfilepath, line, col, 0); + if (ret2 > 0) { + ret = ret2; + } + } + } if (-1 == ret) { return PDFSYNCERR_UNKNOWN_SOURCEFILE; } From 3b38e9bd6e68a451312d2574550c652d75b20ecb Mon Sep 17 00:00:00 2001 From: Henok Date: Sun, 21 Jun 2026 13:07:31 +0300 Subject: [PATCH 06/10] Fix SyncTeX inverse search for Windows files compiled via WSL `SyncTex::DocToSource` now also treats a recorded source path as Unix-style if it starts with `/mnt/`, covering the case where the PDF lives on a Windows drive but was compiled from inside WSL. As with the WSL-files case, this means resolving relative paths against the sync file's directory and skipping Windows-specific normalization and the missing-source-file recovery fallback for these paths. 4/4 scenarios in tests/issue-5702.ts now pass. --- src/PdfSync.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/PdfSync.cpp b/src/PdfSync.cpp index af81bf448942..156d3bebb39c 100644 --- a/src/PdfSync.cpp +++ b/src/PdfSync.cpp @@ -762,9 +762,12 @@ int SyncTex::RebuildIndexIfNeeded() { return MarkIndexWasRebuilt(); } -// Returns true if the sync file itself lives on WSL -static bool IsUnixSourcePath(const char* syncFilePath) { - if (!syncFilePath) { +// Decides whether `resolvedSrcPath` should be treated as a Unix path rather than +// a Windows path. Returns true if the sync file itself lives on WSL, or if the +// resolved path is a WSl mount path (e.g. /mnt/c/...), which happens when the PDF +// lives on a Windows drive but was compiled from inside WSL. +static bool IsUnixSourcePath(const char* syncFilePath, const char* resolvedSrcPath) { + if (!syncFilePath || !resolvedSrcPath) { return false; } @@ -772,6 +775,10 @@ static bool IsUnixSourcePath(const char* syncFilePath) { return true; } + if (str::StartsWithI(resolvedSrcPath, "/mnt/")) { + return true; + } + return false; } @@ -840,7 +847,7 @@ int SyncTex::DocToSource(int pageNo, Point pt, AutoFreeStr& filename, int* line, // Unescape SyncTeX's space encoding: * represents a space in filenames str::TransCharsInPlace(filename, "*", " "); - if (IsUnixSourcePath(syncFilePath.Get())) { + if (IsUnixSourcePath(syncFilePath.Get(), filename)) { // Treat filename as unix path // Resolve relative Unix paths relative to the sync file's directory From abddab9815d8ea5fd51770fd4d0152376023be66 Mon Sep 17 00:00:00 2001 From: Henok Date: Mon, 22 Jun 2026 02:51:10 +0300 Subject: [PATCH 07/10] Improve code reusability in tests for WSL SynctTex support `testForwardSearchWinFiles` and `testForwardSearchWslFiles` are merged into one generic `testForwardSearch` similarly a generic `testInverseSearch` now replaces `testInverseSearchWslFiles` and `testInverseSearchWinFiles` --- tests/issue-5702.ts | 79 ++++++++++++++++----------------------------- 1 file changed, 28 insertions(+), 51 deletions(-) diff --git a/tests/issue-5702.ts b/tests/issue-5702.ts index afbc6c0094ad..1e3113fd5491 100644 --- a/tests/issue-5702.ts +++ b/tests/issue-5702.ts @@ -168,7 +168,7 @@ function verifyCompileOutput( compile: { ok: boolean; stdout: string; stderr: string }, pdfPath: string, synctexPath: string, - label: string, + label: "Windows file" | "WSL file", ): void { if (!compile.ok || !existsSync(pdfPath) || !existsSync(synctexPath)) { console.error(compile.stdout); @@ -195,7 +195,7 @@ function compileWinFiles(): { pdfPath: string; srcPath: string } { const pdfPath = join(WORK, PDF_NAME); const synctexPath = join(WORK, SYNCTEX_NAME); - verifyCompileOutput(compile, pdfPath, synctexPath, "win files"); + verifyCompileOutput(compile, pdfPath, synctexPath, "Windows file"); return { pdfPath, srcPath: TEX_SRC }; } @@ -219,62 +219,39 @@ function compileWslFiles(): { pdfPath: string; srcPath: string } { const pdfPath = join(WSL_TEST_DIR_UNC, PDF_NAME); const synctexPath = join(WSL_TEST_DIR_UNC, SYNCTEX_NAME); - verifyCompileOutput(compile, pdfPath, synctexPath, "wsl files"); + verifyCompileOutput(compile, pdfPath, synctexPath, "WSL file"); return { pdfPath, srcPath }; } -// forward search resolves for a .tex file on the Windows filesystem, -// compiled via wsl.exe -d Ubuntu -- tectonic against its /mnt/c/... path. -async function testForwardSearchWinFiles( - client: ControlClient, pdfPath: string, srcPath: string): Promise<{ ok: boolean, result: FwdSearchResult }> { - const res = await forwardSearch(client, pdfPath, srcPath); - const pass = res.ret === 0 && res.page >= 1 && res.nrects >= 1; - console.log(`${pass ? "PASS" : "FAIL"} forward search (Windows file) -> ${res.raw}`); - return { ok: pass, result: res }; -} - -// forward search resolves for a .tex file living entirely on the WSL -// filesystem, compiled from inside WSL. -async function testForwardSearchWslFiles( - client: ControlClient, pdfPath: string, srcPath: string): Promise<{ ok: boolean, result: FwdSearchResult }> { - const res = await forwardSearch(client, pdfPath, srcPath); - const pass = res.ret === 0 && res.page >= 1 && res.nrects >= 1; - console.log(`${pass ? "PASS" : "FAIL"} forward search (WSL file) -> ${res.raw}`); - return { ok: pass, result: res }; -} - -// inverse search resolves for a .tex file on the Windows filesystem, -// compiled via WSL, DocToSource should return the /mnt/c/... path -// recorded in the synctex file. -async function testInverseSearchWinFiles( - client: ControlClient, pdfPath: string, srcPath: string, fwdResult: FwdSearchResult): Promise<{ ok: boolean }> { - const pt = pointFromFwdSearchResult(fwdResult); - if (!pt) { - console.log(`FAIL inverse search (Windows file) -> could not discover point: ${fwdResult.raw}`); - return { ok: false }; - } - const expectedSrcFile = windowsPathToWslMountPath(srcPath); - const res = await inverseSearch(client, pdfPath, pt.page, pt.x, pt.y); - const pass = res.ret === 0 && res.srcfile === expectedSrcFile && res.line === TARGET_LINE; - console.log(`${pass ? "PASS" : "FAIL"} inverse search (Windows file) -> ${res.raw}`); - return { ok: pass }; +async function testForwardSearch( + client: ControlClient, + pdfPath: string, + srcPath: string, + label: "Windows file" | "WSL file", +): Promise<{ ok: boolean, result: FwdSearchResult }> { + const res = await forwardSearch(client, pdfPath, srcPath); + const pass = res.ret === 0 && res.page >= 1 && res.nrects >= 1; + console.log(`${pass ? "PASS" : "FAIL"} forward search (${label}) -> ${res.raw}`); + return { ok: pass, result: res }; } -// inverse search resolves for a .tex file living entirely on the WSL -// filesystem, DocToSource should return the plain Unix path recorded in -// the synctex file. -async function testInverseSearchWslFiles( - client: ControlClient, pdfPath: string, srcPath: string, fwdResult: FwdSearchResult): Promise<{ ok: boolean }> { +async function testInverseSearch( + client: ControlClient, + pdfPath: string, + srcPath: string, + fwdResult: FwdSearchResult, + label: "Windows file" | "WSL file", +): Promise<{ ok: boolean }> { const pt = pointFromFwdSearchResult(fwdResult); if (!pt) { - console.log(`FAIL inverse search (WSL file) -> could not discover point: ${fwdResult.raw}`); + console.log(`FAIL inverse search (${label}) -> could not discover point: ${fwdResult.raw}`); return { ok: false }; } - const expectedSrcFile = `${WSL_TEST_DIR_UNIX}/${TEX_NAME}`; + const expectedSrcFile = label === "Windows file" ? windowsPathToWslMountPath(srcPath) : `${WSL_TEST_DIR_UNIX}/${TEX_NAME}`; const res = await inverseSearch(client, pdfPath, pt.page, pt.x, pt.y); const pass = res.ret === 0 && res.srcfile === expectedSrcFile && res.line === TARGET_LINE; - console.log(`${pass ? "PASS" : "FAIL"} inverse search (WSL file) -> ${res.raw}`); + console.log(`${pass ? "PASS" : "FAIL"} inverse search (${label}) -> ${res.raw}`); return { ok: pass }; } @@ -296,10 +273,10 @@ export async function testit(): Promise { const wslFiles = compileWslFiles(); const results = await withControlledSumatra(EXE, async (client) => { - const fwdWin = await testForwardSearchWinFiles(client, winFiles.pdfPath, winFiles.srcPath); - const fwdWsl = await testForwardSearchWslFiles(client, wslFiles.pdfPath, wslFiles.srcPath); - const invWin = await testInverseSearchWinFiles(client, winFiles.pdfPath, winFiles.srcPath, fwdWin.result); - const invWsl = await testInverseSearchWslFiles(client, wslFiles.pdfPath, wslFiles.srcPath, fwdWsl.result); + const fwdWin = await testForwardSearch(client, winFiles.pdfPath, winFiles.srcPath, "Windows file"); + const fwdWsl = await testForwardSearch(client, wslFiles.pdfPath, wslFiles.srcPath, "WSL file"); + const invWin = await testInverseSearch(client, winFiles.pdfPath, winFiles.srcPath, fwdWin.result, "Windows file"); + const invWsl = await testInverseSearch(client, wslFiles.pdfPath, wslFiles.srcPath, fwdWsl.result, "WSL file"); return [ { name: "forward search(win files)", ok: fwdWin.ok }, { name: "forward search(wsl files)", ok: fwdWsl.ok }, @@ -311,7 +288,7 @@ export async function testit(): Promise { const failed = results.filter((r) => !r.ok); console.log(""); if (failed.length > 0) { - throw new Error(`${failed.length}/${results.length} scenarios failed: ${failed.map((f) => f.name).join(", ")}`); + throw new Error(`${failed.length}/${results.length} tests failed: ${failed.map((f) => f.name).join(", ")}`); } console.log(`PASS issue-5702: all ${results.length} WSL synctex scenarios resolved correctly`); From 818ce869595ddcad18be4f363f0ee76caf5a2fe7 Mon Sep 17 00:00:00 2001 From: Henok Date: Mon, 22 Jun 2026 03:06:25 +0300 Subject: [PATCH 08/10] Add test to WSL synctex support tests to test forward search on forward-slash windows paths The test will fail as our wsl synctex support patch doesn't recognize forward-slash windows paths (e.g C:/) as windows paths. The next commit will fix this issue and make the test pass. --- tests/issue-5702.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/issue-5702.ts b/tests/issue-5702.ts index 1e3113fd5491..a24ce37a811c 100644 --- a/tests/issue-5702.ts +++ b/tests/issue-5702.ts @@ -277,11 +277,17 @@ export async function testit(): Promise { const fwdWsl = await testForwardSearch(client, wslFiles.pdfPath, wslFiles.srcPath, "WSL file"); const invWin = await testInverseSearch(client, winFiles.pdfPath, winFiles.srcPath, fwdWin.result, "Windows file"); const invWsl = await testInverseSearch(client, wslFiles.pdfPath, wslFiles.srcPath, fwdWsl.result, "WSL file"); + + // forward search on forward-slash Windows path + const fwdWin_fwdslash = await testForwardSearch( + client, winFiles.pdfPath, winFiles.srcPath.replace(/\\/g, "/"), "Windows file"); + return [ { name: "forward search(win files)", ok: fwdWin.ok }, { name: "forward search(wsl files)", ok: fwdWsl.ok }, { name: "inverse search(win files)", ok: invWin.ok }, { name: "inverse search(wsl files)", ok: invWsl.ok }, + { name: "forward search(forward-slash win path)", ok: fwdWin_fwdslash.ok }, ]; }); From 0093ac4a1d50531afa5d9ea7b959526219eb452b Mon Sep 17 00:00:00 2001 From: Henok Date: Mon, 22 Jun 2026 03:07:53 +0300 Subject: [PATCH 09/10] Fix: recognize forward-slash windows path as a valid windows path in `WindowsPathToWslMountPathTemp` --- src/PdfSync.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PdfSync.cpp b/src/PdfSync.cpp index 156d3bebb39c..2d8ae73cdfdf 100644 --- a/src/PdfSync.cpp +++ b/src/PdfSync.cpp @@ -884,8 +884,8 @@ static TempStr WindowsPathToWslMountPathTemp(const char* path) { return nullptr; } - // Require an absolute Windows path (e.g. "C:\") - if (!(std::isalpha((unsigned char)path[0]) && path[1] == ':' && path[2] == '\\')) { + // Require an absolute Windows path (e.g. "C:\", or ""C:/"") + if (!(std::isalpha((unsigned char)path[0]) && path[1] == ':' && path::IsSep(path[2]))) { return nullptr; } From 41e2a7dbd8dcf12c490ea9e68de4d9d880f8b1aa Mon Sep 17 00:00:00 2001 From: Henok Date: Mon, 22 Jun 2026 03:25:22 +0300 Subject: [PATCH 10/10] Narrow WSL mount path detection to require a real drive letter in `IsUnixSourcePath` The segment after /mnt/ is now required to be exactly one letter followed by '/' or end-of-string. --- src/PdfSync.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PdfSync.cpp b/src/PdfSync.cpp index 2d8ae73cdfdf..c8bdf5320a1a 100644 --- a/src/PdfSync.cpp +++ b/src/PdfSync.cpp @@ -775,7 +775,8 @@ static bool IsUnixSourcePath(const char* syncFilePath, const char* resolvedSrcPa return true; } - if (str::StartsWithI(resolvedSrcPath, "/mnt/")) { + if (str::StartsWithI(resolvedSrcPath, "/mnt/") && std::isalpha((unsigned char)resolvedSrcPath[5]) && + (resolvedSrcPath[6] == '/' || resolvedSrcPath[6] == '\0')) { return true; }