Compare commits

..

85 Commits

Author SHA1 Message Date
Thorsten Ball
804b00c12a WIP: Try to drop outdated key press events 2024-06-07 15:58:46 +02:00
Thorsten Ball
f724b2c171 DEBUG 2024-06-06 10:46:18 +02:00
Antonio Scandurra
70ce06cb95 Improve UX for saved contexts (#12721)
Release Notes:

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

- compose key
- `"`
- `a`

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

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

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

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

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

Release Notes:

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

Before:

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

After:

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

Release Notes:

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

Release Notes:

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

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

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

Before:

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

After:

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

Release Notes:

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

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

Release Notes:

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

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

Release Notes:

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

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

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

Release Notes:

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

- Improved performance when handling large responses from language
servers

---------

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

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

- N/A

---------

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

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

---------

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



Release Notes:

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

Release Notes:

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

Release Notes:

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

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

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

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

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

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

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


Release Notes:

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




---------

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


Release Notes:

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

- Fixed file drag and drop not working for terminal

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

Release Notes:

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

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

Release Notes:

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

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

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

Release Notes:

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

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

Release Notes:

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

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

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

cc @someone13574

Release notes:

- N/A

---------

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

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

Changes:

- #11482

Release Notes:

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

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

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

Before:

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

After:

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


Release Notes:

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

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

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

Release Notes:

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

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

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

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

Release Notes:

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

---------

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


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

- N/A

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

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

Release Notes:

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

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

Release Notes:

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

Release Notes:

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

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

Release Notes:

- Improved performance and reliability of completions in large
Typescript projects

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

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


Release Notes:

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

Release Notes:

- N/A

---------

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

- N/A

---------

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

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

Release Notes:

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

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

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

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

Release Notes:

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

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

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

Release Notes:

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

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

Release Notes:

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

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

Release Notes:

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

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

Release Notes:

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

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

Release Notes:

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

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

Release Notes:

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

Related to: #5166 
Release Notes:

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

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

Release Notes:

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


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

Release Notes:

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

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

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

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

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

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



Release Notes:

- N/A

Proof:

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

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

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

/cc @iamnbutler @as-cii 

Release Notes:

- N/A

---------

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

Fixes #12580
Release Notes:

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

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

Release Notes:

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

- N/A

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

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

---------

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

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

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

Release Notes:

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

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


Release Notes:

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

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

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

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

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



Release Notes:

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

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

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

```python
import unittest

class TestStringMethods(unittest.TestCase):

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

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

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

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

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

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

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

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

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

Release Notes:

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

Fixes #12544

Release Notes:

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

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


Release Notes:

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

Release Notes:

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

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


https://github.com/zed-industries/zed/assets/7737375/06f1b4cb-0364-4c30-982d-6dda3193b5d2
2024-05-31 20:49:34 +02:00
Max Brunsfeld
d12b8c3945 Simplify and improve concurrency of git status updates (#12513)
The quest for responsiveness in large git repos continues. This is a
follow-up to https://github.com/zed-industries/zed/pull/12444

Release Notes:

- N/A
2024-05-31 09:10:09 -07:00
Vitaly Slobodin
356fcec337 ruby: Add a new injection for regular expressions (#12533)
# Summary

Hello. This pull request adds a new injection to `injections.scm` for
Ruby language to highlight regular expressions. Thanks.

## Before

![CleanShot 2024-05-31 at 16 25
46@2x](https://github.com/zed-industries/zed/assets/1894248/8b88718e-8f13-4d61-b6f9-6a25b3ebcc57)

## After


![after](https://github.com/zed-industries/zed/assets/1894248/e11f6ec3-45c6-40f8-b6d9-ddbfd16a3331)

Release Notes:

- N/A

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-05-31 12:02:59 -04:00
Vitaly Slobodin
08123a270a ruby: Add proper indentation for singleton methods (#12535)
Hi. Currently, Zed uses incorrect indentation for singleton methods:

```ruby
def self.build
| # <= cursor position after hitting Enter
end
```

Handling the `singleton_method` token indentation
changes this behavior to the following:

```ruby
def self.build
  | # <= cursor position after hitting Enter
end
```

## Before


https://github.com/zed-industries/zed/assets/1894248/40fc2b37-692f-469f-9cbe-05cbb1ab4c3c

## After



https://github.com/zed-industries/zed/assets/1894248/d9ba8d27-fd17-4c74-b22c-a4de124739a3



Release Notes:

- N/A
2024-05-31 11:36:42 -04:00
张小白
6eb8e83411 docs: Update font features (#12229)
This follows up the changes in #11611 and #11898 

Release Notes:

- N/A
2024-05-31 11:34:16 -04:00
Marshall Bowers
4c51ee7816 assistant: Allow passing module paths to /rustdoc command (#12536)
This PR updates the `/rustdoc` command to accept module paths in
addition to just a crate name.

This will return the docs.rs page just for that particular module.

### Examples

```
/rustdoc bevy
/rustdoc bevy::ecs
/rustdoc bevy::ecs::component
```

<img width="641" alt="Screenshot 2024-05-31 at 11 18 25 AM"
src="https://github.com/zed-industries/zed/assets/1486634/d88af19f-5ba1-4073-8108-63cccd138db6">

<img width="641" alt="Screenshot 2024-05-31 at 11 18 35 AM"
src="https://github.com/zed-industries/zed/assets/1486634/9c414ab1-0be8-4d79-8c64-b45f19266556">


Release Notes:

- N/A
2024-05-31 11:31:22 -04:00
Vitaly Slobodin
660cf214c7 ruby: Capture the heredoc content only and downcase the language (#12532)
# Summary

Hi. Current `heredoc` injection for Ruby language captures the
`heredoc_end` token. That's a bit incorrect because we want to capture
the content only. Suppose we have the following Ruby code:

```ruby
<<~JS
  function myFunc() {
    const myConstant = [];
  }

  let a = '1'
JS
```

And this is its syntax tree:

```
[program] [0, 0] - [7, 0]
  [heredoc_beginning] [0, 0] - [0, 5]
  [heredoc_body] [0, 5] - [6, 2]
    [heredoc_content] [0, 5] - [6, 0]
    [heredoc_end] [6, 0] - [6, 2]
```

Current injection capture all content of the `heredoc_body`:

![CleanShot 2024-05-31 at 17 03
31@2x](https://github.com/zed-industries/zed/assets/1894248/ff8c5195-b532-42d2-91b1-48405a6d3b50)

But we want to capture the `heredoc_content` only and this PR resolves
that, additionally it downcases the language like Zed does in other
languages like Terraform.

![CleanShot 2024-05-31 at 17 05
17@2x](https://github.com/zed-industries/zed/assets/1894248/e81dabd0-3246-4ef2-9524-a7adcb9242ab)


Release Notes:

- N/A
2024-05-31 11:19:10 -04:00
Marshall Bowers
b2565fadfb ruby: Fix injections query location (#12534)
This PR fixes the location of the `injections.scm` query within the Ruby
extension.

Same as #12532, but without the content changes to `injections.scm`.

Release Notes:

- N/A
2024-05-31 10:42:39 -04:00
195 changed files with 11257 additions and 5284 deletions

75
Cargo.lock generated
View File

@@ -88,9 +88,8 @@ dependencies = [
[[package]]
name = "alacritty_terminal"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6d1ea4484c8676f295307a4892d478c70ac8da1dbd8c7c10830a504b7f1022f"
version = "0.24.1-dev"
source = "git+https://github.com/alacritty/alacritty?rev=cacdb5bb3b72bad2c729227537979d95af75978f#cacdb5bb3b72bad2c729227537979d95af75978f"
dependencies = [
"base64 0.22.0",
"bitflags 2.4.2",
@@ -107,7 +106,7 @@ dependencies = [
"signal-hook",
"unicode-width",
"vte",
"windows-sys 0.48.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -347,13 +346,13 @@ dependencies = [
"ctor",
"editor",
"env_logger",
"feature_flags",
"file_icons",
"fs",
"futures 0.3.28",
"fuzzy",
"gpui",
"gray_matter",
"heed",
"html_to_markdown",
"http 0.1.0",
"indoc",
"language",
@@ -368,7 +367,6 @@ dependencies = [
"rand 0.8.5",
"regex",
"rope",
"rustdoc_to_markdown",
"schemars",
"search",
"semantic_index",
@@ -2395,6 +2393,7 @@ dependencies = [
"util",
"uuid",
"workspace",
"worktree",
]
[[package]]
@@ -3393,7 +3392,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
dependencies = [
"libloading 0.7.4",
"libloading 0.8.0",
]
[[package]]
@@ -4788,18 +4787,6 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "gray_matter"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7188a951c53316d94711b3d944c28cf79968685d295cbe782494e8811fc75554"
dependencies = [
"serde",
"serde_json",
"toml 0.5.11",
"yaml-rust",
]
[[package]]
name = "grid"
version = "0.13.0"
@@ -5079,6 +5066,18 @@ dependencies = [
"syn 2.0.59",
]
[[package]]
name = "html_to_markdown"
version = "0.1.0"
dependencies = [
"anyhow",
"html5ever",
"indoc",
"markup5ever_rcdom",
"pretty_assertions",
"regex",
]
[[package]]
name = "http"
version = "0.1.0"
@@ -5954,12 +5953,6 @@ dependencies = [
"safemem",
]
[[package]]
name = "linked-hash-map"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linkify"
version = "0.10.0"
@@ -7834,6 +7827,7 @@ dependencies = [
"unicase",
"util",
"workspace",
"worktree",
]
[[package]]
@@ -8635,18 +8629,6 @@ dependencies = [
"semver",
]
[[package]]
name = "rustdoc_to_markdown"
version = "0.1.0"
dependencies = [
"anyhow",
"html5ever",
"indoc",
"markup5ever_rcdom",
"pretty_assertions",
"regex",
]
[[package]]
name = "rustix"
version = "0.37.23"
@@ -13079,15 +13061,6 @@ dependencies = [
"toml 0.8.10",
]
[[package]]
name = "yaml-rust"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
dependencies = [
"linked-hash-map",
]
[[package]]
name = "yansi"
version = "0.5.1"
@@ -13178,7 +13151,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.139.0"
version = "0.140.0"
dependencies = [
"activity_indicator",
"anyhow",
@@ -13313,7 +13286,7 @@ dependencies = [
[[package]]
name = "zed_elixir"
version = "0.0.4"
version = "0.0.5"
dependencies = [
"zed_extension_api 0.0.6",
]
@@ -13412,7 +13385,7 @@ dependencies = [
[[package]]
name = "zed_php"
version = "0.0.5"
version = "0.0.6"
dependencies = [
"zed_extension_api 0.0.4",
]
@@ -13468,7 +13441,7 @@ dependencies = [
[[package]]
name = "zed_vue"
version = "0.0.2"
version = "0.0.3"
dependencies = [
"zed_extension_api 0.0.6",
]

View File

@@ -41,6 +41,7 @@ members = [
"crates/gpui",
"crates/gpui_macros",
"crates/headless",
"crates/html_to_markdown",
"crates/http",
"crates/image_viewer",
"crates/inline_completion_button",
@@ -76,7 +77,6 @@ members = [
"crates/rich_text",
"crates/rope",
"crates/rpc",
"crates/rustdoc_to_markdown",
"crates/task",
"crates/tasks_ui",
"crates/search",
@@ -150,6 +150,7 @@ assets = { path = "crates/assets" }
assistant = { path = "crates/assistant" }
assistant_slash_command = { path = "crates/assistant_slash_command" }
assistant_tooling = { path = "crates/assistant_tooling" }
async-watch = "0.3.1"
audio = { path = "crates/audio" }
auto_update = { path = "crates/auto_update" }
base64 = "0.13"
@@ -166,6 +167,7 @@ color = { path = "crates/color" }
command_palette = { path = "crates/command_palette" }
command_palette_hooks = { path = "crates/command_palette_hooks" }
copilot = { path = "crates/copilot" }
dashmap = "5.5.3"
db = { path = "crates/db" }
diagnostics = { path = "crates/diagnostics" }
editor = { path = "crates/editor" }
@@ -185,6 +187,7 @@ google_ai = { path = "crates/google_ai" }
gpui = { path = "crates/gpui" }
gpui_macros = { path = "crates/gpui_macros" }
headless = { path = "crates/headless" }
html_to_markdown = { path = "crates/html_to_markdown" }
http = { path = "crates/http" }
install_cli = { path = "crates/install_cli" }
image_viewer = { path = "crates/image_viewer" }
@@ -221,7 +224,6 @@ dev_server_projects = { path = "crates/dev_server_projects" }
rich_text = { path = "crates/rich_text" }
rope = { path = "crates/rope" }
rpc = { path = "crates/rpc" }
rustdoc_to_markdown = { path = "crates/rustdoc_to_markdown" }
task = { path = "crates/task" }
tasks_ui = { path = "crates/tasks_ui" }
search = { path = "crates/search" }
@@ -461,6 +463,12 @@ codegen-units = 1
[profile.release.package]
zed = { codegen-units = 16 }
[profile.profiling]
inherits = "release"
debug = true
lto = false
codegen-units = 16
[workspace.lints.clippy]
dbg_macro = "deny"
todo = "deny"
@@ -494,10 +502,5 @@ non_canonical_partial_ord_impl = "allow"
reversed_empty_ranges = "allow"
type_complexity = "allow"
[workspace.lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = [
'cfg(gles)', # used in gpui
] }
[workspace.metadata.cargo-machete]
ignored = ["bindgen", "cbindgen", "prost_build", "serde"]

View File

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

View File

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

After

Width:  |  Height:  |  Size: 299 B

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

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

After

Width:  |  Height:  |  Size: 481 B

View File

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

After

Width:  |  Height:  |  Size: 1.4 KiB

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

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

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

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

After

Width:  |  Height:  |  Size: 794 B

View File

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

After

Width:  |  Height:  |  Size: 1017 B

View File

@@ -28,7 +28,6 @@
"ctrl-0": "zed::ResetBufferFontSize",
"ctrl-,": "zed::OpenSettings",
"ctrl-q": "zed::Quit",
"alt-f9": "zed::Hide",
"f11": "zed::ToggleFullScreen"
}
},
@@ -206,15 +205,10 @@
}
},
{
"context": "ConversationEditor > Editor",
"context": "PromptLibrary",
"bindings": {
"ctrl-enter": "assistant::Assist",
"ctrl-s": "workspace::Save",
"ctrl->": "assistant::QuoteSelection",
"shift-enter": "assistant::Split",
"ctrl-r": "assistant::CycleMessageRole",
"enter": "assistant::ConfirmCommand",
"alt-enter": "editor::Newline"
"ctrl-n": "prompt_library::NewPrompt",
"ctrl-shift-s": "prompt_library::ToggleDefaultPrompt"
}
},
{
@@ -226,7 +220,8 @@
"shift-enter": "search::SelectPrevMatch",
"alt-enter": "search::SelectAllMatches",
"ctrl-f": "search::FocusSearch",
"ctrl-h": "search::ToggleReplace"
"ctrl-h": "search::ToggleReplace",
"ctrl-l": "search::ToggleSelection"
}
},
{
@@ -290,6 +285,7 @@
"ctrl-alt-g": "search::SelectNextMatch",
"ctrl-alt-shift-g": "search::SelectPrevMatch",
"ctrl-alt-shift-h": "search::ToggleReplace",
"ctrl-alt-shift-l": "search::ToggleSelection",
"alt-enter": "search::SelectAllMatches",
"alt-c": "search::ToggleCaseSensitive",
"alt-w": "search::ToggleWholeWord",
@@ -548,6 +544,18 @@
"ctrl-enter": "assistant::InlineAssist"
}
},
{
"context": "ConversationEditor > Editor",
"bindings": {
"ctrl-enter": "assistant::Assist",
"ctrl-s": "workspace::Save",
"ctrl->": "assistant::QuoteSelection",
"shift-enter": "assistant::Split",
"ctrl-r": "assistant::CycleMessageRole",
"enter": "assistant::ConfirmCommand",
"alt-enter": "editor::Newline"
}
},
{
"context": "ProjectSearchBar && !in_replace",
"bindings": {
@@ -636,12 +644,7 @@
"pagedown": ["terminal::SendKeystroke", "pagedown"],
"escape": ["terminal::SendKeystroke", "escape"],
"enter": ["terminal::SendKeystroke", "enter"],
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
// Some nice conveniences
"ctrl-backspace": ["terminal::SendText", "\u0015"],
"ctrl-right": ["terminal::SendText", "\u0005"],
"ctrl-left": ["terminal::SendText", "\u0001"]
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"]
}
}
]

View File

@@ -176,6 +176,12 @@
"replace_enabled": true
}
],
"cmd-alt-l": [
"buffer_search::Deploy",
{
"selection_search_enabled": true
}
],
"cmd-e": [
"buffer_search::Deploy",
{
@@ -233,6 +239,14 @@
"alt-enter": "editor::Newline"
}
},
{
"context": "PromptLibrary",
"bindings": {
"cmd-n": "prompt_library::NewPrompt",
"cmd-shift-s": "prompt_library::ToggleDefaultPrompt",
"cmd-w": "workspace::CloseWindow"
}
},
{
"context": "BufferSearchBar",
"bindings": {
@@ -242,7 +256,8 @@
"shift-enter": "search::SelectPrevMatch",
"alt-enter": "search::SelectAllMatches",
"cmd-f": "search::FocusSearch",
"cmd-alt-f": "search::ToggleReplace"
"cmd-alt-f": "search::ToggleReplace",
"cmd-alt-l": "search::ToggleSelection"
}
},
{
@@ -308,6 +323,7 @@
"cmd-g": "search::SelectNextMatch",
"cmd-shift-g": "search::SelectPrevMatch",
"cmd-shift-h": "search::ToggleReplace",
"cmd-alt-l": "search::ToggleSelection",
"alt-enter": "search::SelectAllMatches",
"alt-cmd-c": "search::ToggleCaseSensitive",
"alt-cmd-w": "search::ToggleWholeWord",
@@ -639,7 +655,7 @@
{
"context": "Picker",
"bindings": {
"alt-e": "picker::UseSelectedQuery",
"f2": "picker::UseSelectedQuery",
"alt-enter": ["picker::ConfirmInput", { "secondary": false }],
"cmd-alt-enter": ["picker::ConfirmInput", { "secondary": true }]
}

View File

@@ -379,8 +379,8 @@
"r": ["vim::PushOperator", "Replace"],
"s": "vim::Substitute",
"shift-s": "vim::SubstituteLine",
"> >": "vim::Indent",
"< <": "vim::Outdent",
">": ["vim::PushOperator", "Indent"],
"<": ["vim::PushOperator", "Outdent"],
"ctrl-pagedown": "pane::ActivateNextItem",
"ctrl-pageup": "pane::ActivatePrevItem",
// tree-sitter related commands
@@ -459,6 +459,18 @@
"s": "vim::CurrentLine"
}
},
{
"context": "Editor && vim_operator == >",
"bindings": {
">": "vim::CurrentLine"
}
},
{
"context": "Editor && vim_operator == <",
"bindings": {
"<": "vim::CurrentLine"
}
},
{
"context": "Editor && VimObject",
"bindings": {
@@ -568,7 +580,7 @@
}
},
{
"context": "Editor && vim_mode == normal",
"context": "Editor && vim_mode == normal && !VimWaiting",
"bindings": {
"g c c": "editor::ToggleComments"
}

View File

@@ -131,14 +131,7 @@
// The default number of lines to expand excerpts in the multibuffer by.
"expand_excerpt_lines": 3,
// Globs to match against file paths to determine if a file is private.
"private_files": [
"**/.env*",
"**/*.pem",
"**/*.key",
"**/*.cert",
"**/*.crt",
"**/secrets.yml"
],
"private_files": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"],
// Whether to use additional LSP queries to format (and amend) the code after
// every "trigger" symbol input, defined by LSP server capabilities.
"use_on_type_format": true,
@@ -164,6 +157,12 @@
// "none"
// 3. Draw all invisible symbols:
// "all"
// 4. Draw whitespaces at boundaries only:
// "boundaries"
// For a whitespace to be on a boundary, any of the following conditions need to be met:
// - It is a tab
// - It is adjacent to an edge (start or end)
// - It is adjacent to a whitespace (left or right)
"show_whitespaces": "selection",
// Settings related to calls in Zed
"calls": {
@@ -453,7 +452,8 @@
// Send anonymized usage data like what languages you're using Zed with.
"metrics": true
},
// Automatically update Zed
// Automatically update Zed. This setting may be ignored on Linux if
// installed through a package manager.
"auto_update": true,
// Diagnostics configuration.
"diagnostics": {
@@ -672,9 +672,6 @@
"Elixir": {
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
},
"Gleam": {
"tab_size": 2
},
"Go": {
"code_actions_on_format": {
"source.organizeImports": true
@@ -700,6 +697,7 @@
}
},
"JavaScript": {
"language_servers": ["typescript-language-server", "!vtsls", "..."],
"prettier": {
"allowed": true
}
@@ -709,9 +707,6 @@
"allowed": true
}
},
"Make": {
"hard_tabs": true
},
"Markdown": {
"format_on_save": "off",
"prettier": {
@@ -724,9 +719,6 @@
"plugins": ["@prettier/plugin-php"]
}
},
"Prisma": {
"tab_size": 2
},
"Ruby": {
"language_servers": ["solargraph", "!ruby-lsp", "..."]
},
@@ -748,6 +740,7 @@
}
},
"TSX": {
"language_servers": ["typescript-language-server", "!vtsls", "..."],
"prettier": {
"allowed": true
}
@@ -758,6 +751,7 @@
}
},
"TypeScript": {
"language_servers": ["typescript-language-server", "!vtsls", "..."],
"prettier": {
"allowed": true
}

View File

@@ -62,16 +62,16 @@ impl ActivityIndicator {
this.update(&mut cx, |this, cx| {
this.statuses.retain(|s| s.name != name);
this.statuses.push(LspStatus { name, status });
cx.notify();
cx.notify(); // commented back in
})?;
}
anyhow::Ok(())
})
.detach();
cx.observe(&project, |_, _, cx| cx.notify()).detach();
cx.observe(&project, |_, _, cx| cx.notify()).detach(); // commented back in
if let Some(auto_updater) = auto_updater.as_ref() {
cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
cx.observe(auto_updater, |_, _, cx| cx.notify()).detach(); // commented back in
}
Self {

View File

@@ -22,12 +22,13 @@ client.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
editor.workspace = true
feature_flags.workspace = true
file_icons.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
heed.workspace = true
html_to_markdown.workspace = true
http.workspace = true
indoc.workspace = true
language.workspace = true
@@ -40,7 +41,6 @@ parking_lot.workspace = true
project.workspace = true
regex.workspace = true
rope.workspace = true
rustdoc_to_markdown.workspace = true
schemars.workspace = true
search.workspace = true
semantic_index.workspace = true
@@ -59,7 +59,6 @@ util.workspace = true
uuid.workspace = true
workspace.workspace = true
picker.workspace = true
gray_matter = "0.2.7"
[dev-dependencies]
ctor.workspace = true

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -14,10 +14,10 @@ use serde::{
use settings::{Settings, SettingsSources};
use strum::{EnumIter, IntoEnumIterator};
use crate::LanguageModel;
use crate::{preprocess_anthropic_request, LanguageModel, LanguageModelRequest};
#[derive(Clone, Debug, Default, PartialEq, EnumIter)]
pub enum ZedDotDevModel {
pub enum CloudModel {
Gpt3Point5Turbo,
Gpt4,
Gpt4Turbo,
@@ -29,7 +29,7 @@ pub enum ZedDotDevModel {
Custom(String),
}
impl Serialize for ZedDotDevModel {
impl Serialize for CloudModel {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
@@ -38,7 +38,7 @@ impl Serialize for ZedDotDevModel {
}
}
impl<'de> Deserialize<'de> for ZedDotDevModel {
impl<'de> Deserialize<'de> for CloudModel {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
@@ -46,7 +46,7 @@ impl<'de> Deserialize<'de> for ZedDotDevModel {
struct ZedDotDevModelVisitor;
impl<'de> Visitor<'de> for ZedDotDevModelVisitor {
type Value = ZedDotDevModel;
type Value = CloudModel;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a string for a ZedDotDevModel variant or a custom model")
@@ -56,9 +56,9 @@ impl<'de> Deserialize<'de> for ZedDotDevModel {
where
E: de::Error,
{
let model = ZedDotDevModel::iter()
let model = CloudModel::iter()
.find(|model| model.id() == value)
.unwrap_or_else(|| ZedDotDevModel::Custom(value.to_string()));
.unwrap_or_else(|| CloudModel::Custom(value.to_string()));
Ok(model)
}
}
@@ -67,13 +67,13 @@ impl<'de> Deserialize<'de> for ZedDotDevModel {
}
}
impl JsonSchema for ZedDotDevModel {
impl JsonSchema for CloudModel {
fn schema_name() -> String {
"ZedDotDevModel".to_owned()
}
fn json_schema(_generator: &mut schemars::gen::SchemaGenerator) -> Schema {
let variants = ZedDotDevModel::iter()
let variants = CloudModel::iter()
.filter_map(|model| {
let id = model.id();
if id.is_empty() {
@@ -88,7 +88,7 @@ impl JsonSchema for ZedDotDevModel {
enum_values: Some(variants.iter().map(|s| s.clone().into()).collect()),
metadata: Some(Box::new(Metadata {
title: Some("ZedDotDevModel".to_owned()),
default: Some(ZedDotDevModel::default().id().into()),
default: Some(CloudModel::default().id().into()),
examples: variants.into_iter().map(Into::into).collect(),
..Default::default()
})),
@@ -97,7 +97,7 @@ impl JsonSchema for ZedDotDevModel {
}
}
impl ZedDotDevModel {
impl CloudModel {
pub fn id(&self) -> &str {
match self {
Self::Gpt3Point5Turbo => "gpt-3.5-turbo",
@@ -133,6 +133,15 @@ impl ZedDotDevModel {
Self::Custom(_) => 4096, // TODO: Make this configurable
}
}
pub fn preprocess_request(&self, request: &mut LanguageModelRequest) {
match self {
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3Haiku => {
preprocess_anthropic_request(request)
}
_ => {}
}
}
}
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
@@ -147,7 +156,7 @@ pub enum AssistantDockPosition {
#[derive(Debug, PartialEq)]
pub enum AssistantProvider {
ZedDotDev {
model: ZedDotDevModel,
model: CloudModel,
},
OpenAi {
model: OpenAiModel,
@@ -175,9 +184,7 @@ impl Default for AssistantProvider {
#[serde(tag = "name", rename_all = "snake_case")]
pub enum AssistantProviderContent {
#[serde(rename = "zed.dev")]
ZedDotDev {
default_model: Option<ZedDotDevModel>,
},
ZedDotDev { default_model: Option<CloudModel> },
#[serde(rename = "openai")]
OpenAi {
default_model: Option<OpenAiModel>,
@@ -281,7 +288,7 @@ impl AssistantSettingsContent {
Some(AssistantProviderContent::ZedDotDev {
default_model: model,
}) => {
if let LanguageModel::ZedDotDev(new_model) = new_model {
if let LanguageModel::Cloud(new_model) = new_model {
*model = Some(new_model);
}
}
@@ -302,7 +309,7 @@ impl AssistantSettingsContent {
}
}
provider => match new_model {
LanguageModel::ZedDotDev(model) => {
LanguageModel::Cloud(model) => {
*provider = Some(AssistantProviderContent::ZedDotDev {
default_model: Some(model),
})
@@ -613,7 +620,7 @@ mod tests {
assert_eq!(
AssistantSettings::get_global(cx).provider,
AssistantProvider::ZedDotDev {
model: ZedDotDevModel::Custom("custom".into())
model: CloudModel::Custom("custom".into())
}
);
}

View File

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

View File

@@ -1,14 +1,14 @@
mod anthropic;
mod cloud;
#[cfg(test)]
mod fake;
mod open_ai;
mod zed;
pub use anthropic::*;
pub use cloud::*;
#[cfg(test)]
pub use fake::*;
pub use open_ai::*;
pub use zed::*;
use crate::{
assistant_settings::{AssistantProvider, AssistantSettings},
@@ -25,8 +25,8 @@ use std::time::Duration;
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
let mut settings_version = 0;
let provider = match &AssistantSettings::get_global(cx).provider {
AssistantProvider::ZedDotDev { model } => CompletionProvider::ZedDotDev(
ZedDotDevCompletionProvider::new(model.clone(), client.clone(), settings_version, cx),
AssistantProvider::ZedDotDev { model } => CompletionProvider::Cloud(
CloudCompletionProvider::new(model.clone(), client.clone(), settings_version, cx),
),
AssistantProvider::OpenAi {
model,
@@ -87,14 +87,11 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
settings_version,
);
}
(
CompletionProvider::ZedDotDev(provider),
AssistantProvider::ZedDotDev { model },
) => {
(CompletionProvider::Cloud(provider), AssistantProvider::ZedDotDev { model }) => {
provider.update(model.clone(), settings_version);
}
(_, AssistantProvider::ZedDotDev { model }) => {
*provider = CompletionProvider::ZedDotDev(ZedDotDevCompletionProvider::new(
*provider = CompletionProvider::Cloud(CloudCompletionProvider::new(
model.clone(),
client.clone(),
settings_version,
@@ -142,7 +139,7 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
pub enum CompletionProvider {
OpenAi(OpenAiCompletionProvider),
Anthropic(AnthropicCompletionProvider),
ZedDotDev(ZedDotDevCompletionProvider),
Cloud(CloudCompletionProvider),
#[cfg(test)]
Fake(FakeCompletionProvider),
}
@@ -164,9 +161,9 @@ impl CompletionProvider {
.available_models()
.map(LanguageModel::Anthropic)
.collect(),
CompletionProvider::ZedDotDev(provider) => provider
CompletionProvider::Cloud(provider) => provider
.available_models()
.map(LanguageModel::ZedDotDev)
.map(LanguageModel::Cloud)
.collect(),
#[cfg(test)]
CompletionProvider::Fake(_) => unimplemented!(),
@@ -177,7 +174,7 @@ impl CompletionProvider {
match self {
CompletionProvider::OpenAi(provider) => provider.settings_version(),
CompletionProvider::Anthropic(provider) => provider.settings_version(),
CompletionProvider::ZedDotDev(provider) => provider.settings_version(),
CompletionProvider::Cloud(provider) => provider.settings_version(),
#[cfg(test)]
CompletionProvider::Fake(_) => unimplemented!(),
}
@@ -187,7 +184,7 @@ impl CompletionProvider {
match self {
CompletionProvider::OpenAi(provider) => provider.is_authenticated(),
CompletionProvider::Anthropic(provider) => provider.is_authenticated(),
CompletionProvider::ZedDotDev(provider) => provider.is_authenticated(),
CompletionProvider::Cloud(provider) => provider.is_authenticated(),
#[cfg(test)]
CompletionProvider::Fake(_) => true,
}
@@ -197,7 +194,7 @@ impl CompletionProvider {
match self {
CompletionProvider::OpenAi(provider) => provider.authenticate(cx),
CompletionProvider::Anthropic(provider) => provider.authenticate(cx),
CompletionProvider::ZedDotDev(provider) => provider.authenticate(cx),
CompletionProvider::Cloud(provider) => provider.authenticate(cx),
#[cfg(test)]
CompletionProvider::Fake(_) => Task::ready(Ok(())),
}
@@ -207,7 +204,7 @@ impl CompletionProvider {
match self {
CompletionProvider::OpenAi(provider) => provider.authentication_prompt(cx),
CompletionProvider::Anthropic(provider) => provider.authentication_prompt(cx),
CompletionProvider::ZedDotDev(provider) => provider.authentication_prompt(cx),
CompletionProvider::Cloud(provider) => provider.authentication_prompt(cx),
#[cfg(test)]
CompletionProvider::Fake(_) => unimplemented!(),
}
@@ -217,7 +214,7 @@ impl CompletionProvider {
match self {
CompletionProvider::OpenAi(provider) => provider.reset_credentials(cx),
CompletionProvider::Anthropic(provider) => provider.reset_credentials(cx),
CompletionProvider::ZedDotDev(_) => Task::ready(Ok(())),
CompletionProvider::Cloud(_) => Task::ready(Ok(())),
#[cfg(test)]
CompletionProvider::Fake(_) => Task::ready(Ok(())),
}
@@ -227,7 +224,7 @@ impl CompletionProvider {
match self {
CompletionProvider::OpenAi(provider) => LanguageModel::OpenAi(provider.model()),
CompletionProvider::Anthropic(provider) => LanguageModel::Anthropic(provider.model()),
CompletionProvider::ZedDotDev(provider) => LanguageModel::ZedDotDev(provider.model()),
CompletionProvider::Cloud(provider) => LanguageModel::Cloud(provider.model()),
#[cfg(test)]
CompletionProvider::Fake(_) => LanguageModel::default(),
}
@@ -241,7 +238,7 @@ impl CompletionProvider {
match self {
CompletionProvider::OpenAi(provider) => provider.count_tokens(request, cx),
CompletionProvider::Anthropic(provider) => provider.count_tokens(request, cx),
CompletionProvider::ZedDotDev(provider) => provider.count_tokens(request, cx),
CompletionProvider::Cloud(provider) => provider.count_tokens(request, cx),
#[cfg(test)]
CompletionProvider::Fake(_) => futures::FutureExt::boxed(futures::future::ready(Ok(0))),
}
@@ -254,7 +251,7 @@ impl CompletionProvider {
match self {
CompletionProvider::OpenAi(provider) => provider.complete(request),
CompletionProvider::Anthropic(provider) => provider.complete(request),
CompletionProvider::ZedDotDev(provider) => provider.complete(request),
CompletionProvider::Cloud(provider) => provider.complete(request),
#[cfg(test)]
CompletionProvider::Fake(provider) => provider.complete(),
}

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
use crate::assistant_settings::ZedDotDevModel;
use crate::assistant_settings::CloudModel;
use crate::{
assistant_settings::OpenAiModel, CompletionProvider, LanguageModel, LanguageModelRequest, Role,
};
@@ -210,9 +210,9 @@ pub fn count_open_ai_tokens(
match request.model {
LanguageModel::Anthropic(_)
| LanguageModel::ZedDotDev(ZedDotDevModel::Claude3Opus)
| LanguageModel::ZedDotDev(ZedDotDevModel::Claude3Sonnet)
| LanguageModel::ZedDotDev(ZedDotDevModel::Claude3Haiku) => {
| LanguageModel::Cloud(CloudModel::Claude3Opus)
| LanguageModel::Cloud(CloudModel::Claude3Sonnet)
| LanguageModel::Cloud(CloudModel::Claude3Haiku) => {
// Tiktoken doesn't yet support these models, so we manually use the
// same tokenizer as GPT-4.
tiktoken_rs::num_tokens_from_messages("gpt-4", &messages)

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,6 +17,8 @@ use std::{
use workspace::Workspace;
pub mod active_command;
pub mod default_command;
pub mod fetch_command;
pub mod file_command;
pub mod project_command;
pub mod prompt_command;
@@ -25,10 +27,10 @@ pub mod search_command;
pub mod tabs_command;
pub(crate) struct SlashCommandCompletionProvider {
editor: WeakView<ConversationEditor>,
commands: Arc<SlashCommandRegistry>,
cancel_flag: Mutex<Arc<AtomicBool>>,
workspace: WeakView<Workspace>,
editor: Option<WeakView<ConversationEditor>>,
workspace: Option<WeakView<Workspace>>,
}
pub(crate) struct SlashCommandLine {
@@ -40,9 +42,9 @@ pub(crate) struct SlashCommandLine {
impl SlashCommandCompletionProvider {
pub fn new(
editor: WeakView<ConversationEditor>,
commands: Arc<SlashCommandRegistry>,
workspace: WeakView<Workspace>,
editor: Option<WeakView<ConversationEditor>>,
workspace: Option<WeakView<Workspace>>,
) -> Self {
Self {
cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
@@ -96,6 +98,30 @@ impl SlashCommandCompletionProvider {
new_text.push(' ');
}
let confirm = editor.clone().zip(workspace.clone()).and_then(
|(editor, workspace)| {
(!requires_argument).then(|| {
let command_name = mat.string.clone();
let command_range = command_range.clone();
let editor = editor.clone();
let workspace = workspace.clone();
Arc::new(move |cx: &mut WindowContext| {
editor
.update(cx, |editor, cx| {
editor.run_command(
command_range.clone(),
&command_name,
None,
true,
workspace.clone(),
cx,
);
})
.ok();
}) as Arc<_>
})
},
);
Some(project::Completion {
old_range: name_range.clone(),
documentation: Some(Documentation::SingleLine(command.description())),
@@ -104,25 +130,7 @@ impl SlashCommandCompletionProvider {
server_id: LanguageServerId(0),
lsp_completion: Default::default(),
show_new_completions_on_confirm: requires_argument,
confirm: (!requires_argument).then(|| {
let command_name = mat.string.clone();
let command_range = command_range.clone();
let editor = editor.clone();
let workspace = workspace.clone();
Arc::new(move |cx: &mut WindowContext| {
editor
.update(cx, |editor, cx| {
editor.run_command(
command_range.clone(),
&command_name,
None,
workspace.clone(),
cx,
);
})
.ok();
}) as Arc<_>
}),
confirm,
})
})
.collect()
@@ -157,33 +165,42 @@ impl SlashCommandCompletionProvider {
Ok(completions
.await?
.into_iter()
.map(|arg| project::Completion {
old_range: argument_range.clone(),
label: CodeLabel::plain(arg.clone(), None),
new_text: arg.clone(),
documentation: None,
server_id: LanguageServerId(0),
lsp_completion: Default::default(),
show_new_completions_on_confirm: false,
confirm: Some(Arc::new({
let command_name = command_name.clone();
let command_range = command_range.clone();
let editor = editor.clone();
let workspace = workspace.clone();
move |cx| {
editor
.update(cx, |editor, cx| {
editor.run_command(
command_range.clone(),
&command_name,
Some(&arg),
workspace.clone(),
cx,
);
})
.ok();
}
})),
.map(|command_argument| {
let confirm =
editor
.clone()
.zip(workspace.clone())
.map(|(editor, workspace)| {
Arc::new({
let command_range = command_range.clone();
let command_name = command_name.clone();
let command_argument = command_argument.clone();
move |cx: &mut WindowContext| {
editor
.update(cx, |editor, cx| {
editor.run_command(
command_range.clone(),
&command_name,
Some(&command_argument),
true,
workspace.clone(),
cx,
);
})
.ok();
}
}) as Arc<_>
});
project::Completion {
old_range: argument_range.clone(),
label: CodeLabel::plain(command_argument.clone(), None),
new_text: command_argument.clone(),
documentation: None,
server_id: LanguageServerId(0),
lsp_completion: Default::default(),
show_new_completions_on_confirm: false,
confirm,
}
})
.collect())
})

View File

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

View File

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

View File

@@ -0,0 +1,134 @@
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use anyhow::{anyhow, bail, Context, Result};
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
use futures::AsyncReadExt;
use gpui::{AppContext, Task, WeakView};
use html_to_markdown::convert_html_to_markdown;
use http::{AsyncBody, HttpClient, HttpClientWithUrl};
use language::LspAdapterDelegate;
use ui::{prelude::*, ButtonLike, ElevationIndex};
use workspace::Workspace;
pub(crate) struct FetchSlashCommand;
impl FetchSlashCommand {
async fn build_message(http_client: Arc<HttpClientWithUrl>, url: &str) -> Result<String> {
let mut url = url.to_owned();
if !url.starts_with("https://") {
url = format!("https://{url}");
}
let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
let mut body = Vec::new();
response
.body_mut()
.read_to_end(&mut body)
.await
.context("error reading response body")?;
if response.status().is_client_error() {
let text = String::from_utf8_lossy(body.as_slice());
bail!(
"status error {}, response: {text:?}",
response.status().as_u16()
);
}
convert_html_to_markdown(&body[..])
}
}
impl SlashCommand for FetchSlashCommand {
fn name(&self) -> String {
"fetch".into()
}
fn description(&self) -> String {
"insert URL contents".into()
}
fn menu_text(&self) -> String {
"Insert fetched URL contents".into()
}
fn requires_argument(&self) -> bool {
true
}
fn complete_argument(
&self,
_query: String,
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
Task::ready(Ok(Vec::new()))
}
fn run(
self: Arc<Self>,
argument: Option<&str>,
workspace: WeakView<Workspace>,
_delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let Some(argument) = argument else {
return Task::ready(Err(anyhow!("missing URL")));
};
let Some(workspace) = workspace.upgrade() else {
return Task::ready(Err(anyhow!("workspace was dropped")));
};
let http_client = workspace.read(cx).client().http_client();
let url = argument.to_string();
let text = cx.background_executor().spawn({
let url = url.clone();
async move { Self::build_message(http_client, &url).await }
});
let url = SharedString::from(url);
cx.foreground_executor().spawn(async move {
let text = text.await?;
let range = 0..text.len();
Ok(SlashCommandOutput {
text,
sections: vec![SlashCommandOutputSection {
range,
render_placeholder: Arc::new(move |id, unfold, _cx| {
FetchPlaceholder {
id,
unfold,
url: url.clone(),
}
.into_any_element()
}),
}],
run_commands_in_text: false,
})
})
}
}
#[derive(IntoElement)]
struct FetchPlaceholder {
pub id: ElementId,
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
pub url: SharedString,
}
impl RenderOnce for FetchPlaceholder {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
let unfold = self.unfold;
ButtonLike::new(self.id)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ElevatedSurface)
.child(Icon::new(IconName::AtSign))
.child(Label::new(format!("fetch {url}", url = self.url)))
.on_click(move |_, cx| unfold(cx))
}
}

View File

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

View File

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

View File

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

View File

@@ -1,26 +1,63 @@
use std::path::Path;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use anyhow::{anyhow, bail, Context, Result};
use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection};
use fs::Fs;
use futures::AsyncReadExt;
use gpui::{AppContext, Task, WeakView};
use gpui::{AppContext, Model, Task, WeakView};
use html_to_markdown::convert_rustdoc_to_markdown;
use http::{AsyncBody, HttpClient, HttpClientWithUrl};
use language::LspAdapterDelegate;
use rustdoc_to_markdown::convert_rustdoc_to_markdown;
use project::{Project, ProjectPath};
use ui::{prelude::*, ButtonLike, ElevationIndex};
use workspace::Workspace;
#[derive(Debug, Clone, Copy)]
enum RustdocSource {
/// The docs were sourced from local `cargo doc` output.
Local,
/// The docs were sourced from `docs.rs`.
DocsDotRs,
}
pub(crate) struct RustdocSlashCommand;
impl RustdocSlashCommand {
async fn build_message(
fs: Arc<dyn Fs>,
http_client: Arc<HttpClientWithUrl>,
crate_name: String,
) -> Result<String> {
module_path: Vec<String>,
path_to_cargo_toml: Option<&Path>,
) -> Result<(RustdocSource, String)> {
let cargo_workspace_root = path_to_cargo_toml.and_then(|path| path.parent());
if let Some(cargo_workspace_root) = cargo_workspace_root {
let mut local_cargo_doc_path = cargo_workspace_root.join("target/doc");
local_cargo_doc_path.push(&crate_name);
if !module_path.is_empty() {
local_cargo_doc_path.push(module_path.join("/"));
}
local_cargo_doc_path.push("index.html");
if let Ok(contents) = fs.load(&local_cargo_doc_path).await {
return Ok((
RustdocSource::Local,
convert_rustdoc_to_markdown(contents.as_bytes())?,
));
}
}
let version = "latest";
let path = format!(
"{crate_name}/{version}/{crate_name}/{module_path}",
module_path = module_path.join("/")
);
let mut response = http_client
.get(
&format!("https://docs.rs/{crate_name}"),
&format!("https://docs.rs/{path}"),
AsyncBody::default(),
true,
)
@@ -41,7 +78,23 @@ impl RustdocSlashCommand {
);
}
convert_rustdoc_to_markdown(&body[..])
Ok((
RustdocSource::DocsDotRs,
convert_rustdoc_to_markdown(&body[..])?,
))
}
fn path_to_cargo_toml(project: Model<Project>, cx: &mut AppContext) -> Option<Arc<Path>> {
let worktree = project.read(cx).worktrees().next()?;
let worktree = worktree.read(cx);
let entry = worktree.entry_for_path("Cargo.toml")?;
let path = ProjectPath {
worktree_id: worktree.id(),
path: entry.path.clone(),
};
Some(Arc::from(
project.read(cx).absolute_path(&path, cx)?.as_path(),
))
}
}
@@ -66,7 +119,7 @@ impl SlashCommand for RustdocSlashCommand {
&self,
_query: String,
_cancel: Arc<AtomicBool>,
_workspace: WeakView<Workspace>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
Task::ready(Ok(Vec::new()))
@@ -86,17 +139,43 @@ impl SlashCommand for RustdocSlashCommand {
return Task::ready(Err(anyhow!("workspace was dropped")));
};
let project = workspace.read(cx).project().clone();
let fs = project.read(cx).fs().clone();
let http_client = workspace.read(cx).client().http_client();
let crate_name = argument.to_string();
let mut path_components = argument.split("::");
let crate_name = match path_components
.next()
.ok_or_else(|| anyhow!("missing crate name"))
{
Ok(crate_name) => crate_name.to_string(),
Err(err) => return Task::ready(Err(err)),
};
let module_path = path_components.map(ToString::to_string).collect::<Vec<_>>();
let path_to_cargo_toml = Self::path_to_cargo_toml(project, cx);
let text = cx.background_executor().spawn({
let crate_name = crate_name.clone();
async move { Self::build_message(http_client, crate_name).await }
let module_path = module_path.clone();
async move {
Self::build_message(
fs,
http_client,
crate_name,
module_path,
path_to_cargo_toml.as_deref(),
)
.await
}
});
let crate_name = SharedString::from(crate_name);
let module_path = if module_path.is_empty() {
None
} else {
Some(SharedString::from(module_path.join("::")))
};
cx.foreground_executor().spawn(async move {
let text = text.await?;
let (source, text) = text.await?;
let range = 0..text.len();
Ok(SlashCommandOutput {
text,
@@ -106,11 +185,14 @@ impl SlashCommand for RustdocSlashCommand {
RustdocPlaceholder {
id,
unfold,
source,
crate_name: crate_name.clone(),
module_path: module_path.clone(),
}
.into_any_element()
}),
}],
run_commands_in_text: false,
})
})
}
@@ -120,18 +202,31 @@ impl SlashCommand for RustdocSlashCommand {
struct RustdocPlaceholder {
pub id: ElementId,
pub unfold: Arc<dyn Fn(&mut WindowContext)>,
pub source: RustdocSource,
pub crate_name: SharedString,
pub module_path: Option<SharedString>,
}
impl RenderOnce for RustdocPlaceholder {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
let unfold = self.unfold;
let crate_path = self
.module_path
.map(|module_path| format!("{crate_name}::{module_path}", crate_name = self.crate_name))
.unwrap_or(self.crate_name.to_string());
ButtonLike::new(self.id)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ElevatedSurface)
.child(Icon::new(IconName::FileRust))
.child(Label::new(format!("rustdoc: {}", self.crate_name)))
.child(Label::new(format!(
"rustdoc ({source}): {crate_path}",
source = match self.source {
RustdocSource::Local => "local",
RustdocSource::DocsDotRs => "docs.rs",
}
)))
.on_click(move |_, cx| unfold(cx))
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -191,14 +191,15 @@ mod linux {
let cli = env::current_exe()?;
let dir = cli
.parent()
.and_then(Path::parent)
.ok_or_else(|| anyhow!("no parent path for cli"))?;
match dir.join("zed").canonicalize() {
match dir.join("libexec").join("zed-editor").canonicalize() {
Ok(path) => Ok(path),
// development builds have Zed capitalized
Err(e) => match dir.join("Zed").canonicalize() {
Ok(path) => Ok(path),
Err(_) => Err(e),
// In development cli and zed are in the ./target/ directory together
Err(e) => match cli.parent().unwrap().join("zed").canonicalize() {
Ok(path) if path != cli => Ok(path),
_ => Err(e),
},
}
}?;
@@ -254,10 +255,8 @@ mod linux {
eprintln!("failed to setsid: {}", std::io::Error::last_os_error());
process::exit(1);
}
if std::env::var("ZED_KEEP_FD").is_err() {
if let Err(_) = fork::close_fd() {
eprintln!("failed to close_fd: {}", std::io::Error::last_os_error());
}
if let Err(_) = fork::close_fd() {
eprintln!("failed to close_fd: {}", std::io::Error::last_os_error());
}
let error =
exec::execvp(path.clone(), &[path.as_os_str(), &OsString::from(ipc_url)]);
@@ -315,7 +314,7 @@ mod flatpak {
if let Some(flatpak_dir) = get_flatpak_dir() {
let mut args = vec!["/usr/bin/flatpak-spawn".into(), "--host".into()];
args.append(&mut get_xdg_env_args());
args.push("--env=ZED_IS_FLATPAK_INSTALL=1".into());
args.push("--env=ZED_UPDATE_EXPLANATION=Please use flatpak to update zed".into());
args.push(
format!(
"--env={EXTRA_LIB_ENV_NAME}={}",
@@ -333,7 +332,7 @@ mod flatpak {
if !is_app_location_set {
args.push("--zed".into());
args.push(flatpak_dir.join("bin").join("zed-app").into());
args.push(flatpak_dir.join("libexec").join("zed-editor").into());
}
let error = exec::execvp("/usr/bin/flatpak-spawn", args);
@@ -347,8 +346,8 @@ mod flatpak {
&& env::var("FLATPAK_ID").map_or(false, |id| id.starts_with("dev.zed.Zed"))
{
if args.zed.is_none() {
args.zed = Some("/app/bin/zed-app".into());
env::set_var("ZED_IS_FLATPAK_INSTALL", "1");
args.zed = Some("/app/libexec/zed-editor".into());
env::set_var("ZED_UPDATE_EXPLANATION", "Please use flatpak to update zed");
}
}
args

View File

@@ -107,4 +107,5 @@ theme.workspace = true
unindent.workspace = true
util.workspace = true
workspace = { workspace = true, features = ["test-support"] }
worktree = { workspace = true, features = ["test-support"] }
headless.workspace = true

View File

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

View File

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

View File

@@ -3022,7 +3022,6 @@ async fn test_fs_operations(
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
let worktree_a = project_a.read_with(cx_a, |project, _| project.worktrees().next().unwrap());
let worktree_b = project_b.read_with(cx_b, |project, _| project.worktrees().next().unwrap());
let entry = project_b
@@ -3031,6 +3030,7 @@ async fn test_fs_operations(
})
.await
.unwrap()
.to_included()
.unwrap();
worktree_a.read_with(cx_a, |worktree, _| {
@@ -3059,6 +3059,7 @@ async fn test_fs_operations(
})
.await
.unwrap()
.to_included()
.unwrap();
worktree_a.read_with(cx_a, |worktree, _| {
@@ -3087,6 +3088,7 @@ async fn test_fs_operations(
})
.await
.unwrap()
.to_included()
.unwrap();
worktree_a.read_with(cx_a, |worktree, _| {
@@ -3115,20 +3117,25 @@ async fn test_fs_operations(
})
.await
.unwrap()
.to_included()
.unwrap();
project_b
.update(cx_b, |project, cx| {
project.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
})
.await
.unwrap()
.to_included()
.unwrap();
project_b
.update(cx_b, |project, cx| {
project.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
})
.await
.unwrap()
.to_included()
.unwrap();
worktree_a.read_with(cx_a, |worktree, _| {

View File

@@ -5,7 +5,7 @@ use client::{proto::PeerId, Client, User, UserStore};
use gpui::{
actions, canvas, div, point, px, Action, AnyElement, AppContext, Element, Hsla,
InteractiveElement, IntoElement, Model, ParentElement, Path, Render,
StatefulInteractiveElement, Styled, Subscription, View, ViewContext, VisualContext, WeakView,
StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext, WeakView,
};
use project::{Project, RepositoryEntry};
use recent_projects::RecentProjects;
@@ -17,7 +17,7 @@ use ui::{
ButtonStyle, ContextMenu, Icon, IconButton, IconName, Indicator, TintColor, TitleBar, Tooltip,
};
use util::ResultExt;
use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
use vcs_menu::{BranchList, OpenRecent as ToggleVcsMenu};
use workspace::{notifications::NotifyResultExt, Workspace};
const MAX_PROJECT_NAME_LENGTH: usize = 40;
@@ -487,7 +487,7 @@ impl CollabTitlebarItem {
}))
}
pub fn render_project_branch(&self, cx: &mut ViewContext<Self>) -> Option<impl Element> {
pub fn render_project_branch(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
let entry = {
let mut names_and_branches =
self.project.read(cx).visible_worktrees(cx).map(|worktree| {
@@ -503,22 +503,23 @@ impl CollabTitlebarItem {
.and_then(RepositoryEntry::branch)
.map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?;
Some(
popover_menu("project_branch_trigger")
.trigger(
Button::new("project_branch_trigger", branch_name)
.color(Color::Muted)
.style(ButtonStyle::Subtle)
.label_size(LabelSize::Small)
.tooltip(move |cx| {
Tooltip::with_meta(
"Recent Branches",
Some(&ToggleVcsMenu),
"Local branches only",
cx,
)
}),
)
.menu(move |cx| Self::render_vcs_popover(workspace.clone(), cx)),
Button::new("project_branch_trigger", branch_name)
.color(Color::Muted)
.style(ButtonStyle::Subtle)
.label_size(LabelSize::Small)
.tooltip(move |cx| {
Tooltip::with_meta(
"Recent Branches",
Some(&ToggleVcsMenu),
"Local branches only",
cx,
)
})
.on_click(move |_, cx| {
let _ = workspace.update(cx, |this, cx| {
BranchList::open(this, &Default::default(), cx)
});
}),
)
}
@@ -650,16 +651,6 @@ impl CollabTitlebarItem {
.log_err();
}
pub fn render_vcs_popover(
workspace: View<Workspace>,
cx: &mut WindowContext<'_>,
) -> Option<View<BranchList>> {
let view = build_branch_list(workspace, cx).log_err()?;
let focus_handle = view.focus_handle(cx);
cx.focus(&focus_handle);
Some(view)
}
fn render_connection_status(
&self,
status: &client::Status,

View File

@@ -289,6 +289,7 @@ gpui::actions!(
ToggleLineNumbers,
ToggleIndentGuides,
ToggleSoftWrap,
ToggleTabBar,
Transpose,
Undo,
UndoSelection,

View File

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

View File

@@ -277,8 +277,55 @@ impl DisplayMap {
block_map.insert(blocks)
}
pub fn replace_blocks(&mut self, styles: HashMap<BlockId, RenderBlock>) {
self.block_map.replace(styles);
pub fn replace_blocks(
&mut self,
heights_and_renderers: HashMap<BlockId, (Option<u8>, RenderBlock)>,
cx: &mut ModelContext<Self>,
) {
//
// Note: previous implementation of `replace_blocks` simply called
// `self.block_map.replace(styles)` which just modified the render by replacing
// the `RenderBlock` with the new one.
//
// ```rust
// for block in &self.blocks {
// if let Some(render) = renderers.remove(&block.id) {
// *block.render.lock() = render;
// }
// }
// ```
//
// If height changes however, we need to update the tree. There's a performance
// cost to this, so we'll split the replace blocks into handling the old behavior
// directly and the new behavior separately.
//
//
let mut only_renderers = HashMap::<BlockId, RenderBlock>::default();
let mut full_replace = HashMap::<BlockId, (u8, RenderBlock)>::default();
for (id, (height, render)) in heights_and_renderers {
if let Some(height) = height {
full_replace.insert(id, (height, render));
} else {
only_renderers.insert(id, render);
}
}
self.block_map.replace_renderers(only_renderers);
if full_replace.is_empty() {
return;
}
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
let mut block_map = self.block_map.write(snapshot, edits);
block_map.replace(full_replace);
}
pub fn remove_blocks(&mut self, ids: HashSet<BlockId>, cx: &mut ModelContext<Self>) {

View File

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

View File

@@ -15,18 +15,17 @@
pub mod actions;
mod blame_entry_tooltip;
mod blink_manager;
mod debounced_delay;
pub mod display_map;
mod editor_settings;
mod element;
mod hunk_diff;
mod inlay_hint_cache;
mod debounced_delay;
mod git;
mod highlight_matching_bracket;
mod hover_links;
mod hover_popover;
mod hunk_diff;
mod indent_guides;
mod inlay_hint_cache;
mod inline_completion_provider;
pub mod items;
mod mouse_context_menu;
@@ -54,8 +53,7 @@ use convert_case::{Case, Casing};
use debounced_delay::DebouncedDelay;
use display_map::*;
pub use display_map::{DisplayPoint, FoldPlaceholder};
use editor_settings::CurrentLineHighlight;
pub use editor_settings::EditorSettings;
pub use editor_settings::{CurrentLineHighlight, EditorSettings};
use element::LineWithInvisibles;
pub use element::{
CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition,
@@ -68,10 +66,10 @@ use gpui::{
div, impl_actions, point, prelude::*, px, relative, size, uniform_list, Action, AnyElement,
AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds, ClipboardItem,
Context, DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusableView, FontId, FontStyle,
FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext, Model, MouseButton, PaintQuad,
ParentElement, Pixels, Render, SharedString, Size, StrikethroughStyle, Styled, StyledText,
Subscription, Task, TextStyle, UnderlineStyle, UniformListScrollHandle, View, ViewContext,
ViewInputHandler, VisualContext, WeakView, WhiteSpace, WindowContext,
FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext, ListSizingBehavior, Model,
MouseButton, PaintQuad, ParentElement, Pixels, Render, SharedString, Size, StrikethroughStyle,
Styled, StyledText, Subscription, Task, TextStyle, UnderlineStyle, UniformListScrollHandle,
View, ViewContext, ViewInputHandler, VisualContext, WeakView, WhiteSpace, WindowContext,
};
use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_popover::{hide_hover, HoverState};
@@ -113,7 +111,7 @@ use rpc::{proto::*, ErrorExt};
use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide};
use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
use settings::{update_settings_file, Settings, SettingsStore};
use smallvec::SmallVec;
use snippet::Snippet;
use std::ops::Not as _;
@@ -145,7 +143,7 @@ use workspace::notifications::{DetachAndPromptErr, NotificationId};
use workspace::{
searchable::SearchEvent, ItemNavHistory, SplitDirection, ViewId, Workspace, WorkspaceId,
};
use workspace::{OpenInTerminal, OpenTerminal, Toast};
use workspace::{OpenInTerminal, OpenTerminal, TabBarSettings, Toast};
use crate::hover_links::find_url;
@@ -481,7 +479,7 @@ pub struct Editor {
pending_rename: Option<RenameState>,
searchable: bool,
cursor_shape: CursorShape,
current_line_highlight: CurrentLineHighlight,
current_line_highlight: Option<CurrentLineHighlight>,
collapse_matches: bool,
autoindent_mode: Option<AutoindentMode>,
workspace: Option<(WeakView<Workspace>, Option<WorkspaceId>)>,
@@ -524,6 +522,7 @@ pub struct Editor {
expect_bounds_change: Option<Bounds<Pixels>>,
tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>,
tasks_update_task: Option<Task<()>>,
previous_search_ranges: Option<Arc<[Range<Anchor>]>>,
}
#[derive(Clone)]
@@ -554,6 +553,20 @@ pub struct GutterDimensions {
pub git_blame_entries_width: Option<Pixels>,
}
impl GutterDimensions {
/// The full width of the space taken up by the gutter.
pub fn full_width(&self) -> Pixels {
self.margin + self.width
}
/// The width of the space reserved for the fold indicators,
/// use alongside 'justify_end' and `gutter_width` to
/// right align content with the line numbers
pub fn fold_area_width(&self) -> Pixels {
self.margin + self.right_padding
}
}
impl Default for GutterDimensions {
fn default() -> Self {
Self {
@@ -1113,7 +1126,8 @@ impl CompletionsMenu {
.occlude()
.max_h(max_height)
.track_scroll(self.scroll_handle.clone())
.with_width_from_item(widest_completion_ix);
.with_width_from_item(widest_completion_ix)
.with_sizing_behavior(ListSizingBehavior::Infer);
Popover::new()
.child(list)
@@ -1460,6 +1474,7 @@ impl CodeActionsMenu {
})
.map(|(ix, _)| ix),
)
.with_sizing_behavior(ListSizingBehavior::Infer)
.into_any_element();
let cursor_position = if let Some(row) = self.deployed_from_indicator {
@@ -1753,7 +1768,7 @@ impl Editor {
pending_rename: Default::default(),
searchable: true,
cursor_shape: Default::default(),
current_line_highlight: EditorSettings::get_global(cx).current_line_highlight,
current_line_highlight: None,
autoindent_mode: Some(AutoindentMode::EachLine),
collapse_matches: false,
workspace: None,
@@ -1810,6 +1825,7 @@ impl Editor {
}),
],
tasks_update_task: None,
previous_search_ranges: None,
};
this.tasks_update_task = Some(this.refresh_runnables(cx));
this._subscriptions.extend(project_subscriptions);
@@ -1977,7 +1993,9 @@ impl Editor {
ongoing_scroll: self.scroll_manager.ongoing_scroll(),
placeholder_text: self.placeholder_text.clone(),
is_focused: self.focus_handle.is_focused(cx),
current_line_highlight: self.current_line_highlight,
current_line_highlight: self
.current_line_highlight
.unwrap_or_else(|| EditorSettings::get_global(cx).current_line_highlight),
gutter_hovered: self.gutter_hovered,
}
}
@@ -2067,7 +2085,10 @@ impl Editor {
cx.notify();
}
pub fn set_current_line_highlight(&mut self, current_line_highlight: CurrentLineHighlight) {
pub fn set_current_line_highlight(
&mut self,
current_line_highlight: Option<CurrentLineHighlight>,
) {
self.current_line_highlight = current_line_highlight;
}
@@ -2798,6 +2819,9 @@ impl Editor {
}
if let Some(bracket_pair) = bracket_pair {
let autoclose = self.use_autoclose
&& snapshot.settings_at(selection.start, cx).use_autoclose;
if selection.is_empty() {
if is_bracket_pair_start {
let prefix_len = bracket_pair.start.len() - text.len();
@@ -2818,8 +2842,6 @@ impl Editor {
),
&bracket_pair.start[..prefix_len],
));
let autoclose = self.use_autoclose
&& snapshot.settings_at(selection.start, cx).use_autoclose;
if autoclose
&& following_text_allows_autoclose
&& preceding_text_matches_prefix
@@ -2872,7 +2894,10 @@ impl Editor {
}
// If an opening bracket is 1 character long and is typed while
// text is selected, then surround that text with the bracket pair.
else if is_bracket_pair_start && bracket_pair.start.chars().count() == 1 {
else if autoclose
&& is_bracket_pair_start
&& bracket_pair.start.chars().count() == 1
{
edits.push((selection.start..selection.start, text.clone()));
edits.push((
selection.end..selection.end,
@@ -2995,12 +3020,7 @@ impl Editor {
s.select(new_selections)
});
if brace_inserted {
// If we inserted a brace while composing text (i.e. typing `"` on a
// Brazilian keyboard), exit the composing state because most likely
// the user wanted to surround the selection.
this.unmark_text(cx);
} else if EditorSettings::get_global(cx).use_on_type_format {
if !brace_inserted && EditorSettings::get_global(cx).use_on_type_format {
if let Some(on_type_format_task) =
this.trigger_on_type_formatting(text.to_string(), cx)
{
@@ -3790,6 +3810,9 @@ impl Editor {
let id = post_inc(&mut self.next_completion_id);
let task = cx.spawn(|this, mut cx| {
async move {
this.update(&mut cx, |this, _| {
this.completion_tasks.retain(|(task_id, _)| *task_id >= id);
})?;
let completions = completions.await.log_err();
let menu = if let Some(completions) = completions {
let mut menu = CompletionsMenu {
@@ -3828,7 +3851,6 @@ impl Editor {
let delay_ms = EditorSettings::get_global(cx)
.completion_documentation_secondary_query_debounce;
let delay = Duration::from_millis(delay_ms);
editor
.completion_documentation_pre_resolve_debounce
.fire_new(delay, cx, |editor, cx| {
@@ -3849,8 +3871,6 @@ impl Editor {
};
this.update(&mut cx, |this, cx| {
this.completion_tasks.retain(|(task_id, _)| *task_id >= id);
let mut context_menu = this.context_menu.write();
match context_menu.as_ref() {
None => {}
@@ -9248,11 +9268,15 @@ impl Editor {
for (block_id, diagnostic) in &active_diagnostics.blocks {
new_styles.insert(
*block_id,
diagnostic_block_renderer(diagnostic.clone(), is_valid),
(
None,
diagnostic_block_renderer(diagnostic.clone(), is_valid),
),
);
}
self.display_map
.update(cx, |display_map, _| display_map.replace_blocks(new_styles));
self.display_map.update(cx, |display_map, cx| {
display_map.replace_blocks(new_styles, cx)
});
}
}
}
@@ -9609,12 +9633,12 @@ impl Editor {
pub fn replace_blocks(
&mut self,
blocks: HashMap<BlockId, RenderBlock>,
blocks: HashMap<BlockId, (Option<u8>, RenderBlock)>,
autoscroll: Option<Autoscroll>,
cx: &mut ViewContext<Self>,
) {
self.display_map
.update(cx, |display_map, _| display_map.replace_blocks(blocks));
.update(cx, |display_map, cx| display_map.replace_blocks(blocks, cx));
if let Some(autoscroll) = autoscroll {
self.request_autoscroll(autoscroll, cx);
}
@@ -9775,20 +9799,31 @@ impl Editor {
cx.notify();
}
pub fn toggle_indent_guides(&mut self, _: &ToggleIndentGuides, cx: &mut ViewContext<Self>) {
let currently_enabled = self.should_show_indent_guides(cx);
self.show_indent_guides = Some(!currently_enabled);
cx.notify();
pub fn toggle_tab_bar(&mut self, _: &ToggleTabBar, cx: &mut ViewContext<Self>) {
let Some(workspace) = self.workspace() else {
return;
};
let fs = workspace.read(cx).app_state().fs.clone();
let current_show = TabBarSettings::get_global(cx).show;
update_settings_file::<TabBarSettings>(fs, cx, move |setting| {
setting.show = Some(!current_show);
});
}
fn should_show_indent_guides(&self, cx: &mut ViewContext<Self>) -> bool {
self.show_indent_guides.unwrap_or_else(|| {
pub fn toggle_indent_guides(&mut self, _: &ToggleIndentGuides, cx: &mut ViewContext<Self>) {
let currently_enabled = self.should_show_indent_guides().unwrap_or_else(|| {
self.buffer
.read(cx)
.settings_at(0, cx)
.indent_guides
.enabled
})
});
self.show_indent_guides = Some(!currently_enabled);
cx.notify();
}
fn should_show_indent_guides(&self) -> Option<bool> {
self.show_indent_guides
}
pub fn toggle_line_numbers(&mut self, _: &ToggleLineNumbers, cx: &mut ViewContext<Self>) {
@@ -10241,6 +10276,27 @@ impl Editor {
self.background_highlights_in_range(start..end, &snapshot, theme)
}
#[cfg(feature = "test-support")]
pub fn search_background_highlights(
&mut self,
cx: &mut ViewContext<Self>,
) -> Vec<Range<Point>> {
let snapshot = self.buffer().read(cx).snapshot(cx);
let highlights = self
.background_highlights
.get(&TypeId::of::<items::BufferSearchHighlights>());
if let Some((_color, ranges)) = highlights {
ranges
.iter()
.map(|range| range.start.to_point(&snapshot)..range.end.to_point(&snapshot))
.collect_vec()
} else {
vec![]
}
}
fn document_highlights_for_position<'a>(
&'a self,
position: Anchor,
@@ -10589,7 +10645,6 @@ impl Editor {
let editor_settings = EditorSettings::get_global(cx);
self.scroll_manager.vertical_scroll_margin = editor_settings.vertical_scroll_margin;
self.show_breadcrumbs = editor_settings.toolbar.breadcrumbs;
self.current_line_highlight = editor_settings.current_line_highlight;
if self.mode == EditorMode::Full {
let inline_blame_enabled = ProjectSettings::get_global(cx).git.inline_blame_enabled();

View File

@@ -20,6 +20,7 @@ use language::{
FakeLspAdapter, IndentGuide, LanguageConfig, LanguageConfigOverride, LanguageMatcher, Override,
Point,
};
use language_settings::IndentGuideSettings;
use multi_buffer::MultiBufferIndentGuide;
use parking_lot::Mutex;
use project::project_settings::{LspSettings, ProjectSettings};
@@ -11505,6 +11506,7 @@ fn assert_indent_guides(
let snapshot = editor.snapshot(cx).display_snapshot;
let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
MultiBufferRow(range.start)..MultiBufferRow(range.end),
true,
&snapshot,
cx,
);
@@ -11543,6 +11545,21 @@ fn assert_indent_guides(
assert_eq!(indent_guides, expected, "Indent guides do not match");
}
fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
IndentGuide {
buffer_id,
start_row,
end_row,
depth,
tab_size: 4,
settings: IndentGuideSettings {
enabled: true,
line_width: 1,
..Default::default()
},
}
}
#[gpui::test]
async fn test_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
@@ -11555,12 +11572,7 @@ async fn test_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
)
.await;
assert_indent_guides(
0..3,
vec![IndentGuide::new(buffer_id, 1, 1, 0, 4)],
None,
&mut cx,
);
assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
}
#[gpui::test]
@@ -11576,12 +11588,7 @@ async fn test_indent_guide_simple_block(cx: &mut gpui::TestAppContext) {
)
.await;
assert_indent_guides(
0..4,
vec![IndentGuide::new(buffer_id, 1, 2, 0, 4)],
None,
&mut cx,
);
assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
}
#[gpui::test]
@@ -11604,9 +11611,9 @@ async fn test_indent_guide_nested(cx: &mut gpui::TestAppContext) {
assert_indent_guides(
0..8,
vec![
IndentGuide::new(buffer_id, 1, 6, 0, 4),
IndentGuide::new(buffer_id, 3, 3, 1, 4),
IndentGuide::new(buffer_id, 5, 5, 1, 4),
indent_guide(buffer_id, 1, 6, 0),
indent_guide(buffer_id, 3, 3, 1),
indent_guide(buffer_id, 5, 5, 1),
],
None,
&mut cx,
@@ -11630,8 +11637,8 @@ async fn test_indent_guide_tab(cx: &mut gpui::TestAppContext) {
assert_indent_guides(
0..5,
vec![
IndentGuide::new(buffer_id, 1, 3, 0, 4),
IndentGuide::new(buffer_id, 2, 2, 1, 4),
indent_guide(buffer_id, 1, 3, 0),
indent_guide(buffer_id, 2, 2, 1),
],
None,
&mut cx,
@@ -11652,12 +11659,7 @@ async fn test_indent_guide_continues_on_empty_line(cx: &mut gpui::TestAppContext
)
.await;
assert_indent_guides(
0..5,
vec![IndentGuide::new(buffer_id, 1, 3, 0, 4)],
None,
&mut cx,
);
assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
}
#[gpui::test]
@@ -11683,9 +11685,9 @@ async fn test_indent_guide_complex(cx: &mut gpui::TestAppContext) {
assert_indent_guides(
0..11,
vec![
IndentGuide::new(buffer_id, 1, 9, 0, 4),
IndentGuide::new(buffer_id, 6, 6, 1, 4),
IndentGuide::new(buffer_id, 8, 8, 1, 4),
indent_guide(buffer_id, 1, 9, 0),
indent_guide(buffer_id, 6, 6, 1),
indent_guide(buffer_id, 8, 8, 1),
],
None,
&mut cx,
@@ -11715,9 +11717,9 @@ async fn test_indent_guide_starts_off_screen(cx: &mut gpui::TestAppContext) {
assert_indent_guides(
1..11,
vec![
IndentGuide::new(buffer_id, 1, 9, 0, 4),
IndentGuide::new(buffer_id, 6, 6, 1, 4),
IndentGuide::new(buffer_id, 8, 8, 1, 4),
indent_guide(buffer_id, 1, 9, 0),
indent_guide(buffer_id, 6, 6, 1),
indent_guide(buffer_id, 8, 8, 1),
],
None,
&mut cx,
@@ -11747,9 +11749,9 @@ async fn test_indent_guide_ends_off_screen(cx: &mut gpui::TestAppContext) {
assert_indent_guides(
1..10,
vec![
IndentGuide::new(buffer_id, 1, 9, 0, 4),
IndentGuide::new(buffer_id, 6, 6, 1, 4),
IndentGuide::new(buffer_id, 8, 8, 1, 4),
indent_guide(buffer_id, 1, 9, 0),
indent_guide(buffer_id, 6, 6, 1),
indent_guide(buffer_id, 8, 8, 1),
],
None,
&mut cx,
@@ -11775,9 +11777,9 @@ async fn test_indent_guide_without_brackets(cx: &mut gpui::TestAppContext) {
assert_indent_guides(
1..10,
vec![
IndentGuide::new(buffer_id, 1, 4, 0, 4),
IndentGuide::new(buffer_id, 2, 3, 1, 4),
IndentGuide::new(buffer_id, 3, 3, 2, 4),
indent_guide(buffer_id, 1, 4, 0),
indent_guide(buffer_id, 2, 3, 1),
indent_guide(buffer_id, 3, 3, 2),
],
None,
&mut cx,
@@ -11802,8 +11804,8 @@ async fn test_indent_guide_ends_before_empty_line(cx: &mut gpui::TestAppContext)
assert_indent_guides(
0..6,
vec![
IndentGuide::new(buffer_id, 1, 2, 0, 4),
IndentGuide::new(buffer_id, 2, 2, 1, 4),
indent_guide(buffer_id, 1, 2, 0),
indent_guide(buffer_id, 2, 2, 1),
],
None,
&mut cx,
@@ -11825,12 +11827,7 @@ async fn test_indent_guide_continuing_off_screen(cx: &mut gpui::TestAppContext)
)
.await;
assert_indent_guides(
0..1,
vec![IndentGuide::new(buffer_id, 1, 1, 0, 4)],
None,
&mut cx,
);
assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
}
#[gpui::test]
@@ -11852,8 +11849,8 @@ async fn test_indent_guide_tabs(cx: &mut gpui::TestAppContext) {
assert_indent_guides(
0..6,
vec![
IndentGuide::new(buffer_id, 1, 6, 0, 4),
IndentGuide::new(buffer_id, 3, 4, 1, 4),
indent_guide(buffer_id, 1, 6, 0),
indent_guide(buffer_id, 3, 4, 1),
],
None,
&mut cx,
@@ -11880,7 +11877,7 @@ async fn test_active_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
assert_indent_guides(
0..3,
vec![IndentGuide::new(buffer_id, 1, 1, 0, 4)],
vec![indent_guide(buffer_id, 1, 1, 0)],
Some(vec![0]),
&mut cx,
);
@@ -11909,8 +11906,8 @@ async fn test_active_indent_guide_respect_indented_range(cx: &mut gpui::TestAppC
assert_indent_guides(
0..4,
vec![
IndentGuide::new(buffer_id, 1, 3, 0, 4),
IndentGuide::new(buffer_id, 2, 2, 1, 4),
indent_guide(buffer_id, 1, 3, 0),
indent_guide(buffer_id, 2, 2, 1),
],
Some(vec![1]),
&mut cx,
@@ -11925,8 +11922,8 @@ async fn test_active_indent_guide_respect_indented_range(cx: &mut gpui::TestAppC
assert_indent_guides(
0..4,
vec![
IndentGuide::new(buffer_id, 1, 3, 0, 4),
IndentGuide::new(buffer_id, 2, 2, 1, 4),
indent_guide(buffer_id, 1, 3, 0),
indent_guide(buffer_id, 2, 2, 1),
],
Some(vec![1]),
&mut cx,
@@ -11941,8 +11938,8 @@ async fn test_active_indent_guide_respect_indented_range(cx: &mut gpui::TestAppC
assert_indent_guides(
0..4,
vec![
IndentGuide::new(buffer_id, 1, 3, 0, 4),
IndentGuide::new(buffer_id, 2, 2, 1, 4),
indent_guide(buffer_id, 1, 3, 0),
indent_guide(buffer_id, 2, 2, 1),
],
Some(vec![0]),
&mut cx,
@@ -11971,7 +11968,7 @@ async fn test_active_indent_guide_empty_line(cx: &mut gpui::TestAppContext) {
assert_indent_guides(
0..5,
vec![IndentGuide::new(buffer_id, 1, 3, 0, 4)],
vec![indent_guide(buffer_id, 1, 3, 0)],
Some(vec![0]),
&mut cx,
);
@@ -11997,7 +11994,7 @@ async fn test_active_indent_guide_non_matching_indent(cx: &mut gpui::TestAppCont
assert_indent_guides(
0..3,
vec![IndentGuide::new(buffer_id, 1, 2, 0, 4)],
vec![indent_guide(buffer_id, 1, 2, 0)],
Some(vec![0]),
&mut cx,
);

View File

@@ -38,7 +38,7 @@ use gpui::{
};
use itertools::Itertools;
use language::language_settings::{
IndentGuideBackgroundColoring, IndentGuideColoring, ShowWhitespaceSetting,
IndentGuideBackgroundColoring, IndentGuideColoring, IndentGuideSettings, ShowWhitespaceSetting,
};
use lsp::DiagnosticSeverity;
use multi_buffer::{Anchor, MultiBufferPoint, MultiBufferRow};
@@ -318,6 +318,7 @@ impl EditorElement {
register_action(view, cx, Editor::open_excerpts);
register_action(view, cx, Editor::open_excerpts_in_split);
register_action(view, cx, Editor::toggle_soft_wrap);
register_action(view, cx, Editor::toggle_tab_bar);
register_action(view, cx, Editor::toggle_line_numbers);
register_action(view, cx, Editor::toggle_indent_guides);
register_action(view, cx, Editor::toggle_inlay_hints);
@@ -1125,9 +1126,7 @@ impl EditorElement {
ix as f32 * line_height - (scroll_pixel_position.y % line_height),
);
let centering_offset = point(
(gutter_dimensions.right_padding + gutter_dimensions.margin
- fold_indicator_size.width)
/ 2.,
(gutter_dimensions.fold_area_width() - fold_indicator_size.width) / 2.,
(line_height - fold_indicator_size.height) / 2.,
);
let origin = gutter_hitbox.origin + position + centering_offset;
@@ -1222,34 +1221,41 @@ impl EditorElement {
.collect::<HashMap<_, _>>()
});
let git_gutter_setting = ProjectSettings::get_global(cx)
.git
.git_gutter
.unwrap_or_default();
buffer_snapshot
.git_diff_hunks_in_range(buffer_start_row..buffer_end_row)
.map(|hunk| diff_hunk_to_display(&hunk, snapshot))
.dedup()
.map(|hunk| {
let hitbox = if let DisplayDiffHunk::Unfolded {
display_row_range, ..
} = &hunk
{
let was_expanded = expanded_hunk_display_rows
.get(&display_row_range.start)
.map(|expanded_end_row| expanded_end_row == &display_row_range.end)
.unwrap_or(false);
if was_expanded {
None
.map(|hunk| match git_gutter_setting {
GitGutterSetting::TrackedFiles => {
let hitbox = if let DisplayDiffHunk::Unfolded {
display_row_range, ..
} = &hunk
{
let was_expanded = expanded_hunk_display_rows
.get(&display_row_range.start)
.map(|expanded_end_row| expanded_end_row == &display_row_range.end)
.unwrap_or(false);
if was_expanded {
None
} else {
let hunk_bounds = Self::diff_hunk_bounds(
&snapshot,
line_height,
gutter_hitbox.bounds,
&hunk,
);
Some(cx.insert_hitbox(hunk_bounds, true))
}
} else {
let hunk_bounds = Self::diff_hunk_bounds(
&snapshot,
line_height,
gutter_hitbox.bounds,
&hunk,
);
Some(cx.insert_hitbox(hunk_bounds, true))
}
} else {
None
};
(hunk, hitbox)
None
};
(hunk, hitbox)
}
GitGutterSetting::Hide => (hunk, None),
})
.collect()
}
@@ -1438,6 +1444,7 @@ impl EditorElement {
single_indent_width,
depth: indent_guide.depth,
active: active_indent_guide_indices.contains(&i),
settings: indent_guide.settings,
})
} else {
None
@@ -2730,14 +2737,6 @@ impl EditorElement {
return;
};
let settings = self
.editor
.read(cx)
.buffer()
.read(cx)
.settings_at(0, cx)
.indent_guides;
let faded_color = |color: Hsla, alpha: f32| {
let mut faded = color;
faded.a = alpha;
@@ -2746,6 +2745,7 @@ impl EditorElement {
for indent_guide in indent_guides {
let indent_accent_colors = cx.theme().accents().color_for_index(indent_guide.depth);
let settings = indent_guide.settings;
// TODO fixed for now, expose them through themes later
const INDENT_AWARE_ALPHA: f32 = 0.2;
@@ -2753,7 +2753,7 @@ impl EditorElement {
const INDENT_AWARE_BACKGROUND_ALPHA: f32 = 0.1;
const INDENT_AWARE_BACKGROUND_ACTIVE_ALPHA: f32 = 0.2;
let line_color = match (&settings.coloring, indent_guide.active) {
let line_color = match (settings.coloring, indent_guide.active) {
(IndentGuideColoring::Disabled, _) => None,
(IndentGuideColoring::Fixed, false) => {
Some(cx.theme().colors().editor_indent_guide)
@@ -2769,7 +2769,7 @@ impl EditorElement {
}
};
let background_color = match (&settings.background_coloring, indent_guide.active) {
let background_color = match (settings.background_coloring, indent_guide.active) {
(IndentGuideBackgroundColoring::Disabled, _) => None,
(IndentGuideBackgroundColoring::IndentAware, false) => Some(faded_color(
indent_accent_colors,
@@ -4072,6 +4072,7 @@ impl LineWithInvisibles {
if non_whitespace_added || !inside_wrapped_string {
invisibles.push(Invisible::Tab {
line_start_offset: line.len(),
line_end_offset: line.len() + line_chunk.len(),
});
}
} else {
@@ -4187,16 +4188,15 @@ impl LineWithInvisibles {
whitespace_setting: ShowWhitespaceSetting,
cx: &mut WindowContext,
) {
let allowed_invisibles_regions = match whitespace_setting {
ShowWhitespaceSetting::None => return,
ShowWhitespaceSetting::Selection => Some(selection_ranges),
ShowWhitespaceSetting::All => None,
};
for invisible in &self.invisibles {
let (&token_offset, invisible_symbol) = match invisible {
Invisible::Tab { line_start_offset } => (line_start_offset, &layout.tab_invisible),
Invisible::Whitespace { line_offset } => (line_offset, &layout.space_invisible),
let extract_whitespace_info = |invisible: &Invisible| {
let (token_offset, token_end_offset, invisible_symbol) = match invisible {
Invisible::Tab {
line_start_offset,
line_end_offset,
} => (*line_start_offset, *line_end_offset, &layout.tab_invisible),
Invisible::Whitespace { line_offset } => {
(*line_offset, line_offset + 1, &layout.space_invisible)
}
};
let x_offset = self.x_for_index(token_offset);
@@ -4208,17 +4208,73 @@ impl LineWithInvisibles {
line_y,
);
if let Some(allowed_regions) = allowed_invisibles_regions {
let invisible_point = DisplayPoint::new(row, token_offset as u32);
if !allowed_regions
(
[token_offset, token_end_offset],
Box::new(move |cx: &mut WindowContext| {
invisible_symbol.paint(origin, line_height, cx).log_err();
}),
)
};
let invisible_iter = self.invisibles.iter().map(extract_whitespace_info);
match whitespace_setting {
ShowWhitespaceSetting::None => return,
ShowWhitespaceSetting::All => invisible_iter.for_each(|(_, paint)| paint(cx)),
ShowWhitespaceSetting::Selection => invisible_iter.for_each(|([start, _], paint)| {
let invisible_point = DisplayPoint::new(row, start as u32);
if !selection_ranges
.iter()
.any(|region| region.start <= invisible_point && invisible_point < region.end)
{
continue;
return;
}
paint(cx);
}),
// For a whitespace to be on a boundary, any of the following conditions need to be met:
// - It is a tab
// - It is adjacent to an edge (start or end)
// - It is adjacent to a whitespace (left or right)
ShowWhitespaceSetting::Boundary => {
// We'll need to keep track of the last invisible we've seen and then check if we are adjacent to it for some of
// the above cases.
// Note: We zip in the original `invisibles` to check for tab equality
let mut last_seen: Option<(bool, usize, Box<dyn Fn(&mut WindowContext)>)> = None;
for (([start, end], paint), invisible) in
invisible_iter.zip_eq(self.invisibles.iter())
{
let should_render = match (&last_seen, invisible) {
(_, Invisible::Tab { .. }) => true,
(Some((_, last_end, _)), _) => *last_end == start,
_ => false,
};
if should_render || start == 0 || end == self.len {
paint(cx);
// Since we are scanning from the left, we will skip over the first available whitespace that is part
// of a boundary between non-whitespace segments, so we correct by manually redrawing it if needed.
if let Some((should_render_last, last_end, paint_last)) = last_seen {
// Note that we need to make sure that the last one is actually adjacent
if !should_render_last && last_end == start {
paint_last(cx);
}
}
}
// Manually render anything within a selection
let invisible_point = DisplayPoint::new(row, start as u32);
if selection_ranges.iter().any(|region| {
region.start <= invisible_point && invisible_point < region.end
}) {
paint(cx);
}
last_seen = Some((should_render, end, paint));
}
}
invisible_symbol.paint(origin, line_height, cx).log_err();
}
};
}
pub fn x_for_index(&self, index: usize) -> Pixels {
@@ -4308,8 +4364,18 @@ impl LineWithInvisibles {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Invisible {
Tab { line_start_offset: usize },
Whitespace { line_offset: usize },
/// A tab character
///
/// A tab character is internally represented by spaces (configured by the user's tab width)
/// aligned to the nearest column, so it's necessary to store the start and end offset for
/// adjacency checks.
Tab {
line_start_offset: usize,
line_end_offset: usize,
},
Whitespace {
line_offset: usize,
},
}
impl EditorElement {
@@ -4635,7 +4701,7 @@ impl Element for EditorElement {
&mut scroll_width,
&gutter_dimensions,
em_width,
gutter_dimensions.width + gutter_dimensions.margin,
gutter_dimensions.full_width(),
line_height,
&line_layouts,
cx,
@@ -5286,6 +5352,7 @@ pub struct IndentGuideLayout {
single_indent_width: Pixels,
depth: u32,
active: bool,
settings: IndentGuideSettings,
}
pub struct CursorLayout {
@@ -5853,15 +5920,18 @@ mod tests {
let expected_invisibles = vec![
Invisible::Tab {
line_start_offset: 0,
line_end_offset: TAB_SIZE as usize,
},
Invisible::Whitespace {
line_offset: TAB_SIZE as usize,
},
Invisible::Tab {
line_start_offset: TAB_SIZE as usize + 1,
line_end_offset: TAB_SIZE as usize * 2,
},
Invisible::Tab {
line_start_offset: TAB_SIZE as usize * 2 + 1,
line_end_offset: TAB_SIZE as usize * 3,
},
Invisible::Whitespace {
line_offset: TAB_SIZE as usize * 3 + 1,
@@ -5915,10 +5985,11 @@ mod tests {
#[gpui::test]
fn test_wrapped_invisibles_drawing(cx: &mut TestAppContext) {
let tab_size = 4;
let input_text = "a\tbcd ".repeat(9);
let input_text = "a\tbcd ".repeat(9);
let repeated_invisibles = [
Invisible::Tab {
line_start_offset: 1,
line_end_offset: tab_size as usize,
},
Invisible::Whitespace {
line_offset: tab_size as usize + 3,
@@ -5929,6 +6000,12 @@ mod tests {
Invisible::Whitespace {
line_offset: tab_size as usize + 5,
},
Invisible::Whitespace {
line_offset: tab_size as usize + 6,
},
Invisible::Whitespace {
line_offset: tab_size as usize + 7,
},
];
let expected_invisibles = std::iter::once(repeated_invisibles)
.cycle()

View File

@@ -1,5 +1,6 @@
use crate::{
hover_popover::{self, InlayHover},
scroll::ScrollAmount,
Anchor, Editor, EditorSnapshot, FindAllReferences, GoToDefinition, GoToTypeDefinition, InlayId,
PointForPosition, SelectPhase,
};
@@ -38,7 +39,11 @@ impl RangeInEditor {
}
}
fn point_within_range(&self, trigger_point: &TriggerPoint, snapshot: &EditorSnapshot) -> bool {
pub fn point_within_range(
&self,
trigger_point: &TriggerPoint,
snapshot: &EditorSnapshot,
) -> bool {
match (self, trigger_point) {
(Self::Text(range), TriggerPoint::Text(point)) => {
let point_after_start = range.start.cmp(point, &snapshot.buffer_snapshot).is_le();
@@ -169,6 +174,21 @@ impl Editor {
.detach();
}
pub fn scroll_hover(&mut self, amount: &ScrollAmount, cx: &mut ViewContext<Self>) -> bool {
let selection = self.selections.newest_anchor().head();
let snapshot = self.snapshot(cx);
let Some(popover) = self.hover_state.info_popovers.iter().find(|popover| {
popover
.symbol_range
.point_within_range(&TriggerPoint::Text(selection), &snapshot)
}) else {
return false;
};
popover.scroll(amount, cx);
true
}
fn cmd_click_reveal_task(
&mut self,
point: PointForPosition,

View File

@@ -1,14 +1,15 @@
use crate::{
display_map::{InlayOffset, ToDisplayPoint},
hover_links::{InlayHighlight, RangeInEditor},
scroll::ScrollAmount,
Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings, EditorSnapshot,
EditorStyle, ExcerptId, Hover, RangeToAnchorExt,
};
use futures::{stream::FuturesUnordered, FutureExt};
use gpui::{
div, px, AnyElement, CursorStyle, Hsla, InteractiveElement, IntoElement, MouseButton,
ParentElement, Pixels, SharedString, Size, StatefulInteractiveElement, Styled, Task,
ViewContext, WeakView,
ParentElement, Pixels, ScrollHandle, SharedString, Size, StatefulInteractiveElement, Styled,
Task, ViewContext, WeakView,
};
use language::{markdown, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown};
@@ -118,6 +119,7 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie
let hover_popover = InfoPopover {
symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
parsed_content,
scroll_handle: ScrollHandle::new(),
};
this.update(&mut cx, |this, cx| {
@@ -317,6 +319,7 @@ fn show_hover(
InfoPopover {
symbol_range: RangeInEditor::Text(range),
parsed_content,
scroll_handle: ScrollHandle::new(),
},
)
})
@@ -423,7 +426,7 @@ async fn parse_blocks(
}
}
#[derive(Default)]
#[derive(Default, Debug)]
pub struct HoverState {
pub info_popovers: Vec<InfoPopover>,
pub diagnostic_popover: Option<DiagnosticPopover>,
@@ -487,10 +490,11 @@ impl HoverState {
}
}
#[derive(Debug, Clone)]
#[derive(Clone, Debug)]
pub struct InfoPopover {
symbol_range: RangeInEditor,
parsed_content: ParsedMarkdown,
pub symbol_range: RangeInEditor,
pub parsed_content: ParsedMarkdown,
pub scroll_handle: ScrollHandle,
}
impl InfoPopover {
@@ -504,23 +508,33 @@ impl InfoPopover {
div()
.id("info_popover")
.elevation_2(cx)
.p_2()
.overflow_y_scroll()
.track_scroll(&self.scroll_handle)
.max_w(max_size.width)
.max_h(max_size.height)
// Prevent a mouse down/move on the popover from being propagated to the editor,
// because that would dismiss the popover.
.on_mouse_move(|_, cx| cx.stop_propagation())
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
.child(crate::render_parsed_markdown(
.child(div().p_2().child(crate::render_parsed_markdown(
"content",
&self.parsed_content,
style,
workspace,
cx,
))
)))
.into_any_element()
}
pub fn scroll(&self, amount: &ScrollAmount, cx: &mut ViewContext<Editor>) {
let mut current = self.scroll_handle.offset();
current.y -= amount.pixels(
cx.line_height(),
self.scroll_handle.bounds().size.height - px(16.),
) / 2.0;
cx.notify();
self.scroll_handle.set_offset(current);
}
}
#[derive(Debug, Clone)]

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,8 @@
use crate::Editor;
use serde::Deserialize;
use ui::{px, Pixels};
#[derive(Clone, PartialEq, Deserialize)]
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub enum ScrollAmount {
// Scroll N lines (positive is towards the end of the document)
Line(f32),
@@ -25,4 +26,11 @@ impl ScrollAmount {
.unwrap_or(0.),
}
}
pub fn pixels(&self, line_height: Pixels, height: Pixels) -> Pixels {
match self {
ScrollAmount::Line(x) => px(line_height.0 * x),
ScrollAmount::Page(x) => px(height.0 * x),
}
}
}

View File

@@ -273,6 +273,13 @@ impl SelectionsCollection {
self.all(cx).last().unwrap().clone()
}
pub fn disjoint_anchor_ranges(&self) -> Vec<Range<Anchor>> {
self.disjoint_anchors()
.iter()
.map(|s| s.start..s.end)
.collect()
}
#[cfg(any(test, feature = "test-support"))]
pub fn ranges<D: TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug>(
&self,

View File

@@ -39,7 +39,7 @@ impl SlashCommand for ExtensionSlashCommand {
&self,
_query: String,
_cancel: Arc<AtomicBool>,
_workspace: WeakView<Workspace>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
Task::ready(Ok(Vec::new()))
@@ -100,6 +100,7 @@ impl SlashCommand for ExtensionSlashCommand {
}
}),
}],
run_commands_in_text: false,
})
})
}

View File

@@ -351,7 +351,7 @@ impl ExtensionStore {
let reload_tx = this.reload_tx.clone();
let installed_dir = this.installed_dir.clone();
async move {
let mut paths = fs.watch(&installed_dir, FS_WATCH_LATENCY).await;
let (mut paths, _) = fs.watch(&installed_dir, FS_WATCH_LATENCY).await;
while let Some(paths) = paths.next().await {
for path in paths {
let Ok(event_path) = path.strip_prefix(&installed_dir) else {

View File

@@ -12,7 +12,7 @@ use editor::{Editor, EditorElement, EditorStyle};
use extension::{ExtensionManifest, ExtensionOperation, ExtensionStore};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
actions, canvas, uniform_list, AnyElement, AppContext, EventEmitter, FocusableView, FontStyle,
actions, uniform_list, AnyElement, AppContext, EventEmitter, FocusableView, FontStyle,
InteractiveElement, KeyContext, ParentElement, Render, Styled, Task, TextStyle,
UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext,
};
@@ -938,24 +938,10 @@ impl Render for ExtensionsPage {
let view = cx.view().clone();
let scroll_handle = self.list.clone();
this.child(
canvas(
move |bounds, cx| {
let mut list = uniform_list::<_, ExtensionCard, _>(
view,
"entries",
count,
Self::render_extensions,
)
.size_full()
.pb_4()
.track_scroll(scroll_handle)
.into_any_element();
list.prepaint_as_root(bounds.origin, bounds.size.into(), cx);
list
},
|_bounds, mut list, cx| list.paint(cx),
)
.size_full(),
uniform_list(view, "entries", count, Self::render_extensions)
.flex_grow()
.pb_4()
.track_scroll(scroll_handle),
)
}))
}

View File

@@ -36,6 +36,11 @@ use smol::io::AsyncReadExt;
#[cfg(any(test, feature = "test-support"))]
use std::ffi::OsStr;
pub trait Watcher: Send + Sync {
fn add(&self, path: &Path) -> Result<()>;
fn remove(&self, path: &Path) -> Result<()>;
}
#[async_trait::async_trait]
pub trait Fs: Send + Sync {
async fn create_dir(&self, path: &Path) -> Result<()>;
@@ -79,7 +84,10 @@ pub trait Fs: Send + Sync {
&self,
path: &Path,
latency: Duration,
) -> Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>>;
) -> (
Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>>,
Arc<dyn Watcher>,
);
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<dyn GitRepository>>;
fn is_fake(&self) -> bool;
@@ -126,6 +134,13 @@ pub struct RealFs {
git_binary_path: Option<PathBuf>,
}
pub struct RealWatcher {
#[cfg(target_os = "linux")]
root_path: PathBuf,
#[cfg(target_os = "linux")]
fs_watcher: parking_lot::Mutex<notify::INotifyWatcher>,
}
impl RealFs {
pub fn new(
git_hosting_provider_registry: Arc<GitHostingProviderRegistry>,
@@ -409,7 +424,10 @@ impl Fs for RealFs {
&self,
path: &Path,
latency: Duration,
) -> Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>> {
) -> (
Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>>,
Arc<dyn Watcher>,
) {
use fsevent::EventStream;
let (tx, rx) = smol::channel::unbounded();
@@ -421,22 +439,76 @@ impl Fs for RealFs {
});
});
Box::pin(rx.chain(futures::stream::once(async move {
drop(handle);
vec![]
})))
(
Box::pin(rx.chain(futures::stream::once(async move {
drop(handle);
vec![]
}))),
Arc::new(RealWatcher {}),
)
}
#[cfg(not(target_os = "macos"))]
#[cfg(target_os = "linux")]
async fn watch(
&self,
path: &Path,
_latency: Duration,
) -> Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>> {
use notify::{event::EventKind, event::ModifyKind, Watcher};
// todo(linux): This spawns two threads, while the macOS impl
// only spawns one. Can we use a OnceLock or some such to make
// this better
) -> (
Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>>,
Arc<dyn Watcher>,
) {
let (tx, rx) = smol::channel::unbounded();
let file_watcher = notify::recommended_watcher({
let tx = tx.clone();
move |event: Result<notify::Event, _>| {
if let Some(event) = event.log_err() {
tx.try_send(event.paths).ok();
}
}
})
.expect("Could not start file watcher");
let watcher = Arc::new(RealWatcher {
root_path: path.to_path_buf(),
fs_watcher: parking_lot::Mutex::new(file_watcher),
});
watcher.add(path).ok(); // Ignore "file doesn't exist error" and rely on parent watcher.
// watch the parent dir so we can tell when settings.json is created
if let Some(parent) = path.parent() {
watcher.add(parent).log_err();
}
(
Box::pin(rx.filter_map({
let watcher = watcher.clone();
move |mut paths| {
paths.retain(|path| path.starts_with(&watcher.root_path));
async move {
if paths.is_empty() {
None
} else {
Some(paths)
}
}
}
})),
watcher,
)
}
#[cfg(target_os = "windows")]
async fn watch(
&self,
path: &Path,
_latency: Duration,
) -> (
Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>>,
Arc<dyn Watcher>,
) {
use notify::Watcher;
let (tx, rx) = smol::channel::unbounded();
@@ -452,56 +524,15 @@ impl Fs for RealFs {
file_watcher
.watch(path, notify::RecursiveMode::Recursive)
.ok(); // It's ok if this fails, the parent watcher will add it.
.log_err();
let mut parent_watcher = notify::recommended_watcher({
let watched_path = path.to_path_buf();
let tx = tx.clone();
move |event: Result<notify::Event, _>| {
if let Some(event) = event.ok() {
if event.paths.into_iter().any(|path| *path == watched_path) {
match event.kind {
EventKind::Modify(ev) => {
if matches!(ev, ModifyKind::Name(_)) {
file_watcher
.watch(
watched_path.as_path(),
notify::RecursiveMode::Recursive,
)
.log_err();
let _ = tx.try_send(vec![watched_path.clone()]).ok();
}
}
EventKind::Create(_) => {
file_watcher
.watch(watched_path.as_path(), notify::RecursiveMode::Recursive)
.log_err();
let _ = tx.try_send(vec![watched_path.clone()]).ok();
}
EventKind::Remove(_) => {
file_watcher.unwatch(&watched_path).log_err();
let _ = tx.try_send(vec![watched_path.clone()]).ok();
}
_ => {}
}
}
}
}
})
.expect("Could not start file watcher");
parent_watcher
.watch(
path.parent()
.expect("Watching root is probably not what you want"),
notify::RecursiveMode::NonRecursive,
)
.expect("Could not start watcher on parent directory");
Box::pin(rx.chain(futures::stream::once(async move {
drop(parent_watcher);
vec![]
})))
(
Box::pin(rx.chain(futures::stream::once(async move {
drop(file_watcher);
vec![]
}))),
Arc::new(RealWatcher {}),
)
}
fn open_repo(&self, dotgit_path: &Path) -> Option<Arc<dyn GitRepository>> {
@@ -560,6 +591,36 @@ impl Fs for RealFs {
}
}
#[cfg(not(target_os = "linux"))]
impl Watcher for RealWatcher {
fn add(&self, _: &Path) -> Result<()> {
Ok(())
}
fn remove(&self, _: &Path) -> Result<()> {
Ok(())
}
}
#[cfg(target_os = "linux")]
impl Watcher for RealWatcher {
fn add(&self, path: &Path) -> Result<()> {
use notify::Watcher;
self.fs_watcher
.lock()
.watch(path, notify::RecursiveMode::NonRecursive)?;
Ok(())
}
fn remove(&self, path: &Path) -> Result<()> {
use notify::Watcher;
self.fs_watcher.lock().unwatch(path)?;
Ok(())
}
}
#[cfg(any(test, feature = "test-support"))]
pub struct FakeFs {
// Use an unfair lock to ensure tests are deterministic.
@@ -1073,6 +1134,20 @@ impl FakeFsEntry {
}
}
#[cfg(any(test, feature = "test-support"))]
struct FakeWatcher {}
#[cfg(any(test, feature = "test-support"))]
impl Watcher for FakeWatcher {
fn add(&self, _: &Path) -> Result<()> {
Ok(())
}
fn remove(&self, _: &Path) -> Result<()> {
Ok(())
}
}
#[cfg(any(test, feature = "test-support"))]
#[async_trait::async_trait]
impl Fs for FakeFs {
@@ -1468,20 +1543,26 @@ impl Fs for FakeFs {
&self,
path: &Path,
_: Duration,
) -> Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>> {
) -> (
Pin<Box<dyn Send + Stream<Item = Vec<PathBuf>>>>,
Arc<dyn Watcher>,
) {
self.simulate_random_delay().await;
let (tx, rx) = smol::channel::unbounded();
self.state.lock().event_txs.push(tx);
let path = path.to_path_buf();
let executor = self.executor.clone();
Box::pin(futures::StreamExt::filter(rx, move |events| {
let result = events.iter().any(|evt_path| evt_path.starts_with(&path));
let executor = executor.clone();
async move {
executor.simulate_random_delay().await;
result
}
}))
(
Box::pin(futures::StreamExt::filter(rx, move |events| {
let result = events.iter().any(|evt_path| evt_path.starts_with(&path));
let executor = executor.clone();
async move {
executor.simulate_random_delay().await;
result
}
})),
Arc::new(FakeWatcher {}),
)
}
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<dyn GitRepository>> {

View File

@@ -99,7 +99,6 @@ objc = "0.2"
[target.'cfg(any(target_os = "linux", target_os = "windows"))'.dependencies]
flume = "0.11"
#TODO: use these on all platforms
blade-graphics.workspace = true
blade-macros.workspace = true
blade-util.workspace = true

View File

@@ -29,10 +29,11 @@ use crate::{
current_platform, init_app_menus, Action, ActionRegistry, Any, AnyView, AnyWindowHandle,
AppMetadata, AssetCache, AssetSource, BackgroundExecutor, ClipboardItem, Context,
DispatchPhase, DisplayId, Entity, EventEmitter, ForegroundExecutor, Global, KeyBinding, Keymap,
Keystroke, LayoutId, Menu, MenuItem, PathPromptOptions, Pixels, Platform, PlatformDisplay,
Point, PromptBuilder, PromptHandle, PromptLevel, Render, RenderablePromptHandle, Reservation,
SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, View, ViewContext,
Window, WindowAppearance, WindowContext, WindowHandle, WindowId,
Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
PlatformDisplay, Point, PromptBuilder, PromptHandle, PromptLevel, Render,
RenderablePromptHandle, Reservation, SharedString, SubscriberSet, Subscription, SvgRenderer,
Task, TextSystem, View, ViewContext, Window, WindowAppearance, WindowContext, WindowHandle,
WindowId,
};
mod async_context;
@@ -734,7 +735,11 @@ impl AppContext {
})
.collect::<Vec<_>>()
{
self.update_window(window, |_, cx| cx.draw()).unwrap();
self.update_window(window, |_, cx| {
println!("flush_effects. cx.draw()");
cx.draw()
})
.unwrap();
}
if self.pending_effects.is_empty() {
@@ -1167,6 +1172,11 @@ impl AppContext {
self.platform.set_menus(menus, &self.keymap.borrow());
}
/// Gets the menu bar for this application.
pub fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
self.platform.get_menus()
}
/// Sets the right click menu for the app icon in the dock
pub fn set_dock_menu(&mut self, menus: Vec<MenuItem>) {
self.platform.set_dock_menu(menus, &self.keymap.borrow());

View File

@@ -2437,7 +2437,7 @@ where
}
}
#[derive(Default)]
#[derive(Default, Debug)]
struct ScrollHandleState {
offset: Rc<RefCell<Point<Pixels>>>,
bounds: Bounds<Pixels>,
@@ -2449,7 +2449,7 @@ struct ScrollHandleState {
/// A handle to the scrollable aspects of an element.
/// Used for accessing scroll state, like the current scroll offset,
/// and for mutating the scroll state, like scrolling to a specific child.
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct ScrollHandle(Rc<RefCell<ScrollHandleState>>);
impl Default for ScrollHandle {
@@ -2526,6 +2526,14 @@ impl ScrollHandle {
}
}
/// Set the offset explicitly. The offset is the distance from the top left of the
/// parent container to the top left of the first child.
/// As you scroll further down the offset becomes more negative.
pub fn set_offset(&self, mut position: Point<Pixels>) {
let state = self.0.borrow();
*state.offset.borrow_mut() = position;
}
/// Get the logical scroll top, based on a child index and a pixel offset.
pub fn logical_scroll_top(&self) -> (usize, Pixels) {
let ix = self.top_item();

View File

@@ -80,7 +80,7 @@ pub struct ListScrollEvent {
}
/// The sizing behavior to apply during layout.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ListSizingBehavior {
/// The list should calculate its size based on the size of its items.
Infer,

View File

@@ -6,8 +6,9 @@
use crate::{
point, px, size, AnyElement, AvailableSpace, Bounds, ContentMask, Element, ElementId,
GlobalElementId, Hitbox, InteractiveElement, Interactivity, IntoElement, LayoutId, Pixels,
Render, ScrollHandle, Size, StyleRefinement, Styled, View, ViewContext, WindowContext,
GlobalElementId, Hitbox, InteractiveElement, Interactivity, IntoElement, LayoutId,
ListSizingBehavior, Pixels, Render, ScrollHandle, Size, StyleRefinement, Styled, View,
ViewContext, WindowContext,
};
use smallvec::SmallVec;
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
@@ -55,6 +56,7 @@ where
..Default::default()
},
scroll_handle: None,
sizing_behavior: ListSizingBehavior::default(),
}
}
@@ -66,6 +68,7 @@ pub struct UniformList {
Box<dyn for<'a> Fn(Range<usize>, &'a mut WindowContext) -> SmallVec<[AnyElement; 64]>>,
interactivity: Interactivity,
scroll_handle: Option<UniformListScrollHandle>,
sizing_behavior: ListSizingBehavior,
}
/// Frame state used by the [UniformList].
@@ -120,24 +123,35 @@ impl Element for UniformList {
let item_size = self.measure_item(None, cx);
let layout_id = self
.interactivity
.request_layout(global_id, cx, |style, cx| {
cx.request_measured_layout(style, move |known_dimensions, available_space, _cx| {
let desired_height = item_size.height * max_items;
let width = known_dimensions
.width
.unwrap_or(match available_space.width {
AvailableSpace::Definite(x) => x,
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
item_size.width
}
});
let height = match available_space.height {
AvailableSpace::Definite(height) => desired_height.min(height),
AvailableSpace::MinContent | AvailableSpace::MaxContent => desired_height,
};
size(width, height)
})
.request_layout(global_id, cx, |style, cx| match self.sizing_behavior {
ListSizingBehavior::Infer => {
cx.with_text_style(style.text_style().cloned(), |cx| {
cx.request_measured_layout(
style,
move |known_dimensions, available_space, _cx| {
let desired_height = item_size.height * max_items;
let width = known_dimensions.width.unwrap_or(match available_space
.width
{
AvailableSpace::Definite(x) => x,
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
item_size.width
}
});
let height = match available_space.height {
AvailableSpace::Definite(height) => desired_height.min(height),
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
desired_height
}
};
size(width, height)
},
)
})
}
ListSizingBehavior::Auto => cx.with_text_style(style.text_style().cloned(), |cx| {
cx.request_layout(style, None)
}),
});
(
@@ -280,6 +294,12 @@ impl UniformList {
self
}
/// Sets the sizing behavior, similar to the `List` element.
pub fn with_sizing_behavior(mut self, behavior: ListSizingBehavior) -> Self {
self.sizing_behavior = behavior;
self
}
fn measure_item(&self, list_width: Option<Pixels>, cx: &mut WindowContext) -> Size<Pixels> {
if self.item_count == 0 {
return Size::default();

View File

@@ -712,7 +712,7 @@ impl Size<Length> {
/// assert_eq!(bounds.origin, origin);
/// assert_eq!(bounds.size, size);
/// ```
#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)]
#[derive(Refineable, Clone, Default, Debug, Eq, PartialEq, Hash)]
#[refineable(Debug)]
#[repr(C)]
pub struct Bounds<T: Clone + Default + Debug> {

View File

@@ -304,6 +304,14 @@ impl KeyBindingContextPredicate {
source,
))
}
_ if is_vim_operator_char(next) => {
let (operator, rest) = source.split_at(1);
source = skip_whitespace(rest);
Ok((
KeyBindingContextPredicate::Identifier(operator.to_string().into()),
source,
))
}
_ => Err(anyhow!("unexpected character {next:?}")),
}
}
@@ -347,6 +355,10 @@ fn is_identifier_char(c: char) -> bool {
c.is_alphanumeric() || c == '_' || c == '-'
}
fn is_vim_operator_char(c: char) -> bool {
c == '>' || c == '<'
}
fn skip_whitespace(source: &str) -> &str {
let len = source
.find(|c: char| !c.is_whitespace())

View File

@@ -1,5 +1,3 @@
// todo(linux): remove
#![cfg_attr(target_os = "linux", allow(dead_code))]
// todo(windows): remove
#![cfg_attr(windows, allow(dead_code))]
@@ -135,6 +133,10 @@ pub(crate) trait Platform: 'static {
fn on_reopen(&self, callback: Box<dyn FnMut()>);
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
None
}
fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap);
fn add_recent_document(&self, _path: &Path) {}
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
@@ -203,7 +205,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn content_size(&self) -> Size<Pixels>;
fn scale_factor(&self) -> f32;
fn appearance(&self) -> WindowAppearance;
fn display(&self) -> Rc<dyn PlatformDisplay>;
fn display(&self) -> Option<Rc<dyn PlatformDisplay>>;
fn mouse_position(&self) -> Point<Pixels>;
fn modifiers(&self) -> Modifiers;
fn set_input_handler(&mut self, input_handler: PlatformInputHandler);
@@ -413,6 +415,7 @@ impl PlatformInputHandler {
.flatten()
}
#[cfg_attr(target_os = "linux", allow(dead_code))]
fn text_for_range(&mut self, range_utf16: Range<usize>) -> Option<String> {
self.cx
.update(|cx| self.handler.text_for_range(range_utf16, cx))
@@ -573,13 +576,17 @@ pub(crate) struct WindowParams {
pub titlebar: Option<TitlebarOptions>,
/// The kind of window to create
#[cfg_attr(target_os = "linux", allow(dead_code))]
pub kind: WindowKind,
/// Whether the window should be movable by the user
#[cfg_attr(target_os = "linux", allow(dead_code))]
pub is_movable: bool,
#[cfg_attr(target_os = "linux", allow(dead_code))]
pub focus: bool,
#[cfg_attr(target_os = "linux", allow(dead_code))]
pub show: bool,
pub display_id: Option<DisplayId>,
@@ -797,10 +804,6 @@ pub enum CursorStyle {
/// corresponds to the CSS curosr value `row-resize`
ResizeRow,
/// A cursor indicating that something will disappear if moved here
/// Does not correspond to a CSS cursor value
DisappearingItem,
/// A text input cursor for vertical layout
/// corresponds to the CSS cursor value `vertical-text`
IBeamCursorForVerticalLayout,
@@ -865,6 +868,7 @@ impl ClipboardItem {
.and_then(|m| serde_json::from_str(m).ok())
}
#[cfg_attr(target_os = "linux", allow(dead_code))]
pub(crate) fn text_hash(text: &str) -> u64 {
let mut hasher = SeaHasher::new();
text.hash(&mut hasher);

View File

@@ -1,3 +1,5 @@
use std::borrow::Cow;
use crate::{Action, AppContext, Platform};
use util::ResultExt;
@@ -10,6 +12,16 @@ pub struct Menu<'a> {
pub items: Vec<MenuItem<'a>>,
}
impl<'a> Menu<'a> {
/// Create an OwnedMenu from this Menu
pub fn owned(self) -> OwnedMenu {
OwnedMenu {
name: self.name.to_string().into(),
items: self.items.into_iter().map(|item| item.owned()).collect(),
}
}
}
/// The different kinds of items that can be in a menu
pub enum MenuItem<'a> {
/// A separator between items
@@ -60,6 +72,73 @@ impl<'a> MenuItem<'a> {
os_action: Some(os_action),
}
}
/// Create an OwnedMenuItem from this MenuItem
pub fn owned(self) -> OwnedMenuItem {
match self {
MenuItem::Separator => OwnedMenuItem::Separator,
MenuItem::Submenu(submenu) => OwnedMenuItem::Submenu(submenu.owned()),
MenuItem::Action {
name,
action,
os_action,
} => OwnedMenuItem::Action {
name: name.into(),
action,
os_action,
},
}
}
}
/// A menu of the application, either a main menu or a submenu
#[derive(Clone)]
pub struct OwnedMenu {
/// The name of the menu
pub name: Cow<'static, str>,
/// The items in the menu
pub items: Vec<OwnedMenuItem>,
}
/// The different kinds of items that can be in a menu
pub enum OwnedMenuItem {
/// A separator between items
Separator,
/// A submenu
Submenu(OwnedMenu),
/// An action that can be performed
Action {
/// The name of this menu item
name: String,
/// the action to perform when this menu item is selected
action: Box<dyn Action>,
/// The OS Action that corresponds to this action, if any
/// See [`OsAction`] for more information
os_action: Option<OsAction>,
},
}
impl Clone for OwnedMenuItem {
fn clone(&self) -> Self {
match self {
OwnedMenuItem::Separator => OwnedMenuItem::Separator,
OwnedMenuItem::Submenu(submenu) => OwnedMenuItem::Submenu(submenu.clone()),
OwnedMenuItem::Action {
name,
action,
os_action,
} => OwnedMenuItem::Action {
name: name.clone(),
action: action.boxed_clone(),
os_action: *os_action,
},
}
}
}
// TODO: As part of the global selections refactor, these should

View File

@@ -20,7 +20,9 @@ use std::{mem, sync::Arc};
const MAX_FRAME_TIME_MS: u32 = 1000;
#[cfg(target_os = "macos")]
pub type Context = ();
#[cfg(target_os = "macos")]
pub type Renderer = BladeRenderer;
#[cfg(target_os = "macos")]

View File

@@ -294,7 +294,7 @@ impl CosmicTextSystemState {
.0,
)
.clone()
.unwrap();
.with_context(|| format!("no image for {params:?} in font {font:?}"))?;
Ok(Bounds {
origin: point(image.placement.left.into(), (-image.placement.top).into()),
size: size(image.placement.width.into(), image.placement.height.into()),
@@ -314,7 +314,7 @@ impl CosmicTextSystemState {
let bitmap_size = glyph_bounds.size;
let font = &self.loaded_fonts_store[params.font_id.0];
let font_system = &mut self.font_system;
let image = self
let mut image = self
.swash_cache
.get_image(
font_system,
@@ -328,7 +328,14 @@ impl CosmicTextSystemState {
.0,
)
.clone()
.unwrap();
.with_context(|| format!("no image for {params:?} in font {font:?}"))?;
if params.is_emoji {
// Convert from RGBA to BGRA.
for pixel in image.data.chunks_exact_mut(4) {
pixel.swap(0, 2);
}
}
Ok((bitmap_size, image.data))
}
@@ -394,13 +401,20 @@ impl CosmicTextSystemState {
for glyph in &layout.glyphs {
let font_id = glyph.font_id;
let font_id = self.font_id_for_cosmic_id(font_id);
let is_emoji = self.is_emoji(font_id);
let mut glyphs = SmallVec::new();
// HACK: Prevent crash caused by variation selectors.
if glyph.glyph_id == 3 && is_emoji {
continue;
}
// todo(linux) this is definitely wrong, each glyph in glyphs from cosmic-text is a cluster with one glyph, ShapedRun takes a run of glyphs with the same font and direction
glyphs.push(ShapedGlyph {
id: GlyphId(glyph.glyph_id as u32),
position: point((glyph.x).into(), glyph.y.into()),
index: glyph.start,
is_emoji: self.is_emoji(font_id),
is_emoji,
});
runs.push(crate::ShapedRun { font_id, glyphs });

View File

@@ -1,6 +1,3 @@
// todo(linux): remove
#![allow(unused)]
mod dispatcher;
mod headless;
mod platform;

View File

@@ -1,9 +1,3 @@
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
// todo(linux): remove
#![allow(unused_variables)]
use crate::{PlatformDispatcher, TaskLabel};
use async_task::Runnable;
use calloop::{
@@ -63,7 +57,7 @@ impl LinuxDispatcher {
timer_handle
.insert_source(
calloop::timer::Timer::from_duration(timer.duration),
move |e, _, _| {
move |_, _, _| {
if let Some(runnable) = runnable.take() {
runnable.run();
}

View File

@@ -1,27 +1,16 @@
use std::cell::RefCell;
use std::ops::Deref;
use std::rc::Rc;
use std::time::{Duration, Instant};
use calloop::{EventLoop, LoopHandle};
use collections::HashMap;
use util::ResultExt;
use crate::platform::linux::LinuxClient;
use crate::platform::{LinuxCommon, PlatformWindow};
use crate::{
px, AnyWindowHandle, Bounds, CursorStyle, DisplayId, Modifiers, ModifiersChangedEvent, Pixels,
PlatformDisplay, PlatformInput, Point, ScrollDelta, Size, TouchPhase, WindowParams,
};
use calloop::{
generic::{FdWrapper, Generic},
RegistrationToken,
};
use crate::{AnyWindowHandle, CursorStyle, DisplayId, PlatformDisplay, WindowParams};
pub struct HeadlessClientState {
pub(crate) loop_handle: LoopHandle<'static, HeadlessClient>,
pub(crate) _loop_handle: LoopHandle<'static, HeadlessClient>,
pub(crate) event_loop: Option<calloop::EventLoop<'static, HeadlessClient>>,
pub(crate) common: LinuxCommon,
}
@@ -37,15 +26,17 @@ impl HeadlessClient {
let handle = event_loop.handle();
handle.insert_source(main_receiver, |event, _, _: &mut HeadlessClient| {
if let calloop::channel::Event::Msg(runnable) = event {
runnable.run();
}
});
handle
.insert_source(main_receiver, |event, _, _: &mut HeadlessClient| {
if let calloop::channel::Event::Msg(runnable) = event {
runnable.run();
}
})
.ok();
HeadlessClient(Rc::new(RefCell::new(HeadlessClientState {
event_loop: Some(event_loop),
loop_handle: handle,
_loop_handle: handle,
common,
})))
}
@@ -64,7 +55,7 @@ impl LinuxClient for HeadlessClient {
None
}
fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
fn display(&self, _id: DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
None
}
@@ -72,10 +63,14 @@ impl LinuxClient for HeadlessClient {
return Err(anyhow::anyhow!("neither DISPLAY, nor WAYLAND_DISPLAY found. You can still run zed for remote development with --dev-server-token."));
}
fn active_window(&self) -> Option<AnyWindowHandle> {
None
}
fn open_window(
&self,
_handle: AnyWindowHandle,
params: WindowParams,
_params: WindowParams,
) -> Box<dyn PlatformWindow> {
unimplemented!()
}
@@ -84,9 +79,9 @@ impl LinuxClient for HeadlessClient {
fn open_uri(&self, _uri: &str) {}
fn write_to_primary(&self, item: crate::ClipboardItem) {}
fn write_to_primary(&self, _item: crate::ClipboardItem) {}
fn write_to_clipboard(&self, item: crate::ClipboardItem) {}
fn write_to_clipboard(&self, _item: crate::ClipboardItem) {}
fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
None

View File

@@ -38,9 +38,9 @@ use crate::platform::linux::xdg_desktop_portal::{should_auto_hide_scrollbars, wi
use crate::{
px, Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CosmicTextSystem, CursorStyle,
DisplayId, ForegroundExecutor, Keymap, Keystroke, LinuxDispatcher, Menu, MenuItem, Modifiers,
PathPromptOptions, Pixels, Platform, PlatformDisplay, PlatformInputHandler, PlatformTextSystem,
PlatformWindow, Point, PromptLevel, Result, SemanticVersion, Size, Task, WindowAppearance,
WindowOptions, WindowParams,
OwnedMenu, PathPromptOptions, Pixels, Platform, PlatformDisplay, PlatformInputHandler,
PlatformTextSystem, PlatformWindow, Point, PromptLevel, Result, SemanticVersion, Size, Task,
WindowAppearance, WindowOptions, WindowParams,
};
use super::x11::X11Client;
@@ -72,6 +72,7 @@ pub trait LinuxClient {
fn write_to_clipboard(&self, item: ClipboardItem);
fn read_from_primary(&self) -> Option<ClipboardItem>;
fn read_from_clipboard(&self) -> Option<ClipboardItem>;
fn active_window(&self) -> Option<AnyWindowHandle>;
fn run(&self);
}
@@ -93,6 +94,7 @@ pub(crate) struct LinuxCommon {
pub(crate) auto_hide_scrollbars: bool,
pub(crate) callbacks: PlatformHandlers,
pub(crate) signal: LoopSignal,
pub(crate) menus: Vec<OwnedMenu>,
}
impl LinuxCommon {
@@ -118,6 +120,7 @@ impl LinuxCommon {
auto_hide_scrollbars,
callbacks,
signal,
menus: Vec::new(),
};
(common, main_receiver)
@@ -210,18 +213,21 @@ impl<P: LinuxClient + 'static> Platform for P {
}
}
// todo(linux)
fn activate(&self, ignoring_other_apps: bool) {}
// todo(linux)
fn hide(&self) {}
fn hide_other_apps(&self) {
log::warn!("hide_other_apps is not implemented on Linux, ignoring the call")
fn activate(&self, ignoring_other_apps: bool) {
log::info!("activate is not implemented on Linux, ignoring the call")
}
// todo(linux)
fn unhide_other_apps(&self) {}
fn hide(&self) {
log::info!("hide is not implemented on Linux, ignoring the call")
}
fn hide_other_apps(&self) {
log::info!("hide_other_apps is not implemented on Linux, ignoring the call")
}
fn unhide_other_apps(&self) {
log::info!("unhide_other_apps is not implemented on Linux, ignoring the call")
}
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
self.primary_display()
@@ -231,9 +237,8 @@ impl<P: LinuxClient + 'static> Platform for P {
self.displays()
}
// todo(linux)
fn active_window(&self) -> Option<AnyWindowHandle> {
None
self.active_window()
}
fn open_window(
@@ -387,15 +392,22 @@ impl<P: LinuxClient + 'static> Platform for P {
Ok(exe_path)
}
// todo(linux)
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap) {}
fn set_menus(&self, menus: Vec<Menu>, _keymap: &Keymap) {
self.with_common(|common| {
common.menus = menus.into_iter().map(|menu| menu.owned()).collect();
})
}
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
self.with_common(|common| Some(common.menus.clone()))
}
fn set_dock_menu(&self, menu: Vec<MenuItem>, keymap: &Keymap) {}
fn local_timezone(&self) -> UtcOffset {
UtcOffset::UTC
}
//todo(linux)
fn path_for_auxiliary_executable(&self, name: &str) -> Result<PathBuf> {
Err(anyhow::Error::msg(
"Platform<LinuxPlatform>::path_for_auxiliary_executable is not implemented yet",
@@ -549,7 +561,6 @@ impl CursorStyle {
CursorStyle::ResizeUpDown => Shape::NsResize,
CursorStyle::ResizeColumn => Shape::ColResize,
CursorStyle::ResizeRow => Shape::RowResize,
CursorStyle::DisappearingItem => Shape::Grabbing, // todo(linux) - couldn't find equivalent icon in linux
CursorStyle::IBeamCursorForVerticalLayout => Shape::VerticalText,
CursorStyle::OperationNotAllowed => Shape::NotAllowed,
CursorStyle::DragLink => Shape::Alias,
@@ -577,7 +588,6 @@ impl CursorStyle {
CursorStyle::ResizeUpDown => "ns-resize",
CursorStyle::ResizeColumn => "col-resize",
CursorStyle::ResizeRow => "row-resize",
CursorStyle::DisappearingItem => "grabbing", // todo(linux) - couldn't find equivalent icon in linux
CursorStyle::IBeamCursorForVerticalLayout => "vertical-text",
CursorStyle::OperationNotAllowed => "not-allowed",
CursorStyle::DragLink => "alias",

View File

@@ -1,14 +1,11 @@
use core::hash;
use std::cell::{RefCell, RefMut};
use std::ffi::OsString;
use std::ops::{Deref, DerefMut};
use std::hash::Hash;
use std::os::fd::{AsRawFd, BorrowedFd};
use std::path::PathBuf;
use std::rc::{Rc, Weak};
use std::sync::Arc;
use std::time::{Duration, Instant};
use async_task::Runnable;
use calloop::timer::{TimeoutAction, Timer};
use calloop::{EventLoop, LoopHandle};
use calloop_wayland_source::WaylandSource;
@@ -16,7 +13,7 @@ use collections::HashMap;
use copypasta::wayland_clipboard::{create_clipboards_from_external, Clipboard, Primary};
use copypasta::ClipboardProvider;
use filedescriptor::Pipe;
use parking_lot::Mutex;
use smallvec::SmallVec;
use util::ResultExt;
use wayland_backend::client::ObjectId;
@@ -26,9 +23,8 @@ use wayland_client::globals::{registry_queue_init, GlobalList, GlobalListContent
use wayland_client::protocol::wl_callback::{self, WlCallback};
use wayland_client::protocol::wl_data_device_manager::DndAction;
use wayland_client::protocol::wl_pointer::AxisSource;
use wayland_client::protocol::wl_seat::WlSeat;
use wayland_client::protocol::{
wl_data_device, wl_data_device_manager, wl_data_offer, wl_data_source, wl_output, wl_region,
wl_data_device, wl_data_device_manager, wl_data_offer, wl_output, wl_region,
};
use wayland_client::{
delegate_noop,
@@ -38,7 +34,6 @@ use wayland_client::{
},
Connection, Dispatch, Proxy, QueueHandle,
};
use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape;
use wayland_protocols::wp::cursor_shape::v1::client::{
wp_cursor_shape_device_v1, wp_cursor_shape_manager_v1,
};
@@ -62,7 +57,8 @@ 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::window::{ImeInput, WaylandWindowState, WaylandWindowStatePtr};
use super::display::WaylandDisplay;
use super::window::{ImeInput, WaylandWindowStatePtr};
use crate::platform::linux::is_within_click_distance;
use crate::platform::linux::wayland::cursor::Cursor;
use crate::platform::linux::wayland::serial::{SerialKind, SerialTracker};
@@ -71,7 +67,7 @@ use crate::platform::linux::xdg_desktop_portal::{Event as XDPEvent, XDPEventSour
use crate::platform::linux::LinuxClient;
use crate::platform::PlatformWindow;
use crate::{
point, px, Bounds, FileDropEvent, ForegroundExecutor, MouseExitEvent, WindowAppearance,
point, px, size, Bounds, DevicePixels, FileDropEvent, ForegroundExecutor, MouseExitEvent, Size,
SCROLL_LINES,
};
use crate::{
@@ -143,11 +139,42 @@ impl Globals {
}
}
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
pub struct InProgressOutput {
name: Option<String>,
scale: Option<i32>,
position: Option<Point<DevicePixels>>,
size: Option<Size<DevicePixels>>,
}
impl InProgressOutput {
fn complete(&self) -> Option<Output> {
if let Some((position, size)) = self.position.zip(self.size) {
let scale = self.scale.unwrap_or(1);
Some(Output {
name: self.name.clone(),
scale,
bounds: Bounds::new(position, size),
})
} else {
None
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Output {
pub name: Option<String>,
pub scale: i32,
pub bounds: Bounds<DevicePixels>,
}
pub(crate) struct WaylandClientState {
serial_tracker: SerialTracker,
globals: Globals,
wl_seat: wl_seat::WlSeat, // todo(linux): multi-seat support
wl_seat: wl_seat::WlSeat, // TODO: Multi seat support
wl_pointer: Option<wl_pointer::WlPointer>,
wl_keyboard: Option<wl_keyboard::WlKeyboard>,
cursor_shape_device: Option<wp_cursor_shape_device_v1::WpCursorShapeDeviceV1>,
data_device: Option<wl_data_device::WlDataDevice>,
text_input: Option<zwp_text_input_v3::ZwpTextInputV3>,
@@ -156,15 +183,16 @@ pub(crate) struct WaylandClientState {
// Surface to Window mapping
windows: HashMap<ObjectId, WaylandWindowStatePtr>,
// Output to scale mapping
output_scales: HashMap<ObjectId, i32>,
outputs: HashMap<ObjectId, Output>,
in_progress_outputs: HashMap<ObjectId, InProgressOutput>,
keymap_state: Option<xkb::State>,
compose_state: Option<xkb::compose::State>,
drag: DragState,
click: ClickState,
repeat: KeyRepeat,
modifiers: Modifiers,
pub modifiers: Modifiers,
axis_source: AxisSource,
mouse_location: Option<Point<Pixels>>,
pub mouse_location: Option<Point<Pixels>>,
continuous_scroll_delta: Option<Point<Pixels>>,
discrete_scroll_delta: Option<Point<f32>>,
vertical_modifier: f32,
@@ -210,7 +238,7 @@ pub(crate) struct KeyRepeat {
pub struct WaylandClientStatePtr(Weak<RefCell<WaylandClientState>>);
impl WaylandClientStatePtr {
fn get_client(&self) -> Rc<RefCell<WaylandClientState>> {
pub fn get_client(&self) -> Rc<RefCell<WaylandClientState>> {
self.0
.upgrade()
.expect("The pointer should always be valid when dispatching in wayland")
@@ -302,7 +330,6 @@ impl Drop for WaylandClient {
}
const WL_DATA_DEVICE_MANAGER_VERSION: u32 = 3;
const WL_OUTPUT_VERSION: u32 = 2;
fn wl_seat_version(version: u32) -> u32 {
// We rely on the wl_pointer.frame event
@@ -319,6 +346,20 @@ fn wl_seat_version(version: u32) -> u32 {
version.clamp(WL_SEAT_MIN_VERSION, WL_SEAT_MAX_VERSION)
}
fn wl_output_version(version: u32) -> u32 {
const WL_OUTPUT_MIN_VERSION: u32 = 2;
const WL_OUTPUT_MAX_VERSION: u32 = 4;
if version < WL_OUTPUT_MIN_VERSION {
panic!(
"wl_output below required version: {} < {}",
version, WL_OUTPUT_MIN_VERSION
);
}
version.clamp(WL_OUTPUT_MIN_VERSION, WL_OUTPUT_MAX_VERSION)
}
impl WaylandClient {
pub(crate) fn new() -> Self {
let conn = Connection::connect_to_env().unwrap();
@@ -328,7 +369,7 @@ impl WaylandClient {
let qh = event_queue.handle();
let mut seat: Option<wl_seat::WlSeat> = None;
let mut outputs = HashMap::default();
let mut in_progress_outputs = HashMap::default();
globals.contents().with_list(|list| {
for global in list {
match &global.interface[..] {
@@ -343,11 +384,11 @@ impl WaylandClient {
"wl_output" => {
let output = globals.registry().bind::<wl_output::WlOutput, _, _>(
global.name,
WL_OUTPUT_VERSION,
wl_output_version(global.version),
&qh,
(),
);
outputs.insert(output.id(), 1);
in_progress_outputs.insert(output.id(), InProgressOutput::default());
}
_ => {}
}
@@ -361,11 +402,13 @@ impl WaylandClient {
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
let handle = event_loop.handle();
handle.insert_source(main_receiver, |event, _, _: &mut WaylandClientStatePtr| {
if let calloop::channel::Event::Msg(runnable) = event {
runnable.run();
}
});
handle
.insert_source(main_receiver, |event, _, _: &mut WaylandClientStatePtr| {
if let calloop::channel::Event::Msg(runnable) = event {
runnable.run();
}
})
.unwrap();
let seat = seat.unwrap();
let globals = Globals::new(
@@ -384,33 +427,37 @@ impl WaylandClient {
let cursor = Cursor::new(&conn, &globals, 24);
handle.insert_source(XDPEventSource::new(&common.background_executor), {
move |event, _, client| match event {
XDPEvent::WindowAppearance(appearance) => {
if let Some(client) = client.0.upgrade() {
let mut client = client.borrow_mut();
handle
.insert_source(XDPEventSource::new(&common.background_executor), {
move |event, _, client| match event {
XDPEvent::WindowAppearance(appearance) => {
if let Some(client) = client.0.upgrade() {
let mut client = client.borrow_mut();
client.common.appearance = appearance;
client.common.appearance = appearance;
for (_, window) in &mut client.windows {
window.set_appearance(appearance);
for (_, window) in &mut client.windows {
window.set_appearance(appearance);
}
}
}
}
}
});
})
.unwrap();
let mut state = Rc::new(RefCell::new(WaylandClientState {
serial_tracker: SerialTracker::new(),
globals,
wl_seat: seat,
wl_pointer: None,
wl_keyboard: None,
cursor_shape_device: None,
data_device,
text_input: None,
pre_edit_text: None,
composing: false,
output_scales: outputs,
outputs: HashMap::default(),
in_progress_outputs,
windows: HashMap::default(),
common,
keymap_state: None,
@@ -459,7 +506,9 @@ impl WaylandClient {
pending_open_uri: None,
}));
WaylandSource::new(conn, event_queue).insert(handle);
WaylandSource::new(conn, event_queue)
.insert(handle)
.unwrap();
Self(state)
}
@@ -467,11 +516,34 @@ impl WaylandClient {
impl LinuxClient for WaylandClient {
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
Vec::new()
self.0
.borrow()
.outputs
.iter()
.map(|(id, output)| {
Rc::new(WaylandDisplay {
id: id.clone(),
name: output.name.clone(),
bounds: output.bounds,
}) as Rc<dyn PlatformDisplay>
})
.collect()
}
fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
unimplemented!()
self.0
.borrow()
.outputs
.iter()
.find_map(|(object_id, output)| {
(object_id.protocol_id() == id.0).then(|| {
Rc::new(WaylandDisplay {
id: object_id.clone(),
name: output.name.clone(),
bounds: output.bounds,
}) as Rc<dyn PlatformDisplay>
})
})
}
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
@@ -486,6 +558,7 @@ impl LinuxClient for WaylandClient {
let mut state = self.0.borrow_mut();
let (window, surface_id) = WaylandWindow::new(
handle,
state.globals.clone(),
WaylandClientStatePtr(Rc::downgrade(&self.0)),
params,
@@ -566,7 +639,8 @@ impl LinuxClient for WaylandClient {
.primary
.as_mut()
.unwrap()
.set_contents(item.text);
.set_contents(item.text)
.ok();
}
fn write_to_clipboard(&self, item: crate::ClipboardItem) {
@@ -575,7 +649,8 @@ impl LinuxClient for WaylandClient {
.clipboard
.as_mut()
.unwrap()
.set_contents(item.text);
.set_contents(item.text)
.ok();
}
fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
@@ -605,6 +680,14 @@ impl LinuxClient for WaylandClient {
metadata: None,
})
}
fn active_window(&self) -> Option<AnyWindowHandle> {
self.0
.borrow_mut()
.keyboard_focused_window
.as_ref()
.map(|window| window.handle())
}
}
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for WaylandClientStatePtr {
@@ -626,18 +709,37 @@ impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for WaylandClientStat
version,
} => match &interface[..] {
"wl_seat" => {
state.wl_pointer = None;
registry.bind::<wl_seat::WlSeat, _, _>(name, wl_seat_version(version), qh, ());
if let Some(wl_pointer) = state.wl_pointer.take() {
wl_pointer.release();
}
if let Some(wl_keyboard) = state.wl_keyboard.take() {
wl_keyboard.release();
}
state.wl_seat.release();
state.wl_seat = registry.bind::<wl_seat::WlSeat, _, _>(
name,
wl_seat_version(version),
qh,
(),
);
}
"wl_output" => {
let output =
registry.bind::<wl_output::WlOutput, _, _>(name, WL_OUTPUT_VERSION, qh, ());
let output = registry.bind::<wl_output::WlOutput, _, _>(
name,
wl_output_version(version),
qh,
(),
);
state.output_scales.insert(output.id(), 1);
state
.in_progress_outputs
.insert(output.id(), InProgressOutput::default());
}
_ => {}
},
wl_registry::Event::GlobalRemove { name: _ } => {}
wl_registry::Event::GlobalRemove { name: _ } => {
// TODO: handle global removal
}
_ => {}
}
}
@@ -667,7 +769,7 @@ impl Dispatch<WlCallback, ObjectId> for WaylandClientStatePtr {
event: wl_callback::Event,
surface_id: &ObjectId,
_: &Connection,
qh: &QueueHandle<Self>,
_: &QueueHandle<Self>,
) {
let client = state.get_client();
let mut state = client.borrow_mut();
@@ -677,7 +779,7 @@ impl Dispatch<WlCallback, ObjectId> for WaylandClientStatePtr {
drop(state);
match event {
wl_callback::Event::Done { callback_data } => {
wl_callback::Event::Done { .. } => {
window.frame(true);
}
_ => {}
@@ -707,10 +809,10 @@ impl Dispatch<wl_surface::WlSurface, ()> for WaylandClientStatePtr {
let Some(window) = get_window(&mut state, &surface.id()) else {
return;
};
let scales = state.output_scales.clone();
let outputs = state.outputs.clone();
drop(state);
window.handle_surface_event(event, scales);
window.handle_surface_event(event, outputs);
}
}
@@ -726,13 +828,28 @@ impl Dispatch<wl_output::WlOutput, ()> for WaylandClientStatePtr {
let mut client = this.get_client();
let mut state = client.borrow_mut();
let Some(mut output_scale) = state.output_scales.get_mut(&output.id()) else {
let Some(mut in_progress_output) = state.in_progress_outputs.get_mut(&output.id()) else {
return;
};
match event {
wl_output::Event::Name { name } => {
in_progress_output.name = Some(name);
}
wl_output::Event::Scale { factor } => {
*output_scale = factor;
in_progress_output.scale = Some(factor);
}
wl_output::Event::Geometry { x, y, .. } => {
in_progress_output.position = Some(point(DevicePixels(x), DevicePixels(y)))
}
wl_output::Event::Mode { width, height, .. } => {
in_progress_output.size = Some(size(DevicePixels(width), DevicePixels(height)))
}
wl_output::Event::Done => {
if let Some(complete) = in_progress_output.complete() {
state.outputs.insert(output.id(), complete);
}
state.in_progress_outputs.remove(&output.id());
}
_ => {}
}
@@ -742,7 +859,7 @@ impl Dispatch<wl_output::WlOutput, ()> for WaylandClientStatePtr {
impl Dispatch<xdg_surface::XdgSurface, ObjectId> for WaylandClientStatePtr {
fn event(
state: &mut Self,
xdg_surface: &xdg_surface::XdgSurface,
_: &xdg_surface::XdgSurface,
event: xdg_surface::Event,
surface_id: &ObjectId,
_: &Connection,
@@ -761,7 +878,7 @@ impl Dispatch<xdg_surface::XdgSurface, ObjectId> for WaylandClientStatePtr {
impl Dispatch<xdg_toplevel::XdgToplevel, ObjectId> for WaylandClientStatePtr {
fn event(
this: &mut Self,
xdg_toplevel: &xdg_toplevel::XdgToplevel,
_: &xdg_toplevel::XdgToplevel,
event: <xdg_toplevel::XdgToplevel as Proxy>::Event,
surface_id: &ObjectId,
_: &Connection,
@@ -824,8 +941,8 @@ impl Dispatch<wl_seat::WlSeat, ()> for WaylandClientStatePtr {
state: &mut Self,
seat: &wl_seat::WlSeat,
event: wl_seat::Event,
data: &(),
conn: &Connection,
_: &(),
_: &Connection,
qh: &QueueHandle<Self>,
) {
if let wl_seat::Event::Capabilities {
@@ -835,12 +952,19 @@ impl Dispatch<wl_seat::WlSeat, ()> for WaylandClientStatePtr {
let client = state.get_client();
let mut state = client.borrow_mut();
if capabilities.contains(wl_seat::Capability::Keyboard) {
seat.get_keyboard(qh, ());
let keyboard = seat.get_keyboard(qh, ());
state.text_input = state
.globals
.text_input_manager
.as_ref()
.map(|text_input_manager| text_input_manager.get_text_input(&seat, qh, ()));
if let Some(wl_keyboard) = &state.wl_keyboard {
wl_keyboard.release();
}
state.wl_keyboard = Some(keyboard);
}
if capabilities.contains(wl_seat::Capability::Pointer) {
let pointer = seat.get_pointer(qh, ());
@@ -849,6 +973,11 @@ impl Dispatch<wl_seat::WlSeat, ()> for WaylandClientStatePtr {
.cursor_shape_manager
.as_ref()
.map(|cursor_shape_manager| cursor_shape_manager.get_pointer(&pointer, qh, ()));
if let Some(wl_pointer) = &state.wl_pointer {
wl_pointer.release();
}
state.wl_pointer = Some(pointer);
}
}
@@ -858,11 +987,11 @@ impl Dispatch<wl_seat::WlSeat, ()> for WaylandClientStatePtr {
impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
fn event(
this: &mut Self,
keyboard: &wl_keyboard::WlKeyboard,
_: &wl_keyboard::WlKeyboard,
event: wl_keyboard::Event,
data: &(),
conn: &Connection,
qh: &QueueHandle<Self>,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
let mut client = this.get_client();
let mut state = client.borrow_mut();
@@ -1018,8 +1147,8 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
state.compose_state = Some(compose);
}
let input = PlatformInput::KeyDown(KeyDownEvent {
keystroke: keystroke,
is_held: false, // todo(linux)
keystroke: keystroke.clone(),
is_held: false,
});
state.repeat.current_id += 1;
@@ -1030,8 +1159,11 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
state
.loop_handle
.insert_source(Timer::from_duration(state.repeat.delay), {
let input = input.clone();
move |event, _metadata, this| {
let input = PlatformInput::KeyDown(KeyDownEvent {
keystroke,
is_held: true,
});
move |_event, _metadata, this| {
let mut client = this.get_client();
let mut state = client.borrow_mut();
let is_repeating = id == state.repeat.current_id
@@ -1080,18 +1212,18 @@ impl Dispatch<zwp_text_input_v3::ZwpTextInputV3, ()> for WaylandClientStatePtr {
this: &mut Self,
text_input: &zwp_text_input_v3::ZwpTextInputV3,
event: <zwp_text_input_v3::ZwpTextInputV3 as Proxy>::Event,
data: &(),
conn: &Connection,
qhandle: &QueueHandle<Self>,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
let client = this.get_client();
let mut state = client.borrow_mut();
match event {
zwp_text_input_v3::Event::Enter { surface } => {
zwp_text_input_v3::Event::Enter { .. } => {
drop(state);
this.enable_ime();
}
zwp_text_input_v3::Event::Leave { surface } => {
zwp_text_input_v3::Event::Leave { .. } => {
drop(state);
this.disable_ime();
}
@@ -1119,11 +1251,7 @@ impl Dispatch<zwp_text_input_v3::ZwpTextInputV3, ()> for WaylandClientStatePtr {
}
}
}
zwp_text_input_v3::Event::PreeditString {
text,
cursor_begin,
cursor_end,
} => {
zwp_text_input_v3::Event::PreeditString { text, .. } => {
state.composing = true;
state.pre_edit_text = text;
}
@@ -1183,9 +1311,9 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
this: &mut Self,
wl_pointer: &wl_pointer::WlPointer,
event: wl_pointer::Event,
data: &(),
conn: &Connection,
qh: &QueueHandle<Self>,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
let mut client = this.get_client();
let mut state = client.borrow_mut();
@@ -1220,7 +1348,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
window.set_focused(true);
}
}
wl_pointer::Event::Leave { surface, .. } => {
wl_pointer::Event::Leave { .. } => {
if let Some(focused_window) = state.mouse_focused_window.clone() {
let input = PlatformInput::MouseExited(MouseExitEvent {
position: state.mouse_location.unwrap(),
@@ -1237,7 +1365,6 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
}
}
wl_pointer::Event::Motion {
time,
surface_x,
surface_y,
..
@@ -1280,7 +1407,6 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
wl_pointer::ButtonState::Pressed => {
if let Some(window) = state.keyboard_focused_window.clone() {
if state.composing && state.text_input.is_some() {
let text_input = state.text_input.as_ref().unwrap();
drop(state);
// text_input_v3 don't have something like a reset function
this.disable_ime();
@@ -1351,7 +1477,6 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
state.axis_source = axis_source;
}
wl_pointer::Event::Axis {
time,
axis: WEnum::Value(axis),
value,
..
@@ -1364,13 +1489,10 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier,
_ => 1.0,
};
let supports_relative_direction =
wl_pointer.version() >= wl_pointer::EVT_AXIS_RELATIVE_DIRECTION_SINCE;
state.scroll_event_received = true;
let scroll_delta = state
.continuous_scroll_delta
.get_or_insert(point(px(0.0), px(0.0)));
// TODO: Make nice feeling kinetic scrolling that integrates with the platform's scroll settings
let modifier = 3.0;
match axis {
wl_pointer::Axis::VerticalScroll => {

View File

@@ -1,31 +1,41 @@
use std::fmt::Debug;
use std::{
fmt::Debug,
hash::{Hash, Hasher},
};
use uuid::Uuid;
use wayland_backend::client::ObjectId;
use crate::{Bounds, DevicePixels, DisplayId, PlatformDisplay, Size};
use crate::{Bounds, DevicePixels, DisplayId, PlatformDisplay};
#[derive(Debug)]
pub(crate) struct WaylandDisplay {}
#[derive(Debug, Clone)]
pub(crate) struct WaylandDisplay {
/// The ID of the wl_output object
pub id: ObjectId,
pub name: Option<String>,
pub bounds: Bounds<DevicePixels>,
}
impl PlatformDisplay for WaylandDisplay {
// todo(linux)
fn id(&self) -> DisplayId {
DisplayId(123) // return some fake data so it doesn't panic
}
// todo(linux)
fn uuid(&self) -> anyhow::Result<Uuid> {
Ok(Uuid::from_bytes([0; 16])) // return some fake data so it doesn't panic
}
// todo(linux)
fn bounds(&self) -> Bounds<DevicePixels> {
Bounds {
origin: Default::default(),
size: Size {
width: DevicePixels(1000),
height: DevicePixels(500),
},
} // return some fake data so it doesn't panic
impl Hash for WaylandDisplay {
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
impl PlatformDisplay for WaylandDisplay {
fn id(&self) -> DisplayId {
DisplayId(self.id.protocol_id())
}
fn uuid(&self) -> anyhow::Result<Uuid> {
if let Some(name) = &self.name {
Ok(Uuid::new_v5(&Uuid::NAMESPACE_DNS, name.as_bytes()))
} else {
Err(anyhow::anyhow!("Wayland display does not have a name"))
}
}
fn bounds(&self) -> Bounds<DevicePixels> {
self.bounds
}
}

View File

@@ -1,5 +1,3 @@
use std::time::Instant;
use collections::HashMap;
#[derive(Debug, Hash, PartialEq, Eq)]
@@ -14,15 +12,11 @@ pub(crate) enum SerialKind {
#[derive(Debug)]
struct SerialData {
serial: u32,
time: Instant,
}
impl SerialData {
fn new(value: u32) -> Self {
Self {
serial: value,
time: Instant::now(),
}
Self { serial: value }
}
}
@@ -52,41 +46,4 @@ impl SerialTracker {
.map(|serial_data| serial_data.serial)
.unwrap_or(0)
}
/// Returns the newest serial of any of the provided [`SerialKind`]
pub fn get_newest_of(&self, kinds: &[SerialKind]) -> u32 {
kinds
.iter()
.filter_map(|kind| self.serials.get(&kind))
.max_by_key(|serial_data| serial_data.time)
.map(|serial_data| serial_data.serial)
.unwrap_or(0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_serial_tracker() {
let mut tracker = SerialTracker::new();
tracker.update(SerialKind::KeyPress, 100);
tracker.update(SerialKind::MousePress, 50);
tracker.update(SerialKind::MouseEnter, 300);
assert_eq!(
tracker.get_newest_of(&[SerialKind::KeyPress, SerialKind::MousePress]),
50
);
assert_eq!(tracker.get(SerialKind::DataDevice), 0);
tracker.update(SerialKind::KeyPress, 2000);
assert_eq!(tracker.get(SerialKind::KeyPress), 2000);
assert_eq!(
tracker.get_newest_of(&[SerialKind::KeyPress, SerialKind::MousePress]),
2000
);
}
}

View File

@@ -1,27 +1,24 @@
use std::any::Any;
use std::cell::{Ref, RefCell, RefMut};
use std::ffi::c_void;
use std::num::NonZeroU32;
use std::ops::{Deref, Range};
use std::ptr::NonNull;
use std::rc::{Rc, Weak};
use std::rc::Rc;
use std::sync::Arc;
use blade_graphics as gpu;
use collections::{HashMap, HashSet};
use collections::HashMap;
use futures::channel::oneshot::Receiver;
use parking_lot::Mutex;
use raw_window_handle as rwh;
use wayland_backend::client::ObjectId;
use wayland_client::protocol::wl_region::WlRegion;
use wayland_client::WEnum;
use wayland_client::{protocol::wl_surface, Proxy};
use wayland_protocols::wp::fractional_scale::v1::client::wp_fractional_scale_v1;
use wayland_protocols::wp::viewporter::client::wp_viewport;
use wayland_protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1;
use wayland_protocols::xdg::shell::client::xdg_surface;
use wayland_protocols::xdg::shell::client::xdg_toplevel::{self, WmCapabilities};
use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blur_manager};
use wayland_protocols::xdg::shell::client::xdg_toplevel::{self};
use wayland_protocols_plasma::blur::client::org_kde_kwin_blur;
use crate::platform::blade::{BladeRenderer, BladeSurfaceConfig};
use crate::platform::linux::wayland::display::WaylandDisplay;
@@ -29,9 +26,9 @@ use crate::platform::linux::wayland::serial::SerialKind;
use crate::platform::{PlatformAtlas, PlatformInputHandler, PlatformWindow};
use crate::scene::Scene;
use crate::{
px, size, Bounds, DevicePixels, Globals, Modifiers, Pixels, PlatformDisplay, PlatformInput,
Point, PromptLevel, Size, WaylandClientStatePtr, WindowAppearance, WindowBackgroundAppearance,
WindowBounds, WindowParams,
px, size, AnyWindowHandle, Bounds, DevicePixels, Globals, Modifiers, Output, Pixels,
PlatformDisplay, PlatformInput, Point, PromptLevel, Size, WaylandClientStatePtr,
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowParams,
};
#[derive(Default)]
@@ -75,7 +72,8 @@ pub struct WaylandWindowState {
blur: Option<org_kde_kwin_blur::OrgKdeKwinBlur>,
toplevel: xdg_toplevel::XdgToplevel,
viewport: Option<wp_viewport::WpViewport>,
outputs: HashSet<ObjectId>,
outputs: HashMap<ObjectId, Output>,
display: Option<(ObjectId, Output)>,
globals: Globals,
renderer: BladeRenderer,
bounds: Bounds<u32>,
@@ -83,10 +81,11 @@ pub struct WaylandWindowState {
input_handler: Option<PlatformInputHandler>,
decoration_state: WaylandDecorationState,
fullscreen: bool,
restore_bounds: Bounds<DevicePixels>,
maximized: bool,
windowed_bounds: Bounds<DevicePixels>,
client: WaylandClientStatePtr,
callbacks: Callbacks,
handle: AnyWindowHandle,
active: bool,
}
#[derive(Clone)]
@@ -98,6 +97,7 @@ pub struct WaylandWindowStatePtr {
impl WaylandWindowState {
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
handle: AnyWindowHandle,
surface: wl_surface::WlSurface,
xdg_surface: xdg_surface::XdgSurface,
toplevel: xdg_toplevel::XdgToplevel,
@@ -150,18 +150,20 @@ impl WaylandWindowState {
toplevel,
viewport,
globals,
outputs: HashSet::default(),
outputs: HashMap::default(),
display: None,
renderer: BladeRenderer::new(gpu, config),
bounds,
scale: 1.0,
input_handler: None,
decoration_state: WaylandDecorationState::Client,
fullscreen: false,
restore_bounds: Bounds::default(),
maximized: false,
callbacks: Callbacks::default(),
windowed_bounds: options.bounds,
client,
appearance,
handle,
active: false,
}
}
}
@@ -217,6 +219,7 @@ impl WaylandWindow {
}
pub fn new(
handle: AnyWindowHandle,
globals: Globals,
client: WaylandClientStatePtr,
params: WindowParams,
@@ -227,6 +230,7 @@ impl WaylandWindow {
.wm_base
.get_xdg_surface(&surface, &globals.qh, surface.id());
let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id());
toplevel.set_min_size(200, 200);
if let Some(fractional_scale_manager) = globals.fractional_scale_manager.as_ref() {
fractional_scale_manager.get_fractional_scale(&surface, &globals.qh, surface.id());
@@ -253,6 +257,7 @@ impl WaylandWindow {
let this = Self(WaylandWindowStatePtr {
state: Rc::new(RefCell::new(WaylandWindowState::new(
handle,
surface.clone(),
xdg_surface,
toplevel,
@@ -274,6 +279,10 @@ impl WaylandWindow {
}
impl WaylandWindowStatePtr {
pub fn handle(&self) -> AnyWindowHandle {
self.state.borrow().handle
}
pub fn surface(&self) -> wl_surface::WlSurface {
self.state.borrow().surface.clone()
}
@@ -340,23 +349,32 @@ impl WaylandWindowStatePtr {
pub fn handle_toplevel_event(&self, event: xdg_toplevel::Event) -> bool {
match event {
xdg_toplevel::Event::Configure {
width,
height,
mut width,
mut height,
states,
} => {
let width = NonZeroU32::new(width as u32);
let height = NonZeroU32::new(height as u32);
let fullscreen = states.contains(&(xdg_toplevel::State::Fullscreen as u8));
let maximized = states.contains(&(xdg_toplevel::State::Maximized as u8));
let mut state = self.state.borrow_mut();
state.maximized = maximized;
let got_unmaximized = state.maximized && !maximized;
state.fullscreen = fullscreen;
if fullscreen || maximized {
state.restore_bounds = state.bounds.map(|p| DevicePixels(p as i32));
state.maximized = maximized;
if got_unmaximized {
width = state.windowed_bounds.size.width.0;
height = state.windowed_bounds.size.height.0;
} else if width != 0 && height != 0 && !fullscreen && !maximized {
state.windowed_bounds = Bounds {
origin: Point::default(),
size: size(width.into(), height.into()),
};
}
let width = NonZeroU32::new(width as u32);
let height = NonZeroU32::new(height as u32);
drop(state);
self.resize(width, height);
self.set_fullscreen(fullscreen);
false
}
@@ -381,58 +399,48 @@ impl WaylandWindowStatePtr {
pub fn handle_surface_event(
&self,
event: wl_surface::Event,
output_scales: HashMap<ObjectId, i32>,
outputs: HashMap<ObjectId, Output>,
) {
let mut state = self.state.borrow_mut();
// We use `WpFractionalScale` instead to set the scale if it's available
if state.globals.fractional_scale_manager.is_some() {
return;
}
match event {
wl_surface::Event::Enter { output } => {
// We use `PreferredBufferScale` instead to set the scale if it's available
if state.surface.version() >= wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE {
let id = output.id();
let Some(output) = outputs.get(&id) else {
return;
};
state.outputs.insert(id, output.clone());
let scale = primary_output_scale(&mut state);
// We use `PreferredBufferScale` instead to set the scale if it's available
if state.surface.version() < wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE {
state.surface.set_buffer_scale(scale);
drop(state);
self.rescale(scale as f32);
}
state.outputs.insert(output.id());
let mut scale = 1;
for output in state.outputs.iter() {
if let Some(s) = output_scales.get(output) {
scale = scale.max(*s)
}
}
state.surface.set_buffer_scale(scale);
drop(state);
self.rescale(scale as f32);
}
wl_surface::Event::Leave { output } => {
// We use `PreferredBufferScale` instead to set the scale if it's available
if state.surface.version() >= wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE {
return;
}
state.outputs.remove(&output.id());
let mut scale = 1;
for output in state.outputs.iter() {
if let Some(s) = output_scales.get(output) {
scale = scale.max(*s)
}
}
let scale = primary_output_scale(&mut state);
state.surface.set_buffer_scale(scale);
drop(state);
self.rescale(scale as f32);
// We use `PreferredBufferScale` instead to set the scale if it's available
if state.surface.version() < wl_surface::EVT_PREFERRED_BUFFER_SCALE_SINCE {
state.surface.set_buffer_scale(scale);
drop(state);
self.rescale(scale as f32);
}
}
wl_surface::Event::PreferredBufferScale { factor } => {
state.surface.set_buffer_scale(factor);
drop(state);
self.rescale(factor as f32);
// We use `WpFractionalScale` instead to set the scale if it's available
if state.globals.fractional_scale_manager.is_none() {
state.surface.set_buffer_scale(factor);
drop(state);
self.rescale(factor as f32);
}
}
_ => {}
}
@@ -534,11 +542,6 @@ impl WaylandWindowStatePtr {
self.set_size_and_scale(None, None, Some(scale));
}
pub fn set_fullscreen(&self, fullscreen: bool) {
let mut state = self.state.borrow_mut();
state.fullscreen = fullscreen;
}
/// Notifies the window of the state of the decorations.
///
/// # Note
@@ -577,6 +580,7 @@ impl WaylandWindowStatePtr {
}
pub fn set_focused(&self, focus: bool) {
self.state.borrow_mut().active = focus;
if let Some(ref mut fun) = self.callbacks.borrow_mut().active_status_change {
fun(focus);
}
@@ -592,6 +596,23 @@ impl WaylandWindowStatePtr {
}
}
fn primary_output_scale(state: &mut RefMut<WaylandWindowState>) -> i32 {
let mut scale = 1;
let mut current_output = state.display.take();
for (id, output) in state.outputs.iter() {
if let Some((_, output_data)) = &current_output {
if output.scale > output_data.scale {
current_output = Some((id.clone(), output.clone()));
}
} else {
current_output = Some((id.clone(), output.clone()));
}
scale = scale.max(output.scale);
}
state.display = current_output;
scale
}
impl rwh::HasWindowHandle for WaylandWindow {
fn window_handle(&self) -> Result<rwh::WindowHandle<'_>, rwh::HandleError> {
unimplemented!()
@@ -612,14 +633,12 @@ impl PlatformWindow for WaylandWindow {
self.borrow().maximized
}
// todo(linux)
// check if it is right
fn window_bounds(&self) -> WindowBounds {
let state = self.borrow();
if state.fullscreen {
WindowBounds::Fullscreen(state.restore_bounds)
WindowBounds::Fullscreen(state.windowed_bounds)
} else if state.maximized {
WindowBounds::Maximized(state.restore_bounds)
WindowBounds::Maximized(state.windowed_bounds)
} else {
WindowBounds::Windowed(state.bounds.map(|p| DevicePixels(p as i32)))
}
@@ -641,19 +660,27 @@ impl PlatformWindow for WaylandWindow {
self.borrow().appearance
}
// todo(linux)
fn display(&self) -> Rc<dyn PlatformDisplay> {
Rc::new(WaylandDisplay {})
fn display(&self) -> Option<Rc<dyn PlatformDisplay>> {
self.borrow().display.as_ref().map(|(id, display)| {
Rc::new(WaylandDisplay {
id: id.clone(),
name: display.name.clone(),
bounds: display.bounds,
}) as Rc<dyn PlatformDisplay>
})
}
// todo(linux)
fn mouse_position(&self) -> Point<Pixels> {
Point::default()
self.borrow()
.client
.get_client()
.borrow()
.mouse_location
.unwrap_or_default()
}
// todo(linux)
fn modifiers(&self) -> Modifiers {
crate::Modifiers::default()
self.borrow().client.get_client().borrow().modifiers
}
fn set_input_handler(&mut self, input_handler: PlatformInputHandler) {
@@ -666,21 +693,20 @@ impl PlatformWindow for WaylandWindow {
fn prompt(
&self,
level: PromptLevel,
msg: &str,
detail: Option<&str>,
answers: &[&str],
_level: PromptLevel,
_msg: &str,
_detail: Option<&str>,
_answers: &[&str],
) -> Option<Receiver<usize>> {
None
}
fn activate(&self) {
// todo(linux)
log::info!("Wayland does not support this API");
}
// todo(linux)
fn is_active(&self) -> bool {
false
self.borrow().active
}
fn set_title(&mut self, title: &str) {
@@ -712,8 +738,8 @@ impl PlatformWindow for WaylandWindow {
}
if let Some(ref blur_manager) = state.globals.blur_manager {
if (background_appearance == WindowBackgroundAppearance::Blurred) {
if (state.blur.is_none()) {
if background_appearance == WindowBackgroundAppearance::Blurred {
if state.blur.is_none() {
let blur = blur_manager.create(&state.surface, &state.globals.qh, ());
blur.set_region(Some(&region));
state.blur = Some(blur);
@@ -731,12 +757,12 @@ impl PlatformWindow for WaylandWindow {
region.destroy();
}
fn set_edited(&mut self, edited: bool) {
// todo(linux)
fn set_edited(&mut self, _edited: bool) {
log::info!("ignoring macOS specific set_edited");
}
fn show_character_palette(&self) {
// todo(linux)
log::info!("ignoring macOS specific show_character_palette");
}
fn minimize(&self) {
@@ -754,7 +780,6 @@ impl PlatformWindow for WaylandWindow {
fn toggle_fullscreen(&self) {
let mut state = self.borrow_mut();
state.restore_bounds = state.bounds.map(|p| DevicePixels(p as i32));
if !state.fullscreen {
state.toplevel.set_fullscreen(None);
} else {

View File

@@ -1,40 +1,38 @@
use std::cell::RefCell;
use std::cell::{Cell, RefCell};
use std::ffi::OsString;
use std::ops::Deref;
use std::rc::{Rc, Weak};
use std::sync::OnceLock;
use std::time::{Duration, Instant};
use calloop::generic::{FdWrapper, Generic};
use calloop::{channel, EventLoop, LoopHandle, RegistrationToken};
use calloop::{EventLoop, LoopHandle, RegistrationToken};
use collections::HashMap;
use copypasta::x11_clipboard::{Clipboard, Primary, X11ClipboardContext};
use copypasta::ClipboardProvider;
use parking_lot::Mutex;
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, ScrollClass};
use x11rb::protocol::xinput::{ConnectionExt, KeyCode};
use x11rb::protocol::xkb::ConnectionExt as _;
use x11rb::protocol::xproto::{ChangeWindowAttributesAux, ConnectionExt as _};
use x11rb::protocol::xproto::{ChangeWindowAttributesAux, ConnectionExt as _, KeyButMask};
use x11rb::protocol::{randr, render, xinput, xkb, xproto, Event};
use x11rb::resource_manager::Database;
use x11rb::xcb_ffi::XCBConnection;
use xim::{x11rb::X11rbClient, Client};
use xim::{AHashMap, AttributeName, ClientHandler, InputStyle};
use xim::{AttributeName, InputStyle};
use xkbc::x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION};
use xkbcommon::xkb as xkbc;
use crate::platform::linux::LinuxClient;
use crate::platform::{LinuxCommon, PlatformWindow, WaylandClientState};
use crate::platform::{LinuxCommon, PlatformWindow};
use crate::{
modifiers_from_xinput_info, point, px, AnyWindowHandle, Bounds, CursorStyle, DisplayId,
ForegroundExecutor, Keystroke, Modifiers, ModifiersChangedEvent, Pixels, PlatformDisplay,
PlatformInput, Point, ScrollDelta, Size, TouchPhase, WindowAppearance, WindowParams, X11Window,
Keystroke, Modifiers, ModifiersChangedEvent, Pixels, PlatformDisplay, PlatformInput, Point,
ScrollDelta, Size, TouchPhase, WindowParams, X11Window, XimXCBConnection,
};
use super::{
@@ -54,6 +52,12 @@ pub(crate) struct WindowRef {
refresh_event_token: RegistrationToken,
}
impl WindowRef {
pub fn handle(&self) -> AnyWindowHandle {
self.window.state.borrow().handle
}
}
impl Deref for WindowRef {
type Target = X11WindowStatePtr;
@@ -92,6 +96,13 @@ impl From<xim::ClientError> for EventHandlerError {
}
}
#[derive(Clone)]
struct KeyEvent {
time: Instant,
state: KeyButMask,
code: KeyCode,
}
pub struct X11ClientState {
pub(crate) loop_handle: LoopHandle<'static, X11Client>,
pub(crate) event_loop: Option<calloop::EventLoop<'static, X11Client>>,
@@ -104,13 +115,14 @@ pub struct X11ClientState {
pub(crate) xcb_connection: Rc<XCBConnection>,
pub(crate) x_root_index: usize,
pub(crate) resource_database: Database,
pub(crate) _resource_database: Database,
pub(crate) atoms: XcbAtoms,
pub(crate) windows: HashMap<xproto::Window, WindowRef>,
pub(crate) focused_window: Option<xproto::Window>,
pub(crate) xkb: xkbc::State,
pub(crate) ximc: Option<X11rbClient<Rc<XCBConnection>>>,
pub(crate) ximc: Option<X11rbClient<XimXCBConnection>>,
pub(crate) xim_handler: Option<XimHandler>,
pub modifiers: Modifiers,
pub(crate) compose_state: xkbc::compose::State,
pub(crate) pre_edit_text: Option<String>,
@@ -126,6 +138,8 @@ pub struct X11ClientState {
pub(crate) common: LinuxCommon,
pub(crate) clipboard: X11ClipboardContext<Clipboard>,
pub(crate) primary: X11ClipboardContext<Primary>,
last_key_event: Option<KeyEvent>,
}
#[derive(Clone)]
@@ -159,11 +173,13 @@ impl X11Client {
let handle = event_loop.handle();
handle.insert_source(main_receiver, |event, _, _: &mut X11Client| {
if let calloop::channel::Event::Msg(runnable) = event {
runnable.run();
}
});
handle
.insert_source(main_receiver, |event, _, _: &mut X11Client| {
if let calloop::channel::Event::Msg(runnable) = event {
runnable.run();
}
})
.unwrap();
let (xcb_connection, x_root_index) = XCBConnection::connect(None).unwrap();
xcb_connection
@@ -248,23 +264,7 @@ impl X11Client {
xkbc::compose::State::new(&table, xkbc::compose::STATE_NO_FLAGS)
};
let screen = xcb_connection.setup().roots.get(x_root_index).unwrap();
// Values from `Database::GET_RESOURCE_DATABASE`
let resource_manager = xcb_connection
.get_property(
false,
screen.root,
xproto::AtomEnum::RESOURCE_MANAGER,
xproto::AtomEnum::STRING,
0,
100_000_000,
)
.unwrap();
let resource_manager = resource_manager.reply().unwrap();
// todo(linux): read hostname
let resource_database = Database::new_from_default(&resource_manager, "HOSTNAME".into());
let resource_database = x11rb::resource_manager::new_from_default(&xcb_connection).unwrap();
let scale_factor = resource_database
.get_value("Xft.dpi", "Xft.dpi")
@@ -283,11 +283,11 @@ impl X11Client {
let xcb_connection = Rc::new(xcb_connection);
let (xim_tx, xim_rx) = channel::channel::<XimCallbackEvent>();
let ximc = X11rbClient::init(Rc::clone(&xcb_connection), x_root_index, None).ok();
let xim_xcb_connection =
XimXCBConnection::new(Rc::clone(&xcb_connection), Rc::new(Cell::new(true)));
let ximc = X11rbClient::init(xim_xcb_connection.clone(), x_root_index, None).ok();
let xim_handler = if ximc.is_some() {
Some(XimHandler::new(xim_tx))
Some(XimHandler::new())
} else {
None
};
@@ -305,15 +305,51 @@ impl X11Client {
{
let xcb_connection = xcb_connection.clone();
move |_readiness, _, client| {
// println!("---------------------------------------------------");
// let mut events_count = 0;
// let start = Instant::now();
xim_xcb_connection.set_can_flush(false);
while let Some(event) = xcb_connection.poll_for_event()? {
// events_count += 1;
// let event_start = Instant::now();
// println!(
// "--> event {}: {} (sequnce: {:?})",
// events_count,
// ev_name,
// event.wire_sequence_number()
// );
// let _drop = util::defer(move || {
// println!(
// "<-- event {}: {}, took: {:?}",
// events_count,
// ev_name,
// event_start.elapsed()
// );
// });
let mut state = client.0.borrow_mut();
let last_key_event = match event {
Event::KeyPress(ev) | Event::KeyRelease(ev) => {
state.last_key_event.replace(KeyEvent { time: Instant::now(), state: ev.state, code: ev.detail });
None
}
_ => state.last_key_event.clone()
};
// let last_key_event_time = state.last_key_event.clone();
if state.ximc.is_none() || state.xim_handler.is_none() {
drop(state);
client.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) {
@@ -323,11 +359,61 @@ impl X11Client {
false
}
};
if let Some(xim_event) = xim_handler.last_callback_event.take() {
match xim_event {
XimCallbackEvent::XimXEvent(
event @ Event::KeyPress(key_event),
)
| XimCallbackEvent::XimXEvent(
event @ Event::KeyRelease(key_event),
) => {
// let ev_name = event_name(&event);
// println!("---> XimXEvent: {}", ev_name);
if let Some(last_key_event) = last_key_event {
// println!(
// "last_key_event_time: {}",
// last_key_event_time
// );
// println!(
// "event.time: {} (sequence: {})",
// key_event.time, key_event.sequence
// );
// let lag_ms = last_key_event_time.saturating_sub(key_event.time);
// if lag_ms > 100
let lag_ms = last_key_event.time.elapsed();
if (lag_ms > Duration::from_millis(100) && last_key_event.state == key_event.state && last_key_event.code == key_event.detail) ||
(lag_ms > Duration::from_millis(50) && last_key_event.state != key_event.state && last_key_event.code != key_event.detail)
{
let name = event_name(&event);
println!(">>>>>>>>>>>>>> dropping old event (name: {}, sequence: {}, lag_ms: {:?}) <<<<<<<<<<<<<<<<<<<<<", name, key_event.sequence, lag_ms);
// drop(state);
// continue;
} else {
client.handle_event(event);
}
} else {
client.handle_event(event);
}
}
XimCallbackEvent::XimXEvent(event) => {
client.handle_event(event);
}
XimCallbackEvent::XimCommitEvent(window, text) => {
client.xim_handle_commit(window, text);
}
XimCallbackEvent::XimPreeditEvent(window, text) => {
client.xim_handle_preedit(window, text);
}
};
}
let mut state = client.0.borrow_mut();
state.ximc = Some(ximc);
state.xim_handler = Some(xim_handler);
drop(state);
if xim_filtered {
// println!(" -> xim filtered");
continue;
}
if xim_connected {
@@ -336,45 +422,61 @@ impl X11Client {
client.handle_event(event);
}
}
// println!(
// "-------------- events_count: {:?}, took: {:?} ----------\n\n",
// events_count,
// start.elapsed()
// );
xim_xcb_connection.set_can_flush(true);
xim_xcb_connection.flush()?;
Ok(calloop::PostAction::Continue)
}
},
)
.expect("Failed to initialize x11 event source");
// handle
// .insert_source(xim_rx, {
// move |chan_event, _, client| match chan_event {
// channel::Event::Msg(xim_event) => {
// // println!(".......... XimCallBackEvent ...............");
// match xim_event {
// XimCallbackEvent::XimXEvent(event) => {
// println!("--> XimXEvent");
// client.handle_event(event);
// }
// XimCallbackEvent::XimCommitEvent(window, text) => {
// client.xim_handle_commit(window, text);
// }
// XimCallbackEvent::XimPreeditEvent(window, text) => {
// client.xim_handle_preedit(window, text);
// }
// };
// // println!("......... DONE: XimCallBackEvent ...............");
// }
// channel::Event::Closed => {
// log::error!("XIM Event Sender dropped")
// }
// }
// })
// .expect("Failed to initialize XIM event source");
handle
.insert_source(xim_rx, {
move |chan_event, _, client| match chan_event {
channel::Event::Msg(xim_event) => {
match (xim_event) {
XimCallbackEvent::XimXEvent(event) => {
client.handle_event(event);
}
XimCallbackEvent::XimCommitEvent(window, text) => {
client.xim_handle_commit(window, text);
}
XimCallbackEvent::XimPreeditEvent(window, text) => {
client.xim_handle_preedit(window, text);
}
};
}
channel::Event::Closed => {
log::error!("XIM Event Sender dropped")
.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);
}
}
}
})
.expect("Failed to initialize XIM 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);
}
}
}
});
.unwrap();
X11Client(Rc::new(RefCell::new(X11ClientState {
modifiers: Modifiers::default(),
event_loop: Some(event_loop),
loop_handle: handle,
common,
@@ -385,7 +487,7 @@ impl X11Client {
xcb_connection,
x_root_index,
resource_database,
_resource_database: resource_database,
atoms,
windows: HashMap::default(),
focused_window: None,
@@ -407,6 +509,8 @@ impl X11Client {
clipboard,
primary,
last_key_event: None,
})))
}
@@ -446,7 +550,8 @@ impl X11Client {
});
}
}
ximc.create_ic(xim_handler.im_id, ic_attributes.build());
ximc.create_ic(xim_handler.im_id, ic_attributes.build())
.ok();
state = self.0.borrow_mut();
state.xim_handler = Some(xim_handler);
state.ximc = Some(ximc);
@@ -457,7 +562,7 @@ impl X11Client {
state.composing = false;
if let Some(mut ximc) = state.ximc.take() {
let xim_handler = state.xim_handler.as_ref().unwrap();
ximc.destroy_ic(xim_handler.im_id, xim_handler.ic_id);
ximc.destroy_ic(xim_handler.im_id, xim_handler.ic_id).ok();
state.ximc = Some(ximc);
}
}
@@ -471,6 +576,8 @@ impl X11Client {
}
fn handle_event(&self, event: Event) -> Option<()> {
// println!("handle_event: {:?}", event_name(&event));
match event {
Event::ClientMessage(event) => {
let window = self.get_window(event.window)?;
@@ -503,6 +610,7 @@ impl X11Client {
Event::Expose(event) => {
let window = self.get_window(event.window)?;
window.refresh();
std::thread::sleep(Duration::from_millis(20));
}
Event::FocusIn(event) => {
let window = self.get_window(event.event)?;
@@ -535,6 +643,7 @@ impl X11Client {
);
let modifiers = Modifiers::from_xkb(&state.xkb);
let focused_window_id = state.focused_window?;
state.modifiers = modifiers;
drop(state);
let focused_window = self.get_window(focused_window_id)?;
@@ -547,6 +656,8 @@ impl X11Client {
let mut state = self.0.borrow_mut();
let modifiers = modifiers_from_state(event.state);
state.modifiers = modifiers;
let keystroke = {
let code = event.detail.into();
let mut keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
@@ -590,6 +701,7 @@ impl X11Client {
keystroke
};
drop(state);
// println!("keydown. keystroke.key: {:?}", keystroke.key);
window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent {
keystroke,
is_held: false,
@@ -600,6 +712,8 @@ impl X11Client {
let mut state = self.0.borrow_mut();
let modifiers = modifiers_from_state(event.state);
state.modifiers = modifiers;
let keystroke = {
let code = event.detail.into();
let keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
@@ -618,6 +732,8 @@ impl X11Client {
let mut state = self.0.borrow_mut();
let modifiers = modifiers_from_xinput_info(event.mods);
state.modifiers = modifiers;
let position = point(
px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor),
px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor),
@@ -664,8 +780,10 @@ impl X11Client {
}
Event::XinputButtonRelease(event) => {
let window = self.get_window(event.event)?;
let state = self.0.borrow();
let mut state = self.0.borrow_mut();
let modifiers = modifiers_from_xinput_info(event.mods);
state.modifiers = modifiers;
let position = point(
px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor),
px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor),
@@ -683,14 +801,15 @@ impl X11Client {
}
Event::XinputMotion(event) => {
let window = self.get_window(event.event)?;
let state = self.0.borrow();
let mut state = self.0.borrow_mut();
let pressed_button = pressed_button_from_mask(event.button_mask[0]);
let position = point(
px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor),
px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor),
);
drop(state);
let modifiers = modifiers_from_xinput_info(event.mods);
state.modifiers = modifiers;
drop(state);
let axisvalues = event
.axisvalues
@@ -761,18 +880,19 @@ impl X11Client {
valuator_idx += 1;
}
}
Event::XinputLeave(event) => {
Event::XinputLeave(event) if event.mode == xinput::NotifyMode::NORMAL => {
self.0.borrow_mut().scroll_x = None; // Set last scroll to `None` so that a large delta isn't created if scrolling is done outside the window (the valuator is global)
self.0.borrow_mut().scroll_y = None;
let window = self.get_window(event.event)?;
let state = self.0.borrow();
let mut state = self.0.borrow_mut();
let pressed_button = pressed_button_from_mask(event.buttons[0]);
let position = point(
px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor),
px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor),
);
let modifiers = modifiers_from_xinput_info(event.mods);
state.modifiers = modifiers;
drop(state);
window.handle_input(PlatformInput::MouseExited(crate::MouseExitEvent {
@@ -788,13 +908,20 @@ impl X11Client {
}
fn xim_handle_event(&self, event: Event) -> Option<()> {
let name = event_name(&event);
match event {
Event::KeyPress(event) | Event::KeyRelease(event) => {
let mut state = self.0.borrow_mut();
let mut ximc = state.ximc.take().unwrap();
let mut xim_handler = state.xim_handler.take().unwrap();
drop(state);
xim_handler.window = event.event;
println!(
"--> ximc.forward_event({}), sequence: {}",
name, event.sequence
);
ximc.forward_event(
xim_handler.im_id,
xim_handler.ic_id,
@@ -855,7 +982,8 @@ impl X11Client {
);
})
.build();
ximc.set_ic_values(xim_handler.im_id, xim_handler.ic_id, ic_attributes);
ximc.set_ic_values(xim_handler.im_id, xim_handler.ic_id, ic_attributes)
.ok();
}
let mut state = self.0.borrow_mut();
state.ximc = Some(ximc);
@@ -904,13 +1032,14 @@ impl LinuxClient for X11Client {
fn open_window(
&self,
_handle: AnyWindowHandle,
handle: AnyWindowHandle,
params: WindowParams,
) -> Box<dyn PlatformWindow> {
let mut state = self.0.borrow_mut();
let x_window = state.xcb_connection.generate_id().unwrap();
let window = X11Window::new(
handle,
X11ClientStatePtr(Rc::downgrade(&self.0)),
state.common.foreground_executor.clone(),
params,
@@ -1034,11 +1163,11 @@ impl LinuxClient for X11Client {
}
fn write_to_primary(&self, item: crate::ClipboardItem) {
self.0.borrow_mut().primary.set_contents(item.text);
self.0.borrow_mut().primary.set_contents(item.text).ok();
}
fn write_to_clipboard(&self, item: crate::ClipboardItem) {
self.0.borrow_mut().clipboard.set_contents(item.text);
self.0.borrow_mut().clipboard.set_contents(item.text).ok();
}
fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
@@ -1075,6 +1204,16 @@ impl LinuxClient for X11Client {
event_loop.run(None, &mut self.clone(), |_| {}).log_err();
}
fn active_window(&self) -> Option<AnyWindowHandle> {
let state = self.0.borrow();
state.focused_window.and_then(|focused_window| {
state
.windows
.get(&focused_window)
.map(|window| window.handle())
})
}
}
// Adatpted from:
@@ -1089,3 +1228,111 @@ pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration {
fn fp3232_to_f32(value: xinput::Fp3232) -> f32 {
value.integral as f32 + value.frac as f32 / u32::MAX as f32
}
fn event_name(event: &Event) -> &'static str {
match event {
Event::Unknown(_) => "Event::Unknown",
Event::Error(_) => "Event::Error",
Event::ButtonPress(_) => "Event::ButtonPress",
Event::ButtonRelease(_) => "Event::ButtonRelease",
Event::CirculateNotify(_) => "Event::CirculateNotify",
Event::CirculateRequest(_) => "Event::CirculateRequest",
Event::ClientMessage(_) => "Event::ClientMessage",
Event::ColormapNotify(_) => "Event::ColormapNotify",
Event::ConfigureNotify(_) => "Event::ConfigureNotify",
Event::ConfigureRequest(_) => "Event::ConfigureRequest",
Event::CreateNotify(_) => "Event::CreateNotify",
Event::DestroyNotify(_) => "Event::DestroyNotify",
Event::EnterNotify(_) => "Event::EnterNotify",
Event::Expose(_) => "Event::Expose",
Event::FocusIn(_) => "Event::FocusIn",
Event::FocusOut(_) => "Event::FocusOut",
Event::GeGeneric(_) => "Event::GeGeneric",
Event::GraphicsExposure(_) => "Event::GraphicsExposure",
Event::GravityNotify(_) => "Event::GravityNotify",
Event::KeyPress(_) => "Event::KeyPress",
Event::KeyRelease(_) => "Event::KeyRelease",
Event::KeymapNotify(_) => "Event::KeymapNotify",
Event::LeaveNotify(_) => "Event::LeaveNotify",
Event::MapNotify(_) => "Event::MapNotify",
Event::MapRequest(_) => "Event::MapRequest",
Event::MappingNotify(_) => "Event::MappingNotify",
Event::MotionNotify(_) => "Event::MotionNotify",
Event::NoExposure(_) => "Event::NoExposure",
Event::PropertyNotify(_) => "Event::PropertyNotify",
Event::ReparentNotify(_) => "Event::ReparentNotify",
Event::ResizeRequest(_) => "Event::ResizeRequest",
Event::SelectionClear(_) => "Event::SelectionClear",
Event::SelectionNotify(_) => "Event::SelectionNotify",
Event::SelectionRequest(_) => "Event::SelectionRequest",
Event::UnmapNotify(_) => "Event::UnmapNotify",
Event::VisibilityNotify(_) => "Event::VisibilityNotify",
Event::RandrNotify(_) => "Event::RandrNotify",
Event::RandrScreenChangeNotify(_) => "Event::RandrScreenChangeNotify",
Event::ShapeNotify(_) => "Event::ShapeNotify",
Event::XfixesCursorNotify(_) => "Event::XfixesCursorNotify",
Event::XfixesSelectionNotify(_) => "Event::XfixesSelectionNotify",
Event::XinputBarrierHit(_) => "Event::XinputBarrierHit",
Event::XinputBarrierLeave(_) => "Event::XinputBarrierLeave",
Event::XinputButtonPress(_) => "Event::XinputButtonPress",
Event::XinputButtonRelease(_) => "Event::XinputButtonRelease",
Event::XinputChangeDeviceNotify(_) => "Event::XinputChangeDeviceNotify",
Event::XinputDeviceButtonPress(_) => "Event::XinputDeviceButtonPress",
Event::XinputDeviceButtonRelease(_) => "Event::XinputDeviceButtonRelease",
Event::XinputDeviceButtonStateNotify(_) => "Event::XinputDeviceButtonStateNotify",
Event::XinputDeviceChanged(_) => "Event::XinputDeviceChanged",
Event::XinputDeviceFocusIn(_) => "Event::XinputDeviceFocusIn",
Event::XinputDeviceFocusOut(_) => "Event::XinputDeviceFocusOut",
Event::XinputDeviceKeyPress(_) => "Event::XinputDeviceKeyPress",
Event::XinputDeviceKeyRelease(_) => "Event::XinputDeviceKeyRelease",
Event::XinputDeviceKeyStateNotify(_) => "Event::XinputDeviceKeyStateNotify",
Event::XinputDeviceMappingNotify(_) => "Event::XinputDeviceMappingNotify",
Event::XinputDeviceMotionNotify(_) => "Event::XinputDeviceMotionNotify",
Event::XinputDevicePresenceNotify(_) => "Event::XinputDevicePresenceNotify",
Event::XinputDevicePropertyNotify(_) => "Event::XinputDevicePropertyNotify",
Event::XinputDeviceStateNotify(_) => "Event::XinputDeviceStateNotify",
Event::XinputDeviceValuator(_) => "Event::XinputDeviceValuator",
Event::XinputEnter(_) => "Event::XinputEnter",
Event::XinputFocusIn(_) => "Event::XinputFocusIn",
Event::XinputFocusOut(_) => "Event::XinputFocusOut",
Event::XinputGesturePinchBegin(_) => "Event::XinputGesturePinchBegin",
Event::XinputGesturePinchEnd(_) => "Event::XinputGesturePinchEnd",
Event::XinputGesturePinchUpdate(_) => "Event::XinputGesturePinchUpdate",
Event::XinputGestureSwipeBegin(_) => "Event::XinputGestureSwipeBegin",
Event::XinputGestureSwipeEnd(_) => "Event::XinputGestureSwipeEnd",
Event::XinputGestureSwipeUpdate(_) => "Event::XinputGestureSwipeUpdate",
Event::XinputHierarchy(_) => "Event::XinputHierarchy",
Event::XinputKeyPress(_) => "Event::XinputKeyPress",
Event::XinputKeyRelease(_) => "Event::XinputKeyRelease",
Event::XinputLeave(_) => "Event::XinputLeave",
Event::XinputMotion(_) => "Event::XinputMotion",
Event::XinputProperty(_) => "Event::XinputProperty",
Event::XinputProximityIn(_) => "Event::XinputProximityIn",
Event::XinputProximityOut(_) => "Event::XinputProximityOut",
Event::XinputRawButtonPress(_) => "Event::XinputRawButtonPress",
Event::XinputRawButtonRelease(_) => "Event::XinputRawButtonRelease",
Event::XinputRawKeyPress(_) => "Event::XinputRawKeyPress",
Event::XinputRawKeyRelease(_) => "Event::XinputRawKeyRelease",
Event::XinputRawMotion(_) => "Event::XinputRawMotion",
Event::XinputRawTouchBegin(_) => "Event::XinputRawTouchBegin",
Event::XinputRawTouchEnd(_) => "Event::XinputRawTouchEnd",
Event::XinputRawTouchUpdate(_) => "Event::XinputRawTouchUpdate",
Event::XinputTouchBegin(_) => "Event::XinputTouchBegin",
Event::XinputTouchEnd(_) => "Event::XinputTouchEnd",
Event::XinputTouchOwnership(_) => "Event::XinputTouchOwnership",
Event::XinputTouchUpdate(_) => "Event::XinputTouchUpdate",
Event::XkbAccessXNotify(_) => "Event::XkbAccessXNotify",
Event::XkbActionMessage(_) => "Event::XkbActionMessage",
Event::XkbBellNotify(_) => "Event::XkbBellNotify",
Event::XkbCompatMapNotify(_) => "Event::XkbCompatMapNotify",
Event::XkbControlsNotify(_) => "Event::XkbControlsNotify",
Event::XkbExtensionDeviceNotify(_) => "Event::XkbExtensionDeviceNotify",
Event::XkbIndicatorMapNotify(_) => "Event::XkbIndicatorMapNotify",
Event::XkbIndicatorStateNotify(_) => "Event::XkbIndicatorStateNotify",
Event::XkbMapNotify(_) => "Event::XkbMapNotify",
Event::XkbNamesNotify(_) => "Event::XkbNamesNotify",
Event::XkbNewKeyboardNotify(_) => "Event::XkbNewKeyboardNotify",
Event::XkbStateNotify(_) => "Event::XkbStateNotify",
_ => "unknown",
}
}

View File

@@ -1,46 +1,35 @@
// todo(linux): remove
#![allow(unused)]
use crate::{
platform::blade::{BladeRenderer, BladeSurfaceConfig},
size, Bounds, DevicePixels, ForegroundExecutor, Modifiers, Pixels, Platform, PlatformAtlas,
PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, PromptLevel,
Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowOptions,
WindowParams, X11Client, X11ClientState, X11ClientStatePtr,
size, AnyWindowHandle, Bounds, DevicePixels, ForegroundExecutor, Modifiers, Pixels,
PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point,
PromptLevel, Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
WindowParams, X11ClientStatePtr,
};
use blade_graphics as gpu;
use parking_lot::Mutex;
use raw_window_handle as rwh;
use util::ResultExt;
use x11rb::{
connection::{Connection as _, RequestConnection as _},
connection::Connection,
protocol::{
render,
xinput::{self, ConnectionExt as _},
xproto::{
self, Atom, ClientMessageEvent, ConnectionExt as _, CreateWindowAux, EventMask,
TranslateCoordinatesReply,
self, ClientMessageEvent, ConnectionExt as _, EventMask, TranslateCoordinatesReply,
},
},
resource_manager::Database,
wrapper::ConnectionExt as _,
xcb_ffi::XCBConnection,
};
use std::ops::Deref;
use std::rc::Weak;
use std::{
cell::{Ref, RefCell, RefMut},
collections::HashMap,
cell::RefCell,
ffi::c_void,
iter::Zip,
mem,
num::NonZeroU32,
ops::Div,
ptr::NonNull,
rc::Rc,
sync::{self, Arc},
time::Instant,
};
use super::{X11Display, XINPUT_MASTER_DEVICE};
@@ -165,29 +154,29 @@ pub struct Callbacks {
appearance_changed: Option<Box<dyn FnMut()>>,
}
pub(crate) struct X11WindowState {
pub struct X11WindowState {
client: X11ClientStatePtr,
executor: ForegroundExecutor,
atoms: XcbAtoms,
x_root_window: xproto::Window,
raw: RawWindow,
_raw: RawWindow,
bounds: Bounds<i32>,
scale_factor: f32,
renderer: BladeRenderer,
display: Rc<dyn PlatformDisplay>,
input_handler: Option<PlatformInputHandler>,
appearance: WindowAppearance,
pub handle: AnyWindowHandle,
}
#[derive(Clone)]
pub(crate) struct X11WindowStatePtr {
pub(crate) state: Rc<RefCell<X11WindowState>>,
pub state: Rc<RefCell<X11WindowState>>,
pub(crate) callbacks: Rc<RefCell<Callbacks>>,
xcb_connection: Rc<XCBConnection>,
x_window: xproto::Window,
}
// todo(linux): Remove other RawWindowHandle implementation
impl rwh::HasWindowHandle for RawWindow {
fn window_handle(&self) -> Result<rwh::WindowHandle, rwh::HandleError> {
let non_zero = NonZeroU32::new(self.window_id).unwrap();
@@ -218,6 +207,7 @@ impl rwh::HasDisplayHandle for X11Window {
impl X11WindowState {
#[allow(clippy::too_many_arguments)]
pub fn new(
handle: AnyWindowHandle,
client: X11ClientStatePtr,
executor: ForegroundExecutor,
params: WindowParams,
@@ -272,8 +262,6 @@ impl X11WindowState {
.event_mask(
xproto::EventMask::EXPOSURE
| xproto::EventMask::STRUCTURE_NOTIFY
| xproto::EventMask::ENTER_WINDOW
| xproto::EventMask::LEAVE_WINDOW
| xproto::EventMask::FOCUS_CHANGE
| xproto::EventMask::KEY_PRESS
| xproto::EventMask::KEY_RELEASE,
@@ -372,7 +360,7 @@ impl X11WindowState {
client,
executor,
display: Rc::new(X11Display::new(xcb_connection, x_screen_index).unwrap()),
raw,
_raw: raw,
x_root_window: visual_set.root,
bounds: params.bounds.map(|v| v.0),
scale_factor,
@@ -380,6 +368,7 @@ impl X11WindowState {
atoms: *atoms,
input_handler: None,
appearance,
handle,
}
}
@@ -420,14 +409,15 @@ impl Drop for X11Window {
}
enum WmHintPropertyState {
Remove = 0,
Add = 1,
// Remove = 0,
// Add = 1,
Toggle = 2,
}
impl X11Window {
#[allow(clippy::too_many_arguments)]
pub fn new(
handle: AnyWindowHandle,
client: X11ClientStatePtr,
executor: ForegroundExecutor,
params: WindowParams,
@@ -440,6 +430,7 @@ impl X11Window {
) -> Self {
Self(X11WindowStatePtr {
state: Rc::new(RefCell::new(X11WindowState::new(
handle,
client,
executor,
params,
@@ -541,8 +532,15 @@ impl X11WindowStatePtr {
}
pub fn handle_input(&self, input: PlatformInput) {
// println!("handle_input called");
// let start = Instant::now();
if let Some(ref mut fun) = self.callbacks.borrow_mut().input {
// println!(
// "handle_input. got input callback. elapsed: {:?}",
// start.elapsed()
// );
if !fun(input.clone()).propagate {
// println!("handle_input. return here. elapsed: {:?}", start.elapsed());
return;
}
}
@@ -617,13 +615,21 @@ impl X11WindowStatePtr {
pub fn configure(&self, bounds: Bounds<i32>) {
let mut resize_args = None;
let do_move;
let is_resize;
{
let mut state = self.state.borrow_mut();
let old_bounds = mem::replace(&mut state.bounds, bounds);
do_move = old_bounds.origin != bounds.origin;
// todo(linux): use normal GPUI types here, refactor out the double
// viewport check and extra casts ( )
is_resize = bounds.size.width != state.bounds.size.width
|| bounds.size.height != state.bounds.size.height;
// If it's a resize event (only width/height changed), we ignore `bounds.origin`
// because it contains wrong values.
if is_resize {
state.bounds.size = bounds.size;
} else {
state.bounds = bounds;
}
let gpu_size = query_render_extent(&self.xcb_connection, self.x_window);
if state.renderer.viewport_size() != gpu_size {
state
@@ -639,7 +645,7 @@ impl X11WindowStatePtr {
fun(content_size, scale_factor)
}
}
if do_move {
if !is_resize {
if let Some(ref mut fun) = callbacks.moved {
fun()
}
@@ -698,8 +704,8 @@ impl PlatformWindow for X11Window {
self.0.state.borrow().appearance
}
fn display(&self) -> Rc<dyn PlatformDisplay> {
self.0.state.borrow().display.clone()
fn display(&self) -> Option<Rc<dyn PlatformDisplay>> {
Some(self.0.state.borrow().display.clone())
}
fn mouse_position(&self) -> Point<Pixels> {
@@ -713,9 +719,15 @@ impl PlatformWindow for X11Window {
Point::new((reply.root_x as u32).into(), (reply.root_y as u32).into())
}
// todo(linux)
fn modifiers(&self) -> Modifiers {
Modifiers::default()
self.0
.state
.borrow()
.client
.0
.upgrade()
.map(|ref_cell| ref_cell.borrow().modifiers)
.unwrap_or_default()
}
fn set_input_handler(&mut self, input_handler: PlatformInputHandler) {
@@ -792,8 +804,9 @@ impl PlatformWindow for X11Window {
.unwrap();
}
// todo(linux)
fn set_edited(&mut self, edited: bool) {}
fn set_edited(&mut self, _edited: bool) {
log::info!("ignoring macOS specific set_edited");
}
fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) {
let mut inner = self.0.state.borrow_mut();
@@ -801,14 +814,8 @@ impl PlatformWindow for X11Window {
inner.renderer.update_transparency(transparent);
}
// todo(linux), this corresponds to `orderFrontCharacterPalette` on macOS,
// but it looks like the equivalent for Linux is GTK specific:
//
// https://docs.gtk.org/gtk3/signal.Entry.insert-emoji.html
//
// This API might need to change, or we might need to build an emoji picker into GPUI
fn show_character_palette(&self) {
unimplemented!()
log::info!("ignoring macOS specific show_character_palette");
}
fn minimize(&self) {

View File

@@ -1,13 +1,16 @@
use std::cell::RefCell;
use std::default::Default;
use std::rc::Rc;
use std::{cell::Cell, default::Default, rc::Rc};
use calloop::channel;
use x11rb::protocol::{xproto, Event};
use xim::{AHashMap, AttributeName, Client, ClientError, ClientHandler, InputStyle, Point};
use crate::{Keystroke, PlatformInput, X11ClientState};
use x11rb::{
connection::{Connection, RequestConnection},
cookie::{CookieWithFds, VoidCookie},
protocol::{xproto, Event},
xcb_ffi::XCBConnection,
};
use xim::{
x11rb::HasConnection, AHashMap, AttributeName, Client, ClientError, ClientHandler, InputStyle,
};
pub enum XimCallbackEvent {
XimXEvent(x11rb::protocol::Event),
@@ -18,19 +21,21 @@ pub enum XimCallbackEvent {
pub struct XimHandler {
pub im_id: u16,
pub ic_id: u16,
pub xim_tx: channel::Sender<XimCallbackEvent>,
// pub xim_tx: channel::Sender<XimCallbackEvent>,
pub connected: bool,
pub window: xproto::Window,
pub last_callback_event: Option<XimCallbackEvent>,
}
impl XimHandler {
pub fn new(xim_tx: channel::Sender<XimCallbackEvent>) -> Self {
pub fn new() -> Self {
Self {
im_id: Default::default(),
ic_id: Default::default(),
xim_tx,
// xim_tx,
connected: false,
window: Default::default(),
last_callback_event: None,
}
}
}
@@ -84,10 +89,12 @@ impl<C: Client<XEvent = xproto::KeyPressEvent>> ClientHandler<C> for XimHandler
_input_context_id: u16,
text: &str,
) -> Result<(), ClientError> {
self.xim_tx.send(XimCallbackEvent::XimCommitEvent(
self.window,
String::from(text),
));
self.last_callback_event
.replace(XimCallbackEvent::XimCommitEvent(
self.window,
String::from(text),
));
// .ok();
Ok(())
}
@@ -99,14 +106,22 @@ impl<C: Client<XEvent = xproto::KeyPressEvent>> ClientHandler<C> for XimHandler
_flag: xim::ForwardEventFlag,
xev: C::XEvent,
) -> Result<(), ClientError> {
match (xev.response_type) {
match xev.response_type {
x11rb::protocol::xproto::KEY_PRESS_EVENT => {
self.xim_tx
.send(XimCallbackEvent::XimXEvent(Event::KeyPress(xev)));
// println!(
// "XimHandler. handle_forward_event(KeyPress). sequence: {}",
// xev.sequence
// );
self.last_callback_event
.replace(XimCallbackEvent::XimXEvent(Event::KeyPress(xev)));
}
x11rb::protocol::xproto::KEY_RELEASE_EVENT => {
self.xim_tx
.send(XimCallbackEvent::XimXEvent(Event::KeyRelease(xev)));
// println!(
// "XimHandler. handle_forward_event(KeyRelease), sequence: {}",
// xev.sequence
// );
self.last_callback_event
.replace(XimCallbackEvent::XimXEvent(Event::KeyRelease(xev)));
}
_ => {}
}
@@ -145,10 +160,185 @@ impl<C: Client<XEvent = xproto::KeyPressEvent>> ClientHandler<C> for XimHandler
// XIMPrimary, XIMHighlight, XIMSecondary, XIMTertiary are not specified,
// but interchangeable as above
// Currently there's no way to support these.
let mark_range = self.xim_tx.send(XimCallbackEvent::XimPreeditEvent(
self.window,
String::from(preedit_string),
));
self.last_callback_event
.replace(XimCallbackEvent::XimPreeditEvent(
self.window,
String::from(preedit_string),
));
// self.xim_tx
// .send()
// .ok();
Ok(())
}
}
#[derive(Clone)]
pub struct XimXCBConnection(Rc<XCBConnection>, Rc<Cell<bool>>);
impl XimXCBConnection {
pub fn new(connection: Rc<XCBConnection>, can_flush: Rc<Cell<bool>>) -> Self {
Self(connection, can_flush)
}
pub fn set_can_flush(&self, can_flush: bool) {
self.1.set(can_flush);
}
}
impl HasConnection for XimXCBConnection {
type Connection = Self;
fn conn(&self) -> &Self::Connection {
self
}
}
impl Connection for XimXCBConnection {
fn wait_for_raw_event_with_sequence(
&self,
) -> Result<x11rb::connection::RawEventAndSeqNumber<Self::Buf>, x11rb::errors::ConnectionError>
{
self.0.wait_for_raw_event_with_sequence()
}
fn poll_for_raw_event_with_sequence(
&self,
) -> Result<
Option<x11rb::connection::RawEventAndSeqNumber<Self::Buf>>,
x11rb::errors::ConnectionError,
> {
self.0.poll_for_raw_event_with_sequence()
}
fn flush(&self) -> Result<(), x11rb::errors::ConnectionError> {
if self.1.get() {
// println!("*real* flush");
self.0.flush()
} else {
// println!("fake flush");
Ok(())
}
}
fn setup(&self) -> &xproto::Setup {
self.0.setup()
}
fn generate_id(&self) -> Result<u32, x11rb::errors::ReplyOrIdError> {
self.0.generate_id()
}
}
impl RequestConnection for XimXCBConnection {
type Buf = <XCBConnection as RequestConnection>::Buf;
fn send_request_with_reply<R>(
&self,
bufs: &[std::io::IoSlice<'_>],
fds: Vec<x11rb::utils::RawFdContainer>,
) -> Result<x11rb::cookie::Cookie<'_, Self, R>, x11rb::errors::ConnectionError>
where
R: x11rb::x11_utils::TryParse,
{
self.0
.send_request_with_reply::<R>(bufs, fds)
.map(|cookie| x11rb::cookie::Cookie::new(self, cookie.sequence_number()))
}
fn send_request_with_reply_with_fds<R>(
&self,
bufs: &[std::io::IoSlice<'_>],
fds: Vec<x11rb::utils::RawFdContainer>,
) -> Result<CookieWithFds<'_, Self, R>, x11rb::errors::ConnectionError>
where
R: x11rb::x11_utils::TryParseFd,
{
self.0
.send_request_with_reply_with_fds::<R>(bufs, fds)
.map(|cookie| CookieWithFds::new(self, cookie.sequence_number()))
}
fn send_request_without_reply(
&self,
bufs: &[std::io::IoSlice<'_>],
fds: Vec<x11rb::utils::RawFdContainer>,
) -> Result<VoidCookie<'_, Self>, x11rb::errors::ConnectionError> {
self.0
.send_request_without_reply(bufs, fds)
.map(|cookie| VoidCookie::new(self, cookie.sequence_number()))
}
fn discard_reply(
&self,
sequence: x11rb::connection::SequenceNumber,
kind: x11rb::connection::RequestKind,
mode: x11rb::connection::DiscardMode,
) {
self.0.discard_reply(sequence, kind, mode)
}
fn prefetch_extension_information(
&self,
extension_name: &'static str,
) -> Result<(), x11rb::errors::ConnectionError> {
self.0.prefetch_extension_information(extension_name)
}
fn extension_information(
&self,
extension_name: &'static str,
) -> Result<Option<x11rb::x11_utils::ExtensionInformation>, x11rb::errors::ConnectionError>
{
self.0.extension_information(extension_name)
}
fn wait_for_reply_or_raw_error(
&self,
sequence: x11rb::connection::SequenceNumber,
) -> Result<x11rb::connection::ReplyOrError<Self::Buf>, x11rb::errors::ConnectionError> {
self.0.wait_for_reply_or_raw_error(sequence)
}
fn wait_for_reply(
&self,
sequence: x11rb::connection::SequenceNumber,
) -> Result<Option<Self::Buf>, x11rb::errors::ConnectionError> {
self.0.wait_for_reply(sequence)
}
fn wait_for_reply_with_fds_raw(
&self,
sequence: x11rb::connection::SequenceNumber,
) -> Result<
x11rb::connection::ReplyOrError<x11rb::connection::BufWithFds<Self::Buf>, Self::Buf>,
x11rb::errors::ConnectionError,
> {
self.0.wait_for_reply_with_fds_raw(sequence)
}
fn check_for_raw_error(
&self,
sequence: x11rb::connection::SequenceNumber,
) -> Result<Option<Self::Buf>, x11rb::errors::ConnectionError> {
self.0.check_for_raw_error(sequence)
}
fn prefetch_maximum_request_bytes(&self) {
self.0.prefetch_maximum_request_bytes()
}
fn maximum_request_bytes(&self) -> usize {
self.0.maximum_request_bytes()
}
fn parse_error(
&self,
error: &[u8],
) -> Result<x11rb::x11_utils::X11Error, x11rb::errors::ParseError> {
self.0.parse_error(error)
}
fn parse_event(&self, event: &[u8]) -> Result<Event, x11rb::errors::ParseError> {
self.0.parse_event(event)
}
}

View File

@@ -6,7 +6,6 @@ use std::future::Future;
use ashpd::desktop::settings::{ColorScheme, Settings};
use calloop::channel::{Channel, Sender};
use calloop::{EventSource, Poll, PostAction, Readiness, Token, TokenFactory};
use parking_lot::Mutex;
use smol::stream::StreamExt;
use util::ResultExt;
@@ -115,6 +114,7 @@ impl WindowAppearance {
}
}
#[cfg_attr(target_os = "linux", allow(dead_code))]
fn set_native(&mut self, cs: ColorScheme) {
*self = Self::from_native(cs);
}

View File

@@ -29,7 +29,10 @@ use cocoa::{
};
use objc::runtime::{BOOL, NO, YES};
use std::ops::Range;
use std::{
ffi::{c_char, CStr},
ops::Range,
};
pub(crate) use dispatcher::*;
pub(crate) use display::*;
@@ -52,6 +55,21 @@ impl BoolExt for bool {
}
}
trait NSStringExt {
unsafe fn to_str(&self) -> &str;
}
impl NSStringExt for id {
unsafe fn to_str(&self) -> &str {
let cstr = self.UTF8String();
if cstr.is_null() {
""
} else {
CStr::from_ptr(cstr as *mut c_char).to_str().unwrap()
}
}
}
#[repr(C)]
#[derive(Copy, Clone, Debug)]
struct NSRange {

View File

@@ -1,12 +1,12 @@
use crate::{
point, px, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton,
MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels,
PlatformInput, ScrollDelta, ScrollWheelEvent, TouchPhase,
platform::mac::NSStringExt, point, px, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers,
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent,
MouseUpEvent, NavigationDirection, Pixels, PlatformInput, ScrollDelta, ScrollWheelEvent,
TouchPhase,
};
use cocoa::{
appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType},
base::{id, YES},
foundation::NSString as _,
};
use core_graphics::{
event::{CGEvent, CGEventFlags, CGKeyCode},
@@ -15,7 +15,7 @@ use core_graphics::{
use ctor::ctor;
use metal::foreign_types::ForeignType as _;
use objc::{class, msg_send, sel, sel_impl};
use std::{borrow::Cow, ffi::CStr, mem, os::raw::c_char, ptr};
use std::{borrow::Cow, mem, ptr};
const BACKSPACE_KEY: u16 = 0x7f;
const SPACE_KEY: u16 = b' ' as u16;
@@ -243,11 +243,10 @@ impl PlatformInput {
unsafe fn parse_keystroke(native_event: id) -> Keystroke {
use cocoa::appkit::*;
let mut chars_ignoring_modifiers =
CStr::from_ptr(native_event.charactersIgnoringModifiers().UTF8String() as *mut c_char)
.to_str()
.unwrap()
.to_string();
let mut chars_ignoring_modifiers = native_event
.charactersIgnoringModifiers()
.to_str()
.to_string();
let first_char = chars_ignoring_modifiers.chars().next().map(|ch| ch as u16);
let modifiers = native_event.modifierFlags();
@@ -351,9 +350,6 @@ fn chars_for_modified_key(code: CGKeyCode, cmd: bool, shift: bool) -> String {
unsafe {
let event: id = msg_send![class!(NSEvent), eventWithCGEvent: &*event];
CStr::from_ptr(event.characters().UTF8String())
.to_str()
.unwrap()
.to_string()
event.characters().to_str().to_string()
}
}

View File

@@ -826,9 +826,6 @@ impl Platform for MacPlatform {
CursorStyle::ResizeDown => msg_send![class!(NSCursor), resizeDownCursor],
CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor],
CursorStyle::ResizeRow => msg_send![class!(NSCursor), resizeUpDownCursor],
CursorStyle::DisappearingItem => {
msg_send![class!(NSCursor), disappearingItemCursor]
}
CursorStyle::IBeamCursorForVerticalLayout => {
msg_send![class!(NSCursor), IBeamCursorForVerticalLayout]
}

View File

@@ -1,4 +1,4 @@
use super::{ns_string, renderer, MacDisplay, NSRange};
use super::{ns_string, renderer, MacDisplay, NSRange, NSStringExt};
use crate::{
platform::PlatformInputHandler, point, px, size, AnyWindowHandle, Bounds, DevicePixels,
DisplayLink, ExternalPaths, FileDropEvent, ForegroundExecutor, KeyDownEvent, Keystroke,
@@ -39,7 +39,6 @@ use std::{
ffi::{c_void, CStr},
mem,
ops::Range,
os::raw::c_char,
path::PathBuf,
ptr::{self, NonNull},
rc::Rc,
@@ -312,6 +311,7 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C
}
#[allow(clippy::enum_variant_names)]
#[derive(Clone)]
enum ImeInput {
InsertText(String, Option<Range<usize>>),
SetMarkedText(String, Option<Range<usize>>, Option<Range<usize>>),
@@ -340,7 +340,7 @@ struct MacWindowState {
traffic_light_position: Option<Point<Pixels>>,
previous_modifiers_changed_event: Option<PlatformInput>,
// State tracking what the IME did after the last request
input_during_keydown: Option<SmallVec<[ImeInput; 1]>>,
last_ime_inputs: Option<SmallVec<[(String, Option<Range<usize>>); 1]>>,
previous_keydown_inserted_text: Option<String>,
external_files_dragged: bool,
// Whether the next left-mouse click is also the focusing click.
@@ -636,7 +636,7 @@ impl MacWindow {
.as_ref()
.and_then(|titlebar| titlebar.traffic_light_position),
previous_modifiers_changed_event: None,
input_during_keydown: None,
last_ime_inputs: None,
previous_keydown_inserted_text: None,
external_files_dragged: false,
first_mouse: false,
@@ -657,7 +657,7 @@ impl MacWindow {
.as_ref()
.and_then(|t| t.title.as_ref().map(AsRef::as_ref))
{
native_window.setTitle_(NSString::alloc(nil).init_str(title));
window.set_title(title);
}
native_window.setMovable_(is_movable as BOOL);
@@ -799,7 +799,7 @@ impl PlatformWindow for MacWindow {
}
}
fn display(&self) -> Rc<dyn PlatformDisplay> {
fn display(&self) -> Option<Rc<dyn PlatformDisplay>> {
unsafe {
let screen = self.0.lock().native_window.screen();
let device_description: id = msg_send![screen, deviceDescription];
@@ -810,7 +810,7 @@ impl PlatformWindow for MacWindow {
let screen_number: u32 = msg_send![screen_number, unsignedIntValue];
Rc::new(MacDisplay(screen_number))
Some(Rc::new(MacDisplay(screen_number)))
}
}
@@ -1191,14 +1191,30 @@ extern "C" fn handle_key_down(this: &Object, _: Sel, native_event: id) {
}
// Things to test if you're modifying this method:
// U.S. layout:
// - The IME consumes characters like 'j' and 'k', which makes paging through `less` in
// the terminal behave incorrectly by default. This behavior should be patched by our
// IME integration
// - `alt-t` should open the tasks menu
// - In vim mode, this keybinding should work:
// ```
// {
// "context": "Editor && vim_mode == insert",
// "bindings": {"j j": "vim::NormalBefore"}
// }
// ```
// and typing 'j k' in insert mode with this keybinding should insert the two characters
// Brazilian layout:
// - `" space` should type a quote
// - `" space` should create an unmarked quote
// - `" backspace` should delete the marked quote
// - `" up` should type the quote, unmark it, and move up one line
// - `" cmd-down` should not leave a marked quote behind (it maybe should dispatch the key though?)
// - `" up` should insert a quote, unmark it, and move up one line
// - `" cmd-down` should insert a quote, unmark it, and move to the end of the file
// - NOTE: The current implementation does not move the selection to the end of the file
// - `cmd-ctrl-space` and clicking on an emoji should type it
// Czech (QWERTY) layout:
// - in vim mode `option-4` should go to end of line (same as $)
// Japanese (Romaji) layout:
// - type `a i left down up enter enter` should create an unmarked text "愛"
extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: bool) -> BOOL {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.as_ref().lock();
@@ -1228,7 +1244,7 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
} else {
lock.last_fresh_keydown = Some(keydown.clone());
}
lock.input_during_keydown = Some(SmallVec::new());
lock.last_ime_inputs = Some(Default::default());
drop(lock);
// Send the event to the input context for IME handling, unless the `fn` modifier is
@@ -1244,15 +1260,16 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
let mut handled = false;
let mut lock = window_state.lock();
let previous_keydown_inserted_text = lock.previous_keydown_inserted_text.take();
let mut input_during_keydown = lock.input_during_keydown.take().unwrap();
let mut last_inserts = lock.last_ime_inputs.take().unwrap();
let mut callback = lock.event_callback.take();
drop(lock);
let last_ime = input_during_keydown.pop();
let last_insert = last_inserts.pop();
// on a brazilian keyboard typing `"` and then hitting `up` will cause two IME
// events, one to unmark the quote, and one to send the up arrow.
for ime in input_during_keydown {
send_to_input_handler(this, ime);
for (text, range) in last_inserts {
send_to_input_handler(this, ImeInput::InsertText(text, range));
}
let is_composing =
@@ -1260,20 +1277,18 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
.flatten()
.is_some();
if let Some(ime) = last_ime {
if let ImeInput::InsertText(text, _) = &ime {
if !is_composing {
window_state.lock().previous_keydown_inserted_text = Some(text.clone());
if let Some(callback) = callback.as_mut() {
event.keystroke.ime_key = Some(text.clone());
handled = !callback(PlatformInput::KeyDown(event)).propagate;
}
if let Some((text, range)) = last_insert {
if !is_composing {
window_state.lock().previous_keydown_inserted_text = Some(text.clone());
if let Some(callback) = callback.as_mut() {
event.keystroke.ime_key = Some(text.clone());
handled = !callback(PlatformInput::KeyDown(event)).propagate;
}
}
if !handled {
handled = true;
send_to_input_handler(this, ime);
send_to_input_handler(this, ImeInput::InsertText(text, range));
}
} else if !is_composing {
let is_held = event.is_held;
@@ -1655,21 +1670,24 @@ extern "C" fn valid_attributes_for_marked_text(_: &Object, _: Sel) -> id {
}
extern "C" fn has_marked_text(this: &Object, _: Sel) -> BOOL {
with_input_handler(this, |input_handler| input_handler.marked_text_range())
.flatten()
.is_some() as BOOL
let has_marked_text_result =
with_input_handler(this, |input_handler| input_handler.marked_text_range()).flatten();
has_marked_text_result.is_some() as BOOL
}
extern "C" fn marked_range(this: &Object, _: Sel) -> NSRange {
with_input_handler(this, |input_handler| input_handler.marked_text_range())
.flatten()
.map_or(NSRange::invalid(), |range| range.into())
let marked_range_result =
with_input_handler(this, |input_handler| input_handler.marked_text_range()).flatten();
marked_range_result.map_or(NSRange::invalid(), |range| range.into())
}
extern "C" fn selected_range(this: &Object, _: Sel) -> NSRange {
with_input_handler(this, |input_handler| input_handler.selected_text_range())
.flatten()
.map_or(NSRange::invalid(), |range| range.into())
let selected_range_result =
with_input_handler(this, |input_handler| input_handler.selected_text_range()).flatten();
selected_range_result.map_or(NSRange::invalid(), |range| range.into())
}
extern "C" fn first_rect_for_character_range(
@@ -1711,9 +1729,8 @@ extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NS
} else {
text
};
let text = CStr::from_ptr(text.UTF8String() as *mut c_char)
.to_str()
.unwrap();
let text = text.to_str();
let replacement_range = replacement_range.to_range();
send_to_input_handler(
this,
@@ -1739,9 +1756,7 @@ extern "C" fn set_marked_text(
};
let selected_range = selected_range.to_range();
let replacement_range = replacement_range.to_range();
let text = CStr::from_ptr(text.UTF8String() as *mut c_char)
.to_str()
.unwrap();
let text = text.to_str();
send_to_input_handler(
this,
@@ -1765,7 +1780,7 @@ extern "C" fn attributed_substring_for_proposed_range(
return None;
}
let selected_text = input_handler.text_for_range(range)?;
let selected_text = input_handler.text_for_range(range.clone())?;
unsafe {
let string: id = msg_send![class!(NSAttributedString), alloc];
let string: id = msg_send![string, initWithString: ns_string(&selected_text)];
@@ -1930,20 +1945,26 @@ fn send_to_input_handler(window: &Object, ime: ImeInput) {
unsafe {
let window_state = get_window_state(window);
let mut lock = window_state.lock();
if let Some(ime_input) = lock.input_during_keydown.as_mut() {
ime_input.push(ime);
return;
}
if let Some(mut input_handler) = lock.input_handler.take() {
drop(lock);
match ime {
match ime.clone() {
ImeInput::InsertText(text, range) => {
if let Some(ime_input) = lock.last_ime_inputs.as_mut() {
ime_input.push((text, range));
lock.input_handler = Some(input_handler);
return;
}
drop(lock);
input_handler.replace_text_in_range(range, &text)
}
ImeInput::SetMarkedText(text, range, marked_range) => {
drop(lock);
input_handler.replace_and_mark_text_in_range(range, &text, marked_range)
}
ImeInput::UnmarkText => input_handler.unmark_text(),
ImeInput::UnmarkText => {
drop(lock);
input_handler.unmark_text()
}
}
window_state.lock().input_handler = Some(input_handler);
}

View File

@@ -1,7 +1,6 @@
mod dispatcher;
mod display;
mod platform;
mod text_system;
mod window;
pub(crate) use dispatcher::*;

View File

@@ -1,50 +0,0 @@
use crate::{
Bounds, DevicePixels, Font, FontId, FontMetrics, FontRun, GlyphId, LineLayout, Pixels,
PlatformTextSystem, RenderGlyphParams, Size,
};
use anyhow::Result;
use std::borrow::Cow;
pub(crate) struct TestTextSystem {}
// todo(linux)
#[allow(unused)]
impl PlatformTextSystem for TestTextSystem {
fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
unimplemented!()
}
fn all_font_names(&self) -> Vec<String> {
unimplemented!()
}
fn all_font_families(&self) -> Vec<String> {
unimplemented!()
}
fn font_id(&self, descriptor: &Font) -> Result<FontId> {
unimplemented!()
}
fn font_metrics(&self, font_id: FontId) -> FontMetrics {
unimplemented!()
}
fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>> {
unimplemented!()
}
fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
unimplemented!()
}
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
unimplemented!()
}
fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
unimplemented!()
}
fn rasterize_glyph(
&self,
params: &RenderGlyphParams,
raster_bounds: Bounds<DevicePixels>,
) -> Result<(Size<DevicePixels>, Vec<u8>)> {
unimplemented!()
}
fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout {
unimplemented!()
}
}

View File

@@ -132,8 +132,8 @@ impl PlatformWindow for TestWindow {
WindowAppearance::Light
}
fn display(&self) -> std::rc::Rc<dyn crate::PlatformDisplay> {
self.0.lock().display.clone()
fn display(&self) -> Option<std::rc::Rc<dyn crate::PlatformDisplay>> {
Some(self.0.lock().display.clone())
}
fn mouse_position(&self) -> Point<Pixels> {

View File

@@ -379,8 +379,8 @@ impl PlatformWindow for WindowsWindow {
WindowAppearance::Dark
}
fn display(&self) -> Rc<dyn PlatformDisplay> {
Rc::new(self.0.state.borrow().display)
fn display(&self) -> Option<Rc<dyn PlatformDisplay>> {
Some(Rc::new(self.0.state.borrow().display))
}
fn mouse_position(&self) -> Point<Pixels> {

View File

@@ -488,7 +488,7 @@ pub struct Window {
pub(crate) handle: AnyWindowHandle,
pub(crate) removed: bool,
pub(crate) platform_window: Box<dyn PlatformWindow>,
display_id: DisplayId,
display_id: Option<DisplayId>,
sprite_atlas: Arc<dyn PlatformAtlas>,
text_system: Arc<WindowTextSystem>,
rem_size: Pixels,
@@ -634,7 +634,7 @@ impl Window {
window_background,
},
);
let display_id = platform_window.display().id();
let display_id = platform_window.display().map(|display| display.id());
let sprite_atlas = platform_window.sprite_atlas();
let mouse_position = platform_window.mouse_position();
let modifiers = platform_window.modifiers();
@@ -691,8 +691,15 @@ impl Window {
measure("frame duration", || {
handle
.update(&mut cx, |_, cx| {
// let start = Instant::now();
cx.draw();
// let elapsed = start.elapsed();
// eprintln!("draw duration: {:?}", elapsed);
// let start = Instant::now();
cx.present();
// let elapsed = start.elapsed();
// eprintln!("present duration: {:?}", elapsed);
})
.log_err();
})
@@ -1036,6 +1043,37 @@ impl<'a> WindowContext<'a> {
});
}
/// Subscribe to events emitted by a model or view.
/// The entity to which you're subscribing must implement the [`EventEmitter`] trait.
/// The callback will be invoked a handle to the emitting entity (either a [`View`] or [`Model`]), the event, and a window context for the current window.
pub fn observe<E, T>(
&mut self,
entity: &E,
mut on_notify: impl FnMut(E, &mut WindowContext<'_>) + 'static,
) -> Subscription
where
E: Entity<T>,
{
let entity_id = entity.entity_id();
let entity = entity.downgrade();
let window_handle = self.window.handle;
self.app.new_observer(
entity_id,
Box::new(move |cx| {
window_handle
.update(cx, |_, cx| {
if let Some(handle) = E::upgrade_from(&entity) {
on_notify(handle, cx);
true
} else {
false
}
})
.unwrap_or(false)
}),
)
}
/// Subscribe to events emitted by a model or view.
/// The entity to which you're subscribing must implement the [`EventEmitter`] trait.
/// The callback will be invoked a handle to the emitting entity (either a [`View`] or [`Model`]), the event, and a window context for the current window.
@@ -1099,7 +1137,12 @@ impl<'a> WindowContext<'a> {
fn bounds_changed(&mut self) {
self.window.scale_factor = self.window.platform_window.scale_factor();
self.window.viewport_size = self.window.platform_window.content_size();
self.window.display_id = self.window.platform_window.display().id();
self.window.display_id = self
.window
.platform_window
.display()
.map(|display| display.id());
self.refresh();
self.window
@@ -1191,7 +1234,7 @@ impl<'a> WindowContext<'a> {
self.platform
.displays()
.into_iter()
.find(|display| display.id() == self.window.display_id)
.find(|display| Some(display.id()) == self.window.display_id)
}
/// Show the platform character palette.

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