Experiment/search text - #212
Conversation
A lexical search path — literal terms + regex, GitHub-code-search style — alongside semantic `search` and structural `grep`. Runs entirely locally (no daemon, no index, no project init): `--text` branches before the project/daemon gate like `grep`, and reuses `file_walk` so the searched file set matches the indexer. - File-level AND across terms; `/re/` marks a regex, `\/` escapes a literal; smart-case by default with `-i` / `-s` overrides; reuses `--lang` / `--path`. - Rejects `--refresh` in `--text` mode (index-only). - Unlike `grep`, covers non-code files (docs/config) with no code language. Experimental, scoped to issue cocoindex-io#192. `--limit` / `--offset` and default-path behavior are pending maintainer confirmation. Adds tests/test_textsearch.py (33 tests).
- compile_terms now yields Term{raw, is_regex, case_sensitive, pattern}, giving
a future inverted index a per-term literal-vs-regex signal instead of a bare
compiled pattern.
- A /…/ term is a regex only when its body has no free (unescaped) slash,
matching GitHub code search (e.g. /blobs/docs-v1/ stays literal); drops the
\/ escaping-outside-regex rule.
- Honor --limit in --text mode (caps printed files), per maintainer feedback.
All contained in textsearch.py + its tests; grep and semantic search untouched.
There was a problem hiding this comment.
Pull request overview
Adds a new ccc search --text mode that performs local, index-free full-text search (literal substrings and /regex/ terms) over the same include/exclude + .gitignore file set used by the indexer/ccc grep, with streaming per-file output.
Changes:
- Introduces
cocoindex_code.textsearchengine: term parsing (smart-case, literal vs regex), file walking, matching, and rendering. - Extends
ccc searchCLI with--textplus case/color controls, branching before the daemon/project gate. - Adds an end-to-end test suite covering parsing, matching semantics, filters, rendering, and CLI behavior.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
tests/test_textsearch.py |
Adds comprehensive tests for term parsing, engine behavior, rendering, and CLI search --text. |
src/cocoindex_code/textsearch.py |
Implements the local text search engine (term compilation, file iteration, matching, and rendering). |
src/cocoindex_code/cli.py |
Adds ccc search --text entrypoint and streaming output logic with case/color options. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if text: | ||
| if refresh: | ||
| # --refresh rebuilds the semantic index — meaningless for a live scan. | ||
| _typer.echo( | ||
| "Error: --refresh does not apply to --text (live scan, no index).", err=True | ||
| ) | ||
| raise _typer.Exit(code=1) | ||
| _run_text_search( |
There was a problem hiding this comment.
Done — --text now errors on --offset, same as --refresh: both are index concepts with no meaning for a live scan. (1a1c7c0)
| return | ||
|
|
||
| project_root = str(require_project_root()) |
| ignore_case: bool = _typer.Option( | ||
| False, "-i", "--ignore-case", help="[--text] Force case-insensitive matching." | ||
| ), | ||
| case_sensitive: bool = _typer.Option( | ||
| False, "-s", "--case-sensitive", help="[--text] Force case-sensitive matching." | ||
| ), |
There was a problem hiding this comment.
They're redundant to each other. We only need one.
There was a problem hiding this comment.
Dropped -s/--case-sensitive and kept just -i/--ignore-case. Smart-case already gives case-sensitivity when a term has an uppercase letter, so the only override worth a flag is force-insensitive (-i). (1a1c7c0)
| return | ||
| block = _ts.render_file(item, color=use_color) # render outside the lock | ||
| with output_lock: | ||
| if limit and matched >= limit: |
There was a problem hiding this comment.
Ideally when the limit is reached, we should stop scanning the files at higher level.
There was a problem hiding this comment.
Done — the cap now lives in the engine: TextSearch.run takes a limit and stops feeding the walk once that many files have matched, rather than scanning everything and capping the output. (e6476c8)
|
|
||
| if matched == 0: | ||
| _typer.echo("No matches found.") | ||
|
|
There was a problem hiding this comment.
I think if we stopped after hitting limit, we may output a message to hint about this, and may say something like if want to see more output, retry with a higher limit.
There was a problem hiding this comment.
Added — when it stops at the limit it prints Stopped at --limit N. Re-run with a higher --limit to see more. (run returns whether it stopped early, so the CLI knows when to show it.) (e6476c8)
| # --refresh rebuilds the semantic index — meaningless for a live scan. | ||
| _typer.echo( | ||
| "Error: --refresh does not apply to --text (live scan, no index).", err=True | ||
| ) |
There was a problem hiding this comment.
--offset is also not supported for --text, so we should also echo an error if offset is not 0.
There was a problem hiding this comment.
Done — --text now errors on --offset, same as --refresh: both are index concepts with no meaning for a live scan. (1a1c7c0) (same as the reply for copilot)
|
Thank you @georgeh0 , I'll fix these by tmr. |
…rop -s - --text now rejects --offset (like --refresh): both are index concepts with no meaning for a live scan. - --ignore-case / --no-color error when used without --text instead of being silently ignored in semantic mode. - Drop the redundant -s / --case-sensitive; smart-case already covers the common case (uppercase → sensitive) and -i handles the force-insensitive override. Addresses review feedback on cocoindex-io#212 (@georgeh0, Copilot).
Both `ccc grep` and `ccc search --text` duplicated the project file walk (resolve root → include/exclude + .gitignore matcher → path-glob → display paths). Factor it into `iter_project_files`, returning a `ProjectFiles` (files iterator + extension→language overrides). Each caller keeps its own per-file gating: grep skips files with no matchable code language; text search keeps every included file. Behavior-preserving — grep's tests stay green. Per review feedback on cocoindex-io#212 (@georgeh0).
TextSearch.run takes an optional `limit`: it stops feeding the walk once that many files have matched (rather than only capping printed output) and returns whether it stopped early. `ccc search --text` uses that to print a "re-run with a higher --limit" hint when results were truncated. Addresses review feedback on cocoindex-io#212 (@georgeh0).
georgeh0
left a comment
There was a problem hiding this comment.
Mostly looks good.
Please also resolve the potential merge conflicts. Thanks!
| gutter = _paint(f"{lm.line_no:>{width}}| ", color, fg="bright_black") | ||
| parts.append(f"{gutter}{_highlight_line(lm.text, lm.spans, color)}") |
There was a problem hiding this comment.
The format is shared with grep.py. Is there any code worth sharing too?
There was a problem hiding this comment.
Shared what's genuinely common: paint / path_header / gutter / gutter_width now live in a small render.py that both use — the header and <line>| gutter format are identical, so that's one definition now.
What stays per-search is the inside of a line: grep dims the context around a structural match (char offsets over a CodeMatch), while text search highlights the matched spans on a line — different data and different emphasis. Output is unchanged; the existing render tests pass as-is. (6787904)
Resolved src/cocoindex_code/cli.py: both sides added new `search` options, so keep both — upstream's `--json` alongside this branch's `--text` / `-i` / `--no-color`. `--json` has no defined shape for line-level text matches yet, so `--text --json` now errors instead of silently ignoring the flag (consistent with how `--refresh` and `--offset` are handled in text mode).
… --text `grep` and `search --text` print the same shape — a bold path header, then `<line>| <text>` behind a dim gutter — and were duplicating those styling primitives. Moved `paint` / `path_header` / `gutter` / `gutter_width` into a small `render.py`; both now use it. What each search draws *inside* a line stays where it was: grep dims the context around a structural match, text search highlights the matched spans. Output is unchanged — the existing render tests pass as-is. Per review feedback on cocoindex-io#212 (@georgeh0).
Merged |
Summary
Adds
ccc search --text— literal + regex full-text search, alongside semanticsearchand structuralgrep. Addresses #192.It's an index-free local scan: like
grep, it branches before the daemon/project gate (runs in any directory, noccc initneeded) and reusesfile_walk, so the searched file set + whitelist match the indexer. Scope and design were aligned with @georgeh0 in the thread.Query model
/regex/marks a regex, otherwise literal — GitHub-style:/…/is a regex only when its body has no free (unescaped)/, so/blobs/v1/stays literal.-i/-sto force); reuses--lang/--path;--limitcaps results;--refreshis rejected (index-only).grepskips (any included file, not just code-matchable languages).Notes
Dockerfile,.github/**, dotfiles excluded — an opt-in--all-filescan come later).tests/test_textsearch.py);ruff/mypyclean. Scan is linear (~0.11 ms/file) — cocoindex (859 files) runs a query in ~0.36 s.Next steps — optional full-text index
Non-indexed by design for v1 (per @georgeh0, indexing can be a future opt-in). A live scan stays fast for typical repos but grows linearly, so very large trees (100K–1M files) will want an index.
Planned follow-up: an opt-in inverted index — likely SQLite FTS5, maintained incrementally by the daemon like the vector index — that
search --textqueries for literal terms, falling back to the live scan for regex and for always-fresh results. This PR is already shaped for it: matching / rendering / walk are index-agnostic, and the newTermtype carries the per-term literal-vs-regex signal the index needs (literals → index lookup, regex → scan).