Compare commits

...

79 Commits

Author SHA1 Message Date
Piotr Osiewicz
7603f0150c Merge branch 'main' into adjustable-context-menu-editor 2024-11-09 13:30:43 +01:00
Nate Butler
0a28800049 Add preview for Checkbox with Label (#20448)
Add previews for Checkbox with Label.

Merge checkbox components.

Release Notes:

- N/A
2024-11-08 22:53:15 -05:00
Nate Butler
31a6ee0229 Add ui::table (#20447)
This PR adds the `ui::Table` component.

It has a rather simple API, but cells can contain either strings or
elements, allowing for some complex uses.

Example usage:

```rust
Table::new(vec!["Product", "Price", "Stock"])
    .width(px(600.))
    .striped()
    .row(vec!["Laptop", "$999", "In Stock"])
    .row(vec!["Phone", "$599", "Low Stock"])
    .row(vec!["Tablet", "$399", "Out of Stock"])
```

For more complex use cases, the table supports mixed content:

```rust
Table::new(vec!["Status", "Name", "Priority", "Deadline", "Action"])
    .width(px(840.))
    .row(vec![
        element_cell(Indicator::dot().color(Color::Success).into_any_element()),
        string_cell("Project A"),
        string_cell("High"),
        string_cell("2023-12-31"),
        element_cell(Button::new("view_a", "View").style(ButtonStyle::Filled).full_width().into_any_element()),
    ])
    // ... more rows
```

Preview:

![CleanShot 2024-11-08 at 20 53
04@2x](https://github.com/user-attachments/assets/b39122f0-a29b-423b-8e24-86ab4c42bac2)

This component is pretty basic, improvements are welcome!

Release Notes:

- N/A
2024-11-08 21:10:15 -05:00
Kyle Kelley
1f974d074e Set up editor actions after workspace not on stack (#20445)
Release Notes:

- N/A

Co-authored-by: Conrad Irwin <conrad@zed.dev>
2024-11-08 17:11:43 -08:00
Elias Müller
72125949d9 Add shortcuts for 'open settings' and 'revert selected hunks' to Jetbrains keymap (#20414) 2024-11-08 19:34:52 -05:00
Kirill Bulatov
d605d192af Use a different keybinding for editor::AcceptPartialInlineCompletion action (Linux) (#20443)
Follow-up of https://github.com/zed-industries/zed/pull/20419

Release Notes:

- - (breaking change) Use `ctrl-right` instead of `cmd-right` as a macOS
default for `editor::AcceptPartialInlineCompletion` (Linux)
2024-11-09 01:38:18 +02:00
Marshall Bowers
f92e6e9a95 Add support for context server extensions (#20250)
This PR adds support for context servers provided by extensions.

To provide a context server from an extension, you need to list the
context servers in your `extension.toml`:

```toml
[context_servers.my-context-server]
```

And then implement the `context_server_command` method to return the
command that will be used to start the context server:

```rs
use zed_extension_api::{self as zed, Command, ContextServerId, Result};

struct ExampleContextServerExtension;

impl zed::Extension for ExampleContextServerExtension {
    fn new() -> Self {
        ExampleContextServerExtension
    }

    fn context_server_command(&mut self, _context_server_id: &ContextServerId) -> Result<Command> {
        Ok(Command {
            command: "node".to_string(),
            args: vec!["/path/to/example-context-server/index.js".to_string()],
            env: Vec::new(),
        })
    }
}

zed::register_extension!(ExampleContextServerExtension);
```

Release Notes:

- N/A
2024-11-08 16:39:21 -05:00
Conrad Irwin
ff4f67993b macOS: Add key equivalents for non-Latin layouts (#20401)
Closes  #16343
Closes #10972

Release Notes:

- (breaking change) On macOS when using a keyboard that supports an
extended Latin character set (e.g. French, German, ...) keyboard
shortcuts are automatically updated so that they can be typed without
`option`. This fixes several long-standing problems where some keyboards
could not type some shortcuts.
- This mapping works the same way as
[macOS](https://developer.apple.com/documentation/swiftui/view/keyboardshortcut(_:modifiers:localization:)).
For example on a German keyboard shortcuts like `cmd->` become `cmd-:`,
`cmd-[` and `cmd-]` become `cmd-ö` and `cmd-ä`. This mapping happens at
the time keyboard layout files are read so the keybindings are visible
in the command palette. To opt out of this behavior for your custom
keyboard shortcuts, set `"use_layout_keys": true` in your binding
section. For the mappings used for each layout [see
here](a890df1863/crates/settings/src/key_equivalents.rs (L7)).

---------

Co-authored-by: Will <will@zed.dev>
2024-11-08 13:05:10 -07:00
Conrad Irwin
07821083df macOS: Allow non-cmd keyboard shortcuts to work on non-Latin layouts (#20336)
Updates #10972

Release Notes:

- Fixed builtin keybindings that don't require cmd on macOS, non-Latin,
ANSI layouts. For example you can now use ctrl-ա (equivalent to ctrl-a)
on an Armenian keyboard to get to the beginning of the line.

---------

Co-authored-by: Will <will@zed.dev>
2024-11-08 11:49:13 -07:00
Marshall Bowers
09c599385a Put context servers behind a trait (#20432)
This PR puts context servers behind the `ContextServer` trait to allow
us to provide context servers from an extension.

Release Notes:

- N/A
2024-11-08 13:36:41 -05:00
Richard Feldman
01503511ad Fix line number whitespace (#20427)
Closes #14025


https://github.com/user-attachments/assets/24b3f321-8246-4203-9510-66a7cf3d22f0

Release Notes:

- Fixed bug where toggling line numbers would incorrectly hide
whitespace indicators.
2024-11-08 13:25:48 -05:00
Marshall Bowers
8bc5bcf0a6 assistant: Fix completions for slash commands provided by context servers (#20423)
This PR fixes an issue introduced in #20372 that was causing slash
commands provided by context servers to not show up in the completions
menu.

Release Notes:

- N/A
2024-11-08 11:35:39 -05:00
Kirill Bulatov
983bb5c5fc Use a different keybinding for editor::AcceptPartialInlineCompletion action (#20419)
Both `editor::AcceptPartialInlineCompletion` and the keybinding for
`editor::MoveToEndOfLine` had the same keybinding inside the editor, and
with Supermaven's fast proposals, it's been very frequently used
incorrectly.

Closes #ISSUE

Release Notes:

- (breaking change) Use `ctrl-right` instead of `cmd-right` as a macOS
default for `editor::AcceptPartialInlineCompletion`
2024-11-08 18:07:38 +02:00
Thorsten Ball
653b2dc676 project panel: Stop flickering border when preview tabs disabled (#20417)
PR #20154 changed the project panel to focus the editor on click in case
preview tabs are disabled.

That lead to a flickering behavior: on mouse-down the border of the
still-selected entry in the project panel would flash, only to disappear
as soon as the entry was opened and editor focused.

This change fixes it by manually keeping track of the mouse-down state,
because we couldn't find a better solution that allows us to simply not
show the border while a "click" is going on.

Release Notes:

- Fixed project panel entries flickering the border when user clicks on
another entry to open it (when preview tabs are disabled.)

Co-authored-by: Piotr <piotr@zed.dev>
2024-11-08 16:53:39 +01:00
Joseph T. Lyons
7142d3777f Add edit events for assistant panel and inline assist (#20418)
Release Notes:

- N/A
2024-11-08 10:37:10 -05:00
Thorsten Ball
01e12c0d3c project panel: Mark entries when opening in project panel (#20412)
This addresses #17746 by marking entries when they're opened in the
project panel.

I think that was the original intention behind the code too, because it
explicitly marks entries before opening them. An event that is emitted
by the workspace reset the mark though.

So what I did was try to emulate the logic I saw in VS Code: when
opening the file, mark it, when the active entry changes, unmark it,
except if you explicitly marked a group of files.

Closes #17746

Release Notes:

- Changed project panel to mark files when opening them, which should
make it more intuitive to mark multiple files after opening a single
one.
2024-11-08 16:29:10 +01:00
Kyle Kelley
706c385c24 Register repl actions with editor after session started (#20396)
Makes repl actions that are specific to running kernels only come up
after a session has been started at least once for the editor.

Release Notes:

- Only show session oriented `repl::` actions for editors after a
session has been created
2024-11-08 06:58:44 -08:00
Thorsten Ball
edb89d8d11 project panel: Fix preview tabs not working when enabled (#20416)
PR #20154 introduced a regression and essentially disabled preview tabs
in code.

This fixes it and restores the old preview tabs behavior.

Release Notes:

- Fixed preview tabs being disabled in code, even if they were enabled
in the settings.

Co-authored-by: Piotr <piotr@zed.dev>
2024-11-08 15:46:53 +01:00
Kyle Kelley
09675d43b3 Disable repl in non-local projects (#20397)
Release Notes:

- Disable REPL buttons and actions for remote projects and collaboration
(only the host should have access).
2024-11-08 06:29:07 -08:00
Danilo Leal
187356ab9b assistant: Show only configured models in the model picker (#20392)
Closes https://github.com/zed-industries/zed/issues/16568

This PR introduces some changes to how we display models in the model
selector within the assistant panel. Basically, it comes down to this:

- If you don't have any provider configured, you should see _all_
available models in the picker
- But, once you've configured some, you should _only_ see models from
them in the picker

Visually, nothing's changed much aside from the added "Configured
Models" label at the top to ensure the understanding that that's a list
of, well, configured models only. 😬

<img width="700" alt="Screenshot 2024-11-07 at 23 42 41"
src="https://github.com/user-attachments/assets/219ed386-2318-43a6-abea-1de0cda8dc53">

Release Notes:

- Change model selector in the assistant panel to only show configured
models
2024-11-08 10:08:59 -03:00
Danilo Leal
435708b615 assistant: Fine-tune crease feedback design (#20395)
Closes https://github.com/zed-industries/zed/issues/13414

Just polishing up how some of these look. Ever since the issue was
opened, we added the "Error" label to the button, as well as
automatically popping open the toast error whenever that happens.
Lastly, there's a tooltip in there as well indicating that you can click
on it to see the details of the error.

<img width="700" alt="Screenshot 2024-11-08 at 00 26 27"
src="https://github.com/user-attachments/assets/ad0962e6-4621-4e8b-be0d-103d71fcf2e6">

Release Notes:

- N/A
2024-11-08 10:08:49 -03:00
Antonio Scandurra
2fe9cd8faa Fix regression in producing sections when converting SlashCommandOutput to event stream (#20404)
Closes #20243 

Release Notes:

- N/A
2024-11-08 09:29:14 +01:00
Conrad Irwin
8cc3ce1f17 Fix extension tests on release branches (#20307)
Co-Authored-By: Max <max@zed.dev>
cc @maxdeviant

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
2024-11-07 22:19:36 -07:00
Conrad Irwin
6ad8c4a0f4 Restore ctrl-b/f (#20399)
Follow up from #20378

Release Notes:

- N/A
2024-11-07 22:18:28 -07:00
David Soria Parra
30a94fa59b context_servers: Fix tool/list and prompt/list (#20387)
There are two issues with too/list and prompt/list at the moment. We
serialize params to `null`, which is not correct according to
context_server spec. While it IS allowed by JSON RPC spec to omit
params, it turns out some servers currently missbehave and don't respect
this. So we do two things

- We omit params if it would be a null value in json.
- We explicitly set params to {} for tool/list and prompt/list to avoid
it being omitted.

Release Notes:

- N/A
2024-11-07 19:09:55 -08:00
Kyle Kelley
36fe364c05 Show kernel options in a picker (#20274)
Closes #18341

* [x] Remove "Change Kernel" Doc link from REPL menu
* [x] Remove chevron
* [x] Set a higher min width
* [x] Include the language along with the kernel name

Future PRs will address

* Add support for Python envs (#18291, #16757, #15563)
* Add support for Remote kernels
* Project settings support (#16898)

Release Notes:

- Added kernel picker for repl

---------

Co-authored-by: Nate Butler <iamnbutler@gmail.com>
2024-11-07 17:59:53 -08:00
Piotr Osiewicz
107cf624e1 Merge branch 'main' into adjustable-context-menu-editor 2024-11-08 01:49:07 +01:00
Finn Evers
f6d4a73c34 terminal: Prevent [] from being sanitized into clickable file link (#20386)
This PR prevents `[]` from being sanitized into an empty string and thus
becoming a "valid", clickable file link in the integrated terminal.


Whenever you type `[]` into the terminal and hover over it while
pressing `cmd`, an empty popup appears and the cursor indicates that
this is a clickable element. Once you click on the brackets, the
worktree root is selected and focused within the file picker.

<img width="87" alt="grafik"
src="https://github.com/user-attachments/assets/01790323-88be-4373-a1ec-a345bcf2521e">


This is because in #2906 support was added for sanititzing file links
like `[/some/path/[slug].tsx]` to `/some/path/[slug].tsx`. In the case
`[]` where an empty string is returned from the sanitation, the string
is considered a valid file path and thus `[]` becomes a valid and
clickable navigation target.

Given that this an edge-case just for this specific one set of brackets
and otherwise no empty strings are matched from the regexes `URL_REGEX`
and `WORD_REGEX`, it seemed that this was the best place to fix this
bug.

Release Notes:

- `[]` is no longer considered a clickable link in the terminal
2024-11-08 02:41:30 +02:00
Marshall Bowers
7e7f25df6c Scope slash commands, context servers, and tools to individual Assistant Panel instances (#20372)
This PR reworks how the Assistant Panel references slash commands,
context servers, and tools.

Previously we were always reading them from the global registries, but
now we store individual collections on each Assistant Panel instance so
that there can be different ones registered for each project.

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Joseph <joseph@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2024-11-07 18:23:25 -05:00
Kirill Bulatov
176314bfd2 Improve outline panel keyboard navigation (#20385)
Closes https://github.com/zed-industries/zed/issues/20187

Make outline panel more eager to open its entries:

* scroll editor to selected outline entries (before it required an extra
`"outline_panel::Open", { "change_selection": false }` action call)
* make any `Open` action call to behave like `"outline_panel::Open", {
"change_selection": true }` and remove the redundant parameter.
Now opening an entry is equal to double clicking the same entry: the
editor gets scrolled and its selection changes
* add a way to open entries the same way as excerpts are open in multi
buffers (will open the entire file, scroll and place the caret)

* additionally, fix another race issue that caused wrong entry to be
revealed after the selection change

Release Notes:

- Improved outline panel keyboard navigation
2024-11-08 01:19:54 +02:00
Peter Tripp
6606e6e37f ci: No GitHub Actions stale check on forks (#20382) 2024-11-07 16:11:18 -05:00
dhaus67
c350321318 Update Copilot Chat max_tokens soft limits (#20363)
Closes #20362 

Co-authored-by: Peter Tripp <peter@zed.dev>
2024-11-07 16:03:12 -05:00
Conrad Irwin
9da040da3f Stop using alt- shortcuts (#20378)
Closes #7688

Release Notes:

- (breaking change) Stop binding keyboard shortcuts to alt-[a-z]. These
get in the way of typing characters. This is usually not an issue for
English speakers because we don't use many characters; but for other
Latin-based languages with diacritics our shortcuts prevent them typing
what they need to type.

This primarily affects Zed's extra features:
* `alt-q` => `cmd-k q` on maOS, `ctrl-k q` on Linux for `editor::Rewrap`
* `alt-z` => `cmd-k z` on macOS `ctrl-k z` on Linux for
`editor::ToggleSoftWrap`
  * `alt-m` => `ctrl-shift-m` for `assistant::ToggleModelSelector`
* `alt-v` => `ctrl-shift-v` for `["editor::MovePageUp", {
"center_cursor": true }]` (macOS only)
* `alt-t` => `cmd-shift-r` on maOS, `ctrl-shift-r` on Linux for
`task::Spawn` (The previous binding for `editor::RevealInFileManager`
now only applies in the project panel)
* `alt-shift-t` => `alt-cmd-r` on maOS, `ctrl-alt-r` on Linux for
`task::Rerun`
* `alt-shift-f` => `ctrl-shift-f` for
`project_panel::SearchInDirectory`.
 
But also overrides some bindings from Readline.
  * `alt-h` => `alt-backspace` for `editor::DeleteToPreviousWordStart`
  * `alt-d` => `alt-delete` for `editor::DeleteToNextWordEnd`
* `alt-f` => `ctrl-f` for `editor:: MoveToNextWordEnd` (`ctrl-f` was
previously `editor::MoveRight`)
* `alt-b` => `ctrl-b` for `editor::MoveToNextWordStart` (`ctrl-b` was
previously `editor::MoveLeft`)

Note that `alt-t` and `alt-shift-t` have been kept as aliases (because
no-one complained about `t` yet; but we may remove them completely in
the future).
2024-11-07 13:52:13 -07:00
Max Brunsfeld
f6385221c5 Add abstract classes to typescript outline query (#20377)
Closes #4553

Release Notes:

- Fixed a bug where abstract classes weren't shown correctly in the
outline view when editing Typescript code.
2024-11-07 12:44:34 -08:00
Douglas Ronne
999853fee0 Add emacs keymap (#19605)
Release Notes:

- Added Emacs (beta) base keymap
2024-11-07 15:22:53 -05:00
Mathias
f924c3ef00 Fix folder expand when dropped on itself in project_panel (#20365)
Closes #13093

Release Notes:

- Fixed folder expand when dropped on itself in project_panel
2024-11-07 20:03:55 +01:00
Michael Sloan
2c4984091c Revert "Use correct context path for focused element in WindowContext::bindings_for_action (#18843)" (#20367)
@JosephTLyons found that this broke display of keybindings in the recent
projects modal.

Release Notes:

- N/A
2024-11-07 09:45:23 -07:00
Richard Feldman
453c41205b Show warning when deleting files with unsaved changes (#20172)
Closes #9905

<img width="172" alt="Screenshot 2024-11-04 at 10 16 34 AM"
src="https://github.com/user-attachments/assets/5fa84e06-bcb9-471d-adab-e06881fbd3ca">
<img width="172" alt="Screenshot 2024-11-04 at 10 16 22 AM"
src="https://github.com/user-attachments/assets/d7def162-e910-4061-a160-6178c9d344e5">
<img width="172" alt="Screenshot 2024-11-04 at 10 17 17 AM"
src="https://github.com/user-attachments/assets/43c7e4fe-1b71-4786-bc05-44f34ed15dc5">
<img width="172" alt="Screenshot 2024-11-04 at 10 17 09 AM"
src="https://github.com/user-attachments/assets/17263782-c706-44b2-acbc-c3d2d14c20ac">


Release Notes:

- When deleting or trashing files, the confirmation prompt now warns if
files have unsaved changes.
2024-11-07 11:40:33 -05:00
Michael Sloan
e85ab077be Add missing field to impl Hash for RenderGlyphParams (#20316)
Unlikely to cause any behavior change but seems more correct to include
`is_emoji` in the hash.

Release Notes:

- N/A
2024-11-07 09:34:54 -07:00
Will Bradley
daa35e98f1 Enable look-around in Project Search using fancy-regex crate (#20308)
Closes #13486 

Release Notes:

- Added support for look-around in project search

Co-authored-by: Max <max@zed.dev>
2024-11-07 09:18:09 -07:00
Will Bradley
de70852497 Fix a test flake involving zeroed out group_intervals (#20328)
Release Notes:

- N/A
2024-11-07 09:17:43 -07:00
Thorsten Ball
454c9dc06d project panel: Add RemoveFromProject action (#20360)
This fixes #15995 by adding a `project panel: remove from project`
action that can be used in a keybinding.

Release Notes:

- Added a `project panel: remove from project` action so that users can
now add a keybinding to trigger it: `project_panel::RemoveFromProject`.
2024-11-07 16:15:09 +01:00
Thorsten Ball
71aeb6a636 editor: Do not show inline completion if snippet is active (#20300)
This avoids inline completions being shown (and overriding `<tab>`
behavior) when a snippet is active and the user wants to go through
snippet placeholders with `<tab>`.

Easy to reproduce:

Open a Rust file and use the `tfn` snippet to produce a test function.
Delete the placeholder. Without the change here, the inline provider
would suggest a function name. If you `<tab>`, you accept it, but then
you can't `<tab>` into the function body.

With this change the inline completions are deactivated as long as a
snippet is active.

Closes #19484 

Release Notes:

- Fixed inline completions (Copilot, Supermaven, ...) taking over when a
snippet completion was active. That resulted in `tab` not working to
jump to the next placeholder in the snippet.
2024-11-07 15:34:19 +01:00
Danilo Leal
cdd2128311 assistant: Add model names in alternative inline tooltips (#20350)
Closes https://github.com/zed-industries/zed/issues/18826

This PR adds the model name to the tooltips on the alternative inline
assistant icon buttons. The default model should be the first, so every
other one set as an alternative appears after.


https://github.com/user-attachments/assets/46faccaa-447c-45a4-b927-49ea3c4f3be1


Release Notes:

- Improve knowledge of which model is used when with alternative inline
models turned on
2024-11-07 10:56:19 -03:00
Danilo Leal
20b60e8dd2 Ensure project search actions are always aligned (#20353)
Follow up to https://github.com/zed-industries/zed/pull/20242

This PR ensures all the actions to the right of the project search
inputs have the same minimum width, ensuring that the inputs themselves
are always aligned. In the previous PR, I didn't considered the scenario
where the project search numbers where beyond 4 or 5 digits, which then
increased their width. This should be treated now!

<img width="700" alt="Screenshot 2024-11-07 at 09 55 11"
src="https://github.com/user-attachments/assets/7a9d8ebd-b575-4141-9242-3044f00150c5">

Release Notes:

- N/A
2024-11-07 10:56:10 -03:00
Danilo Leal
37366ac907 assistant: Improve UI feedback when inserting /delta without new changes (#20356)
Closes https://github.com/zed-industries/zed/issues/18488

### Before

No feedback when inserting `/delta` without new changes.


https://github.com/user-attachments/assets/4cc76ff4-419d-4a3f-a6a2-8712856b1aa8

### After

You now see an error within the `delta` crease.


https://github.com/user-attachments/assets/c56654bb-776f-4dac-a499-db4625a4f1bd

Release Notes:

- Improve UI feedback when inserting `/delta` without new changes
2024-11-07 10:56:01 -03:00
Kirill Bulatov
083f06322d Prefer revealing items in the middle of the list for outline and project panels (#20349)
Closes https://github.com/zed-industries/zed/issues/18255

Zed does not scroll always, but only if the item is out of sight, this
is preserved for now.
Otherwise, if the item is out of sight, project and outline panels + the
syntax tree view now attempt to scroll it into the middle, if there's
enough elements above and below.

Release Notes:

- Improved revealing items for outline and project panels (now center of the list is preferred)
2024-11-07 14:36:29 +02:00
Antonio Scandurra
16cbff9118 Polish streaming slash commands (#20345)
This improves the experience in a few ways:

- It avoids merging slash command output sections that are adjacent.
- When hitting cmd-z, all the output from a command is undone at once.
- When deleting a pending command, it stops the command and prevents new
output from flowing in.

Release Notes:

- N/A
2024-11-07 13:25:26 +01:00
Kirill Bulatov
e62d60c84c Cleanup Framework directory when re-bundling for macOS (#20342)
Release Notes:

- N/A
2024-11-07 11:34:27 +02:00
Axel Carlsson
4f62ebe4be Exclude pinned tabs when closing items (#19593)
Closing multiple items will no longer closed pinned tabs.

Closes #19560

Release Notes:

- Fixed close actions closing pinned tabs.
2024-11-07 11:20:19 +02:00
Kirill Bulatov
b33ae888c0 Use the updated syn in Cargo.lock (#20341)
Renovate did not update that, so helping it out.

Release Notes:

- N/A
2024-11-07 10:54:03 +02:00
xdBronch
029d08350e zig: Switch to official Zig grammar (#20004)
Closes #20001

the old outline was *weird* for many reasons so ill just show it with a
hodgepodge of zig declarations
before:

![image](https://github.com/user-attachments/assets/87395623-3e28-491c-9693-c1714da6b29f)
after:

![image](https://github.com/user-attachments/assets/1aa3f3b7-18b0-4d39-b1c6-99740e7bcd6a)

why were values shown? why werent `var`s or modifiers like pub, const,
export? it was very odd to me and inconsistent with other languages. i
chose to leave out unnamed tests, it just seemed like noise to me since
they werent distinct but i can easily revert that
unfortunately there seems to be a bug upstream which causes those
`t`/`f` decls to show 2 things
https://github.com/tree-sitter-grammars/tree-sitter-zig/issues/3

im very new to treesitter and queries so i really havent looked over the
rest of the stuff here, other than outline theyre unmodified please lmk
if theres anything wrong

Release Notes:

- Changed upstream treesitter grammar for zig
2024-11-07 09:47:15 +01:00
renovate[bot]
6d0aa72226 Update Rust crate url to v2.5.3 (#20334)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [url](https://redirect.github.com/servo/rust-url) |
workspace.dependencies | patch | `2.5.2` -> `2.5.3` |

---

### Release Notes

<details>
<summary>servo/rust-url (url)</summary>

###
[`v2.5.3`](https://redirect.github.com/servo/rust-url/releases/tag/v2.5.3)

[Compare
Source](https://redirect.github.com/servo/rust-url/compare/v2.5.2...v2.5.3)

#### What's Changed

- fix: enable wasip2 feature for wasm32-wasip2 target by
[@&#8203;brooksmtownsend](https://redirect.github.com/brooksmtownsend)
in
[https://github.com/servo/rust-url/pull/960](https://redirect.github.com/servo/rust-url/pull/960)
- Fix idna tests with no_std by
[@&#8203;cjwatson](https://redirect.github.com/cjwatson) in
[https://github.com/servo/rust-url/pull/963](https://redirect.github.com/servo/rust-url/pull/963)
- Fix debugger_visualizer test failures. by
[@&#8203;valenting](https://redirect.github.com/valenting) in
[https://github.com/servo/rust-url/pull/967](https://redirect.github.com/servo/rust-url/pull/967)
- Add AsciiSet::EMPTY and boolean operators by
[@&#8203;joshka](https://redirect.github.com/joshka) in
[https://github.com/servo/rust-url/pull/969](https://redirect.github.com/servo/rust-url/pull/969)
- mention why we pin unicode-width by
[@&#8203;Manishearth](https://redirect.github.com/Manishearth) in
[https://github.com/servo/rust-url/pull/972](https://redirect.github.com/servo/rust-url/pull/972)
- refactor and add tests for percent encoding by
[@&#8203;joshka](https://redirect.github.com/joshka) in
[https://github.com/servo/rust-url/pull/977](https://redirect.github.com/servo/rust-url/pull/977)
- Add a test for and fix issue
[#&#8203;974](https://redirect.github.com/servo/rust-url/issues/974) by
[@&#8203;hansl](https://redirect.github.com/hansl) in
[https://github.com/servo/rust-url/pull/975](https://redirect.github.com/servo/rust-url/pull/975)
- `no_std` support for the `url` crate by
[@&#8203;domenukk](https://redirect.github.com/domenukk) in
[https://github.com/servo/rust-url/pull/831](https://redirect.github.com/servo/rust-url/pull/831)
- Normalize URL paths: convert /.//p, /..//p, and //p to p by
[@&#8203;theskim](https://redirect.github.com/theskim) in
[https://github.com/servo/rust-url/pull/943](https://redirect.github.com/servo/rust-url/pull/943)
- support Hermit by
[@&#8203;m-mueller678](https://redirect.github.com/m-mueller678) in
[https://github.com/servo/rust-url/pull/985](https://redirect.github.com/servo/rust-url/pull/985)
- fix: support `wasm32-wasip2` on the stable channel by
[@&#8203;brooksmtownsend](https://redirect.github.com/brooksmtownsend)
in
[https://github.com/servo/rust-url/pull/983](https://redirect.github.com/servo/rust-url/pull/983)
- Improve serde error output by
[@&#8203;konstin](https://redirect.github.com/konstin) in
[https://github.com/servo/rust-url/pull/982](https://redirect.github.com/servo/rust-url/pull/982)
- OSS-Fuzz: Add more fuzzer by
[@&#8203;arthurscchan](https://redirect.github.com/arthurscchan) in
[https://github.com/servo/rust-url/pull/988](https://redirect.github.com/servo/rust-url/pull/988)
- Merge idna-v1x to main by
[@&#8203;hsivonen](https://redirect.github.com/hsivonen) in
[https://github.com/servo/rust-url/pull/990](https://redirect.github.com/servo/rust-url/pull/990)

#### New Contributors

- [@&#8203;brooksmtownsend](https://redirect.github.com/brooksmtownsend)
made their first contribution in
[https://github.com/servo/rust-url/pull/960](https://redirect.github.com/servo/rust-url/pull/960)
- [@&#8203;cjwatson](https://redirect.github.com/cjwatson) made their
first contribution in
[https://github.com/servo/rust-url/pull/963](https://redirect.github.com/servo/rust-url/pull/963)
- [@&#8203;joshka](https://redirect.github.com/joshka) made their first
contribution in
[https://github.com/servo/rust-url/pull/969](https://redirect.github.com/servo/rust-url/pull/969)
- [@&#8203;hansl](https://redirect.github.com/hansl) made their first
contribution in
[https://github.com/servo/rust-url/pull/975](https://redirect.github.com/servo/rust-url/pull/975)
- [@&#8203;theskim](https://redirect.github.com/theskim) made their
first contribution in
[https://github.com/servo/rust-url/pull/943](https://redirect.github.com/servo/rust-url/pull/943)
- [@&#8203;m-mueller678](https://redirect.github.com/m-mueller678) made
their first contribution in
[https://github.com/servo/rust-url/pull/985](https://redirect.github.com/servo/rust-url/pull/985)
- [@&#8203;konstin](https://redirect.github.com/konstin) made their
first contribution in
[https://github.com/servo/rust-url/pull/982](https://redirect.github.com/servo/rust-url/pull/982)
- [@&#8203;arthurscchan](https://redirect.github.com/arthurscchan) made
their first contribution in
[https://github.com/servo/rust-url/pull/988](https://redirect.github.com/servo/rust-url/pull/988)

**Full Changelog**:
https://github.com/servo/rust-url/compare/v2.5.2...v2.5.3

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4xNDIuNyIsInVwZGF0ZWRJblZlciI6IjM4LjE0Mi43IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-07 10:14:34 +02:00
Cherry
12afd1264e Add long file paths solution to Windows troubleshooting (#20317)
Currently, building on Windows may fail for people who have not enabled
long filepaths on Windows itself.

This PR adds the solution to the troubleshooting section in the
`Building Zed for Windows` guide.

For an example failure error message:
```rs
error: failed to get `pet` as a dependency of package `languages v0.1.0 (D:\a\zed-windows-builds\zed-windows-builds\crates\languages)`

Caused by:
  failed to load source for dependency `pet`

Caused by:
  Unable to update https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f2

Caused by:
  path too long: 'C:/Users/runneradmin/.cargo/git/checkouts/python-environment-tools-903993894b37a7d2/ffcbf3f/crates/pet-conda/tests/unix/conda_env_without_manager_but_found_in_history/some_other_location/conda_install/conda-meta/python-fastjsonschema-2.16.2-py310hca03da5_0.json'; class=Filesystem (30)
```

Release Notes:

- N/A
2024-11-07 10:01:59 +02:00
Kirill Bulatov
b526d69387 Allow Unicode-3.0 license in checks (#20339)
Part of https://github.com/zed-industries/zed/pull/20334

The license is on par with other licenses in the list:
https://www.unicode.org/license.txt

Release Notes:

- N/A
2024-11-07 09:59:41 +02:00
Jeevitha Kannan K S
4f06f5b8fe Refactor scripts to use command -v instead of which (#20306)
`which` is not a core package is some distributions.
Included changes from #20309 

Release Notes:

- N/A
2024-11-07 09:34:56 +02:00
renovate[bot]
555a219f11 Update Rust crate serde_json to v1.0.132 (#20326)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [serde_json](https://redirect.github.com/serde-rs/json) | dependencies
| patch | `1.0.128` -> `1.0.132` |
| [serde_json](https://redirect.github.com/serde-rs/json) |
workspace.dependencies | patch | `1.0.128` -> `1.0.132` |

---

### Release Notes

<details>
<summary>serde-rs/json (serde_json)</summary>

###
[`v1.0.132`](https://redirect.github.com/serde-rs/json/releases/tag/1.0.132)

[Compare
Source](https://redirect.github.com/serde-rs/json/compare/1.0.131...1.0.132)

- Improve binary size and compile time for JSON array and JSON object
deserialization by about 50%
([#&#8203;1205](https://redirect.github.com/serde-rs/json/issues/1205))
- Improve performance of JSON array and JSON object deserialization by
about 8%
([#&#8203;1206](https://redirect.github.com/serde-rs/json/issues/1206))

###
[`v1.0.131`](https://redirect.github.com/serde-rs/json/releases/tag/1.0.131)

[Compare
Source](https://redirect.github.com/serde-rs/json/compare/1.0.130...1.0.131)

- Implement Deserializer and IntoDeserializer for `Map<String, Value>`
and `&Map<String, Value>`
([#&#8203;1135](https://redirect.github.com/serde-rs/json/issues/1135),
thanks [@&#8203;swlynch99](https://redirect.github.com/swlynch99))

###
[`v1.0.130`](https://redirect.github.com/serde-rs/json/releases/tag/1.0.130)

[Compare
Source](https://redirect.github.com/serde-rs/json/compare/1.0.129...1.0.130)

- Support converting and deserializing `Number` from i128 and u128
([#&#8203;1141](https://redirect.github.com/serde-rs/json/issues/1141),
thanks [@&#8203;druide](https://redirect.github.com/druide))

###
[`v1.0.129`](https://redirect.github.com/serde-rs/json/releases/tag/1.0.129)

[Compare
Source](https://redirect.github.com/serde-rs/json/compare/1.0.128...1.0.129)

- Add
[`serde_json::Map::sort_keys`](https://docs.rs/serde_json/1/serde_json/struct.Map.html#method.sort_keys)
and
[`serde_json::Value::sort_all_objects`](https://docs.rs/serde_json/1/serde_json/enum.Value.html#method.sort_all_objects)
([#&#8203;1199](https://redirect.github.com/serde-rs/json/issues/1199))

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4xNDIuNyIsInVwZGF0ZWRJblZlciI6IjM4LjE0Mi43IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-07 09:26:57 +02:00
renovate[bot]
74540231e5 Update Rust crate pulldown-cmark to v0.12.2 (#20325)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [pulldown-cmark](https://redirect.github.com/raphlinus/pulldown-cmark)
| workspace.dependencies | patch | `0.12.1` -> `0.12.2` |

---

### Release Notes

<details>
<summary>raphlinus/pulldown-cmark (pulldown-cmark)</summary>

###
[`v0.12.2`](https://redirect.github.com/pulldown-cmark/pulldown-cmark/releases/tag/v0.12.2):
0.12.2

[Compare
Source](https://redirect.github.com/raphlinus/pulldown-cmark/compare/v0.12.1...v0.12.2)

#### What's Changed

- Fix compiilation error in fuzzers by
[@&#8203;kdarkhan](https://redirect.github.com/kdarkhan) in
[https://github.com/pulldown-cmark/pulldown-cmark/pull/947](https://redirect.github.com/pulldown-cmark/pulldown-cmark/pull/947)
- Make `fuzz` dir part of the workspace by
[@&#8203;kdarkhan](https://redirect.github.com/kdarkhan) in
[https://github.com/pulldown-cmark/pulldown-cmark/pull/948](https://redirect.github.com/pulldown-cmark/pulldown-cmark/pull/948)
- Fix and improve `bench` by
[@&#8203;notriddle](https://redirect.github.com/notriddle) in
[https://github.com/pulldown-cmark/pulldown-cmark/pull/950](https://redirect.github.com/pulldown-cmark/pulldown-cmark/pull/950)
- Reuse a couple hash maps across blocks by
[@&#8203;notriddle](https://redirect.github.com/notriddle) in
[https://github.com/pulldown-cmark/pulldown-cmark/pull/951](https://redirect.github.com/pulldown-cmark/pulldown-cmark/pull/951)
- Reuse outer indent between item list, def list, and blockquote by
[@&#8203;notriddle](https://redirect.github.com/notriddle) in
[https://github.com/pulldown-cmark/pulldown-cmark/pull/952](https://redirect.github.com/pulldown-cmark/pulldown-cmark/pull/952)
- Add instructions on fixing fuzz build by
[@&#8203;kdarkhan](https://redirect.github.com/kdarkhan) in
[https://github.com/pulldown-cmark/pulldown-cmark/pull/953](https://redirect.github.com/pulldown-cmark/pulldown-cmark/pull/953)
- Account for definition list fixups while popping containers by
[@&#8203;notriddle](https://redirect.github.com/notriddle) in
[https://github.com/pulldown-cmark/pulldown-cmark/pull/954](https://redirect.github.com/pulldown-cmark/pulldown-cmark/pull/954)
- Use byte range instead of char count for delim run bounds by
[@&#8203;notriddle](https://redirect.github.com/notriddle) in
[https://github.com/pulldown-cmark/pulldown-cmark/pull/956](https://redirect.github.com/pulldown-cmark/pulldown-cmark/pull/956)
- CI improvements by
[@&#8203;kdarkhan](https://redirect.github.com/kdarkhan) in
[https://github.com/pulldown-cmark/pulldown-cmark/pull/955](https://redirect.github.com/pulldown-cmark/pulldown-cmark/pull/955)
- Fix a problem that causes multiple dt's to be parsed by
[@&#8203;notriddle](https://redirect.github.com/notriddle) in
[https://github.com/pulldown-cmark/pulldown-cmark/pull/958](https://redirect.github.com/pulldown-cmark/pulldown-cmark/pull/958)
- fix: emit `InlineHtml` for inline HTML inside blockquote instead of
`Html` by [@&#8203;rhysd](https://redirect.github.com/rhysd) in
[https://github.com/pulldown-cmark/pulldown-cmark/pull/961](https://redirect.github.com/pulldown-cmark/pulldown-cmark/pull/961)
- Complete the list of block item bodies by
[@&#8203;notriddle](https://redirect.github.com/notriddle) in
[https://github.com/pulldown-cmark/pulldown-cmark/pull/962](https://redirect.github.com/pulldown-cmark/pulldown-cmark/pull/962)
- Implement into_static for CowStr and Event in pulldown-cmark by
[@&#8203;Atreyagaurav](https://redirect.github.com/Atreyagaurav) in
[https://github.com/pulldown-cmark/pulldown-cmark/pull/967](https://redirect.github.com/pulldown-cmark/pulldown-cmark/pull/967)
- Enforce cargo fmt by
[@&#8203;ollpu](https://redirect.github.com/ollpu) in
[https://github.com/pulldown-cmark/pulldown-cmark/pull/971](https://redirect.github.com/pulldown-cmark/pulldown-cmark/pull/971)
- Respect line starts when trimming header endings by
[@&#8203;notriddle](https://redirect.github.com/notriddle) in
[https://github.com/pulldown-cmark/pulldown-cmark/pull/969](https://redirect.github.com/pulldown-cmark/pulldown-cmark/pull/969)

#### New Contributors

- [@&#8203;Atreyagaurav](https://redirect.github.com/Atreyagaurav) made
their first contribution in
[https://github.com/pulldown-cmark/pulldown-cmark/pull/967](https://redirect.github.com/pulldown-cmark/pulldown-cmark/pull/967)

**Full Changelog**:
https://github.com/pulldown-cmark/pulldown-cmark/compare/v0.12.1...v0.12.2

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4xNDIuNyIsInVwZGF0ZWRJblZlciI6IjM4LjE0Mi43IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-07 09:05:22 +02:00
renovate[bot]
5c0ecc09fb Update Rust crate wasmtime-wasi to v24.0.2 (#20335)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [wasmtime-wasi](https://redirect.github.com/bytecodealliance/wasmtime)
| workspace.dependencies | patch | `24.0.1` -> `24.0.2` |

---

### Release Notes

<details>
<summary>bytecodealliance/wasmtime (wasmtime-wasi)</summary>

###
[`v24.0.2`](https://redirect.github.com/bytecodealliance/wasmtime/releases/tag/v24.0.2)

[Compare
Source](https://redirect.github.com/bytecodealliance/wasmtime/compare/v24.0.1...v24.0.2)

#### 24.0.2

Released 2024-11-05.

##### Fixed

- Update to cap-std 3.4.1, for
[#&#8203;9559](https://redirect.github.com/bytecodealliance/wasmtime/issues/9559),
which fixes a wasi-filesystem sandbox
    escape on Windows.

[CVE-2024-51745](https://redirect.github.com/bytecodealliance/wasmtime/security/advisories/GHSA-c2f5-jxjv-2hh8).

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4xNDIuNyIsInVwZGF0ZWRJblZlciI6IjM4LjE0Mi43IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-07 09:04:52 +02:00
renovate[bot]
5b59ef3456 Update Rust crate thiserror to v1.0.68 (#20332) 2024-11-07 08:52:00 +02:00
renovate[bot]
fda3e4c69a Update Rust crate sys-locale to v0.3.2 (#20331)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [sys-locale](https://redirect.github.com/1Password/sys-locale) |
workspace.dependencies | patch | `0.3.1` -> `0.3.2` |

---

### Release Notes

<details>
<summary>1Password/sys-locale (sys-locale)</summary>

###
[`v0.3.2`](https://redirect.github.com/1Password/sys-locale/releases/tag/v0.3.2)

[Compare
Source](https://redirect.github.com/1Password/sys-locale/compare/v0.3.1...v0.3.2)

#### What's Changed

##### Added

- Support for all other Apple targets, such as watchOS and tvOS by
[@&#8203;complexspaces](https://redirect.github.com/complexspaces) in
[https://github.com/1Password/sys-locale/pull/38](https://redirect.github.com/1Password/sys-locale/pull/38).
- Support for ignoring POSIX modifiers in UNIX locales with them present
by [@&#8203;pasabanov](https://redirect.github.com/pasabanov) in
[https://github.com/1Password/sys-locale/pull/33](https://redirect.github.com/1Password/sys-locale/pull/33).
    -   Parsing support/recognition may come at a later date.
- Support for returning a list of user locales on Linux/BSD UNIX
platforms by [@&#8203;pasabanov](https://redirect.github.com/pasabanov)
in
[https://github.com/1Password/sys-locale/pull/35](https://redirect.github.com/1Password/sys-locale/pull/35).

##### Fixed

- No longer use `LC_CTYPE` when determining the locale; the crate now
uses `LC_MESSAGES` in its place by
[@&#8203;pasabanov](https://redirect.github.com/pasabanov) in
[https://github.com/1Password/sys-locale/pull/35](https://redirect.github.com/1Password/sys-locale/pull/35).
- Skip empty locale environment variables on UNIX platforms by
[@&#8203;complexspaces](https://redirect.github.com/complexspaces) in
[https://github.com/1Password/sys-locale/pull/29](https://redirect.github.com/1Password/sys-locale/pull/29).
- Corrected types mentioned and improved the public API documentation by
[@&#8203;pasabanov](https://redirect.github.com/pasabanov) in
[https://github.com/1Password/sys-locale/pull/37](https://redirect.github.com/1Password/sys-locale/pull/37).

##### Changed

- Improved crate download size by excluding unused directories and files
by [@&#8203;pasabanov](https://redirect.github.com/pasabanov).
- Very slight improvement to locale fetching performance on Windows by
[@&#8203;complexspaces](https://redirect.github.com/complexspaces) in
[https://github.com/1Password/sys-locale/pull/29](https://redirect.github.com/1Password/sys-locale/pull/29).
- Increased MSRV to Rust 1.56, which is 3 years old as of this release
by [@&#8203;complexspaces](https://redirect.github.com/complexspaces).

#### New Contributors

- [@&#8203;pasabanov](https://redirect.github.com/pasabanov) made their
first contribution in
[https://github.com/1Password/sys-locale/pull/30](https://redirect.github.com/1Password/sys-locale/pull/30)

**Full Changelog**:
https://github.com/1Password/sys-locale/compare/v0.3.1...v0.3.2

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4xNDIuNyIsInVwZGF0ZWRJblZlciI6IjM4LjE0Mi43IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-06 20:38:00 -07:00
renovate[bot]
028c2a8249 Update Rust crate anyhow to v1.0.93 (#20321)
This PR contains the following updates:

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

---

### Release Notes

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

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

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

-   Update dev-dependencies to `thiserror` v2

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

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

- Support Rust 1.82's `&raw const` and `&raw mut` syntax inside
`ensure!`
([#&#8203;390](https://redirect.github.com/dtolnay/anyhow/issues/390))

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4xNDIuNyIsInVwZGF0ZWRJblZlciI6IjM4LjE0Mi43IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-06 20:37:43 -07:00
renovate[bot]
a86c4deb78 Update Rust crate mdbook to v0.4.41 (#20322)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [mdbook](https://redirect.github.com/rust-lang/mdBook) | dependencies
| patch | `0.4.40` -> `0.4.41` |

---

### Release Notes

<details>
<summary>rust-lang/mdBook (mdbook)</summary>

###
[`v0.4.41`](https://redirect.github.com/rust-lang/mdBook/blob/HEAD/CHANGELOG.md#mdBook-0441)

[Compare
Source](https://redirect.github.com/rust-lang/mdBook/compare/v0.4.40...v0.4.41)


[v0.4.40...v0.4.41](https://redirect.github.com/rust-lang/mdBook/compare/v0.4.40...v0.4.41)

##### Added

-   Added preliminary support for Rust 2024 edition.
[#&#8203;2398](https://redirect.github.com/rust-lang/mdBook/pull/2398)
-   Added a full example of the remove-emphasis preprocessor.
[#&#8203;2464](https://redirect.github.com/rust-lang/mdBook/pull/2464)

##### Changed

-   Adjusted styling of clipboard/play icons.
[#&#8203;2421](https://redirect.github.com/rust-lang/mdBook/pull/2421)
-   Updated to handlebars v6.
[#&#8203;2416](https://redirect.github.com/rust-lang/mdBook/pull/2416)
-   Attr and section rules now have specific code highlighting.
[#&#8203;2448](https://redirect.github.com/rust-lang/mdBook/pull/2448)
- The sidebar is now loaded from a common file, significantly reducing
the book size when there are many chapters.
[#&#8203;2414](https://redirect.github.com/rust-lang/mdBook/pull/2414)
-   Updated dependencies.
[#&#8203;2470](https://redirect.github.com/rust-lang/mdBook/pull/2470)

##### Fixed

-   Improved theme support when JavaScript is disabled.
[#&#8203;2454](https://redirect.github.com/rust-lang/mdBook/pull/2454)
-   Fixed broken themes when localStorage has an invalid theme id.
[#&#8203;2463](https://redirect.github.com/rust-lang/mdBook/pull/2463)
- Adjusted the line-height of superscripts (and footnotes) to avoid
adding extra space between lines.
[#&#8203;2465](https://redirect.github.com/rust-lang/mdBook/pull/2465)

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4xNDIuNyIsInVwZGF0ZWRJblZlciI6IjM4LjE0Mi43IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-06 20:37:36 -07:00
Conrad Irwin
e645aa9d20 Root rename detection (#20313)
Closes #5349

Release Notes:

- Fixed Zed when the directory that you opened is renamed.
2024-11-06 20:36:59 -07:00
Nate Butler
216ea4ddc4 Unify Solarized colors (#20330)
Lily over on discord noticed two of the colors in our Solarized themes
were off by a single point. The two colors are nearly indistinguishable,
so we might as well unify them.

This PR does exactly that.

Release Notes:

- N/A
2024-11-06 21:15:45 -05:00
Nate Butler
29c5ea0a50 More previews (#20329)
Release Notes:

- N/A
2024-11-06 21:15:35 -05:00
Marshall Bowers
b129e18396 Make slash command output streamable (#19632)
This PR adds support for streaming output from slash commands

In this PR we are focused primarily on the interface of the
`SlashCommand` trait to support streaming the output. We will follow up
later with support for extensions and context servers to take advantage
of the streaming nature.

Release Notes:

- N/A

---------

Co-authored-by: David Soria Parra <davidsp@anthropic.com>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: David <david@anthropic.com>
Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Max <max@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Will <will@zed.dev>
2024-11-06 16:24:43 -08:00
Nate Butler
f6fbf662b4 Add ui::ComponentPreview (#20319)
The `ComponentPreview` trait enables rendering storybook-like previews
of components inside of Zed.


![CleanShot 2024-11-06 at 16 32
25@2x](https://github.com/user-attachments/assets/6894663f-1bbc-4a40-b420-33882e9e239a)


This initial version will work for any component that doesn't return a
view.

Example impl:

```rust
impl ComponentPreview for Checkbox {
    fn description() -> impl Into<Option<&'static str>> {
        "A checkbox lets people choose between opposing..."
    }

    fn examples() -> Vec<ComponentExampleGroup<Self>> {
        vec![
            example_group(
                "Default",
                vec![
                    single_example(
                        "Unselected",
                        Checkbox::new("checkbox_unselected", Selection::Unselected),
                    ),
                    // ... more examples
                ],
            ),
            // ... more examples
        ]
    }
}
```

Example usage:

```rust
fn render_components_page(&self, cx: &ViewContext<Self>) -> impl IntoElement {
        v_flex()
            .gap_2()
            .child(Checkbox::render_component_previews(cx))
            .child(Icon::render_component_previews(cx))
    }
}
```

Release Notes:

- N/A
2024-11-06 16:54:18 -05:00
Peter Schilling
a409123342 Show workspace name before filename in window title (#20310)
when searching for the appropriate zed window, i scan a list of window
titles. putting the workspace before the filename makes this list a lot
easier to scan.

![CleanShot 2024-11-06 at 11 07
01@2x](https://github.com/user-attachments/assets/2dcbe96d-6f91-443e-bfd2-10bd1c81e679)

screenshot of [alt tab](https://alt-tab-macos.netlify.app/) in mac os
demonstrating how putting the workspace first makes it easier to locate
a project.


Release Notes:

- Improved window title by showing workspace name before filename
2024-11-06 22:34:49 +02:00
renovate[bot]
b0b29d91f9 Update cloudflare/wrangler-action digest to 05f17c4 (#20315)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[cloudflare/wrangler-action](https://redirect.github.com/cloudflare/wrangler-action)
| action | digest | `b2a0191` -> `05f17c4` |

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4xNDIuNyIsInVwZGF0ZWRJblZlciI6IjM4LjE0Mi43IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-06 22:29:17 +02:00
renovate[bot]
290c9113b7 Update astral-sh/setup-uv digest to 2e657c1 (#20314)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [astral-sh/setup-uv](https://redirect.github.com/astral-sh/setup-uv) |
action | digest | `f3bcaeb` -> `2e657c1` |

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4xNDIuNyIsInVwZGF0ZWRJblZlciI6IjM4LjE0Mi43IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-06 22:26:09 +02:00
Danilo Leal
36427e0a87 assistant: Refine role crease loading state design (#20311)
Follow up to https://github.com/zed-industries/zed/pull/20125 — going
for that <kbd>kbd</kbd> look.

<img width="230" alt="Screenshot 2024-11-06 at 16 51 27"
src="https://github.com/user-attachments/assets/d29a2650-8bf8-4500-8dc1-92b0be210e04">

Release Notes:

- N/A
2024-11-06 17:11:34 -03:00
Floyd Wang
e16d5c3a68 gpui: Bump crates resvg and usvg to 0.44.0 (#20067)
Closes #17388

Release Notes:

- N/A

We are using gpui to build a project, and we want to render SVGs with
the `<text>` tag. We use `resvg` and `usvg` with the same version as
gpui, like `0.41.0`. However, when we enable the feature `text`, we get
an error from `usvg`.

```shell
error[E0061]: this function takes 3 arguments but 2 arguments were supplied
  --> /Users/madcodelife/.cargo/git/checkouts/zed-23e65a6dff445450/e681a4b/crates/gpui/src/svg_renderer.rs:49:20
   |
49 |         let tree = usvg::Tree::from_data(bytes, &usvg::Options::default())?;
   |                    ^^^^^^^^^^^^^^^^^^^^^---------------------------------- argument #3 of type `&Database` is missing
   |
```

This error occurs because when the `text` feature is enabled, the
`form_data` function needs an extra argument, `fontdb`.
[The code is
here](fb7e28513f/crates/usvg/src/parser/mod.rs (L98)).

They changed the API in version
[`0.42.0`](b1d06e9463/crates/usvg/src/parser/mod.rs (L98)).

So, I updated the versions to the latest (0.44.0).

This is our demo:

## Before:
<img width="620" alt="image"
src="https://github.com/user-attachments/assets/7c71f8b1-e5fe-4e60-8f21-bb3bd9924e03">

## After:
<img width="620" alt="image"
src="https://github.com/user-attachments/assets/4b0a0602-928f-4017-b5df-859eeb5f6b4a">
2024-11-06 21:40:22 +02:00
Conrad Irwin
608addf641 Extension refactor (#20305)
This contains the main changes to the extensions crate from #20049. The
primary goal here is removing dependencies that we can't include on the
remote.


Release Notes:

- N/A

---------

Co-authored-by: Mikayla <mikayla@zed.dev>
Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
Co-authored-by: Marshall <marshall@zed.dev>
2024-11-06 10:06:25 -07:00
Nate Butler
f22e56ff42 Improve One theme contrasts (#20304)
Closes #5334
Closes #15521 

Improve contrast across the board in default One themes. 

We are currently building out some theme tools to make improvements to
contrast and tweaking themes in general easier, so these should continue
to improve over time.

**Light**

Before | After

![CleanShot 2024-11-06 at 11 26
02@2x](https://github.com/user-attachments/assets/2e5d5649-4131-4d42-accc-9bbcf34defeb)

**Dark**
Before | After

![CleanShot 2024-11-06 at 11 30
26@2x](https://github.com/user-attachments/assets/78bc5134-6113-48fd-b026-88e427c631a3)

**Note 1**: there are more improvements to be made, but this should deal
with the most egregious issues.

Release Notes:

- Improved contrast in default One themes
2024-11-06 11:56:02 -05:00
renovate[bot]
449e20de3d Update Rust crate wasmtime to v24.0.2 [SECURITY] (#20262)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [wasmtime](https://redirect.github.com/bytecodealliance/wasmtime) |
workspace.dependencies | patch | `24.0.1` -> `24.0.2` |

### GitHub Vulnerability Alerts

####
[CVE-2024-51745](https://redirect.github.com/bytecodealliance/wasmtime/security/advisories/GHSA-c2f5-jxjv-2hh8)

### Impact

Wasmtime's filesystem sandbox implementation on Windows blocks access to
special device filenames such as "COM1", "COM2", "LPT0", "LPT1", and so
on, however it did not block access to the special device filenames
which use superscript digits, such as "COM¹", "COM²", "LPT⁰", "LPT¹",
and so on. Untrusted Wasm programs that are given access to any
filesystem directory could bypass the sandbox and access devices through
those special device filenames with superscript digits, and through them
gain access peripheral devices connected to the computer, or network
resources mapped to those devices. This can include modems, printers,
network printers, and any other device connected to a serial or parallel
port, including emulated USB serial ports.

### Patches

Patch releases for Wasmtime have been issued as 24.0.2, 25.0.3, and
26.0.1. Users of Wasmtime 23.0.x and prior are recommended to upgrade to
one of these patched versions.

### Workarounds

There are no known workarounds for this issue. Affected Windows users
are recommended to upgrade.

### References

- [Microsoft's
documentation](https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions)
of the special device filenames
 - [ISO-8859-1](https://en.wikipedia.org/wiki/ISO/IEC_8859-1)
- [The original PR reporting the
issue](https://redirect.github.com/bytecodealliance/cap-std/pull/371)

---

### Release Notes

<details>
<summary>bytecodealliance/wasmtime (wasmtime)</summary>

###
[`v24.0.2`](https://redirect.github.com/bytecodealliance/wasmtime/releases/tag/v24.0.2)

[Compare
Source](https://redirect.github.com/bytecodealliance/wasmtime/compare/v24.0.1...v24.0.2)

#### 24.0.2

Released 2024-11-05.

##### Fixed

- Update to cap-std 3.4.1, for
[#&#8203;9559](https://redirect.github.com/bytecodealliance/wasmtime/issues/9559),
which fixes a wasi-filesystem sandbox
    escape on Windows.

[CVE-2024-51745](https://redirect.github.com/bytecodealliance/wasmtime/security/advisories/GHSA-c2f5-jxjv-2hh8).

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4xNDIuNyIsInVwZGF0ZWRJblZlciI6IjM4LjE0Mi43IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-06 09:21:50 -07:00
Peter Tripp
1aac35cc1c v0.162.x dev 2024-11-06 11:09:13 -05:00
Piotr Osiewicz
4df1144659 WIP 2024-11-05 18:51:13 +01:00
Piotr Osiewicz
42258616f0 Move context menu code in editor into it's own file + start on adjustable position for completions menu 2024-11-05 17:44:59 +01:00
165 changed files with 8262 additions and 3513 deletions

View File

@@ -6,6 +6,7 @@ on:
jobs:
stale:
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
steps:
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9

View File

@@ -12,7 +12,7 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Set up uv
uses: astral-sh/setup-uv@f3bcaebff5eace81a1c062af9f9011aae482ca9d # v3
uses: astral-sh/setup-uv@2e657c127d5b1635d5a8e3fa40e0ac50a5bf6992 # v3
with:
version: "latest"
enable-cache: true

View File

@@ -12,7 +12,7 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Set up uv
uses: astral-sh/setup-uv@f3bcaebff5eace81a1c062af9f9011aae482ca9d # v3
uses: astral-sh/setup-uv@2e657c127d5b1635d5a8e3fa40e0ac50a5bf6992 # v3
with:
version: "latest"
enable-cache: true

View File

@@ -37,28 +37,28 @@ jobs:
mdbook build ./docs --dest-dir=../target/deploy/docs/
- name: Deploy Docs
uses: cloudflare/wrangler-action@b2a0191ce60d21388e1a8dcc968b4e9966f938e1 # v3
uses: cloudflare/wrangler-action@05f17c4a695b4d94b57b59997562c6a4624c64e4 # v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy target/deploy --project-name=docs
- name: Deploy Install
uses: cloudflare/wrangler-action@b2a0191ce60d21388e1a8dcc968b4e9966f938e1 # v3
uses: cloudflare/wrangler-action@05f17c4a695b4d94b57b59997562c6a4624c64e4 # v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: r2 object put -f script/install.sh zed-open-source-website-assets/install.sh
- name: Deploy Docs Workers
uses: cloudflare/wrangler-action@b2a0191ce60d21388e1a8dcc968b4e9966f938e1 # v3
uses: cloudflare/wrangler-action@05f17c4a695b4d94b57b59997562c6a4624c64e4 # v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy .cloudflare/docs-proxy/src/worker.js
- name: Deploy Install Workers
uses: cloudflare/wrangler-action@b2a0191ce60d21388e1a8dcc968b4e9966f938e1 # v3
uses: cloudflare/wrangler-action@05f17c4a695b4d94b57b59997562c6a4624c64e4 # v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

744
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -339,6 +339,7 @@ chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4.4", features = ["derive"] }
clickhouse = "0.11.6"
cocoa = "0.26"
cocoa-foundation = "0.2.0"
convert_case = "0.6.0"
core-foundation = "0.9.3"
core-foundation-sys = "0.8.6"
@@ -350,6 +351,7 @@ ec4rs = "1.1"
emojis = "0.6.1"
env_logger = "0.11"
exec = "0.3.1"
fancy-regex = "0.14.0"
fork = "0.2.0"
futures = "0.3"
futures-batch = "0.6.1"

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-file-search"><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M4.268 21a2 2 0 0 0 1.727 1H18a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v3"/><path d="m9 18-1.5-1.5"/><circle cx="5" cy="14" r="3"/></svg>

After

Width:  |  Height:  |  Size: 393 B

View File

@@ -56,7 +56,8 @@
"shift-tab": "editor::TabPrev",
"ctrl-k": "editor::CutToEndOfLine",
// "ctrl-t": "editor::Transpose",
"alt-q": "editor::Rewrap",
"ctrl-k ctrl-q": "editor::Rewrap",
"ctrl-k q": "editor::Rewrap",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd",
"shift-delete": "editor::Cut",
@@ -126,7 +127,8 @@
"shift-enter": "editor::Newline",
"ctrl-enter": "editor::NewlineAbove",
"ctrl-shift-enter": "editor::NewlineBelow",
"alt-z": "editor::ToggleSoftWrap",
"ctrl-k ctrl-z": "editor::ToggleSoftWrap",
"ctrl-k z": "editor::ToggleSoftWrap",
"ctrl-f": "buffer_search::Deploy",
"ctrl-h": ["buffer_search::Deploy", { "replace_enabled": true }],
// "cmd-e": ["buffer_search::Deploy", { "focus": false }],
@@ -140,7 +142,7 @@
"bindings": {
"alt-]": "editor::NextInlineCompletion",
"alt-[": "editor::PreviousInlineCompletion",
"ctrl-right": "editor::AcceptPartialInlineCompletion"
"alt-right": "editor::AcceptPartialInlineCompletion"
}
},
{
@@ -169,7 +171,7 @@
"ctrl-k c": "assistant::CopyCode",
"ctrl-g": "search::SelectNextMatch",
"ctrl-shift-g": "search::SelectPrevMatch",
"alt-m": "assistant::ToggleModelSelector",
"ctrl-shift-m": "assistant::ToggleModelSelector",
"ctrl-k h": "assistant::DeployHistory",
"ctrl-k l": "assistant::DeployPromptLibrary",
"ctrl-n": "assistant::NewContext"
@@ -251,10 +253,10 @@
"ctrl-shift-pagedown": "pane::SwapItemRight",
"ctrl-w": "pane::CloseActiveItem",
"ctrl-f4": "pane::CloseActiveItem",
"alt-ctrl-t": "pane::CloseInactiveItems",
"alt-ctrl-t": ["pane::CloseInactiveItems", { "close_pinned": false }],
"alt-ctrl-shift-w": "workspace::CloseInactiveTabsAndPanes",
"ctrl-k u": "pane::CloseCleanItems",
"ctrl-k w": "pane::CloseAllItems",
"ctrl-k u": ["pane::CloseCleanItems", { "close_pinned": false }],
"ctrl-k w": ["pane::CloseAllItems", { "close_pinned": false }],
"ctrl-shift-f": "project_search::ToggleFocus",
"ctrl-alt-g": "search::SelectNextMatch",
"ctrl-alt-shift-g": "search::SelectPrevMatch",
@@ -326,7 +328,6 @@
"ctrl-k ctrl-j": "editor::UnfoldAll",
"ctrl-space": "editor::ShowCompletions",
"ctrl-.": "editor::ToggleCodeActions",
"alt-ctrl-r": "editor::RevealInFileManager",
"ctrl-k r": "editor::RevealInFileManager",
"ctrl-k p": "editor::CopyPath",
"ctrl-\\": "pane::SplitRight",
@@ -417,6 +418,8 @@
"ctrl-k shift-up": ["workspace::SwapPaneInDirection", "Up"],
"ctrl-k shift-down": ["workspace::SwapPaneInDirection", "Down"],
"ctrl-shift-x": "zed::Extensions",
"ctrl-shift-r": "task::Rerun",
"ctrl-alt-r": "task::Rerun",
"alt-t": "task::Rerun",
"alt-shift-t": "task::Spawn"
}
@@ -564,9 +567,11 @@
"ctrl-alt-c": "outline_panel::CopyPath",
"alt-ctrl-shift-c": "outline_panel::CopyRelativePath",
"alt-ctrl-r": "outline_panel::RevealInFileManager",
"space": ["outline_panel::Open", { "change_selection": false }],
"space": "outline_panel::Open",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrev"
"shift-up": "menu::SelectPrev",
"alt-enter": "editor::OpenExcerpts",
"ctrl-k enter": "editor::OpenExcerptsSplit"
}
},
{
@@ -587,7 +592,7 @@
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
"alt-ctrl-r": "project_panel::RevealInFileManager",
"ctrl-shift-enter": "project_panel::OpenWithSystem",
"alt-shift-f": "project_panel::NewSearchInDirectory",
"ctrl-shift-f": "project_panel::NewSearchInDirectory",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrev",
"escape": "menu::Cancel"

View File

@@ -51,14 +51,13 @@
"shift-tab": "editor::TabPrev",
"ctrl-k": "editor::CutToEndOfLine",
"ctrl-t": "editor::Transpose",
"alt-q": "editor::Rewrap",
"cmd-k q": "editor::Rewrap",
"cmd-k cmd-q": "editor::Rewrap",
"cmd-backspace": "editor::DeleteToBeginningOfLine",
"cmd-delete": "editor::DeleteToEndOfLine",
"alt-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-w": "editor::DeleteToPreviousWordStart",
"alt-delete": "editor::DeleteToNextWordEnd",
"alt-h": "editor::DeleteToPreviousWordStart",
"alt-d": "editor::DeleteToNextWordEnd",
"cmd-x": "editor::Cut",
"cmd-c": "editor::Copy",
"cmd-v": "editor::Paste",
@@ -86,9 +85,7 @@
"ctrl-f": "editor::MoveRight",
"ctrl-l": "editor::ScrollCursorCenter",
"alt-left": "editor::MoveToPreviousWordStart",
"alt-b": "editor::MoveToPreviousWordStart",
"alt-right": "editor::MoveToNextWordEnd",
"alt-f": "editor::MoveToNextWordEnd",
"cmd-left": "editor::MoveToBeginningOfLine",
"ctrl-a": "editor::MoveToBeginningOfLine",
"cmd-right": "editor::MoveToEndOfLine",
@@ -104,9 +101,7 @@
"shift-right": "editor::SelectRight",
"ctrl-shift-f": "editor::SelectRight",
"alt-shift-left": "editor::SelectToPreviousWordStart", // cursorWordLeftSelect
"alt-shift-b": "editor::SelectToPreviousWordStart",
"alt-shift-right": "editor::SelectToNextWordEnd", // cursorWordRightSelect
"alt-shift-f": "editor::SelectToNextWordEnd",
"ctrl-shift-up": "editor::SelectToStartOfParagraph",
"ctrl-shift-down": "editor::SelectToEndOfParagraph",
"cmd-shift-up": "editor::SelectToBeginning",
@@ -121,7 +116,7 @@
"shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
"ctrl-shift-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
"ctrl-v": ["editor::MovePageDown", { "center_cursor": true }],
"alt-v": ["editor::MovePageUp", { "center_cursor": true }],
"ctrl-shift-v": ["editor::MovePageUp", { "center_cursor": true }],
"ctrl-cmd-space": "editor::ShowCharacterPalette",
"cmd-;": "editor::ToggleLineNumbers",
"cmd-alt-z": "editor::RevertSelectedHunks",
@@ -140,7 +135,7 @@
"shift-enter": "editor::Newline",
"cmd-enter": "editor::NewlineBelow",
"cmd-shift-enter": "editor::NewlineAbove",
"alt-z": "editor::ToggleSoftWrap",
"cmd-k z": "editor::ToggleSoftWrap",
"cmd-f": "buffer_search::Deploy",
"cmd-alt-f": ["buffer_search::Deploy", { "replace_enabled": true }],
"cmd-alt-l": ["buffer_search::Deploy", { "selection_search_enabled": true }],
@@ -155,7 +150,7 @@
"bindings": {
"alt-]": "editor::NextInlineCompletion",
"alt-[": "editor::PreviousInlineCompletion",
"cmd-right": "editor::AcceptPartialInlineCompletion"
"ctrl-right": "editor::AcceptPartialInlineCompletion"
}
},
{
@@ -191,7 +186,7 @@
"cmd-k c": "assistant::CopyCode",
"cmd-g": "search::SelectNextMatch",
"cmd-shift-g": "search::SelectPrevMatch",
"alt-m": "assistant::ToggleModelSelector",
"cmd-shift-m": "assistant::ToggleModelSelector",
"cmd-k h": "assistant::DeployHistory",
"cmd-k l": "assistant::DeployPromptLibrary",
"cmd-n": "assistant::NewContext"
@@ -291,10 +286,10 @@
"ctrl-shift-pageup": "pane::SwapItemLeft",
"ctrl-shift-pagedown": "pane::SwapItemRight",
"cmd-w": "pane::CloseActiveItem",
"alt-cmd-t": "pane::CloseInactiveItems",
"alt-cmd-t": ["pane::CloseInactiveItems", { "close_pinned": false }],
"ctrl-alt-cmd-w": "workspace::CloseInactiveTabsAndPanes",
"cmd-k u": "pane::CloseCleanItems",
"cmd-k cmd-w": "pane::CloseAllItems",
"cmd-k u": ["pane::CloseCleanItems", { "close_pinned": false }],
"cmd-k cmd-w": ["pane::CloseAllItems", { "close_pinned": false }],
"cmd-f": "project_search::ToggleFocus",
"cmd-g": "search::SelectNextMatch",
"cmd-shift-g": "search::SelectPrevMatch",
@@ -364,7 +359,6 @@
"cmd-k cmd-j": "editor::UnfoldAll",
"ctrl-space": "editor::ShowCompletions",
"cmd-.": "editor::ToggleCodeActions",
"alt-cmd-r": "editor::RevealInFileManager",
"cmd-k r": "editor::RevealInFileManager",
"cmd-k p": "editor::CopyPath",
"cmd-\\": "pane::SplitRight",
@@ -457,7 +451,9 @@
{
"context": "Workspace && !Terminal",
"bindings": {
"alt-t": "task::Rerun",
"cmd-shift-r": "task::Spawn",
"cmd-alt-r": "task::Rerun",
"alt-t": "task::Spawn",
"alt-shift-t": "task::Spawn"
}
},
@@ -577,9 +573,11 @@
"cmd-alt-c": "outline_panel::CopyPath",
"alt-cmd-shift-c": "outline_panel::CopyRelativePath",
"alt-cmd-r": "outline_panel::RevealInFileManager",
"space": ["outline_panel::Open", { "change_selection": false }],
"space": "outline_panel::Open",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrev"
"shift-up": "menu::SelectPrev",
"alt-enter": "editor::OpenExcerpts",
"cmd-k enter": "editor::OpenExcerptsSplit"
}
},
{
@@ -604,7 +602,7 @@
"alt-cmd-r": "project_panel::RevealInFileManager",
"ctrl-shift-enter": "project_panel::OpenWithSystem",
"cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"alt-shift-f": "project_panel::NewSearchInDirectory",
"cmd-shift-f": "project_panel::NewSearchInDirectory",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrev",
"escape": "menu::Cancel"

63
assets/keymaps/linux/emacs.json Executable file
View File

@@ -0,0 +1,63 @@
// documentation: https://zed.dev/docs/key-bindings
//
// To see the default key bindings run `zed: open default keymap`
// from the command palette.
[
{
"context": "Editor",
"bindings": {
"ctrl-g": "editor::Cancel",
"ctrl-shift-g": "go_to_line::Toggle",
//"ctrl-space": "editor::SetMark",
"ctrl-x u": "editor::Undo",
"ctrl-x ctrl-u": "editor::Redo",
"ctrl-f": "editor::MoveRight",
"ctrl-b": "editor::MoveLeft",
"ctrl-n": "editor::MoveDown",
"ctrl-p": "editor::MoveUp",
"ctrl-a": "editor::MoveToBeginningOfLine",
"ctrl-e": "editor::MoveToEndOfLine",
"alt-f": "editor::MoveToNextSubwordEnd",
"alt-b": "editor::MoveToPreviousSubwordStart",
"ctrl-d": "editor::Delete",
"alt-d": "editor::DeleteToNextWordEnd",
"ctrl-k": "editor::CutToEndOfLine",
"ctrl-w": "editor::Cut",
"alt-w": "editor::Copy",
"ctrl-y": "editor::Paste",
"ctrl-_": "editor::Undo",
"ctrl-v": "editor::MovePageDown",
"alt-v": "editor::MovePageUp",
"ctrl-x ]": "editor::MoveToEnd",
"ctrl-x [": "editor::MoveToBeginning",
"ctrl-l": "editor::ScrollCursorCenterTopBottom",
"ctrl-s": "buffer_search::Deploy",
"ctrl-x ctrl-f": "file_finder::Toggle",
"ctrl-shift-r": "editor::Rename"
}
},
{
"context": "Workspace",
"bindings": {
"ctrl-x k": "pane::CloseActiveItem",
"ctrl-x ctrl-c": "workspace::CloseWindow",
"ctrl-x o": "workspace::ActivateNextPane",
"ctrl-x b": "tab_switcher::Toggle",
"ctrl-x 0": "pane::CloseActiveItem",
"ctrl-x 1": "pane::CloseInactiveItems",
"ctrl-x 2": "pane::SplitVertical",
"ctrl-x ctrl-f": "file_finder::Toggle",
"ctrl-x ctrl-s": "workspace::Save",
"ctrl-x ctrl-w": "workspace::SaveAs",
"ctrl-x s": "workspace::SaveAll",
"shift shift": "file_finder::Toggle"
}
},
{
"context": "Pane",
"bindings": {
"ctrl-alt-left": "pane::GoBack",
"ctrl-alt-right": "pane::GoForward"
}
}
]

View File

@@ -1,6 +1,7 @@
[
{
"bindings": {
"ctrl-alt-s": "zed::OpenSettings",
"ctrl-shift-[": "pane::ActivatePrevItem",
"ctrl-shift-]": "pane::ActivateNextItem"
}
@@ -43,6 +44,7 @@
"shift-f2": "editor::GoToPrevDiagnostic",
"ctrl-alt-shift-down": "editor::GoToHunk",
"ctrl-alt-shift-up": "editor::GoToPrevHunk",
"ctrl-alt-z": "editor::RevertSelectedHunks",
"ctrl-home": "editor::MoveToBeginning",
"ctrl-end": "editor::MoveToEnd",
"ctrl-shift-home": "editor::SelectToBeginning",

63
assets/keymaps/macos/emacs.json Executable file
View File

@@ -0,0 +1,63 @@
// documentation: https://zed.dev/docs/key-bindings
//
// To see the default key bindings run `zed: open default keymap`
// from the command palette.
[
{
"context": "Editor",
"bindings": {
"ctrl-g": "editor::Cancel",
"ctrl-shift-g": "go_to_line::Toggle",
//"ctrl-space": "editor::SetMark",
"ctrl-x u": "editor::Undo",
"ctrl-x ctrl-u": "editor::Redo",
"ctrl-f": "editor::MoveRight",
"ctrl-b": "editor::MoveLeft",
"ctrl-n": "editor::MoveDown",
"ctrl-p": "editor::MoveUp",
"ctrl-a": "editor::MoveToBeginningOfLine",
"ctrl-e": "editor::MoveToEndOfLine",
"alt-f": "editor::MoveToNextSubwordEnd",
"alt-b": "editor::MoveToPreviousSubwordStart",
"ctrl-d": "editor::Delete",
"alt-d": "editor::DeleteToNextWordEnd",
"ctrl-k": "editor::CutToEndOfLine",
"ctrl-w": "editor::Cut",
"alt-w": "editor::Copy",
"ctrl-y": "editor::Paste",
"ctrl-_": "editor::Undo",
"ctrl-v": "editor::MovePageDown",
"alt-v": "editor::MovePageUp",
"ctrl-x ]": "editor::MoveToEnd",
"ctrl-x [": "editor::MoveToBeginning",
"ctrl-l": "editor::ScrollCursorCenterTopBottom",
"ctrl-s": "buffer_search::Deploy",
"ctrl-x ctrl-f": "file_finder::Toggle",
"ctrl-shift-r": "editor::Rename"
}
},
{
"context": "Workspace",
"bindings": {
"ctrl-x k": "pane::CloseActiveItem",
"ctrl-x ctrl-c": "workspace::CloseWindow",
"ctrl-x o": "workspace::ActivateNextPane",
"ctrl-x b": "tab_switcher::Toggle",
"ctrl-x 0": "pane::CloseActiveItem",
"ctrl-x 1": "pane::CloseInactiveItems",
"ctrl-x 2": "pane::SplitVertical",
"ctrl-x ctrl-f": "file_finder::Toggle",
"ctrl-x ctrl-s": "workspace::Save",
"ctrl-x ctrl-w": "workspace::SaveAs",
"ctrl-x s": "workspace::SaveAll",
"shift shift": "file_finder::Toggle"
}
},
{
"context": "Pane",
"bindings": {
"ctrl-alt-left": "pane::GoBack",
"ctrl-alt-right": "pane::GoForward"
}
}
]

View File

@@ -1,6 +1,7 @@
[
{
"context": "VimControl && !menu",
"use_layout_keys": true,
"bindings": {
"i": ["vim::PushOperator", { "Object": { "around": false } }],
"a": ["vim::PushOperator", { "Object": { "around": true } }],
@@ -171,6 +172,7 @@
},
{
"context": "vim_mode == normal",
"use_layout_keys": true,
"bindings": {
"escape": "editor::Cancel",
"ctrl-[": "editor::Cancel",
@@ -224,6 +226,7 @@
},
{
"context": "VimControl && VimCount",
"use_layout_keys": true,
"bindings": {
"0": ["vim::Number", 0],
":": "vim::CountCommand"
@@ -231,6 +234,7 @@
},
{
"context": "vim_mode == visual",
"use_layout_keys": true,
"bindings": {
":": "vim::VisualCommand",
"u": "vim::ConvertToLowerCase",
@@ -279,6 +283,7 @@
},
{
"context": "vim_mode == insert",
"use_layout_keys": true,
"bindings": {
"escape": "vim::NormalBefore",
"ctrl-c": "vim::NormalBefore",
@@ -304,6 +309,7 @@
},
{
"context": "vim_mode == insert && !(showing_code_actions || showing_completions)",
"use_layout_keys": true,
"bindings": {
"ctrl-p": "editor::ShowCompletions",
"ctrl-n": "editor::ShowCompletions"
@@ -311,6 +317,7 @@
},
{
"context": "vim_mode == replace",
"use_layout_keys": true,
"bindings": {
"escape": "vim::NormalBefore",
"ctrl-c": "vim::NormalBefore",
@@ -328,6 +335,7 @@
},
{
"context": "vim_mode == waiting",
"use_layout_keys": true,
"bindings": {
"tab": "vim::Tab",
"enter": "vim::Enter",
@@ -341,6 +349,7 @@
},
{
"context": "vim_mode == operator",
"use_layout_keys": true,
"bindings": {
"escape": "vim::ClearOperators",
"ctrl-c": "vim::ClearOperators",
@@ -349,6 +358,7 @@
},
{
"context": "vim_operator == a || vim_operator == i || vim_operator == cs",
"use_layout_keys": true,
"bindings": {
"w": "vim::Word",
"shift-w": ["vim::Word", { "ignorePunctuation": true }],
@@ -376,6 +386,7 @@
},
{
"context": "vim_operator == c",
"use_layout_keys": true,
"bindings": {
"c": "vim::CurrentLine",
"d": "editor::Rename", // zed specific
@@ -384,6 +395,7 @@
},
{
"context": "vim_operator == d",
"use_layout_keys": true,
"bindings": {
"d": "vim::CurrentLine",
"s": ["vim::PushOperator", "DeleteSurrounds"],
@@ -393,6 +405,7 @@
},
{
"context": "vim_operator == gu",
"use_layout_keys": true,
"bindings": {
"g u": "vim::CurrentLine",
"u": "vim::CurrentLine"
@@ -400,6 +413,7 @@
},
{
"context": "vim_operator == gU",
"use_layout_keys": true,
"bindings": {
"g shift-u": "vim::CurrentLine",
"shift-u": "vim::CurrentLine"
@@ -407,6 +421,7 @@
},
{
"context": "vim_operator == g~",
"use_layout_keys": true,
"bindings": {
"g ~": "vim::CurrentLine",
"~": "vim::CurrentLine"
@@ -414,6 +429,7 @@
},
{
"context": "vim_operator == gq",
"use_layout_keys": true,
"bindings": {
"g q": "vim::CurrentLine",
"q": "vim::CurrentLine",
@@ -423,6 +439,7 @@
},
{
"context": "vim_operator == y",
"use_layout_keys": true,
"bindings": {
"y": "vim::CurrentLine",
"s": ["vim::PushOperator", { "AddSurrounds": {} }]
@@ -430,30 +447,35 @@
},
{
"context": "vim_operator == ys",
"use_layout_keys": true,
"bindings": {
"s": "vim::CurrentLine"
}
},
{
"context": "vim_operator == >",
"use_layout_keys": true,
"bindings": {
">": "vim::CurrentLine"
}
},
{
"context": "vim_operator == <",
"use_layout_keys": true,
"bindings": {
"<": "vim::CurrentLine"
}
},
{
"context": "vim_operator == gc",
"use_layout_keys": true,
"bindings": {
"c": "vim::CurrentLine"
}
},
{
"context": "vim_mode == literal",
"use_layout_keys": true,
"bindings": {
"ctrl-@": ["vim::Literal", ["ctrl-@", "\u0000"]],
"ctrl-a": ["vim::Literal", ["ctrl-a", "\u0001"]],
@@ -497,6 +519,7 @@
},
{
"context": "BufferSearchBar && !in_replace",
"use_layout_keys": true,
"bindings": {
"enter": "vim::SearchSubmit",
"escape": "buffer_search::Dismiss"
@@ -504,6 +527,7 @@
},
{
"context": "ProjectPanel || CollabPanel || OutlinePanel || ChatPanel || VimControl || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView",
"use_layout_keys": true,
"bindings": {
// window related commands (ctrl-w X)
"ctrl-w": null,
@@ -554,6 +578,7 @@
},
{
"context": "EmptyPane || SharedScreen || MarkdownPreview || KeyContextView",
"use_layout_keys": true,
"bindings": {
":": "command_palette::Toggle",
"g /": "pane::DeploySearch"
@@ -562,6 +587,7 @@
{
// netrw compatibility
"context": "ProjectPanel && not_editing",
"use_layout_keys": true,
"bindings": {
":": "command_palette::Toggle",
"%": "project_panel::NewFile",
@@ -589,6 +615,7 @@
},
{
"context": "OutlinePanel && not_editing",
"use_layout_keys": true,
"bindings": {
"j": "menu::SelectNext",
"k": "menu::SelectPrev",

View File

@@ -27,15 +27,15 @@
"ghost_element.active": "#454a56ff",
"ghost_element.selected": "#454a56ff",
"ghost_element.disabled": "#2e343eff",
"text": "#c8ccd4ff",
"text.muted": "#838994ff",
"text.placeholder": "#696B77ff",
"text.disabled": "#696B77ff",
"text": "#dce0e5ff",
"text.muted": "#a9afbcff",
"text.placeholder": "#878a98ff",
"text.disabled": "#878a98ff",
"text.accent": "#74ade8ff",
"icon": "#c8ccd4ff",
"icon.muted": "#838994ff",
"icon.disabled": "#696B77ff",
"icon.placeholder": "#838994ff",
"icon": "#dce0e5ff",
"icon.muted": "#a9afbcff",
"icon.disabled": "#878a98ff",
"icon.placeholder": "#a9afbcff",
"icon.accent": "#74ade8ff",
"status_bar.background": "#3b414dff",
"title_bar.background": "#3b414dff",
@@ -60,19 +60,19 @@
"editor.active_line.background": "#2f343ebf",
"editor.highlighted_line.background": "#2f343eff",
"editor.line_number": "#c8ccd459",
"editor.active_line_number": "#c8ccd4ff",
"editor.invisible": "#696B77ff",
"editor.active_line_number": "#dce0e5ff",
"editor.invisible": "#878a98ff",
"editor.wrap_guide": "#c8ccd40d",
"editor.active_wrap_guide": "#c8ccd41a",
"editor.document_highlight.read_background": "#74ade81a",
"editor.document_highlight.write_background": "#555a6366",
"terminal.background": "#282c33ff",
"terminal.foreground": "#c8ccd4ff",
"terminal.bright_foreground": "#c8ccd4ff",
"terminal.foreground": "#dce0e5ff",
"terminal.bright_foreground": "#dce0e5ff",
"terminal.dim_foreground": "#282c33ff",
"terminal.ansi.black": "#282c33ff",
"terminal.ansi.bright_black": "#525561ff",
"terminal.ansi.dim_black": "#c8ccd4ff",
"terminal.ansi.dim_black": "#dce0e5ff",
"terminal.ansi.red": "#d07277ff",
"terminal.ansi.bright_red": "#673a3cff",
"terminal.ansi.dim_red": "#eab7b9ff",
@@ -91,8 +91,8 @@
"terminal.ansi.cyan": "#6eb4bfff",
"terminal.ansi.bright_cyan": "#3a565bff",
"terminal.ansi.dim_cyan": "#b9d9dfff",
"terminal.ansi.white": "#c8ccd4ff",
"terminal.ansi.bright_white": "#c8ccd4ff",
"terminal.ansi.white": "#dce0e5ff",
"terminal.ansi.bright_white": "#dce0e5ff",
"terminal.ansi.dim_white": "#575d65ff",
"link_text.hover": "#74ade8ff",
"conflict": "#dec184ff",
@@ -107,14 +107,14 @@
"error": "#d07277ff",
"error.background": "#d072771a",
"error.border": "#4c2b2cff",
"hidden": "#696B77ff",
"hidden.background": "#696B771a",
"hidden": "#878a98ff",
"hidden.background": "#696b771a",
"hidden.border": "#414754ff",
"hint": "#5a6f89ff",
"hint": "#788ca6ff",
"hint.background": "#5a6f891a",
"hint.border": "#293b5bff",
"ignored": "#696B77ff",
"ignored.background": "#696B771a",
"ignored": "#878a98ff",
"ignored.background": "#696b771a",
"ignored.border": "#464b57ff",
"info": "#74ade8ff",
"info.background": "#74ade81a",
@@ -131,7 +131,7 @@
"success": "#a1c181ff",
"success.background": "#a1c1811a",
"success.border": "#38482fff",
"unreachable": "#838994ff",
"unreachable": "#a9afbcff",
"unreachable.background": "#8389941a",
"unreachable.border": "#464b57ff",
"warning": "#dec184ff",
@@ -211,7 +211,7 @@
"font_weight": null
},
"embedded": {
"color": "#c8ccd4ff",
"color": "#dce0e5ff",
"font_style": null,
"font_weight": null
},
@@ -236,7 +236,7 @@
"font_weight": null
},
"hint": {
"color": "#5a6f89ff",
"color": "#788ca6ff",
"font_style": null,
"font_weight": 700
},
@@ -276,7 +276,7 @@
"font_weight": null
},
"preproc": {
"color": "#c8ccd4ff",
"color": "#dce0e5ff",
"font_style": null,
"font_weight": null
},
@@ -361,7 +361,7 @@
"font_weight": null
},
"variable": {
"color": "#c8ccd4ff",
"color": "#dce0e5ff",
"font_style": null,
"font_weight": null
},
@@ -402,15 +402,15 @@
"ghost_element.active": "#cacacaff",
"ghost_element.selected": "#cacacaff",
"ghost_element.disabled": "#ebebecff",
"text": "#383a41ff",
"text.muted": "#7e8087ff",
"text.placeholder": "#a1a1a3ff",
"text.disabled": "#a1a1a3ff",
"text": "#242529ff",
"text.muted": "#58585aff",
"text.placeholder": "#7e8086ff",
"text.disabled": "#7e8086ff",
"text.accent": "#5c78e2ff",
"icon": "#383a41ff",
"icon.muted": "#7e8087ff",
"icon.disabled": "#a1a1a3ff",
"icon.placeholder": "#7e8087ff",
"icon": "#242529ff",
"icon.muted": "#58585aff",
"icon.disabled": "#7e8086ff",
"icon.placeholder": "#58585aff",
"icon.accent": "#5c78e2ff",
"status_bar.background": "#dcdcddff",
"title_bar.background": "#dcdcddff",
@@ -428,26 +428,26 @@
"scrollbar.thumb.border": "#dfdfe0ff",
"scrollbar.track.background": "#00000000",
"scrollbar.track.border": "#eeeeeeff",
"editor.foreground": "#383a41ff",
"editor.foreground": "#242529ff",
"editor.background": "#fafafaff",
"editor.gutter.background": "#fafafaff",
"editor.subheader.background": "#ebebecff",
"editor.active_line.background": "#ebebecbf",
"editor.highlighted_line.background": "#ebebecff",
"editor.line_number": "#383a4159",
"editor.active_line_number": "#383a41ff",
"editor.active_line_number": "#242529ff",
"editor.invisible": "#a3a3a4ff",
"editor.wrap_guide": "#383a410d",
"editor.active_wrap_guide": "#383a411a",
"editor.document_highlight.read_background": "#5c78e21a",
"editor.document_highlight.write_background": "#a3a3a466",
"terminal.background": "#fafafaff",
"terminal.foreground": "#383a41ff",
"terminal.bright_foreground": "#383a41ff",
"terminal.foreground": "#242529ff",
"terminal.bright_foreground": "#242529ff",
"terminal.dim_foreground": "#fafafaff",
"terminal.ansi.black": "#fafafaff",
"terminal.ansi.bright_black": "#aaaaaaff",
"terminal.ansi.dim_black": "#383a41ff",
"terminal.ansi.dim_black": "#242529ff",
"terminal.ansi.red": "#d36151ff",
"terminal.ansi.bright_red": "#f0b0a4ff",
"terminal.ansi.dim_red": "#6f312aff",
@@ -466,11 +466,11 @@
"terminal.ansi.cyan": "#3a82b7ff",
"terminal.ansi.bright_cyan": "#a3bedaff",
"terminal.ansi.dim_cyan": "#254058ff",
"terminal.ansi.white": "#383a41ff",
"terminal.ansi.bright_white": "#383a41ff",
"terminal.ansi.white": "#242529ff",
"terminal.ansi.bright_white": "#242529ff",
"terminal.ansi.dim_white": "#97979aff",
"link_text.hover": "#5c78e2ff",
"conflict": "#dec184ff",
"conflict": "#a48819ff",
"conflict.background": "#faf2e6ff",
"conflict.border": "#f4e7d1ff",
"created": "#669f59ff",
@@ -482,19 +482,19 @@
"error": "#d36151ff",
"error.background": "#fbdfd9ff",
"error.border": "#f6c6bdff",
"hidden": "#a1a1a3ff",
"hidden": "#7e8086ff",
"hidden.background": "#dcdcddff",
"hidden.border": "#d3d3d4ff",
"hint": "#9294beff",
"hint": "#7274a7ff",
"hint.background": "#e2e2faff",
"hint.border": "#cbcdf6ff",
"ignored": "#a1a1a3ff",
"ignored": "#7e8086ff",
"ignored.background": "#dcdcddff",
"ignored.border": "#c9c9caff",
"info": "#5c78e2ff",
"info.background": "#e2e2faff",
"info.border": "#cbcdf6ff",
"modified": "#a47a23ff",
"modified": "#a48819ff",
"modified.background": "#faf2e6ff",
"modified.border": "#f4e7d1ff",
"predictive": "#9b9ec6ff",
@@ -506,10 +506,10 @@
"success": "#669f59ff",
"success.background": "#dfeadbff",
"success.border": "#c8dcc1ff",
"unreachable": "#7e8087ff",
"unreachable": "#58585aff",
"unreachable.background": "#dcdcddff",
"unreachable.border": "#c9c9caff",
"warning": "#dec184ff",
"warning": "#a48819ff",
"warning.background": "#faf2e6ff",
"warning.border": "#f4e7d1ff",
"players": [
@@ -544,7 +544,7 @@
"selection": "#d361513d"
},
{
"cursor": "#dec184ff",
"cursor": "#a48819ff",
"background": "#dec184ff",
"selection": "#dec1843d"
},
@@ -586,7 +586,7 @@
"font_weight": null
},
"embedded": {
"color": "#383a41ff",
"color": "#242529ff",
"font_style": null,
"font_weight": null
},
@@ -611,7 +611,7 @@
"font_weight": null
},
"hint": {
"color": "#9294beff",
"color": "#7274a7ff",
"font_style": null,
"font_weight": 700
},
@@ -651,12 +651,12 @@
"font_weight": null
},
"preproc": {
"color": "#383a41ff",
"color": "#242529ff",
"font_style": null,
"font_weight": null
},
"primary": {
"color": "#383a41ff",
"color": "#242529ff",
"font_style": null,
"font_weight": null
},
@@ -666,7 +666,7 @@
"font_weight": null
},
"punctuation": {
"color": "#383a41ff",
"color": "#242529ff",
"font_style": null,
"font_weight": null
},
@@ -736,7 +736,7 @@
"font_weight": null
},
"variable": {
"color": "#383a41ff",
"color": "#242529ff",
"font_style": null,
"font_weight": null
},

View File

@@ -519,8 +519,8 @@
"selection": "#d337813d"
},
{
"cursor": "#cb4b17ff",
"background": "#cb4b17ff",
"cursor": "#cb4b16ff",
"background": "#cb4b16ff",
"selection": "#cb4b173d"
},
{
@@ -596,7 +596,7 @@
"font_weight": 700
},
"enum": {
"color": "#cb4b17ff",
"color": "#cb4b16ff",
"font_style": null,
"font_weight": null
},
@@ -621,7 +621,7 @@
"font_weight": null
},
"link_text": {
"color": "#cb4b17ff",
"color": "#cb4b16ff",
"font_style": "italic",
"font_weight": null
},
@@ -636,7 +636,7 @@
"font_weight": null
},
"operator": {
"color": "#cb4b17ff",
"color": "#cb4b16ff",
"font_style": null,
"font_weight": null
},
@@ -686,7 +686,7 @@
"font_weight": null
},
"string": {
"color": "#cb4b17ff",
"color": "#cb4b16ff",
"font_style": null,
"font_weight": null
},
@@ -696,17 +696,17 @@
"font_weight": null
},
"string.regex": {
"color": "#cb4b17ff",
"color": "#cb4b16ff",
"font_style": null,
"font_weight": null
},
"string.special": {
"color": "#cb4b17ff",
"color": "#cb4b16ff",
"font_style": null,
"font_weight": null
},
"string.special.symbol": {
"color": "#cb4b17ff",
"color": "#cb4b16ff",
"font_style": null,
"font_weight": null
},
@@ -716,7 +716,7 @@
"font_weight": null
},
"text.literal": {
"color": "#cb4b17ff",
"color": "#cb4b16ff",
"font_style": null,
"font_weight": null
},

View File

@@ -12,10 +12,14 @@ mod prompts;
mod slash_command;
pub(crate) mod slash_command_picker;
pub mod slash_command_settings;
mod slash_command_working_set;
mod streaming_diff;
mod terminal_inline_assistant;
mod tool_working_set;
mod tools;
pub use crate::slash_command_working_set::{SlashCommandId, SlashCommandWorkingSet};
pub use crate::tool_working_set::{ToolId, ToolWorkingSet};
pub use assistant_panel::{AssistantPanel, AssistantPanelEvent};
use assistant_settings::AssistantSettings;
use assistant_slash_command::SlashCommandRegistry;
@@ -23,12 +27,11 @@ use assistant_tool::ToolRegistry;
use client::{proto, Client};
use command_palette_hooks::CommandPaletteFilter;
pub use context::*;
use context_servers::ContextServerRegistry;
pub use context_store::*;
use feature_flags::FeatureFlagAppExt;
use fs::Fs;
use gpui::impl_actions;
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
use gpui::{impl_actions, Context as _};
use indexed_docs::IndexedDocsRegistry;
pub(crate) use inline_assistant::*;
use language_model::{
@@ -43,10 +46,9 @@ use serde::{Deserialize, Serialize};
use settings::{update_settings_file, Settings, SettingsStore};
use slash_command::search_command::SearchSlashCommandFeatureFlag;
use slash_command::{
auto_command, cargo_workspace_command, context_server_command, default_command, delta_command,
diagnostics_command, docs_command, fetch_command, file_command, now_command, project_command,
prompt_command, search_command, selection_command, symbols_command, tab_command,
terminal_command,
auto_command, cargo_workspace_command, default_command, delta_command, diagnostics_command,
docs_command, fetch_command, file_command, now_command, project_command, prompt_command,
search_command, selection_command, symbols_command, tab_command, terminal_command,
};
use std::path::PathBuf;
use std::sync::Arc;
@@ -281,116 +283,9 @@ pub fn init(
})
.detach();
register_context_server_handlers(cx);
prompt_builder
}
fn register_context_server_handlers(cx: &mut AppContext) {
cx.subscribe(
&context_servers::manager::ContextServerManager::global(cx),
|manager, event, cx| match event {
context_servers::manager::Event::ServerStarted { server_id } => {
cx.update_model(
&manager,
|manager: &mut context_servers::manager::ContextServerManager, cx| {
let slash_command_registry = SlashCommandRegistry::global(cx);
let context_server_registry = ContextServerRegistry::global(cx);
if let Some(server) = manager.get_server(server_id) {
cx.spawn(|_, _| async move {
let Some(protocol) = server.client.read().clone() else {
return;
};
if protocol.capable(context_servers::protocol::ServerCapability::Prompts) {
if let Some(prompts) = protocol.list_prompts().await.log_err() {
for prompt in prompts
.into_iter()
.filter(context_server_command::acceptable_prompt)
{
log::info!(
"registering context server command: {:?}",
prompt.name
);
context_server_registry.register_command(
server.id.clone(),
prompt.name.as_str(),
);
slash_command_registry.register_command(
context_server_command::ContextServerSlashCommand::new(
&server, prompt,
),
true,
);
}
}
}
})
.detach();
}
},
);
cx.update_model(
&manager,
|manager: &mut context_servers::manager::ContextServerManager, cx| {
let tool_registry = ToolRegistry::global(cx);
let context_server_registry = ContextServerRegistry::global(cx);
if let Some(server) = manager.get_server(server_id) {
cx.spawn(|_, _| async move {
let Some(protocol) = server.client.read().clone() else {
return;
};
if protocol.capable(context_servers::protocol::ServerCapability::Tools) {
if let Some(tools) = protocol.list_tools().await.log_err() {
for tool in tools.tools {
log::info!(
"registering context server tool: {:?}",
tool.name
);
context_server_registry.register_tool(
server.id.clone(),
tool.name.as_str(),
);
tool_registry.register_tool(
tools::context_server_tool::ContextServerTool::new(
server.id.clone(),
tool
),
);
}
}
}
})
.detach();
}
},
);
}
context_servers::manager::Event::ServerStopped { server_id } => {
let slash_command_registry = SlashCommandRegistry::global(cx);
let context_server_registry = ContextServerRegistry::global(cx);
if let Some(commands) = context_server_registry.get_commands(server_id) {
for command_name in commands {
slash_command_registry.unregister_command_by_name(&command_name);
context_server_registry.unregister_command(&server_id, &command_name);
}
}
if let Some(tools) = context_server_registry.get_tools(server_id) {
let tool_registry = ToolRegistry::global(cx);
for tool_name in tools {
tool_registry.unregister_tool_by_name(&tool_name);
context_server_registry.unregister_tool(&server_id, &tool_name);
}
}
}
},
)
.detach();
}
fn init_language_model_settings(cx: &mut AppContext) {
update_active_language_model_from_settings(cx);

View File

@@ -1,3 +1,6 @@
use crate::slash_command::file_command::codeblock_fence_for_path;
use crate::slash_command_working_set::SlashCommandWorkingSet;
use crate::ToolWorkingSet;
use crate::{
assistant_settings::{AssistantDockPosition, AssistantSettings},
humanize_token_count,
@@ -6,24 +9,22 @@ use crate::{
slash_command::{
default_command::DefaultSlashCommand,
docs_command::{DocsSlashCommand, DocsSlashCommandArgs},
file_command::{self, codeblock_fence_for_path},
SlashCommandCompletionProvider, SlashCommandRegistry,
file_command, SlashCommandCompletionProvider,
},
slash_command_picker,
terminal_inline_assistant::TerminalInlineAssistant,
Assist, AssistantPatch, AssistantPatchStatus, CacheStatus, ConfirmCommand, Content, Context,
ContextEvent, ContextId, ContextStore, ContextStoreEvent, CopyCode, CycleMessageRole,
DeployHistory, DeployPromptLibrary, Edit, InlineAssistant, InsertDraggedFiles,
InsertIntoEditor, Message, MessageId, MessageMetadata, MessageStatus, ModelPickerDelegate,
ModelSelector, NewContext, PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection,
RemoteContextMetadata, RequestType, SavedContextMetadata, Split, ToggleFocus,
ToggleModelSelector,
InsertIntoEditor, InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId,
MessageMetadata, MessageStatus, ModelPickerDelegate, ModelSelector, NewContext,
ParsedSlashCommand, PendingSlashCommandStatus, QuoteSelection, RemoteContextMetadata,
RequestType, SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector,
};
use anyhow::Result;
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
use assistant_tool::ToolRegistry;
use client::{proto, zed_urls, Client, Status};
use collections::{BTreeSet, HashMap, HashSet};
use collections::{hash_map, BTreeSet, HashMap, HashSet};
use editor::{
actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt},
display_map::{
@@ -38,12 +39,12 @@ use editor::{display_map::CreaseId, FoldPlaceholder};
use fs::Fs;
use futures::FutureExt;
use gpui::{
canvas, div, img, percentage, point, pulsating_between, size, Action, Animation, AnimationExt,
AnyElement, AnyView, AppContext, AsyncWindowContext, ClipboardEntry, ClipboardItem,
CursorStyle, Empty, Entity, EventEmitter, ExternalPaths, FocusHandle, FocusableView,
FontWeight, InteractiveElement, IntoElement, Model, ParentElement, Pixels, Render, RenderImage,
SharedString, Size, StatefulInteractiveElement, Styled, Subscription, Task, Transformation,
UpdateGlobal, View, VisualContext, WeakView, WindowContext,
canvas, div, img, percentage, point, prelude::*, pulsating_between, size, Action, Animation,
AnimationExt, AnyElement, AnyView, AppContext, AsyncWindowContext, ClipboardEntry,
ClipboardItem, CursorStyle, Empty, Entity, EventEmitter, ExternalPaths, FocusHandle,
FocusableView, FontWeight, InteractiveElement, IntoElement, Model, ParentElement, Pixels,
Render, RenderImage, SharedString, Size, StatefulInteractiveElement, Styled, Subscription,
Task, Transformation, UpdateGlobal, View, WeakModel, WeakView,
};
use indexed_docs::IndexedDocsStore;
use language::{
@@ -112,7 +113,8 @@ pub fn init(cx: &mut AppContext) {
.register_action(ContextEditor::copy_code)
.register_action(ContextEditor::insert_dragged_files)
.register_action(AssistantPanel::show_configuration)
.register_action(AssistantPanel::create_new_context);
.register_action(AssistantPanel::create_new_context)
.register_action(AssistantPanel::restart_context_servers);
},
)
.detach();
@@ -315,10 +317,12 @@ impl AssistantPanel {
cx: AsyncWindowContext,
) -> Task<Result<View<Self>>> {
cx.spawn(|mut cx| async move {
let slash_commands = Arc::new(SlashCommandWorkingSet::default());
let tools = Arc::new(ToolWorkingSet::default());
let context_store = workspace
.update(&mut cx, |workspace, cx| {
let project = workspace.project().clone();
ContextStore::new(project, prompt_builder.clone(), cx)
ContextStore::new(project, prompt_builder.clone(), slash_commands, tools, cx)
})?
.await?;
@@ -1080,7 +1084,21 @@ impl AssistantPanel {
self.show_updated_summary(&context_editor, cx);
cx.notify()
}
EditorEvent::Edited { .. } => cx.emit(AssistantPanelEvent::ContextEdited),
EditorEvent::Edited { .. } => {
self.workspace
.update(cx, |workspace, cx| {
let is_via_ssh = workspace
.project()
.update(cx, |project, _| project.is_via_ssh());
workspace
.client()
.telemetry()
.log_edit_event("assistant panel", is_via_ssh);
})
.log_err();
cx.emit(AssistantPanelEvent::ContextEdited)
}
_ => {}
}
}
@@ -1294,6 +1312,24 @@ impl AssistantPanel {
.active_provider()
.map_or(None, |provider| Some(provider.authenticate(cx)))
}
fn restart_context_servers(
workspace: &mut Workspace,
_action: &context_servers::Restart,
cx: &mut ViewContext<Workspace>,
) {
let Some(assistant_panel) = workspace.panel::<AssistantPanel>(cx) else {
return;
};
assistant_panel.update(cx, |assistant_panel, cx| {
assistant_panel
.context_store
.update(cx, |context_store, cx| {
context_store.restart_context_servers(cx);
});
});
}
}
impl Render for AssistantPanel {
@@ -1468,6 +1504,8 @@ enum AssistError {
pub struct ContextEditor {
context: Model<Context>,
fs: Arc<dyn Fs>,
slash_commands: Arc<SlashCommandWorkingSet>,
tools: Arc<ToolWorkingSet>,
workspace: WeakView<Workspace>,
project: Model<Project>,
lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
@@ -1477,7 +1515,7 @@ pub struct ContextEditor {
scroll_position: Option<ScrollPosition>,
remote_id: Option<workspace::ViewId>,
pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
pending_slash_command_blocks: HashMap<Range<language::Anchor>, CustomBlockId>,
invoked_slash_command_creases: HashMap<InvokedSlashCommandId, CreaseId>,
pending_tool_use_creases: HashMap<Range<language::Anchor>, CreaseId>,
_subscriptions: Vec<Subscription>,
patches: HashMap<Range<language::Anchor>, PatchViewState>,
@@ -1509,6 +1547,7 @@ impl ContextEditor {
cx: &mut ViewContext<Self>,
) -> Self {
let completion_provider = SlashCommandCompletionProvider::new(
context.read(cx).slash_commands.clone(),
Some(cx.view().downgrade()),
Some(workspace.clone()),
);
@@ -1536,8 +1575,12 @@ impl ContextEditor {
let sections = context.read(cx).slash_command_output_sections().to_vec();
let patch_ranges = context.read(cx).patch_ranges().collect::<Vec<_>>();
let slash_commands = context.read(cx).slash_commands.clone();
let tools = context.read(cx).tools.clone();
let mut this = Self {
context,
slash_commands,
tools,
editor,
lsp_adapter_delegate,
blocks: Default::default(),
@@ -1548,7 +1591,7 @@ impl ContextEditor {
workspace,
project,
pending_slash_command_creases: HashMap::default(),
pending_slash_command_blocks: HashMap::default(),
invoked_slash_command_creases: HashMap::default(),
pending_tool_use_creases: HashMap::default(),
_subscriptions,
patches: HashMap::default(),
@@ -1573,14 +1616,13 @@ impl ContextEditor {
});
let command = self.context.update(cx, |context, cx| {
context.reparse(cx);
context.pending_slash_commands()[0].clone()
context.parsed_slash_commands()[0].clone()
});
self.run_command(
command.source_range,
&command.name,
&command.arguments,
false,
false,
self.workspace.clone(),
cx,
);
@@ -1689,7 +1731,7 @@ impl ContextEditor {
}
pub fn insert_command(&mut self, name: &str, cx: &mut ViewContext<Self>) {
if let Some(command) = SlashCommandRegistry::global(cx).command(name) {
if let Some(command) = self.slash_commands.command(name, cx) {
self.editor.update(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.try_cancel());
@@ -1753,7 +1795,6 @@ impl ContextEditor {
&command.name,
&command.arguments,
true,
false,
workspace.clone(),
cx,
);
@@ -1769,11 +1810,10 @@ impl ContextEditor {
name: &str,
arguments: &[String],
ensure_trailing_newline: bool,
expand_result: bool,
workspace: WeakView<Workspace>,
cx: &mut ViewContext<Self>,
) {
if let Some(command) = SlashCommandRegistry::global(cx).command(name) {
if let Some(command) = self.slash_commands.command(name, cx) {
let context = self.context.read(cx);
let sections = context
.slash_command_output_sections()
@@ -1793,9 +1833,9 @@ impl ContextEditor {
self.context.update(cx, |context, cx| {
context.insert_command_output(
command_range,
name,
output,
ensure_trailing_newline,
expand_result,
cx,
)
});
@@ -1865,8 +1905,7 @@ impl ContextEditor {
IconName::PocketKnife,
tool_use.name.clone().into(),
),
constrain_width: false,
merge_adjacent: false,
..Default::default()
};
let render_trailer =
move |_row, _unfold, _cx: &mut WindowContext| Empty.into_any();
@@ -1921,11 +1960,10 @@ impl ContextEditor {
ContextEvent::PatchesUpdated { removed, updated } => {
self.patches_updated(removed, updated, cx);
}
ContextEvent::PendingSlashCommandsUpdated { removed, updated } => {
ContextEvent::ParsedSlashCommandsUpdated { removed, updated } => {
self.editor.update(cx, |editor, cx| {
let buffer = editor.buffer().read(cx).snapshot(cx);
let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap();
let excerpt_id = *excerpt_id;
let (&excerpt_id, _, _) = buffer.as_singleton().unwrap();
editor.remove_creases(
removed
@@ -1934,16 +1972,6 @@ impl ContextEditor {
cx,
);
editor.remove_blocks(
HashSet::from_iter(
removed.iter().filter_map(|range| {
self.pending_slash_command_blocks.remove(range)
}),
),
None,
cx,
);
let crease_ids = editor.insert_creases(
updated.iter().map(|command| {
let workspace = self.workspace.clone();
@@ -1958,7 +1986,6 @@ impl ContextEditor {
&command.name,
&command.arguments,
false,
false,
workspace.clone(),
cx,
);
@@ -1968,8 +1995,7 @@ impl ContextEditor {
});
let placeholder = FoldPlaceholder {
render: Arc::new(move |_, _, _| Empty.into_any()),
constrain_width: false,
merge_adjacent: false,
..Default::default()
};
let render_toggle = {
let confirm_command = confirm_command.clone();
@@ -2011,62 +2037,29 @@ impl ContextEditor {
cx,
);
let block_ids = editor.insert_blocks(
updated
.iter()
.filter_map(|command| match &command.status {
PendingSlashCommandStatus::Error(error) => {
Some((command, error.clone()))
}
_ => None,
})
.map(|(command, error_message)| BlockProperties {
style: BlockStyle::Fixed,
height: 1,
placement: BlockPlacement::Below(Anchor {
buffer_id: Some(buffer_id),
excerpt_id,
text_anchor: command.source_range.start,
}),
render: slash_command_error_block_renderer(error_message),
priority: 0,
}),
None,
cx,
);
self.pending_slash_command_creases.extend(
updated
.iter()
.map(|command| command.source_range.clone())
.zip(crease_ids),
);
self.pending_slash_command_blocks.extend(
updated
.iter()
.map(|command| command.source_range.clone())
.zip(block_ids),
);
})
}
ContextEvent::InvokedSlashCommandChanged { command_id } => {
self.update_invoked_slash_command(*command_id, cx);
}
ContextEvent::SlashCommandOutputSectionAdded { section } => {
self.insert_slash_command_output_sections([section.clone()], false, cx);
}
ContextEvent::SlashCommandFinished {
output_range,
sections,
run_commands_in_output,
expand_result,
output_range: _output_range,
run_commands_in_ranges,
} => {
self.insert_slash_command_output_sections(
sections.iter().cloned(),
*expand_result,
cx,
);
if *run_commands_in_output {
for range in run_commands_in_ranges {
let commands = self.context.update(cx, |context, cx| {
context.reparse(cx);
context
.pending_commands_for_range(output_range.clone(), cx)
.pending_commands_for_range(range.clone(), cx)
.to_vec()
});
@@ -2076,7 +2069,6 @@ impl ContextEditor {
&command.name,
&command.arguments,
false,
false,
self.workspace.clone(),
cx,
);
@@ -2094,8 +2086,7 @@ impl ContextEditor {
.collect::<Vec<_>>();
for tool_use in pending_tool_uses {
let tool_registry = ToolRegistry::global(cx);
if let Some(tool) = tool_registry.tool(&tool_use.name) {
if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
let task = tool.run(tool_use.input, self.workspace.clone(), cx);
self.context.update(cx, |context, cx| {
@@ -2119,8 +2110,7 @@ impl ContextEditor {
IconName::PocketKnife,
format!("Tool Result: {tool_use_id}").into(),
),
constrain_width: false,
merge_adjacent: false,
..Default::default()
};
let render_trailer =
move |_row, _unfold, _cx: &mut WindowContext| Empty.into_any();
@@ -2158,6 +2148,76 @@ impl ContextEditor {
}
}
fn update_invoked_slash_command(
&mut self,
command_id: InvokedSlashCommandId,
cx: &mut ViewContext<Self>,
) {
self.editor.update(cx, |editor, cx| {
if let Some(invoked_slash_command) =
self.context.read(cx).invoked_slash_command(&command_id)
{
if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status {
let buffer = editor.buffer().read(cx).snapshot(cx);
let (&excerpt_id, _buffer_id, _buffer_snapshot) =
buffer.as_singleton().unwrap();
let start = buffer
.anchor_in_excerpt(excerpt_id, invoked_slash_command.range.start)
.unwrap();
let end = buffer
.anchor_in_excerpt(excerpt_id, invoked_slash_command.range.end)
.unwrap();
editor.remove_folds_with_type(
&[start..end],
TypeId::of::<PendingSlashCommand>(),
false,
cx,
);
editor.remove_creases(
HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)),
cx,
);
} else if let hash_map::Entry::Vacant(entry) =
self.invoked_slash_command_creases.entry(command_id)
{
let buffer = editor.buffer().read(cx).snapshot(cx);
let (&excerpt_id, _buffer_id, _buffer_snapshot) =
buffer.as_singleton().unwrap();
let context = self.context.downgrade();
let crease_start = buffer
.anchor_in_excerpt(excerpt_id, invoked_slash_command.range.start)
.unwrap();
let crease_end = buffer
.anchor_in_excerpt(excerpt_id, invoked_slash_command.range.end)
.unwrap();
let fold_placeholder =
invoked_slash_command_fold_placeholder(command_id, context);
let crease_ids = editor.insert_creases(
[Crease::new(
crease_start..crease_end,
fold_placeholder.clone(),
fold_toggle("invoked-slash-command"),
|_row, _folded, _cx| Empty.into_any(),
)],
cx,
);
editor.fold_ranges([(crease_start..crease_end, fold_placeholder)], false, cx);
entry.insert(crease_ids[0]);
} else {
cx.notify()
}
} else {
editor.remove_creases(
HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)),
cx,
);
cx.notify();
};
});
}
fn patches_updated(
&mut self,
removed: &Vec<Range<text::Anchor>>,
@@ -2229,8 +2289,7 @@ impl ContextEditor {
.unwrap_or_else(|| Empty.into_any())
})
},
constrain_width: false,
merge_adjacent: false,
..Default::default()
};
let should_refold;
@@ -2288,7 +2347,7 @@ impl ContextEditor {
}
if should_refold {
editor.unfold_ranges([patch_start..patch_end], true, false, cx);
editor.unfold_ranges(&[patch_start..patch_end], true, false, cx);
editor.fold_ranges([(patch_start..patch_end, header_placeholder)], false, cx);
}
}
@@ -2334,8 +2393,8 @@ impl ContextEditor {
section.icon,
section.label.clone(),
),
constrain_width: false,
merge_adjacent: false,
..Default::default()
},
render_slash_command_output_toggle,
|_, _, _| Empty.into_any_element(),
@@ -2579,6 +2638,29 @@ impl ContextEditor {
})
}
fn esc_kbd(cx: &WindowContext) -> Div {
let colors = cx.theme().colors().clone();
h_flex()
.items_center()
.gap_1()
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
.text_size(TextSize::XSmall.rems(cx))
.text_color(colors.text_muted)
.child("Press")
.child(
h_flex()
.rounded_md()
.px_1()
.mr_0p5()
.border_1()
.border_color(theme::color_alpha(colors.border_variant, 0.6))
.bg(theme::color_alpha(colors.element_background, 0.6))
.child("esc"),
)
.child("to cancel")
}
fn update_message_headers(&mut self, cx: &mut ViewContext<Self>) {
self.editor.update(cx, |editor, cx| {
let buffer = editor.buffer().read(cx).snapshot(cx);
@@ -2594,6 +2676,7 @@ impl ContextEditor {
let render_block = |message: MessageMetadata| -> RenderBlock {
Box::new({
let context = self.context.clone();
move |cx| {
let message_id = MessageId(message.timestamp);
let llm_loading = message.role == Role::Assistant
@@ -2615,7 +2698,7 @@ impl ContextEditor {
"pulsating-label",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.3, 0.9)),
.with_easing(pulsating_between(0.4, 0.8)),
|label, delta| label.alpha(delta),
)
.into_any_element()
@@ -2626,7 +2709,7 @@ impl ContextEditor {
spinner = Some(
Icon::new(IconName::ArrowCircle)
.size(IconSize::XSmall)
.color(Color::Muted)
.color(Color::Info)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
@@ -2638,20 +2721,7 @@ impl ContextEditor {
)
.into_any_element(),
);
note = Some(
div()
.font(
theme::ThemeSettings::get_global(cx)
.buffer_font
.clone(),
)
.child(
Label::new("Press 'esc' to cancel")
.color(Color::Muted)
.size(LabelSize::XSmall),
)
.into_any_element(),
);
note = Some(Self::esc_kbd(cx).into_any_element());
}
(animated_label, spinner, note)
}
@@ -2666,7 +2736,7 @@ impl ContextEditor {
let sender = h_flex()
.items_center()
.gap_2()
.gap_2p5()
.child(
ButtonLike::new("role")
.style(ButtonStyle::Filled)
@@ -2705,7 +2775,7 @@ impl ContextEditor {
.h_11()
.w_full()
.relative()
.gap_1()
.gap_1p5()
.child(sender)
.children(match &message.cache {
Some(cache) if cache.is_final_anchor => match cache.status {
@@ -2719,7 +2789,7 @@ impl ContextEditor {
)
.tooltip(|cx| {
Tooltip::with_meta(
"Context cached",
"Context Cached",
None,
"Large messages cached to optimize performance",
cx,
@@ -2747,16 +2817,9 @@ impl ContextEditor {
.selected_icon_color(Color::Error)
.icon(IconName::XCircle)
.icon_color(Color::Error)
.icon_size(IconSize::Small)
.icon_size(IconSize::XSmall)
.icon_position(IconPosition::Start)
.tooltip(move |cx| {
Tooltip::with_meta(
"Error interacting with language model",
None,
"Click for more details",
cx,
)
})
.tooltip(move |cx| Tooltip::text("View Details", cx))
.on_click({
let context = context.clone();
let error = error.clone();
@@ -2771,21 +2834,19 @@ impl ContextEditor {
.into_any_element(),
),
MessageStatus::Canceled => Some(
ButtonLike::new("canceled")
.child(Icon::new(IconName::XCircle).color(Color::Disabled))
h_flex()
.gap_1()
.items_center()
.child(
Icon::new(IconName::XCircle)
.color(Color::Disabled)
.size(IconSize::XSmall),
)
.child(
Label::new("Canceled")
.size(LabelSize::Small)
.color(Color::Disabled),
)
.tooltip(move |cx| {
Tooltip::with_meta(
"Canceled",
None,
"Interaction with the assistant was canceled",
cx,
)
})
.into_any_element(),
),
_ => None,
@@ -3264,13 +3325,12 @@ impl ContextEditor {
Crease::new(
start..end,
FoldPlaceholder {
constrain_width: false,
render: render_fold_icon_button(
weak_editor.clone(),
metadata.crease.icon,
metadata.crease.label.clone(),
),
merge_adjacent: false,
..Default::default()
},
render_slash_command_output_toggle,
|_, _, _| Empty.into_any(),
@@ -3692,6 +3752,19 @@ impl ContextEditor {
})
}
fn render_inject_context_menu(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
slash_command_picker::SlashCommandSelector::new(
self.slash_commands.clone(),
cx.view().downgrade(),
Button::new("trigger", "Add Context")
.icon(IconName::Plus)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.tooltip(|cx| Tooltip::text("Type / to insert via keyboard", cx)),
)
}
fn render_last_error(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
let last_error = self.last_error.as_ref()?;
@@ -4106,11 +4179,7 @@ impl Render for ContextEditor {
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.bg(cx.theme().colors().editor_background)
.child(
h_flex()
.gap_1()
.child(render_inject_context_menu(cx.view().downgrade(), cx)),
)
.child(h_flex().gap_1().child(self.render_inject_context_menu(cx)))
.child(
h_flex()
.w_full()
@@ -4392,24 +4461,6 @@ pub struct ContextEditorToolbarItem {
model_selector_menu_handle: PopoverMenuHandle<Picker<ModelPickerDelegate>>,
}
fn render_inject_context_menu(
active_context_editor: WeakView<ContextEditor>,
cx: &mut WindowContext<'_>,
) -> impl IntoElement {
let commands = SlashCommandRegistry::global(cx);
slash_command_picker::SlashCommandSelector::new(
commands.clone(),
active_context_editor,
Button::new("trigger", "Add Context")
.icon(IconName::Plus)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.tooltip(|cx| Tooltip::text("Type / to insert via keyboard", cx)),
)
}
impl ContextEditorToolbarItem {
pub fn new(
workspace: &Workspace,
@@ -4471,7 +4522,6 @@ impl Render for ContextEditorToolbarItem {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let left_side = h_flex()
.group("chat-title-group")
.pl_0p5()
.gap_1()
.items_center()
.flex_grow()
@@ -4562,6 +4612,7 @@ impl Render for ContextEditorToolbarItem {
.children(self.render_remaining_tokens(cx));
h_flex()
.px_0p5()
.size_full()
.gap_2()
.justify_between()
@@ -4936,8 +4987,8 @@ fn quote_selection_fold_placeholder(title: String, editor: WeakView<Editor>) ->
.into_any_element()
}
}),
constrain_width: false,
merge_adjacent: false,
..Default::default()
}
}
@@ -4981,7 +5032,7 @@ fn render_pending_slash_command_gutter_decoration(
fn render_docs_slash_command_trailer(
row: MultiBufferRow,
command: PendingSlashCommand,
command: ParsedSlashCommand,
cx: &mut WindowContext,
) -> AnyElement {
if command.arguments.is_empty() {
@@ -5065,17 +5116,51 @@ fn make_lsp_adapter_delegate(
})
}
fn slash_command_error_block_renderer(message: String) -> RenderBlock {
Box::new(move |_| {
div()
.pl_6()
.child(
Label::new(format!("error: {}", message))
.single_line()
.color(Color::Error),
)
.into_any()
})
enum PendingSlashCommand {}
fn invoked_slash_command_fold_placeholder(
command_id: InvokedSlashCommandId,
context: WeakModel<Context>,
) -> FoldPlaceholder {
FoldPlaceholder {
constrain_width: false,
merge_adjacent: false,
render: Arc::new(move |fold_id, _, cx| {
let Some(context) = context.upgrade() else {
return Empty.into_any();
};
let Some(command) = context.read(cx).invoked_slash_command(&command_id) else {
return Empty.into_any();
};
h_flex()
.id(fold_id)
.px_1()
.ml_6()
.gap_2()
.bg(cx.theme().colors().surface_background)
.rounded_md()
.child(Label::new(format!("/{}", command.name.clone())))
.map(|parent| match &command.status {
InvokedSlashCommandStatus::Running(_) => {
parent.child(Icon::new(IconName::ArrowCircle).with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(4)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
))
}
InvokedSlashCommandStatus::Error(message) => parent.child(
Label::new(format!("error: {message}"))
.single_line()
.color(Color::Error),
),
InvokedSlashCommandStatus::Finished => parent,
})
.into_any_element()
}),
type_tag: Some(TypeId::of::<PendingSlashCommand>()),
}
}
enum TokenState {

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,22 @@
use super::{AssistantEdit, MessageCacheMetadata};
use crate::slash_command_working_set::SlashCommandWorkingSet;
use crate::ToolWorkingSet;
use crate::{
assistant_panel, prompt_library, slash_command::file_command, AssistantEditKind, CacheStatus,
Context, ContextEvent, ContextId, ContextOperation, MessageId, MessageStatus, PromptBuilder,
Context, ContextEvent, ContextId, ContextOperation, InvokedSlashCommandId, MessageId,
MessageStatus, PromptBuilder,
};
use anyhow::Result;
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
SlashCommandRegistry, SlashCommandResult,
ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent, SlashCommandOutput,
SlashCommandOutputSection, SlashCommandRegistry, SlashCommandResult,
};
use collections::HashSet;
use collections::{HashMap, HashSet};
use fs::FakeFs;
use futures::{
channel::mpsc,
stream::{self, StreamExt},
};
use gpui::{AppContext, Model, SharedString, Task, TestAppContext, WeakView};
use language::{Buffer, BufferSnapshot, LanguageRegistry, LspAdapterDelegate};
use language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Role};
@@ -27,8 +34,8 @@ use std::{
rc::Rc,
sync::{atomic::AtomicBool, Arc},
};
use text::{network::Network, OffsetRangeExt as _, ReplicaId};
use ui::{Context as _, WindowContext};
use text::{network::Network, OffsetRangeExt as _, ReplicaId, ToOffset};
use ui::{Context as _, IconName, WindowContext};
use unindent::Unindent;
use util::{
test::{generate_marked_text, marked_text_ranges},
@@ -44,8 +51,17 @@ fn test_inserting_and_removing_messages(cx: &mut AppContext) {
assistant_panel::init(cx);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context =
cx.new_model(|cx| Context::local(registry, None, None, prompt_builder.clone(), cx));
let context = cx.new_model(|cx| {
Context::local(
registry,
None,
None,
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
Arc::new(ToolWorkingSet::default()),
cx,
)
});
let buffer = context.read(cx).buffer.clone();
let message_1 = context.read(cx).message_anchors[0].clone();
@@ -177,8 +193,17 @@ fn test_message_splitting(cx: &mut AppContext) {
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context =
cx.new_model(|cx| Context::local(registry, None, None, prompt_builder.clone(), cx));
let context = cx.new_model(|cx| {
Context::local(
registry.clone(),
None,
None,
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
Arc::new(ToolWorkingSet::default()),
cx,
)
});
let buffer = context.read(cx).buffer.clone();
let message_1 = context.read(cx).message_anchors[0].clone();
@@ -272,8 +297,17 @@ fn test_messages_for_offsets(cx: &mut AppContext) {
assistant_panel::init(cx);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context =
cx.new_model(|cx| Context::local(registry, None, None, prompt_builder.clone(), cx));
let context = cx.new_model(|cx| {
Context::local(
registry,
None,
None,
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
Arc::new(ToolWorkingSet::default()),
cx,
)
});
let buffer = context.read(cx).buffer.clone();
let message_1 = context.read(cx).message_anchors[0].clone();
@@ -378,23 +412,53 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context =
cx.new_model(|cx| Context::local(registry.clone(), None, None, prompt_builder.clone(), cx));
let context = cx.new_model(|cx| {
Context::local(
registry.clone(),
None,
None,
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
Arc::new(ToolWorkingSet::default()),
cx,
)
});
let output_ranges = Rc::new(RefCell::new(HashSet::default()));
#[derive(Default)]
struct ContextRanges {
parsed_commands: HashSet<Range<language::Anchor>>,
command_outputs: HashMap<InvokedSlashCommandId, Range<language::Anchor>>,
output_sections: HashSet<Range<language::Anchor>>,
}
let context_ranges = Rc::new(RefCell::new(ContextRanges::default()));
context.update(cx, |_, cx| {
cx.subscribe(&context, {
let ranges = output_ranges.clone();
move |_, _, event, _| match event {
ContextEvent::PendingSlashCommandsUpdated { removed, updated } => {
for range in removed {
ranges.borrow_mut().remove(range);
let context_ranges = context_ranges.clone();
move |context, _, event, _| {
let mut context_ranges = context_ranges.borrow_mut();
match event {
ContextEvent::InvokedSlashCommandChanged { command_id } => {
let command = context.invoked_slash_command(command_id).unwrap();
context_ranges
.command_outputs
.insert(*command_id, command.range.clone());
}
for command in updated {
ranges.borrow_mut().insert(command.source_range.clone());
ContextEvent::ParsedSlashCommandsUpdated { removed, updated } => {
for range in removed {
context_ranges.parsed_commands.remove(range);
}
for command in updated {
context_ranges
.parsed_commands
.insert(command.source_range.clone());
}
}
ContextEvent::SlashCommandOutputSectionAdded { section } => {
context_ranges.output_sections.insert(section.range.clone());
}
_ => {}
}
_ => {}
}
})
.detach();
@@ -406,14 +470,12 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
buffer.update(cx, |buffer, cx| {
buffer.edit([(0..0, "/file src/lib.rs")], None, cx);
});
assert_text_and_output_ranges(
assert_text_and_context_ranges(
&buffer,
&output_ranges.borrow(),
"
«/file src/lib.rs»
"
.unindent()
.trim_end(),
&context_ranges,
&"
«/file src/lib.rs»"
.unindent(),
cx,
);
@@ -422,14 +484,12 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
let edit_offset = buffer.text().find("lib.rs").unwrap();
buffer.edit([(edit_offset..edit_offset + "lib".len(), "main")], None, cx);
});
assert_text_and_output_ranges(
assert_text_and_context_ranges(
&buffer,
&output_ranges.borrow(),
"
«/file src/main.rs»
"
.unindent()
.trim_end(),
&context_ranges,
&"
«/file src/main.rs»"
.unindent(),
cx,
);
@@ -442,36 +502,180 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
cx,
);
});
assert_text_and_output_ranges(
assert_text_and_context_ranges(
&buffer,
&output_ranges.borrow(),
&context_ranges,
&"
/unknown src/main.rs"
.unindent(),
cx,
);
// Undoing the insertion of an non-existent slash command resorts the previous one.
buffer.update(cx, |buffer, cx| buffer.undo(cx));
assert_text_and_context_ranges(
&buffer,
&context_ranges,
&"
«/file src/main.rs»"
.unindent(),
cx,
);
let (command_output_tx, command_output_rx) = mpsc::unbounded();
context.update(cx, |context, cx| {
let command_source_range = context.parsed_slash_commands[0].source_range.clone();
context.insert_command_output(
command_source_range,
"file",
Task::ready(Ok(command_output_rx.boxed())),
true,
cx,
);
});
assert_text_and_context_ranges(
&buffer,
&context_ranges,
&"
⟦«/file src/main.rs»
…⟧
"
/unknown src/main.rs
.unindent(),
cx,
);
command_output_tx
.unbounded_send(Ok(SlashCommandEvent::StartSection {
icon: IconName::Ai,
label: "src/main.rs".into(),
metadata: None,
}))
.unwrap();
command_output_tx
.unbounded_send(Ok(SlashCommandEvent::Content("src/main.rs".into())))
.unwrap();
cx.run_until_parked();
assert_text_and_context_ranges(
&buffer,
&context_ranges,
&"
⟦«/file src/main.rs»
src/main.rs…⟧
"
.unindent()
.trim_end(),
.unindent(),
cx,
);
command_output_tx
.unbounded_send(Ok(SlashCommandEvent::Content("\nfn main() {}".into())))
.unwrap();
cx.run_until_parked();
assert_text_and_context_ranges(
&buffer,
&context_ranges,
&"
⟦«/file src/main.rs»
src/main.rs
fn main() {}…⟧
"
.unindent(),
cx,
);
command_output_tx
.unbounded_send(Ok(SlashCommandEvent::EndSection))
.unwrap();
cx.run_until_parked();
assert_text_and_context_ranges(
&buffer,
&context_ranges,
&"
⟦«/file src/main.rs»
⟪src/main.rs
fn main() {}⟫…⟧
"
.unindent(),
cx,
);
drop(command_output_tx);
cx.run_until_parked();
assert_text_and_context_ranges(
&buffer,
&context_ranges,
&"
⟦⟪src/main.rs
fn main() {}⟫⟧
"
.unindent(),
cx,
);
#[track_caller]
fn assert_text_and_output_ranges(
fn assert_text_and_context_ranges(
buffer: &Model<Buffer>,
ranges: &HashSet<Range<language::Anchor>>,
ranges: &RefCell<ContextRanges>,
expected_marked_text: &str,
cx: &mut TestAppContext,
) {
let (expected_text, expected_ranges) = marked_text_ranges(expected_marked_text, false);
let (actual_text, actual_ranges) = buffer.update(cx, |buffer, _| {
let mut ranges = ranges
.iter()
.map(|range| range.to_offset(buffer))
.collect::<Vec<_>>();
ranges.sort_by_key(|a| a.start);
(buffer.text(), ranges)
let mut actual_marked_text = String::new();
buffer.update(cx, |buffer, _| {
struct Endpoint {
offset: usize,
marker: char,
}
let ranges = ranges.borrow();
let mut endpoints = Vec::new();
for range in ranges.command_outputs.values() {
endpoints.push(Endpoint {
offset: range.start.to_offset(buffer),
marker: '',
});
}
for range in ranges.parsed_commands.iter() {
endpoints.push(Endpoint {
offset: range.start.to_offset(buffer),
marker: '«',
});
}
for range in ranges.output_sections.iter() {
endpoints.push(Endpoint {
offset: range.start.to_offset(buffer),
marker: '',
});
}
for range in ranges.output_sections.iter() {
endpoints.push(Endpoint {
offset: range.end.to_offset(buffer),
marker: '',
});
}
for range in ranges.parsed_commands.iter() {
endpoints.push(Endpoint {
offset: range.end.to_offset(buffer),
marker: '»',
});
}
for range in ranges.command_outputs.values() {
endpoints.push(Endpoint {
offset: range.end.to_offset(buffer),
marker: '',
});
}
endpoints.sort_by_key(|endpoint| endpoint.offset);
let mut offset = 0;
for endpoint in endpoints {
actual_marked_text.extend(buffer.text_for_range(offset..endpoint.offset));
actual_marked_text.push(endpoint.marker);
offset = endpoint.offset;
}
actual_marked_text.extend(buffer.text_for_range(offset..buffer.len()));
});
assert_eq!(actual_text, expected_text);
assert_eq!(actual_ranges, expected_ranges);
assert_eq!(actual_marked_text, expected_marked_text);
}
}
@@ -505,6 +709,8 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
Some(project),
None,
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
Arc::new(ToolWorkingSet::default()),
cx,
)
});
@@ -768,6 +974,8 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
Default::default(),
registry.clone(),
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
Arc::new(ToolWorkingSet::default()),
None,
None,
cx,
@@ -876,8 +1084,17 @@ async fn test_serialization(cx: &mut TestAppContext) {
cx.update(assistant_panel::init);
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context =
cx.new_model(|cx| Context::local(registry.clone(), None, None, prompt_builder.clone(), cx));
let context = cx.new_model(|cx| {
Context::local(
registry.clone(),
None,
None,
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
Arc::new(ToolWorkingSet::default()),
cx,
)
});
let buffer = context.read_with(cx, |context, _| context.buffer.clone());
let message_0 = context.read_with(cx, |context, _| context.message_anchors[0].id);
let message_1 = context.update(cx, |context, cx| {
@@ -917,6 +1134,8 @@ async fn test_serialization(cx: &mut TestAppContext) {
Default::default(),
registry.clone(),
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
Arc::new(ToolWorkingSet::default()),
None,
None,
cx,
@@ -975,6 +1194,8 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
language::Capability::ReadWrite,
registry.clone(),
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
Arc::new(ToolWorkingSet::default()),
None,
None,
cx,
@@ -1063,44 +1284,57 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
offset + 1..offset + 1 + command_text.len()
});
let output_len = rng.gen_range(1..=10);
let output_text = RandomCharIter::new(&mut rng)
.filter(|c| *c != '\r')
.take(output_len)
.take(10)
.collect::<String>();
let mut events = vec![Ok(SlashCommandEvent::StartMessage {
role: Role::User,
merge_same_roles: true,
})];
let num_sections = rng.gen_range(0..=3);
let mut sections = Vec::with_capacity(num_sections);
let mut section_start = 0;
for _ in 0..num_sections {
let section_start = rng.gen_range(0..output_len);
let section_end = rng.gen_range(section_start..=output_len);
sections.push(SlashCommandOutputSection {
range: section_start..section_end,
icon: ui::IconName::Ai,
let mut section_end = rng.gen_range(section_start..=output_text.len());
while !output_text.is_char_boundary(section_end) {
section_end += 1;
}
events.push(Ok(SlashCommandEvent::StartSection {
icon: IconName::Ai,
label: "section".into(),
metadata: None,
});
}));
events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text {
text: output_text[section_start..section_end].to_string(),
run_commands_in_text: false,
})));
events.push(Ok(SlashCommandEvent::EndSection));
section_start = section_end;
}
if section_start < output_text.len() {
events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text {
text: output_text[section_start..].to_string(),
run_commands_in_text: false,
})));
}
log::info!(
"Context {}: insert slash command output at {:?} with {:?}",
"Context {}: insert slash command output at {:?} with {:?} events",
context_index,
command_range,
sections
events.len()
);
let command_range = context.buffer.read(cx).anchor_after(command_range.start)
..context.buffer.read(cx).anchor_after(command_range.end);
context.insert_command_output(
command_range,
Task::ready(Ok(SlashCommandOutput {
text: output_text,
sections,
run_commands_in_text: false,
}
.to_event_stream())),
"/command",
Task::ready(Ok(stream::iter(events).boxed())),
true,
false,
cx,
);
});
@@ -1178,7 +1412,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
let first_context = contexts[0].read(cx);
for context in &contexts[1..] {
let context = context.read(cx);
assert!(context.pending_ops.is_empty());
assert!(context.pending_ops.is_empty(), "pending ops: {:?}", context.pending_ops);
assert_eq!(
context.buffer.read(cx).text(),
first_context.buffer.read(cx).text(),
@@ -1215,8 +1449,17 @@ fn test_mark_cache_anchors(cx: &mut AppContext) {
assistant_panel::init(cx);
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context =
cx.new_model(|cx| Context::local(registry, None, None, prompt_builder.clone(), cx));
let context = cx.new_model(|cx| {
Context::local(
registry,
None,
None,
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
Arc::new(ToolWorkingSet::default()),
cx,
)
});
let buffer = context.read(cx).buffer.clone();
// Create a test cache configuration

View File

@@ -1,10 +1,16 @@
use crate::slash_command::context_server_command;
use crate::{
prompts::PromptBuilder, Context, ContextEvent, ContextId, ContextOperation, ContextVersion,
SavedContext, SavedContextMetadata,
prompts::PromptBuilder, slash_command_working_set::SlashCommandWorkingSet, Context,
ContextEvent, ContextId, ContextOperation, ContextVersion, SavedContext, SavedContextMetadata,
};
use crate::{tools, SlashCommandId, ToolId, ToolWorkingSet};
use anyhow::{anyhow, Context as _, Result};
use client::{proto, telemetry::Telemetry, Client, TypedEnvelope};
use clock::ReplicaId;
use collections::HashMap;
use command_palette_hooks::CommandPaletteFilter;
use context_servers::manager::{ContextServerManager, ContextServerSettings};
use context_servers::{ContextServerFactoryRegistry, CONTEXT_SERVERS_NAMESPACE};
use fs::Fs;
use futures::StreamExt;
use fuzzy::StringMatchCandidate;
@@ -16,6 +22,7 @@ use paths::contexts_dir;
use project::Project;
use regex::Regex;
use rpc::AnyProtoClient;
use settings::{Settings as _, SettingsStore};
use std::{
cmp::Reverse,
ffi::OsStr,
@@ -43,9 +50,14 @@ pub struct RemoteContextMetadata {
pub struct ContextStore {
contexts: Vec<ContextHandle>,
contexts_metadata: Vec<SavedContextMetadata>,
context_server_manager: Model<ContextServerManager>,
context_server_slash_command_ids: HashMap<Arc<str>, Vec<SlashCommandId>>,
context_server_tool_ids: HashMap<Arc<str>, Vec<ToolId>>,
host_contexts: Vec<RemoteContextMetadata>,
fs: Arc<dyn Fs>,
languages: Arc<LanguageRegistry>,
slash_commands: Arc<SlashCommandWorkingSet>,
tools: Arc<ToolWorkingSet>,
telemetry: Arc<Telemetry>,
_watch_updates: Task<Option<()>>,
client: Arc<Client>,
@@ -87,6 +99,8 @@ impl ContextStore {
pub fn new(
project: Model<Project>,
prompt_builder: Arc<PromptBuilder>,
slash_commands: Arc<SlashCommandWorkingSet>,
tools: Arc<ToolWorkingSet>,
cx: &mut AppContext,
) -> Task<Result<Model<Self>>> {
let fs = project.read(cx).fs().clone();
@@ -97,12 +111,18 @@ impl ContextStore {
let (mut events, _) = fs.watch(contexts_dir(), CONTEXT_WATCH_DURATION).await;
let this = cx.new_model(|cx: &mut ModelContext<Self>| {
let context_server_manager = cx.new_model(|_cx| ContextServerManager::new());
let mut this = Self {
contexts: Vec::new(),
contexts_metadata: Vec::new(),
context_server_manager,
context_server_slash_command_ids: HashMap::default(),
context_server_tool_ids: HashMap::default(),
host_contexts: Vec::new(),
fs,
languages,
slash_commands,
tools,
telemetry,
_watch_updates: cx.spawn(|this, mut cx| {
async move {
@@ -127,15 +147,89 @@ impl ContextStore {
};
this.handle_project_changed(project, cx);
this.synchronize_contexts(cx);
this.register_context_server_handlers(cx);
// TODO: At the time when we construct the `ContextStore` we may not have yet initialized the extensions.
// In order to register the context servers when the extension is loaded, we're periodically looping to
// see if there are context servers to register.
//
// I tried doing this in a subscription on the `ExtensionStore`, but it never seemed to fire.
//
// We should find a more elegant way to do this.
let context_server_factory_registry =
ContextServerFactoryRegistry::default_global(cx);
cx.spawn(|context_store, mut cx| async move {
loop {
let mut servers_to_register = Vec::new();
for (_id, factory) in
context_server_factory_registry.context_server_factories()
{
if let Some(server) = factory(&cx).await.log_err() {
servers_to_register.push(server);
}
}
let Some(_) = context_store
.update(&mut cx, |this, cx| {
this.context_server_manager.update(cx, |this, cx| {
for server in servers_to_register {
this.add_server(server, cx).detach_and_log_err(cx);
}
})
})
.log_err()
else {
break;
};
smol::Timer::after(Duration::from_millis(100)).await;
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
this
})?;
this.update(&mut cx, |this, cx| this.reload(cx))?
.await
.log_err();
this.update(&mut cx, |this, cx| {
this.watch_context_server_settings(cx);
})
.log_err();
Ok(this)
})
}
fn watch_context_server_settings(&self, cx: &mut ModelContext<Self>) {
cx.observe_global::<SettingsStore>(move |this, cx| {
this.context_server_manager.update(cx, |manager, cx| {
let location = this.project.read(cx).worktrees(cx).next().map(|worktree| {
settings::SettingsLocation {
worktree_id: worktree.read(cx).id(),
path: Path::new(""),
}
});
let settings = ContextServerSettings::get(location, cx);
manager.maintain_servers(settings, cx);
let has_any_context_servers = !manager.servers().is_empty();
CommandPaletteFilter::update_global(cx, |filter, _cx| {
if has_any_context_servers {
filter.show_namespace(CONTEXT_SERVERS_NAMESPACE);
} else {
filter.hide_namespace(CONTEXT_SERVERS_NAMESPACE);
}
});
})
})
.detach();
}
async fn handle_advertise_contexts(
this: Model<Self>,
envelope: TypedEnvelope<proto::AdvertiseContexts>,
@@ -342,6 +436,8 @@ impl ContextStore {
Some(self.project.clone()),
Some(self.telemetry.clone()),
self.prompt_builder.clone(),
self.slash_commands.clone(),
self.tools.clone(),
cx,
)
});
@@ -364,6 +460,8 @@ impl ContextStore {
let project = self.project.clone();
let telemetry = self.telemetry.clone();
let prompt_builder = self.prompt_builder.clone();
let slash_commands = self.slash_commands.clone();
let tools = self.tools.clone();
let request = self.client.request(proto::CreateContext { project_id });
cx.spawn(|this, mut cx| async move {
let response = request.await?;
@@ -376,6 +474,8 @@ impl ContextStore {
capability,
language_registry,
prompt_builder,
slash_commands,
tools,
Some(project),
Some(telemetry),
cx,
@@ -425,6 +525,8 @@ impl ContextStore {
}
});
let prompt_builder = self.prompt_builder.clone();
let slash_commands = self.slash_commands.clone();
let tools = self.tools.clone();
cx.spawn(|this, mut cx| async move {
let saved_context = load.await?;
@@ -434,6 +536,8 @@ impl ContextStore {
path.clone(),
languages,
prompt_builder,
slash_commands,
tools,
Some(project),
Some(telemetry),
cx,
@@ -500,6 +604,8 @@ impl ContextStore {
context_id: context_id.to_proto(),
});
let prompt_builder = self.prompt_builder.clone();
let slash_commands = self.slash_commands.clone();
let tools = self.tools.clone();
cx.spawn(|this, mut cx| async move {
let response = request.await?;
let context_proto = response.context.context("invalid context")?;
@@ -510,6 +616,8 @@ impl ContextStore {
capability,
language_registry,
prompt_builder,
slash_commands,
tools,
Some(project),
Some(telemetry),
cx,
@@ -745,4 +853,114 @@ impl ContextStore {
})
})
}
pub fn restart_context_servers(&mut self, cx: &mut ModelContext<Self>) {
cx.update_model(
&self.context_server_manager,
|context_server_manager, cx| {
for server in context_server_manager.servers() {
context_server_manager
.restart_server(&server.id(), cx)
.detach_and_log_err(cx);
}
},
);
}
fn register_context_server_handlers(&self, cx: &mut ModelContext<Self>) {
cx.subscribe(
&self.context_server_manager.clone(),
Self::handle_context_server_event,
)
.detach();
}
fn handle_context_server_event(
&mut self,
context_server_manager: Model<ContextServerManager>,
event: &context_servers::manager::Event,
cx: &mut ModelContext<Self>,
) {
let slash_command_working_set = self.slash_commands.clone();
let tool_working_set = self.tools.clone();
match event {
context_servers::manager::Event::ServerStarted { server_id } => {
if let Some(server) = context_server_manager.read(cx).get_server(server_id) {
let context_server_manager = context_server_manager.clone();
cx.spawn({
let server = server.clone();
let server_id = server_id.clone();
|this, mut cx| async move {
let Some(protocol) = server.client() else {
return;
};
if protocol.capable(context_servers::protocol::ServerCapability::Prompts) {
if let Some(prompts) = protocol.list_prompts().await.log_err() {
let slash_command_ids = prompts
.into_iter()
.filter(context_server_command::acceptable_prompt)
.map(|prompt| {
log::info!(
"registering context server command: {:?}",
prompt.name
);
slash_command_working_set.insert(Arc::new(
context_server_command::ContextServerSlashCommand::new(
context_server_manager.clone(),
&server,
prompt,
),
))
})
.collect::<Vec<_>>();
this.update(&mut cx, |this, _cx| {
this.context_server_slash_command_ids
.insert(server_id.clone(), slash_command_ids);
})
.log_err();
}
}
if protocol.capable(context_servers::protocol::ServerCapability::Tools) {
if let Some(tools) = protocol.list_tools().await.log_err() {
let tool_ids = tools.tools.into_iter().map(|tool| {
log::info!("registering context server tool: {:?}", tool.name);
tool_working_set.insert(
Arc::new(tools::context_server_tool::ContextServerTool::new(
context_server_manager.clone(),
server.id(),
tool,
)),
)
}).collect::<Vec<_>>();
this.update(&mut cx, |this, _cx| {
this.context_server_tool_ids
.insert(server_id, tool_ids);
})
.log_err();
}
}
}
})
.detach();
}
}
context_servers::manager::Event::ServerStopped { server_id } => {
if let Some(slash_command_ids) =
self.context_server_slash_command_ids.remove(server_id)
{
slash_command_working_set.remove(&slash_command_ids);
}
if let Some(tool_ids) = self.context_server_tool_ids.remove(server_id) {
tool_working_set.remove(&tool_ids);
}
}
}
}
}

View File

@@ -53,7 +53,9 @@ use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
use terminal_view::terminal_panel::TerminalPanel;
use text::{OffsetRangeExt, ToPoint as _};
use theme::ThemeSettings;
use ui::{prelude::*, text_for_action, CheckboxWithLabel, IconButtonShape, Popover, Tooltip};
use ui::{
prelude::*, text_for_action, CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, Tooltip,
};
use util::{RangeExt, ResultExt};
use workspace::{notifications::NotificationId, ItemHandle, Toast, Workspace};
@@ -84,7 +86,7 @@ pub struct InlineAssistant {
confirmed_assists: HashMap<InlineAssistId, Model<CodegenAlternative>>,
prompt_history: VecDeque<String>,
prompt_builder: Arc<PromptBuilder>,
telemetry: Option<Arc<Telemetry>>,
telemetry: Arc<Telemetry>,
fs: Arc<dyn Fs>,
}
@@ -105,7 +107,7 @@ impl InlineAssistant {
confirmed_assists: HashMap::default(),
prompt_history: VecDeque::default(),
prompt_builder,
telemetry: Some(telemetry),
telemetry,
fs,
}
}
@@ -241,19 +243,17 @@ impl InlineAssistant {
codegen_ranges.push(start..end);
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
if let Some(telemetry) = self.telemetry.as_ref() {
telemetry.report_assistant_event(AssistantEvent {
conversation_id: None,
kind: AssistantKind::Inline,
phase: AssistantPhase::Invoked,
message_id: None,
model: model.telemetry_id(),
model_provider: model.provider_id().to_string(),
response_latency: None,
error_message: None,
language_name: buffer.language().map(|language| language.name().to_proto()),
});
}
self.telemetry.report_assistant_event(AssistantEvent {
conversation_id: None,
kind: AssistantKind::Inline,
phase: AssistantPhase::Invoked,
message_id: None,
model: model.telemetry_id(),
model_provider: model.provider_id().to_string(),
response_latency: None,
error_message: None,
language_name: buffer.language().map(|language| language.name().to_proto()),
});
}
}
@@ -816,7 +816,7 @@ impl InlineAssistant {
error_message: None,
language_name: language_name.map(|name| name.to_proto()),
},
self.telemetry.clone(),
Some(self.telemetry.clone()),
cx.http_client(),
model.api_key(cx),
cx.background_executor(),
@@ -1757,6 +1757,20 @@ impl PromptEditor {
) {
match event {
EditorEvent::Edited { .. } => {
if let Some(workspace) = cx.window_handle().downcast::<Workspace>() {
workspace
.update(cx, |workspace, cx| {
let is_via_ssh = workspace
.project()
.update(cx, |project, _| project.is_via_ssh());
workspace
.client()
.telemetry()
.log_edit_event("inline assist", is_via_ssh);
})
.log_err();
}
let prompt = self.editor.read(cx).text(cx);
if self
.prompt_history_ix
@@ -1899,21 +1913,58 @@ impl PromptEditor {
let codegen = self.codegen.read(cx);
let disabled = matches!(codegen.status(cx), CodegenStatus::Idle);
let model_registry = LanguageModelRegistry::read_global(cx);
let default_model = model_registry.active_model();
let alternative_models = model_registry.inline_alternative_models();
let get_model_name = |index: usize| -> String {
let name = |model: &Arc<dyn LanguageModel>| model.name().0.to_string();
match index {
0 => default_model.as_ref().map_or_else(String::new, name),
index if index <= alternative_models.len() => alternative_models
.get(index - 1)
.map_or_else(String::new, name),
_ => String::new(),
}
};
let total_models = alternative_models.len() + 1;
if total_models <= 1 {
return div().into_any_element();
}
let current_index = codegen.active_alternative;
let prev_index = (current_index + total_models - 1) % total_models;
let next_index = (current_index + 1) % total_models;
let prev_model_name = get_model_name(prev_index);
let next_model_name = get_model_name(next_index);
h_flex()
.child(
IconButton::new("previous", IconName::ChevronLeft)
.icon_color(Color::Muted)
.disabled(disabled)
.disabled(disabled || current_index == 0)
.shape(IconButtonShape::Square)
.tooltip({
let focus_handle = self.editor.focus_handle(cx);
move |cx| {
Tooltip::for_action_in(
"Previous Alternative",
&CyclePreviousInlineAssist,
&focus_handle,
cx,
)
cx.new_view(|cx| {
let mut tooltip = Tooltip::new("Previous Alternative").key_binding(
KeyBinding::for_action_in(
&CyclePreviousInlineAssist,
&focus_handle,
cx,
),
);
if !disabled && current_index != 0 {
tooltip = tooltip.meta(prev_model_name.clone());
}
tooltip
})
.into()
}
})
.on_click(cx.listener(|this, _, cx| {
@@ -1937,17 +1988,25 @@ impl PromptEditor {
.child(
IconButton::new("next", IconName::ChevronRight)
.icon_color(Color::Muted)
.disabled(disabled)
.disabled(disabled || current_index == total_models - 1)
.shape(IconButtonShape::Square)
.tooltip({
let focus_handle = self.editor.focus_handle(cx);
move |cx| {
Tooltip::for_action_in(
"Next Alternative",
&CycleNextInlineAssist,
&focus_handle,
cx,
)
cx.new_view(|cx| {
let mut tooltip = Tooltip::new("Next Alternative").key_binding(
KeyBinding::for_action_in(
&CycleNextInlineAssist,
&focus_handle,
cx,
),
);
if !disabled && current_index != total_models - 1 {
tooltip = tooltip.meta(next_model_name.clone());
}
tooltip
})
.into()
}
})
.on_click(cx.listener(|this, _, cx| {
@@ -2290,7 +2349,7 @@ pub struct Codegen {
buffer: Model<MultiBuffer>,
range: Range<Anchor>,
initial_transaction_id: Option<TransactionId>,
telemetry: Option<Arc<Telemetry>>,
telemetry: Arc<Telemetry>,
builder: Arc<PromptBuilder>,
is_insertion: bool,
}
@@ -2300,7 +2359,7 @@ impl Codegen {
buffer: Model<MultiBuffer>,
range: Range<Anchor>,
initial_transaction_id: Option<TransactionId>,
telemetry: Option<Arc<Telemetry>>,
telemetry: Arc<Telemetry>,
builder: Arc<PromptBuilder>,
cx: &mut ModelContext<Self>,
) -> Self {
@@ -2309,7 +2368,7 @@ impl Codegen {
buffer.clone(),
range.clone(),
false,
telemetry.clone(),
Some(telemetry.clone()),
builder.clone(),
cx,
)
@@ -2400,7 +2459,7 @@ impl Codegen {
self.buffer.clone(),
self.range.clone(),
false,
self.telemetry.clone(),
Some(self.telemetry.clone()),
self.builder.clone(),
cx,
)

View File

@@ -1,21 +1,17 @@
use feature_flags::ZedPro;
use gpui::Action;
use gpui::DismissEvent;
use language_model::{LanguageModel, LanguageModelAvailability, LanguageModelRegistry};
use proto::Plan;
use workspace::ShowConfiguration;
use std::sync::Arc;
use ui::ListItemSpacing;
use crate::assistant_settings::AssistantSettings;
use fs::Fs;
use gpui::SharedString;
use gpui::Task;
use gpui::{Action, AnyElement, DismissEvent, SharedString, Task};
use picker::{Picker, PickerDelegate};
use settings::update_settings_file;
use ui::{prelude::*, ListItem, PopoverMenu, PopoverMenuHandle, PopoverTrigger};
use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverMenuHandle, PopoverTrigger};
const TRY_ZED_PRO_URL: &str = "https://zed.dev/pro";
@@ -85,14 +81,36 @@ impl PickerDelegate for ModelPickerDelegate {
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
let all_models = self.all_models.clone();
let llm_registry = LanguageModelRegistry::global(cx);
let configured_models: Vec<_> = llm_registry
.read(cx)
.providers()
.iter()
.filter(|provider| provider.is_authenticated(cx))
.map(|provider| provider.id())
.collect();
cx.spawn(|this, mut cx| async move {
let filtered_models = cx
.background_executor()
.spawn(async move {
if query.is_empty() {
let displayed_models = if configured_models.is_empty() {
all_models
} else {
all_models
.into_iter()
.filter(|model_info| {
configured_models.contains(&model_info.model.provider_id())
})
.collect::<Vec<_>>()
};
if query.is_empty() {
displayed_models
} else {
displayed_models
.into_iter()
.filter(|model_info| {
model_info
@@ -141,6 +159,29 @@ impl PickerDelegate for ModelPickerDelegate {
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
fn render_header(&self, cx: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
let configured_models_count = LanguageModelRegistry::global(cx)
.read(cx)
.providers()
.iter()
.filter(|provider| provider.is_authenticated(cx))
.count();
if configured_models_count > 0 {
Some(
Label::new("Configured Models")
.size(LabelSize::Small)
.color(Color::Muted)
.mt_1()
.mb_0p5()
.ml_3()
.into_any_element(),
)
} else {
None
}
}
fn render_match(
&self,
ix: usize,
@@ -148,9 +189,10 @@ impl PickerDelegate for ModelPickerDelegate {
cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
use feature_flags::FeatureFlagAppExt;
let model_info = self.filtered_models.get(ix)?;
let show_badges = cx.has_flag::<ZedPro>();
let provider_name: String = model_info.model.provider_name().0.into();
let model_info = self.filtered_models.get(ix)?;
let provider_name: String = model_info.model.provider_name().0.clone().into();
Some(
ListItem::new(ix)
@@ -165,27 +207,32 @@ impl PickerDelegate for ModelPickerDelegate {
),
)
.child(
h_flex().w_full().justify_between().min_w(px(200.)).child(
h_flex()
.gap_1p5()
.child(Label::new(model_info.model.name().0.clone()))
.child(
Label::new(provider_name)
.size(LabelSize::XSmall)
.color(Color::Muted),
)
.children(match model_info.availability {
LanguageModelAvailability::Public => None,
LanguageModelAvailability::RequiresPlan(Plan::Free) => None,
LanguageModelAvailability::RequiresPlan(Plan::ZedPro) => {
show_badges.then(|| {
Label::new("Pro")
.size(LabelSize::XSmall)
.color(Color::Muted)
})
}
}),
),
h_flex()
.w_full()
.items_center()
.gap_1p5()
.min_w(px(200.))
.child(Label::new(model_info.model.name().0.clone()))
.child(
h_flex()
.gap_0p5()
.child(
Label::new(provider_name)
.size(LabelSize::XSmall)
.color(Color::Muted),
)
.children(match model_info.availability {
LanguageModelAvailability::Public => None,
LanguageModelAvailability::RequiresPlan(Plan::Free) => None,
LanguageModelAvailability::RequiresPlan(Plan::ZedPro) => {
show_badges.then(|| {
Label::new("Pro")
.size(LabelSize::XSmall)
.color(Color::Muted)
})
}
}),
),
)
.end_slot(div().when(model_info.is_selected, |this| {
this.child(
@@ -213,7 +260,7 @@ impl PickerDelegate for ModelPickerDelegate {
.justify_between()
.when(cx.has_flag::<ZedPro>(), |this| {
this.child(match plan {
// Already a zed pro subscriber
// Already a Zed Pro subscriber
Plan::ZedPro => Button::new("zed-pro", "Zed Pro")
.icon(IconName::ZedAssistant)
.icon_size(IconSize::Small)
@@ -254,6 +301,7 @@ impl<T: PopoverTrigger> RenderOnce for ModelSelector<T> {
let selected_provider = LanguageModelRegistry::read_global(cx)
.active_provider()
.map(|m| m.id());
let selected_model = LanguageModelRegistry::read_global(cx)
.active_model()
.map(|m| m.id());

View File

@@ -1,3 +1,4 @@
use crate::SlashCommandWorkingSet;
use crate::{slash_command::SlashCommandCompletionProvider, AssistantPanel, InlineAssistant};
use anyhow::{anyhow, Result};
use chrono::{DateTime, Utc};
@@ -522,7 +523,11 @@ impl PromptLibrary {
editor.set_use_modal_editing(false);
editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
editor.set_completion_provider(Some(Box::new(
SlashCommandCompletionProvider::new(None, None),
SlashCommandCompletionProvider::new(
Arc::new(SlashCommandWorkingSet::default()),
None,
None,
),
)));
if focus {
editor.focus(cx);

View File

@@ -1,4 +1,5 @@
use crate::assistant_panel::ContextEditor;
use crate::SlashCommandWorkingSet;
use anyhow::Result;
use assistant_slash_command::AfterCompletion;
pub use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandRegistry};
@@ -39,6 +40,7 @@ pub mod terminal_command;
pub(crate) struct SlashCommandCompletionProvider {
cancel_flag: Mutex<Arc<AtomicBool>>,
slash_commands: Arc<SlashCommandWorkingSet>,
editor: Option<WeakView<ContextEditor>>,
workspace: Option<WeakView<Workspace>>,
}
@@ -52,11 +54,13 @@ pub(crate) struct SlashCommandLine {
impl SlashCommandCompletionProvider {
pub fn new(
slash_commands: Arc<SlashCommandWorkingSet>,
editor: Option<WeakView<ContextEditor>>,
workspace: Option<WeakView<Workspace>>,
) -> Self {
Self {
cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
slash_commands,
editor,
workspace,
}
@@ -69,9 +73,9 @@ impl SlashCommandCompletionProvider {
name_range: Range<Anchor>,
cx: &mut WindowContext,
) -> Task<Result<Vec<project::Completion>>> {
let commands = SlashCommandRegistry::global(cx);
let candidates = commands
.command_names()
let slash_commands = self.slash_commands.clone();
let candidates = slash_commands
.command_names(cx)
.into_iter()
.enumerate()
.map(|(ix, def)| StringMatchCandidate {
@@ -98,7 +102,7 @@ impl SlashCommandCompletionProvider {
matches
.into_iter()
.filter_map(|mat| {
let command = commands.command(&mat.string)?;
let command = slash_commands.command(&mat.string, cx)?;
let mut new_text = mat.string.clone();
let requires_argument = command.requires_argument();
let accepts_arguments = command.accepts_arguments();
@@ -127,7 +131,6 @@ impl SlashCommandCompletionProvider {
&command_name,
&[],
true,
false,
workspace.clone(),
cx,
);
@@ -212,7 +215,6 @@ impl SlashCommandCompletionProvider {
&command_name,
&completed_arguments,
true,
false,
workspace.clone(),
cx,
);

View File

@@ -8,7 +8,7 @@ use context_servers::{
manager::{ContextServer, ContextServerManager},
types::Prompt,
};
use gpui::{AppContext, Task, WeakView, WindowContext};
use gpui::{AppContext, Model, Task, WeakView, WindowContext};
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
@@ -19,15 +19,21 @@ use workspace::Workspace;
use crate::slash_command::create_label_for_command;
pub struct ContextServerSlashCommand {
server_id: String,
server_manager: Model<ContextServerManager>,
server_id: Arc<str>,
prompt: Prompt,
}
impl ContextServerSlashCommand {
pub fn new(server: &Arc<ContextServer>, prompt: Prompt) -> Self {
pub fn new(
server_manager: Model<ContextServerManager>,
server: &Arc<dyn ContextServer>,
prompt: Prompt,
) -> Self {
Self {
server_id: server.id.clone(),
server_id: server.id(),
prompt,
server_manager,
}
}
}
@@ -74,20 +80,16 @@ impl SlashCommand for ContextServerSlashCommand {
_workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
let Ok((arg_name, arg_value)) = completion_argument(&self.prompt, arguments) else {
return Task::ready(Err(anyhow!("Failed to complete argument")));
};
let server_id = self.server_id.clone();
let prompt_name = self.prompt.name.clone();
let manager = ContextServerManager::global(cx);
let manager = manager.read(cx);
let (arg_name, arg_val) = match completion_argument(&self.prompt, arguments) {
Ok(tp) => tp,
Err(e) => {
return Task::ready(Err(e));
}
};
if let Some(server) = manager.get_server(&server_id) {
if let Some(server) = self.server_manager.read(cx).get_server(&server_id) {
cx.foreground_executor().spawn(async move {
let Some(protocol) = server.client.read().clone() else {
let Some(protocol) = server.client() else {
return Err(anyhow!("Context server not initialized"));
};
@@ -100,7 +102,7 @@ impl SlashCommand for ContextServerSlashCommand {
},
),
arg_name,
arg_val,
arg_value,
)
.await?;
@@ -138,11 +140,10 @@ impl SlashCommand for ContextServerSlashCommand {
Err(e) => return Task::ready(Err(e)),
};
let manager = ContextServerManager::global(cx);
let manager = manager.read(cx);
let manager = self.server_manager.read(cx);
if let Some(server) = manager.get_server(&server_id) {
cx.foreground_executor().spawn(async move {
let Some(protocol) = server.client.read().clone() else {
let Some(protocol) = server.client() else {
return Err(anyhow!("Context server not initialized"));
};
let result = protocol.run_prompt(&prompt_name, prompt_args).await?;

View File

@@ -58,6 +58,7 @@ impl SlashCommand for DeltaSlashCommand {
let mut paths = HashSet::default();
let mut file_command_old_outputs = Vec::new();
let mut file_command_new_outputs = Vec::new();
for section in context_slash_command_output_sections.iter().rev() {
if let Some(metadata) = section
.metadata
@@ -84,6 +85,7 @@ impl SlashCommand for DeltaSlashCommand {
cx.background_executor().spawn(async move {
let mut output = SlashCommandOutput::default();
let mut changes_detected = false;
let file_command_new_outputs = future::join_all(file_command_new_outputs).await;
for (old_text, new_output) in file_command_old_outputs
@@ -96,6 +98,7 @@ impl SlashCommand for DeltaSlashCommand {
if let Some(file_command_range) = new_output.sections.first() {
let new_text = &new_output.text[file_command_range.range.clone()];
if old_text.chars().ne(new_text.chars()) {
changes_detected = true;
output.sections.extend(new_output.sections.into_iter().map(
|section| SlashCommandOutputSection {
range: output.text.len() + section.range.start
@@ -112,6 +115,10 @@ impl SlashCommand for DeltaSlashCommand {
}
}
if !changes_detected {
return Err(anyhow!("no new changes detected"));
}
Ok(output.to_event_stream())
})
}

View File

@@ -256,8 +256,7 @@ fn collect_files(
break;
}
directory_stack.pop().unwrap();
events_tx
.unbounded_send(Ok(SlashCommandEvent::EndSection { metadata: None }))?;
events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection))?;
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
SlashCommandContent::Text {
text: "\n".into(),
@@ -362,7 +361,7 @@ fn collect_files(
}
while let Some(_) = directory_stack.pop() {
events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection { metadata: None }))?;
events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection))?;
}
}

View File

@@ -84,7 +84,7 @@ impl SlashCommand for SelectionCommand {
text,
run_commands_in_text: false,
})));
events.push(Ok(SlashCommandEvent::EndSection { metadata: None }));
events.push(Ok(SlashCommandEvent::EndSection));
events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text {
text: "\n".to_string(),
run_commands_in_text: false,

View File

@@ -74,13 +74,7 @@ impl SlashCommand for StreamingExampleSlashCommand {
run_commands_in_text: false,
},
)))?;
events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection { metadata: None }))?;
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
SlashCommandContent::Text {
text: "\n".into(),
run_commands_in_text: false,
},
)))?;
events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection))?;
Timer::after(Duration::from_secs(1)).await;
@@ -95,13 +89,7 @@ impl SlashCommand for StreamingExampleSlashCommand {
run_commands_in_text: false,
},
)))?;
events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection { metadata: None }))?;
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
SlashCommandContent::Text {
text: "\n".into(),
run_commands_in_text: false,
},
)))?;
events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection))?;
for n in 1..=10 {
Timer::after(Duration::from_secs(1)).await;
@@ -117,14 +105,7 @@ impl SlashCommand for StreamingExampleSlashCommand {
run_commands_in_text: false,
},
)))?;
events_tx
.unbounded_send(Ok(SlashCommandEvent::EndSection { metadata: None }))?;
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
SlashCommandContent::Text {
text: "\n".into(),
run_commands_in_text: false,
},
)))?;
events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection))?;
}
anyhow::Ok(())

View File

@@ -1,16 +1,15 @@
use std::sync::Arc;
use assistant_slash_command::SlashCommandRegistry;
use gpui::{AnyElement, DismissEvent, SharedString, Task, WeakView};
use picker::{Picker, PickerDelegate, PickerEditorPosition};
use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger};
use crate::assistant_panel::ContextEditor;
use crate::SlashCommandWorkingSet;
#[derive(IntoElement)]
pub(super) struct SlashCommandSelector<T: PopoverTrigger> {
registry: Arc<SlashCommandRegistry>,
working_set: Arc<SlashCommandWorkingSet>,
active_context_editor: WeakView<ContextEditor>,
trigger: T,
}
@@ -51,12 +50,12 @@ pub(crate) struct SlashCommandDelegate {
impl<T: PopoverTrigger> SlashCommandSelector<T> {
pub(crate) fn new(
registry: Arc<SlashCommandRegistry>,
working_set: Arc<SlashCommandWorkingSet>,
active_context_editor: WeakView<ContextEditor>,
trigger: T,
) -> Self {
SlashCommandSelector {
registry,
working_set,
active_context_editor,
trigger,
}
@@ -231,11 +230,11 @@ impl PickerDelegate for SlashCommandDelegate {
impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let all_models = self
.registry
.featured_command_names()
.working_set
.featured_command_names(cx)
.into_iter()
.filter_map(|command_name| {
let command = self.registry.command(&command_name)?;
let command = self.working_set.command(&command_name, cx)?;
let menu_text = SharedString::from(Arc::from(command.menu_text()));
let label = command.label(cx);
let args = label.filter_range.end.ne(&label.text.len()).then(|| {

View File

@@ -0,0 +1,79 @@
use assistant_slash_command::{SlashCommand, SlashCommandRegistry};
use collections::HashMap;
use gpui::AppContext;
use parking_lot::Mutex;
use std::sync::Arc;
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)]
pub struct SlashCommandId(usize);
/// A working set of slash commands for use in one instance of the Assistant Panel.
#[derive(Default)]
pub struct SlashCommandWorkingSet {
state: Mutex<WorkingSetState>,
}
#[derive(Default)]
struct WorkingSetState {
context_server_commands_by_id: HashMap<SlashCommandId, Arc<dyn SlashCommand>>,
context_server_commands_by_name: HashMap<Arc<str>, Arc<dyn SlashCommand>>,
next_command_id: SlashCommandId,
}
impl SlashCommandWorkingSet {
pub fn command(&self, name: &str, cx: &AppContext) -> Option<Arc<dyn SlashCommand>> {
self.state
.lock()
.context_server_commands_by_name
.get(name)
.cloned()
.or_else(|| SlashCommandRegistry::global(cx).command(name))
}
pub fn command_names(&self, cx: &AppContext) -> Vec<Arc<str>> {
let mut command_names = SlashCommandRegistry::global(cx).command_names();
command_names.extend(
self.state
.lock()
.context_server_commands_by_name
.keys()
.cloned(),
);
command_names
}
pub fn featured_command_names(&self, cx: &AppContext) -> Vec<Arc<str>> {
SlashCommandRegistry::global(cx).featured_command_names()
}
pub fn insert(&self, command: Arc<dyn SlashCommand>) -> SlashCommandId {
let mut state = self.state.lock();
let command_id = state.next_command_id;
state.next_command_id.0 += 1;
state
.context_server_commands_by_id
.insert(command_id, command.clone());
state.slash_commands_changed();
command_id
}
pub fn remove(&self, command_ids_to_remove: &[SlashCommandId]) {
let mut state = self.state.lock();
state
.context_server_commands_by_id
.retain(|id, _| !command_ids_to_remove.contains(id));
state.slash_commands_changed();
}
}
impl WorkingSetState {
fn slash_commands_changed(&mut self) {
self.context_server_commands_by_name.clear();
self.context_server_commands_by_name.extend(
self.context_server_commands_by_id
.values()
.map(|command| (command.name().into(), command.clone())),
);
}
}

View File

@@ -0,0 +1,75 @@
use assistant_tool::{Tool, ToolRegistry};
use collections::HashMap;
use gpui::AppContext;
use parking_lot::Mutex;
use std::sync::Arc;
#[derive(Copy, Clone, PartialEq, Eq, Hash, Default)]
pub struct ToolId(usize);
/// A working set of tools for use in one instance of the Assistant Panel.
#[derive(Default)]
pub struct ToolWorkingSet {
state: Mutex<WorkingSetState>,
}
#[derive(Default)]
struct WorkingSetState {
context_server_tools_by_id: HashMap<ToolId, Arc<dyn Tool>>,
context_server_tools_by_name: HashMap<String, Arc<dyn Tool>>,
next_tool_id: ToolId,
}
impl ToolWorkingSet {
pub fn tool(&self, name: &str, cx: &AppContext) -> Option<Arc<dyn Tool>> {
self.state
.lock()
.context_server_tools_by_name
.get(name)
.cloned()
.or_else(|| ToolRegistry::global(cx).tool(name))
}
pub fn tools(&self, cx: &AppContext) -> Vec<Arc<dyn Tool>> {
let mut tools = ToolRegistry::global(cx).tools();
tools.extend(
self.state
.lock()
.context_server_tools_by_id
.values()
.cloned(),
);
tools
}
pub fn insert(&self, command: Arc<dyn Tool>) -> ToolId {
let mut state = self.state.lock();
let command_id = state.next_tool_id;
state.next_tool_id.0 += 1;
state
.context_server_tools_by_id
.insert(command_id, command.clone());
state.tools_changed();
command_id
}
pub fn remove(&self, command_ids_to_remove: &[ToolId]) {
let mut state = self.state.lock();
state
.context_server_tools_by_id
.retain(|id, _| !command_ids_to_remove.contains(id));
state.tools_changed();
}
}
impl WorkingSetState {
fn tools_changed(&mut self) {
self.context_server_tools_by_name.clear();
self.context_server_tools_by_name.extend(
self.context_server_tools_by_id
.values()
.map(|command| (command.name(), command.clone())),
);
}
}

View File

@@ -1,17 +1,25 @@
use std::sync::Arc;
use anyhow::{anyhow, bail};
use assistant_tool::Tool;
use context_servers::manager::ContextServerManager;
use context_servers::types;
use gpui::Task;
use gpui::{Model, Task};
pub struct ContextServerTool {
server_id: String,
server_manager: Model<ContextServerManager>,
server_id: Arc<str>,
tool: types::Tool,
}
impl ContextServerTool {
pub fn new(server_id: impl Into<String>, tool: types::Tool) -> Self {
pub fn new(
server_manager: Model<ContextServerManager>,
server_id: impl Into<Arc<str>>,
tool: types::Tool,
) -> Self {
Self {
server_manager,
server_id: server_id.into(),
tool,
}
@@ -45,13 +53,11 @@ impl Tool for ContextServerTool {
_workspace: gpui::WeakView<workspace::Workspace>,
cx: &mut ui::WindowContext,
) -> gpui::Task<gpui::Result<String>> {
let manager = ContextServerManager::global(cx);
let manager = manager.read(cx);
if let Some(server) = manager.get_server(&self.server_id) {
if let Some(server) = self.server_manager.read(cx).get_server(&self.server_id) {
cx.foreground_executor().spawn({
let tool_name = self.tool.name.clone();
async move {
let Some(protocol) = server.client.read().clone() else {
let Some(protocol) = server.client() else {
bail!("Context server not initialized");
};

View File

@@ -18,6 +18,7 @@ derive_more.workspace = true
futures.workspace = true
gpui.workspace = true
language.workspace = true
language_model.workspace = true
parking_lot.workspace = true
serde.workspace = true
serde_json.workspace = true

View File

@@ -5,6 +5,7 @@ use futures::stream::{self, BoxStream};
use futures::StreamExt;
use gpui::{AnyElement, AppContext, ElementId, SharedString, Task, WeakView, WindowContext};
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt};
pub use language_model::Role;
use serde::{Deserialize, Serialize};
pub use slash_command_registry::*;
use std::{
@@ -103,7 +104,7 @@ pub type RenderFoldPlaceholder = Arc<
+ Fn(ElementId, Arc<dyn Fn(&mut WindowContext)>, &mut WindowContext) -> AnyElement,
>;
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq)]
pub enum SlashCommandContent {
Text {
text: String,
@@ -111,17 +112,28 @@ pub enum SlashCommandContent {
},
}
#[derive(Debug, PartialEq, Eq)]
impl<'a> From<&'a str> for SlashCommandContent {
fn from(text: &'a str) -> Self {
Self::Text {
text: text.into(),
run_commands_in_text: false,
}
}
}
#[derive(Debug, PartialEq)]
pub enum SlashCommandEvent {
StartMessage {
role: Role,
merge_same_roles: bool,
},
StartSection {
icon: IconName,
label: SharedString,
metadata: Option<serde_json::Value>,
},
Content(SlashCommandContent),
EndSection {
metadata: Option<serde_json::Value>,
},
EndSection,
}
#[derive(Debug, Default, PartialEq, Clone)]
@@ -150,43 +162,37 @@ impl SlashCommandOutput {
self.ensure_valid_section_ranges();
let mut events = Vec::new();
let mut last_section_end = 0;
let mut section_endpoints = Vec::new();
for section in self.sections {
if last_section_end < section.range.start {
section_endpoints.push((
section.range.start,
SlashCommandEvent::StartSection {
icon: section.icon,
label: section.label,
metadata: section.metadata,
},
));
section_endpoints.push((section.range.end, SlashCommandEvent::EndSection));
}
section_endpoints.sort_by_key(|(offset, _)| *offset);
let mut content_offset = 0;
for (endpoint_offset, endpoint) in section_endpoints {
if content_offset < endpoint_offset {
events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text {
text: self
.text
.get(last_section_end..section.range.start)
.unwrap_or_default()
.to_string(),
text: self.text[content_offset..endpoint_offset].to_string(),
run_commands_in_text: self.run_commands_in_text,
})));
content_offset = endpoint_offset;
}
events.push(Ok(SlashCommandEvent::StartSection {
icon: section.icon,
label: section.label,
metadata: section.metadata.clone(),
}));
events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text {
text: self
.text
.get(section.range.start..section.range.end)
.unwrap_or_default()
.to_string(),
run_commands_in_text: self.run_commands_in_text,
})));
events.push(Ok(SlashCommandEvent::EndSection {
metadata: section.metadata,
}));
last_section_end = section.range.end;
events.push(Ok(endpoint));
}
if last_section_end < self.text.len() {
if content_offset < self.text.len() {
events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text {
text: self.text[last_section_end..].to_string(),
text: self.text[content_offset..].to_string(),
run_commands_in_text: self.run_commands_in_text,
})));
}
@@ -226,12 +232,12 @@ impl SlashCommandOutput {
section.range.end = output.text.len();
}
}
SlashCommandEvent::EndSection { metadata } => {
if let Some(mut section) = section_stack.pop() {
section.metadata = metadata;
SlashCommandEvent::EndSection => {
if let Some(section) = section_stack.pop() {
output.sections.push(section);
}
}
SlashCommandEvent::StartMessage { .. } => {}
}
}
@@ -299,7 +305,7 @@ mod tests {
text: "Hello, world!".into(),
run_commands_in_text: false
}),
SlashCommandEvent::EndSection { metadata: None }
SlashCommandEvent::EndSection
]
);
@@ -351,7 +357,7 @@ mod tests {
text: "Apple\n".into(),
run_commands_in_text: false
}),
SlashCommandEvent::EndSection { metadata: None },
SlashCommandEvent::EndSection,
SlashCommandEvent::Content(SlashCommandContent::Text {
text: "Cucumber\n".into(),
run_commands_in_text: false
@@ -365,7 +371,7 @@ mod tests {
text: "Banana\n".into(),
run_commands_in_text: false
}),
SlashCommandEvent::EndSection { metadata: None }
SlashCommandEvent::EndSection
]
);
@@ -429,9 +435,7 @@ mod tests {
text: "Line 1".into(),
run_commands_in_text: false
}),
SlashCommandEvent::EndSection {
metadata: Some(json!({ "a": true }))
},
SlashCommandEvent::EndSection,
SlashCommandEvent::Content(SlashCommandContent::Text {
text: "\n".into(),
run_commands_in_text: false
@@ -445,9 +449,7 @@ mod tests {
text: "Line 2".into(),
run_commands_in_text: false
}),
SlashCommandEvent::EndSection {
metadata: Some(json!({ "b": true }))
},
SlashCommandEvent::EndSection,
SlashCommandEvent::Content(SlashCommandContent::Text {
text: "\n".into(),
run_commands_in_text: false
@@ -461,9 +463,7 @@ mod tests {
text: "Line 3".into(),
run_commands_in_text: false
}),
SlashCommandEvent::EndSection {
metadata: Some(json!({ "c": true }))
},
SlashCommandEvent::EndSection,
SlashCommandEvent::Content(SlashCommandContent::Text {
text: "\n".into(),
run_commands_in_text: false
@@ -477,9 +477,7 @@ mod tests {
text: "Line 4".into(),
run_commands_in_text: false
}),
SlashCommandEvent::EndSection {
metadata: Some(json!({ "d": true }))
},
SlashCommandEvent::EndSection,
SlashCommandEvent::Content(SlashCommandContent::Text {
text: "\n".into(),
run_commands_in_text: false

View File

@@ -6,7 +6,7 @@ use crate::{
},
};
use anyhow::{anyhow, Result};
use assistant::{ContextStore, PromptBuilder};
use assistant::{ContextStore, PromptBuilder, SlashCommandWorkingSet, ToolWorkingSet};
use call::{room, ActiveCall, ParticipantLocation, Room};
use client::{User, RECEIVE_TIMEOUT};
use collections::{HashMap, HashSet};
@@ -6489,11 +6489,27 @@ async fn test_context_collaboration_with_reconnect(
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context_store_a = cx_a
.update(|cx| ContextStore::new(project_a.clone(), prompt_builder.clone(), cx))
.update(|cx| {
ContextStore::new(
project_a.clone(),
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
Arc::new(ToolWorkingSet::default()),
cx,
)
})
.await
.unwrap();
let context_store_b = cx_b
.update(|cx| ContextStore::new(project_b.clone(), prompt_builder.clone(), cx))
.update(|cx| {
ContextStore::new(
project_b.clone(),
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
Arc::new(ToolWorkingSet::default()),
cx,
)
})
.await
.unwrap();

View File

@@ -3,7 +3,7 @@ use call::ActiveCall;
use collections::HashSet;
use fs::{FakeFs, Fs as _};
use futures::StreamExt as _;
use gpui::{BackgroundExecutor, Context as _, TestAppContext, UpdateGlobal as _};
use gpui::{BackgroundExecutor, Context as _, SemanticVersion, TestAppContext, UpdateGlobal as _};
use http_client::BlockedHttpClient;
use language::{
language_settings::{
@@ -31,6 +31,12 @@ async fn test_sharing_an_ssh_remote_project(
server_cx: &mut TestAppContext,
) {
let executor = cx_a.executor();
cx_a.update(|cx| {
release_channel::init(SemanticVersion::default(), cx);
});
server_cx.update(|cx| {
release_channel::init(SemanticVersion::default(), cx);
});
let mut server = TestServer::start(executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
@@ -199,6 +205,13 @@ async fn test_ssh_collaboration_git_branches(
cx_b.set_name("b");
server_cx.set_name("server");
cx_a.update(|cx| {
release_channel::init(SemanticVersion::default(), cx);
});
server_cx.update(|cx| {
release_channel::init(SemanticVersion::default(), cx);
});
let mut server = TestServer::start(executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
@@ -329,6 +342,13 @@ async fn test_ssh_collaboration_formatting_with_prettier(
cx_b.set_name("b");
server_cx.set_name("server");
cx_a.update(|cx| {
release_channel::init(SemanticVersion::default(), cx);
});
server_cx.update(|cx| {
release_channel::init(SemanticVersion::default(), cx);
});
let mut server = TestServer::start(executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;

View File

@@ -13,6 +13,7 @@ path = "src/context_servers.rs"
[dependencies]
anyhow.workspace = true
async-trait.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
futures.workspace = true
@@ -27,4 +28,3 @@ settings.workspace = true
smol.workspace = true
url = { workspace = true, features = ["serde"] }
util.workspace = true
workspace.workspace = true

View File

@@ -55,11 +55,20 @@ pub struct Client {
#[repr(transparent)]
pub struct ContextServerId(pub String);
fn is_null_value<T: Serialize>(value: &T) -> bool {
if let Ok(Value::Null) = serde_json::to_value(value) {
true
} else {
false
}
}
#[derive(Serialize, Deserialize)]
struct Request<'a, T> {
jsonrpc: &'static str,
id: RequestId,
method: &'a str,
#[serde(skip_serializing_if = "is_null_value")]
params: T,
}

View File

@@ -1,40 +1,27 @@
use gpui::{actions, AppContext, Context, ViewContext};
use manager::ContextServerManager;
use workspace::Workspace;
pub mod client;
pub mod manager;
pub mod protocol;
mod registry;
pub mod types;
pub use registry::*;
use command_palette_hooks::CommandPaletteFilter;
use gpui::{actions, AppContext};
use settings::Settings;
pub use crate::manager::ContextServer;
use crate::manager::ContextServerSettings;
pub use crate::registry::ContextServerFactoryRegistry;
actions!(context_servers, [Restart]);
/// The namespace for the context servers actions.
const CONTEXT_SERVERS_NAMESPACE: &'static str = "context_servers";
pub const CONTEXT_SERVERS_NAMESPACE: &'static str = "context_servers";
pub fn init(cx: &mut AppContext) {
log::info!("initializing context server client");
manager::init(cx);
ContextServerRegistry::register(cx);
ContextServerSettings::register(cx);
ContextServerFactoryRegistry::default_global(cx);
cx.observe_new_views(
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
workspace.register_action(restart_servers);
},
)
.detach();
}
fn restart_servers(_workspace: &mut Workspace, _action: &Restart, cx: &mut ViewContext<Workspace>) {
let model = ContextServerManager::global(cx);
cx.update_model(&model, |manager, cx| {
for server in manager.servers() {
manager
.restart_server(&server.id, cx)
.detach_and_log_err(cx);
}
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_namespace(CONTEXT_SERVERS_NAMESPACE);
});
}

View File

@@ -14,18 +14,21 @@
//! The module also includes initialization logic to set up the context server system
//! and react to changes in settings.
use std::path::Path;
use std::pin::Pin;
use std::sync::Arc;
use anyhow::Result;
use async_trait::async_trait;
use collections::{HashMap, HashSet};
use command_palette_hooks::CommandPaletteFilter;
use gpui::{AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, Task};
use futures::{Future, FutureExt};
use gpui::{AsyncAppContext, EventEmitter, ModelContext, Task};
use log;
use parking_lot::RwLock;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources, SettingsStore};
use std::path::Path;
use std::sync::Arc;
use settings::{Settings, SettingsSources};
use crate::CONTEXT_SERVERS_NAMESPACE;
use crate::{
client::{self, Client},
types,
@@ -57,51 +60,84 @@ impl Settings for ContextServerSettings {
}
}
pub struct ContextServer {
pub id: String,
pub config: ServerConfig,
#[async_trait(?Send)]
pub trait ContextServer: Send + Sync + 'static {
fn id(&self) -> Arc<str>;
fn config(&self) -> Arc<ServerConfig>;
fn client(&self) -> Option<Arc<crate::protocol::InitializedContextServerProtocol>>;
fn start<'a>(
self: Arc<Self>,
cx: &'a AsyncAppContext,
) -> Pin<Box<dyn 'a + Future<Output = Result<()>>>>;
fn stop(&self) -> Result<()>;
}
pub struct NativeContextServer {
pub id: Arc<str>,
pub config: Arc<ServerConfig>,
pub client: RwLock<Option<Arc<crate::protocol::InitializedContextServerProtocol>>>,
}
impl ContextServer {
fn new(config: ServerConfig) -> Self {
impl NativeContextServer {
pub fn new(config: Arc<ServerConfig>) -> Self {
Self {
id: config.id.clone(),
id: config.id.clone().into(),
config,
client: RwLock::new(None),
}
}
}
async fn start(&self, cx: &AsyncAppContext) -> anyhow::Result<()> {
log::info!("starting context server {}", self.config.id,);
let client = Client::new(
client::ContextServerId(self.config.id.clone()),
client::ModelContextServerBinary {
executable: Path::new(&self.config.executable).to_path_buf(),
args: self.config.args.clone(),
env: self.config.env.clone(),
},
cx.clone(),
)?;
let protocol = crate::protocol::ModelContextProtocol::new(client);
let client_info = types::Implementation {
name: "Zed".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
};
let initialized_protocol = protocol.initialize(client_info).await?;
log::debug!(
"context server {} initialized: {:?}",
self.config.id,
initialized_protocol.initialize,
);
*self.client.write() = Some(Arc::new(initialized_protocol));
Ok(())
#[async_trait(?Send)]
impl ContextServer for NativeContextServer {
fn id(&self) -> Arc<str> {
self.id.clone()
}
async fn stop(&self) -> anyhow::Result<()> {
fn config(&self) -> Arc<ServerConfig> {
self.config.clone()
}
fn client(&self) -> Option<Arc<crate::protocol::InitializedContextServerProtocol>> {
self.client.read().clone()
}
fn start<'a>(
self: Arc<Self>,
cx: &'a AsyncAppContext,
) -> Pin<Box<dyn 'a + Future<Output = Result<()>>>> {
async move {
log::info!("starting context server {}", self.config.id,);
let client = Client::new(
client::ContextServerId(self.config.id.clone()),
client::ModelContextServerBinary {
executable: Path::new(&self.config.executable).to_path_buf(),
args: self.config.args.clone(),
env: self.config.env.clone(),
},
cx.clone(),
)?;
let protocol = crate::protocol::ModelContextProtocol::new(client);
let client_info = types::Implementation {
name: "Zed".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
};
let initialized_protocol = protocol.initialize(client_info).await?;
log::debug!(
"context server {} initialized: {:?}",
self.config.id,
initialized_protocol.initialize,
);
*self.client.write() = Some(Arc::new(initialized_protocol));
Ok(())
}
.boxed_local()
}
fn stop(&self) -> Result<()> {
let mut client = self.client.write();
if let Some(protocol) = client.take() {
drop(protocol);
@@ -115,16 +151,15 @@ impl ContextServer {
/// must go through the `GlobalContextServerManager` which holds
/// a model to the ContextServerManager.
pub struct ContextServerManager {
servers: HashMap<String, Arc<ContextServer>>,
pending_servers: HashSet<String>,
servers: HashMap<Arc<str>, Arc<dyn ContextServer>>,
pending_servers: HashSet<Arc<str>>,
}
pub enum Event {
ServerStarted { server_id: String },
ServerStopped { server_id: String },
ServerStarted { server_id: Arc<str> },
ServerStopped { server_id: Arc<str> },
}
impl Global for ContextServerManager {}
impl EventEmitter<Event> for ContextServerManager {}
impl Default for ContextServerManager {
@@ -140,16 +175,13 @@ impl ContextServerManager {
pending_servers: HashSet::default(),
}
}
pub fn global(cx: &AppContext) -> Model<Self> {
cx.global::<GlobalContextServerManager>().0.clone()
}
pub fn add_server(
&mut self,
config: ServerConfig,
cx: &mut ModelContext<Self>,
server: Arc<dyn ContextServer>,
cx: &ModelContext<Self>,
) -> Task<anyhow::Result<()>> {
let server_id = config.id.clone();
let server_id = server.id();
if self.servers.contains_key(&server_id) || self.pending_servers.contains(&server_id) {
return Task::ready(Ok(()));
@@ -158,8 +190,7 @@ impl ContextServerManager {
let task = {
let server_id = server_id.clone();
cx.spawn(|this, mut cx| async move {
let server = Arc::new(ContextServer::new(config));
server.start(&cx).await?;
server.clone().start(&cx).await?;
this.update(&mut cx, |this, cx| {
this.servers.insert(server_id.clone(), server);
this.pending_servers.remove(&server_id);
@@ -175,22 +206,24 @@ impl ContextServerManager {
task
}
pub fn get_server(&self, id: &str) -> Option<Arc<ContextServer>> {
pub fn get_server(&self, id: &str) -> Option<Arc<dyn ContextServer>> {
self.servers.get(id).cloned()
}
pub fn remove_server(
&mut self,
id: &str,
cx: &mut ModelContext<Self>,
id: &Arc<str>,
cx: &ModelContext<Self>,
) -> Task<anyhow::Result<()>> {
let id = id.to_string();
let id = id.clone();
cx.spawn(|this, mut cx| async move {
if let Some(server) = this.update(&mut cx, |this, _cx| this.servers.remove(&id))? {
server.stop().await?;
if let Some(server) =
this.update(&mut cx, |this, _cx| this.servers.remove(id.as_ref()))?
{
server.stop()?;
}
this.update(&mut cx, |this, cx| {
this.pending_servers.remove(&id);
this.pending_servers.remove(id.as_ref());
cx.emit(Event::ServerStopped {
server_id: id.clone(),
})
@@ -201,16 +234,16 @@ impl ContextServerManager {
pub fn restart_server(
&mut self,
id: &str,
id: &Arc<str>,
cx: &mut ModelContext<Self>,
) -> Task<anyhow::Result<()>> {
let id = id.to_string();
let id = id.clone();
cx.spawn(|this, mut cx| async move {
if let Some(server) = this.update(&mut cx, |this, _cx| this.servers.remove(&id))? {
server.stop().await?;
let config = server.config.clone();
let new_server = Arc::new(ContextServer::new(config));
new_server.start(&cx).await?;
server.stop()?;
let config = server.config();
let new_server = Arc::new(NativeContextServer::new(config));
new_server.clone().start(&cx).await?;
this.update(&mut cx, |this, cx| {
this.servers.insert(id.clone(), new_server);
cx.emit(Event::ServerStopped {
@@ -225,79 +258,43 @@ impl ContextServerManager {
})
}
pub fn servers(&self) -> Vec<Arc<ContextServer>> {
pub fn servers(&self) -> Vec<Arc<dyn ContextServer>> {
self.servers.values().cloned().collect()
}
pub fn model(cx: &mut AppContext) -> Model<Self> {
cx.new_model(|_cx| ContextServerManager::new())
pub fn maintain_servers(&mut self, settings: &ContextServerSettings, cx: &ModelContext<Self>) {
let current_servers = self
.servers()
.into_iter()
.map(|server| (server.id(), server.config()))
.collect::<HashMap<_, _>>();
let new_servers = settings
.servers
.iter()
.map(|config| (config.id.clone(), config.clone()))
.collect::<HashMap<_, _>>();
let servers_to_add = new_servers
.values()
.filter(|config| !current_servers.contains_key(config.id.as_str()))
.cloned()
.collect::<Vec<_>>();
let servers_to_remove = current_servers
.keys()
.filter(|id| !new_servers.contains_key(id.as_ref()))
.cloned()
.collect::<Vec<_>>();
log::trace!("servers_to_add={:?}", servers_to_add);
for config in servers_to_add {
let server = Arc::new(NativeContextServer::new(Arc::new(config)));
self.add_server(server, cx).detach_and_log_err(cx);
}
for id in servers_to_remove {
self.remove_server(&id, cx).detach_and_log_err(cx);
}
}
}
pub struct GlobalContextServerManager(Model<ContextServerManager>);
impl Global for GlobalContextServerManager {}
impl GlobalContextServerManager {
fn register(cx: &mut AppContext) {
let model = ContextServerManager::model(cx);
cx.set_global(Self(model));
}
}
pub fn init(cx: &mut AppContext) {
ContextServerSettings::register(cx);
GlobalContextServerManager::register(cx);
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_namespace(CONTEXT_SERVERS_NAMESPACE);
});
cx.observe_global::<SettingsStore>(|cx| {
let manager = ContextServerManager::global(cx);
cx.update_model(&manager, |manager, cx| {
let settings = ContextServerSettings::get_global(cx);
let current_servers = manager
.servers()
.into_iter()
.map(|server| (server.id.clone(), server.config.clone()))
.collect::<HashMap<_, _>>();
let new_servers = settings
.servers
.iter()
.map(|config| (config.id.clone(), config.clone()))
.collect::<HashMap<_, _>>();
let servers_to_add = new_servers
.values()
.filter(|config| !current_servers.contains_key(&config.id))
.cloned()
.collect::<Vec<_>>();
let servers_to_remove = current_servers
.keys()
.filter(|id| !new_servers.contains_key(*id))
.cloned()
.collect::<Vec<_>>();
log::trace!("servers_to_add={:?}", servers_to_add);
for config in servers_to_add {
manager.add_server(config, cx).detach_and_log_err(cx);
}
for id in servers_to_remove {
manager.remove_server(&id, cx).detach_and_log_err(cx);
}
let has_any_context_servers = !manager.servers().is_empty();
CommandPaletteFilter::update_global(cx, |filter, _cx| {
if has_any_context_servers {
filter.show_namespace(CONTEXT_SERVERS_NAMESPACE);
} else {
filter.hide_namespace(CONTEXT_SERVERS_NAMESPACE);
}
});
})
})
.detach();
}

View File

@@ -113,7 +113,10 @@ impl InitializedContextServerProtocol {
let response: types::PromptsListResponse = self
.inner
.request(types::RequestType::PromptsList.as_str(), ())
.request(
types::RequestType::PromptsList.as_str(),
serde_json::json!({}),
)
.await?;
Ok(response.prompts)
@@ -125,7 +128,10 @@ impl InitializedContextServerProtocol {
let response: types::ResourcesListResponse = self
.inner
.request(types::RequestType::ResourcesList.as_str(), ())
.request(
types::RequestType::ResourcesList.as_str(),
serde_json::json!({}),
)
.await?;
Ok(response)

View File

@@ -1,69 +1,72 @@
use std::sync::Arc;
use anyhow::Result;
use collections::HashMap;
use gpui::{AppContext, Global, ReadGlobal};
use gpui::{AppContext, AsyncAppContext, ReadGlobal};
use gpui::{Global, Task};
use parking_lot::RwLock;
struct GlobalContextServerRegistry(Arc<ContextServerRegistry>);
use crate::ContextServer;
impl Global for GlobalContextServerRegistry {}
pub type ContextServerFactory =
Arc<dyn Fn(&AsyncAppContext) -> Task<Result<Arc<dyn ContextServer>>> + Send + Sync + 'static>;
pub struct ContextServerRegistry {
command_registry: RwLock<HashMap<String, Vec<Arc<str>>>>,
tool_registry: RwLock<HashMap<String, Vec<Arc<str>>>>,
#[derive(Default)]
struct GlobalContextServerFactoryRegistry(Arc<ContextServerFactoryRegistry>);
impl Global for GlobalContextServerFactoryRegistry {}
#[derive(Default)]
struct ContextServerFactoryRegistryState {
context_servers: HashMap<Arc<str>, ContextServerFactory>,
}
impl ContextServerRegistry {
#[derive(Default)]
pub struct ContextServerFactoryRegistry {
state: RwLock<ContextServerFactoryRegistryState>,
}
impl ContextServerFactoryRegistry {
/// Returns the global [`ContextServerFactoryRegistry`].
pub fn global(cx: &AppContext) -> Arc<Self> {
GlobalContextServerRegistry::global(cx).0.clone()
GlobalContextServerFactoryRegistry::global(cx).0.clone()
}
pub fn register(cx: &mut AppContext) {
cx.set_global(GlobalContextServerRegistry(Arc::new(
ContextServerRegistry {
command_registry: RwLock::new(HashMap::default()),
tool_registry: RwLock::new(HashMap::default()),
},
)))
/// Returns the global [`ContextServerFactoryRegistry`].
///
/// Inserts a default [`ContextServerFactoryRegistry`] if one does not yet exist.
pub fn default_global(cx: &mut AppContext) -> Arc<Self> {
cx.default_global::<GlobalContextServerFactoryRegistry>()
.0
.clone()
}
pub fn register_command(&self, server_id: String, command_name: &str) {
let mut registry = self.command_registry.write();
registry
.entry(server_id)
.or_default()
.push(command_name.into());
pub fn new() -> Arc<Self> {
Arc::new(Self {
state: RwLock::new(ContextServerFactoryRegistryState {
context_servers: HashMap::default(),
}),
})
}
pub fn unregister_command(&self, server_id: &str, command_name: &str) {
let mut registry = self.command_registry.write();
if let Some(commands) = registry.get_mut(server_id) {
commands.retain(|name| name.as_ref() != command_name);
}
pub fn context_server_factories(&self) -> Vec<(Arc<str>, ContextServerFactory)> {
self.state
.read()
.context_servers
.iter()
.map(|(id, factory)| (id.clone(), factory.clone()))
.collect()
}
pub fn get_commands(&self, server_id: &str) -> Option<Vec<Arc<str>>> {
let registry = self.command_registry.read();
registry.get(server_id).cloned()
/// Registers the provided [`ContextServerFactory`].
pub fn register_server_factory(&self, id: Arc<str>, factory: ContextServerFactory) {
let mut state = self.state.write();
state.context_servers.insert(id, factory);
}
pub fn register_tool(&self, server_id: String, tool_name: &str) {
let mut registry = self.tool_registry.write();
registry
.entry(server_id)
.or_default()
.push(tool_name.into());
}
pub fn unregister_tool(&self, server_id: &str, tool_name: &str) {
let mut registry = self.tool_registry.write();
if let Some(tools) = registry.get_mut(server_id) {
tools.retain(|name| name.as_ref() != tool_name);
}
}
pub fn get_tools(&self, server_id: &str) -> Option<Vec<Arc<str>>> {
let registry = self.tool_registry.read();
registry.get(server_id).cloned()
/// Unregisters the [`ContextServerFactory`] for the server with the given ID.
pub fn unregister_server_factory_by_id(&self, server_id: &str) {
let mut state = self.state.write();
state.context_servers.remove(server_id);
}
}

View File

@@ -87,11 +87,11 @@ impl Model {
pub fn max_token_count(&self) -> usize {
match self {
Self::Gpt4o => 128000,
Self::Gpt4 => 8192,
Self::Gpt3_5Turbo => 16385,
Self::O1Mini => 128000,
Self::O1Preview => 128000,
Self::Gpt4o => 64000,
Self::Gpt4 => 32768,
Self::Gpt3_5Turbo => 12288,
Self::O1Mini => 20000,
Self::O1Preview => 20000,
Self::Claude3_5Sonnet => 200_000,
}
}

View File

@@ -0,0 +1,859 @@
use std::{
cmp::Reverse,
sync::{atomic::AtomicBool, Arc},
time::Duration,
};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
uniform_list, AnyElement, BackgroundExecutor, FontWeight, ListSizingBehavior, Model,
MouseButton, ScrollStrategy, StrikethroughStyle, StyledText, Task, UniformListScrollHandle,
WeakView,
};
use language::{Buffer, Documentation};
use multi_buffer::{Anchor, ExcerptId};
use ordered_float::OrderedFloat;
use parking_lot::{Mutex, RwLock};
use project::{CodeAction, Completion, TaskSourceKind};
use settings::Settings;
use task::ResolvedTask;
use ui::{
div, h_flex, px, ActiveTheme, Color, Div, FluentBuilder, InteractiveElement, IntoElement,
Label, LabelCommon, LabelSize, ListItem, ParentElement, Pixels, Popover, Selectable,
SharedString, StatefulInteractiveElement, Styled, StyledExt, ViewContext,
};
use util::ResultExt;
use workspace::Workspace;
use crate::{
debounced_delay::DebouncedDelay, render_parsed_markdown, split_words,
styled_runs_for_code_label, CodeActionProvider, CompletionId, CompletionProvider,
ConfirmCodeAction, ConfirmCompletion, DisplayPoint, DisplayRow, Editor, EditorSettings,
EditorStyle, ResolvedTasks,
};
pub(super) enum ContextMenu {
Completions(CompletionsMenu),
CodeActions(CodeActionsMenu),
}
pub(super) struct RenderedContextMenu {
pub(super) origin: ContextMenuOrigin,
pub(super) element: AnyElement,
count: usize,
is_inverted: Arc<AtomicBool>,
scroll_handle: UniformListScrollHandle,
}
impl RenderedContextMenu {
pub(crate) fn invert(&self) {
let was_inverted = self
.is_inverted
.fetch_or(true, std::sync::atomic::Ordering::Release);
if !was_inverted {
self.scroll_handle.scroll_to_item(
self.count - self.scroll_handle.0.borrow().base_handle.top_item() - 1,
ScrollStrategy::Top,
);
}
}
}
impl ContextMenu {
pub(super) fn select_first(
&mut self,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) -> bool {
if self.visible() {
match self {
ContextMenu::Completions(menu) => menu.select_first(provider, cx),
ContextMenu::CodeActions(menu) => menu.select_first(cx),
}
true
} else {
false
}
}
pub(super) fn select_prev(
&mut self,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) -> bool {
if self.visible() {
match self {
ContextMenu::Completions(menu) => menu.select_prev(provider, cx),
ContextMenu::CodeActions(menu) => menu.select_prev(cx),
}
true
} else {
false
}
}
pub(super) fn select_next(
&mut self,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) -> bool {
if self.visible() {
match self {
ContextMenu::Completions(menu) => menu.select_next(provider, cx),
ContextMenu::CodeActions(menu) => menu.select_next(cx),
}
true
} else {
false
}
}
pub(super) fn select_last(
&mut self,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) -> bool {
if self.visible() {
match self {
ContextMenu::Completions(menu) => menu.select_last(provider, cx),
ContextMenu::CodeActions(menu) => menu.select_last(cx),
}
true
} else {
false
}
}
pub(super) fn visible(&self) -> bool {
match self {
ContextMenu::Completions(menu) => menu.visible(),
ContextMenu::CodeActions(menu) => menu.visible(),
}
}
pub(super) fn render(
&self,
cursor_position: DisplayPoint,
style: &EditorStyle,
max_height: Pixels,
workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> RenderedContextMenu {
match self {
ContextMenu::Completions(menu) => {
menu.render(cursor_position, style, max_height, workspace, cx)
}
ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, max_height, cx),
}
}
}
pub(crate) enum ContextMenuOrigin {
EditorPoint(DisplayPoint),
GutterIndicator(DisplayRow),
}
#[derive(Clone)]
pub(super) struct CompletionsMenu {
pub(super) id: CompletionId,
pub(super) sort_completions: bool,
pub(super) initial_position: Anchor,
pub(super) buffer: Model<Buffer>,
pub(super) completions: Arc<RwLock<Box<[Completion]>>>,
pub(super) match_candidates: Arc<[StringMatchCandidate]>,
pub(super) matches: Arc<[StringMatch]>,
pub(super) selected_item: usize,
pub(super) scroll_handle: UniformListScrollHandle,
pub(super) selected_completion_documentation_resolve_debounce: Arc<Mutex<DebouncedDelay>>,
pub(super) is_inverted: Arc<AtomicBool>,
}
impl CompletionsMenu {
fn select_first(
&mut self,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
self.selected_item = 0;
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
self.attempt_resolve_selected_completion_documentation(provider, cx);
cx.notify();
}
fn select_prev(
&mut self,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
if self.selected_item > 0 {
self.selected_item -= 1;
} else {
self.selected_item = self.matches.len() - 1;
}
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
self.attempt_resolve_selected_completion_documentation(provider, cx);
cx.notify();
}
fn select_next(
&mut self,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
if self.selected_item + 1 < self.matches.len() {
self.selected_item += 1;
} else {
self.selected_item = 0;
}
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
self.attempt_resolve_selected_completion_documentation(provider, cx);
cx.notify();
}
fn select_last(
&mut self,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
self.selected_item = self.matches.len() - 1;
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
self.attempt_resolve_selected_completion_documentation(provider, cx);
cx.notify();
}
pub(super) fn pre_resolve_completion_documentation(
buffer: Model<Buffer>,
completions: Arc<RwLock<Box<[Completion]>>>,
matches: Arc<[StringMatch]>,
editor: &Editor,
cx: &mut ViewContext<Editor>,
) -> Task<()> {
let settings = EditorSettings::get_global(cx);
if !settings.show_completion_documentation {
return Task::ready(());
}
let Some(provider) = editor.completion_provider.as_ref() else {
return Task::ready(());
};
let resolve_task = provider.resolve_completions(
buffer,
matches.iter().map(|m| m.candidate_id).collect(),
completions.clone(),
cx,
);
cx.spawn(move |this, mut cx| async move {
if let Some(true) = resolve_task.await.log_err() {
this.update(&mut cx, |_, cx| cx.notify()).ok();
}
})
}
fn attempt_resolve_selected_completion_documentation(
&mut self,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
let settings = EditorSettings::get_global(cx);
if !settings.show_completion_documentation {
return;
}
let completion_index = self.matches[self.selected_item].candidate_id;
let Some(provider) = provider else {
return;
};
let resolve_task = provider.resolve_completions(
self.buffer.clone(),
vec![completion_index],
self.completions.clone(),
cx,
);
let delay_ms =
EditorSettings::get_global(cx).completion_documentation_secondary_query_debounce;
let delay = Duration::from_millis(delay_ms);
self.selected_completion_documentation_resolve_debounce
.lock()
.fire_new(delay, cx, |_, cx| {
cx.spawn(move |this, mut cx| async move {
if let Some(true) = resolve_task.await.log_err() {
this.update(&mut cx, |_, cx| cx.notify()).ok();
}
})
});
}
fn visible(&self) -> bool {
!self.matches.is_empty()
}
fn render(
&self,
cursor_position: DisplayPoint,
style: &EditorStyle,
max_height: Pixels,
workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> RenderedContextMenu {
let settings = EditorSettings::get_global(cx);
let show_completion_documentation = settings.show_completion_documentation;
let widest_completion_ix = self
.matches
.iter()
.enumerate()
.max_by_key(|(_, mat)| {
let completions = self.completions.read();
let completion = &completions[mat.candidate_id];
let documentation = &completion.documentation;
let mut len = completion.label.text.chars().count();
if let Some(Documentation::SingleLine(text)) = documentation {
if show_completion_documentation {
len += text.chars().count();
}
}
len
})
.map(|(ix, _)| ix);
let completions = self.completions.clone();
let matches = self.matches.clone();
let selected_item = self.selected_item;
let style = style.clone();
let multiline_docs = if show_completion_documentation {
let mat = &self.matches[selected_item];
let multiline_docs = match &self.completions.read()[mat.candidate_id].documentation {
Some(Documentation::MultiLinePlainText(text)) => {
Some(div().child(SharedString::from(text.clone())))
}
Some(Documentation::MultiLineMarkdown(parsed)) if !parsed.text.is_empty() => {
Some(div().child(render_parsed_markdown(
"completions_markdown",
parsed,
&style,
workspace,
cx,
)))
}
_ => None,
};
multiline_docs.map(|div| {
div.id("multiline_docs")
.max_h(max_height)
.flex_1()
.px_1p5()
.py_1()
.min_w(px(260.))
.max_w(px(640.))
.w(px(500.))
.overflow_y_scroll()
.occlude()
})
} else {
None
};
let list = uniform_list(
cx.view().clone(),
"completions",
matches.len(),
move |_editor, range, cx| {
let start_ix = range.start;
let completions_guard = completions.read();
matches[range]
.iter()
.enumerate()
.map(|(ix, mat)| {
let item_ix = start_ix + ix;
let candidate_id = mat.candidate_id;
let completion = &completions_guard[candidate_id];
let documentation = if show_completion_documentation {
&completion.documentation
} else {
&None
};
let highlights = gpui::combine_highlights(
mat.ranges().map(|range| (range, FontWeight::BOLD.into())),
styled_runs_for_code_label(&completion.label, &style.syntax).map(
|(range, mut highlight)| {
// Ignore font weight for syntax highlighting, as we'll use it
// for fuzzy matches.
highlight.font_weight = None;
if completion.lsp_completion.deprecated.unwrap_or(false) {
highlight.strikethrough = Some(StrikethroughStyle {
thickness: 1.0.into(),
..Default::default()
});
highlight.color = Some(cx.theme().colors().text_muted);
}
(range, highlight)
},
),
);
let completion_label = StyledText::new(completion.label.text.clone())
.with_highlights(&style.text, highlights);
let documentation_label =
if let Some(Documentation::SingleLine(text)) = documentation {
if text.trim().is_empty() {
None
} else {
Some(
Label::new(text.clone())
.ml_4()
.size(LabelSize::Small)
.color(Color::Muted),
)
}
} else {
None
};
let color_swatch = completion
.color()
.map(|color| div().size_4().bg(color).rounded_sm());
div().min_w(px(220.)).max_w(px(540.)).child(
ListItem::new(mat.candidate_id)
.inset(true)
.selected(item_ix == selected_item)
.on_click(cx.listener(move |editor, _event, cx| {
cx.stop_propagation();
if let Some(task) = editor.confirm_completion(
&ConfirmCompletion {
item_ix: Some(item_ix),
},
cx,
) {
task.detach_and_log_err(cx)
}
}))
.start_slot::<Div>(color_swatch)
.child(h_flex().overflow_hidden().child(completion_label))
.end_slot::<Label>(documentation_label),
)
})
.collect()
},
)
.occlude()
.max_h(max_height)
.track_scroll(self.scroll_handle.clone())
.with_width_from_item(widest_completion_ix)
.with_sizing_behavior(ListSizingBehavior::Infer);
RenderedContextMenu {
origin: ContextMenuOrigin::EditorPoint(cursor_position),
count: self.matches.len(),
scroll_handle: self.scroll_handle.clone(),
element: Popover::new()
.child(list)
.when_some(multiline_docs, |popover, multiline_docs| {
popover.aside(multiline_docs)
})
.into_any_element(),
is_inverted: self.is_inverted.clone(),
}
}
pub async fn filter(&mut self, query: Option<&str>, executor: BackgroundExecutor) {
let mut matches = if let Some(query) = query {
fuzzy::match_strings(
&self.match_candidates,
query,
query.chars().any(|c| c.is_uppercase()),
100,
&Default::default(),
executor,
)
.await
} else {
self.match_candidates
.iter()
.enumerate()
.map(|(candidate_id, candidate)| StringMatch {
candidate_id,
score: Default::default(),
positions: Default::default(),
string: candidate.string.clone(),
})
.collect()
};
// Remove all candidates where the query's start does not match the start of any word in the candidate
if let Some(query) = query {
if let Some(query_start) = query.chars().next() {
matches.retain(|string_match| {
split_words(&string_match.string).any(|word| {
// Check that the first codepoint of the word as lowercase matches the first
// codepoint of the query as lowercase
word.chars()
.flat_map(|codepoint| codepoint.to_lowercase())
.zip(query_start.to_lowercase())
.all(|(word_cp, query_cp)| word_cp == query_cp)
})
});
}
}
let completions = self.completions.read();
if self.sort_completions {
matches.sort_unstable_by_key(|mat| {
// We do want to strike a balance here between what the language server tells us
// to sort by (the sort_text) and what are "obvious" good matches (i.e. when you type
// `Creat` and there is a local variable called `CreateComponent`).
// So what we do is: we bucket all matches into two buckets
// - Strong matches
// - Weak matches
// Strong matches are the ones with a high fuzzy-matcher score (the "obvious" matches)
// and the Weak matches are the rest.
//
// For the strong matches, we sort by our fuzzy-finder score first and for the weak
// matches, we prefer language-server sort_text first.
//
// The thinking behind that: we want to show strong matches first in order of relevance(fuzzy score).
// Rest of the matches(weak) can be sorted as language-server expects.
#[derive(PartialEq, Eq, PartialOrd, Ord)]
enum MatchScore<'a> {
Strong {
score: Reverse<OrderedFloat<f64>>,
sort_text: Option<&'a str>,
sort_key: (usize, &'a str),
},
Weak {
sort_text: Option<&'a str>,
score: Reverse<OrderedFloat<f64>>,
sort_key: (usize, &'a str),
},
}
let completion = &completions[mat.candidate_id];
let sort_key = completion.sort_key();
let sort_text = completion.lsp_completion.sort_text.as_deref();
let score = Reverse(OrderedFloat(mat.score));
if mat.score >= 0.2 {
MatchScore::Strong {
score,
sort_text,
sort_key,
}
} else {
MatchScore::Weak {
sort_text,
score,
sort_key,
}
}
});
}
for mat in &mut matches {
let completion = &completions[mat.candidate_id];
mat.string.clone_from(&completion.label.text);
for position in &mut mat.positions {
*position += completion.label.filter_range.start;
}
}
drop(completions);
self.matches = matches.into();
self.selected_item = 0;
}
}
pub(super) struct AvailableCodeAction {
pub(super) excerpt_id: ExcerptId,
pub(super) action: CodeAction,
pub(super) provider: Arc<dyn CodeActionProvider>,
}
#[derive(Clone)]
pub(super) struct CodeActionContents {
pub(super) tasks: Option<Arc<ResolvedTasks>>,
pub(super) actions: Option<Arc<[AvailableCodeAction]>>,
}
impl CodeActionContents {
fn len(&self) -> usize {
match (&self.tasks, &self.actions) {
(Some(tasks), Some(actions)) => actions.len() + tasks.templates.len(),
(Some(tasks), None) => tasks.templates.len(),
(None, Some(actions)) => actions.len(),
(None, None) => 0,
}
}
fn is_empty(&self) -> bool {
match (&self.tasks, &self.actions) {
(Some(tasks), Some(actions)) => actions.is_empty() && tasks.templates.is_empty(),
(Some(tasks), None) => tasks.templates.is_empty(),
(None, Some(actions)) => actions.is_empty(),
(None, None) => true,
}
}
fn iter(&self) -> impl Iterator<Item = CodeActionsItem> + '_ {
self.tasks
.iter()
.flat_map(|tasks| {
tasks
.templates
.iter()
.map(|(kind, task)| CodeActionsItem::Task(kind.clone(), task.clone()))
})
.chain(self.actions.iter().flat_map(|actions| {
actions.iter().map(|available| CodeActionsItem::CodeAction {
excerpt_id: available.excerpt_id,
action: available.action.clone(),
provider: available.provider.clone(),
})
}))
}
pub(super) fn get(&self, index: usize) -> Option<CodeActionsItem> {
match (&self.tasks, &self.actions) {
(Some(tasks), Some(actions)) => {
if index < tasks.templates.len() {
tasks
.templates
.get(index)
.cloned()
.map(|(kind, task)| CodeActionsItem::Task(kind, task))
} else {
actions.get(index - tasks.templates.len()).map(|available| {
CodeActionsItem::CodeAction {
excerpt_id: available.excerpt_id,
action: available.action.clone(),
provider: available.provider.clone(),
}
})
}
}
(Some(tasks), None) => tasks
.templates
.get(index)
.cloned()
.map(|(kind, task)| CodeActionsItem::Task(kind, task)),
(None, Some(actions)) => {
actions
.get(index)
.map(|available| CodeActionsItem::CodeAction {
excerpt_id: available.excerpt_id,
action: available.action.clone(),
provider: available.provider.clone(),
})
}
(None, None) => None,
}
}
}
#[allow(clippy::large_enum_variant)]
#[derive(Clone)]
pub(super) enum CodeActionsItem {
Task(TaskSourceKind, ResolvedTask),
CodeAction {
excerpt_id: ExcerptId,
action: CodeAction,
provider: Arc<dyn CodeActionProvider>,
},
}
impl CodeActionsItem {
fn as_task(&self) -> Option<&ResolvedTask> {
let Self::Task(_, task) = self else {
return None;
};
Some(task)
}
fn as_code_action(&self) -> Option<&CodeAction> {
let Self::CodeAction { action, .. } = self else {
return None;
};
Some(action)
}
pub(super) fn label(&self) -> String {
match self {
Self::CodeAction { action, .. } => action.lsp_action.title.clone(),
Self::Task(_, task) => task.resolved_label.clone(),
}
}
}
pub(crate) struct CodeActionsMenu {
pub(super) actions: CodeActionContents,
pub(super) buffer: Model<Buffer>,
pub(super) selected_item: usize,
pub(super) scroll_handle: UniformListScrollHandle,
pub(super) deployed_from_indicator: Option<DisplayRow>,
}
impl CodeActionsMenu {
fn select_first(&mut self, cx: &mut ViewContext<Editor>) {
self.selected_item = 0;
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
cx.notify()
}
fn select_prev(&mut self, cx: &mut ViewContext<Editor>) {
if self.selected_item > 0 {
self.selected_item -= 1;
} else {
self.selected_item = self.actions.len() - 1;
}
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
cx.notify();
}
fn select_next(&mut self, cx: &mut ViewContext<Editor>) {
if self.selected_item + 1 < self.actions.len() {
self.selected_item += 1;
} else {
self.selected_item = 0;
}
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
cx.notify();
}
fn select_last(&mut self, cx: &mut ViewContext<Editor>) {
self.selected_item = self.actions.len() - 1;
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
cx.notify()
}
fn visible(&self) -> bool {
!self.actions.is_empty()
}
fn render(
&self,
cursor_position: DisplayPoint,
_style: &EditorStyle,
max_height: Pixels,
cx: &mut ViewContext<Editor>,
) -> RenderedContextMenu {
let actions = self.actions.clone();
let selected_item = self.selected_item;
let is_inverted = Arc::new(Default::default());
let element = uniform_list(
cx.view().clone(),
"code_actions_menu",
self.actions.len(),
move |_this, range, cx| {
actions
.iter()
.skip(range.start)
.take(range.end - range.start)
.enumerate()
.map(|(ix, action)| {
let item_ix = range.start + ix;
let selected = selected_item == item_ix;
let colors = cx.theme().colors();
div()
.px_1()
.rounded_md()
.text_color(colors.text)
.when(selected, |style| {
style
.bg(colors.element_active)
.text_color(colors.text_accent)
})
.hover(|style| {
style
.bg(colors.element_hover)
.text_color(colors.text_accent)
})
.whitespace_nowrap()
.when_some(action.as_code_action(), |this, action| {
this.on_mouse_down(
MouseButton::Left,
cx.listener(move |editor, _, cx| {
cx.stop_propagation();
if let Some(task) = editor.confirm_code_action(
&ConfirmCodeAction {
item_ix: Some(item_ix),
},
cx,
) {
task.detach_and_log_err(cx)
}
}),
)
// TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
.child(SharedString::from(action.lsp_action.title.clone()))
})
.when_some(action.as_task(), |this, task| {
this.on_mouse_down(
MouseButton::Left,
cx.listener(move |editor, _, cx| {
cx.stop_propagation();
if let Some(task) = editor.confirm_code_action(
&ConfirmCodeAction {
item_ix: Some(item_ix),
},
cx,
) {
task.detach_and_log_err(cx)
}
}),
)
.child(SharedString::from(task.resolved_label.clone()))
})
})
.collect()
},
)
.elevation_1(cx)
.p_1()
.max_h(max_height)
.occlude()
.track_scroll(self.scroll_handle.clone())
.with_width_from_item(
self.actions
.iter()
.enumerate()
.max_by_key(|(_, action)| match action {
CodeActionsItem::Task(_, task) => task.resolved_label.chars().count(),
CodeActionsItem::CodeAction { action, .. } => {
action.lsp_action.title.chars().count()
}
})
.map(|(ix, _)| ix),
)
.with_sizing_behavior(ListSizingBehavior::Infer)
.into_any_element();
let origin = if let Some(row) = self.deployed_from_indicator {
ContextMenuOrigin::GutterIndicator(row)
} else {
ContextMenuOrigin::EditorPoint(cursor_position)
};
RenderedContextMenu {
origin,
element,
is_inverted,
count: self.actions.len(),
scroll_handle: self.scroll_handle.clone(),
}
}
}

View File

@@ -36,7 +36,7 @@ use block_map::{BlockRow, BlockSnapshot};
use collections::{HashMap, HashSet};
pub use crease_map::*;
pub use fold_map::{Fold, FoldId, FoldPlaceholder, FoldPoint};
use fold_map::{FoldMap, FoldSnapshot};
use fold_map::{FoldMap, FoldMapWriter, FoldOffset, FoldSnapshot};
use gpui::{
AnyElement, Font, HighlightStyle, LineLayout, Model, ModelContext, Pixels, UnderlineStyle,
};
@@ -65,7 +65,7 @@ use std::{
};
use sum_tree::{Bias, TreeMap};
use tab_map::{TabMap, TabSnapshot};
use text::LineIndent;
use text::{Edit, LineIndent};
use ui::{div, px, IntoElement, ParentElement, SharedString, Styled, WindowContext};
use unicode_segmentation::UnicodeSegmentation;
use wrap_map::{WrapMap, WrapSnapshot};
@@ -206,34 +206,41 @@ impl DisplayMap {
);
}
/// Creates folds for the given ranges.
pub fn fold<T: ToOffset>(
&mut self,
ranges: impl IntoIterator<Item = (Range<T>, FoldPlaceholder)>,
cx: &mut ModelContext<Self>,
) {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
self.block_map.read(snapshot, edits);
let (snapshot, edits) = fold_map.fold(ranges);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
self.block_map.read(snapshot, edits);
self.update_fold_map(cx, |fold_map| fold_map.fold(ranges))
}
pub fn unfold<T: ToOffset>(
/// Removes any folds with the given ranges.
pub fn remove_folds_with_type<T: ToOffset>(
&mut self,
ranges: impl IntoIterator<Item = Range<T>>,
type_id: TypeId,
cx: &mut ModelContext<Self>,
) {
self.update_fold_map(cx, |fold_map| fold_map.remove_folds(ranges, type_id))
}
/// Removes any folds whose ranges intersect any of the given ranges.
pub fn unfold_intersecting<T: ToOffset>(
&mut self,
ranges: impl IntoIterator<Item = Range<T>>,
inclusive: bool,
cx: &mut ModelContext<Self>,
) {
self.update_fold_map(cx, |fold_map| {
fold_map.unfold_intersecting(ranges, inclusive)
})
}
fn update_fold_map(
&mut self,
cx: &mut ModelContext<Self>,
callback: impl FnOnce(&mut FoldMapWriter) -> (FoldSnapshot, Vec<Edit<FoldOffset>>),
) {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
@@ -245,7 +252,7 @@ impl DisplayMap {
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
self.block_map.read(snapshot, edits);
let (snapshot, edits) = fold_map.unfold(ranges, inclusive);
let (snapshot, edits) = callback(&mut fold_map);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
@@ -1442,7 +1449,7 @@ pub mod tests {
if rng.gen() && fold_count > 0 {
log::info!("unfolding ranges: {:?}", ranges);
map.update(cx, |map, cx| {
map.unfold(ranges, true, cx);
map.unfold_intersecting(ranges, true, cx);
});
} else {
log::info!("folding ranges: {:?}", ranges);

View File

@@ -6,12 +6,14 @@ use gpui::{AnyElement, ElementId, WindowContext};
use language::{Chunk, ChunkRenderer, Edit, Point, TextSummary};
use multi_buffer::{Anchor, AnchorRangeExt, MultiBufferRow, MultiBufferSnapshot, ToOffset};
use std::{
any::TypeId,
cmp::{self, Ordering},
fmt, iter,
ops::{Add, AddAssign, Deref, DerefMut, Range, Sub},
sync::Arc,
};
use sum_tree::{Bias, Cursor, FilterCursor, SumTree, Summary};
use ui::IntoElement as _;
use util::post_inc;
#[derive(Clone)]
@@ -22,17 +24,29 @@ pub struct FoldPlaceholder {
pub constrain_width: bool,
/// If true, merges the fold with an adjacent one.
pub merge_adjacent: bool,
/// Category of the fold. Useful for carefully removing from overlapping folds.
pub type_tag: Option<TypeId>,
}
impl Default for FoldPlaceholder {
fn default() -> Self {
Self {
render: Arc::new(|_, _, _| gpui::Empty.into_any_element()),
constrain_width: true,
merge_adjacent: true,
type_tag: None,
}
}
}
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,
type_tag: None,
}
}
}
@@ -173,9 +187,34 @@ impl<'a> FoldMapWriter<'a> {
(self.0.snapshot.clone(), edits)
}
pub(crate) fn unfold<T: ToOffset>(
/// Removes any folds with the given ranges.
pub(crate) fn remove_folds<T: ToOffset>(
&mut self,
ranges: impl IntoIterator<Item = Range<T>>,
type_id: TypeId,
) -> (FoldSnapshot, Vec<FoldEdit>) {
self.remove_folds_with(
ranges,
|fold| fold.placeholder.type_tag == Some(type_id),
false,
)
}
/// Removes any folds whose ranges intersect the given ranges.
pub(crate) fn unfold_intersecting<T: ToOffset>(
&mut self,
ranges: impl IntoIterator<Item = Range<T>>,
inclusive: bool,
) -> (FoldSnapshot, Vec<FoldEdit>) {
self.remove_folds_with(ranges, |_| true, inclusive)
}
/// Removes any folds that intersect the given ranges and for which the given predicate
/// returns true.
fn remove_folds_with<T: ToOffset>(
&mut self,
ranges: impl IntoIterator<Item = Range<T>>,
should_unfold: impl Fn(&Fold) -> bool,
inclusive: bool,
) -> (FoldSnapshot, Vec<FoldEdit>) {
let mut edits = Vec::new();
@@ -183,21 +222,23 @@ impl<'a> FoldMapWriter<'a> {
let snapshot = self.0.snapshot.inlay_snapshot.clone();
let buffer = &snapshot.buffer;
for range in ranges.into_iter() {
// Remove intersecting folds and add their ranges to edits that are passed to sync.
let range = range.start.to_offset(buffer)..range.end.to_offset(buffer);
let mut folds_cursor =
intersecting_folds(&snapshot, &self.0.snapshot.folds, range, inclusive);
intersecting_folds(&snapshot, &self.0.snapshot.folds, range.clone(), inclusive);
while let Some(fold) = folds_cursor.item() {
let offset_range =
fold.range.start.to_offset(buffer)..fold.range.end.to_offset(buffer);
if offset_range.end > offset_range.start {
let inlay_range = snapshot.to_inlay_offset(offset_range.start)
..snapshot.to_inlay_offset(offset_range.end);
edits.push(InlayEdit {
old: inlay_range.clone(),
new: inlay_range,
});
if should_unfold(fold) {
if offset_range.end > offset_range.start {
let inlay_range = snapshot.to_inlay_offset(offset_range.start)
..snapshot.to_inlay_offset(offset_range.end);
edits.push(InlayEdit {
old: inlay_range.clone(),
new: inlay_range,
});
}
fold_ixs_to_delete.push(*folds_cursor.start());
}
fold_ixs_to_delete.push(*folds_cursor.start());
folds_cursor.next(buffer);
}
}
@@ -665,6 +706,8 @@ impl FoldSnapshot {
where
T: ToOffset,
{
let buffer = &self.inlay_snapshot.buffer;
let range = range.start.to_offset(buffer)..range.end.to_offset(buffer);
let mut folds = intersecting_folds(&self.inlay_snapshot, &self.folds, range, false);
iter::from_fn(move || {
let item = folds.item();
@@ -821,15 +864,12 @@ fn push_isomorphic(transforms: &mut SumTree<Transform>, summary: TextSummary) {
}
}
fn intersecting_folds<'a, T>(
fn intersecting_folds<'a>(
inlay_snapshot: &'a InlaySnapshot,
folds: &'a SumTree<Fold>,
range: Range<T>,
range: Range<usize>,
inclusive: bool,
) -> FilterCursor<'a, impl 'a + FnMut(&FoldSummary) -> bool, Fold, usize>
where
T: ToOffset,
{
) -> FilterCursor<'a, impl 'a + FnMut(&FoldSummary) -> bool, Fold, usize> {
let buffer = &inlay_snapshot.buffer;
let start = buffer.anchor_before(range.start.to_offset(buffer));
let end = buffer.anchor_after(range.end.to_offset(buffer));
@@ -1419,12 +1459,12 @@ mod tests {
assert_eq!(snapshot4.text(), "123a⋯c123456eee");
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), false);
writer.unfold_intersecting(Some(Point::new(0, 4)..Point::new(0, 4)), false);
let (snapshot5, _) = map.read(inlay_snapshot.clone(), vec![]);
assert_eq!(snapshot5.text(), "123a⋯c123456eee");
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), true);
writer.unfold_intersecting(Some(Point::new(0, 4)..Point::new(0, 4)), true);
let (snapshot6, _) = map.read(inlay_snapshot, vec![]);
assert_eq!(snapshot6.text(), "123aaaaa\nbbbbbb\nccc123456eee");
}
@@ -1913,7 +1953,7 @@ mod tests {
log::info!("unfolding {:?} (inclusive: {})", to_unfold, inclusive);
let (mut writer, snapshot, edits) = self.write(inlay_snapshot, vec![]);
snapshot_edits.push((snapshot, edits));
let (snapshot, edits) = writer.unfold(to_unfold, inclusive);
let (snapshot, edits) = writer.unfold_intersecting(to_unfold, inclusive);
snapshot_edits.push((snapshot, edits));
}
_ => {

File diff suppressed because it is too large Load Diff

View File

@@ -169,8 +169,12 @@ fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut now = Instant::now();
let buffer = cx.new_model(|cx| language::Buffer::local("123456", cx));
let group_interval = buffer.update(cx, |buffer, _| buffer.transaction_group_interval());
let group_interval = Duration::from_millis(1);
let buffer = cx.new_model(|cx| {
let mut buf = language::Buffer::local("123456", cx);
buf.set_group_interval(group_interval);
buf
});
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let editor = cx.add_window(|cx| build_editor(buffer.clone(), cx));

View File

@@ -1,5 +1,6 @@
use crate::{
blame_entry_tooltip::{blame_entry_relative_timestamp, BlameEntryTooltip},
context_menu::CodeActionsMenu,
display_map::{
Block, BlockContext, BlockStyle, DisplaySnapshot, HighlightedChunk, ToDisplayPoint,
},
@@ -16,12 +17,12 @@ use crate::{
items::BufferSearchHighlights,
mouse_context_menu::{self, MenuPosition, MouseContextMenu},
scroll::scroll_amount::ScrollAmount,
BlockId, CodeActionsMenu, CursorShape, CustomBlockId, DisplayPoint, DisplayRow,
DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, EditorSettings,
EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GutterDimensions, HalfPageDown,
HalfPageUp, HandleInput, HoveredCursor, HoveredHunk, LineDown, LineUp, OpenExcerpts, PageDown,
PageUp, Point, RowExt, RowRangeExt, SelectPhase, Selection, SoftWrap, ToPoint,
CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN,
BlockId, CursorShape, CustomBlockId, DisplayPoint, DisplayRow, DocumentHighlightRead,
DocumentHighlightWrite, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle,
ExpandExcerpts, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput,
HoveredCursor, HoveredHunk, LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point, RowExt,
RowRangeExt, SelectPhase, Selection, SoftWrap, ToPoint, CURSORS_VISIBLE_FOR,
FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN,
MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
};
use client::ParticipantIndex;
@@ -1968,10 +1969,10 @@ impl EditorElement {
fn layout_lines(
rows: Range<DisplayRow>,
line_number_layouts: &[Option<ShapedLine>],
snapshot: &EditorSnapshot,
style: &EditorStyle,
editor_width: Pixels,
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
cx: &mut WindowContext,
) -> Vec<LineWithInvisibles> {
if rows.start >= rows.end {
@@ -2020,9 +2021,9 @@ impl EditorElement {
&style.text,
MAX_LINE_LEN,
rows.len(),
line_number_layouts,
snapshot.mode,
editor_width,
is_row_soft_wrapped,
cx,
)
}
@@ -2071,6 +2072,7 @@ impl EditorElement {
scroll_width: &mut Pixels,
resized_blocks: &mut HashMap<CustomBlockId, u32>,
selections: &[Selection<Point>],
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
cx: &mut WindowContext,
) -> (AnyElement, Size<Pixels>) {
let mut element = match block {
@@ -2083,8 +2085,15 @@ impl EditorElement {
line_layouts[align_to.row().minus(rows.start) as usize]
.x_for_index(align_to.column() as usize)
} else {
layout_line(align_to.row(), snapshot, &self.style, editor_width, cx)
.x_for_index(align_to.column() as usize)
layout_line(
align_to.row(),
snapshot,
&self.style,
editor_width,
is_row_soft_wrapped,
cx,
)
.x_for_index(align_to.column() as usize)
};
let selected = selections
@@ -2447,6 +2456,7 @@ impl EditorElement {
line_height: Pixels,
line_layouts: &[LineWithInvisibles],
selections: &[Selection<Point>],
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
cx: &mut WindowContext,
) -> Result<Vec<BlockLayout>, HashMap<CustomBlockId, u32>> {
let (fixed_blocks, non_fixed_blocks) = snapshot
@@ -2484,6 +2494,7 @@ impl EditorElement {
scroll_width,
&mut resized_blocks,
selections,
is_row_soft_wrapped,
cx,
);
fixed_block_max_width = fixed_block_max_width.max(element_size.width + em_width);
@@ -2529,6 +2540,7 @@ impl EditorElement {
scroll_width,
&mut resized_blocks,
selections,
is_row_soft_wrapped,
cx,
);
@@ -2575,6 +2587,7 @@ impl EditorElement {
scroll_width,
&mut resized_blocks,
selections,
is_row_soft_wrapped,
cx,
);
@@ -2655,12 +2668,12 @@ impl EditorElement {
gutter_overshoot: Pixels,
cx: &mut WindowContext,
) -> bool {
let max_height = cmp::min(
12. * line_height,
cmp::max(3. * line_height, (hitbox.size.height - line_height) / 2.),
);
let Some((position, mut context_menu)) = self.editor.update(cx, |editor, cx| {
let Some(mut context_menu) = self.editor.update(cx, |editor, cx| {
if editor.context_menu_visible() {
let max_height = cmp::min(
12. * line_height,
cmp::max(3. * line_height, (hitbox.size.height - line_height) / 2.),
);
editor.render_context_menu(newest_selection_head, &self.style, max_height, cx)
} else {
None
@@ -2668,18 +2681,19 @@ impl EditorElement {
}) else {
return false;
};
let context_menu_size = context_menu
.element
.layout_as_root(AvailableSpace::min_size(), cx);
let context_menu_size = context_menu.layout_as_root(AvailableSpace::min_size(), cx);
let (x, y) = match position {
crate::ContextMenuOrigin::EditorPoint(point) => {
let (x, y) = match context_menu.origin {
crate::context_menu::ContextMenuOrigin::EditorPoint(point) => {
let cursor_row_layout = &line_layouts[point.row().minus(start_row) as usize];
let x = cursor_row_layout.x_for_index(point.column() as usize)
- scroll_pixel_position.x;
let y = point.row().next_row().as_f32() * line_height - scroll_pixel_position.y;
(x, y)
}
crate::ContextMenuOrigin::GutterIndicator(row) => {
crate::context_menu::ContextMenuOrigin::GutterIndicator(row) => {
// Context menu was spawned via a click on a gutter. Ensure it's a bit closer to the indicator than just a plain first column of the
// text field.
let x = -gutter_overshoot;
@@ -2697,12 +2711,12 @@ impl EditorElement {
if list_origin.x + list_width > cx.viewport_size().width {
list_origin.x = (cx.viewport_size().width - list_width).max(Pixels::ZERO);
}
if list_origin.y + list_height > text_hitbox.lower_right().y {
context_menu.invert();
list_origin.y -= line_height + list_height;
}
cx.defer_draw(context_menu, list_origin, 1);
cx.defer_draw(context_menu.element, list_origin, 1);
true
}
@@ -4359,9 +4373,9 @@ impl LineWithInvisibles {
text_style: &TextStyle,
max_line_len: usize,
max_line_count: usize,
line_number_layouts: &[Option<ShapedLine>],
editor_mode: EditorMode,
text_width: Pixels,
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
cx: &mut WindowContext,
) -> Vec<Self> {
let mut layouts = Vec::with_capacity(max_line_count);
@@ -4489,12 +4503,9 @@ impl LineWithInvisibles {
if editor_mode == EditorMode::Full {
// Line wrap pads its contents with fake whitespaces,
// avoid printing them
let inside_wrapped_string = line_number_layouts
.get(row)
.and_then(|layout| layout.as_ref())
.is_none();
let is_soft_wrapped = is_row_soft_wrapped(row);
if highlighted_chunk.is_tab {
if non_whitespace_added || !inside_wrapped_string {
if non_whitespace_added || !is_soft_wrapped {
invisibles.push(Invisible::Tab {
line_start_offset: line.len(),
line_end_offset: line.len() + line_chunk.len(),
@@ -4510,7 +4521,7 @@ impl LineWithInvisibles {
(*line_byte as char).is_whitespace();
non_whitespace_added |= !is_whitespace;
is_whitespace
&& (non_whitespace_added || !inside_wrapped_string)
&& (non_whitespace_added || !is_soft_wrapped)
})
.map(|(whitespace_index, _)| Invisible::Whitespace {
line_offset: line.len() + whitespace_index,
@@ -4873,10 +4884,10 @@ impl Element for EditorElement {
editor_handle.update(cx, |editor, cx| editor.snapshot(cx));
let line = Self::layout_lines(
DisplayRow(0)..DisplayRow(1),
&[],
&editor_snapshot,
&style,
px(f32::MAX),
|_| false, // Single lines never soft wrap
cx,
)
.pop()
@@ -5085,6 +5096,8 @@ impl Element for EditorElement {
.buffer_rows(start_row)
.take((start_row..end_row).len())
.collect::<Vec<_>>();
let is_row_soft_wrapped =
|row| buffer_rows.get(row).copied().flatten().is_none();
let start_anchor = if start_row == Default::default() {
Anchor::min()
@@ -5176,10 +5189,10 @@ impl Element for EditorElement {
let mut max_visible_line_width = Pixels::ZERO;
let mut line_layouts = Self::layout_lines(
start_row..end_row,
&line_numbers,
&snapshot,
&self.style,
editor_width,
is_row_soft_wrapped,
cx,
);
for line_with_invisibles in &line_layouts {
@@ -5188,9 +5201,15 @@ impl Element for EditorElement {
}
}
let longest_line_width =
layout_line(snapshot.longest_row(), &snapshot, &style, editor_width, cx)
.width;
let longest_line_width = layout_line(
snapshot.longest_row(),
&snapshot,
&style,
editor_width,
is_row_soft_wrapped,
cx,
)
.width;
let mut scroll_width =
longest_line_width.max(max_visible_line_width) + overscroll.width;
@@ -5208,6 +5227,7 @@ impl Element for EditorElement {
line_height,
&line_layouts,
&local_selections,
is_row_soft_wrapped,
cx,
)
});
@@ -5966,6 +5986,7 @@ fn layout_line(
snapshot: &EditorSnapshot,
style: &EditorStyle,
text_width: Pixels,
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
cx: &mut WindowContext,
) -> LineWithInvisibles {
let chunks = snapshot.highlighted_chunks(row..row + DisplayRow(1), true, style);
@@ -5974,9 +5995,9 @@ fn layout_line(
&style.text,
MAX_LINE_LEN,
1,
&[],
snapshot.mode,
text_width,
is_row_soft_wrapped,
cx,
)
.pop()
@@ -6661,15 +6682,22 @@ mod tests {
"Hardcoded expected invisibles differ from the actual ones in '{input_text}'"
);
init_test(cx, |s| {
s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
s.defaults.tab_size = NonZeroU32::new(TAB_SIZE);
});
for show_line_numbers in [true, false] {
init_test(cx, |s| {
s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
s.defaults.tab_size = NonZeroU32::new(TAB_SIZE);
});
let actual_invisibles =
collect_invisibles_from_new_editor(cx, EditorMode::Full, input_text, px(500.0));
let actual_invisibles = collect_invisibles_from_new_editor(
cx,
EditorMode::Full,
input_text,
px(500.0),
show_line_numbers,
);
assert_eq!(expected_invisibles, actual_invisibles);
assert_eq!(expected_invisibles, actual_invisibles);
}
}
#[gpui::test]
@@ -6683,14 +6711,17 @@ mod tests {
EditorMode::SingleLine { auto_width: false },
EditorMode::AutoHeight { max_lines: 100 },
] {
let invisibles = collect_invisibles_from_new_editor(
cx,
editor_mode_without_invisibles,
"\t\t\t| | a b",
px(500.0),
);
assert!(invisibles.is_empty(),
for show_line_numbers in [true, false] {
let invisibles = collect_invisibles_from_new_editor(
cx,
editor_mode_without_invisibles,
"\t\t\t| | a b",
px(500.0),
show_line_numbers,
);
assert!(invisibles.is_empty(),
"For editor mode {editor_mode_without_invisibles:?} no invisibles was expected but got {invisibles:?}");
}
}
}
@@ -6741,43 +6772,48 @@ mod tests {
let resize_step = 10.0;
let mut editor_width = 200.0;
while editor_width <= 1000.0 {
update_test_language_settings(cx, |s| {
s.defaults.tab_size = NonZeroU32::new(tab_size);
s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
s.defaults.preferred_line_length = Some(editor_width as u32);
s.defaults.soft_wrap = Some(language_settings::SoftWrap::PreferredLineLength);
});
for show_line_numbers in [true, false] {
update_test_language_settings(cx, |s| {
s.defaults.tab_size = NonZeroU32::new(tab_size);
s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
s.defaults.preferred_line_length = Some(editor_width as u32);
s.defaults.soft_wrap = Some(language_settings::SoftWrap::PreferredLineLength);
});
let actual_invisibles = collect_invisibles_from_new_editor(
cx,
EditorMode::Full,
&input_text,
px(editor_width),
);
let actual_invisibles = collect_invisibles_from_new_editor(
cx,
EditorMode::Full,
&input_text,
px(editor_width),
show_line_numbers,
);
// Whatever the editor size is, ensure it has the same invisible kinds in the same order
// (no good guarantees about the offsets: wrapping could trigger padding and its tests should check the offsets).
let mut i = 0;
for (actual_index, actual_invisible) in actual_invisibles.iter().enumerate() {
i = actual_index;
match expected_invisibles.get(i) {
Some(expected_invisible) => match (expected_invisible, actual_invisible) {
(Invisible::Whitespace { .. }, Invisible::Whitespace { .. })
| (Invisible::Tab { .. }, Invisible::Tab { .. }) => {}
_ => {
panic!("At index {i}, expected invisible {expected_invisible:?} does not match actual {actual_invisible:?} by kind. Actual invisibles: {actual_invisibles:?}")
// Whatever the editor size is, ensure it has the same invisible kinds in the same order
// (no good guarantees about the offsets: wrapping could trigger padding and its tests should check the offsets).
let mut i = 0;
for (actual_index, actual_invisible) in actual_invisibles.iter().enumerate() {
i = actual_index;
match expected_invisibles.get(i) {
Some(expected_invisible) => match (expected_invisible, actual_invisible) {
(Invisible::Whitespace { .. }, Invisible::Whitespace { .. })
| (Invisible::Tab { .. }, Invisible::Tab { .. }) => {}
_ => {
panic!("At index {i}, expected invisible {expected_invisible:?} does not match actual {actual_invisible:?} by kind. Actual invisibles: {actual_invisibles:?}")
}
},
None => {
panic!("Unexpected extra invisible {actual_invisible:?} at index {i}")
}
},
None => panic!("Unexpected extra invisible {actual_invisible:?} at index {i}"),
}
}
}
let missing_expected_invisibles = &expected_invisibles[i + 1..];
assert!(
missing_expected_invisibles.is_empty(),
"Missing expected invisibles after index {i}: {missing_expected_invisibles:?}"
);
let missing_expected_invisibles = &expected_invisibles[i + 1..];
assert!(
missing_expected_invisibles.is_empty(),
"Missing expected invisibles after index {i}: {missing_expected_invisibles:?}"
);
editor_width += resize_step;
editor_width += resize_step;
}
}
}
@@ -6786,6 +6822,7 @@ mod tests {
editor_mode: EditorMode,
input_text: &str,
editor_width: Pixels,
show_line_numbers: bool,
) -> Vec<Invisible> {
info!(
"Creating editor with mode {editor_mode:?}, width {}px and text '{input_text}'",
@@ -6797,11 +6834,13 @@ mod tests {
});
let cx = &mut VisualTestContext::from_window(*window, cx);
let editor = window.root(cx).unwrap();
let style = cx.update(|cx| editor.read(cx).style().unwrap().clone());
window
.update(cx, |editor, cx| {
editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
editor.set_wrap_width(Some(editor_width), cx);
editor.set_show_line_numbers(show_line_numbers, cx);
})
.unwrap();
let (_, state) = cx.draw(point(px(500.), px(500.)), size(px(500.), px(500.)), |_| {

View File

@@ -1280,7 +1280,7 @@ impl SearchableItem for Editor {
matches: &[Range<Anchor>],
cx: &mut ViewContext<Self>,
) {
self.unfold_ranges([matches[index].clone()], false, true, cx);
self.unfold_ranges(&[matches[index].clone()], false, true, cx);
let range = self.range_for_match(&matches[index]);
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges([range]);
@@ -1288,7 +1288,7 @@ impl SearchableItem for Editor {
}
fn select_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
self.unfold_ranges(matches.to_vec(), false, false, cx);
self.unfold_ranges(matches, false, false, cx);
let mut ranges = Vec::new();
for m in matches {
ranges.push(self.range_for_match(m))

View File

@@ -365,12 +365,15 @@ impl ExtensionBuilder {
let output = Command::new("rustup")
.args(["target", "add", RUST_TARGET])
.stderr(Stdio::inherit())
.stderr(Stdio::piped())
.stdout(Stdio::inherit())
.output()
.context("failed to run `rustup target add`")?;
if !output.status.success() {
bail!("failed to install the `{RUST_TARGET}` target");
bail!(
"failed to install the `{RUST_TARGET}` target: {}",
String::from_utf8_lossy(&rustc_output.stderr)
);
}
Ok(())

View File

@@ -75,6 +75,8 @@ pub struct ExtensionManifest {
#[serde(default)]
pub language_servers: BTreeMap<LanguageServerName, LanguageServerManifestEntry>,
#[serde(default)]
pub context_servers: BTreeMap<Arc<str>, ContextServerManifestEntry>,
#[serde(default)]
pub slash_commands: BTreeMap<Arc<str>, SlashCommandManifestEntry>,
#[serde(default)]
pub indexed_docs_providers: BTreeMap<Arc<str>, IndexedDocsProviderEntry>,
@@ -134,6 +136,9 @@ impl LanguageServerManifestEntry {
}
}
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
pub struct ContextServerManifestEntry {}
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
pub struct SlashCommandManifestEntry {
pub description: String,
@@ -205,6 +210,7 @@ fn manifest_from_old_manifest(
.map(|grammar_name| (grammar_name, Default::default()))
.collect(),
language_servers: Default::default(),
context_servers: BTreeMap::default(),
slash_commands: BTreeMap::default(),
indexed_docs_providers: BTreeMap::default(),
snippets: None,

View File

@@ -129,6 +129,11 @@ pub trait Extension: Send + Sync {
Err("`run_slash_command` not implemented".to_string())
}
/// Returns the command used to start a context server.
fn context_server_command(&mut self, _context_server_id: &ContextServerId) -> Result<Command> {
Err("`context_server_command` not implemented".to_string())
}
/// Returns a list of package names as suggestions to be included in the
/// search results of the `/docs` slash command.
///
@@ -270,6 +275,11 @@ impl wit::Guest for Component {
extension().run_slash_command(command, args, worktree)
}
fn context_server_command(context_server_id: String) -> Result<wit::Command> {
let context_server_id = ContextServerId(context_server_id);
extension().context_server_command(&context_server_id)
}
fn suggest_docs_packages(provider: String) -> Result<Vec<String>, String> {
extension().suggest_docs_packages(provider)
}
@@ -299,6 +309,22 @@ impl fmt::Display for LanguageServerId {
}
}
/// The ID of a context server.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
pub struct ContextServerId(String);
impl AsRef<str> for ContextServerId {
fn as_ref(&self) -> &str {
&self.0
}
}
impl fmt::Display for ContextServerId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl CodeLabelSpan {
/// Returns a [`CodeLabelSpan::CodeRange`].
pub fn code_range(range: impl Into<wit::Range>) -> Self {

View File

@@ -135,6 +135,9 @@ world extension {
/// Returns the output from running the provided slash command.
export run-slash-command: func(command: slash-command, args: list<string>, worktree: option<borrow<worktree>>) -> result<slash-command-output, string>;
/// Returns the command used to start up a context server.
export context-server-command: func(context-server-id: string) -> result<command, string>;
/// Returns a list of packages as suggestions to be included in the `/docs`
/// search results.
///

View File

@@ -12,9 +12,11 @@ workspace = true
path = "src/extension_host.rs"
doctest = false
[features]
test-support = []
[dependencies]
anyhow.workspace = true
assistant_slash_command.workspace = true
async-compression.workspace = true
async-tar.workspace = true
async-trait.workspace = true
@@ -25,7 +27,6 @@ fs.workspace = true
futures.workspace = true
gpui.workspace = true
http_client.workspace = true
indexed_docs.workspace = true
language.workspace = true
log.workspace = true
lsp.workspace = true
@@ -39,16 +40,13 @@ serde.workspace = true
serde_json.workspace = true
serde_json_lenient.workspace = true
settings.workspace = true
snippet_provider.workspace = true
task.workspace = true
theme.workspace = true
toml.workspace = true
ui.workspace = true
url.workspace = true
util.workspace = true
wasmparser.workspace = true
wasmtime-wasi.workspace = true
wasmtime.workspace = true
workspace.workspace = true
[dev-dependencies]
ctor.workspace = true
@@ -59,4 +57,3 @@ language = { workspace = true, features = ["test-support"] }
parking_lot.workspace = true
project = { workspace = true, features = ["test-support"] }
reqwest_client.workspace = true
workspace = { workspace = true, features = ["test-support"] }

View File

@@ -1,23 +1,15 @@
mod extension_indexed_docs_provider;
mod extension_lsp_adapter;
mod extension_settings;
mod extension_slash_command;
mod wasm_host;
pub mod extension_lsp_adapter;
pub mod extension_settings;
pub mod wasm_host;
#[cfg(test)]
mod extension_store_test;
use crate::extension_indexed_docs_provider::ExtensionIndexedDocsProvider;
use crate::extension_slash_command::ExtensionSlashCommand;
use crate::{extension_lsp_adapter::ExtensionLspAdapter, wasm_host::wit};
use anyhow::{anyhow, bail, Context as _, Result};
use assistant_slash_command::SlashCommandRegistry;
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use client::{telemetry::Telemetry, Client, ExtensionMetadata, GetExtensionsResponse};
use collections::{btree_map, BTreeMap, HashSet};
use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder};
use extension::SchemaVersion;
pub use extension::ExtensionManifest;
use fs::{Fs, RemoveOptions};
use futures::{
channel::{
@@ -28,14 +20,13 @@ use futures::{
select_biased, AsyncReadExt as _, Future, FutureExt as _, StreamExt as _,
};
use gpui::{
actions, AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, Task,
WeakModel,
actions, AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext,
SharedString, Task, WeakModel,
};
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
use indexed_docs::{IndexedDocsRegistry, ProviderId};
use language::{
LanguageConfig, LanguageMatcher, LanguageName, LanguageQueries, LanguageRegistry,
LoadedLanguage, QUERY_FILENAME_PREFIXES,
LanguageConfig, LanguageMatcher, LanguageName, LanguageQueries, LoadedLanguage,
QUERY_FILENAME_PREFIXES,
};
use node_runtime::NodeRuntime;
use project::ContextProviderWithTasks;
@@ -43,7 +34,6 @@ use release_channel::ReleaseChannel;
use semantic_version::SemanticVersion;
use serde::{Deserialize, Serialize};
use settings::Settings;
use snippet_provider::SnippetRegistry;
use std::ops::RangeInclusive;
use std::str::FromStr;
use std::{
@@ -52,20 +42,19 @@ use std::{
sync::Arc,
time::{Duration, Instant},
};
use theme::{ThemeRegistry, ThemeSettings};
use url::Url;
use util::{maybe, ResultExt};
use util::ResultExt;
use wasm_host::{
wit::{is_supported_wasm_api_version, wasm_api_version_range},
WasmExtension, WasmHost,
};
pub use extension::{
ExtensionLibraryKind, ExtensionManifest, GrammarManifestEntry, OldExtensionManifest,
ExtensionLibraryKind, GrammarManifestEntry, OldExtensionManifest, SchemaVersion,
};
pub use extension_settings::ExtensionSettings;
const RELOAD_DEBOUNCE_DURATION: Duration = Duration::from_millis(200);
pub const RELOAD_DEBOUNCE_DURATION: Duration = Duration::from_millis(200);
const FS_WATCH_LATENCY: Duration = Duration::from_millis(100);
/// The current extension [`SchemaVersion`] supported by Zed.
@@ -100,26 +89,106 @@ pub fn is_version_compatible(
true
}
pub trait DocsDatabase: Send + Sync + 'static {
fn insert(&self, key: String, docs: String) -> Task<Result<()>>;
}
pub trait ExtensionRegistrationHooks: Send + Sync + 'static {
fn remove_user_themes(&self, _themes: Vec<SharedString>) {}
fn load_user_theme(&self, _theme_path: PathBuf, _fs: Arc<dyn Fs>) -> Task<Result<()>> {
Task::ready(Ok(()))
}
fn list_theme_names(
&self,
_theme_path: PathBuf,
_fs: Arc<dyn Fs>,
) -> Task<Result<Vec<String>>> {
Task::ready(Ok(Vec::new()))
}
fn reload_current_theme(&self, _cx: &mut AppContext) {}
fn register_language(
&self,
_language: LanguageName,
_grammar: Option<Arc<str>>,
_matcher: language::LanguageMatcher,
_load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>,
) {
}
fn register_lsp_adapter(&self, _language: LanguageName, _adapter: ExtensionLspAdapter) {}
fn remove_lsp_adapter(
&self,
_language: &LanguageName,
_server_name: &language::LanguageServerName,
) {
}
fn register_wasm_grammars(&self, _grammars: Vec<(Arc<str>, PathBuf)>) {}
fn remove_languages(
&self,
_languages_to_remove: &[LanguageName],
_grammars_to_remove: &[Arc<str>],
) {
}
fn register_slash_command(
&self,
_slash_command: wit::SlashCommand,
_extension: WasmExtension,
_host: Arc<WasmHost>,
) {
}
fn register_context_server(
&self,
_id: Arc<str>,
_extension: WasmExtension,
_host: Arc<WasmHost>,
) {
}
fn register_docs_provider(
&self,
_extension: WasmExtension,
_host: Arc<WasmHost>,
_provider_id: Arc<str>,
) {
}
fn register_snippets(&self, _path: &PathBuf, _snippet_contents: &str) -> Result<()> {
Ok(())
}
fn update_lsp_status(
&self,
_server_name: language::LanguageServerName,
_status: language::LanguageServerBinaryStatus,
) {
}
}
pub struct ExtensionStore {
builder: Arc<ExtensionBuilder>,
extension_index: ExtensionIndex,
fs: Arc<dyn Fs>,
http_client: Arc<HttpClientWithUrl>,
telemetry: Option<Arc<Telemetry>>,
reload_tx: UnboundedSender<Option<Arc<str>>>,
reload_complete_senders: Vec<oneshot::Sender<()>>,
installed_dir: PathBuf,
outstanding_operations: BTreeMap<Arc<str>, ExtensionOperation>,
index_path: PathBuf,
language_registry: Arc<LanguageRegistry>,
theme_registry: Arc<ThemeRegistry>,
slash_command_registry: Arc<SlashCommandRegistry>,
indexed_docs_registry: Arc<IndexedDocsRegistry>,
snippet_registry: Arc<SnippetRegistry>,
modified_extensions: HashSet<Arc<str>>,
wasm_host: Arc<WasmHost>,
wasm_extensions: Vec<(Arc<ExtensionManifest>, WasmExtension)>,
tasks: Vec<Task<()>>,
pub registration_hooks: Arc<dyn ExtensionRegistrationHooks>,
pub builder: Arc<ExtensionBuilder>,
pub extension_index: ExtensionIndex,
pub fs: Arc<dyn Fs>,
pub http_client: Arc<HttpClientWithUrl>,
pub telemetry: Option<Arc<Telemetry>>,
pub reload_tx: UnboundedSender<Option<Arc<str>>>,
pub reload_complete_senders: Vec<oneshot::Sender<()>>,
pub installed_dir: PathBuf,
pub outstanding_operations: BTreeMap<Arc<str>, ExtensionOperation>,
pub index_path: PathBuf,
pub modified_extensions: HashSet<Arc<str>>,
pub wasm_host: Arc<WasmHost>,
pub wasm_extensions: Vec<(Arc<ExtensionManifest>, WasmExtension)>,
pub tasks: Vec<Task<()>>,
}
#[derive(Clone, Copy)]
@@ -158,26 +227,25 @@ pub struct ExtensionIndexEntry {
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)]
pub struct ExtensionIndexThemeEntry {
extension: Arc<str>,
path: PathBuf,
pub extension: Arc<str>,
pub path: PathBuf,
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)]
pub struct ExtensionIndexLanguageEntry {
extension: Arc<str>,
path: PathBuf,
matcher: LanguageMatcher,
grammar: Option<Arc<str>>,
pub extension: Arc<str>,
pub path: PathBuf,
pub matcher: LanguageMatcher,
pub grammar: Option<Arc<str>>,
}
actions!(zed, [ReloadExtensions]);
pub fn init(
registration_hooks: Arc<dyn ExtensionRegistrationHooks>,
fs: Arc<dyn Fs>,
client: Arc<Client>,
node_runtime: NodeRuntime,
language_registry: Arc<LanguageRegistry>,
theme_registry: Arc<ThemeRegistry>,
cx: &mut AppContext,
) {
ExtensionSettings::register(cx);
@@ -186,16 +254,12 @@ pub fn init(
ExtensionStore::new(
paths::extensions_dir().clone(),
None,
registration_hooks,
fs,
client.http_client().clone(),
client.http_client().clone(),
Some(client.telemetry().clone()),
node_runtime,
language_registry,
theme_registry,
SlashCommandRegistry::global(cx),
IndexedDocsRegistry::global(cx),
SnippetRegistry::global(cx),
cx,
)
});
@@ -222,16 +286,12 @@ impl ExtensionStore {
pub fn new(
extensions_dir: PathBuf,
build_dir: Option<PathBuf>,
extension_api: Arc<dyn ExtensionRegistrationHooks>,
fs: Arc<dyn Fs>,
http_client: Arc<HttpClientWithUrl>,
builder_client: Arc<dyn HttpClient>,
telemetry: Option<Arc<Telemetry>>,
node_runtime: NodeRuntime,
language_registry: Arc<LanguageRegistry>,
theme_registry: Arc<ThemeRegistry>,
slash_command_registry: Arc<SlashCommandRegistry>,
indexed_docs_registry: Arc<IndexedDocsRegistry>,
snippet_registry: Arc<SnippetRegistry>,
cx: &mut ModelContext<Self>,
) -> Self {
let work_dir = extensions_dir.join("work");
@@ -241,6 +301,7 @@ impl ExtensionStore {
let (reload_tx, mut reload_rx) = unbounded();
let mut this = Self {
registration_hooks: extension_api.clone(),
extension_index: Default::default(),
installed_dir,
index_path,
@@ -252,7 +313,7 @@ impl ExtensionStore {
fs.clone(),
http_client.clone(),
node_runtime,
language_registry.clone(),
extension_api,
work_dir,
cx,
),
@@ -260,11 +321,6 @@ impl ExtensionStore {
fs,
http_client,
telemetry,
language_registry,
theme_registry,
slash_command_registry,
indexed_docs_registry,
snippet_registry,
reload_tx,
tasks: Vec::new(),
};
@@ -325,6 +381,7 @@ impl ExtensionStore {
async move {
load_initial_extensions.await;
let mut index_changed = false;
let mut debounce_timer = cx
.background_executor()
.spawn(futures::future::pending())
@@ -332,17 +389,21 @@ impl ExtensionStore {
loop {
select_biased! {
_ = debounce_timer => {
let index = this
.update(&mut cx, |this, cx| this.rebuild_extension_index(cx))?
.await;
this.update(&mut cx, |this, cx| this.extensions_updated(index, cx))?
.await;
if index_changed {
let index = this
.update(&mut cx, |this, cx| this.rebuild_extension_index(cx))?
.await;
this.update(&mut cx, |this, cx| this.extensions_updated(index, cx))?
.await;
index_changed = false;
}
}
extension_id = reload_rx.next() => {
let Some(extension_id) = extension_id else { break; };
this.update(&mut cx, |this, _| {
this.modified_extensions.extend(extension_id);
})?;
index_changed = true;
debounce_timer = cx
.background_executor()
.timer(RELOAD_DEBOUNCE_DURATION)
@@ -386,7 +447,7 @@ impl ExtensionStore {
this
}
fn reload(
pub fn reload(
&mut self,
modified_extension: Option<Arc<str>>,
cx: &mut ModelContext<Self>,
@@ -1039,7 +1100,7 @@ impl ExtensionStore {
grammars_to_remove.extend(extension.manifest.grammars.keys().cloned());
for (language_server_name, config) in extension.manifest.language_servers.iter() {
for language in config.languages() {
self.language_registry
self.registration_hooks
.remove_lsp_adapter(&language, language_server_name);
}
}
@@ -1047,8 +1108,8 @@ impl ExtensionStore {
self.wasm_extensions
.retain(|(extension, _)| !extensions_to_unload.contains(&extension.id));
self.theme_registry.remove_user_themes(&themes_to_remove);
self.language_registry
self.registration_hooks.remove_user_themes(themes_to_remove);
self.registration_hooks
.remove_languages(&languages_to_remove, &grammars_to_remove);
let languages_to_add = new_index
@@ -1083,7 +1144,7 @@ impl ExtensionStore {
}));
}
self.language_registry
self.registration_hooks
.register_wasm_grammars(grammars_to_add);
for (language_name, language) in languages_to_add {
@@ -1092,11 +1153,11 @@ impl ExtensionStore {
Path::new(language.extension.as_ref()),
language.path.as_path(),
]);
self.language_registry.register_language(
self.registration_hooks.register_language(
language_name.clone(),
language.grammar.clone(),
language.matcher.clone(),
move || {
Arc::new(move || {
let config = std::fs::read_to_string(language_path.join("config.toml"))?;
let config: LanguageConfig = ::toml::from_str(&config)?;
let queries = load_plugin_queries(&language_path);
@@ -1115,15 +1176,14 @@ impl ExtensionStore {
context_provider,
toolchain_provider: None,
})
},
}),
);
}
let fs = self.fs.clone();
let wasm_host = self.wasm_host.clone();
let root_dir = self.installed_dir.clone();
let theme_registry = self.theme_registry.clone();
let snippet_registry = self.snippet_registry.clone();
let api = self.registration_hooks.clone();
let extension_entries = extensions_to_load
.iter()
.filter_map(|name| new_index.extensions.get(name).cloned())
@@ -1138,18 +1198,14 @@ impl ExtensionStore {
.spawn({
let fs = fs.clone();
async move {
for theme_path in &themes_to_add {
theme_registry
.load_user_theme(theme_path, fs.clone())
.await
.log_err();
for theme_path in themes_to_add.into_iter() {
api.load_user_theme(theme_path, fs.clone()).await.log_err();
}
for snippets_path in &snippets_to_add {
if let Some(snippets_contents) = fs.load(snippets_path).await.log_err()
{
snippet_registry
.register_snippets(snippets_path, &snippets_contents)
api.register_snippets(snippets_path, &snippets_contents)
.log_err();
}
}
@@ -1163,30 +1219,13 @@ impl ExtensionStore {
continue;
};
let wasm_extension = maybe!(async {
let mut path = root_dir.clone();
path.extend([extension.manifest.clone().id.as_ref(), "extension.wasm"]);
let mut wasm_file = fs
.open_sync(&path)
.await
.context("failed to open wasm file")?;
let mut wasm_bytes = Vec::new();
wasm_file
.read_to_end(&mut wasm_bytes)
.context("failed to read wasm")?;
wasm_host
.load_extension(
wasm_bytes,
extension.manifest.clone().clone(),
cx.background_executor().clone(),
)
.await
.with_context(|| {
format!("failed to load wasm extension {}", extension.manifest.id)
})
})
let extension_path = root_dir.join(extension.manifest.id.as_ref());
let wasm_extension = WasmExtension::load(
extension_path,
&extension.manifest,
wasm_host.clone(),
&cx,
)
.await;
if let Some(wasm_extension) = wasm_extension.log_err() {
@@ -1205,9 +1244,9 @@ impl ExtensionStore {
for (manifest, wasm_extension) in &wasm_extensions {
for (language_server_id, language_server_config) in &manifest.language_servers {
for language in language_server_config.languages() {
this.language_registry.register_lsp_adapter(
this.registration_hooks.register_lsp_adapter(
language.clone(),
Arc::new(ExtensionLspAdapter {
ExtensionLspAdapter {
extension: wasm_extension.clone(),
host: this.wasm_host.clone(),
language_server_id: language_server_id.clone(),
@@ -1215,43 +1254,46 @@ impl ExtensionStore {
name: language_server_id.0.to_string(),
language_name: language.to_string(),
},
}),
},
);
}
}
for (slash_command_name, slash_command) in &manifest.slash_commands {
this.slash_command_registry.register_command(
ExtensionSlashCommand {
command: crate::wit::SlashCommand {
name: slash_command_name.to_string(),
description: slash_command.description.to_string(),
// We don't currently expose this as a configurable option, as it currently drives
// the `menu_text` on the `SlashCommand` trait, which is not used for slash commands
// defined in extensions, as they are not able to be added to the menu.
tooltip_text: String::new(),
requires_argument: slash_command.requires_argument,
},
extension: wasm_extension.clone(),
host: this.wasm_host.clone(),
this.registration_hooks.register_slash_command(
crate::wit::SlashCommand {
name: slash_command_name.to_string(),
description: slash_command.description.to_string(),
// We don't currently expose this as a configurable option, as it currently drives
// the `menu_text` on the `SlashCommand` trait, which is not used for slash commands
// defined in extensions, as they are not able to be added to the menu.
tooltip_text: String::new(),
requires_argument: slash_command.requires_argument,
},
false,
wasm_extension.clone(),
this.wasm_host.clone(),
);
}
for (id, _context_server_entry) in &manifest.context_servers {
this.registration_hooks.register_context_server(
id.clone(),
wasm_extension.clone(),
this.wasm_host.clone(),
);
}
for (provider_id, _provider) in &manifest.indexed_docs_providers {
this.indexed_docs_registry.register_provider(Box::new(
ExtensionIndexedDocsProvider {
extension: wasm_extension.clone(),
host: this.wasm_host.clone(),
id: ProviderId(provider_id.clone()),
},
));
this.registration_hooks.register_docs_provider(
wasm_extension.clone(),
this.wasm_host.clone(),
provider_id.clone(),
);
}
}
this.wasm_extensions.extend(wasm_extensions);
ThemeSettings::reload_current_theme(cx)
this.registration_hooks.reload_current_theme(cx);
})
.ok();
})
@@ -1262,6 +1304,7 @@ impl ExtensionStore {
let work_dir = self.wasm_host.work_dir.clone();
let extensions_dir = self.installed_dir.clone();
let index_path = self.index_path.clone();
let extension_api = self.registration_hooks.clone();
cx.background_executor().spawn(async move {
let start_time = Instant::now();
let mut index = ExtensionIndex::default();
@@ -1283,9 +1326,14 @@ impl ExtensionStore {
continue;
}
Self::add_extension_to_index(fs.clone(), extension_dir, &mut index)
.await
.log_err();
Self::add_extension_to_index(
fs.clone(),
extension_dir,
&mut index,
extension_api.clone(),
)
.await
.log_err();
}
}
@@ -1305,6 +1353,7 @@ impl ExtensionStore {
fs: Arc<dyn Fs>,
extension_dir: PathBuf,
index: &mut ExtensionIndex,
extension_api: Arc<dyn ExtensionRegistrationHooks>,
) -> Result<()> {
let mut extension_manifest = ExtensionManifest::load(fs.clone(), &extension_dir).await?;
let extension_id = extension_manifest.id.clone();
@@ -1356,7 +1405,8 @@ impl ExtensionStore {
continue;
};
let Some(theme_family) = theme::read_user_theme(&theme_path, fs.clone())
let Some(theme_families) = extension_api
.list_theme_names(theme_path.clone(), fs.clone())
.await
.log_err()
else {
@@ -1368,9 +1418,9 @@ impl ExtensionStore {
extension_manifest.themes.push(relative_path.clone());
}
for theme in theme_family.themes {
for theme_name in theme_families {
index.themes.insert(
theme.name.into(),
theme_name.into(),
ExtensionIndexThemeEntry {
extension: extension_id.clone(),
path: relative_path.clone(),

View File

@@ -1,7 +1,7 @@
pub(crate) mod wit;
pub mod wit;
use crate::ExtensionManifest;
use anyhow::{anyhow, Context as _, Result};
use crate::{ExtensionManifest, ExtensionRegistrationHooks};
use anyhow::{anyhow, bail, Context as _, Result};
use fs::{normalize_path, Fs};
use futures::future::LocalBoxFuture;
use futures::{
@@ -14,7 +14,6 @@ use futures::{
};
use gpui::{AppContext, AsyncAppContext, BackgroundExecutor, Task};
use http_client::HttpClient;
use language::LanguageRegistry;
use node_runtime::NodeRuntime;
use release_channel::ReleaseChannel;
use semantic_version::SemanticVersion;
@@ -28,15 +27,16 @@ use wasmtime::{
};
use wasmtime_wasi as wasi;
use wit::Extension;
pub use wit::SlashCommand;
pub(crate) struct WasmHost {
pub struct WasmHost {
engine: Engine,
release_channel: ReleaseChannel,
http_client: Arc<dyn HttpClient>,
node_runtime: NodeRuntime,
pub(crate) language_registry: Arc<LanguageRegistry>,
pub registration_hooks: Arc<dyn ExtensionRegistrationHooks>,
fs: Arc<dyn Fs>,
pub(crate) work_dir: PathBuf,
pub work_dir: PathBuf,
_main_thread_message_task: Task<()>,
main_thread_message_tx: mpsc::UnboundedSender<MainThreadCall>,
}
@@ -44,16 +44,16 @@ pub(crate) struct WasmHost {
#[derive(Clone)]
pub struct WasmExtension {
tx: UnboundedSender<ExtensionCall>,
pub(crate) manifest: Arc<ExtensionManifest>,
pub manifest: Arc<ExtensionManifest>,
#[allow(unused)]
pub zed_api_version: SemanticVersion,
}
pub(crate) struct WasmState {
pub struct WasmState {
manifest: Arc<ExtensionManifest>,
pub(crate) table: ResourceTable,
pub table: ResourceTable,
ctx: wasi::WasiCtx,
pub(crate) host: Arc<WasmHost>,
pub host: Arc<WasmHost>,
}
type MainThreadCall =
@@ -81,7 +81,7 @@ impl WasmHost {
fs: Arc<dyn Fs>,
http_client: Arc<dyn HttpClient>,
node_runtime: NodeRuntime,
language_registry: Arc<LanguageRegistry>,
registration_hooks: Arc<dyn ExtensionRegistrationHooks>,
work_dir: PathBuf,
cx: &mut AppContext,
) -> Arc<Self> {
@@ -97,7 +97,7 @@ impl WasmHost {
work_dir,
http_client,
node_runtime,
language_registry,
registration_hooks,
release_channel: ReleaseChannel::global(cx),
_main_thread_message_task: task,
main_thread_message_tx: tx,
@@ -107,13 +107,13 @@ impl WasmHost {
pub fn load_extension(
self: &Arc<Self>,
wasm_bytes: Vec<u8>,
manifest: Arc<ExtensionManifest>,
manifest: &Arc<ExtensionManifest>,
executor: BackgroundExecutor,
) -> Task<Result<WasmExtension>> {
let this = self.clone();
let manifest = manifest.clone();
executor.clone().spawn(async move {
let zed_api_version =
extension::parse_wasm_extension_version(&manifest.id, &wasm_bytes)?;
let zed_api_version = parse_wasm_extension_version(&manifest.id, &wasm_bytes)?;
let component = Component::from_binary(&this.engine, &wasm_bytes)
.context("failed to compile wasm component")?;
@@ -151,7 +151,7 @@ impl WasmHost {
.detach();
Ok(WasmExtension {
manifest,
manifest: manifest.clone(),
tx,
zed_api_version,
})
@@ -198,7 +198,75 @@ impl WasmHost {
}
}
pub fn parse_wasm_extension_version(
extension_id: &str,
wasm_bytes: &[u8],
) -> Result<SemanticVersion> {
let mut version = None;
for part in wasmparser::Parser::new(0).parse_all(wasm_bytes) {
if let wasmparser::Payload::CustomSection(s) =
part.context("error parsing wasm extension")?
{
if s.name() == "zed:api-version" {
version = parse_wasm_extension_version_custom_section(s.data());
if version.is_none() {
bail!(
"extension {} has invalid zed:api-version section: {:?}",
extension_id,
s.data()
);
}
}
}
}
// The reason we wait until we're done parsing all of the Wasm bytes to return the version
// is to work around a panic that can happen inside of Wasmtime when the bytes are invalid.
//
// By parsing the entirety of the Wasm bytes before we return, we're able to detect this problem
// earlier as an `Err` rather than as a panic.
version.ok_or_else(|| anyhow!("extension {} has no zed:api-version section", extension_id))
}
fn parse_wasm_extension_version_custom_section(data: &[u8]) -> Option<SemanticVersion> {
if data.len() == 6 {
Some(SemanticVersion::new(
u16::from_be_bytes([data[0], data[1]]) as _,
u16::from_be_bytes([data[2], data[3]]) as _,
u16::from_be_bytes([data[4], data[5]]) as _,
))
} else {
None
}
}
impl WasmExtension {
pub async fn load(
extension_dir: PathBuf,
manifest: &Arc<ExtensionManifest>,
wasm_host: Arc<WasmHost>,
cx: &AsyncAppContext,
) -> Result<Self> {
let path = extension_dir.join("extension.wasm");
let mut wasm_file = wasm_host
.fs
.open_sync(&path)
.await
.context("failed to open wasm file")?;
let mut wasm_bytes = Vec::new();
wasm_file
.read_to_end(&mut wasm_bytes)
.context("failed to read wasm")?;
wasm_host
.load_extension(wasm_bytes, manifest, cx.background_executor().clone())
.await
.with_context(|| format!("failed to load wasm extension {}", manifest.id))
}
pub async fn call<T, Fn>(&self, f: Fn) -> T
where
T: 'static + Send,

View File

@@ -3,10 +3,12 @@ mod since_v0_0_4;
mod since_v0_0_6;
mod since_v0_1_0;
mod since_v0_2_0;
use indexed_docs::IndexedDocsDatabase;
// use indexed_docs::IndexedDocsDatabase;
use release_channel::ReleaseChannel;
use since_v0_2_0 as latest;
use crate::DocsDatabase;
use super::{wasm_engine, WasmState};
use anyhow::{anyhow, Context, Result};
use language::{LanguageServerName, LspAdapterDelegate};
@@ -78,13 +80,20 @@ impl Extension {
version: SemanticVersion,
component: &Component,
) -> Result<Self> {
// Note: The release channel can be used to stage a new version of the extension API.
let allow_latest_version = match release_channel {
ReleaseChannel::Dev | ReleaseChannel::Nightly => true,
ReleaseChannel::Stable | ReleaseChannel::Preview => false,
};
if allow_latest_version && version >= latest::MIN_VERSION {
if version >= latest::MIN_VERSION {
// Note: The release channel can be used to stage a new version of the extension API.
// We always allow the latest in tests so that the extension tests pass on release branches.
let allow_latest_version = match release_channel {
ReleaseChannel::Dev | ReleaseChannel::Nightly => true,
ReleaseChannel::Stable | ReleaseChannel::Preview => {
cfg!(any(test, feature = "test-support"))
}
};
if !allow_latest_version {
Err(anyhow!(
"unreleased versions of the extension API can only be used on development builds of Zed"
))?;
}
let extension =
latest::Extension::instantiate_async(store, component, latest::linker())
.await
@@ -375,6 +384,24 @@ impl Extension {
}
}
pub async fn call_context_server_command(
&self,
store: &mut Store<WasmState>,
context_server_id: Arc<str>,
) -> Result<Result<Command, String>> {
match self {
Extension::V020(ext) => {
ext.call_context_server_command(store, &context_server_id)
.await
}
Extension::V001(_) | Extension::V004(_) | Extension::V006(_) | Extension::V010(_) => {
Err(anyhow!(
"`context_server_command` not available prior to v0.2.0"
))
}
}
}
pub async fn call_suggest_docs_packages(
&self,
store: &mut Store<WasmState>,
@@ -394,7 +421,7 @@ impl Extension {
store: &mut Store<WasmState>,
provider: &str,
package_name: &str,
database: Resource<Arc<IndexedDocsDatabase>>,
database: Resource<Arc<dyn DocsDatabase>>,
) -> Result<Result<(), String>> {
match self {
Extension::V020(ext) => {

View File

@@ -148,7 +148,7 @@ impl ExtensionImports for WasmState {
};
self.host
.language_registry
.registration_hooks
.update_lsp_status(language::LanguageServerName(server_name.into()), status);
Ok(())
}

View File

@@ -1,4 +1,5 @@
use crate::wasm_host::{wit::ToWasmtimeResult, WasmState};
use crate::DocsDatabase;
use ::http_client::{AsyncBody, HttpRequestExt};
use ::settings::{Settings, WorktreeId};
use anyhow::{anyhow, bail, Context, Result};
@@ -7,7 +8,6 @@ use async_tar::Archive;
use async_trait::async_trait;
use futures::{io::BufReader, FutureExt as _};
use futures::{lock::Mutex, AsyncReadExt};
use indexed_docs::IndexedDocsDatabase;
use language::{
language_settings::AllLanguageSettings, LanguageServerBinaryStatus, LspAdapterDelegate,
};
@@ -48,7 +48,7 @@ mod settings {
}
pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
pub type ExtensionKeyValueStore = Arc<IndexedDocsDatabase>;
pub type ExtensionKeyValueStore = Arc<dyn DocsDatabase>;
pub type ExtensionHttpResponseStream = Arc<Mutex<::http_client::Response<AsyncBody>>>;
pub fn linker() -> &'static Linker<WasmState> {
@@ -512,7 +512,7 @@ impl ExtensionImports for WasmState {
};
self.host
.language_registry
.registration_hooks
.update_lsp_status(language::LanguageServerName(server_name.into()), status);
Ok(())
}

View File

@@ -1,4 +1,5 @@
use crate::wasm_host::{wit::ToWasmtimeResult, WasmState};
use crate::DocsDatabase;
use ::http_client::{AsyncBody, HttpRequestExt};
use ::settings::{Settings, WorktreeId};
use anyhow::{anyhow, bail, Context, Result};
@@ -7,7 +8,6 @@ use async_tar::Archive;
use async_trait::async_trait;
use futures::{io::BufReader, FutureExt as _};
use futures::{lock::Mutex, AsyncReadExt};
use indexed_docs::IndexedDocsDatabase;
use language::{
language_settings::AllLanguageSettings, LanguageServerBinaryStatus, LspAdapterDelegate,
};
@@ -43,7 +43,7 @@ mod settings {
}
pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
pub type ExtensionKeyValueStore = Arc<IndexedDocsDatabase>;
pub type ExtensionKeyValueStore = Arc<dyn DocsDatabase>;
pub type ExtensionHttpResponseStream = Arc<Mutex<::http_client::Response<AsyncBody>>>;
pub fn linker() -> &'static Linker<WasmState> {
@@ -459,7 +459,7 @@ impl ExtensionImports for WasmState {
};
self.host
.language_registry
.registration_hooks
.update_lsp_status(language::LanguageServerName(server_name.into()), status);
Ok(())
}

View File

@@ -16,14 +16,19 @@ test-support = []
[dependencies]
anyhow.workspace = true
assistant_slash_command.workspace = true
async-trait.workspace = true
client.workspace = true
collections.workspace = true
context_servers.workspace = true
db.workspace = true
editor.workspace = true
extension_host.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
indexed_docs.workspace = true
language.workspace = true
num-format.workspace = true
picker.workspace = true
@@ -33,12 +38,31 @@ semantic_version.workspace = true
serde.workspace = true
settings.workspace = true
smallvec.workspace = true
snippet_provider.workspace = true
theme.workspace = true
theme_selector.workspace = true
ui.workspace = true
util.workspace = true
vim.workspace = true
wasmtime-wasi.workspace = true
workspace.workspace = true
[dev-dependencies]
async-compression.workspace = true
async-tar.workspace = true
ctor.workspace = true
editor = { workspace = true, features = ["test-support"] }
env_logger.workspace = true
extension_host = {workspace = true, features = ["test-support"] }
fs = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
http_client.workspace = true
indexed_docs.workspace = true
language = { workspace = true, features = ["test-support"] }
lsp.workspace = true
node_runtime.workspace = true
parking_lot.workspace = true
project = { workspace = true, features = ["test-support"] }
reqwest_client.workspace = true
serde_json.workspace = true
workspace = { workspace = true, features = ["test-support"] }

View File

@@ -0,0 +1,80 @@
use std::pin::Pin;
use std::sync::Arc;
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use context_servers::manager::{NativeContextServer, ServerConfig};
use context_servers::protocol::InitializedContextServerProtocol;
use context_servers::ContextServer;
use extension_host::wasm_host::{WasmExtension, WasmHost};
use futures::{Future, FutureExt};
use gpui::AsyncAppContext;
pub struct ExtensionContextServer {
#[allow(unused)]
pub(crate) extension: WasmExtension,
#[allow(unused)]
pub(crate) host: Arc<WasmHost>,
id: Arc<str>,
context_server: Arc<NativeContextServer>,
}
impl ExtensionContextServer {
pub async fn new(extension: WasmExtension, host: Arc<WasmHost>, id: Arc<str>) -> Result<Self> {
let command = extension
.call({
let id = id.clone();
|extension, store| {
async move {
let command = extension
.call_context_server_command(store, id.clone())
.await?
.map_err(|e| anyhow!("{}", e))?;
anyhow::Ok(command)
}
.boxed()
}
})
.await?;
let config = Arc::new(ServerConfig {
id: id.to_string(),
executable: command.command,
args: command.args,
env: Some(command.env.into_iter().collect()),
});
anyhow::Ok(Self {
extension,
host,
id,
context_server: Arc::new(NativeContextServer::new(config)),
})
}
}
#[async_trait(?Send)]
impl ContextServer for ExtensionContextServer {
fn id(&self) -> Arc<str> {
self.id.clone()
}
fn config(&self) -> Arc<ServerConfig> {
self.context_server.config()
}
fn client(&self) -> Option<Arc<InitializedContextServerProtocol>> {
self.context_server.client()
}
fn start<'a>(
self: Arc<Self>,
cx: &'a AsyncAppContext,
) -> Pin<Box<dyn 'a + Future<Output = Result<()>>>> {
self.context_server.clone().start(cx)
}
fn stop(&self) -> Result<()> {
self.context_server.stop()
}
}

View File

@@ -7,7 +7,7 @@ use futures::FutureExt;
use indexed_docs::{IndexedDocsDatabase, IndexedDocsProvider, PackageName, ProviderId};
use wasmtime_wasi::WasiView;
use crate::wasm_host::{WasmExtension, WasmHost};
use extension_host::wasm_host::{WasmExtension, WasmHost};
pub struct ExtensionIndexedDocsProvider {
pub(crate) extension: WasmExtension,
@@ -58,7 +58,7 @@ impl IndexedDocsProvider for ExtensionIndexedDocsProvider {
let id = self.id.clone();
|extension, store| {
async move {
let database_resource = store.data_mut().table().push(database)?;
let database_resource = store.data_mut().table().push(database as _)?;
extension
.call_index_docs(
store,

View File

@@ -0,0 +1,183 @@
use std::{path::PathBuf, sync::Arc};
use anyhow::Result;
use assistant_slash_command::SlashCommandRegistry;
use context_servers::ContextServerFactoryRegistry;
use extension_host::{extension_lsp_adapter::ExtensionLspAdapter, wasm_host};
use fs::Fs;
use gpui::{AppContext, BackgroundExecutor, Task};
use indexed_docs::{IndexedDocsRegistry, ProviderId};
use language::{LanguageRegistry, LanguageServerBinaryStatus, LoadedLanguage};
use snippet_provider::SnippetRegistry;
use theme::{ThemeRegistry, ThemeSettings};
use ui::SharedString;
use crate::extension_context_server::ExtensionContextServer;
use crate::{extension_indexed_docs_provider, extension_slash_command::ExtensionSlashCommand};
pub struct ConcreteExtensionRegistrationHooks {
slash_command_registry: Arc<SlashCommandRegistry>,
theme_registry: Arc<ThemeRegistry>,
indexed_docs_registry: Arc<IndexedDocsRegistry>,
snippet_registry: Arc<SnippetRegistry>,
language_registry: Arc<LanguageRegistry>,
context_server_factory_registry: Arc<ContextServerFactoryRegistry>,
executor: BackgroundExecutor,
}
impl ConcreteExtensionRegistrationHooks {
pub fn new(
theme_registry: Arc<ThemeRegistry>,
slash_command_registry: Arc<SlashCommandRegistry>,
indexed_docs_registry: Arc<IndexedDocsRegistry>,
snippet_registry: Arc<SnippetRegistry>,
language_registry: Arc<LanguageRegistry>,
context_server_factory_registry: Arc<ContextServerFactoryRegistry>,
cx: &AppContext,
) -> Arc<dyn extension_host::ExtensionRegistrationHooks> {
Arc::new(Self {
theme_registry,
slash_command_registry,
indexed_docs_registry,
snippet_registry,
language_registry,
context_server_factory_registry,
executor: cx.background_executor().clone(),
})
}
}
impl extension_host::ExtensionRegistrationHooks for ConcreteExtensionRegistrationHooks {
fn remove_user_themes(&self, themes: Vec<SharedString>) {
self.theme_registry.remove_user_themes(&themes);
}
fn load_user_theme(&self, theme_path: PathBuf, fs: Arc<dyn fs::Fs>) -> Task<Result<()>> {
let theme_registry = self.theme_registry.clone();
self.executor
.spawn(async move { theme_registry.load_user_theme(&theme_path, fs).await })
}
fn register_slash_command(
&self,
command: wasm_host::SlashCommand,
extension: wasm_host::WasmExtension,
host: Arc<wasm_host::WasmHost>,
) {
self.slash_command_registry.register_command(
ExtensionSlashCommand {
command,
extension,
host,
},
false,
)
}
fn register_context_server(
&self,
id: Arc<str>,
extension: wasm_host::WasmExtension,
host: Arc<wasm_host::WasmHost>,
) {
self.context_server_factory_registry
.register_server_factory(
id.clone(),
Arc::new({
move |cx| {
let id = id.clone();
let extension = extension.clone();
let host = host.clone();
cx.spawn(|_cx| async move {
let context_server =
ExtensionContextServer::new(extension, host, id).await?;
anyhow::Ok(Arc::new(context_server) as _)
})
}
}),
);
}
fn register_docs_provider(
&self,
extension: wasm_host::WasmExtension,
host: Arc<wasm_host::WasmHost>,
provider_id: Arc<str>,
) {
self.indexed_docs_registry.register_provider(Box::new(
extension_indexed_docs_provider::ExtensionIndexedDocsProvider {
extension,
host,
id: ProviderId(provider_id),
},
));
}
fn register_snippets(&self, path: &PathBuf, snippet_contents: &str) -> Result<()> {
self.snippet_registry
.register_snippets(path, snippet_contents)
}
fn update_lsp_status(
&self,
server_name: language::LanguageServerName,
status: LanguageServerBinaryStatus,
) {
self.language_registry
.update_lsp_status(server_name, status);
}
fn register_lsp_adapter(
&self,
language_name: language::LanguageName,
adapter: ExtensionLspAdapter,
) {
self.language_registry
.register_lsp_adapter(language_name, Arc::new(adapter));
}
fn remove_lsp_adapter(
&self,
language_name: &language::LanguageName,
server_name: &language::LanguageServerName,
) {
self.language_registry
.remove_lsp_adapter(language_name, server_name);
}
fn remove_languages(
&self,
languages_to_remove: &[language::LanguageName],
grammars_to_remove: &[Arc<str>],
) {
self.language_registry
.remove_languages(&languages_to_remove, &grammars_to_remove);
}
fn register_wasm_grammars(&self, grammars: Vec<(Arc<str>, PathBuf)>) {
self.language_registry.register_wasm_grammars(grammars)
}
fn register_language(
&self,
language: language::LanguageName,
grammar: Option<Arc<str>>,
matcher: language::LanguageMatcher,
load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>,
) {
self.language_registry
.register_language(language, grammar, matcher, load)
}
fn reload_current_theme(&self, cx: &mut AppContext) {
ThemeSettings::reload_current_theme(cx)
}
fn list_theme_names(&self, path: PathBuf, fs: Arc<dyn Fs>) -> Task<Result<Vec<String>>> {
self.executor.spawn(async move {
let themes = theme::read_user_theme(&path, fs).await?;
Ok(themes.themes.into_iter().map(|theme| theme.name).collect())
})
}
}

View File

@@ -5,20 +5,20 @@ use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
SlashCommandResult,
};
use futures::FutureExt;
use futures::FutureExt as _;
use gpui::{Task, WeakView, WindowContext};
use language::{BufferSnapshot, LspAdapterDelegate};
use ui::prelude::*;
use wasmtime_wasi::WasiView;
use workspace::Workspace;
use crate::wasm_host::{WasmExtension, WasmHost};
use extension_host::wasm_host::{WasmExtension, WasmHost};
pub struct ExtensionSlashCommand {
pub(crate) extension: WasmExtension,
#[allow(unused)]
pub(crate) host: Arc<WasmHost>,
pub(crate) command: crate::wit::SlashCommand,
pub(crate) command: extension_host::wasm_host::SlashCommand,
}
impl SlashCommand for ExtensionSlashCommand {

View File

@@ -1,13 +1,14 @@
use crate::extension_settings::ExtensionSettings;
use crate::{
use assistant_slash_command::SlashCommandRegistry;
use async_compression::futures::bufread::GzipEncoder;
use collections::BTreeMap;
use context_servers::ContextServerFactoryRegistry;
use extension_host::ExtensionSettings;
use extension_host::SchemaVersion;
use extension_host::{
Event, ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry,
ExtensionIndexThemeEntry, ExtensionManifest, ExtensionStore, GrammarManifestEntry,
RELOAD_DEBOUNCE_DURATION,
};
use assistant_slash_command::SlashCommandRegistry;
use async_compression::futures::bufread::GzipEncoder;
use collections::BTreeMap;
use extension::SchemaVersion;
use fs::{FakeFs, Fs, RealFs};
use futures::{io::BufReader, AsyncReadExt, StreamExt};
use gpui::{Context, SemanticVersion, TestAppContext};
@@ -161,6 +162,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
.into_iter()
.collect(),
language_servers: BTreeMap::default(),
context_servers: BTreeMap::default(),
slash_commands: BTreeMap::default(),
indexed_docs_providers: BTreeMap::default(),
snippets: None,
@@ -187,6 +189,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
languages: Default::default(),
grammars: BTreeMap::default(),
language_servers: BTreeMap::default(),
context_servers: BTreeMap::default(),
slash_commands: BTreeMap::default(),
indexed_docs_providers: BTreeMap::default(),
snippets: None,
@@ -264,27 +267,34 @@ async fn test_extension_store(cx: &mut TestAppContext) {
let slash_command_registry = SlashCommandRegistry::new();
let indexed_docs_registry = Arc::new(IndexedDocsRegistry::new(cx.executor()));
let snippet_registry = Arc::new(SnippetRegistry::new());
let context_server_factory_registry = ContextServerFactoryRegistry::new();
let node_runtime = NodeRuntime::unavailable();
let store = cx.new_model(|cx| {
let extension_registration_hooks = crate::ConcreteExtensionRegistrationHooks::new(
theme_registry.clone(),
slash_command_registry.clone(),
indexed_docs_registry.clone(),
snippet_registry.clone(),
language_registry.clone(),
context_server_factory_registry.clone(),
cx,
);
ExtensionStore::new(
PathBuf::from("/the-extension-dir"),
None,
extension_registration_hooks,
fs.clone(),
http_client.clone(),
http_client.clone(),
None,
node_runtime.clone(),
language_registry.clone(),
theme_registry.clone(),
slash_command_registry.clone(),
indexed_docs_registry.clone(),
snippet_registry.clone(),
cx,
)
});
cx.executor().advance_clock(super::RELOAD_DEBOUNCE_DURATION);
cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
store.read_with(cx, |store, _| {
let index = &store.extension_index;
assert_eq!(index.extensions, expected_index.extensions);
@@ -351,6 +361,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
languages: Default::default(),
grammars: BTreeMap::default(),
language_servers: BTreeMap::default(),
context_servers: BTreeMap::default(),
slash_commands: BTreeMap::default(),
indexed_docs_providers: BTreeMap::default(),
snippets: None,
@@ -395,19 +406,25 @@ async fn test_extension_store(cx: &mut TestAppContext) {
// Create new extension store, as if Zed were restarting.
drop(store);
let store = cx.new_model(|cx| {
let extension_api = crate::ConcreteExtensionRegistrationHooks::new(
theme_registry.clone(),
slash_command_registry,
indexed_docs_registry,
snippet_registry,
language_registry.clone(),
context_server_factory_registry.clone(),
cx,
);
ExtensionStore::new(
PathBuf::from("/the-extension-dir"),
None,
extension_api,
fs.clone(),
http_client.clone(),
http_client.clone(),
None,
node_runtime.clone(),
language_registry.clone(),
theme_registry.clone(),
slash_command_registry,
indexed_docs_registry,
snippet_registry,
cx,
)
});
@@ -490,6 +507,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
let slash_command_registry = SlashCommandRegistry::new();
let indexed_docs_registry = Arc::new(IndexedDocsRegistry::new(cx.executor()));
let snippet_registry = Arc::new(SnippetRegistry::new());
let context_server_factory_registry = ContextServerFactoryRegistry::new();
let node_runtime = NodeRuntime::unavailable();
let mut status_updates = language_registry.language_server_binary_statuses();
@@ -580,19 +598,24 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
Arc::new(ReqwestClient::user_agent(&user_agent).expect("Could not create HTTP client"));
let extension_store = cx.new_model(|cx| {
let extension_api = crate::ConcreteExtensionRegistrationHooks::new(
theme_registry.clone(),
slash_command_registry,
indexed_docs_registry,
snippet_registry,
language_registry.clone(),
context_server_factory_registry.clone(),
cx,
);
ExtensionStore::new(
extensions_dir.clone(),
Some(cache_dir),
extension_api,
fs.clone(),
extension_client.clone(),
builder_client,
None,
node_runtime,
language_registry.clone(),
theme_registry.clone(),
slash_command_registry,
indexed_docs_registry,
snippet_registry,
cx,
)
});
@@ -602,7 +625,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
let executor = cx.executor();
let _task = cx.executor().spawn(async move {
while let Some(event) = events.next().await {
if let crate::Event::StartedReloading = event {
if let extension_host::Event::StartedReloading = event {
executor.advance_clock(RELOAD_DEBOUNCE_DURATION);
}
}

View File

@@ -1,7 +1,16 @@
mod components;
mod extension_context_server;
mod extension_indexed_docs_provider;
mod extension_registration_hooks;
mod extension_slash_command;
mod extension_suggest;
mod extension_version_selector;
#[cfg(test)]
mod extension_store_test;
pub use extension_registration_hooks::ConcreteExtensionRegistrationHooks;
use std::ops::DerefMut;
use std::sync::OnceLock;
use std::time::Duration;

View File

@@ -10,7 +10,11 @@ use git::GitHostingProviderRegistry;
#[cfg(target_os = "linux")]
use ashpd::desktop::trash;
#[cfg(target_os = "linux")]
use std::{fs::File, os::fd::AsFd};
use std::fs::File;
#[cfg(unix)]
use std::os::fd::AsFd;
#[cfg(unix)]
use std::os::fd::AsRawFd;
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
@@ -51,14 +55,14 @@ pub trait Watcher: Send + Sync {
fn remove(&self, path: &Path) -> Result<()>;
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub enum PathEventKind {
Removed,
Created,
Changed,
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct PathEvent {
pub path: PathBuf,
pub kind: Option<PathEventKind>,
@@ -95,6 +99,7 @@ pub trait Fs: Send + Sync {
async fn trash_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
self.remove_file(path, options).await
}
async fn open_handle(&self, path: &Path) -> Result<Arc<dyn FileHandle>>;
async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>>;
async fn load(&self, path: &Path) -> Result<String> {
Ok(String::from_utf8(self.load_bytes(path).await?)?)
@@ -187,6 +192,52 @@ pub struct RealFs {
git_binary_path: Option<PathBuf>,
}
pub trait FileHandle: Send + Sync + std::fmt::Debug {
fn current_path(&self, fs: &Arc<dyn Fs>) -> Result<PathBuf>;
}
impl FileHandle for std::fs::File {
#[cfg(target_os = "macos")]
fn current_path(&self, _: &Arc<dyn Fs>) -> Result<PathBuf> {
use std::{
ffi::{CStr, OsStr},
os::unix::ffi::OsStrExt,
};
let fd = self.as_fd();
let mut path_buf: [libc::c_char; libc::PATH_MAX as usize] = [0; libc::PATH_MAX as usize];
let result = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_GETPATH, path_buf.as_mut_ptr()) };
if result == -1 {
anyhow::bail!("fcntl returned -1".to_string());
}
let c_str = unsafe { CStr::from_ptr(path_buf.as_ptr()) };
let path = PathBuf::from(OsStr::from_bytes(c_str.to_bytes()));
Ok(path)
}
#[cfg(target_os = "linux")]
fn current_path(&self, _: &Arc<dyn Fs>) -> Result<PathBuf> {
let fd = self.as_fd();
let fd_path = format!("/proc/self/fd/{}", fd.as_raw_fd());
let new_path = std::fs::read_link(fd_path)?;
if new_path
.file_name()
.is_some_and(|f| f.to_string_lossy().ends_with(" (deleted)"))
{
anyhow::bail!("file was deleted")
};
Ok(new_path)
}
#[cfg(target_os = "windows")]
fn current_path(&self, _: &Arc<dyn Fs>) -> Result<PathBuf> {
anyhow::bail!("unimplemented")
}
}
pub struct RealWatcher {}
impl RealFs {
@@ -400,6 +451,10 @@ impl Fs for RealFs {
Ok(Box::new(std::fs::File::open(path)?))
}
async fn open_handle(&self, path: &Path) -> Result<Arc<dyn FileHandle>> {
Ok(Arc::new(std::fs::File::open(path)?))
}
async fn load(&self, path: &Path) -> Result<String> {
let path = path.to_path_buf();
let text = smol::unblock(|| std::fs::read_to_string(path)).await?;
@@ -755,6 +810,7 @@ struct FakeFsState {
buffered_events: Vec<PathEvent>,
metadata_call_count: usize,
read_dir_call_count: usize,
moves: std::collections::HashMap<u64, PathBuf>,
}
#[cfg(any(test, feature = "test-support"))]
@@ -926,6 +982,7 @@ impl FakeFs {
events_paused: false,
read_dir_call_count: 0,
metadata_call_count: 0,
moves: Default::default(),
}),
});
@@ -1362,6 +1419,27 @@ impl Watcher for FakeWatcher {
}
}
#[cfg(any(test, feature = "test-support"))]
#[derive(Debug)]
struct FakeHandle {
inode: u64,
}
#[cfg(any(test, feature = "test-support"))]
impl FileHandle for FakeHandle {
fn current_path(&self, fs: &Arc<dyn Fs>) -> Result<PathBuf> {
let state = fs.as_fake().state.lock();
let Some(target) = state.moves.get(&self.inode) else {
anyhow::bail!("fake fd not moved")
};
if state.try_read_path(&target, false).is_some() {
return Ok(target.clone());
}
anyhow::bail!("fake fd target not found")
}
}
#[cfg(any(test, feature = "test-support"))]
#[async_trait::async_trait]
impl Fs for FakeFs {
@@ -1500,6 +1578,14 @@ impl Fs for FakeFs {
}
})?;
let inode = match *moved_entry.lock() {
FakeFsEntry::File { inode, .. } => inode,
FakeFsEntry::Dir { inode, .. } => inode,
_ => 0,
};
state.moves.insert(inode, new_path.clone());
state.write_path(&new_path, |e| {
match e {
btree_map::Entry::Occupied(mut e) => {
@@ -1644,6 +1730,19 @@ impl Fs for FakeFs {
Ok(Box::new(io::Cursor::new(bytes)))
}
async fn open_handle(&self, path: &Path) -> Result<Arc<dyn FileHandle>> {
self.simulate_random_delay().await;
let state = self.state.lock();
let entry = state.read_path(&path)?;
let entry = entry.lock();
let inode = match *entry {
FakeFsEntry::File { inode, .. } => inode,
FakeFsEntry::Dir { inode, .. } => inode,
_ => unreachable!(),
};
Ok(Arc::new(FakeHandle { inode }))
}
async fn load(&self, path: &Path) -> Result<String> {
let content = self.load_internal(path).await?;
Ok(String::from_utf8(content.clone())?)

View File

@@ -90,8 +90,8 @@ profiling.workspace = true
rand = { optional = true, workspace = true }
raw-window-handle = "0.6"
refineable.workspace = true
resvg = { version = "0.41.0", default-features = false }
usvg = { version = "0.41.0", default-features = false }
resvg = { version = "0.44.0", default-features = false }
usvg = { version = "0.44.0", default-features = false }
schemars.workspace = true
seahash = "4.1"
semantic_version.workspace = true

View File

@@ -546,7 +546,6 @@ impl InputExample {
impl Render for InputExample {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let num_keystrokes = self.recent_keystrokes.len();
div()
.bg(rgb(0xaaaaaa))
.track_focus(&self.focus_handle(cx))
@@ -561,7 +560,7 @@ impl Render for InputExample {
.flex()
.flex_row()
.justify_between()
.child(format!("Keystrokes: {}", num_keystrokes))
.child(format!("Keyboard {}", cx.keyboard_layout()))
.child(
div()
.border_1()
@@ -607,6 +606,7 @@ fn main() {
KeyBinding::new("end", End, None),
KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, None),
]);
let window = cx
.open_window(
WindowOptions {
@@ -642,6 +642,13 @@ fn main() {
.unwrap();
})
.detach();
cx.on_keyboard_layout_change({
move |cx| {
window.update(cx, |_, cx| cx.notify()).ok();
}
})
.detach();
window
.update(cx, |view, cx| {
cx.focus_view(&view.text_input);

View File

@@ -243,6 +243,7 @@ pub struct AppContext {
pub(crate) windows: SlotMap<WindowId, Option<Window>>,
pub(crate) window_handles: FxHashMap<WindowId, AnyWindowHandle>,
pub(crate) keymap: Rc<RefCell<Keymap>>,
pub(crate) keyboard_layout: SharedString,
pub(crate) global_action_listeners:
FxHashMap<TypeId, Vec<Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Self)>>>,
pending_effects: VecDeque<Effect>,
@@ -252,6 +253,7 @@ pub struct AppContext {
// TypeId is the type of the event that the listener callback expects
pub(crate) event_listeners: SubscriberSet<EntityId, (TypeId, Listener)>,
pub(crate) keystroke_observers: SubscriberSet<(), KeystrokeObserver>,
pub(crate) keyboard_layout_observers: SubscriberSet<(), Handler>,
pub(crate) release_listeners: SubscriberSet<EntityId, ReleaseListener>,
pub(crate) global_observers: SubscriberSet<TypeId, Handler>,
pub(crate) quit_observers: SubscriberSet<(), QuitHandler>,
@@ -279,6 +281,7 @@ impl AppContext {
let text_system = Arc::new(TextSystem::new(platform.text_system()));
let entities = EntityMap::new();
let keyboard_layout = SharedString::from(platform.keyboard_layout());
let app = Rc::new_cyclic(|this| AppCell {
app: RefCell::new(AppContext {
@@ -302,6 +305,7 @@ impl AppContext {
window_handles: FxHashMap::default(),
windows: SlotMap::with_key(),
keymap: Rc::new(RefCell::new(Keymap::default())),
keyboard_layout,
global_action_listeners: FxHashMap::default(),
pending_effects: VecDeque::new(),
pending_notifications: FxHashSet::default(),
@@ -310,6 +314,7 @@ impl AppContext {
event_listeners: SubscriberSet::new(),
release_listeners: SubscriberSet::new(),
keystroke_observers: SubscriberSet::new(),
keyboard_layout_observers: SubscriberSet::new(),
global_observers: SubscriberSet::new(),
quit_observers: SubscriberSet::new(),
layout_id_buffer: Default::default(),
@@ -323,6 +328,19 @@ impl AppContext {
init_app_menus(platform.as_ref(), &mut app.borrow_mut());
platform.on_keyboard_layout_change(Box::new({
let app = Rc::downgrade(&app);
move || {
if let Some(app) = app.upgrade() {
let cx = &mut app.borrow_mut();
cx.keyboard_layout = SharedString::from(cx.platform.keyboard_layout());
cx.keyboard_layout_observers
.clone()
.retain(&(), move |callback| (callback)(cx));
}
}
}));
platform.on_quit(Box::new({
let cx = app.clone();
move || {
@@ -356,6 +374,27 @@ impl AppContext {
}
}
/// Get the id of the current keyboard layout
pub fn keyboard_layout(&self) -> &SharedString {
&self.keyboard_layout
}
/// Invokes a handler when the current keyboard layout changes
pub fn on_keyboard_layout_change<F>(&self, mut callback: F) -> Subscription
where
F: 'static + FnMut(&mut AppContext),
{
let (subscription, activate) = self.keyboard_layout_observers.insert(
(),
Box::new(move |cx| {
callback(cx);
true
}),
);
activate();
subscription
}
/// Gracefully quit the application via the platform's standard routine.
pub fn quit(&self) {
self.platform.quit();

View File

@@ -88,11 +88,22 @@ pub struct UniformListFrameState {
#[derive(Clone, Debug, Default)]
pub struct UniformListScrollHandle(pub Rc<RefCell<UniformListScrollState>>);
/// Where to place the element scrolled to.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ScrollStrategy {
/// Place the element at the top of the list's viewport.
Top,
/// Attempt to place the element in the middle of the list's viewport.
/// May not be possible if there's not enough list items above the item scrolled to:
/// in this case, the element will be placed at the closest possible position.
Center,
}
#[derive(Clone, Debug, Default)]
#[allow(missing_docs)]
pub struct UniformListScrollState {
pub base_handle: ScrollHandle,
pub deferred_scroll_to_item: Option<usize>,
pub deferred_scroll_to_item: Option<(usize, ScrollStrategy)>,
/// Size of the item, captured during last layout.
pub last_item_size: Option<ItemSize>,
}
@@ -118,14 +129,16 @@ impl UniformListScrollHandle {
}
/// Scroll the list to the given item index.
pub fn scroll_to_item(&self, ix: usize) {
self.0.borrow_mut().deferred_scroll_to_item = Some(ix);
pub fn scroll_to_item(&self, ix: usize, strategy: ScrollStrategy) {
self.0.borrow_mut().deferred_scroll_to_item = Some((ix, strategy));
}
/// Get the index of the topmost visible child.
#[cfg(any(test, feature = "test-support"))]
pub fn logical_scroll_top_index(&self) -> usize {
let this = self.0.borrow();
this.deferred_scroll_to_item
.map(|(ix, _)| ix)
.unwrap_or_else(|| this.base_handle.logical_scroll_top().0)
}
}
@@ -273,18 +286,40 @@ impl Element for UniformList {
scroll_offset.x = Pixels::ZERO;
}
if let Some(ix) = shared_scroll_to_item {
if let Some((ix, scroll_strategy)) = shared_scroll_to_item {
let list_height = padded_bounds.size.height;
let mut updated_scroll_offset = shared_scroll_offset.borrow_mut();
let item_top = item_height * ix + padding.top;
let item_bottom = item_top + item_height;
let scroll_top = -updated_scroll_offset.y;
let mut scrolled_to_top = false;
if item_top < scroll_top + padding.top {
scrolled_to_top = true;
updated_scroll_offset.y = -(item_top) + padding.top;
} else if item_bottom > scroll_top + list_height - padding.bottom {
scrolled_to_top = true;
updated_scroll_offset.y = -(item_bottom - list_height) - padding.bottom;
}
scroll_offset = *updated_scroll_offset;
match scroll_strategy {
ScrollStrategy::Top => {}
ScrollStrategy::Center => {
if scrolled_to_top {
let item_center = item_top + item_height / 2.0;
let target_scroll_top = item_center - list_height / 2.0;
if item_top < scroll_top
|| item_bottom > scroll_top + list_height
{
updated_scroll_offset.y = -target_scroll_top
.max(Pixels::ZERO)
.min(content_height - list_height)
.max(Pixels::ZERO);
}
}
}
}
scroll_offset = *updated_scroll_offset
}
let first_visible_element_ix =

View File

@@ -68,7 +68,7 @@ pub(crate) struct DispatchNodeId(usize);
pub(crate) struct DispatchTree {
node_stack: Vec<DispatchNodeId>,
context_stack: Vec<KeyContext>,
pub(crate) context_stack: Vec<KeyContext>,
view_stack: Vec<EntityId>,
nodes: Vec<DispatchNode>,
focusable_node_ids: FxHashMap<FocusId, DispatchNodeId>,
@@ -396,13 +396,13 @@ impl DispatchTree {
pub fn bindings_for_action(
&self,
action: &dyn Action,
context_path: &[KeyContext],
context_stack: &[KeyContext],
) -> Vec<KeyBinding> {
let keymap = self.keymap.borrow();
keymap
.bindings_for_action(action)
.filter(|binding| {
let (bindings, _) = keymap.bindings_for_input(&binding.keystrokes, context_path);
let (bindings, _) = keymap.bindings_for_input(&binding.keystrokes, context_stack);
bindings
.iter()
.next()
@@ -519,13 +519,6 @@ impl DispatchTree {
dispatch_path
}
pub fn context_path(&self, node_id: DispatchNodeId) -> SmallVec<[KeyContext; 32]> {
self.dispatch_path(node_id)
.into_iter()
.filter_map(|node_id| self.node(node_id).context.clone())
.collect()
}
pub fn focus_path(&self, focus_id: FocusId) -> SmallVec<[FocusId; 8]> {
let mut focus_path: SmallVec<[FocusId; 8]> = SmallVec::new();
let mut current_node_id = self.focusable_node_ids.get(&focus_id).copied();

View File

@@ -139,7 +139,7 @@ impl Keymap {
pub fn bindings_for_input(
&self,
input: &[Keystroke],
context_path: &[KeyContext],
context_stack: &[KeyContext],
) -> (SmallVec<[KeyBinding; 1]>, bool) {
let possibilities = self.bindings().rev().filter_map(|binding| {
binding
@@ -151,8 +151,8 @@ impl Keymap {
let mut is_pending = None;
'outer: for (binding, pending) in possibilities {
for depth in (0..=context_path.len()).rev() {
if self.binding_enabled(binding, &context_path[0..depth]) {
for depth in (0..=context_stack.len()).rev() {
if self.binding_enabled(binding, &context_stack[0..depth]) {
if is_pending.is_none() {
is_pending = Some(pending);
}

View File

@@ -1,3 +1,5 @@
use collections::HashMap;
use crate::{Action, KeyBindingContextPredicate, Keystroke};
use anyhow::Result;
use smallvec::SmallVec;
@@ -22,22 +24,37 @@ impl Clone for KeyBinding {
impl KeyBinding {
/// Construct a new keybinding from the given data.
pub fn new<A: Action>(keystrokes: &str, action: A, context_predicate: Option<&str>) -> Self {
Self::load(keystrokes, Box::new(action), context_predicate).unwrap()
Self::load(keystrokes, Box::new(action), context_predicate, None).unwrap()
}
/// Load a keybinding from the given raw data.
pub fn load(keystrokes: &str, action: Box<dyn Action>, context: Option<&str>) -> Result<Self> {
pub fn load(
keystrokes: &str,
action: Box<dyn Action>,
context: Option<&str>,
key_equivalents: Option<&HashMap<char, char>>,
) -> Result<Self> {
let context = if let Some(context) = context {
Some(KeyBindingContextPredicate::parse(context)?)
} else {
None
};
let keystrokes = keystrokes
let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes
.split_whitespace()
.map(Keystroke::parse)
.collect::<Result<_>>()?;
if let Some(equivalents) = key_equivalents {
for keystroke in keystrokes.iter_mut() {
if keystroke.key.chars().count() == 1 {
if let Some(key) = equivalents.get(&keystroke.key.chars().next().unwrap()) {
keystroke.key = key.to_string();
}
}
}
}
Ok(Self {
keystrokes,
action,

View File

@@ -169,6 +169,7 @@ pub(crate) trait Platform: 'static {
fn on_quit(&self, callback: Box<dyn FnMut()>);
fn on_reopen(&self, callback: Box<dyn FnMut()>);
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
@@ -180,6 +181,7 @@ pub(crate) trait Platform: 'static {
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
fn keyboard_layout(&self) -> String;
fn compositor_name(&self) -> &'static str {
""

View File

@@ -138,6 +138,14 @@ impl<P: LinuxClient + 'static> Platform for P {
self.with_common(|common| common.text_system.clone())
}
fn keyboard_layout(&self) -> String {
"unknown".into()
}
fn on_keyboard_layout_change(&self, _callback: Box<dyn FnMut()>) {
// todo(linux)
}
fn run(&self, on_finish_launching: Box<dyn FnOnce()>) {
on_finish_launching();

View File

@@ -259,10 +259,7 @@ impl PlatformInput {
unsafe fn parse_keystroke(native_event: id) -> Keystroke {
use cocoa::appkit::*;
let mut chars_ignoring_modifiers = native_event
.charactersIgnoringModifiers()
.to_str()
.to_string();
let mut chars_ignoring_modifiers = chars_for_modified_key(native_event.keyCode(), false, false);
let first_char = chars_ignoring_modifiers.chars().next().map(|ch| ch as u16);
let modifiers = native_event.modifierFlags();
@@ -314,28 +311,41 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
Some(NSF18FunctionKey) => "f18".to_string(),
Some(NSF19FunctionKey) => "f19".to_string(),
_ => {
let mut chars_ignoring_modifiers_and_shift =
chars_for_modified_key(native_event.keyCode(), false, false);
// Cases to test when modifying this:
//
// qwerty key | none | cmd | cmd-shift
// * Armenian s | ս | cmd-s | cmd-shift-s (layout is non-ASCII, so we use cmd layout)
// * Dvorak+QWERTY s | o | cmd-s | cmd-shift-s (layout switches on cmd)
// * Ukrainian+QWERTY s | с | cmd-s | cmd-shift-s (macOS reports cmd-s instead of cmd-S)
// * Czech 7 | ý | cmd-ý | cmd-7 (layout has shifted numbers)
// * Norwegian 7 | 7 | cmd-7 | cmd-/ (macOS reports cmd-shift-7 instead of cmd-/)
// * Russian 7 | 7 | cmd-7 | cmd-& (shift-7 is . but when cmd is down, should use cmd layout)
// * German QWERTZ ; | ö | cmd-ö | cmd-Ö (Zed's shift special case only applies to a-z)
let mut chars_with_shift = chars_for_modified_key(native_event.keyCode(), false, true);
// Honor ⌘ when Dvorak-QWERTY is used.
let chars_with_cmd = chars_for_modified_key(native_event.keyCode(), true, false);
if command && chars_ignoring_modifiers_and_shift != chars_with_cmd {
chars_ignoring_modifiers =
chars_for_modified_key(native_event.keyCode(), true, shift);
chars_ignoring_modifiers_and_shift = chars_with_cmd;
// Handle Dvorak+QWERTY / Russian / Armeniam
if command || always_use_command_layout() {
let chars_with_cmd = chars_for_modified_key(native_event.keyCode(), true, false);
let chars_with_both = chars_for_modified_key(native_event.keyCode(), true, true);
// We don't do this in the case that the shifted command key generates
// the same character as the unshifted command key (Norwegian, e.g.)
if chars_with_both != chars_with_cmd {
chars_with_shift = chars_with_both;
// Handle edge-case where cmd-shift-s reports cmd-s instead of
// cmd-shift-s (Ukrainian, etc.)
} else if chars_with_cmd.to_ascii_uppercase() != chars_with_cmd {
chars_with_shift = chars_with_cmd.to_ascii_uppercase();
}
chars_ignoring_modifiers = chars_with_cmd;
}
if shift {
if chars_ignoring_modifiers_and_shift
== chars_ignoring_modifiers.to_ascii_lowercase()
{
chars_ignoring_modifiers_and_shift
} else if chars_ignoring_modifiers_and_shift != chars_ignoring_modifiers {
shift = false;
chars_ignoring_modifiers
} else {
chars_ignoring_modifiers
}
if shift && chars_ignoring_modifiers == chars_with_shift.to_ascii_lowercase() {
chars_ignoring_modifiers
} else if shift {
shift = false;
chars_with_shift
} else {
chars_ignoring_modifiers
}
@@ -355,6 +365,28 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
}
}
fn always_use_command_layout() -> bool {
// look at the key to the right of "tab" ('a' in QWERTY)
// if it produces a non-ASCII character, but with command held produces ASCII,
// we default to the command layout for our keyboard system.
let event = synthesize_keyboard_event(0);
let without_cmd = unsafe {
let event: id = msg_send![class!(NSEvent), eventWithCGEvent: &*event];
event.characters().to_str().to_string()
};
if without_cmd.is_ascii() {
return false;
}
event.set_flags(CGEventFlags::CGEventFlagCommand);
let with_cmd = unsafe {
let event: id = msg_send![class!(NSEvent), eventWithCGEvent: &*event];
event.characters().to_str().to_string()
};
with_cmd.is_ascii()
}
fn chars_for_modified_key(code: CGKeyCode, cmd: bool, shift: bool) -> String {
// Ideally, we would use `[NSEvent charactersByApplyingModifiers]` but that
// always returns an empty string with certain keyboards, e.g. Japanese. Synthesizing

View File

@@ -136,6 +136,11 @@ unsafe fn build_classes() {
open_urls as extern "C" fn(&mut Object, Sel, id, id),
);
decl.add_method(
sel!(onKeyboardLayoutChange:),
on_keyboard_layout_change as extern "C" fn(&mut Object, Sel, id),
);
decl.register()
}
}
@@ -152,6 +157,7 @@ pub(crate) struct MacPlatformState {
text_hash_pasteboard_type: id,
metadata_pasteboard_type: id,
reopen: Option<Box<dyn FnMut()>>,
on_keyboard_layout_change: Option<Box<dyn FnMut()>>,
quit: Option<Box<dyn FnMut()>>,
menu_command: Option<Box<dyn FnMut(&dyn Action)>>,
validate_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
@@ -196,6 +202,7 @@ impl MacPlatform {
open_urls: None,
finish_launching: None,
dock_menu: None,
on_keyboard_layout_change: None,
}))
}
@@ -785,6 +792,10 @@ impl Platform for MacPlatform {
self.0.lock().reopen = Some(callback);
}
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
self.0.lock().on_keyboard_layout_change = Some(callback);
}
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
self.0.lock().menu_command = Some(callback);
}
@@ -797,6 +808,22 @@ impl Platform for MacPlatform {
self.0.lock().validate_menu_command = Some(callback);
}
fn keyboard_layout(&self) -> String {
unsafe {
let current_keyboard = TISCopyCurrentKeyboardLayoutInputSource();
let input_source_id: *mut Object = TISGetInputSourceProperty(
current_keyboard,
kTISPropertyInputSourceID as *const c_void,
);
let input_source_id: *const std::os::raw::c_char =
msg_send![input_source_id, UTF8String];
let input_source_id = CStr::from_ptr(input_source_id).to_str().unwrap();
input_source_id.to_string()
}
}
fn app_path(&self) -> Result<PathBuf> {
unsafe {
let bundle: id = NSBundle::mainBundle();
@@ -1259,6 +1286,16 @@ extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) {
unsafe {
let app: id = msg_send![APP_CLASS, sharedApplication];
app.setActivationPolicy_(NSApplicationActivationPolicyRegular);
let notification_center: *mut Object =
msg_send![class!(NSNotificationCenter), defaultCenter];
let name = ns_string("NSTextInputContextKeyboardSelectionDidChangeNotification");
let _: () = msg_send![notification_center, addObserver: this as id
selector: sel!(onKeyboardLayoutChange:)
name: name
object: nil
];
let platform = get_mac_platform(this);
let callback = platform.0.lock().finish_launching.take();
if let Some(callback) = callback {
@@ -1289,6 +1326,20 @@ extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) {
}
}
extern "C" fn on_keyboard_layout_change(this: &mut Object, _: Sel, _: id) {
let platform = unsafe { get_mac_platform(this) };
let mut lock = platform.0.lock();
if let Some(mut callback) = lock.on_keyboard_layout_change.take() {
drop(lock);
callback();
platform
.0
.lock()
.on_keyboard_layout_change
.get_or_insert(callback);
}
}
extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) {
let urls = unsafe {
(0..urls.count())
@@ -1395,6 +1446,17 @@ unsafe fn ns_url_to_path(url: id) -> Result<PathBuf> {
}
}
#[link(name = "Carbon", kind = "framework")]
extern "C" {
fn TISCopyCurrentKeyboardLayoutInputSource() -> *mut Object;
fn TISGetInputSourceProperty(
inputSource: *mut Object,
propertyKey: *const c_void,
) -> *mut Object;
pub static kTISPropertyInputSourceID: CFStringRef;
}
mod security {
#![allow(non_upper_case_globals)]
use super::*;

View File

@@ -162,6 +162,12 @@ impl Platform for TestPlatform {
self.text_system.clone()
}
fn keyboard_layout(&self) -> String {
"zed.keyboard.example".to_string()
}
fn on_keyboard_layout_change(&self, _: Box<dyn FnMut()>) {}
fn run(&self, _on_finish_launching: Box<dyn FnOnce()>) {
unimplemented!()
}

View File

@@ -197,6 +197,14 @@ impl Platform for WindowsPlatform {
self.text_system.clone()
}
fn keyboard_layout(&self) -> String {
"unknown".into()
}
fn on_keyboard_layout_change(&self, _callback: Box<dyn FnMut()>) {
// todo(windows)
}
fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>) {
on_finish_launching();
let vsync_event = unsafe { Owned::new(CreateEventW(None, false, false, None).unwrap()) };

View File

@@ -60,10 +60,8 @@ impl SvgRenderer {
let mut pixmap = resvg::tiny_skia::Pixmap::new(size.width.into(), size.height.into())
.ok_or(usvg::Error::InvalidSize)?;
let transform = tree.view_box().to_transform(
resvg::tiny_skia::Size::from_wh(size.width.0 as f32, size.height.0 as f32)
.ok_or(usvg::Error::InvalidSize)?,
);
let scale = size.width.0 as f32 / tree.size().width();
let transform = resvg::tiny_skia::Transform::from_scale(scale, scale);
resvg::render(&tree, transform, &mut pixmap.as_mut());

View File

@@ -668,6 +668,7 @@ impl Hash for RenderGlyphParams {
self.font_size.0.to_bits().hash(state);
self.subpixel_variant.hash(state);
self.scale_factor.to_bits().hash(state);
self.is_emoji.hash(state);
}
}

View File

@@ -3683,11 +3683,22 @@ impl<'a> WindowContext<'a> {
/// Returns all available actions for the focused element.
pub fn available_actions(&self) -> Vec<Box<dyn Action>> {
let node_id = self
.window
.focus
.and_then(|focus_id| {
self.window
.rendered_frame
.dispatch_tree
.focusable_node_id(focus_id)
})
.unwrap_or_else(|| self.window.rendered_frame.dispatch_tree.root_node_id());
let mut actions = self
.window
.rendered_frame
.dispatch_tree
.available_actions(self.focused_node_id());
.available_actions(node_id);
for action_type in self.global_action_listeners.keys() {
if let Err(ix) = actions.binary_search_by_key(action_type, |a| a.as_any().type_id()) {
let action = self.actions.build_action_type(action_type).ok();
@@ -3701,9 +3712,13 @@ impl<'a> WindowContext<'a> {
/// Returns key bindings that invoke the given action on the currently focused element.
pub fn bindings_for_action(&self, action: &dyn Action) -> Vec<KeyBinding> {
let dispatch_tree = &self.window.rendered_frame.dispatch_tree;
dispatch_tree
.bindings_for_action(action, &dispatch_tree.context_path(self.focused_node_id()))
self.window
.rendered_frame
.dispatch_tree
.bindings_for_action(
action,
&self.window.rendered_frame.dispatch_tree.context_stack,
)
}
/// Returns key bindings that invoke the given action on the currently focused element.
@@ -3719,23 +3734,15 @@ impl<'a> WindowContext<'a> {
) -> Vec<KeyBinding> {
let dispatch_tree = &self.window.rendered_frame.dispatch_tree;
if let Some(node_id) = dispatch_tree.focusable_node_id(focus_handle.id) {
dispatch_tree.bindings_for_action(action, &dispatch_tree.context_path(node_id))
} else {
vec![]
}
}
fn focused_node_id(&self) -> DispatchNodeId {
self.window
.focus
.and_then(|focus_id| {
self.window
.rendered_frame
.dispatch_tree
.focusable_node_id(focus_id)
})
.unwrap_or_else(|| self.window.rendered_frame.dispatch_tree.root_node_id())
let Some(node_id) = dispatch_tree.focusable_node_id(focus_handle.id) else {
return vec![];
};
let context_stack: Vec<_> = dispatch_tree
.dispatch_path(node_id)
.into_iter()
.filter_map(|node_id| dispatch_tree.node(node_id).context.clone())
.collect();
dispatch_tree.bindings_for_action(action, &context_stack)
}
/// Returns a generic event listener that invokes the given listener with the view and context associated with the given view handle.

View File

@@ -30,6 +30,7 @@ paths.workspace = true
serde.workspace = true
strum.workspace = true
util.workspace = true
extension_host.workspace = true
[dev-dependencies]
indoc.workspace = true

View File

@@ -2,6 +2,7 @@ mod item;
mod to_markdown;
use cargo_metadata::MetadataCommand;
use extension_host::DocsDatabase;
use futures::future::BoxFuture;
pub use item::*;
use parking_lot::RwLock;
@@ -208,7 +209,7 @@ impl IndexedDocsProvider for DocsDotRsProvider {
async fn index_rustdoc(
package: PackageName,
database: Arc<IndexedDocsDatabase>,
database: Arc<dyn DocsDatabase>,
fetch_page: impl Fn(&PackageName, Option<&RustdocItem>) -> BoxFuture<'static, Result<Option<String>>>
+ Send
+ Sync,

View File

@@ -324,8 +324,10 @@ impl IndexedDocsDatabase {
Ok(any)
})
}
}
pub fn insert(&self, key: String, docs: String) -> Task<Result<()>> {
impl extension_host::DocsDatabase for IndexedDocsDatabase {
fn insert(&self, key: String, docs: String) -> Task<Result<()>> {
let env = self.env.clone();
let entries = self.entries;

View File

@@ -288,14 +288,14 @@ impl LanguageRegistry {
config.name.clone(),
config.grammar.clone(),
config.matcher.clone(),
move || {
Arc::new(move || {
Ok(LoadedLanguage {
config: config.clone(),
queries: Default::default(),
toolchain_provider: None,
context_provider: None,
})
},
}),
)
}
@@ -436,9 +436,8 @@ impl LanguageRegistry {
name: LanguageName,
grammar_name: Option<Arc<str>>,
matcher: LanguageMatcher,
load: impl Fn() -> Result<LoadedLanguage> + 'static + Send + Sync,
load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>,
) {
let load = Arc::new(load);
let state = &mut *self.state.write();
for existing_language in &mut state.available_languages {

View File

@@ -912,7 +912,7 @@ impl Render for ConfigurationView {
let is_pro = plan == Some(proto::Plan::ZedPro);
let subscription_text = Label::new(if is_pro {
"You have full access to Zed's hosted models from Anthropic, OpenAI, Google with faster speeds and higher limits through Zed Pro."
"You have full access to Zed's hosted LLMs, which include models from Anthropic, OpenAI, and Google. They come with faster speeds and higher limits through Zed Pro."
} else {
"You have basic access to models from Anthropic through the Zed AI Free plan."
});
@@ -957,27 +957,14 @@ impl Render for ConfigurationView {
})
} else {
v_flex()
.gap_6()
.child(Label::new("Use the zed.dev to access language models."))
.gap_2()
.child(Label::new("Use Zed AI to access hosted language models."))
.child(
v_flex()
.gap_2()
.child(
Button::new("sign_in", "Sign in")
.icon_color(Color::Muted)
.icon(IconName::Github)
.icon_position(IconPosition::Start)
.style(ButtonStyle::Filled)
.full_width()
.on_click(cx.listener(move |this, _, cx| this.authenticate(cx))),
)
.child(
div().flex().w_full().items_center().child(
Label::new("Sign in to enable collaboration.")
.color(Color::Muted)
.size(LabelSize::Small),
),
),
Button::new("sign_in", "Sign In")
.icon_color(Color::Muted)
.icon(IconName::Github)
.icon_position(IconPosition::Start)
.on_click(cx.listener(move |this, _, cx| this.authenticate(cx))),
)
}
}

View File

@@ -2,8 +2,8 @@ use editor::{scroll::Autoscroll, Anchor, Editor, ExcerptId};
use gpui::{
actions, div, rems, uniform_list, AppContext, Div, EventEmitter, FocusHandle, FocusableView,
Hsla, InteractiveElement, IntoElement, Model, MouseButton, MouseDownEvent, MouseMoveEvent,
ParentElement, Render, SharedString, Styled, UniformListScrollHandle, View, ViewContext,
VisualContext, WeakView, WindowContext,
ParentElement, Render, ScrollStrategy, SharedString, Styled, UniformListScrollHandle, View,
ViewContext, VisualContext, WeakView, WindowContext,
};
use language::{Buffer, OwnedSyntaxLayer};
use std::{mem, ops::Range};
@@ -199,7 +199,8 @@ impl SyntaxTreeView {
let descendant_ix = cursor.descendant_index();
self.selected_descendant_ix = Some(descendant_ix);
self.list_scroll_handle.scroll_to_item(descendant_ix);
self.list_scroll_handle
.scroll_to_item(descendant_ix, ScrollStrategy::Center);
cx.notify();
Some(())

View File

@@ -61,14 +61,14 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: NodeRuntime, cx: &mu
config.name.clone(),
config.grammar.clone(),
config.matcher.clone(),
move || {
Arc::new(move || {
Ok(LoadedLanguage {
config: config.clone(),
queries: load_queries($name),
context_provider: None,
toolchain_provider: None,
})
},
}),
);
};
($name:literal, $adapters:expr) => {
@@ -82,14 +82,14 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: NodeRuntime, cx: &mu
config.name.clone(),
config.grammar.clone(),
config.matcher.clone(),
move || {
Arc::new(move || {
Ok(LoadedLanguage {
config: config.clone(),
queries: load_queries($name),
context_provider: None,
toolchain_provider: None,
})
},
}),
);
};
($name:literal, $adapters:expr, $context_provider:expr) => {
@@ -103,14 +103,14 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: NodeRuntime, cx: &mu
config.name.clone(),
config.grammar.clone(),
config.matcher.clone(),
move || {
Arc::new(move || {
Ok(LoadedLanguage {
config: config.clone(),
queries: load_queries($name),
context_provider: Some(Arc::new($context_provider)),
toolchain_provider: None,
})
},
}),
);
};
($name:literal, $adapters:expr, $context_provider:expr, $toolchain_provider:expr) => {
@@ -124,14 +124,14 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: NodeRuntime, cx: &mu
config.name.clone(),
config.grammar.clone(),
config.matcher.clone(),
move || {
Arc::new(move || {
Ok(LoadedLanguage {
config: config.clone(),
queries: load_queries($name),
context_provider: Some(Arc::new($context_provider)),
toolchain_provider: Some($toolchain_provider),
})
},
}),
);
};
}

View File

@@ -38,6 +38,11 @@
"class" @context
name: (_) @name) @item
(abstract_class_declaration
"abstract" @context
"class" @context
name: (_) @name) @item
(method_definition
[
"get"

View File

@@ -38,6 +38,11 @@
"class" @context
name: (_) @name) @item
(abstract_class_declaration
"abstract" @context
"class" @context
name: (_) @name) @item
(method_definition
[
"get"

View File

@@ -6482,11 +6482,21 @@ mod tests {
fn test_history(cx: &mut AppContext) {
let test_settings = SettingsStore::test(cx);
cx.set_global(test_settings);
let buffer_1 = cx.new_model(|cx| Buffer::local("1234", cx));
let buffer_2 = cx.new_model(|cx| Buffer::local("5678", cx));
let group_interval: Duration = Duration::from_millis(1);
let buffer_1 = cx.new_model(|cx| {
let mut buf = Buffer::local("1234", cx);
buf.set_group_interval(group_interval);
buf
});
let buffer_2 = cx.new_model(|cx| {
let mut buf = Buffer::local("5678", cx);
buf.set_group_interval(group_interval);
buf
});
let multibuffer = cx.new_model(|_| MultiBuffer::new(Capability::ReadWrite));
let group_interval = multibuffer.read(cx).history.group_interval;
multibuffer.update(cx, |this, _| {
this.history.group_interval = group_interval;
});
multibuffer.update(cx, |multibuffer, cx| {
multibuffer.push_excerpts(
buffer_1.clone(),

View File

@@ -23,11 +23,11 @@ use editor::{
use file_icons::FileIcons;
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
use gpui::{
actions, anchored, deferred, div, impl_actions, point, px, size, uniform_list, Action,
AnyElement, AppContext, AssetSource, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent,
Div, ElementId, EventEmitter, FocusHandle, FocusableView, HighlightStyle, InteractiveElement,
actions, anchored, deferred, div, point, px, size, uniform_list, Action, AnyElement,
AppContext, AssetSource, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div,
ElementId, EventEmitter, FocusHandle, FocusableView, HighlightStyle, InteractiveElement,
IntoElement, KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior, Model, MouseButton,
MouseDownEvent, ParentElement, Pixels, Point, Render, SharedString, Stateful,
MouseDownEvent, ParentElement, Pixels, Point, Render, ScrollStrategy, SharedString, Stateful,
StatefulInteractiveElement as _, Styled, Subscription, Task, UniformListScrollHandle, View,
ViewContext, VisualContext, WeakView, WindowContext,
};
@@ -58,13 +58,6 @@ use workspace::{
};
use worktree::{Entry, ProjectEntryId, WorktreeId};
#[derive(Clone, Default, Deserialize, PartialEq)]
pub struct Open {
change_selection: bool,
}
impl_actions!(outline_panel, [Open]);
actions!(
outline_panel,
[
@@ -75,9 +68,10 @@ actions!(
ExpandAllEntries,
ExpandSelectedEntry,
FoldDirectory,
ToggleActiveEditorPin,
Open,
RevealInFileManager,
SelectParent,
ToggleActiveEditorPin,
ToggleFocus,
UnfoldDirectory,
]
@@ -813,11 +807,11 @@ impl OutlinePanel {
self.update_cached_entries(None, cx);
}
fn open(&mut self, open: &Open, cx: &mut ViewContext<Self>) {
fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) {
if self.filter_editor.focus_handle(cx).is_focused(cx) {
cx.propagate()
} else if let Some(selected_entry) = self.selected_entry().cloned() {
self.open_entry(&selected_entry, open.change_selection, cx);
self.open_entry(&selected_entry, true, cx);
}
}
@@ -834,6 +828,32 @@ impl OutlinePanel {
}
}
fn open_excerpts(&mut self, action: &editor::OpenExcerpts, cx: &mut ViewContext<Self>) {
if self.filter_editor.focus_handle(cx).is_focused(cx) {
cx.propagate()
} else if let Some((active_editor, selected_entry)) =
self.active_editor().zip(self.selected_entry().cloned())
{
self.open_entry(&selected_entry, true, cx);
active_editor.update(cx, |editor, cx| editor.open_excerpts(action, cx));
}
}
fn open_excerpts_split(
&mut self,
action: &editor::OpenExcerptsSplit,
cx: &mut ViewContext<Self>,
) {
if self.filter_editor.focus_handle(cx).is_focused(cx) {
cx.propagate()
} else if let Some((active_editor, selected_entry)) =
self.active_editor().zip(self.selected_entry().cloned())
{
self.open_entry(&selected_entry, true, cx);
active_editor.update(cx, |editor, cx| editor.open_excerpts_in_split(action, cx));
}
}
fn open_entry(
&mut self,
entry: &PanelEntry,
@@ -851,7 +871,6 @@ impl OutlinePanel {
Point::new(0.0, -(active_editor.read(cx).file_header_size() as f32))
};
self.toggle_expanded(entry, cx);
let scroll_target = match entry {
PanelEntry::FoldedDirs(..) | PanelEntry::Fs(FsEntry::Directory(..)) => None,
PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _)) => {
@@ -949,6 +968,9 @@ impl OutlinePanel {
} else {
self.select_first(&SelectFirst {}, cx)
}
if let Some(selected_entry) = self.selected_entry().cloned() {
self.open_entry(&selected_entry, false, cx);
}
}
fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
@@ -965,6 +987,9 @@ impl OutlinePanel {
} else {
self.select_last(&SelectLast, cx)
}
if let Some(selected_entry) = self.selected_entry().cloned() {
self.open_entry(&selected_entry, false, cx);
}
}
fn select_parent(&mut self, _: &SelectParent, cx: &mut ViewContext<Self>) {
@@ -1078,7 +1103,8 @@ impl OutlinePanel {
.iter()
.position(|cached_entry| &cached_entry.entry == selected_entry);
if let Some(index) = index {
self.scroll_handle.scroll_to_item(index);
self.scroll_handle
.scroll_to_item(index, ScrollStrategy::Center);
cx.notify();
}
}
@@ -1449,10 +1475,7 @@ impl OutlinePanel {
}
fn reveal_entry_for_selection(&mut self, editor: View<Editor>, cx: &mut ViewContext<'_, Self>) {
if !self.active {
return;
}
if !OutlinePanelSettings::get_global(cx).auto_reveal_entries {
if !self.active || !OutlinePanelSettings::get_global(cx).auto_reveal_entries {
return;
}
let project = self.project.clone();
@@ -2005,6 +2028,7 @@ impl OutlinePanel {
return;
}
let change_selection = event.down.click_count > 1;
outline_panel.toggle_expanded(&clicked_entry, cx);
outline_panel.open_entry(&clicked_entry, change_selection, cx);
})
})
@@ -2447,19 +2471,20 @@ impl OutlinePanel {
}
fn clear_previous(&mut self, cx: &mut WindowContext<'_>) {
self.fs_entries_update_task = Task::ready(());
self.outline_fetch_tasks.clear();
self.cached_entries_update_task = Task::ready(());
self.reveal_selection_task = Task::ready(Ok(()));
self.filter_editor.update(cx, |editor, cx| editor.clear(cx));
self.collapsed_entries.clear();
self.unfolded_dirs.clear();
self.selected_entry = SelectedEntry::None;
self.fs_entries_update_task = Task::ready(());
self.cached_entries_update_task = Task::ready(());
self.active_item = None;
self.fs_entries.clear();
self.fs_entries_depth.clear();
self.fs_children_count.clear();
self.outline_fetch_tasks.clear();
self.excerpts.clear();
self.cached_entries = Vec::new();
self.selected_entry = SelectedEntry::None;
self.pinned = false;
self.mode = ItemsDisplayMode::Outline;
}
@@ -2488,10 +2513,6 @@ impl OutlinePanel {
.matches
.iter()
.rev()
.filter(|(match_range, _)| {
match_range.start.excerpt_id == excerpt_id
|| match_range.end.excerpt_id == excerpt_id
})
.min_by_key(|&(match_range, _)| {
let match_display_range =
match_range.clone().to_display_points(&editor_snapshot);
@@ -4244,6 +4265,8 @@ impl Render for OutlinePanel {
.on_action(cx.listener(Self::toggle_active_editor_pin))
.on_action(cx.listener(Self::unfold_directory))
.on_action(cx.listener(Self::fold_directory))
.on_action(cx.listener(Self::open_excerpts))
.on_action(cx.listener(Self::open_excerpts_split))
.when(is_local, |el| {
el.on_action(cx.listener(Self::reveal_in_finder))
})
@@ -4725,6 +4748,189 @@ mod tests {
});
}
#[gpui::test(iterations = 10)]
async fn test_item_opening(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.background_executor.clone());
populate_with_test_ra_project(&fs, "/rust-analyzer").await;
let project = Project::test(fs.clone(), ["/rust-analyzer".as_ref()], cx).await;
project.read_with(cx, |project, _| {
project.languages().add(Arc::new(rust_lang()))
});
let workspace = add_outline_panel(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let outline_panel = outline_panel(&workspace, cx);
outline_panel.update(cx, |outline_panel, cx| outline_panel.set_active(true, cx));
workspace
.update(cx, |workspace, cx| {
ProjectSearchView::deploy_search(workspace, &workspace::DeploySearch::default(), cx)
})
.unwrap();
let search_view = workspace
.update(cx, |workspace, cx| {
workspace
.active_pane()
.read(cx)
.items()
.find_map(|item| item.downcast::<ProjectSearchView>())
.expect("Project search view expected to appear after new search event trigger")
})
.unwrap();
let query = "param_names_for_lifetime_elision_hints";
perform_project_search(&search_view, query, cx);
search_view.update(cx, |search_view, cx| {
search_view
.results_editor()
.update(cx, |results_editor, cx| {
assert_eq!(
results_editor.display_text(cx).match_indices(query).count(),
9
);
});
});
let all_matches = r#"/
crates/
ide/src/
inlay_hints/
fn_lifetime_fn.rs
search: match config.param_names_for_lifetime_elision_hints {
search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
search: Some(it) if config.param_names_for_lifetime_elision_hints => {
search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
inlay_hints.rs
search: pub param_names_for_lifetime_elision_hints: bool,
search: param_names_for_lifetime_elision_hints: self
static_index.rs
search: param_names_for_lifetime_elision_hints: false,
rust-analyzer/src/
cli/
analysis_stats.rs
search: param_names_for_lifetime_elision_hints: true,
config.rs
search: param_names_for_lifetime_elision_hints: self"#;
let select_first_in_all_matches = |line_to_select: &str| {
assert!(all_matches.contains(line_to_select));
all_matches.replacen(
line_to_select,
&format!("{line_to_select}{SELECTED_MARKER}"),
1,
)
};
cx.executor()
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
cx.run_until_parked();
let active_editor = outline_panel.update(cx, |outline_panel, _| {
outline_panel
.active_editor()
.expect("should have an active editor open")
});
let initial_outline_selection =
"search: match config.param_names_for_lifetime_elision_hints {";
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
outline_panel.selected_entry(),
),
select_first_in_all_matches(initial_outline_selection)
);
assert_eq!(
selected_row_text(&active_editor, cx),
initial_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
"Should place the initial editor selection on the corresponding search result"
);
outline_panel.select_next(&SelectNext, cx);
outline_panel.select_next(&SelectNext, cx);
});
let navigated_outline_selection =
"search: Some(it) if config.param_names_for_lifetime_elision_hints => {";
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
outline_panel.selected_entry(),
),
select_first_in_all_matches(navigated_outline_selection)
);
assert_eq!(
selected_row_text(&active_editor, cx),
initial_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
"Should still have the initial caret position after SelectNext calls"
);
});
outline_panel.update(cx, |outline_panel, cx| {
outline_panel.open(&Open, cx);
});
outline_panel.update(cx, |_, cx| {
assert_eq!(
selected_row_text(&active_editor, cx),
navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
"After opening, should move the caret to the opened outline entry's position"
);
});
outline_panel.update(cx, |outline_panel, cx| {
outline_panel.select_next(&SelectNext, cx);
});
let next_navigated_outline_selection =
"search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },";
outline_panel.update(cx, |outline_panel, cx| {
assert_eq!(
display_entries(
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
outline_panel.selected_entry(),
),
select_first_in_all_matches(next_navigated_outline_selection)
);
assert_eq!(
selected_row_text(&active_editor, cx),
navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
"Should again preserve the selection after another SelectNext call"
);
});
outline_panel.update(cx, |outline_panel, cx| {
outline_panel.open_excerpts(&editor::OpenExcerpts, cx);
});
cx.executor()
.advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
cx.run_until_parked();
let new_active_editor = outline_panel.update(cx, |outline_panel, _| {
outline_panel
.active_editor()
.expect("should have an active editor open")
});
outline_panel.update(cx, |outline_panel, cx| {
assert_ne!(
active_editor, new_active_editor,
"After opening an excerpt, new editor should be open"
);
assert_eq!(
display_entries(
&snapshot(&outline_panel, cx),
&outline_panel.cached_entries,
outline_panel.selected_entry(),
),
"fn_lifetime_fn.rs <==== selected"
);
assert_eq!(
selected_row_text(&new_active_editor, cx),
next_navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
"When opening the excerpt, should navigate to the place corresponding the outline entry"
);
});
}
#[gpui::test(iterations = 10)]
async fn test_frontend_repo_structure(cx: &mut TestAppContext) {
init_test(cx);
@@ -5194,4 +5400,16 @@ mod tests {
.read(cx)
.snapshot(cx)
}
fn selected_row_text(editor: &View<Editor>, cx: &mut WindowContext) -> String {
editor.update(cx, |editor, cx| {
let selections = editor.selections.all::<language::Point>(cx);
assert_eq!(selections.len(), 1, "Active editor should have exactly one selection after any outline panel interactions");
let selection = selections.first().unwrap();
let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
let line_start = language::Point::new(selection.start.row, 0);
let line_end = multi_buffer_snapshot.clip_point(language::Point::new(selection.end.row, u32::MAX), language::Bias::Right);
multi_buffer_snapshot.text_for_range(line_start..line_end).collect::<String>().trim().to_owned()
})
}
}

View File

@@ -165,6 +165,22 @@ pub fn extensions_dir() -> &'static PathBuf {
EXTENSIONS_DIR.get_or_init(|| support_dir().join("extensions"))
}
/// Returns the path to the extensions directory.
///
/// This is where installed extensions are stored on a remote.
pub fn remote_extensions_dir() -> &'static PathBuf {
static EXTENSIONS_DIR: OnceLock<PathBuf> = OnceLock::new();
EXTENSIONS_DIR.get_or_init(|| support_dir().join("remote_extensions"))
}
/// Returns the path to the extensions directory.
///
/// This is where installed extensions are stored on a remote.
pub fn remote_extensions_uploads_dir() -> &'static PathBuf {
static UPLOAD_DIR: OnceLock<PathBuf> = OnceLock::new();
UPLOAD_DIR.get_or_init(|| remote_extensions_dir().join("uploads"))
}
/// Returns the path to the themes directory.
///
/// This is where themes that are not provided by extensions are stored.

View File

@@ -3,8 +3,8 @@ use editor::{scroll::Autoscroll, Editor};
use gpui::{
actions, div, impl_actions, list, prelude::*, uniform_list, AnyElement, AppContext, ClickEvent,
DismissEvent, EventEmitter, FocusHandle, FocusableView, Length, ListSizingBehavior, ListState,
MouseButton, MouseUpEvent, Render, Task, UniformListScrollHandle, View, ViewContext,
WindowContext,
MouseButton, MouseUpEvent, Render, ScrollStrategy, Task, UniformListScrollHandle, View,
ViewContext, WindowContext,
};
use head::Head;
use serde::Deserialize;
@@ -495,7 +495,9 @@ impl<D: PickerDelegate> Picker<D> {
fn scroll_to_item_index(&mut self, ix: usize) {
match &mut self.element_container {
ElementContainer::List(state) => state.scroll_to_reveal_item(ix),
ElementContainer::UniformList(scroll_handle) => scroll_handle.scroll_to_item(ix),
ElementContainer::UniformList(scroll_handle) => {
scroll_handle.scroll_to_item(ix, ScrollStrategy::Top)
}
}
}

View File

@@ -70,6 +70,7 @@ text.workspace = true
util.workspace = true
url.workspace = true
which.workspace = true
fancy-regex.workspace = true
[target.'cfg(target_os = "windows")'.dependencies]
windows.workspace = true

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