gh-here 3.0.3 → 3.1.0

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 (41) hide show
  1. package/.env +0 -0
  2. package/.playwright-mcp/fixed-alignment.png +0 -0
  3. package/.playwright-mcp/fixed-layout.png +0 -0
  4. package/.playwright-mcp/gh-here-home-header-table.png +0 -0
  5. package/.playwright-mcp/gh-here-home.png +0 -0
  6. package/.playwright-mcp/line-selection-multiline.png +0 -0
  7. package/.playwright-mcp/line-selection-test-after.png +0 -0
  8. package/.playwright-mcp/line-selection-test-before.png +0 -0
  9. package/.playwright-mcp/page-2026-01-03T17-58-21-336Z.png +0 -0
  10. package/lib/constants.js +25 -15
  11. package/lib/content-search.js +212 -0
  12. package/lib/error-handler.js +39 -28
  13. package/lib/file-utils.js +438 -287
  14. package/lib/git.js +10 -54
  15. package/lib/gitignore.js +70 -41
  16. package/lib/renderers.js +15 -19
  17. package/lib/server.js +70 -193
  18. package/lib/symbol-parser.js +600 -0
  19. package/package.json +1 -1
  20. package/public/app.js +134 -68
  21. package/public/js/constants.js +50 -34
  22. package/public/js/content-search-handler.js +551 -0
  23. package/public/js/file-viewer.js +437 -0
  24. package/public/js/focus-mode.js +280 -0
  25. package/public/js/inline-search.js +659 -0
  26. package/public/js/modal-manager.js +14 -28
  27. package/public/js/symbol-outline.js +454 -0
  28. package/public/js/utils.js +152 -94
  29. package/public/styles.css +2049 -296
  30. package/.claude/settings.local.json +0 -30
  31. package/SAMPLE.md +0 -287
  32. package/lib/validation.js +0 -77
  33. package/public/app.js.backup +0 -1902
  34. package/public/js/draft-manager.js +0 -36
  35. package/public/js/editor-manager.js +0 -159
  36. package/test.js +0 -138
  37. package/tests/draftManager.test.js +0 -241
  38. package/tests/fileTypeDetection.test.js +0 -111
  39. package/tests/httpService.test.js +0 -268
  40. package/tests/languageDetection.test.js +0 -145
  41. package/tests/pathUtils.test.js +0 -136
package/public/app.js CHANGED
@@ -1,16 +1,30 @@
1
1
  /**
2
2
  * Main application entry point
3
3
  * Coordinates all modules and initializes the application
4
+ * @module app
4
5
  */
5
6
 
6
- import { ThemeManager } from './js/theme-manager.js';
7
- import { SearchHandler } from './js/search-handler.js';
8
- import { KeyboardHandler } from './js/keyboard-handler.js';
7
+ // ============================================================================
8
+ // Imports (alpha-sorted)
9
+ // ============================================================================
10
+
11
+ import { ContentSearchHandler } from './js/content-search-handler.js';
12
+ import { copyToClipboard } from './js/clipboard-utils.js';
9
13
  import { FileTreeNavigator } from './js/file-tree.js';
14
+ import { FileViewer } from './js/file-viewer.js';
15
+ import { FocusMode } from './js/focus-mode.js';
16
+ import { InlineSearch } from './js/inline-search.js';
17
+ import { KeyboardHandler } from './js/keyboard-handler.js';
10
18
  import { NavigationHandler } from './js/navigation.js';
11
19
  import { PathUtils } from './js/utils.js';
20
+ import { SearchHandler } from './js/search-handler.js';
12
21
  import { showNotification } from './js/notification.js';
13
- import { copyToClipboard } from './js/clipboard-utils.js';
22
+ import { SymbolOutline } from './js/symbol-outline.js';
23
+ import { ThemeManager } from './js/theme-manager.js';
24
+
25
+ // ============================================================================
26
+ // Application Class
27
+ // ============================================================================
14
28
 
15
29
  class Application {
16
30
  constructor() {
@@ -20,6 +34,11 @@ class Application {
20
34
  this.fileTree = null;
21
35
  this.navigationHandler = null;
22
36
  this.lastSelectedLine = null;
37
+ this.fileViewer = null;
38
+ this.inlineSearch = null;
39
+ this.focusMode = null;
40
+ this.contentSearch = null;
41
+ this.symbolOutline = null;
23
42
  }
24
43
 
25
44
  init() {
@@ -32,6 +51,9 @@ class Application {
32
51
  // Re-initialize components after client-side navigation
33
52
  document.addEventListener('content-loaded', () => {
34
53
  try {
54
+ // Cleanup existing components before re-initializing
55
+ this.cleanupComponents();
56
+
35
57
  // Re-initialize theme manager listeners (button might be re-rendered)
36
58
  if (this.themeManager) {
37
59
  this.themeManager.setupListeners();
@@ -57,8 +79,11 @@ class Application {
57
79
 
58
80
  this.setupGlobalEventListeners();
59
81
  this.setupGitignoreToggle();
60
- this.setupFileOperations();
61
82
  this.highlightLinesFromHash();
83
+ this.initializeFileViewer();
84
+ this.initializeInlineSearch();
85
+ this.initializeFocusMode();
86
+ this.initializeContentSearch();
62
87
  } catch (error) {
63
88
  console.error('Error re-initializing components:', error);
64
89
  }
@@ -84,8 +109,84 @@ class Application {
84
109
 
85
110
  this.setupGlobalEventListeners();
86
111
  this.setupGitignoreToggle();
87
- this.setupFileOperations();
88
112
  this.highlightLinesFromHash();
113
+ this.initializeFileViewer();
114
+ this.initializeInlineSearch();
115
+ this.initializeFocusMode();
116
+ this.initializeContentSearch();
117
+ this.initializeSymbolOutline();
118
+ }
119
+
120
+ // ========================================================================
121
+ // Component Initialization
122
+ // ========================================================================
123
+
124
+ /**
125
+ * Check if current page is a file view page
126
+ */
127
+ isFileViewPage() {
128
+ const fileContent = document.querySelector('.file-content');
129
+ return fileContent?.querySelector('pre code.hljs.with-line-numbers') !== null;
130
+ }
131
+
132
+ /**
133
+ * Initialize Monaco-based file viewer
134
+ */
135
+ initializeFileViewer() {
136
+ if (!this.isFileViewPage()) return;
137
+
138
+ // Wait for Monaco to be ready
139
+ if (typeof require === 'undefined' || !window.monacoReady) {
140
+ // Wait a bit and try again
141
+ setTimeout(() => this.initializeFileViewer(), 100);
142
+ return;
143
+ }
144
+
145
+ if (!this.fileViewer) {
146
+ this.fileViewer = new FileViewer();
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Initialize inline search component
152
+ * Note: Skipped when Monaco viewer is active (Monaco has built-in search)
153
+ */
154
+ initializeInlineSearch() {
155
+ if (!this.isFileViewPage()) return;
156
+
157
+ // Skip if Monaco viewer is active
158
+ if (document.querySelector('.monaco-file-viewer')) {
159
+ return;
160
+ }
161
+
162
+ if (!this.inlineSearch) {
163
+ this.inlineSearch = new InlineSearch();
164
+ }
165
+ this.inlineSearch.init();
166
+ }
167
+
168
+ initializeFocusMode() {
169
+ if (!this.focusMode) {
170
+ this.focusMode = new FocusMode();
171
+ }
172
+ this.focusMode.init();
173
+ }
174
+
175
+ /**
176
+ * Initialize symbol outline panel
177
+ */
178
+ initializeSymbolOutline() {
179
+ // Cleanup previous instance
180
+ if (this.symbolOutline) {
181
+ this.symbolOutline.destroy();
182
+ this.symbolOutline = null;
183
+ }
184
+
185
+ // Only initialize on file view pages
186
+ if (!this.isFileViewPage()) return;
187
+
188
+ this.symbolOutline = new SymbolOutline();
189
+ this.symbolOutline.init();
89
190
  }
90
191
 
91
192
  setupGlobalEventListeners() {
@@ -193,68 +294,6 @@ class Application {
193
294
  }
194
295
 
195
296
 
196
- setupFileOperations() {
197
- document.addEventListener('click', async e => {
198
- if (e.target.closest('.delete-btn')) {
199
- const btn = e.target.closest('.delete-btn');
200
- const itemPath = btn.dataset.path;
201
- const itemName = btn.dataset.name;
202
- const isDirectory = btn.dataset.isDirectory === 'true';
203
-
204
- const message = `Are you sure you want to delete ${isDirectory ? 'folder' : 'file'} "${itemName}"?`;
205
- if (!confirm(message)) {
206
- return;
207
- }
208
-
209
- try {
210
- const response = await fetch('/api/delete', {
211
- method: 'POST',
212
- headers: { 'Content-Type': 'application/json' },
213
- body: JSON.stringify({ path: itemPath })
214
- });
215
-
216
- if (!response.ok) {
217
- throw new Error('Delete failed');
218
- }
219
-
220
- showNotification(`${isDirectory ? 'Folder' : 'File'} deleted successfully`, 'success');
221
- setTimeout(() => window.location.reload(), 600);
222
- } catch (error) {
223
- showNotification('Failed to delete item', 'error');
224
- }
225
- }
226
-
227
- if (e.target.closest('.rename-btn')) {
228
- const btn = e.target.closest('.rename-btn');
229
- const itemPath = btn.dataset.path;
230
- const currentName = btn.dataset.name;
231
- const isDirectory = btn.dataset.isDirectory === 'true';
232
-
233
- const newName = prompt(`Rename ${isDirectory ? 'folder' : 'file'}:`, currentName);
234
- if (!newName || newName.trim() === currentName) {
235
- return;
236
- }
237
-
238
- try {
239
- const response = await fetch('/api/rename', {
240
- method: 'POST',
241
- headers: { 'Content-Type': 'application/json' },
242
- body: JSON.stringify({ path: itemPath, newName: newName.trim() })
243
- });
244
-
245
- if (!response.ok) {
246
- throw new Error('Rename failed');
247
- }
248
-
249
- showNotification(`Renamed to "${newName.trim()}"`, 'success');
250
- setTimeout(() => window.location.reload(), 600);
251
- } catch (error) {
252
- showNotification('Failed to rename item', 'error');
253
- }
254
- }
255
- });
256
- }
257
-
258
297
  showDiffViewer(filePath) {
259
298
  // Simplified - redirect to diff view
260
299
  const url = new URL(window.location.href);
@@ -332,6 +371,33 @@ class Application {
332
371
  firstLine.scrollIntoView({ behavior: 'smooth', block: 'center' });
333
372
  }
334
373
  }
374
+
375
+ initializeContentSearch() {
376
+ if (!this.contentSearch) {
377
+ this.contentSearch = new ContentSearchHandler();
378
+ }
379
+ this.contentSearch.init();
380
+ }
381
+
382
+ /**
383
+ * Cleanup components before navigation/re-initialization
384
+ */
385
+ cleanupComponents() {
386
+ // Cleanup file viewer
387
+ if (this.fileViewer && typeof this.fileViewer.destroy === 'function') {
388
+ this.fileViewer.destroy();
389
+ this.fileViewer = null;
390
+ }
391
+
392
+ // Cleanup inline search
393
+ if (this.inlineSearch && typeof this.inlineSearch.destroy === 'function') {
394
+ this.inlineSearch.destroy();
395
+ this.inlineSearch = null;
396
+ }
397
+
398
+ // Focus mode persists across navigations, no cleanup needed
399
+ // Content search persists across navigations, no cleanup needed
400
+ }
335
401
  }
336
402
 
337
403
  const app = new Application();
@@ -1,60 +1,76 @@
1
1
  /**
2
2
  * Application constants and configuration
3
+ * @module constants
3
4
  */
4
5
 
6
+ // ============================================================================
7
+ // Configuration
8
+ // ============================================================================
9
+
5
10
  export const CONFIG = {
6
- MONACO_CDN: 'https://unpkg.com/monaco-editor@0.45.0/min/vs',
7
- MONACO_VERSION: '0.45.0',
8
- EDITOR_HEIGHT: 600,
9
11
  DEFAULT_PORT: 5555,
12
+ MONACO_CDN: 'https://unpkg.com/monaco-editor@0.45.0/min/vs',
10
13
  NOTIFICATION_DURATION: 4000
11
14
  };
12
15
 
16
+ // ============================================================================
17
+ // Enums
18
+ // ============================================================================
19
+
13
20
  export const THEME = {
14
21
  DARK: 'dark',
15
22
  LIGHT: 'light'
16
23
  };
17
24
 
25
+ // ============================================================================
26
+ // Storage Keys
27
+ // ============================================================================
28
+
18
29
  export const STORAGE_KEYS = {
19
- THEME: 'gh-here-theme',
20
- DRAFT_PREFIX: 'gh-here-draft-'
30
+ THEME: 'gh-here-theme'
21
31
  };
22
32
 
33
+ // ============================================================================
34
+ // Keyboard Shortcuts (alpha-sorted)
35
+ // ============================================================================
36
+
37
+ export const KEYBOARD_SHORTCUTS = {
38
+ ESCAPE: 'Escape',
39
+ GO_UP: 'h',
40
+ HELP: '?',
41
+ NAV_DOWN: 'j',
42
+ NAV_UP: 'k',
43
+ OPEN: 'o',
44
+ REFRESH: 'r',
45
+ SEARCH: ['/', 's'],
46
+ SHOW_DIFF: 'd',
47
+ THEME_TOGGLE: 't',
48
+ TOGGLE_GITIGNORE: 'i'
49
+ };
50
+
51
+ // ============================================================================
52
+ // Monaco Editor Options (for FileViewer read-only mode)
53
+ // ============================================================================
54
+
23
55
  export const EDITOR_OPTIONS = {
24
- minimap: { enabled: false },
25
- lineNumbers: 'on',
26
- wordWrap: 'off',
27
- scrollBeyondLastLine: false,
28
- fontSize: 12,
29
- lineHeight: 20,
30
- fontFamily: "ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace",
31
- padding: { top: 16, bottom: 16 },
32
- renderLineHighlight: 'line',
33
- selectOnLineNumbers: true,
34
56
  automaticLayout: true,
57
+ bracketPairColorization: { enabled: true },
35
58
  folding: true,
36
59
  foldingHighlight: true,
37
60
  foldingStrategy: 'auto',
38
- showFoldingControls: 'mouseover',
39
- bracketPairColorization: { enabled: true },
61
+ fontFamily: "ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace",
62
+ fontSize: 12,
40
63
  guides: {
41
64
  bracketPairs: true,
42
65
  indentation: true
43
- }
44
- };
45
-
46
- export const KEYBOARD_SHORTCUTS = {
47
- SEARCH: ['/', 's'],
48
- THEME_TOGGLE: 't',
49
- HELP: '?',
50
- ESCAPE: 'Escape',
51
- GO_UP: 'h',
52
- REFRESH: 'r',
53
- CREATE_FILE: 'c',
54
- EDIT_FILE: 'e',
55
- SHOW_DIFF: 'd',
56
- TOGGLE_GITIGNORE: 'i',
57
- NAV_DOWN: 'j',
58
- NAV_UP: 'k',
59
- OPEN: 'o'
66
+ },
67
+ lineHeight: 20,
68
+ lineNumbers: 'on',
69
+ minimap: { enabled: false },
70
+ padding: { top: 16, bottom: 16 },
71
+ renderLineHighlight: 'line',
72
+ scrollBeyondLastLine: false,
73
+ selectOnLineNumbers: true,
74
+ showFoldingControls: 'mouseover',
75
+ wordWrap: 'off'
60
76
  };