From 7681d37a8b0b5b6e604afba34163ccff98f4c997 Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Thu, 26 Feb 2026 12:00:24 -0800 Subject: [PATCH 1/4] fix(queryLanguage): allow parenthesized regex alternation in filter values Queries like `file:(test|spec)` or `-file:(test|spec)` previously failed with "No parse at N" because the word tokenizer unconditionally deferred to parenToken whenever a token started with balanced parentheses, even in value contexts (right after a prefix keyword colon like `file:`, `repo:`, `sym:`, etc.). The fix detects value context by looking backward for a preceding ':' and, when found, uses depth-tracking to consume the entire '(...)' as a word instead of deferring. This correctly handles nested parens, stops at an outer ParenExpr closing paren, and leaves all existing parse behaviour unchanged for non-value contexts. Co-Authored-By: Claude Sonnet 4.6 --- packages/queryLanguage/src/tokens.ts | 41 +++++++++++++++++++++--- packages/queryLanguage/test/negation.txt | 24 ++++++++++++++ packages/queryLanguage/test/prefixes.txt | 40 +++++++++++++++++++++++ 3 files changed, 100 insertions(+), 5 deletions(-) diff --git a/packages/queryLanguage/src/tokens.ts b/packages/queryLanguage/src/tokens.ts index 96c66ae59..4fbd656c1 100644 --- a/packages/queryLanguage/src/tokens.ts +++ b/packages/queryLanguage/src/tokens.ts @@ -313,14 +313,44 @@ export const wordToken = new ExternalTokenizer((input, stack) => { return; } - // If starts with '(' and has balanced parens, don't consume as word - // (let parenToken handle it) + // If starts with '(' and has balanced parens, determine whether this is a + // regex alternation value (e.g. file:(test|spec)) or a ParenExpr grouping. + // We're in a value context when the immediately preceding non-whitespace char + // is ':', meaning we're right after a prefix keyword. In that case consume the + // entire '(...)' as a word using depth-tracking so the consuming loop doesn't + // stop early at ')'. Otherwise defer to parenToken for grouping. + let inValueParenContext = false; if (input.next === OPEN_PAREN && hasBalancedParensAt(input, 0)) { - return; + let backOffset = -1; + while (isWhitespace(input.peek(backOffset))) { + backOffset--; + } + if (input.peek(backOffset) === COLON) { + inValueParenContext = true; + } else { + return; // Not a value context — defer to parenToken for grouping + } } - + const startPos = input.pos; + if (inValueParenContext) { + // Consume the parenthesized pattern with depth tracking so we consume + // the matching ')' without stopping early. A ')' at depth 0 means we've + // hit an outer ParenExpr closing paren — stop without consuming it. + let depth = 0; + while (input.next !== EOF) { + const ch = input.next; + if (isWhitespace(ch)) break; + if (ch === OPEN_PAREN) { + depth++; + } else if (ch === CLOSE_PAREN) { + if (depth === 0) break; // outer ParenExpr closing — don't consume + depth--; + } + input.advance(); + } + } else { // Consume characters while (input.next !== EOF) { const ch = input.next; @@ -339,7 +369,8 @@ export const wordToken = new ExternalTokenizer((input, stack) => { input.advance(); } - + } + if (input.pos > startPos) { input.acceptToken(word); } diff --git a/packages/queryLanguage/test/negation.txt b/packages/queryLanguage/test/negation.txt index c229324c0..105c96347 100644 --- a/packages/queryLanguage/test/negation.txt +++ b/packages/queryLanguage/test/negation.txt @@ -253,3 +253,27 @@ Program(NegateExpr(ParenExpr)) ==> Program(NegateExpr(PrefixExpr(FileExpr))) + +# Negate file with regex alternation in value + +-file:(test|spec) + +==> + +Program(NegateExpr(PrefixExpr(FileExpr))) + +# Negate repo with regex alternation in value + +-repo:(org1|org2) + +==> + +Program(NegateExpr(PrefixExpr(RepoExpr))) + +# Complex query with negated file alternation + +chat lang:TypeScript -file:(test|spec) + +==> + +Program(AndExpr(Term,PrefixExpr(LangExpr),NegateExpr(PrefixExpr(FileExpr)))) diff --git a/packages/queryLanguage/test/prefixes.txt b/packages/queryLanguage/test/prefixes.txt index 00533ec03..017939042 100644 --- a/packages/queryLanguage/test/prefixes.txt +++ b/packages/queryLanguage/test/prefixes.txt @@ -334,3 +334,43 @@ Program(ParenExpr(PrefixExpr(FileExpr))) Program(ParenExpr(AndExpr(PrefixExpr(FileExpr),PrefixExpr(LangExpr)))) +# File with regex alternation in value + +file:(test|spec) + +==> + +Program(PrefixExpr(FileExpr)) + +# Repo with regex alternation in value + +repo:(org1|org2) + +==> + +Program(PrefixExpr(RepoExpr)) + +# Sym with regex alternation in value + +sym:(Foo|Bar) + +==> + +Program(PrefixExpr(SymExpr)) + +# Content with regex alternation in value + +content:(error|warning) + +==> + +Program(PrefixExpr(ContentExpr)) + +# File alternation combined with other filters + +file:(test|spec) lang:TypeScript + +==> + +Program(AndExpr(PrefixExpr(FileExpr),PrefixExpr(LangExpr))) + From 3f7be64c23c7533b19f0995aa767202c6eb3c483 Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Thu, 26 Feb 2026 12:00:53 -0800 Subject: [PATCH 2/4] changelog Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2e77a908..6eec98872 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed text inside angle brackets (e.g., ``) being hidden in chat prompt display due to HTML parsing. [#929](https://github.com/sourcebot-dev/sourcebot/pull/929) [#932](https://github.com/sourcebot-dev/sourcebot/pull/932) - Fixed permission sync banner flashing on initial page load. [#942](https://github.com/sourcebot-dev/sourcebot/pull/942) - Fixed issue where the permission sync banner would sometimes not appear until the page was refreshed. [#942](https://github.com/sourcebot-dev/sourcebot/pull/942) +- Fixed search query parser rejecting parenthesized regex alternation in filter values (e.g. `file:(test|spec)`, `-file:(test|spec)`). [#946](https://github.com/sourcebot-dev/sourcebot/pull/946) ## [4.11.7] - 2026-02-23 From ff4fa79e1305b7d2fa52392df6d8efae6a84cacd Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 20:05:07 +0000 Subject: [PATCH 3/4] chore: move fix entry to Unreleased section in CHANGELOG Co-authored-by: Brendan Kellam --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6eec98872..84beae193 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +- Fixed search query parser rejecting parenthesized regex alternation in filter values (e.g. `file:(test|spec)`, `-file:(test|spec)`). [#946](https://github.com/sourcebot-dev/sourcebot/pull/946) + ## [4.12.0] - 2026-02-26 ### Added @@ -28,7 +31,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed text inside angle brackets (e.g., ``) being hidden in chat prompt display due to HTML parsing. [#929](https://github.com/sourcebot-dev/sourcebot/pull/929) [#932](https://github.com/sourcebot-dev/sourcebot/pull/932) - Fixed permission sync banner flashing on initial page load. [#942](https://github.com/sourcebot-dev/sourcebot/pull/942) - Fixed issue where the permission sync banner would sometimes not appear until the page was refreshed. [#942](https://github.com/sourcebot-dev/sourcebot/pull/942) -- Fixed search query parser rejecting parenthesized regex alternation in filter values (e.g. `file:(test|spec)`, `-file:(test|spec)`). [#946](https://github.com/sourcebot-dev/sourcebot/pull/946) ## [4.11.7] - 2026-02-23 From aa44ab940cbbb08db303baf53edaab3811d1459d Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Thu, 26 Feb 2026 12:09:59 -0800 Subject: [PATCH 4/4] chore(ci): consolidate test-backend and test-web into a single test workflow Replaces the two separate workflows with a single `test.yml` that runs `yarn test` at the repo root, which executes all workspace tests topologically via `yarn workspaces foreach`. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/test-backend.yml | 28 -------------------- .github/workflows/{test-web.yml => test.yml} | 15 +++++------ 2 files changed, 7 insertions(+), 36 deletions(-) delete mode 100644 .github/workflows/test-backend.yml rename .github/workflows/{test-web.yml => test.yml} (71%) diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml deleted file mode 100644 index 5d4b5fde1..000000000 --- a/.github/workflows/test-backend.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Test Backend - -on: - pull_request: - branches: ["main"] - - -jobs: - build: - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - submodules: "true" - - name: Use Node.Js - uses: actions/setup-node@v4 - with: - node-version: '20.x' - - - name: Install - run: yarn install --frozen-lockfile - - - name: Test - run: yarn workspace @sourcebot/backend test - diff --git a/.github/workflows/test-web.yml b/.github/workflows/test.yml similarity index 71% rename from .github/workflows/test-web.yml rename to .github/workflows/test.yml index 108cc2778..d52e3147a 100644 --- a/.github/workflows/test-web.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: Test Web +name: Test on: pull_request: @@ -6,7 +6,7 @@ on: jobs: - build: + test: runs-on: ubuntu-latest permissions: contents: read @@ -15,14 +15,13 @@ jobs: uses: actions/checkout@v4 with: submodules: "true" - - name: Use Node.Js + - name: Use Node.js uses: actions/setup-node@v4 with: - node-version: '20.x' - + node-version: '20.x' + - name: Install run: yarn install --frozen-lockfile - + - name: Test - run: yarn workspace @sourcebot/web test - + run: yarn test