Skip to content
Open
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
1 change: 1 addition & 0 deletions cmd/control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export enum ControlCommand {
TestChm = 14,
TestSelectionTranslate = 15,
TestTripleClickLineSelect = 16,
TestInverseSearch = 22,
}

export type ControlArg = number | string | Uint8Array | ControlArg[];
Expand Down
3 changes: 1 addition & 2 deletions src/AppTools.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
142 changes: 131 additions & 11 deletions src/PdfSync.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
}

Expand All @@ -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"));
Expand All @@ -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);
}
Expand Down Expand Up @@ -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;
}

Expand All @@ -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;
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -744,6 +762,59 @@ int SyncTex::RebuildIndexIfNeeded() {
return MarkIndexWasRebuilt();
}

// 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;
}

if (str::StartsWithI(syncFilePath, "\\\\wsl.localhost\\") || str::StartsWithI(syncFilePath, "\\\\wsl$\\")) {
return true;
}

if (str::StartsWithI(resolvedSrcPath, "/mnt/") && std::isalpha((unsigned char)resolvedSrcPath[5]) &&
(resolvedSrcPath[6] == '/' || resolvedSrcPath[6] == '\0')) {
return true;
}

return false;
}

// Converts a WSL UNC path to its equivalent Unix path by stripping the
// \\wsl.localhost\<distro>\ or \\wsl$\<distro>\ 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();
Expand Down Expand Up @@ -774,11 +845,28 @@ 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(), filename)) {
// 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);
}

*line = synctex_node_line(node);
Expand All @@ -790,6 +878,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:\", or ""C:/"")
if (!(std::isalpha((unsigned char)path[0]) && path[1] == ':' && path::IsSep(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<Rect>& rects) {
logfa("SyncTex::SourceToDoc: '%s', line: %d, col: %d\n", srcfilename, line, col);
int res = RebuildIndexIfNeeded();
Expand All @@ -809,9 +915,23 @@ 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);

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;
}
Expand Down
3 changes: 2 additions & 1 deletion src/PdfSync.h
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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);
Expand Down
10 changes: 0 additions & 10 deletions src/SearchAndDDE.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<TextEditor*> editors;
Expand Down
12 changes: 12 additions & 0 deletions src/SumatraControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ enum class ControlCmd : u16 {
TestChm = 14,
TestSelectionTranslate = 15,
TestTripleClickLineSelect = 16,
TestInverseSearch = 22,
};

enum class ControlArgType : u16 {
Expand Down Expand Up @@ -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);
Expand Down
40 changes: 39 additions & 1 deletion src/SumatraTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,45 @@ char* TestSynctexResult(const char* pdfPath, const char* srcPath, int line) {
int page = 0;
Vec<Rect> 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);
Expand Down
1 change: 1 addition & 0 deletions src/SumatraTest.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions tests/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>][] = [
["cmd-start-autoscroll", cmdStartAutoScroll],
Expand All @@ -40,6 +41,7 @@ const tests: [string, () => void | Promise<void>][] = [
["issue-5665", issue5665],
["issue-5677", issue5677],
["issue-5681", issue5681],
["issue-5702", issue5702],
];

export type AllTestOptions = {
Expand Down
2 changes: 2 additions & 0 deletions tests/issue-5702-data/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# scratch dir created at runtime by tests/issue-5702.ts for tectonic output and test results
.work/
11 changes: 11 additions & 0 deletions tests/issue-5702-data/test.tex
Original file line number Diff line number Diff line change
@@ -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}
Loading