Compare commits

..

41 Commits

Author SHA1 Message Date
Nate Butler
02d218ca6a Order matters I guesss 2025-03-13 12:00:53 -04:00
Nate Butler
8e1af73ae3 clippy 2025-03-13 11:53:33 -04:00
Nate Butler
c70becb594 Allow pages in component preview to scroll 2025-03-13 11:33:41 -04:00
Antonio Scandurra
8cf5af1a84 Introduce DiagnosticsTool (#26670)
Release Notes:

- N/A
2025-03-13 14:53:00 +01:00
Albin Kocheril Chacko
247ee880d2 Fix typo in default.json (#26666)
minor typo fix

Release Notes:

- N/A
2025-03-13 13:39:28 +00:00
Nate Butler
2e217759c0 gruvbox: version_control_ -> version_control. (#26665)
Missed this in PR #26606 

Before:

![CleanShot 2025-03-13 at 08 58
59@2x](https://github.com/user-attachments/assets/021df4b1-5a70-4fae-a109-9b8bb35949e3)

After:

![CleanShot 2025-03-13 at 08 59
22@2x](https://github.com/user-attachments/assets/01dca26d-77ec-4a54-8b7c-aa2fb160ff7d)

Release Notes:

- theme: Fixed an issue where version control colors weren't applying
correctly. (again)
2025-03-13 13:13:35 +00:00
Danilo Leal
0a0c163692 assistant2: Use icons for tool call status communication (#26617)
It was hard to catch the running & pending states, though. When running,
it will appear as a spinning arrow circle icon.

<img
src="https://github.com/user-attachments/assets/dbf1bc0a-6fa3-41c6-bcd7-2226e89c87b4"
width="500px" />

Release Notes:

- N/A
2025-03-13 10:01:20 -03:00
Antonio Scandurra
e80df25386 Iterate on tools some more (#26663)
Release Notes:

- N/A
2025-03-13 12:42:02 +00:00
Danilo Leal
d9590f3f0e docs: Improve introduction to Edit Prediction (#26620)
As I was writing a blog post about Edit Prediction, I realized we didn't
have a great section in the docs I could link to talking about
configuring it. We weren't: 1) explicitly exposing the settings code to
add Zed as the edit prediction provider, and 2) not showing an image of
the title bar banner.

Release Notes:

- N/A
2025-03-13 09:03:49 -03:00
Antonio Scandurra
4ecd1b5174 Fix bad cd sometimes used by BashTool and set edit model temperature to 0 (#26656)
Release Notes:

- N/A
2025-03-13 10:47:00 +00:00
Antonio Scandurra
70c973f6c3 Fix issues in EditFilesTool, ListDirectoryTool and BashTool (#26647)
Release Notes:

- N/A
2025-03-13 09:41:27 +00:00
Stanislav Alekseev
e842b4eade macOS: Disable automatic window tabbing in fullscreen mode (#26600)
Fixes #26534 (this time for real)

Release Notes:

- Fixed issue where Zed would behave weirdly when opening new fullscreen
windows by disabling window tabbing

Apple docs:
https://developer.apple.com/documentation/appkit/nswindow/allowsautomaticwindowtabbing
2025-03-13 12:45:01 +05:30
Agus Zubiaga
606aa7a78c Edit tool debugging (#26637)
Adds an `debug: edit tool` action that opens a new view which will help
us debug the edit tool internals. As the edit tool runs, the log
displays:

- Instructions provided by the main model
- Response stream from the editor model
- Parsed edit blocks
- Tool output provided back to main model

The log automatically records all edit tool interactions for staff, so
if you notice something weird, you can debug it retroactively without
having to open the debug tool first. We may want to limit the number of
recorded requests later.

I have a few more ideas for it, but this seems like a good starting
point.


https://github.com/user-attachments/assets/c61f5ce8-08b1-4500-accb-db2a480eb3ab


Release Notes:

- N/A
2025-03-13 04:03:01 +00:00
Mikayla Maki
0081b816fe Fix a bug where the modal layer could not be dismissed by the mouse 2025-03-12 16:44:16 -07:00
Peter Tripp
21949bcf1a ci: Fix tests not-running on main (#26613)
Follow-up to #26551 

Fix for tests being skipped on main.
Also fetch less history: [example
run](https://github.com/zed-industries/zed/actions/runs/13822318758/job/38670334893)

Release Notes:

- N/A
2025-03-12 19:16:23 -04:00
renovate[bot]
ee7ed6d5b8 Update Rust crate anyhow to v1.0.97 (#26576)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [anyhow](https://redirect.github.com/dtolnay/anyhow) |
workspace.dependencies | patch | `1.0.96` -> `1.0.97` |

---

### Release Notes

<details>
<summary>dtolnay/anyhow (anyhow)</summary>

###
[`v1.0.97`](https://redirect.github.com/dtolnay/anyhow/releases/tag/1.0.97)

[Compare
Source](https://redirect.github.com/dtolnay/anyhow/compare/1.0.96...1.0.97)

-   Documentation improvements

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xOTQuMSIsInVwZGF0ZWRJblZlciI6IjM5LjE5NC4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-03-12 23:01:04 +00:00
Marshall Bowers
07b67c1bd3 assistant2: Add ability to enable/disable all tools from a context server (#26610)
This PR adds an option to enable/disable all tools from a specific
context server:

<img width="1297" alt="Screenshot 2025-03-12 at 5 55 45 PM"
src="https://github.com/user-attachments/assets/af6c169e-0462-4a99-9bec-48fbf83dd08a"
/>

Release Notes:

- N/A
2025-03-12 22:14:31 +00:00
Mikayla Maki
f116b44ae8 Rename the editor::ToggleGitBlame action to git::Blame (#26565)
Release Notes:

- Git Beta: Renamed `editor::ToggleGitBlame` to `git::Blame`

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Cole Miller <m@cole-miller.net>
Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-03-12 22:12:42 +00:00
Nate Butler
43ab7fe0e2 theme: Fix incorrect version control keys in One themes (#26606)
While the `.{variants}` of the theme keys _were_ incorrect, they are
actually more consistent with our current theme keys (thanks AI!) So we
will keep theme, and fix the incorrect usages in the one themes and
elsewhere.

Old description:
> 
> This PR fixes an issue where we specified the incorrect theme keys
(thanks AI!) > in the theme schema. The following keys have been changed
to their correct > versions:
> 
> | Before                        | After                   |
> |-------------------------------|-------------------------|
> | version_control.added         | version_control_added   |
> | version_control.deleted       | version_control_deleted |
> | version_control.modified      | version_control_modified|
> | version_control.renamed       | version_control_renamed |
> | version_control.conflict      | version_control_conflict|
> | version_control.ignored       | version_control_ignored |
> 
> Please use the after versions in your themes, as they are correct! 
> 
> We won't be adding secondary keys to fix this automatically as git
only > officially launched today.
> 
> Due to this change, we've also updated the version control keys in the
One > themes to keep the default diff hunks looks from changing.

Closes #26572

Release Notes:

- theme: Fixed an issue where version control colors weren't applying
correctly.
2025-03-12 22:07:04 +00:00
Richard Feldman
6044773043 Add path search glob tool (#26567)
<img width="638" alt="Screenshot 2025-03-12 at 1 33 31 PM"
src="https://github.com/user-attachments/assets/f29b9dae-59eb-4d7a-bc26-aa4721cb829a"
/>

Release Notes:

- N/A
2025-03-12 22:00:54 +00:00
Conrad Irwin
81af2c0bed Fix overflow in create branch label (#26591)
Closes #ISSUE

Release Notes:

- N/A
2025-03-12 21:55:31 +00:00
Peter Tripp
ab199fda47 ci: GitHub actions refactor (#26551)
Refactor GitHub actions CI workflow.
- Single combined 'tests_pass' action so we only need one mandatory
check for merge queue
- Add new `job_spec` job which determines what needs to be run (+5secs)
  - Do not run full CI for docs only changes (~30secs vs 10+mins)
- Only run `script/generate-licenses` if Cargo.lock changed (saves
~23secs on mac_test)
- Move prettier /docs check to ci.yml and remove docs.yml 
- Run Windows tests on every PR commit
- Added new Windows runners named to reflect their OS/capacity
(windows-2025-64, windows-2025-32, windows-2025-16)

Release Notes:

- N/A
2025-03-12 17:32:38 -04:00
Marshall Bowers
e60e8f3a0a assistant_tool: Reduce locking in ToolWorkingSet (#26605)
This PR updates the `ToolWorkingSet` to reduce the amount of locking we
need to do.

A number of the methods have had corresponding versions moved to the
`ToolWorkingSetState` so that we can take out the lock once and do a
number of operations without needing to continually acquire and release
the lock.

Release Notes:

- N/A
2025-03-12 21:26:26 +00:00
brian tan
edeed7b619 workspace::Open: Highlight fuzzy matches (#26320)
Partial: https://github.com/zed-industries/zed/issues/15398

Changes:
Adds highlighting to the matches when using `"use_system_path_prompts":
false`

| before | after |
|---|---|

|![image](https://github.com/user-attachments/assets/60a385a0-abb0-49c5-935c-e71149161562)|![image](https://github.com/user-attachments/assets/d66ce980-cea9-4c22-8e6a-9720344be39a)|

Release Notes:

- N/A
2025-03-12 22:54:38 +02:00
Richard Feldman
9be7934f12 Add Bash tool (#26597)
<img width="636" alt="Screenshot 2025-03-12 at 4 24 18 PM"
src="https://github.com/user-attachments/assets/6f317031-f495-4a5a-8260-79a56b10d628"
/>

<img width="634" alt="Screenshot 2025-03-12 at 4 24 36 PM"
src="https://github.com/user-attachments/assets/27283432-4f94-49f3-9d61-a0a9c737de40"
/>


Release Notes:

- N/A
2025-03-12 20:51:29 +00:00
Peter Tripp
009b90291e Fix formatting in linux.md (#26598)
Merge queue did not require docs tests to pass:
-
https://github.com/zed-industries/zed/actions/runs/13820880465/job/38665664419

This will be fixed with:
- https://github.com/zed-industries/zed/pull/26551

cc: @ConradIrwin 

Release Notes:

- N/A
2025-03-12 16:33:11 -04:00
Michael Kaplan
8b17dc66f6 docs: Document linker issue & workarounds with GCC >= 14 (#26579)
Closes #24880

documents issues with aws-lc-rs and gcc >=14 on linux and provides a
workaround until the issues are fixed in aws-lc-rs
2025-03-12 20:26:08 +00:00
Conrad Irwin
de07b712fd Fix message on push (#26588)
Instead of saying "Successfully pushed new branch" we say "Pushed x to
y"

Release Notes:

- N/A
2025-03-12 20:18:28 +00:00
Richard Feldman
be8f3b3791 Add delete-path tool (#26590)
Release Notes:

- N/A
2025-03-12 20:16:26 +00:00
Richard Feldman
3131b0459f Return which files were touched in the edit tool (#26564)
<img width="631" alt="Screenshot 2025-03-12 at 12 56 43 PM"
src="https://github.com/user-attachments/assets/9ab84a53-829a-4943-ae76-b1d97ee31f55"
/>

<img width="908" alt="Screenshot 2025-03-12 at 12 57 12 PM"
src="https://github.com/user-attachments/assets/bd246231-6c92-4266-b61e-5293adfe2ba0"
/>

Release Notes:

- N/A
2025-03-12 15:56:23 -04:00
Marshall Bowers
3ec323ce0d uiua: Extract to zed-extensions/uiua repository (#26587)
This PR extracts the Uiua extension to the
[zed-extensions/uiua](https://github.com/zed-extensions/uiua)
repository.

Release Notes:

- N/A
2025-03-12 19:55:37 +00:00
Conrad Irwin
c8b782d870 git: Hard wrap in editor (#26507)
This adds the ability for the editor to implement hard wrap (similar to
"textwidth" in vim).

If you are typing and your line extends beyond the limit, a newline is
inserted before the most recent space on the line. If you are otherwise
editing the line, pasting, etc. then you will need to manually rewrap.

Release Notes:

- git: Commit messages are now wrapped "as you type" to 72 characters.
2025-03-12 13:48:13 -06:00
Conrad Irwin
7bca15704b Git on main thread (#26573)
This moves spawning of the git subprocess to the main thread. We're not
yet
sure why, but when we spawn a process using GCD's background queues,
sub-processes like git-credential-manager fail to open windows.

This seems to be fixable either by using the main thread, or by using a
standard background thread,
but for now we use the main thread.


Release Notes:

- Git: Fix git-credential-manager

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
2025-03-12 19:39:30 +00:00
Kirill Bulatov
5268e74315 Properly handle goto single file worktrees during terminal cmd-clicks (#26582)
Closes https://github.com/zed-industries/zed/issues/26431
Follow-up of https://github.com/zed-industries/zed/pull/26174

`path_with_position.path.strip_prefix(&worktree_root)` used in the PR is
wrong for cases of single-file worktrees, where it will return empty
paths that will result in incorrect project and FS entries accessed.

Release Notes:

- Fixed goto single file worktrees during terminal cmd-clicks
2025-03-12 19:38:21 +00:00
Kirill Bulatov
91c209900b Support word-based completions (#26410)
Closes https://github.com/zed-industries/zed/issues/4957


https://github.com/user-attachments/assets/ff491378-376d-48ec-b552-6cc80f74200b

Adds `"completions"` language settings section, to configure LSP and
word completions per language.
Word-based completions may be turned on never, always (returned along
with the LSP ones), and as a fallback if no LSP completion items were
returned.

Future work:

* words are matched with the same fuzzy matching code that the rest of
the completions are

This might worsen the completion menu's usability even more, and will
require work on better completion sorting.

* completion entries currently have no icons or other ways to indicate
those are coming from LSP or from word search, or from something else

* we may work with language scopes more intelligently, group words by
them and distinguish during completions

Release Notes:

- Supported word-based completions

---------

Co-authored-by: Max Brunsfeld <max@zed.dev>
2025-03-12 21:27:10 +02:00
Anthony Eid
74c29f1818 Fix unstage/stage in project diff not working when git panel isn't open (#26575)
Closes #ISSUE

Release Notes:

- Fix Bug where unstage/stage all in project diff wouldn't work while
git panel was closed

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-03-12 19:07:51 +00:00
Marshall Bowers
5858e61327 purescript: Extract to zed-extensions/purescript repository (#26571)
This PR extracts the PureScript extension to the
[zed-extensions/purescript](https://github.com/zed-extensions/purescript)
repository.

Release Notes:

- N/A
2025-03-12 18:42:12 +00:00
Martim Aires de Sousa
21cf2e38c5 Fix pane magnification causing mouse to drag tabs unexpectedly (#26383)
Previously, if a user clicked a button and moved the cursor out before
releasing, the click event was correctly prevented, but the pending
mouse-down state remained.
This caused unintended drags when the UI shifted due to magnification
settings.

Now, mouse-up clears the pending state:
- If over the button → clear state and trigger click handlers.
- If outside the button → clear state without triggering a click.

This avoids accidental drags while preserving expected click behavior.

Closes #24600

Release Notes:

- N/A

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-03-12 13:32:42 -05:00
Marshall Bowers
a3ca5554fd zig: Extract to zed-extensions/zig repository (#26569)
This PR extracts the Zig extension to the
[zed-extensions/zig](https://github.com/zed-extensions/zig) repository.

Release Notes:

- N/A
2025-03-12 18:28:26 +00:00
Marshall Bowers
acf9b22466 extension: Add ExtensionEvents for listening to extension-related events (#26562)
This PR adds a new `ExtensionEvents` event bus that can be used to
listen for extension-related events throughout the app.

Today you need to have a handle to the `ExtensionStore` (which entails
depending on `extension_host`) in order to listen for extension events.

With this change subscribers only need to depend on `extension`, which
has a leaner dependency graph.

Release Notes:

- N/A
2025-03-12 17:01:52 +00:00
Joseph T. Lyons
ffcd023f83 Bump Zed to v0.179 (#26563)
Release Notes:

-N/A
2025-03-12 12:53:37 -04:00
100 changed files with 2470 additions and 2242 deletions

View File

@@ -366,8 +366,7 @@ jobs:
if: |
github.repository_owner == 'zed-industries' &&
needs.job_spec.outputs.run_tests == 'true'
# Use bigger runners for PRs (speed); smaller for async (cost)
runs-on: ${{ github.event_name == 'pull_request' && 'windows-2025-32' || 'windows-2025-16' }}
runs-on: windows-2025-64
steps:
# more info here:- https://github.com/rust-lang/cargo/issues/13020
- name: Enable longer pathnames for git
@@ -432,28 +431,22 @@ jobs:
- macos_tests
- windows_clippy
- windows_tests
if: always()
if: |
always() && (
needs.style.result == 'success'
&& (
needs.job_spec.outputs.run_tests == 'false'
|| (needs.macos_tests.result == 'success'
&& needs.linux_tests.result == 'success'
&& needs.windows_tests.result == 'success'
&& needs.windows_clippy.result == 'success'
&& needs.build_remote_server.result == 'success'
&& needs.migration_checks.result == 'success')
)
)
steps:
- name: Check all tests passed
run: |
# Check dependent jobs...
RET_CODE=0
# Always check style
[[ "${{ needs.style.result }}" != 'success' ]] && { RET_CODE=1; echo "style tests failed"; }
# Only check test jobs if they were supposed to run
if [[ "${{ needs.job_spec.outputs.run_tests }}" == "true" ]]; then
[[ "${{ needs.macos_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "macOS tests failed"; }
[[ "${{ needs.linux_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "Linux tests failed"; }
[[ "${{ needs.windows_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "Windows tests failed"; }
[[ "${{ needs.windows_clippy.result }}" != 'success' ]] && { RET_CODE=1; echo "Windows clippy failed"; }
[[ "${{ needs.migration_checks.result }}" != 'success' ]] && { RET_CODE=1; echo "Migration checks failed"; }
[[ "${{ needs.build_remote_server.result }}" != 'success' ]] && { RET_CODE=1; echo "Remote server build failed"; }
fi
if [[ "$RET_CODE" -eq 0 ]]; then
echo "All tests passed successfully!"
fi
exit $RET_CODE
- name: All tests passed
run: echo "All tests passed successfully!"
bundle-mac:
timeout-minutes: 120

View File

@@ -17,7 +17,7 @@ jobs:
else
URL="https://zed.dev/releases/stable/latest"
fi
echo "URL=$URL" >> $GITHUB_OUTPUT
echo "::set-output name=URL::$URL"
- name: Get content
uses: 2428392/gh-truncate-string-action@b3ff790d21cf42af3ca7579146eedb93c8fb0757 # v1.4.1
id: get-content
@@ -33,33 +33,3 @@ jobs:
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
content: ${{ steps.get-content.outputs.string }}
send_release_notes_email:
if: github.repository_owner == 'zed-industries' && !github.event.release.prerelease
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check if release was promoted from preview
id: check-promotion-from-preview
run: |
VERSION="${{ github.event.release.tag_name }}"
PREVIEW_TAG="${VERSION}-pre"
if git rev-parse "$PREVIEW_TAG" >/dev/null 2>&1; then
echo "was_preview=true" >> $GITHUB_OUTPUT
else
echo "was_preview=false" >> $GITHUB_OUTPUT
fi
- name: Send release notes email
if: steps.check-promotion-from-preview.outputs.was_preview == 'true'
run: |
curl -X POST "https://zed.dev/api/send_release_notes_email" \
-H "Authorization: Bearer ${{ secrets.RELEASE_NOTES_API_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{
"version": "${{ github.event.release.tag_name }}",
"markdown_body": ${{ toJSON(github.event.release.body) }}
}'

34
Cargo.lock generated
View File

@@ -257,9 +257,9 @@ checksum = "34cd60c5e3152cef0a592f1b296f1cc93715d89d2551d85315828c3a09575ff4"
[[package]]
name = "anyhow"
version = "1.0.96"
version = "1.0.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4"
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
[[package]]
name = "approx"
@@ -673,16 +673,22 @@ dependencies = [
"assistant_tool",
"chrono",
"collections",
"feature_flags",
"futures 0.3.31",
"gpui",
"language",
"language_model",
"project",
"rand 0.8.5",
"release_channel",
"schemars",
"serde",
"serde_json",
"settings",
"theme",
"ui",
"util",
"workspace",
]
[[package]]
@@ -4639,6 +4645,7 @@ dependencies = [
"collections",
"db",
"editor",
"extension",
"extension_host",
"feature_flags",
"fs",
@@ -17008,7 +17015,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.178.4"
version = "0.179.0"
dependencies = [
"activity_indicator",
"anyhow",
@@ -17203,13 +17210,6 @@ dependencies = [
"zed_extension_api 0.1.0",
]
[[package]]
name = "zed_purescript"
version = "0.1.0"
dependencies = [
"zed_extension_api 0.1.0",
]
[[package]]
name = "zed_ruff"
version = "0.1.0"
@@ -17239,20 +17239,6 @@ dependencies = [
"zed_extension_api 0.1.0",
]
[[package]]
name = "zed_uiua"
version = "0.0.1"
dependencies = [
"zed_extension_api 0.1.0",
]
[[package]]
name = "zed_zig"
version = "0.3.3"
dependencies = [
"zed_extension_api 0.1.0",
]
[[package]]
name = "zeno"
version = "0.2.3"

View File

@@ -174,14 +174,11 @@ members = [
"extensions/html",
"extensions/perplexity",
"extensions/proto",
"extensions/purescript",
"extensions/ruff",
"extensions/slash-commands-example",
"extensions/snippets",
"extensions/test-extension",
"extensions/toml",
"extensions/uiua",
"extensions/zig",
#
# Tooling

View File

@@ -11,8 +11,8 @@ You should only perform actions that modify the users system if explicitly re
Be concise and direct in your responses.
The user has opened a project that contains the following top-level directories/files:
The user has opened a project that contains the following root directories/files:
{{#each worktree_root_names}}
- {{this}}
{{#each worktrees}}
- {{root_name}} (absolute path: {{abs_path}})
{{/each}}

View File

@@ -336,14 +336,14 @@
"active_line_width": 1,
// Determines how indent guides are colored.
// This setting can take the following three values:
///
//
// 1. "disabled"
// 2. "fixed"
// 3. "indent_aware"
"coloring": "fixed",
// Determines how indent guide backgrounds are colored.
// This setting can take the following two values:
///
//
// 1. "disabled"
// 2. "indent_aware"
"background_coloring": "disabled"
@@ -402,8 +402,8 @@
// Time to wait after scrolling the buffer, before requesting the hints,
// set to 0 to disable debouncing.
"scroll_debounce_ms": 50,
/// A set of modifiers which, when pressed, will toggle the visibility of inlay hints.
/// If the set if empty or not all the modifiers specified are pressed, inlay hints will not be toggled.
// A set of modifiers which, when pressed, will toggle the visibility of inlay hints.
// If the set if empty or not all the modifiers specified are pressed, inlay hints will not be toggled.
"toggle_on_modifiers_press": {
"control": false,
"shift": false,
@@ -440,7 +440,7 @@
"scrollbar": {
// When to show the scrollbar in the project panel.
// This setting can take five values:
///
//
// 1. null (default): Inherit editor settings
// 2. Show the scrollbar if there's important information or
// follow the system's configured behavior (default):
@@ -455,7 +455,7 @@
},
// Which files containing diagnostic errors/warnings to mark in the project panel.
// This setting can take the following three values:
///
//
// 1. Do not mark any files:
// "off"
// 2. Only mark files with errors:
@@ -512,7 +512,7 @@
"scrollbar": {
// When to show the scrollbar in the project panel.
// This setting can take five values:
///
//
// 1. null (default): Inherit editor settings
// 2. Show the scrollbar if there's important information or
// follow the system's configured behavior (default):
@@ -547,7 +547,7 @@
"git_panel": {
// Whether to show the git panel button in the status bar.
"button": true,
// Where to the git panel. Can be 'left' or 'right'.
// Where to show the git panel. Can be 'left' or 'right'.
"dock": "left",
// Default width of the git panel.
"default_width": 360,
@@ -686,7 +686,7 @@
// Which files containing diagnostic errors/warnings to mark in the tabs.
// Diagnostics are only shown when file icons are also active.
// This setting only works when can take the following three values:
///
//
// 1. Do not mark any files:
// "off"
// 2. Only mark files with errors:
@@ -850,15 +850,7 @@
//
// The minimum column number to show the inline blame information at
// "min_column": 0
},
// How git hunks are displayed visually in the editor.
// This setting can take two values:
//
// 1. Show unstaged hunks filled and staged hunks hollow:
// "hunk_style": "staged_hollow"
// 2. Show unstaged hunks hollow and staged hunks filled:
// "hunk_style": "unstaged_hollow"
"hunk_style": "staged_hollow"
}
},
// Configuration for how direnv configuration should be loaded. May take 2 values:
// 1. Load direnv configuration using `direnv export json` directly.
@@ -1022,7 +1014,7 @@
"scrollbar": {
// When to show the scrollbar in the terminal.
// This setting can take five values:
///
//
// 1. null (default): Inherit editor settings
// 2. Show the scrollbar if there's important information or
// follow the system's configured behavior (default):
@@ -1093,6 +1085,31 @@
"auto_install_extensions": {
"html": true
},
// Controls how completions are processed for this language.
"completions": {
// Controls how words are completed.
// For large documents, not all words may be fetched for completion.
//
// May take 3 values:
// 1. "enabled"
// Always fetch document's words for completions.
// 2. "fallback"
// Only if LSP response errors/times out/is empty, use document's words to show completions.
// 3. "disabled"
// Never fetch or complete document's words for completions.
//
// Default: fallback
"words": "fallback",
// Whether to fetch LSP completions or not.
//
// Default: true
"lsp": true,
// When fetching LSP completions, determines how long to wait for a response of a particular server.
// When set to 0, waits indefinitely.
//
// Default: 500
"lsp_fetch_timeout_ms": 500
},
// Different settings for specific languages.
"languages": {
"Astro": {

View File

@@ -1,11 +1,13 @@
use std::sync::Arc;
use std::time::Duration;
use collections::HashMap;
use editor::{Editor, MultiBuffer};
use gpui::{
list, AbsoluteLength, AnyElement, App, ClickEvent, DefiniteLength, EdgesRefinement, Empty,
Entity, Focusable, Length, ListAlignment, ListOffset, ListState, StyleRefinement, Subscription,
Task, TextStyleRefinement, UnderlineStyle,
list, percentage, AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent,
DefiniteLength, EdgesRefinement, Empty, Entity, Focusable, Length, ListAlignment, ListOffset,
ListState, StyleRefinement, Subscription, Task, TextStyleRefinement, Transformation,
UnderlineStyle,
};
use language::{Buffer, LanguageRegistry};
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
@@ -701,13 +703,13 @@ impl ActiveThread {
.child(
h_flex()
.justify_between()
.py_0p5()
.py_1()
.pl_1()
.pr_2()
.bg(cx.theme().colors().editor_foreground.opacity(0.02))
.bg(cx.theme().colors().editor_foreground.opacity(0.025))
.map(|element| {
if is_open {
element.border_b_1().rounded_t(px(6.))
element.border_b_1().rounded_t_md()
} else {
element.rounded_md()
}
@@ -731,16 +733,35 @@ impl ActiveThread {
))
.child(Label::new(tool_use.name)),
)
.child(
Label::new(match tool_use.status {
ToolUseStatus::Pending => "Pending",
ToolUseStatus::Running => "Running",
ToolUseStatus::Finished(_) => "Finished",
ToolUseStatus::Error(_) => "Error",
})
.size(LabelSize::XSmall)
.buffer_font(cx),
),
.child({
let (icon_name, color, animated) = match &tool_use.status {
ToolUseStatus::Pending => {
(IconName::Warning, Color::Warning, false)
}
ToolUseStatus::Running => {
(IconName::ArrowCircle, Color::Accent, true)
}
ToolUseStatus::Finished(_) => {
(IconName::Check, Color::Success, false)
}
ToolUseStatus::Error(_) => (IconName::Close, Color::Error, false),
};
let icon = Icon::new(icon_name).color(color).size(IconSize::Small);
if animated {
icon.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(percentage(delta)))
},
)
.into_any_element()
} else {
icon.into_any_element()
}
}),
)
.map(|parent| {
if !is_open {
@@ -812,7 +833,7 @@ impl ActiveThread {
.bg(cx.theme().colors().editor_foreground.opacity(0.02))
.map(|element| {
if is_open {
element.border_b_1().rounded_t(px(6.))
element.border_b_1().rounded_t_md()
} else {
element.rounded_md()
}

View File

@@ -13,7 +13,7 @@ use language_model::{
Role, StopReason,
};
use project::Project;
use prompt_store::PromptBuilder;
use prompt_store::{AssistantSystemPromptWorktree, PromptBuilder};
use scripting_tool::{ScriptingSession, ScriptingTool};
use serde::{Deserialize, Serialize};
use util::{post_inc, ResultExt, TryFutureExt as _};
@@ -384,8 +384,14 @@ impl Thread {
let worktree_root_names = self
.project
.read(cx)
.worktree_root_names(cx)
.map(ToString::to_string)
.visible_worktrees(cx)
.map(|worktree| {
let worktree = worktree.read(cx);
AssistantSystemPromptWorktree {
root_name: worktree.root_name().into(),
abs_path: worktree.abs_path(),
}
})
.collect::<Vec<_>>();
let system_prompt = self
.prompt_builder

View File

@@ -20,13 +20,14 @@ impl ToolSelector {
cx: &mut Context<Self>,
) -> Entity<ContextMenu> {
ContextMenu::build(window, cx, |mut menu, _window, cx| {
let icon_position = IconPosition::End;
let tools_by_source = self.tools.tools_by_source(cx);
let all_tools_enabled = self.tools.are_all_tools_enabled();
menu = menu.header("Tools").toggleable_entry(
"All Tools",
all_tools_enabled,
IconPosition::End,
icon_position,
None,
{
let tools = self.tools.clone();
@@ -61,31 +62,51 @@ impl ToolSelector {
tools.sort_by(|(_, name_a, _), (_, name_b, _)| name_a.cmp(name_b));
}
menu = match source {
menu = match &source {
ToolSource::Native => menu.header("Zed"),
ToolSource::ContextServer { id } => menu.separator().header(id),
ToolSource::ContextServer { id } => {
let all_tools_from_source_enabled =
self.tools.are_all_tools_from_source_enabled(&source);
menu.separator().header(id).toggleable_entry(
"All Tools",
all_tools_from_source_enabled,
icon_position,
None,
{
let tools = self.tools.clone();
let source = source.clone();
move |_window, cx| {
if all_tools_from_source_enabled {
tools.disable_source(source.clone(), cx);
} else {
tools.enable_source(&source);
}
}
},
)
}
};
for (source, name, is_enabled) in tools {
menu =
menu.toggleable_entry(name.clone(), is_enabled, IconPosition::End, None, {
let tools = self.tools.clone();
move |_window, _cx| {
if name.as_ref() == ScriptingTool::NAME {
if is_enabled {
tools.disable_scripting_tool();
} else {
tools.enable_scripting_tool();
}
menu = menu.toggleable_entry(name.clone(), is_enabled, icon_position, None, {
let tools = self.tools.clone();
move |_window, _cx| {
if name.as_ref() == ScriptingTool::NAME {
if is_enabled {
tools.disable_scripting_tool();
} else {
if is_enabled {
tools.disable(source.clone(), &[name.clone()]);
} else {
tools.enable(source.clone(), &[name.clone()]);
}
tools.enable_scripting_tool();
}
} else {
if is_enabled {
tools.disable(source.clone(), &[name.clone()]);
} else {
tools.enable(source.clone(), &[name.clone()]);
}
}
});
}
});
}
}

View File

@@ -46,56 +46,119 @@ impl ToolWorkingSet {
}
pub fn tools(&self, cx: &App) -> Vec<Arc<dyn Tool>> {
let mut tools = ToolRegistry::global(cx).tools();
tools.extend(
self.state
.lock()
.context_server_tools_by_id
.values()
.cloned(),
);
self.state.lock().tools(cx)
}
tools
pub fn tools_by_source(&self, cx: &App) -> IndexMap<ToolSource, Vec<Arc<dyn Tool>>> {
self.state.lock().tools_by_source(cx)
}
pub fn are_all_tools_enabled(&self) -> bool {
let state = self.state.lock();
state.disabled_tools_by_source.is_empty() && !state.is_scripting_tool_disabled
}
pub fn are_all_tools_from_source_enabled(&self, source: &ToolSource) -> bool {
let state = self.state.lock();
!state.disabled_tools_by_source.contains_key(source)
}
pub fn enabled_tools(&self, cx: &App) -> Vec<Arc<dyn Tool>> {
self.state.lock().enabled_tools(cx)
}
pub fn enable_all_tools(&self) {
let mut state = self.state.lock();
state.disabled_tools_by_source.clear();
state.is_scripting_tool_disabled = false;
state.enable_scripting_tool();
}
pub fn disable_all_tools(&self, cx: &App) {
let tools = self.tools_by_source(cx);
for (source, tools) in tools {
let tool_names = tools
.into_iter()
.map(|tool| tool.name().into())
.collect::<Vec<_>>();
self.disable(source, &tool_names);
}
self.disable_scripting_tool();
let mut state = self.state.lock();
state.disable_all_tools(cx);
}
pub fn enabled_tools(&self, cx: &App) -> Vec<Arc<dyn Tool>> {
let all_tools = self.tools(cx);
all_tools
.into_iter()
.filter(|tool| self.is_enabled(&tool.source(), &tool.name().into()))
.collect()
pub fn enable_source(&self, source: &ToolSource) {
let mut state = self.state.lock();
state.enable_source(source);
}
pub fn tools_by_source(&self, cx: &App) -> IndexMap<ToolSource, Vec<Arc<dyn Tool>>> {
pub fn disable_source(&self, source: ToolSource, cx: &App) {
let mut state = self.state.lock();
state.disable_source(source, cx);
}
pub fn insert(&self, tool: Arc<dyn Tool>) -> ToolId {
let mut state = self.state.lock();
let tool_id = state.next_tool_id;
state.next_tool_id.0 += 1;
state
.context_server_tools_by_id
.insert(tool_id, tool.clone());
state.tools_changed();
tool_id
}
pub fn is_enabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
self.state.lock().is_enabled(source, name)
}
pub fn is_disabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
self.state.lock().is_disabled(source, name)
}
pub fn enable(&self, source: ToolSource, tools_to_enable: &[Arc<str>]) {
let mut state = self.state.lock();
state.enable(source, tools_to_enable);
}
pub fn disable(&self, source: ToolSource, tools_to_disable: &[Arc<str>]) {
let mut state = self.state.lock();
state.disable(source, tools_to_disable);
}
pub fn remove(&self, tool_ids_to_remove: &[ToolId]) {
let mut state = self.state.lock();
state
.context_server_tools_by_id
.retain(|id, _| !tool_ids_to_remove.contains(id));
state.tools_changed();
}
pub fn is_scripting_tool_enabled(&self) -> bool {
let state = self.state.lock();
!state.is_scripting_tool_disabled
}
pub fn enable_scripting_tool(&self) {
let mut state = self.state.lock();
state.enable_scripting_tool();
}
pub fn disable_scripting_tool(&self) {
let mut state = self.state.lock();
state.disable_scripting_tool();
}
}
impl WorkingSetState {
fn tools_changed(&mut self) {
self.context_server_tools_by_name.clear();
self.context_server_tools_by_name.extend(
self.context_server_tools_by_id
.values()
.map(|tool| (tool.name(), tool.clone())),
);
}
fn tools(&self, cx: &App) -> Vec<Arc<dyn Tool>> {
let mut tools = ToolRegistry::global(cx).tools();
tools.extend(self.context_server_tools_by_id.values().cloned());
tools
}
fn tools_by_source(&self, cx: &App) -> IndexMap<ToolSource, Vec<Arc<dyn Tool>>> {
let mut tools_by_source = IndexMap::default();
for tool in self.tools(cx) {
@@ -114,78 +177,78 @@ impl ToolWorkingSet {
tools_by_source
}
pub fn insert(&self, tool: Arc<dyn Tool>) -> ToolId {
let mut state = self.state.lock();
let tool_id = state.next_tool_id;
state.next_tool_id.0 += 1;
state
.context_server_tools_by_id
.insert(tool_id, tool.clone());
state.tools_changed();
tool_id
fn enabled_tools(&self, cx: &App) -> Vec<Arc<dyn Tool>> {
let all_tools = self.tools(cx);
all_tools
.into_iter()
.filter(|tool| self.is_enabled(&tool.source(), &tool.name().into()))
.collect()
}
pub fn is_enabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
fn is_enabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
!self.is_disabled(source, name)
}
pub fn is_disabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
let state = self.state.lock();
state
.disabled_tools_by_source
fn is_disabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
self.disabled_tools_by_source
.get(source)
.map_or(false, |disabled_tools| disabled_tools.contains(name))
}
pub fn enable(&self, source: ToolSource, tools_to_enable: &[Arc<str>]) {
let mut state = self.state.lock();
state
.disabled_tools_by_source
fn enable(&mut self, source: ToolSource, tools_to_enable: &[Arc<str>]) {
self.disabled_tools_by_source
.entry(source)
.or_default()
.retain(|name| !tools_to_enable.contains(name));
}
pub fn disable(&self, source: ToolSource, tools_to_disable: &[Arc<str>]) {
let mut state = self.state.lock();
state
.disabled_tools_by_source
fn disable(&mut self, source: ToolSource, tools_to_disable: &[Arc<str>]) {
self.disabled_tools_by_source
.entry(source)
.or_default()
.extend(tools_to_disable.into_iter().cloned());
}
pub fn remove(&self, tool_ids_to_remove: &[ToolId]) {
let mut state = self.state.lock();
state
.context_server_tools_by_id
.retain(|id, _| !tool_ids_to_remove.contains(id));
state.tools_changed();
fn enable_source(&mut self, source: &ToolSource) {
self.disabled_tools_by_source.remove(source);
}
pub fn is_scripting_tool_enabled(&self) -> bool {
let state = self.state.lock();
!state.is_scripting_tool_disabled
}
fn disable_source(&mut self, source: ToolSource, cx: &App) {
let tools_by_source = self.tools_by_source(cx);
let Some(tools) = tools_by_source.get(&source) else {
return;
};
pub fn enable_scripting_tool(&self) {
let mut state = self.state.lock();
state.is_scripting_tool_disabled = false;
}
pub fn disable_scripting_tool(&self) {
let mut state = self.state.lock();
state.is_scripting_tool_disabled = true;
}
}
impl WorkingSetState {
fn tools_changed(&mut self) {
self.context_server_tools_by_name.clear();
self.context_server_tools_by_name.extend(
self.context_server_tools_by_id
.values()
.map(|tool| (tool.name(), tool.clone())),
self.disabled_tools_by_source.insert(
source,
tools
.into_iter()
.map(|tool| tool.name().into())
.collect::<HashSet<_>>(),
);
}
fn disable_all_tools(&mut self, cx: &App) {
let tools = self.tools_by_source(cx);
for (source, tools) in tools {
let tool_names = tools
.into_iter()
.map(|tool| tool.name().into())
.collect::<Vec<_>>();
self.disable(source, &tool_names);
}
self.disable_scripting_tool();
}
fn enable_scripting_tool(&mut self) {
self.is_scripting_tool_disabled = false;
}
fn disable_scripting_tool(&mut self) {
self.is_scripting_tool_disabled = true;
}
}

View File

@@ -16,15 +16,21 @@ anyhow.workspace = true
assistant_tool.workspace = true
chrono.workspace = true
collections.workspace = true
feature_flags.workspace = true
futures.workspace = true
gpui.workspace = true
language.workspace = true
language_model.workspace = true
project.workspace = true
release_channel.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
settings.workspace = true
[dev-dependencies]
rand.workspace = true

View File

@@ -1,25 +1,38 @@
mod bash_tool;
mod delete_path_tool;
mod diagnostics_tool;
mod edit_files_tool;
mod list_directory_tool;
mod now_tool;
mod path_search_tool;
mod read_file_tool;
mod regex_search;
use assistant_tool::ToolRegistry;
use gpui::App;
use crate::bash_tool::BashTool;
use crate::delete_path_tool::DeletePathTool;
use crate::diagnostics_tool::DiagnosticsTool;
use crate::edit_files_tool::EditFilesTool;
use crate::list_directory_tool::ListDirectoryTool;
use crate::now_tool::NowTool;
use crate::path_search_tool::PathSearchTool;
use crate::read_file_tool::ReadFileTool;
use crate::regex_search::RegexSearchTool;
pub fn init(cx: &mut App) {
assistant_tool::init(cx);
crate::edit_files_tool::log::init(cx);
let registry = ToolRegistry::global(cx);
registry.register_tool(NowTool);
registry.register_tool(ReadFileTool);
registry.register_tool(ListDirectoryTool);
registry.register_tool(BashTool);
registry.register_tool(DeletePathTool);
registry.register_tool(DiagnosticsTool);
registry.register_tool(EditFilesTool);
registry.register_tool(ListDirectoryTool);
registry.register_tool(NowTool);
registry.register_tool(PathSearchTool);
registry.register_tool(ReadFileTool);
registry.register_tool(RegexSearchTool);
}

View File

@@ -0,0 +1,87 @@
use anyhow::{anyhow, Context as _, Result};
use assistant_tool::Tool;
use gpui::{App, Entity, Task};
use language_model::LanguageModelRequestMessage;
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use util::command::new_smol_command;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct BashToolInput {
/// The bash command to execute as a one-liner.
///
/// WARNING: you must not `cd` into the working directory, as that's already
/// taken care of automatically. Doing so will cause the command to fail!
command: String,
/// Working directory for the command. This must be one of the root directories of the project.
working_directory: String,
}
pub struct BashTool;
impl Tool for BashTool {
fn name(&self) -> String {
"bash".to_string()
}
fn description(&self) -> String {
include_str!("./bash_tool/description.md").to_string()
}
fn input_schema(&self) -> serde_json::Value {
let schema = schemars::schema_for!(BashToolInput);
serde_json::to_value(&schema).unwrap()
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
cx: &mut App,
) -> Task<Result<String>> {
let input: BashToolInput = match serde_json::from_value(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
};
let Some(worktree) = project
.read(cx)
.worktree_for_root_name(&input.working_directory, cx)
else {
return Task::ready(Err(anyhow!("Working directory not found in the project")));
};
let working_directory = worktree.read(cx).abs_path();
cx.spawn(|_| async move {
// Add 2>&1 to merge stderr into stdout for proper interleaving.
let command = format!("{} 2>&1", input.command);
let output = new_smol_command("bash")
.arg("-c")
.arg(&command)
.current_dir(working_directory)
.output()
.await
.context("Failed to execute bash command")?;
let output_string = String::from_utf8_lossy(&output.stdout).to_string();
if output.status.success() {
if output_string.is_empty() {
Ok("Command executed successfully.".to_string())
} else {
Ok(output_string)
}
} else {
Ok(format!(
"Command failed with exit code {}\n{}",
output.status.code().unwrap_or(-1),
&output_string
))
}
})
}
}

View File

@@ -0,0 +1,7 @@
Executes a bash one-liner and returns the combined output.
This tool spawns a bash process IN THE SPECIFIED WORKING DIRECTORY, combines stdout and stderr into one interleaved stream as they are produced (preserving the order of writes), and captures that stream into a string which is returned.
WARNING: **NEVER** use 'cd' commands to navigate to the working directory - this is automatically handled by the 'working_directory' parameter. Only use 'cd' to navigate to subdirectories within the specified working directory.
Remember that each invocation of this tool will spawn a new bash process, so you can't rely on any state from previous invocations.

View File

@@ -0,0 +1,165 @@
use anyhow::{anyhow, Result};
use assistant_tool::Tool;
use gpui::{App, Entity, Task};
use language_model::LanguageModelRequestMessage;
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{fs, path::PathBuf, sync::Arc};
use util::paths::PathMatcher;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct DeletePathToolInput {
/// The glob to match files in the project to delete.
///
/// <example>
/// If the project has the following files:
///
/// - directory1/a/something.txt
/// - directory2/a/things.txt
/// - directory3/a/other.txt
///
/// You can delete the first two files by providing a glob of "*thing*.txt"
/// </example>
pub glob: String,
}
pub struct DeletePathTool;
impl Tool for DeletePathTool {
fn name(&self) -> String {
"delete-path".into()
}
fn description(&self) -> String {
include_str!("./delete_path_tool/description.md").into()
}
fn input_schema(&self) -> serde_json::Value {
let schema = schemars::schema_for!(DeletePathToolInput);
serde_json::to_value(&schema).unwrap()
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
cx: &mut App,
) -> Task<Result<String>> {
let glob = match serde_json::from_value::<DeletePathToolInput>(input) {
Ok(input) => input.glob,
Err(err) => return Task::ready(Err(anyhow!(err))),
};
let path_matcher = match PathMatcher::new(&[glob.clone()]) {
Ok(matcher) => matcher,
Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {}", err))),
};
struct Match {
display_path: String,
path: PathBuf,
}
let mut matches = Vec::new();
let mut deleted_paths = Vec::new();
let mut errors = Vec::new();
for worktree_handle in project.read(cx).worktrees(cx) {
let worktree = worktree_handle.read(cx);
let worktree_root = worktree.abs_path().to_path_buf();
// Don't consider ignored entries.
for entry in worktree.entries(false, 0) {
if path_matcher.is_match(&entry.path) {
matches.push(Match {
path: worktree_root.join(&entry.path),
display_path: entry.path.display().to_string(),
});
}
}
}
if matches.is_empty() {
return Task::ready(Ok(format!("No paths in the project matched {glob:?}")));
}
let paths_matched = matches.len();
// Delete the files
for Match { path, display_path } in matches {
match fs::remove_file(&path) {
Ok(()) => {
deleted_paths.push(display_path);
}
Err(file_err) => {
// Try to remove directory if it's not a file. Retrying as a directory
// on error saves a syscall compared to checking whether it's
// a directory up front for every single file.
if let Err(dir_err) = fs::remove_dir_all(&path) {
let error = if path.is_dir() {
format!("Failed to delete directory {}: {dir_err}", display_path)
} else {
format!("Failed to delete file {}: {file_err}", display_path)
};
errors.push(error);
} else {
deleted_paths.push(display_path);
}
}
}
}
if errors.is_empty() {
// 0 deleted paths should never happen if there were no errors;
// we already returned if matches was empty.
let answer = if deleted_paths.len() == 1 {
format!(
"Deleted {}",
deleted_paths.first().unwrap_or(&String::new())
)
} else {
// Sort to group entries in the same directory together
deleted_paths.sort();
let mut buf = format!("Deleted these {} paths:\n", deleted_paths.len());
for path in deleted_paths.iter() {
buf.push('\n');
buf.push_str(path);
}
buf
};
Task::ready(Ok(answer))
} else {
if deleted_paths.is_empty() {
Task::ready(Err(anyhow!(
"{glob:?} matched {} deleted because of {}:\n{}",
if paths_matched == 1 {
"1 path, but it was not".to_string()
} else {
format!("{} paths, but none were", paths_matched)
},
if errors.len() == 1 {
"this error".to_string()
} else {
format!("{} errors", errors.len())
},
errors.join("\n")
)))
} else {
// Sort to group entries in the same directory together
deleted_paths.sort();
Task::ready(Ok(format!(
"Deleted {} paths matching glob {glob:?}:\n{}\n\nErrors:\n{}",
deleted_paths.len(),
deleted_paths.join("\n"),
errors.join("\n")
)))
}
}
}
}

View File

@@ -0,0 +1 @@
Deletes all files and directories in the project which match the given glob, and returns a list of the paths that were deleted.

View File

@@ -0,0 +1,127 @@
use anyhow::{anyhow, Result};
use assistant_tool::Tool;
use gpui::{App, Entity, Task};
use language::{DiagnosticSeverity, OffsetRangeExt};
use language_model::LanguageModelRequestMessage;
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{
fmt::Write,
path::{Path, PathBuf},
sync::Arc,
};
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct DiagnosticsToolInput {
/// The path to get diagnostics for. If not provided, returns a project-wide summary.
///
/// This path should never be absolute, and the first component
/// of the path should always be a root directory in a project.
///
/// <example>
/// If the project has the following root directories:
///
/// - lorem
/// - ipsum
///
/// If you wanna access diagnostics for `dolor.txt` in `ipsum`, you should use the path `ipsum/dolor.txt`.
/// </example>
pub path: Option<PathBuf>,
}
pub struct DiagnosticsTool;
impl Tool for DiagnosticsTool {
fn name(&self) -> String {
"diagnostics".into()
}
fn description(&self) -> String {
include_str!("./diagnostics_tool/description.md").into()
}
fn input_schema(&self) -> serde_json::Value {
let schema = schemars::schema_for!(DiagnosticsToolInput);
serde_json::to_value(&schema).unwrap()
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
cx: &mut App,
) -> Task<Result<String>> {
let input = match serde_json::from_value::<DiagnosticsToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
};
if let Some(path) = input.path {
let Some(project_path) = project.read(cx).find_project_path(&path, cx) else {
return Task::ready(Err(anyhow!("Could not find path in project")));
};
let buffer = project.update(cx, |project, cx| project.open_buffer(project_path, cx));
cx.spawn(|cx| async move {
let mut output = String::new();
let buffer = buffer.await?;
let snapshot = buffer.read_with(&cx, |buffer, _cx| buffer.snapshot())?;
for (_, group) in snapshot.diagnostic_groups(None) {
let entry = &group.entries[group.primary_ix];
let range = entry.range.to_point(&snapshot);
let severity = match entry.diagnostic.severity {
DiagnosticSeverity::ERROR => "error",
DiagnosticSeverity::WARNING => "warning",
_ => continue,
};
writeln!(
output,
"{} at line {}: {}",
severity,
range.start.row + 1,
entry.diagnostic.message
)?;
}
if output.is_empty() {
Ok("File doesn't have errors or warnings!".to_string())
} else {
Ok(output)
}
})
} else {
let project = project.read(cx);
let mut output = String::new();
let mut has_diagnostics = false;
for (project_path, _, summary) in project.diagnostic_summaries(true, cx) {
if summary.error_count > 0 || summary.warning_count > 0 {
let Some(worktree) = project.worktree_for_id(project_path.worktree_id, cx)
else {
continue;
};
has_diagnostics = true;
output.push_str(&format!(
"{}: {} error(s), {} warning(s)\n",
Path::new(worktree.read(cx).root_name())
.join(project_path.path)
.display(),
summary.error_count,
summary.warning_count
));
}
}
if has_diagnostics {
Task::ready(Ok(output))
} else {
Task::ready(Ok("No errors or warnings found in the project.".to_string()))
}
}
}
}

View File

@@ -0,0 +1,16 @@
Get errors and warnings for the project or a specific file.
This tool can be invoked after a series of edits to determine if further edits are necessary, or if the user asks to fix errors or warnings in their codebase.
When a path is provided, shows all diagnostics for that specific file.
When no path is provided, shows a summary of error and warning counts for all files in the project.
<example>
To get diagnostics for a specific file:
{
"path": "src/main.rs"
}
To get a project-wide diagnostic summary:
{}
</example>

View File

@@ -1,4 +1,5 @@
mod edit_action;
pub mod log;
use anyhow::{anyhow, Context, Result};
use assistant_tool::Tool;
@@ -9,24 +10,50 @@ use gpui::{App, Entity, Task};
use language_model::{
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
};
use project::{Project, ProjectPath};
use log::{EditToolLog, EditToolRequestId};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt::Write;
use std::sync::Arc;
use util::ResultExt;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct EditFilesToolInput {
/// High-level edit instructions. These will be interpreted by a smaller model,
/// so explain the edits you want that model to make and to which files need changing.
/// The description should be concise and clear. We will show this description to the user
/// as well.
/// High-level edit instructions. These will be interpreted by a smaller
/// model, so explain the changes you want that model to make and which
/// file paths need changing.
///
/// The description should be concise and clear. We will show this
/// description to the user as well.
///
/// WARNING: When specifying which file paths need changing, you MUST
/// start each path with one of the project's root directories.
///
/// WARNING: NEVER include code blocks or snippets in edit instructions.
/// Only provide natural language descriptions of the changes needed! The tool will
/// reject any instructions that contain code blocks or snippets.
///
/// The following examples assume we have two root directories in the project:
/// - root-1
/// - root-2
///
/// <example>
/// If you want to rename a function you can say "Rename the function 'foo' to 'bar'".
/// If you want to introduce a new quit function to kill the process, your
/// instructions should be: "Add a new `quit` function to
/// `root-1/src/main.rs` to kill the process".
///
/// Notice how the file path starts with root-1. Without that, the path
/// would be ambiguous and the call would fail!
/// </example>
///
/// <example>
/// If you want to add a new function you can say "Add a new method to the `User` struct that prints the age".
/// If you want to change documentation to always start with a capital
/// letter, your instructions should be: "In `root-2/db.js`,
/// `root-2/inMemory.js` and `root-2/sql.js`, change all the documentation
/// to start with a capital letter".
///
/// Notice how we never specify code snippets in the instructions!
/// </example>
pub edit_instructions: String,
}
@@ -59,6 +86,45 @@ impl Tool for EditFilesTool {
Err(err) => return Task::ready(Err(anyhow!(err))),
};
match EditToolLog::try_global(cx) {
Some(log) => {
let req_id = log.update(cx, |log, cx| {
log.new_request(input.edit_instructions.clone(), cx)
});
let task =
EditFilesTool::run(input, messages, project, Some((log.clone(), req_id)), cx);
cx.spawn(|mut cx| async move {
let result = task.await;
let str_result = match &result {
Ok(out) => Ok(out.clone()),
Err(err) => Err(err.to_string()),
};
log.update(&mut cx, |log, cx| {
log.set_tool_output(req_id, str_result, cx)
})
.log_err();
result
})
}
None => EditFilesTool::run(input, messages, project, None, cx),
}
}
}
impl EditFilesTool {
fn run(
input: EditFilesToolInput,
messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
log: Option<(Entity<EditToolLog>, EditToolRequestId)>,
cx: &mut App,
) -> Task<Result<String>> {
let model_registry = LanguageModelRegistry::read_global(cx);
let Some(model) = model_registry.editor_model() else {
return Task::ready(Err(anyhow!("No editor model configured")));
@@ -85,7 +151,7 @@ impl Tool for EditFilesTool {
messages,
tools: vec![],
stop: vec![],
temperature: None,
temperature: Some(0.0),
};
let mut parser = EditActionParser::new();
@@ -96,26 +162,25 @@ impl Tool for EditFilesTool {
let mut changed_buffers = HashSet::default();
let mut applied_edits = 0;
let log = log.clone();
while let Some(chunk) = chunks.stream.next().await {
for action in parser.parse_chunk(&chunk?) {
let chunk = chunk?;
let new_actions = parser.parse_chunk(&chunk);
if let Some((ref log, req_id)) = log {
log.update(&mut cx, |log, cx| {
log.push_editor_response_chunk(req_id, &chunk, &new_actions, cx)
})
.log_err();
}
for action in new_actions {
let project_path = project.read_with(&cx, |project, cx| {
let worktree_root_name = action
.file_path()
.components()
.next()
.context("Invalid path")?;
let worktree = project
.worktree_for_root_name(
&worktree_root_name.as_os_str().to_string_lossy(),
cx,
)
.context("Directory not found in project")?;
anyhow::Ok(ProjectPath {
worktree_id: worktree.read(cx).id(),
path: Arc::from(
action.file_path().strip_prefix(worktree_root_name).unwrap(),
),
})
project
.find_project_path(action.file_path(), cx)
.context("Path not found in project")
})??;
let buffer = project
@@ -125,15 +190,27 @@ impl Tool for EditFilesTool {
let diff = buffer
.read_with(&cx, |buffer, cx| {
let new_text = match action {
EditAction::Replace { old, new, .. } => {
EditAction::Replace {
file_path,
old,
new,
} => {
// TODO: Replace in background?
buffer.text().replace(&old, &new)
let text = buffer.text();
if text.contains(&old) {
text.replace(&old, &new)
} else {
return Err(anyhow!(
"Could not find search text in {}",
file_path.display()
));
}
}
EditAction::Write { content, .. } => content,
};
buffer.diff(new_text, cx)
})?
anyhow::Ok(buffer.diff(new_text, cx))
})??
.await;
let _clock =
@@ -145,17 +222,29 @@ impl Tool for EditFilesTool {
}
}
let mut answer = match changed_buffers.len() {
0 => "No files were edited.".to_string(),
1 => "Successfully edited ".to_string(),
_ => "Successfully edited these files:\n\n".to_string(),
};
// Save each buffer once at the end
for buffer in changed_buffers {
project
.update(&mut cx, |project, cx| project.save_buffer(buffer, cx))?
.update(&mut cx, |project, cx| {
if let Some(file) = buffer.read(&cx).file() {
let _ = writeln!(&mut answer, "{}", &file.full_path(cx).display());
}
project.save_buffer(buffer, cx)
})?
.await?;
}
let errors = parser.errors();
if errors.is_empty() {
Ok("Successfully applied all edits".into())
Ok(answer.trim_end().to_string())
} else {
let error_message = errors
.iter()

View File

@@ -1,4 +1,4 @@
Edit files in the current project.
Edit files in the current project by specifying instructions in natural language.
When using this tool, you should suggest one coherent edit that can be made to the codebase.

View File

@@ -106,7 +106,7 @@ Every *SEARCH/REPLACE block* must use this format:
7. The end of the replace block: >>>>>>> REPLACE
8. The closing fence: ```
Use the *FULL* file path, as shown to you by the user.
Use the *FULL* file path, as shown to you by the user. Make sure to include the project's root directory name at the start of the path. *NEVER* specify the absolute path of the file!
Every *SEARCH* section must *EXACTLY MATCH* the existing file content, character for character, including all comments, docstrings, etc.
If the file contains code or other data wrapped/escaped in json/xml/quotes or other containers, you need to propose edits to the literal contents of the file, including the container markup.

View File

@@ -0,0 +1,415 @@
use std::path::Path;
use collections::HashSet;
use feature_flags::FeatureFlagAppExt;
use gpui::{
actions, list, prelude::*, App, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global,
ListAlignment, ListState, SharedString, Subscription, Window,
};
use release_channel::ReleaseChannel;
use settings::Settings;
use ui::prelude::*;
use workspace::{item::ItemEvent, Item, Workspace, WorkspaceId};
use super::edit_action::EditAction;
actions!(debug, [EditTool]);
pub fn init(cx: &mut App) {
if cx.is_staff() || ReleaseChannel::global(cx) == ReleaseChannel::Dev {
// Track events even before opening the log
EditToolLog::global(cx);
}
cx.observe_new(|workspace: &mut Workspace, _, _| {
workspace.register_action(|workspace, _: &EditTool, window, cx| {
let viewer = cx.new(EditToolLogViewer::new);
workspace.add_item_to_active_pane(Box::new(viewer), None, true, window, cx)
});
})
.detach();
}
pub struct GlobalEditToolLog(Entity<EditToolLog>);
impl Global for GlobalEditToolLog {}
#[derive(Default)]
pub struct EditToolLog {
requests: Vec<EditToolRequest>,
}
#[derive(Clone, Copy, Hash, Eq, PartialEq)]
pub struct EditToolRequestId(u32);
impl EditToolLog {
pub fn global(cx: &mut App) -> Entity<Self> {
match Self::try_global(cx) {
Some(entity) => entity,
None => {
let entity = cx.new(|_cx| Self::default());
cx.set_global(GlobalEditToolLog(entity.clone()));
entity
}
}
}
pub fn try_global(cx: &App) -> Option<Entity<Self>> {
cx.try_global::<GlobalEditToolLog>()
.map(|log| log.0.clone())
}
pub fn new_request(
&mut self,
instructions: String,
cx: &mut Context<Self>,
) -> EditToolRequestId {
let id = EditToolRequestId(self.requests.len() as u32);
self.requests.push(EditToolRequest {
id,
instructions,
editor_response: None,
tool_output: None,
parsed_edits: Vec::new(),
});
cx.emit(EditToolLogEvent::Inserted);
id
}
pub fn push_editor_response_chunk(
&mut self,
id: EditToolRequestId,
chunk: &str,
new_actions: &[EditAction],
cx: &mut Context<Self>,
) {
if let Some(request) = self.requests.get_mut(id.0 as usize) {
match &mut request.editor_response {
None => {
request.editor_response = Some(chunk.to_string());
}
Some(response) => {
response.push_str(chunk);
}
}
request.parsed_edits.extend(new_actions.iter().cloned());
cx.emit(EditToolLogEvent::Updated);
}
}
pub fn set_tool_output(
&mut self,
id: EditToolRequestId,
tool_output: Result<String, String>,
cx: &mut Context<Self>,
) {
if let Some(request) = self.requests.get_mut(id.0 as usize) {
request.tool_output = Some(tool_output);
cx.emit(EditToolLogEvent::Updated);
}
}
}
enum EditToolLogEvent {
Inserted,
Updated,
}
impl EventEmitter<EditToolLogEvent> for EditToolLog {}
pub struct EditToolRequest {
id: EditToolRequestId,
instructions: String,
// we don't use a result here because the error might have occurred after we got a response
editor_response: Option<String>,
parsed_edits: Vec<EditAction>,
tool_output: Option<Result<String, String>>,
}
pub struct EditToolLogViewer {
focus_handle: FocusHandle,
log: Entity<EditToolLog>,
list_state: ListState,
expanded_edits: HashSet<(EditToolRequestId, usize)>,
_subscription: Subscription,
}
impl EditToolLogViewer {
pub fn new(cx: &mut Context<Self>) -> Self {
let log = EditToolLog::global(cx);
let subscription = cx.subscribe(&log, Self::handle_log_event);
Self {
focus_handle: cx.focus_handle(),
log: log.clone(),
list_state: ListState::new(
log.read(cx).requests.len(),
ListAlignment::Bottom,
px(1024.),
{
let this = cx.entity().downgrade();
move |ix, window: &mut Window, cx: &mut App| {
this.update(cx, |this, cx| this.render_request(ix, window, cx))
.unwrap()
}
},
),
expanded_edits: HashSet::default(),
_subscription: subscription,
}
}
fn handle_log_event(
&mut self,
_: Entity<EditToolLog>,
event: &EditToolLogEvent,
cx: &mut Context<Self>,
) {
match event {
EditToolLogEvent::Inserted => {
let count = self.list_state.item_count();
self.list_state.splice(count..count, 1);
}
EditToolLogEvent::Updated => {}
}
cx.notify();
}
fn render_request(
&self,
index: usize,
_window: &mut Window,
cx: &mut Context<Self>,
) -> AnyElement {
let requests = &self.log.read(cx).requests;
let request = &requests[index];
v_flex()
.gap_3()
.child(Self::render_section(IconName::ArrowRight, "Tool Input"))
.child(request.instructions.clone())
.py_5()
.when(index + 1 < requests.len(), |element| {
element
.border_b_1()
.border_color(cx.theme().colors().border)
})
.map(|parent| match &request.editor_response {
None => {
if request.tool_output.is_none() {
parent.child("...")
} else {
parent
}
}
Some(response) => parent
.child(Self::render_section(
IconName::ZedAssistant,
"Editor Response",
))
.child(Label::new(response.clone()).buffer_font(cx)),
})
.when(!request.parsed_edits.is_empty(), |parent| {
parent
.child(Self::render_section(IconName::Microscope, "Parsed Edits"))
.child(
v_flex()
.gap_2()
.children(request.parsed_edits.iter().enumerate().map(
|(index, edit)| {
self.render_edit_action(edit, request.id, index, cx)
},
)),
)
})
.when_some(request.tool_output.as_ref(), |parent, output| {
parent
.child(Self::render_section(IconName::ArrowLeft, "Tool Output"))
.child(match output {
Ok(output) => Label::new(output.clone()).color(Color::Success),
Err(error) => Label::new(error.clone()).color(Color::Error),
})
})
.into_any()
}
fn render_section(icon: IconName, title: &'static str) -> AnyElement {
h_flex()
.gap_1()
.child(Icon::new(icon).color(Color::Muted))
.child(Label::new(title).size(LabelSize::Small).color(Color::Muted))
.into_any()
}
fn render_edit_action(
&self,
edit_action: &EditAction,
request_id: EditToolRequestId,
index: usize,
cx: &Context<Self>,
) -> AnyElement {
let expanded_id = (request_id, index);
match edit_action {
EditAction::Replace {
file_path,
old,
new,
} => self
.render_edit_action_container(
expanded_id,
&file_path,
[
Self::render_block(IconName::MagnifyingGlass, "Search", old.clone(), cx)
.border_r_1()
.border_color(cx.theme().colors().border)
.into_any(),
Self::render_block(IconName::Replace, "Replace", new.clone(), cx)
.into_any(),
],
cx,
)
.into_any(),
EditAction::Write { file_path, content } => self
.render_edit_action_container(
expanded_id,
&file_path,
[
Self::render_block(IconName::Pencil, "Write", content.clone(), cx)
.into_any(),
],
cx,
)
.into_any(),
}
}
fn render_edit_action_container(
&self,
expanded_id: (EditToolRequestId, usize),
file_path: &Path,
content: impl IntoIterator<Item = AnyElement>,
cx: &Context<Self>,
) -> AnyElement {
let is_expanded = self.expanded_edits.contains(&expanded_id);
v_flex()
.child(
h_flex()
.bg(cx.theme().colors().element_background)
.border_1()
.border_color(cx.theme().colors().border)
.rounded_t_md()
.when(!is_expanded, |el| el.rounded_b_md())
.py_1()
.px_2()
.gap_1()
.child(
ui::Disclosure::new(ElementId::Integer(expanded_id.1), is_expanded)
.on_click(cx.listener(move |this, _ev, _window, cx| {
if is_expanded {
this.expanded_edits.remove(&expanded_id);
} else {
this.expanded_edits.insert(expanded_id);
}
cx.notify();
})),
)
.child(Label::new(file_path.display().to_string()).size(LabelSize::Small)),
)
.child(if is_expanded {
h_flex()
.border_1()
.border_t_0()
.border_color(cx.theme().colors().border)
.rounded_b_md()
.children(content)
.into_any()
} else {
Empty.into_any()
})
.into_any()
}
fn render_block(icon: IconName, title: &'static str, content: String, cx: &App) -> Div {
v_flex()
.p_1()
.gap_1()
.flex_1()
.h_full()
.child(
h_flex()
.gap_1()
.child(Icon::new(icon).color(Color::Muted))
.child(Label::new(title).size(LabelSize::Small).color(Color::Muted)),
)
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
.text_sm()
.child(content)
.child(div().flex_1())
}
}
impl EventEmitter<()> for EditToolLogViewer {}
impl Focusable for EditToolLogViewer {
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
impl Item for EditToolLogViewer {
type Event = ();
fn to_item_events(_: &Self::Event, _: impl FnMut(ItemEvent)) {}
fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
Some("Edit Tool Log".into())
}
fn telemetry_event_text(&self) -> Option<&'static str> {
None
}
fn clone_on_split(
&self,
_workspace_id: Option<WorkspaceId>,
_window: &mut Window,
cx: &mut Context<Self>,
) -> Option<Entity<Self>>
where
Self: Sized,
{
Some(cx.new(Self::new))
}
}
impl Render for EditToolLogViewer {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if self.list_state.item_count() == 0 {
return v_flex()
.justify_center()
.size_full()
.gap_1()
.bg(cx.theme().colors().editor_background)
.text_center()
.text_lg()
.child("No requests yet")
.child(
div()
.text_ui(cx)
.child("Go ask the assistant to perform some edits"),
);
}
v_flex()
.p_4()
.bg(cx.theme().colors().editor_background)
.size_full()
.child(list(self.list_state.clone()).flex_grow())
}
}

View File

@@ -12,10 +12,10 @@ pub struct ListDirectoryToolInput {
/// The relative path of the directory to list.
///
/// This path should never be absolute, and the first component
/// of the path should always be a top-level directory in a project.
/// of the path should always be a root directory in a project.
///
/// <example>
/// If the project has the following top-level directories:
/// If the project has the following root directories:
///
/// - directory1
/// - directory2
@@ -24,7 +24,7 @@ pub struct ListDirectoryToolInput {
/// </example>
///
/// <example>
/// If the project has the following top-level directories:
/// If the project has the following root directories:
///
/// - foo
/// - bar
@@ -62,27 +62,37 @@ impl Tool for ListDirectoryTool {
Err(err) => return Task::ready(Err(anyhow!(err))),
};
let Some(worktree_root_name) = input.path.components().next() else {
return Task::ready(Err(anyhow!("Invalid path")));
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
return Task::ready(Err(anyhow!("Path not found in project")));
};
let Some(worktree) = project
.read(cx)
.worktree_for_root_name(&worktree_root_name.as_os_str().to_string_lossy(), cx)
.worktree_for_id(project_path.worktree_id, cx)
else {
return Task::ready(Err(anyhow!("Directory not found in the project")));
return Task::ready(Err(anyhow!("Worktree not found")));
};
let path = input.path.strip_prefix(worktree_root_name).unwrap();
let worktree = worktree.read(cx);
let Some(entry) = worktree.entry_for_path(&project_path.path) else {
return Task::ready(Err(anyhow!("Path not found: {}", input.path.display())));
};
if !entry.is_dir() {
return Task::ready(Err(anyhow!("{} is a file.", input.path.display())));
}
let mut output = String::new();
for entry in worktree.read(cx).child_entries(path) {
for entry in worktree.child_entries(&project_path.path) {
writeln!(
output,
"{}",
Path::new(worktree_root_name.as_os_str())
.join(&entry.path)
.display(),
Path::new(worktree.root_name()).join(&entry.path).display(),
)
.unwrap();
}
if output.is_empty() {
return Task::ready(Ok(format!("{} is empty.", input.path.display())));
}
Task::ready(Ok(output))
}
}

View File

@@ -0,0 +1,88 @@
use anyhow::{anyhow, Result};
use assistant_tool::Tool;
use gpui::{App, Entity, Task};
use language_model::LanguageModelRequestMessage;
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{path::PathBuf, sync::Arc};
use util::paths::PathMatcher;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct PathSearchToolInput {
/// The glob to search all project paths for.
///
/// <example>
/// If the project has the following root directories:
///
/// - directory1/a/something.txt
/// - directory2/a/things.txt
/// - directory3/a/other.txt
///
/// You can get back the first two paths by providing a glob of "*thing*.txt"
/// </example>
pub glob: String,
}
pub struct PathSearchTool;
impl Tool for PathSearchTool {
fn name(&self) -> String {
"path-search".into()
}
fn description(&self) -> String {
include_str!("./path_search_tool/description.md").into()
}
fn input_schema(&self) -> serde_json::Value {
let schema = schemars::schema_for!(PathSearchToolInput);
serde_json::to_value(&schema).unwrap()
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
cx: &mut App,
) -> Task<Result<String>> {
let glob = match serde_json::from_value::<PathSearchToolInput>(input) {
Ok(input) => input.glob,
Err(err) => return Task::ready(Err(anyhow!(err))),
};
let path_matcher = match PathMatcher::new(&[glob.clone()]) {
Ok(matcher) => matcher,
Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {}", err))),
};
let mut matches = Vec::new();
for worktree_handle in project.read(cx).worktrees(cx) {
let worktree = worktree_handle.read(cx);
let root_name = worktree.root_name();
// Don't consider ignored entries.
for entry in worktree.entries(false, 0) {
if path_matcher.is_match(&entry.path) {
matches.push(
PathBuf::from(root_name)
.join(&entry.path)
.to_string_lossy()
.to_string(),
);
}
}
}
if matches.is_empty() {
Task::ready(Ok(format!(
"No paths in the project matched the glob {glob:?}"
)))
} else {
// Sort to group entries in the same directory together.
matches.sort();
Task::ready(Ok(matches.join("\n")))
}
}
}

View File

@@ -0,0 +1 @@
Returns all the paths in the project which match the given glob.

View File

@@ -5,7 +5,7 @@ use anyhow::{anyhow, Result};
use assistant_tool::Tool;
use gpui::{App, Entity, Task};
use language_model::LanguageModelRequestMessage;
use project::{Project, ProjectPath};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -14,10 +14,10 @@ pub struct ReadFileToolInput {
/// The relative path of the file to read.
///
/// This path should never be absolute, and the first component
/// of the path should always be a top-level directory in a project.
/// of the path should always be a root directory in a project.
///
/// <example>
/// If the project has the following top-level directories:
/// If the project has the following root directories:
///
/// - directory1
/// - directory2
@@ -56,18 +56,8 @@ impl Tool for ReadFileTool {
Err(err) => return Task::ready(Err(anyhow!(err))),
};
let Some(worktree_root_name) = input.path.components().next() else {
return Task::ready(Err(anyhow!("Invalid path")));
};
let Some(worktree) = project
.read(cx)
.worktree_for_root_name(&worktree_root_name.as_os_str().to_string_lossy(), cx)
else {
return Task::ready(Err(anyhow!("Directory not found in the project")));
};
let project_path = ProjectPath {
worktree_id: worktree.read(cx).id(),
path: Arc::from(input.path.strip_prefix(worktree_root_name).unwrap()),
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
return Task::ready(Err(anyhow!("Path not found in project")));
};
cx.spawn(|cx| async move {
let buffer = cx

View File

@@ -22,7 +22,6 @@ git2.workspace = true
gpui.workspace = true
language.workspace = true
log.workspace = true
pretty_assertions.workspace = true
rope.workspace = true
sum_tree.workspace = true
text.workspace = true
@@ -32,6 +31,7 @@ util.workspace = true
ctor.workspace = true
env_logger.workspace = true
gpui = { workspace = true, features = ["test-support"] }
pretty_assertions.workspace = true
rand.workspace = true
serde_json.workspace = true
text = { workspace = true, features = ["test-support"] }

View File

@@ -6,9 +6,9 @@ use rope::Rope;
use std::cmp::Ordering;
use std::mem;
use std::{future::Future, iter, ops::Range, sync::Arc};
use sum_tree::SumTree;
use sum_tree::{SumTree, TreeMap};
use text::ToOffset as _;
use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point};
use text::{AnchorRangeExt, ToOffset as _};
use util::ResultExt;
pub struct BufferDiff {
@@ -26,7 +26,7 @@ pub struct BufferDiffSnapshot {
#[derive(Clone)]
struct BufferDiffInner {
hunks: SumTree<InternalDiffHunk>,
pending_hunks: SumTree<PendingHunk>,
pending_hunks: TreeMap<usize, PendingHunk>,
base_text: language::BufferSnapshot,
base_text_exists: bool,
}
@@ -48,7 +48,7 @@ pub enum DiffHunkStatusKind {
pub enum DiffHunkSecondaryStatus {
HasSecondaryHunk,
OverlapsWithSecondaryHunk,
NoSecondaryHunk,
None,
SecondaryHunkAdditionPending,
SecondaryHunkRemovalPending,
}
@@ -74,8 +74,6 @@ struct InternalDiffHunk {
#[derive(Debug, Clone, PartialEq, Eq)]
struct PendingHunk {
buffer_range: Range<Anchor>,
diff_base_byte_range: Range<usize>,
buffer_version: clock::Global,
new_status: DiffHunkSecondaryStatus,
}
@@ -95,16 +93,6 @@ impl sum_tree::Item for InternalDiffHunk {
}
}
impl sum_tree::Item for PendingHunk {
type Summary = DiffHunkSummary;
fn summary(&self, _cx: &text::BufferSnapshot) -> Self::Summary {
DiffHunkSummary {
buffer_range: self.buffer_range.clone(),
}
}
}
impl sum_tree::Summary for DiffHunkSummary {
type Context = text::BufferSnapshot;
@@ -188,7 +176,6 @@ impl BufferDiffSnapshot {
}
impl BufferDiffInner {
/// Returns the new index text and new pending hunks.
fn stage_or_unstage_hunks(
&mut self,
unstaged_diff: &Self,
@@ -196,7 +183,7 @@ impl BufferDiffInner {
hunks: &[DiffHunk],
buffer: &text::BufferSnapshot,
file_exists: bool,
) -> (Option<Rope>, SumTree<PendingHunk>) {
) -> (Option<Rope>, Vec<(usize, PendingHunk)>) {
let head_text = self
.base_text_exists
.then(|| self.base_text.as_rope().clone());
@@ -208,41 +195,41 @@ impl BufferDiffInner {
// entire file must be either created or deleted in the index.
let (index_text, head_text) = match (index_text, head_text) {
(Some(index_text), Some(head_text)) if file_exists || !stage => (index_text, head_text),
(index_text, head_text) => {
let (rope, new_status) = if stage {
(_, head_text @ _) => {
if stage {
log::debug!("stage all");
(
return (
file_exists.then(|| buffer.as_rope().clone()),
DiffHunkSecondaryStatus::SecondaryHunkRemovalPending,
)
vec![(
0,
PendingHunk {
buffer_version: buffer.version().clone(),
new_status: DiffHunkSecondaryStatus::SecondaryHunkRemovalPending,
},
)],
);
} else {
log::debug!("unstage all");
(
return (
head_text,
DiffHunkSecondaryStatus::SecondaryHunkAdditionPending,
)
};
let hunk = PendingHunk {
buffer_range: Anchor::MIN..Anchor::MAX,
diff_base_byte_range: 0..index_text.map_or(0, |rope| rope.len()),
buffer_version: buffer.version().clone(),
new_status,
};
let tree = SumTree::from_item(hunk, buffer);
return (rope, tree);
vec![(
0,
PendingHunk {
buffer_version: buffer.version().clone(),
new_status: DiffHunkSecondaryStatus::SecondaryHunkAdditionPending,
},
)],
);
}
}
};
let mut unstaged_hunk_cursor = unstaged_diff.hunks.cursor::<DiffHunkSummary>(buffer);
unstaged_hunk_cursor.next(buffer);
let mut pending_hunks = SumTree::new(buffer);
let mut old_pending_hunks = unstaged_diff
.pending_hunks
.cursor::<DiffHunkSummary>(buffer);
// first, merge new hunks into pending_hunks
let mut edits = Vec::new();
let mut pending_hunks = Vec::new();
let mut prev_unstaged_hunk_buffer_offset = 0;
let mut prev_unstaged_hunk_base_text_offset = 0;
for DiffHunk {
buffer_range,
diff_base_byte_range,
@@ -250,58 +237,12 @@ impl BufferDiffInner {
..
} in hunks.iter().cloned()
{
let preceding_pending_hunks =
old_pending_hunks.slice(&buffer_range.start, Bias::Left, buffer);
pending_hunks.append(preceding_pending_hunks, buffer);
// skip all overlapping old pending hunks
while old_pending_hunks
.item()
.is_some_and(|preceding_pending_hunk_item| {
preceding_pending_hunk_item
.buffer_range
.overlaps(&buffer_range, buffer)
})
{
old_pending_hunks.next(buffer);
}
// merge into pending hunks
if (stage && secondary_status == DiffHunkSecondaryStatus::NoSecondaryHunk)
if (stage && secondary_status == DiffHunkSecondaryStatus::None)
|| (!stage && secondary_status == DiffHunkSecondaryStatus::HasSecondaryHunk)
{
continue;
}
pending_hunks.push(
PendingHunk {
buffer_range,
diff_base_byte_range,
buffer_version: buffer.version().clone(),
new_status: if stage {
DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
} else {
DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
},
},
buffer,
);
}
// append the remainder
pending_hunks.append(old_pending_hunks.suffix(buffer), buffer);
let mut prev_unstaged_hunk_buffer_offset = 0;
let mut prev_unstaged_hunk_base_text_offset = 0;
let mut edits = Vec::<(Range<usize>, String)>::new();
// then, iterate over all pending hunks (both new ones and the existing ones) and compute the edits
for PendingHunk {
buffer_range,
diff_base_byte_range,
..
} in pending_hunks.iter().cloned()
{
let skipped_hunks = unstaged_hunk_cursor.slice(&buffer_range.start, Bias::Left, buffer);
if let Some(secondary_hunk) = skipped_hunks.last() {
@@ -353,15 +294,22 @@ impl BufferDiffInner {
.chunks_in_range(diff_base_byte_range.clone())
.collect::<String>()
};
pending_hunks.push((
diff_base_byte_range.start,
PendingHunk {
buffer_version: buffer.version().clone(),
new_status: if stage {
DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
} else {
DiffHunkSecondaryStatus::SecondaryHunkAdditionPending
},
},
));
edits.push((index_range, replacement_text));
}
debug_assert!(edits.iter().is_sorted_by_key(|(range, _)| range.start));
let mut new_index_text = Rope::new();
let mut index_cursor = index_text.cursor(0);
for (old_range, replacement_text) in edits {
new_index_text.append(index_cursor.slice(old_range.start));
index_cursor.seek_forward(old_range.end);
@@ -406,14 +354,12 @@ impl BufferDiffInner {
});
let mut secondary_cursor = None;
let mut pending_hunks_cursor = None;
let mut pending_hunks = TreeMap::default();
if let Some(secondary) = secondary.as_ref() {
let mut cursor = secondary.hunks.cursor::<DiffHunkSummary>(buffer);
cursor.next(buffer);
secondary_cursor = Some(cursor);
let mut cursor = secondary.pending_hunks.cursor::<DiffHunkSummary>(buffer);
cursor.next(buffer);
pending_hunks_cursor = Some(cursor);
pending_hunks = secondary.pending_hunks.clone();
}
let max_point = buffer.max_point();
@@ -432,33 +378,16 @@ impl BufferDiffInner {
end_anchor = buffer.anchor_before(end_point);
}
let mut secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
let mut secondary_status = DiffHunkSecondaryStatus::None;
let mut has_pending = false;
if let Some(pending_cursor) = pending_hunks_cursor.as_mut() {
if start_anchor
.cmp(&pending_cursor.start().buffer_range.start, buffer)
.is_gt()
{
pending_cursor.seek_forward(&start_anchor, Bias::Left, buffer);
}
if let Some(pending_hunk) = pending_cursor.item() {
let mut pending_range = pending_hunk.buffer_range.to_point(buffer);
if pending_range.end.column > 0 {
pending_range.end.row += 1;
pending_range.end.column = 0;
}
if pending_range == (start_point..end_point) {
if !buffer.has_edits_since_in_range(
&pending_hunk.buffer_version,
start_anchor..end_anchor,
) {
has_pending = true;
secondary_status = pending_hunk.new_status;
}
}
if let Some(pending_hunk) = pending_hunks.get(&start_base) {
if !buffer.has_edits_since_in_range(
&pending_hunk.buffer_version,
start_anchor..end_anchor,
) {
has_pending = true;
secondary_status = pending_hunk.new_status;
}
}
@@ -520,7 +449,7 @@ impl BufferDiffInner {
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
buffer_range: hunk.buffer_range.clone(),
// The secondary status is not used by callers of this method.
secondary_status: DiffHunkSecondaryStatus::NoSecondaryHunk,
secondary_status: DiffHunkSecondaryStatus::None,
})
})
}
@@ -795,7 +724,7 @@ impl BufferDiff {
base_text,
hunks,
base_text_exists,
pending_hunks: SumTree::new(&buffer),
pending_hunks: TreeMap::default(),
}
}
}
@@ -811,8 +740,8 @@ impl BufferDiff {
cx.background_spawn(async move {
BufferDiffInner {
base_text: base_text_snapshot,
pending_hunks: SumTree::new(&buffer),
hunks: compute_hunks(base_text_pair, buffer),
pending_hunks: TreeMap::default(),
base_text_exists,
}
})
@@ -822,7 +751,7 @@ impl BufferDiff {
BufferDiffInner {
base_text: language::Buffer::build_empty_snapshot(cx),
hunks: SumTree::new(buffer),
pending_hunks: SumTree::new(buffer),
pending_hunks: TreeMap::default(),
base_text_exists: false,
}
}
@@ -838,7 +767,7 @@ impl BufferDiff {
pub fn clear_pending_hunks(&mut self, cx: &mut Context<Self>) {
if let Some(secondary_diff) = &self.secondary_diff {
secondary_diff.update(cx, |diff, _| {
diff.inner.pending_hunks = SumTree::from_summary(DiffHunkSummary::default());
diff.inner.pending_hunks.clear();
});
cx.emit(BufferDiffEvent::DiffChanged {
changed_range: Some(Anchor::MIN..Anchor::MAX),
@@ -854,17 +783,18 @@ impl BufferDiff {
file_exists: bool,
cx: &mut Context<Self>,
) -> Option<Rope> {
let (new_index_text, new_pending_hunks) = self.inner.stage_or_unstage_hunks(
let (new_index_text, pending_hunks) = self.inner.stage_or_unstage_hunks(
&self.secondary_diff.as_ref()?.read(cx).inner,
stage,
&hunks,
buffer,
file_exists,
);
if let Some(unstaged_diff) = &self.secondary_diff {
unstaged_diff.update(cx, |diff, _| {
diff.inner.pending_hunks = new_pending_hunks;
for (offset, pending_hunk) in pending_hunks {
diff.inner.pending_hunks.insert(offset, pending_hunk);
}
});
}
cx.emit(BufferDiffEvent::HunksStagedOrUnstaged(
@@ -986,9 +916,7 @@ impl BufferDiff {
}
_ => (true, Some(text::Anchor::MIN..text::Anchor::MAX)),
};
let pending_hunks = mem::replace(&mut self.inner.pending_hunks, SumTree::new(buffer));
let pending_hunks = mem::take(&mut self.inner.pending_hunks);
self.inner = new_state;
if !base_text_changed {
self.inner.pending_hunks = pending_hunks;
@@ -1221,21 +1149,21 @@ impl DiffHunkStatus {
pub fn deleted_none() -> Self {
Self {
kind: DiffHunkStatusKind::Deleted,
secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
secondary: DiffHunkSecondaryStatus::None,
}
}
pub fn added_none() -> Self {
Self {
kind: DiffHunkStatusKind::Added,
secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
secondary: DiffHunkSecondaryStatus::None,
}
}
pub fn modified_none() -> Self {
Self {
kind: DiffHunkStatusKind::Modified,
secondary: DiffHunkSecondaryStatus::NoSecondaryHunk,
secondary: DiffHunkSecondaryStatus::None,
}
}
}
@@ -1243,14 +1171,13 @@ impl DiffHunkStatus {
/// Range (crossing new lines), old, new
#[cfg(any(test, feature = "test-support"))]
#[track_caller]
pub fn assert_hunks<ExpectedText, HunkIter>(
diff_hunks: HunkIter,
pub fn assert_hunks<Iter>(
diff_hunks: Iter,
buffer: &text::BufferSnapshot,
diff_base: &str,
expected_hunks: &[(Range<u32>, ExpectedText, ExpectedText, DiffHunkStatus)],
expected_hunks: &[(Range<u32>, &str, &str, DiffHunkStatus)],
) where
HunkIter: Iterator<Item = DiffHunk>,
ExpectedText: AsRef<str>,
Iter: Iterator<Item = DiffHunk>,
{
let actual_hunks = diff_hunks
.map(|hunk| {
@@ -1270,14 +1197,14 @@ pub fn assert_hunks<ExpectedText, HunkIter>(
.map(|(r, old_text, new_text, status)| {
(
Point::new(r.start, 0)..Point::new(r.end, 0),
old_text.as_ref(),
new_text.as_ref().to_string(),
*old_text,
new_text.to_string(),
*status,
)
})
.collect();
pretty_assertions::assert_eq!(actual_hunks, expected_hunks);
assert_eq!(actual_hunks, expected_hunks);
}
#[cfg(test)]
@@ -1336,7 +1263,7 @@ mod tests {
);
diff = cx.update(|cx| BufferDiff::build_empty(&buffer, cx));
assert_hunks::<&str, _>(
assert_hunks(
diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None),
&buffer,
&diff_base,
@@ -1674,10 +1601,7 @@ mod tests {
.hunks_intersecting_range(hunk_range.clone(), &buffer, &cx)
.collect::<Vec<_>>();
for hunk in &hunks {
assert_ne!(
hunk.secondary_status,
DiffHunkSecondaryStatus::NoSecondaryHunk
)
assert_ne!(hunk.secondary_status, DiffHunkSecondaryStatus::None)
}
let new_index_text = diff
@@ -1956,10 +1880,10 @@ mod tests {
let hunk_to_change = hunk.clone();
let stage = match hunk.secondary_status {
DiffHunkSecondaryStatus::HasSecondaryHunk => {
hunk.secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
hunk.secondary_status = DiffHunkSecondaryStatus::None;
true
}
DiffHunkSecondaryStatus::NoSecondaryHunk => {
DiffHunkSecondaryStatus::None => {
hunk.secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk;
false
}

View File

@@ -2,6 +2,8 @@
//!
//! A view for exploring Zed components.
#![allow(unused, dead_code)]
use std::iter::Iterator;
use std::sync::Arc;
@@ -14,7 +16,7 @@ use gpui::{
use collections::HashMap;
use gpui::{ListState, ScrollHandle, UniformListScrollHandle};
use gpui::{ListState, UniformListScrollHandle};
use languages::LanguageRegistry;
use notifications::status_toast::{StatusToast, ToastIcon};
use project::Project;
@@ -88,7 +90,6 @@ enum PreviewPage {
struct ComponentPreview {
focus_handle: FocusHandle,
_view_scroll_handle: ScrollHandle,
nav_scroll_handle: UniformListScrollHandle,
component_map: HashMap<ComponentId, ComponentMetadata>,
active_page: PreviewPage,
@@ -130,7 +131,6 @@ impl ComponentPreview {
let mut component_preview = Self {
focus_handle: cx.focus_handle(),
_view_scroll_handle: ScrollHandle::new(),
nav_scroll_handle: UniformListScrollHandle::new(),
language_registry,
user_store,
@@ -401,10 +401,12 @@ impl ComponentPreview {
let component = self.component_map.get(&component_id);
if let Some(component) = component {
v_flex()
.w_full()
.flex_initial()
.min_h_full()
div()
.id("component-page-container")
.flex_1()
.size_full()
.overflow_x_hidden()
.overflow_y_scroll()
.child(self.render_preview(component, window, cx))
.into_any_element()
} else {
@@ -412,6 +414,7 @@ impl ComponentPreview {
.size_full()
.items_center()
.justify_center()
.text_center()
.child("Component not found")
.into_any_element()
}

View File

@@ -271,7 +271,10 @@ mod tests {
use gpui::{AppContext as _, BackgroundExecutor, TestAppContext, UpdateGlobal};
use indoc::indoc;
use language::{
language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
language_settings::{
AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
WordsCompletionMode,
},
Point,
};
use project::Project;
@@ -286,7 +289,13 @@ mod tests {
#[gpui::test(iterations = 10)]
async fn test_copilot(executor: BackgroundExecutor, cx: &mut TestAppContext) {
// flaky
init_test(cx, |_| {});
init_test(cx, |settings| {
settings.defaults.completions = Some(CompletionSettings {
words: WordsCompletionMode::Disabled,
lsp: true,
lsp_fetch_timeout_ms: 0,
});
});
let (copilot, copilot_lsp) = Copilot::fake(cx);
let mut cx = EditorLspTestContext::new_rust(
@@ -511,7 +520,13 @@ mod tests {
cx: &mut TestAppContext,
) {
// flaky
init_test(cx, |_| {});
init_test(cx, |settings| {
settings.defaults.completions = Some(CompletionSettings {
words: WordsCompletionMode::Disabled,
lsp: true,
lsp_fetch_timeout_ms: 0,
});
});
let (copilot, copilot_lsp) = Copilot::fake(cx);
let mut cx = EditorLspTestContext::new_rust(

View File

@@ -101,6 +101,7 @@ use itertools::Itertools;
use language::{
language_settings::{
self, all_language_settings, language_settings, InlayHintSettings, RewrapBehavior,
WordsCompletionMode,
},
point_from_lsp, text_diff_with_options, AutoindentMode, BracketMatch, BracketPair, Buffer,
Capability, CharKind, CodeLabel, CursorShape, Diagnostic, DiffOptions, EditPredictionsMode,
@@ -4021,9 +4022,8 @@ impl Editor {
} else {
return;
};
let show_completion_documentation = buffer
.read(cx)
.snapshot()
let buffer_snapshot = buffer.read(cx).snapshot();
let show_completion_documentation = buffer_snapshot
.settings_at(buffer_position, cx)
.show_completion_documentation;
@@ -4047,6 +4047,51 @@ impl Editor {
};
let completions =
provider.completions(&buffer, buffer_position, completion_context, window, cx);
let (old_range, word_kind) = buffer_snapshot.surrounding_word(buffer_position);
let (old_range, word_to_exclude) = if word_kind == Some(CharKind::Word) {
let word_to_exclude = buffer_snapshot
.text_for_range(old_range.clone())
.collect::<String>();
(
buffer_snapshot.anchor_before(old_range.start)
..buffer_snapshot.anchor_after(old_range.end),
Some(word_to_exclude),
)
} else {
(buffer_position..buffer_position, None)
};
let completion_settings = language_settings(
buffer_snapshot
.language_at(buffer_position)
.map(|language| language.name()),
buffer_snapshot.file(),
cx,
)
.completions;
// The document can be large, so stay in reasonable bounds when searching for words,
// otherwise completion pop-up might be slow to appear.
const WORD_LOOKUP_ROWS: u32 = 5_000;
let buffer_row = text::ToPoint::to_point(&buffer_position, &buffer_snapshot).row;
let min_word_search = buffer_snapshot.clip_point(
Point::new(buffer_row.saturating_sub(WORD_LOOKUP_ROWS), 0),
Bias::Left,
);
let max_word_search = buffer_snapshot.clip_point(
Point::new(buffer_row + WORD_LOOKUP_ROWS, 0).min(buffer_snapshot.max_point()),
Bias::Right,
);
let word_search_range = buffer_snapshot.point_to_offset(min_word_search)
..buffer_snapshot.point_to_offset(max_word_search);
let words = match completion_settings.words {
WordsCompletionMode::Disabled => Task::ready(HashMap::default()),
WordsCompletionMode::Enabled | WordsCompletionMode::Fallback => {
cx.background_spawn(async move {
buffer_snapshot.words_in_range(None, word_search_range)
})
}
};
let sort_completions = provider.sort_completions();
let id = post_inc(&mut self.next_completion_id);
@@ -4055,8 +4100,55 @@ impl Editor {
editor.update(&mut cx, |this, _| {
this.completion_tasks.retain(|(task_id, _)| *task_id >= id);
})?;
let completions = completions.await.log_err();
let menu = if let Some(completions) = completions {
let mut completions = completions.await.log_err().unwrap_or_default();
match completion_settings.words {
WordsCompletionMode::Enabled => {
completions.extend(
words
.await
.into_iter()
.filter(|(word, _)| word_to_exclude.as_ref() != Some(word))
.map(|(word, word_range)| Completion {
old_range: old_range.clone(),
new_text: word.clone(),
label: CodeLabel::plain(word, None),
documentation: None,
source: CompletionSource::BufferWord {
word_range,
resolved: false,
},
confirm: None,
}),
);
}
WordsCompletionMode::Fallback => {
if completions.is_empty() {
completions.extend(
words
.await
.into_iter()
.filter(|(word, _)| word_to_exclude.as_ref() != Some(word))
.map(|(word, word_range)| Completion {
old_range: old_range.clone(),
new_text: word.clone(),
label: CodeLabel::plain(word, None),
documentation: None,
source: CompletionSource::BufferWord {
word_range,
resolved: false,
},
confirm: None,
}),
);
}
}
WordsCompletionMode::Disabled => {}
}
let menu = if completions.is_empty() {
None
} else {
let mut menu = CompletionsMenu::new(
id,
sort_completions,
@@ -4070,8 +4162,6 @@ impl Editor {
.await;
menu.visible().then_some(menu)
} else {
None
};
editor.update_in(&mut cx, |editor, window, cx| {

View File

@@ -16,7 +16,8 @@ use gpui::{
use indoc::indoc;
use language::{
language_settings::{
AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent, PrettierSettings,
AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
LanguageSettingsContent, PrettierSettings,
},
BracketPairConfig,
Capability::ReadWrite,
@@ -30,7 +31,7 @@ use pretty_assertions::{assert_eq, assert_ne};
use project::project_settings::{LspSettings, ProjectSettings};
use project::FakeFs;
use serde_json::{self, json};
use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
use std::{
iter,
sync::atomic::{self, AtomicUsize},
@@ -9194,6 +9195,101 @@ async fn test_completion(cx: &mut TestAppContext) {
apply_additional_edits.await.unwrap();
}
#[gpui::test]
async fn test_words_completion(cx: &mut TestAppContext) {
let lsp_fetch_timeout_ms = 10;
init_test(cx, |language_settings| {
language_settings.defaults.completions = Some(CompletionSettings {
words: WordsCompletionMode::Fallback,
lsp: true,
lsp_fetch_timeout_ms: 10,
});
});
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
..lsp::CompletionOptions::default()
}),
signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
..lsp::ServerCapabilities::default()
},
cx,
)
.await;
let throttle_completions = Arc::new(AtomicBool::new(false));
let lsp_throttle_completions = throttle_completions.clone();
let _completion_requests_handler =
cx.lsp
.server
.on_request::<lsp::request::Completion, _, _>(move |_, cx| {
let lsp_throttle_completions = lsp_throttle_completions.clone();
async move {
if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
cx.background_executor()
.timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
.await;
}
Ok(Some(lsp::CompletionResponse::Array(vec![
lsp::CompletionItem {
label: "first".into(),
..Default::default()
},
lsp::CompletionItem {
label: "last".into(),
..Default::default()
},
])))
}
});
cx.set_state(indoc! {"
oneˇ
two
three
"});
cx.simulate_keystroke(".");
cx.executor().run_until_parked();
cx.condition(|editor, _| editor.context_menu_visible())
.await;
cx.update_editor(|editor, window, cx| {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
{
assert_eq!(
completion_menu_entries(&menu),
&["first", "last"],
"When LSP server is fast to reply, no fallback word completions are used"
);
} else {
panic!("expected completion menu to be open");
}
editor.cancel(&Cancel, window, cx);
});
cx.executor().run_until_parked();
cx.condition(|editor, _| !editor.context_menu_visible())
.await;
throttle_completions.store(true, atomic::Ordering::Release);
cx.simulate_keystroke(".");
cx.executor()
.advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
cx.executor().run_until_parked();
cx.condition(|editor, _| editor.context_menu_visible())
.await;
cx.update_editor(|editor, _, _| {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
{
assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
"When LSP server is slow, document words can be shown instead, if configured accordingly");
} else {
panic!("expected completion menu to be open");
}
});
}
#[gpui::test]
async fn test_multiline_completion(cx: &mut TestAppContext) {
init_test(cx, |_| {});

View File

@@ -55,7 +55,7 @@ use multi_buffer::{
Anchor, ExcerptId, ExcerptInfo, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow,
RowInfo,
};
use project::project_settings::{self, GitGutterSetting, GitHunkStyleSetting, ProjectSettings};
use project::project_settings::{self, GitGutterSetting, ProjectSettings};
use settings::Settings;
use smallvec::{smallvec, SmallVec};
use std::{
@@ -4415,7 +4415,7 @@ impl EditorElement {
}),
};
if let Some((hunk_bounds, background_color, corner_radii, status)) = hunk_to_paint {
if let Some((hunk_bounds, background_color, corner_radii, _)) = hunk_to_paint {
// Flatten the background color with the editor color to prevent
// elements below transparent hunks from showing through
let flattened_background_color = cx
@@ -4424,29 +4424,13 @@ impl EditorElement {
.editor_background
.blend(background_color);
if !Self::diff_hunk_hollow(status, cx) {
window.paint_quad(quad(
hunk_bounds,
corner_radii,
flattened_background_color,
Edges::default(),
transparent_black(),
));
} else {
let flattened_unstaged_background_color = cx
.theme()
.colors()
.editor_background
.blend(background_color.opacity(0.3));
window.paint_quad(quad(
hunk_bounds,
corner_radii,
flattened_unstaged_background_color,
Edges::all(Pixels(1.0)),
flattened_background_color,
));
}
window.paint_quad(quad(
hunk_bounds,
corner_radii,
flattened_background_color,
Edges::default(),
transparent_black(),
));
}
}
});
@@ -5651,18 +5635,6 @@ impl EditorElement {
&[run],
)
}
fn diff_hunk_hollow(status: DiffHunkStatus, cx: &mut App) -> bool {
let unstaged = status.has_secondary_hunk();
let unstaged_hollow = ProjectSettings::get_global(cx)
.git
.hunk_style
.map_or(false, |style| {
matches!(style, GitHunkStyleSetting::UnstagedHollow)
});
unstaged == unstaged_hollow
}
}
fn header_jump_data(
@@ -6814,9 +6786,10 @@ impl Element for EditorElement {
}
};
let unstaged = diff_status.has_secondary_hunk();
let hunk_opacity = if is_light { 0.16 } else { 0.12 };
let hollow_highlight = LineHighlight {
let staged_highlight = LineHighlight {
background: (background_color.opacity(if is_light {
0.08
} else {
@@ -6830,13 +6803,13 @@ impl Element for EditorElement {
}),
};
let filled_highlight =
let unstaged_highlight =
solid_background(background_color.opacity(hunk_opacity)).into();
let background = if Self::diff_hunk_hollow(diff_status, cx) {
hollow_highlight
let background = if unstaged {
unstaged_highlight
} else {
filled_highlight
staged_highlight
};
highlighted_rows

View File

@@ -488,7 +488,7 @@ async fn parse_commit_messages(
},
))
} else {
None
continue;
};
let remote = parsed_remote_url

View File

@@ -1,4 +1,5 @@
pub mod extension_builder;
mod extension_events;
mod extension_host_proxy;
mod extension_manifest;
mod types;
@@ -14,12 +15,14 @@ use gpui::{App, Task};
use language::LanguageName;
use semantic_version::SemanticVersion;
pub use crate::extension_events::*;
pub use crate::extension_host_proxy::*;
pub use crate::extension_manifest::*;
pub use crate::types::*;
/// Initializes the `extension` crate.
pub fn init(cx: &mut App) {
extension_events::init(cx);
ExtensionHostProxy::default_global(cx);
}

View File

@@ -0,0 +1,35 @@
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Global, ReadGlobal as _};
pub fn init(cx: &mut App) {
let extension_events = cx.new(ExtensionEvents::new);
cx.set_global(GlobalExtensionEvents(extension_events));
}
struct GlobalExtensionEvents(Entity<ExtensionEvents>);
impl Global for GlobalExtensionEvents {}
/// An event bus for broadcasting extension-related events throughout the app.
pub struct ExtensionEvents;
impl ExtensionEvents {
/// Returns the global [`ExtensionEvents`].
pub fn global(cx: &App) -> Entity<Self> {
GlobalExtensionEvents::global(cx).0.clone()
}
fn new(_cx: &mut Context<Self>) -> Self {
Self
}
pub fn emit(&mut self, event: Event, cx: &mut Context<Self>) {
cx.emit(event)
}
}
#[derive(Clone)]
pub enum Event {
ExtensionsUpdated,
}
impl EventEmitter<Event> for ExtensionEvents {}

View File

@@ -14,7 +14,7 @@ use collections::{btree_map, BTreeMap, BTreeSet, HashMap, HashSet};
use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder};
pub use extension::ExtensionManifest;
use extension::{
ExtensionContextServerProxy, ExtensionGrammarProxy, ExtensionHostProxy,
ExtensionContextServerProxy, ExtensionEvents, ExtensionGrammarProxy, ExtensionHostProxy,
ExtensionIndexedDocsProviderProxy, ExtensionLanguageProxy, ExtensionLanguageServerProxy,
ExtensionSlashCommandProxy, ExtensionSnippetProxy, ExtensionThemeProxy,
};
@@ -127,7 +127,6 @@ pub enum ExtensionOperation {
#[derive(Clone)]
pub enum Event {
ExtensionsUpdated,
StartedReloading,
ExtensionInstalled(Arc<str>),
ExtensionFailedToLoad(Arc<str>),
@@ -1214,7 +1213,9 @@ impl ExtensionStore {
self.extension_index = new_index;
cx.notify();
cx.emit(Event::ExtensionsUpdated);
ExtensionEvents::global(cx).update(cx, |this, cx| {
this.emit(extension::Event::ExtensionsUpdated, cx)
});
cx.spawn(|this, mut cx| async move {
cx.background_spawn({

View File

@@ -780,6 +780,7 @@ fn init_test(cx: &mut TestAppContext) {
let store = SettingsStore::test(cx);
cx.set_global(store);
release_channel::init(SemanticVersion::default(), cx);
extension::init(cx);
theme::init(theme::LoadThemes::JustBase, cx);
Project::init_settings(cx);
ExtensionSettings::register(cx);

View File

@@ -17,6 +17,7 @@ client.workspace = true
collections.workspace = true
db.workspace = true
editor.workspace = true
extension.workspace = true
extension_host.workspace = true
feature_flags.workspace = true
fs.workspace = true

View File

@@ -9,6 +9,7 @@ use std::{ops::Range, sync::Arc};
use client::{ExtensionMetadata, ExtensionProvides};
use collections::{BTreeMap, BTreeSet};
use editor::{Editor, EditorElement, EditorStyle};
use extension::ExtensionEvents;
use extension_host::{ExtensionManifest, ExtensionOperation, ExtensionStore};
use feature_flags::FeatureFlagAppExt as _;
use fuzzy::{match_strings, StringMatchCandidate};
@@ -212,7 +213,7 @@ pub struct ExtensionsPage {
query_editor: Entity<Editor>,
query_contains_error: bool,
provides_filter: Option<ExtensionProvides>,
_subscriptions: [gpui::Subscription; 2],
_subscriptions: Vec<gpui::Subscription>,
extension_fetch_task: Option<Task<()>>,
upsells: BTreeSet<Feature>,
}
@@ -226,15 +227,12 @@ impl ExtensionsPage {
cx.new(|cx| {
let store = ExtensionStore::global(cx);
let workspace_handle = workspace.weak_handle();
let subscriptions = [
let subscriptions = vec![
cx.observe(&store, |_: &mut Self, _, cx| cx.notify()),
cx.subscribe_in(
&store,
window,
move |this, _, event, window, cx| match event {
extension_host::Event::ExtensionsUpdated => {
this.fetch_extensions_debounced(cx)
}
extension_host::Event::ExtensionInstalled(extension_id) => this
.on_extension_installed(
workspace_handle.clone(),
@@ -245,6 +243,15 @@ impl ExtensionsPage {
_ => {}
},
),
cx.subscribe_in(
&ExtensionEvents::global(cx),
window,
move |this, _, event, _window, cx| match event {
extension::Event::ExtensionsUpdated => {
this.fetch_extensions_debounced(cx);
}
},
),
];
let query_editor = cx.new(|cx| {

View File

@@ -1,5 +1,5 @@
use futures::channel::oneshot;
use fuzzy::StringMatchCandidate;
use fuzzy::{StringMatch, StringMatchCandidate};
use picker::{Picker, PickerDelegate};
use project::DirectoryLister;
use std::{
@@ -9,7 +9,7 @@ use std::{
Arc,
},
};
use ui::{prelude::*, LabelLike, ListItemSpacing};
use ui::{prelude::*, HighlightedLabel, ListItemSpacing};
use ui::{Context, ListItem, Window};
use util::{maybe, paths::compare_paths};
use workspace::Workspace;
@@ -22,6 +22,7 @@ pub struct OpenPathDelegate {
selected_index: usize,
directory_state: Option<DirectoryState>,
matches: Vec<usize>,
string_matches: Vec<StringMatch>,
cancel_flag: Arc<AtomicBool>,
should_dismiss: bool,
}
@@ -34,6 +35,7 @@ impl OpenPathDelegate {
selected_index: 0,
directory_state: None,
matches: Vec::new(),
string_matches: Vec::new(),
cancel_flag: Arc::new(AtomicBool::new(false)),
should_dismiss: true,
}
@@ -223,6 +225,7 @@ impl PickerDelegate for OpenPathDelegate {
if suffix == "" {
this.update(&mut cx, |this, cx| {
this.delegate.matches.clear();
this.delegate.string_matches.clear();
this.delegate
.matches
.extend(match_candidates.iter().map(|m| m.path.id));
@@ -249,6 +252,7 @@ impl PickerDelegate for OpenPathDelegate {
this.update(&mut cx, |this, cx| {
this.delegate.matches.clear();
this.delegate.string_matches = matches.clone();
this.delegate
.matches
.extend(matches.into_iter().map(|m| m.candidate_id));
@@ -337,13 +341,22 @@ impl PickerDelegate for OpenPathDelegate {
let m = self.matches.get(ix)?;
let directory_state = self.directory_state.as_ref()?;
let candidate = directory_state.match_candidates.get(*m)?;
let highlight_positions = self
.string_matches
.iter()
.find(|string_match| string_match.candidate_id == *m)
.map(|string_match| string_match.positions.clone())
.unwrap_or_default();
Some(
ListItem::new(ix)
.spacing(ListItemSpacing::Sparse)
.inset(true)
.toggle_state(selected)
.child(LabelLike::new().child(candidate.path.string.clone())),
.child(HighlightedLabel::new(
candidate.path.string.clone(),
highlight_positions,
)),
)
}

View File

@@ -795,8 +795,9 @@ impl GitRepository for RealGitRepository {
cx: AsyncApp,
) -> BoxFuture<Result<()>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
cx.background_spawn(async move {
let mut cmd = new_smol_command("git");
let mut cmd = new_smol_command(&git_binary_path);
cmd.current_dir(&working_directory?)
.envs(env)
.args(["commit", "--quiet", "-m"])

View File

@@ -146,7 +146,7 @@ impl CommitModal {
cx: &mut Context<Self>,
) -> Self {
let panel = git_panel.read(cx);
let suggested_commit_message = panel.suggest_commit_message(cx);
let suggested_commit_message = panel.suggest_commit_message();
let commit_editor = git_panel.update(cx, |git_panel, cx| {
git_panel.set_modal_open(true, cx);

View File

@@ -1413,7 +1413,7 @@ impl GitPanel {
return Some(message.to_string());
}
self.suggest_commit_message(cx)
self.suggest_commit_message()
.filter(|message| !message.trim().is_empty())
}
@@ -1582,15 +1582,7 @@ impl GitPanel {
}
/// Suggests a commit message based on the changed files and their statuses
pub fn suggest_commit_message(&self, cx: &App) -> Option<String> {
if let Some(merge_message) = self
.active_repository
.as_ref()
.and_then(|repo| repo.read(cx).merge_message.as_ref())
{
return Some(merge_message.clone());
}
pub fn suggest_commit_message(&self) -> Option<String> {
let git_status_entry = if let Some(staged_entry) = &self.single_staged_entry {
Some(staged_entry)
} else if let Some(single_tracked_entry) = &self.single_tracked_entry {
@@ -1726,6 +1718,19 @@ impl GitPanel {
}));
}
fn update_editor_placeholder(&mut self, cx: &mut Context<Self>) {
let suggested_commit_message = self.suggest_commit_message();
let placeholder_text = suggested_commit_message
.as_deref()
.unwrap_or("Enter commit message");
self.commit_editor.update(cx, |editor, cx| {
editor.set_placeholder_text(Arc::from(placeholder_text), cx)
});
cx.notify();
}
pub(crate) fn fetch(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if !self.can_push_and_pull(cx) {
return;
@@ -2182,6 +2187,7 @@ impl GitPanel {
git_panel.clear_pending();
}
git_panel.update_visible_entries(cx);
git_panel.update_editor_placeholder(cx);
git_panel.update_scrollbar_properties(window, cx);
})
.ok();
@@ -2217,7 +2223,7 @@ impl GitPanel {
git_panel.commit_editor = cx.new(|cx| {
commit_message_editor(
buffer,
git_panel.suggest_commit_message(cx).as_deref(),
git_panel.suggest_commit_message().as_deref(),
git_panel.project.clone(),
true,
window,
@@ -2237,15 +2243,7 @@ impl GitPanel {
fn update_visible_entries(&mut self, cx: &mut Context<Self>) {
self.entries.clear();
self.single_staged_entry.take();
self.single_tracked_entry.take();
self.conflicted_count = 0;
self.conflicted_staged_count = 0;
self.new_count = 0;
self.tracked_count = 0;
self.new_staged_count = 0;
self.tracked_staged_count = 0;
self.entry_count = 0;
self.single_staged_entry.take();
let mut changed_entries = Vec::new();
let mut new_entries = Vec::new();
let mut conflict_entries = Vec::new();
@@ -2397,15 +2395,6 @@ impl GitPanel {
self.select_first_entry_if_none(cx);
let suggested_commit_message = self.suggest_commit_message(cx);
let placeholder_text = suggested_commit_message
.as_deref()
.unwrap_or("Enter commit message");
self.commit_editor.update(cx, |editor, cx| {
editor.set_placeholder_text(Arc::from(placeholder_text), cx)
});
cx.notify();
}
@@ -2929,10 +2918,6 @@ impl GitPanel {
.disabled(!can_commit || self.modal_open)
.on_click({
cx.listener(move |this, _: &ClickEvent, window, cx| {
telemetry::event!(
"Git Committed",
source = "Git Panel"
);
this.commit_changes(window, cx)
})
}),
@@ -3063,24 +3048,21 @@ impl GitPanel {
"No Git repositories"
},
))
.children({
let worktree_count = self.project.read(cx).visible_worktrees(cx).count();
(worktree_count > 0 && self.active_repository.is_none()).then(|| {
h_flex().w_full().justify_around().child(
panel_filled_button("Initialize Repository")
.tooltip(Tooltip::for_action_title_in(
"git init",
&git::Init,
&self.focus_handle,
))
.on_click(move |_, _, cx| {
cx.defer(move |cx| {
cx.dispatch_action(&git::Init);
})
}),
)
})
})
.children(self.active_repository.is_none().then(|| {
h_flex().w_full().justify_around().child(
panel_filled_button("Initialize Repository")
.tooltip(Tooltip::for_action_title_in(
"git init",
&git::Init,
&self.focus_handle,
))
.on_click(move |_, _, cx| {
cx.defer(move |cx| {
cx.dispatch_action(&git::Init);
})
}),
)
}))
.text_ui_sm(cx)
.mx_auto()
.text_color(Color::Placeholder.color(cx)),

View File

@@ -278,7 +278,7 @@ impl ProjectDiff {
has_staged_hunks = true;
has_unstaged_hunks = true;
}
DiffHunkSecondaryStatus::NoSecondaryHunk
DiffHunkSecondaryStatus::None
| DiffHunkSecondaryStatus::SecondaryHunkRemovalPending => {
has_staged_hunks = true;
}
@@ -308,7 +308,7 @@ impl ProjectDiff {
fn handle_editor_event(
&mut self,
editor: &Entity<Editor>,
_: &Entity<Editor>,
event: &EditorEvent,
window: &mut Window,
cx: &mut Context<Self>,
@@ -330,11 +330,6 @@ impl ProjectDiff {
}
_ => {}
}
if editor.focus_handle(cx).contains_focused(window, cx) {
if self.multibuffer.read(cx).is_empty() {
self.focus_handle.focus(window)
}
}
}
fn load_buffers(&mut self, cx: &mut Context<Self>) -> Vec<Task<Result<DiffBuffer>>> {
@@ -924,6 +919,12 @@ impl Render for ProjectDiffToolbar {
&StageAndNext,
&focus_handle,
))
// don't actually disable the button so it's mashable
.color(if button_states.stage {
Color::Default
} else {
Color::Disabled
})
.on_click(cx.listener(|this, _, window, cx| {
this.dispatch_action(&StageAndNext, window, cx)
})),
@@ -935,6 +936,11 @@ impl Render for ProjectDiffToolbar {
&UnstageAndNext,
&focus_handle,
))
.color(if button_states.unstage {
Color::Default
} else {
Color::Disabled
})
.on_click(cx.listener(|this, _, window, cx| {
this.dispatch_action(&UnstageAndNext, window, cx)
})),

View File

@@ -1948,6 +1948,14 @@ impl Interactivity {
if pending_mouse_down.is_some() && hitbox.is_hovered(window) {
captured_mouse_down = pending_mouse_down.take();
window.refresh();
} else if pending_mouse_down.is_some() {
// Clear the pending mouse down event (without firing click handlers)
// if the hitbox is not being hovered.
// This avoids dragging elements that changed their position
// immediately after being clicked.
// See https://github.com/zed-industries/zed/issues/24600 for more details
pending_mouse_down.take();
window.refresh();
}
}
// Fire click handlers during the bubble phase.

View File

@@ -92,6 +92,8 @@ extern "C" {
#[ctor]
unsafe fn build_classes() {
let _: () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: NO];
WINDOW_CLASS = build_window_class("GPUIWindow", class!(NSWindow));
PANEL_CLASS = build_window_class("GPUIPanel", class!(NSPanel));
VIEW_CLASS = {

View File

@@ -4145,6 +4145,63 @@ impl BufferSnapshot {
None
}
}
pub fn words_in_range(
&self,
query: Option<&str>,
range: Range<usize>,
) -> HashMap<String, Range<Anchor>> {
if query.map_or(false, |query| query.is_empty()) {
return HashMap::default();
}
let classifier = CharClassifier::new(self.language.clone().map(|language| LanguageScope {
language,
override_id: None,
}));
let mut query_ix = 0;
let query = query.map(|query| query.chars().collect::<Vec<_>>());
let query_len = query.as_ref().map_or(0, |query| query.len());
let mut words = HashMap::default();
let mut current_word_start_ix = None;
let mut chunk_ix = range.start;
for chunk in self.chunks(range, false) {
for (i, c) in chunk.text.char_indices() {
let ix = chunk_ix + i;
if classifier.is_word(c) {
if current_word_start_ix.is_none() {
current_word_start_ix = Some(ix);
}
if let Some(query) = &query {
if query_ix < query_len {
let query_c = query.get(query_ix).expect(
"query_ix is a vec of chars, which we access only if before the end",
);
if c.to_lowercase().eq(query_c.to_lowercase()) {
query_ix += 1;
}
}
}
continue;
} else if let Some(word_start) = current_word_start_ix.take() {
if query_ix == query_len {
let word_range = self.anchor_before(word_start)..self.anchor_after(ix);
words.insert(
self.text_for_range(word_start..ix).collect::<String>(),
word_range,
);
}
}
query_ix = 0;
}
chunk_ix += chunk.text.len();
}
words
}
}
fn indent_size_for_line(text: &text::BufferSnapshot, row: u32) -> IndentSize {

View File

@@ -13,6 +13,7 @@ use proto::deserialize_operation;
use rand::prelude::*;
use regex::RegexBuilder;
use settings::SettingsStore;
use std::collections::BTreeSet;
use std::{
env,
ops::Range,
@@ -3140,6 +3141,93 @@ fn test_trailing_whitespace_ranges(mut rng: StdRng) {
);
}
#[gpui::test]
fn test_words_in_range(cx: &mut gpui::App) {
init_settings(cx, |_| {});
let contents = r#"let word=öäpple.bar你 Öäpple word2-öÄpPlE-Pizza-word ÖÄPPLE word"#;
let buffer = cx.new(|cx| {
let buffer = Buffer::local(contents, cx).with_language(Arc::new(rust_lang()), cx);
assert_eq!(buffer.text(), contents);
buffer.check_invariants();
buffer
});
buffer.update(cx, |buffer, _| {
let snapshot = buffer.snapshot();
assert_eq!(
BTreeSet::from_iter(["Pizza".to_string()]),
snapshot
.words_in_range(Some("piz"), 0..snapshot.len())
.into_keys()
.collect::<BTreeSet<_>>()
);
assert_eq!(
BTreeSet::from_iter([
"öäpple".to_string(),
"Öäpple".to_string(),
"öÄpPlE".to_string(),
"ÖÄPPLE".to_string(),
]),
snapshot
.words_in_range(Some("öp"), 0..snapshot.len())
.into_keys()
.collect::<BTreeSet<_>>()
);
assert_eq!(
BTreeSet::from_iter([
"öÄpPlE".to_string(),
"Öäpple".to_string(),
"ÖÄPPLE".to_string(),
"öäpple".to_string(),
]),
snapshot
.words_in_range(Some("öÄ"), 0..snapshot.len())
.into_keys()
.collect::<BTreeSet<_>>()
);
assert_eq!(
BTreeSet::default(),
snapshot
.words_in_range(Some("öÄ好"), 0..snapshot.len())
.into_keys()
.collect::<BTreeSet<_>>()
);
assert_eq!(
BTreeSet::from_iter(["bar你".to_string(),]),
snapshot
.words_in_range(Some(""), 0..snapshot.len())
.into_keys()
.collect::<BTreeSet<_>>()
);
assert_eq!(
BTreeSet::default(),
snapshot
.words_in_range(Some(""), 0..snapshot.len())
.into_keys()
.collect::<BTreeSet<_>>()
);
assert_eq!(
BTreeSet::from_iter([
"bar你".to_string(),
"öÄpPlE".to_string(),
"Öäpple".to_string(),
"ÖÄPPLE".to_string(),
"öäpple".to_string(),
"let".to_string(),
"Pizza".to_string(),
"word".to_string(),
"word2".to_string(),
]),
snapshot
.words_in_range(None, 0..snapshot.len())
.into_keys()
.collect::<BTreeSet<_>>()
);
});
}
fn ruby_lang() -> Language {
Language::new(
LanguageConfig {

View File

@@ -79,10 +79,10 @@ pub struct LanguageSettings {
/// The column at which to soft-wrap lines, for buffers where soft-wrap
/// is enabled.
pub preferred_line_length: u32,
// Whether to show wrap guides (vertical rulers) in the editor.
// Setting this to true will show a guide at the 'preferred_line_length' value
// if softwrap is set to 'preferred_line_length', and will show any
// additional guides as specified by the 'wrap_guides' setting.
/// Whether to show wrap guides (vertical rulers) in the editor.
/// Setting this to true will show a guide at the 'preferred_line_length' value
/// if softwrap is set to 'preferred_line_length', and will show any
/// additional guides as specified by the 'wrap_guides' setting.
pub show_wrap_guides: bool,
/// Character counts at which to show wrap guides (vertical rulers) in the editor.
pub wrap_guides: Vec<usize>,
@@ -137,7 +137,7 @@ pub struct LanguageSettings {
pub use_on_type_format: bool,
/// Whether indentation of pasted content should be adjusted based on the context.
pub auto_indent_on_paste: bool,
// Controls how the editor handles the autoclosed characters.
/// Controls how the editor handles the autoclosed characters.
pub always_treat_brackets_as_autoclosed: bool,
/// Which code actions to run on save
pub code_actions_on_format: HashMap<String, bool>,
@@ -151,6 +151,8 @@ pub struct LanguageSettings {
/// Whether to display inline and alongside documentation for items in the
/// completions menu.
pub show_completion_documentation: bool,
/// Completion settings for this language.
pub completions: CompletionSettings,
}
impl LanguageSettings {
@@ -306,6 +308,50 @@ pub struct AllLanguageSettingsContent {
pub file_types: HashMap<Arc<str>, Vec<String>>,
}
/// Controls how completions are processed for this language.
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct CompletionSettings {
/// Controls how words are completed.
/// For large documents, not all words may be fetched for completion.
///
/// Default: `fallback`
#[serde(default = "default_words_completion_mode")]
pub words: WordsCompletionMode,
/// Whether to fetch LSP completions or not.
///
/// Default: true
#[serde(default = "default_true")]
pub lsp: bool,
/// When fetching LSP completions, determines how long to wait for a response of a particular server.
/// When set to 0, waits indefinitely.
///
/// Default: 500
#[serde(default = "lsp_fetch_timeout_ms")]
pub lsp_fetch_timeout_ms: u64,
}
/// Controls how document's words are completed.
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum WordsCompletionMode {
/// Always fetch document's words for completions.
Enabled,
/// Only if LSP response errors/times out/is empty,
/// use document's words to show completions.
Fallback,
/// Never fetch or complete document's words for completions.
Disabled,
}
fn default_words_completion_mode() -> WordsCompletionMode {
WordsCompletionMode::Fallback
}
fn lsp_fetch_timeout_ms() -> u64 {
500
}
/// The settings for a particular language.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct LanguageSettingsContent {
@@ -478,6 +524,8 @@ pub struct LanguageSettingsContent {
///
/// Default: true
pub show_completion_documentation: Option<bool>,
/// Controls how completions are processed for this language.
pub completions: Option<CompletionSettings>,
}
/// The behavior of `editor::Rewrap`.
@@ -1381,6 +1429,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
&mut settings.show_completion_documentation,
src.show_completion_documentation,
);
merge(&mut settings.completions, src.completions);
}
/// Allows to enable/disable formatting with Prettier

View File

@@ -2007,17 +2007,22 @@ impl MultiBuffer {
cx: &App,
) -> Option<(Entity<Buffer>, Point, ExcerptId)> {
let snapshot = self.read(cx);
let (buffer, point, is_main_buffer) =
snapshot.point_to_buffer_point(point.to_point(&snapshot))?;
Some((
self.buffers
.borrow()
.get(&buffer.remote_id())?
let point = point.to_point(&snapshot);
let mut cursor = snapshot.cursor::<Point>();
cursor.seek(&point);
cursor.region().and_then(|region| {
if !region.is_main_buffer {
return None;
}
let overshoot = point - region.range.start;
let buffer_point = region.buffer_range.start + overshoot;
let buffer = self.buffers.borrow()[&region.buffer.remote_id()]
.buffer
.clone(),
point,
is_main_buffer,
))
.clone();
Some((buffer, buffer_point, region.excerpt.id))
})
}
pub fn buffer_point_to_anchor(
@@ -4171,36 +4176,22 @@ impl MultiBufferSnapshot {
let region = cursor.region()?;
let overshoot = offset - region.range.start;
let buffer_offset = region.buffer_range.start + overshoot;
if buffer_offset == region.buffer.len() + 1
&& region.has_trailing_newline
&& !region.is_main_buffer
{
return Some((&cursor.excerpt()?.buffer, cursor.main_buffer_position()?));
} else if buffer_offset > region.buffer.len() {
if buffer_offset > region.buffer.len() {
return None;
}
Some((region.buffer, buffer_offset))
}
pub fn point_to_buffer_point(
&self,
point: Point,
) -> Option<(&BufferSnapshot, Point, ExcerptId)> {
pub fn point_to_buffer_point(&self, point: Point) -> Option<(&BufferSnapshot, Point, bool)> {
let mut cursor = self.cursor::<Point>();
cursor.seek(&point);
let region = cursor.region()?;
let overshoot = point - region.range.start;
let buffer_point = region.buffer_range.start + overshoot;
let excerpt = cursor.excerpt()?;
if buffer_point == region.buffer.max_point() + Point::new(1, 0)
&& region.has_trailing_newline
&& !region.is_main_buffer
{
return Some((&excerpt.buffer, cursor.main_buffer_position()?, excerpt.id));
} else if buffer_point > region.buffer.max_point() {
if buffer_point > region.buffer.max_point() {
return None;
}
Some((region.buffer, buffer_point, excerpt.id))
Some((region.buffer, buffer_point, region.is_main_buffer))
}
pub fn suggested_indents(
@@ -4742,9 +4733,6 @@ impl MultiBufferSnapshot {
.buffer
.text_summary_for_range(region.buffer_range.start.key..buffer_point),
);
if point == region.range.end.key && region.has_trailing_newline {
position.add_assign(&D::from_text_summary(&TextSummary::newline()));
}
return Some(position);
} else {
return Some(D::from_text_summary(&self.text_summary()));

View File

@@ -3121,100 +3121,6 @@ fn test_summaries_for_anchors(cx: &mut TestAppContext) {
assert_eq!(point_2, Point::new(3, 0));
}
#[gpui::test]
fn test_trailing_deletion_without_newline(cx: &mut TestAppContext) {
let base_text_1 = "one\ntwo".to_owned();
let text_1 = "one\n".to_owned();
let buffer_1 = cx.new(|cx| Buffer::local(text_1, cx));
let diff_1 = cx.new(|cx| BufferDiff::new_with_base_text(&base_text_1, &buffer_1, cx));
cx.run_until_parked();
let multibuffer = cx.new(|cx| {
let mut multibuffer = MultiBuffer::singleton(buffer_1.clone(), cx);
multibuffer.add_diff(diff_1.clone(), cx);
multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx);
multibuffer
});
let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
(multibuffer.snapshot(cx), multibuffer.subscribe())
});
assert_new_snapshot(
&multibuffer,
&mut snapshot,
&mut subscription,
cx,
indoc!(
"
one
- two
"
),
);
assert_eq!(snapshot.max_point(), Point::new(2, 0));
assert_eq!(snapshot.len(), 8);
assert_eq!(
snapshot
.dimensions_from_points::<Point>([Point::new(2, 0)])
.collect::<Vec<_>>(),
vec![Point::new(2, 0)]
);
let (_, translated_offset) = snapshot.point_to_buffer_offset(Point::new(2, 0)).unwrap();
assert_eq!(translated_offset, "one\n".len());
let (_, translated_point, _) = snapshot.point_to_buffer_point(Point::new(2, 0)).unwrap();
assert_eq!(translated_point, Point::new(1, 0));
// The same, for an excerpt that's not at the end of the multibuffer.
let text_2 = "foo\n".to_owned();
let buffer_2 = cx.new(|cx| Buffer::local(&text_2, cx));
multibuffer.update(cx, |multibuffer, cx| {
multibuffer.push_excerpts(
buffer_2.clone(),
[ExcerptRange {
context: Point::new(0, 0)..Point::new(1, 0),
primary: None,
}],
cx,
);
});
assert_new_snapshot(
&multibuffer,
&mut snapshot,
&mut subscription,
cx,
indoc!(
"
one
- two
foo
"
),
);
assert_eq!(
snapshot
.dimensions_from_points::<Point>([Point::new(2, 0)])
.collect::<Vec<_>>(),
vec![Point::new(2, 0)]
);
let buffer_1_id = buffer_1.read_with(cx, |buffer_1, _| buffer_1.remote_id());
let (buffer, translated_offset) = snapshot.point_to_buffer_offset(Point::new(2, 0)).unwrap();
assert_eq!(buffer.remote_id(), buffer_1_id);
assert_eq!(translated_offset, "one\n".len());
let (buffer, translated_point, _) = snapshot.point_to_buffer_point(Point::new(2, 0)).unwrap();
assert_eq!(buffer.remote_id(), buffer_1_id);
assert_eq!(translated_point, Point::new(1, 0));
}
fn format_diff(
text: &str,
row_infos: &Vec<RowInfo>,
@@ -3473,12 +3379,16 @@ fn assert_position_translation(snapshot: &MultiBufferSnapshot) {
}
}
if let Some((buffer, offset)) = snapshot.point_to_buffer_offset(snapshot.max_point()) {
assert!(offset <= buffer.len());
}
if let Some((buffer, point, _)) = snapshot.point_to_buffer_point(snapshot.max_point()) {
assert!(point <= buffer.max_point());
}
let point = snapshot.max_point();
let Some((buffer, offset)) = snapshot.point_to_buffer_offset(point) else {
return;
};
assert!(offset <= buffer.len(),);
let Some((buffer, point, _)) = snapshot.point_to_buffer_point(point) else {
return;
};
assert!(point <= buffer.max_point(),);
}
fn assert_line_indents(snapshot: &MultiBufferSnapshot) {

View File

@@ -318,9 +318,20 @@ impl GitStore {
}
// Update the statuses and merge message but keep everything else.
let existing_handle = handle.clone();
existing_handle.update(cx, |existing_handle, _| {
existing_handle.update(cx, |existing_handle, cx| {
existing_handle.repository_entry = repo.clone();
if matches!(git_repo, GitRepo::Local { .. }) {
if matches!(git_repo, GitRepo::Local { .. })
&& existing_handle.merge_message != merge_message
{
if let (Some(merge_message), Some(buffer)) =
(&merge_message, &existing_handle.commit_message_buffer)
{
buffer.update(cx, |buffer, cx| {
if buffer.is_empty() {
buffer.set_text(merge_message.as_str(), cx);
}
})
}
existing_handle.merge_message = merge_message;
}
});
@@ -1246,6 +1257,7 @@ impl Repository {
buffer_store: Entity<BufferStore>,
cx: &mut Context<Self>,
) -> Task<Result<Entity<Buffer>>> {
let merge_message = self.merge_message.clone();
cx.spawn(|repository, mut cx| async move {
let buffer = buffer_store
.update(&mut cx, |buffer_store, cx| buffer_store.create_buffer(cx))?
@@ -1258,6 +1270,12 @@ impl Repository {
})?;
}
if let Some(merge_message) = merge_message {
buffer.update(&mut cx, |buffer, cx| {
buffer.set_text(merge_message.as_str(), cx)
})?;
}
repository.update(&mut cx, |repository, _| {
repository.commit_message_buffer = Some(buffer.clone());
})?;

View File

@@ -23,13 +23,13 @@ use client::{proto, TypedEnvelope};
use collections::{btree_map, BTreeMap, BTreeSet, HashMap, HashSet};
use futures::{
future::{join_all, Shared},
select,
select, select_biased,
stream::FuturesUnordered,
AsyncWriteExt, Future, FutureExt, StreamExt,
};
use globset::{Glob, GlobBuilder, GlobMatcher, GlobSet, GlobSetBuilder};
use gpui::{
App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, PromptLevel, SharedString, Task,
App, AppContext, AsyncApp, Context, Entity, EventEmitter, PromptLevel, SharedString, Task,
WeakEntity,
};
use http_client::HttpClient;
@@ -4325,6 +4325,15 @@ impl LspStore {
let offset = position.to_offset(&snapshot);
let scope = snapshot.language_scope_at(offset);
let language = snapshot.language().cloned();
let completion_settings = language_settings(
language.as_ref().map(|language| language.name()),
buffer.read(cx).file(),
cx,
)
.completions;
if !completion_settings.lsp {
return Task::ready(Ok(Vec::new()));
}
let server_ids: Vec<_> = buffer.update(cx, |buffer, cx| {
local
@@ -4341,23 +4350,51 @@ impl LspStore {
});
let buffer = buffer.clone();
let lsp_timeout = completion_settings.lsp_fetch_timeout_ms;
let lsp_timeout = if lsp_timeout > 0 {
Some(Duration::from_millis(lsp_timeout))
} else {
None
};
cx.spawn(move |this, mut cx| async move {
let mut tasks = Vec::with_capacity(server_ids.len());
this.update(&mut cx, |this, cx| {
this.update(&mut cx, |lsp_store, cx| {
for server_id in server_ids {
let lsp_adapter = this.language_server_adapter_for_id(server_id);
tasks.push((
lsp_adapter,
this.request_lsp(
buffer.clone(),
LanguageServerToQuery::Other(server_id),
GetCompletions {
position,
context: context.clone(),
let lsp_adapter = lsp_store.language_server_adapter_for_id(server_id);
let lsp_timeout = lsp_timeout
.map(|lsp_timeout| cx.background_executor().timer(lsp_timeout));
let mut timeout = cx.background_spawn(async move {
match lsp_timeout {
Some(lsp_timeout) => {
lsp_timeout.await;
true
},
cx,
),
));
None => false,
}
}).fuse();
let mut lsp_request = lsp_store.request_lsp(
buffer.clone(),
LanguageServerToQuery::Other(server_id),
GetCompletions {
position,
context: context.clone(),
},
cx,
).fuse();
let new_task = cx.background_spawn(async move {
select_biased! {
response = lsp_request => response,
timeout_happened = timeout => {
if timeout_happened {
log::warn!("Fetching completions from server {server_id} timed out, timeout ms: {}", completion_settings.lsp_fetch_timeout_ms);
return anyhow::Ok(Vec::new())
} else {
lsp_request.await
}
},
}
});
tasks.push((lsp_adapter, new_task));
}
})?;
@@ -4416,47 +4453,58 @@ impl LspStore {
{
did_resolve = true;
}
} else {
resolve_word_completion(
&buffer_snapshot,
&mut completions.borrow_mut()[completion_index],
);
}
}
} else {
for completion_index in completion_indices {
let Some(server_id) = completions.borrow()[completion_index].source.server_id()
else {
continue;
let server_id = {
let completion = &completions.borrow()[completion_index];
completion.source.server_id()
};
if let Some(server_id) = server_id {
let server_and_adapter = this
.read_with(&cx, |lsp_store, _| {
let server = lsp_store.language_server_for_id(server_id)?;
let adapter =
lsp_store.language_server_adapter_for_id(server.server_id())?;
Some((server, adapter))
})
.ok()
.flatten();
let Some((server, adapter)) = server_and_adapter else {
continue;
};
let server_and_adapter = this
.read_with(&cx, |lsp_store, _| {
let server = lsp_store.language_server_for_id(server_id)?;
let adapter =
lsp_store.language_server_adapter_for_id(server.server_id())?;
Some((server, adapter))
})
.ok()
.flatten();
let Some((server, adapter)) = server_and_adapter else {
continue;
};
let resolved = Self::resolve_completion_local(
server,
&buffer_snapshot,
completions.clone(),
completion_index,
)
.await
.log_err()
.is_some();
if resolved {
Self::regenerate_completion_labels(
adapter,
let resolved = Self::resolve_completion_local(
server,
&buffer_snapshot,
completions.clone(),
completion_index,
)
.await
.log_err();
did_resolve = true;
.log_err()
.is_some();
if resolved {
Self::regenerate_completion_labels(
adapter,
&buffer_snapshot,
completions.clone(),
completion_index,
)
.await
.log_err();
did_resolve = true;
}
} else {
resolve_word_completion(
&buffer_snapshot,
&mut completions.borrow_mut()[completion_index],
);
}
}
}
@@ -4500,7 +4548,9 @@ impl LspStore {
);
server.request::<lsp::request::ResolveCompletionItem>(*lsp_completion.clone())
}
CompletionSource::Custom => return Ok(()),
CompletionSource::BufferWord { .. } | CompletionSource::Custom => {
return Ok(());
}
}
};
let resolved_completion = request.await?;
@@ -4641,7 +4691,9 @@ impl LspStore {
}
serde_json::to_string(lsp_completion).unwrap().into_bytes()
}
CompletionSource::Custom => return Ok(()),
CompletionSource::Custom | CompletionSource::BufferWord { .. } => {
return Ok(());
}
}
};
let request = proto::ResolveCompletionDocumentation {
@@ -8172,51 +8224,54 @@ impl LspStore {
}
pub(crate) fn serialize_completion(completion: &CoreCompletion) -> proto::Completion {
let (source, server_id, lsp_completion, lsp_defaults, resolved) = match &completion.source {
let mut serialized_completion = proto::Completion {
old_start: Some(serialize_anchor(&completion.old_range.start)),
old_end: Some(serialize_anchor(&completion.old_range.end)),
new_text: completion.new_text.clone(),
..proto::Completion::default()
};
match &completion.source {
CompletionSource::Lsp {
server_id,
lsp_completion,
lsp_defaults,
resolved,
} => (
proto::completion::Source::Lsp as i32,
server_id.0 as u64,
serde_json::to_vec(lsp_completion).unwrap(),
lsp_defaults
} => {
serialized_completion.source = proto::completion::Source::Lsp as i32;
serialized_completion.server_id = server_id.0 as u64;
serialized_completion.lsp_completion = serde_json::to_vec(lsp_completion).unwrap();
serialized_completion.lsp_defaults = lsp_defaults
.as_deref()
.map(|lsp_defaults| serde_json::to_vec(lsp_defaults).unwrap()),
*resolved,
),
CompletionSource::Custom => (
proto::completion::Source::Custom as i32,
0,
Vec::new(),
None,
true,
),
};
proto::Completion {
old_start: Some(serialize_anchor(&completion.old_range.start)),
old_end: Some(serialize_anchor(&completion.old_range.end)),
new_text: completion.new_text.clone(),
server_id,
lsp_completion,
lsp_defaults,
resolved,
source,
.map(|lsp_defaults| serde_json::to_vec(lsp_defaults).unwrap());
serialized_completion.resolved = *resolved;
}
CompletionSource::BufferWord {
word_range,
resolved,
} => {
serialized_completion.source = proto::completion::Source::BufferWord as i32;
serialized_completion.buffer_word_start = Some(serialize_anchor(&word_range.start));
serialized_completion.buffer_word_end = Some(serialize_anchor(&word_range.end));
serialized_completion.resolved = *resolved;
}
CompletionSource::Custom => {
serialized_completion.source = proto::completion::Source::Custom as i32;
serialized_completion.resolved = true;
}
}
serialized_completion
}
pub(crate) fn deserialize_completion(completion: proto::Completion) -> Result<CoreCompletion> {
let old_start = completion
.old_start
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("invalid old start"))?;
.context("invalid old start")?;
let old_end = completion
.old_end
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("invalid old end"))?;
.context("invalid old end")?;
Ok(CoreCompletion {
old_range: old_start..old_end,
new_text: completion.new_text,
@@ -8232,6 +8287,20 @@ impl LspStore {
.transpose()?,
resolved: completion.resolved,
},
Some(proto::completion::Source::BufferWord) => {
let word_range = completion
.buffer_word_start
.and_then(deserialize_anchor)
.context("invalid buffer word start")?
..completion
.buffer_word_end
.and_then(deserialize_anchor)
.context("invalid buffer word end")?;
CompletionSource::BufferWord {
word_range,
resolved: completion.resolved,
}
}
_ => anyhow::bail!("Unexpected completion source {}", completion.source),
},
})
@@ -8296,6 +8365,40 @@ impl LspStore {
}
}
fn resolve_word_completion(snapshot: &BufferSnapshot, completion: &mut Completion) {
let CompletionSource::BufferWord {
word_range,
resolved,
} = &mut completion.source
else {
return;
};
if *resolved {
return;
}
if completion.new_text
!= snapshot
.text_for_range(word_range.clone())
.collect::<String>()
{
return;
}
let mut offset = 0;
for chunk in snapshot.chunks(word_range.clone(), true) {
let end_offset = offset + chunk.text.len();
if let Some(highlight_id) = chunk.syntax_highlight_id {
completion
.label
.runs
.push((offset..end_offset, highlight_id));
}
offset = end_offset;
}
*resolved = true;
}
impl EventEmitter<LspStoreEvent> for LspStore {}
fn remove_empty_hover_blocks(mut hover: Hover) -> Option<Hover> {

View File

@@ -388,6 +388,10 @@ pub enum CompletionSource {
resolved: bool,
},
Custom,
BufferWord {
word_range: Range<Anchor>,
resolved: bool,
},
}
impl CompletionSource {

View File

@@ -168,10 +168,6 @@ pub struct GitSettings {
///
/// Default: on
pub inline_blame: Option<InlineBlameSettings>,
/// How hunks are displayed visually in the editor.
///
/// Default: staged_hollow
pub hunk_style: Option<GitHunkStyleSetting>,
}
impl GitSettings {
@@ -207,11 +203,20 @@ impl GitSettings {
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum GitHunkStyleSetting {
/// Show unstaged hunks with a filled background and staged hunks hollow.
/// Show unstaged hunks with a transparent background
#[default]
StagedHollow,
/// Show unstaged hunks hollow and staged hunks with a filled background.
UnstagedHollow,
Transparent,
/// Show unstaged hunks with a pattern background
Pattern,
/// Show unstaged hunks with a border background
Border,
/// Show staged hunks with a pattern background
StagedPattern,
/// Show staged hunks with a pattern background
StagedTransparent,
/// Show staged hunks with a pattern background
StagedBorder,
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]

View File

@@ -941,7 +941,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
cx.executor().run_until_parked();
// Start the language server by opening a buffer with a compatible file extension.
project
let _ = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp(path!("/the-root/src/a.rs"), cx)
})
@@ -6008,7 +6008,7 @@ async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) {
0..0,
"// the-deleted-contents\n",
"",
DiffHunkStatus::deleted(DiffHunkSecondaryStatus::NoSecondaryHunk),
DiffHunkStatus::deleted(DiffHunkSecondaryStatus::None),
)],
);
});
@@ -6168,12 +6168,7 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
"",
DiffHunkStatus::deleted(HasSecondaryHunk),
),
(
1..2,
"two\n",
"TWO\n",
DiffHunkStatus::modified(NoSecondaryHunk),
),
(1..2, "two\n", "TWO\n", DiffHunkStatus::modified(None)),
(
3..4,
"four\n",
@@ -6222,12 +6217,7 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
"",
DiffHunkStatus::deleted(HasSecondaryHunk),
),
(
1..2,
"two\n",
"TWO\n",
DiffHunkStatus::modified(NoSecondaryHunk),
),
(1..2, "two\n", "TWO\n", DiffHunkStatus::modified(None)),
(
3..4,
"four\n",
@@ -6266,12 +6256,7 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
"",
DiffHunkStatus::deleted(HasSecondaryHunk),
),
(
1..2,
"two\n",
"TWO\n",
DiffHunkStatus::modified(NoSecondaryHunk),
),
(1..2, "two\n", "TWO\n", DiffHunkStatus::modified(None)),
(
3..4,
"four\n",
@@ -6292,223 +6277,6 @@ async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
} else {
panic!("Unexpected event {event:?}");
}
// Allow writing to the git index to succeed again.
fs.set_error_message_for_index_write("/dir/.git".as_ref(), None);
// Stage two hunks with separate operations.
uncommitted_diff.update(cx, |diff, cx| {
let hunks = diff.hunks(&snapshot, cx).collect::<Vec<_>>();
diff.stage_or_unstage_hunks(true, &hunks[0..1], &snapshot, true, cx);
diff.stage_or_unstage_hunks(true, &hunks[2..3], &snapshot, true, cx);
});
// Both staged hunks appear as pending.
uncommitted_diff.update(cx, |diff, cx| {
assert_hunks(
diff.hunks(&snapshot, cx),
&snapshot,
&diff.base_text_string().unwrap(),
&[
(
0..0,
"zero\n",
"",
DiffHunkStatus::deleted(SecondaryHunkRemovalPending),
),
(
1..2,
"two\n",
"TWO\n",
DiffHunkStatus::modified(NoSecondaryHunk),
),
(
3..4,
"four\n",
"FOUR\n",
DiffHunkStatus::modified(SecondaryHunkRemovalPending),
),
],
);
});
// Both staging operations take effect.
cx.run_until_parked();
uncommitted_diff.update(cx, |diff, cx| {
assert_hunks(
diff.hunks(&snapshot, cx),
&snapshot,
&diff.base_text_string().unwrap(),
&[
(0..0, "zero\n", "", DiffHunkStatus::deleted(NoSecondaryHunk)),
(
1..2,
"two\n",
"TWO\n",
DiffHunkStatus::modified(NoSecondaryHunk),
),
(
3..4,
"four\n",
"FOUR\n",
DiffHunkStatus::modified(NoSecondaryHunk),
),
],
);
});
}
#[allow(clippy::format_collect)]
#[gpui::test]
async fn test_staging_lots_of_hunks_fast(cx: &mut gpui::TestAppContext) {
use DiffHunkSecondaryStatus::*;
init_test(cx);
let different_lines = (0..500)
.step_by(5)
.map(|i| format!("diff {}\n", i))
.collect::<Vec<String>>();
let committed_contents = (0..500).map(|i| format!("{}\n", i)).collect::<String>();
let file_contents = (0..500)
.map(|i| {
if i % 5 == 0 {
different_lines[i / 5].clone()
} else {
format!("{}\n", i)
}
})
.collect::<String>();
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(
"/dir",
json!({
".git": {},
"file.txt": file_contents.clone()
}),
)
.await;
fs.set_head_for_repo(
"/dir/.git".as_ref(),
&[("file.txt".into(), committed_contents.clone())],
);
fs.set_index_for_repo(
"/dir/.git".as_ref(),
&[("file.txt".into(), committed_contents.clone())],
);
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
let buffer = project
.update(cx, |project, cx| {
project.open_local_buffer("/dir/file.txt", cx)
})
.await
.unwrap();
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
let uncommitted_diff = project
.update(cx, |project, cx| {
project.open_uncommitted_diff(buffer.clone(), cx)
})
.await
.unwrap();
let range = Anchor::MIN..snapshot.anchor_after(snapshot.max_point());
let mut expected_hunks: Vec<(Range<u32>, String, String, DiffHunkStatus)> = (0..500)
.step_by(5)
.map(|i| {
(
i as u32..i as u32 + 1,
format!("{}\n", i),
different_lines[i / 5].clone(),
DiffHunkStatus::modified(HasSecondaryHunk),
)
})
.collect();
// The hunks are initially unstaged
uncommitted_diff.read_with(cx, |diff, cx| {
assert_hunks(
diff.hunks(&snapshot, cx),
&snapshot,
&diff.base_text_string().unwrap(),
&expected_hunks,
);
});
for (_, _, _, status) in expected_hunks.iter_mut() {
*status = DiffHunkStatus::modified(SecondaryHunkRemovalPending);
}
// Stage every hunk with a different call
uncommitted_diff.update(cx, |diff, cx| {
let hunks = diff
.hunks_intersecting_range(range.clone(), &snapshot, cx)
.collect::<Vec<_>>();
for hunk in hunks {
diff.stage_or_unstage_hunks(true, &[hunk], &snapshot, true, cx);
}
assert_hunks(
diff.hunks(&snapshot, cx),
&snapshot,
&diff.base_text_string().unwrap(),
&expected_hunks,
);
});
// If we wait, we'll have no pending hunks
cx.run_until_parked();
for (_, _, _, status) in expected_hunks.iter_mut() {
*status = DiffHunkStatus::modified(NoSecondaryHunk);
}
uncommitted_diff.update(cx, |diff, cx| {
assert_hunks(
diff.hunks(&snapshot, cx),
&snapshot,
&diff.base_text_string().unwrap(),
&expected_hunks,
);
});
for (_, _, _, status) in expected_hunks.iter_mut() {
*status = DiffHunkStatus::modified(SecondaryHunkAdditionPending);
}
// Unstage every hunk with a different call
uncommitted_diff.update(cx, |diff, cx| {
let hunks = diff
.hunks_intersecting_range(range, &snapshot, cx)
.collect::<Vec<_>>();
for hunk in hunks {
diff.stage_or_unstage_hunks(false, &[hunk], &snapshot, true, cx);
}
assert_hunks(
diff.hunks(&snapshot, cx),
&snapshot,
&diff.base_text_string().unwrap(),
&expected_hunks,
);
});
// If we wait, we'll have no pending hunks, again
cx.run_until_parked();
for (_, _, _, status) in expected_hunks.iter_mut() {
*status = DiffHunkStatus::modified(HasSecondaryHunk);
}
uncommitted_diff.update(cx, |diff, cx| {
assert_hunks(
diff.hunks(&snapshot, cx),
&snapshot,
&diff.base_text_string().unwrap(),
&expected_hunks,
);
});
}
#[gpui::test]

View File

@@ -7,13 +7,24 @@ use handlebars::{Handlebars, RenderError};
use language::{BufferSnapshot, LanguageName, Point};
use parking_lot::Mutex;
use serde::Serialize;
use std::{ops::Range, path::PathBuf, sync::Arc, time::Duration};
use std::{
ops::Range,
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use text::LineEnding;
use util::ResultExt;
#[derive(Serialize)]
pub struct AssistantSystemPromptContext {
pub worktree_root_names: Vec<String>,
pub worktrees: Vec<AssistantSystemPromptWorktree>,
}
#[derive(Serialize)]
pub struct AssistantSystemPromptWorktree {
pub root_name: String,
pub abs_path: Arc<Path>,
}
#[derive(Serialize)]
@@ -223,11 +234,9 @@ impl PromptBuilder {
pub fn generate_assistant_system_prompt(
&self,
worktree_root_names: Vec<String>,
worktrees: Vec<AssistantSystemPromptWorktree>,
) -> Result<String, RenderError> {
let prompt = AssistantSystemPromptContext {
worktree_root_names,
};
let prompt = AssistantSystemPromptContext { worktrees };
self.handlebars
.lock()
.render("assistant_system_prompt", &prompt)

View File

@@ -1002,10 +1002,13 @@ message Completion {
bool resolved = 6;
Source source = 7;
optional bytes lsp_defaults = 8;
optional Anchor buffer_word_start = 9;
optional Anchor buffer_word_end = 10;
enum Source {
Lsp = 0;
Custom = 1;
BufferWord = 2;
}
}

View File

@@ -406,7 +406,6 @@ where
self.seek_internal(pos, bias, &mut (), cx)
}
/// Advances the cursor and returns traversed items as a tree.
#[track_caller]
pub fn slice<Target>(
&mut self,

View File

@@ -225,15 +225,6 @@ impl<T: Item> SumTree<T> {
}))
}
/// Useful in cases where the item type has a non-trivial context type, but the zero value of the summary type doesn't depend on that context.
pub fn from_summary(summary: T::Summary) -> Self {
SumTree(Arc::new(Node::Leaf {
summary,
items: ArrayVec::new(),
item_summaries: ArrayVec::new(),
}))
}
pub fn from_item(item: T, cx: &<T::Summary as Summary>::Context) -> Self {
let mut tree = Self::new(cx);
tree.push(item, cx);

View File

@@ -935,19 +935,12 @@ impl Terminal {
if is_path_surrounded_by_common_symbols(&file_path) {
word_match = Match::new(
word_match.start().add(term, Boundary::Grid, 1),
word_match.end().sub(term, Boundary::Grid, 1),
word_match.start().add(term, Boundary::Cursor, 1),
word_match.end().sub(term, Boundary::Cursor, 1),
);
file_path = file_path[1..file_path.len() - 1].to_owned();
}
while file_path.ends_with(':') {
file_path.pop();
word_match = Match::new(
*word_match.start(),
word_match.end().sub(term, Boundary::Grid, 1),
);
}
let mut colon_count = 0;
for c in file_path.chars() {
if c == ':' {
@@ -973,7 +966,7 @@ impl Terminal {
let stripped_len = file_path.len() - last_index;
word_match = Match::new(
*word_match.start(),
word_match.end().sub(term, Boundary::Grid, stripped_len),
word_match.end().sub(term, Boundary::Cursor, stripped_len),
);
file_path = file_path[0..last_index].to_owned();
}

View File

@@ -136,7 +136,6 @@ where
pub trait AnchorRangeExt {
fn cmp(&self, b: &Range<Anchor>, buffer: &BufferSnapshot) -> Ordering;
fn overlaps(&self, b: &Range<Anchor>, buffer: &BufferSnapshot) -> bool;
}
impl AnchorRangeExt for Range<Anchor> {
@@ -146,8 +145,4 @@ impl AnchorRangeExt for Range<Anchor> {
ord => ord,
}
}
fn overlaps(&self, other: &Range<Anchor>, buffer: &BufferSnapshot) -> bool {
self.start.cmp(&other.end, buffer).is_lt() && other.start.cmp(&self.end, buffer).is_lt()
}
}

View File

@@ -193,7 +193,7 @@ impl Render for ModalLayer {
.child(
h_flex()
.occlude()
.child(active_modal.modal.view())
.child(div().child(active_modal.modal.view()))
.on_mouse_down(MouseButton::Left, |_, _, cx| {
cx.stop_propagation();
}),

View File

@@ -8,7 +8,7 @@ use ui::{animation::DefaultAnimations, prelude::*};
use crate::Workspace;
const DEFAULT_TOAST_DURATION: Duration = Duration::from_secs(10);
const DEFAULT_TOAST_DURATION: Duration = Duration::from_millis(2400);
const MINIMUM_RESUME_DURATION: Duration = Duration::from_millis(800);
actions!(toast, [RunAction]);

View File

@@ -61,7 +61,7 @@ use std::{
path::{Component, Path, PathBuf},
pin::Pin,
sync::{
atomic::{self, AtomicI32, AtomicUsize, Ordering::SeqCst},
atomic::{self, AtomicU32, AtomicUsize, Ordering::SeqCst},
Arc,
},
time::{Duration, Instant},
@@ -1525,7 +1525,6 @@ impl LocalWorktree {
fs,
fs_case_sensitive,
status_updates_tx: scan_states_tx,
scans_running: Arc::new(AtomicI32::new(0)),
executor: background,
scan_requests_rx,
path_prefixes_to_scan_rx,
@@ -4250,6 +4249,11 @@ struct PathEntry {
scan_id: usize,
}
#[derive(Debug, Default)]
struct FsScanned {
status_scans: Arc<AtomicU32>,
}
impl sum_tree::Item for PathEntry {
type Summary = PathEntrySummary;
@@ -4317,7 +4321,6 @@ struct BackgroundScanner {
fs: Arc<dyn Fs>,
fs_case_sensitive: bool,
status_updates_tx: UnboundedSender<ScanState>,
scans_running: Arc<AtomicI32>,
executor: BackgroundExecutor,
scan_requests_rx: channel::Receiver<ScanRequest>,
path_prefixes_to_scan_rx: channel::Receiver<PathPrefixScanRequest>,
@@ -4425,13 +4428,13 @@ impl BackgroundScanner {
// Perform an initial scan of the directory.
drop(scan_job_tx);
self.scan_dirs(true, scan_job_rx).await;
let scans_running = self.scan_dirs(true, scan_job_rx).await;
{
let mut state = self.state.lock();
state.snapshot.completed_scan_id = state.snapshot.scan_id;
}
let scanning = self.scans_running.load(atomic::Ordering::Acquire) > 0;
let scanning = scans_running.status_scans.load(atomic::Ordering::Acquire) > 0;
self.send_status_update(scanning, SmallVec::new());
// Process any any FS events that occurred while performing the initial scan.
@@ -4458,7 +4461,7 @@ impl BackgroundScanner {
// these before handling changes reported by the filesystem.
request = self.next_scan_request().fuse() => {
let Ok(request) = request else { break };
let scanning = self.scans_running.load(atomic::Ordering::Acquire) > 0;
let scanning = scans_running.status_scans.load(atomic::Ordering::Acquire) > 0;
if !self.process_scan_request(request, scanning).await {
return;
}
@@ -4481,7 +4484,7 @@ impl BackgroundScanner {
self.process_events(vec![abs_path]).await;
}
}
let scanning = self.scans_running.load(atomic::Ordering::Acquire) > 0;
let scanning = scans_running.status_scans.load(atomic::Ordering::Acquire) > 0;
self.send_status_update(scanning, request.done);
}
@@ -4675,7 +4678,7 @@ impl BackgroundScanner {
.await;
self.update_ignore_statuses(scan_job_tx).await;
self.scan_dirs(false, scan_job_rx).await;
let scans_running = self.scan_dirs(false, scan_job_rx).await;
let status_update = if !dot_git_abs_paths.is_empty() {
Some(self.update_git_repositories(dot_git_abs_paths))
@@ -4686,7 +4689,6 @@ impl BackgroundScanner {
let phase = self.phase;
let status_update_tx = self.status_updates_tx.clone();
let state = self.state.clone();
let scans_running = self.scans_running.clone();
self.executor
.spawn(async move {
if let Some(status_update) = status_update {
@@ -4702,7 +4704,7 @@ impl BackgroundScanner {
#[cfg(test)]
state.snapshot.check_git_invariants();
}
let scanning = scans_running.load(atomic::Ordering::Acquire) > 0;
let scanning = scans_running.status_scans.load(atomic::Ordering::Acquire) > 0;
send_status_update_inner(phase, state, status_update_tx, scanning, SmallVec::new());
})
.detach();
@@ -4727,8 +4729,9 @@ impl BackgroundScanner {
}
drop(scan_job_tx);
}
let scans_running = Arc::new(AtomicU32::new(0));
while let Ok(job) = scan_job_rx.recv().await {
self.scan_dir(&job).await.log_err();
self.scan_dir(&scans_running, &job).await.log_err();
}
!mem::take(&mut self.state.lock().paths_to_scan).is_empty()
@@ -4738,16 +4741,16 @@ impl BackgroundScanner {
&self,
enable_progress_updates: bool,
scan_jobs_rx: channel::Receiver<ScanJob>,
) {
) -> FsScanned {
if self
.status_updates_tx
.unbounded_send(ScanState::Started)
.is_err()
{
return;
return FsScanned::default();
}
inc_scans_running(&self.scans_running);
let scans_running = Arc::new(AtomicU32::new(1));
let progress_update_count = AtomicUsize::new(0);
self.executor
.scoped(|scope| {
@@ -4792,7 +4795,7 @@ impl BackgroundScanner {
// Recursively load directories from the file system.
job = scan_jobs_rx.recv().fuse() => {
let Ok(job) = job else { break };
if let Err(err) = self.scan_dir(&job).await {
if let Err(err) = self.scan_dir(&scans_running, &job).await {
if job.path.as_ref() != Path::new("") {
log::error!("error scanning directory {:?}: {}", job.abs_path, err);
}
@@ -4805,7 +4808,10 @@ impl BackgroundScanner {
})
.await;
dec_scans_running(&self.scans_running, 1);
scans_running.fetch_sub(1, atomic::Ordering::Release);
FsScanned {
status_scans: scans_running,
}
}
fn send_status_update(&self, scanning: bool, barrier: SmallVec<[barrier::Sender; 1]>) -> bool {
@@ -4818,7 +4824,7 @@ impl BackgroundScanner {
)
}
async fn scan_dir(&self, job: &ScanJob) -> Result<()> {
async fn scan_dir(&self, scans_running: &Arc<AtomicU32>, job: &ScanJob) -> Result<()> {
let root_abs_path;
let root_char_bag;
{
@@ -4873,7 +4879,7 @@ impl BackgroundScanner {
self.watcher.as_ref(),
);
if let Some(local_repo) = repo {
inc_scans_running(&self.scans_running);
scans_running.fetch_add(1, atomic::Ordering::Release);
git_status_update_jobs
.push(self.schedule_git_statuses_update(&mut state, local_repo));
}
@@ -4996,7 +5002,7 @@ impl BackgroundScanner {
let task_state = self.state.clone();
let phase = self.phase;
let status_updates_tx = self.status_updates_tx.clone();
let scans_running = self.scans_running.clone();
let scans_running = scans_running.clone();
self.executor
.spawn(async move {
if !git_status_update_jobs.is_empty() {
@@ -5004,7 +5010,7 @@ impl BackgroundScanner {
let status_updated = status_updates
.iter()
.any(|update_result| update_result.is_ok());
dec_scans_running(&scans_running, status_updates.len() as i32);
scans_running.fetch_sub(status_updates.len() as u32, atomic::Ordering::Release);
if status_updated {
let scanning = scans_running.load(atomic::Ordering::Acquire) > 0;
send_status_update_inner(
@@ -5464,7 +5470,6 @@ impl BackgroundScanner {
}
};
inc_scans_running(&self.scans_running);
status_updates
.push(self.schedule_git_statuses_update(&mut state, local_repository));
}
@@ -5497,12 +5502,9 @@ impl BackgroundScanner {
});
}
let scans_running = self.scans_running.clone();
self.executor.spawn(async move {
let updates_finished: Vec<Result<(), oneshot::Canceled>> =
let _updates_finished: Vec<Result<(), oneshot::Canceled>> =
join_all(status_updates).await;
let n = updates_finished.len();
dec_scans_running(&scans_running, n as i32);
})
}
@@ -5510,15 +5512,106 @@ impl BackgroundScanner {
fn schedule_git_statuses_update(
&self,
state: &mut BackgroundScannerState,
local_repository: LocalRepositoryEntry,
mut local_repository: LocalRepositoryEntry,
) -> oneshot::Receiver<()> {
let repository_name = local_repository.work_directory.display_name();
let path_key = local_repository.work_directory.path_key();
let job_state = self.state.clone();
let (tx, rx) = oneshot::channel();
state.repository_scans.insert(
local_repository.work_directory.path_key(),
self.executor
.spawn(do_git_status_update(job_state, local_repository, tx)),
path_key.clone(),
self.executor.spawn(async move {
update_branches(&job_state, &mut local_repository)
.await
.log_err();
log::trace!("updating git statuses for repo {repository_name}",);
let t0 = Instant::now();
let Some(statuses) = local_repository
.repo()
.status(&[git::WORK_DIRECTORY_REPO_PATH.clone()])
.log_err()
else {
return;
};
log::trace!(
"computed git statuses for repo {repository_name} in {:?}",
t0.elapsed()
);
let t0 = Instant::now();
let mut changed_paths = Vec::new();
let snapshot = job_state.lock().snapshot.snapshot.clone();
let Some(mut repository) = snapshot
.repository(path_key)
.context(
"Tried to update git statuses for a repository that isn't in the snapshot",
)
.log_err()
else {
return;
};
let merge_head_shas = local_repository.repo().merge_head_shas();
if merge_head_shas != local_repository.current_merge_head_shas {
mem::take(&mut repository.current_merge_conflicts);
}
let mut new_entries_by_path = SumTree::new(&());
for (repo_path, status) in statuses.entries.iter() {
let project_path = repository.work_directory.try_unrelativize(repo_path);
new_entries_by_path.insert_or_replace(
StatusEntry {
repo_path: repo_path.clone(),
status: *status,
},
&(),
);
if status.is_conflicted() {
repository.current_merge_conflicts.insert(repo_path.clone());
}
if let Some(path) = project_path {
changed_paths.push(path);
}
}
repository.statuses_by_path = new_entries_by_path;
let mut state = job_state.lock();
state
.snapshot
.repositories
.insert_or_replace(repository, &());
state.snapshot.git_repositories.update(
&local_repository.work_directory_id,
|entry| {
entry.current_merge_head_shas = merge_head_shas;
entry.merge_message = std::fs::read_to_string(
local_repository.dot_git_dir_abs_path.join("MERGE_MSG"),
)
.ok()
.and_then(|merge_msg| Some(merge_msg.lines().next()?.to_owned()));
entry.status_scan_id += 1;
},
);
util::extend_sorted(
&mut state.changed_paths,
changed_paths,
usize::MAX,
Ord::cmp,
);
log::trace!(
"applied git status updates for repo {repository_name} in {:?}",
t0.elapsed(),
);
tx.send(()).ok();
}),
);
rx
}
@@ -5550,15 +5643,6 @@ impl BackgroundScanner {
}
}
fn inc_scans_running(scans_running: &AtomicI32) {
scans_running.fetch_add(1, atomic::Ordering::Release);
}
fn dec_scans_running(scans_running: &AtomicI32, by: i32) {
let old = scans_running.fetch_sub(by, atomic::Ordering::Release);
debug_assert!(old >= by);
}
fn send_status_update_inner(
phase: BackgroundScannerPhase,
state: Arc<Mutex<BackgroundScannerState>>,
@@ -5606,100 +5690,6 @@ async fn update_branches(
Ok(())
}
async fn do_git_status_update(
job_state: Arc<Mutex<BackgroundScannerState>>,
mut local_repository: LocalRepositoryEntry,
tx: oneshot::Sender<()>,
) {
let repository_name = local_repository.work_directory.display_name();
log::trace!("updating git branches for repo {repository_name}");
update_branches(&job_state, &mut local_repository)
.await
.log_err();
let t0 = Instant::now();
log::trace!("updating git statuses for repo {repository_name}");
let Some(statuses) = local_repository
.repo()
.status(&[git::WORK_DIRECTORY_REPO_PATH.clone()])
.log_err()
else {
return;
};
log::trace!(
"computed git statuses for repo {repository_name} in {:?}",
t0.elapsed()
);
let t0 = Instant::now();
let mut changed_paths = Vec::new();
let snapshot = job_state.lock().snapshot.snapshot.clone();
let Some(mut repository) = snapshot
.repository(local_repository.work_directory.path_key())
.context("Tried to update git statuses for a repository that isn't in the snapshot")
.log_err()
else {
return;
};
let merge_head_shas = local_repository.repo().merge_head_shas();
if merge_head_shas != local_repository.current_merge_head_shas {
mem::take(&mut repository.current_merge_conflicts);
}
let mut new_entries_by_path = SumTree::new(&());
for (repo_path, status) in statuses.entries.iter() {
let project_path = repository.work_directory.try_unrelativize(repo_path);
new_entries_by_path.insert_or_replace(
StatusEntry {
repo_path: repo_path.clone(),
status: *status,
},
&(),
);
if status.is_conflicted() {
repository.current_merge_conflicts.insert(repo_path.clone());
}
if let Some(path) = project_path {
changed_paths.push(path);
}
}
repository.statuses_by_path = new_entries_by_path;
let mut state = job_state.lock();
state
.snapshot
.repositories
.insert_or_replace(repository, &());
state
.snapshot
.git_repositories
.update(&local_repository.work_directory_id, |entry| {
entry.current_merge_head_shas = merge_head_shas;
entry.merge_message =
std::fs::read_to_string(local_repository.dot_git_dir_abs_path.join("MERGE_MSG"))
.ok()
.and_then(|merge_msg| Some(merge_msg.lines().next()?.to_owned()));
entry.status_scan_id += 1;
});
util::extend_sorted(
&mut state.changed_paths,
changed_paths,
usize::MAX,
Ord::cmp,
);
log::trace!(
"applied git status updates for repo {repository_name} in {:?}",
t0.elapsed(),
);
tx.send(()).ok();
}
fn build_diff(
phase: BackgroundScannerPhase,
old_snapshot: &Snapshot,

View File

@@ -2426,10 +2426,11 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) {
});
}
// NOTE: This test always fails on Windows, because on Windows, unlike on Unix,
// you can't rename a directory which some program has already open. This is a
// limitation of the Windows. See:
// https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
// NOTE:
// This test always fails on Windows, because on Windows, unlike on Unix, you can't rename
// a directory which some program has already open.
// This is a limitation of the Windows.
// See: https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
#[gpui::test]
#[cfg_attr(target_os = "windows", ignore)]
async fn test_file_status(cx: &mut TestAppContext) {
@@ -3574,6 +3575,7 @@ fn git_checkout(name: &str, repo: &git2::Repository) {
repo.checkout_head(None).expect("Failed to check out head");
}
#[allow(dead_code)]
#[track_caller]
fn git_status(repo: &git2::Repository) -> collections::HashMap<String, git2::Status> {
repo.statuses(None)

View File

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition.workspace = true
name = "zed"
version = "0.178.4"
version = "0.179.0"
publish.workspace = true
license = "GPL-3.0-or-later"
authors = ["Zed Team <hi@zed.dev>"]

View File

@@ -1 +1 @@
preview
dev

View File

@@ -22,10 +22,24 @@ For more information, see:
## Edit Predictions {#edit-predictions}
Zed has built-in support for predicting multiple edits at a time via its [Zeta model](https://huggingface.co/zed-industries/zeta). Clicking "Introducing: Edit Prediction" on the top right will open a brief prompt setting up this feature.
Zed has built-in support for predicting multiple edits at a time [via Zeta](https://huggingface.co/zed-industries/zeta), Zed's open-source and open-data model.
Edit predictions appear as you type, and most of the time, you can accept them by pressing `tab`.
### Configuring Zeta
Zed's Edit Prediction was initially introduced via a banner on the title bar.
Clicking on it would take you to a modal with a button ("Enable Edit Prediction") that sets `zed` as your `edit_prediction_provider`.
![Onboarding banner and modal](https://zed.dev/img/edit-prediction/docs.webp)
But, if you haven't come across the banner, start using Zed's Edit Prediction by adding this to your settings:
```json
"features": {
"edit_prediction_provider": "zed"
},
```
### Conflict With Other `tab` Actions {#edit-predictions-conflict}
By default, when `tab` would normally perform a different action, Zed requires a modifier key to accept predictions:

View File

@@ -63,6 +63,31 @@ You can install a local build on your machine with:
This will build zed and the cli in release mode and make them available at `~/.local/bin/zed`, installing .desktop files to `~/.local/share`.
> **_Note_**: If you encounter linker errors similar to the following:
>
> ```bash
> error: linking with `cc` failed: exit status: 1 ...
> = note: /usr/bin/ld: /tmp/rustcISMaod/libaws_lc_sys-79f08eb6d32e546e.rlib(f8e4fd781484bd36-bcm.o): in function `aws_lc_0_25_0_handle_cpu_env':
> /aws-lc/crypto/fipsmodule/cpucap/cpu_intel.c:(.text.aws_lc_0_25_0_handle_cpu_env+0x63): undefined reference to `__isoc23_sscanf'
> /usr/bin/ld: /tmp/rustcISMaod/libaws_lc_sys-79f08eb6d32e546e.rlib(f8e4fd781484bd36-bcm.o): in function `pkey_rsa_ctrl_str':
> /aws-lc/crypto/fipsmodule/evp/p_rsa.c:741:(.text.pkey_rsa_ctrl_str+0x20d): undefined reference to `__isoc23_strtol'
> /usr/bin/ld: /aws-lc/crypto/fipsmodule/evp/p_rsa.c:752:(.text.pkey_rsa_ctrl_str+0x258): undefined reference to `__isoc23_strtol'
> collect2: error: ld returned 1 exit status
> = note: some `extern` functions couldn't be found; some native libraries may need to be installed or have their path specified
> = note: use the `-l` flag to specify native libraries to link
> = note: use the `cargo:rustc-link-lib` directive to specify the native libraries to link with Cargo (see https://doc.rust-lang.org/cargo/reference/build-scripts.html#rustc-link-lib)
> error: could not compile `remote_server` (bin "remote_server") due to 1 previous error
> ```
>
> **Cause**:
> this is caused by known bugs in aws-lc-rs(doesn't support GCC >= 14): [FIPS fails to build with GCC >= 14](https://github.com/aws/aws-lc-rs/issues/569)
> & [GCC-14 - build failure for FIPS module](https://github.com/aws/aws-lc/issues/2010)
>
> You can refer to [linux: Linker error for remote_server when using script/install-linux](https://github.com/zed-industries/zed/issues/24880) for more information.
>
> **Workarounds**:
> Set the remote server target to `x86_64-unknown-linux-gnu` like so `export REMOTE_SERVER_TARGET=x86_64-unknown-linux-gnu; script/install-linux`
## Wayland & X11
Zed supports both X11 and Wayland. By default, we pick whichever we can find at runtime. If you're on Wayland and want to run in X11 mode, use the environment variable `WAYLAND_DISPLAY=''`.

View File

@@ -1,6 +1,6 @@
# PureScript
PureScript support is available through the [PureScript extension](https://github.com/zed-industries/zed/tree/main/extensions/purescript).
PureScript support is available through the [PureScript extension](https://github.com/zed-extensions/purescript).
- Tree-sitter: [postsolar/tree-sitter-purescript](https://github.com/postsolar/tree-sitter-purescript)
- Language-Server: [nwolverson/purescript-language-server](https://github.com/nwolverson/purescript-language-server)

View File

@@ -2,7 +2,7 @@
[Uiua](https://www.uiua.org/) is a general purpose, stack-based, array-oriented programming language with a focus on simplicity, beauty, and tacit code.
Uiua support is available through the [Uiua extension](https://github.com/zed-industries/zed/tree/main/extensions/uiua).
Uiua support is available through the [Uiua extension](https://github.com/zed-extensions/uiua).
- Tree-sitter: [shnarazk/tree-sitter-uiua](https://github.com/shnarazk/tree-sitter-uiua)
- Language Server: [uiua-lang/uiua](https://github.com/uiua-lang/uiua/)

View File

@@ -1,6 +1,6 @@
# Zig
Zig support is available through the [Zig extension](https://github.com/zed-industries/zed/tree/main/extensions/zig).
Zig support is available through the [Zig extension](https://github.com/zed-extensions/zig).
- Tree-sitter: [tree-sitter-zig](https://github.com/tree-sitter-grammars/tree-sitter-zig)
- Language Server: [zls](https://github.com/zigtools/zls)

View File

@@ -1,16 +0,0 @@
[package]
name = "zed_purescript"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "Apache-2.0"
[lints]
workspace = true
[lib]
path = "src/purescript.rs"
crate-type = ["cdylib"]
[dependencies]
zed_extension_api = "0.1.0"

View File

@@ -1 +0,0 @@
../../LICENSE-APACHE

View File

@@ -1,15 +0,0 @@
id = "purescript"
name = "PureScript"
description = "PureScript support."
version = "0.1.0"
schema_version = 1
authors = ["Iván Molina Rebolledo <ivanmolinarebolledo@gmail.com>"]
repository = "https://github.com/zed-industries/zed"
[language_servers.purescript-language-server]
name = "PureScript Language Server"
language = "PureScript"
[grammars.purescript]
repository = "https://github.com/postsolar/tree-sitter-purescript"
commit = "0554811a512b9cec08b5a83ce9096eb22da18213"

View File

@@ -1,3 +0,0 @@
("(" @open ")" @close)
("[" @open "]" @close)
("{" @open "}" @close)

View File

@@ -1,14 +0,0 @@
name = "PureScript"
grammar = "purescript"
path_suffixes = ["purs"]
autoclose_before = ",=)}]"
line_comments = ["-- "]
block_comment = ["{- ", " -}"]
brackets = [
{ start = "{", end = "}", close = true, newline = true },
{ start = "[", end = "]", close = true, newline = true },
{ start = "(", end = ")", close = true, newline = true },
{ start = "\"", end = "\"", close = true, newline = false },
{ start = "'", end = "'", close = true, newline = false },
{ start = "`", end = "`", close = true, newline = false },
]

View File

@@ -1,144 +0,0 @@
;; Copyright 2022 nvim-treesitter
;;
;; Licensed under the Apache License, Version 2.0 (the "License");
;; you may not use this file except in compliance with the License.
;; You may obtain a copy of the License at
;;
;; http://www.apache.org/licenses/LICENSE-2.0
;;
;; Unless required by applicable law or agreed to in writing, software
;; distributed under the License is distributed on an "AS IS" BASIS,
;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
;; See the License for the specific language governing permissions and
;; limitations under the License.
;; ----------------------------------------------------------------------------
;; Literals and comments
(integer) @number
(exp_negation) @number
(exp_literal (number)) @float
(char) @string
[
(string)
(triple_quote_string)
] @string
(comment) @comment
;; ----------------------------------------------------------------------------
;; Punctuation
[
"("
")"
"{"
"}"
"["
"]"
] @punctuation.bracket
[
(comma)
";"
] @punctuation.delimiter
;; ----------------------------------------------------------------------------
;; Keywords, operators, includes
[
"forall"
"∀"
] @keyword
;; (pragma) @constant
[
"if"
"then"
"else"
"case"
"of"
] @keyword
[
"import"
"module"
] @keyword
[
(operator)
(constructor_operator)
(type_operator)
(qualified_module) ; grabs the `.` (dot), ex: import System.IO
(all_names)
(wildcard)
"="
"|"
"::"
"=>"
"->"
"<-"
"\\"
"`"
"@"
"∷"
"⇒"
"<="
"⇐"
"→"
"←"
] @operator
(module) @title
[
(where)
"let"
"in"
"class"
"instance"
"derive"
"foreign"
"data"
"newtype"
"type"
"as"
"hiding"
"do"
"ado"
"infix"
"infixl"
"infixr"
] @keyword
;; ----------------------------------------------------------------------------
;; Functions and variables
(variable) @variable
(pat_wildcard) @variable
(signature name: (variable) @type)
(function
name: (variable) @function
patterns: (patterns))
(exp_infix (exp_name) @function (#set! "priority" 101))
(exp_apply . (exp_name (variable) @function))
(exp_apply . (exp_name (qualified_variable (variable) @function)))
;; ----------------------------------------------------------------------------
;; Types
(type) @type
(type_variable) @type
(constructor) @constructor
; True or False
((constructor) @_bool (#match? @_bool "(True|False)")) @boolean

View File

@@ -1,3 +0,0 @@
(_ "[" "]" @end) @indent
(_ "{" "}" @end) @indent
(_ "(" ")" @end) @indent

View File

@@ -1,97 +0,0 @@
use std::{env, fs};
use zed_extension_api::{self as zed, serde_json, Result};
const SERVER_PATH: &str = "node_modules/.bin/purescript-language-server";
const PACKAGE_NAME: &str = "purescript-language-server";
struct PurescriptExtension {
did_find_server: bool,
}
impl PurescriptExtension {
fn server_exists(&self) -> bool {
fs::metadata(SERVER_PATH).map_or(false, |stat| stat.is_file())
}
fn server_script_path(&mut self, language_server_id: &zed::LanguageServerId) -> Result<String> {
let server_exists = self.server_exists();
if self.did_find_server && server_exists {
return Ok(SERVER_PATH.to_string());
}
zed::set_language_server_installation_status(
language_server_id,
&zed::LanguageServerInstallationStatus::CheckingForUpdate,
);
let version = zed::npm_package_latest_version(PACKAGE_NAME)?;
if !server_exists
|| zed::npm_package_installed_version(PACKAGE_NAME)?.as_ref() != Some(&version)
{
zed::set_language_server_installation_status(
language_server_id,
&zed::LanguageServerInstallationStatus::Downloading,
);
let result = zed::npm_install_package(PACKAGE_NAME, &version);
match result {
Ok(()) => {
if !self.server_exists() {
Err(format!(
"installed package '{PACKAGE_NAME}' did not contain expected path '{SERVER_PATH}'",
))?;
}
}
Err(error) => {
if !self.server_exists() {
Err(error)?;
}
}
}
}
self.did_find_server = true;
Ok(SERVER_PATH.to_string())
}
}
impl zed::Extension for PurescriptExtension {
fn new() -> Self {
Self {
did_find_server: false,
}
}
fn language_server_command(
&mut self,
language_server_id: &zed::LanguageServerId,
_worktree: &zed::Worktree,
) -> Result<zed::Command> {
let server_path = self.server_script_path(language_server_id)?;
Ok(zed::Command {
command: zed::node_binary_path()?,
args: vec![
env::current_dir()
.unwrap()
.join(&server_path)
.to_string_lossy()
.to_string(),
"--stdio".to_string(),
],
env: Default::default(),
})
}
fn language_server_initialization_options(
&mut self,
_language_server_id: &zed::LanguageServerId,
_worktree: &zed::Worktree,
) -> Result<Option<serde_json::Value>> {
Ok(Some(serde_json::json!({
"purescript": {
"addSpagoSources": true
}
})))
}
}
zed::register_extension!(PurescriptExtension);

View File

@@ -1,16 +0,0 @@
[package]
name = "zed_uiua"
version = "0.0.1"
edition.workspace = true
publish.workspace = true
license = "Apache-2.0"
[lints]
workspace = true
[lib]
path = "src/uiua.rs"
crate-type = ["cdylib"]
[dependencies]
zed_extension_api = "0.1.0"

View File

@@ -1 +0,0 @@
../../LICENSE-APACHE

View File

@@ -1,15 +0,0 @@
id = "uiua"
name = "Uiua"
description = "Uiua support."
version = "0.0.1"
schema_version = 1
authors = ["Max Brunsfeld <max@zed.dev>"]
repository = "https://github.com/zed-industries/zed"
[language_servers.uiua]
name = "Uiua LSP"
language = "Uiua"
[grammars.uiua]
repository = "https://github.com/shnarazk/tree-sitter-uiua"
commit = "21dc2db39494585bf29a3f86d5add6e9d11a22ba"

View File

@@ -1,11 +0,0 @@
name = "Uiua"
grammar = "uiua"
path_suffixes = ["ua"]
line_comments = ["# "]
autoclose_before = ")]}\""
brackets = [
{ start = "{", end = "}", close = true, newline = false},
{ start = "[", end = "]", close = true, newline = false },
{ start = "(", end = ")", close = true, newline = false },
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
]

View File

@@ -1,50 +0,0 @@
[
(openParen)
(closeParen)
(openCurly)
(closeCurly)
(openBracket)
(closeBracket)
] @punctuation.bracket
[
(branchSeparator)
(underscore)
] @constructor
; ] @punctuation.delimiter
[ (character) ] @constant.character
[ (comment) ] @comment
[ (constant) ] @constant.numeric
[ (identifier) ] @variable
[ (leftArrow) ] @keyword
[ (function) ] @function
[ (modifier1) ] @operator
[ (modifier2) ] @operator
[ (number) ] @constant.numeric
[ (placeHolder) ] @special
[ (otherConstant) ] @string.special
[ (signature) ] @type
[ (system) ] @function.builtin
[ (tripleMinus) ] @module
; planet
[
"id"
"identity"
"∘"
"dip"
"⊙"
"gap"
"⋅"
] @tag
[
(string)
(multiLineString)
] @string
; [
; (deprecated)
; (identifierDeprecated)
; ] @warning

View File

@@ -1,3 +0,0 @@
[
(array)
] @indent

View File

@@ -1,27 +0,0 @@
use zed_extension_api::{self as zed, Result};
struct UiuaExtension;
impl zed::Extension for UiuaExtension {
fn new() -> Self {
Self
}
fn language_server_command(
&mut self,
_language_server_id: &zed::LanguageServerId,
worktree: &zed::Worktree,
) -> Result<zed::Command> {
let path = worktree
.which("uiua")
.ok_or_else(|| "uiua is not installed".to_string())?;
Ok(zed::Command {
command: path,
args: vec!["lsp".to_string()],
env: Default::default(),
})
}
}
zed::register_extension!(UiuaExtension);

View File

@@ -1,16 +0,0 @@
[package]
name = "zed_zig"
version = "0.3.3"
edition.workspace = true
publish.workspace = true
license = "Apache-2.0"
[lints]
workspace = true
[lib]
path = "src/zig.rs"
crate-type = ["cdylib"]
[dependencies]
zed_extension_api = "0.1.0"

View File

@@ -1 +0,0 @@
../../LICENSE-APACHE

View File

@@ -1,15 +0,0 @@
id = "zig"
name = "Zig"
description = "Zig support."
version = "0.3.3"
schema_version = 1
authors = ["Allan Calix <contact@acx.dev>"]
repository = "https://github.com/zed-industries/zed"
[language_servers.zls]
name = "zls"
language = "Zig"
[grammars.zig]
repository = "https://github.com/tree-sitter-grammars/tree-sitter-zig"
commit = "eb7d58c2dc4fbeea4745019dee8df013034ae66b"

View File

@@ -1,3 +0,0 @@
("(" @open ")" @close)
("[" @open "]" @close)
("{" @open "}" @close)

View File

@@ -1,12 +0,0 @@
name = "Zig"
grammar = "zig"
path_suffixes = ["zig", "zon"]
line_comments = ["// ", "/// ", "//! "]
autoclose_before = ";:.,=}])"
brackets = [
{ start = "{", end = "}", close = true, newline = true },
{ start = "[", end = "]", close = true, newline = true },
{ start = "(", end = ")", close = true, newline = true },
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
{ start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
]

View File

@@ -1,295 +0,0 @@
; Variables
(identifier) @variable
; Parameters
(parameter
name: (identifier) @variable.parameter)
; Types
(parameter
type: (identifier) @type)
((identifier) @type
(#match? @type "^[A-Z_][a-zA-Z0-9_]*"))
(variable_declaration
(identifier) @type
"="
[
(struct_declaration)
(enum_declaration)
(union_declaration)
(opaque_declaration)
])
[
(builtin_type)
"anyframe"
] @type.builtin
; Constants
((identifier) @constant
(#match? @constant "^[A-Z][A-Z_0-9]+$"))
[
"null"
"unreachable"
"undefined"
] @constant.builtin
(field_expression
.
member: (identifier) @constant)
(enum_declaration
(container_field
type: (identifier) @constant))
; Labels
(block_label (identifier) @label)
(break_label (identifier) @label)
; Fields
(field_initializer
.
(identifier) @variable.member)
(field_expression
(_)
member: (identifier) @property)
(field_expression
(_)
member: (identifier) @type (#match? @type "^[A-Z_][a-zA-Z0-9_]*"))
(container_field
name: (identifier) @property)
(initializer_list
(assignment_expression
left: (field_expression
.
member: (identifier) @property)))
; Functions
(builtin_identifier) @function.builtin
(call_expression
function: (identifier) @function.call)
(call_expression
function: (field_expression
member: (identifier) @function.call))
(function_declaration
name: (identifier) @function)
; Modules
(variable_declaration
(identifier) @module
(builtin_function
(builtin_identifier) @keyword.import
(#any-of? @keyword.import "@import" "@cImport")))
; Builtins
[
"c"
"..."
] @variable.builtin
((identifier) @variable.builtin
(#eq? @variable.builtin "_"))
(calling_convention
(identifier) @variable.builtin)
; Keywords
[
"asm"
"defer"
"errdefer"
"test"
"error"
"const"
"var"
] @keyword
[
"struct"
"union"
"enum"
"opaque"
] @keyword.type
[
"async"
"await"
"suspend"
"nosuspend"
"resume"
] @keyword.coroutine
"fn" @keyword.function
[
"and"
"or"
"orelse"
] @keyword.operator
"return" @keyword.return
[
"if"
"else"
"switch"
] @keyword.conditional
[
"for"
"while"
"break"
"continue"
] @keyword.repeat
[
"usingnamespace"
"export"
] @keyword.import
[
"try"
"catch"
] @keyword.exception
[
"volatile"
"allowzero"
"noalias"
"addrspace"
"align"
"callconv"
"linksection"
"pub"
"inline"
"noinline"
"extern"
"comptime"
"packed"
"threadlocal"
] @keyword.modifier
; Operator
[
"="
"*="
"*%="
"*|="
"/="
"%="
"+="
"+%="
"+|="
"-="
"-%="
"-|="
"<<="
"<<|="
">>="
"&="
"^="
"|="
"!"
"~"
"-"
"-%"
"&"
"=="
"!="
">"
">="
"<="
"<"
"&"
"^"
"|"
"<<"
">>"
"<<|"
"+"
"++"
"+%"
"-%"
"+|"
"-|"
"*"
"/"
"%"
"**"
"*%"
"*|"
"||"
".*"
".?"
"?"
".."
] @operator
; Literals
(character) @string
([
(string)
(multiline_string)
] @string
(#set! "priority" 95))
(integer) @number
(float) @number.float
(boolean) @boolean
(escape_sequence) @string.escape
; Punctuation
[
"["
"]"
"("
")"
"{"
"}"
] @punctuation.bracket
[
";"
"."
","
":"
"=>"
"->"
] @punctuation.delimiter
(payload "|" @punctuation.bracket)
; Comments
(comment) @comment
((comment) @comment.documentation
(#match? @comment.documentation "^//(/|!)"))

View File

@@ -1,17 +0,0 @@
[
(block)
(switch_expression)
(initializer_list)
] @indent.begin
(block
"}" @indent.end)
(_ "[" "]" @end) @indent
(_ "{" "}" @end) @indent
(_ "(" ")" @end) @indent
[
(comment)
(multiline_string)
] @indent.ignore

View File

@@ -1,10 +0,0 @@
((comment) @injection.content
(#set! injection.language "comment"))
; TODO: add when asm is added
; (asm_output_item (string) @injection.content
; (#set! injection.language "asm"))
; (asm_input_item (string) @injection.content
; (#set! injection.language "asm"))
; (asm_clobbers (string) @injection.content
; (#set! injection.language "asm"))

View File

@@ -1,50 +0,0 @@
(test_declaration
"test" @context
[
(string)
(identifier)
] @name) @item
(function_declaration
"pub"? @context
[
"extern"
"export"
"inline"
"noinline"
]? @context
"fn" @context
name: (_) @name) @item
(source_file
(variable_declaration
"pub"? @context
(identifier) @name
"=" (_) @context) @item)
(struct_declaration
(variable_declaration
"pub"? @context
(identifier) @name
"=" (_) @context) @item)
(union_declaration
(variable_declaration
"pub"? @context
(identifier) @name
"=" (_) @context) @item)
(enum_declaration
(variable_declaration
"pub"? @context
(identifier) @name
"=" (_) @context) @item)
(opaque_declaration
(variable_declaration
"pub"? @context
(identifier) @name
"=" (_) @context) @item)
(container_field
. (_) @name) @item

View File

@@ -1,27 +0,0 @@
(function_declaration
body: (_
"{"
(_)* @function.inside
"}")) @function.around
(test_declaration
(block
"{"
(_)* @function.inside
"}")) @function.around
(variable_declaration
(struct_declaration
"struct"
"{"
[(_) ","]* @class.inside
"}")) @class.around
(variable_declaration
(enum_declaration
"enum"
"{"
(_)* @class.inside
"}")) @class.around
(comment)+ @comment.around

View File

@@ -1,171 +0,0 @@
use std::fs;
use zed_extension_api::{self as zed, serde_json, settings::LspSettings, LanguageServerId, Result};
struct ZigExtension {
cached_binary_path: Option<String>,
}
#[derive(Clone)]
struct ZlsBinary {
path: String,
args: Option<Vec<String>>,
environment: Option<Vec<(String, String)>>,
}
impl ZigExtension {
fn language_server_binary(
&mut self,
language_server_id: &LanguageServerId,
worktree: &zed::Worktree,
) -> Result<ZlsBinary> {
let mut args: Option<Vec<String>> = None;
let (platform, arch) = zed::current_platform();
let environment = match platform {
zed::Os::Mac | zed::Os::Linux => Some(worktree.shell_env()),
zed::Os::Windows => None,
};
if let Ok(lsp_settings) = LspSettings::for_worktree("zls", worktree) {
if let Some(binary) = lsp_settings.binary {
args = binary.arguments;
if let Some(path) = binary.path {
return Ok(ZlsBinary {
path: path.clone(),
args,
environment,
});
}
}
}
if let Some(path) = worktree.which("zls") {
return Ok(ZlsBinary {
path,
args,
environment,
});
}
if let Some(path) = &self.cached_binary_path {
if fs::metadata(path).map_or(false, |stat| stat.is_file()) {
return Ok(ZlsBinary {
path: path.clone(),
args,
environment,
});
}
}
zed::set_language_server_installation_status(
language_server_id,
&zed::LanguageServerInstallationStatus::CheckingForUpdate,
);
// Note that in github releases and on zlstools.org the tar.gz asset is not shown
// but is available at https://builds.zigtools.org/zls-{os}-{arch}-{version}.tar.gz
let release = zed::latest_github_release(
"zigtools/zls",
zed::GithubReleaseOptions {
require_assets: true,
pre_release: false,
},
)?;
let arch: &str = match arch {
zed::Architecture::Aarch64 => "aarch64",
zed::Architecture::X86 => "x86",
zed::Architecture::X8664 => "x86_64",
};
let os: &str = match platform {
zed::Os::Mac => "macos",
zed::Os::Linux => "linux",
zed::Os::Windows => "windows",
};
let extension: &str = match platform {
zed::Os::Mac | zed::Os::Linux => "tar.gz",
zed::Os::Windows => "zip",
};
let asset_name: String = format!("zls-{}-{}-{}.{}", os, arch, release.version, extension);
let download_url = format!("https://builds.zigtools.org/{}", asset_name);
let version_dir = format!("zls-{}", release.version);
let binary_path = match platform {
zed::Os::Mac | zed::Os::Linux => format!("{version_dir}/zls"),
zed::Os::Windows => format!("{version_dir}/zls.exe"),
};
if !fs::metadata(&binary_path).map_or(false, |stat| stat.is_file()) {
zed::set_language_server_installation_status(
language_server_id,
&zed::LanguageServerInstallationStatus::Downloading,
);
zed::download_file(
&download_url,
&version_dir,
match platform {
zed::Os::Mac | zed::Os::Linux => zed::DownloadedFileType::GzipTar,
zed::Os::Windows => zed::DownloadedFileType::Zip,
},
)
.map_err(|e| format!("failed to download file: {e}"))?;
zed::make_file_executable(&binary_path)?;
let entries =
fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?;
for entry in entries {
let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?;
if entry.file_name().to_str() != Some(&version_dir) {
fs::remove_dir_all(entry.path()).ok();
}
}
}
self.cached_binary_path = Some(binary_path.clone());
Ok(ZlsBinary {
path: binary_path,
args,
environment,
})
}
}
impl zed::Extension for ZigExtension {
fn new() -> Self {
Self {
cached_binary_path: None,
}
}
fn language_server_command(
&mut self,
language_server_id: &LanguageServerId,
worktree: &zed::Worktree,
) -> Result<zed::Command> {
let zls_binary = self.language_server_binary(language_server_id, worktree)?;
Ok(zed::Command {
command: zls_binary.path,
args: zls_binary.args.unwrap_or_default(),
env: zls_binary.environment.unwrap_or_default(),
})
}
fn language_server_workspace_configuration(
&mut self,
_language_server_id: &zed::LanguageServerId,
worktree: &zed::Worktree,
) -> Result<Option<serde_json::Value>> {
let settings = LspSettings::for_worktree("zls", worktree)
.ok()
.and_then(|lsp_settings| lsp_settings.settings.clone())
.unwrap_or_default();
Ok(Some(settings))
}
}
zed::register_extension!(ZigExtension);