@compilr-dev/cli 0.4.0 → 0.5.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.
- package/README.md +30 -12
- package/dist/agent.d.ts +74 -1
- package/dist/agent.js +259 -76
- package/dist/anchors/index.d.ts +9 -0
- package/dist/anchors/index.js +9 -0
- package/dist/anchors/project-anchors.d.ts +79 -0
- package/dist/anchors/project-anchors.js +202 -0
- package/dist/commands/handler-types.d.ts +68 -0
- package/dist/commands/handler-types.js +8 -0
- package/dist/commands/handlers/agent-commands.d.ts +13 -0
- package/dist/commands/handlers/agent-commands.js +305 -0
- package/dist/commands/handlers/design-commands.d.ts +15 -0
- package/dist/commands/handlers/design-commands.js +334 -0
- package/dist/commands/handlers/index.d.ts +20 -0
- package/dist/commands/handlers/index.js +43 -0
- package/dist/commands/handlers/overlay-commands.d.ts +21 -0
- package/dist/commands/handlers/overlay-commands.js +287 -0
- package/dist/commands/handlers/project-commands.d.ts +11 -0
- package/dist/commands/handlers/project-commands.js +167 -0
- package/dist/commands/handlers/simple-commands.d.ts +19 -0
- package/dist/commands/handlers/simple-commands.js +144 -0
- package/dist/commands/index.d.ts +2 -1
- package/dist/commands/registry.d.ts +50 -0
- package/dist/commands/registry.js +75 -0
- package/dist/commands-v2/handlers/context.d.ts +13 -0
- package/dist/commands-v2/handlers/context.js +348 -0
- package/dist/commands-v2/handlers/core.d.ts +13 -0
- package/dist/commands-v2/handlers/core.js +165 -0
- package/dist/commands-v2/handlers/debug.d.ts +11 -0
- package/dist/commands-v2/handlers/debug.js +159 -0
- package/dist/commands-v2/handlers/index.d.ts +12 -0
- package/dist/commands-v2/handlers/index.js +24 -0
- package/dist/commands-v2/handlers/project.d.ts +22 -0
- package/dist/commands-v2/handlers/project.js +814 -0
- package/dist/commands-v2/handlers/settings.d.ts +15 -0
- package/dist/commands-v2/handlers/settings.js +235 -0
- package/dist/commands-v2/index.d.ts +13 -0
- package/dist/commands-v2/index.js +15 -0
- package/dist/commands-v2/registry.d.ts +37 -0
- package/dist/commands-v2/registry.js +80 -0
- package/dist/commands-v2/types.d.ts +75 -0
- package/dist/commands-v2/types.js +7 -0
- package/dist/commands.js +110 -7
- package/dist/index.js +288 -29
- package/dist/input-handlers/index.d.ts +7 -0
- package/dist/input-handlers/index.js +7 -0
- package/dist/input-handlers/memory-handler.d.ts +26 -0
- package/dist/input-handlers/memory-handler.js +68 -0
- package/dist/repl-helpers.d.ts +63 -0
- package/dist/repl-helpers.js +318 -0
- package/dist/repl-v2.d.ts +155 -0
- package/dist/repl-v2.js +774 -0
- package/dist/repl.d.ts +32 -4
- package/dist/repl.js +250 -977
- package/dist/settings/index.d.ts +23 -0
- package/dist/settings/index.js +48 -0
- package/dist/settings/paths.d.ts +110 -0
- package/dist/settings/paths.js +264 -0
- package/dist/templates/compilr-md.js +7 -4
- package/dist/templates/index.js +3 -4
- package/dist/themes/colors.js +3 -1
- package/dist/themes/registry.d.ts +5 -36
- package/dist/themes/registry.js +11 -95
- package/dist/themes/types.d.ts +3 -38
- package/dist/themes/types.js +2 -2
- package/dist/tools/anchor-tools.d.ts +31 -0
- package/dist/tools/anchor-tools.js +255 -0
- package/dist/tools/backlog-wrappers.d.ts +54 -0
- package/dist/tools/backlog-wrappers.js +338 -0
- package/dist/tools/backlog.js +1 -1
- package/dist/tools/db-tools.d.ts +65 -0
- package/dist/tools/db-tools.js +19 -0
- package/dist/tools/document-db.d.ts +43 -0
- package/dist/tools/document-db.js +220 -0
- package/dist/tools/project-db.d.ts +102 -0
- package/dist/tools/project-db.js +370 -0
- package/dist/tools/workitem-db.d.ts +103 -0
- package/dist/tools/workitem-db.js +549 -0
- package/dist/tools.js +13 -3
- package/dist/ui/agents-overlay-v2.d.ts +43 -0
- package/dist/ui/agents-overlay-v2.js +809 -0
- package/dist/ui/agents-overlay.d.ts +5 -5
- package/dist/ui/agents-overlay.js +782 -420
- package/dist/ui/anchors-overlay.d.ts +12 -0
- package/dist/ui/anchors-overlay.js +775 -0
- package/dist/ui/arch-type-overlay.d.ts +1 -6
- package/dist/ui/arch-type-overlay.js +175 -203
- package/dist/ui/ask-user-overlay-v2.d.ts +26 -0
- package/dist/ui/ask-user-overlay-v2.js +555 -0
- package/dist/ui/ask-user-overlay.d.ts +2 -2
- package/dist/ui/ask-user-overlay.js +443 -535
- package/dist/ui/ask-user-simple-overlay-v2.d.ts +25 -0
- package/dist/ui/ask-user-simple-overlay-v2.js +215 -0
- package/dist/ui/ask-user-simple-overlay.d.ts +2 -2
- package/dist/ui/ask-user-simple-overlay.js +182 -209
- package/dist/ui/backlog-overlay.d.ts +16 -1
- package/dist/ui/backlog-overlay.js +525 -659
- package/dist/ui/base/index.d.ts +26 -0
- package/dist/ui/base/index.js +33 -0
- package/dist/ui/base/inline-overlay-utils.d.ts +217 -0
- package/dist/ui/base/inline-overlay-utils.js +320 -0
- package/dist/ui/base/inline-overlay.d.ts +159 -0
- package/dist/ui/base/inline-overlay.js +257 -0
- package/dist/ui/base/key-utils.d.ts +15 -0
- package/dist/ui/base/key-utils.js +30 -0
- package/dist/ui/base/overlay-base-v2.d.ts +193 -0
- package/dist/ui/base/overlay-base-v2.js +246 -0
- package/dist/ui/base/overlay-base.d.ts +156 -0
- package/dist/ui/base/overlay-base.js +238 -0
- package/dist/ui/base/overlay-lifecycle.d.ts +65 -0
- package/dist/ui/base/overlay-lifecycle.js +159 -0
- package/dist/ui/base/overlay-types.d.ts +185 -0
- package/dist/ui/base/overlay-types.js +7 -0
- package/dist/ui/base/render-utils.d.ts +8 -0
- package/dist/ui/base/render-utils.js +11 -0
- package/dist/ui/base/screen-stack.d.ts +148 -0
- package/dist/ui/base/screen-stack.js +184 -0
- package/dist/ui/base/tabbed-list-overlay-v2.d.ts +103 -0
- package/dist/ui/base/tabbed-list-overlay-v2.js +317 -0
- package/dist/ui/base/tabbed-list-overlay.d.ts +153 -0
- package/dist/ui/base/tabbed-list-overlay.js +369 -0
- package/dist/ui/commands-overlay-v2.d.ts +33 -0
- package/dist/ui/commands-overlay-v2.js +441 -0
- package/dist/ui/commands-overlay.d.ts +7 -2
- package/dist/ui/commands-overlay.js +384 -355
- package/dist/ui/config-overlay.d.ts +5 -4
- package/dist/ui/config-overlay.js +243 -513
- package/dist/ui/conversation.d.ts +75 -4
- package/dist/ui/conversation.js +374 -161
- package/dist/ui/docs-overlay.d.ts +17 -0
- package/dist/ui/docs-overlay.js +303 -0
- package/dist/ui/ephemeral.d.ts +1 -1
- package/dist/ui/ephemeral.js +1 -1
- package/dist/ui/features/index.d.ts +34 -0
- package/dist/ui/features/index.js +34 -0
- package/dist/ui/features/input-feature.d.ts +85 -0
- package/dist/ui/features/input-feature.js +238 -0
- package/dist/ui/features/list-feature.d.ts +155 -0
- package/dist/ui/features/list-feature.js +244 -0
- package/dist/ui/features/pagination-feature.d.ts +154 -0
- package/dist/ui/features/pagination-feature.js +238 -0
- package/dist/ui/features/search-feature.d.ts +148 -0
- package/dist/ui/features/search-feature.js +185 -0
- package/dist/ui/features/tab-feature.d.ts +194 -0
- package/dist/ui/features/tab-feature.js +307 -0
- package/dist/ui/footer-v2.d.ts +222 -0
- package/dist/ui/footer-v2.js +1349 -0
- package/dist/ui/footer.d.ts +107 -0
- package/dist/ui/footer.js +359 -67
- package/dist/ui/guardrail-overlay.d.ts +29 -0
- package/dist/ui/guardrail-overlay.js +145 -0
- package/dist/ui/help-overlay-v2.d.ts +34 -0
- package/dist/ui/help-overlay-v2.js +309 -0
- package/dist/ui/help-overlay.d.ts +16 -0
- package/dist/ui/help-overlay.js +316 -0
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +1 -3
- package/dist/ui/init-overlay-v2.d.ts +34 -0
- package/dist/ui/init-overlay-v2.js +600 -0
- package/dist/ui/init-overlay.d.ts +12 -2
- package/dist/ui/init-overlay.js +349 -270
- package/dist/ui/input-prompt-v2.d.ts +1 -0
- package/dist/ui/input-prompt-v2.js +14 -6
- package/dist/ui/input-prompt.d.ts +116 -33
- package/dist/ui/input-prompt.js +536 -337
- package/dist/ui/iteration-limit-overlay-v2.d.ts +21 -0
- package/dist/ui/iteration-limit-overlay-v2.js +114 -0
- package/dist/ui/iteration-limit-overlay.d.ts +2 -2
- package/dist/ui/iteration-limit-overlay.js +92 -128
- package/dist/ui/keys-overlay-v2.d.ts +41 -0
- package/dist/ui/keys-overlay-v2.js +248 -0
- package/dist/ui/keys-overlay.d.ts +1 -0
- package/dist/ui/keys-overlay.js +203 -141
- package/dist/ui/line-utils.d.ts +88 -0
- package/dist/ui/line-utils.js +150 -0
- package/dist/ui/live-region.d.ts +161 -0
- package/dist/ui/live-region.js +387 -0
- package/dist/ui/mascot/expressions.d.ts +32 -0
- package/dist/ui/mascot/expressions.js +213 -0
- package/dist/ui/mascot/index.d.ts +8 -0
- package/dist/ui/mascot/index.js +8 -0
- package/dist/ui/mascot/renderer.d.ts +19 -0
- package/dist/ui/mascot/renderer.js +97 -0
- package/dist/ui/mascot-overlay-v2.d.ts +41 -0
- package/dist/ui/mascot-overlay-v2.js +138 -0
- package/dist/ui/mascot-overlay.d.ts +21 -0
- package/dist/ui/mascot-overlay.js +146 -0
- package/dist/ui/model-overlay-v2.d.ts +49 -0
- package/dist/ui/model-overlay-v2.js +118 -0
- package/dist/ui/model-overlay.d.ts +27 -0
- package/dist/ui/model-overlay.js +221 -0
- package/dist/ui/model-warning-overlay.js +3 -5
- package/dist/ui/new-overlay.d.ts +34 -0
- package/dist/ui/new-overlay.js +604 -0
- package/dist/ui/overlay/impl/agents-overlay-v2.d.ts +45 -0
- package/dist/ui/overlay/impl/agents-overlay-v2.js +825 -0
- package/dist/ui/overlay/impl/anchors-overlay-v2.d.ts +47 -0
- package/dist/ui/overlay/impl/anchors-overlay-v2.js +783 -0
- package/dist/ui/overlay/impl/arch-type-overlay-v2.d.ts +37 -0
- package/dist/ui/overlay/impl/arch-type-overlay-v2.js +240 -0
- package/dist/ui/overlay/impl/ask-user-overlay-v2.d.ts +72 -0
- package/dist/ui/overlay/impl/ask-user-overlay-v2.js +584 -0
- package/dist/ui/overlay/impl/ask-user-simple-overlay-v2.d.ts +46 -0
- package/dist/ui/overlay/impl/ask-user-simple-overlay-v2.js +204 -0
- package/dist/ui/overlay/impl/backlog-overlay-v2.d.ts +49 -0
- package/dist/ui/overlay/impl/backlog-overlay-v2.js +642 -0
- package/dist/ui/overlay/impl/commands-overlay-v2.d.ts +33 -0
- package/dist/ui/overlay/impl/commands-overlay-v2.js +441 -0
- package/dist/ui/overlay/impl/config-overlay-v2.d.ts +100 -0
- package/dist/ui/overlay/impl/config-overlay-v2.js +654 -0
- package/dist/ui/overlay/impl/dashboard-overlay-v2.d.ts +55 -0
- package/dist/ui/overlay/impl/dashboard-overlay-v2.js +359 -0
- package/dist/ui/overlay/impl/docs-overlay-v2.d.ts +45 -0
- package/dist/ui/overlay/impl/docs-overlay-v2.js +114 -0
- package/dist/ui/overlay/impl/document-detail-overlay-v2.d.ts +77 -0
- package/dist/ui/overlay/impl/document-detail-overlay-v2.js +1071 -0
- package/dist/ui/overlay/impl/guardrail-overlay-v2.d.ts +43 -0
- package/dist/ui/overlay/impl/guardrail-overlay-v2.js +114 -0
- package/dist/ui/overlay/impl/help-overlay-v2.d.ts +34 -0
- package/dist/ui/overlay/impl/help-overlay-v2.js +309 -0
- package/dist/ui/overlay/impl/init-overlay-v2.d.ts +77 -0
- package/dist/ui/overlay/impl/init-overlay-v2.js +593 -0
- package/dist/ui/overlay/impl/init-setup-overlay-v2.d.ts +25 -0
- package/dist/ui/overlay/impl/init-setup-overlay-v2.js +97 -0
- package/dist/ui/overlay/impl/iteration-limit-overlay-v2.d.ts +35 -0
- package/dist/ui/overlay/impl/iteration-limit-overlay-v2.js +105 -0
- package/dist/ui/overlay/impl/keys-overlay-v2.d.ts +41 -0
- package/dist/ui/overlay/impl/keys-overlay-v2.js +248 -0
- package/dist/ui/overlay/impl/mascot-overlay-v2.d.ts +41 -0
- package/dist/ui/overlay/impl/mascot-overlay-v2.js +138 -0
- package/dist/ui/overlay/impl/model-overlay-v2.d.ts +49 -0
- package/dist/ui/overlay/impl/model-overlay-v2.js +118 -0
- package/dist/ui/overlay/impl/model-warning-overlay-v2.d.ts +46 -0
- package/dist/ui/overlay/impl/model-warning-overlay-v2.js +132 -0
- package/dist/ui/overlay/impl/new-overlay-v2.d.ts +77 -0
- package/dist/ui/overlay/impl/new-overlay-v2.js +593 -0
- package/dist/ui/overlay/impl/permission-overlay-v2.d.ts +36 -0
- package/dist/ui/overlay/impl/permission-overlay-v2.js +380 -0
- package/dist/ui/overlay/impl/projects-overlay-v2.d.ts +36 -0
- package/dist/ui/overlay/impl/projects-overlay-v2.js +499 -0
- package/dist/ui/overlay/impl/theme-overlay-v2.d.ts +42 -0
- package/dist/ui/overlay/impl/theme-overlay-v2.js +135 -0
- package/dist/ui/overlay/impl/tools-overlay-v2.d.ts +47 -0
- package/dist/ui/overlay/impl/tools-overlay-v2.js +218 -0
- package/dist/ui/overlay/impl/tutorial-overlay-v2.d.ts +31 -0
- package/dist/ui/overlay/impl/tutorial-overlay-v2.js +1035 -0
- package/dist/ui/overlay/impl/workflow-overlay-v2.d.ts +80 -0
- package/dist/ui/overlay/impl/workflow-overlay-v2.js +637 -0
- package/dist/ui/overlay/index.d.ts +33 -0
- package/dist/ui/overlay/index.js +35 -0
- package/dist/ui/overlay/key-utils.d.ts +6 -0
- package/dist/ui/overlay/key-utils.js +6 -0
- package/dist/ui/overlay/overlay-types.d.ts +128 -0
- package/dist/ui/overlay/overlay-types.js +22 -0
- package/dist/ui/overlay/types.d.ts +135 -0
- package/dist/ui/overlay/types.js +22 -0
- package/dist/ui/overlays/help-overlay-v2.d.ts +28 -0
- package/dist/ui/overlays/help-overlay-v2.js +198 -0
- package/dist/ui/overlays/index.d.ts +11 -0
- package/dist/ui/overlays/index.js +11 -0
- package/dist/ui/overlays.d.ts +0 -4
- package/dist/ui/overlays.js +0 -444
- package/dist/ui/permission-overlay-v2.d.ts +36 -0
- package/dist/ui/permission-overlay-v2.js +380 -0
- package/dist/ui/permission-overlay.d.ts +1 -1
- package/dist/ui/permission-overlay.js +186 -298
- package/dist/ui/projects-overlay.d.ts +19 -0
- package/dist/ui/projects-overlay.js +484 -0
- package/dist/ui/providers/types.d.ts +178 -0
- package/dist/ui/providers/types.js +9 -0
- package/dist/ui/render-modes.d.ts +36 -0
- package/dist/ui/render-modes.js +44 -0
- package/dist/ui/startup-menu.d.ts +36 -0
- package/dist/ui/startup-menu.js +236 -0
- package/dist/ui/subagent-renderer.d.ts +117 -0
- package/dist/ui/subagent-renderer.js +334 -0
- package/dist/ui/terminal-codes.d.ts +94 -0
- package/dist/ui/terminal-codes.js +124 -0
- package/dist/ui/terminal-renderer.d.ts +221 -0
- package/dist/ui/terminal-renderer.js +751 -0
- package/dist/ui/terminal-ui.d.ts +463 -0
- package/dist/ui/terminal-ui.js +2296 -0
- package/dist/ui/terminal.d.ts +20 -0
- package/dist/ui/terminal.js +72 -0
- package/dist/ui/theme-overlay-v2.d.ts +42 -0
- package/dist/ui/theme-overlay-v2.js +135 -0
- package/dist/ui/theme-overlay.d.ts +24 -0
- package/dist/ui/theme-overlay.js +127 -0
- package/dist/ui/todo-zone.js +53 -25
- package/dist/ui/tool-formatters.d.ts +16 -0
- package/dist/ui/tool-formatters.js +516 -0
- package/dist/ui/tools-overlay-v2.d.ts +47 -0
- package/dist/ui/tools-overlay-v2.js +218 -0
- package/dist/ui/tools-overlay.d.ts +10 -2
- package/dist/ui/tools-overlay.js +172 -220
- package/dist/ui/tutorial-overlay-v2.d.ts +31 -0
- package/dist/ui/tutorial-overlay-v2.js +1035 -0
- package/dist/ui/tutorial-overlay.d.ts +1 -0
- package/dist/ui/tutorial-overlay.js +400 -302
- package/dist/ui/workflow-overlay.d.ts +22 -0
- package/dist/ui/workflow-overlay.js +636 -0
- package/dist/utils/debug-log.d.ts +28 -0
- package/dist/utils/debug-log.js +57 -0
- package/dist/utils/model-tiers.js +1 -1
- package/dist/utils/path-safety.d.ts +56 -0
- package/dist/utils/path-safety.js +239 -0
- package/dist/workflow/guided-mode-injector.d.ts +42 -0
- package/dist/workflow/guided-mode-injector.js +191 -0
- package/dist/workflow/index.d.ts +8 -0
- package/dist/workflow/index.js +8 -0
- package/dist/workflow/step-criteria.d.ts +62 -0
- package/dist/workflow/step-criteria.js +150 -0
- package/dist/workflow/step-tracker.d.ts +92 -0
- package/dist/workflow/step-tracker.js +141 -0
- package/package.json +12 -5
|
@@ -0,0 +1,1071 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document Detail Overlay V2
|
|
3
|
+
*
|
|
4
|
+
* Fullscreen markdown document viewer AND editor with vim-style navigation.
|
|
5
|
+
* Separate from DocsOverlayV2 (inline list) to allow proper fullscreen rendering.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Full-screen document viewing with markdown rendering
|
|
9
|
+
* - Vim-style navigation (j/k, g/G, PgUp/PgDn)
|
|
10
|
+
* - Edit mode with raw markdown editing
|
|
11
|
+
* - Save to database (Ctrl+S)
|
|
12
|
+
* - Dirty check with save prompt on exit
|
|
13
|
+
*/
|
|
14
|
+
import chalk from 'chalk';
|
|
15
|
+
import hljs from 'highlight.js';
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// Alternate Screen Buffer Management
|
|
18
|
+
// =============================================================================
|
|
19
|
+
let inAlternateScreen = false;
|
|
20
|
+
function enterAlternateScreen() {
|
|
21
|
+
if (inAlternateScreen)
|
|
22
|
+
return;
|
|
23
|
+
process.stdout.write('\x1b[?1049h'); // Switch to alternate screen
|
|
24
|
+
process.stdout.write('\x1b[H'); // Move cursor to home
|
|
25
|
+
inAlternateScreen = true;
|
|
26
|
+
}
|
|
27
|
+
function exitAlternateScreen() {
|
|
28
|
+
if (!inAlternateScreen)
|
|
29
|
+
return;
|
|
30
|
+
process.stdout.write('\x1b[?1049l'); // Switch back to main screen
|
|
31
|
+
inAlternateScreen = false;
|
|
32
|
+
}
|
|
33
|
+
// Ensure we exit alternate screen on process termination
|
|
34
|
+
function setupAlternateScreenCleanup() {
|
|
35
|
+
const cleanup = () => {
|
|
36
|
+
exitAlternateScreen();
|
|
37
|
+
};
|
|
38
|
+
// Handle normal exit
|
|
39
|
+
process.on('exit', cleanup);
|
|
40
|
+
// Handle Ctrl+C
|
|
41
|
+
process.on('SIGINT', () => {
|
|
42
|
+
cleanup();
|
|
43
|
+
process.exit(130);
|
|
44
|
+
});
|
|
45
|
+
// Handle termination
|
|
46
|
+
process.on('SIGTERM', () => {
|
|
47
|
+
cleanup();
|
|
48
|
+
process.exit(143);
|
|
49
|
+
});
|
|
50
|
+
// Handle uncaught exceptions
|
|
51
|
+
process.on('uncaughtException', (err) => {
|
|
52
|
+
cleanup();
|
|
53
|
+
console.error('Uncaught exception:', err);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
});
|
|
56
|
+
// Handle unhandled promise rejections
|
|
57
|
+
process.on('unhandledRejection', (reason) => {
|
|
58
|
+
cleanup();
|
|
59
|
+
console.error('Unhandled rejection:', reason);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
// Setup cleanup handlers once when module loads
|
|
64
|
+
setupAlternateScreenCleanup();
|
|
65
|
+
import { marked } from 'marked';
|
|
66
|
+
import { markedTerminal } from 'marked-terminal';
|
|
67
|
+
import { BaseOverlayV2 } from '../../base/overlay-base-v2.js';
|
|
68
|
+
import { renderBorder, isCtrlC, isClose } from '../../base/index.js';
|
|
69
|
+
import { documentRepository } from '../../../db/repositories/document-repository.js';
|
|
70
|
+
import { getCurrentTheme } from '../../../themes/index.js';
|
|
71
|
+
import * as terminal from '../../terminal.js';
|
|
72
|
+
// =============================================================================
|
|
73
|
+
// Register Additional Languages for Syntax Highlighting
|
|
74
|
+
// =============================================================================
|
|
75
|
+
// Register aliases for common language names that map to existing languages
|
|
76
|
+
const languageAliases = {
|
|
77
|
+
// Shell variants → bash
|
|
78
|
+
'sh': 'bash', 'zsh': 'bash', 'fish': 'bash',
|
|
79
|
+
'console': 'bash', 'terminal': 'bash', 'command': 'bash',
|
|
80
|
+
// JSX/TSX → base language (highlighting won't be perfect but won't error)
|
|
81
|
+
'jsx': 'javascript', 'tsx': 'typescript',
|
|
82
|
+
// HTML variants → xml
|
|
83
|
+
'htm': 'xml', 'html': 'xml', 'vue': 'xml', 'svelte': 'xml', 'astro': 'xml',
|
|
84
|
+
// Plain text variants
|
|
85
|
+
'text': 'plaintext', 'txt': 'plaintext', 'log': 'plaintext',
|
|
86
|
+
'output': 'plaintext', 'result': 'plaintext',
|
|
87
|
+
'gitignore': 'plaintext', 'dockerignore': 'plaintext', 'ignore': 'plaintext',
|
|
88
|
+
'requirements': 'plaintext', 'csv': 'plaintext', 'tsv': 'plaintext',
|
|
89
|
+
// Diff variants
|
|
90
|
+
'patch': 'diff',
|
|
91
|
+
// JSON variants
|
|
92
|
+
'jsonc': 'json', 'json5': 'json', 'jsonl': 'json', 'ndjson': 'json',
|
|
93
|
+
// Config file variants → ini
|
|
94
|
+
'conf': 'ini', 'config': 'ini', 'env': 'ini', 'dotenv': 'ini',
|
|
95
|
+
'editorconfig': 'ini', 'toml': 'ini', 'pipfile': 'ini',
|
|
96
|
+
// Infrastructure as code (approximate)
|
|
97
|
+
'tf': 'json', 'hcl': 'json', 'terraform': 'json',
|
|
98
|
+
};
|
|
99
|
+
for (const [alias, target] of Object.entries(languageAliases)) {
|
|
100
|
+
try {
|
|
101
|
+
hljs.registerAliases(alias, { languageName: target });
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Ignore if alias already exists
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Register custom language definitions for diagram/DSL languages
|
|
108
|
+
// Mermaid diagrams
|
|
109
|
+
hljs.registerLanguage('mermaid', () => ({
|
|
110
|
+
name: 'Mermaid',
|
|
111
|
+
case_insensitive: true,
|
|
112
|
+
keywords: {
|
|
113
|
+
keyword: 'graph subgraph end flowchart sequenceDiagram classDiagram stateDiagram erDiagram gantt pie journey gitGraph',
|
|
114
|
+
built_in: 'TD TB BT RL LR participant actor loop alt else opt par and critical break'
|
|
115
|
+
},
|
|
116
|
+
contains: [
|
|
117
|
+
hljs.QUOTE_STRING_MODE,
|
|
118
|
+
hljs.COMMENT('%%', '$'),
|
|
119
|
+
{ className: 'operator', begin: /-->|--o|--x|<-->|---|->|<-|==>|==|-.->|-.-/ },
|
|
120
|
+
{ className: 'string', begin: /\|/, end: /\|/ },
|
|
121
|
+
{ className: 'title', begin: /\[/, end: /\]/ }
|
|
122
|
+
]
|
|
123
|
+
}));
|
|
124
|
+
// PlantUML diagrams
|
|
125
|
+
hljs.registerLanguage('plantuml', () => ({
|
|
126
|
+
name: 'PlantUML',
|
|
127
|
+
case_insensitive: true,
|
|
128
|
+
keywords: {
|
|
129
|
+
keyword: 'startuml enduml participant actor usecase class interface abstract enum component package node database cloud frame folder rectangle',
|
|
130
|
+
built_in: 'left right up down over note end activate deactivate return'
|
|
131
|
+
},
|
|
132
|
+
contains: [
|
|
133
|
+
hljs.QUOTE_STRING_MODE,
|
|
134
|
+
hljs.COMMENT("'", '$'),
|
|
135
|
+
{ className: 'operator', begin: /-->|->|<--|<-|\.\.>|<\.\.|--|-/ }
|
|
136
|
+
]
|
|
137
|
+
}));
|
|
138
|
+
// GraphQL
|
|
139
|
+
hljs.registerLanguage('graphql', () => ({
|
|
140
|
+
name: 'GraphQL',
|
|
141
|
+
keywords: {
|
|
142
|
+
keyword: 'query mutation subscription fragment on type interface union enum scalar input extend implements directive',
|
|
143
|
+
literal: 'true false null'
|
|
144
|
+
},
|
|
145
|
+
contains: [
|
|
146
|
+
hljs.QUOTE_STRING_MODE,
|
|
147
|
+
hljs.HASH_COMMENT_MODE,
|
|
148
|
+
{ className: 'variable', begin: /\$\w+/ },
|
|
149
|
+
{ className: 'type', begin: /\b[A-Z]\w*\b/ }
|
|
150
|
+
]
|
|
151
|
+
}));
|
|
152
|
+
hljs.registerAliases('gql', { languageName: 'graphql' });
|
|
153
|
+
// Graphviz DOT
|
|
154
|
+
hljs.registerLanguage('dot', () => ({
|
|
155
|
+
name: 'DOT',
|
|
156
|
+
keywords: {
|
|
157
|
+
keyword: 'graph digraph subgraph node edge strict',
|
|
158
|
+
built_in: 'shape label color style fillcolor fontcolor fontsize rankdir rank'
|
|
159
|
+
},
|
|
160
|
+
contains: [
|
|
161
|
+
hljs.QUOTE_STRING_MODE,
|
|
162
|
+
hljs.C_LINE_COMMENT_MODE,
|
|
163
|
+
hljs.C_BLOCK_COMMENT_MODE,
|
|
164
|
+
{ className: 'operator', begin: /->|--/ }
|
|
165
|
+
]
|
|
166
|
+
}));
|
|
167
|
+
hljs.registerAliases('graphviz', { languageName: 'dot' });
|
|
168
|
+
// =============================================================================
|
|
169
|
+
// Constants
|
|
170
|
+
// =============================================================================
|
|
171
|
+
const DOC_TYPE_LABELS = {
|
|
172
|
+
'prd': 'PRD',
|
|
173
|
+
'architecture': 'ARCH',
|
|
174
|
+
'design': 'DESIGN',
|
|
175
|
+
'notes': 'NOTES',
|
|
176
|
+
};
|
|
177
|
+
const TAB_SIZE = 2;
|
|
178
|
+
// =============================================================================
|
|
179
|
+
// Markdown Rendering
|
|
180
|
+
// =============================================================================
|
|
181
|
+
function getThemedMarkedOptions(termWidth) {
|
|
182
|
+
const theme = getCurrentTheme();
|
|
183
|
+
const primaryColor = chalk.hex(theme.colors.primary);
|
|
184
|
+
const secondaryColor = chalk.hex(theme.colors.secondary || theme.colors.primary);
|
|
185
|
+
const mutedColor = chalk.hex(theme.colors.muted);
|
|
186
|
+
const borderColor = chalk.hex(theme.colors.border);
|
|
187
|
+
const fgColor = chalk.hex(theme.colors.foreground);
|
|
188
|
+
return {
|
|
189
|
+
showSectionPrefix: false,
|
|
190
|
+
reflowText: false,
|
|
191
|
+
width: Math.min(100, termWidth - 4),
|
|
192
|
+
unescape: true,
|
|
193
|
+
tab: 2,
|
|
194
|
+
heading: primaryColor.bold,
|
|
195
|
+
firstHeading: primaryColor.bold,
|
|
196
|
+
strong: fgColor.bold,
|
|
197
|
+
em: fgColor.italic,
|
|
198
|
+
link: secondaryColor,
|
|
199
|
+
href: secondaryColor.underline,
|
|
200
|
+
blockquote: mutedColor.italic,
|
|
201
|
+
del: mutedColor.strikethrough,
|
|
202
|
+
hr: borderColor,
|
|
203
|
+
codespan: secondaryColor,
|
|
204
|
+
code: secondaryColor,
|
|
205
|
+
html: mutedColor,
|
|
206
|
+
table: fgColor,
|
|
207
|
+
listitem: fgColor,
|
|
208
|
+
paragraph: fgColor,
|
|
209
|
+
tableOptions: {
|
|
210
|
+
style: { head: ['bold'], border: [] },
|
|
211
|
+
chars: {
|
|
212
|
+
'top': '─', 'top-mid': '─', 'top-left': '─', 'top-right': '─',
|
|
213
|
+
'bottom': '─', 'bottom-mid': '─', 'bottom-left': '─', 'bottom-right': '─',
|
|
214
|
+
'left': ' ', 'left-mid': ' ', 'mid': '─', 'mid-mid': '─',
|
|
215
|
+
'right': ' ', 'right-mid': ' ', 'middle': ' ',
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function preprocessMarkdown(content) {
|
|
221
|
+
const lines = content.split('\n');
|
|
222
|
+
return lines.map(line => /^(\s*)\* /.test(line) ? line.replace(/^(\s*)\* /, '$1- ') : line).join('\n');
|
|
223
|
+
}
|
|
224
|
+
function postProcessInlineFormatting(text) {
|
|
225
|
+
return text.replace(/\*\*([^*]+)\*\*/g, (_match, content) => chalk.bold(content));
|
|
226
|
+
}
|
|
227
|
+
function renderMarkdownSync(content, termWidth) {
|
|
228
|
+
const preprocessed = preprocessMarkdown(content);
|
|
229
|
+
const options = getThemedMarkedOptions(termWidth);
|
|
230
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
|
|
231
|
+
marked.use(markedTerminal(options));
|
|
232
|
+
const rendered = marked.parse(preprocessed, { async: false });
|
|
233
|
+
const postProcessed = postProcessInlineFormatting(rendered);
|
|
234
|
+
return postProcessed.split('\n');
|
|
235
|
+
}
|
|
236
|
+
// =============================================================================
|
|
237
|
+
// Edit Mode Syntax Highlighting
|
|
238
|
+
// =============================================================================
|
|
239
|
+
// Import cli-highlight for code block highlighting
|
|
240
|
+
import { highlight as cliHighlight, supportsLanguage } from 'cli-highlight';
|
|
241
|
+
/**
|
|
242
|
+
* Highlight a line, considering code block context.
|
|
243
|
+
* Returns the highlighted line and updated code block state.
|
|
244
|
+
*/
|
|
245
|
+
function highlightEditLine(line, codeBlockState, theme) {
|
|
246
|
+
const secondary = chalk.hex(theme.colors.secondary || theme.colors.primary);
|
|
247
|
+
// Check for code fence start/end
|
|
248
|
+
const fenceMatch = line.match(/^(`{3,}|~{3,})(\w*)\s*$/);
|
|
249
|
+
if (fenceMatch) {
|
|
250
|
+
const [, fence, lang] = fenceMatch;
|
|
251
|
+
const fenceChar = fence[0];
|
|
252
|
+
const fenceLength = fence.length;
|
|
253
|
+
if (!codeBlockState.inCodeBlock) {
|
|
254
|
+
// Starting a code block
|
|
255
|
+
return {
|
|
256
|
+
highlighted: secondary(line),
|
|
257
|
+
newState: {
|
|
258
|
+
inCodeBlock: true,
|
|
259
|
+
language: lang || 'plaintext',
|
|
260
|
+
fenceChar,
|
|
261
|
+
fenceLength,
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
else if (fenceChar === codeBlockState.fenceChar && fenceLength >= codeBlockState.fenceLength) {
|
|
266
|
+
// Ending a code block (matching or longer fence)
|
|
267
|
+
return {
|
|
268
|
+
highlighted: secondary(line),
|
|
269
|
+
newState: { inCodeBlock: false, language: '', fenceChar: '', fenceLength: 0 },
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// Inside a code block - apply language-specific highlighting
|
|
274
|
+
if (codeBlockState.inCodeBlock) {
|
|
275
|
+
return {
|
|
276
|
+
highlighted: highlightCodeLine(line, codeBlockState.language, theme),
|
|
277
|
+
newState: codeBlockState,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
// Regular markdown line
|
|
281
|
+
return {
|
|
282
|
+
highlighted: highlightMarkdownLine(line, theme),
|
|
283
|
+
newState: codeBlockState,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Highlight a single line of code using cli-highlight.
|
|
288
|
+
*/
|
|
289
|
+
function highlightCodeLine(line, language, theme) {
|
|
290
|
+
// Map language aliases to supported languages
|
|
291
|
+
const langMap = {
|
|
292
|
+
'js': 'javascript',
|
|
293
|
+
'ts': 'typescript',
|
|
294
|
+
'py': 'python',
|
|
295
|
+
'rb': 'ruby',
|
|
296
|
+
'sh': 'bash',
|
|
297
|
+
'shell': 'bash',
|
|
298
|
+
'zsh': 'bash',
|
|
299
|
+
'yml': 'yaml',
|
|
300
|
+
'md': 'markdown',
|
|
301
|
+
'jsx': 'javascript',
|
|
302
|
+
'tsx': 'typescript',
|
|
303
|
+
'mermaid': 'plaintext', // highlight.js handles via our custom registration
|
|
304
|
+
'plantuml': 'plaintext',
|
|
305
|
+
'graphql': 'graphql',
|
|
306
|
+
'gql': 'graphql',
|
|
307
|
+
'dot': 'plaintext',
|
|
308
|
+
'graphviz': 'plaintext',
|
|
309
|
+
};
|
|
310
|
+
const mappedLang = langMap[language.toLowerCase()] || language.toLowerCase();
|
|
311
|
+
// Check if language is supported
|
|
312
|
+
if (!supportsLanguage(mappedLang) && mappedLang !== 'plaintext') {
|
|
313
|
+
// Fall back to plaintext styling for unsupported languages
|
|
314
|
+
const secondary = chalk.hex(theme.colors.secondary || theme.colors.primary);
|
|
315
|
+
return secondary(line);
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
// cli-highlight for supported languages
|
|
319
|
+
if (mappedLang === 'plaintext') {
|
|
320
|
+
const secondary = chalk.hex(theme.colors.secondary || theme.colors.primary);
|
|
321
|
+
return secondary(line);
|
|
322
|
+
}
|
|
323
|
+
return cliHighlight(line, { language: mappedLang, ignoreIllegals: true });
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
// Fallback to secondary color on error
|
|
327
|
+
const secondary = chalk.hex(theme.colors.secondary || theme.colors.primary);
|
|
328
|
+
return secondary(line);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Apply syntax highlighting to a raw markdown line for edit mode.
|
|
333
|
+
* Shows the raw markdown characters with visual styling.
|
|
334
|
+
*/
|
|
335
|
+
function highlightMarkdownLine(line, theme) {
|
|
336
|
+
const primary = chalk.hex(theme.colors.primary);
|
|
337
|
+
const secondary = chalk.hex(theme.colors.secondary || theme.colors.primary);
|
|
338
|
+
const muted = chalk.hex(theme.colors.muted);
|
|
339
|
+
// Empty line
|
|
340
|
+
if (!line.trim()) {
|
|
341
|
+
return line;
|
|
342
|
+
}
|
|
343
|
+
// Headers: # ## ### etc.
|
|
344
|
+
const headerMatch = line.match(/^(#{1,6})\s(.*)$/);
|
|
345
|
+
if (headerMatch) {
|
|
346
|
+
return primary(headerMatch[1]) + ' ' + primary.bold(headerMatch[2]);
|
|
347
|
+
}
|
|
348
|
+
// Blockquote: > text
|
|
349
|
+
const blockquoteMatch = line.match(/^(>\s*)(.*)$/);
|
|
350
|
+
if (blockquoteMatch) {
|
|
351
|
+
return muted(blockquoteMatch[1]) + muted.italic(blockquoteMatch[2]);
|
|
352
|
+
}
|
|
353
|
+
// Horizontal rule: --- or *** or ___
|
|
354
|
+
if (line.match(/^([-*_])\1{2,}\s*$/)) {
|
|
355
|
+
return muted(line);
|
|
356
|
+
}
|
|
357
|
+
// Unordered list: - or * or +
|
|
358
|
+
const ulMatch = line.match(/^(\s*)([-*+])\s+(.*)$/);
|
|
359
|
+
if (ulMatch) {
|
|
360
|
+
const [, indent, bullet, content] = ulMatch;
|
|
361
|
+
return indent + secondary(bullet) + ' ' + highlightInlineMarkdown(content, theme);
|
|
362
|
+
}
|
|
363
|
+
// Ordered list: 1. 2. etc.
|
|
364
|
+
const olMatch = line.match(/^(\s*)(\d+\.)\s+(.*)$/);
|
|
365
|
+
if (olMatch) {
|
|
366
|
+
const [, indent, num, content] = olMatch;
|
|
367
|
+
return indent + secondary(num) + ' ' + highlightInlineMarkdown(content, theme);
|
|
368
|
+
}
|
|
369
|
+
// Regular line - apply inline formatting
|
|
370
|
+
return highlightInlineMarkdown(line, theme);
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Apply inline markdown highlighting (bold, italic, code, links).
|
|
374
|
+
*/
|
|
375
|
+
function highlightInlineMarkdown(text, theme) {
|
|
376
|
+
const primary = chalk.hex(theme.colors.primary);
|
|
377
|
+
const secondary = chalk.hex(theme.colors.secondary || theme.colors.primary);
|
|
378
|
+
const muted = chalk.hex(theme.colors.muted);
|
|
379
|
+
const fg = chalk.hex(theme.colors.foreground);
|
|
380
|
+
let result = text;
|
|
381
|
+
// Images first (before links): 
|
|
382
|
+
result = result.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_match, alt, url) => {
|
|
383
|
+
return muted(' + secondary(url) + muted(')');
|
|
384
|
+
});
|
|
385
|
+
// Links: [text](url)
|
|
386
|
+
result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, linkText, url) => {
|
|
387
|
+
return muted('[') + primary(linkText) + muted('](') + secondary(url) + muted(')');
|
|
388
|
+
});
|
|
389
|
+
// Inline code: `code` (must be before bold/italic to avoid conflicts)
|
|
390
|
+
result = result.replace(/`([^`]+)`/g, (_match, code) => {
|
|
391
|
+
return muted('`') + secondary(code) + muted('`');
|
|
392
|
+
});
|
|
393
|
+
// Bold: **text** or __text__
|
|
394
|
+
result = result.replace(/(\*\*|__)(.+?)\1/g, (_match, marker, content) => {
|
|
395
|
+
return muted(marker) + fg.bold(content) + muted(marker);
|
|
396
|
+
});
|
|
397
|
+
// Italic: *text* or _text_
|
|
398
|
+
result = result.replace(/(\*|_)(.+?)\1/g, (_match, marker, content) => {
|
|
399
|
+
return muted(marker) + fg.italic(content) + muted(marker);
|
|
400
|
+
});
|
|
401
|
+
return result;
|
|
402
|
+
}
|
|
403
|
+
// =============================================================================
|
|
404
|
+
// Key Detection Helpers
|
|
405
|
+
// =============================================================================
|
|
406
|
+
function isNavigateUp(data) {
|
|
407
|
+
const str = data.toString();
|
|
408
|
+
return str === '\x1b[A' || str === 'k';
|
|
409
|
+
}
|
|
410
|
+
function isNavigateDown(data) {
|
|
411
|
+
const str = data.toString();
|
|
412
|
+
return str === '\x1b[B' || str === 'j';
|
|
413
|
+
}
|
|
414
|
+
function isArrowUp(data) {
|
|
415
|
+
return data.toString() === '\x1b[A';
|
|
416
|
+
}
|
|
417
|
+
function isArrowDown(data) {
|
|
418
|
+
return data.toString() === '\x1b[B';
|
|
419
|
+
}
|
|
420
|
+
function isArrowLeft(data) {
|
|
421
|
+
return data.toString() === '\x1b[D';
|
|
422
|
+
}
|
|
423
|
+
function isArrowRight(data) {
|
|
424
|
+
return data.toString() === '\x1b[C';
|
|
425
|
+
}
|
|
426
|
+
function isHome(data) {
|
|
427
|
+
const str = data.toString();
|
|
428
|
+
return str === '\x1b[H' || str === '\x1b[1~';
|
|
429
|
+
}
|
|
430
|
+
function isEnd(data) {
|
|
431
|
+
const str = data.toString();
|
|
432
|
+
return str === '\x1b[F' || str === '\x1b[4~';
|
|
433
|
+
}
|
|
434
|
+
function isPageUp(data) {
|
|
435
|
+
return data.toString() === '\x1b[5~';
|
|
436
|
+
}
|
|
437
|
+
function isPageDown(data) {
|
|
438
|
+
return data.toString() === '\x1b[6~';
|
|
439
|
+
}
|
|
440
|
+
function isSpace(data) {
|
|
441
|
+
return data.length === 1 && data[0] === 0x20;
|
|
442
|
+
}
|
|
443
|
+
function isBackspace(data) {
|
|
444
|
+
return data.length === 1 && (data[0] === 0x7f || data[0] === 0x08);
|
|
445
|
+
}
|
|
446
|
+
function isDelete(data) {
|
|
447
|
+
return data.toString() === '\x1b[3~';
|
|
448
|
+
}
|
|
449
|
+
function isEnter(data) {
|
|
450
|
+
return data.length === 1 && (data[0] === 0x0d || data[0] === 0x0a);
|
|
451
|
+
}
|
|
452
|
+
function isTab(data) {
|
|
453
|
+
return data.length === 1 && data[0] === 0x09;
|
|
454
|
+
}
|
|
455
|
+
function isCtrlS(data) {
|
|
456
|
+
return data.length === 1 && data[0] === 0x13;
|
|
457
|
+
}
|
|
458
|
+
function isEscape(data) {
|
|
459
|
+
return data.length === 1 && data[0] === 0x1b;
|
|
460
|
+
}
|
|
461
|
+
function isEditKey(data) {
|
|
462
|
+
const char = data.length === 1 && data[0] >= 0x20 && data[0] < 0x7f
|
|
463
|
+
? String.fromCharCode(data[0])
|
|
464
|
+
: null;
|
|
465
|
+
return char === 'e' || char === 'i';
|
|
466
|
+
}
|
|
467
|
+
function isPrintableChar(data) {
|
|
468
|
+
if (data.length === 1 && data[0] >= 0x20 && data[0] < 0x7f) {
|
|
469
|
+
return String.fromCharCode(data[0]);
|
|
470
|
+
}
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
// =============================================================================
|
|
474
|
+
// Document Detail Overlay V2
|
|
475
|
+
// =============================================================================
|
|
476
|
+
/**
|
|
477
|
+
* Fullscreen document viewer and editor overlay.
|
|
478
|
+
* Shows a single document with vim-style scrolling navigation.
|
|
479
|
+
* Supports edit mode for modifying raw markdown content.
|
|
480
|
+
*/
|
|
481
|
+
export class DocumentDetailOverlayV2 extends BaseOverlayV2 {
|
|
482
|
+
document;
|
|
483
|
+
// Use 'inline' type so terminal-ui doesn't clear screen - we manage alternate screen ourselves
|
|
484
|
+
type = 'inline';
|
|
485
|
+
id = 'document-detail-overlay-v2';
|
|
486
|
+
// Tell terminal-ui we manage our own screen buffer - skip clearing on close
|
|
487
|
+
usesAlternateScreen = true;
|
|
488
|
+
constructor(document) {
|
|
489
|
+
// Initialize state with rendered markdown
|
|
490
|
+
const termWidth = terminal.getTerminalWidth();
|
|
491
|
+
const contentLines = renderMarkdownSync(document.content, termWidth);
|
|
492
|
+
const rawLines = document.content.split('\n');
|
|
493
|
+
super({
|
|
494
|
+
contentLines,
|
|
495
|
+
scrollOffset: 0,
|
|
496
|
+
isEditMode: false,
|
|
497
|
+
rawLines,
|
|
498
|
+
cursorLine: 0,
|
|
499
|
+
cursorColumn: 0,
|
|
500
|
+
editScrollOffset: 0,
|
|
501
|
+
isDirty: false,
|
|
502
|
+
originalContent: document.content,
|
|
503
|
+
showSavePrompt: false,
|
|
504
|
+
saveMessage: null,
|
|
505
|
+
});
|
|
506
|
+
this.document = document;
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Enter alternate screen buffer when overlay mounts.
|
|
510
|
+
* This preserves the main screen content.
|
|
511
|
+
*/
|
|
512
|
+
onMount() {
|
|
513
|
+
enterAlternateScreen();
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Exit alternate screen buffer when overlay unmounts.
|
|
517
|
+
* This restores the main screen content.
|
|
518
|
+
*/
|
|
519
|
+
onUnmount() {
|
|
520
|
+
exitAlternateScreen();
|
|
521
|
+
}
|
|
522
|
+
renderContent(context) {
|
|
523
|
+
if (this.state.isEditMode) {
|
|
524
|
+
return this.renderEditMode(context);
|
|
525
|
+
}
|
|
526
|
+
return this.renderViewMode(context);
|
|
527
|
+
}
|
|
528
|
+
// ===========================================================================
|
|
529
|
+
// View Mode Rendering
|
|
530
|
+
// ===========================================================================
|
|
531
|
+
renderViewMode(context) {
|
|
532
|
+
const s = context.styles;
|
|
533
|
+
const cols = context.width;
|
|
534
|
+
const rows = context.height;
|
|
535
|
+
const border = renderBorder(cols, s);
|
|
536
|
+
const lines = [];
|
|
537
|
+
// Header
|
|
538
|
+
const typeLabel = DOC_TYPE_LABELS[this.document.docType] || this.document.docType.toUpperCase();
|
|
539
|
+
lines.push(border);
|
|
540
|
+
lines.push(` ${s.primaryBold(typeLabel)}${s.muted(' │ ')}${chalk.bold(this.document.title)}`);
|
|
541
|
+
lines.push(border);
|
|
542
|
+
// Calculate content area
|
|
543
|
+
const headerLines = 3;
|
|
544
|
+
const footerLines = 3;
|
|
545
|
+
const contentHeight = rows - headerLines - footerLines;
|
|
546
|
+
// Get visible lines
|
|
547
|
+
const totalLines = this.state.contentLines.length;
|
|
548
|
+
const visibleLines = [];
|
|
549
|
+
let physicalLinesUsed = 0;
|
|
550
|
+
for (let i = this.state.scrollOffset; i < totalLines && physicalLinesUsed < contentHeight; i++) {
|
|
551
|
+
const line = this.state.contentLines[i];
|
|
552
|
+
// eslint-disable-next-line no-control-regex
|
|
553
|
+
const visualLen = line.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
554
|
+
const linePhysicalHeight = Math.max(1, Math.ceil(visualLen / cols));
|
|
555
|
+
if (physicalLinesUsed + linePhysicalHeight <= contentHeight) {
|
|
556
|
+
visibleLines.push(line);
|
|
557
|
+
physicalLinesUsed += linePhysicalHeight;
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
// Content
|
|
564
|
+
for (const line of visibleLines) {
|
|
565
|
+
lines.push(line);
|
|
566
|
+
}
|
|
567
|
+
// Pad remaining space
|
|
568
|
+
for (let i = physicalLinesUsed; i < contentHeight; i++) {
|
|
569
|
+
lines.push('');
|
|
570
|
+
}
|
|
571
|
+
// Footer with position info
|
|
572
|
+
const canScrollUp = this.state.scrollOffset > 0;
|
|
573
|
+
const canScrollDown = this.state.scrollOffset + contentHeight < totalLines;
|
|
574
|
+
const arrows = (canScrollUp ? '▲' : ' ') + (canScrollDown ? '▼' : ' ');
|
|
575
|
+
const position = totalLines > 0
|
|
576
|
+
? `Line ${String(this.state.scrollOffset + 1)}-${String(Math.min(this.state.scrollOffset + contentHeight, totalLines))} of ${String(totalLines)}`
|
|
577
|
+
: '';
|
|
578
|
+
const scrollPercent = totalLines > contentHeight
|
|
579
|
+
? Math.round(((this.state.scrollOffset + contentHeight) / totalLines) * 100)
|
|
580
|
+
: 100;
|
|
581
|
+
lines.push(border);
|
|
582
|
+
lines.push(s.muted(` ${arrows} ${position} (${String(scrollPercent)}%) ↑↓/jk Scroll · e/i Edit · q/Esc Back`));
|
|
583
|
+
lines.push(border);
|
|
584
|
+
return lines;
|
|
585
|
+
}
|
|
586
|
+
// ===========================================================================
|
|
587
|
+
// Edit Mode Rendering
|
|
588
|
+
// ===========================================================================
|
|
589
|
+
renderEditMode(context) {
|
|
590
|
+
const s = context.styles;
|
|
591
|
+
const cols = context.width;
|
|
592
|
+
const rows = context.height;
|
|
593
|
+
const border = renderBorder(cols, s);
|
|
594
|
+
const lines = [];
|
|
595
|
+
// Header with [EDIT] indicator
|
|
596
|
+
const typeLabel = DOC_TYPE_LABELS[this.document.docType] || this.document.docType.toUpperCase();
|
|
597
|
+
const editIndicator = this.state.isDirty ? s.warning('[EDIT*]') : s.primary('[EDIT]');
|
|
598
|
+
lines.push(border);
|
|
599
|
+
lines.push(` ${s.primaryBold(typeLabel)}${s.muted(' │ ')}${chalk.bold(this.document.title)} ${editIndicator}`);
|
|
600
|
+
lines.push(border);
|
|
601
|
+
// Calculate content area
|
|
602
|
+
const headerLines = 3;
|
|
603
|
+
const footerLines = 3;
|
|
604
|
+
const contentHeight = rows - headerLines - footerLines;
|
|
605
|
+
// Ensure cursor is visible (adjust scroll if needed)
|
|
606
|
+
this.ensureCursorVisible(contentHeight);
|
|
607
|
+
// Get visible lines with line numbers
|
|
608
|
+
const totalLines = this.state.rawLines.length;
|
|
609
|
+
const lineNumWidth = String(totalLines).length + 1;
|
|
610
|
+
// Get current theme for syntax highlighting
|
|
611
|
+
const theme = getCurrentTheme();
|
|
612
|
+
// Pre-compute code block state for all lines up to and including visible area
|
|
613
|
+
// This ensures we know if we're inside a code block when rendering
|
|
614
|
+
let codeBlockState = { inCodeBlock: false, language: '', fenceChar: '', fenceLength: 0 };
|
|
615
|
+
const lineStates = [];
|
|
616
|
+
for (let i = 0; i < totalLines; i++) {
|
|
617
|
+
const line = this.state.rawLines[i];
|
|
618
|
+
// Check for code fence (we only need to track state, not highlight)
|
|
619
|
+
const fenceMatch = line.match(/^(`{3,}|~{3,})(\w*)\s*$/);
|
|
620
|
+
if (fenceMatch) {
|
|
621
|
+
const [, fence, lang] = fenceMatch;
|
|
622
|
+
const fenceChar = fence[0];
|
|
623
|
+
const fenceLength = fence.length;
|
|
624
|
+
if (!codeBlockState.inCodeBlock) {
|
|
625
|
+
codeBlockState = { inCodeBlock: true, language: lang || 'plaintext', fenceChar, fenceLength };
|
|
626
|
+
}
|
|
627
|
+
else if (fenceChar === codeBlockState.fenceChar && fenceLength >= codeBlockState.fenceLength) {
|
|
628
|
+
codeBlockState = { inCodeBlock: false, language: '', fenceChar: '', fenceLength: 0 };
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
lineStates[i] = { ...codeBlockState };
|
|
632
|
+
}
|
|
633
|
+
for (let i = 0; i < contentHeight; i++) {
|
|
634
|
+
const lineIndex = this.state.editScrollOffset + i;
|
|
635
|
+
if (lineIndex < totalLines) {
|
|
636
|
+
const lineNum = String(lineIndex + 1).padStart(lineNumWidth);
|
|
637
|
+
const lineContent = this.state.rawLines[lineIndex];
|
|
638
|
+
const isCursorLine = lineIndex === this.state.cursorLine;
|
|
639
|
+
// Render line with cursor if this is the cursor line (no highlighting for accurate positioning)
|
|
640
|
+
if (isCursorLine) {
|
|
641
|
+
const renderedLine = this.renderLineWithCursor(lineContent, this.state.cursorColumn, cols - lineNumWidth - 2);
|
|
642
|
+
lines.push(s.muted(`${lineNum}│`) + renderedLine);
|
|
643
|
+
}
|
|
644
|
+
else {
|
|
645
|
+
// Apply syntax highlighting to non-cursor lines
|
|
646
|
+
const maxLen = cols - lineNumWidth - 2;
|
|
647
|
+
const truncated = lineContent.length > maxLen;
|
|
648
|
+
const displayContent = truncated
|
|
649
|
+
? lineContent.slice(0, maxLen - 1)
|
|
650
|
+
: lineContent;
|
|
651
|
+
// Get the state BEFORE this line to know if we're inside a code block
|
|
652
|
+
const prevState = lineIndex > 0 ? lineStates[lineIndex - 1] : { inCodeBlock: false, language: '', fenceChar: '', fenceLength: 0 };
|
|
653
|
+
const { highlighted } = highlightEditLine(displayContent, prevState, theme);
|
|
654
|
+
lines.push(s.muted(`${lineNum}│`) + highlighted + (truncated ? s.muted('…') : ''));
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
else {
|
|
658
|
+
// Empty line beyond document
|
|
659
|
+
const lineNum = ' '.repeat(lineNumWidth);
|
|
660
|
+
lines.push(s.muted(`${lineNum}│`) + s.muted('~'));
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
// Footer
|
|
664
|
+
lines.push(border);
|
|
665
|
+
if (this.state.showSavePrompt) {
|
|
666
|
+
// Save prompt
|
|
667
|
+
lines.push(s.warning(' Unsaved changes. Save? ') + s.primary('(y)') + 'es / ' + s.primary('(n)') + 'o / ' + s.primary('(c)') + 'ancel');
|
|
668
|
+
}
|
|
669
|
+
else if (this.state.saveMessage) {
|
|
670
|
+
// Brief save message
|
|
671
|
+
lines.push(s.success(` ${this.state.saveMessage}`));
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
// Normal edit mode footer
|
|
675
|
+
const cursorPos = `Ln ${String(this.state.cursorLine + 1)}, Col ${String(this.state.cursorColumn + 1)}`;
|
|
676
|
+
const modifiedIndicator = this.state.isDirty ? s.warning('[modified]') : '';
|
|
677
|
+
lines.push(s.primaryBold(' ── INSERT ── ') + s.muted(cursorPos) + ' ' + modifiedIndicator + s.muted(' Ctrl+S Save · Esc Exit'));
|
|
678
|
+
}
|
|
679
|
+
lines.push(border);
|
|
680
|
+
return lines;
|
|
681
|
+
}
|
|
682
|
+
renderLineWithCursor(line, cursorCol, maxLen) {
|
|
683
|
+
const theme = getCurrentTheme();
|
|
684
|
+
const cursorStyle = chalk.inverse;
|
|
685
|
+
// Handle cursor beyond line end
|
|
686
|
+
const effectiveCursorCol = Math.min(cursorCol, line.length);
|
|
687
|
+
// Calculate visible portion
|
|
688
|
+
let viewStart = 0;
|
|
689
|
+
if (effectiveCursorCol >= maxLen - 5) {
|
|
690
|
+
viewStart = effectiveCursorCol - maxLen + 10;
|
|
691
|
+
}
|
|
692
|
+
viewStart = Math.max(0, viewStart);
|
|
693
|
+
const viewEnd = viewStart + maxLen;
|
|
694
|
+
const visibleLine = line.slice(viewStart, viewEnd);
|
|
695
|
+
const cursorPosInView = effectiveCursorCol - viewStart;
|
|
696
|
+
// Build the line with cursor
|
|
697
|
+
let result = '';
|
|
698
|
+
for (let i = 0; i < visibleLine.length; i++) {
|
|
699
|
+
if (i === cursorPosInView) {
|
|
700
|
+
result += cursorStyle(visibleLine[i]);
|
|
701
|
+
}
|
|
702
|
+
else {
|
|
703
|
+
result += visibleLine[i];
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
// If cursor is at end of line (or beyond), show cursor as block
|
|
707
|
+
if (cursorPosInView >= visibleLine.length) {
|
|
708
|
+
result += cursorStyle(' ');
|
|
709
|
+
}
|
|
710
|
+
// Add truncation indicator
|
|
711
|
+
if (viewStart > 0) {
|
|
712
|
+
result = chalk.hex(theme.colors.muted)('…') + result.slice(1);
|
|
713
|
+
}
|
|
714
|
+
if (line.length > viewEnd) {
|
|
715
|
+
result = result.slice(0, -1) + chalk.hex(theme.colors.muted)('…');
|
|
716
|
+
}
|
|
717
|
+
return result;
|
|
718
|
+
}
|
|
719
|
+
ensureCursorVisible(contentHeight) {
|
|
720
|
+
// Scroll up if cursor is above visible area
|
|
721
|
+
if (this.state.cursorLine < this.state.editScrollOffset) {
|
|
722
|
+
this.state.editScrollOffset = this.state.cursorLine;
|
|
723
|
+
}
|
|
724
|
+
// Scroll down if cursor is below visible area
|
|
725
|
+
if (this.state.cursorLine >= this.state.editScrollOffset + contentHeight) {
|
|
726
|
+
this.state.editScrollOffset = this.state.cursorLine - contentHeight + 1;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
// ===========================================================================
|
|
730
|
+
// Key Handling
|
|
731
|
+
// ===========================================================================
|
|
732
|
+
handleKey(key) {
|
|
733
|
+
const data = key.raw;
|
|
734
|
+
// Handle save prompt state
|
|
735
|
+
if (this.state.showSavePrompt) {
|
|
736
|
+
return this.handleSavePromptKey(data);
|
|
737
|
+
}
|
|
738
|
+
// Handle edit mode
|
|
739
|
+
if (this.state.isEditMode) {
|
|
740
|
+
return this.handleEditModeKey(data);
|
|
741
|
+
}
|
|
742
|
+
// Handle view mode
|
|
743
|
+
return this.handleViewModeKey(data);
|
|
744
|
+
}
|
|
745
|
+
// ===========================================================================
|
|
746
|
+
// View Mode Key Handling
|
|
747
|
+
// ===========================================================================
|
|
748
|
+
handleViewModeKey(data) {
|
|
749
|
+
const char = isPrintableChar(data);
|
|
750
|
+
const keyStr = data.toString();
|
|
751
|
+
const rows = terminal.getTerminalHeight();
|
|
752
|
+
const contentHeight = rows - 6;
|
|
753
|
+
const totalLines = this.state.contentLines.length;
|
|
754
|
+
const maxScroll = Math.max(0, totalLines - contentHeight);
|
|
755
|
+
// Ctrl+C closes everything (no going back)
|
|
756
|
+
if (isCtrlC(data)) {
|
|
757
|
+
return this.close({ goBack: false });
|
|
758
|
+
}
|
|
759
|
+
// Escape or q - go back to list
|
|
760
|
+
if (isClose(data)) {
|
|
761
|
+
return this.close({ goBack: true });
|
|
762
|
+
}
|
|
763
|
+
// e or i - enter edit mode
|
|
764
|
+
if (isEditKey(data)) {
|
|
765
|
+
this.state.isEditMode = true;
|
|
766
|
+
this.state.cursorLine = 0;
|
|
767
|
+
this.state.cursorColumn = 0;
|
|
768
|
+
this.state.editScrollOffset = 0;
|
|
769
|
+
this.state.saveMessage = null;
|
|
770
|
+
return this.rerender();
|
|
771
|
+
}
|
|
772
|
+
// Up / k
|
|
773
|
+
if (isNavigateUp(data)) {
|
|
774
|
+
if (this.state.scrollOffset > 0) {
|
|
775
|
+
this.state.scrollOffset--;
|
|
776
|
+
return this.rerender();
|
|
777
|
+
}
|
|
778
|
+
return this.noAction();
|
|
779
|
+
}
|
|
780
|
+
// Down / j
|
|
781
|
+
if (isNavigateDown(data)) {
|
|
782
|
+
if (this.state.scrollOffset < maxScroll) {
|
|
783
|
+
this.state.scrollOffset++;
|
|
784
|
+
return this.rerender();
|
|
785
|
+
}
|
|
786
|
+
return this.noAction();
|
|
787
|
+
}
|
|
788
|
+
// Page Up
|
|
789
|
+
if (keyStr === '\x1b[5~') {
|
|
790
|
+
this.state.scrollOffset = Math.max(0, this.state.scrollOffset - contentHeight);
|
|
791
|
+
return this.rerender();
|
|
792
|
+
}
|
|
793
|
+
// Page Down / Space
|
|
794
|
+
if (keyStr === '\x1b[6~' || isSpace(data)) {
|
|
795
|
+
this.state.scrollOffset = Math.min(maxScroll, this.state.scrollOffset + contentHeight);
|
|
796
|
+
return this.rerender();
|
|
797
|
+
}
|
|
798
|
+
// g = top
|
|
799
|
+
if (char === 'g') {
|
|
800
|
+
this.state.scrollOffset = 0;
|
|
801
|
+
return this.rerender();
|
|
802
|
+
}
|
|
803
|
+
// G = bottom
|
|
804
|
+
if (char === 'G') {
|
|
805
|
+
this.state.scrollOffset = maxScroll;
|
|
806
|
+
return this.rerender();
|
|
807
|
+
}
|
|
808
|
+
// Home
|
|
809
|
+
if (keyStr === '\x1b[H') {
|
|
810
|
+
this.state.scrollOffset = 0;
|
|
811
|
+
return this.rerender();
|
|
812
|
+
}
|
|
813
|
+
// End
|
|
814
|
+
if (keyStr === '\x1b[F') {
|
|
815
|
+
this.state.scrollOffset = maxScroll;
|
|
816
|
+
return this.rerender();
|
|
817
|
+
}
|
|
818
|
+
return this.noAction();
|
|
819
|
+
}
|
|
820
|
+
// ===========================================================================
|
|
821
|
+
// Edit Mode Key Handling
|
|
822
|
+
// ===========================================================================
|
|
823
|
+
handleEditModeKey(data) {
|
|
824
|
+
// Clear save message on any key
|
|
825
|
+
if (this.state.saveMessage) {
|
|
826
|
+
this.state.saveMessage = null;
|
|
827
|
+
}
|
|
828
|
+
// Ctrl+C - exit immediately without saving
|
|
829
|
+
if (isCtrlC(data)) {
|
|
830
|
+
return this.close({ goBack: false });
|
|
831
|
+
}
|
|
832
|
+
// Ctrl+S - save
|
|
833
|
+
if (isCtrlS(data)) {
|
|
834
|
+
this.saveDocument();
|
|
835
|
+
return this.rerender();
|
|
836
|
+
}
|
|
837
|
+
// Escape - exit edit mode (prompt if dirty)
|
|
838
|
+
if (isEscape(data)) {
|
|
839
|
+
if (this.state.isDirty) {
|
|
840
|
+
this.state.showSavePrompt = true;
|
|
841
|
+
return this.rerender();
|
|
842
|
+
}
|
|
843
|
+
// Exit edit mode, re-render markdown
|
|
844
|
+
return this.exitEditMode();
|
|
845
|
+
}
|
|
846
|
+
// Navigation
|
|
847
|
+
if (isArrowUp(data)) {
|
|
848
|
+
return this.moveCursorUp();
|
|
849
|
+
}
|
|
850
|
+
if (isArrowDown(data)) {
|
|
851
|
+
return this.moveCursorDown();
|
|
852
|
+
}
|
|
853
|
+
if (isArrowLeft(data)) {
|
|
854
|
+
return this.moveCursorLeft();
|
|
855
|
+
}
|
|
856
|
+
if (isArrowRight(data)) {
|
|
857
|
+
return this.moveCursorRight();
|
|
858
|
+
}
|
|
859
|
+
if (isHome(data)) {
|
|
860
|
+
this.state.cursorColumn = 0;
|
|
861
|
+
return this.rerender();
|
|
862
|
+
}
|
|
863
|
+
if (isEnd(data)) {
|
|
864
|
+
this.state.cursorColumn = this.getCurrentLine().length;
|
|
865
|
+
return this.rerender();
|
|
866
|
+
}
|
|
867
|
+
if (isPageUp(data)) {
|
|
868
|
+
const rows = terminal.getTerminalHeight();
|
|
869
|
+
const pageSize = rows - 6;
|
|
870
|
+
this.state.cursorLine = Math.max(0, this.state.cursorLine - pageSize);
|
|
871
|
+
this.clampCursorColumn();
|
|
872
|
+
return this.rerender();
|
|
873
|
+
}
|
|
874
|
+
if (isPageDown(data)) {
|
|
875
|
+
const rows = terminal.getTerminalHeight();
|
|
876
|
+
const pageSize = rows - 6;
|
|
877
|
+
this.state.cursorLine = Math.min(this.state.rawLines.length - 1, this.state.cursorLine + pageSize);
|
|
878
|
+
this.clampCursorColumn();
|
|
879
|
+
return this.rerender();
|
|
880
|
+
}
|
|
881
|
+
// Editing
|
|
882
|
+
if (isBackspace(data)) {
|
|
883
|
+
return this.handleBackspace();
|
|
884
|
+
}
|
|
885
|
+
if (isDelete(data)) {
|
|
886
|
+
return this.handleDelete();
|
|
887
|
+
}
|
|
888
|
+
if (isEnter(data)) {
|
|
889
|
+
return this.handleEnter();
|
|
890
|
+
}
|
|
891
|
+
if (isTab(data)) {
|
|
892
|
+
return this.insertText(' '.repeat(TAB_SIZE));
|
|
893
|
+
}
|
|
894
|
+
// Printable characters
|
|
895
|
+
const char = isPrintableChar(data);
|
|
896
|
+
if (char) {
|
|
897
|
+
return this.insertText(char);
|
|
898
|
+
}
|
|
899
|
+
return this.noAction();
|
|
900
|
+
}
|
|
901
|
+
// ===========================================================================
|
|
902
|
+
// Save Prompt Key Handling
|
|
903
|
+
// ===========================================================================
|
|
904
|
+
handleSavePromptKey(data) {
|
|
905
|
+
const char = isPrintableChar(data);
|
|
906
|
+
// y = save and exit edit mode
|
|
907
|
+
if (char === 'y' || char === 'Y') {
|
|
908
|
+
this.saveDocument();
|
|
909
|
+
this.state.showSavePrompt = false;
|
|
910
|
+
return this.exitEditMode();
|
|
911
|
+
}
|
|
912
|
+
// n = discard and exit edit mode
|
|
913
|
+
if (char === 'n' || char === 'N') {
|
|
914
|
+
// Revert to original content
|
|
915
|
+
this.state.rawLines = this.state.originalContent.split('\n');
|
|
916
|
+
this.state.isDirty = false;
|
|
917
|
+
this.state.showSavePrompt = false;
|
|
918
|
+
return this.exitEditMode();
|
|
919
|
+
}
|
|
920
|
+
// c or Escape = cancel, stay in edit mode
|
|
921
|
+
if (char === 'c' || char === 'C' || isEscape(data)) {
|
|
922
|
+
this.state.showSavePrompt = false;
|
|
923
|
+
return this.rerender();
|
|
924
|
+
}
|
|
925
|
+
// Ctrl+C = exit everything
|
|
926
|
+
if (isCtrlC(data)) {
|
|
927
|
+
return this.close({ goBack: false });
|
|
928
|
+
}
|
|
929
|
+
return this.noAction();
|
|
930
|
+
}
|
|
931
|
+
// ===========================================================================
|
|
932
|
+
// Cursor Movement
|
|
933
|
+
// ===========================================================================
|
|
934
|
+
moveCursorUp() {
|
|
935
|
+
if (this.state.cursorLine > 0) {
|
|
936
|
+
this.state.cursorLine--;
|
|
937
|
+
this.clampCursorColumn();
|
|
938
|
+
}
|
|
939
|
+
return this.rerender();
|
|
940
|
+
}
|
|
941
|
+
moveCursorDown() {
|
|
942
|
+
if (this.state.cursorLine < this.state.rawLines.length - 1) {
|
|
943
|
+
this.state.cursorLine++;
|
|
944
|
+
this.clampCursorColumn();
|
|
945
|
+
}
|
|
946
|
+
return this.rerender();
|
|
947
|
+
}
|
|
948
|
+
moveCursorLeft() {
|
|
949
|
+
if (this.state.cursorColumn > 0) {
|
|
950
|
+
this.state.cursorColumn--;
|
|
951
|
+
}
|
|
952
|
+
else if (this.state.cursorLine > 0) {
|
|
953
|
+
// Wrap to end of previous line
|
|
954
|
+
this.state.cursorLine--;
|
|
955
|
+
this.state.cursorColumn = this.getCurrentLine().length;
|
|
956
|
+
}
|
|
957
|
+
return this.rerender();
|
|
958
|
+
}
|
|
959
|
+
moveCursorRight() {
|
|
960
|
+
const lineLen = this.getCurrentLine().length;
|
|
961
|
+
if (this.state.cursorColumn < lineLen) {
|
|
962
|
+
this.state.cursorColumn++;
|
|
963
|
+
}
|
|
964
|
+
else if (this.state.cursorLine < this.state.rawLines.length - 1) {
|
|
965
|
+
// Wrap to start of next line
|
|
966
|
+
this.state.cursorLine++;
|
|
967
|
+
this.state.cursorColumn = 0;
|
|
968
|
+
}
|
|
969
|
+
return this.rerender();
|
|
970
|
+
}
|
|
971
|
+
clampCursorColumn() {
|
|
972
|
+
const lineLen = this.getCurrentLine().length;
|
|
973
|
+
this.state.cursorColumn = Math.min(this.state.cursorColumn, lineLen);
|
|
974
|
+
}
|
|
975
|
+
getCurrentLine() {
|
|
976
|
+
return this.state.rawLines[this.state.cursorLine] || '';
|
|
977
|
+
}
|
|
978
|
+
// ===========================================================================
|
|
979
|
+
// Text Editing
|
|
980
|
+
// ===========================================================================
|
|
981
|
+
insertText(text) {
|
|
982
|
+
const line = this.getCurrentLine();
|
|
983
|
+
const before = line.slice(0, this.state.cursorColumn);
|
|
984
|
+
const after = line.slice(this.state.cursorColumn);
|
|
985
|
+
this.state.rawLines[this.state.cursorLine] = before + text + after;
|
|
986
|
+
this.state.cursorColumn += text.length;
|
|
987
|
+
this.state.isDirty = true;
|
|
988
|
+
return this.rerender();
|
|
989
|
+
}
|
|
990
|
+
handleBackspace() {
|
|
991
|
+
if (this.state.cursorColumn > 0) {
|
|
992
|
+
// Delete character before cursor
|
|
993
|
+
const line = this.getCurrentLine();
|
|
994
|
+
const before = line.slice(0, this.state.cursorColumn - 1);
|
|
995
|
+
const after = line.slice(this.state.cursorColumn);
|
|
996
|
+
this.state.rawLines[this.state.cursorLine] = before + after;
|
|
997
|
+
this.state.cursorColumn--;
|
|
998
|
+
this.state.isDirty = true;
|
|
999
|
+
}
|
|
1000
|
+
else if (this.state.cursorLine > 0) {
|
|
1001
|
+
// Join with previous line
|
|
1002
|
+
const currentLine = this.getCurrentLine();
|
|
1003
|
+
const prevLine = this.state.rawLines[this.state.cursorLine - 1];
|
|
1004
|
+
this.state.rawLines[this.state.cursorLine - 1] = prevLine + currentLine;
|
|
1005
|
+
this.state.rawLines.splice(this.state.cursorLine, 1);
|
|
1006
|
+
this.state.cursorLine--;
|
|
1007
|
+
this.state.cursorColumn = prevLine.length;
|
|
1008
|
+
this.state.isDirty = true;
|
|
1009
|
+
}
|
|
1010
|
+
return this.rerender();
|
|
1011
|
+
}
|
|
1012
|
+
handleDelete() {
|
|
1013
|
+
const line = this.getCurrentLine();
|
|
1014
|
+
if (this.state.cursorColumn < line.length) {
|
|
1015
|
+
// Delete character at cursor
|
|
1016
|
+
const before = line.slice(0, this.state.cursorColumn);
|
|
1017
|
+
const after = line.slice(this.state.cursorColumn + 1);
|
|
1018
|
+
this.state.rawLines[this.state.cursorLine] = before + after;
|
|
1019
|
+
this.state.isDirty = true;
|
|
1020
|
+
}
|
|
1021
|
+
else if (this.state.cursorLine < this.state.rawLines.length - 1) {
|
|
1022
|
+
// Join with next line
|
|
1023
|
+
const nextLine = this.state.rawLines[this.state.cursorLine + 1];
|
|
1024
|
+
this.state.rawLines[this.state.cursorLine] = line + nextLine;
|
|
1025
|
+
this.state.rawLines.splice(this.state.cursorLine + 1, 1);
|
|
1026
|
+
this.state.isDirty = true;
|
|
1027
|
+
}
|
|
1028
|
+
return this.rerender();
|
|
1029
|
+
}
|
|
1030
|
+
handleEnter() {
|
|
1031
|
+
const line = this.getCurrentLine();
|
|
1032
|
+
const before = line.slice(0, this.state.cursorColumn);
|
|
1033
|
+
const after = line.slice(this.state.cursorColumn);
|
|
1034
|
+
this.state.rawLines[this.state.cursorLine] = before;
|
|
1035
|
+
this.state.rawLines.splice(this.state.cursorLine + 1, 0, after);
|
|
1036
|
+
this.state.cursorLine++;
|
|
1037
|
+
this.state.cursorColumn = 0;
|
|
1038
|
+
this.state.isDirty = true;
|
|
1039
|
+
return this.rerender();
|
|
1040
|
+
}
|
|
1041
|
+
// ===========================================================================
|
|
1042
|
+
// Save and Exit
|
|
1043
|
+
// ===========================================================================
|
|
1044
|
+
saveDocument() {
|
|
1045
|
+
const content = this.state.rawLines.join('\n');
|
|
1046
|
+
try {
|
|
1047
|
+
documentRepository.update(this.document.id, { content });
|
|
1048
|
+
this.state.originalContent = content;
|
|
1049
|
+
this.state.isDirty = false;
|
|
1050
|
+
this.state.saveMessage = 'Saved!';
|
|
1051
|
+
// Clear message after a delay (will be cleared on next keypress anyway)
|
|
1052
|
+
setTimeout(() => {
|
|
1053
|
+
if (this.state.saveMessage === 'Saved!') {
|
|
1054
|
+
this.state.saveMessage = null;
|
|
1055
|
+
}
|
|
1056
|
+
}, 2000);
|
|
1057
|
+
}
|
|
1058
|
+
catch (error) {
|
|
1059
|
+
this.state.saveMessage = `Error: ${error.message}`;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
exitEditMode() {
|
|
1063
|
+
this.state.isEditMode = false;
|
|
1064
|
+
// Re-render markdown from current content
|
|
1065
|
+
const content = this.state.rawLines.join('\n');
|
|
1066
|
+
const termWidth = terminal.getTerminalWidth();
|
|
1067
|
+
this.state.contentLines = renderMarkdownSync(content, termWidth);
|
|
1068
|
+
this.state.scrollOffset = 0;
|
|
1069
|
+
return this.rerender();
|
|
1070
|
+
}
|
|
1071
|
+
}
|