Compare commits

..

134 Commits

Author SHA1 Message Date
Antonio Scandurra
1e3063b65e WIP 2024-07-11 14:20:46 +02:00
Max Brunsfeld
6b3d53159e WIP - Start work on following items in panels 2024-07-10 18:10:33 -07:00
Max Brunsfeld
1641f0f73d Add the ability to follow into context editors when they're in a center pane 2024-07-10 17:22:58 -07:00
Antonio Scandurra
8f3cb69daa Introduce FollowableViewRegistry
Co-Authored-By: Max <max@zed.dev>
2024-07-10 19:47:51 +02:00
Antonio Scandurra
6bebf42ed7 Add FollowableItem::dedup
Co-Authored-By: Max <max@zed.dev>
2024-07-10 18:37:17 +02:00
Antonio Scandurra
abf9f86fb7 Don't pass pane to FollowableItemHandle::from_state_proto 2024-07-10 18:37:17 +02:00
Antonio Scandurra
6591344b60 WIP 2024-07-10 18:37:16 +02:00
Antonio Scandurra
8bfab8fef6 WIP 2024-07-10 18:37:16 +02:00
sgj123456
8a659af82c gpui_macros: Enable extra-traits feature for syn (#14067)
Must enable extra-traits of syn feature to enable Debug trait of
Visibility

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-07-10 12:32:46 -04:00
Kyle Kelley
07dc4050bf Do not bind cmd-enter for repl::Run when in AssistantContext (#14066)
Don't pollute cmd-enter in the Assistant's `ContextEditor`.

Release Notes:

- N/A
2024-07-10 09:25:07 -07:00
Kyle Kelley
896b9bda23 Stick REPL icon in quick action bar (#14064)
REPL Quick Actions

<img width="325" alt="image"
src="https://github.com/zed-industries/zed/assets/836375/faaf4c8f-ef12-4417-a9dd-158d5beae8ba">

When the Jupyter REPL is enabled and a kernel is available, show the
status in the editor bar:

![quick action bar
repl](https://github.com/zed-industries/zed/assets/836375/f3445283-f1fc-4714-895b-7aa842d4ab76)


Release Notes:

- N/A

---------

Co-authored-by: Nate Butler <iamnbutler@gmail.com>
2024-07-10 09:20:52 -07:00
Conrad Irwin
9282bf97ae Default linux to stable (#14061)
Release Notes:

- linux: default install.sh to stable
2024-07-10 10:10:16 -06:00
Piotr Osiewicz
33a67ad6b9 chore: Clippy fixes for 1.80 (#13987)
The biggest hurdle turned out to be use of `Arc<Language>` in maps, as
`clippy::mutable_key_type` started triggering on it (due to - I suppose
- internal mutability on `HighlightMap`?). I switched over to using
`LanguageId` as the key type in some of the callsites, as that's what
`Language` uses anyways for it's hash/eq, though I've still had to
suppress the lint outside of language crate.

/cc @maxdeviant , le clippy guru.

Release Notes:

- N/A
2024-07-10 17:53:17 +02:00
Jason Lee
d4ddc4c62c gpui: Fix cx.bounds, cx.open_window position on macOS (#14044)
Release Notes:

- gpui: Fixed `cx.bounds` method to get correct `y` position on macOS.
- gpui: Fixed `cx.open_window` position when macOS Dock is existed.
- Fixed call notification and reopen window position.

## Before


![image](https://github.com/zed-industries/zed/assets/5518/4a435ffd-d7ef-4de7-a7de-44d21db4a719)


https://github.com/zed-industries/zed/assets/5518/ab925779-4253-4b27-9084-01023888087f


## After

<img width="533" alt="image"
src="https://github.com/zed-industries/zed/assets/5518/142e9aaa-ae82-4a72-9acf-04097c545bf0">


https://github.com/zed-industries/zed/assets/5518/8793824a-8b74-4913-8204-7b39649aeeed


---

The case is I have made a Popover by use child window, the coordinate of
the window is always can't placement a right position.

So, I make this example to test the `cx.bounds` and set bounds to
window.

---

By this test, is the `cx.bounds` have a bug?

For example the **Top Left** window, we give it origin (150,150), but it
`cx.bounds()` returns (150,262)

> On the window label, middle line is the `bounds` that we set to the
window, last line is `cx.bounds()` result.

Display 1:

<img width="1512" alt="CleanShot 2024-07-10 at 14 52 26@2x"
src="https://github.com/zed-industries/zed/assets/5518/3adf9e79-f237-431a-a72b-02face7b2361">


---

Or is there something I missed. Is it correct to use `cx.bounds` method
to get the bounds of the current window?

At the same time, I also found that when there are multiple screens, the
information obtained by cx.bounds is very different on different
screens, and it seems that the origin is not relative to the screen.

Display 2:

<img width="2560" alt="SCR-20240710-nkmq"
src="https://github.com/zed-industries/zed/assets/5518/d87d4151-0562-4bf8-b3b3-5da3b4d09d82">
2024-07-10 09:52:33 -06:00
Antonio Scandurra
8944af7406 Lay the groundwork for collaborating on assistant panel (#13991)
This pull request introduces collaboration for the assistant panel by
turning `Context` into a CRDT. `ContextStore` is responsible for sending
and applying operations, as well as synchronizing missed changes while
the connection was lost.

Contexts are shared on a per-project basis, and only the host can share
them for now. Shared contexts can be accessed via the `History` tab in
the assistant panel.

<img width="1819" alt="image"
src="https://github.com/zed-industries/zed/assets/482957/c7ae46d2-cde3-4b03-b74a-6e9b1555c154">


Please note that this doesn't implement following yet, which is
scheduled for a subsequent pull request.

Release Notes:

- N/A
2024-07-10 17:36:22 +02:00
张小白
1662993811 windows: Revert "windows: Fix font clipping issue" (#14045)
The implemetation of that PR is totally wrong, sorry for that!

Release Notes:

- N/A
2024-07-10 08:19:29 -07:00
Thorsten Ball
7ef64fe6db vim: Add ctrl-m binding (equivalent to <CR>) (#14057)
Now that we have macros I noticed how much I rely on this.

Release Notes:

- vim: `ctrl-m` now is equivalent to `enter` in editor.
2024-07-10 16:31:54 +02:00
Joseph T Lyons
f147722fe0 v0.145.x dev 2024-07-10 10:12:35 -04:00
Daniel Schmidt
e1a6efa609 go: Quote targeting expression on runnables (#14055)
Release Notes:

- Go: fix test runnables in fish shell.
2024-07-10 16:02:29 +02:00
Saurabh
ba7d5a3d4c Fixed keymap for toggling right dock in linux (#14041)
Release Notes:

- N/A
2024-07-10 10:49:28 +02:00
Piotr Osiewicz
6f99399224 extensions: Add support for snippets provided by extensions (#14020)
For now extensions can only register global snippets, but there'll be
follow-up work to support scope attribute in snippets.json.

Release Notes:

- Extensions can now provide snippets by including `snippets.json` file
next to the extension manifest.

---------

Co-authored-by: Marshall <marshall@zed.dev>
Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-07-10 09:40:50 +02:00
CharlesChen0823
2f2047ab22 outline_panel: Fix outline panel should autoscroll when selection has changed (#14038)
Fixed selection changed, outline panel not autoscroll.

Release Notes:

- N/A
2024-07-10 10:12:09 +03:00
Marshall Bowers
d01d76482d gpui: Expose more granular style macros (#14035)
This PR extract more GPUI style methods into macros that can be composed
together to selectively add styles to components.

Release Notes:

- N/A
2024-07-09 19:49:25 -04:00
Marshall Bowers
a46a562dc2 ui: Add margin style methods to Label and LabelLike (#14032)
This PR adds margin style methods to the `Label` and `LabelLike`
components.

This allows for callers to provide a margin to these components without
needing to introduce a wrapping `div` to do so.

Release Notes:

- N/A
2024-07-09 17:41:54 -04:00
Kyle Kelley
4bb8a0845f Measure maximum width of each cell to render table (#14026) 2024-07-09 14:19:10 -07:00
Marshall Bowers
c4bca874b6 assistant: Replace margin with gap (#14027)
This PR replaces a usage of margin with a gap. This allows us to remove
an extra wrapping `div`.

Release Notes:

- N/A
2024-07-09 17:08:29 -04:00
Mikayla Maki
46c0aa5fc2 Update README.md 2024-07-09 14:05:29 -07:00
Marshall Bowers
2db06c1567 assistant: Remove unneeded wrapping div in ModelSelector (#14024)
This PR removes an unneeded wrapping `div` in the `ModelSelector`.

Release Notes:

- N/A
2024-07-09 16:40:00 -04:00
Joe Fitzgibbons
c59d5fbae7 Update .dockerignore (#14016)
Release Notes:

- N/A
2024-07-09 16:27:55 -04:00
Mikayla Maki
8df098ff35 Update linux.md 2024-07-09 13:03:53 -07:00
Mikayla Maki
639b21a7c5 Update README.md 2024-07-09 13:01:15 -07:00
Conrad Irwin
c65673feae Fix linux prompts (#14021)
Release Notes:

- N/A
2024-07-09 13:00:33 -07:00
Mikayla Maki
6c9da838b7 Update README.md 2024-07-09 12:57:49 -07:00
Mikayla Maki
a173beeb81 Set minversion to next Zed Linux release 2024-07-09 12:55:29 -07:00
Mikayla Maki
002ce6c343 Update README.md 2024-07-09 12:51:52 -07:00
Mikayla Maki
2c30b8836e Update README.md 2024-07-09 12:46:46 -07:00
Conrad Irwin
0d527dfb9e Better zsh install fix (#14017)
Fix it on linux too 🤦

Release Notes:

- N/A
2024-07-09 13:34:32 -06:00
Peter Tripp
110ce8a267 Python: Fix auto close for single quotes (#14014)
Fixes #13972
2024-07-09 15:27:57 -04:00
Marshall Bowers
c6b9f1920f Remove additional wrapping elements in the chat panel (#14013)
This PR removes some wrapping elements that were used inside of the chat
panel.

To facilitate this, the `Label` component now has a `weight` method to
change the font weight.

Release Notes:

- N/A
2024-07-09 15:27:37 -04:00
张小白
df935df5a3 windows: Obtain mouse double-click information from the system instead of hardcoding (#13391)
Release Notes:

- N/A
2024-07-09 12:17:55 -07:00
Matin Aniss
68b5ea4e60 windows: Fix and simplify title bar padding (#13420)
This PR fixes the off by one pixel of the top client rect when not
maximized due to the added border. It also simplifies and properly fixes
the title bar padding problem when maximized, it is now properly taken
care of in GPUI rather then adding the padding in the UI.

Release Notes:

- N/A
2024-07-09 12:15:15 -07:00
Aleksei Gusev
5e1521eded Change the default shortcut for git blame on Linux (#13637)
Zed already has a shortcut assigned to ctrl-alt-g and it's mapped to
`search::SelectNextMatch`. Having another multi shortcut with the same
prefix makes `ctrl-alt-g` to have a very noticeable delay when pressed.

This commit changes the default shortcut for git blame to `alt-g b`

Release Notes:

- N/A
2024-07-09 12:13:20 -07:00
Mikayla Maki
ba28827de5 Add support for numpad keys on linux (#14018)
Fixes https://github.com/zed-industries/zed/issues/12117

Partial application of the changes in
https://github.com/zed-industries/zed/pull/13396

Release Notes:

- N/A
2024-07-09 12:07:48 -07:00
张小白
c22dbbebe2 windows: Fix tailwindcss-language-server (#13891)
We should run this server with `powershell`, or we will get some runtime
errors.

![Screenshot 2024-07-06
180154](https://github.com/zed-industries/zed/assets/14981363/e272e146-d4a8-4447-aa65-b657a49622de)


Release Notes:

- Fixed `tailwindcss-language-server` on Windows.
2024-07-09 12:07:20 -07:00
张小白
8f29ff8a63 windows: Fix font clipping issue (#13854)
Closes #12737 . Left before this PR, right after.

![Screenshot 2024-07-05
180308](https://github.com/zed-industries/zed/assets/14981363/437baec3-2672-4b19-8595-17a6c564506e)


Release Notes:

- Fixed font rendering clipping issue on Windows.(#12737 )
2024-07-09 12:06:14 -07:00
Jason Lee
ae414e21f0 gpui: Fix hide titlebar on Windows, with titlebar: None option (#13975)
Release Notes:

- N/A

Ref the macOS Window:

When the `titlebar` is none, the titlebar should be hidden.


adaa483176/crates/gpui/src/platform/mac/window.rs (L516-L528)

```
cargo run -p gpui --example window_positioning
```

## Before


![img_v3_02ck_939c23d0-acc6-40c1-aaf7-c6dd73ddf7ag](https://github.com/zed-industries/zed/assets/5518/f2c7aa02-a102-4f24-8243-74219957d16b)

## After

<img width="466" alt="image"
src="https://github.com/zed-industries/zed/assets/5518/176ce4ea-14e9-44c8-8f2d-01e20ff3e543">
2024-07-09 12:04:55 -07:00
francesco-gaglione
2dd486733b Reveal in files instead of Finder (#13432)
fixes: #12776 

Release Notes:

- Renamed `editor::RevealInFinder` to `editor::RevealInFileManager`

---------

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2024-07-09 11:54:14 -07:00
Mikayla Maki
f44e81b3b5 Add more package managers to docs (#14015)
Release Notes:

- N/A
2024-07-09 11:47:58 -07:00
Andy Weiss
c093bc8aa8 Fix search/replace start of line anchor (#13920)
This is related to #9428 

I noticed that doing a search and replace for the beginning of a line
`^` results in the trailing line being included in the search. This
seems to be because of the way the range is generated for generating
matches being the up to the start of the trailing line rather than up to
the end of the last line.

I added a test and took a stab at fixing it but it is a bit yolo as this
is the first time I've seen this codebase.
2024-07-09 12:39:24 -06:00
Piotr Osiewicz
09e7b481b8 lsp: Add support for ShowMessage notification (#14012)
When "one newer language" sends these messages, "one newer editor" will
display a pop-up for users to see. :)

Related to https://github.com/gleam-lang/gleam/issues/3274


![image](https://github.com/zed-industries/zed/assets/24362066/00d2c168-59f0-4033-91c8-af29c47516b3)

Release Notes:

- A certain popular language recently had to work around a missing LSP
notification. This has been fixed
2024-07-09 20:12:05 +02:00
Conrad Irwin
8cfa690271 Fix transparency (#14010)
Release Notes:

- (preview only) Fix transparent themes
2024-07-09 12:11:18 -06:00
Marshall Bowers
3cdd465226 gpui: Make style macros more composable (#14007)
This PR begins the process of breaking up the `style_helpers!` macro
into smaller macros that can be used to generate methods for a related
subset of styles.

The style method macros also now accept an optional `visibility`
parameter to control the visibility of the generated methods. This
allows for adding these methods to a struct instead of a just a trait.

For example, to expose just the padding styles on a `Facepile` we can do
this:

```rs
impl Facepile {
    fn style(&mut self) -> &mut StyleRefinement {
        self.base.style()
    }

    gpui::padding_style_methods!({
        visibility: pub
    });
}
```

Release Notes:

- N/A
2024-07-09 13:52:52 -04:00
张小白
8203b6875b windows: Remove more todos (#13818)
Release Notes:

- N/A
2024-07-09 10:44:42 -07:00
Antonio Scandurra
ce7074c883 Fix panic when opening the same context twice (#14004)
Release Notes:

- Fixed a crash that occurred when opening the same context twice in the
assistant panel (preview-only).
2024-07-09 19:10:20 +02:00
Peter Tripp
6cc8412a05 Prevent dumping of temporary files in config_dir (#14002)
Move telemetry temp files from `config_dir` to `log_dir`. Fixes #7155 

- On MacOS: from `~/.config/zed` to `~/Library/Logs/Zed`
- On Linux: from `~/.config/zed` to `.local/share/zed/logs` (or
`$FLATPAK_XDG_DATA_HOME/zed/logs`).

Release Notes:

- Fixed telemetry putting temporary files in config_dir
([#7155](https://github.com/zed-industries/zed/issues/7155)).
2024-07-09 12:59:17 -04:00
Stanislav Alekseev
2a97aad273 Fix scrolling sticking to top (#13874)
The problem seemingly was that scrolling only started after autoscroll
has finished. I have added a function to forcefully stop it, which I
call when scroll event happens
Release Notes:

- Fixed delay when changing scrolling direction (#13720)

---------

Co-authored-by: Piotr <piotr@zed.dev>
2024-07-09 18:44:46 +02:00
Marshall Bowers
275dd3fa81 Remove extraneous Cargo.lock files (#14001)
This PR removes some extraneous `Cargo.lock` files for the `storybook`
and `sqlez` crates.

These lockfiles were not used, as everything uses the workspace's
`Cargo.lock`.

Release Notes:

- N/A
2024-07-09 12:15:34 -04:00
Marshall Bowers
3cb2a1404c gpui_macros: Refactor style helpers (#13999)
This PR refactors the style definitions in the `gpui_macros` style
helpers to use structs instead of tuples for additional clarity.

Release Notes:

- N/A
2024-07-09 11:35:54 -04:00
Peter Tripp
dd9b2e2cde PR template: Make issue numbers double clickable (no brackets) (#13989)
Release Notes:

- N/A
2024-07-09 11:30:06 -04:00
Nate Butler
b691d1baf2 Improve experience when themes provide transparent status colors (#13996)
We shouldn't assume all themes will give us solid status color
backgrounds.

This change makes it so the status color renders on top of a normal
elevated surface background.

#### Before | After (Transparent status background color – Fixed)

![CleanShot 2024-07-09 at 10 50
17@2x](https://github.com/zed-industries/zed/assets/1714999/5f4b24c1-335a-4ed8-a1d0-f511e217e4a5)

![CleanShot 2024-07-09 at 10 50
31@2x](https://github.com/zed-industries/zed/assets/1714999/38c06533-bda5-4cfb-822a-ed5a9639fc33)

---

#### Before | After (Solid status background color – No change)

![CleanShot 2024-07-09 at 10 49
43@2x](https://github.com/zed-industries/zed/assets/1714999/bd60c807-a7bb-4f60-ab47-ddba17288e93)

![CleanShot 2024-07-09 at 10 49
58@2x](https://github.com/zed-industries/zed/assets/1714999/6ab27d60-5a77-448c-a23b-569b337f11e1)



Release Notes:

- Improved support for transparent status colors in themes.
2024-07-09 11:27:47 -04:00
Conrad Irwin
bc0359a474 gpui: Input example log keystrokes (#13963)
Release Notes:

- N/A
2024-07-09 08:33:29 -06:00
apricotbucket28
23c84f8dc0 linux: Treat fullscreen as tiled on X11 and prevent resizing while maximized (#13990)
Two quick fixes for issues I noticed:

1. Fullscreening an unmaximized X11 window still showed rounded window
corners and allowed resizing
2. Maximized windows still allowed for resizing on corners due to
missing checks

![image](https://github.com/zed-industries/zed/assets/71973804/47df4de2-4013-4e51-88c3-d33b52a909f5)


Release Notes:

- N/A
2024-07-09 16:30:30 +02:00
Danilo Leal
29226170f1 docs: Add tiny tweaks to the Linux page (#13994)
Release Notes:

- N/A
2024-07-09 11:26:22 -03:00
Peter Tripp
9a523ef730 Fix keybind conflicts (atom mac/linux default) (#13988)
- atom(mac): Cmd+j conflicts with `workspace: ToggleBottomDock` in
default map. Revert.
- default(linux): `ctrl-shift-t` conflict. Move
`project_symbols::Toggle` to `ctrl-t` to match vscode linux. Leave
`pane::ReopenClosedItem` at `ctrl-shift-t` to match vscode/chrome on
linux.
- Fixes #13973
2024-07-09 10:02:21 -04:00
Kirill Bulatov
9b688655a8 Add a way to filter items in the outline panel (#13984)
https://github.com/zed-industries/zed/assets/2690773/145a7cf2-332c-46c9-ab2f-42a77504f54f

Adds a way to filter entries in the outline panel, by showing all
entries (even if their parents were collapsed) that fuzzy match a given
query.

Release Notes:

- Added a way to filter items in the outline panel
2024-07-09 16:44:24 +03:00
Piotr Osiewicz
9a6f30fd95 Snippets: Move snippets into the core of editor (#13937)
Release Notes:

- Move snippet support into core editor experience, marking the official
extension as deprecated. Snippets now show up in any buffer (including
plain text buffers).
2024-07-09 14:02:36 +02:00
Thorsten Ball
b3dad0bfcb Revert "x11: Differentiate between mouse and keyboard focus #13943" (#13974)
This reverts #13943 and reopens #13897 since the fix in #13943 comes
with a regression:

Sometimes Zed loses keyboard focus and can't be restored. I haven't
figured out yet exactly when and how this happens and can't reliably
reproduce it yet, but there's something off with focus handling.

One reliable way to reproduce _one_ of the problems:

1. Open two zed windows
2. Focus one Zed window
3. Hover with the mouse over the other
4. Try to type in the window that should still be focused

So, to be careful, I'm going to revert the PR first, since I couldn't
find an obvious fix yet. If we do find a fix, we can unrevert.


Release Notes:

- N/A
2024-07-09 11:12:42 +02:00
Aleksei Gusev
18d6be250f Add keyboard shortcuts for the prompts on Linux (#13915)
This change adds ability to choose any action from prompts, not just the
default one and cancel as Zed has right now. For example, when a user
tries to close a file with edits in it the prompt offers "Don't save"
option that can be selected only with mouse. Now you can use arrows,
tab/shift-tab to pick action and enter/space to confirm it.

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


Release Notes:

- Added keyboard navigation in the prompts on Linux
([#13906](https://github.com/zed-industries/zed/issues/13906)).


Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
2024-07-09 10:50:13 +02:00
killian
5e1c690888 Add Nix/NixOS dev-shell (#13407)
This PR adds a Nix/NixOS development-shell (`shell.nix`), which is based
on the upstream
[nixpkgs](c5d4d45811/pkgs/by-name/ze/zed-editor/package.nix),
as well as its corresponding `flake.nix` file.

To use it, run either the `nix-shell` command (uses the `shell.nix`
file), or the newer but experimental `nix develop` command (uses
`flake.nix`)

~~This has not been tested on macOS, tho preliminary code is there to
try and support it, feel free to report any issues.~~ Zed unfortunately
doesn't build on nix-darwin (see
https://github.com/NixOS/nixpkgs/issues/320084), so this PR doesn't aim
to add darwin support.

---

Release Notes:

- N/A

---------

Signed-off-by: xtrm <oss@xtrm.me>
Co-authored-by: Niklas Korz <niklas@niklaskorz.de>
2024-07-09 09:21:42 +02:00
Matt Fellenz
034d905435 Allow vim counts with undo and redo (#13950)
These were previously passed directly to the editor module, which knows
nothing about vim counts. Instead, implement new actions in the vim
module which take the count and use it to invoke the corresponding
action in the editor module, properly repeated.

Release Notes:

- Fixed vim undo and redo commands not taking counts.
2024-07-08 23:16:52 -06:00
Conrad Irwin
0d7bd0c535 vim: Disable default ctrl-x/ctrl-w on linux (#13966)
Release Notes:

- N/A
2024-07-08 22:36:21 -06:00
Stanislav Alekseev
ed50dea042 Only clear selections when right click was performed outside of selection (#13701)
Release Notes:

- Fixed selections being cleared when right-click was performed outside
of a selection
([#4267](https://github.com/zed-industries/zed/pull/13701)).

<img width="1136" alt="Screenshot 2024-07-01 at 16 53 58"
src="https://github.com/zed-industries/zed/assets/43210583/082bfb0a-c679-4e87-a4e8-7dd751d8f4a2">
2024-07-08 22:07:09 -06:00
Conrad Irwin
5c95d2806b Ensure people who hit /linux directly have the right instructions (#13959)
Release Notes:

- N/A
2024-07-08 21:43:12 -06:00
Conrad Irwin
05e2e4d929 Send IME-supported key downs (#13964)
Release Notes:

- N/A
2024-07-08 21:37:19 -06:00
Sebastijan Kelnerič
30479bf062 Improve window decorations: check for compositor support (#13822)
Adds the `compositor_support` to the `X11WindowState` struct so that
correct window decorations are selected

Release notes:

- N/A

---------

Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-07-08 20:34:37 -06:00
Conrad Irwin
a40a16ab98 zsh instructions too (#13944)
Release Notes:

- N/A
2024-07-08 20:18:03 -06:00
Conrad Irwin
2e7db8f855 Add mouse handling to gpui input example (#13960)
Release Notes:

- N/A
2024-07-08 20:04:28 -06:00
Conrad Irwin
b0ecda6370 x11 calloop 2 (#13955)
Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
2024-07-08 18:38:36 -06:00
Marshall Bowers
efc2336be5 title_bar: Factor out collab-related code into collab module (#13957)
This PR refactors the `TitleBar` component to move all of the
collab-related code into the `collab` module.

This simplifies the top-level `Render` implementation of `TitleBar` by a
lot and makes it easier to read.

Release Notes:

- N/A
2024-07-08 19:43:20 -04:00
Marshall Bowers
9e36a66fec ui: Add NumericStepper component (#13954)
This PR adds a `NumericStepper` component that can be used to display a
numeric value along with controls to increment, decrement, and reset the
value.

The `ApplicationMenu` has been updated to use the `NumericStepper` for
adjusting the buffer and UI font size.

Here it is in action:


https://github.com/zed-industries/zed/assets/1486634/03cffe67-1256-4283-aa3d-560fffa06dad

Note: Due to the way we do font adjustments, once modified the reset
button will be displayed until it is clicked (or the font size
adjustment is otherwise reset). Simply returning to the original value
will currently not hide the reset button.

Release Notes:

- N/A
2024-07-08 18:45:49 -04:00
Conrad Irwin
97f315356d Linux docs (#13945)
Release Notes:

- N/A

---------

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-07-08 15:29:28 -07:00
apricotbucket28
0b6ef995d4 wayland: Implement activate() API and use portals to open URLs and paths (#13336)
This PR consists of two main changes:
1. The first commit changes the `open` crate for opening URLs/paths for
the `OpenURI` desktop portal. This fixes the activation token not being
passed to programs (at least on KDE).
2. The second commit implements the window `activate()` API on Wayland.
This allows KWin and Mutter to show a visual indicator when the window
is requesting attention. (see
https://github.com/zed-industries/zed/issues/12557)

![image](https://github.com/zed-industries/zed/assets/71973804/ce148f8e-28fd-4249-8f8d-3a5828ed6f83)


Release Notes:

- N/A
2024-07-08 15:29:13 -07:00
Nate Butler
414cff5c14 One Dark Updates (#13777)
Before | After

![CleanShot 2024-07-03 at 10 25
24@2x](https://github.com/zed-industries/zed/assets/1714999/7b355217-7dcf-416a-8b6f-63a12d3f2ea7)

Release Notes:

- Improved contrast between some items in the One Dark theme.
2024-07-08 18:01:51 -04:00
Marshall Bowers
2925f3d33c Rename ui_text_field crate to ui_input (#13949)
This PR renames the `ui_text_field` crate to `ui_input` to make it a bit
more generic.

We'll likely end up with multiple kinds of input components in this
crate.

Release Notes:

- N/A
2024-07-08 17:05:30 -04:00
Marshall Bowers
1a0242eff7 Add story for ApplicationMenu (#13948)
This PR adds a story for the `ApplicationMenu` so it can be viewed in
isolation.

<img width="664" alt="Screenshot 2024-07-08 at 4 45 24 PM"
src="https://github.com/zed-industries/zed/assets/1486634/dca3dd32-4845-4009-b781-b4bac9ba6049">

Release Notes:

- N/A
2024-07-08 16:55:55 -04:00
Marshall Bowers
ea9ba6863d title_bar: Factor out application menu into its own component (#13947)
This PR factors out the application menu in the title bar into its own
component.

Release Notes:

- N/A
2024-07-08 16:34:00 -04:00
Peter Tripp
af697d9cc2 Better tooltips for back/forward (#13946)
Fixes #8459
2024-07-08 16:33:41 -04:00
apricotbucket28
75377bbe0f x11: Differentiate between mouse and keyboard focus (#13943)
Fixes https://github.com/zed-industries/zed/issues/13897

Release Notes:

- N/A
2024-07-08 13:59:30 -06:00
Mikayla Maki
032b203519 Separate out macOS and Linux keymaps (#13792)
Release Notes:

- Added Linux-Specific keymaps for JetBrains, Atom and Sublime Text
- Improved MacOS-specific keymaps for JetBrains and Atom
- Improved Linux default keymap (VSCode compatibility)
- Windows now uses same keymap as Linux

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2024-07-08 15:05:29 -04:00
Marshall Bowers
f555f66a8c clojure: Bump to v0.0.3 (#13935)
This PR bumps the Clojure extension to v0.0.3.

Changes:

- https://github.com/zed-industries/zed/pull/13914
- https://github.com/zed-industries/zed/pull/13933

Release Notes:

- N/A
2024-07-08 11:29:07 -04:00
Joey Riches
d3f869acd8 scripts/flatpak: Escape XML characters in convert-release-notes.py (#13801)
Resolves #13791

Release Notes:

- N/A
2024-07-08 09:25:17 -06:00
Marshall Bowers
c617d48e16 clojure: Upgrade zed_extension_api to v0.0.6 (#13933)
This PR upgrades the Clojure extension to use v0.0.6 of the
`zed_extension_api`.

Release Notes:

- N/A
2024-07-08 11:07:53 -04:00
Ghostylab
7f50055d70 windows: Replace symlinks with files for TSX Tree-sitter queries (#13893)
Previously to these changes, as stated in the issue, when someone tried
to use TSX as language for the file, the language was not set and it
disappeared from the selectable language list. This was due to the fact
that that the three files were symlinks, and windows couldn't read them.
I replaced them with normal files.

Fixes #12208.

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-07-08 10:32:20 -04:00
zachcp
c5e5add094 clojure: Recognize ClojureDart files (#13914)
Added support for ClojureDart (cljd) files in the Clojure Extension

Release Notes:

- N/A
2024-07-08 10:14:49 -04:00
fsh
65e463b599 x11: Properly update XKB group state (#13931)
Pass on all the XkbStateNotify information to XKB.

> "All parameters must always be passed, or the resulting state may be
incoherent."
>
https://docs.rs/xkbcommon/latest/xkbcommon/xkb/struct.State.html#method.update_mask

Previously, many keymaps using multiple groups/layers would not work and
remain in group0.

Release Notes:

- Fixed handling of Xkb keymap groups on X11.
2024-07-08 07:14:07 -07:00
Aleksei Gusev
11c7374f76 Use user-defined font weight in terminal (#13926)
Related #13653

Release Notes:

- Fixed honoring of the `terminal.font_weight` user setting
2024-07-08 14:43:23 +03:00
Thorsten Ball
cd7268f21f linux: Add tracing logs to the x11 client and linux dispatcher (#13928)
This adds some tracing logging that can be toggled on to debug issues.

How I used it:

```
RUST_LOG=gpui=trace cargo run
```


Release Notes:

- N/A
2024-07-08 12:39:04 +02:00
Thorsten Ball
a1eaf1bb3c tailwind: Check user settings for classAttributes (#13923)
This addresses the question in [this

comment](https://github.com/zed-industries/zed/issues/5830#issuecomment-2211942554)
by adding support for `classAttributes` to the settings.

Meaning that the following Zed `settings.json` now works:

```jsonc
{
  "lsp": {
    "tailwindcss-language-server": {
      "settings": {
        "classAttributes": [
          "class",
          "className",
          "ngClass",

          // add styles so will give intellisense to styles constant.
          "styles"
        ]
        // Optional:
        // "includeLanguages": {
        //   "erb": "html",
        //   "ruby": "html"
        // },
        // "experimental": {
        //   "classRegex": ["\\bclass:\\s*['\"]([^'\"]*)['\"]"]
        // }
      }
    }
  }
}
```



Release Notes:

- Added support for setting `classAttributes` in the configuration for
`tailwindcss-language-server`. Example: `{ "lsp": {
"tailwindcss-language-server": { "settings": { "classAttributes": [
"class", "className", "ngClass", "styles" ] } } } }`
2024-07-08 08:39:08 +02:00
jansol
ab83820b6e linux: Remove StartupWMClass from .desktop file, add NewWorkspace action (#13807)
Release Notes:

- N/A
2024-07-08 08:31:08 +02:00
Taimuraz Kaitmazov
800bdf34d5 linux: Fix dropping action when action is just started (#13840)
```
Thread "main" panicked with "divide by zero error when dividing duration by scalar" at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/core/src/time.rs:1172:31
   0: zed::reliability::init_panic_hook::{{closure}}
             at crates/zed/src/reliability.rs:58:29
   1: <alloc::boxed::Box<F,A> as core::ops::function::Fn<Args>>::call
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/alloc/src/boxed.rs:2036:9
      std::panicking::rust_panic_with_hook
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/std/src/panicking.rs:799:13
   2: std::panicking::begin_panic_handler::{{closure}}
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/std/src/panicking.rs:664:13
   3: std::sys_common::backtrace::__rust_end_short_backtrace
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/std/src/sys_common/backtrace.rs:171:18
   4: rust_begin_unwind
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/std/src/panicking.rs:652:5
   5: core::panicking::panic_fmt
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/core/src/panicking.rs:72:14
   6: core::panicking::panic_display
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/core/src/panicking.rs:263:5
   7: core::option::expect_failed
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/core/src/option.rs:1994:5
   8: core::option::Option<T>::expect
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/core/src/option.rs:895:21
      <core::time::Duration as core::ops::arith::Div<u32>>::div
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/core/src/time.rs:1172:31
   9: <gpui::platform::linux::wayland::client::WaylandClientStatePtr as wayland_client::event_queue::Dispatch<wayland_client::protocol::wl_keyboard::WlKeyboard,()>>::event::{{closure}}
             at crates/gpui/src/platform/linux/wayland/client.rs:1211:63
  10: <core::cell::RefCell<calloop::sources::DispatcherInner<S,F>> as calloop::sources::EventDispatcher<Data>>::process_events::{{closure}}
             at /home/atassis/.cargo/registry/src/index.crates.io-6f17d22bba15001f/calloop-0.13.0/src/sources/mod.rs:327:61
  11: <calloop::sources::timer::Timer as calloop::sources::EventSource>::process_events
             at /home/atassis/.cargo/registry/src/index.crates.io-6f17d22bba15001f/calloop-0.13.0/src/sources/timer.rs:122:38
  12: <core::cell::RefCell<calloop::sources::DispatcherInner<S,F>> as calloop::sources::EventDispatcher<Data>>::process_events
             at /home/atassis/.cargo/registry/src/index.crates.io-6f17d22bba15001f/calloop-0.13.0/src/sources/mod.rs:326:9
  13: calloop::loop_logic::EventLoop<Data>::dispatch_events
             at /home/atassis/.cargo/registry/src/index.crates.io-6f17d22bba15001f/calloop-0.13.0/src/loop_logic.rs:445:31
  14: calloop::loop_logic::EventLoop<Data>::dispatch
             at /home/atassis/.cargo/registry/src/index.crates.io-6f17d22bba15001f/calloop-0.13.0/src/loop_logic.rs:559:9
  15: calloop::loop_logic::EventLoop<Data>::run
             at /home/atassis/.cargo/registry/src/index.crates.io-6f17d22bba15001f/calloop-0.13.0/src/loop_logic.rs:596:13
  16: <gpui::platform::linux::wayland::client::WaylandClient as gpui::platform::linux::platform::LinuxClient>::run
             at crates/gpui/src/platform/linux/wayland/client.rs:655:9
  17: gpui::platform::linux::platform::<impl gpui::platform::Platform for P>::run
             at crates/gpui/src/platform/linux/platform.rs:153:9
  18: gpui::app::App::run
             at crates/gpui/src/app.rs:140:9
  19: zed::main
             at crates/zed/src/main.rs:382:5
  20: core::ops::function::FnOnce::call_once
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/core/src/ops/function.rs:250:5
  21: std::sys_common::backtrace::__rust_begin_short_backtrace
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/std/src/sys_common/backtrace.rs:155:18
  22: std::rt::lang_start::{{closure}}
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/std/src/rt.rs:159:18
  23: core::ops::function::impls::<impl core::ops::function::FnOnce<A> for &F>::call_once
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/core/src/ops/function.rs:284:13
      std::panicking::try::do_call
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/std/src/panicking.rs:559:40
      std::panicking::try
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/std/src/panicking.rs:523:19
      std::panic::catch_unwind
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/std/src/panic.rs:149:14
      std::rt::lang_start_internal::{{closure}}
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/std/src/rt.rs:141:48
      std::panicking::try::do_call
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/std/src/panicking.rs:559:40
      std::panicking::try
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/std/src/panicking.rs:523:19
      std::panic::catch_unwind
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/std/src/panic.rs:149:14
      std::rt::lang_start_internal
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/std/src/rt.rs:141:20
  24: std::rt::lang_start
             at /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/std/src/rt.rs:158:17
  25: main
  26: __libc_start_call_main
  27: __libc_start_main_impl
  28: _start
```
This error was happening when I started typing. This PR fixes this
error.
Fedora 40, latest kernel, gnome 46, wayland.


Release Notes:

- N/A
2024-07-08 07:40:09 +02:00
Vince
79f3646325 Add missing clipboard shortcut to default-linux.json (#13900)
Bind shift-delete to editor::Cut (similar to #11799, which added copy
and paste, but not cut)


Release Notes:

- N/A
2024-07-07 14:40:23 +03:00
Peter Tripp
813cc3f5e5 Keymap oneliners (#13887)
This is should be a no-op, whitespace formatting only.
Removes 425 lines of excess whitespace in our default keymap json files.

Release Notes:

- Improved formatting of default keymaps (single line per bind)
2024-07-05 22:00:18 -04:00
Marshall Bowers
c190ed49da Treat flake.lock files as JSON (#13886)
This PR updates the default settings to treat Nix's `flake.lock` files
as JSON.

Resolves https://github.com/zed-extensions/nix/issues/2.

Release Notes:

- Nix's `flake.lock` files are now automatically identified as JSON.
2024-07-05 17:17:27 -04:00
apricotbucket28
5c7e6b7eff wayland: Fix window state issues (#13885)
Fixes some issues with the CSD added in
https://github.com/zed-industries/zed/pull/13611

Here's a video comparing the master branch (yellow icon) with this PR
(blue icon):


https://github.com/zed-industries/zed/assets/71973804/35be443a-8f24-4aed-910b-625bad9821e2

_Note: the flicker at the bottom of the window when maximizing is an
issue with the KDE floating task bar, it happens with all programs._

Release Notes:

- N/A
2024-07-05 13:42:11 -07:00
Kyle Kelley
1c1fd6aaa1 Finely scope repl events for runs and output clearing (#13872)
Sets up the `cmd-enter` keybinding for the jupyter repl to only apply
when enabled.

Release Notes:

- N/A

---------

Co-authored-by: Kirill <kirill@zed.dev>
2024-07-05 13:38:44 -07:00
Kyle Kelley
750df6c93d Clean up comments in runtimes (#13870) 2024-07-05 12:37:01 -07:00
Kirill Bulatov
a53b3b6b10 Explicitly specify php files' formatter for prettier (#13883)
Closes https://github.com/zed-industries/zed/issues/13878

Seems that all regular files types are being recognized by Prettier from
their paths, but the PHP one does not despite the PHP plugin used when
formatting.


Release Notes:

- Fixed PHP prettier formatting, by including `php` parser name into
formatting queries
([13878](https://github.com/zed-industries/zed/issues/13878))
2024-07-05 21:52:47 +03:00
Marshall Bowers
078ce330c6 lua: Bump to v0.0.3 (#13882)
This PR bumps the Lua extension to v0.0.3.

Changes:

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

Release Notes:

- N/A
2024-07-05 14:17:53 -04:00
Idris Saklou
19490d8806 lua: Add Windows support (#13871)
The current version of the extension tries to download the Windows
binary files for lua-language-server like this:
`lua-language-server-3.9.3-win32-x64.tar.gz`

The [Windows binary
files](https://github.com/LuaLS/lua-language-server/releases) are only
released as zip archives, so it will fail to get the required files.


This pr changes the following:
- Add check for Windows specific zip archive
- Add check for Windows specific .exe executable

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-07-05 14:11:11 -04:00
Marshall Bowers
61e4b6413a zed_extension_api: Return structured slash command completions (#13879)
This PR updates the extension API to use structured slash command
completions instead of plain strings.

This allows slash commands defined in extensions to take advantage of
the improvements made in #13876.

Release Notes:

- N/A
2024-07-05 14:08:42 -04:00
Marshall Bowers
950e7e5414 assistant: Clean up completion building in /docs command (#13877)
This PR cleans up the building of completions in the `/docs` command a
bit after #13876.

Release Notes:

- N/A
2024-07-05 13:42:27 -04:00
Marshall Bowers
68accaeb00 assistant: Improve /docs argument completions (#13876)
This PR improves the completions for arguments in the `/docs` slash
command.

We achieved this by extending the `complete_argument` method on the
`SlashCommand` trait to return a `Vec<ArgumentCompletion>` instead of a
`Vec<String>`.

In addition to the completion `label`, `ArgumentCompletion` has two new
fields that are can be used to customize the completion behavior:

- `new_text`: The actual text that will be inserted when the completion
is accepted, which may be different from what is shown by the completion
label.
- `run_command`: Whether the command is run when the completion is
accepted. This can be set to `false` to allow accepting a completion
without running the command.

Release Notes:

- N/A

---------

Co-authored-by: Antonio <antonio@zed.dev>
2024-07-05 13:29:17 -04:00
Marshall Bowers
ca27f42a9d extension: Don't use unzip to extract .zip files (#13869)
This PR replaces the usage of `unzip` for extracting `.zip` files
downloaded by extensions with extraction via a library.

This will allow us to extract `.zip` files even if `unzip` is not
available (e.g., on Windows).

Release Notes:

- Removed the need for `unzip` to be present on the system to extract
`.zip` files downloaded by extensions.
2024-07-05 11:38:48 -04:00
Marshall Bowers
d70c577293 Remove stray println! for window-side decorations (#13868)
This PR removes a stray `println!` that was spamming stdout with the
window decorations in use.

Release Notes:

- N/A
2024-07-05 11:19:05 -04:00
Kyle Kelley
c77ea47f43 Runtimes UI Starter (#13625)
Initial runtimes UI panel. The main draw here is that all message
subscription occurs with two background tasks that run for the life of
the kernel. Follow on to #12062

* [x] Disable previous cmd-enter behavior only if runtimes are enabled
in settings
* [x] Only show the runtimes panel if it is enabled via settings
* [x] Create clean UI for the current sessions

### Running Kernels UI

<img width="205" alt="image"
src="https://github.com/zed-industries/zed/assets/836375/814ae79b-0807-4e23-bc95-77ce64f9d732">

* [x] List running kernels
* [x] Implement shutdown
* [x] Delete connection file on `drop` of `RunningKernel`
* [x] Implement interrupt

#### Project-specific Kernel Settings

- [x] Modify JupyterSettings to include a `kernel_selections` field
(`HashMap<String, String>`).
- [x] Implement saving and loading of kernel selections to/from
`.zed/settings.json` (by default, rather than global settings?)

#### Kernel Selection Persistence

- [x] Save the selected kernel for each language when the user makes a
choice.
- [x] Load these selections when the RuntimePanel is initialized.

#### Use Selected Kernels

- [x] Modify kernel launch to use the selected kernel for the detected
language.
- [x] Fallback to default behavior if no selection is made.

### Empty states

- [x] Create helpful UI for when the user has 0 kernels they can launch
and/or 0 kernels running

<img width="694" alt="image"
src="https://github.com/zed-industries/zed/assets/836375/d6a75939-e4e4-40fb-80fe-014da041cc3c">

## Future work

### Kernel Discovery

- Improve the kernel discovery process to handle various installation
methods (system, virtualenv, poetry, etc.).
- Create a way to refresh the available kernels on demand

### Documentation:

- Update documentation to explain how users can configure kernels for
their projects.
- Provide examples of .zed/settings.json configurations for kernel
selection.

### Kernel Selection UI

- Implement a new section in the RuntimePanel to display available
kernels.
- Group on the language name from the kernel specification 
- Create a dropdown for each language group to select the default
kernel.


Release Notes:

- N/A

---------

Co-authored-by: Kirill <kirill@zed.dev>
2024-07-05 08:15:50 -07:00
Peter Tripp
821aa0811d Fix delay when changing scrolling direction (#13867)
Fixes: #13720.

Co-authored-by: Antonio Scandurra <antonio@zed.dev>
2024-07-05 11:11:43 -04:00
Thorsten Ball
fa602001e3 Configurable window decorations (#13866)
Introduces the `ZED_WINDOW_DECORATIONS` env variable.

- Not set, defaulting to client-side decorations
- Value is "client": client-side decorations
- Value is "server": server-side decorations

I think it's good to have this escape-hatch next to all possible
detection mechanisms.

Release Notes:

- N/A
2024-07-05 16:54:33 +02:00
Marshall Bowers
9b7bc04a87 ocaml: Bump to v0.0.2 (#13864)
This PR bumps the OCaml extension to v0.0.2.

Changes:

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

Release Notes:

- N/A
2024-07-05 10:10:54 -04:00
Stanislav Alekseev
a61188d137 ocaml: Pass environment to language server (#13834)
Fixed the environment not being passed to ocaml lsp causing it to not
work with direnv based installs

Release Notes:

- N/A
2024-07-05 09:59:17 -04:00
Zhangfan
1bd585186a docs: Update the example to set up black formatter in Python (#13839)
As titled. The new example is consistent with the instructions in
"Configuring Zed". I verified that the example works as expected.

Release Notes:

- Update the instructions to set up external formatter for Python.
2024-07-05 15:32:48 +02:00
Thorsten Ball
e69f9d6cf9 linux/x11: Fix gap when tiling windows side by side (#13859)
By leveraging the `_GTK_EDGE_CONSTRAINTS` atom we can get all four
booleans for the `Tiling` struct and figure out which side is free when
the window is tiled to half of the screen.

For the logic behind the `_GTK_EDGE_CONSTRAINTS` see:
-
8e9d13aa3b/src/x11/window-x11.c (L65-L75)
-
8e9d13aa3b/src/x11/window-x11.c (L1205-L1231)

(I used Claude 3.5 Sonnet with our code and these pieces from `mutter`
to generate the Rust code, that was pretty sweet)

This fixes the gap in the middle when a GPUI window is tiled to the left
and another window to the right.

It's not _perfect_ but it looks a lot better.

Here's a diff that makes it look better:

```diff
diff --git a/crates/gpui/examples/window_shadow.rs b/crates/gpui/examples/window_shadow.rs
index 122231f6b..7fa29dadc 100644
--- a/crates/gpui/examples/window_shadow.rs
+++ b/crates/gpui/examples/window_shadow.rs
@@ -72,8 +72,8 @@ impl Render for WindowShadow {
                     .when(!(tiling.top || tiling.left), |div| div.rounded_tl(rounding))
                     .when(!tiling.top, |div| div.pt(shadow_size))
                     .when(!tiling.bottom, |div| div.pb(shadow_size))
-                    .when(!tiling.left, |div| div.pl(shadow_size))
-                    .when(!tiling.right, |div| div.pr(shadow_size))
+                    .when(!tiling.left, |div| div.pl(shadow_size - border_size))
+                    .when(!tiling.right, |div| div.pr(shadow_size - border_size))
                     .on_mouse_move(|_e, cx| cx.refresh())
                     .on_mouse_down(MouseButton::Left, move |e, cx| {
                         let size = cx.window_bounds().get_bounds().size;
```

But that makes it look weird on Wayland, so I didn't do it.

I think it's fine for now. Chromium looks bad and has a gap, so we're
already better.

## Before

![before_1](https://github.com/zed-industries/zed/assets/1185253/875c5cdd-c0be-4295-beb0-bb9ba5beaa52)


![before_2](https://github.com/zed-industries/zed/assets/1185253/0b96be70-4c34-4e99-aeb2-ab741171ad14)

## After

![after_1](https://github.com/zed-industries/zed/assets/1185253/aa51da77-daf1-4ef8-a33f-a83731e0c7e1)

![after_2](https://github.com/zed-industries/zed/assets/1185253/8ce7902d-90b6-4f06-ba2c-626e643abe56)


Release Notes:

- N/A
2024-07-05 15:02:14 +02:00
Bennet Bo Fenner
c4dbe32f20 assistant: Limit amount of concurrent completion requests (#13856)
This PR refactors the completion providers to only process a maximum
amount of completion requests at a time.

Also started refactoring language model providers to use traits, so it's
easier to allow specifying multiple providers in the future.

Release Notes:

- N/A
2024-07-05 14:52:45 +02:00
Thorsten Ball
f2711b2fca ui: Don't show tooltip when ButtonLike is selected (#13857)
This fixes the issue of a tooltip covering the thing that the button has
revealed.

It also mirrors what other UI frameworks do. Chrome on Linux behaves the
same, and Safari does the same thing on macOS.

It fixes this:

![Screenshot from 2024-07-05
12-39-22](https://github.com/zed-industries/zed/assets/1185253/51ce4347-d12f-463a-9adb-da031fe2f751)


Release Notes:

- N/A
2024-07-05 14:27:53 +02:00
Chinmay Dalal
1260b52c82 linux scripts: Respect $CARGO_TARGET_DIR (#13830)
https://doc.rust-lang.org/cargo/reference/environment-variables.html

Some people (myself included) set this variable to have a single
directory to clean up (or whatever reason one might have for having a
single `target` directory). This changes the linux scripts to respect
that

Release Notes:

- N/A
2024-07-05 14:19:05 +03:00
Thorsten Ball
fc8749ffd7 linux: Set directory in SaveFileRequest dialog (#13850)
This has been bugging me for a while. If you create a new file and then
save it, the dialogue would show the home directory and not the folder
that you were in.

This fixes it.

Release Notes:

- N/A
2024-07-05 11:33:31 +02:00
Thorsten Ball
2a923e338f linux/x11: Set transparency to false by default (#13848)
The previous code lead to a ton of error messages from Blade on my X11
machine, even with *client-side decorations working well!*

As @someone13574 pointed out
[here](https://github.com/zed-industries/zed/pull/13611#issuecomment-2201685030)
things still work with this being false. And if someone changes a theme
with transparent background, then it will get set anyway. We just don't
do it by default.

And as @jansol pointed out
[here](https://github.com/zed-industries/zed/issues/5040#issuecomment-2096560629):

> On X11 it may be possible to configure a compositor to blur window
backgrounds but Zed has no way to influence that.

So we don't lose anything, I think, but get rid of a ton of error
messages in the logs.

Proof of shadows etc. still working:

![Screenshot from 2024-07-05
10-17-38](https://github.com/zed-industries/zed/assets/1185253/1216b38b-8011-46e7-b86f-c0f5fc3f6f64)



Release Notes:

- N/A
2024-07-05 10:55:45 +02:00
Owen Law
56e3fc794a linux/x11: Fix bugs related to unflushed commands (#13844)
**Edit**:

This PR adds flushes to functions which should have an immediate affect.
I've observed it fixing the following bugs (but there are probably
more):

- The cursor not updating when just hovering.
- The window not maximising after clicking the full-screen button until
you move the mouse.
- The window not minimising after clicking the minimise button until you
move the mouse.

---

**Original content**:

Following #13646, the cursor style wouldn't change because the
`change_window_attributes` command wasn't being flushed. I guess it was
working before because something else was flushing it somewhere else so
it was never noticed.

I just added `check()` which flushes the command so that the cursor will
actually update when just hovering. Before you would need to interact
with the window so that something else could flush the command.

Release Notes:

- N/A
2024-07-05 09:16:28 +02:00
Thorsten Ball
c8b106245c linux/x11: Resize on GTK_EDGE_CONSTRAINTS atom (#13833)
With the new window decorations resizing was _really_ laggy on my X11
machine.

Before:
- Click on window border (hitbox doesn't work properly, but that's
another issue)
- Drag and resize
- 4-5s nothing happens
- Window is resized

After:
- Click on window border
- Drag and resize
- Window is resized

I'm still not 100% sure on why this happens on my machine and not
Conrad's/Mikayla's, but seems like that GTK_EDGE_CONSTRAINTS atom is
sent when resizing.

The other thing that I can't explain is that we get a `ConfigureNotify`
when resizing, with the right size, but maybe not often enough?

Anyway, for now we'll go with this.

Release Notes:

- N/A
2024-07-04 18:29:08 +02:00
Marshall Bowers
398c2f91dd docs: Add Dart (#13829)
This PR adds Dart support to the docs, as it was one of the first-party
extensions that wasn't mentioned anywhere.

Release Notes:

- N/A
2024-07-04 11:14:39 -04:00
Marshall Bowers
3a5d116ffe docs: Update language docs and include links in the sidebar (#13828)
This PR updates the supported language docs and adds them to the sidebar
for better discoverability.

Release Notes:

- N/A
2024-07-04 11:08:38 -04:00
Marshall Bowers
e3cd1dd2d0 docs: Update language pages to indicate whether they are native or from an extension (#13827)
This PR updates the language pages in the docs to indicate whether the
support is available natively or provided by an extension.

Release Notes:

- N/A
2024-07-04 10:40:21 -04:00
Keenan Wresch
b1f8fc88a1 Allow Shift + Scroll to Horizontally Scroll in X11 and Wayland (#13676)
Release Notes:

- Allows shift + scroll horizontal scrolling on X11 and Wayland.

[Screencast from 2024-06-29
17-17-59.webm](https://github.com/zed-industries/zed/assets/14155062/2cac77b9-ecc8-4ddb-b08d-b5d964c8dc84)
2024-07-04 16:22:42 +02:00
张小白
d450a1d9e6 windows: Fix package-version-server (#13821)
Now, it can run on windows.

![Screenshot 2024-07-04
173832](https://github.com/zed-industries/zed/assets/14981363/d3c17fe3-6e79-46cd-b9a3-f6655109463c)


Release Notes:

- N/A
2024-07-04 12:49:17 +02:00
Antonio Scandurra
818e6e53d6 Introduce Tabs to Assistant Panel (#13783)
<img width="1652" alt="image"
src="https://github.com/zed-industries/zed/assets/482957/8397ea87-51aa-4b69-85c7-bf963fe2b08a">

Release Notes:

- N/A
2024-07-04 10:11:24 +02:00
271 changed files with 14894 additions and 11850 deletions

View File

@@ -1,6 +1,13 @@
.git
.github
**/.gitignore
**/.gitkeep
.gitattributes
.mailmap
**/target
zed.xcworkspace
.DS_Store
compose.yml
plugins/bin
script/node_modules
styles/node_modules

View File

@@ -32,9 +32,10 @@ body:
required: false
- type: textarea
attributes:
label: If applicable, attach your `~/Library/Logs/Zed/Zed.log` file to this issue.
label: If applicable, attach your Zed.log file to this issue.
description: |
Drag Zed.log into the text input below.
macOS: `~/Library/Logs/Zed/Zed.log`
Linux: `~/.local/share/zed/logs/Zed.log` or $XDG_DATA_HOME
If you only need the most recent lines, you can run the `zed: open log` command palette action to see the last 1000.
value: |
<details><summary>Zed.log</summary><pre>

View File

@@ -2,7 +2,7 @@
Release Notes:
- Added/Fixed/Improved ... ([#<public_issue_number_if_exists>](https://github.com/zed-industries/zed/issues/<public_issue_number_if_exists>)).
- Added/Fixed/Improved ... ([#NNNNN](https://github.com/zed-industries/zed/issues/NNNNN)).
Optionally, include screenshots / media showcasing your addition that can be included in the release notes.

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
/.direnv
.idea
**/target
**/cargo-target

View File

@@ -14,6 +14,12 @@
},
"JSON": {
"tab_size": 2,
"preferred_line_length": 100,
"formatter": "prettier"
},
"JSONC": {
"tab_size": 2,
"preferred_line_length": 100,
"formatter": "prettier"
},
"JavaScript": {

98
Cargo.lock generated
View File

@@ -341,9 +341,8 @@ dependencies = [
[[package]]
name = "ashpd"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd884d7c72877a94102c3715f3b1cd09ff4fac28221add3e57cfbe25c236d093"
version = "0.9.0"
source = "git+https://github.com/bilelmoussaoui/ashpd?rev=29f2e1a#29f2e1a6f4b0911f504658f5f4630c02e01b13f2"
dependencies = [
"async-fs 2.1.1",
"async-net 2.0.0",
@@ -374,16 +373,17 @@ dependencies = [
"anyhow",
"assistant_slash_command",
"async-watch",
"breadcrumbs",
"cargo_toml",
"chrono",
"client",
"clock",
"collections",
"command_palette_hooks",
"ctor",
"editor",
"env_logger",
"feature_flags",
"file_icons",
"fs",
"futures 0.3.28",
"fuzzy",
@@ -420,6 +420,7 @@ dependencies = [
"telemetry_events",
"terminal",
"terminal_view",
"text",
"theme",
"tiktoken-rs",
"toml 0.8.10",
@@ -2406,6 +2407,7 @@ version = "0.1.0"
dependencies = [
"chrono",
"parking_lot",
"serde",
"smallvec",
]
@@ -2464,6 +2466,7 @@ version = "0.44.0"
dependencies = [
"anthropic",
"anyhow",
"assistant",
"async-trait",
"async-tungstenite",
"audio",
@@ -3933,6 +3936,7 @@ dependencies = [
"serde_json",
"serde_json_lenient",
"settings",
"snippet_provider",
"task",
"theme",
"toml 0.8.10",
@@ -4889,11 +4893,9 @@ dependencies = [
"log",
"media",
"metal",
"mio 1.0.0",
"num_cpus",
"objc",
"oo7",
"open",
"parking",
"parking_lot",
"pathfinder_geometry",
@@ -5691,7 +5693,7 @@ dependencies = [
"fnv",
"lazy_static",
"libc",
"mio 0.8.11",
"mio",
"rand 0.8.5",
"serde",
"tempfile",
@@ -5705,25 +5707,6 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6"
[[package]]
name = "is-docker"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
dependencies = [
"once_cell",
]
[[package]]
name = "is-wsl"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
dependencies = [
"is-docker",
"once_cell",
]
[[package]]
name = "isahc"
version = "1.7.2"
@@ -6640,19 +6623,6 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "mio"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4929e1f84c5e54c3ec6141cd5d8b5a5c055f031f80cf78f2072920173cb4d880"
dependencies = [
"hermit-abi 0.3.9",
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.52.0",
]
[[package]]
name = "miow"
version = "0.6.0"
@@ -6891,7 +6861,7 @@ dependencies = [
"kqueue",
"libc",
"log",
"mio 0.8.11",
"mio",
"walkdir",
"windows-sys 0.48.0",
]
@@ -7225,17 +7195,6 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "open"
version = "5.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "449f0ff855d85ddbf1edd5b646d65249ead3f5e422aaa86b7d2d0b049b103e32"
dependencies = [
"is-wsl",
"libc",
"pathdiff",
]
[[package]]
name = "open_ai"
version = "0.1.0"
@@ -7401,6 +7360,7 @@ dependencies = [
"db",
"editor",
"file_icons",
"fuzzy",
"gpui",
"itertools 0.11.0",
"language",
@@ -8086,6 +8046,7 @@ dependencies = [
"similar",
"smol",
"snippet",
"snippet_provider",
"task",
"tempfile",
"terminal",
@@ -8313,6 +8274,7 @@ dependencies = [
"assistant",
"editor",
"gpui",
"repl",
"search",
"settings",
"ui",
@@ -8539,7 +8501,7 @@ dependencies = [
"task",
"terminal_view",
"ui",
"ui_text_field",
"ui_input",
"util",
"workspace",
]
@@ -9902,6 +9864,22 @@ dependencies = [
"smallvec",
]
[[package]]
name = "snippet_provider"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"fs",
"futures 0.3.28",
"gpui",
"parking_lot",
"serde",
"serde_json",
"snippet",
"util",
]
[[package]]
name = "socket2"
version = "0.4.9"
@@ -10282,6 +10260,7 @@ dependencies = [
"story",
"strum",
"theme",
"title_bar",
"ui",
]
@@ -11112,7 +11091,7 @@ dependencies = [
"backtrace",
"bytes 1.5.0",
"libc",
"mio 0.8.11",
"mio",
"num_cpus",
"parking_lot",
"pin-project-lite",
@@ -11737,7 +11716,7 @@ dependencies = [
]
[[package]]
name = "ui_text_field"
name = "ui_input"
version = "0.1.0"
dependencies = [
"editor",
@@ -13607,7 +13586,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.144.0"
version = "0.145.0"
dependencies = [
"activity_indicator",
"anyhow",
@@ -13682,6 +13661,7 @@ dependencies = [
"settings",
"simplelog",
"smol",
"snippet_provider",
"supermaven",
"tab_switcher",
"task",
@@ -13720,9 +13700,9 @@ dependencies = [
[[package]]
name = "zed_clojure"
version = "0.0.2"
version = "0.0.3"
dependencies = [
"zed_extension_api 0.0.4",
"zed_extension_api 0.0.6",
]
[[package]]
@@ -13834,14 +13814,14 @@ dependencies = [
[[package]]
name = "zed_lua"
version = "0.0.2"
version = "0.0.3"
dependencies = [
"zed_extension_api 0.0.6",
]
[[package]]
name = "zed_ocaml"
version = "0.0.1"
version = "0.0.2"
dependencies = [
"zed_extension_api 0.0.6",
]

View File

@@ -88,6 +88,7 @@ members = [
"crates/semantic_version",
"crates/settings",
"crates/snippet",
"crates/snippet_provider",
"crates/sqlez",
"crates/sqlez_macros",
"crates/story",
@@ -108,7 +109,7 @@ members = [
"crates/time_format",
"crates/title_bar",
"crates/ui",
"crates/ui_text_field",
"crates/ui_input",
"crates/util",
"crates/vcs_menu",
"crates/vim",
@@ -239,6 +240,7 @@ semantic_index = { path = "crates/semantic_index" }
semantic_version = { path = "crates/semantic_version" }
settings = { path = "crates/settings" }
snippet = { path = "crates/snippet" }
snippet_provider = { path = "crates/snippet_provider" }
sqlez = { path = "crates/sqlez" }
sqlez_macros = { path = "crates/sqlez_macros" }
story = { path = "crates/story" }
@@ -259,7 +261,7 @@ theme_selector = { path = "crates/theme_selector" }
time_format = { path = "crates/time_format" }
title_bar = { path = "crates/title_bar" }
ui = { path = "crates/ui" }
ui_text_field = { path = "crates/ui_text_field" }
ui_input = { path = "crates/ui_input" }
util = { path = "crates/util" }
vcs_menu = { path = "crates/vcs_menu" }
vim = { path = "crates/vim" }
@@ -272,9 +274,9 @@ zed_actions = { path = "crates/zed_actions" }
alacritty_terminal = "0.23"
any_vec = "0.13"
anyhow = "1.0.57"
ashpd = "0.8.0"
ashpd = { git = "https://github.com/bilelmoussaoui/ashpd", rev = "29f2e1a" }
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
async-dispatcher = { version = "0.1"}
async-dispatcher = { version = "0.1" }
async-fs = "1.6"
async-recursion = "1.0.0"
async-tar = "0.4.2"
@@ -315,7 +317,9 @@ image = "0.25.1"
indexmap = { version = "1.6.2", features = ["serde"] }
indoc = "1"
# We explicitly disable http2 support in isahc.
isahc = { version = "1.7.2", default-features = false, features = [ "text-decoding" ] }
isahc = { version = "1.7.2", default-features = false, features = [
"text-decoding",
] }
itertools = "0.11.0"
lazy_static = "1.4.0"
libc = "0.2"
@@ -341,7 +345,9 @@ rand = "0.8.5"
refineable = { path = "./crates/refineable" }
regex = "1.5"
repair_json = "0.1.0"
runtimelib = { version="0.12", default-features = false, features = ["async-dispatcher-runtime"] }
runtimelib = { version = "0.12", default-features = false, features = [
"async-dispatcher-runtime",
] }
rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
rust-embed = { version = "8.4", features = ["include-exclude"] }
schemars = "0.8"
@@ -512,7 +518,7 @@ single_range_in_vec_init = "allow"
# There are a bunch of rules currently failing in the `style` group, so
# allow all of those, for now.
style = "allow"
style = { level = "allow", priority = -1 }
# Individual rules that have violations in the codebase:
almost_complete_range = "allow"

View File

@@ -4,42 +4,36 @@
Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter).
## Installation
--------
You can [download](https://zed.dev/download) Zed today for macOS (v10.15+).
### Installation
Support for additional platforms is on our [roadmap](https://zed.dev/roadmap):
- Linux ([tracking issue](https://github.com/zed-industries/zed/issues/7015))
<a href="https://repology.org/project/zed-editor/versions">
<img src="https://repology.org/badge/vertical-allrepos/zed-editor.svg?minversion=0.143.5" alt="Packaging status" align="right">
</a>
On macOS and Linux you can [download Zed directly](https://zed.dev/download) or [install Zed via your local package manager](https://zed.dev/docs/linux#installing-via-a-package-manager).
Other platforms are not yet available:
- Windows ([tracking issue](https://github.com/zed-industries/zed/issues/5394))
- Web ([tracking issue](https://github.com/zed-industries/zed/issues/5396))
For macOS users, you can also install Zed using [Homebrew](https://brew.sh/):
```sh
brew install --cask zed
```
Alternatively, to install the Preview release:
```sh
brew install --cask zed@preview
```
## Developing Zed
### Developing Zed
- [Building Zed for macOS](./docs/src/development/macos.md)
- [Building Zed for Linux](./docs/src/development/linux.md)
- [Building Zed for Windows](./docs/src/development/windows.md)
- [Running Collaboration Locally](./docs/src/development/local-collaboration.md)
## Contributing
### Contributing
See [CONTRIBUTING.md](./CONTRIBUTING.md) for ways you can contribute to Zed.
Also... we're hiring! Check out our [jobs](https://zed.dev/jobs) page for open roles.
## Licensing
### Licensing
License information for third party dependencies must be correctly provided for CI to pass.

View File

@@ -0,0 +1,14 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_32_58)">
<path d="M19 7C20.1046 7 21 6.10457 21 5C21 3.89543 20.1046 3 19 3C17.8954 3 17 3.89543 17 5C17 6.10457 17.8954 7 19 7Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 21C6.10457 21 7 20.1046 7 19C7 17.8954 6.10457 17 5 17C3.89543 17 3 17.8954 3 19C3 20.1046 3.89543 21 5 21Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.3999 21.9C12.3227 22.2159 14.2958 21.9632 16.0769 21.173C17.858 20.3827 19.3694 19.0893 20.4254 17.4517C21.4814 15.8142 22.036 13.9037 22.021 11.9553C22.006 10.0068 21.422 8.10512 20.3409 6.48401" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.4998 2.10002C11.5849 1.8076 9.62631 2.07763 7.86198 2.87732C6.09765 3.677 4.60356 4.9719 3.56126 6.60468C2.51896 8.23745 1.97332 10.1378 1.99063 12.0748C2.00795 14.0118 2.58749 15.9021 3.65882 17.516" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 13C12.5523 13 13 12.5523 13 12C13 11.4477 12.5523 11 12 11C11.4477 11 11 11.4477 11 12C11 12.5523 11.4477 13 12 13Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_32_58">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

20
assets/icons/repl_off.svg Normal file
View File

@@ -0,0 +1,20 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_39_129)">
<path d="M22.0209 11.9553C22.0059 10.0068 21.4219 8.10512 20.3408 6.48401" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.1001 2.18C11.355 1.93537 12.1493 1.93674 13.5027 2.10594" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M21.8198 10.1C22.0644 11.3548 22.0644 12.6451 21.8198 13.9" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M20.2898 17.6C19.5716 18.6622 18.6548 19.5757 17.5898 20.29" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.9008 21.82C12.6459 22.0644 11.6432 22.1543 10.3883 21.91" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.18005 13.9C1.93543 12.6451 1.93543 11.3548 2.18005 10.1" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.70996 6.40002C4.42822 5.33775 5.34503 4.42433 6.40996 3.71002" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 13C12.5523 13 13 12.5523 13 12C13 11.4477 12.5523 11 12 11C11.4477 11 11 11.4477 11 12C11 12.5523 11.4477 13 12 13Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19 7C20.1046 7 21 6.10457 21 5C21 3.89543 20.1046 3 19 3C17.8954 3 17 3.89543 17 5C17 6.10457 17.8954 7 19 7Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 21C6.10457 21 7 20.1046 7 19C7 17.8954 6.10457 17 5 17C3.89543 17 3 17.8954 3 19C3 20.1046 3.89543 21 5 21Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1.99072 12.0748C2.00804 14.0118 2.58758 15.9021 3.65891 17.516" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_39_129">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,15 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_32_70)">
<path d="M19 7C20.1046 7 21 6.10457 21 5C21 3.89543 20.1046 3 19 3C17.8954 3 17 3.89543 17 5C17 6.10457 17.8954 7 19 7Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 21C6.10457 21 7 20.1046 7 19C7 17.8954 6.10457 17 5 17C3.89543 17 3 17.8954 3 19C3 20.1046 3.89543 21 5 21Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.3999 21.9C12.3227 22.2159 14.2958 21.9632 16.0769 21.173C17.858 20.3827 19.3694 19.0893 20.4254 17.4517C21.4814 15.8142 22.036 13.9037 22.021 11.9553C22.006 10.0068 21.422 8.10512 20.3409 6.48401" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.4998 2.10002C11.5849 1.8076 9.62631 2.07763 7.86198 2.87732C6.09765 3.677 4.60356 4.9719 3.56126 6.60468C2.51896 8.23745 1.97332 10.1378 1.99063 12.0748C2.00795 14.0118 2.58749 15.9021 3.65882 17.516" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 15V9" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14 15V9" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_32_70">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,14 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_32_64)">
<path d="M19 7C20.1046 7 21 6.10457 21 5C21 3.89543 20.1046 3 19 3C17.8954 3 17 3.89543 17 5C17 6.10457 17.8954 7 19 7Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 21C6.10457 21 7 20.1046 7 19C7 17.8954 6.10457 17 5 17C3.89543 17 3 17.8954 3 19C3 20.1046 3.89543 21 5 21Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.3999 21.9C12.3227 22.2159 14.2958 21.9632 16.0769 21.173C17.858 20.3827 19.3694 19.0893 20.4254 17.4517C21.4814 15.8142 22.036 13.9037 22.021 11.9553C22.006 10.0068 21.422 8.10512 20.3409 6.48401" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.4998 2.10002C11.5849 1.8076 9.62631 2.07763 7.86198 2.87732C6.09765 3.677 4.60356 4.9719 3.56126 6.60468C2.51896 8.23745 1.97332 10.1378 1.99063 12.0748C2.00795 14.0118 2.58749 15.9021 3.65882 17.516" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 8.56055C10 8.32095 10.267 8.17803 10.4664 8.31094L15.6256 11.7504C15.8037 11.8691 15.8037 12.1309 15.6256 12.2496L10.4664 15.6891C10.267 15.822 10 15.6791 10 15.4394V8.56055Z" fill="white" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_32_64">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -3,10 +3,14 @@
{
"bindings": {
"up": "menu::SelectPrev",
"shift-tab": "menu::SelectPrev",
"home": "menu::SelectFirst",
"pageup": "menu::SelectFirst",
"shift-pageup": "menu::SelectFirst",
"ctrl-p": "menu::SelectPrev",
"down": "menu::SelectNext",
"tab": "menu::SelectNext",
"end": "menu::SelectLast",
"pagedown": "menu::SelectLast",
"shift-pagedown": "menu::SelectFirst",
"ctrl-n": "menu::SelectNext",
@@ -41,9 +45,10 @@
"tab": "editor::Tab",
"shift-tab": "editor::TabPrev",
"ctrl-k": "editor::CutToEndOfLine",
"ctrl-t": "editor::Transpose",
// "ctrl-t": "editor::Transpose",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd",
"shift-delete": "editor::Cut",
"ctrl-x": "editor::Cut",
"ctrl-insert": "editor::Copy",
"ctrl-c": "editor::Copy",
@@ -83,54 +88,19 @@
"ctrl-a": "editor::SelectAll",
"ctrl-l": "editor::SelectLine",
"ctrl-shift-i": "editor::Format",
// "cmd-shift-left": [
// "editor::SelectToBeginningOfLine",
// {
// "stop_at_soft_wraps": true
// }
// ],
"shift-home": [
"editor::SelectToBeginningOfLine",
{
"stop_at_soft_wraps": true
}
],
// "ctrl-shift-a": [
// "editor::SelectToBeginningOfLine",
// {
// "stop_at_soft_wraps": true
// }
// ],
// "cmd-shift-right": [
// "editor::SelectToEndOfLine",
// {
// "stop_at_soft_wraps": true
// }
// ],
"shift-end": [
"editor::SelectToEndOfLine",
{
"stop_at_soft_wraps": true
}
],
// "ctrl-shift-e": [
// "editor::SelectToEndOfLine",
// {
// "stop_at_soft_wraps": true
// }
// ],
// "alt-v": [
// "editor::MovePageUp",
// {
// "center_cursor": true
// }
// ],
// "cmd-shift-left": ["editor::SelectToBeginningOfLine", {"stop_at_soft_wraps": true }],
"shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true }],
// "ctrl-shift-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true }],
// "cmd-shift-right": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
"shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
// "ctrl-shift-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
// "alt-v": ["editor::MovePageUp", { "center_cursor": true }],
"ctrl-alt-space": "editor::ShowCharacterPalette",
"ctrl-;": "editor::ToggleLineNumbers",
"ctrl-k ctrl-r": "editor::RevertSelectedHunks",
"ctrl-'": "editor::ToggleHunkDiff",
"ctrl-\"": "editor::ExpandAllHunkDiffs",
"ctrl-alt-g b": "editor::ToggleGitBlame"
"alt-g b": "editor::ToggleGitBlame"
}
},
{
@@ -142,18 +112,8 @@
"ctrl-enter": "editor::NewlineAbove",
"alt-z": "editor::ToggleSoftWrap",
"ctrl-f": "buffer_search::Deploy",
"ctrl-h": [
"buffer_search::Deploy",
{
"replace_enabled": true
}
],
// "cmd-e": [
// "buffer_search::Deploy",
// {
// "focus": false
// }
// ],
"ctrl-h": ["buffer_search::Deploy", { "replace_enabled": true }],
// "cmd-e": ["buffer_search::Deploy", { "focus": false }],
"ctrl->": "assistant::QuoteSelection",
"ctrl-<": "assistant::InsertIntoEditor",
"ctrl-alt-e": "editor::SelectEnclosingSymbol"
@@ -268,6 +228,7 @@
"ctrl-pageup": "pane::ActivatePrevItem",
"ctrl-pagedown": "pane::ActivateNextItem",
"ctrl-w": "pane::CloseActiveItem",
"ctrl-f4": "pane::CloseActiveItem",
"alt-ctrl-t": "pane::CloseInactiveItems",
"alt-ctrl-shift-w": "workspace::CloseInactiveTabsAndPanes",
"ctrl-k u": "pane::CloseCleanItems",
@@ -310,38 +271,13 @@
"ctrl-shift-right": "editor::SelectToNextWordEnd",
"ctrl-shift-up": "editor::SelectLargerSyntaxNode", //todo(linux) tmp keybinding
"ctrl-shift-down": "editor::SelectSmallerSyntaxNode", //todo(linux) tmp keybinding
"ctrl-d": [
"editor::SelectNext",
{
"replace_newest": false
}
],
"ctrl-d": ["editor::SelectNext", { "replace_newest": false }],
"ctrl-shift-l": "editor::SelectAllMatches",
"ctrl-shift-d": [
"editor::SelectPrevious",
{
"replace_newest": false
}
],
"ctrl-k ctrl-d": [
"editor::SelectNext",
{
"replace_newest": true
}
],
"ctrl-k ctrl-shift-d": [
"editor::SelectPrevious",
{
"replace_newest": true
}
],
"ctrl-shift-d": ["editor::SelectPrevious", { "replace_newest": false }],
"ctrl-k ctrl-d": ["editor::SelectNext", { "replace_newest": true }],
"ctrl-k ctrl-shift-d": ["editor::SelectPrevious", { "replace_newest": true }],
"ctrl-k ctrl-i": "editor::Hover",
"ctrl-/": [
"editor::ToggleComments",
{
"advance_downwards": false
}
],
"ctrl-/": ["editor::ToggleComments", { "advance_downwards": false }],
"ctrl-u": "editor::UndoSelection",
"ctrl-shift-u": "editor::RedoSelection",
"f8": "editor::GoToDiagnostic",
@@ -349,16 +285,23 @@
"f2": "editor::Rename",
"f12": "editor::GoToDefinition",
"alt-f12": "editor::GoToDefinitionSplit",
"ctrl-shift-f10": "editor::GoToDefinitionSplit",
"ctrl-f12": "editor::GoToTypeDefinition",
"shift-f12": "editor::GoToImplementation",
"alt-ctrl-f12": "editor::GoToTypeDefinitionSplit",
"alt-shift-f12": "editor::FindAllReferences",
"ctrl-m": "editor::MoveToEnclosingBracket",
"ctrl-shift-\\": "editor::MoveToEnclosingBracket",
"ctrl-shift-[": "editor::Fold",
"ctrl-shift-]": "editor::UnfoldLines",
"ctrl-space": "editor::ShowCompletions",
"ctrl-.": "editor::ToggleCodeActions",
"alt-ctrl-r": "editor::RevealInFinder",
"alt-ctrl-r": "editor::RevealInFileManager",
"ctrl-k r": "editor::RevealInFileManager",
"ctrl-k p": "editor::CopyPath",
"ctrl-\\": "pane::SplitRight",
"ctrl-k v": "markdown::OpenPreviewToTheSide",
"ctrl-shift-v": "markdown::OpenPreview",
"ctrl-alt-shift-c": "editor::DisplayCursorNames"
}
},
@@ -383,8 +326,10 @@
"alt-9": ["pane::ActivateItem", 8],
"alt-0": "pane::ActivateLastItem",
"ctrl-alt--": "pane::GoBack",
"ctrl-alt-_": "pane::GoForward",
"ctrl-alt-shift--": "pane::GoForward",
"ctrl-shift-t": "pane::ReopenClosedItem",
"f3": "search::SelectNextMatch",
"shift-f3": "search::SelectPrevMatch",
"ctrl-shift-f": "project_search::ToggleFocus"
}
},
@@ -392,12 +337,7 @@
"context": "Workspace",
"bindings": {
// Change the default action on `menu::Confirm` by setting the parameter
// "alt-cmd-o": [
// "projects::OpenRecent",
// {
// "create_new_window": true
// }
// ]
// "alt-ctrl-o": ["projects::OpenRecent", { "create_new_window": true }],
"alt-ctrl-o": "projects::OpenRecent",
"alt-ctrl-shift-b": "branches::OpenRecent",
"ctrl-~": "workspace::NewTerminal",
@@ -416,25 +356,21 @@
"alt-7": ["workspace::ActivatePane", 6],
"alt-8": ["workspace::ActivatePane", 7],
"alt-9": ["workspace::ActivatePane", 8],
"ctrl-alt-b": "workspace::ToggleLeftDock",
"ctrl-alt-b": "workspace::ToggleRightDock",
"ctrl-b": "workspace::ToggleLeftDock",
"ctrl-j": "workspace::ToggleBottomDock",
"ctrl-alt-y": "workspace::CloseAllDocks",
"ctrl-shift-f": "pane::DeploySearch",
"ctrl-shift-h": [
"pane::DeploySearch",
{
"replace_enabled": true
}
],
"ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
"ctrl-k ctrl-s": "zed::OpenKeymap",
"ctrl-k ctrl-t": "theme_selector::Toggle",
"ctrl-shift-t": "project_symbols::Toggle",
"ctrl-t": "project_symbols::Toggle",
"ctrl-p": "file_finder::Toggle",
"ctrl-tab": "tab_switcher::Toggle",
"ctrl-shift-tab": ["tab_switcher::Toggle", { "select_last": true }],
"ctrl-e": "file_finder::Toggle",
"ctrl-shift-p": "command_palette::Toggle",
"f1": "command_palette::Toggle",
"ctrl-shift-m": "diagnostics::Deploy",
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-b": "outline_panel::ToggleFocus",
@@ -450,6 +386,7 @@
"ctrl-k shift-right": ["workspace::SwapPaneInDirection", "Right"],
"ctrl-k shift-up": ["workspace::SwapPaneInDirection", "Up"],
"ctrl-k shift-down": ["workspace::SwapPaneInDirection", "Down"],
"ctrl-shift-x": "zed::Extensions",
"alt-t": "task::Rerun",
"alt-shift-t": "task::Spawn"
}
@@ -466,7 +403,7 @@
"ctrl-alt-delete": "editor::DeleteToNextSubwordEnd",
"ctrl-alt-d": "editor::DeleteToNextSubwordEnd",
"ctrl-alt-left": "editor::MoveToPreviousSubwordStart",
"ctrl-alt-b": "editor::MoveToPreviousSubwordStart",
// "ctrl-alt-b": "editor::MoveToPreviousSubwordStart",
"ctrl-alt-right": "editor::MoveToNextSubwordEnd",
"ctrl-alt-f": "editor::MoveToNextSubwordEnd",
"ctrl-alt-shift-left": "editor::SelectToPreviousSubwordStart",
@@ -565,11 +502,12 @@
{
"context": "OutlinePanel",
"bindings": {
"escape": "menu::Cancel",
"left": "outline_panel::CollapseSelectedEntry",
"right": "outline_panel::ExpandSelectedEntry",
"ctrl-alt-c": "outline_panel::CopyPath",
"alt-ctrl-shift-c": "outline_panel::CopyRelativePath",
"alt-ctrl-r": "outline_panel::RevealInFinder",
"alt-ctrl-r": "outline_panel::RevealInFileManager",
"space": "outline_panel::Open",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrev"
@@ -596,7 +534,7 @@
"delete": ["project_panel::Trash", { "skip_prompt": false }],
"ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
"alt-ctrl-r": "project_panel::RevealInFinder",
"alt-ctrl-r": "project_panel::RevealInFileManager",
"alt-shift-f": "project_panel::NewSearchInDirectory",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrev",

View File

@@ -3,10 +3,14 @@
{
"bindings": {
"up": "menu::SelectPrev",
"shift-tab": "menu::SelectPrev",
"home": "menu::SelectFirst",
"pageup": "menu::SelectFirst",
"shift-pageup": "menu::SelectFirst",
"ctrl-p": "menu::SelectPrev",
"down": "menu::SelectNext",
"tab": "menu::SelectNext",
"end": "menu::SelectLast",
"pagedown": "menu::SelectLast",
"shift-pagedown": "menu::SelectFirst",
"ctrl-n": "menu::SelectNext",
@@ -109,54 +113,14 @@
"cmd-a": "editor::SelectAll",
"cmd-l": "editor::SelectLine",
"cmd-shift-i": "editor::Format",
"cmd-shift-left": [
"editor::SelectToBeginningOfLine",
{
"stop_at_soft_wraps": true
}
],
"shift-home": [
"editor::SelectToBeginningOfLine",
{
"stop_at_soft_wraps": true
}
],
"ctrl-shift-a": [
"editor::SelectToBeginningOfLine",
{
"stop_at_soft_wraps": true
}
],
"cmd-shift-right": [
"editor::SelectToEndOfLine",
{
"stop_at_soft_wraps": true
}
],
"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
}
],
"cmd-shift-left": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true }],
"shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true }],
"ctrl-shift-a": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true }],
"cmd-shift-right": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
"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-cmd-space": "editor::ShowCharacterPalette",
"cmd-;": "editor::ToggleLineNumbers",
"cmd-alt-z": "editor::RevertSelectedHunks",
@@ -171,32 +135,22 @@
"enter": "editor::Newline",
"shift-enter": "editor::Newline",
"cmd-shift-enter": "editor::NewlineAbove",
"cmd-enter": "editor::NewlineBelow",
"alt-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
}
],
"cmd-e": [
"buffer_search::Deploy",
{
"focus": false
}
],
"cmd-alt-f": ["buffer_search::Deploy", { "replace_enabled": true }],
"cmd-alt-l": ["buffer_search::Deploy", { "selection_search_enabled": true }],
"cmd-e": ["buffer_search::Deploy", { "focus": false }],
"cmd->": "assistant::QuoteSelection",
"cmd-<": "assistant::InsertIntoEditor",
"cmd-alt-e": "editor::SelectEnclosingSymbol"
}
},
{
"context": "Editor && mode == full && !jupyter",
"bindings": {
"cmd-enter": "editor::NewlineBelow"
}
},
{
"context": "Editor && mode == full && inline_completion",
"bindings": {
@@ -285,6 +239,7 @@
"context": "ProjectSearchBar",
"bindings": {
"escape": "project_search::ToggleFocus",
"cmd-shift-j": "project_search::ToggleFilters",
"cmd-shift-f": "search::FocusSearch",
"cmd-shift-h": "search::ToggleReplace",
"alt-cmd-g": "search::ToggleRegex",
@@ -309,6 +264,7 @@
"context": "ProjectSearchView",
"bindings": {
"escape": "project_search::ToggleFocus",
"cmd-shift-j": "project_search::ToggleFilters",
"cmd-shift-h": "search::ToggleReplace",
"alt-cmd-g": "search::ToggleRegex",
"alt-cmd-x": "search::ToggleRegex"
@@ -356,38 +312,13 @@
"alt-shift-down": "editor::DuplicateLineDown",
"ctrl-shift-right": "editor::SelectLargerSyntaxNode",
"ctrl-shift-left": "editor::SelectSmallerSyntaxNode",
"cmd-d": [
"editor::SelectNext",
{
"replace_newest": false
}
],
"cmd-d": ["editor::SelectNext", { "replace_newest": false }],
"cmd-shift-l": "editor::SelectAllMatches",
"ctrl-cmd-d": [
"editor::SelectPrevious",
{
"replace_newest": false
}
],
"cmd-k cmd-d": [
"editor::SelectNext",
{
"replace_newest": true
}
],
"cmd-k ctrl-cmd-d": [
"editor::SelectPrevious",
{
"replace_newest": true
}
],
"ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": false }],
"cmd-k cmd-d": ["editor::SelectNext", { "replace_newest": true }],
"cmd-k ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": true }],
"cmd-k cmd-i": "editor::Hover",
"cmd-/": [
"editor::ToggleComments",
{
"advance_downwards": false
}
],
"cmd-/": ["editor::ToggleComments", { "advance_downwards": false }],
"cmd-u": "editor::UndoSelection",
"cmd-shift-u": "editor::RedoSelection",
"f8": "editor::GoToDiagnostic",
@@ -400,11 +331,17 @@
"alt-cmd-f12": "editor::GoToTypeDefinitionSplit",
"alt-shift-f12": "editor::FindAllReferences",
"ctrl-m": "editor::MoveToEnclosingBracket",
"cmd-shift-\\": "editor::MoveToEnclosingBracket",
"alt-cmd-[": "editor::Fold",
"alt-cmd-]": "editor::UnfoldLines",
"ctrl-space": "editor::ShowCompletions",
"cmd-.": "editor::ToggleCodeActions",
"alt-cmd-r": "editor::RevealInFinder",
"alt-cmd-r": "editor::RevealInFileManager",
"cmd-k r": "editor::RevealInFileManager",
"cmd-k p": "editor::CopyPath",
"cmd-\\": "pane::SplitRight",
"cmd-k v": "markdown::OpenPreviewToTheSide",
"cmd-shift-v": "markdown::OpenPreview",
"ctrl-cmd-c": "editor::DisplayCursorNames"
}
},
@@ -429,7 +366,7 @@
"ctrl-9": ["pane::ActivateItem", 8],
"ctrl-0": "pane::ActivateLastItem",
"ctrl--": "pane::GoBack",
"ctrl-_": "pane::GoForward",
"ctrl-shift--": "pane::GoForward",
"cmd-shift-t": "pane::ReopenClosedItem",
"cmd-shift-f": "project_search::ToggleFocus"
}
@@ -438,12 +375,7 @@
"context": "Workspace",
"bindings": {
// Change the default action on `menu::Confirm` by setting the parameter
// "alt-cmd-o": [
// "projects::OpenRecent",
// {
// "create_new_window": true
// }
// ]
// "alt-cmd-o": ["projects::OpenRecent", {"create_new_window": true }],
"alt-cmd-o": "projects::OpenRecent",
"alt-cmd-b": "branches::OpenRecent",
"ctrl-~": "workspace::NewTerminal",
@@ -467,12 +399,7 @@
"cmd-j": "workspace::ToggleBottomDock",
"alt-cmd-y": "workspace::CloseAllDocks",
"cmd-shift-f": "pane::DeploySearch",
"cmd-shift-h": [
"pane::DeploySearch",
{
"replace_enabled": true
}
],
"cmd-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
"cmd-k cmd-s": "zed::OpenKeymap",
"cmd-k cmd-t": "theme_selector::Toggle",
"cmd-t": "project_symbols::Toggle",
@@ -495,6 +422,7 @@
"cmd-k shift-right": ["workspace::SwapPaneInDirection", "Right"],
"cmd-k shift-up": ["workspace::SwapPaneInDirection", "Up"],
"cmd-k shift-down": ["workspace::SwapPaneInDirection", "Down"],
"cmd-shift-x": "zed::Extensions",
"alt-t": "task::Rerun",
"alt-shift-t": "task::Spawn"
}
@@ -595,11 +523,12 @@
{
"context": "OutlinePanel",
"bindings": {
"escape": "menu::Cancel",
"left": "outline_panel::CollapseSelectedEntry",
"right": "outline_panel::ExpandSelectedEntry",
"cmd-alt-c": "outline_panel::CopyPath",
"alt-cmd-shift-c": "outline_panel::CopyRelativePath",
"alt-cmd-r": "outline_panel::RevealInFinder",
"alt-cmd-r": "outline_panel::RevealInFileManager",
"space": "outline_panel::Open",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrev"
@@ -619,12 +548,14 @@
"cmd-alt-c": "project_panel::CopyPath",
"alt-cmd-shift-c": "project_panel::CopyRelativePath",
"enter": "project_panel::Rename",
"f2": "project_panel::Rename",
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
"delete": ["project_panel::Trash", { "skip_prompt": false }],
"cmd-backspace": ["project_panel::Trash", { "skip_prompt": true }],
"cmd-delete": ["project_panel::Delete", { "skip_prompt": false }],
"alt-cmd-r": "project_panel::RevealInFileManager",
"cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"alt-cmd-r": "project_panel::RevealInFinder",
"alt-shift-f": "project_panel::NewSearchInDirectory",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrev",
@@ -637,6 +568,12 @@
"space": "project_panel::Open"
}
},
{
"context": "Editor && jupyter && !ContextEditor",
"bindings": {
"cmd-enter": "repl::Run"
}
},
{
"context": "CollabPanel && not_editing",
"bindings": {
@@ -707,10 +644,14 @@
"escape": ["terminal::SendKeystroke", "escape"],
"enter": ["terminal::SendKeystroke", "enter"],
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
"cmd-up": "terminal::ScrollPageUp",
"cmd-down": "terminal::ScrollPageDown",
"shift-pageup": "terminal::ScrollPageUp",
"shift-pagedown": "terminal::ScrollPageDown",
"shift-up": "terminal::ScrollLineUp",
"shift-down": "terminal::ScrollLineDown",
"cmd-home": "terminal::ScrollToTop",
"cmd-end": "terminal::ScrollToBottom",
"shift-home": "terminal::ScrollToTop",
"shift-end": "terminal::ScrollToBottom"
}

View File

@@ -0,0 +1,93 @@
// Default Keymap (Atom) for Zed on Linux
[
{
"bindings": {
"ctrl-shift-f5": "workspace::Reload", // window:reload
"ctrl-k ctrl-n": "workspace::ActivatePreviousPane", // window:focus-next-pane
"ctrl-k ctrl-p": "workspace::ActivateNextPane" // window:focus-previous-pane
}
},
{
"context": "Editor",
"bindings": {
"ctrl-shift-l": "language_selector::Toggle", // grammar-selector:show
"ctrl-|": "pane::RevealInProjectPanel", // tree-view:reveal-active-file
"ctrl-b": "editor::GoToDefinition", // fuzzy-finder:toggle-buffer-finder
"ctrl-alt-b": "editor::GoToDefinitionSplit", // N/A: From JetBrains
"ctrl-<": "editor::ScrollCursorCenter", // editor:scroll-to-cursor
"f3": ["editor::SelectNext", { "replace_newest": true }], // find-and-replace:find-next
"shift-f3": ["editor::SelectPrevious", { "replace_newest": true }], //find-and-replace:find-previous
"alt-shift-down": "editor::AddSelectionBelow", // editor:add-selection-below
"alt-shift-up": "editor::AddSelectionAbove", // editor:add-selection-above
"ctrl-k ctrl-u": "editor::ConvertToUpperCase", // editor:upper-case
"ctrl-k ctrl-l": "editor::ConvertToLowerCase", // editor:lower-case
"ctrl-j": "editor::JoinLines", // editor:join-lines
"ctrl-shift-d": "editor::DuplicateLineDown", // editor:duplicate-lines
"ctrl-up": "editor::MoveLineUp", // editor:move-line-up
"ctrl-down": "editor::MoveLineDown", // editor:move-line-down
"ctrl-shift-m": "markdown::OpenPreviewToTheSide" // markdown-preview:toggle
}
},
{
"context": "Editor && mode == full",
"bindings": {
"ctrl-r": "outline::Toggle" // symbols-view:toggle-project-symbols
}
},
{
"context": "BufferSearchBar",
"bindings": {
"ctrl-f3": "search::SelectNextMatch", // find-and-replace:find-next-selected
"ctrl-shift-f3": "search::SelectPrevMatch" // find-and-replace:find-previous-selected
}
},
{
"context": "Workspace",
"bindings": {
"ctrl-\\": "workspace::ToggleLeftDock", // tree-view:toggle
"ctrl-k ctrl-b": "workspace::ToggleLeftDock", // tree-view:toggle
"ctrl-t": "file_finder::Toggle", // fuzzy-finder:toggle-file-finder
"ctrl-r": "project_symbols::Toggle" // symbols-view:toggle-project-symbols
}
},
{
"context": "Pane",
"bindings": {
// "ctrl-0": "project_panel::ToggleFocus", // tree-view:toggle-focus
"ctrl-1": ["pane::ActivateItem", 0], // tree-view:open-selected-entry-in-pane-1
"ctrl-2": ["pane::ActivateItem", 1], // tree-view:open-selected-entry-in-pane-2
"ctrl-3": ["pane::ActivateItem", 2], // tree-view:open-selected-entry-in-pane-3
"ctrl-4": ["pane::ActivateItem", 3], // tree-view:open-selected-entry-in-pane-4
"ctrl-5": ["pane::ActivateItem", 4], // tree-view:open-selected-entry-in-pane-5
"ctrl-6": ["pane::ActivateItem", 5], // tree-view:open-selected-entry-in-pane-6
"ctrl-7": ["pane::ActivateItem", 6], // tree-view:open-selected-entry-in-pane-7
"ctrl-8": ["pane::ActivateItem", 7], // tree-view:open-selected-entry-in-pane-8
"ctrl-9": ["pane::ActivateItem", 8] // tree-view:open-selected-entry-in-pane-9
}
},
{
"context": "ProjectPanel",
"bindings": {
"f2": "project_panel::Rename", // tree-view:rename
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
"ctrl-x": "project_panel::Cut", // tree-view:cut
"ctrl-c": "project_panel::Copy", // tree-view:copy
"ctrl-v": "project_panel::Paste" // tree-view:paste
}
},
{
"context": "ProjectPanel && not_editing",
"bindings": {
"ctrl-shift-c": "project_panel::CopyPath", // tree-view:copy-full-path
"ctrl-[": "project_panel::CollapseSelectedEntry", // tree-view:collapse-directory
"ctrl-b": "project_panel::CollapseSelectedEntry", // tree-view:collapse-directory
"ctrl-]": "project_panel::ExpandSelectedEntry", // tree-view:expand-item
"ctrl-f": "project_panel::ExpandSelectedEntry", // tree-view:expand-item
"a": "project_panel::NewFile", // tree-view:add-file
"d": "project_panel::Duplicate", // tree-view:duplicate
"home": "menu::SelectFirst", // core:move-to-top
"end": "menu::SelectLast", // core:move-to-bottom
"shift-a": "project_panel::NewDirectory" // tree-view:add-folder
}
}
]

View File

@@ -0,0 +1,90 @@
[
{
"bindings": {
"ctrl-shift-[": "pane::ActivatePrevItem",
"ctrl-shift-]": "pane::ActivateNextItem"
}
},
{
"context": "Editor",
"bindings": {
"ctrl->": "zed::IncreaseBufferFontSize",
"ctrl-<": "zed::DecreaseBufferFontSize",
"ctrl-shift-j": "editor::JoinLines",
"ctrl-d": "editor::DuplicateLineDown",
"ctrl-y": "editor::DeleteLine",
"ctrl-pagedown": "editor::MovePageDown",
"ctrl-pageup": "editor::MovePageUp",
// "ctrl-alt-shift-b": "editor::SelectToPreviousWordStart",
"ctrl-alt-enter": "editor::NewlineAbove",
"shift-enter": "editor::NewlineBelow",
// "ctrl--": "editor::Fold", // TODO: `ctrl-numpad--` (numpad not implemented)
// "ctrl-+": "editor::UnfoldLines", // TODO: `ctrl-numpad+` (numpad not implemented)
"alt-shift-g": "editor::SplitSelectionIntoLines",
"alt-j": ["editor::SelectNext", { "replace_newest": false }],
"alt-shift-j": ["editor::SelectPrevious", { "replace_newest": false }],
"ctrl-/": ["editor::ToggleComments", { "advance_downwards": true }],
"alt-up": "editor::SelectLargerSyntaxNode",
"alt-down": "editor::SelectSmallerSyntaxNode",
"shift-alt-up": "editor::MoveLineUp",
"shift-alt-down": "editor::MoveLineDown",
"ctrl-alt-l": "editor::Format",
"shift-f6": "editor::Rename",
"ctrl-alt-left": "pane::GoBack",
"ctrl-alt-right": "pane::GoForward",
"alt-f7": "editor::FindAllReferences",
"ctrl-alt-f7": "editor::FindAllReferences",
// "ctrl-b": "editor::GoToDefinition", // Conflicts with workspace::ToggleLeftDock
// "ctrl-alt-b": "editor::GoToDefinitionSplit", // Conflicts with workspace::ToggleLeftDock
"ctrl-shift-b": "editor::GoToTypeDefinition",
"ctrl-alt-shift-b": "editor::GoToTypeDefinitionSplit",
"f2": "editor::GoToDiagnostic",
"shift-f2": "editor::GoToPrevDiagnostic",
"ctrl-alt-shift-down": "editor::GoToHunk",
"ctrl-alt-shift-up": "editor::GoToPrevHunk",
"ctrl-home": "editor::MoveToBeginning",
"ctrl-end": "editor::MoveToEnd",
"ctrl-shift-home": "editor::SelectToBeginning",
"ctrl-shift-end": "editor::SelectToEnd"
}
},
{
"context": "Editor && mode == full",
"bindings": {
"ctrl-f12": "outline::Toggle",
"alt-7": "outline::Toggle",
"ctrl-shift-n": "file_finder::Toggle",
"ctrl-g": "go_to_line::Toggle",
"alt-enter": "editor::ToggleCodeActions"
}
},
{
"context": "Workspace",
"bindings": {
"ctrl-shift-n": "file_finder::Toggle",
"ctrl-shift-a": "command_palette::Toggle",
"shift shift": "command_palette::Toggle",
"ctrl-alt-shift-n": "project_symbols::Toggle",
"alt-1": "workspace::ToggleLeftDock",
"ctrl-e": "tab_switcher::Toggle",
"alt-6": "diagnostics::Deploy"
}
},
{
"context": "Pane",
"bindings": {
"ctrl-alt-left": "pane::GoBack",
"ctrl-alt-right": "pane::GoForward"
}
},
{
"context": "ProjectPanel",
"bindings": {
"enter": "project_panel::Open",
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
"delete": ["project_panel::Trash", { "skip_prompt": false }],
"shift-delete": ["project_panel::Delete", { "skip_prompt": false }],
"shift-f6": "project_panel::Rename"
}
}
]

View File

@@ -0,0 +1,53 @@
[
{
"bindings": {
"ctrl-shift-[": "pane::ActivatePrevItem",
"ctrl-shift-]": "pane::ActivateNextItem",
"ctrl-pagedown": "pane::ActivatePrevItem",
"ctrl-pageup": "pane::ActivateNextItem",
"ctrl-shift-tab": "pane::ActivateNextItem",
"ctrl-tab": "pane::ActivatePrevItem"
}
},
{
"context": "Editor",
"bindings": {
"ctrl-shift-up": "editor::AddSelectionAbove",
"ctrl-shift-down": "editor::AddSelectionBelow",
"ctrl-shift-m": "editor::SelectLargerSyntaxNode",
"ctrl-shift-l": "editor::SplitSelectionIntoLines",
"ctrl-shift-a": "editor::SelectLargerSyntaxNode",
"ctrl-shift-d": "editor::DuplicateLineDown",
"f12": "editor::GoToDefinition",
"ctrl-f12": "editor::GoToDefinitionSplit",
"shift-f12": "editor::FindAllReferences",
"ctrl-shift-f12": "editor::FindAllReferences",
"ctrl-.": "editor::GoToHunk",
"ctrl-,": "editor::GoToPrevHunk",
"shift-alt-m": "markdown::OpenPreviewToTheSide",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd"
}
},
{
"context": "Editor && mode == full",
"bindings": {
"ctrl-r": "outline::Toggle"
}
},
{
"context": "Pane",
"bindings": {
"f4": "search::SelectNextMatch",
"shift-f4": "search::SelectPrevMatch"
}
},
{
"context": "Workspace",
"bindings": {
"ctrl-k ctrl-b": "workspace::ToggleLeftDock",
// "ctrl-0": "project_panel::ToggleFocus", // normally resets zoom
"shift-ctrl-r": "project_symbols::Toggle"
}
}
]

View File

@@ -1,6 +1,8 @@
// Default Keymap (Atom) for Zed on MacOS
[
{
"bindings": {
"ctrl-alt-cmd-l": "workspace::Reload",
"cmd-k cmd-p": "workspace::ActivatePreviousPane",
"cmd-k cmd-n": "workspace::ActivateNextPane"
}
@@ -8,24 +10,22 @@
{
"context": "Editor",
"bindings": {
"ctrl-shift-l": "language_selector::Toggle",
"cmd-|": "pane::RevealInProjectPanel",
"cmd-b": "editor::GoToDefinition",
"alt-cmd-b": "editor::GoToDefinitionSplit",
"cmd-<": "editor::ScrollCursorCenter",
"cmd-g": [
"editor::SelectNext",
{
"replace_newest": true
}
],
"ctrl-cmd-g": [
"editor::SelectPrevious",
{
"replace_newest": true
}
],
"cmd-g": ["editor::SelectNext", { "replace_newest": true }],
"cmd-shift-g": ["editor::SelectPrevious", { "replace_newest": true }],
"ctrl-shift-down": "editor::AddSelectionBelow",
"ctrl-shift-up": "editor::AddSelectionAbove",
"cmd-shift-backspace": "editor::DeleteToBeginningOfLine",
"cmd-k cmd-u": "editor::ConvertToUpperCase",
"cmd-k cmd-l": "editor::ConvertToLowerCase",
"alt-enter": "editor::Newline",
"cmd-shift-d": "editor::DuplicateLineDown",
"ctrl-cmd-up": "editor::MoveLineUp",
"ctrl-cmd-down": "editor::MoveLineDown",
"ctrl-shift-m": "markdown::OpenPreviewToTheSide"
}
},
@@ -74,21 +74,22 @@
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
"cmd-x": "project_panel::Cut",
"cmd-c": "project_panel::Copy",
"cmd-v": "project_panel::Paste",
"ctrl-[": "project_panel::CollapseSelectedEntry",
"ctrl-b": "project_panel::CollapseSelectedEntry",
"alt-b": "project_panel::CollapseSelectedEntry",
"ctrl-]": "project_panel::ExpandSelectedEntry",
"ctrl-f": "project_panel::ExpandSelectedEntry",
"ctrl-shift-c": "project_panel::CopyPath"
"cmd-v": "project_panel::Paste"
}
},
{
"context": "ProjectPanel && not_editing",
"bindings": {
"ctrl-shift-c": "project_panel::CopyPath",
"ctrl-[": "project_panel::CollapseSelectedEntry",
"ctrl-b": "project_panel::CollapseSelectedEntry",
"ctrl-]": "project_panel::ExpandSelectedEntry",
"ctrl-f": "project_panel::ExpandSelectedEntry",
"a": "project_panel::NewFile",
"shift-a": "project_panel::NewDirectory",
"shift-d": "project_panel::Duplicate"
"d": "project_panel::Duplicate",
"home": "menu::SelectFirst",
"end": "menu::SelectLast",
"shift-a": "project_panel::NewDirectory"
}
}
]

View File

@@ -21,24 +21,9 @@
"cmd--": "editor::Fold",
"cmd-+": "editor::UnfoldLines",
"alt-shift-g": "editor::SplitSelectionIntoLines",
"ctrl-g": [
"editor::SelectNext",
{
"replace_newest": false
}
],
"ctrl-cmd-g": [
"editor::SelectPrevious",
{
"replace_newest": false
}
],
"cmd-/": [
"editor::ToggleComments",
{
"advance_downwards": true
}
],
"ctrl-g": ["editor::SelectNext", { "replace_newest": false }],
"ctrl-cmd-g": ["editor::SelectPrevious", { "replace_newest": false }],
"cmd-/": ["editor::ToggleComments", { "advance_downwards": true }],
"alt-up": "editor::SelectLargerSyntaxNode",
"alt-down": "editor::SelectSmallerSyntaxNode",
"shift-alt-up": "editor::MoveLineUp",
@@ -54,7 +39,7 @@
"cmd-shift-b": "editor::GoToTypeDefinition",
"cmd-alt-shift-b": "editor::GoToTypeDefinitionSplit",
"f2": "editor::GoToDiagnostic",
"cmd-f2": "editor::GoToPrevDiagnostic",
"shift-f2": "editor::GoToPrevDiagnostic",
"ctrl-alt-shift-down": "editor::GoToHunk",
"ctrl-alt-shift-up": "editor::GoToPrevHunk",
"cmd-home": "editor::MoveToBeginning",

View File

@@ -6,8 +6,7 @@
"ctrl-pagedown": "pane::ActivatePrevItem",
"ctrl-pageup": "pane::ActivateNextItem",
"ctrl-shift-tab": "pane::ActivateNextItem",
"ctrl-tab": "pane::ActivatePrevItem",
"cmd-+": "zed::IncreaseBufferFontSize"
"ctrl-tab": "pane::ActivatePrevItem"
}
},
{
@@ -26,6 +25,9 @@
"alt-shift-cmd-down": "editor::FindAllReferences",
"ctrl-.": "editor::GoToHunk",
"ctrl-,": "editor::GoToPrevHunk",
"cmd-k cmd-u": "editor::ConvertToUpperCase",
"cmd-k cmd-l": "editor::ConvertToLowerCase",
"shift-alt-m": "markdown::OpenPreviewToTheSide",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd"
}

View File

@@ -22,34 +22,14 @@
"alt-shift-delete": "editor::DeleteToNextWordEnd",
"ctrl-backspace": "editor::DeleteToPreviousSubwordStart",
"ctrl-delete": "editor::DeleteToNextSubwordEnd",
"alt-left": [
"editor::MoveToPreviousWordStart",
{
"stop_at_soft_wraps": true
}
],
"alt-right": [
"editor::MoveToNextWordEnd",
{
"stop_at_soft_wraps": true
}
],
"alt-left": ["editor::MoveToPreviousWordStart", { "stop_at_soft_wraps": true }],
"alt-right": ["editor::MoveToNextWordEnd", { "stop_at_soft_wraps": true }],
"ctrl-left": "editor::MoveToPreviousSubwordStart",
"ctrl-right": "editor::MoveToNextSubwordEnd",
"cmd-shift-left": "editor::SelectToBeginningOfLine",
"cmd-shift-right": "editor::SelectToEndOfLine",
"alt-shift-left": [
"editor::SelectToPreviousWordStart",
{
"stop_at_soft_wraps": true
}
],
"alt-shift-right": [
"editor::SelectToNextWordEnd",
{
"stop_at_soft_wraps": true
}
],
"alt-shift-left": ["editor::SelectToPreviousWordStart", { "stop_at_soft_wraps": true }],
"alt-shift-right": ["editor::SelectToNextWordEnd", { "stop_at_soft_wraps": true }],
"ctrl-shift-left": "editor::SelectToPreviousSubwordStart",
"ctrl-shift-right": "editor::SelectToNextSubwordEnd",
"ctrl-w": "editor::SelectNext",

View File

@@ -8,22 +8,8 @@
{
"context": "Editor && VimControl && !VimWaiting && !menu",
"bindings": {
"i": [
"vim::PushOperator",
{
"Object": {
"around": false
}
}
],
"a": [
"vim::PushOperator",
{
"Object": {
"around": true
}
}
],
"i": ["vim::PushOperator", { "Object": { "around": false } }],
"a": ["vim::PushOperator", { "Object": { "around": true } }],
":": "command_palette::Toggle",
"h": "vim::Left",
"left": "vim::Left",
@@ -31,6 +17,7 @@
"j": "vim::Down",
"down": "vim::Down",
"enter": "vim::NextLineStart",
"ctrl-m": "vim::NextLineStart",
"tab": "vim::Tab",
"shift-tab": "vim::Tab",
"k": "vim::Up",
@@ -57,92 +44,25 @@
// "b": "vim::PreviousSubwordStart",
// "e": "vim::NextSubwordEnd",
// "g e": "vim::PreviousSubwordEnd",
"shift-w": [
"vim::NextWordStart",
{
"ignorePunctuation": true
}
],
"shift-e": [
"vim::NextWordEnd",
{
"ignorePunctuation": true
}
],
"shift-b": [
"vim::PreviousWordStart",
{
"ignorePunctuation": true
}
],
"g shift-e": [
"vim::PreviousWordEnd",
{
"ignorePunctuation": true
}
],
"shift-w": ["vim::NextWordStart", { "ignorePunctuation": true }],
"shift-e": ["vim::NextWordEnd", { "ignorePunctuation": true }],
"shift-b": ["vim::PreviousWordStart", { "ignorePunctuation": true }],
"g shift-e": ["vim::PreviousWordEnd", { "ignorePunctuation": true }],
"/": "vim::Search",
"g /": "pane::DeploySearch",
"?": [
"vim::Search",
{
"backwards": true
}
],
"?": ["vim::Search", { "backwards": true }],
"*": "vim::MoveToNext",
"#": "vim::MoveToPrev",
"n": "vim::MoveToNextMatch",
"shift-n": "vim::MoveToPrevMatch",
"%": "vim::Matching",
"f": [
"vim::PushOperator",
{
"FindForward": {
"before": false
}
}
],
"t": [
"vim::PushOperator",
{
"FindForward": {
"before": true
}
}
],
"shift-f": [
"vim::PushOperator",
{
"FindBackward": {
"after": false
}
}
],
"shift-t": [
"vim::PushOperator",
{
"FindBackward": {
"after": true
}
}
],
"f": ["vim::PushOperator", { "FindForward": { "before": false } }],
"t": ["vim::PushOperator", { "FindForward": { "before": true } }],
"shift-f": ["vim::PushOperator", { "FindBackward": { "after": false } }],
"shift-t": ["vim::PushOperator", { "FindBackward": { "after": true } }],
"m": ["vim::PushOperator", "Mark"],
"'": [
"vim::PushOperator",
{
"Jump": {
"line": true
}
}
],
"`": [
"vim::PushOperator",
{
"Jump": {
"line": false
}
}
],
"'": ["vim::PushOperator", { "Jump": { "line": true } }],
"`": ["vim::PushOperator", { "Jump": { "line": false } }],
";": "vim::RepeatFind",
",": "vim::RepeatFindReversed",
"ctrl-o": "pane::GoBack",
@@ -179,90 +99,25 @@
"g shift-n": "vim::SelectPreviousMatch",
"g l": "vim::SelectNext",
"g shift-l": "vim::SelectPrevious",
"g >": [
"editor::SelectNext",
{
"replace_newest": true
}
],
"g <": [
"editor::SelectPrevious",
{
"replace_newest": true
}
],
"g >": ["editor::SelectNext", { "replace_newest": true }],
"g <": ["editor::SelectPrevious", { "replace_newest": true }],
"g a": "editor::SelectAllMatches",
"g s": "outline::Toggle",
"g shift-s": "project_symbols::Toggle",
"g .": "editor::ToggleCodeActions", // zed specific
"g shift-a": "editor::FindAllReferences", // zed specific
"g space": "editor::OpenExcerpts", // zed specific
"g *": [
"vim::MoveToNext",
{
"partialWord": true
}
],
"g #": [
"vim::MoveToPrev",
{
"partialWord": true
}
],
"g j": [
"vim::Down",
{
"displayLines": true
}
],
"g down": [
"vim::Down",
{
"displayLines": true
}
],
"g k": [
"vim::Up",
{
"displayLines": true
}
],
"g up": [
"vim::Up",
{
"displayLines": true
}
],
"g $": [
"vim::EndOfLine",
{
"displayLines": true
}
],
"g end": [
"vim::EndOfLine",
{
"displayLines": true
}
],
"g 0": [
"vim::StartOfLine",
{
"displayLines": true
}
],
"g home": [
"vim::StartOfLine",
{
"displayLines": true
}
],
"g ^": [
"vim::FirstNonWhitespace",
{
"displayLines": true
}
],
"g *": ["vim::MoveToNext", { "partialWord": true }],
"g #": ["vim::MoveToPrev", { "partialWord": true }],
"g j": ["vim::Down", { "displayLines": true }],
"g down": ["vim::Down", { "displayLines": true }],
"g k": ["vim::Up", { "displayLines": true }],
"g up": ["vim::Up", { "displayLines": true }],
"g $": ["vim::EndOfLine", { "displayLines": true }],
"g end": ["vim::EndOfLine", { "displayLines": true }],
"g 0": ["vim::StartOfLine", { "displayLines": true }],
"g home": ["vim::StartOfLine", { "displayLines": true }],
"g ^": ["vim::FirstNonWhitespace", { "displayLines": true }],
"g v": "vim::RestoreVisualSelection",
"g ]": "editor::GoToDiagnostic",
"g [": "editor::GoToPrevDiagnostic",
@@ -280,18 +135,8 @@
"z c": "editor::Fold",
"z o": "editor::UnfoldLines",
"z f": "editor::FoldSelectedRanges",
"shift-z shift-q": [
"pane::CloseActiveItem",
{
"saveIntent": "skip"
}
],
"shift-z shift-z": [
"pane::CloseActiveItem",
{
"saveIntent": "saveAll"
}
],
"shift-z shift-q": ["pane::CloseActiveItem", { "saveIntent": "skip" }],
"shift-z shift-z": ["pane::CloseActiveItem", { "saveIntent": "saveAll" }],
// Count support
"1": ["vim::Number", 1],
"2": ["vim::Number", 2],
@@ -303,6 +148,7 @@
"8": ["vim::Number", 8],
"9": ["vim::Number", 9],
// window related commands (ctrl-w X)
"ctrl-w": null,
"ctrl-w left": ["workspace::ActivatePaneInDirection", "Left"],
"ctrl-w right": ["workspace::ActivatePaneInDirection", "Right"],
"ctrl-w up": ["workspace::ActivatePaneInDirection", "Up"],
@@ -386,14 +232,9 @@
"ctrl-a": "vim::Increment",
"ctrl-x": "vim::Decrement",
"p": "vim::Paste",
"shift-p": [
"vim::Paste",
{
"before": true
}
],
"u": "editor::Undo",
"ctrl-r": "editor::Redo",
"shift-p": ["vim::Paste", { "before": true }],
"u": "vim::Undo",
"ctrl-r": "vim::Redo",
"r": ["vim::PushOperator", "Replace"],
"s": "vim::Substitute",
"shift-s": "vim::SubstituteLine",
@@ -442,12 +283,7 @@
{
"context": "Editor && vim_mode == normal && vim_operator == c",
"bindings": {
"s": [
"vim::PushOperator",
{
"ChangeSurrounds": {}
}
]
"s": ["vim::PushOperator", { "ChangeSurrounds": {} }]
}
},
{
@@ -492,12 +328,7 @@
{
"context": "Editor && vim_mode == normal && vim_operator == y",
"bindings": {
"s": [
"vim::PushOperator",
{
"AddSurrounds": {}
}
]
"s": ["vim::PushOperator", { "AddSurrounds": {} }]
}
},
{
@@ -522,12 +353,7 @@
"context": "Editor && VimObject",
"bindings": {
"w": "vim::Word",
"shift-w": [
"vim::Word",
{
"ignorePunctuation": true
}
],
"shift-w": ["vim::Word", { "ignorePunctuation": true }],
"t": "vim::Tag",
"s": "vim::Sentence",
"p": "vim::Paragraph",
@@ -562,43 +388,18 @@
"y": "vim::VisualYank",
"shift-y": "vim::VisualYank",
"p": "vim::Paste",
"shift-p": [
"vim::Paste",
{
"preserveClipboard": true
}
],
"shift-p": ["vim::Paste", { "preserveClipboard": true }],
"s": "vim::Substitute",
"shift-s": "vim::SubstituteLine",
"shift-r": "vim::SubstituteLine",
"c": "vim::Substitute",
"~": "vim::ChangeCase",
"*": [
"vim::MoveToNext",
{
"partialWord": true
}
],
"#": [
"vim::MoveToPrev",
{
"partialWord": true
}
],
"*": ["vim::MoveToNext", { "partialWord": true }],
"#": ["vim::MoveToPrev", { "partialWord": true }],
"ctrl-a": "vim::Increment",
"ctrl-x": "vim::Decrement",
"g ctrl-a": [
"vim::Increment",
{
"step": true
}
],
"g ctrl-x": [
"vim::Decrement",
{
"step": true
}
],
"g ctrl-a": ["vim::Increment", { "step": true }],
"g ctrl-x": ["vim::Decrement", { "step": true }],
"shift-i": "vim::InsertBefore",
"shift-a": "vim::InsertAfter",
"shift-j": "vim::JoinLines",
@@ -608,22 +409,8 @@
"ctrl-[": ["vim::SwitchMode", "Normal"],
">": "vim::Indent",
"<": "vim::Outdent",
"i": [
"vim::PushOperator",
{
"Object": {
"around": false
}
}
],
"a": [
"vim::PushOperator",
{
"Object": {
"around": true
}
}
]
"i": ["vim::PushOperator", { "Object": { "around": false } }],
"a": ["vim::PushOperator", { "Object": { "around": true } }]
}
},
{
@@ -644,6 +431,7 @@
"escape": "vim::NormalBefore",
"ctrl-c": "vim::NormalBefore",
"ctrl-[": "vim::NormalBefore",
"ctrl-x": null,
"ctrl-x ctrl-o": "editor::ShowCompletions",
"ctrl-x ctrl-a": "assistant::InlineAssist", // zed specific
"ctrl-x ctrl-c": "editor::ShowInlineCompletion", // zed specific
@@ -717,7 +505,7 @@
"t": "project_panel::OpenPermanent",
"v": "project_panel::OpenPermanent",
"p": "project_panel::Open",
"x": "project_panel::RevealInFinder",
"x": "project_panel::RevealInFileManager",
"shift-g": "menu::SelectLast",
"g g": "menu::SelectFirst",
"-": "project_panel::SelectParent"

View File

@@ -690,6 +690,7 @@
// }
//
"file_types": {
"JSON": ["flake.lock"],
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json"]
},
// The extensions that Zed should automatically install on startup.
@@ -775,7 +776,8 @@
"PHP": {
"prettier": {
"allowed": true,
"plugins": ["@prettier/plugin-php"]
"plugins": ["@prettier/plugin-php"],
"parser": "php"
}
},
"Ruby": {

View File

@@ -15,25 +15,25 @@
"elevated_surface.background": "#2f343eff",
"surface.background": "#2f343eff",
"background": "#3b414dff",
"element.background": "#2f343eff",
"element.background": "#2e343eff",
"element.hover": "#363c46ff",
"element.active": "#454a56ff",
"element.selected": "#454a56ff",
"element.disabled": "#2f343eff",
"element.disabled": "#2e343eff",
"drop_target.background": "#83899480",
"ghost_element.background": "#00000000",
"ghost_element.hover": "#363c46ff",
"ghost_element.active": "#454a56ff",
"ghost_element.selected": "#454a56ff",
"ghost_element.disabled": "#2f343eff",
"ghost_element.disabled": "#2e343eff",
"text": "#c8ccd4ff",
"text.muted": "#838994ff",
"text.placeholder": "#555a63ff",
"text.disabled": "#555a63ff",
"text.placeholder": "#696B77ff",
"text.disabled": "#696B77ff",
"text.accent": "#74ade8ff",
"icon": "#c8ccd4ff",
"icon.muted": "#838994ff",
"icon.disabled": "#555a63ff",
"icon.disabled": "#696B77ff",
"icon.placeholder": "#838994ff",
"icon.accent": "#74ade8ff",
"status_bar.background": "#3b414dff",
@@ -59,7 +59,7 @@
"editor.highlighted_line.background": "#2f343eff",
"editor.line_number": "#c8ccd459",
"editor.active_line_number": "#c8ccd4ff",
"editor.invisible": "#555a63ff",
"editor.invisible": "#696B77ff",
"editor.wrap_guide": "#c8ccd40d",
"editor.active_wrap_guide": "#c8ccd41a",
"editor.document_highlight.read_background": "#74ade81a",
@@ -94,46 +94,46 @@
"terminal.ansi.dim_white": "#575d65ff",
"link_text.hover": "#74ade8ff",
"conflict": "#dec184ff",
"conflict.background": "#41321dff",
"conflict.background": "#dec1841a",
"conflict.border": "#5d4c2fff",
"created": "#a1c181ff",
"created.background": "#222e1dff",
"created.background": "#a1c1811a",
"created.border": "#38482fff",
"deleted": "#d07277ff",
"deleted.background": "#301b1bff",
"deleted.background": "#d072771a",
"deleted.border": "#4c2b2cff",
"error": "#d07277ff",
"error.background": "#301b1bff",
"error.background": "#d072771a",
"error.border": "#4c2b2cff",
"hidden": "#555a63ff",
"hidden.background": "#3b414dff",
"hidden": "#696B77ff",
"hidden.background": "#696B771a",
"hidden.border": "#414754ff",
"hint": "#5a6f89ff",
"hint.background": "#18243dff",
"hint.background": "#5a6f891a",
"hint.border": "#293b5bff",
"ignored": "#555a63ff",
"ignored.background": "#3b414dff",
"ignored": "#696B77ff",
"ignored.background": "#696B771a",
"ignored.border": "#464b57ff",
"info": "#74ade8ff",
"info.background": "#18243dff",
"info.background": "#74ade81a",
"info.border": "#293b5bff",
"modified": "#dec184ff",
"modified.background": "#41321dff",
"modified.background": "#dec1841a",
"modified.border": "#5d4c2fff",
"predictive": "#5a6a87ff",
"predictive.background": "#222e1dff",
"predictive.background": "#5a6a871a",
"predictive.border": "#38482fff",
"renamed": "#74ade8ff",
"renamed.background": "#18243dff",
"renamed.background": "#74ade81a",
"renamed.border": "#293b5bff",
"success": "#a1c181ff",
"success.background": "#222e1dff",
"success.background": "#a1c1811a",
"success.border": "#38482fff",
"unreachable": "#838994ff",
"unreachable.background": "#3b414dff",
"unreachable.background": "#8389941a",
"unreachable.border": "#464b57ff",
"warning": "#dec184ff",
"warning.background": "#41321dff",
"warning.background": "#dec1841a",
"warning.border": "#5d4c2fff",
"players": [
{

View File

@@ -12,19 +12,28 @@ workspace = true
path = "src/assistant.rs"
doctest = false
[features]
test-support = [
"editor/test-support",
"language/test-support",
"project/test-support",
"text/test-support",
]
[dependencies]
anthropic = { workspace = true, features = ["schemars"] }
anyhow.workspace = true
assistant_slash_command.workspace = true
async-watch.workspace = true
breadcrumbs.workspace = true
cargo_toml.workspace = true
chrono.workspace = true
client.workspace = true
clock.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
editor.workspace = true
feature_flags.workspace = true
file_icons.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
@@ -72,7 +81,9 @@ picker.workspace = true
ctor.workspace = true
editor = { workspace = true, features = ["test-support"] }
env_logger.workspace = true
language = { workspace = true, features = ["test-support"] }
log.workspace = true
project = { workspace = true, features = ["test-support"] }
rand.workspace = true
text = { workspace = true, features = ["test-support"] }
unindent.workspace = true

View File

@@ -1,7 +1,8 @@
pub mod assistant_panel;
pub mod assistant_settings;
mod completion_provider;
mod context_store;
mod context;
pub mod context_store;
mod inline_assistant;
mod model_selector;
mod prompt_library;
@@ -16,8 +17,9 @@ use assistant_settings::{AnthropicModel, AssistantSettings, CloudModel, OllamaMo
use assistant_slash_command::SlashCommandRegistry;
use client::{proto, Client};
use command_palette_hooks::CommandPaletteFilter;
pub(crate) use completion_provider::*;
pub(crate) use context_store::*;
pub use completion_provider::*;
pub use context::*;
pub use context_store::*;
use fs::Fs;
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
use indexed_docs::IndexedDocsRegistry;
@@ -49,17 +51,22 @@ actions!(
ResetKey,
InlineAssist,
InsertActivePrompt,
ToggleHistory,
DeployHistory,
DeployPromptLibrary,
ApplyEdit,
ConfirmCommand,
ToggleModelSelector
]
);
#[derive(
Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
struct MessageId(usize);
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct MessageId(clock::Lamport);
impl MessageId {
pub fn as_u64(self) -> u64 {
self.0.as_u64()
}
}
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
@@ -70,8 +77,26 @@ pub enum Role {
}
impl Role {
pub fn cycle(&mut self) {
*self = match self {
pub fn from_proto(role: i32) -> Role {
match proto::LanguageModelRole::from_i32(role) {
Some(proto::LanguageModelRole::LanguageModelUser) => Role::User,
Some(proto::LanguageModelRole::LanguageModelAssistant) => Role::Assistant,
Some(proto::LanguageModelRole::LanguageModelSystem) => Role::System,
Some(proto::LanguageModelRole::LanguageModelTool) => Role::System,
None => Role::User,
}
}
pub fn to_proto(&self) -> proto::LanguageModelRole {
match self {
Role::User => proto::LanguageModelRole::LanguageModelUser,
Role::Assistant => proto::LanguageModelRole::LanguageModelAssistant,
Role::System => proto::LanguageModelRole::LanguageModelSystem,
}
}
pub fn cycle(self) -> Role {
match self {
Role::User => Role::Assistant,
Role::Assistant => Role::System,
Role::System => Role::User,
@@ -150,11 +175,7 @@ pub struct LanguageModelRequestMessage {
impl LanguageModelRequestMessage {
pub fn to_proto(&self) -> proto::LanguageModelRequestMessage {
proto::LanguageModelRequestMessage {
role: match self.role {
Role::User => proto::LanguageModelRole::LanguageModelUser,
Role::Assistant => proto::LanguageModelRole::LanguageModelAssistant,
Role::System => proto::LanguageModelRole::LanguageModelSystem,
} as i32,
role: self.role.to_proto() as i32,
content: self.content.clone(),
tool_calls: Vec::new(),
tool_call_id: None,
@@ -162,7 +183,7 @@ impl LanguageModelRequestMessage {
}
}
#[derive(Debug, Default, Serialize)]
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct LanguageModelRequest {
pub model: LanguageModel,
pub messages: Vec<LanguageModelRequestMessage>,
@@ -221,19 +242,48 @@ pub struct LanguageModelChoiceDelta {
pub finish_reason: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
struct MessageMetadata {
role: Role,
status: MessageStatus,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
enum MessageStatus {
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum MessageStatus {
Pending,
Done,
Error(SharedString),
}
impl MessageStatus {
pub fn from_proto(status: proto::ContextMessageStatus) -> MessageStatus {
match status.variant {
Some(proto::context_message_status::Variant::Pending(_)) => MessageStatus::Pending,
Some(proto::context_message_status::Variant::Done(_)) => MessageStatus::Done,
Some(proto::context_message_status::Variant::Error(error)) => {
MessageStatus::Error(error.message.into())
}
None => MessageStatus::Pending,
}
}
pub fn to_proto(&self) -> proto::ContextMessageStatus {
match self {
MessageStatus::Pending => proto::ContextMessageStatus {
variant: Some(proto::context_message_status::Variant::Pending(
proto::context_message_status::Pending {},
)),
},
MessageStatus::Done => proto::ContextMessageStatus {
variant: Some(proto::context_message_status::Variant::Done(
proto::context_message_status::Done {},
)),
},
MessageStatus::Error(message) => proto::ContextMessageStatus {
variant: Some(proto::context_message_status::Variant::Error(
proto::context_message_status::Error {
message: message.to_string(),
},
)),
},
}
}
}
/// The state pertaining to the Assistant.
#[derive(Default)]
struct Assistant {
@@ -286,6 +336,7 @@ pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, cx: &mut AppContext) {
})
.detach();
context_store::init(&client);
prompt_library::init(cx);
completion_provider::init(client.clone(), cx);
assistant_slash_command::init(cx);

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
use std::fmt;
use crate::{preprocess_anthropic_request, LanguageModel, LanguageModelRequest};
pub use anthropic::Model as AnthropicModel;
use gpui::Pixels;
pub use ollama::Model as OllamaModel;
@@ -15,8 +16,6 @@ use serde::{
use settings::{Settings, SettingsSources};
use strum::{EnumIter, IntoEnumIterator};
use crate::{preprocess_anthropic_request, LanguageModel, LanguageModelRequest};
#[derive(Clone, Debug, Default, PartialEq, EnumIter)]
pub enum CloudModel {
Gpt3Point5Turbo,

View File

@@ -1,16 +1,18 @@
mod anthropic;
mod cloud;
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
mod fake;
mod ollama;
mod open_ai;
pub use anthropic::*;
pub use cloud::*;
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
pub use fake::*;
pub use ollama::*;
pub use open_ai::*;
use parking_lot::RwLock;
use smol::lock::{Semaphore, SemaphoreGuardArc};
use crate::{
assistant_settings::{AssistantProvider, AssistantSettings},
@@ -21,8 +23,8 @@ use client::Client;
use futures::{future::BoxFuture, stream::BoxStream};
use gpui::{AnyView, AppContext, BorrowAppContext, Task, WindowContext};
use settings::{Settings, SettingsStore};
use std::sync::Arc;
use std::time::Duration;
use std::{any::Any, sync::Arc};
/// Choose which model to use for openai provider.
/// If the model is not available, try to use the first available model, or fallback to the original model.
@@ -39,176 +41,117 @@ fn choose_openai_model(
}
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
let mut settings_version = 0;
let provider = match &AssistantSettings::get_global(cx).provider {
AssistantProvider::ZedDotDev { model } => CompletionProvider::Cloud(
CloudCompletionProvider::new(model.clone(), client.clone(), settings_version, cx),
),
AssistantProvider::OpenAi {
model,
api_url,
low_speed_timeout_in_seconds,
available_models,
} => CompletionProvider::OpenAi(OpenAiCompletionProvider::new(
choose_openai_model(model, available_models),
api_url.clone(),
client.http_client(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
)),
AssistantProvider::Anthropic {
model,
api_url,
low_speed_timeout_in_seconds,
} => CompletionProvider::Anthropic(AnthropicCompletionProvider::new(
model.clone(),
api_url.clone(),
client.http_client(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
)),
AssistantProvider::Ollama {
model,
api_url,
low_speed_timeout_in_seconds,
} => CompletionProvider::Ollama(OllamaCompletionProvider::new(
model.clone(),
api_url.clone(),
client.http_client(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
cx,
)),
};
cx.set_global(provider);
let provider = create_provider_from_settings(client.clone(), 0, cx);
cx.set_global(CompletionProvider::new(provider, Some(client)));
let mut settings_version = 0;
cx.observe_global::<SettingsStore>(move |cx| {
settings_version += 1;
cx.update_global::<CompletionProvider, _>(|provider, cx| {
match (&mut *provider, &AssistantSettings::get_global(cx).provider) {
(
CompletionProvider::OpenAi(provider),
AssistantProvider::OpenAi {
model,
api_url,
low_speed_timeout_in_seconds,
available_models,
},
) => {
provider.update(
choose_openai_model(model, available_models),
api_url.clone(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
);
}
(
CompletionProvider::Anthropic(provider),
AssistantProvider::Anthropic {
model,
api_url,
low_speed_timeout_in_seconds,
},
) => {
provider.update(
model.clone(),
api_url.clone(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
);
}
(
CompletionProvider::Ollama(provider),
AssistantProvider::Ollama {
model,
api_url,
low_speed_timeout_in_seconds,
},
) => {
provider.update(
model.clone(),
api_url.clone(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
cx,
);
}
(CompletionProvider::Cloud(provider), AssistantProvider::ZedDotDev { model }) => {
provider.update(model.clone(), settings_version);
}
(_, AssistantProvider::ZedDotDev { model }) => {
*provider = CompletionProvider::Cloud(CloudCompletionProvider::new(
model.clone(),
client.clone(),
settings_version,
cx,
));
}
(
_,
AssistantProvider::OpenAi {
model,
api_url,
low_speed_timeout_in_seconds,
available_models,
},
) => {
*provider = CompletionProvider::OpenAi(OpenAiCompletionProvider::new(
choose_openai_model(model, available_models),
api_url.clone(),
client.http_client(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
));
}
(
_,
AssistantProvider::Anthropic {
model,
api_url,
low_speed_timeout_in_seconds,
},
) => {
*provider = CompletionProvider::Anthropic(AnthropicCompletionProvider::new(
model.clone(),
api_url.clone(),
client.http_client(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
));
}
(
_,
AssistantProvider::Ollama {
model,
api_url,
low_speed_timeout_in_seconds,
},
) => {
*provider = CompletionProvider::Ollama(OllamaCompletionProvider::new(
model.clone(),
api_url.clone(),
client.http_client(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
cx,
));
}
}
provider.update_settings(settings_version, cx);
})
})
.detach();
}
pub enum CompletionProvider {
OpenAi(OpenAiCompletionProvider),
Anthropic(AnthropicCompletionProvider),
Cloud(CloudCompletionProvider),
#[cfg(test)]
Fake(FakeCompletionProvider),
Ollama(OllamaCompletionProvider),
pub struct CompletionResponse {
pub inner: BoxFuture<'static, Result<BoxStream<'static, Result<String>>>>,
_lock: SemaphoreGuardArc,
}
pub trait LanguageModelCompletionProvider: Send + Sync {
fn available_models(&self, cx: &AppContext) -> Vec<LanguageModel>;
fn settings_version(&self) -> usize;
fn is_authenticated(&self) -> bool;
fn authenticate(&self, cx: &AppContext) -> Task<Result<()>>;
fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView;
fn reset_credentials(&self, cx: &AppContext) -> Task<Result<()>>;
fn model(&self) -> LanguageModel;
fn count_tokens(
&self,
request: LanguageModelRequest,
cx: &AppContext,
) -> BoxFuture<'static, Result<usize>>;
fn complete(
&self,
request: LanguageModelRequest,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>>;
fn as_any_mut(&mut self) -> &mut dyn Any;
}
const MAX_CONCURRENT_COMPLETION_REQUESTS: usize = 4;
pub struct CompletionProvider {
provider: Arc<RwLock<dyn LanguageModelCompletionProvider>>,
client: Option<Arc<Client>>,
request_limiter: Arc<Semaphore>,
}
impl CompletionProvider {
pub fn new(
provider: Arc<RwLock<dyn LanguageModelCompletionProvider>>,
client: Option<Arc<Client>>,
) -> Self {
Self {
provider,
client,
request_limiter: Arc::new(Semaphore::new(MAX_CONCURRENT_COMPLETION_REQUESTS)),
}
}
pub fn available_models(&self, cx: &AppContext) -> Vec<LanguageModel> {
self.provider.read().available_models(cx)
}
pub fn settings_version(&self) -> usize {
self.provider.read().settings_version()
}
pub fn is_authenticated(&self) -> bool {
self.provider.read().is_authenticated()
}
pub fn authenticate(&self, cx: &AppContext) -> Task<Result<()>> {
self.provider.read().authenticate(cx)
}
pub fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
self.provider.read().authentication_prompt(cx)
}
pub fn reset_credentials(&self, cx: &AppContext) -> Task<Result<()>> {
self.provider.read().reset_credentials(cx)
}
pub fn model(&self) -> LanguageModel {
self.provider.read().model()
}
pub fn count_tokens(
&self,
request: LanguageModelRequest,
cx: &AppContext,
) -> BoxFuture<'static, Result<usize>> {
self.provider.read().count_tokens(request, cx)
}
pub fn complete(
&self,
request: LanguageModelRequest,
cx: &AppContext,
) -> Task<CompletionResponse> {
let rate_limiter = self.request_limiter.clone();
let provider = self.provider.clone();
cx.background_executor().spawn(async move {
let lock = rate_limiter.acquire_arc().await;
let response = provider.read().complete(request);
CompletionResponse {
inner: response,
_lock: lock,
}
})
}
}
impl gpui::Global for CompletionProvider {}
@@ -218,121 +161,213 @@ impl CompletionProvider {
cx.global::<Self>()
}
pub fn available_models(&self, cx: &AppContext) -> Vec<LanguageModel> {
match self {
CompletionProvider::OpenAi(provider) => provider
.available_models(cx)
.map(LanguageModel::OpenAi)
.collect(),
CompletionProvider::Anthropic(provider) => provider
.available_models()
.map(LanguageModel::Anthropic)
.collect(),
CompletionProvider::Cloud(provider) => provider
.available_models()
.map(LanguageModel::Cloud)
.collect(),
CompletionProvider::Ollama(provider) => provider
.available_models()
.map(|model| LanguageModel::Ollama(model.clone()))
.collect(),
#[cfg(test)]
CompletionProvider::Fake(_) => unimplemented!(),
pub fn update_current_as<R, T: LanguageModelCompletionProvider + 'static>(
&mut self,
update: impl FnOnce(&mut T) -> R,
) -> Option<R> {
let mut provider = self.provider.write();
if let Some(provider) = provider.as_any_mut().downcast_mut::<T>() {
Some(update(provider))
} else {
None
}
}
pub fn settings_version(&self) -> usize {
match self {
CompletionProvider::OpenAi(provider) => provider.settings_version(),
CompletionProvider::Anthropic(provider) => provider.settings_version(),
CompletionProvider::Cloud(provider) => provider.settings_version(),
CompletionProvider::Ollama(provider) => provider.settings_version(),
#[cfg(test)]
CompletionProvider::Fake(_) => unimplemented!(),
}
}
pub fn update_settings(&mut self, version: usize, cx: &mut AppContext) {
let updated = match &AssistantSettings::get_global(cx).provider {
AssistantProvider::ZedDotDev { model } => self
.update_current_as::<_, CloudCompletionProvider>(|provider| {
provider.update(model.clone(), version);
}),
AssistantProvider::OpenAi {
model,
api_url,
low_speed_timeout_in_seconds,
available_models,
} => self.update_current_as::<_, OpenAiCompletionProvider>(|provider| {
provider.update(
choose_openai_model(&model, &available_models),
api_url.clone(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
version,
);
}),
AssistantProvider::Anthropic {
model,
api_url,
low_speed_timeout_in_seconds,
} => self.update_current_as::<_, AnthropicCompletionProvider>(|provider| {
provider.update(
model.clone(),
api_url.clone(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
version,
);
}),
AssistantProvider::Ollama {
model,
api_url,
low_speed_timeout_in_seconds,
} => self.update_current_as::<_, OllamaCompletionProvider>(|provider| {
provider.update(
model.clone(),
api_url.clone(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
version,
cx,
);
}),
};
pub fn is_authenticated(&self) -> bool {
match self {
CompletionProvider::OpenAi(provider) => provider.is_authenticated(),
CompletionProvider::Anthropic(provider) => provider.is_authenticated(),
CompletionProvider::Cloud(provider) => provider.is_authenticated(),
CompletionProvider::Ollama(provider) => provider.is_authenticated(),
#[cfg(test)]
CompletionProvider::Fake(_) => true,
}
}
pub fn authenticate(&self, cx: &AppContext) -> Task<Result<()>> {
match self {
CompletionProvider::OpenAi(provider) => provider.authenticate(cx),
CompletionProvider::Anthropic(provider) => provider.authenticate(cx),
CompletionProvider::Cloud(provider) => provider.authenticate(cx),
CompletionProvider::Ollama(provider) => provider.authenticate(cx),
#[cfg(test)]
CompletionProvider::Fake(_) => Task::ready(Ok(())),
}
}
pub fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
match self {
CompletionProvider::OpenAi(provider) => provider.authentication_prompt(cx),
CompletionProvider::Anthropic(provider) => provider.authentication_prompt(cx),
CompletionProvider::Cloud(provider) => provider.authentication_prompt(cx),
CompletionProvider::Ollama(provider) => provider.authentication_prompt(cx),
#[cfg(test)]
CompletionProvider::Fake(_) => unimplemented!(),
}
}
pub fn reset_credentials(&self, cx: &AppContext) -> Task<Result<()>> {
match self {
CompletionProvider::OpenAi(provider) => provider.reset_credentials(cx),
CompletionProvider::Anthropic(provider) => provider.reset_credentials(cx),
CompletionProvider::Cloud(_) => Task::ready(Ok(())),
CompletionProvider::Ollama(provider) => provider.reset_credentials(cx),
#[cfg(test)]
CompletionProvider::Fake(_) => Task::ready(Ok(())),
}
}
pub fn model(&self) -> LanguageModel {
match self {
CompletionProvider::OpenAi(provider) => LanguageModel::OpenAi(provider.model()),
CompletionProvider::Anthropic(provider) => LanguageModel::Anthropic(provider.model()),
CompletionProvider::Cloud(provider) => LanguageModel::Cloud(provider.model()),
CompletionProvider::Ollama(provider) => LanguageModel::Ollama(provider.model()),
#[cfg(test)]
CompletionProvider::Fake(_) => LanguageModel::default(),
}
}
pub fn count_tokens(
&self,
request: LanguageModelRequest,
cx: &AppContext,
) -> BoxFuture<'static, Result<usize>> {
match self {
CompletionProvider::OpenAi(provider) => provider.count_tokens(request, cx),
CompletionProvider::Anthropic(provider) => provider.count_tokens(request, cx),
CompletionProvider::Cloud(provider) => provider.count_tokens(request, cx),
CompletionProvider::Ollama(provider) => provider.count_tokens(request, cx),
#[cfg(test)]
CompletionProvider::Fake(_) => futures::FutureExt::boxed(futures::future::ready(Ok(0))),
}
}
pub fn complete(
&self,
request: LanguageModelRequest,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
match self {
CompletionProvider::OpenAi(provider) => provider.complete(request),
CompletionProvider::Anthropic(provider) => provider.complete(request),
CompletionProvider::Cloud(provider) => provider.complete(request),
CompletionProvider::Ollama(provider) => provider.complete(request),
#[cfg(test)]
CompletionProvider::Fake(provider) => provider.complete(),
// Previously configured provider was changed to another one
if updated.is_none() {
if let Some(client) = self.client.clone() {
self.provider = create_provider_from_settings(client, version, cx);
} else {
log::warn!("completion provider cannot be created because client is not set");
}
}
}
}
fn create_provider_from_settings(
client: Arc<Client>,
settings_version: usize,
cx: &mut AppContext,
) -> Arc<RwLock<dyn LanguageModelCompletionProvider>> {
match &AssistantSettings::get_global(cx).provider {
AssistantProvider::ZedDotDev { model } => Arc::new(RwLock::new(
CloudCompletionProvider::new(model.clone(), client.clone(), settings_version, cx),
)),
AssistantProvider::OpenAi {
model,
api_url,
low_speed_timeout_in_seconds,
available_models,
} => Arc::new(RwLock::new(OpenAiCompletionProvider::new(
choose_openai_model(&model, &available_models),
api_url.clone(),
client.http_client(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
))),
AssistantProvider::Anthropic {
model,
api_url,
low_speed_timeout_in_seconds,
} => Arc::new(RwLock::new(AnthropicCompletionProvider::new(
model.clone(),
api_url.clone(),
client.http_client(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
))),
AssistantProvider::Ollama {
model,
api_url,
low_speed_timeout_in_seconds,
} => Arc::new(RwLock::new(OllamaCompletionProvider::new(
model.clone(),
api_url.clone(),
client.http_client(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
cx,
))),
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use gpui::AppContext;
use parking_lot::RwLock;
use settings::SettingsStore;
use smol::stream::StreamExt;
use crate::{
completion_provider::MAX_CONCURRENT_COMPLETION_REQUESTS, CompletionProvider,
FakeCompletionProvider, LanguageModelRequest,
};
#[gpui::test]
fn test_rate_limiting(cx: &mut AppContext) {
SettingsStore::test(cx);
let fake_provider = FakeCompletionProvider::setup_test(cx);
let provider = CompletionProvider::new(Arc::new(RwLock::new(fake_provider.clone())), None);
// Enqueue some requests
for i in 0..MAX_CONCURRENT_COMPLETION_REQUESTS * 2 {
let response = provider.complete(
LanguageModelRequest {
temperature: i as f32 / 10.0,
..Default::default()
},
cx,
);
cx.background_executor()
.spawn(async move {
let response = response.await;
let mut stream = response.inner.await.unwrap();
while let Some(message) = stream.next().await {
message.unwrap();
}
})
.detach();
}
cx.background_executor().run_until_parked();
assert_eq!(
fake_provider.completion_count(),
MAX_CONCURRENT_COMPLETION_REQUESTS
);
// Get the first completion request that is in flight and mark it as completed.
let completion = fake_provider
.running_completions()
.into_iter()
.next()
.unwrap();
fake_provider.finish_completion(&completion);
// Ensure that the number of in-flight completion requests is reduced.
assert_eq!(
fake_provider.completion_count(),
MAX_CONCURRENT_COMPLETION_REQUESTS - 1
);
cx.background_executor().run_until_parked();
// Ensure that another completion request was allowed to acquire the lock.
assert_eq!(
fake_provider.completion_count(),
MAX_CONCURRENT_COMPLETION_REQUESTS
);
// Mark all completion requests as finished that are in flight.
for request in fake_provider.running_completions() {
fake_provider.finish_completion(&request);
}
assert_eq!(fake_provider.completion_count(), 0);
// Wait until the background tasks acquire the lock again.
cx.background_executor().run_until_parked();
assert_eq!(
fake_provider.completion_count(),
MAX_CONCURRENT_COMPLETION_REQUESTS - 1
);
// Finish all remaining completion requests.
for request in fake_provider.running_completions() {
fake_provider.finish_completion(&request);
}
cx.background_executor().run_until_parked();
assert_eq!(fake_provider.completion_count(), 0);
}
}

View File

@@ -2,7 +2,7 @@ use crate::{
assistant_settings::AnthropicModel, CompletionProvider, LanguageModel, LanguageModelRequest,
Role,
};
use crate::{count_open_ai_tokens, LanguageModelRequestMessage};
use crate::{count_open_ai_tokens, LanguageModelCompletionProvider, LanguageModelRequestMessage};
use anthropic::{stream_completion, Request, RequestMessage};
use anyhow::{anyhow, Result};
use editor::{Editor, EditorElement, EditorStyle};
@@ -26,50 +26,22 @@ pub struct AnthropicCompletionProvider {
settings_version: usize,
}
impl AnthropicCompletionProvider {
pub fn new(
model: AnthropicModel,
api_url: String,
http_client: Arc<dyn HttpClient>,
low_speed_timeout: Option<Duration>,
settings_version: usize,
) -> Self {
Self {
api_key: None,
api_url,
model,
http_client,
low_speed_timeout,
settings_version,
}
}
pub fn update(
&mut self,
model: AnthropicModel,
api_url: String,
low_speed_timeout: Option<Duration>,
settings_version: usize,
) {
self.model = model;
self.api_url = api_url;
self.low_speed_timeout = low_speed_timeout;
self.settings_version = settings_version;
}
pub fn available_models(&self) -> impl Iterator<Item = AnthropicModel> {
impl LanguageModelCompletionProvider for AnthropicCompletionProvider {
fn available_models(&self, _cx: &AppContext) -> Vec<LanguageModel> {
AnthropicModel::iter()
.map(LanguageModel::Anthropic)
.collect()
}
pub fn settings_version(&self) -> usize {
fn settings_version(&self) -> usize {
self.settings_version
}
pub fn is_authenticated(&self) -> bool {
fn is_authenticated(&self) -> bool {
self.api_key.is_some()
}
pub fn authenticate(&self, cx: &AppContext) -> Task<Result<()>> {
fn authenticate(&self, cx: &AppContext) -> Task<Result<()>> {
if self.is_authenticated() {
Task::ready(Ok(()))
} else {
@@ -85,36 +57,36 @@ impl AnthropicCompletionProvider {
String::from_utf8(api_key)?
};
cx.update_global::<CompletionProvider, _>(|provider, _cx| {
if let CompletionProvider::Anthropic(provider) = provider {
provider.update_current_as::<_, AnthropicCompletionProvider>(|provider| {
provider.api_key = Some(api_key);
}
});
})
})
}
}
pub fn reset_credentials(&self, cx: &AppContext) -> Task<Result<()>> {
fn reset_credentials(&self, cx: &AppContext) -> Task<Result<()>> {
let delete_credentials = cx.delete_credentials(&self.api_url);
cx.spawn(|mut cx| async move {
delete_credentials.await.log_err();
cx.update_global::<CompletionProvider, _>(|provider, _cx| {
if let CompletionProvider::Anthropic(provider) = provider {
provider.update_current_as::<_, AnthropicCompletionProvider>(|provider| {
provider.api_key = None;
}
});
})
})
}
pub fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
cx.new_view(|cx| AuthenticationPrompt::new(self.api_url.clone(), cx))
.into()
}
pub fn model(&self) -> AnthropicModel {
self.model.clone()
fn model(&self) -> LanguageModel {
LanguageModel::Anthropic(self.model.clone())
}
pub fn count_tokens(
fn count_tokens(
&self,
request: LanguageModelRequest,
cx: &AppContext,
@@ -122,7 +94,7 @@ impl AnthropicCompletionProvider {
count_open_ai_tokens(request, cx.background_executor())
}
pub fn complete(
fn complete(
&self,
request: LanguageModelRequest,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
@@ -167,12 +139,48 @@ impl AnthropicCompletionProvider {
.boxed()
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
impl AnthropicCompletionProvider {
pub fn new(
model: AnthropicModel,
api_url: String,
http_client: Arc<dyn HttpClient>,
low_speed_timeout: Option<Duration>,
settings_version: usize,
) -> Self {
Self {
api_key: None,
api_url,
model,
http_client,
low_speed_timeout,
settings_version,
}
}
pub fn update(
&mut self,
model: AnthropicModel,
api_url: String,
low_speed_timeout: Option<Duration>,
settings_version: usize,
) {
self.model = model;
self.api_url = api_url;
self.low_speed_timeout = low_speed_timeout;
self.settings_version = settings_version;
}
fn to_anthropic_request(&self, mut request: LanguageModelRequest) -> Request {
preprocess_anthropic_request(&mut request);
let model = match request.model {
LanguageModel::Anthropic(model) => model,
_ => self.model(),
_ => self.model.clone(),
};
let mut system_message = String::new();
@@ -278,9 +286,9 @@ impl AuthenticationPrompt {
cx.spawn(|_, mut cx| async move {
write_credentials.await?;
cx.update_global::<CompletionProvider, _>(|provider, _cx| {
if let CompletionProvider::Anthropic(provider) = provider {
provider.update_current_as::<_, AnthropicCompletionProvider>(|provider| {
provider.api_key = Some(api_key);
}
});
})
})
.detach_and_log_err(cx);

View File

@@ -1,6 +1,6 @@
use crate::{
assistant_settings::CloudModel, count_open_ai_tokens, CompletionProvider, LanguageModel,
LanguageModelRequest,
LanguageModelCompletionProvider, LanguageModelRequest,
};
use anyhow::{anyhow, Result};
use client::{proto, Client};
@@ -30,11 +30,9 @@ impl CloudCompletionProvider {
let maintain_client_status = cx.spawn(|mut cx| async move {
while let Some(status) = status_rx.next().await {
let _ = cx.update_global::<CompletionProvider, _>(|provider, _cx| {
if let CompletionProvider::Cloud(provider) = provider {
provider.update_current_as::<_, Self>(|provider| {
provider.status = status;
} else {
unreachable!()
}
});
});
}
});
@@ -51,44 +49,53 @@ impl CloudCompletionProvider {
self.model = model;
self.settings_version = settings_version;
}
}
pub fn available_models(&self) -> impl Iterator<Item = CloudModel> {
impl LanguageModelCompletionProvider for CloudCompletionProvider {
fn available_models(&self, _cx: &AppContext) -> Vec<LanguageModel> {
let mut custom_model = if let CloudModel::Custom(custom_model) = self.model.clone() {
Some(custom_model)
} else {
None
};
CloudModel::iter().filter_map(move |model| {
if let CloudModel::Custom(_) = model {
Some(CloudModel::Custom(custom_model.take()?))
} else {
Some(model)
}
})
CloudModel::iter()
.filter_map(move |model| {
if let CloudModel::Custom(_) = model {
Some(CloudModel::Custom(custom_model.take()?))
} else {
Some(model)
}
})
.map(LanguageModel::Cloud)
.collect()
}
pub fn settings_version(&self) -> usize {
fn settings_version(&self) -> usize {
self.settings_version
}
pub fn model(&self) -> CloudModel {
self.model.clone()
}
pub fn is_authenticated(&self) -> bool {
fn is_authenticated(&self) -> bool {
self.status.is_connected()
}
pub fn authenticate(&self, cx: &AppContext) -> Task<Result<()>> {
fn authenticate(&self, cx: &AppContext) -> Task<Result<()>> {
let client = self.client.clone();
cx.spawn(move |cx| async move { client.authenticate_and_connect(true, &cx).await })
}
pub fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
cx.new_view(|_cx| AuthenticationPrompt).into()
}
pub fn count_tokens(
fn reset_credentials(&self, _cx: &AppContext) -> Task<Result<()>> {
Task::ready(Ok(()))
}
fn model(&self) -> LanguageModel {
LanguageModel::Cloud(self.model.clone())
}
fn count_tokens(
&self,
request: LanguageModelRequest,
cx: &AppContext,
@@ -128,7 +135,7 @@ impl CloudCompletionProvider {
}
}
pub fn complete(
fn complete(
&self,
mut request: LanguageModelRequest,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
@@ -161,6 +168,10 @@ impl CloudCompletionProvider {
})
.boxed()
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
struct AuthenticationPrompt;

View File

@@ -1,29 +1,106 @@
use anyhow::Result;
use collections::HashMap;
use futures::{channel::mpsc, future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
use gpui::{AnyView, AppContext, Task};
use std::sync::Arc;
use ui::WindowContext;
use crate::{LanguageModel, LanguageModelCompletionProvider, LanguageModelRequest};
#[derive(Clone, Default)]
pub struct FakeCompletionProvider {
current_completion_tx: Arc<parking_lot::Mutex<Option<mpsc::UnboundedSender<String>>>>,
current_completion_txs: Arc<parking_lot::Mutex<HashMap<String, mpsc::UnboundedSender<String>>>>,
}
impl FakeCompletionProvider {
pub fn complete(&self) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
let (tx, rx) = mpsc::unbounded();
*self.current_completion_tx.lock() = Some(tx);
async move { Ok(rx.map(Ok).boxed()) }.boxed()
pub fn setup_test(cx: &mut AppContext) -> Self {
use crate::CompletionProvider;
use parking_lot::RwLock;
let this = Self::default();
let provider = CompletionProvider::new(Arc::new(RwLock::new(this.clone())), None);
cx.set_global(provider);
this
}
pub fn send_completion(&self, chunk: String) {
self.current_completion_tx
pub fn running_completions(&self) -> Vec<LanguageModelRequest> {
self.current_completion_txs
.lock()
.as_ref()
.keys()
.map(|k| serde_json::from_str(k).unwrap())
.collect()
}
pub fn completion_count(&self) -> usize {
self.current_completion_txs.lock().len()
}
pub fn send_completion(&self, request: &LanguageModelRequest, chunk: String) {
let json = serde_json::to_string(request).unwrap();
self.current_completion_txs
.lock()
.get(&json)
.unwrap()
.unbounded_send(chunk)
.unwrap();
}
pub fn finish_completion(&self) {
self.current_completion_tx.lock().take();
pub fn finish_completion(&self, request: &LanguageModelRequest) {
self.current_completion_txs
.lock()
.remove(&serde_json::to_string(request).unwrap());
}
}
impl LanguageModelCompletionProvider for FakeCompletionProvider {
fn available_models(&self, _cx: &AppContext) -> Vec<LanguageModel> {
vec![LanguageModel::default()]
}
fn settings_version(&self) -> usize {
0
}
fn is_authenticated(&self) -> bool {
true
}
fn authenticate(&self, _cx: &AppContext) -> Task<Result<()>> {
Task::ready(Ok(()))
}
fn authentication_prompt(&self, _cx: &mut WindowContext) -> AnyView {
unimplemented!()
}
fn reset_credentials(&self, _cx: &AppContext) -> Task<Result<()>> {
Task::ready(Ok(()))
}
fn model(&self) -> LanguageModel {
LanguageModel::default()
}
fn count_tokens(
&self,
_request: LanguageModelRequest,
_cx: &AppContext,
) -> BoxFuture<'static, Result<usize>> {
futures::future::ready(Ok(0)).boxed()
}
fn complete(
&self,
_request: LanguageModelRequest,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
let (tx, rx) = mpsc::unbounded();
self.current_completion_txs
.lock()
.insert(serde_json::to_string(&_request).unwrap(), tx);
async move { Ok(rx.map(Ok).boxed()) }.boxed()
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}

View File

@@ -1,3 +1,4 @@
use crate::LanguageModelCompletionProvider;
use crate::{
assistant_settings::OllamaModel, CompletionProvider, LanguageModel, LanguageModelRequest, Role,
};
@@ -26,6 +27,108 @@ pub struct OllamaCompletionProvider {
available_models: Vec<OllamaModel>,
}
impl LanguageModelCompletionProvider for OllamaCompletionProvider {
fn available_models(&self, _cx: &AppContext) -> Vec<LanguageModel> {
self.available_models
.iter()
.map(|m| LanguageModel::Ollama(m.clone()))
.collect()
}
fn settings_version(&self) -> usize {
self.settings_version
}
fn is_authenticated(&self) -> bool {
!self.available_models.is_empty()
}
fn authenticate(&self, cx: &AppContext) -> Task<Result<()>> {
if self.is_authenticated() {
Task::ready(Ok(()))
} else {
self.fetch_models(cx)
}
}
fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
let fetch_models = Box::new(move |cx: &mut WindowContext| {
cx.update_global::<CompletionProvider, _>(|provider, cx| {
provider
.update_current_as::<_, OllamaCompletionProvider>(|provider| {
provider.fetch_models(cx)
})
.unwrap_or_else(|| Task::ready(Ok(())))
})
});
cx.new_view(|cx| DownloadOllamaMessage::new(fetch_models, cx))
.into()
}
fn reset_credentials(&self, cx: &AppContext) -> Task<Result<()>> {
self.fetch_models(cx)
}
fn model(&self) -> LanguageModel {
LanguageModel::Ollama(self.model.clone())
}
fn count_tokens(
&self,
request: LanguageModelRequest,
_cx: &AppContext,
) -> BoxFuture<'static, Result<usize>> {
// There is no endpoint for this _yet_ in Ollama
// see: https://github.com/ollama/ollama/issues/1716 and https://github.com/ollama/ollama/issues/3582
let token_count = request
.messages
.iter()
.map(|msg| msg.content.chars().count())
.sum::<usize>()
/ 4;
async move { Ok(token_count) }.boxed()
}
fn complete(
&self,
request: LanguageModelRequest,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
let request = self.to_ollama_request(request);
let http_client = self.http_client.clone();
let api_url = self.api_url.clone();
let low_speed_timeout = self.low_speed_timeout;
async move {
let request =
stream_chat_completion(http_client.as_ref(), &api_url, request, low_speed_timeout);
let response = request.await?;
let stream = response
.filter_map(|response| async move {
match response {
Ok(delta) => {
let content = match delta.message {
ChatMessage::User { content } => content,
ChatMessage::Assistant { content } => content,
ChatMessage::System { content } => content,
};
Some(Ok(content))
}
Err(error) => Some(Err(error)),
}
})
.boxed();
Ok(stream)
}
.boxed()
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
impl OllamaCompletionProvider {
pub fn new(
model: OllamaModel,
@@ -87,36 +190,12 @@ impl OllamaCompletionProvider {
self.settings_version = settings_version;
}
pub fn available_models(&self) -> impl Iterator<Item = &OllamaModel> {
self.available_models.iter()
}
pub fn select_first_available_model(&mut self) {
if let Some(model) = self.available_models.first() {
self.model = model.clone();
}
}
pub fn settings_version(&self) -> usize {
self.settings_version
}
pub fn is_authenticated(&self) -> bool {
!self.available_models.is_empty()
}
pub fn authenticate(&self, cx: &AppContext) -> Task<Result<()>> {
if self.is_authenticated() {
Task::ready(Ok(()))
} else {
self.fetch_models(cx)
}
}
pub fn reset_credentials(&self, cx: &AppContext) -> Task<Result<()>> {
self.fetch_models(cx)
}
pub fn fetch_models(&self, cx: &AppContext) -> Task<Result<()>> {
let http_client = self.http_client.clone();
let api_url = self.api_url.clone();
@@ -137,90 +216,21 @@ impl OllamaCompletionProvider {
models.sort_by(|a, b| a.name.cmp(&b.name));
cx.update_global::<CompletionProvider, _>(|provider, _cx| {
if let CompletionProvider::Ollama(provider) = provider {
provider.update_current_as::<_, OllamaCompletionProvider>(|provider| {
provider.available_models = models;
if !provider.available_models.is_empty() && provider.model.name.is_empty() {
provider.select_first_available_model()
}
}
});
})
})
}
pub fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
let fetch_models = Box::new(move |cx: &mut WindowContext| {
cx.update_global::<CompletionProvider, _>(|provider, cx| {
if let CompletionProvider::Ollama(provider) = provider {
provider.fetch_models(cx)
} else {
Task::ready(Ok(()))
}
})
});
cx.new_view(|cx| DownloadOllamaMessage::new(fetch_models, cx))
.into()
}
pub fn model(&self) -> OllamaModel {
self.model.clone()
}
pub fn count_tokens(
&self,
request: LanguageModelRequest,
_cx: &AppContext,
) -> BoxFuture<'static, Result<usize>> {
// There is no endpoint for this _yet_ in Ollama
// see: https://github.com/ollama/ollama/issues/1716 and https://github.com/ollama/ollama/issues/3582
let token_count = request
.messages
.iter()
.map(|msg| msg.content.chars().count())
.sum::<usize>()
/ 4;
async move { Ok(token_count) }.boxed()
}
pub fn complete(
&self,
request: LanguageModelRequest,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
let request = self.to_ollama_request(request);
let http_client = self.http_client.clone();
let api_url = self.api_url.clone();
let low_speed_timeout = self.low_speed_timeout;
async move {
let request =
stream_chat_completion(http_client.as_ref(), &api_url, request, low_speed_timeout);
let response = request.await?;
let stream = response
.filter_map(|response| async move {
match response {
Ok(delta) => {
let content = match delta.message {
ChatMessage::User { content } => content,
ChatMessage::Assistant { content } => content,
ChatMessage::System { content } => content,
};
Some(Ok(content))
}
Err(error) => Some(Err(error)),
}
})
.boxed();
Ok(stream)
}
.boxed()
}
fn to_ollama_request(&self, request: LanguageModelRequest) -> ChatRequest {
let model = match request.model {
LanguageModel::Ollama(model) => model,
_ => self.model(),
_ => self.model.clone(),
};
ChatRequest {

View File

@@ -1,5 +1,6 @@
use crate::assistant_settings::CloudModel;
use crate::assistant_settings::{AssistantProvider, AssistantSettings};
use crate::LanguageModelCompletionProvider;
use crate::{
assistant_settings::OpenAiModel, CompletionProvider, LanguageModel, LanguageModelRequest, Role,
};
@@ -57,37 +58,75 @@ impl OpenAiCompletionProvider {
self.settings_version = settings_version;
}
pub fn available_models(&self, cx: &AppContext) -> impl Iterator<Item = OpenAiModel> {
fn to_open_ai_request(&self, request: LanguageModelRequest) -> Request {
let model = match request.model {
LanguageModel::OpenAi(model) => model,
_ => self.model.clone(),
};
Request {
model,
messages: request
.messages
.into_iter()
.map(|msg| match msg.role {
Role::User => RequestMessage::User {
content: msg.content,
},
Role::Assistant => RequestMessage::Assistant {
content: Some(msg.content),
tool_calls: Vec::new(),
},
Role::System => RequestMessage::System {
content: msg.content,
},
})
.collect(),
stream: true,
stop: request.stop,
temperature: request.temperature,
tools: Vec::new(),
tool_choice: None,
}
}
}
impl LanguageModelCompletionProvider for OpenAiCompletionProvider {
fn available_models(&self, cx: &AppContext) -> Vec<LanguageModel> {
if let AssistantProvider::OpenAi {
available_models, ..
} = &AssistantSettings::get_global(cx).provider
{
if !available_models.is_empty() {
// available_models is set, just return it
return available_models.clone().into_iter();
return available_models
.iter()
.cloned()
.map(LanguageModel::OpenAi)
.collect();
}
}
let available_models = if matches!(self.model, OpenAiModel::Custom { .. }) {
// available_models is not set but the default model is set to custom, only show custom
vec![self.model.clone()]
} else {
// default case, use all models except custom
OpenAiModel::iter()
.filter(|model| !matches!(model, OpenAiModel::Custom { .. }))
.collect()
};
available_models.into_iter()
available_models
.into_iter()
.map(LanguageModel::OpenAi)
.collect()
}
pub fn settings_version(&self) -> usize {
fn settings_version(&self) -> usize {
self.settings_version
}
pub fn is_authenticated(&self) -> bool {
fn is_authenticated(&self) -> bool {
self.api_key.is_some()
}
pub fn authenticate(&self, cx: &AppContext) -> Task<Result<()>> {
fn authenticate(&self, cx: &AppContext) -> Task<Result<()>> {
if self.is_authenticated() {
Task::ready(Ok(()))
} else {
@@ -103,36 +142,36 @@ impl OpenAiCompletionProvider {
String::from_utf8(api_key)?
};
cx.update_global::<CompletionProvider, _>(|provider, _cx| {
if let CompletionProvider::OpenAi(provider) = provider {
provider.update_current_as::<_, Self>(|provider| {
provider.api_key = Some(api_key);
}
});
})
})
}
}
pub fn reset_credentials(&self, cx: &AppContext) -> Task<Result<()>> {
fn reset_credentials(&self, cx: &AppContext) -> Task<Result<()>> {
let delete_credentials = cx.delete_credentials(&self.api_url);
cx.spawn(|mut cx| async move {
delete_credentials.await.log_err();
cx.update_global::<CompletionProvider, _>(|provider, _cx| {
if let CompletionProvider::OpenAi(provider) = provider {
provider.update_current_as::<_, Self>(|provider| {
provider.api_key = None;
}
});
})
})
}
pub fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
fn authentication_prompt(&self, cx: &mut WindowContext) -> AnyView {
cx.new_view(|cx| AuthenticationPrompt::new(self.api_url.clone(), cx))
.into()
}
pub fn model(&self) -> OpenAiModel {
self.model.clone()
fn model(&self) -> LanguageModel {
LanguageModel::OpenAi(self.model.clone())
}
pub fn count_tokens(
fn count_tokens(
&self,
request: LanguageModelRequest,
cx: &AppContext,
@@ -140,7 +179,7 @@ impl OpenAiCompletionProvider {
count_open_ai_tokens(request, cx.background_executor())
}
pub fn complete(
fn complete(
&self,
request: LanguageModelRequest,
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
@@ -173,36 +212,8 @@ impl OpenAiCompletionProvider {
.boxed()
}
fn to_open_ai_request(&self, request: LanguageModelRequest) -> Request {
let model = match request.model {
LanguageModel::OpenAi(model) => model,
_ => self.model(),
};
Request {
model,
messages: request
.messages
.into_iter()
.map(|msg| match msg.role {
Role::User => RequestMessage::User {
content: msg.content,
},
Role::Assistant => RequestMessage::Assistant {
content: Some(msg.content),
tool_calls: Vec::new(),
},
Role::System => RequestMessage::System {
content: msg.content,
},
})
.collect(),
stream: true,
stop: request.stop,
temperature: request.temperature,
tools: Vec::new(),
tool_choice: None,
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
@@ -284,9 +295,9 @@ impl AuthenticationPrompt {
cx.spawn(|_, mut cx| async move {
write_credentials.await?;
cx.update_global::<CompletionProvider, _>(|provider, _cx| {
if let CompletionProvider::OpenAi(provider) = provider {
provider.update_current_as::<_, OpenAiCompletionProvider>(|provider| {
provider.api_key = Some(api_key);
}
});
})
})
.detach_and_log_err(cx);

File diff suppressed because it is too large Load Diff

View File

@@ -1,97 +1,117 @@
use crate::{assistant_settings::OpenAiModel, MessageId, MessageMetadata};
use anyhow::{anyhow, Result};
use assistant_slash_command::SlashCommandOutputSection;
use collections::HashMap;
use crate::{
Context, ContextEvent, ContextId, ContextOperation, ContextVersion, SavedContext,
SavedContextMetadata,
};
use anyhow::{anyhow, Context as _, Result};
use client::{proto, telemetry::Telemetry, Client, TypedEnvelope};
use clock::ReplicaId;
use fs::Fs;
use futures::StreamExt;
use fuzzy::StringMatchCandidate;
use gpui::{AppContext, Model, ModelContext, Task};
use gpui::{AppContext, AsyncAppContext, Context as _, Model, ModelContext, Task, WeakModel};
use language::LanguageRegistry;
use paths::contexts_dir;
use project::Project;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc, time::Duration};
use ui::Context;
use std::{
cmp::Reverse,
ffi::OsStr,
mem,
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use util::{ResultExt, TryFutureExt};
#[derive(Serialize, Deserialize)]
pub struct SavedMessage {
pub id: MessageId,
pub start: usize,
}
#[derive(Serialize, Deserialize)]
pub struct SavedContext {
pub id: Option<String>,
pub zed: String,
pub version: String,
pub text: String,
pub messages: Vec<SavedMessage>,
pub message_metadata: HashMap<MessageId, MessageMetadata>,
pub summary: String,
pub slash_command_output_sections: Vec<SlashCommandOutputSection<usize>>,
}
impl SavedContext {
pub const VERSION: &'static str = "0.3.0";
}
#[derive(Serialize, Deserialize)]
pub struct SavedContextV0_2_0 {
pub id: Option<String>,
pub zed: String,
pub version: String,
pub text: String,
pub messages: Vec<SavedMessage>,
pub message_metadata: HashMap<MessageId, MessageMetadata>,
pub summary: String,
}
#[derive(Serialize, Deserialize)]
struct SavedContextV0_1_0 {
id: Option<String>,
zed: String,
version: String,
text: String,
messages: Vec<SavedMessage>,
message_metadata: HashMap<MessageId, MessageMetadata>,
summary: String,
api_url: Option<String>,
model: OpenAiModel,
pub fn init(client: &Arc<Client>) {
client.add_model_message_handler(ContextStore::handle_advertise_contexts);
client.add_model_request_handler(ContextStore::handle_open_context);
client.add_model_message_handler(ContextStore::handle_update_context);
client.add_model_request_handler(ContextStore::handle_synchronize_contexts);
}
#[derive(Clone)]
pub struct SavedContextMetadata {
pub title: String,
pub path: PathBuf,
pub mtime: chrono::DateTime<chrono::Local>,
pub struct RemoteContextMetadata {
pub id: ContextId,
pub summary: Option<String>,
}
pub struct ContextStore {
contexts: Vec<ContextHandle>,
contexts_metadata: Vec<SavedContextMetadata>,
host_contexts: Vec<RemoteContextMetadata>,
fs: Arc<dyn Fs>,
languages: Arc<LanguageRegistry>,
telemetry: Arc<Telemetry>,
_watch_updates: Task<Option<()>>,
client: Arc<Client>,
project: Model<Project>,
project_is_shared: bool,
client_subscription: Option<client::Subscription>,
_project_subscriptions: Vec<gpui::Subscription>,
}
enum ContextHandle {
Weak(WeakModel<Context>),
Strong(Model<Context>),
}
impl ContextHandle {
fn upgrade(&self) -> Option<Model<Context>> {
match self {
ContextHandle::Weak(weak) => weak.upgrade(),
ContextHandle::Strong(strong) => Some(strong.clone()),
}
}
fn downgrade(&self) -> WeakModel<Context> {
match self {
ContextHandle::Weak(weak) => weak.clone(),
ContextHandle::Strong(strong) => strong.downgrade(),
}
}
}
impl ContextStore {
pub fn new(fs: Arc<dyn Fs>, cx: &mut AppContext) -> Task<Result<Model<Self>>> {
pub fn new(project: Model<Project>, cx: &mut AppContext) -> Task<Result<Model<Self>>> {
let fs = project.read(cx).fs().clone();
let languages = project.read(cx).languages().clone();
let telemetry = project.read(cx).client().telemetry().clone();
cx.spawn(|mut cx| async move {
const CONTEXT_WATCH_DURATION: Duration = Duration::from_millis(100);
let (mut events, _) = fs.watch(contexts_dir(), CONTEXT_WATCH_DURATION).await;
let this = cx.new_model(|cx: &mut ModelContext<Self>| Self {
contexts_metadata: Vec::new(),
fs,
_watch_updates: cx.spawn(|this, mut cx| {
async move {
while events.next().await.is_some() {
this.update(&mut cx, |this, cx| this.reload(cx))?
.await
.log_err();
let this = cx.new_model(|cx: &mut ModelContext<Self>| {
let mut this = Self {
contexts: Vec::new(),
contexts_metadata: Vec::new(),
host_contexts: Vec::new(),
fs,
languages,
telemetry,
_watch_updates: cx.spawn(|this, mut cx| {
async move {
while events.next().await.is_some() {
this.update(&mut cx, |this, cx| this.reload(cx))?
.await
.log_err();
}
anyhow::Ok(())
}
anyhow::Ok(())
}
.log_err()
}),
.log_err()
}),
client_subscription: None,
_project_subscriptions: vec![
cx.observe(&project, Self::handle_project_changed),
cx.subscribe(&project, Self::handle_project_event),
],
project_is_shared: false,
client: project.read(cx).client(),
project: project.clone(),
};
this.handle_project_changed(project, cx);
this.synchronize_contexts(cx);
this
})?;
this.update(&mut cx, |this, cx| this.reload(cx))?
.await
@@ -100,54 +120,433 @@ impl ContextStore {
})
}
pub fn load(&self, path: PathBuf, cx: &AppContext) -> Task<Result<SavedContext>> {
async fn handle_advertise_contexts(
this: Model<Self>,
envelope: TypedEnvelope<proto::AdvertiseContexts>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
this.host_contexts = envelope
.payload
.contexts
.into_iter()
.map(|context| RemoteContextMetadata {
id: ContextId::from_proto(context.context_id),
summary: context.summary,
})
.collect();
cx.notify();
})
}
async fn handle_open_context(
this: Model<Self>,
envelope: TypedEnvelope<proto::OpenContext>,
mut cx: AsyncAppContext,
) -> Result<proto::OpenContextResponse> {
let context_id = ContextId::from_proto(envelope.payload.context_id);
let operations = this.update(&mut cx, |this, cx| {
if this.project.read(cx).is_remote() {
return Err(anyhow!("only the host contexts can be opened"));
}
let context = this
.loaded_context_for_id(&context_id, cx)
.context("context not found")?;
if context.read(cx).replica_id() != ReplicaId::default() {
return Err(anyhow!("context must be opened via the host"));
}
anyhow::Ok(
context
.read(cx)
.serialize_ops(&ContextVersion::default(), cx),
)
})??;
let operations = operations.await;
Ok(proto::OpenContextResponse {
context: Some(proto::Context { operations }),
})
}
async fn handle_update_context(
this: Model<Self>,
envelope: TypedEnvelope<proto::UpdateContext>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, cx| {
let context_id = ContextId::from_proto(envelope.payload.context_id);
if let Some(context) = this.loaded_context_for_id(&context_id, cx) {
let operation_proto = envelope.payload.operation.context("invalid operation")?;
let operation = ContextOperation::from_proto(operation_proto)?;
context.update(cx, |context, cx| context.apply_ops([operation], cx))?;
}
Ok(())
})?
}
async fn handle_synchronize_contexts(
this: Model<Self>,
envelope: TypedEnvelope<proto::SynchronizeContexts>,
mut cx: AsyncAppContext,
) -> Result<proto::SynchronizeContextsResponse> {
this.update(&mut cx, |this, cx| {
if this.project.read(cx).is_remote() {
return Err(anyhow!("only the host can synchronize contexts"));
}
let mut local_versions = Vec::new();
for remote_version_proto in envelope.payload.contexts {
let remote_version = ContextVersion::from_proto(&remote_version_proto);
let context_id = ContextId::from_proto(remote_version_proto.context_id);
if let Some(context) = this.loaded_context_for_id(&context_id, cx) {
let context = context.read(cx);
let operations = context.serialize_ops(&remote_version, cx);
local_versions.push(context.version(cx).to_proto(context_id.clone()));
let client = this.client.clone();
let project_id = envelope.payload.project_id;
cx.background_executor()
.spawn(async move {
let operations = operations.await;
for operation in operations {
client.send(proto::UpdateContext {
project_id,
context_id: context_id.to_proto(),
operation: Some(operation),
})?;
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
}
this.advertise_contexts(cx);
anyhow::Ok(proto::SynchronizeContextsResponse {
contexts: local_versions,
})
})?
}
fn handle_project_changed(&mut self, _: Model<Project>, cx: &mut ModelContext<Self>) {
let is_shared = self.project.read(cx).is_shared();
let was_shared = mem::replace(&mut self.project_is_shared, is_shared);
if is_shared == was_shared {
return;
}
if is_shared {
self.contexts.retain_mut(|context| {
if let Some(strong_context) = context.upgrade() {
*context = ContextHandle::Strong(strong_context);
true
} else {
false
}
});
let remote_id = self.project.read(cx).remote_id().unwrap();
self.client_subscription = self
.client
.subscribe_to_entity(remote_id)
.log_err()
.map(|subscription| subscription.set_model(&cx.handle(), &mut cx.to_async()));
self.advertise_contexts(cx);
} else {
self.client_subscription = None;
}
}
fn handle_project_event(
&mut self,
_: Model<Project>,
event: &project::Event,
cx: &mut ModelContext<Self>,
) {
match event {
project::Event::Reshared => {
self.advertise_contexts(cx);
}
project::Event::HostReshared | project::Event::Rejoined => {
self.synchronize_contexts(cx);
}
project::Event::DisconnectedFromHost => {
self.contexts.retain_mut(|context| {
if let Some(strong_context) = context.upgrade() {
*context = ContextHandle::Weak(context.downgrade());
strong_context.update(cx, |context, cx| {
if context.replica_id() != ReplicaId::default() {
context.set_capability(language::Capability::ReadOnly, cx);
}
});
true
} else {
false
}
});
self.host_contexts.clear();
cx.notify();
}
_ => {}
}
}
pub fn create(&mut self, cx: &mut ModelContext<Self>) -> Model<Context> {
let context = cx.new_model(|cx| {
Context::local(self.languages.clone(), Some(self.telemetry.clone()), cx)
});
self.register_context(&context, cx);
context
}
pub fn open_local_context(
&mut self,
path: PathBuf,
cx: &ModelContext<Self>,
) -> Task<Result<Model<Context>>> {
if let Some(existing_context) = self.loaded_context_for_path(&path, cx) {
return Task::ready(Ok(existing_context));
}
let fs = self.fs.clone();
cx.background_executor().spawn(async move {
let saved_context = fs.load(&path).await?;
let saved_context_json = serde_json::from_str::<serde_json::Value>(&saved_context)?;
match saved_context_json
.get("version")
.ok_or_else(|| anyhow!("version not found"))?
{
serde_json::Value::String(version) => match version.as_str() {
SavedContext::VERSION => {
Ok(serde_json::from_value::<SavedContext>(saved_context_json)?)
}
"0.2.0" => {
let saved_context =
serde_json::from_value::<SavedContextV0_2_0>(saved_context_json)?;
Ok(SavedContext {
id: saved_context.id,
zed: saved_context.zed,
version: saved_context.version,
text: saved_context.text,
messages: saved_context.messages,
message_metadata: saved_context.message_metadata,
summary: saved_context.summary,
slash_command_output_sections: Vec::new(),
})
}
"0.1.0" => {
let saved_context =
serde_json::from_value::<SavedContextV0_1_0>(saved_context_json)?;
Ok(SavedContext {
id: saved_context.id,
zed: saved_context.zed,
version: saved_context.version,
text: saved_context.text,
messages: saved_context.messages,
message_metadata: saved_context.message_metadata,
summary: saved_context.summary,
slash_command_output_sections: Vec::new(),
})
}
_ => Err(anyhow!("unrecognized saved context version: {}", version)),
},
_ => Err(anyhow!("version not found on saved context")),
let languages = self.languages.clone();
let telemetry = self.telemetry.clone();
let load = cx.background_executor().spawn({
let path = path.clone();
async move {
let saved_context = fs.load(&path).await?;
SavedContext::from_json(&saved_context)
}
});
cx.spawn(|this, mut cx| async move {
let saved_context = load.await?;
let context = cx.new_model(|cx| {
Context::deserialize(saved_context, path.clone(), languages, Some(telemetry), cx)
})?;
this.update(&mut cx, |this, cx| {
if let Some(existing_context) = this.loaded_context_for_path(&path, cx) {
existing_context
} else {
this.register_context(&context, cx);
context
}
})
})
}
fn loaded_context_for_path(&self, path: &Path, cx: &AppContext) -> Option<Model<Context>> {
self.contexts.iter().find_map(|context| {
let context = context.upgrade()?;
if context.read(cx).path() == Some(path) {
Some(context)
} else {
None
}
})
}
fn loaded_context_for_id(&self, id: &ContextId, cx: &AppContext) -> Option<Model<Context>> {
self.contexts.iter().find_map(|context| {
let context = context.upgrade()?;
if context.read(cx).id() == id {
Some(context)
} else {
None
}
})
}
pub fn open_remote_context(
&mut self,
context_id: ContextId,
cx: &mut ModelContext<Self>,
) -> Task<Result<Model<Context>>> {
let project = self.project.read(cx);
let Some(project_id) = project.remote_id() else {
return Task::ready(Err(anyhow!("project was not remote")));
};
if project.is_local() {
return Task::ready(Err(anyhow!("cannot open remote contexts as the host")));
}
if let Some(context) = self.loaded_context_for_id(&context_id, cx) {
return Task::ready(Ok(context));
}
let replica_id = project.replica_id();
let capability = project.capability();
let language_registry = self.languages.clone();
let telemetry = self.telemetry.clone();
let request = self.client.request(proto::OpenContext {
project_id,
context_id: context_id.to_proto(),
});
cx.spawn(|this, mut cx| async move {
let response = request.await?;
let context_proto = response.context.context("invalid context")?;
let context = cx.new_model(|cx| {
Context::new(
context_id.clone(),
replica_id,
capability,
language_registry,
Some(telemetry),
cx,
)
})?;
let operations = cx
.background_executor()
.spawn(async move {
context_proto
.operations
.into_iter()
.map(|op| ContextOperation::from_proto(op))
.collect::<Result<Vec<_>>>()
})
.await?;
context.update(&mut cx, |context, cx| context.apply_ops(operations, cx))??;
this.update(&mut cx, |this, cx| {
if let Some(existing_context) = this.loaded_context_for_id(&context_id, cx) {
existing_context
} else {
this.register_context(&context, cx);
this.synchronize_contexts(cx);
context
}
})
})
}
fn register_context(&mut self, context: &Model<Context>, cx: &mut ModelContext<Self>) {
let handle = if self.project_is_shared {
ContextHandle::Strong(context.clone())
} else {
ContextHandle::Weak(context.downgrade())
};
self.contexts.push(handle);
self.advertise_contexts(cx);
cx.subscribe(context, Self::handle_context_event).detach();
}
fn handle_context_event(
&mut self,
context: Model<Context>,
event: &ContextEvent,
cx: &mut ModelContext<Self>,
) {
let Some(project_id) = self.project.read(cx).remote_id() else {
return;
};
match event {
ContextEvent::SummaryChanged => {
self.advertise_contexts(cx);
}
ContextEvent::Operation(operation) => {
let context_id = context.read(cx).id().to_proto();
let operation = operation.to_proto();
self.client
.send(proto::UpdateContext {
project_id,
context_id,
operation: Some(operation),
})
.log_err();
}
_ => {}
}
}
fn advertise_contexts(&self, cx: &AppContext) {
let Some(project_id) = self.project.read(cx).remote_id() else {
return;
};
// For now, only the host can advertise their open contexts.
if self.project.read(cx).is_remote() {
return;
}
let contexts = self
.contexts
.iter()
.rev()
.filter_map(|context| {
let context = context.upgrade()?.read(cx);
if context.replica_id() == ReplicaId::default() {
Some(proto::ContextMetadata {
context_id: context.id().to_proto(),
summary: context.summary().map(|summary| summary.text.clone()),
})
} else {
None
}
})
.collect();
self.client
.send(proto::AdvertiseContexts {
project_id,
contexts,
})
.ok();
}
fn synchronize_contexts(&mut self, cx: &mut ModelContext<Self>) {
let Some(project_id) = self.project.read(cx).remote_id() else {
return;
};
let contexts = self
.contexts
.iter()
.filter_map(|context| {
let context = context.upgrade()?.read(cx);
if context.replica_id() != ReplicaId::default() {
Some(context.version(cx).to_proto(context.id().clone()))
} else {
None
}
})
.collect();
let client = self.client.clone();
let request = self.client.request(proto::SynchronizeContexts {
project_id,
contexts,
});
cx.spawn(|this, cx| async move {
let response = request.await?;
let mut context_ids = Vec::new();
let mut operations = Vec::new();
this.read_with(&cx, |this, cx| {
for context_version_proto in response.contexts {
let context_version = ContextVersion::from_proto(&context_version_proto);
let context_id = ContextId::from_proto(context_version_proto.context_id);
if let Some(context) = this.loaded_context_for_id(&context_id, cx) {
context_ids.push(context_id);
operations.push(context.read(cx).serialize_ops(&context_version, cx));
}
}
})?;
let operations = futures::future::join_all(operations).await;
for (context_id, operations) in context_ids.into_iter().zip(operations) {
for operation in operations {
client.send(proto::UpdateContext {
project_id,
context_id: context_id.to_proto(),
operation: Some(operation),
})?;
}
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
pub fn search(&self, query: String, cx: &AppContext) -> Task<Vec<SavedContextMetadata>> {
let metadata = self.contexts_metadata.clone();
let executor = cx.background_executor().clone();
@@ -178,6 +577,10 @@ impl ContextStore {
})
}
pub fn host_contexts(&self) -> &[RemoteContextMetadata] {
&self.host_contexts
}
fn reload(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
let fs = self.fs.clone();
cx.spawn(|this, mut cx| async move {

View File

@@ -1986,13 +1986,14 @@ impl Codegen {
.unwrap_or_else(|| snapshot.indent_size_for_line(MultiBufferRow(selection_start.row)));
let model_telemetry_id = prompt.model.telemetry_id();
let response = CompletionProvider::global(cx).complete(prompt);
let response = CompletionProvider::global(cx).complete(prompt, cx);
let telemetry = self.telemetry.clone();
self.edit_position = range.start;
self.diff = Diff::default();
self.status = CodegenStatus::Pending;
self.generation = cx.spawn(|this, mut cx| {
async move {
let response = response.await;
let generate = async {
let mut edit_start = range.start.to_offset(&snapshot);
@@ -2002,7 +2003,7 @@ impl Codegen {
let mut response_latency = None;
let request_start = Instant::now();
let diff = async {
let chunks = StripInvalidSpans::new(response.await?);
let chunks = StripInvalidSpans::new(response.inner.await?);
futures::pin_mut!(chunks);
let mut diff = StreamingDiff::new(selected_text.to_string());
@@ -2473,9 +2474,8 @@ mod tests {
#[gpui::test(iterations = 10)]
async fn test_transform_autoindent(cx: &mut TestAppContext, mut rng: StdRng) {
let provider = FakeCompletionProvider::default();
cx.set_global(cx.update(SettingsStore::test));
cx.set_global(CompletionProvider::Fake(provider.clone()));
let provider = cx.update(|cx| FakeCompletionProvider::setup_test(cx));
cx.update(language_settings::init);
let text = indoc! {"
@@ -2495,8 +2495,11 @@ mod tests {
});
let codegen = cx.new_model(|cx| Codegen::new(buffer.clone(), range, None, cx));
let request = LanguageModelRequest::default();
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
codegen.update(cx, |codegen, cx| {
codegen.start(LanguageModelRequest::default(), cx)
});
cx.background_executor.run_until_parked();
let mut new_text = concat!(
" let mut x = 0;\n",
@@ -2508,11 +2511,11 @@ mod tests {
let max_len = cmp::min(new_text.len(), 10);
let len = rng.gen_range(1..=max_len);
let (chunk, suffix) = new_text.split_at(len);
provider.send_completion(chunk.into());
provider.send_completion(&LanguageModelRequest::default(), chunk.into());
new_text = suffix;
cx.background_executor.run_until_parked();
}
provider.finish_completion();
provider.finish_completion(&LanguageModelRequest::default());
cx.background_executor.run_until_parked();
assert_eq!(
@@ -2533,8 +2536,7 @@ mod tests {
cx: &mut TestAppContext,
mut rng: StdRng,
) {
let provider = FakeCompletionProvider::default();
cx.set_global(CompletionProvider::Fake(provider.clone()));
let provider = cx.update(|cx| FakeCompletionProvider::setup_test(cx));
cx.set_global(cx.update(SettingsStore::test));
cx.update(language_settings::init);
@@ -2555,6 +2557,8 @@ mod tests {
let request = LanguageModelRequest::default();
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
cx.background_executor.run_until_parked();
let mut new_text = concat!(
"t mut x = 0;\n",
"while x < 10 {\n",
@@ -2565,11 +2569,11 @@ mod tests {
let max_len = cmp::min(new_text.len(), 10);
let len = rng.gen_range(1..=max_len);
let (chunk, suffix) = new_text.split_at(len);
provider.send_completion(chunk.into());
provider.send_completion(&LanguageModelRequest::default(), chunk.into());
new_text = suffix;
cx.background_executor.run_until_parked();
}
provider.finish_completion();
provider.finish_completion(&LanguageModelRequest::default());
cx.background_executor.run_until_parked();
assert_eq!(
@@ -2590,8 +2594,7 @@ mod tests {
cx: &mut TestAppContext,
mut rng: StdRng,
) {
let provider = FakeCompletionProvider::default();
cx.set_global(CompletionProvider::Fake(provider.clone()));
let provider = cx.update(|cx| FakeCompletionProvider::setup_test(cx));
cx.set_global(cx.update(SettingsStore::test));
cx.update(language_settings::init);
@@ -2612,6 +2615,8 @@ mod tests {
let request = LanguageModelRequest::default();
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
cx.background_executor.run_until_parked();
let mut new_text = concat!(
"let mut x = 0;\n",
"while x < 10 {\n",
@@ -2622,11 +2627,11 @@ mod tests {
let max_len = cmp::min(new_text.len(), 10);
let len = rng.gen_range(1..=max_len);
let (chunk, suffix) = new_text.split_at(len);
provider.send_completion(chunk.into());
provider.send_completion(&LanguageModelRequest::default(), chunk.into());
new_text = suffix;
cx.background_executor.run_until_parked();
}
provider.finish_completion();
provider.finish_completion(&LanguageModelRequest::default());
cx.background_executor.run_until_parked();
assert_eq!(

View File

@@ -49,6 +49,7 @@ impl RenderOnce for ModelSelector {
})
.trigger(
ButtonLike::new("active-model")
.style(ButtonStyle::Subtle)
.child(
h_flex()
.w_full()
@@ -67,18 +68,15 @@ impl RenderOnce for ModelSelector {
),
)
.child(
div().child(
Icon::new(IconName::ChevronDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
Icon::new(IconName::ChevronDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
)
.style(ButtonStyle::Subtle)
.tooltip(move |cx| {
Tooltip::for_action("Change Model", &ToggleModelSelector, cx)
}),
)
.anchor(gpui::AnchorCorner::BottomRight)
.attach(gpui::AnchorCorner::BottomLeft)
}
}

View File

@@ -3,7 +3,6 @@ use crate::{
InlineAssist, InlineAssistant, LanguageModelRequest, LanguageModelRequestMessage, Role,
};
use anyhow::{anyhow, Result};
use assistant_slash_command::SlashCommandRegistry;
use chrono::{DateTime, Utc};
use collections::{HashMap, HashSet};
use editor::{actions::Tab, CurrentLineHighlight, Editor, EditorElement, EditorEvent, EditorStyle};
@@ -448,7 +447,6 @@ impl PromptLibrary {
self.set_active_prompt(Some(prompt_id), cx);
} else if let Some(prompt_metadata) = self.store.metadata(prompt_id) {
let language_registry = self.language_registry.clone();
let commands = SlashCommandRegistry::global(cx);
let prompt = self.store.load(prompt_id);
self.pending_load = cx.spawn(|this, mut cx| async move {
let prompt = prompt.await;
@@ -477,7 +475,7 @@ impl PromptLibrary {
editor.set_use_modal_editing(false);
editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
editor.set_completion_provider(Box::new(
SlashCommandCompletionProvider::new(commands, None, None),
SlashCommandCompletionProvider::new(None, None),
));
if focus {
editor.focus(cx);

View File

@@ -31,7 +31,6 @@ pub mod tabs_command;
pub mod term_command;
pub(crate) struct SlashCommandCompletionProvider {
commands: Arc<SlashCommandRegistry>,
cancel_flag: Mutex<Arc<AtomicBool>>,
editor: Option<WeakView<ContextEditor>>,
workspace: Option<WeakView<Workspace>>,
@@ -46,14 +45,12 @@ pub(crate) struct SlashCommandLine {
impl SlashCommandCompletionProvider {
pub fn new(
commands: Arc<SlashCommandRegistry>,
editor: Option<WeakView<ContextEditor>>,
workspace: Option<WeakView<Workspace>>,
) -> Self {
Self {
cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
editor,
commands,
workspace,
}
}
@@ -65,8 +62,8 @@ impl SlashCommandCompletionProvider {
name_range: Range<Anchor>,
cx: &mut WindowContext,
) -> Task<Result<Vec<project::Completion>>> {
let candidates = self
.commands
let commands = SlashCommandRegistry::global(cx);
let candidates = commands
.command_names()
.into_iter()
.enumerate()
@@ -76,7 +73,6 @@ impl SlashCommandCompletionProvider {
char_bag: def.as_ref().into(),
})
.collect::<Vec<_>>();
let commands = self.commands.clone();
let command_name = command_name.to_string();
let editor = self.editor.clone();
let workspace = self.workspace.clone();
@@ -155,7 +151,8 @@ impl SlashCommandCompletionProvider {
flag.store(true, SeqCst);
*flag = new_cancel_flag.clone();
if let Some(command) = self.commands.command(command_name) {
let commands = SlashCommandRegistry::global(cx);
if let Some(command) = commands.command(command_name) {
let completions = command.complete_argument(
argument,
new_cancel_flag.clone(),
@@ -170,7 +167,7 @@ impl SlashCommandCompletionProvider {
.await?
.into_iter()
.map(|command_argument| {
let confirm =
let confirm = if command_argument.run_command {
editor
.clone()
.zip(workspace.clone())
@@ -178,7 +175,7 @@ impl SlashCommandCompletionProvider {
Arc::new({
let command_range = command_range.clone();
let command_name = command_name.clone();
let command_argument = command_argument.clone();
let command_argument = command_argument.new_text.clone();
move |cx: &mut WindowContext| {
editor
.update(cx, |editor, cx| {
@@ -194,15 +191,24 @@ impl SlashCommandCompletionProvider {
.ok();
}
}) as Arc<_>
});
})
} else {
None
};
let mut new_text = command_argument.new_text.clone();
if !command_argument.run_command {
new_text.push(' ');
}
project::Completion {
old_range: argument_range.clone(),
label: CodeLabel::plain(command_argument.clone(), None),
new_text: command_argument.clone(),
label: CodeLabel::plain(command_argument.label, None),
new_text,
documentation: None,
server_id: LanguageServerId(0),
lsp_completion: Default::default(),
show_new_completions_on_confirm: false,
show_new_completions_on_confirm: !command_argument.run_command,
confirm,
}
})

View File

@@ -4,6 +4,7 @@ use super::{
SlashCommand, SlashCommandOutput,
};
use anyhow::{anyhow, Result};
use assistant_slash_command::ArgumentCompletion;
use editor::Editor;
use gpui::{AppContext, Task, WeakView};
use language::LspAdapterDelegate;
@@ -33,7 +34,7 @@ impl SlashCommand for ActiveSlashCommand {
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}

View File

@@ -1,7 +1,7 @@
use super::{SlashCommand, SlashCommandOutput};
use crate::prompt_library::PromptStore;
use anyhow::{anyhow, Result};
use assistant_slash_command::SlashCommandOutputSection;
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
use gpui::{AppContext, Task, WeakView};
use language::LspAdapterDelegate;
use std::{
@@ -36,7 +36,7 @@ impl SlashCommand for DefaultSlashCommand {
_cancellation_flag: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}

View File

@@ -1,6 +1,6 @@
use super::{create_label_for_command, SlashCommand, SlashCommandOutput};
use anyhow::{anyhow, Result};
use assistant_slash_command::SlashCommandOutputSection;
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
use fuzzy::{PathMatch, StringMatchCandidate};
use gpui::{AppContext, Model, Task, View, WeakView};
use language::{
@@ -108,7 +108,7 @@ impl SlashCommand for DiagnosticsSlashCommand {
cancellation_flag: Arc<AtomicBool>,
workspace: Option<WeakView<Workspace>>,
cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
) -> Task<Result<Vec<ArgumentCompletion>>> {
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
return Task::ready(Err(anyhow!("workspace was dropped")));
};
@@ -143,7 +143,14 @@ impl SlashCommand for DiagnosticsSlashCommand {
.map(|candidate| candidate.string),
);
Ok(matches)
Ok(matches
.into_iter()
.map(|completion| ArgumentCompletion {
label: completion.clone(),
new_text: completion,
run_command: true,
})
.collect())
})
}

View File

@@ -3,7 +3,9 @@ use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use anyhow::{anyhow, bail, Result};
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
};
use gpui::{AppContext, Model, Task, WeakView};
use indexed_docs::{
IndexedDocsRegistry, IndexedDocsStore, LocalProvider, PackageName, ProviderId, RustdocIndexer,
@@ -92,7 +94,7 @@ impl SlashCommand for DocsSlashCommand {
_cancel: Arc<AtomicBool>,
workspace: Option<WeakView<Workspace>>,
cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
) -> Task<Result<Vec<ArgumentCompletion>>> {
self.ensure_rustdoc_provider_is_registered(workspace, cx);
let indexed_docs_registry = IndexedDocsRegistry::global(cx);
@@ -102,15 +104,17 @@ impl SlashCommand for DocsSlashCommand {
.ok_or_else(|| anyhow!("no docs provider specified"))
.and_then(|provider| IndexedDocsStore::try_global(provider, cx));
cx.background_executor().spawn(async move {
/// HACK: Prefixes the completions with the provider ID so that it doesn't get deleted
/// when a completion is accepted.
///
/// We will likely want to extend `complete_argument` with support for replacing just
/// a particular range of the argument when a completion is accepted.
fn prefix_with_provider(provider: ProviderId, items: Vec<String>) -> Vec<String> {
fn build_completions(
provider: ProviderId,
items: Vec<String>,
) -> Vec<ArgumentCompletion> {
items
.into_iter()
.map(|item| format!("{provider} {item}"))
.map(|item| ArgumentCompletion {
label: item.clone(),
new_text: format!("{provider} {item}"),
run_command: true,
})
.collect()
}
@@ -119,7 +123,11 @@ impl SlashCommand for DocsSlashCommand {
let providers = indexed_docs_registry.list_providers();
Ok(providers
.into_iter()
.map(|provider| provider.to_string())
.map(|provider| ArgumentCompletion {
label: provider.to_string(),
new_text: provider.to_string(),
run_command: false,
})
.collect())
}
DocsSlashCommandArgs::SearchPackageDocs {
@@ -136,7 +144,7 @@ impl SlashCommand for DocsSlashCommand {
}
let items = store.search(package).await;
Ok(prefix_with_provider(provider, items))
Ok(build_completions(provider, items))
}
DocsSlashCommandArgs::SearchItemDocs {
provider,
@@ -145,7 +153,7 @@ impl SlashCommand for DocsSlashCommand {
} => {
let store = store?;
let items = store.search(item_path).await;
Ok(prefix_with_provider(provider, items))
Ok(build_completions(provider, items))
}
}
})

View File

@@ -4,7 +4,9 @@ use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use anyhow::{anyhow, bail, Context, Result};
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
};
use futures::AsyncReadExt;
use gpui::{AppContext, Task, WeakView};
use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
@@ -119,7 +121,7 @@ impl SlashCommand for FetchSlashCommand {
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(Vec::new()))
}

View File

@@ -1,6 +1,6 @@
use super::{diagnostics_command::write_single_file_diagnostics, SlashCommand, SlashCommandOutput};
use anyhow::{anyhow, Result};
use assistant_slash_command::SlashCommandOutputSection;
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
use fuzzy::PathMatch;
use gpui::{AppContext, Model, Task, View, WeakView};
use language::{BufferSnapshot, LineEnding, LspAdapterDelegate};
@@ -105,7 +105,7 @@ impl SlashCommand for FileSlashCommand {
cancellation_flag: Arc<AtomicBool>,
workspace: Option<WeakView<Workspace>>,
cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
) -> Task<Result<Vec<ArgumentCompletion>>> {
let Some(workspace) = workspace.and_then(|workspace| workspace.upgrade()) else {
return Task::ready(Err(anyhow!("workspace was dropped")));
};
@@ -116,11 +116,17 @@ impl SlashCommand for FileSlashCommand {
.await
.into_iter()
.map(|path_match| {
format!(
let text = format!(
"{}{}",
path_match.path_prefix,
path_match.path.to_string_lossy()
)
);
ArgumentCompletion {
label: text.clone(),
new_text: text,
run_command: true,
}
})
.collect())
})

View File

@@ -2,7 +2,9 @@ use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use anyhow::Result;
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
};
use chrono::Local;
use gpui::{AppContext, Task, WeakView};
use language::LspAdapterDelegate;
@@ -34,7 +36,7 @@ impl SlashCommand for NowSlashCommand {
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(Vec::new()))
}

View File

@@ -1,6 +1,6 @@
use super::{SlashCommand, SlashCommandOutput};
use anyhow::{anyhow, Context, Result};
use assistant_slash_command::SlashCommandOutputSection;
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
use fs::Fs;
use gpui::{AppContext, Model, Task, WeakView};
use language::LspAdapterDelegate;
@@ -107,7 +107,7 @@ impl SlashCommand for ProjectSlashCommand {
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}

View File

@@ -1,7 +1,7 @@
use super::{SlashCommand, SlashCommandOutput};
use crate::prompt_library::PromptStore;
use anyhow::{anyhow, Context, Result};
use assistant_slash_command::SlashCommandOutputSection;
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
use gpui::{AppContext, Task, WeakView};
use language::LspAdapterDelegate;
use std::sync::{atomic::AtomicBool, Arc};
@@ -33,13 +33,20 @@ impl SlashCommand for PromptSlashCommand {
_cancellation_flag: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
) -> Task<Result<Vec<ArgumentCompletion>>> {
let store = PromptStore::global(cx);
cx.background_executor().spawn(async move {
let prompts = store.await?.search(query).await;
Ok(prompts
.into_iter()
.filter_map(|prompt| Some(prompt.title?.to_string()))
.filter_map(|prompt| {
let prompt_title = prompt.title?.to_string();
Some(ArgumentCompletion {
label: prompt_title.clone(),
new_text: prompt_title,
run_command: true,
})
})
.collect())
})
}

View File

@@ -4,7 +4,7 @@ use super::{
SlashCommand, SlashCommandOutput,
};
use anyhow::Result;
use assistant_slash_command::SlashCommandOutputSection;
use assistant_slash_command::{ArgumentCompletion, SlashCommandOutputSection};
use gpui::{AppContext, Task, WeakView};
use language::{CodeLabel, LineEnding, LspAdapterDelegate};
use semantic_index::SemanticIndex;
@@ -46,7 +46,7 @@ impl SlashCommand for SearchSlashCommand {
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(Vec::new()))
}

View File

@@ -4,6 +4,7 @@ use super::{
SlashCommand, SlashCommandOutput,
};
use anyhow::{anyhow, Result};
use assistant_slash_command::ArgumentCompletion;
use collections::HashMap;
use editor::Editor;
use gpui::{AppContext, Entity, Task, WeakView};
@@ -37,7 +38,7 @@ impl SlashCommand for TabsSlashCommand {
_cancel: Arc<std::sync::atomic::AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}

View File

@@ -2,7 +2,9 @@ use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use anyhow::Result;
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
};
use gpui::{AppContext, Task, WeakView};
use language::{CodeLabel, LspAdapterDelegate};
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
@@ -42,8 +44,12 @@ impl SlashCommand for TermSlashCommand {
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
Task::ready(Ok(vec![LINE_COUNT_ARG.to_string()]))
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(vec![ArgumentCompletion {
label: LINE_COUNT_ARG.to_string(),
new_text: LINE_COUNT_ARG.to_string(),
run_command: true,
}]))
}
fn run(

View File

@@ -1026,9 +1026,10 @@ impl Codegen {
let telemetry = self.telemetry.clone();
let model_telemetry_id = prompt.model.telemetry_id();
let response = CompletionProvider::global(cx).complete(prompt);
let response = CompletionProvider::global(cx).complete(prompt, cx);
self.generation = cx.spawn(|this, mut cx| async move {
let response = response.await;
let generate = async {
let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
@@ -1036,7 +1037,7 @@ impl Codegen {
let mut response_latency = None;
let request_start = Instant::now();
let task = async {
let mut response = response.await?;
let mut response = response.inner.await?;
while let Some(chunk) = response.next().await {
if response_latency.is_none() {
response_latency = Some(request_start.elapsed());

View File

@@ -15,6 +15,16 @@ pub fn init(cx: &mut AppContext) {
SlashCommandRegistry::default_global(cx);
}
#[derive(Debug)]
pub struct ArgumentCompletion {
/// The label to display for this completion.
pub label: String,
/// The new text that should be inserted into the command when this completion is accepted.
pub new_text: String,
/// Whether the command should be run when accepting this completion.
pub run_command: bool,
}
pub trait SlashCommand: 'static + Send + Sync {
fn name(&self) -> String;
fn label(&self, _cx: &AppContext) -> CodeLabel {
@@ -28,7 +38,7 @@ pub trait SlashCommand: 'static + Send + Sync {
cancel: Arc<AtomicBool>,
workspace: Option<WeakView<Workspace>>,
cx: &mut AppContext,
) -> Task<Result<Vec<String>>>;
) -> Task<Result<Vec<ArgumentCompletion>>>;
fn requires_argument(&self) -> bool;
fn run(
self: Arc<Self>,
@@ -57,7 +67,7 @@ pub struct SlashCommandOutput {
pub run_commands_in_text: bool,
}
#[derive(Clone, Serialize, Deserialize)]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SlashCommandOutputSection<T> {
pub range: Range<T>,
pub icon: IconName,

View File

@@ -227,7 +227,7 @@ impl Telemetry {
let state = state.clone();
async move {
if let Some(tempfile) =
NamedTempFile::new_in(paths::config_dir().as_path()).log_err()
NamedTempFile::new_in(paths::logs_dir().as_path()).log_err()
{
state.lock().log_file = Some(tempfile);
}

View File

@@ -18,4 +18,5 @@ test-support = ["dep:parking_lot"]
[dependencies]
chrono.workspace = true
parking_lot = { workspace = true, optional = true }
serde.workspace = true
smallvec.workspace = true

View File

@@ -1,5 +1,6 @@
mod system_clock;
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use std::{
cmp::{self, Ordering},
@@ -16,7 +17,7 @@ pub type Seq = u32;
/// A [Lamport timestamp](https://en.wikipedia.org/wiki/Lamport_timestamp),
/// used to determine the ordering of events in the editor.
#[derive(Clone, Copy, Default, Eq, Hash, PartialEq)]
#[derive(Clone, Copy, Default, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub struct Lamport {
pub replica_id: ReplicaId,
pub value: Seq,
@@ -161,6 +162,10 @@ impl Lamport {
}
}
pub fn as_u64(self) -> u64 {
((self.value as u64) << 32) | (self.replica_id as u64)
}
pub fn tick(&mut self) -> Self {
let timestamp = *self;
self.value += 1;

View File

@@ -71,6 +71,7 @@ util.workspace = true
uuid.workspace = true
[dev-dependencies]
assistant = { workspace = true, features = ["test-support"] }
async-trait.workspace = true
audio.workspace = true
call = { workspace = true, features = ["test-support"] }

View File

@@ -562,7 +562,7 @@ fn test_fuzzy_like_string() {
assert_eq!(Database::fuzzy_like_string(" z "), "%z%");
}
#[cfg(target = "macos")]
#[cfg(target_os = "macos")]
#[gpui::test]
async fn test_fuzzy_search_users(cx: &mut gpui::TestAppContext) {
let test_db = tests::TestDb::postgres(cx.executor());

View File

@@ -595,6 +595,14 @@ impl Server {
.add_message_handler(user_message_handler(acknowledge_channel_message))
.add_message_handler(user_message_handler(acknowledge_buffer_version))
.add_request_handler(user_handler(get_supermaven_api_key))
.add_request_handler(user_handler(
forward_mutating_project_request::<proto::OpenContext>,
))
.add_request_handler(user_handler(
forward_mutating_project_request::<proto::SynchronizeContexts>,
))
.add_message_handler(broadcast_project_message_from_host::<proto::AdvertiseContexts>)
.add_message_handler(update_context)
.add_streaming_request_handler({
let app_state = app_state.clone();
move |request, response, session| {
@@ -3056,6 +3064,53 @@ async fn update_buffer(
Ok(())
}
async fn update_context(message: proto::UpdateContext, session: Session) -> Result<()> {
let project_id = ProjectId::from_proto(message.project_id);
let operation = message.operation.as_ref().context("invalid operation")?;
let capability = match operation.variant.as_ref() {
Some(proto::context_operation::Variant::BufferOperation(buffer_op)) => {
if let Some(buffer_op) = buffer_op.operation.as_ref() {
match buffer_op.variant {
None | Some(proto::operation::Variant::UpdateSelections(_)) => {
Capability::ReadOnly
}
_ => Capability::ReadWrite,
}
} else {
Capability::ReadWrite
}
}
Some(_) => Capability::ReadWrite,
None => Capability::ReadOnly,
};
let guard = session
.db()
.await
.connections_for_buffer_update(
project_id,
session.principal_id(),
session.connection_id,
capability,
)
.await?;
let (host, guests) = &*guard;
broadcast(
Some(session.connection_id),
guests.iter().chain([host]).copied(),
|connection_id| {
session
.peer
.forward_send(session.connection_id, connection_id, message.clone())
},
);
Ok(())
}
/// Notify other participants that a project has been updated.
async fn broadcast_project_message_from_host<T: EntityMessage<Entity = ShareProject>>(
request: T,

View File

@@ -135,7 +135,7 @@ async fn test_basic_following(
assert_eq!(editor.selections.ranges(cx), vec![2..1]);
});
// When client B starts following client A, all visible view states are replicated to client B.
// When client B starts following client A, only the active view state is replicated to client B.
workspace_b.update(cx_b, |workspace, cx| workspace.follow(peer_id_a, cx));
cx_c.executor().run_until_parked();
@@ -156,7 +156,7 @@ async fn test_basic_following(
);
assert_eq!(
editor_b1.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
vec![3..2]
vec![3..3]
);
executor.run_until_parked();
@@ -601,6 +601,7 @@ async fn test_following_tab_order(
let pane_paths = |pane: &View<workspace::Pane>, cx: &mut VisualTestContext| {
pane.update(cx, |pane, cx| {
pane.items()
.iter()
.map(|item| {
item.project_path(cx)
.unwrap()
@@ -1829,6 +1830,7 @@ fn pane_summaries(workspace: &View<Workspace>, cx: &mut VisualTestContext) -> Ve
leader,
items: pane
.items()
.iter()
.enumerate()
.map(|(ix, item)| {
(

View File

@@ -6,6 +6,7 @@ use crate::{
},
};
use anyhow::{anyhow, Result};
use assistant::ContextStore;
use call::{room, ActiveCall, ParticipantLocation, Room};
use client::{User, RECEIVE_TIMEOUT};
use collections::{HashMap, HashSet};
@@ -6242,10 +6243,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
assert_eq!(pane.items_len(), 2);
assert_eq!(get_path(pane, 0, cx), path_3.clone());
assert_eq!(get_path(pane, 1, cx), path_1.clone());
assert_eq!(
pane.preview_item_id(),
Some(pane.items().nth(1).unwrap().item_id())
);
assert_eq!(pane.preview_item_id(), Some(pane.items()[1].item_id()));
assert!(pane.can_navigate_backward());
assert!(!pane.can_navigate_forward());
@@ -6263,10 +6261,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
assert_eq!(pane.items_len(), 2);
assert_eq!(get_path(pane, 0, cx), path_3.clone());
assert_eq!(get_path(pane, 1, cx), path_2.clone());
assert_eq!(
pane.preview_item_id(),
Some(pane.items().nth(1).unwrap().item_id())
);
assert_eq!(pane.preview_item_id(), Some(pane.items()[1].item_id()));
assert!(pane.can_navigate_backward());
assert!(!pane.can_navigate_forward());
@@ -6282,10 +6277,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
assert_eq!(pane.items_len(), 2);
assert_eq!(get_path(pane, 0, cx), path_3.clone());
assert_eq!(get_path(pane, 1, cx), path_1.clone());
assert_eq!(
pane.preview_item_id(),
Some(pane.items().nth(1).unwrap().item_id())
);
assert_eq!(pane.preview_item_id(), Some(pane.items()[1].item_id()));
assert!(pane.can_navigate_backward());
assert!(pane.can_navigate_forward());
@@ -6321,10 +6313,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
assert_eq!(pane.items_len(), 2);
assert_eq!(get_path(pane, 0, cx), path_3.clone());
assert_eq!(get_path(pane, 1, cx), path_1.clone());
assert_eq!(
pane.preview_item_id(),
Some(pane.items().nth(1).unwrap().item_id())
);
assert_eq!(pane.preview_item_id(), Some(pane.items()[1].item_id()));
assert!(pane.can_navigate_backward());
assert!(pane.can_navigate_forward());
@@ -6332,7 +6321,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
// Close permanent tab
pane.update(cx, |pane, cx| {
let id = pane.items().nth(0).unwrap().item_id();
let id = pane.items()[0].item_id();
pane.close_item_by_id(id, workspace::SaveIntent::Skip, cx)
})
.await
@@ -6341,10 +6330,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
pane.update(cx, |pane, cx| {
assert_eq!(pane.items_len(), 1);
assert_eq!(get_path(pane, 0, cx), path_1.clone());
assert_eq!(
pane.preview_item_id(),
Some(pane.items().nth(0).unwrap().item_id())
);
assert_eq!(pane.preview_item_id(), Some(pane.items()[0].item_id()));
assert!(pane.can_navigate_backward());
assert!(pane.can_navigate_forward());
@@ -6360,10 +6346,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
pane.update(cx, |pane, cx| {
assert_eq!(pane.items_len(), 1);
assert_eq!(get_path(pane, 0, cx), path_1.clone());
assert_eq!(
pane.preview_item_id(),
Some(pane.items().nth(0).unwrap().item_id())
);
assert_eq!(pane.preview_item_id(), Some(pane.items()[0].item_id()));
assert!(pane.can_navigate_backward());
assert!(pane.can_navigate_forward());
@@ -6389,10 +6372,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
pane.update(cx, |pane, cx| {
assert_eq!(pane.items_len(), 1);
assert_eq!(get_path(pane, 0, cx), path_1.clone());
assert_eq!(
pane.preview_item_id(),
Some(pane.items().nth(0).unwrap().item_id())
);
assert_eq!(pane.preview_item_id(), Some(pane.items()[0].item_id()));
assert!(pane.can_navigate_backward());
assert!(pane.can_navigate_forward());
@@ -6402,10 +6382,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
assert_eq!(pane.items_len(), 2);
assert_eq!(get_path(pane, 0, cx), path_1.clone());
assert_eq!(get_path(pane, 1, cx), path_2.clone());
assert_eq!(
pane.preview_item_id(),
Some(pane.items().nth(1).unwrap().item_id())
);
assert_eq!(pane.preview_item_id(), Some(pane.items()[1].item_id()));
assert!(pane.can_navigate_backward());
assert!(!pane.can_navigate_forward());
@@ -6427,10 +6404,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
pane.update(cx, |pane, cx| {
assert_eq!(pane.items_len(), 1);
assert_eq!(get_path(pane, 0, cx), path_2.clone());
assert_eq!(
pane.preview_item_id(),
Some(pane.items().nth(0).unwrap().item_id())
);
assert_eq!(pane.preview_item_id(), Some(pane.items()[0].item_id()));
assert!(pane.can_navigate_backward());
assert!(!pane.can_navigate_forward());
@@ -6440,12 +6414,129 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
assert_eq!(pane.items_len(), 2);
assert_eq!(get_path(pane, 0, cx), path_1.clone());
assert_eq!(get_path(pane, 1, cx), path_2.clone());
assert_eq!(
pane.preview_item_id(),
Some(pane.items().nth(1).unwrap().item_id())
);
assert_eq!(pane.preview_item_id(), Some(pane.items()[1].item_id()));
assert!(pane.can_navigate_backward());
assert!(!pane.can_navigate_forward());
});
}
#[gpui::test(iterations = 10)]
async fn test_context_collaboration_with_reconnect(
executor: BackgroundExecutor,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
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;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
client_a.fs().insert_tree("/a", Default::default()).await;
let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
// Client A sees that a guest has joined.
executor.run_until_parked();
project_a.read_with(cx_a, |project, _| {
assert_eq!(project.collaborators().len(), 1);
});
project_b.read_with(cx_b, |project, _| {
assert_eq!(project.collaborators().len(), 1);
});
let context_store_a = cx_a
.update(|cx| ContextStore::new(project_a.clone(), cx))
.await
.unwrap();
let context_store_b = cx_b
.update(|cx| ContextStore::new(project_b.clone(), cx))
.await
.unwrap();
// Client A creates a new context.
let context_a = context_store_a.update(cx_a, |store, cx| store.create(cx));
executor.run_until_parked();
// Client B retrieves host's contexts and joins one.
let context_b = context_store_b
.update(cx_b, |store, cx| {
let host_contexts = store.host_contexts().to_vec();
assert_eq!(host_contexts.len(), 1);
store.open_remote_context(host_contexts[0].id.clone(), cx)
})
.await
.unwrap();
// Host and guest make changes
context_a.update(cx_a, |context, cx| {
context.buffer().update(cx, |buffer, cx| {
buffer.edit([(0..0, "Host change\n")], None, cx)
})
});
context_b.update(cx_b, |context, cx| {
context.buffer().update(cx, |buffer, cx| {
buffer.edit([(0..0, "Guest change\n")], None, cx)
})
});
executor.run_until_parked();
assert_eq!(
context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()),
"Guest change\nHost change\n"
);
assert_eq!(
context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()),
"Guest change\nHost change\n"
);
// Disconnect client A and make some changes while disconnected.
server.disconnect_client(client_a.peer_id().unwrap());
server.forbid_connections();
context_a.update(cx_a, |context, cx| {
context.buffer().update(cx, |buffer, cx| {
buffer.edit([(0..0, "Host offline change\n")], None, cx)
})
});
context_b.update(cx_b, |context, cx| {
context.buffer().update(cx, |buffer, cx| {
buffer.edit([(0..0, "Guest offline change\n")], None, cx)
})
});
executor.run_until_parked();
assert_eq!(
context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()),
"Host offline change\nGuest change\nHost change\n"
);
assert_eq!(
context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()),
"Guest offline change\nGuest change\nHost change\n"
);
// Allow client A to reconnect and verify that contexts converge.
server.allow_connections();
executor.advance_clock(RECEIVE_TIMEOUT);
assert_eq!(
context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()),
"Guest offline change\nHost offline change\nGuest change\nHost change\n"
);
assert_eq!(
context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()),
"Guest offline change\nHost offline change\nGuest change\nHost change\n"
);
// Client A disconnects without being able to reconnect. Context B becomes readonly.
server.forbid_connections();
server.disconnect_client(client_a.peer_id().unwrap());
executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
context_b.read_with(cx_b, |context, cx| {
assert!(context.buffer().read(cx).read_only());
});
}

View File

@@ -294,6 +294,8 @@ impl TestServer {
menu::init();
dev_server_projects::init(client.clone(), cx);
settings::KeymapFile::load_asset(os_keymap, cx).unwrap();
assistant::FakeCompletionProvider::setup_test(cx);
assistant::context_store::init(&client);
});
client

View File

@@ -22,10 +22,9 @@ use std::{
};
use ui::{prelude::*, Label};
use util::ResultExt;
use workspace::notifications::NotificationId;
use workspace::{item::Dedup, notifications::NotificationId};
use workspace::{
item::{FollowableItem, Item, ItemEvent, ItemHandle, TabContentParams},
register_followable_item,
searchable::SearchableItemHandle,
ItemNavHistory, Pane, SaveIntent, Toast, ViewId, Workspace, WorkspaceId,
};
@@ -33,7 +32,7 @@ use workspace::{
actions!(collab, [CopyLink]);
pub fn init(cx: &mut AppContext) {
register_followable_item::<ChannelView>(cx)
workspace::FollowableViewRegistry::register::<ChannelView>(cx)
}
pub struct ChannelView {
@@ -83,6 +82,56 @@ impl ChannelView {
pane: View<Pane>,
workspace: View<Workspace>,
cx: &mut WindowContext,
) -> Task<Result<View<Self>>> {
let channel_view = Self::load(channel_id, workspace, cx);
cx.spawn(|mut cx| async move {
let channel_view = channel_view.await?;
pane.update(&mut cx, |pane, cx| {
let buffer_id = channel_view.read(cx).channel_buffer.read(cx).remote_id(cx);
let existing_view = pane
.items_of_type::<Self>()
.find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
// If this channel buffer is already open in this pane, just return it.
if let Some(existing_view) = existing_view.clone() {
if existing_view.read(cx).channel_buffer == channel_view.read(cx).channel_buffer
{
if let Some(link_position) = link_position {
existing_view.update(cx, |channel_view, cx| {
channel_view.focus_position_from_link(link_position, true, cx)
});
}
return existing_view;
}
}
// If the pane contained a disconnected view for this channel buffer,
// replace that.
if let Some(existing_item) = existing_view {
if let Some(ix) = pane.index_for_item(&existing_item) {
pane.close_item_by_id(existing_item.entity_id(), SaveIntent::Skip, cx)
.detach();
pane.add_item(Box::new(channel_view.clone()), true, true, Some(ix), cx);
}
}
if let Some(link_position) = link_position {
channel_view.update(cx, |channel_view, cx| {
channel_view.focus_position_from_link(link_position, true, cx)
});
}
channel_view
})
})
}
pub fn load(
channel_id: ChannelId,
workspace: View<Workspace>,
cx: &mut WindowContext,
) -> Task<Result<View<Self>>> {
let weak_workspace = workspace.downgrade();
let workspace = workspace.read(cx);
@@ -107,49 +156,11 @@ impl ChannelView {
})
})?;
pane.update(&mut cx, |pane, cx| {
let buffer_id = channel_buffer.read(cx).remote_id(cx);
let existing_view = pane
.items_of_type::<Self>()
.find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
// If this channel buffer is already open in this pane, just return it.
if let Some(existing_view) = existing_view.clone() {
if existing_view.read(cx).channel_buffer == channel_buffer {
if let Some(link_position) = link_position {
existing_view.update(cx, |channel_view, cx| {
channel_view.focus_position_from_link(link_position, true, cx)
});
}
return existing_view;
}
}
let view = cx.new_view(|cx| {
let mut this =
Self::new(project, weak_workspace, channel_store, channel_buffer, cx);
this.acknowledge_buffer_version(cx);
this
});
// If the pane contained a disconnected view for this channel buffer,
// replace that.
if let Some(existing_item) = existing_view {
if let Some(ix) = pane.index_for_item(&existing_item) {
pane.close_item_by_id(existing_item.entity_id(), SaveIntent::Skip, cx)
.detach();
pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx);
}
}
if let Some(link_position) = link_position {
view.update(cx, |channel_view, cx| {
channel_view.focus_position_from_link(link_position, true, cx)
});
}
view
cx.new_view(|cx| {
let mut this =
Self::new(project, weak_workspace, channel_store, channel_buffer, cx);
this.acknowledge_buffer_version(cx);
this
})
})
}
@@ -478,7 +489,6 @@ impl FollowableItem for ChannelView {
}
fn from_state_proto(
pane: View<workspace::Pane>,
workspace: View<workspace::Workspace>,
remote_id: workspace::ViewId,
state: &mut Option<proto::view::Variant>,
@@ -491,8 +501,7 @@ impl FollowableItem for ChannelView {
unreachable!()
};
let open =
ChannelView::open_in_pane(ChannelId(state.channel_id), None, pane, workspace, cx);
let open = ChannelView::load(ChannelId(state.channel_id), workspace, cx);
Some(cx.spawn(|mut cx| async move {
let this = open.await?;
@@ -556,6 +565,10 @@ impl FollowableItem for ChannelView {
})
}
fn leader_peer_id(&self, cx: &AppContext) -> Option<PeerId> {
self.editor.read(cx).leader_peer_id(cx)
}
fn is_project_item(&self, _cx: &WindowContext) -> bool {
false
}
@@ -563,6 +576,19 @@ impl FollowableItem for ChannelView {
fn to_follow_event(event: &Self::Event) -> Option<workspace::item::FollowEvent> {
Editor::to_follow_event(event)
}
fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<Dedup> {
let existing = existing.channel_buffer.read(cx);
if self.channel_buffer.read(cx).channel_id == existing.channel_id {
if existing.is_connected() {
Some(Dedup::KeepExisting)
} else {
Some(Dedup::ReplaceExisting)
}
} else {
None
}
}
}
struct ChannelBufferCollaborationHub(Model<ChannelBuffer>);

View File

@@ -355,11 +355,10 @@ impl ChatPanel {
.child(Icon::new(IconName::ReplyArrowRight).color(Color::Muted))
.child(Avatar::new(user_being_replied_to.avatar_uri.clone()).size(rems(0.7)))
.child(
div().font_weight(FontWeight::SEMIBOLD).child(
Label::new(format!("@{}", user_being_replied_to.github_login))
.size(LabelSize::XSmall)
.color(Color::Muted),
),
Label::new(format!("@{}", user_being_replied_to.github_login))
.size(LabelSize::XSmall)
.weight(FontWeight::SEMIBOLD)
.color(Color::Muted),
)
.child(
div().overflow_y_hidden().child(
@@ -490,22 +489,16 @@ impl ChatPanel {
|this| {
this.child(
h_flex()
.gap_2()
.text_ui_sm(cx)
.child(
div().absolute().child(
Avatar::new(message.sender.avatar_uri.clone())
.size(rems(1.)),
),
Avatar::new(message.sender.avatar_uri.clone())
.size(rems(1.)),
)
.child(
div()
.pl(cx.rem_size() + px(6.0))
.pr(px(8.0))
.font_weight(FontWeight::BOLD)
.child(
Label::new(message.sender.github_login.clone())
.size(LabelSize::Small),
),
Label::new(message.sender.github_login.clone())
.size(LabelSize::Small)
.weight(FontWeight::BOLD),
)
.child(
Label::new(time_format::format_localized_timestamp(
@@ -1044,13 +1037,12 @@ impl Render for ChatPanel {
.id(("reply-preview", reply_to_message_id))
.child(Label::new("Replying to ").size(LabelSize::Small))
.child(
div().font_weight(FontWeight::BOLD).child(
Label::new(format!(
"@{}",
user_being_replied_to.github_login.clone()
))
.size(LabelSize::Small),
),
Label::new(format!(
"@{}",
user_being_replied_to.github_login.clone()
))
.size(LabelSize::Small)
.weight(FontWeight::BOLD),
)
.when_some(channel_id, |this, channel_id| {
this.cursor_pointer().on_click(cx.listener(

View File

@@ -2547,9 +2547,8 @@ impl CollabPanel {
.take(FACEPILE_LIMIT)
.chain(if extra_count > 0 {
Some(
div()
Label::new(format!("+{extra_count}"))
.ml_2()
.child(Label::new(format!("+{extra_count}")))
.into_any_element(),
)
} else {

View File

@@ -258,7 +258,7 @@ gpui::actions!(
RedoSelection,
Rename,
RestartLanguageServer,
RevealInFinder,
RevealInFileManager,
ReverseLines,
RevertSelectedHunks,
ScrollCursorBottom,

View File

@@ -89,13 +89,16 @@ use language::{
CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, OffsetRangeExt,
Point, Selection, SelectionGoal, TransactionId,
};
use language::{BufferRow, Runnable, RunnableRange};
use language::{point_to_lsp, BufferRow, Runnable, RunnableRange};
use linked_editing_ranges::refresh_linked_ranges;
use task::{ResolvedTask, TaskTemplate, TaskVariables};
use hover_links::{HoverLink, HoveredLinkState, InlayHighlight};
pub use lsp::CompletionContext;
use lsp::{CompletionTriggerKind, DiagnosticSeverity, LanguageServerId};
use lsp::{
CompletionItemKind, CompletionTriggerKind, DiagnosticSeverity, InsertTextFormat,
LanguageServerId,
};
use mouse_context_menu::MouseContextMenu;
use movement::TextLayoutDetails;
pub use multi_buffer::{
@@ -265,7 +268,7 @@ pub fn init(cx: &mut AppContext) {
init_settings(cx);
workspace::register_project_item::<Editor>(cx);
workspace::register_followable_item::<Editor>(cx);
workspace::FollowableViewRegistry::register::<Editor>(cx);
workspace::register_deserializable_item::<Editor>(cx);
cx.observe_new_views(
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
@@ -1126,11 +1129,10 @@ impl CompletionsMenu {
None
} else {
Some(
h_flex().ml_4().child(
Label::new(text.clone())
.size(LabelSize::Small)
.color(Color::Muted),
),
Label::new(text.clone())
.ml_4()
.size(LabelSize::Small)
.color(Color::Muted),
)
}
} else {
@@ -1153,7 +1155,7 @@ impl CompletionsMenu {
}
}))
.child(h_flex().overflow_hidden().child(completion_label))
.end_slot::<Div>(documentation_label),
.end_slot::<Label>(documentation_label),
)
})
.collect()
@@ -1927,6 +1929,11 @@ impl Editor {
EditorMode::AutoHeight { .. } => "auto_height",
EditorMode::Full => "full",
};
if EditorSettings::get_global(cx).jupyter.enabled {
key_context.add("jupyter");
}
key_context.set("mode", mode);
if self.pending_rename.is_some() {
key_context.add("renaming");
@@ -2030,10 +2037,6 @@ impl Editor {
self.buffer.read(cx).replica_id()
}
pub fn leader_peer_id(&self) -> Option<PeerId> {
self.leader_peer_id
}
pub fn buffer(&self) -> &Model<MultiBuffer> {
&self.buffer
}
@@ -5152,7 +5155,6 @@ impl Editor {
})
.collect::<Vec<_>>()
});
if let Some(tabstop) = tabstops.first() {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges(tabstop.ranges.iter().cloned());
@@ -10326,7 +10328,7 @@ impl Editor {
cx.notify();
}
pub fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext<Self>) {
pub fn reveal_in_finder(&mut self, _: &RevealInFileManager, cx: &mut ViewContext<Self>) {
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
cx.reveal_path(&file.abs_path(cx));
@@ -11087,6 +11089,7 @@ impl Editor {
if *singleton_buffer_edited {
if let Some(project) = &self.project {
let project = project.read(cx);
#[allow(clippy::mutable_key_type)]
let languages_affected = multibuffer
.read(cx)
.all_buffers()
@@ -11752,6 +11755,97 @@ pub trait CompletionProvider {
) -> bool;
}
fn snippet_completions(
project: &Project,
buffer: &Model<Buffer>,
buffer_position: text::Anchor,
cx: &mut AppContext,
) -> Vec<Completion> {
let language = buffer.read(cx).language_at(buffer_position);
let language_name = language.as_ref().map(|language| language.lsp_id());
let snippet_store = project.snippets().read(cx);
let snippets = snippet_store.snippets_for(language_name, cx);
if snippets.is_empty() {
return vec![];
}
let snapshot = buffer.read(cx).text_snapshot();
let chunks = snapshot.reversed_chunks_in_range(text::Anchor::MIN..buffer_position);
let mut lines = chunks.lines();
let Some(line_at) = lines.next().filter(|line| !line.is_empty()) else {
return vec![];
};
let scope = language.map(|language| language.default_scope());
let mut last_word = line_at
.chars()
.rev()
.take_while(|c| char_kind(&scope, *c) == CharKind::Word)
.collect::<String>();
last_word = last_word.chars().rev().collect();
let as_offset = text::ToOffset::to_offset(&buffer_position, &snapshot);
let to_lsp = |point: &text::Anchor| {
let end = text::ToPointUtf16::to_point_utf16(point, &snapshot);
point_to_lsp(end)
};
let lsp_end = to_lsp(&buffer_position);
snippets
.into_iter()
.filter_map(|snippet| {
let matching_prefix = snippet
.prefix
.iter()
.find(|prefix| prefix.starts_with(&last_word))?;
let start = as_offset - last_word.len();
let start = snapshot.anchor_before(start);
let range = start..buffer_position;
let lsp_start = to_lsp(&start);
let lsp_range = lsp::Range {
start: lsp_start,
end: lsp_end,
};
Some(Completion {
old_range: range,
new_text: snippet.body.clone(),
label: CodeLabel {
text: matching_prefix.clone(),
runs: vec![],
filter_range: 0..matching_prefix.len(),
},
server_id: LanguageServerId(usize::MAX),
documentation: snippet
.description
.clone()
.map(|description| Documentation::SingleLine(description)),
lsp_completion: lsp::CompletionItem {
label: snippet.prefix.first().unwrap().clone(),
kind: Some(CompletionItemKind::SNIPPET),
label_details: snippet.description.as_ref().map(|description| {
lsp::CompletionItemLabelDetails {
detail: Some(description.clone()),
description: None,
}
}),
insert_text_format: Some(InsertTextFormat::SNIPPET),
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
lsp::InsertReplaceEdit {
new_text: snippet.body.clone(),
insert: lsp_range,
replace: lsp_range,
},
)),
filter_text: Some(snippet.body.clone()),
sort_text: Some(char::MAX.to_string()),
..Default::default()
},
confirm: None,
show_new_completions_on_confirm: false,
})
})
.collect()
}
impl CompletionProvider for Model<Project> {
fn completions(
&self,
@@ -11761,7 +11855,14 @@ impl CompletionProvider for Model<Project> {
cx: &mut ViewContext<Editor>,
) -> Task<Result<Vec<Completion>>> {
self.update(cx, |project, cx| {
project.completions(&buffer, buffer_position, options, cx)
let snippets = snippet_completions(project, buffer, buffer_position, cx);
let project_completions = project.completions(&buffer, buffer_position, options, cx);
cx.background_executor().spawn(async move {
let mut completions = project_completions.await?;
//let snippets = snippets.into_iter().;
completions.extend(snippets);
Ok(completions)
})
})
}
@@ -12719,7 +12820,7 @@ pub(crate) fn split_words(text: &str) -> impl std::iter::Iterator<Item = &str> +
})
}
trait RangeToAnchorExt {
pub trait RangeToAnchorExt {
fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range<Anchor>;
}

View File

@@ -25,6 +25,8 @@ pub struct EditorSettings {
pub expand_excerpt_lines: u32,
#[serde(default)]
pub double_click_in_multibuffer: DoubleClickInMultibuffer,
#[serde(default)]
pub jupyter: Jupyter,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
@@ -64,6 +66,15 @@ pub enum DoubleClickInMultibuffer {
Open,
}
#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct Jupyter {
/// Whether the Jupyter feature is enabled.
///
/// Default: `false`
pub enabled: bool,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct Toolbar {
pub breadcrumbs: bool,
@@ -217,6 +228,9 @@ pub struct EditorSettingsContent {
///
/// Default: select
pub double_click_in_multibuffer: Option<DoubleClickInMultibuffer>,
/// Jupyter REPL settings.
pub jupyter: Option<Jupyter>,
}
// Toolbar related settings

View File

@@ -8189,7 +8189,6 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
let follower_1 = cx
.update_window(*workspace.deref(), |_, cx| {
Editor::from_state_proto(
pane.clone(),
workspace.root_view(cx).unwrap(),
ViewId {
creator: Default::default(),
@@ -8281,7 +8280,6 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
let follower_2 = cx
.update_window(*workspace.deref(), |_, cx| {
Editor::from_state_proto(
pane.clone(),
workspace.root_view(cx).unwrap().clone(),
ViewId {
creator: Default::default(),

View File

@@ -3660,6 +3660,11 @@ impl EditorElement {
if scroll_position != current_scroll_position {
editor.scroll(scroll_position, axis, cx);
cx.stop_propagation();
} else if y < 0. {
// Due to clamping, we may fail to detect cases of overscroll to the top;
// We want the scroll manager to get an update in such cases and detect the change of direction
// on the next frame.
cx.notify();
}
});
}

View File

@@ -19,7 +19,7 @@ use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart};
use settings::Settings;
use smol::stream::StreamExt;
use std::{ops::Range, sync::Arc, time::Duration};
use ui::{prelude::*, Tooltip};
use ui::{prelude::*, window_is_transparent, Tooltip};
use util::TryFutureExt;
use workspace::Workspace;
@@ -587,15 +587,12 @@ impl DiagnosticPopover {
div()
.id("diagnostic")
.block()
.elevation_2(cx)
.overflow_y_scroll()
.px_2()
.py_1()
.bg(diagnostic_colors.background)
.text_color(style.text.color)
.border_1()
.border_color(diagnostic_colors.border)
.rounded_md()
.elevation_2_borderless(cx)
// Don't draw the background color if the theme
// allows transparent surfaces.
.when(window_is_transparent(cx), |this| {
this.bg(gpui::transparent_black())
})
.max_w(max_size.width)
.max_h(max_size.height)
.cursor(CursorStyle::PointingHand)
@@ -607,7 +604,19 @@ impl DiagnosticPopover {
// because that would move the cursor.
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
.on_click(cx.listener(|editor, _, cx| editor.go_to_diagnostic(&Default::default(), cx)))
.child(SharedString::from(text))
.child(
div()
.id("diagnostic-inner")
.overflow_y_scroll()
.px_2()
.py_1()
.bg(diagnostic_colors.background)
.text_color(style.text.color)
.border_1()
.border_color(diagnostic_colors.border)
.rounded_lg()
.child(SharedString::from(text)),
)
.into_any_element()
}

View File

@@ -19,7 +19,7 @@ use multi_buffer::AnchorRangeExt;
use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath};
use rpc::proto::{self, update_view, PeerId};
use settings::Settings;
use workspace::item::{ItemSettings, TabContentParams};
use workspace::item::{Dedup, ItemSettings, TabContentParams};
use std::{
any::TypeId,
@@ -34,7 +34,7 @@ use text::{BufferId, Selection};
use theme::{Theme, ThemeSettings};
use ui::{h_flex, prelude::*, Label};
use util::{paths::PathExt, ResultExt, TryFutureExt};
use workspace::item::{BreadcrumbText, FollowEvent, FollowableItemHandle};
use workspace::item::{BreadcrumbText, FollowEvent};
use workspace::{
item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem},
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
@@ -49,7 +49,6 @@ impl FollowableItem for Editor {
}
fn from_state_proto(
pane: View<workspace::Pane>,
workspace: View<Workspace>,
remote_id: ViewId,
state: &mut Option<proto::view::Variant>,
@@ -63,7 +62,6 @@ impl FollowableItem for Editor {
unreachable!()
};
let client = project.read(cx).client();
let replica_id = project.read(cx).replica_id();
let buffer_ids = state
.excerpts
@@ -77,72 +75,55 @@ impl FollowableItem for Editor {
.collect::<Result<Vec<_>>>()
});
let pane = pane.downgrade();
Some(cx.spawn(|mut cx| async move {
let mut buffers = futures::future::try_join_all(buffers?)
.await
.debug_assert_ok("leaders don't share views for unshared buffers")?;
let editor = pane.update(&mut cx, |pane, cx| {
let mut editors = pane.items_of_type::<Self>();
editors.find(|editor| {
let ids_match = editor.remote_id(&client, cx) == Some(remote_id);
let singleton_buffer_matches = state.singleton
&& buffers.first()
== editor.read(cx).buffer.read(cx).as_singleton().as_ref();
ids_match || singleton_buffer_matches
let editor = cx.update(|cx| {
let multibuffer = cx.new_model(|cx| {
let mut multibuffer;
if state.singleton && buffers.len() == 1 {
multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx)
} else {
multibuffer = MultiBuffer::new(replica_id, project.read(cx).capability());
let mut excerpts = state.excerpts.into_iter().peekable();
while let Some(excerpt) = excerpts.peek() {
let Ok(buffer_id) = BufferId::new(excerpt.buffer_id) else {
continue;
};
let buffer_excerpts = iter::from_fn(|| {
let excerpt = excerpts.peek()?;
(excerpt.buffer_id == u64::from(buffer_id))
.then(|| excerpts.next().unwrap())
});
let buffer =
buffers.iter().find(|b| b.read(cx).remote_id() == buffer_id);
if let Some(buffer) = buffer {
multibuffer.push_excerpts(
buffer.clone(),
buffer_excerpts.filter_map(deserialize_excerpt_range),
cx,
);
}
}
};
if let Some(title) = &state.title {
multibuffer = multibuffer.with_title(title.clone())
}
multibuffer
});
cx.new_view(|cx| {
let mut editor =
Editor::for_multibuffer(multibuffer, Some(project.clone()), true, cx);
editor.remote_id = Some(remote_id);
editor
})
})?;
let editor = if let Some(editor) = editor {
editor
} else {
pane.update(&mut cx, |_, cx| {
let multibuffer = cx.new_model(|cx| {
let mut multibuffer;
if state.singleton && buffers.len() == 1 {
multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx)
} else {
multibuffer =
MultiBuffer::new(replica_id, project.read(cx).capability());
let mut excerpts = state.excerpts.into_iter().peekable();
while let Some(excerpt) = excerpts.peek() {
let Ok(buffer_id) = BufferId::new(excerpt.buffer_id) else {
continue;
};
let buffer_excerpts = iter::from_fn(|| {
let excerpt = excerpts.peek()?;
(excerpt.buffer_id == u64::from(buffer_id))
.then(|| excerpts.next().unwrap())
});
let buffer =
buffers.iter().find(|b| b.read(cx).remote_id() == buffer_id);
if let Some(buffer) = buffer {
multibuffer.push_excerpts(
buffer.clone(),
buffer_excerpts.filter_map(deserialize_excerpt_range),
cx,
);
}
}
};
if let Some(title) = &state.title {
multibuffer = multibuffer.with_title(title.clone())
}
multibuffer
});
cx.new_view(|cx| {
let mut editor =
Editor::for_multibuffer(multibuffer, Some(project.clone()), true, cx);
editor.remote_id = Some(remote_id);
editor
})
})?
};
update_editor_from_message(
editor.downgrade(),
project,
@@ -181,6 +162,10 @@ impl FollowableItem for Editor {
cx.notify();
}
fn leader_peer_id(&self, _cx: &AppContext) -> Option<PeerId> {
self.leader_peer_id.clone()
}
fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
let buffer = self.buffer.read(cx);
if buffer
@@ -327,6 +312,16 @@ impl FollowableItem for Editor {
fn is_project_item(&self, _cx: &WindowContext) -> bool {
true
}
fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<Dedup> {
let self_singleton = self.buffer.read(cx).as_singleton()?;
let other_singleton = existing.buffer.read(cx).as_singleton()?;
if self_singleton == other_singleton {
Some(Dedup::KeepExisting)
} else {
None
}
}
}
async fn update_editor_from_message(

View File

@@ -1,8 +1,12 @@
use std::ops::Range;
use crate::{
Copy, CopyPermalinkToLine, Cut, DisplayPoint, Editor, EditorMode, FindAllReferences,
GoToDefinition, GoToImplementation, GoToTypeDefinition, Paste, Rename, RevealInFinder,
SelectMode, ToggleCodeActions,
selections_collection::SelectionsCollection, Copy, CopyPermalinkToLine, Cut, DisplayPoint,
DisplaySnapshot, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToImplementation,
GoToTypeDefinition, Paste, Rename, RevealInFileManager, SelectMode, ToDisplayPoint,
ToggleCodeActions,
};
use gpui::prelude::FluentBuilder;
use gpui::{DismissEvent, Pixels, Point, Subscription, View, ViewContext};
use workspace::OpenInTerminal;
@@ -37,6 +41,23 @@ impl MouseContextMenu {
}
}
fn display_ranges<'a>(
display_map: &'a DisplaySnapshot,
selections: &'a SelectionsCollection,
) -> impl Iterator<Item = Range<DisplayPoint>> + 'a {
let pending = selections
.pending
.as_ref()
.map(|pending| &pending.selection);
selections.disjoint.iter().chain(pending).map(move |s| {
if s.reversed {
s.end.to_display_point(&display_map)..s.start.to_display_point(&display_map)
} else {
s.start.to_display_point(&display_map)..s.end.to_display_point(&display_map)
}
})
}
pub fn deploy_context_menu(
editor: &mut Editor,
position: Point<Pixels>,
@@ -65,11 +86,14 @@ pub fn deploy_context_menu(
return;
}
// Move the cursor to the clicked location so that dispatched actions make sense
editor.change_selections(None, cx, |s| {
s.clear_disjoint();
s.set_pending_display_range(point..point, SelectMode::Character);
});
let display_map = editor.selections.display_map(cx);
if !display_ranges(&display_map, &editor.selections).any(|r| r.contains(&point)) {
// Move the cursor to the clicked location so that dispatched actions make sense
editor.change_selections(None, cx, |s| {
s.clear_disjoint();
s.set_pending_display_range(point..point, SelectMode::Character);
});
}
let focus = cx.focused();
ui::ContextMenu::build(cx, |menu, _cx| {
@@ -90,7 +114,12 @@ pub fn deploy_context_menu(
.action("Copy", Box::new(Copy))
.action("Paste", Box::new(Paste))
.separator()
.action("Reveal in Finder", Box::new(RevealInFinder))
.when(cfg!(target_os = "macos"), |builder| {
builder.action("Reveal in Finder", Box::new(RevealInFileManager))
})
.when(cfg!(not(target_os = "macos")), |builder| {
builder.action("Reveal in File Manager", Box::new(RevealInFileManager))
})
.action("Open in Terminal", Box::new(OpenInTerminal))
.action("Copy Permalink", Box::new(CopyPermalinkToLine));
match focus {

View File

@@ -42,6 +42,7 @@ semantic_version.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
snippet_provider.workspace = true
theme.workspace = true
toml.workspace = true
ui.workspace = true

View File

@@ -526,6 +526,11 @@ fn populate_defaults(manifest: &mut ExtensionManifest, extension_path: &Path) ->
}
}
let snippets_json_path = extension_path.join("snippets.json");
if snippets_json_path.exists() {
manifest.snippets = Some(snippets_json_path);
}
// For legacy extensions on the v0 schema (aka, using `extension.json`), we want to populate the grammars in
// the manifest using the contents of the `grammars` directory.
if manifest.schema_version.is_v0() {

View File

@@ -78,6 +78,8 @@ pub struct ExtensionManifest {
pub slash_commands: BTreeMap<Arc<str>, SlashCommandManifestEntry>,
#[serde(default)]
pub indexed_docs_providers: BTreeMap<Arc<str>, IndexedDocsProviderEntry>,
#[serde(default)]
pub snippets: Option<PathBuf>,
}
#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)]
@@ -206,5 +208,6 @@ fn manifest_from_old_manifest(
language_servers: Default::default(),
slash_commands: BTreeMap::default(),
indexed_docs_providers: BTreeMap::default(),
snippets: None,
}
}

View File

@@ -1,7 +1,9 @@
use std::sync::{atomic::AtomicBool, Arc};
use anyhow::{anyhow, Result};
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
};
use futures::FutureExt;
use gpui::{AppContext, Task, WeakView, WindowContext};
use language::LspAdapterDelegate;
@@ -41,7 +43,7 @@ impl SlashCommand for ExtensionSlashCommand {
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
) -> Task<Result<Vec<ArgumentCompletion>>> {
cx.background_executor().spawn(async move {
self.extension
.call({
@@ -57,7 +59,16 @@ impl SlashCommand for ExtensionSlashCommand {
.await?
.map_err(|e| anyhow!("{}", e))?;
anyhow::Ok(completions)
anyhow::Ok(
completions
.into_iter()
.map(|completion| ArgumentCompletion {
label: completion.label,
new_text: completion.new_text,
run_command: completion.run_command,
})
.collect(),
)
}
.boxed()
}

View File

@@ -44,6 +44,7 @@ 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::{
@@ -115,6 +116,7 @@ pub struct ExtensionStore {
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)>,
@@ -193,6 +195,7 @@ pub fn init(
theme_registry,
SlashCommandRegistry::global(cx),
IndexedDocsRegistry::global(cx),
SnippetRegistry::global(cx),
cx,
)
});
@@ -227,6 +230,7 @@ impl ExtensionStore {
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");
@@ -259,6 +263,7 @@ impl ExtensionStore {
theme_registry,
slash_command_registry,
indexed_docs_registry,
snippet_registry,
reload_tx,
tasks: Vec::new(),
};
@@ -1045,6 +1050,7 @@ impl ExtensionStore {
.collect::<Vec<_>>();
let mut grammars_to_add = Vec::new();
let mut themes_to_add = Vec::new();
let mut snippets_to_add = Vec::new();
for extension_id in &extensions_to_load {
let Some(extension) = new_index.extensions.get(extension_id) else {
continue;
@@ -1062,6 +1068,11 @@ impl ExtensionStore {
path.extend([Path::new(extension_id.as_ref()), theme_path.as_path()]);
path
}));
snippets_to_add.extend(extension.manifest.snippets.iter().map(|snippets_path| {
let mut path = self.installed_dir.clone();
path.extend([Path::new(extension_id.as_ref()), snippets_path.as_path()]);
path
}));
}
self.language_registry
@@ -1097,6 +1108,7 @@ impl ExtensionStore {
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 extension_entries = extensions_to_load
.iter()
.filter_map(|name| new_index.extensions.get(name).cloned())
@@ -1117,6 +1129,15 @@ impl ExtensionStore {
.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)
.log_err();
}
}
}
})
.await;

View File

@@ -19,6 +19,7 @@ use parking_lot::Mutex;
use project::{Project, DEFAULT_COMPLETION_CONTEXT};
use serde_json::json;
use settings::{Settings as _, SettingsStore};
use snippet_provider::SnippetRegistry;
use std::{
ffi::OsString,
path::{Path, PathBuf},
@@ -160,6 +161,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
language_servers: BTreeMap::default(),
slash_commands: BTreeMap::default(),
indexed_docs_providers: BTreeMap::default(),
snippets: None,
}),
dev: false,
},
@@ -185,6 +187,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
language_servers: BTreeMap::default(),
slash_commands: BTreeMap::default(),
indexed_docs_providers: BTreeMap::default(),
snippets: None,
}),
dev: false,
},
@@ -258,6 +261,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
let slash_command_registry = SlashCommandRegistry::new();
let indexed_docs_registry = Arc::new(IndexedDocsRegistry::new(cx.executor()));
let snippet_registry = Arc::new(SnippetRegistry::new());
let node_runtime = FakeNodeRuntime::new();
let store = cx.new_model(|cx| {
@@ -272,6 +276,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
theme_registry.clone(),
slash_command_registry.clone(),
indexed_docs_registry.clone(),
snippet_registry.clone(),
cx,
)
});
@@ -345,6 +350,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
language_servers: BTreeMap::default(),
slash_commands: BTreeMap::default(),
indexed_docs_providers: BTreeMap::default(),
snippets: None,
}),
dev: false,
},
@@ -396,6 +402,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
theme_registry.clone(),
slash_command_registry,
indexed_docs_registry,
snippet_registry,
cx,
)
});
@@ -477,6 +484,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
let slash_command_registry = SlashCommandRegistry::new();
let indexed_docs_registry = Arc::new(IndexedDocsRegistry::new(cx.executor()));
let snippet_registry = Arc::new(SnippetRegistry::new());
let node_runtime = FakeNodeRuntime::new();
let mut status_updates = language_registry.language_server_binary_statuses();
@@ -568,6 +576,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
theme_registry.clone(),
slash_command_registry,
indexed_docs_registry,
snippet_registry,
cx,
)
});

View File

@@ -20,7 +20,7 @@ use wasmtime::{
pub use latest::CodeLabelSpanLiteral;
pub use latest::{
zed::extension::lsp::{Completion, CompletionKind, InsertTextFormat, Symbol, SymbolKind},
zed::extension::slash_command::SlashCommandOutput,
zed::extension::slash_command::{SlashCommandArgumentCompletion, SlashCommandOutput},
CodeLabel, CodeLabelSpan, Command, Range, SlashCommand,
};
pub use since_v0_0_4::LanguageServerConfig;
@@ -263,7 +263,7 @@ impl Extension {
store: &mut Store<WasmState>,
command: &SlashCommand,
query: &str,
) -> Result<Result<Vec<String>, String>> {
) -> Result<Result<Vec<SlashCommandArgumentCompletion>, String>> {
match self {
Extension::V007(ext) => {
ext.call_complete_slash_command_argument(store, command, query)

View File

@@ -421,27 +421,10 @@ impl ExtensionImports for WasmState {
.await?;
}
DownloadedFileType::Zip => {
let file_name = destination_path
.file_name()
.ok_or_else(|| anyhow!("invalid download path"))?
.to_string_lossy();
let zip_filename = format!("{file_name}.zip");
let mut zip_path = destination_path.clone();
zip_path.set_file_name(zip_filename);
futures::pin_mut!(body);
self.host.fs.create_file_with(&zip_path, body).await?;
let unzip_status = std::process::Command::new("unzip")
.current_dir(&extension_work_dir)
.arg("-d")
.arg(&destination_path)
.arg(&zip_path)
.output()?
.status;
if !unzip_status.success() {
Err(anyhow!("failed to unzip {} archive", path.display()))?;
}
node_runtime::extract_zip(&destination_path, body)
.await
.with_context(|| format!("failed to unzip {} archive", path.display()))?;
}
}

View File

@@ -25,7 +25,9 @@ pub use wit::{
npm_package_latest_version,
},
zed::extension::platform::{current_platform, Architecture, Os},
zed::extension::slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection},
zed::extension::slash_command::{
SlashCommand, SlashCommandArgumentCompletion, SlashCommandOutput, SlashCommandOutputSection,
},
CodeLabel, CodeLabelSpan, CodeLabelSpanLiteral, Command, DownloadedFileType, EnvVars,
KeyValueStore, LanguageServerInstallationStatus, Range, Worktree,
};
@@ -114,7 +116,7 @@ pub trait Extension: Send + Sync {
&self,
_command: SlashCommand,
_query: String,
) -> Result<Vec<String>, String> {
) -> Result<Vec<SlashCommandArgumentCompletion>, String> {
Ok(Vec::new())
}
@@ -247,7 +249,7 @@ impl wit::Guest for Component {
fn complete_slash_command_argument(
command: SlashCommand,
query: String,
) -> Result<Vec<String>, String> {
) -> Result<Vec<SlashCommandArgumentCompletion>, String> {
extension().complete_slash_command_argument(command, query)
}

View File

@@ -8,7 +8,7 @@ world extension {
use common.{range};
use lsp.{completion, symbol};
use slash-command.{slash-command, slash-command-output};
use slash-command.{slash-command, slash-command-argument-completion, slash-command-output};
/// Initializes the extension.
export init-extension: func();
@@ -130,7 +130,7 @@ world extension {
export labels-for-symbols: func(language-server-id: string, symbols: list<symbol>) -> result<list<option<code-label>>, string>;
/// Returns the completions that should be shown when completing the provided slash command with the given query.
export complete-slash-command-argument: func(command: slash-command, query: string) -> result<list<string>, string>;
export complete-slash-command-argument: func(command: slash-command, query: string) -> result<list<slash-command-argument-completion>, string>;
/// Returns the output from running the provided slash command.
export run-slash-command: func(command: slash-command, argument: option<string>, worktree: borrow<worktree>) -> result<slash-command-output, string>;

View File

@@ -28,4 +28,14 @@ interface slash-command {
/// The label to display in the placeholder for this section.
label: string,
}
/// A completion for a slash command argument.
record slash-command-argument-completion {
/// The label to display for this completion.
label: string,
/// The new text that should be inserted into the command when this completion is accepted.
new-text: string,
/// Whether the command should be run when accepting this completion.
run-command: bool,
}
}

View File

@@ -41,6 +41,7 @@ pub fn init(cx: &mut AppContext) {
.active_pane()
.read(cx)
.items()
.iter()
.find_map(|item| item.downcast::<ExtensionsPage>());
if let Some(existing) = existing {

View File

@@ -124,7 +124,6 @@ wayland-protocols = { version = "0.31.2", features = [
] }
wayland-protocols-plasma = { version = "0.2.0", features = ["client"] }
oo7 = "0.3.0"
open = "5.1.2"
filedescriptor = "0.8.2"
x11rb = { version = "0.13.0", features = [
"allow-unsafe-code",
@@ -142,7 +141,6 @@ xim = { git = "https://github.com/npmania/xim-rs", rev = "27132caffc5b9bc9c432ca
] }
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "5a5c4d4", features = ["source-fontconfig-dlopen"] }
x11-clipboard = "0.9.2"
mio = { version = "1.0.0", features = ["os-poll", "os-ext"] }
[target.'cfg(windows)'.dependencies]
windows.workspace = true

View File

@@ -7,7 +7,7 @@ use std::env;
fn main() {
let target = env::var("CARGO_CFG_TARGET_OS");
println!("cargo::rustc-check-cfg=cfg(gles)");
match target.as_deref() {
Ok("macos") => {
#[cfg(target_os = "macos")]

View File

@@ -1,4 +1,4 @@
use std::{ops::Range, time::Duration};
use std::ops::Range;
use gpui::*;
use unicode_segmentation::*;
@@ -26,6 +26,8 @@ struct TextInput {
selection_reversed: bool,
marked_range: Option<Range<usize>>,
last_layout: Option<ShapedLine>,
last_bounds: Option<Bounds<Pixels>>,
is_selecting: bool,
}
impl TextInput {
@@ -80,6 +82,21 @@ impl TextInput {
self.replace_text_in_range(None, "", cx)
}
fn on_mouse_down(&mut self, event: &MouseDownEvent, cx: &mut ViewContext<Self>) {
self.is_selecting = true;
self.move_to(self.index_for_mouse_position(event.position), cx)
}
fn on_mouse_up(&mut self, _: &MouseUpEvent, _: &mut ViewContext<Self>) {
self.is_selecting = false;
}
fn on_mouse_move(&mut self, event: &MouseMoveEvent, cx: &mut ViewContext<Self>) {
if self.is_selecting {
self.select_to(self.index_for_mouse_position(event.position), cx);
}
}
fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext<Self>) {
cx.show_character_palette();
}
@@ -97,6 +114,20 @@ impl TextInput {
}
}
fn index_for_mouse_position(&self, position: Point<Pixels>) -> usize {
let (Some(bounds), Some(line)) = (self.last_bounds.as_ref(), self.last_layout.as_ref())
else {
return 0;
};
if position.y < bounds.top() {
return 0;
}
if position.y > bounds.bottom() {
return self.content.len();
}
line.closest_index_for_x(position.x - bounds.left())
}
fn select_to(&mut self, offset: usize, cx: &mut ViewContext<Self>) {
if self.selection_reversed {
self.selected_range.start = offset
@@ -409,6 +440,7 @@ impl Element for TextElement {
}
self.input.update(cx, |input, _cx| {
input.last_layout = Some(line);
input.last_bounds = Some(bounds);
});
}
}
@@ -419,6 +451,7 @@ impl Render for TextInput {
.flex()
.key_context("TextInput")
.track_focus(&self.focus_handle)
.cursor(CursorStyle::IBeam)
.on_action(cx.listener(Self::backspace))
.on_action(cx.listener(Self::delete))
.on_action(cx.listener(Self::left))
@@ -429,6 +462,10 @@ impl Render for TextInput {
.on_action(cx.listener(Self::home))
.on_action(cx.listener(Self::end))
.on_action(cx.listener(Self::show_character_palette))
.on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down))
.on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up))
.on_mouse_up_out(MouseButton::Left, cx.listener(Self::on_mouse_up))
.on_mouse_move(cx.listener(Self::on_mouse_move))
.bg(rgb(0xeeeeee))
.size_full()
.line_height(px(30.))
@@ -446,6 +483,39 @@ impl Render for TextInput {
}
}
impl FocusableView for TextInput {
fn focus_handle(&self, _: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
struct InputExample {
text_input: View<TextInput>,
recent_keystrokes: Vec<Keystroke>,
}
impl Render for InputExample {
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
div()
.bg(rgb(0xaaaaaa))
.flex()
.flex_col()
.size_full()
.child(self.text_input.clone())
.children(self.recent_keystrokes.iter().rev().map(|ks| {
format!(
"{:} {}",
ks,
if let Some(ime_key) = ks.ime_key.as_ref() {
format!("-> {}", ime_key)
} else {
"".to_owned()
}
)
}))
}
}
fn main() {
App::new().run(|cx: &mut AppContext| {
let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
@@ -468,42 +538,36 @@ fn main() {
..Default::default()
},
|cx| {
cx.new_view(|cx| TextInput {
let text_input = cx.new_view(|cx| TextInput {
focus_handle: cx.focus_handle(),
content: "".into(),
selected_range: 0..0,
selection_reversed: false,
marked_range: None,
last_layout: None,
last_bounds: None,
is_selecting: false,
});
cx.new_view(|_| InputExample {
text_input,
recent_keystrokes: vec![],
})
},
)
.unwrap();
cx.observe_keystrokes(move |ev, cx| {
window
.update(cx, |view, cx| {
view.recent_keystrokes.push(ev.keystroke.clone());
cx.notify();
})
.unwrap();
})
.detach();
window
.update(cx, |view, cx| {
view.focus_handle.focus(cx);
cx.focus_view(&view.text_input);
cx.activate(true);
cx.spawn(|this, mut cx| async move {
cx.background_executor().timer(Duration::from_secs(5)).await;
this.update(&mut cx, |_, cx| {
println!("calling cx.activate()");
cx.activate(true);
println!("cx.activate() called.");
})
.unwrap();
println!("----------");
cx.update(|cx| {
println!("calling cx.activate_window()");
cx.activate_window();
println!("cx.activate_window() called");
})
.unwrap();
})
.detach();
})
.unwrap();
});

View File

@@ -2,63 +2,209 @@ use gpui::*;
struct WindowContent {
text: SharedString,
bounds: Bounds<Pixels>,
bg: Hsla,
}
impl Render for WindowContent {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let window_bounds = cx.bounds();
div()
.flex()
.bg(rgb(0x1e2025))
.flex_col()
.bg(self.bg)
.size_full()
.justify_center()
.items_center()
.text_xl()
.text_color(rgb(0xffffff))
.child(self.text.clone())
.child(
div()
.flex()
.flex_col()
.text_sm()
.items_center()
.size_full()
.child(format!(
"origin: {}, {} size: {}, {}",
self.bounds.origin.x,
self.bounds.origin.y,
self.bounds.size.width,
self.bounds.size.height
))
.child(format!(
"cx.bounds() origin: {}, {} size {}, {}",
window_bounds.origin.x,
window_bounds.origin.y,
window_bounds.size.width,
window_bounds.size.height
)),
)
}
}
fn build_window_options(display_id: DisplayId, bounds: Bounds<Pixels>) -> WindowOptions {
WindowOptions {
// Set the bounds of the window in screen coordinates
window_bounds: Some(WindowBounds::Windowed(bounds)),
// Specify the display_id to ensure the window is created on the correct screen
display_id: Some(display_id),
titlebar: None,
window_background: WindowBackgroundAppearance::Transparent,
focus: false,
show: true,
kind: WindowKind::PopUp,
is_movable: false,
app_id: None,
window_min_size: None,
window_decorations: None,
}
}
fn main() {
App::new().run(|cx: &mut AppContext| {
// Create several new windows, positioned in the top right corner of each screen
let size = Size {
width: px(350.),
height: px(75.),
};
let margin_offset = px(150.);
for screen in cx.displays() {
let options = {
let margin_right = px(16.);
let margin_height = px(-48.);
let size = Size {
width: px(400.),
height: px(72.),
};
let bounds = gpui::Bounds::<Pixels> {
origin: screen.bounds().upper_right()
- point(size.width + margin_right, margin_height),
size,
};
WindowOptions {
// Set the bounds of the window in screen coordinates
window_bounds: Some(WindowBounds::Windowed(bounds)),
// Specify the display_id to ensure the window is created on the correct screen
display_id: Some(screen.id()),
titlebar: None,
window_background: WindowBackgroundAppearance::default(),
focus: false,
show: true,
kind: WindowKind::PopUp,
is_movable: false,
app_id: None,
window_min_size: None,
window_decorations: None,
}
let bounds = Bounds {
origin: point(margin_offset, margin_offset),
size,
};
cx.open_window(options, |cx| {
cx.open_window(build_window_options(screen.id(), bounds), |cx| {
cx.new_view(|_| WindowContent {
text: format!("{:?}", screen.id()).into(),
text: format!("Top Left {:?}", screen.id()).into(),
bg: gpui::red(),
bounds,
})
})
.unwrap();
let bounds = Bounds {
origin: screen.bounds().upper_right()
- point(size.width + margin_offset, -margin_offset),
size,
};
cx.open_window(build_window_options(screen.id(), bounds), |cx| {
cx.new_view(|_| WindowContent {
text: format!("Top Right {:?}", screen.id()).into(),
bg: gpui::red(),
bounds,
})
})
.unwrap();
let bounds = Bounds {
origin: screen.bounds().lower_left()
- point(-margin_offset, size.height + margin_offset),
size,
};
cx.open_window(build_window_options(screen.id(), bounds), |cx| {
cx.new_view(|_| WindowContent {
text: format!("Bottom Left {:?}", screen.id()).into(),
bg: gpui::blue(),
bounds,
})
})
.unwrap();
let bounds = Bounds {
origin: screen.bounds().lower_right()
- point(size.width + margin_offset, size.height + margin_offset),
size,
};
cx.open_window(build_window_options(screen.id(), bounds), |cx| {
cx.new_view(|_| WindowContent {
text: format!("Bottom Right {:?}", screen.id()).into(),
bg: gpui::blue(),
bounds,
})
})
.unwrap();
let bounds = Bounds {
origin: point(screen.bounds().center().x - size.center().x, margin_offset),
size,
};
cx.open_window(build_window_options(screen.id(), bounds), |cx| {
cx.new_view(|_| WindowContent {
text: format!("Top Center {:?}", screen.id()).into(),
bg: gpui::black(),
bounds,
})
})
.unwrap();
let bounds = Bounds {
origin: point(margin_offset, screen.bounds().center().y - size.center().y),
size,
};
cx.open_window(build_window_options(screen.id(), bounds), |cx| {
cx.new_view(|_| WindowContent {
text: format!("Left Center {:?}", screen.id()).into(),
bg: gpui::black(),
bounds,
})
})
.unwrap();
let bounds = Bounds {
origin: point(
screen.bounds().center().x - size.center().x,
screen.bounds().center().y - size.center().y,
),
size,
};
cx.open_window(build_window_options(screen.id(), bounds), |cx| {
cx.new_view(|_| WindowContent {
text: format!("Center {:?}", screen.id()).into(),
bg: gpui::black(),
bounds,
})
})
.unwrap();
let bounds = Bounds {
origin: point(
screen.bounds().size.width - size.width - margin_offset,
screen.bounds().center().y - size.center().y,
),
size,
};
cx.open_window(build_window_options(screen.id(), bounds), |cx| {
cx.new_view(|_| WindowContent {
text: format!("Right Center {:?}", screen.id()).into(),
bg: gpui::black(),
bounds,
})
})
.unwrap();
let bounds = Bounds {
origin: point(
screen.bounds().center().x - size.center().x,
screen.bounds().size.height - size.height - margin_offset,
),
size,
};
cx.open_window(build_window_options(screen.id(), bounds), |cx| {
cx.new_view(|_| WindowContent {
text: format!("Bottom Center {:?}", screen.id()).into(),
bg: gpui::black(),
bounds,
})
})
.unwrap();

View File

@@ -2320,6 +2320,18 @@ impl Pixels {
Self(self.0.abs())
}
/// Returns the sign of the `Pixels` value.
///
/// # Returns
///
/// Returns:
/// * `1.0` if the value is positive
/// * `-1.0` if the value is negative
/// * `0.0` if the value is zero
pub fn signum(&self) -> f32 {
self.0.signum()
}
/// Returns the f64 value of `Pixels`.
///
/// # Returns

View File

@@ -291,6 +291,10 @@ pub trait BorrowAppContext {
fn update_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
where
G: Global;
/// Updates the global state of the given type, creating a default if it didn't exist before.
fn update_default_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
where
G: Global + Default;
}
impl<C> BorrowAppContext for C
@@ -310,6 +314,14 @@ where
self.borrow_mut().end_global_lease(global);
result
}
fn update_default_global<G, R>(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R
where
G: Global + Default,
{
self.borrow_mut().default_global::<G>();
self.update_global(f)
}
}
/// A flatten equivalent for anyhow `Result`s.

View File

@@ -291,14 +291,41 @@ impl ScrollDelta {
}
/// Combines two scroll deltas into one.
/// If the signs of the deltas are the same (both positive or both negative),
/// the deltas are added together. If the signs are opposite, the second delta
/// (other) is used, effectively overriding the first delta.
pub fn coalesce(self, other: ScrollDelta) -> ScrollDelta {
match (self, other) {
(ScrollDelta::Pixels(px_a), ScrollDelta::Pixels(px_b)) => {
ScrollDelta::Pixels(px_a + px_b)
(ScrollDelta::Pixels(a), ScrollDelta::Pixels(b)) => {
let x = if a.x.signum() * b.x.signum() >= 0. {
a.x + b.x
} else {
b.x
};
let y = if a.y.signum() * b.y.signum() >= 0. {
a.y + b.y
} else {
b.y
};
ScrollDelta::Pixels(point(x, y))
}
(ScrollDelta::Lines(lines_a), ScrollDelta::Lines(lines_b)) => {
ScrollDelta::Lines(lines_a + lines_b)
(ScrollDelta::Lines(a), ScrollDelta::Lines(b)) => {
let x = if a.x.signum() * b.x.signum() >= 0. {
a.x + b.x
} else {
b.x
};
let y = if a.y.signum() * b.y.signum() >= 0. {
a.y + b.y
} else {
b.y
};
ScrollDelta::Lines(point(x, y))
}
_ => other,

View File

@@ -281,6 +281,16 @@ pub struct Tiling {
}
impl Tiling {
/// Initializes a [`Tiling`] type with all sides tiled
pub fn tiled() -> Self {
Self {
top: true,
left: true,
right: true,
bottom: true,
}
}
/// Whether any edge is tiled
pub fn is_tiled(&self) -> bool {
self.top || self.left || self.right || self.bottom

View File

@@ -5,10 +5,12 @@ use calloop::{
timer::TimeoutAction,
EventLoop,
};
use mio::Waker;
use parking::{Parker, Unparker};
use parking_lot::Mutex;
use std::{sync::Arc, thread, time::Duration};
use std::{
thread,
time::{Duration, Instant},
};
use util::ResultExt;
struct TimerAfter {
@@ -19,7 +21,6 @@ struct TimerAfter {
pub(crate) struct LinuxDispatcher {
parker: Mutex<Parker>,
main_sender: Sender<Runnable>,
main_waker: Option<Arc<Waker>>,
timer_sender: Sender<TimerAfter>,
background_sender: flume::Sender<Runnable>,
_background_threads: Vec<thread::JoinHandle<()>>,
@@ -27,18 +28,26 @@ pub(crate) struct LinuxDispatcher {
}
impl LinuxDispatcher {
pub fn new(main_sender: Sender<Runnable>, main_waker: Option<Arc<Waker>>) -> Self {
pub fn new(main_sender: Sender<Runnable>) -> Self {
let (background_sender, background_receiver) = flume::unbounded::<Runnable>();
let thread_count = std::thread::available_parallelism()
.map(|i| i.get())
.unwrap_or(1);
let mut background_threads = (0..thread_count)
.map(|_| {
.map(|i| {
let receiver = background_receiver.clone();
std::thread::spawn(move || {
for runnable in receiver {
let start = Instant::now();
runnable.run();
log::trace!(
"background thread {}: ran runnable. took: {:?}",
i,
start.elapsed()
);
}
})
})
@@ -79,7 +88,6 @@ impl LinuxDispatcher {
Self {
parker: Mutex::new(Parker::new()),
main_sender,
main_waker,
timer_sender,
background_sender,
_background_threads: background_threads,
@@ -99,9 +107,6 @@ impl PlatformDispatcher for LinuxDispatcher {
fn dispatch_on_main_thread(&self, runnable: Runnable) {
self.main_sender.send(runnable).ok();
if let Some(main_waker) = self.main_waker.as_ref() {
main_waker.wake().ok();
}
}
fn dispatch_after(&self, duration: Duration, runnable: Runnable) {

View File

@@ -22,7 +22,7 @@ impl HeadlessClient {
pub(crate) fn new() -> Self {
let event_loop = EventLoop::try_new().unwrap();
let (common, main_receiver) = LinuxCommon::new(Box::new(event_loop.get_signal()), None);
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
let handle = event_loop.handle();
@@ -81,6 +81,8 @@ impl LinuxClient for HeadlessClient {
fn open_uri(&self, _uri: &str) {}
fn reveal_path(&self, _path: std::path::PathBuf) {}
fn write_to_primary(&self, _item: crate::ClipboardItem) {}
fn write_to_clipboard(&self, _item: crate::ClipboardItem) {}

View File

@@ -7,7 +7,7 @@ use std::ffi::OsString;
use std::fs::File;
use std::io::Read;
use std::ops::{Deref, DerefMut};
use std::os::fd::{AsRawFd, FromRawFd};
use std::os::fd::{AsFd, AsRawFd, FromRawFd};
use std::panic::Location;
use std::rc::Weak;
use std::{
@@ -20,13 +20,14 @@ use std::{
use anyhow::anyhow;
use ashpd::desktop::file_chooser::{OpenFileRequest, SaveFileRequest};
use ashpd::desktop::open_uri::{OpenDirectoryRequest, OpenFileRequest as OpenUriRequest};
use ashpd::{url, ActivationToken};
use async_task::Runnable;
use calloop::channel::Channel;
use calloop::{EventLoop, LoopHandle, LoopSignal};
use filedescriptor::FileDescriptor;
use flume::{Receiver, Sender};
use futures::channel::oneshot;
use mio::Waker;
use parking_lot::Mutex;
use time::UtcOffset;
use util::ResultExt;
@@ -67,6 +68,7 @@ pub trait LinuxClient {
) -> anyhow::Result<Box<dyn PlatformWindow>>;
fn set_cursor_style(&self, style: CursorStyle);
fn open_uri(&self, uri: &str);
fn reveal_path(&self, path: PathBuf);
fn write_to_primary(&self, item: ClipboardItem);
fn write_to_clipboard(&self, item: ClipboardItem);
fn read_from_primary(&self) -> Option<ClipboardItem>;
@@ -85,16 +87,6 @@ pub(crate) struct PlatformHandlers {
pub(crate) validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
}
pub trait QuitSignal {
fn quit(&mut self);
}
impl QuitSignal for LoopSignal {
fn quit(&mut self) {
self.stop();
}
}
pub(crate) struct LinuxCommon {
pub(crate) background_executor: BackgroundExecutor,
pub(crate) foreground_executor: ForegroundExecutor,
@@ -102,20 +94,17 @@ pub(crate) struct LinuxCommon {
pub(crate) appearance: WindowAppearance,
pub(crate) auto_hide_scrollbars: bool,
pub(crate) callbacks: PlatformHandlers,
pub(crate) quit_signal: Box<dyn QuitSignal>,
pub(crate) signal: LoopSignal,
pub(crate) menus: Vec<OwnedMenu>,
}
impl LinuxCommon {
pub fn new(
quit_signal: Box<dyn QuitSignal>,
main_waker: Option<Arc<Waker>>,
) -> (Self, Channel<Runnable>) {
pub fn new(signal: LoopSignal) -> (Self, Channel<Runnable>) {
let (main_sender, main_receiver) = calloop::channel::channel::<Runnable>();
let text_system = Arc::new(CosmicTextSystem::new());
let callbacks = PlatformHandlers::default();
let dispatcher = Arc::new(LinuxDispatcher::new(main_sender.clone(), main_waker));
let dispatcher = Arc::new(LinuxDispatcher::new(main_sender.clone()));
let background_executor = BackgroundExecutor::new(dispatcher.clone());
@@ -126,7 +115,7 @@ impl LinuxCommon {
appearance: WindowAppearance::Light,
auto_hide_scrollbars: false,
callbacks,
quit_signal,
signal,
menus: Vec::new(),
};
@@ -160,7 +149,7 @@ impl<P: LinuxClient + 'static> Platform for P {
}
fn quit(&self) {
self.with_common(|common| common.quit_signal.quit());
self.with_common(|common| common.signal.stop());
}
fn compositor_name(&self) -> &'static str {
@@ -221,10 +210,6 @@ impl<P: LinuxClient + 'static> Platform for P {
}
fn activate(&self, ignoring_other_apps: bool) {
println!(
"Platform.activate(ignoring_other_apps: {}) called",
ignoring_other_apps
);
log::info!("activate is not implemented on Linux, ignoring the call")
}
@@ -318,20 +303,27 @@ impl<P: LinuxClient + 'static> Platform for P {
let directory = directory.to_owned();
self.foreground_executor()
.spawn(async move {
let result = SaveFileRequest::default()
let request = SaveFileRequest::default()
.modal(true)
.title("Select new path")
.accept_label("Accept")
.send()
.await
.ok()
.and_then(|request| request.response().ok())
.and_then(|response| {
response
.uris()
.first()
.and_then(|uri| uri.to_file_path().ok())
});
.current_folder(directory);
let result = if let Ok(request) = request {
request
.send()
.await
.ok()
.and_then(|request| request.response().ok())
.and_then(|response| {
response
.uris()
.first()
.and_then(|uri| uri.to_file_path().ok())
})
} else {
None
};
done_tx.send(result);
})
@@ -341,13 +333,7 @@ impl<P: LinuxClient + 'static> Platform for P {
}
fn reveal_path(&self, path: &Path) {
if path.is_dir() {
open::that_detached(path);
return;
}
// If `path` is a file, the system may try to open it in a text editor
let dir = path.parent().unwrap_or(Path::new(""));
open::that_detached(dir);
self.reveal_path(path.to_owned());
}
fn on_quit(&self, callback: Box<dyn FnMut()>) {
@@ -508,18 +494,40 @@ impl<P: LinuxClient + 'static> Platform for P {
fn add_recent_document(&self, _path: &Path) {}
}
pub(super) fn open_uri_internal(uri: &str, activation_token: Option<&str>) {
let mut last_err = None;
for mut command in open::commands(uri) {
if let Some(token) = activation_token {
command.env("XDG_ACTIVATION_TOKEN", token);
}
match command.spawn() {
Ok(_) => return,
Err(err) => last_err = Some(err),
}
pub(super) fn open_uri_internal(
executor: BackgroundExecutor,
uri: &str,
activation_token: Option<String>,
) {
if let Some(uri) = url::Url::parse(uri).log_err() {
executor
.spawn(async move {
OpenUriRequest::default()
.activation_token(activation_token.map(ActivationToken::from))
.send_uri(&uri)
.await
.log_err();
})
.detach();
}
log::error!("failed to open uri: {uri:?}, last error: {last_err:?}");
}
pub(super) fn reveal_path_internal(
executor: BackgroundExecutor,
path: PathBuf,
activation_token: Option<String>,
) {
executor
.spawn(async move {
if let Some(dir) = File::open(path).log_err() {
OpenDirectoryRequest::default()
.activation_token(activation_token.map(ActivationToken::from))
.send(&dir.as_fd())
.await
.log_err();
}
})
.detach();
}
pub(super) fn is_within_click_distance(a: Point<Pixels>, b: Point<Pixels>) -> bool {
@@ -632,6 +640,8 @@ impl Keystroke {
Keysym::Prior => "pageup".to_owned(),
Keysym::Next => "pagedown".to_owned(),
Keysym::ISO_Left_Tab => "tab".to_owned(),
Keysym::KP_Prior => "pageup".to_owned(),
Keysym::KP_Next => "pagedown".to_owned(),
Keysym::comma => ",".to_owned(),
Keysym::period => ".".to_owned(),
@@ -669,7 +679,14 @@ impl Keystroke {
Keysym::equal => "=".to_owned(),
Keysym::plus => "+".to_owned(),
_ => xkb::keysym_get_name(key_sym).to_lowercase(),
_ => {
let name = xkb::keysym_get_name(key_sym).to_lowercase();
if key_sym.is_keypad_key() {
name.replace("kp_", "")
} else {
name
}
}
};
if modifiers.shift {

View File

@@ -21,7 +21,6 @@ use wayland_client::protocol::wl_callback::{self, WlCallback};
use wayland_client::protocol::wl_data_device_manager::DndAction;
use wayland_client::protocol::wl_data_offer::WlDataOffer;
use wayland_client::protocol::wl_pointer::AxisSource;
use wayland_client::protocol::wl_surface::WlSurface;
use wayland_client::protocol::{
wl_data_device, wl_data_device_manager, wl_data_offer, wl_data_source, wl_output, wl_region,
};
@@ -62,7 +61,6 @@ use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blu
use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1;
use xkbcommon::xkb::{self, Keycode, KEYMAP_COMPILE_NO_FLAGS};
use super::super::{open_uri_internal, read_fd, DOUBLE_CLICK_INTERVAL};
use super::display::WaylandDisplay;
use super::window::{ImeInput, WaylandWindowStatePtr};
use crate::platform::linux::wayland::clipboard::{
@@ -73,11 +71,14 @@ use crate::platform::linux::wayland::serial::{SerialKind, SerialTracker};
use crate::platform::linux::wayland::window::WaylandWindow;
use crate::platform::linux::xdg_desktop_portal::{Event as XDPEvent, XDPEventSource};
use crate::platform::linux::LinuxClient;
use crate::platform::linux::{get_xkb_compose_state, is_within_click_distance};
use crate::platform::linux::{
get_xkb_compose_state, is_within_click_distance, open_uri_internal, read_fd,
reveal_path_internal,
};
use crate::platform::PlatformWindow;
use crate::{
point, px, size, Bounds, DevicePixels, FileDropEvent, ForegroundExecutor, MouseExitEvent, Size,
SCROLL_LINES,
DOUBLE_CLICK_INTERVAL, SCROLL_LINES,
};
use crate::{
AnyWindowHandle, CursorStyle, DisplayId, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers,
@@ -221,8 +222,7 @@ pub(crate) struct WaylandClientState {
data_offers: Vec<DataOffer<WlDataOffer>>,
primary_data_offer: Option<DataOffer<ZwpPrimarySelectionOfferV1>>,
cursor: Cursor,
pending_open_uri: Option<String>,
pub pending_window_activation: Option<WlSurface>,
pending_activation: Option<PendingActivation>,
event_loop: Option<EventLoop<'static, WaylandClientStatePtr>>,
common: LinuxCommon,
}
@@ -246,6 +246,15 @@ pub(crate) struct KeyRepeat {
current_keycode: Option<xkb::Keycode>,
}
pub(crate) enum PendingActivation {
/// URI to open in the web browser.
Uri(String),
/// Path to open in the file explorer.
Path(PathBuf),
/// A window from ourselves to raise.
Window(ObjectId),
}
/// This struct is required to conform to Rust's orphan rules, so we can dispatch on the state but hand the
/// window to GPUI.
#[derive(Clone)]
@@ -262,6 +271,11 @@ impl WaylandClientStatePtr {
self.0.upgrade().unwrap().borrow().serial_tracker.get(kind)
}
pub fn set_pending_activation(&self, window: ObjectId) {
self.0.upgrade().unwrap().borrow_mut().pending_activation =
Some(PendingActivation::Window(window));
}
pub fn enable_ime(&self) {
let client = self.get_client();
let mut state = client.borrow_mut();
@@ -312,7 +326,7 @@ impl WaylandClientStatePtr {
}
}
if state.windows.is_empty() {
state.common.quit_signal.quit();
state.common.signal.stop();
}
}
}
@@ -408,7 +422,7 @@ impl WaylandClient {
let event_loop = EventLoop::<WaylandClientStatePtr>::try_new().unwrap();
let (common, main_receiver) = LinuxCommon::new(Box::new(event_loop.get_signal()), None);
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
let handle = event_loop.handle();
handle
@@ -445,7 +459,7 @@ impl WaylandClient {
let mut cursor = Cursor::new(&conn, &globals, 24);
handle
.insert_source(XDPEventSource::new(&common.background_executor, None), {
.insert_source(XDPEventSource::new(&common.background_executor), {
move |event, _, client| match event {
XDPEvent::WindowAppearance(appearance) => {
if let Some(client) = client.0.upgrade() {
@@ -532,9 +546,8 @@ impl WaylandClient {
data_offers: Vec::new(),
primary_data_offer: None,
cursor,
pending_open_uri: None,
pending_activation: None,
event_loop: Some(event_loop),
pending_window_activation: None,
}));
WaylandSource::new(conn, event_queue)
@@ -632,14 +645,33 @@ impl LinuxClient for WaylandClient {
state.globals.activation.clone(),
state.mouse_focused_window.clone(),
) {
state.pending_open_uri = Some(uri.to_owned());
state.pending_activation = Some(PendingActivation::Uri(uri.to_string()));
let token = activation.get_activation_token(&state.globals.qh, ());
let serial = state.serial_tracker.get(SerialKind::MousePress);
token.set_serial(serial, &state.wl_seat);
token.set_surface(&window.surface());
token.commit();
} else {
open_uri_internal(uri, None);
let executor = state.common.background_executor.clone();
open_uri_internal(executor, uri, None);
}
}
fn reveal_path(&self, path: PathBuf) {
let mut state = self.0.borrow_mut();
if let (Some(activation), Some(window)) = (
state.globals.activation.clone(),
state.mouse_focused_window.clone(),
) {
state.pending_activation = Some(PendingActivation::Path(path));
let token = activation.get_activation_token(&state.globals.qh, ());
let serial = state.serial_tracker.get(SerialKind::MousePress);
token.set_serial(serial, &state.wl_seat);
token.set_surface(&window.surface());
token.commit();
} else {
let executor = state.common.background_executor.clone();
reveal_path_internal(executor, path, None);
}
}
@@ -812,7 +844,7 @@ impl Dispatch<WlCallback, ObjectId> for WaylandClientStatePtr {
match event {
wl_callback::Event::Done { .. } => {
window.frame(true);
window.frame();
}
_ => {}
}
@@ -957,22 +989,25 @@ impl Dispatch<xdg_activation_token_v1::XdgActivationTokenV1, ()> for WaylandClie
) {
let client = this.get_client();
let mut state = client.borrow_mut();
if let xdg_activation_token_v1::Event::Done { token } = event {
if let Some(uri) = state.pending_open_uri.take() {
open_uri_internal(&uri, Some(&token));
} else if let Some(surface) = state.pending_window_activation.take() {
println!("we got a surface");
if let Some(ref manager) = state.globals.activation {
println!("we're calling activate. token: {:?}", token);
manager.activate(token, &surface);
let executor = state.common.background_executor.clone();
match state.pending_activation.take() {
Some(PendingActivation::Uri(uri)) => open_uri_internal(executor, &uri, Some(token)),
Some(PendingActivation::Path(path)) => {
reveal_path_internal(executor, path, Some(token))
}
// if let Some(surface) = state.pending_activations.remove(&token) {
// if let Some(ref manager) = state.globals.activation {
// manager.activate(token, &surface);
// }
// }
Some(PendingActivation::Window(window)) => {
let Some(window) = get_window(&mut state, &window) else {
return;
};
let activation = state.globals.activation.as_ref().unwrap();
activation.activate(token, &window.surface());
}
None => log::error!("activation token received with no pending activation"),
}
}
token.destroy();
}
}
@@ -1206,7 +1241,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
&& state.repeat.current_keycode.is_some()
&& state.keyboard_focused_window.is_some();
if !is_repeating {
if !is_repeating || rate == 0 {
return TimeoutAction::Drop;
}
@@ -1520,6 +1555,11 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
if state.axis_source == AxisSource::Wheel {
return;
}
let axis = if state.modifiers.shift {
wl_pointer::Axis::HorizontalScroll
} else {
axis
};
let axis_modifier = match axis {
wl_pointer::Axis::VerticalScroll => state.vertical_modifier,
wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier,
@@ -1545,6 +1585,11 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
discrete,
} => {
state.scroll_event_received = true;
let axis = if state.modifiers.shift {
wl_pointer::Axis::HorizontalScroll
} else {
axis
};
let axis_modifier = match axis {
wl_pointer::Axis::VerticalScroll => state.vertical_modifier,
wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier,
@@ -1567,6 +1612,11 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
value120,
} => {
state.scroll_event_received = true;
let axis = if state.modifiers.shift {
wl_pointer::Axis::HorizontalScroll
} else {
axis
};
let axis_modifier = match axis {
wl_pointer::Axis::VerticalScroll => state.vertical_modifier,
wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier,

View File

@@ -74,9 +74,9 @@ struct InProgressConfigure {
pub struct WaylandWindowState {
xdg_surface: xdg_surface::XdgSurface,
acknowledged_first_configure: bool,
app_id: Option<String>,
pub surface: wl_surface::WlSurface,
decoration: Option<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1>,
app_id: Option<String>,
appearance: WindowAppearance,
blur: Option<org_kde_kwin_blur::OrgKdeKwinBlur>,
toplevel: xdg_toplevel::XdgToplevel,
@@ -101,7 +101,6 @@ pub struct WaylandWindowState {
in_progress_window_controls: Option<WindowControls>,
window_controls: WindowControls,
inset: Option<Pixels>,
requested_inset: Option<Pixels>,
}
#[derive(Clone)]
@@ -157,10 +156,10 @@ impl WaylandWindowState {
Ok(Self {
xdg_surface,
app_id: None,
acknowledged_first_configure: false,
surface,
decoration,
app_id: None,
blur: None,
toplevel,
viewport,
@@ -191,7 +190,6 @@ impl WaylandWindowState {
window_menu: true,
},
inset: None,
requested_inset: None,
})
}
@@ -318,12 +316,11 @@ impl WaylandWindowStatePtr {
Rc::ptr_eq(&self.state, &other.state)
}
pub fn frame(&self, request_frame_callback: bool) {
if request_frame_callback {
let mut state = self.state.borrow_mut();
state.surface.frame(&state.globals.qh, state.surface.id());
drop(state);
}
pub fn frame(&self) {
let mut state = self.state.borrow_mut();
state.surface.frame(&state.globals.qh, state.surface.id());
drop(state);
let mut cb = self.callbacks.borrow_mut();
if let Some(fun) = cb.request_frame.as_mut() {
fun();
@@ -353,13 +350,12 @@ impl WaylandWindowStatePtr {
state.fullscreen = configure.fullscreen;
state.maximized = configure.maximized;
state.tiling = configure.tiling;
if got_unmaximized {
configure.size = Some(state.window_bounds.size);
} else if !configure.maximized {
configure.size =
compute_outer_size(state.inset, configure.size, state.tiling);
}
if !configure.fullscreen && !configure.maximized {
configure.size = if got_unmaximized {
Some(state.window_bounds.size)
} else {
compute_outer_size(state.inset, configure.size, state.tiling)
};
if let Some(size) = configure.size {
state.window_bounds = Bounds {
origin: Point::default(),
@@ -375,12 +371,27 @@ impl WaylandWindowStatePtr {
}
let mut state = self.state.borrow_mut();
state.xdg_surface.ack_configure(serial);
let request_frame_callback = !state.acknowledged_first_configure;
state.acknowledged_first_configure = true;
let window_geometry = inset_by_tiling(
state.bounds.map_origin(|_| px(0.0)),
state.inset.unwrap_or(px(0.0)),
state.tiling,
)
.map(|v| v.0 as i32)
.map_size(|v| if v <= 0 { 1 } else { v });
state.xdg_surface.set_window_geometry(
window_geometry.origin.x,
window_geometry.origin.y,
window_geometry.size.width,
window_geometry.size.height,
);
let request_frame_callback = !state.acknowledged_first_configure;
if request_frame_callback {
state.acknowledged_first_configure = true;
drop(state);
self.frame(true);
self.frame();
}
}
_ => {}
@@ -472,6 +483,10 @@ impl WaylandWindowStatePtr {
}
}
if fullscreen || maximized {
tiling = Tiling::tiled();
}
let mut state = self.state.borrow_mut();
state.in_progress_configure = Some(InProgressConfigure {
size,
@@ -810,35 +825,19 @@ impl PlatformWindow for WaylandWindow {
}
fn activate(&self) {
println!("WaylandWindow.activate called");
// Try to request an activation token. Even though the activation is likely going to be rejected,
// KWin and Mutter can use the app_id to visually indicate we're requesting attention.
let state = self.borrow();
let activation = state.globals.activation.as_ref().cloned();
if let Some(manager) = activation {
let token = manager.get_activation_token(&state.globals.qh, ());
if let (Some(activation), Some(app_id)) = (&state.globals.activation, state.app_id.clone())
{
state.client.set_pending_activation(state.surface.id());
let token = activation.get_activation_token(&state.globals.qh, ());
// The serial isn't exactly important here, since the activation is probably going to be rejected anyway.
let serial = state.client.get_serial(SerialKind::MousePress);
println!("Creating activation token...");
token.set_app_id(app_id);
token.set_serial(serial, &state.globals.seat);
println!("Token serial set: {:?}", serial);
token.set_surface(&state.surface);
println!("Surface set for activation: {:?}", &state.surface);
if let Some(app_id) = state.app_id.as_ref() {
token.set_app_id(app_id.clone());
println!("App ID set for activation: {}", app_id);
}
state
.client
.get_client()
.borrow_mut()
.pending_window_activation = Some(state.surface.clone());
token.commit();
println!("Activation token committed, waiting for done event.");
} else {
log::warn!("Wayland compositor does not support xdg_activation_v1");
}
}
@@ -852,8 +851,8 @@ impl PlatformWindow for WaylandWindow {
fn set_app_id(&mut self, app_id: &str) {
let mut state = self.borrow_mut();
state.app_id = Some(app_id.to_owned());
state.toplevel.set_app_id(app_id.to_owned());
state.app_id = Some(app_id.to_owned());
}
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
@@ -926,26 +925,7 @@ impl PlatformWindow for WaylandWindow {
}
fn completed_frame(&self) {
let mut state = self.borrow_mut();
if let Some(area) = state.requested_inset {
state.inset = Some(area);
}
let window_geometry = inset_by_tiling(
state.bounds.map_origin(|_| px(0.0)),
state.inset.unwrap_or(px(0.0)),
state.tiling,
)
.map(|v| v.0 as i32)
.map_size(|v| if v <= 0 { 1 } else { v });
state.xdg_surface.set_window_geometry(
window_geometry.origin.x,
window_geometry.origin.y,
window_geometry.size.width,
window_geometry.size.height,
);
let state = self.borrow();
state.surface.commit();
}
@@ -1006,7 +986,7 @@ impl PlatformWindow for WaylandWindow {
fn set_client_inset(&self, inset: Pixels) {
let mut state = self.borrow_mut();
if Some(inset) != state.inset {
state.requested_inset = Some(inset);
state.inset = Some(inset);
update_window(state);
}
}

View File

@@ -1,26 +1,23 @@
use std::cell::RefCell;
use std::collections::HashSet;
use std::ops::Deref;
use std::os::fd::AsRawFd;
use std::path::PathBuf;
use std::rc::{Rc, Weak};
use std::sync::Arc;
use std::time::{Duration, Instant};
use anyhow::Context;
use async_task::Runnable;
use calloop::channel::Channel;
use calloop::generic::{FdWrapper, Generic};
use calloop::{EventLoop, LoopHandle, RegistrationToken};
use collections::HashMap;
use futures::channel::oneshot;
use mio::{Interest, Token, Waker};
use util::ResultExt;
use x11rb::connection::{Connection, RequestConnection};
use x11rb::cursor;
use x11rb::errors::ConnectionError;
use x11rb::protocol::randr::ConnectionExt as _;
use x11rb::protocol::xinput::ConnectionExt;
use x11rb::protocol::xkb::ConnectionExt as _;
use x11rb::protocol::xproto::{ChangeWindowAttributesAux, ConnectionExt as _};
use x11rb::protocol::xproto::{ChangeWindowAttributesAux, ConnectionExt as _, KeyPressEvent};
use x11rb::protocol::{randr, render, xinput, xkb, xproto, Event};
use x11rb::resource_manager::Database;
use x11rb::xcb_ffi::XCBConnection;
@@ -33,24 +30,24 @@ use crate::platform::linux::LinuxClient;
use crate::platform::{LinuxCommon, PlatformWindow};
use crate::{
modifiers_from_xinput_info, point, px, AnyWindowHandle, Bounds, ClipboardItem, CursorStyle,
DisplayId, Keystroke, Modifiers, ModifiersChangedEvent, Pixels, PlatformDisplay, PlatformInput,
Point, QuitSignal, ScrollDelta, Size, TouchPhase, WindowParams, X11Window,
DisplayId, Keystroke, Modifiers, ModifiersChangedEvent, Pixels, Platform, PlatformDisplay,
PlatformInput, Point, ScrollDelta, Size, TouchPhase, WindowParams, X11Window,
};
use super::{
super::{get_xkb_compose_state, open_uri_internal, SCROLL_LINES},
X11Display, X11WindowStatePtr, XcbAtoms,
};
use super::{button_of_key, modifiers_from_state, pressed_button_from_mask};
use super::{X11Display, X11WindowStatePtr, XcbAtoms};
use super::{XimCallbackEvent, XimHandler};
use crate::platform::linux::is_within_click_distance;
use crate::platform::linux::platform::DOUBLE_CLICK_INTERVAL;
use crate::platform::linux::platform::{DOUBLE_CLICK_INTERVAL, SCROLL_LINES};
use crate::platform::linux::xdg_desktop_portal::{Event as XDPEvent, XDPEventSource};
use crate::platform::linux::{
get_xkb_compose_state, is_within_click_distance, open_uri_internal, reveal_path_internal,
};
pub(super) const XINPUT_MASTER_DEVICE: u16 = 1;
pub(crate) struct WindowRef {
window: X11WindowStatePtr,
refresh_event_token: RegistrationToken,
}
impl WindowRef {
@@ -98,19 +95,17 @@ impl From<xim::ClientError> for EventHandlerError {
}
pub struct X11ClientState {
/// poll is in an Option so we can take it out in `run()` without
/// mutating self.
poll: Option<mio::Poll>,
quit_signal_rx: oneshot::Receiver<()>,
runnables: Channel<Runnable>,
xdp_event_source: XDPEventSource,
pub(crate) loop_handle: LoopHandle<'static, X11Client>,
pub(crate) event_loop: Option<calloop::EventLoop<'static, X11Client>>,
pub(crate) last_click: Instant,
pub(crate) last_location: Point<Pixels>,
pub(crate) current_count: usize,
pub(crate) scale_factor: f32,
pub(crate) xcb_connection: Rc<XCBConnection>,
client_side_decorations_supported: bool,
pub(crate) x_root_index: usize,
pub(crate) _resource_database: Database,
pub(crate) atoms: XcbAtoms,
@@ -124,6 +119,7 @@ pub struct X11ClientState {
pub(crate) compose_state: Option<xkbc::compose::State>,
pub(crate) pre_edit_text: Option<String>,
pub(crate) composing: bool,
pub(crate) pre_ime_key_down: Option<Keystroke>,
pub(crate) cursor_handle: cursor::Handle,
pub(crate) cursor_styles: HashMap<xproto::Window, CursorStyle>,
pub(crate) cursor_cache: HashMap<CursorStyle, xproto::Cursor>,
@@ -145,46 +141,14 @@ impl X11ClientStatePtr {
let client = X11Client(self.0.upgrade().expect("client already dropped"));
let mut state = client.0.borrow_mut();
if state.windows.remove(&x_window).is_none() {
log::warn!(
"failed to remove X window {} from client state, does not exist",
x_window
);
if let Some(window_ref) = state.windows.remove(&x_window) {
state.loop_handle.remove(window_ref.refresh_event_token);
}
state.cursor_styles.remove(&x_window);
if state.windows.is_empty() {
state.common.quit_signal.quit();
}
}
}
struct ChannelQuitSignal {
tx: Option<oneshot::Sender<()>>,
waker: Option<Arc<Waker>>,
}
impl ChannelQuitSignal {
fn new(waker: Option<Arc<Waker>>) -> (Self, oneshot::Receiver<()>) {
let (tx, rx) = oneshot::channel::<()>();
let quit_signal = ChannelQuitSignal {
tx: Some(tx),
waker,
};
(quit_signal, rx)
}
}
impl QuitSignal for ChannelQuitSignal {
fn quit(&mut self) {
if let Some(tx) = self.tx.take() {
tx.send(()).log_err();
if let Some(waker) = self.waker.as_ref() {
waker.wake().ok();
}
state.common.signal.stop();
}
}
}
@@ -194,12 +158,27 @@ pub(crate) struct X11Client(Rc<RefCell<X11ClientState>>);
impl X11Client {
pub(crate) fn new() -> Self {
let mut poll = mio::Poll::new().unwrap();
let event_loop = EventLoop::try_new().unwrap();
let waker = Arc::new(Waker::new(poll.registry(), WAKER_TOKEN).unwrap());
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
let (quit_signal, quit_signal_rx) = ChannelQuitSignal::new(Some(waker.clone()));
let (common, runnables) = LinuxCommon::new(Box::new(quit_signal), Some(waker.clone()));
let handle = event_loop.handle();
handle
.insert_source(main_receiver, {
let handle = handle.clone();
move |event, _, _: &mut X11Client| {
if let calloop::channel::Event::Msg(runnable) = event {
// Insert the runnables as idle callbacks, so we make sure that user-input and X11
// events have higher priority and runnables are only worked off after the event
// callbacks.
handle.insert_idle(|_| {
runnable.run();
});
}
}
})
.unwrap();
let (xcb_connection, x_root_index) = XCBConnection::connect(None).unwrap();
xcb_connection
@@ -241,13 +220,25 @@ impl X11Client {
.map(|class| *class)
.collect::<Vec<_>>();
let atoms = XcbAtoms::new(&xcb_connection).unwrap();
let atoms = XcbAtoms::new(&xcb_connection).unwrap().reply().unwrap();
let root = xcb_connection.setup().roots[0].root;
let compositor_present = check_compositor_present(&xcb_connection, root);
let gtk_frame_extents_supported =
check_gtk_frame_extents_supported(&xcb_connection, &atoms, root);
let client_side_decorations_supported = compositor_present && gtk_frame_extents_supported;
log::info!(
"x11: compositor present: {}, gtk_frame_extents_supported: {}",
compositor_present,
gtk_frame_extents_supported
);
let xkb = xcb_connection
.xkb_use_extension(XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION)
.unwrap()
.reply()
.unwrap();
let atoms = atoms.reply().unwrap();
let xkb = xkb.reply().unwrap();
let events = xkb::EventType::STATE_NOTIFY;
xcb_connection
.xkb_select_events(
@@ -298,24 +289,54 @@ impl X11Client {
None
};
let xdp_event_source =
XDPEventSource::new(&common.background_executor, Some(waker.clone()));
// Safety: Safe if xcb::Connection always returns a valid fd
let fd = unsafe { FdWrapper::new(Rc::clone(&xcb_connection)) };
handle
.insert_source(
Generic::new_with_error::<EventHandlerError>(
fd,
calloop::Interest::READ,
calloop::Mode::Level,
),
{
let xcb_connection = xcb_connection.clone();
move |_readiness, _, client| {
client.process_x11_events(&xcb_connection)?;
Ok(calloop::PostAction::Continue)
}
},
)
.expect("Failed to initialize x11 event source");
handle
.insert_source(XDPEventSource::new(&common.background_executor), {
move |event, _, client| match event {
XDPEvent::WindowAppearance(appearance) => {
client.with_common(|common| common.appearance = appearance);
for (_, window) in &mut client.0.borrow_mut().windows {
window.window.set_appearance(appearance);
}
}
XDPEvent::CursorTheme(_) | XDPEvent::CursorSize(_) => {
// noop, X11 manages this for us.
}
}
})
.unwrap();
X11Client(Rc::new(RefCell::new(X11ClientState {
poll: Some(poll),
runnables,
xdp_event_source,
quit_signal_rx,
common,
modifiers: Modifiers::default(),
event_loop: Some(event_loop),
loop_handle: handle,
common,
last_click: Instant::now(),
last_location: Point::new(px(0.0), px(0.0)),
current_count: 0,
scale_factor,
xcb_connection,
client_side_decorations_supported,
x_root_index,
_resource_database: resource_database,
atoms,
@@ -327,6 +348,7 @@ impl X11Client {
compose_state,
pre_edit_text: None,
pre_ime_key_down: None,
composing: false,
cursor_handle,
@@ -342,6 +364,125 @@ impl X11Client {
})))
}
pub fn process_x11_events(
&self,
xcb_connection: &XCBConnection,
) -> Result<(), EventHandlerError> {
loop {
let mut events = Vec::new();
let mut windows_to_refresh = HashSet::new();
let mut last_key_release = None;
let mut last_key_press: Option<KeyPressEvent> = None;
loop {
match xcb_connection.poll_for_event() {
Ok(Some(event)) => {
match event {
Event::Expose(expose_event) => {
windows_to_refresh.insert(expose_event.window);
}
Event::KeyRelease(_) => {
last_key_release = Some(event);
}
Event::KeyPress(key_press) => {
if let Some(last_press) = last_key_press.as_ref() {
if last_press.detail == key_press.detail {
continue;
}
}
if let Some(Event::KeyRelease(key_release)) =
last_key_release.take()
{
// We ignore that last KeyRelease if it's too close to this KeyPress,
// suggesting that it's auto-generated by X11 as a key-repeat event.
if key_release.detail != key_press.detail
|| key_press.time.saturating_sub(key_release.time) > 20
{
events.push(Event::KeyRelease(key_release));
}
}
events.push(Event::KeyPress(key_press));
last_key_press = Some(key_press);
}
_ => {
if let Some(release_event) = last_key_release.take() {
events.push(release_event);
}
events.push(event);
}
}
}
Ok(None) => {
// Add any remaining stored KeyRelease event
if let Some(release_event) = last_key_release.take() {
events.push(release_event);
}
break;
}
Err(e) => {
log::warn!("error polling for X11 events: {e:?}");
break;
}
}
}
if events.is_empty() && windows_to_refresh.is_empty() {
break;
}
for window in windows_to_refresh.into_iter() {
if let Some(window) = self.get_window(window) {
window.refresh();
}
}
for event in events.into_iter() {
let mut state = self.0.borrow_mut();
if state.ximc.is_none() || state.xim_handler.is_none() {
drop(state);
self.handle_event(event);
continue;
}
let mut ximc = state.ximc.take().unwrap();
let mut xim_handler = state.xim_handler.take().unwrap();
let xim_connected = xim_handler.connected;
drop(state);
let xim_filtered = match ximc.filter_event(&event, &mut xim_handler) {
Ok(handled) => handled,
Err(err) => {
log::error!("XIMClientError: {}", err);
false
}
};
let xim_callback_event = xim_handler.last_callback_event.take();
let mut state = self.0.borrow_mut();
state.ximc = Some(ximc);
state.xim_handler = Some(xim_handler);
drop(state);
if let Some(event) = xim_callback_event {
self.handle_xim_callback_event(event);
}
if xim_filtered {
continue;
}
if xim_connected {
self.xim_handle_event(event);
} else {
self.handle_event(event);
}
}
}
Ok(())
}
pub fn enable_ime(&self) {
let mut state = self.0.borrow_mut();
if state.ximc.is_none() {
@@ -404,110 +545,6 @@ impl X11Client {
.map(|window_reference| window_reference.window.clone())
}
fn read_x11_events(&self) -> (HashSet<u32>, Vec<Event>) {
let mut events = Vec::new();
let mut windows_to_refresh = HashSet::new();
let mut state = self.0.borrow_mut();
let mut last_key_release: Option<Event> = None;
loop {
match state.xcb_connection.poll_for_event() {
Ok(Some(event)) => {
if let Event::Expose(expose_event) = event {
windows_to_refresh.insert(expose_event.window);
} else {
match event {
Event::KeyRelease(_) => {
last_key_release = Some(event);
}
Event::KeyPress(key_press) => {
if let Some(Event::KeyRelease(key_release)) =
last_key_release.take()
{
// We ignore that last KeyRelease if it's too close to this KeyPress,
// suggesting that it's auto-generated by X11 as a key-repeat event.
if key_release.detail != key_press.detail
|| key_press.time.wrapping_sub(key_release.time) > 20
{
events.push(Event::KeyRelease(key_release));
}
}
events.push(Event::KeyPress(key_press));
}
_ => {
if let Some(release_event) = last_key_release.take() {
events.push(release_event);
}
events.push(event);
}
}
}
}
Ok(None) => {
// Add any remaining stored KeyRelease event
if let Some(release_event) = last_key_release.take() {
events.push(release_event);
}
break;
}
Err(e) => {
log::warn!("error polling for X11 events: {e:?}");
break;
}
}
}
(windows_to_refresh, events)
}
fn process_x11_events(&self, events: Vec<Event>) {
for event in events.into_iter() {
let mut state = self.0.borrow_mut();
if state.ximc.is_none() || state.xim_handler.is_none() {
drop(state);
self.handle_event(event);
continue;
}
let mut ximc = state.ximc.take().unwrap();
let mut xim_handler = state.xim_handler.take().unwrap();
let xim_connected = xim_handler.connected;
drop(state);
// let xim_filtered = false;
let xim_filtered = match ximc.filter_event(&event, &mut xim_handler) {
Ok(handled) => handled,
Err(err) => {
log::error!("XIMClientError: {}", err);
false
}
};
let xim_callback_event = xim_handler.last_callback_event.take();
let mut state = self.0.borrow_mut();
state.ximc = Some(ximc);
state.xim_handler = Some(xim_handler);
if let Some(event) = xim_callback_event {
drop(state);
self.handle_xim_callback_event(event);
} else {
drop(state);
}
if xim_filtered {
continue;
}
if xim_connected {
self.xim_handle_event(event);
} else {
self.handle_event(event);
}
}
}
fn handle_event(&self, event: Event) -> Option<()> {
match event {
Event::ClientMessage(event) => {
@@ -547,10 +584,6 @@ impl X11Client {
let window = self.get_window(event.window)?;
window.property_notify(event);
}
Event::Expose(event) => {
let window = self.get_window(event.window)?;
window.refresh();
}
Event::FocusIn(event) => {
let window = self.get_window(event.event)?;
window.set_focused(true);
@@ -578,8 +611,8 @@ impl X11Client {
event.base_mods.into(),
event.latched_mods.into(),
event.locked_mods.into(),
0,
0,
event.base_group as u32,
event.latched_group as u32,
event.locked_group.into(),
);
@@ -603,6 +636,7 @@ impl X11Client {
let modifiers = modifiers_from_state(event.state);
state.modifiers = modifiers;
state.pre_ime_key_down.take();
let keystroke = {
let code = event.detail.into();
@@ -817,10 +851,15 @@ impl X11Client {
if let Some(old_scroll) = old_scroll {
let delta_scroll = old_scroll - new_scroll;
let (x, y) = if !modifiers.shift {
(0.0, delta_scroll)
} else {
(delta_scroll, 0.0)
};
window.handle_input(PlatformInput::ScrollWheel(
crate::ScrollWheelEvent {
position,
delta: ScrollDelta::Lines(Point::new(0.0, delta_scroll)),
delta: ScrollDelta::Lines(Point::new(x, y)),
modifiers,
touch_phase: TouchPhase::default(),
},
@@ -877,6 +916,11 @@ impl X11Client {
match event {
Event::KeyPress(event) | Event::KeyRelease(event) => {
let mut state = self.0.borrow_mut();
state.pre_ime_key_down = Some(Keystroke::from_xkb(
&state.xkb,
state.modifiers,
event.detail.into(),
));
let mut ximc = state.ximc.take().unwrap();
let mut xim_handler = state.xim_handler.take().unwrap();
drop(state);
@@ -903,6 +947,16 @@ impl X11Client {
fn xim_handle_commit(&self, window: xproto::Window, text: String) -> Option<()> {
let window = self.get_window(window).unwrap();
let mut state = self.0.borrow_mut();
if !state.composing {
if let Some(keystroke) = state.pre_ime_key_down.take() {
drop(state);
window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent {
keystroke,
is_held: false,
}));
return Some(());
}
}
state.composing = false;
drop(state);
@@ -952,13 +1006,11 @@ impl X11Client {
}
}
const XCB_CONNECTION_TOKEN: Token = Token(0);
const WAKER_TOKEN: Token = Token(1);
impl LinuxClient for X11Client {
fn compositor_name(&self) -> &'static str {
"X11"
}
fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R {
f(&mut self.0.borrow_mut().common)
}
@@ -1017,6 +1069,7 @@ impl LinuxClient for X11Client {
state.common.foreground_executor.clone(),
params,
&state.xcb_connection,
state.client_side_decorations_supported,
state.x_root_index,
x_window,
&state.atoms,
@@ -1024,8 +1077,61 @@ impl LinuxClient for X11Client {
state.common.appearance,
)?;
let screen_resources = state
.xcb_connection
.randr_get_screen_resources(x_window)
.unwrap()
.reply()
.expect("Could not find available screens");
let mode = screen_resources
.crtcs
.iter()
.find_map(|crtc| {
let crtc_info = state
.xcb_connection
.randr_get_crtc_info(*crtc, x11rb::CURRENT_TIME)
.ok()?
.reply()
.ok()?;
screen_resources
.modes
.iter()
.find(|m| m.id == crtc_info.mode)
})
.expect("Unable to find screen refresh rate");
let refresh_event_token = state
.loop_handle
.insert_source(calloop::timer::Timer::immediate(), {
let refresh_duration = mode_refresh_rate(mode);
move |mut instant, (), client| {
let xcb_connection = {
let state = client.0.borrow_mut();
let xcb_connection = state.xcb_connection.clone();
if let Some(window) = state.windows.get(&x_window) {
let window = window.window.clone();
drop(state);
window.refresh();
}
xcb_connection
};
client.process_x11_events(&xcb_connection).log_err();
// Take into account that some frames have been skipped
let now = Instant::now();
while instant < now {
instant += refresh_duration;
}
calloop::timer::TimeoutAction::ToInstant(instant)
}
})
.expect("Failed to initialize refresh timer");
let window_ref = WindowRef {
window: window.0.clone(),
refresh_event_token,
};
state.windows.insert(x_window, window_ref);
@@ -1067,11 +1173,17 @@ impl LinuxClient for X11Client {
..Default::default()
},
)
.expect("failed to change window cursor");
.expect("failed to change window cursor")
.check()
.unwrap();
}
fn open_uri(&self, uri: &str) {
open_uri_internal(uri, None);
open_uri_internal(self.background_executor(), uri, None);
}
fn reveal_path(&self, path: PathBuf) {
reveal_path_internal(self.background_executor(), path, None);
}
fn write_to_primary(&self, item: crate::ClipboardItem) {
@@ -1148,123 +1260,14 @@ impl LinuxClient for X11Client {
}
fn run(&self) {
let mut poll = self
let mut event_loop = self
.0
.borrow_mut()
.poll
.event_loop
.take()
.context("no poll set on X11Client. calling run more than once is not possible")
.unwrap();
.expect("App is already running");
let xcb_fd = self.0.borrow().xcb_connection.as_raw_fd();
let mut xcb_source = mio::unix::SourceFd(&xcb_fd);
poll.registry()
.register(&mut xcb_source, XCB_CONNECTION_TOKEN, Interest::READABLE)
.unwrap();
let mut events = mio::Events::with_capacity(1024);
let mut next_refresh_needed = Instant::now();
'run_loop: loop {
let poll_timeout = next_refresh_needed - Instant::now();
// We rounding the poll_timeout down so `mio` doesn't round it up to the next higher milliseconds
let poll_timeout = Duration::from_millis(poll_timeout.as_millis() as u64);
if poll_timeout >= Duration::from_millis(1) {
let _ = poll.poll(&mut events, Some(poll_timeout));
};
let mut state = self.0.borrow_mut();
// Check if we need to quit
if let Ok(Some(())) = state.quit_signal_rx.try_recv() {
return;
}
// Redraw windows
let now = Instant::now();
if now > next_refresh_needed {
// This will be pulled down to 16ms (or less) if a window is open
let mut frame_length = Duration::from_millis(100);
let mut windows = vec![];
for (_, window_ref) in state.windows.iter() {
if !window_ref.window.state.borrow().destroyed {
frame_length = frame_length.min(window_ref.window.refresh_rate());
windows.push(window_ref.window.clone());
}
}
drop(state);
for window in windows {
window.refresh();
}
state = self.0.borrow_mut();
// In the case that we're looping a bit too fast, slow down
next_refresh_needed = now.max(next_refresh_needed) + frame_length;
}
// X11 events
drop(state);
loop {
let (x_windows, events) = self.read_x11_events();
for x_window in x_windows {
if let Some(window) = self.get_window(x_window) {
window.refresh();
}
}
if events.len() == 0 {
break;
}
self.process_x11_events(events);
// When X11 is sending us events faster than we can handle we'll
// let the frame rate drop to 10fps to try and avoid getting too behind.
if Instant::now() > next_refresh_needed + Duration::from_millis(80) {
continue 'run_loop;
}
}
state = self.0.borrow_mut();
// Runnables
while let Ok(runnable) = state.runnables.try_recv() {
drop(state);
runnable.run();
state = self.0.borrow_mut();
if Instant::now() + Duration::from_millis(1) >= next_refresh_needed {
continue 'run_loop;
}
}
// XDG events
if let Ok(event) = state.xdp_event_source.try_recv() {
match event {
XDPEvent::WindowAppearance(appearance) => {
let mut windows = state
.windows
.values()
.map(|window| window.window.clone())
.collect::<Vec<_>>();
drop(state);
self.with_common(|common| common.appearance = appearance);
for mut window in windows {
window.set_appearance(appearance);
}
}
XDPEvent::CursorTheme(_) | XDPEvent::CursorSize(_) => {
// noop, X11 manages this for us.
}
};
};
}
event_loop.run(None, &mut self.clone(), |_| {}).log_err();
}
fn active_window(&self) -> Option<AnyWindowHandle> {
@@ -1278,6 +1281,120 @@ impl LinuxClient for X11Client {
}
}
// Adatpted from:
// https://docs.rs/winit/0.29.11/src/winit/platform_impl/linux/x11/monitor.rs.html#103-111
pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration {
if mode.dot_clock == 0 || mode.htotal == 0 || mode.vtotal == 0 {
return Duration::from_millis(16);
}
let millihertz = mode.dot_clock as u64 * 1_000 / (mode.htotal as u64 * mode.vtotal as u64);
let micros = 1_000_000_000 / millihertz;
log::info!("Refreshing at {} micros", micros);
Duration::from_micros(micros)
}
fn fp3232_to_f32(value: xinput::Fp3232) -> f32 {
value.integral as f32 + value.frac as f32 / u32::MAX as f32
}
fn check_compositor_present(xcb_connection: &XCBConnection, root: u32) -> bool {
// Method 1: Check for _NET_WM_CM_S{root}
let atom_name = format!("_NET_WM_CM_S{}", root);
let atom = xcb_connection
.intern_atom(false, atom_name.as_bytes())
.unwrap()
.reply()
.map(|reply| reply.atom)
.unwrap_or(0);
let method1 = if atom != 0 {
xcb_connection
.get_selection_owner(atom)
.unwrap()
.reply()
.map(|reply| reply.owner != 0)
.unwrap_or(false)
} else {
false
};
// Method 2: Check for _NET_WM_CM_OWNER
let atom_name = "_NET_WM_CM_OWNER";
let atom = xcb_connection
.intern_atom(false, atom_name.as_bytes())
.unwrap()
.reply()
.map(|reply| reply.atom)
.unwrap_or(0);
let method2 = if atom != 0 {
xcb_connection
.get_property(false, root, atom, xproto::AtomEnum::WINDOW, 0, 1)
.unwrap()
.reply()
.map(|reply| reply.value_len > 0)
.unwrap_or(false)
} else {
false
};
// Method 3: Check for _NET_SUPPORTING_WM_CHECK
let atom_name = "_NET_SUPPORTING_WM_CHECK";
let atom = xcb_connection
.intern_atom(false, atom_name.as_bytes())
.unwrap()
.reply()
.map(|reply| reply.atom)
.unwrap_or(0);
let method3 = if atom != 0 {
xcb_connection
.get_property(false, root, atom, xproto::AtomEnum::WINDOW, 0, 1)
.unwrap()
.reply()
.map(|reply| reply.value_len > 0)
.unwrap_or(false)
} else {
false
};
// TODO: Remove this
log::info!(
"Compositor detection: _NET_WM_CM_S?={}, _NET_WM_CM_OWNER={}, _NET_SUPPORTING_WM_CHECK={}",
method1,
method2,
method3
);
method1 || method2 || method3
}
fn check_gtk_frame_extents_supported(
xcb_connection: &XCBConnection,
atoms: &XcbAtoms,
root: xproto::Window,
) -> bool {
let supported_atoms = xcb_connection
.get_property(
false,
root,
atoms._NET_SUPPORTED,
xproto::AtomEnum::ATOM,
0,
1024,
)
.unwrap()
.reply()
.map(|reply| {
// Convert Vec<u8> to Vec<u32>
reply
.value
.chunks_exact(4)
.map(|chunk| u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
.collect::<Vec<u32>>()
})
.unwrap_or_default();
supported_atoms.contains(&atoms._GTK_FRAME_EXTENTS)
}

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