Compare commits

..

24 Commits

Author SHA1 Message Date
Warren Gifford
dc97541a28
cherry-pick 856e41e with resolved conflicts (#64228)
<!-- PR description tips:
https://www.notion.so/sourcegraph/Write-a-good-pull-request-description-610a7fd3e613496eb76f450db5a49b6e
-->
Cherry pick changes from
https://github.com/sourcegraph/sourcegraph/pull/64227 into release
branch

## Test plan
tested via sg start with both:
- export APPLIANCE_UPDATE_TARGET=http://www.google.com
- export APPLIANCE_MENU_TARGET=http://www.warhammer.com

<!-- REQUIRED; info at
https://docs-legacy.sourcegraph.com/dev/background-information/testing_principles
-->

## Changelog

<!-- OPTIONAL; info at
https://www.notion.so/sourcegraph/Writing-a-changelog-entry-dd997f411d524caabf0d8d38a24a878c
-->
2024-08-01 23:32:25 +00:00
Release Bot
e1e2029d29
[Backport 5.5.x] fix(appliance): reliably redirect to site-admin post-install (#64220)
Backport e54407d9f5 from #64216

Co-authored-by: Craig Furman <craig.furman@sourcegraph.com>
2024-08-01 17:06:28 +00:00
Release Bot
17871a4647
[Backport 5.5.x] fix(appliance): cache authorization status (#64219)
Backport 156aa5a0ad from #64213

Co-authored-by: Craig Furman <craig.furman@sourcegraph.com>
2024-08-01 16:57:47 +00:00
Craig Furman
d24e8fe7f3
feat(appliance): backport all recent appliance changes (#64182)
Draft in case plan in
https://linear.app/sourcegraph/issue/REL-309/release-process-for-appliance
not agreed. Please see that first.

Generated by:

```
git log --format=%H d47b4cc48b6ea27cf6b5a274b79a6a4c8f38cf8c..origin/main -- cmd/appliance internal/appliance docker-images/appliance-frontend | tac | xargs git cherry-pick
```

d47b4cc48b being the commit we branched
off main from to create the 5.5.x branch
(https://buildkite.com/sourcegraph/sourcegraph/builds/281882).

Commits (generated by `git log --format='-
https://github.com/sourcegraph/sourcegraph/commit/%H'
d47b4cc48b6ea27cf6b5a274b79a6a4c8f38cf8c..origin/main -- cmd/appliance
internal/appliance docker-images/appliance-frontend | tac`):

-
a20b0650b4
-
b71c986c77
-
91864283bc
-
c88b57020f
-
0491839942
-
619fc57074
-
e81c39a834
-
a61f353e0e
-
0abef7b43d
-
0e391a964a
-
daae9adfb6
-
6e31f0f4cc
-
49a600220d
-
37cf4a7b7e
-
29fc613c37
-
255e6387cc
-
49b32fcf3a
-
9f4c160f91
-
3814fd7390
-
c68e92bc28
-
7e82c27ab5
-
98c6b9703f
-
a01ebad841
-
8c2d8da234
-
ebec72d7ed
-
d945f19285
-
84e28998e9


## Test plan

Tests pass.

## Changelog

- Backport all recent appliance changes. The appliance is still
pre-release.

---------

Co-authored-by: Jacob Pleiness <jdpleiness@users.noreply.github.com>
Co-authored-by: Anish Lakhwara <anish+github@lakhwara.com>
Co-authored-by: Warren Gifford <warren@sourcegraph.com>
Co-authored-by: Nelson Araujo <nelsonjr@users.noreply.github.com>
2024-07-31 17:26:56 +00:00
Ara
162d3836da
Backport 5ce2eea to 5.5.x (#64166)
This is a backport PR to add changes from
https://github.com/sourcegraph/sourcegraph/pull/64116 to v5.5.x to main
to create a release of the frontend.


## Test plan

<!-- REQUIRED; info at
https://docs-legacy.sourcegraph.com/dev/background-information/testing_principles
-->

## Changelog

<!-- OPTIONAL; info at
https://www.notion.so/sourcegraph/Writing-a-changelog-entry-dd997f411d524caabf0d8d38a24a878c
-->

---------

Co-authored-by: Vincent <evict@users.noreply.github.com>
2024-07-31 09:19:19 -07:00
Release Bot
8cf3916c44
[Backport 5.5.x] fix: Fix Chrome stack overflow during highlighting (#64074)
Using the spread operator with large arrays can trigger a
stack overflow in Chrome/V8.

In a highlighting context, we can have 10k-100k occurrences
in a file, so let's avoid using the spread operator.

Fixes https://linear.app/sourcegraph/issue/GRAPH-772

## Test plan

Manually tested against sample file.

![CleanShot 2024-07-25 at 11 10 43@2x](https://github.com/user-attachments/assets/e096c664-063e-44ed-a991-72629af36651)

## Changelog

- Fixes a Chrome-specific stack overflow when highlighting large files.
 <br> Backport 2644e24244 from #64072

Co-authored-by: Varun Gandhi <varun.gandhi@sourcegraph.com>
2024-07-25 20:44:22 +08:00
Release Bot
72ab1f818b
[Backport 5.5.x] ci: make internal+promote release higher priority in runtypes (#64050)
With the https://github.com/sourcegraph/sourcegraph/pull/63985/files
PatchRelease is matched before InternalRelease leading to the wrong
build being generated.

We therefore move the Promote and Internal Release runtypes higher in
priority so that they get matched first.

## Test plan
```
export RELEASE_INTERNAL=true
export VERSION=&quot;5.5.2463&quot;
go run ./dev/sg ci preview
```
👇🏼 
```
go run ./dev/sg ci preview
⚠️ Running sg with a dev build, following flags have different default value unless explictly set: skip-auto-update, disable-analytics
If the current branch were to be pushed, the following pipeline would be run:
  Parsed diff:
  changed files: [WORKSPACE client/web-sveltekit/BUILD.bazel client/web-sveltekit/playwright.config.ts client/web-sveltekit/src/lib/navigation/GlobalHeader.svelte client/web-
  sveltekit/src/routes/[...repo=reporev]/(validrev)/(code)/page.spec.ts client/web/src/cody/chat/new-chat/NewCodyChatPage.tsx client/web/src/cody/sidebar/new-cody-sidebar/NewCodySidebar.tsx
  client/web/src/cody/sidebar/new-cody-sidebar/NewCodySidebarWebChat.tsx client/web/src/enterprise/batches/settings/AddCredentialModal.tsx
  client/web/src/enterprise/batches/settings/BatchChangesCreateGitHubAppPage.tsx client/web/src/repo/blame/hooks.ts client/web/src/repo/blame/shared.ts cmd/frontend/auth/user.go
  cmd/frontend/auth/user_test.go cmd/frontend/internal/codycontext/context.go cmd/frontend/internal/codycontext/context_test.go deps.bzl dev/ci/push_all.sh dev/ci/runtype/runtype.go go.mod go.sum
  internal/codeintel/uploads/BUILD.bazel internal/codeintel/uploads/internal/background/backfiller/BUILD.bazel internal/codeintel/uploads/internal/background/backfiller/mocks_test.go
  internal/codeintel/uploads/internal/background/commitgraph/BUILD.bazel internal/codeintel/uploads/internal/background/commitgraph/job_commitgraph.go
  internal/codeintel/uploads/internal/background/expirer/BUILD.bazel internal/codeintel/uploads/internal/background/expirer/mocks_test.go
  internal/codeintel/uploads/internal/background/processor/BUILD.bazel internal/codeintel/uploads/internal/background/processor/mocks_test.go internal/codeintel/uploads/internal/store/BUILD.bazel
  internal/codeintel/uploads/internal/store/commitdate.go internal/codeintel/uploads/internal/store/commitdate_test.go internal/codeintel/uploads/internal/store/observability.go
  internal/codeintel/uploads/internal/store/store.go internal/codeintel/uploads/mocks_test.go internal/database/migration/shared/data/cmd/generator/consts.go
  internal/database/migration/shared/data/stitched-migration-graph.json package.json pnpm-lock.yaml schema/schema.go schema/site.schema.json]
  diff changes: &quot;Go, Client, pnpm, Docs, Shell&quot;
  The generated build pipeline will now follow, see you next time!

  • Detected run type: Internal release
  • Detected diffs: Go, Client, pnpm, Docs, Shell
  • Computed variables:
    • VERSION=5.5.2463
  • Computed build steps:
    • Aspect Workflow specific steps
      • 🤖 Generated steps that include Buildifier, Gazelle, Test and Integration/E2E tests
    • Image builds
      • :bazel::packer: 🚧 Build executor image
    • :bazel: Bazel prechecks &amp; build  sg
    • :bazel: BackCompat Tests
    • :bazel:🧹 Go mod tidy
    • Linters and static analysis
      • 🍍:lint-roller: Run sg lint → depends on bazel-prechecks
    • Client checks
      • :java: Build (client/jetbrains)
      • :vscode: Tests for VS Code extension
      • :stylelint: Stylelint (all)
    • Security Scanning
      • Semgrep SAST Scan
    • Publish candidate images
      • :bazel::docker: Push candidate Images
    • End-to-end tests
      • :bazel::docker::packer: Executors E2E → depends on bazel-push-images-candidate
    • Publish images
      • :bazel::packer:  Publish executor image → depends on executor-vm-image:candidate
      • :bazel:⤴️ Publish executor binary
      • :bazel::docker: Push final images → depends on main::test main::test_2
    • Release
      • Release tests → depends on bazel-push-images
      • Finalize internal release

```


## Changelog


 <br> Backport 0309564f93 from #64049

Co-authored-by: William Bezuidenhout <william.bezuidenhout@sourcegraph.com>
2024-07-24 19:22:30 +00:00
Will Dollman
fa826c30dc
Bump openjdk-11 version in blobstore image (#64047)
<!-- PR description tips:
https://www.notion.so/sourcegraph/Write-a-good-pull-request-description-610a7fd3e613496eb76f450db5a49b6e
-->
Update the version of openjdk-11 we use in the blobstore image.

We updated the blobstore image earlier today to try and fix this issue,
but were thwarted by an old version pin. This has now been
[removed](https://github.com/sourcegraph/sourcegraph/pull/64045), so
updating packages fully resolves the issue.

This PR branch is not based off `main` as backporting package changes
often results in merge conflicts. It was generated by checking our
`5.5.x` and running `sg wolfi lock blobstore`.

## Test plan

- CI
- Run image locally

<!-- REQUIRED; info at
https://docs-legacy.sourcegraph.com/dev/background-information/testing_principles
-->

## Changelog

<!-- OPTIONAL; info at
https://www.notion.so/sourcegraph/Writing-a-changelog-entry-dd997f411d524caabf0d8d38a24a878c
-->
2024-07-24 16:20:47 +00:00
Release Bot
a88bc4d2d8
[Backport 5.5.x] fix(source): Fix issue where worker would crash if a Bitbucket Cloud token couldn''t be refreshed (#64037)
A Bitbucket Cloud incident caused APIs to error which caused Bitbucket
Cloud OAuth tokens to fail to refresh. This revealed that the Bitbucket
Cloud client called `oauthutil.DoRequest` with a `nil` logger, causing a
nil pointer dereference.

This PR simply creates the logger before calling `DoRequest`, which is
what the other clients do.

## Test plan

No more cases of DoRequest with a nil logger.



## Changelog

- Fixed an issue where a Bitbucket Cloud OAuth token failing to refresh
would crash the `worker` service.


 <br> Backport bc036ad2ba from #64028

Co-authored-by: Petri-Johan Last <petri.last@sourcegraph.com>
2024-07-24 16:22:12 +02:00
sourcegraph-buildkite
d498442148
security: Auto-update package lockfiles for Sourcegraph base images (#64035)
Automatically generated PR to update package lockfiles for Sourcegraph
base images.

Built from Buildkite run
[#283970](https://buildkite.com/sourcegraph/sourcegraph/builds/283970).
## Test Plan
- CI build verifies image functionality

Co-authored-by: Buildkite <buildkite@sourcegraph.com>
2024-07-24 11:46:40 +00:00
Release Bot
0f4cbff0ca
[Backport 5.5.x] Integrate security release approval into release pipeline (#64030)
As part of the [Vuln Scanning
Improvements](https://linear.app/sourcegraph/project/[p0]-vulnerability-scanning-improvements-75299c4312dd/issues)
project, I&#39;ve been working on tooling to automate the security
approval step of the release process.

This PR integrates these improvements into the release pipeline:

* Internal releases will run a vulnerability scan
* Promote-to-public releases will check for security approval

If a public release does not have security approval, it will block the
promotion process. The step happens at the start of the pipeline so
should be a fast-fail. You can also check for release approval before
running promotion by running `@secbot cve approve-release
&lt;version&gt;` in the #secbot-commands channel. In an ideal world we
(security) will have already gone through and approved ahead of release.

I&#39;ve tested this PR as much as I can without running an actual
release! We have a 5.5.x release tomorrow so it&#39;ll be a good test.
If it does cause problems that can&#39;t be easily solved, it can always
be temporarily disabled.

I&#39;ve tagged this PR to be backported to `5.5.x`.



## Pre-merge checklist

- [x] Revert commit that disables release promotion

## Test plan

Manual testing of the release process:
- [x] [Successful test
run](https://buildkite.com/sourcegraph/sourcegraph/builds/283774#0190dfd6-fa70-4cea-9711-f5b8493c7714)
that shows the security scan being triggered
- [x] [Promote to public test
run](https://buildkite.com/sourcegraph/sourcegraph/builds/283826) that
shows the security approval approving a release
- [x] [Promote to public test
run](https://buildkite.com/sourcegraph/sourcegraph/builds/283817#0190e0ec-0641-4451-b7c7-171e664a3127)
that shows the security approval rejecting a release with un-accepted
CVEs



## Changelog


 <br> Backport 9dd901f3c9 from #63990

Co-authored-by: Will Dollman <will.dollman@sourcegraph.com>
2024-07-24 10:42:33 +01:00
Release Bot
1a463ba167
[Backport 5.5.x] [logging] Only record events if a new user was created (#64005)
Follow-up on https://github.com/sourcegraph/sourcegraph/pull/63843

Based on comments from
[this](https://sourcegraph.slack.com/archives/C04RG0JD8L9/p1721668767261719?thread_ts=1721661216.365709&amp;cid=C04RG0JD8L9)
Slack thread, it seems like the events causing the spam are ones where a
new ext acct is saved without a user being created. So if we want to fix
the spam we need to only save an event if a user was created.



## Test plan

Test updated.



## Changelog


 <br> Backport 777c7a0899 from #64004

Co-authored-by: Petri-Johan Last <petri.last@sourcegraph.com>
2024-07-23 13:09:11 +01:00
Release Bot
074af1bda8
[Backport 5.5.x] Publish images for all commits on release branches (#63987)
In order to run nightly vulnerability scans of Sourcegraph releases, we
need to publish a new set of images whenever the release branch is
pushed to.

Previously, this was implemented in
https://github.com/sourcegraph/sourcegraph/pull/63379 but with RFC 795
the release branch format changed from 5.5.1234 to 5.5.x.

This PR updates the regex to catch this new format.

The end result of this is that whenever Buildkite runs on a branch
matching `\d.\d.x`, it will push images to the
`us.gcr.io/sourcegraph-dev/gitserver` registry with the tag
`$branch-insiders`.

I&#39;ve also tagged this PR for backport as we want it on the current
patch release branch 5.5.x :)



## Test plan

- Test buildkite run on branch `will-0.0.x` (with modified regex to
match that branch)
https://buildkite.com/sourcegraph/sourcegraph/builds/283608



## Changelog


 <br> Backport b7242d280f from #63985

Co-authored-by: Will Dollman <will.dollman@sourcegraph.com>
2024-07-22 16:22:32 +00:00
Release Bot
9522c46e6b
[Backport 5.5.x] [fix] Only trigger externalAcctSignup event when a new user is created (#63975)
Currently events are triggered whenever a user signs in with
`http-header` auth. This is because of the `GetAndSaveUser` function
always triggering an event.

However, before the new telemetry events, these events were only created
when a new user was created.

This PR brings the new telemetry code in line with the old telemetry
code to stop the massive amounts of spam caused by this event.

Closes SRC-461

## Test plan

Adjust expected events in unit test.



## Changelog

- Fixed an issue where the `http-header` auth would cause a massive
amount of event logs spam


 <br> Backport cd65951961 from #63843

Co-authored-by: Petri-Johan Last <petri.last@sourcegraph.com>
2024-07-22 13:06:38 +01:00
Camden Cheek
556b880329
Backport: Blame: fix some issues with the stream (#63865) (#63929)
Contributes to SRCH-738

Notably, this does not yet identify the root cause of SRCH-738, but it
does identify and fix some confounding bugs. It's possible that these
actually also _cause_ some of the issues in SRCH-738, but I wanted to at
least push these to dotcom, where we can reproduce some of the
weirdness. At the very least, it doesn't explain the auth errors being
reported.

(cherry picked from commit d91fab39e2)

Co-authored-by: Michael Bahr <michael.bahr@sourcegraph.com>
2024-07-22 10:43:52 +01:00
Michael Lin
3117b03be9
[Backport 5.5.x] Upgrade cody web experimental package to 0.2.7 (#63863) (#63901)
backport https://github.com/sourcegraph/sourcegraph/pull/63863

S2 Cody Web is broken at the moment. New client-config handlers fail
with 401 status because we don't send custom headers, this works for gql
queries since they all are POST requests and the browser automatically
sends an Origin header for them and this is enough for our auth
middleware to check cookies, but with client-config which is rest it's
not the case and we should send `X-Requested-Client: Sourcegraph` header
to make our auth middleware to pass this query correctly

Note that this problem doesn't exist in local builds since we proxy all
requests and add `X-Requested-Client: Sourcegraph` in dev server.

See Cody latest build PR for more details
https://github.com/sourcegraph/cody/pull/4898

## Test plan

CI

Co-authored-by: Vova Kulikov <vovakulikov@icloud.com>
2024-07-18 10:29:02 +01:00
Release Bot
9cf00da25a
[Backport 5.5.x] fix(batches): the baseURL for github instance is now updated when creating a GitHub app (#63833)
Closes SRCH-723

The baseURL for GitHub apps defaults to `https://github.com` when no
`externalServiceURL`, we somehow missed this during our testing.

![CleanShot 2024-07-12 at 11 57
00@2x](https://github.com/user-attachments/assets/99b68a11-de38-4a2d-8c4c-3219f0c9abf7)


## Test plan



Manual testing with the GHE instance.

## Changelog


 <br> Backport 1c40c9e5bc from #63803

Co-authored-by: Bolaji Olajide <25608335+BolajiOlajide@users.noreply.github.com>
Co-authored-by: Anish Lakhwara <anish+github@lakhwara.com>
2024-07-17 18:18:13 +00:00
Varun Gandhi
6b8d334563
Backport 63870 to 5.5.x (#63882)
See  https://github.com/sourcegraph/sourcegraph/pull/63870

cc @sourcegraph/release

## Test plan

Covered by existing tests

## Changelog

- Adds an experimental feature `commitGraphUpdates` to control how
upload visibility is calculated.
2024-07-17 13:45:36 -04:00
Release Bot
21247e44ac
[Backport 5.5.x] Upgrade cody web experimental package to 0.2.5 (#63856)
This PR upgrades the cody web experimental package to 0.2.5, in the new
version we fixed
- Telemetry problem with init extension-related events (we don&#39;t
send install extension events anymore)
- Most recent updates on LLM availability for enterprise instances 
 
## Test plan
- CI is green
- Manual check on basic Cody Web functionality (highly recommended) <br>
Backport e6bd85e4b7 from #63839

Co-authored-by: Vova Kulikov <vovakulikov@icloud.com>
2024-07-17 12:01:11 -04:00
Vova Kulikov
20adc60d67
[Backport-5.5.x]: Move Cody Web to beta (#63806) (#63808)
Closes

https://linear.app/sourcegraph/issue/CODY-2847/change-experimental-labels-to-beta

## Test plan
- Check that the cody web page and cody web side panel have beta badges

(cherry-picked from commit fbb0a1fec1)

## Test plan
- Check that the side-panel Cody and Cody Chat page have beta product
status badges
2024-07-15 12:07:10 -07:00
Release Bot
308624f144
[Backport 5.5.x] Context: return lines around symbol match (#63788)
This PR fixes an important bug in #62976, where we didn&#39;t properly
map the
symbol line match to the return type. Instead, we accidentally treated
symbol
matches like file matches and returned the start of the file.

## Test plan

Add new unit test for symbol match conversion. Extensive manual testing.
<br> Backport 004eb0fd83 from #63773

Co-authored-by: Julie Tibshirani <julietibs@apache.org>
2024-07-11 16:14:52 -04:00
Release Bot
174c08c8c2
[Backport 5.5.x] fix/alertmanager: downgrade prometheus/common to fix generated config (#63793)
The OTEL upgrade https://github.com/sourcegraph/sourcegraph/pull/63171
bumps the `prometheus/common` package too far via transitive deps,
causing us to generate configuration for alertmanager that altertmanager
doesn&#39;t accept, at least until the alertmanager project cuts a new
release with a newer version of `promethues/common`.

For now we forcibly downgrade with a replace. Everything still builds,
so we should be good to go.

## Test plan
`sg start` and `sg run prometheus`. On `main`, editing
`observability.alerts` will cause Alertmanager to refuse to accept the
generated configuration. With this patch, all is well it seems - config
changes go through as expected. This is a similar test plan for
https://github.com/sourcegraph/sourcegraph/pull/63329

## Changelog

- Fix Prometheus Alertmanager configuration failing to apply
`observability.alerts` from site config <br> Backport
ffa873f3ad from #63790

Co-authored-by: Robert Lin <robert@bobheadxi.dev>
2024-07-11 15:49:58 -04:00
Release Bot
8ee41490b9
[Backport 5.5.x] release/bug: generate a new stitched migration graph (#63769)
This will correct6 upgrade path for mvu plan creation

## Test plan

CI test



## Changelog


 <br> Backport cb19d6f0a9 from #63764

Co-authored-by: Warren Gifford <warren@sourcegraph.com>
2024-07-10 22:01:36 +00:00
Release Bot
344169fd47
[Backport 5.5.x] chore(release): bump stitch graph generation (#63768)
Missing bit for the minor release version bump

## Test plan

CI


 <br> Backport 087ad83995 from #63767

Co-authored-by: Jean-Hadrien Chabran <jean-hadrien.chabran@sourcegraph.com>
2024-07-10 21:00:59 +00:00
2813 changed files with 75567 additions and 99164 deletions

View File

@ -59,4 +59,9 @@ test --build_event_binary_file_path_conversion=false
test --build_event_binary_file_upload_mode=wait_for_upload_complete
test --build_event_publish_all_actions=true
build --experimental_execution_log_compact_file=execution_log.zstd
test --experimental_execution_log_compact_file=execution_log.zstd
# These likely perform faster locally than the overhead of pulling/pushing from/to the remote cache,
# as well as being able to reduce how much we push to the cache
common --modify_execution_info=CopyDirectory=+no-remote,CopyToDirectory=+no-remote,CopyFile=+no-remote

View File

@ -26,7 +26,6 @@ client/web/node_modules
client/web-sveltekit/node_modules
client/wildcard/node_modules
internal/appliance/frontend/maintenance/node_modules
internal/openapi/node_modules
cmd/symbols/internal/squirrel/test_repos/starlark

View File

@ -30,8 +30,6 @@ const config = {
'typedoc.js',
'client/web/dev/**/*',
'graphql-schema-linter.config.js',
// Generated code
'client/web/src/enterprise/site-admin/dotcom/productSubscriptions/enterpriseportalgen/**',
],
extends: ['@sourcegraph/eslint-config', 'plugin:storybook/recommended'],
env: {
@ -94,7 +92,6 @@ const config = {
'@typescript-eslint/no-unused-vars': 'off', // also duplicated by tsconfig noUnused{Locals,Parameters}
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'etc/no-deprecated': 'off',
'no-restricted-imports': [
@ -114,6 +111,10 @@ const config = {
importNames: ['Link'],
message: 'Use the <Link /> component from @sourcegraph/wildcard instead.',
},
{
name: 'chromatic/isChromatic',
message: 'Please use `isChromatic` from the `@sourcegraph/storybook` package.',
},
],
patterns: [
{

6
.github/CODEOWNERS vendored
View File

@ -1,6 +0,0 @@
# Sourcegraph uses CODENOTIFY to make individuals or groups aware of changes that are happening in code they care about,
# without explicitly requiring those engineers to "own" the code.
# This file is meant to protect critical code from accidental changes, and should be used sparsingly to prevent slowdowns
# from code reviews.
/internal/tenant/ @sourcegraph/multi-tenant

View File

@ -1,12 +1,11 @@
---
name: Bug report
about: Report problems and unexpected behavior (Code Search ONLY)
about: Report problems and unexpected behavior
title: ''
labels: ''
assignees: ''
---
<!-- Please submit all feedback or bug reports for Cody in the [VS Code](https://github.com/sourcegraph/cody) or [JetBrains](https://github.com/sourcegraph/jetbrains) repo. -->
- **Sourcegraph version:** <!-- the version of Sourcegraph or "Sourcegraph.com" -->
- **Platform information:** <!-- OS version, cloud provider, web browser version, Docker version, etc., depending on the issue -->

View File

@ -1,112 +0,0 @@
# Cloud controller has tight integration with GraphQL API
# This workflow ensures that the query/mutation the controller uses are compatible with any changes to the GraphQL schema
#
# Maintained by the Cloud Operation team
name: Cloud Controller GQL Compat Test
on:
pull_request:
paths:
- '**.graphql'
jobs:
run:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: actions/create-github-app-token@v1
id: app-token
with:
# The GitHub App is here:
# https://github.com/organizations/sourcegraph/settings/apps/cloud-srcgql-compat-test-invoker
app-id: ${{ secrets.CLOUD_SRCGQL_COMPAT_TEST_INVOKER_GITHUB_APP_ID }}
private-key: ${{ secrets.CLOUD_SRCGQL_COMPAT_TEST_INVOKER_GITHUB_APP_PRIVATE_KEY_PEM }}
owner: sourcegraph
- uses: lasith-kg/dispatch-workflow@91345a2a3b705e950978a584446ab59f7e815ae3 #v2.0.0
id: workflow-dispatch
with:
dispatch-method: workflow_dispatch
discover: true
repo: controller
owner: sourcegraph
ref: main
# using ID instead of workflow file name to avoid requring content:read permission to the repo
# retrieve by running 'gh workflow list' in the controller repo
workflow: 58413286 # srcgql-compat.yaml
token: ${{ steps.app-token.outputs.token }}
workflow-inputs: |
{
"ref": "${{ github.sha }}",
"upstream_pr_number": "${{ github.event.number }}"
}
- name: await workflow run (id:${{ steps.workflow-dispatch.outputs.run-id }})
uses: codex-/await-remote-run@d4a6dbf57245924ff4f23e0db929b8e3ef65486b #1.12.2
with:
token: ${{ steps.app-token.outputs.token }}
repo: controller
owner: sourcegraph
run_id: ${{ steps.workflow-dispatch.outputs.run-id }}
run_timeout_seconds: 300
poll_interval_ms: 5000
- uses: actions/github-script@v7
if: ${{ success() || failure() }}
env:
FAILED: ${{ job.status == 'failure' }}
RUN_URL: https://github.com/sourcegraph/controller/actions/runs/${{ steps.workflow-dispatch.outputs.run-id }}
with:
script: |
const isFailed = process.env.FAILED === 'true'
const commentMarker = '<!-- cloud-gql-compat-test-result-marker -->'
let message
if (isFailed) {
message = `
## :x: Cloud Controller GraphQL Compatability Test Result
[sourcegraph/controller](https://github.com/sourcegraph/controller) uses the GraphQL API to perform automation. The compatibility test has failed and this pull request may have introduced breaking changes to the GraphQL schema.
Next steps:
- Review the GitHub Actions [workflow logs](${process.env.RUN_URL}) for more details.
- Reach out to the Cloud Ops team to resolve the issue before merging this pull request.
${commentMarker}
`
} else {
message = `
## :white_check_mark: Cloud Controller GraphQL Compatability Test Result
[sourcegraph/controller](https://github.com/sourcegraph/controller) uses the GraphQL API to perform automation. The compatibility test has passed.
Learn more from [workflow logs](${process.env.RUN_URL}).
${commentMarker}
`
}
const { data: comments } = await github.rest.issues.listComments({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 100,
})
let existingComment = comments.find(comment => comment.body.includes(commentMarker))
if (existingComment) {
await github.rest.issues.updateComment({
comment_id: existingComment.id,
owner: context.repo.owner,
repo: context.repo.repo,
body: message
})
} else if (isFailed) {
// we only create comment if the test failed
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: message
})
}

View File

@ -1,21 +1,21 @@
# See https://docs.sourcegraph.com/dev/background-information/ci#pr-auditor
name: pr-auditor
on:
pull_request_target:
types: [ closed, edited, opened, synchronize, ready_for_review ]
workflow_dispatch:
jobs:
check-pr:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
with:
repository: 'sourcegraph/devx-service'
token: ${{ secrets.PR_AUDITOR_TOKEN }}
repository: 'sourcegraph/pr-auditor'
- uses: actions/setup-go@v4
with: { go-version: '1.22' }
- run: 'go run ./cmd/pr-auditor'
- run: './check-pr.sh'
env:
GITHUB_EVENT_PATH: ${{ env.GITHUB_EVENT_PATH }}
GITHUB_TOKEN: ${{ secrets.PR_AUDITOR_TOKEN }}

View File

@ -70,6 +70,3 @@ dev/linearhooks/internal/lineargql/schema.graphql
# This is an embedded external minified library and should not be modified
internal/appliance/web/static/script/htmx.min.js
internal/appliance/web/static/script/bootstrap.bundle.min.js
# Generated code
client/web/src/enterprise/site-admin/dotcom/productSubscriptions/enterpriseportalgen/**

View File

@ -1,4 +1,4 @@
load("@bazel_skylib//rules:common_settings.bzl", "bool_flag")
load("@bazel_skylib//rules:common_settings.bzl", "bool_flag", "bool_setting")
load("@io_bazel_rules_go//go:def.bzl", "go_library", "nogo")
load("@aspect_bazel_lib//lib:copy_to_bin.bzl", "copy_to_bin")
load("@aspect_rules_ts//ts:defs.bzl", "ts_config")
@ -271,6 +271,17 @@ go_proto_compiler(
],
)
# Settings for automatic building of frontend/single-server without client bundle included
bool_setting(
name = "integration_testing",
build_setting_default = False,
)
config_setting(
name = "integration_testing_enabled",
flag_values = {":integration_testing": "true"},
)
# nogo config
#
# For nogo to be able to run a linter, it needs to have `var Analyzer analysis.Analyzer` defined in the main package.
@ -296,7 +307,6 @@ nogo(
"//conditions:default": [
"//dev/linters/bodyclose",
"//dev/linters/depguard",
"//dev/linters/exhaustruct",
"//dev/linters/forbidigo",
"//dev/linters/gocheckcompilerdirectives",
"//dev/linters/gocritic",

View File

@ -37,7 +37,6 @@ All notable changes to Sourcegraph are documented in this file.
- The default and recommended chat model for Anthropic and Cody Gateway configurations is now `claude-3-sonnet-20240229`. [#62757](https://github.com/sourcegraph/sourcegraph/pull/62757)
- The default and recommended autocomplete model for Cody Gateway configurations is now `fireworks/starcoder`. [#62757](https://github.com/sourcegraph/sourcegraph/pull/62757)
- Code Insights: Language Stats Insights performance improved by another 70-90%. It's now able to handle repositories above 40 GB. [#62946](https://github.com/sourcegraph/sourcegraph/pull/62946)
- The keyword search toggle has been removed from the search results page. [Keyword search](https://sourcegraph.com/docs/code-search/queries#keyword-search-default) is now enabled by default for all searches in the Sourcegraph web app. [#63584](https://github.com/sourcegraph/sourcegraph/pull/63584)
### Fixed

View File

@ -28,30 +28,30 @@ bazel_skylib_workspace()
http_archive(
name = "aspect_bazel_lib",
sha256 = "c780120ab99a4ca9daac69911eb06434b297214743ee7e0a1f1298353ef686db",
strip_prefix = "bazel-lib-2.7.9",
url = "https://github.com/aspect-build/bazel-lib/releases/download/v2.7.9/bazel-lib-v2.7.9.tar.gz",
sha256 = "6d758a8f646ecee7a3e294fbe4386daafbe0e5966723009c290d493f227c390b",
strip_prefix = "bazel-lib-2.7.7",
url = "https://github.com/aspect-build/bazel-lib/releases/download/v2.7.7/bazel-lib-v2.7.7.tar.gz",
)
http_archive(
name = "aspect_rules_js",
sha256 = "f8536470864c91f91c83aea91de9a27607ca5e6d8a9fcdd56132cf422c6b7b56",
strip_prefix = "rules_js-2.0.0-rc9",
url = "https://github.com/aspect-build/rules_js/releases/download/v2.0.0-rc9/rules_js-v2.0.0-rc9.tar.gz",
sha256 = "3bad4ab669d4d38d0d137275b946a46ce6f8f17fecc6c7affba64966a9054246",
strip_prefix = "rules_js-2.0.0-rc5",
url = "https://github.com/aspect-build/rules_js/releases/download/v2.0.0-rc5/rules_js-v2.0.0-rc5.tar.gz",
)
http_archive(
name = "aspect_rules_ts",
sha256 = "1d745fd7a5ffdb5bb7c0b77b36b91409a5933c0cbe25af32b05d90e26b7d14a7",
strip_prefix = "rules_ts-3.0.0-rc2",
url = "https://github.com/aspect-build/rules_ts/releases/download/v3.0.0-rc2/rules_ts-v3.0.0-rc2.tar.gz",
sha256 = "3ea5cdb825d5dbffe286b3d9c5197a2648cf04b5e6bd8b913a45823cdf0ae960",
strip_prefix = "rules_ts-3.0.0-rc0",
url = "https://github.com/aspect-build/rules_ts/releases/download/v3.0.0-rc0/rules_ts-v3.0.0-rc0.tar.gz",
)
http_archive(
name = "aspect_rules_swc",
sha256 = "0c2e8912725a1d97a37bb751777c9846783758f5a0a8e996f1b9d21cad42e839",
strip_prefix = "rules_swc-2.0.0-rc1",
url = "https://github.com/aspect-build/rules_swc/releases/download/v2.0.0-rc1/rules_swc-v2.0.0-rc1.tar.gz",
sha256 = "c085647585c3d01bee3966eb9ba433a1efbb0ee79bb1b8c67882a81d82a9b37f",
strip_prefix = "rules_swc-2.0.0-rc0",
url = "https://github.com/aspect-build/rules_swc/releases/download/v2.0.0-rc0/rules_swc-v2.0.0-rc0.tar.gz",
)
http_archive(
@ -106,9 +106,9 @@ http_archive(
# Container rules
http_archive(
name = "rules_oci",
sha256 = "311e78803a4161688cc79679c0fb95c56445a893868320a3caf174ff6e2c383b",
strip_prefix = "rules_oci-2.0.0-beta2",
url = "https://github.com/bazel-contrib/rules_oci/releases/download/v2.0.0-beta2/rules_oci-v2.0.0-beta2.tar.gz",
sha256 = "647f4c6fd092dc7a86a7f79892d4b1b7f1de288bdb4829ca38f74fd430fcd2fe",
strip_prefix = "rules_oci-1.7.6",
url = "https://github.com/bazel-contrib/rules_oci/releases/download/v1.7.6/rules_oci-v1.7.6.tar.gz",
)
http_archive(
@ -407,9 +407,15 @@ load("@rules_oci//oci:dependencies.bzl", "rules_oci_dependencies")
rules_oci_dependencies()
load("@rules_oci//oci:repositories.bzl", "oci_register_toolchains")
load("@rules_oci//oci:repositories.bzl", "LATEST_CRANE_VERSION", "oci_register_toolchains")
oci_register_toolchains(name = "oci")
oci_register_toolchains(
name = "oci",
crane_version = LATEST_CRANE_VERSION,
# Uncommenting the zot toolchain will cause it to be used instead of crane for some tasks.
# Note that it does not support docker-format images.
# zot_version = LATEST_ZOT_VERSION,
)
# Optional, for oci_tarball rule
load("@rules_pkg//:deps.bzl", "rules_pkg_dependencies")
@ -495,7 +501,7 @@ load("//dev:schema_migrations.bzl", "schema_migrations")
schema_migrations(
name = "schemas_migrations",
updated_at = "2024-08-07 19:10",
updated_at = "2024-07-10 23:24",
)
# wolfi images setup ================================

View File

@ -36,6 +36,12 @@ export const Interactive: StoryFn = () => {
return <ToggleExample value={value} onToggle={onToggle} />
}
Interactive.parameters = {
chromatic: {
disable: true,
},
}
export const Variants: StoryFn = () => (
<>
<ToggleExample value={true} onToggle={onToggle} />

View File

@ -13,7 +13,11 @@ const decorator: Decorator = story => (
const config: Meta = {
title: 'branded/TabbedPanelContent',
decorators: [decorator],
parameters: {},
parameters: {
chromatic: {
viewports: [320, 576, 978, 1440],
},
},
}
export default config

View File

@ -7,7 +7,9 @@ import { type RepoMetadataItem, RepoMetadata } from './RepoMetadata'
const config: Meta = {
title: 'branded/search-ui/RepoMetadata',
parameters: {},
parameters: {
chromatic: { viewports: [480] },
},
}
export default config
@ -48,3 +50,8 @@ export const RepoMetadataStory: StoryFn = () => (
)
RepoMetadataStory.storyName = 'RepoMetadata'
RepoMetadataStory.parameters = {
chromatic: {
disableSnapshot: false,
},
}

View File

@ -8,7 +8,9 @@ import { SyntaxHighlightedSearchQuery } from './SyntaxHighlightedSearchQuery'
const config: Meta = {
title: 'branded/search-ui/SyntaxHighlightedSearchQuery',
parameters: {},
parameters: {
chromatic: { viewports: [480] },
},
}
export default config

View File

@ -10,7 +10,9 @@ import { BaseCodeMirrorQueryInput, type BaseCodeMirrorQueryInputProps } from './
const config: Meta = {
title: 'branded/search-ui/input/BaseCodeMirrorQueryInput',
parameters: {},
parameters: {
chromatic: { viewports: [500] },
},
}
export default config

View File

@ -16,7 +16,9 @@ import { SearchBox, type SearchBoxProps } from './SearchBox'
const config: Meta = {
title: 'branded/search-ui/input/SearchBox',
parameters: {},
parameters: {
chromatic: { viewports: [575, 700], disableSnapshot: false },
},
}
export default config

View File

@ -22,6 +22,7 @@ const decorator: Decorator = story => (
const config: Meta = {
title: 'branded/search-ui/input/SearchContextMenu',
parameters: {
chromatic: { viewports: [500], disableSnapshot: false },
design: {
type: 'figma',
url: 'https://www.figma.com/file/4Fy9rURbfF2bsl4BvYunUO/RFC-261-Search-Contexts?node-id=581%3A4754',

View File

@ -13,7 +13,9 @@ const decorator: Decorator = story => (
const config: Meta = {
title: 'branded/search-ui/input/SearchContextMenuItem',
parameters: {},
parameters: {
chromatic: { viewports: [1200], disableSnapshot: false },
},
decorators: [decorator],
}

View File

@ -11,7 +11,9 @@ const decorator: Decorator = story => <BrandedStory>{props => story()}</BrandedS
const config: Meta = {
title: 'branded/search-ui/filters',
decorators: [decorator],
parameters: {},
parameters: {
chromatic: { viewports: [575, 700] },
},
}
export default config

View File

@ -15,6 +15,7 @@ const config: Meta = {
type: 'figma',
url: 'https://www.figma.com/file/IyiXZIbPHK447NCXov0AvK/13928-Streaming-search?node-id=280%3A17768',
},
chromatic: { viewports: [1200], disableSnapshot: false },
},
}

View File

@ -14,6 +14,7 @@ const config: Meta = {
type: 'figma',
url: 'https://www.figma.com/file/IyiXZIbPHK447NCXov0AvK/13928-Streaming-search?node-id=280%3A17768',
},
chromatic: { viewports: [350], disableSnapshot: false },
},
}

View File

@ -356,7 +356,7 @@ export const ExhaustiveSearchMessage: FC<ExhaustiveSearchMessageProps> = props =
if (!validationLoading) {
telemetryService.log('SearchJobsSearchFormShown', { validState }, { validState })
telemetryRecorder.recordEvent('search.exhaustiveJobs', 'view', {
metadata: { validState: validState === 'valid' ? 1 : 0 },
metadata: { validState: validState ? 1 : 0 },
})
}
}, [telemetryService, telemetryRecorder, validationError, validationLoading])

View File

@ -8,7 +8,12 @@ import brandedStyles from '../../branded.scss'
const config: Meta = {
title: 'browser/AfterInstallPage',
parameters: {},
parameters: {
chromatic: {
enableDarkMode: true,
disableSnapshot: false,
},
},
}
export default config

View File

@ -162,3 +162,10 @@ AllOptionsPages.args = {
version: '0.0.0',
showSourcegraphComAlert: false,
}
AllOptionsPages.parameters = {
chromatic: {
enableDarkMode: true,
disableSnapshot: false,
},
}

View File

@ -52,8 +52,8 @@ describe('GitHub', () => {
})
testContext.overrideGraphQL({
ViewerSettings: () => ({
viewerSettings: {
ViewerConfiguration: () => ({
viewerConfiguration: {
subjects: [],
merged: { contents: '', messages: [] },
},
@ -158,8 +158,8 @@ describe('GitHub', () => {
// extensions: extensionSettings,
// }
// testContext.overrideGraphQL({
// ViewerSettings: () => ({
// viewerSettings: {
// ViewerConfiguration: () => ({
// viewerConfiguration: {
// subjects: [
// {
// __typename: 'User',
@ -321,8 +321,8 @@ describe('GitHub', () => {
extensions: extensionSettings,
}
testContext.overrideGraphQL({
ViewerSettings: () => ({
viewerSettings: {
ViewerConfiguration: () => ({
viewerConfiguration: {
subjects: [
{
__typename: 'User',
@ -643,8 +643,8 @@ describe('GitHub', () => {
extensions: extensionSettings,
}
testContext.overrideGraphQL({
ViewerSettings: () => ({
viewerSettings: {
ViewerConfiguration: () => ({
viewerConfiguration: {
subjects: [
{
__typename: 'User',

View File

@ -47,8 +47,8 @@ describe('GitLab', () => {
})
testContext.overrideGraphQL({
ViewerSettings: () => ({
viewerSettings: {
ViewerConfiguration: () => ({
viewerConfiguration: {
subjects: [],
merged: { contents: '', messages: [] },
},
@ -152,8 +152,8 @@ describe('GitLab', () => {
extensions: extensionSettings,
}
testContext.overrideGraphQL({
ViewerSettings: () => ({
viewerSettings: {
ViewerConfiguration: () => ({
viewerConfiguration: {
subjects: [
{
__typename: 'User',

View File

@ -28,7 +28,12 @@ const config: Meta = {
// it uses the browser extension styles and bitbucket CSS module styles.
title: 'shared/HoverOverlay',
decorators: [decorator],
parameters: {},
parameters: {
chromatic: {
enableDarkMode: true,
disableSnapshot: false,
},
},
}
export default config

View File

@ -14,7 +14,7 @@ import {
} from '@sourcegraph/shared/src/settings/settings'
import { observeStorageKey, storage } from '../../browser-extension/web-extension-api/storage'
import type { ViewerSettingsResult } from '../../graphql-operations'
import type { ViewerConfigurationResult } from '../../graphql-operations'
import { isInPage } from '../context'
const inPageClientSettingsKey = 'sourcegraphClientSettings'
@ -35,8 +35,7 @@ function observeLocalStorageKey(key: string, defaultValue: string): Observable<s
}
const createStorageSettingsCascade: () => Observable<SettingsCascade> = () => {
/**
* Observable of the JSONC string of the settings.
/** Observable of the JSONC string of the settings.
*
* NOTE: We can't use LocalStorageSubject here because the JSONC string is stored raw in localStorage and LocalStorageSubject also does parsing.
* This could be changed, but users already have settings stored, so it would need a migration for little benefit.
@ -96,8 +95,9 @@ export function mergeCascades(
}
}
const settingsCascadeFragment = gql`
fragment SettingsCascadeFields on SettingsCascade {
// This is a fragment on the DEPRECATED GraphQL API type ConfigurationCascade (not SettingsCascade) for backcompat.
const configurationCascadeFragment = gql`
fragment ConfigurationCascadeFields on ConfigurationCascade {
subjects {
__typename
...OrgSettingFields
@ -167,40 +167,42 @@ const settingsCascadeFragment = gql`
/**
* Fetches the settings cascade for the viewer.
*
* TODO(sqs): This uses the DEPRECATED GraphQL Query.viewerConfiguration and ConfigurationCascade for backcompat.
*/
export function fetchViewerSettings(requestGraphQL: PlatformContext['requestGraphQL']): Observable<{
final: string
subjects: SettingsSubject[]
}> {
return from(
requestGraphQL<ViewerSettingsResult>({
requestGraphQL<ViewerConfigurationResult>({
request: gql`
query ViewerSettings {
viewerSettings {
...SettingsCascadeFields
query ViewerConfiguration {
viewerConfiguration {
...ConfigurationCascadeFields
}
}
${settingsCascadeFragment}
${configurationCascadeFragment}
`,
variables: {},
mightContainPrivateInfo: false,
})
).pipe(
map(dataOrThrowErrors),
map(({ viewerSettings }) => {
if (!viewerSettings) {
throw new Error('fetchViewerSettings: empty viewerSettings')
map(({ viewerConfiguration }) => {
if (!viewerConfiguration) {
throw new Error('fetchViewerSettings: empty viewerConfiguration')
}
for (const subject of viewerSettings.subjects) {
for (const subject of viewerConfiguration.subjects) {
// User/org/global settings cannot be edited from the
// browser extension (only client settings can).
subject.viewerCanAdminister = false
}
return {
subjects: viewerSettings.subjects,
final: viewerSettings.merged.contents,
subjects: viewerConfiguration.subjects,
final: viewerConfiguration.merged.contents,
}
})
)

View File

@ -150,9 +150,7 @@ function highlightNodeHelper(
}
let newNode: Node
if (newNodes.length === 0) {
newNode = document.createTextNode('')
} else if (newNodes.length === 1) {
if (newNodes.length === 1) {
// If we only have one new node, no need to wrap it in a containing span
newNode = newNodes[0]
} else {

View File

@ -41,42 +41,6 @@ export const highlightCodeSafe = (code: string, language?: string): string => {
}
}
export interface RenderMarkdownOptions {
/**
* Whether to render markdown inline, without paragraph tags
*/
inline?: boolean
/**
* Whether to render line breaks as HTML `<br>`s
*/
breaks?: boolean
/**
* Whether to disable autolinks. Explicit links using `[text](url)` are still allowed.
*/
disableAutolinks?: boolean
/**
* A custom renderer to use
*/
renderer?: marked.Renderer
/**
* A prefix to add to all header IDs
*/
headerPrefix?: string
/**
* Strip off any HTML and return a plain text string, useful for previews
*/
plainText?: boolean
/**
* DOMPurify configuration to use
*/
dompurifyConfig?: DOMPurifyConfig & { RETURN_DOM_FRAGMENT?: false; RETURN_DOM?: false }
/**
* Add target="_blank" and rel="noopener" to all <a> links that have a href value.
* This affects all markdown-formatted links and all inline HTML links.
*/
addTargetBlankToAllLinks?: boolean
}
/**
* Renders the given markdown to HTML, highlighting code and sanitizing dangerous HTML.
* Can throw an exception on parse errors.
@ -91,7 +55,18 @@ export interface RenderMarkdownOptions {
* @param options.addTargetBlankToAllLinks Add target="_blank" and rel="noopener" to all <a> links
* that have a href value. This affects all markdown-formatted links and all inline HTML links.
*/
export const renderMarkdown = (markdown: string, options: RenderMarkdownOptions = {}): string => {
export const renderMarkdown = (
markdown: string,
options: {
breaks?: boolean
disableAutolinks?: boolean
renderer?: marked.Renderer
headerPrefix?: string
plainText?: boolean
dompurifyConfig?: DOMPurifyConfig & { RETURN_DOM_FRAGMENT?: false; RETURN_DOM?: false }
addTargetBlankToAllLinks?: boolean
} = {}
): string => {
const tokenizer = new marked.Tokenizer()
if (options.disableAutolinks) {
// Why the odd double-casting below?
@ -101,7 +76,7 @@ export const renderMarkdown = (markdown: string, options: RenderMarkdownOptions
tokenizer.url = () => undefined as unknown as marked.Tokens.Link
}
const rendered = (options.inline ? marked.parseInline : marked)(markdown, {
const rendered = marked(markdown, {
gfm: true,
breaks: options.breaks,
highlight: (code, language) => highlightCodeSafe(code, language),

View File

@ -86,3 +86,9 @@ export const JetBrainsSearchBoxStory: StoryFn = () => {
</WildcardThemeContext.Provider>
)
}
JetBrainsSearchBoxStory.parameters = {
chromatic: {
disableSnapshot: false,
},
}

View File

@ -123,3 +123,9 @@ export const JetBrainsSearchResultListStory: StoryFn = () => {
</div>
)
}
JetBrainsSearchResultListStory.parameters = {
chromatic: {
disableSnapshot: false,
},
}

View File

@ -30,4 +30,4 @@
"@sourcegraph/wildcard": "workspace:*"
},
"sideEffects": true
}
}

View File

@ -63,6 +63,12 @@ export const CommandAction: StoryFn = () => (
)
CommandAction.storyName = 'Command action'
CommandAction.parameters = {
chromatic: {
enableDarkMode: true,
disableSnapshot: false,
},
}
export const LinkAction: StoryFn = () => (
<ActionItem

View File

@ -31,6 +31,7 @@ export const currentAuthStateQuery = gql`
__typename
id
name
displayName
url
settingsURL
}

View File

@ -23,7 +23,7 @@ import { parseRepoGitURI } from '../util/url'
import type { DocumentSelector, TextDocument, DocumentHighlight } from './legacy-extensions/api'
import * as sourcegraph from './legacy-extensions/api'
import type { LanguageSpec } from './legacy-extensions/language-specs/language-spec'
import { findLanguageSpec, languageSpecs } from './legacy-extensions/language-specs/languages'
import { languageSpecs } from './legacy-extensions/language-specs/languages'
import { RedactingLogger } from './legacy-extensions/logging'
import { createProviders, emptySourcegraphProviders, type SourcegraphProviders } from './legacy-extensions/providers'
import { SymbolRole, Occurrence } from './scip'
@ -172,7 +172,12 @@ const languages: Language[] = languageSpecs.map(spec => ({
// Returns true if the provided language supports "Find implementations"
export function hasFindImplementationsSupport(language: string): boolean {
return findLanguageSpec(language)?.textDocumentImplemenationSupport ?? false
for (const spec of languageSpecs) {
if (spec.languageID === language) {
return spec.textDocumentImplemenationSupport ?? false
}
}
return false
}
function selectorForSpec(languageSpec: LanguageSpec): DocumentSelector {

View File

@ -431,6 +431,5 @@ export const languageSpecs: LanguageSpec[] = [
* @deprecated See FIXME(id: language-detection)
*/
export function findLanguageSpec(languageID: string): LanguageSpec | undefined {
languageID = languageID.toLowerCase()
return languageSpecs.find(spec => spec.languageID === languageID || spec.additionalLanguages?.includes(languageID))
}

View File

@ -2,7 +2,6 @@ import gql from 'tagged-template-noop'
import { isErrorLike } from '@sourcegraph/common'
import { SearchVersion } from '../../../graphql-operations'
import type * as sourcegraph from '../api'
import { cache } from '../util'
@ -252,7 +251,6 @@ export class API {
const data = await queryGraphQL<Response>(buildSearchQuery(fileLocal), {
query,
version: SearchVersion.V3,
})
return data.search.results.results.filter(isDefined)
}
@ -324,8 +322,8 @@ function buildSearchQuery(fileLocal: boolean): string {
if (fileLocal) {
return gql`
query LegacyCodeIntelSearch2($query: String!, $version: SearchVersion!) {
search(query: $query, version: $version) {
query LegacyCodeIntelSearch2($query: String!) {
search(query: $query) {
...SearchResults
...FileLocal
}
@ -336,8 +334,8 @@ function buildSearchQuery(fileLocal: boolean): string {
}
return gql`
query LegacyCodeIntelSearch3($query: String!, $version: SearchVersion!) {
search(query: $query, version: $version) {
query LegacyCodeIntelSearch3($query: String!) {
search(query: $query) {
...SearchResults
}
}

View File

@ -2,8 +2,6 @@
// but it doesn't make sense to do so right now.
import type * as extensions from '@sourcegraph/extension-api-types'
import { CodeGraphDataProvenance } from '../graphql-operations'
import type * as sourcegraph from './legacy-extensions/api'
export interface JsonDocument {
@ -146,8 +144,7 @@ export class Occurrence {
public readonly range: Range,
public readonly kind?: SyntaxKind,
public readonly symbol?: string,
public readonly symbolRoles?: number,
public readonly symbolProvenance?: CodeGraphDataProvenance
public readonly symbolRoles?: number
) {}
public withStartPosition(newStartPosition: Position): Occurrence {

View File

@ -1675,40 +1675,40 @@ describe('scanSearchQuery() and decorate()', () => {
test('highlight repo:has predicate', () => {
expect(getTokens(toSuccess(scanSearchQuery('repo:has(key:value)')))).toMatchInlineSnapshot(`
[
{
"startIndex": 0,
"scopes": "field"
},
{
"startIndex": 4,
"scopes": "metaFilterSeparator"
},
{
"startIndex": 5,
"scopes": "metaPredicateNameAccess"
},
{
"startIndex": 8,
"scopes": "metaPredicateParenthesis"
},
{
"startIndex": 9,
"scopes": "identifier"
},
{
"startIndex": 12,
"scopes": "metaFilterSeparator"
},
{
"startIndex": 13,
"scopes": "identifier"
},
{
"startIndex": 18,
"scopes": "metaPredicateParenthesis"
}
]
[
{
"startIndex": 0,
"scopes": "field"
},
{
"startIndex": 4,
"scopes": "metaFilterSeparator"
},
{
"startIndex": 5,
"scopes": "metaPredicateNameAccess"
},
{
"startIndex": 8,
"scopes": "metaPredicateParenthesis"
},
{
"startIndex": 9,
"scopes": "identifier"
},
{
"startIndex": 12,
"scopes": "metaFilterSeparator"
},
{
"startIndex": 13,
"scopes": "identifier"
},
{
"startIndex": 18,
"scopes": "metaPredicateParenthesis"
}
]
`)
})
@ -1891,103 +1891,6 @@ describe('scanSearchQuery() and decorate()', () => {
`)
})
test('decorate repo:has.meta with regexp', () => {
expect(
getTokens(
toSuccess(
scanSearchQuery(String.raw`repo:has.meta(/abc.*/:/[def]+/)`, false, SearchPatternType.keyword)
)
)
).toMatchInlineSnapshot(`
[
{
"startIndex": 0,
"scopes": "field"
},
{
"startIndex": 4,
"scopes": "metaFilterSeparator"
},
{
"startIndex": 5,
"scopes": "metaPredicateNameAccess"
},
{
"startIndex": 8,
"scopes": "metaPredicateDot"
},
{
"startIndex": 9,
"scopes": "metaPredicateNameAccess"
},
{
"startIndex": 13,
"scopes": "metaPredicateParenthesis"
},
{
"startIndex": 14,
"scopes": "metaRegexpDelimited"
},
{
"startIndex": 15,
"scopes": "identifier"
},
{
"startIndex": 18,
"scopes": "metaRegexpCharacterSet"
},
{
"startIndex": 19,
"scopes": "metaRegexpRangeQuantifier"
},
{
"startIndex": 20,
"scopes": "metaRegexpDelimited"
},
{
"startIndex": 21,
"scopes": "metaFilterSeparator"
},
{
"startIndex": 22,
"scopes": "metaRegexpDelimited"
},
{
"startIndex": 23,
"scopes": "metaRegexpCharacterClass"
},
{
"startIndex": 24,
"scopes": "metaRegexpCharacterClassMember"
},
{
"startIndex": 25,
"scopes": "metaRegexpCharacterClassMember"
},
{
"startIndex": 26,
"scopes": "metaRegexpCharacterClassMember"
},
{
"startIndex": 27,
"scopes": "metaRegexpCharacterClass"
},
{
"startIndex": 28,
"scopes": "metaRegexpRangeQuantifier"
},
{
"startIndex": 29,
"scopes": "metaRegexpDelimited"
},
{
"startIndex": 30,
"scopes": "metaPredicateParenthesis"
}
]
`)
})
test('do not decorate quotes inside quoted filter values', () => {
expect(getTokens(toSuccess(scanSearchQuery(String.raw`file:"foo\"bar"`, false, SearchPatternType.keyword))))
.toMatchInlineSnapshot(`

View File

@ -13,8 +13,8 @@ import type {
import { SearchPatternType } from '../../graphql-operations'
import { type PredicateInstance, scanPredicate } from './predicates'
import { quoted, scanSearchQuery, oneOf, ScanResult, toPatternResult } from './scanner'
import { type Predicate, scanPredicate } from './predicates'
import { scanSearchQuery } from './scanner'
import { type Token, type Pattern, type Literal, PatternKind, type CharacterRange, createLiteral } from './token'
/* eslint-disable unicorn/better-regex */
@ -200,7 +200,7 @@ export interface MetaPredicate {
range: CharacterRange
groupRange?: CharacterRange
kind: MetaPredicateKind
value: PredicateInstance
value: Predicate
}
enum MetaKeywordKind {
@ -1014,48 +1014,33 @@ const decorateRepoHasMetaBody = (body: string, offset: number): DecoratedToken[]
return undefined
}
const offsetToken = (offset: number) => (token: DecoratedToken) => {
token.range.start += offset
token.range.end += offset
return token
}
return [
...(decoratePattern(matches[1])?.map(offsetToken(offset)) ?? []),
{
type: 'literal',
value: matches[1],
range: { start: offset, end: offset + matches[1].length },
quoted: false,
},
{
type: 'metaFilterSeparator',
range: { start: offset + matches[1].length, end: offset + matches[1].length + 1 },
value: ':',
},
...(decoratePattern(matches[2])?.map(offsetToken(offset + matches[1].length + 1)) ?? []),
{
type: 'literal',
value: matches[1],
range: { start: offset + matches[1].length + 1, end: offset + matches[1].length + 1 + matches[2].length },
quoted: false,
},
]
}
const decoratePattern = (token: string): DecoratedToken[] | undefined => {
const plainString = (token: string, offset: number): ScanResult<Literal> => ({
type: 'success',
term: createLiteral(token, { start: offset, end: offset + token.length }),
})
const scanner = oneOf<Pattern>(
toPatternResult(quoted("'"), PatternKind.Literal),
toPatternResult(quoted('"'), PatternKind.Literal),
toPatternResult(quoted('/'), PatternKind.Regexp),
toPatternResult(plainString, PatternKind.Literal)
)
const scanResult = scanner(token, 0)
if (scanResult.type === 'error') {
return undefined
}
return decorate(scanResult.term)
}
/**
* Decorates the body part of predicate syntax `name(body)`.
*/
const decoratePredicateBody = (name: string, body: string, offset: number): DecoratedToken[] => {
const decoratePredicateBody = (path: string[], body: string, offset: number): DecoratedToken[] => {
const decorated: DecoratedToken[] = []
switch (name) {
switch (path.join('.')) {
case 'contains.file':
case 'has.file': {
const result = decorateContainsFileBody(body, offset)
@ -1123,18 +1108,18 @@ const decoratePredicateBody = (name: string, body: string, offset: number): Deco
return decorated
}
const decoratePredicate = (predicate: PredicateInstance, range: CharacterRange): DecoratedToken[] => {
const decoratePredicate = (predicate: Predicate, range: CharacterRange): DecoratedToken[] => {
let offset = range.start
const decorated: DecoratedToken[] = []
for (const namePart of predicate.name.split('.')) {
for (const nameAccess of predicate.path) {
decorated.push({
type: 'metaPredicate',
kind: MetaPredicateKind.NameAccess,
range: { start: offset, end: offset + namePart.length },
range: { start: offset, end: offset + nameAccess.length },
groupRange: range,
value: predicate,
})
offset = offset + namePart.length
offset = offset + nameAccess.length
decorated.push({
type: 'metaPredicate',
kind: MetaPredicateKind.Dot,
@ -1155,7 +1140,7 @@ const decoratePredicate = (predicate: PredicateInstance, range: CharacterRange):
value: predicate,
})
offset = offset + 1
decorated.push(...decoratePredicateBody(predicate.name, body, offset))
decorated.push(...decoratePredicateBody(predicate.path, body, offset))
offset = offset + body.length
decorated.push({
type: 'metaPredicate',

View File

@ -76,7 +76,7 @@ describe('getDiagnostics()', () => {
[
{
"severity": "error",
"message": "Invalid filter value, expected one of: commit, diff, file, path, repo, symbol.",
"message": "Invalid filter value, expected one of: diff, commit, symbol, repo, path, file.",
"range": {
"start": 10,
"end": 15

View File

@ -322,33 +322,32 @@ export const FILTERS: Record<NegatableFilter, NegatableFilterDefinition> &
},
[FilterType.type]: {
description: 'Limit results to diffs, commits, file paths, symbols and other entities.',
discreteValues: () =>
[
{
label: 'diff',
description: 'Search for file changes',
},
{
label: 'commit',
description: 'Search in commit messages',
},
{
label: 'symbol',
description: 'Search for symbol names',
},
{
label: 'repo',
description: 'Search for repositories',
},
{
label: 'path',
description: 'Search for file/directory names',
},
{
label: 'file',
description: 'Search for file content',
},
].sort((a, b) => a.label.localeCompare(b.label)),
discreteValues: () => [
{
label: 'diff',
description: 'Search for file changes',
},
{
label: 'commit',
description: 'Search in commit messages',
},
{
label: 'symbol',
description: 'Search for symbol names',
},
{
label: 'repo',
description: 'Search for repositories',
},
{
label: 'path',
description: 'Search for file/directory names',
},
{
label: 'file',
description: 'Search for file content',
},
],
},
[FilterType.visibility]: {
discreteValues: () => ['any', 'private', 'public'].map(value => ({ label: value })),

View File

@ -197,7 +197,7 @@ const toSelectorHover = (token: MetaSelector): string => {
const toPredicateHover = (token: MetaPredicate): string => {
const parameters = token.value.parameters.slice(1, -1)
switch (token.value.name) {
switch (token.value.path.join('.')) {
case 'contains.file':
case 'has.file': {
return '**Built-in predicate**. Search only inside repositories that satisfy the specified `path:` and `content:` filters. `path:` and `content:` filters should be regular expressions.'

View File

@ -130,7 +130,7 @@ export const collectMetrics = (query: string): Metrics | undefined => {
if (!predicate) {
continue
}
switch (predicate.name) {
switch (predicate.path.join('.')) {
case 'contains.path': {
count_repo_contains_path += 1
break

View File

@ -1,6 +1,6 @@
import { describe, expect, test } from 'vitest'
import { scanPredicate } from './predicates'
import { scanPredicate, resolveAccess, PREDICATES } from './predicates'
expect.addSnapshotSerializer({
serialize: value => (value ? JSON.stringify(value) : 'invalid'),
@ -10,19 +10,19 @@ expect.addSnapshotSerializer({
describe('scanPredicate', () => {
test('scan recognized and valid syntax', () => {
expect(scanPredicate('repo', 'contains.file(content:stuff)')).toMatchInlineSnapshot(
'{"field":"repo","name":"contains.file","parameters":"(content:stuff)"}'
'{"path":["contains","file"],"parameters":"(content:stuff)"}'
)
})
test('scan recognized dot syntax', () => {
expect(scanPredicate('repo', 'contains.commit.after(stuff)')).toMatchInlineSnapshot(
'{"field":"repo","name":"contains.commit.after","parameters":"(stuff)"}'
'{"path":["contains","commit","after"],"parameters":"(stuff)"}'
)
})
test('scan recognized and valid syntax with escapes', () => {
expect(scanPredicate('repo', 'contains.file(content:\\((stuff))')).toMatchInlineSnapshot(
'{"field":"repo","name":"contains.file","parameters":"(content:\\\\((stuff))"}'
'{"path":["contains","file"],"parameters":"(content:\\\\((stuff))"}'
)
})
@ -40,13 +40,13 @@ describe('scanPredicate', () => {
test('resolve field aliases for predicates', () => {
expect(scanPredicate('r', 'contains.file(content:stuff)')).toMatchInlineSnapshot(
'{"field":"repo","name":"contains.file","parameters":"(content:stuff)"}'
'{"path":["contains","file"],"parameters":"(content:stuff)"}'
)
})
test('scan recognized file:contains.content syntax', () => {
expect(scanPredicate('file', 'contains.content(stuff)')).toMatchInlineSnapshot(
'{"field":"file","name":"contains.content","parameters":"(stuff)"}'
'{"path":["contains","content"],"parameters":"(stuff)"}'
)
})
@ -57,10 +57,28 @@ describe('scanPredicate', () => {
test('scan invalid file:contains() syntax', () => {
expect(scanPredicate('file', 'contains(stuff')).toMatchInlineSnapshot('invalid')
})
})
test('scan repo:has.meta with regex', () => {
expect(scanPredicate('repo', 'has.meta(/abc.*/:/def.*/)')).toMatchInlineSnapshot(
'{"field":"repo","name":"has.meta","parameters":"(/abc.*/:/def.*/)"}'
describe('resolveAccess', () => {
test('resolves partial access tree', () => {
expect(resolveAccess(['repo'], PREDICATES)).toMatchInlineSnapshot(
'[{"name":"contains","fields":[{"name":"file"},{"name":"path"},{"name":"content"},{"name":"commit","fields":[{"name":"after"}]}]},{"name":"has","fields":[{"name":"file"},{"name":"path"},{"name":"content"},{"name":"commit","fields":[{"name":"after"}]},{"name":"description"},{"name":"tag"},{"name":"key"},{"name":"meta"},{"name":"topic"}]}]'
)
})
test('resolves partial access tree depth 2', () => {
expect(resolveAccess(['repo', 'contains', 'commit'], PREDICATES)).toMatchInlineSnapshot('[{"name":"after"}]')
})
test('resolves fully qualified path', () => {
expect(resolveAccess(['repo', 'contains', 'file'], PREDICATES)).toMatchInlineSnapshot('[]')
})
test('undefind path', () => {
expect(resolveAccess(['OCOTILLO', 'contains', 'file'], PREDICATES)).toMatchInlineSnapshot('invalid')
})
test('invalid predicate syntax', () => {
expect(resolveAccess(['repo', 'contains'], PREDICATES)).toMatchInlineSnapshot('invalid')
})
})

View File

@ -1,39 +1,102 @@
/* eslint-disable no-template-curly-in-string */
import { type Completion, resolveFieldAlias, FilterType } from './filters'
interface PredicateDefinition {
field: string
interface Access {
name: string
fields?: Access[]
}
// PREDICATES is a registry of predicates, grouped by field they belong to
export const PREDICATES: PredicateDefinition[] = [
{ field: 'repo', name: 'contains.file' },
{ field: 'repo', name: 'contains.path' },
{ field: 'repo', name: 'contains.content' },
{ field: 'repo', name: 'contains.commit.after' },
{ field: 'repo', name: 'contains.commit.after' },
{ field: 'repo', name: 'has' },
{ field: 'repo', name: 'has.file' },
{ field: 'repo', name: 'has.path' },
{ field: 'repo', name: 'has.content' },
{ field: 'repo', name: 'has.commit.after' },
{ field: 'repo', name: 'has.description' },
{ field: 'repo', name: 'has.tag' },
{ field: 'repo', name: 'has.key' },
{ field: 'repo', name: 'has.meta' },
{ field: 'repo', name: 'has.topic' },
{ field: 'file', name: 'contains.content' },
{ field: 'file', name: 'has.content' },
{ field: 'file', name: 'has.owner' },
{ field: 'rev', name: 'at.time' },
/**
* Represents recognized predicate accesses associated with fields. The
* data structure is a tree, where nodes are lists to preserve ordering
* for autocomplete suggestions.
*/
export const PREDICATES: Access[] = [
{
name: 'repo',
fields: [
{
name: 'contains',
fields: [
{ name: 'file' },
{ name: 'path' },
{ name: 'content' },
{
name: 'commit',
fields: [{ name: 'after' }],
},
],
},
{
name: 'has',
fields: [
{ name: 'file' },
{ name: 'path' },
{ name: 'content' },
{
name: 'commit',
fields: [{ name: 'after' }],
},
{ name: 'description' },
{ name: 'tag' },
{ name: 'key' },
{ name: 'meta' },
{ name: 'topic' },
],
},
],
},
{
name: 'file',
fields: [
{
name: 'contains',
fields: [{ name: 'content' }],
},
{
name: 'has',
fields: [{ name: 'content' }, { name: 'owner' }],
},
],
},
{
name: 'rev',
fields: [
{
name: 'at',
fields: [{ name: 'time' }],
},
],
},
]
/** Represents a predicate's components corresponding to the syntax path(parameters). */
export interface PredicateInstance extends PredicateDefinition {
export interface Predicate {
path: string[]
parameters: string
}
/** Returns the access tree for a predicate path. */
export const resolveAccess = (path: string[], tree: Access[]): Access[] | undefined => {
if (path.length === 0) {
return tree
}
// repo:contains() and file:contains() are not supported
if (path.length === 1 && path[0] === 'contains') {
return undefined
}
const subtree = tree.find(value => value.name === path[0])
if (!subtree) {
return undefined
}
if (!subtree.fields) {
return []
}
return resolveAccess(path.slice(1), subtree.fields)
}
// scans a string up to closing parentheses. Examples:
// - `foo` succeeds, parentheses are absent, so it is vacuously balanced
// - `foo(...)` succeeds up to the closing `)`
@ -91,22 +154,23 @@ const scanBalancedParens = (input: string): string | undefined => {
* (1) The (field, name) pair is a recognized predicate.
* (2) The parameters value is well-balanced.
*/
export const scanPredicate = (field: string, value: string): PredicateInstance | undefined => {
export const scanPredicate = (field: string, value: string): Predicate | undefined => {
const match = value.match(/^[.a-z]+/i)
if (!match) {
return undefined
}
const name = match[0]
const path = name.split('.')
// Remove negation from the field for lookup
if (field.startsWith('-')) {
field = field.slice(1)
}
field = resolveFieldAlias(field)
const predicate = PREDICATES.find(predicate => predicate.field === field && predicate.name === name)
if (!predicate) {
const access = resolveAccess([field, ...path], PREDICATES)
if (!access) {
return undefined
}
const rest = value.slice(predicate.name.length)
const rest = value.slice(name.length)
const parameters = scanBalancedParens(rest)
if (!parameters) {
return undefined
@ -115,7 +179,7 @@ export const scanPredicate = (field: string, value: string): PredicateInstance |
return undefined
}
return { ...predicate, parameters }
return { path, parameters }
}
export const predicateCompletion = (field: FilterType): Completion[] => {

View File

@ -93,7 +93,7 @@ const zeroOrMore =
/**
* Returns a {@link Scanner} that succeeds if any of the given scanner succeeds.
*/
export const oneOf =
const oneOf =
<T>(...scanners: Scanner<T>[]): Scanner<T> =>
(input, start) => {
const expected: string[] = []
@ -115,7 +115,7 @@ export const oneOf =
* A {@link Scanner} that will attempt to scan delimited strings for an arbitrary
* delimiter. `\` is treated as an escape character for the delimited string.
*/
export const quoted =
const quoted =
(delimiter: string): Scanner<Literal> =>
(input, start) => {
if (input[start] !== delimiter) {
@ -297,7 +297,7 @@ export const scanPredicateValue = (input: string, start: number, field: Literal)
at: start,
}
}
const value = `${result.name}${result.parameters}`
const value = `${result.path.join('.')}${result.parameters}`
return {
type: 'success',
term: createLiteral(value, { start, end: start + value.length }),

View File

@ -3,10 +3,10 @@ import { describe, expect, test } from 'vitest'
import { createAggregateError, isErrorLike } from '@sourcegraph/common'
import {
type CustomMergeFunctions,
gqlToCascade,
merge,
mergeSettings,
type CustomMergeFunctions,
type Settings,
type SettingsCascade,
type SettingsSubject,
@ -198,6 +198,7 @@ describe('mergeSettings', () => {
key: '1',
description: 'global saved query',
query: 'type:diff global',
notify: true,
},
],
},
@ -207,6 +208,7 @@ describe('mergeSettings', () => {
key: '2',
description: 'org saved query',
query: 'type:diff org',
notify: true,
},
],
},
@ -216,6 +218,7 @@ describe('mergeSettings', () => {
key: '3',
description: 'user saved query',
query: 'type:diff user',
notify: true,
},
],
},
@ -226,16 +229,19 @@ describe('mergeSettings', () => {
key: '1',
description: 'global saved query',
query: 'type:diff global',
notify: true,
},
{
key: '2',
description: 'org saved query',
query: 'type:diff org',
notify: true,
},
{
key: '3',
description: 'user saved query',
query: 'type:diff user',
notify: true,
},
],
}))

View File

@ -311,6 +311,7 @@ const defaultFeatures: SettingsExperimentalFeatures = {
isInitialized: true,
searchQueryInput: 'v2',
newSearchResultFiltersPanel: true,
newCodyWeb: true,
}
/**

View File

@ -96,11 +96,6 @@ export interface TemporarySettingsSchema {
/** OpenCodeGraph */
'openCodeGraph.annotations.visible': boolean
'webNext.welcomeOverlay.dismissed': boolean
'webNext.welcomeOverlay.show': boolean
'webNext.departureMessage.dismissed': boolean
'webNext.departureMessage.show': boolean
}
/**
@ -166,10 +161,6 @@ const TEMPORARY_SETTINGS: Record<keyof TemporarySettings, null> = {
'simple.search.toggle': null,
'cody.onboarding.completed': null,
'openCodeGraph.annotations.visible': null,
'webNext.welcomeOverlay.dismissed': null,
'webNext.welcomeOverlay.show': null,
'webNext.departureMessage.dismissed': null,
'webNext.departureMessage.show': null,
}
export const TEMPORARY_SETTINGS_KEYS = Object.keys(TEMPORARY_SETTINGS) as readonly (keyof TemporarySettings)[]

View File

@ -10,7 +10,9 @@ const decorator: Decorator = story => <div className="p-3 container">{story()}</
const config: Meta = {
title: 'shared/SymbolTag',
parameters: {},
parameters: {
chromatic: { disableSnapshots: false },
},
decorators: [decorator],
}

View File

@ -1,3 +1,4 @@
// NOTE(naman): Remember to add events to allow list: https://docs-legacy.sourcegraph.com/dev/background-information/data-usage-pipeline#allow-list
export const enum EventName {
CODY_CHAT_PAGE_VIEWED = 'web:codyChat:pageViewed',
CODY_CHAT_SUBMIT = 'web:codyChat:submit',

View File

@ -1,5 +1,6 @@
load("@npm//:defs.bzl", "npm_link_all_packages")
load("//dev:defs.bzl", "npm_package", "ts_project")
load("//dev:defs.bzl", "npm_package", "sass", "ts_project")
load("//client/shared/dev:tools.bzl", "module_style_typings")
load("//dev:eslint.bzl", "eslint_config_and_lint_root")
# gazelle:js_resolve **/*.module.scss :module_style_typings
@ -24,10 +25,23 @@ ts_config(
],
)
module_style_typings(
name = "module_style_typings",
)
sass(
name = "module_styles",
srcs = glob(["src/**/*.module.scss"]),
)
ts_project(
name = "storybook_lib",
srcs = [
"globals.d.ts",
"src/decorators/withChromaticThemes/ChromaticRoot/ChromaticRoot.tsx",
"src/decorators/withChromaticThemes/ChromaticRoot/index.ts",
"src/decorators/withChromaticThemes/index.ts",
"src/decorators/withChromaticThemes/withChromaticThemes.tsx",
"src/dummyEventSourcePolyfill.ts",
"src/environment-config.ts",
"src/main.ts",
@ -36,6 +50,7 @@ ts_project(
],
tsconfig = ":tsconfig",
deps = [
":module_style_typings",
":node_modules/@sourcegraph/build-config",
":node_modules/@sourcegraph/wildcard",
"//:node_modules/@storybook/addon-actions",
@ -45,11 +60,14 @@ ts_project(
"//:node_modules/@storybook/react-vite",
"//:node_modules/@storybook/theming",
"//:node_modules/@storybook/types",
"//:node_modules/@types/classnames",
"//:node_modules/@types/node",
"//:node_modules/@types/react",
"//:node_modules/classnames",
"//:node_modules/focus-visible",
"//:node_modules/open-color",
"//:node_modules/react",
"//:node_modules/vite-plugin-turbosnap",
],
)
@ -57,6 +75,7 @@ npm_package(
name = "storybook_pkg",
srcs = [
"package.json",
":module_styles", #keep
":storybook_lib",
],
)

View File

@ -9,6 +9,7 @@
"scripts": {
"lint:js": "eslint --cache 'src/**/*.[jt]s?(x)'",
"start": "TS_NODE_TRANSPILE_ONLY=true sb dev -p 9001 -c ./src",
"start:chromatic": "CHROMATIC=true TS_NODE_TRANSPILE_ONLY=true sb dev -p 9001 -c ./src",
"build": "TS_NODE_TRANSPILE_ONLY=true sb build -c ./src",
"test": "echo no tests"
},

View File

@ -0,0 +1,7 @@
.theme-wrapper {
padding: 1rem;
color: var(--body-color);
background-color: var(--body-bg);
position: relative;
min-height: 50vh;
}

View File

@ -0,0 +1,30 @@
import { type FunctionComponent, type PropsWithChildren, useState } from 'react'
import classNames from 'classnames'
import { PopoverRoot } from '@sourcegraph/wildcard'
import { ChromaticThemeContext, type ChromaticTheme } from '@sourcegraph/wildcard/src/stories'
import styles from './ChromaticRoot.module.scss'
interface ChromaticRootProps extends ChromaticTheme {}
export const ChromaticRoot: FunctionComponent<PropsWithChildren<ChromaticRootProps>> = props => {
const { theme, children } = props
const [rootReference, setElement] = useState<HTMLDivElement | null>(null)
const themeClass = theme === 'light' ? 'theme-light' : 'theme-dark'
return (
<ChromaticThemeContext.Provider value={{ theme }}>
{/* Required to render `Popover` inside of the `ChromaticRoot` component. */}
<PopoverRoot.Provider value={{ renderRoot: rootReference }}>
<div className={classNames(themeClass, styles.themeWrapper)}>
{children}
<div ref={setElement} />
</div>
</PopoverRoot.Provider>
</ChromaticThemeContext.Provider>
)
}

View File

@ -0,0 +1 @@
export * from './ChromaticRoot'

View File

@ -0,0 +1 @@
export * from './withChromaticThemes'

View File

@ -0,0 +1,32 @@
import type { ReactElement } from 'react'
import type { Decorator } from '@storybook/react'
import { ChromaticRoot } from './ChromaticRoot'
/**
* The global Storybook decorator used to snapshot stories with multiple themes in Chromatic.
*
* It's a recommended way of achieving this goal:
* https://www.chromatic.com/docs/faq#do-you-support-taking-snapshots-of-a-component-with-multiple-the
*
* If the `chromatic.enableDarkMode` story parameter is set to `true`, the story will
* be rendered twice in Chromatic in light and dark modes.
*/
export const withChromaticThemes: Decorator<ReactElement> = (StoryFunc, { parameters }) => {
if (parameters?.chromatic?.enableDarkMode) {
return (
<>
<ChromaticRoot theme="light">
<StoryFunc />
</ChromaticRoot>
<ChromaticRoot theme="dark">
<StoryFunc />
</ChromaticRoot>
</>
)
}
return <StoryFunc />
}

View File

@ -1,3 +1,6 @@
import { getEnvironmentBoolean } from '@sourcegraph/build-config'
export const ENVIRONMENT_CONFIG = {
STORIES_GLOB: process.env.STORIES_GLOB,
CHROMATIC: getEnvironmentBoolean('CHROMATIC'),
}

View File

@ -3,8 +3,9 @@ import path from 'path'
import type { StorybookConfigVite } from '@storybook/builder-vite'
import type { StorybookConfig as ReactViteStorybookConfig } from '@storybook/react-vite'
import type { StorybookConfig } from '@storybook/types'
import turbosnap from 'vite-plugin-turbosnap'
import { ROOT_PATH, STATIC_ASSETS_PATH } from '@sourcegraph/build-config'
import { ROOT_PATH, STATIC_ASSETS_PATH, getEnvironmentBoolean } from '@sourcegraph/build-config'
import { ENVIRONMENT_CONFIG } from './environment-config'
@ -25,7 +26,12 @@ const getStoriesGlob = (): string[] => {
}
const config: StorybookConfig & StorybookConfigVite & ReactViteStorybookConfig = {
framework: '@storybook/react-vite',
// TODO: This has to be an object and not a string for now due to a bug in Chromatic
// that would cause the builder to not be identified correctly.
framework: {
name: '@storybook/react-vite',
options: {},
},
staticDirs: [path.resolve(__dirname, '../assets'), STATIC_ASSETS_PATH],
stories: getStoriesGlob(),
@ -50,6 +56,15 @@ const config: StorybookConfig & StorybookConfigVite & ReactViteStorybookConfig =
},
viteFinal: (config, { configType }) => {
const isChromatic = getEnvironmentBoolean('CHROMATIC')
config.define = { ...config.define, 'process.env.CHROMATIC': isChromatic }
if (isChromatic && configType === 'PRODUCTION') {
// eslint-disable-next-line no-console
console.log('Using TurboSnap plugin!')
config.plugins = config.plugins ?? []
config.plugins.push(turbosnap({ rootDir: config.root ?? ROOT_PATH }))
}
config.build = {
...config.build,
minify: false,
@ -99,4 +114,23 @@ const config: StorybookConfig & StorybookConfigVite & ReactViteStorybookConfig =
},
}
// TODO: We need to replace the @storybook/addon-storysource plugin with an object
// definition to supply options here because chromatic CLI does not properly understand
// the configured addons otherwise.
const idx = config.addons?.findIndex(addon => addon === '@storybook/addon-storysource')
if (idx !== undefined && idx >= 0) {
config.addons![idx] = {
name: '@storybook/addon-storysource',
options: {
rule: {
test: /\.story\.tsx?$/,
},
sourceLoaderOptions: {
injectStoryParameters: false,
prettierConfig: { printWidth: 80, singleQuote: false },
},
},
}
}
module.exports = config

View File

@ -7,12 +7,14 @@ import { withConsole } from '@storybook/addon-console'
import type { DecoratorFn, Parameters } from '@storybook/react'
import { setLinkComponent, AnchorLink } from '@sourcegraph/wildcard'
import { isChromatic } from '@sourcegraph/wildcard/src/stories'
import { withChromaticThemes } from './decorators/withChromaticThemes'
import { themeDark, themeLight, THEME_DARK_CLASS, THEME_LIGHT_CLASS } from './themes'
const withConsoleDecorator: DecoratorFn = (storyFunc, context): ReactElement => withConsole()(storyFunc)(context)
export const decorators = [withConsoleDecorator].filter(Boolean)
export const decorators = [withConsoleDecorator, isChromatic() && withChromaticThemes].filter(Boolean)
export const parameters: Parameters = {
layout: 'fullscreen',
@ -29,12 +31,36 @@ export const parameters: Parameters = {
light: themeLight,
dark: themeDark,
},
// disables snapshotting for all stories by default
chromatic: { disableSnapshot: true },
}
configureActions({ depth: 100, limit: 20 })
setLinkComponent(AnchorLink)
// Default to light theme for Chromatic and "Open canvas in new tab" button.
// addon-dark-mode will override this if it's running.
if (!document.body.classList.contains('theme-dark')) {
document.body.classList.add('theme-light')
}
// Default to light theme for Chromatic and "Open canvas in new tab" button.
// addon-dark-mode will override this if it's running.
if (!document.body.classList.contains('theme-dark')) {
document.body.classList.add('theme-light')
}
if (isChromatic()) {
const style = document.createElement('style')
style.innerHTML = `
.monaco-editor .cursor {
visibility: hidden !important;
}
`
document.head.append(style)
}
declare global {
interface Window {
STORYBOOK_ENV?: string

View File

@ -2,6 +2,7 @@ import ResizeObserver from 'resize-observer-polyfill'
import { vi } from 'vitest'
if ('ResizeObserver' in window === false) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
window.ResizeObserver = ResizeObserver
}

View File

@ -13,7 +13,9 @@ Apache
## Feedback
Your feedback is important to us and is greatly appreciated. Please do not hesitate to submit your ideas or suggestions
about how we can improve the extension to our [Code Search discussion community](https://community.sourcegraph.com/c/code-search/9).
about how we can improve the extension to
our [VS Code Extension Feedback Discussion Thread](https://github.com/sourcegraph/sourcegraph/discussions/34821) on
GitHub.
## Issues / Bugs
@ -194,7 +196,7 @@ with us on the [Sourcegraph Community Slack group](https://about.sourcegraph.com
- [Code of Conduct](https://handbook.sourcegraph.com/company-info-and-process/community/code_of_conduct/)
- [Developing Sourcegraph guide](https://docs.sourcegraph.com/dev)
- [Developing the web clients](https://docs.sourcegraph.com/dev/background-information/web)
- [Feedback / Feature Request](https://community.sourcegraph.com/c/code-search/9)
- [Feedback / Feature Request](https://github.com/sourcegraph/sourcegraph/discussions/34821)
- [Issue Tracker](https://github.com/sourcegraph/sourcegraph/labels/vscode-extension)
- [Report a bug](https://github.com/sourcegraph/sourcegraph/issues/new?labels=team/integrations,vscode-extension&title=VSCode+Bug+report:+&projects=Integrations%20Project%20Board)
- [Troubleshooting docs](https://docs.sourcegraph.com/admin/how-to/troubleshoot-sg-extension#vs-code-extension)

View File

@ -118,7 +118,9 @@ This extension contributes the following settings:
## Questions & Feedback
Feedback and feature requests can be submitted to our [Code Search discussion community](https://community.sourcegraph.com/c/code-search/9).
Feedback and feature requests can be submitted to
our [VS Code Extension Feedback Discussion Board](https://github.com/sourcegraph/sourcegraph/discussions/34821) on
GitHub.
## Uninstallation

View File

@ -28,6 +28,7 @@ const currentAuthStateQuery = gql`
nodes {
id
name
displayName
url
settingsURL
}

View File

@ -7,7 +7,6 @@ import { requestGraphQLFromVSCode } from './requestGraphQl'
const blobContentQuery = gql`
query BlobContent($repository: String!, $revision: String!, $path: String!) {
repository(name: $repository) {
id
commit(rev: $revision) {
blob(path: $path) {
content

View File

@ -34,7 +34,7 @@ const VSCE_LINK_PARAMS_UTM_SIDEBAR = {
export const VSCE_LINK_MARKETPLACE = 'https://marketplace.visualstudio.com/items?itemName=sourcegraph.sourcegraph'
export const VSCE_LINK_USER_DOCS =
'https://docs.sourcegraph.com/cli/how-tos/creating_an_access_token' + VSCE_SIDEBAR_PARAMS
export const VSCE_LINK_FEEDBACK = 'https://community.sourcegraph.com'
export const VSCE_LINK_FEEDBACK = 'https://github.com/sourcegraph/sourcegraph/discussions/categories/feedback'
export const VSCE_LINK_ISSUES =
'https://github.com/sourcegraph/sourcegraph/issues/new?labels=team/integrations,vscode-extension&title=VSCode+Bug+report:+&projects=Integrations%20Project%20Board'
export const VSCE_LINK_TROUBLESHOOT =

View File

@ -0,0 +1,5 @@
PUBLIC_DOTCOM=
PUBLIC_CODY_ENABLED_ON_INSTANCE=true
PUBLIC_CODY_ENABLED_FOR_CURRENT_USER=true
PUBLIC_BATCH_CHANGES_ENABLED=true
PUBLIC_CODE_INSIGHTS_ENABLED=true

View File

@ -0,0 +1,5 @@
PUBLIC_DOTCOM=true
PUBLIC_CODY_ENABLED_ON_INSTANCE=true
PUBLIC_CODY_ENABLED_FOR_CURRENT_USER=true
PUBLIC_BATCH_CHANGES_ENABLED=
PUBLIC_CODE_INSIGHTS_ENABLED=

View File

@ -1,10 +1,7 @@
import type { Preview } from '@storybook/svelte'
import { initialize, mswLoader } from 'msw-storybook-addon'
// Global imports kept in sync with routes/+layout.svelte
import '../src/routes/styles.scss'
import '@fontsource-variable/roboto-mono'
import '@fontsource-variable/inter'
// Initialize MSW
initialize()

View File

@ -18,6 +18,8 @@ SRCS = [
".eslintignore",
".eslintrc.cjs",
".prettierignore",
".env",
".env.dotcom",
"//client/wildcard:sass-breakpoints",
"//client/wildcard:global-style-sources",
"//client/web/dist/img:copy",
@ -97,20 +99,24 @@ BUILD_DEPS = [
":node_modules/@sentry/sveltekit",
":node_modules/@sourcegraph/branded",
":node_modules/@sourcegraph/client-api",
":node_modules/@sourcegraph/cody-web",
":node_modules/@sourcegraph/common",
":node_modules/@sourcegraph/http-client",
":node_modules/@sourcegraph/shared",
":node_modules/@sourcegraph/telemetry",
":node_modules/@sourcegraph/web",
":node_modules/@sourcegraph/wildcard",
":node_modules/@storybook/svelte",
":node_modules/@sveltejs/adapter-static",
":node_modules/@sveltejs/kit",
":node_modules/@sveltejs/vite-plugin-svelte",
":node_modules/@types/prismjs",
":node_modules/@urql/core",
":node_modules/cody-web-experimental",
":node_modules/fzf",
":node_modules/graphql",
":node_modules/hotkeys-js",
":node_modules/mermaid",
":node_modules/prismjs",
":node_modules/re2js",
":node_modules/sass",
":node_modules/signale",
@ -192,21 +198,19 @@ copy_to_directory(
playwright_test_bin.playwright_test(
name = "e2e_test",
timeout = "long",
timeout = "short",
args = [
"test",
"--config $(location playwright.config.ts)",
],
data = glob(
[
"src/**/*.ts",
"src/**/*.spec.ts",
"src/testing/*.ts",
],
) + [
"playwright.config.ts",
"tsconfig.json",
":generate-graphql-types",
":sveltekit-sync",
":test_app_assets",
"//cmd/frontend/graphqlbackend:graphql_schema",
"//dev/tools:chromium",
@ -216,6 +220,7 @@ playwright_test_bin.playwright_test(
"BAZEL": "1",
"ASSETS_DIR": "./client/web-sveltekit/test_app_assets/test_build/_sk/",
},
flaky = True,
)
TESTS = glob([
@ -244,9 +249,7 @@ vitest_test(
bin = vitest_bin,
chdir = package_name(),
data = SRCS + BUILD_DEPS + CONFIGS + TESTS + TEST_BUILD_DEPS,
tags = [
TAG_SEARCHSUITE,
],
tags = [TAG_SEARCHSUITE],
with_vitest_config = False,
)
@ -330,10 +333,10 @@ svelte_check.svelte_check_test(
args = [
"--tsconfig",
"tsconfig.json",
# This causes only errors to be displayed, which is what we want
# to keep noise down in CI
"--threshold",
"error",
"--compiler-warnings",
# missing-declaration is raised for our icon components. The Svelte compiler
# does not take into account ambient declarations (will be fixed in Svelte 5).
"missing-declaration:ignore",
],
chdir = package_name(),
data = SRCS + BUILD_DEPS + CONFIGS + [
@ -344,11 +347,6 @@ svelte_check.svelte_check_test(
# Needed to properly extend vite's UserConfig type
":node_modules/vitest",
],
env = {
# It appears that svelte-check will start the vite dev server,
# but in this case we don't want the proxy to be enabled.
"SK_DISABLE_PROXY": "1",
},
)
filegroup(

View File

@ -3,51 +3,28 @@
This folder contains the experimental [SvelteKit](https://kit.svelte.dev/)
implementation of the Sourcegraph app.
**NOTE:** This is a _very early_ prototype and it will change a lot.
## Developing
There are multiple ways to start the app:
1. Standalone and proxying to S2
```bash
cd client/web-sveltekit
pnpm dev
# Install dependencies
pnpm install
# Run dev server
pnpm run dev
```
Then go to (usually) http://localhost:5173.
The dev server can be accessed on http://localhost:5173. API requests and
signin/signout are proxied to an actual Sourcegraph instance,
https://sourcegraph.com by default (can be overwritten via the
`SOURCEGRAPH_API_URL` environment variable.
Or via `sg`:
If you're a Sourcegraph employee you should run this command to use the right auth instance:
```bash
sg start web-sveltekit-standalone
SOURCEGRAPH_API_URL=https://sourcegraph.sourcegraph.com pnpm run dev
```
Then go to https://sourcegraph.test:5173.
2. Standalone and proxying to dotcom
```bash
cd client/web-sveltekit
pnpm dev:dotcom
```
3. Standalone and proxying to another Sourcegraph instance
```bash
cd client/web-sveltekit
SOURCEGRAPH_API_URL=https://<instance> pnpm dev
```
Then go to (usually) http://localhost:5173.
3. Against a local Sourcegraph instance
```bash
sg start enterprise-sveltekit
```
Then go to https://sourcegraph.test:5173.
### Using code from `@sourcegraph/*`
There are some things to consider when using code from other `@sourcegraph`
@ -82,20 +59,7 @@ pnpm vitest # Run vitest tests
pnpm test # Run playwright tests
```
You can also run playwright tests against a running vite dev server. This is
useful for debugging tests.
```sh
# In one terminal
pnpm dev
```
```sh
# In another terminal
pnpm test:dev
```
Both vitest and playwright tests are run in CI.
In CI we run vitest tests. Playwright test support is currently being worked on.
### Formatting and linting
@ -123,34 +87,6 @@ This noise can be avoided by running the corresponding bazel command instead:
bazel test //client/web-sveltekit:svelte-check
```
### Icons
We use [unplugin-icons](https://github.com/unplugin/unplugin-icons) together
with [unplugin-auto-import](https://github.com/unplugin/unplugin-auto-import)
to manage icons. This allows us to use icons from multiple icon sets without
having to import them manually.
For a list of currently available icon sets see the `@iconify-json/*` packages
in the `package.json` file.
Icon references have the form `I<IconSetName><IconName>`. For example the
[corner down left arrow from Lucide](https://lucide.dev/icons/corner-down-left)
can be referenced as `ILucideCornerDownLeft`.
The icon reference is then used in the `Icon` component. Note that the icon
doesn't have to be imported manually.
```svelte
<script lang="ts">
import { Icon } from '$lib/Icon.svelte';
</script>
<Icon icon={ILucideCornerDownLeft} />
```
When the development server is running, the icon will be automatically added to
`auto-imports.d.ts` so TypeScript knows about it.
### Data loading with GraphQL
This project makes use of query composition, i.e. components define their own

View File

@ -1,15 +0,0 @@
<svg viewBox="0 0 52 52" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M30.8 51.8c-2.8.5-5.5-1.3-6-4.1L17.2 6.2c-.5-2.8 1.3-5.5 4.1-6s5.5 1.3 6 4.1l7.6 41.5c.5 2.8-1.4 5.5-4.1 6z"
fill="var(--icon-color, #FF5543)"
/>
<path
d="M10.9 44.7C9.1 45 7.3 44.4 6 43c-1.8-2.2-1.6-5.4.6-7.2L38.7 8.5c2.2-1.8 5.4-1.6 7.2.6 1.8 2.2 1.6 5.4-.6 7.2l-32 27.3c-.7.6-1.6 1-2.4 1.1z"
fill="var(--icon-color, #A112FF)"
/>
<path
d="M46.8 38.1c-.9.2-1.8.1-2.6-.2L4.4 23.8c-2.7-1-4.1-3.9-3.1-6.6 1-2.7 3.9-4.1 6.6-3.1l39.7 14.1c2.7 1 4.1 3.9 3.1 6.6-.6 1.8-2.2 3-3.9 3.3z"
fill="var(--icon-color, #00CBEC)"
/>
</svg>

Before

Width:  |  Height:  |  Size: 679 B

View File

@ -1,13 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id=".Icon / Symbols" clip-path="url(#clip0_4465_142348)">
<path id="Vector" d="M1.75 0.75H8.75C8.75 0.75 9.75 0.75 9.75 1.75V8.75C9.75 8.75 9.75 9.75 8.75 9.75H1.75C1.75 9.75 0.75 9.75 0.75 8.75V1.75C0.75 1.75 0.75 0.75 1.75 0.75Z" stroke="var(--icon-color)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<path id="Vector_2" d="M14.25 5.25C14.25 5.84095 14.3664 6.42611 14.5925 6.97208C14.8187 7.51804 15.1502 8.01412 15.568 8.43198C15.9859 8.84984 16.482 9.18131 17.0279 9.40746C17.5739 9.6336 18.1591 9.75 18.75 9.75C19.3409 9.75 19.9261 9.6336 20.4721 9.40746C21.018 9.18131 21.5141 8.84984 21.932 8.43198C22.3498 8.01412 22.6813 7.51804 22.9075 6.97208C23.1336 6.42611 23.25 5.84095 23.25 5.25C23.25 4.65905 23.1336 4.07389 22.9075 3.52792C22.6813 2.98196 22.3498 2.48588 21.932 2.06802C21.5141 1.65016 21.018 1.31869 20.4721 1.09254C19.9261 0.866396 19.3409 0.75 18.75 0.75C18.1591 0.75 17.5739 0.866396 17.0279 1.09254C16.482 1.31869 15.9859 1.65016 15.568 2.06802C15.1502 2.48588 14.8187 2.98196 14.5925 3.52792C14.3664 4.07389 14.25 4.65905 14.25 5.25Z" stroke="var(--icon-color)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<path id="Vector_3" d="M18.7859 13.9771C18.7111 13.8333 18.5982 13.7127 18.4596 13.6286C18.321 13.5445 18.162 13.5 17.9999 13.5C17.8378 13.5 17.6787 13.5445 17.5401 13.6286C17.4016 13.7127 17.2887 13.8333 17.2139 13.9771L12.8769 21.7841C12.7948 21.9334 12.7512 22.1008 12.75 22.2712C12.7488 22.4416 12.79 22.6096 12.8699 22.7601C12.9452 22.906 13.0587 23.0287 13.1984 23.1151C13.3381 23.2014 13.4987 23.2481 13.6629 23.2501H22.3369C22.5011 23.2481 22.6616 23.2014 22.8013 23.1151C22.941 23.0287 23.0546 22.906 23.1299 22.7601C23.2098 22.6096 23.251 22.4416 23.2497 22.2712C23.2485 22.1008 23.2049 21.9334 23.1229 21.7841L18.7859 13.9771Z" stroke="var(--icon-color)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<path id="Vector_4" d="M5.85108 13.0347C5.77808 12.9463 5.68644 12.8751 5.58272 12.8262C5.47899 12.7773 5.36574 12.752 5.25108 12.752C5.13641 12.752 5.02316 12.7773 4.91944 12.8262C4.81571 12.8751 4.72407 12.9463 4.65108 13.0347L0.941077 17.4717C0.817448 17.6203 0.749756 17.8074 0.749756 18.0007C0.749756 18.194 0.817448 18.3811 0.941077 18.5297L4.64908 22.9667C4.72207 23.0551 4.81371 23.1263 4.91744 23.1752C5.02116 23.2241 5.13441 23.2494 5.24908 23.2494C5.36374 23.2494 5.47699 23.2241 5.58072 23.1752C5.68444 23.1263 5.77608 23.0551 5.84908 22.9667L9.55708 18.5297C9.68071 18.3811 9.7484 18.194 9.7484 18.0007C9.7484 17.8074 9.68071 17.6203 9.55708 17.4717L5.85108 13.0347Z" stroke="var(--icon-color)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</g>
<defs>
<clipPath id="clip0_4465_142348">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -1,276 +0,0 @@
import type { IncomingMessage } from 'http'
import { Transform } from 'stream'
import { createLogger, type Plugin, type ProxyOptions } from 'vite'
import { svelteKitRoutes, type SvelteKitRoute } from '../src/lib/routes'
interface Options {
target: string
}
/**
* This plugin proxies certain requests to a real Sourcegraph instance. These include
* - API and auth requests
* - asset requests for the React app
* - requests for pages that are not known by the SvelteKit app (e.g. code insights)
*
* It does this by first fetching the sign-in page from the real Sourcegraph instance
* and extracting the JS context object from it. This object contains a list of
* routes known by the server, some of which will be handled by the SvelteKit app.
* (the other data in JS context is ignored)
* Those that are not will be proxied to the real Sourcegraph instance.
*
* Additionally, the plugin injects the JS context provided by the origin server into
* locally generated HTML pages.
*
* This plugin is only enabled in 'serve' mode.
*/
export function sgProxy(options: Options): Plugin {
const name = 'sg:proxy'
const logger = createLogger(undefined, { prefix: `[${name}]` })
// Needs to be kept in sync with app.html
const contextPlaceholder = '// ---window.context---'
// Additional endpoints that should be proxied to the real Sourcegraph instance.
const additionalEndpoints = [
// These are not part of the known routes list, but are required for the SvelteKit app to work
// in development mode.
'^/.api/',
'^/.assets/',
'^/.auth/',
// Repo sub pages are also not part of the known routes list. They are listed here so that we
// proxy them to the real Sourcegraph instance, for consistency with the production setup.
'/-/raw/',
'/-/batch-changes/?',
'/-/settings/?',
'/-/code-graph/?',
'/-/own/?',
]
// Routes known by the server that need to (potentially) be proxied to the real Sourcegraph instance.
let knownServerRoutes: string[] = []
function extractContextRaw(body: string): string | null {
const match = body.match(/window\.context\s*=\s*{.*}/)
return match?.[0] ?? null
}
function extractContext(body: string): Window['context'] | null {
const context = extractContextRaw(body)
if (!context) {
return null
}
return new Function(`return ${context.match(/\{.*\}/)?.[0] ?? ''}`)()
}
/**
* Returns true if the request should be handled by the SvelteKit app. This uses similar
* logic to the `isRouteEnabled` function in the SvelteKit app.
* If the request
*/
function isHandledBySvelteKit(req: IncomingMessage, knownRoutes: string[]) {
const url = new URL(req.url ?? '', `http://${req.headers.host}`)
let foundRoute: SvelteKitRoute | undefined
for (const route of svelteKitRoutes) {
if (route.pattern.test(url.pathname)) {
foundRoute = route
if (!route.isRepoRoot) {
break
}
}
}
if (foundRoute) {
return foundRoute.isRepoRoot ? !knownRoutes.some(route => new RegExp(route).test(url.pathname)) : true
}
return false
}
return {
name,
apply: 'serve',
async config() {
if (!options.target) {
logger.info('No target specified, not proxying requests', { timestamp: true })
return
}
let context: Window['context'] | null
// At startup we fetch the sign-in page from the real Sourcegraph instance to extract the `knownRoutes` array
// from the JS context object. This is used to determine which requests should be proxied to the real Sourcegraph
// instance.
// We keep trying to connect to the origin server in case it is not yet available (e.g. when just starting up a
// local Sourcegraph instance).
let backoff = 1
while (true) {
try {
logger.info(`Fetching JS context from ${options.target}`, { timestamp: true })
// The /sign-in endpoint is always available on dotcom and enterprise instances.
context = await fetch(`${options.target}/sign-in`)
.then(response => response.text())
.then(extractContext)
break
} catch (error) {
logger.error(`Failed to fetch JS context: ${(error as Error).message}`, { timestamp: true })
logger.info(`Retrying in ${backoff} second(s)...`, { timestamp: true })
await new Promise(resolve => setTimeout(resolve, backoff * 1000))
backoff = Math.min(backoff * 2, 10)
}
}
if (!context) {
logger.error('Failed to extract JS context from origin', { timestamp: true })
return
}
knownServerRoutes = context.svelteKit?.knownRoutes ?? []
if (!knownServerRoutes.length) {
logger.error('Failed to extract known routes from JS context', { timestamp: true })
return
}
logger.info(`Known routes from origin JS context\n - ${knownServerRoutes.join('\n - ')}\n`, {
timestamp: true,
})
const baseOptions: ProxyOptions = {
target: options.target,
changeOrigin: true,
secure: false,
headers: context.xhrHeaders,
}
const proxyConfig: Record<string, ProxyOptions> = {
// Proxy requests to specific endpoints to a real Sourcegraph instance.
[`${additionalEndpoints.join('|')}`]: baseOptions,
}
const dynamicOptions: ProxyOptions = {
bypass(req) {
if (!req.url) {
return null
}
// If the request is for a SvelteKit route, we want to serve the SvelteKit app.
return isHandledBySvelteKit(req, knownServerRoutes) ? req.url : null
},
...baseOptions,
}
for (const route of knownServerRoutes) {
// vite's proxy server matches full URL, including query parameters.
// That means a route regex like `^/search[/]?$` (which the server provides)
// would not match `/search?q=foo`. We extend every route regex to allow
// for any query parameters
proxyConfig[route.replace(/\$$/, '(\\?.*)?$')] = dynamicOptions
}
return {
server: {
proxy: proxyConfig,
},
}
},
configureServer(server) {
if (!options.target) {
return
}
server.middlewares.use(function proxyHTML(req, res, next) {
// When a request is made for an HTML page that is handled by the SvelteKit
// we want to inject the same JS context object that we would have fetched
// from the origin server.
// The implementation is quite hacky but apparently but it seems there is no
// better way to do this. It was inspired by the express compression middleware:
// https://github.com/expressjs/compression/blob/f3e6f389cb87e090438e13c04d67cec9e22f8098/index.js
if (req.headers.accept?.includes('html') && isHandledBySvelteKit(req, knownServerRoutes)) {
const setHeader = res.setHeader
const write = res.write
const on = res.on
const end = res.end
const context = fetch(`${options.target}${req.url}`, {
headers: req.headers.cookie ? { cookie: req.headers.cookie } : {},
})
.then(response => response.text())
.then(body => {
const context = extractContextRaw(body)
if (!context) {
throw new Error('window.context not found in response from origin')
}
return context
})
const transform = new Transform({
transform(chunk, encoding, callback) {
context
.then(context => {
let body = Buffer.from(chunk).toString()
if (body.includes(contextPlaceholder)) {
body = body.replace(contextPlaceholder, context)
logger.info(`${req.url} - injected JS context`, { timestamp: true })
}
callback(null, body)
})
.catch(error => {
logger.error(`Error fetching JS context: ${error.message}`, { timestamp: true })
// We explicitly pass null to not cause the proxy to terminate
callback(null, chunk)
})
},
})
transform
.on('data', chunk => {
// @ts-expect-error - the overload signature of write seems to prevent TS from recognizing the correct arguments
if (write.call(res, chunk) === false) {
transform.pause()
}
})
.on('end', () => {
// @ts-expect-error - the overload signature of end seems to prevent TS from recognizing the correct arguments
end.call(res)
})
res.on('drain', () => transform.resume())
let ended = false
res.setHeader = (name, value) => {
// content-length is set and sent before we have a chance to modify the response
// we need to ignore it, otherwise the browser will not render the page
// properly
return name === 'content-length' ? res : setHeader.call(res, name, value)
}
// @ts-expect-error - the overload signature of write seems to prevent TS from recognizing the correct arguments
res.write = (chunk, encoding, cb) => {
if (ended) {
return false
}
return transform.write(chunk, encoding, cb)
}
// @ts-expect-error - the overload signature of write seems to prevent TS from recognizing the correct arguments
res.end = (chunk, encoding, cb) => {
if (ended) {
return false
}
ended = true
return transform.end(chunk, encoding, cb)
}
// @ts-expect-error - the overload signature of write seems to prevent TS from recognizing the correct arguments
res.on = (type, listener) => {
if (type === 'drain') {
return transform.on(type, listener)
}
return on.call(res, type, listener)
}
}
next()
})
},
}
}

View File

@ -8,15 +8,14 @@
"build": "vite build",
"build:preview": "vite build --mode=preview",
"build:watch": "vite build --watch",
"build:enterprise": "DEPLOY_TYPE=dev vite build",
"preview": "vite preview",
"install:browsers": "playwright install",
"test": "DISABLE_APP_ASSETS_MOCKING=true playwright test",
"test:dev": "DISABLE_APP_ASSETS_MOCKING=true PORT=5173 playwright test --ui",
"test:svelte": "vitest --run",
"sync": "svelte-kit sync",
"check": "SK_DISABLE_PROXY=true svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "SK_DISABLE_PROXY=true svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "eslint .",
"format": "prettier --config ./prettier.config.cjs --write . --plugin prettier-plugin-svelte",
"generate": "pnpm -w generate",
@ -38,11 +37,15 @@
"@iconify-json/mdi": "^1.1.67",
"@iconify-json/ph": "^1.1.13",
"@iconify-json/simple-icons": "^1.1.104",
"@playwright/test": "1.46.0",
"@playwright/test": "1.42.1",
"@storybook/addon-essentials": "^8.0.5",
"@storybook/addon-interactions": "^7.2.0",
"@storybook/addon-links": "^7.2.0",
"@storybook/addon-svelte-csf": "^4.1.2",
"@storybook/blocks": "^8.0.5",
"@storybook/svelte": "^8.0.5",
"@storybook/sveltekit": "^8.0.5",
"@storybook/testing-library": "0.2.0",
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/adapter-static": "^3.0.0",
"@sveltejs/kit": "^2.5.17",
@ -51,12 +54,13 @@
"@testing-library/user-event": "^14.4.3",
"@types/cookie": "^0.5.1",
"@types/highlight.js": "^9.12.4",
"@types/prismjs": "^1.26.0",
"eslint-plugin-storybook": "^0.8.0",
"eslint-plugin-svelte3": "^4.0.0",
"graphql": "^15.0.0",
"msw": "^1.2.3",
"msw-storybook-addon": "^1.10.0",
"playwright": "1.46.0",
"playwright": "1.42.1",
"prettier": "2.8.1",
"prettier-plugin-svelte": "^2.0.0",
"sass": "^1.32.4",
@ -81,17 +85,21 @@
"@sentry/sveltekit": "^8.7.0",
"@sourcegraph/branded": "workspace:*",
"@sourcegraph/client-api": "workspace:*",
"@sourcegraph/cody-web": "^0.4.0",
"@sourcegraph/common": "workspace:*",
"@sourcegraph/http-client": "workspace:*",
"@sourcegraph/shared": "workspace:*",
"@sourcegraph/telemetry": "^0.11.0",
"@sourcegraph/web": "workspace:*",
"@sourcegraph/wildcard": "workspace:*",
"@storybook/test": "^8.0.5",
"@urql/core": "^4.2.3",
"cody-web-experimental": "^0.2.4",
"copy-to-clipboard": "^3.3.1",
"fzf": "^0.5.2",
"highlight.js": "^10.0.0",
"hotkeys-js": "^3.13.7",
"mermaid": "^10.9.1",
"prismjs": "^1.29.0",
"re2js": "^0.4.1",
"ts-key-enum": "^2.0.12",
"wonka": "^6.3.4",

View File

@ -11,11 +11,6 @@ const config: PlaywrightTestConfig = {
command: 'pnpm build:preview && pnpm preview',
port: PORT,
reuseExistingServer: true,
env: {
// Disable proxying to a real Sourcegraph instance in local testing
SK_DISABLE_PROXY: 'true',
},
timeout: 5 * 60_000,
}
: undefined,
reporter: 'list',

View File

@ -2,8 +2,5 @@ const baseConfig = require('../../prettier.config.js')
module.exports = {
...baseConfig,
plugins: [...(baseConfig.plugins || []), 'prettier-plugin-svelte'],
overrides: [
...(baseConfig.overrides || []),
{ files: '*.svelte', options: { parser: 'svelte', htmlWhitespaceSensitivity: 'strict' } },
],
overrides: [...(baseConfig.overrides || []), { files: '*.svelte', options: { parser: 'svelte' } }],
}

View File

@ -12,21 +12,22 @@
<meta name="color-scheme" content="light dark" />
<script ignore-csp>
// The window.context object extracted from the origin server is injected here.
// Needs to be kept in sync with the vite-sg-proxy.ts plugin
// ---window.context---
window.context = Object.assign(
{},
// Injected window.context (via proxy) if available
window.context,
// Dev specific overwrites
{
sentryDSN: undefined,
window.context = {
// Necessary to make authenticated GraphQL requests in dev mode
xhrHeaders: {
'X-Requested-With': 'Sourcegraph',
},
// Playwright specific overwrites
window.playwrightContext
)
// Local standalone dev server for dotcom can be started with
// pnpm dev:dotcom
sourcegraphDotComMode: !!'%sveltekit.env.PUBLIC_DOTCOM%',
codyEnabledOnInstance: !!'%sveltekit.env.PUBLIC_CODY_ENABLED_ON_INSTANCE%',
codyEnabledForCurrentUser: !!'%sveltekit.env.PUBLIC_CODY_ENABLED_FOR_CURRENT_USER%',
batchChangesEnabled: !!'%sveltekit.env.PUBLIC_BATCH_CHANGES_ENABLED%',
codeInsightsEnabled: !!'%sveltekit.env.PUBLIC_CODE_INSIGHTS_ENABLED%',
// The following are used to mock context in playwright tests
...(typeof window.context === 'object' ? window.context : {}),
}
window.pageError = undefined
</script>

View File

@ -11,7 +11,6 @@ declare global {
const ILucideArchive: typeof import('~icons/lucide/archive')['default']
const ILucideArrowDownFromLine: typeof import('~icons/lucide/arrow-down-from-line')['default']
const ILucideArrowLeftFromLine: typeof import('~icons/lucide/arrow-left-from-line')['default']
const ILucideArrowRight: typeof import('~icons/lucide/arrow-right')['default']
const ILucideArrowRightFromLine: typeof import('~icons/lucide/arrow-right-from-line')['default']
const ILucideBarChartBig: typeof import('~icons/lucide/bar-chart-big')['default']
const ILucideBookOpen: typeof import('~icons/lucide/book-open')['default']
@ -40,9 +39,9 @@ declare global {
const ILucideEllipsis: typeof import('~icons/lucide/ellipsis')['default']
const ILucideExternalLink: typeof import('~icons/lucide/external-link')['default']
const ILucideEye: typeof import('~icons/lucide/eye')['default']
const ILucideFIleText: typeof import('~icons/lucide/f-ile-text')['default']
const ILucideFile: typeof import('~icons/lucide/file')['default']
const ILucideFileCode: typeof import('~icons/lucide/file-code')['default']
const ILucideFileDiff: typeof import('~icons/lucide/file-diff')['default']
const ILucideFileJson: typeof import('~icons/lucide/file-json')['default']
const ILucideFileSearch2: typeof import('~icons/lucide/file-search2')['default']
const ILucideFileStack: typeof import('~icons/lucide/file-stack')['default']
@ -62,14 +61,12 @@ declare global {
const ILucideGitCompareArrows: typeof import('~icons/lucide/git-compare-arrows')['default']
const ILucideGitFork: typeof import('~icons/lucide/git-fork')['default']
const ILucideGitMerge: typeof import('~icons/lucide/git-merge')['default']
const ILucideHelp: typeof import('~icons/lucide/help')['default']
const ILucideHistory: typeof import('~icons/lucide/history')['default']
const ILucideHome: typeof import('~icons/lucide/home')['default']
const ILucideInfo: typeof import('~icons/lucide/info')['default']
const ILucideLink: typeof import('~icons/lucide/link')['default']
const ILucideLock: typeof import('~icons/lucide/lock')['default']
const ILucideMenu: typeof import('~icons/lucide/menu')['default']
const ILucideNetwork: typeof import('~icons/lucide/network')['default']
const ILucideOctagonX: typeof import('~icons/lucide/octagon-x')['default']
const ILucidePanelBottomClose: typeof import('~icons/lucide/panel-bottom-close')['default']
const ILucidePanelLeftClose: typeof import('~icons/lucide/panel-left-close')['default']
@ -77,7 +74,6 @@ declare global {
const ILucidePencil: typeof import('~icons/lucide/pencil')['default']
const ILucideRegex: typeof import('~icons/lucide/regex')['default']
const ILucideRepeat: typeof import('~icons/lucide/repeat')['default']
const ILucideScanSearch: typeof import('~icons/lucide/scan-search')['default']
const ILucideSearch: typeof import('~icons/lucide/search')['default']
const ILucideSearchX: typeof import('~icons/lucide/search-x')['default']
const ILucideSettings: typeof import('~icons/lucide/settings')['default']
@ -85,16 +81,15 @@ declare global {
const ILucideSquareFunction: typeof import('~icons/lucide/square-function')['default']
const ILucideSquareSlash: typeof import('~icons/lucide/square-slash')['default']
const ILucideStar: typeof import('~icons/lucide/star')['default']
const ILucideSymbols: typeof import('~icons/lucide/symbols')['default']
const ILucideTag: typeof import('~icons/lucide/tag')['default']
const ILucideText: typeof import('~icons/lucide/text')['default']
const ILucideUser: typeof import('~icons/lucide/user')['default']
const ILucideUsers: typeof import('~icons/lucide/users')['default']
const ILucideWrapText: typeof import('~icons/lucide/wrap-text')['default']
const ILucideX: typeof import('~icons/lucide/x')['default']
const ILucidehevronLeft: typeof import('~icons/lucide/hevron-left')['default']
const IMdiFormatLetterCase: typeof import('~icons/mdi/format-letter-case')['default']
const IMdiRegex: typeof import('~icons/mdi/regex')['default']
const IMdiTools: typeof import('~icons/mdi/tools')['default']
const IPhFileJpgLight: typeof import('~icons/ph/file-jpg-light')['default']
const IPhFilePngLight: typeof import('~icons/ph/file-png-light')['default']
const IPhGifFill: typeof import('~icons/ph/gif-fill')['default']
@ -105,8 +100,6 @@ declare global {
const IPhosphorePngLight: typeof import('~icons/ph/osphore-png-light')['default']
const ISgBatchChanges: typeof import('~icons/sg/batch-changes')['default']
const ISgCody: typeof import('~icons/sg/cody')['default']
const ISgMark: typeof import('~icons/sg/mark')['default']
const ISgSymbols: typeof import('~icons/sg/symbols')['default']
const ISimpleIconsApachegroovy: typeof import('~icons/simple-icons/apachegroovy')['default']
const ISimpleIconsBitbucket: typeof import('~icons/simple-icons/bitbucket')['default']
const ISimpleIconsC: typeof import('~icons/simple-icons/c')['default']

View File

@ -32,13 +32,14 @@
{#if avatarURL}
<img src={avatarURL} role="presentation" aria-hidden="true" alt="Avatar of {name}" data-avatar />
{:else}
<div data-avatar title={name}>
<div data-avatar>
<span>{getInitials(name)}</span>
</div>
{/if}
<style lang="scss">
span {
z-index: 1;
color: var(--text-muted);
font-size: calc(var(--size) * 0.5);
font-weight: 500;
@ -49,21 +50,29 @@
--min-size: 1.25rem;
--size: var(--avatar-size, var(--icon-inline-size, var(--min-size)));
flex: none;
min-width: var(--min-size);
min-height: var(--min-size);
width: var(--size);
height: var(--size);
border-radius: 50%;
isolation: isolate;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50%;
text-transform: capitalize;
color: var(--color-bg-1);
align-items: center;
justify-content: center;
position: relative;
background: var(--secondary);
user-select: none;
flex: none;
}
div::after {
content: '';
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
border-radius: 50%;
}
</style>

View File

@ -1,29 +0,0 @@
fragment Changelist on PerforceChangelist {
cid
canonicalURL
commit {
message
oid
body
subject
author {
person {
...Avatar_Person
}
date
}
parents {
id
oid
abbreviatedOID
parent: perforceChangelist {
cid
canonicalURL
}
}
perforceChangelist {
cid
canonicalURL
}
}
}

View File

@ -1,157 +0,0 @@
<svelte:options immutable />
<script lang="ts">
import Avatar from '$lib/Avatar.svelte'
import Icon from '$lib/Icon.svelte'
import Timestamp from '$lib/Timestamp.svelte'
import Tooltip from '$lib/Tooltip.svelte'
import type { Changelist } from './Changelist.gql'
import { isViewportMobile } from './stores'
import Button from './wildcard/Button.svelte'
export let changelist: Changelist
export let alwaysExpanded: boolean = false
$: expanded = alwaysExpanded
$: author = changelist.commit.author
$: commitDate = new Date(author.date)
$: authorAvatarTooltip = author.person.name + (author ? ' (author)' : '')
</script>
<div class="root">
<div class="avatar">
<Tooltip tooltip={authorAvatarTooltip}>
<Avatar avatar={author.person} />
</Tooltip>
</div>
<div class="title">
<!-- TODO need subject-->
<a class="subject" href={changelist.canonicalURL}>{changelist.commit.subject}</a>
{#if !alwaysExpanded && changelist.commit.body && !$isViewportMobile}
<Button
variant="secondary"
size="sm"
on:click={() => (expanded = !expanded)}
aria-label="{expanded ? 'Hide' : 'Show'} changelist message"
>
<Icon icon={ILucideEllipsis} inline aria-hidden />
</Button>
{/if}
</div>
<div class="author">
submitted by <strong>{author.person.name}</strong>
<Timestamp date={commitDate} />
</div>
{#if changelist.commit.body}
<div class="message" class:expanded>
{#if $isViewportMobile}
{#if expanded}
<Button variant="secondary" size="lg" display="block" on:click={() => (expanded = false)}>
Close
</Button>
{:else}
<Button variant="secondary" size="sm" display="block" on:click={() => (expanded = true)}>
Show changelist message
</Button>
{/if}
{/if}
<pre>{changelist.commit.body}</pre>
</div>
{/if}
</div>
<style lang="scss">
.root {
display: grid;
overflow: hidden;
grid-template-columns: auto 1fr;
grid-template-areas: 'avatar title' 'avatar author' '. message';
column-gap: 1rem;
@media (--mobile) {
grid-template-columns: auto 1fr;
grid-template-areas: 'avatar title' 'author author' 'message message';
row-gap: 0.5rem;
}
}
.avatar {
grid-area: avatar;
display: flex;
gap: 0.25rem;
align-self: center;
}
.title {
grid-area: title;
align-self: center;
display: flex;
gap: 0.5rem;
align-items: center;
overflow: hidden;
.subject {
font-weight: 600;
flex: 0 1 auto;
color: var(--body-color);
min-width: 0;
@media (--sm-breakpoint-up) {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
.author {
grid-area: author;
color: var(--text-muted);
}
.message {
grid-area: message;
overflow: hidden;
@media (--mobile) {
&.expanded {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: 0;
display: flex;
flex-direction: column;
background-color: var(--color-bg-1);
}
}
}
pre {
display: none;
margin-top: 0.5rem;
margin-bottom: 1.5rem;
font-size: 0.75rem;
max-width: 100%;
word-wrap: break-word;
white-space: pre-wrap;
.expanded & {
display: block;
}
@media (--mobile) {
padding: 0.5rem;
overflow: auto;
margin: 0;
}
}
</style>

View File

@ -71,7 +71,7 @@
padding: '0 1.5ex',
},
'.cm-line': {
lineHeight: 'var(--code-line-height)',
lineHeight: '1.54',
padding: '0',
},
'.selected-line': {
@ -131,7 +131,6 @@
import { browser } from '$app/environment'
import { goto } from '$app/navigation'
import { getExplorePanelContext } from '$lib/codenav/ExplorePanel.svelte'
import type { LineOrPositionOrRange } from '$lib/common'
import { type CodeIntelAPI, Occurrence } from '$lib/shared'
import {
@ -167,7 +166,7 @@
getScrollSnapshot as getScrollSnapshot_internal,
} from './codemirror/utils'
import { registerHotkey } from './Hotkey'
import { goToDefinition } from './repo/blob'
import { goToDefinition, openImplementations, openReferences } from './repo/blob'
import { createLocalWritable } from './stores'
export let blobInfo: BlobInfo
@ -230,23 +229,15 @@
filePath: blobInfo.filePath,
languages: blobInfo.languages,
}
const { openReferences, openDefinitions, openImplementations } = getExplorePanelContext()
$: codeIntelExtension = codeIntelAPI
? createCodeIntelExtension({
api: {
api: codeIntelAPI,
documentInfo: documentInfo,
goToDefinition: (view, definition, options) => {
if (definition.type === 'multiple') {
// Open the explore panel with the definitions
openDefinitions({ documentInfo, occurrence: definition.occurrence })
} else {
goToDefinition(documentInfo, view, definition, options)
}
},
openReferences: (_view, documentInfo, occurrence) => openReferences({ documentInfo, occurrence }),
openImplementations: (_view, documentInfo, occurrence) =>
openImplementations({ documentInfo, occurrence }),
goToDefinition: (view, definition, options) =>
goToDefinition(documentInfo, view, definition, options),
openReferences,
openImplementations,
createTooltipView: options => new HovercardView(options.view, options.token, options.hovercardData),
},
// TODO(fkling): Support tooltip pinning

View File

@ -7,8 +7,6 @@
import Tooltip from '$lib/Tooltip.svelte'
import type { Commit } from './Commit.gql'
import { isViewportMobile } from './stores'
import Button from './wildcard/Button.svelte'
export let commit: Commit
export let alwaysExpanded: boolean = false
@ -24,13 +22,12 @@
return committer
}
$: expanded = alwaysExpanded
$: author = commit.author
$: committer = getCommitter(commit) ?? author
$: committerIsAuthor = committer.person.email === author.person.email
$: commitDate = new Date(committer.date)
$: authorAvatarTooltip = author.person.name + (committer ? ' (author)' : '')
let expanded = alwaysExpanded
</script>
<div class="root">
@ -38,79 +35,54 @@
<Tooltip tooltip={authorAvatarTooltip}>
<Avatar avatar={author.person} />
</Tooltip>
{#if !committerIsAuthor}
</div>
{#if !committerIsAuthor}
<div class="avatar">
<Tooltip tooltip="{committer.person.name} (committer)">
<Avatar avatar={committer.person} />
</Tooltip>
{/if}
</div>
<div class="title">
<a class="subject" href={commit.canonicalURL}>{commit.subject}</a>
{#if !alwaysExpanded && commit.body && !$isViewportMobile}
<Button
variant="secondary"
size="sm"
on:click={() => (expanded = !expanded)}
aria-label="{expanded ? 'Hide' : 'Show'} commit message"
>
<Icon icon={ILucideEllipsis} inline aria-hidden />
</Button>
{/if}
</div>
<div class="author">
{#if !committerIsAuthor}authored by <strong>{author.person.name}</strong> and{/if}
committed by <strong>{committer.person.name}</strong>
<Timestamp date={commitDate} />
</div>
{#if commit.body}
<div class="message" class:expanded>
{#if $isViewportMobile}
{#if expanded}
<Button variant="secondary" size="lg" display="block" on:click={() => (expanded = false)}>
Close
</Button>
{:else}
<Button variant="secondary" size="sm" display="block" on:click={() => (expanded = true)}>
Show commit message
</Button>
{/if}
{/if}
<pre>{commit.body}</pre>
</div>
{/if}
<div class="info">
<span class="title">
<a class="subject" href={commit.canonicalURL}>{commit.subject}</a>
{#if !alwaysExpanded && commit.body}
<button
type="button"
on:click={() => (expanded = !expanded)}
aria-label="{expanded ? 'Hide' : 'Show'} commit message"
>
<Icon icon={ILucideEllipsis} inline aria-hidden />
</button>
{/if}
</span>
<span>
{#if !committerIsAuthor}authored by <strong>{author.person.name}</strong> and{/if}
committed by <strong>{committer.person.name}</strong>
<Timestamp date={commitDate} />
</span>
{#if expanded && commit.body}
<pre>{commit.body}</pre>
{/if}
</div>
</div>
<style lang="scss">
.root {
display: grid;
overflow: hidden;
grid-template-columns: auto 1fr;
grid-template-areas: 'avatar title' 'avatar author' '. message';
column-gap: 1rem;
@media (--mobile) {
grid-template-columns: auto 1fr;
grid-template-areas: 'avatar title' 'author author' 'message message';
row-gap: 0.5rem;
}
display: flex;
gap: 1rem;
}
.avatar {
grid-area: avatar;
.info {
display: flex;
gap: 0.25rem;
align-self: center;
flex-direction: column;
flex: 1;
min-width: 0;
}
.title {
grid-area: title;
align-self: center;
display: flex;
gap: 0.5rem;
align-items: center;
overflow: hidden;
.subject {
font-weight: 600;
@ -126,50 +98,35 @@
}
}
.author {
grid-area: author;
.avatar {
flex: 0 0 auto;
display: flex;
width: 2.75rem;
height: 2.75rem;
font-size: 1.5rem;
}
span {
color: var(--text-muted);
}
.message {
grid-area: message;
overflow: hidden;
button {
color: var(--body-color);
border: 1px solid var(--secondary);
cursor: pointer;
@media (--mobile) {
&.expanded {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: 0;
display: flex;
flex-direction: column;
background-color: var(--color-bg-1);
}
@media (--xs-breakpoint-down) {
align-self: flex-start;
}
}
pre {
display: none;
margin-top: 0.5rem;
margin-bottom: 1.5rem;
font-size: 0.75rem;
overflow: visible;
max-width: 100%;
word-wrap: break-word;
white-space: pre-wrap;
.expanded & {
display: block;
}
@media (--mobile) {
padding: 0.5rem;
overflow: auto;
margin: 0;
}
}
</style>

View File

@ -4,36 +4,30 @@ A component to display the keyboard shortcuts for the application.
<script lang="ts">
import { isMacPlatform } from '$lib/common'
import { formatShortcutParts, type Keys } from '$lib/Hotkey'
import { isViewportMobile } from './stores'
export let shortcut: Keys
export let inline: boolean = false
const separator = isMacPlatform() ? '' : '+'
// No need to do this work if we are on a mobile device
$: parts = $isViewportMobile
? []
: (() => {
const result: string[] = []
let parts = formatShortcutParts(shortcut)
for (let i = 0; i < parts.length; i++) {
if (i > 0) {
result.push(separator)
}
result.push(parts[i])
}
return result
})()
$: parts = (() => {
const result: string[] = []
let parts = formatShortcutParts(shortcut)
for (let i = 0; i < parts.length; i++) {
if (i > 0) {
result.push(separator)
}
result.push(parts[i])
}
return result
})()
</script>
{#if !$isViewportMobile}
<kbd class:inline>
{#each parts as part}
<span>{part}</span>
{/each}
</kbd>
{/if}
<kbd class:inline>
{#each parts as part}
<span>{part}</span>
{/each}
</kbd>
<style lang="scss">
kbd {

View File

@ -1,55 +0,0 @@
<script lang="ts" context="module">
import { Story } from '@storybook/addon-svelte-csf'
import LoadingSpinner from './LoadingSpinner.svelte'
export const meta = {
component: LoadingSpinner,
}
</script>
<Story name="Default">
<h3>Default</h3>
<div class="wrapper">
<LoadingSpinner />
</div>
<h3>--size="2rem"</h3>
<div class="wrapper">
<LoadingSpinner --size="2rem" />
</div>
<h3>center={false}</h3>
<div class="wrapper">
<LoadingSpinner center={false} />
</div>
<h3>inline={true}</h3>
<div class="wrapper">
<LoadingSpinner inline />
</div>
<button>
<LoadingSpinner inline />
<span>Loading...</span>
</button>
<br />
<button style="font-size: 48px">
<LoadingSpinner inline />
<span>Loading...</span>
</button>
</Story>
<style lang="scss">
.wrapper {
display: flex;
margin: 1rem;
width: 10rem;
height: 10rem;
background-color: lightblue;
}
button span {
vertical-align: middle;
}
</style>

View File

@ -3,8 +3,8 @@
export let center = true
</script>
<div class:center class:inline>
<div class="loading-spinner" aria-label="loading" aria-live="polite" />
<div class:center>
<div class="loading-spinner" class:inline aria-label="loading" aria-live="polite" />
</div>
<style lang="scss">
@ -14,11 +14,6 @@
align-items: center;
flex: 1;
justify-content: center;
}
.inline {
display: contents;
}
.loading-spinner {
@ -33,12 +28,13 @@
width: var(--size, 1rem);
height: var(--size, 1rem);
.inline & {
&.inline {
width: #{(16 / 14)}em;
height: #{(16 / 14)}em;
vertical-align: middle;
display: inline-block;
vertical-align: bottom;
display: inline-flex;
align-items: center;
}
border-radius: 50%;

View File

@ -5,7 +5,6 @@
import { createHotkey } from '$lib/Hotkey'
import { popover, onClickOutside, portal } from './dom'
import { isViewportMobile } from './stores'
/**
* Show the popover when hovering over the trigger.
@ -16,7 +15,6 @@
export let hoverDelay: number = 500
export let hoverCloseDelay: number = 150
export let closeOnEsc: boolean = true
export let flip: boolean = true
export let trigger: HTMLElement | null = null
export let target: HTMLElement | undefined = undefined
@ -96,6 +94,7 @@
trigger.addEventListener('mouseenter', handleMouseEnterTrigger)
trigger.addEventListener('mouseleave', handleMouseLeaveTrigger)
trigger.addEventListener('mousemove', handleMouseMoveTrigger)
trigger.addEventListener('click', close)
window.addEventListener('blur', close)
}
@ -103,6 +102,7 @@
trigger.removeEventListener('mouseenter', handleMouseEnterTrigger)
trigger.removeEventListener('mouseleave', handleMouseLeaveTrigger)
trigger.removeEventListener('mousemove', handleMouseMoveTrigger)
trigger.removeEventListener('click', close)
window.removeEventListener('blur', close)
}
@ -111,9 +111,7 @@
let oldTrigger: HTMLElement | null
$: {
oldTrigger && showOnHover && unwatchTrigger(oldTrigger)
if (!$isViewportMobile) {
trigger && showOnHover && watchTrigger(trigger)
}
trigger && showOnHover && watchTrigger(trigger)
oldTrigger = trigger
}
@ -157,9 +155,6 @@
placement,
offset,
shift: { padding: 4 },
flip: flip ? {
fallbackAxisSideDirection: 'start',
} : undefined,
},
}}
on:click-outside={handleClickOutside}
@ -183,7 +178,7 @@
border: 1px solid var(--dropdown-border-color);
border-radius: var(--popover-border-radius);
// Ensure child elements do not overflow the border radius
overflow-y: scroll;
overflow: hidden;
// We always display the popover on hover, but there may not be anything
// inside until something we load something. This ensures we do not

View File

@ -8,11 +8,9 @@
import { afterUpdate, createEventDispatcher } from 'svelte'
export let margin: number
export let viewport: HTMLElement | undefined = undefined
export let scroller: HTMLElement | undefined = undefined
export function capture(): Capture {
return { scroll: scroller?.scrollTop || 0 }
return { scroll: scroller.scrollTop }
}
export function restore(data?: Capture) {
@ -33,13 +31,14 @@
const dispatch = createEventDispatcher<{ more: void }>()
function handleScroll() {
if (scroller && viewport) {
const remaining = scroller.scrollHeight - (scroller.scrollTop + (viewport?.clientHeight ?? 0))
let viewport: HTMLElement
let scroller: HTMLElement
if (remaining < margin) {
dispatch('more')
}
function handleScroll() {
const remaining = scroller.scrollHeight - (scroller.scrollTop + viewport.clientHeight)
if (remaining < margin) {
dispatch('more')
}
}
@ -47,13 +46,13 @@
// This premptively triggers a 'more' event when the scrollable content is smaller than than
// scroller. Without this, the 'more' event would not be triggered because there is nothing
// to scroll.
if (scroller && scroller.scrollHeight <= scroller.clientHeight) {
if (scroller.scrollHeight <= scroller.clientHeight) {
dispatch('more')
}
})
</script>
<div class="viewport" bind:this={viewport} data-viewport>
<div class="viewport" bind:this={viewport}>
<div class="scroller" bind:this={scroller} on:scroll={handleScroll} data-scroller>
<slot />
</div>

View File

@ -0,0 +1,45 @@
<script lang="ts" context="module">
import Separator, { getSeparatorPosition } from '$lib/Separator.svelte'
import { Story } from '@storybook/addon-svelte-csf'
export const meta = {
component: Separator,
}
</script>
<script lang="ts">
const currentPosition = getSeparatorPosition('separator-example', 0.5)
$: width = `${$currentPosition * 100}%`
</script>
<Story name="Default">
<section>
<div class="left match-highlight" style:min-width={width} style:max-width={width}>Left content</div>
<Separator {currentPosition} />
<div class="right">Right content</div>
</section>
</Story>
<style lang="scss">
section {
display: flex;
height: 90vh;
}
div {
padding: 1rem;
display: flex;
align-items: center;
justify-content: center;
background-clip: content-box;
}
.left {
background-color: var(--color-bg-2);
}
.right {
flex: 1;
background-color: var(--color-bg-2);
}
</style>

View File

@ -0,0 +1,106 @@
<script lang="ts" context="module">
import { derived, type Writable } from 'svelte/store'
import { createLocalWritable } from '$lib/stores'
const dividerStore = createLocalWritable<Record<string, number>>('dividers', {})
export function getSeparatorPosition(name: string, defaultValue: number): Writable<number> {
const { subscribe } = derived(dividerStore, dividers => dividers[name] ?? defaultValue)
return {
subscribe,
set(value) {
dividerStore.update(dividers => ({ ...dividers, [name]: value }))
},
update(updater) {
dividerStore.update(dividers => ({ ...dividers, [name]: updater(dividers[name]) }))
},
}
}
</script>
<script lang="ts">
/**
* Store to write current position (0-1) to.
*/
export let currentPosition: Writable<number>
let divider: HTMLElement | null = null
let offset = 0
let dragging = false
function onMouseMove(event: MouseEvent) {
event.preventDefault()
if (divider?.parentElement) {
let width = (event.x - offset) / divider.parentElement.clientWidth
if (width < 0) {
width = 0
} else if (width > 1) {
width = 1
}
$currentPosition = width
}
}
function endResize() {
dragging = false
window.removeEventListener('mousemove', onMouseMove)
window.removeEventListener('mouseup', endResize)
}
function startResize(event: MouseEvent) {
event.preventDefault()
if (divider?.parentElement) {
dragging = true
offset = divider.parentElement.getBoundingClientRect().x + divider.clientWidth
window.addEventListener('mousemove', onMouseMove)
window.addEventListener('mouseup', endResize)
}
}
</script>
<!-- TODO: implement keyboard handlers. See https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/ -->
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<div
bind:this={divider}
role="separator"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={$currentPosition}
class:dragging
on:mousedown={startResize}
>
<!-- spacer is used to increase the interactable surface-->
<div class="spacer" />
</div>
<style lang="scss">
div[role='separator'] {
flex-shrink: 0;
position: relative;
width: 1px;
background-color: var(--border-color);
cursor: col-resize;
.spacer {
position: absolute;
top: 0;
bottom: 0;
left: -5px;
margin-left: -50%;
width: 10px;
}
&.dragging {
z-index: 1;
outline: 1px solid var(--oc-blue-3);
background-color: var(--oc-blue-3);
}
&:hover:not(.dragging) {
z-index: 1;
outline: 1px solid var(--border-color);
}
}
</style>

View File

@ -26,10 +26,6 @@
*/
export let selected: number | null = 0
export let toggable = false
/**
* Whether or not to show the tab header when there is only one tab.
*/
export let showSingleTabHeader = false
const dispatch = createEventDispatcher<{ select: number | null }>()
const id = uuid.v4()
@ -70,14 +66,12 @@
</script>
<div class="tabs" data-tabs>
{#if $tabs.length > 1 || showSingleTabHeader}
<header>
<TabsHeader {id} tabs={$tabs} selected={$selectedTab} on:select={selectTab} />
<div class="actions">
<slot name="header-actions" />
</div>
</header>
{/if}
<header>
<TabsHeader {id} tabs={$tabs} selected={$selectedTab} on:select={selectTab} />
<div class="actions">
<slot name="header-actions" />
</div>
</header>
<slot />
</div>
@ -93,12 +87,10 @@
display: flex;
align-items: center;
border-bottom: 1px solid var(--border-color);
gap: 2rem;
.actions {
margin-left: auto;
margin-right: var(--tabs-horizontal-spacing);
min-width: 0;
}
}
}

View File

@ -66,7 +66,8 @@
display: flex;
align-items: stretch;
justify-content: var(--tabs-header-align, flex-start);
justify-content: var(--align-tabs, center);
gap: var(--tabs-gap, 0);
}
[role='tab'] {
@ -76,7 +77,7 @@
align-items: center;
min-height: 2rem;
padding: 0.25rem 0.75rem;
color: var(--text-body);
color: var(--text-muted);
display: inline-flex;
flex-flow: row nowrap;
justify-content: center;
@ -113,15 +114,13 @@
span {
display: inline-block;
&[data-tab-title] {
// Hidden rendering of the bold tab title to prevent
// shifting when the tab is selected.
&::before {
content: attr(data-tab-title);
display: block;
height: 0;
visibility: hidden;
}
// Hidden rendering of the bold tab title to prevent
// shifting when the tab is selected.
&::before {
content: attr(data-tab-title);
display: block;
height: 0;
visibility: hidden;
}
}

View File

@ -5,9 +5,7 @@
</script>
<script lang="ts">
import { onMount, tick } from 'svelte'
import { type PopoverOptions, popover, portal, uniqueID } from './dom'
import { popover, portal, uniqueID } from './dom'
/**
* The content of the tooltip.
@ -43,24 +41,8 @@
shift: {
padding: 4,
},
onSize(element, { availableWidth, availableHeight }) {
Object.assign(element.style, {
maxWidth: `min(var(--tooltip-max-width), ${availableWidth}px)`,
maxHeight: `${availableHeight}px`,
})
},
} satisfies PopoverOptions
$: if (target && tooltip) {
target.setAttribute('aria-label', tooltip)
}
onMount(async () => {
// We need to wait for the element to be rendered before we can check whether it
// is part of the layout.
// (this fixes and issue where the tooltip would not show up in hovercards)
await tick()
$: {
let node = wrapper?.firstElementChild
// Use `getClientRects` to check if the element is part of the layout.
// For example, an element with `display: contents` will not be part of the layout.
@ -72,7 +54,10 @@
if (node) {
target = node
}
})
}
$: if (target && tooltip) {
target.setAttribute('aria-label', tooltip)
}
</script>
<!-- TODO: close tooltip on escape -->
@ -88,16 +73,12 @@
on:mouseleave={hide}
on:focusin={show}
on:focusout={hide}
data-tooltip-root><!--
--><slot /><!--
--></div
><!--
-->{#if (alwaysVisible || visible) && target && tooltip}<div
role="tooltip"
{id}
use:popover={{ reference: target, options }}
use:portal
>
data-tooltip-root
>
<slot />
</div>
{#if (alwaysVisible || visible) && target && tooltip}
<div role="tooltip" {id} use:popover={{ reference: target, options }} use:portal>
<div class="content">{tooltip}</div>
<div data-arrow />
</div>

View File

@ -31,7 +31,6 @@
$: selected = $treeState.selected === nodeID
$: tabindex = $treeState.focused === nodeID ? 0 : -1
$: children = expandable && expanded ? treeProvider.fetchChildren(entry) : null
$: disableScope = $treeState.disableScope
let level = getContext<TreeNodeContext>('tree-node-nesting')?.level ?? 0
setContext('tree-node-nesting', { level: level + 1 })
@ -77,37 +76,30 @@
{tabindex}
data-treeitem
data-node-id={nodeID}
class:disable-scope={disableScope}
style="--tree-node-nested-level: {level}"
>
<div bind:this={label} class="label" data-treeitem-label class:expandable>
<!-- TODO: scoping is an operation specific to the file tree, but this
is intended to be a generic tree component. We should not add a scope
button here. -->
<span bind:this={label} class="label" data-treeitem-label class:expandable>
<Button variant="icon" on:click={handleScopeChange} data-scope-button>
<Icon icon={ILucideFocus} inline aria-hidden="true" />
</Button>
<!-- hide the open/close button to preserve alignment with expandable entries -->
<div class="indented">
{#if expandable}
<!-- We have to stop even propagation because the tree root
listens for click events for selecting items. We don't want the
item to be selected when the open/close button is pressed. -->
<Button
variant="icon"
on:click={event => {
event.stopPropagation()
toggleOpen()
}}
tabindex={-1}
aria-label="{expanded ? 'Collapse' : 'Expand'} subtree"
>
<Icon icon={expanded ? ILucideChevronDown : ILucideChevronRight} inline aria-hidden="true" />
</Button>
{/if}
<slot {entry} {expanded} toggle={toggleOpen} {label} />
</div>
</div>
{#if expandable}
<!-- We have to stop even propagation because the tree root listens for click events for
selecting items. We don't want the item to be selected when the open/close button is pressed.
-->
<Button
variant="icon"
on:click={event => {
event.stopPropagation()
toggleOpen()
}}
tabindex={-1}
>
<Icon icon={expanded ? ILucideChevronDown : ILucideChevronRight} inline />
</Button>
{/if}
<slot {entry} {expanded} toggle={toggleOpen} {label} />
</span>
{#if expanded && children}
{#await children}
<div class="loading">
@ -130,74 +122,68 @@
<style lang="scss">
$shiftWidth: 1.25rem;
$gap: 0.25rem;
$indentSize: calc(var(--tree-node-nested-level) * #{$shiftWidth});
li[role='treeitem'] {
--scope-size: calc(var(--icon-inline-size) + #{$gap} - 1px);
&.disable-scope {
--scope-size: 0px;
:global([data-scope-button]) {
display: none;
}
}
[role='treeitem'] {
border-radius: var(--border-radius);
&[tabindex='0']:focus {
box-shadow: none;
> .label {
box-shadow: var(--focus-shadow-inset);
}
}
.label {
display: flex;
gap: $gap;
padding: 0.2rem $gap;
align-items: center;
// Change icon color based on selected item state
--icon-color: var(--tree-node-expand-icon-color);
color: var(--tree-node-label-color, var(--text-body));
:global([data-scope-button]) {
visibility: hidden;
}
&.expandable:hover,
&.expandable:focus {
:global([data-scope-button]) {
visibility: visible;
}
}
.indented {
display: inherit;
gap: inherit;
margin-left: $indentSize;
width: 100%;
}
}
.loading {
// Indent with two rem since loading represents next nested level
margin-left: calc(var(--scope-size) + #{$indentSize} + 2 * #{$gap});
margin-top: 0.25rem;
}
ul {
position: relative;
isolation: isolate;
// The visual guide line for expanded subtrees
&::before {
position: absolute;
content: '';
border-left: 1px solid var(--secondary);
height: 100%;
transform: translateX(
calc(#{$gap} + var(--scope-size) + #{$indentSize} + var(--icon-inline-size) / 2 - 1px)
);
z-index: 1;
box-shadow: var(--focus-box-shadow);
}
}
}
.loading {
// Indent with two rem since loading represents next nested level
margin-left: calc(var(--tree-node-nested-level) * #{$shiftWidth} + 2 * var(--icon-inline-size) + 2 * #{$gap});
margin-top: 0.25rem;
}
.label {
display: flex;
gap: $gap;
padding: 0.2rem $gap;
justify-content: space-between;
align-items: center;
// Change icon color based on selected item state
--icon-color: var(--tree-node-expand-icon-color);
color: var(--tree-node-label-color, var(--text-body));
li[data-treeitem][aria-selected='true'] > & {
--icon-color: currentColor;
--file-icon-color: currentColor;
color: var(--tree-node-label-color, var(--body-bg));
}
:global([data-scope-button]) {
visibility: hidden;
margin-right: calc(var(--tree-node-nested-level) * #{$shiftWidth});
}
&.expandable:hover,
&.expandable:focus {
:global([data-scope-button]) {
visibility: visible;
}
}
}
ul {
position: relative;
isolation: isolate;
&::before {
position: absolute;
content: '';
border-left: 1px solid var(--secondary);
height: 100%;
transform: translateX(
calc(var(--tree-node-nested-level) * #{$shiftWidth} + var(--icon-inline-size) * 1.5 + #{$gap} + 2px)
);
z-index: 1;
}
}
</style>

View File

@ -7,7 +7,7 @@ export interface TreeProvider<T> {
*/
getEntries(): T[]
/**
* Whether or not the provided entry has (possibly) children or not.
* Whether or not the provided entry is has (possibly) children or not.
*/
isExpandable(entry: T): boolean
/**
@ -29,7 +29,6 @@ export interface SingleSelectTreeState {
focused: string
selected: string
expandedNodes: Set<string>
disableScope: boolean
}
export type TreeState = SingleSelectTreeState
@ -39,7 +38,6 @@ export function createEmptySingleSelectTreeState(): SingleSelectTreeState {
focused: '',
selected: '',
expandedNodes: new Set(),
disableScope: false,
}
}

Some files were not shown because too many files have changed in this diff Show More