Compare commits

...

127 Commits

Author SHA1 Message Date
Mikayla Maki
8b55cb40c1 Restore test running 2024-05-29 19:38:00 -07:00
Mikayla Maki
5a149b970c Make tests less noisy (#12463)
When running the tests for linux, I found a lot of benign errors getting
logged. This PR cuts down some of the noise from unnecessary workspace
serialization and SVG renders

Release Notes:

- N/A
2024-05-29 18:06:45 -07:00
Marshall Bowers
bdf627ce07 rustdoc_to_markdown: Fix code blocks (#12460)
This PR fixes an issue in `rustdoc_to_markdown` with code blocks being
trimmed incorrectly.

We were erroneously popping from the current element stack even if we
didn't push an element onto the stack.

Added test coverage for this case as well, so we don't regress.

Release Notes:

- N/A
2024-05-29 19:23:06 -04:00
Marshall Bowers
a5011996fb rustdoc_to_markdown: Recognize Rust code blocks (#12458)
This PR makes it so Rust code blocks are recognized and
syntax-highlighted when converting from rustdoc to Markdown.

Release Notes:

- N/A
2024-05-29 18:57:20 -04:00
Nathan Sobo
b8d9713b4f Make prompt library icon in context panel staff-only for now (#12457)
This is still pretty raw, so I'd like to hold off on shipping it to all
users.

Release Notes:

- Hide the prompt library for non-staff until it is in a more complete
state.
2024-05-29 16:53:45 -06:00
Marshall Bowers
abec028e58 rustdoc_to_markdown: Clean up heading spacing (#12456)
This PR cleans up the spacing around the Markdown headings in the output
so that they are consistent.

Release Notes:

- N/A
2024-05-29 18:39:51 -04:00
Marshall Bowers
08881828ce assistant: Add /rustdoc slash command (#12453)
This PR adds a `/rustdoc` slash command for retrieving and inserting
rustdoc docs into the Assistant.

Right now the command accepts the crate name as an argument and will
return the top-level docs from `docs.rs`.

Release Notes:

- N/A
2024-05-29 18:14:29 -04:00
Max Brunsfeld
dd328efaa7 Compute git statuses using the bundled git executable, not libgit2 (#12444)
I realized that somehow, the `git` executable is able to compute `git
status` much more quickly than libgit2, so I've switched our git status
logic to use `git`. Follow-up to
https://github.com/zed-industries/zed/pull/12266.

Release Notes:

- Improved the performance of git status updated when working in large
git repositories.
2024-05-29 14:31:24 -07:00
Joshua Ferguson
6294a3b80b Add xdg trash support (#12391)
- Added support for xdg trash when deleting files on linux
- moved ashpd depency to toplevel to use it in both fs and gpui

If I need to add test, or change anything, please let me know. I tested
locally by creating and deleting a file and confirming it showed up in
my trashcan, but that probably a less than ideal method of confirming
correct behavior

Also, I could remove the delete directory function for linux, and change
the one configured for macos to compile for both macos and linux (they
are the same, the version of the function they are calling is
different).

Release Notes:

- N/A
2024-05-29 14:15:29 -07:00
Kirill Bulatov
0f927fa6fb One less unwrap (#12448)
Fixes
https://zed-industries.slack.com/archives/C04S6T1T7TQ/p1717011343884699

Release Notes:

- N/A
2024-05-29 23:44:56 +03:00
Marshall Bowers
5bcb9ed017 Add rustdoc_to_markdown crate (#12445)
This PR adds a new crate for converting rustdoc output to Markdown.

We're leveraging Servo's `html5ever` to parse the Markdown content, and
then walking the DOM nodes to convert it to a Markdown string.

The Markdown output will be continued to be refined, but it's in a place
where it should be reasonable.

Release Notes:

- N/A
2024-05-29 16:05:16 -04:00
Bennet Bo Fenner
a22cd95f9d Fix deleted hunk offset when zooming (#12442)
Release Notes:

- Fixed an issue where expanded hunks could be rendered at the wrong
position when zooming
- Fixed an issue where expanded hunks could be rendered at the wrong
position when toggling git blame
([#11941](https://github.com/zed-industries/zed/issues/11941))
2024-05-29 20:06:10 +02:00
Dzmitry Malyshau
44c50da94f blade: Use BufferBelt from blade-utils (#12411)
Release Notes:

- N/A

Follow-up to #12340
Carries https://github.com/kvark/blade/pull/122 and
https://github.com/kvark/blade/pull/119
2024-05-29 09:50:45 -07:00
Joseph T. Lyons
c34d36161d v0.139.x dev 2024-05-29 12:15:12 -04:00
Max Brunsfeld
2772f87198 Make sure not to signal the main thread on fs events in ignored directories (#12436)
Release Notes:

- N/A
2024-05-29 09:11:28 -07:00
Antonio Scandurra
66affa969a Show recently-opened files when autocompleting /file without arguments (#12434)
<img width="1588" alt="image"
src="https://github.com/zed-industries/zed/assets/482957/ea63b046-64d6-419e-8135-4863748b58fa">


Release Notes:

- N/A
2024-05-29 17:46:18 +02:00
Bennet Bo Fenner
dca5bc5986 Fix editor scrolling when closing tab using middle mouse button (#12429)
#12005 introduced a side effect, that would cause the editor to react to
a scroll event when using the middle mouse button to close a tab

Before:


https://github.com/zed-industries/zed/assets/53836821/46cb2e71-e9d5-477c-b34a-dc9b6bc9b1f1

After:


https://github.com/zed-industries/zed/assets/53836821/6ada7985-97c9-4b52-848f-c7f9461c5216

Release Notes:

- Fixed an issue where the editor would scroll upwards if other tabs
were closed using the middle mouse button
2024-05-29 16:07:59 +02:00
Antonio Scandurra
1db33b5f4e Always include context when performing inline transformation (#12426)
Release Notes:

- Improved clarity for inline transformations by always including the
active assistant context.
2024-05-29 15:17:01 +02:00
Bennet Bo Fenner
16745542b5 Fix hunk diff hitbox (#12425)
You can see in the video that hunks were getting toggled even when
clicking on the scrollbar.


https://github.com/zed-industries/zed/assets/53836821/11f201e8-c5f1-49c8-a4d2-ac3b3343b5a8

This happened because the hitbox was actually extended to the left of
the hunk indicator, I believe that was done so that "Removed" hunk
indicators rendered correctly (Red triangles)

Fixed:


https://github.com/zed-industries/zed/assets/53836821/7b451b37-da85-4e56-9127-2a5512bd9e7a

Release Notes:

- Fixed hunk indicators getting expanded when clicking near them (but
not on them)
2024-05-29 14:45:27 +02:00
Antonio Scandurra
a0644ac601 Allow specifying a custom limit for /search results (#12423)
<img width="497" alt="image"
src="https://github.com/zed-industries/zed/assets/482957/94e94326-fb3c-4f9b-b4d9-7dd6f6f8d537">


e.g.

```
/search --9 foobar
```

Release Notes:

- N/A
2024-05-29 14:11:05 +02:00
Piotr Osiewicz
f3e6a0beab project panel: Copy dragged entry when opt is pressed (#12348)
Partially fixes #5119 
TODO:
- [ ] Change cursor style to Copy when dragging an entry with opt
pressed.

Release Notes:

- Drag-and-dropping a project panel entry with opt modifier pressed now
copies the entry instead of moving it.
2024-05-29 12:34:58 +02:00
Antonio Scandurra
4acfab689e Fix logic errors in RateLimiter (#12421)
This pull request fixes two issues in `RateLimiter` that caused
excessive rate-limiting to take place:

- c19083a35c fixes a mistake that caused
us to load buckets from the database incorrectly and set the
`refill_time_per_token` to equal the `refill_duration`. This was the
primary reason why rate limiting was acting oddly.
- 34b88d14f6 fixes another slight logic
error that caused tokens to be underprovisioned. This was minor compared
to the bug above.

Release Notes:

- N/A
2024-05-29 12:05:40 +02:00
Jason Lee
3c6c850390 task: Add re-run task button to terminal title (#12379)
Release Notes:

- Added re-run task button to terminal title.

Close #12277

## Demo


https://github.com/zed-industries/zed/assets/5518/4cd05fa5-4255-412b-8583-68e22f86561e

---------

Co-authored-by: Piotr Osiewicz <piotr@zed.dev>
2024-05-29 11:40:43 +02:00
Piotr Osiewicz
36d0b71f27 project panel: adjust right border and make active entry more prominent when panel is not focused (#12420)
Release Notes:

- N/A
2024-05-29 11:34:59 +02:00
Kirill Bulatov
ef58509797 Show symlink tooltip on icon hover only (#12419)
Small follow-up of https://github.com/zed-industries/zed/pull/12263 

Release Notes:

- N/A
2024-05-29 12:05:53 +03:00
Bennet Bo Fenner
cad3d2d355 inline assistant: Fix overlapping wrap/indent guides (#12417)
Before:

<img width="603" alt="image"
src="https://github.com/zed-industries/zed/assets/53836821/08b7566c-4afa-40b6-8e73-4b03d26572b7">

After:

<img width="539" alt="image"
src="https://github.com/zed-industries/zed/assets/53836821/ebd69e15-c1e1-485e-9c11-35b4316ede40">


Fixes #9819

Release Notes:

- Fix wrap guides overlapping with the inline assistant
([#9819](https://github.com/zed-industries/zed/issues/9819)).
2024-05-29 10:05:41 +02:00
Nathan Sobo
c03600c55e Stop silently appending a system prompt for edit formatting (#12407)
We should reintroduce this as part of the prompt library.

Release Notes:

- Removed an over-eager system prompt from the assistant that was
causing misbehavior. Going forward, our intent is to always let you
observe and edit text before we send it.

---------

Co-authored-by: Marshall <marshall@zed.dev>
2024-05-28 20:31:58 -06:00
Joseph T. Lyons
30e498a1c1 Fix GitHub Issue-creation commands 2024-05-28 21:51:24 -04:00
Marshall Bowers
82a57a121a Disable calls.share_on_join by default (#12401)
This PR changes the default value of the `calls.share_on_join` setting
from `true` to `false`.

Nathan mentioned that project sharing should be opt-in so that projects
aren't shared unless you intend for them to be.

Release Notes:

- Changed the default `calls.share_on_join` value to `false`.
2024-05-28 19:28:07 -04:00
Danielle Maywood
7969a10643 Support setting custom background color for syntax highlighting (#12400)
Release Notes:

- Added support for `background_color` in `syntax` map in `theme.json`.

This adds support for setting a `background_color` for styles inside the
`syntax` map for themes defined in `theme.json`. The field is optional
so there should be no backwards compatibility issues.

It is worth noting that the current behaviour for selecting text is that
the background colours will mix/blend (I'm not sure the correct term
here). Changing this behaviour, or making it configurable, looks to be a
far more complex issue and I'm not sure I know how to do it.
2024-05-28 19:25:09 -04:00
barseek
fcb4abf18b Update Elixir docs (#12349)
This PR updates the Elixir docs with a note on how to switch to using
other language server.

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-05-28 18:52:22 -04:00
Marshall Bowers
cd77e72479 docs: Demonstrate disabling solargraph when enabling ruby-lsp (#12399)
This PR fixes a small issue in the Ruby docs, where we weren't properly
demonstrating that `solargraph` should be disabled when enabling
`ruby-lsp`.

Release Notes:

- N/A
2024-05-28 18:50:00 -04:00
Nathan Sobo
bde3056e84 Increase rate limit on LLM requests to 1000/hr (via env var) (#12396)
Release Notes:

- N/A

Co-authored-by: Marshall <marshall@zed.dev>
2024-05-28 14:20:25 -06:00
moshyfawn
6a48e3b8c6 ruby: Bump to v0.0.6 (#12395)
Release Notes:

- N/A

Related: #12392
2024-05-28 16:14:38 -04:00
moshyfawn
fb911bc478 ruby: Add Podfile as Ruby extension language path suffix (#12392)
Release Notes:

- N/A

Closes zed-industries/extensions#803
2024-05-28 15:58:48 -04:00
Max Brunsfeld
726026a267 Tweak client reconnect timing (#12393)
* Start with a longer duration
* Widen the range used for randomizing the duration between retries
* Increase the maximum duration between retries

Release Notes:

- N/A
2024-05-28 12:42:10 -07:00
Raunak Raj
da70741ece linux: Implement should_auto_hide_scrollbars (#12366)
Implemented the should_auto_hide_scrollbars method for Linux using
`xdg_desktop_portal` approach

Release Notes:

- N/A
2024-05-28 11:50:14 -07:00
Dzmitry Malyshau
08e3840379 Update blade to pick up the descriptor initialization changes (#12340)
Release Notes:

- N/A

Picks up https://github.com/kvark/blade/pull/118
Fixes #10351

Seeing that Zed loaded with Blade repository is consuming 260Mb of RAM.
We can tune this to be lower, but ultimately it doesn't matter: this
memory isn't wasted, it's just pools for memory and descriptors, which
may be used by bigger and more complex views of Zed.
2024-05-28 11:41:46 -07:00
Piotr Osiewicz
ff9d6cc512 project panel: Remove active selection border when project panel is not focused (#12385)
This should make it less attention-grabbing.

Release Notes:

- N/A
2024-05-28 19:12:28 +02:00
Marshall Bowers
01e86881f0 Add placeholder for extension slash commands (#12387)
This PR adds a slightly better placeholder for extension slash commands
than the current "TODO" text.

Right now we display the command name and an arbitrary icon.

<img width="644" alt="Screenshot 2024-05-28 at 12 15 07 PM"
src="https://github.com/zed-industries/zed/assets/1486634/11761797-5ccc-4209-8b00-70b714f10a78">

Release Notes:

- N/A
2024-05-28 12:16:31 -04:00
Antonio Scandurra
ac7aca335d Fix assistant message header padding (#12386)
Release Notes:

- N/A

Co-authored-by: Nathan <nathan@zed.dev>
2024-05-28 18:10:35 +02:00
Antonio Scandurra
371abd37f6 Introduce a new /tabs command (#12382)
This inserts the content of the open tabs sorted by recency.

Release Notes:

- N/A

---------

Co-authored-by: Nathan <nathan@zed.dev>
2024-05-28 17:17:34 +02:00
Antonio Scandurra
b466a8b828 Remove the assistant2 crate (#12380)
Release Notes:

- N/A
2024-05-28 16:26:35 +02:00
Antonio Scandurra
59662fbeb6 Introduce /search command to assistant (#12372)
This pull request introduces semantic search to the assistant using a
slash command:


https://github.com/zed-industries/zed/assets/482957/62f39eae-d7d5-46bf-a356-dd081ff88312

Moreover, this also adds a status to pending slash commands, so that we
can show when a query is running or whether it failed:

<img width="1588" alt="image"
src="https://github.com/zed-industries/zed/assets/482957/e8d85960-6275-4552-a068-85efb74cfde1">

I think this could be better design-wise, but seems like a pretty good
start.

Release Notes:

- N/A
2024-05-28 16:06:09 +02:00
Piotr Osiewicz
016a1444a7 project panel: Allow selecting multiple entries & add support for multiselect actions (#12363)
Fixes #4983 
TODO:
- [x] Improve release note.
- [x] Tests

Release Notes:

- Project panel now supports selecting multiple entries via cmd-click
and shift-click/shift-up/shift-down.
- Added support for handling multiple selected project panel entries to
Cut, Copy, Trash and Delete actions.
2024-05-28 15:51:23 +02:00
Bennet Bo Fenner
1cf3496fda indent guides: Adjust handling of folded lines (#12375)
Release Notes:

- N/A
2024-05-28 14:54:33 +02:00
Bennet Bo Fenner
df4c60e36c docs: Indent guides (#12373)
Release Notes:

- N/A
2024-05-28 14:41:39 +02:00
Patryck
a03813a471 Add icon and hover description for symlinks (#12263)
![image](https://github.com/zed-industries/zed/assets/12102857/c6ed8140-1256-4618-aa46-64e66becdf7e)


![Screenshot_20240524_201346](https://github.com/zed-industries/zed/assets/12102857/2577067c-4f61-486a-8613-14941555f5a8)

Resolves https://github.com/zed-industries/zed/issues/12142


Release Notes:

- Added in project panel an icon and hover description for symlinks ([12142](https://github.com/zed-industries/zed/issues/12142))
2024-05-28 09:50:58 +03:00
Owen Law
f7115be3d1 Add Flatpak build system and support (#12006)
ping #6687 

This is the third iteration of this PR ([v2
here](https://github.com/zed-industries/zed/pull/11949)) and uses a
different approach to the first two (the process wrapper lib was a
maintainability nightmare). While the first two attempted to spawn the
necessary processes using flatpak-spawn and host-spawn from the app
inside the sandbox, this version first spawns the cli binary which then
restart's itself *outside* of the sandbox using flatpak-spawn. The
restarted cli process than can call the bundled app binary normally,
with no need for flatpak-spawn because it is already outside of the
sandbox. This is done instead of keeping the cli in the sandbox because
ipc becomes very difficult and broken when trying to do it across the
sandbox.

Gnome software (example using nightly channel and release notes
generated using the script):
<img
src="https://github.com/zed-industries/zed/assets/81528246/6391d217-0f44-4638-9569-88c46e5fc4ba"
width="600"/>

TODO in this PR:
- [x] Bundle libs.
- [x] Cleanup release note converter.

Future work:

- [ ] Auto-update dialog
- [ ] Flatpak auto-update (complete 'Auto-update dialog' first)
- [ ] Experimental
[bundle](https://docs.flatpak.org/en/latest/single-file-bundles.html)
releases for feedback (?).

*(?) = Maybe / Request for feedback*

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2024-05-27 19:01:20 -06:00
Antonio Scandurra
7e3ab9acc9 Rework context insertion UX (#12360)
- Confirming a completion now runs the command immediately
- Hitting `enter` on a line with a command now runs it
- The output of commands gets folded away and replaced with a custom
placeholder
- Eliminated ambient context

<img width="1588" alt="image"
src="https://github.com/zed-industries/zed/assets/482957/b1927a45-52d6-4634-acc9-2ee539c1d89a">

Release Notes:

- N/A

---------

Co-authored-by: Nathan Sobo <nathan@zed.dev>
2024-05-27 17:44:54 -06:00
Piotr Osiewicz
20f37f0647 chore: Change git deps to crates.io dependencies where possible (#12362)
Release Notes:

- N/A
2024-05-27 23:32:51 +02:00
Nate Butler
a6dd2ca694 Allow saving prompts from the Prompt Manager (#12359)
Adds the following features to the prompt manager:

- New prompt – Create a new prompt from the UI. It will only persist if
it is saved.
- Save prompt – Save a prompt by clicking the save button in the UI. A
keybinding will be added for this in the future.
- Reveal prompt - Show the selected prompt on the file system. Only
available for saved prompts.

New prompts that are saved will use the
`{slugified_title}_{ver}_{id}.md` format which all imported prompts will
move to in the near future.

Also orders prompts in alphabetical order by default.

Release Notes:

- N/A
2024-05-27 13:48:21 -04:00
Piotr Osiewicz
345361cd38 json: Register tasks on Rust side and not via tasks.json (#12345)
/cc @RemcoSmitsDev new task indicators weren't showing for me in JSON
files.
`tasks.json` of native grammars is not being read by anything by
default, so we tend to register tasks as Rust structs, foregoing the
deserialization step. This doesn't apply to tasks registered in
extensions, which have to have tasks.json.

Release Notes:

- N/A
2024-05-27 11:50:45 +02:00
Nathan Sobo
e19339bc1d Allow UI font weight to be assigned in settings (#12333)
Release Notes:

- Added the ability to configure the weight of your UI font in standard
CSS weight units from 0 to 900.
2024-05-26 23:06:58 -06:00
Fernando Tagawa
6276281e8a linux: Fix IBus in vim mode and some compose state fixes (#12299)
Release Notes:

- N/A

Fixes #12198 and some minor fixes:
* IBus was intercepting normal keys like `a`, `k` which caused some
problems in vim mode.
* Wayland: Trying to commit the pre_edit on click wasn't working
properly, should be fixed now.
* X11: The pre_edit was supposed to be cleared when losing keyboard
focus.
* X11: We should commit the pre_edit on click.

---------

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2024-05-26 17:17:38 -07:00
派卡 (pi-cla)
74c972fa37 Fix openSUSE Vulkan package name (#12301)
Release Notes:

- N/A
2024-05-26 17:14:09 -07:00
citorva
a84344a82e linux: Get the color scheme through xdg-desktop-portal (#11926)
The method has been tested on:
- Gnome 46 (Working)
- Gnome 40 (Not supported)

Tasks

- [x] Implements a draft which get and provides the user theme to
components which needs it
- [x] Implements a way to call the callback function when the theme is
updated
- [X] Cleans the code

Release notes:
- N/A
2024-05-26 17:00:10 -07:00
Jakob Hellermann
b1cfd46d37 Fix ctrl click to open file on windows (#12294)
There were two issues:
1. the `ModifiersChanged` event was never emitted on windows.
macOS, x11 and wayland have separate events for this, while on windows
they are sent via the usual `keyup` and `keydown` events, but
`parse_keydown_msg_keystroke` just ignored them.
2. the word segmenting regex didn't include '\' so paths weren't
correctly detected

fixes https://github.com/zed-industries/zed/issues/12321

Release Notes:

- N/A
2024-05-26 16:57:35 -07:00
Jakob Hellermann
e54455bcad Call OleInitialize to fix DirectWrite in TestPlatform on windows (#12289)
Running the tests on windows currently fails for every gpui test using
the `TestPlatform` with

```rs
called `Result::unwrap()` on an `Err` value: CoInitialize has not been called. (0x800401F0)
```
trying to call `CoCreateInstance`in the `DirectWriteComponent`.

The `WindowsPlatform` calls
[`OleInitialize`](https://learn.microsoft.com/de-de/windows/win32/api/ole2/nf-ole2-oleinitialize)
which internally calls `CoInitializeEx` so I just copied that to the
`TestPlatform`.

Release Notes:

- N/A
2024-05-26 16:56:09 -07:00
Robert Borghese
9cc5ba86d1 windows: Make "Reveal in Finder" always open the file explorer (#12207)
At the current moment, the "Reveal in Finder" behavior on Windows
"opens" the file using direct execution. This causes files to be opened
with whatever software they are associated with (i.e. will open Sublime
Text instead of the file explorer).

Release Notes:

- Fixed "Reveal in Finder" on Windows to open with the File Explorer.
The new behavior always opens the file explorer with the target
folder/file pre-selected.


https://github.com/zed-industries/zed/assets/28355157/b8ba471d-2f5b-4529-90c3-4dc59f308b99
2024-05-26 16:55:35 -07:00
Maksim Bondarenkov
9e3a182177 docs: Add an entry for msys2 package (#12287)
a package for Zed is now avialable in msys2 repository so let the users
know about it. [package
page](https://packages.msys2.org/base/mingw-w64-zed)

Release Notes:

- N/A
2024-05-26 16:55:26 -07:00
Maksim Bondarenkov
b36d62887e Update winapi-util crate (#12239)
starting with v0.1.7, winapi-util switched to windows-sys crate. this
small update will reduce dependence from winapi crate

Release Notes:

- N/A
2024-05-26 16:49:18 -07:00
Jakob Hellermann
8e07fd2214 Update time crate to fix compilation error (#12189)
run `cargo update --package time` due to
https://github.com/time-rs/time/issues/681

Release Notes:

- N/A
2024-05-26 16:44:15 -07:00
apricotbucket28
d8605c8614 x11: Implement various window functions (#12070)
This (mostly) allows the CSD added in
https://github.com/zed-industries/zed/pull/11525 to work in X11. It's
still a bit buggy as it detects a second window drag right after the
first one finishes, but it's probably better to change the way window
drags are detected in the title bar itself (as that causes other
issues).

The CSD can be tested by changing the return value of
`should_render_window_controls` to true.

Also fixes F11 crashing.

Release Notes:

- N/A
2024-05-26 16:43:24 -07:00
张小白
c0259a448d windows: Slightly improve font rendering quality (#12015)
Upper before this PR, lower after.

![Screenshot 2024-05-20
144852](https://github.com/zed-industries/zed/assets/14981363/88995482-3a98-41be-9c2c-6b781bef6ad2)

This PR manually applies a MSAA to the font atlas. Before this PR, the
font may seem aliased ( espeacially on low DPI monitors ), that's
because `DirectWrite` and `CoreText` ( on which currently `Zed` built
the whole text system ) uses different anti-aliasing strategy. The
different anti-aliasing approach used by `DirectWrite` and `CoreText`:

![Screenshot 2024-05-20
151114](https://github.com/zed-industries/zed/assets/14981363/21a2fc1e-48a2-4cff-a9d1-41602eff3658)

The upper is `VSCode` font rendering result, middle `macOS`, lower this
PR ( pic captured with same font face, same font size, same DPI, same
physical resolution, same editor theme, and same magnification rate ).

This PR brings a quality similiar to `CoreText`. What's more, from the
`VSCode` image, you can see how `DirectWrite` sub-pixel anti-aliasing is
performed on the edge of the glyph. Can we achieve the same rendering
quality? Currently, No. `Zed` use a grayscale image to render glyph, and
a sub-pixel anti-aliasing `DirectWrite` requires all RGB channels and
the foreground color of the rendering glyph, which `Zed` dose not
provide.

So, to achieve the quality of `VSCode` font rendering, the text system
of `Zed` needs much much more efforts to refactor the codes.


Release Notes:

- N/A
2024-05-26 16:41:55 -07:00
Mikayla Maki
a9e3d4ec4e Improve context expansion (#10957)
Release Notes:

- Improved expand excerpt indicators to allow unidirectional expansion.
Also added the `editor::ExpandExcerptsUp` and
`editor::ExpandExcerptsDown` actions, which can both take a `lines`
parameter. Also added a `expand_excerpt_lines` setting which controls
the default number of lines that the indicators and actions use.

---------

Co-authored-by: conrad <conrad@zed.dev>
2024-05-26 16:30:09 -07:00
Piotr Osiewicz
a0f91299dd task: Do not wrap custom task variables in braces (#12322)
Fixes #10998

Release Notes:

- N/A
2024-05-27 00:15:53 +02:00
Piotr Osiewicz
71451b59cd php/ruby: bump version to 0.0.5 (#12330)
Includes: https://github.com/zed-industries/zed/pull/12237

Release Notes:

- N/A
2024-05-27 00:07:33 +02:00
Anıl Şenay
ddb551c794 go: Add runnables for Go (#12003)
Implemented runnables for specially for running tests for Go.

I'm grateful for your feedback because this is my first experience with
Rust and Zed codebase.
![resim](https://github.com/zed-industries/zed/assets/1047345/789b31da-554f-47cd-a08c-444eced104f4)

https://github.com/zed-industries/zed/assets/1047345/ae1abd9e-3657-4322-9c28-02d0752b5ccd


Release Notes:

- Added Runnables/Tasks for:
  - Run test functions which start with "Test"
  - Run subtests
  - Run benchmark tests
  - Run main function



---------

Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2024-05-26 15:16:52 +02:00
Remco Smits
5665cad250 json: Add runnable for package.json and composer.json scripts (#12285)
**Package.json**


https://github.com/zed-industries/zed/assets/62463826/f8ca12a5-1292-4465-83e1-3c2ab65f5833

**Composer.json**


https://github.com/zed-industries/zed/assets/62463826/61f9e74d-c6ed-4329-855b-d0161e0a117b

Release Notes:

- Added runnable for `package.json` and `composer.json` scripts
([#12215](https://github.com/zed-industries/zed/issues/12215)).
2024-05-26 13:47:49 +02:00
Jakob Hellermann
a1e5b122e7 Fix some warnings/issues uncovered by the new cfg checking (#12310)
Rust recently got the ability to check for typos or errors in `cfg`
attributes: https://blog.rust-lang.org/2024/05/06/check-cfg.html
This PR fixes the new warnings.

- gpui can be run with `RUSTFLAGS="--cfg gles"`, make this explicit in
`[workspace.lints.rust]`
- `cfg!(any(test, sqlite))` was just a bug, it should be
`feature(sqlite)`
- the `languages` crate had a `#[cfg(any(test, feature =
"test-support"))]` function without ever declaring the `test-support`
feature
- the `MarkdownTag` enum had a `cfg_attr` for serde without actually
having serde support


Now the only warnings when building are unused fields
`InlayHover.excerpt`, `SavedConversationMetadata.path` ,
`UserTestPlan.allow_client_reconnection` and `SyntaxMapCapture.depth`.

Release Notes:

- N/A
2024-05-26 12:50:20 +02:00
Marshall Bowers
78aaa29d5b assistant: Replace an expect with a precondition check (#12292)
This PR replaces an `expect` with a precondition check to avoid a panic
if the `LspAdapterDelegate` isn't set when invoking a slash command.

Release Notes:

- N/A
2024-05-25 15:59:40 -04:00
Marshall Bowers
8450d63ed6 Remove potential crash when missing worktrees (#12291)
This PR removes a potential crash when there are no worktrees in the
project.

Present on Nightly only.

Release Notes:

- N/A
2024-05-25 15:24:15 -04:00
Marshall Bowers
ace371a0d8 node_runtime: Restrict the windows dependency to the Windows target (#12284)
This PR fixes an issue where the `windows` dependency was being included
on non-Windows targets.

Resolves https://github.com/zed-industries/zed/issues/12282.

Release Notes:

- N/A
2024-05-25 11:04:22 -04:00
Marshall Bowers
66e40a5d7e docs: Update Ruby docs (#12283)
This PR updates the Ruby docs with a note on how to switch to using
`ruby-lsp`.

Release Notes:

- N/A
2024-05-25 10:50:54 -04:00
Danilo Leal
5213f6207d docs: Add tweaks to the "Remote development" page (#12267)
Mostly tiny stuff. Quick run-down of the changes:
- Using `*Note*` instead of `NOTE` for the blockquote callouts
(piggybacking from https://github.com/zed-industries/zed/pull/11724)
- Use hyphens for bullet lists instead of asterisks
- Capitalize every (applicable) mention to Zed
- Capitalize mentions of other products (e.g., google cloud → Google
Cloud)
- Swap e.g. → for example — for clarity (latinisms may be unfamiliar for
some non-native English speakers, surprisingly!)

Release Notes:

N/A
2024-05-25 15:33:11 +03:00
Piotr Osiewicz
e5085dfef6 lsp: explicitly drop locks in handle_input (#12276)
Due to lifetime extension rules, we were holding onto the request
handler map mutex during parsing of the request itself. This had no
grand repercussions; it only prevented registering a handler for next
request until parsing of the previous one was done.



Release Notes:

- N/A
2024-05-25 12:25:17 +02:00
Kirill Bulatov
32f11dfa00 Use language settings' prettier parsers as a fallback for files with no path (#12273)
Follow-up of
https://github.com/zed-industries/zed/pull/12095#issuecomment-2123230762
reverting back part of https://github.com/zed-industries/zed/pull/11558
that was related to `language.toml` parsing.

Now all extensions that define `prettier_parser_name` in their language
configs, will enable formatting untitled buffers without any extra
language settings like

```json
{
  "languages": {
    "JSON": {
      "prettier": {
        "allowed": true,
        "parser": "json"
      }
    }
  }
}
```



Release Notes:

- Improved ergonomics of untitled buffer formatting with prettier, no
extra language settings are needed by default.
2024-05-25 10:50:53 +03:00
Nate Butler
d5fe2c85d8 Order prompts by default (#12268)
Prompts in the prompt library will be in A->Z order by default.

Release Notes:

- N/A
2024-05-24 22:59:37 -04:00
Max Brunsfeld
f7a86967fd Avoid holding worktree lock for a long time while updating large repos' git status (#12266)
Fixes https://github.com/zed-industries/zed/issues/9575
Fixes https://github.com/zed-industries/zed/issues/4294

### Problem

When a large git repository's `.git` folder changes (due to a `git
commit`, `git reset` etc), Zed needs to recompute the git status for
every file in that git repository. Part of computing the git status is
the *unstaged* part - the comparison between the content of the file and
the version in the git index. In a large git repository like `chromium`
or `linux`, this is inherently pretty slow.

Previously, we performed this git status all at once, and held a lock on
our `BackgroundScanner`'s state for the entire time. On my laptop, in
the `linux` repo, this would often take around 13 seconds.

When opening a file, Zed always refreshes the metadata for that file in
its in-memory snapshot of worktree. This is normally very fast, but if
another task is holding a lock on the `BackgroundScanner`, it blocks.

###  Solution

I've restructured how Zed handles Git statuses, so that when a git
repository is updated, we recompute files' git statuses in fixed-sized
batches. In between these batches, the `BackgroundScanner` is free to
perform other work, so that file operations coming from the main thread
will still be responsive.

Release Notes:

- Fixed a bug that caused long delays in opening files right after
performing a commit in very large git repositories.
2024-05-24 17:41:35 -07:00
Nate Butler
800c1ba916 Rework prompt frontmatter (#12262)
Moved some things around so prompts now always have front-matter to
return, either by creating a prompt with default front-matter, or
bailing earlier on importing the prompt to the library.

In the future we'll improve visibility of malformed prompts in the
`prompts` folder in the prompt manager UI.

Fixes:

- Prompts inserted with the `/prompt` command now only include their
body, not the entire file including metadata.
- Prompts with an invalid title will now show "Untitled prompt" instead
of an empty line.

Release Notes:

- N/A
2024-05-24 19:15:02 -04:00
Avinash Thakur
461e7d00a6 Create a new directory when a new file ends with / (#12018)
Release Notes:

- Made project panel to create directories when renaming into paths ending with `/`
2024-05-24 23:46:19 +03:00
Marshall Bowers
82f5f36422 Allow defining slash commands in extensions (#12255)
This PR adds initial support for defining slash commands for the
Assistant from extensions.

Slash commands are defined in an extension's `extension.toml`:

```toml
[slash_commands.gleam-project]
description = "Returns information about the current Gleam project."
requires_argument = false
```

and then executed via the `run_slash_command` method on the `Extension`
trait:

```rs
impl Extension for GleamExtension {
    // ...

    fn run_slash_command(
        &self,
        command: SlashCommand,
        _argument: Option<String>,
        worktree: &zed::Worktree,
    ) -> Result<Option<String>, String> {
        match command.name.as_str() {
            "gleam-project" => Ok(Some("Yayyy".to_string())),
            command => Err(format!("unknown slash command: \"{command}\"")),
        }
    }
}
```

Release Notes:

- N/A
2024-05-24 15:44:32 -04:00
Kirill Bulatov
055a13a9b6 Allow clients to run Zed tasks on remote projects (#12199)
Release Notes:

- Enabled Zed tasks on remote projects with ssh connection string
specified

---------

Co-authored-by: Conrad Irwin <conrad@zed.dev>
2024-05-24 22:26:57 +03:00
Kirill Bulatov
df35fd0026 Show Delete shortcuts in project panel context menu (#12250)
Closes https://github.com/zed-industries/zed/issues/12234 by making both
default keymap and the menu `Delete` action declarations to have the
same `skip_prompt` value.
`Trash` action got more explicit `skip_prompt` declarations in this PR,
but those were the defaults already, so not changed.

Now, `Delete` action in the project panel will always show a prompt
before removing, both on the keystroke and menu item click.
To note, VSCode does skips prompt for the `Trash` action, so we might
want to change that too (later?), the PR does not alter it.

Release Notes:

- Shows Delete action binding keys in the project panel context menu
([12234](https://github.com/zed-industries/zed/issues/12234))
2024-05-24 22:02:21 +03:00
Piotr Osiewicz
27229bba6b tasks: Provide task variables from matching runnable ranges in task modal (#12237)
In #12003 we found ourselves in need for precise region tracking in
which a given runnable has an effect in order to grab variables from it.
This PR makes it so that in task modal all task variables from queries
overlapping current cursor position.
However, in the process of working on that I've found that we cannot
always use a top-level capture to represent the full match range of
runnable (which has been my assumption up to this point). Tree-sitter
captures cannot capture sibling groups; we did just that in Rust
queries.

Thankfully, none of the extensions are affected as in them, a capture is
always attached to single node. This PR adds annotations to them
nonetheless; we'll be able to get rid of top-level captures in extension
runnables.scm once this PR is in stable version of Zed.


Release Notes:

- N/A
2024-05-24 21:00:23 +02:00
Marshall Bowers
08a3d3a0c2 assistant: Add missing lints.workspace (#12253)
This PR adds the missing `lints.workspace` setting to the `assistant`
crate's `Cargo.toml`.

Release Notes:

- N/A
2024-05-24 13:14:27 -04:00
Marshall Bowers
8040e43520 Extract SlashCommand trait from assistant (#12252)
This PR extracts the `SlashCommand` trait (along with the
`SlashCommandRegistry`) from the `assistant` crate.

This will allow us to register slash commands from extensions without
having to make `extension` depend on `assistant`.

Release Notes:

- N/A
2024-05-24 13:03:41 -04:00
Bennet Bo Fenner
af3d7a60c8 indent guides: Fix tab handling (#12249)
Fixes indent guides when using tabs, 
Fixes: #12209, fixes #12210

Release Notes:

- N/A
2024-05-24 18:24:03 +02:00
Vitaly Slobodin
414f97f702 docs: Fix path to "configuring-zed" page in Python lang doc (#12233)
Fix path to the `configuring-zed` page in Python language documentation.

Release Notes:

- N/A
2024-05-24 10:31:32 -04:00
Piotr Osiewicz
0eff1eae76 task: Add ZED_DIRNAME and ZED_RELATIVE_FILE task variables (#12245)
Release Notes:

- Added ZED_RELATIVE_FILE (path to current file relative to worktree
root) and ZED_DIRNAME (path to the directory containing current file)
task variables.
2024-05-24 16:04:24 +02:00
Congyu
b0d89d6f34 Fix jetbrains keymap alt-enter to do search::SelectAllMatches (#11951)
The default keymap uses alt-enter for `SelectAllMatches` for `context:
BufferSearchBar`.

Jetbrains keymap uses alt-enter for `ToggleCodeActions` for `context:
Editor`.

When focusing on search bar, currently alt-enter does not perform
`SelectAllMatches`, whereas `ToggleCodeActions` is triggered instead,
because search bar's text input element has `context: Editor
mode=single_line`.

This PR restricts `ToggleCodeActions` to `Editor (full mode)` context to
allow `SelectAllMatches` to be triggered for alt-enter when the search
bar is active.

Release Notes:

- Fixed alt-enter with JetBrains keymap ignoring `search::SelectAllMatches` in certain contexts ([11840](https://github.com/zed-industries/zed/issues/11840))
2024-05-24 12:47:38 +03:00
bbb651
d116f3c292 Use checked str slices in Rgba::TryFrom<str> (#12097)
Release Notes:

- N/A

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2024-05-24 12:32:46 +03:00
Ephram
70e8737918 Allow information popovers while showing autocomplete suggestions (#12157)
Allows information popovers to display while autocomplete suggestions
are also being displayed.

Before: 


https://github.com/zed-industries/zed/assets/50590465/a61f0b4e-1521-42de-84fd-c5a3586b74ab

After:


https://github.com/zed-industries/zed/assets/50590465/ad2daae7-aebc-4c71-ad4c-f9d2deb6d0d0

Release Notes:
- Allow hover and completion popovers to appear at the same time ([12152](https://github.com/zed-industries/zed/issues/12152))
2024-05-24 12:32:17 +03:00
Elliot Thomas
b9697fb487 Enable manual worktree organization (#11504)
Release Notes:

- Preserve order of worktrees in project
([#10883](https://github.com/zed-industries/zed/issues/10883)).
- Enable drag-and-drop reordering for project worktrees

Note: worktree order is not synced during collaboration but guests can
reorder their own project panels.

![Reordering
worktrees](https://github.com/zed-industries/zed/assets/1347854/1c63d83c-5d4e-4b55-b840-bfbf32521b2a)

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2024-05-24 12:15:48 +03:00
Remco Smits
1e5389a2be rust: Add runnable icon for #[cfg(test)] mod tests (#12017)
This allows you to run all the tests inside the `mod tests` block. 
Fixes #11967.

<img width="366" alt="Screenshot 2024-05-18 at 16 07 32"
src="https://github.com/zed-industries/zed/assets/62463826/01fc378c-1546-421d-8250-fe0227c1e5a0">

<img width="874" alt="Screenshot 2024-05-18 at 16 37 21"
src="https://github.com/zed-industries/zed/assets/62463826/4a880b91-df84-4917-a16e-5d5fe20e22ac">

Release Notes:

- Added runnable icon for Rust `#[cfg(test)] mod tests` blocks
([#11967](https://github.com/zed-industries/zed/issues/11967)).
2024-05-24 11:02:23 +02:00
Philip Durbin
2177ee8cc0 Highlight files ending in mdwn as Markdown (#12224)
Highlight files ending in `mdwn` as Markdown.

(Ikiwiki uses `mdwn` as the file extension for Markdown.)

This pull request was inspired by this one:

- #1209/

Release Notes:

- Added ".mdwn" as a Markdown file extension.
2024-05-24 10:01:56 +03:00
Conrad Irwin
3ec94697b4 Make reconnects smoother for dev servers (#12223)
Release Notes:

- N/A

Co-authored-by: Nathan <nathan@zed.dev>
2024-05-23 21:11:14 -06:00
Conrad Irwin
656edc4b2e Vim smorgasbord (#12222)
Release Notes:

- vim: Added `]d/[d` for go to prev/next diagnostic
- vim: Added `]c/[c` to go to prev/next git change (`:diff` and
`:revert` show the diff and revert it)
- vim: Added `g cmd-d` for go to implementation
2024-05-23 21:09:32 -06:00
Conrad Irwin
ec4703a8d5 Add missing access control check (#12213)
Release Notes:

- N/A
2024-05-23 16:59:04 -06:00
Fernando Tagawa
3b14115c2f X11: Implement missing XKB Compose (#12150)
Release Notes:

- N/A

We have implemented XKB Compose for Wayland, but we forgot to implement
it for X11.
Fixed #12089
2024-05-23 15:09:20 -07:00
Antonio Scandurra
57d570c281 Introduce custom fold placeholders (#12214)
This pull request replaces the static `⋯` character we used to insert
when folding a range with a custom render function that return an
`AnyElement`. We plan to use this in the assistant, but for now this
should be behavior-preserving.

Release Notes:

- N/A

---------

Co-authored-by: Nathan <nathan@zed.dev>
Co-authored-by: Conrad <conrad@zed.dev>
2024-05-23 15:22:30 -06:00
Marshall Bowers
e0cfba43aa gleam: Detect tests using describe API for Startest (#12221)
This PR updates the Gleam runnables to detect tests using the `describe`
API in Startest.

This isn't entirely functional yet, as it is still just uses the test
function name to run the tests (which Startest doesn't yet support).

Release Notes:

- N/A
2024-05-23 17:20:06 -04:00
Max Brunsfeld
e15b902974 Fix double lease panic in Quote Selection command (#12217)
Release Notes:

- Fixed a panic that occurred when using the `assistant: quote
selection` command while signed out.
2024-05-23 12:14:34 -07:00
Conrad Irwin
9c35187aac Fix line with invisibles positioning (#12211)
Release Notes:

- fixed positioning of whitespace dots on multibyte text
([#10332](https://github.com/zed-industries/zed/issues/10332)).
2024-05-23 12:16:16 -06:00
Marshall Bowers
951fbc57ba Fix rustfmt issues in dev_servers.rs (#12205)
This PR fixes some issues preventing `rustfmt` from running properly in
`dev_servers.rs`.

The culprit was some long strings being inlined.

Release Notes:

- N/A
2024-05-23 11:46:15 -04:00
Bennet Bo Fenner
e903742d52 indent guides: Cache active indent range (#12204)
Caching the active indent range allows us to not re-compute the indent
range when it is not necessary

Release Notes:

- N/A
2024-05-23 17:44:31 +02:00
Bennet Bo Fenner
58a3ba02c6 indent guides: Toggle action (#12203)
Added a new action `editor: Toggle indent guides`, to show/hide indent
guides

Release Notes:

- N/A
2024-05-23 16:45:20 +02:00
Bennet Bo Fenner
feea607bac Indent guides (#11503)
Builds on top of existing work from #2249, but here's a showcase:


https://github.com/zed-industries/zed/assets/53836821/4b346965-6654-496c-b379-75425d9b493f

TODO:
- [x] handle line wrapping
- [x] implement handling in multibuffer (crashes currently)
- [x] add configuration option
- [x] new theme properties? What colors to use?
- [x] Possibly support indents with different colors or background
colors
- [x] investigate edge cases (e.g. indent guides and folds continue on
empty lines even if the next indent is different)
- [x] add more tests (also test `find_active_indent_index`)
- [x] docs (will do in a follow up PR)
- [x] benchmark performance impact

Release Notes:

- Added indent guides
([#5373](https://github.com/zed-industries/zed/issues/5373))

---------

Co-authored-by: Nate Butler <1714999+iamnbutler@users.noreply.github.com>
Co-authored-by: Remco <djsmits12@gmail.com>
2024-05-23 15:50:59 +02:00
Mikayla Maki
3eb0418bda Make a macro for less boilerplate when moving variables (#12182)
Also: 
- Simplify open listener implementation
- Add set_global API to global traits

Release Notes:

- N/A
2024-05-22 22:07:29 -07:00
Conrad Irwin
8b57d6d4c6 remote config fixes (#12178)
Release Notes:

- N/A
2024-05-22 22:28:00 -06:00
Conrad Irwin
af8641ce5b reconnect ssh (#12147)
Release Notes:

- N/A

---------

Co-authored-by: Bennet <bennet@zed.dev>
2024-05-22 21:25:38 -06:00
Nathan Sobo
ea166f0b27 Add a send button to the assistant (#12171)
![CleanShot 2024-05-22 at 18 20
45@2x](https://github.com/zed-industries/zed/assets/1789/dac9fcde-9fcb-4c40-b5da-ebdc847b3962)

Include the keybinding to help people discover how to submit from the
keyboard.

I'm also shifting from the word "Conversation" to "Context".

Release Notes:

- Added a send button to the assistant panel.
2024-05-22 19:17:28 -06:00
Marshall Bowers
457fbd742f zig: Pin ZLS to v0.11.0 (#12173)
This PR updates the Zig extension to pin ZLS to v0.11.0, as the more
recent releases of ZLS don't have `.tar.gz` assets available.

Note that this depends on the next version of the `zed_extension_api`,
which has yet to be released.

Release Notes:

- N/A
2024-05-22 20:47:49 -04:00
Marshall Bowers
054c36cc29 zed_extension_api: Add github_release_by_tag_name (#12172)
This PR adds a new `github_release_by_tag_name` method to the
`zed_extension_api` to allow for retrieving a GitHub release by its tag
name.

Release Notes:

- N/A
2024-05-22 20:40:31 -04:00
Marshall Bowers
85ff80f3c0 Restrict v0.0.7 of the zed_extension_api to dev builds, for now (#12170)
This PR restricts usage of v0.0.7 of the `zed_extension_api` to dev
builds, for now.

As we're still making changes to it, we don't want to ship a version of
Zed to Preview/Stable that claims to support a yet-unreleased version of
the extension API.

Release Notes:

- N/A
2024-05-22 19:45:34 -04:00
Marshall Bowers
80bd40cfa3 zed_extension_api: Fork new version (#12160)
This PR forks a new version of the `zed_extension_api` in preparation
for some upcoming changes that require breaking changes to the WIT.

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
2024-05-22 19:07:52 -04:00
Marshall Bowers
2564d5d648 docs: Remove references to language_overrides (#12169)
This PR replaces references to `language_overrides` in the docs with
just `languages`.

`language_overrides` is an alias for `languages`, and we want to move
towards just using `languages`.

Release Notes:

- N/A
2024-05-22 18:37:24 -04:00
Max Brunsfeld
770a702981 Write a randomized test and fix bugs in the logic for omitting slash commands from completion requests (#12164)
Release Notes:

- N/A
2024-05-22 15:20:55 -07:00
Nate Butler
0a848f29e8 Prompt library updates (#11988)
Restructure prompts & the prompt library.

- Prompts are now written in markdown
- The prompt manager has a picker and editable prompts
- Saving isn't wired up yet
- This also removes the "Insert active prompt" button as this concept doesn't exist anymore, and will be replaced with slash commands.

I didn't staff flag this, but if you do play around with it expect it to still be pretty rough.

Release Notes:

- N/A

---------

Co-authored-by: Nathan Sobo <1789+nathansobo@users.noreply.github.com>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
2024-05-22 18:04:47 -04:00
Max Brunsfeld
a73a3ef243 Add slash commands for adding context into the assistant (#12102)
Tasks

* [x] remove old flaps and output when editing a slash command
* [x] the completing a command name that takes args, insert a space to
prepare for typing an arg
* [x] always trigger completions when  typing in a slash command
* [x] don't show line numbers
* [x] implement `prompt` command
* [x] `current-file` command
* [x] state gets corrupted on `duplicate line up` on a slash command
* [x] exclude slash command source from completion request

Next steps:
* show output token count in flap trailer
* add `/project` command that matches project ambient context
* delete ambient context

Release Notes:

- N/A

---------

Co-authored-by: Marshall <marshall@zed.dev>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
2024-05-22 14:06:28 -07:00
Joseph T. Lyons
d6e59bfae1 Fix proxy setting documentation (#12151)
Release Notes:

- N/A
2024-05-22 16:35:44 -04:00
Conrad Irwin
c2f650fe49 Fix some edge-cases in vim visual delete (#12131)
Release Notes:

- vim: Fixed `shift-d` in visual and visual block mode.
2024-05-22 12:54:41 -06:00
Conrad Irwin
c084b6aade remoting fixes (#12137)
Release Notes:

- N/A
2024-05-22 12:49:42 -06:00
Remco Smits
8af9fa6320 php: Bump to v0.0.4 (#12140)
This PR bumps the PHP extension to v0.0.4.

Changes:

- https://github.com/zed-industries/zed/pull/11514

Release Notes:

- N/A
2024-05-22 14:11:48 -04:00
Piotr Osiewicz
58796a8480 tasks: Expose captured variables to ContextProvider (#12134)
This PR changes the interface of ContextProvider, allowing it to inspect
*all* variables set so far during the process of building
`TaskVariables`. This makes it possible to capture e.g. an identifier in
tree-sitter query, process it and then export it as a task variable.

Notably, the list of variables includes captures prefixed with leading
underscore; they are removed after all calls to `build_context`, but it
makes it possible to capture something and then conditionally preserve
it (and perhaps modify it).

Release Notes:

- N/A
2024-05-22 19:45:43 +02:00
Joseph T. Lyons
ba9449692e v0.138.x dev 2024-05-22 11:26:58 -04:00
324 changed files with 16326 additions and 9241 deletions

View File

@@ -99,7 +99,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:
name: (Linux) Run Clippy and tests
runs-on:
@@ -117,6 +116,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

3
.gitignore vendored
View File

@@ -7,6 +7,8 @@
/script/node_modules
/crates/theme/schemas/theme.json
/crates/collab/seed.json
/crates/zed/resources/flatpak/flatpak-cargo-sources.json
/dev.zed.Zed*.json
/assets/*licenses.md
**/venv
.build
@@ -25,3 +27,4 @@ DerivedData/
.blob_store
.vscode
.wrangler
.flatpak-builder

487
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@ members = [
"crates/anthropic",
"crates/assets",
"crates/assistant",
"crates/assistant2",
"crates/assistant_slash_command",
"crates/assistant_tooling",
"crates/audio",
"crates/auto_update",
@@ -76,6 +76,7 @@ members = [
"crates/rich_text",
"crates/rope",
"crates/rpc",
"crates/rustdoc_to_markdown",
"crates/task",
"crates/tasks_ui",
"crates/search",
@@ -147,7 +148,7 @@ ai = { path = "crates/ai" }
anthropic = { path = "crates/anthropic" }
assets = { path = "crates/assets" }
assistant = { path = "crates/assistant" }
assistant2 = { path = "crates/assistant2" }
assistant_slash_command = { path = "crates/assistant_slash_command" }
assistant_tooling = { path = "crates/assistant_tooling" }
audio = { path = "crates/audio" }
auto_update = { path = "crates/auto_update" }
@@ -220,6 +221,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" }
task = { path = "crates/task" }
tasks_ui = { path = "crates/tasks_ui" }
search = { path = "crates/search" }
@@ -255,6 +257,7 @@ zed_actions = { path = "crates/zed_actions" }
anyhow = "1.0.57"
any_vec = "0.13"
ashpd = "0.8.0"
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
async-fs = "1.6"
async-recursion = "1.0.0"
@@ -262,8 +265,9 @@ 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 = "e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c" }
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" }
cap-std = "3.0"
cargo_toml = "0.20"
chrono = { version = "0.4", features = ["serde"] }
@@ -283,10 +287,9 @@ futures-batch = "0.6.1"
futures-lite = "1.13"
git2 = { version = "0.18", default-features = false }
globset = "0.4"
heed = { git = "https://github.com/meilisearch/heed", rev = "036ac23f73a021894974b9adc815bc95b3e0482a", features = [
"read-txn-no-tls",
] }
heed = { version = "0.20.1", features = ["read-txn-no-tls"] }
hex = "0.4.3"
html5ever = "0.27.0"
ignore = "0.4.22"
indoc = "1"
# We explicitly disable http2 support in isahc.
@@ -299,12 +302,14 @@ lazy_static = "1.4.0"
libc = "0.2"
linkify = "0.10.0"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
markup5ever_rcdom = "0.3.0"
nanoid = "0.4"
nix = "0.28"
once_cell = "1.19.0"
ordered-float = "2.1.1"
palette = { version = "0.7.5", default-features = false, features = ["std"] }
parking_lot = "0.12.1"
pathdiff = "0.2"
profiling = "1"
postage = { version = "0.5", features = ["futures-traits"] }
pretty_assertions = "1.3.0"
@@ -350,33 +355,33 @@ toml = "0.8"
tokio = { version = "1", features = ["full"] }
tower-http = "0.4.4"
tree-sitter = { version = "0.20", features = ["wasm"] }
tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "7331995b19b8f8aba2d5e26deb51d2195c18bc94" }
tree-sitter-bash = "0.20.5"
tree-sitter-c = "0.20.1"
tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "f44509141e7e483323d2ec178f2d2e6c0fc041c1" }
tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" }
tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "a2861e88a730287a60c11ea9299c033c7d076e30" }
tree-sitter-cpp = "0.20.5"
tree-sitter-css = "0.20"
tree-sitter-elixir = "0.1.1"
tree-sitter-embedded-template = "0.20.0"
tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "b82ab803d887002a0af11f6ce63d72884580bf33" }
tree-sitter-gomod = { git = "https://github.com/camdencheek/tree-sitter-go-mod" }
tree-sitter-gomod = "1.0.1"
tree-sitter-gowork = { git = "https://github.com/d1y/tree-sitter-go-work" }
rustc-demangle = "0.1.23"
tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "2e1348c3cf2c9323e87c2744796cf3f3868aa82a" }
tree-sitter-html = "0.19.0"
tree-sitter-jsdoc = { git = "https://github.com/tree-sitter/tree-sitter-jsdoc", rev = "6a6cf9e7341af32d8e2b2e24a37fbfebefc3dc55" }
tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "40a81c01a40ac48744e0c8ccabbaba1920441199" }
tree-sitter-json = "0.20.2"
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
tree-sitter-proto = { git = "https://github.com/rewinfrey/tree-sitter-proto", rev = "36d54f288aee112f13a67b550ad32634d0c2cb52" }
tree-sitter-python = "0.20.2"
tree-sitter-regex = "0.20.0"
tree-sitter-ruby = "0.20.0"
tree-sitter-rust = "0.20.3"
tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259" }
tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "f545a41f57502e1b5ddf2a6668896c1b0620f930" }
tree-sitter-typescript = "0.20.5"
tree-sitter-yaml = "0.0.1"
unindent = "0.1.7"
unicase = "2.6"
unicode-segmentation = "1.10"
url = "2.2"
uuid = { version = "1.1.2", features = ["v4", "v5"] }
uuid = { version = "1.1.2", features = ["v4", "v5", "serde"] }
wasmparser = "0.201"
wasm-encoder = "0.201"
wasmtime = { version = "19.0.0", default-features = false, features = [

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-arrow-down-from-line"><path d="M19 3H5"/><path d="M12 21V7"/><path d="m6 15 6 6 6-6"/></svg>

After

Width:  |  Height:  |  Size: 295 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-arrow-up-from-line"><path d="m18 9-6-6-6 6"/><path d="M12 3v14"/><path d="M5 21h14"/></svg>

After

Width:  |  Height:  |  Size: 294 B

7
assets/icons/rerun.svg Normal file
View File

@@ -0,0 +1,7 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 12C3 9.61305 3.94821 7.32387 5.63604 5.63604C7.32387 3.94821 9.61305 3 12 3C14.516 3.00947 16.931 3.99122 18.74 5.74L21 8" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21 3V8H16" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21 12C21 14.3869 20.0518 16.6761 18.364 18.364C16.6761 20.0518 14.3869 21 12 21C9.48395 20.9905 7.06897 20.0088 5.26 18.26L3 16" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 16H3V21" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 9.37052C10 8.98462 10.4186 8.74419 10.7519 8.93863L15.2596 11.5681C15.5904 11.761 15.5904 12.2389 15.2596 12.4319L10.7519 15.0614C10.4186 15.2558 10 15.0154 10 14.6295V9.37052Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 949 B

1
assets/icons/reveal.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-folder-search"><circle cx="17" cy="17" r="3"/><path d="M10.7 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v4.1"/><path d="m21 21-1.5-1.5"/></svg>

After

Width:  |  Height:  |  Size: 400 B

1
assets/icons/save.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-save"><path d="M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"/><path d="M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7"/><path d="M7 3v4a1 1 0 0 0 1 1h7"/></svg>

After

Width:  |  Height:  |  Size: 412 B

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 11L6 4L10.5 7.5L6 11Z" fill="currentColor"></path></svg>

After

Width:  |  Height:  |  Size: 164 B

View File

@@ -211,7 +211,9 @@
"ctrl-s": "workspace::Save",
"ctrl->": "assistant::QuoteSelection",
"shift-enter": "assistant::Split",
"ctrl-r": "assistant::CycleMessageRole"
"ctrl-r": "assistant::CycleMessageRole",
"enter": "assistant::ConfirmCommand",
"alt-enter": "editor::Newline"
}
},
{
@@ -569,8 +571,8 @@
"enter": "project_panel::Rename",
"backspace": "project_panel::Trash",
"delete": "project_panel::Trash",
"ctrl-backspace": ["project_panel::Delete", { "skip_prompt": true }],
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": true }],
"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"
}

View File

@@ -227,7 +227,9 @@
"cmd-s": "workspace::Save",
"cmd->": "assistant::QuoteSelection",
"shift-enter": "assistant::Split",
"ctrl-r": "assistant::CycleMessageRole"
"ctrl-r": "assistant::CycleMessageRole",
"enter": "assistant::ConfirmCommand",
"alt-enter": "editor::Newline"
}
},
{
@@ -578,12 +580,15 @@
"cmd-alt-c": "project_panel::CopyPath",
"alt-cmd-shift-c": "project_panel::CopyRelativePath",
"enter": "project_panel::Rename",
"backspace": "project_panel::Trash",
"delete": "project_panel::Trash",
"cmd-backspace": ["project_panel::Delete", { "skip_prompt": true }],
"cmd-delete": ["project_panel::Delete", { "skip_prompt": true }],
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
"delete": ["project_panel::Trash", { "skip_prompt": false }],
"cmd-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"cmd-delete": ["project_panel::Delete", { "skip_prompt": false }],
"alt-cmd-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"
}
},
{

View File

@@ -53,7 +53,6 @@
"cmd-alt-b": "editor::GoToDefinitionSplit",
"cmd-shift-b": "editor::GoToTypeDefinition",
"cmd-alt-shift-b": "editor::GoToTypeDefinitionSplit",
"alt-enter": "editor::ToggleCodeActions",
"f2": "editor::GoToDiagnostic",
"cmd-f2": "editor::GoToPrevDiagnostic",
"ctrl-alt-shift-down": "editor::GoToHunk",
@@ -70,7 +69,8 @@
"cmd-f12": "outline::Toggle",
"cmd-7": "outline::Toggle",
"cmd-shift-o": "file_finder::Toggle",
"cmd-l": "go_to_line::Toggle"
"cmd-l": "go_to_line::Toggle",
"alt-enter": "editor::ToggleCodeActions"
}
},
{

View File

@@ -155,6 +155,7 @@
"g shift-t": "pane::ActivatePrevItem",
"g d": "editor::GoToDefinition",
"g shift-d": "editor::GoToTypeDefinition",
"g cmd-d": "editor::GoToImplementation",
"g x": "editor::OpenUrl",
"g n": "vim::SelectNextMatch",
"g shift-n": "vim::SelectPreviousMatch",
@@ -384,7 +385,11 @@
"ctrl-pageup": "pane::ActivatePrevItem",
// tree-sitter related commands
"[ x": "editor::SelectLargerSyntaxNode",
"] x": "editor::SelectSmallerSyntaxNode"
"] x": "editor::SelectSmallerSyntaxNode",
"] d": "editor::GoToDiagnostic",
"[ d": "editor::GoToPrevDiagnostic",
"] c": "editor::GoToHunk",
"[ c": "editor::GoToPrevHunk"
}
},
{
@@ -493,8 +498,8 @@
"shift-o": "vim::OtherEnd",
"d": "vim::VisualDelete",
"x": "vim::VisualDelete",
"shift-d": "vim::VisualDelete",
"shift-x": "vim::VisualDelete",
"shift-d": "vim::VisualDeleteLine",
"shift-x": "vim::VisualDeleteLine",
"y": "vim::VisualYank",
"shift-y": "vim::VisualYank",
"p": "vim::Paste",

View File

@@ -37,6 +37,8 @@
},
// The default font size for text in the editor
"buffer_font_size": 15,
// The weight of the editor font in standard CSS units from 100 to 900.
"buffer_font_weight": 400,
// Set the buffer's line height.
// May take 3 values:
// 1. Use a line height that's comfortable for reading (1.618)
@@ -55,6 +57,8 @@
// Disable ligatures:
"calt": false
},
// The weight of the UI font in standard CSS units from 100 to 900.
"ui_font_weight": 400,
// The default font size for text in the UI
"ui_font_size": 16,
// The factor to grow the active pane by. Defaults to 1.0
@@ -124,6 +128,8 @@
"wrap_guides": [],
// Hide the values of in variables from visual display in private files
"redact_private_values": false,
// 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*",
@@ -164,7 +170,7 @@
// Join calls with the microphone live by default
"mute_on_join": false,
// Share your project when you are the first to join a channel
"share_on_join": true
"share_on_join": false
},
// Toolbar related settings
"toolbar": {
@@ -216,6 +222,25 @@
// Whether to show fold buttons in the gutter.
"folds": true
},
"indent_guides": {
/// Whether to show indent guides in the editor.
"enabled": true,
/// The width of the indent guides in pixels, between 1 and 10.
"line_width": 1,
/// Determines how indent guides are colored.
/// This setting can take the following three values:
///
/// 1. "disabled"
/// 2. "fixed"
/// 3. "indent_aware"
"coloring": "fixed",
/// Determines how indent guide backgrounds are colored.
/// This setting can take the following two values:
///
/// 1. "disabled"
/// 2. "indent_aware"
"background_coloring": "disabled"
},
// The number of lines to keep above/below the cursor when scrolling.
"vertical_scroll_margin": 3,
// Scroll sensitivity multiplier. This multiplier is applied
@@ -316,7 +341,7 @@
// AI provider.
"provider": {
"name": "openai",
// The default model to use when starting new conversations. This
// The default model to use when creating new contexts. This
// setting can take three values:
//
// 1. "gpt-3.5-turbo"
@@ -836,7 +861,7 @@
// environment variables.
//
// Examples:
// - "proxy" = "socks5://localhost:10808"
// - "proxy" = "http://127.0.0.1:10809"
// - "proxy": "socks5://localhost:10808"
// - "proxy": "http://127.0.0.1:10809"
"proxy": null
}

View File

@@ -5,6 +5,15 @@
{
"name": "Gruvbox Dark",
"appearance": "dark",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"style": {
"border": "#5b534dff",
"border.variant": "#494340ff",
@@ -379,6 +388,15 @@
{
"name": "Gruvbox Dark Hard",
"appearance": "dark",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"style": {
"border": "#5b534dff",
"border.variant": "#494340ff",
@@ -753,6 +771,15 @@
{
"name": "Gruvbox Dark Soft",
"appearance": "dark",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"style": {
"border": "#5b534dff",
"border.variant": "#494340ff",
@@ -1127,6 +1154,15 @@
{
"name": "Gruvbox Light",
"appearance": "light",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"style": {
"border": "#c8b899ff",
"border.variant": "#ddcca7ff",
@@ -1501,6 +1537,15 @@
{
"name": "Gruvbox Light Hard",
"appearance": "light",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"style": {
"border": "#c8b899ff",
"border.variant": "#ddcca7ff",
@@ -1875,6 +1920,15 @@
{
"name": "Gruvbox Light Soft",
"appearance": "light",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"style": {
"border": "#c8b899ff",
"border.variant": "#ddcca7ff",

View File

@@ -16,9 +16,9 @@ use rust_embed::RustEmbed;
pub struct Assets;
impl AssetSource for Assets {
fn load(&self, path: &str) -> Result<std::borrow::Cow<'static, [u8]>> {
fn load(&self, path: &str) -> Result<Option<std::borrow::Cow<'static, [u8]>>> {
Self::get(path)
.map(|f| f.data)
.map(|f| Some(f.data))
.ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
}
@@ -42,7 +42,10 @@ impl Assets {
let mut embedded_fonts = Vec::new();
for font_path in font_paths {
if font_path.ends_with(".ttf") {
let font_bytes = cx.asset_source().load(&font_path)?;
let font_bytes = cx
.asset_source()
.load(&font_path)?
.expect("Assets should never return None");
embedded_fonts.push(font_bytes);
}
}

View File

@@ -5,6 +5,9 @@ edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/assistant.rs"
doctest = false
@@ -12,15 +15,18 @@ doctest = false
[dependencies]
anyhow.workspace = true
anthropic = { workspace = true, features = ["schemars"] }
assistant_slash_command.workspace = true
cargo_toml.workspace = true
chrono.workspace = true
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
http.workspace = true
indoc.workspace = true
@@ -34,8 +40,10 @@ parking_lot.workspace = true
project.workspace = true
regex.workspace = true
rope.workspace = true
rustdoc_to_markdown.workspace = true
schemars.workspace = true
search.workspace = true
semantic_index.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
@@ -49,6 +57,8 @@ ui.workspace = true
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 +0,0 @@
mod current_project;
mod recent_buffers;
pub use current_project::*;
pub use recent_buffers::*;
#[derive(Default)]
pub struct AmbientContext {
pub recent_buffers: RecentBuffersContext,
pub current_project: CurrentProjectContext,
}
impl AmbientContext {
pub fn snapshot(&self) -> AmbientContextSnapshot {
AmbientContextSnapshot {
recent_buffers: self.recent_buffers.snapshot.clone(),
}
}
}
#[derive(Clone, Default, Debug)]
pub struct AmbientContextSnapshot {
pub recent_buffers: RecentBuffersSnapshot,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
pub enum ContextUpdated {
Updating,
Disabled,
}

View File

@@ -1,178 +0,0 @@
use std::fmt::Write;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use anyhow::{anyhow, Result};
use fs::Fs;
use gpui::{AsyncAppContext, ModelContext, Task, WeakModel};
use project::{Project, ProjectPath};
use util::ResultExt;
use crate::ambient_context::ContextUpdated;
use crate::assistant_panel::Conversation;
use crate::{LanguageModelRequestMessage, Role};
/// Ambient context about the current project.
pub struct CurrentProjectContext {
pub enabled: bool,
pub message: String,
pub pending_message: Option<Task<()>>,
}
#[allow(clippy::derivable_impls)]
impl Default for CurrentProjectContext {
fn default() -> Self {
Self {
enabled: false,
message: String::new(),
pending_message: None,
}
}
}
impl CurrentProjectContext {
/// Returns the [`CurrentProjectContext`] as a message to the language model.
pub fn to_message(&self) -> Option<LanguageModelRequestMessage> {
self.enabled.then(|| LanguageModelRequestMessage {
role: Role::System,
content: self.message.clone(),
})
}
/// Updates the [`CurrentProjectContext`] for the given [`Project`].
pub fn update(
&mut self,
fs: Arc<dyn Fs>,
project: WeakModel<Project>,
cx: &mut ModelContext<Conversation>,
) -> ContextUpdated {
if !self.enabled {
self.message.clear();
self.pending_message = None;
cx.notify();
return ContextUpdated::Disabled;
}
self.pending_message = Some(cx.spawn(|conversation, mut cx| async move {
const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100);
cx.background_executor().timer(DEBOUNCE_TIMEOUT).await;
let Some(path_to_cargo_toml) = Self::path_to_cargo_toml(project, &mut cx).log_err()
else {
return;
};
let Some(path_to_cargo_toml) = path_to_cargo_toml
.ok_or_else(|| anyhow!("no Cargo.toml"))
.log_err()
else {
return;
};
let message_task = cx
.background_executor()
.spawn(async move { Self::build_message(fs, &path_to_cargo_toml).await });
if let Some(message) = message_task.await.log_err() {
conversation
.update(&mut cx, |conversation, cx| {
conversation.ambient_context.current_project.message = message;
conversation.count_remaining_tokens(cx);
cx.notify();
})
.log_err();
}
}));
ContextUpdated::Updating
}
async fn build_message(fs: Arc<dyn Fs>, path_to_cargo_toml: &Path) -> Result<String> {
let buffer = fs.load(path_to_cargo_toml).await?;
let cargo_toml: cargo_toml::Manifest = toml::from_str(&buffer)?;
let mut message = String::new();
writeln!(message, "You are in a Rust project.")?;
if let Some(workspace) = cargo_toml.workspace {
writeln!(
message,
"The project is a Cargo workspace with the following members:"
)?;
for member in workspace.members {
writeln!(message, "- {member}")?;
}
if !workspace.default_members.is_empty() {
writeln!(message, "The default members are:")?;
for member in workspace.default_members {
writeln!(message, "- {member}")?;
}
}
if !workspace.dependencies.is_empty() {
writeln!(
message,
"The following workspace dependencies are installed:"
)?;
for dependency in workspace.dependencies.keys() {
writeln!(message, "- {dependency}")?;
}
}
} else if let Some(package) = cargo_toml.package {
writeln!(
message,
"The project name is \"{name}\".",
name = package.name
)?;
let description = package
.description
.as_ref()
.and_then(|description| description.get().ok().cloned());
if let Some(description) = description.as_ref() {
writeln!(message, "It describes itself as \"{description}\".")?;
}
if !cargo_toml.dependencies.is_empty() {
writeln!(message, "The following dependencies are installed:")?;
for dependency in cargo_toml.dependencies.keys() {
writeln!(message, "- {dependency}")?;
}
}
}
Ok(message)
}
fn path_to_cargo_toml(
project: WeakModel<Project>,
cx: &mut AsyncAppContext,
) -> Result<Option<PathBuf>> {
cx.update(|cx| {
let worktree = project.update(cx, |project, _cx| {
project
.worktrees()
.next()
.ok_or_else(|| anyhow!("no worktree"))
})??;
let path_to_cargo_toml = worktree.update(cx, |worktree, _cx| {
let cargo_toml = worktree.entry_for_path("Cargo.toml")?;
Some(ProjectPath {
worktree_id: worktree.id(),
path: cargo_toml.path.clone(),
})
});
let path_to_cargo_toml = path_to_cargo_toml.and_then(|path| {
project
.update(cx, |project, cx| project.absolute_path(&path, cx))
.ok()
.flatten()
});
Ok(path_to_cargo_toml)
})?
}
}

View File

@@ -1,145 +0,0 @@
use crate::{assistant_panel::Conversation, LanguageModelRequestMessage, Role};
use gpui::{ModelContext, Subscription, Task, WeakModel};
use language::{Buffer, BufferSnapshot, Rope};
use std::{fmt::Write, path::PathBuf, time::Duration};
use super::ContextUpdated;
pub struct RecentBuffersContext {
pub enabled: bool,
pub buffers: Vec<RecentBuffer>,
pub snapshot: RecentBuffersSnapshot,
pub pending_message: Option<Task<()>>,
}
pub struct RecentBuffer {
pub buffer: WeakModel<Buffer>,
pub _subscription: Subscription,
}
impl Default for RecentBuffersContext {
fn default() -> Self {
Self {
enabled: true,
buffers: Vec::new(),
snapshot: RecentBuffersSnapshot::default(),
pending_message: None,
}
}
}
impl RecentBuffersContext {
pub fn update(&mut self, cx: &mut ModelContext<Conversation>) -> ContextUpdated {
let source_buffers = self
.buffers
.iter()
.filter_map(|recent| {
let (full_path, snapshot) = recent
.buffer
.read_with(cx, |buffer, cx| {
(
buffer.file().map(|file| file.full_path(cx)),
buffer.snapshot(),
)
})
.ok()?;
Some(SourceBufferSnapshot {
full_path,
model: recent.buffer.clone(),
snapshot,
})
})
.collect::<Vec<_>>();
if !self.enabled || source_buffers.is_empty() {
self.snapshot.message = Default::default();
self.snapshot.source_buffers.clear();
self.pending_message = None;
cx.notify();
ContextUpdated::Disabled
} else {
self.pending_message = Some(cx.spawn(|this, mut cx| async move {
const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100);
cx.background_executor().timer(DEBOUNCE_TIMEOUT).await;
let message = if source_buffers.is_empty() {
Rope::new()
} else {
cx.background_executor()
.spawn({
let source_buffers = source_buffers.clone();
async move { message_for_recent_buffers(source_buffers) }
})
.await
};
this.update(&mut cx, |this, cx| {
this.ambient_context.recent_buffers.snapshot.source_buffers = source_buffers;
this.ambient_context.recent_buffers.snapshot.message = message;
this.count_remaining_tokens(cx);
cx.notify();
})
.ok();
}));
ContextUpdated::Updating
}
}
/// Returns the [`RecentBuffersContext`] as a message to the language model.
pub fn to_message(&self) -> Option<LanguageModelRequestMessage> {
self.enabled.then(|| LanguageModelRequestMessage {
role: Role::System,
content: self.snapshot.message.to_string(),
})
}
}
#[derive(Clone, Default, Debug)]
pub struct RecentBuffersSnapshot {
pub message: Rope,
pub source_buffers: Vec<SourceBufferSnapshot>,
}
#[derive(Clone)]
pub struct SourceBufferSnapshot {
pub full_path: Option<PathBuf>,
pub model: WeakModel<Buffer>,
pub snapshot: BufferSnapshot,
}
impl std::fmt::Debug for SourceBufferSnapshot {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SourceBufferSnapshot")
.field("full_path", &self.full_path)
.field("model (entity id)", &self.model.entity_id())
.field("snapshot (text)", &self.snapshot.text())
.finish()
}
}
fn message_for_recent_buffers(buffers: Vec<SourceBufferSnapshot>) -> Rope {
let mut message = String::new();
writeln!(
message,
"The following is a list of recent buffers that the user has opened."
)
.unwrap();
for buffer in buffers {
if let Some(path) = buffer.full_path {
writeln!(message, "```{}", path.display()).unwrap();
} else {
writeln!(message, "```untitled").unwrap();
}
for chunk in buffer.snapshot.chunks(0..buffer.snapshot.len(), false) {
message.push_str(chunk.text);
}
if !message.ends_with('\n') {
message.push('\n');
}
message.push_str("```\n");
}
Rope::from(message.as_str())
}

View File

@@ -1,28 +1,29 @@
mod ambient_context;
pub mod assistant_panel;
pub mod assistant_settings;
mod codegen;
mod completion_provider;
mod prompt_library;
mod prompts;
mod saved_conversation;
mod search;
mod slash_command;
mod streaming_diff;
use ambient_context::AmbientContextSnapshot;
pub use assistant_panel::AssistantPanel;
use assistant_settings::{AnthropicModel, AssistantSettings, OpenAiModel, ZedDotDevModel};
use client::{proto, Client};
use command_palette_hooks::CommandPaletteFilter;
pub(crate) use completion_provider::*;
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
pub(crate) use saved_conversation::*;
use semantic_index::{CloudEmbeddingProvider, SemanticIndex};
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
use std::{
fmt::{self, Display},
sync::Arc,
};
use util::paths::EMBEDDINGS_DIR;
actions!(
assistant,
@@ -35,9 +36,9 @@ actions!(
ResetKey,
InlineAssist,
InsertActivePrompt,
ToggleIncludeConversation,
ToggleHistory,
ApplyEdit
ApplyEdit,
ConfirmCommand
]
);
@@ -187,9 +188,6 @@ pub struct LanguageModelChoiceDelta {
struct MessageMetadata {
role: Role,
status: MessageStatus,
// todo!("delete this")
#[serde(skip)]
ambient_context: AmbientContextSnapshot,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
@@ -235,7 +233,23 @@ impl Assistant {
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
cx.set_global(Assistant::default());
AssistantSettings::register(cx);
cx.spawn(|mut cx| {
let client = client.clone();
async move {
let embedding_provider = CloudEmbeddingProvider::new(client.clone());
let semantic_index = SemanticIndex::new(
EMBEDDINGS_DIR.join("semantic-index-db.0.mdb"),
Arc::new(embedding_provider),
&mut cx,
)
.await?;
cx.update(|cx| cx.set_global(semantic_index))
}
})
.detach();
completion_provider::init(client, cx);
assistant_slash_command::init(cx);
assistant_panel::init(cx);
CommandPaletteFilter::update_global(cx, |filter, _cx| {

File diff suppressed because it is too large Load Diff

View File

@@ -339,11 +339,11 @@ pub struct LegacyAssistantSettingsContent {
///
/// Default: 320
pub default_height: Option<f32>,
/// The default OpenAI model to use when starting new conversations.
/// The default OpenAI model to use when creating new contexts.
///
/// Default: gpt-4-1106-preview
pub default_open_ai_model: Option<OpenAiModel>,
/// OpenAI API base URL to use when starting new conversations.
/// OpenAI API base URL to use when creating new contexts.
///
/// Default: https://api.openai.com/v1
pub openai_api_url: Option<String>,

View File

@@ -233,7 +233,7 @@ impl CompletionProvider {
CompletionProvider::Anthropic(provider) => provider.count_tokens(request, cx),
CompletionProvider::ZedDotDev(provider) => provider.count_tokens(request, cx),
#[cfg(test)]
CompletionProvider::Fake(_) => unimplemented!(),
CompletionProvider::Fake(_) => futures::FutureExt::boxed(futures::future::ready(Ok(0))),
}
}

View File

@@ -7,7 +7,7 @@ use anthropic::{stream_completion, Request, RequestMessage, Role as AnthropicRol
use anyhow::{anyhow, Result};
use editor::{Editor, EditorElement, EditorStyle};
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
use gpui::{AnyView, AppContext, FontStyle, FontWeight, Task, TextStyle, View, WhiteSpace};
use gpui::{AnyView, AppContext, FontStyle, Task, TextStyle, View, WhiteSpace};
use http::HttpClient;
use settings::Settings;
use std::time::Duration;
@@ -261,7 +261,7 @@ impl AuthenticationPrompt {
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features.clone(),
font_size: rems(0.875).into(),
font_weight: FontWeight::NORMAL,
font_weight: settings.ui_font.weight,
font_style: FontStyle::Normal,
line_height: relative(1.3),
background_color: None,

View File

@@ -5,7 +5,7 @@ use crate::{
use anyhow::{anyhow, Result};
use editor::{Editor, EditorElement, EditorStyle};
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
use gpui::{AnyView, AppContext, FontStyle, FontWeight, Task, TextStyle, View, WhiteSpace};
use gpui::{AnyView, AppContext, FontStyle, Task, TextStyle, View, WhiteSpace};
use http::HttpClient;
use open_ai::{stream_completion, Request, RequestMessage, Role as OpenAiRole};
use settings::Settings;
@@ -273,7 +273,7 @@ impl AuthenticationPrompt {
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features.clone(),
font_size: rems(0.875).into(),
font_weight: FontWeight::NORMAL,
font_weight: settings.ui_font.weight,
font_style: FontStyle::Normal,
line_height: relative(1.3),
background_color: None,

View File

@@ -1,454 +0,0 @@
use fs::Fs;
use futures::StreamExt;
use gpui::{AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Render};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use ui::{prelude::*, Checkbox, ModalHeader};
use util::{paths::PROMPTS_DIR, ResultExt};
use workspace::ModalView;
pub struct PromptLibraryState {
/// The default prompt all assistant contexts will start with
_system_prompt: String,
/// All [UserPrompt]s loaded into the library
prompts: HashMap<String, UserPrompt>,
/// Prompts included in the default prompt
default_prompts: Vec<String>,
/// Prompts that have a pending update that hasn't been applied yet
_updateable_prompts: Vec<String>,
/// Prompts that have been changed since they were loaded
/// and can be reverted to their original state
_revertable_prompts: Vec<String>,
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 {
_system_prompt: String::new(),
prompts: HashMap::new(),
default_prompts: Vec::new(),
_updateable_prompts: Vec::new(),
_revertable_prompts: Vec::new(),
version: 0,
}),
}
}
pub async fn init(fs: Arc<dyn Fs>) -> anyhow::Result<Self> {
let prompt_library = PromptLibrary::new();
prompt_library.load_prompts(fs)?;
Ok(prompt_library)
}
fn load_prompts(&self, fs: Arc<dyn Fs>) -> anyhow::Result<()> {
let prompts = futures::executor::block_on(UserPrompt::list(fs))?;
let prompts_with_ids = prompts
.clone()
.into_iter()
.map(|prompt| {
let id = uuid::Uuid::new_v4().to_string();
(id, prompt)
})
.collect::<Vec<_>>();
let mut state = self.state.write();
state.prompts.extend(prompts_with_ids);
state.version += 1;
Ok(())
}
pub fn default_prompt(&self) -> Option<String> {
let state = self.state.read();
if state.default_prompts.is_empty() {
None
} else {
Some(self.join_default_prompts())
}
}
pub fn add_prompt_to_default(&self, prompt_id: String) -> anyhow::Result<()> {
let mut state = self.state.write();
if !state.default_prompts.contains(&prompt_id) && state.prompts.contains_key(&prompt_id) {
state.default_prompts.push(prompt_id);
state.version += 1;
}
Ok(())
}
pub fn remove_prompt_from_default(&self, prompt_id: String) -> anyhow::Result<()> {
let mut state = self.state.write();
state.default_prompts.retain(|id| id != &prompt_id);
state.version += 1;
Ok(())
}
fn join_default_prompts(&self) -> String {
let state = self.state.read();
let active_prompt_ids = state.default_prompts.to_vec();
active_prompt_ids
.iter()
.filter_map(|id| state.prompts.get(id).map(|p| p.prompt.clone()))
.collect::<Vec<_>>()
.join("\n\n---\n\n")
}
#[allow(unused)]
pub fn prompts(&self) -> Vec<UserPrompt> {
let state = self.state.read();
state.prompts.values().cloned().collect()
}
pub fn prompts_with_ids(&self) -> Vec<(String, UserPrompt)> {
let state = self.state.read();
state
.prompts
.iter()
.map(|(id, prompt)| (id.clone(), prompt.clone()))
.collect()
}
pub fn _default_prompts(&self) -> Vec<UserPrompt> {
let state = self.state.read();
state
.default_prompts
.iter()
.filter_map(|id| state.prompts.get(id).cloned())
.collect()
}
pub fn default_prompt_ids(&self) -> Vec<String> {
let state = self.state.read();
state.default_prompts.clone()
}
}
/// A custom prompt that can be loaded into the prompt library
///
/// Example:
///
/// ```json
/// {
/// "title": "Foo",
/// "version": "1.0",
/// "author": "Jane Kim <jane@kim.com>",
/// "languages": ["*"], // or ["rust", "python", "javascript"] etc...
/// "prompt": "bar"
/// }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct UserPrompt {
version: String,
title: String,
author: String,
languages: Vec<String>,
prompt: String,
}
impl UserPrompt {
async fn list(fs: Arc<dyn Fs>) -> anyhow::Result<Vec<Self>> {
fs.create_dir(&PROMPTS_DIR).await?;
let mut paths = fs.read_dir(&PROMPTS_DIR).await?;
let mut prompts = Vec::new();
while let Some(path_result) = paths.next().await {
let path = match path_result {
Ok(p) => p,
Err(e) => {
eprintln!("Error reading path: {:?}", e);
continue;
}
};
if path.extension() == Some(std::ffi::OsStr::new("json")) {
match fs.load(&path).await {
Ok(content) => {
let user_prompt: UserPrompt =
serde_json::from_str(&content).map_err(|e| {
anyhow::anyhow!("Failed to deserialize UserPrompt: {}", e)
})?;
prompts.push(user_prompt);
}
Err(e) => eprintln!("Failed to load file {}: {}", path.display(), e),
}
}
}
Ok(prompts)
}
}
pub struct PromptManager {
focus_handle: FocusHandle,
prompt_library: Arc<PromptLibrary>,
active_prompt: Option<String>,
}
impl PromptManager {
pub fn new(prompt_library: Arc<PromptLibrary>, cx: &mut WindowContext) -> Self {
let focus_handle = cx.focus_handle();
Self {
focus_handle,
prompt_library,
active_prompt: None,
}
}
pub fn set_active_prompt(&mut self, prompt_id: Option<String>) {
self.active_prompt = prompt_id;
}
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
cx.emit(DismissEvent);
}
}
impl Render for PromptManager {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let prompt_library = self.prompt_library.clone();
let prompts = prompt_library
.clone()
.prompts_with_ids()
.clone()
.into_iter()
.collect::<Vec<_>>();
let active_prompt = self.active_prompt.as_ref().and_then(|id| {
prompt_library
.prompts_with_ids()
.iter()
.find(|(prompt_id, _)| prompt_id == id)
.map(|(_, prompt)| prompt.clone())
});
v_flex()
.key_context("PromptManager")
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::dismiss))
.elevation_3(cx)
.size_full()
.flex_none()
.w(rems(54.))
.h(rems(40.))
.overflow_hidden()
.child(
ModalHeader::new()
.headline("Prompt Library")
.show_dismiss_button(true),
)
.child(
h_flex()
.flex_grow()
.overflow_hidden()
.border_t_1()
.border_color(cx.theme().colors().border)
.child(
div()
.id("prompt-preview")
.overflow_y_scroll()
.h_full()
.min_w_64()
.max_w_1_2()
.child(
v_flex()
.justify_start()
.py(Spacing::Medium.rems(cx))
.px(Spacing::Large.rems(cx))
.bg(cx.theme().colors().surface_background)
.when_else(
!prompts.is_empty(),
|with_items| {
with_items.children(prompts.into_iter().map(
|(id, prompt)| {
let prompt_library = prompt_library.clone();
let prompt = prompt.clone();
let prompt_id = id.clone();
let shared_string_id: SharedString =
id.clone().into();
let default_prompt_ids =
prompt_library.clone().default_prompt_ids();
let is_default =
default_prompt_ids.contains(&id);
// We'll use this for conditionally enabled prompts
// like those loaded only for certain languages
let is_conditional = false;
let selection =
match (is_default, is_conditional) {
(_, true) => Selection::Indeterminate,
(true, _) => Selection::Selected,
(false, _) => Selection::Unselected,
};
v_flex()
.id(ElementId::Name(
format!("prompt-{}", shared_string_id)
.into(),
))
.p(Spacing::Small.rems(cx))
.on_click(cx.listener({
let prompt_id = prompt_id.clone();
move |this, _event, _cx| {
this.set_active_prompt(Some(
prompt_id.clone(),
));
}
}))
.child(
h_flex()
.justify_between()
.child(
h_flex()
.gap(Spacing::Large.rems(cx))
.child(
Checkbox::new(
shared_string_id,
selection,
)
.on_click(move |_, _cx| {
if is_default {
prompt_library
.clone()
.remove_prompt_from_default(
prompt_id.clone(),
)
.log_err();
} else {
prompt_library
.clone()
.add_prompt_to_default(
prompt_id.clone(),
)
.log_err();
}
}),
)
.child(Label::new(
prompt.title,
)),
)
.child(div()),
)
},
))
},
|no_items| {
no_items.child(
Label::new("No prompts").color(Color::Placeholder),
)
},
),
),
)
.child(
div()
.id("prompt-preview")
.overflow_y_scroll()
.border_l_1()
.border_color(cx.theme().colors().border)
.size_full()
.flex_none()
.child(
v_flex()
.justify_start()
.py(Spacing::Medium.rems(cx))
.px(Spacing::Large.rems(cx))
.gap(Spacing::Large.rems(cx))
.when_else(
active_prompt.is_some(),
|with_prompt| {
let active_prompt = active_prompt.as_ref().unwrap();
with_prompt
.child(
v_flex()
.gap_0p5()
.child(
Headline::new(
active_prompt.title.clone(),
)
.size(HeadlineSize::XSmall),
)
.child(
h_flex()
.child(
Label::new(
active_prompt
.author
.clone(),
)
.size(LabelSize::XSmall)
.color(Color::Muted),
)
.child(
Label::new(
if active_prompt
.languages
.is_empty()
|| active_prompt
.languages[0]
== "*"
{
" · Global".to_string()
} else {
format!(
" · {}",
active_prompt
.languages
.join(", ")
)
},
)
.size(LabelSize::XSmall)
.color(Color::Muted),
),
),
)
.child(
div()
.w_full()
.max_w(rems(30.))
.text_ui(cx)
.child(active_prompt.prompt.clone()),
)
},
|without_prompt| {
without_prompt.justify_center().items_center().child(
Label::new("Select a prompt to view details.")
.color(Color::Placeholder),
)
},
),
),
),
)
}
}
impl EventEmitter<DismissEvent> for PromptManager {}
impl ModalView for PromptManager {}
impl FocusableView for PromptManager {
fn focus_handle(&self, _cx: &AppContext) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}

View File

@@ -1,95 +1,7 @@
use language::BufferSnapshot;
use std::{fmt::Write, ops::Range};
mod prompt;
mod prompt_library;
mod 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)
}
pub use prompt::*;
pub use prompt_library::*;
pub use prompt_manager::*;

View File

@@ -0,0 +1,360 @@
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

@@ -0,0 +1,245 @@
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

@@ -0,0 +1,512 @@
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

@@ -0,0 +1,329 @@
use crate::assistant_panel::ConversationEditor;
use anyhow::Result;
pub use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandRegistry};
use editor::{CompletionProvider, Editor};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{Model, Task, ViewContext, WeakView, WindowContext};
use language::{Anchor, Buffer, CodeLabel, Documentation, LanguageServerId, ToPoint};
use parking_lot::{Mutex, RwLock};
use rope::Point;
use std::{
ops::Range,
sync::{
atomic::{AtomicBool, Ordering::SeqCst},
Arc,
},
};
use workspace::Workspace;
pub mod active_command;
pub mod file_command;
pub mod project_command;
pub mod prompt_command;
pub mod rustdoc_command;
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>,
}
pub(crate) struct SlashCommandLine {
/// The range within the line containing the command name.
pub name: Range<usize>,
/// The range within the line containing the command argument.
pub argument: Option<Range<usize>>,
}
impl SlashCommandCompletionProvider {
pub fn new(
editor: WeakView<ConversationEditor>,
commands: Arc<SlashCommandRegistry>,
workspace: WeakView<Workspace>,
) -> Self {
Self {
cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
editor,
commands,
workspace,
}
}
fn complete_command_name(
&self,
command_name: &str,
command_range: Range<Anchor>,
name_range: Range<Anchor>,
cx: &mut WindowContext,
) -> Task<Result<Vec<project::Completion>>> {
let candidates = self
.commands
.command_names()
.into_iter()
.enumerate()
.map(|(ix, def)| StringMatchCandidate {
id: ix,
string: def.to_string(),
char_bag: def.as_ref().into(),
})
.collect::<Vec<_>>();
let commands = self.commands.clone();
let command_name = command_name.to_string();
let editor = self.editor.clone();
let workspace = self.workspace.clone();
cx.spawn(|mut cx| async move {
let matches = match_strings(
&candidates,
&command_name,
true,
usize::MAX,
&Default::default(),
cx.background_executor().clone(),
)
.await;
cx.update(|cx| {
matches
.into_iter()
.filter_map(|mat| {
let command = commands.command(&mat.string)?;
let mut new_text = mat.string.clone();
let requires_argument = command.requires_argument();
if requires_argument {
new_text.push(' ');
}
Some(project::Completion {
old_range: name_range.clone(),
documentation: Some(Documentation::SingleLine(command.description())),
new_text,
label: command.label(cx),
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<_>
}),
})
})
.collect()
})
})
}
fn complete_command_argument(
&self,
command_name: &str,
argument: String,
command_range: Range<Anchor>,
argument_range: Range<Anchor>,
cx: &mut WindowContext,
) -> Task<Result<Vec<project::Completion>>> {
let new_cancel_flag = Arc::new(AtomicBool::new(false));
let mut flag = self.cancel_flag.lock();
flag.store(true, SeqCst);
*flag = new_cancel_flag.clone();
if let Some(command) = self.commands.command(command_name) {
let completions = command.complete_argument(
argument,
new_cancel_flag.clone(),
self.workspace.clone(),
cx,
);
let command_name: Arc<str> = command_name.into();
let editor = self.editor.clone();
let workspace = self.workspace.clone();
cx.background_executor().spawn(async move {
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();
}
})),
})
.collect())
})
} else {
cx.background_executor()
.spawn(async move { Ok(Vec::new()) })
}
}
}
impl CompletionProvider for SlashCommandCompletionProvider {
fn completions(
&self,
buffer: &Model<Buffer>,
buffer_position: Anchor,
cx: &mut ViewContext<Editor>,
) -> Task<Result<Vec<project::Completion>>> {
let Some((name, argument, command_range, argument_range)) =
buffer.update(cx, |buffer, _cx| {
let position = buffer_position.to_point(buffer);
let line_start = Point::new(position.row, 0);
let mut lines = buffer.text_for_range(line_start..position).lines();
let line = lines.next()?;
let call = SlashCommandLine::parse(line)?;
let command_range_start = Point::new(position.row, call.name.start as u32 - 1);
let command_range_end = Point::new(
position.row,
call.argument.as_ref().map_or(call.name.end, |arg| arg.end) as u32,
);
let command_range = buffer.anchor_after(command_range_start)
..buffer.anchor_after(command_range_end);
let name = line[call.name.clone()].to_string();
Some(if let Some(argument) = call.argument {
let start =
buffer.anchor_after(Point::new(position.row, argument.start as u32));
let argument = line[argument.clone()].to_string();
(name, Some(argument), command_range, start..buffer_position)
} else {
let start =
buffer.anchor_after(Point::new(position.row, call.name.start as u32));
(name, None, command_range, start..buffer_position)
})
})
else {
return Task::ready(Ok(Vec::new()));
};
if let Some(argument) = argument {
self.complete_command_argument(&name, argument, command_range, argument_range, cx)
} else {
self.complete_command_name(&name, command_range, argument_range, cx)
}
}
fn resolve_completions(
&self,
_: Model<Buffer>,
_: Vec<usize>,
_: Arc<RwLock<Box<[project::Completion]>>>,
_: &mut ViewContext<Editor>,
) -> Task<Result<bool>> {
Task::ready(Ok(true))
}
fn apply_additional_edits_for_completion(
&self,
_: Model<Buffer>,
_: project::Completion,
_: bool,
_: &mut ViewContext<Editor>,
) -> Task<Result<Option<language::Transaction>>> {
Task::ready(Ok(None))
}
fn is_completion_trigger(
&self,
buffer: &Model<Buffer>,
position: language::Anchor,
_text: &str,
_trigger_in_words: bool,
cx: &mut ViewContext<Editor>,
) -> bool {
let buffer = buffer.read(cx);
let position = position.to_point(buffer);
let line_start = Point::new(position.row, 0);
let mut lines = buffer.text_for_range(line_start..position).lines();
if let Some(line) = lines.next() {
SlashCommandLine::parse(line).is_some()
} else {
false
}
}
}
impl SlashCommandLine {
pub(crate) fn parse(line: &str) -> Option<Self> {
let mut call: Option<Self> = None;
let mut ix = 0;
for c in line.chars() {
let next_ix = ix + c.len_utf8();
if let Some(call) = &mut call {
// The command arguments start at the first non-whitespace character
// after the command name, and continue until the end of the line.
if let Some(argument) = &mut call.argument {
if (*argument).is_empty() && c.is_whitespace() {
argument.start = next_ix;
}
argument.end = next_ix;
}
// The command name ends at the first whitespace character.
else if !call.name.is_empty() {
if c.is_whitespace() {
call.argument = Some(next_ix..next_ix);
} else {
call.name.end = next_ix;
}
}
// The command name must begin with a letter.
else if c.is_alphabetic() {
call.name.end = next_ix;
} else {
return None;
}
}
// Commands start with a slash.
else if c == '/' {
call = Some(SlashCommandLine {
name: next_ix..next_ix,
argument: None,
});
}
// The line can't contain anything before the slash except for whitespace.
else if !c.is_whitespace() {
return None;
}
ix = next_ix;
}
call
}
}

View File

@@ -0,0 +1,104 @@
use super::{file_command::FilePlaceholder, SlashCommand, SlashCommandOutput};
use anyhow::{anyhow, Result};
use assistant_slash_command::SlashCommandOutputSection;
use editor::Editor;
use gpui::{AppContext, Task, WeakView};
use language::LspAdapterDelegate;
use std::{borrow::Cow, sync::Arc};
use ui::{IntoElement, WindowContext};
use workspace::Workspace;
pub(crate) struct ActiveSlashCommand;
impl SlashCommand for ActiveSlashCommand {
fn name(&self) -> String {
"active".into()
}
fn description(&self) -> String {
"insert active tab".into()
}
fn tooltip_text(&self) -> String {
"insert active tab".into()
}
fn complete_argument(
&self,
_query: String,
_cancel: std::sync::Arc<std::sync::atomic::AtomicBool>,
_workspace: WeakView<Workspace>,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
fn requires_argument(&self) -> bool {
false
}
fn run(
self: Arc<Self>,
_argument: Option<&str>,
workspace: WeakView<Workspace>,
_delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let output = workspace.update(cx, |workspace, cx| {
let Some(active_item) = workspace.active_item(cx) else {
return Task::ready(Err(anyhow!("no active tab")));
};
let Some(buffer) = active_item
.downcast::<Editor>()
.and_then(|editor| editor.read(cx).buffer().read(cx).as_singleton())
else {
return Task::ready(Err(anyhow!("active tab is not an editor")));
};
let snapshot = buffer.read(cx).snapshot();
let path = snapshot.resolve_file_path(cx, true);
let text = cx.background_executor().spawn({
let path = path.clone();
async move {
let path = path
.as_ref()
.map(|path| path.to_string_lossy())
.unwrap_or_else(|| Cow::Borrowed("untitled"));
let mut output = String::with_capacity(path.len() + snapshot.len() + 9);
output.push_str("```");
output.push_str(&path);
output.push('\n');
for chunk in snapshot.as_rope().chunks() {
output.push_str(chunk);
}
if !output.ends_with('\n') {
output.push('\n');
}
output.push_str("```");
output
}
});
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, _| {
FilePlaceholder {
id,
path: path.clone(),
line_range: None,
unfold,
}
.into_any_element()
}),
}],
})
})
});
output.unwrap_or_else(|error| Task::ready(Err(error)))
}
}

View File

@@ -0,0 +1,225 @@
use super::{SlashCommand, SlashCommandOutput};
use anyhow::{anyhow, Result};
use assistant_slash_command::SlashCommandOutputSection;
use fuzzy::PathMatch;
use gpui::{AppContext, RenderOnce, SharedString, Task, View, WeakView};
use language::{LineEnding, LspAdapterDelegate};
use project::PathMatchCandidateSet;
use std::{
ops::Range,
path::{Path, PathBuf},
sync::{atomic::AtomicBool, Arc},
};
use ui::{prelude::*, ButtonLike, ElevationIndex};
use workspace::Workspace;
pub(crate) struct FileSlashCommand;
impl FileSlashCommand {
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 project = workspace.project().read(cx);
let entries = workspace.recent_navigation_history(Some(10), cx);
let path_prefix: Arc<str> = "".into();
Task::ready(
entries
.into_iter()
.filter_map(|(entry, _)| {
let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
let mut full_path = PathBuf::from(worktree.read(cx).root_name());
full_path.push(&entry.path);
Some(PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: entry.worktree_id.to_usize(),
path: full_path.into(),
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: true,
directories_only: false,
}
})
.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 FileSlashCommand {
fn name(&self) -> String {
"file".into()
}
fn description(&self) -> String {
"insert a file".into()
}
fn tooltip_text(&self) -> String {
"insert file".into()
}
fn requires_argument(&self) -> bool {
true
}
fn complete_argument(
&self,
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: WeakView<Workspace>,
cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow!("workspace was dropped")));
};
let paths = self.search_paths(query, cancellation_flag, &workspace, cx);
cx.background_executor().spawn(async move {
Ok(paths
.await
.into_iter()
.map(|path_match| {
format!(
"{}{}",
path_match.path_prefix,
path_match.path.to_string_lossy()
)
})
.collect())
})
}
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 Some(argument) = argument else {
return Task::ready(Err(anyhow!("missing path")));
};
let path = PathBuf::from(argument);
let abs_path = workspace
.read(cx)
.visible_worktrees(cx)
.find_map(|worktree| {
let worktree = worktree.read(cx);
let worktree_root_path = Path::new(worktree.root_name());
let relative_path = path.strip_prefix(worktree_root_path).ok()?;
worktree.absolutize(&relative_path).ok()
});
let Some(abs_path) = abs_path else {
return Task::ready(Err(anyhow!("missing path")));
};
let fs = workspace.read(cx).app_state().fs.clone();
let argument = argument.to_string();
let text = cx.background_executor().spawn(async move {
let mut content = fs.load(&abs_path).await?;
LineEnding::normalize(&mut content);
let mut output = String::with_capacity(argument.len() + content.len() + 9);
output.push_str("```");
output.push_str(&argument);
output.push('\n');
output.push_str(&content);
if !output.ends_with('\n') {
output.push('\n');
}
output.push_str("```");
anyhow::Ok(output)
});
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| {
FilePlaceholder {
path: Some(path.clone()),
line_range: None,
id,
unfold,
}
.into_any_element()
}),
}],
})
})
}
}
#[derive(IntoElement)]
pub struct FilePlaceholder {
pub path: Option<PathBuf>,
pub line_range: Option<Range<u32>>,
pub id: ElementId,
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
}
impl RenderOnce for FilePlaceholder {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
let unfold = self.unfold;
let title = if let Some(path) = self.path.as_ref() {
SharedString::from(path.to_string_lossy().to_string())
} else {
SharedString::from("untitled")
};
ButtonLike::new(self.id)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ElevatedSurface)
.child(Icon::new(IconName::File))
.child(Label::new(title))
.when_some(self.line_range, |button, line_range| {
button.child(Label::new(":")).child(Label::new(format!(
"{}-{}",
line_range.start, line_range.end
)))
})
.on_click(move |_, cx| unfold(cx))
}
}

View File

@@ -0,0 +1,156 @@
use super::{SlashCommand, SlashCommandOutput};
use anyhow::{anyhow, Context, Result};
use assistant_slash_command::SlashCommandOutputSection;
use fs::Fs;
use gpui::{AppContext, Model, Task, WeakView};
use language::LspAdapterDelegate;
use project::{Project, ProjectPath};
use std::{
fmt::Write,
path::Path,
sync::{atomic::AtomicBool, Arc},
};
use ui::{prelude::*, ButtonLike, ElevationIndex};
use workspace::Workspace;
pub(crate) struct ProjectSlashCommand;
impl ProjectSlashCommand {
async fn build_message(fs: Arc<dyn Fs>, path_to_cargo_toml: &Path) -> Result<String> {
let buffer = fs.load(path_to_cargo_toml).await?;
let cargo_toml: cargo_toml::Manifest = toml::from_str(&buffer)?;
let mut message = String::new();
writeln!(message, "You are in a Rust project.")?;
if let Some(workspace) = cargo_toml.workspace {
writeln!(
message,
"The project is a Cargo workspace with the following members:"
)?;
for member in workspace.members {
writeln!(message, "- {member}")?;
}
if !workspace.default_members.is_empty() {
writeln!(message, "The default members are:")?;
for member in workspace.default_members {
writeln!(message, "- {member}")?;
}
}
if !workspace.dependencies.is_empty() {
writeln!(
message,
"The following workspace dependencies are installed:"
)?;
for dependency in workspace.dependencies.keys() {
writeln!(message, "- {dependency}")?;
}
}
} else if let Some(package) = cargo_toml.package {
writeln!(
message,
"The project name is \"{name}\".",
name = package.name
)?;
let description = package
.description
.as_ref()
.and_then(|description| description.get().ok().cloned());
if let Some(description) = description.as_ref() {
writeln!(message, "It describes itself as \"{description}\".")?;
}
if !cargo_toml.dependencies.is_empty() {
writeln!(message, "The following dependencies are installed:")?;
for dependency in cargo_toml.dependencies.keys() {
writeln!(message, "- {dependency}")?;
}
}
}
Ok(message)
}
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(),
))
}
}
impl SlashCommand for ProjectSlashCommand {
fn name(&self) -> String {
"project".into()
}
fn description(&self) -> String {
"insert current project context".into()
}
fn tooltip_text(&self) -> String {
"insert current project context".into()
}
fn complete_argument(
&self,
_query: String,
_cancel: Arc<AtomicBool>,
_workspace: WeakView<Workspace>,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
fn requires_argument(&self) -> bool {
false
}
fn run(
self: Arc<Self>,
_argument: Option<&str>,
workspace: WeakView<Workspace>,
_delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let output = workspace.update(cx, |workspace, cx| {
let project = workspace.project().clone();
let fs = workspace.project().read(cx).fs().clone();
let path = Self::path_to_cargo_toml(project, cx);
let output = cx.background_executor().spawn(async move {
let path = path.with_context(|| "Cargo.toml not found")?;
Self::build_message(fs, &path).await
});
cx.foreground_executor().spawn(async move {
let text = output.await?;
let range = 0..text.len();
Ok(SlashCommandOutput {
text,
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::FileTree))
.child(Label::new("Project"))
.on_click(move |_, cx| unfold(cx))
.into_any_element()
}),
}],
})
})
});
output.unwrap_or_else(|error| Task::ready(Err(error)))
}
}

View File

@@ -0,0 +1,117 @@
use super::{SlashCommand, SlashCommandOutput};
use crate::prompts::PromptLibrary;
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 }
}
}
impl SlashCommand for PromptSlashCommand {
fn name(&self) -> String {
"prompt".into()
}
fn description(&self) -> String {
"insert a prompt from the library".into()
}
fn tooltip_text(&self) -> String {
"insert prompt".into()
}
fn requires_argument(&self) -> bool {
true
}
fn complete_argument(
&self,
query: String,
cancellation_flag: Arc<AtomicBool>,
_workspace: WeakView<Workspace>,
cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
let library = self.library.clone();
let executor = cx.background_executor().clone();
cx.background_executor().spawn(async move {
let candidates = library
.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())
.collect())
})
}
fn run(
self: Arc<Self>,
title: Option<&str>,
_workspace: WeakView<Workspace>,
_delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let Some(title) = title else {
return Task::ready(Err(anyhow!("missing prompt name")));
};
let library = self.library.clone();
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())
}
});
cx.foreground_executor().spawn(async move {
let prompt = prompt.await?;
let range = 0..prompt.len();
Ok(SlashCommandOutput {
text: prompt,
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()
}),
}],
})
})
}
}

View File

@@ -0,0 +1,137 @@
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 http::{AsyncBody, HttpClient, HttpClientWithUrl};
use language::LspAdapterDelegate;
use rustdoc_to_markdown::convert_rustdoc_to_markdown;
use ui::{prelude::*, ButtonLike, ElevationIndex};
use workspace::Workspace;
pub(crate) struct RustdocSlashCommand;
impl RustdocSlashCommand {
async fn build_message(
http_client: Arc<HttpClientWithUrl>,
crate_name: String,
) -> Result<String> {
let mut response = http_client
.get(
&format!("https://docs.rs/{crate_name}"),
AsyncBody::default(),
true,
)
.await?;
let mut body = Vec::new();
response
.body_mut()
.read_to_end(&mut body)
.await
.context("error reading docs.rs 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()
);
}
convert_rustdoc_to_markdown(&body[..])
}
}
impl SlashCommand for RustdocSlashCommand {
fn name(&self) -> String {
"rustdoc".into()
}
fn description(&self) -> String {
"insert the docs for a Rust crate".into()
}
fn tooltip_text(&self) -> String {
"insert rustdoc".into()
}
fn requires_argument(&self) -> bool {
true
}
fn complete_argument(
&self,
_query: String,
_cancel: Arc<AtomicBool>,
_workspace: 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 crate name")));
};
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow!("workspace was dropped")));
};
let http_client = workspace.read(cx).client().http_client();
let crate_name = argument.to_string();
let text = cx.background_executor().spawn({
let crate_name = crate_name.clone();
async move { Self::build_message(http_client, crate_name).await }
});
let crate_name = SharedString::from(crate_name);
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| {
RustdocPlaceholder {
id,
unfold,
crate_name: crate_name.clone(),
}
.into_any_element()
}),
}],
})
})
}
}
#[derive(IntoElement)]
struct RustdocPlaceholder {
pub id: ElementId,
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
pub crate_name: SharedString,
}
impl RenderOnce for RustdocPlaceholder {
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::FileRust))
.child(Label::new(format!("rustdoc: {}", self.crate_name)))
.on_click(move |_, cx| unfold(cx))
}
}

View File

@@ -0,0 +1,191 @@
use super::{file_command::FilePlaceholder, SlashCommand, SlashCommandOutput};
use anyhow::Result;
use assistant_slash_command::SlashCommandOutputSection;
use gpui::{AppContext, Task, WeakView};
use language::{CodeLabel, HighlightId, LineEnding, LspAdapterDelegate};
use semantic_index::SemanticIndex;
use std::{
fmt::Write,
path::PathBuf,
sync::{atomic::AtomicBool, Arc},
};
use ui::{prelude::*, ButtonLike, ElevationIndex, Icon, IconName};
use util::ResultExt;
use workspace::Workspace;
pub(crate) struct SearchSlashCommand;
impl SlashCommand for SearchSlashCommand {
fn name(&self) -> String {
"search".into()
}
fn label(&self, cx: &AppContext) -> CodeLabel {
let mut label = CodeLabel::default();
label.push_str("search ", None);
label.push_str(
"--n",
cx.theme().syntax().highlight_id("comment").map(HighlightId),
);
label.filter_range = 0.."search".len();
label
}
fn description(&self) -> String {
"semantically search files".into()
}
fn tooltip_text(&self) -> String {
"search".into()
}
fn requires_argument(&self) -> bool {
true
}
fn complete_argument(
&self,
_query: String,
_cancel: Arc<AtomicBool>,
_workspace: 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(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
};
let Some(argument) = argument else {
return Task::ready(Err(anyhow::anyhow!("missing search query")));
};
let mut limit = None;
let mut query = String::new();
for part in argument.split(' ') {
if let Some(parameter) = part.strip_prefix("--") {
if let Ok(count) = parameter.parse::<usize>() {
limit = Some(count);
continue;
}
}
query.push_str(part);
query.push(' ');
}
query.pop();
if query.is_empty() {
return Task::ready(Err(anyhow::anyhow!("missing search query")));
}
let project = workspace.read(cx).project().clone();
let fs = project.read(cx).fs().clone();
let project_index =
cx.update_global(|index: &mut SemanticIndex, cx| index.project_index(project, cx));
cx.spawn(|cx| async move {
let results = project_index
.read_with(&cx, |project_index, cx| {
project_index.search(query.clone(), limit.unwrap_or(5), cx)
})?
.await?;
let mut loaded_results = Vec::new();
for result in results {
let (full_path, file_content) =
result.worktree.read_with(&cx, |worktree, _cx| {
let entry_abs_path = worktree.abs_path().join(&result.path);
let mut entry_full_path = PathBuf::from(worktree.root_name());
entry_full_path.push(&result.path);
let file_content = async {
let entry_abs_path = entry_abs_path;
fs.load(&entry_abs_path).await
};
(entry_full_path, file_content)
})?;
if let Some(file_content) = file_content.await.log_err() {
loaded_results.push((result, full_path, file_content));
}
}
let output = cx
.background_executor()
.spawn(async move {
let mut text = format!("Search results for {query}:\n");
let mut sections = Vec::new();
for (result, full_path, file_content) in loaded_results {
let range_start = result.range.start.min(file_content.len());
let range_end = result.range.end.min(file_content.len());
let start_line =
file_content[0..range_start].matches('\n').count() as u32 + 1;
let end_line = file_content[0..range_end].matches('\n').count() as u32 + 1;
let start_line_byte_offset = file_content[0..range_start]
.rfind('\n')
.map(|pos| pos + 1)
.unwrap_or_default();
let end_line_byte_offset = file_content[range_end..]
.find('\n')
.map(|pos| range_end + pos)
.unwrap_or_else(|| file_content.len());
let section_start_ix = text.len();
writeln!(
text,
"```{}:{}-{}",
result.path.display(),
start_line,
end_line,
)
.unwrap();
let mut excerpt =
file_content[start_line_byte_offset..end_line_byte_offset].to_string();
LineEnding::normalize(&mut excerpt);
text.push_str(&excerpt);
writeln!(text, "\n```\n").unwrap();
let section_end_ix = text.len() - 1;
sections.push(SlashCommandOutputSection {
range: section_start_ix..section_end_ix,
render_placeholder: Arc::new(move |id, unfold, _| {
FilePlaceholder {
id,
path: Some(full_path.clone()),
line_range: Some(start_line..end_line),
unfold,
}
.into_any_element()
}),
});
}
let query = SharedString::from(query);
sections.push(SlashCommandOutputSection {
range: 0..text.len(),
render_placeholder: Arc::new(move |id, unfold, _cx| {
ButtonLike::new(id)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ElevatedSurface)
.child(Icon::new(IconName::MagnifyingGlass))
.child(Label::new(query.clone()))
.on_click(move |_, cx| unfold(cx))
.into_any_element()
}),
});
SlashCommandOutput { text, sections }
})
.await;
Ok(output)
})
}
}

View File

@@ -0,0 +1,117 @@
use super::{file_command::FilePlaceholder, SlashCommand, SlashCommandOutput};
use anyhow::{anyhow, Result};
use assistant_slash_command::SlashCommandOutputSection;
use collections::HashMap;
use editor::Editor;
use gpui::{AppContext, Entity, Task, WeakView};
use language::LspAdapterDelegate;
use std::{fmt::Write, path::Path, sync::Arc};
use ui::{IntoElement, WindowContext};
use workspace::Workspace;
pub(crate) struct TabsSlashCommand;
impl SlashCommand for TabsSlashCommand {
fn name(&self) -> String {
"tabs".into()
}
fn description(&self) -> String {
"insert content from open tabs".into()
}
fn tooltip_text(&self) -> String {
"insert open tabs".into()
}
fn requires_argument(&self) -> bool {
false
}
fn complete_argument(
&self,
_query: String,
_cancel: Arc<std::sync::atomic::AtomicBool>,
_workspace: 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 open_buffers = workspace.update(cx, |workspace, cx| {
let mut timestamps_by_entity_id = HashMap::default();
let mut open_buffers = Vec::new();
for pane in workspace.panes() {
let pane = pane.read(cx);
for entry in pane.activation_history() {
timestamps_by_entity_id.insert(entry.entity_id, entry.timestamp);
}
}
for editor in workspace.items_of_type::<Editor>(cx) {
if let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() {
if let Some(timestamp) = timestamps_by_entity_id.get(&editor.entity_id()) {
let snapshot = buffer.read(cx).snapshot();
let full_path = snapshot.resolve_file_path(cx, true);
open_buffers.push((full_path, snapshot, *timestamp));
}
}
}
open_buffers
});
match open_buffers {
Ok(mut open_buffers) => cx.background_executor().spawn(async move {
open_buffers.sort_by_key(|(_, _, timestamp)| *timestamp);
let mut sections = Vec::new();
let mut text = String::new();
for (full_path, buffer, _) in open_buffers {
let section_start_ix = text.len();
writeln!(
text,
"```{}\n",
full_path
.as_deref()
.unwrap_or(Path::new("untitled"))
.display()
)
.unwrap();
for chunk in buffer.as_rope().chunks() {
text.push_str(chunk);
}
if !text.ends_with('\n') {
text.push('\n');
}
writeln!(text, "```\n").unwrap();
let section_end_ix = text.len() - 1;
sections.push(SlashCommandOutputSection {
range: section_start_ix..section_end_ix,
render_placeholder: Arc::new(move |id, unfold, _| {
FilePlaceholder {
id,
path: full_path.clone(),
line_range: None,
unfold,
}
.into_any_element()
}),
});
}
Ok(SlashCommandOutput { text, sections })
}),
Err(error) => Task::ready(Err(error)),
}
}
}

View File

@@ -1,66 +0,0 @@
[package]
name = "assistant2"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/assistant2.rs"
[features]
default = []
stories = ["dep:story"]
[dependencies]
anyhow.workspace = true
assistant_tooling.workspace = true
client.workspace = true
chrono.workspace = true
collections.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
language.workspace = true
log.workspace = true
markdown.workspace = true
open_ai.workspace = true
picker.workspace = true
project.workspace = true
regex.workspace = true
schemars.workspace = true
semantic_index.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
story = { workspace = true, optional = true }
theme.workspace = true
ui.workspace = true
util.workspace = true
unindent.workspace = true
workspace.workspace = true
[dev-dependencies]
assets.workspace = true
editor = { workspace = true, features = ["test-support"] }
env_logger.workspace = true
gpui = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] }
languages.workspace = true
markdown = { workspace = true, features = ["test-support"] }
node_runtime.workspace = true
project = { workspace = true, features = ["test-support"] }
rand.workspace = true
release_channel.workspace = true
settings = { workspace = true, features = ["test-support"] }
theme = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] }
http = { workspace = true, features = ["test-support"] }
workspace = { workspace = true, features = ["test-support"] }

View File

@@ -1 +0,0 @@
> Give me a comprehensive list of all the elements defined in my project using the following query: `impl Element for {}, impl<T: 'static> Element for {}, impl IntoElement for {})`

View File

@@ -1 +0,0 @@
> What are all the places we define a new gpui element in my project? (impl Element for {})

View File

@@ -1,3 +0,0 @@
Use tools frequently, especially when referring to files and code. The Zed editor we're working in can show me files directly when you add annotations. Be concise in chat, bountiful in tool calling.
Teach me everything you can about how zed loads settings. Please annotate the code inline.

View File

@@ -1 +0,0 @@
> Can you tell me what the assistant2 crate is for in my project? Tell me in 100 words or less.

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +0,0 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
#[derive(Default, Debug, Deserialize, Serialize, Clone)]
pub struct AssistantSettings {
pub enabled: bool,
}
#[derive(Default, Debug, Deserialize, Serialize, Clone, JsonSchema)]
pub struct AssistantSettingsContent {
pub enabled: Option<bool>,
}
impl Settings for AssistantSettings {
const KEY: Option<&'static str> = Some("assistant_v2");
type FileContent = AssistantSettingsContent;
fn load(
sources: SettingsSources<Self::FileContent>,
_: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
Ok(sources.json_merge().unwrap_or_else(|_| Default::default()))
}
}

View File

@@ -1,3 +0,0 @@
mod active_file;
pub use active_file::*;

View File

@@ -1,144 +0,0 @@
use std::{path::PathBuf, sync::Arc};
use anyhow::{anyhow, Result};
use assistant_tooling::{AttachmentOutput, LanguageModelAttachment, ProjectContext};
use editor::Editor;
use gpui::{Render, Task, View, WeakModel, WeakView};
use language::Buffer;
use project::ProjectPath;
use serde::{Deserialize, Serialize};
use ui::{prelude::*, ButtonLike, Tooltip, WindowContext};
use util::maybe;
use workspace::Workspace;
#[derive(Serialize, Deserialize)]
pub struct ActiveEditorAttachment {
#[serde(skip)]
buffer: Option<WeakModel<Buffer>>,
path: Option<PathBuf>,
}
pub struct FileAttachmentView {
project_path: Option<ProjectPath>,
buffer: Option<WeakModel<Buffer>>,
error: Option<anyhow::Error>,
}
impl Render for FileAttachmentView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
if let Some(error) = &self.error {
return div().child(error.to_string()).into_any_element();
}
let filename: SharedString = self
.project_path
.as_ref()
.and_then(|p| p.path.file_name()?.to_str())
.unwrap_or("Untitled")
.to_string()
.into();
ButtonLike::new("file-attachment")
.child(
h_flex()
.gap_1()
.bg(cx.theme().colors().editor_background)
.rounded_md()
.child(ui::Icon::new(IconName::File))
.child(filename.clone()),
)
.tooltip(move |cx| Tooltip::with_meta("File Attached", None, filename.clone(), cx))
.into_any_element()
}
}
impl AttachmentOutput for FileAttachmentView {
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String {
if let Some(path) = &self.project_path {
project.add_file(path.clone());
return format!("current file: {}", path.path.display());
}
if let Some(buffer) = self.buffer.as_ref().and_then(|buffer| buffer.upgrade()) {
return format!("current untitled buffer text:\n{}", buffer.read(cx).text());
}
String::new()
}
}
pub struct ActiveEditorAttachmentTool {
workspace: WeakView<Workspace>,
}
impl ActiveEditorAttachmentTool {
pub fn new(workspace: WeakView<Workspace>, _cx: &mut WindowContext) -> Self {
Self { workspace }
}
}
impl LanguageModelAttachment for ActiveEditorAttachmentTool {
type Output = ActiveEditorAttachment;
type View = FileAttachmentView;
fn name(&self) -> Arc<str> {
"active-editor-attachment".into()
}
fn run(&self, cx: &mut WindowContext) -> Task<Result<ActiveEditorAttachment>> {
Task::ready(maybe!({
let active_buffer = self
.workspace
.update(cx, |workspace, cx| {
workspace
.active_item(cx)
.and_then(|item| Some(item.act_as::<Editor>(cx)?.read(cx).buffer().clone()))
})?
.ok_or_else(|| anyhow!("no active buffer"))?;
let buffer = active_buffer.read(cx);
if let Some(buffer) = buffer.as_singleton() {
let path = project::File::from_dyn(buffer.read(cx).file())
.and_then(|file| file.worktree.read(cx).absolutize(&file.path).ok());
return Ok(ActiveEditorAttachment {
buffer: Some(buffer.downgrade()),
path,
});
} else {
Err(anyhow!("no active buffer"))
}
}))
}
fn view(
&self,
output: Result<ActiveEditorAttachment>,
cx: &mut WindowContext,
) -> View<Self::View> {
let error;
let project_path;
let buffer;
match output {
Ok(output) => {
error = None;
let workspace = self.workspace.upgrade().unwrap();
let project = workspace.read(cx).project();
project_path = output
.path
.and_then(|path| project.read(cx).project_path_for_absolute_path(&path, cx));
buffer = output.buffer;
}
Err(err) => {
error = Some(err);
buffer = None;
project_path = None;
}
}
cx.new_view(|_cx| FileAttachmentView {
project_path,
buffer,
error,
})
}
}

View File

@@ -1,179 +0,0 @@
use anyhow::Result;
use assistant_tooling::ToolFunctionDefinition;
use client::{proto, Client};
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
use gpui::Global;
use std::sync::Arc;
pub use open_ai::RequestMessage as CompletionMessage;
#[derive(Clone)]
pub struct CompletionProvider(Arc<dyn CompletionProviderBackend>);
impl CompletionProvider {
pub fn new(backend: impl CompletionProviderBackend) -> Self {
Self(Arc::new(backend))
}
pub fn default_model(&self) -> String {
self.0.default_model()
}
pub fn available_models(&self) -> Vec<String> {
self.0.available_models()
}
pub fn complete(
&self,
model: String,
messages: Vec<CompletionMessage>,
stop: Vec<String>,
temperature: f32,
tools: Vec<ToolFunctionDefinition>,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<proto::LanguageModelResponseMessage>>>>
{
self.0.complete(model, messages, stop, temperature, tools)
}
}
impl Global for CompletionProvider {}
pub trait CompletionProviderBackend: 'static {
fn default_model(&self) -> String;
fn available_models(&self) -> Vec<String>;
fn complete(
&self,
model: String,
messages: Vec<CompletionMessage>,
stop: Vec<String>,
temperature: f32,
tools: Vec<ToolFunctionDefinition>,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<proto::LanguageModelResponseMessage>>>>;
}
pub struct CloudCompletionProvider {
client: Arc<Client>,
}
impl CloudCompletionProvider {
pub fn new(client: Arc<Client>) -> Self {
Self { client }
}
}
impl CompletionProviderBackend for CloudCompletionProvider {
fn default_model(&self) -> String {
"gpt-4-turbo".into()
}
fn available_models(&self) -> Vec<String> {
vec!["gpt-4-turbo".into(), "gpt-4".into(), "gpt-3.5-turbo".into()]
}
fn complete(
&self,
model: String,
messages: Vec<CompletionMessage>,
stop: Vec<String>,
temperature: f32,
tools: Vec<ToolFunctionDefinition>,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<proto::LanguageModelResponseMessage>>>>
{
let client = self.client.clone();
let tools: Vec<proto::ChatCompletionTool> = tools
.iter()
.filter_map(|tool| {
Some(proto::ChatCompletionTool {
variant: Some(proto::chat_completion_tool::Variant::Function(
proto::chat_completion_tool::FunctionObject {
name: tool.name.clone(),
description: Some(tool.description.clone()),
parameters: Some(serde_json::to_string(&tool.parameters).ok()?),
},
)),
})
})
.collect();
let tool_choice = match tools.is_empty() {
true => None,
false => Some("auto".into()),
};
async move {
let stream = client
.request_stream(proto::CompleteWithLanguageModel {
model,
messages: messages
.into_iter()
.map(|message| match message {
CompletionMessage::Assistant {
content,
tool_calls,
} => proto::LanguageModelRequestMessage {
role: proto::LanguageModelRole::LanguageModelAssistant as i32,
content: content.unwrap_or_default(),
tool_call_id: None,
tool_calls: tool_calls
.into_iter()
.map(|tool_call| match tool_call.content {
open_ai::ToolCallContent::Function { function } => {
proto::ToolCall {
id: tool_call.id,
variant: Some(proto::tool_call::Variant::Function(
proto::tool_call::FunctionCall {
name: function.name,
arguments: function.arguments,
},
)),
}
}
})
.collect(),
},
CompletionMessage::User { content } => {
proto::LanguageModelRequestMessage {
role: proto::LanguageModelRole::LanguageModelUser as i32,
content,
tool_call_id: None,
tool_calls: Vec::new(),
}
}
CompletionMessage::System { content } => {
proto::LanguageModelRequestMessage {
role: proto::LanguageModelRole::LanguageModelSystem as i32,
content,
tool_calls: Vec::new(),
tool_call_id: None,
}
}
CompletionMessage::Tool {
content,
tool_call_id,
} => proto::LanguageModelRequestMessage {
role: proto::LanguageModelRole::LanguageModelTool as i32,
content,
tool_call_id: Some(tool_call_id),
tool_calls: Vec::new(),
},
})
.collect(),
stop,
temperature,
tool_choice,
tools,
})
.await?;
Ok(stream
.filter_map(|response| async move {
match response {
Ok(mut response) => Some(Ok(response.choices.pop()?.delta?)),
Err(error) => Some(Err(error)),
}
})
.boxed())
}
.boxed()
}
}

View File

@@ -1,90 +0,0 @@
use std::cmp::Reverse;
use std::ffi::OsStr;
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::Result;
use assistant_tooling::{SavedToolFunctionCall, SavedUserAttachment};
use fs::Fs;
use futures::StreamExt;
use gpui::SharedString;
use regex::Regex;
use serde::{Deserialize, Serialize};
use util::paths::CONVERSATIONS_DIR;
use crate::MessageId;
#[derive(Serialize, Deserialize)]
pub struct SavedConversation {
/// The schema version of the conversation.
pub version: String,
/// The title of the conversation, generated by the Assistant.
pub title: String,
pub messages: Vec<SavedChatMessage>,
}
#[derive(Serialize, Deserialize)]
pub enum SavedChatMessage {
User {
id: MessageId,
body: String,
attachments: Vec<SavedUserAttachment>,
},
Assistant {
id: MessageId,
messages: Vec<SavedAssistantMessagePart>,
error: Option<SharedString>,
},
}
#[derive(Serialize, Deserialize)]
pub struct SavedAssistantMessagePart {
pub body: SharedString,
pub tool_calls: Vec<SavedToolFunctionCall>,
}
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::new();
while let Some(path) = paths.next().await {
let path = path?;
if path.extension() != Some(OsStr::new("json")) {
continue;
}
let pattern = r" - \d+.zed.\d.\d.\d.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 old 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,196 +0,0 @@
use std::sync::Arc;
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
use gpui::{AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, View, WeakView};
use picker::{Picker, PickerDelegate};
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
use util::ResultExt;
use crate::saved_conversation::SavedConversationMetadata;
pub struct SavedConversations {
focus_handle: FocusHandle,
picker: Option<View<Picker<SavedConversationPickerDelegate>>>,
}
impl EventEmitter<DismissEvent> for SavedConversations {}
impl FocusableView for SavedConversations {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
if let Some(picker) = self.picker.as_ref() {
picker.focus_handle(cx)
} else {
self.focus_handle.clone()
}
}
}
impl SavedConversations {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
Self {
focus_handle: cx.focus_handle(),
picker: None,
}
}
pub fn init(
&mut self,
saved_conversations: Vec<SavedConversationMetadata>,
cx: &mut ViewContext<Self>,
) {
let delegate =
SavedConversationPickerDelegate::new(cx.view().downgrade(), saved_conversations);
self.picker = Some(cx.new_view(|cx| Picker::uniform_list(delegate, cx).modal(false)));
}
}
impl Render for SavedConversations {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
v_flex()
.w_full()
.bg(cx.theme().colors().panel_background)
.children(self.picker.clone())
}
}
pub struct SavedConversationPickerDelegate {
view: WeakView<SavedConversations>,
saved_conversations: Vec<SavedConversationMetadata>,
selected_index: usize,
matches: Vec<StringMatch>,
}
impl SavedConversationPickerDelegate {
pub fn new(
weak_view: WeakView<SavedConversations>,
saved_conversations: Vec<SavedConversationMetadata>,
) -> Self {
let matches = saved_conversations
.iter()
.map(|conversation| StringMatch {
candidate_id: 0,
score: 0.0,
positions: Default::default(),
string: conversation.title.clone(),
})
.collect();
Self {
view: weak_view,
saved_conversations,
selected_index: 0,
matches,
}
}
}
impl PickerDelegate for SavedConversationPickerDelegate {
type ListItem = ui::ListItem;
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
"Select saved conversation...".into()
}
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
self.selected_index = ix;
}
fn update_matches(
&mut self,
query: String,
cx: &mut ViewContext<Picker<Self>>,
) -> gpui::Task<()> {
let background_executor = cx.background_executor().clone();
let candidates = self
.saved_conversations
.iter()
.enumerate()
.map(|(id, conversation)| {
let text = conversation.title.clone();
StringMatchCandidate {
id,
char_bag: text.as_str().into(),
string: text,
}
})
.collect::<Vec<_>>();
cx.spawn(move |this, mut cx| async move {
let matches = if query.is_empty() {
candidates
.into_iter()
.enumerate()
.map(|(index, candidate)| StringMatch {
candidate_id: index,
string: candidate.string,
positions: Vec::new(),
score: 0.0,
})
.collect()
} else {
match_strings(
&candidates,
&query,
false,
100,
&Default::default(),
background_executor,
)
.await
};
this.update(&mut cx, |this, _cx| {
this.delegate.matches = matches;
this.delegate.selected_index = this
.delegate
.selected_index
.min(this.delegate.matches.len().saturating_sub(1));
})
.log_err();
})
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
if self.matches.is_empty() {
self.dismissed(cx);
return;
}
// TODO: Implement selecting a saved conversation.
}
fn dismissed(&mut self, cx: &mut ui::prelude::ViewContext<Picker<Self>>) {
self.view
.update(cx, |_, cx| cx.emit(DismissEvent))
.log_err();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let conversation_match = &self.matches[ix];
let _conversation = &self.saved_conversations[conversation_match.candidate_id];
Some(
ListItem::new(ix)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.child(HighlightedLabel::new(
conversation_match.string.clone(),
conversation_match.positions.clone(),
)),
)
}
}

View File

@@ -1,7 +0,0 @@
mod annotate_code;
mod create_buffer;
mod project_index;
pub use annotate_code::*;
pub use create_buffer::*;
pub use project_index::*;

View File

@@ -1,304 +0,0 @@
use anyhow::Result;
use assistant_tooling::{LanguageModelTool, ProjectContext, ToolView};
use editor::{
display_map::{BlockContext, BlockDisposition, BlockProperties, BlockStyle},
Editor, MultiBuffer,
};
use futures::{channel::mpsc::UnboundedSender, StreamExt as _};
use gpui::{prelude::*, AnyElement, AsyncWindowContext, Model, Task, View, WeakView};
use language::ToPoint;
use project::{search::SearchQuery, Project, ProjectPath};
use schemars::JsonSchema;
use serde::Deserialize;
use std::path::Path;
use ui::prelude::*;
use util::ResultExt;
use workspace::Workspace;
pub struct AnnotationTool {
workspace: WeakView<Workspace>,
project: Model<Project>,
}
impl AnnotationTool {
pub fn new(workspace: WeakView<Workspace>, project: Model<Project>) -> Self {
Self { workspace, project }
}
}
#[derive(Default, Debug, Deserialize, JsonSchema, Clone)]
pub struct AnnotationInput {
/// Name for this set of annotations
#[serde(default = "default_title")]
title: String,
/// Excerpts from the file to show to the user.
excerpts: Vec<Excerpt>,
}
fn default_title() -> String {
"Untitled".to_string()
}
#[derive(Debug, Deserialize, JsonSchema, Clone)]
struct Excerpt {
/// Path to the file
path: String,
/// A short, distinctive string that appears in the file, used to define a location in the file.
text_passage: String,
/// Text to display above the code excerpt
annotation: String,
}
impl LanguageModelTool for AnnotationTool {
type View = AnnotationResultView;
fn name(&self) -> String {
"annotate_code".to_string()
}
fn description(&self) -> String {
"Dynamically annotate symbols in the current codebase. Opens a buffer in a panel in their editor, to the side of the conversation. The annotations are shown in the editor as a block decoration.".to_string()
}
fn view(&self, cx: &mut WindowContext) -> View<Self::View> {
cx.new_view(|cx| {
let (tx, mut rx) = futures::channel::mpsc::unbounded();
cx.spawn(|view, mut cx| async move {
while let Some(excerpt) = rx.next().await {
AnnotationResultView::add_excerpt(view.clone(), excerpt, &mut cx).await?;
}
anyhow::Ok(())
})
.detach();
AnnotationResultView {
project: self.project.clone(),
workspace: self.workspace.clone(),
tx,
pending_excerpt: None,
added_editor_to_workspace: false,
editor: None,
error: None,
rendered_excerpt_count: 0,
}
})
}
}
pub struct AnnotationResultView {
workspace: WeakView<Workspace>,
project: Model<Project>,
pending_excerpt: Option<Excerpt>,
added_editor_to_workspace: bool,
editor: Option<View<Editor>>,
tx: UnboundedSender<Excerpt>,
error: Option<anyhow::Error>,
rendered_excerpt_count: usize,
}
impl AnnotationResultView {
async fn add_excerpt(
this: WeakView<Self>,
excerpt: Excerpt,
cx: &mut AsyncWindowContext,
) -> Result<()> {
let project = this.update(cx, |this, _cx| this.project.clone())?;
let worktree_id = project.update(cx, |project, cx| {
let worktree = project.worktrees().next()?;
let worktree_id = worktree.read(cx).id();
Some(worktree_id)
})?;
let worktree_id = if let Some(worktree_id) = worktree_id {
worktree_id
} else {
return Err(anyhow::anyhow!("No worktree found"));
};
let buffer_task = project.update(cx, |project, cx| {
project.open_buffer(
ProjectPath {
worktree_id,
path: Path::new(&excerpt.path).into(),
},
cx,
)
})?;
let buffer = match buffer_task.await {
Ok(buffer) => buffer,
Err(error) => {
return this.update(cx, |this, cx| {
this.error = Some(error);
cx.notify();
})
}
};
let snapshot = buffer.update(cx, |buffer, _cx| buffer.snapshot())?;
let query = SearchQuery::text(&excerpt.text_passage, false, false, false, vec![], vec![])?;
let matches = query.search(&snapshot, None).await;
let Some(first_match) = matches.first() else {
log::warn!(
"text {:?} does not appear in '{}'",
excerpt.text_passage,
excerpt.path
);
return Ok(());
};
this.update(cx, |this, cx| {
let mut start = first_match.start.to_point(&snapshot);
start.column = 0;
if let Some(editor) = &this.editor {
editor.update(cx, |editor, cx| {
let ranges = editor.buffer().update(cx, |multibuffer, cx| {
multibuffer.push_excerpts_with_context_lines(
buffer.clone(),
vec![start..start],
5,
cx,
)
});
let annotation = SharedString::from(excerpt.annotation);
editor.insert_blocks(
[BlockProperties {
position: ranges[0].start,
height: annotation.split('\n').count() as u8 + 1,
style: BlockStyle::Fixed,
render: Box::new(move |cx| Self::render_note_block(&annotation, cx)),
disposition: BlockDisposition::Above,
}],
None,
cx,
);
});
if !this.added_editor_to_workspace {
this.added_editor_to_workspace = true;
this.workspace
.update(cx, |workspace, cx| {
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, cx);
})
.log_err();
}
}
})?;
Ok(())
}
fn render_note_block(explanation: &SharedString, cx: &mut BlockContext) -> AnyElement {
let anchor_x = cx.anchor_x;
let gutter_width = cx.gutter_dimensions.width;
h_flex()
.w_full()
.py_2()
.border_y_1()
.border_color(cx.theme().colors().border)
.child(
h_flex()
.justify_center()
.w(gutter_width)
.child(Icon::new(IconName::Ai).color(Color::Hint)),
)
.child(
h_flex()
.w_full()
.ml(anchor_x - gutter_width)
.child(explanation.clone()),
)
.into_any_element()
}
}
impl Render for AnnotationResultView {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
if let Some(error) = &self.error {
ui::Label::new(error.to_string()).into_any_element()
} else {
ui::Label::new(SharedString::from(format!(
"Opened a buffer with {} excerpts",
self.rendered_excerpt_count
)))
.into_any_element()
}
}
}
impl ToolView for AnnotationResultView {
type Input = AnnotationInput;
type SerializedState = Option<String>;
fn generate(&self, _: &mut ProjectContext, _: &mut ViewContext<Self>) -> String {
if let Some(error) = &self.error {
format!("Failed to create buffer: {error:?}")
} else {
format!(
"opened {} excerpts in a buffer",
self.rendered_excerpt_count
)
}
}
fn set_input(&mut self, mut input: Self::Input, cx: &mut ViewContext<Self>) {
let editor = if let Some(editor) = &self.editor {
editor.clone()
} else {
let multibuffer = cx.new_model(|_cx| {
MultiBuffer::new(0, language::Capability::ReadWrite).with_title(String::new())
});
let editor = cx.new_view(|cx| {
Editor::for_multibuffer(multibuffer.clone(), Some(self.project.clone()), cx)
});
self.editor = Some(editor.clone());
editor
};
editor.update(cx, |editor, cx| {
editor.buffer().update(cx, |multibuffer, cx| {
if multibuffer.title(cx) != input.title {
multibuffer.set_title(input.title.clone(), cx);
}
});
self.pending_excerpt = input.excerpts.pop();
for excerpt in input.excerpts.iter().skip(self.rendered_excerpt_count) {
self.tx.unbounded_send(excerpt.clone()).ok();
}
self.rendered_excerpt_count = input.excerpts.len();
});
cx.notify();
}
fn execute(&mut self, _cx: &mut ViewContext<Self>) -> Task<Result<()>> {
if let Some(excerpt) = self.pending_excerpt.take() {
self.rendered_excerpt_count += 1;
self.tx.unbounded_send(excerpt.clone()).ok();
}
self.tx.close_channel();
Task::ready(Ok(()))
}
fn serialize(&self, _cx: &mut ViewContext<Self>) -> Self::SerializedState {
self.error.as_ref().map(|error| error.to_string())
}
fn deserialize(
&mut self,
output: Self::SerializedState,
_cx: &mut ViewContext<Self>,
) -> Result<()> {
if let Some(error_message) = output {
self.error = Some(anyhow::anyhow!("{}", error_message));
}
Ok(())
}
}

View File

@@ -1,145 +0,0 @@
use anyhow::{anyhow, Result};
use assistant_tooling::{LanguageModelTool, ProjectContext, ToolView};
use editor::Editor;
use gpui::{prelude::*, Model, Task, View, WeakView};
use project::Project;
use schemars::JsonSchema;
use serde::Deserialize;
use ui::prelude::*;
use util::ResultExt;
use workspace::Workspace;
pub struct CreateBufferTool {
workspace: WeakView<Workspace>,
project: Model<Project>,
}
impl CreateBufferTool {
pub fn new(workspace: WeakView<Workspace>, project: Model<Project>) -> Self {
Self { workspace, project }
}
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct CreateBufferInput {
/// The contents of the buffer.
text: String,
/// The name of the language to use for the buffer.
///
/// This should be a human-readable name, like "Rust", "JavaScript", or "Python".
language: String,
}
impl LanguageModelTool for CreateBufferTool {
type View = CreateBufferView;
fn name(&self) -> String {
"create_file".to_string()
}
fn description(&self) -> String {
"Create a new untitled file in the current codebase. Side effect: opens it in a new pane/tab for the user to edit.".to_string()
}
fn view(&self, cx: &mut WindowContext) -> View<Self::View> {
cx.new_view(|_cx| CreateBufferView {
workspace: self.workspace.clone(),
project: self.project.clone(),
input: None,
error: None,
})
}
}
pub struct CreateBufferView {
workspace: WeakView<Workspace>,
project: Model<Project>,
input: Option<CreateBufferInput>,
error: Option<anyhow::Error>,
}
impl Render for CreateBufferView {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
ui::Label::new("Opening a buffer")
}
}
impl ToolView for CreateBufferView {
type Input = CreateBufferInput;
type SerializedState = ();
fn generate(&self, _project: &mut ProjectContext, _cx: &mut ViewContext<Self>) -> String {
let Some(input) = self.input.as_ref() else {
return "No input".to_string();
};
match &self.error {
None => format!("Created a new {} buffer", input.language),
Some(err) => format!("Failed to create buffer: {err:?}"),
}
}
fn set_input(&mut self, input: Self::Input, cx: &mut ViewContext<Self>) {
self.input = Some(input);
cx.notify();
}
fn execute(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
cx.spawn({
let workspace = self.workspace.clone();
let project = self.project.clone();
let input = self.input.clone();
|_this, mut cx| async move {
let input = input.ok_or_else(|| anyhow!("no input"))?;
let text = input.text.clone();
let language_name = input.language.clone();
let language = cx
.update(|cx| {
project
.read(cx)
.languages()
.language_for_name(&language_name)
})?
.await?;
let buffer = cx
.update(|cx| project.update(cx, |project, cx| project.create_buffer(cx)))?
.await?;
buffer.update(&mut cx, |buffer, cx| {
buffer.edit([(0..0, text)], None, cx);
buffer.set_language(Some(language), cx)
})?;
workspace
.update(&mut cx, |workspace, cx| {
workspace.add_item_to_active_pane(
Box::new(
cx.new_view(|cx| Editor::for_buffer(buffer, Some(project), cx)),
),
None,
cx,
);
})
.log_err();
Ok(())
}
})
}
fn serialize(&self, _cx: &mut ViewContext<Self>) -> Self::SerializedState {
()
}
fn deserialize(
&mut self,
_output: Self::SerializedState,
_cx: &mut ViewContext<Self>,
) -> Result<()> {
Ok(())
}
}

View File

@@ -1,428 +0,0 @@
use anyhow::Result;
use assistant_tooling::{LanguageModelTool, ToolView};
use collections::BTreeMap;
use file_icons::FileIcons;
use gpui::{prelude::*, AnyElement, Model, Task};
use project::ProjectPath;
use schemars::JsonSchema;
use semantic_index::{ProjectIndex, Status};
use serde::{Deserialize, Serialize};
use std::{
fmt::Write as _,
ops::Range,
path::{Path, PathBuf},
str::FromStr as _,
sync::Arc,
};
use ui::{prelude::*, CollapsibleContainer, Color, Icon, IconName, Label, WindowContext};
const DEFAULT_SEARCH_LIMIT: usize = 20;
pub struct ProjectIndexTool {
project_index: Model<ProjectIndex>,
}
#[derive(Default)]
enum ProjectIndexToolState {
#[default]
CollectingQuery,
Searching,
Error(anyhow::Error),
Finished {
excerpts: BTreeMap<ProjectPath, Vec<Range<usize>>>,
index_status: Status,
},
}
pub struct ProjectIndexView {
project_index: Model<ProjectIndex>,
input: CodebaseQuery,
expanded_header: bool,
state: ProjectIndexToolState,
}
#[derive(Default, Deserialize, JsonSchema)]
pub struct CodebaseQuery {
/// Semantic search query
query: String,
/// Criteria to include results
includes: Option<SearchFilter>,
/// Criteria to exclude results
excludes: Option<SearchFilter>,
}
#[derive(Deserialize, JsonSchema, Clone, Default)]
pub struct SearchFilter {
/// Filter by file path prefix
prefix_path: Option<String>,
/// Filter by file extension
extension: Option<String>,
// Note: we possibly can't do content filtering very easily given the project context handling
// the final results, so we're leaving out direct string matches for now
}
fn project_starts_with(prefix_path: Option<String>, project_path: ProjectPath) -> bool {
if let Some(path) = &prefix_path {
if let Some(path) = PathBuf::from_str(path).ok() {
return project_path.path.starts_with(path);
}
}
return false;
}
impl SearchFilter {
fn matches(&self, project_path: &ProjectPath) -> bool {
let path_match = project_starts_with(self.prefix_path.clone(), project_path.clone());
path_match
&& (if let Some(extension) = &self.extension {
project_path
.path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext == extension)
.unwrap_or(false)
} else {
true
})
}
}
#[derive(Serialize, Deserialize)]
pub struct SerializedState {
index_status: Status,
error_message: Option<String>,
worktrees: BTreeMap<Arc<Path>, WorktreeIndexOutput>,
}
#[derive(Default, Serialize, Deserialize)]
struct WorktreeIndexOutput {
excerpts: BTreeMap<Arc<Path>, Vec<Range<usize>>>,
}
impl ProjectIndexView {
fn toggle_header(&mut self, cx: &mut ViewContext<Self>) {
self.expanded_header = !self.expanded_header;
cx.notify();
}
fn render_filter_section(
&mut self,
heading: &str,
filter: Option<SearchFilter>,
cx: &mut ViewContext<Self>,
) -> Option<AnyElement> {
let filter = match filter {
Some(filter) => filter,
None => return None,
};
// Any of the filter fields can be empty. We'll show nothing if they're all empty.
let path = filter.prefix_path.as_ref().map(|path| {
let icon_path = FileIcons::get_icon(Path::new(path), cx)
.map(SharedString::from)
.unwrap_or_else(|| SharedString::from("icons/file_icons/file.svg"));
h_flex()
.gap_1()
.child("Paths: ")
.child(Icon::from_path(icon_path))
.child(ui::Label::new(path.clone()).color(Color::Muted))
});
let extension = filter.extension.as_ref().map(|extension| {
let icon_path = FileIcons::get_icon(Path::new(extension), cx)
.map(SharedString::from)
.unwrap_or_else(|| SharedString::from("icons/file_icons/file.svg"));
h_flex()
.gap_1()
.child("Extensions: ")
.child(Icon::from_path(icon_path))
.child(ui::Label::new(extension.clone()).color(Color::Muted))
});
if path.is_none() && extension.is_none() {
return None;
}
Some(
v_flex()
.child(ui::Label::new(heading.to_string()))
.gap_1()
.children(path)
.children(extension)
.into_any_element(),
)
}
}
impl Render for ProjectIndexView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let query = self.input.query.clone();
let (header_text, content) = match &self.state {
ProjectIndexToolState::Error(error) => {
return format!("failed to search: {error:?}").into_any_element()
}
ProjectIndexToolState::CollectingQuery | ProjectIndexToolState::Searching => {
("Searching...".to_string(), div())
}
ProjectIndexToolState::Finished { excerpts, .. } => {
let file_count = excerpts.len();
if excerpts.is_empty() {
("No results found".to_string(), div())
} else {
let header_text = format!(
"Read {} {}",
file_count,
if file_count == 1 { "file" } else { "files" }
);
let el = v_flex().gap_2().children(excerpts.keys().map(|path| {
h_flex().gap_2().child(Icon::new(IconName::File)).child(
Label::new(path.path.to_string_lossy().to_string()).color(Color::Muted),
)
}));
(header_text, el)
}
}
};
let header = h_flex()
.gap_2()
.child(Icon::new(IconName::File))
.child(header_text);
v_flex()
.gap_3()
.child(
CollapsibleContainer::new("collapsible-container", self.expanded_header)
.start_slot(header)
.on_click(cx.listener(move |this, _, cx| {
this.toggle_header(cx);
}))
.child(
v_flex()
.gap_3()
.p_3()
.child(
h_flex()
.gap_2()
.child(Icon::new(IconName::MagnifyingGlass))
.child(Label::new(format!("`{}`", query)).color(Color::Muted)),
)
.children(self.render_filter_section(
"Includes",
self.input.includes.clone(),
cx,
))
.children(self.render_filter_section(
"Excludes",
self.input.excludes.clone(),
cx,
))
.child(content),
),
)
.into_any_element()
}
}
impl ToolView for ProjectIndexView {
type Input = CodebaseQuery;
type SerializedState = SerializedState;
fn generate(
&self,
context: &mut assistant_tooling::ProjectContext,
_: &mut ViewContext<Self>,
) -> String {
match &self.state {
ProjectIndexToolState::CollectingQuery => String::new(),
ProjectIndexToolState::Searching => String::new(),
ProjectIndexToolState::Error(error) => format!("failed to search: {error:?}"),
ProjectIndexToolState::Finished {
excerpts,
index_status,
} => {
let mut body = "found results in the following paths:\n".to_string();
for (project_path, ranges) in excerpts {
context.add_excerpts(project_path.clone(), ranges);
writeln!(&mut body, "* {}", &project_path.path.display()).unwrap();
}
if *index_status != Status::Idle {
body.push_str("Still indexing. Results may be incomplete.\n");
}
body
}
}
}
fn set_input(&mut self, input: Self::Input, cx: &mut ViewContext<Self>) {
self.input = input;
cx.notify();
}
fn execute(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
self.state = ProjectIndexToolState::Searching;
cx.notify();
let project_index = self.project_index.read(cx);
let index_status = project_index.status();
// TODO: wire the filters into the search here instead of processing after.
// Otherwise we'll get zero results sometimes.
let search = project_index.search(self.input.query.clone(), DEFAULT_SEARCH_LIMIT, cx);
let includes = self.input.includes.clone();
let excludes = self.input.excludes.clone();
cx.spawn(|this, mut cx| async move {
let search_result = search.await;
this.update(&mut cx, |this, cx| {
match search_result {
Ok(search_results) => {
let mut excerpts = BTreeMap::<ProjectPath, Vec<Range<usize>>>::new();
for search_result in search_results {
let project_path = ProjectPath {
worktree_id: search_result.worktree.read(cx).id(),
path: search_result.path,
};
if let Some(includes) = &includes {
if !includes.matches(&project_path) {
continue;
}
} else if let Some(excludes) = &excludes {
if excludes.matches(&project_path) {
continue;
}
}
excerpts
.entry(project_path)
.or_default()
.push(search_result.range);
}
this.state = ProjectIndexToolState::Finished {
excerpts,
index_status,
};
}
Err(error) => {
this.state = ProjectIndexToolState::Error(error);
}
}
cx.notify();
})
})
}
fn serialize(&self, cx: &mut ViewContext<Self>) -> Self::SerializedState {
let mut serialized = SerializedState {
error_message: None,
index_status: Status::Idle,
worktrees: Default::default(),
};
match &self.state {
ProjectIndexToolState::Error(err) => serialized.error_message = Some(err.to_string()),
ProjectIndexToolState::Finished {
excerpts,
index_status,
} => {
serialized.index_status = *index_status;
if let Some(project) = self.project_index.read(cx).project().upgrade() {
let project = project.read(cx);
for (project_path, excerpts) in excerpts {
if let Some(worktree) =
project.worktree_for_id(project_path.worktree_id, cx)
{
let worktree_path = worktree.read(cx).abs_path();
serialized
.worktrees
.entry(worktree_path)
.or_default()
.excerpts
.insert(project_path.path.clone(), excerpts.clone());
}
}
}
}
_ => {}
}
serialized
}
fn deserialize(
&mut self,
serialized: Self::SerializedState,
cx: &mut ViewContext<Self>,
) -> Result<()> {
if !serialized.worktrees.is_empty() {
let mut excerpts = BTreeMap::<ProjectPath, Vec<Range<usize>>>::new();
if let Some(project) = self.project_index.read(cx).project().upgrade() {
let project = project.read(cx);
for (worktree_path, worktree_state) in serialized.worktrees {
if let Some(worktree) = project
.worktrees()
.find(|worktree| worktree.read(cx).abs_path() == worktree_path)
{
let worktree_id = worktree.read(cx).id();
for (path, serialized_excerpts) in worktree_state.excerpts {
excerpts.insert(ProjectPath { worktree_id, path }, serialized_excerpts);
}
}
}
}
self.state = ProjectIndexToolState::Finished {
excerpts,
index_status: serialized.index_status,
};
}
cx.notify();
Ok(())
}
}
impl ProjectIndexTool {
pub fn new(project_index: Model<ProjectIndex>) -> Self {
Self { project_index }
}
}
impl LanguageModelTool for ProjectIndexTool {
type View = ProjectIndexView;
fn name(&self) -> String {
"semantic_search_codebase".to_string()
}
fn description(&self) -> String {
unindent::unindent(
r#"This search tool uses a semantic index to perform search queries across your codebase, identifying and returning excerpts of text and code possibly related to the query.
Ideal for:
- Discovering implementations of similar logic within the project
- Finding usage examples of functions, classes/structures, libraries, and other code elements
- Developing understanding of the codebase's architecture and design
Note: The search's effectiveness is directly related to the current state of the codebase and the specificity of your query. It is recommended that you use snippets of code that are similar to the code you wish to find."#,
)
}
fn view(&self, cx: &mut WindowContext) -> gpui::View<Self::View> {
cx.new_view(|_| ProjectIndexView {
state: ProjectIndexToolState::CollectingQuery,
input: Default::default(),
expanded_header: false,
project_index: self.project_index.clone(),
})
}
}

View File

@@ -1,17 +0,0 @@
mod active_file_button;
mod chat_message;
mod chat_notice;
mod composer;
mod project_index_button;
#[cfg(feature = "stories")]
mod stories;
pub use active_file_button::*;
pub use chat_message::*;
pub use chat_notice::*;
pub use composer::*;
pub use project_index_button::*;
#[cfg(feature = "stories")]
pub use stories::*;

View File

@@ -1,134 +0,0 @@
use crate::attachments::ActiveEditorAttachmentTool;
use assistant_tooling::AttachmentRegistry;
use editor::Editor;
use gpui::{prelude::*, Subscription, View};
use std::sync::Arc;
use ui::{prelude::*, ButtonLike, Color, Icon, IconName, Tooltip};
use workspace::Workspace;
#[derive(Clone)]
enum Status {
ActiveFile(String),
#[allow(dead_code)]
NoFile,
}
pub struct ActiveFileButton {
attachment_registry: Arc<AttachmentRegistry>,
status: Status,
#[allow(dead_code)]
workspace_subscription: Subscription,
}
impl ActiveFileButton {
pub fn new(
attachment_registry: Arc<AttachmentRegistry>,
workspace: View<Workspace>,
cx: &mut ViewContext<Self>,
) -> Self {
let workspace_subscription = cx.subscribe(&workspace, Self::handle_workspace_event);
cx.defer(move |this, cx| this.update_active_buffer(workspace.clone(), cx));
Self {
attachment_registry,
status: Status::NoFile,
workspace_subscription,
}
}
pub fn set_enabled(&mut self, enabled: bool) {
self.attachment_registry
.set_attachment_tool_enabled::<ActiveEditorAttachmentTool>(enabled);
}
pub fn update_active_buffer(&mut self, workspace: View<Workspace>, cx: &mut ViewContext<Self>) {
let active_buffer = workspace
.read(cx)
.active_item(cx)
.and_then(|item| Some(item.act_as::<Editor>(cx)?.read(cx).buffer().clone()));
if let Some(buffer) = active_buffer {
let buffer = buffer.read(cx);
if let Some(singleton) = buffer.as_singleton() {
let singleton = singleton.read(cx);
let filename: String = singleton
.file()
.map(|file| file.path().to_string_lossy())
.unwrap_or("Untitled".into())
.into();
self.status = Status::ActiveFile(filename);
}
}
}
fn handle_workspace_event(
&mut self,
workspace: View<Workspace>,
event: &workspace::Event,
cx: &mut ViewContext<Self>,
) {
if let workspace::Event::ActiveItemChanged = event {
self.update_active_buffer(workspace, cx);
}
}
}
impl Render for ActiveFileButton {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let is_enabled = self
.attachment_registry
.is_attachment_tool_enabled::<ActiveEditorAttachmentTool>();
let icon = if is_enabled {
Icon::new(IconName::File)
.size(IconSize::XSmall)
.color(Color::Default)
} else {
Icon::new(IconName::File)
.size(IconSize::XSmall)
.color(Color::Disabled)
};
let indicator = None;
let status = self.status.clone();
ButtonLike::new("active-file-button")
.child(
ui::IconWithIndicator::new(icon, indicator)
.indicator_border_color(Some(gpui::transparent_black())),
)
.tooltip({
move |cx| {
let status = status.clone();
let (tooltip, meta) = match (is_enabled, status) {
(false, _) => (
"Active file disabled".to_string(),
Some("Click to enable".to_string()),
),
(true, Status::ActiveFile(filename)) => (
format!("Active file {filename} enabled"),
Some("Click to disable".to_string()),
),
(true, Status::NoFile) => {
("No file active for conversation".to_string(), None)
}
};
if let Some(meta) = meta {
Tooltip::with_meta(tooltip, None, meta, cx)
} else {
Tooltip::text(tooltip, cx)
}
}
})
.on_click(cx.listener(move |this, _, cx| {
this.set_enabled(!is_enabled);
cx.notify();
}))
}
}

View File

@@ -1,140 +0,0 @@
use std::sync::Arc;
use client::User;
use gpui::{hsla, AnyElement, ClickEvent};
use ui::{prelude::*, Avatar, Tooltip};
use crate::MessageId;
pub enum UserOrAssistant {
User(Option<Arc<User>>),
Assistant,
}
#[derive(IntoElement)]
pub struct ChatMessage {
id: MessageId,
player: UserOrAssistant,
messages: Vec<AnyElement>,
selected: bool,
collapsed: bool,
on_collapse_handle_click: Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>,
}
impl ChatMessage {
pub fn new(
id: MessageId,
player: UserOrAssistant,
messages: Vec<AnyElement>,
collapsed: bool,
on_collapse_handle_click: Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>,
) -> Self {
Self {
id,
player,
messages,
selected: false,
collapsed,
on_collapse_handle_click,
}
}
}
impl Selectable for ChatMessage {
fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
}
impl RenderOnce for ChatMessage {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let message_group = SharedString::from(format!("{}_group", self.id.0));
let collapse_handle_id = SharedString::from(format!("{}_collapse_handle", self.id.0));
let content_padding = Spacing::Small.rems(cx);
// Clamp the message height to exactly 1.5 lines when collapsed.
let collapsed_height = content_padding.to_pixels(cx.rem_size()) + cx.line_height() * 1.5;
let background_color = if let UserOrAssistant::User(_) = &self.player {
Some(cx.theme().colors().surface_background)
} else {
None
};
let (username, avatar_uri) = match self.player {
UserOrAssistant::Assistant => (
"Assistant".into(),
Some("https://zed.dev/assistant_avatar.png".into()),
),
UserOrAssistant::User(Some(user)) => {
(user.github_login.clone(), Some(user.avatar_uri.clone()))
}
UserOrAssistant::User(None) => ("You".into(), None),
};
v_flex()
.group(message_group.clone())
.gap(Spacing::XSmall.rems(cx))
.p(Spacing::XSmall.rems(cx))
.when(self.selected, |element| {
element.bg(hsla(0.6, 0.67, 0.46, 0.12))
})
.rounded_lg()
.child(
h_flex()
.justify_between()
.px(content_padding)
.child(
h_flex()
.gap_2()
.map(|this| {
let avatar_size = rems_from_px(20.);
if let Some(avatar_uri) = avatar_uri {
this.child(Avatar::new(avatar_uri).size(avatar_size))
} else {
this.child(div().size(avatar_size))
}
})
.child(Label::new(username).color(Color::Muted)),
)
.child(
h_flex().visible_on_hover(message_group).child(
// temp icons
IconButton::new(
collapse_handle_id.clone(),
if self.collapsed {
IconName::ArrowUp
} else {
IconName::ArrowDown
},
)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.on_click(self.on_collapse_handle_click)
.tooltip(|cx| Tooltip::text("Collapse Message", cx)),
),
),
)
.when(self.messages.len() > 0, |el| {
el.child(
h_flex().w_full().child(
v_flex()
.relative()
.overflow_hidden()
.w_full()
.p(content_padding)
.gap_3()
.text_ui(cx)
.rounded_lg()
.when_some(background_color, |this, background_color| {
this.bg(background_color)
})
.when(self.collapsed, |this| this.h(collapsed_height))
.children(self.messages),
),
)
})
}
}

View File

@@ -1,71 +0,0 @@
use ui::{prelude::*, Avatar, IconButtonShape};
#[derive(IntoElement)]
pub struct ChatNotice {
message: SharedString,
meta: Option<SharedString>,
}
impl ChatNotice {
pub fn new(message: impl Into<SharedString>) -> Self {
Self {
message: message.into(),
meta: None,
}
}
pub fn meta(mut self, meta: impl Into<SharedString>) -> Self {
self.meta = Some(meta.into());
self
}
}
impl RenderOnce for ChatNotice {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
h_flex()
.w_full()
.items_start()
.mt_4()
.gap_3()
.child(
// TODO: Replace with question mark.
Avatar::new("https://zed.dev/assistant_avatar.png").size(rems_from_px(20.)),
)
.child(
v_flex()
.size_full()
.gap_1()
.pr_4()
.overflow_hidden()
.child(
h_flex()
.justify_between()
.overflow_hidden()
.child(
h_flex()
.flex_none()
.overflow_hidden()
.child(Label::new(self.message)),
)
.child(
h_flex()
.flex_shrink_0()
.gap_1()
.child(Button::new("allow", "Allow"))
.child(
IconButton::new("deny", IconName::Close)
.shape(IconButtonShape::Square)
.icon_color(Color::Muted)
.size(ButtonSize::None)
.icon_size(IconSize::XSmall),
),
),
)
.children(
self.meta.map(|meta| {
Label::new(meta).size(LabelSize::Small).color(Color::Muted)
}),
),
)
}
}

View File

@@ -1,193 +0,0 @@
use crate::{
ui::{ActiveFileButton, ProjectIndexButton},
AssistantChat, CompletionProvider,
};
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{AnyElement, FontStyle, FontWeight, ReadGlobal, TextStyle, View, WeakView, WhiteSpace};
use settings::Settings;
use theme::ThemeSettings;
use ui::{popover_menu, prelude::*, ButtonLike, ContextMenu, Divider, TextSize, Tooltip};
#[derive(IntoElement)]
pub struct Composer {
editor: View<Editor>,
project_index_button: View<ProjectIndexButton>,
active_file_button: Option<View<ActiveFileButton>>,
model_selector: AnyElement,
}
impl Composer {
pub fn new(
editor: View<Editor>,
project_index_button: View<ProjectIndexButton>,
active_file_button: Option<View<ActiveFileButton>>,
model_selector: AnyElement,
) -> Self {
Self {
editor,
project_index_button,
active_file_button,
model_selector,
}
}
fn render_tools(&mut self, _cx: &mut WindowContext) -> impl IntoElement {
h_flex().child(self.project_index_button.clone())
}
fn render_attachment_tools(&mut self, _cx: &mut WindowContext) -> impl IntoElement {
h_flex().children(
self.active_file_button
.clone()
.map(|view| view.into_any_element()),
)
}
}
impl RenderOnce for Composer {
fn render(mut self, cx: &mut WindowContext) -> impl IntoElement {
let font_size = TextSize::Default.rems(cx);
let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
let mut editor_border = cx.theme().colors().text;
editor_border.fade_out(0.90);
// Remove the extra 1px added by the border
let padding = Spacing::XLarge.rems(cx) - rems_from_px(1.);
h_flex()
.p(Spacing::Small.rems(cx))
.w_full()
.items_start()
.child(
v_flex()
.w_full()
.rounded_lg()
.p(padding)
.border_1()
.border_color(editor_border)
.bg(cx.theme().colors().editor_background)
.child(
v_flex()
.justify_between()
.w_full()
.gap_2()
.child({
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: cx.theme().colors().editor_foreground,
font_family: settings.buffer_font.family.clone(),
font_features: settings.buffer_font.features.clone(),
font_size: font_size.into(),
font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal,
line_height: line_height.into(),
background_color: None,
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
};
EditorElement::new(
&self.editor,
EditorStyle {
background: cx.theme().colors().editor_background,
local_player: cx.theme().players().local(),
text: text_style,
..Default::default()
},
)
})
.child(
h_flex()
.flex_none()
.gap_2()
.justify_between()
.w_full()
.child(
h_flex().gap_1().child(
h_flex()
.gap_2()
.child(self.render_tools(cx))
.child(Divider::vertical())
.child(self.render_attachment_tools(cx)),
),
)
.child(h_flex().gap_1().child(self.model_selector)),
),
),
)
}
}
#[derive(IntoElement)]
pub struct ModelSelector {
assistant_chat: WeakView<AssistantChat>,
model: String,
}
impl ModelSelector {
pub fn new(assistant_chat: WeakView<AssistantChat>, model: String) -> Self {
Self {
assistant_chat,
model,
}
}
}
impl RenderOnce for ModelSelector {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
popover_menu("model-switcher")
.menu(move |cx| {
ContextMenu::build(cx, |mut menu, cx| {
for model in CompletionProvider::global(cx).available_models() {
menu = menu.custom_entry(
{
let model = model.clone();
move |_| Label::new(model.clone()).into_any_element()
},
{
let assistant_chat = self.assistant_chat.clone();
move |cx| {
_ = assistant_chat.update(cx, |assistant_chat, cx| {
assistant_chat.model.clone_from(&model);
cx.notify();
});
}
},
);
}
menu
})
.into()
})
.trigger(
ButtonLike::new("active-model")
.child(
h_flex()
.w_full()
.gap_0p5()
.child(
div()
.overflow_x_hidden()
.flex_grow()
.whitespace_nowrap()
.child(
Label::new(self.model)
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.child(
div().child(
Icon::new(IconName::ChevronDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
),
)
.style(ButtonStyle::Subtle)
.tooltip(move |cx| Tooltip::text("Change Model", cx)),
)
.anchor(gpui::AnchorCorner::BottomRight)
}
}

View File

@@ -1,112 +0,0 @@
use assistant_tooling::ToolRegistry;
use gpui::{percentage, prelude::*, Animation, AnimationExt, Model, Transformation};
use semantic_index::{ProjectIndex, Status};
use std::{sync::Arc, time::Duration};
use ui::{prelude::*, ButtonLike, Color, Icon, IconName, Indicator, Tooltip};
use crate::tools::ProjectIndexTool;
pub struct ProjectIndexButton {
project_index: Model<ProjectIndex>,
tool_registry: Arc<ToolRegistry>,
}
impl ProjectIndexButton {
pub fn new(
project_index: Model<ProjectIndex>,
tool_registry: Arc<ToolRegistry>,
cx: &mut ViewContext<Self>,
) -> Self {
cx.subscribe(&project_index, |_this, _, _status: &Status, cx| {
cx.notify();
})
.detach();
Self {
project_index,
tool_registry,
}
}
pub fn set_enabled(&mut self, enabled: bool) {
self.tool_registry
.set_tool_enabled::<ProjectIndexTool>(enabled);
}
}
impl Render for ProjectIndexButton {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let status = self.project_index.read(cx).status();
let is_enabled = self.tool_registry.is_tool_enabled::<ProjectIndexTool>();
let icon = if is_enabled {
match status {
Status::Idle => Icon::new(IconName::Code)
.size(IconSize::XSmall)
.color(Color::Default),
Status::Loading => Icon::new(IconName::Code)
.size(IconSize::XSmall)
.color(Color::Muted),
Status::Scanning { .. } => Icon::new(IconName::Code)
.size(IconSize::XSmall)
.color(Color::Muted),
}
} else {
Icon::new(IconName::Code)
.size(IconSize::XSmall)
.color(Color::Disabled)
};
let indicator = if is_enabled {
match status {
Status::Idle => Some(Indicator::dot().color(Color::Success)),
Status::Scanning { .. } => Some(Indicator::dot().color(Color::Warning)),
Status::Loading => Some(Indicator::icon(
Icon::new(IconName::Spinner)
.color(Color::Accent)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
),
)),
}
} else {
None
};
ButtonLike::new("project-index")
.child(
ui::IconWithIndicator::new(icon, indicator)
.indicator_border_color(Some(gpui::transparent_black())),
)
.tooltip({
move |cx| {
let (tooltip, meta) = match (is_enabled, status) {
(false, _) => (
"Project index disabled".to_string(),
Some("Click to enable".to_string()),
),
(_, Status::Idle) => (
"Project index ready".to_string(),
Some("Click to disable".to_string()),
),
(_, Status::Loading) => ("Project index loading...".to_string(), None),
(_, Status::Scanning { remaining_count }) => (
"Project index scanning...".to_string(),
Some(format!("{} remaining...", remaining_count)),
),
};
if let Some(meta) = meta {
Tooltip::with_meta(tooltip, None, meta, cx)
} else {
Tooltip::text(tooltip, cx)
}
}
})
.on_click(cx.listener(move |this, _, cx| {
this.set_enabled(!is_enabled);
cx.notify();
}))
}
}

View File

@@ -1,5 +0,0 @@
mod chat_message;
mod chat_notice;
pub use chat_message::*;
pub use chat_notice::*;

View File

@@ -1,101 +0,0 @@
use std::sync::Arc;
use client::User;
use story::{StoryContainer, StoryItem, StorySection};
use ui::prelude::*;
use crate::ui::{ChatMessage, UserOrAssistant};
use crate::MessageId;
pub struct ChatMessageStory;
impl Render for ChatMessageStory {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
let user_1 = Arc::new(User {
id: 12345,
github_login: "iamnbutler".into(),
avatar_uri: "https://avatars.githubusercontent.com/u/1714999?v=4".into(),
});
StoryContainer::new(
"ChatMessage Story",
"crates/assistant2/src/ui/stories/chat_message.rs",
)
.child(
StorySection::new()
.child(StoryItem::new(
"User chat message",
ChatMessage::new(
MessageId(0),
UserOrAssistant::User(Some(user_1.clone())),
vec![div().child("What can I do here?").into_any_element()],
false,
Box::new(|_, _| {}),
),
))
.child(StoryItem::new(
"User chat message (collapsed)",
ChatMessage::new(
MessageId(0),
UserOrAssistant::User(Some(user_1.clone())),
vec![div().child("What can I do here?").into_any_element()],
true,
Box::new(|_, _| {}),
),
)),
)
.child(
StorySection::new()
.child(StoryItem::new(
"Assistant chat message",
ChatMessage::new(
MessageId(0),
UserOrAssistant::Assistant,
vec![div().child("You can talk to me!").into_any_element()],
false,
Box::new(|_, _| {}),
),
))
.child(StoryItem::new(
"Assistant chat message (collapsed)",
ChatMessage::new(
MessageId(0),
UserOrAssistant::Assistant,
vec![div().child(MULTI_LINE_MESSAGE).into_any_element()],
true,
Box::new(|_, _| {}),
),
)),
)
.child(
StorySection::new().child(StoryItem::new(
"Conversation between user and assistant",
v_flex()
.gap_2()
.child(ChatMessage::new(
MessageId(0),
UserOrAssistant::User(Some(user_1.clone())),
vec![div().child("What is Rust??").into_any_element()],
false,
Box::new(|_, _| {}),
))
.child(ChatMessage::new(
MessageId(0),
UserOrAssistant::Assistant,
vec![div().child("Rust is a multi-paradigm programming language focused on performance and safety").into_any_element()],
false,
Box::new(|_, _| {}),
))
.child(ChatMessage::new(
MessageId(0),
UserOrAssistant::User(Some(user_1)),
vec![div().child("Sounds pretty cool!").into_any_element()],
false,
Box::new(|_, _| {}),
)),
)),
)
}
}
const MULTI_LINE_MESSAGE: &str = "In 2010, the movies nominated for the 82nd Academy Awards, for films released in 2009, were as follows. Note that 2010 nominees were announced for the ceremony happening in that year, but they honor movies from the previous year";

View File

@@ -1,22 +0,0 @@
use story::{StoryContainer, StoryItem, StorySection};
use ui::prelude::*;
use crate::ui::ChatNotice;
pub struct ChatNoticeStory;
impl Render for ChatNoticeStory {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
StoryContainer::new(
"ChatNotice Story",
"crates/assistant2/src/ui/stories/chat_notice.rs",
)
.child(
StorySection::new().child(StoryItem::new(
"Project index request",
ChatNotice::new("Allow assistant to index your project?")
.meta("Enabling will allow responses more relevant to this project."),
)),
)
}
}

View File

@@ -0,0 +1,21 @@
[package]
name = "assistant_slash_command"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/assistant_slash_command.rs"
[dependencies]
anyhow.workspace = true
collections.workspace = true
derive_more.workspace = true
gpui.workspace = true
language.workspace = true
parking_lot.workspace = true
workspace.workspace = true

View File

@@ -0,0 +1,61 @@
mod slash_command_registry;
use anyhow::Result;
use gpui::{AnyElement, AppContext, ElementId, Task, WeakView, WindowContext};
use language::{CodeLabel, LspAdapterDelegate};
pub use slash_command_registry::*;
use std::{
ops::Range,
sync::{atomic::AtomicBool, Arc},
};
use workspace::Workspace;
pub fn init(cx: &mut AppContext) {
SlashCommandRegistry::default_global(cx);
}
pub trait SlashCommand: 'static + Send + Sync {
fn name(&self) -> String;
fn label(&self, _cx: &AppContext) -> CodeLabel {
CodeLabel::plain(self.name(), None)
}
fn description(&self) -> String;
fn tooltip_text(&self) -> String;
fn complete_argument(
&self,
query: String,
cancel: Arc<AtomicBool>,
workspace: WeakView<Workspace>,
cx: &mut AppContext,
) -> Task<Result<Vec<String>>>;
fn requires_argument(&self) -> bool;
fn run(
self: Arc<Self>,
argument: Option<&str>,
workspace: WeakView<Workspace>,
// TODO: We're just using the `LspAdapterDelegate` here because that is
// what the extension API is already expecting.
//
// It may be that `LspAdapterDelegate` needs a more general name, or
// perhaps another kind of delegate is needed here.
delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>>;
}
pub type RenderFoldPlaceholder = Arc<
dyn Send
+ Sync
+ Fn(ElementId, Arc<dyn Fn(&mut WindowContext)>, &mut WindowContext) -> AnyElement,
>;
pub struct SlashCommandOutput {
pub text: String,
pub sections: Vec<SlashCommandOutputSection<usize>>,
}
#[derive(Clone)]
pub struct SlashCommandOutputSection<T> {
pub range: Range<T>,
pub render_placeholder: RenderFoldPlaceholder,
}

View File

@@ -0,0 +1,64 @@
use std::sync::Arc;
use collections::HashMap;
use derive_more::{Deref, DerefMut};
use gpui::Global;
use gpui::{AppContext, ReadGlobal};
use parking_lot::RwLock;
use crate::SlashCommand;
#[derive(Default, Deref, DerefMut)]
struct GlobalSlashCommandRegistry(Arc<SlashCommandRegistry>);
impl Global for GlobalSlashCommandRegistry {}
#[derive(Default)]
struct SlashCommandRegistryState {
commands: HashMap<Arc<str>, Arc<dyn SlashCommand>>,
}
#[derive(Default)]
pub struct SlashCommandRegistry {
state: RwLock<SlashCommandRegistryState>,
}
impl SlashCommandRegistry {
/// Returns the global [`SlashCommandRegistry`].
pub fn global(cx: &AppContext) -> Arc<Self> {
GlobalSlashCommandRegistry::global(cx).0.clone()
}
/// Returns the global [`SlashCommandRegistry`].
///
/// Inserts a default [`SlashCommandRegistry`] if one does not yet exist.
pub fn default_global(cx: &mut AppContext) -> Arc<Self> {
cx.default_global::<GlobalSlashCommandRegistry>().0.clone()
}
pub fn new() -> Arc<Self> {
Arc::new(Self {
state: RwLock::new(SlashCommandRegistryState {
commands: HashMap::default(),
}),
})
}
/// Registers the provided [`SlashCommand`].
pub fn register_command(&self, command: impl SlashCommand) {
self.state
.write()
.commands
.insert(command.name().into(), Arc::new(command));
}
/// Returns the names of registered [`SlashCommand`]s.
pub fn command_names(&self) -> Vec<Arc<str>> {
self.state.read().commands.keys().cloned().collect()
}
/// Returns the [`SlashCommand`] with the given name.
pub fn command(&self, name: &str) -> Option<Arc<dyn SlashCommand>> {
self.state.read().commands.get(name).cloned()
}
}

View File

@@ -41,7 +41,12 @@ impl SoundRegistry {
}
let path = format!("sounds/{}.wav", name);
let bytes = self.assets.load(&path)?.into_owned();
let bytes = self
.assets
.load(&path)?
.map(|asset| Ok(asset))
.unwrap_or_else(|| Err(anyhow::anyhow!("No such asset available")))?
.into_owned();
let cursor = Cursor::new(bytes);
let source = Decoder::new(cursor)?.convert_samples::<f32>().buffered();

View File

@@ -237,8 +237,9 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Wo
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let tab_description = SharedString::from(body.title.to_string());
let editor = cx
.new_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx));
let editor = cx.new_view(|cx| {
Editor::for_multibuffer(buffer, Some(project), true, cx)
});
let workspace_handle = workspace.weak_handle();
let view: View<MarkdownPreviewView> = MarkdownPreviewView::new(
MarkdownPreviewMode::Default,
@@ -341,6 +342,16 @@ 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)
})?;

View File

@@ -60,6 +60,13 @@ fn parse_path_with_position(
}
fn main() -> Result<()> {
// Exit flatpak sandbox if needed
#[cfg(target_os = "linux")]
{
flatpak::try_restart_to_host();
flatpak::ld_extra_libs();
}
// Intercept version designators
#[cfg(target_os = "macos")]
if let Some(channel) = std::env::args().nth(1).filter(|arg| arg.starts_with("--")) {
@@ -72,6 +79,9 @@ fn main() -> Result<()> {
}
let args = Args::parse();
#[cfg(target_os = "linux")]
let args = flatpak::set_bin_if_no_escape(args);
let app = Detect::detect(args.zed.as_deref()).context("Bundle detection")?;
if args.version {
@@ -275,6 +285,114 @@ mod linux {
}
}
#[cfg(target_os = "linux")]
mod flatpak {
use std::ffi::OsString;
use std::path::PathBuf;
use std::process::Command;
use std::{env, process};
const EXTRA_LIB_ENV_NAME: &'static str = "ZED_FLATPAK_LIB_PATH";
const NO_ESCAPE_ENV_NAME: &'static str = "ZED_FLATPAK_NO_ESCAPE";
/// Adds bundled libraries to LD_LIBRARY_PATH if running under flatpak
pub fn ld_extra_libs() {
let mut paths = if let Ok(paths) = env::var("LD_LIBRARY_PATH") {
env::split_paths(&paths).collect()
} else {
Vec::new()
};
if let Ok(extra_path) = env::var(EXTRA_LIB_ENV_NAME) {
paths.push(extra_path.into());
}
env::set_var("LD_LIBRARY_PATH", env::join_paths(paths).unwrap());
}
/// Restarts outside of the sandbox if currently running within it
pub fn try_restart_to_host() {
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(
format!(
"--env={EXTRA_LIB_ENV_NAME}={}",
flatpak_dir.join("lib").to_str().unwrap()
)
.into(),
);
args.push(flatpak_dir.join("bin").join("zed").into());
let mut is_app_location_set = false;
for arg in &env::args_os().collect::<Vec<_>>()[1..] {
args.push(arg.clone());
is_app_location_set |= arg == "--zed";
}
if !is_app_location_set {
args.push("--zed".into());
args.push(flatpak_dir.join("bin").join("zed-app").into());
}
let error = exec::execvp("/usr/bin/flatpak-spawn", args);
eprintln!("failed restart cli on host: {:?}", error);
process::exit(1);
}
}
pub fn set_bin_if_no_escape(mut args: super::Args) -> super::Args {
if env::var(NO_ESCAPE_ENV_NAME).is_ok()
&& 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
}
fn get_flatpak_dir() -> Option<PathBuf> {
if env::var(NO_ESCAPE_ENV_NAME).is_ok() {
return None;
}
if let Ok(flatpak_id) = env::var("FLATPAK_ID") {
if !flatpak_id.starts_with("dev.zed.Zed") {
return None;
}
let install_dir = Command::new("/usr/bin/flatpak-spawn")
.arg("--host")
.arg("flatpak")
.arg("info")
.arg("--show-location")
.arg(flatpak_id)
.output()
.unwrap();
let install_dir = PathBuf::from(String::from_utf8(install_dir.stdout).unwrap().trim());
Some(install_dir.join("files"))
} else {
None
}
}
fn get_xdg_env_args() -> Vec<OsString> {
let xdg_keys = [
"XDG_DATA_HOME",
"XDG_CONFIG_HOME",
"XDG_CACHE_HOME",
"XDG_STATE_HOME",
];
env::vars()
.filter(|(key, _)| xdg_keys.contains(&key.as_str()))
.map(|(key, val)| format!("--env=FLATPAK_{}={}", key, val).into())
.collect()
}
}
// todo("windows")
#[cfg(target_os = "windows")]
mod windows {

View File

@@ -84,7 +84,8 @@ lazy_static! {
std::env::var("ZED_ALWAYS_ACTIVE").map_or(false, |e| !e.is_empty());
}
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100);
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(500);
pub const MAX_RECONNECTION_DELAY: Duration = Duration::from_secs(10);
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(20);
actions!(client, [SignIn, SignOut, Reconnect]);
@@ -287,7 +288,6 @@ struct ClientState {
status: (watch::Sender<Status>, watch::Receiver<Status>),
entity_id_extractors: HashMap<TypeId, fn(&dyn AnyTypedEnvelope) -> u64>,
_reconnect_task: Option<Task<()>>,
reconnect_interval: Duration,
entities_by_type_and_remote_id: HashMap<(TypeId, u64), WeakSubscriber>,
models_by_message_type: HashMap<TypeId, AnyWeakModel>,
entity_types_by_message_type: HashMap<TypeId, TypeId>,
@@ -363,7 +363,6 @@ impl Default for ClientState {
status: watch::channel_with(Status::SignedOut),
entity_id_extractors: Default::default(),
_reconnect_task: None,
reconnect_interval: Duration::from_secs(5),
models_by_message_type: Default::default(),
entities_by_type_and_remote_id: Default::default(),
entity_types_by_message_type: Default::default(),
@@ -623,7 +622,6 @@ impl Client {
}
Status::ConnectionLost => {
let this = self.clone();
let reconnect_interval = state.reconnect_interval;
state._reconnect_task = Some(cx.spawn(move |cx| async move {
#[cfg(any(test, feature = "test-support"))]
let mut rng = StdRng::seed_from_u64(0);
@@ -642,8 +640,9 @@ impl Client {
);
cx.background_executor().timer(delay).await;
delay = delay
.mul_f32(rng.gen_range(1.0..=2.0))
.min(reconnect_interval);
.mul_f32(rng.gen_range(0.5..=2.5))
.max(INITIAL_RECONNECTION_DELAY)
.min(MAX_RECONNECTION_DELAY);
} else {
break;
}

View File

@@ -192,10 +192,13 @@ impl UserStore {
cx.update(|cx| {
if let Some(info) = info {
cx.update_flags(info.staff, info.flags);
let disable_staff = std::env::var("ZED_DISABLE_STAFF")
.map_or(false, |v| v != "" && v != "0");
let staff = info.staff && !disable_staff;
cx.update_flags(staff, info.flags);
client.telemetry.set_authenticated_user_info(
Some(info.metrics_id.clone()),
info.staff,
staff,
)
}
})?;

View File

@@ -172,6 +172,8 @@ spec:
secretKeyRef:
name: slack
key: panics_webhook
- name: COMPLETE_WITH_LANGUAGE_MODEL_RATE_LIMIT_PER_HOUR
value: "1000"
- name: SUPERMAVEN_ADMIN_API_KEY
valueFrom:
secretKeyRef:

View File

@@ -713,7 +713,7 @@ impl Database {
.find_also_related(user::Entity)
.filter(channel_member::Column::ChannelId.eq(channel.root_id()));
if cfg!(any(test, sqlite)) && self.pool.get_database_backend() == DbBackend::Sqlite {
if cfg!(any(test, feature = "sqlite")) && self.pool.get_database_backend() == DbBackend::Sqlite {
query = query.filter(Expr::cust_with_values(
"UPPER(github_login) LIKE ?",
[Self::fuzzy_like_string(&filter.to_uppercase())],

View File

@@ -66,6 +66,16 @@ impl Database {
.await?
.ok_or_else(|| anyhow!("no remote project"))?;
let (_, dev_server) = dev_server_project::Entity::find_by_id(dev_server_project_id)
.find_also_related(dev_server::Entity)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no dev_server_project"))?;
if !dev_server.is_some_and(|dev_server| dev_server.user_id == participant.user_id) {
return Err(anyhow!("not your dev server"))?;
}
if project.room_id.is_some() {
return Err(anyhow!("project already shared"))?;
};
@@ -77,7 +87,6 @@ impl Database {
.exec(&*tx)
.await?;
// todo! check user is a project-collaborator
let room = self.get_room(room_id, &tx).await?;
return Ok((project.id, room));
}
@@ -1092,6 +1101,36 @@ impl Database {
.map(|guard| guard.into_inner())
}
/// Returns the host connection for a request to join a shared project.
pub async fn host_for_owner_project_request(
&self,
project_id: ProjectId,
_connection_id: ConnectionId,
user_id: UserId,
) -> Result<ConnectionId> {
self.project_transaction(project_id, |tx| async move {
let (project, dev_server_project) = project::Entity::find_by_id(project_id)
.find_also_related(dev_server_project::Entity)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?;
let Some(dev_server_project) = dev_server_project else {
return Err(anyhow!("not a dev server project"))?;
};
let dev_server = dev_server::Entity::find_by_id(dev_server_project.dev_server_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such dev server"))?;
if dev_server.user_id != user_id {
return Err(anyhow!("not your project"))?;
}
project.host_connection()
})
.await
.map(|guard| guard.into_inner())
}
pub async fn connections_for_buffer_update(
&self,
project_id: ProjectId,

View File

@@ -62,7 +62,7 @@ impl RateLimiter {
let mut bucket = self
.buckets
.entry(bucket_key.clone())
.or_insert_with(|| RateBucket::new(T::capacity(), T::refill_duration(), now));
.or_insert_with(|| RateBucket::new::<T>(now));
if bucket.value_mut().allow(now) {
self.dirty_buckets.insert(bucket_key);
@@ -72,19 +72,19 @@ impl RateLimiter {
}
}
async fn load_bucket<K: RateLimit>(
async fn load_bucket<T: RateLimit>(
&self,
user_id: UserId,
) -> Result<Option<RateBucket>, Error> {
Ok(self
.db
.get_rate_bucket(user_id, K::db_name())
.get_rate_bucket(user_id, T::db_name())
.await?
.map(|saved_bucket| RateBucket {
capacity: K::capacity(),
refill_time_per_token: K::refill_duration(),
token_count: saved_bucket.token_count as usize,
last_refill: DateTime::from_naive_utc_and_offset(saved_bucket.last_refill, Utc),
.map(|saved_bucket| {
RateBucket::from_db::<T>(
saved_bucket.token_count as usize,
DateTime::from_naive_utc_and_offset(saved_bucket.last_refill, Utc),
)
}))
}
@@ -124,15 +124,24 @@ struct RateBucket {
}
impl RateBucket {
fn new(capacity: usize, refill_duration: Duration, now: DateTimeUtc) -> Self {
RateBucket {
capacity,
token_count: capacity,
refill_time_per_token: refill_duration / capacity as i32,
fn new<T: RateLimit>(now: DateTimeUtc) -> Self {
Self {
capacity: T::capacity(),
token_count: T::capacity(),
refill_time_per_token: T::refill_duration() / T::capacity() as i32,
last_refill: now,
}
}
fn from_db<T: RateLimit>(token_count: usize, last_refill: DateTimeUtc) -> Self {
Self {
capacity: T::capacity(),
token_count,
refill_time_per_token: T::refill_duration() / T::capacity() as i32,
last_refill,
}
}
fn allow(&mut self, now: DateTimeUtc) -> bool {
self.refill(now);
if self.token_count > 0 {
@@ -148,9 +157,12 @@ impl RateBucket {
if elapsed >= self.refill_time_per_token {
let new_tokens =
elapsed.num_milliseconds() / self.refill_time_per_token.num_milliseconds();
self.token_count = (self.token_count + new_tokens as usize).min(self.capacity);
self.last_refill = now;
let unused_refill_time = Duration::milliseconds(
elapsed.num_milliseconds() % self.refill_time_per_token.num_milliseconds(),
);
self.last_refill = now - unused_refill_time;
}
}
}
@@ -218,8 +230,19 @@ mod tests {
.await
.unwrap();
// After one second, user 1 can make another request before being rate-limited again.
now += Duration::seconds(1);
// After 1.5s, user 1 can make another request before being rate-limited again.
now += Duration::milliseconds(1500);
rate_limiter
.check_internal::<RateLimitA>(user_1, now)
.await
.unwrap();
rate_limiter
.check_internal::<RateLimitA>(user_1, now)
.await
.unwrap_err();
// After 500ms, user 1 can make another request before being rate-limited again.
now += Duration::milliseconds(500);
rate_limiter
.check_internal::<RateLimitA>(user_1, now)
.await
@@ -238,6 +261,17 @@ mod tests {
.check_internal::<RateLimitA>(user_1, now)
.await
.unwrap_err();
// After 1s, user 1 can make another request before being rate-limited again.
now += Duration::seconds(1);
rate_limiter
.check_internal::<RateLimitA>(user_1, now)
.await
.unwrap();
rate_limiter
.check_internal::<RateLimitA>(user_1, now)
.await
.unwrap_err();
}
struct RateLimitA;

View File

@@ -446,6 +446,12 @@ impl Server {
.add_message_handler(update_language_server)
.add_message_handler(update_diagnostic_summary)
.add_message_handler(update_worktree_settings)
.add_request_handler(user_handler(
forward_project_request_for_owner::<proto::TaskContextForLocation>,
))
.add_request_handler(user_handler(
forward_project_request_for_owner::<proto::TaskTemplates>,
))
.add_request_handler(user_handler(
forward_read_only_project_request::<proto::GetHover>,
))
@@ -1140,8 +1146,17 @@ impl Server {
Principal::DevServer(dev_server) => {
{
let mut pool = self.connection_pool.lock();
if pool.dev_server_connection_id(dev_server.id).is_some() {
return Err(anyhow!(ErrorCode::DevServerAlreadyOnline))?;
if let Some(stale_connection_id) = pool.dev_server_connection_id(dev_server.id)
{
self.peer.send(
stale_connection_id,
proto::ShutdownDevServer {
reason: Some(
"another dev server connected with the same token".to_string(),
),
},
)?;
pool.remove_connection(stale_connection_id)?;
};
pool.add_dev_server(connection_id, dev_server.id, zed_version);
}
@@ -2398,9 +2413,12 @@ async fn regenerate_dev_server_token(
.dev_server_connection_id(dev_server_id);
if let Some(connection_id) = connection_id {
shutdown_dev_server_internal(dev_server_id, connection_id, &session).await?;
session
.peer
.send(connection_id, proto::ShutdownDevServer {})?;
session.peer.send(
connection_id,
proto::ShutdownDevServer {
reason: Some("dev server token was regenerated".to_string()),
},
)?;
let _ = remove_dev_server_connection(dev_server_id, &session).await;
}
@@ -2470,9 +2488,12 @@ async fn delete_dev_server(
.dev_server_connection_id(dev_server_id);
if let Some(connection_id) = connection_id {
shutdown_dev_server_internal(dev_server_id, connection_id, &session).await?;
session
.peer
.send(connection_id, proto::ShutdownDevServer {})?;
session.peer.send(
connection_id,
proto::ShutdownDevServer {
reason: Some("dev server was deleted".to_string()),
},
)?;
let _ = remove_dev_server_connection(dev_server_id, &session).await;
}
@@ -2874,6 +2895,31 @@ where
Ok(())
}
/// forward a project request to the dev server. Only allowed
/// if it's your dev server.
async fn forward_project_request_for_owner<T>(
request: T,
response: Response<T>,
session: UserSession,
) -> Result<()>
where
T: EntityMessage + RequestMessage,
{
let project_id = ProjectId::from_proto(request.remote_entity_id());
let host_connection_id = session
.db()
.await
.host_for_owner_project_request(project_id, session.connection_id, session.user_id())
.await?;
let payload = session
.peer
.forward_request(session.connection_id, host_connection_id, request)
.await?;
response.send(payload)?;
Ok(())
}
/// forward a project request to the host. These requests are disallowed
/// for guests.
async fn forward_mutating_project_request<T>(

View File

@@ -572,7 +572,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

@@ -42,7 +42,7 @@ use std::{
},
};
use text::Point;
use workspace::{Workspace, WorkspaceId};
use workspace::Workspace;
#[gpui::test(iterations = 10)]
async fn test_host_disconnect(
@@ -85,14 +85,8 @@ async fn test_host_disconnect(
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
let workspace_b = cx_b.add_window(|cx| {
Workspace::new(
WorkspaceId::default(),
project_b.clone(),
client_b.app_state.clone(),
cx,
)
});
let workspace_b = cx_b
.add_window(|cx| Workspace::new(None, project_b.clone(), client_b.app_state.clone(), cx));
let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b);
let workspace_b_view = workspace_b.root_view(cx_b).unwrap();

View File

@@ -308,8 +308,9 @@ async fn test_basic_following(
result
});
let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| {
let editor =
cx.new_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
let editor = cx.new_view(|cx| {
Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), true, cx)
});
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, cx);
editor
});

View File

@@ -42,7 +42,7 @@ use std::{
Arc,
},
};
use workspace::{Workspace, WorkspaceId, WorkspaceStore};
use workspace::{Workspace, WorkspaceStore};
pub struct TestServer {
pub app_state: Arc<AppState>,
@@ -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);
@@ -906,12 +902,7 @@ impl TestClient {
) -> (View<Workspace>, &'a mut VisualTestContext) {
cx.add_window_view(|cx| {
cx.activate_window();
Workspace::new(
WorkspaceId::default(),
project.clone(),
self.app_state.clone(),
cx,
)
Workspace::new(None, project.clone(), self.app_state.clone(), cx)
})
}
@@ -922,12 +913,7 @@ impl TestClient {
let project = self.build_test_project(cx).await;
cx.add_window_view(|cx| {
cx.activate_window();
Workspace::new(
WorkspaceId::default(),
project.clone(),
self.app_state.clone(),
cx,
)
Workspace::new(None, project.clone(), self.app_state.clone(), cx)
})
}

View File

@@ -400,7 +400,11 @@ impl Item for ChannelView {
None
}
fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<View<Self>> {
fn clone_on_split(
&self,
_: Option<WorkspaceId>,
cx: &mut ViewContext<Self>,
) -> Option<View<Self>> {
Some(cx.new_view(|cx| {
Self::new(
self.project.clone(),

View File

@@ -75,6 +75,17 @@ impl CompletionProvider for MessageEditorCompletionProvider {
) -> Task<Result<Option<language::Transaction>>> {
Task::ready(Ok(None))
}
fn is_completion_trigger(
&self,
_buffer: &Model<Buffer>,
_position: language::Anchor,
text: &str,
_trigger_in_words: bool,
_cx: &mut ViewContext<Editor>,
) -> bool {
text == "@"
}
}
impl MessageEditor {
@@ -89,6 +100,9 @@ impl MessageEditor {
editor.update(cx, |editor, cx| {
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
editor.set_use_autoclose(false);
editor.set_show_gutter(false, cx);
editor.set_show_wrap_guides(false, cx);
editor.set_show_indent_guides(false, cx);
editor.set_completion_provider(Box::new(MessageEditorCompletionProvider(this)));
editor.set_auto_replace_emoji_shortcode(
MessageEditorSettings::get_global(cx)
@@ -291,6 +305,8 @@ impl MessageEditor {
documentation: None,
server_id: LanguageServerId(0), // TODO: Make this optional or something?
lsp_completion: Default::default(), // TODO: Make this optional or something?
confirm: None,
show_new_completions_on_confirm: false,
}
})
.collect()
@@ -510,7 +526,7 @@ impl Render for MessageEditor {
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features.clone(),
font_size: TextSize::Small.rems(cx).into(),
font_weight: FontWeight::NORMAL,
font_weight: settings.ui_font.weight,
font_style: FontStyle::Normal,
line_height: relative(1.3),
background_color: None,

View File

@@ -16,10 +16,10 @@ use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
actions, anchored, canvas, deferred, div, fill, list, point, prelude::*, px, AnyElement,
AppContext, AsyncWindowContext, Bounds, ClickEvent, ClipboardItem, DismissEvent, Div,
EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight, InteractiveElement,
IntoElement, ListOffset, ListState, Model, MouseDownEvent, ParentElement, Pixels, Point,
PromptLevel, Render, SharedString, Styled, Subscription, Task, TextStyle, View, ViewContext,
VisualContext, WeakView, WhiteSpace,
EventEmitter, FocusHandle, FocusableView, FontStyle, InteractiveElement, IntoElement,
ListOffset, ListState, Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel,
Render, SharedString, Styled, Subscription, Task, TextStyle, View, ViewContext, VisualContext,
WeakView, WhiteSpace,
};
use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrev};
use project::{Fs, Project};
@@ -2190,7 +2190,7 @@ impl CollabPanel {
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features.clone(),
font_size: rems(0.875).into(),
font_weight: FontWeight::NORMAL,
font_weight: settings.ui_font.weight,
font_style: FontStyle::Normal,
line_height: relative(1.3),
background_color: None,

View File

@@ -116,15 +116,12 @@ impl Render for IncomingCallNotification {
// TODO: Is there a better place for us to initialize the font?
let (ui_font, ui_font_size) = {
let theme_settings = ThemeSettings::get_global(cx);
(
theme_settings.ui_font.family.clone(),
theme_settings.ui_font_size,
)
(theme_settings.ui_font.clone(), theme_settings.ui_font_size)
};
cx.set_rem_size(ui_font_size);
div().size_full().font_family(ui_font).child(
div().size_full().font(ui_font).child(
CollabNotification::new(
self.state.call.calling_user.avatar_uri.clone(),
Button::new("accept", "Accept").on_click({

View File

@@ -121,15 +121,12 @@ impl Render for ProjectSharedNotification {
// TODO: Is there a better place for us to initialize the font?
let (ui_font, ui_font_size) = {
let theme_settings = ThemeSettings::get_global(cx);
(
theme_settings.ui_font.family.clone(),
theme_settings.ui_font_size,
)
(theme_settings.ui_font.clone(), theme_settings.ui_font_size)
};
cx.set_rem_size(ui_font_size);
div().size_full().font_family(ui_font).child(
div().size_full().font(ui_font).child(
CollabNotification::new(
self.owner.avatar_uri.clone(),
Button::new("open", "Open").on_click(cx.listener(move |this, _event, cx| {

View File

@@ -781,7 +781,7 @@ mod tests {
);
multibuffer
});
let editor = cx.add_window(|cx| Editor::for_multibuffer(multibuffer, None, cx));
let editor = cx.add_window(|cx| Editor::for_multibuffer(multibuffer, None, true, cx));
editor.update(cx, |editor, cx| editor.focus(cx)).unwrap();
let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
editor
@@ -811,7 +811,7 @@ mod tests {
assert!(editor.has_active_inline_completion(cx));
assert_eq!(
editor.display_text(cx),
"\n\na = 1\nb = 2 + a\n\n\n\nc = 3\nd = 4\n"
"\n\n\na = 1\nb = 2 + a\n\n\n\n\n\nc = 3\nd = 4\n\n"
);
assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
});
@@ -833,7 +833,7 @@ mod tests {
assert!(!editor.has_active_inline_completion(cx));
assert_eq!(
editor.display_text(cx),
"\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4\n"
"\n\n\na = 1\nb = 2\n\n\n\n\n\nc = 3\nd = 4\n\n"
);
assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
@@ -842,7 +842,7 @@ mod tests {
assert!(!editor.has_active_inline_completion(cx));
assert_eq!(
editor.display_text(cx),
"\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 \n"
"\n\n\na = 1\nb = 2\n\n\n\n\n\nc = 3\nd = 4 \n\n"
);
assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
});
@@ -853,7 +853,7 @@ mod tests {
assert!(editor.has_active_inline_completion(cx));
assert_eq!(
editor.display_text(cx),
"\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 + c\n"
"\n\n\na = 1\nb = 2\n\n\n\n\n\nc = 3\nd = 4 + c\n\n"
);
assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
});
@@ -1032,7 +1032,7 @@ mod tests {
);
multibuffer
});
let editor = cx.add_window(|cx| Editor::for_multibuffer(multibuffer, None, cx));
let editor = cx.add_window(|cx| Editor::for_multibuffer(multibuffer, None, true, cx));
let copilot_provider = cx.new_model(|_| CopilotCompletionProvider::new(copilot));
editor
.update(cx, |editor, cx| {

View File

@@ -161,7 +161,7 @@ impl ProjectDiagnosticsEditor {
});
let editor = cx.new_view(|cx| {
let mut editor =
Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), cx);
Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), false, cx);
editor.set_vertical_scroll_margin(5, cx);
editor
});
@@ -704,7 +704,7 @@ impl Item for ProjectDiagnosticsEditor {
fn clone_on_split(
&self,
_workspace_id: workspace::WorkspaceId,
_workspace_id: Option<workspace::WorkspaceId>,
cx: &mut ViewContext<Self>,
) -> Option<View<Self>>
where
@@ -792,13 +792,15 @@ impl Item for ProjectDiagnosticsEditor {
}
}
const DIAGNOSTIC_HEADER: &'static str = "diagnostic header";
fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
let (message, code_ranges) = highlight_diagnostic_message(&diagnostic);
let message: SharedString = message;
Box::new(move |cx| {
let highlight_style: HighlightStyle = cx.theme().colors().text_accent.into();
h_flex()
.id("diagnostic header")
.id(DIAGNOSTIC_HEADER)
.py_2()
.pl_10()
.pr_5()

View File

@@ -158,11 +158,11 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
assert_eq!(
editor_blocks(&editor, cx),
[
(DisplayRow(0), "path header block".into()),
(DisplayRow(2), "diagnostic header".into()),
(DisplayRow(15), "collapsed context".into()),
(DisplayRow(16), "diagnostic header".into()),
(DisplayRow(25), "collapsed context".into()),
(DisplayRow(0), FILE_HEADER.into()),
(DisplayRow(2), DIAGNOSTIC_HEADER.into()),
(DisplayRow(15), EXCERPT_HEADER.into()),
(DisplayRow(16), DIAGNOSTIC_HEADER.into()),
(DisplayRow(25), EXCERPT_HEADER.into()),
]
);
assert_eq!(
@@ -243,13 +243,13 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
assert_eq!(
editor_blocks(&editor, cx),
[
(DisplayRow(0), "path header block".into()),
(DisplayRow(2), "diagnostic header".into()),
(DisplayRow(7), "path header block".into()),
(DisplayRow(9), "diagnostic header".into()),
(DisplayRow(22), "collapsed context".into()),
(DisplayRow(23), "diagnostic header".into()),
(DisplayRow(32), "collapsed context".into()),
(DisplayRow(0), FILE_HEADER.into()),
(DisplayRow(2), DIAGNOSTIC_HEADER.into()),
(DisplayRow(7), FILE_HEADER.into()),
(DisplayRow(9), DIAGNOSTIC_HEADER.into()),
(DisplayRow(22), EXCERPT_HEADER.into()),
(DisplayRow(23), DIAGNOSTIC_HEADER.into()),
(DisplayRow(32), EXCERPT_HEADER.into()),
]
);
@@ -355,15 +355,15 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
assert_eq!(
editor_blocks(&editor, cx),
[
(DisplayRow(0), "path header block".into()),
(DisplayRow(2), "diagnostic header".into()),
(DisplayRow(7), "collapsed context".into()),
(DisplayRow(8), "diagnostic header".into()),
(DisplayRow(13), "path header block".into()),
(DisplayRow(15), "diagnostic header".into()),
(DisplayRow(28), "collapsed context".into()),
(DisplayRow(29), "diagnostic header".into()),
(DisplayRow(38), "collapsed context".into()),
(DisplayRow(0), FILE_HEADER.into()),
(DisplayRow(2), DIAGNOSTIC_HEADER.into()),
(DisplayRow(7), EXCERPT_HEADER.into()),
(DisplayRow(8), DIAGNOSTIC_HEADER.into()),
(DisplayRow(13), FILE_HEADER.into()),
(DisplayRow(15), DIAGNOSTIC_HEADER.into()),
(DisplayRow(28), EXCERPT_HEADER.into()),
(DisplayRow(29), DIAGNOSTIC_HEADER.into()),
(DisplayRow(38), EXCERPT_HEADER.into()),
]
);
@@ -493,8 +493,8 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
assert_eq!(
editor_blocks(&editor, cx),
[
(DisplayRow(0), "path header block".into()),
(DisplayRow(2), "diagnostic header".into()),
(DisplayRow(0), FILE_HEADER.into()),
(DisplayRow(2), DIAGNOSTIC_HEADER.into()),
]
);
assert_eq!(
@@ -539,10 +539,10 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
assert_eq!(
editor_blocks(&editor, cx),
[
(DisplayRow(0), "path header block".into()),
(DisplayRow(2), "diagnostic header".into()),
(DisplayRow(6), "collapsed context".into()),
(DisplayRow(7), "diagnostic header".into()),
(DisplayRow(0), FILE_HEADER.into()),
(DisplayRow(2), DIAGNOSTIC_HEADER.into()),
(DisplayRow(6), EXCERPT_HEADER.into()),
(DisplayRow(7), DIAGNOSTIC_HEADER.into()),
]
);
assert_eq!(
@@ -605,10 +605,10 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
assert_eq!(
editor_blocks(&editor, cx),
[
(DisplayRow(0), "path header block".into()),
(DisplayRow(2), "diagnostic header".into()),
(DisplayRow(7), "collapsed context".into()),
(DisplayRow(8), "diagnostic header".into()),
(DisplayRow(0), FILE_HEADER.into()),
(DisplayRow(2), DIAGNOSTIC_HEADER.into()),
(DisplayRow(7), EXCERPT_HEADER.into()),
(DisplayRow(8), DIAGNOSTIC_HEADER.into()),
]
);
assert_eq!(
@@ -661,10 +661,10 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
assert_eq!(
editor_blocks(&editor, cx),
[
(DisplayRow(0), "path header block".into()),
(DisplayRow(2), "diagnostic header".into()),
(DisplayRow(7), "collapsed context".into()),
(DisplayRow(8), "diagnostic header".into()),
(DisplayRow(0), FILE_HEADER.into()),
(DisplayRow(2), DIAGNOSTIC_HEADER.into()),
(DisplayRow(7), EXCERPT_HEADER.into()),
(DisplayRow(8), DIAGNOSTIC_HEADER.into()),
]
);
assert_eq!(
@@ -958,6 +958,10 @@ fn random_diagnostic(
}
}
const FILE_HEADER: &'static str = "file header";
const EXCERPT_HEADER: &'static str = "excerpt header";
const EXCERPT_FOOTER: &'static str = "excerpt footer";
fn editor_blocks(
editor: &View<Editor>,
cx: &mut VisualTestContext,
@@ -996,11 +1000,12 @@ fn editor_blocks(
starts_new_buffer, ..
} => {
if *starts_new_buffer {
"path header block".into()
FILE_HEADER.into()
} else {
"collapsed context".into()
EXCERPT_HEADER.into()
}
}
TransformBlock::ExcerptFooter { .. } => EXCERPT_FOOTER.into(),
};
Some((row, name))

View File

@@ -114,12 +114,26 @@ pub struct ExpandExcerpts {
pub(super) lines: u32,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct ExpandExcerptsUp {
#[serde(default)]
pub(super) lines: u32,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct ExpandExcerptsDown {
#[serde(default)]
pub(super) lines: u32,
}
impl_actions!(
editor,
[
ConfirmCodeAction,
ConfirmCompletion,
ExpandExcerpts,
ExpandExcerptsUp,
ExpandExcerptsDown,
FoldAt,
MoveDownByLines,
MovePageDown,
@@ -273,6 +287,7 @@ gpui::actions!(
ToggleHunkDiff,
ToggleInlayHints,
ToggleLineNumbers,
ToggleIndentGuides,
ToggleSoftWrap,
Transpose,
Undo,

View File

@@ -24,17 +24,27 @@ mod inlay_map;
mod tab_map;
mod wrap_map;
use crate::{hover_links::InlayHighlight, movement::TextLayoutDetails, InlayId};
use crate::{EditorStyle, RowExt};
pub use block_map::{BlockMap, BlockPoint};
use collections::{HashMap, HashSet};
use fold_map::FoldMap;
use gpui::{
AnyElement, Font, HighlightStyle, Hsla, LineLayout, Model, ModelContext, Pixels, UnderlineStyle,
use crate::{
hover_links::InlayHighlight, movement::TextLayoutDetails, EditorStyle, InlayId, RowExt,
};
use inlay_map::InlayMap;
pub use block_map::{
BlockBufferRows, BlockChunks as DisplayChunks, BlockContext, BlockDisposition, BlockId,
BlockMap, BlockPoint, BlockProperties, BlockStyle, RenderBlock, TransformBlock,
};
use block_map::{BlockRow, BlockSnapshot};
use collections::{HashMap, HashSet};
pub use flap_map::*;
pub use fold_map::{Fold, FoldId, FoldPlaceholder, FoldPoint};
use fold_map::{FoldMap, FoldSnapshot};
use gpui::{
AnyElement, Font, HighlightStyle, LineLayout, Model, ModelContext, Pixels, UnderlineStyle,
};
pub(crate) use inlay_map::Inlay;
use inlay_map::{InlayMap, InlaySnapshot};
pub use inlay_map::{InlayOffset, InlayPoint};
use language::{
language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription,
language_settings::language_settings, ChunkRenderer, OffsetUtf16, Point,
Subscription as BufferSubscription,
};
use lsp::DiagnosticSeverity;
use multi_buffer::{
@@ -42,27 +52,13 @@ 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 sum_tree::{Bias, TreeMap};
use tab_map::TabMap;
use tab_map::{TabMap, TabSnapshot};
use text::LineIndent;
use ui::WindowContext;
use wrap_map::WrapMap;
pub use block_map::{
BlockBufferRows, BlockChunks as DisplayChunks, BlockContext, BlockDisposition, BlockId,
BlockProperties, BlockStyle, RenderBlock, TransformBlock,
};
pub use flap_map::*;
use self::block_map::{BlockRow, BlockSnapshot};
use self::fold_map::FoldSnapshot;
pub use self::fold_map::{Fold, FoldId, FoldPoint};
use self::inlay_map::InlaySnapshot;
pub use self::inlay_map::{InlayOffset, InlayPoint};
use self::tab_map::TabSnapshot;
use self::wrap_map::WrapSnapshot;
pub(crate) use inlay_map::Inlay;
use wrap_map::{WrapMap, WrapSnapshot};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum FoldStatus {
@@ -105,17 +101,22 @@ pub struct DisplayMap {
inlay_highlights: InlayHighlights,
/// A container for explicitly foldable ranges, which supersede indentation based fold range suggestions.
flap_map: FlapMap,
fold_placeholder: FoldPlaceholder,
pub clip_at_line_ends: bool,
}
impl DisplayMap {
#[allow(clippy::too_many_arguments)]
pub fn new(
buffer: Model<MultiBuffer>,
font: Font,
font_size: Pixels,
wrap_width: Option<Pixels>,
show_excerpt_controls: bool,
buffer_header_height: u8,
excerpt_header_height: u8,
excerpt_footer_height: u8,
fold_placeholder: FoldPlaceholder,
cx: &mut ModelContext<Self>,
) -> Self {
let buffer_subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
@@ -125,8 +126,15 @@ impl DisplayMap {
let (fold_map, snapshot) = FoldMap::new(snapshot);
let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
let (wrap_map, snapshot) = WrapMap::new(snapshot, font, font_size, wrap_width, cx);
let block_map = BlockMap::new(snapshot, buffer_header_height, excerpt_header_height);
let block_map = BlockMap::new(
snapshot,
show_excerpt_controls,
buffer_header_height,
excerpt_header_height,
excerpt_footer_height,
);
let flap_map = FlapMap::default();
cx.observe(&wrap_map, |_, _, cx| cx.notify()).detach();
DisplayMap {
@@ -138,6 +146,7 @@ impl DisplayMap {
wrap_map,
block_map,
flap_map,
fold_placeholder,
text_highlights: Default::default(),
inlay_highlights: Default::default(),
clip_at_line_ends: false,
@@ -167,6 +176,7 @@ impl DisplayMap {
text_highlights: self.text_highlights.clone(),
inlay_highlights: self.inlay_highlights.clone(),
clip_at_line_ends: self.clip_at_line_ends,
fold_placeholder: self.fold_placeholder.clone(),
}
}
@@ -174,14 +184,19 @@ impl DisplayMap {
self.fold(
other
.folds_in_range(0..other.buffer_snapshot.len())
.map(|fold| (fold.range.to_offset(&other.buffer_snapshot), fold.text)),
.map(|fold| {
(
fold.range.to_offset(&other.buffer_snapshot),
fold.placeholder.clone(),
)
}),
cx,
);
}
pub fn fold<T: ToOffset>(
&mut self,
ranges: impl IntoIterator<Item = (Range<T>, &'static str)>,
ranges: impl IntoIterator<Item = (Range<T>, FoldPlaceholder)>,
cx: &mut ModelContext<Self>,
) {
let snapshot = self.buffer.read(cx).snapshot(cx);
@@ -324,10 +339,6 @@ impl DisplayMap {
.update(cx, |map, cx| map.set_font_with_size(font, font_size, cx))
}
pub fn set_fold_ellipses_color(&mut self, color: Hsla) -> bool {
self.fold_map.set_ellipses_color(color)
}
pub fn set_wrap_width(&self, width: Option<Pixels>, cx: &mut ModelContext<Self>) -> bool {
self.wrap_map
.update(cx, |map, cx| map.set_wrap_width(width, cx))
@@ -378,6 +389,10 @@ impl DisplayMap {
pub fn is_rewrapping(&self, cx: &gpui::AppContext) -> bool {
self.wrap_map.read(cx).is_rewrapping()
}
pub fn show_excerpt_controls(&self) -> bool {
self.block_map.show_excerpt_controls()
}
}
#[derive(Debug, Default)]
@@ -394,9 +409,10 @@ pub struct HighlightStyles {
}
pub struct HighlightedChunk<'a> {
pub chunk: &'a str,
pub text: &'a str,
pub style: Option<HighlightStyle>,
pub is_tab: bool,
pub renderer: Option<ChunkRenderer>,
}
#[derive(Clone)]
@@ -411,6 +427,7 @@ pub struct DisplaySnapshot {
text_highlights: TextHighlights,
inlay_highlights: InlayHighlights,
clip_at_line_ends: bool,
pub(crate) fold_placeholder: FoldPlaceholder,
}
impl DisplaySnapshot {
@@ -648,9 +665,10 @@ impl DisplaySnapshot {
}
HighlightedChunk {
chunk: chunk.text,
text: chunk.text,
style: highlight_style,
is_tab: chunk.is_tab,
renderer: chunk.renderer,
}
})
}
@@ -672,7 +690,7 @@ impl DisplaySnapshot {
let range = display_row..display_row.next_row();
for chunk in self.highlighted_chunks(range, false, &editor_style) {
line.push_str(chunk.chunk);
line.push_str(chunk.text);
let text_style = if let Some(style) = chunk.style {
Cow::Owned(editor_style.text.clone().highlight(style))
@@ -680,7 +698,7 @@ impl DisplaySnapshot {
Cow::Borrowed(&editor_style.text)
};
runs.push(text_style.to_run(chunk.chunk.len()))
runs.push(text_style.to_run(chunk.text.len()))
}
if line.ends_with('\n') {
@@ -839,26 +857,13 @@ impl DisplaySnapshot {
result
}
pub fn line_indent_for_buffer_row(&self, buffer_row: MultiBufferRow) -> (u32, bool) {
pub fn line_indent_for_buffer_row(&self, buffer_row: MultiBufferRow) -> LineIndent {
let (buffer, range) = self
.buffer_snapshot
.buffer_line_for_row(buffer_row)
.unwrap();
let mut indent_size = 0;
let mut is_blank = false;
for c in buffer.chars_at(Point::new(range.start.row, 0)) {
if c == ' ' || c == '\t' {
indent_size += 1;
} else {
if c == '\n' {
is_blank = true;
}
break;
}
}
(indent_size, is_blank)
buffer.line_indent_for_row(range.start.row)
}
pub fn line_len(&self, row: DisplayRow) -> u32 {
@@ -875,17 +880,16 @@ impl DisplaySnapshot {
return false;
}
let (indent_size, is_blank) = self.line_indent_for_buffer_row(buffer_row);
if is_blank {
let line_indent = self.line_indent_for_buffer_row(buffer_row);
if line_indent.is_line_blank() {
return false;
}
for next_row in (buffer_row.0 + 1)..=max_row.0 {
let (next_indent_size, next_line_is_blank) =
self.line_indent_for_buffer_row(MultiBufferRow(next_row));
if next_indent_size > indent_size {
let next_line_indent = self.line_indent_for_buffer_row(MultiBufferRow(next_row));
if next_line_indent.raw_len() > line_indent.raw_len() {
return true;
} else if !next_line_is_blank {
} else if !next_line_indent.is_line_blank() {
break;
}
}
@@ -896,23 +900,28 @@ impl DisplaySnapshot {
pub fn foldable_range(
&self,
buffer_row: MultiBufferRow,
) -> Option<(Range<Point>, &'static str)> {
) -> Option<(Range<Point>, FoldPlaceholder)> {
let start = MultiBufferPoint::new(buffer_row.0, self.buffer_snapshot.line_len(buffer_row));
if let Some(flap) = self
.flap_snapshot
.query_row(buffer_row, &self.buffer_snapshot)
{
Some((flap.range.to_point(&self.buffer_snapshot), ""))
Some((
flap.range.to_point(&self.buffer_snapshot),
flap.placeholder.clone(),
))
} else if self.starts_indent(MultiBufferRow(start.row))
&& !self.is_line_folded(MultiBufferRow(start.row))
{
let (start_indent, _) = self.line_indent_for_buffer_row(buffer_row);
let start_line_indent = self.line_indent_for_buffer_row(buffer_row);
let max_point = self.buffer_snapshot.max_point();
let mut end = None;
for row in (buffer_row.0 + 1)..=max_point.row {
let (indent, is_blank) = self.line_indent_for_buffer_row(MultiBufferRow(row));
if !is_blank && indent <= start_indent {
let line_indent = self.line_indent_for_buffer_row(MultiBufferRow(row));
if !line_indent.is_line_blank()
&& line_indent.raw_len() <= start_line_indent.raw_len()
{
let prev_row = row - 1;
end = Some(Point::new(
prev_row,
@@ -922,7 +931,7 @@ impl DisplaySnapshot {
}
}
let end = end.unwrap_or(max_point);
Some((start..end, ""))
Some((start..end, self.fold_placeholder.clone()))
} else {
None
}
@@ -963,6 +972,14 @@ impl Debug for DisplayPoint {
#[serde(transparent)]
pub struct DisplayRow(pub u32);
impl Add for DisplayRow {
type Output = Self;
fn add(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)))
@@ -1094,8 +1111,11 @@ pub mod tests {
font("Helvetica"),
font_size,
wrap_width,
true,
buffer_start_excerpt_header_height,
excerpt_header_height,
0,
FoldPlaceholder::test(),
cx,
)
});
@@ -1199,7 +1219,12 @@ pub mod tests {
} else {
log::info!("folding ranges: {:?}", ranges);
map.update(cx, |map, cx| {
map.fold(ranges.into_iter().map(|range| (range, "")), cx);
map.fold(
ranges
.into_iter()
.map(|range| (range, FoldPlaceholder::test())),
cx,
);
});
}
}
@@ -1334,8 +1359,11 @@ pub mod tests {
font("Helvetica"),
font_size,
wrap_width,
true,
1,
1,
0,
FoldPlaceholder::test(),
cx,
)
});
@@ -1437,7 +1465,18 @@ pub mod tests {
let font_size = px(14.0);
let map = cx.new_model(|cx| {
DisplayMap::new(buffer.clone(), font("Helvetica"), font_size, None, 1, 1, cx)
DisplayMap::new(
buffer.clone(),
font("Helvetica"),
font_size,
None,
true,
1,
1,
0,
FoldPlaceholder::test(),
cx,
)
});
buffer.update(cx, |buffer, cx| {
@@ -1523,8 +1562,20 @@ pub mod tests {
let font_size = px(14.0);
let map = cx
.new_model(|cx| DisplayMap::new(buffer, font("Helvetica"), font_size, None, 1, 1, cx));
let map = cx.new_model(|cx| {
DisplayMap::new(
buffer,
font("Helvetica"),
font_size,
None,
true,
1,
1,
1,
FoldPlaceholder::test(),
cx,
)
});
assert_eq!(
cx.update(|cx| syntax_chunks(DisplayRow(0)..DisplayRow(5), &map, &theme, cx)),
vec![
@@ -1549,7 +1600,7 @@ pub mod tests {
map.fold(
vec![(
MultiBufferPoint::new(0, 6)..MultiBufferPoint::new(3, 2),
"",
FoldPlaceholder::test(),
)],
cx,
)
@@ -1615,7 +1666,18 @@ pub mod tests {
let font_size = px(16.0);
let map = cx.new_model(|cx| {
DisplayMap::new(buffer, font("Courier"), font_size, Some(px(40.0)), 1, 1, cx)
DisplayMap::new(
buffer,
font("Courier"),
font_size,
Some(px(40.0)),
true,
1,
1,
0,
FoldPlaceholder::test(),
cx,
)
});
assert_eq!(
cx.update(|cx| syntax_chunks(DisplayRow(0)..DisplayRow(5), &map, &theme, cx)),
@@ -1634,7 +1696,7 @@ pub mod tests {
map.fold(
vec![(
MultiBufferPoint::new(0, 6)..MultiBufferPoint::new(3, 2),
"",
FoldPlaceholder::test(),
)],
cx,
)
@@ -1687,8 +1749,20 @@ pub mod tests {
let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
let font_size = px(16.0);
let map =
cx.new_model(|cx| DisplayMap::new(buffer, font("Courier"), font_size, None, 1, 1, cx));
let map = cx.new_model(|cx| {
DisplayMap::new(
buffer,
font("Courier"),
font_size,
None,
true,
1,
1,
1,
FoldPlaceholder::test(),
cx,
)
});
enum MyType {}
@@ -1802,8 +1876,18 @@ pub mod tests {
let buffer = MultiBuffer::build_simple(text, cx);
let font_size = px(14.0);
cx.new_model(|cx| {
let mut map =
DisplayMap::new(buffer.clone(), font("Helvetica"), font_size, None, 1, 1, cx);
let mut map = DisplayMap::new(
buffer.clone(),
font("Helvetica"),
font_size,
None,
true,
1,
1,
0,
FoldPlaceholder::test(),
cx,
);
let snapshot = map.buffer.read(cx).snapshot(cx);
let range =
snapshot.anchor_before(Point::new(2, 0))..snapshot.anchor_after(Point::new(3, 3));
@@ -1811,6 +1895,7 @@ pub mod tests {
map.flap_map.insert(
[Flap::new(
range,
FoldPlaceholder::test(),
|_row, _status, _toggle, _cx| div(),
|_row, _status, _cx| div(),
)],
@@ -1830,7 +1915,18 @@ pub mod tests {
let font_size = px(14.0);
let map = cx.new_model(|cx| {
DisplayMap::new(buffer.clone(), font("Helvetica"), font_size, None, 1, 1, cx)
DisplayMap::new(
buffer.clone(),
font("Helvetica"),
font_size,
None,
true,
1,
1,
0,
FoldPlaceholder::test(),
cx,
)
});
let map = map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(map.text(), "α\nβ \n🏀β γ");
@@ -1896,7 +1992,18 @@ pub mod tests {
let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx);
let font_size = px(14.0);
let map = cx.new_model(|cx| {
DisplayMap::new(buffer.clone(), font("Helvetica"), font_size, None, 1, 1, cx)
DisplayMap::new(
buffer.clone(),
font("Helvetica"),
font_size,
None,
true,
1,
1,
0,
FoldPlaceholder::test(),
cx,
)
});
assert_eq!(
map.update(cx, |map, cx| map.snapshot(cx)).max_point(),

View File

@@ -12,7 +12,7 @@ use std::{
cell::RefCell,
cmp::{self, Ordering},
fmt::Debug,
ops::{Deref, DerefMut, Range},
ops::{Deref, DerefMut, Range, RangeBounds},
sync::{
atomic::{AtomicUsize, Ordering::SeqCst},
Arc,
@@ -31,8 +31,10 @@ pub struct BlockMap {
wrap_snapshot: RefCell<WrapSnapshot>,
blocks: Vec<Arc<Block>>,
transforms: RefCell<SumTree<Transform>>,
show_excerpt_controls: bool,
buffer_header_height: u8,
excerpt_header_height: u8,
excerpt_footer_height: u8,
}
pub struct BlockMapWriter<'a>(&'a mut BlockMap);
@@ -92,6 +94,7 @@ pub struct BlockContext<'a, 'b> {
pub editor_style: &'b EditorStyle,
}
/// Whether the block should be considered above or below the anchor line
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum BlockDisposition {
Above,
@@ -104,6 +107,17 @@ struct Transform {
block: Option<TransformBlock>,
}
pub(crate) enum BlockType {
Custom(BlockId),
Header,
Footer,
}
pub(crate) trait BlockLike {
fn block_type(&self) -> BlockType;
fn disposition(&self) -> BlockDisposition;
}
#[allow(clippy::large_enum_variant)]
#[derive(Clone)]
pub enum TransformBlock {
@@ -114,7 +128,27 @@ pub enum TransformBlock {
range: ExcerptRange<text::Anchor>,
height: u8,
starts_new_buffer: bool,
show_excerpt_controls: bool,
},
ExcerptFooter {
id: ExcerptId,
disposition: BlockDisposition,
height: u8,
},
}
impl BlockLike for TransformBlock {
fn block_type(&self) -> BlockType {
match self {
TransformBlock::Custom(block) => BlockType::Custom(block.id),
TransformBlock::ExcerptHeader { .. } => BlockType::Header,
TransformBlock::ExcerptFooter { .. } => BlockType::Footer,
}
}
fn disposition(&self) -> BlockDisposition {
self.disposition()
}
}
impl TransformBlock {
@@ -122,6 +156,7 @@ impl TransformBlock {
match self {
TransformBlock::Custom(block) => block.disposition,
TransformBlock::ExcerptHeader { .. } => BlockDisposition::Above,
TransformBlock::ExcerptFooter { disposition, .. } => *disposition,
}
}
@@ -129,6 +164,7 @@ impl TransformBlock {
match self {
TransformBlock::Custom(block) => block.height,
TransformBlock::ExcerptHeader { height, .. } => *height,
TransformBlock::ExcerptFooter { height, .. } => *height,
}
}
}
@@ -137,9 +173,23 @@ impl Debug for TransformBlock {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Custom(block) => f.debug_struct("Custom").field("block", block).finish(),
Self::ExcerptHeader { buffer, .. } => f
Self::ExcerptHeader {
buffer,
starts_new_buffer,
id,
..
} => f
.debug_struct("ExcerptHeader")
.field("id", &id)
.field("path", &buffer.file().map(|f| f.path()))
.field("starts_new_buffer", &starts_new_buffer)
.finish(),
TransformBlock::ExcerptFooter {
id, disposition, ..
} => f
.debug_struct("ExcerptFooter")
.field("id", &id)
.field("disposition", &disposition)
.finish(),
}
}
@@ -170,8 +220,10 @@ pub struct BlockBufferRows<'a> {
impl BlockMap {
pub fn new(
wrap_snapshot: WrapSnapshot,
show_excerpt_controls: bool,
buffer_header_height: u8,
excerpt_header_height: u8,
excerpt_footer_height: u8,
) -> Self {
let row_count = wrap_snapshot.max_point().row() + 1;
let map = Self {
@@ -179,8 +231,10 @@ impl BlockMap {
blocks: Vec::new(),
transforms: RefCell::new(SumTree::from_item(Transform::isomorphic(row_count), &())),
wrap_snapshot: RefCell::new(wrap_snapshot.clone()),
show_excerpt_controls,
buffer_header_height,
excerpt_header_height,
excerpt_footer_height,
};
map.sync(
&wrap_snapshot,
@@ -364,49 +418,20 @@ impl BlockMap {
(position.row(), TransformBlock::Custom(block.clone()))
}),
);
if buffer.show_headers() {
blocks_in_edit.extend(
buffer
.excerpt_boundaries_in_range((start_bound, end_bound))
.map(|excerpt_boundary| {
(
wrap_snapshot
.make_wrap_point(
Point::new(excerpt_boundary.row.0, 0),
Bias::Left,
)
.row(),
TransformBlock::ExcerptHeader {
id: excerpt_boundary.id,
buffer: excerpt_boundary.buffer,
range: excerpt_boundary.range,
height: if excerpt_boundary.starts_new_buffer {
self.buffer_header_height
} else {
self.excerpt_header_height
},
starts_new_buffer: excerpt_boundary.starts_new_buffer,
},
)
}),
);
blocks_in_edit.extend(BlockMap::header_blocks(
self.show_excerpt_controls,
self.excerpt_footer_height,
self.buffer_header_height,
self.excerpt_header_height,
buffer,
(start_bound, end_bound),
wrap_snapshot,
));
}
// Place excerpt headers above custom blocks on the same row.
blocks_in_edit.sort_unstable_by(|(row_a, block_a), (row_b, block_b)| {
row_a.cmp(row_b).then_with(|| match (block_a, block_b) {
(
TransformBlock::ExcerptHeader { .. },
TransformBlock::ExcerptHeader { .. },
) => Ordering::Equal,
(TransformBlock::ExcerptHeader { .. }, _) => Ordering::Less,
(_, TransformBlock::ExcerptHeader { .. }) => Ordering::Greater,
(TransformBlock::Custom(block_a), TransformBlock::Custom(block_b)) => block_a
.disposition
.cmp(&block_b.disposition)
.then_with(|| block_a.id.cmp(&block_b.id)),
})
});
BlockMap::sort_blocks(&mut blocks_in_edit);
// For each of these blocks, insert a new isomorphic transform preceding the block,
// and then insert the block itself.
@@ -449,6 +474,95 @@ impl BlockMap {
}
}
}
pub fn show_excerpt_controls(&self) -> bool {
self.show_excerpt_controls
}
pub fn header_blocks<'a, 'b: 'a, 'c: 'a + 'b, R, T>(
show_excerpt_controls: bool,
excerpt_footer_height: u8,
buffer_header_height: u8,
excerpt_header_height: u8,
buffer: &'b multi_buffer::MultiBufferSnapshot,
range: R,
wrap_snapshot: &'c WrapSnapshot,
) -> impl Iterator<Item = (u32, TransformBlock)> + 'b
where
R: RangeBounds<T>,
T: multi_buffer::ToOffset,
{
buffer
.excerpt_boundaries_in_range(range)
.flat_map(move |excerpt_boundary| {
let wrap_row = wrap_snapshot
.make_wrap_point(Point::new(excerpt_boundary.row.0, 0), Bias::Left)
.row();
[
show_excerpt_controls
.then(|| {
excerpt_boundary.prev.as_ref().map(|prev| {
(
wrap_row,
TransformBlock::ExcerptFooter {
id: prev.id,
height: excerpt_footer_height,
disposition: if excerpt_boundary.next.is_some() {
BlockDisposition::Above
} else {
BlockDisposition::Below
},
},
)
})
})
.flatten(),
excerpt_boundary.next.map(|next| {
let starts_new_buffer = excerpt_boundary
.prev
.map_or(true, |prev| prev.buffer_id != next.buffer_id);
(
wrap_row,
TransformBlock::ExcerptHeader {
id: next.id,
buffer: next.buffer,
range: next.range,
height: if starts_new_buffer {
buffer_header_height
} else {
excerpt_header_height
},
starts_new_buffer,
show_excerpt_controls,
},
)
}),
]
})
.flatten()
}
pub(crate) fn sort_blocks<B: BlockLike>(blocks: &mut Vec<(u32, B)>) {
// Place excerpt headers and footers above custom blocks on the same row
blocks.sort_unstable_by(|(row_a, block_a), (row_b, block_b)| {
row_a.cmp(row_b).then_with(|| {
block_a
.disposition()
.cmp(&block_b.disposition())
.then_with(|| match ((block_a.block_type()), (block_b.block_type())) {
(BlockType::Footer, BlockType::Footer) => Ordering::Equal,
(BlockType::Footer, _) => Ordering::Less,
(_, BlockType::Footer) => Ordering::Greater,
(BlockType::Header, BlockType::Header) => Ordering::Equal,
(BlockType::Header, _) => Ordering::Less,
(_, BlockType::Header) => Ordering::Greater,
(BlockType::Custom(a_id), BlockType::Custom(b_id)) => a_id.cmp(&b_id),
})
})
});
}
}
fn push_isomorphic(tree: &mut SumTree<Transform>, rows: u32) {
@@ -875,7 +989,7 @@ impl<'a> Iterator for BlockChunks<'a> {
Some(Chunk {
text: prefix,
..self.input_chunk
..self.input_chunk.clone()
})
}
}
@@ -996,6 +1110,8 @@ fn offset_for_row(s: &str, target: u32) -> (u32, usize) {
#[cfg(test)]
mod tests {
use std::env;
use super::*;
use crate::display_map::inlay_map::InlayMap;
use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap};
@@ -1003,7 +1119,6 @@ mod tests {
use multi_buffer::MultiBuffer;
use rand::prelude::*;
use settings::SettingsStore;
use std::env;
use util::RandomCharIter;
#[gpui::test]
@@ -1034,7 +1149,7 @@ mod tests {
let (mut 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(), 1, 1);
let mut block_map = BlockMap::new(wraps_snapshot.clone(), true, 1, 1, 1);
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
let block_ids = writer.insert(vec![
@@ -1206,7 +1321,7 @@ mod tests {
let (_, wraps_snapshot) = cx.update(|cx| {
WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), Some(px(60.)), cx)
});
let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
let mut block_map = BlockMap::new(wraps_snapshot.clone(), true, 1, 1, 0);
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
writer.insert(vec![
@@ -1252,9 +1367,11 @@ mod tests {
let font_size = px(14.0);
let buffer_start_header_height = rng.gen_range(1..=5);
let excerpt_header_height = rng.gen_range(1..=5);
let excerpt_footer_height = rng.gen_range(1..=5);
log::info!("Wrap width: {:?}", wrap_width);
log::info!("Excerpt Header Height: {:?}", excerpt_header_height);
log::info!("Excerpt Footer Height: {:?}", excerpt_footer_height);
let buffer = if rng.gen() {
let len = rng.gen_range(0..10);
@@ -1273,8 +1390,10 @@ mod tests {
.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), font_size, wrap_width, cx));
let mut block_map = BlockMap::new(
wraps_snapshot,
true,
buffer_start_header_height,
excerpt_header_height,
excerpt_footer_height,
);
let mut custom_blocks = Vec::new();
@@ -1410,24 +1529,23 @@ mod tests {
},
)
}));
expected_blocks.extend(buffer_snapshot.excerpt_boundaries_in_range(0..).map(
|boundary| {
let position =
wraps_snapshot.make_wrap_point(Point::new(boundary.row.0, 0), Bias::Left);
(
position.row(),
ExpectedBlock::ExcerptHeader {
height: if boundary.starts_new_buffer {
buffer_start_header_height
} else {
excerpt_header_height
},
starts_new_buffer: boundary.starts_new_buffer,
},
)
},
));
expected_blocks.sort_unstable();
// Note that this needs to be synced with the related section in BlockMap::sync
expected_blocks.extend(
BlockMap::header_blocks(
true,
excerpt_footer_height,
buffer_start_header_height,
excerpt_header_height,
&buffer_snapshot,
0..,
&wraps_snapshot,
)
.map(|(row, block)| (row, block.into())),
);
BlockMap::sort_blocks(&mut expected_blocks);
let mut sorted_blocks_iter = expected_blocks.into_iter().peekable();
let input_buffer_rows = buffer_snapshot
@@ -1593,12 +1711,16 @@ mod tests {
}
}
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
#[derive(Debug, Eq, PartialEq)]
enum ExpectedBlock {
ExcerptHeader {
height: u8,
starts_new_buffer: bool,
},
ExcerptFooter {
height: u8,
disposition: BlockDisposition,
},
Custom {
disposition: BlockDisposition,
id: BlockId,
@@ -1606,11 +1728,26 @@ mod tests {
},
}
impl BlockLike for ExpectedBlock {
fn block_type(&self) -> BlockType {
match self {
ExpectedBlock::Custom { id, .. } => BlockType::Custom(*id),
ExpectedBlock::ExcerptHeader { .. } => BlockType::Header,
ExpectedBlock::ExcerptFooter { .. } => BlockType::Footer,
}
}
fn disposition(&self) -> BlockDisposition {
self.disposition()
}
}
impl ExpectedBlock {
fn height(&self) -> u8 {
match self {
ExpectedBlock::ExcerptHeader { height, .. } => *height,
ExpectedBlock::Custom { height, .. } => *height,
ExpectedBlock::ExcerptFooter { height, .. } => *height,
}
}
@@ -1618,6 +1755,7 @@ mod tests {
match self {
ExpectedBlock::ExcerptHeader { .. } => BlockDisposition::Above,
ExpectedBlock::Custom { disposition, .. } => *disposition,
ExpectedBlock::ExcerptFooter { disposition, .. } => *disposition,
}
}
}
@@ -1638,6 +1776,14 @@ mod tests {
height,
starts_new_buffer,
},
TransformBlock::ExcerptFooter {
height,
disposition,
..
} => ExpectedBlock::ExcerptFooter {
height,
disposition,
},
}
}
}
@@ -1654,6 +1800,7 @@ mod tests {
match self {
TransformBlock::Custom(block) => Some(block),
TransformBlock::ExcerptHeader { .. } => None,
TransformBlock::ExcerptFooter { .. } => None,
}
}
}

View File

@@ -6,6 +6,8 @@ use sum_tree::{Bias, SeekTarget, SumTree};
use text::Point;
use ui::WindowContext;
use crate::FoldPlaceholder;
#[derive(Copy, Clone, Default, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)]
pub struct FlapId(usize);
@@ -34,7 +36,13 @@ impl FlapSnapshot {
while let Some(item) = cursor.item() {
match Ord::cmp(&item.flap.range.start.to_point(snapshot).row, &row.0) {
Ordering::Less => cursor.next(snapshot),
Ordering::Equal => return Some(&item.flap),
Ordering::Equal => {
if item.flap.range.start.is_valid(snapshot) {
return Some(&item.flap);
} else {
cursor.next(snapshot);
}
}
Ordering::Greater => break,
}
}
@@ -76,6 +84,7 @@ type RenderTrailerFn =
#[derive(Clone)]
pub struct Flap {
pub range: Range<Anchor>,
pub placeholder: FoldPlaceholder,
pub render_toggle: RenderToggleFn,
pub render_trailer: RenderTrailerFn,
}
@@ -83,6 +92,7 @@ pub struct Flap {
impl Flap {
pub fn new<RenderToggle, ToggleElement, RenderTrailer, TrailerElement>(
range: Range<Anchor>,
placeholder: FoldPlaceholder,
render_toggle: RenderToggle,
render_trailer: RenderTrailer,
) -> Self
@@ -107,6 +117,7 @@ impl Flap {
{
Flap {
range,
placeholder,
render_toggle: Arc::new(move |row, folded, toggle, cx| {
render_toggle(row, folded, toggle, cx).into_any_element()
}),
@@ -256,11 +267,13 @@ mod test {
let flaps = [
Flap::new(
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(1, 5)),
FoldPlaceholder::test(),
|_row, _folded, _toggle, _cx| div(),
|_row, _folded, _cx| div(),
),
Flap::new(
snapshot.anchor_before(Point::new(3, 0))..snapshot.anchor_after(Point::new(3, 5)),
FoldPlaceholder::test(),
|_row, _folded, _toggle, _cx| div(),
|_row, _folded, _cx| div(),
),

View File

@@ -2,17 +2,57 @@ use super::{
inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot},
Highlights,
};
use gpui::{ElementId, HighlightStyle, Hsla};
use language::{Chunk, Edit, Point, TextSummary};
use gpui::{AnyElement, ElementId, WindowContext};
use language::{Chunk, ChunkRenderer, Edit, Point, TextSummary};
use multi_buffer::{Anchor, AnchorRangeExt, MultiBufferRow, MultiBufferSnapshot, ToOffset};
use std::{
cmp::{self, Ordering},
iter,
fmt, iter,
ops::{Add, AddAssign, Deref, DerefMut, Range, Sub},
sync::Arc,
};
use sum_tree::{Bias, Cursor, FilterCursor, SumTree};
use util::post_inc;
#[derive(Clone)]
pub struct FoldPlaceholder {
/// Creates an element to represent this fold's placeholder.
pub render: Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut WindowContext) -> AnyElement>,
/// If true, the element is constrained to the shaped width of an ellipsis.
pub constrain_width: bool,
/// If true, merges the fold with an adjacent one.
pub merge_adjacent: bool,
}
impl FoldPlaceholder {
#[cfg(any(test, feature = "test-support"))]
pub fn test() -> Self {
use gpui::IntoElement;
Self {
render: Arc::new(|_id, _range, _cx| gpui::Empty.into_any_element()),
constrain_width: true,
merge_adjacent: true,
}
}
}
impl fmt::Debug for FoldPlaceholder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("FoldPlaceholder")
.field("constrain_width", &self.constrain_width)
.finish()
}
}
impl Eq for FoldPlaceholder {}
impl PartialEq for FoldPlaceholder {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.render, &other.render) && self.constrain_width == other.constrain_width
}
}
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
pub struct FoldPoint(pub Point);
@@ -54,7 +94,7 @@ impl FoldPoint {
let mut offset = cursor.start().1.output.len;
if !overshoot.is_zero() {
let transform = cursor.item().expect("display point out of range");
assert!(transform.output_text.is_none());
assert!(transform.placeholder.is_none());
let end_inlay_offset = snapshot
.inlay_snapshot
.to_offset(InlayPoint(cursor.start().1.input.lines + overshoot));
@@ -75,7 +115,7 @@ pub(crate) struct FoldMapWriter<'a>(&'a mut FoldMap);
impl<'a> FoldMapWriter<'a> {
pub(crate) fn fold<T: ToOffset>(
&mut self,
ranges: impl IntoIterator<Item = (Range<T>, &'static str)>,
ranges: impl IntoIterator<Item = (Range<T>, FoldPlaceholder)>,
) -> (FoldSnapshot, Vec<FoldEdit>) {
let mut edits = Vec::new();
let mut folds = Vec::new();
@@ -99,7 +139,7 @@ impl<'a> FoldMapWriter<'a> {
folds.push(Fold {
id: FoldId(post_inc(&mut self.0.next_fold_id.0)),
range: fold_range,
text: fold_text,
placeholder: fold_text,
});
let inlay_range =
@@ -183,7 +223,6 @@ impl<'a> FoldMapWriter<'a> {
/// See the [`display_map` module documentation](crate::display_map) for more information.
pub(crate) struct FoldMap {
snapshot: FoldSnapshot,
ellipses_color: Option<Hsla>,
next_fold_id: FoldId,
}
@@ -198,15 +237,13 @@ impl FoldMap {
input: inlay_snapshot.text_summary(),
output: inlay_snapshot.text_summary(),
},
output_text: None,
placeholder: None,
},
&(),
),
inlay_snapshot: inlay_snapshot.clone(),
version: 0,
ellipses_color: None,
},
ellipses_color: None,
next_fold_id: FoldId::default(),
};
let snapshot = this.snapshot.clone();
@@ -232,15 +269,6 @@ impl FoldMap {
(FoldMapWriter(self), snapshot, edits)
}
pub fn set_ellipses_color(&mut self, color: Hsla) -> bool {
if self.ellipses_color == Some(color) {
false
} else {
self.ellipses_color = Some(color);
true
}
}
fn check_invariants(&self) {
if cfg!(test) {
assert_eq!(
@@ -329,9 +357,9 @@ impl FoldMap {
let buffer_start = fold.range.start.to_offset(&inlay_snapshot.buffer);
let buffer_end = fold.range.end.to_offset(&inlay_snapshot.buffer);
(
fold.clone(),
inlay_snapshot.to_inlay_offset(buffer_start)
..inlay_snapshot.to_inlay_offset(buffer_end),
fold.text,
)
});
folds_cursor.next(&inlay_snapshot.buffer);
@@ -342,17 +370,20 @@ impl FoldMap {
while folds
.peek()
.map_or(false, |(fold_range, _)| fold_range.start < edit.new.end)
.map_or(false, |(_, fold_range)| fold_range.start < edit.new.end)
{
let (mut fold_range, fold_text) = folds.next().unwrap();
let (fold, mut fold_range) = folds.next().unwrap();
let sum = new_transforms.summary();
assert!(fold_range.start.0 >= sum.input.len);
while folds.peek().map_or(false, |(next_fold_range, _)| {
next_fold_range.start <= fold_range.end
while folds.peek().map_or(false, |(next_fold, next_fold_range)| {
next_fold_range.start < fold_range.end
|| (next_fold_range.start == fold_range.end
&& fold.placeholder.merge_adjacent
&& next_fold.placeholder.merge_adjacent)
}) {
let (next_fold_range, _) = folds.next().unwrap();
let (_, next_fold_range) = folds.next().unwrap();
if next_fold_range.end > fold_range.end {
fold_range.end = next_fold_range.end;
}
@@ -367,21 +398,36 @@ impl FoldMap {
output: text_summary.clone(),
input: text_summary,
},
output_text: None,
placeholder: None,
},
&(),
);
}
if fold_range.end > fold_range.start {
const ELLIPSIS: &'static str = "";
let fold_id = fold.id;
new_transforms.push(
Transform {
summary: TransformSummary {
output: TextSummary::from(fold_text),
output: TextSummary::from(ELLIPSIS),
input: inlay_snapshot
.text_summary_for_range(fold_range.start..fold_range.end),
},
output_text: Some(fold_text),
placeholder: Some(TransformPlaceholder {
text: ELLIPSIS,
renderer: ChunkRenderer {
render: Arc::new(move |cx| {
(fold.placeholder.render)(
fold_id,
fold.range.0.clone(),
cx,
)
}),
constrain_width: fold.placeholder.constrain_width,
},
}),
},
&(),
);
@@ -398,7 +444,7 @@ impl FoldMap {
output: text_summary.clone(),
input: text_summary,
},
output_text: None,
placeholder: None,
},
&(),
);
@@ -414,7 +460,7 @@ impl FoldMap {
output: text_summary.clone(),
input: text_summary,
},
output_text: None,
placeholder: None,
},
&(),
);
@@ -484,7 +530,6 @@ pub struct FoldSnapshot {
folds: SumTree<Fold>,
pub inlay_snapshot: InlaySnapshot,
pub version: usize,
pub ellipses_color: Option<Hsla>,
}
impl FoldSnapshot {
@@ -508,9 +553,9 @@ impl FoldSnapshot {
if let Some(transform) = cursor.item() {
let start_in_transform = range.start.0 - cursor.start().0 .0;
let end_in_transform = cmp::min(range.end, cursor.end(&()).0).0 - cursor.start().0 .0;
if let Some(output_text) = transform.output_text {
if let Some(placeholder) = transform.placeholder.as_ref() {
summary = TextSummary::from(
&output_text
&placeholder.text
[start_in_transform.column as usize..end_in_transform.column as usize],
);
} else {
@@ -533,8 +578,9 @@ impl FoldSnapshot {
.output;
if let Some(transform) = cursor.item() {
let end_in_transform = range.end.0 - cursor.start().0 .0;
if let Some(output_text) = transform.output_text {
summary += TextSummary::from(&output_text[..end_in_transform.column as usize]);
if let Some(placeholder) = transform.placeholder.as_ref() {
summary +=
TextSummary::from(&placeholder.text[..end_in_transform.column as usize]);
} else {
let inlay_start = self.inlay_snapshot.to_offset(cursor.start().1);
let inlay_end = self
@@ -631,7 +677,7 @@ impl FoldSnapshot {
let inlay_offset = self.inlay_snapshot.to_inlay_offset(buffer_offset);
let mut cursor = self.transforms.cursor::<InlayOffset>();
cursor.seek(&inlay_offset, Bias::Right, &());
cursor.item().map_or(false, |t| t.output_text.is_some())
cursor.item().map_or(false, |t| t.placeholder.is_some())
}
pub fn is_line_folded(&self, buffer_row: MultiBufferRow) -> bool {
@@ -646,7 +692,7 @@ impl FoldSnapshot {
let buffer_point = self.inlay_snapshot.to_buffer_point(inlay_point);
if buffer_point.row != buffer_row.0 {
return false;
} else if transform.output_text.is_some() {
} else if transform.placeholder.is_some() {
return true;
}
}
@@ -693,7 +739,6 @@ impl FoldSnapshot {
inlay_offset: inlay_start,
output_offset: range.start.0,
max_output_offset: range.end.0,
ellipses_color: self.ellipses_color,
}
}
@@ -720,7 +765,7 @@ impl FoldSnapshot {
cursor.seek(&point, Bias::Right, &());
if let Some(transform) = cursor.item() {
let transform_start = cursor.start().0 .0;
if transform.output_text.is_some() {
if transform.placeholder.is_some() {
if point.0 == transform_start || matches!(bias, Bias::Left) {
FoldPoint(transform_start)
} else {
@@ -810,15 +855,21 @@ fn consolidate_fold_edits(edits: &mut Vec<FoldEdit>) {
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
#[derive(Clone, Debug, Default)]
struct Transform {
summary: TransformSummary,
output_text: Option<&'static str>,
placeholder: Option<TransformPlaceholder>,
}
#[derive(Clone, Debug)]
struct TransformPlaceholder {
text: &'static str,
renderer: ChunkRenderer,
}
impl Transform {
fn is_fold(&self) -> bool {
self.output_text.is_some()
self.placeholder.is_some()
}
}
@@ -858,7 +909,7 @@ impl Into<ElementId> for FoldId {
pub struct Fold {
pub id: FoldId,
pub range: FoldRange,
pub text: &'static str,
pub placeholder: FoldPlaceholder,
}
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -1004,7 +1055,6 @@ pub struct FoldChunks<'a> {
inlay_offset: InlayOffset,
output_offset: usize,
max_output_offset: usize,
ellipses_color: Option<Hsla>,
}
impl<'a> Iterator for FoldChunks<'a> {
@@ -1019,7 +1069,7 @@ impl<'a> Iterator for FoldChunks<'a> {
// If we're in a fold, then return the fold's display text and
// advance the transform and buffer cursors to the end of the fold.
if let Some(output_text) = transform.output_text {
if let Some(placeholder) = transform.placeholder.as_ref() {
self.inlay_chunk.take();
self.inlay_offset += InlayOffset(transform.summary.input.len);
self.inlay_chunks.seek(self.inlay_offset);
@@ -1030,13 +1080,10 @@ impl<'a> Iterator for FoldChunks<'a> {
self.transform_cursor.next(&());
}
self.output_offset += output_text.len();
self.output_offset += placeholder.text.len();
return Some(Chunk {
text: output_text,
highlight_style: self.ellipses_color.map(|color| HighlightStyle {
color: Some(color),
..Default::default()
}),
text: placeholder.text,
renderer: Some(placeholder.renderer.clone()),
..Default::default()
});
}
@@ -1048,7 +1095,7 @@ impl<'a> Iterator for FoldChunks<'a> {
}
// Otherwise, take a chunk from the buffer's text.
if let Some((buffer_chunk_start, mut chunk)) = self.inlay_chunk {
if let Some((buffer_chunk_start, mut chunk)) = self.inlay_chunk.clone() {
let buffer_chunk_end = buffer_chunk_start + InlayOffset(chunk.text.len());
let transform_end = self.transform_cursor.end(&()).1;
let chunk_end = buffer_chunk_end.min(transform_end);
@@ -1165,8 +1212,8 @@ mod tests {
let (mut writer, _, _) = map.write(inlay_snapshot, vec![]);
let (snapshot2, edits) = writer.fold(vec![
(Point::new(0, 2)..Point::new(2, 2), ""),
(Point::new(2, 4)..Point::new(4, 1), ""),
(Point::new(0, 2)..Point::new(2, 2), FoldPlaceholder::test()),
(Point::new(2, 4)..Point::new(4, 1), FoldPlaceholder::test()),
]);
assert_eq!(snapshot2.text(), "aa⋯cc⋯eeeee");
assert_eq!(
@@ -1245,19 +1292,25 @@ mod tests {
let mut map = FoldMap::new(inlay_snapshot.clone()).0;
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
writer.fold(vec![(5..8, "")]);
writer.fold(vec![(5..8, FoldPlaceholder::test())]);
let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
assert_eq!(snapshot.text(), "abcde⋯ijkl");
// Create an fold adjacent to the start of the first fold.
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
writer.fold(vec![(0..1, ""), (2..5, "")]);
writer.fold(vec![
(0..1, FoldPlaceholder::test()),
(2..5, FoldPlaceholder::test()),
]);
let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
assert_eq!(snapshot.text(), "⋯b⋯ijkl");
// Create an fold adjacent to the end of the first fold.
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
writer.fold(vec![(11..11, ""), (8..10, "")]);
writer.fold(vec![
(11..11, FoldPlaceholder::test()),
(8..10, FoldPlaceholder::test()),
]);
let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
assert_eq!(snapshot.text(), "⋯b⋯kl");
}
@@ -1267,7 +1320,10 @@ mod tests {
// Create two adjacent folds.
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
writer.fold(vec![(0..2, ""), (2..5, "")]);
writer.fold(vec![
(0..2, FoldPlaceholder::test()),
(2..5, FoldPlaceholder::test()),
]);
let (snapshot, _) = map.read(inlay_snapshot, vec![]);
assert_eq!(snapshot.text(), "⋯fghijkl");
@@ -1291,10 +1347,10 @@ mod tests {
let mut map = FoldMap::new(inlay_snapshot.clone()).0;
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
writer.fold(vec![
(Point::new(0, 2)..Point::new(2, 2), ""),
(Point::new(0, 4)..Point::new(1, 0), ""),
(Point::new(1, 2)..Point::new(3, 2), ""),
(Point::new(3, 1)..Point::new(4, 1), ""),
(Point::new(0, 2)..Point::new(2, 2), FoldPlaceholder::test()),
(Point::new(0, 4)..Point::new(1, 0), FoldPlaceholder::test()),
(Point::new(1, 2)..Point::new(3, 2), FoldPlaceholder::test()),
(Point::new(3, 1)..Point::new(4, 1), FoldPlaceholder::test()),
]);
let (snapshot, _) = map.read(inlay_snapshot, vec![]);
assert_eq!(snapshot.text(), "aa⋯eeeee");
@@ -1311,8 +1367,8 @@ mod tests {
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
writer.fold(vec![
(Point::new(0, 2)..Point::new(2, 2), ""),
(Point::new(3, 1)..Point::new(4, 1), ""),
(Point::new(0, 2)..Point::new(2, 2), FoldPlaceholder::test()),
(Point::new(3, 1)..Point::new(4, 1), FoldPlaceholder::test()),
]);
let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
assert_eq!(snapshot.text(), "aa⋯cccc\nd⋯eeeee");
@@ -1336,10 +1392,10 @@ mod tests {
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
writer.fold(vec![
(Point::new(0, 2)..Point::new(2, 2), ""),
(Point::new(0, 4)..Point::new(1, 0), ""),
(Point::new(1, 2)..Point::new(3, 2), ""),
(Point::new(3, 1)..Point::new(4, 1), ""),
(Point::new(0, 2)..Point::new(2, 2), FoldPlaceholder::test()),
(Point::new(0, 4)..Point::new(1, 0), FoldPlaceholder::test()),
(Point::new(1, 2)..Point::new(3, 2), FoldPlaceholder::test()),
(Point::new(3, 1)..Point::new(4, 1), FoldPlaceholder::test()),
]);
let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
let fold_ranges = snapshot
@@ -1414,10 +1470,10 @@ mod tests {
snapshot_edits.push((snapshot.clone(), edits));
let mut expected_text: String = inlay_snapshot.text().to_string();
for (fold_range, fold_text) in map.merged_folds().into_iter().rev() {
for fold_range in map.merged_folds().into_iter().rev() {
let fold_inlay_start = inlay_snapshot.to_inlay_offset(fold_range.start);
let fold_inlay_end = inlay_snapshot.to_inlay_offset(fold_range.end);
expected_text.replace_range(fold_inlay_start.0..fold_inlay_end.0, fold_text);
expected_text.replace_range(fold_inlay_start.0..fold_inlay_end.0, "");
}
assert_eq!(snapshot.text(), expected_text);
@@ -1429,7 +1485,7 @@ mod tests {
let mut prev_row = 0;
let mut expected_buffer_rows = Vec::new();
for (fold_range, _fold_text) in map.merged_folds().into_iter() {
for fold_range in map.merged_folds() {
let fold_start = inlay_snapshot
.to_point(inlay_snapshot.to_inlay_offset(fold_range.start))
.row();
@@ -1543,7 +1599,7 @@ mod tests {
let folded_buffer_rows = map
.merged_folds()
.iter()
.flat_map(|(fold_range, _)| {
.flat_map(|fold_range| {
let start_row = fold_range.start.to_point(&buffer_snapshot).row;
let end = fold_range.end.to_point(&buffer_snapshot);
if end.column == 0 {
@@ -1640,8 +1696,8 @@ mod tests {
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
writer.fold(vec![
(Point::new(0, 2)..Point::new(2, 2), ""),
(Point::new(3, 1)..Point::new(4, 1), ""),
(Point::new(0, 2)..Point::new(2, 2), FoldPlaceholder::test()),
(Point::new(3, 1)..Point::new(4, 1), FoldPlaceholder::test()),
]);
let (snapshot, _) = map.read(inlay_snapshot, vec![]);
@@ -1659,7 +1715,7 @@ mod tests {
}
impl FoldMap {
fn merged_folds(&self) -> Vec<(Range<usize>, &'static str)> {
fn merged_folds(&self) -> Vec<Range<usize>> {
let inlay_snapshot = self.snapshot.inlay_snapshot.clone();
let buffer = &inlay_snapshot.buffer;
let mut folds = self.snapshot.folds.items(buffer);
@@ -1667,17 +1723,12 @@ mod tests {
folds.sort_by(|a, b| a.range.cmp(&b.range, buffer));
let mut folds = folds
.iter()
.map(|fold| {
(
fold.range.start.to_offset(buffer)..fold.range.end.to_offset(buffer),
fold.text,
)
})
.map(|fold| fold.range.start.to_offset(buffer)..fold.range.end.to_offset(buffer))
.peekable();
let mut merged_folds = Vec::new();
while let Some((mut fold_range, fold_text)) = folds.next() {
while let Some((next_range, _)) = folds.peek() {
while let Some(mut fold_range) = folds.next() {
while let Some(next_range) = folds.peek() {
if fold_range.end >= next_range.start {
if next_range.end > fold_range.end {
fold_range.end = next_range.end;
@@ -1688,7 +1739,7 @@ mod tests {
}
}
if fold_range.end > fold_range.start {
merged_folds.push((fold_range, fold_text));
merged_folds.push(fold_range);
}
}
merged_folds
@@ -1723,8 +1774,7 @@ mod tests {
for _ in 0..rng.gen_range(1..=2) {
let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right);
let start = buffer.clip_offset(rng.gen_range(0..=end), Left);
let text = if rng.gen() { "" } else { "" };
to_fold.push((start..end, text));
to_fold.push((start..end, FoldPlaceholder::test()));
}
log::info!("folding {:?}", to_fold);
let (mut writer, snapshot, edits) = self.write(inlay_snapshot, vec![]);

View File

@@ -284,7 +284,7 @@ impl<'a> Iterator for InlayChunks<'a> {
self.output_offset.0 += prefix.len();
let mut prefix = Chunk {
text: prefix,
..*chunk
..chunk.clone()
};
if !self.active_highlights.is_empty() {
let mut highlight_style = HighlightStyle::default();

View File

@@ -508,7 +508,7 @@ impl<'a> Iterator for TabChunks<'a> {
self.chunk.text = suffix;
return Some(Chunk {
text: prefix,
..self.chunk
..self.chunk.clone()
});
} else {
self.chunk.text = &self.chunk.text[1..];
@@ -529,7 +529,7 @@ impl<'a> Iterator for TabChunks<'a> {
return Some(Chunk {
text: &SPACES[..len as usize],
is_tab: true,
..self.chunk
..self.chunk.clone()
});
}
}

View File

@@ -812,7 +812,7 @@ impl<'a> Iterator for WrapChunks<'a> {
self.transforms.next(&());
return Some(Chunk {
text: &display_text[start_ix..end_ix],
..self.input_chunk
..self.input_chunk.clone()
});
}
@@ -842,7 +842,7 @@ impl<'a> Iterator for WrapChunks<'a> {
self.input_chunk.text = suffix;
Some(Chunk {
text: prefix,
..self.input_chunk
..self.input_chunk.clone()
})
}
}

View File

@@ -26,6 +26,7 @@ mod git;
mod highlight_matching_bracket;
mod hover_links;
mod hover_popover;
mod indent_guides;
mod inline_completion_provider;
pub mod items;
mod mouse_context_menu;
@@ -51,8 +52,8 @@ use clock::ReplicaId;
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
use convert_case::{Case, Casing};
use debounced_delay::DebouncedDelay;
pub use display_map::DisplayPoint;
use display_map::*;
pub use display_map::{DisplayPoint, FoldPlaceholder};
use editor_settings::CurrentLineHighlight;
pub use editor_settings::EditorSettings;
use element::LineWithInvisibles;
@@ -76,6 +77,7 @@ use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_popover::{hide_hover, HoverState};
use hunk_diff::ExpandedHunks;
pub(crate) use hunk_diff::HunkToExpand;
use indent_guides::ActiveIndentGuidesState;
use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
pub use inline_completion_provider::*;
pub use items::MAX_TAB_TITLE_LEN;
@@ -98,7 +100,7 @@ pub use multi_buffer::{
Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset,
ToPoint,
};
use multi_buffer::{MultiBufferPoint, MultiBufferRow, ToOffsetUtf16};
use multi_buffer::{ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow, ToOffsetUtf16};
use ordered_float::OrderedFloat;
use parking_lot::{Mutex, RwLock};
use project::project_settings::{GitGutterSetting, ProjectSettings};
@@ -404,9 +406,13 @@ impl Default for ScrollbarMarkerState {
#[derive(Clone, Debug)]
struct RunnableTasks {
templates: Vec<(TaskSourceKind, TaskTemplate)>,
// We need the column at which the task context evaluation should take place.
offset: MultiBufferOffset,
// We need the column at which the task context evaluation should take place (when we're spawning it via gutter).
column: u32,
// Values of all named captures, including those starting with '_'
extra_variables: HashMap<String, String>,
// Full range of the tagged region. We use it to determine which `extra_variables` to grab for context resolution in e.g. a modal.
context_range: Range<BufferOffset>,
}
#[derive(Clone)]
@@ -414,7 +420,10 @@ struct ResolvedTasks {
templates: SmallVec<[(TaskSourceKind, ResolvedTask); 1]>,
position: Anchor,
}
#[derive(Copy, Clone, Debug)]
struct MultiBufferOffset(usize);
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
struct BufferOffset(usize);
/// Zed's primary text input `View`, allowing users to edit a [`MultiBuffer`]
///
/// See the [module level documentation](self) for more information.
@@ -448,12 +457,17 @@ pub struct Editor {
mode: EditorMode,
show_breadcrumbs: bool,
show_gutter: bool,
show_line_numbers: Option<bool>,
show_git_diff_gutter: Option<bool>,
show_code_actions: Option<bool>,
show_wrap_guides: Option<bool>,
show_indent_guides: Option<bool>,
placeholder_text: Option<Arc<str>>,
highlight_order: usize,
highlighted_rows: HashMap<TypeId, Vec<RowHighlight>>,
background_highlights: TreeMap<TypeId, BackgroundHighlight>,
scrollbar_marker_state: ScrollbarMarkerState,
active_indent_guides_state: ActiveIndentGuidesState,
nav_history: Option<ItemNavHistory>,
context_menu: RwLock<Option<ContextMenu>>,
mouse_context_menu: Option<MouseContextMenu>,
@@ -470,7 +484,7 @@ pub struct Editor {
current_line_highlight: CurrentLineHighlight,
collapse_matches: bool,
autoindent_mode: Option<AutoindentMode>,
workspace: Option<(WeakView<Workspace>, WorkspaceId)>,
workspace: Option<(WeakView<Workspace>, Option<WorkspaceId>)>,
keymap_context_layers: BTreeMap<TypeId, KeyContext>,
input_enabled: bool,
use_modal_editing: bool,
@@ -508,7 +522,7 @@ pub struct Editor {
>,
last_bounds: Option<Bounds<Pixels>>,
expect_bounds_change: Option<Bounds<Pixels>>,
tasks: HashMap<(BufferId, BufferRow), (usize, RunnableTasks)>,
tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>,
tasks_update_task: Option<Task<()>>,
}
@@ -516,6 +530,9 @@ pub struct Editor {
pub struct EditorSnapshot {
pub mode: EditorMode,
show_gutter: bool,
show_line_numbers: Option<bool>,
show_git_diff_gutter: Option<bool>,
show_code_actions: Option<bool>,
render_git_blame_gutter: bool,
pub display_snapshot: DisplaySnapshot,
pub placeholder_text: Option<Arc<str>>,
@@ -1512,19 +1529,25 @@ impl Editor {
pub fn single_line(cx: &mut ViewContext<Self>) -> Self {
let buffer = cx.new_model(|cx| Buffer::local("", cx));
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
Self::new(EditorMode::SingleLine, buffer, None, cx)
Self::new(EditorMode::SingleLine, buffer, None, false, cx)
}
pub fn multi_line(cx: &mut ViewContext<Self>) -> Self {
let buffer = cx.new_model(|cx| Buffer::local("", cx));
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
Self::new(EditorMode::Full, buffer, None, cx)
Self::new(EditorMode::Full, buffer, None, false, cx)
}
pub fn auto_height(max_lines: usize, cx: &mut ViewContext<Self>) -> Self {
let buffer = cx.new_model(|cx| Buffer::local("", cx));
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
Self::new(EditorMode::AutoHeight { max_lines }, buffer, None, cx)
Self::new(
EditorMode::AutoHeight { max_lines },
buffer,
None,
false,
cx,
)
}
pub fn for_buffer(
@@ -1533,19 +1556,27 @@ impl Editor {
cx: &mut ViewContext<Self>,
) -> Self {
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
Self::new(EditorMode::Full, buffer, project, cx)
Self::new(EditorMode::Full, buffer, project, false, cx)
}
pub fn for_multibuffer(
buffer: Model<MultiBuffer>,
project: Option<Model<Project>>,
show_excerpt_controls: bool,
cx: &mut ViewContext<Self>,
) -> Self {
Self::new(EditorMode::Full, buffer, project, cx)
Self::new(EditorMode::Full, buffer, project, show_excerpt_controls, cx)
}
pub fn clone(&self, cx: &mut ViewContext<Self>) -> Self {
let mut clone = Self::new(self.mode, self.buffer.clone(), self.project.clone(), cx);
let show_excerpt_controls = self.display_map.read(cx).show_excerpt_controls();
let mut clone = Self::new(
self.mode,
self.buffer.clone(),
self.project.clone(),
show_excerpt_controls,
cx,
);
self.display_map.update(cx, |display_map, cx| {
let snapshot = display_map.snapshot(cx);
clone.display_map.update(cx, |display_map, cx| {
@@ -1562,12 +1593,58 @@ impl Editor {
mode: EditorMode,
buffer: Model<MultiBuffer>,
project: Option<Model<Project>>,
show_excerpt_controls: bool,
cx: &mut ViewContext<Self>,
) -> Self {
let style = cx.text_style();
let font_size = style.font_size.to_pixels(cx.rem_size());
let editor = cx.view().downgrade();
let fold_placeholder = FoldPlaceholder {
constrain_width: true,
render: Arc::new(move |fold_id, fold_range, cx| {
let editor = editor.clone();
div()
.id(fold_id)
.bg(cx.theme().colors().ghost_element_background)
.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
.active(|style| style.bg(cx.theme().colors().ghost_element_active))
.rounded_sm()
.size_full()
.cursor_pointer()
.child("")
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
.on_click(move |_, cx| {
editor
.update(cx, |editor, cx| {
editor.unfold_ranges(
[fold_range.start..fold_range.end],
true,
false,
cx,
);
cx.stop_propagation();
})
.ok();
})
.into_any()
}),
merge_adjacent: true,
};
let display_map = cx.new_model(|cx| {
DisplayMap::new(buffer.clone(), style.font(), font_size, None, 2, 1, cx)
let file_header_size = if show_excerpt_controls { 3 } else { 2 };
DisplayMap::new(
buffer.clone(),
style.font(),
font_size,
None,
show_excerpt_controls,
file_header_size,
1,
1,
fold_placeholder,
cx,
)
});
let selections = SelectionsCollection::new(display_map.clone(), buffer.clone());
@@ -1645,12 +1722,17 @@ impl Editor {
mode,
show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs,
show_gutter: mode == EditorMode::Full,
show_line_numbers: None,
show_git_diff_gutter: None,
show_code_actions: None,
show_wrap_guides: None,
show_indent_guides: None,
placeholder_text: None,
highlight_order: 0,
highlighted_rows: HashMap::default(),
background_highlights: Default::default(),
scrollbar_marker_state: ScrollbarMarkerState::default(),
active_indent_guides_state: ActiveIndentGuidesState::default(),
nav_history: None,
context_menu: RwLock::new(None),
mouse_context_menu: None,
@@ -1880,6 +1962,9 @@ impl Editor {
EditorSnapshot {
mode: self.mode,
show_gutter: self.show_gutter,
show_line_numbers: self.show_line_numbers,
show_git_diff_gutter: self.show_git_diff_gutter,
show_code_actions: self.show_code_actions,
render_git_blame_gutter: self.render_git_blame_gutter(cx),
display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)),
scroll_anchor: self.scroll_manager.anchor(),
@@ -1932,8 +2017,8 @@ impl Editor {
self.custom_context_menu = Some(Box::new(f))
}
pub fn set_completion_provider(&mut self, hub: Box<dyn CompletionProvider>) {
self.completion_provider = Some(hub);
pub fn set_completion_provider(&mut self, provider: Box<dyn CompletionProvider>) {
self.completion_provider = Some(provider);
}
pub fn set_inline_completion_provider<T>(
@@ -3279,22 +3364,41 @@ impl Editor {
trigger_in_words: bool,
cx: &mut ViewContext<Self>,
) {
if !EditorSettings::get_global(cx).show_completions_on_input {
return;
}
let selection = self.selections.newest_anchor();
if self
.buffer
.read(cx)
.is_completion_trigger(selection.head(), text, trigger_in_words, cx)
{
if self.is_completion_trigger(text, trigger_in_words, cx) {
self.show_completions(&ShowCompletions, cx);
} else {
self.hide_context_menu(cx);
}
}
fn is_completion_trigger(
&self,
text: &str,
trigger_in_words: bool,
cx: &mut ViewContext<Self>,
) -> bool {
let position = self.selections.newest_anchor().head();
let multibuffer = self.buffer.read(cx);
let Some(buffer) = position
.buffer_id
.and_then(|buffer_id| multibuffer.buffer(buffer_id).clone())
else {
return false;
};
if let Some(completion_provider) = &self.completion_provider {
completion_provider.is_completion_trigger(
&buffer,
position.text_anchor,
text,
trigger_in_words,
cx,
)
} else {
false
}
}
/// If any empty selections is touching the start of its innermost containing autoclose
/// region, expand it to select the brackets.
fn select_autoclose_pair(&mut self, cx: &mut ViewContext<Self>) {
@@ -3802,6 +3906,7 @@ impl Editor {
let snippet;
let text;
if completion.is_snippet() {
snippet = Some(Snippet::parse(&completion.new_text).log_err()?);
text = snippet.as_ref().unwrap().text.clone();
@@ -3895,6 +4000,14 @@ impl Editor {
this.refresh_inline_completion(true, cx);
});
if let Some(confirm) = completion.confirm.as_ref() {
(confirm)(cx);
}
if completion.show_new_completions_on_confirm {
self.show_completions(&ShowCompletions, cx);
}
let provider = self.completion_provider.as_ref()?;
let apply_edits = provider.apply_additional_edits_for_completion(
buffer_handle,
@@ -3927,28 +4040,29 @@ impl Editor {
let deployed_from_indicator = action.deployed_from_indicator;
let mut task = self.code_actions_task.take();
let action = action.clone();
cx.spawn(|this, mut cx| async move {
cx.spawn(|editor, mut cx| async move {
while let Some(prev_task) = task {
prev_task.await;
task = this.update(&mut cx, |this, _| this.code_actions_task.take())?;
task = editor.update(&mut cx, |this, _| this.code_actions_task.take())?;
}
let spawned_test_task = this.update(&mut cx, |this, cx| {
if this.focus_handle.is_focused(cx) {
let spawned_test_task = editor.update(&mut cx, |editor, cx| {
if editor.focus_handle.is_focused(cx) {
let multibuffer_point = action
.deployed_from_indicator
.map(|row| DisplayPoint::new(row, 0).to_point(&snapshot))
.unwrap_or_else(|| this.selections.newest::<Point>(cx).head());
.unwrap_or_else(|| editor.selections.newest::<Point>(cx).head());
let (buffer, buffer_row) = snapshot
.buffer_snapshot
.buffer_line_for_row(MultiBufferRow(multibuffer_point.row))
.and_then(|(buffer_snapshot, range)| {
this.buffer
editor
.buffer
.read(cx)
.buffer(buffer_snapshot.remote_id())
.map(|buffer| (buffer, range.start.row))
})?;
let (_, code_actions) = this
let (_, code_actions) = editor
.available_code_actions
.clone()
.and_then(|(location, code_actions)| {
@@ -3963,7 +4077,7 @@ impl Editor {
})
.unzip();
let buffer_id = buffer.read(cx).remote_id();
let tasks = this
let tasks = editor
.tasks
.get(&(buffer_id, buffer_row))
.map(|t| Arc::new(t.to_owned()));
@@ -3971,80 +4085,100 @@ impl Editor {
return None;
}
this.completion_tasks.clear();
this.discard_inline_completion(false, cx);
let task_context = tasks.as_ref().zip(this.workspace.clone()).and_then(
|(tasks, (workspace, _))| {
let position = Point::new(buffer_row, tasks.1.column);
let range_start = buffer.read(cx).anchor_at(position, Bias::Right);
let location = Location {
buffer: buffer.clone(),
range: range_start..range_start,
};
workspace
.update(cx, |workspace, cx| {
tasks::task_context_for_location(workspace, location, cx)
})
.ok()
.flatten()
},
);
let tasks = tasks.zip(task_context).map(|(tasks, mut task_context)| {
// Fill in the environmental variables from the tree-sitter captures
let mut additional_task_variables = TaskVariables::default();
for (capture_name, value) in tasks.1.extra_variables.clone() {
additional_task_variables.insert(
task::VariableName::Custom(capture_name.into()),
value.clone(),
);
}
task_context
.task_variables
.extend(additional_task_variables);
Arc::new(ResolvedTasks {
templates: tasks
.1
.templates
.iter()
.filter_map(|(kind, template)| {
template
.resolve_task(&kind.to_id_base(), &task_context)
.map(|task| (kind.clone(), task))
})
.collect(),
position: snapshot
.buffer_snapshot
.anchor_before(Point::new(multibuffer_point.row, tasks.1.column)),
})
});
let spawn_straight_away = tasks
.as_ref()
.map_or(false, |tasks| tasks.templates.len() == 1)
&& code_actions
editor.completion_tasks.clear();
editor.discard_inline_completion(false, cx);
let task_context =
tasks
.as_ref()
.map_or(true, |actions| actions.is_empty());
*this.context_menu.write() = Some(ContextMenu::CodeActions(CodeActionsMenu {
buffer,
actions: CodeActionContents {
tasks,
actions: code_actions,
},
selected_item: Default::default(),
scroll_handle: UniformListScrollHandle::default(),
deployed_from_indicator,
}));
if spawn_straight_away {
if let Some(task) =
this.confirm_code_action(&ConfirmCodeAction { item_ix: Some(0) }, cx)
.zip(editor.project.clone())
.map(|(tasks, project)| {
let position = Point::new(buffer_row, tasks.column);
let range_start = buffer.read(cx).anchor_at(position, Bias::Right);
let location = Location {
buffer: buffer.clone(),
range: range_start..range_start,
};
// Fill in the environmental variables from the tree-sitter captures
let mut captured_task_variables = TaskVariables::default();
for (capture_name, value) in tasks.extra_variables.clone() {
captured_task_variables.insert(
task::VariableName::Custom(capture_name.into()),
value.clone(),
);
}
project.update(cx, |project, cx| {
project.task_context_for_location(
captured_task_variables,
location,
cx,
)
})
});
Some(cx.spawn(|editor, mut cx| async move {
let task_context = match task_context {
Some(task_context) => task_context.await,
None => None,
};
let resolved_tasks =
tasks.zip(task_context).map(|(tasks, task_context)| {
Arc::new(ResolvedTasks {
templates: tasks
.templates
.iter()
.filter_map(|(kind, template)| {
template
.resolve_task(&kind.to_id_base(), &task_context)
.map(|task| (kind.clone(), task))
})
.collect(),
position: snapshot.buffer_snapshot.anchor_before(Point::new(
multibuffer_point.row,
tasks.column,
)),
})
});
let spawn_straight_away = resolved_tasks
.as_ref()
.map_or(false, |tasks| tasks.templates.len() == 1)
&& code_actions
.as_ref()
.map_or(true, |actions| actions.is_empty());
if let Some(task) = editor
.update(&mut cx, |editor, cx| {
*editor.context_menu.write() =
Some(ContextMenu::CodeActions(CodeActionsMenu {
buffer,
actions: CodeActionContents {
tasks: resolved_tasks,
actions: code_actions,
},
selected_item: Default::default(),
scroll_handle: UniformListScrollHandle::default(),
deployed_from_indicator,
}));
if spawn_straight_away {
if let Some(task) = editor.confirm_code_action(
&ConfirmCodeAction { item_ix: Some(0) },
cx,
) {
cx.notify();
return task;
}
}
cx.notify();
Task::ready(Ok(()))
})
.ok()
{
cx.notify();
return Some(task);
task.await
} else {
Ok(())
}
}
cx.notify();
}))
} else {
Some(Task::ready(Ok(())))
}
Some(Task::ready(Ok(())))
})?;
if let Some(task) = spawned_test_task {
task.await?;
@@ -4182,7 +4316,7 @@ impl Editor {
workspace.update(&mut cx, |workspace, cx| {
let project = workspace.project().clone();
let editor =
cx.new_view(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), cx));
cx.new_view(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), true, cx));
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, cx);
editor.update(cx, |editor, cx| {
editor.highlight_background::<Self>(
@@ -4613,7 +4747,7 @@ impl Editor {
self.tasks.clear()
}
fn insert_tasks(&mut self, key: (BufferId, BufferRow), value: (usize, RunnableTasks)) {
fn insert_tasks(&mut self, key: (BufferId, BufferRow), value: RunnableTasks) {
if let Some(_) = self.tasks.insert(key, value) {
// This case should hopefully be rare, but just in case...
log::error!("multiple different run targets found on a single line, only the last target will be rendered")
@@ -5786,7 +5920,7 @@ impl Editor {
let mut end = fold.range.end.to_point(&buffer);
start.row -= row_delta;
end.row -= row_delta;
refold_ranges.push((start..end, fold.text));
refold_ranges.push((start..end, fold.placeholder.clone()));
}
}
}
@@ -5880,7 +6014,7 @@ impl Editor {
let mut end = fold.range.end.to_point(&buffer);
start.row += row_delta;
end.row += row_delta;
refold_ranges.push((start..end, fold.text));
refold_ranges.push((start..end, fold.placeholder.clone()));
}
}
}
@@ -7812,11 +7946,14 @@ impl Editor {
let Some(project) = project else {
return;
};
if project
.update(&mut cx, |this, _| this.is_remote())
.unwrap_or(true)
{
// Do not display any test indicators in remote projects.
let hide_runnables = project
.update(&mut cx, |project, cx| {
// Do not display any test indicators in non-dev server remote projects.
project.is_remote() && project.ssh_connection_string(cx).is_none()
})
.unwrap_or(true);
if hide_runnables {
return;
}
let new_rows =
@@ -7851,14 +7988,12 @@ impl Editor {
snapshot: DisplaySnapshot,
runnable_ranges: Vec<RunnableRange>,
mut cx: AsyncWindowContext,
) -> Vec<((BufferId, u32), (usize, RunnableTasks))> {
) -> Vec<((BufferId, u32), RunnableTasks)> {
runnable_ranges
.into_iter()
.filter_map(|mut runnable| {
let (tasks, _) = cx
.update(|cx| {
Self::resolve_runnable(project.clone(), &mut runnable.runnable, cx)
})
let tasks = cx
.update(|cx| Self::templates_with_tags(&project, &mut runnable.runnable, cx))
.ok()?;
if tasks.is_empty() {
return None;
@@ -7873,26 +8008,27 @@ impl Editor {
.start
.row;
let context_range =
BufferOffset(runnable.full_range.start)..BufferOffset(runnable.full_range.end);
Some((
(runnable.buffer_id, row),
(
runnable.run_range.start,
RunnableTasks {
templates: tasks,
column: point.column,
extra_variables: runnable.extra_captures,
},
),
RunnableTasks {
templates: tasks,
offset: MultiBufferOffset(runnable.run_range.start),
context_range,
column: point.column,
extra_variables: runnable.extra_captures,
},
))
})
.collect()
}
fn resolve_runnable(
project: Model<Project>,
fn templates_with_tags(
project: &Model<Project>,
runnable: &mut Runnable,
cx: &WindowContext<'_>,
) -> (Vec<(TaskSourceKind, TaskTemplate)>, Option<WorktreeId>) {
) -> Vec<(TaskSourceKind, TaskTemplate)> {
let (inventory, worktree_id) = project.read_with(cx, |project, cx| {
let worktree_id = project
.buffer_for_id(runnable.buffer)
@@ -7929,7 +8065,7 @@ impl Editor {
}
}
(tags, worktree_id)
tags
}
pub fn move_to_enclosing_bracket(
@@ -8020,9 +8156,34 @@ impl Editor {
}
pub fn expand_excerpts(&mut self, action: &ExpandExcerpts, cx: &mut ViewContext<Self>) {
self.expand_excerpts_for_direction(action.lines, ExpandExcerptDirection::UpAndDown, cx)
}
pub fn expand_excerpts_down(
&mut self,
action: &ExpandExcerptsDown,
cx: &mut ViewContext<Self>,
) {
self.expand_excerpts_for_direction(action.lines, ExpandExcerptDirection::Down, cx)
}
pub fn expand_excerpts_up(&mut self, action: &ExpandExcerptsUp, cx: &mut ViewContext<Self>) {
self.expand_excerpts_for_direction(action.lines, ExpandExcerptDirection::Up, cx)
}
pub fn expand_excerpts_for_direction(
&mut self,
lines: u32,
direction: ExpandExcerptDirection,
cx: &mut ViewContext<Self>,
) {
let selections = self.selections.disjoint_anchors();
let lines = if action.lines == 0 { 3 } else { action.lines };
let lines = if lines == 0 {
EditorSettings::get_global(cx).expand_excerpt_lines
} else {
lines
};
self.buffer.update(cx, |buffer, cx| {
buffer.expand_excerpts(
@@ -8031,14 +8192,22 @@ impl Editor {
.map(|selection| selection.head().excerpt_id)
.dedup(),
lines,
direction,
cx,
)
})
}
pub fn expand_excerpt(&mut self, excerpt: ExcerptId, cx: &mut ViewContext<Self>) {
self.buffer
.update(cx, |buffer, cx| buffer.expand_excerpts([excerpt], 3, cx))
pub fn expand_excerpt(
&mut self,
excerpt: ExcerptId,
direction: ExpandExcerptDirection,
cx: &mut ViewContext<Self>,
) {
let lines = EditorSettings::get_global(cx).expand_excerpt_lines;
self.buffer.update(cx, |buffer, cx| {
buffer.expand_excerpts([excerpt], lines, direction, cx)
})
}
fn go_to_diagnostic(&mut self, _: &GoToDiagnostic, cx: &mut ViewContext<Self>) {
@@ -8685,7 +8854,7 @@ impl Editor {
});
let editor = cx.new_view(|cx| {
Editor::for_multibuffer(excerpt_buffer, Some(workspace.project().clone()), cx)
Editor::for_multibuffer(excerpt_buffer, Some(workspace.project().clone()), true, cx)
});
editor.update(cx, |editor, cx| {
editor.highlight_background::<Self>(
@@ -9258,14 +9427,14 @@ impl Editor {
let buffer_row = fold_at.buffer_row;
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
if let Some((fold_range, fold_text)) = display_map.foldable_range(buffer_row) {
if let Some((fold_range, placeholder)) = display_map.foldable_range(buffer_row) {
let autoscroll = self
.selections
.all::<Point>(cx)
.iter()
.any(|selection| fold_range.overlaps(&selection.range()));
self.fold_ranges([(fold_range, fold_text)], autoscroll, cx);
self.fold_ranges([(fold_range, placeholder)], autoscroll, cx);
}
}
@@ -9319,9 +9488,9 @@ impl Editor {
.buffer_snapshot
.line_len(MultiBufferRow(s.end.row)),
);
(start..end, "")
(start..end, display_map.fold_placeholder.clone())
} else {
(s.start..s.end, "")
(s.start..s.end, display_map.fold_placeholder.clone())
}
});
self.fold_ranges(ranges, true, cx);
@@ -9329,7 +9498,7 @@ impl Editor {
pub fn fold_ranges<T: ToOffset + Clone>(
&mut self,
ranges: impl IntoIterator<Item = (Range<T>, &'static str)>,
ranges: impl IntoIterator<Item = (Range<T>, FoldPlaceholder)>,
auto_scroll: bool,
cx: &mut ViewContext<Self>,
) {
@@ -9406,6 +9575,7 @@ impl Editor {
cx.notify();
self.scrollbar_marker_state.dirty = true;
self.active_indent_guides_state.dirty = true;
}
}
@@ -9599,6 +9769,22 @@ impl Editor {
cx.notify();
}
pub fn toggle_indent_guides(&mut self, _: &ToggleIndentGuides, cx: &mut ViewContext<Self>) {
let currently_enabled = self.should_show_indent_guides(cx);
self.show_indent_guides = Some(!currently_enabled);
cx.notify();
}
fn should_show_indent_guides(&self, cx: &mut ViewContext<Self>) -> bool {
self.show_indent_guides.unwrap_or_else(|| {
self.buffer
.read(cx)
.settings_at(0, cx)
.indent_guides
.enabled
})
}
pub fn toggle_line_numbers(&mut self, _: &ToggleLineNumbers, cx: &mut ViewContext<Self>) {
let mut editor_settings = EditorSettings::get_global(cx).clone();
editor_settings.gutter.line_numbers = !editor_settings.gutter.line_numbers;
@@ -9610,8 +9796,32 @@ impl Editor {
cx.notify();
}
pub fn set_show_wrap_guides(&mut self, show_gutter: bool, cx: &mut ViewContext<Self>) {
self.show_wrap_guides = Some(show_gutter);
pub fn set_show_line_numbers(&mut self, show_line_numbers: bool, cx: &mut ViewContext<Self>) {
self.show_line_numbers = Some(show_line_numbers);
cx.notify();
}
pub fn set_show_git_diff_gutter(
&mut self,
show_git_diff_gutter: bool,
cx: &mut ViewContext<Self>,
) {
self.show_git_diff_gutter = Some(show_git_diff_gutter);
cx.notify();
}
pub fn set_show_code_actions(&mut self, show_code_actions: bool, cx: &mut ViewContext<Self>) {
self.show_code_actions = Some(show_code_actions);
cx.notify();
}
pub fn set_show_wrap_guides(&mut self, show_wrap_guides: bool, cx: &mut ViewContext<Self>) {
self.show_wrap_guides = Some(show_wrap_guides);
cx.notify();
}
pub fn set_show_indent_guides(&mut self, show_indent_guides: bool, cx: &mut ViewContext<Self>) {
self.show_indent_guides = Some(show_indent_guides);
cx.notify();
}
@@ -10250,6 +10460,7 @@ impl Editor {
singleton_buffer_edited,
} => {
self.scrollbar_marker_state.dirty = true;
self.active_indent_guides_state.dirty = true;
self.refresh_active_diagnostics(cx);
self.refresh_code_actions(cx);
if self.has_active_inline_completion(cx) {
@@ -10885,6 +11096,15 @@ pub trait CompletionProvider {
push_to_history: bool,
cx: &mut ViewContext<Editor>,
) -> Task<Result<Option<language::Transaction>>>;
fn is_completion_trigger(
&self,
buffer: &Model<Buffer>,
position: language::Anchor,
text: &str,
trigger_in_words: bool,
cx: &mut ViewContext<Editor>,
) -> bool;
}
impl CompletionProvider for Model<Project> {
@@ -10922,6 +11142,40 @@ impl CompletionProvider for Model<Project> {
project.apply_additional_edits_for_completion(buffer, completion, push_to_history, cx)
})
}
fn is_completion_trigger(
&self,
buffer: &Model<Buffer>,
position: language::Anchor,
text: &str,
trigger_in_words: bool,
cx: &mut ViewContext<Editor>,
) -> bool {
if !EditorSettings::get_global(cx).show_completions_on_input {
return false;
}
let mut chars = text.chars();
let char = if let Some(char) = chars.next() {
char
} else {
return false;
};
if chars.next().is_some() {
return false;
}
let buffer = buffer.read(cx);
let scope = buffer.snapshot().language_scope_at(position);
if trigger_in_words && char_kind(&scope, char) == CharKind::Word {
return true;
}
buffer
.completion_triggers()
.iter()
.any(|string| string == text)
}
}
fn inlay_hint_settings(
@@ -11027,13 +11281,17 @@ impl EditorSnapshot {
}
let descent = cx.text_system().descent(font_id, font_size);
let show_git_gutter = matches!(
ProjectSettings::get_global(cx).git.git_gutter,
Some(GitGutterSetting::TrackedFiles)
);
let show_git_gutter = self.show_git_diff_gutter.unwrap_or_else(|| {
matches!(
ProjectSettings::get_global(cx).git.git_gutter,
Some(GitGutterSetting::TrackedFiles)
)
});
let gutter_settings = EditorSettings::get_global(cx).gutter;
let gutter_lines_enabled = gutter_settings.line_numbers;
let line_gutter_width = if gutter_lines_enabled {
let show_line_numbers = self
.show_line_numbers
.unwrap_or_else(|| gutter_settings.line_numbers);
let line_gutter_width = if show_line_numbers {
// Avoid flicker-like gutter resizes when the line number gains another digit and only resize the gutter on files with N*10^5 lines.
let min_width_for_number_on_gutter = em_width * 4.0;
max_line_number_width.max(min_width_for_number_on_gutter)
@@ -11041,26 +11299,30 @@ impl EditorSnapshot {
0.0.into()
};
let show_code_actions = self
.show_code_actions
.unwrap_or_else(|| gutter_settings.code_actions);
let git_blame_entries_width = self
.render_git_blame_gutter
.then_some(em_width * GIT_BLAME_GUTTER_WIDTH_CHARS);
let mut left_padding = git_blame_entries_width.unwrap_or(Pixels::ZERO);
left_padding += if gutter_settings.code_actions {
left_padding += if show_code_actions {
em_width * 3.0
} else if show_git_gutter && gutter_lines_enabled {
} else if show_git_gutter && show_line_numbers {
em_width * 2.0
} else if show_git_gutter || gutter_lines_enabled {
} else if show_git_gutter || show_line_numbers {
em_width
} else {
px(0.)
};
let right_padding = if gutter_settings.folds && gutter_lines_enabled {
let right_padding = if gutter_settings.folds && show_line_numbers {
em_width * 4.0
} else if gutter_settings.folds {
em_width * 3.0
} else if gutter_lines_enabled {
} else if show_line_numbers {
em_width
} else {
px(0.)
@@ -11214,7 +11476,7 @@ impl Render for Editor {
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features.clone(),
font_size: rems(0.875).into(),
font_weight: FontWeight::NORMAL,
font_weight: settings.ui_font.weight,
font_style: FontStyle::Normal,
line_height: relative(settings.buffer_line_height.value()),
background_color: None,
@@ -11227,7 +11489,7 @@ impl Render for Editor {
font_family: settings.buffer_font.family.clone(),
font_features: settings.buffer_font.features.clone(),
font_size: settings.buffer_font_size(cx).into(),
font_weight: FontWeight::NORMAL,
font_weight: settings.buffer_font.weight,
font_style: FontStyle::Normal,
line_height: relative(settings.buffer_line_height.value()),
background_color: None,

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