diff --git a/.aspect/bazelrc/ci.sourcegraph.bazelrc b/.aspect/bazelrc/ci.sourcegraph.bazelrc index 572c553af4927..98307106d9ff5 100644 --- a/.aspect/bazelrc/ci.sourcegraph.bazelrc +++ b/.aspect/bazelrc/ci.sourcegraph.bazelrc @@ -5,44 +5,47 @@ try-import %workspace%/.aspect/bazelrc/ci.generated.bazelrc # Use repo caching for building and testing. # Article: https://buildkite.com/blog/how-bazel-built-its-ci-system-on-top-of-buildkite # Docs: https://bazel.build/reference/command-line-reference#flag--repository_cache -build --repository_cache=/home/buildkite/repocache-sourcegraph +common --repository_cache=/home/buildkite/repocache-sourcegraph # We need /usr/local/bin # TODO(DevX) we should be narrower here. -build --test_env=PATH +common --test_env=PATH # Needed for DB in CI -build --test_env=PGUSER -build --test_env=PGSSLMODE -build --test_env=PGDATABASE +common --test_env=PGUSER=postgres +common --test_env=PGPASSWORD=postgres +common --test_env=PGSSLMODE=disable +common --test_env=PGDATABASE=postgres # Allow tests to understand they're running in CI, which forces dbtest to drop database even in case of failures. # TODO(JH) we should instead wipe all templates after a job finishes. -build --test_env=CI +common --test_env=CI # Ensure we're not exhausting database connections. -build --test_env=GOMAXPROCS=10 -build --test_env=TESTDB_MAXOPENCONNS=15 +common --test_env=GOMAXPROCS=10 +common --test_env=TESTDB_MAXOPENCONNS=15 # Needed for E2E -build --test_env=BUILDKITE +common --test_env=BUILDKITE # Needed for mocha tests # We have to use the `--define` flag here instead of `--test_env` because # the mocha tests target is the build target and it's tested with `build_test`. -build --define=E2E_HEADLESS=false -build --define=E2E_SOURCEGRAPH_BASE_URL="http://localhost:7080" -build --define=DISPLAY=:99 +common --define=E2E_HEADLESS=false +# if we set this to localhost, chrome will refuse to conenct since local host is in its HTTP Strict Transport Security +# by setting the loopback address we get passed that +common --define=E2E_SOURCEGRAPH_BASE_URL="http://127.0.0.1:7080" +common --define=DISPLAY=:99 # Provides git commit, branch information to build targets like Percy via status file. # https://bazel.build/docs/user-manual#workspace-status -build --workspace_status_command=./dev/bazel_buildkite_stamp_vars.sh +common --workspace_status_command=./dev/bazel_buildkite_stamp_vars.sh # temp -build --test_env=INCLUDE_ADMIN_ONBOARDING=false +common --test_env=INCLUDE_ADMIN_ONBOARDING=false # Used for container_structure_tests -build --test_env=DOCKER_HOST +common --test_env=DOCKER_HOST # Used by migration rules -build --action_env=PGUSER=postgres +common --action_env=PGUSER=postgres diff --git a/.aspect/bazelrc/performance.bazelrc b/.aspect/bazelrc/performance.bazelrc index fff4c7c5eed2b..26dbbe1f8d34d 100644 --- a/.aspect/bazelrc/performance.bazelrc +++ b/.aspect/bazelrc/performance.bazelrc @@ -27,12 +27,3 @@ build --experimental_reuse_sandbox_directories # author. # Docs: https://bazel.build/reference/command-line-reference#flag--legacy_external_runfiles build --nolegacy_external_runfiles - -# Some actions are always IO-intensive but require little compute. It's wasteful to put the output -# in the remote cache, it just saturates the network and fills the cache storage causing earlier -# evictions. It's also not worth sending them for remote execution. -# For actions like PackageTar it's usually faster to just re-run the work locally every time. -# You'll have to look at an execution log to figure out what other action mnemonics you care about. -# In some cases you may need to patch rulesets to add a mnemonic to actions that don't have one. -# https://bazel.build/reference/command-line-reference#flag--modify_execution_info -build --modify_execution_info=PackageTar=+no-remote diff --git a/.aspect/workflows/bazelrc b/.aspect/workflows/bazelrc new file mode 100644 index 0000000000000..2b7d0e2b67045 --- /dev/null +++ b/.aspect/workflows/bazelrc @@ -0,0 +1,3 @@ +common --remote_download_minimal +common --nobuild_runfile_links +common --noexperimental_reuse_sandbox_directories diff --git a/.aspect/workflows/config.yaml b/.aspect/workflows/config.yaml new file mode 100644 index 0000000000000..54ccfae340f9c --- /dev/null +++ b/.aspect/workflows/config.yaml @@ -0,0 +1,30 @@ +--- +bazel: + rcfiles: + - ".aspect/bazelrc/ci.sourcegraph.bazelrc" + flags: + # This flag is required because otherwise the integration tests fail with `fcmod` Operation not permitted + # which is probably related to the launced containers writing to mapped in directories as root and then + # when the container exits the files that are left over are root. + # TODO(burmudar): launch containers with uid/guid mapped in + - --noexperimental_reuse_sandbox_directories +env: + REDIS_CACHE_ENDPOINT: ":6379" + GIT_PAGER: '' +tasks: + # Checks that BUILD files are formatted + buildifier: + # Checks that BUILD file content is up-to-date with sources + gazelle: + target: //:configure + fix_target: //:configure + # Checks that all tests are passing + test: + include_eternal_tests: true + targets: + - //... + - //testing:grpc_backend_integration_test + # This target should only really run when on main which we aren't handling. For the time being while we + # evaluate Aspect Workflows it is ok + # TODO(burmudar): Let this only run on main branch + - //testing:codeintel_integration_test diff --git a/.eslintrc.js b/.eslintrc.js index 9bd5e4cdc48a0..75b90793d20f5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -17,13 +17,19 @@ const config = { '**/vendor/*.js', 'svelte.config.js', 'vite.config.ts', + 'vitest.config.ts', + 'postcss.config.js', 'playwright.config.ts', + 'bundlesize.config.js', + 'prettier.config.js', + 'svgo.config.js', '.vscode-test', '**/*.json', '**/*.d.ts', 'eslint-relative-formatter.js', 'typedoc.js', - 'bundlesize.config.js', + 'client/web/dev/**/*', + 'graphql-schema-linter.config.js', ], extends: ['@sourcegraph/eslint-config', 'plugin:storybook/recommended'], env: { @@ -37,8 +43,8 @@ const config = { ecmaFeatures: { jsx: true, }, - EXPERIMENTAL_projectService: true, - project: __dirname + '/tsconfig.all.json', + EXPERIMENTAL_useProjectService: true, + project: true, }, settings: { react: { diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index 7cc5bb26eec45..0000000000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1,14 +0,0 @@ -# Lines starting with '#' are comments. -# Each line is a file pattern followed by one or more owners. -# https://help.github.com/articles/about-codeowners/ -# -# Order is important; the last matching pattern takes the most -# precedence. - -# We prefer to use Codenotify (https://github.com/sourcegraph/codenotify) instead of CODEOWNERS. -# More context is in this blog post: https://about.sourcegraph.com/blog/a-different-way-to-think-about-code-ownership/ -# If you are tempted to add an entry to CODEOWNERS, please try using Codenotify first for some amount of time. -# If Codenotify does not satisfy your needs, then you can open a PR to propose adding a new entry to CODEOWNERS and Nick will review. -# The PR description should describe why using Codenotify was insufficient. Thanks! -CODEOWNERS @nicksnyder -.github/CODEOWNERS @nicksnyder diff --git a/.github/ISSUE_TEMPLATE/good_first_issue.md b/.github/ISSUE_TEMPLATE/good_first_issue.md index 6a0a4b0c5fe1e..dac97a9a4ae22 100644 --- a/.github/ISSUE_TEMPLATE/good_first_issue.md +++ b/.github/ISSUE_TEMPLATE/good_first_issue.md @@ -37,5 +37,5 @@ T-shirt size estimate: **S**. ## Contributors 1. Check out [contributing guidelines](https://github.com/sourcegraph/sourcegraph/blob/main/CONTRIBUTING.md). -2. Join the Sourcegraph [Community Space](https://srcgr.ph/join-community-space) on Slack and join the `#help` channel where the Sourcegraph team can help you! +2. Join the Sourcegraph [Community Space](https://srcgr.ph/join-community-space) on Discord where the Sourcegraph team can help you! 3. Check out [the good first issues board](https://github.com/orgs/sourcegraph/projects/210) to find more curated issues. diff --git a/.github/workflows/licenses-check.yml b/.github/workflows/licenses-check.yml index 193a27a723851..a1c16c86f18e9 100644 --- a/.github/workflows/licenses-check.yml +++ b/.github/workflows/licenses-check.yml @@ -33,7 +33,7 @@ jobs: with: ruby-version: "3.2.2" # Not needed with a .ruby-version file - uses: actions/setup-ruby@v1 - uses: actions/setup-go@v2 - with: { go-version: "1.20" } + with: { go-version: "1.21" } - name: Install license_finder diff --git a/.github/workflows/pr-auditor.yml b/.github/workflows/pr-auditor.yml index e83c8ee658d42..defce42ce6ca6 100644 --- a/.github/workflows/pr-auditor.yml +++ b/.github/workflows/pr-auditor.yml @@ -13,7 +13,7 @@ jobs: with: repository: 'sourcegraph/pr-auditor' - uses: actions/setup-go@v4 - with: { go-version: '1.20' } + with: { go-version: '1.21' } - run: './check-pr.sh' env: diff --git a/.github/workflows/sg-binary-release.yml b/.github/workflows/sg-binary-release.yml index b443008226d9a..6fafc23678523 100644 --- a/.github/workflows/sg-binary-release.yml +++ b/.github/workflows/sg-binary-release.yml @@ -100,7 +100,7 @@ jobs: - name: Install Go uses: actions/setup-go@v2 with: - go-version: 1.20.8 + go-version: 1.21.4 - name: Build and upload macOS if: startsWith(matrix.os, 'macos-') == true diff --git a/.github/workflows/sg-setup.yml b/.github/workflows/sg-setup.yml index 464b19864e8f4..78a653c570274 100644 --- a/.github/workflows/sg-setup.yml +++ b/.github/workflows/sg-setup.yml @@ -24,7 +24,7 @@ jobs: - name: Install Go uses: actions/setup-go@v2 with: - go-version: 1.20.10 + go-version: 1.21.4 - name: Install asdf plugins uses: asdf-vm/actions/install@v1 diff --git a/.gitignore b/.gitignore index 64c0672b4fce7..6ad74f09cd10c 100644 --- a/.gitignore +++ b/.gitignore @@ -98,6 +98,7 @@ sourcegraph-webapp-*.tgz *.tsbuildinfo graphql-operations.ts *.module.scss.d.ts +*.module.css.d.ts dll-bundle # Extensions diff --git a/.prettierignore b/.prettierignore index cba1e1765a010..0a6b5e2ac50d1 100644 --- a/.prettierignore +++ b/.prettierignore @@ -19,7 +19,7 @@ vendor/ .nyc_output/ out/ dist/ -dist-types/ +client/web/dist/ client/shared/src/schema/*.d.ts ts-node-* testdata diff --git a/.tool-versions b/.tool-versions index 02599e3a4c30b..329eb790d535f 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,4 +1,4 @@ -golang 1.20.10 +golang 1.21.4 nodejs 20.8.1 fd 8.6.0 shfmt 3.5.0 @@ -10,6 +10,6 @@ trivy 0.30.3 kustomize 4.5.7 awscli 2.4.7 python 3.11.3 system -rust 1.68.0 +rust 1.73.0 ruby 3.1.3 pnpm 8.9.2 diff --git a/BUILD.bazel b/BUILD.bazel index c1b1cd0e19e40..70ac840864a3f 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -9,8 +9,8 @@ load("@io_bazel_rules_go//proto/wkt:well_known_types.bzl", "WELL_KNOWN_TYPES_API load("@npm//:defs.bzl", "npm_link_all_packages") load("//dev/linters/staticcheck:analyzers.bzl", "STATIC_CHECK_ANALYZERS") load("@npm//:eslint/package_json.bzl", eslint_bin = "bin") -load("//:stamp_tags.bzl", "stamp_tags") load("//dev:eslint.bzl", "eslint_test_with_types") +load("@buildifier_prebuilt//:rules.bzl", "buildifier") # Gazelle config # @@ -180,6 +180,28 @@ eslint_test_with_types( ], ) +buildifier( + name = "buildifier", + exclude_patterns = [ + "./.git/*", + "cmd/symbols/squirrel/test_repos/starlark/**/*", + ], + # TODO: enable these lint checks + # lint_mode = "fix", + mode = "fix", +) + +buildifier( + name = "buildifier.check", + exclude_patterns = [ + "./.git/*", + "cmd/symbols/squirrel/test_repos/starlark/**/*", + ], + # TODO: enable these lint checks + # lint_mode = "warn", + mode = "diff", +) + # Go gazelle_binary( @@ -201,6 +223,13 @@ gazelle( gazelle = ":gazelle-buf", ) +sh_binary( + name = "configure", + srcs = ["//dev/ci:bazel-configure.sh"], + data = ["@go_sdk//:bin/go"], + env = {"GO": "$(rootpath @go_sdk//:bin/go)"}, +) + go_library( name = "sourcegraph", srcs = [ @@ -301,3 +330,18 @@ exports_files([ "CONTRIBUTING.md", ".swcrc", ]) + +# sg msp settings + +bool_flag( + name = "sg_msp", + build_setting_default = False, + visibility = ["//visibility:public"], +) + +config_setting( + name = "sg_msp_flag", + flag_values = { + "//:sg_msp": "True", + }, +) diff --git a/CHANGELOG.md b/CHANGELOG.md index 016eb1a2e8fb1..3fdbf2f3bf6e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,13 @@ All notable changes to Sourcegraph are documented in this file. - The setting `experimentalFeatures.searchQueryInput` now refers to the new query input as `v2` (not `experimental`). - Search-based code intel doesn't include the currently selected search context anymore. It was possible to get into a situation where search-based code intel wouldn't find any information due to being restricted by the current search context. [#58010](https://github.com/sourcegraph/sourcegraph/pull/58010) +- The last commit which changed a file/directory is now shown in the files panel on the repo and file pages. To avoid duplicating information and confusion, the commits panel was removed. [58328](https://github.com/sourcegraph/sourcegraph/pull/58328) +- Clicking on a search result now opens the blob view at the same commit as the search result. Before, blob views were opened at the tip of the default branch, which sometimes caused inconsistencies in line numbers if the index was out of date. [#58381](https://github.com/sourcegraph/sourcegraph/pull/58381) +- The `exclude` configuration for code host configuration has been updated to allow chaining multiple conditions together and filtering GitHub repositories based on their size or number of GitHub stars. [#58377](https://github.com/sourcegraph/sourcegraph/pull/58377) and [#58405](https://github.com/sourcegraph/sourcegraph/pull/58405) + - Multiple attributes on _a single_ `exclude` entry now have to be all true for a repository to be excluded. Example: `{"exclude": [{"name": "github.com/example/example"}, {"id": "my-id"}]}` will _only_ exclude repositories that have the name _and_ the id mentioned. + - GitHub code host connections can exclude by size and stars now: `{"exclude": [{"name": "github.com/example/example"}, {"stars": "< 100", "size": ">= 1GB"}]}` + - For `size` and `stars` the supported operators are `<`, `>`, `<=`, `>=`. + - For `size` the supported units are `B`, `b`, `kB`, `KB`, `kiB`, `KiB`, `MiB`, `MB`, `GiB`, `GB`. No decimals points are supported. ### Fixed @@ -34,6 +41,7 @@ All notable changes to Sourcegraph are documented in this file. - Fixed a bug where typing in the GraphQL editor in the Site Admin API console could cause the cursor to jump to the start of the editor. [#57862](https://github.com/sourcegraph/sourcegraph/pull/57862) - The blame column no longer ignores whitespace-only changes by default. [#58134](https://github.com/sourcegraph/sourcegraph/pull/58134) - Long lines now wrap correctly in the diff view. [#58138](https://github.com/sourcegraph/sourcegraph/pull/58138) +- Fixed an issue in the search input where pressing Enter after selecting a suggestion would sometimes insert another suggestions instead of submitting the query. [#58186](https://github.com/sourcegraph/sourcegraph/pull/58186) ### Removed @@ -43,26 +51,35 @@ All notable changes to Sourcegraph are documented in this file. - The GitHub Proxy service is no longer required and has been removed from deployment options. [#55290](https://github.com/sourcegraph/sourcegraph/issues/55290) - The VSCode search extension "Sourcegraph for VS Code" has been sunset and removed from Sourcegraph repository. [#58023](https://github.com/sourcegraph/sourcegraph/pull/58023) +- The `rateLimit` configuration for Perforce code host connections has been removed to avoid confusion, it was unused. [#58188](https://github.com/sourcegraph/sourcegraph/pull/58188) - The feature flag `search-ranking` is now completely removed. [#58156](https://github.com/sourcegraph/sourcegraph/pull/58156) - The notepad UI, notebook creation feature. [#58217](https://github.com/sourcegraph/sourcegraph/pull/58217) -## Unreleased 5.2.3 +## Unreleased 5.2.4 ### Added -- +- Added the ability to use Workload Identity, Managed Identity and Environmental credentials when using the Azure OpenAI completions and embeddings providers [#58289](https://github.com/sourcegraph/sourcegraph/pull/58289) +- Added support for cloning via SSH from Azure DevOps. [#58655](https://github.com/sourcegraph/sourcegraph/pull/58655) + +### Fixed + +- Fixed two issues in Zoekt that could cause out of memory errors during search indexing. [sourcegraph/zoekt#686](https://github.com/sourcegraph/zoekt/pull/686), [sourcegraph/zoekt#689](https://github.com/sourcegraph/zoekt/pull/689) ### Changed -- +### Removed -### Fixed +## 5.2.3 -- +### Added -### Removed +- Added configurable GraphQL query cost limitations to prevent unintended resource exhaustion. Default values are now provided and enforced, replacing the previously unlimited behaviour. For more information, please refer to: [GraphQL Cost Limits Documentation](https://docs.sourcegraph.com/api/graphql#cost-limits). See details at [#58346](https://github.com/sourcegraph/sourcegraph/pull/58346). +- Sourcegraph now supports connecting to Bitbucket Cloud using Workspace Access Tokens. [#58465](https://github.com/sourcegraph/sourcegraph/pull/58465). -- +### Fixed + +- Defining file filters for embeddings jobs no longer causes all files to be skipped if `MaxFileSizeBytes` isn't defined. [#58262](https://github.com/sourcegraph/sourcegraph/pull/58262) ## 5.2.2 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 86395a9b50e1f..9dd61c27544ae 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,6 +26,7 @@ All interactions with the Sourcegraph open source project are governed by the 6. Once you've chosen an issue, **comment on it to announce that you will be working on it**, making it visible for others that this issue is being tackled. If you end up not creating a pull request for this issue, please delete your comment. 7. If you have any questions, please [refer to the docs first](https://docs.sourcegraph.com/). If you don’t find any relevant information, mention the issue author. 8. The issue author will try to provide guidance. Sourcegraph always works in async mode. We will try to answer as soon as possible, but please keep time zones differences in mind. +9. Join the [Sourcegraph Community Space](https://srcgr.ph/join-community-space) on Discord where the Sourcegraph team can help you! ## Can I pick up this issue? @@ -47,7 +48,6 @@ All open issues are not yet solved. If the task is interesting to you, take it a ### Pull Requests - [How to structure](https://docs.sourcegraph.com/dev/background-information/pull_request_reviews#what-makes-an-effective-pull-request-pr) -- [PR guidelines](https://handbook.sourcegraph.com/departments/engineering/dev/onboarding/pr-checklist/) - Git branch name convention: `[developer-initials]/short-feature-description` - [Examples on Github](https://github.com/sourcegraph/sourcegraph/pulls?q=is%3Apr+label%3Ateam%2Ffrontend-platform) - (For Sourcegraph team) [How to accept contributions](https://docs.sourcegraph.com/dev/contributing/accepting_contribution) diff --git a/WORKSPACE b/WORKSPACE index 17712c0947c39..4605a4cfe3ea8 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -15,9 +15,9 @@ bazel_skylib_workspace() http_archive( name = "aspect_bazel_lib", - sha256 = "cbf473d630ab67b36461d83b38fdc44e56f45b78d03c405e4958280211124d79", - strip_prefix = "bazel-lib-1.36.0", - url = "https://github.com/aspect-build/bazel-lib/releases/download/v1.36.0/bazel-lib-v1.36.0.tar.gz", + sha256 = "262e3d6693cdc16dd43880785cdae13c64e6a3f63f75b1993c716295093d117f", + strip_prefix = "bazel-lib-1.38.1", + url = "https://github.com/aspect-build/bazel-lib/releases/download/v1.38.1/bazel-lib-v1.38.1.tar.gz", ) # rules_js defines an older rules_nodejs, so we override it here @@ -51,10 +51,19 @@ http_archive( http_archive( name = "io_bazel_rules_go", - sha256 = "51dc53293afe317d2696d4d6433a4c33feedb7748a9e352072e2ec3c0dafd2c6", + sha256 = "d6ab6b57e48c09523e93050f13698f708428cfd5e619252e369d377af6597707", urls = [ - "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.40.1/rules_go-v0.40.1.zip", - "https://github.com/bazelbuild/rules_go/releases/download/v0.40.1/rules_go-v0.40.1.zip", + "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.43.0/rules_go-v0.43.0.zip", + "https://github.com/bazelbuild/rules_go/releases/download/v0.43.0/rules_go-v0.43.0.zip", + ], +) + +http_archive( + name = "rules_proto", + sha256 = "dc3fb206a2cb3441b485eb1e423165b231235a1ea9b031b4433cf7bc1fa460dd", + strip_prefix = "rules_proto-5.3.0-21.7", + urls = [ + "https://github.com/bazelbuild/rules_proto/archive/refs/tags/5.3.0-21.7.tar.gz", ], ) @@ -67,19 +76,19 @@ http_archive( http_archive( name = "rules_buf", - sha256 = "523a4e06f0746661e092d083757263a249fedca535bd6dd819a8c50de074731a", - strip_prefix = "rules_buf-0.1.1", + sha256 = "bc2488ee497c3fbf2efee19ce21dceed89310a08b5a9366cc133dd0eb2118498", + strip_prefix = "rules_buf-0.2.0", urls = [ - "https://github.com/bufbuild/rules_buf/archive/refs/tags/v0.1.1.zip", + "https://github.com/bufbuild/rules_buf/archive/refs/tags/v0.2.0.zip", ], ) http_archive( name = "bazel_gazelle", - sha256 = "ecba0f04f96b4960a5b250c8e8eeec42281035970aa8852dda73098274d14a1d", + sha256 = "b7387f72efb59f876e4daae42f1d3912d0d45563eac7cb23d1de0b094ab588cf", urls = [ - "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.29.0/bazel-gazelle-v0.29.0.tar.gz", - "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.29.0/bazel-gazelle-v0.29.0.tar.gz", + "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.34.0/bazel-gazelle-v0.34.0.tar.gz", + "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.34.0/bazel-gazelle-v0.34.0.tar.gz", ], ) @@ -113,6 +122,13 @@ http_archive( urls = ["https://github.com/GoogleContainerTools/container-structure-test/archive/104a53ede5f78fff72172639781ac52df9f5b18f.zip"], ) +http_archive( + name = "buildifier_prebuilt", + sha256 = "e46c16180bc49487bfd0f1ffa7345364718c57334fa0b5b67cb5f27eba10f309", + strip_prefix = "buildifier-prebuilt-6.1.0", + urls = ["https://github.com/keith/buildifier-prebuilt/archive/6.1.0.tar.gz"], +) + # hermetic_cc_toolchain setup ================================ HERMETIC_CC_TOOLCHAIN_VERSION = "v2.1.2" @@ -223,32 +239,8 @@ esbuild_register_toolchains( ) # Go toolchain setup -load("@rules_proto_grpc//:repositories.bzl", "rules_proto_grpc_repos", "rules_proto_grpc_toolchains") - -rules_proto_grpc_toolchains() - -rules_proto_grpc_repos() - -load("@rules_proto_grpc//doc:repositories.bzl", rules_proto_grpc_doc_repos = "doc_repos") - -rules_proto_grpc_doc_repos() - -load("@rules_proto//proto:repositories.bzl", "rules_proto_dependencies", "rules_proto_toolchains") - -rules_proto_dependencies() - -rules_proto_toolchains() - -load("@rules_buf//buf:repositories.bzl", "rules_buf_dependencies", "rules_buf_toolchains") - -rules_buf_dependencies() - -rules_buf_toolchains(version = "v1.11.0") - -load("@rules_buf//gazelle/buf:repositories.bzl", "gazelle_buf_dependencies") - -gazelle_buf_dependencies() +load("@rules_buf//buf:defs.bzl", "buf_dependencies") load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies") load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies", "go_repository") load("//:linter_deps.bzl", "linter_dependencies") @@ -269,7 +261,7 @@ go_repository( build_file_proto_mode = "disable_global", importpath = "google.golang.org/protobuf", sum = "h1:7QBf+IK2gx70Ap/hDsOmam3GE0v9HicjfEdAxE62UoM=", - version = "v1.29.1", + version = "v1.31.1", ) # keep # Pin protoc-gen-go-grpc to 1.3.0 @@ -289,23 +281,19 @@ go_rules_dependencies() go_register_toolchains( nogo = "@//:sg_nogo", - version = "1.20.10", + version = "1.21.4", ) linter_dependencies() gazelle_dependencies() -load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps") - -protobuf_deps() - # rust toolchain setup load("@rules_rust//rust:repositories.bzl", "rules_rust_dependencies", "rust_register_toolchains", "rust_repository_set") rules_rust_dependencies() -rust_version = "1.68.0" +rust_version = "1.73.0" rust_register_toolchains( edition = "2021", @@ -380,10 +368,6 @@ load("//dev:oci_deps.bzl", "oci_deps") oci_deps() -load("//cmd/embeddings/shared:assets.bzl", "embbedings_assets_deps") - -embbedings_assets_deps() - load("@container_structure_test//:repositories.bzl", "container_structure_test_register_toolchain") container_structure_test_register_toolchain(name = "cst") @@ -395,3 +379,47 @@ tool_deps() load("//tools/release:schema_deps.bzl", "schema_deps") schema_deps() + +# Buildifier +load("@buildifier_prebuilt//:deps.bzl", "buildifier_prebuilt_deps") + +buildifier_prebuilt_deps() + +load("@buildifier_prebuilt//:defs.bzl", "buildifier_prebuilt_register_toolchains") + +buildifier_prebuilt_register_toolchains() + +load("@rules_proto//proto:repositories.bzl", "rules_proto_dependencies", "rules_proto_toolchains") + +rules_proto_dependencies() + +rules_proto_toolchains() + +load("@rules_proto_grpc//:repositories.bzl", "rules_proto_grpc_repos", "rules_proto_grpc_toolchains") +load("@rules_proto_grpc//go:repositories.bzl", rules_proto_grpc_go_repos = "go_repos") +load("@rules_proto_grpc//doc:repositories.bzl", rules_proto_grpc_doc_repos = "doc_repos") + +rules_proto_grpc_toolchains() + +rules_proto_grpc_repos() + +rules_proto_grpc_go_repos() + +rules_proto_grpc_doc_repos() + +load("@rules_buf//buf:repositories.bzl", "rules_buf_dependencies", "rules_buf_toolchains") + +rules_buf_dependencies() + +rules_buf_toolchains( + sha256 = "05dfb45d2330559d258e1230f5a25e154f0a328afda2a434348b5ba4c124ece7", + version = "v1.28.1", +) + +load("@rules_buf//gazelle/buf:repositories.bzl", "gazelle_buf_dependencies") + +gazelle_buf_dependencies() + +load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps") + +protobuf_deps() diff --git a/client/branded/BUILD.bazel b/client/branded/BUILD.bazel index 92672052ddfff..bb5f6da18d254 100644 --- a/client/branded/BUILD.bazel +++ b/client/branded/BUILD.bazel @@ -86,6 +86,7 @@ ts_project( "src/search-ui/components/RepoMetadata.tsx", "src/search-ui/components/RepoSearchResult.tsx", "src/search-ui/components/ResultContainer.tsx", + "src/search-ui/components/SearchResultPreviewButton.tsx", "src/search-ui/components/SearchResultStar.tsx", "src/search-ui/components/SmartSearchPreview.tsx", "src/search-ui/components/SymbolSearchResult.tsx", @@ -135,6 +136,7 @@ ts_project( "src/search-ui/input/experimental/utils.ts", "src/search-ui/input/toggles/QueryInputToggle.tsx", "src/search-ui/input/toggles/SmartSearchToggle.tsx", + "src/search-ui/input/toggles/SmartSearchToggleExtended.tsx", "src/search-ui/input/toggles/Toggles.tsx", "src/search-ui/input/toggles/index.ts", "src/search-ui/results/AnnotatedSearchExample.tsx", @@ -151,12 +153,13 @@ ts_project( "src/search-ui/results/sidebar/QuickLink.tsx", "src/search-ui/results/sidebar/SearchFilterSection.tsx", "src/search-ui/results/sidebar/SearchReference.tsx", - "src/search-ui/results/sidebar/SearchSidebar.tsx", "src/search-ui/results/sidebar/SearchTypeLink.tsx", + "src/search-ui/results/sidebar/StickySearchSidebar.tsx", "src/search-ui/results/sidebar/helpers.ts", "src/search-ui/results/sidebar/revisions.ts", "src/search-ui/results/use-items-to-show.ts", "src/search-ui/results/useSearchResultsKeyboardNavigation.ts", + "src/search-ui/stores/results-store.ts", "src/search-ui/util/index.ts", "src/search-ui/util/query.ts", "src/search-ui/util/stars.ts", @@ -198,6 +201,7 @@ ts_project( "//:node_modules/ts-key-enum", "//:node_modules/use-callback-ref", "//:node_modules/use-resize-observer", + "//:node_modules/zustand", "//client/shared:graphql_operations", "//client/shared:shared_lib", ], @@ -279,6 +283,7 @@ vitest_test( timeout = "long", data = [ ":branded_tests", + ":module_styles", ":snapshots", ], ) diff --git a/client/branded/src/search-ui/components/FileContentSearchResult.tsx b/client/branded/src/search-ui/components/FileContentSearchResult.tsx index e229e1439a9d1..4ce09b83e8ac4 100644 --- a/client/branded/src/search-ui/components/FileContentSearchResult.tsx +++ b/client/branded/src/search-ui/components/FileContentSearchResult.tsx @@ -25,6 +25,7 @@ import { CopyPathAction } from './CopyPathAction' import { FileMatchChildren } from './FileMatchChildren' import { RepoFileLink } from './RepoFileLink' import { ResultContainer } from './ResultContainer' +import { SearchResultPreviewButton } from './SearchResultPreviewButton' import resultContainerStyles from './ResultContainer.module.scss' import styles from './SearchResult.module.scss' @@ -105,6 +106,14 @@ export const FileContentSearchResult: React.FunctionComponent { + const settings = settingsCascade.final + if (!isErrorLike(settings)) { + return settings?.experimentalFeatures?.newSearchNavigationUI + } + return false + }, [settingsCascade]) + // The number of lines of context to show before and after each match. const context = useMemo(() => { if (location?.pathname === '/search') { @@ -250,6 +259,7 @@ export const FileContentSearchResult: React.FunctionComponent} >
void @@ -26,10 +29,19 @@ export const FilePathSearchResult: React.FunctionComponent { const repoAtRevisionURL = getRepositoryUrl(result.repository, result.branches) const revisionDisplayName = getRevision(result.branches, result.commit) + const newSearchUIEnabled = useMemo(() => { + const settings = settingsCascade.final + if (!isErrorLike(settings)) { + return settings?.experimentalFeatures?.newSearchNavigationUI + } + return false + }, [settingsCascade]) + const title = ( } >
{result.pathMatches ? 'Path match' : 'File contains matching content'} diff --git a/client/branded/src/search-ui/components/ResultContainer.module.scss b/client/branded/src/search-ui/components/ResultContainer.module.scss index 5f0462676b385..ded4f5494c281 100644 --- a/client/branded/src/search-ui/components/ResultContainer.module.scss +++ b/client/branded/src/search-ui/components/ResultContainer.module.scss @@ -17,7 +17,10 @@ align-items: center; flex-wrap: wrap; position: sticky; - top: 0; + // With 0 value there is a rendering bug in Safari where this block + // doesn't fit tight enough and hence it's leaving a gap between sticky + // header and top of the scrolling block + top: -1px; z-index: 1; // Show on top of search result contents background-color: var(--body-bg); diff --git a/client/branded/src/search-ui/components/ResultContainer.tsx b/client/branded/src/search-ui/components/ResultContainer.tsx index 3a472444d32f3..ef0cb9cb61077 100644 --- a/client/branded/src/search-ui/components/ResultContainer.tsx +++ b/client/branded/src/search-ui/components/ResultContainer.tsx @@ -1,11 +1,11 @@ -import React from 'react' +import React, { ReactElement } from 'react' import classNames from 'classnames' import type { SearchMatch } from '@sourcegraph/shared/src/search/stream' import type { ForwardReferenceExoticComponent } from '@sourcegraph/wildcard' -import { formatRepositoryStarCount } from '../util/stars' +import { formatRepositoryStarCount } from '../util' import { CodeHostIcon } from './CodeHostIcon' import { LastSyncedIcon } from './LastSyncedIcon' @@ -24,6 +24,7 @@ export interface ResultContainerProps { repoName?: string className?: string rankingDebug?: string + actions?: ReactElement | boolean onResultClicked?: () => void } @@ -55,9 +56,10 @@ export const ResultContainer: ForwardReferenceExoticComponent< repoName, className, rankingDebug, + actions, + repoLastFetched, as: Component = 'div', onResultClicked, - repoLastFetched, } = props const formattedRepositoryStarCount = formatRepositoryStarCount(repoStars) @@ -84,6 +86,8 @@ export const ResultContainer: ForwardReferenceExoticComponent< > {title}
+ + {actions} {formattedRepositoryStarCount && ( diff --git a/client/branded/src/search-ui/components/SearchResultPreviewButton.tsx b/client/branded/src/search-ui/components/SearchResultPreviewButton.tsx new file mode 100644 index 0000000000000..88f3aa1ddb0a3 --- /dev/null +++ b/client/branded/src/search-ui/components/SearchResultPreviewButton.tsx @@ -0,0 +1,33 @@ +import { FC } from 'react' + +import { Button } from '@sourcegraph/wildcard' + +import { useSearchResultState, type SearchResultPreview } from '../stores/results-store' + +interface SearchResultPreviewButtonProps { + result: SearchResultPreview +} + +export const SearchResultPreviewButton: FC = props => { + const { result } = props + const { previewBlob, setPreviewBlob, clearPreview } = useSearchResultState() + + const isActive = + previewBlob?.repository === result.repository && + previewBlob?.path === result.path && + previewBlob.commit === result.commit + + const handleClick = (): void => { + if (isActive) { + clearPreview() + } else { + setPreviewBlob(result) + } + } + + return ( + + ) +} diff --git a/client/branded/src/search-ui/index.ts b/client/branded/src/search-ui/index.ts index 4ebcebd22bf86..22551a352f7de 100644 --- a/client/branded/src/search-ui/index.ts +++ b/client/branded/src/search-ui/index.ts @@ -10,10 +10,12 @@ export * from './results/progress/StreamingProgressCount' export * from './results/progress/utils' export * from './results/sidebar/FilterLink' export * from './results/sidebar/revisions' -export * from './results/sidebar/SearchSidebar' +export * from './results/sidebar/StickySearchSidebar' export * from './results/sidebar/QuickLink' export * from './results/sidebar/helpers' export * from './results/sidebar/SearchReference' export * from './results/sidebar/SearchTypeLink' export * from './results/StreamingSearchResultsList' export * from './util' + +export { useSearchResultState } from './stores/results-store' diff --git a/client/branded/src/search-ui/input/codemirror/token-info.ts b/client/branded/src/search-ui/input/codemirror/token-info.ts index 265fc6acae1ca..d28be2b88704e 100644 --- a/client/branded/src/search-ui/input/codemirror/token-info.ts +++ b/client/branded/src/search-ui/input/codemirror/token-info.ts @@ -86,7 +86,7 @@ export function tokenInfo(): Extension { effect.is(setHighlighedTokenPosition) ) if (effect) { - position = effect?.value + position = effect.value } if (position !== null) { // Mapping the position might not be necessary since we clear diff --git a/client/branded/src/search-ui/input/experimental/suggestionsExtension.ts b/client/branded/src/search-ui/input/experimental/suggestionsExtension.ts index 9dffabf44282b..975ae6320a09a 100644 --- a/client/branded/src/search-ui/input/experimental/suggestionsExtension.ts +++ b/client/branded/src/search-ui/input/experimental/suggestionsExtension.ts @@ -245,6 +245,16 @@ enum QueryState { Complete, } +/** + * Used to identify whether two queries refer to the same + * completion request. A new completion request is triggered + * by e.g. typing into the query input or moving the cursor. + * This is used to properly reset internal state. + * + * Because objects are always unique we can use them as IDs. + */ +interface CompletionID {} + /** * Wrapper around the configered sources. Keeps track of the state and results. */ @@ -252,7 +262,8 @@ class Query { constructor( public readonly sources: readonly Source[], public readonly state: QueryState, - public readonly result: Result + public readonly result: Result, + private readonly completionID: CompletionID ) {} public update(transaction: Transaction): Query { @@ -276,7 +287,7 @@ class Query { // Only "apply" the effect if the results are for the curent query. This prevents // overwriting the results from stale requests. if (effect.is(updateResultEffect) && effect.value.query === query) { - query = query.updateWithSuggestionResult(effect.value.result) + query = query.updateWithSuggestionResult(effect.value.result, query.completionID) } else if (effect.is(startCompletionEffect)) { query = query.run(transaction.state) } else if (effect.is(hideCompletionEffect)) { @@ -287,23 +298,28 @@ class Query { return query } + private createNewCompletionID(): CompletionID { + // We use an object as completion ID because objects are always unique. + return {} + } + private run(state: EditorState): Query { const selectedMode = getSelectedMode(state) const activeSources = this.sources.filter(source => source.mode === selectedMode?.name) const result = combineResults( activeSources.map(source => source.query(state, state.selection.main.head, selectedMode?.name)) ) - return this.updateWithSuggestionResult(result) + return this.updateWithSuggestionResult(result, this.createNewCompletionID()) } - private updateWithSuggestionResult(result: SuggestionResult): Query { + private updateWithSuggestionResult(result: SuggestionResult, completionID: CompletionID): Query { return result.next - ? new PendingQuery(this.sources, Result.fromSuggestionResult(result), result.next) - : new Query(this.sources, QueryState.Complete, Result.fromSuggestionResult(result)) + ? new PendingQuery(this.sources, Result.fromSuggestionResult(result), result.next, completionID) + : new Query(this.sources, QueryState.Complete, Result.fromSuggestionResult(result), completionID) } private updateWithState(state: QueryState.Inactive | QueryState.Complete): Query { - return state !== this.state ? new Query(this.sources, state, this.result) : this + return state !== this.state ? new Query(this.sources, state, this.result, this.completionID) : this } public isInactive(): boolean { @@ -313,15 +329,22 @@ class Query { public isPending(): this is PendingQuery { return this.state === QueryState.Pending } + + public isSameRequest(query: Query): boolean { + return this.completionID === query.completionID + } } class PendingQuery extends Query { constructor( public readonly sources: readonly Source[], public readonly result: Result, - public readonly fetch: () => Promise + public readonly fetch: () => Promise, + // Used to identify whether two queries refer to the same + // completion request. + completionID: CompletionID ) { - super(sources, QueryState.Pending, result) + super(sources, QueryState.Pending, result, completionID) } } @@ -378,15 +401,16 @@ class SuggestionsState { let state: SuggestionsState = this const sources = transaction.state.facet(suggestionSources) - let query = sources === state.query.sources ? state.query : new Query(sources, QueryState.Inactive, emptyResult) + let query = + sources === state.query.sources ? state.query : new Query(sources, QueryState.Inactive, emptyResult, {}) query = query.update(transaction) if (query !== state.query) { state = new SuggestionsState( query, !query.isInactive(), - // Preserve the currently selected option if the query was pending - // (ensures that the selected option doesn't change when new options become available) - state.query.isPending() ? state.selectedOption : -1 + // Preserve the currently selected option if the query _was_ pending and refers to the same request. + // This ensures that the selected option doesn't change as new options become available. + state.query.isPending() && state.query.isSameRequest(query) ? state.selectedOption : -1 ) } @@ -436,7 +460,7 @@ const hideCompletionEffect = StateEffect.define() const updateResultEffect = StateEffect.define<{ query: Query; result: SuggestionResult }>() const suggestionsStateField = StateField.define({ create() { - return new SuggestionsState(new Query([], QueryState.Inactive, emptyResult), false, -1) + return new SuggestionsState(new Query([], QueryState.Inactive, emptyResult, {}), false, -1) }, update(state, transaction) { diff --git a/client/branded/src/search-ui/input/toggles/SmartSearchToggleExtended.tsx b/client/branded/src/search-ui/input/toggles/SmartSearchToggleExtended.tsx new file mode 100644 index 0000000000000..f0eb473ee7fed --- /dev/null +++ b/client/branded/src/search-ui/input/toggles/SmartSearchToggleExtended.tsx @@ -0,0 +1,171 @@ +import React, { useCallback, useEffect, useState } from 'react' + +import { mdiClose, mdiRadioboxBlank, mdiRadioboxMarked, mdiHeart } from '@mdi/js' +import classNames from 'classnames' + +import { + Button, + Icon, + Input, + Label, + Popover, + PopoverContent, + PopoverTrigger, + Position, + Tooltip, + H4, + H2, +} from '@sourcegraph/wildcard' + +import type { ToggleProps } from './QueryInputToggle' + +import smartStyles from './SmartSearchToggle.module.scss' +import styles from './Toggles.module.scss' + +export const smartSearchIconSvgPath = + 'M11.3956 20H10.2961L11.3956 13.7778H7.54754C6.58003 13.7778 7.18473 13.1111 7.20671 13.0844C8.62499 11.0578 10.7579 8.03556 13.6054 4H14.7049L13.6054 10.2222H17.4645C17.9042 10.2222 18.1461 10.3911 17.9042 10.8089C13.5615 16.9333 11.3956 20 11.3956 20Z' + +export enum SearchModes { + Smart = 'Smart', + PreciseNew = 'Precise (NEW) πŸ’–', + Precise = 'Precise (legacy)', +} + +interface SmartSearchToggleProps extends Omit { + onSelect: (mode: SearchModes) => void + mode: SearchModes +} + +/** + * A toggle displayed in the QueryInput. + */ +export const SmartSearchToggleExtended: React.FunctionComponent = ({ + onSelect, + interactive = true, + mode, + className, +}) => { + const tooltipValue = mode.toString() + + const interactiveProps = interactive ? {} : { tabIndex: -1, 'aria-hidden': true } + + const [isPopoverOpen, setIsPopoverOpen] = useState(false) + + return ( + setIsPopoverOpen(event.isOpen)}> + + + + + + + setIsPopoverOpen(false)} /> + + ) +} + +const SmartSearchToggleMenu: React.FunctionComponent< + Pick & { closeMenu: () => void } +> = ({ onSelect, mode, closeMenu }) => { + const [getMode, setMode] = useState(mode) + useEffect(() => { + setMode(mode) + }, [mode]) + + const onChange = useCallback( + (value: SearchModes) => { + setMode(value) + // Wait a tiny bit for user to see the selection change before closing the popover + setTimeout(() => { + onSelect(value) + closeMenu() + }, 100) + }, + [onSelect, closeMenu] + ) + + return ( + +
+

+ Search Mode Picker +

+ +
+ + + +
+ ) +} + +const RadioItem: React.FunctionComponent<{ + value: SearchModes + isChecked: boolean + onSelect: (value: SearchModes) => void + header: string + description: string +}> = ({ value, isChecked, onSelect, header, description }) => ( + +) diff --git a/client/branded/src/search-ui/input/toggles/Toggles.tsx b/client/branded/src/search-ui/input/toggles/Toggles.tsx index 48e5cea2bab9e..82f2960680756 100644 --- a/client/branded/src/search-ui/input/toggles/Toggles.tsx +++ b/client/branded/src/search-ui/input/toggles/Toggles.tsx @@ -5,17 +5,18 @@ import classNames from 'classnames' import { SearchPatternType } from '@sourcegraph/shared/src/graphql-operations' import { - type SearchPatternTypeProps, type CaseSensitivityProps, type SearchPatternTypeMutationProps, type SubmitSearchProps, SearchMode, type SearchModeProps, + type SearchPatternTypeProps, } from '@sourcegraph/shared/src/search' import { findFilter, FilterKind } from '@sourcegraph/shared/src/search/query/query' import { QueryInputToggle } from './QueryInputToggle' import { SmartSearchToggle } from './SmartSearchToggle' +import { SmartSearchToggleExtended, SearchModes } from './SmartSearchToggleExtended' import styles from './Toggles.module.scss' @@ -28,6 +29,11 @@ export interface TogglesProps navbarSearchQuery: string className?: string showSmartSearchButton?: boolean + /** + * If set to true, the search mode picker will let the user select the new + * pattern type as a new alternative + */ + showExtendedPicker?: boolean /** * If set to false makes all buttons non-actionable. The main use case for * this prop is showing the toggles in examples. This is different from @@ -53,6 +59,7 @@ export const Toggles: React.FunctionComponent { + const newPatternType = + patternType !== SearchPatternType.newStandardRC1 + ? SearchPatternType.newStandardRC1 + : SearchPatternType.standard + + setPatternType(newPatternType) + + // We always want precise mode when switching to the experimental pattern type. + setSearchMode(SearchMode.Precise) + + submitOnToggle({ newPatternType }) + }, [patternType, setPatternType, submitOnToggle, setSearchMode]) + const toggleStructuralSearch = useCallback((): void => { const newPatternType: SearchPatternType = patternType !== SearchPatternType.structural ? SearchPatternType.structural : SearchPatternType.standard @@ -100,10 +121,30 @@ export const Toggles: React.FunctionComponent { const newSearchMode: SearchMode = enabled ? SearchMode.SmartSearch : SearchMode.Precise + // Disable the experimental pattern type the user activates smart search + if (patternType === SearchPatternType.newStandardRC1) { + setPatternType(SearchPatternType.standard) + } + setSearchMode(newSearchMode) submitOnToggle({ newSearchMode }) }, - [setSearchMode, submitOnToggle] + [setSearchMode, submitOnToggle, patternType, setPatternType] + ) + + // This is hacky and is just for demo purposes. Once we have made the new + // pattern type the default we can revert this. + const onSelectSearchMode = useCallback( + (mode: SearchModes): void => { + if (mode === SearchModes.Smart) { + onSelectSmartSearch(true) + } else if (mode === SearchModes.PreciseNew) { + toggleNewStandard() + } else { + onSelectSmartSearch(false) + } + }, + [onSelectSmartSearch, toggleNewStandard] ) return ( @@ -168,14 +209,28 @@ export const Toggles: React.FunctionComponent {showSmartSearchButton &&
} - {showSmartSearchButton && ( - - )} + {showSmartSearchButton && + (showExtendedPicker ? ( + + ) : ( + + ))}
) diff --git a/client/branded/src/search-ui/results/NoResultsPage.tsx b/client/branded/src/search-ui/results/NoResultsPage.tsx index a7a12944b8341..148ce8a938e5a 100644 --- a/client/branded/src/search-ui/results/NoResultsPage.tsx +++ b/client/branded/src/search-ui/results/NoResultsPage.tsx @@ -3,12 +3,7 @@ import React, { useCallback, useEffect } from 'react' import { mdiClose, mdiOpenInNew } from '@mdi/js' import classNames from 'classnames' -import { - type QueryState, - type SearchContextProps, - SearchMode, - type SubmitSearchParameters, -} from '@sourcegraph/shared/src/search' +import { type SearchContextProps, SearchMode, type SubmitSearchParameters } from '@sourcegraph/shared/src/search' import { NoResultsSectionID as SectionID } from '@sourcegraph/shared/src/settings/temporary/searchSidebar' import { useTemporarySetting } from '@sourcegraph/shared/src/settings/temporary/useTemporarySetting' import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService' @@ -53,7 +48,6 @@ interface NoResultsPageProps extends TelemetryProps, Pick void searchMode?: SearchMode setSearchMode?: (mode: SearchMode) => void submitSearch?: (parameters: SubmitSearchParameters) => void @@ -68,7 +62,6 @@ export const NoResultsPage: React.FunctionComponent )} - {showQueryExamples && setQueryState && ( + {showQueryExamples && ( <>

Search basics

diff --git a/client/branded/src/search-ui/results/StreamingSearchResultsList.tsx b/client/branded/src/search-ui/results/StreamingSearchResultsList.tsx index 12df1a126ffb4..370c3b3ac819e 100644 --- a/client/branded/src/search-ui/results/StreamingSearchResultsList.tsx +++ b/client/branded/src/search-ui/results/StreamingSearchResultsList.tsx @@ -26,12 +26,14 @@ import { import type { SettingsCascadeProps } from '@sourcegraph/shared/src/settings/settings' import type { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService' -import { CommitSearchResult } from '../components/CommitSearchResult' -import { FileContentSearchResult } from '../components/FileContentSearchResult' -import { FilePathSearchResult } from '../components/FilePathSearchResult' +import { + CommitSearchResult, + FileContentSearchResult, + FilePathSearchResult, + RepoSearchResult, + SymbolSearchResult, +} from '../components' import { OwnerSearchResult } from '../components/OwnerSearchResult' -import { RepoSearchResult } from '../components/RepoSearchResult' -import { SymbolSearchResult } from '../components/SymbolSearchResult' import { NoResultsPage } from './NoResultsPage' import { StreamingSearchResultFooter } from './StreamingSearchResultsFooter' @@ -77,7 +79,6 @@ export interface StreamingSearchResultsListProps * allow modifying the query. */ queryState?: QueryState - setQueryState?: (queryState: QueryState) => void buildSearchURLQueryFromQueryState?: (queryParameters: BuildSearchQueryURLParameters) => string searchMode?: SearchMode @@ -92,7 +93,7 @@ export interface StreamingSearchResultsListProps * An optional callback invoked whenever a search result is clicked. * It's passed the index of the result in the list and the result type. */ - logSearchResultClicked?: (index: number, type: string) => void + logSearchResultClicked?: (index: number, type: string, resultsLength: number) => void enableRepositoryMetadata?: boolean } @@ -116,7 +117,6 @@ export const StreamingSearchResultsList: React.FunctionComponent< enableKeyboardNavigation, showQueryExamplesOnNoResultsPage, queryState, - setQueryState, buildSearchURLQueryFromQueryState, searchMode, setSearchMode, @@ -157,7 +157,7 @@ export const StreamingSearchResultsList: React.FunctionComponent< location={location} telemetryService={telemetryService} result={result} - onSelect={() => logSearchResultClicked?.(index, 'fileMatch')} + onSelect={() => logSearchResultClicked?.(index, 'fileMatch', resultsNumber)} defaultExpanded={false} showAllMatches={false} allExpanded={allExpanded} @@ -173,7 +173,7 @@ export const StreamingSearchResultsList: React.FunctionComponent< index={index} telemetryService={telemetryService} result={result} - onSelect={() => logSearchResultClicked?.(index, 'symbolMatch')} + onSelect={() => logSearchResultClicked?.(index, 'symbolMatch', resultsNumber)} fetchHighlightedFileLineRanges={fetchHighlightedFileLineRanges} repoDisplayName={displayRepoName(result.repository)} settingsCascade={settingsCascade} @@ -185,10 +185,11 @@ export const StreamingSearchResultsList: React.FunctionComponent< logSearchResultClicked?.(index, 'filePathMatch')} + onSelect={() => logSearchResultClicked?.(index, 'filePathMatch', resultsNumber)} repoDisplayName={displayRepoName(result.repository)} containerClassName={resultClassName} telemetryService={telemetryService} + settingsCascade={settingsCascade} /> )} @@ -200,7 +201,7 @@ export const StreamingSearchResultsList: React.FunctionComponent< index={index} result={result} platformContext={platformContext} - onSelect={() => logSearchResultClicked?.(index, 'commit')} + onSelect={() => logSearchResultClicked?.(index, 'commit', resultsNumber)} openInNewTab={openMatchesInNewTab} containerClassName={resultClassName} as="li" @@ -212,7 +213,7 @@ export const StreamingSearchResultsList: React.FunctionComponent< logSearchResultClicked?.(index, 'repo')} + onSelect={() => logSearchResultClicked?.(index, 'repo', resultsNumber)} containerClassName={resultClassName} buildSearchURLQueryFromQueryState={buildSearchURLQueryFromQueryState} queryState={queryState} @@ -228,7 +229,7 @@ export const StreamingSearchResultsList: React.FunctionComponent< index={index} result={result} as="li" - onSelect={() => logSearchResultClicked?.(index, 'person')} + onSelect={() => logSearchResultClicked?.(index, 'person', resultsNumber)} containerClassName={resultClassName} telemetryService={telemetryService} queryState={queryState} @@ -255,6 +256,7 @@ export const StreamingSearchResultsList: React.FunctionComponent< prefetchFileEnabled, prefetchFile, location, + resultsNumber, telemetryService, allExpanded, fetchHighlightedFileLineRanges, @@ -306,7 +308,6 @@ export const StreamingSearchResultsList: React.FunctionComponent< telemetryService={telemetryService} showSearchContext={searchContextsEnabled} showQueryExamples={showQueryExamplesOnNoResultsPage} - setQueryState={setQueryState} searchMode={searchMode} setSearchMode={setSearchMode} submitSearch={submitSearch} diff --git a/client/branded/src/search-ui/results/progress/exhaustive-search/exhaustive-search-validation.test.ts b/client/branded/src/search-ui/results/progress/exhaustive-search/exhaustive-search-validation.test.ts index 5242ea42589d7..c0eb3e589af3a 100644 --- a/client/branded/src/search-ui/results/progress/exhaustive-search/exhaustive-search-validation.test.ts +++ b/client/branded/src/search-ui/results/progress/exhaustive-search/exhaustive-search-validation.test.ts @@ -11,25 +11,49 @@ describe('exhaustive search validation', () => { expect(validateQueryForExhaustiveSearch('context:global rev:* insights')).toStrictEqual([]) }) + test('[repo:has.content() predicate]', () => { + expect(validateQueryForExhaustiveSearch('context:global repo:has.content(insights) foo').length).toStrictEqual( + 0 + ) + }) + + test('[repo:has.meta() predicate]', () => { + expect(validateQueryForExhaustiveSearch('context:global repo:has.meta(insights) foo').length).toStrictEqual(0) + }) + describe('works properly with invalid query', () => { test('[multiple rev operator case]', () => { expect(validateQueryForExhaustiveSearch('context:global rev:* insights rev:vk').length).toStrictEqual(1) }) - test('[has regexp generic patter]', () => { + test('[has regexp generic pattern]', () => { expect(validateQueryForExhaustiveSearch('context:global .* patterntype:regexp').length).toStrictEqual(1) }) - test('[repo:has.file() operator]', () => { - expect(validateQueryForExhaustiveSearch('context:global repo:has.file(insights)').length).toStrictEqual(1) - }) - - test('[file:has.content() operator]', () => { + test('[file:has.content() predicate]', () => { expect(validateQueryForExhaustiveSearch('context:global file:has.content(insights)').length).toStrictEqual( 1 ) }) + test('[file:has.owner() predicate]', () => { + expect( + validateQueryForExhaustiveSearch('context:global file:has.owner(insights) foo').length + ).toStrictEqual(1) + }) + + test('[f:has.contributor() predicate]', () => { + expect( + validateQueryForExhaustiveSearch('context:global f:has.contributor(insights) foo').length + ).toStrictEqual(1) + }) + + test('[f:contains.content() predicate]', () => { + expect( + validateQueryForExhaustiveSearch('context:global f:contains.content(insights) foo').length + ).toStrictEqual(1) + }) + test('[or operator]', () => { expect(validateQueryForExhaustiveSearch('insights or batch-changes').length).toStrictEqual(1) }) @@ -49,9 +73,9 @@ describe('exhaustive search validation', () => { test('[all cases combined]', () => { expect( validateQueryForExhaustiveSearch( - 'context:global (repo:has.file(insights) rev:* ) or (file:has.content(batch-changes) rev:vk batch) or (patterntype:regexp .*)' + 'context:global (file:has.content(batch-changes) rev:vk batch) or (patterntype:regexp .* rev:foo)' ).length - ).toStrictEqual(5) + ).toStrictEqual(4) }) }) }) diff --git a/client/branded/src/search-ui/results/progress/exhaustive-search/exhaustive-search-validation.ts b/client/branded/src/search-ui/results/progress/exhaustive-search/exhaustive-search-validation.ts index d3a3c2f7736df..96e0bbaba67a8 100644 --- a/client/branded/src/search-ui/results/progress/exhaustive-search/exhaustive-search-validation.ts +++ b/client/branded/src/search-ui/results/progress/exhaustive-search/exhaustive-search-validation.ts @@ -7,7 +7,6 @@ enum ValidationErrorType { INVALID_QUERY = 'invalid_query', GENERIC_REGEXP = 'generic_regexp', HAS_CONTENT_PREDICATE = 'has_content_predicate', - HAS_FILE_PREDICATE = 'has_file_predicate', OR_OPERATOR = 'or_operator', AND_OPERATOR = 'and_operator', } @@ -68,21 +67,16 @@ export function validateQueryForExhaustiveSearch(query: string): ValidationError }) } - const repoHasContentFilter = filters.some(filter => filter.value?.value.startsWith('has.content(')) - - if (repoHasContentFilter) { + const filePredicates = ['has.content', 'has.owner', 'has.contributor', 'contains.content'] + const fileHasContentFilter = filters.some( + filter => + filePredicates.some(startString => filter.value?.value.startsWith(startString)) && + filter.field.value?.startsWith('f') + ) + if (fileHasContentFilter) { validationErrors.push({ type: ValidationErrorType.HAS_CONTENT_PREDICATE, - reason: 'repo.has.content predicate is not compatible', - }) - } - - const repoHasFileFilter = filters.some(filter => filter.value?.value.startsWith('has.file(')) - - if (repoHasFileFilter) { - validationErrors.push({ - type: ValidationErrorType.HAS_FILE_PREDICATE, - reason: 'repo.has.file predicate is not compatible', + reason: 'file: predicate is not compatible', }) } diff --git a/client/branded/src/search-ui/results/sidebar/SearchSidebar.tsx b/client/branded/src/search-ui/results/sidebar/StickySearchSidebar.tsx similarity index 87% rename from client/branded/src/search-ui/results/sidebar/SearchSidebar.tsx rename to client/branded/src/search-ui/results/sidebar/StickySearchSidebar.tsx index 22e90d8f8e796..697ca0580cbbd 100644 --- a/client/branded/src/search-ui/results/sidebar/SearchSidebar.tsx +++ b/client/branded/src/search-ui/results/sidebar/StickySearchSidebar.tsx @@ -45,27 +45,8 @@ interface SearchSidebarProps extends HTMLAttributes { * Also provides shared through context internal state for compound SearchSidebarSection * components. */ -export const SearchSidebar: FC> = props => { +export const StickySearchSidebar: FC> = props => { const { children, className, onClose, ...attributes } = props - const [collapsedSections, setCollapsedSections] = useTemporarySetting('search.collapsedSidebarSections', {}) - - const persistToggleState = useCallback( - (id: string, open: boolean) => { - setCollapsedSections(openSections => { - const newSettings: TemporarySettings['search.collapsedSidebarSections'] = { - ...openSections, - [id]: !open, - } - return newSettings - }) - }, - [setCollapsedSections] - ) - - const sidebarStore = useMemo( - () => ({ collapsedSections, persistToggleState }), - [collapsedSections, persistToggleState] - ) return (
- { - // collapsedSections is undefined on first render. To prevent the sections - // being rendered open and immediately closing them, we render them only after - // we got the settings. - collapsedSections && ( - {children} - ) - } + {children} ) } +export const PersistSidebarStoreProvider: FC> = props => { + const [collapsedSections, setCollapsedSections] = useTemporarySetting('search.collapsedSidebarSections', {}) + + const persistToggleState = useCallback( + (id: string, open: boolean) => { + setCollapsedSections(openSections => { + const newSettings: TemporarySettings['search.collapsedSidebarSections'] = { + ...openSections, + [id]: !open, + } + return newSettings + }) + }, + [setCollapsedSections] + ) + + const sidebarStore = useMemo( + () => ({ collapsedSections, persistToggleState }), + [collapsedSections, persistToggleState] + ) + + // collapsedSections is undefined on first render. To prevent the sections + // being rendered open and immediately closing them, we render them only after + // we got the settings. + if (!collapsedSections) { + return null + } + + return {props.children} +} + interface SearchSidebarSectionProps extends Omit, 'startCollapsed' | 'onToggle'> { sectionId: SectionID diff --git a/client/branded/src/search-ui/stores/results-store.ts b/client/branded/src/search-ui/stores/results-store.ts new file mode 100644 index 0000000000000..0373ef0adffab --- /dev/null +++ b/client/branded/src/search-ui/stores/results-store.ts @@ -0,0 +1,21 @@ +import create from 'zustand' + +import type { ContentMatch, PathMatch } from '@sourcegraph/shared/src/search/stream' + +/** + * At the moment search result preview panel supports only + * blob-like type of search results to preview. + */ +export type SearchResultPreview = ContentMatch | PathMatch + +export interface SearchResultState { + previewBlob: SearchResultPreview | null + setPreviewBlob: (blobInfo: SearchResultPreview) => void + clearPreview: () => void +} + +export const useSearchResultState = create((set, get) => ({ + previewBlob: null, + setPreviewBlob: blobInfo => set({ previewBlob: blobInfo }), + clearPreview: () => set({ previewBlob: null }), +})) diff --git a/client/browser/BUILD.bazel b/client/browser/BUILD.bazel index 635297bfd794c..e82df65f0ed2b 100644 --- a/client/browser/BUILD.bazel +++ b/client/browser/BUILD.bazel @@ -342,6 +342,7 @@ vitest_test( timeout = "moderate", data = [ ":browser_tests", + ":module_styles", ":snapshots", ], ) diff --git a/client/codeintellify/vitest.config.ts b/client/codeintellify/vitest.config.ts index e16b5fdd7c1ab..0a2927f06bd01 100644 --- a/client/codeintellify/vitest.config.ts +++ b/client/codeintellify/vitest.config.ts @@ -3,7 +3,6 @@ import { defineProjectWithDefaults } from '../../vitest.shared' export default defineProjectWithDefaults(__dirname, { test: { environment: 'jsdom', - setupFiles: ['src/testSetup.test.ts'], - singleThread: true, // got `failed to terminate worker` occasionally in Bazel CI + setupFiles: ['src/testSetup.test.ts', '../testing/src/fetch.js'], }, }) diff --git a/client/shared/BUILD.bazel b/client/shared/BUILD.bazel index f41c3fa1eef27..3676c9e7e3ce8 100644 --- a/client/shared/BUILD.bazel +++ b/client/shared/BUILD.bazel @@ -353,6 +353,7 @@ ts_project( "//:node_modules/@types/minimatch", "//:node_modules/@types/node", "//:node_modules/@types/react", + "//:node_modules/@types/whatwg-url", "//:node_modules/classnames", "//:node_modules/comlink", "//:node_modules/core-js", @@ -372,6 +373,7 @@ ts_project( "//:node_modules/use-deep-compare-effect", "//:node_modules/util", "//:node_modules/utility-types", + "//:node_modules/whatwg-url", "//:node_modules/zustand", "//schema:settings", #keep ], @@ -519,6 +521,7 @@ filegroup( vitest_test( name = "test", data = [ + ":module_styles", ":shared_tests", ":snapshots", ], diff --git a/client/shared/dev/BUILD.bazel b/client/shared/dev/BUILD.bazel index 167a8daf28e04..c7ea16f4212f8 100644 --- a/client/shared/dev/BUILD.bazel +++ b/client/shared/dev/BUILD.bazel @@ -1,5 +1,5 @@ load("@aspect_rules_js//js:defs.bzl", "js_binary", "js_library") -load("//dev:defs.bzl", "ts_binary", "ts_project") +load("//dev:defs.bzl", "ts_binary") # gazelle:ignore @@ -57,6 +57,7 @@ js_binary( name = "run_mocha_tests_with_percy", data = [ "//:node_modules/@percy/cli", + "//:node_modules/@percy/puppeteer", "//:node_modules/mocha", "//:node_modules/puppeteer", "//:node_modules/resolve-bin", diff --git a/client/shared/dev/generateCssModulesTypes.ts b/client/shared/dev/generateCssModulesTypes.ts index 120f76190ccfd..12494db2777e3 100644 --- a/client/shared/dev/generateCssModulesTypes.ts +++ b/client/shared/dev/generateCssModulesTypes.ts @@ -2,8 +2,8 @@ import { spawn } from 'child_process' import path from 'path' const REPO_ROOT = path.join(__dirname, '../../..') -const CSS_MODULES_GLOB = path.resolve(__dirname, '../../*/src/**/*.module.scss') -const JETBRAINS_CSS_MODULES_GLOB = path.resolve(__dirname, '../../jetbrains/webview/**/*.module.scss') +const CSS_MODULES_GLOB = path.resolve(__dirname, '../../*/src/**/*.module.{scss,css}') +const JETBRAINS_CSS_MODULES_GLOB = path.resolve(__dirname, '../../jetbrains/webview/**/*.module.{scss,css}') const TSM_COMMAND = `pnpm exec tsm --logLevel error "{${CSS_MODULES_GLOB},${JETBRAINS_CSS_MODULES_GLOB}}" --includePaths node_modules client` const [BIN, ...ARGS] = TSM_COMMAND.split(' ') diff --git a/client/shared/dev/runMochaTestsWithPercy.js b/client/shared/dev/runMochaTestsWithPercy.js index 7758a75145651..eaf467e7ea46a 100644 --- a/client/shared/dev/runMochaTestsWithPercy.js +++ b/client/shared/dev/runMochaTestsWithPercy.js @@ -13,11 +13,10 @@ const resolveBin = require('resolve-bin') /** @returns {Record} env vars */ function getEnvVars() { // JS_BINARY__EXECROOT – Set by Bazel `js_run_binary` rule. - // BAZEL_VOLATILE_STATUS_FILE – Set by Bazel when the `stamp` attribute on the `js_run_binary_rule` equals 1. - // https://docs.aspect.build/rules/aspect_rules_js/docs/js_run_binary#stamp - const { JS_BINARY__EXECROOT, BAZEL_VOLATILE_STATUS_FILE } = process.env + // BAZEL_BINDIR – Set by Bazel `js_run_binary` rule. + const { JS_BINARY__EXECROOT, BAZEL_BINDIR } = process.env - if (!JS_BINARY__EXECROOT || !BAZEL_VOLATILE_STATUS_FILE) { + if (!JS_BINARY__EXECROOT || !BAZEL_BINDIR) { throw new Error('Missing required environment variables') } @@ -26,7 +25,18 @@ function getEnvVars() { // build the correct visual diff report and auto-accept change on `main` if we're on it. // https://github.com/percy/cli/blob/059ec21653a07105e223aa5a3ec1f815a7123ad7/packages/env/src/environment.js#L138-L139 // https://bazel.build/docs/user-manual#workspace-status - const statusFilePath = path.join(JS_BINARY__EXECROOT, BAZEL_VOLATILE_STATUS_FILE) + // + // NB: we derive the volatile-status.txt file path from the BAZEL_BINDIR since we are + // intentionally pulling volatile data without defining the volatile status as an input so we + // don't bust the cache with its contents of volatile-status.txt + // (https://github.com/bazelbuild/bazel/issues/16231). This can be improved in the future by using + // the new --experimental_remote_cache_key_ignore_stamping flag in Bazel to filter out the + // volatile-status.txt file from the action inputs + // (https://github.com/bazelbuild/bazel/pull/16240) + const statusFilePath = path.join( + path.dirname(path.dirname(path.join(JS_BINARY__EXECROOT, BAZEL_BINDIR))), + 'volatile-status.txt' + ) const volatileEnvVariables = Object.fromEntries( readFileSync(statusFilePath, 'utf8') .split('\n') diff --git a/client/shared/src/api/extension/test/extensionHost.documentHighlights.test.ts b/client/shared/src/api/extension/test/extensionHost.documentHighlights.test.ts index 6d1e9ec821728..fe722decf79a0 100644 --- a/client/shared/src/api/extension/test/extensionHost.documentHighlights.test.ts +++ b/client/shared/src/api/extension/test/extensionHost.documentHighlights.test.ts @@ -37,7 +37,7 @@ describe('getDocumentHighlights from ExtensionHost API, it aims to have more e2e }) it('restarts document highlights call if a provider was added or removed', () => { - const typescriptFileUri = 'file:///f.ts' + const typescriptFileUri = 'git://repo#src/f.ts' const { extensionHostAPI, extensionAPI } = initializeExtensionHostTest( { initialSettings, clientApplication: 'sourcegraph', sourcegraphURL: 'https://example.com/' }, diff --git a/client/shared/src/api/extension/test/extensionHost.hover.test.ts b/client/shared/src/api/extension/test/extensionHost.hover.test.ts index ef8c3d7a77934..4de966aac1442 100644 --- a/client/shared/src/api/extension/test/extensionHost.hover.test.ts +++ b/client/shared/src/api/extension/test/extensionHost.hover.test.ts @@ -38,7 +38,7 @@ describe('getHover from ExtensionHost API, it aims to have more e2e feel', () => }) it('restarts hover call if a provider was added or removed', () => { - const typescriptFileUri = 'file:///f.ts' + const typescriptFileUri = 'git://repo#src/f.ts' const { extensionHostAPI, extensionAPI } = initializeExtensionHostTest( { initialSettings, clientApplication: 'sourcegraph', sourcegraphURL: 'https://example.com/' }, diff --git a/client/shared/src/api/extension/test/extensionHost.providers.test.ts b/client/shared/src/api/extension/test/extensionHost.providers.test.ts index 51244bbe0447f..9363670947d0b 100644 --- a/client/shared/src/api/extension/test/extensionHost.providers.test.ts +++ b/client/shared/src/api/extension/test/extensionHost.providers.test.ts @@ -13,6 +13,8 @@ const scheduler = (): TestScheduler => new TestScheduler((a, b) => expect(a).toE type Provider = RegisteredProvider> +const documentURI = 'git://repo#src/f.ts' + const getResultsFromProviders = (providersObservable: Observable, document: TextDocumentIdentifier) => callProviders( providersObservable, @@ -31,18 +33,19 @@ describe('callProviders()', () => { describe('1 provider', () => { it('returns empty non loading result with no providers', () => { scheduler().run(({ cold, expectObservable }) => - expectObservable( - getResultsFromProviders(cold('-a', { a: [] }), { uri: 'file:///f.ts' }) - ).toBe('-a', { - a: { isLoading: false, result: [] }, - }) + expectObservable(getResultsFromProviders(cold('-a', { a: [] }), { uri: documentURI })).toBe( + '-a', + { + a: { isLoading: false, result: [] }, + } + ) ) }) it('returns a result from a provider synchronously with raw values', () => { scheduler().run(({ cold, expectObservable }) => expectObservable( - getResultsFromProviders(cold('-a', { a: [provide(1)] }), { uri: 'file:///f.ts' }) + getResultsFromProviders(cold('-a', { a: [provide(1)] }), { uri: documentURI }) ).toBe('-(lr)', { l: { isLoading: true, result: [LOADING] }, r: { isLoading: false, result: [1] }, @@ -54,7 +57,7 @@ describe('callProviders()', () => { scheduler().run(({ cold, expectObservable }) => expectObservable( getResultsFromProviders(cold('-a', { a: [provide(cold('--a', { a: 1 }))] }), { - uri: 'file:///f.ts', + uri: documentURI, }) ).toBe('-l-r', { l: { isLoading: true, result: [LOADING] }, @@ -69,7 +72,7 @@ describe('callProviders()', () => { scheduler().run(({ cold, expectObservable }) => expectObservable( getResultsFromProviders(cold('-a', { a: [provide(1), provide(2)] }), { - uri: 'file:///f.ts', + uri: documentURI, }) ).toBe('-(lr)', { l: { isLoading: true, result: [1, LOADING] }, @@ -85,7 +88,7 @@ describe('callProviders()', () => { cold('-a', { a: [provide(cold('-a', { a: 1 })), provide(cold('-a', { a: 2 }))], }), - { uri: 'file:///f.ts' } + { uri: documentURI } ) ).toBe('-lr', { l: { isLoading: true, result: [LOADING, LOADING] }, @@ -101,7 +104,7 @@ describe('callProviders()', () => { cold('-a', { a: [provide(cold('-a', { a: 1 })), provide(cold('-#', {}, new Error('boom!')))], }), - { uri: 'file:///f.ts' } + { uri: documentURI } ) ).toBe('-lr', { l: { isLoading: true, result: [LOADING, LOADING] }, @@ -116,7 +119,7 @@ describe('callProviders()', () => { scheduler().run(({ cold, expectObservable }) => expectObservable( getResultsFromProviders(cold('-a', { a: [provide(1, '*.ts'), provide(2, '*.js')] }), { - uri: 'file:///f.ts', + uri: documentURI, }) ).toBe('-(lr)', { l: { isLoading: true, result: [LOADING] }, @@ -135,7 +138,7 @@ describe('callProviders()', () => { a: [provide(cold('-a', { a: 1 })), provide(cold('-a', { a: 2 }))], b: [provide(cold('-a', { a: 3 }))], }), - { uri: 'file:///f.ts' } + { uri: documentURI } ) ).toBe('-abcd', { a: { isLoading: true, result: [LOADING, LOADING] }, diff --git a/client/shared/src/backend/apolloCache.ts b/client/shared/src/backend/apolloCache.ts index dc6396126190a..3861fedbf484e 100644 --- a/client/shared/src/backend/apolloCache.ts +++ b/client/shared/src/backend/apolloCache.ts @@ -35,6 +35,8 @@ export const generateCache = (): InMemoryCache => Changeset: ['ExternalChangeset', 'HiddenExternalChangeset'], TeamMember: ['User'], Owner: ['Person', 'Team'], + TreeEntry: ['GitBlob', 'GitTree'], + File2: ['GitBlob', 'VirtualFile'], }, }) diff --git a/client/shared/src/backend/repo.ts b/client/shared/src/backend/repo.ts index 5934b0a1e3910..a0e380828c387 100644 --- a/client/shared/src/backend/repo.ts +++ b/client/shared/src/backend/repo.ts @@ -74,7 +74,7 @@ export const fetchTreeEntries = memoizeObservable( fragment TreeFields on GitTree { isRoot url - entries(first: $first, recursiveSingleChild: true) { + entries(first: $first) { ...TreeEntryFields } } @@ -87,7 +87,6 @@ export const fetchTreeEntries = memoizeObservable( url commit } - isSingleChild } `, variables: args, diff --git a/client/shared/src/codeintel/legacy-extensions/lsif/api.ts b/client/shared/src/codeintel/legacy-extensions/lsif/api.ts index ef92ee84efb47..b9e6c2ac87013 100644 --- a/client/shared/src/codeintel/legacy-extensions/lsif/api.ts +++ b/client/shared/src/codeintel/legacy-extensions/lsif/api.ts @@ -23,7 +23,7 @@ export async function queryLSIF

( }: P, queryGraphQL: QueryGraphQLFn> ): Promise { - const { repo, commit, path } = parseGitURI(new URL(uri)) + const { repo, commit, path } = parseGitURI(uri) const queryArguments = { repository: repo, commit, path, ...rest } const data = await queryGraphQL(query, queryArguments) return data.repository?.commit?.blob?.lsif || null diff --git a/client/shared/src/codeintel/legacy-extensions/lsif/locations.ts b/client/shared/src/codeintel/legacy-extensions/lsif/locations.ts index 803a385772079..620470a6e4a7f 100644 --- a/client/shared/src/codeintel/legacy-extensions/lsif/locations.ts +++ b/client/shared/src/codeintel/legacy-extensions/lsif/locations.ts @@ -23,7 +23,7 @@ export function nodeToLocation( textDocument: sourcegraph.TextDocument, { resource: { repository, commit, path }, range }: LocationConnectionNode ): sourcegraph.Location { - const { repo: currentRepo, commit: currentCommit } = parseGitURI(new URL(textDocument.uri)) + const { repo: currentRepo, commit: currentCommit } = parseGitURI(textDocument.uri) return { uri: new URL(`git://${repository?.name || currentRepo}?${commit?.oid || currentCommit}#${path}`), diff --git a/client/shared/src/codeintel/legacy-extensions/lsif/providers.ts b/client/shared/src/codeintel/legacy-extensions/lsif/providers.ts index 9f4e5205323a0..7b84d533241f5 100644 --- a/client/shared/src/codeintel/legacy-extensions/lsif/providers.ts +++ b/client/shared/src/codeintel/legacy-extensions/lsif/providers.ts @@ -1,11 +1,8 @@ -import { once } from 'lodash' - import * as scip from '../../scip' import type * as sourcegraph from '../api' import type { Logger } from '../logging' import type { CombinedProviders, DefinitionAndHover } from '../providers' import { cache } from '../util' -import { API } from '../util/api' import { queryGraphQL as sgQueryGraphQL, type QueryGraphQLFn } from '../util/graphql' import { asyncGeneratorFromPromise } from '../util/ix' import { raceWithDelayOffset } from '../util/promise' @@ -26,10 +23,7 @@ import { makeStencilFn, type StencilFn } from './stencil' export function createProviders(hasImplementationsField: boolean, logger: Logger): CombinedProviders { const providers = createGraphQLProviders( sgQueryGraphQL, - makeStencilFn( - sgQueryGraphQL, - once(() => new API().hasStencils()) - ), + makeStencilFn(sgQueryGraphQL), makeRangeWindowFactory(hasImplementationsField, sgQueryGraphQL) ) diff --git a/client/shared/src/codeintel/legacy-extensions/lsif/stencil.ts b/client/shared/src/codeintel/legacy-extensions/lsif/stencil.ts index 8a98687f4f493..37b791e39ee08 100644 --- a/client/shared/src/codeintel/legacy-extensions/lsif/stencil.ts +++ b/client/shared/src/codeintel/legacy-extensions/lsif/stencil.ts @@ -8,13 +8,8 @@ import { type GenericLSIFResponse, queryLSIF } from './api' export const stencil = async ( uri: string, - hasStencilSupport: () => Promise, queryGraphQL: QueryGraphQLFn> = sgQueryGraphQL ): Promise => { - if (!(await hasStencilSupport())) { - return undefined - } - const response = await queryLSIF({ query: stencilQuery, uri }, queryGraphQL) return response?.stencil } @@ -45,6 +40,5 @@ const stencilQuery = gql` export type StencilFn = (uri: string) => Promise export const makeStencilFn = ( - queryGraphQL: QueryGraphQLFn>, - hasStencilSupport: () => Promise = () => Promise.resolve(true) -): StencilFn => cache(uri => stencil(uri, hasStencilSupport, queryGraphQL), { max: 10 }) + queryGraphQL: QueryGraphQLFn> +): StencilFn => cache(uri => stencil(uri, queryGraphQL), { max: 10 }) diff --git a/client/shared/src/codeintel/legacy-extensions/providers.ts b/client/shared/src/codeintel/legacy-extensions/providers.ts index 43b9207d55074..916c57253e5b6 100644 --- a/client/shared/src/codeintel/legacy-extensions/providers.ts +++ b/client/shared/src/codeintel/legacy-extensions/providers.ts @@ -233,7 +233,7 @@ export function createDefinitionProvider( textDocument: sourcegraph.TextDocument, position: sourcegraph.Position ): AsyncGenerator { - const { repo } = parseGitURI(new URL(textDocument.uri)) + const { repo } = parseGitURI(textDocument.uri) const repoId = (await api.resolveRepo(repo)).id const emitter = new TelemetryEmitter(languageID, repoId, !quiet) const commonFields = { provider: 'definition', repo, textDocument, position, emitter, logger } @@ -324,7 +324,7 @@ export function createReferencesProvider( position: sourcegraph.Position, context: sourcegraph.ReferenceContext ): AsyncGenerator { - const { repo } = parseGitURI(new URL(textDocument.uri)) + const { repo } = parseGitURI(textDocument.uri) const repoId = (await api.resolveRepo(repo)).id const emitter = new TelemetryEmitter(languageID, repoId) const commonFields = { repo, textDocument, position, emitter, logger, provider: 'references' } @@ -432,7 +432,7 @@ export function createImplementationsProvider( textDocument: sourcegraph.TextDocument, position: sourcegraph.Position ): AsyncGenerator { - const { repo } = parseGitURI(new URL(textDocument.uri)) + const { repo } = parseGitURI(textDocument.uri) const repoId = (await api.resolveRepo(repo)).id const emitter = new TelemetryEmitter(languageID, repoId) const commonFields = { repo, textDocument, position, emitter, logger, provider: 'implementations' } @@ -476,7 +476,7 @@ function logLocationResults, emitter?.emitOnce(action) // Emit xrepo event if we contain a result from another repository - if (asArray(results).some(location => parseGitURI(location.uri).repo !== repo)) { + if (asArray(results).some(location => parseGitURI(location.uri.toString()).repo !== repo)) { emitter?.emitOnce(action + '.xrepo') } @@ -492,7 +492,7 @@ function logLocationResults, arrayResults = arrayResults.slice(0, 500) } - const { path } = parseGitURI(new URL(textDocument.uri)) + const { path } = parseGitURI(textDocument.uri) const { line, character } = position logger.log({ @@ -533,7 +533,7 @@ export function createHoverProvider( textDocument: sourcegraph.TextDocument, position: sourcegraph.Position ): AsyncGenerator | null | undefined, void, undefined> { - const { repo, path } = parseGitURI(new URL(textDocument.uri)) + const { repo, path } = parseGitURI(textDocument.uri) const repoId = (await api.resolveRepo(repo)).id const emitter = new TelemetryEmitter(languageID, repoId) const commonLogFields = { path, line: position.line, character: position.character } @@ -626,7 +626,7 @@ export function createDocumentHighlightProvider( textDocument: sourcegraph.TextDocument, position: sourcegraph.Position ): AsyncGenerator { - const { repo } = parseGitURI(new URL(textDocument.uri)) + const { repo } = parseGitURI(textDocument.uri) const repoId = (await api.resolveRepo(repo)).id const emitter = new TelemetryEmitter(languageID, repoId) diff --git a/client/shared/src/codeintel/legacy-extensions/search/providers.test.ts b/client/shared/src/codeintel/legacy-extensions/search/providers.test.ts index 07c5ff531f72c..2bbc1403904d3 100644 --- a/client/shared/src/codeintel/legacy-extensions/search/providers.test.ts +++ b/client/shared/src/codeintel/legacy-extensions/search/providers.test.ts @@ -139,9 +139,6 @@ describe('search providers', () => { const stubResolveRepo = sinon.stub(api, 'resolveRepo') stubResolveRepo.callsFake(repo => Promise.resolve({ name: repo, isFork, isArchived, id })) - const stubHasLocalCodeIntelField = sinon.stub(api, 'hasLocalCodeIntelField') - stubHasLocalCodeIntelField.callsFake(() => Promise.resolve(true)) - const stubFindSymbol = sinon.stub(api, 'findLocalSymbol') stubFindSymbol.callsFake(() => Promise.resolve(undefined)) diff --git a/client/shared/src/codeintel/legacy-extensions/search/providers.ts b/client/shared/src/codeintel/legacy-extensions/search/providers.ts index 5ffd2c13280b4..20d5908a58ed6 100644 --- a/client/shared/src/codeintel/legacy-extensions/search/providers.ts +++ b/client/shared/src/codeintel/legacy-extensions/search/providers.ts @@ -60,7 +60,7 @@ export function createProviders( cachedFileContents.delete(Array.from(cachedFileContents.keys())[index]) } - const { repo, commit, path } = parseGitURI(new URL(uri)) + const { repo, commit, path } = parseGitURI(uri) const fileContent = api.getFileContent(repo, commit, path) cachedFileContents.set(uri, fileContent) return fileContent @@ -119,7 +119,7 @@ export function createProviders( return null } const { text, searchToken } = contentAndToken - const { repo, commit, path } = parseGitURI(new URL(textDocument.uri)) + const { repo, commit, path } = parseGitURI(textDocument.uri) const { isFork, isArchived } = await api.resolveRepo(repo) // Construct base definition query without scoping terms @@ -173,7 +173,7 @@ export function createProviders( return [] } const { searchToken } = contentAndToken - const { repo, commit } = parseGitURI(new URL(textDocument.uri)) + const { repo, commit } = parseGitURI(textDocument.uri) const { isFork, isArchived } = await api.resolveRepo(repo) // Construct base references query without scoping terms diff --git a/client/shared/src/codeintel/legacy-extensions/search/queries.ts b/client/shared/src/codeintel/legacy-extensions/search/queries.ts index a88452a9a30ae..60d7124b1a471 100644 --- a/client/shared/src/codeintel/legacy-extensions/search/queries.ts +++ b/client/shared/src/codeintel/legacy-extensions/search/queries.ts @@ -72,7 +72,7 @@ const excludelist = new Set(['thrift', 'proto', 'graphql']) * @param includelist The file extensions for the current language. */ function fileExtensionTerm(textDocument: sourcegraph.TextDocument, includelist: string[]): string { - const { path } = parseGitURI(new URL(textDocument.uri)) + const { path } = parseGitURI(textDocument.uri) const extension = extname(path).slice(1) if (!extension || excludelist.has(extension) || !includelist.includes(extension)) { return '' diff --git a/client/shared/src/codeintel/legacy-extensions/search/squirrel.ts b/client/shared/src/codeintel/legacy-extensions/search/squirrel.ts index f6044f985f06b..b410662dcac79 100644 --- a/client/shared/src/codeintel/legacy-extensions/search/squirrel.ts +++ b/client/shared/src/codeintel/legacy-extensions/search/squirrel.ts @@ -12,7 +12,7 @@ export const mkSquirrel = (api: API): PromiseProviders => ({ const local = await api.findLocalSymbol(document, position) if (local?.def) { - return mkSourcegraphLocation({ ...parseGitURI(new URL(document.uri)), range: local.def }) + return mkSourcegraphLocation({ ...parseGitURI(document.uri), range: local.def }) } const symbolInfo = await api.fetchSymbolInfo(document, position) @@ -30,7 +30,7 @@ export const mkSquirrel = (api: API): PromiseProviders => ({ length: symbolInfo.definition.range.length, }, } - return mkSourcegraphLocation({ ...parseGitURI(new URL(document.uri)), ...location }) + return mkSourcegraphLocation({ ...parseGitURI(document.uri), ...location }) }, async references(document, position) { const symbol = await api.findLocalSymbol(document, position) @@ -40,9 +40,7 @@ export const mkSquirrel = (api: API): PromiseProviders => ({ symbol.refs = sortBy(symbol.refs, reference => reference.row) - return symbol.refs.map(reference => - mkSourcegraphLocation({ ...parseGitURI(new URL(document.uri)), range: reference }) - ) + return symbol.refs.map(reference => mkSourcegraphLocation({ ...parseGitURI(document.uri), range: reference })) }, async hover(document, position) { const symbol = await api.findLocalSymbol(document, position) diff --git a/client/shared/src/codeintel/legacy-extensions/util/api.ts b/client/shared/src/codeintel/legacy-extensions/util/api.ts index bc2d042d65acf..20c251ff6ba2c 100644 --- a/client/shared/src/codeintel/legacy-extensions/util/api.ts +++ b/client/shared/src/codeintel/legacy-extensions/util/api.ts @@ -1,4 +1,3 @@ -import { once } from 'lodash' import gql from 'tagged-template-noop' import { isErrorLike } from '@sourcegraph/common' @@ -86,7 +85,7 @@ export class API { } const metaRequest = (async (name: string): Promise => { - const queryWithFork = gql` + const query = gql` query LegacyResolveRepo($name: String!) { repository(name: $name) { id @@ -97,14 +96,6 @@ export class API { } ` - const queryWithoutFork = gql` - query LegacyResolveRepo2($name: String!) { - repository(name: $name) { - name - } - } - ` - interface Response { repository: { id: string @@ -114,7 +105,7 @@ export class API { } } - const data = await queryGraphQL((await this.hasForkField()) ? queryWithFork : queryWithoutFork, { + const data = await queryGraphQL(query, { name, }) @@ -131,107 +122,6 @@ export class API { return metaRequest } - /** - * Determines via introspection if the GraphQL API has isFork field on the Repository type. - * - * TODO(efritz) - Remove this when we no longer need to support pre-3.15 instances. - */ - private async hasForkField(): Promise { - const introspectionQuery = gql` - query LegacyRepositoryIntrospection { - __type(name: "Repository") { - fields { - name - } - } - } - ` - - interface IntrospectionResponse { - __type: { fields: { name: string }[] } - } - - return (await queryGraphQL(introspectionQuery)).__type.fields.some( - field => field.name === 'isFork' - ) - } - - /** - * Determines via introspection if the GraphQL API has local code intelligence available - * - * TODO(chrismwendt) - Remove this when we no longer need to support versions without local code - * intelligence - */ - public hasLocalCodeIntelField = once(async () => { - const introspectionQuery = gql` - query LegacyLocalCodeIntelIntrospectionQuery { - __type(name: "GitBlob") { - fields { - name - } - } - } - ` - - interface IntrospectionResponse { - __type: { fields: { name: string }[] } - } - - return (await queryGraphQL(introspectionQuery)).__type.fields.some( - field => field.name === 'localCodeIntel' - ) - }) - - /** - * Determines via introspection if the GraphQL API has symbol info available - * - * TODO(chrismwendt) - Remove this when we no longer need to support versions without symbol info - */ - public hasSymbolInfo = once(async () => { - const introspectionQuery = gql` - query LegacySymbolInfoIntrospectionQuery { - __type(name: "GitBlob") { - fields { - name - } - } - } - ` - - interface IntrospectionResponse { - __type: { fields: { name: string }[] } - } - - return (await queryGraphQL(introspectionQuery)).__type.fields.some( - field => field.name === 'symbolInfo' - ) - }) - - /** - * Determines via introspection if the GraphQL API has symbolInfo.range available - * - * TODO(chrismwendt) - Remove this when we no longer need to support versions without symbolInfo.range - */ - public hasSymbolLocationRange = once(async () => { - const introspectionQuery = gql` - query LegacySymbolLocationRangeIntrospectionQuery { - __type(name: "SymbolLocation") { - fields { - name - } - } - } - ` - - interface IntrospectionResponse { - __type: { fields: { name: string }[] } - } - - return (await queryGraphQL(introspectionQuery)).__type.fields.some( - field => field.name === 'range' - ) - }) - public fetchLocalCodeIntelPayload = cache( async ({ repo, commit, path }: RepoCommitPath): Promise => { const vars = { repository: repo, commit, path } @@ -260,11 +150,7 @@ export class API { document: sourcegraph.TextDocument, position: sourcegraph.Position ): Promise => { - if (!(await this.hasLocalCodeIntelField())) { - return - } - - const { repo, commit, path } = parseGitURI(new URL(document.uri)) + const { repo, commit, path } = parseGitURI(document.uri) const payload = await this.fetchLocalCodeIntelPayload({ repo, commit, path }) if (!payload) { @@ -291,20 +177,12 @@ export class API { document: sourcegraph.TextDocument, position: sourcegraph.Position ): Promise => { - if (!(await this.hasSymbolInfo())) { - return - } - - const query = (await this.hasSymbolLocationRange()) - ? symbolInfoDefinitionQueryWithRange - : symbolInfoDefinitionQueryWithoutRange - - const { repo, commit, path } = parseGitURI(new URL(document.uri)) + const { repo, commit, path } = parseGitURI(document.uri) const vars = { repository: repo, commit, path, line: position.line, character: position.character } const response = await (async (): Promise => { try { - return await queryGraphQL(query, vars) + return await queryGraphQL(symbolInfoDefinitionQuery, vars) } catch (error) { if (isKnownSquirrelErrorLike(error)) { return { repository: null } @@ -373,34 +251,6 @@ export class API { }) return data.search.results.results.filter(isDefined) } - - /** - * Determines via introspection if the GraphQL API supports stencils - * - * TODO(chrismwendt) - Remove this when we no longer need to support Sourcegraph versions that don't - * have stencil support - */ - public async hasStencils(): Promise { - const introspectionQuery = gql` - query LegacyStencilIntrospectionQuery { - __type(name: "GitBlobLSIFData") { - fields { - name - } - } - } - ` - - interface IntrospectionResponse { - __type: { fields: { name: string }[] } - } - - return Boolean( - (await queryGraphQL(introspectionQuery)).__type?.fields.some( - field => field.name === 'stencil' - ) - ) - } } function buildSearchQuery(fileLocal: boolean): string { @@ -577,29 +427,7 @@ const symbolInfoFlexibleToCanonical = (flexible: SymbolInfoFlexible): SymbolInfo hover: flexible.hover, }) -const symbolInfoDefinitionQueryWithoutRange = gql` - query LegacySymbolInfo($repository: String!, $commit: String!, $path: String!, $line: Int!, $character: Int!) { - repository(name: $repository) { - commit(rev: $commit) { - blob(path: $path) { - symbolInfo(line: $line, character: $character) { - definition { - repo - commit - path - line - character - length - } - hover - } - } - } - } - } -` - -const symbolInfoDefinitionQueryWithRange = gql` +const symbolInfoDefinitionQuery = gql` query LegacySymbolInfo2($repository: String!, $commit: String!, $path: String!, $line: Int!, $character: Int!) { repository(name: $repository) { commit(rev: $commit) { diff --git a/client/shared/src/codeintel/legacy-extensions/util/uri.test.ts b/client/shared/src/codeintel/legacy-extensions/util/uri.test.ts index 217626fd7048d..a19a0e3a8734f 100644 --- a/client/shared/src/codeintel/legacy-extensions/util/uri.test.ts +++ b/client/shared/src/codeintel/legacy-extensions/util/uri.test.ts @@ -7,9 +7,7 @@ import { parseGitURI } from './uri' describe('parseGitURI', () => { it('returns components', () => { assert.deepStrictEqual( - parseGitURI( - new URL('git://github.com/microsoft/vscode?dbd76d987cf1a412401bdbd3fb785217ac94197e#src/vs/css.js') - ), + parseGitURI('git://github.com/microsoft/vscode?dbd76d987cf1a412401bdbd3fb785217ac94197e#src/vs/css.js'), { repo: 'github.com/microsoft/vscode', commit: 'dbd76d987cf1a412401bdbd3fb785217ac94197e', @@ -21,9 +19,7 @@ describe('parseGitURI', () => { it('decodes repos with spaces', () => { assert.deepStrictEqual( parseGitURI( - new URL( - 'git://sourcegraph.visualstudio.com/Test%20Repo?dbd76d987cf1a412401bdbd3fb785217ac94197e#src/vs/css.js' - ) + 'git://sourcegraph.visualstudio.com/Test%20Repo?dbd76d987cf1a412401bdbd3fb785217ac94197e#src/vs/css.js' ), { repo: 'sourcegraph.visualstudio.com/Test Repo', diff --git a/client/shared/src/codeintel/legacy-extensions/util/uri.ts b/client/shared/src/codeintel/legacy-extensions/util/uri.ts index a57994d0e9d67..c9ac6ee943cc1 100644 --- a/client/shared/src/codeintel/legacy-extensions/util/uri.ts +++ b/client/shared/src/codeintel/legacy-extensions/util/uri.ts @@ -1,12 +1,15 @@ +import { parseRepoURI } from '../../../util/url' + /** * Extracts the components of a text document URI. * - * @param url The text document URL. + * @param uri The text document URL. */ -export function parseGitURI({ hostname, pathname, search, hash }: URL): { repo: string; commit: string; path: string } { +export function parseGitURI(uri: string): { repo: string; commit: string; path: string } { + const result = parseRepoURI(uri) return { - repo: hostname + decodeURIComponent(pathname), - commit: decodeURIComponent(search.slice(1)), - path: decodeURIComponent(hash.slice(1)), + repo: result.repoName, + commit: result.revision ?? '', + path: result.filePath ?? '', } } diff --git a/client/shared/src/search/query/scanner.ts b/client/shared/src/search/query/scanner.ts index a4a559972708c..c060824137513 100644 --- a/client/shared/src/search/query/scanner.ts +++ b/client/shared/src/search/query/scanner.ts @@ -538,6 +538,7 @@ export const scanSearchQuery = ( switch (patternType) { case SearchPatternType.standard: case SearchPatternType.lucky: + case SearchPatternType.newStandardRC1: case SearchPatternType.keyword: { return scanStandard(query) } diff --git a/client/shared/src/search/stream.ts b/client/shared/src/search/stream.ts index 1017f5e6fff76..00b47b536ad24 100644 --- a/client/shared/src/search/stream.ts +++ b/client/shared/src/search/stream.ts @@ -579,21 +579,19 @@ export function getRepositoryUrl(repository: string, branches?: string[]): strin } export function getRevision(branches?: string[], version?: string): string { - let revision = '' - if (branches) { - const branch = branches[0] - if (branch !== '') { - revision = branch - } - } else if (version) { - revision = version + if (branches && branches.length > 0) { + return branches[0] } - - return revision + if (version) { + return version + } + return '' } export function getFileMatchUrl(fileMatch: ContentMatch | SymbolMatch | PathMatch): string { - const revision = getRevision(fileMatch.branches, fileMatch.commit) + // We are not using getRevision here, because we want to flip the logic from + // "branches first" to "revsion first" + const revision = fileMatch.commit ?? fileMatch.branches?.[0] const encodedFilePath = fileMatch.path.split('/').map(encodeURIComponent).join('/') return `/${fileMatch.repository}${revision ? '@' + revision : ''}/-/blob/${encodedFilePath}` } diff --git a/client/shared/src/settings/settings.tsx b/client/shared/src/settings/settings.tsx index 5c4da20b8c68f..7f4ec0362d0af 100644 --- a/client/shared/src/settings/settings.tsx +++ b/client/shared/src/settings/settings.tsx @@ -308,6 +308,7 @@ const defaultFeatures: SettingsExperimentalFeatures = { showMultilineSearchConsole: false, codeMonitoringWebHooks: true, showCodeMonitoringLogs: true, + showFullTreeContext: false, codeInsightsCompute: false, editor: 'codemirror6', codeInsightsRepoUI: 'search-query-or-strict-list', diff --git a/client/shared/src/settings/temporary/TemporarySettings.ts b/client/shared/src/settings/temporary/TemporarySettings.ts index bfe48ac135c22..2af60143a5737 100644 --- a/client/shared/src/settings/temporary/TemporarySettings.ts +++ b/client/shared/src/settings/temporary/TemporarySettings.ts @@ -85,6 +85,8 @@ export interface TemporarySettingsSchema { 'admin.hasDismissedCodeHostPrivacyWarning': boolean 'admin.hasCompletedLicenseCheck': boolean 'simple.search.toggle': boolean + 'cody.onboarding.completed': boolean + 'cody.onboarding.step': number } /** @@ -146,6 +148,8 @@ const TEMPORARY_SETTINGS: Record = { 'admin.hasDismissedCodeHostPrivacyWarning': null, 'admin.hasCompletedLicenseCheck': null, 'simple.search.toggle': null, + 'cody.onboarding.completed': null, + 'cody.onboarding.step': null, } export const TEMPORARY_SETTINGS_KEYS = Object.keys(TEMPORARY_SETTINGS) as readonly (keyof TemporarySettings)[] diff --git a/client/shared/src/util/url.ts b/client/shared/src/util/url.ts index d95ad0622fe37..f02673f05ae32 100644 --- a/client/shared/src/util/url.ts +++ b/client/shared/src/util/url.ts @@ -1,3 +1,5 @@ +import { parseURL } from 'whatwg-url' + import { addLineRangeQueryParameter, encodeURIPathComponent, @@ -177,14 +179,22 @@ const parsePosition = (string: string): Position => { * @deprecated Migrate to using URLs to the Sourcegraph raw API (or other concrete URLs) instead. */ export function parseRepoURI(uri: RepoURI): ParsedRepoURI { - const parsed = new URL(uri) - const repoName = parsed.hostname + decodeURIComponent(parsed.pathname) - const revision = decodeURIComponent(parsed.search.slice('?'.length)) || undefined + // We are not using the environments URL constructor because Chrome and Firefox do + // not correctly parse out the hostname for URLs . We have a polyfill for the main web app + // (see client/shared/src/polyfills/configure-core-js.ts) but that might not be used in all apps. + const parsed = parseURL(uri) + if (!parsed?.host) { + throw new Error('Unable to parse repo URI: ' + uri) + } + const pathname = + typeof parsed.path === 'string' ? parsed.path : parsed.path.length === 0 ? '' : '/' + parsed.path.join('/') + const repoName = String(parsed.host) + decodeURIComponent(pathname) + const revision = parsed.query ? decodeURIComponent(parsed.query) : undefined let commitID: string | undefined if (revision?.match(/[\dA-f]{40}/)) { commitID = revision } - const fragmentSplit = parsed.hash.slice('#'.length).split(':').map(decodeURIComponent) + const fragmentSplit = parsed.fragment ? parsed.fragment.split(':').map(decodeURIComponent) : [] let filePath: string | undefined let position: UIPosition | undefined let range: UIRange | undefined @@ -207,7 +217,7 @@ export function parseRepoURI(uri: RepoURI): ParsedRepoURI { } } if (fragmentSplit.length > 2) { - throw new Error('unexpected fragment: ' + parsed.hash) + throw new Error('unexpected fragment: ' + parsed.fragment) } return { repoName, revision, commitID, filePath: filePath || undefined, position, range } diff --git a/client/testing/BUILD.bazel b/client/testing/BUILD.bazel index 8c3e05d4a1304..a7d81adcd200c 100644 --- a/client/testing/BUILD.bazel +++ b/client/testing/BUILD.bazel @@ -31,6 +31,7 @@ ts_project( "src/mockMatchMedia.ts", "src/mockResizeObserver.ts", "src/mockUniqueId.ts", + "src/perTestSetup.ts", "src/reactCleanup.ts", ], tsconfig = ":tsconfig", @@ -40,6 +41,7 @@ ts_project( "//:node_modules/@testing-library/react", "//:node_modules/@types/lodash", "//:node_modules/@types/mockdate", + "//:node_modules/@types/node", "//:node_modules/lodash", "//:node_modules/mockdate", "//:node_modules/resize-observer-polyfill", diff --git a/client/testing/src/perTestSetup.ts b/client/testing/src/perTestSetup.ts new file mode 100644 index 0000000000000..45686d09cb7f5 --- /dev/null +++ b/client/testing/src/perTestSetup.ts @@ -0,0 +1,9 @@ +import { TextEncoder, TextDecoder } from 'node:util' + +// TextEncoder and TextDecoder are not available in a jsdom environment but +// required in various contexts. +if (!global.TextEncoder) { + global.TextEncoder = TextEncoder + // @ts-expect-error The interface of TextDecoder is not compatible with whatever TS thinks gobal.TextDecoder is. + global.TextDecoder = TextDecoder +} diff --git a/client/web-sveltekit/BUILD.bazel b/client/web-sveltekit/BUILD.bazel index 2a6234f908f5d..b02cf418394a3 100644 --- a/client/web-sveltekit/BUILD.bazel +++ b/client/web-sveltekit/BUILD.bazel @@ -2,9 +2,7 @@ load("@bazel_skylib//rules:build_test.bzl", "build_test") load("//client/shared/dev:generate_graphql_operations.bzl", "generate_graphql_operations") load("@aspect_rules_js//js:defs.bzl", "js_library") load("@npm//:defs.bzl", "npm_link_all_packages") -load("@aspect_rules_js//js:defs.bzl", "js_run_binary", "js_run_devserver") load("@npm//:vite/package_json.bzl", vite_bin = "bin") -load("@aspect_rules_ts//ts:defs.bzl", "ts_config") # gazelle:ignore diff --git a/client/web-sveltekit/src/app.html b/client/web-sveltekit/src/app.html index 02b1c75e6dda9..7a7e97215b1eb 100644 --- a/client/web-sveltekit/src/app.html +++ b/client/web-sveltekit/src/app.html @@ -1,4 +1,4 @@ - + diff --git a/client/web-sveltekit/src/lib/branded.ts b/client/web-sveltekit/src/lib/branded.ts index ce480e69ad09a..79fc934ebd48a 100644 --- a/client/web-sveltekit/src/lib/branded.ts +++ b/client/web-sveltekit/src/lib/branded.ts @@ -1,11 +1,17 @@ export { formatRepositoryStarCount } from '@sourcegraph/branded/src/search-ui/util/stars' export { limitHit, sortBySeverity } from '@sourcegraph/branded/src/search-ui/results/progress/utils' -export { - basicSyntaxColumns, - exampleQueryColumns, -} from '@sourcegraph/branded/src/search-ui/components/QueryExamples.constants' export { createDefaultSuggestions } from '@sourcegraph/branded/src/search-ui/input/codemirror' -export { multiline } from '@sourcegraph/branded/src/search-ui/input/codemirror/multiline' export { parseInputAsQuery } from '@sourcegraph/branded/src/search-ui/input/codemirror/parsedQuery' export { querySyntaxHighlighting } from '@sourcegraph/branded/src/search-ui/input/codemirror/syntax-highlighting' export { decorateQuery } from '@sourcegraph/branded/src/search-ui/util/query' +export * from '@sourcegraph/branded/src/search-ui/input/codemirror/multiline' +export * from '@sourcegraph/branded/src/search-ui/input/codemirror/event-handlers' +export { tokenInfo } from '@sourcegraph/branded/src/search-ui/input/codemirror/token-info' +export * from '@sourcegraph/branded/src/search-ui/input/codemirror/diagnostics' +export * from '@sourcegraph/branded/src/search-ui/input/experimental/modes' +export * from '@sourcegraph/branded/src/search-ui/input/experimental/utils' +export * from '@sourcegraph/branded/src/search-ui/input/experimental/suggestionsExtension' +export { placeholder } from '@sourcegraph/branded/src/search-ui/input/codemirror/placeholder' +export { showWhenEmptyWithoutContext } from '@sourcegraph/branded/src/search-ui/input/experimental/placeholder' +export { filterDecoration } from '@sourcegraph/branded/src/search-ui/input/experimental/codemirror/syntax-highlighting' +export { overrideContextOnPaste } from '@sourcegraph/branded/src/search-ui/input/experimental/codemirror/searchcontext' diff --git a/client/web-sveltekit/src/lib/codemirror/theme.ts b/client/web-sveltekit/src/lib/codemirror/theme.ts new file mode 100644 index 0000000000000..03b49904c4a46 --- /dev/null +++ b/client/web-sveltekit/src/lib/codemirror/theme.ts @@ -0,0 +1,8 @@ +import { EditorView } from '@codemirror/view' + +export const defaultTheme = EditorView.baseTheme({ + // Overwrites the default cursor color, which has too low contrast in dark mode + '.theme-dark & .cm-cursor': { + borderLeftColor: 'var(--grey-07)', + }, +}) diff --git a/client/web-sveltekit/src/lib/codemirror/utils.ts b/client/web-sveltekit/src/lib/codemirror/utils.ts new file mode 100644 index 0000000000000..17dda466acac4 --- /dev/null +++ b/client/web-sveltekit/src/lib/codemirror/utils.ts @@ -0,0 +1,59 @@ +import { Compartment, type StateEffect, type Extension } from '@codemirror/state' +import type { EditorView } from '@codemirror/view' + +type Extensions = Record +type UpdatedExtensions = { [key in keyof T]: Extension } + +interface Compartments { + extension: Extension + /** + * Initialize compartments with a different value + */ + init(extensions: UpdatedExtensions): Extension + /** + * Update compartments. Only compartments for which the provided + * value has changed will be updated. + */ + update(view: EditorView, extensions: UpdatedExtensions): void +} + +/** + * Helper function for creating a compartments extension. Each record + * entry will get its own compartment and the value will be the initial + * value of the compartment. + */ +export function createCompartments>(extensions: T): Compartments { + const compartments: Record = {} + + function init(extensions: UpdatedExtensions, compartments: Record): Extension { + const extension: Extension[] = [] + for (const [name, ext] of Object.entries(extensions)) { + let compartment = compartments[name] + if (!compartment) { + compartment = compartments[name] = new Compartment() + } + extension.push(compartment.of(ext)) + } + return extension + } + + return { + extension: init(extensions, compartments), + init(extensions) { + return init(extensions, compartments) + }, + update(view, extensions) { + const effects: StateEffect[] = [] + + for (const [name, ext] of Object.entries(extensions)) { + if (compartments[name].get(view.state) !== ext) { + effects.push(compartments[name].reconfigure(ext)) + } + } + + if (effects.length > 0) { + view.dispatch({ effects }) + } + }, + } +} diff --git a/client/web-sveltekit/src/lib/common.ts b/client/web-sveltekit/src/lib/common.ts index 89dd687fd10eb..bd988ffb1e68c 100644 --- a/client/web-sveltekit/src/lib/common.ts +++ b/client/web-sveltekit/src/lib/common.ts @@ -14,3 +14,4 @@ export { pluralize, numberWithCommas } from '@sourcegraph/common/src/util/string export { renderMarkdown } from '@sourcegraph/common/src/util/markdown/markdown' export { highlightNodeMultiline, highlightNode } from '@sourcegraph/common/src/util/highlightNode' export { logger } from '@sourcegraph/common/src/util/logger' +export { isSafari } from '@sourcegraph/common/src/util/browserDetection' diff --git a/client/web-sveltekit/src/lib/dom.ts b/client/web-sveltekit/src/lib/dom.ts index cad2d8c8c15e1..5c9fe8816e74f 100644 --- a/client/web-sveltekit/src/lib/dom.ts +++ b/client/web-sveltekit/src/lib/dom.ts @@ -99,3 +99,31 @@ export const highlightRanges: Action = (node, parameters) => { + let offset = parameters.offset ?? 0 + + function setMaxHeight(): void { + node.style.maxHeight = window.innerHeight - node.getBoundingClientRect().top + offset + 'px' + } + + window.addEventListener('resize', setMaxHeight) + + setMaxHeight() + + return { + update(parameter) { + offset = parameter.offset ?? 0 + setMaxHeight() + }, + + destroy() { + window.removeEventListener('resize', setMaxHeight) + }, + } +} diff --git a/client/web-sveltekit/src/lib/repo/api/tree.ts b/client/web-sveltekit/src/lib/repo/api/tree.ts index ed5373819d54c..ade3c9d59f53e 100644 --- a/client/web-sveltekit/src/lib/repo/api/tree.ts +++ b/client/web-sveltekit/src/lib/repo/api/tree.ts @@ -40,8 +40,7 @@ const treeEntriesQuery = gql` submodule { commit } - isSingleChild - entries(first: $first, recursiveSingleChild: false) { + entries(first: $first) { canonicalURL name path @@ -49,7 +48,6 @@ const treeEntriesQuery = gql` submodule { commit } - isSingleChild } } } diff --git a/client/web-sveltekit/src/lib/search-ui.ts b/client/web-sveltekit/src/lib/search-ui.ts new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/client/web-sveltekit/src/lib/search/BaseQueryInput.svelte b/client/web-sveltekit/src/lib/search/BaseQueryInput.svelte new file mode 100644 index 0000000000000..4aa91ac6dd6aa --- /dev/null +++ b/client/web-sveltekit/src/lib/search/BaseQueryInput.svelte @@ -0,0 +1,175 @@ + + + + +{#if browser} +

+{:else} +
+ +
+{/if} + + diff --git a/client/web-sveltekit/src/lib/search/CodeMirrorQueryInput.svelte b/client/web-sveltekit/src/lib/search/CodeMirrorQueryInput.svelte deleted file mode 100644 index 47ffe2cfc479b..0000000000000 --- a/client/web-sveltekit/src/lib/search/CodeMirrorQueryInput.svelte +++ /dev/null @@ -1,219 +0,0 @@ - - -{#if browser} -
-{:else} -
- -
-{/if} - - diff --git a/client/web-sveltekit/src/lib/search/EmphasizedLabel.svelte b/client/web-sveltekit/src/lib/search/EmphasizedLabel.svelte new file mode 100644 index 0000000000000..ded6e254bba48 --- /dev/null +++ b/client/web-sveltekit/src/lib/search/EmphasizedLabel.svelte @@ -0,0 +1,27 @@ + + +{#if spans} + {#each spans as [start, end, match]} + {#if match} + {label.slice(start, end + 1)} + {:else} + {label.slice(start, end + 1)} + {/if} + {/each} +{:else} + {label} +{/if} + + diff --git a/client/web-sveltekit/src/lib/search/SearchBox.svelte b/client/web-sveltekit/src/lib/search/SearchBox.svelte deleted file mode 100644 index 5efe8bc8095a0..0000000000000 --- a/client/web-sveltekit/src/lib/search/SearchBox.svelte +++ /dev/null @@ -1,238 +0,0 @@ - - -