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/.claude/settings.local.json +15 -10
- package/README.md +6 -3
- package/bin/gh-here.js +8 -1255
- package/lib/file-utils.js +264 -0
- package/lib/git.js +207 -0
- package/lib/gitignore.js +91 -0
- package/lib/renderers.js +569 -0
- package/lib/server.js +391 -0
- package/package.json +1 -1
- package/public/app.js +692 -129
- package/public/styles.css +414 -44
- package/tests/draftManager.test.js +241 -0
- package/tests/httpService.test.js +268 -0
- package/tests/languageDetection.test.js +145 -0
- package/tests/pathUtils.test.js +136 -0
package/public/app.js
CHANGED
|
@@ -1,16 +1,186 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
256
|
-
const currentPath = currentUrl.searchParams.get('path') || '';
|
|
514
|
+
const currentPath = PathUtils.getCurrentPath();
|
|
257
515
|
|
|
258
516
|
// Navigate to new file creation mode
|
|
259
|
-
|
|
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 =
|
|
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
|
|
588
|
-
const
|
|
844
|
+
const currentPath = PathUtils.getCurrentPath();
|
|
845
|
+
const newPath = PathUtils.getParentPath(currentPath);
|
|
589
846
|
|
|
590
|
-
if (
|
|
847
|
+
if (newPath === null) {
|
|
591
848
|
// Already at root, do nothing
|
|
592
849
|
return;
|
|
593
850
|
}
|
|
594
851
|
|
|
595
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
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
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
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
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
1281
|
-
const
|
|
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">×</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
|
+
}
|