codebase-onboarder 0.6.0 → 0.7.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 +174 -46
- package/bin/build-if-source.js +23 -0
- package/cli/commands.d.ts +26 -0
- package/cli/commands.js +836 -811
- package/cli/commands.js.map +1 -0
- package/cli/explorer/advanced.d.ts +14 -0
- package/cli/explorer/advanced.js +321 -329
- package/cli/explorer/advanced.js.map +1 -0
- package/cli/explorer/app.d.ts +1 -0
- package/cli/explorer/app.js +384 -313
- package/cli/explorer/app.js.map +1 -0
- package/cli/explorer/assistant.d.ts +9 -0
- package/cli/explorer/assistant.js +58 -0
- package/cli/explorer/assistant.js.map +1 -0
- package/cli/explorer/commands.d.ts +17 -0
- package/cli/explorer/commands.js +368 -302
- package/cli/explorer/commands.js.map +1 -0
- package/cli/explorer/featureViews.d.ts +124 -0
- package/cli/explorer/featureViews.js +179 -0
- package/cli/explorer/featureViews.js.map +1 -0
- package/cli/explorer/github.d.ts +21 -0
- package/cli/explorer/github.js +42 -44
- package/cli/explorer/github.js.map +1 -0
- package/cli/explorer/graphs.d.ts +7 -0
- package/cli/explorer/graphs.js +90 -95
- package/cli/explorer/graphs.js.map +1 -0
- package/cli/explorer/picker.d.ts +22 -0
- package/cli/explorer/picker.js +150 -0
- package/cli/explorer/picker.js.map +1 -0
- package/cli/explorer/report.d.ts +85 -0
- package/cli/explorer/report.js +60 -0
- package/cli/explorer/report.js.map +1 -0
- package/cli/explorer/session.d.ts +449 -0
- package/cli/explorer/session.js +143 -144
- package/cli/explorer/session.js.map +1 -0
- package/cli/explorer/views.d.ts +19 -0
- package/cli/explorer/views.js +465 -482
- package/cli/explorer/views.js.map +1 -0
- package/cli/explorer/wrap.d.ts +1 -0
- package/cli/explorer/wrap.js +28 -44
- package/cli/explorer/wrap.js.map +1 -0
- package/cli/main.d.ts +1 -0
- package/cli/main.js +214 -161
- package/cli/main.js.map +1 -0
- package/cli/prompt.d.ts +5 -0
- package/cli/prompt.js +84 -80
- package/cli/prompt.js.map +1 -0
- package/cli/scanOptions.d.ts +2 -0
- package/cli/scanOptions.js +11 -0
- package/cli/scanOptions.js.map +1 -0
- package/cli/ui.d.ts +33 -0
- package/cli/ui.js +35 -44
- package/cli/ui.js.map +1 -0
- package/cli/wizard.d.ts +187 -0
- package/cli/wizard.js +304 -284
- package/cli/wizard.js.map +1 -0
- package/package.json +15 -4
- package/public/app.d.ts +1 -0
- package/public/app.js +1057 -1009
- package/public/app.js.map +1 -0
- package/public/browser.d.ts +68 -0
- package/public/browser.js +2 -0
- package/public/browser.js.map +1 -0
- package/public/index.html +30 -13
- package/public/js/about.d.ts +6 -0
- package/public/js/about.js +152 -172
- package/public/js/about.js.map +1 -0
- package/public/js/aiDraft.d.ts +3 -0
- package/public/js/aiDraft.js +116 -124
- package/public/js/aiDraft.js.map +1 -0
- package/public/js/analysisPanel.d.ts +7 -0
- package/public/js/analysisPanel.js +166 -166
- package/public/js/analysisPanel.js.map +1 -0
- package/public/js/analysisReport.d.ts +38 -0
- package/public/js/analysisReport.js +105 -110
- package/public/js/analysisReport.js.map +1 -0
- package/public/js/api.d.ts +26 -0
- package/public/js/api.js +176 -167
- package/public/js/api.js.map +1 -0
- package/public/js/atlas.d.ts +6 -0
- package/public/js/atlas.js +50 -58
- package/public/js/atlas.js.map +1 -0
- package/public/js/blameView.d.ts +2 -0
- package/public/js/blameView.js +36 -37
- package/public/js/blameView.js.map +1 -0
- package/public/js/codeTab.d.ts +3 -0
- package/public/js/codeTab.js +175 -180
- package/public/js/codeTab.js.map +1 -0
- package/public/js/codeViewer.d.ts +5 -0
- package/public/js/codeViewer.js +162 -150
- package/public/js/codeViewer.js.map +1 -0
- package/public/js/components/FileChip.d.ts +9 -0
- package/public/js/components/FileChip.js +45 -49
- package/public/js/components/FileChip.js.map +1 -0
- package/public/js/components/MetricSparkline.d.ts +7 -0
- package/public/js/components/MetricSparkline.js +35 -44
- package/public/js/components/MetricSparkline.js.map +1 -0
- package/public/js/components/RiskBadge.d.ts +9 -0
- package/public/js/components/RiskBadge.js +55 -63
- package/public/js/components/RiskBadge.js.map +1 -0
- package/public/js/deepAnalysisView.d.ts +99 -0
- package/public/js/deepAnalysisView.js +282 -302
- package/public/js/deepAnalysisView.js.map +1 -0
- package/public/js/diagramPane.d.ts +8 -0
- package/public/js/diagramPane.js +133 -135
- package/public/js/diagramPane.js.map +1 -0
- package/public/js/diffView.d.ts +6 -0
- package/public/js/diffView.js +222 -209
- package/public/js/diffView.js.map +1 -0
- package/public/js/docsView.d.ts +2 -0
- package/public/js/docsView.js +441 -442
- package/public/js/docsView.js.map +1 -0
- package/public/js/dom.d.ts +189 -0
- package/public/js/dom.js +5 -0
- package/public/js/dom.js.map +1 -0
- package/public/js/fileSourceBrowser.d.ts +5 -0
- package/public/js/fileSourceBrowser.js +33 -41
- package/public/js/fileSourceBrowser.js.map +1 -0
- package/public/js/flameGraph.d.ts +5 -0
- package/public/js/flameGraph.js +62 -72
- package/public/js/flameGraph.js.map +1 -0
- package/public/js/forceGraph.d.ts +10 -0
- package/public/js/forceGraph.js +581 -556
- package/public/js/forceGraph.js.map +1 -0
- package/public/js/heatmap.d.ts +4 -0
- package/public/js/heatmap.js +142 -164
- package/public/js/heatmap.js.map +1 -0
- package/public/js/highlight.d.ts +2 -0
- package/public/js/highlight.js +116 -107
- package/public/js/highlight.js.map +1 -0
- package/public/js/html.d.ts +2 -0
- package/public/js/html.js +14 -44
- package/public/js/html.js.map +1 -0
- package/public/js/insightsView.d.ts +5 -0
- package/public/js/insightsView.js +93 -111
- package/public/js/insightsView.js.map +1 -0
- package/public/js/inspector.d.ts +37 -0
- package/public/js/inspector.js +416 -445
- package/public/js/inspector.js.map +1 -0
- package/public/js/llm.d.ts +113 -0
- package/public/js/llm.js +233 -272
- package/public/js/llm.js.map +1 -0
- package/public/js/mapView.d.ts +3 -0
- package/public/js/mapView.js +121 -120
- package/public/js/mapView.js.map +1 -0
- package/public/js/markdown.d.ts +3 -0
- package/public/js/markdown.js +84 -91
- package/public/js/markdown.js.map +1 -0
- package/public/js/mindmap.d.ts +32 -0
- package/public/js/mindmap.js +244 -259
- package/public/js/mindmap.js.map +1 -0
- package/public/js/mobilePanels.d.ts +5 -0
- package/public/js/mobilePanels.js +50 -0
- package/public/js/mobilePanels.js.map +1 -0
- package/public/js/repoFiles.d.ts +12 -0
- package/public/js/repoFiles.js +28 -27
- package/public/js/repoFiles.js.map +1 -0
- package/public/js/reviewView.d.ts +28 -0
- package/public/js/reviewView.js +256 -0
- package/public/js/reviewView.js.map +1 -0
- package/public/js/sbomView.d.ts +3 -0
- package/public/js/sbomView.js +59 -67
- package/public/js/sbomView.js.map +1 -0
- package/public/js/scanCache.d.ts +16 -0
- package/public/js/scanCache.js +67 -65
- package/public/js/scanCache.js.map +1 -0
- package/public/js/search.d.ts +7 -0
- package/public/js/search.js +868 -800
- package/public/js/search.js.map +1 -0
- package/public/js/searchLocal.d.ts +42 -0
- package/public/js/searchLocal.js +87 -0
- package/public/js/searchLocal.js.map +1 -0
- package/public/js/serverSettings.d.ts +5 -0
- package/public/js/serverSettings.js +153 -164
- package/public/js/serverSettings.js.map +1 -0
- package/public/js/state.d.ts +798 -0
- package/public/js/state.js +114 -122
- package/public/js/state.js.map +1 -0
- package/public/js/tour.d.ts +6 -0
- package/public/js/tour.js +5 -7
- package/public/js/tour.js.map +1 -0
- package/public/js/transitions.d.ts +2 -0
- package/public/js/transitions.js +9 -10
- package/public/js/transitions.js.map +1 -0
- package/public/js/tree.d.ts +12 -0
- package/public/js/tree.js +157 -154
- package/public/js/tree.js.map +1 -0
- package/public/js/workflowsView.d.ts +3 -0
- package/public/js/workflowsView.js +15 -22
- package/public/js/workflowsView.js.map +1 -0
- package/public/search.css +495 -0
- package/public/styles.css +163 -346
- package/public/workbench.css +166 -0
- package/server/apiAuth.d.ts +3 -0
- package/server/apiAuth.js +18 -20
- package/server/apiAuth.js.map +1 -0
- package/server/apiDiff.d.ts +2 -0
- package/server/apiDiff.js +20 -18
- package/server/apiDiff.js.map +1 -0
- package/server/apiDocs.d.ts +2 -0
- package/server/apiDocs.js +38 -40
- package/server/apiDocs.js.map +1 -0
- package/server/apiFile.d.ts +1 -0
- package/server/apiFile.js +14 -17
- package/server/apiFile.js.map +1 -0
- package/server/apiGitBlame.d.ts +2 -0
- package/server/apiGitBlame.js +50 -51
- package/server/apiGitBlame.js.map +1 -0
- package/server/apiMcp.d.ts +4 -0
- package/server/apiMcp.js +64 -66
- package/server/apiMcp.js.map +1 -0
- package/server/apiReview.d.ts +2 -0
- package/server/apiReview.js +45 -0
- package/server/apiReview.js.map +1 -0
- package/server/apiScan.d.ts +5 -0
- package/server/apiScan.js +90 -71
- package/server/apiScan.js.map +1 -0
- package/server/apiSearch.d.ts +33 -0
- package/server/apiSearch.js +184 -200
- package/server/apiSearch.js.map +1 -0
- package/server/apiSettings.d.ts +4 -0
- package/server/apiSettings.js +84 -85
- package/server/apiSettings.js.map +1 -0
- package/server/apiTools.d.ts +3 -0
- package/server/apiTools.js +55 -56
- package/server/apiTools.js.map +1 -0
- package/server/auth.d.ts +5 -0
- package/server/auth.js +54 -51
- package/server/auth.js.map +1 -0
- package/server/config.d.ts +42 -0
- package/server/config.js +189 -194
- package/server/config.js.map +1 -0
- package/server/daemon.d.ts +12 -0
- package/server/daemon.js +124 -114
- package/server/daemon.js.map +1 -0
- package/server/fileSourceNode.d.ts +2 -0
- package/server/fileSourceNode.js +34 -36
- package/server/fileSourceNode.js.map +1 -0
- package/server/gitClone.d.ts +9 -0
- package/server/gitClone.js +59 -61
- package/server/gitClone.js.map +1 -0
- package/server/gitDiff.d.ts +36 -0
- package/server/gitDiff.js +271 -169
- package/server/gitDiff.js.map +1 -0
- package/server/gitHistory.d.ts +12 -0
- package/server/gitHistory.js +80 -80
- package/server/gitHistory.js.map +1 -0
- package/server/gitRemote.d.ts +12 -0
- package/server/gitRemote.js +37 -38
- package/server/gitRemote.js.map +1 -0
- package/server/htmlText.d.ts +4 -0
- package/server/htmlText.js +16 -23
- package/server/htmlText.js.map +1 -0
- package/server/http.d.ts +5 -0
- package/server/http.js +27 -30
- package/server/http.js.map +1 -0
- package/server/httpGuards.d.ts +2 -0
- package/server/httpGuards.js +47 -43
- package/server/httpGuards.js.map +1 -0
- package/server/https.d.ts +34 -0
- package/server/https.js +121 -111
- package/server/https.js.map +1 -0
- package/server/index.d.ts +40 -0
- package/server/index.js +139 -147
- package/server/index.js.map +1 -0
- package/server/layout.d.ts +26 -0
- package/server/layout.js +59 -64
- package/server/layout.js.map +1 -0
- package/server/llmProxy.d.ts +3 -0
- package/server/llmProxy.js +137 -135
- package/server/llmProxy.js.map +1 -0
- package/server/logger.d.ts +38 -0
- package/server/logger.js +198 -211
- package/server/logger.js.map +1 -0
- package/server/mcp/analysis.d.ts +29 -0
- package/server/mcp/analysis.js +99 -120
- package/server/mcp/analysis.js.map +1 -0
- package/server/mcp/http.d.ts +9 -0
- package/server/mcp/http.js +128 -137
- package/server/mcp/http.js.map +1 -0
- package/server/mcp/runner.d.ts +68 -0
- package/server/mcp/runner.js +208 -218
- package/server/mcp/runner.js.map +1 -0
- package/server/mcp/server.d.ts +29 -0
- package/server/mcp/server.js +166 -179
- package/server/mcp/server.js.map +1 -0
- package/server/mcp/standalone.d.ts +1 -0
- package/server/mcp/standalone.js +8 -10
- package/server/mcp/standalone.js.map +1 -0
- package/server/mcp/tools.d.ts +2020 -0
- package/server/mcp/tools.js +523 -574
- package/server/mcp/tools.js.map +1 -0
- package/server/paths.d.ts +3 -0
- package/server/paths.js +17 -15
- package/server/paths.js.map +1 -0
- package/server/pidfile.d.ts +9 -0
- package/server/pidfile.js +43 -45
- package/server/pidfile.js.map +1 -0
- package/server/review.d.ts +5 -0
- package/server/review.js +35 -0
- package/server/review.js.map +1 -0
- package/server/router.d.ts +33 -0
- package/server/router.js +191 -197
- package/server/router.js.map +1 -0
- package/server/scanRequest.d.ts +3 -0
- package/server/scanRequest.js +36 -0
- package/server/scanRequest.js.map +1 -0
- package/server/searchIndex.d.ts +17 -0
- package/server/searchIndex.js +83 -80
- package/server/searchIndex.js.map +1 -0
- package/server/sessions.d.ts +14 -0
- package/server/sessions.js +62 -70
- package/server/sessions.js.map +1 -0
- package/server/startup.d.ts +118 -0
- package/server/startup.js +143 -148
- package/server/startup.js.map +1 -0
- package/server/static.d.ts +6 -0
- package/server/static.js +28 -31
- package/server/static.js.map +1 -0
- package/server/tools/install.d.ts +4 -0
- package/server/tools/install.js +176 -185
- package/server/tools/install.js.map +1 -0
- package/server/tools/parse.d.ts +8 -0
- package/server/tools/parse.js +112 -114
- package/server/tools/parse.js.map +1 -0
- package/server/tools/platform.d.ts +9 -0
- package/server/tools/platform.js +49 -47
- package/server/tools/platform.js.map +1 -0
- package/server/tools/registry.d.ts +137 -0
- package/server/tools/registry.js +173 -172
- package/server/tools/registry.js.map +1 -0
- package/server/tools/scan.d.ts +34 -0
- package/server/tools/scan.js +89 -98
- package/server/tools/scan.js.map +1 -0
- package/server/tools.d.ts +34 -0
- package/server/tools.js +94 -107
- package/server/tools.js.map +1 -0
- package/server/tunnel.d.ts +29 -0
- package/server/tunnel.js +61 -66
- package/server/tunnel.js.map +1 -0
- package/shared/analyzer/docs.d.ts +16 -0
- package/shared/analyzer/docs.js +99 -93
- package/shared/analyzer/docs.js.map +1 -0
- package/shared/analyzer/explainLocal.d.ts +4 -0
- package/shared/analyzer/explainLocal.js +122 -142
- package/shared/analyzer/explainLocal.js.map +1 -0
- package/shared/analyzer/github.d.ts +37 -0
- package/shared/analyzer/github.js +41 -43
- package/shared/analyzer/github.js.map +1 -0
- package/shared/analyzer/graph.d.ts +41 -0
- package/shared/analyzer/graph.js +393 -400
- package/shared/analyzer/graph.js.map +1 -0
- package/shared/analyzer/health.d.ts +33 -0
- package/shared/analyzer/health.js +197 -193
- package/shared/analyzer/health.js.map +1 -0
- package/shared/analyzer/history.d.ts +29 -0
- package/shared/analyzer/history.js +110 -111
- package/shared/analyzer/history.js.map +1 -0
- package/shared/analyzer/jsonConfig.d.ts +2 -0
- package/shared/analyzer/jsonConfig.js +61 -0
- package/shared/analyzer/jsonConfig.js.map +1 -0
- package/shared/analyzer/languages/csharp.d.ts +26 -0
- package/shared/analyzer/languages/csharp.js +83 -88
- package/shared/analyzer/languages/csharp.js.map +1 -0
- package/shared/analyzer/languages/generic.d.ts +23 -0
- package/shared/analyzer/languages/generic.js +94 -105
- package/shared/analyzer/languages/generic.js.map +1 -0
- package/shared/analyzer/languages/go.d.ts +24 -0
- package/shared/analyzer/languages/go.js +45 -54
- package/shared/analyzer/languages/go.js.map +1 -0
- package/shared/analyzer/languages/index.d.ts +6 -0
- package/shared/analyzer/languages/index.js +19 -20
- package/shared/analyzer/languages/index.js.map +1 -0
- package/shared/analyzer/languages/java.d.ts +20 -0
- package/shared/analyzer/languages/java.js +73 -78
- package/shared/analyzer/languages/java.js.map +1 -0
- package/shared/analyzer/languages/javascript.d.ts +12 -0
- package/shared/analyzer/languages/javascript.js +258 -209
- package/shared/analyzer/languages/javascript.js.map +1 -0
- package/shared/analyzer/languages/python.d.ts +23 -0
- package/shared/analyzer/languages/python.js +92 -100
- package/shared/analyzer/languages/python.js.map +1 -0
- package/shared/analyzer/languages/rust.d.ts +17 -0
- package/shared/analyzer/languages/rust.js +96 -98
- package/shared/analyzer/languages/rust.js.map +1 -0
- package/shared/analyzer/languages/typescript.d.ts +11 -0
- package/shared/analyzer/languages/typescript.js +117 -116
- package/shared/analyzer/languages/typescript.js.map +1 -0
- package/shared/analyzer/licenses.d.ts +18 -0
- package/shared/analyzer/licenses.js +130 -133
- package/shared/analyzer/licenses.js.map +1 -0
- package/shared/analyzer/metrics.d.ts +20 -0
- package/shared/analyzer/metrics.js +112 -104
- package/shared/analyzer/metrics.js.map +1 -0
- package/shared/analyzer/pathUtil.d.ts +10 -0
- package/shared/analyzer/pathUtil.js +29 -35
- package/shared/analyzer/pathUtil.js.map +1 -0
- package/shared/analyzer/patterns.d.ts +11 -0
- package/shared/analyzer/patterns.js +231 -253
- package/shared/analyzer/patterns.js.map +1 -0
- package/shared/analyzer/scan.d.ts +62 -0
- package/shared/analyzer/scan.js +451 -442
- package/shared/analyzer/scan.js.map +1 -0
- package/shared/analyzer/scanControl.d.ts +11 -0
- package/shared/analyzer/scanControl.js +62 -0
- package/shared/analyzer/scanControl.js.map +1 -0
- package/shared/analyzer/security.d.ts +18 -0
- package/shared/analyzer/security.js +159 -159
- package/shared/analyzer/security.js.map +1 -0
- package/shared/analyzer/services.d.ts +5 -0
- package/shared/analyzer/services.js +141 -138
- package/shared/analyzer/services.js.map +1 -0
- package/shared/analyzer/stack.d.ts +6 -0
- package/shared/analyzer/stack.js +134 -141
- package/shared/analyzer/stack.js.map +1 -0
- package/shared/analyzer/tour.d.ts +2 -0
- package/shared/analyzer/tour.js +45 -53
- package/shared/analyzer/tour.js.map +1 -0
- package/shared/analyzer/util.d.ts +8 -0
- package/shared/analyzer/util.js +130 -76
- package/shared/analyzer/util.js.map +1 -0
- package/shared/analyzer/workflows.d.ts +8 -0
- package/shared/analyzer/workflows.js +139 -134
- package/shared/analyzer/workflows.js.map +1 -0
- package/shared/contracts.d.ts +250 -0
- package/shared/contracts.js +2 -0
- package/shared/contracts.js.map +1 -0
- package/shared/diagram/aiFacts.d.ts +35 -0
- package/shared/diagram/aiFacts.js +102 -105
- package/shared/diagram/aiFacts.js.map +1 -0
- package/shared/diagram/aiMermaid.d.ts +5 -0
- package/shared/diagram/aiMermaid.js +46 -45
- package/shared/diagram/aiMermaid.js.map +1 -0
- package/shared/diagram/atlas.d.ts +16 -0
- package/shared/diagram/atlas.js +51 -60
- package/shared/diagram/atlas.js.map +1 -0
- package/shared/diagram/mermaid.d.ts +43 -0
- package/shared/diagram/mermaid.js +348 -370
- package/shared/diagram/mermaid.js.map +1 -0
- package/shared/review/contracts.d.ts +95 -0
- package/shared/review/contracts.js +2 -0
- package/shared/review/contracts.js.map +1 -0
- package/shared/review/review.d.ts +10 -0
- package/shared/review/review.js +184 -0
- package/shared/review/review.js.map +1 -0
- package/shared/search/query.d.ts +32 -0
- package/shared/search/query.js +395 -388
- package/shared/search/query.js.map +1 -0
package/public/js/search.js
CHANGED
|
@@ -24,145 +24,142 @@
|
|
|
24
24
|
//
|
|
25
25
|
// All DOM work happens inside `initSearch`. The module is importable in Node,
|
|
26
26
|
// which is the rule that keeps it testable — see Agent.md.
|
|
27
|
-
|
|
28
27
|
import { escapeHtml } from './html.js';
|
|
29
|
-
import {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
} from '/shared/search/query.js';
|
|
33
|
-
|
|
28
|
+
import { eventElement } from './dom.js';
|
|
29
|
+
import { buildLocalSearchIndex, searchLocalIndex } from './searchLocal.js';
|
|
30
|
+
import { highlightRanges, matchesPathFilters, parseQuery, queryIsEmpty, removeClause, } from '/shared/search/query.js';
|
|
34
31
|
// Remembering what you searched for is a courtesy, never a hard dependency.
|
|
35
32
|
const RECENTS_KEY = 'onboarder:recent-searches';
|
|
36
33
|
const MAX_RECENTS = 8;
|
|
37
|
-
|
|
38
34
|
// Rows drawn per group. The *counts* stay honest — the header says how many
|
|
39
35
|
// there are — but the palette is for jumping somewhere, not for scrolling a
|
|
40
36
|
// list of four hundred files.
|
|
41
37
|
const PREVIEW_LIMIT = 40;
|
|
42
38
|
const CODE_LIMIT = 40;
|
|
43
|
-
|
|
44
39
|
// Long enough that a fast typist makes one request instead of nine, short
|
|
45
40
|
// enough that a slow one never notices.
|
|
46
41
|
const DEBOUNCE_MS = 140;
|
|
47
|
-
|
|
48
42
|
const EXAMPLES = [
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
43
|
+
{ query: 'ext:js createServer', hint: 'a function, in one language' },
|
|
44
|
+
{ query: 'is:test', hint: 'tests only' },
|
|
45
|
+
{ query: 'path:server -vendor', hint: 'somewhere, minus a folder' },
|
|
46
|
+
{ query: '"exact phrase"', hint: 'words in that order' },
|
|
47
|
+
{ query: '/create.*Router/', hint: 'a regular expression' },
|
|
48
|
+
{ query: 'kind:class', hint: 'declarations of one kind' },
|
|
55
49
|
];
|
|
56
|
-
|
|
57
50
|
// localStorage throws in private windows, and in browser settings that forbid
|
|
58
51
|
// it. A search box that refused to open because it could not remember your last
|
|
59
52
|
// query would be an absurd way to fail, so every access is guarded.
|
|
60
53
|
function readRecents() {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
54
|
+
try {
|
|
55
|
+
const raw = localStorage.getItem(RECENTS_KEY);
|
|
56
|
+
const list = raw ? JSON.parse(raw) : [];
|
|
57
|
+
if (!Array.isArray(list))
|
|
58
|
+
return [];
|
|
59
|
+
return list.filter((entry) => typeof entry === 'string' && entry.trim()).slice(0, MAX_RECENTS);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
69
64
|
}
|
|
70
|
-
|
|
71
65
|
function writeRecents(list) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
66
|
+
try {
|
|
67
|
+
localStorage.setItem(RECENTS_KEY, JSON.stringify(list.slice(0, MAX_RECENTS)));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
/* remembering is optional */
|
|
71
|
+
}
|
|
77
72
|
}
|
|
78
|
-
|
|
79
73
|
function extensionOf(path) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
74
|
+
const name = path.split('/').pop() || '';
|
|
75
|
+
const dot = name.lastIndexOf('.');
|
|
76
|
+
if (dot <= 0)
|
|
77
|
+
return 'FILE';
|
|
78
|
+
return name.slice(dot + 1).toUpperCase();
|
|
84
79
|
}
|
|
85
|
-
|
|
86
80
|
// The human word for each clause the parser found, used on the chips row.
|
|
87
81
|
const CLAUSE_LABELS = {
|
|
88
|
-
|
|
89
|
-
|
|
82
|
+
ext: 'ext', path: 'path', kind: 'kind', is: 'is',
|
|
83
|
+
neg: 'not', phrase: 'phrase', regex: 'regex',
|
|
90
84
|
};
|
|
91
|
-
|
|
92
85
|
// One palette is live at a time, and one Cmd+K listener serves them all — it is
|
|
93
86
|
// installed once, on the first `initSearch`, and routes to the newest controller.
|
|
94
87
|
let keyListenerAttached = false;
|
|
95
88
|
let activeInstance = null;
|
|
96
|
-
|
|
97
89
|
export function initSearch(files = [], options) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
90
|
+
activeInstance?.destroy();
|
|
91
|
+
const opts = options || {};
|
|
92
|
+
let fileList = Array.isArray(files) ? files : [];
|
|
93
|
+
let localIndex = buildLocalSearchIndex(fileList);
|
|
94
|
+
const localCache = new Map();
|
|
95
|
+
const listeners = new AbortController();
|
|
96
|
+
let localFrame = null;
|
|
97
|
+
let focusBefore = null;
|
|
98
|
+
let selectedRow = null;
|
|
99
|
+
const background = new Map();
|
|
100
|
+
let scanIdProvider = opts.getScanId || (() => opts.scanId || null);
|
|
101
|
+
let isShowing = false;
|
|
102
|
+
let overlay = null;
|
|
103
|
+
let input = null;
|
|
104
|
+
let modeBar = null;
|
|
105
|
+
let chipsBar = null;
|
|
106
|
+
let chipMarkup = '';
|
|
107
|
+
let resultsList = null;
|
|
108
|
+
let statusBar = null;
|
|
109
|
+
let activeMode = 'all'; // 'all' | 'files' | 'symbols' | 'code'
|
|
110
|
+
let caseSensitive = false;
|
|
111
|
+
let selectedIndex = 0;
|
|
112
|
+
let groups = []; // what is rendered, in order
|
|
113
|
+
let flat = []; // `groups` flattened — what the arrow keys walk
|
|
114
|
+
let localFiles = [];
|
|
115
|
+
let localSymbols = [];
|
|
116
|
+
let localCounts = { files: 0, symbols: 0 };
|
|
117
|
+
let code = null; // the last /api/search payload
|
|
118
|
+
let codeLoading = false;
|
|
119
|
+
let codeError = null;
|
|
120
|
+
let parseError = null;
|
|
121
|
+
let requestSeq = 0; // every request gets a number; only the newest paints
|
|
122
|
+
let inFlight = null; // the AbortController for the request above
|
|
123
|
+
let debounceTimer = null;
|
|
124
|
+
let recents = [];
|
|
125
|
+
let lastQuery = '';
|
|
126
|
+
function getPath(file) {
|
|
127
|
+
if (!file)
|
|
128
|
+
return '';
|
|
129
|
+
return typeof file === 'string' ? file : file.path || '';
|
|
130
|
+
}
|
|
131
|
+
// The shell is built once and then reused for the session. It goes *into* the
|
|
132
|
+
// `#searchOverlay` element the page already has rather than replacing it, so
|
|
133
|
+
// the markup in `index.html` stays the single source of position and z-index.
|
|
134
|
+
function buildShell() {
|
|
135
|
+
overlay = document.getElementById('searchOverlay');
|
|
136
|
+
if (!overlay) {
|
|
137
|
+
overlay = document.createElement('div');
|
|
138
|
+
overlay.id = 'searchOverlay';
|
|
139
|
+
overlay.className = 'search-overlay';
|
|
140
|
+
document.body.appendChild(overlay);
|
|
141
|
+
}
|
|
142
|
+
if (overlay.querySelector('.search-modal')) {
|
|
143
|
+
input = overlay.querySelector('.search-input');
|
|
144
|
+
modeBar = overlay.querySelector('.search-modes');
|
|
145
|
+
chipsBar = overlay.querySelector('.search-chips');
|
|
146
|
+
resultsList = overlay.querySelector('.search-results');
|
|
147
|
+
statusBar = overlay.querySelector('.search-status');
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
overlay.hidden = true;
|
|
151
|
+
overlay.setAttribute('role', 'dialog');
|
|
152
|
+
overlay.setAttribute('aria-modal', 'true');
|
|
153
|
+
overlay.setAttribute('aria-label', 'Search this codebase');
|
|
154
|
+
overlay.innerHTML = `<div class="search-modal">
|
|
159
155
|
<div class="search-header">
|
|
160
156
|
<div class="search-inputrow">
|
|
161
157
|
<span class="search-icon" aria-hidden="true"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg></span>
|
|
162
158
|
<input class="search-input" type="text" role="combobox" aria-expanded="true"
|
|
163
159
|
aria-controls="searchResultList" aria-autocomplete="list" autocomplete="off"
|
|
164
|
-
spellcheck="false" placeholder="Search files, symbols and contents\u2026">
|
|
165
|
-
<button class="search-toggle" data-toggle="case" title="Match case" aria-pressed="false">Aa</button>
|
|
160
|
+
aria-label="Search files, symbols and contents" spellcheck="false" placeholder="Search files, symbols and contents\u2026">
|
|
161
|
+
<button class="search-toggle" data-toggle="case" title="Match case" aria-label="Match case" aria-pressed="false">Aa</button>
|
|
162
|
+
<button class="search-close" type="button" aria-label="Close search" title="Close search">×</button>
|
|
166
163
|
</div>
|
|
167
164
|
<div class="search-modes" role="tablist" aria-label="Result kinds"></div>
|
|
168
165
|
<div class="search-chips"></div>
|
|
@@ -170,705 +167,776 @@ export function initSearch(files = [], options) {
|
|
|
170
167
|
<div class="search-results" id="searchResultList" role="listbox" aria-label="Search results"></div>
|
|
171
168
|
<div class="search-status"></div>
|
|
172
169
|
</div>`;
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
170
|
+
input = overlay.querySelector('.search-input');
|
|
171
|
+
modeBar = overlay.querySelector('.search-modes');
|
|
172
|
+
chipsBar = overlay.querySelector('.search-chips');
|
|
173
|
+
resultsList = overlay.querySelector('.search-results');
|
|
174
|
+
statusBar = overlay.querySelector('.search-status');
|
|
175
|
+
input.addEventListener('input', (event) => {
|
|
176
|
+
if (event.isComposing)
|
|
177
|
+
return;
|
|
178
|
+
selectedIndex = 0;
|
|
179
|
+
scheduleLocalSearch();
|
|
180
|
+
}, { signal: listeners.signal });
|
|
181
|
+
input.addEventListener('compositionend', () => {
|
|
182
|
+
selectedIndex = 0;
|
|
183
|
+
scheduleLocalSearch();
|
|
184
|
+
}, { signal: listeners.signal });
|
|
185
|
+
input.addEventListener('keydown', onInputKeyDown, { signal: listeners.signal });
|
|
186
|
+
overlay.addEventListener('keydown', onDialogKeyDown, { signal: listeners.signal });
|
|
187
|
+
// One delegated listener for the whole palette. Every row in it is rebuilt
|
|
188
|
+
// on each keystroke, so per-row listeners would be churn; the shape of the
|
|
189
|
+
// markup is the only contract this needs.
|
|
190
|
+
overlay.addEventListener('click', (event) => {
|
|
191
|
+
if (event.target === overlay)
|
|
192
|
+
return hide();
|
|
193
|
+
const target = eventElement(event);
|
|
194
|
+
if (!target)
|
|
195
|
+
return;
|
|
196
|
+
if (target.closest('.search-close'))
|
|
197
|
+
return hide();
|
|
198
|
+
const mode = eventElement(event)?.closest('.search-mode-chip');
|
|
199
|
+
if (mode)
|
|
200
|
+
return setMode(mode.dataset.mode);
|
|
201
|
+
const toggle = eventElement(event)?.closest('.search-toggle');
|
|
202
|
+
if (toggle)
|
|
203
|
+
return toggleCase();
|
|
204
|
+
const remove = eventElement(event)?.closest('.search-chip-remove');
|
|
205
|
+
if (remove)
|
|
206
|
+
return removeChip(Number(remove.dataset.clause));
|
|
207
|
+
const row = eventElement(event)?.closest('.search-result');
|
|
208
|
+
if (row) {
|
|
209
|
+
const index = Number(row.dataset.index);
|
|
210
|
+
if (Number.isFinite(index) && flat[index])
|
|
211
|
+
select(flat[index]);
|
|
212
|
+
}
|
|
213
|
+
return undefined;
|
|
214
|
+
}, { signal: listeners.signal });
|
|
215
|
+
overlay.addEventListener('mouseover', (event) => {
|
|
216
|
+
const row = eventElement(event)?.closest('.search-result');
|
|
217
|
+
if (!row)
|
|
218
|
+
return;
|
|
219
|
+
const index = Number(row.dataset.index);
|
|
220
|
+
if (!Number.isFinite(index) || index === selectedIndex)
|
|
221
|
+
return;
|
|
222
|
+
selectedIndex = index;
|
|
223
|
+
paintSelection(false);
|
|
224
|
+
}, { signal: listeners.signal });
|
|
225
|
+
}
|
|
226
|
+
// ---- the pipeline -------------------------------------------------------
|
|
227
|
+
// The symbol index belongs to the scan; repeated queries reuse their bounded
|
|
228
|
+
// previews rather than extracting declarations and sorting every match again.
|
|
229
|
+
function collectLocal(parsed) {
|
|
230
|
+
const key = `${caseSensitive}:${parsed.raw}`;
|
|
231
|
+
const cached = localCache.get(key);
|
|
232
|
+
if (cached)
|
|
233
|
+
return cached;
|
|
234
|
+
const result = searchLocalIndex(localIndex, parsed, PREVIEW_LIMIT);
|
|
235
|
+
if (localCache.size >= 12)
|
|
236
|
+
localCache.delete(localCache.keys().next().value);
|
|
237
|
+
localCache.set(key, result);
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
// Coalesce keys in the same frame and invalidate wire results immediately.
|
|
241
|
+
function scheduleLocalSearch() {
|
|
242
|
+
requestSeq++;
|
|
243
|
+
inFlight?.abort();
|
|
244
|
+
inFlight = null;
|
|
245
|
+
clearTimeout(debounceTimer);
|
|
246
|
+
if (localFrame !== null)
|
|
247
|
+
cancelAnimationFrame(localFrame);
|
|
248
|
+
localFrame = requestAnimationFrame(() => { localFrame = null; if (isShowing)
|
|
249
|
+
runSearch(); });
|
|
250
|
+
}
|
|
251
|
+
// Is a content search worth a round trip? In a single-kind view, only that
|
|
252
|
+
// kind matters. In the combined view a filter-only query — `is:test`, say —
|
|
253
|
+
// is already answered by the file list, so asking the server would be a
|
|
254
|
+
// request whose answer we would throw away.
|
|
255
|
+
function wantsCode(parsed) {
|
|
256
|
+
if (queryIsEmpty(parsed) || !scanIdProvider())
|
|
257
|
+
return false;
|
|
258
|
+
if (activeMode === 'files' || activeMode === 'symbols')
|
|
259
|
+
return false;
|
|
260
|
+
if (activeMode === 'code')
|
|
261
|
+
return true;
|
|
262
|
+
return parsed.terms.some((term) => term.length >= 2)
|
|
263
|
+
|| parsed.phrases.length > 0 || parsed.regex !== null;
|
|
264
|
+
}
|
|
265
|
+
function scheduleCodeSearch(raw) {
|
|
266
|
+
clearTimeout(debounceTimer);
|
|
267
|
+
codeLoading = true;
|
|
268
|
+
debounceTimer = setTimeout(() => {
|
|
269
|
+
sendCodeSearch(raw);
|
|
270
|
+
}, DEBOUNCE_MS);
|
|
271
|
+
}
|
|
272
|
+
// The sequencing is the point. `requestSeq` is bumped by every keystroke *and*
|
|
273
|
+
// by every send, and a response is only allowed to paint if its number is
|
|
274
|
+
// still the current one. Without that, a slow answer to a short query could
|
|
275
|
+
// land after a fast answer to a longer one, and the palette would show results
|
|
276
|
+
// for something you had already stopped typing — the bug that made fast
|
|
277
|
+
// typing untrustworthy.
|
|
278
|
+
async function sendCodeSearch(raw) {
|
|
279
|
+
const scanId = scanIdProvider();
|
|
280
|
+
if (!scanId) {
|
|
281
|
+
codeLoading = false;
|
|
282
|
+
render(currentParsed);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const seq = ++requestSeq;
|
|
286
|
+
const controller = new AbortController();
|
|
287
|
+
inFlight = controller;
|
|
288
|
+
try {
|
|
289
|
+
const res = await fetch('/api/search', {
|
|
290
|
+
method: 'POST',
|
|
291
|
+
headers: { 'Content-Type': 'application/json' },
|
|
292
|
+
signal: controller.signal,
|
|
293
|
+
body: JSON.stringify({ scanId, query: raw, limit: CODE_LIMIT, caseSensitive }),
|
|
294
|
+
});
|
|
295
|
+
if (seq !== requestSeq)
|
|
296
|
+
return;
|
|
297
|
+
const data = await res.json().catch(() => null);
|
|
298
|
+
if (seq !== requestSeq)
|
|
299
|
+
return;
|
|
300
|
+
if (!res.ok)
|
|
301
|
+
throw new Error(data?.error || `Search failed (${res.status}).`);
|
|
302
|
+
if (!data) {
|
|
303
|
+
code = null;
|
|
304
|
+
codeError = 'The search endpoint returned nothing readable.';
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
code = data;
|
|
308
|
+
codeError = data.error || null;
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
// An aborted request is not a failure; it is the newer query arriving.
|
|
312
|
+
if (seq !== requestSeq || (err && err.name === 'AbortError'))
|
|
313
|
+
return;
|
|
314
|
+
code = null;
|
|
315
|
+
codeError = 'Could not search file contents: ' + ((err && err.message) || err);
|
|
316
|
+
}
|
|
317
|
+
finally {
|
|
318
|
+
if (seq === requestSeq) {
|
|
319
|
+
codeLoading = false;
|
|
320
|
+
inFlight = null;
|
|
321
|
+
render(currentParsed);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// The whole keystroke-to-pixels path. Local work first, so results are on
|
|
326
|
+
// screen before the request for the slower half has even been sent.
|
|
327
|
+
function runSearch() {
|
|
328
|
+
if (localFrame !== null) {
|
|
329
|
+
cancelAnimationFrame(localFrame);
|
|
330
|
+
localFrame = null;
|
|
331
|
+
}
|
|
332
|
+
const raw = input ? input.value.trim() : '';
|
|
333
|
+
lastQuery = raw;
|
|
334
|
+
const parsed = parsedQuery(raw);
|
|
335
|
+
currentParsed = parsed;
|
|
336
|
+
parseError = parsed.error;
|
|
337
|
+
const local = collectLocal(parsed);
|
|
338
|
+
localFiles = local.files;
|
|
339
|
+
localSymbols = local.symbols;
|
|
340
|
+
localCounts = local.counts;
|
|
341
|
+
// Whatever was asked before this keystroke is stale now, on the wire or not.
|
|
342
|
+
requestSeq++;
|
|
343
|
+
if (inFlight) {
|
|
344
|
+
inFlight.abort();
|
|
345
|
+
inFlight = null;
|
|
346
|
+
}
|
|
347
|
+
clearTimeout(debounceTimer);
|
|
324
348
|
code = null;
|
|
325
|
-
codeError =
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
code = data;
|
|
329
|
-
codeError = data.error || null;
|
|
330
|
-
} catch (err) {
|
|
331
|
-
// An aborted request is not a failure; it is the newer query arriving.
|
|
332
|
-
if (seq !== requestSeq || (err && err.name === 'AbortError')) return;
|
|
333
|
-
code = null;
|
|
334
|
-
codeError = 'Could not search file contents: ' + ((err && err.message) || err);
|
|
335
|
-
} finally {
|
|
336
|
-
if (seq === requestSeq) {
|
|
349
|
+
codeError = null;
|
|
337
350
|
codeLoading = false;
|
|
351
|
+
if (!parseError && wantsCode(parsed))
|
|
352
|
+
scheduleCodeSearch(raw);
|
|
353
|
+
render(parsed);
|
|
354
|
+
}
|
|
355
|
+
// ---- what to show -------------------------------------------------------
|
|
356
|
+
function flatten(list) {
|
|
357
|
+
const out = [];
|
|
358
|
+
for (const group of list) {
|
|
359
|
+
for (const item of group.items)
|
|
360
|
+
out.push(item);
|
|
361
|
+
}
|
|
362
|
+
return out;
|
|
363
|
+
}
|
|
364
|
+
// A content hit on the line a symbol row already names is the same
|
|
365
|
+
// destination twice, and the symbol row is the more useful label ("createRouter
|
|
366
|
+
// in server/router.js, line 90" beats "a line that contains createRouter").
|
|
367
|
+
function codeMatches() {
|
|
368
|
+
if (!code || !Array.isArray(code.results))
|
|
369
|
+
return [];
|
|
370
|
+
const claimed = new Set(localSymbols.map((sym) => `${sym.path}:${sym.line || ''}`));
|
|
371
|
+
return code.results
|
|
372
|
+
.filter((hit) => hit && hit.path && !claimed.has(`${hit.path}:${hit.line || ''}`))
|
|
373
|
+
.map((hit) => ({
|
|
374
|
+
type: 'code',
|
|
375
|
+
path: hit.path,
|
|
376
|
+
line: hit.line,
|
|
377
|
+
snippet: hit.snippet || '',
|
|
378
|
+
score: hit.score || 0,
|
|
379
|
+
}));
|
|
380
|
+
}
|
|
381
|
+
function buildGroups(parsed) {
|
|
382
|
+
const list = [];
|
|
383
|
+
if (queryIsEmpty(parsed)) {
|
|
384
|
+
// Nothing typed: offer what was typed before, then the way in, then the
|
|
385
|
+
// syntax — which is the only place the advanced grammar is discoverable.
|
|
386
|
+
if (recents.length) {
|
|
387
|
+
list.push({
|
|
388
|
+
key: 'recents',
|
|
389
|
+
label: 'Recent searches',
|
|
390
|
+
items: recents.map((query, i) => ({ type: 'recent', query, score: MAX_RECENTS - i })),
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
if (fileList.length) {
|
|
394
|
+
list.push({
|
|
395
|
+
key: 'start',
|
|
396
|
+
label: 'Start somewhere',
|
|
397
|
+
items: fileList.slice(0, 8).map((file) => ({ type: 'file', path: getPath(file), score: 1 })),
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
list.push({
|
|
401
|
+
key: 'examples',
|
|
402
|
+
label: 'Search syntax',
|
|
403
|
+
items: EXAMPLES.map((example) => ({
|
|
404
|
+
type: 'example', query: example.query, hint: example.hint, score: 1,
|
|
405
|
+
})),
|
|
406
|
+
});
|
|
407
|
+
groups = list;
|
|
408
|
+
flat = flatten(list);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const showFiles = activeMode === 'all' || activeMode === 'files';
|
|
412
|
+
const showSymbols = activeMode === 'all' || activeMode === 'symbols';
|
|
413
|
+
const showCode = activeMode === 'all' || activeMode === 'code';
|
|
414
|
+
if (showFiles && localFiles.length) {
|
|
415
|
+
list.push({ key: 'files', label: 'Files', total: localCounts.files, items: localFiles.slice(0, PREVIEW_LIMIT) });
|
|
416
|
+
}
|
|
417
|
+
if (showSymbols && localSymbols.length) {
|
|
418
|
+
list.push({ key: 'symbols', label: 'Symbols', total: localCounts.symbols, items: localSymbols.slice(0, PREVIEW_LIMIT) });
|
|
419
|
+
}
|
|
420
|
+
if (showCode) {
|
|
421
|
+
const hits = codeMatches();
|
|
422
|
+
if (hits.length) {
|
|
423
|
+
list.push({
|
|
424
|
+
key: 'code',
|
|
425
|
+
label: 'Code matches',
|
|
426
|
+
total: (code && code.total) || hits.length,
|
|
427
|
+
items: hits.slice(0, PREVIEW_LIMIT),
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
groups = list;
|
|
432
|
+
flat = flatten(list);
|
|
433
|
+
}
|
|
434
|
+
// ---- painting -----------------------------------------------------------
|
|
435
|
+
// Every span the query matched, not just the first. The old highlighter
|
|
436
|
+
// stopped at one occurrence, which made a two-word query look like it had
|
|
437
|
+
// only matched half of itself.
|
|
438
|
+
function highlightAll(text, parsed) {
|
|
439
|
+
const value = String(text ?? '');
|
|
440
|
+
const ranges = highlightRanges(value, parsed);
|
|
441
|
+
if (!ranges.length)
|
|
442
|
+
return escapeHtml(value);
|
|
443
|
+
let out = '';
|
|
444
|
+
let at = 0;
|
|
445
|
+
for (const [start, end] of ranges) {
|
|
446
|
+
if (start < at)
|
|
447
|
+
continue; // merged already, but never double-wrap
|
|
448
|
+
out += escapeHtml(value.slice(at, start));
|
|
449
|
+
out += `<mark class="search-match">${escapeHtml(value.slice(start, end))}</mark>`;
|
|
450
|
+
at = end;
|
|
451
|
+
}
|
|
452
|
+
return out + escapeHtml(value.slice(at));
|
|
453
|
+
}
|
|
454
|
+
function render(parsed) {
|
|
455
|
+
if (!isShowing || !overlay || !resultsList)
|
|
456
|
+
return;
|
|
457
|
+
buildGroups(parsed);
|
|
458
|
+
renderModes(parsed);
|
|
459
|
+
renderChips(parsed);
|
|
460
|
+
renderRows(parsed);
|
|
461
|
+
renderStatus(parsed);
|
|
462
|
+
}
|
|
463
|
+
function renderModes(parsed) {
|
|
464
|
+
if (!modeBar)
|
|
465
|
+
return;
|
|
466
|
+
const empty = queryIsEmpty(parsed);
|
|
467
|
+
const localSum = localCounts.files + localCounts.symbols;
|
|
468
|
+
const codeTotal = (code && code.total) || 0;
|
|
469
|
+
const codeText = codeLoading ? '\u2026' : (code ? String(codeTotal) : '');
|
|
470
|
+
const allText = codeLoading ? `${localSum}+` : String(localSum + codeTotal);
|
|
471
|
+
const chips = [
|
|
472
|
+
['all', 'All', allText],
|
|
473
|
+
['files', 'Files', String(localCounts.files)],
|
|
474
|
+
['symbols', 'Symbols', String(localCounts.symbols)],
|
|
475
|
+
['code', 'Code', codeText],
|
|
476
|
+
];
|
|
477
|
+
if (!modeBar.children.length)
|
|
478
|
+
modeBar.innerHTML = chips.map(([mode, label]) => `<button class="search-mode-chip" data-mode="${mode}" role="tab"><span>${label}</span><span class="search-mode-count"></span></button>`).join('');
|
|
479
|
+
for (const [mode, , count] of chips) {
|
|
480
|
+
const button = modeBar.querySelector(`[data-mode="${mode}"]`);
|
|
481
|
+
const active = mode === activeMode;
|
|
482
|
+
button.classList.toggle('is-active', active);
|
|
483
|
+
button.setAttribute('aria-selected', String(active));
|
|
484
|
+
button.tabIndex = active ? 0 : -1;
|
|
485
|
+
const badge = button.querySelector('.search-mode-count');
|
|
486
|
+
badge.hidden = empty || count === '';
|
|
487
|
+
badge.textContent = count;
|
|
488
|
+
}
|
|
489
|
+
const toggle = overlay.querySelector('.search-toggle');
|
|
490
|
+
toggle.classList.toggle('is-active', caseSensitive);
|
|
491
|
+
toggle.setAttribute('aria-pressed', String(caseSensitive));
|
|
492
|
+
}
|
|
493
|
+
function renderChips(parsed) {
|
|
494
|
+
if (!chipsBar)
|
|
495
|
+
return;
|
|
496
|
+
// Terms are deliberately not chipped: they are already legible in the input,
|
|
497
|
+
// and a chip per word would double the width of the row to say what you can
|
|
498
|
+
// see. Filters, phrases and regexes are the decisions worth showing — and
|
|
499
|
+
// worth being able to take back with one click. The case toggle is not a
|
|
500
|
+
// clause, so it lives beside the input rather than here; without a chip of
|
|
501
|
+
// its own, an empty query leaves this row hidden.
|
|
502
|
+
const parts = queryIsEmpty(parsed)
|
|
503
|
+
? []
|
|
504
|
+
: parsed.clauses
|
|
505
|
+
.map((clause, index) => ({ clause, index }))
|
|
506
|
+
.filter((entry) => entry.clause.kind !== 'term')
|
|
507
|
+
.map(({ clause, index }) => {
|
|
508
|
+
const kind = CLAUSE_LABELS[clause.kind] || clause.kind;
|
|
509
|
+
return `<span class="search-chip"><span class="search-chip-kind">${escapeHtml(kind)}</span>`
|
|
510
|
+
+ `${escapeHtml(clause.raw)}<button class="search-chip-remove" data-clause="${index}" `
|
|
511
|
+
+ `title="Remove this filter" aria-label="Remove this filter">\u00d7</button></span>`;
|
|
512
|
+
});
|
|
513
|
+
const markup = parts.join('');
|
|
514
|
+
// A content response should not replace a filter button that has focus.
|
|
515
|
+
if (markup !== chipMarkup) {
|
|
516
|
+
chipsBar.innerHTML = markup;
|
|
517
|
+
chipMarkup = markup;
|
|
518
|
+
}
|
|
519
|
+
chipsBar.hidden = !parts.length;
|
|
520
|
+
}
|
|
521
|
+
function rowHtml(item, index, parsed) {
|
|
522
|
+
const on = index === selectedIndex;
|
|
523
|
+
const attrs = `class="search-result is-${item.type}${on ? ' is-selected' : ''}" `
|
|
524
|
+
+ `role="option" aria-selected="${on}" id="search-option-${index}" data-index="${index}"`;
|
|
525
|
+
if (item.type === 'recent') {
|
|
526
|
+
return `<div ${attrs}><div class="search-item-main">`
|
|
527
|
+
+ `<span class="search-ext-badge is-recent">\u21ba</span>`
|
|
528
|
+
+ `<span class="search-title"><span class="search-name">${escapeHtml(item.query)}</span>`
|
|
529
|
+
+ `<span class="search-dir">recent search</span></span></div></div>`;
|
|
530
|
+
}
|
|
531
|
+
if (item.type === 'example') {
|
|
532
|
+
return `<div ${attrs}><div class="search-item-main">`
|
|
533
|
+
+ `<span class="search-ext-badge is-syntax">/</span>`
|
|
534
|
+
+ `<span class="search-title"><span class="search-name">${escapeHtml(item.query)}</span>`
|
|
535
|
+
+ `<span class="search-dir">${escapeHtml(item.hint || '')}</span></span></div></div>`;
|
|
536
|
+
}
|
|
537
|
+
const path = item.path;
|
|
538
|
+
const name = path.split('/').pop();
|
|
539
|
+
const dir = path.includes('/') ? path.slice(0, path.lastIndexOf('/')) : '';
|
|
540
|
+
if (item.type === 'symbol') {
|
|
541
|
+
return `<div ${attrs}><div class="search-item-main">`
|
|
542
|
+
+ `<span class="search-ext-badge is-symbol">SYM</span>`
|
|
543
|
+
+ `<span class="search-title"><span class="search-name">${highlightAll(item.name, parsed)}</span>`
|
|
544
|
+
+ `<span class="search-dir">${escapeHtml(path)}${item.line ? ' \u00b7 L' + item.line : ''}</span></span>`
|
|
545
|
+
+ `<span class="search-type-tag is-sym">${escapeHtml(item.kind || 'symbol')}</span>`
|
|
546
|
+
+ `</div></div>`;
|
|
547
|
+
}
|
|
548
|
+
if (item.type === 'code') {
|
|
549
|
+
const snippet = item.snippet
|
|
550
|
+
? `<div class="search-snippet"><code>${highlightAll(item.snippet, parsed)}</code></div>`
|
|
551
|
+
: '';
|
|
552
|
+
return `<div ${attrs}><div class="search-item-main">`
|
|
553
|
+
+ `<span class="search-ext-badge is-code">CODE</span>`
|
|
554
|
+
+ `<span class="search-title"><span class="search-name">${escapeHtml(name)}</span>`
|
|
555
|
+
+ `<span class="search-dir">${escapeHtml(path)}${item.line ? ' \u00b7 Line ' + item.line : ''}</span></span>`
|
|
556
|
+
+ `<span class="search-type-tag is-code">match</span>`
|
|
557
|
+
+ `</div>${snippet}</div>`;
|
|
558
|
+
}
|
|
559
|
+
return `<div ${attrs}><div class="search-item-main">`
|
|
560
|
+
+ `<span class="search-ext-badge">${escapeHtml(extensionOf(path))}</span>`
|
|
561
|
+
+ `<span class="search-title"><span class="search-name">${highlightAll(name, parsed)}</span>`
|
|
562
|
+
+ `<span class="search-dir">${escapeHtml(dir)}</span></span>`
|
|
563
|
+
+ `<span class="search-type-tag">file</span></div></div>`;
|
|
564
|
+
}
|
|
565
|
+
function renderRows(parsed) {
|
|
566
|
+
if (!resultsList)
|
|
567
|
+
return;
|
|
568
|
+
selectedRow = null;
|
|
569
|
+
input?.removeAttribute('aria-activedescendant');
|
|
570
|
+
if (!groups.length) {
|
|
571
|
+
resultsList.innerHTML = emptyState(parsed);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
if (selectedIndex >= flat.length)
|
|
575
|
+
selectedIndex = Math.max(0, flat.length - 1);
|
|
576
|
+
let index = -1;
|
|
577
|
+
resultsList.innerHTML = groups.map((group) => {
|
|
578
|
+
const rows = group.items.map((item) => {
|
|
579
|
+
index++;
|
|
580
|
+
return rowHtml(item, index, parsed);
|
|
581
|
+
}).join('');
|
|
582
|
+
const count = group.total === undefined ? group.items.length : group.total;
|
|
583
|
+
return `<div class="search-group" role="group" aria-label="${escapeHtml(group.label)}">`
|
|
584
|
+
+ `<div class="search-group-head"><span>${escapeHtml(group.label)}</span>`
|
|
585
|
+
+ `<span class="search-group-count">${escapeHtml(String(count))}</span></div>`
|
|
586
|
+
+ `${rows}</div>`;
|
|
587
|
+
}).join('');
|
|
588
|
+
selectedRow = resultsList.querySelector(`[data-index="${selectedIndex}"]`);
|
|
589
|
+
if (selectedRow)
|
|
590
|
+
input.setAttribute('aria-activedescendant', selectedRow.id);
|
|
591
|
+
}
|
|
592
|
+
function emptyState(parsed) {
|
|
593
|
+
if (parseError) {
|
|
594
|
+
return '<div class="search-empty"><strong>That pattern will not compile.</strong>'
|
|
595
|
+
+ `<div class="search-empty-hint">${escapeHtml(parseError)}</div></div>`;
|
|
596
|
+
}
|
|
597
|
+
if (codeLoading) {
|
|
598
|
+
return '<div class="search-empty"><span class="search-spinner"></span> Searching file contents\u2026</div>';
|
|
599
|
+
}
|
|
600
|
+
if (queryIsEmpty(parsed)) {
|
|
601
|
+
return '<div class="search-empty">Type to search this codebase.</div>';
|
|
602
|
+
}
|
|
603
|
+
return `<div class="search-empty">Nothing matched <code>${escapeHtml(parsed.raw.trim())}</code>.`
|
|
604
|
+
+ '<div class="search-empty-hint">Try fewer words, or drop a filter — <code>ext:</code>, '
|
|
605
|
+
+ '<code>path:</code> and <code>-word</code> all narrow the same list, and <kbd>Alt + ←/→</kbd> '
|
|
606
|
+
+ 'switches what kind of result you are looking at.</div></div>';
|
|
607
|
+
}
|
|
608
|
+
function renderStatus(parsed) {
|
|
609
|
+
if (!statusBar)
|
|
610
|
+
return;
|
|
611
|
+
const bits = [];
|
|
612
|
+
if (parseError) {
|
|
613
|
+
bits.push(`<span class="search-err">${escapeHtml(parseError)}</span>`);
|
|
614
|
+
}
|
|
615
|
+
else if (codeError) {
|
|
616
|
+
bits.push(`<span class="search-err">${escapeHtml(codeError)}</span>`);
|
|
617
|
+
}
|
|
618
|
+
else if (queryIsEmpty(parsed)) {
|
|
619
|
+
bits.push('Type to search. <kbd>Tab</kbd> moves focus, <kbd>Esc</kbd> clears or closes.');
|
|
620
|
+
}
|
|
621
|
+
else {
|
|
622
|
+
const total = localCounts.files + localCounts.symbols + ((code && code.total) || 0);
|
|
623
|
+
bits.push(`${total} match${total === 1 ? '' : 'es'}`);
|
|
624
|
+
if (codeLoading)
|
|
625
|
+
bits.push('<span class="search-spinner"></span> searching contents');
|
|
626
|
+
if (code) {
|
|
627
|
+
bits.push(`${code.indexed || 0} files indexed`);
|
|
628
|
+
if (code.capped)
|
|
629
|
+
bits.push(`showing the top ${code.results.length}`);
|
|
630
|
+
// Saying that the filters are enforced is the honest counterpart to the
|
|
631
|
+
// forgiving ranking a plain word search still gets.
|
|
632
|
+
if (code.advanced)
|
|
633
|
+
bits.push('filters enforced');
|
|
634
|
+
}
|
|
635
|
+
const skipped = code && code.stats && code.stats.skipped;
|
|
636
|
+
if (skipped) {
|
|
637
|
+
const count = Object.keys(skipped).reduce((sum, key) => sum + (skipped[key] || 0), 0);
|
|
638
|
+
if (count)
|
|
639
|
+
bits.push(`${count} not indexed`);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
statusBar.innerHTML = `<div class="search-status-line">${bits.join('<span class="search-sep">\u00b7</span>')}</div>`
|
|
643
|
+
+ '<div class="search-keys"><span><kbd>\u2191</kbd><kbd>\u2193</kbd> move</span>'
|
|
644
|
+
+ '<span><kbd>\u21b5</kbd> open</span><span><kbd>\u2318\u21b5</kbd> code</span>'
|
|
645
|
+
+ '<span><kbd>alt</kbd><kbd>←/→</kbd> kind</span><span><kbd>esc</kbd> close</span></div>';
|
|
646
|
+
}
|
|
647
|
+
// ---- interaction --------------------------------------------------------
|
|
648
|
+
let currentParsed = parseQuery('');
|
|
649
|
+
function parsedQuery(raw) {
|
|
650
|
+
return parseQuery(raw, { caseSensitive });
|
|
651
|
+
}
|
|
652
|
+
function moveSelection(delta) {
|
|
653
|
+
if (!flat.length)
|
|
654
|
+
return;
|
|
655
|
+
selectedIndex = Math.min(Math.max(selectedIndex + delta, 0), flat.length - 1);
|
|
656
|
+
paintSelection();
|
|
657
|
+
}
|
|
658
|
+
function paintSelection(scroll = true) {
|
|
659
|
+
if (!resultsList)
|
|
660
|
+
return;
|
|
661
|
+
const next = resultsList.querySelector(`[data-index="${selectedIndex}"]`);
|
|
662
|
+
if (selectedRow && selectedRow !== next) {
|
|
663
|
+
selectedRow.classList.remove('is-selected');
|
|
664
|
+
selectedRow.setAttribute('aria-selected', 'false');
|
|
665
|
+
}
|
|
666
|
+
selectedRow = next;
|
|
667
|
+
if (!next) {
|
|
668
|
+
input.removeAttribute('aria-activedescendant');
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
next.classList.add('is-selected');
|
|
672
|
+
next.setAttribute('aria-selected', 'true');
|
|
673
|
+
input.setAttribute('aria-activedescendant', next.id);
|
|
674
|
+
if (scroll) {
|
|
675
|
+
const row = next.getBoundingClientRect();
|
|
676
|
+
const list = resultsList.getBoundingClientRect();
|
|
677
|
+
if (row.bottom > list.bottom)
|
|
678
|
+
resultsList.scrollTop += row.bottom - list.bottom;
|
|
679
|
+
else if (row.top < list.top)
|
|
680
|
+
resultsList.scrollTop -= list.top - row.top;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
function onDialogKeyDown(event) {
|
|
684
|
+
if (event.key === 'Escape' && event.target !== input) {
|
|
685
|
+
event.preventDefault();
|
|
686
|
+
event.stopPropagation();
|
|
687
|
+
hide();
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
const tab = eventElement(event)?.closest('.search-mode-chip');
|
|
691
|
+
if (tab && ['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) {
|
|
692
|
+
event.preventDefault();
|
|
693
|
+
event.stopPropagation();
|
|
694
|
+
const tabs = [...modeBar.querySelectorAll('.search-mode-chip')];
|
|
695
|
+
const at = tabs.indexOf(tab);
|
|
696
|
+
const next = event.key === 'Home' ? 0 : event.key === 'End' ? tabs.length - 1
|
|
697
|
+
: (at + (event.key === 'ArrowLeft' ? -1 : 1) + tabs.length) % tabs.length;
|
|
698
|
+
setMode(tabs[next].dataset.mode);
|
|
699
|
+
tabs[next].focus({ preventScroll: true });
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
// Tab follows the controls and wraps inside the modal.
|
|
703
|
+
if (event.key !== 'Tab')
|
|
704
|
+
return;
|
|
705
|
+
const focusable = [...overlay.querySelectorAll('input, button')].filter((el) => !el.hidden && el.getClientRects().length);
|
|
706
|
+
const at = focusable.indexOf(document.activeElement);
|
|
707
|
+
event.preventDefault();
|
|
708
|
+
event.stopPropagation();
|
|
709
|
+
focusable[(at + (event.shiftKey ? -1 : 1) + focusable.length) % focusable.length]?.focus({ preventScroll: true });
|
|
710
|
+
}
|
|
711
|
+
function onInputKeyDown(event) {
|
|
712
|
+
const key = event.key;
|
|
713
|
+
if (event.isComposing)
|
|
714
|
+
return;
|
|
715
|
+
if (localFrame !== null && ['ArrowDown', 'ArrowUp', 'Home', 'End', 'Enter'].includes(key))
|
|
716
|
+
runSearch();
|
|
717
|
+
if (key === 'Tab')
|
|
718
|
+
return;
|
|
719
|
+
if (event.altKey && (key === 'ArrowLeft' || key === 'ArrowRight')) {
|
|
720
|
+
event.preventDefault();
|
|
721
|
+
event.stopPropagation();
|
|
722
|
+
return cycleMode(key === 'ArrowLeft' ? -1 : 1);
|
|
723
|
+
}
|
|
724
|
+
// The palette is modal, so the keys it handles stop here instead of reaching
|
|
725
|
+
// the window listener — otherwise one Escape would clear the query and close
|
|
726
|
+
// the palette in the same keystroke.
|
|
727
|
+
if (key === 'ArrowDown' || key === 'ArrowUp' || key === 'Tab' || key === 'Home'
|
|
728
|
+
|| key === 'End' || key === 'Enter' || key === 'Escape') {
|
|
729
|
+
event.stopPropagation();
|
|
730
|
+
}
|
|
731
|
+
if (key === 'ArrowDown') {
|
|
732
|
+
event.preventDefault();
|
|
733
|
+
return moveSelection(1);
|
|
734
|
+
}
|
|
735
|
+
if (key === 'ArrowUp') {
|
|
736
|
+
event.preventDefault();
|
|
737
|
+
return moveSelection(-1);
|
|
738
|
+
}
|
|
739
|
+
if (key === 'Home') {
|
|
740
|
+
event.preventDefault();
|
|
741
|
+
selectedIndex = 0;
|
|
742
|
+
return paintSelection();
|
|
743
|
+
}
|
|
744
|
+
if (key === 'End') {
|
|
745
|
+
event.preventDefault();
|
|
746
|
+
selectedIndex = Math.max(0, flat.length - 1);
|
|
747
|
+
return paintSelection();
|
|
748
|
+
}
|
|
749
|
+
if (key === 'Enter') {
|
|
750
|
+
event.preventDefault();
|
|
751
|
+
const item = flat[selectedIndex];
|
|
752
|
+
if (item)
|
|
753
|
+
select(item, { forceCode: event.metaKey || event.ctrlKey });
|
|
754
|
+
return undefined;
|
|
755
|
+
}
|
|
756
|
+
if (key === 'Escape') {
|
|
757
|
+
event.preventDefault();
|
|
758
|
+
// One Escape clears the query, a second closes the palette. Losing a long
|
|
759
|
+
// query to a stray keypress is worse than one more keystroke to close.
|
|
760
|
+
if (input && input.value) {
|
|
761
|
+
input.value = '';
|
|
762
|
+
selectedIndex = 0;
|
|
763
|
+
return runSearch();
|
|
764
|
+
}
|
|
765
|
+
return hide();
|
|
766
|
+
}
|
|
767
|
+
if (key === 'Backspace' && input && !input.value) {
|
|
768
|
+
// Backspace on an empty input takes back the last filter, the way a token
|
|
769
|
+
// editor does. There is nothing else for the key to mean here.
|
|
770
|
+
const removable = currentParsed.clauses.filter((clause) => clause.kind !== 'term');
|
|
771
|
+
if (removable.length) {
|
|
772
|
+
event.preventDefault();
|
|
773
|
+
return removeChip(currentParsed.clauses.indexOf(removable[removable.length - 1]));
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
return undefined;
|
|
777
|
+
}
|
|
778
|
+
function setMode(mode) {
|
|
779
|
+
if (!mode || mode === activeMode)
|
|
780
|
+
return;
|
|
781
|
+
activeMode = mode;
|
|
782
|
+
selectedIndex = 0;
|
|
783
|
+
runSearch();
|
|
784
|
+
if (input)
|
|
785
|
+
input.focus();
|
|
786
|
+
}
|
|
787
|
+
function cycleMode(delta) {
|
|
788
|
+
const order = ['all', 'files', 'symbols', 'code'];
|
|
789
|
+
const at = order.indexOf(activeMode);
|
|
790
|
+
setMode(order[(at + delta + order.length) % order.length]);
|
|
791
|
+
}
|
|
792
|
+
function toggleCase() {
|
|
793
|
+
caseSensitive = !caseSensitive;
|
|
794
|
+
selectedIndex = 0;
|
|
795
|
+
runSearch();
|
|
796
|
+
if (input)
|
|
797
|
+
input.focus();
|
|
798
|
+
}
|
|
799
|
+
function useQuery(query) {
|
|
800
|
+
if (!input)
|
|
801
|
+
return;
|
|
802
|
+
input.value = query;
|
|
803
|
+
selectedIndex = 0;
|
|
804
|
+
runSearch();
|
|
805
|
+
input.focus();
|
|
806
|
+
}
|
|
807
|
+
function removeChip(index) {
|
|
808
|
+
if (!input)
|
|
809
|
+
return;
|
|
810
|
+
const clause = currentParsed.clauses[index];
|
|
811
|
+
if (!clause)
|
|
812
|
+
return;
|
|
813
|
+
input.value = removeClause(input.value, clause);
|
|
814
|
+
selectedIndex = 0;
|
|
815
|
+
runSearch();
|
|
816
|
+
input.focus();
|
|
817
|
+
}
|
|
818
|
+
function rememberSearch(query) {
|
|
819
|
+
const value = (query || '').trim();
|
|
820
|
+
if (!value)
|
|
821
|
+
return;
|
|
822
|
+
const list = recents.filter((entry) => entry !== value);
|
|
823
|
+
list.unshift(value);
|
|
824
|
+
recents = list.slice(0, MAX_RECENTS);
|
|
825
|
+
writeRecents(recents);
|
|
826
|
+
}
|
|
827
|
+
// Where a result takes you. A content match belongs in the Code tab at the
|
|
828
|
+
// line it matched, and a symbol belongs there too, at the line it was
|
|
829
|
+
// declared — both are facts the scan already knows, so neither should dump
|
|
830
|
+
// you at line 1. A file's natural home is the graph. Cmd+Enter overrides all
|
|
831
|
+
// of it for when the graph is not what you meant.
|
|
832
|
+
function select(item, flags) {
|
|
833
|
+
if (!item)
|
|
834
|
+
return;
|
|
835
|
+
if (item.type === 'example' || item.type === 'recent')
|
|
836
|
+
return useQuery(item.query);
|
|
837
|
+
rememberSearch(lastQuery);
|
|
838
|
+
const toCode = !!((flags && flags.forceCode)
|
|
839
|
+
|| item.type === 'symbol' || item.type === 'code');
|
|
840
|
+
hide(false);
|
|
841
|
+
document.dispatchEvent(new CustomEvent('search-select', {
|
|
842
|
+
detail: { path: item.path, line: item.line, target: toCode ? 'code' : 'files' },
|
|
843
|
+
}));
|
|
844
|
+
return undefined;
|
|
845
|
+
}
|
|
846
|
+
function show(opener) {
|
|
847
|
+
if (isShowing)
|
|
848
|
+
return;
|
|
849
|
+
buildShell();
|
|
850
|
+
// Pointer clicks do not focus buttons in WebKit; remember the explicit trigger.
|
|
851
|
+
focusBefore = opener || (document.activeElement instanceof HTMLElement ? document.activeElement : null);
|
|
852
|
+
recents = readRecents();
|
|
853
|
+
selectedIndex = 0;
|
|
854
|
+
for (const el of document.querySelectorAll('.topbar, #landing, #explorer')) {
|
|
855
|
+
background.set(el, el.inert);
|
|
856
|
+
el.inert = true;
|
|
857
|
+
}
|
|
858
|
+
overlay.hidden = false;
|
|
859
|
+
isShowing = true;
|
|
860
|
+
if (input)
|
|
861
|
+
input.value = '';
|
|
862
|
+
runSearch();
|
|
863
|
+
input?.focus({ preventScroll: true });
|
|
864
|
+
document.dispatchEvent(new CustomEvent('search-visibility', { detail: { visible: true } }));
|
|
865
|
+
}
|
|
866
|
+
function hide(restoreFocus = true) {
|
|
867
|
+
const wasShowing = isShowing;
|
|
868
|
+
requestSeq++;
|
|
869
|
+
inFlight?.abort();
|
|
338
870
|
inFlight = null;
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
// syntax — which is the only place the advanced grammar is discoverable.
|
|
407
|
-
if (recents.length) {
|
|
408
|
-
list.push({
|
|
409
|
-
key: 'recents',
|
|
410
|
-
label: 'Recent searches',
|
|
411
|
-
items: recents.map((query, i) => ({ type: 'recent', query, score: MAX_RECENTS - i })),
|
|
412
|
-
});
|
|
413
|
-
}
|
|
414
|
-
if (fileList.length) {
|
|
415
|
-
list.push({
|
|
416
|
-
key: 'start',
|
|
417
|
-
label: 'Start somewhere',
|
|
418
|
-
items: fileList.slice(0, 8).map((file) => ({ type: 'file', path: getPath(file), score: 1 })),
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
|
-
list.push({
|
|
422
|
-
key: 'examples',
|
|
423
|
-
label: 'Search syntax',
|
|
424
|
-
items: EXAMPLES.map((example) => ({
|
|
425
|
-
type: 'example', query: example.query, hint: example.hint, score: 1,
|
|
426
|
-
})),
|
|
427
|
-
});
|
|
428
|
-
groups = list;
|
|
429
|
-
flat = flatten(list);
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
const showFiles = activeMode === 'all' || activeMode === 'files';
|
|
434
|
-
const showSymbols = activeMode === 'all' || activeMode === 'symbols';
|
|
435
|
-
const showCode = activeMode === 'all' || activeMode === 'code';
|
|
436
|
-
|
|
437
|
-
if (showFiles && localFiles.length) {
|
|
438
|
-
list.push({ key: 'files', label: 'Files', total: localCounts.files, items: localFiles.slice(0, PREVIEW_LIMIT) });
|
|
439
|
-
}
|
|
440
|
-
if (showSymbols && localSymbols.length) {
|
|
441
|
-
list.push({ key: 'symbols', label: 'Symbols', total: localCounts.symbols, items: localSymbols.slice(0, PREVIEW_LIMIT) });
|
|
442
|
-
}
|
|
443
|
-
if (showCode) {
|
|
444
|
-
const hits = codeMatches();
|
|
445
|
-
if (hits.length) {
|
|
446
|
-
list.push({
|
|
447
|
-
key: 'code',
|
|
448
|
-
label: 'Code matches',
|
|
449
|
-
total: (code && code.total) || hits.length,
|
|
450
|
-
items: hits.slice(0, PREVIEW_LIMIT),
|
|
451
|
-
});
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
groups = list;
|
|
456
|
-
flat = flatten(list);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// ---- painting -----------------------------------------------------------
|
|
460
|
-
|
|
461
|
-
// Every span the query matched, not just the first. The old highlighter
|
|
462
|
-
// stopped at one occurrence, which made a two-word query look like it had
|
|
463
|
-
// only matched half of itself.
|
|
464
|
-
function highlightAll(text, parsed) {
|
|
465
|
-
const value = String(text ?? '');
|
|
466
|
-
const ranges = highlightRanges(value, parsed);
|
|
467
|
-
if (!ranges.length) return escapeHtml(value);
|
|
468
|
-
let out = '';
|
|
469
|
-
let at = 0;
|
|
470
|
-
for (const [start, end] of ranges) {
|
|
471
|
-
if (start < at) continue; // merged already, but never double-wrap
|
|
472
|
-
out += escapeHtml(value.slice(at, start));
|
|
473
|
-
out += `<mark class="search-match">${escapeHtml(value.slice(start, end))}</mark>`;
|
|
474
|
-
at = end;
|
|
475
|
-
}
|
|
476
|
-
return out + escapeHtml(value.slice(at));
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
function render(parsed) {
|
|
480
|
-
if (!overlay || !resultsList) return;
|
|
481
|
-
buildGroups(parsed);
|
|
482
|
-
renderModes(parsed);
|
|
483
|
-
renderChips(parsed);
|
|
484
|
-
renderRows(parsed);
|
|
485
|
-
renderStatus(parsed);
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
function renderModes(parsed) {
|
|
489
|
-
if (!modeBar) return;
|
|
490
|
-
const empty = queryIsEmpty(parsed);
|
|
491
|
-
const localSum = localCounts.files + localCounts.symbols;
|
|
492
|
-
const codeTotal = (code && code.total) || 0;
|
|
493
|
-
const codeText = codeLoading ? '\u2026' : (code ? String(codeTotal) : '');
|
|
494
|
-
const allText = codeLoading ? `${localSum}+` : String(localSum + codeTotal);
|
|
495
|
-
|
|
496
|
-
const chips = [
|
|
497
|
-
['all', 'All', allText],
|
|
498
|
-
['files', 'Files', String(localCounts.files)],
|
|
499
|
-
['symbols', 'Symbols', String(localCounts.symbols)],
|
|
500
|
-
['code', 'Code', codeText],
|
|
501
|
-
];
|
|
502
|
-
|
|
503
|
-
modeBar.innerHTML = chips.map(([mode, label, count]) => {
|
|
504
|
-
const active = mode === activeMode ? ' is-active' : '';
|
|
505
|
-
const showCount = !empty && count !== '';
|
|
506
|
-
const badge = showCount ? `<span class="search-mode-count">${escapeHtml(count)}</span>` : '';
|
|
507
|
-
return `<button class="search-mode-chip${active}" data-mode="${mode}" role="tab" `
|
|
508
|
-
+ `aria-selected="${mode === activeMode}">${escapeHtml(label)}${badge}</button>`;
|
|
509
|
-
}).join('');
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
function renderChips(parsed) {
|
|
513
|
-
if (!chipsBar) return;
|
|
514
|
-
// Terms are deliberately not chipped: they are already legible in the input,
|
|
515
|
-
// and a chip per word would double the width of the row to say what you can
|
|
516
|
-
// see. Filters, phrases and regexes are the decisions worth showing — and
|
|
517
|
-
// worth being able to take back with one click. The case toggle is not a
|
|
518
|
-
// clause, so it lives beside the input rather than here; without a chip of
|
|
519
|
-
// its own, an empty query leaves this row hidden.
|
|
520
|
-
const parts = queryIsEmpty(parsed)
|
|
521
|
-
? []
|
|
522
|
-
: parsed.clauses
|
|
523
|
-
.map((clause, index) => ({ clause, index }))
|
|
524
|
-
.filter((entry) => entry.clause.kind !== 'term')
|
|
525
|
-
.map(({ clause, index }) => {
|
|
526
|
-
const kind = CLAUSE_LABELS[clause.kind] || clause.kind;
|
|
527
|
-
return `<span class="search-chip"><span class="search-chip-kind">${escapeHtml(kind)}</span>`
|
|
528
|
-
+ `${escapeHtml(clause.raw)}<button class="search-chip-remove" data-clause="${index}" `
|
|
529
|
-
+ `title="Remove this filter" aria-label="Remove this filter">\u00d7</button></span>`;
|
|
871
|
+
clearTimeout(debounceTimer);
|
|
872
|
+
if (localFrame !== null) {
|
|
873
|
+
cancelAnimationFrame(localFrame);
|
|
874
|
+
localFrame = null;
|
|
875
|
+
}
|
|
876
|
+
if (overlay)
|
|
877
|
+
overlay.hidden = true;
|
|
878
|
+
isShowing = false;
|
|
879
|
+
for (const [el, inert] of background)
|
|
880
|
+
el.inert = inert;
|
|
881
|
+
background.clear();
|
|
882
|
+
if (wasShowing) {
|
|
883
|
+
document.dispatchEvent(new CustomEvent('search-visibility', { detail: { visible: false } }));
|
|
884
|
+
if (restoreFocus && focusBefore?.isConnected)
|
|
885
|
+
focusBefore.focus({ preventScroll: true });
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
function destroy() {
|
|
889
|
+
hide();
|
|
890
|
+
listeners.abort();
|
|
891
|
+
if (overlay)
|
|
892
|
+
overlay.replaceChildren();
|
|
893
|
+
localCache.clear();
|
|
894
|
+
}
|
|
895
|
+
function updateFiles(newFiles, newScanId) {
|
|
896
|
+
fileList = Array.isArray(newFiles) ? newFiles : [];
|
|
897
|
+
localIndex = buildLocalSearchIndex(fileList);
|
|
898
|
+
localCache.clear();
|
|
899
|
+
if (newScanId !== undefined)
|
|
900
|
+
scanIdProvider = () => newScanId;
|
|
901
|
+
// A new scan brings a new session, and the old session's content index went
|
|
902
|
+
// with it. So the previous payload is not merely stale — it is unreachable,
|
|
903
|
+
// and showing it would offer hits into a repo that is no longer loaded.
|
|
904
|
+
code = null;
|
|
905
|
+
codeError = null;
|
|
906
|
+
if (isShowing)
|
|
907
|
+
runSearch();
|
|
908
|
+
}
|
|
909
|
+
const controller = {
|
|
910
|
+
show,
|
|
911
|
+
hide,
|
|
912
|
+
destroy,
|
|
913
|
+
updateFiles,
|
|
914
|
+
isShowing: () => isShowing,
|
|
915
|
+
};
|
|
916
|
+
activeInstance = controller;
|
|
917
|
+
// One listener for the life of the page, because Cmd+K has to work before any
|
|
918
|
+
// scan exists. Only one palette is ever live, so the listener talks to the
|
|
919
|
+
// newest controller and *asks* whether it is open, rather than keeping its own
|
|
920
|
+
// idea of that — the old version captured the first instance's flag in a
|
|
921
|
+
// closure, so Cmd+K stopped toggling correctly after a second scan.
|
|
922
|
+
if (!keyListenerAttached) {
|
|
923
|
+
keyListenerAttached = true;
|
|
924
|
+
window.addEventListener('keydown', (event) => {
|
|
925
|
+
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {
|
|
926
|
+
event.preventDefault();
|
|
927
|
+
if (!activeInstance)
|
|
928
|
+
return;
|
|
929
|
+
if (activeInstance.isShowing())
|
|
930
|
+
activeInstance.hide();
|
|
931
|
+
else
|
|
932
|
+
activeInstance.show();
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
if (event.key === 'Escape' && activeInstance && activeInstance.isShowing()) {
|
|
936
|
+
activeInstance.hide();
|
|
937
|
+
}
|
|
530
938
|
});
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
chipsBar.innerHTML = '';
|
|
534
|
-
chipsBar.hidden = true;
|
|
535
|
-
return;
|
|
536
|
-
}
|
|
537
|
-
chipsBar.hidden = false;
|
|
538
|
-
chipsBar.innerHTML = parts.join('');
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
function rowHtml(item, index, parsed) {
|
|
542
|
-
const on = index === selectedIndex;
|
|
543
|
-
const attrs = `class="search-result is-${item.type}${on ? ' is-selected' : ''}" `
|
|
544
|
-
+ `role="option" aria-selected="${on}" id="search-option-${index}" data-index="${index}"`;
|
|
545
|
-
|
|
546
|
-
if (item.type === 'recent') {
|
|
547
|
-
return `<div ${attrs}><div class="search-item-main">`
|
|
548
|
-
+ `<span class="search-ext-badge is-recent">\u21ba</span>`
|
|
549
|
-
+ `<span class="search-title"><span class="search-name">${escapeHtml(item.query)}</span>`
|
|
550
|
-
+ `<span class="search-dir">recent search</span></span></div></div>`;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
if (item.type === 'example') {
|
|
554
|
-
return `<div ${attrs}><div class="search-item-main">`
|
|
555
|
-
+ `<span class="search-ext-badge is-syntax">/</span>`
|
|
556
|
-
+ `<span class="search-title"><span class="search-name">${escapeHtml(item.query)}</span>`
|
|
557
|
-
+ `<span class="search-dir">${escapeHtml(item.hint || '')}</span></span></div></div>`;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
const path = item.path;
|
|
561
|
-
const name = path.split('/').pop();
|
|
562
|
-
const dir = path.includes('/') ? path.slice(0, path.lastIndexOf('/')) : '';
|
|
563
|
-
|
|
564
|
-
if (item.type === 'symbol') {
|
|
565
|
-
return `<div ${attrs}><div class="search-item-main">`
|
|
566
|
-
+ `<span class="search-ext-badge is-symbol">SYM</span>`
|
|
567
|
-
+ `<span class="search-title"><span class="search-name">${highlightAll(item.name, parsed)}</span>`
|
|
568
|
-
+ `<span class="search-dir">${escapeHtml(path)}${item.line ? ' \u00b7 L' + item.line : ''}</span></span>`
|
|
569
|
-
+ `<span class="search-type-tag is-sym">${escapeHtml(item.kind || 'symbol')}</span>`
|
|
570
|
-
+ `</div></div>`;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
if (item.type === 'code') {
|
|
574
|
-
const snippet = item.snippet
|
|
575
|
-
? `<div class="search-snippet"><code>${highlightAll(item.snippet, parsed)}</code></div>`
|
|
576
|
-
: '';
|
|
577
|
-
return `<div ${attrs}><div class="search-item-main">`
|
|
578
|
-
+ `<span class="search-ext-badge is-code">CODE</span>`
|
|
579
|
-
+ `<span class="search-title"><span class="search-name">${escapeHtml(name)}</span>`
|
|
580
|
-
+ `<span class="search-dir">${escapeHtml(path)}${item.line ? ' \u00b7 Line ' + item.line : ''}</span></span>`
|
|
581
|
-
+ `<span class="search-type-tag is-code">match</span>`
|
|
582
|
-
+ `</div>${snippet}</div>`;
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
return `<div ${attrs}><div class="search-item-main">`
|
|
586
|
-
+ `<span class="search-ext-badge">${escapeHtml(extensionOf(path))}</span>`
|
|
587
|
-
+ `<span class="search-title"><span class="search-name">${highlightAll(name, parsed)}</span>`
|
|
588
|
-
+ `<span class="search-dir">${escapeHtml(dir)}</span></span>`
|
|
589
|
-
+ `<span class="search-type-tag">file</span></div></div>`;
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
function renderRows(parsed) {
|
|
593
|
-
if (!resultsList) return;
|
|
594
|
-
if (!groups.length) {
|
|
595
|
-
resultsList.innerHTML = emptyState(parsed);
|
|
596
|
-
return;
|
|
597
|
-
}
|
|
598
|
-
if (selectedIndex >= flat.length) selectedIndex = Math.max(0, flat.length - 1);
|
|
599
|
-
|
|
600
|
-
let index = -1;
|
|
601
|
-
resultsList.innerHTML = groups.map((group) => {
|
|
602
|
-
const rows = group.items.map((item) => {
|
|
603
|
-
index++;
|
|
604
|
-
return rowHtml(item, index, parsed);
|
|
605
|
-
}).join('');
|
|
606
|
-
const count = group.total === undefined ? group.items.length : group.total;
|
|
607
|
-
return `<div class="search-group" role="group" aria-label="${escapeHtml(group.label)}">`
|
|
608
|
-
+ `<div class="search-group-head"><span>${escapeHtml(group.label)}</span>`
|
|
609
|
-
+ `<span class="search-group-count">${escapeHtml(String(count))}</span></div>`
|
|
610
|
-
+ `${rows}</div>`;
|
|
611
|
-
}).join('');
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
function emptyState(parsed) {
|
|
615
|
-
if (parseError) {
|
|
616
|
-
return '<div class="search-empty"><strong>That pattern will not compile.</strong>'
|
|
617
|
-
+ `<div class="search-empty-hint">${escapeHtml(parseError)}</div></div>`;
|
|
618
|
-
}
|
|
619
|
-
if (codeLoading) {
|
|
620
|
-
return '<div class="search-empty"><span class="search-spinner"></span> Searching file contents\u2026</div>';
|
|
621
|
-
}
|
|
622
|
-
if (queryIsEmpty(parsed)) {
|
|
623
|
-
return '<div class="search-empty">Type to search this codebase.</div>';
|
|
624
|
-
}
|
|
625
|
-
return `<div class="search-empty">Nothing matched <code>${escapeHtml(parsed.raw.trim())}</code>.`
|
|
626
|
-
+ '<div class="search-empty-hint">Try fewer words, or drop a filter — <code>ext:</code>, '
|
|
627
|
-
+ '<code>path:</code> and <code>-word</code> all narrow the same list, and <kbd>Tab</kbd> '
|
|
628
|
-
+ 'switches what kind of result you are looking at.</div></div>';
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
function renderStatus(parsed) {
|
|
632
|
-
if (!statusBar) return;
|
|
633
|
-
const bits = [];
|
|
634
|
-
|
|
635
|
-
if (parseError) {
|
|
636
|
-
bits.push(`<span class="search-err">${escapeHtml(parseError)}</span>`);
|
|
637
|
-
} else if (codeError) {
|
|
638
|
-
bits.push(`<span class="search-err">${escapeHtml(codeError)}</span>`);
|
|
639
|
-
} else if (queryIsEmpty(parsed)) {
|
|
640
|
-
bits.push('Type to search. <kbd>Tab</kbd> changes kind, <kbd>Esc</kbd> closes.');
|
|
641
|
-
} else {
|
|
642
|
-
const total = localCounts.files + localCounts.symbols + ((code && code.total) || 0);
|
|
643
|
-
bits.push(`${total} match${total === 1 ? '' : 'es'}`);
|
|
644
|
-
if (codeLoading) bits.push('<span class="search-spinner"></span> searching contents');
|
|
645
|
-
if (code) {
|
|
646
|
-
bits.push(`${code.indexed || 0} files indexed`);
|
|
647
|
-
if (code.capped) bits.push(`showing the top ${code.results.length}`);
|
|
648
|
-
// Saying that the filters are enforced is the honest counterpart to the
|
|
649
|
-
// forgiving ranking a plain word search still gets.
|
|
650
|
-
if (code.advanced) bits.push('filters enforced');
|
|
651
|
-
}
|
|
652
|
-
const skipped = code && code.stats && code.stats.skipped;
|
|
653
|
-
if (skipped) {
|
|
654
|
-
const count = Object.keys(skipped).reduce((sum, key) => sum + (skipped[key] || 0), 0);
|
|
655
|
-
if (count) bits.push(`${count} not indexed`);
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
statusBar.innerHTML = `<div class="search-status-line">${bits.join('<span class="search-sep">\u00b7</span>')}</div>`
|
|
660
|
-
+ '<div class="search-keys"><span><kbd>\u2191</kbd><kbd>\u2193</kbd> move</span>'
|
|
661
|
-
+ '<span><kbd>\u21b5</kbd> open</span><span><kbd>\u2318\u21b5</kbd> code</span>'
|
|
662
|
-
+ '<span><kbd>tab</kbd> kind</span><span><kbd>esc</kbd> close</span></div>';
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
// ---- interaction --------------------------------------------------------
|
|
666
|
-
|
|
667
|
-
let currentParsed = parseQuery('');
|
|
668
|
-
|
|
669
|
-
function parsedQuery(raw) {
|
|
670
|
-
return parseQuery(raw, { caseSensitive });
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
function moveSelection(delta) {
|
|
674
|
-
if (!flat.length) return;
|
|
675
|
-
selectedIndex = Math.min(Math.max(selectedIndex + delta, 0), flat.length - 1);
|
|
676
|
-
paintSelection();
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
function paintSelection() {
|
|
680
|
-
if (!resultsList) return;
|
|
681
|
-
const rows = resultsList.querySelectorAll('.search-result');
|
|
682
|
-
if (!rows.length) return;
|
|
683
|
-
for (let i = 0; i < rows.length; i++) {
|
|
684
|
-
const row = rows[i];
|
|
685
|
-
const on = Number(row.dataset.index) === selectedIndex;
|
|
686
|
-
row.classList.toggle('is-selected', on);
|
|
687
|
-
row.setAttribute('aria-selected', String(on));
|
|
688
|
-
if (on) row.scrollIntoView({ block: 'nearest' });
|
|
689
|
-
}
|
|
690
|
-
const selected = rows[selectedIndex];
|
|
691
|
-
if (input) input.setAttribute('aria-activedescendant', selected ? selected.id : '');
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
function onInputKeyDown(event) {
|
|
695
|
-
const key = event.key;
|
|
696
|
-
// The palette is modal, so the keys it handles stop here instead of reaching
|
|
697
|
-
// the window listener — otherwise one Escape would clear the query and close
|
|
698
|
-
// the palette in the same keystroke.
|
|
699
|
-
if (key === 'ArrowDown' || key === 'ArrowUp' || key === 'Tab' || key === 'Home'
|
|
700
|
-
|| key === 'End' || key === 'Enter' || key === 'Escape') {
|
|
701
|
-
event.stopPropagation();
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
if (key === 'ArrowDown') { event.preventDefault(); return moveSelection(1); }
|
|
705
|
-
if (key === 'ArrowUp') { event.preventDefault(); return moveSelection(-1); }
|
|
706
|
-
if (key === 'Home') { event.preventDefault(); selectedIndex = 0; return paintSelection(); }
|
|
707
|
-
if (key === 'End') {
|
|
708
|
-
event.preventDefault();
|
|
709
|
-
selectedIndex = Math.max(0, flat.length - 1);
|
|
710
|
-
return paintSelection();
|
|
711
|
-
}
|
|
712
|
-
if (key === 'Tab') { event.preventDefault(); return cycleMode(event.shiftKey ? -1 : 1); }
|
|
713
|
-
|
|
714
|
-
if (key === 'Enter') {
|
|
715
|
-
event.preventDefault();
|
|
716
|
-
const item = flat[selectedIndex];
|
|
717
|
-
if (item) select(item, { forceCode: event.metaKey || event.ctrlKey });
|
|
718
|
-
return undefined;
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
if (key === 'Escape') {
|
|
722
|
-
event.preventDefault();
|
|
723
|
-
// One Escape clears the query, a second closes the palette. Losing a long
|
|
724
|
-
// query to a stray keypress is worse than one more keystroke to close.
|
|
725
|
-
if (input && input.value) {
|
|
726
|
-
input.value = '';
|
|
727
|
-
selectedIndex = 0;
|
|
728
|
-
return runSearch();
|
|
729
|
-
}
|
|
730
|
-
return hide();
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
if (key === 'Backspace' && input && !input.value) {
|
|
734
|
-
// Backspace on an empty input takes back the last filter, the way a token
|
|
735
|
-
// editor does. There is nothing else for the key to mean here.
|
|
736
|
-
const removable = currentParsed.clauses.filter((clause) => clause.kind !== 'term');
|
|
737
|
-
if (removable.length) {
|
|
738
|
-
event.preventDefault();
|
|
739
|
-
return removeChip(currentParsed.clauses.indexOf(removable[removable.length - 1]));
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
return undefined;
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
function setMode(mode) {
|
|
746
|
-
if (!mode || mode === activeMode) return;
|
|
747
|
-
activeMode = mode;
|
|
748
|
-
selectedIndex = 0;
|
|
749
|
-
runSearch();
|
|
750
|
-
if (input) input.focus();
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
function cycleMode(delta) {
|
|
754
|
-
const order = ['all', 'files', 'symbols', 'code'];
|
|
755
|
-
const at = order.indexOf(activeMode);
|
|
756
|
-
setMode(order[(at + delta + order.length) % order.length]);
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
function toggleCase() {
|
|
760
|
-
caseSensitive = !caseSensitive;
|
|
761
|
-
selectedIndex = 0;
|
|
762
|
-
runSearch();
|
|
763
|
-
if (input) input.focus();
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
function useQuery(query) {
|
|
767
|
-
if (!input) return;
|
|
768
|
-
input.value = query;
|
|
769
|
-
selectedIndex = 0;
|
|
770
|
-
runSearch();
|
|
771
|
-
input.focus();
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
function removeChip(index) {
|
|
775
|
-
if (!input) return;
|
|
776
|
-
const clause = currentParsed.clauses[index];
|
|
777
|
-
if (!clause) return;
|
|
778
|
-
input.value = removeClause(input.value, clause);
|
|
779
|
-
selectedIndex = 0;
|
|
780
|
-
runSearch();
|
|
781
|
-
input.focus();
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
function rememberSearch(query) {
|
|
785
|
-
const value = (query || '').trim();
|
|
786
|
-
if (!value) return;
|
|
787
|
-
const list = readRecents().filter((entry) => entry !== value);
|
|
788
|
-
list.unshift(value);
|
|
789
|
-
writeRecents(list);
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
// Where a result takes you. A content match belongs in the Code tab at the
|
|
793
|
-
// line it matched, and a symbol belongs there too, at the line it was
|
|
794
|
-
// declared — both are facts the scan already knows, so neither should dump
|
|
795
|
-
// you at line 1. A file's natural home is the graph. Cmd+Enter overrides all
|
|
796
|
-
// of it for when the graph is not what you meant.
|
|
797
|
-
function select(item, flags) {
|
|
798
|
-
if (!item) return;
|
|
799
|
-
if (item.type === 'example' || item.type === 'recent') return useQuery(item.query);
|
|
800
|
-
|
|
801
|
-
rememberSearch(lastQuery);
|
|
802
|
-
const toCode = !!((flags && flags.forceCode)
|
|
803
|
-
|| item.type === 'symbol' || item.type === 'code');
|
|
804
|
-
document.dispatchEvent(new CustomEvent('search-select', {
|
|
805
|
-
detail: { path: item.path, line: item.line, target: toCode ? 'code' : 'files' },
|
|
806
|
-
}));
|
|
807
|
-
return hide();
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
function show() {
|
|
811
|
-
buildShell();
|
|
812
|
-
recents = readRecents();
|
|
813
|
-
overlay.hidden = false;
|
|
814
|
-
isShowing = true;
|
|
815
|
-
if (input) {
|
|
816
|
-
input.value = '';
|
|
817
|
-
input.focus();
|
|
818
|
-
}
|
|
819
|
-
selectedIndex = 0;
|
|
820
|
-
runSearch();
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
function hide() {
|
|
824
|
-
if (inFlight) {
|
|
825
|
-
inFlight.abort();
|
|
826
|
-
inFlight = null;
|
|
827
|
-
}
|
|
828
|
-
clearTimeout(debounceTimer);
|
|
829
|
-
if (overlay) overlay.hidden = true;
|
|
830
|
-
isShowing = false;
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
function updateFiles(newFiles, newScanId) {
|
|
834
|
-
fileList = Array.isArray(newFiles) ? newFiles : [];
|
|
835
|
-
if (newScanId) scanIdProvider = () => newScanId;
|
|
836
|
-
// A new scan brings a new session, and the old session's content index went
|
|
837
|
-
// with it. So the previous payload is not merely stale — it is unreachable,
|
|
838
|
-
// and showing it would offer hits into a repo that is no longer loaded.
|
|
839
|
-
code = null;
|
|
840
|
-
codeError = null;
|
|
841
|
-
if (isShowing) runSearch();
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
const controller = {
|
|
845
|
-
show,
|
|
846
|
-
hide,
|
|
847
|
-
updateFiles,
|
|
848
|
-
isShowing: () => isShowing,
|
|
849
|
-
};
|
|
850
|
-
activeInstance = controller;
|
|
851
|
-
|
|
852
|
-
// One listener for the life of the page, because Cmd+K has to work before any
|
|
853
|
-
// scan exists. Only one palette is ever live, so the listener talks to the
|
|
854
|
-
// newest controller and *asks* whether it is open, rather than keeping its own
|
|
855
|
-
// idea of that — the old version captured the first instance's flag in a
|
|
856
|
-
// closure, so Cmd+K stopped toggling correctly after a second scan.
|
|
857
|
-
if (!keyListenerAttached) {
|
|
858
|
-
keyListenerAttached = true;
|
|
859
|
-
window.addEventListener('keydown', (event) => {
|
|
860
|
-
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {
|
|
861
|
-
event.preventDefault();
|
|
862
|
-
if (!activeInstance) return;
|
|
863
|
-
if (activeInstance.isShowing()) activeInstance.hide();
|
|
864
|
-
else activeInstance.show();
|
|
865
|
-
return;
|
|
866
|
-
}
|
|
867
|
-
if (event.key === 'Escape' && activeInstance && activeInstance.isShowing()) {
|
|
868
|
-
activeInstance.hide();
|
|
869
|
-
}
|
|
870
|
-
});
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
return controller;
|
|
939
|
+
}
|
|
940
|
+
return controller;
|
|
874
941
|
}
|
|
942
|
+
//# sourceMappingURL=search.js.map
|