gitlatex 1.5.2__tar.gz → 1.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. {gitlatex-1.5.2 → gitlatex-1.7.0}/PKG-INFO +6 -3
  2. {gitlatex-1.5.2 → gitlatex-1.7.0}/README.md +5 -2
  3. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/__init__.py +1 -1
  4. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/app.js +20 -0
  5. gitlatex-1.7.0/gitlatex/public/css/editor.css +255 -0
  6. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/css/settings.css +122 -0
  7. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/index.html +94 -1
  8. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/js/build/compile.js +7 -8
  9. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/js/editor/monaco.js +37 -25
  10. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/js/editor/session.js +6 -6
  11. gitlatex-1.7.0/gitlatex/public/js/editor/snippets.js +333 -0
  12. gitlatex-1.7.0/gitlatex/public/js/editor/synctex.js +102 -0
  13. gitlatex-1.7.0/gitlatex/public/js/editor/vim.js +122 -0
  14. gitlatex-1.7.0/gitlatex/public/js/editor/vimtex.js +673 -0
  15. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/js/git/actions.js +14 -1
  16. gitlatex-1.7.0/gitlatex/public/js/ui/pdfviewer.js +334 -0
  17. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/js/ui/settings.js +5 -0
  18. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/routes/__init__.py +7 -1
  19. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/routes/git.py +10 -2
  20. gitlatex-1.7.0/gitlatex/routes/snippets.py +22 -0
  21. gitlatex-1.7.0/gitlatex/routes/synctex.py +78 -0
  22. gitlatex-1.7.0/gitlatex/services/snippets.py +90 -0
  23. gitlatex-1.7.0/gitlatex/services/synctex.py +118 -0
  24. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex.egg-info/PKG-INFO +6 -3
  25. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex.egg-info/SOURCES.txt +9 -0
  26. {gitlatex-1.5.2 → gitlatex-1.7.0}/pyproject.toml +1 -1
  27. gitlatex-1.5.2/gitlatex/public/css/editor.css +0 -97
  28. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/__main__.py +0 -0
  29. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/app.py +0 -0
  30. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/http.py +0 -0
  31. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/bib-commands.json +0 -0
  32. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/css/base.css +0 -0
  33. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/css/console.css +0 -0
  34. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/css/docs.css +0 -0
  35. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/css/home.css +0 -0
  36. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/css/layout.css +0 -0
  37. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/css/loading.css +0 -0
  38. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/css/outline.css +0 -0
  39. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/css/problems.css +0 -0
  40. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/css/sidebar.css +0 -0
  41. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/css/support.css +0 -0
  42. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/css/toolbar.css +0 -0
  43. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/css/updates.css +0 -0
  44. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/css/versions.css +0 -0
  45. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/favicon.svg +0 -0
  46. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/js/build/problems.js +0 -0
  47. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/js/core/api.js +0 -0
  48. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/js/core/filetypes.js +0 -0
  49. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/js/core/router.js +0 -0
  50. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/js/core/state.js +0 -0
  51. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/js/core/storage.js +0 -0
  52. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/js/editor/completions.js +0 -0
  53. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/js/editor/envcolors.js +0 -0
  54. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/js/editor/filetree.js +0 -0
  55. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/js/editor/languages.js +0 -0
  56. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/js/editor/mainfile.js +0 -0
  57. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/js/editor/outline.js +0 -0
  58. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/js/editor/projectindex.js +0 -0
  59. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/js/editor/spell.js +0 -0
  60. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/js/git/diffview.js +0 -0
  61. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/js/git/menu.js +0 -0
  62. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/js/git/versions.js +0 -0
  63. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/js/home/repolist.js +0 -0
  64. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/js/ui/consolepane.js +0 -0
  65. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/js/ui/layout.js +0 -0
  66. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/js/ui/loading.js +0 -0
  67. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/js/ui/modals.js +0 -0
  68. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/js/ui/theme.js +0 -0
  69. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/js/ui/viewer.js +0 -0
  70. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/public/latex-commands.json +0 -0
  71. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/routes/compile.py +0 -0
  72. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/routes/files.py +0 -0
  73. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/routes/pages.py +0 -0
  74. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/routes/projectindex.py +0 -0
  75. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/routes/repos.py +0 -0
  76. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/routes/spell.py +0 -0
  77. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/routes/system.py +0 -0
  78. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/server.py +0 -0
  79. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/services/__init__.py +0 -0
  80. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/services/git_backend.py +0 -0
  81. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/services/gitrepo.py +0 -0
  82. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/services/latex.py +0 -0
  83. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/services/paths.py +0 -0
  84. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/services/projectindex.py +0 -0
  85. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/services/spell.py +0 -0
  86. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/services/updates.py +0 -0
  87. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex/state.py +0 -0
  88. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex.egg-info/dependency_links.txt +0 -0
  89. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex.egg-info/entry_points.txt +0 -0
  90. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex.egg-info/requires.txt +0 -0
  91. {gitlatex-1.5.2 → gitlatex-1.7.0}/gitlatex.egg-info/top_level.txt +0 -0
  92. {gitlatex-1.5.2 → gitlatex-1.7.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitlatex
3
- Version: 1.5.2
3
+ Version: 1.7.0
4
4
  Summary: A simple UI for LaTeX projects with Git support. Clone repos, edit, compile, and push.
5
5
  License-Expression: ISC
6
6
  Project-URL: Homepage, https://github.com/abdussamiakanda/gitlatex
@@ -152,8 +152,11 @@ powers VS Code.
152
152
 
153
153
  ### Git and version history
154
154
 
155
- - **Status, diff, pull and push** from the toolbar. Push stages everything,
156
- commits with a timestamp, and pushes in one action.
155
+ - **Status, diff, pull and push** from the toolbar. Push asks for a commit message
156
+ (default: `Update LaTeX project`), then stages everything, commits and pushes.
157
+ Edit the message or accept the default; Cancel or Escape stops the
158
+ operation. Submitting an empty message also cancels. If the working tree is
159
+ clean, existing commits are pushed without creating an empty commit.
157
160
  - Pull and push **enable themselves only when there is something to do**, with
158
161
  ahead/behind counts read from the remote.
159
162
  - **Version history panel** listing your commits, with the files each one touched
@@ -124,8 +124,11 @@ powers VS Code.
124
124
 
125
125
  ### Git and version history
126
126
 
127
- - **Status, diff, pull and push** from the toolbar. Push stages everything,
128
- commits with a timestamp, and pushes in one action.
127
+ - **Status, diff, pull and push** from the toolbar. Push asks for a commit message
128
+ (default: `Update LaTeX project`), then stages everything, commits and pushes.
129
+ Edit the message or accept the default; Cancel or Escape stops the
130
+ operation. Submitting an empty message also cancels. If the working tree is
131
+ clean, existing commits are pushed without creating an empty commit.
129
132
  - Pull and push **enable themselves only when there is something to do**, with
130
133
  ahead/behind counts read from the remote.
131
134
  - **Version history panel** listing your commits, with the files each one touched
@@ -1,2 +1,2 @@
1
1
  """GitLaTeX IDE - LaTeX projects with Git support."""
2
- __version__ = "1.5.2"
2
+ __version__ = "1.7.0"
@@ -15,7 +15,9 @@ import { setCompilerApi, setCompilerApiKey, setLatexEngine, setUseCompilerApi }
15
15
  import { addNewFileSidebar, addNewFolderSidebar, handleSidebarFileInputChange, uploadFilesSidebar } from "./js/editor/filetree.js";
16
16
  import { populateMainFileDropdown } from "./js/editor/mainfile.js";
17
17
  import { toggleOutlineSection } from "./js/editor/outline.js";
18
+ import { closeSnippetForm, exportSnippets, importSnippetsFile, openSnippetForm, submitSnippetForm } from "./js/editor/snippets.js";
18
19
  import { setSpellCheckEnabled } from "./js/editor/spell.js";
20
+ import { setVimEnabled } from "./js/editor/vim.js";
19
21
  import { hideDiffView } from "./js/git/diffview.js";
20
22
  import { closeGitDropdown, toggleGitDropdown } from "./js/git/menu.js";
21
23
  import { pullChanges, pushChanges, showDiff, showStatus } from "./js/git/actions.js";
@@ -23,6 +25,8 @@ import { clearCompareSelection, closeVersionsPanel, loadCommits, loadComparison,
23
25
  import { cloneRepoAndRefresh, closeCloneModal, closeCreateWorkspaceModal, createWorkspaceAndOpen, initRepoListControls, openCloneModal, openCreateWorkspaceModal } from "./js/home/repolist.js";
24
26
  import { showConsoleTab } from "./js/ui/consolepane.js";
25
27
  import { setupResizers, toggleSidebar } from "./js/ui/layout.js";
28
+ import { initPdfViewer, setPdfDoubleClickHandler } from "./js/ui/pdfviewer.js";
29
+ import { syncFromPdf } from "./js/editor/synctex.js";
26
30
  import { checkForUpdate, syncCompilerApiFields } from "./js/ui/settings.js";
27
31
  import { getStoredTheme, setTheme } from "./js/ui/theme.js";
28
32
 
@@ -55,6 +59,8 @@ document.addEventListener("click", function (e) {
55
59
  document.documentElement.setAttribute("data-theme", getStoredTheme());
56
60
  populateMainFileDropdown();
57
61
  initRepoListControls();
62
+ initPdfViewer();
63
+ setPdfDoubleClickHandler(syncFromPdf);
58
64
  document.getElementById("versions-close")?.addEventListener("click", closeVersionsPanel);
59
65
  document.getElementById("versions-refresh")?.addEventListener("click", loadCommits);
60
66
  document.getElementById("diff-view-close")?.addEventListener("click", hideDiffView);
@@ -74,6 +80,20 @@ document.getElementById("settings-use-api")?.addEventListener("change", function
74
80
  document.getElementById("settings-spellcheck")?.addEventListener("change", function () {
75
81
  setSpellCheckEnabled(this.checked);
76
82
  });
83
+ document.getElementById("settings-vim")?.addEventListener("change", function () {
84
+ setVimEnabled(this.checked);
85
+ });
86
+ document.getElementById("snippets-new")?.addEventListener("click", () => openSnippetForm(-1));
87
+ document.getElementById("snippet-cancel")?.addEventListener("click", closeSnippetForm);
88
+ document.getElementById("snippet-save")?.addEventListener("click", submitSnippetForm);
89
+ document.getElementById("snippets-export")?.addEventListener("click", exportSnippets);
90
+ document.getElementById("snippets-import")?.addEventListener("click", function () {
91
+ document.getElementById("snippets-import-input")?.click();
92
+ });
93
+ document.getElementById("snippets-import-input")?.addEventListener("change", function () {
94
+ if (this.files && this.files[0]) importSnippetsFile(this.files[0]);
95
+ this.value = "";
96
+ });
77
97
  document.getElementById("compare-swap")?.addEventListener("click", function () {
78
98
  state.compareOrderSwapped = !state.compareOrderSwapped;
79
99
  renderCompareBar();
@@ -0,0 +1,255 @@
1
+ /*
2
+ * The editor pane and the PDF preview beside it.
3
+ */
4
+
5
+ /* --- Editor pane (contains editor and file viewer) --- */
6
+ #editor-pane {
7
+ flex: 1 1 0;
8
+ min-width: 180px;
9
+ min-height: 0;
10
+ width: 0;
11
+ border-right: 1px solid var(--border-subtle);
12
+ position: relative;
13
+ overflow: hidden;
14
+ }
15
+
16
+ #editor {
17
+ position: absolute;
18
+ inset: 0;
19
+ min-height: 0;
20
+ }
21
+
22
+ #editor-pane.viewer-active #editor {
23
+ visibility: hidden;
24
+ pointer-events: none;
25
+ }
26
+
27
+ #file-viewer {
28
+ position: absolute;
29
+ inset: 0;
30
+ display: flex;
31
+ align-items: center;
32
+ justify-content: center;
33
+ background: var(--bg-app);
34
+ overflow: auto;
35
+ }
36
+
37
+ #file-viewer.hidden {
38
+ display: none !important;
39
+ }
40
+
41
+ .file-viewer-content {
42
+ max-width: 100%;
43
+ max-height: 100%;
44
+ object-fit: contain;
45
+ }
46
+
47
+ #file-viewer-pdf.file-viewer-content {
48
+ width: 100%;
49
+ height: 100%;
50
+ border: none;
51
+ }
52
+
53
+ #file-viewer-pdf.hidden {
54
+ display: none !important;
55
+ }
56
+
57
+ .file-viewer-unavailable {
58
+ color: var(--text-muted, #888);
59
+ font-size: 1rem;
60
+ padding: 1rem;
61
+ }
62
+
63
+ .file-viewer-unavailable.hidden {
64
+ display: none !important;
65
+ }
66
+
67
+ /* Monaco fills the pane; absolute so it stays within #editor below the navbar */
68
+ #editor > .monaco-editor {
69
+ position: absolute !important;
70
+ top: 0 !important;
71
+ right: 0 !important;
72
+ bottom: 0 !important;
73
+ left: 0 !important;
74
+ width: 100% !important;
75
+ height: 100% !important;
76
+ }
77
+
78
+ /* --- PDF pane (drawn by js/ui/pdfviewer.js) --- */
79
+ #pdf {
80
+ width: var(--pdf-width);
81
+ min-width: 200px;
82
+ flex: 0 0 auto;
83
+ height: 100%;
84
+ display: flex;
85
+ flex-direction: column;
86
+ position: relative;
87
+ background: var(--bg-app);
88
+ }
89
+
90
+ .pdf-toolbar {
91
+ display: flex;
92
+ align-items: center;
93
+ gap: 2px;
94
+ padding: 2px 8px;
95
+ border-bottom: 1px solid var(--border-subtle);
96
+ background: var(--bg-elevated);
97
+ flex: 0 0 auto;
98
+ min-width: 0;
99
+ }
100
+
101
+ .pdf-toolbar .icon-btn {
102
+ width: 28px;
103
+ height: 28px;
104
+ }
105
+
106
+ .pdf-zoom-label {
107
+ min-width: 3.2em;
108
+ text-align: center;
109
+ font-size: 0.8rem;
110
+ color: var(--text-secondary);
111
+ font-variant-numeric: tabular-nums;
112
+ }
113
+
114
+ .pdf-toolbar-hint {
115
+ margin-left: auto;
116
+ font-size: 0.75rem;
117
+ color: var(--text-muted);
118
+ white-space: nowrap;
119
+ overflow: hidden;
120
+ text-overflow: ellipsis;
121
+ min-width: 0;
122
+ }
123
+
124
+ #pdf-pages {
125
+ flex: 1 1 0;
126
+ min-height: 0;
127
+ overflow: auto;
128
+ position: relative;
129
+ }
130
+
131
+ #pdf-pages.hidden,
132
+ #pdf-fallback.hidden,
133
+ .pdf-empty.hidden {
134
+ display: none;
135
+ }
136
+
137
+ /* Padding here is PAGE_PADDING in pdfviewer.js. max-content + margin auto
138
+ centres pages that fit and lets zoomed ones scroll sideways. */
139
+ .pdf-pages-inner {
140
+ padding: 12px;
141
+ width: max-content;
142
+ min-width: 100%;
143
+ box-sizing: border-box;
144
+ }
145
+
146
+ .pdf-pages-inner.staging {
147
+ position: absolute;
148
+ top: 0;
149
+ left: 0;
150
+ visibility: hidden;
151
+ pointer-events: none;
152
+ }
153
+
154
+ .pdf-page {
155
+ position: relative;
156
+ margin: 0 auto 12px;
157
+ background: #fff;
158
+ box-shadow: var(--shadow-md);
159
+ user-select: none;
160
+ }
161
+
162
+ .pdf-page canvas {
163
+ display: block;
164
+ }
165
+
166
+ .pdf-sync-mark {
167
+ position: absolute;
168
+ background: rgba(241, 80, 47, 0.28);
169
+ outline: 1px solid rgba(241, 80, 47, 0.7);
170
+ border-radius: 2px;
171
+ pointer-events: none;
172
+ animation: pdf-sync-fade 1.8s ease-out forwards;
173
+ }
174
+
175
+ @keyframes pdf-sync-fade {
176
+ 0%, 60% { opacity: 1; }
177
+ 100% { opacity: 0; }
178
+ }
179
+
180
+ #pdf-fallback {
181
+ flex: 1 1 0;
182
+ min-height: 0;
183
+ width: 100%;
184
+ border: none;
185
+ }
186
+
187
+ .pdf-empty {
188
+ position: absolute;
189
+ inset: 34px 0 0 0;
190
+ display: flex;
191
+ align-items: center;
192
+ justify-content: center;
193
+ color: var(--text-muted);
194
+ font-size: 0.9rem;
195
+ pointer-events: none;
196
+ }
197
+
198
+ /* Editor line flashed after a jump from the PDF */
199
+ .monaco-editor .synctex-line-flash {
200
+ background: rgba(241, 80, 47, 0.22);
201
+ animation: pdf-sync-fade 1.5s ease-out forwards;
202
+ }
203
+
204
+ /* Sidebar hidden: collapse with animation */
205
+ .sidebar-hidden #sidebar {
206
+ width: 0;
207
+ min-width: 0;
208
+ border-right-color: transparent;
209
+ }
210
+
211
+ .sidebar-hidden #editor-pane {
212
+ flex: 1;
213
+ }
214
+
215
+ /* --- Vim mode status bar (js/editor/vim.js) --- */
216
+ .vim-statusbar {
217
+ display: none;
218
+ }
219
+
220
+ #editor-pane.vim-on #editor {
221
+ bottom: 24px;
222
+ }
223
+
224
+ #editor-pane.vim-on .vim-statusbar {
225
+ position: absolute;
226
+ left: 0;
227
+ right: 0;
228
+ bottom: 0;
229
+ height: 24px;
230
+ display: flex;
231
+ align-items: center;
232
+ gap: 12px;
233
+ padding: 0 10px;
234
+ font-family: var(--font-mono);
235
+ font-size: 12px;
236
+ color: var(--text-secondary);
237
+ background: var(--bg-elevated);
238
+ border-top: 1px solid var(--border-subtle);
239
+ box-sizing: border-box;
240
+ overflow: hidden;
241
+ }
242
+
243
+ #editor-pane.viewer-active .vim-statusbar {
244
+ visibility: hidden;
245
+ }
246
+
247
+ .vim-statusbar input {
248
+ flex: 1 1 auto;
249
+ min-width: 0;
250
+ font: inherit;
251
+ color: var(--text-primary);
252
+ background: transparent;
253
+ border: none;
254
+ outline: none;
255
+ }
@@ -393,3 +393,125 @@
393
393
  #settings-test-api.hidden {
394
394
  display: none;
395
395
  }
396
+
397
+ /* --- Settings: Vim help and snippets --- */
398
+ .vimtex-help summary {
399
+ font-size: 12px;
400
+ color: var(--accent);
401
+ cursor: pointer;
402
+ margin-top: 2px;
403
+ }
404
+ .vimtex-table {
405
+ width: 100%;
406
+ margin-top: 8px;
407
+ border-collapse: collapse;
408
+ font-size: 12px;
409
+ color: var(--text-secondary);
410
+ }
411
+ .vimtex-table th,
412
+ .vimtex-table td {
413
+ padding: 4px 0;
414
+ text-align: left;
415
+ vertical-align: top;
416
+ border-top: 1px solid var(--border-subtle);
417
+ }
418
+ .vimtex-table th {
419
+ font-weight: 400;
420
+ white-space: nowrap;
421
+ padding-right: 12px;
422
+ width: 1%;
423
+ }
424
+ .vimtex-table code,
425
+ .snippet-prefix {
426
+ font-family: var(--font-mono);
427
+ font-size: 11px;
428
+ padding: 1px 4px;
429
+ background: var(--bg-hover);
430
+ border-radius: 3px;
431
+ }
432
+
433
+ .snippets-list {
434
+ display: flex;
435
+ flex-direction: column;
436
+ }
437
+ .snippet-row {
438
+ display: flex;
439
+ align-items: center;
440
+ gap: 10px;
441
+ padding: 4px 0;
442
+ border-top: 1px solid var(--border-subtle);
443
+ font-size: 12px;
444
+ }
445
+ .snippet-row:first-child {
446
+ border-top: none;
447
+ }
448
+ .snippet-prefix {
449
+ flex-shrink: 0;
450
+ color: var(--text-primary);
451
+ }
452
+ .snippet-desc {
453
+ flex: 1 1 auto;
454
+ min-width: 0;
455
+ color: var(--text-secondary);
456
+ white-space: nowrap;
457
+ overflow: hidden;
458
+ text-overflow: ellipsis;
459
+ }
460
+ .snippet-scope {
461
+ flex-shrink: 0;
462
+ color: var(--text-muted);
463
+ font-size: 11px;
464
+ }
465
+ .snippet-row .snippet-action {
466
+ width: 26px;
467
+ height: 26px;
468
+ flex-shrink: 0;
469
+ }
470
+ .snippet-row .snippet-action .material-icons {
471
+ font-size: 16px;
472
+ }
473
+ .snippets-empty.hidden,
474
+ .snippet-form.hidden {
475
+ display: none;
476
+ }
477
+
478
+ .snippet-form {
479
+ display: flex;
480
+ flex-direction: column;
481
+ gap: 4px;
482
+ margin-top: 8px;
483
+ padding: 12px;
484
+ border: 1px solid var(--border-subtle);
485
+ border-radius: var(--radius-sm);
486
+ }
487
+ .snippet-form .settings-label {
488
+ margin-top: 6px;
489
+ }
490
+ .snippet-form-row {
491
+ display: flex;
492
+ gap: 12px;
493
+ }
494
+ .snippet-field {
495
+ flex: 1 1 auto;
496
+ display: flex;
497
+ flex-direction: column;
498
+ gap: 4px;
499
+ min-width: 0;
500
+ }
501
+ .snippet-field .settings-label {
502
+ margin-top: 0;
503
+ }
504
+ .snippet-field-scope {
505
+ flex: 0 0 100px;
506
+ }
507
+ .snippet-body {
508
+ font-family: var(--font-mono);
509
+ font-size: 12px;
510
+ line-height: 1.5;
511
+ resize: vertical;
512
+ white-space: pre;
513
+ tab-size: 2;
514
+ }
515
+ .settings-actions .btn-toolbar {
516
+ white-space: nowrap;
517
+ }
@@ -206,6 +206,81 @@
206
206
  Underlines unknown words in .tex, .txt and .md files. Comments, math and
207
207
  command arguments are skipped.
208
208
  </p>
209
+ <label class="settings-toggle" for="settings-vim">
210
+ <span class="settings-toggle-text">Vim keybindings</span>
211
+ <input type="checkbox" id="settings-vim" class="switch-input">
212
+ <span class="switch" aria-hidden="true"></span>
213
+ </label>
214
+ <p class="settings-hint">
215
+ Normal, insert and visual modes with <code>:w</code>, plus VimTeX-style
216
+ mappings in .tex files.
217
+ </p>
218
+ <details class="vimtex-help">
219
+ <summary>VimTeX mappings</summary>
220
+ <table class="vimtex-table">
221
+ <tbody>
222
+ <tr><th><code>ie</code> <code>ae</code></th><td>Inner / around environment</td></tr>
223
+ <tr><th><code>i$</code> <code>a$</code></th><td>Inner / around math</td></tr>
224
+ <tr><th><code>ic</code> <code>ac</code></th><td>Inner / around command</td></tr>
225
+ <tr><th><code>id</code> <code>ad</code></th><td>Inner / around delimiters</td></tr>
226
+ <tr><th><code>iP</code> <code>aP</code></th><td>Inner / around section</td></tr>
227
+ <tr><th><code>]]</code> <code>[[</code></th><td>Next / previous section</td></tr>
228
+ <tr><th><code>]m</code> <code>[m</code> <code>]M</code> <code>[M</code></th><td>Next / previous <code>\begin</code>, <code>\end</code></td></tr>
229
+ <tr><th><code>]n</code> <code>[n</code></th><td>Next / previous math</td></tr>
230
+ <tr><th><code>%</code></th><td>Jump between <code>\begin</code> and <code>\end</code>, or brackets</td></tr>
231
+ <tr><th><code>dse</code> <code>cse</code> <code>tse</code></th><td>Delete / change / toggle-star environment</td></tr>
232
+ <tr><th><code>dsc</code> <code>csc</code> <code>tsc</code></th><td>Delete / change / toggle-star command</td></tr>
233
+ <tr><th><code>ds$</code> <code>cs$</code> <code>ts$</code></th><td>Delete / change math, toggle inline ↔ display</td></tr>
234
+ <tr><th><code>dsd</code> <code>tsd</code></th><td>Delete delimiters, toggle <code>\left</code>/<code>\right</code></td></tr>
235
+ <tr><th><code>tsf</code></th><td>Toggle <code>\frac{a}{b}</code> ↔ <code>a/b</code></td></tr>
236
+ <tr><th><code>]]</code> (insert)</th><td>Close the current environment</td></tr>
237
+ <tr><th><code>\ll</code> <code>\lv</code></th><td>Compile, show cursor in PDF</td></tr>
238
+ <tr><th><code>\lt</code> <code>\le</code></th><td>Toggle outline, show errors</td></tr>
239
+ </tbody>
240
+ </table>
241
+ </details>
242
+ </div>
243
+ <div class="settings-group settings-group-stack">
244
+ <label class="settings-label">Snippets</label>
245
+ <p class="settings-hint">
246
+ Type a snippet's prefix and press <code>Tab</code>, or pick it from the suggestions.
247
+ Bodies use <code>$1</code>, <code>${1:default}</code> and <code>$0</code> for tab stops.
248
+ Tip: select text in the editor and right-click → <em>Save Selection as Snippet</em>.
249
+ </p>
250
+ <div id="snippets-list" class="snippets-list"></div>
251
+ <p id="snippets-empty" class="settings-hint snippets-empty hidden">No snippets yet.</p>
252
+ <div id="snippet-form" class="snippet-form hidden">
253
+ <div class="snippet-form-row">
254
+ <div class="snippet-field">
255
+ <label class="settings-label" for="snippet-prefix">Prefix</label>
256
+ <input type="text" id="snippet-prefix" class="settings-input" placeholder="fig" autocomplete="off" spellcheck="false">
257
+ </div>
258
+ <div class="snippet-field snippet-field-scope">
259
+ <label class="settings-label" for="snippet-scope">Files</label>
260
+ <select id="snippet-scope" class="settings-input">
261
+ <option value="latex">.tex</option>
262
+ <option value="bib">.bib</option>
263
+ <option value="all">All</option>
264
+ </select>
265
+ </div>
266
+ </div>
267
+ <label class="settings-label" for="snippet-description">Description</label>
268
+ <input type="text" id="snippet-description" class="settings-input" placeholder="Figure with caption and label" autocomplete="off">
269
+ <label class="settings-label" for="snippet-body">Body</label>
270
+ <textarea id="snippet-body" class="settings-input snippet-body" rows="7" spellcheck="false"
271
+ placeholder="\begin{figure}[${1:htbp}]&#10; \centering&#10; \includegraphics[width=\linewidth]{$2}&#10; \caption{$3}&#10; \label{fig:$4}&#10;\end{figure}$0"></textarea>
272
+ <div class="settings-actions">
273
+ <button type="button" class="btn-toolbar" id="snippet-cancel">Cancel</button>
274
+ <button type="button" class="btn-toolbar primary" id="snippet-save">Save snippet</button>
275
+ </div>
276
+ </div>
277
+ <div class="settings-actions">
278
+ <span id="snippets-status" class="settings-save-status"></span>
279
+ <button type="button" class="btn-toolbar" id="snippets-import" title="Import a GitLaTeX or VS Code snippets .json file">Import</button>
280
+ <button type="button" class="btn-toolbar" id="snippets-export">Export</button>
281
+ <button type="button" class="btn-toolbar primary" id="snippets-new">New snippet</button>
282
+ <input type="file" id="snippets-import-input" accept=".json,.code-snippets,application/json" hidden>
283
+ </div>
209
284
  </div>
210
285
  <div class="settings-group settings-group-stack settings-tech-info">
211
286
  <label class="settings-label">Technical info</label>
@@ -599,6 +674,7 @@ app.listen(8080, () => console.log("Compiler API on http://localhost:8080/compil
599
674
  <div class="resizer resizer-v" id="resizer-sidebar" title="Resize sidebar"></div>
600
675
  <div id="editor-pane">
601
676
  <div id="editor"></div>
677
+ <div id="vim-statusbar" class="vim-statusbar" aria-live="polite"></div>
602
678
  <div id="file-viewer" class="hidden" aria-hidden="true">
603
679
  <img id="file-viewer-img" alt="" class="file-viewer-content">
604
680
  <iframe id="file-viewer-pdf" class="file-viewer-content hidden" title="PDF viewer"></iframe>
@@ -620,7 +696,24 @@ app.listen(8080, () => console.log("Compiler API on http://localhost:8080/compil
620
696
  </div>
621
697
  </div>
622
698
  <div class="resizer resizer-v" id="resizer-pdf" title="Resize editor / PDF"></div>
623
- <iframe id="pdf"></iframe>
699
+ <div id="pdf">
700
+ <div class="pdf-toolbar">
701
+ <button type="button" class="icon-btn" id="pdf-zoom-out" title="Zoom out" aria-label="Zoom out">
702
+ <span class="material-icons" aria-hidden="true">remove</span>
703
+ </button>
704
+ <span id="pdf-zoom-label" class="pdf-zoom-label">100%</span>
705
+ <button type="button" class="icon-btn" id="pdf-zoom-in" title="Zoom in" aria-label="Zoom in">
706
+ <span class="material-icons" aria-hidden="true">add</span>
707
+ </button>
708
+ <button type="button" class="icon-btn" id="pdf-fit-width" title="Fit width" aria-label="Fit width">
709
+ <span class="material-icons" aria-hidden="true">fit_screen</span>
710
+ </button>
711
+ <span class="pdf-toolbar-hint" title="Cmd/Ctrl+click a line in the editor to find it in the PDF">Double-click the PDF to jump to its source</span>
712
+ </div>
713
+ <div id="pdf-pages"></div>
714
+ <iframe id="pdf-fallback" class="hidden" title="PDF preview"></iframe>
715
+ <div id="pdf-empty" class="pdf-empty">Compile to see the PDF here.</div>
716
+ </div>
624
717
  <div class="resizer resizer-v" id="resizer-versions" title="Resize versions panel"></div>
625
718
  <aside id="versions-panel" class="hidden" aria-hidden="true" aria-label="Version history">
626
719
  <div class="versions-header">
@@ -10,6 +10,7 @@ import { getMainFile } from "../editor/mainfile.js";
10
10
  import { refreshProjectIndex } from "../editor/projectindex.js";
11
11
  import { saveCurrentFile } from "../editor/session.js";
12
12
  import { setConsole } from "../ui/consolepane.js";
13
+ import { pdfUrlFor, showPdf } from "../ui/pdfviewer.js";
13
14
 
14
15
  export function setCompileLoading(loading) {
15
16
  const btn = document.getElementById("btn-compile");
@@ -56,7 +57,7 @@ export async function compile() {
56
57
  if (isDataUrl) {
57
58
  const base64 = pdfStr.includes(",") ? pdfStr.slice(pdfStr.indexOf(",") + 1).trim() : pdfStr;
58
59
  if (!base64) {
59
- document.getElementById("pdf").src = data.pdf;
60
+ showPdf(data.pdf);
60
61
  setConsole("Compiled " + mainFile + " via API.");
61
62
  } else {
62
63
  const savePayload = { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path: pdfPath, content: base64 }) };
@@ -82,21 +83,19 @@ export async function compile() {
82
83
  }
83
84
  try {
84
85
  if (saveRes.ok && saveData.success) {
85
- const base = getApiBase() || "";
86
- const pdfUrl = base + (pdfPath.includes("/") ? "/pdf?path=" + encodeURIComponent(pdfPath) : "/pdf/" + pdfPath);
87
- document.getElementById("pdf").src = pdfUrl;
86
+ showPdf(pdfUrlFor(pdfPath) + (pdfPath.includes("/") ? "&" : "?") + "t=" + Date.now(), pdfPath);
88
87
  setConsole("Compiled " + mainFile + " via API. PDF saved to " + pdfPath);
89
88
  } else {
90
- document.getElementById("pdf").src = data.pdf;
89
+ showPdf(data.pdf);
91
90
  setConsole("Compiled " + mainFile + " via API. Save to repo failed: " + (saveData.error || saveRes.status || "unknown"));
92
91
  }
93
92
  } catch (e) {
94
- document.getElementById("pdf").src = data.pdf;
93
+ showPdf(data.pdf);
95
94
  setConsole("Compiled " + mainFile + " via API. Save to repo failed: " + (e.message || "network error"));
96
95
  }
97
96
  }
98
97
  } else {
99
- document.getElementById("pdf").src = data.pdf;
98
+ showPdf(data.pdf);
100
99
  setConsole("Compiled " + mainFile + " via API.");
101
100
  }
102
101
  } else if (data.error) {
@@ -121,7 +120,7 @@ export async function compile() {
121
120
  applyProblems(data.problems || []);
122
121
  if (data.success && data.pdf) {
123
122
  // Cache-bust so the viewer shows the freshly built PDF.
124
- document.getElementById("pdf").src = (getApiBase() || "") + data.pdf + "?t=" + Date.now();
123
+ showPdf((getApiBase() || "") + data.pdf + "?t=" + Date.now(), mainFile.replace(/\.tex$/i, ".pdf"));
125
124
  renderProblems(data.problems || [], describeBuild(mainFile, data), data.log);
126
125
  } else if (data.error) {
127
126
  renderProblems(data.problems || [], "Compile error: " + data.error, data.log);