diff --git a/package-lock.json b/package-lock.json index 9f3223186..ce5bb7776 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "micromatch": "4.0.5", "pdfjs-dist": "4.3.136", "tmp": "0.2.3", + "vsls": "1.0.4753", "workerpool": "9.1.1", "ws": "8.17.1" }, @@ -574,6 +575,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@microsoft/servicehub-framework": { + "version": "2.6.74", + "resolved": "https://registry.npmjs.org/@microsoft/servicehub-framework/-/servicehub-framework-2.6.74.tgz", + "integrity": "sha512-QJ//zzvxffupIkzupnVbMYY5YDOP+g5FlG6x0Pl7svRyq8pAouiibckJJcZlMtsMypKWwAnVBKb9/sonEOsUxw==", + "dependencies": { + "await-semaphore": "^0.1.3", + "msgpack-lite": "^0.1.26", + "nerdbank-streams": "2.5.60", + "strict-event-emitter-types": "^2.0.0", + "vscode-jsonrpc": "^4.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1678,6 +1691,11 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, + "node_modules/await-semaphore": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/await-semaphore/-/await-semaphore-0.1.3.tgz", + "integrity": "sha512-d1W2aNSYcz/sxYO4pMGX9vq65qOTu0P800epMud+6cYYX0QcT7zyqcxec3VWzpgvdXo57UWmVbZpLMjX2m1I7Q==" + }, "node_modules/azure-devops-node-api": { "version": "12.5.0", "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", @@ -1899,6 +1917,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cancellationtoken": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cancellationtoken/-/cancellationtoken-2.2.0.tgz", + "integrity": "sha512-uF4sHE5uh2VdEZtIRJKGoXAD9jm7bFY0tDRCzH4iLp262TOJ2lrtNHjMG2zc8H+GICOpELIpM7CGW5JeWnb3Hg==" + }, "node_modules/canvas": { "version": "2.11.2", "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", @@ -1920,6 +1943,11 @@ "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", "dev": true }, + "node_modules/caught": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/caught/-/caught-0.1.3.tgz", + "integrity": "sha512-DTWI84qfoqHEV5jHRpsKNnEisVCeuBDscXXaXyRLXC+4RD6rFftUNuTElcQ7LeO7w622pfzWkA1f6xu5qEAidw==" + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -2800,6 +2828,11 @@ "node": ">=0.10.0" } }, + "node_modules/event-lite": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/event-lite/-/event-lite-0.1.3.tgz", + "integrity": "sha512-8qz9nOz5VeD2z96elrEKD2U433+L3DWdUdDkOINLGOJvx1GsMBbMn0aCeu28y8/e85A6mCigBiFlYMnTBEGlSw==" + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -3457,7 +3490,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -3471,8 +3503,7 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "optional": true + ] }, "node_modules/ignore": { "version": "5.3.1", @@ -3537,6 +3568,11 @@ "dev": true, "optional": true }, + "node_modules/int64-buffer": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/int64-buffer/-/int64-buffer-0.1.10.tgz", + "integrity": "sha512-v7cSY1J8ydZ0GyjUHqF+1bshJ6cnEVLo9EnjB8p+4HDRPZc9N5jjmvUV7NvEsqQOKyH0pmIBFWXVQbiS0+OBbA==" + }, "node_modules/is-arrayish": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", @@ -3678,8 +3714,7 @@ "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, "node_modules/isexe": { "version": "2.0.0", @@ -4577,6 +4612,20 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "devOptional": true }, + "node_modules/msgpack-lite": { + "version": "0.1.26", + "resolved": "https://registry.npmjs.org/msgpack-lite/-/msgpack-lite-0.1.26.tgz", + "integrity": "sha512-SZ2IxeqZ1oRFGo0xFGbvBJWMp3yLIY9rlIJyxy8CGrwZn1f0ZK4r6jV/AM1r0FZMDUkWkglOk/eeKIL9g77Nxw==", + "dependencies": { + "event-lite": "^0.1.1", + "ieee754": "^1.1.8", + "int64-buffer": "^0.1.9", + "isarray": "^1.0.0" + }, + "bin": { + "msgpack": "bin/msgpack" + } + }, "node_modules/mute-stream": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", @@ -4602,6 +4651,17 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/nerdbank-streams": { + "version": "2.5.60", + "resolved": "https://registry.npmjs.org/nerdbank-streams/-/nerdbank-streams-2.5.60.tgz", + "integrity": "sha512-saQaMyTtVDAEc+S+BPXKM6K1AF3FyrorFSDzaCkdmtDe2kZzu1aYPQZNLmnxJhxbTcghYrEmYFFoaDxBDVadCw==", + "dependencies": { + "await-semaphore": "^0.1.3", + "cancellationtoken": "^2.0.1", + "caught": "^0.1.3", + "msgpack-lite": "^0.1.26" + } + }, "node_modules/nise": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/nise/-/nise-6.0.0.tgz", @@ -5526,6 +5586,11 @@ "npm": ">=6" } }, + "node_modules/strict-event-emitter-types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz", + "integrity": "sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==" + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -6092,6 +6157,22 @@ "node": ">=16" } }, + "node_modules/vscode-jsonrpc": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-4.0.0.tgz", + "integrity": "sha512-perEnXQdQOJMTDFNv+UF3h1Y0z4iSiaN9jIlb0OqIYgosPCZGYh/MCUlkFtV2668PL69lRDO32hmvL2yiidUYg==", + "engines": { + "node": ">=8.0.0 || >=10.0.0" + } + }, + "node_modules/vsls": { + "version": "1.0.4753", + "resolved": "https://registry.npmjs.org/vsls/-/vsls-1.0.4753.tgz", + "integrity": "sha512-hmrsMbhjuLoU8GgtVfqhbV4ZkGvDpLV2AFmzx+cCOGNra2qk0Q36dYkfwENqy/vJVQ/2/lhxcn+69FYnKQRhgg==", + "dependencies": { + "@microsoft/servicehub-framework": "^2.6.74" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/package.json b/package.json index de49dbf47..49e3b9636 100644 --- a/package.json +++ b/package.json @@ -285,6 +285,11 @@ } ], "commands": [ + { + "command": "latex-workshop.hostPort", + "title": "%command.hostPort%", + "category": "LaTeX Workshop" + }, { "command": "latex-workshop.navigate-envpair", "title": "%command.navigate-envpair%", @@ -357,8 +362,7 @@ "command": "latex-workshop.view", "title": "%command.view%", "category": "LaTeX Workshop", - "icon": "$(open-preview)", - "enablement": "!virtualWorkspace" + "icon": "$(open-preview)" }, { "command": "latex-workshop.tab", @@ -368,8 +372,7 @@ { "command": "latex-workshop.viewInBrowser", "title": "%command.viewInBrowser%", - "category": "LaTeX Workshop", - "enablement": "!virtualWorkspace" + "category": "LaTeX Workshop" }, { "command": "latex-workshop.viewExternal", @@ -391,8 +394,7 @@ { "command": "latex-workshop.synctex", "title": "%command.synctex%", - "category": "LaTeX Workshop", - "enablement": "!virtualWorkspace" + "category": "LaTeX Workshop" }, { "command": "latex-workshop.clean", @@ -403,8 +405,7 @@ { "command": "latex-workshop.citation", "title": "%command.citation%", - "category": "LaTeX Workshop", - "enablement": "!virtualWorkspace" + "category": "LaTeX Workshop" }, { "command": "latex-workshop.addtexroot", @@ -541,13 +542,13 @@ "key": "ctrl+l alt+v", "mac": "cmd+l alt+v", "command": "latex-workshop.view", - "when": "editorLangId =~ /^latex$|^latex-expl3$|^doctex$|^rsweave$|^jlweave$|^pweave$/ && config.latex-workshop.bind.altKeymap.enabled && !virtualWorkspace" + "when": "editorLangId =~ /^latex$|^latex-expl3$|^doctex$|^rsweave$|^jlweave$|^pweave$/ && config.latex-workshop.bind.altKeymap.enabled" }, { "key": "ctrl+l alt+j", "mac": "cmd+l alt+j", "command": "latex-workshop.synctex", - "when": "editorTextFocus && editorLangId =~ /^latex$|^latex-expl3$|^doctex$/ && config.latex-workshop.bind.altKeymap.enabled && !virtualWorkspace" + "when": "editorTextFocus && editorLangId =~ /^latex$|^latex-expl3$|^doctex$/ && config.latex-workshop.bind.altKeymap.enabled" }, { "key": "ctrl+l alt+x", @@ -577,13 +578,13 @@ "key": "ctrl+alt+v", "mac": "cmd+alt+v", "command": "latex-workshop.view", - "when": "editorLangId =~ /^latex$|^latex-expl3$|^doctex$|^rsweave$|^jlweave$|^pweave$/ && !config.latex-workshop.bind.altKeymap.enabled && !virtualWorkspace" + "when": "editorLangId =~ /^latex$|^latex-expl3$|^doctex$|^rsweave$|^jlweave$|^pweave$/ && !config.latex-workshop.bind.altKeymap.enabled" }, { "key": "ctrl+alt+j", "mac": "cmd+alt+j", "command": "latex-workshop.synctex", - "when": "editorTextFocus && editorLangId =~ /^latex$|^latex-expl3$|^doctex$/ && !config.latex-workshop.bind.altKeymap.enabled && !virtualWorkspace" + "when": "editorTextFocus && editorLangId =~ /^latex$|^latex-expl3$|^doctex$/ && !config.latex-workshop.bind.altKeymap.enabled" }, { "key": "ctrl+alt+x", @@ -2493,7 +2494,7 @@ ], "editor/title": [ { - "when": "editorLangId =~ /^latex$|^latex-expl3$|^doctex$|^rsweave$|^jlweave$|^pweave$/ && !virtualWorkspace", + "when": "editorLangId =~ /^latex$|^latex-expl3$|^doctex$|^rsweave$|^jlweave$|^pweave$/", "command": "latex-workshop.view", "group": "navigation@2" }, @@ -2570,6 +2571,7 @@ "micromatch": "4.0.5", "pdfjs-dist": "4.3.136", "tmp": "0.2.3", + "vsls": "1.0.4753", "workerpool": "9.1.1", "ws": "8.17.1" }, diff --git a/package.nls.bg.json b/package.nls.bg.json index 2827eccfa..75b24706f 100644 --- a/package.nls.bg.json +++ b/package.nls.bg.json @@ -1,4 +1,5 @@ { + "command.hostPort": "Споделяне (на хоста) / Придобиване (на госта) на порт за споделяне на живо на хоста", "command.navigate-envpair": "Навигирай до съвпадение на \\begin{}/\\end{}", "command.select-envname": "Избери името на текущото среда", "command.select-envcontent": "Избери съдържанието на текущото среда", diff --git a/package.nls.cs.json b/package.nls.cs.json index 0d67dad0f..aed6ad2d7 100644 --- a/package.nls.cs.json +++ b/package.nls.cs.json @@ -1,4 +1,5 @@ { + "command.hostPort": "Sdílet (na hostiteli) / Získat (na hostu) port pro Live Share hostitele", "command.navigate-envpair": "Přejít k odpovídajícímu \\begin{}/\\end{}", "command.select-envname": "Vybrat název aktuálního prostředí", "command.select-envcontent": "Vybrat obsah aktuálního prostředí", diff --git a/package.nls.de.json b/package.nls.de.json index 57fff9517..a64d5dfe7 100644 --- a/package.nls.de.json +++ b/package.nls.de.json @@ -1,4 +1,5 @@ { + "command.hostPort": "Teilen (auf Host) / Erwerben (auf Gast) Live Share Host-Port", "command.navigate-envpair": "Zu passendem \\begin{}/\\end{} navigieren", "command.select-envname": "Aktuellen Umgebungsnamen auswählen", "command.select-envcontent": "Aktuellen Umgebungsinhalt auswählen", diff --git a/package.nls.es.json b/package.nls.es.json index 5dc2162cc..c6c7e0511 100644 --- a/package.nls.es.json +++ b/package.nls.es.json @@ -1,4 +1,5 @@ { + "command.hostPort": "Compartir (en anfitrión) / Adquirir (en invitado) puerto de anfitrión de Live Share", "command.navigate-envpair": "Navegar al \\begin{}/\\end{} correspondiente", "command.select-envname": "Seleccionar el nombre del entorno actual", "command.select-envcontent": "Seleccionar el contenido del entorno actual", diff --git a/package.nls.fr.json b/package.nls.fr.json index 06a85ac3b..c4089b47f 100644 --- a/package.nls.fr.json +++ b/package.nls.fr.json @@ -1,4 +1,5 @@ { + "command.hostPort": "Partager (sur l'hôte) / Acquérir (sur l'invité) le port de l'hôte Live Share", "command.navigate-envpair": "Naviguer vers \\begin{}/\\end{} correspondant", "command.select-envname": "Sélectionner le nom de l'environnement actuel", "command.select-envcontent": "Sélectionner le contenu de l'environnement actuel", diff --git a/package.nls.hu.json b/package.nls.hu.json index 4d9166687..a145b7363 100644 --- a/package.nls.hu.json +++ b/package.nls.hu.json @@ -1,4 +1,5 @@ { + "command.hostPort": "Megosztás (a gazdagépen) / Megszerzés (a vendégen) Live Share gazdagép port", "command.navigate-envpair": "\\begin{}/\\end{} párok közötti navigáció", "command.select-envname": "Jelenlegi környezet nevének kijelölése", "command.select-envcontent": "Jelenlegi környezet tartalmának kijelölése", diff --git a/package.nls.it.json b/package.nls.it.json index e7c518528..51f256713 100644 --- a/package.nls.it.json +++ b/package.nls.it.json @@ -1,4 +1,5 @@ { + "command.hostPort": "Condividi (su host) / Acquisisci (su guest) porta dell'host Live Share", "command.navigate-envpair": "Vai alla coppia corrispondente \\begin{}/\\end{}", "command.select-envname": "Seleziona il nome dell'ambiente corrente", "command.select-envcontent": "Seleziona il contenuto dell'ambiente corrente", diff --git a/package.nls.ja.json b/package.nls.ja.json index 7d0329483..5bf6c1e80 100644 --- a/package.nls.ja.json +++ b/package.nls.ja.json @@ -1,4 +1,5 @@ { + "command.hostPort": "共有 (ホスト側) / 取得 (ゲスト側) Live Share ホストポート", "command.navigate-envpair": "\\begin{}/\\end{}に移動", "command.select-envname": "現在の環境名を選択", "command.select-envcontent": "現在の環境内容を選択", diff --git a/package.nls.json b/package.nls.json index 39e3f9cd6..027a32b96 100644 --- a/package.nls.json +++ b/package.nls.json @@ -1,4 +1,5 @@ { + "command.hostPort": "Share (on host) / Acquire (on guest) Live Share host port", "command.navigate-envpair": "Navigate to matching \\begin{}/\\end{}", "command.select-envname": "Select the current environment name", "command.select-envcontent": "Select the current environment content", diff --git a/package.nls.ko.json b/package.nls.ko.json index cf5229fd3..49c161f8d 100644 --- a/package.nls.ko.json +++ b/package.nls.ko.json @@ -1,4 +1,5 @@ { + "command.hostPort": "공유 (호스트) / 획득 (게스트) Live Share 호스트 포트", "command.navigate-envpair": "\\begin{}/\\end{}와 일치하는 곳으로 이동", "command.select-envname": "현재 환경 이름 선택", "command.select-envcontent": "현재 환경 내용 선택", diff --git a/package.nls.pl.json b/package.nls.pl.json index 16324bba7..0fb849525 100644 --- a/package.nls.pl.json +++ b/package.nls.pl.json @@ -1,4 +1,5 @@ { + "command.hostPort": "Udostępnij (na hoście) / Uzyskaj (na gościu) port hosta Live Share", "command.navigate-envpair": "Przejdź do pasującego \\begin{}/\\end{}", "command.select-envname": "Wybierz nazwę bieżącego środowiska", "command.select-envcontent": "Wybierz zawartość bieżącego środowiska", diff --git a/package.nls.pt-br.json b/package.nls.pt-br.json index c812c3363..71488cbee 100644 --- a/package.nls.pt-br.json +++ b/package.nls.pt-br.json @@ -1,4 +1,5 @@ { + "command.hostPort": "Compartilhar (no host) / Adquirir (no guest) porta do host do Live Share", "command.navigate-envpair": "Navegar para correspondência \\begin{}/\\end{}", "command.select-envname": "Selecionar o nome do ambiente atual", "command.select-envcontent": "Selecionar o conteúdo do ambiente atual", diff --git a/package.nls.ru.json b/package.nls.ru.json index cd9721419..227674a36 100644 --- a/package.nls.ru.json +++ b/package.nls.ru.json @@ -1,4 +1,5 @@ { + "command.hostPort": "Поделиться (на хосте) / Получить (на госте) порт хоста Live Share", "command.navigate-envpair": "Перейти к соответствующему \\begin{}/\\end{}", "command.select-envname": "Выбрать имя текущего окружения", "command.select-envcontent": "Выбрать содержимое текущего окружения", diff --git a/package.nls.tr.json b/package.nls.tr.json index 47325a0bb..33db0b7fa 100644 --- a/package.nls.tr.json +++ b/package.nls.tr.json @@ -1,4 +1,5 @@ { + "command.hostPort": "Paylaş (ev sahibinde) / Al (misafirde) Live Share ev sahibi portu", "command.navigate-envpair": "\\begin{}/\\end{} eşleşmesine git", "command.select-envname": "Geçerli ortam adını seç", "command.select-envcontent": "Geçerli ortam içeriğini seç", diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json index f38ce7958..54a1c2ad2 100644 --- a/package.nls.zh-cn.json +++ b/package.nls.zh-cn.json @@ -1,4 +1,5 @@ { + "command.hostPort": "共享(在主机上)/ 获取(在客户端)Live Share 主机端口", "command.navigate-envpair": "导航到匹配的 \\begin{}/\\end{}", "command.select-envname": "选择当前环境名称", "command.select-envcontent": "选择当前环境内容", diff --git a/package.nls.zh-tw.json b/package.nls.zh-tw.json index c2a9ab49d..d7ef7e6d1 100644 --- a/package.nls.zh-tw.json +++ b/package.nls.zh-tw.json @@ -1,4 +1,5 @@ { + "command.hostPort": "共享(在主機上)/ 獲取(在客戶端)Live Share 主機端口", "command.navigate-envpair": "導航至匹配的 \\begin{}/\\end{}", "command.select-envname": "選擇當前環境名稱", "command.select-envcontent": "選擇當前環境內容", diff --git a/src/compile/build.ts b/src/compile/build.ts index 76fe35629..04f6cf67c 100644 --- a/src/compile/build.ts +++ b/src/compile/build.ts @@ -30,7 +30,7 @@ lw.watcher.bib.onChange(filePath => autoBuild(filePath, 'onFileChange', true)) * changed. */ function autoBuild(file: string, type: 'onFileChange' | 'onSave', bibChanged: boolean = false) { - const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(file)) + const configuration = vscode.workspace.getConfiguration('latex-workshop', lw.file.toUri(file)) if (configuration.get('latex.autoBuild.run') as string !== type) { return } @@ -57,7 +57,7 @@ function autoBuild(file: string, type: 'onFileChange' | 'onSave', bibChanged: bo * @returns {boolean} - True if auto-build can be triggered, false otherwise. */ function canAutoBuild(): boolean { - const configuration = vscode.workspace.getConfiguration('latex-workshop', lw.root.file.path ? vscode.Uri.file(lw.root.file.path) : undefined) + const configuration = vscode.workspace.getConfiguration('latex-workshop', lw.root.file.path ? lw.file.toUri(lw.root.file.path) : undefined) return Date.now() - lw.compile.lastAutoBuildTime >= (configuration.get('latex.autoBuild.interval', 1000) as number) } @@ -91,7 +91,7 @@ async function build(skipSelection: boolean = false, rootFile: string | undefine logger.log(`The document of the active editor: ${activeEditor.document.uri.toString(true)}`) logger.log(`The languageId of the document: ${activeEditor.document.languageId}`) - const workspace = rootFile ? vscode.Uri.file(rootFile) : activeEditor.document.uri + const workspace = rootFile ? lw.file.toUri(rootFile) : activeEditor.document.uri const configuration = vscode.workspace.getConfiguration('latex-workshop', workspace) const externalBuildCommand = configuration.get('latex.external.build.command') as string const externalBuildArgs = configuration.get('latex.external.build.args') as string[] @@ -187,7 +187,7 @@ async function buildLoop() { * process. */ function spawnProcess(step: Step): ProcessEnv { - const configuration = vscode.workspace.getConfiguration('latex-workshop', step.rootFile ? vscode.Uri.file(step.rootFile) : undefined) + const configuration = vscode.workspace.getConfiguration('latex-workshop', step.rootFile ? lw.file.toUri(step.rootFile) : undefined) if (step.index === 0 || configuration.get('latex.build.clearLog.everyRecipeStep.enabled') as boolean) { logger.clearCompilerMessage() } @@ -332,7 +332,7 @@ function handleExitCodeError(step: Step, env: ProcessEnv, stderr: string, code: logger.log(`${stderr}`) } - const configuration = vscode.workspace.getConfiguration('latex-workshop', step.rootFile ? vscode.Uri.file(step.rootFile) : undefined) + const configuration = vscode.workspace.getConfiguration('latex-workshop', step.rootFile ? lw.file.toUri(step.rootFile) : undefined) if (!step.isExternal && signal !== 'SIGTERM' && !step.isRetry && configuration.get('latex.autoBuild.cleanAndRetry.enabled')) { handleRetryError(step) } else if (!step.isExternal && signal !== 'SIGTERM') { @@ -423,7 +423,7 @@ async function afterSuccessfulBuilt(lastStep: Step, skipped: boolean) { lw.viewer.refresh(lw.file.getPdfPath(lastStep.rootFile)) lw.completion.reference.setNumbersFromAuxFile(lastStep.rootFile) await lw.cache.loadFlsFile(lastStep.rootFile ?? '') - const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(lastStep.rootFile)) + const configuration = vscode.workspace.getConfiguration('latex-workshop', lw.file.toUri(lastStep.rootFile)) // If the PDF viewer is internal, we call SyncTeX in src/components/viewer.ts. if (configuration.get('view.pdf.viewer') === 'external' && configuration.get('synctex.afterBuild.enabled')) { const pdfFile = lw.file.getPdfPath(lastStep.rootFile) diff --git a/src/compile/queue.ts b/src/compile/queue.ts index c028dd01f..9d0d2a71d 100644 --- a/src/compile/queue.ts +++ b/src/compile/queue.ts @@ -1,5 +1,6 @@ import vscode from 'vscode' import type { ExternalStep, RecipeStep, Step, StepQueue, Tool } from '../types' +import { lw } from '../lw' const stepQueue: StepQueue = { steps: [], nextSteps: [] } @@ -99,7 +100,7 @@ function getStepString(step: Step): string { // Determine the format of the stepString based on timestamp and index if(step.rootFile) { - const rootFileUri = vscode.Uri.file(step.rootFile) + const rootFileUri = lw.file.toUri(step.rootFile) const configuration = vscode.workspace.getConfiguration('latex-workshop', rootFileUri) const showFilename = configuration.get('latex.build.rootfileInStatus', false) if(showFilename) { diff --git a/src/compile/recipe.ts b/src/compile/recipe.ts index 1e7b50415..5863a8cd4 100644 --- a/src/compile/recipe.ts +++ b/src/compile/recipe.ts @@ -120,7 +120,7 @@ function createOutputSubFolders(rootFile: string) { function createBuildTools(rootFile: string, langId: string, recipeName?: string): Tool[] | undefined { let buildTools: Tool[] = [] - const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(rootFile)) + const configuration = vscode.workspace.getConfiguration('latex-workshop', lw.file.toUri(rootFile)) const magic = findMagicComments(rootFile) if (recipeName === undefined && magic.tex && !configuration.get('latex.build.forceRecipeUsage')) { @@ -233,7 +233,7 @@ function findMagicComments(rootFile: string): {tex?: Tool, bib?: Tool, recipe?: * @returns {Tool[]} - An array of Tool objects representing the build tools. */ function createBuildMagic(rootFile: string, magicTex: Tool, magicBib?: Tool): Tool[] { - const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(rootFile)) + const configuration = vscode.workspace.getConfiguration('latex-workshop', lw.file.toUri(rootFile)) if (!magicTex.args) { magicTex.args = configuration.get('latex.magic.args') as string[] @@ -261,7 +261,7 @@ function createBuildMagic(rootFile: string, magicTex: Tool, magicBib?: Tool): To * provided parameters. */ function findRecipe(rootFile: string, langId: string, recipeName?: string): Recipe | undefined { - const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(rootFile)) + const configuration = vscode.workspace.getConfiguration('latex-workshop', lw.file.toUri(rootFile)) const recipes = configuration.get('latex.recipes') as Recipe[] const defaultRecipeName = configuration.get('latex.recipe.default') as string @@ -317,7 +317,7 @@ function findRecipe(rootFile: string, langId: string, recipeName?: string): Reci * @returns {Tool[]} - An array of Tool objects with expanded values. */ function populateTools(rootFile: string, buildTools: Tool[]): Tool[] { - const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(rootFile)) + const configuration = vscode.workspace.getConfiguration('latex-workshop', lw.file.toUri(rootFile)) const docker = configuration.get('docker.enabled') buildTools.forEach(tool => { diff --git a/src/completion/completer/citation.ts b/src/completion/completer/citation.ts index cef247709..1a38e0161 100644 --- a/src/completion/completer/citation.ts +++ b/src/completion/completer/citation.ts @@ -238,7 +238,7 @@ function updateAll(bibFiles?: string[]): CitationItem[] { */ async function parseBibFile(fileName: string) { logger.log(`Parsing .bib entries from ${fileName}`) - const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(fileName)) + const configuration = vscode.workspace.getConfiguration('latex-workshop', lw.file.toUri(fileName)) if (fs.statSync(fileName).size >= (configuration.get('bibtex.maxFileSize') as number) * 1024 * 1024) { logger.log(`Bib file is too large, ignoring it: ${fileName}`) data.bibEntries.delete(fileName) diff --git a/src/completion/completer/input.ts b/src/completion/completer/input.ts index 5d2896074..8dae26088 100644 --- a/src/completion/completer/input.ts +++ b/src/completion/completer/input.ts @@ -148,7 +148,7 @@ class Input extends InputAbstract { if (['includegraphics', 'includesvg'].includes(macro) && this.graphicsPath.size > 0) { baseDir = Array.from(this.graphicsPath).map(dir => path.join(rootDir, dir)) } else { - const baseConfig = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(currentFile)).get('intellisense.file.base') + const baseConfig = vscode.workspace.getConfiguration('latex-workshop', lw.file.toUri(currentFile)).get('intellisense.file.base') const baseDirCurrentFile = path.dirname(currentFile) switch (baseConfig) { case 'root relative': diff --git a/src/completion/completer/macro.ts b/src/completion/completer/macro.ts index 0f63d5725..805ae93e4 100644 --- a/src/completion/completer/macro.ts +++ b/src/completion/completer/macro.ts @@ -289,7 +289,7 @@ function parseAst(node: Ast.Node, filePath: string, defined?: Set): CmdE data.definedCmds.set(cmd.signatureAsString(), { filePath, location: new vscode.Location( - vscode.Uri.file(filePath), + lw.file.toUri(filePath), new vscode.Position( (node.position?.start.line ?? 1) - 1, (node.position?.start.column ?? 1) - 1)) @@ -387,7 +387,7 @@ function parseContent(content: string, filePath: string): CmdEnvSuggestion[] { data.definedCmds.set(result[1], { filePath, location: new vscode.Location( - vscode.Uri.file(filePath), + lw.file.toUri(filePath), new vscode.Position(content.substring(0, result.index).split('\n').length - 1, 0)) }) } diff --git a/src/core/commands.ts b/src/core/commands.ts index a835e3d95..35f733164 100644 --- a/src/core/commands.ts +++ b/src/core/commands.ts @@ -5,6 +5,16 @@ import { getSurroundingMacroRange, stripText } from '../utils/utils' const logger = lw.log('Commander') +export async function hostPort() { + logger.log('HOSTPORT command invoked.') + if (lw.extra.liveshare.isGuest()) { + await lw.extra.liveshare.getHostServerPort(true) + } + else { + await lw.extra.liveshare.shareServer() + } +} + export async function build(skipSelection: boolean = false, rootFile: string | undefined = undefined, languageId: string | undefined = undefined, recipe: string | undefined = undefined) { logger.log('BUILD command invoked.') await lw.compile.build(skipSelection, rootFile, languageId, recipe) @@ -16,13 +26,13 @@ export async function revealOutputDir() { const workspaceFolder = vscode.workspace.workspaceFolders?.[0] const rootDir = lw.root.dir.path || workspaceFolder?.uri.fsPath if (rootDir === undefined) { - logger.log(`Cannot reveal ${vscode.Uri.file(outDir)}: no root dir can be identified.`) + logger.log(`Cannot reveal ${lw.file.toUri(outDir)}: no root dir can be identified.`) return } outDir = path.resolve(rootDir, outDir) } - logger.log(`Reveal ${vscode.Uri.file(outDir)}`) - await vscode.commands.executeCommand('revealFileInOS', vscode.Uri.file(outDir)) + logger.log(`Reveal ${lw.file.toUri(outDir)}`) + await vscode.commands.executeCommand('revealFileInOS', lw.file.toUri(outDir)) } export function recipes(recipe?: string) { @@ -93,6 +103,10 @@ export function synctex() { return } const configuration = vscode.workspace.getConfiguration('latex-workshop', lw.root.getWorkspace()) + + if (lw.extra.liveshare.handle.command.syncTeX()) { + return + } let pdfFile: string | undefined = undefined if (lw.root.subfiles.path && configuration.get('latex.rootFile.useSubFile')) { pdfFile = lw.file.getPdfPath(lw.root.subfiles.path) @@ -462,7 +476,7 @@ export function toggleMathPreviewPanel() { } async function quickPickRootFile(rootFile: string, localRootFile: string, verb: string): Promise { - const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(rootFile)) + const configuration = vscode.workspace.getConfiguration('latex-workshop', lw.file.toUri(rootFile)) const doNotPrompt = configuration.get('latex.rootFile.doNotPrompt') as boolean if (doNotPrompt) { if (configuration.get('latex.rootFile.useSubFile')) { diff --git a/src/core/file.ts b/src/core/file.ts index a5f01cf17..68b95d902 100644 --- a/src/core/file.ts +++ b/src/core/file.ts @@ -24,6 +24,9 @@ export const file = { exists, read, kpsewhich, + getUriScheme, + isUriScheme, + toUri, _test: { createTmpDir } @@ -176,7 +179,7 @@ function hasDtxLangId(langId: string): boolean { * * @type {Object.} */ -const texDirs: {[tex: string]: {out?: string, aux?: string}} = {} +const texDirs: { [tex: string]: { out?: string, aux?: string } } = {} /** * Sets the output and auxiliary files directory for a root TeX file. * @@ -199,7 +202,7 @@ function setTeXDirs(tex: string, out?: string, aux?: string) { if (!tex.endsWith('.tex')) { tex += '.tex' } - texDirs[tex] = {out, aux} + texDirs[tex] = { out, aux } } /** @@ -237,7 +240,7 @@ function getOutDir(texPath?: string): string { return './' } - const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(texPath)) + const configuration = vscode.workspace.getConfiguration('latex-workshop', lw.file.toUri(texPath)) const outDir = configuration.get('latex.outDir') as string || './' const out = utils.replaceArgumentPlaceholders(texPath, file.tmpDirPath)(outDir) let result = undefined @@ -290,7 +293,7 @@ function getLangId(filename: string): string | undefined { * configuration or derived from the file name. */ function getJobname(texPath: string): string { - const jobname = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(texPath)).get('latex.jobname') as string + const jobname = vscode.workspace.getConfiguration('latex-workshop', lw.file.toUri(texPath)).get('latex.jobname') as string return jobname || path.parse(texPath).name } @@ -350,7 +353,7 @@ async function getFlsPath(texPath: string): Promise { * avoiding redundant executions of the `kpsewhich` command by returning * previously computed results quickly. */ -const kpsecache: {[query: string]: string} = {} +const kpsecache: { [query: string]: string } = {} /** * Resolves the path to a given LaTeX target using the `kpsewhich` command. * @@ -380,7 +383,7 @@ function kpsewhich(target: string, isBib: boolean = false): string | undefined { try { const args = isBib ? ['-format=.bib', target] : [target] - const kpsewhichReturn = lw.external.sync(command, args, {cwd: lw.root.dir.path || vscode.workspace.workspaceFolders?.[0].uri.path}) + const kpsewhichReturn = lw.external.sync(command, args, { cwd: lw.root.dir.path || vscode.workspace.workspaceFolders?.[0].uri.path }) if (kpsewhichReturn.status === 0) { const output = kpsewhichReturn.stdout.toString().replace(/\r?\n/, '') logger.log(`kpsewhich returned with '${output}'.`) @@ -431,7 +434,7 @@ function getBibPath(bib: string, baseDir: string): string[] { if (bibPath === undefined || bibPath.length === 0) { if (configuration.get('kpsewhich.bibtex.enabled')) { const kpsePath = kpsewhich(bib, true) - return kpsePath ? [ kpsePath ] : [] + return kpsePath ? [kpsePath] : [] } else { logger.log(`Cannot resolve bib path: ${bib} .`) return [] @@ -440,9 +443,9 @@ function getBibPath(bib: string, baseDir: string): string[] { if (os.platform() === 'win32') { // Normalize drive letters on Windows. - return [ bibPath ].flat().map(p => p.replace(/^([a-zA-Z]):/, (_, p1: string) => p1.toLowerCase() + ':')) + return [bibPath].flat().map(p => p.replace(/^([a-zA-Z]):/, (_, p1: string) => p1.toLowerCase() + ':')) } else { - return [ bibPath ].flat() + return [bibPath].flat() } } @@ -472,7 +475,7 @@ function getBibPath(bib: string, baseDir: string): string[] { */ async function read(filePath: string, raise: boolean = false): Promise { try { - return (await vscode.workspace.fs.readFile(vscode.Uri.file(filePath))).toString() + return (await vscode.workspace.fs.readFile(lw.file.toUri(filePath))).toString() } catch (err) { if (raise === false) { return undefined @@ -486,7 +489,7 @@ async function read(filePath: string, raise: boolean = false): Promise { - if (typeof(uri) === 'string') { - uri = vscode.Uri.file(uri) + if (typeof (uri) === 'string') { + uri = lw.file.toUri(uri) } try { await lw.external.stat(uri) @@ -509,3 +512,29 @@ async function exists(uri: vscode.Uri | string): Promise { return false } } + +/** +* Returns the scheme of the URI based on the file path. +* +* This function determines the URI scheme based on the file path provided. It +* checks if the file path starts with any of the workspace folder paths and +* returns the scheme associated with the workspace folder. If the file path +* does not match any workspace folder, it returns the default scheme based on +* whether the user is a guest in a Live Share session. +* +* @param {string} filePath - The file path for which to determine the URI scheme. +* @returns {string} - The URI scheme associated with the file path. +*/ +function getUriScheme(filePath?: string): string { + const scheme = + vscode.workspace.workspaceFolders?.filter(folder => filePath?.startsWith(folder.uri.path))[0]?.uri.scheme + return scheme ?? (lw.extra.liveshare.isGuest() ? 'vsls' : 'file') +} + +function isUriScheme(fileUri: vscode.Uri): boolean { + return ['file', 'vsls'].includes(fileUri.scheme) +} + +function toUri(filePath: string): vscode.Uri { + return vscode.Uri.file(filePath).with({ scheme: getUriScheme(filePath) }) +} diff --git a/src/core/root.ts b/src/core/root.ts index c8906aa84..f220d5315 100644 --- a/src/core/root.ts +++ b/src/core/root.ts @@ -138,7 +138,7 @@ function getWorkspace(filePath?: string): vscode.Uri | undefined { } // If provided with a filePath, check its workspace if (filePath !== undefined) { - return (vscode.workspace.getWorkspaceFolder(vscode.Uri.file(filePath)) ?? firstWorkspace).uri + return (vscode.workspace.getWorkspaceFolder(lw.file.toUri(filePath)) ?? firstWorkspace).uri } // If we don't have an active text editor, we can only make a guess. // Let's guess the first one. @@ -208,7 +208,7 @@ function findFromRoot(): string | undefined { if (!vscode.window.activeTextEditor || root.file.path === undefined) { return } - if (vscode.window.activeTextEditor.document.uri.scheme !== 'file') { + if (!lw.file.isUriScheme(vscode.window.activeTextEditor.document.uri)) { logger.log(`The active document cannot be used as the root file: ${vscode.window.activeTextEditor.document.uri.toString(true)}`) return } @@ -231,7 +231,7 @@ function findFromActive(): string | undefined { if (!vscode.window.activeTextEditor) { return } - if (vscode.window.activeTextEditor.document.uri.scheme !== 'file') { + if (!lw.file.isUriScheme(vscode.window.activeTextEditor.document.uri)) { logger.log(`The active document cannot be used as the root file: ${vscode.window.activeTextEditor.document.uri.toString(true)}`) return } @@ -303,7 +303,7 @@ async function findInWorkspace(): Promise { const fileUris = await vscode.workspace.findFiles(rootFilesIncludeGlob, rootFilesExcludeGlob) const candidates: string[] = [] for (const fileUri of fileUris) { - if (fileUri.scheme !== 'file') { + if (!lw.file.isUriScheme(fileUri)) { logger.log(`Skip the file: ${fileUri.toString(true)}`) continue } diff --git a/src/core/watcher.ts b/src/core/watcher.ts index c20161e03..82a25b171 100644 --- a/src/core/watcher.ts +++ b/src/core/watcher.ts @@ -139,7 +139,7 @@ class Watcher { private async initiatePolling(uri: vscode.Uri): Promise { const filePath = uri.fsPath const firstChangeTime = Date.now() - const size = (await lw.external.stat(vscode.Uri.file(filePath))).size + const size = (await lw.external.stat(lw.file.toUri(filePath))).size this.polling[filePath] = { size, time: firstChangeTime } @@ -170,7 +170,7 @@ class Watcher { return } - const currentSize = (await lw.external.stat(vscode.Uri.file(filePath))).size + const currentSize = (await lw.external.stat(lw.file.toUri(filePath))).size if (currentSize !== size) { this.polling[filePath].size = currentSize diff --git a/src/extras/cleaner.ts b/src/extras/cleaner.ts index e3d8b2182..45ee32501 100644 --- a/src/extras/cleaner.ts +++ b/src/extras/cleaner.ts @@ -45,7 +45,7 @@ async function clean(rootFile?: string): Promise { return } } - const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(rootFile)) + const configuration = vscode.workspace.getConfiguration('latex-workshop', lw.file.toUri(rootFile)) const cleanMethod = configuration.get('latex.clean.method') as string switch (cleanMethod) { case 'glob': @@ -102,7 +102,7 @@ function splitGlobs(globs: string[]): { fileOrFolderGlobs: string[], folderGlobs * intentionally by the user. Otherwise, the folders will be ignored. */ async function cleanGlob(rootFile: string): Promise { - const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(rootFile)) + const configuration = vscode.workspace.getConfiguration('latex-workshop', lw.file.toUri(rootFile)) const globPrefix = (configuration.get('latex.clean.subfolder.enabled') as boolean) ? './**/' : '' const globs = (configuration.get('latex.clean.fileTypes') as string[]) .map(globType => globPrefix + replaceArgumentPlaceholders(rootFile, lw.file.tmpDirPath)(globType)) @@ -149,7 +149,7 @@ async function cleanGlob(rootFile: string): Promise { } function cleanCommand(rootFile: string): Promise { - const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(rootFile)) + const configuration = vscode.workspace.getConfiguration('latex-workshop', lw.file.toUri(rootFile)) const command = configuration.get('latex.clean.command') as string const args = (configuration.get('latex.clean.args') as string[]) .map(arg => { return replaceArgumentPlaceholders(rootFile, lw.file.tmpDirPath)(arg) diff --git a/src/extras/index.ts b/src/extras/index.ts index db426ee75..12c6962a1 100644 --- a/src/extras/index.ts +++ b/src/extras/index.ts @@ -5,6 +5,7 @@ import { texroot } from './texroot' import { section } from './section' import * as commands from './activity-bar' import * as snippet from './snippet-view' +import * as liveshare from './liveshare' export const extra = { count, @@ -13,5 +14,6 @@ export const extra = { texroot, section, commands, - snippet + snippet, + liveshare } diff --git a/src/extras/liveshare.ts b/src/extras/liveshare.ts new file mode 100644 index 000000000..311c22cb8 --- /dev/null +++ b/src/extras/liveshare.ts @@ -0,0 +1,369 @@ +import * as vsls from 'vsls/vscode' +import * as vscode from 'vscode' +import * as url from 'url' +import http from 'http' +import ws from 'ws' +import { lw } from '../lw' +import type { ClientRequest, ServerResponse } from '../../types/latex-workshop-protocol-types' +import type { Client } from '../preview/viewer/client' +import { getClients } from '../preview/viewer/pdfviewermanager' + +const logger = lw.log('LiveShare') + +export { + getApi, + getHostServerPort, + isGuest, + isHost, + handle, + register, + shareServer +} + +const handle = { + command: { + syncTeX: handleCommandSyncTeX + }, + viewer: { + refresh: handleViewerRefresh, + reverseSyncTeX: handleViewerReverseSyncTeX, + syncTeX: handleViewerSyncTeX + }, + server: { + request: handleServerRequest + } +} + +const state: { + initialized: Promise, + liveshare: vsls.LiveShare | undefined, + role: vsls.Role, + hostServerPort: number | undefined, + shareServerDisposable: vscode.Disposable | undefined, + connected: boolean, + ws: ws.WebSocket | undefined +} = { + initialized: new Promise(resolve => + vsls.getApi().then(api => { + if (api === null) { + resolve() + return + } + setRole(api.session.role) + state.liveshare = api + state.liveshare.onDidChangeSession(e => setRole(e.session.role)) + resolve() + }) + ), + liveshare: undefined, + role: vsls.Role.None, + hostServerPort: undefined, + shareServerDisposable: undefined, + connected: false, + ws: undefined +} + +function isGuest() { + return state.role === vsls.Role.Guest +} + +function isHost() { + return state.role === vsls.Role.Host +} + +function getApi() { + return state.liveshare +} + +function setRole(role: vsls.Role) { + state.role = role + state.hostServerPort = undefined + state.shareServerDisposable?.dispose() + resetConnection() + if (role === vsls.Role.Guest) { + void initGuest() + } else if (role === vsls.Role.Host) { + void initHost() + } +} + +async function initGuest() { + await getHostServerPort() + await connectHost() +} + +async function initHost() { + await shareServer() +} + +async function getHostServerPort(reset: boolean = false): Promise { + if (!reset && state.hostServerPort !== undefined) { + return state.hostServerPort + } + const savedClipboard = await vscode.env.clipboard.readText() + void vscode.commands.executeCommand('liveshare.listServers') + // delay here instead of doing await vscode.commands.executeCommand acquires the port more reliably because await vscode.commands.executeCommand does not return until the user closes the info box of the command or clicks copy again. + await sleep(500) + const hostUrl = await vscode.env.clipboard.readText() + const hostServerPort = Number(url.parse(hostUrl).port) + state.hostServerPort = hostServerPort + await vscode.env.clipboard.writeText(savedClipboard) + return hostServerPort +} + +async function shareServer() { + if (state.role !== vsls.Role.Host) { + return + } + state.shareServerDisposable?.dispose() + await state.initialized + state.shareServerDisposable = await state.liveshare?.shareServer({ port: lw.server.getPort(), displayName: 'latex-workshop-server' }) +} + + +async function connectHost() { + logger.log('Connecting to host') + if (state.role !== vsls.Role.Guest) { + resetConnection() + return + } + + if (state.connected) { + logger.log('Already connected to host.') + return + } + + const server = await vscode.env.asExternalUri(vscode.Uri.parse(`http://127.0.0.1:${await getHostServerPort()}`, true)) + + await new Promise(resolve => { + const websocket = new ws.WebSocket(server.toString(true)) + websocket.addEventListener('open', () => { + logger.log('Connected to host') + state.ws = websocket + state.connected = true + resolve() + }) + }) + state.ws?.addEventListener('message', event => { + if (event.type === 'message') { + connectionHandler(event.data as string) + } + }) + state.ws?.addEventListener('close', async () => { + logger.log('Connection to host disconnected') + state.connected = false + await reconnect() + }) + state.ws?.addEventListener('error', err => logger.logError(`Failed to connect to ${server}`, err)) + + const id = setInterval(() => { + try { + sendToHost({ type: 'ping' }) + } + catch { + clearInterval(id) + } + }, 30000) +} + +function resetConnection() { + logger.log('Reset connection to host') + state.connected = false + state.ws = undefined +} + +function connectionHandler(msg: string): void { + const data = JSON.parse(msg) as ServerResponse + logger.log(`Handle data type: ${data.type}`) + + switch (data.type) { + case 'refresh': { + lw.viewer.refresh(vscode.Uri.parse(data.pdfFileUri).fsPath) + break + } + case 'reverse_synctex_result': { + void lw.locate.synctex.components.openTeX(vscode.Uri.parse(data.input).fsPath, data.line, data.column, data.textBeforeSelection, data.textAfterSelection) + break + } + case 'synctex_result': { + void lw.viewer.locate(vscode.Uri.parse(data.pdfFile, true).fsPath, data.synctexData) + break + } + default: { + break + } + } +} + +async function reconnect() { + // Since WebSockets are disconnected when PC resumes from sleep, + // we have to reconnect. https://github.com/James-Yu/LaTeX-Workshop/pull/1812 + await sleep(3000) + + let tries = 1 + while (tries <= 10) { + try { + await connectHost() + register() + if (state.ws?.readyState !== 1) { + throw new Error('Connection to host is not open.') + } + return + } catch (e) { + } + + await sleep(1000 * (tries + 2)) + tries++ + } +} + +function register(client?: Client) { + if (client) { + sendToHost({ type: 'open', pdfFileUri: client.pdfFileUri, viewer: client.viewer }) + } + + getClients()?.forEach(guestClient => { + sendToHost({ type: 'open', pdfFileUri: guestClient.pdfFileUri, viewer: guestClient.viewer }) + }) +} + +function sendToHost(message: ClientRequest) { + logger.log(`Sends message ${JSON.stringify(message)} to host`) + if (state.role !== vsls.Role.Guest) { + return + } + + if (state.ws?.readyState === 1) { + state.ws.send(JSON.stringify(message)) + } +} + +function handleCommandSyncTeX(): boolean { + if (!isGuest()) { + return false + } + const coords = lw.locate.synctex.components.getCurrentEditorCoordinates() + + if (lw.root.file.path === undefined || coords === undefined) { + logger.log('Cannot find LaTeX root PDF to perform synctex.') + return true + } + + const pdfFileUri = lw.file.toUri(lw.file.getPdfPath(lw.root.file.path)) + const configuration = vscode.workspace.getConfiguration('latex-workshop', lw.root.getWorkspace()) + const indicator = configuration.get('synctex.indicator') as 'none' | 'circle' | 'rectangle' + sendToHost({type: 'synctex', line: coords.line, column: coords.column, filePath: coords.inputFileUri.toString(true), targetPdfFile: pdfFileUri.toString(true), indicator}) + return true +} + +function handleViewerRefresh(pdfFile?: string, clientSet?: Set) { + if (isHost() && state.liveshare && pdfFile !== undefined) { + const sharedUri = state.liveshare.convertLocalUriToShared(lw.file.toUri(pdfFile)) + const guestClients = getClients(sharedUri) + if (guestClients) { + clientSet?.forEach(client => guestClients.add(client)) + return guestClients + } + } + return clientSet +} + +function handleViewerReverseSyncTeX(websocket: ws, uri: vscode.Uri, data: Extract): boolean { + if (isGuest()) { + state.ws?.send(JSON.stringify(data)) // forward the request to host + return true + } else if (isHost() && uri.scheme === 'vsls' && state.liveshare) { // reply to guest if request comes from guest + const localUri = state.liveshare.convertSharedUriToLocal(uri) ?? uri + const record = lw.locate.synctex.components.computeToTeX(data, localUri.fsPath) + if (record) { + const response: ServerResponse = { + type: 'reverse_synctex_result', + input: state.liveshare.convertLocalUriToShared(vscode.Uri.file(record.input)).toString(true), + line: record.line, + column: record.column, + textBeforeSelection: data.textAfterSelection, + textAfterSelection: data.textAfterSelection + } + websocket.send(JSON.stringify(response)) + } + return true + } + return false +} + +function handleViewerSyncTeX(websocket: ws, data: ClientRequest): boolean { + if (data.type !== 'synctex') { + return false + } + if (!isHost() || !state.liveshare) { + return true + } + + const filePath = state.liveshare.convertSharedUriToLocal(vscode.Uri.parse(data.filePath, true)).fsPath + const targetPdfFile = state.liveshare.convertSharedUriToLocal(vscode.Uri.parse(data.targetPdfFile, true)).fsPath + void lw.locate.synctex.components.synctexToPDFCombined(data.line, data.column, filePath, targetPdfFile, data.indicator).then(record => { + if (!record) { + logger.log(`Failed to locate synctex for ${filePath}. This was requested from a guest.`) + return + } + + const response: ServerResponse = { + type: 'synctex_result', + pdfFile: data.targetPdfFile, + synctexData: record + } + + websocket.send(JSON.stringify(response)) + }) + return true +} + +async function handleServerRequest(request: http.IncomingMessage, response: http.ServerResponse): Promise { + if (!isGuest()) { + return false + } + + if (!request.url) { + return true + } + + const requestUrl = url.parse(request.url) + + const options = { + host: requestUrl.hostname, + port: await getHostServerPort(), + path: requestUrl.path, + method: request.method, + headers: request.headers, + } + + const backendReq = http.request(options, (backendRes) => { + if (!backendRes.statusCode) { + response.end() + return + } + response.writeHead(backendRes.statusCode, backendRes.headers) + + backendRes.on('data', (chunk) => { + response.write(chunk) + }) + + backendRes.on('end', () => { + response.end() + }) + }) + + request.on('data', (chunk) => { + backendReq.write(chunk) + }) + + request.on('end', () => { + backendReq.end() + }) + + return true +} + +async function sleep(timeout: number) { + await new Promise((resolve) => setTimeout(resolve, timeout)) +} diff --git a/src/language/definition.ts b/src/language/definition.ts index 683fef513..4dfa96e31 100644 --- a/src/language/definition.ts +++ b/src/language/definition.ts @@ -41,7 +41,7 @@ export class DefinitionProvider implements vscode.DefinitionProvider { } provideDefinition(document: vscode.TextDocument, position: vscode.Position): vscode.Location | undefined { - if (document.uri.scheme !== 'file') { + if (!lw.file.isUriScheme(document.uri)) { return } const token = tokenizer(document, position) @@ -58,15 +58,15 @@ export class DefinitionProvider implements vscode.DefinitionProvider { } const ref = lw.completion.reference.getItem(token) if (ref) { - return new vscode.Location(vscode.Uri.file(ref.file), ref.position) + return new vscode.Location(lw.file.toUri(ref.file), ref.position) } const cite = lw.completion.citation.getItem(token) if (cite) { - return new vscode.Location(vscode.Uri.file(cite.file), cite.position) + return new vscode.Location(lw.file.toUri(cite.file), cite.position) } const glossary = lw.completion.glossary.getItem(token) if (glossary) { - return new vscode.Location(vscode.Uri.file(glossary.filePath), glossary.position) + return new vscode.Location(lw.file.toUri(glossary.filePath), glossary.position) } if (vscode.window.activeTextEditor && token.includes('.')) { // We skip graphics files @@ -77,13 +77,13 @@ export class DefinitionProvider implements vscode.DefinitionProvider { } const absolutePath = path.resolve(path.dirname(vscode.window.activeTextEditor.document.fileName), token) if (fs.existsSync(absolutePath)) { - return new vscode.Location( vscode.Uri.file(absolutePath), new vscode.Position(0, 0) ) + return new vscode.Location( lw.file.toUri(absolutePath), new vscode.Position(0, 0) ) } } const filename = this.onAFilename(document, position, token) if (filename) { - return new vscode.Location( vscode.Uri.file(filename), new vscode.Position(0, 0) ) + return new vscode.Location( lw.file.toUri(filename), new vscode.Position(0, 0) ) } return } diff --git a/src/language/symbol-document.ts b/src/language/symbol-document.ts index 3057289c7..50959271e 100644 --- a/src/language/symbol-document.ts +++ b/src/language/symbol-document.ts @@ -3,6 +3,7 @@ import { type TeXElement, TeXElementType } from '../types' import { buildBibTeX } from '../outline/structure/bibtex' import { construct as constructLaTeX } from '../outline/structure/latex' import { construct } from '../outline/structure/doctex' +import { lw } from '../lw' export class DocSymbolProvider implements vscode.DocumentSymbolProvider { @@ -12,7 +13,7 @@ export class DocSymbolProvider implements vscode.DocumentSymbolProvider { } else if (document.languageId === 'doctex') { return construct(document).then((sections: TeXElement[]) => this.sectionToSymbols(sections)) } - if (document.uri.scheme !== 'file') { + if (!lw.file.isUriScheme(document.uri)) { return [] } const sections = await constructLaTeX(document.fileName, false) diff --git a/src/language/symbol-project.ts b/src/language/symbol-project.ts index e572eabb3..a0b355384 100644 --- a/src/language/symbol-project.ts +++ b/src/language/symbol-project.ts @@ -16,7 +16,7 @@ export class ProjectSymbolProvider implements vscode.WorkspaceSymbolProvider { private sectionToSymbols(sections: TeXElement[], containerName: string = 'Document'): vscode.SymbolInformation[] { let symbols: vscode.SymbolInformation[] = [] sections.forEach(section => { - const location = new vscode.Location(vscode.Uri.file(section.filePath), new vscode.Range(section.lineFr, 0, section.lineTo, 65535)) + const location = new vscode.Location(lw.file.toUri(section.filePath), new vscode.Range(section.lineFr, 0, section.lineTo, 65535)) symbols.push(new vscode.SymbolInformation(section.label, vscode.SymbolKind.String, containerName, location)) if (section.children.length > 0) { symbols = [...symbols, ...this.sectionToSymbols(section.children, section.label)] diff --git a/src/lint/duplicate-label.ts b/src/lint/duplicate-label.ts index 449ddc746..d505aa92e 100644 --- a/src/lint/duplicate-label.ts +++ b/src/lint/duplicate-label.ts @@ -79,7 +79,7 @@ function showDiagnostics(duplicates: string[]) { for (const file in diagsCollection) { if (path.extname(file) === '.tex') { - duplicatedLabelsDiagnostics.set(vscode.Uri.file(file), diagsCollection[file]) + duplicatedLabelsDiagnostics.set(lw.file.toUri(file), diagsCollection[file]) } } } diff --git a/src/lint/latex-linter/chktex.ts b/src/lint/latex-linter/chktex.ts index efee015cd..20007dfd3 100644 --- a/src/lint/latex-linter/chktex.ts +++ b/src/lint/latex-linter/chktex.ts @@ -24,7 +24,7 @@ let linterProcess: ChildProcessWithoutNullStreams | undefined async function lintRootFile(rootPath: string) { const requiredArgs = ['-f%f:%l:%c:%d:%k:%n:%m\n', rootPath] - const stdout = await chktexWrapper('root', vscode.Uri.file(rootPath), rootPath, requiredArgs, undefined) + const stdout = await chktexWrapper('root', lw.file.toUri(rootPath), rootPath, requiredArgs, undefined) if (stdout === undefined) { // It's possible to have empty string as output return } @@ -131,7 +131,7 @@ function globalRcPath(): string | undefined { } function getChktexrcTabSize(file: string): number | undefined { - const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(file)) + const configuration = vscode.workspace.getConfiguration('latex-workshop', lw.file.toUri(file)) const args = configuration.get('linting.chktex.exec.args') as string[] let filePath: string | undefined if (args.includes('-l')) { @@ -197,7 +197,7 @@ function parseLog(log: string, singleFileOriginalPath?: string, tabSizeArg?: num } else if (linterLog.length === 0) { // We are linting a single file and the new log is empty for it - // clean existing records. - chkTeX.linterDiagnostics.set(vscode.Uri.file(singleFileOriginalPath), []) + chkTeX.linterDiagnostics.set(lw.file.toUri(singleFileOriginalPath), []) } showLinterDiagnostics(linterLog) } @@ -279,7 +279,7 @@ function showLinterDiagnostics(linterLog: ChkTeXLogEntry[]) { file1 = f } } - chkTeX.linterDiagnostics.set(vscode.Uri.file(file1), diagsCollection[file]) + chkTeX.linterDiagnostics.set(lw.file.toUri(file1), diagsCollection[file]) } } } diff --git a/src/lint/latex-linter/lacheck.ts b/src/lint/latex-linter/lacheck.ts index fb2df8948..3ab2630de 100644 --- a/src/lint/latex-linter/lacheck.ts +++ b/src/lint/latex-linter/lacheck.ts @@ -21,7 +21,7 @@ export const laCheck: LaTeXLinter = { let linterProcess: ChildProcessWithoutNullStreams | undefined async function lintRootFile(rootPath: string) { - const stdout = await lacheckWrapper('root', vscode.Uri.file(rootPath), rootPath, undefined) + const stdout = await lacheckWrapper('root', lw.file.toUri(rootPath), rootPath, undefined) if (stdout === undefined) { // It's possible to have empty string as output return } @@ -130,7 +130,7 @@ function showLinterDiagnostics(linterLog: LaCheckLogEntry[]) { file1 = f } } - laCheck.linterDiagnostics.set(vscode.Uri.file(file1), diagsCollection[file]) + laCheck.linterDiagnostics.set(lw.file.toUri(file1), diagsCollection[file]) } } } diff --git a/src/locate/synctex.ts b/src/locate/synctex.ts index b0f3a3054..a8bb63946 100644 --- a/src/locate/synctex.ts +++ b/src/locate/synctex.ts @@ -14,7 +14,13 @@ const logger = lw.log('Locator') export const synctex = { toPDF, toPDFFromRef, - toTeX + toTeX, + components: { + synctexToPDFCombined, + computeToTeX, + openTeX, + getCurrentEditorCoordinates + } } /** @@ -174,6 +180,47 @@ function parseToPDFList(result: string): SyncTeXRecordToPDFAll[] { // } // } +function getCurrentEditorCoordinates(): {line: number, column: number, inputFileUri: vscode.Uri} | undefined { + if (!vscode.window.activeTextEditor) { + logger.log('No active editor found.') + return + } + + const inputFileUri = vscode.window.activeTextEditor.document.uri + if (!lw.file.hasTeXLangId(vscode.window.activeTextEditor.document.languageId)) { + logger.log(`${inputFileUri} is not valid LaTeX.`) + return + } + const position = vscode.window.activeTextEditor.selection.active + if (!position) { + logger.log(`No cursor position from ${position}`) + return + } + + let line = position.line + 1 + const column = position.character + + if (vscode.window.activeTextEditor.document.lineCount === line && + vscode.window.activeTextEditor.document.lineAt(line - 1).text === '') { + line -= 1 + } + + return {line, column, inputFileUri} +} + +async function synctexToPDFCombined(line: number, col: number, filePath: string, targetPdfFile: string, indicator: 'none' | 'circle' | 'rectangle'): Promise { + try { + return await callSyncTeXToPDF(line, col, filePath, targetPdfFile, indicator) + } catch { + logger.log(`Compute with synctex.js from ${filePath} to ${targetPdfFile} on line ${line}.`) + const record = syncTeXToPDF(line, filePath, targetPdfFile) + if (!record) { + throw new Error('Failed to compute the SyncTeX record.') + } + return record + } +} + /** * Execute forward SyncTeX with respect to the provided arguments. * @@ -192,59 +239,44 @@ function parseToPDFList(result: string): SyncTeXRecordToPDFAll[] { function toPDF(args?: {line: number, filePath: string}, forcedViewer: 'auto' | 'tabOrBrowser' | 'external' = 'auto', pdfFile?: string) { let line: number let filePath: string - let character = 0 + let column = 0 + if (!vscode.window.activeTextEditor) { logger.log('No active editor found.') return } + if (lw.root.file.path === undefined) { + return + } + if (args === undefined) { - filePath = vscode.window.activeTextEditor.document.uri.fsPath - if (!lw.file.hasTeXLangId(vscode.window.activeTextEditor.document.languageId)) { - logger.log(`${filePath} is not valid LaTeX.`) + const currentEditorCoordinates = getCurrentEditorCoordinates() + if (currentEditorCoordinates === undefined) { return } - const position = vscode.window.activeTextEditor.selection.active - if (!position) { - logger.log(`No cursor position from ${position}`) - return - } - line = position.line + 1 - character = position.character + line = currentEditorCoordinates.line + column = currentEditorCoordinates.column + filePath = currentEditorCoordinates.inputFileUri.fsPath } else { line = args.line filePath = args.filePath } + + const rootFile = lw.file.toUri(lw.root.file.path).fsPath + const targetPdfFile = pdfFile ?? lw.file.toUri(lw.file.getPdfPath(lw.root.file.path)).fsPath + const configuration = vscode.workspace.getConfiguration('latex-workshop') - const rootFile = lw.root.file.path - if (rootFile === undefined) { - logger.log('No root file found.') - return - } - const targetPdfFile = pdfFile ?? lw.file.getPdfPath(rootFile) - if (vscode.window.activeTextEditor.document.lineCount === line && - vscode.window.activeTextEditor.document.lineAt(line - 1).text === '') { - line -= 1 - } if (forcedViewer === 'external' || (forcedViewer === 'auto' && configuration.get('view.pdf.viewer') === 'external') ) { syncTeXExternal(line, targetPdfFile, rootFile) return } - callSyncTeXToPDF(line, character, filePath, targetPdfFile, configuration.get('synctex.indicator') as 'none' | 'circle' | 'rectangle').then((record) => { - void lw.viewer.locate(targetPdfFile, record) - }).catch(() => { - try { - logger.log(`Forward with synctex.js from ${filePath} to ${pdfFile} on line ${line}.`) - const record = syncTeXToPDF(line, filePath, targetPdfFile) - if (!record) { - return - } - void lw.viewer.locate(targetPdfFile, record) - } catch (e) { - logger.logError('Forward SyncTeX failed.', e) - } - }) + void synctexToPDFCombined(line, column, filePath, targetPdfFile, configuration.get('synctex.indicator') as 'none' | 'circle' | 'rectangle').then(async (record) => { + await lw.viewer.locate(targetPdfFile, record) + }).catch(e => + logger.logError('Forward SyncTeX failed.', e) + ) } /** @@ -420,6 +452,13 @@ function toPDFFromRef(args: {line: number, filePath: string}) { * @param pdfPath - The path of the PDF file. */ async function toTeX(data: Extract, pdfPath: string) { + const record = computeToTeX(data, pdfPath) + if (record) { + await openTeX(record.input, record.line, record.column, data.textBeforeSelection, data.textAfterSelection) + } +} + +function computeToTeX(data: Extract, pdfPath: string): SyncTeXRecordToTeX | undefined { let record: SyncTeXRecordToTeX // We only use synctex.js for backward sync as the binary cannot handle CJK encodings #4239. @@ -460,19 +499,27 @@ async function toTeX(data: Extract, pd } } - const filePath = path.resolve(record.input) - if (!fs.existsSync(filePath)) { + record.input = path.resolve(record.input) + return record +} + +async function openTeX(input: string, line: number, column: number, textBeforeSelection: string, textAfterSelection: string) { + const filePath = path.resolve(input) + const uri = lw.file.toUri(input) + try { + await vscode.workspace.fs.stat(uri) + } catch (e) { logger.log(`Backward SyncTeX failed on non-existent ${filePath} .`) return } logger.log(`Backward SyncTeX to ${filePath} .`) try { - const doc = await vscode.workspace.openTextDocument(filePath) - let row = record.line - 1 - let col = record.column < 0 ? 0 : record.column + const doc = await vscode.workspace.openTextDocument(uri) + let row = line - 1 + let col = column < 0 ? 0 : column // columns are typically not supplied by SyncTex, this could change in the future for some engines though if (col === 0) { - [row, col] = getRowAndColumn(doc, row, data.textBeforeSelection, data.textAfterSelection) + [row, col] = getRowAndColumn(doc, row, textBeforeSelection, textAfterSelection) } const pos = new vscode.Position(row, col) diff --git a/src/main.ts b/src/main.ts index 6e34d2f0c..2c0d127cd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -67,7 +67,7 @@ export function activate(extensionContext: vscode.ExtensionContext) { })) extensionContext.subscriptions.push(vscode.workspace.onDidSaveTextDocument( (e: vscode.TextDocument) => { - if (e.uri.scheme !== 'file'){ + if (!lw.file.isUriScheme(e.uri)){ return } if (lw.file.hasTeXLangId(e.languageId) || @@ -100,7 +100,7 @@ export function activate(extensionContext: vscode.ExtensionContext) { } else if (vscode.window.activeTextEditor && vscode.window.activeTextEditor.document.languageId.toLowerCase() === 'log') { logger.showStatus() } - if (e && e.document.uri.scheme !== 'file'){ + if (e && !lw.file.isUriScheme(e.document.uri)) { return } if (e && lw.file.hasTeXLangId(e.document.languageId) && e.document.fileName !== prevTeXDocumentPath) { @@ -119,7 +119,7 @@ export function activate(extensionContext: vscode.ExtensionContext) { })) extensionContext.subscriptions.push(vscode.workspace.onDidChangeTextDocument((e: vscode.TextDocumentChangeEvent) => { - if (e.document.uri.scheme !== 'file'){ + if (!lw.file.isUriScheme(e.document.uri)){ return } if (!lw.file.hasTeXLangId(e.document.languageId) && @@ -156,6 +156,7 @@ export function activate(extensionContext: vscode.ExtensionContext) { function registerLatexWorkshopCommands(extensionContext: vscode.ExtensionContext) { extensionContext.subscriptions.push( + vscode.commands.registerCommand('latex-workshop.hostPort', () => lw.commands.hostPort()), vscode.commands.registerCommand('latex-workshop.saveWithoutBuilding', () => lw.commands.saveActive()), vscode.commands.registerCommand('latex-workshop.build', () => lw.commands.build()), vscode.commands.registerCommand('latex-workshop.recipes', (recipe: string | undefined) => lw.commands.recipes(recipe)), @@ -266,6 +267,7 @@ function registerProviders(extensionContext: vscode.ExtensionContext) { extensionContext.subscriptions.push( vscode.languages.registerCompletionItemProvider({ scheme: 'file', language: 'tex'}, lw.completion.provider, '\\', '{'), + vscode.languages.registerCompletionItemProvider({ scheme: 'vsls', language: 'tex'}, lw.completion.provider, '\\', '{'), vscode.languages.registerCompletionItemProvider(bibtexSelector, lw.completion.bibProvider, '@') ) @@ -336,7 +338,7 @@ function conflictCheck() { function selectDocumentsWithId(ids: string[]): vscode.DocumentSelector { const selector = ids.map( (id) => { - return { scheme: 'file', language: id } + return { scheme: lw.file.getUriScheme(), language: id } }) return selector } diff --git a/src/outline/structure/bibtex.ts b/src/outline/structure/bibtex.ts index 1d0e24d9a..292d20e19 100644 --- a/src/outline/structure/bibtex.ts +++ b/src/outline/structure/bibtex.ts @@ -22,7 +22,7 @@ function fieldValueToString(field: bibtexParser.FieldValue, abbreviations: {[abb } export async function buildBibTeX(document: vscode.TextDocument): Promise { - const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(document.fileName)) + const configuration = vscode.workspace.getConfiguration('latex-workshop', lw.file.toUri(document.fileName)) if (document.getText().length >= (configuration.get('bibtex.maxFileSize') as number) * 1024 * 1024) { logger.log(`Bib file is too large, ignoring it: ${document.fileName}`) return [] diff --git a/src/parse/parser/parserutils.ts b/src/parse/parser/parserutils.ts index 7ba7c9d23..5f1b9c9ae 100644 --- a/src/parse/parser/parserutils.ts +++ b/src/parse/parser/parserutils.ts @@ -74,6 +74,6 @@ export function showCompilerDiagnostics(diagnostics: vscode.DiagnosticCollection file1 = f } } - diagnostics.set(vscode.Uri.file(file1), diagsCollection[file]) + diagnostics.set(lw.file.toUri(file1), diagsCollection[file]) } } diff --git a/src/preview/hover/ongraphics.ts b/src/preview/hover/ongraphics.ts index be8952c2e..e4705f319 100644 --- a/src/preview/hover/ongraphics.ts +++ b/src/preview/hover/ongraphics.ts @@ -37,7 +37,7 @@ export async function onGraphics(document: vscode.TextDocument, position: vscode } export async function graph2md(filePath: string, opts: { height: number, width: number, pageNumber?: number }): Promise { - const filePathUriString = vscode.Uri.file(filePath).toString() + const filePathUriString = (filePath).toString() if (/\.(bmp|jpg|jpeg|gif|png)$/i.exec(filePath)) { // Workaround for https://github.com/microsoft/vscode/issues/137632 if (vscode.env.remoteName) { @@ -57,7 +57,7 @@ export async function graph2md(filePath: string, opts: { height: number, width: return md } else { let msg = '$(error) Failed to render.' - if (!vscode.workspace.getWorkspaceFolder(vscode.Uri.file(filePath))) { + if (!vscode.workspace.getWorkspaceFolder(lw.file.toUri(filePath))) { msg = '$(warning) Cannot render a PDF file not in workspaces.' } else if (lw.extra.snippet.state.view?.webview === undefined) { msg = '$(info) Please activate the LaTeX Workshop activity bar item to render PDF thumbnails.' @@ -73,19 +73,19 @@ async function renderPdfFileAsDataUrl(pdfFilePath: string, opts: { height: numbe const maxDataUrlLength = 99980 let scale = 1.5 let newOpts = { height: opts.height * scale , width: opts.width * scale, pageNumber: opts.pageNumber } - let dataUrl = await lw.extra.snippet.render(vscode.Uri.file(pdfFilePath), newOpts) + let dataUrl = await lw.extra.snippet.render(lw.file.toUri(pdfFilePath), newOpts) if (!dataUrl || dataUrl.length < maxDataUrlLength) { return dataUrl } scale = 1 newOpts = { height: opts.height * scale , width: opts.width * scale, pageNumber: opts.pageNumber } - dataUrl = await lw.extra.snippet.render(vscode.Uri.file(pdfFilePath), newOpts) + dataUrl = await lw.extra.snippet.render(lw.file.toUri(pdfFilePath), newOpts) if (!dataUrl || dataUrl.length < maxDataUrlLength) { return dataUrl } scale = Math.sqrt(maxDataUrlLength/dataUrl.length) / 1.2 newOpts = { height: opts.height * scale , width: opts.width * scale, pageNumber: opts.pageNumber } - dataUrl = await lw.extra.snippet.render(vscode.Uri.file(pdfFilePath), newOpts) + dataUrl = await lw.extra.snippet.render(lw.file.toUri(pdfFilePath), newOpts) if (dataUrl && dataUrl.length >= maxDataUrlLength) { logger.log(`Data URL still too large: ${pdfFilePath}`) return diff --git a/src/preview/math-preview-panel.ts b/src/preview/math-preview-panel.ts index 393aadbe4..27ce59a64 100644 --- a/src/preview/math-preview-panel.ts +++ b/src/preview/math-preview-panel.ts @@ -23,11 +23,11 @@ type UpdateEvent = { function resourcesFolder(extensionRoot: string) { const folder = path.join(extensionRoot, 'resources', 'mathpreviewpanel') - return vscode.Uri.file(folder) + return lw.file.toUri(folder) } class MathPreviewPanelSerializer implements vscode.WebviewPanelSerializer { - deserializeWebviewPanel(panel: vscode.WebviewPanel) { + async deserializeWebviewPanel(panel: vscode.WebviewPanel) { initializePanel(panel) panel.webview.options = { enableScripts: true, @@ -113,14 +113,14 @@ function close() { function toggle(action?: 'open' | 'close') { if (action) { if (action === 'open') { - open() + void open() } else { close() } } else if (state.panel) { close() } else { - open() + void open() } } @@ -157,7 +157,7 @@ function getHtml() { padding-left: 50px; } - +
diff --git a/src/preview/server.ts b/src/preview/server.ts index 657cecd14..410d25f83 100644 --- a/src/preview/server.ts +++ b/src/preview/server.ts @@ -30,10 +30,10 @@ class WsServer extends ws.Server { // - https://github.com/websockets/ws/blob/master/doc/ws.md#servershouldhandlerequest // shouldHandle(req: http.IncomingMessage): boolean { - if (!this.validOrigin.includes('127.0.0.1')) { + const reqOrigin = req.headers['origin'] + if (!this.validOrigin.includes('127.0.0.1') || reqOrigin?.includes('127.0.0.1')) { return true } - const reqOrigin = req.headers['origin'] if (reqOrigin !== undefined && reqOrigin !== this.validOrigin) { logger.log(`Origin in WebSocket upgrade request is invalid: ${JSON.stringify(req.headers)}`) logger.log(`Valid origin: ${this.validOrigin}`) @@ -149,7 +149,7 @@ function initializeWsServer(httpServer: http.Server, validOrigin: string) { // function checkHttpOrigin(req: http.IncomingMessage, response: http.ServerResponse): boolean { const validOrigin = getValidOrigin() - if (!validOrigin.includes('127.0.0.1')) { + if (!validOrigin.includes('127.0.0.1') || req.headers['origin']?.includes('127.0.0.1')) { return true } const reqOrigin = req.headers['origin'] @@ -185,6 +185,10 @@ function sendOkResponse(response: http.ServerResponse, content: Buffer, contentT } async function handler(request: http.IncomingMessage, response: http.ServerResponse) { + if (await lw.extra.liveshare.handle.server.request(request, response)) { + return + } + if (!request.url) { return } @@ -194,8 +198,12 @@ async function handler(request: http.IncomingMessage, response: http.ServerRespo } if (hasPrefix(request.url) && !request.url.includes('viewer.html')) { const s = request.url.replace('/', '') - const fileUri = decodePathWithPrefix(s) - if (!lw.viewer.isViewing(fileUri)) { + let fileUri = decodePathWithPrefix(s) + const isVsls = (fileUri.scheme === 'vsls') && (lw.extra.liveshare.isHost()) + if (isVsls) { + fileUri = lw.extra.liveshare.getApi()?.convertSharedUriToLocal(fileUri) ?? fileUri + } + if (!lw.viewer.isViewing(fileUri) && !isVsls) { logger.log(`Invalid PDF request: ${fileUri.toString(true)}`) return } diff --git a/src/preview/viewer.ts b/src/preview/viewer.ts index 6bad370ef..31f2edf4d 100644 --- a/src/preview/viewer.ts +++ b/src/preview/viewer.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode' -import type ws from 'ws' +import ws from 'ws' import * as path from 'path' import * as os from 'os' import * as cs from 'cross-spawn' @@ -53,26 +53,27 @@ function reload(): void { */ function refresh(pdfFile?: string): void { logger.log(`Call refreshExistingViewer: ${JSON.stringify(pdfFile)} .`) - const pdfUri = pdfFile ? vscode.Uri.file(pdfFile) : undefined + const pdfUri = pdfFile ? lw.file.toUri(pdfFile) : undefined if (pdfUri === undefined) { manager.getClients()?.forEach(client => { - client.send({type: 'refresh'}) + client.send({type: 'refresh', pdfFileUri: client.pdfFileUri}) }) return } - const clientSet = manager.getClients(pdfUri) + let clientSet = manager.getClients(pdfUri) + clientSet = lw.extra.liveshare.handle.viewer.refresh(pdfFile, clientSet) if (!clientSet) { logger.log(`Not found PDF viewers to refresh: ${pdfFile}`) return } logger.log(`Refresh PDF viewer: ${pdfFile}`) clientSet.forEach(client => { - client.send({type: 'refresh'}) + client.send({type: 'refresh', pdfFileUri: client.pdfFileUri}) }) } async function getUrl(pdfFile: string): Promise { - const pdfUri = vscode.Uri.file(pdfFile) + const pdfUri = lw.file.toUri(pdfFile) if (!await lw.file.exists(pdfUri)) { logger.log(`Cannot find PDF file ${pdfUri}`) logger.refreshStatus('check', 'statusBar.foreground', `Cannot view file PDF file. File not found: ${pdfUri}`, 'warning') @@ -111,7 +112,7 @@ async function viewInBrowser(pdfFile: string): Promise { if (!url) { return } - const pdfUri = vscode.Uri.file(pdfFile) + const pdfUri = lw.file.toUri(pdfFile) manager.create(pdfUri) lw.watcher.pdf.add(pdfUri.fsPath) try { @@ -139,7 +140,7 @@ async function viewInTab(pdfFile: string, tabEditorGroup: string, preserveFocus: if (!url) { return } - const pdfUri = vscode.Uri.file(pdfFile) + const pdfUri = lw.file.toUri(pdfFile) return viewInWebviewPanel(pdfUri, tabEditorGroup, preserveFocus) } @@ -150,7 +151,7 @@ async function viewInCustomEditor(pdfFile: string): Promise { } const configuration = vscode.workspace.getConfiguration('latex-workshop') const editorGroup = configuration.get('view.pdf.tab.editorGroup') as string - const pdfUri = vscode.Uri.file(pdfFile) + const pdfUri = lw.file.toUri(pdfFile) const showOptions: vscode.TextDocumentShowOptions = { viewColumn: vscode.ViewColumn.Active, preserveFocus: true @@ -274,11 +275,15 @@ function handler(websocket: ws, msg: string): void { switch (data.type) { case 'open': { const pdfUri = vscode.Uri.parse(data.pdfFileUri, true) + if (pdfUri.scheme === 'vsls' && lw.extra.liveshare.isHost()) { + manager.create(pdfUri) + } const clientSet = manager.getClients(pdfUri) if (clientSet === undefined) { break } - const client = new Client(data.viewer, websocket) + const client = new Client(data.viewer, websocket, pdfUri.toString(true)) + lw.extra.liveshare.register(client) clientSet.add(client) client.onDidDispose(() => { clientSet.delete(client) @@ -297,6 +302,9 @@ function handler(websocket: ws, msg: string): void { } case 'reverse_synctex': { const uri = vscode.Uri.parse(data.pdfFileUri, true) + if (lw.extra.liveshare.handle.viewer.reverseSyncTeX(websocket, uri, data)) { + break + } void lw.locate.synctex.toTeX(data, uri.fsPath) break } @@ -338,6 +346,9 @@ function handler(websocket: ws, msg: string): void { break } default: { + if (lw.extra.liveshare.handle.viewer.syncTeX(websocket, data)) { + break + } logger.log(`Unknown websocket message: ${msg}`) break } @@ -393,7 +404,7 @@ function getParams(): PdfViewerParams { * @param record The position to be revealed. */ async function locate(pdfFile: string, record: SyncTeXRecordToPDF | SyncTeXRecordToPDFAll[]): Promise { - const pdfUri = vscode.Uri.file(pdfFile) + const pdfUri = lw.file.toUri(pdfFile) let clientSet = manager.getClients(pdfUri) if (clientSet === undefined || clientSet.size === 0) { logger.log(`PDF is not opened: ${pdfFile} , try opening.`) diff --git a/src/preview/viewer/client.ts b/src/preview/viewer/client.ts index 56274cd2b..d978804de 100644 --- a/src/preview/viewer/client.ts +++ b/src/preview/viewer/client.ts @@ -5,14 +5,16 @@ import type { ServerResponse } from '../../../types/latex-workshop-protocol-type export class Client { readonly viewer: 'browser' | 'tab' readonly websocket: ws + readonly pdfFileUri: string private readonly disposables = new Set() - constructor(viewer: 'browser' | 'tab', websocket: ws) { + constructor(viewer: 'browser' | 'tab', websocket: ws, pdfFileUri: string) { this.viewer = viewer this.websocket = websocket this.websocket.on('close', () => { this.disposeDisposables() }) + this.pdfFileUri = pdfFileUri } private disposeDisposables() { diff --git a/src/preview/viewer/pdfviewerpanel.ts b/src/preview/viewer/pdfviewerpanel.ts index 03e51cf8e..6db592062 100644 --- a/src/preview/viewer/pdfviewerpanel.ts +++ b/src/preview/viewer/pdfviewerpanel.ts @@ -47,7 +47,7 @@ class PdfViewerPanelSerializer implements vscode.WebviewPanelSerializer { const state = argState.state let pdfFileUri: vscode.Uri | undefined if (state.path) { - pdfFileUri = vscode.Uri.file(state.path) + pdfFileUri = lw.file.toUri(state.path) } else if (state.pdfFileUri) { pdfFileUri = vscode.Uri.parse(state.pdfFileUri, true) } diff --git a/src/utils/quick-pick.ts b/src/utils/quick-pick.ts index fe315c820..74061927a 100644 --- a/src/utils/quick-pick.ts +++ b/src/utils/quick-pick.ts @@ -1,7 +1,8 @@ import * as vscode from 'vscode' +import { lw } from '../lw' export async function pickRootPath(rootPath: string, subRootPath: string, verb: string): Promise { - const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(rootPath)) + const configuration = vscode.workspace.getConfiguration('latex-workshop', lw.file.toUri(rootPath)) const doNotPrompt = configuration.get('latex.rootFile.doNotPrompt') as boolean if (doNotPrompt) { if (configuration.get('latex.rootFile.useSubFile')) { diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 52a44415d..d4efd4054 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -2,6 +2,7 @@ import * as vscode from 'vscode' import * as path from 'path' import * as fs from 'fs' import { glob } from 'glob' +import { lw } from '../lw' export function sleep(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)) @@ -268,7 +269,7 @@ export function resolveFileGlob(dirs: string[], inputGlob: string, suffix: strin */ export function replaceArgumentPlaceholders(rootFile: string, tmpDir: string): (arg: string) => string { return (arg: string) => { - const configuration = vscode.workspace.getConfiguration('latex-workshop', vscode.Uri.file(rootFile)) + const configuration = vscode.workspace.getConfiguration('latex-workshop', lw.file.toUri(rootFile)) const docker = configuration.get('docker.enabled') const workspaceFolder = vscode.workspace.workspaceFolders?.[0] diff --git a/types/latex-workshop-protocol-types/index.d.ts b/types/latex-workshop-protocol-types/index.d.ts index 8d29e59d7..fc8fa18d1 100644 --- a/types/latex-workshop-protocol-types/index.d.ts +++ b/types/latex-workshop-protocol-types/index.d.ts @@ -14,10 +14,22 @@ type SynctexRangeData = SynctexData & { } export type ServerResponse = { - type: 'refresh' + type: 'refresh', + pdfFileUri: string } | { type: 'synctex', data: SynctexData | SynctexRangeData[] +} | { + type: 'synctex_result', + pdfFile: string, // vsls scheme + synctexData: SynctexData | SynctexRangeData +} | { + type: 'reverse_synctex_result', + input: string, // input file path, in vsls scheme + line: number, + column: number, + textBeforeSelection: string, + textAfterSelection: string } | { type: 'reload' } @@ -82,6 +94,13 @@ export type ClientRequest = { type: 'copy', content: string, isMetaKey: boolean +} | { + type: 'synctex', + line: number, + column: number, + filePath: string, + targetPdfFile: string, + indicator: 'none' | 'circle' | 'rectangle' } export type PanelManagerResponse = { diff --git a/viewer/components/utils.ts b/viewer/components/utils.ts index 577b99d85..f74bc73ce 100644 --- a/viewer/components/utils.ts +++ b/viewer/components/utils.ts @@ -30,7 +30,7 @@ export function parseURL(): { encodedPath: string, pdfFileUri: string, docTitle: for (let i = 0, ii = parts.length; i < ii; ++i) { const param = parts[i].split('=') - if (param[0].toLowerCase() === 'file') { + if (param[0].toLowerCase() === 'file' || param[0].toLowerCase() === 'vsls') { const encodedPath = param[1].replace(pdfFilePrefix, '') const pdfFileUri = decodePath(encodedPath) const docTitle = pdfFileUri.split(/[\\/]/).pop() ?? 'Untitled PDF'