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

Fix forward SyncTex accuracy in internal browser by providing a range location indication alternative #4194

Merged
merged 8 commits into from
Apr 15, 2024
Merged
17 changes: 16 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1672,7 +1672,22 @@
"scope": "window",
"type": "boolean",
"default": true,
"markdownDescription": "Define the visibility of SyncTeX indicator (a red highlighting circle) after a forward SyncTeX in the PDF viewer."
"markdownDescription": "Define the visibility of SyncTeX indicator after a forward SyncTeX in the PDF viewer."
},
"latex-workshop.synctex.indicator.type": {
"scope": "window",
"type": "string",
"when": "!latex-workshop.synctex.synctexjs.enabled",
"enum": [
"Range",
"Spot"
],
"enumDescriptions": [
"Indicates a possible range with a rectangular SyncTeX indicator.",
"Indicates a single specific spot with a red circle SyncTeX indicator"
],
"default": "Spot",
"markdownDescription": "Define the behavior of SyncTex indicator after a forward SyncTeX in the PDF viewer (tab or browser). Valid when use SyncTex binary (latex-workshop.synctex.synctexjs.enabled = false)."
},
"latex-workshop.synctex.afterBuild.enabled": {
"scope": "window",
Expand Down
143 changes: 137 additions & 6 deletions src/locate/synctex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as fs from 'fs'
import * as path from 'path'
import * as cs from 'cross-spawn'
import { lw } from '../lw'
import type { SyncTeXRecordToPDF, SyncTeXRecordToTeX } from '../types'
import type { SyncTeXRecordToPDF, SyncTeXRecordToPDFAll, SyncTeXRecordToPDFAllList, SyncTeXRecordToTeX } from '../types'
import { syncTeXToPDF, syncTeXToTeX } from './synctex/worker'
import { replaceArgumentPlaceholders } from '../utils/utils'
import { isSameRealPath } from '../utils/pathnormalize'
Expand Down Expand Up @@ -61,6 +61,68 @@ function parseToPDF(result: string): SyncTeXRecordToPDF {
}
}

/**
* Parse the result of SyncTeX forward to PDF with a list.
*
* This function takes the result of SyncTeX forward to PDF as a string and
* parses it to extract page number, x-coordinate, y-coordinate, box-based
* coordinates (h, v, H, W), and whether the red indicator should be shown
* in the viewer.
*
* @param result - The result string of SyncTeX forward to PDF.
* @returns A SyncTeXRecordToPDFAllList object containing a list of records,
* with each record containing page number, x-coordinate, y-coordinate,
* h-coordinate, v-coordinate, H-coordinate, W-coordinate, and an indicator.
* @throws Error if there is a parsing error.
*/
function parseToPDFList(result: string): SyncTeXRecordToPDFAllList {
const records: SyncTeXRecordToPDFAll[] = [];
let started = false;
let recordIndex = -1;

for (const line of result.split('\n')) {
if (line.includes('SyncTeX result begin')) {
started = true;
continue;
}

if (line.includes('SyncTeX result end')) {
break;
}

if (!started) {
continue;
}

const pos = line.indexOf(':');
if (pos < 0) {
continue;
}

const key = line.substring(0, pos);
const value = line.substring(pos + 1).trim();

if (key == 'Output') {
recordIndex += 1;
const record : SyncTeXRecordToPDFAll = { Page: 0, x: 0, y: 0, h: 0, v: 0, W: 0, H: 0 };
records[recordIndex] = record;
}

if (key === 'Page' || key === 'h' || key === 'v' || key === 'W' || key === 'H' || key === 'x' || key === 'y') {
const record = records[recordIndex];
if (record) {
record[key] = Number(value);
}
}
}

if (recordIndex != -1) {
return { records };
} else {
throw(new Error('parse error when parsing the result of synctex forward.'))
}
}

/**
* Parse the result of SyncTeX backward to TeX.
*
Expand Down Expand Up @@ -181,11 +243,21 @@ function toPDF(args?: {line: number, filePath: string}, forcedViewer: 'auto' | '
logger.logError('Forward SyncTeX failed.', e)
}
} else {
void callSyncTeXToPDF(line, character, filePath, pdfFile).then( (record) => {
if (pdfFile) {
void lw.viewer.locate(pdfFile, record)
}
})
const indicatorType = configuration.get('synctex.indicator.type')

if (indicatorType === 'Range') {
void callSyncTeXToPDFRange(line, filePath, pdfFile).then( (recordList) => {
if (pdfFile) {
void lw.viewer.locateRange(pdfFile, recordList)
}
})
} else {
void callSyncTeXToPDF(line, character, filePath, pdfFile).then( (record) => {
if (pdfFile) {
void lw.viewer.locate(pdfFile, record)
}
})
}
}
}

Expand Down Expand Up @@ -247,6 +319,65 @@ function callSyncTeXToPDF(line: number, col: number, filePath: string, pdfFile:
})
}

/**
* Call SyncTeX to PDF for a specific line and character position.
*
* This function calls the SyncTeX binary to retrieve PDF information for a
* given line as a range in a TeX file. It returns a promise
* resolving to a SyncTeXRecordToPDFAllList object.
*
* @param line - The line number in the TeX file.
* @param filePath - The path of the TeX file.
* @param pdfFile - The path of the PDF file.
* @returns A promise resolving to a SyncTeXRecordToPDFAllList object.
*/
function callSyncTeXToPDFRange(line: number, filePath: string, pdfFile: string): Thenable<SyncTeXRecordToPDFAllList> {
const configuration = vscode.workspace.getConfiguration('latex-workshop')
const docker = configuration.get('docker.enabled')
// Pass column parameter with 0 to make SyncTex Cmd ignore specific character, returning whole range of location with respect to the line.
const args = ['view', '-i', `${line}:0:${docker ? path.basename(filePath): filePath}`, '-o', docker ? path.basename(pdfFile): pdfFile]

let command = configuration.get('synctex.path') as string
if (docker) {
if (process.platform === 'win32') {
command = path.resolve(lw.extensionRoot, './scripts/synctex.bat')
} else {
command = path.resolve(lw.extensionRoot, './scripts/synctex')
fs.chmodSync(command, 0o755)
}
}
const logTag = docker ? 'Docker' : 'Legacy'
logger.log(`Forward from ${filePath} to ${pdfFile} on line ${line}.`)
const proc = cs.spawn(command, args, {cwd: path.dirname(pdfFile)})
proc.stdout.setEncoding('utf8')
proc.stderr.setEncoding('utf8')

let stdout = ''
proc.stdout.on('data', newStdout => {
stdout += newStdout
})

let stderr = ''
proc.stderr.on('data', newStderr => {
stderr += newStderr
})

proc.on('error', err => {
logger.logError(`(${logTag}) Forward SyncTeX failed.`, err, stderr)
})

return new Promise( (resolve) => {
proc.on('exit', exitCode => {
if (exitCode !== 0) {
logger.logError(`(${logTag}) Forward SyncTeX failed.`, exitCode, stderr)
} else {
resolve(parseToPDFList(stdout))
}
})
})

}

/**
* Execute forward SyncTeX based on the provided arguments and viewer
* preference.
Expand Down
35 changes: 34 additions & 1 deletion src/preview/viewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as path from 'path'
import * as os from 'os'
import * as cs from 'cross-spawn'
import { lw } from '../lw'
import type { SyncTeXRecordToPDF, ViewerMode } from '../types'
import type { SyncTeXRecordToPDF, SyncTeXRecordToPDFAllList, ViewerMode } from '../types'
import * as manager from './viewer/pdfviewermanager'
import { populate } from './viewer/pdfviewerpanel'

Expand All @@ -21,6 +21,7 @@ export {
handler,
isViewing,
locate,
locateRange,
viewInWebviewPanel,
refresh,
view
Expand Down Expand Up @@ -414,6 +415,38 @@ async function locate(pdfFile: string, record: SyncTeXRecordToPDF): Promise<void
}
}

/**
* Reveals the position range of `records` on the internal PDF viewers with a Rectangle indicator.
*
* @param pdfFile The path of a PDF file.
* @param recordList The list of positions to be revealed.
*/
async function locateRange(pdfFile: string, recordList: SyncTeXRecordToPDFAllList): Promise<void> {
const pdfUri = vscode.Uri.file(pdfFile);
let clientSet = manager.getClients(pdfUri);
if (clientSet === undefined || clientSet.size === 0) {
logger.log(`PDF is not opened: ${pdfFile}, try opening.`);
await view(pdfFile);
clientSet = manager.getClients(pdfUri);
}
if (clientSet === undefined || clientSet.size === 0) {
logger.log(`PDF cannot be opened: ${pdfFile}.`);
return;
}
const needDelay = showInvisibleWebviewPanel(pdfUri);
for (const client of clientSet) {
setTimeout(() => {
const indicator = vscode.workspace
.getConfiguration('latex-workshop')
.get('synctex.indicator.enabled') as boolean;

client.send({ type: 'synctexRange', data: { records: recordList.records, indicator } });

}, needDelay ? 200 : 0);
logger.log(`Try to synctex ${pdfFile}`);
}
}

/**
* Reveals the internal PDF viewer of `pdfUri`.
* The first one is revealed.
Expand Down
14 changes: 14 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,20 @@ export type SyncTeXRecordToTeX = {
column: number
}

export type SyncTeXRecordToPDFAll = {
Page: number;
x: number;
y: number;
h: number;
v: number;
W: number;
H: number;
}

export type SyncTeXRecordToPDFAllList = {
records: SyncTeXRecordToPDFAll[];
}

export interface LaTeXLinter {
readonly linterDiagnostics: vscode.DiagnosticCollection,
getName(): string,
Expand Down
17 changes: 17 additions & 0 deletions types/latex-workshop-protocol-types/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@

type SyncTeXRecordToPDFAll = {
Page: number;
x: number;
y: number;
h: number;
v: number;
W: number;
H: number;
}

export type ServerResponse = {
type: 'refresh'
} | {
Expand All @@ -10,6 +21,12 @@ export type ServerResponse = {
}
} | {
type: 'reload'
} | {
type: 'synctexRange',
data: {
records: SyncTeXRecordToPDFAll[],
indicator: boolean
}
}

export type PdfViewerParams = {
Expand Down
17 changes: 17 additions & 0 deletions viewer/latexworkshop.css
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,23 @@ html[dir='rtl'] .findbar {
transform: translate(-50%, -50%);
}

@keyframes synctex-indicator-fadeOut {
0% {
background-color: rgba(0, 0, 255, 0.4);
}
100% {
background-color: rgba(0, 0, 255, 0);
}
}

.synctex-indicator-rect {
position: absolute;
z-index: 100000;
background-color: rgba(0, 0, 255, 0.5);
pointer-events: none;
animation: synctex-indicator-fadeOut 2s forwards;
}

#synctex-indicator.show {
transition: none;
opacity: 0.8;
Expand Down
Loading