mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-11 20:05:25 +00:00
Compare commits
32 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
095474a901 | ||
|
|
d63f3cf9d9 | ||
|
|
3d85a0595c | ||
|
|
84cf524d84 | ||
|
|
7c72578765 | ||
|
|
483b433aab | ||
|
|
bcca1d6d7d | ||
|
|
0e88eecc30 | ||
|
|
a4685e34ab | ||
|
|
76dc2f5a12 | ||
|
|
7fc068f8b2 | ||
|
|
91caf129ed | ||
|
|
92578881df | ||
|
|
28986f4355 | ||
|
|
41a6eb48a0 | ||
|
|
92ae76168c | ||
|
|
f1dd16be82 | ||
|
|
cc2837b740 | ||
|
|
0633d1f23c | ||
|
|
8bc4f1e520 | ||
|
|
c962fdd636 | ||
|
|
8e036a340f | ||
|
|
fb305c2808 | ||
|
|
c671e96139 | ||
|
|
f3a8fa3dab | ||
|
|
09507d3e89 | ||
|
|
97dd54d48f | ||
|
|
831197980c | ||
|
|
9bee8c2c59 | ||
|
|
e20d514569 | ||
|
|
1dff20d47a | ||
|
|
fbe1073d0e |
216 changed files with 9673 additions and 2591 deletions
|
|
@ -6,8 +6,6 @@ DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
|
|||
ZOEKT_WEBSERVER_URL="http://localhost:6070"
|
||||
# The command to use for generating ctags.
|
||||
CTAGS_COMMAND=ctags
|
||||
# logging, strict
|
||||
SRC_TENANT_ENFORCEMENT_MODE=strict
|
||||
|
||||
# Auth.JS
|
||||
# You can generate a new secret with:
|
||||
|
|
@ -23,7 +21,7 @@ AUTH_URL="http://localhost:3000"
|
|||
|
||||
DATA_CACHE_DIR=${PWD}/.sourcebot # Path to the sourcebot cache dir (ex. ~/sourcebot/.sourcebot)
|
||||
SOURCEBOT_PUBLIC_KEY_PATH=${PWD}/public.pem
|
||||
# CONFIG_PATH=${PWD}/config.json # Path to the sourcebot config file (if one exists)
|
||||
CONFIG_PATH=${PWD}/config.json # Path to the sourcebot config file (if one exists)
|
||||
|
||||
# Email
|
||||
# EMAIL_FROM_ADDRESS="" # The from address for transactional emails.
|
||||
|
|
@ -31,7 +29,6 @@ SOURCEBOT_PUBLIC_KEY_PATH=${PWD}/public.pem
|
|||
|
||||
# PostHog
|
||||
# POSTHOG_PAPIK=""
|
||||
# NEXT_PUBLIC_POSTHOG_PAPIK=""
|
||||
|
||||
# Sentry
|
||||
# SENTRY_BACKEND_DSN=""
|
||||
|
|
|
|||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
|
|
@ -1,4 +1,4 @@
|
|||
contact_links:
|
||||
- name: 👾 Discord
|
||||
url: https://discord.gg/GbXMEM5H
|
||||
url: https://discord.gg/HDScTs3ptP
|
||||
about: Something else? Join the Discord!
|
||||
|
|
|
|||
1
.github/workflows/_gcp-deploy.yml
vendored
1
.github/workflows/_gcp-deploy.yml
vendored
|
|
@ -55,7 +55,6 @@ jobs:
|
|||
${{ env.IMAGE_PATH }}:latest
|
||||
build-args: |
|
||||
NEXT_PUBLIC_SOURCEBOT_VERSION=${{ github.ref_name }}
|
||||
NEXT_PUBLIC_POSTHOG_PAPIK=${{ vars.NEXT_PUBLIC_POSTHOG_PAPIK }}
|
||||
NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT=${{ vars.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT }}
|
||||
NEXT_PUBLIC_SENTRY_ENVIRONMENT=${{ vars.NEXT_PUBLIC_SENTRY_ENVIRONMENT }}
|
||||
NEXT_PUBLIC_SENTRY_WEBAPP_DSN=${{ vars.NEXT_PUBLIC_SENTRY_WEBAPP_DSN }}
|
||||
|
|
|
|||
1
.github/workflows/ghcr-publish.yml
vendored
1
.github/workflows/ghcr-publish.yml
vendored
|
|
@ -77,7 +77,6 @@ jobs:
|
|||
outputs: type=image,name=${{ env.REGISTRY_IMAGE }},push-by-digest=true,name-canonical=true,push=true,annotation.org.opencontainers.image.description=Blazingly fast code search
|
||||
build-args: |
|
||||
NEXT_PUBLIC_SOURCEBOT_VERSION=${{ github.ref_name }}
|
||||
NEXT_PUBLIC_POSTHOG_PAPIK=${{ vars.NEXT_PUBLIC_POSTHOG_PAPIK }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
|
|
|
|||
56
CHANGELOG.md
56
CHANGELOG.md
|
|
@ -7,6 +7,62 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
- Fixed review agent so that it works with GHES instances [#611](https://github.com/sourcebot-dev/sourcebot/pull/611)
|
||||
|
||||
### Added
|
||||
- Added support for arbitrary user IDs required for OpenShift. [#658](https://github.com/sourcebot-dev/sourcebot/pull/658)
|
||||
|
||||
### Updated
|
||||
- Improved error messages in file source api. [#665](https://github.com/sourcebot-dev/sourcebot/pull/665)
|
||||
|
||||
## [4.10.2] - 2025-12-04
|
||||
|
||||
### Fixed
|
||||
- Fixed issue where the disable telemetry flag was not being respected for web server telemetry. [#657](https://github.com/sourcebot-dev/sourcebot/pull/657)
|
||||
|
||||
## [4.10.1] - 2025-12-03
|
||||
|
||||
### Added
|
||||
- Added `ALWAYS_INDEX_FILE_PATTERNS` environment variable to allow specifying a comma seperated list of glob patterns matching file paths that should always be indexed, regardless of size or # of trigrams. [#631](https://github.com/sourcebot-dev/sourcebot/pull/631)
|
||||
- Added button to explore menu to toggle cross-repository search. [#647](https://github.com/sourcebot-dev/sourcebot/pull/647)
|
||||
- Added server side telemetry for search metrics. [#652](https://github.com/sourcebot-dev/sourcebot/pull/652)
|
||||
|
||||
### Fixed
|
||||
- Fixed issue where single quotes could not be used in search queries. [#629](https://github.com/sourcebot-dev/sourcebot/pull/629)
|
||||
- Fixed issue where files with special characters would fail to load. [#636](https://github.com/sourcebot-dev/sourcebot/issues/636)
|
||||
- Fixed Ask performance issues. [#632](https://github.com/sourcebot-dev/sourcebot/pull/632)
|
||||
- Fixed regression where creating a new Ask thread when unauthenticated would result in a 404. [#641](https://github.com/sourcebot-dev/sourcebot/pull/641)
|
||||
- Updated react and next package versions to fix CVE 2025-55182. [#654](https://github.com/sourcebot-dev/sourcebot/pull/654)
|
||||
|
||||
### Changed
|
||||
- Changed the default behaviour for code nav to scope references & definitions search to the current repository. [#647](https://github.com/sourcebot-dev/sourcebot/pull/647)
|
||||
|
||||
## [4.10.0] - 2025-11-24
|
||||
|
||||
### Added
|
||||
- Added support for streaming code search results. [#623](https://github.com/sourcebot-dev/sourcebot/pull/623)
|
||||
- Added buttons to toggle case sensitivity and regex patterns. [#623](https://github.com/sourcebot-dev/sourcebot/pull/623)
|
||||
- Added counts to members, requets, and invites tabs in the members settings. [#621](https://github.com/sourcebot-dev/sourcebot/pull/621)
|
||||
- [Sourcebot EE] Add support for Authentik as a identity provider. [#627](https://github.com/sourcebot-dev/sourcebot/pull/627)
|
||||
|
||||
### Changed
|
||||
- Changed the default search behaviour to match patterns as substrings and **not** regular expressions. Regular expressions can be used by toggling the regex button in search bar. [#623](https://github.com/sourcebot-dev/sourcebot/pull/623)
|
||||
- Renamed `public` query prefix to `visibility`. Allowed values for `visibility` are `public`, `private`, and `any`. [#623](https://github.com/sourcebot-dev/sourcebot/pull/623)
|
||||
- Changed `archived` query prefix to accept values `yes`, `no`, and `only`. [#623](https://github.com/sourcebot-dev/sourcebot/pull/623)
|
||||
|
||||
### Removed
|
||||
- Removed `case` query prefix. [#623](https://github.com/sourcebot-dev/sourcebot/pull/623)
|
||||
- Removed `branch` and `b` query prefixes. Please use `rev:` instead. [#623](https://github.com/sourcebot-dev/sourcebot/pull/623)
|
||||
- Removed `regex` query prefix. [#623](https://github.com/sourcebot-dev/sourcebot/pull/623)
|
||||
|
||||
### Fixed
|
||||
- Fixed spurious infinite loads with explore panel, file tree, and file search command. [#617](https://github.com/sourcebot-dev/sourcebot/pull/617)
|
||||
- Wipe search context on init if entitlement no longer exists [#618](https://github.com/sourcebot-dev/sourcebot/pull/618)
|
||||
- Fixed Bitbucket repository exclusions not supporting glob patterns. [#620](https://github.com/sourcebot-dev/sourcebot/pull/620)
|
||||
- Fixed issue where the repo driven permission syncer was attempting to sync public repositories. [#624](https://github.com/sourcebot-dev/sourcebot/pull/624)
|
||||
- Fixed issue where worker would not shutdown while a permission sync job (repo or user) was in progress. [#624](https://github.com/sourcebot-dev/sourcebot/pull/624)
|
||||
|
||||
## [4.9.2] - 2025-11-13
|
||||
|
||||
### Changed
|
||||
|
|
|
|||
51
Dockerfile
51
Dockerfile
|
|
@ -1,3 +1,4 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
# ------ Global scope variables ------
|
||||
|
||||
# Set of global build arguments.
|
||||
|
|
@ -8,11 +9,6 @@
|
|||
# @see: https://docs.docker.com/build/building/variables/#scoping
|
||||
|
||||
ARG NEXT_PUBLIC_SOURCEBOT_VERSION
|
||||
# PAPIK = Project API Key
|
||||
# Note that this key does not need to be kept secret, so it's not
|
||||
# necessary to use Docker build secrets here.
|
||||
# @see: https://posthog.com/tutorials/api-capture-events#authenticating-with-the-project-api-key
|
||||
ARG NEXT_PUBLIC_POSTHOG_PAPIK
|
||||
ARG NEXT_PUBLIC_SENTRY_ENVIRONMENT
|
||||
ARG NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT
|
||||
ARG NEXT_PUBLIC_SENTRY_WEBAPP_DSN
|
||||
|
|
@ -43,10 +39,12 @@ COPY .yarn ./.yarn
|
|||
COPY ./packages/db ./packages/db
|
||||
COPY ./packages/schemas ./packages/schemas
|
||||
COPY ./packages/shared ./packages/shared
|
||||
COPY ./packages/queryLanguage ./packages/queryLanguage
|
||||
|
||||
RUN yarn workspace @sourcebot/db install
|
||||
RUN yarn workspace @sourcebot/schemas install
|
||||
RUN yarn workspace @sourcebot/shared install
|
||||
RUN yarn workspace @sourcebot/query-language install
|
||||
# ------------------------------------
|
||||
|
||||
# ------ Build Web ------
|
||||
|
|
@ -55,8 +53,6 @@ ENV SKIP_ENV_VALIDATION=1
|
|||
# -----------
|
||||
ARG NEXT_PUBLIC_SOURCEBOT_VERSION
|
||||
ENV NEXT_PUBLIC_SOURCEBOT_VERSION=$NEXT_PUBLIC_SOURCEBOT_VERSION
|
||||
ARG NEXT_PUBLIC_POSTHOG_PAPIK
|
||||
ENV NEXT_PUBLIC_POSTHOG_PAPIK=$NEXT_PUBLIC_POSTHOG_PAPIK
|
||||
ARG NEXT_PUBLIC_SENTRY_ENVIRONMENT
|
||||
ENV NEXT_PUBLIC_SENTRY_ENVIRONMENT=$NEXT_PUBLIC_SENTRY_ENVIRONMENT
|
||||
ARG NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT
|
||||
|
|
@ -92,6 +88,7 @@ COPY --from=shared-libs-builder /app/node_modules ./node_modules
|
|||
COPY --from=shared-libs-builder /app/packages/db ./packages/db
|
||||
COPY --from=shared-libs-builder /app/packages/schemas ./packages/schemas
|
||||
COPY --from=shared-libs-builder /app/packages/shared ./packages/shared
|
||||
COPY --from=shared-libs-builder /app/packages/queryLanguage ./packages/queryLanguage
|
||||
|
||||
# Fixes arm64 timeouts
|
||||
RUN yarn workspace @sourcebot/web install
|
||||
|
|
@ -130,6 +127,7 @@ COPY --from=shared-libs-builder /app/node_modules ./node_modules
|
|||
COPY --from=shared-libs-builder /app/packages/db ./packages/db
|
||||
COPY --from=shared-libs-builder /app/packages/schemas ./packages/schemas
|
||||
COPY --from=shared-libs-builder /app/packages/shared ./packages/shared
|
||||
COPY --from=shared-libs-builder /app/packages/queryLanguage ./packages/queryLanguage
|
||||
RUN yarn workspace @sourcebot/backend install
|
||||
RUN yarn workspace @sourcebot/backend build
|
||||
|
||||
|
|
@ -150,8 +148,6 @@ FROM node-alpine AS runner
|
|||
# -----------
|
||||
ARG NEXT_PUBLIC_SOURCEBOT_VERSION
|
||||
ENV NEXT_PUBLIC_SOURCEBOT_VERSION=$NEXT_PUBLIC_SOURCEBOT_VERSION
|
||||
ARG NEXT_PUBLIC_POSTHOG_PAPIK
|
||||
ENV NEXT_PUBLIC_POSTHOG_PAPIK=$NEXT_PUBLIC_POSTHOG_PAPIK
|
||||
ARG NEXT_PUBLIC_SENTRY_ENVIRONMENT
|
||||
ENV NEXT_PUBLIC_SENTRY_ENVIRONMENT=$NEXT_PUBLIC_SENTRY_ENVIRONMENT
|
||||
ARG NEXT_PUBLIC_SENTRY_WEBAPP_DSN
|
||||
|
|
@ -173,8 +169,13 @@ ENV DATA_DIR=/data
|
|||
ENV DATA_CACHE_DIR=$DATA_DIR/.sourcebot
|
||||
ENV DATABASE_DATA_DIR=$DATA_CACHE_DIR/db
|
||||
ENV REDIS_DATA_DIR=$DATA_CACHE_DIR/redis
|
||||
ENV SRC_TENANT_ENFORCEMENT_MODE=strict
|
||||
ENV SOURCEBOT_PUBLIC_KEY_PATH=/app/public.pem
|
||||
# PAPIK = Project API Key
|
||||
# Note that this key does not need to be kept secret, so it's not
|
||||
# necessary to use Docker build secrets here.
|
||||
# @see: https://posthog.com/tutorials/api-capture-events#authenticating-with-the-project-api-key
|
||||
# @note: this is also declared in the shared env.server.ts file.
|
||||
ENV POSTHOG_PAPIK=phc_lLPuFFi5LH6c94eFJcqvYVFwiJffVcV6HD8U4a1OnRW
|
||||
|
||||
# Valid values are: debug, info, warn, error
|
||||
ENV SOURCEBOT_LOG_LEVEL=info
|
||||
|
|
@ -194,6 +195,7 @@ RUN addgroup -g $GID sourcebot && \
|
|||
adduser -D -u $UID -h /app -S sourcebot && \
|
||||
adduser sourcebot postgres && \
|
||||
adduser sourcebot redis && \
|
||||
chown -R sourcebot /app && \
|
||||
adduser sourcebot node && \
|
||||
mkdir /var/log/sourcebot && \
|
||||
chown sourcebot /var/log/sourcebot
|
||||
|
|
@ -217,18 +219,23 @@ COPY --from=zoekt-builder \
|
|||
/cmd/zoekt-index \
|
||||
/usr/local/bin/
|
||||
|
||||
RUN chown -R sourcebot:sourcebot /app
|
||||
|
||||
# Copy zoekt proto files (needed for gRPC client at runtime)
|
||||
COPY --chown=sourcebot:sourcebot vendor/zoekt/grpc/protos /app/vendor/zoekt/grpc/protos
|
||||
|
||||
# Copy all of the things
|
||||
COPY --from=web-builder /app/packages/web/public ./packages/web/public
|
||||
COPY --from=web-builder /app/packages/web/.next/standalone ./
|
||||
COPY --from=web-builder /app/packages/web/.next/static ./packages/web/.next/static
|
||||
COPY --chown=sourcebot:sourcebot --from=web-builder /app/packages/web/public ./packages/web/public
|
||||
COPY --chown=sourcebot:sourcebot --from=web-builder /app/packages/web/.next/standalone ./
|
||||
COPY --chown=sourcebot:sourcebot --from=web-builder /app/packages/web/.next/static ./packages/web/.next/static
|
||||
|
||||
COPY --from=backend-builder /app/node_modules ./node_modules
|
||||
COPY --from=backend-builder /app/packages/backend ./packages/backend
|
||||
COPY --chown=sourcebot:sourcebot --from=backend-builder /app/node_modules ./node_modules
|
||||
COPY --chown=sourcebot:sourcebot --from=backend-builder /app/packages/backend ./packages/backend
|
||||
|
||||
COPY --from=shared-libs-builder /app/node_modules ./node_modules
|
||||
COPY --from=shared-libs-builder /app/packages/db ./packages/db
|
||||
COPY --from=shared-libs-builder /app/packages/schemas ./packages/schemas
|
||||
COPY --from=shared-libs-builder /app/packages/shared ./packages/shared
|
||||
COPY --chown=sourcebot:sourcebot --from=shared-libs-builder /app/packages/db ./packages/db
|
||||
COPY --chown=sourcebot:sourcebot --from=shared-libs-builder /app/packages/schemas ./packages/schemas
|
||||
COPY --chown=sourcebot:sourcebot --from=shared-libs-builder /app/packages/shared ./packages/shared
|
||||
COPY --chown=sourcebot:sourcebot --from=shared-libs-builder /app/packages/queryLanguage ./packages/queryLanguage
|
||||
|
||||
# Fixes git "dubious ownership" issues when the volume is mounted with different permissions to the container.
|
||||
RUN git config --global safe.directory "*"
|
||||
|
|
@ -239,9 +246,11 @@ RUN mkdir -p /run/postgresql && \
|
|||
chmod 775 /run/postgresql
|
||||
|
||||
# Make app directory accessible to both root and sourcebot user
|
||||
RUN chown -R sourcebot:sourcebot /app
|
||||
RUN chown -R sourcebot /app \
|
||||
&& chgrp -R 0 /app \
|
||||
&& chmod -R g=u /app
|
||||
# Make data directory accessible to both root and sourcebot user
|
||||
RUN chown -R sourcebot:sourcebot /data
|
||||
RUN chown -R sourcebot /data
|
||||
|
||||
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||
COPY prefix-output.sh ./prefix-output.sh
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@
|
|||
"socials": {
|
||||
"github": "https://github.com/sourcebot-dev/sourcebot",
|
||||
"twitter": "https://x.com/sourcebot_dev",
|
||||
"discord": "https://discord.gg/GbXMEM5H",
|
||||
"discord": "https://discord.gg/HDScTs3ptP",
|
||||
"linkedin": "https://www.linkedin.com/company/sourcebot"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -25,4 +25,4 @@ Sourcebot's built-in authentication system gates your deployment, and allows adm
|
|||
# Troubleshooting
|
||||
|
||||
- If you experience issues logging in, logging out, or accessing an organization you should have access to, try clearing your cookies & performing a full page refresh (`Cmd/Ctrl + Shift + R` on most browsers).
|
||||
- Still not working? Reach out to us on our [discord](https://discord.gg/GbXMEM5H) or [GitHub](https://github.com/sourcebot-dev/sourcebot/issues/new/choose)
|
||||
- Still not working? Reach out to us on our [discord](https://discord.gg/HDScTs3ptP) or [GitHub](https://github.com/sourcebot-dev/sourcebot/issues/new/choose)
|
||||
|
|
@ -35,6 +35,7 @@ The following environment variables allow you to configure your Sourcebot deploy
|
|||
| `SOURCEBOT_STRUCTURED_LOGGING_FILE` | - | <p>Optional file to log to if structured logging is enabled</p> |
|
||||
| `SOURCEBOT_TELEMETRY_DISABLED` | `false` | <p>Enables/disables telemetry collection in Sourcebot. See [this doc](/docs/overview.mdx#telemetry) for more info.</p> |
|
||||
| `DEFAULT_MAX_MATCH_COUNT` | `10000` | <p>The default maximum number of search results to return when using search in the web app.</p> |
|
||||
| `ALWAYS_INDEX_FILE_PATTERNS` | - | <p>A comma separated list of glob patterns matching file paths that should always be indexed, regardless of size or number of trigrams.</p> |
|
||||
|
||||
### Enterprise Environment Variables
|
||||
| Variable | Default | Description |
|
||||
|
|
|
|||
|
|
@ -366,3 +366,53 @@ A Microsoft Entra ID connection can be used for [authentication](/docs/configura
|
|||
</Steps>
|
||||
</Accordion>
|
||||
|
||||
### Authentik
|
||||
|
||||
[Auth.js Authentik Provider Docs](https://authjs.dev/getting-started/providers/authentik)
|
||||
|
||||
An Authentik connection can be used for [authentication](/docs/configuration/auth).
|
||||
|
||||
<Accordion title="instructions">
|
||||
<Steps>
|
||||
<Step title="Create a OAuth2/OpenID Connect application">
|
||||
To begin, you must create a OAuth2/OpenID Connect application in Authentik. For more information, see the [Authentik documentation](https://docs.goauthentik.io/add-secure-apps/applications/manage_apps/#create-an-application-and-provider-pair).
|
||||
|
||||
When configuring your application:
|
||||
- Set the provider type to "OAuth2/OpenID Connect"
|
||||
- Set the client type to "Confidential"
|
||||
- Add `<sourcebot_url>/api/auth/callback/authentik` to the redirect URIs (ex. https://sourcebot.coolcorp.com/api/auth/callback/authentik)
|
||||
|
||||
After creating the application, open the application details to obtain the client id, client secret, and issuer URL (typically in the format `https://<authentik-domain>/application/o/<provider-slug>/`).
|
||||
</Step>
|
||||
<Step title="Define environment variables">
|
||||
The client id, secret, and issuer URL are provided to Sourcebot via environment variables. These can be named whatever you like
|
||||
(ex. `AUTHENTIK_IDENTITY_PROVIDER_CLIENT_ID`, `AUTHENTIK_IDENTITY_PROVIDER_CLIENT_SECRET`, and `AUTHENTIK_IDENTITY_PROVIDER_ISSUER`)
|
||||
</Step>
|
||||
<Step title="Define the identity provider config">
|
||||
Create a `identityProvider` object in the [config file](/docs/configuration/config-file) with the following fields:
|
||||
|
||||
```json wrap icon="code"
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/sourcebot-dev/sourcebot/main/schemas/v3/index.json",
|
||||
"identityProviders": [
|
||||
{
|
||||
"provider": "authentik",
|
||||
"purpose": "sso",
|
||||
"clientId": {
|
||||
"env": "AUTHENTIK_IDENTITY_PROVIDER_CLIENT_ID"
|
||||
},
|
||||
"clientSecret": {
|
||||
"env": "AUTHENTIK_IDENTITY_PROVIDER_CLIENT_SECRET"
|
||||
},
|
||||
"issuer": {
|
||||
"env": "AUTHENTIK_IDENTITY_PROVIDER_ISSUER"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
</Accordion>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -69,6 +69,26 @@ To learn more about how to create a connection for a specific code host, check o
|
|||
|
||||
<Note>Missing your code host? [Submit a feature request on GitHub](https://github.com/sourcebot-dev/sourcebot/issues/new?template=feature_request.md).</Note>
|
||||
|
||||
## Indexing Large Files
|
||||
|
||||
By default, Sourcebot will skip indexing files that are larger than 2MB or have more than 20,000 trigrams. You can configure this by setting the `maxFileSize` and `maxTrigramCount` [settings](/docs/configuration/config-file#settings).
|
||||
|
||||
These limits can be ignored for specific files by passing in a comma separated list of glob patterns matching file paths to the `ALWAYS_INDEX_FILE_PATTERNS` environment variable. For example:
|
||||
|
||||
```bash
|
||||
# Always index all .sum and .lock files
|
||||
ALWAYS_INDEX_FILE_PATTERNS=**/*.sum,**/*.lock
|
||||
```
|
||||
|
||||
Files that have been skipped are assigned the `skipped` language. You can view a list of all skipped files by using the following query:
|
||||
```
|
||||
lang:skipped
|
||||
```
|
||||
|
||||
## Indexing Binary Files
|
||||
|
||||
Binary files cannot be indexed by Sourcebot. See [#575](https://github.com/sourcebot-dev/sourcebot/issues/575) for more information.
|
||||
|
||||
|
||||
## Schema reference
|
||||
---
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import LicenseKeyRequired from '/snippets/license-key-required.mdx'
|
|||
| **Go to definition** | Clicking the "go to definition" button in the popover or clicking the symbol name navigates to the symbol's definition. |
|
||||
| **Find references** | Clicking the "find all references" button in the popover lists all references in the explore panel. |
|
||||
| **Explore panel** | Lists all references and definitions for the symbol selected in the popover. |
|
||||
| **Cross-repository navigation** | You can search across all repositories by clicking the globe icon in the explore panel. By default, references and definitions are scoped to the repository where the symbol is being resolved. |
|
||||
|
||||
## How does it work?
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
---
|
||||
title: "Permission syncing"
|
||||
sidebarTitle: "Permission syncing"
|
||||
tag: "experimental"
|
||||
---
|
||||
|
||||
import LicenseKeyRequired from '/snippets/license-key-required.mdx'
|
||||
import ExperimentalFeatureWarning from '/snippets/experimental-feature-warning.mdx'
|
||||
|
||||
<LicenseKeyRequired />
|
||||
<ExperimentalFeatureWarning />
|
||||
|
||||
# Overview
|
||||
|
||||
|
|
|
|||
|
|
@ -4,32 +4,51 @@ title: Writing search queries
|
|||
|
||||
Sourcebot uses a powerful regex-based query language that enabled precise code search within large codebases.
|
||||
|
||||
|
||||
## Syntax reference guide
|
||||
|
||||
Queries consist of space-separated regular expressions. Wrapping expressions in `""` combines them. By default, a file must have at least one match for each expression to be included.
|
||||
Queries consist of space-separated search patterns that are matched against file contents. A file must have at least one match for each expression to be included. Queries can optionally contain search filters to further refine the search results.
|
||||
|
||||
## Keyword search (default)
|
||||
|
||||
Keyword search matches search patterns exactly in file contents. Wrapping search patterns in `""` combines them as a single expression.
|
||||
|
||||
| Example | Explanation |
|
||||
| :--- | :--- |
|
||||
| `foo` | Match files containing the keyword `foo` |
|
||||
| `foo bar` | Match files containing both `foo` **and** `bar` |
|
||||
| `"foo bar"` | Match files containing the phrase `foo bar` |
|
||||
| `"foo \"bar\""` | Match files containing `foo "bar"` exactly (escaped quotes) |
|
||||
|
||||
## Regex search
|
||||
|
||||
Toggle the regex button (`.*`) in the search bar to interpret search patterns as regular expressions.
|
||||
|
||||
| Example | Explanation |
|
||||
| :--- | :--- |
|
||||
| `foo` | Match files with regex `/foo/` |
|
||||
| `foo bar` | Match files with regex `/foo/` **and** `/bar/` |
|
||||
| `"foo bar"` | Match files with regex `/foo bar/` |
|
||||
| `foo.*bar` | Match files with regex `/foo.*bar/` (foo followed by any characters, then bar) |
|
||||
| `^function\s+\w+` | Match files with regex `/^function\s+\w+/` (function at start of line, followed by whitespace and word characters) |
|
||||
| `"foo bar"` | Match files with regex `/foo bar/`. Quotes are not matched. |
|
||||
|
||||
Multiple expressions can be or'd together with `or`, negated with `-`, or grouped with `()`.
|
||||
## Search filters
|
||||
|
||||
| Example | Explanation |
|
||||
| :--- | :--- |
|
||||
| `foo or bar` | Match files with regex `/foo/` **or** `/bar/` |
|
||||
| `foo -bar` | Match files with regex `/foo/` but **not** `/bar/` |
|
||||
| `foo (bar or baz)` | Match files with regex `/foo/` **and** either `/bar/` **or** `/baz/` |
|
||||
|
||||
Expressions can be prefixed with certain keywords to modify search behavior. Some keywords can be negated using the `-` prefix.
|
||||
Search queries (keyword or regex) can include multiple search filters to further refine the search results. Some filters can be negated using the `-` prefix.
|
||||
|
||||
| Prefix | Description | Example |
|
||||
| :--- | :--- | :--- |
|
||||
| `file:` | Filter results from filepaths that match the regex. By default all files are searched. | `file:README` - Filter results to filepaths that match regex `/README/`<br/>`file:"my file"` - Filter results to filepaths that match regex `/my file/`<br/>`-file:test\.ts$` - Ignore results from filepaths match regex `/test\.ts$/` |
|
||||
| `repo:` | Filter results from repos that match the regex. By default all repos are searched. | `repo:linux` - Filter results to repos that match regex `/linux/`<br/>`-repo:^web/.*` - Ignore results from repos that match regex `/^web\/.*` |
|
||||
| `repo:` | Filter results from repos that match the regex. By default all repos are searched. | `repo:linux` - Filter results to repos that match regex `/linux/`<br/>`-repo:^web/.*` - Ignore results from repos that match regex `/^web\/.*/` |
|
||||
| `rev:` | Filter results from a specific branch or tag. By default **only** the default branch is searched. | `rev:beta` - Filter results to branches that match regex `/beta/` |
|
||||
| `lang:` | Filter results by language (as defined by [linguist](https://github.com/github-linguist/linguist/blob/main/lib/linguist/languages.yml)). By default all languages are searched. | `lang:TypeScript` - Filter results to TypeScript files<br/>`-lang:YAML` - Ignore results from YAML files |
|
||||
| `sym:` | Match symbol definitions created by [universal ctags](https://ctags.io/) at index time. | `sym:\bmain\b` - Filter results to symbols that match regex `/\bmain\b/` |
|
||||
| `context:` | Filter results to a predefined [search context](/docs/features/search/search-contexts). | `context:web` - Filter results to the web context<br/>`-context:pipelines` - Ignore results from the pipelines context |
|
||||
|
||||
## Boolean operators & grouping
|
||||
|
||||
By default, space-separated expressions are and'd together. Using the `or` keyword as well as parentheses `()` can be used to create more complex boolean logic. Parentheses can be negated using the `-` prefix.
|
||||
|
||||
| Example | Explanation |
|
||||
| :--- | :--- |
|
||||
| `foo or bar` | Match files containing `foo` **or** `bar` |
|
||||
| `foo (bar or baz)` | Match files containing `foo` **and** either `bar` **or** `baz`. |
|
||||
| `-(foo) bar` | Match files containing `bar` **and not** `foo`. |
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ If your deployment is dependent on these features, please [reach out](https://gi
|
|||
After updating your configuration file, restart your Sourcebot deployment to pick up the new changes.
|
||||
</Step>
|
||||
<Step title="You're done!">
|
||||
Congrats, you've successfully migrated to v3! Please let us know what you think of the new features by reaching out on our [discord](https://discord.gg/GbXMEM5H) or on [GitHub](https://github.com/sourcebot-dev/sourcebot/issues/new/choose).
|
||||
Congrats, you've successfully migrated to v3! Please let us know what you think of the new features by reaching out on our [discord](https://discord.gg/HDScTs3ptP) or on [GitHub](https://github.com/sourcebot-dev/sourcebot/issues/new/choose).
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
|
|
@ -90,4 +90,4 @@ Some things to check:
|
|||
- Make sure you have a name for each `connection`, and that the name only contains letters, digits, hyphens, or underscores
|
||||
- Make sure each `connection` has a `type` field with a valid value (`gitlab`, `github`, `gitea`, `gerrit`)
|
||||
|
||||
Having troubles migrating from v2 to v3? Reach out to us on [discord](https://discord.gg/GbXMEM5H) or [GitHub](https://github.com/sourcebot-dev/sourcebot/issues/new/choose) and we'll try our best to help
|
||||
Having troubles migrating from v2 to v3? Reach out to us on [discord](https://discord.gg/HDScTs3ptP) or [GitHub](https://github.com/sourcebot-dev/sourcebot/issues/new/choose) and we'll try our best to help
|
||||
|
|
@ -40,7 +40,7 @@ Please note that the following features are no longer supported in v4:
|
|||
|
||||
</Step>
|
||||
<Step title="You're done!">
|
||||
Congrats, you've successfully migrated to v4! Please let us know what you think of the new features by reaching out on our [discord](https://discord.gg/GbXMEM5H) or [GitHub](https://github.com/sourcebot-dev/sourcebot/issues/new/choose)
|
||||
Congrats, you've successfully migrated to v4! Please let us know what you think of the new features by reaching out on our [discord](https://discord.gg/HDScTs3ptP) or [GitHub](https://github.com/sourcebot-dev/sourcebot/issues/new/choose)
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
|
|
@ -58,4 +58,4 @@ to finish upgrading to v4 in single-tenant mode.
|
|||
- If you're hitting issues with signing into your Sourcebot instance, make sure you're setting `AUTH_URL` correctly to your deployment domain (ex. `https://sourcebot.yourcompany.com`)
|
||||
|
||||
|
||||
Having troubles migrating from v3 to v4? Reach out to us on [discord](https://discord.gg/GbXMEM5H) or [GitHub](https://github.com/sourcebot-dev/sourcebot/issues/new/choose) and we'll try our best to help
|
||||
Having troubles migrating from v3 to v4? Reach out to us on [discord](https://discord.gg/HDScTs3ptP) or [GitHub](https://github.com/sourcebot-dev/sourcebot/issues/new/choose) and we'll try our best to help
|
||||
|
|
@ -647,6 +647,115 @@
|
|||
"purpose",
|
||||
"audience"
|
||||
]
|
||||
},
|
||||
"AuthentikIdentityProviderConfig": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "authentik"
|
||||
},
|
||||
"purpose": {
|
||||
"const": "sso"
|
||||
},
|
||||
"clientId": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"googleCloudSecret": {
|
||||
"type": "string",
|
||||
"description": "The resource name of a Google Cloud secret. Must be in the format `projects/<project-id>/secrets/<secret-name>/versions/<version-id>`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"googleCloudSecret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"clientSecret": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"googleCloudSecret": {
|
||||
"type": "string",
|
||||
"description": "The resource name of a Google Cloud secret. Must be in the format `projects/<project-id>/secrets/<secret-name>/versions/<version-id>`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"googleCloudSecret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"issuer": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"googleCloudSecret": {
|
||||
"type": "string",
|
||||
"description": "The resource name of a Google Cloud secret. Must be in the format `projects/<project-id>/secrets/<secret-name>/versions/<version-id>`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"googleCloudSecret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"purpose",
|
||||
"clientId",
|
||||
"clientSecret",
|
||||
"issuer"
|
||||
]
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
|
|
@ -1293,6 +1402,115 @@
|
|||
"purpose",
|
||||
"audience"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "authentik"
|
||||
},
|
||||
"purpose": {
|
||||
"const": "sso"
|
||||
},
|
||||
"clientId": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"googleCloudSecret": {
|
||||
"type": "string",
|
||||
"description": "The resource name of a Google Cloud secret. Must be in the format `projects/<project-id>/secrets/<secret-name>/versions/<version-id>`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"googleCloudSecret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"clientSecret": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"googleCloudSecret": {
|
||||
"type": "string",
|
||||
"description": "The resource name of a Google Cloud secret. Must be in the format `projects/<project-id>/secrets/<secret-name>/versions/<version-id>`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"googleCloudSecret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"issuer": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"googleCloudSecret": {
|
||||
"type": "string",
|
||||
"description": "The resource name of a Google Cloud secret. Must be in the format `projects/<project-id>/secrets/<secret-name>/versions/<version-id>`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"googleCloudSecret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"purpose",
|
||||
"clientId",
|
||||
"clientSecret",
|
||||
"issuer"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5163,6 +5163,115 @@
|
|||
"purpose",
|
||||
"audience"
|
||||
]
|
||||
},
|
||||
"AuthentikIdentityProviderConfig": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "authentik"
|
||||
},
|
||||
"purpose": {
|
||||
"const": "sso"
|
||||
},
|
||||
"clientId": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"googleCloudSecret": {
|
||||
"type": "string",
|
||||
"description": "The resource name of a Google Cloud secret. Must be in the format `projects/<project-id>/secrets/<secret-name>/versions/<version-id>`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"googleCloudSecret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"clientSecret": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"googleCloudSecret": {
|
||||
"type": "string",
|
||||
"description": "The resource name of a Google Cloud secret. Must be in the format `projects/<project-id>/secrets/<secret-name>/versions/<version-id>`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"googleCloudSecret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"issuer": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"googleCloudSecret": {
|
||||
"type": "string",
|
||||
"description": "The resource name of a Google Cloud secret. Must be in the format `projects/<project-id>/secrets/<secret-name>/versions/<version-id>`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"googleCloudSecret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"purpose",
|
||||
"clientId",
|
||||
"clientSecret",
|
||||
"issuer"
|
||||
]
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
|
|
@ -5809,6 +5918,115 @@
|
|||
"purpose",
|
||||
"audience"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "authentik"
|
||||
},
|
||||
"purpose": {
|
||||
"const": "sso"
|
||||
},
|
||||
"clientId": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"googleCloudSecret": {
|
||||
"type": "string",
|
||||
"description": "The resource name of a Google Cloud secret. Must be in the format `projects/<project-id>/secrets/<secret-name>/versions/<version-id>`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"googleCloudSecret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"clientSecret": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"googleCloudSecret": {
|
||||
"type": "string",
|
||||
"description": "The resource name of a Google Cloud secret. Must be in the format `projects/<project-id>/secrets/<secret-name>/versions/<version-id>`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"googleCloudSecret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"issuer": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"googleCloudSecret": {
|
||||
"type": "string",
|
||||
"description": "The resource name of a Google Cloud secret. Must be in the format `projects/<project-id>/secrets/<secret-name>/versions/<version-id>`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"googleCloudSecret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"purpose",
|
||||
"clientId",
|
||||
"clientSecret",
|
||||
"issuer"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,12 +66,6 @@ fi
|
|||
|
||||
echo -e "\e[34m[Info] Sourcebot version: $NEXT_PUBLIC_SOURCEBOT_VERSION\e[0m"
|
||||
|
||||
# If we don't have a PostHog key, then we need to disable telemetry.
|
||||
if [ -z "$NEXT_PUBLIC_POSTHOG_PAPIK" ]; then
|
||||
echo -e "\e[33m[Warning] NEXT_PUBLIC_POSTHOG_PAPIK was not set. Setting SOURCEBOT_TELEMETRY_DISABLED.\e[0m"
|
||||
export SOURCEBOT_TELEMETRY_DISABLED=true
|
||||
fi
|
||||
|
||||
if [ -n "$SOURCEBOT_TELEMETRY_DISABLED" ]; then
|
||||
# Validate that SOURCEBOT_TELEMETRY_DISABLED is either "true" or "false"
|
||||
if [ "$SOURCEBOT_TELEMETRY_DISABLED" != "true" ] && [ "$SOURCEBOT_TELEMETRY_DISABLED" != "false" ]; then
|
||||
|
|
@ -159,7 +153,7 @@ if [ ! -f "$FIRST_RUN_FILE" ]; then
|
|||
# (if telemetry is enabled)
|
||||
if [ "$SOURCEBOT_TELEMETRY_DISABLED" = "false" ]; then
|
||||
if ! ( curl -L --output /dev/null --silent --fail --header "Content-Type: application/json" -d '{
|
||||
"api_key": "'"$NEXT_PUBLIC_POSTHOG_PAPIK"'",
|
||||
"api_key": "'"$POSTHOG_PAPIK"'",
|
||||
"event": "install",
|
||||
"distinct_id": "'"$SOURCEBOT_INSTALL_ID"'",
|
||||
"properties": {
|
||||
|
|
@ -179,7 +173,7 @@ else
|
|||
|
||||
if [ "$SOURCEBOT_TELEMETRY_DISABLED" = "false" ]; then
|
||||
if ! ( curl -L --output /dev/null --silent --fail --header "Content-Type: application/json" -d '{
|
||||
"api_key": "'"$NEXT_PUBLIC_POSTHOG_PAPIK"'",
|
||||
"api_key": "'"$POSTHOG_PAPIK"'",
|
||||
"event": "upgrade",
|
||||
"distinct_id": "'"$SOURCEBOT_INSTALL_ID"'",
|
||||
"properties": {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
"dev:prisma:studio": "yarn with-env yarn workspace @sourcebot/db prisma:studio",
|
||||
"dev:prisma:migrate:reset": "yarn with-env yarn workspace @sourcebot/db prisma:migrate:reset",
|
||||
"dev:prisma:db:push": "yarn with-env yarn workspace @sourcebot/db prisma:db:push",
|
||||
"build:deps": "yarn workspaces foreach --recursive --topological --from '{@sourcebot/schemas,@sourcebot/db,@sourcebot/shared}' run build"
|
||||
"build:deps": "yarn workspaces foreach --recursive --topological --from '{@sourcebot/schemas,@sourcebot/db,@sourcebot/shared,@sourcebot/query-language}' run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^9.2.1",
|
||||
|
|
@ -27,6 +27,7 @@
|
|||
},
|
||||
"packageManager": "yarn@4.7.0",
|
||||
"resolutions": {
|
||||
"prettier": "3.5.3"
|
||||
"prettier": "3.5.3",
|
||||
"@lezer/common": "1.3.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import type { ClientOptions, ClientPathsWithMethod } from "openapi-fetch";
|
|||
import { createLogger } from "@sourcebot/shared";
|
||||
import { measure, fetchWithRetry } from "./utils.js";
|
||||
import * as Sentry from "@sentry/node";
|
||||
import micromatch from "micromatch";
|
||||
import {
|
||||
SchemaRepository as CloudRepository,
|
||||
} from "@coderabbitai/bitbucket/cloud/openapi";
|
||||
|
|
@ -346,10 +347,15 @@ async function cloudGetRepos(client: BitbucketClient, repoList: string[]): Promi
|
|||
|
||||
function cloudShouldExcludeRepo(repo: BitbucketRepository, config: BitbucketConnectionConfig): boolean {
|
||||
const cloudRepo = repo as CloudRepository;
|
||||
let reason = '';
|
||||
const repoName = cloudRepo.full_name!;
|
||||
|
||||
const shouldExclude = (() => {
|
||||
if (config.exclude?.repos && config.exclude.repos.includes(cloudRepo.full_name!)) {
|
||||
return true;
|
||||
if (config.exclude?.repos) {
|
||||
if (micromatch.isMatch(repoName, config.exclude.repos)) {
|
||||
reason = `\`exclude.repos\` contains ${repoName}`;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!!config.exclude?.archived) {
|
||||
|
|
@ -357,12 +363,15 @@ function cloudShouldExcludeRepo(repo: BitbucketRepository, config: BitbucketConn
|
|||
}
|
||||
|
||||
if (!!config.exclude?.forks && cloudRepo.parent !== undefined) {
|
||||
reason = `\`exclude.forks\` is true`;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})();
|
||||
|
||||
if (shouldExclude) {
|
||||
logger.debug(`Excluding repo ${cloudRepo.full_name} because it matches the exclude pattern`);
|
||||
logger.debug(`Excluding repo ${repoName}. Reason: ${reason}`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
@ -548,23 +557,32 @@ function serverShouldExcludeRepo(repo: BitbucketRepository, config: BitbucketCon
|
|||
|
||||
const projectName = serverRepo.project!.key;
|
||||
const repoSlug = serverRepo.slug!;
|
||||
const repoName = `${projectName}/${repoSlug}`;
|
||||
let reason = '';
|
||||
|
||||
const shouldExclude = (() => {
|
||||
if (config.exclude?.repos && config.exclude.repos.includes(`${projectName}/${repoSlug}`)) {
|
||||
return true;
|
||||
if (config.exclude?.repos) {
|
||||
if (micromatch.isMatch(repoName, config.exclude.repos)) {
|
||||
reason = `\`exclude.repos\` contains ${repoName}`;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!!config.exclude?.archived && serverRepo.archived) {
|
||||
reason = `\`exclude.archived\` is true`;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!!config.exclude?.forks && serverRepo.origin !== undefined) {
|
||||
reason = `\`exclude.forks\` is true`;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
})();
|
||||
|
||||
if (shouldExclude) {
|
||||
logger.debug(`Excluding repo ${projectName}/${repoSlug} because it matches the exclude pattern`);
|
||||
logger.debug(`Excluding repo ${repoName}. Reason: ${reason}`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ export class AccountPermissionSyncer {
|
|||
if (this.interval) {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
await this.worker.close();
|
||||
await this.worker.close(/* force = */ true);
|
||||
await this.queue.close();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,19 +55,27 @@ export class RepoPermissionSyncer {
|
|||
const repos = await this.db.repo.findMany({
|
||||
// Repos need their permissions to be synced against the code host when...
|
||||
where: {
|
||||
// They belong to a code host that supports permissions syncing
|
||||
AND: [
|
||||
// They are not public. Public repositories are always visible to all users, therefore we don't
|
||||
// need to explicitly perform permission syncing for them.
|
||||
// @see: packages/web/src/prisma.ts
|
||||
{
|
||||
isPublic: false
|
||||
},
|
||||
// They belong to a code host that supports permissions syncing
|
||||
{
|
||||
external_codeHostType: {
|
||||
in: PERMISSION_SYNC_SUPPORTED_CODE_HOST_TYPES,
|
||||
}
|
||||
},
|
||||
// They have not been synced within the threshold date.
|
||||
{
|
||||
OR: [
|
||||
{ permissionSyncedAt: null },
|
||||
{ permissionSyncedAt: { lt: thresholdDate } },
|
||||
],
|
||||
},
|
||||
// There aren't any active or recently failed jobs.
|
||||
{
|
||||
NOT: {
|
||||
permissionSyncJobs: {
|
||||
|
|
@ -106,7 +114,7 @@ export class RepoPermissionSyncer {
|
|||
if (this.interval) {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
await this.worker.close();
|
||||
await this.worker.close(/* force = */ true);
|
||||
await this.queue.close();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -94,7 +94,6 @@ const listenToShutdownSignals = () => {
|
|||
const cleanup = async (signal: string) => {
|
||||
try {
|
||||
if (receivedSignal) {
|
||||
logger.debug(`Recieved repeat signal ${signal}, ignoring.`);
|
||||
return;
|
||||
}
|
||||
receivedSignal = true;
|
||||
|
|
@ -112,19 +111,20 @@ const listenToShutdownSignals = () => {
|
|||
await api.dispose();
|
||||
await shutdownPosthog();
|
||||
|
||||
|
||||
logger.info('All workers shut down gracefully');
|
||||
signals.forEach(sig => process.removeListener(sig, cleanup));
|
||||
return 0;
|
||||
} catch (error) {
|
||||
Sentry.captureException(error);
|
||||
logger.error('Error shutting down worker:', error);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
signals.forEach(signal => {
|
||||
process.on(signal, (err) => {
|
||||
cleanup(err).finally(() => {
|
||||
process.kill(process.pid, signal);
|
||||
cleanup(err).then(code => {
|
||||
process.exit(code);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -132,14 +132,14 @@ const listenToShutdownSignals = () => {
|
|||
// Register handlers for uncaught exceptions and unhandled rejections
|
||||
process.on('uncaughtException', (err) => {
|
||||
logger.error(`Uncaught exception: ${err.message}`);
|
||||
cleanup('uncaughtException').finally(() => {
|
||||
cleanup('uncaughtException').then(() => {
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
logger.error(`Unhandled rejection at: ${promise}, reason: ${reason}`);
|
||||
cleanup('unhandledRejection').finally(() => {
|
||||
cleanup('unhandledRejection').then(() => {
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import { PosthogEvent, PosthogEventMap } from './posthogEvents.js';
|
|||
|
||||
let posthog: PostHog | undefined = undefined;
|
||||
|
||||
if (clientEnv.NEXT_PUBLIC_POSTHOG_PAPIK) {
|
||||
if (env.POSTHOG_PAPIK) {
|
||||
posthog = new PostHog(
|
||||
clientEnv.NEXT_PUBLIC_POSTHOG_PAPIK,
|
||||
env.POSTHOG_PAPIK,
|
||||
{
|
||||
host: "https://us.i.posthog.com",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Repo } from "@sourcebot/db";
|
||||
import { createLogger } from "@sourcebot/shared";
|
||||
import { createLogger, env } from "@sourcebot/shared";
|
||||
import { exec } from "child_process";
|
||||
import { INDEX_CACHE_DIR } from "./constants.js";
|
||||
import { Settings } from "./types.js";
|
||||
|
|
@ -11,6 +11,8 @@ export const indexGitRepository = async (repo: Repo, settings: Settings, revisio
|
|||
const { path: repoPath } = getRepoPath(repo);
|
||||
const shardPrefix = getShardPrefix(repo.orgId, repo.id);
|
||||
|
||||
const largeFileGlobPatterns = env.ALWAYS_INDEX_FILE_PATTERNS?.split(',').map(pattern => pattern.trim()) ?? [];
|
||||
|
||||
const command = [
|
||||
'zoekt-git-index',
|
||||
'-allow_missing_branches',
|
||||
|
|
@ -21,6 +23,7 @@ export const indexGitRepository = async (repo: Repo, settings: Settings, revisio
|
|||
`-tenant_id ${repo.orgId}`,
|
||||
`-repo_id ${repo.id}`,
|
||||
`-shard_prefix ${shardPrefix}`,
|
||||
...largeFileGlobPatterns.map((pattern) => `-large_file ${pattern}`),
|
||||
repoPath
|
||||
].join(' ');
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
-- First, remove the NOT NULL constraint on the createdById column.
|
||||
ALTER TABLE "Chat" ALTER COLUMN "createdById" DROP NOT NULL;
|
||||
|
||||
-- Then, set all chats created by the guest user (id: 1) to have a NULL createdById.
|
||||
UPDATE "Chat" SET "createdById" = NULL WHERE "createdById" = '1';
|
||||
|
|
@ -437,8 +437,8 @@ model Chat {
|
|||
|
||||
name String?
|
||||
|
||||
createdBy User @relation(fields: [createdById], references: [id], onDelete: Cascade)
|
||||
createdById String
|
||||
createdBy User? @relation(fields: [createdById], references: [id], onDelete: Cascade)
|
||||
createdById String?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
|
|
|||
|
|
@ -1 +1,3 @@
|
|||
import type { User, Account } from ".prisma/client";
|
||||
export type UserWithAccounts = User & { accounts: Account[] };
|
||||
export * from ".prisma/client";
|
||||
|
|
@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.0.11] - 2025-12-03
|
||||
|
||||
### Changed
|
||||
- Updated API client to match the latest Sourcebot release. [#652](https://github.com/sourcebot-dev/sourcebot/pull/652)
|
||||
|
||||
## [1.0.10] - 2025-11-24
|
||||
|
||||
### Changed
|
||||
- Updated API client to match the latest Sourcebot release. [#555](https://github.com/sourcebot-dev/sourcebot/pull/555)
|
||||
|
||||
## [1.0.9] - 2025-11-17
|
||||
|
||||
### Added
|
||||
- Added pagination and filtering to `list_repos` tool to handle large repository lists efficiently and prevent oversized responses that waste token context. [#614](https://github.com/sourcebot-dev/sourcebot/pull/614)
|
||||
|
||||
## [1.0.8] - 2025-11-10
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
|
|
@ -182,7 +182,18 @@ Fetches code that matches the provided regex pattern in `query`.
|
|||
|
||||
### list_repos
|
||||
|
||||
Lists all repositories indexed by Sourcebot.
|
||||
Lists repositories indexed by Sourcebot with optional filtering and pagination.
|
||||
|
||||
<details>
|
||||
<summary>Parameters</summary>
|
||||
|
||||
| Name | Required | Description |
|
||||
|:-------------|:---------|:--------------------------------------------------------------------|
|
||||
| `query` | no | Filter repositories by name (case-insensitive). |
|
||||
| `pageNumber` | no | Page number (1-indexed, default: 1). |
|
||||
| `limit` | no | Number of repositories per page (default: 50). |
|
||||
|
||||
</details>
|
||||
|
||||
### get_file_source
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@sourcebot/mcp",
|
||||
"version": "1.0.8",
|
||||
"version": "1.0.11",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ export const search = async (request: SearchRequest): Promise<SearchResponse | S
|
|||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Org-Domain': '~',
|
||||
...(env.SOURCEBOT_API_KEY ? { 'X-Sourcebot-Api-Key': env.SOURCEBOT_API_KEY } : {})
|
||||
},
|
||||
body: JSON.stringify(request)
|
||||
|
|
@ -26,7 +25,6 @@ export const listRepos = async (): Promise<ListRepositoriesResponse | ServiceErr
|
|||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Org-Domain': '~',
|
||||
...(env.SOURCEBOT_API_KEY ? { 'X-Sourcebot-Api-Key': env.SOURCEBOT_API_KEY } : {})
|
||||
},
|
||||
}).then(response => response.json());
|
||||
|
|
@ -43,7 +41,6 @@ export const getFileSource = async (request: FileSourceRequest): Promise<FileSou
|
|||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Org-Domain': '~',
|
||||
...(env.SOURCEBOT_API_KEY ? { 'X-Sourcebot-Api-Key': env.SOURCEBOT_API_KEY } : {})
|
||||
},
|
||||
body: JSON.stringify(request)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import escapeStringRegexp from 'escape-string-regexp';
|
|||
import { z } from 'zod';
|
||||
import { listRepos, search, getFileSource } from './client.js';
|
||||
import { env, numberSchema } from './env.js';
|
||||
import { listReposRequestSchema } from './schemas.js';
|
||||
import { TextContent } from './types.js';
|
||||
import { isServiceError } from './utils.js';
|
||||
|
||||
|
|
@ -69,16 +70,13 @@ server.tool(
|
|||
query += ` ( lang:${languages.join(' or lang:')} )`;
|
||||
}
|
||||
|
||||
if (caseSensitive) {
|
||||
query += ` case:yes`;
|
||||
} else {
|
||||
query += ` case:no`;
|
||||
}
|
||||
|
||||
const response = await search({
|
||||
query,
|
||||
matches: env.DEFAULT_MATCHES,
|
||||
contextLines: env.DEFAULT_CONTEXT_LINES,
|
||||
isRegexEnabled: true,
|
||||
isCaseSensitivityEnabled: caseSensitive,
|
||||
source: 'mcp'
|
||||
});
|
||||
|
||||
if (isServiceError(response)) {
|
||||
|
|
@ -165,8 +163,13 @@ server.tool(
|
|||
|
||||
server.tool(
|
||||
"list_repos",
|
||||
"Lists all repositories in the organization. If you receive an error that indicates that you're not authenticated, please inform the user to set the SOURCEBOT_API_KEY environment variable.",
|
||||
async () => {
|
||||
"Lists repositories in the organization with optional filtering and pagination. If you receive an error that indicates that you're not authenticated, please inform the user to set the SOURCEBOT_API_KEY environment variable.",
|
||||
listReposRequestSchema.shape,
|
||||
async ({ query, pageNumber = 1, limit = 50 }: {
|
||||
query?: string;
|
||||
pageNumber?: number;
|
||||
limit?: number;
|
||||
}) => {
|
||||
const response = await listRepos();
|
||||
if (isServiceError(response)) {
|
||||
return {
|
||||
|
|
@ -177,13 +180,45 @@ server.tool(
|
|||
};
|
||||
}
|
||||
|
||||
const content: TextContent[] = response.map(repo => {
|
||||
// Apply query filter if provided
|
||||
let filtered = response;
|
||||
if (query) {
|
||||
const lowerQuery = query.toLowerCase();
|
||||
filtered = response.filter(repo =>
|
||||
repo.repoName.toLowerCase().includes(lowerQuery) ||
|
||||
repo.repoDisplayName?.toLowerCase().includes(lowerQuery)
|
||||
);
|
||||
}
|
||||
|
||||
// Sort alphabetically for consistent pagination
|
||||
filtered.sort((a, b) => a.repoName.localeCompare(b.repoName));
|
||||
|
||||
// Apply pagination
|
||||
const startIndex = (pageNumber - 1) * limit;
|
||||
const endIndex = startIndex + limit;
|
||||
const paginated = filtered.slice(startIndex, endIndex);
|
||||
|
||||
// Format output
|
||||
const content: TextContent[] = paginated.map(repo => {
|
||||
return {
|
||||
type: "text",
|
||||
text: `id: ${repo.repoName}\nurl: ${repo.webUrl}`,
|
||||
}
|
||||
});
|
||||
|
||||
// Add pagination info
|
||||
if (content.length === 0 && filtered.length > 0) {
|
||||
content.push({
|
||||
type: "text",
|
||||
text: `No results on page ${pageNumber}. Total matching repositories: ${filtered.length}`,
|
||||
});
|
||||
} else if (filtered.length > endIndex) {
|
||||
content.push({
|
||||
type: "text",
|
||||
text: `Showing ${paginated.length} repositories (page ${pageNumber}). Total matching: ${filtered.length}. Use pageNumber ${pageNumber + 1} to see more.`,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
content,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -21,15 +21,18 @@ export const symbolSchema = z.object({
|
|||
kind: z.string(),
|
||||
});
|
||||
|
||||
export const searchOptionsSchema = z.object({
|
||||
matches: z.number(), // The number of matches to return.
|
||||
contextLines: z.number().optional(), // The number of context lines to return.
|
||||
whole: z.boolean().optional(), // Whether to return the whole file as part of the response.
|
||||
isRegexEnabled: z.boolean().optional(), // Whether to enable regular expression search.
|
||||
isCaseSensitivityEnabled: z.boolean().optional(), // Whether to enable case sensitivity.
|
||||
});
|
||||
|
||||
export const searchRequestSchema = z.object({
|
||||
// The zoekt query to execute.
|
||||
query: z.string(),
|
||||
// The number of matches to return.
|
||||
matches: z.number(),
|
||||
// The number of context lines to return.
|
||||
contextLines: z.number().optional(),
|
||||
// Whether to return the whole file as part of the response.
|
||||
whole: z.boolean().optional(),
|
||||
query: z.string(), // The zoekt query to execute.
|
||||
source: z.string().optional(), // The source of the search request.
|
||||
...searchOptionsSchema.shape,
|
||||
});
|
||||
|
||||
export const repositoryInfoSchema = z.object({
|
||||
|
|
@ -109,7 +112,7 @@ export const searchStatsSchema = z.object({
|
|||
regexpsConsidered: z.number(),
|
||||
|
||||
// FlushReason explains why results were flushed.
|
||||
flushReason: z.number(),
|
||||
flushReason: z.string(),
|
||||
});
|
||||
|
||||
export const searchResponseSchema = z.object({
|
||||
|
|
@ -139,7 +142,6 @@ export const searchResponseSchema = z.object({
|
|||
content: z.string().optional(),
|
||||
})),
|
||||
repositoryInfo: z.array(repositoryInfoSchema),
|
||||
isBranchFilteringEnabled: z.boolean(),
|
||||
isSearchExhaustive: z.boolean(),
|
||||
});
|
||||
|
||||
|
|
@ -156,6 +158,25 @@ export const repositoryQuerySchema = z.object({
|
|||
|
||||
export const listRepositoriesResponseSchema = repositoryQuerySchema.array();
|
||||
|
||||
export const listReposRequestSchema = z.object({
|
||||
query: z
|
||||
.string()
|
||||
.describe("Filter repositories by name or displayName (case-insensitive)")
|
||||
.optional(),
|
||||
pageNumber: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.describe("Page number (1-indexed, default: 1)")
|
||||
.default(1),
|
||||
limit: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.describe("Number of repositories per page (default: 50)")
|
||||
.default(50),
|
||||
});
|
||||
|
||||
export const fileSourceRequestSchema = z.object({
|
||||
fileName: z.string(),
|
||||
repository: z.string(),
|
||||
|
|
|
|||
2
packages/queryLanguage/.gitignore
vendored
Normal file
2
packages/queryLanguage/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/node_modules/
|
||||
/dist
|
||||
20
packages/queryLanguage/package.json
Normal file
20
packages/queryLanguage/package.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "@sourcebot/query-language",
|
||||
"private": true,
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "lezer-generator src/query.grammar -o src/parser --typeScript --names && tsc",
|
||||
"test": "vitest",
|
||||
"postinstall": "yarn build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lezer/generator": "^1.8.0",
|
||||
"tsx": "^4.19.1",
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^2.1.9"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.3.0",
|
||||
"@lezer/lr": "^1.4.3"
|
||||
}
|
||||
}
|
||||
7
packages/queryLanguage/src/index.ts
Normal file
7
packages/queryLanguage/src/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { parser } from "./parser";
|
||||
|
||||
type Tree = ReturnType<typeof parser.parse>;
|
||||
type SyntaxNode = Tree['topNode'];
|
||||
export type { Tree, SyntaxNode };
|
||||
export * from "./parser";
|
||||
export * from "./parser.terms";
|
||||
22
packages/queryLanguage/src/parser.terms.ts
Normal file
22
packages/queryLanguage/src/parser.terms.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
export const
|
||||
negate = 23,
|
||||
Program = 1,
|
||||
OrExpr = 2,
|
||||
AndExpr = 3,
|
||||
NegateExpr = 4,
|
||||
PrefixExpr = 5,
|
||||
ArchivedExpr = 6,
|
||||
RevisionExpr = 7,
|
||||
ContentExpr = 8,
|
||||
ContextExpr = 9,
|
||||
FileExpr = 10,
|
||||
ForkExpr = 11,
|
||||
VisibilityExpr = 12,
|
||||
RepoExpr = 13,
|
||||
LangExpr = 14,
|
||||
SymExpr = 15,
|
||||
RepoSetExpr = 16,
|
||||
ParenExpr = 17,
|
||||
QuotedTerm = 18,
|
||||
Term = 19
|
||||
18
packages/queryLanguage/src/parser.ts
Normal file
18
packages/queryLanguage/src/parser.ts
Normal file
File diff suppressed because one or more lines are too long
102
packages/queryLanguage/src/query.grammar
Normal file
102
packages/queryLanguage/src/query.grammar
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
@external tokens negateToken from "./tokens" { negate }
|
||||
|
||||
@top Program { query }
|
||||
|
||||
@precedence {
|
||||
negate,
|
||||
and,
|
||||
or @left
|
||||
}
|
||||
|
||||
query {
|
||||
OrExpr |
|
||||
AndExpr |
|
||||
expr
|
||||
}
|
||||
|
||||
OrExpr { andExpr (or andExpr)+ }
|
||||
|
||||
AndExpr { expr expr+ }
|
||||
|
||||
andExpr { AndExpr | expr }
|
||||
|
||||
expr {
|
||||
NegateExpr |
|
||||
ParenExpr |
|
||||
PrefixExpr |
|
||||
QuotedTerm |
|
||||
Term
|
||||
}
|
||||
|
||||
NegateExpr { !negate negate (PrefixExpr | ParenExpr) }
|
||||
|
||||
ParenExpr { "(" query? ")" }
|
||||
|
||||
PrefixExpr {
|
||||
ArchivedExpr |
|
||||
RevisionExpr |
|
||||
ContentExpr |
|
||||
ContextExpr |
|
||||
FileExpr |
|
||||
ForkExpr |
|
||||
VisibilityExpr |
|
||||
RepoExpr |
|
||||
LangExpr |
|
||||
SymExpr |
|
||||
RepoSetExpr
|
||||
}
|
||||
|
||||
RevisionExpr { revisionKw value }
|
||||
ContentExpr { contentKw value }
|
||||
ContextExpr { contextKw value }
|
||||
FileExpr { fileKw value }
|
||||
RepoExpr { repoKw value }
|
||||
LangExpr { langKw value }
|
||||
SymExpr { symKw value }
|
||||
RepoSetExpr { reposetKw value }
|
||||
|
||||
// Modifiers
|
||||
ArchivedExpr { archivedKw archivedValue }
|
||||
ForkExpr { forkKw forkValue }
|
||||
VisibilityExpr { visibilityKw visibilityValue }
|
||||
|
||||
archivedValue { "yes" | "no" | "only" }
|
||||
forkValue { "yes" | "no" | "only" }
|
||||
visibilityValue { "public" | "private" | "any" }
|
||||
|
||||
QuotedTerm { quotedString }
|
||||
Term { word }
|
||||
|
||||
value { quotedString | word }
|
||||
|
||||
@skip { space }
|
||||
|
||||
@tokens {
|
||||
archivedKw { "archived:" }
|
||||
revisionKw { "rev:" }
|
||||
contentKw { "content:" | "c:" }
|
||||
contextKw { "context:" }
|
||||
fileKw { "file:" | "f:" }
|
||||
forkKw { "fork:" }
|
||||
visibilityKw { "visibility:" }
|
||||
repoKw { "repo:" | "r:" }
|
||||
langKw { "lang:" }
|
||||
symKw { "sym:" }
|
||||
reposetKw { "reposet:" }
|
||||
|
||||
or { "or" ![a-zA-Z0-9_] }
|
||||
|
||||
quotedString { '"' (!["\\\n] | "\\" _)* '"' }
|
||||
|
||||
word { (![ \t\n()]) (![ \t\n():] | ":" | "-")* }
|
||||
|
||||
space { $[ \t\n]+ }
|
||||
|
||||
@precedence {
|
||||
quotedString,
|
||||
archivedKw, revisionKw, contentKw, contextKw, fileKw,
|
||||
forkKw, visibilityKw, repoKw, langKw,
|
||||
symKw, reposetKw, or,
|
||||
word
|
||||
}
|
||||
}
|
||||
59
packages/queryLanguage/src/tokens.ts
Normal file
59
packages/queryLanguage/src/tokens.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { ExternalTokenizer } from "@lezer/lr";
|
||||
import { negate } from "./parser.terms";
|
||||
|
||||
// External tokenizer for negation
|
||||
// Only tokenizes `-` as negate when followed by a prefix keyword or `(`
|
||||
export const negateToken = new ExternalTokenizer((input) => {
|
||||
if (input.next !== 45 /* '-' */) return; // Not a dash
|
||||
|
||||
const startPos = input.pos;
|
||||
|
||||
// Look ahead to see what follows the dash
|
||||
input.advance();
|
||||
|
||||
// Skip whitespace
|
||||
let ch = input.next;
|
||||
while (ch === 32 || ch === 9 || ch === 10) {
|
||||
input.advance();
|
||||
ch = input.next;
|
||||
}
|
||||
|
||||
// Check if followed by opening paren
|
||||
if (ch === 40 /* '(' */) {
|
||||
input.acceptToken(negate, -input.pos + startPos + 1); // Accept just the dash
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if followed by a prefix keyword (by checking for keyword followed by colon)
|
||||
// Look ahead until we hit a delimiter or colon
|
||||
const checkPos = input.pos;
|
||||
let foundColon = false;
|
||||
|
||||
// Look ahead until we hit a delimiter or colon
|
||||
while (ch >= 0) {
|
||||
if (ch === 58 /* ':' */) {
|
||||
foundColon = true;
|
||||
break;
|
||||
}
|
||||
// Hit a delimiter (whitespace, paren, or quote) - not a prefix keyword
|
||||
if (ch === 32 || ch === 9 || ch === 10 || ch === 40 || ch === 41 || ch === 34) {
|
||||
break;
|
||||
}
|
||||
input.advance();
|
||||
ch = input.next;
|
||||
}
|
||||
|
||||
// Reset position
|
||||
while (input.pos > checkPos) {
|
||||
input.advance(-1);
|
||||
}
|
||||
|
||||
if (foundColon) {
|
||||
// It's a prefix keyword, accept as negate
|
||||
input.acceptToken(negate, -input.pos + startPos + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, don't tokenize as negate (let word handle it)
|
||||
});
|
||||
|
||||
72
packages/queryLanguage/test/basic.txt
Normal file
72
packages/queryLanguage/test/basic.txt
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# Single term
|
||||
|
||||
hello
|
||||
|
||||
==>
|
||||
|
||||
Program(Term)
|
||||
|
||||
# Multiple terms
|
||||
|
||||
hello world
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(Term,Term))
|
||||
|
||||
# Multiple terms with various characters
|
||||
|
||||
console.log error_handler
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(Term,Term))
|
||||
|
||||
# Term with underscores
|
||||
|
||||
my_variable_name
|
||||
|
||||
==>
|
||||
|
||||
Program(Term)
|
||||
|
||||
# Term with dots
|
||||
|
||||
com.example.package
|
||||
|
||||
==>
|
||||
|
||||
Program(Term)
|
||||
|
||||
# Term with numbers
|
||||
|
||||
func123 test_456
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(Term,Term))
|
||||
|
||||
# Regex pattern
|
||||
|
||||
[a-z]+
|
||||
|
||||
==>
|
||||
|
||||
Program(Term)
|
||||
|
||||
# Wildcard pattern
|
||||
|
||||
test.*
|
||||
|
||||
==>
|
||||
|
||||
Program(Term)
|
||||
|
||||
# Multiple regex patterns
|
||||
|
||||
\w+ [0-9]+ \s*
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(Term,Term,Term))
|
||||
|
||||
21
packages/queryLanguage/test/grammar.test.ts
Normal file
21
packages/queryLanguage/test/grammar.test.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { parser } from "../src/parser";
|
||||
import { fileTests } from "@lezer/generator/dist/test";
|
||||
import { describe, it } from "vitest";
|
||||
import { fileURLToPath } from "url"
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
const caseDir = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
for (const file of fs.readdirSync(caseDir)) {
|
||||
if (!/\.txt$/.test(file)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let name = /^[^\.]*/.exec(file)?.[0];
|
||||
describe(name ?? "unknown", () => {
|
||||
for (const { name, run } of fileTests(fs.readFileSync(path.join(caseDir, file), "utf8"), file)) {
|
||||
it(name, () => run(parser));
|
||||
}
|
||||
});
|
||||
}
|
||||
120
packages/queryLanguage/test/grouping.txt
Normal file
120
packages/queryLanguage/test/grouping.txt
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
# Empty parentheses
|
||||
|
||||
()
|
||||
|
||||
==>
|
||||
|
||||
Program(ParenExpr)
|
||||
|
||||
# Simple grouping
|
||||
|
||||
(test)
|
||||
|
||||
==>
|
||||
|
||||
Program(ParenExpr(Term))
|
||||
|
||||
# Multiple terms in group
|
||||
|
||||
(hello world)
|
||||
|
||||
==>
|
||||
|
||||
Program(ParenExpr(AndExpr(Term,Term)))
|
||||
|
||||
# Nested parentheses
|
||||
|
||||
((test))
|
||||
|
||||
==>
|
||||
|
||||
Program(ParenExpr(ParenExpr(Term)))
|
||||
|
||||
# Multiple groups
|
||||
|
||||
(first) (second)
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(ParenExpr(Term),ParenExpr(Term)))
|
||||
|
||||
# Group with multiple terms
|
||||
|
||||
(one two three)
|
||||
|
||||
==>
|
||||
|
||||
Program(ParenExpr(AndExpr(Term,Term,Term)))
|
||||
|
||||
# Mixed grouped and ungrouped
|
||||
|
||||
test (grouped) another
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(Term,ParenExpr(Term),Term))
|
||||
|
||||
# Deeply nested
|
||||
|
||||
(((nested)))
|
||||
|
||||
==>
|
||||
|
||||
Program(ParenExpr(ParenExpr(ParenExpr(Term))))
|
||||
|
||||
# Multiple nested groups
|
||||
|
||||
((a b) (c d))
|
||||
|
||||
==>
|
||||
|
||||
Program(ParenExpr(AndExpr(ParenExpr(AndExpr(Term,Term)),ParenExpr(AndExpr(Term,Term)))))
|
||||
|
||||
# Group at start
|
||||
|
||||
(start) middle end
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(ParenExpr(Term),Term,Term))
|
||||
|
||||
# Group at end
|
||||
|
||||
start middle (end)
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(Term,Term,ParenExpr(Term)))
|
||||
|
||||
# Complex grouping pattern
|
||||
|
||||
(a (b c) d)
|
||||
|
||||
==>
|
||||
|
||||
Program(ParenExpr(AndExpr(Term,ParenExpr(AndExpr(Term,Term)),Term)))
|
||||
|
||||
# Sequential groups
|
||||
|
||||
(a)(b)(c)
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(ParenExpr(Term),ParenExpr(Term),ParenExpr(Term)))
|
||||
|
||||
# Group with regex
|
||||
|
||||
([a-z]+)
|
||||
|
||||
==>
|
||||
|
||||
Program(ParenExpr(Term))
|
||||
|
||||
# Group with dots
|
||||
|
||||
(com.example.test)
|
||||
|
||||
==>
|
||||
|
||||
Program(ParenExpr(Term))
|
||||
|
||||
255
packages/queryLanguage/test/negation.txt
Normal file
255
packages/queryLanguage/test/negation.txt
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
# Literal dash term
|
||||
|
||||
-test
|
||||
|
||||
==>
|
||||
|
||||
Program(Term)
|
||||
|
||||
# Quoted dash term
|
||||
|
||||
"-excluded"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Dash in middle
|
||||
|
||||
test-case
|
||||
|
||||
==>
|
||||
|
||||
Program(Term)
|
||||
|
||||
# Multiple dash terms
|
||||
|
||||
-one -two -three
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(Term,Term,Term))
|
||||
|
||||
# Negate file prefix
|
||||
|
||||
-file:test.js
|
||||
|
||||
==>
|
||||
|
||||
Program(NegateExpr(PrefixExpr(FileExpr)))
|
||||
|
||||
# Negate repo prefix
|
||||
|
||||
-repo:archived
|
||||
|
||||
==>
|
||||
|
||||
Program(NegateExpr(PrefixExpr(RepoExpr)))
|
||||
|
||||
# Negate lang prefix
|
||||
|
||||
-lang:python
|
||||
|
||||
==>
|
||||
|
||||
Program(NegateExpr(PrefixExpr(LangExpr)))
|
||||
|
||||
# Negate content prefix
|
||||
|
||||
-content:TODO
|
||||
|
||||
==>
|
||||
|
||||
Program(NegateExpr(PrefixExpr(ContentExpr)))
|
||||
|
||||
# Negate revision prefix
|
||||
|
||||
-rev:develop
|
||||
|
||||
==>
|
||||
|
||||
Program(NegateExpr(PrefixExpr(RevisionExpr)))
|
||||
|
||||
# Negate archived prefix
|
||||
|
||||
-archived:yes
|
||||
|
||||
==>
|
||||
|
||||
Program(NegateExpr(PrefixExpr(ArchivedExpr)))
|
||||
|
||||
# Negate fork prefix
|
||||
|
||||
-fork:yes
|
||||
|
||||
==>
|
||||
|
||||
Program(NegateExpr(PrefixExpr(ForkExpr)))
|
||||
|
||||
# Negate visibility prefix
|
||||
|
||||
-visibility:any
|
||||
|
||||
==>
|
||||
|
||||
Program(NegateExpr(PrefixExpr(VisibilityExpr)))
|
||||
|
||||
# Negate context prefix
|
||||
|
||||
-context:backend
|
||||
|
||||
==>
|
||||
|
||||
Program(NegateExpr(PrefixExpr(ContextExpr)))
|
||||
|
||||
# Negate symbol prefix
|
||||
|
||||
-sym:OldClass
|
||||
|
||||
==>
|
||||
|
||||
Program(NegateExpr(PrefixExpr(SymExpr)))
|
||||
|
||||
# Negate parentheses
|
||||
|
||||
-(test)
|
||||
|
||||
==>
|
||||
|
||||
Program(NegateExpr(ParenExpr(Term)))
|
||||
|
||||
# Negate group with multiple terms
|
||||
|
||||
-(test exclude)
|
||||
|
||||
==>
|
||||
|
||||
Program(NegateExpr(ParenExpr(AndExpr(Term,Term))))
|
||||
|
||||
# Negate group with prefix
|
||||
|
||||
-(file:test.js console.log)
|
||||
|
||||
==>
|
||||
|
||||
Program(NegateExpr(ParenExpr(AndExpr(PrefixExpr(FileExpr),Term))))
|
||||
|
||||
# Prefix with negated term
|
||||
|
||||
file:test.js -console
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(PrefixExpr(FileExpr),Term))
|
||||
|
||||
# Multiple prefixes with negation
|
||||
|
||||
file:test.js -lang:python
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(PrefixExpr(FileExpr),NegateExpr(PrefixExpr(LangExpr))))
|
||||
|
||||
# Complex negation pattern
|
||||
|
||||
function -file:test.js -lang:java
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(Term,NegateExpr(PrefixExpr(FileExpr)),NegateExpr(PrefixExpr(LangExpr))))
|
||||
|
||||
# Negation inside parentheses
|
||||
|
||||
(-file:test.js)
|
||||
|
||||
==>
|
||||
|
||||
Program(ParenExpr(NegateExpr(PrefixExpr(FileExpr))))
|
||||
|
||||
# Multiple negations in group
|
||||
|
||||
(-file:a.js -lang:python)
|
||||
|
||||
==>
|
||||
|
||||
Program(ParenExpr(AndExpr(NegateExpr(PrefixExpr(FileExpr)),NegateExpr(PrefixExpr(LangExpr)))))
|
||||
|
||||
# Mixed in parentheses
|
||||
|
||||
(include -file:test.js)
|
||||
|
||||
==>
|
||||
|
||||
Program(ParenExpr(AndExpr(Term,NegateExpr(PrefixExpr(FileExpr)))))
|
||||
|
||||
# Negate nested group
|
||||
|
||||
-((file:test.js))
|
||||
|
||||
==>
|
||||
|
||||
Program(NegateExpr(ParenExpr(ParenExpr(PrefixExpr(FileExpr)))))
|
||||
|
||||
# Negate short form prefix
|
||||
|
||||
-f:test.js
|
||||
|
||||
==>
|
||||
|
||||
Program(NegateExpr(PrefixExpr(FileExpr)))
|
||||
|
||||
# Negate short form repo
|
||||
|
||||
-r:myrepo
|
||||
|
||||
==>
|
||||
|
||||
Program(NegateExpr(PrefixExpr(RepoExpr)))
|
||||
|
||||
# Negate short form content
|
||||
|
||||
-c:console
|
||||
|
||||
==>
|
||||
|
||||
Program(NegateExpr(PrefixExpr(ContentExpr)))
|
||||
|
||||
# Negate with prefix in quotes
|
||||
|
||||
-file:"test file.js"
|
||||
|
||||
==>
|
||||
|
||||
Program(NegateExpr(PrefixExpr(FileExpr)))
|
||||
|
||||
# Complex with multiple negated prefixes
|
||||
|
||||
lang:typescript -file:*.test.ts -file:*.spec.ts
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(PrefixExpr(LangExpr),NegateExpr(PrefixExpr(FileExpr)),NegateExpr(PrefixExpr(FileExpr))))
|
||||
|
||||
# Negated group with prefix
|
||||
|
||||
-(file:test.js lang:python)
|
||||
|
||||
==>
|
||||
|
||||
Program(NegateExpr(ParenExpr(AndExpr(PrefixExpr(FileExpr),PrefixExpr(LangExpr)))))
|
||||
|
||||
# Negate empty group
|
||||
|
||||
-()
|
||||
|
||||
==>
|
||||
|
||||
Program(NegateExpr(ParenExpr))
|
||||
|
||||
# Negate with space after dash
|
||||
|
||||
- file:test.js
|
||||
|
||||
==>
|
||||
|
||||
Program(NegateExpr(PrefixExpr(FileExpr)))
|
||||
271
packages/queryLanguage/test/operators.txt
Normal file
271
packages/queryLanguage/test/operators.txt
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
# Simple OR
|
||||
|
||||
test or example
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(Term,Term))
|
||||
|
||||
# Multiple OR
|
||||
|
||||
one or two or three
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(Term,Term,Term))
|
||||
|
||||
# OR with prefixes
|
||||
|
||||
file:test.js or file:example.js
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(PrefixExpr(FileExpr),PrefixExpr(FileExpr)))
|
||||
|
||||
# OR with negation
|
||||
|
||||
test or -file:excluded.js
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(Term,NegateExpr(PrefixExpr(FileExpr))))
|
||||
|
||||
# OR with quoted strings
|
||||
|
||||
"first option" or "second option"
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(QuotedTerm,QuotedTerm))
|
||||
|
||||
# OR with different prefixes
|
||||
|
||||
lang:python or lang:javascript
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(PrefixExpr(LangExpr),PrefixExpr(LangExpr)))
|
||||
|
||||
# Multiple terms with OR
|
||||
|
||||
function test or class example
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(AndExpr(Term,Term),AndExpr(Term,Term)))
|
||||
|
||||
# OR in parentheses
|
||||
|
||||
(test or example)
|
||||
|
||||
==>
|
||||
|
||||
Program(ParenExpr(OrExpr(Term,Term)))
|
||||
|
||||
# OR with parentheses outside
|
||||
|
||||
(test) or (example)
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(ParenExpr(Term),ParenExpr(Term)))
|
||||
|
||||
# Complex OR with grouping
|
||||
|
||||
(file:*.js lang:javascript) or (file:*.ts lang:typescript)
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(ParenExpr(AndExpr(PrefixExpr(FileExpr),PrefixExpr(LangExpr))),ParenExpr(AndExpr(PrefixExpr(FileExpr),PrefixExpr(LangExpr)))))
|
||||
|
||||
# OR with mixed content
|
||||
|
||||
test or file:example.js
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(Term,PrefixExpr(FileExpr)))
|
||||
|
||||
# Prefix OR term
|
||||
|
||||
file:test.js or example
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(PrefixExpr(FileExpr),Term))
|
||||
|
||||
# OR with short form prefixes
|
||||
|
||||
f:test.js or r:myrepo
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(PrefixExpr(FileExpr),PrefixExpr(RepoExpr)))
|
||||
|
||||
# OR with repo prefixes
|
||||
|
||||
repo:project1 or repo:project2
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(PrefixExpr(RepoExpr),PrefixExpr(RepoExpr)))
|
||||
|
||||
# OR with revision prefixes
|
||||
|
||||
rev:main or rev:develop
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(PrefixExpr(RevisionExpr),PrefixExpr(RevisionExpr)))
|
||||
|
||||
# OR with lang prefixes
|
||||
|
||||
lang:rust or lang:go
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(PrefixExpr(LangExpr),PrefixExpr(LangExpr)))
|
||||
|
||||
# OR with content
|
||||
|
||||
content:TODO or content:FIXME
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(PrefixExpr(ContentExpr),PrefixExpr(ContentExpr)))
|
||||
|
||||
# OR with negated terms
|
||||
|
||||
-file:test.js or -file:spec.js
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(NegateExpr(PrefixExpr(FileExpr)),NegateExpr(PrefixExpr(FileExpr))))
|
||||
|
||||
# OR in nested parentheses
|
||||
|
||||
((a or b) or (c or d))
|
||||
|
||||
==>
|
||||
|
||||
Program(ParenExpr(OrExpr(ParenExpr(OrExpr(Term,Term)),ParenExpr(OrExpr(Term,Term)))))
|
||||
|
||||
# Multiple OR with parentheses and implicit AND
|
||||
|
||||
(a or b) and (c or d)
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(ParenExpr(OrExpr(Term,Term)),Term,ParenExpr(OrExpr(Term,Term))))
|
||||
|
||||
# OR with wildcards
|
||||
|
||||
*.test.js or *.spec.js
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(Term,Term))
|
||||
|
||||
# OR with regex patterns
|
||||
|
||||
[a-z]+ or [0-9]+
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(Term,Term))
|
||||
|
||||
# OR with dots
|
||||
|
||||
com.example.test or org.example.test
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(Term,Term))
|
||||
|
||||
# OR with dashes
|
||||
|
||||
test-one or test-two
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(Term,Term))
|
||||
|
||||
# Word containing 'or'
|
||||
|
||||
order
|
||||
|
||||
==>
|
||||
|
||||
Program(Term)
|
||||
|
||||
# Word containing 'or' in middle
|
||||
|
||||
before
|
||||
|
||||
==>
|
||||
|
||||
Program(Term)
|
||||
|
||||
# OR at start
|
||||
|
||||
or test
|
||||
|
||||
==>
|
||||
|
||||
Program(⚠,Term)
|
||||
|
||||
# OR at end (or becomes term)
|
||||
|
||||
test or
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(Term,Term))
|
||||
|
||||
# Multiple consecutive OR
|
||||
|
||||
test or or example
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(Term,⚠,Term))
|
||||
|
||||
# OR with all prefix types
|
||||
|
||||
file:*.js or repo:myrepo or lang:javascript
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(PrefixExpr(FileExpr),PrefixExpr(RepoExpr),PrefixExpr(LangExpr)))
|
||||
|
||||
# Complex query with OR and negation
|
||||
|
||||
(lang:python or lang:ruby) -file:test.py
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(ParenExpr(OrExpr(PrefixExpr(LangExpr),PrefixExpr(LangExpr))),NegateExpr(PrefixExpr(FileExpr))))
|
||||
|
||||
# OR with quoted prefix values
|
||||
|
||||
file:"test one.js" or file:"test two.js"
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(PrefixExpr(FileExpr),PrefixExpr(FileExpr)))
|
||||
|
||||
# OR with empty parentheses
|
||||
|
||||
() or ()
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(ParenExpr,ParenExpr))
|
||||
|
||||
# OR with negated groups
|
||||
|
||||
-(file:a.js) or -(file:b.js)
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(NegateExpr(ParenExpr(PrefixExpr(FileExpr))),NegateExpr(ParenExpr(PrefixExpr(FileExpr)))))
|
||||
200
packages/queryLanguage/test/precedence.txt
Normal file
200
packages/queryLanguage/test/precedence.txt
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
# OR has lowest precedence - implicit AND groups first
|
||||
|
||||
a b or c d
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(AndExpr(Term,Term),AndExpr(Term,Term)))
|
||||
|
||||
# Multiple OR operators are left-associative
|
||||
|
||||
a or b or c
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(Term,Term,Term))
|
||||
|
||||
# AND before OR
|
||||
|
||||
file:test.js error or file:test.go panic
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(AndExpr(PrefixExpr(FileExpr),Term),AndExpr(PrefixExpr(FileExpr),Term)))
|
||||
|
||||
# Negation binds tighter than AND
|
||||
|
||||
-file:test.js error
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(NegateExpr(PrefixExpr(FileExpr)),Term))
|
||||
|
||||
# Negation binds tighter than OR
|
||||
|
||||
-file:a.js or file:b.js
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(NegateExpr(PrefixExpr(FileExpr)),PrefixExpr(FileExpr)))
|
||||
|
||||
# Parentheses override precedence
|
||||
|
||||
(a or b) c
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(ParenExpr(OrExpr(Term,Term)),Term))
|
||||
|
||||
# Parentheses override - OR inside parens groups first
|
||||
|
||||
a (b or c)
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(Term,ParenExpr(OrExpr(Term,Term))))
|
||||
|
||||
# Complex: AND, OR, and negation
|
||||
|
||||
a -b or c d
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(AndExpr(Term,Term),AndExpr(Term,Term)))
|
||||
|
||||
# Negated group in OR expression
|
||||
|
||||
-(a b) or c
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(NegateExpr(ParenExpr(AndExpr(Term,Term))),Term))
|
||||
|
||||
# Multiple negations in OR
|
||||
|
||||
-file:a.js or -file:b.js or file:c.js
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(NegateExpr(PrefixExpr(FileExpr)),NegateExpr(PrefixExpr(FileExpr)),PrefixExpr(FileExpr)))
|
||||
|
||||
# Prefix binds to its value only
|
||||
|
||||
file:a.js b.js
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(PrefixExpr(FileExpr),Term))
|
||||
|
||||
# OR with prefixes and terms mixed
|
||||
|
||||
repo:backend error or repo:frontend warning
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(AndExpr(PrefixExpr(RepoExpr),Term),AndExpr(PrefixExpr(RepoExpr),Term)))
|
||||
|
||||
# Nested parentheses with OR
|
||||
|
||||
((a or b) c) or d
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(ParenExpr(AndExpr(ParenExpr(OrExpr(Term,Term)),Term)),Term))
|
||||
|
||||
# OR at different nesting levels
|
||||
|
||||
(a or (b or c))
|
||||
|
||||
==>
|
||||
|
||||
Program(ParenExpr(OrExpr(Term,ParenExpr(OrExpr(Term,Term)))))
|
||||
|
||||
# Implicit AND groups all adjacent terms before OR
|
||||
|
||||
a b c or d e f
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(AndExpr(Term,Term,Term),AndExpr(Term,Term,Term)))
|
||||
|
||||
# Mixed prefix and regular terms with OR
|
||||
|
||||
lang:go func or lang:rust fn
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(AndExpr(PrefixExpr(LangExpr),Term),AndExpr(PrefixExpr(LangExpr),Term)))
|
||||
|
||||
# Negation doesn't affect OR grouping
|
||||
|
||||
a or -b or c
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(Term,Term,Term))
|
||||
|
||||
# Parentheses can isolate OR from surrounding AND
|
||||
|
||||
a (b or c) d
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(Term,ParenExpr(OrExpr(Term,Term)),Term))
|
||||
|
||||
# Multiple parenthesized groups with AND
|
||||
|
||||
(a or b) (c or d)
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(ParenExpr(OrExpr(Term,Term)),ParenExpr(OrExpr(Term,Term))))
|
||||
|
||||
# Quoted strings are atomic - no precedence inside
|
||||
|
||||
"a or b"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Prefix with OR value doesn't split
|
||||
|
||||
file:"a.js or b.js"
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(FileExpr))
|
||||
|
||||
# Negated prefix in complex expression
|
||||
|
||||
-file:test.js lang:go error or warning
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(AndExpr(NegateExpr(PrefixExpr(FileExpr)),PrefixExpr(LangExpr),Term),Term))
|
||||
|
||||
# OR followed by parenthesized AND
|
||||
|
||||
a or (b c)
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(Term,ParenExpr(AndExpr(Term,Term))))
|
||||
|
||||
# Empty parens don't affect precedence
|
||||
|
||||
() or a b
|
||||
|
||||
==>
|
||||
|
||||
Program(OrExpr(ParenExpr,AndExpr(Term,Term)))
|
||||
|
||||
# Negation of empty group
|
||||
|
||||
-() a
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(NegateExpr(ParenExpr),Term))
|
||||
|
||||
336
packages/queryLanguage/test/prefixes.txt
Normal file
336
packages/queryLanguage/test/prefixes.txt
Normal file
|
|
@ -0,0 +1,336 @@
|
|||
# File prefix
|
||||
|
||||
file:README.md
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(FileExpr))
|
||||
|
||||
# File prefix short form
|
||||
|
||||
f:index.ts
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(FileExpr))
|
||||
|
||||
# Repo prefix
|
||||
|
||||
repo:myproject
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(RepoExpr))
|
||||
|
||||
# Repo prefix short form
|
||||
|
||||
r:github.com/user/repo
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(RepoExpr))
|
||||
|
||||
# Content prefix
|
||||
|
||||
content:function
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(ContentExpr))
|
||||
|
||||
# Content prefix short form
|
||||
|
||||
c:console.log
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(ContentExpr))
|
||||
|
||||
# Revision prefix
|
||||
|
||||
rev:main
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(RevisionExpr))
|
||||
|
||||
# Lang prefix
|
||||
|
||||
lang:typescript
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(LangExpr))
|
||||
|
||||
# Archived prefix - no
|
||||
|
||||
archived:no
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(ArchivedExpr))
|
||||
|
||||
# Archived prefix - only
|
||||
|
||||
archived:only
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(ArchivedExpr))
|
||||
|
||||
# Fork prefix - yes
|
||||
|
||||
fork:yes
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(ForkExpr))
|
||||
|
||||
# Fork prefix - only
|
||||
|
||||
fork:only
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(ForkExpr))
|
||||
|
||||
# Visibility prefix - public
|
||||
|
||||
visibility:public
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(VisibilityExpr))
|
||||
|
||||
# Context prefix
|
||||
|
||||
context:web
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(ContextExpr))
|
||||
|
||||
# Symbol prefix
|
||||
|
||||
sym:MyClass
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(SymExpr))
|
||||
|
||||
# RepoSet prefix
|
||||
|
||||
reposet:repo1,repo2
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(RepoSetExpr))
|
||||
|
||||
# File with wildcard
|
||||
|
||||
file:*.ts
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(FileExpr))
|
||||
|
||||
# File with path
|
||||
|
||||
file:src/components/Button.tsx
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(FileExpr))
|
||||
|
||||
# Repo with full URL
|
||||
|
||||
repo:github.com/org/project
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(RepoExpr))
|
||||
|
||||
# Multiple prefixes
|
||||
|
||||
file:test.js repo:myproject
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(PrefixExpr(FileExpr),PrefixExpr(RepoExpr)))
|
||||
|
||||
# Prefix with term
|
||||
|
||||
file:test.js console.log
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(PrefixExpr(FileExpr),Term))
|
||||
|
||||
# Term then prefix
|
||||
|
||||
console.log file:handler.ts
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(Term,PrefixExpr(FileExpr)))
|
||||
|
||||
# Multiple prefixes and terms
|
||||
|
||||
lang:typescript function file:handler.ts
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(PrefixExpr(LangExpr),Term,PrefixExpr(FileExpr)))
|
||||
|
||||
# Prefix with regex pattern
|
||||
|
||||
file:[a-z]+\.test\.js
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(FileExpr))
|
||||
|
||||
# Content with spaces in value (no quotes)
|
||||
|
||||
content:hello
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(ContentExpr))
|
||||
|
||||
# Revision with slashes
|
||||
|
||||
rev:feature/new-feature
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(RevisionExpr))
|
||||
|
||||
# RepoSet with multiple repos
|
||||
|
||||
reposet:repo1,repo2,repo3
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(RepoSetExpr))
|
||||
|
||||
# Symbol with dots
|
||||
|
||||
sym:package.Class.method
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(SymExpr))
|
||||
|
||||
# Lang with various languages
|
||||
|
||||
lang:python
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(LangExpr))
|
||||
|
||||
# Archived prefix - yes
|
||||
|
||||
archived:yes
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(ArchivedExpr))
|
||||
|
||||
# Archived prefix - invalid value (error case)
|
||||
|
||||
archived:invalid
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(PrefixExpr(ArchivedExpr(⚠)),Term))
|
||||
|
||||
# Fork prefix - no
|
||||
|
||||
fork:no
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(ForkExpr))
|
||||
|
||||
# Fork prefix - invalid value (error case)
|
||||
|
||||
fork:invalid
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(PrefixExpr(ForkExpr(⚠)),Term))
|
||||
|
||||
# Visibility prefix - private
|
||||
|
||||
visibility:private
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(VisibilityExpr))
|
||||
|
||||
# Visibility prefix - any
|
||||
|
||||
visibility:any
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(VisibilityExpr))
|
||||
|
||||
# Visibility prefix - invalid value (error case)
|
||||
|
||||
visibility:invalid
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(PrefixExpr(VisibilityExpr(⚠)),Term))
|
||||
|
||||
# File with dashes
|
||||
|
||||
file:my-component.tsx
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(FileExpr))
|
||||
|
||||
# Repo with numbers
|
||||
|
||||
repo:project123
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(RepoExpr))
|
||||
|
||||
# Content with special chars
|
||||
|
||||
content:@Component
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(ContentExpr))
|
||||
|
||||
# Context with underscores
|
||||
|
||||
context:data_engineering
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(ContextExpr))
|
||||
|
||||
# Prefix in parentheses
|
||||
|
||||
(file:test.js)
|
||||
|
||||
==>
|
||||
|
||||
Program(ParenExpr(PrefixExpr(FileExpr)))
|
||||
|
||||
# Multiple prefixes in group
|
||||
|
||||
(file:*.ts lang:typescript)
|
||||
|
||||
==>
|
||||
|
||||
Program(ParenExpr(AndExpr(PrefixExpr(FileExpr),PrefixExpr(LangExpr))))
|
||||
|
||||
503
packages/queryLanguage/test/quoted.txt
Normal file
503
packages/queryLanguage/test/quoted.txt
Normal file
|
|
@ -0,0 +1,503 @@
|
|||
# Simple quoted string
|
||||
|
||||
"hello"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Leading unclosed quote
|
||||
|
||||
"hello
|
||||
|
||||
==>
|
||||
|
||||
Program(Term)
|
||||
|
||||
# Trailing unclosed quote
|
||||
|
||||
hello"
|
||||
|
||||
==>
|
||||
|
||||
Program(Term)
|
||||
|
||||
# Quoted string with spaces
|
||||
|
||||
"hello world"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Multiple words in quotes
|
||||
|
||||
"this is a search term"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quoted string with escaped quote
|
||||
|
||||
"hello \"world\""
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quoted string with escaped backslash
|
||||
|
||||
"path\\to\\file"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Double backslash
|
||||
|
||||
"test\\\\path"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Multiple escaped quotes
|
||||
|
||||
"\"quoted\" \"words\""
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Mixed escaped characters
|
||||
|
||||
"test\\nvalue\"quoted"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Empty quoted string
|
||||
|
||||
""
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quoted string with only spaces
|
||||
|
||||
" "
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quoted string in file prefix
|
||||
|
||||
file:"my file.txt"
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(FileExpr))
|
||||
|
||||
# Quoted string in repo prefix
|
||||
|
||||
repo:"github.com/user/repo name"
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(RepoExpr))
|
||||
|
||||
# Quoted string in content prefix
|
||||
|
||||
content:"console.log"
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(ContentExpr))
|
||||
|
||||
# Quoted string in revision prefix
|
||||
|
||||
rev:"feature/my feature"
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(RevisionExpr))
|
||||
|
||||
# Multiple quoted strings
|
||||
|
||||
"first string" "second string"
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(QuotedTerm,QuotedTerm))
|
||||
|
||||
# Quoted and unquoted mixed
|
||||
|
||||
unquoted "quoted string" another
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(Term,QuotedTerm,Term))
|
||||
|
||||
# Quoted string with parentheses inside
|
||||
|
||||
"(test)"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quoted string with brackets
|
||||
|
||||
"[a-z]+"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quoted string with special chars
|
||||
|
||||
"test@example.com"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quoted string with colons
|
||||
|
||||
"key:value"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quoted string with dashes
|
||||
|
||||
"test-case-example"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quoted string with dots
|
||||
|
||||
"com.example.package"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quoted string with regex pattern
|
||||
|
||||
"\\w+\\s*=\\s*\\d+"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quoted string with forward slashes
|
||||
|
||||
"path/to/file"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quoted string with underscores
|
||||
|
||||
"my_variable_name"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quoted string with numbers
|
||||
|
||||
"test123"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quoted string with mixed case
|
||||
|
||||
"CamelCaseTest"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quoted prefix value with spaces
|
||||
|
||||
file:"test file.js"
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(FileExpr))
|
||||
|
||||
# Multiple prefixes with quoted values
|
||||
|
||||
file:"my file.txt" repo:"my repo"
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(PrefixExpr(FileExpr),PrefixExpr(RepoExpr)))
|
||||
|
||||
# Quoted string in parentheses
|
||||
|
||||
("quoted term")
|
||||
|
||||
==>
|
||||
|
||||
Program(ParenExpr(QuotedTerm))
|
||||
|
||||
# Multiple quoted in parentheses
|
||||
|
||||
("first" "second")
|
||||
|
||||
==>
|
||||
|
||||
Program(ParenExpr(AndExpr(QuotedTerm,QuotedTerm)))
|
||||
|
||||
# Quoted with escaped newline
|
||||
|
||||
"line1\\nline2"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quoted with tab character
|
||||
|
||||
"value\\ttab"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Lang prefix with quoted value
|
||||
|
||||
lang:"objective-c"
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(LangExpr))
|
||||
|
||||
# Sym prefix with quoted value
|
||||
|
||||
sym:"My Class"
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(SymExpr))
|
||||
|
||||
# Content with quoted phrase
|
||||
|
||||
content:"TODO: fix this"
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(ContentExpr))
|
||||
|
||||
# Quoted string with at symbol
|
||||
|
||||
"@decorator"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quoted string with hash
|
||||
|
||||
"#define"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quoted string with dollar sign
|
||||
|
||||
"$variable"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quoted string with percent
|
||||
|
||||
"100%"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quoted string with ampersand
|
||||
|
||||
"foo&bar"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quoted string with asterisk
|
||||
|
||||
"test*"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quoted string with plus
|
||||
|
||||
"a+b"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quoted string with equals
|
||||
|
||||
"a=b"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quoted string with angle brackets
|
||||
|
||||
"<template>"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quoted string with pipe
|
||||
|
||||
"a|b"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quoted string with tilde
|
||||
|
||||
"~/.config"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quoted string with backtick
|
||||
|
||||
"`code`"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quoted string with question mark
|
||||
|
||||
"what?"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quoted string with exclamation
|
||||
|
||||
"important!"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quoted string with semicolon
|
||||
|
||||
"stmt;"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quoted string with comma
|
||||
|
||||
"a,b,c"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Multiple quotes in content
|
||||
|
||||
content:"function \"test\" {"
|
||||
|
||||
==>
|
||||
|
||||
Program(PrefixExpr(ContentExpr))
|
||||
|
||||
# Quoted prefix keyword becomes literal
|
||||
|
||||
"repo:hello"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quoted file prefix as literal
|
||||
|
||||
"file:test.js"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quoted lang prefix as literal
|
||||
|
||||
"lang:python"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quoted partial prefix
|
||||
|
||||
"repo:"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Mix of quoted prefix and real prefix
|
||||
|
||||
"repo:test" file:actual.js
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(QuotedTerm,PrefixExpr(FileExpr)))
|
||||
|
||||
# Quoted short form prefix
|
||||
|
||||
"f:test"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quoted revision prefix
|
||||
|
||||
"rev:main"
|
||||
|
||||
==>
|
||||
|
||||
Program(QuotedTerm)
|
||||
|
||||
# Quotes can be used within words
|
||||
|
||||
name\s*=\s*"projectmanagementlugapi lang:HCL
|
||||
|
||||
==>
|
||||
|
||||
Program(AndExpr(Term, PrefixExpr(LangExpr)))
|
||||
23
packages/queryLanguage/tsconfig.json
Normal file
23
packages/queryLanguage/tsconfig.json
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"lib": ["ES2023"],
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/index.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
8
packages/queryLanguage/vitest.config.ts
Normal file
8
packages/queryLanguage/vitest.config.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'node',
|
||||
watch: false,
|
||||
}
|
||||
});
|
||||
|
|
@ -646,6 +646,115 @@ const schema = {
|
|||
"purpose",
|
||||
"audience"
|
||||
]
|
||||
},
|
||||
"AuthentikIdentityProviderConfig": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "authentik"
|
||||
},
|
||||
"purpose": {
|
||||
"const": "sso"
|
||||
},
|
||||
"clientId": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"googleCloudSecret": {
|
||||
"type": "string",
|
||||
"description": "The resource name of a Google Cloud secret. Must be in the format `projects/<project-id>/secrets/<secret-name>/versions/<version-id>`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"googleCloudSecret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"clientSecret": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"googleCloudSecret": {
|
||||
"type": "string",
|
||||
"description": "The resource name of a Google Cloud secret. Must be in the format `projects/<project-id>/secrets/<secret-name>/versions/<version-id>`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"googleCloudSecret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"issuer": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"googleCloudSecret": {
|
||||
"type": "string",
|
||||
"description": "The resource name of a Google Cloud secret. Must be in the format `projects/<project-id>/secrets/<secret-name>/versions/<version-id>`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"googleCloudSecret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"purpose",
|
||||
"clientId",
|
||||
"clientSecret",
|
||||
"issuer"
|
||||
]
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
|
|
@ -1292,6 +1401,115 @@ const schema = {
|
|||
"purpose",
|
||||
"audience"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "authentik"
|
||||
},
|
||||
"purpose": {
|
||||
"const": "sso"
|
||||
},
|
||||
"clientId": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"googleCloudSecret": {
|
||||
"type": "string",
|
||||
"description": "The resource name of a Google Cloud secret. Must be in the format `projects/<project-id>/secrets/<secret-name>/versions/<version-id>`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"googleCloudSecret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"clientSecret": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"googleCloudSecret": {
|
||||
"type": "string",
|
||||
"description": "The resource name of a Google Cloud secret. Must be in the format `projects/<project-id>/secrets/<secret-name>/versions/<version-id>`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"googleCloudSecret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"issuer": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"googleCloudSecret": {
|
||||
"type": "string",
|
||||
"description": "The resource name of a Google Cloud secret. Must be in the format `projects/<project-id>/secrets/<secret-name>/versions/<version-id>`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"googleCloudSecret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"purpose",
|
||||
"clientId",
|
||||
"clientSecret",
|
||||
"issuer"
|
||||
]
|
||||
}
|
||||
]
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ export type IdentityProviderConfig =
|
|||
| OktaIdentityProviderConfig
|
||||
| KeycloakIdentityProviderConfig
|
||||
| MicrosoftEntraIDIdentityProviderConfig
|
||||
| GCPIAPIdentityProviderConfig;
|
||||
| GCPIAPIdentityProviderConfig
|
||||
| AuthentikIdentityProviderConfig;
|
||||
|
||||
export interface GitHubIdentityProviderConfig {
|
||||
provider: "github";
|
||||
|
|
@ -255,3 +256,46 @@ export interface GCPIAPIdentityProviderConfig {
|
|||
googleCloudSecret: string;
|
||||
};
|
||||
}
|
||||
export interface AuthentikIdentityProviderConfig {
|
||||
provider: "authentik";
|
||||
purpose: "sso";
|
||||
clientId:
|
||||
| {
|
||||
/**
|
||||
* The name of the environment variable that contains the token.
|
||||
*/
|
||||
env: string;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* The resource name of a Google Cloud secret. Must be in the format `projects/<project-id>/secrets/<secret-name>/versions/<version-id>`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets
|
||||
*/
|
||||
googleCloudSecret: string;
|
||||
};
|
||||
clientSecret:
|
||||
| {
|
||||
/**
|
||||
* The name of the environment variable that contains the token.
|
||||
*/
|
||||
env: string;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* The resource name of a Google Cloud secret. Must be in the format `projects/<project-id>/secrets/<secret-name>/versions/<version-id>`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets
|
||||
*/
|
||||
googleCloudSecret: string;
|
||||
};
|
||||
issuer:
|
||||
| {
|
||||
/**
|
||||
* The name of the environment variable that contains the token.
|
||||
*/
|
||||
env: string;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* The resource name of a Google Cloud secret. Must be in the format `projects/<project-id>/secrets/<secret-name>/versions/<version-id>`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets
|
||||
*/
|
||||
googleCloudSecret: string;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5162,6 +5162,115 @@ const schema = {
|
|||
"purpose",
|
||||
"audience"
|
||||
]
|
||||
},
|
||||
"AuthentikIdentityProviderConfig": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "authentik"
|
||||
},
|
||||
"purpose": {
|
||||
"const": "sso"
|
||||
},
|
||||
"clientId": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"googleCloudSecret": {
|
||||
"type": "string",
|
||||
"description": "The resource name of a Google Cloud secret. Must be in the format `projects/<project-id>/secrets/<secret-name>/versions/<version-id>`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"googleCloudSecret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"clientSecret": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"googleCloudSecret": {
|
||||
"type": "string",
|
||||
"description": "The resource name of a Google Cloud secret. Must be in the format `projects/<project-id>/secrets/<secret-name>/versions/<version-id>`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"googleCloudSecret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"issuer": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"googleCloudSecret": {
|
||||
"type": "string",
|
||||
"description": "The resource name of a Google Cloud secret. Must be in the format `projects/<project-id>/secrets/<secret-name>/versions/<version-id>`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"googleCloudSecret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"purpose",
|
||||
"clientId",
|
||||
"clientSecret",
|
||||
"issuer"
|
||||
]
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
|
|
@ -5808,6 +5917,115 @@ const schema = {
|
|||
"purpose",
|
||||
"audience"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "authentik"
|
||||
},
|
||||
"purpose": {
|
||||
"const": "sso"
|
||||
},
|
||||
"clientId": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"googleCloudSecret": {
|
||||
"type": "string",
|
||||
"description": "The resource name of a Google Cloud secret. Must be in the format `projects/<project-id>/secrets/<secret-name>/versions/<version-id>`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"googleCloudSecret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"clientSecret": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"googleCloudSecret": {
|
||||
"type": "string",
|
||||
"description": "The resource name of a Google Cloud secret. Must be in the format `projects/<project-id>/secrets/<secret-name>/versions/<version-id>`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"googleCloudSecret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"issuer": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"env": {
|
||||
"type": "string",
|
||||
"description": "The name of the environment variable that contains the token."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"env"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"googleCloudSecret": {
|
||||
"type": "string",
|
||||
"description": "The resource name of a Google Cloud secret. Must be in the format `projects/<project-id>/secrets/<secret-name>/versions/<version-id>`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"googleCloudSecret"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"purpose",
|
||||
"clientId",
|
||||
"clientSecret",
|
||||
"issuer"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,8 @@ export type IdentityProviderConfig =
|
|||
| OktaIdentityProviderConfig
|
||||
| KeycloakIdentityProviderConfig
|
||||
| MicrosoftEntraIDIdentityProviderConfig
|
||||
| GCPIAPIdentityProviderConfig;
|
||||
| GCPIAPIdentityProviderConfig
|
||||
| AuthentikIdentityProviderConfig;
|
||||
|
||||
export interface SourcebotConfig {
|
||||
$schema?: string;
|
||||
|
|
@ -1401,3 +1402,46 @@ export interface GCPIAPIdentityProviderConfig {
|
|||
googleCloudSecret: string;
|
||||
};
|
||||
}
|
||||
export interface AuthentikIdentityProviderConfig {
|
||||
provider: "authentik";
|
||||
purpose: "sso";
|
||||
clientId:
|
||||
| {
|
||||
/**
|
||||
* The name of the environment variable that contains the token.
|
||||
*/
|
||||
env: string;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* The resource name of a Google Cloud secret. Must be in the format `projects/<project-id>/secrets/<secret-name>/versions/<version-id>`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets
|
||||
*/
|
||||
googleCloudSecret: string;
|
||||
};
|
||||
clientSecret:
|
||||
| {
|
||||
/**
|
||||
* The name of the environment variable that contains the token.
|
||||
*/
|
||||
env: string;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* The resource name of a Google Cloud secret. Must be in the format `projects/<project-id>/secrets/<secret-name>/versions/<version-id>`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets
|
||||
*/
|
||||
googleCloudSecret: string;
|
||||
};
|
||||
issuer:
|
||||
| {
|
||||
/**
|
||||
* The name of the environment variable that contains the token.
|
||||
*/
|
||||
env: string;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* The resource name of a Google Cloud secret. Must be in the format `projects/<project-id>/secrets/<secret-name>/versions/<version-id>`. See https://cloud.google.com/secret-manager/docs/creating-and-accessing-secrets
|
||||
*/
|
||||
googleCloudSecret: string;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ export const env = createEnv({
|
|||
client: {
|
||||
NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT: z.enum(SOURCEBOT_CLOUD_ENVIRONMENT).optional(),
|
||||
NEXT_PUBLIC_SOURCEBOT_VERSION: z.string().default("unknown"),
|
||||
NEXT_PUBLIC_POSTHOG_PAPIK: z.string().optional(),
|
||||
NEXT_PUBLIC_SENTRY_BACKEND_DSN: z.string().optional(),
|
||||
NEXT_PUBLIC_SENTRY_ENVIRONMENT: z.string().optional(),
|
||||
NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY: z.string().optional(),
|
||||
|
|
@ -16,7 +15,6 @@ export const env = createEnv({
|
|||
runtimeEnvStrict: {
|
||||
NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT: process.env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT,
|
||||
NEXT_PUBLIC_SOURCEBOT_VERSION: process.env.NEXT_PUBLIC_SOURCEBOT_VERSION,
|
||||
NEXT_PUBLIC_POSTHOG_PAPIK: process.env.NEXT_PUBLIC_POSTHOG_PAPIK,
|
||||
NEXT_PUBLIC_SENTRY_BACKEND_DSN: process.env.NEXT_PUBLIC_SENTRY_BACKEND_DSN,
|
||||
NEXT_PUBLIC_SENTRY_ENVIRONMENT: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT,
|
||||
NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY: process.env.NEXT_PUBLIC_LANGFUSE_PUBLIC_KEY,
|
||||
|
|
|
|||
|
|
@ -120,6 +120,8 @@ export const env = createEnv({
|
|||
CONFIG_MAX_REPOS_NO_TOKEN: numberSchema.default(Number.MAX_SAFE_INTEGER),
|
||||
NODE_ENV: z.enum(["development", "test", "production"]),
|
||||
SOURCEBOT_TELEMETRY_DISABLED: booleanSchema.default('false'),
|
||||
// @note: this is also declared in the Dockerfile.
|
||||
POSTHOG_PAPIK: z.string().default("phc_lLPuFFi5LH6c94eFJcqvYVFwiJffVcV6HD8U4a1OnRW"),
|
||||
|
||||
// Database variables
|
||||
// Either DATABASE_URL or DATABASE_HOST, DATABASE_USERNAME, DATABASE_PASSWORD, and DATABASE_NAME must be set.
|
||||
|
|
@ -219,6 +221,9 @@ export const env = createEnv({
|
|||
|
||||
// Configure the default maximum number of search results to return by default.
|
||||
DEFAULT_MAX_MATCH_COUNT: numberSchema.default(10_000),
|
||||
|
||||
// A comma separated list of glob patterns that shwould always be indexed regardless of their size.
|
||||
ALWAYS_INDEX_FILE_PATTERNS: z.string().optional(),
|
||||
},
|
||||
runtimeEnv,
|
||||
emptyStringAsUndefined: true,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
# shadcn components
|
||||
src/components/
|
||||
next-env.d.ts
|
||||
src/proto/**
|
||||
|
|
@ -8,6 +8,7 @@
|
|||
"start": "next start",
|
||||
"lint": "cross-env SKIP_ENV_VALIDATION=1 eslint .",
|
||||
"test": "cross-env SKIP_ENV_VALIDATION=1 vitest",
|
||||
"generate:protos": "proto-loader-gen-types --includeComments --longs=Number --enums=String --defaults --oneofs --grpcLib=@grpc/grpc-js --keepCase --includeDirs=../../vendor/zoekt/grpc/protos --outDir=src/proto zoekt/webserver/v1/webserver.proto zoekt/webserver/v1/query.proto",
|
||||
"dev:emails": "email dev --dir ./src/emails",
|
||||
"stripe:listen": "stripe listen --forward-to http://localhost:3000/api/stripe"
|
||||
},
|
||||
|
|
@ -52,6 +53,8 @@
|
|||
"@codemirror/state": "^6.4.1",
|
||||
"@codemirror/view": "^6.33.0",
|
||||
"@floating-ui/react": "^0.27.2",
|
||||
"@grpc/grpc-js": "^1.14.1",
|
||||
"@grpc/proto-loader": "^0.8.0",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@iconify/react": "^5.1.0",
|
||||
"@iizukak/codemirror-lang-wgsl": "^0.3.0",
|
||||
|
|
@ -91,6 +94,7 @@
|
|||
"@shopify/lang-jsonc": "^1.0.0",
|
||||
"@sourcebot/codemirror-lang-tcl": "^1.0.12",
|
||||
"@sourcebot/db": "workspace:*",
|
||||
"@sourcebot/query-language": "workspace:*",
|
||||
"@sourcebot/schemas": "workspace:*",
|
||||
"@sourcebot/shared": "workspace:*",
|
||||
"@ssddanbrown/codemirror-lang-twig": "^1.0.0",
|
||||
|
|
@ -133,6 +137,7 @@
|
|||
"embla-carousel-auto-scroll": "^8.3.0",
|
||||
"embla-carousel-react": "^8.3.0",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fuse.js": "^7.0.0",
|
||||
"google-auth-library": "^10.1.0",
|
||||
"graphql": "^16.9.0",
|
||||
|
|
@ -142,7 +147,7 @@
|
|||
"langfuse-vercel": "^3.38.4",
|
||||
"lucide-react": "^0.517.0",
|
||||
"micromatch": "^4.0.8",
|
||||
"next": "15.5.0",
|
||||
"next": "^15.5.7",
|
||||
"next-auth": "^5.0.0-beta.30",
|
||||
"next-navigation-guard": "^0.2.0",
|
||||
"next-themes": "^0.3.0",
|
||||
|
|
@ -151,11 +156,12 @@
|
|||
"openai": "^4.98.0",
|
||||
"parse-diff": "^0.11.1",
|
||||
"posthog-js": "^1.161.5",
|
||||
"posthog-node": "^5.15.0",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"psl": "^1.15.0",
|
||||
"react": "19.1.1",
|
||||
"react": "^19.2.1",
|
||||
"react-device-detect": "^2.2.3",
|
||||
"react-dom": "19.1.1",
|
||||
"react-dom": "^19.2.1",
|
||||
"react-hook-form": "^7.53.0",
|
||||
"react-hotkeys-hook": "^4.5.1",
|
||||
"react-icons": "^5.3.0",
|
||||
|
|
@ -191,8 +197,8 @@
|
|||
"@types/node": "^20",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@types/psl": "^1.1.3",
|
||||
"@types/react": "19.1.10",
|
||||
"@types/react-dom": "19.1.7",
|
||||
"@types/react": "19.2.1",
|
||||
"@types/react-dom": "19.2.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.40.0",
|
||||
"@typescript-eslint/parser": "^8.40.0",
|
||||
"cross-env": "^7.0.3",
|
||||
|
|
@ -212,7 +218,7 @@
|
|||
"vitest-mock-extended": "^3.1.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "19.1.10",
|
||||
"@types/react-dom": "19.1.7"
|
||||
"@types/react": "19.2.1",
|
||||
"@types/react-dom": "19.2.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1
packages/web/public/authentik.svg
Normal file
1
packages/web/public/authentik.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" id="Layer_1" x="0" y="0" version="1.1" viewBox="-0.03 59.9 512.03 392.1"><style>.st0{fill:#fd4b2d}</style><path d="M279.9 141h17.9v51.2h-17.9zm46.6-2.2h17.9v40h-17.9zM65.3 197.3c-24 0-46 13.2-57.4 34.3h30.4c13.5-11.6 33-15 47.1 0h32.2c-12.6-17.1-31.4-34.3-52.3-34.3" class="st0"/><path d="M108.7 262.4C66.8 350-6.6 275.3 38.3 231.5H7.9C-15.9 273 17 329 65.3 327.8c37.4 0 68.2-55.5 68.2-65.3 0-4.3-6-17.6-16-31H85.4c10.7 9.7 20 23.7 23.3 30.9m1.1-2.6" class="st0"/><path d="M512 140.3v231.3c0 44.3-36.1 80.4-80.4 80.4h-34.1v-78.8h-163V452h-34.1c-44.4 0-80.4-36.1-80.4-80.4v-72.8h258.4v-139H253.6V238H119.9v-97.6c0-3.1.2-6.2.5-9.2.4-3.7 1.1-7.3 2-10.8.3-1.1.6-2.3 1-3.4.1-.3.2-.6.3-.8.2-.6.4-1.1.5-1.7.2-.5.4-1.1.6-1.7s.5-1.2.7-1.8.5-1.2.8-1.8c2-4.7 4.4-9.3 7.3-13.6l.1-.1c.7-1.1 1.5-2.1 2.3-3.2.7-.9 1.3-1.7 2-2.6.8-.9 1.6-1.9 2.4-2.8s1.6-1.8 2.4-2.6l.1-.1c.4-.5.9-.9 1.4-1.4 3-2.9 6.2-5.6 9.6-8 .9-.7 1.9-1.3 2.8-1.9 1.1-.7 2.2-1.4 3.3-2 2.1-1.2 4.2-2.4 6.5-3.4.7-.3 1.4-.7 2.1-1 3.1-1.3 6.2-2.5 9.4-3.4 1.2-.4 2.5-.7 3.7-1 .6-.2 1.2-.3 1.8-.4 3.6-.8 7.2-1.3 10.9-1.6l1.6-.1h.8c1.2-.1 2.4-.1 3.7-.1h231.3c1.2 0 2.5 0 3.7.1h.8l1.6.1c3.7.3 7.3.8 10.9 1.6.6.1 1.2.3 1.8.4 1.3.3 2.5.6 3.7 1 3.2.9 6.3 2.1 9.4 3.4.7.3 1.4.6 2.1 1 2.2 1 4.4 2.2 6.5 3.4 1.1.7 2.2 1.3 3.3 2 1 .6 1.9 1.3 2.8 1.9 3.9 2.8 7.6 6 11 9.4.8.8 1.7 1.7 2.4 2.6.8.9 1.6 1.9 2.4 2.8.7.8 1.3 1.7 2 2.6.8 1.1 1.5 2.1 2.3 3.2l.1.1c2.9 4.3 5.3 8.8 7.3 13.6.2.6.5 1.2.8 1.8.2.6.5 1.2.7 1.8.2.5.4 1.1.6 1.7s.4 1.1.5 1.7c.1.3.2.6.3.8.3 1.1.7 2.3 1 3.4.9 3.6 1.6 7.2 2 10.8 0 3.1.2 6.1.2 9.2" class="st0"/><path d="M498.3 95.5H133.5c14.9-22.2 40-35.6 66.7-35.6h231.3c26.9 0 51.9 13.4 66.8 35.6m13.2 35.6H120.4c1.4-12.8 6-25 13.1-35.6h364.8c7.2 10.6 11.7 22.9 13.2 35.6m.5 9.2v26.4H378.3v-6.9H253.6v6.9H119.9v-26.4c0-3.1.2-6.2.5-9.2h391.1c.3 3.1.5 6.1.5 9.2M119.9 166.7h133.7v35.6H119.9zm258.4 0H512v35.6H378.3zm-258.4 35.6h133.7v35.6H119.9zm258.4 0H512v35.6H378.3z" class="st0"/></svg>
|
||||
|
After Width: | Height: | Size: 2 KiB |
|
|
@ -38,8 +38,8 @@ const auditService = getAuditService();
|
|||
/**
|
||||
* "Service Error Wrapper".
|
||||
*
|
||||
* Captures any thrown exceptions and converts them to a unexpected
|
||||
* service error. Also logs them with Sentry.
|
||||
* Captures any thrown exceptions, logs them to the console and Sentry,
|
||||
* and returns a generic unexpected service error.
|
||||
*/
|
||||
export const sew = async <T>(fn: () => Promise<T>): Promise<T | ServiceError> => {
|
||||
try {
|
||||
|
|
@ -48,8 +48,8 @@ export const sew = async <T>(fn: () => Promise<T>): Promise<T | ServiceError> =>
|
|||
Sentry.captureException(e);
|
||||
logger.error(e);
|
||||
|
||||
if (e instanceof Error) {
|
||||
return unexpectedError(e.message);
|
||||
if (e instanceof ServiceErrorException) {
|
||||
return e.serviceError;
|
||||
}
|
||||
|
||||
return unexpectedError(`An unexpected error occurred. Please try again later.`);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { getRepoInfoByName } from "@/actions";
|
||||
import { PathHeader } from "@/app/[domain]/components/pathHeader";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { getFileSource } from "@/features/search/fileSourceApi";
|
||||
import { cn, getCodeHostInfoForRepo, isServiceError } from "@/lib/utils";
|
||||
import Image from "next/image";
|
||||
import { PureCodePreviewPanel } from "./pureCodePreviewPanel";
|
||||
import { getFileSource } from "@/features/search/fileSourceApi";
|
||||
|
||||
interface CodePreviewPanelProps {
|
||||
path: string;
|
||||
|
|
@ -22,8 +22,12 @@ export const CodePreviewPanel = async ({ path, repoName, revisionName }: CodePre
|
|||
getRepoInfoByName(repoName),
|
||||
]);
|
||||
|
||||
if (isServiceError(fileSourceResponse) || isServiceError(repoInfoResponse)) {
|
||||
return <div>Error loading file source</div>
|
||||
if (isServiceError(fileSourceResponse)) {
|
||||
return <div>Error loading file source: {fileSourceResponse.message}</div>
|
||||
}
|
||||
|
||||
if (isServiceError(repoInfoResponse)) {
|
||||
return <div>Error loading repo info: {repoInfoResponse.message}</div>
|
||||
}
|
||||
|
||||
const codeHostInfo = getCodeHostInfoForRepo({
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { SymbolHoverPopup } from "@/ee/features/codeNav/components/symbolHoverPopup";
|
||||
import { symbolHoverTargetsExtension } from "@/ee/features/codeNav/components/symbolHoverPopup/symbolHoverTargetsExtension";
|
||||
import { SymbolDefinition } from "@/ee/features/codeNav/components/symbolHoverPopup/useHoveredOverSymbolInfo";
|
||||
import { useHasEntitlement } from "@/features/entitlements/useHasEntitlement";
|
||||
import { useCodeMirrorLanguageExtension } from "@/hooks/useCodeMirrorLanguageExtension";
|
||||
import { useCodeMirrorTheme } from "@/hooks/useCodeMirrorTheme";
|
||||
|
|
@ -11,15 +10,10 @@ import { useKeymapExtension } from "@/hooks/useKeymapExtension";
|
|||
import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam";
|
||||
import { search } from "@codemirror/search";
|
||||
import CodeMirror, { EditorSelection, EditorView, ReactCodeMirrorRef, SelectionRange, ViewUpdate } from "@uiw/react-codemirror";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { EditorContextMenu } from "../../../components/editorContextMenu";
|
||||
import { useBrowseNavigation } from "../../hooks/useBrowseNavigation";
|
||||
import { BrowseHighlightRange, HIGHLIGHT_RANGE_QUERY_PARAM } from "../../hooks/utils";
|
||||
import { useBrowseState } from "../../hooks/useBrowseState";
|
||||
import { rangeHighlightingExtension } from "./rangeHighlightingExtension";
|
||||
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
||||
import { createAuditAction } from "@/ee/features/audit/actions";
|
||||
import { useDomain } from "@/hooks/useDomain";
|
||||
|
||||
interface PureCodePreviewPanelProps {
|
||||
path: string;
|
||||
|
|
@ -41,10 +35,6 @@ export const PureCodePreviewPanel = ({
|
|||
const [currentSelection, setCurrentSelection] = useState<SelectionRange>();
|
||||
const keymapExtension = useKeymapExtension(editorRef?.view);
|
||||
const hasCodeNavEntitlement = useHasEntitlement("code-nav");
|
||||
const { updateBrowseState } = useBrowseState();
|
||||
const { navigateToPath } = useBrowseNavigation();
|
||||
const domain = useDomain();
|
||||
const captureEvent = useCaptureEvent();
|
||||
|
||||
const highlightRangeQuery = useNonEmptyQueryParam(HIGHLIGHT_RANGE_QUERY_PARAM);
|
||||
const highlightRange = useMemo((): BrowseHighlightRange | undefined => {
|
||||
|
|
@ -90,7 +80,6 @@ export const PureCodePreviewPanel = ({
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}, [highlightRangeQuery]);
|
||||
|
||||
const extensions = useMemo(() => {
|
||||
|
|
@ -118,90 +107,31 @@ export const PureCodePreviewPanel = ({
|
|||
|
||||
// Scroll the highlighted range into view.
|
||||
useEffect(() => {
|
||||
if (!highlightRange || !editorRef || !editorRef.state) {
|
||||
if (!highlightRange || !editorRef || !editorRef.state || !editorRef.view) {
|
||||
return;
|
||||
}
|
||||
|
||||
const doc = editorRef.state.doc;
|
||||
const { start, end } = highlightRange;
|
||||
const selection = EditorSelection.range(
|
||||
doc.line(start.lineNumber).from,
|
||||
doc.line(end.lineNumber).from,
|
||||
);
|
||||
|
||||
const from = doc.line(start.lineNumber).from;
|
||||
const to = doc.line(end.lineNumber).to;
|
||||
const selection = EditorSelection.range(from, to);
|
||||
|
||||
// When the selection is in view, we don't want to perform any scrolling
|
||||
// as it could be jarring for the user. If it is not in view, scroll to the
|
||||
// center of the viewport.
|
||||
const viewport = editorRef.view.viewport;
|
||||
const isInView = from >= viewport.from && to <= viewport.to;
|
||||
const scrollStrategy = isInView ? "nearest" : "center";
|
||||
|
||||
editorRef.view?.dispatch({
|
||||
effects: [
|
||||
EditorView.scrollIntoView(selection, { y: "center" }),
|
||||
EditorView.scrollIntoView(selection, { y: scrollStrategy }),
|
||||
]
|
||||
});
|
||||
}, [editorRef, highlightRange]);
|
||||
|
||||
const onFindReferences = useCallback((symbolName: string) => {
|
||||
captureEvent('wa_find_references_pressed', {
|
||||
source: 'browse',
|
||||
});
|
||||
createAuditAction({
|
||||
action: "user.performed_find_references",
|
||||
metadata: {
|
||||
message: symbolName,
|
||||
},
|
||||
}, domain)
|
||||
|
||||
updateBrowseState({
|
||||
selectedSymbolInfo: {
|
||||
repoName,
|
||||
symbolName,
|
||||
revisionName,
|
||||
language,
|
||||
},
|
||||
isBottomPanelCollapsed: false,
|
||||
activeExploreMenuTab: "references",
|
||||
})
|
||||
}, [captureEvent, updateBrowseState, repoName, revisionName, language, domain]);
|
||||
|
||||
|
||||
// If we resolve multiple matches, instead of navigating to the first match, we should
|
||||
// instead popup the bottom sheet with the list of matches.
|
||||
const onGotoDefinition = useCallback((symbolName: string, symbolDefinitions: SymbolDefinition[]) => {
|
||||
captureEvent('wa_goto_definition_pressed', {
|
||||
source: 'browse',
|
||||
});
|
||||
createAuditAction({
|
||||
action: "user.performed_goto_definition",
|
||||
metadata: {
|
||||
message: symbolName,
|
||||
},
|
||||
}, domain)
|
||||
|
||||
if (symbolDefinitions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (symbolDefinitions.length === 1) {
|
||||
const symbolDefinition = symbolDefinitions[0];
|
||||
const { fileName, repoName } = symbolDefinition;
|
||||
|
||||
navigateToPath({
|
||||
repoName,
|
||||
revisionName,
|
||||
path: fileName,
|
||||
pathType: 'blob',
|
||||
highlightRange: symbolDefinition.range,
|
||||
})
|
||||
} else {
|
||||
updateBrowseState({
|
||||
selectedSymbolInfo: {
|
||||
symbolName,
|
||||
repoName,
|
||||
revisionName,
|
||||
language,
|
||||
},
|
||||
activeExploreMenuTab: "definitions",
|
||||
isBottomPanelCollapsed: false,
|
||||
})
|
||||
}
|
||||
}, [captureEvent, navigateToPath, revisionName, updateBrowseState, repoName, language, domain]);
|
||||
|
||||
const theme = useCodeMirrorTheme();
|
||||
|
||||
return (
|
||||
|
|
@ -225,11 +155,12 @@ export const PureCodePreviewPanel = ({
|
|||
)}
|
||||
{editorRef && hasCodeNavEntitlement && (
|
||||
<SymbolHoverPopup
|
||||
source="preview"
|
||||
editorRef={editorRef}
|
||||
revisionName={revisionName}
|
||||
language={language}
|
||||
onFindReferences={onFindReferences}
|
||||
onGotoDefinition={onGotoDefinition}
|
||||
fileName={path}
|
||||
repoName={repoName}
|
||||
/>
|
||||
)}
|
||||
</CodeMirror>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
'use client';
|
||||
|
||||
import { useRef } from "react";
|
||||
import { FileTreeItem } from "@/features/fileTree/actions";
|
||||
import { FileTreeItemComponent } from "@/features/fileTree/components/fileTreeItemComponent";
|
||||
import { getBrowsePath } from "../../hooks/utils";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { useBrowseParams } from "../../hooks/useBrowseParams";
|
||||
import { useDomain } from "@/hooks/useDomain";
|
||||
import { FileTreeItem } from "@/features/fileTree/types";
|
||||
|
||||
interface PureTreePreviewPanelProps {
|
||||
items: FileTreeItem[];
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { Separator } from "@/components/ui/separator";
|
||||
import { getRepoInfoByName } from "@/actions";
|
||||
import { PathHeader } from "@/app/[domain]/components/pathHeader";
|
||||
import { getFolderContents } from "@/features/fileTree/actions";
|
||||
import { getFolderContents } from "@/features/fileTree/api";
|
||||
import { isServiceError } from "@/lib/utils";
|
||||
import { PureTreePreviewPanel } from "./pureTreePreviewPanel";
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { useState, useRef, useMemo, useEffect, useCallback } from "react";
|
|||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { unwrapServiceError } from "@/lib/utils";
|
||||
import { FileTreeItem, getFiles } from "@/features/fileTree/actions";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog";
|
||||
import { useBrowseNavigation } from "../hooks/useBrowseNavigation";
|
||||
import { useBrowseState } from "../hooks/useBrowseState";
|
||||
|
|
@ -13,6 +12,8 @@ import { useBrowseParams } from "../hooks/useBrowseParams";
|
|||
import { FileTreeItemIcon } from "@/features/fileTree/components/fileTreeItemIcon";
|
||||
import { useLocalStorage } from "usehooks-ts";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { FileTreeItem } from "@/features/fileTree/types";
|
||||
import { getFiles } from "@/app/api/(client)/client";
|
||||
|
||||
const MAX_RESULTS = 100;
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { useBrowseParams } from "./hooks/useBrowseParams";
|
|||
import { FileSearchCommandDialog } from "./components/fileSearchCommandDialog";
|
||||
import { useDomain } from "@/hooks/useDomain";
|
||||
import { SearchBar } from "../components/searchBar";
|
||||
import escapeStringRegexp from "escape-string-regexp";
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode;
|
||||
|
|
@ -29,7 +30,9 @@ export default function Layout({
|
|||
>
|
||||
<SearchBar
|
||||
size="sm"
|
||||
defaultQuery={`repo:${repoName}${revisionName ? ` rev:${revisionName}` : ''} `}
|
||||
defaults={{
|
||||
query: `repo:^${escapeStringRegexp(repoName)}$${revisionName ? ` rev:${revisionName}` : ''} `,
|
||||
}}
|
||||
className="w-full"
|
||||
/>
|
||||
</TopBar>
|
||||
|
|
|
|||
|
|
@ -24,9 +24,9 @@ export default async function Page(props: PageProps) {
|
|||
const languageModels = await getConfiguredLanguageModelsInfo();
|
||||
const repos = await getRepos();
|
||||
const searchContexts = await getSearchContexts(params.domain);
|
||||
const chatInfo = await getChatInfo({ chatId: params.id }, params.domain);
|
||||
const chatInfo = await getChatInfo({ chatId: params.id });
|
||||
const session = await auth();
|
||||
const chatHistory = session ? await getUserChatHistory(params.domain) : [];
|
||||
const chatHistory = session ? await getUserChatHistory() : [];
|
||||
|
||||
if (isServiceError(chatHistory)) {
|
||||
throw new ServiceErrorException(chatHistory);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { useToast } from "@/components/hooks/use-toast";
|
|||
import { Badge } from "@/components/ui/badge";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { updateChatName } from "@/features/chat/actions";
|
||||
import { useDomain } from "@/hooks/useDomain";
|
||||
import { isServiceError } from "@/lib/utils";
|
||||
import { GlobeIcon } from "@radix-ui/react-icons";
|
||||
import { ChatVisibility } from "@sourcebot/db";
|
||||
|
|
@ -23,7 +22,6 @@ interface ChatNameProps {
|
|||
export const ChatName = ({ name, visibility, id, isReadonly }: ChatNameProps) => {
|
||||
const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
|
||||
const { toast } = useToast();
|
||||
const domain = useDomain();
|
||||
const router = useRouter();
|
||||
|
||||
const onRenameChat = useCallback(async (name: string) => {
|
||||
|
|
@ -31,7 +29,7 @@ export const ChatName = ({ name, visibility, id, isReadonly }: ChatNameProps) =>
|
|||
const response = await updateChatName({
|
||||
chatId: id,
|
||||
name: name,
|
||||
}, domain);
|
||||
});
|
||||
|
||||
if (isServiceError(response)) {
|
||||
toast({
|
||||
|
|
@ -43,7 +41,7 @@ export const ChatName = ({ name, visibility, id, isReadonly }: ChatNameProps) =>
|
|||
});
|
||||
router.refresh();
|
||||
}
|
||||
}, [id, domain, toast, router]);
|
||||
}, [id, toast, router]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import { ScrollArea } from "@/components/ui/scroll-area";
|
|||
import { Separator } from "@/components/ui/separator";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { deleteChat, updateChatName } from "@/features/chat/actions";
|
||||
import { useDomain } from "@/hooks/useDomain";
|
||||
import { cn, isServiceError } from "@/lib/utils";
|
||||
import { CirclePlusIcon, EllipsisIcon, PencilIcon, TrashIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
|
@ -23,6 +22,7 @@ import { useChatId } from "../useChatId";
|
|||
import { RenameChatDialog } from "./renameChatDialog";
|
||||
import { DeleteChatDialog } from "./deleteChatDialog";
|
||||
import Link from "next/link";
|
||||
import { SINGLE_TENANT_ORG_DOMAIN } from "@/lib/constants";
|
||||
|
||||
interface ChatSidePanelProps {
|
||||
order: number;
|
||||
|
|
@ -41,7 +41,6 @@ export const ChatSidePanel = ({
|
|||
isAuthenticated,
|
||||
isCollapsedInitially,
|
||||
}: ChatSidePanelProps) => {
|
||||
const domain = useDomain();
|
||||
const [isCollapsed, setIsCollapsed] = useState(isCollapsedInitially);
|
||||
const sidePanelRef = useRef<ImperativePanelHandle>(null);
|
||||
const router = useRouter();
|
||||
|
|
@ -72,7 +71,7 @@ export const ChatSidePanel = ({
|
|||
const response = await updateChatName({
|
||||
chatId,
|
||||
name: name,
|
||||
}, domain);
|
||||
});
|
||||
|
||||
if (isServiceError(response)) {
|
||||
toast({
|
||||
|
|
@ -84,14 +83,14 @@ export const ChatSidePanel = ({
|
|||
});
|
||||
router.refresh();
|
||||
}
|
||||
}, [router, toast, domain]);
|
||||
}, [router, toast]);
|
||||
|
||||
const onDeleteChat = useCallback(async (chatIdToDelete: string) => {
|
||||
if (!chatIdToDelete) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await deleteChat({ chatId: chatIdToDelete }, domain);
|
||||
const response = await deleteChat({ chatId: chatIdToDelete });
|
||||
|
||||
if (isServiceError(response)) {
|
||||
toast({
|
||||
|
|
@ -104,12 +103,12 @@ export const ChatSidePanel = ({
|
|||
|
||||
// If we just deleted the current chat, navigate to new chat
|
||||
if (chatIdToDelete === chatId) {
|
||||
router.push(`/${domain}/chat`);
|
||||
router.push(`/${SINGLE_TENANT_ORG_DOMAIN}/chat`);
|
||||
}
|
||||
|
||||
router.refresh();
|
||||
}
|
||||
}, [chatId, router, toast, domain]);
|
||||
}, [chatId, router, toast]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -131,7 +130,7 @@ export const ChatSidePanel = ({
|
|||
size="sm"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
router.push(`/${domain}/chat`);
|
||||
router.push(`/${SINGLE_TENANT_ORG_DOMAIN}/chat`);
|
||||
}}
|
||||
>
|
||||
<CirclePlusIcon className="w-4 h-4 mr-1" />
|
||||
|
|
@ -145,7 +144,7 @@ export const ChatSidePanel = ({
|
|||
<div className="flex flex-col">
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
<Link
|
||||
href={`/login?callbackUrl=${encodeURIComponent(`/${domain}/chat`)}`}
|
||||
href={`/login?callbackUrl=${encodeURIComponent(`/${SINGLE_TENANT_ORG_DOMAIN}/chat`)}`}
|
||||
className="text-sm text-link hover:underline cursor-pointer"
|
||||
>
|
||||
Sign in
|
||||
|
|
@ -163,7 +162,7 @@ export const ChatSidePanel = ({
|
|||
chat.id === chatId && "bg-muted"
|
||||
)}
|
||||
onClick={() => {
|
||||
router.push(`/${domain}/chat/${chat.id}`);
|
||||
router.push(`/${SINGLE_TENANT_ORG_DOMAIN}/chat/${chat.id}`);
|
||||
}}
|
||||
>
|
||||
<span className="text-sm truncate">{chat.name ?? 'Untitled chat'}</span>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { memo, useEffect, useMemo, useState } from 'react'
|
|||
import { useCodeMirrorHighlighter } from '@/hooks/useCodeMirrorHighlighter'
|
||||
import tailwind from '@/tailwind'
|
||||
import { measure } from '@/lib/utils'
|
||||
import { SourceRange } from '@/features/search/types'
|
||||
import { SourceRange } from '@/features/search'
|
||||
|
||||
// Define a plain text language
|
||||
const plainTextLanguage = StreamLanguage.define({
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { getConnectionStats, getRepos, getReposStats } from "@/actions";
|
||||
import { getConnectionStats, getCurrentUserRole, getOrgAccountRequests, getRepos, getReposStats } from "@/actions";
|
||||
import { SourcebotLogo } from "@/app/components/sourcebotLogo";
|
||||
import { auth } from "@/auth";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -10,7 +10,7 @@ import { env } from "@sourcebot/shared";
|
|||
import { ServiceErrorException } from "@/lib/serviceError";
|
||||
import { isServiceError } from "@/lib/utils";
|
||||
import { DiscordLogoIcon, GitHubLogoIcon } from "@radix-ui/react-icons";
|
||||
import { RepoIndexingJobStatus, RepoIndexingJobType } from "@sourcebot/db";
|
||||
import { OrgRole, RepoIndexingJobStatus, RepoIndexingJobType } from "@sourcebot/db";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { OrgSelector } from "../orgSelector";
|
||||
|
|
@ -20,7 +20,7 @@ import { NavigationItems } from "./navigationItems";
|
|||
import { ProgressIndicator } from "./progressIndicator";
|
||||
import { TrialIndicator } from "./trialIndicator";
|
||||
|
||||
const SOURCEBOT_DISCORD_URL = "https://discord.gg/GbXMEM5H";
|
||||
const SOURCEBOT_DISCORD_URL = "https://discord.gg/HDScTs3ptP";
|
||||
const SOURCEBOT_GITHUB_URL = "https://github.com/sourcebot-dev/sourcebot";
|
||||
|
||||
interface NavigationMenuProps {
|
||||
|
|
@ -39,11 +39,32 @@ export const NavigationMenu = async ({
|
|||
throw new ServiceErrorException(repoStats);
|
||||
}
|
||||
|
||||
const connectionStats = isAuthenticated ? await getConnectionStats() : null;
|
||||
if (isServiceError(connectionStats)) {
|
||||
throw new ServiceErrorException(connectionStats);
|
||||
const role = isAuthenticated ? await getCurrentUserRole(domain) : null;
|
||||
if (isServiceError(role)) {
|
||||
throw new ServiceErrorException(role);
|
||||
}
|
||||
|
||||
const stats = await (async () => {
|
||||
if (!isAuthenticated || role !== OrgRole.OWNER) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const joinRequests = await getOrgAccountRequests(domain);
|
||||
if (isServiceError(joinRequests)) {
|
||||
throw new ServiceErrorException(joinRequests);
|
||||
}
|
||||
|
||||
const connectionStats = await getConnectionStats();
|
||||
if (isServiceError(connectionStats)) {
|
||||
throw new ServiceErrorException(connectionStats);
|
||||
}
|
||||
|
||||
return {
|
||||
numJoinRequests: joinRequests.length,
|
||||
connectionStats,
|
||||
};
|
||||
})();
|
||||
|
||||
const sampleRepos = await getRepos({
|
||||
where: {
|
||||
jobs: {
|
||||
|
|
@ -100,9 +121,10 @@ export const NavigationMenu = async ({
|
|||
numberOfRepos={numberOfRepos}
|
||||
isReposButtonNotificationDotVisible={numberOfReposWithFirstTimeIndexingJobsInProgress > 0}
|
||||
isSettingsButtonNotificationDotVisible={
|
||||
connectionStats ?
|
||||
connectionStats.numberOfConnectionsWithFirstTimeSyncJobsInProgress > 0 :
|
||||
false
|
||||
stats ? (
|
||||
stats.connectionStats.numberOfConnectionsWithFirstTimeSyncJobsInProgress > 0 ||
|
||||
stats.numJoinRequests > 0
|
||||
) : false
|
||||
}
|
||||
isAuthenticated={isAuthenticated}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -233,7 +233,7 @@ export const PathHeader = ({
|
|||
}}
|
||||
>
|
||||
<span className="mr-0.5">@</span>
|
||||
{`${branchDisplayName}`}
|
||||
{`${branchDisplayName.replace(/^refs\/(heads|tags)\//, '')}`}
|
||||
</p>
|
||||
)}
|
||||
<span>·</span>
|
||||
|
|
|
|||
|
|
@ -16,57 +16,53 @@ export enum SearchPrefix {
|
|||
sym = "sym:",
|
||||
content = "content:",
|
||||
archived = "archived:",
|
||||
case = "case:",
|
||||
fork = "fork:",
|
||||
public = "public:",
|
||||
visibility = "visibility:",
|
||||
context = "context:",
|
||||
}
|
||||
|
||||
export const publicModeSuggestions: Suggestion[] = [
|
||||
export const visibilityModeSuggestions: Suggestion[] = [
|
||||
{
|
||||
value: "yes",
|
||||
value: "public",
|
||||
description: "Only include results from public repositories."
|
||||
},
|
||||
{
|
||||
value: "no",
|
||||
value: "private",
|
||||
description: "Only include results from private repositories."
|
||||
},
|
||||
{
|
||||
value: "any",
|
||||
description: "Include results from both public and private repositories (default)."
|
||||
},
|
||||
];
|
||||
|
||||
export const forkModeSuggestions: Suggestion[] = [
|
||||
{
|
||||
value: "yes",
|
||||
description: "Include results from forked repositories (default)."
|
||||
},
|
||||
{
|
||||
value: "no",
|
||||
description: "Exclude results from forked repositories."
|
||||
},
|
||||
{
|
||||
value: "only",
|
||||
description: "Only include results from forked repositories."
|
||||
},
|
||||
{
|
||||
value: "no",
|
||||
description: "Only include results from non-forked repositories."
|
||||
},
|
||||
];
|
||||
|
||||
export const caseModeSuggestions: Suggestion[] = [
|
||||
{
|
||||
value: "auto",
|
||||
description: "Search patterns are case-insensitive if all characters are lowercase, and case sensitive otherwise (default)."
|
||||
},
|
||||
{
|
||||
value: "yes",
|
||||
description: "Case sensitive search."
|
||||
},
|
||||
{
|
||||
value: "no",
|
||||
description: "Case insensitive search."
|
||||
},
|
||||
}
|
||||
];
|
||||
|
||||
export const archivedModeSuggestions: Suggestion[] = [
|
||||
{
|
||||
value: "yes",
|
||||
description: "Only include results in archived repositories."
|
||||
description: "Include results from archived repositories (default)."
|
||||
},
|
||||
{
|
||||
value: "no",
|
||||
description: "Only include results in non-archived repositories."
|
||||
description: "Exclude results from archived repositories."
|
||||
},
|
||||
{
|
||||
value: "only",
|
||||
description: "Only include results from archived repositories."
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -42,14 +42,18 @@ import { Separator } from "@/components/ui/separator";
|
|||
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
|
||||
import { Toggle } from "@/components/ui/toggle";
|
||||
import { useDomain } from "@/hooks/useDomain";
|
||||
import { KeyboardShortcutHint } from "@/app/components/keyboardShortcutHint";
|
||||
import { createAuditAction } from "@/ee/features/audit/actions";
|
||||
import tailwind from "@/tailwind";
|
||||
import { CaseSensitiveIcon, RegexIcon } from "lucide-react";
|
||||
|
||||
interface SearchBarProps {
|
||||
className?: string;
|
||||
size?: "default" | "sm";
|
||||
defaultQuery?: string;
|
||||
defaults?: {
|
||||
isRegexEnabled?: boolean;
|
||||
isCaseSensitivityEnabled?: boolean;
|
||||
query?: string;
|
||||
}
|
||||
autoFocus?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -91,8 +95,12 @@ const searchBarContainerVariants = cva(
|
|||
export const SearchBar = ({
|
||||
className,
|
||||
size,
|
||||
defaultQuery,
|
||||
autoFocus,
|
||||
defaults: {
|
||||
isRegexEnabled: defaultIsRegexEnabled = false,
|
||||
isCaseSensitivityEnabled: defaultIsCaseSensitivityEnabled = false,
|
||||
query: defaultQuery = "",
|
||||
} = {}
|
||||
}: SearchBarProps) => {
|
||||
const router = useRouter();
|
||||
const domain = useDomain();
|
||||
|
|
@ -102,11 +110,13 @@ export const SearchBar = ({
|
|||
const [isSuggestionsEnabled, setIsSuggestionsEnabled] = useState(false);
|
||||
const [isSuggestionsBoxFocused, setIsSuggestionsBoxFocused] = useState(false);
|
||||
const [isHistorySearchEnabled, setIsHistorySearchEnabled] = useState(false);
|
||||
const [isRegexEnabled, setIsRegexEnabled] = useState(defaultIsRegexEnabled);
|
||||
const [isCaseSensitivityEnabled, setIsCaseSensitivityEnabled] = useState(defaultIsCaseSensitivityEnabled);
|
||||
|
||||
const focusEditor = useCallback(() => editorRef.current?.view?.focus(), []);
|
||||
const focusSuggestionsBox = useCallback(() => suggestionBoxRef.current?.focus(), []);
|
||||
|
||||
const [_query, setQuery] = useState(defaultQuery ?? "");
|
||||
const [_query, setQuery] = useState(defaultQuery);
|
||||
const query = useMemo(() => {
|
||||
// Replace any newlines with spaces to handle
|
||||
// copy & pasting text with newlines.
|
||||
|
|
@ -211,13 +221,15 @@ export const SearchBar = ({
|
|||
metadata: {
|
||||
message: query,
|
||||
},
|
||||
}, domain)
|
||||
})
|
||||
|
||||
const url = createPathWithQueryParams(`/${domain}/search`,
|
||||
[SearchQueryParams.query, query],
|
||||
[SearchQueryParams.isRegexEnabled, isRegexEnabled ? "true" : null],
|
||||
[SearchQueryParams.isCaseSensitivityEnabled, isCaseSensitivityEnabled ? "true" : null],
|
||||
);
|
||||
router.push(url);
|
||||
}, [domain, router]);
|
||||
}, [domain, router, isRegexEnabled, isCaseSensitivityEnabled]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -275,18 +287,40 @@ export const SearchBar = ({
|
|||
indentWithTab={false}
|
||||
autoFocus={autoFocus ?? false}
|
||||
/>
|
||||
<Tooltip
|
||||
delayDuration={100}
|
||||
>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<KeyboardShortcutHint shortcut="/" />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="flex flex-row items-center gap-2">
|
||||
Focus search bar
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<div className="flex flex-row items-center gap-1 ml-1">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Toggle
|
||||
className="h-7 w-7 min-w-7 p-0 cursor-pointer"
|
||||
pressed={isCaseSensitivityEnabled}
|
||||
onPressedChange={setIsCaseSensitivityEnabled}
|
||||
>
|
||||
<CaseSensitiveIcon className="w-4 h-4" />
|
||||
</Toggle>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="flex flex-row items-center gap-2">
|
||||
{isCaseSensitivityEnabled ? "Disable" : "Enable"} case sensitivity
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Toggle
|
||||
className="h-7 w-7 min-w-7 p-0 cursor-pointer"
|
||||
pressed={isRegexEnabled}
|
||||
onPressedChange={setIsRegexEnabled}
|
||||
>
|
||||
<RegexIcon className="w-4 h-4" />
|
||||
</Toggle>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="flex flex-row items-center gap-2">
|
||||
{isRegexEnabled ? "Disable" : "Enable"} regular expressions
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<SearchSuggestionsBox
|
||||
ref={suggestionBoxRef}
|
||||
query={query}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,8 @@ import Fuse from "fuse.js";
|
|||
import { forwardRef, Ref, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
archivedModeSuggestions,
|
||||
caseModeSuggestions,
|
||||
forkModeSuggestions,
|
||||
publicModeSuggestions,
|
||||
visibilityModeSuggestions,
|
||||
} from "./constants";
|
||||
import { IconType } from "react-icons/lib";
|
||||
import { VscFile, VscFilter, VscRepo, VscSymbolMisc } from "react-icons/vsc";
|
||||
|
|
@ -32,9 +31,8 @@ export type SuggestionMode =
|
|||
"archived" |
|
||||
"file" |
|
||||
"language" |
|
||||
"case" |
|
||||
"fork" |
|
||||
"public" |
|
||||
"visibility" |
|
||||
"revision" |
|
||||
"symbol" |
|
||||
"content" |
|
||||
|
|
@ -137,9 +135,9 @@ const SearchSuggestionsBox = forwardRef(({
|
|||
DefaultIcon?: IconType
|
||||
} => {
|
||||
switch (suggestionMode) {
|
||||
case "public":
|
||||
case "visibility":
|
||||
return {
|
||||
list: publicModeSuggestions,
|
||||
list: visibilityModeSuggestions,
|
||||
onSuggestionClicked: createOnSuggestionClickedHandler(),
|
||||
}
|
||||
case "fork":
|
||||
|
|
@ -147,11 +145,6 @@ const SearchSuggestionsBox = forwardRef(({
|
|||
list: forkModeSuggestions,
|
||||
onSuggestionClicked: createOnSuggestionClickedHandler(),
|
||||
}
|
||||
case "case":
|
||||
return {
|
||||
list: caseModeSuggestions,
|
||||
onSuggestionClicked: createOnSuggestionClickedHandler(),
|
||||
}
|
||||
case "archived":
|
||||
return {
|
||||
list: archivedModeSuggestions,
|
||||
|
|
@ -183,7 +176,7 @@ const SearchSuggestionsBox = forwardRef(({
|
|||
case "file":
|
||||
return {
|
||||
list: fileSuggestions,
|
||||
onSuggestionClicked: createOnSuggestionClickedHandler(),
|
||||
onSuggestionClicked: createOnSuggestionClickedHandler({ regexEscaped: true }),
|
||||
isClientSideSearchEnabled: false,
|
||||
DefaultIcon: VscFile,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export const useRefineModeSuggestions = () => {
|
|||
},
|
||||
] : []),
|
||||
{
|
||||
value: SearchPrefix.public,
|
||||
value: SearchPrefix.visibility,
|
||||
description: "Filter on repository visibility."
|
||||
},
|
||||
{
|
||||
|
|
@ -86,10 +86,6 @@ export const useRefineModeSuggestions = () => {
|
|||
value: SearchPrefix.archived,
|
||||
description: "Include results from archived repositories.",
|
||||
},
|
||||
{
|
||||
value: SearchPrefix.case,
|
||||
description: "Control case-sensitivity of search patterns."
|
||||
},
|
||||
{
|
||||
value: SearchPrefix.fork,
|
||||
description: "Include only results from forked repositories."
|
||||
|
|
|
|||
|
|
@ -70,12 +70,6 @@ export const useSuggestionModeMappings = () => {
|
|||
SearchPrefix.archived
|
||||
]
|
||||
},
|
||||
{
|
||||
suggestionMode: "case",
|
||||
prefixes: [
|
||||
SearchPrefix.case
|
||||
]
|
||||
},
|
||||
{
|
||||
suggestionMode: "fork",
|
||||
prefixes: [
|
||||
|
|
@ -83,9 +77,9 @@ export const useSuggestionModeMappings = () => {
|
|||
]
|
||||
},
|
||||
{
|
||||
suggestionMode: "public",
|
||||
suggestionMode: "visibility",
|
||||
prefixes: [
|
||||
SearchPrefix.public
|
||||
SearchPrefix.visibility
|
||||
]
|
||||
},
|
||||
...(isSearchContextsEnabled ? [
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { Suggestion, SuggestionMode } from "./searchSuggestionsBox";
|
|||
import { getRepos, search } from "@/app/api/(client)/client";
|
||||
import { getSearchContexts } from "@/actions";
|
||||
import { useMemo } from "react";
|
||||
import { SearchSymbol } from "@/features/search/types";
|
||||
import { SearchSymbol } from "@/features/search";
|
||||
import { languageMetadataMap } from "@/lib/languageMetadata";
|
||||
import {
|
||||
VscSymbolClass,
|
||||
|
|
@ -55,7 +55,8 @@ export const useSuggestionsData = ({
|
|||
query: `file:${suggestionQuery}`,
|
||||
matches: 15,
|
||||
contextLines: 1,
|
||||
}, domain),
|
||||
source: 'search-bar-file-suggestions'
|
||||
}),
|
||||
select: (data): Suggestion[] => {
|
||||
if (isServiceError(data)) {
|
||||
return [];
|
||||
|
|
@ -75,7 +76,8 @@ export const useSuggestionsData = ({
|
|||
query: `sym:${suggestionQuery.length > 0 ? suggestionQuery : ".*"}`,
|
||||
matches: 15,
|
||||
contextLines: 1,
|
||||
}, domain),
|
||||
source: 'search-bar-symbol-suggestions'
|
||||
}),
|
||||
select: (data): Suggestion[] => {
|
||||
if (isServiceError(data)) {
|
||||
return [];
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export const zoekt = () => {
|
|||
|
||||
// Check for prefixes first
|
||||
// If these match, we return 'keyword'
|
||||
if (stream.match(/(archived:|branch:|b:|rev:|c:|case:|content:|f:|file:|fork:|public:|r:|repo:|regex:|lang:|sym:|t:|type:|context:)/)) {
|
||||
if (stream.match(/(archived:|rev:|content:|f:|file:|fork:|visibility:|r:|repo:|regex:|lang:|sym:|t:|type:|context:)/)) {
|
||||
return t.keyword.toString();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { useCallback, useRef } from "react";
|
|||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { useSyntaxGuide } from "./syntaxGuideProvider";
|
||||
import { CodeSnippet } from "@/app/components/codeSnippet";
|
||||
import { ExternalLinkIcon, RegexIcon } from "lucide-react";
|
||||
|
||||
const LINGUIST_LINK = "https://github.com/github-linguist/linguist/blob/main/lib/linguist/languages.yml";
|
||||
const CTAGS_LINK = "https://ctags.io/";
|
||||
|
|
@ -61,70 +62,92 @@ export const SyntaxReferenceGuide = () => {
|
|||
onOpenChange={handleOpenChange}
|
||||
>
|
||||
<DialogContent
|
||||
className="max-h-[80vh] max-w-[700px] overflow-scroll"
|
||||
className="max-h-[80vh] max-w-[700px] overflow-scroll gap-2"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Syntax Reference Guide</DialogTitle>
|
||||
<DialogTitle>Syntax Reference Guide <Link href="https://docs.sourcebot.dev/docs/features/search/syntax-reference"><ExternalLinkIcon className="inline w-4 h-4 ml-1 mb-1 text-muted-foreground cursor-pointer" /></Link></DialogTitle>
|
||||
<DialogDescription className="text-sm text-foreground">
|
||||
Queries consist of space-seperated regular expressions. Wrapping expressions in <CodeSnippet>{`""`}</CodeSnippet> combines them. By default, a file must have at least one match for each expression to be included.
|
||||
Queries consist of space-separated search patterns that are matched against file contents. A file must have at least one match for each expression to be included. Queries can optionally contain search filters to further refine the search results.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="py-2">Example</TableHead>
|
||||
<TableHead className="py-2">Explanation</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><CodeSnippet>foo</CodeSnippet></TableCell>
|
||||
<TableCell className="py-2">Match files with regex <CodeSnippet>/foo/</CodeSnippet></TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><CodeSnippet>foo bar</CodeSnippet></TableCell>
|
||||
<TableCell className="py-2">Match files with regex <CodeSnippet>/foo/</CodeSnippet> <b>and</b> <CodeSnippet>/bar/</CodeSnippet></TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><CodeSnippet>{`"foo bar"`}</CodeSnippet></TableCell>
|
||||
<TableCell className="py-2">Match files with regex <CodeSnippet>/foo bar/</CodeSnippet></TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<Separator className="my-2"/>
|
||||
<p className="text-sm">
|
||||
{`Multiple expressions can be or'd together with `}<CodeSnippet>or</CodeSnippet>, negated with <CodeSnippet>-</CodeSnippet>, or grouped with <CodeSnippet>()</CodeSnippet>.
|
||||
</p>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="py-2">Example</TableHead>
|
||||
<TableHead className="py-2">Explanation</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><CodeSnippet>foo <Highlight>or</Highlight> bar</CodeSnippet></TableCell>
|
||||
<TableCell className="py-2">Match files with regex <CodeSnippet>/foo/</CodeSnippet> <b>or</b> <CodeSnippet>/bar/</CodeSnippet></TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><CodeSnippet>foo -bar</CodeSnippet></TableCell>
|
||||
<TableCell className="py-2">Match files with regex <CodeSnippet>/foo/</CodeSnippet> but <b>not</b> <CodeSnippet>/bar/</CodeSnippet></TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><CodeSnippet>foo (bar <Highlight>or</Highlight> baz)</CodeSnippet></TableCell>
|
||||
<TableCell className="py-2">Match files with regex <CodeSnippet>/foo/</CodeSnippet> <b>and</b> either <CodeSnippet>/bar/</CodeSnippet> <b>or</b> <CodeSnippet>/baz/</CodeSnippet></TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mt-4 mb-0">Keyword search (default)</h3>
|
||||
<p className="text-sm mb-2 mt-0">
|
||||
Keyword search matches search patterns exactly in file contents. Wrapping search patterns in <CodeSnippet>{`""`}</CodeSnippet> combines them as a single expression.
|
||||
</p>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="py-2">Example</TableHead>
|
||||
<TableHead className="py-2">Explanation</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><CodeSnippet>foo</CodeSnippet></TableCell>
|
||||
<TableCell className="py-2">Match files containing the keyword <CodeSnippet>foo</CodeSnippet></TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><CodeSnippet>foo bar</CodeSnippet></TableCell>
|
||||
<TableCell className="py-2">Match files containing both <CodeSnippet>foo</CodeSnippet> <b>and</b> <CodeSnippet>bar</CodeSnippet></TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><CodeSnippet>{`"foo bar"`}</CodeSnippet></TableCell>
|
||||
<TableCell className="py-2">Match files containing the phrase <CodeSnippet>foo bar</CodeSnippet></TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><CodeSnippet>{'"foo \\"bar\\""'}</CodeSnippet></TableCell>
|
||||
<TableCell className="py-2">Match files containing <CodeSnippet>foo "bar"</CodeSnippet> exactly (escaped quotes)</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<Separator className="my-2"/>
|
||||
<p className="text-sm">
|
||||
Expressions can be prefixed with certain keywords to modify search behavior. Some keywords can be negated using the <CodeSnippet>-</CodeSnippet> prefix.
|
||||
</p>
|
||||
<Separator className="my-4"/>
|
||||
|
||||
<Table>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mt-4 mb-0">Regex search</h3>
|
||||
<p className="text-sm mb-2 mt-0">
|
||||
Toggle the <RegexIcon className="inline w-4 h-4 align-middle mx-0.5 border rounded px-0.5 py-0.5" /> button to interpret search patterns as regular expressions.
|
||||
</p>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="py-2">Example</TableHead>
|
||||
<TableHead className="py-2">Explanation</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><CodeSnippet>foo</CodeSnippet></TableCell>
|
||||
<TableCell className="py-2">Match files with regex <CodeSnippet>/foo/</CodeSnippet></TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><CodeSnippet>foo.*bar</CodeSnippet></TableCell>
|
||||
<TableCell className="py-2">Match files with regex <CodeSnippet>/foo.*bar/</CodeSnippet> (foo followed by any characters, then bar)</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><CodeSnippet>{`^function\\s+\\w+`}</CodeSnippet></TableCell>
|
||||
<TableCell className="py-2">Match files with regex <CodeSnippet>/^function\s+\w+/</CodeSnippet> (function at start of line, followed by whitespace and word characters)</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><CodeSnippet>{`"foo bar"`}</CodeSnippet></TableCell>
|
||||
<TableCell className="py-2">Match files with regex <CodeSnippet>/foo bar/</CodeSnippet>. Quotes are not matched.</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<Separator className="my-4"/>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mt-4 mb-0">Search filters</h3>
|
||||
<p className="text-sm mb-2 mt-0">
|
||||
Search queries (keyword or regex) can include multiple search filters to further refine the search results. Some filters can be negated using the <CodeSnippet>-</CodeSnippet> prefix.
|
||||
</p>
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="py-2">Prefix</TableHead>
|
||||
|
|
@ -219,7 +242,39 @@ export const SyntaxReferenceGuide = () => {
|
|||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<Separator className="my-4"/>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mt-4 mb-0">Boolean operators & grouping</h3>
|
||||
<p className="text-sm mb-2 mt-0">
|
||||
By default, space-seperated expressions are and'd together. Using the <CodeSnippet>or</CodeSnippet> keyword as well as parantheses <CodeSnippet>()</CodeSnippet> can be used to create more complex boolean logic. Parantheses can be negated using the <CodeSnippet>-</CodeSnippet> prefix.
|
||||
</p>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="py-2">Example</TableHead>
|
||||
<TableHead className="py-2">Explanation</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><CodeSnippet>foo <Highlight>or</Highlight> bar</CodeSnippet></TableCell>
|
||||
<TableCell className="py-2">Match files containing <CodeSnippet>foo</CodeSnippet> <b>or</b> <CodeSnippet>bar</CodeSnippet></TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><CodeSnippet>foo (bar <Highlight>or</Highlight> baz)</CodeSnippet></TableCell>
|
||||
<TableCell className="py-2">Match files containing <CodeSnippet>foo</CodeSnippet> <b>and</b> either <CodeSnippet>bar</CodeSnippet> <b>or</b> <CodeSnippet>baz</CodeSnippet>.</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="py-2"><CodeSnippet>-(foo) bar</CodeSnippet></TableCell>
|
||||
<TableCell className="py-2">Match files containing <CodeSnippet>bar</CodeSnippet> <b>and not</b> <CodeSnippet>foo</CodeSnippet>.</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
'use client';
|
||||
|
||||
import { useBrowseNavigation } from "@/app/[domain]/browse/hooks/useBrowseNavigation";
|
||||
import { EditorContextMenu } from "@/app/[domain]/components/editorContextMenu";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { SearchResultChunk } from "@/features/search/types";
|
||||
import { SymbolHoverPopup } from "@/ee/features/codeNav/components/symbolHoverPopup";
|
||||
import { symbolHoverTargetsExtension } from "@/ee/features/codeNav/components/symbolHoverPopup/symbolHoverTargetsExtension";
|
||||
import { useHasEntitlement } from "@/features/entitlements/useHasEntitlement";
|
||||
import { SearchResultChunk } from "@/features/search";
|
||||
import { useCodeMirrorLanguageExtension } from "@/hooks/useCodeMirrorLanguageExtension";
|
||||
import { useCodeMirrorTheme } from "@/hooks/useCodeMirrorTheme";
|
||||
import { useKeymapExtension } from "@/hooks/useKeymapExtension";
|
||||
import { useCodeMirrorLanguageExtension } from "@/hooks/useCodeMirrorLanguageExtension";
|
||||
import { gutterWidthExtension } from "@/lib/extensions/gutterWidthExtension";
|
||||
import { highlightRanges, searchResultHighlightExtension } from "@/lib/extensions/searchResultHighlightExtension";
|
||||
import { search } from "@codemirror/search";
|
||||
|
|
@ -16,15 +20,6 @@ import { Scrollbar } from "@radix-ui/react-scroll-area";
|
|||
import CodeMirror, { ReactCodeMirrorRef, SelectionRange } from '@uiw/react-codemirror';
|
||||
import { ArrowDown, ArrowUp } from "lucide-react";
|
||||
import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useBrowseNavigation } from "@/app/[domain]/browse/hooks/useBrowseNavigation";
|
||||
import { SymbolHoverPopup } from "@/ee/features/codeNav/components/symbolHoverPopup";
|
||||
import { symbolHoverTargetsExtension } from "@/ee/features/codeNav/components/symbolHoverPopup/symbolHoverTargetsExtension";
|
||||
import { useHasEntitlement } from "@/features/entitlements/useHasEntitlement";
|
||||
import { SymbolDefinition } from "@/ee/features/codeNav/components/symbolHoverPopup/useHoveredOverSymbolInfo";
|
||||
import { createAuditAction } from "@/ee/features/audit/actions";
|
||||
import { useDomain } from "@/hooks/useDomain";
|
||||
|
||||
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
||||
|
||||
export interface CodePreviewFile {
|
||||
content: string;
|
||||
|
|
@ -53,7 +48,6 @@ export const CodePreview = ({
|
|||
const [editorRef, setEditorRef] = useState<ReactCodeMirrorRef | null>(null);
|
||||
const { navigateToPath } = useBrowseNavigation();
|
||||
const hasCodeNavEntitlement = useHasEntitlement("code-nav");
|
||||
const domain = useDomain();
|
||||
|
||||
const [gutterWidth, setGutterWidth] = useState(0);
|
||||
const theme = useCodeMirrorTheme();
|
||||
|
|
@ -62,8 +56,6 @@ export const CodePreview = ({
|
|||
const languageExtension = useCodeMirrorLanguageExtension(file?.language ?? '', editorRef?.view);
|
||||
const [currentSelection, setCurrentSelection] = useState<SelectionRange>();
|
||||
|
||||
const captureEvent = useCaptureEvent();
|
||||
|
||||
const extensions = useMemo(() => {
|
||||
return [
|
||||
keymapExtension,
|
||||
|
|
@ -118,81 +110,6 @@ export const CodePreview = ({
|
|||
onSelectedMatchIndexChange((prev) => prev + 1);
|
||||
}, [onSelectedMatchIndexChange]);
|
||||
|
||||
const onGotoDefinition = useCallback((symbolName: string, symbolDefinitions: SymbolDefinition[]) => {
|
||||
captureEvent('wa_goto_definition_pressed', {
|
||||
source: 'preview',
|
||||
});
|
||||
createAuditAction({
|
||||
action: "user.performed_goto_definition",
|
||||
metadata: {
|
||||
message: symbolName,
|
||||
},
|
||||
}, domain)
|
||||
|
||||
if (symbolDefinitions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (symbolDefinitions.length === 1) {
|
||||
const symbolDefinition = symbolDefinitions[0];
|
||||
const { fileName, repoName } = symbolDefinition;
|
||||
|
||||
navigateToPath({
|
||||
repoName,
|
||||
revisionName: file.revision,
|
||||
path: fileName,
|
||||
pathType: 'blob',
|
||||
highlightRange: symbolDefinition.range,
|
||||
})
|
||||
} else {
|
||||
navigateToPath({
|
||||
repoName,
|
||||
revisionName: file.revision,
|
||||
path: file.filepath,
|
||||
pathType: 'blob',
|
||||
setBrowseState: {
|
||||
selectedSymbolInfo: {
|
||||
symbolName,
|
||||
repoName,
|
||||
revisionName: file.revision,
|
||||
language: file.language,
|
||||
},
|
||||
activeExploreMenuTab: "definitions",
|
||||
isBottomPanelCollapsed: false,
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [captureEvent, file.filepath, file.language, file.revision, navigateToPath, repoName, domain]);
|
||||
|
||||
const onFindReferences = useCallback((symbolName: string) => {
|
||||
captureEvent('wa_find_references_pressed', {
|
||||
source: 'preview',
|
||||
});
|
||||
createAuditAction({
|
||||
action: "user.performed_find_references",
|
||||
metadata: {
|
||||
message: symbolName,
|
||||
},
|
||||
}, domain)
|
||||
|
||||
navigateToPath({
|
||||
repoName,
|
||||
revisionName: file.revision,
|
||||
path: file.filepath,
|
||||
pathType: 'blob',
|
||||
setBrowseState: {
|
||||
selectedSymbolInfo: {
|
||||
repoName,
|
||||
symbolName,
|
||||
revisionName: file.revision,
|
||||
language: file.language,
|
||||
},
|
||||
activeExploreMenuTab: "references",
|
||||
isBottomPanelCollapsed: false,
|
||||
}
|
||||
})
|
||||
}, [captureEvent, file.filepath, file.language, file.revision, navigateToPath, repoName, domain]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex flex-row bg-accent items-center justify-between pr-3 py-0.5 mt-7">
|
||||
|
|
@ -289,11 +206,12 @@ export const CodePreview = ({
|
|||
|
||||
{editorRef && hasCodeNavEntitlement && (
|
||||
<SymbolHoverPopup
|
||||
source="preview"
|
||||
editorRef={editorRef}
|
||||
language={file.language}
|
||||
revisionName={file.revision}
|
||||
onFindReferences={onFindReferences}
|
||||
onGotoDefinition={onGotoDefinition}
|
||||
fileName={file.filepath}
|
||||
repoName={repoName}
|
||||
/>
|
||||
)}
|
||||
</CodeMirror>
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { CodePreview } from "./codePreview";
|
||||
import { SearchResultFile } from "@/features/search/types";
|
||||
import { SearchResultFile } from "@/features/search";
|
||||
import { SymbolIcon } from "@radix-ui/react-icons";
|
||||
import { SetStateAction, Dispatch, useMemo } from "react";
|
||||
import { getFileSource } from "@/features/search/fileSourceApi";
|
||||
import { unwrapServiceError } from "@/lib/utils";
|
||||
import { getFileSource } from "@/app/api/(client)/client";
|
||||
|
||||
interface CodePreviewPanelProps {
|
||||
previewedFile: SearchResultFile;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { compareEntries, Entry } from "./entry";
|
|||
import { Input } from "@/components/ui/input";
|
||||
import Fuse from "fuse.js";
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
interface FilterProps {
|
||||
title: string,
|
||||
|
|
@ -12,6 +13,7 @@ interface FilterProps {
|
|||
entries: Entry[],
|
||||
onEntryClicked: (key: string) => void,
|
||||
className?: string,
|
||||
isStreaming: boolean,
|
||||
}
|
||||
|
||||
export const Filter = ({
|
||||
|
|
@ -20,6 +22,7 @@ export const Filter = ({
|
|||
entries,
|
||||
onEntryClicked,
|
||||
className,
|
||||
isStreaming,
|
||||
}: FilterProps) => {
|
||||
const [searchFilter, setSearchFilter] = useState<string>("");
|
||||
|
||||
|
|
@ -43,27 +46,34 @@ export const Filter = ({
|
|||
className
|
||||
)}>
|
||||
<h2 className="text-sm font-semibold">{title}</h2>
|
||||
<div className="pr-1">
|
||||
<Input
|
||||
placeholder={searchPlaceholder}
|
||||
className="h-8"
|
||||
onChange={(event) => setSearchFilter(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex flex-col gap-0.5 text-sm overflow-scroll no-scrollbar"
|
||||
>
|
||||
{filteredEntries
|
||||
.sort((entryA, entryB) => compareEntries(entryB, entryA))
|
||||
.map((entry) => (
|
||||
<Entry
|
||||
key={entry.key}
|
||||
entry={entry}
|
||||
onClicked={() => onEntryClicked(entry.key)}
|
||||
{(isStreaming && entries.length === 0) ? (
|
||||
<Skeleton className="h-12 w-full" />
|
||||
) : (
|
||||
<>
|
||||
<div className="pr-1">
|
||||
<Input
|
||||
placeholder={searchPlaceholder}
|
||||
className="h-8"
|
||||
onChange={(event) => setSearchFilter(event.target.value)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex flex-col gap-0.5 text-sm overflow-scroll no-scrollbar"
|
||||
>
|
||||
{filteredEntries
|
||||
.sort((entryA, entryB) => compareEntries(entryB, entryA))
|
||||
.map((entry) => (
|
||||
<Entry
|
||||
key={entry.key}
|
||||
entry={entry}
|
||||
onClicked={() => onEntryClicked(entry.key)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import { FileIcon } from "@/components/ui/fileIcon";
|
||||
import { RepositoryInfo, SearchResultFile } from "@/features/search/types";
|
||||
import { RepositoryInfo, SearchResultFile } from "@/features/search";
|
||||
import { cn, getCodeHostInfoForRepo } from "@/lib/utils";
|
||||
import { LaptopIcon } from "@radix-ui/react-icons";
|
||||
import Image from "next/image";
|
||||
|
|
@ -15,6 +15,8 @@ import { useGetSelectedFromQuery } from "./useGetSelectedFromQuery";
|
|||
interface FilePanelProps {
|
||||
matches: SearchResultFile[];
|
||||
repoInfo: Record<number, RepositoryInfo>;
|
||||
onFilterChange?: () => void;
|
||||
isStreaming: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -31,10 +33,14 @@ interface FilePanelProps {
|
|||
*
|
||||
* @param matches - Array of search result files to filter
|
||||
* @param repoInfo - Information about repositories including their display names and icons
|
||||
* @param onFilterChange - Optional callback that is called whenever a filter is applied or removed
|
||||
* @param isStreaming - Whether the search is streaming
|
||||
*/
|
||||
export const FilterPanel = ({
|
||||
matches,
|
||||
repoInfo,
|
||||
onFilterChange,
|
||||
isStreaming,
|
||||
}: FilePanelProps) => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
|
@ -148,9 +154,11 @@ export const FilterPanel = ({
|
|||
|
||||
if (newParams.toString() !== searchParams.toString()) {
|
||||
router.replace(`?${newParams.toString()}`, { scroll: false });
|
||||
onFilterChange?.();
|
||||
}
|
||||
}}
|
||||
className="max-h-[50%]"
|
||||
isStreaming={isStreaming}
|
||||
/>
|
||||
<Filter
|
||||
title="Filter By Language"
|
||||
|
|
@ -170,9 +178,11 @@ export const FilterPanel = ({
|
|||
|
||||
if (newParams.toString() !== searchParams.toString()) {
|
||||
router.replace(`?${newParams.toString()}`, { scroll: false });
|
||||
onFilterChange?.();
|
||||
}
|
||||
}}
|
||||
className="overflow-auto"
|
||||
isStreaming={isStreaming}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { SearchResultFile } from "@/features/search/types";
|
||||
import { SearchResultFile } from "@/features/search";
|
||||
import { useMemo } from "react";
|
||||
import { useGetSelectedFromQuery } from "./useGetSelectedFromQuery";
|
||||
|
||||
|
|
|
|||
|
|
@ -11,76 +11,76 @@ import {
|
|||
} from "@/components/ui/resizable";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { RepositoryInfo, SearchResultFile, SearchStats } from "@/features/search/types";
|
||||
import { RepositoryInfo, SearchResultFile, SearchStats } from "@/features/search";
|
||||
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
||||
import { useDomain } from "@/hooks/useDomain";
|
||||
import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam";
|
||||
import { useSearchHistory } from "@/hooks/useSearchHistory";
|
||||
import { SearchQueryParams } from "@/lib/types";
|
||||
import { createPathWithQueryParams, measure, unwrapServiceError } from "@/lib/utils";
|
||||
import { InfoCircledIcon, SymbolIcon } from "@radix-ui/react-icons";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { createPathWithQueryParams } from "@/lib/utils";
|
||||
import { InfoCircledIcon } from "@radix-ui/react-icons";
|
||||
import { useLocalStorage } from "@uidotdev/usehooks";
|
||||
import { AlertTriangleIcon, BugIcon, FilterIcon } from "lucide-react";
|
||||
import { AlertTriangleIcon, BugIcon, FilterIcon, RefreshCwIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { ImperativePanelHandle } from "react-resizable-panels";
|
||||
import { search } from "../../../api/(client)/client";
|
||||
import { CopyIconButton } from "../../components/copyIconButton";
|
||||
import { SearchBar } from "../../components/searchBar";
|
||||
import { TopBar } from "../../components/topBar";
|
||||
import { useStreamedSearch } from "../useStreamedSearch";
|
||||
import { CodePreviewPanel } from "./codePreviewPanel";
|
||||
import { FilterPanel } from "./filterPanel";
|
||||
import { useFilteredMatches } from "./filterPanel/useFilterMatches";
|
||||
import { SearchResultsPanel } from "./searchResultsPanel";
|
||||
import { SearchResultsPanel, SearchResultsPanelHandle } from "./searchResultsPanel";
|
||||
import { ServiceErrorException } from "@/lib/serviceError";
|
||||
|
||||
interface SearchResultsPageProps {
|
||||
searchQuery: string;
|
||||
defaultMaxMatchCount: number;
|
||||
isRegexEnabled: boolean;
|
||||
isCaseSensitivityEnabled: boolean;
|
||||
}
|
||||
|
||||
export const SearchResultsPage = ({
|
||||
searchQuery,
|
||||
defaultMaxMatchCount,
|
||||
isRegexEnabled,
|
||||
isCaseSensitivityEnabled,
|
||||
}: SearchResultsPageProps) => {
|
||||
const router = useRouter();
|
||||
const { setSearchHistory } = useSearchHistory();
|
||||
const captureEvent = useCaptureEvent();
|
||||
const domain = useDomain();
|
||||
const { toast } = useToast();
|
||||
const captureEvent = useCaptureEvent();
|
||||
|
||||
// Encodes the number of matches to return in the search response.
|
||||
const _maxMatchCount = parseInt(useNonEmptyQueryParam(SearchQueryParams.matches) ?? `${defaultMaxMatchCount}`);
|
||||
const maxMatchCount = isNaN(_maxMatchCount) ? defaultMaxMatchCount : _maxMatchCount;
|
||||
|
||||
const {
|
||||
data: searchResponse,
|
||||
isPending: isSearchPending,
|
||||
isFetching: isFetching,
|
||||
error
|
||||
} = useQuery({
|
||||
queryKey: ["search", searchQuery, maxMatchCount],
|
||||
queryFn: () => measure(() => unwrapServiceError(search({
|
||||
query: searchQuery,
|
||||
matches: maxMatchCount,
|
||||
contextLines: 3,
|
||||
whole: false,
|
||||
}, domain)), "client.search"),
|
||||
select: ({ data, durationMs }) => ({
|
||||
...data,
|
||||
totalClientSearchDurationMs: durationMs,
|
||||
}),
|
||||
enabled: searchQuery.length > 0,
|
||||
refetchOnWindowFocus: false,
|
||||
retry: false,
|
||||
staleTime: 0,
|
||||
error,
|
||||
files,
|
||||
repoInfo,
|
||||
timeToSearchCompletionMs,
|
||||
timeToFirstSearchResultMs,
|
||||
isStreaming,
|
||||
numMatches,
|
||||
isExhaustive,
|
||||
stats,
|
||||
} = useStreamedSearch({
|
||||
query: searchQuery,
|
||||
matches: maxMatchCount,
|
||||
contextLines: 3,
|
||||
whole: false,
|
||||
isRegexEnabled,
|
||||
isCaseSensitivityEnabled,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
toast({
|
||||
description: `❌ Search failed. Reason: ${error.message}`,
|
||||
description: `❌ Search failed. Reason: ${error instanceof ServiceErrorException ? error.serviceError.message : error.message}`,
|
||||
});
|
||||
}
|
||||
}, [error, toast]);
|
||||
|
|
@ -103,38 +103,51 @@ export const SearchResultsPage = ({
|
|||
}, [searchQuery, setSearchHistory]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!searchResponse) {
|
||||
if (isStreaming || !stats) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileLanguages = searchResponse.files?.map(file => file.language) || [];
|
||||
const fileLanguages = files.map(file => file.language) || [];
|
||||
|
||||
console.debug('timeToFirstSearchResultMs:', timeToFirstSearchResultMs);
|
||||
console.debug('timeToSearchCompletionMs:', timeToSearchCompletionMs);
|
||||
|
||||
captureEvent("search_finished", {
|
||||
durationMs: searchResponse.totalClientSearchDurationMs,
|
||||
fileCount: searchResponse.stats.fileCount,
|
||||
matchCount: searchResponse.stats.totalMatchCount,
|
||||
actualMatchCount: searchResponse.stats.actualMatchCount,
|
||||
filesSkipped: searchResponse.stats.filesSkipped,
|
||||
contentBytesLoaded: searchResponse.stats.contentBytesLoaded,
|
||||
indexBytesLoaded: searchResponse.stats.indexBytesLoaded,
|
||||
crashes: searchResponse.stats.crashes,
|
||||
shardFilesConsidered: searchResponse.stats.shardFilesConsidered,
|
||||
filesConsidered: searchResponse.stats.filesConsidered,
|
||||
filesLoaded: searchResponse.stats.filesLoaded,
|
||||
shardsScanned: searchResponse.stats.shardsScanned,
|
||||
shardsSkipped: searchResponse.stats.shardsSkipped,
|
||||
shardsSkippedFilter: searchResponse.stats.shardsSkippedFilter,
|
||||
ngramMatches: searchResponse.stats.ngramMatches,
|
||||
ngramLookups: searchResponse.stats.ngramLookups,
|
||||
wait: searchResponse.stats.wait,
|
||||
matchTreeConstruction: searchResponse.stats.matchTreeConstruction,
|
||||
matchTreeSearch: searchResponse.stats.matchTreeSearch,
|
||||
regexpsConsidered: searchResponse.stats.regexpsConsidered,
|
||||
flushReason: searchResponse.stats.flushReason,
|
||||
durationMs: timeToSearchCompletionMs,
|
||||
timeToSearchCompletionMs,
|
||||
timeToFirstSearchResultMs,
|
||||
fileCount: stats.fileCount,
|
||||
matchCount: stats.totalMatchCount,
|
||||
actualMatchCount: stats.actualMatchCount,
|
||||
filesSkipped: stats.filesSkipped,
|
||||
contentBytesLoaded: stats.contentBytesLoaded,
|
||||
indexBytesLoaded: stats.indexBytesLoaded,
|
||||
crashes: stats.crashes,
|
||||
shardFilesConsidered: stats.shardFilesConsidered,
|
||||
filesConsidered: stats.filesConsidered,
|
||||
filesLoaded: stats.filesLoaded,
|
||||
shardsScanned: stats.shardsScanned,
|
||||
shardsSkipped: stats.shardsSkipped,
|
||||
shardsSkippedFilter: stats.shardsSkippedFilter,
|
||||
ngramMatches: stats.ngramMatches,
|
||||
ngramLookups: stats.ngramLookups,
|
||||
wait: stats.wait,
|
||||
matchTreeConstruction: stats.matchTreeConstruction,
|
||||
matchTreeSearch: stats.matchTreeSearch,
|
||||
regexpsConsidered: stats.regexpsConsidered,
|
||||
flushReason: stats.flushReason,
|
||||
fileLanguages,
|
||||
isSearchExhaustive: isExhaustive,
|
||||
});
|
||||
}, [captureEvent, searchQuery, searchResponse]);
|
||||
|
||||
}, [
|
||||
captureEvent,
|
||||
files,
|
||||
isStreaming,
|
||||
isExhaustive,
|
||||
stats,
|
||||
timeToSearchCompletionMs,
|
||||
timeToFirstSearchResultMs,
|
||||
]);
|
||||
|
||||
const onLoadMoreResults = useCallback(() => {
|
||||
const url = createPathWithQueryParams(`/${domain}/search`,
|
||||
|
|
@ -144,6 +157,13 @@ export const SearchResultsPage = ({
|
|||
router.push(url);
|
||||
}, [maxMatchCount, router, searchQuery, domain]);
|
||||
|
||||
// Look for any files that are not on the default branch.
|
||||
const isBranchFilteringEnabled = useMemo(() => {
|
||||
return files.some((file) => {
|
||||
return file.branches?.some((branch) => branch !== 'HEAD') ?? false;
|
||||
});
|
||||
}, [files]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen overflow-clip">
|
||||
{/* TopBar */}
|
||||
|
|
@ -152,32 +172,32 @@ export const SearchResultsPage = ({
|
|||
>
|
||||
<SearchBar
|
||||
size="sm"
|
||||
defaultQuery={searchQuery}
|
||||
defaults={{
|
||||
isRegexEnabled,
|
||||
isCaseSensitivityEnabled,
|
||||
query: searchQuery,
|
||||
}}
|
||||
className="w-full"
|
||||
/>
|
||||
</TopBar>
|
||||
|
||||
{(isSearchPending || isFetching) ? (
|
||||
<div className="flex flex-col items-center justify-center h-full gap-2">
|
||||
<SymbolIcon className="h-6 w-6 animate-spin" />
|
||||
<p className="font-semibold text-center">Searching...</p>
|
||||
</div>
|
||||
) : error ? (
|
||||
{error ? (
|
||||
<div className="flex flex-col items-center justify-center h-full gap-2">
|
||||
<AlertTriangleIcon className="h-6 w-6" />
|
||||
<p className="font-semibold text-center">Failed to search</p>
|
||||
<p className="text-sm text-center">{error.message}</p>
|
||||
<p className="text-sm text-center">{error instanceof ServiceErrorException ? error.serviceError.message : error.message}</p>
|
||||
</div>
|
||||
) : (
|
||||
<PanelGroup
|
||||
fileMatches={searchResponse.files}
|
||||
isMoreResultsButtonVisible={searchResponse.isSearchExhaustive === false}
|
||||
fileMatches={files}
|
||||
onLoadMoreResults={onLoadMoreResults}
|
||||
isBranchFilteringEnabled={searchResponse.isBranchFilteringEnabled}
|
||||
repoInfo={searchResponse.repositoryInfo}
|
||||
searchDurationMs={searchResponse.totalClientSearchDurationMs}
|
||||
numMatches={searchResponse.stats.actualMatchCount}
|
||||
searchStats={searchResponse.stats}
|
||||
numMatches={numMatches}
|
||||
repoInfo={repoInfo}
|
||||
searchDurationMs={timeToSearchCompletionMs}
|
||||
isStreaming={isStreaming}
|
||||
searchStats={stats}
|
||||
isMoreResultsButtonVisible={!isExhaustive}
|
||||
isBranchFilteringEnabled={isBranchFilteringEnabled}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -186,10 +206,11 @@ export const SearchResultsPage = ({
|
|||
|
||||
interface PanelGroupProps {
|
||||
fileMatches: SearchResultFile[];
|
||||
isMoreResultsButtonVisible?: boolean;
|
||||
onLoadMoreResults: () => void;
|
||||
isStreaming: boolean;
|
||||
isMoreResultsButtonVisible?: boolean;
|
||||
isBranchFilteringEnabled: boolean;
|
||||
repoInfo: RepositoryInfo[];
|
||||
repoInfo: Record<number, RepositoryInfo>;
|
||||
searchDurationMs: number;
|
||||
numMatches: number;
|
||||
searchStats?: SearchStats;
|
||||
|
|
@ -198,9 +219,10 @@ interface PanelGroupProps {
|
|||
const PanelGroup = ({
|
||||
fileMatches,
|
||||
isMoreResultsButtonVisible,
|
||||
isStreaming,
|
||||
onLoadMoreResults,
|
||||
isBranchFilteringEnabled,
|
||||
repoInfo: _repoInfo,
|
||||
repoInfo,
|
||||
searchDurationMs: _searchDurationMs,
|
||||
numMatches,
|
||||
searchStats,
|
||||
|
|
@ -208,6 +230,7 @@ const PanelGroup = ({
|
|||
const [previewedFile, setPreviewedFile] = useState<SearchResultFile | undefined>(undefined);
|
||||
const filteredFileMatches = useFilteredMatches(fileMatches);
|
||||
const filterPanelRef = useRef<ImperativePanelHandle>(null);
|
||||
const searchResultsPanelRef = useRef<SearchResultsPanelHandle>(null);
|
||||
const [selectedMatchIndex, setSelectedMatchIndex] = useState(0);
|
||||
|
||||
const [isFilterPanelCollapsed, setIsFilterPanelCollapsed] = useLocalStorage('isFilterPanelCollapsed', false);
|
||||
|
|
@ -228,13 +251,6 @@ const PanelGroup = ({
|
|||
return Math.round(_searchDurationMs);
|
||||
}, [_searchDurationMs]);
|
||||
|
||||
const repoInfo = useMemo(() => {
|
||||
return _repoInfo.reduce((acc, repo) => {
|
||||
acc[repo.id] = repo;
|
||||
return acc;
|
||||
}, {} as Record<number, RepositoryInfo>);
|
||||
}, [_repoInfo]);
|
||||
|
||||
return (
|
||||
<ResizablePanelGroup
|
||||
direction="horizontal"
|
||||
|
|
@ -255,6 +271,10 @@ const PanelGroup = ({
|
|||
<FilterPanel
|
||||
matches={fileMatches}
|
||||
repoInfo={repoInfo}
|
||||
isStreaming={isStreaming}
|
||||
onFilterChange={() => {
|
||||
searchResultsPanelRef.current?.resetScroll();
|
||||
}}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
{isFilterPanelCollapsed && (
|
||||
|
|
@ -291,45 +311,58 @@ const PanelGroup = ({
|
|||
order={2}
|
||||
>
|
||||
<div className="py-1 px-2 flex flex-row items-center">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InfoCircledIcon className="w-4 h-4 mr-2" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" className="flex flex-col items-start gap-2 p-4">
|
||||
<div className="flex flex-row items-center w-full">
|
||||
<BugIcon className="w-4 h-4 mr-1.5" />
|
||||
<p className="text-md font-medium">Search stats for nerds</p>
|
||||
<CopyIconButton
|
||||
onCopy={() => {
|
||||
navigator.clipboard.writeText(JSON.stringify(searchStats, null, 2));
|
||||
return true;
|
||||
}}
|
||||
className="ml-auto"
|
||||
/>
|
||||
</div>
|
||||
<CodeSnippet renderNewlines>
|
||||
{JSON.stringify(searchStats, null, 2)}
|
||||
</CodeSnippet>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{
|
||||
fileMatches.length > 0 ? (
|
||||
<p className="text-sm font-medium">{`[${searchDurationMs} ms] Found ${numMatches} matches in ${fileMatches.length} ${fileMatches.length > 1 ? 'files' : 'file'}`}</p>
|
||||
) : (
|
||||
<p className="text-sm font-medium">No results</p>
|
||||
)
|
||||
}
|
||||
{isMoreResultsButtonVisible && (
|
||||
<div
|
||||
className="cursor-pointer text-blue-500 text-sm hover:underline ml-4"
|
||||
onClick={onLoadMoreResults}
|
||||
>
|
||||
(load more)
|
||||
</div>
|
||||
{isStreaming ? (
|
||||
<>
|
||||
<RefreshCwIcon className="h-4 w-4 animate-spin mr-2" />
|
||||
<p className="text-sm font-medium mr-1">Searching...</p>
|
||||
{numMatches > 0 && (
|
||||
<p className="text-sm font-medium">{`Found ${numMatches} matches in ${fileMatches.length} ${fileMatches.length > 1 ? 'files' : 'file'}`}</p>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<InfoCircledIcon className="w-4 h-4 mr-2" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" className="flex flex-col items-start gap-2 p-4">
|
||||
<div className="flex flex-row items-center w-full">
|
||||
<BugIcon className="w-4 h-4 mr-1.5" />
|
||||
<p className="text-md font-medium">Search stats for nerds</p>
|
||||
<CopyIconButton
|
||||
onCopy={() => {
|
||||
navigator.clipboard.writeText(JSON.stringify(searchStats, null, 2));
|
||||
return true;
|
||||
}}
|
||||
className="ml-auto"
|
||||
/>
|
||||
</div>
|
||||
<CodeSnippet renderNewlines>
|
||||
{JSON.stringify(searchStats, null, 2)}
|
||||
</CodeSnippet>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{
|
||||
fileMatches.length > 0 ? (
|
||||
<p className="text-sm font-medium">{`[${searchDurationMs} ms] Found ${numMatches} matches in ${fileMatches.length} ${fileMatches.length > 1 ? 'files' : 'file'}`}</p>
|
||||
) : (
|
||||
<p className="text-sm font-medium">No results</p>
|
||||
)
|
||||
}
|
||||
{isMoreResultsButtonVisible && (
|
||||
<div
|
||||
className="cursor-pointer text-blue-500 text-sm hover:underline ml-4"
|
||||
onClick={onLoadMoreResults}
|
||||
>
|
||||
(load more)
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{filteredFileMatches.length > 0 ? (
|
||||
<SearchResultsPanel
|
||||
ref={searchResultsPanelRef}
|
||||
fileMatches={filteredFileMatches}
|
||||
onOpenFilePreview={(fileMatch, matchIndex) => {
|
||||
setSelectedMatchIndex(matchIndex ?? 0);
|
||||
|
|
@ -340,6 +373,11 @@ const PanelGroup = ({
|
|||
isBranchFilteringEnabled={isBranchFilteringEnabled}
|
||||
repoInfo={repoInfo}
|
||||
/>
|
||||
) : isStreaming ? (
|
||||
<div className="flex flex-col items-center justify-center h-full gap-2">
|
||||
<RefreshCwIcon className="h-6 w-6 animate-spin" />
|
||||
<p className="font-semibold text-center">Searching...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<p className="text-sm text-muted-foreground">No results found</p>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { SearchResultFile, SearchResultChunk } from "@/features/search/types";
|
||||
import { SearchResultFile, SearchResultChunk } from "@/features/search";
|
||||
import { LightweightCodeHighlighter } from "@/app/[domain]/components/lightweightCodeHighlighter";
|
||||
import Link from "next/link";
|
||||
import { getBrowsePath } from "@/app/[domain]/browse/hooks/utils";
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { Separator } from "@/components/ui/separator";
|
|||
import { DoubleArrowDownIcon, DoubleArrowUpIcon } from "@radix-ui/react-icons";
|
||||
import { useMemo } from "react";
|
||||
import { FileMatch } from "./fileMatch";
|
||||
import { RepositoryInfo, SearchResultFile } from "@/features/search/types";
|
||||
import { RepositoryInfo, SearchResultFile } from "@/features/search";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export const MAX_MATCHES_TO_PREVIEW = 3;
|
||||
|
|
@ -75,7 +75,7 @@ export const FileMatchContainer = ({
|
|||
}
|
||||
|
||||
return `${branches[0]}${branches.length > 1 ? ` +${branches.length - 1}` : ''}`;
|
||||
}, [isBranchFilteringEnabled, branches]);
|
||||
}, [branches, isBranchFilteringEnabled]);
|
||||
|
||||
const repo = useMemo(() => {
|
||||
return repoInfo[file.repositoryId];
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
'use client';
|
||||
|
||||
import { RepositoryInfo, SearchResultFile } from "@/features/search/types";
|
||||
import { FileMatchContainer, MAX_MATCHES_TO_PREVIEW } from "./fileMatchContainer";
|
||||
import { RepositoryInfo, SearchResultFile } from "@/features/search";
|
||||
import { useVirtualizer, VirtualItem } from "@tanstack/react-virtual";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useDebounce, usePrevious } from "@uidotdev/usehooks";
|
||||
import { useDebounce } from "@uidotdev/usehooks";
|
||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from "react";
|
||||
import { useMap } from "usehooks-ts";
|
||||
import { FileMatchContainer, MAX_MATCHES_TO_PREVIEW } from "./fileMatchContainer";
|
||||
|
||||
interface SearchResultsPanelProps {
|
||||
fileMatches: SearchResultFile[];
|
||||
|
|
@ -15,6 +16,10 @@ interface SearchResultsPanelProps {
|
|||
repoInfo: Record<number, RepositoryInfo>;
|
||||
}
|
||||
|
||||
export interface SearchResultsPanelHandle {
|
||||
resetScroll: () => void;
|
||||
}
|
||||
|
||||
const ESTIMATED_LINE_HEIGHT_PX = 20;
|
||||
const ESTIMATED_NUMBER_OF_LINES_PER_CODE_CELL = 10;
|
||||
const ESTIMATED_MATCH_CONTAINER_HEIGHT_PX = 30;
|
||||
|
|
@ -22,17 +27,25 @@ const ESTIMATED_MATCH_CONTAINER_HEIGHT_PX = 30;
|
|||
type ScrollHistoryState = {
|
||||
scrollOffset?: number;
|
||||
measurementsCache?: VirtualItem[];
|
||||
showAllMatchesStates?: boolean[];
|
||||
showAllMatchesMap?: [string, boolean][];
|
||||
}
|
||||
|
||||
export const SearchResultsPanel = ({
|
||||
/**
|
||||
* Unique key for a given file match. Used to store the "show all matches" state for a
|
||||
* given file match.
|
||||
*/
|
||||
const getFileMatchKey = (fileMatch: SearchResultFile) => {
|
||||
return `${fileMatch.repository}-${fileMatch.fileName.text}`;
|
||||
}
|
||||
|
||||
export const SearchResultsPanel = forwardRef<SearchResultsPanelHandle, SearchResultsPanelProps>(({
|
||||
fileMatches,
|
||||
onOpenFilePreview,
|
||||
isLoadMoreButtonVisible,
|
||||
onLoadMoreButtonClicked,
|
||||
isBranchFilteringEnabled,
|
||||
repoInfo,
|
||||
}: SearchResultsPanelProps) => {
|
||||
}, ref) => {
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Restore the scroll offset, measurements cache, and other state from the history
|
||||
|
|
@ -42,17 +55,17 @@ export const SearchResultsPanel = ({
|
|||
const {
|
||||
scrollOffset: restoreOffset,
|
||||
measurementsCache: restoreMeasurementsCache,
|
||||
showAllMatchesStates: restoreShowAllMatchesStates,
|
||||
} = history.state as ScrollHistoryState;
|
||||
showAllMatchesMap: restoreShowAllMatchesStates,
|
||||
} = (history.state ?? {}) as ScrollHistoryState;
|
||||
|
||||
const [showAllMatchesStates, setShowAllMatchesStates] = useState(restoreShowAllMatchesStates || Array(fileMatches.length).fill(false));
|
||||
const [showAllMatchesMap, showAllMatchesActions] = useMap<string, boolean>(restoreShowAllMatchesStates || []);
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: fileMatches.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: (index) => {
|
||||
const fileMatch = fileMatches[index];
|
||||
const showAllMatches = showAllMatchesStates[index];
|
||||
const showAllMatches = showAllMatchesMap.get(getFileMatchKey(fileMatch));
|
||||
|
||||
// Quick guesstimation ;) This needs to be quick since the virtualizer will
|
||||
// run this upfront for all items in the list.
|
||||
|
|
@ -73,38 +86,33 @@ export const SearchResultsPanel = ({
|
|||
debug: false,
|
||||
});
|
||||
|
||||
// When the number of file matches changes, we need to reset our scroll state.
|
||||
const prevFileMatches = usePrevious(fileMatches);
|
||||
useEffect(() => {
|
||||
if (!prevFileMatches) {
|
||||
return;
|
||||
}
|
||||
const resetScroll = useCallback(() => {
|
||||
virtualizer.scrollToIndex(0);
|
||||
}, [virtualizer]);
|
||||
|
||||
// Expose the resetScroll function to parent components
|
||||
useImperativeHandle(ref, () => ({
|
||||
resetScroll,
|
||||
}), [resetScroll]);
|
||||
|
||||
if (prevFileMatches.length !== fileMatches.length) {
|
||||
setShowAllMatchesStates(Array(fileMatches.length).fill(false));
|
||||
virtualizer.scrollToIndex(0);
|
||||
}
|
||||
}, [fileMatches.length, prevFileMatches, virtualizer]);
|
||||
|
||||
// Save the scroll state to the history stack.
|
||||
const debouncedScrollOffset = useDebounce(virtualizer.scrollOffset, 100);
|
||||
const debouncedScrollOffset = useDebounce(virtualizer.scrollOffset, 500);
|
||||
useEffect(() => {
|
||||
history.replaceState(
|
||||
{
|
||||
scrollOffset: debouncedScrollOffset ?? undefined,
|
||||
measurementsCache: virtualizer.measurementsCache,
|
||||
showAllMatchesStates,
|
||||
showAllMatchesMap: Array.from(showAllMatchesMap.entries()),
|
||||
} satisfies ScrollHistoryState,
|
||||
'',
|
||||
window.location.href
|
||||
);
|
||||
}, [debouncedScrollOffset, virtualizer.measurementsCache, showAllMatchesStates]);
|
||||
}, [debouncedScrollOffset, virtualizer.measurementsCache, showAllMatchesMap]);
|
||||
|
||||
const onShowAllMatchesButtonClicked = useCallback((index: number) => {
|
||||
const states = [...showAllMatchesStates];
|
||||
const wasShown = states[index];
|
||||
states[index] = !wasShown;
|
||||
setShowAllMatchesStates(states);
|
||||
const onShowAllMatchesButtonClicked = useCallback((fileMatchKey: string, index: number) => {
|
||||
const wasShown = showAllMatchesMap.get(fileMatchKey) ?? false;
|
||||
showAllMatchesActions.set(fileMatchKey, !wasShown);
|
||||
|
||||
// When collapsing, scroll to the top of the file match container. This ensures
|
||||
// that the focused "show fewer matches" button is visible.
|
||||
|
|
@ -113,7 +121,7 @@ export const SearchResultsPanel = ({
|
|||
align: 'start'
|
||||
});
|
||||
}
|
||||
}, [showAllMatchesStates, virtualizer]);
|
||||
}, [showAllMatchesActions, showAllMatchesMap, virtualizer]);
|
||||
|
||||
|
||||
return (
|
||||
|
|
@ -153,9 +161,9 @@ export const SearchResultsPanel = ({
|
|||
onOpenFilePreview={(matchIndex) => {
|
||||
onOpenFilePreview(file, matchIndex);
|
||||
}}
|
||||
showAllMatches={showAllMatchesStates[virtualRow.index]}
|
||||
showAllMatches={showAllMatchesMap.get(getFileMatchKey(file)) ?? false}
|
||||
onShowAllMatchesButtonClicked={() => {
|
||||
onShowAllMatchesButtonClicked(virtualRow.index);
|
||||
onShowAllMatchesButtonClicked(getFileMatchKey(file), virtualRow.index);
|
||||
}}
|
||||
isBranchFilteringEnabled={isBranchFilteringEnabled}
|
||||
repoInfo={repoInfo}
|
||||
|
|
@ -177,4 +185,6 @@ export const SearchResultsPanel = ({
|
|||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
SearchResultsPanel.displayName = 'SearchResultsPanel';
|
||||
|
|
|
|||
|
|
@ -4,13 +4,19 @@ import { SearchResultsPage } from "./components/searchResultsPage";
|
|||
|
||||
interface SearchPageProps {
|
||||
params: Promise<{ domain: string }>;
|
||||
searchParams: Promise<{ query?: string }>;
|
||||
searchParams: Promise<{
|
||||
query?: string;
|
||||
isRegexEnabled?: "true" | "false";
|
||||
isCaseSensitivityEnabled?: "true" | "false";
|
||||
}>;
|
||||
}
|
||||
|
||||
export default async function SearchPage(props: SearchPageProps) {
|
||||
const { domain } = await props.params;
|
||||
const searchParams = await props.searchParams;
|
||||
const query = searchParams?.query;
|
||||
const isRegexEnabled = searchParams?.isRegexEnabled === "true";
|
||||
const isCaseSensitivityEnabled = searchParams?.isCaseSensitivityEnabled === "true";
|
||||
|
||||
if (query === undefined || query.length === 0) {
|
||||
return <SearchLandingPage domain={domain} />
|
||||
|
|
@ -20,6 +26,8 @@ export default async function SearchPage(props: SearchPageProps) {
|
|||
<SearchResultsPage
|
||||
searchQuery={query}
|
||||
defaultMaxMatchCount={env.DEFAULT_MAX_MATCH_COUNT}
|
||||
isRegexEnabled={isRegexEnabled}
|
||||
isCaseSensitivityEnabled={isCaseSensitivityEnabled}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
289
packages/web/src/app/[domain]/search/useStreamedSearch.ts
Normal file
289
packages/web/src/app/[domain]/search/useStreamedSearch.ts
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
'use client';
|
||||
|
||||
import { RepositoryInfo, SearchRequest, SearchResultFile, SearchStats, StreamedSearchResponse } from '@/features/search';
|
||||
import { ServiceErrorException } from '@/lib/serviceError';
|
||||
import { isServiceError } from '@/lib/utils';
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface CacheEntry {
|
||||
files: SearchResultFile[];
|
||||
repoInfo: Record<number, RepositoryInfo>;
|
||||
numMatches: number;
|
||||
timeToSearchCompletionMs: number;
|
||||
timeToFirstSearchResultMs: number;
|
||||
timestamp: number;
|
||||
isExhaustive: boolean;
|
||||
}
|
||||
|
||||
const searchCache = new Map<string, CacheEntry>();
|
||||
const CACHE_TTL = 5 * 60 * 1000;
|
||||
|
||||
const createCacheKey = (params: SearchRequest): string => {
|
||||
return JSON.stringify({
|
||||
query: params.query,
|
||||
matches: params.matches,
|
||||
contextLines: params.contextLines,
|
||||
whole: params.whole,
|
||||
isRegexEnabled: params.isRegexEnabled,
|
||||
isCaseSensitivityEnabled: params.isCaseSensitivityEnabled,
|
||||
});
|
||||
};
|
||||
|
||||
const isCacheValid = (entry: CacheEntry): boolean => {
|
||||
return Date.now() - entry.timestamp < CACHE_TTL;
|
||||
};
|
||||
|
||||
export const useStreamedSearch = ({ query, matches, contextLines, whole, isRegexEnabled, isCaseSensitivityEnabled }: SearchRequest) => {
|
||||
const [state, setState] = useState<{
|
||||
isStreaming: boolean,
|
||||
isExhaustive: boolean,
|
||||
error: Error | null,
|
||||
files: SearchResultFile[],
|
||||
repoInfo: Record<number, RepositoryInfo>,
|
||||
timeToSearchCompletionMs: number,
|
||||
timeToFirstSearchResultMs: number,
|
||||
numMatches: number,
|
||||
stats?: SearchStats,
|
||||
}>({
|
||||
isStreaming: false,
|
||||
isExhaustive: false,
|
||||
error: null,
|
||||
files: [],
|
||||
repoInfo: {},
|
||||
timeToSearchCompletionMs: 0,
|
||||
timeToFirstSearchResultMs: 0,
|
||||
numMatches: 0,
|
||||
stats: undefined,
|
||||
});
|
||||
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
const cancel = useCallback(() => {
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
abortControllerRef.current = null;
|
||||
}
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isStreaming: false,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const search = async () => {
|
||||
const startTime = performance.now();
|
||||
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
abortControllerRef.current = new AbortController();
|
||||
|
||||
const cacheKey = createCacheKey({
|
||||
query,
|
||||
matches,
|
||||
contextLines,
|
||||
whole,
|
||||
isRegexEnabled,
|
||||
isCaseSensitivityEnabled,
|
||||
});
|
||||
|
||||
// Check if we have a valid cached result. If so, use it.
|
||||
const cachedEntry = searchCache.get(cacheKey);
|
||||
if (cachedEntry && isCacheValid(cachedEntry)) {
|
||||
console.debug('Using cached search results');
|
||||
setState({
|
||||
isStreaming: false,
|
||||
isExhaustive: cachedEntry.isExhaustive,
|
||||
error: null,
|
||||
files: cachedEntry.files,
|
||||
repoInfo: cachedEntry.repoInfo,
|
||||
timeToSearchCompletionMs: cachedEntry.timeToSearchCompletionMs,
|
||||
timeToFirstSearchResultMs: cachedEntry.timeToFirstSearchResultMs,
|
||||
numMatches: cachedEntry.numMatches,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setState({
|
||||
isStreaming: true,
|
||||
isExhaustive: false,
|
||||
error: null,
|
||||
files: [],
|
||||
repoInfo: {},
|
||||
timeToSearchCompletionMs: 0,
|
||||
timeToFirstSearchResultMs: 0,
|
||||
numMatches: 0,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/stream_search', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query,
|
||||
matches,
|
||||
contextLines,
|
||||
whole,
|
||||
isRegexEnabled,
|
||||
isCaseSensitivityEnabled,
|
||||
source: 'sourcebot-web-client'
|
||||
} satisfies SearchRequest),
|
||||
signal: abortControllerRef.current.signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// Check if this is a service error response
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (contentType?.includes('application/json')) {
|
||||
const errorData = await response.json();
|
||||
if (isServiceError(errorData)) {
|
||||
throw new ServiceErrorException(errorData);
|
||||
}
|
||||
}
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
throw new Error('No response body');
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
let numMessagesProcessed = 0;
|
||||
|
||||
while (true as boolean) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Decode the chunk and add to buffer
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
|
||||
// Process complete SSE messages (separated by \n\n)
|
||||
const messages = buffer.split('\n\n');
|
||||
|
||||
// Keep the last element (potentially incomplete message) in the buffer for the next chunk.
|
||||
// Stream chunks can split messages mid-way, so we only process complete messages.
|
||||
buffer = messages.pop() || '';
|
||||
|
||||
for (const message of messages) {
|
||||
if (!message.trim()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// SSE messages start with "data: "
|
||||
const dataMatch = message.match(/^data: (.+)$/);
|
||||
if (!dataMatch) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const data = dataMatch[1];
|
||||
|
||||
// Check for completion signal
|
||||
if (data === '[DONE]') {
|
||||
break;
|
||||
}
|
||||
|
||||
const response: StreamedSearchResponse = JSON.parse(data);
|
||||
const isFirstMessage = numMessagesProcessed === 0;
|
||||
switch (response.type) {
|
||||
case 'chunk':
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
files: [
|
||||
...prev.files,
|
||||
...response.files
|
||||
],
|
||||
repoInfo: {
|
||||
...prev.repoInfo,
|
||||
...response.repositoryInfo.reduce((acc, repo) => {
|
||||
acc[repo.id] = repo;
|
||||
return acc;
|
||||
}, {} as Record<number, RepositoryInfo>),
|
||||
},
|
||||
numMatches: prev.numMatches + response.stats.actualMatchCount,
|
||||
...(isFirstMessage ? {
|
||||
timeToFirstSearchResultMs: performance.now() - startTime,
|
||||
} : {}),
|
||||
}));
|
||||
break;
|
||||
case 'final':
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isExhaustive: response.isSearchExhaustive,
|
||||
stats: response.accumulatedStats,
|
||||
...(isFirstMessage ? {
|
||||
timeToFirstSearchResultMs: performance.now() - startTime,
|
||||
} : {}),
|
||||
}));
|
||||
break;
|
||||
case 'error':
|
||||
throw new ServiceErrorException(response.error);
|
||||
}
|
||||
|
||||
numMessagesProcessed++;
|
||||
}
|
||||
}
|
||||
|
||||
const timeToSearchCompletionMs = performance.now() - startTime;
|
||||
setState(prev => {
|
||||
// Cache the final results after the stream has completed.
|
||||
searchCache.set(cacheKey, {
|
||||
files: prev.files,
|
||||
repoInfo: prev.repoInfo,
|
||||
isExhaustive: prev.isExhaustive,
|
||||
numMatches: prev.numMatches,
|
||||
timeToFirstSearchResultMs: prev.timeToFirstSearchResultMs,
|
||||
timeToSearchCompletionMs,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
return {
|
||||
...prev,
|
||||
timeToSearchCompletionMs,
|
||||
isStreaming: false,
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
if ((error as Error).name === 'AbortError') {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(error);
|
||||
Sentry.captureException(error);
|
||||
const timeToSearchCompletionMs = performance.now() - startTime;
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isStreaming: false,
|
||||
timeToSearchCompletionMs,
|
||||
error: error instanceof Error ? error : null,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
search();
|
||||
|
||||
return () => {
|
||||
cancel();
|
||||
}
|
||||
}, [
|
||||
query,
|
||||
matches,
|
||||
contextLines,
|
||||
whole,
|
||||
isRegexEnabled,
|
||||
isCaseSensitivityEnabled,
|
||||
cancel,
|
||||
]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
cancel,
|
||||
};
|
||||
}
|
||||
|
|
@ -69,7 +69,7 @@ export default async function SettingsLayout(
|
|||
throw new ServiceErrorException(connectionStats);
|
||||
}
|
||||
|
||||
const hasPermissionSyncingEntitlement = await hasEntitlement("permission-syncing");
|
||||
const hasPermissionSyncingEntitlement = hasEntitlement("permission-syncing");
|
||||
|
||||
const sidebarNavItems: SidebarNavItem[] = [
|
||||
{
|
||||
|
|
@ -89,16 +89,8 @@ export default async function SettingsLayout(
|
|||
}
|
||||
] : []),
|
||||
...(userRoleInOrg === OrgRole.OWNER ? [{
|
||||
title: (
|
||||
<div className="flex items-center gap-2">
|
||||
Members
|
||||
{numJoinRequests !== undefined && numJoinRequests > 0 && (
|
||||
<span className="inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-primary px-1.5 text-xs font-medium text-primary-foreground">
|
||||
{numJoinRequests}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
title:"Members",
|
||||
isNotificationDotVisible: numJoinRequests !== undefined && numJoinRequests > 0,
|
||||
href: `/${domain}/settings/members`,
|
||||
}] : []),
|
||||
...(userRoleInOrg === OrgRole.OWNER ? [
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import { getSeats, SOURCEBOT_UNLIMITED_SEATS } from "@sourcebot/shared";
|
|||
import { RequestsList } from "./components/requestsList";
|
||||
import { OrgRole } from "@prisma/client";
|
||||
import { redirect } from "next/navigation";
|
||||
import { NotificationDot } from "../../components/notificationDot";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
interface MembersSettingsPageProps {
|
||||
params: Promise<{
|
||||
|
|
@ -106,32 +108,45 @@ export default async function MembersSettingsPage(props: MembersSettingsPageProp
|
|||
<TabSwitcher
|
||||
className="h-auto p-0 bg-transparent"
|
||||
tabs={[
|
||||
{ label: "Team Members", value: "members" },
|
||||
...(userRoleInOrg === OrgRole.OWNER ? [
|
||||
{
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
Pending Requests
|
||||
{requests.length > 0 && (
|
||||
<span className="inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-primary px-1.5 text-xs font-medium text-primary-foreground">
|
||||
{requests.length}
|
||||
</span>
|
||||
)}
|
||||
Team Members
|
||||
<Badge variant="secondary" className="px-1.5 relative">
|
||||
{members.length}
|
||||
</Badge>
|
||||
</div>
|
||||
),
|
||||
value: "requests"
|
||||
value: "members"
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
Pending Invites
|
||||
{invites.length > 0 && (
|
||||
<span className="inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-primary px-1.5 text-xs font-medium text-primary-foreground">
|
||||
{invites.length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
...(userRoleInOrg === OrgRole.OWNER ? [
|
||||
{
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
{requests.length > 0 && (
|
||||
<NotificationDot />
|
||||
)}
|
||||
Pending Requests
|
||||
{requests.length > 0 && (
|
||||
<Badge variant="secondary" className="px-1.5 relative">
|
||||
{requests.length}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
value: "requests"
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<div className="flex items-center gap-2">
|
||||
Pending Invites
|
||||
{invites.length > 0 && (
|
||||
<Badge variant="secondary" className="px-1.5 relative">
|
||||
{invites.length}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
value: "invites"
|
||||
},
|
||||
] : []),
|
||||
|
|
|
|||
|
|
@ -4,18 +4,29 @@ import { ServiceError } from "@/lib/serviceError";
|
|||
import { GetVersionResponse, GetReposResponse } from "@/lib/types";
|
||||
import { isServiceError } from "@/lib/utils";
|
||||
import {
|
||||
FileSourceResponse,
|
||||
FileSourceRequest,
|
||||
SearchRequest,
|
||||
SearchResponse,
|
||||
} from "@/features/search";
|
||||
import {
|
||||
FileSourceRequest,
|
||||
FileSourceResponse,
|
||||
} from "@/features/search/types";
|
||||
import {
|
||||
FindRelatedSymbolsRequest,
|
||||
FindRelatedSymbolsResponse,
|
||||
} from "@/features/codeNav/types";
|
||||
import {
|
||||
GetFilesRequest,
|
||||
GetFilesResponse,
|
||||
GetTreeRequest,
|
||||
GetTreeResponse,
|
||||
} from "@/features/fileTree/types";
|
||||
|
||||
export const search = async (body: SearchRequest, domain: string): Promise<SearchResponse | ServiceError> => {
|
||||
export const search = async (body: SearchRequest): Promise<SearchResponse | ServiceError> => {
|
||||
const result = await fetch("/api/search", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Org-Domain": domain,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
}).then(response => response.json());
|
||||
|
|
@ -27,12 +38,11 @@ export const search = async (body: SearchRequest, domain: string): Promise<Searc
|
|||
return result as SearchResponse | ServiceError;
|
||||
}
|
||||
|
||||
export const fetchFileSource = async (body: FileSourceRequest, domain: string): Promise<FileSourceResponse | ServiceError> => {
|
||||
export const getFileSource = async (body: FileSourceRequest): Promise<FileSourceResponse | ServiceError> => {
|
||||
const result = await fetch("/api/source", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Org-Domain": domain,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
}).then(response => response.json());
|
||||
|
|
@ -60,3 +70,35 @@ export const getVersion = async (): Promise<GetVersionResponse> => {
|
|||
}).then(response => response.json());
|
||||
return result as GetVersionResponse;
|
||||
}
|
||||
|
||||
export const findSearchBasedSymbolReferences = async (body: FindRelatedSymbolsRequest): Promise<FindRelatedSymbolsResponse | ServiceError> => {
|
||||
const result = await fetch("/api/find_references", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
}).then(response => response.json());
|
||||
return result as FindRelatedSymbolsResponse | ServiceError;
|
||||
}
|
||||
|
||||
export const findSearchBasedSymbolDefinitions = async (body: FindRelatedSymbolsRequest): Promise<FindRelatedSymbolsResponse | ServiceError> => {
|
||||
const result = await fetch("/api/find_definitions", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
}).then(response => response.json());
|
||||
return result as FindRelatedSymbolsResponse | ServiceError;
|
||||
}
|
||||
|
||||
export const getTree = async (body: GetTreeRequest): Promise<GetTreeResponse | ServiceError> => {
|
||||
const result = await fetch("/api/tree", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
}).then(response => response.json());
|
||||
return result as GetTreeResponse | ServiceError;
|
||||
}
|
||||
|
||||
export const getFiles = async (body: GetFilesRequest): Promise<GetFilesResponse | ServiceError> => {
|
||||
const result = await fetch("/api/files", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
}).then(response => response.json());
|
||||
return result as GetFilesResponse | ServiceError;
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { sew, withAuth, withOrgMembership } from "@/actions";
|
||||
import { sew } from "@/actions";
|
||||
import { _getConfiguredLanguageModelsFull, _getAISDKLanguageModelAndOptions, updateChatMessages } from "@/features/chat/actions";
|
||||
import { createAgentStream } from "@/features/chat/agent";
|
||||
import { additionalChatRequestParamsSchema, LanguageModelInfo, SBChatMessage, SearchScope } from "@/features/chat/types";
|
||||
|
|
@ -6,10 +6,10 @@ import { getAnswerPartFromAssistantMessage, getLanguageModelKey } from "@/featur
|
|||
import { ErrorCode } from "@/lib/errorCodes";
|
||||
import { notFound, schemaValidationError, serviceErrorResponse } from "@/lib/serviceError";
|
||||
import { isServiceError } from "@/lib/utils";
|
||||
import { prisma } from "@/prisma";
|
||||
import { withOptionalAuthV2 } from "@/withAuthV2";
|
||||
import { LanguageModelV2 as AISDKLanguageModelV2 } from "@ai-sdk/provider";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { OrgRole } from "@sourcebot/db";
|
||||
import { PrismaClient } from "@sourcebot/db";
|
||||
import { createLogger } from "@sourcebot/shared";
|
||||
import {
|
||||
createUIMessageStream,
|
||||
|
|
@ -34,15 +34,6 @@ const chatRequestSchema = z.object({
|
|||
})
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const domain = req.headers.get("X-Org-Domain");
|
||||
if (!domain) {
|
||||
return serviceErrorResponse({
|
||||
statusCode: StatusCodes.BAD_REQUEST,
|
||||
errorCode: ErrorCode.MISSING_ORG_DOMAIN_HEADER,
|
||||
message: "Missing X-Org-Domain header",
|
||||
});
|
||||
}
|
||||
|
||||
const requestBody = await req.json();
|
||||
const parsed = await chatRequestSchema.safeParseAsync(requestBody);
|
||||
if (!parsed.success) {
|
||||
|
|
@ -56,56 +47,54 @@ export async function POST(req: Request) {
|
|||
const languageModel = _languageModel as LanguageModelInfo;
|
||||
|
||||
const response = await sew(() =>
|
||||
withAuth((userId) =>
|
||||
withOrgMembership(userId, domain, async ({ org }) => {
|
||||
// Validate that the chat exists and is not readonly.
|
||||
const chat = await prisma.chat.findUnique({
|
||||
where: {
|
||||
orgId: org.id,
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!chat) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
if (chat.isReadonly) {
|
||||
return serviceErrorResponse({
|
||||
statusCode: StatusCodes.BAD_REQUEST,
|
||||
errorCode: ErrorCode.INVALID_REQUEST_BODY,
|
||||
message: "Chat is readonly and cannot be edited.",
|
||||
});
|
||||
}
|
||||
|
||||
// From the language model ID, attempt to find the
|
||||
// corresponding config in `config.json`.
|
||||
const languageModelConfig =
|
||||
(await _getConfiguredLanguageModelsFull())
|
||||
.find((model) => getLanguageModelKey(model) === getLanguageModelKey(languageModel));
|
||||
|
||||
if (!languageModelConfig) {
|
||||
return serviceErrorResponse({
|
||||
statusCode: StatusCodes.BAD_REQUEST,
|
||||
errorCode: ErrorCode.INVALID_REQUEST_BODY,
|
||||
message: `Language model ${languageModel.model} is not configured.`,
|
||||
});
|
||||
}
|
||||
|
||||
const { model, providerOptions } = await _getAISDKLanguageModelAndOptions(languageModelConfig);
|
||||
|
||||
return createMessageStreamResponse({
|
||||
messages,
|
||||
id,
|
||||
selectedSearchScopes,
|
||||
model,
|
||||
modelName: languageModelConfig.displayName ?? languageModelConfig.model,
|
||||
modelProviderOptions: providerOptions,
|
||||
domain,
|
||||
withOptionalAuthV2(async ({ org, prisma }) => {
|
||||
// Validate that the chat exists and is not readonly.
|
||||
const chat = await prisma.chat.findUnique({
|
||||
where: {
|
||||
orgId: org.id,
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!chat) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
if (chat.isReadonly) {
|
||||
return serviceErrorResponse({
|
||||
statusCode: StatusCodes.BAD_REQUEST,
|
||||
errorCode: ErrorCode.INVALID_REQUEST_BODY,
|
||||
message: "Chat is readonly and cannot be edited.",
|
||||
});
|
||||
}, /* minRequiredRole = */ OrgRole.GUEST), /* allowSingleTenantUnauthedAccess = */ true
|
||||
)
|
||||
}
|
||||
|
||||
// From the language model ID, attempt to find the
|
||||
// corresponding config in `config.json`.
|
||||
const languageModelConfig =
|
||||
(await _getConfiguredLanguageModelsFull())
|
||||
.find((model) => getLanguageModelKey(model) === getLanguageModelKey(languageModel));
|
||||
|
||||
if (!languageModelConfig) {
|
||||
return serviceErrorResponse({
|
||||
statusCode: StatusCodes.BAD_REQUEST,
|
||||
errorCode: ErrorCode.INVALID_REQUEST_BODY,
|
||||
message: `Language model ${languageModel.model} is not configured.`,
|
||||
});
|
||||
}
|
||||
|
||||
const { model, providerOptions } = await _getAISDKLanguageModelAndOptions(languageModelConfig);
|
||||
|
||||
return createMessageStreamResponse({
|
||||
messages,
|
||||
id,
|
||||
selectedSearchScopes,
|
||||
model,
|
||||
modelName: languageModelConfig.displayName ?? languageModelConfig.model,
|
||||
modelProviderOptions: providerOptions,
|
||||
orgId: org.id,
|
||||
prisma,
|
||||
});
|
||||
})
|
||||
)
|
||||
|
||||
if (isServiceError(response)) {
|
||||
|
|
@ -132,8 +121,8 @@ interface CreateMessageStreamResponseProps {
|
|||
model: AISDKLanguageModelV2;
|
||||
modelName: string;
|
||||
modelProviderOptions?: Record<string, Record<string, JSONValue>>;
|
||||
domain: string;
|
||||
orgId: number;
|
||||
prisma: PrismaClient;
|
||||
}
|
||||
|
||||
const createMessageStreamResponse = async ({
|
||||
|
|
@ -143,8 +132,8 @@ const createMessageStreamResponse = async ({
|
|||
model,
|
||||
modelName,
|
||||
modelProviderOptions,
|
||||
domain,
|
||||
orgId,
|
||||
prisma,
|
||||
}: CreateMessageStreamResponseProps) => {
|
||||
const latestMessage = messages[messages.length - 1];
|
||||
const sources = latestMessage.parts
|
||||
|
|
@ -254,7 +243,7 @@ const createMessageStreamResponse = async ({
|
|||
await updateChatMessages({
|
||||
chatId: id,
|
||||
messages
|
||||
}, domain);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,46 +1,25 @@
|
|||
'use server';
|
||||
|
||||
import { NextRequest } from "next/server";
|
||||
import { fetchAuditRecords } from "@/ee/features/audit/actions";
|
||||
import { isServiceError } from "@/lib/utils";
|
||||
import { serviceErrorResponse } from "@/lib/serviceError";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
import { ErrorCode } from "@/lib/errorCodes";
|
||||
import { env } from "@sourcebot/shared";
|
||||
import { serviceErrorResponse } from "@/lib/serviceError";
|
||||
import { isServiceError } from "@/lib/utils";
|
||||
import { getEntitlements } from "@sourcebot/shared";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
|
||||
export const GET = async (request: NextRequest) => {
|
||||
const domain = request.headers.get("X-Org-Domain");
|
||||
const apiKey = request.headers.get("X-Sourcebot-Api-Key") ?? undefined;
|
||||
export const GET = async () => {
|
||||
const entitlements = getEntitlements();
|
||||
if (!entitlements.includes('audit')) {
|
||||
return serviceErrorResponse({
|
||||
statusCode: StatusCodes.FORBIDDEN,
|
||||
errorCode: ErrorCode.NOT_FOUND,
|
||||
message: "Audit logging is not enabled for your license",
|
||||
});
|
||||
}
|
||||
|
||||
if (!domain) {
|
||||
return serviceErrorResponse({
|
||||
statusCode: StatusCodes.BAD_REQUEST,
|
||||
errorCode: ErrorCode.MISSING_ORG_DOMAIN_HEADER,
|
||||
message: "Missing X-Org-Domain header",
|
||||
});
|
||||
}
|
||||
|
||||
if (env.SOURCEBOT_EE_AUDIT_LOGGING_ENABLED === 'false') {
|
||||
return serviceErrorResponse({
|
||||
statusCode: StatusCodes.NOT_FOUND,
|
||||
errorCode: ErrorCode.NOT_FOUND,
|
||||
message: "Audit logging is not enabled",
|
||||
});
|
||||
}
|
||||
|
||||
const entitlements = getEntitlements();
|
||||
if (!entitlements.includes('audit')) {
|
||||
return serviceErrorResponse({
|
||||
statusCode: StatusCodes.FORBIDDEN,
|
||||
errorCode: ErrorCode.NOT_FOUND,
|
||||
message: "Audit logging is not enabled for your license",
|
||||
});
|
||||
}
|
||||
|
||||
const result = await fetchAuditRecords(domain, apiKey);
|
||||
if (isServiceError(result)) {
|
||||
return serviceErrorResponse(result);
|
||||
}
|
||||
return Response.json(result);
|
||||
const result = await fetchAuditRecords();
|
||||
if (isServiceError(result)) {
|
||||
return serviceErrorResponse(result);
|
||||
}
|
||||
return Response.json(result);
|
||||
};
|
||||
23
packages/web/src/app/api/(server)/files/route.ts
Normal file
23
packages/web/src/app/api/(server)/files/route.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
'use server';
|
||||
|
||||
import { getFiles } from "@/features/fileTree/api";
|
||||
import { getFilesRequestSchema } from "@/features/fileTree/types";
|
||||
import { schemaValidationError, serviceErrorResponse } from "@/lib/serviceError";
|
||||
import { isServiceError } from "@/lib/utils";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
export const POST = async (request: NextRequest) => {
|
||||
const body = await request.json();
|
||||
const parsed = await getFilesRequestSchema.safeParseAsync(body);
|
||||
if (!parsed.success) {
|
||||
return serviceErrorResponse(schemaValidationError(parsed.error));
|
||||
}
|
||||
|
||||
const response = await getFiles(parsed.data);
|
||||
if (isServiceError(response)) {
|
||||
return serviceErrorResponse(response);
|
||||
}
|
||||
|
||||
return Response.json(response);
|
||||
}
|
||||
|
||||
22
packages/web/src/app/api/(server)/find_definitions/route.ts
Normal file
22
packages/web/src/app/api/(server)/find_definitions/route.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
'use server';
|
||||
|
||||
import { findSearchBasedSymbolDefinitions } from "@/features/codeNav/api";
|
||||
import { findRelatedSymbolsRequestSchema } from "@/features/codeNav/types";
|
||||
import { schemaValidationError, serviceErrorResponse } from "@/lib/serviceError";
|
||||
import { isServiceError } from "@/lib/utils";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
export const POST = async (request: NextRequest) => {
|
||||
const body = await request.json();
|
||||
const parsed = await findRelatedSymbolsRequestSchema.safeParseAsync(body);
|
||||
if (!parsed.success) {
|
||||
return serviceErrorResponse(schemaValidationError(parsed.error));
|
||||
}
|
||||
|
||||
const response = await findSearchBasedSymbolDefinitions(parsed.data);
|
||||
if (isServiceError(response)) {
|
||||
return serviceErrorResponse(response);
|
||||
}
|
||||
|
||||
return Response.json(response);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue