diff --git a/.aspect/bazelrc/ci.sourcegraph.bazelrc b/.aspect/bazelrc/ci.sourcegraph.bazelrc index 572c553af492..98307106d9ff 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 fff4c7c5eed2..26dbbe1f8d34 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 000000000000..2b7d0e2b6704 --- /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 000000000000..54ccfae340f9 --- /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 9bd5e4cdc48a..75b90793d20f 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 7cc5bb26eec4..000000000000 --- 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 6a0a4b0c5fe1..dac97a9a4ae2 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 193a27a72385..a1c16c86f18e 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 e83c8ee658d4..defce42ce6ca 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 b443008226d9..6fafc2367852 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 464b19864e8f..78a653c57027 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 64c0672b4fce..6ad74f09cd10 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 cba1e1765a01..0a6b5e2ac50d 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 02599e3a4c30..329eb790d535 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 c1b1cd0e19e4..70ac840864a3 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 016eb1a2e8fb..3fdbf2f3bf6e 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 86395a9b50e1..9dd61c27544a 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 17712c0947c3..4605a4cfe3ea 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 92672052ddff..bb5f6da18d25 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 e229e1439a9d..4ce09b83e8ac 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 5f0462676b38..ded4f5494c28 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 3a472444d32f..ef0cb9cb6107 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 000000000000..88f3aa1ddb0a --- /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 4ebcebd22bf8..22551a352f7d 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 265fc6acae1c..d28be2b88704 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 9dffabf44282..975ae6320a09 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 000000000000..f0eb473ee7fe --- /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 48e5cea2bab9..82f296068075 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 a7a12944b834..148ce8a938e5 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 12df1a126ffb..370c3b3ac819 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 5242ea42589d..c0eb3e589af3 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 d3a3c2f7736d..96e0bbaba67a 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 22e90d8f8e79..697ca0580cbb 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 000000000000..0373ef0adffa --- /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 635297bfd794..e82df65f0ed2 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 e16b5fdd7c1a..0a2927f06bd0 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 f41c3fa1eef2..3676c9e7e3ce 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 167a8daf28e0..c7ea16f4212f 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 120f76190ccf..12494db2777e 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 7758a7514565..eaf467e7ea46 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 6d1e9ec82172..fe722decf79a 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 ef8c3d7a7793..4de966aac144 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 51244bbe0447..9363670947d0 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 dc6396126190..3861fedbf484 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 5934b0a1e391..a0e380828c38 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 ef92ee84efb4..b9e6c2ac8701 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 803a38577207..620470a6e4a7 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 9f4e5205323a..7b84d533241f 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 8a98687f4f49..37b791e39ee0 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 43b9207d5507..916c57253e5b 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 07c5ff531f72..2bbc1403904d 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 5ffd2c13280b..20d5908a58ed 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 a88452a9a30a..60d7124b1a47 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 f6044f985f06..b410662dcac7 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 bc2d042d65ac..20c251ff6ba2 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 217626fd7048..a19a0e3a8734 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 a57994d0e9d6..c9ac6ee943cc 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 a4a559972708..c06082413751 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 1017f5e6fff7..00b47b536ad2 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 5c4da20b8c68..7f4ec0362d0a 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 bfe48ac135c2..2af60143a573 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 d95ad0622fe3..f02673f05ae3 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 8c3e05d4a130..a7d81adcd200 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 000000000000..45686d09cb7f --- /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 2a6234f908f5..b02cf418394a 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 02b1c75e6dda..7a7e97215b1e 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 ce480e69ad09..79fc934ebd48 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 000000000000..03b49904c4a4 --- /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 000000000000..17dda466acac --- /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 89dd687fd10e..bd988ffb1e68 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 cad2d8c8c15e..5c9fe8816e74 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 ed5373819d54..ade3c9d59f53 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 000000000000..e69de29bb2d1 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 000000000000..4aa91ac6dd6a --- /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 47ffe2cfc479..000000000000 --- 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 000000000000..ded6e254bba4 --- /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 5efe8bc8095a..000000000000 --- a/client/web-sveltekit/src/lib/search/SearchBox.svelte +++ /dev/null @@ -1,238 +0,0 @@ - - -