Compare commits

..

35 Commits

Author SHA1 Message Date
Smit Barmase
131ed8add1 move include languages in workspace configuration 2025-12-09 10:39:30 +05:30
mafiefa02
1f296334e5 fix(language): Init tw options with includeLanguages instead of
`userLanguages`
2025-12-02 16:44:01 +07:00
Arjun Bajaj
19aba43f3e Add Tailwind CSS support for Gleam (#43968)
Currently, Zed does not provide suggestions and validations for Gleam,
as it is only available for languages specified in `tailwind.rs`. This
pull-request adds Gleam to that list of languages.

After this, if Tailwind is configured to work with Gleam, suggestions
and validation appear correctly.

Even after this change, Tailwind will not be able to detect and give
suggestions in Gleam directly. Below is the config required for Tailwind
classes to be detected in all Gleam strings.


<details><summary>Zed Config for Tailwind detection in Gleam</summary>
<p>

```
{
  "languages": {
    "Gleam": {
      "language_servers": [
        "gleam",
        "tailwindcss-language-server"
      ]
    }
  },
  "lsp": {
    "tailwindcss-language-server": {
      "settings": {
        "experimental": {
          "classRegex": [
            "\"([^\"]*)\""
          ]
        }
      }
    }
  }
}
```

The `classRegex` will match all Gleam strings, making it work seamlessly
with Lustre templates and plain string literals.

</p>
</details> 

Release Notes:

- Added support for Tailwind suggestions and validations for the [Gleam
programming language](https://gleam.run/).
2025-12-02 03:43:31 -05:00
Lukas Wirth
8d09610748 git_ui: Fix utf8 panic in compress_commit_diff (#43972)
Fixes ZED-3QG

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-12-02 08:31:44 +00:00
Lukas Wirth
5b6663ef97 terminal: Try to fix flaky macOS test (#43971)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-12-02 08:21:18 +00:00
Josh Piasecki
f445f22fe6 Add an action listener to workspace for ActivatePreviousItem and ActivateNextItem (#42588)
Release Notes:

- pane::ActivatePreviousItem and pane::ActivateNextItem now toggle the
most recent pane when called from a dock panel


a couple months ago i posted a work around that used `SendKeystrokes` to
cycle through pane items when focused on a dock.
#35253

this pr would add this functionality to the these actions by default.
i implemented this by adding an action listener to the workspace level.

------
if the current context is a dock that does not hold a pane
it retrieves the most recent pane from `activation_history` and
activates the next item on that pane instead.

- `"Pane > Editor"`
cycles through the current pane like normal
- `"Dock > Pane > Terminal"`
also cycles through the pane items like normal
- `"Dock > (Any Child that is not a child of Pane)"`
cycles through the items of the most recent pane.

this is the standard behavior in VS Code i believe.

in the video below you can see the actions cycling through the editor
like normal when focus is on the editor.
then you can see the editor continue to cycle when the focus is on the
project panel.
and that the focus stays on the project panel.
and you can see the action cycle the terminal items when the focus is
moved to the terminal


https://github.com/user-attachments/assets/999ab740-d2fa-4d00-9e53-f7605217e6ac

the only thing i noticed is that for this to work the keybindings must
be set above `Pane`
so they have to be set globally or on workspace. otherwise they do not
match in the context
2025-12-01 22:01:11 -07:00
Connor Tsui
6216af9b5a Allow dynamic set_theme based on Appearance (#42812)
Tracking Issue (does not close):
https://github.com/zed-industries/zed/issues/35552

This is somewhat of a blocker for
https://github.com/zed-industries/zed/pull/40035 (but also the current
behavior doesn't really make sense).

The current behavior of `ThemeSelectorDelegate::set_theme` (the theme
selector menu) is to simply set the in-memory settings to `Static`,
regardless of if it is currently `Dynamic`. The reason this doesn't
matter now is that the `theme::set_theme` function that updates the
user's settings file _will_ make this check, so dynamic settings stay
dynamic in `settings.json`, but not in memory.

But this is also sort of strange, because `theme::set_theme` will set
the setting of whatever the old appearance was to the new theme name. In
other words, if I am currently on a light mode theme and I change my
theme to a dark mode theme using the theme selector, the `light` field
of `theme` in `settings.json` is set to a dark mode theme!

_I think this is because displaying the new theme in the theme selector
does not update the global context, so
`ThemeSettings::get_global(cx).theme.name(appearance).0` returns the
original theme appearance, not the new one._

---

This PR makes `ThemeSelectorDelegate::set_theme` keep the current
`ThemeSelection`, as well as changes the behavior of the
`theme::set_theme` call to always choose the correct setting to update.

One edge case that might be slightly strange now is that if the user has
specified the mode as `System`, this will now override that with the
appearance of the new theme. I think this is fine, as otherwise a user
might set a dark theme and nothing will change because the
`ThemeAppearanceMode` is set to `light` or `system` (where `system` is
also light).

I also have an `unreachable!` in there that I'm pretty sure is true but
I don't really know how to formally prove that...

Release Notes:

- N/A *or* Added/Fixed/Improved ...

---------

Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
2025-12-01 20:52:57 -07:00
Anthony Eid
464c0be2b7 git: Add word diff highlighting (#43269)
This PR adds word/character diff for expanded diff hunks that have both
a deleted and added section, as well as a setting `word_diff_enabled` to
enable/disable word diffs per language.

- `word_diff_enabled`: Defaults to true. Whether or not expanded diff
hunks will show word diff highlights when they're able to.

### Preview
<img width="1502" height="430" alt="image"
src="https://github.com/user-attachments/assets/1a8d5b71-449e-44cd-bc87-d6b65bfca545"
/>

### Architecture

I had three architecture goals I wanted to have when adding word diff
support:

- Caching: We should only calculate word diffs once and save the result.
This is because calculating word diffs can be expensive, and Zed should
always be responsive.
- Don't block the main thread: Word diffs should be computed in the
background to prevent hanging Zed.
- Lazy calculation: We should calculate word diffs for buffers that are
not visible to a user.

To accomplish the three goals, word diffs are computed as a part of
`BufferDiff` diff hunk processing because it happens on a background
thread, is cached until the file is edited, and is only refreshed for
open buffers.

My original implementation calculated word diffs every frame in the
Editor element. This had the benefit of lazy evaluation because it only
calculated visible frames, but it didn't have caching for the
calculations, and the code wasn't organized. Because the hunk
calculations would happen in two separate places instead of just
`BufferDiff`. Finally, it always happened on the main thread because it
was during the `EditorElement` layout phase.

I used Zed's
[`diff_internal`](02b2aa6c50/crates/language/src/text_diff.rs (L230-L267))
as a starting place for word diff calculations because it uses
`Imara_diff` behind the scenes and already has language-specific
support.

#### Future Improvements

In the future, we could add `AST` based word diff highlights, e.g.
https://github.com/zed-industries/zed/pull/43691.

Release Notes:

- git: Show word diff highlight in expanded diff hunks with less than 5
lines.
- git: Add `word_diff_enabled` as a language setting that defaults to
true.

---------

Co-authored-by: David Kleingeld <davidsk@zed.dev>
Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: cameron <cameron.studdstreet@gmail.com>
Co-authored-by: Lukas Wirth <lukas@zed.dev>
2025-12-01 22:36:30 -05:00
Moritz Fröhlich
2df5993eb0 workspace: Add ctrl-w x support for vim (#42792)
Adds support for the vim CTRL-W x keybinding, which swaps the active
pane with the next adjacent one, prioritizing column over row and next
over previous. Upon swap, the pane which was swapped with is activated
(this is the vim behavior).

See also
ca6a260ef1/runtime/doc/windows.txt (L514C1-L521C24)

Release Notes:

- Added ctrl-w x keybinding in Vim mode, which swaps the active window
with the next adjacent one (aligning with Vim behavior)

**Vim behavior**


https://github.com/user-attachments/assets/435a8b52-5d1c-4d4b-964e-4f0f3c9aca31


https://github.com/user-attachments/assets/7aa40014-1eac-4cce-858f-516cd06d13f6

**Zed behavior**


https://github.com/user-attachments/assets/2431e860-4e11-45c6-a3f2-08f1a9b610c1


https://github.com/user-attachments/assets/30432d9d-5db1-4650-af30-232b1340229c

Note: There is a discrepancy where in Vim, if vertical and horizontal
splits are mixed, swapping from a column with a single window does not
work (see the vertical video), whilst in Zed it does. However, I don't
see a good reason as to why this should not be supported and would argue
that it makes more sense to keep the clear priority swap behavior,
instead of adding a workaround to supports such cases.

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-12-02 03:36:06 +00:00
Hans
04e92fb2d2 vim: Fix :s command ignoring case sensitivity settings (#42457)
Closes #36260 

This PR fixes the vim :s Command Ignores Case Sensitivity Settings

Release Notes:

- N/A
2025-12-01 20:22:22 -07:00
Conrad Irwin
e27590432f Actually show settings errors on app load (#43268)
They were previously hidden by the global settings file succeeding to
parse :face-palm:

Release Notes:

- N/A
2025-12-01 19:53:36 -07:00
Cole Miller
a675eb1667 Fix regression in closing project diff (#43964)
Follow-up to #43586--when the diff is not split, just render the primary
editor. Otherwise the child `Pane` intercepts `CloseActiveItem`. (This
is still a bug for the actual split diff, but just fixing the
user-visible regression for now.)

Release Notes:

- N/A
2025-12-02 02:32:36 +00:00
Agus Zubiaga
b27ad98520 zeta: Put back 15s reject debounce (#43958)
Release Notes:

- N/A
2025-12-01 23:57:09 +00:00
Finn Evers
9c4e16088c extension_ci: Use more robust bash syntax for bump_extension_version (#43955)
Also does some more cleanup here and there.

Release Notes:

- N/A
2025-12-01 23:38:05 +00:00
Finn Evers
34a2bfd6b7 ci: Supply github_token to bufbuild_setup_action (#43957)
This should hopefully resolve errors like
https://github.com/zed-industries/zed/actions/runs/19839788214/job/56845583441

Release Notes:

- N/A
2025-12-01 23:37:33 +00:00
Finn Evers
99d8d34d48 Fix failing CI on main (#43956)
Release Notes:

- N/A
2025-12-01 23:27:27 +00:00
Finn Evers
bd79edee71 extension_ci: Improve behavior when no Rust is present (#43953)
Release Notes:

- N/A
2025-12-01 22:58:15 +00:00
Agus Zubiaga
0bb1c6ad3e Return json value instead of empty string from accept/reject endpoints 2025-12-01 19:53:06 -03:00
Clément Lap
fd146757cf Add Doxygen injection into C and C++ comments (#43581)
Release Notes:

- C/C++ files now support Doxygen grammars (if a Doxygen extension is installed).
2025-12-01 23:32:23 +01:00
mikeHag
6eb9f9add7 Debug a test annotated with the ignore attribute if the test name partially matches another test (#43110)
Related: #42574
If an integration test is annotated with the ignore attribute, allow the
"debug: Test" option of the debug scenario or Code Action to run with
the "--include-ignored" and "--exact" arguments. Inclusion of "--exact"
covers the case where more that one test shares a base name. For
example, consider two tests named "test_no_ace_in_middle_of_straight"
and "test_no_ace_in_middle_of_straight_flush." Without the "--exact"
argument both tests would run if a user attempts to debug
"test_no_ace_in_middle_of_straight".

Release Notes:

- Improved "debug test" experience in Rust with ignored tests.
2025-12-01 23:28:37 +01:00
Katie Geer
3d3d124e01 docs: Migrate from VS Code (#43438)
Add guide for users coming from VS Code

Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-12-01 14:08:07 -08:00
Kirill Bulatov
de392cda39 Fix bracket colorization not working in file-less files with a proper language (#43947)
Follow-up of https://github.com/zed-industries/zed/pull/43172

Release Notes:

- Fixed bracket colorization in file-less files with a proper language
2025-12-01 21:53:54 +00:00
Piotr Osiewicz
33513292af script: Fix upload-nightly versioning (#43933)
Release Notes:

- N/A
2025-12-01 22:38:58 +01:00
Finn Evers
1535e95066 ci: Always run nextest for extensions (#43945)
This makes rolling this out across extensions a far bit easier and also
safer, because we don't have to manually set `run_tests` for every
extension (and never have to consider this when updating these).

Release Notes:

- N/A
2025-12-01 21:20:47 +00:00
Agus Zubiaga
26f77032a2 edit prediction: Do not attempt to gather context for non-zeta2 models (#43943)
We were running the LLM-based context gathering for zeta1 and sweep
which don't use it.

Release Notes:

- N/A
2025-12-01 21:14:00 +00:00
Agus Zubiaga
efff602909 zeta: Smaller reject batches (#43942)
We were allowing the client to build up to
`MAX_EDIT_PREDICTION_REJECTIONS_PER_REQUEST`. We'll now attempt to flush
the rejections when we reach half max.

Release Notes:

- N/A
2025-12-01 21:10:13 +00:00
Danilo Leal
58c9cbae40 git_ui: Clean up file history view design (#43941)
Mostly cleaning up the UI code. The UI looks fairly the same just a bit
more polished, with proper colors and spacing. Also added a scrollbar in
there. Next step would be to make it keyboard navigable.

<img width="500" height="1948" alt="Screenshot 2025-12-01 at 5  38@2x"
src="https://github.com/user-attachments/assets/c266b97c-4a79-4a0e-8e78-2f1ed1ba495f"
/>

Release Notes:

- N/A
2025-12-01 20:56:34 +00:00
Finn Evers
7881551dda ci: Request GitHub token for proper repository (#43940)
Release Notes:

- N/A
2025-12-01 21:30:51 +01:00
Ben Kunkle
ff6bd7d82e sweep: Add UI for setting Sweep API token in system keychain (#43502)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-12-01 14:36:49 -05:00
Finn Evers
bfb876c782 Improve extension CI concurrency (#43935)
While this does work for PRs and such, it does not work with main...
Hence, moving the token a few chars to the right to fix this issue.

Release Notes:

- N/A
2025-12-01 19:29:20 +00:00
Joseph T. Lyons
64b432e4ac Update crash report template (#43934)
- Updates tone to match bug template
- Removes the "current vs expected" behavior section
- It doesn't feel very useful. Users expect Zed to not crash, regardless
of what they are doing.

Release Notes:

- N/A
2025-12-01 19:05:10 +00:00
Richard Feldman
33ecb0a68f Clarify how outlining works in read_file_tool description (#43929)
<img width="698" height="218" alt="Screenshot 2025-12-01 at 1 27 02 PM"
src="https://github.com/user-attachments/assets/a5d9e121-4e68-40d0-a346-4dd39e77233b"
/>

Closes #419

Release Notes:

- Revise tool call description for read file tool to explain outlining
behavior
2025-12-01 14:00:18 -05:00
Danilo Leal
7b7ddbd1e8 Adjust edit prediction upsell copy and animation (#43931)
Release Notes:

- N/A
2025-12-01 18:45:45 +00:00
Finn Evers
ed81ef0442 ci: Add extension workflow concurrency rules (#43930)
Release Notes:

- N/A
2025-12-01 18:42:46 +00:00
Finn Evers
88fffae9dd Fix extension CI workflow disclaimer (#43926)
Release Notes:

- N/A
2025-12-01 18:22:52 +00:00
76 changed files with 1811 additions and 486 deletions

View File

@@ -6,28 +6,18 @@ body:
- type: textarea
attributes:
label: Reproduction steps
description: A step-by-step description of how to reproduce the crash from a **clean Zed install**. **Be verbose**. **Issues with insufficient detail may be summarily closed**.
description: A step-by-step description of how to reproduce the crash from a **clean Zed install**. The more context you provide, the easier it is to find and fix the problem fast.
placeholder: |
1. Start Zed
2. Perform an action
3. Zed crashes
validations:
required: true
- type: textarea
attributes:
label: Current vs. Expected behavior
description: |
Go into depth about what actions youre performing in Zed to trigger the crash. If Zed crashes before it loads any windows, make sure to mention that. Again, **be verbose**.
**Skipping this/failure to provide complete information will result in the issue being closed.**
placeholder: "Based on my reproduction steps above, when I perform said action, I expect this to happen, but instead Zed crashes."
validations:
required: true
- type: textarea
attributes:
label: Zed version and system specs
description: |
Open the command palette in Zed, then type “zed: copy system specs into clipboard”. **Skipping this/failure to provide complete information will result in the issue being closed**.
Open the command palette in Zed, then type “zed: copy system specs into clipboard”.
placeholder: |
Zed: v0.215.0 (Zed Nightly bfe141ea79aa4984028934067ba75c48d99136ae)
OS: macOS 15.1
@@ -37,7 +27,7 @@ body:
required: true
- type: textarea
attributes:
label: If applicable, attach your Zed log file to this issue
label: Attach Zed log file
description: |
Open the command palette in Zed, then type `zed: open log` to see the last 1000 lines. Or type `zed: reveal log in file manager` in the command palette to reveal the log file itself.
value: |

View File

@@ -51,7 +51,7 @@ jobs:
mkdir -p /tmp/ext-output
./zed-extension --source-dir . --scratch-dir /tmp/ext-scratch --output-dir /tmp/ext-output
shell: bash -euxo pipefail {0}
timeout-minutes: 1
timeout-minutes: 2
check_bump_needed:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
runs-on: namespace-profile-2x4-ubuntu-2404
@@ -114,21 +114,18 @@ jobs:
run: |
OLD_VERSION="${{ needs.check_bump_needed.outputs.current_version }}"
cat <<EOF > .bumpversion.cfg
[bumpversion]
current_version = "$OLD_VERSION"
BUMP_FILES=("extension.toml")
if [[ -f "Cargo.toml" ]]; then
BUMP_FILES+=("Cargo.toml")
fi
[bumpversion:file:Cargo.toml]
bump2version --verbose --current-version "$OLD_VERSION" --no-configured-files ${{ inputs.bump-type }} "${BUMP_FILES[@]}"
[bumpversion:file:extension.toml]
if [[ -f "Cargo.toml" ]]; then
cargo update --workspace
fi
EOF
bump2version --verbose ${{ inputs.bump-type }}
NEW_VERSION="$(sed -n 's/version = \"\(.*\)\"/\1/p' < extension.toml)"
cargo update --workspace
rm .bumpversion.cfg
echo "new_version=${NEW_VERSION}" >> "$GITHUB_OUTPUT"
shell: bash -euxo pipefail {0}

View File

@@ -21,6 +21,8 @@ jobs:
with:
app-id: ${{ secrets.app-id }}
private-key: ${{ secrets.app-secret }}
owner: zed-industries
repositories: extensions
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:

View File

@@ -7,12 +7,7 @@ env:
CARGO_INCREMENTAL: '0'
ZED_EXTENSION_CLI_SHA: 7cfce605704d41ca247e3f84804bf323f6c6caaf
on:
workflow_call:
inputs:
run_tests:
description: Whether the workflow should run rust tests
required: true
type: boolean
workflow_call: {}
jobs:
orchestrate:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
@@ -73,12 +68,12 @@ jobs:
run: cargo clippy --release --all-targets --all-features -- --deny warnings
shell: bash -euxo pipefail {0}
- name: steps::cargo_install_nextest
if: inputs.run_tests
uses: taiki-e/install-action@nextest
- name: steps::cargo_nextest
if: inputs.run_tests
run: cargo nextest run --workspace --no-fail-fast
shell: bash -euxo pipefail {0}
env:
NEXTEST_NO_TESTS: warn
timeout-minutes: 3
check_extension:
needs:
@@ -108,7 +103,7 @@ jobs:
mkdir -p /tmp/ext-output
./zed-extension --source-dir . --scratch-dir /tmp/ext-scratch --output-dir /tmp/ext-output
shell: bash -euxo pipefail {0}
timeout-minutes: 1
timeout-minutes: 2
tests_pass:
needs:
- orchestrate

View File

@@ -520,6 +520,7 @@ jobs:
uses: bufbuild/buf-setup-action@v1
with:
version: v1.29.0
github_token: ${{ secrets.GITHUB_TOKEN }}
- name: run_tests::check_postgres_and_protobuf_migrations::bufbuild_breaking_action
uses: bufbuild/buf-breaking-action@v1
with:

4
Cargo.lock generated
View File

@@ -2423,6 +2423,7 @@ dependencies = [
"rand 0.9.2",
"rope",
"serde_json",
"settings",
"sum_tree",
"text",
"unindent",
@@ -5299,6 +5300,7 @@ dependencies = [
"indoc",
"language",
"lsp",
"menu",
"paths",
"project",
"regex",
@@ -5308,6 +5310,7 @@ dependencies = [
"telemetry",
"theme",
"ui",
"ui_input",
"util",
"workspace",
"zed_actions",
@@ -21678,6 +21681,7 @@ dependencies = [
"collections",
"command_palette_hooks",
"copilot",
"credentials_provider",
"ctor",
"db",
"edit_prediction",

View File

@@ -639,6 +639,7 @@ serde_urlencoded = "0.7"
sha2 = "0.10"
shellexpand = "2.1.0"
shlex = "1.3.0"
similar = "2.6"
simplelog = "0.12.2"
slotmap = "1.0.6"
smallvec = { version = "1.6", features = ["union"] }

View File

@@ -857,6 +857,8 @@
"ctrl-w shift-right": "workspace::SwapPaneRight",
"ctrl-w shift-up": "workspace::SwapPaneUp",
"ctrl-w shift-down": "workspace::SwapPaneDown",
"ctrl-w x": "workspace::SwapPaneAdjacent",
"ctrl-w ctrl-x": "workspace::SwapPaneAdjacent",
"ctrl-w shift-h": "workspace::MovePaneLeft",
"ctrl-w shift-l": "workspace::MovePaneRight",
"ctrl-w shift-k": "workspace::MovePaneUp",

View File

@@ -1209,6 +1209,13 @@
"tab_size": 4,
// What debuggers are preferred by default for all languages.
"debuggers": [],
// Whether to enable word diff highlighting in the editor.
//
// When enabled, changed words within modified lines are highlighted
// to show exactly what changed.
//
// Default: true
"word_diff_enabled": true,
// Control what info is collected by Zed.
"telemetry": {
// Send debug info like crash reports.

View File

@@ -98,6 +98,8 @@
"link_text.hover": "#74ade8ff",
"version_control.added": "#27a657ff",
"version_control.modified": "#d3b020ff",
"version_control.word_added": "#2EA04859",
"version_control.word_deleted": "#78081BCC",
"version_control.deleted": "#e06c76ff",
"version_control.conflict_marker.ours": "#a1c1811a",
"version_control.conflict_marker.theirs": "#74ade81a",
@@ -499,6 +501,8 @@
"link_text.hover": "#5c78e2ff",
"version_control.added": "#27a657ff",
"version_control.modified": "#d3b020ff",
"version_control.word_added": "#2EA04859",
"version_control.word_deleted": "#F85149CC",
"version_control.deleted": "#e06c76ff",
"conflict": "#a48819ff",
"conflict.background": "#faf2e6ff",

View File

@@ -66,11 +66,9 @@ pub async fn get_buffer_content_or_outline(
let outline_text = render_outline(outline_items, None, 0, usize::MAX).await?;
let text = if let Some(path) = path {
format!(
"# File outline for {path} (file too large to show full content)\n\n{outline_text}",
)
format!("# File outline for {path}\n\n{outline_text}",)
} else {
format!("# File outline (file too large to show full content)\n\n{outline_text}",)
format!("# File outline\n\n{outline_text}",)
};
Ok(BufferContent {
text,

View File

@@ -6,33 +6,6 @@ The text you output will be saved verbatim as the content of the file.
Tool calls have been disabled.
Start your response with ```.
IMPORTANT: Output ONLY the file content between the backticks. Do NOT include:
- The file path or name (like "path/to/file.txt")
- Any markdown formatting within the content
- Any explanatory text before or after the backticks
- Any nested code fences within your output
<example>
If asked to create a file with "hello" inside:
CORRECT output:
```
hello
```
INCORRECT output (includes file path):
```path/to/file.txt
hello
```
INCORRECT output (nested code fences):
```
```
hello
```
```
</example>
<file_path>
{{path}}
</file_path>

View File

@@ -44,16 +44,8 @@ pub struct EditFileToolInput {
///
/// NEVER mention the file path in this description.
///
/// IMPORTANT: Do NOT include markdown code fences (```) or other markdown formatting in this description.
/// Just describe what should be done - another model will generate the actual content.
///
/// <example>Fix API endpoint URLs</example>
/// <example>Update copyright year in `page_footer`</example>
/// <example>Create a Python script that prints hello world</example>
///
/// INCORRECT examples (do not do this):
/// <example>Create a file with:\n```python\nprint('hello')\n```</example>
/// <example>Add this code:\n```\nif err:\n return\n```</example>
///
/// Make sure to include this field before all the others in the input object so that we can display it immediately.
pub display_description: String,

View File

@@ -17,6 +17,9 @@ use crate::{AgentTool, Thread, ToolCallEventStream, outline};
/// Reads the content of the given file in the project.
///
/// - Never attempt to read a path that hasn't been previously mentioned.
/// - For large files, this tool returns a file outline with symbol names and line numbers instead of the full content.
/// This outline IS a successful response - use the line numbers to read specific sections with start_line/end_line.
/// Do NOT retry reading the same file without line numbers if you receive an outline.
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ReadFileToolInput {
/// The relative path of the file to read.
@@ -254,16 +257,15 @@ impl AgentTool for ReadFileTool {
if buffer_content.is_outline {
Ok(formatdoc! {"
This file was too big to read all at once.
SUCCESS: File outline retrieved. This file is too large to read all at once, so the outline below shows the file's structure with line numbers.
IMPORTANT: Do NOT retry this call without line numbers - you will get the same outline.
Instead, use the line numbers below to read specific sections by calling this tool again with start_line and end_line parameters.
{}
Using the line numbers in this outline, you can call this tool again
while specifying the start_line and end_line fields to see the
implementations of symbols in the outline.
Alternatively, you can fall back to the `grep` tool (if available)
to search the file for specific content.", buffer_content.text
NEXT STEPS: To read a specific symbol's implementation, call read_file with the same path plus start_line and end_line from the outline above.
For example, to read a function shown as [L100-150], use start_line: 100 and end_line: 150.", buffer_content.text
}
.into())
} else {
@@ -440,7 +442,7 @@ mod test {
let content = result.to_str().unwrap();
assert_eq!(
content.lines().skip(4).take(6).collect::<Vec<_>>(),
content.lines().skip(7).take(6).collect::<Vec<_>>(),
vec![
"struct Test0 [L1-4]",
" a [L2]",
@@ -475,7 +477,7 @@ mod test {
pretty_assertions::assert_eq!(
content
.lines()
.skip(4)
.skip(7)
.take(expected_content.len())
.collect::<Vec<_>>(),
expected_content

View File

@@ -12,7 +12,7 @@ workspace = true
path = "src/buffer_diff.rs"
[features]
test-support = []
test-support = ["settings"]
[dependencies]
anyhow.workspace = true
@@ -24,6 +24,7 @@ language.workspace = true
log.workspace = true
pretty_assertions.workspace = true
rope.workspace = true
settings = { workspace = true, optional = true }
sum_tree.workspace = true
text.workspace = true
util.workspace = true
@@ -33,6 +34,7 @@ ctor.workspace = true
gpui = { workspace = true, features = ["test-support"] }
rand.workspace = true
serde_json.workspace = true
settings.workspace = true
text = { workspace = true, features = ["test-support"] }
unindent.workspace = true
zlog.workspace = true

View File

@@ -1,7 +1,10 @@
use futures::channel::oneshot;
use git2::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch};
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Task, TaskLabel};
use language::{BufferRow, Language, LanguageRegistry};
use language::{
BufferRow, DiffOptions, File, Language, LanguageName, LanguageRegistry,
language_settings::language_settings, word_diff_ranges,
};
use rope::Rope;
use std::{
cmp::Ordering,
@@ -15,10 +18,12 @@ use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point, ToOffset as _, ToPoint
use util::ResultExt;
pub static CALCULATE_DIFF_TASK: LazyLock<TaskLabel> = LazyLock::new(TaskLabel::new);
pub const MAX_WORD_DIFF_LINE_COUNT: usize = 5;
pub struct BufferDiff {
pub buffer_id: BufferId,
inner: BufferDiffInner,
// diff of the index vs head
secondary_diff: Option<Entity<BufferDiff>>,
}
@@ -31,6 +36,7 @@ pub struct BufferDiffSnapshot {
#[derive(Clone)]
struct BufferDiffInner {
hunks: SumTree<InternalDiffHunk>,
// Used for making staging mo
pending_hunks: SumTree<PendingHunk>,
base_text: language::BufferSnapshot,
base_text_exists: bool,
@@ -50,11 +56,18 @@ pub enum DiffHunkStatusKind {
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
/// Diff of Working Copy vs Index
/// aka 'is this hunk staged or not'
pub enum DiffHunkSecondaryStatus {
/// Unstaged
HasSecondaryHunk,
/// Partially staged
OverlapsWithSecondaryHunk,
/// Staged
NoSecondaryHunk,
/// We are unstaging
SecondaryHunkAdditionPending,
/// We are stagind
SecondaryHunkRemovalPending,
}
@@ -68,6 +81,10 @@ pub struct DiffHunk {
/// The range in the buffer's diff base text to which this hunk corresponds.
pub diff_base_byte_range: Range<usize>,
pub secondary_status: DiffHunkSecondaryStatus,
// Anchors representing the word diff locations in the active buffer
pub buffer_word_diffs: Vec<Range<Anchor>>,
// Offsets relative to the start of the deleted diff that represent word diff locations
pub base_word_diffs: Vec<Range<usize>>,
}
/// We store [`InternalDiffHunk`]s internally so we don't need to store the additional row range.
@@ -75,6 +92,8 @@ pub struct DiffHunk {
struct InternalDiffHunk {
buffer_range: Range<Anchor>,
diff_base_byte_range: Range<usize>,
base_word_diffs: Vec<Range<usize>>,
buffer_word_diffs: Vec<Range<Anchor>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -208,6 +227,13 @@ impl BufferDiffSnapshot {
let base_text_pair;
let base_text_exists;
let base_text_snapshot;
let diff_options = build_diff_options(
None,
language.as_ref().map(|l| l.name()),
language.as_ref().map(|l| l.default_scope()),
cx,
);
if let Some(text) = &base_text {
let base_text_rope = Rope::from(text.as_str());
base_text_pair = Some((text.clone(), base_text_rope.clone()));
@@ -225,7 +251,7 @@ impl BufferDiffSnapshot {
.background_executor()
.spawn_labeled(*CALCULATE_DIFF_TASK, {
let buffer = buffer.clone();
async move { compute_hunks(base_text_pair, buffer) }
async move { compute_hunks(base_text_pair, buffer, diff_options) }
});
async move {
@@ -248,6 +274,12 @@ impl BufferDiffSnapshot {
base_text_snapshot: language::BufferSnapshot,
cx: &App,
) -> impl Future<Output = Self> + use<> {
let diff_options = build_diff_options(
base_text_snapshot.file(),
base_text_snapshot.language().map(|l| l.name()),
base_text_snapshot.language().map(|l| l.default_scope()),
cx,
);
let base_text_exists = base_text.is_some();
let base_text_pair = base_text.map(|text| {
debug_assert_eq!(&*text, &base_text_snapshot.text());
@@ -259,7 +291,7 @@ impl BufferDiffSnapshot {
inner: BufferDiffInner {
base_text: base_text_snapshot,
pending_hunks: SumTree::new(&buffer),
hunks: compute_hunks(base_text_pair, buffer),
hunks: compute_hunks(base_text_pair, buffer, diff_options),
base_text_exists,
},
secondary_diff: None,
@@ -602,11 +634,15 @@ impl BufferDiffInner {
[
(
&hunk.buffer_range.start,
(hunk.buffer_range.start, hunk.diff_base_byte_range.start),
(
hunk.buffer_range.start,
hunk.diff_base_byte_range.start,
hunk,
),
),
(
&hunk.buffer_range.end,
(hunk.buffer_range.end, hunk.diff_base_byte_range.end),
(hunk.buffer_range.end, hunk.diff_base_byte_range.end, hunk),
),
]
});
@@ -625,8 +661,11 @@ impl BufferDiffInner {
let mut summaries = buffer.summaries_for_anchors_with_payload::<Point, _, _>(anchor_iter);
iter::from_fn(move || {
loop {
let (start_point, (start_anchor, start_base)) = summaries.next()?;
let (mut end_point, (mut end_anchor, end_base)) = summaries.next()?;
let (start_point, (start_anchor, start_base, hunk)) = summaries.next()?;
let (mut end_point, (mut end_anchor, end_base, _)) = summaries.next()?;
let base_word_diffs = hunk.base_word_diffs.clone();
let buffer_word_diffs = hunk.buffer_word_diffs.clone();
if !start_anchor.is_valid(buffer) {
continue;
@@ -696,6 +735,8 @@ impl BufferDiffInner {
range: start_point..end_point,
diff_base_byte_range: start_base..end_base,
buffer_range: start_anchor..end_anchor,
base_word_diffs,
buffer_word_diffs,
secondary_status,
});
}
@@ -727,6 +768,8 @@ impl BufferDiffInner {
buffer_range: hunk.buffer_range.clone(),
// The secondary status is not used by callers of this method.
secondary_status: DiffHunkSecondaryStatus::NoSecondaryHunk,
base_word_diffs: hunk.base_word_diffs.clone(),
buffer_word_diffs: hunk.buffer_word_diffs.clone(),
})
})
}
@@ -795,9 +838,36 @@ impl BufferDiffInner {
}
}
fn build_diff_options(
file: Option<&Arc<dyn File>>,
language: Option<LanguageName>,
language_scope: Option<language::LanguageScope>,
cx: &App,
) -> Option<DiffOptions> {
#[cfg(any(test, feature = "test-support"))]
{
if !cx.has_global::<settings::SettingsStore>() {
return Some(DiffOptions {
language_scope,
max_word_diff_line_count: MAX_WORD_DIFF_LINE_COUNT,
..Default::default()
});
}
}
language_settings(language, file, cx)
.word_diff_enabled
.then_some(DiffOptions {
language_scope,
max_word_diff_line_count: MAX_WORD_DIFF_LINE_COUNT,
..Default::default()
})
}
fn compute_hunks(
diff_base: Option<(Arc<String>, Rope)>,
buffer: text::BufferSnapshot,
diff_options: Option<DiffOptions>,
) -> SumTree<InternalDiffHunk> {
let mut tree = SumTree::new(&buffer);
@@ -823,6 +893,8 @@ fn compute_hunks(
InternalDiffHunk {
buffer_range: buffer.anchor_before(0)..buffer.anchor_before(0),
diff_base_byte_range: 0..diff_base.len() - 1,
base_word_diffs: Vec::default(),
buffer_word_diffs: Vec::default(),
},
&buffer,
);
@@ -838,6 +910,7 @@ fn compute_hunks(
&diff_base_rope,
&buffer,
&mut divergence,
diff_options.as_ref(),
);
tree.push(hunk, &buffer);
}
@@ -847,6 +920,8 @@ fn compute_hunks(
InternalDiffHunk {
buffer_range: Anchor::min_max_range_for_buffer(buffer.remote_id()),
diff_base_byte_range: 0..0,
base_word_diffs: Vec::default(),
buffer_word_diffs: Vec::default(),
},
&buffer,
);
@@ -861,6 +936,7 @@ fn process_patch_hunk(
diff_base: &Rope,
buffer: &text::BufferSnapshot,
buffer_row_divergence: &mut i64,
diff_options: Option<&DiffOptions>,
) -> InternalDiffHunk {
let line_item_count = patch.num_lines_in_hunk(hunk_index).unwrap();
assert!(line_item_count > 0);
@@ -925,9 +1001,49 @@ fn process_patch_hunk(
let start = Point::new(buffer_row_range.start, 0);
let end = Point::new(buffer_row_range.end, 0);
let buffer_range = buffer.anchor_before(start)..buffer.anchor_before(end);
let base_line_count = line_item_count.saturating_sub(buffer_row_range.len());
let (base_word_diffs, buffer_word_diffs) = if let Some(diff_options) = diff_options
&& !buffer_row_range.is_empty()
&& base_line_count == buffer_row_range.len()
&& diff_options.max_word_diff_line_count >= base_line_count
{
let base_text: String = diff_base
.chunks_in_range(diff_base_byte_range.clone())
.collect();
let buffer_text: String = buffer.text_for_range(buffer_range.clone()).collect();
let (base_word_diffs, buffer_word_diffs_relative) = word_diff_ranges(
&base_text,
&buffer_text,
DiffOptions {
language_scope: diff_options.language_scope.clone(),
..*diff_options
},
);
let buffer_start_offset = buffer_range.start.to_offset(buffer);
let buffer_word_diffs = buffer_word_diffs_relative
.into_iter()
.map(|range| {
let start = buffer.anchor_after(buffer_start_offset + range.start);
let end = buffer.anchor_after(buffer_start_offset + range.end);
start..end
})
.collect();
(base_word_diffs, buffer_word_diffs)
} else {
(Vec::default(), Vec::default())
};
InternalDiffHunk {
buffer_range,
diff_base_byte_range,
base_word_diffs,
buffer_word_diffs,
}
}

View File

@@ -32,6 +32,8 @@ settings.workspace = true
supermaven.workspace = true
telemetry.workspace = true
ui.workspace = true
ui_input.workspace = true
menu.workspace = true
util.workspace = true
workspace.workspace = true
zed_actions.workspace = true

View File

@@ -1,3 +1,7 @@
mod sweep_api_token_modal;
pub use sweep_api_token_modal::SweepApiKeyModal;
use anyhow::Result;
use client::{Client, UserStore, zed_urls};
use cloud_llm_client::UsageLimit;
@@ -40,8 +44,7 @@ use workspace::{
notifications::NotificationId,
};
use zed_actions::OpenBrowser;
use zeta::RateCompletions;
use zeta::{SweepFeatureFlag, Zeta2FeatureFlag};
use zeta::{RateCompletions, SweepFeatureFlag, Zeta2FeatureFlag};
actions!(
edit_prediction,
@@ -313,6 +316,10 @@ impl Render for EditPredictionButton {
)
);
let sweep_missing_token = is_sweep
&& !zeta::Zeta::try_global(cx)
.map_or(false, |zeta| zeta.read(cx).has_sweep_api_token());
let zeta_icon = match (is_sweep, enabled) {
(true, _) => IconName::SweepAi,
(false, true) => IconName::ZedPredict,
@@ -360,19 +367,24 @@ impl Render for EditPredictionButton {
let show_editor_predictions = self.editor_show_predictions;
let user = self.user_store.read(cx).current_user();
let indicator_color = if sweep_missing_token {
Some(Color::Error)
} else if enabled && (!show_editor_predictions || over_limit) {
Some(if over_limit {
Color::Error
} else {
Color::Muted
})
} else {
None
};
let icon_button = IconButton::new("zed-predict-pending-button", zeta_icon)
.shape(IconButtonShape::Square)
.when(
enabled && (!show_editor_predictions || over_limit),
|this| {
this.indicator(Indicator::dot().when_else(
over_limit,
|dot| dot.color(Color::Error),
|dot| dot.color(Color::Muted),
))
.when_some(indicator_color, |this, color| {
this.indicator(Indicator::dot().color(color))
.indicator_border_color(Some(cx.theme().colors().status_bar_background))
},
)
})
.when(!self.popover_menu_handle.is_deployed(), |element| {
let user = user.clone();
element.tooltip(move |_window, cx| {
@@ -537,23 +549,23 @@ impl EditPredictionButton {
const ZED_AI_CALLOUT: &str =
"Zed's edit prediction is powered by Zeta, an open-source, dataset mode.";
const USE_SWEEP_API_TOKEN_CALLOUT: &str =
"Set the SWEEP_API_TOKEN environment variable to use Sweep";
let other_providers: Vec<_> = available_providers
let providers: Vec<_> = available_providers
.into_iter()
.filter(|p| *p != current_provider && *p != EditPredictionProvider::None)
.filter(|p| *p != EditPredictionProvider::None)
.collect();
if !other_providers.is_empty() {
menu = menu.separator().header("Switch Providers");
if !providers.is_empty() {
menu = menu.separator().header("Providers");
for provider in other_providers {
for provider in providers {
let is_current = provider == current_provider;
let fs = self.fs.clone();
menu = match provider {
EditPredictionProvider::Zed => menu.item(
ContextMenuEntry::new("Zed AI")
.toggleable(IconPosition::Start, is_current)
.documentation_aside(
DocumentationSide::Left,
DocumentationEdge::Bottom,
@@ -563,46 +575,77 @@ impl EditPredictionButton {
set_completion_provider(fs.clone(), cx, provider);
}),
),
EditPredictionProvider::Copilot => {
menu.entry("GitHub Copilot", None, move |_, cx| {
set_completion_provider(fs.clone(), cx, provider);
})
}
EditPredictionProvider::Supermaven => {
menu.entry("Supermaven", None, move |_, cx| {
set_completion_provider(fs.clone(), cx, provider);
})
}
EditPredictionProvider::Codestral => {
menu.entry("Codestral", None, move |_, cx| {
set_completion_provider(fs.clone(), cx, provider);
})
}
EditPredictionProvider::Copilot => menu.item(
ContextMenuEntry::new("GitHub Copilot")
.toggleable(IconPosition::Start, is_current)
.handler(move |_, cx| {
set_completion_provider(fs.clone(), cx, provider);
}),
),
EditPredictionProvider::Supermaven => menu.item(
ContextMenuEntry::new("Supermaven")
.toggleable(IconPosition::Start, is_current)
.handler(move |_, cx| {
set_completion_provider(fs.clone(), cx, provider);
}),
),
EditPredictionProvider::Codestral => menu.item(
ContextMenuEntry::new("Codestral")
.toggleable(IconPosition::Start, is_current)
.handler(move |_, cx| {
set_completion_provider(fs.clone(), cx, provider);
}),
),
EditPredictionProvider::Experimental(
EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME,
) => {
let has_api_token = zeta::Zeta::try_global(cx)
.map_or(false, |zeta| zeta.read(cx).has_sweep_api_token());
let entry = ContextMenuEntry::new("Sweep")
.when(!has_api_token, |this| {
this.disabled(true).documentation_aside(
let should_open_modal = !has_api_token || is_current;
let entry = if has_api_token {
ContextMenuEntry::new("Sweep")
.toggleable(IconPosition::Start, is_current)
} else {
ContextMenuEntry::new("Sweep")
.icon(IconName::XCircle)
.icon_color(Color::Error)
.documentation_aside(
DocumentationSide::Left,
DocumentationEdge::Bottom,
|_| Label::new(USE_SWEEP_API_TOKEN_CALLOUT).into_any_element(),
|_| {
Label::new("Click to configure your Sweep API token")
.into_any_element()
},
)
})
.handler(move |_, cx| {
};
let entry = entry.handler(move |window, cx| {
if should_open_modal {
if let Some(workspace) = window.root::<Workspace>().flatten() {
workspace.update(cx, |workspace, cx| {
workspace.toggle_modal(window, cx, |window, cx| {
SweepApiKeyModal::new(window, cx)
});
});
};
} else {
set_completion_provider(fs.clone(), cx, provider);
});
}
});
menu.item(entry)
}
EditPredictionProvider::Experimental(
EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME,
) => menu.entry("Zeta2", None, move |_, cx| {
set_completion_provider(fs.clone(), cx, provider);
}),
) => menu.item(
ContextMenuEntry::new("Zeta2")
.toggleable(IconPosition::Start, is_current)
.handler(move |_, cx| {
set_completion_provider(fs.clone(), cx, provider);
}),
),
EditPredictionProvider::None | EditPredictionProvider::Experimental(_) => {
continue;
}
@@ -1078,8 +1121,8 @@ impl EditPredictionButton {
menu = menu
.custom_row(move |_window, cx| {
let description = indoc! {
"Sign in for 2,000 worth of accepted suggestions at every keystroke, \
powered by Zeta, our open-source, open-data model."
"You get 2,000 accepted suggestions at every keystroke for free, \
powered by Zeta, our open-source, open-data model"
};
v_flex()
@@ -1332,21 +1375,28 @@ fn render_zeta_tab_animation(cx: &App) -> impl IntoElement {
.child("tab")
.with_animation(
ElementId::Integer(n),
Animation::new(Duration::from_secs(4)).repeat(),
Animation::new(Duration::from_secs(3)).repeat(),
move |tab, delta| {
let n_f32 = n as f32;
let delta = if inverted {
(delta - 0.15 * (5.0 - n_f32)) / 0.7
let offset = if inverted {
0.2 * (4.0 - n_f32)
} else {
(delta - 0.15 * n_f32) / 0.7
0.2 * n_f32
};
let delta = 1.0 - (0.5 - delta).abs() * 2.;
let delta = ease_in_out(delta.clamp(0., 1.));
let delta = 0.1 + 0.5 * delta;
let phase = (delta - offset + 1.0) % 1.0;
let pulse = if phase < 0.6 {
let t = phase / 0.6;
1.0 - (0.5 - t).abs() * 2.0
} else {
0.0
};
tab.text_color(text_color.opacity(delta))
let eased = ease_in_out(pulse);
let opacity = 0.1 + 0.5 * eased;
tab.text_color(text_color.opacity(opacity))
},
),
)

View File

@@ -0,0 +1,84 @@
use gpui::{
DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render,
};
use ui::{Button, ButtonStyle, Clickable, Headline, HeadlineSize, prelude::*};
use ui_input::InputField;
use workspace::ModalView;
use zeta::Zeta;
pub struct SweepApiKeyModal {
api_key_input: Entity<InputField>,
focus_handle: FocusHandle,
}
impl SweepApiKeyModal {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let api_key_input = cx.new(|cx| InputField::new(window, cx, "Enter your Sweep API token"));
Self {
api_key_input,
focus_handle: cx.focus_handle(),
}
}
fn cancel(&mut self, _: &menu::Cancel, _window: &mut Window, cx: &mut Context<Self>) {
cx.emit(DismissEvent);
}
fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
let api_key = self.api_key_input.read(cx).text(cx);
let api_key = (!api_key.trim().is_empty()).then_some(api_key);
if let Some(zeta) = Zeta::try_global(cx) {
zeta.update(cx, |zeta, cx| {
zeta.sweep_ai
.set_api_token(api_key, cx)
.detach_and_log_err(cx);
});
}
cx.emit(DismissEvent);
}
}
impl EventEmitter<DismissEvent> for SweepApiKeyModal {}
impl ModalView for SweepApiKeyModal {}
impl Focusable for SweepApiKeyModal {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for SweepApiKeyModal {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.key_context("SweepApiKeyModal")
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::confirm))
.elevation_2(cx)
.w(px(400.))
.p_4()
.gap_3()
.child(Headline::new("Sweep API Token").size(HeadlineSize::Small))
.child(self.api_key_input.clone())
.child(
h_flex()
.justify_end()
.gap_2()
.child(Button::new("cancel", "Cancel").on_click(cx.listener(
|_, _, _window, cx| {
cx.emit(DismissEvent);
},
)))
.child(
Button::new("save", "Save")
.style(ButtonStyle::Filled)
.on_click(cx.listener(|this, _, window, cx| {
this.confirm(&menu::Confirm, window, cx);
})),
),
)
}
}

View File

@@ -43,7 +43,7 @@ impl Editor {
.collect_array()
};
let bracket_matches_by_accent = self.visible_excerpts(cx).into_iter().fold(
let bracket_matches_by_accent = self.visible_excerpts(false, cx).into_iter().fold(
HashMap::default(),
|mut acc, (excerpt_id, (buffer, buffer_version, buffer_range))| {
let buffer_snapshot = buffer.read(cx).snapshot();
@@ -164,7 +164,7 @@ mod tests {
use super::*;
use crate::{
DisplayPoint, EditorSnapshot, MoveToBeginning, MoveToEnd, MoveUp,
DisplayPoint, EditorMode, EditorSnapshot, MoveToBeginning, MoveToEnd, MoveUp,
display_map::{DisplayRow, ToDisplayPoint},
editor_tests::init_test,
test::{
@@ -276,6 +276,40 @@ where
);
}
#[gpui::test]
async fn test_file_less_file_colorization(cx: &mut gpui::TestAppContext) {
init_test(cx, |language_settings| {
language_settings.defaults.colorize_brackets = Some(true);
});
let editor = cx.add_window(|window, cx| {
let multi_buffer = MultiBuffer::build_simple("fn main() {}", cx);
multi_buffer.update(cx, |multi_buffer, cx| {
multi_buffer
.as_singleton()
.unwrap()
.update(cx, |buffer, cx| {
buffer.set_language(Some(rust_lang()), cx);
});
});
Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
});
cx.executor().advance_clock(Duration::from_millis(100));
cx.executor().run_until_parked();
assert_eq!(
"fn main«1()1» «1{}1»
1 hsla(207.80, 16.20%, 69.19%, 1.00)
",
editor
.update(cx, |editor, window, cx| {
editor_bracket_colors_markup(&editor.snapshot(window, cx))
})
.unwrap(),
"File-less buffer should still have its brackets colorized"
);
}
#[gpui::test]
async fn test_markdown_bracket_colorization(cx: &mut gpui::TestAppContext) {
init_test(cx, |language_settings| {

View File

@@ -181,6 +181,8 @@ impl DisplayMap {
.update(cx, |map, cx| map.sync(tab_snapshot, edits, cx));
let block_snapshot = self.block_map.read(wrap_snapshot, edits).snapshot;
// todo word diff here?
DisplaySnapshot {
block_snapshot,
diagnostics_max_severity: self.diagnostics_max_severity,

View File

@@ -284,6 +284,9 @@ pub enum ConflictsTheirs {}
pub enum ConflictsOursMarker {}
pub enum ConflictsTheirsMarker {}
pub struct HunkAddedColor;
pub struct HunkRemovedColor;
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Navigated {
Yes,
@@ -307,6 +310,7 @@ enum DisplayDiffHunk {
display_row_range: Range<DisplayRow>,
multi_buffer_range: Range<Anchor>,
status: DiffHunkStatus,
word_diffs: Vec<Range<MultiBufferOffset>>,
},
}
@@ -5308,12 +5312,10 @@ impl Editor {
pub fn visible_excerpts(
&self,
lsp_related_only: bool,
cx: &mut Context<Editor>,
) -> HashMap<ExcerptId, (Entity<Buffer>, clock::Global, Range<usize>)> {
let Some(project) = self.project() else {
return HashMap::default();
};
let project = project.read(cx);
let project = self.project().cloned();
let multi_buffer = self.buffer().read(cx);
let multi_buffer_snapshot = multi_buffer.snapshot(cx);
let multi_buffer_visible_start = self
@@ -5331,6 +5333,18 @@ impl Editor {
.into_iter()
.filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty())
.filter_map(|(buffer, excerpt_visible_range, excerpt_id)| {
if !lsp_related_only {
return Some((
excerpt_id,
(
multi_buffer.buffer(buffer.remote_id()).unwrap(),
buffer.version().clone(),
excerpt_visible_range.start.0..excerpt_visible_range.end.0,
),
));
}
let project = project.as_ref()?.read(cx);
let buffer_file = project::File::from_dyn(buffer.file())?;
let buffer_worktree = project.worktree_for_id(buffer_file.worktree_id(cx), cx)?;
let worktree_entry = buffer_worktree
@@ -19447,6 +19461,10 @@ impl Editor {
&hunks
.map(|hunk| buffer_diff::DiffHunk {
buffer_range: hunk.buffer_range,
// We don't need to pass in word diffs here because they're only used for rendering and
// this function changes internal state
base_word_diffs: Vec::default(),
buffer_word_diffs: Vec::default(),
diff_base_byte_range: hunk.diff_base_byte_range.start.0
..hunk.diff_base_byte_range.end.0,
secondary_status: hunk.secondary_status,
@@ -22592,7 +22610,7 @@ impl Editor {
if self.ignore_lsp_data() {
return;
}
for (_, (visible_buffer, _, _)) in self.visible_excerpts(cx) {
for (_, (visible_buffer, _, _)) in self.visible_excerpts(true, cx) {
self.register_buffer(visible_buffer.read(cx).remote_id(), cx);
}
}
@@ -24116,10 +24134,12 @@ impl EditorSnapshot {
end_row.0 += 1;
}
let is_created_file = hunk.is_created_file();
DisplayDiffHunk::Unfolded {
status: hunk.status(),
diff_base_byte_range: hunk.diff_base_byte_range.start.0
..hunk.diff_base_byte_range.end.0,
word_diffs: hunk.word_diffs,
display_row_range: hunk_display_start.row()..end_row,
multi_buffer_range: Anchor::range_in_buffer(
hunk.excerpt_id,

View File

@@ -5572,6 +5572,50 @@ impl EditorElement {
}
}
fn layout_word_diff_highlights(
display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
row_infos: &[RowInfo],
start_row: DisplayRow,
snapshot: &EditorSnapshot,
highlighted_ranges: &mut Vec<(Range<DisplayPoint>, Hsla)>,
cx: &mut App,
) {
let colors = cx.theme().colors();
let word_highlights = display_hunks
.into_iter()
.filter_map(|(hunk, _)| match hunk {
DisplayDiffHunk::Unfolded {
word_diffs, status, ..
} => Some((word_diffs, status)),
_ => None,
})
.filter(|(_, status)| status.is_modified())
.flat_map(|(word_diffs, _)| word_diffs)
.filter_map(|word_diff| {
let start_point = word_diff.start.to_display_point(&snapshot.display_snapshot);
let end_point = word_diff.end.to_display_point(&snapshot.display_snapshot);
let start_row_offset = start_point.row().0.saturating_sub(start_row.0) as usize;
row_infos
.get(start_row_offset)
.and_then(|row_info| row_info.diff_status)
.and_then(|diff_status| {
let background_color = match diff_status.kind {
DiffHunkStatusKind::Added => colors.version_control_word_added,
DiffHunkStatusKind::Deleted => colors.version_control_word_deleted,
DiffHunkStatusKind::Modified => {
debug_panic!("modified diff status for row info");
return None;
}
};
Some((start_point..end_point, background_color))
})
});
highlighted_ranges.extend(word_highlights);
}
fn layout_diff_hunk_controls(
&self,
row_range: Range<DisplayRow>,
@@ -9122,7 +9166,7 @@ impl Element for EditorElement {
);
let end_row = DisplayRow(end_row);
let row_infos = snapshot
let row_infos = snapshot // note we only get the visual range
.row_infos(start_row)
.take((start_row..end_row).len())
.collect::<Vec<RowInfo>>();
@@ -9153,16 +9197,27 @@ impl Element for EditorElement {
let is_light = cx.theme().appearance().is_light();
let mut highlighted_ranges = self
.editor_with_selections(cx)
.map(|editor| {
editor.read(cx).background_highlights_in_range(
start_anchor..end_anchor,
&snapshot.display_snapshot,
cx.theme(),
)
})
.unwrap_or_default();
for (ix, row_info) in row_infos.iter().enumerate() {
let Some(diff_status) = row_info.diff_status else {
continue;
};
let background_color = match diff_status.kind {
DiffHunkStatusKind::Added => cx.theme().colors().version_control_added,
DiffHunkStatusKind::Deleted => {
cx.theme().colors().version_control_deleted
}
DiffHunkStatusKind::Added =>
cx.theme().colors().version_control_added,
DiffHunkStatusKind::Deleted =>
cx.theme().colors().version_control_deleted,
DiffHunkStatusKind::Modified => {
debug_panic!("modified diff status for row info");
continue;
@@ -9200,21 +9255,14 @@ impl Element for EditorElement {
filled_highlight
};
let base_display_point =
DisplayPoint::new(start_row + DisplayRow(ix as u32), 0);
highlighted_rows
.entry(start_row + DisplayRow(ix as u32))
.entry(base_display_point.row())
.or_insert(background);
}
let highlighted_ranges = self
.editor_with_selections(cx)
.map(|editor| {
editor.read(cx).background_highlights_in_range(
start_anchor..end_anchor,
&snapshot.display_snapshot,
cx.theme(),
)
})
.unwrap_or_default();
let highlighted_gutter_ranges =
self.editor.read(cx).gutter_highlights_in_range(
start_anchor..end_anchor,
@@ -9387,7 +9435,7 @@ impl Element for EditorElement {
let crease_trailers =
window.with_element_namespace("crease_trailers", |window| {
self.layout_crease_trailers(
row_infos.iter().copied(),
row_infos.iter().cloned(),
&snapshot,
window,
cx,
@@ -9403,6 +9451,15 @@ impl Element for EditorElement {
cx,
);
Self::layout_word_diff_highlights(
&display_hunks,
&row_infos,
start_row,
&snapshot,
&mut highlighted_ranges,
cx,
);
let merged_highlighted_ranges =
if let Some((_, colors)) = document_colors.as_ref() {
&highlighted_ranges

View File

@@ -291,7 +291,7 @@ impl Editor {
}),
};
let mut visible_excerpts = self.visible_excerpts(cx);
let mut visible_excerpts = self.visible_excerpts(true, cx);
let mut invalidate_hints_for_buffers = HashSet::default();
let ignore_previous_fetches = match reason {
InlayHintRefreshReason::ModifiersChanged(_)
@@ -2211,7 +2211,7 @@ pub mod tests {
cx: &mut gpui::TestAppContext,
) -> Range<Point> {
let ranges = editor
.update(cx, |editor, _window, cx| editor.visible_excerpts(cx))
.update(cx, |editor, _window, cx| editor.visible_excerpts(true, cx))
.unwrap();
assert_eq!(
ranges.len(),

View File

@@ -164,7 +164,7 @@ impl Editor {
}
let visible_buffers = self
.visible_excerpts(cx)
.visible_excerpts(true, cx)
.into_values()
.map(|(buffer, ..)| buffer)
.filter(|editor_buffer| {

View File

@@ -243,20 +243,25 @@ impl Render for SplittableEditor {
window: &mut ui::Window,
cx: &mut ui::Context<Self>,
) -> impl ui::IntoElement {
let Some(active) = self.panes.panes().into_iter().next() else {
return div().into_any_element();
let inner = if self.secondary.is_none() {
self.primary_editor.clone().into_any_element()
} else if let Some(active) = self.panes.panes().into_iter().next() {
self.panes
.render(
None,
&ActivePaneDecorator::new(active, &self.workspace),
window,
cx,
)
.into_any_element()
} else {
div().into_any_element()
};
div()
.id("splittable-editor")
.on_action(cx.listener(Self::split))
.on_action(cx.listener(Self::unsplit))
.size_full()
.child(self.panes.render(
None,
&ActivePaneDecorator::new(active, &self.workspace),
window,
cx,
))
.into_any_element()
.child(inner)
}
}

View File

@@ -4,8 +4,7 @@ use git::repository::{FileHistory, FileHistoryEntry, RepoPath};
use git::{GitHostingProviderRegistry, GitRemote, parse_git_remote_url};
use gpui::{
AnyElement, AnyEntity, App, Asset, Context, Entity, EventEmitter, FocusHandle, Focusable,
IntoElement, ListSizingBehavior, Render, Task, UniformListScrollHandle, WeakEntity, Window,
actions, rems, uniform_list,
IntoElement, Render, Task, UniformListScrollHandle, WeakEntity, Window, actions, uniform_list,
};
use project::{
Project, ProjectPath,
@@ -14,11 +13,8 @@ use project::{
use std::any::{Any, TypeId};
use time::OffsetDateTime;
use ui::{
Avatar, Button, ButtonStyle, Color, Icon, IconName, IconSize, Label, LabelCommon as _,
LabelSize, SharedString, prelude::*,
};
use util::{ResultExt, truncate_and_trailoff};
use ui::{Avatar, Chip, Divider, ListItem, WithScrollbar, prelude::*};
use util::ResultExt;
use workspace::{
Item, Workspace,
item::{ItemEvent, SaveOptions},
@@ -195,38 +191,24 @@ impl FileHistoryView {
task.detach();
}
fn list_item_height(&self) -> Rems {
rems(2.0)
}
fn fallback_commit_avatar() -> AnyElement {
Icon::new(IconName::Person)
.color(Color::Muted)
.size(IconSize::Small)
.into_element()
.into_any()
}
fn render_commit_avatar(
&self,
sha: &SharedString,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
) -> impl IntoElement {
let remote = self.remote.as_ref().filter(|r| r.host_supports_avatars());
let size = rems_from_px(20.);
if let Some(remote) = remote {
let avatar_asset = CommitAvatarAsset::new(remote.clone(), sha.clone());
if let Some(Some(url)) = window.use_asset::<CommitAvatarAsset>(&avatar_asset, cx) {
Avatar::new(url.to_string())
.size(rems(1.25))
.into_element()
.into_any()
Avatar::new(url.to_string()).size(size)
} else {
Self::fallback_commit_avatar()
Avatar::new("").size(size)
}
} else {
Self::fallback_commit_avatar()
Avatar::new("").size(size)
}
}
@@ -263,34 +245,52 @@ impl FileHistoryView {
time_format::TimestampFormat::Relative,
);
let selected = self.selected_entry == Some(ix);
let sha = entry.sha.clone();
let repo = self.repository.clone();
let workspace = self.workspace.clone();
let file_path = self.history.path.clone();
let base_bg = if selected {
cx.theme().status().info.alpha(0.1)
} else {
cx.theme().colors().editor_background
};
let hover_bg = if selected {
cx.theme().status().info.alpha(0.15)
} else {
cx.theme().colors().element_hover
};
h_flex()
.id(("commit", ix))
.h(self.list_item_height())
.w_full()
.items_center()
.px(rems(0.75))
.gap_2()
.bg(base_bg)
.hover(|style| style.bg(hover_bg))
.cursor_pointer()
ListItem::new(("commit", ix))
.child(
h_flex()
.h_8()
.w_full()
.pl_0p5()
.pr_2p5()
.gap_2()
.child(
div()
.w(rems_from_px(52.))
.flex_none()
.child(Chip::new(pr_number)),
)
.child(self.render_commit_avatar(&entry.sha, window, cx))
.child(
h_flex()
.w_full()
.justify_between()
.child(
h_flex()
.gap_1()
.child(
Label::new(entry.author_name.clone())
.size(LabelSize::Small)
.color(Color::Default),
)
.child(
Label::new(&entry.subject)
.size(LabelSize::Small)
.color(Color::Muted)
.truncate(),
),
)
.child(
Label::new(relative_timestamp)
.size(LabelSize::Small)
.color(Color::Muted),
),
),
)
.on_click(cx.listener(move |this, _, window, cx| {
this.selected_entry = Some(ix);
cx.notify();
@@ -308,56 +308,6 @@ impl FileHistoryView {
);
}
}))
.child(
div().flex_none().min_w(rems(4.0)).child(
div()
.px(rems(0.5))
.py(rems(0.25))
.rounded_md()
.bg(cx.theme().colors().element_background)
.border_1()
.border_color(cx.theme().colors().border)
.child(
Label::new(pr_number)
.size(LabelSize::Small)
.color(Color::Muted)
.single_line(),
),
),
)
.child(
div()
.flex_none()
.w(rems(1.75))
.child(self.render_commit_avatar(&entry.sha, window, cx)),
)
.child(
div().flex_1().overflow_hidden().child(
h_flex()
.gap_3()
.items_center()
.child(
Label::new(entry.author_name.clone())
.size(LabelSize::Small)
.color(Color::Default)
.single_line(),
)
.child(
Label::new(truncate_and_trailoff(&entry.subject, 100))
.size(LabelSize::Small)
.color(Color::Muted)
.single_line(),
),
),
)
.child(
div().flex_none().child(
Label::new(relative_timestamp)
.size(LabelSize::Small)
.color(Color::Muted)
.single_line(),
),
)
.into_any_element()
}
}
@@ -419,31 +369,49 @@ impl Focusable for FileHistoryView {
}
impl Render for FileHistoryView {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let _file_name = self.history.path.file_name().unwrap_or("File");
let entry_count = self.history.entries.len();
v_flex()
.size_full()
.bg(cx.theme().colors().editor_background)
.child(
h_flex()
.px(rems(0.75))
.py(rems(0.5))
.border_b_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().title_bar_background)
.items_center()
.h(rems_from_px(41.))
.pl_3()
.pr_2()
.justify_between()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.child(
h_flex().gap_2().items_center().child(
Label::new(format!("History: {}", self.history.path.as_unix_str()))
.size(LabelSize::Default),
),
Label::new(self.history.path.as_unix_str().to_string())
.color(Color::Muted)
.buffer_font(cx),
)
.child(
Label::new(format!("{} commits", entry_count))
.size(LabelSize::Small)
.color(Color::Muted),
h_flex()
.gap_1p5()
.child(
Label::new(format!("{} commits", entry_count))
.size(LabelSize::Small)
.color(Color::Muted)
.when(self.has_more, |this| this.mr_1()),
)
.when(self.has_more, |this| {
this.child(Divider::vertical()).child(
Button::new("load-more", "Load More")
.disabled(self.loading_more)
.label_size(LabelSize::Small)
.icon(IconName::ArrowCircle)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(cx.listener(|this, _, window, cx| {
this.load_more(window, cx);
})),
)
}),
),
)
.child(
@@ -474,24 +442,9 @@ impl Render for FileHistoryView {
)
.flex_1()
.size_full()
.with_sizing_behavior(ListSizingBehavior::Auto)
.track_scroll(&self.scroll_handle)
})
.when(self.has_more, |this| {
this.child(
div().p(rems(0.75)).flex().justify_start().child(
Button::new("load-more", "Load more")
.style(ButtonStyle::Subtle)
.disabled(self.loading_more)
.label_size(LabelSize::Small)
.icon(IconName::ArrowCircle)
.icon_position(IconPosition::Start)
.on_click(cx.listener(|this, _, window, cx| {
this.load_more(window, cx);
})),
),
)
}),
.vertical_scrollbar_for(&self.scroll_handle, window, cx),
)
}
}
@@ -518,7 +471,7 @@ impl Item for FileHistoryView {
}
fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
Some(Icon::new(IconName::FileGit))
Some(Icon::new(IconName::GitBranch))
}
fn telemetry_event_text(&self) -> Option<&'static str> {

View File

@@ -1950,7 +1950,7 @@ impl GitPanel {
.lines()
.map(|line| {
if line.len() > 256 {
format!("{}...[truncated]\n", &line[..256])
format!("{}...[truncated]\n", &line[..line.floor_char_boundary(256)])
} else {
format!("{}\n", line)
}
@@ -5910,7 +5910,7 @@ mod tests {
#[test]
fn test_compress_diff_truncate_long_lines() {
let long_line = "a".repeat(300);
let long_line = "🦀".repeat(300);
let diff = indoc::formatdoc! {"
--- a/file.txt
+++ b/file.txt

View File

@@ -66,6 +66,7 @@ use task::RunnableTag;
pub use task_context::{ContextLocation, ContextProvider, RunnableRange};
pub use text_diff::{
DiffOptions, apply_diff_patch, line_diff, text_diff, text_diff_with_options, unified_diff,
word_diff_ranges,
};
use theme::SyntaxTheme;
pub use toolchain::{

View File

@@ -153,6 +153,13 @@ pub struct LanguageSettings {
pub completions: CompletionSettings,
/// Preferred debuggers for this language.
pub debuggers: Vec<String>,
/// Whether to enable word diff highlighting in the editor.
///
/// When enabled, changed words within modified lines are highlighted
/// to show exactly what changed.
///
/// Default: `true`
pub word_diff_enabled: bool,
/// Whether to use tree-sitter bracket queries to detect and colorize the brackets in the editor.
pub colorize_brackets: bool,
}
@@ -595,6 +602,7 @@ impl settings::Settings for AllLanguageSettings {
lsp_insert_mode: completions.lsp_insert_mode.unwrap(),
},
debuggers: settings.debuggers.unwrap(),
word_diff_enabled: settings.word_diff_enabled.unwrap(),
}
}

View File

@@ -44,6 +44,92 @@ pub fn text_diff(old_text: &str, new_text: &str) -> Vec<(Range<usize>, Arc<str>)
text_diff_with_options(old_text, new_text, DiffOptions::default())
}
/// Computes word-level diff ranges between two strings.
///
/// Returns a tuple of (old_ranges, new_ranges) where each vector contains
/// the byte ranges of changed words in the respective text.
/// Whitespace-only changes are excluded from the results.
pub fn word_diff_ranges(
old_text: &str,
new_text: &str,
options: DiffOptions,
) -> (Vec<Range<usize>>, Vec<Range<usize>>) {
let mut input: InternedInput<&str> = InternedInput::default();
input.update_before(tokenize(old_text, options.language_scope.clone()));
input.update_after(tokenize(new_text, options.language_scope));
let mut old_ranges: Vec<Range<usize>> = Vec::new();
let mut new_ranges: Vec<Range<usize>> = Vec::new();
diff_internal(&input, |old_byte_range, new_byte_range, _, _| {
for range in split_on_whitespace(old_text, &old_byte_range) {
if let Some(last) = old_ranges.last_mut()
&& last.end >= range.start
{
last.end = range.end;
} else {
old_ranges.push(range);
}
}
for range in split_on_whitespace(new_text, &new_byte_range) {
if let Some(last) = new_ranges.last_mut()
&& last.end >= range.start
{
last.end = range.end;
} else {
new_ranges.push(range);
}
}
});
(old_ranges, new_ranges)
}
fn split_on_whitespace(text: &str, range: &Range<usize>) -> Vec<Range<usize>> {
if range.is_empty() {
return Vec::new();
}
let slice = &text[range.clone()];
let mut ranges = Vec::new();
let mut offset = 0;
for line in slice.lines() {
let line_start = offset;
let line_end = line_start + line.len();
offset = line_end + 1;
let trimmed = line.trim();
if !trimmed.is_empty() {
let leading = line.len() - line.trim_start().len();
let trailing = line.len() - line.trim_end().len();
let trimmed_start = range.start + line_start + leading;
let trimmed_end = range.start + line_end - trailing;
let original_line_start = text[..range.start + line_start]
.rfind('\n')
.map(|i| i + 1)
.unwrap_or(0);
let original_line_end = text[range.start + line_start..]
.find('\n')
.map(|i| range.start + line_start + i)
.unwrap_or(text.len());
let original_line = &text[original_line_start..original_line_end];
let original_trimmed_start =
original_line_start + (original_line.len() - original_line.trim_start().len());
let original_trimmed_end =
original_line_end - (original_line.len() - original_line.trim_end().len());
if trimmed_start > original_trimmed_start || trimmed_end < original_trimmed_end {
ranges.push(trimmed_start..trimmed_end);
}
}
}
ranges
}
pub struct DiffOptions {
pub language_scope: Option<LanguageScope>,
pub max_word_diff_len: usize,

View File

@@ -1,6 +1,7 @@
((comment) @injection.content
(#set! injection.language "comment")
)
(#match? @injection.content "^(///|//!|/\\*\\*|/\\*!)(.*)")
(#set! injection.language "doxygen")
(#set! injection.include-children))
(preproc_def
value: (preproc_arg) @injection.content

View File

@@ -1,6 +1,7 @@
((comment) @injection.content
(#set! injection.language "comment")
)
(#match? @injection.content "^(///|//!|/\\*\\*|/\\*!)(.*)")
(#set! injection.language "doxygen")
(#set! injection.include-children))
(preproc_def
value: (preproc_arg) @injection.content

View File

@@ -140,13 +140,7 @@ impl LspAdapter for TailwindLspAdapter {
) -> Result<Option<serde_json::Value>> {
Ok(Some(json!({
"provideFormatter": true,
"userLanguages": {
"html": "html",
"css": "css",
"javascript": "javascript",
"typescript": "typescript",
"typescriptreact": "typescriptreact",
},
})))
}
@@ -167,8 +161,18 @@ impl LspAdapter for TailwindLspAdapter {
tailwind_user_settings["emmetCompletions"] = Value::Bool(true);
}
if tailwind_user_settings.get("includeLanguages").is_none() {
tailwind_user_settings["includeLanguages"] = json!({
"html": "html",
"css": "css",
"javascript": "javascript",
"typescript": "typescript",
"typescriptreact": "typescriptreact",
});
}
Ok(json!({
"tailwindCSS": tailwind_user_settings,
"tailwindCSS": tailwind_user_settings
}))
}
@@ -176,6 +180,7 @@ impl LspAdapter for TailwindLspAdapter {
HashMap::from_iter([
(LanguageName::new("Astro"), "astro".to_string()),
(LanguageName::new("HTML"), "html".to_string()),
(LanguageName::new("Gleam"), "html".to_string()),
(LanguageName::new("CSS"), "css".to_string()),
(LanguageName::new("JavaScript"), "javascript".to_string()),
(LanguageName::new("TypeScript"), "typescript".to_string()),

View File

@@ -152,6 +152,8 @@ pub struct MultiBufferDiffHunk {
pub diff_base_byte_range: Range<BufferOffset>,
/// Whether or not this hunk also appears in the 'secondary diff'.
pub secondary_status: DiffHunkSecondaryStatus,
/// The word diffs for this hunk.
pub word_diffs: Vec<Range<MultiBufferOffset>>,
}
impl MultiBufferDiffHunk {
@@ -561,6 +563,7 @@ pub struct MultiBufferSnapshot {
}
#[derive(Debug, Clone)]
/// A piece of text in the multi-buffer
enum DiffTransform {
Unmodified {
summary: MBTextSummary,
@@ -961,6 +964,8 @@ struct MultiBufferCursor<'a, MBD, BD> {
cached_region: Option<MultiBufferRegion<'a, MBD, BD>>,
}
/// Matches transformations to an item
/// This is essentially a more detailed version of DiffTransform
#[derive(Clone)]
struct MultiBufferRegion<'a, MBD, BD> {
buffer: &'a BufferSnapshot,
@@ -3870,11 +3875,31 @@ impl MultiBufferSnapshot {
} else {
range.end.row + 1
};
let word_diffs = (!hunk.base_word_diffs.is_empty()
|| !hunk.buffer_word_diffs.is_empty())
.then(|| {
let hunk_start_offset =
Anchor::in_buffer(excerpt.id, hunk.buffer_range.start).to_offset(self);
hunk.base_word_diffs
.iter()
.map(|diff| hunk_start_offset + diff.start..hunk_start_offset + diff.end)
.chain(
hunk.buffer_word_diffs
.into_iter()
.map(|diff| Anchor::range_in_buffer(excerpt.id, diff).to_offset(self)),
)
.collect()
})
.unwrap_or_default();
Some(MultiBufferDiffHunk {
row_range: MultiBufferRow(range.start.row)..MultiBufferRow(end_row),
buffer_id: excerpt.buffer_id,
excerpt_id: excerpt.id,
buffer_range: hunk.buffer_range.clone(),
word_diffs,
diff_base_byte_range: BufferOffset(hunk.diff_base_byte_range.start)
..BufferOffset(hunk.diff_base_byte_range.end),
secondary_status: hunk.secondary_status,
@@ -6834,6 +6859,7 @@ where
TextDimension::add_assign(&mut buffer_end, &buffer_range_len);
let start = self.diff_transforms.start().output_dimension.0;
let end = self.diff_transforms.end().output_dimension.0;
Some(MultiBufferRegion {
buffer,
excerpt,

View File

@@ -351,7 +351,7 @@ fn test_excerpt_boundaries_and_clipping(cx: &mut App) {
}
#[gpui::test]
fn test_diff_boundary_anchors(cx: &mut TestAppContext) {
async fn test_diff_boundary_anchors(cx: &mut TestAppContext) {
let base_text = "one\ntwo\nthree\n";
let text = "one\nthree\n";
let buffer = cx.new(|cx| Buffer::local(text, cx));
@@ -393,7 +393,7 @@ fn test_diff_boundary_anchors(cx: &mut TestAppContext) {
}
#[gpui::test]
fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
async fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
let base_text = "one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\n";
let text = "one\nfour\nseven\n";
let buffer = cx.new(|cx| Buffer::local(text, cx));
@@ -473,7 +473,7 @@ fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
}
#[gpui::test]
fn test_editing_text_in_diff_hunks(cx: &mut TestAppContext) {
async fn test_editing_text_in_diff_hunks(cx: &mut TestAppContext) {
let base_text = "one\ntwo\nfour\nfive\nsix\nseven\n";
let text = "one\ntwo\nTHREE\nfour\nfive\nseven\n";
let buffer = cx.new(|cx| Buffer::local(text, cx));
@@ -905,7 +905,7 @@ fn test_empty_multibuffer(cx: &mut App) {
}
#[gpui::test]
fn test_empty_diff_excerpt(cx: &mut TestAppContext) {
async fn test_empty_diff_excerpt(cx: &mut TestAppContext) {
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
let buffer = cx.new(|cx| Buffer::local("", cx));
let base_text = "a\nb\nc";
@@ -1235,7 +1235,7 @@ fn test_resolving_anchors_after_replacing_their_excerpts(cx: &mut App) {
}
#[gpui::test]
fn test_basic_diff_hunks(cx: &mut TestAppContext) {
async fn test_basic_diff_hunks(cx: &mut TestAppContext) {
let text = indoc!(
"
ZERO
@@ -1480,7 +1480,7 @@ fn test_basic_diff_hunks(cx: &mut TestAppContext) {
}
#[gpui::test]
fn test_repeatedly_expand_a_diff_hunk(cx: &mut TestAppContext) {
async fn test_repeatedly_expand_a_diff_hunk(cx: &mut TestAppContext) {
let text = indoc!(
"
one
@@ -1994,7 +1994,7 @@ fn test_set_excerpts_for_buffer_rename(cx: &mut TestAppContext) {
}
#[gpui::test]
fn test_diff_hunks_with_multiple_excerpts(cx: &mut TestAppContext) {
async fn test_diff_hunks_with_multiple_excerpts(cx: &mut TestAppContext) {
let base_text_1 = indoc!(
"
one
@@ -3236,6 +3236,7 @@ fn check_multibuffer_edits(
fn test_history(cx: &mut App) {
let test_settings = SettingsStore::test(cx);
cx.set_global(test_settings);
let group_interval: Duration = Duration::from_millis(1);
let buffer_1 = cx.new(|cx| {
let mut buf = Buffer::local("1234", cx);
@@ -3476,7 +3477,7 @@ async fn test_enclosing_indent(cx: &mut TestAppContext) {
}
#[gpui::test]
fn test_summaries_for_anchors(cx: &mut TestAppContext) {
async fn test_summaries_for_anchors(cx: &mut TestAppContext) {
let base_text_1 = indoc!(
"
bar
@@ -3553,7 +3554,7 @@ fn test_summaries_for_anchors(cx: &mut TestAppContext) {
}
#[gpui::test]
fn test_trailing_deletion_without_newline(cx: &mut TestAppContext) {
async fn test_trailing_deletion_without_newline(cx: &mut TestAppContext) {
let base_text_1 = "one\ntwo".to_owned();
let text_1 = "one\n".to_owned();
@@ -4278,8 +4279,10 @@ fn test_random_chunk_bitmaps(cx: &mut App, mut rng: StdRng) {
}
}
#[gpui::test(iterations = 100)]
#[gpui::test(iterations = 10)]
fn test_random_chunk_bitmaps_with_diffs(cx: &mut App, mut rng: StdRng) {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
use buffer_diff::BufferDiff;
use util::RandomCharIter;
@@ -4435,6 +4438,105 @@ fn test_random_chunk_bitmaps_with_diffs(cx: &mut App, mut rng: StdRng) {
}
}
fn collect_word_diffs(
base_text: &str,
modified_text: &str,
cx: &mut TestAppContext,
) -> Vec<String> {
let buffer = cx.new(|cx| Buffer::local(modified_text, cx));
let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
cx.run_until_parked();
let multibuffer = cx.new(|cx| {
let mut multibuffer = MultiBuffer::singleton(buffer.clone(), cx);
multibuffer.add_diff(diff.clone(), cx);
multibuffer
});
multibuffer.update(cx, |multibuffer, cx| {
multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx);
});
let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx));
let text = snapshot.text();
snapshot
.diff_hunks()
.flat_map(|hunk| hunk.word_diffs)
.map(|range| text[range.start.0..range.end.0].to_string())
.collect()
}
#[gpui::test]
async fn test_word_diff_simple_replacement(cx: &mut TestAppContext) {
let settings_store = cx.update(|cx| SettingsStore::test(cx));
cx.set_global(settings_store);
let base_text = "hello world foo bar\n";
let modified_text = "hello WORLD foo BAR\n";
let word_diffs = collect_word_diffs(base_text, modified_text, cx);
assert_eq!(word_diffs, vec!["world", "bar", "WORLD", "BAR"]);
}
#[gpui::test]
async fn test_word_diff_consecutive_modified_lines(cx: &mut TestAppContext) {
let settings_store = cx.update(|cx| SettingsStore::test(cx));
cx.set_global(settings_store);
let base_text = "aaa bbb\nccc ddd\n";
let modified_text = "aaa BBB\nccc DDD\n";
let word_diffs = collect_word_diffs(base_text, modified_text, cx);
assert_eq!(
word_diffs,
vec!["bbb", "ddd", "BBB", "DDD"],
"consecutive modified lines should produce word diffs when line counts match"
);
}
#[gpui::test]
async fn test_word_diff_modified_lines_with_deletion_between(cx: &mut TestAppContext) {
let settings_store = cx.update(|cx| SettingsStore::test(cx));
cx.set_global(settings_store);
let base_text = "aaa bbb\ndeleted line\nccc ddd\n";
let modified_text = "aaa BBB\nccc DDD\n";
let word_diffs = collect_word_diffs(base_text, modified_text, cx);
assert_eq!(
word_diffs,
Vec::<String>::new(),
"modified lines with a deleted line between should not produce word diffs"
);
}
#[gpui::test]
async fn test_word_diff_disabled(cx: &mut TestAppContext) {
let settings_store = cx.update(|cx| {
let mut settings_store = SettingsStore::test(cx);
settings_store.update_user_settings(cx, |settings| {
settings.project.all_languages.defaults.word_diff_enabled = Some(false);
});
settings_store
});
cx.set_global(settings_store);
let base_text = "hello world\n";
let modified_text = "hello WORLD\n";
let word_diffs = collect_word_diffs(base_text, modified_text, cx);
assert_eq!(
word_diffs,
Vec::<String>::new(),
"word diffs should be empty when disabled"
);
}
/// Tests `excerpt_containing` and `excerpts_for_range` (functions mapping multi-buffer text-coordinates to excerpts)
#[gpui::test]
fn test_excerpts_containment_functions(cx: &mut App) {

View File

@@ -220,7 +220,7 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement
});
} else {
let appearance = *SystemAppearance::global(cx);
theme::set_theme(settings, theme, appearance);
theme::set_theme(settings, theme, appearance, appearance);
}
});
}

View File

@@ -206,6 +206,7 @@ impl DapLocator for CargoLocator {
args.push("--nocapture".to_owned());
if is_ignored {
args.push("--include-ignored".to_owned());
args.push("--exact".to_owned());
}
}

View File

@@ -418,6 +418,13 @@ pub struct LanguageSettingsContent {
///
/// Default: []
pub debuggers: Option<Vec<String>>,
/// Whether to enable word diff highlighting in the editor.
///
/// When enabled, changed words within modified lines are highlighted
/// to show exactly what changed.
///
/// Default: true
pub word_diff_enabled: Option<bool>,
/// Whether to use tree-sitter bracket queries to detect and colorize the brackets in the editor.
///
/// Default: false

View File

@@ -861,6 +861,14 @@ pub struct ThemeColorsContent {
#[serde(rename = "version_control.ignored")]
pub version_control_ignored: Option<String>,
/// Color for added words in word diffs.
#[serde(rename = "version_control.word_added")]
pub version_control_word_added: Option<String>,
/// Color for deleted words in word diffs.
#[serde(rename = "version_control.word_deleted")]
pub version_control_word_deleted: Option<String>,
/// Background color for row highlights of "ours" regions in merge conflicts.
#[serde(rename = "version_control.conflict_marker.ours")]
pub version_control_conflict_marker_ours: Option<String>,

View File

@@ -490,6 +490,7 @@ impl VsCodeSettings {
.flat_map(|n| n.as_u64().map(|n| n as usize))
.collect()
}),
word_diff_enabled: None,
}
}

View File

@@ -6981,6 +6981,25 @@ fn language_settings_data() -> Vec<SettingsPageItem> {
files: USER | PROJECT,
}),
SettingsPageItem::SectionHeader("Miscellaneous"),
SettingsPageItem::SettingItem(SettingItem {
title: "Word Diff Enabled",
description: "Whether to enable word diff highlighting in the editor. When enabled, changed words within modified lines are highlighted to show exactly what changed.",
field: Box::new(SettingField {
json_path: Some("languages.$(language).word_diff_enabled"),
pick: |settings_content| {
language_settings_field(settings_content, |language| {
language.word_diff_enabled.as_ref()
})
},
write: |settings_content, value| {
language_settings_field_mut(settings_content, value, |language, value| {
language.word_diff_enabled = value;
})
},
}),
metadata: None,
files: USER | PROJECT,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Debuggers",
description: "Preferred debuggers for this language.",

View File

@@ -2503,9 +2503,7 @@ mod tests {
})
.detach();
let first_event = Event::Wakeup;
let wakeup = event_rx.recv().await.expect("No wakeup event received");
assert_eq!(wakeup, first_event, "Expected wakeup, got {wakeup:?}");
let first_event = event_rx.recv().await.expect("No wakeup event received");
terminal.update(cx, |terminal, _| {
let success = terminal.try_keystroke(&Keystroke::parse("ctrl-c").unwrap(), false);

View File

@@ -9,11 +9,17 @@ pub(crate) fn neutral() -> ColorScaleSet {
}
const ADDED_COLOR: Hsla = Hsla {
h: 142. / 360.,
s: 0.68,
l: 0.45,
h: 134. / 360.,
s: 0.55,
l: 0.40,
a: 1.0,
};
const WORD_ADDED_COLOR: Hsla = Hsla {
h: 134. / 360.,
s: 0.55,
l: 0.40,
a: 0.35,
};
const MODIFIED_COLOR: Hsla = Hsla {
h: 48. / 360.,
s: 0.76,
@@ -21,11 +27,17 @@ const MODIFIED_COLOR: Hsla = Hsla {
a: 1.0,
};
const REMOVED_COLOR: Hsla = Hsla {
h: 355. / 360.,
s: 0.65,
l: 0.65,
h: 350. / 360.,
s: 0.88,
l: 0.25,
a: 1.0,
};
const WORD_DELETED_COLOR: Hsla = Hsla {
h: 350. / 360.,
s: 0.88,
l: 0.25,
a: 0.80,
};
/// The default colors for the theme.
///
@@ -152,6 +164,8 @@ impl ThemeColors {
version_control_renamed: MODIFIED_COLOR,
version_control_conflict: orange().light().step_12(),
version_control_ignored: gray().light().step_12(),
version_control_word_added: WORD_ADDED_COLOR,
version_control_word_deleted: WORD_DELETED_COLOR,
version_control_conflict_marker_ours: green().light().step_10().alpha(0.5),
version_control_conflict_marker_theirs: blue().light().step_10().alpha(0.5),
vim_normal_background: system.transparent,
@@ -287,6 +301,8 @@ impl ThemeColors {
version_control_renamed: MODIFIED_COLOR,
version_control_conflict: orange().dark().step_12(),
version_control_ignored: gray().dark().step_12(),
version_control_word_added: WORD_ADDED_COLOR,
version_control_word_deleted: WORD_DELETED_COLOR,
version_control_conflict_marker_ours: green().dark().step_10().alpha(0.5),
version_control_conflict_marker_theirs: blue().dark().step_10().alpha(0.5),
vim_normal_background: system.transparent,

View File

@@ -71,11 +71,17 @@ pub(crate) fn zed_default_dark() -> Theme {
let yellow = hsla(39. / 360., 67. / 100., 69. / 100., 1.0);
const ADDED_COLOR: Hsla = Hsla {
h: 142. / 360.,
s: 0.68,
l: 0.45,
h: 134. / 360.,
s: 0.55,
l: 0.40,
a: 1.0,
};
const WORD_ADDED_COLOR: Hsla = Hsla {
h: 134. / 360.,
s: 0.55,
l: 0.40,
a: 0.35,
};
const MODIFIED_COLOR: Hsla = Hsla {
h: 48. / 360.,
s: 0.76,
@@ -83,11 +89,17 @@ pub(crate) fn zed_default_dark() -> Theme {
a: 1.0,
};
const REMOVED_COLOR: Hsla = Hsla {
h: 355. / 360.,
s: 0.65,
l: 0.65,
h: 350. / 360.,
s: 0.88,
l: 0.25,
a: 1.0,
};
const WORD_DELETED_COLOR: Hsla = Hsla {
h: 350. / 360.,
s: 0.88,
l: 0.25,
a: 0.80,
};
let player = PlayerColors::dark();
Theme {
@@ -231,6 +243,8 @@ pub(crate) fn zed_default_dark() -> Theme {
version_control_renamed: MODIFIED_COLOR,
version_control_conflict: crate::orange().light().step_12(),
version_control_ignored: crate::gray().light().step_12(),
version_control_word_added: WORD_ADDED_COLOR,
version_control_word_deleted: WORD_DELETED_COLOR,
version_control_conflict_marker_ours: crate::green().light().step_12().alpha(0.5),
version_control_conflict_marker_theirs: crate::blue().light().step_12().alpha(0.5),

View File

@@ -744,6 +744,14 @@ pub fn theme_colors_refinement(
.and_then(|color| try_parse_color(color).ok())
// Fall back to `conflict`, for backwards compatibility.
.or(status_colors.ignored),
version_control_word_added: this
.version_control_word_added
.as_ref()
.and_then(|color| try_parse_color(color).ok()),
version_control_word_deleted: this
.version_control_word_deleted
.as_ref()
.and_then(|color| try_parse_color(color).ok()),
#[allow(deprecated)]
version_control_conflict_marker_ours: this
.version_control_conflict_marker_ours

View File

@@ -304,31 +304,50 @@ impl IconThemeSelection {
}
}
// impl ThemeSettingsContent {
/// Sets the theme for the given appearance to the theme with the specified name.
///
/// The caller should make sure that the [`Appearance`] matches the theme associated with the name.
///
/// If the current [`ThemeAppearanceMode`] is set to [`System`] and the user's system [`Appearance`]
/// is different than the new theme's [`Appearance`], this function will update the
/// [`ThemeAppearanceMode`] to the new theme's appearance in order to display the new theme.
///
/// [`System`]: ThemeAppearanceMode::System
pub fn set_theme(
current: &mut SettingsContent,
theme_name: impl Into<Arc<str>>,
appearance: Appearance,
theme_appearance: Appearance,
system_appearance: Appearance,
) {
if let Some(selection) = current.theme.theme.as_mut() {
let theme_to_update = match selection {
settings::ThemeSelection::Static(theme) => theme,
settings::ThemeSelection::Dynamic { mode, light, dark } => match mode {
ThemeAppearanceMode::Light => light,
ThemeAppearanceMode::Dark => dark,
ThemeAppearanceMode::System => match appearance {
Appearance::Light => light,
Appearance::Dark => dark,
},
},
};
let theme_name = ThemeName(theme_name.into());
*theme_to_update = ThemeName(theme_name.into());
} else {
current.theme.theme = Some(settings::ThemeSelection::Static(ThemeName(
theme_name.into(),
)));
let Some(selection) = current.theme.theme.as_mut() else {
current.theme.theme = Some(settings::ThemeSelection::Static(theme_name));
return;
};
match selection {
settings::ThemeSelection::Static(theme) => {
*theme = theme_name;
}
settings::ThemeSelection::Dynamic { mode, light, dark } => {
// Update the appropriate theme slot based on appearance.
match theme_appearance {
Appearance::Light => *light = theme_name,
Appearance::Dark => *dark = theme_name,
}
// Don't update the theme mode if it is set to system and the new theme has the same
// appearance.
let should_update_mode =
!(mode == &ThemeAppearanceMode::System && theme_appearance == system_appearance);
if should_update_mode {
// Update the mode to the specified appearance (otherwise we might set the theme and
// nothing gets updated because the system specified the other mode appearance).
*mode = ThemeAppearanceMode::from(theme_appearance);
}
}
}
}

View File

@@ -300,7 +300,10 @@ pub struct ThemeColors {
pub version_control_conflict: Hsla,
/// Represents an ignored entry in version control systems.
pub version_control_ignored: Hsla,
/// Represents an added word in a word diff.
pub version_control_word_added: Hsla,
/// Represents a deleted word in a word diff.
pub version_control_word_deleted: Hsla,
/// Represents the "ours" region of a merge conflict.
pub version_control_conflict_marker_ours: Hsla,
/// Represents the "theirs" region of a merge conflict.

View File

@@ -84,6 +84,15 @@ impl From<WindowAppearance> for Appearance {
}
}
impl From<Appearance> for ThemeAppearanceMode {
fn from(value: Appearance) -> Self {
match value {
Appearance::Light => Self::Light,
Appearance::Dark => Self::Dark,
}
}
}
/// Which themes should be loaded. This is used primarily for testing.
pub enum LoadThemes {
/// Only load the base theme.

View File

@@ -9,7 +9,10 @@ use gpui::{
use picker::{Picker, PickerDelegate};
use settings::{Settings, SettingsStore, update_settings_file};
use std::sync::Arc;
use theme::{Appearance, Theme, ThemeMeta, ThemeRegistry, ThemeSettings};
use theme::{
Appearance, SystemAppearance, Theme, ThemeAppearanceMode, ThemeMeta, ThemeName, ThemeRegistry,
ThemeSelection, ThemeSettings,
};
use ui::{ListItem, ListItemSpacing, prelude::*, v_flex};
use util::ResultExt;
use workspace::{ModalView, Workspace, ui::HighlightedLabel, with_active_or_new_workspace};
@@ -114,7 +117,14 @@ struct ThemeSelectorDelegate {
fs: Arc<dyn Fs>,
themes: Vec<ThemeMeta>,
matches: Vec<StringMatch>,
original_theme: Arc<Theme>,
/// The theme that was selected before the `ThemeSelector` menu was opened.
///
/// We use this to return back to theme that was set if the user dismisses the menu.
original_theme_settings: ThemeSettings,
/// The current system appearance.
original_system_appearance: Appearance,
/// The currently selected new theme.
new_theme: Arc<Theme>,
selection_completed: bool,
selected_theme: Option<Arc<Theme>>,
selected_index: usize,
@@ -129,6 +139,8 @@ impl ThemeSelectorDelegate {
cx: &mut Context<ThemeSelector>,
) -> Self {
let original_theme = cx.theme().clone();
let original_theme_settings = ThemeSettings::get_global(cx).clone();
let original_system_appearance = SystemAppearance::global(cx).0;
let registry = ThemeRegistry::global(cx);
let mut themes = registry
@@ -143,13 +155,15 @@ impl ThemeSelectorDelegate {
})
.collect::<Vec<_>>();
// Sort by dark vs light, then by name.
themes.sort_unstable_by(|a, b| {
a.appearance
.is_light()
.cmp(&b.appearance.is_light())
.then(a.name.cmp(&b.name))
});
let matches = themes
let matches: Vec<StringMatch> = themes
.iter()
.map(|meta| StringMatch {
candidate_id: 0,
@@ -158,19 +172,25 @@ impl ThemeSelectorDelegate {
string: meta.name.to_string(),
})
.collect();
let mut this = Self {
// The current theme is likely in this list, so default to first showing that.
let selected_index = matches
.iter()
.position(|mat| mat.string == original_theme.name)
.unwrap_or(0);
Self {
fs,
themes,
matches,
original_theme: original_theme.clone(),
selected_index: 0,
original_theme_settings,
original_system_appearance,
new_theme: original_theme, // Start with the original theme.
selected_index,
selection_completed: false,
selected_theme: None,
selector,
};
this.select_if_matching(&original_theme.name);
this
}
}
fn show_selected_theme(
@@ -179,9 +199,10 @@ impl ThemeSelectorDelegate {
) -> Option<Arc<Theme>> {
if let Some(mat) = self.matches.get(self.selected_index) {
let registry = ThemeRegistry::global(cx);
match registry.get(&mat.string) {
Ok(theme) => {
Self::set_theme(theme.clone(), cx);
self.set_theme(theme.clone(), cx);
Some(theme)
}
Err(error) => {
@@ -194,21 +215,122 @@ impl ThemeSelectorDelegate {
}
}
fn select_if_matching(&mut self, theme_name: &str) {
self.selected_index = self
.matches
.iter()
.position(|mat| mat.string == theme_name)
.unwrap_or(self.selected_index);
}
fn set_theme(theme: Arc<Theme>, cx: &mut App) {
fn set_theme(&mut self, new_theme: Arc<Theme>, cx: &mut App) {
// Update the global (in-memory) theme settings.
SettingsStore::update_global(cx, |store, _| {
let mut theme_settings = store.get::<ThemeSettings>(None).clone();
let name = theme.as_ref().name.clone().into();
theme_settings.theme = theme::ThemeSelection::Static(theme::ThemeName(name));
store.override_global(theme_settings);
override_global_theme(
store,
&new_theme,
&self.original_theme_settings.theme,
self.original_system_appearance,
)
});
self.new_theme = new_theme;
}
}
/// Overrides the global (in-memory) theme settings.
///
/// Note that this does **not** update the user's `settings.json` file (see the
/// [`ThemeSelectorDelegate::confirm`] method and [`theme::set_theme`] function).
fn override_global_theme(
store: &mut SettingsStore,
new_theme: &Theme,
original_theme: &ThemeSelection,
system_appearance: Appearance,
) {
let theme_name = ThemeName(new_theme.name.clone().into());
let new_appearance = new_theme.appearance();
let new_theme_is_light = new_appearance.is_light();
let mut curr_theme_settings = store.get::<ThemeSettings>(None).clone();
match (original_theme, &curr_theme_settings.theme) {
// Override the currently selected static theme.
(ThemeSelection::Static(_), ThemeSelection::Static(_)) => {
curr_theme_settings.theme = ThemeSelection::Static(theme_name);
}
// If the current theme selection is dynamic, then only override the global setting for the
// specific mode (light or dark).
(
ThemeSelection::Dynamic {
mode: original_mode,
light: original_light,
dark: original_dark,
},
ThemeSelection::Dynamic { .. },
) => {
let new_mode = update_mode_if_new_appearance_is_different_from_system(
original_mode,
system_appearance,
new_appearance,
);
let updated_theme = retain_original_opposing_theme(
new_theme_is_light,
new_mode,
theme_name,
original_light,
original_dark,
);
curr_theme_settings.theme = updated_theme;
}
// The theme selection mode changed while selecting new themes (someone edited the settings
// file on disk while we had the dialogue open), so don't do anything.
_ => return,
};
store.override_global(curr_theme_settings);
}
/// Helper function for determining the new [`ThemeAppearanceMode`] for the new theme.
///
/// If the the original theme mode was [`System`] and the new theme's appearance matches the system
/// appearance, we don't need to change the mode setting.
///
/// Otherwise, we need to change the mode in order to see the new theme.
///
/// [`System`]: ThemeAppearanceMode::System
fn update_mode_if_new_appearance_is_different_from_system(
original_mode: &ThemeAppearanceMode,
system_appearance: Appearance,
new_appearance: Appearance,
) -> ThemeAppearanceMode {
if original_mode == &ThemeAppearanceMode::System && system_appearance == new_appearance {
ThemeAppearanceMode::System
} else {
ThemeAppearanceMode::from(new_appearance)
}
}
/// Helper function for updating / displaying the [`ThemeSelection`] while using the theme selector.
///
/// We want to retain the alternate theme selection of the original settings (before the menu was
/// opened), not the currently selected theme (which likely has changed multiple times while the
/// menu has been open).
fn retain_original_opposing_theme(
new_theme_is_light: bool,
new_mode: ThemeAppearanceMode,
theme_name: ThemeName,
original_light: &ThemeName,
original_dark: &ThemeName,
) -> ThemeSelection {
if new_theme_is_light {
ThemeSelection::Dynamic {
mode: new_mode,
light: theme_name,
dark: original_dark.clone(),
}
} else {
ThemeSelection::Dynamic {
mode: new_mode,
light: original_light.clone(),
dark: theme_name,
}
}
}
@@ -225,19 +347,20 @@ impl PickerDelegate for ThemeSelectorDelegate {
fn confirm(
&mut self,
_: bool,
window: &mut Window,
_secondary: bool,
_window: &mut Window,
cx: &mut Context<Picker<ThemeSelectorDelegate>>,
) {
self.selection_completed = true;
let appearance = Appearance::from(window.appearance());
let theme_name = ThemeSettings::get_global(cx).theme.name(appearance).0;
let theme_name: Arc<str> = self.new_theme.name.as_str().into();
let theme_appearance = self.new_theme.appearance;
let system_appearance = SystemAppearance::global(cx).0;
telemetry::event!("Settings Changed", setting = "theme", value = theme_name);
update_settings_file(self.fs.clone(), cx, move |settings, _| {
theme::set_theme(settings, theme_name.to_string(), appearance);
theme::set_theme(settings, theme_name, theme_appearance, system_appearance);
});
self.selector
@@ -249,7 +372,9 @@ impl PickerDelegate for ThemeSelectorDelegate {
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<ThemeSelectorDelegate>>) {
if !self.selection_completed {
Self::set_theme(self.original_theme.clone(), cx);
SettingsStore::update_global(cx, |store, _| {
store.override_global(self.original_theme_settings.clone());
});
self.selection_completed = true;
}

View File

@@ -91,10 +91,16 @@ impl RenderOnce for Avatar {
self.image
.size(image_size)
.rounded_full()
.bg(cx.theme().colors().ghost_element_background)
.bg(cx.theme().colors().element_disabled)
.with_fallback(|| {
Icon::new(IconName::Person)
.color(Color::Muted)
h_flex()
.size_full()
.justify_center()
.child(
Icon::new(IconName::Person)
.color(Color::Muted)
.size(IconSize::Small),
)
.into_any_element()
}),
)

View File

@@ -506,7 +506,12 @@ impl Vim {
search_bar.is_contains_uppercase(&search),
);
} else {
options.set(SearchOptions::CASE_SENSITIVE, false)
// Fallback: no explicit i/I flags and smartcase disabled;
// use global editor.search.case_sensitive.
options.set(
SearchOptions::CASE_SENSITIVE,
EditorSettings::get_global(cx).search.case_sensitive,
)
}
if !replacement.flag_g {

View File

@@ -963,6 +963,15 @@ impl SplitDirection {
Self::Down | Self::Right => true,
}
}
pub fn opposite(&self) -> SplitDirection {
match self {
Self::Down => Self::Up,
Self::Up => Self::Down,
Self::Left => Self::Right,
Self::Right => Self::Left,
}
}
}
mod element {

View File

@@ -437,6 +437,8 @@ actions!(
SwapPaneUp,
/// Swaps the current pane with the one below.
SwapPaneDown,
// Swaps the current pane with the first available adjacent pane (searching in order: below, above, right, left) and activates that pane.
SwapPaneAdjacent,
/// Move the current pane to be at the far left.
MovePaneLeft,
/// Move the current pane to be at the far right.
@@ -5823,6 +5825,21 @@ impl Workspace {
.on_action(cx.listener(|workspace, _: &SwapPaneDown, _, cx| {
workspace.swap_pane_in_direction(SplitDirection::Down, cx)
}))
.on_action(cx.listener(|workspace, _: &SwapPaneAdjacent, window, cx| {
const DIRECTION_PRIORITY: [SplitDirection; 4] = [
SplitDirection::Down,
SplitDirection::Up,
SplitDirection::Right,
SplitDirection::Left,
];
for dir in DIRECTION_PRIORITY {
if workspace.find_pane_in_direction(dir, cx).is_some() {
workspace.swap_pane_in_direction(dir, cx);
workspace.activate_pane_in_direction(dir.opposite(), window, cx);
break;
}
}
}))
.on_action(cx.listener(|workspace, _: &MovePaneLeft, _, cx| {
workspace.move_pane_to_border(SplitDirection::Left, cx)
}))
@@ -5950,6 +5967,88 @@ impl Workspace {
},
))
.on_action(cx.listener(Workspace::toggle_centered_layout))
.on_action(cx.listener(
|workspace: &mut Workspace, _action: &pane::ActivateNextItem, window, cx| {
if let Some(active_dock) = workspace.active_dock(window, cx) {
let dock = active_dock.read(cx);
if let Some(active_panel) = dock.active_panel() {
if active_panel.pane(cx).is_none() {
let mut recent_pane: Option<Entity<Pane>> = None;
let mut recent_timestamp = 0;
for pane_handle in workspace.panes() {
let pane = pane_handle.read(cx);
for entry in pane.activation_history() {
if entry.timestamp > recent_timestamp {
recent_timestamp = entry.timestamp;
recent_pane = Some(pane_handle.clone());
}
}
}
if let Some(pane) = recent_pane {
pane.update(cx, |pane, cx| {
let current_index = pane.active_item_index();
let items_len = pane.items_len();
if items_len > 0 {
let next_index = if current_index + 1 < items_len {
current_index + 1
} else {
0
};
pane.activate_item(
next_index, false, false, window, cx,
);
}
});
return;
}
}
}
}
cx.propagate();
},
))
.on_action(cx.listener(
|workspace: &mut Workspace, _action: &pane::ActivatePreviousItem, window, cx| {
if let Some(active_dock) = workspace.active_dock(window, cx) {
let dock = active_dock.read(cx);
if let Some(active_panel) = dock.active_panel() {
if active_panel.pane(cx).is_none() {
let mut recent_pane: Option<Entity<Pane>> = None;
let mut recent_timestamp = 0;
for pane_handle in workspace.panes() {
let pane = pane_handle.read(cx);
for entry in pane.activation_history() {
if entry.timestamp > recent_timestamp {
recent_timestamp = entry.timestamp;
recent_pane = Some(pane_handle.clone());
}
}
}
if let Some(pane) = recent_pane {
pane.update(cx, |pane, cx| {
let current_index = pane.active_item_index();
let items_len = pane.items_len();
if items_len > 0 {
let prev_index = if current_index > 0 {
current_index - 1
} else {
items_len.saturating_sub(1)
};
pane.activate_item(
prev_index, false, false, window, cx,
);
}
});
return;
}
}
}
}
cx.propagate();
},
))
.on_action(cx.listener(Workspace::cancel))
}

View File

@@ -1394,8 +1394,7 @@ fn notify_settings_errors(result: settings::SettingsParseResult, is_user: bool,
settings::ParseStatus::Failed { error } => Some(anyhow::format_err!(error)),
settings::ParseStatus::Success => None,
};
struct SettingsParseErrorNotification;
let id = NotificationId::unique::<SettingsParseErrorNotification>();
let id = NotificationId::Named(format!("failed-to-parse-settings-{is_user}").into());
let showed_parse_error = match error {
Some(error) => {
@@ -1427,7 +1426,7 @@ fn notify_settings_errors(result: settings::SettingsParseResult, is_user: bool,
false
}
};
let id = NotificationId::Named("failed-to-migrate-settings".into());
let id = NotificationId::Named(format!("failed-to-migrate-settings-{is_user}").into());
match result.migration_status {
settings::MigrationStatus::Succeeded | settings::MigrationStatus::NotNeeded => {

View File

@@ -23,9 +23,10 @@ buffer_diff.workspace = true
client.workspace = true
cloud_llm_client.workspace = true
cloud_zeta2_prompt.workspace = true
copilot.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
copilot.workspace = true
credentials_provider.workspace = true
db.workspace = true
edit_prediction.workspace = true
edit_prediction_context.workspace = true
@@ -43,12 +44,12 @@ lsp.workspace = true
markdown.workspace = true
menu.workspace = true
open_ai.workspace = true
pretty_assertions.workspace = true
postage.workspace = true
pretty_assertions.workspace = true
project.workspace = true
rand.workspace = true
release_channel.workspace = true
regex.workspace = true
release_channel.workspace = true
semver.workspace = true
serde.workspace = true
serde_json.workspace = true
@@ -60,8 +61,8 @@ telemetry.workspace = true
telemetry_events.workspace = true
theme.workspace = true
thiserror.workspace = true
util.workspace = true
ui.workspace = true
util.workspace = true
uuid.workspace = true
workspace.workspace = true
worktree.workspace = true

View File

@@ -78,7 +78,7 @@ impl EditPredictionProvider for ZetaEditPredictionProvider {
) -> bool {
let zeta = self.zeta.read(cx);
if zeta.edit_prediction_model == ZetaEditPredictionModel::Sweep {
zeta.sweep_ai.api_token.is_some()
zeta.has_sweep_api_token()
} else {
true
}

View File

@@ -1,6 +1,7 @@
use anyhow::Result;
use anyhow::{Context as _, Result};
use cloud_llm_client::predict_edits_v3::Event;
use futures::AsyncReadExt as _;
use credentials_provider::CredentialsProvider;
use futures::{AsyncReadExt as _, FutureExt, future::Shared};
use gpui::{
App, AppContext as _, Entity, Task,
http_client::{self, AsyncBody, Method},
@@ -23,18 +24,23 @@ use crate::{EditPredictionId, EditPredictionInputs, prediction::EditPredictionRe
const SWEEP_API_URL: &str = "https://autocomplete.sweep.dev/backend/next_edit_autocomplete";
pub struct SweepAi {
pub api_token: Option<String>,
pub api_token: Shared<Task<Option<String>>>,
pub debug_info: Arc<str>,
}
impl SweepAi {
pub fn new(cx: &App) -> Self {
SweepAi {
api_token: std::env::var("SWEEP_AI_TOKEN").ok(),
api_token: load_api_token(cx).shared(),
debug_info: debug_info(cx),
}
}
pub fn set_api_token(&mut self, api_token: Option<String>, cx: &mut App) -> Task<Result<()>> {
self.api_token = Task::ready(api_token.clone()).shared();
store_api_token_in_keychain(api_token, cx)
}
pub fn request_prediction_with_sweep(
&self,
project: &Entity<Project>,
@@ -47,7 +53,7 @@ impl SweepAi {
cx: &mut App,
) -> Task<Result<Option<EditPredictionResult>>> {
let debug_info = self.debug_info.clone();
let Some(api_token) = self.api_token.clone() else {
let Some(api_token) = self.api_token.clone().now_or_never().flatten() else {
return Task::ready(Ok(None));
};
let full_path: Arc<Path> = snapshot
@@ -260,6 +266,49 @@ impl SweepAi {
}
}
pub const SWEEP_CREDENTIALS_URL: &str = "https://autocomplete.sweep.dev";
pub const SWEEP_CREDENTIALS_USERNAME: &str = "sweep-api-token";
pub fn load_api_token(cx: &App) -> Task<Option<String>> {
if let Some(api_token) = std::env::var("SWEEP_AI_TOKEN")
.ok()
.filter(|value| !value.is_empty())
{
return Task::ready(Some(api_token));
}
let credentials_provider = <dyn CredentialsProvider>::global(cx);
cx.spawn(async move |cx| {
let (_, credentials) = credentials_provider
.read_credentials(SWEEP_CREDENTIALS_URL, &cx)
.await
.ok()??;
String::from_utf8(credentials).ok()
})
}
fn store_api_token_in_keychain(api_token: Option<String>, cx: &App) -> Task<Result<()>> {
let credentials_provider = <dyn CredentialsProvider>::global(cx);
cx.spawn(async move |cx| {
if let Some(api_token) = api_token {
credentials_provider
.write_credentials(
SWEEP_CREDENTIALS_URL,
SWEEP_CREDENTIALS_USERNAME,
api_token.as_bytes(),
cx,
)
.await
.context("Failed to save Sweep API token to system keychain")
} else {
credentials_provider
.delete_credentials(SWEEP_CREDENTIALS_URL, cx)
.await
.context("Failed to delete Sweep API token from system keychain")
}
})
}
#[derive(Debug, Clone, Serialize)]
struct AutocompleteRequest {
pub debug_info: Arc<str>,

View File

@@ -20,7 +20,7 @@ use edit_prediction_context::{
};
use feature_flags::{FeatureFlag, FeatureFlagAppExt as _, PredictEditsRateCompletionsFeatureFlag};
use futures::channel::{mpsc, oneshot};
use futures::{AsyncReadExt as _, StreamExt as _};
use futures::{AsyncReadExt as _, FutureExt as _, StreamExt as _};
use gpui::{
App, AsyncApp, Entity, EntityId, Global, SharedString, Subscription, Task, WeakEntity, actions,
http_client::{self, AsyncBody, Method},
@@ -61,7 +61,7 @@ mod prediction;
mod provider;
mod rate_prediction_modal;
pub mod retrieval_search;
mod sweep_ai;
pub mod sweep_ai;
pub mod udiff;
mod xml_edits;
pub mod zeta1;
@@ -80,7 +80,7 @@ use crate::rate_prediction_modal::{
NextEdit, PreviousEdit, RatePredictionsModal, ThumbsDownActivePrediction,
ThumbsUpActivePrediction,
};
use crate::sweep_ai::SweepAi;
pub use crate::sweep_ai::SweepAi;
use crate::zeta1::request_prediction_with_zeta1;
pub use provider::ZetaEditPredictionProvider;
@@ -193,7 +193,7 @@ pub struct Zeta {
#[cfg(feature = "eval-support")]
eval_cache: Option<Arc<dyn EvalCache>>,
edit_prediction_model: ZetaEditPredictionModel,
sweep_ai: SweepAi,
pub sweep_ai: SweepAi,
data_collection_choice: DataCollectionChoice,
rejected_predictions: Vec<EditPredictionRejection>,
reject_predictions_tx: mpsc::UnboundedSender<()>,
@@ -553,7 +553,12 @@ impl Zeta {
}
pub fn has_sweep_api_token(&self) -> bool {
self.sweep_ai.api_token.is_some()
self.sweep_ai
.api_token
.clone()
.now_or_never()
.flatten()
.is_some()
}
#[cfg(feature = "eval-support")]
@@ -988,7 +993,7 @@ impl Zeta {
});
let reached_request_limit =
self.rejected_predictions.len() >= MAX_EDIT_PREDICTION_REJECTIONS_PER_REQUEST;
self.rejected_predictions.len() >= MAX_EDIT_PREDICTION_REJECTIONS_PER_REQUEST / 2;
let reject_tx = self.reject_predictions_tx.clone();
self.reject_predictions_debounce_task = Some(cx.spawn(async move |_this, cx| {
const REJECT_REQUEST_DEBOUNCE: Duration = Duration::from_secs(15);
@@ -2058,6 +2063,10 @@ impl Zeta {
cursor_position: language::Anchor,
cx: &mut Context<Self>,
) {
if !matches!(self.edit_prediction_model, ZetaEditPredictionModel::Zeta2) {
return;
}
if !matches!(&self.options().context, ContextMode::Agentic { .. }) {
return;
}

View File

@@ -85,6 +85,10 @@
- [Agent Server Extensions](./extensions/agent-servers.md)
- [MCP Server Extensions](./extensions/mcp-extensions.md)
# Migrate
- [VS Code](./migrate/vs-code.md)
# Language Support
- [All Languages](./languages.md)

View File

@@ -28,6 +28,7 @@ Languages which can be used with Tailwind CSS in Zed:
- [Astro](./astro.md)
- [CSS](./css.md)
- [ERB](./ruby.md)
- [Gleam](./gleam.md)
- [HEEx](./elixir.md#heex)
- [HTML](./html.md)
- [TypeScript](./typescript.md)

373
docs/src/migrate/vs-code.md Normal file
View File

@@ -0,0 +1,373 @@
# How to Migrate from VS Code to Zed
This guide is for developers who spent serious time in VS Code and want to try Zed without starting from scratch.
If youre here, you might be looking for a faster editor. Or something less cluttered. Or youre curious about built-in collaboration. Whatever brought you here, this guide helps you move over your habits, shortcuts, and settings.
Well cover what to bring, what to change, and whats different. You can ease in gradually or switch all at once. Either way, youll stay productive.
## Install Zed
Zed is available on macOS, Windows, and Linux.
For macOS, you can download it from zed.dev/download, or install via Homebrew:
`brew install zed-editor/zed/zed`
For most Linux users, the easiest way to install Zed is through our installation script:
`curl -f https://zed.dev/install.sh | sh`
After installation, you can launch Zed from your Applications folder (macOS) or directly from the terminal (Linux) using:
`zed .`
This opens the current directory in Zed.
## Import Settings from VS Code
During setup, you have the option to import key settings from VS Code. Zed imports the following settings:
### Settings Imported from VS Code
The following VS Code settings are automatically imported when you use **Import Settings from VS Code**:
**Editor**
| VS Code Setting | Zed Setting |
| ------------------------------------------- | ---------------------------------------------- |
| `editor.fontFamily` | `buffer_font_family` |
| `editor.fontSize` | `buffer_font_size` |
| `editor.fontWeight` | `buffer_font_weight` |
| `editor.tabSize` | `tab_size` |
| `editor.insertSpaces` | `hard_tabs` (inverted) |
| `editor.wordWrap` | `soft_wrap` |
| `editor.wordWrapColumn` | `preferred_line_length` |
| `editor.cursorStyle` | `cursor_shape` |
| `editor.cursorBlinking` | `cursor_blink` |
| `editor.renderLineHighlight` | `current_line_highlight` |
| `editor.lineNumbers` | `gutter.line_numbers`, `relative_line_numbers` |
| `editor.showFoldingControls` | `gutter.folds` |
| `editor.minimap.enabled` | `minimap.show` |
| `editor.minimap.autohide` | `minimap.show` |
| `editor.minimap.showSlider` | `minimap.thumb` |
| `editor.minimap.maxColumn` | `minimap.max_width_columns` |
| `editor.stickyScroll.enabled` | `sticky_scroll.enabled` |
| `editor.scrollbar.horizontal` | `scrollbar.axes.horizontal` |
| `editor.scrollbar.vertical` | `scrollbar.axes.vertical` |
| `editor.mouseWheelScrollSensitivity` | `scroll_sensitivity` |
| `editor.fastScrollSensitivity` | `fast_scroll_sensitivity` |
| `editor.cursorSurroundingLines` | `vertical_scroll_margin` |
| `editor.hover.enabled` | `hover_popover_enabled` |
| `editor.hover.delay` | `hover_popover_delay` |
| `editor.parameterHints.enabled` | `auto_signature_help` |
| `editor.multiCursorModifier` | `multi_cursor_modifier` |
| `editor.selectionHighlight` | `selection_highlight` |
| `editor.roundedSelection` | `rounded_selection` |
| `editor.find.seedSearchStringFromSelection` | `seed_search_query_from_cursor` |
| `editor.rulers` | `wrap_guides` |
| `editor.renderWhitespace` | `show_whitespaces` |
| `editor.guides.indentation` | `indent_guides.enabled` |
| `editor.linkedEditing` | `linked_edits` |
| `editor.autoSurround` | `use_auto_surround` |
| `editor.formatOnSave` | `format_on_save` |
| `editor.formatOnPaste` | `auto_indent_on_paste` |
| `editor.formatOnType` | `use_on_type_format` |
| `editor.trimAutoWhitespace` | `remove_trailing_whitespace_on_save` |
| `editor.suggestOnTriggerCharacters` | `show_completions_on_input` |
| `editor.suggest.showWords` | `completions.words` |
| `editor.inlineSuggest.enabled` | `show_edit_predictions` |
**Files & Workspace**
| VS Code Setting | Zed Setting |
| --------------------------- | ------------------------------ |
| `files.autoSave` | `autosave` |
| `files.autoSaveDelay` | `autosave.milliseconds` |
| `files.insertFinalNewline` | `ensure_final_newline_on_save` |
| `files.associations` | `file_types` |
| `files.watcherExclude` | `file_scan_exclusions` |
| `files.watcherInclude` | `file_scan_inclusions` |
| `files.simpleDialog.enable` | `use_system_path_prompts` |
| `search.smartCase` | `use_smartcase_search` |
| `search.useIgnoreFiles` | `search.include_ignored` |
**Terminal**
| VS Code Setting | Zed Setting |
| ------------------------------------- | ----------------------------------- |
| `terminal.integrated.fontFamily` | `terminal.font_family` |
| `terminal.integrated.fontSize` | `terminal.font_size` |
| `terminal.integrated.lineHeight` | `terminal.line_height` |
| `terminal.integrated.cursorStyle` | `terminal.cursor_shape` |
| `terminal.integrated.cursorBlinking` | `terminal.blinking` |
| `terminal.integrated.copyOnSelection` | `terminal.copy_on_select` |
| `terminal.integrated.scrollback` | `terminal.max_scroll_history_lines` |
| `terminal.integrated.macOptionIsMeta` | `terminal.option_as_meta` |
| `terminal.integrated.{platform}Exec` | `terminal.shell` |
| `terminal.integrated.env.{platform}` | `terminal.env` |
**Tabs & Panels**
| VS Code Setting | Zed Setting |
| -------------------------------------------------- | -------------------------------------------------- |
| `workbench.editor.showTabs` | `tab_bar.show` |
| `workbench.editor.showIcons` | `tabs.file_icons` |
| `workbench.editor.tabActionLocation` | `tabs.close_position` |
| `workbench.editor.tabActionCloseVisibility` | `tabs.show_close_button` |
| `workbench.editor.focusRecentEditorAfterClose` | `tabs.activate_on_close` |
| `workbench.editor.enablePreview` | `preview_tabs.enabled` |
| `workbench.editor.enablePreviewFromQuickOpen` | `preview_tabs.enable_preview_from_file_finder` |
| `workbench.editor.enablePreviewFromCodeNavigation` | `preview_tabs.enable_preview_from_code_navigation` |
| `workbench.editor.editorActionsLocation` | `tab_bar.show_tab_bar_buttons` |
| `workbench.editor.limit.enabled` / `value` | `max_tabs` |
| `workbench.editor.restoreViewState` | `restore_on_file_reopen` |
| `workbench.statusBar.visible` | `status_bar.show` |
**Project Panel (File Explorer)**
| VS Code Setting | Zed Setting |
| ------------------------------ | ----------------------------------- |
| `explorer.compactFolders` | `project_panel.auto_fold_dirs` |
| `explorer.autoReveal` | `project_panel.auto_reveal_entries` |
| `explorer.excludeGitIgnore` | `project_panel.hide_gitignore` |
| `problems.decorations.enabled` | `project_panel.show_diagnostics` |
| `explorer.decorations.badges` | `project_panel.git_status` |
**Git**
| VS Code Setting | Zed Setting |
| ------------------------------------ | ---------------------------------------------- |
| `git.enabled` | `git_panel.button` |
| `git.defaultBranchName` | `git_panel.fallback_branch_name` |
| `git.decorations.enabled` | `git.inline_blame`, `project_panel.git_status` |
| `git.blame.editorDecoration.enabled` | `git.inline_blame.enabled` |
**Window & Behavior**
| VS Code Setting | Zed Setting |
| ------------------------------------------------ | ---------------------------------------- |
| `window.confirmBeforeClose` | `confirm_quit` |
| `window.nativeTabs` | `use_system_window_tabs` |
| `window.closeWhenEmpty` | `when_closing_with_no_tabs` |
| `accessibility.dimUnfocused.enabled` / `opacity` | `active_pane_modifiers.inactive_opacity` |
**Other**
| VS Code Setting | Zed Setting |
| -------------------------- | -------------------------------------------------------- |
| `http.proxy` | `proxy` |
| `npm.packageManager` | `node.npm_path` |
| `telemetry.telemetryLevel` | `telemetry.metrics`, `telemetry.diagnostics` |
| `outline.icons` | `outline_panel.file_icons`, `outline_panel.folder_icons` |
| `chat.agent.enabled` | `agent.enabled` |
| `mcp` | `context_servers` |
Zed doesnt import extensions or keybindings, but this is the fastest way to get a familiar feel while trying something new. If you skip that step during setup, you can still import settings manually later via the command palette:
`Cmd+Shift+P → Zed: Import VS Code Settings`
## Set Up Editor Preferences
You can also configure settings manually in the Settings Editor.
To edit your settings:
1. `Cmd+,` to open the Settings Editor.
2. Run `zed: open settings` in the Command Palette.
Heres how common VS Code settings translate:
| VS Code | Zed | Notes |
| --- | --- | --- |
| editor.fontFamily | buffer_font_family | Zed uses Zed Mono by default |
| editor.fontSize | buffer_font_size | Set in pixels |
| editor.tabSize | tab_size | Can override per language |
| editor.insertSpaces | insert_spaces | Boolean |
| editor.formatOnSave | format_on_save | Works with formatter enabled |
| editor.wordWrap | soft_wrap | Supports optional wrap column |
Zed also supports per-project settings. You can find these in the Settings Editor as well.
## Open or Create a Project
After setup, press `Cmd+O` (`Ctrl+O` on Linux) to open a folder. This becomes your workspace in Zed. There's no support for multi-root workspaces or `.code-workspace` files like in VS Code. Zed keeps it simple: one folder, one workspace.
To start a new project, create a directory using your terminal or file manager, then open it in Zed. The editor will treat that folder as the root of your project.
You can also launch Zed from the terminal inside any folder with:
`zed .`
Once inside a project, use `Cmd+P` to jump between files quickly. `Cmd+Shift+P` (`Ctrl+Shift+P` on Linux) opens the command palette for running actions / tasks, toggling settings, or starting a collaboration session.
Open buffers appear as tabs across the top. The sidebar shows your file tree and Git status. Collapse it with `Cmd+B` for a distraction-free view.
## Differences in Keybindings
If you chose the VS Code keymap during onboarding, you're likely good to go, and most of your shortcuts should already feel familiar.
Heres a quick reference guide for how our keybindings compare to what youre used to coming from VS Code.
### Common Shared Keybindings (Zed <> VS Code)
| Action | Shortcut |
| --------------------------- | ---------------------- |
| Find files | `Cmd + P` |
| Run a command | `Cmd + Shift + P` |
| Search text (project-wide) | `Cmd + Shift + F` |
| Find symbols (project-wide) | `Cmd + T` |
| Find symbols (file-wide) | `Cmd + Shift + O` |
| Toggle left dock | `Cmd + B` |
| Toggle bottom dock | `Cmd + J` |
| Open terminal | `Ctrl + ~` |
| Open file tree explorer | `Cmd + Shift + E` |
| Close current buffer | `Cmd + W` |
| Close whole project | `Cmd + Shift + W` |
| Refactor: rename symbol | `F2` |
| Change theme | `Cmd + K, Cmd + T` |
| Wrap text | `Opt + Z` |
| Navigate open tabs | `Cmd + Opt + Arrow` |
| Syntactic fold / unfold | `Cmd + Opt + {` or `}` |
### Different Keybindings (Zed <> VS Code)
| Action | VS Code | Zed |
| ------------------- | --------------------- | ---------------------- |
| Open recent project | `Ctrl + R` | `Cmd + Opt + O` |
| Move lines up/down | `Opt + Up/Down` | `Cmd + Ctrl + Up/Down` |
| Split panes | `Cmd + \` | `Cmd + K, Arrow Keys` |
| Expand Selection | `Shift + Alt + Right` | `Opt + Up` |
### Unique to Zed
| Action | Shortcut | Notes |
| ------------------- | ---------------------------- | ------------------------------------------------ |
| Toggle right dock | `Cmd + R` or `Cmd + Alt + B` | |
| Syntactic selection | `Opt + Up/Down` | Selects code by structure (e.g., inside braces). |
### How to Customize Keybindings
To edit your keybindings:
- Open the command palette (`Cmd+Shift+P`)
- Run `Zed: Open Keymap Editor`
This opens a list of all available bindings. You can override individual shortcuts, remove conflicts, or build a layout that works better for your setup.
Zed also supports chords (multi-key sequences) like `Cmd+K Cmd+C`, like VS Code does.
## Differences in User Interfaces
### No Workspace
VS Code uses a dedicated Workspace concept, with multi-root folders, `.code-workspace` files, and a clear distinction between “a window” and “a workspace.”
Zed simplifies this model.
In Zed:
- There is no workspace file format. Opening a folder is your project context.
- Zed does not support multi-root workspaces. You can only open one folder at a time in a window.
- Most project-level behavior is scoped to the folder you open. Search, Git integration, tasks, and environment detection all treat the opened directory as the project root.
- Per-project settings are optional. You can add a `.zed/settings.json` file inside a project to override global settings, but Zed does not use `.code-workspace` files and wont import them.
- You can start from a single file or an empty window. Zed doesnt require you to open a folder to begin editing.
The result is a simpler model:
Open a folder → work inside that folder → no additional workspace layer.
### Navigating in a Project
In VS Code, the standard entry point is opening a folder. From there, the left-hand sidebar is central to your navigation.
Zed takes a different approach:
- You can still open folders, but you dont need to. Opening a single file or even starting with an empty workspace is valid.
- The Command Palette (`Cmd+Shift+P`) and File Finder (`Cmd+P`) are your primary navigation tools. The File Finder searches across the entire workspace instantly; files, symbols, commands, even teammates if you're collaborating.
- Instead of a persistent sidebar, Zed encourages you to:
- Fuzzy-find files by name (`Cmd+P`)
- Jump directly to symbols (`Cmd+Shift+O`)
- Use split panes and tabs for context, rather than keeping a large file tree open (though you can do this with the Project Panel if you prefer).
The UI is intentionally minimal. Panels slide in only when needed, then get out of your way. The focus is on flowing between code instead of managing panes.
### Extensions vs. Marketplace
Zed does not offer as many extensions as VS Code. The available extensions are focused on language support, themes, syntax highlighting, and other core editing enhancements.
However there are several features that typically require extensions in VS Code which we built directly into Zed:
- Real-time collaboration with voice and cursor sharing (no Live Share required)
- AI coding assistance (no Copilot extension needed)
- Built-in terminal panel
- Project-wide fuzzy search
- Task runner with JSON config
- Inline diagnostics and code actions via LSP
You wont find one-to-one replacements for every VS Code extension, especially if you rely on tools for DevOps, containers, or test runners. Zed's extension ecosystem is still growing, and the catalog is smaller by design.
### Collaboration in Zed vs. VS Code
Unlike VS Code, Zed doesnt require an extension to collaborate. Its built into the core experience.
- Open the Collab Panel in the left dock.
- Create a channel and [invite your collaborators](https://zed.dev/docs/collaboration#inviting-a-collaborator) to join.
- [Share your screen or your codebase](https://zed.dev/docs/collaboration#share-a-project) directly.
Once connected, youll see each other's cursors, selections, and edits in real time. Voice chat is included, so you can talk as you work. Theres no need for separate tools or third-party logins. Zeds collaboration is designed for everything from quick pair programming to longer team sessions.
Learn how [Zed uses Zed](https://zed.dev/blog/zed-is-our-office) to plan work and collaborate.
### Using AI in Zed
If youre used to GitHub Copilot in VS Code, you can do the same in Zed. You can also explore other agents through Zed Pro, or bring your own keys and connect without authentication. Zed is designed to enable many options for using AI, including disabling it entirely.
#### Configuring GitHub Copilot
You should be able to sign-in to GitHub Copilot by clicking on the Zeta icon in the status bar and following the setup instructions.
You can also add this to your settings:
```json
{
"features": {
"edit_prediction_provider": "copilot"
}
}
```
To invoke completions, just start typing. Zed will offer suggestions inline for you to accept.
#### Additional AI Options
To use other AI models in Zed, you have several options:
- Use Zeds hosted models, with higher rate limits. Requires [authentication](https://zed.dev/docs/accounts.html) and subscription to [Zed Pro](https://zed.dev/docs/ai/subscription.html).
- Bring your own [API keys](https://zed.dev/docs/ai/llm-providers.html), no authentication needed
- Use [external agents like Claude Code](https://zed.dev/docs/ai/external-agents.html).
### Advanced Config and Productivity Tweaks
Zed exposes advanced settings for power users who want to fine-tune their environment.
Here are a few useful tweaks:
**Format on Save:**
```json
"format_on_save": "on"
```
**Enable direnv support:**
```json
"load_direnv": "shell_hook"
```
**Custom Tasks**: Define build or run commands in your `tasks.json` (accessed via command palette: `zed: open tasks`):
```json
[
{
"label": "build",
"command": "cargo build"
}
]
```
**Bring over custom snippets**
Copy your VS Code snippet JSON directly into Zed's snippets folder (`zed: configure snippets`).

View File

@@ -1,4 +1,4 @@
# Generated from xtask::workflows:: within the Zed repository.extensions::bump_version
# Generated from xtask::workflows::extensions::bump_version within the Zed repository.
# Rebuild with `cargo xtask workflows`.
name: extensions::bump_version
on:
@@ -36,9 +36,7 @@ jobs:
call_bump_version:
needs:
- determine_bump_type
if: |-
(github.event.action == 'labeled' && needs.determine_bump_type.outputs.bump_type != 'patch') ||
github.event_name == 'push'
if: github.event.action != 'labeled' || needs.determine_bump_type.outputs.bump_type != 'patch'
uses: zed-industries/zed/.github/workflows/extension_bump.yml@main
secrets:
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
@@ -46,3 +44,6 @@ jobs:
with:
bump-type: ${{ needs.determine_bump_type.outputs.bump_type }}
force-bump: true
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}labels
cancel-in-progress: true

View File

@@ -1,4 +1,4 @@
# Generated from xtask::workflows:: within the Zed repository.extensions::release_version
# Generated from xtask::workflows::extensions::release_version within the Zed repository.
# Rebuild with `cargo xtask workflows`.
name: extensions::release_version
on:

View File

@@ -1,4 +1,4 @@
# Generated from xtask::workflows:: within the Zed repository.extensions::run_tests
# Generated from xtask::workflows::extensions::run_tests within the Zed repository.
# Rebuild with `cargo xtask workflows`.
name: extensions::run_tests
on:
@@ -9,5 +9,5 @@ jobs:
call_extension_tests:
uses: zed-industries/zed/.github/workflows/extension_tests.yml@main
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}pr
cancel-in-progress: true

View File

@@ -4,7 +4,7 @@ bash -euo pipefail
source script/lib/blob-store.sh
bucket_name="zed-nightly-host"
version=$(./script/get-crate-version zed)-"${GITHUB_RUN_NUMBER}+${GITHUB_SHA}"
version=$(./script/get-crate-version zed)+nightly."${GITHUB_RUN_NUMBER}.${GITHUB_SHA}"
for file_to_upload in ./release-artifacts/*; do
[ -f "$file_to_upload" ] || continue

View File

@@ -13,7 +13,7 @@ Write-Host "Uploading nightly for target: $target"
$bucketName = "zed-nightly-host"
$releaseVersion = & "$PSScriptRoot\get-crate-version.ps1" zed
$version = "$releaseVersion-$env:GITHUB_RUN_NUMBER+$env:GITHUB_SHA"
$version = "$releaseVersion+nightly.$env:GITHUB_RUN_NUMBER.$env:GITHUB_SHA"
# TODO:
# Upload remote server files

View File

@@ -82,10 +82,10 @@ impl WorkflowType {
"# Generated from xtask::workflows::{}{}\n",
"# Rebuild with `cargo xtask workflows`.",
),
workflow_name,
matches!(self, WorkflowType::Extensions)
.then_some(" within the Zed repository.")
.unwrap_or_default(),
workflow_name
)
}

View File

@@ -5,22 +5,14 @@ use crate::tasks::workflows::{
extension_release::extension_workflow_secrets,
extension_tests::{self},
runners,
steps::{self, CommonJobConditions, DEFAULT_REPOSITORY_OWNER_GUARD, NamedJob, named},
steps::{
self, CommonJobConditions, DEFAULT_REPOSITORY_OWNER_GUARD, FluentBuilder, NamedJob, named,
},
vars::{
JobOutput, StepOutput, WorkflowInput, WorkflowSecret, one_workflow_per_non_main_branch,
},
};
const BUMPVERSION_CONFIG: &str = indoc! {r#"
[bumpversion]
current_version = "$OLD_VERSION"
[bumpversion:file:Cargo.toml]
[bumpversion:file:extension.toml]
"#
};
const VERSION_CHECK: &str = r#"sed -n 's/version = \"\(.*\)\"/\1/p' < extension.toml"#;
// This is used by various extensions repos in the zed-extensions org to bump extension versions.
@@ -113,7 +105,7 @@ fn create_version_label(
app_id: &WorkflowSecret,
app_secret: &WorkflowSecret,
) -> NamedJob {
let (generate_token, generated_token) = generate_token(app_id, app_secret);
let (generate_token, generated_token) = generate_token(app_id, app_secret, None);
let job = steps::dependant_job(dependencies)
.cond(Expression::new(format!(
"{DEFAULT_REPOSITORY_OWNER_GUARD} && github.event_name == 'push' && github.ref == 'refs/heads/main' && {} == 'false'",
@@ -193,7 +185,7 @@ fn bump_extension_version(
app_id: &WorkflowSecret,
app_secret: &WorkflowSecret,
) -> NamedJob {
let (generate_token, generated_token) = generate_token(app_id, app_secret);
let (generate_token, generated_token) = generate_token(app_id, app_secret, None);
let (bump_version, new_version) = bump_version(current_version, bump_type);
let job = steps::dependant_job(dependencies)
@@ -216,13 +208,24 @@ fn bump_extension_version(
pub(crate) fn generate_token(
app_id: &WorkflowSecret,
app_secret: &WorkflowSecret,
repository_target: Option<RepositoryTarget>,
) -> (Step<Use>, StepOutput) {
let step = named::uses("actions", "create-github-app-token", "v2")
.id("generate-token")
.add_with(
Input::default()
.add("app-id", app_id.to_string())
.add("private-key", app_secret.to_string()),
.add("private-key", app_secret.to_string())
.when_some(
repository_target,
|input,
RepositoryTarget {
owner,
repositories,
}| {
input.add("owner", owner).add("repositories", repositories)
},
),
);
let generated_token = StepOutput::new(&step, "token");
@@ -239,20 +242,23 @@ fn bump_version(current_version: &JobOutput, bump_type: &WorkflowInput) -> (Step
indoc! {r#"
OLD_VERSION="{}"
cat <<EOF > .bumpversion.cfg
{}
EOF
BUMP_FILES=("extension.toml")
if [[ -f "Cargo.toml" ]]; then
BUMP_FILES+=("Cargo.toml")
fi
bump2version --verbose --current-version "$OLD_VERSION" --no-configured-files {} "${{BUMP_FILES[@]}}"
if [[ -f "Cargo.toml" ]]; then
cargo update --workspace
fi
bump2version --verbose {}
NEW_VERSION="$({})"
cargo update --workspace
rm .bumpversion.cfg
echo "new_version=${{NEW_VERSION}}" >> "$GITHUB_OUTPUT"
"#
},
current_version, BUMPVERSION_CONFIG, bump_type, VERSION_CHECK
current_version, bump_type, VERSION_CHECK
))
.id("bump-version");
@@ -288,3 +294,17 @@ fn create_pull_request(new_version: StepOutput, generated_token: StepOutput) ->
.add("sign-commits", true),
)
}
pub(crate) struct RepositoryTarget {
owner: String,
repositories: String,
}
impl RepositoryTarget {
pub fn new<T: ToString>(owner: T, repositories: &[&str]) -> Self {
Self {
owner: owner.to_string(),
repositories: repositories.join("\n"),
}
}
}

View File

@@ -2,7 +2,7 @@ use gh_workflow::{Event, Job, Run, Step, Use, Workflow, WorkflowCall};
use indoc::indoc;
use crate::tasks::workflows::{
extension_bump::generate_token,
extension_bump::{RepositoryTarget, generate_token},
runners,
steps::{CommonJobConditions, NamedJob, checkout_repo, named},
vars::{StepOutput, WorkflowSecret},
@@ -26,7 +26,9 @@ pub(crate) fn extension_release() -> Workflow {
}
fn create_release(app_id: &WorkflowSecret, app_secret: &WorkflowSecret) -> NamedJob {
let (generate_token, generated_token) = generate_token(&app_id, &app_secret);
let extension_registry = RepositoryTarget::new("zed-industries", &["extensions"]);
let (generate_token, generated_token) =
generate_token(&app_id, &app_secret, Some(extension_registry));
let (get_extension_id, extension_id) = get_extension_id();
let job = Job::default()

View File

@@ -8,7 +8,6 @@ use crate::tasks::workflows::{
vars::{PathCondition, StepOutput, one_workflow_per_non_main_branch},
};
const RUN_TESTS_INPUT: &str = "run_tests";
pub(crate) const ZED_EXTENSION_CLI_SHA: &str = "7cfce605704d41ca247e3f84804bf323f6c6caaf";
// This is used by various extensions repos in the zed-extensions org to run automated tests.
@@ -27,17 +26,7 @@ pub(crate) fn extension_tests() -> Workflow {
let tests_pass = tests_pass(&jobs);
named::workflow()
.add_event(
Event::default().workflow_call(WorkflowCall::default().add_input(
RUN_TESTS_INPUT,
WorkflowCallInput {
description: "Whether the workflow should run rust tests".into(),
required: true,
input_type: "boolean".into(),
default: None,
},
)),
)
.add_event(Event::default().workflow_call(WorkflowCall::default()))
.concurrency(one_workflow_per_non_main_branch())
.add_env(("CARGO_TERM_COLOR", "always"))
.add_env(("RUST_BACKTRACE", 1))
@@ -65,13 +54,9 @@ fn check_rust() -> NamedJob {
.add_step(steps::cache_rust_dependencies_namespace())
.add_step(steps::cargo_fmt())
.add_step(run_clippy())
.add_step(steps::cargo_install_nextest())
.add_step(
steps::cargo_install_nextest()
.if_condition(Expression::new(format!("inputs.{RUN_TESTS_INPUT}"))),
)
.add_step(
steps::cargo_nextest(runners::Platform::Linux)
.if_condition(Expression::new(format!("inputs.{RUN_TESTS_INPUT}"))),
steps::cargo_nextest(runners::Platform::Linux).add_env(("NEXTEST_NO_TESTS", "warn")),
);
named::job(job)
@@ -82,7 +67,7 @@ pub(crate) fn check_extension() -> NamedJob {
let job = Job::default()
.with_repository_owner_guard()
.runs_on(runners::LINUX_SMALL)
.timeout_minutes(1u32)
.timeout_minutes(2u32)
.add_step(steps::checkout_repo())
.add_step(cache_download)
.add_step(download_zed_extension_cli(cache_hit))

View File

@@ -7,7 +7,7 @@ use indoc::indoc;
use crate::tasks::workflows::{
runners,
steps::{NamedJob, named},
vars::{self, JobOutput, StepOutput},
vars::{self, JobOutput, StepOutput, one_workflow_per_non_main_branch_and_token},
};
pub(crate) fn bump_version() -> Workflow {
@@ -20,6 +20,7 @@ pub(crate) fn bump_version() -> Workflow {
.on(Event::default()
.push(Push::default().add_branch("main"))
.pull_request(PullRequest::default().add_type(PullRequestType::Labeled)))
.concurrency(one_workflow_per_non_main_branch_and_token("labels"))
.add_job(determine_bump_type.name, determine_bump_type.job)
.add_job(call_bump_version.name, call_bump_version.job)
}
@@ -30,10 +31,7 @@ pub(crate) fn call_bump_version(
) -> NamedJob<UsesJob> {
let job = Job::default()
.cond(Expression::new(format!(
indoc! {
"(github.event.action == 'labeled' && {} != 'patch') ||
github.event_name == 'push'"
},
"github.event.action != 'labeled' || {} != 'patch'",
bump_type.expr()
)))
.uses(

View File

@@ -2,14 +2,14 @@ use gh_workflow::{Event, Job, PullRequest, UsesJob, Workflow};
use crate::tasks::workflows::{
steps::{NamedJob, named},
vars::one_workflow_per_non_main_branch,
vars::one_workflow_per_non_main_branch_and_token,
};
pub(crate) fn run_tests() -> Workflow {
let call_extension_tests = call_extension_tests();
named::workflow()
.on(Event::default().pull_request(PullRequest::default().add_branch("**")))
.concurrency(one_workflow_per_non_main_branch())
.concurrency(one_workflow_per_non_main_branch_and_token("pr"))
.add_job(call_extension_tests.name, call_extension_tests.job)
}

View File

@@ -7,7 +7,7 @@ use crate::tasks::workflows::{
nix_build::build_nix,
runners::Arch,
steps::{BASH_SHELL, CommonJobConditions, repository_owner_guard_expression},
vars::PathCondition,
vars::{self, PathCondition},
};
use super::{
@@ -353,7 +353,9 @@ pub(crate) fn check_postgres_and_protobuf_migrations() -> NamedJob {
}
fn bufbuild_setup_action() -> Step<Use> {
named::uses("bufbuild", "buf-setup-action", "v1").add_with(("version", "v1.29.0"))
named::uses("bufbuild", "buf-setup-action", "v1")
.add_with(("version", "v1.29.0"))
.add_with(("github_token", vars::GITHUB_TOKEN))
}
fn bufbuild_breaking_action() -> Step<Use> {

View File

@@ -180,6 +180,7 @@ pub(crate) fn dependant_job(deps: &[&NamedJob]) -> Job {
impl FluentBuilder for Job {}
impl FluentBuilder for Workflow {}
impl FluentBuilder for Input {}
/// A helper trait for building complex objects with imperative conditionals in a fluent style.
/// Copied from GPUI to avoid adding GPUI as dependency

View File

@@ -80,8 +80,18 @@ pub fn bundle_envs(platform: Platform) -> Env {
}
pub fn one_workflow_per_non_main_branch() -> Concurrency {
one_workflow_per_non_main_branch_and_token("")
}
pub fn one_workflow_per_non_main_branch_and_token<T: AsRef<str>>(token: T) -> Concurrency {
Concurrency::default()
.group("${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}")
.group(format!(
concat!(
"${{{{ github.workflow }}}}-${{{{ github.ref_name }}}}-",
"${{{{ github.ref_name == 'main' && github.sha || 'anysha' }}}}{}"
),
token.as_ref()
))
.cancel_in_progress(true)
}