gh-here 1.0.2 → 1.0.4

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.
package/public/app.js CHANGED
@@ -1,16 +1,186 @@
1
- document.addEventListener('DOMContentLoaded', function() {
1
+ // Octicons directly embedded for frontend use
2
+ const octicons = {
3
+ 'file-directory': '<svg version="1.1" width="16" height="16" viewBox="0 0 16 16" class="octicon octicon-file-directory" aria-hidden="true"><path d="M0 2.75C0 1.784.784 1 1.75 1H5c.55 0 1.07.26 1.4.7l.9 1.2a.25.25 0 0 0 .2.1h6.75c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0 1 14.25 15H1.75A1.75 1.75 0 0 1 0 13.25Zm1.75-.25a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25v-8.5a.25.25 0 0 0-.25-.25H7.5c-.55 0-1.07-.26-1.4-.7l-.9-1.2a.25.25 0 0 0-.2-.1Z"></path></svg>',
4
+ 'git-commit': '<svg version="1.1" width="16" height="16" viewBox="0 0 16 16" class="octicon octicon-git-commit" aria-hidden="true"><path d="M11.93 8.5a4.002 4.002 0 0 1-7.86 0H.75a.75.75 0 0 1 0-1.5h3.32a4.002 4.002 0 0 1 7.86 0h3.32a.75.75 0 0 1 0 1.5Zm-1.43-.75a2.5 2.5 0 1 0-5 0 2.5 2.5 0 0 0 5 0Z"></path></svg>',
5
+ 'diff': '<svg version="1.1" width="16" height="16" viewBox="0 0 16 16" class="octicon octicon-diff" aria-hidden="true"><path d="M8.75 1.75V5H12a.75.75 0 0 1 0 1.5H8.75v3.25a.75.75 0 0 1-1.5 0V6.5H4A.75.75 0 0 1 4 5h3.25V1.75a.75.75 0 0 1 1.5 0ZM4 13h8a.75.75 0 0 1 0 1.5H4A.75.75 0 0 1 4 13Z"></path></svg>',
6
+ 'copy': '<svg version="1.1" width="16" height="16" viewBox="0 0 16 16" class="octicon octicon-copy" aria-hidden="true"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path></svg>',
7
+ 'download': '<svg version="1.1" width="16" height="16" viewBox="0 0 16 16" class="octicon octicon-download" aria-hidden="true"><path d="M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14Z"></path><path d="M7.25 7.689V2a.75.75 0 0 1 1.5 0v5.689l1.97-1.969a.749.749 0 1 1 1.06 1.06l-3.25 3.25a.749.749 0 0 1-1.06 0L4.22 6.78a.749.749 0 1 1 1.06-1.06l1.97 1.969Z"></path></svg>',
8
+ 'pencil': '<svg version="1.1" width="16" height="16" viewBox="0 0 16 16" class="octicon octicon-pencil" aria-hidden="true"><path d="M11.013 1.427a1.75 1.75 0 0 1 2.474 0l1.086 1.086a1.75 1.75 0 0 1 0 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 0 1-.927-.928l.929-3.25c.081-.286.235-.547.445-.758l8.61-8.61Zm.176 4.823L9.75 4.81l-6.286 6.287a.253.253 0 0 0-.064.108l-.558 1.953 1.953-.558a.253.253 0 0 0 .108-.064Zm1.238-3.763a.25.25 0 0 0-.354 0L10.811 3.75l1.439 1.44 1.263-1.263a.25.25 0 0 0 0-.354Z"></path></svg>',
9
+ 'x': '<svg version="1.1" width="16" height="16" viewBox="0 0 16 16" class="octicon octicon-x" aria-hidden="true"><path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734L9.06 8l3.22 3.22a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L8 9.06l-3.22 3.22a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06Z"></path></svg>',
10
+ 'check': '<svg version="1.1" width="16" height="16" viewBox="0 0 16 16" class="octicon octicon-check" aria-hidden="true"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"></path></svg>',
11
+
12
+ // Helper to get an icon with optional classes
13
+ get(name, options = {}) {
14
+ const svg = this[name];
15
+ if (!svg) return `<span class="missing-icon">[${name}]</span>`;
16
+
17
+ if (options.class) {
18
+ return svg.replace('class="octicon', `class="${options.class} octicon`);
19
+ }
20
+ return svg;
21
+ }
22
+ };
23
+
24
+ // Language detection utility - pure function, easily testable
25
+ function getLanguageFromExtension(filename) {
26
+ const ext = filename.split('.').pop().toLowerCase();
27
+ const languageMap = {
28
+ // JavaScript family
29
+ 'js': 'javascript',
30
+ 'mjs': 'javascript',
31
+ 'jsx': 'javascript',
32
+ 'ts': 'typescript',
33
+ 'tsx': 'typescript',
34
+
35
+ // Web languages
36
+ 'html': 'html',
37
+ 'htm': 'html',
38
+ 'css': 'css',
39
+ 'scss': 'scss',
40
+ 'sass': 'sass',
41
+ 'less': 'less',
42
+
43
+ // Data formats
44
+ 'json': 'json',
45
+ 'xml': 'xml',
46
+ 'yaml': 'yaml',
47
+ 'yml': 'yaml',
48
+
49
+ // Programming languages
50
+ 'py': 'python',
51
+ 'java': 'java',
52
+ 'go': 'go',
53
+ 'rs': 'rust',
54
+ 'php': 'php',
55
+ 'rb': 'ruby',
56
+ 'swift': 'swift',
57
+ 'kt': 'kotlin',
58
+ 'dart': 'dart',
59
+
60
+ // Systems languages
61
+ 'c': 'c',
62
+ 'cpp': 'cpp',
63
+ 'cc': 'cpp',
64
+ 'cxx': 'cpp',
65
+ 'h': 'c',
66
+ 'hpp': 'cpp',
67
+
68
+ // Shell and scripts
69
+ 'sh': 'shell',
70
+ 'bash': 'shell',
71
+ 'zsh': 'shell',
72
+ 'fish': 'shell',
73
+ 'ps1': 'powershell',
74
+
75
+ // Other languages
76
+ 'sql': 'sql',
77
+ 'r': 'r',
78
+ 'scala': 'scala',
79
+ 'clj': 'clojure',
80
+ 'lua': 'lua',
81
+ 'pl': 'perl',
82
+ 'groovy': 'groovy',
83
+
84
+ // Config and text
85
+ 'md': 'markdown',
86
+ 'txt': 'plaintext',
87
+ 'log': 'plaintext'
88
+ };
2
89
 
3
- const themeToggle = document.getElementById('theme-toggle');
4
- const html = document.documentElement;
5
- const searchInput = document.getElementById('file-search');
6
- const fileTable = document.getElementById('file-table');
7
- const fileEditor = document.getElementById('file-editor');
90
+ // Special filename handling
91
+ const basename = filename.toLowerCase();
92
+ if (basename === 'dockerfile' || basename.startsWith('dockerfile.')) return 'dockerfile';
93
+ if (basename === 'makefile') return 'makefile';
94
+ if (basename.startsWith('.env')) return 'dotenv';
95
+ if (basename === 'package.json' || basename === 'composer.json') return 'json';
8
96
 
9
- let currentFocusIndex = -1;
10
- let fileRows = [];
97
+ return languageMap[ext] || 'plaintext';
98
+ }
99
+
100
+ // Draft management utilities - pure functions, easily testable
101
+ const DraftManager = {
102
+ STORAGE_PREFIX: 'gh-here-draft-',
103
+
104
+ saveDraft(filePath, content) {
105
+ localStorage.setItem(`${this.STORAGE_PREFIX}${filePath}`, content);
106
+ },
11
107
 
12
- // Notification system
13
- function showNotification(message, type = 'info') {
108
+ loadDraft(filePath) {
109
+ return localStorage.getItem(`${this.STORAGE_PREFIX}${filePath}`);
110
+ },
111
+
112
+ clearDraft(filePath) {
113
+ localStorage.removeItem(`${this.STORAGE_PREFIX}${filePath}`);
114
+ },
115
+
116
+ // Helper to check if draft exists and differs from content
117
+ hasDraftChanges(filePath, originalContent) {
118
+ const draft = this.loadDraft(filePath);
119
+ return draft !== null && draft !== originalContent;
120
+ },
121
+
122
+ // Get all draft keys for cleanup/debugging
123
+ getAllDrafts() {
124
+ const drafts = {};
125
+ for (let i = 0; i < localStorage.length; i++) {
126
+ const key = localStorage.key(i);
127
+ if (key.startsWith(this.STORAGE_PREFIX)) {
128
+ const filePath = key.replace(this.STORAGE_PREFIX, '');
129
+ drafts[filePath] = localStorage.getItem(key);
130
+ }
131
+ }
132
+ return drafts;
133
+ }
134
+ };
135
+
136
+ // Path utility functions - pure functions, easily testable
137
+ const PathUtils = {
138
+ // Extract current path from URL parameters
139
+ getCurrentPath() {
140
+ const currentUrl = new URL(window.location.href);
141
+ return currentUrl.searchParams.get('path') || '';
142
+ },
143
+
144
+ // Navigate to parent directory
145
+ getParentPath(currentPath) {
146
+ if (!currentPath || currentPath === '') {
147
+ return null; // Already at root
148
+ }
149
+
150
+ const pathParts = currentPath.split('/').filter(p => p);
151
+ if (pathParts.length === 0) {
152
+ return null; // Already at root
153
+ }
154
+
155
+ pathParts.pop();
156
+ return pathParts.join('/');
157
+ },
158
+
159
+ // Build file path from directory and filename
160
+ buildFilePath(currentPath, filename) {
161
+ return currentPath ? `${currentPath}/${filename}` : filename;
162
+ },
163
+
164
+ // Get filename from full path
165
+ getFileName(filePath) {
166
+ return filePath.split('/').pop() || 'file.txt';
167
+ },
168
+
169
+ // Build URL with encoded path parameter
170
+ buildPathUrl(basePath, targetPath) {
171
+ return targetPath ? `${basePath}?path=${encodeURIComponent(targetPath)}` : basePath;
172
+ },
173
+
174
+ // Extract directory path from file path
175
+ getDirectoryPath(filePath) {
176
+ const parts = filePath.split('/').filter(p => p);
177
+ if (parts.length <= 1) return '';
178
+ return parts.slice(0, -1).join('/');
179
+ }
180
+ };
181
+
182
+ // Notification system - global scope for access from commit functions
183
+ function showNotification(message, type = 'info') {
14
184
  // Remove existing notifications
15
185
  const existingNotifications = document.querySelectorAll('.notification');
16
186
  existingNotifications.forEach(n => n.remove());
@@ -28,10 +198,89 @@ document.addEventListener('DOMContentLoaded', function() {
28
198
  setTimeout(() => notification.remove(), 300);
29
199
  }
30
200
  }, 4000);
31
- }
201
+ }
202
+
203
+ document.addEventListener('DOMContentLoaded', function() {
204
+
205
+ const themeToggle = document.getElementById('theme-toggle');
206
+ const html = document.documentElement;
207
+ const searchInput = document.getElementById('file-search');
208
+ const fileTable = document.getElementById('file-table');
209
+
210
+ let currentFocusIndex = -1;
211
+ let fileRows = [];
32
212
 
33
213
  // Removed loading state utilities - not needed for local operations
34
214
 
215
+ // Monaco Editor integration
216
+ let monacoFileEditor = null;
217
+ let monacoNewFileEditor = null;
218
+
219
+ // Note: getLanguageFromExtension is now a global utility function
220
+
221
+ // Initialize Monaco Editor
222
+ function initializeMonaco() {
223
+ if (typeof require !== 'undefined') {
224
+ require.config({ paths: { 'vs': 'https://unpkg.com/monaco-editor@0.45.0/min/vs' }});
225
+
226
+ require(['vs/editor/editor.main'], function () {
227
+ // Configure Monaco Editor to work without web workers (avoids CORS issues)
228
+ self.MonacoEnvironment = {
229
+ getWorker: function(workerId, label) {
230
+ // Return undefined to disable workers and use main thread processing
231
+ // This avoids CORS issues with unpkg CDN while still providing syntax highlighting
232
+ return undefined;
233
+ }
234
+ };
235
+
236
+ // Set Monaco theme based on current theme
237
+ const currentTheme = html.getAttribute('data-theme');
238
+ const monacoTheme = currentTheme === 'dark' ? 'vs-dark' : 'vs';
239
+ monaco.editor.setTheme(monacoTheme);
240
+
241
+ // Initialize new file editor if container exists
242
+ const newFileEditorContainer = document.getElementById('new-file-content');
243
+ if (newFileEditorContainer) {
244
+ monacoNewFileEditor = monaco.editor.create(newFileEditorContainer, {
245
+ value: '',
246
+ language: 'plaintext',
247
+ theme: monacoTheme,
248
+ minimap: { enabled: false },
249
+ lineNumbers: 'on',
250
+ wordWrap: 'off',
251
+ scrollBeyondLastLine: false,
252
+ fontSize: 12,
253
+ lineHeight: 20, // Match view mode breathing
254
+ fontFamily: "'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace",
255
+ padding: { top: 20, bottom: 20, left: 20, right: 20 },
256
+ renderLineHighlight: 'line',
257
+ selectOnLineNumbers: true,
258
+ automaticLayout: true,
259
+ folding: true,
260
+ foldingHighlight: true,
261
+ foldingStrategy: 'auto',
262
+ showFoldingControls: 'mouseover',
263
+ bracketPairColorization: { enabled: true },
264
+ guides: {
265
+ bracketPairs: true,
266
+ indentation: true
267
+ }
268
+ });
269
+ }
270
+
271
+ // Set global flag that Monaco is ready
272
+ window.monacoReady = true;
273
+ });
274
+ }
275
+ }
276
+
277
+ // Initialize Monaco when DOM is ready
278
+ if (document.readyState === 'loading') {
279
+ document.addEventListener('DOMContentLoaded', initializeMonaco);
280
+ } else {
281
+ initializeMonaco();
282
+ }
283
+
35
284
  // Initialize
36
285
  updateFileRows();
37
286
 
@@ -65,6 +314,12 @@ document.addEventListener('DOMContentLoaded', function() {
65
314
  html.setAttribute('data-theme', newTheme);
66
315
  localStorage.setItem('gh-here-theme', newTheme);
67
316
  updateThemeIcon(newTheme);
317
+
318
+ // Update Monaco editor themes
319
+ if (typeof monaco !== 'undefined') {
320
+ const monacoTheme = newTheme === 'dark' ? 'vs-dark' : 'vs';
321
+ monaco.editor.setTheme(monacoTheme);
322
+ }
68
323
  });
69
324
 
70
325
  // Gitignore toggle functionality
@@ -189,6 +444,10 @@ document.addEventListener('DOMContentLoaded', function() {
189
444
  <span class="shortcut-desc">Save file (in editor)</span>
190
445
  <div class="shortcut-keys"><kbd>⌘</kbd> <kbd>S</kbd></div>
191
446
  </div>
447
+ <div class="shortcut-item">
448
+ <span class="shortcut-desc">Toggle word wrap (in editor)</span>
449
+ <div class="shortcut-keys"><kbd>Alt</kbd> <kbd>Z</kbd></div>
450
+ </div>
192
451
  <div class="shortcut-item">
193
452
  <span class="shortcut-desc">Close help/cancel</span>
194
453
  <div class="shortcut-keys"><kbd>Esc</kbd></div>
@@ -217,7 +476,7 @@ document.addEventListener('DOMContentLoaded', function() {
217
476
  // Keyboard navigation
218
477
  document.addEventListener('keydown', function(e) {
219
478
  // Handle help overlay first
220
- if (e.key === '?' && document.activeElement !== searchInput && document.activeElement !== fileEditor) {
479
+ if (e.key === '?' && document.activeElement !== searchInput) {
221
480
  e.preventDefault();
222
481
  showKeyboardHelp();
223
482
  return;
@@ -235,7 +494,7 @@ document.addEventListener('DOMContentLoaded', function() {
235
494
  // Don't handle shortcuts when editor is active
236
495
  const editorContainer = document.getElementById('editor-container');
237
496
  if (editorContainer && editorContainer.style.display !== 'none' &&
238
- (document.activeElement === fileEditor || editorContainer.contains(document.activeElement))) {
497
+ editorContainer.contains(document.activeElement)) {
239
498
  return;
240
499
  }
241
500
 
@@ -252,12 +511,10 @@ document.addEventListener('DOMContentLoaded', function() {
252
511
 
253
512
  if (newFileBtn) {
254
513
  newFileBtn.addEventListener('click', function() {
255
- const currentUrl = new URL(window.location.href);
256
- const currentPath = currentUrl.searchParams.get('path') || '';
514
+ const currentPath = PathUtils.getCurrentPath();
257
515
 
258
516
  // Navigate to new file creation mode
259
- const newFileUrl = `/new?path=${encodeURIComponent(currentPath)}`;
260
- window.location.href = newFileUrl;
517
+ window.location.href = PathUtils.buildPathUrl('/new', currentPath);
261
518
  });
262
519
  }
263
520
 
@@ -497,7 +754,7 @@ document.addEventListener('DOMContentLoaded', function() {
497
754
  if (rowType === 'file') {
498
755
  const filePath = focusedRow.dataset.path;
499
756
  // Navigate to the file and trigger edit mode
500
- window.location.href = `/?path=${encodeURIComponent(filePath)}#edit`;
757
+ window.location.href = PathUtils.buildPathUrl('/', filePath) + '#edit';
501
758
  } else {
502
759
  // If we're on a file page, use the edit button
503
760
  const editBtn = document.getElementById('edit-btn');
@@ -584,24 +841,15 @@ document.addEventListener('DOMContentLoaded', function() {
584
841
  }
585
842
 
586
843
  function goUpDirectory() {
587
- const currentUrl = new URL(window.location.href);
588
- const currentPath = currentUrl.searchParams.get('path');
844
+ const currentPath = PathUtils.getCurrentPath();
845
+ const newPath = PathUtils.getParentPath(currentPath);
589
846
 
590
- if (!currentPath || currentPath === '') {
847
+ if (newPath === null) {
591
848
  // Already at root, do nothing
592
849
  return;
593
850
  }
594
851
 
595
- const pathParts = currentPath.split('/').filter(p => p);
596
- if (pathParts.length === 0) {
597
- // Go to root
598
- window.location.href = '/';
599
- } else {
600
- // Go up one directory
601
- pathParts.pop();
602
- const newPath = pathParts.join('/');
603
- window.location.href = `/?path=${encodeURIComponent(newPath)}`;
604
- }
852
+ window.location.href = PathUtils.buildPathUrl('/', newPath);
605
853
  }
606
854
 
607
855
 
@@ -701,37 +949,10 @@ document.addEventListener('DOMContentLoaded', function() {
701
949
  const editorContainer = document.getElementById('editor-container');
702
950
  const saveBtn = document.getElementById('save-btn');
703
951
  const cancelBtn = document.getElementById('cancel-btn');
952
+ const wordWrapBtn = document.getElementById('word-wrap-btn');
704
953
  const fileContent = document.querySelector('.file-content');
705
954
 
706
- // Line numbers functionality
707
- function updateLineNumbers(textarea, lineNumbersDiv) {
708
- if (!textarea || !lineNumbersDiv) return;
709
-
710
- const lines = textarea.value.split('\n');
711
- const lineNumbers = lines.map((_, index) => index + 1).join('\n');
712
- lineNumbersDiv.textContent = lineNumbers || '1';
713
- }
714
-
715
- // Initialize and handle line numbers for both editors
716
- const editorLineNumbers = document.getElementById('editor-line-numbers');
717
- const newFileContent = document.getElementById('new-file-content');
718
- const newFileLineNumbers = document.getElementById('new-file-line-numbers');
719
-
720
- // Get fileEditor reference (declared earlier in the file)
721
- if (document.getElementById('file-editor') && editorLineNumbers) {
722
- const fileEditorElement = document.getElementById('file-editor');
723
- fileEditorElement.addEventListener('input', () => updateLineNumbers(fileEditorElement, editorLineNumbers));
724
- fileEditorElement.addEventListener('scroll', () => {
725
- editorLineNumbers.scrollTop = fileEditorElement.scrollTop;
726
- });
727
- }
728
-
729
- if (newFileContent && newFileLineNumbers) {
730
- newFileContent.addEventListener('input', () => updateLineNumbers(newFileContent, newFileLineNumbers));
731
- newFileContent.addEventListener('scroll', () => {
732
- newFileLineNumbers.scrollTop = newFileContent.scrollTop;
733
- });
734
- }
955
+
735
956
 
736
957
  // Auto-open editor if hash is #edit
737
958
  if (window.location.hash === '#edit' && editBtn) {
@@ -740,8 +961,30 @@ document.addEventListener('DOMContentLoaded', function() {
740
961
  setTimeout(() => editBtn.click(), 100);
741
962
  }
742
963
 
743
- if (editBtn && editorContainer && fileEditor) {
964
+ if (editBtn && editorContainer) {
744
965
  let originalContent = '';
966
+ let wordWrapEnabled = false; // Default to disabled
967
+
968
+ // Word wrap toggle functionality
969
+ function toggleWordWrap() {
970
+ if (monacoFileEditor) {
971
+ wordWrapEnabled = !wordWrapEnabled;
972
+ monacoFileEditor.updateOptions({ wordWrap: wordWrapEnabled ? 'on' : 'off' });
973
+
974
+ // Update button appearance
975
+ if (wordWrapBtn) {
976
+ wordWrapBtn.classList.toggle('btn-primary', wordWrapEnabled);
977
+ wordWrapBtn.classList.toggle('btn-secondary', !wordWrapEnabled);
978
+ wordWrapBtn.textContent = wordWrapEnabled ? '↩ Wrap ON' : '↩ Wrap OFF';
979
+ wordWrapBtn.title = `Toggle word wrap (Alt+Z) - ${wordWrapEnabled ? 'ON' : 'OFF'}`;
980
+ }
981
+ }
982
+ }
983
+
984
+ // Word wrap button click
985
+ if (wordWrapBtn) {
986
+ wordWrapBtn.addEventListener('click', toggleWordWrap);
987
+ }
745
988
 
746
989
  // Editor keyboard shortcuts
747
990
  document.addEventListener('keydown', function(e) {
@@ -751,6 +994,11 @@ document.addEventListener('DOMContentLoaded', function() {
751
994
  e.preventDefault();
752
995
  saveBtn.click();
753
996
  }
997
+ // Alt+Z to toggle word wrap
998
+ if (e.altKey && e.key === 'z') {
999
+ e.preventDefault();
1000
+ toggleWordWrap();
1001
+ }
754
1002
  // Escape to cancel
755
1003
  if (e.key === 'Escape') {
756
1004
  e.preventDefault();
@@ -759,73 +1007,177 @@ document.addEventListener('DOMContentLoaded', function() {
759
1007
  }
760
1008
  });
761
1009
 
762
- // Auto-save functionality
763
- function saveDraft(filePath, content) {
764
- localStorage.setItem(`gh-here-draft-${filePath}`, content);
765
- }
1010
+ // Note: Draft management functions are now in global DraftManager object
766
1011
 
767
- function loadDraft(filePath) {
768
- return localStorage.getItem(`gh-here-draft-${filePath}`);
769
- }
770
-
771
- function clearDraft(filePath) {
772
- localStorage.removeItem(`gh-here-draft-${filePath}`);
1012
+ // Show draft dialog with Load/Discard/Cancel options
1013
+ function showDraftDialog(filePath) {
1014
+ return new Promise((resolve) => {
1015
+ const modal = document.createElement('div');
1016
+ modal.className = 'modal-overlay';
1017
+ modal.innerHTML = `
1018
+ <div class="modal-content draft-modal">
1019
+ <h3>Unsaved Changes Found</h3>
1020
+ <p>You have unsaved changes for this file. What would you like to do?</p>
1021
+ <div class="draft-actions">
1022
+ <button class="btn btn-primary" data-action="load">Load Draft</button>
1023
+ <button class="btn btn-secondary" data-action="discard">Discard Draft</button>
1024
+ <button class="btn btn-secondary" data-action="cancel">Cancel</button>
1025
+ </div>
1026
+ </div>
1027
+ `;
1028
+
1029
+ modal.addEventListener('click', (e) => {
1030
+ if (e.target.matches('[data-action]') || e.target === modal) {
1031
+ const action = e.target.dataset?.action || 'cancel';
1032
+ modal.remove();
1033
+ resolve(action);
1034
+ }
1035
+ });
1036
+
1037
+ document.body.appendChild(modal);
1038
+ });
773
1039
  }
774
1040
 
775
- editBtn.addEventListener('click', function() {
1041
+ editBtn.addEventListener('click', async function() {
776
1042
  // Get current file path
777
1043
  const currentUrl = new URL(window.location.href);
778
1044
  const filePath = currentUrl.searchParams.get('path') || '';
779
1045
 
780
1046
 
781
1047
  // Fetch original file content
782
- fetch(`/api/file-content?path=${encodeURIComponent(filePath)}`)
783
- .then(response => {
784
- if (!response.ok) {
785
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1048
+ try {
1049
+ const response = await fetch(`/api/file-content?path=${encodeURIComponent(filePath)}`);
1050
+ if (!response.ok) {
1051
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1052
+ }
1053
+ const content = await response.text();
1054
+ originalContent = content;
1055
+
1056
+ // Check for draft
1057
+ const draft = DraftManager.loadDraft(filePath);
1058
+ let contentToLoad = content;
1059
+
1060
+ if (DraftManager.hasDraftChanges(filePath, content)) {
1061
+ // Show custom draft dialog with 3 options
1062
+ const draftChoice = await showDraftDialog(filePath);
1063
+ if (draftChoice === 'load') {
1064
+ contentToLoad = draft;
1065
+ } else if (draftChoice === 'discard') {
1066
+ DraftManager.clearDraft(filePath);
1067
+ contentToLoad = content;
1068
+ } else {
1069
+ // User cancelled, use original content
1070
+ contentToLoad = content;
786
1071
  }
787
- return response.text();
788
- })
789
- .then(content => {
790
- originalContent = content;
791
-
792
- // Check for draft
793
- const draft = loadDraft(filePath);
794
- if (draft && draft !== content) {
795
- if (confirm('You have unsaved changes for this file. Load draft?')) {
796
- fileEditor.value = draft;
797
- } else {
798
- fileEditor.value = content;
799
- clearDraft(filePath);
1072
+ }
1073
+
1074
+ if (draft && draft === content) {
1075
+ DraftManager.clearDraft(filePath);
1076
+ }
1077
+
1078
+ // Create Monaco editor if it doesn't exist
1079
+ if (!monacoFileEditor && window.monacoReady) {
1080
+ const fileEditorContainer = document.getElementById('file-editor');
1081
+ if (fileEditorContainer) {
1082
+ // Get filename reliably from the current URL path parameter
1083
+ const currentUrl = new URL(window.location.href);
1084
+ const filePath = currentUrl.searchParams.get('path') || '';
1085
+ const filename = PathUtils.getFileName(filePath);
1086
+
1087
+ const language = getLanguageFromExtension(filename);
1088
+
1089
+ // Validate language exists in Monaco
1090
+ const availableLanguages = monaco.languages.getLanguages().map(lang => lang.id);
1091
+ const validLanguage = availableLanguages.includes(language) ? language : 'plaintext';
1092
+
1093
+ const currentTheme = html.getAttribute('data-theme');
1094
+ const monacoTheme = currentTheme === 'dark' ? 'vs-dark' : 'vs';
1095
+
1096
+ // Ensure Monaco is fully ready before creating editor
1097
+ const createEditorWhenReady = () => {
1098
+ if (monaco && monaco.editor && monaco.languages) {
1099
+ monacoFileEditor = monaco.editor.create(fileEditorContainer, {
1100
+ value: '',
1101
+ language: validLanguage,
1102
+ theme: monacoTheme,
1103
+ minimap: { enabled: false },
1104
+ lineNumbers: 'on',
1105
+ wordWrap: 'off',
1106
+ scrollBeyondLastLine: false,
1107
+ fontSize: 12,
1108
+ lineHeight: 20, // Match view mode (12px * 1.5 = 18, but Monaco uses pixels)
1109
+ fontFamily: "'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace",
1110
+ padding: { top: 20, bottom: 20, left: 20, right: 20 },
1111
+ renderLineHighlight: 'line',
1112
+ selectOnLineNumbers: true,
1113
+ automaticLayout: true,
1114
+ folding: true,
1115
+ foldingHighlight: true,
1116
+ foldingStrategy: 'auto',
1117
+ showFoldingControls: 'mouseover',
1118
+ bracketPairColorization: { enabled: true },
1119
+ guides: {
1120
+ bracketPairs: true,
1121
+ indentation: true
1122
+ }
1123
+ });
1124
+ } else {
1125
+ // Monaco not fully ready yet, retry
1126
+ setTimeout(createEditorWhenReady, 10);
1127
+ }
1128
+ };
1129
+
1130
+ // Start the ready check
1131
+ createEditorWhenReady();
800
1132
  }
801
- } else {
802
- fileEditor.value = content;
803
1133
  }
804
1134
 
805
- fileContent.style.display = 'none';
806
- editorContainer.style.display = 'block';
807
- fileEditor.focus();
808
-
809
- // Set up auto-save and update line numbers
810
- fileEditor.addEventListener('input', function() {
811
- saveDraft(filePath, fileEditor.value);
812
- });
813
-
814
- // Update line numbers for loaded content
815
- updateLineNumbers(fileEditor, editorLineNumbers);
816
- })
817
- .catch(error => {
818
- console.error('Error fetching file content:', error);
819
- let errorMessage = 'Failed to load file content for editing';
820
- if (error.message.includes('HTTP 403')) {
821
- errorMessage = 'Access denied: Cannot read this file';
822
- } else if (error.message.includes('HTTP 404')) {
823
- errorMessage = 'File not found';
824
- } else if (error.message.includes('NetworkError') || error.message.includes('Failed to fetch')) {
825
- errorMessage = 'Network error: Please check your connection';
826
- }
827
- showNotification(errorMessage, 'error');
828
- });
1135
+ // Set content in Monaco editor - wait for Monaco to be ready
1136
+ const setContentWhenReady = () => {
1137
+ if (monacoFileEditor && window.monacoReady) {
1138
+ // Make sure the editor is visible and properly sized
1139
+ monacoFileEditor.layout();
1140
+ monacoFileEditor.setValue(contentToLoad);
1141
+
1142
+ // Force another layout after a brief delay to ensure proper sizing
1143
+ setTimeout(() => {
1144
+ monacoFileEditor.layout();
1145
+ }, 50);
1146
+
1147
+ // Language is already set during editor creation, no need to set it again
1148
+
1149
+ // Set up auto-save
1150
+ monacoFileEditor.onDidChangeModelContent(() => {
1151
+ DraftManager.saveDraft(filePath, monacoFileEditor.getValue());
1152
+ });
1153
+ } else {
1154
+ // Monaco not ready yet, wait and try again
1155
+ setTimeout(setContentWhenReady, 100);
1156
+ }
1157
+ };
1158
+ setContentWhenReady();
1159
+
1160
+ // Show editor UI
1161
+ fileContent.style.display = 'none';
1162
+ editorContainer.style.display = 'block';
1163
+
1164
+ // Focus Monaco editor
1165
+ if (monacoFileEditor) {
1166
+ monacoFileEditor.focus();
1167
+ }
1168
+
1169
+ } catch (error) {
1170
+ console.error('Error fetching file content:', error);
1171
+ let errorMessage = 'Failed to load file content for editing';
1172
+ if (error.message.includes('HTTP 403')) {
1173
+ errorMessage = 'Access denied: Cannot read this file';
1174
+ } else if (error.message.includes('HTTP 404')) {
1175
+ errorMessage = 'File not found';
1176
+ } else if (error.message.includes('NetworkError') || error.message.includes('Failed to fetch')) {
1177
+ errorMessage = 'Network error: Please check your connection';
1178
+ }
1179
+ showNotification(errorMessage, 'error');
1180
+ }
829
1181
  });
830
1182
 
831
1183
  cancelBtn.addEventListener('click', function() {
@@ -845,7 +1197,7 @@ document.addEventListener('DOMContentLoaded', function() {
845
1197
  },
846
1198
  body: JSON.stringify({
847
1199
  path: filePath,
848
- content: fileEditor.value
1200
+ content: monacoFileEditor ? monacoFileEditor.getValue() : ''
849
1201
  })
850
1202
  })
851
1203
  .then(response => {
@@ -857,10 +1209,9 @@ document.addEventListener('DOMContentLoaded', function() {
857
1209
  .then(data => {
858
1210
  if (data.success) {
859
1211
  // Clear draft on successful save
860
- clearDraft(filePath);
861
- showNotification('File saved successfully', 'success');
1212
+ DraftManager.clearDraft(filePath);
862
1213
  // Refresh the page to show updated content
863
- setTimeout(() => window.location.reload(), 800);
1214
+ window.location.reload();
864
1215
  } else {
865
1216
  showNotification('Failed to save file: ' + data.error, 'error');
866
1217
  }
@@ -888,7 +1239,7 @@ document.addEventListener('DOMContentLoaded', function() {
888
1239
  const button = e.target.closest('.edit-file-btn');
889
1240
  const filePath = button.dataset.path;
890
1241
  // Navigate to the file and trigger edit mode
891
- window.location.href = `/?path=${encodeURIComponent(filePath)}#edit`;
1242
+ window.location.href = PathUtils.buildPathUrl('/', filePath) + '#edit';
892
1243
  }
893
1244
 
894
1245
  // Git diff viewer functionality
@@ -1194,13 +1545,27 @@ document.addEventListener('DOMContentLoaded', function() {
1194
1545
 
1195
1546
  // New file interface functionality
1196
1547
  const newFilenameInput = document.getElementById('new-filename-input');
1548
+
1549
+ // Update Monaco language when filename changes
1550
+ if (newFilenameInput) {
1551
+ newFilenameInput.addEventListener('input', function() {
1552
+ const filename = this.value.trim();
1553
+ if (monacoNewFileEditor && filename) {
1554
+ const language = getLanguageFromExtension(filename);
1555
+ const model = monacoNewFileEditor.getModel();
1556
+ if (model) {
1557
+ monaco.editor.setModelLanguage(model, language);
1558
+ }
1559
+ }
1560
+ });
1561
+ }
1197
1562
  const createNewFileBtn = document.getElementById('create-new-file');
1198
1563
  const cancelNewFileBtn = document.getElementById('cancel-new-file');
1199
1564
 
1200
1565
  if (createNewFileBtn) {
1201
1566
  createNewFileBtn.addEventListener('click', function() {
1202
1567
  const filename = newFilenameInput.value.trim();
1203
- const content = newFileContent.value;
1568
+ const content = monacoNewFileEditor ? monacoNewFileEditor.getValue() : '';
1204
1569
 
1205
1570
  if (!filename) {
1206
1571
  showNotification('Please enter a filename', 'error');
@@ -1209,8 +1574,7 @@ document.addEventListener('DOMContentLoaded', function() {
1209
1574
  }
1210
1575
 
1211
1576
 
1212
- const currentUrl = new URL(window.location.href);
1213
- const currentPath = currentUrl.searchParams.get('path') || '';
1577
+ const currentPath = PathUtils.getCurrentPath();
1214
1578
 
1215
1579
  fetch('/api/create-file', {
1216
1580
  method: 'POST',
@@ -1232,7 +1596,7 @@ document.addEventListener('DOMContentLoaded', function() {
1232
1596
  if (data.success) {
1233
1597
  // If there's content, save it
1234
1598
  if (content.trim()) {
1235
- const filePath = currentPath ? `${currentPath}/${filename}` : filename;
1599
+ const filePath = PathUtils.buildFilePath(currentPath, filename);
1236
1600
  return fetch('/api/save-file', {
1237
1601
  method: 'POST',
1238
1602
  headers: {
@@ -1254,7 +1618,7 @@ document.addEventListener('DOMContentLoaded', function() {
1254
1618
  if (data.success) {
1255
1619
  showNotification(`File "${filename}" created successfully`, 'success');
1256
1620
  // Navigate back to the directory or to the new file
1257
- const redirectPath = currentPath ? `/?path=${encodeURIComponent(currentPath)}` : '/';
1621
+ const redirectPath = PathUtils.buildPathUrl('/', currentPath);
1258
1622
  setTimeout(() => window.location.href = redirectPath, 800);
1259
1623
  } else {
1260
1624
  throw new Error(data.error);
@@ -1277,9 +1641,8 @@ document.addEventListener('DOMContentLoaded', function() {
1277
1641
 
1278
1642
  if (cancelNewFileBtn) {
1279
1643
  cancelNewFileBtn.addEventListener('click', function() {
1280
- const currentUrl = new URL(window.location.href);
1281
- const currentPath = currentUrl.searchParams.get('path') || '';
1282
- const redirectPath = currentPath ? `/?path=${encodeURIComponent(currentPath)}` : '/';
1644
+ const currentPath = PathUtils.getCurrentPath();
1645
+ const redirectPath = PathUtils.buildPathUrl('/', currentPath);
1283
1646
  window.location.href = redirectPath;
1284
1647
  });
1285
1648
  }
@@ -1330,4 +1693,204 @@ document.addEventListener('DOMContentLoaded', function() {
1330
1693
  document.body.removeChild(textArea);
1331
1694
  }
1332
1695
  }
1333
- });
1696
+ });
1697
+
1698
+ // Simple commit modal functionality
1699
+ document.addEventListener('click', (e) => {
1700
+ if (e.target.matches('#commit-btn') || e.target.closest('#commit-btn')) {
1701
+ e.preventDefault();
1702
+ showCommitModal();
1703
+ }
1704
+ });
1705
+
1706
+ async function showCommitModal() {
1707
+ try {
1708
+ // Get current path from URL
1709
+ const urlParams = new URLSearchParams(window.location.search);
1710
+ const currentPath = urlParams.get('path') || '';
1711
+
1712
+ // Fetch git changes from current directory and subdirectories only
1713
+ const response = await fetch(`/api/git-status?currentPath=${encodeURIComponent(currentPath)}`);
1714
+ const data = await response.json();
1715
+
1716
+ if (!data.success) {
1717
+ showNotification('❌ Failed to load git changes', 'error');
1718
+ return;
1719
+ }
1720
+
1721
+ const changedFiles = data.changes;
1722
+
1723
+ if (changedFiles.length === 0) {
1724
+ showNotification('ℹ️ No changes to commit', 'info');
1725
+ return;
1726
+ }
1727
+
1728
+ showCommitModalWithFiles(changedFiles);
1729
+ } catch (error) {
1730
+ console.error('Error fetching git status:', error);
1731
+ showNotification('❌ Failed to load git changes', 'error');
1732
+ }
1733
+ }
1734
+
1735
+ function groupFilesByDirectory(files) {
1736
+ const groups = new Map();
1737
+
1738
+ files.forEach(file => {
1739
+ const parts = file.name.split('/');
1740
+ if (parts.length === 1) {
1741
+ // Root level file
1742
+ if (!groups.has('')) {
1743
+ groups.set('', { directory: null, files: [] });
1744
+ }
1745
+ groups.get('').files.push(file);
1746
+ } else {
1747
+ // File in subdirectory
1748
+ const directory = PathUtils.getDirectoryPath(file.name);
1749
+ if (!groups.has(directory)) {
1750
+ groups.set(directory, { directory, files: [] });
1751
+ }
1752
+ groups.get(directory).files.push(file);
1753
+ }
1754
+ });
1755
+
1756
+ // Convert to array and sort
1757
+ const result = Array.from(groups.values());
1758
+ result.sort((a, b) => {
1759
+ if (!a.directory && b.directory) return -1; // Root files first
1760
+ if (a.directory && !b.directory) return 1;
1761
+ if (!a.directory && !b.directory) return 0;
1762
+ return a.directory.localeCompare(b.directory);
1763
+ });
1764
+
1765
+ return result;
1766
+ }
1767
+
1768
+ async function showCommitModalWithFiles(changedFiles) {
1769
+ // Group files by directory for better display
1770
+ const groupedFiles = groupFilesByDirectory(changedFiles);
1771
+
1772
+ // Get octicons for the modal
1773
+ const folderIcon = octicons.get('file-directory', { class: 'folder-icon' });
1774
+
1775
+ // Create modal
1776
+ const modal = document.createElement('div');
1777
+ modal.className = 'commit-modal-overlay';
1778
+ modal.innerHTML = `
1779
+ <div class="commit-modal">
1780
+ <div class="commit-modal-header">
1781
+ <h3>Commit Changes</h3>
1782
+ <button class="modal-close">&times;</button>
1783
+ </div>
1784
+ <div class="commit-modal-body">
1785
+ <div class="changed-files">
1786
+ <h4>Changed Files (${changedFiles.length})</h4>
1787
+ <ul class="file-list">
1788
+ ${groupedFiles.map(group => `
1789
+ ${group.directory ? `<li class="directory-group"><strong>${folderIcon} ${group.directory}/</strong></li>` : ''}
1790
+ ${group.files.map(file => `
1791
+ <li class="file-item ${group.directory ? 'indented' : ''}">
1792
+ <label class="file-checkbox-label">
1793
+ <input type="checkbox" class="file-checkbox" data-file="${file.name}" checked>
1794
+ <span class="file-status">${file.status}</span>
1795
+ <span class="file-name">${group.directory ? file.name.split('/').pop() : file.name}</span>
1796
+ </label>
1797
+ </li>
1798
+ `).join('')}
1799
+ `).join('')}
1800
+ </ul>
1801
+ </div>
1802
+ <div class="commit-message-section">
1803
+ <textarea id="modal-commit-message" placeholder="Enter commit message..." rows="4"></textarea>
1804
+ </div>
1805
+ </div>
1806
+ <div class="commit-modal-footer">
1807
+ <button class="btn-cancel">Cancel</button>
1808
+ <button class="btn-commit" disabled>Commit Changes</button>
1809
+ </div>
1810
+ </div>
1811
+ `;
1812
+
1813
+ document.body.appendChild(modal);
1814
+
1815
+ // Handle modal interactions
1816
+ const messageInput = modal.querySelector('#modal-commit-message');
1817
+ const commitBtn = modal.querySelector('.btn-commit');
1818
+ const cancelBtn = modal.querySelector('.btn-cancel');
1819
+ const closeBtn = modal.querySelector('.modal-close');
1820
+
1821
+ // Update commit button based on message and selected files
1822
+ const updateCommitButton = () => {
1823
+ const hasMessage = messageInput.value.trim();
1824
+ const selectedFiles = modal.querySelectorAll('.file-checkbox:checked').length;
1825
+ commitBtn.disabled = !hasMessage || selectedFiles === 0;
1826
+ commitBtn.textContent = selectedFiles > 0
1827
+ ? `Commit ${selectedFiles} file${selectedFiles === 1 ? '' : 's'}`
1828
+ : 'No files selected';
1829
+ };
1830
+
1831
+ messageInput.addEventListener('input', updateCommitButton);
1832
+
1833
+ // Handle checkbox changes
1834
+ modal.querySelectorAll('.file-checkbox').forEach(checkbox => {
1835
+ checkbox.addEventListener('change', updateCommitButton);
1836
+ });
1837
+
1838
+ // Initial button state
1839
+ updateCommitButton();
1840
+
1841
+ // Close modal handlers
1842
+ const closeModal = () => {
1843
+ document.body.removeChild(modal);
1844
+ };
1845
+
1846
+ cancelBtn.addEventListener('click', closeModal);
1847
+ closeBtn.addEventListener('click', closeModal);
1848
+ modal.addEventListener('click', (e) => {
1849
+ if (e.target === modal) closeModal();
1850
+ });
1851
+
1852
+ // Prevent keyboard shortcuts from interfering with the modal
1853
+ modal.addEventListener('keydown', (e) => {
1854
+ e.stopPropagation();
1855
+ });
1856
+
1857
+ // Commit handler
1858
+ commitBtn.addEventListener('click', async () => {
1859
+ const message = messageInput.value.trim();
1860
+ const selectedCheckboxes = modal.querySelectorAll('.file-checkbox:checked');
1861
+ const selectedFiles = Array.from(selectedCheckboxes).map(cb => cb.dataset.file);
1862
+
1863
+ if (!message || selectedFiles.length === 0) return;
1864
+
1865
+ const originalText = commitBtn.textContent;
1866
+ commitBtn.textContent = 'Committing...';
1867
+ commitBtn.disabled = true;
1868
+
1869
+ try {
1870
+ // Commit selected files
1871
+ const response = await fetch('/api/git-commit-selected', {
1872
+ method: 'POST',
1873
+ headers: { 'Content-Type': 'application/json' },
1874
+ body: JSON.stringify({ message, files: selectedFiles })
1875
+ });
1876
+
1877
+ if (response.ok) {
1878
+ showNotification(`✅ Successfully committed ${selectedFiles.length} file${selectedFiles.length === 1 ? '' : 's'}!`, 'success');
1879
+ closeModal();
1880
+ setTimeout(() => location.reload(), 1000);
1881
+ } else {
1882
+ const error = await response.text();
1883
+ showNotification(`❌ Commit failed: ${error}`, 'error');
1884
+ commitBtn.textContent = originalText;
1885
+ commitBtn.disabled = false;
1886
+ }
1887
+ } catch (err) {
1888
+ showNotification('❌ Commit failed', 'error');
1889
+ commitBtn.textContent = originalText;
1890
+ commitBtn.disabled = false;
1891
+ }
1892
+ });
1893
+
1894
+ // Focus the message input
1895
+ messageInput.focus();
1896
+ }