Compare commits

...

228 Commits

Author SHA1 Message Date
Mikayla Maki
99105ea26e Attempt a feature based way of switching to dynamic libraries 2024-06-18 22:00:58 -07:00
Mikayla Maki
72f54f1c44 Switch to feature based library linking 2024-06-18 21:21:57 -07:00
Mikayla
66c667b6c6 fix typo in arm builders 2024-06-14 17:26:48 -07:00
Mikayla
85613af919 Disable usage of sqlite3 features that are too new for Ubuntu 2024-06-14 17:00:10 -07:00
Mikayla
e1c0d10f76 Add sqlite3 to arm runners 2024-06-14 15:48:36 -07:00
Mikayla Maki
81c12961db Fix windows bundles 2024-06-14 15:13:23 -07:00
Mikayla Maki
3ae9d61791 Ensure we only dynamically link fontconfig on linux 2024-06-14 14:48:22 -07:00
Mikayla Maki
6d0042b502 Remove async-native-tls 2024-06-14 14:39:28 -07:00
Mikayla Maki
27f0665246 Make macOS use statically linked libraries 2024-06-14 14:37:28 -07:00
Conrad Irwin
6aa78ff095 Dynamicer builds
Co-Authored-By: Mikayla <mikayla@zed.dev>
2024-06-14 15:14:20 -06:00
Joseph T. Lyons
d5a6ca4914 Add os_name and os_version to all event types (#13063)
Release Notes:

- N/A
2024-06-14 16:38:08 -04:00
Marshall Bowers
ea69846281 Silence error logs in zed tests (#13069)
This PR silences the remaining error logs in the `zed` crate tests by
initializing `env_logger` in test mode.

This means that the logs will no longer be shown unless `--nocapture` is
passed to `cargo test`.

Release Notes:

- N/A
2024-06-14 16:30:15 -04:00
Kirill Bulatov
ff8486e67f Properly align excerpt and outline items (#13070) 2024-06-14 23:26:07 +03:00
Marshall Bowers
9bc3c6810b Register Markdown language in some tests to silence error logs (#13066)
This PR registers the Markdown language in some of the tests in the
`zed` crate to silence the error logs about the language not being found
when the chat panel attempts to load it.

Release Notes:

- N/A
2024-06-14 15:29:20 -04:00
Conrad Irwin
45ae0dcc2d Fix dw at the end of a soft wrapped line (#13065)
Co-Authored-By: Richard <richard@zed.dev>
Release Notes:

- vim: Fixed behavior of `dw` at the end of a soft wrapped line

Co-authored-by: Richard <richard@zed.dev>
2024-06-14 13:18:28 -06:00
Joseph T. Lyons
e40c49a143 Fix incorrect data being assigned to os_name (#13064)
Release Notes:

- N/A
2024-06-14 14:40:22 -04:00
versecafe
0d43d484f6 Use square buttons for code action and run indicators in the gutter (#12906)
### Before

<img width="94" alt="Screenshot 2024-06-14 at 1 34 54 PM"
src="https://github.com/zed-industries/zed/assets/1486634/fe756434-f072-4506-8fd2-c220c17cf112">

<img width="115" alt="Screenshot 2024-06-14 at 1 35 04 PM"
src="https://github.com/zed-industries/zed/assets/1486634/e378f02b-cb55-467d-9a5e-04e162d6daab">

### After

<img width="128" alt="Screenshot 2024-06-14 at 1 34 27 PM"
src="https://github.com/zed-industries/zed/assets/1486634/3d857a85-7673-43b1-8c48-56766455dd81">

<img width="134" alt="Screenshot 2024-06-14 at 1 34 33 PM"
src="https://github.com/zed-industries/zed/assets/1486634/b04c1fef-0a30-4eb1-b8f7-4eff351fcdc7">


Release Notes:

- Improved the look of code action and run indicators in the gutter
([#12803](https://github.com/zed-industries/zed/issues/12803)).

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-06-14 14:21:16 -04:00
Marshall Bowers
6ca09bd4ba project: Disable project_tests::test_rescan_and_remote_updates on Linux (#13062)
This PR disables the
`project::project_tests::test_rescan_and_remote_updates` test on Linux,
as we've been seeing it fail quite consistently in CI.

We can re-enable it once we've had a chance to investigate and fix.

Release Notes:

- N/A
2024-06-14 14:20:52 -04:00
Kyle Kelley
53f702c92f Allow Ollama Model KeepAlive to be None, defaulting to indefinite (#13059)
Putting this back to `Option<KeepAlive>` to make existing configs keep
working.

Release Notes:

- N/A
2024-06-14 10:33:28 -07:00
Marshall Bowers
b03653321f Update Cargo.lock (#13061)
This PR updates `Cargo.lock`, since it was missed in #12818.

Release Notes:

- N/A
2024-06-14 13:26:25 -04:00
Conrad Irwin
993109aee1 Fix panic in worktree scanning (#13057)
Release Notes:

- Fixed a panic when worktree paths are incorrectly relative.
2024-06-14 10:23:20 -07:00
张小白
4cb45e63f4 windows: Update windows-rs crate and better error handling in DirectWrite (#12818)
- Update `windows-rs` from `0.56` to `0.57`
- Use the newly introduced `Owned` struct in `0.57` to handle the RAII
stuff of `HANDLE`
- Better error handling in `DirectWrite`

Release Notes:

- N/A
2024-06-14 10:12:20 -07:00
Kyle Kelley
1413b5af93 Select the first available model when none configured for Ollama (#13048)
Selects the first available model for Ollama if a model is not
configured.

Release Notes:

- N/A
2024-06-14 09:35:13 -07:00
Kyle Kelley
d9c21b4eb1 Accept numeric keep alive in Ollama settings (#13046)
This adds the ability to set the keep alive as an integer, including
`-1` for staying alive indefinitely until a new model is loaded or
Ollama exits. I've also set the default to `-1` so that models stay
ready to go for Zed to use.

Release Notes:

- N/A
2024-06-14 09:35:04 -07:00
Marshall Bowers
44f66aa426 rustdoc: Add CrateName newtype (#13056)
This PR adds a `CrateName` newtype used to represent crate names.

This makes the code a bit more self-descriptive and prevents confusing
other string values for a crate name.

It also changes the internal representation from a `String` to an
`Arc<str>` for cheaper clones.

Release Notes:

- N/A
2024-06-14 12:21:03 -04:00
Conrad Irwin
3b84b106e2 vim gigv (#13028)
Release Notes:

- vim: Fix `gi` when the insert ended at the end of a line (#12162)
- vim: Add `gv` to restore previous visual selection (#12888)
- vim: Fix `gl` when the first match is at the end of a line
2024-06-14 10:16:59 -06:00
Marshall Bowers
3539a7c04a Fix a cargo doc warning in the zed crate (#13054)
This PR fixes a warning I observed when running `cargo doc` against the
`zed` crate:

```
 Documenting zed v0.141.0 (/Users/maxdeviant/projects/zed/crates/zed)
warning: this URL is not a hyperlink
   --> crates/zed/src/main.rs:860:69
    |
860 |     /// URLs can either be file:// or zed:// scheme, or relative to https://zed.dev.
    |                                                                     ^^^^^^^^^^^^^^^^ help: use an automatic link instead: `<https://zed.dev.>`
    |
    = note: bare URLs are not automatically turned into clickable links
    = note: `#[warn(rustdoc::bare_urls)]` on by default
```

Release Notes:

- N/A
2024-06-14 11:41:11 -04:00
Kirill Bulatov
a8481099ca Prefer the same order of entries inside outline and project panels, project search multi buffer (#13044)
Release Notes:

- N/A
2024-06-14 18:33:36 +03:00
Marshall Bowers
6c28b7e8b8 danger: Check PR titles (#13053)
This PR sets up Danger to check PR titles for consistency, using
[`danger-plugin-pr-hygiene`](https://github.com/maxdeviant/danger-plugin-pr-hygiene).

<img width="919" alt="Screenshot 2024-06-14 at 11 16 31 AM"
src="https://github.com/zed-industries/zed/assets/1486634/167fe698-2505-422b-8e41-e121d9fe933f">

Release Notes:

- N/A
2024-06-14 11:18:41 -04:00
Bennet Bo Fenner
0d8e6e6b12 assistant: Add diagnostics slash command (#12998)
This adds a `diagnostics` command to the assistant which allows to
inject compile errors/warnings into the context.

Release Notes:

- N/A
2024-06-14 17:14:50 +02:00
Marshall Bowers
bf03f66d02 danger: Upgrade to pnpm v9 (#13051)
This PR upgrades Danger to use pnpm v9.

Release Notes:

- N/A
2024-06-14 11:08:15 -04:00
Peter Tripp
0f59607100 docs: wrap_guides (#12992)
- Add 'wrap_guides' to website config docs.
- Add the word 'ruler' to improve searchability.
2024-06-14 09:48:25 -04:00
Piotr Osiewicz
902d7150fe collab_ui: Re-enable deafening and screen share on Mac (#13040)
Fixes regression from https://github.com/zed-industries/zed/pull/12994
Release Notes:

- N/A
2024-06-14 15:47:53 +02:00
Piotr Osiewicz
55ba80ddd1 lsp: Add support for label_details in completions (#13043)
This fixes an issue reported by @Spoutnik97 in
https://github.com/zed-industries/zed/issues/12711#issuecomment-2163785111
- vtsls returns auxiliary docs via .label_details and not plain .details
field.

Release Notes:

- Improved quality of auxiliary details in completions returned by VTSLS
2024-06-14 13:49:08 +02:00
Piotr Osiewicz
dcb8dc16ca editor: Update insert_text_format based on resolved completion (#13041)
Fixes #12920

VTSLS does not mark snippet completions as such in the initial
completion response - not until we resolve them; however, we do not
touch initial contents of completion during resolution, which led to us
not treating a snippet as such.

Release Notes:

- Fixed snippet completions sometimes being treated as plain text
completions when using VTSLS
2024-06-14 13:31:02 +02:00
Kirill Bulatov
eb7a09b459 Add excerpts into outline panel (#13034)
Follow-up of https://github.com/zed-industries/zed/pull/12637

Adds excerpt items into the outline panel: now all outline items are
initially hidden under excerpt items that could be toggled open/closed
similar to directories.


![Screenshot 2024-06-14 at 10 45
04](https://github.com/zed-industries/zed/assets/2690773/9c9ef91b-1666-43c3-acc4-96f850098a28)

On active editor's selection change, a corresponding outline will be
revealed still, expanding the corresponding excerpt

![Screenshot 2024-06-14 at 10 45
13](https://github.com/zed-industries/zed/assets/2690773/7dfd14f7-4aca-48f2-8760-8e1362b9a043)

Release Notes:

- N/A
2024-06-14 12:03:16 +03:00
Kirill Bulatov
1248788780 Add a new show whitespaces config option 2024-06-14 09:25:25 +03:00
Angelo.Mateus
64bb79b71d Allow held key events if key is modifier (#13000)
Release Notes:

- (Fixed) Allows held down key events for modifier keys.
([#12566](https://github.com/zed-industries/zed/issues/12566))

---------

Co-authored-by: Angelo <>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-06-13 20:54:59 -06:00
Conrad Irwin
a5af5b2883 Multicursor vim registers (#13025)
Release Notes:

- vim: Added support for multicursor registers (#11687)
- vim: Added support for the `"/` register
2024-06-13 20:32:58 -06:00
Winston Hoy
068b1c235c Add elfutils dep for Debian (#13023)
Release Notes:

- Fixed https://github.com/zed-industries/zed/issues/13021
2024-06-13 20:05:58 -06:00
Yan Qian
8edfd0a963 ui: Fix doctest (#12985)
Fix the documentation tests failed when running `cargo test --workspace`

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-06-13 20:32:41 -04:00
Owen Law
0ed5327b1c Fix Flatpak desktop entry (#13019)
Fixes a problem where the env variable was being set in the wrong spot
in #12951

Release Notes:

- N/A
2024-06-13 20:23:49 -04:00
Mikayla Maki
10d3ad4e33 Enable linux tests (#12493)
Note:
- We have disabled all tests that rely on Postgres in the Linux CI. We
only really need to test these once, and as macOS is our team's primary
platform, we'll only enable them on macOS for local reproduction.
- We have disabled all tests that rely on the font metrics. We
standardized on Zed Mono in many fonts, but our CoreText Text System and
Cosmic Text System proved to be very different in effect. We should
revisit if we decide to standardize our text system across platforms
(e.g. using Harfbuzz everywhere)
- Extended the condition timeout significantly. Our CI machines are slow
enough that this is causing spurious errors in random tests.

Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-06-13 16:38:53 -07:00
Max Brunsfeld
066cdc2297 Fix panic when doing various cursor movements with a pending mouse selection (#13016)
This fixes a panic in the `SelectionsCollection::first_anchor` when
there was a pending mouse selection and no other selections. Until
recently, this method was only used in vim mode, but as of
53b0720d54,
it's also used in the normal `move_up` and `move_down` actions.

So until recently, the panic that this fixes could only happen in vim
mode.

Release Notes:

- Fixed a crash that could happen when using certain cursor-motion
bindings with a pending mouse selection.
2024-06-13 16:22:45 -07:00
Marshall Bowers
01ba1ddef7 gpui_macros: Disable doctests (#13015)
This PR disables the doctests in the `gpui_macros` crate, as they depend
on `gpui` to run.

Since `gpui` depends on `gpui_macros`, we don't really want to add a
dependency on `gpui` (even though it _appears_ to work as a dev
dependency).

Also did some minor stylistic cleanup of some doc comments.

Release Notes:

- N/A
2024-06-13 18:45:28 -04:00
Marshall Bowers
86167138a9 rustdoc: Automatically index crates (#13014)
This PR removes the need to use `/rustdoc --index <CRATE_NAME>` and
instead indexes the crates once they are referenced.

As soon as the first `:` is added after the crate name, the indexing
will kick off in the background and update the index as it goes.

Release Notes:

- N/A
2024-06-13 18:30:15 -04:00
Marshall Bowers
e0c1ab650e rustdoc: Fix duplicated item path (#13013)
This PR fixes a bug that was introduced in #13011 where the item path
would get duplicated twice in the database key.

Release Notes:

- N/A
2024-06-13 18:21:52 -04:00
Piotr Osiewicz
407188f816 chore: Bump Rust version to 1.79 (#12987)
This is blocked on a release of new Docker image for 1.79
(https://github.com/docker-library/official-images/pull/16981)

Release Notes:

- N/A
2024-06-13 23:05:25 +02:00
Marshall Bowers
6181ac6bad rustdoc: Index crates progressively (#13011)
This PR updates the rustdoc indexing to be more progressive.

Rather than waiting until we've crawled the entire crate to begin
writing to the database, we instead start writing the docs as we go.

This makes it so you can start getting completions while the indexing is
still running.

Release Notes:

- N/A
2024-06-13 16:40:06 -04:00
Marshall Bowers
0705fb9b97 ui: Remove unused CollapsibleContainer component (#13009)
This PR removes the `CollapsibleContainer` component, as it wasn't used
anywhere.

Release Notes:

- N/A
2024-06-13 16:30:11 -04:00
Kyle Kelley
042be3529d Add affordance for retry and button to visit Ollama library (#13003) 2024-06-13 13:25:24 -07:00
Marshall Bowers
1a40e98413 Render editor fold indicators using Disclosures (#13008)
This PR updates the spots where we render the fold indicators in editors
to use the `Disclosure` component instead of re-implementing similar UI.

This makes this UI more consistent across Zed.

Release Notes:

- N/A
2024-06-13 16:05:47 -04:00
Max Brunsfeld
af8e7af265 Keep symbol names in bundled linux binaries (#13006)
This ensures that linux panics still contain symbol names. It also
allows us to profile Zed on linux with `perf` and get symbol names.

Release Notes:

- N/A
2024-06-13 12:40:52 -07:00
Philip Schatz
ce51c264a6 Add libstdc++-12-dev for linux (#12962)
Release Notes:

- N/A

PS: 👋 Congrats on the release 🎊 and hey from
discussing CRDTs at the [GitPod/DevX conference last
year](https://www.youtube.com/watch?v=wXT73bBr83s)! Just read the [blog
post](https://zed.dev/blog/zed-decoded-linux-when) and thought I'd
finally try zed out (I have a linux laptop). This was the only snag I
ran into 👏
2024-06-13 12:39:58 -07:00
Marshall Bowers
702fd8f168 ui: Render disclosures with IconButtonShape::Square (#13004)
This PR adjusts the `Disclosure` component to render using
`IconButtonShape::Square`.

This tightens up the hover styles so they aren't quite so massive.

### Before

<img width="116" alt="Screenshot 2024-06-13 at 3 22 43 PM"
src="https://github.com/zed-industries/zed/assets/1486634/dea8f8fb-a041-4aa7-89be-0cd2d7889955">

### After

<img width="113" alt="Screenshot 2024-06-13 at 3 23 02 PM"
src="https://github.com/zed-industries/zed/assets/1486634/7682cce6-4f83-4f3e-b91f-3023849bd314">

Release Notes:

- Tweaked the style of disclosure controls throughout the UI.
2024-06-13 15:39:06 -04:00
Conrad Irwin
2e758dcb64 X11: Fix black flashes on boot and while resizing (#13002)
Release Notes:

- N/A
2024-06-13 13:35:38 -06:00
Bennet Bo Fenner
38d9ee3731 project panel: Support dropping files from finder (#12880)
Partially addresses #7386 



https://github.com/zed-industries/zed/assets/53836821/fc2e9864-40a8-4ada-ac95-a76a31c44437



Release Notes:

- Added support for dropping files from the finder onto the project
panel
2024-06-13 20:48:28 +02:00
Conrad Irwin
95c69d0696 Fix git watching on linux (#12989)
Release Notes:

- N/A

Co-authored-by: Nathan <nathan@zed.dev>
2024-06-13 11:59:57 -06:00
张小白
599102573a windows: Implement window_appearance() and should_auto_hide_scrollbars() (#12527)
Release Notes:

- N/A
2024-06-13 10:52:53 -07:00
Max Brunsfeld
da281d6d8f Fix issues where screen and window sizes contained Pixels, but were declared as DevicePixels (#12991)
On most platforms, things were working correctly, but had the wrong
type. On X11, there were some problems with window and display size
calculations.

Release Notes:

- Fixed issues with window positioning on X11

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Mikayla <mikayla@zed.dev>
2024-06-13 10:48:37 -07:00
张小白
22dc88ed3d windows: Fix scrolling bug (#12431)
Closes #12369 



https://github.com/zed-industries/zed/assets/14981363/14df820e-9be3-4b89-882b-a083ea42d59e



Release Notes:

- N/A
2024-06-13 10:48:20 -07:00
DocKDE
3c0310273b Add missing Linux build dependencies (#12972)
I found that builds failed on Arch and OpenSUSE so I added missing
dependencies. I also found that OpenSUSE Leap is currently not able to
install the required dependencies so I added a check to limit the
supported distros to Tumbleweed.

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-06-13 10:45:39 -07:00
Piotr Osiewicz
14bf07c916 worktree: Fix race condition when a root of worktree is .git directory (#12995)
It was possible to unload a root of worktree when it was a .git
directory; due to that, test_fs_events_in_dot_git_worktree was sometimes
stuck in an infinite loop on CI.

The gist of an issue is that when .git dir is a root dir, then modifying
a file within this directory could sometimes unload the .git dir; the
test went into an infinite loop when the first event in an filesystem
stream was not the event for the file creation, but for a dir
modification. In that case we'd unload the root directory and a
subsequent event for file creation would never be registered, leading to
the test being stuck waiting for it to happen.

This commit alleviates it by special-casing worktrees rooted in .git
directories.



Release Notes:

- Fixed a possible hang when opening a worktree in .git directory.
2024-06-13 19:24:41 +02:00
Mikayla Maki
284559742d Disable mic and screenshare buttons on non-macOS platforms (#12994)
Release Notes:

- N/A
2024-06-13 10:16:10 -07:00
Marshall Bowers
85acc2be44 Persist index for /rustdoc in LMDB (#12988)
This PR updates the `/rustdoc` command with persistence for the
documented rustdoc items.

Now when you run `/rustdoc --index <CRATE_NAME>` it will index the crate
and store the results in LMDB.

The documented items will then be read from the database when searching
using `/rustdoc` and persist across restarts of Zed.

Release Notes:

- N/A
2024-06-13 12:07:26 -04:00
Inam Ul Haq
6c70a809ec Update linux keybinds (#12822)
Release Notes:
- Add `ctrl-y` for Redo.
- Reorder bindings for copy/paste Fixes #11912
- Update `ctrl-b` to show hide left dock. This is the default behavior
in VSCode.


![image](https://github.com/zed-industries/zed/assets/22079654/e0cc6c0e-81df-443f-b77c-9a96370b6cb4)

### Or...

Release Notes:

- N/A
2024-06-13 08:38:27 -06:00
Joel Tony
a35947c883 Add dependencies for Ubuntu/Debian (#12959)
Release Notes:

- Fixed #12890
2024-06-13 08:36:18 -06:00
Conrad Irwin
f8ad5fe3e9 Arm builds (#12961)
Release Notes:

- N/A
2024-06-13 08:00:15 -06:00
Conrad Irwin
e2cfbc54ad Fix headless mode (#12960)
This was broken by two things:
1. A merge conflict in the install.sh script leading to bad sh syntax
2. A return removed by accident when we refactored main

Release Notes:

- N/A
2024-06-13 07:59:28 -06:00
Piotr Osiewicz
0a13b9ee01 lsp: Provide completion reason in the request (#12893)
This should help LS make a better call about the completions it should
return back to the caller. For example, it speeds up import completions
for typescript.
Before: 


https://github.com/zed-industries/zed/assets/24362066/b38fd565-f9ff-4db7-a87f-c3b31a9fdc96

after: 


https://github.com/zed-industries/zed/assets/24362066/d4fbc9ae-9aab-4543-b9f6-16acf1619576


This should be merged after 06.12 Preview to give it some time on
Nightly.

Release Notes:

- N/A
2024-06-13 14:38:34 +02:00
Piotr Osiewicz
eb7b5a7131 project panel: Improve performance in worktrees with lots of files. (#12980)
When working on a repro for a different issue that involved a worktree
with lots of files (100k to be precise), UI became pretty unresponsive.
I pinned it down to us repeatedly preparing a HashSet of all paths in
the currently-scrolled-to worktree, once per each entry in the range
passed to for_each_visible_range (which is e.g. called during
rendering).

This PR makes that hashing happen just once per worktree. Additionally,
we no longer iterate over (potentially) all entries in a given worktree
when calculating the depth of a given entry.

Note that we could probably be smarter about this still; instead of
recalculating the hashset per each call to for_each_visible_entry, we
could do it whenever we update entries in the project panel. However,
with this PR I wanted to get a quick bang for a small buck; I'm pretty
confident in the change as is, it is relatively straightforward and
messing with worktree updates is more involved.



Release Notes:

- Improvement performance of project panel in large worktrees
2024-06-13 14:20:38 +02:00
Piotr Osiewicz
7798f64d1b chore: Bump lsp-types to 0.97.0 (#12928)
This also includes https://github.com/gluon-lang/lsp-types/pull/287,
which should significantly reduce the time it takes for us to
deserialize completion lists.


Release Notes:
- N/A
2024-06-13 13:48:12 +02:00
yodatak
21764c38dd Fix typo error in flatpak script (#12978)
Fix https://github.com/zed-industries/zed/issues/12977

Release Notes:

- N/A
2024-06-13 07:40:34 -04:00
Subodh Upreti
cfbf5dca7a Add mold package for opensuse, arch, void and gentoo (#12964)
Release Notes:

- N/A
2024-06-13 10:47:37 +03:00
Kirill Bulatov
2f43d52e7e A set of outline panel fixes (#12965)
Follow-up of https://github.com/zed-industries/zed/pull/12637

* Wrong font size for the outline items (fixes
https://github.com/zed-industries/zed/pull/12637#issuecomment-2164084021)
* Duplicate context menu item (fixes
https://github.com/zed-industries/zed/issues/12957)
* Missing `space` keybinding for item opening (fixes
https://github.com/zed-industries/zed/issues/12956)
* Adds 60px more to the default width (fixes
https://github.com/zed-industries/zed/issues/12955)
* Incorrect scroll for singleton buffers (fixes
https://github.com/zed-industries/zed/issues/12953)

Release Notes:

- N/A
2024-06-13 10:46:51 +03:00
Antonio Scandurra
e1f4dfc068 Refine inline transformation UX (#12939)
https://github.com/zed-industries/zed/assets/482957/1790e32e-1f59-4831-8a4c-722cf441e7e9



Release Notes:

- N/A

---------

Co-authored-by: Richard <richard@zed.dev>
Co-authored-by: Nathan <nathan@zed.dev>
2024-06-13 08:35:22 +02:00
Conrad Irwin
9e3c5f3e12 Docs to help people find their way back (#12958)
Release Notes:

- N/A
2024-06-12 22:10:49 -06:00
Joey Riches
f780504b68 zed.desktop.in: Don't hardcode executable name due to binary conflicts (#12951)
> * There are a couple of other `zed` binaries that may be present on
linux systems
    ([1](https://openzfs.github.io/openzfs-docs/man/v2.2/8/zed.8.html),
[2](https://zed.brimdata.io/docs/commands/zed)). If you want to rename
our CLI
binary because of these issues, we suggest `zedit`, `zeditor`, or
`zed-cli`.

Due to aformentioned issue don't hardcode the executable name in the
.desktop file so envsubst can change it in accordance with the
distributor's requirement.

Resolves #12290.

Release Notes:

- N/A
2024-06-12 17:41:25 -07:00
Max Brunsfeld
76b0120665 Reveal the selected item when cycling a picker's selection (#12950)
Release Notes:

- Fixed a bug where the selected tab was not always shown when cycling
between tabs with `ctrl-tab`.
2024-06-12 17:40:53 -07:00
Marshall Bowers
0ac9af94e0 assistant: Add MVP for /rustdoc using indexed docs (#12952)
This PR adds an MVP of retrieving docs using the `/rustdoc` command from
an indexed set of docs.

To try this out:

1. Build local docs using `cargo doc`
2. Index the docs for the crate you want to search using `/rustdoc
--index <CRATE_NAME>`
    - Note: This may take a while, depending on the size of the crate
3. Search for docs using `/rustdoc my_crate::path::to::item`
    - You should get completions for the available items

Here are some screenshots of it in action:

<img width="640" alt="Screenshot 2024-06-12 at 6 19 20 PM"
src="https://github.com/zed-industries/zed/assets/1486634/6c49bec9-d084-4dcb-a92c-1b4c557ee9ce">

<img width="636" alt="Screenshot 2024-06-12 at 6 52 56 PM"
src="https://github.com/zed-industries/zed/assets/1486634/636a651c-7d02-48dc-b05c-931f33c49f9c">

Release Notes:

- N/A
2024-06-12 19:33:31 -04:00
Conrad Irwin
ec086945fc Use clang+mold on linux (#12944)
Takes a clean build on my machine from 222s to 185s

Release Notes:

- N/A
2024-06-12 15:16:05 -06:00
Kirill Bulatov
8451dba6a7 Introduce an outline panel (#12637)
Adds a new panel: `OutlinePanel` which looks very close to project
panel:

<img width="256" alt="Screenshot 2024-06-10 at 23 19 05"
src="https://github.com/zed-industries/zed/assets/2690773/c66e6e78-44ec-4de8-8d60-43238bb09ae9">

has similar settings and keymap (actions work in the `OutlinePanel`
context and are under `outline_panel::` namespace), with two notable
differences:
* no "edit" actions such as cut/copy/paste/delete/etc.
* directory auto folding is enabled by default

Empty view: 
<img width="841" alt="Screenshot 2024-06-10 at 23 19 11"
src="https://github.com/zed-industries/zed/assets/2690773/dc8bf37c-5a70-4fd5-9b57-76271eb7a40c">


When editor gets active, the panel displays all related files in a tree
(similar to what the project panel does) and all related excerpts'
outlines under each file.
Same as in the project panel, directories can be expanded or collapsed,
unfolded or folded; clicking file entries or outlines scrolls the buffer
to the corresponding excerpt; changing editor's selection reveals the
corresponding outline in the panel.

The panel is applicable to any singleton buffer:
<img width="1215" alt="Screenshot 2024-06-10 at 23 19 35"
src="https://github.com/zed-industries/zed/assets/2690773/a087631f-5c2d-4d4d-ae25-30ab9731d528">

<img width="1728" alt="image"
src="https://github.com/zed-industries/zed/assets/2690773/e4f8082c-d12d-4473-8500-e8fd1051285b">

or any multi buffer:

(search multi buffer)

<img width="1728" alt="Screenshot 2024-06-10 at 23 19 41"
src="https://github.com/zed-industries/zed/assets/2690773/60f768a3-6716-4520-9b13-42da8fd15f50">

(diagnostics multi buffer)
<img width="1728" alt="image"
src="https://github.com/zed-industries/zed/assets/2690773/64e285bd-9530-4bf2-8f1f-10ee5596067c">

Release Notes:
- Added an outline panel to show a "map" of the active editor
2024-06-12 23:22:52 +03:00
DocKDE
7f56f4e78e Add libgit2 build dependency to Arch-based distros (#12924)
Release Notes:

- N/A
2024-06-12 14:16:27 -06:00
Marshall Bowers
6fa347dff7 Move rustdoc-related code to rustdoc crate (#12945)
This PR moves the rustdoc-related code out of `html_to_markdown` and
into the `rustdoc` crate.

Release Notes:

- N/A
2024-06-12 15:53:05 -04:00
Marshall Bowers
c3df9b79c6 Start on rustdoc crawler (#12942)
This PR adds a first pass at a rustdoc crawler.

We'll be using this to get information about a crate from the rustdoc
artifacts for use in the Assistant.

Release Notes:

- N/A

---------

Co-authored-by: Richard <richard@zed.dev>
2024-06-12 15:21:50 -04:00
Marshall Bowers
72dac24acf Add missing LICENSE file to ollama crate (#12943)
This PR adds a missing LICENSE file to the recently-added `ollama`
crate.

Also added the missing `lints.workspace = true` to the `Cargo.toml`.

Release Notes:

- N/A
2024-06-12 15:12:36 -04:00
Paul Eguisier
001f17c011 vim: Implement named registers (#12895)
Release Notes:

- vim: Add support for register selection `"a`-`"z`, `"0`-`"9`, `"-`.
`"_` and `"%`
([#11511](https://github.com/zed-industries/zed/issues/11511))

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-06-12 10:40:27 -06:00
Joseph T Lyons
3c3dad6830 v0.141.x dev 2024-06-12 12:35:03 -04:00
Conrad Irwin
1b28f93c64 Make notification windows not have titles on X11 (#12935)
Co-Authored-By: Max <max@zed.dev>

Release Notes:

- N/A

Co-authored-by: Max <max@zed.dev>
2024-06-12 10:27:40 -06:00
Thorsten Ball
2fd00a8f35 docs: Capitalize Vim in Vim mode documentation (#12934)
Release Notes:

- N/A
2024-06-12 17:38:25 +02:00
Kyle Kelley
bee3441c78 Ollama improvements (#12921)
Attempt to load the model early on when the user has switched the model.

This is a follow up to #12902

Release Notes:

- N/A
2024-06-12 08:10:51 -07:00
Joseph T Lyons
113546f766 Do not encourage additional ignored labels
We recently reduced the set of ignored labels down to a single label: "ignore top-ranking issues." It makes sense that we don't allow for multiple to be registered in this script now.
2024-06-12 10:57:33 -04:00
claytonrcarter
5e9f9b4edd Wrap JS/TS runnables in quotes (#12932)
Some of the runnables added in #12118 don't work for tests (or code)
that contain spaces. In other words, the runnable for a test like
```js
it('does the thing', () => ...)
```
would end up w/ something like `npx jest does the thing
/path/to/file.spec.js`, but what we really want is `npx jest
--testNamePattern "does the thing" /path/to/file.spec.js`. A similar
thing was happening for the "node execute selection" runnable: selecting
`let foo = 1` would run `node -e let foo = 1`, not `node -e "let foo =
1"`.

In my (somewhat limited?) experience, it's very common for tests like
these to include spaces, and of course a code selection is almost
certain to contain whitespace.

Not covered: 
- this just blindly wraps quotes around the symbol/code; in the future
it may make sense to try to figure out *what type of quote* to use. (eg
`it('does the "thing"', () => ...)` is a valid test name, but
`--testNamePattern "does the "thing""` would not work. Note the doubled
quotes.)
- I did not wrap the filenames in quotes to escape those for the shell,
nor did I test if that's actually an issue. In my experience, I've not
seen many (any?) test files that contain spaces in the name, but I
suspect that it would be an issue if a containing dir includes spaces.
(eg `npx jest ... /path/to/My Documents/Code/file.spec.js`

/cc @RemcoSmitsDev 

Release Notes:

- Fixed some runnables in Javascript/Typescript
2024-06-12 14:58:04 +02:00
Chung Wei Leong
ec95a33d8c Added TSX test runnables (#12922)
Fix #12884

Release Notes:

- Added runnable tests for TSX files.

---
Runnable tests can be customized via `tsx-test` tag

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2024-06-12 11:02:24 +02:00
maan2003
b82350979f linux: Use filesystem based unix socket instead of abstract namespace (#12756)
Release Notes:

- N/A

fixes: unable to launch multiple zed instances even if the support dirs
are different(example: bwrap based sandboxing)
2024-06-11 19:17:07 -06:00
Conrad Irwin
e16bbe048f Make install.sh sh-compatible (#12899)
Co-Authored-By: Max <max@zed.dev>

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
Co-authored-by: Shyim <github@shyim.de>
2024-06-11 19:12:14 -06:00
Mikayla Maki
ab41eddd8b Fix cursors on some GNOME installations (#12914)
This PR adds support for `org.gnome.desktop.interface`'s `cursor-theme`
setting on Wayland. This should fix cursors not showing up on some GNOME
installs. This PR also adds the wiring to watch the current cursor theme
value.

Thanks to @apricotbucket28 for helping debug the issue.

Release Notes:

- N/A
2024-06-11 17:39:25 -07:00
Kyle Kelley
4cb8d6f40e Ollama Provider for Assistant (#12902)
Closes #4424.

A few design decisions that may need some rethinking or later PRs:

* Other providers have a check for authentication. I use this
opportunity to fetch the models which doubles as a way of finding out if
the Ollama server is running.
* Ollama has _no_ API for getting the max tokens per model
* Ollama has _no_ API for getting the current token count
https://github.com/ollama/ollama/issues/1716
* Ollama does allow setting the `num_ctx` so I've defaulted this to
4096. It can be overridden in settings.
* Ollama models will be "slow" to start inference because they're
loading the model into memory. It's faster after that. There's no UI
affordance to show that the model is being loaded.

Release Notes:

- Added an Ollama Provider for the assistant. If you have
[Ollama](https://ollama.com/) running locally on your machine, you can
enable it in your settings under:

```jsonc
"assistant": {
    "version": "1",
    "provider": {
      "name": "ollama",
      // Recommended setting to allow for model startup
      "low_speed_timeout_in_seconds": 30,
    }
}
```

Chat like usual

<img width="1840" alt="image"
src="https://github.com/zed-industries/zed/assets/836375/4e0af266-4c4f-4d9e-9d74-1a91f76a12fe">

Interact with any model from the [Ollama
Library](https://ollama.com/library)

<img width="587" alt="image"
src="https://github.com/zed-industries/zed/assets/836375/87433ac6-bf87-4a99-89e1-96a93bf8de8a">

Open up the terminal to download new models via `ollama pull`:


![image](https://github.com/zed-industries/zed/assets/836375/af7ec411-76bf-41c7-ba81-64bbaeea98a8)
2024-06-11 17:35:27 -07:00
Marshall Bowers
127b9ed857 project_panel: Don't show file icon during rename when file icons are otherwise hidden (#12910)
This PR fixes an instance where the file icon for a project panel entry
would be shown during a rename even when the `project_panel.file_icons`
setting was set to `false`.

Resolves #12905.

Release Notes:

- Fixed an issue where file icons were displayed in the project panel
during a rename even when `project_panel.file_icons` was set to `false`
([#12905](https://github.com/zed-industries/zed/issues/12905)).
2024-06-11 19:45:47 -04:00
Mikayla Maki
c30f6a1582 Fix alt key getting stuck when tabbing on linux (#12912)
Release Notes:

- N/A
2024-06-11 15:14:01 -07:00
Marshall Bowers
8ccd2a0c99 Add tag handler for collecting crate items from rustdoc output (#12903)
This PR adds a tag handler for collecting crate items from rustdoc's
HTML output.

This will serve as the foundation for getting more insight into a
crate's contents.

Release Notes:

- N/A
2024-06-11 15:56:37 -04:00
Marshall Bowers
57b87be3a0 Hoist indexmap to workspace level (#12901)
This PR hoists `indexmap` up to a workspace dependency.

Release Notes:

- N/A
2024-06-11 15:31:55 -04:00
Mikayla Maki
80c14c9198 Pull app / OS info out of GPUI, add Linux information, make fallible window initialization (#12869)
TODO:
- [x] Finish GPUI changes on other operating systems 

This is a largely internal change to how we report data to our
diagnostics and telemetry. This PR also includes an update to our blade
backend which allows us to report errors in a more useful way when
failing to initialize blade.


Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-06-11 11:43:12 -07:00
Thorsten Ball
ec9e700e70 linux/x11: Mark windows as destroyed after destroy request (#12892)
Turns out we still get FocusOut and UnmapNotify events after the window
has been destroyed, which resulted in error messages popping up because
we can't find the window anymore that we want to mark as unfocused.



Release Notes:

- N/A
2024-06-11 17:23:48 +02:00
Conrad Irwin
a06189bbed Fix most vim tests on linux (#12873)
Fixes most vim tests on linux (and a few editor ones) by loading Zed
Mono
instead of relying on the system fallback stack.

Release Notes:

- N/A
2024-06-11 08:27:55 -06:00
Antonio Scandurra
53b0720d54 Remove headers from prompt library picker (#12889)
Also, as a drive-by, we're fixing up/down not working in inline
assistant editor.

Release Notes:

- N/A
2024-06-11 15:59:30 +02:00
Piotr Osiewicz
b6ea393d14 lsp: Add support for linked editing range edits (HTML tag autorenaming) (#12769)
This PR adds support for [linked editing of
ranges](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_linkedEditingRange),
which in short means that editing one part of a file can now change
related parts in that same file. Think of automatically renaming
HTML/TSX closing tags when the opening one is changed.
TODO:
- [x] proto changes
- [x] Allow disabling linked editing ranges on a per language basis.

Fixes #4535 

Release Notes:
- Added support for linked editing ranges LSP request. Editing opening
tags in HTML/TSX files (with vtsls) performs the same edit on the
closing tag as well (and vice versa). It can be turned off on a language-by-language basis with the following setting:
```
  "languages": {
    "HTML": {
      "linked_edits": true
    },
  }
```

---------

Co-authored-by: Bennet <bennet@zed.dev>
2024-06-11 15:52:38 +02:00
Antonio Scandurra
98659eabf1 Overhaul inline assistant (#12846)
This pull request introduces a new diff mechanism that helps users
understand exactly which lines were changed by the LLM.

Release Notes:

- N/A
2024-06-11 12:39:45 +02:00
Thorsten Ball
3722275cfa linux/x11: Only create ModifiersChanged event if they changed (#12879)
I noticed that when I use my mouse wheel, we get a ton of the
`XkbStateNotify` events, but the modifiers don't change, so we add a ton
of useless input events for the window.

Release Notes:

- N/A
2024-06-11 11:00:26 +02:00
Conrad Irwin
ef84ce76e3 linux make install (#12870)
Release Notes:

- N/A
2024-06-10 20:43:13 -06:00
Conrad Irwin
44a58647e4 Wait for composition to end before sending InputIgnored (#12871)
Release Notes:

- vim: Fixed `f`/`t` etc. for keys that require IME (#12522)
2024-06-10 20:03:21 -06:00
Conrad Irwin
4e98c23463 Reconnect button for remote projects (#12669)
Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
2024-06-10 18:09:47 -06:00
IceSentry
1914a42b1c Update windows doc to mention rust-lld linker error (#12859)
Release Notes:

- N/A

## Description

When using rust-lld it's possible to get a `STATUS_ACCESS_VIOLATION`
error at compile time. I added a bit of information about it in the
build guide for windows to recommend using a different linker when
building `zed`.

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-06-10 17:51:30 -04:00
Joseph T. Lyons
6afed19a00 Update EULA (#12858)
Release Notes:

- N/A
2024-06-10 16:09:18 -04:00
Marshall Bowers
2509af723f assistant: Improve JSON handling in /fetch command (#12864)
This PR improves the `/fetch` command with better support for URLs that
return JSON content.

JSON response bodies will now be pretty-printed and placed within a
Markdown code block:

<img width="690" alt="Screenshot 2024-06-10 at 3 39 52 PM"
src="https://github.com/zed-industries/zed/assets/1486634/4a7c1cb7-9f5b-4a63-9e8e-5168bf9a6625">

Release Notes:

- Improved the handling of JSON response bodies in the `/fetch` command
in the Assistant.
2024-06-10 15:49:51 -04:00
Marshall Bowers
8078e58494 Remove unused color crate (#12860)
This PR removes the `color` crate, as it was not used anywhere.

We had added this experimentally, but right now its existence is just a
source of confusion.

Release Notes:

- N/A
2024-06-10 15:35:44 -04:00
Marshall Bowers
b69c3129d0 Add missing LICENSE file to proto crate (#12863)
This PR adds a missing LICENSE file to the recently-extracted `proto`
crate.

Release Notes:

- N/A
2024-06-10 15:35:37 -04:00
Nathan Sobo
e2c5ce588b Fix target of proto diff on CI (#12861)
Release Notes:

- N/A
2024-06-10 15:16:23 -04:00
Mikayla Maki
7005aaa54d Update linux.md 2024-06-10 12:03:47 -07:00
Mikayla Maki
9db269735d Update linux.md 2024-06-10 12:01:56 -07:00
Antonio Scandurra
77e88c1ded Extract a proto crate out of rpc (#12852)
Release Notes:

- N/A

---------

Co-authored-by: Nathan <nathan@zed.dev>
2024-06-10 12:49:53 -06:00
Marshall Bowers
57c40299a5 Show extension download counts with thousands separators (#12857)
This PR adjusts the extension download counts to be displayed using
thousands separators.

Release Notes:

- Adjusted extension download counts to display with thousands
separators (e.g., `1,000,000`).
2024-06-10 14:19:17 -04:00
Marshall Bowers
0d5485bd6c assistant: Add /now slash command (#12856)
This PR adds a `/now` command to the Assistant for indicating the
current date and time to the model.

Release Notes:

- Added `/now` command to the Assistant for getting the current date and
time.
2024-06-10 14:05:02 -04:00
Vitaly Slobodin
a600799840 ruby: Remove outline for running tests (#12642)
Hi, this pull request superseeds the
https://github.com/zed-industries/zed/pull/12624
and removes queries for runnables from `outline.scm`. This pull request
has couple things to mention:

- Removed task for running tests with `minitest` as I think it's not
reliable in its state because, AFAIK, the only way to run `minitest`
with the specific line, i.e. `bundle exec rake test
spec/models/some_model.rb:12` is to use it with Rails. The support for
`minitest` is still there and users can add their own task, for
instance, when they use `minitest` in Rails to get support for running
tests:

  ```json
  {
    "label": "test $ZED_RELATIVE_FILE:$ZED_ROW",
    "command": "./bin/rails",
    "args": ["test", "\"$ZED_RELATIVE_FILE:$ZED_ROW\""],
    "tags": ["minitest-test"]
  }
  ```

**Question:** Perhaps that should be mentioned in the Ruby extension
documentation?

- Adjusted runnables queries to work without `ZED_SYMBOL`.

Release Notes:

- N/A
2024-06-10 18:04:43 +02:00
Thorsten Ball
05b6581147 linux/x11: handle XIM events sync to reduce lag (#12840)
This helps with the problem of keyboard input feeling laggy when the
event loop is under load.

What would previously happen is:

- N events from X11 arrive
- N events get forwarded to XIM
- N events are handled in N iterations of the event loop (sadly, yes: we
only seem to be getting back one `ClientMessage` per poll from XCB
connection)
- Each event is pushed into the channel
- N event loop iterations are needed to get the events off the channel
and handle them

With this change, we get rid of the last 2 steps: instead of pushing the
event onto a channel, we store it on the XIM handler itself, and then
work it off synchronously.

Usually one shouldn't block the event loop, but I think in this case -
user input! - it's better to handle the events directly instead of
re-enqueuing them again in a channel, where they can accumulate and need
multiple iterations of the loop to be worked off.

This does *not* fix the problem of input feeling choppy/slower when the
system is under load, but it makes the behavior now feel exactly the
same as when XIM is disabled.

I also think the code is easier to understand since it's more
straightforward.

Release Notes:

- N/A
2024-06-10 14:08:16 +02:00
Thorsten Ball
43d1a8040d linux: run runnables only when event loop is idle (#12839)
This change ensures that the event loop prioritizes enqueueing another
render or handling user input over executing runnables.

It's a subtle change as a result of a week of digging into performance
on X11. It's also not perfect: ideally we'd get rid of the intermediate
channel here and had more control over when and how we run runnables vs.
X11 events, but I think short of rewriting how we use an event loop,
this is good cost/benefit change.

To illustrate:

Before this change, it was possible to block the app from rendering for
a long time by just creating a ton of futures that were executed on the
"main" thread (we don't have a "main" thread on Linux, but we have a
single thread in which we run the event loop).

That was relatively easy to reproduce by opening the `zed` repository
and starting `rust-analyzer`: at some point `rust-analyzer` sends us so
many notifications, that are all handled in futures, that the event loop
is busy just working off the runnables, never getting to the events that
X11 sends us or our own timer to re-enqueue another render.

When you put print statements into the code to show when which event was
handled, you'd see something like this **before this change**:

```
[ ... hundreds of runnable.run() ... ]
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
new render tick timer. lag: 56.942049ms
X11 event
new render tick timer. lag: 9.668µs
X11 event
new render tick timer. lag: 9.955µs
X11 event
runnable.run()
runnable.run()
runnable.run()
runnable.run()
new render tick timer. lag: 12.462µs
X11 event
new render tick timer. lag: 14.868µs
X11 event
new render tick timer. lag: 11.234µs
X11 event
new render tick timer. lag: 11.681µs
X11 event
new render tick timer. lag: 13.926µs
X11 event
```

Note the `lag: 56ms`: that's the difference between when we wanted to
execute the callback that enqueues another render and when it ran.

Longer lags are possible, this is just the first one I grabbed from the
logs.

Now, compare this with the logs **after this change**:

```
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
new render tick timer. lag: 36.051µs
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
X11 event
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
runnable.run()
```

In-between many `runnable.run()` we'll always handle events.

So, in essence, what this change does is to introduce 2 priorities into
the X11 event queue:

- high: X11 events (user events, render events, ...), render tick, XIM
events, ...
- low: all async rust code

I've tested this with a debug build and release build and I think the
app now feels more responsive. It doesn't feel perfect still, especially
in the slow debug builds, but I couldn't observe 10s lockups anymore.

Since it's a pretty small change, I think we should go for it and see
how it behaves.

Thanks to @maan2003 this now also includes the same change to Wayland.

Release Notes:

- N/A

---------

Co-authored-by: maan2003 <manmeetmann2003@gmail.com>
2024-06-10 14:04:41 +02:00
Panghu
e829a8c3b0 Add auto-completion support for package.json files (#12792)
![截屏2024-06-08 07 56
41](https://github.com/zed-industries/zed/assets/21101490/da97e7d4-458b-4262-ac23-a4704af4f015)

Release Notes:

- Added auto-completion support for `package.json` files.
2024-06-08 13:33:29 +03:00
Arseny Kapoulkine
87845a349d cpp: Highlight sized type specifiers as keywords (#12751)
Without this, `unsigned` or `unsigned int` is not highlighted properly:
`int` is a primitive_type but `unsigned` is a sized_type_specifier. This
is already handled in C as both are part of @type highlight group.

Before:

![image](https://github.com/zed-industries/zed/assets/1106629/7210b769-9dff-428c-9e4f-55b652f91674)

After:

![image](https://github.com/zed-industries/zed/assets/1106629/8661c412-30f0-4b44-a4a2-1860a0b56a4e)

Release Notes:

- N/A
2024-06-08 13:26:10 +03:00
Kirill Bulatov
953393f6ce Rename workspace::Restart action into workspace::Reload (#12672)
Closes https://github.com/zed-industries/zed/issues/12609

Instead of adding some ordering mechanism to the actions, rename the
action so that it's not interfering with the `editor: restart language
server` command.

Before:

![image](https://github.com/zed-industries/zed/assets/2690773/b5e86eda-d766-49fc-a25b-f8b9fdb7b521)

![image](https://github.com/zed-industries/zed/assets/2690773/c5edeb56-12aa-496b-bb6f-dc705cbb9ae3)


After:

![image](https://github.com/zed-industries/zed/assets/2690773/ed30c68d-bfdd-4e00-bb5d-0be52fbe4e16)
![Screenshot 2024-06-05 at 09 46
25](https://github.com/zed-industries/zed/assets/2690773/9fe4eb52-0399-4321-85a9-3b07c11395ce)


Release Notes:

- Improved language server restart command ergonomics by renaming
`workspace::Restart` action into `workspace::Reload` to remove any other
"restart"-worded actions in the list
2024-06-08 13:23:59 +03:00
Conrad Irwin
75f8be6a0f vim: add guu gUU g~~ g/ (#12789)
Release Notes:

- vim: Add `g/` for project search
2024-06-07 16:45:38 -06:00
Max Brunsfeld
e174f16d50 Refactor: Make it possible to share a remote worktree (#12775)
This PR is an internal refactor in preparation for remote editing. It
restructures the public interface of `Worktree`, reducing the number of
call sites that assume that a worktree is local or remote.

* The Project no longer calls `worktree.as_local_mut().unwrap()` in code
paths related to basic file operations
* Fewer code paths in the app rely on the worktree's `LocalSnapshot`
* Worktree-related RPC message handling is more fully encapsulated by
the `Worktree` type.

to do:
* [x] file manipulation operations
* [x] sending worktree updates when sharing

for later
* opening buffers
* updating open buffers upon worktree changes

Release Notes:

- N/A
2024-06-07 12:53:01 -07:00
slowlydev
aa60fc2f19 Use the new assistant icon in the setup instructions (#12787)
This is a PR with just a small visual adjustment, so instructions are
up-to-date with the new icon.
I did not remove the "old" ai.svg as I am not sure if its gonna be used
in the future or if its has been completely replaced by the new "zed
assistant" icon.

Release Notes:

- Fixed the wrong icon being used in the assistant setup instructions.

For open ai

<img width="543" alt="image"
src="https://github.com/zed-industries/zed/assets/61624214/5f18a8f4-6761-4df5-8482-92582545dee5">

and anthropic

<img width="544" alt="image"
src="https://github.com/zed-industries/zed/assets/61624214/6ca3ed23-0f68-4c0d-bc8a-32ab7c607029">

how it looked before (Zed Preview 0.139.3
0c083b7f38):

<img width="526" alt="image"
src="https://github.com/zed-industries/zed/assets/61624214/af9c9fa8-89ed-4f6a-88ca-b285b4c522c3">
2024-06-07 15:12:16 -04:00
Conrad Irwin
5548773b2e vim: Add gU/gu/g~ (#12782)
Co-Authored-By: ethanmsl@gmail.com

Release Notes:

- vim: Added `gu`/`gU`/`g~` for changing case. (#12565)
2024-06-07 12:38:12 -06:00
Joseph T. Lyons
3eac83eece Add event for yarn project identification (#12785)
Report a `open yarn project` `app_event` for each worktree where
`yarn.lock` is found and only report it once per session.

Release Notes:

- N/A
2024-06-07 14:30:38 -04:00
Marshall Bowers
243a0e764d Block publishing of zed_extension_api v0.0.7 (#12784)
This PR adds a temporary block on publishing v0.0.7 of the
`zed_extension_api`.

We have breaking changes to the extension API that are currently staged
on `main` and are still being iterated on, so we don't want to publish
again until we're ready to commit to the new API.

This change is intended to prevent accidental publishing of the crate
before we're ready.

Release Notes:

- N/A
2024-06-07 14:16:21 -04:00
Conrad Irwin
6fa6e0718c Check validity of new.range too (#12781)
I'm not certain yet how it could be invalid, but we are still seeing
panics here.

Release Notes:

- Fixed a panic when opening the diagnostics view
2024-06-07 11:48:23 -06:00
Marshall Bowers
834089feb1 Handle Wikipedia code blocks in /fetch command (#12780)
This PR extends the `/fetch` command with support for Wikipedia code
blocks.

Release Notes:

- N/A
2024-06-07 12:54:33 -04:00
Marshall Bowers
9174858225 Add basic Wikipedia support to /fetch (#12777)
This PR extends the `/fetch` slash command with the initial support for
Wikipedia's HTML structure.

Release Notes:

- N/A
2024-06-07 12:03:43 -04:00
Thorsten Ball
a910f192db docs: Document how to setup Tailwind CSS support in Ruby (#12762)
Release Notes:

- N/A
2024-06-07 13:43:57 +02:00
Piotr Osiewicz
5f5e6b8616 workspace: Fix drag&dropping project panel entries into editor area (#12767)
Fixes #12733 

Release Notes:

- Fixed drag&dropping project panel entries into editor area & tab bar
2024-06-07 10:23:57 +02:00
Stanislav Alekseev
07dbd2bce8 Use rust-analyzer from path if possible (#12418)
Release Notes:

- Added support for looking up the `rust-analyzer` binary in `$PATH`. This allows using such tools as `asdf` and nix to configure per-folder rust installations. To enable this behavior, use the `path_lookup` key when configuring the `rust-analyzer` `binary`: `{"lsp": {"rust-analyzer": {"binary": {"path_lookup": true }}}}`.
2024-06-07 06:56:38 +02:00
Max Brunsfeld
48581167b7 Remove dependencies from the Worktree crate and make it more focused (#12747)
The `worktree` crate mainly provides an in-memory model of a directory
and its git repositories. But because it was originally extracted from
the Project crate, it also contained lingering bits of code that were
outside of that area:
* it had a little bit of logic related to buffers (though most buffer
management lives in `project`)
* it had a *little* bit of logic for storing diagnostics (though the
vast majority of LSP and diagnostic logic lives in `project`)
* it had a little bit of logic for sending RPC message (though the
*receiving* logic for those RPC messages lived in `project`)

In this PR, I've moved those concerns entirely to the project crate
(where they were already dealt with for the most part), so that the
worktree crate can be more focused on its main job, and have fewer
dependencies.

Worktree no longer depends on `client` or `lsp`. It still depends on
`language`, but only because of `impl language::File for
worktree::File`.

Release Notes:

- N/A
2024-06-06 11:16:58 -07:00
Thorsten Ball
00dfd217d8 astro: Bump version to 0.0.3 (#12744)
(Forgot in the other PR)

Release Notes:

- N/A
2024-06-06 19:17:51 +02:00
Thorsten Ball
22490f7968 ruby: Bump version to 0.0.7 (#12743)
(Forgot to bump in the other PR)
Release Notes:

- N/A
2024-06-06 19:07:41 +02:00
Thorsten Ball
880940856d ruby: Allow opt-in to Tailwind LS in string (#12742)
This fixes #12728 as much as I can tell.

The problem was that inside ERB files, when inside Ruby code, we didn't
treat `-` as part of the word, which broke completions.

So, with the change in here, and the following Zed settings, it works.

```json
{
  "languages": {
    "Ruby": {
      "language_servers": ["tailwindcss-language-server", "solargraph"]
    }
  },
  "lsp": {
    "tailwindcss-language-server": {
      "settings": {
        "includeLanguages": {
          "erb": "html",
          "ruby": "html"
        },
        "experimental": {
          "classRegex": ["\\bclass:\\s*['\"]([^'\"]*)['\"]"]
        }
    }
  }
}
```

This enabled `tailwindcss-language-server` for Ruby files and tells the
language server to look for classes inside `class: ""` strings.

See demo video.

Release Notes:

- Fixed `tailwindcss-language-server` not being activated inside Ruby
strings (inside `.erb`)
([#12728](https://github.com/zed-industries/zed/issues/12728)).

Demo video:


https://github.com/zed-industries/zed/assets/1185253/643343b4-d64f-4c4e-98a1-d10df0b24e31

Co-authored-by: Max Brunsfeld <max@zed.dev>
2024-06-06 19:02:25 +02:00
Thorsten Ball
c354793871 astro: Fix Tailwind LS not working in attributes (#12741)
This fixes #12402.

We already had the `tailwind-language-server` config in Astro's
`config.toml` here:
fd39f20842/extensions/astro/languages/astro/config.toml (L17-L23)

But it's not enough to add `overrides.string` to the `config.toml`, you
also need an `overrides.scm` file that sets the overrides.

And, tricky bit, when you add a single override to the `overrides.scm`
file you have to add all of them that Zed knows about. In my case, I had
to add `@comment` too, because Zed somehow expects that.

Release Notes:

- Fixed `tailwind-language-server` not working in attributes inside of
`*.astro` files.
([#12402](https://github.com/zed-industries/zed/issues/12402)).

Demo/proof:


https://github.com/zed-industries/zed/assets/1185253/05677a2d-831d-4e05-a1a2-4d1730ce2a46
2024-06-06 18:57:10 +02:00
Paul Eguisier
2f057785f7 Maintain cursor to upper line in visual mode indent/outdent (#12582)
Release Notes:

- vim: Fix indent via `<` and `>` not being repeatable with `.`.
[#12351](https://github.com/zed-industries/zed/issues/12351)
2024-06-06 17:45:25 +02:00
Panghu
fd39f20842 Prevent folder expansion when all items are closed (#12729)
Release Notes:

- Prevent folder expansion when all items are closed

### Problem
When all items are closed, the next activated file expands (see the
video below).


https://github.com/zed-industries/zed/assets/21101490/a7631cd2-4e97-4954-8b01-d283dd4796be


### Cause
When the currently active item is closed, Zed tries to activate the
previously active item. Activating an item by default expands the
corresponding folder, which can result in folders being expanded when
all files are closed.

### Fixed Video


https://github.com/zed-industries/zed/assets/21101490/d30f05c5-6d86-4e11-b349-337fa75586f3
2024-06-06 17:32:58 +02:00
Thorsten Ball
0c7e745be8 docs: Fix Vim documentation for bindings (#12735)
Release Notes:

- N/A
2024-06-06 15:55:07 +02:00
Nycheporuk Zakhar
3000f6ea22 Use cwd to run package.json script (#12700)
Fixes case when `package.json` is not in root directory. 
Usually in mono repository, where multiple `package.json` may be present

Release Notes:

- Fixed runnable for package.json in monorepos
2024-06-06 15:17:45 +03:00
Piotr Osiewicz
377e24b798 chore: Fix clippy for upcoming 1.79 Rust release (#12727)
1.79 is due for release in a week.
Release Notes:

- N/A
2024-06-06 12:46:53 +02:00
Antonio Scandurra
a0c0f1ebcd Rename conversations to contexts (#12724)
This just changes nomenclature within the codebase and should have no
external effect.

Release Notes:

- N/A
2024-06-06 11:40:54 +02:00
Antonio Scandurra
70ce06cb95 Improve UX for saved contexts (#12721)
Release Notes:

- Added search for saved contexts.
- Fixed a bug that caused titles generate by the LLM to be longer than
one line.
2024-06-06 10:22:39 +02:00
Thorsten Ball
9a5b97db00 linux/x11: Don't surround selection when XMI composing (#12632)
On X11 I was unable to type ä ü and other umlauts in files with
autoclose enabled, because typing ä requires me to hit

- compose key
- `"`
- `a`

When the `"` was typed, Zed would insert a matching `"` because it had a
selection around the dead-key that was inserted by the compose key.

We ran into a similar issue in #7611, but in the case of the Brazilian
keyboard, the `"` is the compose key so we didn't trigger the matching
`"`, because we didn't have a selection yet.

What this does is it fixes the issue by making the
surround-selection-with-quotes-or-brackets also depend on the autoclose
settings, which is didn't do before. This is a breaking change for users
of a Brazilian keyboard layout in which `"` cannot be used to surround
an existing selection with quotes anymore.

That _might_ be a change that users notice, but I can't think of
scenario for that where the user wants, say, `"` to be NOT autoclosed,
but work with selections. (Example is Markdown, for which autoclose for
`"` is disabled. Do we want that but allow surrounding with quotes?)

So it fixes the issue and makes the behavior slightly more consistent,
in my eyes.

Release Notes:

- Changed the behavior of surrounding selections with brackets/quotes to
also depend on the auto-close settings of the language. This is a
breaking change for users of a Brazilian keyboard layout in which `"`
cannot be used to surround an existing selection with quotes anymore.

Before:

[Screencast from 2024-06-04
11-49-51.webm](https://github.com/zed-industries/zed/assets/1185253/6bf255b5-32e9-4ba7-8b46-1e49ace2ba7c)

After:

[Screencast from 2024-06-04
11-52-19.webm](https://github.com/zed-industries/zed/assets/1185253/3cd196fc-20ba-465f-bb54-e257f7f6d9f3)
2024-06-06 10:01:38 +02:00
Dhairya Nadapara
0b75afd322 chore: added inl to cpp config (#12710)
Screenshot:
<img width="1027" alt="image"
src="https://github.com/zed-industries/zed/assets/19250981/1d35d35c-d31c-4feb-b2ca-a417972fadf6">

Release Notes:

- Added `inl` to cpp config ([12605](https://github.com/zed-industries/zed/issues/12605))
2024-06-06 10:23:36 +03:00
Brian Schwind
4fd698a093 Fix key-bindings doc typo (#12718)
`base_keymap` is a property of `settings.json`, not `keymap.json`. If
you run "toggle base keymap selector" and select a particular editor,
you will notice that it places the `base_keymap` property in
`settings.json`.

Release Notes:

- N/A
2024-06-06 09:41:28 +03:00
Chung Wei Leong
b50846205c Fixed default LSP default settings for JavaScript, TypeScript & TSX (#12716)
Fixed the default LSP settings for `JavaScript`, `TypeScript` & `TSX`,
correcting the "rest" value from `".."` to `"..."`.

Release Notes:
- N/A
2024-06-06 09:40:20 +03:00
Kirill Bulatov
a574036efd Update the whitespace docs in the default settings file (#12717) 2024-06-06 08:29:01 +03:00
Conrad Irwin
89641acf2f Fix ordering of keyboard shortcuts so that you can use AI on linux (#12714)
Release Notes:

- N/A
2024-06-05 21:58:37 -06:00
Nate Butler
611bf2d905 Update prompt library styles (#12689)
- Extend Picker to allow passing a custom editor. This allows creating a
custom styled input.
- Updates various picker styles

Before:

![CleanShot 2024-06-05 at 22 08
36@2x](https://github.com/zed-industries/zed/assets/1714999/96bc62c6-839d-405b-b030-31491aab8710)

After:

![CleanShot 2024-06-05 at 22 09
15@2x](https://github.com/zed-industries/zed/assets/1714999/a4938885-e825-4880-955e-f3f47c81e1e3)

Release Notes:

- N/A
2024-06-05 22:10:02 -04:00
Andrew Lygin
f476a8bc2a editor: Add ToggleTabBar action (#12499)
This PR adds the `editor: toggle tab bar` action that hides / shows the
tab bar and updates the `tab_bar.show` setting in `settings.json`
accordingly.

First mentioned in
https://github.com/zed-industries/zed/pull/7356#issuecomment-2118445379.

Release Notes:

- Added the `editor: toggle tab bar` action.
2024-06-05 19:50:57 -06:00
Mikayla Maki
d3d0d01571 Adjust IME action buffering to only apply to insert actions (#12702)
Follow up to https://github.com/zed-industries/zed/pull/12678
fixes https://github.com/zed-industries/zed/issues/11829

In this solution, we only buffer Insert Text actions from the macOS IME.
The marked text and unmark actions are eagerly processed, so that the
IME state is synchronized with the editor state during multi step
pre-edit composition.

Release Notes:

- Fixed an issue where the IME pre-edit could desynchronize from the
editor on macOS
([#11829](https://github.com/zed-industries/zed/pull/12651)).

Co-authored-by: Conrad <conrad@zed.dev>
2024-06-05 16:13:03 -07:00
Marshall Bowers
29d29f5a90 assistant: Initialize the UI font in the prompt library window (#12701)
This PR fixes an issue where the prompt library did not properly have
the UI font or rem size set.

Since it is being opened in a new window, we need to re-initialize these
values the same way we do in the main window.

Release Notes:

- N/A
2024-06-05 17:41:03 -04:00
Bennet Bo Fenner
9824e40878 lsp: Handle responses in background thread (#12640)
Release Notes:

- Improved performance when handling large responses from language
servers

---------

Co-authored-by: Piotr <piotr@zed.dev>
2024-06-05 23:06:44 +02:00
Conrad Irwin
1ad8d6ab1c Don't show backtraces in prompts (#12699)
Release Notes:

- N/A
2024-06-05 15:00:23 -06:00
CharlesChen0823
8745719687 vim: Fix g _ not having the expected behavior (#12607)
Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-06-05 15:00:13 -06:00
kshokhin
c7c19609b3 Search in selections (#10831)
Release Notes:

- Adding [#8617 ](https://github.com/zed-industries/zed/issues/8617)

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-06-05 13:42:51 -06:00
Conrad Irwin
428c143fbb Add ability to scroll popovers with vim (#12650)
Co-Authored-By: ahmadraheel@gmail.com



Release Notes:

- vim: allow scrolling the currently open information overlay using
`ctrl-{u,d,e,y}`etc. (#11883)
2024-06-05 13:39:17 -06:00
Marshall Bowers
f3460d440c html_to_markdown: Move TableHandler out of rustdoc (#12697)
This PR moves the `TableHandler` out of the `rustdoc` module, as it
doesn't contain anything specific to rustdoc.

Release Notes:

- N/A
2024-06-05 15:37:02 -04:00
Joseph T Lyons
071270fe88 Remove meta label
This label has been deleted. Now, the only label used for ignoring top-ranking issues is `ignore top-ranking issues`.
2024-06-05 13:29:34 -04:00
Joseph T Lyons
a59dd7d06d v0.140.x dev 2024-06-05 12:23:48 -04:00
Conrad Irwin
868284876d Bump alacritty to fix some file descriptor yuck (#12687)
https://github.com/alacritty/alacritty/pull/7996

Release Notes:

- Fixed a crash caused by bad file descriptor lifetime handling.
2024-06-05 09:12:05 -06:00
Antonio Scandurra
6bbe9a2253 Polish prompt library some more (#12686)
Release Notes:

- N/A
2024-06-05 16:55:37 +02:00
Antonio Scandurra
7a05db6d3d Cancel inline assist editor on blur if it wasn't confirmed (#12684)
Release Notes:

- N/A
2024-06-05 16:31:45 +02:00
Antonio Scandurra
3587e9726b Support wrapping and hard newlines in inline assistant (#12683)
Release Notes:

- Improved UX for the inline assistant. It will now automatically wrap
when the text gets too long, and you can insert newlines using
`shift-enter`.
2024-06-05 16:10:56 +02:00
Antonio Scandurra
a96782cc6b Allow using the inline assistant in prompt library (#12680)
Release Notes:

- N/A
2024-06-05 14:46:33 +02:00
Nicholas Cioli
0289c312c9 editor: Render boundary whitespace (#11954)
![image](https://github.com/zed-industries/zed/assets/1240491/3dd06e45-ae8e-49d5-984d-3d8bdf98d983)

Added support for only rendering whitespace that is on a
boundary, the logic of which is explained below:

- Any tab character
- Whitespace at the start and end of a line
- Whitespace that is directly adjacent to another whitespace


Release Notes:

- Added `boundary` whitespace rendering option
([#4290](https://github.com/zed-industries/zed/issues/4290)).




---------

Co-authored-by: Nicholas Cioli <nicholascioli@users.noreply.github.com>
2024-06-05 14:02:55 +03:00
Kirill Bulatov
63a8095879 Revert "Fix a bug where the IME pre-edit would desync from Zed (#12651)" (#12678)
This reverts commit 1a0708f28c since after
that, default task-related keybindings (alt-t and alt-shift-t) started
to leave `†` and `ˇ` symbols in the text editors before triggering
actions.


Release Notes:

- N/A
2024-06-05 13:54:06 +03:00
Kirill Bulatov
1768c0d996 Do not occlude terminal pane by terminal element (#12677)
Release Notes:

- Fixed file drag and drop not working for terminal

Co-authored-by: Antonio Scandurra <antonio@zed.dev>
2024-06-05 13:43:18 +03:00
Antonio Scandurra
27e9c68988 Autocomplete commands that don't require access to workspace in prompt library (#12674)
This is useful to autocomplete prompts when writing a new one in the
prompt library.

Release Notes:

- N/A
2024-06-05 10:07:43 +02:00
Kirill Bulatov
ad2ddf1200 Omit clickable hunks when git hunks are disabled in the gutter (#12671)
Closes https://github.com/zed-industries/zed/issues/12644 

https://github.com/zed-industries/zed/pull/12425 fixes the issue so that
clicks are never reaching the hunks' hitboxes in such case, this PR
actually removes those hitboxes.
Hunks can still be toggled via the action.

Release Notes:

- N/A
2024-06-05 10:05:53 +03:00
Owen Law
d6e271c956 Add note about auto-updating on Linux (#12662)
Release Notes:

- N/A
2024-06-04 19:13:08 -06:00
Mikayla Maki
da29e33f50 Auto updater disabler (#12660)
Supersedes https://github.com/zed-industries/zed/pull/12659
Fixes https://github.com/zed-industries/zed/issues/12588

One of Zed's core features is our collaboration software. As such, it is
important that we notify the user when their RPC protocol is out of
date, and how to update it. This PR adds a mechanism to replace the
existing auto updater with a message explaining how to update Zed for
this environment.

Release Notes:

- N/A
2024-06-04 15:56:18 -07:00
Marshall Bowers
3fd118f8e1 html_to_markdown: Remove unused examples (#12658)
This PR removes the unused `examples` from the `html_to_markdown` crate.

I was just using these to dogfood the parsing initially, but now that
it's wired up in the Assistant, the examples are no longer useful.

Release Notes:

- N/A
2024-06-04 18:39:41 -04:00
Conrad Irwin
27beb9e697 Update linux binary expectations (#12622)
Fixes #12585

This changes the expectations for installed binaries on linux based on
work
that @jirutka has done for Alpine.

In particular, we now put the cli in place as `bin/zed` and the zed
binary as
`libexec/zed-editor`, and assume that packagers do the same.

cc @someone13574

Release notes:

- N/A

---------

Co-authored-by: Mikayla <mikayla@zed.dev>
2024-06-04 15:31:01 -07:00
Conrad Irwin
c7d56302d2 Always open the project panel for dev server projects (#12654)
Release Notes:

- N/A
2024-06-04 16:07:12 -06:00
Marshall Bowers
74cb92f9cc vue: Bump to v0.0.3 (#12656)
This PR bumps the Vue extension to v0.0.3.

Changes:

- #11482

Release Notes:

- N/A
2024-06-04 18:06:40 -04:00
Paul Eguisier
8a659b0c60 Implement Indent & Outdent as operators (#12430)
Release Notes:

- Fixes [#9697](https://github.com/zed-industries/zed/issues/9697).

Implements `>` and `<` with motions and text objects.
Works with repeat action `.`
2024-06-04 15:17:01 -06:00
d1y
25050e8027 vue: Improve syntax highlighting (#11482)
close #8989

Before:

<img width="690" alt="image"
src="https://github.com/zed-industries/zed/assets/45585937/02b6f703-d54a-4e08-82f8-4ed624f37a64">

After:

<img width="571" alt="image"
src="https://github.com/zed-industries/zed/assets/45585937/9abd39d0-c4e8-426e-b0d8-62e26090332a">


Release Notes:

- Improve vue highlighting
2024-06-04 14:04:54 -07:00
Marshall Bowers
2d9479667f Make HTML to Markdown conversion more pluggable (#12653)
This PR overhauls the HTML to Markdown conversion functionality in order
to make it more pluggable. This will ultimately allow for supporting a
variety of different HTML input structures (both natively and via
extensions).

As part of this, the `rustdoc_to_markdown` crate has been renamed to
`html_to_markdown`.

The `MarkdownWriter` now accepts a list of trait objects that can be
used to drive the conversion of the HTML into Markdown. Right now we
have some generic handler implementations for going from plain HTML
elements to their Markdown equivalents, as well as some rustdoc-specific
ones.

Release Notes:

- N/A
2024-06-04 16:14:26 -04:00
Conrad Irwin
1c617474fe Allow restarting remote language servers (#12652)
Release Notes:

- Added the ability to restart the remote language servers when
collaborating
2024-06-04 14:09:01 -06:00
Mikayla Maki
1a0708f28c Fix a bug where the IME pre-edit would desync from Zed (#12651)
fixes #11829 

In https://github.com/zed-industries/zed/pull/7494, we introduced IME
event buffering, so that we could preempt the IME with a keystroke event
in some cases. However, this caused a desynchronization bug in long
multi-step IME composition, such as the pre-edit used in the Japanese
Romaji keyboard (and other languages). We found that this was due to the
IME issuing actions, and then immediately querying the editor's state
before we had applied those actions. Therefore, this PR removes IME
action buffering.

We have tested all of the cases in the `handle_key_event` documentation
and added a few of our own.

Release Notes:

- Fixed an issue where the IME pre-edit could desynchronize from the
editor on macOS
([#11829](https://github.com/zed-industries/zed/pull/12651))

---------

Co-authored-by: Jan Solanti <jhs@psonet.com>
2024-06-04 12:17:44 -07:00
Piotr Osiewicz
62e790074c vcs_menu: Fix header taking up too much space (#12646)
We've spotted a regression following
https://github.com/zed-industries/zed/pull/12468


![image](https://github.com/zed-industries/zed/assets/24362066/8e2659c7-50fe-4a09-af9d-d04416a66276)
This PR addresses that.
Release Notes:

- N/A

Co-authored-by: Bennet <bennet@zed.dev>
2024-06-04 19:13:21 +02:00
Antonio Scandurra
c5b22eee2d Polish prompt library UX (#12647)
This could still use some improvement UI-wise but the user experience
should be a lot better.

- [x] Show in "Window" application menu
- [x] Load prompt as it's selected in the picker
- [x] Refocus picker on `esc`
- [x] When creating a new prompt, if a new prompt already exists and is
unedited, activate it instead
- [x] Add `/default` command
- [x] Evaluate /commands on prompt insertion
- [x] Autocomplete /commands (but don't evaluate) during prompt editing
- [x] Show token count using the settings model, right-aligned in the
editor
- [x] Picker 
- [x] Sorted alpha
- [x] 2 sublists
    - Default
        - Empty state: Star a prompt to add it to your default prompt
        - Otherwise show prompts with star on hover
    - All
        - Move prompts with star on hover

Release Notes:

- N/A
2024-06-04 18:36:54 +02:00
Marshall Bowers
e4bb666eab assistant: Add /fetch slash command (#12645)
This PR adds a new `/fetch` slash command to the Assistant for fetching
the content of an arbitrary URL as Markdown.

Currently it's just using the same HTML to Markdown conversion that
`/rustdoc` uses, but I'll be working to refine the output to be more
widely useful.

Release Notes:

- N/A
2024-06-04 11:56:23 -04:00
Piotr Osiewicz
910f668f4d php/elixir: Bump extensions versions to 0.0.6 and 0.0.5 (#12636)
Includes:
- https://github.com/zed-industries/zed/pull/12526
- https://github.com/zed-industries/zed/pull/11879
- https://github.com/zed-industries/zed/pull/12467

Release Notes:

- N/A
2024-06-04 13:24:31 +02:00
Piotr Osiewicz
8e79609288 editor: Cancel ongoing completion requests more eagerly (#12630)
Previously, we were:
- cancelling previous requests only after the latest one has completed
- always running the debounced documentation resolution to completion,
even when we had no need for it.

In this commit, we drop the ongoing completion requests as soon as the
new one is fired.
Fixes #5166 

Release Notes:

- Improved performance and reliability of completions in large
Typescript projects

Co-authored-by: Bennet Bo <bennet@zed.dev>
2024-06-04 12:22:01 +02:00
Kirill Bulatov
47122a3115 Fix excluded file creation (#12620)
Fixes https://github.com/zed-industries/zed/issues/10890

* removes `unwrap()` that caused panics for text elements with no text,
remaining after edit state is cleared but project entries are not
updated, having the fake, "new entry"
* improves discoverability of the FS errors during file/directory
creation: now those are shown as workspace notifications
* stops printing anyhow backtraces in workspace notifications, printing
the more readable chain of contexts instead
* better indicates when new entries are created as excluded ones


Release Notes:

- Improve excluded entry creation workflow in the project panel
([10890](https://github.com/zed-industries/zed/issues/10890))
2024-06-04 10:31:43 +03:00
Conrad Irwin
edd613062a linux watcher (#12615)
fixes https://github.com/zed-industries/zed/issues/12297
fixes https://github.com/zed-industries/zed/issues/11345

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
2024-06-03 22:17:10 -06:00
Mikayla Maki
3cd6719b30 Fix issues with Claude in Assistant2 (#12619)
Release Notes:

- N/A

---------

Co-authored-by: Nathan <nathan@zed.dev>
2024-06-03 16:30:09 -07:00
Marshall Bowers
afc0650a49 Paginate through extensions from the blob store (#12614)
This PR updates the background task used to fetch extensions from the
blob store to account for the possibility that the result set will be
paginated.

Will now paginate through all of the results and collect them up before
proceeding to determining which extensions need to be synced.

Release Notes:

- N/A
2024-06-03 17:17:46 -04:00
Marshall Bowers
14c2fab8ab assistant: Allow /rustdoc to use local docs (#12613)
This PR updates the `/rustdoc` slash command to use local docs built
with `cargo doc`.

If the docs for a particular crate/module are available locally, those
will be used. Otherwise, it will fall back to retrieving the docs from
`docs.rs`.

The placeholder output for the slash command will indicate which source
was used for the docs:

<img width="289" alt="Screenshot 2024-06-03 at 4 13 42 PM"
src="https://github.com/zed-industries/zed/assets/1486634/729112e4-80ca-4f08-bdb3-88fc950351c3">

Release Notes:

- N/A
2024-06-03 16:23:25 -04:00
Marshall Bowers
c752763301 Remove default language settings that are provided by extensions (#12612)
This PR removes some default language settings that are now provided by
their respective extensions.

In #10296 we added the ability for the language configuration within
extensions to provide certain language settings (e.g., `tab_size`).

New versions of the extensions have been published that take advantage
of that and have been in circulation for over a month now. To that end,
we no longer need these settings provided as defaults.

Release Notes:

- N/A
2024-06-03 14:40:23 -04:00
Marshall Bowers
2f65c3c6e6 Disable vtsls by default (#12611)
This PR adds default settings to disable `vtsls` by default so that
there aren't multiple TypeScript language servers running.

I also renamed the language server from `vtsls-language-server` to just
`vtsls`, since the `-language-server` suffix was redundant.

Release Notes:

- N/A
2024-06-03 14:30:33 -04:00
apricotbucket28
959f0dcded Fix terminal key bindings (#12558)
Fixes https://github.com/zed-industries/zed/issues/12556

Only changed the keymap on Linux since I'm not sure if the behaviour is
correct on macOS

Release Notes:

- N/A
2024-06-03 11:25:27 -07:00
Sean Billig
be2df79d5c gpui: Handle null string pointer in window::insert_text (#12446)
`[NSString UTF8String]` sometimes returns null (it's documented as
such), and when it does, zed crashes in `window::insert_text`. I'm
running into this sometimes when using alt-d to delete forward. It
usually only happens with multiple cursors, but sometimes with a single
cursor. It *might* only happen when using the "Unicode Hex Input"
keyboard 'Input Source' (which I started using to avoid entering weird
characters in zed when using emacs meta keybindings that I haven't
defined in zed).

When using the US English input source, alt-d always results in a call
to `insert_text`. When using the Unicode Hex Input source it usually
doesn't, but when it does `text.UTF8String()` returns null. `text` isn't
null. `[text length]` returns 1. `[text characterAtIndex: 0]` seems to
always return `56797` (an undefined utf-16 codepoint).

Release Notes:

- Fixed crash on mac when deleting with alt-d
2024-06-03 09:36:38 -07:00
apricotbucket28
344e5e1cf2 wayland: Fix window bounds restoration (#12581)
Fixes multiple issues that prevented window bounds restoration to not
work on Wayland.

Note: Since the display uuid depends on the `wl_output.name` field, this
only works properly on KDE 5.26+ or Gnome 44+ ([kwin
commit](330a02d862),
[mutter](7e838b1115)).

Release Notes:

- N/A
2024-06-03 09:27:01 -07:00
apricotbucket28
ed86b86dc7 cosmic_text: Handle variation selectors; fix emoji colors (#12587)
Basically, we detect if a glyph is a variation selector if its `id` is 3
(i.e. a whitespace character) and if it comes from an emoji font (since
variation selectors are only used for emoji glyphs).

- Fixes https://github.com/zed-industries/zed/issues/11703 and
https://github.com/zed-industries/zed/issues/12022

Release Notes:

- N/A
2024-06-03 09:25:44 -07:00
xzbdmw
726f23e867 Add !vimwaiting to togglecomment (#12552)
Release Notes:
-Fixed #12483 
It turns out to be very simple. `fg` has conflict with `gc` mapping so
when you type g editor state is pending.
2024-06-03 17:23:29 +02:00
Piotr Osiewicz
b1efea1100 typescript: Add support for VTSLS (#12606)
You can opt into using VTSLS instead of typescript-language-server by
pasting the following snippet into your settings:
```
  "languages": {
    "TSX": {
      "language_servers": [
        "!typescript-language-server",
        "vtsls-language-server"
      ]
    }
  },
```

Related to: #5166 
Release Notes:

- Added support for using [vtsls](https://github.com/yioneko/vtsls)
language server for Typescript/Javascript.
2024-06-03 17:11:28 +02:00
Owen Law
2b21c89e3c Fix XI2 Scrolling Issue (#12603)
ref #11679
https://github.com/zed-industries/zed/pull/11235#issuecomment-2144727144

Filters leave events to ensure they are in the normal notify leave
events (not grab or ungrab)
([spec](https://www.x.org/releases/X11R7.7/doc/inputproto/XI2proto.txt)).
Confirmed to fix the issue @mrnugget was having.

Release Notes:

- linux: Fixed a regression that caused some X11 input devices being
unable to scroll.
2024-06-03 17:10:14 +02:00
Bennet Bo Fenner
d0fa012bf8 Support formatting unsaved buffers with external formatter (#12597)
Closes #4529


https://github.com/zed-industries/zed/assets/53836821/b84efd5e-89da-4ff7-9a29-2b2f7285d427

Release Notes:

- Added ability to format unsaved buffers with external formatters
([#4529](https://github.com/zed-industries/zed/issues/4529))
2024-06-03 16:32:16 +02:00
Thorsten Ball
338df5de1d linux/x11: Ignore bounds.origin on resize event (#12604)
This fixes #11236 by ignoring the `bounds.origin` values when the window
is only being resized.

The cause for the issue was that the `ConfigureNotify` event would
contain "wrong" values when the window was being resized (by dragging a
corner).

In my case it would *always* contain x:14/y:49, which is I think might
map to the origin of the top bar in GNOME.

We would then persist these wrong values when serializing the workspace.
On restart, we'd use these values and end up with the window decorations
in the wrong place.

What I still don't know:
1. What exactly the 14/49 map to, because it's not the origin of the top
bar in GNOME. I also tried the X11 TranslateCoordinates call but
couldn't get meaningful results back (even taking scale factor into
account).
2. Why the window decorations end up looking wrong vs. the window being
in the first place. But if you look at my screenshot in #11236, it looks
like the decorations are off exactly by 14/49px.

That being said, I think the solution here is a good one for now: we
don't do an additional X11 call and when we're resizing, we're not
interested in the origin changing.



Release Notes:

- N/A

Proof:

[Screencast from 2024-06-03
15-08-36.webm](https://github.com/zed-industries/zed/assets/1185253/90efccfc-8ec6-42d2-8380-1625eff57805)
2024-06-03 16:25:12 +02:00
Nathan Sobo
5f98b9617a Start on a database-backed prompt library (#12468)
Using the file system as a database seems like it's easy, but it's
actually a real pain. I'd like to use LMDB to store the prompts locally
so we have more control. We can always add an export option, but I want
the source of truth to be somewhere other than the file system.

So far, I have a PromptStore which is global to the application and can
be initialized on startup. Then there's a `PromptLibrary` which is
intended to be the root of a new kind of Zed window. I haven't actually
seen pixels yet, but I've sketched out the basics needed to create a new
prompt, save, etc.

Still lots to figure out but the foundations of being backed by a DB and
rendering in an independent window are in place.

/cc @iamnbutler @as-cii 

Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
2024-06-03 15:58:43 +02:00
Piotr Osiewicz
18e2b43d6d task: Rebind UseSelectedQuery in modal to F2 (#12601)
Also fix click handler for "Rerun last task".

Fixes #12580
Release Notes:

- Fixed click handler for "rerun last task" in task modal not working.
- Rebound "picker::UseSelectedQuery" from `opt-E` to `F2`.
2024-06-03 12:52:44 +02:00
Piotr Osiewicz
5e3d85c023 json: Fix tsconfig.json schema overriding other schemas (such as keymap) (#12600)
@mrnugget spotted that tsconfig.json schema is getting applied on
current Nightly. I've tracked it down to a misconfiguration of JSON
language server. Mea culpa.

No release note as that change has not went out to the public yet.

Release Notes:

- N/A
2024-06-03 12:16:09 +02:00
Jason Lee
ae55d35f19 windows: Fix project prepare_ssh_shell to support setting PATH on Windows (#12370)
Release Notes:

- N/A

Update to use `std::env::join_paths` to prepare `PATH` env.

Ref
https://github.com/zed-industries/zed/pull/12087#issuecomment-2122852384
@mrnugget

---------

Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
2024-06-03 11:57:50 +02:00
Francisco Rivas
d0142b820f Change on remote setup and one more SSH command as example (#12591)
Release Notes:
- N/A
2024-06-03 11:34:09 +03:00
Jayden Seric
b218d8778d Fix TSX and JavaScript shorthand property syntax highlighting (#12512)
This replicates the fix for the TypeScript language for
https://github.com/zed-industries/zed/issues/5239 in
https://github.com/zed-industries/zed/pull/12505 for the TSX and
JavaScript languages, fixing
https://github.com/zed-industries/zed/issues/12510 and fixing
https://github.com/zed-industries/zed/issues/12509 .

See
https://github.com/zed-industries/zed/pull/12505#issuecomment-2141002505
.

Keep in mind I don't have a proper Zed local development environment
setup to test these simple changes.

Release Notes:

- Fixed TSX shorthand property syntax highlighting
([#12510](https://github.com/zed-industries/zed/issues/12510)).
- Fixed JavaScript shorthand property syntax highlighting
([#12509](https://github.com/zed-industries/zed/issues/12509)).
2024-06-03 00:52:16 +02:00
Bennet Bo Fenner
de8ef08143 Disable indent guides for single line editors (#12584)
This PR disables indent guides by default for single line editors. Right
now indent guides show up in the project search editor (which is only a
single line)

<img width="715" alt="image"
src="https://github.com/zed-industries/zed/assets/53836821/0b61da71-6f64-424d-9612-6a34eac4686a">


Release Notes:

- Fixed an issue where indent guides would show up in a single line
editor (e.g. project search, buffer search)
2024-06-02 17:57:45 +02:00
Matin Aniss
66b73c2d60 Fix GPUI get_menus documentation (#12571)
Release Notes:

- N/A
2024-06-02 13:06:14 +02:00
Bennet Bo Fenner
ab8d25e0a2 indent guides: Respect language specific settings in multibuffers (#12528)
Indent guides can be configured per language, meaning that in a multi
buffer we can get excerpts where indent guides should be
disabled/enabled/styled differently than other excerpts.

Imagine the following scenario, i have indent guides disabled in my
settings, but want to enable them for JS and Python. I also want to use
a different line width for python files. Something like this is now
supported:

<img width="445" alt="image"
src="https://github.com/zed-industries/zed/assets/53836821/0c91411c-145c-4210-a883-4c469d5cb828">

And the relevant settings for the example above:
```json
"indent_guides": {
  "enabled": false
},
"languages": {
  "JavaScript": {
    "indent_guides": {
      "enabled": true
    }
  },
  "Python": {
    "indent_guides": {
      "enabled": true,
      "line_width": 5
    }
  }
}
```



Release Notes:

- Respect language specific settings when showing indent guides in a
multibuffer
- Fixes an issue where indent guide specific settings were not
recognized when specified in local settings
2024-06-01 20:33:32 +02:00
Rayduck
95e360b170 python: Add runnable unittest tasks (#12451)
Add runnable tasks for Python, starting with `unittest` from the
standard library. Both `TestCase`s (classes meant to be a unit of
testing) and individual test functions in a `TestCase` will have
runnable icons. For completeness, I also included a task that will run
`unittest` on the current file.

The implementation follows the `unittest` CLI. The unittest module can
be used from the command line to run tests from modules, classes or even
individual test methods:

```
python -m unittest test_module.TestClass
python -m unittest test_module.TestClass.test_method
```

```python
import unittest

class TestStringMethods(unittest.TestCase):

    def test_upper(self):
        self.assertEqual('foo'.upper(), 'FOO')

    def test_isupper(self):
        self.assertTrue('FOO'.isupper())
        self.assertFalse('Foo'.isupper())

    def test_split(self):
        s = 'hello world'
        self.assertEqual(s.split(), ['hello', 'world'])
        # check that s.split fails when the separator is not a string
        with self.assertRaises(TypeError):
            s.split(2)

if __name__ == '__main__':
    unittest.main()
```

From the snippet provided by `unittest` docs, a user may want to run
test_split independently of the other test functions in the test case.
Hence, I decided to make each test function runnable despite `TestCase`s
being the unit of testing.

## Example of running a `TestCase`
<img width="600" alt="image"
src="https://github.com/zed-industries/zed/assets/16619392/7be38b71-9d51-4b44-9840-f819502d600a">

## Example of running a test function in a `TestCase`
<img width="600" alt="image"
src="https://github.com/zed-industries/zed/assets/16619392/f0b6274c-4fa7-424e-a0f5-1dc723842046">

`unittest` will also run the `setUp` and `tearDown` fixtures.

Eventually, I want to add the more commonly used `pytest` runnables
(perhaps as an extension instead).

Release Notes:

- Added runnable tasks for Python `unittest`.
([#12080](https://github.com/zed-industries/zed/issues/12080)).
2024-06-01 18:46:36 +02:00
Piotr Osiewicz
f0d979576d collab_ui: remove branch menu popover in favor of opening a modal (#12562)
This commit also removes a bunch of dead code.

Fixes #12544

Release Notes:

- Removed branch popover menu - clicking on the branch name in left-hand
corner now always opens a branch modal
2024-06-01 18:40:25 +02:00
Piotr Osiewicz
fbcc5ccdb9 typescript: Add completions for tsconfig.json properties (#12560)
Release Notes:

- Added completions for tsconfig.json config file.
2024-06-01 17:51:58 +02:00
Remco Smits
29b5253a1d JavaScript: Add runnable tests (#12118)
https://github.com/zed-industries/zed/assets/62463826/2912c940-bd00-483d-9ce7-df1a2539560a


Release Notes:

- Added runnable tests for JavaScript & Typescript files.
- Added task to run selected javascript code.
2024-06-01 14:28:53 +02:00
Mikayla Maki
94c3101fb0 Fix or promote leftover TODOs and GPUI APIs (#12514)
fixes https://github.com/zed-industries/zed/issues/11966

Release Notes:

- N/A
2024-05-31 18:36:15 -07:00
João Otávio Biondo
a6e0c8aca1 elixir: Add runnable tasks (#12526)
Release Notes:

- Added runnable tasks to Elixir tests (modules, `describe` and `test`
blocks)


https://github.com/zed-industries/zed/assets/7737375/06f1b4cb-0364-4c30-982d-6dda3193b5d2
2024-05-31 20:49:34 +02:00
391 changed files with 26638 additions and 11207 deletions

View File

@@ -4,3 +4,11 @@ rustflags = ["-C", "symbol-mangling-version=v0", "--cfg", "tokio_unstable"]
[alias]
xtask = "run --package xtask --"
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
[target.aarch64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]

View File

@@ -74,8 +74,8 @@ jobs:
version: v1.29.0
- uses: bufbuild/buf-breaking-action@v1
with:
input: "crates/rpc/proto/"
against: "https://github.com/${GITHUB_REPOSITORY}.git#branch=${BUF_BASE_BRANCH},subdir=crates/rpc/proto/"
input: "crates/proto/proto/"
against: "https://github.com/${GITHUB_REPOSITORY}.git#branch=${BUF_BASE_BRANCH},subdir=crates/proto/proto/"
macos_tests:
timeout-minutes: 60
@@ -101,7 +101,6 @@ jobs:
- name: Build other binaries and features
run: cargo build --workspace --bins --all-features; cargo check -p gpui --features "macos-blade"
# todo(linux): Actually run the tests
linux_tests:
timeout-minutes: 60
name: (Linux) Run Clippy and tests
@@ -120,6 +119,9 @@ jobs:
- name: cargo clippy
run: cargo xtask clippy
- name: Run tests
uses: ./.github/actions/run_tests
- name: Build Zed
run: cargo build -p zed
@@ -305,9 +307,6 @@ jobs:
exit 1
fi
- name: Generate license file
run: script/generate-licenses
- name: Create and upload Linux .tar.gz bundle
run: script/bundle-linux
@@ -328,3 +327,86 @@ jobs:
body: ""
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
bundle-linux-aarch64:
timeout-minutes: 60
name: Create arm64 Linux bundle
runs-on:
- hosted-linux-arm-1
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
needs: [linux_tests]
env:
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
clean: false
- name: "Setup jq"
uses: dcarbone/install-jq-action@v2
- name: Set up Clang
run: |
sudo apt-get update
sudo apt-get install -y llvm-10 clang-10 build-essential cmake pkg-config libasound2-dev libfontconfig-dev libwayland-dev libxkbcommon-x11-dev libssl-dev libzstd-dev libvulkan1 libgit2-dev
echo "/usr/lib/llvm-10/bin" >> $GITHUB_PATH
- uses: rui314/setup-mold@v1
with:
mold-version: 2.32.0
- name: rustup
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Limit target directory size
run: script/clear-target-dir-if-larger-than 100
- name: Determine version and release channel
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
run: |
set -eu
version=$(script/get-crate-version zed)
channel=$(cat crates/zed/RELEASE_CHANNEL)
echo "Publishing version: ${version} on release channel ${channel}"
echo "RELEASE_CHANNEL=${channel}" >> $GITHUB_ENV
expected_tag_name=""
case ${channel} in
stable)
expected_tag_name="v${version}";;
preview)
expected_tag_name="v${version}-pre";;
nightly)
expected_tag_name="v${version}-nightly";;
*)
echo "can't publish a release on channel ${channel}"
exit 1;;
esac
if [[ $GITHUB_REF_NAME != $expected_tag_name ]]; then
echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}"
exit 1
fi
- name: Create and upload Linux .tar.gz bundle
run: script/bundle-linux
- name: Upload Linux bundle to workflow run if main branch or specific label
uses: actions/upload-artifact@v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-aarch64-unknown-linux-gnu.tar.gz
path: target/release/zed-*.tar.gz
- name: Upload app bundle to release
uses: softprops/action-gh-release@v1
if: ${{ env.RELEASE_CHANNEL == 'preview' }}
with:
draft: true
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
files: target/release/zed-linux-aarch64.tar.gz
body: ""
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -18,7 +18,7 @@ jobs:
- uses: pnpm/action-setup@v3
with:
version: 8
version: 9
- name: Setup Node
uses: actions/setup-node@v4

252
Cargo.lock generated
View File

@@ -88,9 +88,8 @@ dependencies = [
[[package]]
name = "alacritty_terminal"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6d1ea4484c8676f295307a4892d478c70ac8da1dbd8c7c10830a504b7f1022f"
version = "0.24.1-dev"
source = "git+https://github.com/alacritty/alacritty?rev=cacdb5bb3b72bad2c729227537979d95af75978f#cacdb5bb3b72bad2c729227537979d95af75978f"
dependencies = [
"base64 0.22.0",
"bitflags 2.4.2",
@@ -107,7 +106,7 @@ dependencies = [
"signal-hook",
"unicode-width",
"vte",
"windows-sys 0.48.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -347,19 +346,20 @@ dependencies = [
"ctor",
"editor",
"env_logger",
"feature_flags",
"file_icons",
"fs",
"futures 0.3.28",
"fuzzy",
"gpui",
"gray_matter",
"heed",
"html_to_markdown",
"http 0.1.0",
"indoc",
"language",
"log",
"menu",
"multi_buffer",
"ollama",
"open_ai",
"ordered-float 2.10.0",
"parking_lot",
@@ -368,13 +368,14 @@ dependencies = [
"rand 0.8.5",
"regex",
"rope",
"rustdoc_to_markdown",
"rustdoc",
"schemars",
"search",
"semantic_index",
"serde",
"serde_json",
"settings",
"similar",
"smol",
"strsim 0.11.1",
"strum",
@@ -1513,7 +1514,7 @@ dependencies = [
[[package]]
name = "blade-graphics"
version = "0.4.0"
source = "git+https://github.com/kvark/blade?rev=bdaf8c534fbbc9fbca71d1cf272f45640b3a068d#bdaf8c534fbbc9fbca71d1cf272f45640b3a068d"
source = "git+https://github.com/zed-industries/blade?rev=33fd51359d113c03b785e28f4a6cf75bacb0b26d#33fd51359d113c03b785e28f4a6cf75bacb0b26d"
dependencies = [
"ash",
"ash-window",
@@ -1543,7 +1544,7 @@ dependencies = [
[[package]]
name = "blade-macros"
version = "0.2.1"
source = "git+https://github.com/kvark/blade?rev=bdaf8c534fbbc9fbca71d1cf272f45640b3a068d#bdaf8c534fbbc9fbca71d1cf272f45640b3a068d"
source = "git+https://github.com/zed-industries/blade?rev=33fd51359d113c03b785e28f4a6cf75bacb0b26d#33fd51359d113c03b785e28f4a6cf75bacb0b26d"
dependencies = [
"proc-macro2",
"quote",
@@ -1553,7 +1554,7 @@ dependencies = [
[[package]]
name = "blade-util"
version = "0.1.0"
source = "git+https://github.com/kvark/blade?rev=bdaf8c534fbbc9fbca71d1cf272f45640b3a068d#bdaf8c534fbbc9fbca71d1cf272f45640b3a068d"
source = "git+https://github.com/zed-industries/blade?rev=33fd51359d113c03b785e28f4a6cf75bacb0b26d#33fd51359d113c03b785e28f4a6cf75bacb0b26d"
dependencies = [
"blade-graphics",
"bytemuck",
@@ -2150,7 +2151,6 @@ dependencies = [
"exec",
"fork",
"ipc-channel",
"libc",
"once_cell",
"plist",
"release_channel",
@@ -2211,11 +2211,14 @@ dependencies = [
"async-tungstenite",
"chrono",
"clock",
"cocoa",
"collections",
"feature_flags",
"fs",
"futures 0.3.28",
"gpui",
"http 0.1.0",
"isahc",
"lazy_static",
"log",
"once_cell",
@@ -2239,6 +2242,7 @@ dependencies = [
"tiny_http",
"url",
"util",
"windows 0.57.0",
]
[[package]]
@@ -2365,6 +2369,7 @@ dependencies = [
"prometheus",
"prost",
"rand 0.8.5",
"recent_projects",
"release_channel",
"reqwest",
"rpc",
@@ -2395,6 +2400,7 @@ dependencies = [
"util",
"uuid",
"workspace",
"worktree",
]
[[package]]
@@ -2454,13 +2460,6 @@ dependencies = [
"rustc-hash",
]
[[package]]
name = "color"
version = "0.1.0"
dependencies = [
"palette",
]
[[package]]
name = "color_quant"
version = "1.1.0"
@@ -3393,7 +3392,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
dependencies = [
"libloading 0.7.4",
"libloading 0.8.0",
]
[[package]]
@@ -3453,6 +3452,7 @@ version = "0.1.0"
dependencies = [
"aho-corasick",
"anyhow",
"assets",
"client",
"clock",
"collections",
@@ -3834,6 +3834,7 @@ dependencies = [
"fuzzy",
"gpui",
"language",
"num-format",
"picker",
"project",
"release_channel",
@@ -4049,6 +4050,15 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bad48618fdb549078c333a7a8528acb57af271d0433bdecd523eb620628364e"
[[package]]
name = "fluent-uri"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "flume"
version = "0.11.0"
@@ -4229,7 +4239,7 @@ dependencies = [
"text",
"time",
"util",
"windows 0.56.0",
"windows 0.57.0",
]
[[package]]
@@ -4549,7 +4559,7 @@ dependencies = [
"unindent",
"url",
"util",
"windows 0.56.0",
"windows 0.57.0",
]
[[package]]
@@ -4772,8 +4782,8 @@ dependencies = [
"wayland-cursor",
"wayland-protocols",
"wayland-protocols-plasma",
"windows 0.56.0",
"windows-core 0.56.0",
"windows 0.57.0",
"windows-core 0.57.0",
"x11rb",
"xim",
"xkbcommon",
@@ -4788,18 +4798,6 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "gray_matter"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7188a951c53316d94711b3d944c28cf79968685d295cbe782494e8811fc75554"
dependencies = [
"serde",
"serde_json",
"toml 0.5.11",
"yaml-rust",
]
[[package]]
name = "grid"
version = "0.13.0"
@@ -5079,6 +5077,18 @@ dependencies = [
"syn 2.0.59",
]
[[package]]
name = "html_to_markdown"
version = "0.1.0"
dependencies = [
"anyhow",
"html5ever",
"indoc",
"markup5ever_rcdom",
"pretty_assertions",
"regex",
]
[[package]]
name = "http"
version = "0.1.0"
@@ -5928,6 +5938,7 @@ version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326"
dependencies = [
"bindgen 0.64.0",
"cc",
"pkg-config",
"vcpkg",
@@ -5954,12 +5965,6 @@ dependencies = [
"safemem",
]
[[package]]
name = "linked-hash-map"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linkify"
version = "0.10.0"
@@ -6086,25 +6091,26 @@ dependencies = [
"log",
"lsp-types",
"parking_lot",
"pct-str",
"postage",
"release_channel",
"serde",
"serde_json",
"smol",
"util",
"windows 0.56.0",
"windows 0.57.0",
]
[[package]]
name = "lsp-types"
version = "0.95.1"
source = "git+https://github.com/zed-industries/lsp-types?branch=apply-snippet-edit#853c7881d200777e20799026651ca36727144646"
version = "0.97.0"
source = "git+https://github.com/zed-industries/lsp-types?branch=zed-main#258db672ceab9e66c6da3883d37c4dcf1094c6ac"
dependencies = [
"bitflags 1.3.2",
"fluent-uri",
"serde",
"serde_json",
"serde_repr",
"url",
]
[[package]]
@@ -6588,7 +6594,7 @@ dependencies = [
"tempfile",
"util",
"walkdir",
"windows 0.56.0",
"windows 0.57.0",
]
[[package]]
@@ -6742,6 +6748,16 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "num-format"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3"
dependencies = [
"arrayvec",
"itoa",
]
[[package]]
name = "num-integer"
version = "0.1.45"
@@ -6916,6 +6932,19 @@ dependencies = [
"cc",
]
[[package]]
name = "ollama"
version = "0.1.0"
dependencies = [
"anyhow",
"futures 0.3.28",
"http 0.1.0",
"isahc",
"schemars",
"serde",
"serde_json",
]
[[package]]
name = "once_cell"
version = "1.19.0"
@@ -7022,9 +7051,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-src"
version = "300.2.3+3.2.1"
version = "300.3.0+3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cff92b6f71555b61bb9315f7c64da3ca43d87531622120fea0195fc761b4843"
checksum = "eba8804a1c5765b18c4b3f907e6897ebabeedebc9830e1a0046c4a4cf44663e1"
dependencies = [
"cc",
]
@@ -7115,7 +7144,6 @@ dependencies = [
"project",
"rope",
"serde_json",
"settings",
"smol",
"theme",
"tree-sitter-rust",
@@ -7125,6 +7153,31 @@ dependencies = [
"workspace",
]
[[package]]
name = "outline_panel"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"db",
"editor",
"file_icons",
"futures 0.3.28",
"gpui",
"itertools 0.11.0",
"language",
"log",
"menu",
"project",
"schemars",
"serde",
"serde_json",
"settings",
"util",
"workspace",
"worktree",
]
[[package]]
name = "outref"
version = "0.5.1"
@@ -7283,6 +7336,16 @@ dependencies = [
"hmac 0.12.1",
]
[[package]]
name = "pct-str"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf1bdcc492c285a50bed60860dfa00b50baf1f60c73c7d6b435b01a2a11fd6ff"
dependencies = [
"thiserror",
"utf8-decode",
]
[[package]]
name = "peeking_take_while"
version = "0.1.2"
@@ -7801,6 +7864,7 @@ dependencies = [
"tempfile",
"terminal",
"text",
"unicase",
"unindent",
"util",
"which 6.0.0",
@@ -7831,9 +7895,9 @@ dependencies = [
"settings",
"theme",
"ui",
"unicase",
"util",
"workspace",
"worktree",
]
[[package]]
@@ -7926,6 +7990,17 @@ dependencies = [
"prost",
]
[[package]]
name = "proto"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"prost",
"prost-build",
"serde",
]
[[package]]
name = "protobuf"
version = "2.28.0"
@@ -8509,8 +8584,7 @@ dependencies = [
"futures 0.3.28",
"gpui",
"parking_lot",
"prost",
"prost-build",
"proto",
"rand 0.8.5",
"rsa 0.4.0",
"serde",
@@ -8636,15 +8710,27 @@ dependencies = [
]
[[package]]
name = "rustdoc_to_markdown"
name = "rustdoc"
version = "0.1.0"
dependencies = [
"anyhow",
"html5ever",
"async-trait",
"collections",
"derive_more",
"fs",
"futures 0.3.28",
"fuzzy",
"gpui",
"heed",
"html_to_markdown",
"http 0.1.0",
"indexmap 1.9.3",
"indoc",
"markup5ever_rcdom",
"parking_lot",
"pretty_assertions",
"regex",
"serde",
"strum",
"util",
]
[[package]]
@@ -10356,7 +10442,7 @@ dependencies = [
"theme",
"thiserror",
"util",
"windows 0.56.0",
"windows 0.57.0",
]
[[package]]
@@ -10425,7 +10511,6 @@ version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"color",
"derive_more",
"fs",
"futures 0.3.28",
@@ -11285,7 +11370,7 @@ dependencies = [
"story",
"strum",
"theme",
"windows 0.56.0",
"windows 0.57.0",
]
[[package]]
@@ -11441,6 +11526,12 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utf8-decode"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca61eb27fa339aa08826a29f03e87b99b4d8f0fc2255306fd266bb1b6a9de498"
[[package]]
name = "utf8parse"
version = "0.2.1"
@@ -12423,11 +12514,11 @@ dependencies = [
[[package]]
name = "windows"
version = "0.56.0"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132"
checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143"
dependencies = [
"windows-core 0.56.0",
"windows-core 0.57.0",
"windows-targets 0.52.5",
]
@@ -12442,9 +12533,9 @@ dependencies = [
[[package]]
name = "windows-core"
version = "0.56.0"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6"
checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
dependencies = [
"windows-implement",
"windows-interface",
@@ -12454,9 +12545,9 @@ dependencies = [
[[package]]
name = "windows-implement"
version = "0.56.0"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b"
checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
dependencies = [
"proc-macro2",
"quote",
@@ -12465,9 +12556,9 @@ dependencies = [
[[package]]
name = "windows-interface"
version = "0.56.0"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc"
checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
dependencies = [
"proc-macro2",
"quote",
@@ -12905,7 +12996,6 @@ name = "worktree"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"clock",
"collections",
"env_logger",
@@ -12917,10 +13007,8 @@ dependencies = [
"gpui",
"http 0.1.0",
"ignore",
"itertools 0.11.0",
"language",
"log",
"lsp",
"parking_lot",
"postage",
"pretty_assertions",
@@ -13079,15 +13167,6 @@ dependencies = [
"toml 0.8.10",
]
[[package]]
name = "yaml-rust"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
dependencies = [
"linked-hash-map",
]
[[package]]
name = "yansi"
version = "0.5.1"
@@ -13178,10 +13257,11 @@ dependencies = [
[[package]]
name = "zed"
version = "0.139.0"
version = "0.141.0"
dependencies = [
"activity_indicator",
"anyhow",
"ashpd",
"assets",
"assistant",
"audio",
@@ -13234,6 +13314,7 @@ dependencies = [
"node_runtime",
"notifications",
"outline",
"outline_panel",
"parking_lot",
"profiling",
"project",
@@ -13243,12 +13324,14 @@ dependencies = [
"recent_projects",
"release_channel",
"rope",
"rpc",
"search",
"serde",
"serde_json",
"settings",
"simplelog",
"smol",
"sqlez",
"supermaven",
"tab_switcher",
"task",
@@ -13257,6 +13340,7 @@ dependencies = [
"terminal_view",
"theme",
"theme_selector",
"tree-sitter-markdown",
"tree-sitter-rust",
"urlencoding",
"util",
@@ -13313,7 +13397,7 @@ dependencies = [
[[package]]
name = "zed_elixir"
version = "0.0.4"
version = "0.0.5"
dependencies = [
"zed_extension_api 0.0.6",
]
@@ -13412,7 +13496,7 @@ dependencies = [
[[package]]
name = "zed_php"
version = "0.0.5"
version = "0.0.6"
dependencies = [
"zed_extension_api 0.0.4",
]
@@ -13468,7 +13552,7 @@ dependencies = [
[[package]]
name = "zed_vue"
version = "0.0.2"
version = "0.0.3"
dependencies = [
"zed_extension_api 0.0.6",
]

View File

@@ -41,6 +41,7 @@ members = [
"crates/gpui",
"crates/gpui_macros",
"crates/headless",
"crates/html_to_markdown",
"crates/http",
"crates/image_viewer",
"crates/inline_completion_button",
@@ -60,13 +61,16 @@ members = [
"crates/multi_buffer",
"crates/node_runtime",
"crates/notifications",
"crates/ollama",
"crates/open_ai",
"crates/outline",
"crates/outline_panel",
"crates/picker",
"crates/prettier",
"crates/project",
"crates/project_panel",
"crates/project_symbols",
"crates/proto",
"crates/quick_action_bar",
"crates/recent_projects",
"crates/refineable",
@@ -76,7 +80,7 @@ members = [
"crates/rich_text",
"crates/rope",
"crates/rpc",
"crates/rustdoc_to_markdown",
"crates/rustdoc",
"crates/task",
"crates/tasks_ui",
"crates/search",
@@ -150,6 +154,7 @@ assets = { path = "crates/assets" }
assistant = { path = "crates/assistant" }
assistant_slash_command = { path = "crates/assistant_slash_command" }
assistant_tooling = { path = "crates/assistant_tooling" }
async-watch = "0.3.1"
audio = { path = "crates/audio" }
auto_update = { path = "crates/auto_update" }
base64 = "0.13"
@@ -162,10 +167,10 @@ clock = { path = "crates/clock" }
collab = { path = "crates/collab" }
collab_ui = { path = "crates/collab_ui" }
collections = { path = "crates/collections" }
color = { path = "crates/color" }
command_palette = { path = "crates/command_palette" }
command_palette_hooks = { path = "crates/command_palette_hooks" }
copilot = { path = "crates/copilot" }
dashmap = "5.5.3"
db = { path = "crates/db" }
diagnostics = { path = "crates/diagnostics" }
editor = { path = "crates/editor" }
@@ -185,6 +190,7 @@ google_ai = { path = "crates/google_ai" }
gpui = { path = "crates/gpui" }
gpui_macros = { path = "crates/gpui_macros" }
headless = { path = "crates/headless" }
html_to_markdown = { path = "crates/html_to_markdown" }
http = { path = "crates/http" }
install_cli = { path = "crates/install_cli" }
image_viewer = { path = "crates/image_viewer" }
@@ -204,13 +210,16 @@ menu = { path = "crates/menu" }
multi_buffer = { path = "crates/multi_buffer" }
node_runtime = { path = "crates/node_runtime" }
notifications = { path = "crates/notifications" }
ollama = { path = "crates/ollama" }
open_ai = { path = "crates/open_ai" }
outline = { path = "crates/outline" }
outline_panel = { path = "crates/outline_panel" }
picker = { path = "crates/picker" }
plugin = { path = "crates/plugin" }
plugin_macros = { path = "crates/plugin_macros" }
prettier = { path = "crates/prettier" }
project = { path = "crates/project" }
proto = { path = "crates/proto" }
worktree = { path = "crates/worktree" }
project_panel = { path = "crates/project_panel" }
project_symbols = { path = "crates/project_symbols" }
@@ -221,7 +230,7 @@ dev_server_projects = { path = "crates/dev_server_projects" }
rich_text = { path = "crates/rich_text" }
rope = { path = "crates/rope" }
rpc = { path = "crates/rpc" }
rustdoc_to_markdown = { path = "crates/rustdoc_to_markdown" }
rustdoc = { path = "crates/rustdoc" }
task = { path = "crates/task" }
tasks_ui = { path = "crates/tasks_ui" }
search = { path = "crates/search" }
@@ -265,14 +274,15 @@ async-tar = "0.4.2"
async-trait = "0.1"
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
bitflags = "2.4.2"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "bdaf8c534fbbc9fbca71d1cf272f45640b3a068d" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "bdaf8c534fbbc9fbca71d1cf272f45640b3a068d" }
blade-util = { git = "https://github.com/kvark/blade", rev = "bdaf8c534fbbc9fbca71d1cf272f45640b3a068d" }
blade-graphics = { git = "https://github.com/zed-industries/blade", rev = "33fd51359d113c03b785e28f4a6cf75bacb0b26d" }
blade-macros = { git = "https://github.com/zed-industries/blade", rev = "33fd51359d113c03b785e28f4a6cf75bacb0b26d" }
blade-util = { git = "https://github.com/zed-industries/blade", rev = "33fd51359d113c03b785e28f4a6cf75bacb0b26d" }
cap-std = "3.0"
cargo_toml = "0.20"
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4.4", features = ["derive"] }
clickhouse = { version = "0.11.6" }
cocoa = "0.25"
ctor = "0.2.6"
signal-hook = "0.3.17"
core-foundation = { version = "0.9.3" }
@@ -291,12 +301,10 @@ heed = { version = "0.20.1", features = ["read-txn-no-tls"] }
hex = "0.4.3"
html5ever = "0.27.0"
ignore = "0.4.22"
indexmap = { version = "1.6.2", features = ["serde"] }
indoc = "1"
# We explicitly disable http2 support in isahc.
isahc = { version = "1.7.2", default-features = false, features = [
"static-curl",
"text-decoding",
] }
isahc = { version = "1.7.2", default-features = false, features = [ "text-decoding" ] }
itertools = "0.11.0"
lazy_static = "1.4.0"
libc = "0.2"
@@ -305,6 +313,7 @@ log = { version = "0.4.16", features = ["kv_unstable_serde"] }
markup5ever_rcdom = "0.3.0"
nanoid = "0.4"
nix = "0.28"
num-format = "0.4.4"
once_cell = "1.19.0"
ordered-float = "2.1.1"
palette = { version = "0.7.5", default-features = false, features = ["std"] }
@@ -336,6 +345,7 @@ serde_repr = "0.1"
sha2 = "0.10"
shellexpand = "2.1.0"
shlex = "1.3.0"
similar = "1.3"
smallvec = { version = "1.6", features = ["union"] }
smol = "1.2"
strum = { version = "0.25.0", features = ["derive"] }
@@ -397,12 +407,13 @@ wit-component = "0.201"
sys-locale = "0.3.1"
[workspace.dependencies.windows]
version = "0.56.0"
version = "0.57"
features = [
"implement",
"Foundation_Numerics",
"System",
"System_Threading",
"UI_ViewManagement",
"Wdk_System_SystemServices",
"Win32_Globalization",
"Win32_Graphics_Direct2D",
@@ -494,10 +505,5 @@ non_canonical_partial_ord_impl = "allow"
reversed_empty_ranges = "allow"
type_complexity = "allow"
[workspace.lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = [
'cfg(gles)', # used in gpui
] }
[workspace.metadata.cargo-machete]
ignored = ["bindgen", "cbindgen", "prost_build", "serde"]

View File

@@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.2
FROM rust:1.78-bookworm as builder
FROM rust:1.79-bookworm as builder
WORKDIR app
COPY . .

6
assets/icons/context.svg Normal file
View File

@@ -0,0 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6667 2H3.33333C2.59695 2 2 2.59695 2 3.33333V12.6667C2 13.403 2.59695 14 3.33333 14H12.6667C13.403 14 14 13.403 14 12.6667V3.33333C14 2.59695 13.403 2 12.6667 2Z" stroke="#888888" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 5H5" stroke="#888888" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.5 8H5" stroke="#888888" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 10.9502H5" stroke="#888888" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 683 B

View File

@@ -74,6 +74,7 @@
"ib": "storage",
"ico": "image",
"ini": "settings",
"inl": "cpp",
"j2k": "image",
"java": "java",
"jfif": "image",

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-list-tree"><path d="M21 12h-8"/><path d="M21 6H8"/><path d="M21 18h-8"/><path d="M3 6v4c0 1.1.9 2 2 2h3"/><path d="M3 10v6c0 1.1.9 2 2 2h3"/></svg>

After

Width:  |  Height:  |  Size: 349 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rotate-cw"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>

After

Width:  |  Height:  |  Size: 303 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-text-quote"><path d="M17 6H3"/><path d="M21 12H8"/><path d="M21 18H8"/><path d="M3 12v6"/></svg>

After

Width:  |  Height:  |  Size: 299 B

1
assets/icons/sparkle.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-sparkle"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/></svg>

After

Width:  |  Height:  |  Size: 481 B

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.937 15.5C9.84772 15.1539 9.66734 14.8381 9.41462 14.5854C9.1619 14.3327 8.84607 14.1523 8.5 14.063L2.365 12.481C2.26033 12.4513 2.16821 12.3883 2.10261 12.3014C2.03702 12.2146 2.00153 12.1088 2.00153 12C2.00153 11.8912 2.03702 11.7854 2.10261 11.6986C2.16821 11.6118 2.26033 11.5487 2.365 11.519L8.5 9.93601C8.84595 9.84681 9.16169 9.66658 9.4144 9.41404C9.66711 9.16151 9.84757 8.84589 9.937 8.50001L11.519 2.36501C11.5484 2.25992 11.6114 2.16735 11.6983 2.1014C11.7853 2.03545 11.8914 1.99976 12.0005 1.99976C12.1096 1.99976 12.2157 2.03545 12.3027 2.1014C12.3896 2.16735 12.4526 2.25992 12.482 2.36501L14.063 8.50001C14.1523 8.84608 14.3327 9.1619 14.5854 9.41462C14.8381 9.66734 15.1539 9.84773 15.5 9.93701L21.635 11.518C21.7405 11.5471 21.8335 11.61 21.8998 11.6971C21.9661 11.7841 22.0021 11.8906 22.0021 12C22.0021 12.1094 21.9661 12.2159 21.8998 12.3029C21.8335 12.39 21.7405 12.4529 21.635 12.482L15.5 14.063C15.1539 14.1523 14.8381 14.3327 14.5854 14.5854C14.3327 14.8381 14.1523 15.1539 14.063 15.5L12.481 21.635C12.4516 21.7401 12.3886 21.8327 12.3017 21.8986C12.2147 21.9646 12.1086 22.0003 11.9995 22.0003C11.8904 22.0003 11.7843 21.9646 11.6973 21.8986C11.6104 21.8327 11.5474 21.7401 11.518 21.635L9.937 15.5Z" fill="black" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

1
assets/icons/star.svg Normal file
View File

@@ -0,0 +1 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6.97942 1.25171L6.9585 1.30199L5.58662 4.60039C5.54342 4.70426 5.44573 4.77523 5.3336 4.78422L1.7727 5.0697L1.71841 5.07405L1.38687 5.10063L1.08608 5.12475C0.820085 5.14607 0.712228 5.47802 0.914889 5.65162L1.14406 5.84793L1.39666 6.06431L1.43802 6.09974L4.15105 8.42374C4.23648 8.49692 4.2738 8.61176 4.24769 8.72118L3.41882 12.196L3.40618 12.249L3.32901 12.5725L3.25899 12.866C3.19708 13.1256 3.47945 13.3308 3.70718 13.1917L3.9647 13.0344L4.24854 12.861L4.29502 12.8326L7.34365 10.9705C7.43965 10.9119 7.5604 10.9119 7.6564 10.9705L10.705 12.8326L10.7515 12.861L11.0354 13.0344L11.2929 13.1917C11.5206 13.3308 11.803 13.1256 11.7411 12.866L11.671 12.5725L11.5939 12.249L11.5812 12.196L10.7524 8.72118C10.7263 8.61176 10.7636 8.49692 10.849 8.42374L13.562 6.09974L13.6034 6.06431L13.856 5.84793L14.0852 5.65162C14.2878 5.47802 14.18 5.14607 13.914 5.12475L13.6132 5.10063L13.2816 5.07405L13.2274 5.0697L9.66645 4.78422C9.55432 4.77523 9.45663 4.70426 9.41343 4.60039L8.04155 1.30199L8.02064 1.25171L7.89291 0.944609L7.77702 0.665992C7.67454 0.419604 7.32551 0.419604 7.22303 0.665992L7.10715 0.944609L6.97942 1.25171ZM7.50003 2.60397L6.50994 4.98442C6.32273 5.43453 5.89944 5.74207 5.41351 5.78103L2.84361 5.98705L4.8016 7.66428C5.17183 7.98142 5.33351 8.47903 5.2204 8.95321L4.62221 11.461L6.8224 10.1171C7.23842 9.86302 7.76164 9.86302 8.17766 10.1171L10.3778 11.461L9.77965 8.95321C9.66654 8.47903 9.82822 7.98142 10.1984 7.66428L12.1564 5.98705L9.58654 5.78103C9.10061 5.74207 8.67732 5.43453 8.49011 4.98442L7.50003 2.60397Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.22303 0.665992C7.32551 0.419604 7.67454 0.419604 7.77702 0.665992L9.41343 4.60039C9.45663 4.70426 9.55432 4.77523 9.66645 4.78422L13.914 5.12475C14.18 5.14607 14.2878 5.47802 14.0852 5.65162L10.849 8.42374C10.7636 8.49692 10.7263 8.61176 10.7524 8.72118L11.7411 12.866C11.803 13.1256 11.5206 13.3308 11.2929 13.1917L7.6564 10.9705C7.5604 10.9119 7.43965 10.9119 7.34365 10.9705L3.70718 13.1917C3.47945 13.3308 3.19708 13.1256 3.25899 12.866L4.24769 8.72118C4.2738 8.61176 4.23648 8.49692 4.15105 8.42374L0.914889 5.65162C0.712228 5.47802 0.820086 5.14607 1.08608 5.12475L5.3336 4.78422C5.44573 4.77523 5.54342 4.70426 5.58662 4.60039L7.22303 0.665992Z" fill="currentColor"></path></svg>

After

Width:  |  Height:  |  Size: 794 B

3
assets/icons/stop.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.88889 1H2.11111C1.49746 1 1 1.49746 1 2.11111V9.88889C1 10.5025 1.49746 11 2.11111 11H9.88889C10.5025 11 11 10.5025 11 9.88889V2.11111C11 1.49746 10.5025 1 9.88889 1Z" stroke="#C56757" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 369 B

View File

@@ -0,0 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 1.75L5.88467 5.14092C5.82759 5.31446 5.73055 5.47218 5.60136 5.60136C5.47218 5.73055 5.31446 5.82759 5.14092 5.88467L1.75 7L5.14092 8.11533C5.31446 8.17241 5.47218 8.26945 5.60136 8.39864C5.73055 8.52782 5.82759 8.68554 5.88467 8.85908L7 12.25L8.11533 8.85908C8.17241 8.68554 8.26945 8.52782 8.39864 8.39864C8.52782 8.26945 8.68554 8.17241 8.85908 8.11533L12.25 7L8.85908 5.88467C8.68554 5.82759 8.52782 5.73055 8.39864 5.60136C8.26945 5.47218 8.17241 5.31446 8.11533 5.14092L7 1.75Z" fill="black" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.91667 1.75V4.08333M1.75 2.91667H4.08333" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.0833 9.91667V12.25M9.91667 11.0833H12.25" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1017 B

View File

@@ -28,7 +28,6 @@
"ctrl-0": "zed::ResetBufferFontSize",
"ctrl-,": "zed::OpenSettings",
"ctrl-q": "zed::Quit",
"alt-f9": "zed::Hide",
"f11": "zed::ToggleFullScreen"
}
},
@@ -52,10 +51,11 @@
// "alt-h": "editor::DeleteToPreviousWordStart",
// "alt-d": "editor::DeleteToNextWordEnd",
"ctrl-x": "editor::Cut",
"ctrl-c": "editor::Copy",
"ctrl-insert": "editor::Copy",
"ctrl-v": "editor::Paste",
"ctrl-c": "editor::Copy",
"shift-insert": "editor::Paste",
"ctrl-v": "editor::Paste",
"ctrl-y": "editor::Redo",
"ctrl-z": "editor::Undo",
"ctrl-shift-z": "editor::Redo",
"up": "editor::MoveUp",
@@ -206,15 +206,10 @@
}
},
{
"context": "ConversationEditor > Editor",
"context": "PromptLibrary",
"bindings": {
"ctrl-enter": "assistant::Assist",
"ctrl-s": "workspace::Save",
"ctrl->": "assistant::QuoteSelection",
"shift-enter": "assistant::Split",
"ctrl-r": "assistant::CycleMessageRole",
"enter": "assistant::ConfirmCommand",
"alt-enter": "editor::Newline"
"ctrl-n": "prompt_library::NewPrompt",
"ctrl-shift-s": "prompt_library::ToggleDefaultPrompt"
}
},
{
@@ -226,7 +221,8 @@
"shift-enter": "search::SelectPrevMatch",
"alt-enter": "search::SelectAllMatches",
"ctrl-f": "search::FocusSearch",
"ctrl-h": "search::ToggleReplace"
"ctrl-h": "search::ToggleReplace",
"ctrl-l": "search::ToggleSelection"
}
},
{
@@ -290,6 +286,7 @@
"ctrl-alt-g": "search::SelectNextMatch",
"ctrl-alt-shift-g": "search::SelectPrevMatch",
"ctrl-alt-shift-h": "search::ToggleReplace",
"ctrl-alt-shift-l": "search::ToggleSelection",
"alt-enter": "search::SelectAllMatches",
"alt-c": "search::ToggleCaseSensitive",
"alt-w": "search::ToggleWholeWord",
@@ -423,7 +420,7 @@
"alt-8": ["workspace::ActivatePane", 7],
"alt-9": ["workspace::ActivatePane", 8],
"ctrl-alt-b": "workspace::ToggleLeftDock",
"ctrl-b": "workspace::ToggleRightDock",
"ctrl-b": "workspace::ToggleLeftDock",
"ctrl-j": "workspace::ToggleBottomDock",
"ctrl-alt-y": "workspace::CloseAllDocks",
"ctrl-shift-f": "pane::DeploySearch",
@@ -443,6 +440,7 @@
"ctrl-shift-p": "command_palette::Toggle",
"ctrl-shift-m": "diagnostics::Deploy",
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-b": "outline_panel::ToggleFocus",
"ctrl-?": "assistant::ToggleFocus",
"ctrl-alt-s": "workspace::SaveAll",
"ctrl-k m": "language_selector::Toggle",
@@ -548,12 +546,37 @@
"ctrl-enter": "assistant::InlineAssist"
}
},
{
"context": "ContextEditor > Editor",
"bindings": {
"ctrl-enter": "assistant::Assist",
"ctrl-s": "workspace::Save",
"ctrl->": "assistant::QuoteSelection",
"shift-enter": "assistant::Split",
"ctrl-r": "assistant::CycleMessageRole",
"enter": "assistant::ConfirmCommand",
"alt-enter": "editor::Newline"
}
},
{
"context": "ProjectSearchBar && !in_replace",
"bindings": {
"ctrl-enter": "project_search::SearchInNew"
}
},
{
"context": "OutlinePanel",
"bindings": {
"left": "outline_panel::CollapseSelectedEntry",
"right": "outline_panel::ExpandSelectedEntry",
"ctrl-alt-c": "outline_panel::CopyPath",
"alt-ctrl-shift-c": "outline_panel::CopyRelativePath",
"alt-ctrl-r": "outline_panel::RevealInFinder",
"space": "outline_panel::Open",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrev"
}
},
{
"context": "ProjectPanel",
"bindings": {
@@ -575,7 +598,10 @@
"ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
"alt-ctrl-r": "project_panel::RevealInFinder",
"alt-shift-f": "project_panel::NewSearchInDirectory"
"alt-shift-f": "project_panel::NewSearchInDirectory",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrev",
"escape": "menu::Cancel"
}
},
{
@@ -636,12 +662,7 @@
"pagedown": ["terminal::SendKeystroke", "pagedown"],
"escape": ["terminal::SendKeystroke", "escape"],
"enter": ["terminal::SendKeystroke", "enter"],
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
// Some nice conveniences
"ctrl-backspace": ["terminal::SendText", "\u0015"],
"ctrl-right": ["terminal::SendText", "\u0005"],
"ctrl-left": ["terminal::SendText", "\u0001"]
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"]
}
}
]

View File

@@ -176,6 +176,12 @@
"replace_enabled": true
}
],
"cmd-alt-l": [
"buffer_search::Deploy",
{
"selection_search_enabled": true
}
],
"cmd-e": [
"buffer_search::Deploy",
{
@@ -222,7 +228,7 @@
}
},
{
"context": "ConversationEditor > Editor",
"context": "ContextEditor > Editor",
"bindings": {
"cmd-enter": "assistant::Assist",
"cmd-s": "workspace::Save",
@@ -233,6 +239,14 @@
"alt-enter": "editor::Newline"
}
},
{
"context": "PromptLibrary",
"bindings": {
"cmd-n": "prompt_library::NewPrompt",
"cmd-shift-s": "prompt_library::ToggleDefaultPrompt",
"cmd-w": "workspace::CloseWindow"
}
},
{
"context": "BufferSearchBar",
"bindings": {
@@ -242,7 +256,8 @@
"shift-enter": "search::SelectPrevMatch",
"alt-enter": "search::SelectAllMatches",
"cmd-f": "search::FocusSearch",
"cmd-alt-f": "search::ToggleReplace"
"cmd-alt-f": "search::ToggleReplace",
"cmd-alt-l": "search::ToggleSelection"
}
},
{
@@ -308,6 +323,7 @@
"cmd-g": "search::SelectNextMatch",
"cmd-shift-g": "search::SelectPrevMatch",
"cmd-shift-h": "search::ToggleReplace",
"cmd-alt-l": "search::ToggleSelection",
"alt-enter": "search::SelectAllMatches",
"alt-cmd-c": "search::ToggleCaseSensitive",
"alt-cmd-w": "search::ToggleWholeWord",
@@ -459,6 +475,7 @@
"cmd-shift-p": "command_palette::Toggle",
"cmd-shift-m": "diagnostics::Deploy",
"cmd-shift-e": "project_panel::ToggleFocus",
"cmd-shift-b": "outline_panel::ToggleFocus",
"cmd-?": "assistant::ToggleFocus",
"cmd-alt-s": "workspace::SaveAll",
"cmd-k m": "language_selector::Toggle",
@@ -568,6 +585,19 @@
"cmd-enter": "project_search::SearchInNew"
}
},
{
"context": "OutlinePanel",
"bindings": {
"left": "outline_panel::CollapseSelectedEntry",
"right": "outline_panel::ExpandSelectedEntry",
"cmd-alt-c": "outline_panel::CopyPath",
"alt-cmd-shift-c": "outline_panel::CopyRelativePath",
"alt-cmd-r": "outline_panel::RevealInFinder",
"space": "outline_panel::Open",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrev"
}
},
{
"context": "ProjectPanel",
"bindings": {
@@ -639,7 +669,7 @@
{
"context": "Picker",
"bindings": {
"alt-e": "picker::UseSelectedQuery",
"f2": "picker::UseSelectedQuery",
"alt-enter": ["picker::ConfirmInput", { "secondary": false }],
"cmd-alt-enter": ["picker::ConfirmInput", { "secondary": true }]
}

View File

@@ -80,6 +80,7 @@
"g shift-e": ["vim::PreviousWordEnd", { "ignorePunctuation": true }],
"/": "vim::Search",
"g /": "pane::DeploySearch",
"?": [
"vim::Search",
{
@@ -245,9 +246,10 @@
"displayLines": true
}
],
"g v": "vim::RestoreVisualSelection",
"g ]": "editor::GoToDiagnostic",
"g [": "editor::GoToPrevDiagnostic",
"g i": ["workspace::SendKeystrokes", "` ^ i"],
"g i": "vim::InsertAtPrevious",
"g ,": "vim::ChangeListNewer",
"g ;": "vim::ChangeListOlder",
"shift-h": "vim::WindowTop",
@@ -379,8 +381,12 @@
"r": ["vim::PushOperator", "Replace"],
"s": "vim::Substitute",
"shift-s": "vim::SubstituteLine",
"> >": "vim::Indent",
"< <": "vim::Outdent",
">": ["vim::PushOperator", "Indent"],
"<": ["vim::PushOperator", "Outdent"],
"g u": ["vim::PushOperator", "Lowercase"],
"g shift-u": ["vim::PushOperator", "Uppercase"],
"g ~": ["vim::PushOperator", "OppositeCase"],
"\"": ["vim::PushOperator", "Register"],
"ctrl-pagedown": "pane::ActivateNextItem",
"ctrl-pageup": "pane::ActivatePrevItem",
// tree-sitter related commands
@@ -395,6 +401,7 @@
{
"context": "Editor && vim_mode == visual && vim_operator == none && !VimWaiting",
"bindings": {
"\"": ["vim::PushOperator", "Register"],
// tree-sitter related commands
"[ x": "editor::SelectLargerSyntaxNode",
"] x": "editor::SelectSmallerSyntaxNode"
@@ -430,6 +437,27 @@
"d": "vim::CurrentLine"
}
},
{
"context": "Editor && vim_operator == gu",
"bindings": {
"g u": "vim::CurrentLine",
"u": "vim::CurrentLine"
}
},
{
"context": "Editor && vim_operator == gU",
"bindings": {
"g shift-u": "vim::CurrentLine",
"shift-u": "vim::CurrentLine"
}
},
{
"context": "Editor && vim_operator == g~",
"bindings": {
"g ~": "vim::CurrentLine",
"~": "vim::CurrentLine"
}
},
{
"context": "Editor && vim_mode == normal && vim_operator == d",
"bindings": {
@@ -459,6 +487,18 @@
"s": "vim::CurrentLine"
}
},
{
"context": "Editor && vim_operator == >",
"bindings": {
">": "vim::CurrentLine"
}
},
{
"context": "Editor && vim_operator == <",
"bindings": {
"<": "vim::CurrentLine"
}
},
{
"context": "Editor && VimObject",
"bindings": {
@@ -568,7 +608,7 @@
}
},
{
"context": "Editor && vim_mode == normal",
"context": "Editor && vim_mode == normal && !VimWaiting",
"bindings": {
"g c c": "editor::ToggleComments"
}

View File

@@ -119,10 +119,10 @@
// The debounce delay before re-querying the language server for completion
// documentation when not included in original completion list.
"completion_documentation_secondary_query_debounce": 300,
// Whether to show wrap guides in the editor. Setting this to true will
// show a guide at the 'preferred_line_length' value if 'soft_wrap' 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.
"show_wrap_guides": true,
// Character counts at which to show wrap guides in the editor.
"wrap_guides": [],
@@ -131,14 +131,7 @@
// The default number of lines to expand excerpts in the multibuffer by.
"expand_excerpt_lines": 3,
// Globs to match against file paths to determine if a file is private.
"private_files": [
"**/.env*",
"**/*.pem",
"**/*.key",
"**/*.cert",
"**/*.crt",
"**/secrets.yml"
],
"private_files": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"],
// Whether to use additional LSP queries to format (and amend) the code after
// every "trigger" symbol input, defined by LSP server capabilities.
"use_on_type_format": true,
@@ -164,6 +157,12 @@
// "none"
// 3. Draw all invisible symbols:
// "all"
// 4. Draw whitespaces at boundaries only:
// "boundaries"
// For a whitespace to be on a boundary, any of the following conditions need to be met:
// - It is a tab
// - It is adjacent to an edge (start or end)
// - It is adjacent to a whitespace (left or right)
"show_whitespaces": "selection",
// Settings related to calls in Zed
"calls": {
@@ -296,6 +295,29 @@
/// when a directory has only one directory inside.
"auto_fold_dirs": false
},
"outline_panel": {
// Whether to show the outline panel button in the status bar
"button": true,
// Default width of the outline panel.
"default_width": 300,
// Where to dock the outline panel. Can be 'left' or 'right'.
"dock": "left",
// Whether to show file icons in the outline panel.
"file_icons": true,
// Whether to show folder icons or chevrons for directories in the outline panel.
"folder_icons": true,
// Whether to show the git status in the outline panel.
"git_status": true,
// Amount of indentation for nested items.
"indent_size": 20,
// Whether to reveal it in the outline panel automatically,
// when a corresponding outline entry becomes active.
// Gitignored entries are never auto revealed.
"auto_reveal_entries": true,
/// Whether to fold directories automatically
/// when a directory has only one directory inside.
"auto_fold_dirs": true
},
"collaboration_panel": {
// Whether to show the collaboration panel button in the status bar.
"button": true,
@@ -355,6 +377,9 @@
"show_call_status_icon": true,
// Whether to use language servers to provide code intelligence.
"enable_language_server": true,
// Whether to perform linked edits of associated ranges, if the language server supports it.
// For example, when editing opening <html> tag, the contents of the closing </html> tag will be edited as well.
"linked_edits": true,
// The list of language servers to use (or disable) for all languages.
//
// This is typically customized on a per-language basis.
@@ -453,7 +478,8 @@
// Send anonymized usage data like what languages you're using Zed with.
"metrics": true
},
// Automatically update Zed
// Automatically update Zed. This setting may be ignored on Linux if
// installed through a package manager.
"auto_update": true,
// Diagnostics configuration.
"diagnostics": {
@@ -672,9 +698,6 @@
"Elixir": {
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
},
"Gleam": {
"tab_size": 2
},
"Go": {
"code_actions_on_format": {
"source.organizeImports": true
@@ -700,6 +723,7 @@
}
},
"JavaScript": {
"language_servers": ["typescript-language-server", "!vtsls", "..."],
"prettier": {
"allowed": true
}
@@ -709,9 +733,6 @@
"allowed": true
}
},
"Make": {
"hard_tabs": true
},
"Markdown": {
"format_on_save": "off",
"prettier": {
@@ -724,9 +745,6 @@
"plugins": ["@prettier/plugin-php"]
}
},
"Prisma": {
"tab_size": 2
},
"Ruby": {
"language_servers": ["solargraph", "!ruby-lsp", "..."]
},
@@ -748,6 +766,7 @@
}
},
"TSX": {
"language_servers": ["typescript-language-server", "!vtsls", "..."],
"prettier": {
"allowed": true
}
@@ -758,6 +777,7 @@
}
},
"TypeScript": {
"language_servers": ["typescript-language-server", "!vtsls", "..."],
"prettier": {
"allowed": true
}

View File

@@ -285,10 +285,10 @@ impl ActivityIndicator {
icon: None,
message: "Click to restart and update Zed".to_string(),
on_click: Some(Arc::new({
let restart = workspace::Restart {
let reload = workspace::Reload {
binary_path: Some(binary_path.clone()),
};
move |_, cx| workspace::restart(&restart, cx)
move |_, cx| workspace::reload(&reload, cx)
})),
},
AutoUpdateStatus::Errored => Content {

View File

@@ -22,31 +22,34 @@ client.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
editor.workspace = true
feature_flags.workspace = true
file_icons.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
heed.workspace = true
html_to_markdown.workspace = true
http.workspace = true
indoc.workspace = true
language.workspace = true
log.workspace = true
menu.workspace = true
multi_buffer.workspace = true
ollama = { workspace = true, features = ["schemars"] }
open_ai = { workspace = true, features = ["schemars"] }
ordered-float.workspace = true
parking_lot.workspace = true
project.workspace = true
regex.workspace = true
rope.workspace = true
rustdoc_to_markdown.workspace = true
rustdoc.workspace = true
schemars.workspace = true
search.workspace = true
semantic_index.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
similar.workspace = true
smol.workspace = true
strsim = "0.11"
strum.workspace = true
@@ -59,7 +62,6 @@ util.workspace = true
uuid.workspace = true
workspace.workspace = true
picker.workspace = true
gray_matter = "0.2.7"
[dev-dependencies]
ctor.workspace = true

View File

@@ -1,30 +1,39 @@
pub mod assistant_panel;
pub mod assistant_settings;
mod codegen;
mod completion_provider;
mod context_store;
mod inline_assistant;
mod model_selector;
mod prompt_library;
mod prompts;
mod saved_conversation;
mod search;
mod slash_command;
mod streaming_diff;
pub use assistant_panel::AssistantPanel;
use assistant_settings::{AnthropicModel, AssistantSettings, OpenAiModel, ZedDotDevModel};
use assistant_settings::{AnthropicModel, AssistantSettings, CloudModel, OllamaModel, OpenAiModel};
use assistant_slash_command::SlashCommandRegistry;
use client::{proto, Client};
use command_palette_hooks::CommandPaletteFilter;
pub(crate) use completion_provider::*;
pub(crate) use context_store::*;
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
pub(crate) use inline_assistant::*;
pub(crate) use model_selector::*;
pub(crate) use saved_conversation::*;
use rustdoc::RustdocStore;
use semantic_index::{CloudEmbeddingProvider, SemanticIndex};
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
use slash_command::{
active_command, default_command, diagnostics_command, fetch_command, file_command, now_command,
project_command, prompt_command, rustdoc_command, search_command, tabs_command,
};
use std::{
fmt::{self, Display},
sync::Arc,
};
pub(crate) use streaming_diff::*;
use util::paths::EMBEDDINGS_DIR;
actions!(
@@ -80,14 +89,15 @@ impl Display for Role {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum LanguageModel {
ZedDotDev(ZedDotDevModel),
Cloud(CloudModel),
OpenAi(OpenAiModel),
Anthropic(AnthropicModel),
Ollama(OllamaModel),
}
impl Default for LanguageModel {
fn default() -> Self {
LanguageModel::ZedDotDev(ZedDotDevModel::default())
LanguageModel::Cloud(CloudModel::default())
}
}
@@ -96,7 +106,8 @@ impl LanguageModel {
match self {
LanguageModel::OpenAi(model) => format!("openai/{}", model.id()),
LanguageModel::Anthropic(model) => format!("anthropic/{}", model.id()),
LanguageModel::ZedDotDev(model) => format!("zed.dev/{}", model.id()),
LanguageModel::Cloud(model) => format!("zed.dev/{}", model.id()),
LanguageModel::Ollama(model) => format!("ollama/{}", model.id()),
}
}
@@ -104,7 +115,8 @@ impl LanguageModel {
match self {
LanguageModel::OpenAi(model) => model.display_name().into(),
LanguageModel::Anthropic(model) => model.display_name().into(),
LanguageModel::ZedDotDev(model) => model.display_name().into(),
LanguageModel::Cloud(model) => model.display_name().into(),
LanguageModel::Ollama(model) => model.display_name().into(),
}
}
@@ -112,7 +124,8 @@ impl LanguageModel {
match self {
LanguageModel::OpenAi(model) => model.max_token_count(),
LanguageModel::Anthropic(model) => model.max_token_count(),
LanguageModel::ZedDotDev(model) => model.max_token_count(),
LanguageModel::Cloud(model) => model.max_token_count(),
LanguageModel::Ollama(model) => model.max_token_count(),
}
}
@@ -120,7 +133,8 @@ impl LanguageModel {
match self {
LanguageModel::OpenAi(model) => model.id(),
LanguageModel::Anthropic(model) => model.id(),
LanguageModel::ZedDotDev(model) => model.id(),
LanguageModel::Cloud(model) => model.id(),
LanguageModel::Ollama(model) => model.id(),
}
}
}
@@ -165,6 +179,21 @@ impl LanguageModelRequest {
tools: Vec::new(),
}
}
/// Before we send the request to the server, we can perform fixups on it appropriate to the model.
pub fn preprocess(&mut self) {
match &self.model {
LanguageModel::OpenAi(_) => {}
LanguageModel::Anthropic(_) => {}
LanguageModel::Ollama(_) => {}
LanguageModel::Cloud(model) => match model {
CloudModel::Claude3Opus | CloudModel::Claude3Sonnet | CloudModel::Claude3Haiku => {
preprocess_anthropic_request(self);
}
_ => {}
},
}
}
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
@@ -251,9 +280,14 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
}
})
.detach();
completion_provider::init(client, cx);
prompt_library::init(cx);
completion_provider::init(client.clone(), cx);
assistant_slash_command::init(cx);
register_slash_commands(cx);
assistant_panel::init(cx);
inline_assistant::init(client.telemetry().clone(), cx);
RustdocStore::init_global(cx);
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_namespace(Assistant::NAMESPACE);
@@ -266,13 +300,27 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
cx.observe_global::<SettingsStore>(|cx| {
Assistant::update_global(cx, |assistant, cx| {
let settings = AssistantSettings::get_global(cx);
assistant.set_enabled(settings.enabled, cx);
});
})
.detach();
}
fn register_slash_commands(cx: &mut AppContext) {
let slash_command_registry = SlashCommandRegistry::global(cx);
slash_command_registry.register_command(file_command::FileSlashCommand, true);
slash_command_registry.register_command(active_command::ActiveSlashCommand, true);
slash_command_registry.register_command(tabs_command::TabsSlashCommand, true);
slash_command_registry.register_command(project_command::ProjectSlashCommand, true);
slash_command_registry.register_command(search_command::SearchSlashCommand, true);
slash_command_registry.register_command(prompt_command::PromptSlashCommand, true);
slash_command_registry.register_command(default_command::DefaultSlashCommand, true);
slash_command_registry.register_command(now_command::NowSlashCommand, true);
slash_command_registry.register_command(diagnostics_command::DiagnosticsCommand, true);
slash_command_registry.register_command(rustdoc_command::RustdocSlashCommand, false);
slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
}
#[cfg(test)]
#[ctor::ctor]
fn init_logger() {

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@ use std::fmt;
pub use anthropic::Model as AnthropicModel;
use gpui::Pixels;
pub use ollama::Model as OllamaModel;
pub use open_ai::Model as OpenAiModel;
use schemars::{
schema::{InstanceType, Metadata, Schema, SchemaObject},
@@ -14,10 +15,10 @@ use serde::{
use settings::{Settings, SettingsSources};
use strum::{EnumIter, IntoEnumIterator};
use crate::LanguageModel;
use crate::{preprocess_anthropic_request, LanguageModel, LanguageModelRequest};
#[derive(Clone, Debug, Default, PartialEq, EnumIter)]
pub enum ZedDotDevModel {
pub enum CloudModel {
Gpt3Point5Turbo,
Gpt4,
Gpt4Turbo,
@@ -29,7 +30,7 @@ pub enum ZedDotDevModel {
Custom(String),
}
impl Serialize for ZedDotDevModel {
impl Serialize for CloudModel {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
@@ -38,7 +39,7 @@ impl Serialize for ZedDotDevModel {
}
}
impl<'de> Deserialize<'de> for ZedDotDevModel {
impl<'de> Deserialize<'de> for CloudModel {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
@@ -46,7 +47,7 @@ impl<'de> Deserialize<'de> for ZedDotDevModel {
struct ZedDotDevModelVisitor;
impl<'de> Visitor<'de> for ZedDotDevModelVisitor {
type Value = ZedDotDevModel;
type Value = CloudModel;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a string for a ZedDotDevModel variant or a custom model")
@@ -56,9 +57,9 @@ impl<'de> Deserialize<'de> for ZedDotDevModel {
where
E: de::Error,
{
let model = ZedDotDevModel::iter()
let model = CloudModel::iter()
.find(|model| model.id() == value)
.unwrap_or_else(|| ZedDotDevModel::Custom(value.to_string()));
.unwrap_or_else(|| CloudModel::Custom(value.to_string()));
Ok(model)
}
}
@@ -67,13 +68,13 @@ impl<'de> Deserialize<'de> for ZedDotDevModel {
}
}
impl JsonSchema for ZedDotDevModel {
impl JsonSchema for CloudModel {
fn schema_name() -> String {
"ZedDotDevModel".to_owned()
}
fn json_schema(_generator: &mut schemars::gen::SchemaGenerator) -> Schema {
let variants = ZedDotDevModel::iter()
let variants = CloudModel::iter()
.filter_map(|model| {
let id = model.id();
if id.is_empty() {
@@ -88,7 +89,7 @@ impl JsonSchema for ZedDotDevModel {
enum_values: Some(variants.iter().map(|s| s.clone().into()).collect()),
metadata: Some(Box::new(Metadata {
title: Some("ZedDotDevModel".to_owned()),
default: Some(ZedDotDevModel::default().id().into()),
default: Some(CloudModel::default().id().into()),
examples: variants.into_iter().map(Into::into).collect(),
..Default::default()
})),
@@ -97,7 +98,7 @@ impl JsonSchema for ZedDotDevModel {
}
}
impl ZedDotDevModel {
impl CloudModel {
pub fn id(&self) -> &str {
match self {
Self::Gpt3Point5Turbo => "gpt-3.5-turbo",
@@ -133,6 +134,15 @@ impl ZedDotDevModel {
Self::Custom(_) => 4096, // TODO: Make this configurable
}
}
pub fn preprocess_request(&self, request: &mut LanguageModelRequest) {
match self {
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3Haiku => {
preprocess_anthropic_request(request)
}
_ => {}
}
}
}
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
@@ -147,7 +157,7 @@ pub enum AssistantDockPosition {
#[derive(Debug, PartialEq)]
pub enum AssistantProvider {
ZedDotDev {
model: ZedDotDevModel,
model: CloudModel,
},
OpenAi {
model: OpenAiModel,
@@ -159,6 +169,11 @@ pub enum AssistantProvider {
api_url: String,
low_speed_timeout_in_seconds: Option<u64>,
},
Ollama {
model: OllamaModel,
api_url: String,
low_speed_timeout_in_seconds: Option<u64>,
},
}
impl Default for AssistantProvider {
@@ -175,9 +190,7 @@ impl Default for AssistantProvider {
#[serde(tag = "name", rename_all = "snake_case")]
pub enum AssistantProviderContent {
#[serde(rename = "zed.dev")]
ZedDotDev {
default_model: Option<ZedDotDevModel>,
},
ZedDotDev { default_model: Option<CloudModel> },
#[serde(rename = "openai")]
OpenAi {
default_model: Option<OpenAiModel>,
@@ -190,6 +203,12 @@ pub enum AssistantProviderContent {
api_url: Option<String>,
low_speed_timeout_in_seconds: Option<u64>,
},
#[serde(rename = "ollama")]
Ollama {
default_model: Option<OllamaModel>,
api_url: Option<String>,
low_speed_timeout_in_seconds: Option<u64>,
},
}
#[derive(Debug, Default)]
@@ -281,7 +300,7 @@ impl AssistantSettingsContent {
Some(AssistantProviderContent::ZedDotDev {
default_model: model,
}) => {
if let LanguageModel::ZedDotDev(new_model) = new_model {
if let LanguageModel::Cloud(new_model) = new_model {
*model = Some(new_model);
}
}
@@ -302,7 +321,7 @@ impl AssistantSettingsContent {
}
}
provider => match new_model {
LanguageModel::ZedDotDev(model) => {
LanguageModel::Cloud(model) => {
*provider = Some(AssistantProviderContent::ZedDotDev {
default_model: Some(model),
})
@@ -321,6 +340,13 @@ impl AssistantSettingsContent {
low_speed_timeout_in_seconds: None,
})
}
LanguageModel::Ollama(model) => {
*provider = Some(AssistantProviderContent::Ollama {
default_model: Some(model),
api_url: None,
low_speed_timeout_in_seconds: None,
})
}
},
},
},
@@ -465,6 +491,27 @@ impl Settings for AssistantSettings {
Some(low_speed_timeout_in_seconds_override);
}
}
(
AssistantProvider::Ollama {
model,
api_url,
low_speed_timeout_in_seconds,
},
AssistantProviderContent::Ollama {
default_model: model_override,
api_url: api_url_override,
low_speed_timeout_in_seconds: low_speed_timeout_in_seconds_override,
},
) => {
merge(model, model_override);
merge(api_url, api_url_override);
if let Some(low_speed_timeout_in_seconds_override) =
low_speed_timeout_in_seconds_override
{
*low_speed_timeout_in_seconds =
Some(low_speed_timeout_in_seconds_override);
}
}
(
AssistantProvider::Anthropic {
model,
@@ -512,6 +559,15 @@ impl Settings for AssistantSettings {
.unwrap_or_else(|| anthropic::ANTHROPIC_API_URL.into()),
low_speed_timeout_in_seconds,
},
AssistantProviderContent::Ollama {
default_model: model,
api_url,
low_speed_timeout_in_seconds,
} => AssistantProvider::Ollama {
model: model.unwrap_or_default(),
api_url: api_url.unwrap_or_else(|| ollama::OLLAMA_API_URL.into()),
low_speed_timeout_in_seconds,
},
};
}
}
@@ -613,7 +669,7 @@ mod tests {
assert_eq!(
AssistantSettings::get_global(cx).provider,
AssistantProvider::ZedDotDev {
model: ZedDotDevModel::Custom("custom".into())
model: CloudModel::Custom("custom".into())
}
);
}

View File

@@ -1,696 +0,0 @@
use crate::{
streaming_diff::{Hunk, StreamingDiff},
CompletionProvider, LanguageModelRequest,
};
use anyhow::Result;
use client::telemetry::Telemetry;
use editor::{Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
use gpui::{EventEmitter, Model, ModelContext, Task};
use language::{Rope, TransactionId};
use multi_buffer::MultiBufferRow;
use std::{cmp, future, ops::Range, sync::Arc, time::Instant};
pub enum Event {
Finished,
Undone,
}
#[derive(Clone)]
pub enum CodegenKind {
Transform { range: Range<Anchor> },
Generate { position: Anchor },
}
pub struct Codegen {
buffer: Model<MultiBuffer>,
snapshot: MultiBufferSnapshot,
kind: CodegenKind,
last_equal_ranges: Vec<Range<Anchor>>,
transaction_id: Option<TransactionId>,
error: Option<anyhow::Error>,
generation: Task<()>,
idle: bool,
telemetry: Option<Arc<Telemetry>>,
_subscription: gpui::Subscription,
}
impl EventEmitter<Event> for Codegen {}
impl Codegen {
pub fn new(
buffer: Model<MultiBuffer>,
kind: CodegenKind,
telemetry: Option<Arc<Telemetry>>,
cx: &mut ModelContext<Self>,
) -> Self {
let snapshot = buffer.read(cx).snapshot(cx);
Self {
buffer: buffer.clone(),
snapshot,
kind,
last_equal_ranges: Default::default(),
transaction_id: Default::default(),
error: Default::default(),
idle: true,
generation: Task::ready(()),
telemetry,
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
}
}
fn handle_buffer_event(
&mut self,
_buffer: Model<MultiBuffer>,
event: &multi_buffer::Event,
cx: &mut ModelContext<Self>,
) {
if let multi_buffer::Event::TransactionUndone { transaction_id } = event {
if self.transaction_id == Some(*transaction_id) {
self.transaction_id = None;
self.generation = Task::ready(());
cx.emit(Event::Undone);
}
}
}
pub fn range(&self) -> Range<Anchor> {
match &self.kind {
CodegenKind::Transform { range } => range.clone(),
CodegenKind::Generate { position } => position.bias_left(&self.snapshot)..*position,
}
}
pub fn kind(&self) -> &CodegenKind {
&self.kind
}
pub fn last_equal_ranges(&self) -> &[Range<Anchor>] {
&self.last_equal_ranges
}
pub fn idle(&self) -> bool {
self.idle
}
pub fn error(&self) -> Option<&anyhow::Error> {
self.error.as_ref()
}
pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut ModelContext<Self>) {
let range = self.range();
let snapshot = self.snapshot.clone();
let selected_text = snapshot
.text_for_range(range.start..range.end)
.collect::<Rope>();
let selection_start = range.start.to_point(&snapshot);
let suggested_line_indent = snapshot
.suggested_indents(selection_start.row..selection_start.row + 1, cx)
.into_values()
.next()
.unwrap_or_else(|| snapshot.indent_size_for_line(MultiBufferRow(selection_start.row)));
let model_telemetry_id = prompt.model.telemetry_id();
let response = CompletionProvider::global(cx).complete(prompt);
let telemetry = self.telemetry.clone();
self.generation = cx.spawn(|this, mut cx| {
async move {
let generate = async {
let mut edit_start = range.start.to_offset(&snapshot);
let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
let diff = cx.background_executor().spawn(async move {
let mut response_latency = None;
let request_start = Instant::now();
let diff = async {
let chunks = strip_invalid_spans_from_codeblock(response.await?);
futures::pin_mut!(chunks);
let mut diff = StreamingDiff::new(selected_text.to_string());
let mut new_text = String::new();
let mut base_indent = None;
let mut line_indent = None;
let mut first_line = true;
while let Some(chunk) = chunks.next().await {
if response_latency.is_none() {
response_latency = Some(request_start.elapsed());
}
let chunk = chunk?;
let mut lines = chunk.split('\n').peekable();
while let Some(line) = lines.next() {
new_text.push_str(line);
if line_indent.is_none() {
if let Some(non_whitespace_ch_ix) =
new_text.find(|ch: char| !ch.is_whitespace())
{
line_indent = Some(non_whitespace_ch_ix);
base_indent = base_indent.or(line_indent);
let line_indent = line_indent.unwrap();
let base_indent = base_indent.unwrap();
let indent_delta =
line_indent as i32 - base_indent as i32;
let mut corrected_indent_len = cmp::max(
0,
suggested_line_indent.len as i32 + indent_delta,
)
as usize;
if first_line {
corrected_indent_len = corrected_indent_len
.saturating_sub(
selection_start.column as usize,
);
}
let indent_char = suggested_line_indent.char();
let mut indent_buffer = [0; 4];
let indent_str =
indent_char.encode_utf8(&mut indent_buffer);
new_text.replace_range(
..line_indent,
&indent_str.repeat(corrected_indent_len),
);
}
}
if line_indent.is_some() {
hunks_tx.send(diff.push_new(&new_text)).await?;
new_text.clear();
}
if lines.peek().is_some() {
hunks_tx.send(diff.push_new("\n")).await?;
line_indent = None;
first_line = false;
}
}
}
hunks_tx.send(diff.push_new(&new_text)).await?;
hunks_tx.send(diff.finish()).await?;
anyhow::Ok(())
};
let error_message = diff.await.err().map(|error| error.to_string());
if let Some(telemetry) = telemetry {
telemetry.report_assistant_event(
None,
telemetry_events::AssistantKind::Inline,
model_telemetry_id,
response_latency,
error_message,
);
}
});
while let Some(hunks) = hunks_rx.next().await {
this.update(&mut cx, |this, cx| {
this.last_equal_ranges.clear();
let transaction = this.buffer.update(cx, |buffer, cx| {
// Avoid grouping assistant edits with user edits.
buffer.finalize_last_transaction(cx);
buffer.start_transaction(cx);
buffer.edit(
hunks.into_iter().filter_map(|hunk| match hunk {
Hunk::Insert { text } => {
let edit_start = snapshot.anchor_after(edit_start);
Some((edit_start..edit_start, text))
}
Hunk::Remove { len } => {
let edit_end = edit_start + len;
let edit_range = snapshot.anchor_after(edit_start)
..snapshot.anchor_before(edit_end);
edit_start = edit_end;
Some((edit_range, String::new()))
}
Hunk::Keep { len } => {
let edit_end = edit_start + len;
let edit_range = snapshot.anchor_after(edit_start)
..snapshot.anchor_before(edit_end);
edit_start = edit_end;
this.last_equal_ranges.push(edit_range);
None
}
}),
None,
cx,
);
buffer.end_transaction(cx)
});
if let Some(transaction) = transaction {
if let Some(first_transaction) = this.transaction_id {
// Group all assistant edits into the first transaction.
this.buffer.update(cx, |buffer, cx| {
buffer.merge_transactions(
transaction,
first_transaction,
cx,
)
});
} else {
this.transaction_id = Some(transaction);
this.buffer.update(cx, |buffer, cx| {
buffer.finalize_last_transaction(cx)
});
}
}
cx.notify();
})?;
}
diff.await;
anyhow::Ok(())
};
let result = generate.await;
this.update(&mut cx, |this, cx| {
this.last_equal_ranges.clear();
this.idle = true;
if let Err(error) = result {
this.error = Some(error);
}
cx.emit(Event::Finished);
cx.notify();
})
.ok();
}
});
self.error.take();
self.idle = false;
cx.notify();
}
pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
if let Some(transaction_id) = self.transaction_id {
self.buffer
.update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx));
}
}
}
fn strip_invalid_spans_from_codeblock(
stream: impl Stream<Item = Result<String>>,
) -> impl Stream<Item = Result<String>> {
let mut first_line = true;
let mut buffer = String::new();
let mut starts_with_markdown_codeblock = false;
let mut includes_start_or_end_span = false;
stream.filter_map(move |chunk| {
let chunk = match chunk {
Ok(chunk) => chunk,
Err(err) => return future::ready(Some(Err(err))),
};
buffer.push_str(&chunk);
if buffer.len() > "<|S|".len() && buffer.starts_with("<|S|") {
includes_start_or_end_span = true;
buffer = buffer
.strip_prefix("<|S|>")
.or_else(|| buffer.strip_prefix("<|S|"))
.unwrap_or(&buffer)
.to_string();
} else if buffer.ends_with("|E|>") {
includes_start_or_end_span = true;
} else if buffer.starts_with("<|")
|| buffer.starts_with("<|S")
|| buffer.starts_with("<|S|")
|| buffer.ends_with('|')
|| buffer.ends_with("|E")
|| buffer.ends_with("|E|")
{
return future::ready(None);
}
if first_line {
if buffer.is_empty() || buffer == "`" || buffer == "``" {
return future::ready(None);
} else if buffer.starts_with("```") {
starts_with_markdown_codeblock = true;
if let Some(newline_ix) = buffer.find('\n') {
buffer.replace_range(..newline_ix + 1, "");
first_line = false;
} else {
return future::ready(None);
}
}
}
let mut text = buffer.to_string();
if starts_with_markdown_codeblock {
text = text
.strip_suffix("\n```\n")
.or_else(|| text.strip_suffix("\n```"))
.or_else(|| text.strip_suffix("\n``"))
.or_else(|| text.strip_suffix("\n`"))
.or_else(|| text.strip_suffix('\n'))
.unwrap_or(&text)
.to_string();
}
if includes_start_or_end_span {
text = text
.strip_suffix("|E|>")
.or_else(|| text.strip_suffix("E|>"))
.or_else(|| text.strip_prefix("|>"))
.or_else(|| text.strip_prefix('>'))
.unwrap_or(&text)
.to_string();
};
if text.contains('\n') {
first_line = false;
}
let remainder = buffer.split_off(text.len());
let result = if buffer.is_empty() {
None
} else {
Some(Ok(buffer.clone()))
};
buffer = remainder;
future::ready(result)
})
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use crate::FakeCompletionProvider;
use super::*;
use futures::stream::{self};
use gpui::{Context, TestAppContext};
use indoc::indoc;
use language::{
language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, LanguageMatcher,
Point,
};
use rand::prelude::*;
use serde::Serialize;
use settings::SettingsStore;
#[derive(Serialize)]
pub struct DummyCompletionRequest {
pub name: String,
}
#[gpui::test(iterations = 10)]
async fn test_transform_autoindent(cx: &mut TestAppContext, mut rng: StdRng) {
let provider = FakeCompletionProvider::default();
cx.set_global(cx.update(SettingsStore::test));
cx.set_global(CompletionProvider::Fake(provider.clone()));
cx.update(language_settings::init);
let text = indoc! {"
fn main() {
let x = 0;
for _ in 0..10 {
x += 1;
}
}
"};
let buffer =
cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let range = buffer.read_with(cx, |buffer, cx| {
let snapshot = buffer.snapshot(cx);
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
});
let codegen = cx.new_model(|cx| {
Codegen::new(buffer.clone(), CodegenKind::Transform { range }, None, cx)
});
let request = LanguageModelRequest::default();
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
let mut new_text = concat!(
" let mut x = 0;\n",
" while x < 10 {\n",
" x += 1;\n",
" }",
);
while !new_text.is_empty() {
let max_len = cmp::min(new_text.len(), 10);
let len = rng.gen_range(1..=max_len);
let (chunk, suffix) = new_text.split_at(len);
provider.send_completion(chunk.into());
new_text = suffix;
cx.background_executor.run_until_parked();
}
provider.finish_completion();
cx.background_executor.run_until_parked();
assert_eq!(
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
indoc! {"
fn main() {
let mut x = 0;
while x < 10 {
x += 1;
}
}
"}
);
}
#[gpui::test(iterations = 10)]
async fn test_autoindent_when_generating_past_indentation(
cx: &mut TestAppContext,
mut rng: StdRng,
) {
let provider = FakeCompletionProvider::default();
cx.set_global(CompletionProvider::Fake(provider.clone()));
cx.set_global(cx.update(SettingsStore::test));
cx.update(language_settings::init);
let text = indoc! {"
fn main() {
le
}
"};
let buffer =
cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let position = buffer.read_with(cx, |buffer, cx| {
let snapshot = buffer.snapshot(cx);
snapshot.anchor_before(Point::new(1, 6))
});
let codegen = cx.new_model(|cx| {
Codegen::new(buffer.clone(), CodegenKind::Generate { position }, None, cx)
});
let request = LanguageModelRequest::default();
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
let mut new_text = concat!(
"t mut x = 0;\n",
"while x < 10 {\n",
" x += 1;\n",
"}", //
);
while !new_text.is_empty() {
let max_len = cmp::min(new_text.len(), 10);
let len = rng.gen_range(1..=max_len);
let (chunk, suffix) = new_text.split_at(len);
provider.send_completion(chunk.into());
new_text = suffix;
cx.background_executor.run_until_parked();
}
provider.finish_completion();
cx.background_executor.run_until_parked();
assert_eq!(
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
indoc! {"
fn main() {
let mut x = 0;
while x < 10 {
x += 1;
}
}
"}
);
}
#[gpui::test(iterations = 10)]
async fn test_autoindent_when_generating_before_indentation(
cx: &mut TestAppContext,
mut rng: StdRng,
) {
let provider = FakeCompletionProvider::default();
cx.set_global(CompletionProvider::Fake(provider.clone()));
cx.set_global(cx.update(SettingsStore::test));
cx.update(language_settings::init);
let text = concat!(
"fn main() {\n",
" \n",
"}\n" //
);
let buffer =
cx.new_model(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let position = buffer.read_with(cx, |buffer, cx| {
let snapshot = buffer.snapshot(cx);
snapshot.anchor_before(Point::new(1, 2))
});
let codegen = cx.new_model(|cx| {
Codegen::new(buffer.clone(), CodegenKind::Generate { position }, None, cx)
});
let request = LanguageModelRequest::default();
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
let mut new_text = concat!(
"let mut x = 0;\n",
"while x < 10 {\n",
" x += 1;\n",
"}", //
);
while !new_text.is_empty() {
let max_len = cmp::min(new_text.len(), 10);
let len = rng.gen_range(1..=max_len);
let (chunk, suffix) = new_text.split_at(len);
provider.send_completion(chunk.into());
new_text = suffix;
cx.background_executor.run_until_parked();
}
provider.finish_completion();
cx.background_executor.run_until_parked();
assert_eq!(
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
indoc! {"
fn main() {
let mut x = 0;
while x < 10 {
x += 1;
}
}
"}
);
}
#[gpui::test]
async fn test_strip_invalid_spans_from_codeblock() {
assert_eq!(
strip_invalid_spans_from_codeblock(chunks("Lorem ipsum dolor", 2))
.map(|chunk| chunk.unwrap())
.collect::<String>()
.await,
"Lorem ipsum dolor"
);
assert_eq!(
strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor", 2))
.map(|chunk| chunk.unwrap())
.collect::<String>()
.await,
"Lorem ipsum dolor"
);
assert_eq!(
strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor\n```", 2))
.map(|chunk| chunk.unwrap())
.collect::<String>()
.await,
"Lorem ipsum dolor"
);
assert_eq!(
strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor\n```\n", 2))
.map(|chunk| chunk.unwrap())
.collect::<String>()
.await,
"Lorem ipsum dolor"
);
assert_eq!(
strip_invalid_spans_from_codeblock(chunks(
"```html\n```js\nLorem ipsum dolor\n```\n```",
2
))
.map(|chunk| chunk.unwrap())
.collect::<String>()
.await,
"```js\nLorem ipsum dolor\n```"
);
assert_eq!(
strip_invalid_spans_from_codeblock(chunks("``\nLorem ipsum dolor\n```", 2))
.map(|chunk| chunk.unwrap())
.collect::<String>()
.await,
"``\nLorem ipsum dolor\n```"
);
assert_eq!(
strip_invalid_spans_from_codeblock(chunks("<|S|Lorem ipsum|E|>", 2))
.map(|chunk| chunk.unwrap())
.collect::<String>()
.await,
"Lorem ipsum"
);
assert_eq!(
strip_invalid_spans_from_codeblock(chunks("<|S|>Lorem ipsum", 2))
.map(|chunk| chunk.unwrap())
.collect::<String>()
.await,
"Lorem ipsum"
);
assert_eq!(
strip_invalid_spans_from_codeblock(chunks("```\n<|S|>Lorem ipsum\n```", 2))
.map(|chunk| chunk.unwrap())
.collect::<String>()
.await,
"Lorem ipsum"
);
assert_eq!(
strip_invalid_spans_from_codeblock(chunks("```\n<|S|Lorem ipsum|E|>\n```", 2))
.map(|chunk| chunk.unwrap())
.collect::<String>()
.await,
"Lorem ipsum"
);
fn chunks(text: &str, size: usize) -> impl Stream<Item = Result<String>> {
stream::iter(
text.chars()
.collect::<Vec<_>>()
.chunks(size)
.map(|chunk| Ok(chunk.iter().collect::<String>()))
.collect::<Vec<_>>(),
)
}
}
fn rust_lang() -> Language {
Language::new(
LanguageConfig {
name: "Rust".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_rust::language()),
)
.with_indents_query(
r#"
(call_expression) @indent
(field_expression) @indent
(_ "(" ")" @end) @indent
(_ "{" "}" @end) @indent
"#,
)
.unwrap()
}
}

View File

@@ -1,14 +1,16 @@
mod anthropic;
mod cloud;
#[cfg(test)]
mod fake;
mod ollama;
mod open_ai;
mod zed;
pub use anthropic::*;
pub use cloud::*;
#[cfg(test)]
pub use fake::*;
pub use ollama::*;
pub use open_ai::*;
pub use zed::*;
use crate::{
assistant_settings::{AssistantProvider, AssistantSettings},
@@ -25,8 +27,8 @@ use std::time::Duration;
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
let mut settings_version = 0;
let provider = match &AssistantSettings::get_global(cx).provider {
AssistantProvider::ZedDotDev { model } => CompletionProvider::ZedDotDev(
ZedDotDevCompletionProvider::new(model.clone(), client.clone(), settings_version, cx),
AssistantProvider::ZedDotDev { model } => CompletionProvider::Cloud(
CloudCompletionProvider::new(model.clone(), client.clone(), settings_version, cx),
),
AssistantProvider::OpenAi {
model,
@@ -50,6 +52,18 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
)),
AssistantProvider::Ollama {
model,
api_url,
low_speed_timeout_in_seconds,
} => CompletionProvider::Ollama(OllamaCompletionProvider::new(
model.clone(),
api_url.clone(),
client.http_client(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
cx,
)),
};
cx.set_global(provider);
@@ -87,14 +101,29 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
settings_version,
);
}
(
CompletionProvider::ZedDotDev(provider),
AssistantProvider::ZedDotDev { model },
CompletionProvider::Ollama(provider),
AssistantProvider::Ollama {
model,
api_url,
low_speed_timeout_in_seconds,
},
) => {
provider.update(
model.clone(),
api_url.clone(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
cx,
);
}
(CompletionProvider::Cloud(provider), AssistantProvider::ZedDotDev { model }) => {
provider.update(model.clone(), settings_version);
}
(_, AssistantProvider::ZedDotDev { model }) => {
*provider = CompletionProvider::ZedDotDev(ZedDotDevCompletionProvider::new(
*provider = CompletionProvider::Cloud(CloudCompletionProvider::new(
model.clone(),
client.clone(),
settings_version,
@@ -133,6 +162,23 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
settings_version,
));
}
(
_,
AssistantProvider::Ollama {
model,
api_url,
low_speed_timeout_in_seconds,
},
) => {
*provider = CompletionProvider::Ollama(OllamaCompletionProvider::new(
model.clone(),
api_url.clone(),
client.http_client(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
cx,
));
}
}
})
})
@@ -142,9 +188,10 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
pub enum CompletionProvider {
OpenAi(OpenAiCompletionProvider),
Anthropic(AnthropicCompletionProvider),
ZedDotDev(ZedDotDevCompletionProvider),
Cloud(CloudCompletionProvider),
#[cfg(test)]
Fake(FakeCompletionProvider),
Ollama(OllamaCompletionProvider),
}
impl gpui::Global for CompletionProvider {}
@@ -164,9 +211,13 @@ impl CompletionProvider {
.available_models()
.map(LanguageModel::Anthropic)
.collect(),
CompletionProvider::ZedDotDev(provider) => provider
CompletionProvider::Cloud(provider) => provider
.available_models()
.map(LanguageModel::ZedDotDev)
.map(LanguageModel::Cloud)
.collect(),
CompletionProvider::Ollama(provider) => provider
.available_models()
.map(|model| LanguageModel::Ollama(model.clone()))
.collect(),
#[cfg(test)]
CompletionProvider::Fake(_) => unimplemented!(),
@@ -177,7 +228,8 @@ impl CompletionProvider {
match self {
CompletionProvider::OpenAi(provider) => provider.settings_version(),
CompletionProvider::Anthropic(provider) => provider.settings_version(),
CompletionProvider::ZedDotDev(provider) => provider.settings_version(),
CompletionProvider::Cloud(provider) => provider.settings_version(),
CompletionProvider::Ollama(provider) => provider.settings_version(),
#[cfg(test)]
CompletionProvider::Fake(_) => unimplemented!(),
}
@@ -187,7 +239,8 @@ impl CompletionProvider {
match self {
CompletionProvider::OpenAi(provider) => provider.is_authenticated(),
CompletionProvider::Anthropic(provider) => provider.is_authenticated(),
CompletionProvider::ZedDotDev(provider) => provider.is_authenticated(),
CompletionProvider::Cloud(provider) => provider.is_authenticated(),
CompletionProvider::Ollama(provider) => provider.is_authenticated(),
#[cfg(test)]
CompletionProvider::Fake(_) => true,
}
@@ -197,7 +250,8 @@ impl CompletionProvider {
match self {
CompletionProvider::OpenAi(provider) => provider.authenticate(cx),
CompletionProvider::Anthropic(provider) => provider.authenticate(cx),
CompletionProvider::ZedDotDev(provider) => provider.authenticate(cx),
CompletionProvider::Cloud(provider) => provider.authenticate(cx),
CompletionProvider::Ollama(provider) => provider.authenticate(cx),
#[cfg(test)]
CompletionProvider::Fake(_) => Task::ready(Ok(())),
}
@@ -207,7 +261,8 @@ impl CompletionProvider {
match self {
CompletionProvider::OpenAi(provider) => provider.authentication_prompt(cx),
CompletionProvider::Anthropic(provider) => provider.authentication_prompt(cx),
CompletionProvider::ZedDotDev(provider) => provider.authentication_prompt(cx),
CompletionProvider::Cloud(provider) => provider.authentication_prompt(cx),
CompletionProvider::Ollama(provider) => provider.authentication_prompt(cx),
#[cfg(test)]
CompletionProvider::Fake(_) => unimplemented!(),
}
@@ -217,7 +272,8 @@ impl CompletionProvider {
match self {
CompletionProvider::OpenAi(provider) => provider.reset_credentials(cx),
CompletionProvider::Anthropic(provider) => provider.reset_credentials(cx),
CompletionProvider::ZedDotDev(_) => Task::ready(Ok(())),
CompletionProvider::Cloud(_) => Task::ready(Ok(())),
CompletionProvider::Ollama(provider) => provider.reset_credentials(cx),
#[cfg(test)]
CompletionProvider::Fake(_) => Task::ready(Ok(())),
}
@@ -227,7 +283,8 @@ impl CompletionProvider {
match self {
CompletionProvider::OpenAi(provider) => LanguageModel::OpenAi(provider.model()),
CompletionProvider::Anthropic(provider) => LanguageModel::Anthropic(provider.model()),
CompletionProvider::ZedDotDev(provider) => LanguageModel::ZedDotDev(provider.model()),
CompletionProvider::Cloud(provider) => LanguageModel::Cloud(provider.model()),
CompletionProvider::Ollama(provider) => LanguageModel::Ollama(provider.model()),
#[cfg(test)]
CompletionProvider::Fake(_) => LanguageModel::default(),
}
@@ -241,7 +298,8 @@ impl CompletionProvider {
match self {
CompletionProvider::OpenAi(provider) => provider.count_tokens(request, cx),
CompletionProvider::Anthropic(provider) => provider.count_tokens(request, cx),
CompletionProvider::ZedDotDev(provider) => provider.count_tokens(request, cx),
CompletionProvider::Cloud(provider) => provider.count_tokens(request, cx),
CompletionProvider::Ollama(provider) => provider.count_tokens(request, cx),
#[cfg(test)]
CompletionProvider::Fake(_) => futures::FutureExt::boxed(futures::future::ready(Ok(0))),
}
@@ -254,7 +312,8 @@ impl CompletionProvider {
match self {
CompletionProvider::OpenAi(provider) => provider.complete(request),
CompletionProvider::Anthropic(provider) => provider.complete(request),
CompletionProvider::ZedDotDev(provider) => provider.complete(request),
CompletionProvider::Cloud(provider) => provider.complete(request),
CompletionProvider::Ollama(provider) => provider.complete(request),
#[cfg(test)]
CompletionProvider::Fake(provider) => provider.complete(),
}

View File

@@ -1,9 +1,9 @@
use crate::count_open_ai_tokens;
use crate::{
assistant_settings::AnthropicModel, CompletionProvider, LanguageModel, LanguageModelRequest,
Role,
};
use anthropic::{stream_completion, Request, RequestMessage, Role as AnthropicRole};
use crate::{count_open_ai_tokens, LanguageModelRequestMessage};
use anthropic::{stream_completion, Request, RequestMessage};
use anyhow::{anyhow, Result};
use editor::{Editor, EditorElement, EditorStyle};
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
@@ -167,53 +167,37 @@ impl AnthropicCompletionProvider {
.boxed()
}
fn to_anthropic_request(&self, request: LanguageModelRequest) -> Request {
fn to_anthropic_request(&self, mut request: LanguageModelRequest) -> Request {
preprocess_anthropic_request(&mut request);
let model = match request.model {
LanguageModel::Anthropic(model) => model,
_ => self.model(),
};
let mut system_message = String::new();
let mut messages: Vec<RequestMessage> = Vec::new();
for message in request.messages {
if message.content.is_empty() {
continue;
}
match message.role {
Role::User | Role::Assistant => {
let role = match message.role {
Role::User => AnthropicRole::User,
Role::Assistant => AnthropicRole::Assistant,
_ => unreachable!(),
};
if let Some(last_message) = messages.last_mut() {
if last_message.role == role {
last_message.content.push_str("\n\n");
last_message.content.push_str(&message.content);
continue;
}
}
messages.push(RequestMessage {
role,
content: message.content,
});
}
Role::System => {
if !system_message.is_empty() {
system_message.push_str("\n\n");
}
system_message.push_str(&message.content);
}
}
if request
.messages
.first()
.map_or(false, |message| message.role == Role::System)
{
system_message = request.messages.remove(0).content;
}
Request {
model,
messages,
messages: request
.messages
.iter()
.map(|msg| RequestMessage {
role: match msg.role {
Role::User => anthropic::Role::User,
Role::Assistant => anthropic::Role::Assistant,
Role::System => unreachable!("filtered out by preprocess_request"),
},
content: msg.content.clone(),
})
.collect(),
stream: true,
system: system_message,
max_tokens: 4092,
@@ -221,6 +205,49 @@ impl AnthropicCompletionProvider {
}
}
pub fn preprocess_anthropic_request(request: &mut LanguageModelRequest) {
let mut new_messages: Vec<LanguageModelRequestMessage> = Vec::new();
let mut system_message = String::new();
for message in request.messages.drain(..) {
if message.content.is_empty() {
continue;
}
match message.role {
Role::User | Role::Assistant => {
if let Some(last_message) = new_messages.last_mut() {
if last_message.role == message.role {
last_message.content.push_str("\n\n");
last_message.content.push_str(&message.content);
continue;
}
}
new_messages.push(message);
}
Role::System => {
if !system_message.is_empty() {
system_message.push_str("\n\n");
}
system_message.push_str(&message.content);
}
}
}
if !system_message.is_empty() {
request.messages.insert(
0,
LanguageModelRequestMessage {
role: Role::System,
content: system_message,
},
);
}
request.messages = new_messages;
}
struct AuthenticationPrompt {
api_key: View<Editor>,
api_url: String,
@@ -322,7 +349,7 @@ impl Render for AuthenticationPrompt {
h_flex()
.gap_2()
.child(Label::new("Click on").size(LabelSize::Small))
.child(Icon::new(IconName::Ai).size(IconSize::XSmall))
.child(Icon::new(IconName::ZedAssistant).size(IconSize::XSmall))
.child(
Label::new("in the status bar to close this panel.").size(LabelSize::Small),
),

View File

@@ -1,5 +1,5 @@
use crate::{
assistant_settings::ZedDotDevModel, count_open_ai_tokens, CompletionProvider, LanguageModel,
assistant_settings::CloudModel, count_open_ai_tokens, CompletionProvider, LanguageModel,
LanguageModelRequest,
};
use anyhow::{anyhow, Result};
@@ -10,17 +10,17 @@ use std::{future, sync::Arc};
use strum::IntoEnumIterator;
use ui::prelude::*;
pub struct ZedDotDevCompletionProvider {
pub struct CloudCompletionProvider {
client: Arc<Client>,
model: ZedDotDevModel,
model: CloudModel,
settings_version: usize,
status: client::Status,
_maintain_client_status: Task<()>,
}
impl ZedDotDevCompletionProvider {
impl CloudCompletionProvider {
pub fn new(
model: ZedDotDevModel,
model: CloudModel,
client: Arc<Client>,
settings_version: usize,
cx: &mut AppContext,
@@ -30,7 +30,7 @@ impl ZedDotDevCompletionProvider {
let maintain_client_status = cx.spawn(|mut cx| async move {
while let Some(status) = status_rx.next().await {
let _ = cx.update_global::<CompletionProvider, _>(|provider, _cx| {
if let CompletionProvider::ZedDotDev(provider) = provider {
if let CompletionProvider::Cloud(provider) = provider {
provider.status = status;
} else {
unreachable!()
@@ -47,20 +47,20 @@ impl ZedDotDevCompletionProvider {
}
}
pub fn update(&mut self, model: ZedDotDevModel, settings_version: usize) {
pub fn update(&mut self, model: CloudModel, settings_version: usize) {
self.model = model;
self.settings_version = settings_version;
}
pub fn available_models(&self) -> impl Iterator<Item = ZedDotDevModel> {
let mut custom_model = if let ZedDotDevModel::Custom(custom_model) = self.model.clone() {
pub fn available_models(&self) -> impl Iterator<Item = CloudModel> {
let mut custom_model = if let CloudModel::Custom(custom_model) = self.model.clone() {
Some(custom_model)
} else {
None
};
ZedDotDevModel::iter().filter_map(move |model| {
if let ZedDotDevModel::Custom(_) = model {
Some(ZedDotDevModel::Custom(custom_model.take()?))
CloudModel::iter().filter_map(move |model| {
if let CloudModel::Custom(_) = model {
Some(CloudModel::Custom(custom_model.take()?))
} else {
Some(model)
}
@@ -71,7 +71,7 @@ impl ZedDotDevCompletionProvider {
self.settings_version
}
pub fn model(&self) -> ZedDotDevModel {
pub fn model(&self) -> CloudModel {
self.model.clone()
}
@@ -94,21 +94,19 @@ impl ZedDotDevCompletionProvider {
cx: &AppContext,
) -> BoxFuture<'static, Result<usize>> {
match request.model {
LanguageModel::ZedDotDev(ZedDotDevModel::Gpt4)
| LanguageModel::ZedDotDev(ZedDotDevModel::Gpt4Turbo)
| LanguageModel::ZedDotDev(ZedDotDevModel::Gpt4Omni)
| LanguageModel::ZedDotDev(ZedDotDevModel::Gpt3Point5Turbo) => {
LanguageModel::Cloud(CloudModel::Gpt4)
| LanguageModel::Cloud(CloudModel::Gpt4Turbo)
| LanguageModel::Cloud(CloudModel::Gpt4Omni)
| LanguageModel::Cloud(CloudModel::Gpt3Point5Turbo) => {
count_open_ai_tokens(request, cx.background_executor())
}
LanguageModel::ZedDotDev(
ZedDotDevModel::Claude3Opus
| ZedDotDevModel::Claude3Sonnet
| ZedDotDevModel::Claude3Haiku,
LanguageModel::Cloud(
CloudModel::Claude3Opus | CloudModel::Claude3Sonnet | CloudModel::Claude3Haiku,
) => {
// Can't find a tokenizer for Claude 3, so for now just use the same as OpenAI's as an approximation.
count_open_ai_tokens(request, cx.background_executor())
}
LanguageModel::ZedDotDev(ZedDotDevModel::Custom(model)) => {
LanguageModel::Cloud(CloudModel::Custom(model)) => {
let request = self.client.request(proto::CountTokensWithLanguageModel {
model,
messages: request
@@ -129,8 +127,10 @@ impl ZedDotDevCompletionProvider {
pub fn complete(
&self,
request: LanguageModelRequest,
mut request: LanguageModelRequest,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
request.preprocess();
let request = proto::CompleteWithLanguageModel {
model: request.model.id().to_string(),
messages: request

View File

@@ -0,0 +1,348 @@
use crate::{
assistant_settings::OllamaModel, CompletionProvider, LanguageModel, LanguageModelRequest, Role,
};
use anyhow::Result;
use futures::StreamExt as _;
use futures::{future::BoxFuture, stream::BoxStream, FutureExt};
use gpui::{AnyView, AppContext, Task};
use http::HttpClient;
use ollama::{
get_models, preload_model, stream_chat_completion, ChatMessage, ChatOptions, ChatRequest,
Role as OllamaRole,
};
use std::sync::Arc;
use std::time::Duration;
use ui::{prelude::*, ButtonLike, ElevationIndex};
const OLLAMA_DOWNLOAD_URL: &str = "https://ollama.com/download";
const OLLAMA_LIBRARY_URL: &str = "https://ollama.com/library";
pub struct OllamaCompletionProvider {
api_url: String,
model: OllamaModel,
http_client: Arc<dyn HttpClient>,
low_speed_timeout: Option<Duration>,
settings_version: usize,
available_models: Vec<OllamaModel>,
}
impl OllamaCompletionProvider {
pub fn new(
model: OllamaModel,
api_url: String,
http_client: Arc<dyn HttpClient>,
low_speed_timeout: Option<Duration>,
settings_version: usize,
cx: &AppContext,
) -> Self {
cx.spawn({
let api_url = api_url.clone();
let client = http_client.clone();
let model = model.name.clone();
|_| async move {
if model.is_empty() {
return Ok(());
}
preload_model(client.as_ref(), &api_url, &model).await
}
})
.detach_and_log_err(cx);
Self {
api_url,
model,
http_client,
low_speed_timeout,
settings_version,
available_models: Default::default(),
}
}
pub fn update(
&mut self,
model: OllamaModel,
api_url: String,
low_speed_timeout: Option<Duration>,
settings_version: usize,
cx: &AppContext,
) {
cx.spawn({
let api_url = api_url.clone();
let client = self.http_client.clone();
let model = model.name.clone();
|_| async move { preload_model(client.as_ref(), &api_url, &model).await }
})
.detach_and_log_err(cx);
if model.name.is_empty() {
self.select_first_available_model()
} else {
self.model = model;
}
self.api_url = api_url;
self.low_speed_timeout = low_speed_timeout;
self.settings_version = settings_version;
}
pub fn available_models(&self) -> impl Iterator<Item = &OllamaModel> {
self.available_models.iter()
}
pub fn select_first_available_model(&mut self) {
if let Some(model) = self.available_models.first() {
self.model = model.clone();
}
}
pub fn settings_version(&self) -> usize {
self.settings_version
}
pub fn is_authenticated(&self) -> bool {
!self.available_models.is_empty()
}
pub fn authenticate(&self, cx: &AppContext) -> Task<Result<()>> {
if self.is_authenticated() {
Task::ready(Ok(()))
} else {
self.fetch_models(cx)
}
}
pub fn reset_credentials(&self, cx: &AppContext) -> Task<Result<()>> {
self.fetch_models(cx)
}
pub fn fetch_models(&self, cx: &AppContext) -> Task<Result<()>> {
let http_client = self.http_client.clone();
let api_url = self.api_url.clone();
// As a proxy for the server being "authenticated", we'll check if its up by fetching the models
cx.spawn(|mut cx| async move {
let models = get_models(http_client.as_ref(), &api_url, None).await?;
let mut models: Vec<OllamaModel> = models
.into_iter()
// Since there is no metadata from the Ollama API
// indicating which models are embedding models,
// simply filter out models with "-embed" in their name
.filter(|model| !model.name.contains("-embed"))
.map(|model| OllamaModel::new(&model.name))
.collect();
models.sort_by(|a, b| a.name.cmp(&b.name));
cx.update_global::<CompletionProvider, _>(|provider, _cx| {
if let CompletionProvider::Ollama(provider) = provider {
provider.available_models = models;
if !provider.available_models.is_empty() && provider.model.name.is_empty() {
provider.select_first_available_model()
}
}
})
})
}
pub fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
let fetch_models = Box::new(move |cx: &mut WindowContext| {
cx.update_global::<CompletionProvider, _>(|provider, cx| {
if let CompletionProvider::Ollama(provider) = provider {
provider.fetch_models(cx)
} else {
Task::ready(Ok(()))
}
})
});
cx.new_view(|cx| DownloadOllamaMessage::new(fetch_models, cx))
.into()
}
pub fn model(&self) -> OllamaModel {
self.model.clone()
}
pub fn count_tokens(
&self,
request: LanguageModelRequest,
_cx: &AppContext,
) -> BoxFuture<'static, Result<usize>> {
// There is no endpoint for this _yet_ in Ollama
// see: https://github.com/ollama/ollama/issues/1716 and https://github.com/ollama/ollama/issues/3582
let token_count = request
.messages
.iter()
.map(|msg| msg.content.chars().count())
.sum::<usize>()
/ 4;
async move { Ok(token_count) }.boxed()
}
pub fn complete(
&self,
request: LanguageModelRequest,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
let request = self.to_ollama_request(request);
let http_client = self.http_client.clone();
let api_url = self.api_url.clone();
let low_speed_timeout = self.low_speed_timeout;
async move {
let request =
stream_chat_completion(http_client.as_ref(), &api_url, request, low_speed_timeout);
let response = request.await?;
let stream = response
.filter_map(|response| async move {
match response {
Ok(delta) => {
let content = match delta.message {
ChatMessage::User { content } => content,
ChatMessage::Assistant { content } => content,
ChatMessage::System { content } => content,
};
Some(Ok(content))
}
Err(error) => Some(Err(error)),
}
})
.boxed();
Ok(stream)
}
.boxed()
}
fn to_ollama_request(&self, request: LanguageModelRequest) -> ChatRequest {
let model = match request.model {
LanguageModel::Ollama(model) => model,
_ => self.model(),
};
ChatRequest {
model: model.name,
messages: request
.messages
.into_iter()
.map(|msg| match msg.role {
Role::User => ChatMessage::User {
content: msg.content,
},
Role::Assistant => ChatMessage::Assistant {
content: msg.content,
},
Role::System => ChatMessage::System {
content: msg.content,
},
})
.collect(),
keep_alive: model.keep_alive.unwrap_or_default(),
stream: true,
options: Some(ChatOptions {
num_ctx: Some(model.max_tokens),
stop: Some(request.stop),
temperature: Some(request.temperature),
..Default::default()
}),
}
}
}
impl From<Role> for ollama::Role {
fn from(val: Role) -> Self {
match val {
Role::User => OllamaRole::User,
Role::Assistant => OllamaRole::Assistant,
Role::System => OllamaRole::System,
}
}
}
struct DownloadOllamaMessage {
retry_connection: Box<dyn Fn(&mut WindowContext) -> Task<Result<()>>>,
}
impl DownloadOllamaMessage {
pub fn new(
retry_connection: Box<dyn Fn(&mut WindowContext) -> Task<Result<()>>>,
_cx: &mut ViewContext<Self>,
) -> Self {
Self { retry_connection }
}
fn render_download_button(&self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
ButtonLike::new("download_ollama_button")
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.layer(ElevationIndex::ModalSurface)
.child(Label::new("Get Ollama"))
.on_click(move |_, cx| cx.open_url(OLLAMA_DOWNLOAD_URL))
}
fn render_retry_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
ButtonLike::new("retry_ollama_models")
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.layer(ElevationIndex::ModalSurface)
.child(Label::new("Retry"))
.on_click(cx.listener(move |this, _, cx| {
let connected = (this.retry_connection)(cx);
cx.spawn(|_this, _cx| async move {
connected.await?;
anyhow::Ok(())
})
.detach_and_log_err(cx)
}))
}
fn render_next_steps(&self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
v_flex()
.p_4()
.size_full()
.gap_2()
.child(
Label::new("Once Ollama is on your machine, make sure to download a model or two.")
.size(LabelSize::Large),
)
.child(
h_flex().w_full().p_4().justify_center().gap_2().child(
ButtonLike::new("view-models")
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.layer(ElevationIndex::ModalSurface)
.child(Label::new("View Available Models"))
.on_click(move |_, cx| cx.open_url(OLLAMA_LIBRARY_URL)),
),
)
}
}
impl Render for DownloadOllamaMessage {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
v_flex()
.p_4()
.size_full()
.gap_2()
.child(Label::new("To use Ollama models via the assistant, Ollama must be running on your machine with at least one model downloaded.").size(LabelSize::Large))
.child(
h_flex()
.w_full()
.p_4()
.justify_center()
.gap_2()
.child(
self.render_download_button(cx)
)
.child(
self.render_retry_button(cx)
)
)
.child(self.render_next_steps(cx))
.into_any()
}
}

View File

@@ -1,4 +1,4 @@
use crate::assistant_settings::ZedDotDevModel;
use crate::assistant_settings::CloudModel;
use crate::{
assistant_settings::OpenAiModel, CompletionProvider, LanguageModel, LanguageModelRequest, Role,
};
@@ -210,9 +210,9 @@ pub fn count_open_ai_tokens(
match request.model {
LanguageModel::Anthropic(_)
| LanguageModel::ZedDotDev(ZedDotDevModel::Claude3Opus)
| LanguageModel::ZedDotDev(ZedDotDevModel::Claude3Sonnet)
| LanguageModel::ZedDotDev(ZedDotDevModel::Claude3Haiku) => {
| LanguageModel::Cloud(CloudModel::Claude3Opus)
| LanguageModel::Cloud(CloudModel::Claude3Sonnet)
| LanguageModel::Cloud(CloudModel::Claude3Haiku) => {
// Tiktoken doesn't yet support these models, so we manually use the
// same tokenizer as GPT-4.
tiktoken_rs::num_tokens_from_messages("gpt-4", &messages)
@@ -336,7 +336,7 @@ impl Render for AuthenticationPrompt {
h_flex()
.gap_2()
.child(Label::new("Click on").size(LabelSize::Small))
.child(Icon::new(IconName::Ai).size(IconSize::XSmall))
.child(Icon::new(IconName::ZedAssistant).size(IconSize::XSmall))
.child(
Label::new("in the status bar to close this panel.").size(LabelSize::Small),
),

View File

@@ -0,0 +1,196 @@
use crate::{assistant_settings::OpenAiModel, MessageId, MessageMetadata};
use anyhow::{anyhow, Result};
use collections::HashMap;
use fs::Fs;
use futures::StreamExt;
use fuzzy::StringMatchCandidate;
use gpui::{AppContext, Model, ModelContext, Task};
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc, time::Duration};
use ui::Context;
use util::{paths::CONTEXTS_DIR, ResultExt, TryFutureExt};
#[derive(Serialize, Deserialize)]
pub struct SavedMessage {
pub id: MessageId,
pub start: usize,
}
#[derive(Serialize, Deserialize)]
pub struct SavedContext {
pub id: Option<String>,
pub zed: String,
pub version: String,
pub text: String,
pub messages: Vec<SavedMessage>,
pub message_metadata: HashMap<MessageId, MessageMetadata>,
pub summary: String,
}
impl SavedContext {
pub const VERSION: &'static str = "0.2.0";
}
#[derive(Serialize, Deserialize)]
struct SavedContextV0_1_0 {
id: Option<String>,
zed: String,
version: String,
text: String,
messages: Vec<SavedMessage>,
message_metadata: HashMap<MessageId, MessageMetadata>,
summary: String,
api_url: Option<String>,
model: OpenAiModel,
}
#[derive(Clone)]
pub struct SavedContextMetadata {
pub title: String,
pub path: PathBuf,
pub mtime: chrono::DateTime<chrono::Local>,
}
pub struct ContextStore {
contexts_metadata: Vec<SavedContextMetadata>,
fs: Arc<dyn Fs>,
_watch_updates: Task<Option<()>>,
}
impl ContextStore {
pub fn new(fs: Arc<dyn Fs>, cx: &mut AppContext) -> Task<Result<Model<Self>>> {
cx.spawn(|mut cx| async move {
const CONTEXT_WATCH_DURATION: Duration = Duration::from_millis(100);
let (mut events, _) = fs.watch(&CONTEXTS_DIR, CONTEXT_WATCH_DURATION).await;
let this = cx.new_model(|cx: &mut ModelContext<Self>| Self {
contexts_metadata: Vec::new(),
fs,
_watch_updates: cx.spawn(|this, mut cx| {
async move {
while events.next().await.is_some() {
this.update(&mut cx, |this, cx| this.reload(cx))?
.await
.log_err();
}
anyhow::Ok(())
}
.log_err()
}),
})?;
this.update(&mut cx, |this, cx| this.reload(cx))?
.await
.log_err();
Ok(this)
})
}
pub fn load(&self, path: PathBuf, cx: &AppContext) -> Task<Result<SavedContext>> {
let fs = self.fs.clone();
cx.background_executor().spawn(async move {
let saved_context = fs.load(&path).await?;
let saved_context_json = serde_json::from_str::<serde_json::Value>(&saved_context)?;
match saved_context_json
.get("version")
.ok_or_else(|| anyhow!("version not found"))?
{
serde_json::Value::String(version) => match version.as_str() {
SavedContext::VERSION => {
Ok(serde_json::from_value::<SavedContext>(saved_context_json)?)
}
"0.1.0" => {
let saved_context =
serde_json::from_value::<SavedContextV0_1_0>(saved_context_json)?;
Ok(SavedContext {
id: saved_context.id,
zed: saved_context.zed,
version: saved_context.version,
text: saved_context.text,
messages: saved_context.messages,
message_metadata: saved_context.message_metadata,
summary: saved_context.summary,
})
}
_ => Err(anyhow!("unrecognized saved context version: {}", version)),
},
_ => Err(anyhow!("version not found on saved context")),
}
})
}
pub fn search(&self, query: String, cx: &AppContext) -> Task<Vec<SavedContextMetadata>> {
let metadata = self.contexts_metadata.clone();
let executor = cx.background_executor().clone();
cx.background_executor().spawn(async move {
if query.is_empty() {
metadata
} else {
let candidates = metadata
.iter()
.enumerate()
.map(|(id, metadata)| StringMatchCandidate::new(id, metadata.title.clone()))
.collect::<Vec<_>>();
let matches = fuzzy::match_strings(
&candidates,
&query,
false,
100,
&Default::default(),
executor,
)
.await;
matches
.into_iter()
.map(|mat| metadata[mat.candidate_id].clone())
.collect()
}
})
}
fn reload(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
let fs = self.fs.clone();
cx.spawn(|this, mut cx| async move {
fs.create_dir(&CONTEXTS_DIR).await?;
let mut paths = fs.read_dir(&CONTEXTS_DIR).await?;
let mut contexts = Vec::<SavedContextMetadata>::new();
while let Some(path) = paths.next().await {
let path = path?;
if path.extension() != Some(OsStr::new("json")) {
continue;
}
let pattern = r" - \d+.zed.json$";
let re = Regex::new(pattern).unwrap();
let metadata = fs.metadata(&path).await?;
if let Some((file_name, metadata)) = path
.file_name()
.and_then(|name| name.to_str())
.zip(metadata)
{
// This is used to filter out contexts saved by the new assistant.
if !re.is_match(file_name) {
continue;
}
if let Some(title) = re.replace(file_name, "").lines().next() {
contexts.push(SavedContextMetadata {
title: title.to_string(),
path,
mtime: metadata.mtime.into(),
});
}
}
}
contexts.sort_unstable_by_key(|context| Reverse(context.mtime));
this.update(&mut cx, |this, cx| {
this.contexts_metadata = contexts;
cx.notify();
})
})
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,95 @@
mod prompt;
mod prompt_library;
mod prompt_manager;
use language::BufferSnapshot;
use std::{fmt::Write, ops::Range};
pub use prompt::*;
pub use prompt_library::*;
pub use prompt_manager::*;
pub fn generate_content_prompt(
user_prompt: String,
language_name: Option<&str>,
buffer: BufferSnapshot,
range: Range<usize>,
project_name: Option<String>,
) -> anyhow::Result<String> {
let mut prompt = String::new();
let content_type = match language_name {
None | Some("Markdown" | "Plain Text") => {
writeln!(prompt, "You are an expert engineer.")?;
"Text"
}
Some(language_name) => {
writeln!(prompt, "You are an expert {language_name} engineer.")?;
writeln!(
prompt,
"Your answer MUST always and only be valid {}.",
language_name
)?;
"Code"
}
};
if let Some(project_name) = project_name {
writeln!(
prompt,
"You are currently working inside the '{project_name}' project in code editor Zed."
)?;
}
// Include file content.
for chunk in buffer.text_for_range(0..range.start) {
prompt.push_str(chunk);
}
if range.is_empty() {
prompt.push_str("<|START|>");
} else {
prompt.push_str("<|START|");
}
for chunk in buffer.text_for_range(range.clone()) {
prompt.push_str(chunk);
}
if !range.is_empty() {
prompt.push_str("|END|>");
}
for chunk in buffer.text_for_range(range.end..buffer.len()) {
prompt.push_str(chunk);
}
prompt.push('\n');
if range.is_empty() {
writeln!(
prompt,
"Assume the cursor is located where the `<|START|>` span is."
)
.unwrap();
writeln!(
prompt,
"{content_type} can't be replaced, so assume your answer will be inserted at the cursor.",
)
.unwrap();
writeln!(
prompt,
"Generate {content_type} based on the users prompt: {user_prompt}",
)
.unwrap();
} else {
writeln!(prompt, "Modify the user's selected {content_type} based upon the users prompt: '{user_prompt}'").unwrap();
writeln!(prompt, "You must reply with only the adjusted {content_type} (within the '<|START|' and '|END|>' spans) not the entire file.").unwrap();
writeln!(
prompt,
"Double check that you only return code and not the '<|START|' and '|END|'> spans"
)
.unwrap();
}
writeln!(prompt, "Never make remarks about the output.").unwrap();
writeln!(
prompt,
"Do not return anything else, except the generated {content_type}."
)
.unwrap();
Ok(prompt)
}

View File

@@ -1,360 +0,0 @@
use fs::Fs;
use language::BufferSnapshot;
use std::{fmt::Write, ops::Range, path::PathBuf, sync::Arc};
use ui::SharedString;
use util::paths::PROMPTS_DIR;
use gray_matter::{engine::YAML, Matter};
use serde::{Deserialize, Serialize};
use super::prompt_library::PromptId;
pub const PROMPT_DEFAULT_TITLE: &str = "Untitled Prompt";
fn standardize_value(value: String) -> String {
value.replace(['\n', '\r', '"', '\''], "")
}
fn slugify(input: String) -> String {
let mut slug = String::new();
for c in input.chars() {
if c.is_alphanumeric() {
slug.push(c.to_ascii_lowercase());
} else if c.is_whitespace() {
slug.push('-');
} else {
slug.push('_');
}
}
slug
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct StaticPromptFrontmatter {
title: String,
version: String,
author: String,
#[serde(default)]
languages: Vec<String>,
#[serde(default)]
dependencies: Vec<String>,
}
impl Default for StaticPromptFrontmatter {
fn default() -> Self {
Self {
title: PROMPT_DEFAULT_TITLE.to_string(),
version: "1.0".to_string(),
author: "You <you@email.com>".to_string(),
languages: vec![],
dependencies: vec![],
}
}
}
impl StaticPromptFrontmatter {
/// Returns the frontmatter as a markdown frontmatter string
pub fn frontmatter_string(&self) -> String {
let mut frontmatter = format!(
"---\ntitle: \"{}\"\nversion: \"{}\"\nauthor: \"{}\"\n",
standardize_value(self.title.clone()),
standardize_value(self.version.clone()),
standardize_value(self.author.clone()),
);
if !self.languages.is_empty() {
let languages = self
.languages
.iter()
.map(|l| standardize_value(l.clone()))
.collect::<Vec<String>>()
.join(", ");
writeln!(frontmatter, "languages: [{}]", languages).unwrap();
}
if !self.dependencies.is_empty() {
let dependencies = self
.dependencies
.iter()
.map(|d| standardize_value(d.clone()))
.collect::<Vec<String>>()
.join(", ");
writeln!(frontmatter, "dependencies: [{}]", dependencies).unwrap();
}
frontmatter.push_str("---\n");
frontmatter
}
}
/// A static prompt that can be loaded into the prompt library
/// from Markdown with a frontmatter header
///
/// Examples:
///
/// ### Globally available prompt
///
/// ```markdown
/// ---
/// title: Foo
/// version: 1.0
/// author: Jane Kim <jane@kim.com
/// languages: ["*"]
/// dependencies: []
/// ---
///
/// Foo and bar are terms used in programming to describe generic concepts.
/// ```
///
/// ### Language-specific prompt
///
/// ```markdown
/// ---
/// title: UI with GPUI
/// version: 1.0
/// author: Nate Butler <iamnbutler@gmail.com>
/// languages: ["rust"]
/// dependencies: ["gpui"]
/// ---
///
/// When building a UI with GPUI, ensure you...
/// ```
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct StaticPrompt {
#[serde(skip_deserializing)]
id: PromptId,
#[serde(skip)]
metadata: StaticPromptFrontmatter,
content: String,
file_name: Option<SharedString>,
}
impl Default for StaticPrompt {
fn default() -> Self {
let metadata = StaticPromptFrontmatter::default();
let content = metadata.clone().frontmatter_string();
Self {
id: PromptId::new(),
metadata,
content,
file_name: None,
}
}
}
impl StaticPrompt {
pub fn new(content: String, file_name: Option<String>) -> Self {
let matter = Matter::<YAML>::new();
let result = matter.parse(&content);
let file_name = if let Some(file_name) = file_name {
let shared_filename: SharedString = file_name.into();
Some(shared_filename)
} else {
None
};
let metadata = result
.data
.map_or_else(
|| Err(anyhow::anyhow!("Failed to parse frontmatter")),
|data| {
let front_matter: StaticPromptFrontmatter = data.deserialize()?;
Ok(front_matter)
},
)
.unwrap_or_else(|e| {
if let Some(file_name) = &file_name {
log::error!("Failed to parse frontmatter for {}: {}", file_name, e);
} else {
log::error!("Failed to parse frontmatter: {}", e);
}
StaticPromptFrontmatter::default()
});
let id = if let Some(file_name) = &file_name {
PromptId::from_str(file_name).unwrap_or_default()
} else {
PromptId::new()
};
StaticPrompt {
id,
content,
file_name,
metadata,
}
}
pub fn update(&mut self, id: PromptId, content: String) {
let mut updated_prompt =
StaticPrompt::new(content, self.file_name.clone().map(|s| s.to_string()));
updated_prompt.id = id;
*self = updated_prompt;
}
}
impl StaticPrompt {
/// Returns the prompt's id
pub fn id(&self) -> &PromptId {
&self.id
}
pub fn file_name(&self) -> Option<&SharedString> {
self.file_name.as_ref()
}
/// Sets the file name of the prompt
pub fn new_file_name(&self) -> String {
let in_name = format!(
"{}_{}_{}",
standardize_value(self.metadata.title.clone()),
standardize_value(self.metadata.version.clone()),
standardize_value(self.id.0.to_string())
);
let out_name = slugify(in_name);
out_name
}
/// Returns the prompt's content
pub fn content(&self) -> &String {
&self.content
}
/// Returns the prompt's metadata
pub fn _metadata(&self) -> &StaticPromptFrontmatter {
&self.metadata
}
/// Returns the prompt's title
pub fn title(&self) -> SharedString {
self.metadata.title.clone().into()
}
pub fn body(&self) -> String {
let matter = Matter::<YAML>::new();
let result = matter.parse(self.content.as_str());
result.content.clone()
}
pub fn path(&self) -> Option<PathBuf> {
if let Some(file_name) = self.file_name() {
let path_str = format!("{}", file_name);
Some(PROMPTS_DIR.join(path_str))
} else {
None
}
}
pub async fn save(&self, fs: Arc<dyn Fs>) -> anyhow::Result<()> {
let file_name = self.file_name();
let new_file_name = self.new_file_name();
let out_name = if let Some(file_name) = file_name {
file_name.to_owned().to_string()
} else {
format!("{}.md", new_file_name)
};
let path = PROMPTS_DIR.join(&out_name);
let json = self.content.clone();
fs.atomic_write(path, json).await?;
Ok(())
}
}
pub fn generate_content_prompt(
user_prompt: String,
language_name: Option<&str>,
buffer: BufferSnapshot,
range: Range<usize>,
project_name: Option<String>,
) -> anyhow::Result<String> {
let mut prompt = String::new();
let content_type = match language_name {
None | Some("Markdown" | "Plain Text") => {
writeln!(prompt, "You are an expert engineer.")?;
"Text"
}
Some(language_name) => {
writeln!(prompt, "You are an expert {language_name} engineer.")?;
writeln!(
prompt,
"Your answer MUST always and only be valid {}.",
language_name
)?;
"Code"
}
};
if let Some(project_name) = project_name {
writeln!(
prompt,
"You are currently working inside the '{project_name}' project in code editor Zed."
)?;
}
// Include file content.
for chunk in buffer.text_for_range(0..range.start) {
prompt.push_str(chunk);
}
if range.is_empty() {
prompt.push_str("<|START|>");
} else {
prompt.push_str("<|START|");
}
for chunk in buffer.text_for_range(range.clone()) {
prompt.push_str(chunk);
}
if !range.is_empty() {
prompt.push_str("|END|>");
}
for chunk in buffer.text_for_range(range.end..buffer.len()) {
prompt.push_str(chunk);
}
prompt.push('\n');
if range.is_empty() {
writeln!(
prompt,
"Assume the cursor is located where the `<|START|>` span is."
)
.unwrap();
writeln!(
prompt,
"{content_type} can't be replaced, so assume your answer will be inserted at the cursor.",
)
.unwrap();
writeln!(
prompt,
"Generate {content_type} based on the users prompt: {user_prompt}",
)
.unwrap();
} else {
writeln!(prompt, "Modify the user's selected {content_type} based upon the users prompt: '{user_prompt}'").unwrap();
writeln!(prompt, "You must reply with only the adjusted {content_type} (within the '<|START|' and '|END|>' spans) not the entire file.").unwrap();
writeln!(
prompt,
"Double check that you only return code and not the '<|START|' and '|END|'> spans"
)
.unwrap();
}
writeln!(prompt, "Never make remarks about the output.").unwrap();
writeln!(
prompt,
"Do not return anything else, except the generated {content_type}."
)
.unwrap();
Ok(prompt)
}

View File

@@ -1,245 +0,0 @@
use anyhow::Context;
use collections::HashMap;
use fs::Fs;
use gray_matter::{engine::YAML, Matter};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use smol::stream::StreamExt;
use std::sync::Arc;
use util::paths::PROMPTS_DIR;
use uuid::Uuid;
use super::prompt::StaticPrompt;
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct PromptId(pub Uuid);
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum SortOrder {
Alphabetical,
}
#[allow(unused)]
impl PromptId {
pub fn new() -> Self {
Self(Uuid::new_v4())
}
pub fn from_str(id: &str) -> anyhow::Result<Self> {
Ok(Self(Uuid::parse_str(id)?))
}
}
impl Default for PromptId {
fn default() -> Self {
Self::new()
}
}
#[derive(Default, Serialize, Deserialize)]
pub struct PromptLibraryState {
/// A set of prompts that all assistant contexts will start with
default_prompt: Vec<PromptId>,
/// All [Prompt]s loaded into the library
prompts: HashMap<PromptId, StaticPrompt>,
/// Prompts that have been changed but haven't been
/// saved back to the file system
dirty_prompts: Vec<PromptId>,
version: usize,
}
pub struct PromptLibrary {
state: RwLock<PromptLibraryState>,
}
impl Default for PromptLibrary {
fn default() -> Self {
Self::new()
}
}
impl PromptLibrary {
fn new() -> Self {
Self {
state: RwLock::new(PromptLibraryState::default()),
}
}
pub fn new_prompt(&self) -> StaticPrompt {
StaticPrompt::default()
}
pub fn add_prompt(&self, prompt: StaticPrompt) {
let mut state = self.state.write();
let id = *prompt.id();
state.prompts.insert(id, prompt);
state.version += 1;
}
pub fn prompts(&self) -> HashMap<PromptId, StaticPrompt> {
let state = self.state.read();
state.prompts.clone()
}
pub fn sorted_prompts(&self, sort_order: SortOrder) -> Vec<(PromptId, StaticPrompt)> {
let state = self.state.read();
let mut prompts = state
.prompts
.iter()
.map(|(id, prompt)| (*id, prompt.clone()))
.collect::<Vec<_>>();
match sort_order {
SortOrder::Alphabetical => prompts.sort_by(|(_, a), (_, b)| a.title().cmp(&b.title())),
};
prompts
}
pub fn prompt_by_id(&self, id: PromptId) -> Option<StaticPrompt> {
let state = self.state.read();
state.prompts.get(&id).cloned()
}
pub fn first_prompt_id(&self) -> Option<PromptId> {
let state = self.state.read();
state.prompts.keys().next().cloned()
}
pub fn is_dirty(&self, id: &PromptId) -> bool {
let state = self.state.read();
state.dirty_prompts.contains(&id)
}
pub fn set_dirty(&self, id: PromptId, dirty: bool) {
let mut state = self.state.write();
if dirty {
if !state.dirty_prompts.contains(&id) {
state.dirty_prompts.push(id);
}
state.version += 1;
} else {
state.dirty_prompts.retain(|&i| i != id);
state.version += 1;
}
}
/// Load the state of the prompt library from the file system
/// or create a new one if it doesn't exist
pub async fn load_index(fs: Arc<dyn Fs>) -> anyhow::Result<Self> {
let path = PROMPTS_DIR.join("index.json");
let state = if fs.is_file(&path).await {
let json = fs.load(&path).await?;
serde_json::from_str(&json)?
} else {
PromptLibraryState::default()
};
let mut prompt_library = Self {
state: RwLock::new(state),
};
prompt_library.load_prompts(fs).await?;
Ok(prompt_library)
}
/// Load all prompts from the file system
/// adding them to the library if they don't already exist
pub async fn load_prompts(&mut self, fs: Arc<dyn Fs>) -> anyhow::Result<()> {
self.state.get_mut().prompts.clear();
let mut prompt_paths = fs.read_dir(&PROMPTS_DIR).await?;
while let Some(prompt_path) = prompt_paths.next().await {
let prompt_path = prompt_path.with_context(|| "Failed to read prompt path")?;
let file_name_lossy = if prompt_path.file_name().is_some() {
Some(
prompt_path
.file_name()
.unwrap()
.to_string_lossy()
.to_string(),
)
} else {
None
};
if !fs.is_file(&prompt_path).await
|| prompt_path.extension().and_then(|ext| ext.to_str()) != Some("md")
{
continue;
}
let json = fs
.load(&prompt_path)
.await
.with_context(|| format!("Failed to load prompt {:?}", prompt_path))?;
// Check that the prompt is valid
let matter = Matter::<YAML>::new();
let result = matter.parse(&json);
if result.data.is_none() {
log::warn!("Invalid prompt: {:?}", prompt_path);
continue;
}
let static_prompt = StaticPrompt::new(json, file_name_lossy.clone());
let state = self.state.get_mut();
let id = Uuid::new_v4();
state.prompts.insert(PromptId(id), static_prompt);
state.version += 1;
}
// Write any changes back to the file system
self.save_index(fs.clone()).await?;
Ok(())
}
/// Save the current state of the prompt library to the
/// file system as a JSON file
pub async fn save_index(&self, fs: Arc<dyn Fs>) -> anyhow::Result<()> {
fs.create_dir(&PROMPTS_DIR).await?;
let path = PROMPTS_DIR.join("index.json");
let json = {
let state = self.state.read();
serde_json::to_string(&*state)?
};
fs.atomic_write(path, json).await?;
Ok(())
}
pub async fn save_prompt(
&self,
prompt_id: PromptId,
updated_content: Option<String>,
fs: Arc<dyn Fs>,
) -> anyhow::Result<()> {
if let Some(updated_content) = updated_content {
let mut state = self.state.write();
if let Some(prompt) = state.prompts.get_mut(&prompt_id) {
prompt.update(prompt_id, updated_content);
state.version += 1;
}
}
if let Some(prompt) = self.prompt_by_id(prompt_id) {
prompt.save(fs).await?;
self.set_dirty(prompt_id, false);
} else {
log::warn!("Failed to save prompt: {:?}", prompt_id);
}
Ok(())
}
}

View File

@@ -1,512 +0,0 @@
use collections::HashMap;
use editor::{Editor, EditorEvent};
use fs::Fs;
use gpui::{prelude::FluentBuilder, *};
use language::{language_settings, Buffer, LanguageRegistry};
use picker::{Picker, PickerDelegate};
use std::sync::Arc;
use ui::{prelude::*, IconButtonShape, Indicator, ListItem, ListItemSpacing, Tooltip};
use util::{ResultExt, TryFutureExt};
use workspace::ModalView;
use crate::prompts::{PromptId, PromptLibrary, SortOrder, StaticPrompt, PROMPT_DEFAULT_TITLE};
actions!(prompt_manager, [NewPrompt, SavePrompt]);
pub struct PromptManager {
focus_handle: FocusHandle,
prompt_library: Arc<PromptLibrary>,
language_registry: Arc<LanguageRegistry>,
#[allow(dead_code)]
fs: Arc<dyn Fs>,
picker: View<Picker<PromptManagerDelegate>>,
prompt_editors: HashMap<PromptId, View<Editor>>,
active_prompt_id: Option<PromptId>,
last_new_prompt_id: Option<PromptId>,
_subscriptions: Vec<Subscription>,
}
impl PromptManager {
pub fn new(
prompt_library: Arc<PromptLibrary>,
language_registry: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
cx: &mut ViewContext<Self>,
) -> Self {
let prompt_manager = cx.view().downgrade();
let picker = cx.new_view(|cx| {
Picker::uniform_list(
PromptManagerDelegate {
prompt_manager,
matching_prompts: vec![],
matching_prompt_ids: vec![],
prompt_library: prompt_library.clone(),
selected_index: 0,
_subscriptions: vec![],
},
cx,
)
.max_height(rems(35.75))
.modal(false)
});
let focus_handle = picker.focus_handle(cx);
let subscriptions = vec![
// cx.on_focus_in(&focus_handle, Self::focus_in),
// cx.on_focus_out(&focus_handle, Self::focus_out),
];
let mut manager = Self {
focus_handle,
prompt_library,
language_registry,
fs,
picker,
prompt_editors: HashMap::default(),
active_prompt_id: None,
last_new_prompt_id: None,
_subscriptions: subscriptions,
};
manager.active_prompt_id = manager.prompt_library.first_prompt_id();
manager
}
fn dispatch_context(&self, cx: &ViewContext<Self>) -> KeyContext {
let mut dispatch_context = KeyContext::new_with_defaults();
dispatch_context.add("PromptManager");
let identifier = match self.active_editor() {
Some(active_editor) if active_editor.focus_handle(cx).is_focused(cx) => "editing",
_ => "not_editing",
};
dispatch_context.add(identifier);
dispatch_context
}
pub fn new_prompt(&mut self, _: &NewPrompt, cx: &mut ViewContext<Self>) {
// TODO: Why doesn't this prevent making a new prompt if you
// move the picker selection/maybe unfocus the editor?
// Prevent making a new prompt if the last new prompt is still empty
//
// Instead, we'll focus the last new prompt
if let Some(last_new_prompt_id) = self.last_new_prompt_id() {
if let Some(last_new_prompt) = self.prompt_library.prompt_by_id(last_new_prompt_id) {
let normalized_body = last_new_prompt
.body()
.trim()
.replace(['\r', '\n'], "")
.to_string();
if last_new_prompt.title() == PROMPT_DEFAULT_TITLE && normalized_body.is_empty() {
self.set_editor_for_prompt(last_new_prompt_id, cx);
self.focus_active_editor(cx);
}
}
}
let prompt = self.prompt_library.new_prompt();
self.set_last_new_prompt_id(Some(prompt.id().to_owned()));
self.prompt_library.add_prompt(prompt.clone());
let id = *prompt.id();
self.picker.update(cx, |picker, _cx| {
let prompts = self
.prompt_library
.sorted_prompts(SortOrder::Alphabetical)
.clone()
.into_iter();
picker.delegate.prompt_library = self.prompt_library.clone();
picker.delegate.matching_prompts = prompts.clone().map(|(_, p)| Arc::new(p)).collect();
picker.delegate.matching_prompt_ids = prompts.map(|(id, _)| id).collect();
picker.delegate.selected_index = picker
.delegate
.matching_prompts
.iter()
.position(|p| p.id() == &id)
.unwrap_or(0);
});
self.active_prompt_id = Some(id);
cx.notify();
}
pub fn save_prompt(
&mut self,
fs: Arc<dyn Fs>,
prompt_id: PromptId,
new_content: String,
cx: &mut ViewContext<Self>,
) -> Result<()> {
let library = self.prompt_library.clone();
if library.prompt_by_id(prompt_id).is_some() {
cx.spawn(|_, _| async move {
library
.save_prompt(prompt_id, Some(new_content), fs)
.log_err()
.await;
})
.detach();
cx.notify();
}
Ok(())
}
pub fn set_active_prompt(&mut self, prompt_id: Option<PromptId>, cx: &mut ViewContext<Self>) {
self.active_prompt_id = prompt_id;
cx.notify();
}
pub fn last_new_prompt_id(&self) -> Option<PromptId> {
self.last_new_prompt_id
}
pub fn set_last_new_prompt_id(&mut self, id: Option<PromptId>) {
self.last_new_prompt_id = id;
}
pub fn focus_active_editor(&self, cx: &mut ViewContext<Self>) {
if let Some(active_prompt_id) = self.active_prompt_id {
if let Some(editor) = self.prompt_editors.get(&active_prompt_id) {
let focus_handle = editor.focus_handle(cx);
cx.focus(&focus_handle)
}
}
}
pub fn active_editor(&self) -> Option<&View<Editor>> {
self.active_prompt_id
.and_then(|active_prompt_id| self.prompt_editors.get(&active_prompt_id))
}
fn set_editor_for_prompt(
&mut self,
prompt_id: PromptId,
cx: &mut ViewContext<Self>,
) -> impl IntoElement {
let prompt_library = self.prompt_library.clone();
let editor_for_prompt = self.prompt_editors.entry(prompt_id).or_insert_with(|| {
cx.new_view(|cx| {
let text = if let Some(prompt) = prompt_library.prompt_by_id(prompt_id) {
prompt.content().to_owned()
} else {
"".to_string()
};
let buffer = cx.new_model(|cx| {
let mut buffer = Buffer::local(text, cx);
let markdown = self.language_registry.language_for_name("Markdown");
cx.spawn(|buffer, mut cx| async move {
if let Some(markdown) = markdown.await.log_err() {
_ = buffer.update(&mut cx, |buffer, cx| {
buffer.set_language(Some(markdown), cx);
});
}
})
.detach();
buffer.set_language_registry(self.language_registry.clone());
buffer
});
let mut editor = Editor::for_buffer(buffer, None, cx);
editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
editor.set_show_gutter(false, cx);
editor
})
});
editor_for_prompt.clone()
}
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
cx.emit(DismissEvent);
}
fn render_prompt_list(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let picker = self.picker.clone();
v_flex()
.id("prompt-list")
.bg(cx.theme().colors().surface_background)
.h_full()
.w_1_3()
.overflow_hidden()
.child(
h_flex()
.bg(cx.theme().colors().background)
.p(Spacing::Small.rems(cx))
.border_b_1()
.border_color(cx.theme().colors().border)
.h(rems(1.75))
.w_full()
.flex_none()
.justify_between()
.child(Label::new("Prompt Library").size(LabelSize::Small))
.child(
IconButton::new("new-prompt", IconName::Plus)
.shape(IconButtonShape::Square)
.tooltip(move |cx| Tooltip::text("New Prompt", cx))
.on_click(|_, cx| {
cx.dispatch_action(NewPrompt.boxed_clone());
}),
),
)
.child(
v_flex()
.h(rems(38.25))
.flex_grow()
.justify_start()
.child(picker),
)
}
}
impl Render for PromptManager {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let active_prompt_id = self.active_prompt_id;
let active_prompt = if let Some(active_prompt_id) = active_prompt_id {
self.prompt_library.clone().prompt_by_id(active_prompt_id)
} else {
None
};
let active_editor = self.active_editor().map(|editor| editor.clone());
let updated_content = if let Some(editor) = active_editor {
Some(editor.read(cx).text(cx))
} else {
None
};
let can_save = active_prompt_id.is_some() && updated_content.is_some();
let fs = self.fs.clone();
h_flex()
.id("prompt-manager")
.key_context(self.dispatch_context(cx))
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::dismiss))
.on_action(cx.listener(Self::new_prompt))
.elevation_3(cx)
.size_full()
.flex_none()
.w(rems(64.))
.h(rems(40.))
.overflow_hidden()
.child(self.render_prompt_list(cx))
.child(
div().w_2_3().h_full().child(
v_flex()
.id("prompt-editor")
.border_l_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().editor_background)
.size_full()
.flex_none()
.min_w_64()
.h_full()
.overflow_hidden()
.child(
h_flex()
.bg(cx.theme().colors().background)
.p(Spacing::Small.rems(cx))
.border_b_1()
.border_color(cx.theme().colors().border)
.h_7()
.w_full()
.justify_between()
.child(
h_flex()
.gap(Spacing::XXLarge.rems(cx))
.child(if can_save {
IconButton::new("save", IconName::Save)
.shape(IconButtonShape::Square)
.tooltip(move |cx| Tooltip::text("Save Prompt", cx))
.on_click(cx.listener(move |this, _event, cx| {
if let Some(prompt_id) = active_prompt_id {
this.save_prompt(
fs.clone(),
prompt_id,
updated_content.clone().unwrap_or(
"TODO: make unreachable"
.to_string(),
),
cx,
)
.log_err();
}
}))
} else {
IconButton::new("save", IconName::Save)
.shape(IconButtonShape::Square)
.disabled(true)
})
.when_some(active_prompt, |this, active_prompt| {
let path = active_prompt.path();
this.child(
IconButton::new("reveal", IconName::Reveal)
.shape(IconButtonShape::Square)
.disabled(path.is_none())
.tooltip(move |cx| {
Tooltip::text("Reveal in Finder", cx)
})
.on_click(cx.listener(move |_, _event, cx| {
if let Some(path) = path.clone() {
cx.reveal_path(&path);
}
})),
)
}),
)
.child(
IconButton::new("dismiss", IconName::Close)
.shape(IconButtonShape::Square)
.tooltip(move |cx| Tooltip::text("Close", cx))
.on_click(|_, cx| {
cx.dispatch_action(menu::Cancel.boxed_clone());
}),
),
)
.when_some(active_prompt_id, |this, active_prompt_id| {
this.child(
h_flex()
.flex_1()
.w_full()
.py(Spacing::Large.rems(cx))
.px(Spacing::XLarge.rems(cx))
.child(self.set_editor_for_prompt(active_prompt_id, cx)),
)
}),
),
)
}
}
impl EventEmitter<DismissEvent> for PromptManager {}
impl EventEmitter<EditorEvent> for PromptManager {}
impl ModalView for PromptManager {}
impl FocusableView for PromptManager {
fn focus_handle(&self, _cx: &AppContext) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
pub struct PromptManagerDelegate {
prompt_manager: WeakView<PromptManager>,
matching_prompts: Vec<Arc<StaticPrompt>>,
matching_prompt_ids: Vec<PromptId>,
prompt_library: Arc<PromptLibrary>,
selected_index: usize,
_subscriptions: Vec<Subscription>,
}
impl PickerDelegate for PromptManagerDelegate {
type ListItem = ListItem;
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
"Find a prompt…".into()
}
fn match_count(&self) -> usize {
self.matching_prompt_ids.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
self.selected_index = ix;
}
fn selected_index_changed(
&self,
ix: usize,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Box<dyn Fn(&mut WindowContext) + 'static>> {
let prompt_id = self.matching_prompt_ids.get(ix).copied()?;
let prompt_manager = self.prompt_manager.upgrade()?;
Some(Box::new(move |cx| {
prompt_manager.update(cx, |manager, cx| {
manager.set_active_prompt(Some(prompt_id), cx);
})
}))
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
let prompt_library = self.prompt_library.clone();
cx.spawn(|picker, mut cx| async move {
async {
let prompts = prompt_library.sorted_prompts(SortOrder::Alphabetical);
let matching_prompts = prompts
.into_iter()
.filter(|(_, prompt)| {
prompt
.content()
.to_lowercase()
.contains(&query.to_lowercase())
})
.collect::<Vec<_>>();
picker.update(&mut cx, |picker, cx| {
picker.delegate.matching_prompt_ids =
matching_prompts.iter().map(|(id, _)| *id).collect();
picker.delegate.matching_prompts = matching_prompts
.into_iter()
.map(|(_, prompt)| Arc::new(prompt))
.collect();
cx.notify();
})?;
anyhow::Ok(())
}
.log_err()
.await;
})
}
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
let prompt_manager = self.prompt_manager.upgrade().unwrap();
prompt_manager.update(cx, move |manager, cx| manager.focus_active_editor(cx));
}
fn should_dismiss(&self) -> bool {
false
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
self.prompt_manager
.update(cx, |_, cx| {
cx.emit(DismissEvent);
})
.ok();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let prompt = self.matching_prompts.get(ix)?;
let is_diry = self.prompt_library.is_dirty(prompt.id());
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.child(Label::new(prompt.title()))
.end_slot(div().when(is_diry, |this| this.child(Indicator::dot()))),
)
}
}

View File

@@ -1,126 +0,0 @@
use crate::{assistant_settings::OpenAiModel, MessageId, MessageMetadata};
use anyhow::{anyhow, Result};
use collections::HashMap;
use fs::Fs;
use futures::StreamExt;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::{
cmp::Reverse,
ffi::OsStr,
path::{Path, PathBuf},
sync::Arc,
};
use util::paths::CONVERSATIONS_DIR;
#[derive(Serialize, Deserialize)]
pub struct SavedMessage {
pub id: MessageId,
pub start: usize,
}
#[derive(Serialize, Deserialize)]
pub struct SavedConversation {
pub id: Option<String>,
pub zed: String,
pub version: String,
pub text: String,
pub messages: Vec<SavedMessage>,
pub message_metadata: HashMap<MessageId, MessageMetadata>,
pub summary: String,
}
impl SavedConversation {
pub const VERSION: &'static str = "0.2.0";
pub async fn load(path: &Path, fs: &dyn Fs) -> Result<Self> {
let saved_conversation = fs.load(path).await?;
let saved_conversation_json =
serde_json::from_str::<serde_json::Value>(&saved_conversation)?;
match saved_conversation_json
.get("version")
.ok_or_else(|| anyhow!("version not found"))?
{
serde_json::Value::String(version) => match version.as_str() {
Self::VERSION => Ok(serde_json::from_value::<Self>(saved_conversation_json)?),
"0.1.0" => {
let saved_conversation =
serde_json::from_value::<SavedConversationV0_1_0>(saved_conversation_json)?;
Ok(Self {
id: saved_conversation.id,
zed: saved_conversation.zed,
version: saved_conversation.version,
text: saved_conversation.text,
messages: saved_conversation.messages,
message_metadata: saved_conversation.message_metadata,
summary: saved_conversation.summary,
})
}
_ => Err(anyhow!(
"unrecognized saved conversation version: {}",
version
)),
},
_ => Err(anyhow!("version not found on saved conversation")),
}
}
}
#[derive(Serialize, Deserialize)]
struct SavedConversationV0_1_0 {
id: Option<String>,
zed: String,
version: String,
text: String,
messages: Vec<SavedMessage>,
message_metadata: HashMap<MessageId, MessageMetadata>,
summary: String,
api_url: Option<String>,
model: OpenAiModel,
}
pub struct SavedConversationMetadata {
pub title: String,
pub path: PathBuf,
pub mtime: chrono::DateTime<chrono::Local>,
}
impl SavedConversationMetadata {
pub async fn list(fs: Arc<dyn Fs>) -> Result<Vec<Self>> {
fs.create_dir(&CONVERSATIONS_DIR).await?;
let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?;
let mut conversations = Vec::<SavedConversationMetadata>::new();
while let Some(path) = paths.next().await {
let path = path?;
if path.extension() != Some(OsStr::new("json")) {
continue;
}
let pattern = r" - \d+.zed.json$";
let re = Regex::new(pattern).unwrap();
let metadata = fs.metadata(&path).await?;
if let Some((file_name, metadata)) = path
.file_name()
.and_then(|name| name.to_str())
.zip(metadata)
{
// This is used to filter out conversations saved by the new assistant.
if !re.is_match(file_name) {
continue;
}
let title = re.replace(file_name, "");
conversations.push(Self {
title: title.into_owned(),
path,
mtime: metadata.mtime.into(),
});
}
}
conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime));
Ok(conversations)
}
}

View File

@@ -1,4 +1,4 @@
use crate::assistant_panel::ConversationEditor;
use crate::assistant_panel::ContextEditor;
use anyhow::Result;
pub use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandRegistry};
use editor::{CompletionProvider, Editor};
@@ -17,7 +17,11 @@ use std::{
use workspace::Workspace;
pub mod active_command;
pub mod default_command;
pub mod diagnostics_command;
pub mod fetch_command;
pub mod file_command;
pub mod now_command;
pub mod project_command;
pub mod prompt_command;
pub mod rustdoc_command;
@@ -25,10 +29,10 @@ pub mod search_command;
pub mod tabs_command;
pub(crate) struct SlashCommandCompletionProvider {
editor: WeakView<ConversationEditor>,
commands: Arc<SlashCommandRegistry>,
cancel_flag: Mutex<Arc<AtomicBool>>,
workspace: WeakView<Workspace>,
editor: Option<WeakView<ContextEditor>>,
workspace: Option<WeakView<Workspace>>,
}
pub(crate) struct SlashCommandLine {
@@ -40,9 +44,9 @@ pub(crate) struct SlashCommandLine {
impl SlashCommandCompletionProvider {
pub fn new(
editor: WeakView<ConversationEditor>,
commands: Arc<SlashCommandRegistry>,
workspace: WeakView<Workspace>,
editor: Option<WeakView<ContextEditor>>,
workspace: Option<WeakView<Workspace>>,
) -> Self {
Self {
cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
@@ -96,6 +100,30 @@ impl SlashCommandCompletionProvider {
new_text.push(' ');
}
let confirm = editor.clone().zip(workspace.clone()).and_then(
|(editor, workspace)| {
(!requires_argument).then(|| {
let command_name = mat.string.clone();
let command_range = command_range.clone();
let editor = editor.clone();
let workspace = workspace.clone();
Arc::new(move |cx: &mut WindowContext| {
editor
.update(cx, |editor, cx| {
editor.run_command(
command_range.clone(),
&command_name,
None,
true,
workspace.clone(),
cx,
);
})
.ok();
}) as Arc<_>
})
},
);
Some(project::Completion {
old_range: name_range.clone(),
documentation: Some(Documentation::SingleLine(command.description())),
@@ -104,25 +132,7 @@ impl SlashCommandCompletionProvider {
server_id: LanguageServerId(0),
lsp_completion: Default::default(),
show_new_completions_on_confirm: requires_argument,
confirm: (!requires_argument).then(|| {
let command_name = mat.string.clone();
let command_range = command_range.clone();
let editor = editor.clone();
let workspace = workspace.clone();
Arc::new(move |cx: &mut WindowContext| {
editor
.update(cx, |editor, cx| {
editor.run_command(
command_range.clone(),
&command_name,
None,
workspace.clone(),
cx,
);
})
.ok();
}) as Arc<_>
}),
confirm,
})
})
.collect()
@@ -157,33 +167,42 @@ impl SlashCommandCompletionProvider {
Ok(completions
.await?
.into_iter()
.map(|arg| project::Completion {
old_range: argument_range.clone(),
label: CodeLabel::plain(arg.clone(), None),
new_text: arg.clone(),
documentation: None,
server_id: LanguageServerId(0),
lsp_completion: Default::default(),
show_new_completions_on_confirm: false,
confirm: Some(Arc::new({
let command_name = command_name.clone();
let command_range = command_range.clone();
let editor = editor.clone();
let workspace = workspace.clone();
move |cx| {
editor
.update(cx, |editor, cx| {
editor.run_command(
command_range.clone(),
&command_name,
Some(&arg),
workspace.clone(),
cx,
);
})
.ok();
}
})),
.map(|command_argument| {
let confirm =
editor
.clone()
.zip(workspace.clone())
.map(|(editor, workspace)| {
Arc::new({
let command_range = command_range.clone();
let command_name = command_name.clone();
let command_argument = command_argument.clone();
move |cx: &mut WindowContext| {
editor
.update(cx, |editor, cx| {
editor.run_command(
command_range.clone(),
&command_name,
Some(&command_argument),
true,
workspace.clone(),
cx,
);
})
.ok();
}
}) as Arc<_>
});
project::Completion {
old_range: argument_range.clone(),
label: CodeLabel::plain(command_argument.clone(), None),
new_text: command_argument.clone(),
documentation: None,
server_id: LanguageServerId(0),
lsp_completion: Default::default(),
show_new_completions_on_confirm: false,
confirm,
}
})
.collect())
})
@@ -199,6 +218,7 @@ impl CompletionProvider for SlashCommandCompletionProvider {
&self,
buffer: &Model<Buffer>,
buffer_position: Anchor,
_: editor::CompletionContext,
cx: &mut ViewContext<Editor>,
) -> Task<Result<Vec<project::Completion>>> {
let Some((name, argument, command_range, argument_range)) =

View File

@@ -27,7 +27,7 @@ impl SlashCommand for ActiveSlashCommand {
&self,
_query: String,
_cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
_workspace: WeakView<Workspace>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
@@ -96,6 +96,7 @@ impl SlashCommand for ActiveSlashCommand {
.into_any_element()
}),
}],
run_commands_in_text: false,
})
})
});

View File

@@ -0,0 +1,81 @@
use super::{prompt_command::PromptPlaceholder, SlashCommand, SlashCommandOutput};
use crate::prompt_library::PromptStore;
use anyhow::{anyhow, Result};
use assistant_slash_command::SlashCommandOutputSection;
use gpui::{AppContext, Task, WeakView};
use language::LspAdapterDelegate;
use std::{
fmt::Write,
sync::{atomic::AtomicBool, Arc},
};
use ui::prelude::*;
use workspace::Workspace;
pub(crate) struct DefaultSlashCommand;
impl SlashCommand for DefaultSlashCommand {
fn name(&self) -> String {
"default".into()
}
fn description(&self) -> String {
"insert default prompt".into()
}
fn menu_text(&self) -> String {
"Insert Default Prompt".into()
}
fn requires_argument(&self) -> bool {
false
}
fn complete_argument(
&self,
_query: String,
_cancellation_flag: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
fn run(
self: Arc<Self>,
_argument: Option<&str>,
_workspace: WeakView<Workspace>,
_delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let store = PromptStore::global(cx);
cx.background_executor().spawn(async move {
let store = store.await?;
let prompts = store.default_prompt_metadata();
let mut text = String::new();
writeln!(text, "Default Prompt:").unwrap();
for prompt in prompts {
if let Some(title) = prompt.title {
writeln!(text, "/prompt {}", title).unwrap();
}
}
text.pop();
Ok(SlashCommandOutput {
sections: vec![SlashCommandOutputSection {
range: 0..text.len(),
render_placeholder: Arc::new(move |id, unfold, _cx| {
PromptPlaceholder {
title: "Default".into(),
id,
unfold,
}
.into_any_element()
}),
}],
text,
run_commands_in_text: true,
})
})
}
}

View File

@@ -0,0 +1,444 @@
use super::{SlashCommand, SlashCommandOutput};
use anyhow::{anyhow, Result};
use assistant_slash_command::SlashCommandOutputSection;
use fuzzy::{PathMatch, StringMatchCandidate};
use gpui::{svg, AppContext, Model, RenderOnce, Task, View, WeakView};
use language::{
Anchor, BufferSnapshot, DiagnosticEntry, DiagnosticSeverity, LspAdapterDelegate,
OffsetRangeExt, ToOffset,
};
use project::{DiagnosticSummary, PathMatchCandidateSet, Project};
use rope::Point;
use std::fmt::Write;
use std::{
ops::Range,
sync::{atomic::AtomicBool, Arc},
};
use ui::{prelude::*, ButtonLike, ElevationIndex};
use util::paths::PathMatcher;
use util::ResultExt;
use workspace::Workspace;
pub(crate) struct DiagnosticsCommand;
impl DiagnosticsCommand {
fn search_paths(
&self,
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: &View<Workspace>,
cx: &mut AppContext,
) -> Task<Vec<PathMatch>> {
if query.is_empty() {
let workspace = workspace.read(cx);
let entries = workspace.recent_navigation_history(Some(10), cx);
let path_prefix: Arc<str> = "".into();
Task::ready(
entries
.into_iter()
.map(|(entry, _)| PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: entry.worktree_id.to_usize(),
path: entry.path.clone(),
path_prefix: path_prefix.clone(),
distance_to_relative_ancestor: 0,
})
.collect(),
)
} else {
let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
let candidate_sets = worktrees
.into_iter()
.map(|worktree| {
let worktree = worktree.read(cx);
PathMatchCandidateSet {
snapshot: worktree.snapshot(),
include_ignored: worktree
.root_entry()
.map_or(false, |entry| entry.is_ignored),
include_root_name: false,
candidates: project::Candidates::Entries,
}
})
.collect::<Vec<_>>();
let executor = cx.background_executor().clone();
cx.foreground_executor().spawn(async move {
fuzzy::match_path_sets(
candidate_sets.as_slice(),
query.as_str(),
None,
false,
100,
&cancellation_flag,
executor,
)
.await
})
}
}
}
impl SlashCommand for DiagnosticsCommand {
fn name(&self) -> String {
"diagnostics".into()
}
fn description(&self) -> String {
"Insert diagnostics".into()
}
fn menu_text(&self) -> String {
"Insert Diagnostics".into()
}
fn requires_argument(&self) -> bool {
false
}
fn complete_argument(
&self,
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: Option<WeakView<Workspace>>,
cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
return Task::ready(Err(anyhow!("workspace was dropped")));
};
let query = query.split_whitespace().last().unwrap_or("").to_string();
let paths = self.search_paths(query.clone(), cancellation_flag.clone(), &workspace, cx);
let executor = cx.background_executor().clone();
cx.background_executor().spawn(async move {
let mut matches: Vec<String> = paths
.await
.into_iter()
.map(|path_match| {
format!(
"{}{}",
path_match.path_prefix,
path_match.path.to_string_lossy()
)
})
.collect();
matches.extend(
fuzzy::match_strings(
&Options::match_candidates_for_args(),
&query,
false,
10,
&cancellation_flag,
executor,
)
.await
.into_iter()
.map(|candidate| candidate.string),
);
Ok(matches)
})
}
fn run(
self: Arc<Self>,
argument: Option<&str>,
workspace: WeakView<Workspace>,
_delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow!("workspace was dropped")));
};
let options = Options::parse(argument);
let task = collect_diagnostics(workspace.read(cx).project().clone(), options, cx);
cx.spawn(move |_| async move {
let (text, sections) = task.await?;
Ok(SlashCommandOutput {
text,
sections: sections
.into_iter()
.map(|(range, placeholder_type)| SlashCommandOutputSection {
range,
render_placeholder: Arc::new(move |id, unfold, _cx| {
DiagnosticsPlaceholder {
id,
unfold,
placeholder_type: placeholder_type.clone(),
}
.into_any_element()
}),
})
.collect(),
run_commands_in_text: false,
})
})
}
}
#[derive(Default)]
struct Options {
include_warnings: bool,
path_matcher: Option<PathMatcher>,
}
const INCLUDE_WARNINGS_ARGUMENT: &str = "--include-warnings";
impl Options {
pub fn parse(arguments_line: Option<&str>) -> Self {
arguments_line
.map(|arguments_line| {
let args = arguments_line.split_whitespace().collect::<Vec<_>>();
let mut include_warnings = false;
let mut path_matcher = None;
for arg in args {
if arg == INCLUDE_WARNINGS_ARGUMENT {
include_warnings = true;
} else {
path_matcher = PathMatcher::new(arg).log_err();
}
}
Self {
include_warnings,
path_matcher,
}
})
.unwrap_or_default()
}
fn match_candidates_for_args() -> [StringMatchCandidate; 1] {
[StringMatchCandidate::new(
0,
INCLUDE_WARNINGS_ARGUMENT.to_string(),
)]
}
}
fn collect_diagnostics(
project: Model<Project>,
options: Options,
cx: &mut AppContext,
) -> Task<Result<(String, Vec<(Range<usize>, PlaceholderType)>)>> {
let header = if let Some(path_matcher) = &options.path_matcher {
format!("diagnostics: {}", path_matcher.source())
} else {
"diagnostics".to_string()
};
let project_handle = project.downgrade();
let diagnostic_summaries: Vec<_> = project.read(cx).diagnostic_summaries(false, cx).collect();
cx.spawn(|mut cx| async move {
let mut text = String::new();
writeln!(text, "{}", &header).unwrap();
let mut sections: Vec<(Range<usize>, PlaceholderType)> = Vec::new();
let mut project_summary = DiagnosticSummary::default();
for (project_path, _, summary) in diagnostic_summaries {
if let Some(path_matcher) = &options.path_matcher {
if !path_matcher.is_match(&project_path.path) {
continue;
}
}
project_summary.error_count += summary.error_count;
if options.include_warnings {
project_summary.warning_count += summary.warning_count;
} else if summary.error_count == 0 {
continue;
}
let last_end = text.len();
let file_path = project_path.path.to_string_lossy().to_string();
writeln!(&mut text, "{file_path}").unwrap();
if let Some(buffer) = project_handle
.update(&mut cx, |project, cx| project.open_buffer(project_path, cx))?
.await
.log_err()
{
collect_buffer_diagnostics(
&mut text,
&mut sections,
cx.read_model(&buffer, |buffer, _| buffer.snapshot())?,
options.include_warnings,
);
}
sections.push((
last_end..text.len().saturating_sub(1),
PlaceholderType::File(file_path),
))
}
sections.push((
0..text.len(),
PlaceholderType::Root(project_summary, header),
));
Ok((text, sections))
})
}
fn collect_buffer_diagnostics(
text: &mut String,
sections: &mut Vec<(Range<usize>, PlaceholderType)>,
snapshot: BufferSnapshot,
include_warnings: bool,
) {
for (_, group) in snapshot.diagnostic_groups(None) {
let entry = &group.entries[group.primary_ix];
collect_diagnostic(text, sections, entry, &snapshot, include_warnings)
}
}
fn collect_diagnostic(
text: &mut String,
sections: &mut Vec<(Range<usize>, PlaceholderType)>,
entry: &DiagnosticEntry<Anchor>,
snapshot: &BufferSnapshot,
include_warnings: bool,
) {
const EXCERPT_EXPANSION_SIZE: u32 = 2;
const MAX_MESSAGE_LENGTH: usize = 2000;
let ty = match entry.diagnostic.severity {
DiagnosticSeverity::WARNING => {
if !include_warnings {
return;
}
DiagnosticType::Warning
}
DiagnosticSeverity::ERROR => DiagnosticType::Error,
_ => return,
};
let prev_len = text.len();
let range = entry.range.to_point(snapshot);
let diagnostic_row_number = range.start.row + 1;
let start_row = range.start.row.saturating_sub(EXCERPT_EXPANSION_SIZE);
let end_row = (range.end.row + EXCERPT_EXPANSION_SIZE).min(snapshot.max_point().row) + 1;
let excerpt_range =
Point::new(start_row, 0).to_offset(&snapshot)..Point::new(end_row, 0).to_offset(&snapshot);
text.push_str("```");
if let Some(language_name) = snapshot.language().map(|l| l.code_fence_block_name()) {
text.push_str(&language_name);
}
text.push('\n');
let mut buffer_text = String::new();
for chunk in snapshot.text_for_range(excerpt_range) {
buffer_text.push_str(chunk);
}
for (i, line) in buffer_text.lines().enumerate() {
let line_number = start_row + i as u32 + 1;
writeln!(text, "{}", line).unwrap();
if line_number == diagnostic_row_number {
text.push_str("//");
let prev_len = text.len();
write!(text, " {}: ", ty.as_str()).unwrap();
let padding = text.len() - prev_len;
let message = util::truncate(&entry.diagnostic.message, MAX_MESSAGE_LENGTH)
.replace('\n', format!("\n//{:padding$}", "").as_str());
writeln!(text, "{message}").unwrap();
}
}
writeln!(text, "```").unwrap();
sections.push((
prev_len..text.len().saturating_sub(1),
PlaceholderType::Diagnostic(ty, entry.diagnostic.message.clone()),
))
}
#[derive(Clone)]
pub enum PlaceholderType {
Root(DiagnosticSummary, String),
File(String),
Diagnostic(DiagnosticType, String),
}
#[derive(Copy, Clone, IntoElement)]
pub enum DiagnosticType {
Warning,
Error,
}
impl DiagnosticType {
pub fn as_str(&self) -> &'static str {
match self {
DiagnosticType::Warning => "warning",
DiagnosticType::Error => "error",
}
}
}
#[derive(IntoElement)]
pub struct DiagnosticsPlaceholder {
pub id: ElementId,
pub placeholder_type: PlaceholderType,
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
}
impl RenderOnce for DiagnosticsPlaceholder {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
let unfold = self.unfold;
let (icon, content) = match self.placeholder_type {
PlaceholderType::Root(summary, title) => (
h_flex()
.w_full()
.gap_0p5()
.when(summary.error_count > 0, |this| {
this.child(DiagnosticType::Error)
.child(Label::new(summary.error_count.to_string()))
})
.when(summary.warning_count > 0, |this| {
this.child(DiagnosticType::Warning)
.child(Label::new(summary.warning_count.to_string()))
})
.into_any_element(),
Label::new(title),
),
PlaceholderType::File(file) => (
Icon::new(IconName::File).into_any_element(),
Label::new(file),
),
PlaceholderType::Diagnostic(diagnostic_type, message) => (
diagnostic_type.into_any_element(),
Label::new(message).single_line(),
),
};
ButtonLike::new(self.id)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ElevatedSurface)
.child(icon)
.child(content)
.on_click(move |_, cx| unfold(cx))
}
}
impl RenderOnce for DiagnosticType {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
svg()
.size(cx.text_style().font_size)
.flex_none()
.map(|icon| match self {
DiagnosticType::Error => icon
.path(IconName::XCircle.path())
.text_color(Color::Error.color(cx)),
DiagnosticType::Warning => icon
.path(IconName::ExclamationTriangle.path())
.text_color(Color::Warning.color(cx)),
})
}
}

View File

@@ -0,0 +1,188 @@
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use anyhow::{anyhow, bail, Context, Result};
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
use futures::AsyncReadExt;
use gpui::{AppContext, Task, WeakView};
use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
use http::{AsyncBody, HttpClient, HttpClientWithUrl};
use language::LspAdapterDelegate;
use ui::{prelude::*, ButtonLike, ElevationIndex};
use workspace::Workspace;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
enum ContentType {
Html,
Plaintext,
Json,
}
pub(crate) struct FetchSlashCommand;
impl FetchSlashCommand {
async fn build_message(http_client: Arc<HttpClientWithUrl>, url: &str) -> Result<String> {
let mut url = url.to_owned();
if !url.starts_with("https://") {
url = format!("https://{url}");
}
let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
let mut body = Vec::new();
response
.body_mut()
.read_to_end(&mut body)
.await
.context("error reading response body")?;
if response.status().is_client_error() {
let text = String::from_utf8_lossy(body.as_slice());
bail!(
"status error {}, response: {text:?}",
response.status().as_u16()
);
}
let Some(content_type) = response.headers().get("content-type") else {
bail!("missing Content-Type header");
};
let content_type = content_type
.to_str()
.context("invalid Content-Type header")?;
let content_type = match content_type {
"text/html" => ContentType::Html,
"text/plain" => ContentType::Plaintext,
"application/json" => ContentType::Json,
_ => ContentType::Html,
};
match content_type {
ContentType::Html => {
let mut handlers: Vec<TagHandler> = vec![
Rc::new(RefCell::new(markdown::ParagraphHandler)),
Rc::new(RefCell::new(markdown::HeadingHandler)),
Rc::new(RefCell::new(markdown::ListHandler)),
Rc::new(RefCell::new(markdown::TableHandler::new())),
Rc::new(RefCell::new(markdown::StyledTextHandler)),
];
if url.contains("wikipedia.org") {
use html_to_markdown::structure::wikipedia;
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
handlers.push(Rc::new(
RefCell::new(wikipedia::WikipediaCodeHandler::new()),
));
} else {
handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
}
convert_html_to_markdown(&body[..], &mut handlers)
}
ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
ContentType::Json => {
let json: serde_json::Value = serde_json::from_slice(&body)?;
Ok(format!(
"```json\n{}\n```",
serde_json::to_string_pretty(&json)?
))
}
}
}
}
impl SlashCommand for FetchSlashCommand {
fn name(&self) -> String {
"fetch".into()
}
fn description(&self) -> String {
"insert URL contents".into()
}
fn menu_text(&self) -> String {
"Insert fetched URL contents".into()
}
fn requires_argument(&self) -> bool {
true
}
fn complete_argument(
&self,
_query: String,
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
Task::ready(Ok(Vec::new()))
}
fn run(
self: Arc<Self>,
argument: Option<&str>,
workspace: WeakView<Workspace>,
_delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let Some(argument) = argument else {
return Task::ready(Err(anyhow!("missing URL")));
};
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow!("workspace was dropped")));
};
let http_client = workspace.read(cx).client().http_client();
let url = argument.to_string();
let text = cx.background_executor().spawn({
let url = url.clone();
async move { Self::build_message(http_client, &url).await }
});
let url = SharedString::from(url);
cx.foreground_executor().spawn(async move {
let text = text.await?;
let range = 0..text.len();
Ok(SlashCommandOutput {
text,
sections: vec![SlashCommandOutputSection {
range,
render_placeholder: Arc::new(move |id, unfold, _cx| {
FetchPlaceholder {
id,
unfold,
url: url.clone(),
}
.into_any_element()
}),
}],
run_commands_in_text: false,
})
})
}
}
#[derive(IntoElement)]
struct FetchPlaceholder {
pub id: ElementId,
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
pub url: SharedString,
}
impl RenderOnce for FetchPlaceholder {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
let unfold = self.unfold;
ButtonLike::new(self.id)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ElevatedSurface)
.child(Icon::new(IconName::AtSign))
.child(Label::new(format!("fetch {url}", url = self.url)))
.on_click(move |_, cx| unfold(cx))
}
}

View File

@@ -58,7 +58,7 @@ impl FileSlashCommand {
.root_entry()
.map_or(false, |entry| entry.is_ignored),
include_root_name: true,
directories_only: false,
candidates: project::Candidates::Files,
}
})
.collect::<Vec<_>>();
@@ -101,10 +101,10 @@ impl SlashCommand for FileSlashCommand {
&self,
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: WeakView<Workspace>,
workspace: Option<WeakView<Workspace>>,
cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
let Some(workspace) = workspace.upgrade() else {
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
return Task::ready(Err(anyhow!("workspace was dropped")));
};
@@ -187,6 +187,7 @@ impl SlashCommand for FileSlashCommand {
.into_any_element()
}),
}],
run_commands_in_text: false,
})
})
}

View File

@@ -0,0 +1,83 @@
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use anyhow::Result;
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
use chrono::{DateTime, Local};
use gpui::{AppContext, Task, WeakView};
use language::LspAdapterDelegate;
use ui::{prelude::*, ButtonLike, ElevationIndex};
use workspace::Workspace;
pub(crate) struct NowSlashCommand;
impl SlashCommand for NowSlashCommand {
fn name(&self) -> String {
"now".into()
}
fn description(&self) -> String {
"insert the current date and time".into()
}
fn menu_text(&self) -> String {
"Insert current date and time".into()
}
fn requires_argument(&self) -> bool {
false
}
fn complete_argument(
&self,
_query: String,
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
Task::ready(Ok(Vec::new()))
}
fn run(
self: Arc<Self>,
_argument: Option<&str>,
_workspace: WeakView<Workspace>,
_delegate: Arc<dyn LspAdapterDelegate>,
_cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let now = Local::now();
let text = format!("Today is {now}.", now = now.to_rfc3339());
let range = 0..text.len();
Task::ready(Ok(SlashCommandOutput {
text,
sections: vec![SlashCommandOutputSection {
range,
render_placeholder: Arc::new(move |id, unfold, _cx| {
NowPlaceholder { id, unfold, now }.into_any_element()
}),
}],
run_commands_in_text: false,
}))
}
}
#[derive(IntoElement)]
struct NowPlaceholder {
pub id: ElementId,
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
pub now: DateTime<Local>,
}
impl RenderOnce for NowPlaceholder {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
let unfold = self.unfold;
ButtonLike::new(self.id)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ElevatedSurface)
.child(Icon::new(IconName::CountdownTimer))
.child(Label::new(self.now.to_rfc3339()))
.on_click(move |_, cx| unfold(cx))
}
}

View File

@@ -105,7 +105,7 @@ impl SlashCommand for ProjectSlashCommand {
&self,
_query: String,
_cancel: Arc<AtomicBool>,
_workspace: WeakView<Workspace>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
@@ -148,6 +148,7 @@ impl SlashCommand for ProjectSlashCommand {
.into_any_element()
}),
}],
run_commands_in_text: false,
})
})
});

View File

@@ -1,23 +1,14 @@
use super::{SlashCommand, SlashCommandOutput};
use crate::prompts::PromptLibrary;
use crate::prompt_library::PromptStore;
use anyhow::{anyhow, Context, Result};
use assistant_slash_command::SlashCommandOutputSection;
use fuzzy::StringMatchCandidate;
use gpui::{AppContext, Task, WeakView};
use language::LspAdapterDelegate;
use std::sync::{atomic::AtomicBool, Arc};
use ui::{prelude::*, ButtonLike, ElevationIndex};
use workspace::Workspace;
pub(crate) struct PromptSlashCommand {
library: Arc<PromptLibrary>,
}
impl PromptSlashCommand {
pub fn new(library: Arc<PromptLibrary>) -> Self {
Self { library }
}
}
pub(crate) struct PromptSlashCommand;
impl SlashCommand for PromptSlashCommand {
fn name(&self) -> String {
@@ -39,31 +30,16 @@ impl SlashCommand for PromptSlashCommand {
fn complete_argument(
&self,
query: String,
cancellation_flag: Arc<AtomicBool>,
_workspace: WeakView<Workspace>,
_cancellation_flag: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
let library = self.library.clone();
let executor = cx.background_executor().clone();
let store = PromptStore::global(cx);
cx.background_executor().spawn(async move {
let candidates = library
.prompts()
let prompts = store.await?.search(query).await;
Ok(prompts
.into_iter()
.enumerate()
.map(|(ix, prompt)| StringMatchCandidate::new(ix, prompt.1.title().to_string()))
.collect::<Vec<_>>();
let matches = fuzzy::match_strings(
&candidates,
&query,
false,
100,
&cancellation_flag,
executor,
)
.await;
Ok(matches
.into_iter()
.map(|mat| candidates[mat.candidate_id].string.clone())
.filter_map(|prompt| Some(prompt.title?.to_string()))
.collect())
})
}
@@ -79,19 +55,17 @@ impl SlashCommand for PromptSlashCommand {
return Task::ready(Err(anyhow!("missing prompt name")));
};
let library = self.library.clone();
let store = PromptStore::global(cx);
let title = SharedString::from(title.to_string());
let prompt = cx.background_executor().spawn({
let title = title.clone();
async move {
let prompt = library
.prompts()
.into_iter()
.map(|prompt| (prompt.1.title(), prompt))
.find(|(t, _)| t == &title)
.with_context(|| format!("no prompt found with title {:?}", title))?
.1;
anyhow::Ok(prompt.1.body())
let store = store.await?;
let prompt_id = store
.id_for_title(&title)
.with_context(|| format!("no prompt found with title {:?}", title))?;
let body = store.load(prompt_id).await?;
anyhow::Ok(body)
}
});
cx.foreground_executor().spawn(async move {
@@ -102,16 +76,35 @@ impl SlashCommand for PromptSlashCommand {
sections: vec![SlashCommandOutputSection {
range,
render_placeholder: Arc::new(move |id, unfold, _cx| {
ButtonLike::new(id)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ElevatedSurface)
.child(Icon::new(IconName::Library))
.child(Label::new(title.clone()))
.on_click(move |_, cx| unfold(cx))
.into_any_element()
PromptPlaceholder {
id,
unfold,
title: title.clone(),
}
.into_any_element()
}),
}],
run_commands_in_text: true,
})
})
}
}
#[derive(IntoElement)]
pub struct PromptPlaceholder {
pub title: SharedString,
pub id: ElementId,
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
}
impl RenderOnce for PromptPlaceholder {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
let unfold = self.unfold;
ButtonLike::new(self.id)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ElevatedSurface)
.child(Icon::new(IconName::Library))
.child(Label::new(self.title))
.on_click(move |_, cx| unfold(cx))
}
}

View File

@@ -1,24 +1,55 @@
use std::path::Path;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use anyhow::{anyhow, bail, Context, Result};
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
use fs::Fs;
use futures::AsyncReadExt;
use gpui::{AppContext, Task, WeakView};
use gpui::{AppContext, Model, Task, WeakView};
use http::{AsyncBody, HttpClient, HttpClientWithUrl};
use language::LspAdapterDelegate;
use rustdoc_to_markdown::convert_rustdoc_to_markdown;
use project::{Project, ProjectPath};
use rustdoc::{convert_rustdoc_to_markdown, RustdocStore};
use rustdoc::{CrateName, LocalProvider};
use ui::{prelude::*, ButtonLike, ElevationIndex};
use util::{maybe, ResultExt};
use workspace::Workspace;
#[derive(Debug, Clone, Copy)]
enum RustdocSource {
/// The docs were sourced from local `cargo doc` output.
Local,
/// The docs were sourced from `docs.rs`.
DocsDotRs,
}
pub(crate) struct RustdocSlashCommand;
impl RustdocSlashCommand {
async fn build_message(
fs: Arc<dyn Fs>,
http_client: Arc<HttpClientWithUrl>,
crate_name: String,
crate_name: CrateName,
module_path: Vec<String>,
) -> Result<String> {
path_to_cargo_toml: Option<&Path>,
) -> Result<(RustdocSource, String)> {
let cargo_workspace_root = path_to_cargo_toml.and_then(|path| path.parent());
if let Some(cargo_workspace_root) = cargo_workspace_root {
let mut local_cargo_doc_path = cargo_workspace_root.join("target/doc");
local_cargo_doc_path.push(crate_name.as_ref());
if !module_path.is_empty() {
local_cargo_doc_path.push(module_path.join("/"));
}
local_cargo_doc_path.push("index.html");
if let Ok(contents) = fs.load(&local_cargo_doc_path).await {
let (markdown, _items) = convert_rustdoc_to_markdown(contents.as_bytes())?;
return Ok((RustdocSource::Local, markdown));
}
}
let version = "latest";
let path = format!(
"{crate_name}/{version}/{crate_name}/{module_path}",
@@ -48,7 +79,22 @@ impl RustdocSlashCommand {
);
}
convert_rustdoc_to_markdown(&body[..])
let (markdown, _items) = convert_rustdoc_to_markdown(&body[..])?;
Ok((RustdocSource::DocsDotRs, markdown))
}
fn path_to_cargo_toml(project: Model<Project>, cx: &mut AppContext) -> Option<Arc<Path>> {
let worktree = project.read(cx).worktrees().next()?;
let worktree = worktree.read(cx);
let entry = worktree.entry_for_path("Cargo.toml")?;
let path = ProjectPath {
worktree_id: worktree.id(),
path: entry.path.clone(),
};
Some(Arc::from(
project.read(cx).absolute_path(&path, cx)?.as_path(),
))
}
}
@@ -71,12 +117,41 @@ impl SlashCommand for RustdocSlashCommand {
fn complete_argument(
&self,
_query: String,
query: String,
_cancel: Arc<AtomicBool>,
_workspace: WeakView<Workspace>,
_cx: &mut AppContext,
workspace: Option<WeakView<Workspace>>,
cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
Task::ready(Ok(Vec::new()))
let index_provider_deps = maybe!({
let workspace = workspace.ok_or_else(|| anyhow!("no workspace"))?;
let workspace = workspace
.upgrade()
.ok_or_else(|| anyhow!("workspace was dropped"))?;
let project = workspace.read(cx).project().clone();
let fs = project.read(cx).fs().clone();
let cargo_workspace_root = Self::path_to_cargo_toml(project, cx)
.and_then(|path| path.parent().map(|path| path.to_path_buf()))
.ok_or_else(|| anyhow!("no Cargo workspace root found"))?;
anyhow::Ok((fs, cargo_workspace_root))
});
let store = RustdocStore::global(cx);
cx.background_executor().spawn(async move {
if let Some((crate_name, rest)) = query.split_once(':') {
if rest.is_empty() {
if let Some((fs, cargo_workspace_root)) = index_provider_deps.log_err() {
let provider = Box::new(LocalProvider::new(fs, cargo_workspace_root));
// We don't need to hold onto this task, as the `RustdocStore` will hold it
// until it completes.
let _ = store.clone().index(crate_name.into(), provider);
}
}
}
let items = store.search(query).await;
Ok(items)
})
}
fn run(
@@ -93,31 +168,52 @@ impl SlashCommand for RustdocSlashCommand {
return Task::ready(Err(anyhow!("workspace was dropped")));
};
let project = workspace.read(cx).project().clone();
let fs = project.read(cx).fs().clone();
let http_client = workspace.read(cx).client().http_client();
let path_to_cargo_toml = Self::path_to_cargo_toml(project, cx);
let mut path_components = argument.split("::");
let crate_name = match path_components
.next()
.ok_or_else(|| anyhow!("missing crate name"))
{
Ok(crate_name) => crate_name.to_string(),
Ok(crate_name) => CrateName::from(crate_name),
Err(err) => return Task::ready(Err(err)),
};
let module_path = path_components.map(ToString::to_string).collect::<Vec<_>>();
let item_path = path_components.map(ToString::to_string).collect::<Vec<_>>();
let text = cx.background_executor().spawn({
let rustdoc_store = RustdocStore::global(cx);
let crate_name = crate_name.clone();
let module_path = module_path.clone();
async move { Self::build_message(http_client, crate_name, module_path).await }
let item_path = item_path.clone();
async move {
let item_docs = rustdoc_store
.load(crate_name.clone(), Some(item_path.join("::")))
.await;
if let Ok(item_docs) = item_docs {
anyhow::Ok((RustdocSource::Local, item_docs.docs().to_owned()))
} else {
Self::build_message(
fs,
http_client,
crate_name,
item_path,
path_to_cargo_toml.as_deref(),
)
.await
}
}
});
let crate_name = SharedString::from(crate_name);
let module_path = if module_path.is_empty() {
let module_path = if item_path.is_empty() {
None
} else {
Some(SharedString::from(module_path.join("::")))
Some(SharedString::from(item_path.join("::")))
};
cx.foreground_executor().spawn(async move {
let text = text.await?;
let (source, text) = text.await?;
let range = 0..text.len();
Ok(SlashCommandOutput {
text,
@@ -127,12 +223,14 @@ impl SlashCommand for RustdocSlashCommand {
RustdocPlaceholder {
id,
unfold,
source,
crate_name: crate_name.clone(),
module_path: module_path.clone(),
}
.into_any_element()
}),
}],
run_commands_in_text: false,
})
})
}
@@ -142,7 +240,8 @@ impl SlashCommand for RustdocSlashCommand {
struct RustdocPlaceholder {
pub id: ElementId,
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
pub crate_name: SharedString,
pub source: RustdocSource,
pub crate_name: CrateName,
pub module_path: Option<SharedString>,
}
@@ -159,7 +258,13 @@ impl RenderOnce for RustdocPlaceholder {
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ElevatedSurface)
.child(Icon::new(IconName::FileRust))
.child(Label::new(format!("rustdoc: {crate_path}")))
.child(Label::new(format!(
"rustdoc ({source}): {crate_path}",
source = match self.source {
RustdocSource::Local => "local",
RustdocSource::DocsDotRs => "docs.rs",
}
)))
.on_click(move |_, cx| unfold(cx))
}
}

View File

@@ -47,7 +47,7 @@ impl SlashCommand for SearchSlashCommand {
&self,
_query: String,
_cancel: Arc<AtomicBool>,
_workspace: WeakView<Workspace>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
Task::ready(Ok(Vec::new()))
@@ -181,7 +181,11 @@ impl SlashCommand for SearchSlashCommand {
}),
});
SlashCommandOutput { text, sections }
SlashCommandOutput {
text,
sections,
run_commands_in_text: false,
}
})
.await;

View File

@@ -32,7 +32,7 @@ impl SlashCommand for TabsSlashCommand {
&self,
_query: String,
_cancel: Arc<std::sync::atomic::AtomicBool>,
_workspace: WeakView<Workspace>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
@@ -109,7 +109,11 @@ impl SlashCommand for TabsSlashCommand {
});
}
Ok(SlashCommandOutput { text, sections })
Ok(SlashCommandOutput {
text,
sections,
run_commands_in_text: false,
})
}),
Err(error) => Task::ready(Err(error)),
}

View File

@@ -25,7 +25,7 @@ pub trait SlashCommand: 'static + Send + Sync {
&self,
query: String,
cancel: Arc<AtomicBool>,
workspace: WeakView<Workspace>,
workspace: Option<WeakView<Workspace>>,
cx: &mut AppContext,
) -> Task<Result<Vec<String>>>;
fn requires_argument(&self) -> bool;
@@ -52,6 +52,7 @@ pub type RenderFoldPlaceholder = Arc<
pub struct SlashCommandOutput {
pub text: String,
pub sections: Vec<SlashCommandOutputSection<usize>>,
pub run_commands_in_text: bool,
}
#[derive(Clone)]

View File

@@ -23,7 +23,10 @@ use smol::{fs::File, process::Command};
use http::{HttpClient, HttpClientWithUrl};
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
use std::{
env::consts::{ARCH, OS},
env::{
self,
consts::{ARCH, OS},
},
ffi::OsString,
path::PathBuf,
sync::Arc,
@@ -138,20 +141,24 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
let auto_updater = cx.new_model(|cx| {
let updater = AutoUpdater::new(version, http_client);
let mut update_subscription = AutoUpdateSetting::get_global(cx)
.0
.then(|| updater.start_polling(cx));
if option_env!("ZED_UPDATE_EXPLANATION").is_none()
&& env::var("ZED_UPDATE_EXPLANATION").is_err()
{
let mut update_subscription = AutoUpdateSetting::get_global(cx)
.0
.then(|| updater.start_polling(cx));
cx.observe_global::<SettingsStore>(move |updater, cx| {
if AutoUpdateSetting::get_global(cx).0 {
if update_subscription.is_none() {
update_subscription = Some(updater.start_polling(cx))
cx.observe_global::<SettingsStore>(move |updater, cx| {
if AutoUpdateSetting::get_global(cx).0 {
if update_subscription.is_none() {
update_subscription = Some(updater.start_polling(cx))
}
} else {
update_subscription.take();
}
} else {
update_subscription.take();
}
})
.detach();
})
.detach();
}
updater
});
@@ -159,6 +166,26 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
}
pub fn check(_: &Check, cx: &mut WindowContext) {
if let Some(message) = option_env!("ZED_UPDATE_EXPLANATION") {
drop(cx.prompt(
gpui::PromptLevel::Info,
"Zed was installed via a package manager.",
Some(message),
&["Ok"],
));
return;
}
if let Some(message) = env::var("ZED_UPDATE_EXPLANATION").ok() {
drop(cx.prompt(
gpui::PromptLevel::Info,
"Zed was installed via a package manager.",
Some(&message),
&["Ok"],
));
return;
}
if let Some(updater) = AutoUpdater::get(cx) {
updater.update(cx, |updater, cx| updater.poll(cx));
} else {
@@ -342,16 +369,6 @@ impl AutoUpdater {
}
async fn update(this: Model<Self>, mut cx: AsyncAppContext) -> Result<()> {
// Skip auto-update for flatpaks
#[cfg(target_os = "linux")]
if matches!(std::env::var("ZED_IS_FLATPAK_INSTALL"), Ok(_)) {
this.update(&mut cx, |this, cx| {
this.status = AutoUpdateStatus::Idle;
cx.notify();
})?;
return Ok(());
}
let (client, current_version) = this.read_with(&cx, |this, _| {
(this.http_client.clone(), this.current_version)
})?;
@@ -509,7 +526,7 @@ async fn install_release_linux(
cx: &AsyncAppContext,
) -> Result<()> {
let channel = cx.update(|cx| ReleaseChannel::global(cx).dev_name())?;
let home_dir = PathBuf::from(std::env::var("HOME").context("no HOME env var set")?);
let home_dir = PathBuf::from(env::var("HOME").context("no HOME env var set")?);
let extracted = temp_dir.path().join("zed");
fs::create_dir_all(&extracted)

View File

@@ -267,7 +267,7 @@ impl Room {
.await
{
Ok(()) => Ok(room),
Err(error) => Err(anyhow!("room creation failed: {:?}", error)),
Err(error) => Err(error.context("room creation failed")),
}
})
}

View File

@@ -3,7 +3,7 @@ use crate::channel_chat::ChannelChatEvent;
use super::*;
use client::{test::FakeServer, Client, UserStore};
use clock::FakeSystemClock;
use gpui::{AppContext, Context, Model, TestAppContext};
use gpui::{AppContext, Context, Model, SemanticVersion, TestAppContext};
use http::FakeHttpClient;
use rpc::proto::{self};
use settings::SettingsStore;
@@ -340,7 +340,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
fn init_test(cx: &mut AppContext) -> Model<ChannelStore> {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
release_channel::init("0.0.0", cx);
release_channel::init(SemanticVersion::default(), cx);
client::init_settings(cx);
let clock = Arc::new(FakeSystemClock::default());

View File

@@ -19,7 +19,6 @@ path = "src/main.rs"
[dependencies]
anyhow.workspace = true
clap.workspace = true
libc.workspace = true
ipc-channel = "0.18"
once_cell.workspace = true
release_channel.workspace = true

View File

@@ -161,10 +161,7 @@ mod linux {
env,
ffi::OsString,
io,
os::{
linux::net::SocketAddrExt,
unix::net::{SocketAddr, UnixDatagram},
},
os::unix::net::{SocketAddr, UnixDatagram},
path::{Path, PathBuf},
process::{self, ExitStatus},
thread,
@@ -175,6 +172,7 @@ mod linux {
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
use fork::Fork;
use once_cell::sync::Lazy;
use util::paths;
use crate::{Detect, InstalledApp};
@@ -191,14 +189,15 @@ mod linux {
let cli = env::current_exe()?;
let dir = cli
.parent()
.and_then(Path::parent)
.ok_or_else(|| anyhow!("no parent path for cli"))?;
match dir.join("zed").canonicalize() {
match dir.join("libexec").join("zed-editor").canonicalize() {
Ok(path) => Ok(path),
// development builds have Zed capitalized
Err(e) => match dir.join("Zed").canonicalize() {
Ok(path) => Ok(path),
Err(_) => Err(e),
// In development cli and zed are in the ./target/ directory together
Err(e) => match cli.parent().unwrap().join("zed").canonicalize() {
Ok(path) if path != cli => Ok(path),
_ => Err(e),
},
}
}?;
@@ -222,12 +221,9 @@ mod linux {
}
fn launch(&self, ipc_url: String) -> anyhow::Result<()> {
let uid: u32 = unsafe { libc::getuid() };
let sock_addr =
SocketAddr::from_abstract_name(format!("zed-{}-{}", *RELEASE_CHANNEL, uid))?;
let sock_path = paths::SUPPORT_DIR.join(format!("zed-{}.sock", *RELEASE_CHANNEL));
let sock = UnixDatagram::unbound()?;
if sock.connect_addr(&sock_addr).is_err() {
if sock.connect(&sock_path).is_err() {
self.boot_background(ipc_url)?;
} else {
sock.send(ipc_url.as_bytes())?;
@@ -254,10 +250,8 @@ mod linux {
eprintln!("failed to setsid: {}", std::io::Error::last_os_error());
process::exit(1);
}
if std::env::var("ZED_KEEP_FD").is_err() {
if let Err(_) = fork::close_fd() {
eprintln!("failed to close_fd: {}", std::io::Error::last_os_error());
}
if let Err(_) = fork::close_fd() {
eprintln!("failed to close_fd: {}", std::io::Error::last_os_error());
}
let error =
exec::execvp(path.clone(), &[path.as_os_str(), &OsString::from(ipc_url)]);
@@ -315,7 +309,7 @@ mod flatpak {
if let Some(flatpak_dir) = get_flatpak_dir() {
let mut args = vec!["/usr/bin/flatpak-spawn".into(), "--host".into()];
args.append(&mut get_xdg_env_args());
args.push("--env=ZED_IS_FLATPAK_INSTALL=1".into());
args.push("--env=ZED_UPDATE_EXPLANATION=Please use flatpak to update zed".into());
args.push(
format!(
"--env={EXTRA_LIB_ENV_NAME}={}",
@@ -333,7 +327,7 @@ mod flatpak {
if !is_app_location_set {
args.push("--zed".into());
args.push(flatpak_dir.join("bin").join("zed-app").into());
args.push(flatpak_dir.join("libexec").join("zed-editor").into());
}
let error = exec::execvp("/usr/bin/flatpak-spawn", args);
@@ -347,8 +341,8 @@ mod flatpak {
&& env::var("FLATPAK_ID").map_or(false, |id| id.starts_with("dev.zed.Zed"))
{
if args.zed.is_none() {
args.zed = Some("/app/bin/zed-app".into());
env::set_var("ZED_IS_FLATPAK_INSTALL", "1");
args.zed = Some("/app/libexec/zed-editor".into());
env::set_var("ZED_UPDATE_EXPLANATION", "Please use flatpak to update zed");
}
}
args

View File

@@ -13,17 +13,25 @@ path = "src/client.rs"
doctest = false
[features]
default = ["static-libraries"]
test-support = ["clock/test-support", "collections/test-support", "gpui/test-support", "rpc/test-support"]
static-libraries = ["async-native-tls", "isahc"]
dynamic-libraries = []
# Revert the changes
# Reintroduce them, but with a feature flag.
[dependencies]
anyhow.workspace = true
async-recursion = "0.3"
async-tungstenite = { version = "0.16", features = ["async-std", "async-native-tls"] }
async-native-tls = { version = "0.5.0", features = ["vendored"] }
async-native-tls = { version = "0.5.0", features = ["vendored"], optional = true}
isahc = { workspace = true, features = ["static-curl"], optional = true}
chrono = { workspace = true, features = ["serde"] }
clock.workspace = true
collections.workspace = true
feature_flags.workspace = true
fs.workspace = true
futures.workspace = true
gpui.workspace = true
http.workspace = true
@@ -60,9 +68,8 @@ settings = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] }
http = { workspace = true, features = ["test-support"] }
[target.'cfg(target_os = "linux")'.dependencies]
async-native-tls = {"version" = "0.5.0", features = ["vendored"]}
# This is an indirect dependency of async-tungstenite that is included
# here so we can vendor libssl with the feature flag.
[package.metadata.cargo-machete]
ignored = ["async-native-tls"]
[target.'cfg(target_os = "windows")'.dependencies]
windows.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
cocoa.workspace = true

View File

@@ -1429,6 +1429,31 @@ impl Client {
}
}
pub fn request_dynamic(
&self,
envelope: proto::Envelope,
request_type: &'static str,
) -> impl Future<Output = Result<proto::Envelope>> {
let client_id = self.id();
log::debug!(
"rpc request start. client_id:{}. name:{}",
client_id,
request_type
);
let response = self
.connection_id()
.map(|conn_id| self.peer.request_dynamic(conn_id, envelope, request_type));
async move {
let response = response?.await;
log::debug!(
"rpc request finish. client_id:{}. name:{}",
client_id,
request_type
);
Ok(response?.0)
}
}
fn respond<T: RequestMessage>(&self, receipt: Receipt<T>, response: T::Response) -> Result<()> {
log::debug!("rpc respond. client_id:{}. name:{}", self.id(), T::NAME);
self.peer.respond(receipt, response)
@@ -1704,6 +1729,7 @@ mod tests {
use gpui::{BackgroundExecutor, Context, TestAppContext};
use http::FakeHttpClient;
use parking_lot::Mutex;
use proto::TypedEnvelope;
use settings::SettingsStore;
use std::future;

View File

@@ -4,7 +4,7 @@ use crate::{ChannelId, TelemetrySettings};
use chrono::{DateTime, Utc};
use clock::SystemClock;
use futures::Future;
use gpui::{AppContext, AppMetadata, BackgroundExecutor, Task};
use gpui::{AppContext, BackgroundExecutor, Task};
use http::{self, HttpClient, HttpClientWithUrl, Method};
use once_cell::sync::Lazy;
use parking_lot::Mutex;
@@ -39,7 +39,6 @@ struct TelemetryState {
installation_id: Option<Arc<str>>, // Per app installation (different for dev, nightly, preview, and stable)
session_id: Option<String>, // Per app launch
release_channel: Option<&'static str>,
app_metadata: AppMetadata,
architecture: &'static str,
events_queue: Vec<EventWrapper>,
flush_events_task: Option<Task<()>>,
@@ -48,6 +47,10 @@ struct TelemetryState {
first_event_date_time: Option<DateTime<Utc>>,
event_coalescer: EventCoalescer,
max_queue_size: usize,
os_name: String,
app_version: String,
os_version: Option<String>,
}
#[cfg(debug_assertions)]
@@ -71,6 +74,87 @@ static ZED_CLIENT_CHECKSUM_SEED: Lazy<Option<Vec<u8>>> = Lazy::new(|| {
})
});
pub fn os_name() -> String {
#[cfg(target_os = "macos")]
{
"macOS".to_string()
}
#[cfg(target_os = "linux")]
{
format!("Linux {}", gpui::guess_compositor())
}
#[cfg(target_os = "windows")]
{
"Windows".to_string()
}
}
/// Note: This might do blocking IO! Only call from background threads
pub fn os_version() -> String {
#[cfg(target_os = "macos")]
{
use cocoa::base::nil;
use cocoa::foundation::NSProcessInfo;
unsafe {
let process_info = cocoa::foundation::NSProcessInfo::processInfo(nil);
let version = process_info.operatingSystemVersion();
gpui::SemanticVersion::new(
version.majorVersion as usize,
version.minorVersion as usize,
version.patchVersion as usize,
)
.to_string()
}
}
#[cfg(target_os = "linux")]
{
use std::path::Path;
let content = if let Ok(file) = std::fs::read_to_string(&Path::new("/etc/os-release")) {
file
} else if let Ok(file) = std::fs::read_to_string(&Path::new("/usr/lib/os-release")) {
file
} else {
log::error!("Failed to load /etc/os-release, /usr/lib/os-release");
"".to_string()
};
let mut name = "unknown".to_string();
let mut version = "unknown".to_string();
for line in content.lines() {
if line.starts_with("ID=") {
name = line.trim_start_matches("ID=").trim_matches('"').to_string();
}
if line.starts_with("VERSION_ID=") {
version = line
.trim_start_matches("VERSION_ID=")
.trim_matches('"')
.to_string();
}
}
format!("{} {}", name, version)
}
#[cfg(target_os = "windows")]
{
let mut info = unsafe { std::mem::zeroed() };
let status = unsafe { windows::Wdk::System::SystemServices::RtlGetVersion(&mut info) };
if status.is_ok() {
gpui::SemanticVersion::new(
info.dwMajorVersion as _,
info.dwMinorVersion as _,
info.dwBuildNumber as _,
)
.to_string()
} else {
"unknown".to_string()
}
}
}
impl Telemetry {
pub fn new(
clock: Arc<dyn SystemClock>,
@@ -84,7 +168,6 @@ impl Telemetry {
let state = Arc::new(Mutex::new(TelemetryState {
settings: *TelemetrySettings::get_global(cx),
app_metadata: cx.app_metadata(),
architecture: env::consts::ARCH,
release_channel,
installation_id: None,
@@ -97,6 +180,10 @@ impl Telemetry {
first_event_date_time: None,
event_coalescer: EventCoalescer::new(clock.clone()),
max_queue_size: MAX_QUEUE_LEN,
os_version: None,
os_name: os_name(),
app_version: release_channel::AppVersion::global(cx).to_string(),
}));
#[cfg(not(debug_assertions))]
@@ -168,6 +255,9 @@ impl Telemetry {
let mut state = self.state.lock();
state.installation_id = installation_id.map(|id| id.into());
state.session_id = Some(session_id);
state.app_version = release_channel::AppVersion::global(cx).to_string();
state.os_name = os_name();
drop(state);
let this = self.clone();
@@ -445,20 +535,14 @@ impl Telemetry {
{
let state = this.state.lock();
let request_body = EventRequestBody {
installation_id: state.installation_id.as_deref().map(Into::into),
session_id: state.session_id.clone(),
is_staff: state.is_staff,
app_version: state
.app_metadata
.app_version
.unwrap_or_default()
.to_string(),
os_name: state.app_metadata.os_name.to_string(),
os_version: state
.app_metadata
.os_version
.map(|version| version.to_string()),
app_version: state.app_version.clone(),
os_name: state.os_name.clone(),
os_version: state.os_version.clone(),
architecture: state.architecture.to_string(),
release_channel: state.release_channel.map(Into::into),

View File

@@ -96,6 +96,7 @@ node_runtime.workspace = true
notifications = { workspace = true, features = ["test-support"] }
pretty_assertions.workspace = true
project = { workspace = true, features = ["test-support"] }
recent_projects = { workspace = true }
release_channel.workspace = true
dev_server_projects.workspace = true
rpc = { workspace = true, features = ["test-support"] }
@@ -107,4 +108,5 @@ theme.workspace = true
unindent.workspace = true
util.workspace = true
workspace = { workspace = true, features = ["test-support"] }
worktree = { workspace = true, features = ["test-support"] }
headless.workspace = true

View File

@@ -308,6 +308,14 @@ pub async fn post_panic(
.map_err(|_| Error::Http(StatusCode::BAD_REQUEST, "invalid json".into()))?;
let panic = report.panic;
// better OS reporting for linux (because linux is hard):
// - Remove os_version/app_version/os_name from the gpui platform trait
// - Move platform processing data into client/telemetry
// - Duplicate some small code in macOS platform for a version check
// - Add GPUI API for reporting the selected platform integration
// - macos-blade, macos-metal, linux-X11, linux-headless
// if cfg(macos( { "Macos" } else { "Linux-{cx.compositor_name()"} ))
tracing::error!(
service = "client",
version = %panic.app_version,
@@ -716,25 +724,25 @@ impl EditorEventRow {
#[derive(Serialize, Debug, clickhouse::Row)]
pub struct InlineCompletionEventRow {
pub installation_id: String,
pub provider: String,
pub suggestion_accepted: bool,
pub app_version: String,
pub file_extension: String,
pub os_name: String,
pub os_version: String,
pub release_channel: String,
pub signed_in: bool,
installation_id: String,
provider: String,
suggestion_accepted: bool,
app_version: String,
file_extension: String,
os_name: String,
os_version: String,
release_channel: String,
signed_in: bool,
#[serde(serialize_with = "serialize_country_code")]
pub country_code: String,
pub region_code: String,
pub city: String,
pub time: i64,
pub is_staff: Option<bool>,
pub session_id: Option<String>,
pub major: Option<i32>,
pub minor: Option<i32>,
pub patch: Option<i32>,
country_code: String,
region_code: String,
city: String,
time: i64,
is_staff: Option<bool>,
session_id: Option<String>,
major: Option<i32>,
minor: Option<i32>,
patch: Option<i32>,
}
impl InlineCompletionEventRow {
@@ -780,6 +788,8 @@ pub struct CallEventRow {
minor: Option<i32>,
patch: Option<i32>,
release_channel: String,
os_name: String,
os_version: String,
// ClientEventBase
installation_id: String,
@@ -810,6 +820,8 @@ impl CallEventRow {
minor: semver.map(|v| v.minor() as i32),
patch: semver.map(|v| v.patch() as i32),
release_channel: body.release_channel.clone().unwrap_or_default(),
os_name: body.os_name.clone(),
os_version: body.os_version.clone().unwrap_or_default(),
installation_id: body.installation_id.clone().unwrap_or_default(),
session_id: body.session_id.clone(),
is_staff: body.is_staff,
@@ -829,6 +841,8 @@ pub struct AssistantEventRow {
minor: Option<i32>,
patch: Option<i32>,
release_channel: String,
os_name: String,
os_version: String,
// ClientEventBase
installation_id: Option<String>,
@@ -861,6 +875,8 @@ impl AssistantEventRow {
minor: semver.map(|v| v.minor() as i32),
patch: semver.map(|v| v.patch() as i32),
release_channel: body.release_channel.clone().unwrap_or_default(),
os_name: body.os_name.clone(),
os_version: body.os_version.clone().unwrap_or_default(),
installation_id: body.installation_id.clone(),
session_id: body.session_id.clone(),
is_staff: body.is_staff,
@@ -878,18 +894,20 @@ impl AssistantEventRow {
#[derive(Debug, clickhouse::Row, Serialize)]
pub struct CpuEventRow {
pub installation_id: Option<String>,
pub is_staff: Option<bool>,
pub usage_as_percentage: f32,
pub core_count: u32,
pub app_version: String,
pub release_channel: String,
pub time: i64,
pub session_id: Option<String>,
installation_id: Option<String>,
is_staff: Option<bool>,
usage_as_percentage: f32,
core_count: u32,
app_version: String,
release_channel: String,
os_name: String,
os_version: String,
time: i64,
session_id: Option<String>,
// pub normalized_cpu_usage: f64, MATERIALIZED
pub major: Option<i32>,
pub minor: Option<i32>,
pub patch: Option<i32>,
major: Option<i32>,
minor: Option<i32>,
patch: Option<i32>,
}
impl CpuEventRow {
@@ -909,6 +927,8 @@ impl CpuEventRow {
minor: semver.map(|v| v.minor() as i32),
patch: semver.map(|v| v.patch() as i32),
release_channel: body.release_channel.clone().unwrap_or_default(),
os_name: body.os_name.clone(),
os_version: body.os_version.clone().unwrap_or_default(),
installation_id: body.installation_id.clone(),
session_id: body.session_id.clone(),
is_staff: body.is_staff,
@@ -927,6 +947,8 @@ pub struct MemoryEventRow {
minor: Option<i32>,
patch: Option<i32>,
release_channel: String,
os_name: String,
os_version: String,
// ClientEventBase
installation_id: Option<String>,
@@ -956,6 +978,8 @@ impl MemoryEventRow {
minor: semver.map(|v| v.minor() as i32),
patch: semver.map(|v| v.patch() as i32),
release_channel: body.release_channel.clone().unwrap_or_default(),
os_name: body.os_name.clone(),
os_version: body.os_version.clone().unwrap_or_default(),
installation_id: body.installation_id.clone(),
session_id: body.session_id.clone(),
is_staff: body.is_staff,
@@ -974,6 +998,8 @@ pub struct AppEventRow {
minor: Option<i32>,
patch: Option<i32>,
release_channel: String,
os_name: String,
os_version: String,
// ClientEventBase
installation_id: Option<String>,
@@ -1002,6 +1028,8 @@ impl AppEventRow {
minor: semver.map(|v| v.minor() as i32),
patch: semver.map(|v| v.patch() as i32),
release_channel: body.release_channel.clone().unwrap_or_default(),
os_name: body.os_name.clone(),
os_version: body.os_version.clone().unwrap_or_default(),
installation_id: body.installation_id.clone(),
session_id: body.session_id.clone(),
is_staff: body.is_staff,
@@ -1019,6 +1047,8 @@ pub struct SettingEventRow {
minor: Option<i32>,
patch: Option<i32>,
release_channel: String,
os_name: String,
os_version: String,
// ClientEventBase
installation_id: Option<String>,
@@ -1047,6 +1077,8 @@ impl SettingEventRow {
minor: semver.map(|v| v.minor() as i32),
patch: semver.map(|v| v.patch() as i32),
release_channel: body.release_channel.clone().unwrap_or_default(),
os_name: body.os_name.clone(),
os_version: body.os_version.clone().unwrap_or_default(),
installation_id: body.installation_id.clone(),
session_id: body.session_id.clone(),
is_staff: body.is_staff,
@@ -1065,6 +1097,8 @@ pub struct ExtensionEventRow {
minor: Option<i32>,
patch: Option<i32>,
release_channel: String,
os_name: String,
os_version: String,
// ClientEventBase
installation_id: Option<String>,
@@ -1098,6 +1132,8 @@ impl ExtensionEventRow {
minor: semver.map(|v| v.minor() as i32),
patch: semver.map(|v| v.patch() as i32),
release_channel: body.release_channel.clone().unwrap_or_default(),
os_name: body.os_name.clone(),
os_version: body.os_version.clone().unwrap_or_default(),
installation_id: body.installation_id.clone(),
session_id: body.session_id.clone(),
is_staff: body.is_staff,
@@ -1127,6 +1163,8 @@ pub struct EditEventRow {
minor: Option<i32>,
patch: Option<i32>,
release_channel: String,
os_name: String,
os_version: String,
// ClientEventBase
installation_id: Option<String>,
@@ -1162,6 +1200,8 @@ impl EditEventRow {
minor: semver.map(|v| v.minor() as i32),
patch: semver.map(|v| v.patch() as i32),
release_channel: body.release_channel.clone().unwrap_or_default(),
os_name: body.os_name.clone(),
os_version: body.os_version.clone().unwrap_or_default(),
installation_id: body.installation_id.clone(),
session_id: body.session_id.clone(),
is_staff: body.is_staff,
@@ -1181,6 +1221,8 @@ pub struct ActionEventRow {
minor: Option<i32>,
patch: Option<i32>,
release_channel: String,
os_name: String,
os_version: String,
// ClientEventBase
installation_id: Option<String>,
@@ -1211,6 +1253,8 @@ impl ActionEventRow {
minor: semver.map(|v| v.minor() as i32),
patch: semver.map(|v| v.patch() as i32),
release_channel: body.release_channel.clone().unwrap_or_default(),
os_name: body.os_name.clone(),
os_version: body.os_version.clone().unwrap_or_default(),
installation_id: body.installation_id.clone(),
session_id: body.session_id.clone(),
is_staff: body.is_staff,

View File

@@ -239,61 +239,74 @@ async fn fetch_extensions_from_blob_store(
) -> anyhow::Result<()> {
log::info!("fetching extensions from blob store");
let list = blob_store_client
.list_objects()
.bucket(blob_store_bucket)
.prefix("extensions/")
.send()
.await?;
let mut next_marker = None;
let mut published_versions = HashMap::<String, Vec<String>>::default();
let objects = list.contents.unwrap_or_default();
loop {
let list = blob_store_client
.list_objects()
.bucket(blob_store_bucket)
.prefix("extensions/")
.set_marker(next_marker.clone())
.send()
.await?;
let objects = list.contents.unwrap_or_default();
log::info!("fetched {} object(s) from blob store", objects.len());
let mut published_versions = HashMap::<&str, Vec<&str>>::default();
for object in &objects {
let Some(key) = object.key.as_ref() else {
continue;
};
let mut parts = key.split('/');
let Some(_) = parts.next().filter(|part| *part == "extensions") else {
continue;
};
let Some(extension_id) = parts.next() else {
continue;
};
let Some(version) = parts.next() else {
continue;
};
if parts.next() == Some("manifest.json") {
published_versions
.entry(extension_id)
.or_default()
.push(version);
for object in &objects {
let Some(key) = object.key.as_ref() else {
continue;
};
let mut parts = key.split('/');
let Some(_) = parts.next().filter(|part| *part == "extensions") else {
continue;
};
let Some(extension_id) = parts.next() else {
continue;
};
let Some(version) = parts.next() else {
continue;
};
if parts.next() == Some("manifest.json") {
published_versions
.entry(extension_id.to_owned())
.or_default()
.push(version.to_owned());
}
}
if let (Some(true), Some(last_object)) = (list.is_truncated, objects.last()) {
next_marker.clone_from(&last_object.key);
} else {
break;
}
}
log::info!("found {} published extensions", published_versions.len());
let known_versions = app_state.db.get_known_extension_versions().await?;
let mut new_versions = HashMap::<&str, Vec<NewExtensionVersion>>::default();
let empty = Vec::new();
for (extension_id, published_versions) in published_versions {
for (extension_id, published_versions) in &published_versions {
let known_versions = known_versions.get(extension_id).unwrap_or(&empty);
for published_version in published_versions {
if known_versions
.binary_search_by_key(&published_version, String::as_str)
.binary_search_by_key(&published_version, |known_version| known_version)
.is_err()
{
if let Some(extension) = fetch_extension_manifest(
blob_store_client,
blob_store_bucket,
extension_id,
published_version,
&extension_id,
&published_version,
)
.await
.log_err()
{
new_versions
.entry(extension_id)
.entry(&extension_id)
.or_default()
.push(extension);
}

View File

@@ -277,7 +277,7 @@ mod test {
#[gpui::test]
async fn test_verify_access_token(cx: &mut gpui::TestAppContext) {
let test_db = crate::db::TestDb::postgres(cx.executor().clone());
let test_db = crate::db::TestDb::sqlite(cx.executor().clone());
let db = test_db.db();
let user = db

View File

@@ -2,6 +2,8 @@ mod buffer_tests;
mod channel_tests;
mod contributor_tests;
mod db_tests;
// we only run postgres tests on macos right now
#[cfg(target_os = "macos")]
mod embedding_tests;
mod extension_tests;
mod feature_flag_tests;
@@ -108,6 +110,7 @@ impl TestDb {
#[macro_export]
macro_rules! test_both_dbs {
($test_name:ident, $postgres_test_name:ident, $sqlite_test_name:ident) => {
#[cfg(target_os = "macos")]
#[gpui::test]
async fn $postgres_test_name(cx: &mut gpui::TestAppContext) {
let test_db = $crate::db::TestDb::postgres(cx.executor().clone());

View File

@@ -1,9 +1,7 @@
use super::*;
use crate::test_both_dbs;
use gpui::TestAppContext;
use pretty_assertions::{assert_eq, assert_ne};
use std::sync::Arc;
use tests::TestDb;
test_both_dbs!(
test_get_users,
@@ -564,9 +562,10 @@ fn test_fuzzy_like_string() {
assert_eq!(Database::fuzzy_like_string(" z "), "%z%");
}
#[cfg(target = "macos")]
#[gpui::test]
async fn test_fuzzy_search_users(cx: &mut TestAppContext) {
let test_db = TestDb::postgres(cx.executor());
async fn test_fuzzy_search_users(cx: &mut gpui::TestAppContext) {
let test_db = tests::TestDb::postgres(cx.executor());
let db = test_db.db();
for (i, github_login) in [
"California",

View File

@@ -545,6 +545,12 @@ impl Server {
.add_request_handler(user_handler(
forward_mutating_project_request::<proto::MultiLspQuery>,
))
.add_request_handler(user_handler(
forward_mutating_project_request::<proto::RestartLanguageServers>,
))
.add_request_handler(user_handler(
forward_mutating_project_request::<proto::LinkedEditingRange>,
))
.add_message_handler(create_buffer_for_peer)
.add_request_handler(update_buffer)
.add_message_handler(broadcast_project_message_from_host::<proto::RefreshInlayHints>)

View File

@@ -68,6 +68,7 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC
assert_eq!(projects.len(), 1);
assert_eq!(projects[0].path, "/remote");
workspace::join_dev_server_project(
projects[0].id,
projects[0].project_id.unwrap(),
client.app_state.clone(),
None,
@@ -207,6 +208,7 @@ async fn create_dev_server_project(
assert_eq!(projects.len(), 1);
assert_eq!(projects[0].path, "/remote");
workspace::join_dev_server_project(
projects[0].id,
projects[0].project_id.unwrap(),
client_app_state,
None,
@@ -491,6 +493,7 @@ async fn test_dev_server_reconnect(
.update(cx2, |store, cx| {
let projects = store.dev_server_projects();
workspace::join_dev_server_project(
projects[0].id,
projects[0].project_id.unwrap(),
client2.app_state.clone(),
None,
@@ -572,7 +575,8 @@ async fn test_save_as_remote(cx1: &mut gpui::TestAppContext, cx2: &mut gpui::Tes
let title = remote_workspace
.update(&mut cx, |ws, cx| {
ws.active_item(cx).unwrap().tab_description(0, &cx).unwrap()
let active_item = ws.active_item(cx).unwrap();
active_item.tab_description(0, &cx).unwrap()
})
.unwrap();

View File

@@ -30,6 +30,7 @@ use project::{
project_settings::{InlineBlameSettings, ProjectSettings},
SERVER_PROGRESS_DEBOUNCE_TIMEOUT,
};
use recent_projects::disconnected_overlay::DisconnectedOverlay;
use rpc::RECEIVE_TIMEOUT;
use serde_json::json;
use settings::SettingsStore;
@@ -59,6 +60,7 @@ async fn test_host_disconnect(
.await;
cx_b.update(editor::init);
cx_b.update(recent_projects::init);
client_a
.fs()
@@ -83,7 +85,7 @@ async fn test_host_disconnect(
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
cx_a.background_executor.run_until_parked();
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer()));
let workspace_b = cx_b
.add_window(|cx| Workspace::new(None, project_b.clone(), client_b.app_state.clone(), cx));
@@ -120,14 +122,13 @@ async fn test_host_disconnect(
project_b.read_with(cx_b, |project, _| project.is_read_only());
assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
assert!(worktree_a.read_with(cx_a, |tree, _| !tree.has_update_observer()));
// Ensure client B's edited state is reset and that the whole window is blurred.
workspace_b
.update(cx_b, |workspace, cx| {
assert_eq!(cx.focused(), None);
assert!(!workspace.is_edited())
assert!(workspace.active_modal::<DisconnectedOverlay>(cx).is_some());
assert!(!workspace.is_edited());
})
.unwrap();
@@ -343,7 +344,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
.handle_request::<lsp::request::Completion, _, _>(|params, _| async move {
assert_eq!(
params.text_document_position.text_document.uri,
lsp::Url::from_file_path("/a/main.rs").unwrap(),
lsp::Uri::from_file_path("/a/main.rs").unwrap().into(),
);
assert_eq!(
params.text_document_position.position,
@@ -460,7 +461,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
.handle_request::<lsp::request::Completion, _, _>(|params, _| async move {
assert_eq!(
params.text_document_position.text_document.uri,
lsp::Url::from_file_path("/a/main.rs").unwrap(),
lsp::Uri::from_file_path("/a/main.rs").unwrap().into(),
);
assert_eq!(
params.text_document_position.position,
@@ -584,7 +585,7 @@ async fn test_collaborating_with_code_actions(
.handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path("/a/main.rs").unwrap(),
lsp::Uri::from_file_path("/a/main.rs").unwrap().into(),
);
assert_eq!(params.range.start, lsp::Position::new(0, 0));
assert_eq!(params.range.end, lsp::Position::new(0, 0));
@@ -606,7 +607,7 @@ async fn test_collaborating_with_code_actions(
.handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path("/a/main.rs").unwrap(),
lsp::Uri::from_file_path("/a/main.rs").unwrap().into(),
);
assert_eq!(params.range.start, lsp::Position::new(1, 31));
assert_eq!(params.range.end, lsp::Position::new(1, 31));
@@ -618,7 +619,7 @@ async fn test_collaborating_with_code_actions(
changes: Some(
[
(
lsp::Url::from_file_path("/a/main.rs").unwrap(),
lsp::Uri::from_file_path("/a/main.rs").unwrap().into(),
vec![lsp::TextEdit::new(
lsp::Range::new(
lsp::Position::new(1, 22),
@@ -628,7 +629,7 @@ async fn test_collaborating_with_code_actions(
)],
),
(
lsp::Url::from_file_path("/a/other.rs").unwrap(),
lsp::Uri::from_file_path("/a/other.rs").unwrap().into(),
vec![lsp::TextEdit::new(
lsp::Range::new(
lsp::Position::new(0, 0),
@@ -688,7 +689,7 @@ async fn test_collaborating_with_code_actions(
changes: Some(
[
(
lsp::Url::from_file_path("/a/main.rs").unwrap(),
lsp::Uri::from_file_path("/a/main.rs").unwrap().into(),
vec![lsp::TextEdit::new(
lsp::Range::new(
lsp::Position::new(1, 22),
@@ -698,7 +699,7 @@ async fn test_collaborating_with_code_actions(
)],
),
(
lsp::Url::from_file_path("/a/other.rs").unwrap(),
lsp::Uri::from_file_path("/a/other.rs").unwrap().into(),
vec![lsp::TextEdit::new(
lsp::Range::new(
lsp::Position::new(0, 0),
@@ -896,14 +897,14 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
changes: Some(
[
(
lsp::Url::from_file_path("/dir/one.rs").unwrap(),
lsp::Uri::from_file_path("/dir/one.rs").unwrap().into(),
vec![lsp::TextEdit::new(
lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
"THREE".to_string(),
)],
),
(
lsp::Url::from_file_path("/dir/two.rs").unwrap(),
lsp::Uri::from_file_path("/dir/two.rs").unwrap().into(),
vec![
lsp::TextEdit::new(
lsp::Range::new(
@@ -1312,7 +1313,7 @@ async fn test_on_input_format_from_host_to_guest(
|params, _| async move {
assert_eq!(
params.text_document_position.text_document.uri,
lsp::Url::from_file_path("/a/main.rs").unwrap(),
lsp::Uri::from_file_path("/a/main.rs").unwrap().into(),
);
assert_eq!(
params.text_document_position.position,
@@ -1440,7 +1441,7 @@ async fn test_on_input_format_from_guest_to_host(
.handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
assert_eq!(
params.text_document_position.text_document.uri,
lsp::Url::from_file_path("/a/main.rs").unwrap(),
lsp::Uri::from_file_path("/a/main.rs").unwrap().into(),
);
assert_eq!(
params.text_document_position.position,
@@ -1609,7 +1610,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path("/a/main.rs").unwrap(),
lsp::Uri::from_file_path("/a/main.rs").unwrap().into(),
);
let edits_made = task_edits_made.load(atomic::Ordering::Acquire);
Ok(Some(vec![lsp::InlayHint {
@@ -1872,7 +1873,7 @@ async fn test_inlay_hint_refresh_is_forwarded(
async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path("/a/main.rs").unwrap(),
lsp::Uri::from_file_path("/a/main.rs").unwrap().into(),
);
let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
let character = if other_hints { 0 } else { 2 };

View File

@@ -1378,7 +1378,7 @@ async fn test_unshare_project(
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
executor.run_until_parked();
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer()));
project_b
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
@@ -1403,7 +1403,7 @@ async fn test_unshare_project(
.unwrap();
executor.run_until_parked();
assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
assert!(worktree_a.read_with(cx_a, |tree, _| !tree.has_update_observer()));
assert!(project_c.read_with(cx_c, |project, _| project.is_disconnected()));
@@ -1415,7 +1415,7 @@ async fn test_unshare_project(
let project_c2 = client_c.build_dev_server_project(project_id, cx_c).await;
executor.run_until_parked();
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer()));
project_c2
.update(cx_c, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
.await
@@ -1522,7 +1522,7 @@ async fn test_project_reconnect(
executor.run_until_parked();
let worktree1_id = worktree_a1.read_with(cx_a, |worktree, _| {
assert!(worktree.as_local().unwrap().is_shared());
assert!(worktree.has_update_observer());
worktree.id()
});
let (worktree_a2, _) = project_a1
@@ -1534,7 +1534,7 @@ async fn test_project_reconnect(
executor.run_until_parked();
let worktree2_id = worktree_a2.read_with(cx_a, |tree, _| {
assert!(tree.as_local().unwrap().is_shared());
assert!(tree.has_update_observer());
tree.id()
});
executor.run_until_parked();
@@ -1567,9 +1567,7 @@ async fn test_project_reconnect(
assert_eq!(project.collaborators().len(), 1);
});
worktree_a1.read_with(cx_a, |tree, _| {
assert!(tree.as_local().unwrap().is_shared())
});
worktree_a1.read_with(cx_a, |tree, _| assert!(tree.has_update_observer()));
// While client A is disconnected, add and remove files from client A's project.
client_a
@@ -1611,7 +1609,7 @@ async fn test_project_reconnect(
.await;
let worktree3_id = worktree_a3.read_with(cx_a, |tree, _| {
assert!(!tree.as_local().unwrap().is_shared());
assert!(!tree.has_update_observer());
tree.id()
});
executor.run_until_parked();
@@ -1634,7 +1632,7 @@ async fn test_project_reconnect(
project_a1.read_with(cx_a, |project, cx| {
assert!(project.is_shared());
assert!(worktree_a1.read(cx).as_local().unwrap().is_shared());
assert!(worktree_a1.read(cx).has_update_observer());
assert_eq!(
worktree_a1
.read(cx)
@@ -1652,7 +1650,7 @@ async fn test_project_reconnect(
"subdir2/i.txt"
]
);
assert!(worktree_a3.read(cx).as_local().unwrap().is_shared());
assert!(worktree_a3.read(cx).has_update_observer());
assert_eq!(
worktree_a3
.read(cx)
@@ -1733,7 +1731,7 @@ async fn test_project_reconnect(
executor.run_until_parked();
let worktree4_id = worktree_a4.read_with(cx_a, |tree, _| {
assert!(tree.as_local().unwrap().is_shared());
assert!(tree.has_update_observer());
tree.id()
});
project_a1.update(cx_a, |project, cx| {
@@ -3022,7 +3020,6 @@ async fn test_fs_operations(
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
let worktree_a = project_a.read_with(cx_a, |project, _| project.worktrees().next().unwrap());
let worktree_b = project_b.read_with(cx_b, |project, _| project.worktrees().next().unwrap());
let entry = project_b
@@ -3031,6 +3028,7 @@ async fn test_fs_operations(
})
.await
.unwrap()
.to_included()
.unwrap();
worktree_a.read_with(cx_a, |worktree, _| {
@@ -3059,6 +3057,7 @@ async fn test_fs_operations(
})
.await
.unwrap()
.to_included()
.unwrap();
worktree_a.read_with(cx_a, |worktree, _| {
@@ -3087,6 +3086,7 @@ async fn test_fs_operations(
})
.await
.unwrap()
.to_included()
.unwrap();
worktree_a.read_with(cx_a, |worktree, _| {
@@ -3115,20 +3115,25 @@ async fn test_fs_operations(
})
.await
.unwrap()
.to_included()
.unwrap();
project_b
.update(cx_b, |project, cx| {
project.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
})
.await
.unwrap()
.to_included()
.unwrap();
project_b
.update(cx_b, |project, cx| {
project.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
})
.await
.unwrap()
.to_included()
.unwrap();
worktree_a.read_with(cx_a, |worktree, _| {
@@ -3892,7 +3897,7 @@ async fn test_collaborating_with_diagnostics(
.await;
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
uri: lsp::Uri::from_file_path("/a/a.rs").unwrap().into(),
version: None,
diagnostics: vec![lsp::Diagnostic {
severity: Some(lsp::DiagnosticSeverity::WARNING),
@@ -3912,7 +3917,7 @@ async fn test_collaborating_with_diagnostics(
.unwrap();
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
uri: lsp::Uri::from_file_path("/a/a.rs").unwrap().into(),
version: None,
diagnostics: vec![lsp::Diagnostic {
severity: Some(lsp::DiagnosticSeverity::ERROR),
@@ -3986,7 +3991,7 @@ async fn test_collaborating_with_diagnostics(
// Simulate a language server reporting more errors for a file.
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
uri: lsp::Uri::from_file_path("/a/a.rs").unwrap().into(),
version: None,
diagnostics: vec![
lsp::Diagnostic {
@@ -4080,7 +4085,7 @@ async fn test_collaborating_with_diagnostics(
// Simulate a language server reporting no errors for a file.
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
uri: lsp::Uri::from_file_path("/a/a.rs").unwrap().into(),
version: None,
diagnostics: vec![],
},
@@ -4184,7 +4189,9 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
for file_name in file_names {
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(Path::new("/test").join(file_name)).unwrap(),
uri: lsp::Uri::from_file_path(Path::new("/test").join(file_name))
.unwrap()
.into(),
version: None,
diagnostics: vec![lsp::Diagnostic {
severity: Some(lsp::DiagnosticSeverity::WARNING),
@@ -4602,7 +4609,7 @@ async fn test_definition(
fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Scalar(
lsp::Location::new(
lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
lsp::Uri::from_file_path("/root/dir-2/b.rs").unwrap().into(),
lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
),
)))
@@ -4631,7 +4638,7 @@ async fn test_definition(
fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Scalar(
lsp::Location::new(
lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
lsp::Uri::from_file_path("/root/dir-2/b.rs").unwrap().into(),
lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
),
)))
@@ -4667,7 +4674,7 @@ async fn test_definition(
);
Ok(Some(lsp::GotoDefinitionResponse::Scalar(
lsp::Location::new(
lsp::Url::from_file_path("/root/dir-2/c.rs").unwrap(),
lsp::Uri::from_file_path("/root/dir-2/c.rs").unwrap().into(),
lsp::Range::new(lsp::Position::new(0, 5), lsp::Position::new(0, 7)),
),
)))
@@ -4779,15 +4786,21 @@ async fn test_references(
lsp_response_tx
.unbounded_send(Ok(Some(vec![
lsp::Location {
uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
uri: lsp::Uri::from_file_path("/root/dir-1/two.rs")
.unwrap()
.into(),
range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
},
lsp::Location {
uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
uri: lsp::Uri::from_file_path("/root/dir-1/two.rs")
.unwrap()
.into(),
range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
},
lsp::Location {
uri: lsp::Url::from_file_path("/root/dir-2/three.rs").unwrap(),
uri: lsp::Uri::from_file_path("/root/dir-2/three.rs")
.unwrap()
.into(),
range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
},
])))
@@ -5287,7 +5300,9 @@ async fn test_project_symbols(
lsp::SymbolInformation {
name: "TWO".into(),
location: lsp::Location {
uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
uri: lsp::Uri::from_file_path("/code/crate-2/two.rs")
.unwrap()
.into(),
range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
},
kind: lsp::SymbolKind::CONSTANT,
@@ -5377,7 +5392,7 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Scalar(
lsp::Location::new(
lsp::Url::from_file_path("/root/b.rs").unwrap(),
lsp::Uri::from_file_path("/root/b.rs").unwrap().into(),
lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
),
)))

View File

@@ -14,7 +14,9 @@ use language::{
};
use lsp::FakeLanguageServer;
use pretty_assertions::assert_eq;
use project::{search::SearchQuery, Project, ProjectPath, SearchResult};
use project::{
search::SearchQuery, Project, ProjectPath, SearchResult, DEFAULT_COMPLETION_CONTEXT,
};
use rand::{
distributions::{Alphanumeric, DistString},
prelude::*,
@@ -303,7 +305,7 @@ impl RandomizedTest for ProjectCollaborationTest {
.filter(|worktree| {
let worktree = worktree.read(cx);
worktree.is_visible()
&& worktree.entries(false).any(|e| e.is_file())
&& worktree.entries(false, 0).any(|e| e.is_file())
&& worktree.root_entry().map_or(false, |e| e.is_dir())
})
.choose(rng)
@@ -425,14 +427,14 @@ impl RandomizedTest for ProjectCollaborationTest {
.filter(|worktree| {
let worktree = worktree.read(cx);
worktree.is_visible()
&& worktree.entries(false).any(|e| e.is_file())
&& worktree.entries(false, 0).any(|e| e.is_file())
})
.choose(rng)
});
let Some(worktree) = worktree else { continue };
let full_path = worktree.read_with(cx, |worktree, _| {
let entry = worktree
.entries(false)
.entries(false, 0)
.filter(|e| e.is_file())
.choose(rng)
.unwrap();
@@ -829,7 +831,7 @@ impl RandomizedTest for ProjectCollaborationTest {
.map_ok(|_| ())
.boxed(),
LspRequestKind::Completion => project
.completions(&buffer, offset, cx)
.completions(&buffer, offset, DEFAULT_COMPLETION_CONTEXT, cx)
.map_ok(|_| ())
.boxed(),
LspRequestKind::CodeAction => project
@@ -1099,7 +1101,7 @@ impl RandomizedTest for ProjectCollaborationTest {
files
.into_iter()
.map(|file| lsp::Location {
uri: lsp::Url::from_file_path(file).unwrap(),
uri: lsp::Uri::from_file_path(file).unwrap().into(),
range: Default::default(),
})
.collect(),
@@ -1204,8 +1206,8 @@ impl RandomizedTest for ProjectCollaborationTest {
guest_project.remote_id(),
);
assert_eq!(
guest_snapshot.entries(false).collect::<Vec<_>>(),
host_snapshot.entries(false).collect::<Vec<_>>(),
guest_snapshot.entries(false, 0).collect::<Vec<_>>(),
host_snapshot.entries(false, 0).collect::<Vec<_>>(),
"{} has different snapshot than the host for worktree {:?} ({:?}) and project {:?}",
client.username,
host_snapshot.abs_path(),

View File

@@ -69,7 +69,6 @@ struct TestPlan<T: RandomizedTest> {
pub struct UserTestPlan {
pub user_id: UserId,
pub username: String,
pub allow_client_reconnection: bool,
pub allow_client_disconnection: bool,
next_root_id: usize,
operation_ix: usize,
@@ -237,7 +236,6 @@ impl<T: RandomizedTest> TestPlan<T> {
next_root_id: 0,
operation_ix: 0,
allow_client_disconnection,
allow_client_reconnection,
});
}

View File

@@ -161,7 +161,7 @@ impl TestServer {
}
let settings = SettingsStore::test(cx);
cx.set_global(settings);
release_channel::init("0.0.0", cx);
release_channel::init(SemanticVersion::default(), cx);
client::init_settings(cx);
});
@@ -277,11 +277,7 @@ impl TestServer {
node_runtime: FakeNodeRuntime::new(),
});
let os_keymap = if cfg!(target_os = "linux") {
"keymaps/default-linux.json"
} else {
"keymaps/default-macos.json"
};
let os_keymap = "keymaps/default-macos.json";
cx.update(|cx| {
theme::init(theme::LoadThemes::JustBase, cx);
@@ -327,7 +323,7 @@ impl TestServer {
}
let settings = SettingsStore::test(cx);
cx.set_global(settings);
release_channel::init("0.0.0", cx);
release_channel::init(SemanticVersion::default(), cx);
client::init_settings(cx);
});
let (dev_server_id, _) = split_dev_server_token(&access_token).unwrap();

View File

@@ -228,11 +228,11 @@ impl ChannelView {
&self.editor,
move |this, _, e: &EditorEvent, cx| {
match e {
EditorEvent::Reparsed => {
EditorEvent::Reparsed(_) => {
this.focus_position_from_link(position.clone(), false, cx);
this._reparse_subscription.take();
}
EditorEvent::Edited | EditorEvent::SelectionsChanged { local: true } => {
EditorEvent::Edited { .. } | EditorEvent::SelectionsChanged { local: true } => {
this._reparse_subscription.take();
}
_ => {}

View File

@@ -1,4 +1,4 @@
use anyhow::Result;
use anyhow::{Context, Result};
use channel::{ChannelChat, ChannelStore, MessageParams};
use client::{UserId, UserStore};
use collections::HashSet;
@@ -46,6 +46,7 @@ impl CompletionProvider for MessageEditorCompletionProvider {
&self,
buffer: &Model<Buffer>,
buffer_position: language::Anchor,
_: editor::CompletionContext,
cx: &mut ViewContext<Editor>,
) -> Task<anyhow::Result<Vec<Completion>>> {
let Some(handle) = self.0.upgrade() else {
@@ -132,7 +133,7 @@ impl MessageEditor {
let markdown = language_registry.language_for_name("Markdown");
cx.spawn(|_, mut cx| async move {
let markdown = markdown.await?;
let markdown = markdown.await.context("failed to load Markdown language")?;
buffer.update(&mut cx, |buffer, cx| {
buffer.set_language(Some(markdown), cx)
})

View File

@@ -5,7 +5,7 @@ use client::{proto::PeerId, Client, User, UserStore};
use gpui::{
actions, canvas, div, point, px, Action, AnyElement, AppContext, Element, Hsla,
InteractiveElement, IntoElement, Model, ParentElement, Path, Render,
StatefulInteractiveElement, Styled, Subscription, View, ViewContext, VisualContext, WeakView,
StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext, WeakView,
};
use project::{Project, RepositoryEntry};
use recent_projects::RecentProjects;
@@ -17,7 +17,7 @@ use ui::{
ButtonStyle, ContextMenu, Icon, IconButton, IconName, Indicator, TintColor, TitleBar, Tooltip,
};
use util::ResultExt;
use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
use vcs_menu::{BranchList, OpenRecent as ToggleVcsMenu};
use workspace::{notifications::NotifyResultExt, Workspace};
const MAX_PROJECT_NAME_LENGTH: usize = 40;
@@ -58,6 +58,8 @@ impl Render for CollabTitlebarItem {
let project_id = self.project.read(cx).remote_id();
let workspace = self.workspace.upgrade();
let platform_supported = cfg!(target_os = "macos");
TitleBar::new("collab-titlebar", Box::new(workspace::CloseWindow))
// note: on windows titlebar behaviour is handled by the platform implementation
.when(cfg!(not(windows)), |this| {
@@ -243,7 +245,9 @@ impl Render for CollabTitlebarItem {
)
.tooltip(move |cx| {
Tooltip::text(
if is_muted {
if !platform_supported {
"Cannot share microphone"
} else if is_muted {
"Unmute microphone"
} else {
"Mute microphone"
@@ -253,7 +257,8 @@ impl Render for CollabTitlebarItem {
})
.style(ButtonStyle::Subtle)
.icon_size(IconSize::Small)
.selected(is_muted)
.selected(platform_supported && is_muted)
.disabled(!platform_supported)
.selected_style(ButtonStyle::Tinted(TintColor::Negative))
.on_click(move |_, cx| crate::toggle_mute(&Default::default(), cx)),
)
@@ -271,8 +276,11 @@ impl Render for CollabTitlebarItem {
.selected_style(ButtonStyle::Tinted(TintColor::Negative))
.icon_size(IconSize::Small)
.selected(is_deafened)
.disabled(!platform_supported)
.tooltip(move |cx| {
if can_use_microphone {
if !platform_supported {
Tooltip::text("Cannot share microphone", cx)
} else if can_use_microphone {
Tooltip::with_meta(
"Deafen Audio",
None,
@@ -291,10 +299,13 @@ impl Render for CollabTitlebarItem {
.style(ButtonStyle::Subtle)
.icon_size(IconSize::Small)
.selected(is_screen_sharing)
.disabled(!platform_supported)
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.tooltip(move |cx| {
Tooltip::text(
if is_screen_sharing {
if !platform_supported {
"Cannot share screen"
} else if is_screen_sharing {
"Stop Sharing Screen"
} else {
"Share Screen"
@@ -413,6 +424,17 @@ impl CollabTitlebarItem {
);
}
if self.project.read(cx).is_disconnected() {
return Some(
Button::new("disconnected", "Disconnected")
.disabled(true)
.color(Color::Disabled)
.style(ButtonStyle::Subtle)
.label_size(LabelSize::Small)
.into_any_element(),
);
}
let host = self.project.read(cx).host()?;
let host_user = self.user_store.read(cx).get_cached_user(host.user_id)?;
let participant_index = self
@@ -487,7 +509,7 @@ impl CollabTitlebarItem {
}))
}
pub fn render_project_branch(&self, cx: &mut ViewContext<Self>) -> Option<impl Element> {
pub fn render_project_branch(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
let entry = {
let mut names_and_branches =
self.project.read(cx).visible_worktrees(cx).map(|worktree| {
@@ -503,22 +525,23 @@ impl CollabTitlebarItem {
.and_then(RepositoryEntry::branch)
.map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?;
Some(
popover_menu("project_branch_trigger")
.trigger(
Button::new("project_branch_trigger", branch_name)
.color(Color::Muted)
.style(ButtonStyle::Subtle)
.label_size(LabelSize::Small)
.tooltip(move |cx| {
Tooltip::with_meta(
"Recent Branches",
Some(&ToggleVcsMenu),
"Local branches only",
cx,
)
}),
)
.menu(move |cx| Self::render_vcs_popover(workspace.clone(), cx)),
Button::new("project_branch_trigger", branch_name)
.color(Color::Muted)
.style(ButtonStyle::Subtle)
.label_size(LabelSize::Small)
.tooltip(move |cx| {
Tooltip::with_meta(
"Recent Branches",
Some(&ToggleVcsMenu),
"Local branches only",
cx,
)
})
.on_click(move |_, cx| {
let _ = workspace.update(cx, |this, cx| {
BranchList::open(this, &Default::default(), cx)
});
}),
)
}
@@ -650,16 +673,6 @@ impl CollabTitlebarItem {
.log_err();
}
pub fn render_vcs_popover(
workspace: View<Workspace>,
cx: &mut WindowContext<'_>,
) -> Option<View<BranchList>> {
let view = build_branch_list(workspace, cx).log_err()?;
let focus_handle = view.focus_handle(cx);
cx.focus(&focus_handle);
Some(view)
}
fn render_connection_status(
&self,
status: &client::Status,
@@ -695,7 +708,7 @@ impl CollabTitlebarItem {
.on_click(|_, cx| {
if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
if auto_updater.read(cx).status().is_updated() {
workspace::restart(&Default::default(), cx);
workspace::reload(&Default::default(), cx);
return;
}
}

View File

@@ -13,8 +13,8 @@ use call::{report_call_event_for_room, ActiveCall};
pub use collab_panel::CollabPanel;
pub use collab_titlebar_item::CollabTitlebarItem;
use gpui::{
actions, point, AppContext, DevicePixels, Pixels, PlatformDisplay, Size, Task,
WindowBackgroundAppearance, WindowBounds, WindowContext, WindowKind, WindowOptions,
actions, point, AppContext, Pixels, PlatformDisplay, Size, Task, WindowBackgroundAppearance,
WindowBounds, WindowContext, WindowKind, WindowOptions,
};
use panel_settings::MessageEditorSettings;
pub use panel_settings::{
@@ -22,6 +22,7 @@ pub use panel_settings::{
};
use release_channel::ReleaseChannel;
use settings::Settings;
use ui::px;
use workspace::{notifications::DetachAndPromptErr, AppState};
actions!(
@@ -96,22 +97,19 @@ pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
fn notification_window_options(
screen: Rc<dyn PlatformDisplay>,
window_size: Size<Pixels>,
size: Size<Pixels>,
cx: &AppContext,
) -> WindowOptions {
let notification_margin_width = DevicePixels::from(16);
let notification_margin_height = DevicePixels::from(-0) - DevicePixels::from(48);
let notification_margin_width = px(16.);
let notification_margin_height = px(-48.);
let screen_bounds = screen.bounds();
let size: Size<DevicePixels> = window_size.into();
let bounds = gpui::Bounds::<DevicePixels> {
origin: screen_bounds.upper_right()
let bounds = gpui::Bounds::<Pixels> {
origin: screen.bounds().upper_right()
- point(
size.width + notification_margin_width,
notification_margin_height,
),
size: window_size.into(),
size,
};
let app_id = ReleaseChannel::global(cx).app_id();

View File

@@ -8,6 +8,7 @@ use settings::Settings;
use std::sync::{Arc, Weak};
use theme::ThemeSettings;
use ui::{prelude::*, Button, Label};
use util::ResultExt;
use workspace::AppState;
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
@@ -27,16 +28,21 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
for screen in cx.displays() {
let options = notification_window_options(screen, window_size, cx);
let window = cx.open_window(options, |cx| {
cx.new_view(|_| {
ProjectSharedNotification::new(
owner.clone(),
*project_id,
worktree_root_names.clone(),
app_state.clone(),
)
let Some(window) = cx
.open_window(options, |cx| {
cx.new_view(|_| {
ProjectSharedNotification::new(
owner.clone(),
*project_id,
worktree_root_names.clone(),
app_state.clone(),
)
})
})
});
.log_err()
else {
continue;
};
notification_windows
.entry(*project_id)
.or_insert(Vec::new())

View File

@@ -1,19 +0,0 @@
[package]
name = "color"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[features]
default = []
[lib]
path = "src/color.rs"
doctest = true
[dependencies]
palette.workspace = true

View File

@@ -1,227 +0,0 @@
//! # Color
//!
//! The `color` crate provides a set utilities for working with colors. It is a wrapper around the [`palette`](https://docs.rs/palette) crate with some additional functionality.
//!
//! It is used to create a manipulate colors when building themes.
//!
//! === In development note ===
//!
//! This crate is meant to sit between gpui and the theme/ui for all the color related stuff.
//!
//! It could be folded into gpui, ui or theme potentially but for now we'll continue
//! to develop it in isolation.
//!
//! Once we have a good idea of the needs of the theme system and color in gpui in general I see 3 paths:
//! 1. Use `palette` (or another color library) directly in gpui and everywhere else, rather than rolling our own color system.
//! 2. Keep this crate as a thin wrapper around `palette` and use it everywhere except gpui, and convert to gpui's color system when needed.
//! 3. Build the needed functionality into gpui and keep using its color system everywhere.
//!
//! I'm leaning towards 2 in the short term and 1 in the long term, but we'll need to discuss it more.
//!
//! === End development note ===
use palette::{
blend::Blend, convert::FromColorUnclamped, encoding, rgb::Rgb, Clamp, Mix, Srgb, WithAlpha,
};
/// The types of blend modes supported
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum BlendMode {
/// Multiplies the colors, resulting in a darker color. This mode is useful for creating shadows.
Multiply,
/// Lightens the color by adding the source and destination colors. It results in a lighter color.
Screen,
/// Combines Multiply and Screen blend modes. Parts of the image that are lighter than 50% gray are lightened, and parts that are darker are darkened.
Overlay,
/// Selects the darker of the base or blend color as the resulting color. Useful for darkening images without affecting the overall contrast.
Darken,
/// Selects the lighter of the base or blend color as the resulting color. Useful for lightening images without affecting the overall contrast.
Lighten,
/// Brightens the base color to reflect the blend color. The result is a lightened image.
Dodge,
/// Darkens the base color to reflect the blend color. The result is a darkened image.
Burn,
/// Similar to Overlay, but with a stronger effect. Hard Light can either multiply or screen colors, depending on the blend color.
HardLight,
/// A softer version of Hard Light. Soft Light either darkens or lightens colors, depending on the blend color.
SoftLight,
/// Subtracts the darker of the two constituent colors from the lighter color. Difference mode is useful for creating more vivid colors.
Difference,
/// Similar to Difference, but with a lower contrast. Exclusion mode produces an effect similar to Difference but with less intensity.
Exclusion,
}
/// Converts a hexadecimal color string to a `palette::Hsla` color.
///
/// This function supports the following hex formats:
/// `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`.
pub fn hex_to_hsla(s: &str) -> Result<RGBAColor, String> {
let hex = s.trim_start_matches('#');
// Expand shorthand formats #RGB and #RGBA to #RRGGBB and #RRGGBBAA
let h = hex.as_bytes();
let arr: [u8; 8] = match h.len() {
// #RGB => #RRGGBBAA
3 => [h[0], h[0], h[1], h[1], h[2], h[2], b'f', b'f'],
// #RGBA => #RRGGBBAA
4 => [h[0], h[0], h[1], h[1], h[2], h[2], h[3], h[3]],
// #RRGGBB => #RRGGBBAA
6 => [h[0], h[1], h[2], h[3], h[4], h[5], b'f', b'f'],
// Already in #RRGGBBAA
8 => h.try_into().unwrap(),
_ => return Err("Invalid hexadecimal string length".to_string()),
};
let hex =
std::str::from_utf8(&arr).map_err(|_| format!("Invalid hexadecimal string: {}", s))?;
let hex_val =
u32::from_str_radix(hex, 16).map_err(|_| format!("Invalid hexadecimal string: {}", s))?;
Ok(RGBAColor {
r: ((hex_val >> 24) & 0xFF) as f32 / 255.0,
g: ((hex_val >> 16) & 0xFF) as f32 / 255.0,
b: ((hex_val >> 8) & 0xFF) as f32 / 255.0,
a: (hex_val & 0xFF) as f32 / 255.0,
})
}
// These derives implement to and from palette's color types.
#[derive(FromColorUnclamped, WithAlpha, Debug, Clone)]
#[palette(skip_derives(Rgb), rgb_standard = "encoding::Srgb")]
pub struct RGBAColor {
r: f32,
g: f32,
b: f32,
// Let Palette know this is our alpha channel.
#[palette(alpha)]
a: f32,
}
impl FromColorUnclamped<RGBAColor> for RGBAColor {
fn from_color_unclamped(color: RGBAColor) -> RGBAColor {
color
}
}
impl<S> FromColorUnclamped<Rgb<S, f32>> for RGBAColor
where
Srgb: FromColorUnclamped<Rgb<S, f32>>,
{
fn from_color_unclamped(color: Rgb<S, f32>) -> RGBAColor {
let srgb = Srgb::from_color_unclamped(color);
RGBAColor {
r: srgb.red,
g: srgb.green,
b: srgb.blue,
a: 1.0,
}
}
}
impl<S> FromColorUnclamped<RGBAColor> for Rgb<S, f32>
where
Rgb<S, f32>: FromColorUnclamped<Srgb>,
{
fn from_color_unclamped(color: RGBAColor) -> Self {
Self::from_color_unclamped(Srgb::new(color.r, color.g, color.b))
}
}
impl Clamp for RGBAColor {
fn clamp(self) -> Self {
RGBAColor {
r: self.r.min(1.0).max(0.0),
g: self.g.min(1.0).max(0.0),
b: self.b.min(1.0).max(0.0),
a: self.a.min(1.0).max(0.0),
}
}
}
impl RGBAColor {
/// Creates a new color from the given RGBA values.
///
/// This color can be used to convert to any [`palette::Color`] type.
pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
RGBAColor { r, g, b, a }
}
/// Returns a set of states for this color.
pub fn states(self, is_light: bool) -> ColorStates {
states_for_color(self, is_light)
}
/// Mixes this color with another [`palette::Hsl`] color at the given `mix_ratio`.
pub fn mixed(&self, other: RGBAColor, mix_ratio: f32) -> Self {
let srgb_self = Srgb::new(self.r, self.g, self.b);
let srgb_other = Srgb::new(other.r, other.g, other.b);
// Directly mix the colors as sRGB values
let mixed = srgb_self.mix(srgb_other, mix_ratio);
RGBAColor::from_color_unclamped(mixed)
}
pub fn blend(&self, other: RGBAColor, blend_mode: BlendMode) -> Self {
let srgb_self = Srgb::new(self.r, self.g, self.b);
let srgb_other = Srgb::new(other.r, other.g, other.b);
let blended = match blend_mode {
// replace hsl methods with the respective sRGB methods
BlendMode::Multiply => srgb_self.multiply(srgb_other),
_ => unimplemented!(),
};
Self {
r: blended.red,
g: blended.green,
b: blended.blue,
a: self.a,
}
}
}
/// A set of colors for different states of an element.
#[derive(Debug, Clone)]
pub struct ColorStates {
/// The default color.
pub default: RGBAColor,
/// The color when the mouse is hovering over the element.
pub hover: RGBAColor,
/// The color when the mouse button is held down on the element.
pub active: RGBAColor,
/// The color when the element is focused with the keyboard.
pub focused: RGBAColor,
/// The color when the element is disabled.
pub disabled: RGBAColor,
}
/// Returns a set of colors for different states of an element.
///
/// todo("This should take a theme and use appropriate colors from it")
pub fn states_for_color(color: RGBAColor, is_light: bool) -> ColorStates {
let adjustment_factor = if is_light { 0.1 } else { -0.1 };
let hover_adjustment = 1.0 - adjustment_factor;
let active_adjustment = 1.0 - 2.0 * adjustment_factor;
let focused_adjustment = 1.0 - 3.0 * adjustment_factor;
let disabled_adjustment = 1.0 - 4.0 * adjustment_factor;
let make_adjustment = |color: RGBAColor, adjustment: f32| -> RGBAColor {
// Adjust lightness for each state
// Note: Adjustment logic may differ; simplify as needed for sRGB
RGBAColor::new(
color.r * adjustment,
color.g * adjustment,
color.b * adjustment,
color.a,
)
};
let color = color.clamp();
ColorStates {
default: color.clone(),
hover: make_adjustment(color.clone(), hover_adjustment),
active: make_adjustment(color.clone(), active_adjustment),
focused: make_adjustment(color.clone(), focused_adjustment),
disabled: make_adjustment(color.clone(), disabled_adjustment),
}
}

View File

@@ -188,7 +188,7 @@ impl Status {
}
struct RegisteredBuffer {
uri: lsp::Url,
uri: lsp::RawUri,
language_id: String,
snapshot: BufferSnapshot,
snapshot_version: i32,
@@ -644,7 +644,7 @@ impl Copilot {
registered_buffers
.entry(buffer.entity_id())
.or_insert_with(|| {
let uri: lsp::Url = uri_for_buffer(buffer, cx);
let uri = uri_for_buffer(buffer, cx);
let language_id = id_for_language(buffer.read(cx).language());
let snapshot = buffer.read(cx).snapshot();
server
@@ -959,9 +959,9 @@ fn id_for_language(language: Option<&Arc<Language>>) -> String {
.unwrap_or_else(|| "plaintext".to_string())
}
fn uri_for_buffer(buffer: &Model<Buffer>, cx: &AppContext) -> lsp::Url {
fn uri_for_buffer(buffer: &Model<Buffer>, cx: &AppContext) -> lsp::RawUri {
if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
lsp::Url::from_file_path(file.abs_path(cx)).unwrap()
lsp::Uri::from_file_path(file.abs_path(cx)).unwrap().into()
} else {
format!("buffer://{}", buffer.entity_id()).parse().unwrap()
}
@@ -1042,18 +1042,18 @@ async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
#[cfg(test)]
mod tests {
use std::str::FromStr;
use super::*;
use gpui::TestAppContext;
use language::BufferId;
#[gpui::test(iterations = 10)]
async fn test_buffer_management(cx: &mut TestAppContext) {
let (copilot, mut lsp) = Copilot::fake(cx);
let buffer_1 = cx.new_model(|cx| Buffer::local("Hello", cx));
let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.entity_id().as_u64())
.parse()
.unwrap();
let buffer_1_uri =
lsp::RawUri::from_str(&format!("buffer://{}", buffer_1.entity_id().as_u64())).unwrap();
copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx));
assert_eq!(
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
@@ -1069,9 +1069,8 @@ mod tests {
);
let buffer_2 = cx.new_model(|cx| Buffer::local("Goodbye", cx));
let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.entity_id().as_u64())
.parse()
.unwrap();
let buffer_2_uri =
lsp::RawUri::from_str(&format!("buffer://{}", buffer_2.entity_id().as_u64())).unwrap();
copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx));
assert_eq!(
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
@@ -1120,7 +1119,9 @@ mod tests {
text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri),
}
);
let buffer_1_uri = lsp::Url::from_file_path("/root/child/buffer-1").unwrap();
let buffer_1_uri: lsp::RawUri = lsp::Uri::from_file_path("/root/child/buffer-1")
.unwrap()
.into();
assert_eq!(
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
.await,
@@ -1258,16 +1259,5 @@ mod tests {
fn load(&self, _: &AppContext) -> Task<Result<String>> {
unimplemented!()
}
fn buffer_reloaded(
&self,
_: BufferId,
_: &clock::Global,
_: language::LineEnding,
_: Option<std::time::SystemTime>,
_: &mut AppContext,
) {
unimplemented!()
}
}
}

View File

@@ -1121,7 +1121,10 @@ mod tests {
cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
let completions = completions.clone();
async move {
assert_eq!(params.text_document_position.text_document.uri, url.clone());
assert_eq!(
params.text_document_position.text_document.uri,
url.clone().into()
);
assert_eq!(
params.text_document_position.position,
complete_from_position

View File

@@ -102,7 +102,7 @@ pub struct GetCompletionsDocument {
pub tab_size: u32,
pub indent_size: u32,
pub insert_spaces: bool,
pub uri: lsp::Url,
pub uri: lsp::RawUri,
pub relative_path: String,
pub position: lsp::Position,
pub version: usize,

View File

@@ -867,10 +867,12 @@ fn compare_diagnostics(
snapshot: &language::BufferSnapshot,
) -> Ordering {
use language::ToOffset;
// The old diagnostics may point to a previously open Buffer for this file.
if !old.range.start.is_valid(snapshot) {
// The diagnostics may point to a previously open Buffer for this file.
if !old.range.start.is_valid(snapshot) || !new.range.start.is_valid(snapshot) {
return Ordering::Greater;
}
old.range
.start
.to_offset(snapshot)

View File

@@ -30,6 +30,7 @@ test-support = [
[dependencies]
aho-corasick = "1.1"
anyhow.workspace = true
assets.workspace = true
client.workspace = true
clock.workspace = true
collections.workspace = true

View File

@@ -125,6 +125,11 @@ pub struct ExpandExcerptsDown {
#[serde(default)]
pub(super) lines: u32,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct ShowCompletions {
#[serde(default)]
pub(super) trigger: Option<char>,
}
impl_actions!(
editor,
@@ -147,6 +152,7 @@ impl_actions!(
SelectToBeginningOfLine,
SelectToEndOfLine,
SelectUpByLines,
ShowCompletions,
ToggleCodeActions,
ToggleComments,
UnfoldAt,
@@ -274,7 +280,6 @@ gpui::actions!(
SelectToStartOfParagraph,
SelectUp,
ShowCharacterPalette,
ShowCompletions,
ShowInlineCompletion,
ShuffleLines,
SortLinesCaseInsensitive,
@@ -289,6 +294,7 @@ gpui::actions!(
ToggleLineNumbers,
ToggleIndentGuides,
ToggleSoftWrap,
ToggleTabBar,
Transpose,
Undo,
UndoSelection,

View File

@@ -29,13 +29,9 @@ impl DebouncedDelay {
let (sender, mut receiver) = oneshot::channel::<()>();
self.cancel_channel = Some(sender);
let previous_task = self.task.take();
drop(self.task.take());
self.task = Some(cx.spawn(move |model, mut cx| async move {
let mut timer = cx.background_executor().timer(delay).fuse();
if let Some(previous_task) = previous_task {
previous_task.await;
}
futures::select_biased! {
_ = receiver => return,
_ = timer => {}

View File

@@ -52,8 +52,14 @@ use multi_buffer::{
ToOffset, ToPoint,
};
use serde::Deserialize;
use std::ops::Add;
use std::{any::TypeId, borrow::Cow, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
use std::{
any::TypeId,
borrow::Cow,
fmt::Debug,
num::NonZeroU32,
ops::{Add, Range, Sub},
sync::Arc,
};
use sum_tree::{Bias, TreeMap};
use tab_map::{TabMap, TabSnapshot};
use text::LineIndent;
@@ -277,8 +283,55 @@ impl DisplayMap {
block_map.insert(blocks)
}
pub fn replace_blocks(&mut self, styles: HashMap<BlockId, RenderBlock>) {
self.block_map.replace(styles);
pub fn replace_blocks(
&mut self,
heights_and_renderers: HashMap<BlockId, (Option<u8>, RenderBlock)>,
cx: &mut ModelContext<Self>,
) {
//
// Note: previous implementation of `replace_blocks` simply called
// `self.block_map.replace(styles)` which just modified the render by replacing
// the `RenderBlock` with the new one.
//
// ```rust
// for block in &self.blocks {
// if let Some(render) = renderers.remove(&block.id) {
// *block.render.lock() = render;
// }
// }
// ```
//
// If height changes however, we need to update the tree. There's a performance
// cost to this, so we'll split the replace blocks into handling the old behavior
// directly and the new behavior separately.
//
//
let mut only_renderers = HashMap::<BlockId, RenderBlock>::default();
let mut full_replace = HashMap::<BlockId, (u8, RenderBlock)>::default();
for (id, (height, render)) in heights_and_renderers {
if let Some(height) = height {
full_replace.insert(id, (height, render));
} else {
only_renderers.insert(id, render);
}
}
self.block_map.replace_renderers(only_renderers);
if full_replace.is_empty() {
return;
}
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
let mut block_map = self.block_map.write(snapshot, edits);
block_map.replace(full_replace);
}
pub fn remove_blocks(&mut self, ids: HashSet<BlockId>, cx: &mut ModelContext<Self>) {
@@ -980,6 +1033,14 @@ impl Add for DisplayRow {
}
}
impl Sub for DisplayRow {
type Output = Self;
fn sub(self, other: Self) -> Self::Output {
DisplayRow(self.0 - other.0)
}
}
impl DisplayPoint {
pub fn new(row: DisplayRow, column: u32) -> Self {
Self(BlockPoint(Point::new(row.0, column)))
@@ -1050,14 +1111,11 @@ impl ToDisplayPoint for Anchor {
#[cfg(test)]
pub mod tests {
use super::*;
use crate::{
movement,
test::{editor_test_context::EditorTestContext, marked_display_snapshot},
};
use crate::{movement, test::marked_display_snapshot};
use gpui::{div, font, observe, px, AppContext, BorrowAppContext, Context, Element, Hsla};
use language::{
language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
Buffer, Language, LanguageConfig, LanguageMatcher, SelectionGoal,
Buffer, Language, LanguageConfig, LanguageMatcher,
};
use project::Project;
use rand::{prelude::*, Rng};
@@ -1332,6 +1390,7 @@ pub mod tests {
}
}
#[cfg(target_os = "macos")]
#[gpui::test(retries = 5)]
async fn test_soft_wraps(cx: &mut gpui::TestAppContext) {
cx.background_executor
@@ -1340,7 +1399,7 @@ pub mod tests {
init_test(cx, |_| {});
});
let mut cx = EditorTestContext::new(cx).await;
let mut cx = crate::test::editor_test_context::EditorTestContext::new(cx).await;
let editor = cx.editor.clone();
let window = cx.window;
@@ -1396,39 +1455,39 @@ pub mod tests {
movement::up(
&snapshot,
DisplayPoint::new(DisplayRow(1), 10),
SelectionGoal::None,
language::SelectionGoal::None,
false,
&text_layout_details,
),
(
DisplayPoint::new(DisplayRow(0), 7),
SelectionGoal::HorizontalPosition(x.0)
language::SelectionGoal::HorizontalPosition(x.0)
)
);
assert_eq!(
movement::down(
&snapshot,
DisplayPoint::new(DisplayRow(0), 7),
SelectionGoal::HorizontalPosition(x.0),
language::SelectionGoal::HorizontalPosition(x.0),
false,
&text_layout_details
),
(
DisplayPoint::new(DisplayRow(1), 10),
SelectionGoal::HorizontalPosition(x.0)
language::SelectionGoal::HorizontalPosition(x.0)
)
);
assert_eq!(
movement::down(
&snapshot,
DisplayPoint::new(DisplayRow(1), 10),
SelectionGoal::HorizontalPosition(x.0),
language::SelectionGoal::HorizontalPosition(x.0),
false,
&text_layout_details
),
(
DisplayPoint::new(DisplayRow(2), 4),
SelectionGoal::HorizontalPosition(x.0)
language::SelectionGoal::HorizontalPosition(x.0)
)
);
@@ -1618,6 +1677,8 @@ pub mod tests {
);
}
// todo(linux) fails due to pixel differences in text rendering
#[cfg(target_os = "macos")]
#[gpui::test]
async fn test_chunks_with_soft_wrapping(cx: &mut gpui::TestAppContext) {
use unindent::Unindent as _;

View File

@@ -467,8 +467,8 @@ impl BlockMap {
*transforms = new_transforms;
}
pub fn replace(&mut self, mut renderers: HashMap<BlockId, RenderBlock>) {
for block in &self.blocks {
pub fn replace_renderers(&mut self, mut renderers: HashMap<BlockId, RenderBlock>) {
for block in &mut self.blocks {
if let Some(render) = renderers.remove(&block.id) {
*block.render.lock() = render;
}
@@ -659,6 +659,48 @@ impl<'a> BlockMapWriter<'a> {
ids
}
pub fn replace(&mut self, mut heights_and_renderers: HashMap<BlockId, (u8, RenderBlock)>) {
let wrap_snapshot = &*self.0.wrap_snapshot.borrow();
let buffer = wrap_snapshot.buffer_snapshot();
let mut edits = Patch::default();
let mut last_block_buffer_row = None;
for block in &mut self.0.blocks {
if let Some((new_height, render)) = heights_and_renderers.remove(&block.id) {
if block.height != new_height {
let new_block = Block {
id: block.id,
position: block.position,
height: new_height,
style: block.style,
render: Mutex::new(render),
disposition: block.disposition,
};
*block = Arc::new(new_block);
let buffer_row = block.position.to_point(buffer).row;
if last_block_buffer_row != Some(buffer_row) {
last_block_buffer_row = Some(buffer_row);
let wrap_row = wrap_snapshot
.make_wrap_point(Point::new(buffer_row, 0), Bias::Left)
.row();
let start_row =
wrap_snapshot.prev_row_boundary(WrapPoint::new(wrap_row, 0));
let end_row = wrap_snapshot
.next_row_boundary(WrapPoint::new(wrap_row, 0))
.unwrap_or(wrap_snapshot.max_point().row() + 1);
edits.push(Edit {
old: start_row..end_row,
new: start_row..end_row,
})
}
}
}
}
self.0.sync(wrap_snapshot, edits);
}
pub fn remove(&mut self, block_ids: HashSet<BlockId>) {
let wrap_snapshot = &*self.0.wrap_snapshot.borrow();
let buffer = wrap_snapshot.buffer_snapshot();
@@ -1115,7 +1157,7 @@ mod tests {
use super::*;
use crate::display_map::inlay_map::InlayMap;
use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap};
use gpui::{div, font, px, Element};
use gpui::{div, font, px, AssetSource, Element};
use multi_buffer::MultiBuffer;
use rand::prelude::*;
use settings::SettingsStore;
@@ -1305,6 +1347,112 @@ mod tests {
assert_eq!(snapshot.text(), "aaa\n\nb!!!\n\n\nbb\nccc\nddd\n\n\n");
}
#[gpui::test]
fn test_replace_with_heights(cx: &mut gpui::TestAppContext) {
let _update = cx.update(|cx| init_test(cx));
let text = "aaa\nbbb\nccc\nddd";
let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx));
let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx));
let _subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let (_inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap());
let (_wrap_map, wraps_snapshot) =
cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx));
let mut block_map = BlockMap::new(wraps_snapshot.clone(), false, 1, 1, 0);
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
let block_ids = writer.insert(vec![
BlockProperties {
style: BlockStyle::Fixed,
position: buffer_snapshot.anchor_after(Point::new(1, 0)),
height: 1,
disposition: BlockDisposition::Above,
render: Box::new(|_| div().into_any()),
},
BlockProperties {
style: BlockStyle::Fixed,
position: buffer_snapshot.anchor_after(Point::new(1, 2)),
height: 2,
disposition: BlockDisposition::Above,
render: Box::new(|_| div().into_any()),
},
BlockProperties {
style: BlockStyle::Fixed,
position: buffer_snapshot.anchor_after(Point::new(3, 3)),
height: 3,
disposition: BlockDisposition::Below,
render: Box::new(|_| div().into_any()),
},
]);
{
let snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
assert_eq!(snapshot.text(), "aaa\n\n\n\nbbb\nccc\nddd\n\n\n");
let mut block_map_writer = block_map.write(wraps_snapshot.clone(), Default::default());
let mut hash_map = HashMap::default();
let render: RenderBlock = Box::new(|_| div().into_any());
hash_map.insert(block_ids[0], (2_u8, render));
block_map_writer.replace(hash_map);
let snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
assert_eq!(snapshot.text(), "aaa\n\n\n\n\nbbb\nccc\nddd\n\n\n");
}
{
let mut block_map_writer = block_map.write(wraps_snapshot.clone(), Default::default());
let mut hash_map = HashMap::default();
let render: RenderBlock = Box::new(|_| div().into_any());
hash_map.insert(block_ids[0], (1_u8, render));
block_map_writer.replace(hash_map);
let snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
assert_eq!(snapshot.text(), "aaa\n\n\n\nbbb\nccc\nddd\n\n\n");
}
{
let mut block_map_writer = block_map.write(wraps_snapshot.clone(), Default::default());
let mut hash_map = HashMap::default();
let render: RenderBlock = Box::new(|_| div().into_any());
hash_map.insert(block_ids[0], (0_u8, render));
block_map_writer.replace(hash_map);
let snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
assert_eq!(snapshot.text(), "aaa\n\n\nbbb\nccc\nddd\n\n\n");
}
{
let mut block_map_writer = block_map.write(wraps_snapshot.clone(), Default::default());
let mut hash_map = HashMap::default();
let render: RenderBlock = Box::new(|_| div().into_any());
hash_map.insert(block_ids[0], (3_u8, render));
block_map_writer.replace(hash_map);
let snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
assert_eq!(snapshot.text(), "aaa\n\n\n\n\n\nbbb\nccc\nddd\n\n\n");
}
{
let mut block_map_writer = block_map.write(wraps_snapshot.clone(), Default::default());
let mut hash_map = HashMap::default();
let render: RenderBlock = Box::new(|_| div().into_any());
hash_map.insert(block_ids[0], (3_u8, render));
block_map_writer.replace(hash_map);
let snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
// Same height as before, should remain the same
assert_eq!(snapshot.text(), "aaa\n\n\n\n\n\nbbb\nccc\nddd\n\n\n");
}
}
#[cfg(target_os = "macos")]
#[gpui::test]
fn test_blocks_on_wrapped_lines(cx: &mut gpui::TestAppContext) {
cx.update(|cx| init_test(cx));
@@ -1793,6 +1941,12 @@ mod tests {
let settings = SettingsStore::test(cx);
cx.set_global(settings);
theme::init(theme::LoadThemes::JustBase, cx);
cx.text_system()
.add_fonts(vec![assets::Assets
.load("fonts/zed-mono/zed-mono-extended.ttf")
.unwrap()
.unwrap()])
.unwrap();
}
impl TransformBlock {

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,10 @@ use crate::{
JoinLines,
};
use futures::StreamExt;
use gpui::{div, TestAppContext, UpdateGlobal, VisualTestContext, WindowBounds, WindowOptions};
use gpui::{
div, AssetSource, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext,
WindowBounds, WindowOptions,
};
use indoc::indoc;
use language::{
language_settings::{
@@ -20,6 +23,7 @@ use language::{
FakeLspAdapter, IndentGuide, LanguageConfig, LanguageConfigOverride, LanguageMatcher, Override,
Point,
};
use language_settings::IndentGuideSettings;
use multi_buffer::MultiBufferIndentGuide;
use parking_lot::Mutex;
use project::project_settings::{LspSettings, ProjectSettings};
@@ -53,10 +57,10 @@ fn test_edit_events(cx: &mut TestAppContext) {
let events = events.clone();
|cx| {
let view = cx.view().clone();
cx.subscribe(&view, move |_, _, event: &EditorEvent, _| {
if matches!(event, EditorEvent::Edited | EditorEvent::BufferEdited) {
events.borrow_mut().push(("editor1", event.clone()));
}
cx.subscribe(&view, move |_, _, event: &EditorEvent, _| match event {
EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
EditorEvent::BufferEdited => events.borrow_mut().push(("editor1", "buffer edited")),
_ => {}
})
.detach();
Editor::for_buffer(buffer.clone(), None, cx)
@@ -66,11 +70,16 @@ fn test_edit_events(cx: &mut TestAppContext) {
let editor2 = cx.add_window({
let events = events.clone();
|cx| {
cx.subscribe(&cx.view().clone(), move |_, _, event: &EditorEvent, _| {
if matches!(event, EditorEvent::Edited | EditorEvent::BufferEdited) {
events.borrow_mut().push(("editor2", event.clone()));
}
})
cx.subscribe(
&cx.view().clone(),
move |_, _, event: &EditorEvent, _| match event {
EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
EditorEvent::BufferEdited => {
events.borrow_mut().push(("editor2", "buffer edited"))
}
_ => {}
},
)
.detach();
Editor::for_buffer(buffer.clone(), None, cx)
}
@@ -83,9 +92,9 @@ fn test_edit_events(cx: &mut TestAppContext) {
assert_eq!(
mem::take(&mut *events.borrow_mut()),
[
("editor1", EditorEvent::Edited),
("editor1", EditorEvent::BufferEdited),
("editor2", EditorEvent::BufferEdited),
("editor1", "edited"),
("editor1", "buffer edited"),
("editor2", "buffer edited"),
]
);
@@ -94,9 +103,9 @@ fn test_edit_events(cx: &mut TestAppContext) {
assert_eq!(
mem::take(&mut *events.borrow_mut()),
[
("editor2", EditorEvent::Edited),
("editor1", EditorEvent::BufferEdited),
("editor2", EditorEvent::BufferEdited),
("editor2", "edited"),
("editor1", "buffer edited"),
("editor2", "buffer edited"),
]
);
@@ -105,9 +114,9 @@ fn test_edit_events(cx: &mut TestAppContext) {
assert_eq!(
mem::take(&mut *events.borrow_mut()),
[
("editor1", EditorEvent::Edited),
("editor1", EditorEvent::BufferEdited),
("editor2", EditorEvent::BufferEdited),
("editor1", "edited"),
("editor1", "buffer edited"),
("editor2", "buffer edited"),
]
);
@@ -116,9 +125,9 @@ fn test_edit_events(cx: &mut TestAppContext) {
assert_eq!(
mem::take(&mut *events.borrow_mut()),
[
("editor1", EditorEvent::Edited),
("editor1", EditorEvent::BufferEdited),
("editor2", EditorEvent::BufferEdited),
("editor1", "edited"),
("editor1", "buffer edited"),
("editor2", "buffer edited"),
]
);
@@ -127,9 +136,9 @@ fn test_edit_events(cx: &mut TestAppContext) {
assert_eq!(
mem::take(&mut *events.borrow_mut()),
[
("editor2", EditorEvent::Edited),
("editor1", EditorEvent::BufferEdited),
("editor2", EditorEvent::BufferEdited),
("editor2", "edited"),
("editor1", "buffer edited"),
("editor2", "buffer edited"),
]
);
@@ -138,9 +147,9 @@ fn test_edit_events(cx: &mut TestAppContext) {
assert_eq!(
mem::take(&mut *events.borrow_mut()),
[
("editor2", EditorEvent::Edited),
("editor1", EditorEvent::BufferEdited),
("editor2", EditorEvent::BufferEdited),
("editor2", "edited"),
("editor1", "buffer edited"),
("editor2", "buffer edited"),
]
);
@@ -472,6 +481,42 @@ fn test_canceling_pending_selection(cx: &mut TestAppContext) {
});
}
#[gpui::test]
fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let view = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
build_editor(buffer, cx)
});
_ = view.update(cx, |view, cx| {
view.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, cx);
assert_eq!(
view.selections.display_ranges(cx),
[DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
);
view.move_down(&Default::default(), cx);
assert_eq!(
view.selections.display_ranges(cx),
[DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
);
view.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, cx);
assert_eq!(
view.selections.display_ranges(cx),
[DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
);
view.move_up(&Default::default(), cx);
assert_eq!(
view.selections.display_ranges(cx),
[DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
);
});
}
#[gpui::test]
fn test_clone(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -508,6 +553,7 @@ fn test_clone(cx: &mut TestAppContext) {
.update(cx, |editor, cx| {
cx.open_window(Default::default(), |cx| cx.new_view(|cx| editor.clone(cx)))
})
.unwrap()
.unwrap();
let snapshot = editor.update(cx, |e, cx| e.snapshot(cx)).unwrap();
@@ -890,6 +936,8 @@ fn test_move_cursor(cx: &mut TestAppContext) {
});
}
// TODO: Re-enable this test
#[cfg(target_os = "macos")]
#[gpui::test]
fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -5813,7 +5861,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path("/file.rs").unwrap()
lsp::Uri::from_file_path("/file.rs").unwrap().into()
);
assert_eq!(params.options.tab_size, 4);
Ok(Some(vec![lsp::TextEdit::new(
@@ -5839,7 +5887,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path("/file.rs").unwrap()
lsp::Uri::from_file_path("/file.rs").unwrap().into()
);
futures::future::pending::<()>().await;
unreachable!()
@@ -5888,7 +5936,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path("/file.rs").unwrap()
lsp::Uri::from_file_path("/file.rs").unwrap().into()
);
assert_eq!(params.options.tab_size, 8);
Ok(Some(vec![]))
@@ -6091,7 +6139,7 @@ async fn test_multibuffer_format_during_save(cx: &mut gpui::TestAppContext) {
.on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
Ok(Some(vec![lsp::TextEdit::new(
lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
format!("[{} formatted]", params.text_document.uri),
format!("[{} formatted]", params.text_document.uri.as_str()),
)]))
})
.detach();
@@ -6165,7 +6213,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
.handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path("/file.rs").unwrap()
lsp::Uri::from_file_path("/file.rs").unwrap().into()
);
assert_eq!(params.options.tab_size, 4);
Ok(Some(vec![lsp::TextEdit::new(
@@ -6191,7 +6239,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
move |params, _| async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path("/file.rs").unwrap()
lsp::Uri::from_file_path("/file.rs").unwrap().into()
);
futures::future::pending::<()>().await;
unreachable!()
@@ -6241,7 +6289,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
.handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path("/file.rs").unwrap()
lsp::Uri::from_file_path("/file.rs").unwrap().into()
);
assert_eq!(params.options.tab_size, 8);
Ok(Some(vec![]))
@@ -6315,7 +6363,7 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path("/file.rs").unwrap()
lsp::Uri::from_file_path("/file.rs").unwrap().into()
);
assert_eq!(params.options.tab_size, 4);
Ok(Some(vec![lsp::TextEdit::new(
@@ -6337,7 +6385,7 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path("/file.rs").unwrap()
lsp::Uri::from_file_path("/file.rs").unwrap().into()
);
futures::future::pending::<()>().await;
unreachable!()
@@ -6695,7 +6743,7 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
cx.assert_editor_state("editor.cloˇ");
assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
cx.update_editor(|editor, cx| {
editor.show_completions(&ShowCompletions, cx);
editor.show_completions(&ShowCompletions { trigger: None }, cx);
});
handle_completion_request(
&mut cx,
@@ -7649,13 +7697,14 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
gpui::Point::new(0.into(), 0.into()),
gpui::Point::new(10.into(), 80.into()),
gpui::Point::new(px(0.), px(0.)),
gpui::Point::new(px(10.), px(80.)),
))),
..Default::default()
},
|cx| cx.new_view(|cx| build_editor(buffer.clone(), cx)),
)
.unwrap()
});
let is_still_following = Rc::new(RefCell::new(true));
@@ -7979,7 +8028,7 @@ async fn go_to_prev_overlapping_diagnostic(
.update_diagnostics(
LanguageServerId(0),
lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path("/root/file").unwrap(),
uri: lsp::Uri::from_file_path("/root/file").unwrap().into(),
version: None,
diagnostics: vec![
lsp::Diagnostic {
@@ -8351,7 +8400,7 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
fake_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
assert_eq!(
params.text_document_position.text_document.uri,
lsp::Url::from_file_path("/a/main.rs").unwrap(),
lsp::Uri::from_file_path("/a/main.rs").unwrap().into(),
);
assert_eq!(
params.text_document_position.position,
@@ -11505,6 +11554,7 @@ fn assert_indent_guides(
let snapshot = editor.snapshot(cx).display_snapshot;
let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
MultiBufferRow(range.start)..MultiBufferRow(range.end),
true,
&snapshot,
cx,
);
@@ -11543,6 +11593,21 @@ fn assert_indent_guides(
assert_eq!(indent_guides, expected, "Indent guides do not match");
}
fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
IndentGuide {
buffer_id,
start_row,
end_row,
depth,
tab_size: 4,
settings: IndentGuideSettings {
enabled: true,
line_width: 1,
..Default::default()
},
}
}
#[gpui::test]
async fn test_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
@@ -11555,12 +11620,7 @@ async fn test_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
)
.await;
assert_indent_guides(
0..3,
vec![IndentGuide::new(buffer_id, 1, 1, 0, 4)],
None,
&mut cx,
);
assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
}
#[gpui::test]
@@ -11576,12 +11636,7 @@ async fn test_indent_guide_simple_block(cx: &mut gpui::TestAppContext) {
)
.await;
assert_indent_guides(
0..4,
vec![IndentGuide::new(buffer_id, 1, 2, 0, 4)],
None,
&mut cx,
);
assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
}
#[gpui::test]
@@ -11604,9 +11659,9 @@ async fn test_indent_guide_nested(cx: &mut gpui::TestAppContext) {
assert_indent_guides(
0..8,
vec![
IndentGuide::new(buffer_id, 1, 6, 0, 4),
IndentGuide::new(buffer_id, 3, 3, 1, 4),
IndentGuide::new(buffer_id, 5, 5, 1, 4),
indent_guide(buffer_id, 1, 6, 0),
indent_guide(buffer_id, 3, 3, 1),
indent_guide(buffer_id, 5, 5, 1),
],
None,
&mut cx,
@@ -11630,8 +11685,8 @@ async fn test_indent_guide_tab(cx: &mut gpui::TestAppContext) {
assert_indent_guides(
0..5,
vec![
IndentGuide::new(buffer_id, 1, 3, 0, 4),
IndentGuide::new(buffer_id, 2, 2, 1, 4),
indent_guide(buffer_id, 1, 3, 0),
indent_guide(buffer_id, 2, 2, 1),
],
None,
&mut cx,
@@ -11652,12 +11707,7 @@ async fn test_indent_guide_continues_on_empty_line(cx: &mut gpui::TestAppContext
)
.await;
assert_indent_guides(
0..5,
vec![IndentGuide::new(buffer_id, 1, 3, 0, 4)],
None,
&mut cx,
);
assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
}
#[gpui::test]
@@ -11683,9 +11733,9 @@ async fn test_indent_guide_complex(cx: &mut gpui::TestAppContext) {
assert_indent_guides(
0..11,
vec![
IndentGuide::new(buffer_id, 1, 9, 0, 4),
IndentGuide::new(buffer_id, 6, 6, 1, 4),
IndentGuide::new(buffer_id, 8, 8, 1, 4),
indent_guide(buffer_id, 1, 9, 0),
indent_guide(buffer_id, 6, 6, 1),
indent_guide(buffer_id, 8, 8, 1),
],
None,
&mut cx,
@@ -11715,9 +11765,9 @@ async fn test_indent_guide_starts_off_screen(cx: &mut gpui::TestAppContext) {
assert_indent_guides(
1..11,
vec![
IndentGuide::new(buffer_id, 1, 9, 0, 4),
IndentGuide::new(buffer_id, 6, 6, 1, 4),
IndentGuide::new(buffer_id, 8, 8, 1, 4),
indent_guide(buffer_id, 1, 9, 0),
indent_guide(buffer_id, 6, 6, 1),
indent_guide(buffer_id, 8, 8, 1),
],
None,
&mut cx,
@@ -11747,9 +11797,9 @@ async fn test_indent_guide_ends_off_screen(cx: &mut gpui::TestAppContext) {
assert_indent_guides(
1..10,
vec![
IndentGuide::new(buffer_id, 1, 9, 0, 4),
IndentGuide::new(buffer_id, 6, 6, 1, 4),
IndentGuide::new(buffer_id, 8, 8, 1, 4),
indent_guide(buffer_id, 1, 9, 0),
indent_guide(buffer_id, 6, 6, 1),
indent_guide(buffer_id, 8, 8, 1),
],
None,
&mut cx,
@@ -11775,9 +11825,9 @@ async fn test_indent_guide_without_brackets(cx: &mut gpui::TestAppContext) {
assert_indent_guides(
1..10,
vec![
IndentGuide::new(buffer_id, 1, 4, 0, 4),
IndentGuide::new(buffer_id, 2, 3, 1, 4),
IndentGuide::new(buffer_id, 3, 3, 2, 4),
indent_guide(buffer_id, 1, 4, 0),
indent_guide(buffer_id, 2, 3, 1),
indent_guide(buffer_id, 3, 3, 2),
],
None,
&mut cx,
@@ -11802,8 +11852,8 @@ async fn test_indent_guide_ends_before_empty_line(cx: &mut gpui::TestAppContext)
assert_indent_guides(
0..6,
vec![
IndentGuide::new(buffer_id, 1, 2, 0, 4),
IndentGuide::new(buffer_id, 2, 2, 1, 4),
indent_guide(buffer_id, 1, 2, 0),
indent_guide(buffer_id, 2, 2, 1),
],
None,
&mut cx,
@@ -11825,12 +11875,7 @@ async fn test_indent_guide_continuing_off_screen(cx: &mut gpui::TestAppContext)
)
.await;
assert_indent_guides(
0..1,
vec![IndentGuide::new(buffer_id, 1, 1, 0, 4)],
None,
&mut cx,
);
assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
}
#[gpui::test]
@@ -11852,8 +11897,8 @@ async fn test_indent_guide_tabs(cx: &mut gpui::TestAppContext) {
assert_indent_guides(
0..6,
vec![
IndentGuide::new(buffer_id, 1, 6, 0, 4),
IndentGuide::new(buffer_id, 3, 4, 1, 4),
indent_guide(buffer_id, 1, 6, 0),
indent_guide(buffer_id, 3, 4, 1),
],
None,
&mut cx,
@@ -11880,7 +11925,7 @@ async fn test_active_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
assert_indent_guides(
0..3,
vec![IndentGuide::new(buffer_id, 1, 1, 0, 4)],
vec![indent_guide(buffer_id, 1, 1, 0)],
Some(vec![0]),
&mut cx,
);
@@ -11909,8 +11954,8 @@ async fn test_active_indent_guide_respect_indented_range(cx: &mut gpui::TestAppC
assert_indent_guides(
0..4,
vec![
IndentGuide::new(buffer_id, 1, 3, 0, 4),
IndentGuide::new(buffer_id, 2, 2, 1, 4),
indent_guide(buffer_id, 1, 3, 0),
indent_guide(buffer_id, 2, 2, 1),
],
Some(vec![1]),
&mut cx,
@@ -11925,8 +11970,8 @@ async fn test_active_indent_guide_respect_indented_range(cx: &mut gpui::TestAppC
assert_indent_guides(
0..4,
vec![
IndentGuide::new(buffer_id, 1, 3, 0, 4),
IndentGuide::new(buffer_id, 2, 2, 1, 4),
indent_guide(buffer_id, 1, 3, 0),
indent_guide(buffer_id, 2, 2, 1),
],
Some(vec![1]),
&mut cx,
@@ -11941,8 +11986,8 @@ async fn test_active_indent_guide_respect_indented_range(cx: &mut gpui::TestAppC
assert_indent_guides(
0..4,
vec![
IndentGuide::new(buffer_id, 1, 3, 0, 4),
IndentGuide::new(buffer_id, 2, 2, 1, 4),
indent_guide(buffer_id, 1, 3, 0),
indent_guide(buffer_id, 2, 2, 1),
],
Some(vec![0]),
&mut cx,
@@ -11971,7 +12016,7 @@ async fn test_active_indent_guide_empty_line(cx: &mut gpui::TestAppContext) {
assert_indent_guides(
0..5,
vec![IndentGuide::new(buffer_id, 1, 3, 0, 4)],
vec![indent_guide(buffer_id, 1, 3, 0)],
Some(vec![0]),
&mut cx,
);
@@ -11997,7 +12042,7 @@ async fn test_active_indent_guide_non_matching_indent(cx: &mut gpui::TestAppCont
assert_indent_guides(
0..3,
vec![IndentGuide::new(buffer_id, 1, 2, 0, 4)],
vec![indent_guide(buffer_id, 1, 2, 0)],
Some(vec![0]),
&mut cx,
);
@@ -12106,7 +12151,10 @@ pub fn handle_completion_request(
let completions = completions.clone();
counter.fetch_add(1, atomic::Ordering::Release);
async move {
assert_eq!(params.text_document_position.text_document.uri, url.clone());
assert_eq!(
params.text_document_position.text_document.uri,
url.clone().into()
);
assert_eq!(
params.text_document_position.position,
complete_from_position
@@ -12187,10 +12235,16 @@ pub(crate) fn update_test_project_settings(
pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
_ = cx.update(|cx| {
cx.text_system()
.add_fonts(vec![assets::Assets
.load("fonts/zed-mono/zed-mono-extended.ttf")
.unwrap()
.unwrap()])
.unwrap();
let store = SettingsStore::test(cx);
cx.set_global(store);
theme::init(theme::LoadThemes::JustBase, cx);
release_channel::init("0.0.0", cx);
release_channel::init(SemanticVersion::default(), cx);
client::init_settings(cx);
language::init(cx);
Project::init_settings(cx);

View File

@@ -38,7 +38,7 @@ use gpui::{
};
use itertools::Itertools;
use language::language_settings::{
IndentGuideBackgroundColoring, IndentGuideColoring, ShowWhitespaceSetting,
IndentGuideBackgroundColoring, IndentGuideColoring, IndentGuideSettings, ShowWhitespaceSetting,
};
use lsp::DiagnosticSeverity;
use multi_buffer::{Anchor, MultiBufferPoint, MultiBufferRow};
@@ -153,7 +153,7 @@ impl EditorElement {
fn register_actions(&self, cx: &mut WindowContext) {
let view = &self.editor;
view.update(cx, |editor, cx| {
for action in editor.editor_actions.iter() {
for action in editor.editor_actions.borrow().values() {
(action)(cx)
}
});
@@ -318,6 +318,7 @@ impl EditorElement {
register_action(view, cx, Editor::open_excerpts);
register_action(view, cx, Editor::open_excerpts_in_split);
register_action(view, cx, Editor::toggle_soft_wrap);
register_action(view, cx, Editor::toggle_tab_bar);
register_action(view, cx, Editor::toggle_line_numbers);
register_action(view, cx, Editor::toggle_indent_guides);
register_action(view, cx, Editor::toggle_inlay_hints);
@@ -585,7 +586,7 @@ impl EditorElement {
editor.handle_click_hovered_link(point, event.modifiers, cx);
cx.stop_propagation();
} else if end_selection {
} else if end_selection && pending_nonempty_selections {
cx.stop_propagation();
} else if cfg!(target_os = "linux") && event.button == MouseButton::Middle {
if !text_hitbox.is_hovered(cx) || editor.read_only(cx) {
@@ -593,7 +594,7 @@ impl EditorElement {
}
#[cfg(target_os = "linux")]
if let Some(item) = cx.read_from_clipboard() {
if let Some(item) = cx.read_from_primary() {
let point_for_position =
position_map.point_for_position(text_hitbox.bounds, event.position);
let position = point_for_position.previous_valid;
@@ -1125,9 +1126,7 @@ impl EditorElement {
ix as f32 * line_height - (scroll_pixel_position.y % line_height),
);
let centering_offset = point(
(gutter_dimensions.right_padding + gutter_dimensions.margin
- fold_indicator_size.width)
/ 2.,
(gutter_dimensions.fold_area_width() - fold_indicator_size.width) / 2.,
(line_height - fold_indicator_size.height) / 2.,
);
let origin = gutter_hitbox.origin + position + centering_offset;
@@ -1222,34 +1221,41 @@ impl EditorElement {
.collect::<HashMap<_, _>>()
});
let git_gutter_setting = ProjectSettings::get_global(cx)
.git
.git_gutter
.unwrap_or_default();
buffer_snapshot
.git_diff_hunks_in_range(buffer_start_row..buffer_end_row)
.map(|hunk| diff_hunk_to_display(&hunk, snapshot))
.dedup()
.map(|hunk| {
let hitbox = if let DisplayDiffHunk::Unfolded {
display_row_range, ..
} = &hunk
{
let was_expanded = expanded_hunk_display_rows
.get(&display_row_range.start)
.map(|expanded_end_row| expanded_end_row == &display_row_range.end)
.unwrap_or(false);
if was_expanded {
None
.map(|hunk| match git_gutter_setting {
GitGutterSetting::TrackedFiles => {
let hitbox = if let DisplayDiffHunk::Unfolded {
display_row_range, ..
} = &hunk
{
let was_expanded = expanded_hunk_display_rows
.get(&display_row_range.start)
.map(|expanded_end_row| expanded_end_row == &display_row_range.end)
.unwrap_or(false);
if was_expanded {
None
} else {
let hunk_bounds = Self::diff_hunk_bounds(
&snapshot,
line_height,
gutter_hitbox.bounds,
&hunk,
);
Some(cx.insert_hitbox(hunk_bounds, true))
}
} else {
let hunk_bounds = Self::diff_hunk_bounds(
&snapshot,
line_height,
gutter_hitbox.bounds,
&hunk,
);
Some(cx.insert_hitbox(hunk_bounds, true))
}
} else {
None
};
(hunk, hitbox)
None
};
(hunk, hitbox)
}
GitGutterSetting::Hide => (hunk, None),
})
.collect()
}
@@ -1438,6 +1444,7 @@ impl EditorElement {
single_indent_width,
depth: indent_guide.depth,
active: active_indent_guide_indices.contains(&i),
settings: indent_guide.settings,
})
} else {
None
@@ -2730,14 +2737,6 @@ impl EditorElement {
return;
};
let settings = self
.editor
.read(cx)
.buffer()
.read(cx)
.settings_at(0, cx)
.indent_guides;
let faded_color = |color: Hsla, alpha: f32| {
let mut faded = color;
faded.a = alpha;
@@ -2746,6 +2745,7 @@ impl EditorElement {
for indent_guide in indent_guides {
let indent_accent_colors = cx.theme().accents().color_for_index(indent_guide.depth);
let settings = indent_guide.settings;
// TODO fixed for now, expose them through themes later
const INDENT_AWARE_ALPHA: f32 = 0.2;
@@ -2753,7 +2753,7 @@ impl EditorElement {
const INDENT_AWARE_BACKGROUND_ALPHA: f32 = 0.1;
const INDENT_AWARE_BACKGROUND_ACTIVE_ALPHA: f32 = 0.2;
let line_color = match (&settings.coloring, indent_guide.active) {
let line_color = match (settings.coloring, indent_guide.active) {
(IndentGuideColoring::Disabled, _) => None,
(IndentGuideColoring::Fixed, false) => {
Some(cx.theme().colors().editor_indent_guide)
@@ -2769,7 +2769,7 @@ impl EditorElement {
}
};
let background_color = match (&settings.background_coloring, indent_guide.active) {
let background_color = match (settings.background_coloring, indent_guide.active) {
(IndentGuideBackgroundColoring::Disabled, _) => None,
(IndentGuideBackgroundColoring::IndentAware, false) => Some(faded_color(
indent_accent_colors,
@@ -2837,6 +2837,8 @@ impl EditorElement {
Self::paint_diff_hunks(layout.gutter_hitbox.bounds, layout, cx)
}
self.paint_gutter_highlights(layout, cx);
if layout.blamed_display_rows.is_some() {
self.paint_blamed_display_rows(layout, cx);
}
@@ -3006,6 +3008,37 @@ impl EditorElement {
}
}
fn paint_gutter_highlights(&self, layout: &EditorLayout, cx: &mut WindowContext) {
let highlight_width = 0.275 * layout.position_map.line_height;
let highlight_corner_radii = Corners::all(0.05 * layout.position_map.line_height);
cx.paint_layer(layout.gutter_hitbox.bounds, |cx| {
for (range, color) in &layout.highlighted_gutter_ranges {
let start_row = if range.start.row() < layout.visible_display_row_range.start {
layout.visible_display_row_range.start - DisplayRow(1)
} else {
range.start.row()
};
let end_row = if range.end.row() > layout.visible_display_row_range.end {
layout.visible_display_row_range.end + DisplayRow(1)
} else {
range.end.row()
};
let start_y = layout.gutter_hitbox.top()
+ start_row.0 as f32 * layout.position_map.line_height
- layout.position_map.scroll_pixel_position.y;
let end_y = layout.gutter_hitbox.top()
+ (end_row.0 + 1) as f32 * layout.position_map.line_height
- layout.position_map.scroll_pixel_position.y;
let bounds = Bounds::from_corners(
point(layout.gutter_hitbox.left(), start_y),
point(layout.gutter_hitbox.left() + highlight_width, end_y),
);
cx.paint_quad(fill(bounds, *color).corner_radii(highlight_corner_radii));
}
});
}
fn paint_blamed_display_rows(&self, layout: &mut EditorLayout, cx: &mut WindowContext) {
let Some(blamed_display_rows) = layout.blamed_display_rows.take() else {
return;
@@ -4072,6 +4105,7 @@ impl LineWithInvisibles {
if non_whitespace_added || !inside_wrapped_string {
invisibles.push(Invisible::Tab {
line_start_offset: line.len(),
line_end_offset: line.len() + line_chunk.len(),
});
}
} else {
@@ -4187,16 +4221,15 @@ impl LineWithInvisibles {
whitespace_setting: ShowWhitespaceSetting,
cx: &mut WindowContext,
) {
let allowed_invisibles_regions = match whitespace_setting {
ShowWhitespaceSetting::None => return,
ShowWhitespaceSetting::Selection => Some(selection_ranges),
ShowWhitespaceSetting::All => None,
};
for invisible in &self.invisibles {
let (&token_offset, invisible_symbol) = match invisible {
Invisible::Tab { line_start_offset } => (line_start_offset, &layout.tab_invisible),
Invisible::Whitespace { line_offset } => (line_offset, &layout.space_invisible),
let extract_whitespace_info = |invisible: &Invisible| {
let (token_offset, token_end_offset, invisible_symbol) = match invisible {
Invisible::Tab {
line_start_offset,
line_end_offset,
} => (*line_start_offset, *line_end_offset, &layout.tab_invisible),
Invisible::Whitespace { line_offset } => {
(*line_offset, line_offset + 1, &layout.space_invisible)
}
};
let x_offset = self.x_for_index(token_offset);
@@ -4208,17 +4241,73 @@ impl LineWithInvisibles {
line_y,
);
if let Some(allowed_regions) = allowed_invisibles_regions {
let invisible_point = DisplayPoint::new(row, token_offset as u32);
if !allowed_regions
(
[token_offset, token_end_offset],
Box::new(move |cx: &mut WindowContext| {
invisible_symbol.paint(origin, line_height, cx).log_err();
}),
)
};
let invisible_iter = self.invisibles.iter().map(extract_whitespace_info);
match whitespace_setting {
ShowWhitespaceSetting::None => return,
ShowWhitespaceSetting::All => invisible_iter.for_each(|(_, paint)| paint(cx)),
ShowWhitespaceSetting::Selection => invisible_iter.for_each(|([start, _], paint)| {
let invisible_point = DisplayPoint::new(row, start as u32);
if !selection_ranges
.iter()
.any(|region| region.start <= invisible_point && invisible_point < region.end)
{
continue;
return;
}
paint(cx);
}),
// For a whitespace to be on a boundary, any of the following conditions need to be met:
// - It is a tab
// - It is adjacent to an edge (start or end)
// - It is adjacent to a whitespace (left or right)
ShowWhitespaceSetting::Boundary => {
// We'll need to keep track of the last invisible we've seen and then check if we are adjacent to it for some of
// the above cases.
// Note: We zip in the original `invisibles` to check for tab equality
let mut last_seen: Option<(bool, usize, Box<dyn Fn(&mut WindowContext)>)> = None;
for (([start, end], paint), invisible) in
invisible_iter.zip_eq(self.invisibles.iter())
{
let should_render = match (&last_seen, invisible) {
(_, Invisible::Tab { .. }) => true,
(Some((_, last_end, _)), _) => *last_end == start,
_ => false,
};
if should_render || start == 0 || end == self.len {
paint(cx);
// Since we are scanning from the left, we will skip over the first available whitespace that is part
// of a boundary between non-whitespace segments, so we correct by manually redrawing it if needed.
if let Some((should_render_last, last_end, paint_last)) = last_seen {
// Note that we need to make sure that the last one is actually adjacent
if !should_render_last && last_end == start {
paint_last(cx);
}
}
}
// Manually render anything within a selection
let invisible_point = DisplayPoint::new(row, start as u32);
if selection_ranges.iter().any(|region| {
region.start <= invisible_point && invisible_point < region.end
}) {
paint(cx);
}
last_seen = Some((should_render, end, paint));
}
}
invisible_symbol.paint(origin, line_height, cx).log_err();
}
};
}
pub fn x_for_index(&self, index: usize) -> Pixels {
@@ -4308,8 +4397,18 @@ impl LineWithInvisibles {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Invisible {
Tab { line_start_offset: usize },
Whitespace { line_offset: usize },
/// A tab character
///
/// A tab character is internally represented by spaces (configured by the user's tab width)
/// aligned to the nearest column, so it's necessary to store the start and end offset for
/// adjacency checks.
Tab {
line_start_offset: usize,
line_end_offset: usize,
},
Whitespace {
line_offset: usize,
},
}
impl EditorElement {
@@ -4565,6 +4664,12 @@ impl Element for EditorElement {
&snapshot.display_snapshot,
cx.theme().colors(),
);
let highlighted_gutter_ranges =
self.editor.read(cx).gutter_highlights_in_range(
start_anchor..end_anchor,
&snapshot.display_snapshot,
cx,
);
let redacted_ranges = self.editor.read(cx).redacted_ranges(
start_anchor..end_anchor,
@@ -4635,7 +4740,7 @@ impl Element for EditorElement {
&mut scroll_width,
&gutter_dimensions,
em_width,
gutter_dimensions.width + gutter_dimensions.margin,
gutter_dimensions.full_width(),
line_height,
&line_layouts,
cx,
@@ -4925,6 +5030,7 @@ impl Element for EditorElement {
active_rows,
highlighted_rows,
highlighted_ranges,
highlighted_gutter_ranges,
redacted_ranges,
line_elements,
line_numbers,
@@ -5055,6 +5161,7 @@ pub struct EditorLayout {
inline_blame: Option<AnyElement>,
blocks: Vec<BlockLayout>,
highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
highlighted_gutter_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
redacted_ranges: Vec<Range<DisplayPoint>>,
cursors: Vec<(DisplayPoint, Hsla)>,
visible_cursors: Vec<CursorLayout>,
@@ -5286,6 +5393,7 @@ pub struct IndentGuideLayout {
single_indent_width: Pixels,
depth: u32,
active: bool,
settings: IndentGuideSettings,
}
pub struct CursorLayout {
@@ -5853,15 +5961,18 @@ mod tests {
let expected_invisibles = vec![
Invisible::Tab {
line_start_offset: 0,
line_end_offset: TAB_SIZE as usize,
},
Invisible::Whitespace {
line_offset: TAB_SIZE as usize,
},
Invisible::Tab {
line_start_offset: TAB_SIZE as usize + 1,
line_end_offset: TAB_SIZE as usize * 2,
},
Invisible::Tab {
line_start_offset: TAB_SIZE as usize * 2 + 1,
line_end_offset: TAB_SIZE as usize * 3,
},
Invisible::Whitespace {
line_offset: TAB_SIZE as usize * 3 + 1,
@@ -5915,10 +6026,11 @@ mod tests {
#[gpui::test]
fn test_wrapped_invisibles_drawing(cx: &mut TestAppContext) {
let tab_size = 4;
let input_text = "a\tbcd ".repeat(9);
let input_text = "a\tbcd ".repeat(9);
let repeated_invisibles = [
Invisible::Tab {
line_start_offset: 1,
line_end_offset: tab_size as usize,
},
Invisible::Whitespace {
line_offset: tab_size as usize + 3,
@@ -5929,6 +6041,12 @@ mod tests {
Invisible::Whitespace {
line_offset: tab_size as usize + 5,
},
Invisible::Whitespace {
line_offset: tab_size as usize + 6,
},
Invisible::Whitespace {
line_offset: tab_size as usize + 7,
},
];
let expected_invisibles = std::iter::once(repeated_invisibles)
.cycle()

View File

@@ -1,5 +1,6 @@
use crate::{
hover_popover::{self, InlayHover},
scroll::ScrollAmount,
Anchor, Editor, EditorSnapshot, FindAllReferences, GoToDefinition, GoToTypeDefinition, InlayId,
PointForPosition, SelectPhase,
};
@@ -38,7 +39,11 @@ impl RangeInEditor {
}
}
fn point_within_range(&self, trigger_point: &TriggerPoint, snapshot: &EditorSnapshot) -> bool {
pub fn point_within_range(
&self,
trigger_point: &TriggerPoint,
snapshot: &EditorSnapshot,
) -> bool {
match (self, trigger_point) {
(Self::Text(range), TriggerPoint::Text(point)) => {
let point_after_start = range.start.cmp(point, &snapshot.buffer_snapshot).is_le();
@@ -169,6 +174,21 @@ impl Editor {
.detach();
}
pub fn scroll_hover(&mut self, amount: &ScrollAmount, cx: &mut ViewContext<Self>) -> bool {
let selection = self.selections.newest_anchor().head();
let snapshot = self.snapshot(cx);
let Some(popover) = self.hover_state.info_popovers.iter().find(|popover| {
popover
.symbol_range
.point_within_range(&TriggerPoint::Text(selection), &snapshot)
}) else {
return false;
};
popover.scroll(amount, cx);
true
}
fn cmd_click_reveal_task(
&mut self,
point: PointForPosition,
@@ -302,7 +322,6 @@ pub fn update_inlay_link_and_hover_points(
hover_popover::hover_at_inlay(
editor,
InlayHover {
excerpt: excerpt_id,
tooltip: match tooltip {
InlayHintTooltip::String(text) => HoverBlock {
text,
@@ -350,7 +369,6 @@ pub fn update_inlay_link_and_hover_points(
hover_popover::hover_at_inlay(
editor,
InlayHover {
excerpt: excerpt_id,
tooltip: match tooltip {
InlayHintLabelPartTooltip::String(text) => {
HoverBlock {
@@ -723,16 +741,20 @@ mod tests {
Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
lsp::LocationLink {
origin_selection_range: Some(symbol_range),
target_uri: url.clone(),
target_uri: url.clone().into(),
target_range,
target_selection_range: target_range,
},
])))
});
cx.cx
.cx
.simulate_mouse_move(screen_coord.unwrap(), None, Modifiers::command_shift());
let modifiers = if cfg!(target_os = "macos") {
Modifiers::command_shift()
} else {
Modifiers::control_shift()
};
cx.simulate_mouse_move(screen_coord.unwrap(), None, modifiers);
requests.next().await;
cx.run_until_parked();
@@ -749,9 +771,7 @@ mod tests {
let variable = A;
"});
cx.cx
.cx
.simulate_click(screen_coord.unwrap(), Modifiers::command_shift());
cx.simulate_click(screen_coord.unwrap(), modifiers);
cx.assert_editor_state(indoc! {"
struct «Aˇ»;
@@ -795,7 +815,7 @@ mod tests {
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
lsp::LocationLink {
origin_selection_range: Some(symbol_range),
target_uri: url.clone(),
target_uri: url.clone().into(),
target_range,
target_selection_range: target_range,
},
@@ -821,7 +841,7 @@ mod tests {
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
lsp::LocationLink {
origin_selection_range: Some(symbol_range),
target_uri: url.clone(),
target_uri: url.clone().into(),
target_range,
target_selection_range: target_range,
},
@@ -884,7 +904,7 @@ mod tests {
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
lsp::LocationLink {
origin_selection_range: Some(symbol_range),
target_uri: url,
target_uri: url.into(),
target_range,
target_selection_range: target_range,
},
@@ -960,7 +980,7 @@ mod tests {
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
lsp::LocationLink {
origin_selection_range: None,
target_uri: url,
target_uri: url.into(),
target_range,
target_selection_range: target_range,
},
@@ -988,7 +1008,7 @@ mod tests {
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
lsp::LocationLink {
origin_selection_range: None,
target_uri: url,
target_uri: url.into(),
target_range,
target_selection_range: target_range,
},
@@ -1068,7 +1088,7 @@ mod tests {
let hint_label = ": TestStruct";
cx.lsp
.handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
let expected_uri = expected_uri.clone();
let expected_uri = expected_uri.clone().into();
async move {
assert_eq!(params.text_document.uri, expected_uri);
Ok(Some(vec![lsp::InlayHint {

View File

@@ -1,14 +1,15 @@
use crate::{
display_map::{InlayOffset, ToDisplayPoint},
hover_links::{InlayHighlight, RangeInEditor},
scroll::ScrollAmount,
Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings, EditorSnapshot,
EditorStyle, ExcerptId, Hover, RangeToAnchorExt,
EditorStyle, Hover, RangeToAnchorExt,
};
use futures::{stream::FuturesUnordered, FutureExt};
use gpui::{
div, px, AnyElement, CursorStyle, Hsla, InteractiveElement, IntoElement, MouseButton,
ParentElement, Pixels, SharedString, Size, StatefulInteractiveElement, Styled, Task,
ViewContext, WeakView,
ParentElement, Pixels, ScrollHandle, SharedString, Size, StatefulInteractiveElement, Styled,
Task, ViewContext, WeakView,
};
use language::{markdown, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown};
@@ -48,7 +49,6 @@ pub fn hover_at(editor: &mut Editor, anchor: Option<Anchor>, cx: &mut ViewContex
}
pub struct InlayHover {
pub excerpt: ExcerptId,
pub range: InlayHighlight,
pub tooltip: HoverBlock,
}
@@ -118,6 +118,7 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie
let hover_popover = InfoPopover {
symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
parsed_content,
scroll_handle: ScrollHandle::new(),
};
this.update(&mut cx, |this, cx| {
@@ -317,6 +318,7 @@ fn show_hover(
InfoPopover {
symbol_range: RangeInEditor::Text(range),
parsed_content,
scroll_handle: ScrollHandle::new(),
},
)
})
@@ -423,7 +425,7 @@ async fn parse_blocks(
}
}
#[derive(Default)]
#[derive(Default, Debug)]
pub struct HoverState {
pub info_popovers: Vec<InfoPopover>,
pub diagnostic_popover: Option<DiagnosticPopover>,
@@ -487,10 +489,11 @@ impl HoverState {
}
}
#[derive(Debug, Clone)]
#[derive(Clone, Debug)]
pub struct InfoPopover {
symbol_range: RangeInEditor,
parsed_content: ParsedMarkdown,
pub symbol_range: RangeInEditor,
pub parsed_content: ParsedMarkdown,
pub scroll_handle: ScrollHandle,
}
impl InfoPopover {
@@ -504,23 +507,33 @@ impl InfoPopover {
div()
.id("info_popover")
.elevation_2(cx)
.p_2()
.overflow_y_scroll()
.track_scroll(&self.scroll_handle)
.max_w(max_size.width)
.max_h(max_size.height)
// Prevent a mouse down/move on the popover from being propagated to the editor,
// because that would dismiss the popover.
.on_mouse_move(|_, cx| cx.stop_propagation())
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
.child(crate::render_parsed_markdown(
.child(div().p_2().child(crate::render_parsed_markdown(
"content",
&self.parsed_content,
style,
workspace,
cx,
))
)))
.into_any_element()
}
pub fn scroll(&self, amount: &ScrollAmount, cx: &mut ViewContext<Editor>) {
let mut current = self.scroll_handle.offset();
current.y -= amount.pixels(
cx.line_height(),
self.scroll_handle.bounds().size.height - px(16.),
) / 2.0;
cx.notify();
self.scroll_handle.set_offset(current);
}
}
#[derive(Debug, Clone)]
@@ -1363,7 +1376,7 @@ mod tests {
let closure_uri = uri.clone();
cx.lsp
.handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
let task_uri = closure_uri.clone();
let task_uri = closure_uri.clone().into();
async move {
assert_eq!(params.text_document.uri, task_uri);
Ok(Some(vec![lsp::InlayHint {
@@ -1454,7 +1467,7 @@ mod tests {
lsp::InlayHintLabelPart {
value: new_type_label.to_string(),
location: Some(lsp::Location {
uri: task_uri.clone(),
uri: task_uri.clone().into(),
range: new_type_target_range,
}),
tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!(
@@ -1469,7 +1482,7 @@ mod tests {
lsp::InlayHintLabelPart {
value: struct_label.to_string(),
location: Some(lsp::Location {
uri: task_uri,
uri: task_uri.into(),
range: struct_target_range,
}),
tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent(

View File

@@ -10,7 +10,7 @@ use language::Buffer;
use multi_buffer::{
Anchor, ExcerptRange, MultiBuffer, MultiBufferRow, MultiBufferSnapshot, ToPoint,
};
use settings::{Settings, SettingsStore};
use settings::SettingsStore;
use text::{BufferId, Point};
use ui::{
div, ActiveTheme, Context as _, IntoElement, ParentElement, Styled, ViewContext, VisualContext,
@@ -21,7 +21,7 @@ use crate::{
editor_settings::CurrentLineHighlight,
git::{diff_hunk_to_display, DisplayDiffHunk},
hunk_status, hunks_for_selections, BlockDisposition, BlockId, BlockProperties, BlockStyle,
DiffRowHighlight, Editor, EditorSettings, EditorSnapshot, ExpandAllHunkDiffs, RangeToAnchorExt,
DiffRowHighlight, Editor, EditorSnapshot, ExpandAllHunkDiffs, RangeToAnchorExt,
RevertSelectedHunks, ToDisplayPoint, ToggleHunkDiff,
};
@@ -320,7 +320,7 @@ impl Editor {
div()
.bg(deleted_hunk_color)
.size_full()
.pl(gutter_dimensions.width + gutter_dimensions.margin)
.pl(gutter_dimensions.full_width())
.child(editor_with_deleted_text.clone())
.into_any_element()
}),
@@ -591,7 +591,7 @@ fn editor_with_deleted_text(
let subscription_editor = parent_editor.clone();
editor._subscriptions.extend([
cx.on_blur(&editor.focus_handle, |editor, cx| {
editor.set_current_line_highlight(CurrentLineHighlight::None);
editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
editor.change_selections(None, cx, |s| {
s.try_cancel();
});
@@ -602,45 +602,49 @@ fn editor_with_deleted_text(
{
parent_editor.read(cx).current_line_highlight
} else {
EditorSettings::get_global(cx).current_line_highlight
None
};
editor.set_current_line_highlight(restored_highlight);
cx.notify();
}),
cx.observe_global::<SettingsStore>(|editor, cx| {
if !editor.is_focused(cx) {
editor.set_current_line_highlight(CurrentLineHighlight::None);
editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
}
}),
]);
let original_multi_buffer_range = hunk.multi_buffer_range.clone();
let diff_base_range = hunk.diff_base_byte_range.clone();
editor.register_action::<RevertSelectedHunks>(move |_, cx| {
parent_editor
.update(cx, |editor, cx| {
let Some((buffer, original_text)) = editor.buffer().update(cx, |buffer, cx| {
let (_, buffer, _) =
buffer.excerpt_containing(original_multi_buffer_range.start, cx)?;
let original_text =
buffer.read(cx).diff_base()?.slice(diff_base_range.clone());
Some((buffer, Arc::from(original_text.to_string())))
}) else {
return;
};
buffer.update(cx, |buffer, cx| {
buffer.edit(
Some((
original_multi_buffer_range.start.text_anchor
..original_multi_buffer_range.end.text_anchor,
original_text,
)),
None,
cx,
)
});
})
.ok();
});
editor
.register_action::<RevertSelectedHunks>(move |_, cx| {
parent_editor
.update(cx, |editor, cx| {
let Some((buffer, original_text)) =
editor.buffer().update(cx, |buffer, cx| {
let (_, buffer, _) = buffer
.excerpt_containing(original_multi_buffer_range.start, cx)?;
let original_text =
buffer.read(cx).diff_base()?.slice(diff_base_range.clone());
Some((buffer, Arc::from(original_text.to_string())))
})
else {
return;
};
buffer.update(cx, |buffer, cx| {
buffer.edit(
Some((
original_multi_buffer_range.start.text_anchor
..original_multi_buffer_range.end.text_anchor,
original_text,
)),
None,
cx,
)
});
})
.ok();
})
.detach();
editor
});

View File

@@ -2,7 +2,7 @@ use std::{ops::Range, time::Duration};
use collections::HashSet;
use gpui::{AppContext, Task};
use language::BufferRow;
use language::{language_settings::language_settings, BufferRow};
use multi_buffer::{MultiBufferIndentGuide, MultiBufferRow};
use text::{BufferId, LineIndent, Point};
use ui::ViewContext;
@@ -37,13 +37,26 @@ impl Editor {
snapshot: &DisplaySnapshot,
cx: &mut ViewContext<Editor>,
) -> Option<Vec<MultiBufferIndentGuide>> {
let enabled = self.should_show_indent_guides(cx);
let show_indent_guides = self.should_show_indent_guides().unwrap_or_else(|| {
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
language_settings(buffer.read(cx).language(), buffer.read(cx).file(), cx)
.indent_guides
.enabled
} else {
true
}
});
if enabled {
Some(indent_guides_in_range(visible_buffer_range, snapshot, cx))
} else {
None
if !show_indent_guides {
return None;
}
Some(indent_guides_in_range(
visible_buffer_range,
self.should_show_indent_guides() == Some(true),
snapshot,
cx,
))
}
pub fn find_active_indent_guide_indices(
@@ -77,9 +90,14 @@ impl Editor {
if state.should_refresh() {
state.cursor_row = cursor_row;
let snapshot = snapshot.clone();
state.dirty = false;
if indent_guides.is_empty() {
return None;
}
let snapshot = snapshot.clone();
let task = cx
.background_executor()
.spawn(resolve_indented_range(snapshot, cursor_row));
@@ -131,6 +149,7 @@ impl Editor {
pub fn indent_guides_in_range(
visible_buffer_range: Range<MultiBufferRow>,
ignore_disabled_for_language: bool,
snapshot: &DisplaySnapshot,
cx: &AppContext,
) -> Vec<MultiBufferIndentGuide> {
@@ -143,7 +162,7 @@ pub fn indent_guides_in_range(
snapshot
.buffer_snapshot
.indent_guides_in_range(start_anchor..end_anchor, cx)
.indent_guides_in_range(start_anchor..end_anchor, ignore_disabled_for_language, cx)
.into_iter()
.filter(|indent_guide| {
// Filter out indent guides that are inside a fold

View File

@@ -1268,7 +1268,7 @@ pub mod tests {
ExcerptRange,
};
use futures::StreamExt;
use gpui::{Context, TestAppContext, WindowHandle};
use gpui::{Context, SemanticVersion, TestAppContext, WindowHandle};
use itertools::Itertools;
use language::{
language_settings::AllLanguageSettingsContent, Capability, FakeLspAdapter, Language,
@@ -1307,7 +1307,7 @@ pub mod tests {
async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(file_with_hints).unwrap(),
lsp::Uri::from_file_path(file_with_hints).unwrap().into(),
);
let current_call_id =
Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
@@ -1439,7 +1439,7 @@ pub mod tests {
async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(file_with_hints).unwrap(),
lsp::Uri::from_file_path(file_with_hints).unwrap().into(),
);
let current_call_id =
Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
@@ -1613,7 +1613,7 @@ pub mod tests {
async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path("/a/main.rs").unwrap(),
lsp::Uri::from_file_path("/a/main.rs").unwrap().into(),
);
let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
Ok(Some(vec![lsp::InlayHint {
@@ -1666,7 +1666,7 @@ pub mod tests {
async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path("/a/other.md").unwrap(),
lsp::Uri::from_file_path("/a/other.md").unwrap().into(),
);
let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
Ok(Some(vec![lsp::InlayHint {
@@ -1790,7 +1790,7 @@ pub mod tests {
Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(file_with_hints).unwrap(),
lsp::Uri::from_file_path(file_with_hints).unwrap().into(),
);
Ok(Some(vec![
lsp::InlayHint {
@@ -2136,7 +2136,7 @@ pub mod tests {
let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(file_with_hints).unwrap(),
lsp::Uri::from_file_path(file_with_hints).unwrap().into(),
);
Ok(Some(vec![lsp::InlayHint {
position: lsp::Position::new(0, i),
@@ -2305,7 +2305,7 @@ pub mod tests {
async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path("/a/main.rs").unwrap(),
lsp::Uri::from_file_path("/a/main.rs").unwrap().into(),
);
task_lsp_request_ranges.lock().push(params.range);
@@ -2673,11 +2673,11 @@ pub mod tests {
let task_editor_edited = Arc::clone(&closure_editor_edited);
async move {
let hint_text = if params.text_document.uri
== lsp::Url::from_file_path("/a/main.rs").unwrap()
== lsp::Uri::from_file_path("/a/main.rs").unwrap().into()
{
"main hint"
} else if params.text_document.uri
== lsp::Url::from_file_path("/a/other.rs").unwrap()
== lsp::Uri::from_file_path("/a/other.rs").unwrap().into()
{
"other hint"
} else {
@@ -2981,11 +2981,11 @@ pub mod tests {
let task_editor_edited = Arc::clone(&closure_editor_edited);
async move {
let hint_text = if params.text_document.uri
== lsp::Url::from_file_path("/a/main.rs").unwrap()
== lsp::Uri::from_file_path("/a/main.rs").unwrap().into()
{
"main hint"
} else if params.text_document.uri
== lsp::Url::from_file_path("/a/other.rs").unwrap()
== lsp::Uri::from_file_path("/a/other.rs").unwrap().into()
{
"other hint"
} else {
@@ -3177,7 +3177,7 @@ pub mod tests {
async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path("/a/main.rs").unwrap(),
lsp::Uri::from_file_path("/a/main.rs").unwrap().into(),
);
let query_start = params.range.start;
let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1;
@@ -3244,7 +3244,7 @@ pub mod tests {
async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(file_with_hints).unwrap(),
lsp::Uri::from_file_path(file_with_hints).unwrap().into(),
);
let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
@@ -3361,7 +3361,7 @@ pub mod tests {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
theme::init(theme::LoadThemes::JustBase, cx);
release_channel::init("0.0.0", cx);
release_channel::init(SemanticVersion::default(), cx);
client::init_settings(cx);
language::init(cx);
Project::init_settings(cx);

View File

@@ -13,8 +13,7 @@ use gpui::{
VisualContext, WeakView, WindowContext,
};
use language::{
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, OffsetRangeExt,
Point, SelectionGoal,
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, Point, SelectionGoal,
};
use multi_buffer::AnchorRangeExt;
use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath};
@@ -235,7 +234,7 @@ impl FollowableItem for Editor {
fn to_follow_event(event: &EditorEvent) -> Option<workspace::item::FollowEvent> {
match event {
EditorEvent::Edited => Some(FollowEvent::Unfollow),
EditorEvent::Edited { .. } => Some(FollowEvent::Unfollow),
EditorEvent::SelectionsChanged { local }
| EditorEvent::ScrollPositionChanged { local, .. } => {
if *local {
@@ -904,7 +903,7 @@ impl Item for Editor {
f(ItemEvent::UpdateBreadcrumbs);
}
EditorEvent::Reparsed => {
EditorEvent::Reparsed(_) => {
f(ItemEvent::UpdateBreadcrumbs);
}
@@ -1008,6 +1007,25 @@ impl SearchableItem for Editor {
self.has_background_highlights::<SearchWithinRange>()
}
fn toggle_filtered_search_ranges(&mut self, enabled: bool, cx: &mut ViewContext<Self>) {
if self.has_filtered_search_ranges() {
self.previous_search_ranges = self
.clear_background_highlights::<SearchWithinRange>(cx)
.map(|(_, ranges)| ranges)
}
if !enabled {
return;
}
let ranges = self.selections.disjoint_anchor_ranges();
if ranges.iter().any(|range| range.start != range.end) {
self.set_search_within_ranges(&ranges, cx);
} else if let Some(previous_search_ranges) = self.previous_search_ranges.take() {
self.set_search_within_ranges(&previous_search_ranges, cx)
}
}
fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
let setting = EditorSettings::get_global(cx).seed_search_query_from_cursor;
let snapshot = &self.snapshot(cx).buffer_snapshot;
@@ -1016,9 +1034,14 @@ impl SearchableItem for Editor {
match setting {
SeedQuerySetting::Never => String::new(),
SeedQuerySetting::Selection | SeedQuerySetting::Always if !selection.is_empty() => {
snapshot
let text: String = snapshot
.text_for_range(selection.start..selection.end)
.collect()
.collect();
if text.contains('\n') {
String::new()
} else {
text
}
}
SeedQuerySetting::Selection => String::new(),
SeedQuerySetting::Always => {
@@ -1135,58 +1158,64 @@ impl SearchableItem for Editor {
let search_within_ranges = self
.background_highlights
.get(&TypeId::of::<SearchWithinRange>())
.map(|(_color, ranges)| {
ranges
.iter()
.map(|range| range.to_offset(&buffer))
.collect::<Vec<_>>()
.map_or(vec![], |(_color, ranges)| {
ranges.iter().map(|range| range.clone()).collect::<Vec<_>>()
});
cx.background_executor().spawn(async move {
let mut ranges = Vec::new();
if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() {
if let Some(search_within_ranges) = search_within_ranges {
for range in search_within_ranges {
let offset = range.start;
ranges.extend(
query
.search(excerpt_buffer, Some(range))
.await
.into_iter()
.map(|range| {
buffer.anchor_after(range.start + offset)
..buffer.anchor_before(range.end + offset)
}),
);
}
let search_within_ranges = if search_within_ranges.is_empty() {
vec![None]
} else {
ranges.extend(query.search(excerpt_buffer, None).await.into_iter().map(
|range| buffer.anchor_after(range.start)..buffer.anchor_before(range.end),
));
search_within_ranges
.into_iter()
.map(|range| Some(range.to_offset(&buffer)))
.collect::<Vec<_>>()
};
for range in search_within_ranges {
let buffer = &buffer;
ranges.extend(
query
.search(excerpt_buffer, range.clone())
.await
.into_iter()
.map(|matched_range| {
let offset = range.clone().map(|r| r.start).unwrap_or(0);
buffer.anchor_after(matched_range.start + offset)
..buffer.anchor_before(matched_range.end + offset)
}),
);
}
} else {
for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) {
if let Some(next_excerpt) = excerpt.next {
let excerpt_range =
next_excerpt.range.context.to_offset(&next_excerpt.buffer);
ranges.extend(
query
.search(&next_excerpt.buffer, Some(excerpt_range.clone()))
.await
.into_iter()
.map(|range| {
let start = next_excerpt
.buffer
.anchor_after(excerpt_range.start + range.start);
let end = next_excerpt
.buffer
.anchor_before(excerpt_range.start + range.end);
buffer.anchor_in_excerpt(next_excerpt.id, start).unwrap()
..buffer.anchor_in_excerpt(next_excerpt.id, end).unwrap()
}),
);
}
let search_within_ranges = if search_within_ranges.is_empty() {
vec![buffer.anchor_before(0)..buffer.anchor_after(buffer.len())]
} else {
search_within_ranges
};
for (excerpt_id, search_buffer, search_range) in
buffer.excerpts_in_ranges(search_within_ranges)
{
ranges.extend(
query
.search(&search_buffer, Some(search_range.clone()))
.await
.into_iter()
.map(|match_range| {
let start = search_buffer
.anchor_after(search_range.start + match_range.start);
let end = search_buffer
.anchor_before(search_range.start + match_range.end);
buffer.anchor_in_excerpt(excerpt_id, start).unwrap()
..buffer.anchor_in_excerpt(excerpt_id, end).unwrap()
}),
);
}
}
};
ranges
})
}

View File

@@ -0,0 +1,150 @@
use std::ops::Range;
use collections::HashMap;
use itertools::Itertools;
use text::{AnchorRangeExt, BufferId, ToPoint};
use ui::ViewContext;
use util::ResultExt;
use crate::Editor;
#[derive(Clone, Default)]
pub(super) struct LinkedEditingRanges(
/// Ranges are non-overlapping and sorted by .0 (thus, [x + 1].start > [x].end must hold)
pub HashMap<BufferId, Vec<(Range<text::Anchor>, Vec<Range<text::Anchor>>)>>,
);
impl LinkedEditingRanges {
pub(super) fn get(
&self,
id: BufferId,
anchor: Range<text::Anchor>,
snapshot: &text::BufferSnapshot,
) -> Option<&(Range<text::Anchor>, Vec<Range<text::Anchor>>)> {
let ranges_for_buffer = self.0.get(&id)?;
let lower_bound = ranges_for_buffer
.partition_point(|(range, _)| range.start.cmp(&anchor.start, snapshot).is_le());
if lower_bound == 0 {
// None of the linked ranges contains `anchor`.
return None;
}
ranges_for_buffer
.get(lower_bound - 1)
.filter(|(range, _)| range.end.cmp(&anchor.end, snapshot).is_ge())
}
pub(super) fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
pub(super) fn refresh_linked_ranges(this: &mut Editor, cx: &mut ViewContext<Editor>) -> Option<()> {
if this.pending_rename.is_some() {
return None;
}
let project = this.project.clone()?;
let buffer = this.buffer.read(cx);
let mut applicable_selections = vec![];
let selections = this.selections.all::<usize>(cx);
let snapshot = buffer.snapshot(cx);
for selection in selections {
let cursor_position = selection.head();
let start_position = snapshot.anchor_before(cursor_position);
let end_position = snapshot.anchor_after(selection.tail());
if start_position.buffer_id != end_position.buffer_id || end_position.buffer_id.is_none() {
// Throw away selections spanning multiple buffers.
continue;
}
if let Some(buffer) = end_position.buffer_id.and_then(|id| buffer.buffer(id)) {
applicable_selections.push((
buffer,
start_position.text_anchor,
end_position.text_anchor,
));
}
}
if applicable_selections.is_empty() {
return None;
}
this.linked_editing_range_task = Some(cx.spawn(|this, mut cx| async move {
let highlights = project
.update(&mut cx, |project, cx| {
let mut linked_edits_tasks = vec![];
for (buffer, start, end) in &applicable_selections {
let snapshot = buffer.read(cx).snapshot();
let buffer_id = buffer.read(cx).remote_id();
let linked_edits_task = project.linked_edit(&buffer, *start, cx);
let highlights = move || async move {
let edits = linked_edits_task.await.log_err()?;
// Find the range containing our current selection.
// We might not find one, because the selection contains both the start and end of the contained range
// (think of selecting <`html>foo`</html> - even though there's a matching closing tag, the selection goes beyond the range of the opening tag)
// or the language server may not have returned any ranges.
let start_point = start.to_point(&snapshot);
let end_point = end.to_point(&snapshot);
let _current_selection_contains_range = edits.iter().find(|range| {
range.start.to_point(&snapshot) <= start_point
&& range.end.to_point(&snapshot) >= end_point
});
if _current_selection_contains_range.is_none() {
return None;
}
// Now link every range as each-others sibling.
let mut siblings: HashMap<Range<text::Anchor>, Vec<_>> = Default::default();
let mut insert_sorted_anchor =
|key: &Range<text::Anchor>, value: &Range<text::Anchor>| {
siblings.entry(key.clone()).or_default().push(value.clone());
};
for items in edits.into_iter().combinations(2) {
let Ok([first, second]): Result<[_; 2], _> = items.try_into() else {
unreachable!()
};
insert_sorted_anchor(&first, &second);
insert_sorted_anchor(&second, &first);
}
let mut siblings: Vec<(_, _)> = siblings.into_iter().collect();
siblings.sort_by(|lhs, rhs| lhs.0.cmp(&rhs.0, &snapshot));
Some((buffer_id, siblings))
};
linked_edits_tasks.push(highlights());
}
linked_edits_tasks
})
.log_err()?;
let highlights = futures::future::join_all(highlights).await;
this.update(&mut cx, |this, cx| {
this.linked_edit_ranges.0.clear();
if this.pending_rename.is_some() {
return;
}
for (buffer_id, ranges) in highlights.into_iter().flatten() {
this.linked_edit_ranges
.0
.entry(buffer_id)
.or_default()
.extend(ranges);
}
for (buffer_id, values) in this.linked_edit_ranges.0.iter_mut() {
let Some(snapshot) = this
.buffer
.read(cx)
.buffer(*buffer_id)
.map(|buffer| buffer.read(cx).snapshot())
else {
continue;
};
values.sort_by(|lhs, rhs| lhs.0.cmp(&rhs.0, &snapshot));
}
cx.notify();
})
.log_err();
Some(())
}));
None
}

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