codebase-onboarder 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/LICENSE +21 -0
- package/README.md +275 -0
- package/bin/onboarder.js +14 -0
- package/bin/postinstall.js +6 -0
- package/cli/commands.js +436 -0
- package/cli/main.js +129 -0
- package/cli/prompt.js +101 -0
- package/cli/ui.js +55 -0
- package/cli/wizard.js +325 -0
- package/package.json +48 -0
- package/public/app.js +1339 -0
- package/public/index.html +455 -0
- package/public/js/about.js +284 -0
- package/public/js/aiDraft.js +163 -0
- package/public/js/analysisPanel.js +270 -0
- package/public/js/analysisReport.js +154 -0
- package/public/js/api.js +264 -0
- package/public/js/atlas.js +98 -0
- package/public/js/blameView.js +41 -0
- package/public/js/codeTab.js +256 -0
- package/public/js/codeViewer.js +184 -0
- package/public/js/components/FileChip.js +102 -0
- package/public/js/components/MetricSparkline.js +89 -0
- package/public/js/components/RiskBadge.js +105 -0
- package/public/js/deepAnalysisView.js +496 -0
- package/public/js/diagramPane.js +154 -0
- package/public/js/diffView.js +352 -0
- package/public/js/docsView.js +566 -0
- package/public/js/fileSourceBrowser.js +47 -0
- package/public/js/flameGraph.js +75 -0
- package/public/js/forceGraph.js +592 -0
- package/public/js/heatmap.js +185 -0
- package/public/js/highlight.js +122 -0
- package/public/js/html.js +50 -0
- package/public/js/insightsView.js +236 -0
- package/public/js/inspector.js +535 -0
- package/public/js/llm.js +345 -0
- package/public/js/mapView.js +159 -0
- package/public/js/markdown.js +119 -0
- package/public/js/mindmap.js +289 -0
- package/public/js/repoFiles.js +44 -0
- package/public/js/sbomView.js +142 -0
- package/public/js/scanCache.js +76 -0
- package/public/js/search.js +874 -0
- package/public/js/serverSettings.js +169 -0
- package/public/js/state.js +192 -0
- package/public/js/tour.js +17 -0
- package/public/js/transitions.js +15 -0
- package/public/js/tree.js +200 -0
- package/public/js/workflowsView.js +95 -0
- package/public/styles.css +2767 -0
- package/public/vendor/mermaid.min.js +3587 -0
- package/public/vendor/monaco/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
- package/public/vendor/monaco/vs/base/worker/workerMain.js +31 -0
- package/public/vendor/monaco/vs/basic-languages/abap/abap.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/apex/apex.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/azcli/azcli.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/bat/bat.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/bicep/bicep.js +11 -0
- package/public/vendor/monaco/vs/basic-languages/cameligo/cameligo.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/clojure/clojure.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/coffee/coffee.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/cpp/cpp.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/csharp/csharp.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/csp/csp.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/css/css.js +12 -0
- package/public/vendor/monaco/vs/basic-languages/cypher/cypher.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/dart/dart.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/dockerfile/dockerfile.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/ecl/ecl.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/elixir/elixir.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/flow9/flow9.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/freemarker2/freemarker2.js +12 -0
- package/public/vendor/monaco/vs/basic-languages/fsharp/fsharp.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/go/go.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/graphql/graphql.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/handlebars/handlebars.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/hcl/hcl.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/html/html.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/ini/ini.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/java/java.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/javascript/javascript.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/julia/julia.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/kotlin/kotlin.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/less/less.js +11 -0
- package/public/vendor/monaco/vs/basic-languages/lexon/lexon.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/liquid/liquid.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/lua/lua.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/m3/m3.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/markdown/markdown.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/mdx/mdx.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/mips/mips.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/msdax/msdax.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/mysql/mysql.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/objective-c/objective-c.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/pascal/pascal.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/pascaligo/pascaligo.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/perl/perl.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/pgsql/pgsql.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/php/php.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/pla/pla.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/postiats/postiats.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/powerquery/powerquery.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/powershell/powershell.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/protobuf/protobuf.js +11 -0
- package/public/vendor/monaco/vs/basic-languages/pug/pug.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/python/python.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/qsharp/qsharp.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/r/r.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/razor/razor.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/redis/redis.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/redshift/redshift.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/restructuredtext/restructuredtext.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/ruby/ruby.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/rust/rust.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/sb/sb.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/scala/scala.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/scheme/scheme.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/scss/scss.js +12 -0
- package/public/vendor/monaco/vs/basic-languages/shell/shell.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/solidity/solidity.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/sophia/sophia.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/sparql/sparql.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/sql/sql.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/st/st.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/swift/swift.js +13 -0
- package/public/vendor/monaco/vs/basic-languages/systemverilog/systemverilog.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/tcl/tcl.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/twig/twig.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/typescript/typescript.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/typespec/typespec.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/vb/vb.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/wgsl/wgsl.js +307 -0
- package/public/vendor/monaco/vs/basic-languages/xml/xml.js +10 -0
- package/public/vendor/monaco/vs/basic-languages/yaml/yaml.js +10 -0
- package/public/vendor/monaco/vs/editor/editor.main.css +8 -0
- package/public/vendor/monaco/vs/editor/editor.main.js +798 -0
- package/public/vendor/monaco/vs/language/css/cssMode.js +13 -0
- package/public/vendor/monaco/vs/language/css/cssWorker.js +77 -0
- package/public/vendor/monaco/vs/language/html/htmlMode.js +13 -0
- package/public/vendor/monaco/vs/language/html/htmlWorker.js +454 -0
- package/public/vendor/monaco/vs/language/json/jsonMode.js +19 -0
- package/public/vendor/monaco/vs/language/json/jsonWorker.js +42 -0
- package/public/vendor/monaco/vs/language/typescript/tsMode.js +20 -0
- package/public/vendor/monaco/vs/language/typescript/tsWorker.js +51328 -0
- package/public/vendor/monaco/vs/loader.js +11 -0
- package/public/vendor/monaco/worker-boot.js +9 -0
- package/server/.fuse_hidden0000000800000001 +36 -0
- package/server/apiDiff.js +25 -0
- package/server/apiDocs.js +67 -0
- package/server/apiFile.js +36 -0
- package/server/apiGitBlame.js +63 -0
- package/server/apiMcp.js +110 -0
- package/server/apiScan.js +119 -0
- package/server/apiSearch.js +253 -0
- package/server/apiSettings.js +118 -0
- package/server/apiTools.js +90 -0
- package/server/config.js +227 -0
- package/server/fileSourceNode.js +44 -0
- package/server/gitClone.js +95 -0
- package/server/gitDiff.js +184 -0
- package/server/gitHistory.js +110 -0
- package/server/htmlText.js +44 -0
- package/server/http.js +55 -0
- package/server/httpGuards.js +87 -0
- package/server/index.js +156 -0
- package/server/llmProxy.js +162 -0
- package/server/logger.js +29 -0
- package/server/mcp/analysis.js +209 -0
- package/server/mcp/http.js +213 -0
- package/server/mcp/runner.js +278 -0
- package/server/mcp/server.js +241 -0
- package/server/mcp/standalone.js +42 -0
- package/server/mcp/tools.js +683 -0
- package/server/paths.js +39 -0
- package/server/router.js +218 -0
- package/server/searchIndex.js +118 -0
- package/server/sessions.js +163 -0
- package/server/static.js +59 -0
- package/server/tools/install.js +246 -0
- package/server/tools/parse.js +170 -0
- package/server/tools/platform.js +91 -0
- package/server/tools/registry.js +212 -0
- package/server/tools/scan.js +136 -0
- package/server/tools.js +212 -0
- package/server/tunnel.js +92 -0
- package/shared/analyzer/docs.js +135 -0
- package/shared/analyzer/explainLocal.js +163 -0
- package/shared/analyzer/graph.js +461 -0
- package/shared/analyzer/health.js +215 -0
- package/shared/analyzer/history.js +146 -0
- package/shared/analyzer/languages/csharp.js +39 -0
- package/shared/analyzer/languages/generic.js +131 -0
- package/shared/analyzer/languages/go.js +70 -0
- package/shared/analyzer/languages/index.js +43 -0
- package/shared/analyzer/languages/java.js +39 -0
- package/shared/analyzer/languages/javascript.js +240 -0
- package/shared/analyzer/languages/python.js +120 -0
- package/shared/analyzer/languages/rust.js +42 -0
- package/shared/analyzer/languages/typescript.js +138 -0
- package/shared/analyzer/licenses.js +151 -0
- package/shared/analyzer/metrics.js +120 -0
- package/shared/analyzer/pathUtil.js +69 -0
- package/shared/analyzer/patterns.js +272 -0
- package/shared/analyzer/scan.js +473 -0
- package/shared/analyzer/security.js +187 -0
- package/shared/analyzer/services.js +157 -0
- package/shared/analyzer/stack.js +173 -0
- package/shared/analyzer/tour.js +69 -0
- package/shared/analyzer/util.js +123 -0
- package/shared/analyzer/workflows.js +143 -0
- package/shared/diagram/aiFacts.js +145 -0
- package/shared/diagram/aiMermaid.js +59 -0
- package/shared/diagram/atlas.js +93 -0
- package/shared/diagram/mermaid.js +459 -0
- package/shared/search/query.js +518 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// The atlas view: a scrollable catalogue of every map, and the card that opens.
|
|
2
|
+
//
|
|
3
|
+
// Two things make this view unlike the others. It is a document rather than a
|
|
4
|
+
// diagram, so it opts out of the canvas's pan and zoom (`atlas-mode`, which
|
|
5
|
+
// diagramPane.js checks before it swallows a drag or a wheel). And a card, once
|
|
6
|
+
// opened, *becomes* another view: the canvas draws the card's diagram and the AI
|
|
7
|
+
// draft button borrows that view's fact sheet. Which is why `atlasOpen` holds
|
|
8
|
+
// the whole item and not just an index.
|
|
9
|
+
//
|
|
10
|
+
// Deciding what is in the list and drawing one card are both pure, and live in
|
|
11
|
+
// shared/diagram/atlas.js with tests. What is left here is the DOM.
|
|
12
|
+
|
|
13
|
+
import { atlasSpecs, atlasDiagram, filterAtlasItems } from '/shared/diagram/atlas.js';
|
|
14
|
+
import { escapeHtml } from './html.js';
|
|
15
|
+
import { state, focusFile, focusFolder, clearFocus } from './state.js';
|
|
16
|
+
|
|
17
|
+
let host = null; // #canvasContent — the pannable layer inside the canvas
|
|
18
|
+
let canvas = null; // #canvas — the viewport, which is what fixes the list's size
|
|
19
|
+
let hooks = {
|
|
20
|
+
onSyncInspector: () => {},
|
|
21
|
+
onRender: () => {},
|
|
22
|
+
onHome: () => {},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function initAtlas(options) {
|
|
26
|
+
host = options.host;
|
|
27
|
+
canvas = options.canvas;
|
|
28
|
+
hooks = { ...hooks, ...options };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Built once per repo and kept: the catalogue is a function of the scan, and the
|
|
32
|
+
// scan does not change while it is loaded. `unloadRepo` resets the flag.
|
|
33
|
+
function specs() {
|
|
34
|
+
if (!state.atlas.built) {
|
|
35
|
+
const { scan, facts, manifest, patterns } = state;
|
|
36
|
+
state.atlas = { built: true, items: atlasSpecs(scan, facts, manifest, patterns) };
|
|
37
|
+
}
|
|
38
|
+
return state.atlas.items;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function renderAtlasList() {
|
|
42
|
+
const items = filterAtlasItems(specs(), state.filters);
|
|
43
|
+
host.classList.add('atlas-mode');
|
|
44
|
+
hooks.onHome(); // the list is a document — never inherit a diagram's zoom
|
|
45
|
+
|
|
46
|
+
const list = document.createElement('div');
|
|
47
|
+
list.className = 'atlas-list';
|
|
48
|
+
// Exact viewport fit, because the list scrolls itself rather than being panned.
|
|
49
|
+
list.style.width = canvas.clientWidth + 'px';
|
|
50
|
+
list.style.height = canvas.clientHeight + 'px';
|
|
51
|
+
|
|
52
|
+
const head = document.createElement('p');
|
|
53
|
+
head.className = 'atlas-head';
|
|
54
|
+
head.textContent = `${items.length} maps, drawn from the import graph — no AI involved. `
|
|
55
|
+
+ `Open one; if it doesn't satisfy you, the AI draft button redraws it.`;
|
|
56
|
+
list.appendChild(head);
|
|
57
|
+
|
|
58
|
+
const grid = document.createElement('div');
|
|
59
|
+
grid.className = 'atlas-grid';
|
|
60
|
+
for (const it of items) {
|
|
61
|
+
const card = document.createElement('div');
|
|
62
|
+
card.className = 'atlas-card';
|
|
63
|
+
card.innerHTML = `
|
|
64
|
+
<div class="atlas-card-top">
|
|
65
|
+
<span class="atlas-kind is-${escapeHtml(it.kind)}">${escapeHtml(it.kind)}</span>
|
|
66
|
+
<span class="atlas-title" title="${escapeHtml(it.path || it.title)}">${escapeHtml(it.title)}</span>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="atlas-sub">${escapeHtml(it.sub)}</div>
|
|
69
|
+
`;
|
|
70
|
+
card.addEventListener('click', () => openAtlasItem(it));
|
|
71
|
+
grid.appendChild(card);
|
|
72
|
+
}
|
|
73
|
+
list.appendChild(grid);
|
|
74
|
+
|
|
75
|
+
host.innerHTML = '';
|
|
76
|
+
host.appendChild(list);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Opening a card points the rest of the app at the same subject, so the sidebar,
|
|
80
|
+
// the inspector and the breadcrumbs agree with the picture on the canvas.
|
|
81
|
+
function openAtlasItem(item) {
|
|
82
|
+
state.atlasOpen = item;
|
|
83
|
+
if (item.kind === 'file') focusFile(item.path);
|
|
84
|
+
else if (item.kind === 'folder') focusFolder(item.path);
|
|
85
|
+
else clearFocus();
|
|
86
|
+
|
|
87
|
+
hooks.onSyncInspector();
|
|
88
|
+
hooks.onRender();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// The diagram for whichever card is open, drawn fresh on each canvas render.
|
|
92
|
+
// Fresh matters: a Mermaid source carries its own palette, so a cached one keeps
|
|
93
|
+
// the theme it was born under. The old atlas stored the source on the item and
|
|
94
|
+
// an opened card stayed light after switching to dark.
|
|
95
|
+
export function openCardDiagram() {
|
|
96
|
+
const { scan, facts, manifest, patterns } = state;
|
|
97
|
+
return atlasDiagram(state.atlasOpen, { scan, facts, manifest, patterns });
|
|
98
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export function initBlameView(container) {
|
|
2
|
+
return function renderBlame(blameData, options = {}) {
|
|
3
|
+
if (!blameData || !blameData.lines) {
|
|
4
|
+
container.hidden = true;
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
container.hidden = false;
|
|
8
|
+
container.innerHTML = '';
|
|
9
|
+
|
|
10
|
+
// Hash author to color
|
|
11
|
+
function getAuthorColor(author) {
|
|
12
|
+
let hash = 0;
|
|
13
|
+
for (let i = 0; i < author.length; i++) hash = author.charCodeAt(i) + ((hash << 5) - hash);
|
|
14
|
+
return `hsl(${Math.abs(hash) % 360}, 60%, 50%)`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
blameData.lines.forEach(line => {
|
|
18
|
+
const div = document.createElement('div');
|
|
19
|
+
div.className = 'blame-line';
|
|
20
|
+
div.style.backgroundColor = getAuthorColor(line.author || 'Unknown');
|
|
21
|
+
|
|
22
|
+
const tooltip = document.createElement('div');
|
|
23
|
+
tooltip.className = 'blame-tooltip';
|
|
24
|
+
tooltip.textContent = `${line.author} - ${line.date || ''} (${(line.sha || '').substring(0, 7)})`;
|
|
25
|
+
|
|
26
|
+
div.appendChild(tooltip);
|
|
27
|
+
container.appendChild(div);
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function fetchBlame(scanId, path) {
|
|
33
|
+
try {
|
|
34
|
+
const res = await fetch(`/api/blame?scan=${encodeURIComponent(scanId)}&path=${encodeURIComponent(path)}`);
|
|
35
|
+
if (!res.ok) return null;
|
|
36
|
+
return await res.json();
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.error('Failed to fetch blame:', err);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// The Code tab: one file at a time, with its imports as chips you can click.
|
|
2
|
+
//
|
|
3
|
+
// Two viewers, one behind the other. Monaco is loaded from the app's own
|
|
4
|
+
// `vendor/` folder and used if it comes up; if it does not, a hand-rolled
|
|
5
|
+
// highlighter takes over for the rest of the session. Neither is required for
|
|
6
|
+
// the page to be useful — the header, the facts and the import chips are drawn
|
|
7
|
+
// from the graph before any file text arrives.
|
|
8
|
+
//
|
|
9
|
+
// The back/forward stack and the "which file should I show" decision live in
|
|
10
|
+
// state.js, where they are pure and tested. What is left here is DOM.
|
|
11
|
+
|
|
12
|
+
import { roleOf } from '/shared/analyzer/graph.js';
|
|
13
|
+
import { escapeHtml } from './html.js';
|
|
14
|
+
import { highlightCode, langOf } from './highlight.js';
|
|
15
|
+
import { initViewer, showInViewer, monacoLangOf, revealLineInViewer } from './codeViewer.js';
|
|
16
|
+
import { fetchBlame, initBlameView } from './blameView.js';
|
|
17
|
+
import {
|
|
18
|
+
state, focusFile, defaultCodePath, pushCodeHistory, codeHistoryTarget,
|
|
19
|
+
} from './state.js';
|
|
20
|
+
import { readRepoFile } from './repoFiles.js';
|
|
21
|
+
|
|
22
|
+
const MAX_FALLBACK_LINES = 3000;
|
|
23
|
+
|
|
24
|
+
let host = null;
|
|
25
|
+
let renderBlameGutter = null;
|
|
26
|
+
let hooks = {
|
|
27
|
+
onSyncInspector: () => {},
|
|
28
|
+
onRenderSidebar: () => {},
|
|
29
|
+
onOpenFile: () => {},
|
|
30
|
+
onToast: () => {},
|
|
31
|
+
getTheme: () => 'light',
|
|
32
|
+
blameGutter: null,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
let codeSeq = 0; // render generations — a slow read never overwrites a newer one
|
|
36
|
+
let viewerFailed = false; // Monaco failed to load → hand-rolled fallback
|
|
37
|
+
let pendingReveal = null; // a line a search result asked for, waiting for its file
|
|
38
|
+
|
|
39
|
+
// A search result knows which line it matched. The reveal has to wait until the
|
|
40
|
+
// text is on screen, and it must not leak onto the *next* file you open — which
|
|
41
|
+
// is why it is taken once, at the top of `renderCode`, rather than living in
|
|
42
|
+
// `state` where it would outlive the file it was meant for.
|
|
43
|
+
export function revealLineInCode(line) {
|
|
44
|
+
const target = Math.floor(Number(line));
|
|
45
|
+
pendingReveal = Number.isFinite(target) && target > 0 ? target : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Monaco is the real answer; without it, the fallback is a single scrolling
|
|
49
|
+
// `<pre>`, so the best we can do is put the line near the top. Approximate, and
|
|
50
|
+
// honest about being approximate — the file is still the right file.
|
|
51
|
+
function applyReveal(alt, line) {
|
|
52
|
+
if (!line) return;
|
|
53
|
+
if (!viewerFailed) {
|
|
54
|
+
revealLineInViewer(line);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const shell = alt.querySelector('.code-shell');
|
|
58
|
+
if (!shell) return;
|
|
59
|
+
const gutter = shell.querySelector('.code-gutter');
|
|
60
|
+
const total = gutter ? (gutter.textContent || '').split('\n').length : 0;
|
|
61
|
+
if (!total) return;
|
|
62
|
+
const perLine = shell.scrollHeight / total;
|
|
63
|
+
shell.scrollTop = Math.max(0, (line - 2) * perLine);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function initCodeTab(options) {
|
|
67
|
+
host = options.host;
|
|
68
|
+
hooks = { ...hooks, ...options };
|
|
69
|
+
if (options.blameGutter) {
|
|
70
|
+
renderBlameGutter = initBlameView(options.blameGutter);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
host.addEventListener('click', (event) => {
|
|
74
|
+
const b = event.target.closest('button');
|
|
75
|
+
if (!b) return;
|
|
76
|
+
if (b.dataset.codeopen) return openCodeFile(b.dataset.codeopen);
|
|
77
|
+
if (b.dataset.codenav) return codeNav(Number(b.dataset.codenav));
|
|
78
|
+
if (b.dataset.codedive !== undefined) return (hooks.onDeepDive || hooks.onOpenFile)(state.code.path); // empty attribute is falsy — test undefined
|
|
79
|
+
if (b.dataset.codeblame !== undefined) {
|
|
80
|
+
if (!hooks.blameGutter) return;
|
|
81
|
+
if (!hooks.blameGutter.hidden) {
|
|
82
|
+
hooks.blameGutter.hidden = true;
|
|
83
|
+
b.classList.remove('is-active');
|
|
84
|
+
} else {
|
|
85
|
+
hooks.onToast('Fetching git blame...');
|
|
86
|
+
fetchBlame(state.scanId, state.code.path).then((data) => {
|
|
87
|
+
if (data && data.lines && data.lines.length) {
|
|
88
|
+
renderBlameGutter(data);
|
|
89
|
+
hooks.blameGutter.hidden = false;
|
|
90
|
+
b.classList.add('is-active');
|
|
91
|
+
hooks.onToast(`Blame loaded for ${data.lines.length} lines`);
|
|
92
|
+
} else {
|
|
93
|
+
hooks.onToast(data?.reason || 'No blame available for this file.');
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (b.dataset.codecopy !== undefined) {
|
|
100
|
+
const text = state.code.cache[state.code.path];
|
|
101
|
+
if (text !== undefined) {
|
|
102
|
+
navigator.clipboard.writeText(text)
|
|
103
|
+
.then(() => hooks.onToast('Copied to the clipboard.'))
|
|
104
|
+
.catch(() => hooks.onToast('Could not copy to the clipboard.'));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// In-tab navigation (the import chips) keeps the sidebar and inspector in step,
|
|
111
|
+
// so the rest of the app follows along rather than pointing at the old file.
|
|
112
|
+
function openCodeFile(path) {
|
|
113
|
+
state.code.path = path;
|
|
114
|
+
focusFile(path);
|
|
115
|
+
hooks.onSyncInspector();
|
|
116
|
+
hooks.onRenderSidebar();
|
|
117
|
+
renderCode();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function codeNav(delta) {
|
|
121
|
+
const path = codeHistoryTarget(delta);
|
|
122
|
+
if (path) openCodeFile(path);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function renderCode() {
|
|
126
|
+
const seq = ++codeSeq;
|
|
127
|
+
const path = defaultCodePath();
|
|
128
|
+
// Taken once and cleared, so a reveal belongs only to the render that followed
|
|
129
|
+
// the request for it — never to the next file you happen to open.
|
|
130
|
+
const revealLine = pendingReveal;
|
|
131
|
+
pendingReveal = null;
|
|
132
|
+
if (!path) {
|
|
133
|
+
host.innerHTML = '<div class="code-empty">Nothing to preview — this repo has no readable files.</div>';
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
state.code.path = path;
|
|
137
|
+
pushCodeHistory(path);
|
|
138
|
+
|
|
139
|
+
// The shell is built once: the Monaco container must persist across file
|
|
140
|
+
// switches, or the editor instance would be destroyed on every render.
|
|
141
|
+
if (!host.querySelector('.code-page')) {
|
|
142
|
+
host.innerHTML = `<div class="code-page">
|
|
143
|
+
<div class="code-head" id="codeHead"></div>
|
|
144
|
+
<div id="codeLinks"></div>
|
|
145
|
+
<div class="code-monaco" id="codeMonaco" hidden></div>
|
|
146
|
+
<div id="codeAlt"></div>
|
|
147
|
+
</div>`;
|
|
148
|
+
}
|
|
149
|
+
const headEl = host.querySelector('#codeHead');
|
|
150
|
+
const linksEl = host.querySelector('#codeLinks');
|
|
151
|
+
const monacoEl = host.querySelector('#codeMonaco');
|
|
152
|
+
const alt = host.querySelector('#codeAlt');
|
|
153
|
+
|
|
154
|
+
let text = state.code.cache[path];
|
|
155
|
+
if (text === undefined) {
|
|
156
|
+
monacoEl.hidden = true;
|
|
157
|
+
alt.innerHTML = '<div class="code-empty">Reading ' + escapeHtml(path) + '…</div>';
|
|
158
|
+
try {
|
|
159
|
+
text = await readRepoFile(path);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
if (seq !== codeSeq) return;
|
|
162
|
+
alt.innerHTML = '<div class="code-empty">Could not read that file: ' + escapeHtml(err.message) + '</div>';
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (seq !== codeSeq) return;
|
|
166
|
+
state.code.cache[path] = text;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const { facts } = state;
|
|
170
|
+
const parsed = facts.fanIn[path] !== undefined;
|
|
171
|
+
const name = path.split('/').pop();
|
|
172
|
+
const ext = (name.match(/\.([A-Za-z0-9]+)$/) || [, ''])[1].toLowerCase();
|
|
173
|
+
const isBinary = text.slice(0, 8000).includes('\0');
|
|
174
|
+
|
|
175
|
+
renderHead(headEl, { path, name, ext, text, parsed, isBinary });
|
|
176
|
+
linksEl.innerHTML = parsed ? importChips(path) : '';
|
|
177
|
+
|
|
178
|
+
if (isBinary) {
|
|
179
|
+
monacoEl.hidden = true;
|
|
180
|
+
alt.innerHTML = '<div class="code-empty">Binary file — nothing to preview.</div>';
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Monaco first; the hand-rolled highlighter stands by if it can't load.
|
|
185
|
+
if (!viewerFailed) {
|
|
186
|
+
monacoEl.hidden = false;
|
|
187
|
+
const ok = await initViewer(monacoEl, hooks.getTheme());
|
|
188
|
+
if (seq !== codeSeq) return;
|
|
189
|
+
if (ok) {
|
|
190
|
+
alt.innerHTML = '';
|
|
191
|
+
showInViewer(text, monacoLangOf(path), hooks.getTheme());
|
|
192
|
+
applyReveal(alt, revealLine);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
viewerFailed = true;
|
|
196
|
+
monacoEl.hidden = true;
|
|
197
|
+
}
|
|
198
|
+
renderFallback(alt, text, path);
|
|
199
|
+
applyReveal(alt, revealLine);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Navigation, identity, actions.
|
|
203
|
+
function renderHead(headEl, { path, name, ext, text, parsed, isBinary }) {
|
|
204
|
+
const { facts } = state;
|
|
205
|
+
const canBack = state.code.idx > 0;
|
|
206
|
+
const canFwd = state.code.idx < state.code.history.length - 1;
|
|
207
|
+
const meta = [
|
|
208
|
+
(ext || 'text').toUpperCase(),
|
|
209
|
+
`${text.split('\n').length.toLocaleString()} lines`,
|
|
210
|
+
`${(new Blob([text]).size / 1024).toFixed(1)} KB`,
|
|
211
|
+
parsed ? `${roleOf(path, facts)} · ${facts.fanIn[path]} dependents · pulls in ${facts.fanOut[path]}` : 'not parsed',
|
|
212
|
+
].join(' · ');
|
|
213
|
+
|
|
214
|
+
headEl.innerHTML = `
|
|
215
|
+
<div class="code-nav">
|
|
216
|
+
<button class="code-navbtn" data-codenav="-1" ${canBack ? '' : 'disabled'} title="Previous file">←</button>
|
|
217
|
+
<button class="code-navbtn" data-codenav="1" ${canFwd ? '' : 'disabled'} title="Next file">→</button>
|
|
218
|
+
</div>
|
|
219
|
+
<div class="code-title">
|
|
220
|
+
<h2>${escapeHtml(name)}</h2>
|
|
221
|
+
<div class="code-path">${escapeHtml(path)}</div>
|
|
222
|
+
<div class="code-meta">${escapeHtml(meta)}</div>
|
|
223
|
+
</div>
|
|
224
|
+
<div class="code-actions">
|
|
225
|
+
<button class="code-navbtn" data-codeblame ${isBinary ? 'disabled' : ''} title="Toggle Git blame annotations">Blame</button>
|
|
226
|
+
<button class="code-navbtn" data-codecopy ${isBinary ? 'disabled' : ''}>Copy</button>
|
|
227
|
+
<button class="code-navbtn" data-codedive>Deep-dive →</button>
|
|
228
|
+
</div>`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// The file's neighbours, both directions, as one-click jumps.
|
|
232
|
+
function importChips(path) {
|
|
233
|
+
const { facts } = state;
|
|
234
|
+
const chip = (p) => `<button class="code-chip" data-codeopen="${escapeHtml(p)}" title="${escapeHtml(p)}">${escapeHtml(p.split('/').pop())}</button>`;
|
|
235
|
+
const pulls = (facts.importsOf[path] || []).map(chip).join('');
|
|
236
|
+
const leans = (facts.importers[path] || []).map(chip).join('');
|
|
237
|
+
if (!pulls && !leans) return '';
|
|
238
|
+
return '<div class="code-links">'
|
|
239
|
+
+ (pulls ? `<span class="code-links-label">pulls in</span>${pulls}` : '')
|
|
240
|
+
+ (leans ? `<span class="code-links-label">leaned on by</span>${leans}` : '')
|
|
241
|
+
+ '</div>';
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Without Monaco: a gutter, a highlighted body, and a cap, because pasting a
|
|
245
|
+
// 40,000-line bundle into innerHTML locks the tab.
|
|
246
|
+
function renderFallback(alt, text, path) {
|
|
247
|
+
const all = text.split('\n');
|
|
248
|
+
const capped = all.length > MAX_FALLBACK_LINES;
|
|
249
|
+
const lines = capped ? all.slice(0, MAX_FALLBACK_LINES) : all;
|
|
250
|
+
const gutter = lines.map((_, i) => i + 1).join('\n');
|
|
251
|
+
const note = capped
|
|
252
|
+
? `<div class="code-note">Showing the first ${MAX_FALLBACK_LINES.toLocaleString()} of ${all.length.toLocaleString()} lines.</div>`
|
|
253
|
+
: '';
|
|
254
|
+
alt.innerHTML = `${note}<div class="code-shell"><pre class="code-gutter">${gutter}</pre>`
|
|
255
|
+
+ `<pre class="code-body"><code>${highlightCode(lines.join('\n'), langOf(path))}</code></pre></div>`;
|
|
256
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// Monaco Editor wrapper: lazy-loads the vendored AMD build, defines themes
|
|
2
|
+
// that match the app's palette, and exposes a tiny read-only viewer API.
|
|
3
|
+
// The module itself is import-safe in Node (nothing runs until initViewer);
|
|
4
|
+
// only the language mapper is used there.
|
|
5
|
+
|
|
6
|
+
// Extension → monaco language id. Anything unmapped renders as plaintext.
|
|
7
|
+
const MONACO_LANGS = {
|
|
8
|
+
js: 'javascript', mjs: 'javascript', cjs: 'javascript', jsx: 'javascript',
|
|
9
|
+
ts: 'typescript', tsx: 'typescript', mts: 'typescript',
|
|
10
|
+
py: 'python', pyi: 'python',
|
|
11
|
+
go: 'go', java: 'java', kt: 'kotlin', kts: 'kotlin', scala: 'scala',
|
|
12
|
+
c: 'c', h: 'c', cpp: 'cpp', cc: 'cpp', cxx: 'cpp', hpp: 'cpp',
|
|
13
|
+
cs: 'csharp', rs: 'rust', rb: 'ruby', php: 'php', swift: 'swift',
|
|
14
|
+
m: 'objective-c', mm: 'objective-c', dart: 'dart', lua: 'lua', r: 'r',
|
|
15
|
+
pl: 'perl', pm: 'perl', ex: 'elixir', exs: 'elixir', clj: 'clojure',
|
|
16
|
+
fs: 'fsharp', vb: 'vb', ps1: 'powershell', sh: 'shell', bash: 'shell',
|
|
17
|
+
zsh: 'shell', fish: 'shell', bat: 'bat', cmd: 'bat',
|
|
18
|
+
json: 'json', jsonc: 'json', yaml: 'yaml', yml: 'yaml', toml: 'ini',
|
|
19
|
+
ini: 'ini', env: 'ini', cfg: 'ini', xml: 'xml', svg: 'xml', xsl: 'xml',
|
|
20
|
+
html: 'html', htm: 'html', css: 'css', scss: 'scss', less: 'less',
|
|
21
|
+
md: 'markdown', markdown: 'markdown', mdx: 'markdown',
|
|
22
|
+
sql: 'sql', mysql: 'mysql', pgsql: 'pgsql', graphql: 'graphql',
|
|
23
|
+
dockerfile: 'dockerfile', tf: 'hcl', hcl: 'hcl', proto: 'protobuf',
|
|
24
|
+
sol: 'solidity', tcl: 'tcl', jl: 'julia', coffee: 'coffeescript',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function monacoLangOf(path) {
|
|
28
|
+
const name = path.split('/').pop().toLowerCase();
|
|
29
|
+
if (name === 'dockerfile' || name.startsWith('dockerfile.')) return 'dockerfile';
|
|
30
|
+
if (name === 'makefile' || name === 'gnumakefile') return 'makefile';
|
|
31
|
+
if (name === 'cmakelists.txt') return 'cmake';
|
|
32
|
+
const ext = (name.match(/\.([a-z0-9]+)$/) || [, ''])[1];
|
|
33
|
+
return MONACO_LANGS[ext] || 'plaintext';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Themes mirroring styles.css: same paper/ink/accent, same token colors.
|
|
37
|
+
function defineThemes(monaco) {
|
|
38
|
+
monaco.editor.defineTheme('ob-light', {
|
|
39
|
+
base: 'vs',
|
|
40
|
+
inherit: true,
|
|
41
|
+
rules: [
|
|
42
|
+
{ token: 'comment', foreground: '8a8578', fontStyle: 'italic' },
|
|
43
|
+
{ token: 'string', foreground: '2f5d3f' },
|
|
44
|
+
{ token: 'keyword', foreground: '9a3b2e', fontStyle: 'bold' },
|
|
45
|
+
{ token: 'number', foreground: '1f5f8b' },
|
|
46
|
+
{ token: 'type', foreground: '6b4fa0' },
|
|
47
|
+
],
|
|
48
|
+
colors: {
|
|
49
|
+
'editor.background': '#fdfbf6',
|
|
50
|
+
'editor.foreground': '#1c1a16',
|
|
51
|
+
'editorLineNumber.foreground': '#a8a296',
|
|
52
|
+
'editorLineNumber.activeForeground': '#6b675c',
|
|
53
|
+
'editor.lineHighlightBackground': '#f1ede2',
|
|
54
|
+
'editorCursor.foreground': '#1c1a16',
|
|
55
|
+
'editor.selectionBackground': '#d8e4d2',
|
|
56
|
+
'editorIndentGuide.background1': '#e5e0d3',
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
monaco.editor.defineTheme('ob-dark', {
|
|
60
|
+
base: 'vs-dark',
|
|
61
|
+
inherit: true,
|
|
62
|
+
rules: [
|
|
63
|
+
{ token: 'comment', foreground: '6f6a5e', fontStyle: 'italic' },
|
|
64
|
+
{ token: 'string', foreground: '8fc7a0' },
|
|
65
|
+
{ token: 'keyword', foreground: 'e08a6d', fontStyle: 'bold' },
|
|
66
|
+
{ token: 'number', foreground: '7fb3d9' },
|
|
67
|
+
{ token: 'type', foreground: 'b49ae0' },
|
|
68
|
+
],
|
|
69
|
+
colors: {
|
|
70
|
+
'editor.background': '#232019',
|
|
71
|
+
'editor.foreground': '#e8e2d4',
|
|
72
|
+
'editorLineNumber.foreground': '#5c574b',
|
|
73
|
+
'editorLineNumber.activeForeground': '#a8a296',
|
|
74
|
+
'editor.lineHighlightBackground': '#2b2720',
|
|
75
|
+
'editorCursor.foreground': '#e8e2d4',
|
|
76
|
+
'editor.selectionBackground': '#3d4a38',
|
|
77
|
+
'editorIndentGuide.background1': '#343026',
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let editor = null;
|
|
83
|
+
let loadPromise = null;
|
|
84
|
+
|
|
85
|
+
// Loads Monaco once and creates the single reusable editor instance.
|
|
86
|
+
// Resolves false when the vendored build is unavailable — callers fall back
|
|
87
|
+
// to the hand-rolled highlighter.
|
|
88
|
+
export function initViewer(container, theme) {
|
|
89
|
+
if (editor) return Promise.resolve(true);
|
|
90
|
+
if (loadPromise) return loadPromise;
|
|
91
|
+
|
|
92
|
+
loadPromise = new Promise((resolve) => {
|
|
93
|
+
if (typeof window.require !== 'function') return resolve(false);
|
|
94
|
+
window.require.config({ paths: { vs: '/vendor/monaco/vs' } });
|
|
95
|
+
self.MonacoEnvironment = {
|
|
96
|
+
// Language workers are AMD modules — they boot through worker-boot.js,
|
|
97
|
+
// which loads the AMD loader inside the worker first.
|
|
98
|
+
getWorkerUrl: (workerId, label) => {
|
|
99
|
+
const boot = '/vendor/monaco/worker-boot.js';
|
|
100
|
+
if (label === 'typescript' || label === 'javascript') return `${boot}?vs/language/typescript/tsWorker`;
|
|
101
|
+
if (label === 'json') return `${boot}?vs/language/json/jsonWorker`;
|
|
102
|
+
if (label === 'css' || label === 'scss' || label === 'less') return `${boot}?vs/language/css/cssWorker`;
|
|
103
|
+
if (label === 'html' || label === 'handlebars' || label === 'razor') return `${boot}?vs/language/html/htmlWorker`;
|
|
104
|
+
return '/vendor/monaco/vs/base/worker/workerMain.js'; // standalone editor worker
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
const fail = setTimeout(() => resolve(false), 8000); // never wedge the tab
|
|
108
|
+
window.require(['vs/editor/editor.main'], () => {
|
|
109
|
+
clearTimeout(fail);
|
|
110
|
+
defineThemes(monaco);
|
|
111
|
+
editor = monaco.editor.create(container, {
|
|
112
|
+
value: '',
|
|
113
|
+
language: 'plaintext',
|
|
114
|
+
theme: theme === 'dark' ? 'ob-dark' : 'ob-light',
|
|
115
|
+
readOnly: true,
|
|
116
|
+
domReadOnly: true,
|
|
117
|
+
automaticLayout: true,
|
|
118
|
+
minimap: { enabled: false },
|
|
119
|
+
scrollBeyondLastLine: false,
|
|
120
|
+
renderLineHighlight: 'none',
|
|
121
|
+
occurrencesHighlight: 'off',
|
|
122
|
+
folding: true,
|
|
123
|
+
wordWrap: 'off',
|
|
124
|
+
contextmenu: false,
|
|
125
|
+
fontSize: 12.5,
|
|
126
|
+
fontFamily: "'IBM Plex Mono', 'SF Mono', ui-monospace, Menlo, Consolas, monospace",
|
|
127
|
+
lineNumbersMinChars: 4,
|
|
128
|
+
padding: { top: 12, bottom: 12 },
|
|
129
|
+
scrollbar: { verticalScrollbarSize: 10, horizontalScrollbarSize: 10 },
|
|
130
|
+
});
|
|
131
|
+
resolve(true);
|
|
132
|
+
}, () => { clearTimeout(fail); resolve(false); });
|
|
133
|
+
});
|
|
134
|
+
return loadPromise;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// The TS/JS language service loads lazily on the first such model. Once it
|
|
138
|
+
// exists, turn validation off: a read-only preview should never draw red
|
|
139
|
+
// squiggles for imports the standalone worker cannot resolve.
|
|
140
|
+
let tsTamed = false;
|
|
141
|
+
function tameTypeScript() {
|
|
142
|
+
if (tsTamed) return;
|
|
143
|
+
const t0 = Date.now();
|
|
144
|
+
const iv = setInterval(() => {
|
|
145
|
+
if (window.monaco?.languages?.typescript) {
|
|
146
|
+
const diag = { noSemanticValidation: true, noSyntaxValidation: true };
|
|
147
|
+
monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions(diag);
|
|
148
|
+
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions(diag);
|
|
149
|
+
tsTamed = true;
|
|
150
|
+
clearInterval(iv);
|
|
151
|
+
} else if (Date.now() - t0 > 5000) {
|
|
152
|
+
clearInterval(iv);
|
|
153
|
+
}
|
|
154
|
+
}, 200);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function showInViewer(text, lang, theme) {
|
|
158
|
+
if (!editor) return;
|
|
159
|
+
monaco.editor.setTheme(theme === 'dark' ? 'ob-dark' : 'ob-light');
|
|
160
|
+
const model = editor.getModel();
|
|
161
|
+
editor.setValue(text);
|
|
162
|
+
monaco.editor.setModelLanguage(model, lang);
|
|
163
|
+
if (lang === 'javascript' || lang === 'typescript') tameTypeScript();
|
|
164
|
+
editor.setScrollTop(0);
|
|
165
|
+
editor.setScrollLeft(0);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function setViewerTheme(theme) {
|
|
169
|
+
if (editor) monaco.editor.setTheme(theme === 'dark' ? 'ob-dark' : 'ob-light');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Put the viewer on a line and leave the cursor there. Used when a search result
|
|
173
|
+
// knows where it matched — opening the right file at line 1 is only half an
|
|
174
|
+
// answer, and the other half is the reason a content hit is worth showing.
|
|
175
|
+
export function revealLineInViewer(line) {
|
|
176
|
+
if (!editor) return;
|
|
177
|
+
const target = Math.floor(Number(line));
|
|
178
|
+
if (!Number.isFinite(target) || target < 1) return;
|
|
179
|
+
const model = editor.getModel();
|
|
180
|
+
if (!model) return;
|
|
181
|
+
const clamped = Math.min(target, model.getLineCount());
|
|
182
|
+
editor.revealLineInCenter(clamped);
|
|
183
|
+
editor.setPosition({ lineNumber: clamped, column: 1 });
|
|
184
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export class FileChip extends HTMLElement {
|
|
2
|
+
constructor() {
|
|
3
|
+
super();
|
|
4
|
+
this.attachShadow({ mode: 'open' });
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
static get observedAttributes() {
|
|
8
|
+
return ['path', 'lang', 'risk'];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
connectedCallback() {
|
|
12
|
+
this.render();
|
|
13
|
+
this.addEventListener('click', () => {
|
|
14
|
+
this.dispatchEvent(new CustomEvent('file-select', {
|
|
15
|
+
detail: { path: this.getAttribute('path') },
|
|
16
|
+
bubbles: true,
|
|
17
|
+
composed: true
|
|
18
|
+
}));
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
23
|
+
if (oldValue !== newValue) {
|
|
24
|
+
this.render();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getRiskColor(risk) {
|
|
29
|
+
const score = parseInt(risk, 10) || 0;
|
|
30
|
+
if (score <= 33) return 'rgb(76, 175, 80)';
|
|
31
|
+
if (score <= 66) return 'rgb(255, 193, 7)';
|
|
32
|
+
return 'rgb(244, 67, 54)';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
render() {
|
|
36
|
+
const path = this.getAttribute('path') || '';
|
|
37
|
+
const lang = this.getAttribute('lang') || 'txt';
|
|
38
|
+
const risk = this.getAttribute('risk') || '0';
|
|
39
|
+
|
|
40
|
+
const basename = path.split('/').pop();
|
|
41
|
+
const riskColor = this.getRiskColor(risk);
|
|
42
|
+
|
|
43
|
+
// Hash for lang color
|
|
44
|
+
let hash = 0;
|
|
45
|
+
for (let i = 0; i < lang.length; i++) hash = lang.charCodeAt(i) + ((hash << 5) - hash);
|
|
46
|
+
const langHue = Math.abs(hash) % 360;
|
|
47
|
+
|
|
48
|
+
this.shadowRoot.innerHTML = `
|
|
49
|
+
<style>
|
|
50
|
+
:host {
|
|
51
|
+
display: inline-flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
padding: 4px 8px;
|
|
54
|
+
border-radius: 16px;
|
|
55
|
+
background: rgba(128, 128, 128, 0.1);
|
|
56
|
+
border: 1px solid rgba(128, 128, 128, 0.2);
|
|
57
|
+
cursor: pointer;
|
|
58
|
+
font-family: sans-serif;
|
|
59
|
+
font-size: 12px;
|
|
60
|
+
transition: background 0.2s;
|
|
61
|
+
}
|
|
62
|
+
:host(:hover) {
|
|
63
|
+
background: rgba(128, 128, 128, 0.2);
|
|
64
|
+
}
|
|
65
|
+
.lang-icon {
|
|
66
|
+
width: 16px;
|
|
67
|
+
height: 16px;
|
|
68
|
+
border-radius: 50%;
|
|
69
|
+
background: hsl(${langHue}, 70%, 50%);
|
|
70
|
+
color: white;
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
justify-content: center;
|
|
74
|
+
font-size: 8px;
|
|
75
|
+
font-weight: bold;
|
|
76
|
+
margin-right: 6px;
|
|
77
|
+
text-transform: uppercase;
|
|
78
|
+
}
|
|
79
|
+
.name {
|
|
80
|
+
flex: 1;
|
|
81
|
+
white-space: nowrap;
|
|
82
|
+
overflow: hidden;
|
|
83
|
+
text-overflow: ellipsis;
|
|
84
|
+
max-width: 150px;
|
|
85
|
+
}
|
|
86
|
+
.risk-dot {
|
|
87
|
+
width: 8px;
|
|
88
|
+
height: 8px;
|
|
89
|
+
border-radius: 50%;
|
|
90
|
+
background: ${riskColor};
|
|
91
|
+
margin-left: 6px;
|
|
92
|
+
}
|
|
93
|
+
</style>
|
|
94
|
+
<div class="lang-icon" title="${lang}">${lang.substring(0, 2)}</div>
|
|
95
|
+
<div class="name" title="${path}">${basename}</div>
|
|
96
|
+
<div class="risk-dot" title="Risk: ${risk}"></div>
|
|
97
|
+
`;
|
|
98
|
+
this.title = path;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
customElements.define('file-chip', FileChip);
|