march-cli 0.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.
- package/bin/march.mjs +13 -0
- package/package.json +36 -0
- package/src/agent/command-exec-tool.mjs +91 -0
- package/src/agent/context-stats-tool.mjs +57 -0
- package/src/agent/editing/diff-apply.mjs +28 -0
- package/src/agent/editing/diff-format.mjs +57 -0
- package/src/agent/file-edit-tool.mjs +276 -0
- package/src/agent/find-tool.mjs +112 -0
- package/src/agent/model-payload-dumper.mjs +201 -0
- package/src/agent/pi-session/pi-session-sidecar-failure.mjs +10 -0
- package/src/agent/provider/payload-messages.mjs +138 -0
- package/src/agent/read-file-tool.mjs +112 -0
- package/src/agent/runner/fast-model.mjs +36 -0
- package/src/agent/runner/runner-cleanup.mjs +12 -0
- package/src/agent/runner/runner-init.mjs +15 -0
- package/src/agent/runner/runner-session-state.mjs +40 -0
- package/src/agent/runner.mjs +266 -0
- package/src/agent/runtime/runner-runtime-host.mjs +73 -0
- package/src/agent/runtime/runtime-factory.mjs +42 -0
- package/src/agent/runtime/runtime-host.mjs +34 -0
- package/src/agent/session/session-auto-name.mjs +41 -0
- package/src/agent/session/session-binding.mjs +12 -0
- package/src/agent/session/session-options.mjs +46 -0
- package/src/agent/tool-names.mjs +1 -0
- package/src/agent/tool-result.mjs +3 -0
- package/src/agent/tools.mjs +54 -0
- package/src/agent/turn/turn-events.mjs +64 -0
- package/src/agent/turn/turn-runner.mjs +103 -0
- package/src/auth/login-command.mjs +90 -0
- package/src/auth/storage.mjs +33 -0
- package/src/cli/args.mjs +71 -0
- package/src/cli/commands/copy-command.mjs +73 -0
- package/src/cli/commands/export-command.mjs +206 -0
- package/src/cli/commands/extensions-command.mjs +53 -0
- package/src/cli/commands/help-command.mjs +7 -0
- package/src/cli/commands/model-command.mjs +110 -0
- package/src/cli/commands/paste-image-command.mjs +43 -0
- package/src/cli/commands/provider-command.mjs +55 -0
- package/src/cli/commands/status-command.mjs +157 -0
- package/src/cli/commands/thinking-command.mjs +80 -0
- package/src/cli/fallback-ui.mjs +156 -0
- package/src/cli/input/attachment-tokens.mjs +20 -0
- package/src/cli/input/autocomplete.mjs +106 -0
- package/src/cli/input/external-editor.mjs +39 -0
- package/src/cli/input/history-store.mjs +35 -0
- package/src/cli/input/image-clipboard.mjs +55 -0
- package/src/cli/input/keybinding-dispatch.mjs +76 -0
- package/src/cli/input/keybindings.mjs +96 -0
- package/src/cli/input/mode-state.mjs +43 -0
- package/src/cli/input/prompt-templates.mjs +84 -0
- package/src/cli/input/select-with-keyboard.mjs +67 -0
- package/src/cli/permissions.mjs +103 -0
- package/src/cli/repl-commands.mjs +86 -0
- package/src/cli/repl-loop.mjs +157 -0
- package/src/cli/selector-list.mjs +21 -0
- package/src/cli/session/pi-session-switch-command.mjs +41 -0
- package/src/cli/session/session-command.mjs +23 -0
- package/src/cli/session/session-list-command.mjs +68 -0
- package/src/cli/session/session-name-command.mjs +26 -0
- package/src/cli/session/session-source-command.mjs +89 -0
- package/src/cli/session/session-switch-command.mjs +1 -0
- package/src/cli/shell/shell-command.mjs +55 -0
- package/src/cli/shell/shell-drawer-controls.mjs +33 -0
- package/src/cli/shell/shell-drawer.mjs +192 -0
- package/src/cli/shell/shell-split-layout.mjs +70 -0
- package/src/cli/slash-commands.mjs +176 -0
- package/src/cli/startup/startup-banner.mjs +17 -0
- package/src/cli/startup/startup-session.mjs +51 -0
- package/src/cli/status-line-updater.mjs +74 -0
- package/src/cli/tool-output.mjs +9 -0
- package/src/cli/tui/editor/external-editor-runner.mjs +24 -0
- package/src/cli/tui/input/mouse-selection-controller.mjs +89 -0
- package/src/cli/tui/input/mouse-tracking.mjs +20 -0
- package/src/cli/tui/layout/main-pane-layout.mjs +38 -0
- package/src/cli/tui/layout/safe-render-boundary.mjs +46 -0
- package/src/cli/tui/markdown-renderer.mjs +279 -0
- package/src/cli/tui/output/scroll-state.mjs +79 -0
- package/src/cli/tui/output/tool-card-renderer.mjs +59 -0
- package/src/cli/tui/output-buffer.mjs +297 -0
- package/src/cli/tui/permission-request-ui.mjs +18 -0
- package/src/cli/tui/recall-rendering.mjs +25 -0
- package/src/cli/tui/select/editor-select-list.mjs +111 -0
- package/src/cli/tui/selection-screen.mjs +212 -0
- package/src/cli/tui/status/retry-status.mjs +72 -0
- package/src/cli/tui/status/spinner-status.mjs +42 -0
- package/src/cli/tui/status/status-bar.mjs +88 -0
- package/src/cli/tui/syntax/highlighting.mjs +277 -0
- package/src/cli/tui/syntax/languages.mjs +91 -0
- package/src/cli/tui/syntax/tree-sitter/bash.highlights.scm +261 -0
- package/src/cli/tui/syntax/tree-sitter/c.highlights.scm +341 -0
- package/src/cli/tui/syntax/tree-sitter/cpp.highlights.scm +268 -0
- package/src/cli/tui/syntax/tree-sitter/csharp.highlights.scm +577 -0
- package/src/cli/tui/syntax/tree-sitter/css.highlights.scm +109 -0
- package/src/cli/tui/syntax/tree-sitter/diff.highlights.scm +49 -0
- package/src/cli/tui/syntax/tree-sitter/go.highlights.scm +254 -0
- package/src/cli/tui/syntax/tree-sitter/html.highlights.scm +13 -0
- package/src/cli/tui/syntax/tree-sitter/java.highlights.scm +330 -0
- package/src/cli/tui/syntax/tree-sitter/json.highlights.scm +38 -0
- package/src/cli/tui/syntax/tree-sitter/php.highlights.scm +203 -0
- package/src/cli/tui/syntax/tree-sitter/python.highlights.scm +137 -0
- package/src/cli/tui/syntax/tree-sitter/ruby.highlights.scm +309 -0
- package/src/cli/tui/syntax/tree-sitter/rust.highlights.scm +531 -0
- package/src/cli/tui/syntax/tree-sitter/toml.highlights.scm +39 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-bash.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-c-sharp.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-c.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-cpp.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-css.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-diff.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-go.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-html.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-java.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-json.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-php.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-python.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-ruby.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-rust.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-toml.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-tsx.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-typescript.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-yaml.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tsx.highlights.scm +35 -0
- package/src/cli/tui/syntax/tree-sitter/typescript.highlights.scm +35 -0
- package/src/cli/tui/syntax/tree-sitter/yaml.highlights.scm +99 -0
- package/src/cli/tui/tool-rendering.mjs +194 -0
- package/src/cli/tui/tui-diff-rendering.mjs +157 -0
- package/src/cli/tui/tui-handlers.mjs +110 -0
- package/src/cli/tui/tui-input-controller.mjs +61 -0
- package/src/cli/tui/ui-theme.mjs +148 -0
- package/src/cli/ui.mjs +299 -0
- package/src/config/config-json.mjs +73 -0
- package/src/config/dotenv.mjs +20 -0
- package/src/config/features.mjs +75 -0
- package/src/config/loader.mjs +109 -0
- package/src/config/settings-command.mjs +97 -0
- package/src/context/diagnostics.mjs +70 -0
- package/src/context/engine.mjs +148 -0
- package/src/context/injections.mjs +26 -0
- package/src/context/project-context.mjs +20 -0
- package/src/context/session-status.mjs +15 -0
- package/src/context/shell-layers.mjs +23 -0
- package/src/context/system-core/base.md +60 -0
- package/src/context/system-core/prompts/deepseek-v4-pro.md +3 -0
- package/src/context/system-core/prompts/default.md +3 -0
- package/src/context/system-core.mjs +35 -0
- package/src/debug/model-context-dumper.mjs +52 -0
- package/src/extensions/discovery.mjs +40 -0
- package/src/extensions/lifecycle-adapter.mjs +210 -0
- package/src/extensions/lifecycle-manifest.mjs +69 -0
- package/src/image-gen/index.mjs +7 -0
- package/src/image-gen/provider.mjs +231 -0
- package/src/image-gen/tool.mjs +84 -0
- package/src/lsp/client.mjs +204 -0
- package/src/lsp/diagnostic-store.mjs +39 -0
- package/src/lsp/servers.mjs +212 -0
- package/src/lsp/service.mjs +65 -0
- package/src/main.mjs +294 -0
- package/src/mcp/client.mjs +195 -0
- package/src/mcp/config.mjs +130 -0
- package/src/mcp/index.mjs +48 -0
- package/src/mcp/tools.mjs +98 -0
- package/src/memory/database.mjs +219 -0
- package/src/memory/glossary.mjs +124 -0
- package/src/memory/graph/graph-cascades.mjs +109 -0
- package/src/memory/graph/graph-diagnostics.mjs +73 -0
- package/src/memory/graph/graph-path-removal.mjs +50 -0
- package/src/memory/graph/graph-path-utils.mjs +17 -0
- package/src/memory/graph/graph-primitives.mjs +103 -0
- package/src/memory/graph/graph-read.mjs +159 -0
- package/src/memory/graph.mjs +282 -0
- package/src/memory/markdown/markdown-delete.mjs +23 -0
- package/src/memory/markdown/markdown-format.mjs +128 -0
- package/src/memory/markdown/markdown-recall.mjs +28 -0
- package/src/memory/markdown/ripgrep.mjs +16 -0
- package/src/memory/markdown/sqlite-index.mjs +87 -0
- package/src/memory/markdown-store.mjs +286 -0
- package/src/memory/markdown-tools.mjs +103 -0
- package/src/memory/search.mjs +142 -0
- package/src/memory/snapshot.mjs +86 -0
- package/src/memory/system-views.mjs +120 -0
- package/src/memory/tools.mjs +282 -0
- package/src/notification/desktop-notifier.mjs +85 -0
- package/src/platform/open-file.mjs +28 -0
- package/src/provider/config-command.mjs +129 -0
- package/src/provider/presets.mjs +72 -0
- package/src/session/attachment-display.mjs +16 -0
- package/src/session/attachment-references.mjs +65 -0
- package/src/session/attachments.mjs +140 -0
- package/src/session/persist.mjs +1 -0
- package/src/session/pi-manager.mjs +34 -0
- package/src/session/session-utils.mjs +16 -0
- package/src/session/sidecar-sync.mjs +19 -0
- package/src/session/sidecar.mjs +68 -0
- package/src/session/transcript.mjs +83 -0
- package/src/session/tree.mjs +42 -0
- package/src/shell/cli-runtime.mjs +11 -0
- package/src/shell/hints.mjs +12 -0
- package/src/shell/node-pty-adapter.mjs +81 -0
- package/src/shell/runtime-state.mjs +126 -0
- package/src/shell/runtime.mjs +244 -0
- package/src/shell/screen-buffer.mjs +136 -0
- package/src/shell/tool-read.mjs +74 -0
- package/src/shell/tools.mjs +299 -0
- package/src/supergrok/actions/image-generate.mjs +60 -0
- package/src/supergrok/actions/search.mjs +78 -0
- package/src/supergrok/auth.mjs +36 -0
- package/src/supergrok/constants.mjs +18 -0
- package/src/supergrok/oauth-provider.mjs +278 -0
- package/src/supergrok/provider.mjs +36 -0
- package/src/supergrok/response.mjs +76 -0
- package/src/supergrok/tool.mjs +61 -0
- package/src/text/ansi.mjs +3 -0
- package/src/web/config-command.mjs +43 -0
- package/src/web/fetch.mjs +78 -0
- package/src/web/presets.mjs +16 -0
- package/src/web/search.mjs +83 -0
- package/src/web/tools.mjs +107 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { yellow, brightBlack, red } from "../ui-theme.mjs";
|
|
2
|
+
|
|
3
|
+
export function formatRetryWaitMessage({ attempt, maxAttempts, remainingMs }) {
|
|
4
|
+
const seconds = Math.ceil(Math.max(0, remainingMs) / 1000);
|
|
5
|
+
return `Retrying (${attempt}/${maxAttempts}) in ${seconds}s... Esc to cancel`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function formatRetryStartLine(errorMessage) {
|
|
9
|
+
return yellow(`● retrying after error: ${String(errorMessage || "Unknown error").slice(0, 160)}`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function formatRetryEndLine({ success, attempt, finalError }) {
|
|
13
|
+
const attempts = `after ${attempt} attempt${attempt === 1 ? "" : "s"}`;
|
|
14
|
+
if (success) return brightBlack(`● retry recovered ${attempts}`);
|
|
15
|
+
return red(`● retry stopped ${attempts}${finalError ? `: ${finalError}` : ""}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createRetryStatusController({
|
|
19
|
+
output,
|
|
20
|
+
requestRender,
|
|
21
|
+
stopSpinner,
|
|
22
|
+
setIntervalImpl = setInterval,
|
|
23
|
+
clearIntervalImpl = clearInterval,
|
|
24
|
+
now = () => Date.now(),
|
|
25
|
+
}) {
|
|
26
|
+
let retryTimer = null;
|
|
27
|
+
let retrySpinnerActive = false;
|
|
28
|
+
|
|
29
|
+
function stop() {
|
|
30
|
+
if (retryTimer) {
|
|
31
|
+
clearIntervalImpl(retryTimer);
|
|
32
|
+
retryTimer = null;
|
|
33
|
+
}
|
|
34
|
+
if (retrySpinnerActive) {
|
|
35
|
+
output.setSpinner(false, "");
|
|
36
|
+
retrySpinnerActive = false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function start({ attempt, maxAttempts, delayMs, errorMessage }) {
|
|
41
|
+
stopSpinner();
|
|
42
|
+
stop();
|
|
43
|
+
const startedAt = now();
|
|
44
|
+
const message = () => formatRetryWaitMessage({
|
|
45
|
+
attempt,
|
|
46
|
+
maxAttempts,
|
|
47
|
+
remainingMs: delayMs - (now() - startedAt),
|
|
48
|
+
});
|
|
49
|
+
writeStatusLine(output, formatRetryStartLine(errorMessage));
|
|
50
|
+
output.setSpinner(true, message());
|
|
51
|
+
retrySpinnerActive = true;
|
|
52
|
+
retryTimer = setIntervalImpl(() => {
|
|
53
|
+
output.setSpinner(true, message());
|
|
54
|
+
output.tick();
|
|
55
|
+
requestRender();
|
|
56
|
+
}, 250);
|
|
57
|
+
requestRender();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function end({ success, attempt, finalError }) {
|
|
61
|
+
stop();
|
|
62
|
+
writeStatusLine(output, formatRetryEndLine({ success, attempt, finalError }));
|
|
63
|
+
requestRender();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { start, end, stop };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function writeStatusLine(output, line) {
|
|
70
|
+
if (typeof output.setOverlayStatus === "function") output.setOverlayStatus([line]);
|
|
71
|
+
else output.writeln(line);
|
|
72
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const DEFAULT_SPINNER_INTERVAL = 80;
|
|
2
|
+
|
|
3
|
+
export function createSpinnerStatusController({
|
|
4
|
+
output,
|
|
5
|
+
requestRender,
|
|
6
|
+
intervalMs = DEFAULT_SPINNER_INTERVAL,
|
|
7
|
+
setIntervalImpl = setInterval,
|
|
8
|
+
clearIntervalImpl = clearInterval,
|
|
9
|
+
} = {}) {
|
|
10
|
+
let timer = null;
|
|
11
|
+
let spinning = false;
|
|
12
|
+
|
|
13
|
+
function start(text) {
|
|
14
|
+
spinning = true;
|
|
15
|
+
output.setSpinner(true, text);
|
|
16
|
+
if (!timer) {
|
|
17
|
+
timer = setIntervalImpl(() => {
|
|
18
|
+
output.tick();
|
|
19
|
+
requestRender();
|
|
20
|
+
}, intervalMs);
|
|
21
|
+
}
|
|
22
|
+
requestRender();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function stop() {
|
|
26
|
+
if (!spinning && !timer) return false;
|
|
27
|
+
if (timer) {
|
|
28
|
+
clearIntervalImpl(timer);
|
|
29
|
+
timer = null;
|
|
30
|
+
}
|
|
31
|
+
spinning = false;
|
|
32
|
+
output.setSpinner(false, "");
|
|
33
|
+
requestRender();
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
start,
|
|
39
|
+
stop,
|
|
40
|
+
isRunning: () => spinning,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import { statusBar, R } from "../ui-theme.mjs";
|
|
3
|
+
|
|
4
|
+
const ANSI_RE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
|
|
5
|
+
const DEFAULT_STATUS_TEXT = "March";
|
|
6
|
+
|
|
7
|
+
export class StatusBar {
|
|
8
|
+
constructor(text = DEFAULT_STATUS_TEXT) {
|
|
9
|
+
this.text = normalizeStatusText(text);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
setText(text) {
|
|
13
|
+
const next = normalizeStatusText(text);
|
|
14
|
+
if (next === this.text) return false;
|
|
15
|
+
this.text = next;
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
invalidate() {}
|
|
20
|
+
|
|
21
|
+
render(width) {
|
|
22
|
+
if (width <= 0) return [""];
|
|
23
|
+
const text = fitStatusText(this.text, width);
|
|
24
|
+
const padded = padToWidth(text, width);
|
|
25
|
+
// If text already has ANSI coloring, only apply background
|
|
26
|
+
if (hasAnsi(padded)) {
|
|
27
|
+
return [statusBar.background(`${padded}${R}`)];
|
|
28
|
+
}
|
|
29
|
+
return [statusBar.background(statusBar.text(padded))];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function hasAnsi(text) {
|
|
34
|
+
return ANSI_RE.test(text);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function normalizeStatusText(text) {
|
|
38
|
+
const normalized = String(text || DEFAULT_STATUS_TEXT).replace(/\s+/g, " ").trim();
|
|
39
|
+
return normalized || DEFAULT_STATUS_TEXT;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function padToWidth(text, width) {
|
|
43
|
+
const plainWidth = visibleWidth(stripAnsi(text));
|
|
44
|
+
const padding = Math.max(0, width - plainWidth);
|
|
45
|
+
return text + " ".repeat(padding);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function fitStatusText(text, width) {
|
|
49
|
+
const normalized = normalizeStatusText(text);
|
|
50
|
+
if (visibleWidth(stripAnsi(normalized)) <= width) return normalized;
|
|
51
|
+
|
|
52
|
+
const segments = normalized.split(" | ");
|
|
53
|
+
if (segments.length < 2) return clipToWidth(normalized, width);
|
|
54
|
+
|
|
55
|
+
const tail = segments.at(-1);
|
|
56
|
+
const separator = " | ";
|
|
57
|
+
const tailWidth = visibleWidth(stripAnsi(tail));
|
|
58
|
+
const separatorWidth = visibleWidth(separator);
|
|
59
|
+
if (tailWidth + separatorWidth >= width) return clipToWidth(tail, width);
|
|
60
|
+
|
|
61
|
+
const headWidth = width - tailWidth - separatorWidth;
|
|
62
|
+
const head = clipToWidth(segments.slice(0, -1).join(separator), headWidth);
|
|
63
|
+
return `${head}${separator}${tail}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function clipToWidth(text, width) {
|
|
67
|
+
// For ANSI-containing text, build output character by character and measure plain width
|
|
68
|
+
let output = "";
|
|
69
|
+
let plainWidth = 0;
|
|
70
|
+
let inAnsi = false;
|
|
71
|
+
for (const ch of Array.from(String(text || ""))) {
|
|
72
|
+
if (ch === "\x1b") inAnsi = true;
|
|
73
|
+
if (inAnsi) {
|
|
74
|
+
output += ch;
|
|
75
|
+
if (/[@-~]/.test(ch)) inAnsi = false;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const charWidth = visibleWidth(ch);
|
|
79
|
+
if (plainWidth + charWidth > width) break;
|
|
80
|
+
output += ch;
|
|
81
|
+
plainWidth += charWidth;
|
|
82
|
+
}
|
|
83
|
+
return output;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function stripAnsi(text) {
|
|
87
|
+
return String(text ?? "").replace(ANSI_RE, "");
|
|
88
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { Language, Parser, Query } from "web-tree-sitter";
|
|
5
|
+
import { R } from "../ui-theme.mjs";
|
|
6
|
+
import {
|
|
7
|
+
CONSTANTS,
|
|
8
|
+
KEYWORDS,
|
|
9
|
+
LANG_ALIASES,
|
|
10
|
+
LANGUAGES,
|
|
11
|
+
NUMBER_TYPES,
|
|
12
|
+
OPERATORS,
|
|
13
|
+
PROPERTY_TYPES,
|
|
14
|
+
PUNCTUATION,
|
|
15
|
+
SCOPE_PRIORITY,
|
|
16
|
+
SCOPE_STYLE,
|
|
17
|
+
STRING_TYPES,
|
|
18
|
+
TYPE_TYPES,
|
|
19
|
+
} from "./languages.mjs";
|
|
20
|
+
|
|
21
|
+
const RESOURCE_DIR = join(dirname(fileURLToPath(import.meta.url)), "tree-sitter");
|
|
22
|
+
const ENCODER = new TextEncoder();
|
|
23
|
+
|
|
24
|
+
let initPromise;
|
|
25
|
+
let initialized = false;
|
|
26
|
+
const parsers = new Map();
|
|
27
|
+
const queries = new Map();
|
|
28
|
+
const highlightCache = new Map();
|
|
29
|
+
|
|
30
|
+
export function initializeTreeSitterHighlighting() {
|
|
31
|
+
if (initPromise) return initPromise;
|
|
32
|
+
initPromise = initializeParsers();
|
|
33
|
+
return initPromise;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isTreeSitterHighlightingReady() {
|
|
37
|
+
return initialized;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function normalizeLanguage(langOrPath = "") {
|
|
41
|
+
const raw = String(langOrPath ?? "").trim().toLowerCase();
|
|
42
|
+
if (!raw) return "";
|
|
43
|
+
const direct = LANG_ALIASES.get(raw);
|
|
44
|
+
if (direct) return direct;
|
|
45
|
+
const ext = raw.match(/\.([a-z0-9]+)$/)?.[1];
|
|
46
|
+
return ext ? (LANG_ALIASES.get(ext) ?? "") : "";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function highlightCodeLines(code, langOrPath = "", options = {}) {
|
|
50
|
+
const text = String(code ?? "");
|
|
51
|
+
const lang = normalizeLanguage(langOrPath);
|
|
52
|
+
const key = `${lang}\0${options.bg ?? ""}\0${text}`;
|
|
53
|
+
const cached = highlightCache.get(key);
|
|
54
|
+
if (cached) return cached;
|
|
55
|
+
|
|
56
|
+
const runs = treeSitterRuns(text, lang) ?? fallbackRuns(text, lang);
|
|
57
|
+
const rendered = renderRunsByLine(text, runs, options);
|
|
58
|
+
if (highlightCache.size > 200) highlightCache.clear();
|
|
59
|
+
highlightCache.set(key, rendered);
|
|
60
|
+
return rendered;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function highlightCodeLine(line, langOrPath = "", options = {}) {
|
|
64
|
+
return highlightCodeLines(String(line ?? ""), langOrPath, options)[0] ?? "";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function initializeParsers() {
|
|
68
|
+
try {
|
|
69
|
+
await Parser.init();
|
|
70
|
+
for (const [lang, config] of Object.entries(LANGUAGES)) {
|
|
71
|
+
const language = await Language.load(join(RESOURCE_DIR, config.file));
|
|
72
|
+
const parser = new Parser();
|
|
73
|
+
parser.setLanguage(language);
|
|
74
|
+
parsers.set(lang, parser);
|
|
75
|
+
const query = loadHighlightQuery(language, config.query);
|
|
76
|
+
if (query) queries.set(lang, query);
|
|
77
|
+
}
|
|
78
|
+
initialized = true;
|
|
79
|
+
highlightCache.clear();
|
|
80
|
+
} catch {
|
|
81
|
+
initialized = false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function treeSitterRuns(text, lang) {
|
|
86
|
+
const parser = parsers.get(lang);
|
|
87
|
+
if (!initialized || !parser || !text) return null;
|
|
88
|
+
try {
|
|
89
|
+
const tree = parser.parse(text);
|
|
90
|
+
const byteToIndex = buildByteToIndex(text);
|
|
91
|
+
const scopes = Array.from({ length: text.length }, () => ({ scope: "default", priority: 0 }));
|
|
92
|
+
const query = queries.get(lang);
|
|
93
|
+
if (query) applyQueryScopes(query, tree.rootNode, byteToIndex, scopes);
|
|
94
|
+
collectNodeScopes(tree.rootNode, byteToIndex, scopes);
|
|
95
|
+
return scopesToRuns(text, scopes);
|
|
96
|
+
} catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function loadHighlightQuery(language, queryFile) {
|
|
102
|
+
if (!queryFile) return null;
|
|
103
|
+
const path = join(RESOURCE_DIR, queryFile);
|
|
104
|
+
if (!existsSync(path)) return null;
|
|
105
|
+
try {
|
|
106
|
+
return new Query(language, readFileSync(path, "utf8"));
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function applyQueryScopes(query, rootNode, byteToIndex, scopes) {
|
|
113
|
+
for (const capture of query.captures(rootNode)) {
|
|
114
|
+
const scope = captureScope(capture.name);
|
|
115
|
+
if (!scope) continue;
|
|
116
|
+
applyScope(scopes, byteToIndex[capture.node.startIndex] ?? 0, byteToIndex[capture.node.endIndex] ?? scopes.length, scope);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function captureScope(name) {
|
|
121
|
+
const value = String(name ?? "").toLowerCase();
|
|
122
|
+
if (!value) return null;
|
|
123
|
+
if (value.includes("comment")) return "comment";
|
|
124
|
+
if (value.includes("string") || value.includes("escape") || value.includes("regex")) return "string";
|
|
125
|
+
if (value.includes("number") || value.includes("float") || value.includes("boolean") || value.includes("constant")) return "constant";
|
|
126
|
+
if (value.includes("keyword") || value.includes("conditional") || value.includes("repeat") || value.includes("include")) return "keyword";
|
|
127
|
+
if (value.includes("function") || value.includes("method") || value.includes("constructor")) return "function";
|
|
128
|
+
if (value.includes("type") || value.includes("class") || value.includes("namespace") || value.includes("module")) return "type";
|
|
129
|
+
if (value.includes("property") || value.includes("field")) return "property";
|
|
130
|
+
if (value.includes("attribute") || value.includes("annotation")) return "attribute";
|
|
131
|
+
if (value.includes("operator")) return "operator";
|
|
132
|
+
if (value.includes("punctuation") || value.includes("delimiter") || value.includes("bracket")) return "punctuation";
|
|
133
|
+
if (value.includes("tag")) return "tag";
|
|
134
|
+
if (value.includes("variable") || value.includes("parameter")) return "variable";
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function collectNodeScopes(node, byteToIndex, scopes) {
|
|
139
|
+
const scope = classifyNode(node);
|
|
140
|
+
if (scope) applyScope(scopes, byteToIndex[node.startIndex] ?? 0, byteToIndex[node.endIndex] ?? scopes.length, scope);
|
|
141
|
+
for (const child of node.children ?? []) collectNodeScopes(child, byteToIndex, scopes);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function classifyNode(node) {
|
|
145
|
+
const type = node.type;
|
|
146
|
+
if (type === "comment") return "comment";
|
|
147
|
+
if (STRING_TYPES.has(type)) return "string";
|
|
148
|
+
if (type === "raw_string_literal" || type === "interpreted_string_literal" || type === "char_literal") return "string";
|
|
149
|
+
if (NUMBER_TYPES.has(type)) return "number";
|
|
150
|
+
if (type === "integer_literal" || type === "float_literal" || type === "decimal_integer_literal") return "number";
|
|
151
|
+
if (TYPE_TYPES.has(type)) return "type";
|
|
152
|
+
if (type === "type_identifier" || type === "scoped_type_identifier") return "type";
|
|
153
|
+
if (PROPERTY_TYPES.has(type)) return "property";
|
|
154
|
+
if (type === "field_identifier" || type === "property_name") return "property";
|
|
155
|
+
if (type === "tag_name") return "tag";
|
|
156
|
+
if (type === "attribute_name") return "attribute";
|
|
157
|
+
if (type === "regex_pattern") return "string";
|
|
158
|
+
if (type === "identifier") return classifyIdentifier(node);
|
|
159
|
+
if (type === "jsx_identifier") return "type";
|
|
160
|
+
if (type === "null" || type === "true" || type === "false") return "constant";
|
|
161
|
+
if (!node.isNamed) {
|
|
162
|
+
if (KEYWORDS.has(type)) return "keyword";
|
|
163
|
+
if (CONSTANTS.has(type)) return "constant";
|
|
164
|
+
if (OPERATORS.has(type)) return "operator";
|
|
165
|
+
if (PUNCTUATION.has(type)) return "punctuation";
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function classifyIdentifier(node) {
|
|
171
|
+
const text = node.text;
|
|
172
|
+
if (KEYWORDS.has(text)) return "keyword";
|
|
173
|
+
if (CONSTANTS.has(text)) return "constant";
|
|
174
|
+
|
|
175
|
+
const parent = node.parent;
|
|
176
|
+
const field = childFieldName(parent, node);
|
|
177
|
+
if (field === "name" && /function|method|declaration/.test(parent?.type ?? "")) return "function";
|
|
178
|
+
if (field === "name" && /class|interface|type_alias|enum/.test(parent?.type ?? "")) return "type";
|
|
179
|
+
if (field === "function" && parent?.type === "call_expression") return "function";
|
|
180
|
+
if (field === "property") return "property";
|
|
181
|
+
if (parent?.type === "member_expression" || parent?.type === "subscript_expression") return "property";
|
|
182
|
+
if (/type|heritage|implements/.test(parent?.type ?? "")) return "type";
|
|
183
|
+
return "variable";
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function childFieldName(parent, child) {
|
|
187
|
+
if (!parent) return "";
|
|
188
|
+
const children = parent.children ?? [];
|
|
189
|
+
for (let i = 0; i < children.length; i++) {
|
|
190
|
+
if (children[i].equals?.(child)) return parent.fieldNameForChild(i) ?? "";
|
|
191
|
+
}
|
|
192
|
+
return "";
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function applyScope(scopes, start, end, scope) {
|
|
196
|
+
const priority = SCOPE_PRIORITY[scope] ?? 0;
|
|
197
|
+
for (let i = Math.max(0, start); i < Math.min(scopes.length, end); i++) {
|
|
198
|
+
if (priority >= scopes[i].priority) scopes[i] = { scope, priority };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function scopesToRuns(text, scopes) {
|
|
203
|
+
const runs = [];
|
|
204
|
+
for (let i = 0; i < text.length;) {
|
|
205
|
+
const scope = scopes[i]?.scope ?? "default";
|
|
206
|
+
let end = i + 1;
|
|
207
|
+
while (end < text.length && (scopes[end]?.scope ?? "default") === scope) end++;
|
|
208
|
+
runs.push({ text: text.slice(i, end), scope });
|
|
209
|
+
i = end;
|
|
210
|
+
}
|
|
211
|
+
return runs;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function fallbackRuns(text, lang) {
|
|
215
|
+
const pattern = fallbackPattern(lang);
|
|
216
|
+
if (!pattern) return [{ text, scope: "default" }];
|
|
217
|
+
const runs = [];
|
|
218
|
+
let index = 0;
|
|
219
|
+
for (const match of text.matchAll(pattern)) {
|
|
220
|
+
if (match.index > index) runs.push({ text: text.slice(index, match.index), scope: "default" });
|
|
221
|
+
runs.push({ text: match[0], scope: fallbackScope(match[0]) });
|
|
222
|
+
index = match.index + match[0].length;
|
|
223
|
+
}
|
|
224
|
+
if (index < text.length) runs.push({ text: text.slice(index), scope: "default" });
|
|
225
|
+
return runs;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function fallbackPattern(lang) {
|
|
229
|
+
if (["javascript", "typescript", "jsx", "tsx"].includes(lang)) {
|
|
230
|
+
return /\/\/.*|\/\*[\s\S]*?\*\/|"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|`(?:\\.|[^`])*`|\b(?:async|await|break|case|catch|class|const|continue|default|else|export|for|from|function|if|import|interface|let|new|null|return|throw|try|type|undefined|while)\b|\b\d+(?:\.\d+)?\b/g;
|
|
231
|
+
}
|
|
232
|
+
if (lang === "json") return /"(?:\\.|[^"])*"|\b(?:true|false|null)\b|\b\d+(?:\.\d+)?\b/g;
|
|
233
|
+
return /#.*|"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|\b(?:cd|cp|echo|exit|git|grep|ls|mkdir|mv|npm|node|pnpm|rm|yarn)\b|\b\d+(?:\.\d+)?\b/g;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function fallbackScope(token) {
|
|
237
|
+
if (token.startsWith("//") || token.startsWith("#") || token.startsWith("/*")) return "comment";
|
|
238
|
+
if (/^["'`]/.test(token)) return "string";
|
|
239
|
+
if (/^\d/.test(token)) return "number";
|
|
240
|
+
if (CONSTANTS.has(token)) return "constant";
|
|
241
|
+
return "keyword";
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function renderRunsByLine(source, runs, options) {
|
|
245
|
+
const lines = [""];
|
|
246
|
+
for (const run of runs.length ? runs : [{ text: source, scope: "default" }]) {
|
|
247
|
+
const parts = run.text.split("\n");
|
|
248
|
+
for (let i = 0; i < parts.length; i++) {
|
|
249
|
+
if (i > 0) lines.push("");
|
|
250
|
+
if (parts[i]) lines[lines.length - 1] += styleSyntax(parts[i], run.scope, options.bg);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return lines;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function styleSyntax(text, scope = "default", bg = "") {
|
|
257
|
+
const codes = [SCOPE_STYLE[scope] ?? SCOPE_STYLE.default];
|
|
258
|
+
if (bg) codes.push(bg);
|
|
259
|
+
return `\x1b[${codes.join(";")}m${text}${R}`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function buildByteToIndex(text) {
|
|
263
|
+
const map = [];
|
|
264
|
+
let byte = 0;
|
|
265
|
+
for (let index = 0; index < text.length;) {
|
|
266
|
+
const codePoint = text.codePointAt(index);
|
|
267
|
+
const char = String.fromCodePoint(codePoint);
|
|
268
|
+
const bytes = ENCODER.encode(char).length;
|
|
269
|
+
for (let i = 0; i < bytes; i++) map[byte + i] = index;
|
|
270
|
+
byte += bytes;
|
|
271
|
+
index += char.length;
|
|
272
|
+
}
|
|
273
|
+
map[byte] = text.length;
|
|
274
|
+
return map;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
void initializeTreeSitterHighlighting();
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export const LANGUAGES = {
|
|
2
|
+
bash: { file: "tree-sitter-bash.wasm", query: "bash.highlights.scm" },
|
|
3
|
+
c: { file: "tree-sitter-c.wasm", query: "c.highlights.scm" },
|
|
4
|
+
cpp: { file: "tree-sitter-cpp.wasm", query: "cpp.highlights.scm" },
|
|
5
|
+
csharp: { file: "tree-sitter-c-sharp.wasm", query: "csharp.highlights.scm" },
|
|
6
|
+
css: { file: "tree-sitter-css.wasm", query: "css.highlights.scm" },
|
|
7
|
+
diff: { file: "tree-sitter-diff.wasm", query: "diff.highlights.scm" },
|
|
8
|
+
go: { file: "tree-sitter-go.wasm", query: "go.highlights.scm" },
|
|
9
|
+
html: { file: "tree-sitter-html.wasm", query: "html.highlights.scm" },
|
|
10
|
+
java: { file: "tree-sitter-java.wasm", query: "java.highlights.scm" },
|
|
11
|
+
javascript: { file: "tree-sitter-typescript.wasm", query: "typescript.highlights.scm" },
|
|
12
|
+
json: { file: "tree-sitter-json.wasm", query: "json.highlights.scm" },
|
|
13
|
+
php: { file: "tree-sitter-php.wasm", query: "php.highlights.scm" },
|
|
14
|
+
python: { file: "tree-sitter-python.wasm", query: "python.highlights.scm" },
|
|
15
|
+
ruby: { file: "tree-sitter-ruby.wasm", query: "ruby.highlights.scm" },
|
|
16
|
+
rust: { file: "tree-sitter-rust.wasm", query: "rust.highlights.scm" },
|
|
17
|
+
toml: { file: "tree-sitter-toml.wasm", query: "toml.highlights.scm" },
|
|
18
|
+
tsx: { file: "tree-sitter-tsx.wasm", query: "tsx.highlights.scm" },
|
|
19
|
+
typescript: { file: "tree-sitter-typescript.wasm", query: "typescript.highlights.scm" },
|
|
20
|
+
yaml: { file: "tree-sitter-yaml.wasm", query: "yaml.highlights.scm" },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const LANG_ALIASES = new Map([
|
|
24
|
+
["bash", "bash"], ["c", "c"], ["cc", "cpp"], ["cjs", "javascript"], ["cpp", "cpp"],
|
|
25
|
+
["cs", "csharp"], ["css", "css"], ["cts", "typescript"], ["csharp", "csharp"], ["cxx", "cpp"],
|
|
26
|
+
["diff", "diff"], ["go", "go"], ["h", "c"], ["hh", "cpp"], ["htm", "html"], ["html", "html"],
|
|
27
|
+
["hpp", "cpp"], ["hxx", "cpp"], ["java", "java"], ["javascript", "javascript"], ["js", "javascript"],
|
|
28
|
+
["json", "json"], ["jsonc", "json"], ["jsx", "jsx"], ["mjs", "javascript"], ["mts", "typescript"],
|
|
29
|
+
["patch", "diff"], ["php", "php"], ["py", "python"], ["python", "python"], ["rb", "ruby"],
|
|
30
|
+
["rs", "rust"], ["ruby", "ruby"], ["rust", "rust"], ["sh", "bash"], ["toml", "toml"],
|
|
31
|
+
["ts", "typescript"], ["tsx", "tsx"], ["typescript", "typescript"], ["yaml", "yaml"], ["yml", "yaml"],
|
|
32
|
+
["zsh", "bash"],
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
export const SCOPE_STYLE = {
|
|
36
|
+
default: "38;2;127;216;143",
|
|
37
|
+
comment: "2;90",
|
|
38
|
+
string: "38;2;127;216;143",
|
|
39
|
+
number: "36",
|
|
40
|
+
constant: "36",
|
|
41
|
+
keyword: "38;2;245;167;66",
|
|
42
|
+
function: "38;5;117",
|
|
43
|
+
type: "38;5;141",
|
|
44
|
+
property: "38;5;116",
|
|
45
|
+
operator: "38;5;250",
|
|
46
|
+
punctuation: "38;5;245",
|
|
47
|
+
variable: "38;5;250",
|
|
48
|
+
tag: "38;5;203",
|
|
49
|
+
attribute: "38;5;179",
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const SCOPE_PRIORITY = {
|
|
53
|
+
default: 0,
|
|
54
|
+
punctuation: 1,
|
|
55
|
+
operator: 2,
|
|
56
|
+
variable: 3,
|
|
57
|
+
property: 4,
|
|
58
|
+
attribute: 4,
|
|
59
|
+
type: 5,
|
|
60
|
+
function: 6,
|
|
61
|
+
keyword: 7,
|
|
62
|
+
constant: 8,
|
|
63
|
+
number: 9,
|
|
64
|
+
string: 10,
|
|
65
|
+
comment: 11,
|
|
66
|
+
tag: 12,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const KEYWORDS = new Set([
|
|
70
|
+
"abstract", "as", "async", "await", "break", "case", "catch", "class", "const", "continue",
|
|
71
|
+
"debugger", "declare", "default", "delete", "do", "else", "enum", "export", "extends", "finally",
|
|
72
|
+
"for", "from", "function", "get", "if", "implements", "import", "in", "infer", "instanceof",
|
|
73
|
+
"interface", "keyof", "let", "module", "namespace", "new", "of", "private", "protected", "public",
|
|
74
|
+
"readonly", "return", "satisfies", "set", "static", "switch", "throw", "try", "type", "typeof",
|
|
75
|
+
"var", "void", "while", "with", "yield",
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
export const CONSTANTS = new Set(["false", "null", "super", "this", "true", "undefined"]);
|
|
79
|
+
export const OPERATORS = new Set([
|
|
80
|
+
"+", "-", "*", "/", "%", "=", "==", "===", "!=", "!==", "<", "<=", ">", ">=", "=>",
|
|
81
|
+
"&&", "||", "!", "?", "??", "|", "&", "^", "~", ":",
|
|
82
|
+
]);
|
|
83
|
+
export const PUNCTUATION = new Set(["(", ")", "[", "]", "{", "}", ".", ",", ";"]);
|
|
84
|
+
export const STRING_TYPES = new Set(["string", "string_fragment", "template_string", "regex", "escape_sequence"]);
|
|
85
|
+
export const NUMBER_TYPES = new Set(["number", "number_fragment"]);
|
|
86
|
+
export const TYPE_TYPES = new Set(["type_identifier", "predefined_type", "primitive_type", "type_annotation"]);
|
|
87
|
+
export const PROPERTY_TYPES = new Set([
|
|
88
|
+
"property_identifier",
|
|
89
|
+
"shorthand_property_identifier",
|
|
90
|
+
"shorthand_property_identifier_pattern",
|
|
91
|
+
]);
|