-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy pathmain.go
More file actions
489 lines (473 loc) · 11.3 KB
/
main.go
File metadata and controls
489 lines (473 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
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
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
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
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
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
// SPDX-License-Identifier: MIT
package main
import (
"fmt"
"os"
"runtime/pprof"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/spf13/cobra"
)
const Version = "3.1.0"
func main() {
cfg := DefaultConfig()
var cpuProfile string
initLanguageDatabase()
rootCmd := &cobra.Command{
Use: "cs",
Long: "code spelunker (cs) code search.\n" +
"Version " + Version + "\n" +
"Ben Boyter <ben@boyter.org>" +
"\n\n" +
"cs recursively searches the current directory using some boolean logic\n" +
"optionally combined with regular expressions.\n" +
"\n" +
"Works via command line where passed in arguments are the search terms\n" +
"or in a TUI mode with no arguments. Can also run in HTTP mode with\n" +
"the -d or --http-server flag.\n" +
"\n" +
"Searches by default use AND boolean syntax for all terms\n" +
" - exact match using quotes \"find this\"\n" +
" - fuzzy match within 1 or 2 distance fuzzy~1 fuzzy~2\n" +
" - negate using NOT such as pride NOT prejudice\n" +
" - OR syntax such as catch OR throw\n" +
" - group with parentheses (cat OR dog) NOT fish\n" +
" - note: NOT binds to next term, use () with OR\n" +
" - regex with toothpick syntax /pr[e-i]de/\n" +
"\n" +
"Searches can filter which files are searched by adding\n" +
"the following syntax\n" +
" - file:test (substring match on filename)\n" +
" - filename:.go (substring match on filename)\n" +
" - path:pkg/search (substring match on full file path)\n" +
"\n" +
"Example search that uses all current functionality\n" +
" - darcy NOT collins wickham~1 \"ten thousand a year\" /pr[e-i]de/ file:test path:pkg\n" +
"\n" +
"The default input field in tui mode supports some nano commands\n" +
"- CTRL+a move to the beginning of the input\n" +
"- CTRL+e move to the end of the input\n" +
"- CTRL+k to clear from the cursor location forward\n" +
"\n" +
"- F1 cycle ranker (simple/tfidf/bm25/structural)\n" +
"- F2 cycle code filter (default/only-code/only-comments/only-strings/only-declarations/only-usages)\n" +
"- F3 cycle gravity (off/low/default/logic/brain)\n" +
"- F4 cycle noise (silence/quiet/default/loud/raw)\n",
Version: Version,
Run: func(cmd *cobra.Command, args []string) {
if cpuProfile != "" {
f, err := os.Create(cpuProfile)
if err != nil {
fmt.Fprintf(os.Stderr, "error: could not create CPU profile: %v\n", err)
os.Exit(1)
}
pprof.StartCPUProfile(f)
defer func() {
pprof.StopCPUProfile()
f.Close()
}()
}
cfg.SearchString = args
// Validate --profile
switch cfg.Profile {
case "", "balanced", "precise", "broad":
// ok
default:
fmt.Fprintf(os.Stderr, "error: unknown --profile %q (valid: balanced, precise, broad)\n", cfg.Profile)
os.Exit(1)
}
// Mutual exclusivity check
count := 0
if cfg.OnlyCode {
count++
}
if cfg.OnlyComments {
count++
}
if cfg.OnlyStrings {
count++
}
if cfg.OnlyDeclarations {
count++
}
if cfg.OnlyUsages {
count++
}
if count > 1 {
fmt.Fprintf(os.Stderr, "error: --only-code, --only-comments, --only-strings, --only-declarations, and --only-usages are mutually exclusive\n")
os.Exit(1)
}
// Auto-select structural ranker when a content filter is set
if cfg.HasContentFilter() && cfg.Ranker != "structural" {
fmt.Fprintf(os.Stderr, "warning: --only-code/--only-comments/--only-strings requires structural ranker, setting --ranker=structural\n")
cfg.Ranker = "structural"
}
// Validate git-sync flags before entering any mode
if cfg.GitSync {
if cfg.GitSyncInterval <= 0 {
fmt.Fprintf(os.Stderr, "error: --git-sync-interval must be a positive duration\n")
os.Exit(1)
}
if cfg.GitSyncWorkers < 1 {
fmt.Fprintf(os.Stderr, "error: --git-sync-workers must be at least 1\n")
os.Exit(1)
}
}
if cfg.MCPServer {
if cfg.GitSync {
stopSync := startGitSync(&cfg)
defer stopSync()
}
StartMCPServer(&cfg)
} else if cfg.HttpServer {
if cfg.GitSync {
stopSync := startGitSync(&cfg)
defer stopSync()
}
StartHttpServer(&cfg)
} else if len(cfg.SearchString) != 0 {
if cfg.GitSync {
fmt.Fprintf(os.Stderr, "warning: --git-sync is ignored in console mode (not a long-running process)\n")
}
ConsoleSearch(&cfg)
} else {
if cfg.GitSync {
stopSync := startGitSync(&cfg)
defer stopSync()
}
p := tea.NewProgram(initialModel(&cfg), tea.WithAltScreen(), tea.WithMouseCellMotion(), tea.WithOutput(os.Stderr))
m, err := p.Run()
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if fm, ok := m.(model); ok && fm.chosen != "" {
fmt.Println(fm.chosen)
}
}
},
}
flags := rootCmd.PersistentFlags()
flags.BoolVar(
&cfg.IncludeBinaryFiles,
"binary",
false,
"set to disable binary file detection and search binary files",
)
flags.BoolVar(
&cfg.IgnoreIgnoreFile,
"no-ignore",
false,
"disables .ignore file logic",
)
flags.BoolVar(
&cfg.IgnoreGitIgnore,
"no-gitignore",
false,
"disables .gitignore file logic",
)
flags.IntVarP(
&cfg.SnippetLength,
"snippet-length",
"n",
300,
"size of the snippet to display",
)
flags.IntVarP(
&cfg.SnippetCount,
"snippet-count",
"s",
1,
"number of snippets to display",
)
flags.BoolVar(
&cfg.IncludeHidden,
"hidden",
false,
"include hidden files",
)
flags.StringSliceVarP(
&cfg.AllowListExtensions,
"include-ext",
"i",
[]string{},
"limit to file extensions (N.B. case sensitive) [comma separated list: e.g. go,java,js,C,cpp]",
)
flags.StringSliceVarP(
&cfg.LanguageTypes,
"type",
"t",
[]string{},
"limit to language types [comma separated list: e.g. Go,Java,Python]",
)
flags.BoolVarP(
&cfg.FindRoot,
"find-root",
"r",
false,
"attempts to find the root of this repository by traversing in reverse looking for .git or .hg",
)
flags.StringSliceVar(
&cfg.PathDenylist,
"exclude-dir",
[]string{".git", ".hg", ".svn"},
"directories to exclude",
)
flags.BoolVarP(
&cfg.CaseSensitive,
"case-sensitive",
"c",
false,
"make the search case sensitive",
)
flags.StringSliceVarP(
&cfg.LocationExcludePattern,
"exclude-pattern",
"x",
[]string{},
"file and directory locations matching case sensitive patterns will be ignored [comma separated list: e.g. vendor,_test.go]",
)
flags.BoolVar(
&cfg.IncludeMinified,
"min",
false,
"include minified files",
)
flags.IntVar(
&cfg.MinifiedLineByteLength,
"min-line-length",
255,
"number of bytes per average line for file to be considered minified",
)
flags.Int64Var(
&cfg.MaxReadSizeBytes,
"max-read-size-bytes",
1_000_000,
"number of bytes to read into a file with the remaining content ignored",
)
flags.StringVarP(
&cfg.Format,
"format",
"f",
"text",
"set output format [text, json, vimgrep]",
)
flags.StringVar(
&cfg.Ranker,
"ranker",
"structural",
"set ranking algorithm [simple, tfidf, bm25, structural]",
)
flags.StringVar(
&cfg.Profile,
"profile",
"",
"ranking profile [balanced, precise, broad] — overrides --gravity, --noise, and --test-penalty when set",
)
flags.StringVar(
&cfg.GravityIntent,
"gravity",
"default",
"complexity gravity intent: brain (2.5), logic (1.5), default (1.0), low (0.2), off (0.0)",
)
flags.StringVar(
&cfg.NoiseIntent,
"noise",
"default",
"noise penalty intent: silence (0.1), quiet (0.5), default (1.0), loud (2.0), raw (off)",
)
flags.Float64Var(
&cfg.TestPenalty,
"test-penalty",
0.4,
"score multiplier for test files when query has no test intent (0.0-1.0, 1.0=disabled)",
)
flags.StringVarP(
&cfg.FileOutput,
"output",
"o",
"",
"output filename (default stdout)",
)
flags.StringVar(
&cfg.Directory,
"dir",
"",
"directory to search, if not set defaults to current working directory",
)
flags.StringVar(
&cfg.SnippetMode,
"snippet-mode",
"auto",
"snippet extraction mode: auto, snippet, lines, or grep",
)
flags.IntVar(
&cfg.ResultLimit,
"result-limit",
-1,
"maximum number of results to return (-1 for unlimited)",
)
flags.IntVar(
&cfg.LineLimit,
"line-limit",
-1,
"max matching lines per file in grep mode (-1 = unlimited)",
)
flags.IntVarP(
&cfg.ContextBefore,
"before-context",
"B",
0,
"lines of context before each match (grep mode)",
)
flags.IntVarP(
&cfg.ContextAfter,
"after-context",
"A",
0,
"lines of context after each match (grep mode)",
)
flags.IntVarP(
&cfg.ContextAround,
"context",
"C",
0,
"lines of context before and after each match (grep mode)",
)
flags.BoolVar(
&cfg.Dedup,
"dedup",
false,
"collapse byte-identical search matches, keeping the highest-scored representative",
)
flags.BoolVar(
&cfg.MCPServer,
"mcp",
false,
"start as an MCP (Model Context Protocol) server over stdio",
)
flags.BoolVarP(
&cfg.HttpServer,
"http-server",
"d",
false,
"start the HTTP server",
)
flags.StringVar(
&cfg.Address,
"address",
":8080",
"address and port to listen on",
)
flags.StringVar(
&cfg.SearchTemplate,
"template-search",
"",
"path to a custom search template",
)
flags.StringVar(
&cfg.DisplayTemplate,
"template-display",
"",
"path to a custom display template",
)
flags.StringVar(
&cfg.TemplateStyle,
"template-style",
"dark",
"built-in theme for the HTTP server UI [dark, light, bare]",
)
flags.BoolVar(
&cfg.NoSyntax,
"no-syntax",
false,
"disable syntax highlighting in output",
)
flags.StringVar(
&cfg.Color,
"color",
"auto",
"color output mode [auto, always, never]",
)
flags.BoolVar(
&cfg.Reverse,
"reverse",
false,
"reverse the result order",
)
flags.StringVar(
&cpuProfile,
"cpu-profile",
"",
"write CPU profile to file (for use with go tool pprof or PGO)",
)
flags.Float64Var(
&cfg.WeightCode,
"weight-code",
1.0,
"structural ranker: weight for matches in code (default 1.0)",
)
flags.Float64Var(
&cfg.WeightComment,
"weight-comment",
0.2,
"structural ranker: weight for matches in comments (default 0.2)",
)
flags.Float64Var(
&cfg.WeightString,
"weight-string",
0.5,
"structural ranker: weight for matches in strings (default 0.5)",
)
flags.BoolVar(
&cfg.OnlyCode,
"only-code",
false,
"only rank matches in code (auto-selects structural ranker)",
)
flags.BoolVar(
&cfg.OnlyComments,
"only-comments",
false,
"only rank matches in comments (auto-selects structural ranker)",
)
flags.BoolVar(
&cfg.OnlyStrings,
"only-strings",
false,
"only rank matches in string literals (auto-selects structural ranker)",
)
flags.BoolVar(
&cfg.OnlyDeclarations,
"only-declarations",
false,
"only show matches on declaration lines (func, type, var, const, class, def, etc.)",
)
flags.BoolVar(
&cfg.OnlyUsages,
"only-usages",
false,
"only show matches on usage lines (excludes declarations)",
)
flags.BoolVar(
&cfg.GitSync,
"git-sync",
false,
"periodically git pull repositories found in the search directory (TUI/HTTP/MCP only)",
)
flags.DurationVar(
&cfg.GitSyncInterval,
"git-sync-interval",
5*time.Minute,
"interval between git sync pulls (e.g. 5m, 30s, 1h)",
)
flags.IntVar(
&cfg.GitSyncWorkers,
"git-sync-workers",
1,
"number of concurrent git pull workers",
)
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}