Compare commits

...

58 Commits

Author SHA1 Message Date
Thorsten Ball
feb7efff4e linux/x11: use a channel for quit signal 2024-06-27 18:01:59 +02:00
Thorsten Ball
3956717065 WIP linux/x11: Tweak when we sleep in the event loop 2024-06-27 16:24:30 +02:00
Thorsten Ball
d3e2327099 WIP: linux/x11: Replace calloop event loop with custom loop 2024-06-27 16:06:32 +02:00
Thorsten Ball
fc945cc351 linux/x11: Store refresh rate on X11Window itself 2024-06-27 13:52:10 +02:00
Piotr Osiewicz
da22e0dd0b Revert "vue: Release 0.0.4" (#13584)
Reverts zed-industries/zed#13580 as it turned out that the issue lied in
incorrect user settings.

Release notes:
- N/A
2024-06-27 11:36:17 +02:00
Tim Havlicek
fb3ef0d140 Add separate JSONC language (#12655)
Resolves https://github.com/zed-industries/extensions/issues/860 and
https://github.com/zed-industries/zed/issues/10921, also
https://github.com/biomejs/biome-zed/issues/11.

### Problem:
When opening .json files, zed allows comments by default in the JSON
language, which can cause some problems.
For example, language-servers also get "json" as the language, which may
show errors for those comments.

<img width="935" alt="image"
src="https://github.com/zed-industries/zed/assets/10381895/fed3d83d-abc0-44b5-9982-eb249bb04c3b">

### Solution:

This PR adds a JSONC language. 

<img width="816" alt="image"
src="https://github.com/zed-industries/zed/assets/10381895/8b40e671-d4f0-4e8d-80cb-82ee7c0ec490">

This allows for more specific configuration for language servers. 
Also any json file can be set explicitly to be JSONC using the
file_types setting:

```jsonc
{
  "file_types": {
    // set all .json files to be seen as JSONC
    "JSONC": ["*.json"]
  }
}
```


Release Notes:

- N/A
2024-06-27 11:12:02 +02:00
Piotr Osiewicz
e71b642f44 vue: Release 0.0.4 (#13580)
Respect user settings in initialization_options.


Release Notes:

- Fixed Vue extension not picking up user-provided initialization
options.
2024-06-27 11:11:22 +02:00
Jason Lee
6cedfa0ce7 example: Fix Input example mistake (#13574)
![CleanShot 2024-06-27 at 15 52
48](https://github.com/zed-industries/zed/assets/5518/71b25759-0cd5-40ed-b7c2-2f1045f81683)

Release Notes:

- N/A
2024-06-27 11:28:44 +03:00
Gilles Peiffer
209b1d1931 Code maintenance in the editor crate (#13565)
Release Notes:

- N/A
2024-06-27 09:40:48 +03:00
Gilles Peiffer
6986ac4c27 Use iterators instead of loops in clock.rs (#13561)
This should be slightly faster and makes the code easier to read.

Release Notes:

- N/A
2024-06-27 09:30:21 +03:00
Peter Tripp
d50d1611b9 Release notes upload fix (#13560)
- Action for release notes upload (softprops/action-gh-release) configured with incorrect key. 
- Valid keys here: https://github.com/softprops/action-gh-release?tab=readme-ov-file#-customizing
2024-06-26 17:24:59 -04:00
Gilles Peiffer
1260c616ba Simplify font feature tag validation (#13548)
Simplifies the logic for the changes of #13542.

Release Notes:

- N/A
2024-06-26 17:11:57 -04:00
Joseph T. Lyons
89951f7e66 Add shift shift to open command palette (#13556)
I've add `shift shift` as a default keybinding to open command palette,
when using JetBrains keymap, along with the already existing
`cmd-shift-a`. This isn't quite right, as in JetBrains, `cmd-shift-a`
opens the actions modal, which would be our command palette, and `shift
shift` actually opens up a view for searching everything, commands,
actions, settings, etc - we do not have a unified modal for these
things, so I think this is the best thing we can do. Some users might
want to change this to be our file picker, but I think adding it as the
default at least puts it on their radar that they can use this type of
binding; they can change it if they want.

Release Notes:

- Added `shift shift` as a default binding to open the command palette
in the JetBrains keymap.
2024-06-26 16:44:40 -04:00
Conrad Irwin
cd81dad2fa fix panics (#13554)
Release Notes:

- Fixed a panic when editing HTML near the end of a file
- Fixed a panic when editing settings.json from inside the .zed
directory
2024-06-26 14:32:16 -06:00
Piotr Osiewicz
3a08d7ab43 json: Fix package-version-server referencing the wrong path to the binary (#13555)
We were trying to access the binary at
package-version-server-{VERSION}/package-version-server, whereas the
binary itself is placed at package-version-server-{VERSION}

Release Notes:

- Fixed package.json language server failing to start.

Co-authored-by: Peter Tripp <peter@zed.dev>
2024-06-26 16:17:55 -04:00
Josef Zoller
49dc63812a Stop relying on binary location to be in libexec on Linux (#13374)
This fixes #13360 by adding fallback directories that are searched by
the CLI if the main executable cannot be found in the `libexec`
directory.

Release Notes:

- Added the fallback directories `lib/zed` and `lib/zed-editor` for the
main executable search in the CLI
([#13360](https://github.com/zed-industries/zed/issues/13360)).

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-06-26 13:00:52 -06:00
Tristan Hume
c0a3642f77 Improve prompt for Claude models (#13531)
This inline assistant prompt is one I designed that in my experience
works much better with Claude 3.5 Sonnet than the default prompt.

Mainly because it takes advantage of a weird property of our finetuning
which is that when you use XML tags it knows that it's doing a
machine-read tasks and stops trying to elide things for brevity. The
default prompt will often remove comments and otherwise add elisions for
brevity when doing large rewrites.

It also avoids giving the entire file content twice when the rewrite
region is large relative to the non-rewritten region.

Not necessarily meant to be merged as-is since it may mess up OAI
models. This is mainly meant for your reference. But everyone should be
using 3.5 Sonnet for coding use cases now anyhow 😛

Release Notes:

- N/A
2024-06-26 20:41:40 +02:00
Nate Butler
4d5441c09d Add UI setting components (#13550)
Adds some of the UI components to allow us to visually render settings.

These are UI only and are not functional yet (@maxdeviant will be
working on these when he is back.)

You can see some examples by running `script/storybook setting`.

![CleanShot 2024-06-26 at 12 38
37@2x](https://github.com/zed-industries/zed/assets/1714999/b5e6434d-3bc5-4fcd-9c0a-d280950cbef2)

Release Notes:

- N/A
2024-06-26 13:02:58 -04:00
Peter Tripp
2dc840132b v0.143.x dev 2024-06-26 12:20:15 -04:00
Fernando Tagawa
5d766f61fa linux: Fix some panics related to xkb compose (#13529)
Release Notes:

- N/A

Fixed #13463 Fixed crash when the locale was non UTF-8 and fixed the
fallback locale.
Fixed #13010 Fixed crash when `compose.keysym()` was `XKB_KEY_NoSymbol`

I also extracted the `xkb_compose_state` to a single place
2024-06-26 09:34:39 -06:00
张小白
18b4573064 Fix font feature tag validation (#13542)
The previous implementation that I implemented had two issues:
1. It did not throw an error when the user input some invalid values
such as "panic".
2. The feature tag for OpenType fonts should be a combination of letters
and digits. We only checked if the input was an ASCII character, which
could lead to undefined behavior.

Closes #13517 

Release Notes:

- N/A
2024-06-26 11:01:48 -04:00
Toshimaru
d044dc8485 Update Docker Compose configuration (#13530)
- Fix Docker Compose obsolete setting

## Remove `version`

Fix the following error:

```
WARN[0000] /docker-compose.yml: `version` is obsolete
```

see also.
https://github.com/compose-spec/compose-spec/blob/master/spec.md#version-top-level-element-obsolete

## Rename: docker-compose.yml -> compose.yml

The preferred file name is now `compose.yml`.

> The default path for a Compose file is compose.yaml (preferred)

ref.
https://docs.docker.com/compose/compose-application-model/#the-compose-file

Release Notes:

- N/A
2024-06-26 08:05:23 -04:00
Alexander Mankuta
f00bea5d0f docs: Fix Decrease buffer font size key binding (#13453)
Release Notes:

- N/A
2024-06-26 10:48:00 +03:00
Conrad Irwin
b43df6048b Add an input example to gpui (#13534)
Add a single-line text input example to gpui

(I'm hoping to be able to debug keyboard issues without rebuilding the
whole
app every time)

Release Notes:

- N/A
2024-06-25 22:06:50 -06:00
Conrad Irwin
eb914682b3 Fix multi-cursor copy/paste on linux (#13523)
The clipboard library we use for X11 doesn't yet support multiple
formats on the clipboard, so for now we just store this in memory for
the current zed process, as we do for Wayland.

Fixes: #11971

Release Notes:

- N/A

---------

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2024-06-25 14:54:52 -06:00
Joseph T. Lyons
5b7e31c075 Add metrics_id to editor_events (#13525)
Release Notes:

- N/A
2024-06-25 16:47:55 -04:00
ᴀᴍᴛᴏᴀᴇʀ
922fcaf5a6 Add the ability to customize available models for OpenAI-compatible services (#13276)
Closes #11984, closes #11075.

Release Notes:

- Added the ability to customize available models for OpenAI-compatible
services ([#11984](https://github.com/zed-industries/zed/issues/11984))
([#11075](https://github.com/zed-industries/zed/issues/11075)).


![image](https://github.com/zed-industries/zed/assets/32017007/01057e7b-1f21-49ad-a3ad-abc5282ffaf0)
2024-06-25 16:37:02 -04:00
Nate Butler
9f88460870 Move token count in prompt editor (#13524)
Moves the token count back up to the editor header.

Release Notes:

- N/A
2024-06-25 16:10:05 -04:00
Mikayla Maki
e5d1cf84cf Fix 9263 (#13521)
Fix #9263

Release Notes:

- N/A
2024-06-25 11:35:50 -07:00
Mikayla Maki
41d2c52638 Adjust keybindings for deletion in the project panel (#13326)
- Improve compatibility keybindings (Atom, JetBrains, TextMate)
- Revert MacOS cmd+backspace regression. Should trash without prompting (like MacOS)

Co-authored-by: Peter Tripp <peter@zed.dev>
2024-06-25 14:21:44 -04:00
张小白
d1a55d64a8 Change window_min_size from Size<Pixels> to Option<Size<Pixels>> (#13501)
Now we can set `window_min_size` to `None` instead of `Size::default()`.
I think this makes more sense.

Release Notes:

- N/A
2024-06-25 12:09:08 -06:00
Shubham Kanodia
db06244972 typescript: Pass hostInfo to tsserver (#12055)
- Added `hostInfo` property to zed's typescript plugin. This can be
useful for telemetry (for e.g. identifying the usage of editors based on
typescript usage) when building typescript plugins.

- VSCode / IntelliJ based editors already set this property
([see](aa31bfc9fd/extensions/typescript-language-features/src/typescriptServiceClient.ts (L574)))

The config option as available —
https://github.com/typescript-language-server/typescript-language-server/blob/master/docs/configuration.md#initializationoptions

Release Notes:

- N/A
2024-06-25 13:51:30 -04:00
Marshall Bowers
597469bbbd Remove blank line (#13519)
This PR removes an extra blank line that was missed in #13518.

Release Notes:

- N/A
2024-06-25 13:11:25 -04:00
Marshall Bowers
e0c192d831 Clean up json! literal for vtsls configuration (#13518)
This PR cleans up the formatting of the `json!` literal used to provided
`vtsls` configuration.

Release Notes:

- N/A
2024-06-25 13:04:31 -04:00
Mikayla Maki
b2a0a7fa3c Fix a bug introduced by #13479 (#13516)
Fixes a bug introduced by
https://github.com/zed-industries/zed/pull/13479 where dot files might
not be processed in the correct order.

Release Notes:

- N/A
2024-06-25 10:03:29 -07:00
Dov Alperin
0b1a589183 keymap: Allow modifiers as keys (#12047)
It is sometimes desirable to allow modifers to serve as keys themselves
for the purposes of keybinds. For example, the popular keybind in
jetbrains IDEs `shift shift` which opens the file finder.

This change treats modifers in the keymaps as keys themselves if they
are not accompanied by a key they are modifying.

Further this change wires up they key dispatcher to treat modifer change
events as key presses which are considered for matching against
keybinds.


Release Notes:

- Fixes #6460

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-06-25 10:17:23 -06:00
ᴀᴍᴛᴏᴀᴇʀ
7e694d1bcf Fix an issue where provider settings were lost when switching between Ollama models (#13402)
Closes #13399.

Release Notes:

- Fixed an issue where provider settings were lost when switching
between Ollama models
([#13399](https://github.com/zed-industries/zed/issues/13399)).
2024-06-25 11:58:13 -04:00
Nate Butler
890443241d Prompt Library Refinements (#13470)
TODO:

- [x] Moving the cursor out of the title editor should unselect any
selected text

Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Richard <richard@zed.dev>
2024-06-25 11:43:30 -04:00
Gilles Peiffer
b014f9f017 docs: Fix some typos (#13509)
Minor fixes I came across while reading the docs.

Release Notes:

- N/A
2024-06-25 10:58:11 -04:00
Hamir Mahal
f40d2313fb Use string interpolation (#13482)
Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-06-25 10:57:50 -04:00
Matin Aniss
2dee4f87fd windows: Fix title bar font for Windows 10 (#13425)
This should fix the title bar font for Windows 10 as `Segoe Fluent
Icons` is only for Windows 11 and Windows 10 should be using `Segoe MDL2
Assets`, I haven't tested this myself on a Windows 10 machine but the
fonts work fine.

Release Notes:

- N/A
2024-06-25 10:51:00 -04:00
Matin Aniss
54afa6f69f Bump image crate (#13397)
Bumping the image crate for better support of image formats.

The latest version does not have a `BGRA` type it only has `RGBA` it
doesn't really matter as the size is the same but the type name is a
little confusing as we need it as `BGRA`. Also there is no `into_bgra8`
but we can use `into_rgba8` but then it must be converted before
creating the `ImageData`.

Release Notes:

- N/A
2024-06-25 15:12:45 +02:00
Piotr Osiewicz
55511d1591 JSON: Show package.json dependency tooltips on hover (#13481)
Fixes https://github.com/zed-industries/zed/issues/13303

Release Notes:

- Added package version tooltips when hovering over package.json
dependency entries.
2024-06-25 14:59:29 +02:00
Piotr Osiewicz
6c0cb9eaa3 project panel: Respect y offset of a click-and-drag on a scrollbar (#13506)
Previously we've always snapped the scrollbar to the cursor position,
without regard for the position of the thumb.



Release Notes:

- N/A
2024-06-25 14:20:37 +02:00
Piotr Osiewicz
24e7b69f8f Rust: fix module names not showing in completion list (#13505)
This is a regression from
https://github.com/zed-industries/zed/issues/13043

Fixes https://github.com/zed-industries/zed/issues/13494



Release Notes:

- Fix crate names not being shown in Rust completions menu.
2024-06-25 13:42:29 +02:00
Antonio Scandurra
a4cdca5141 Refine UX for assistants (#13502)
<img width="1652" alt="image"
src="https://github.com/zed-industries/zed/assets/482957/376d1915-1e15-4d6c-966e-48f55f7cb249">


Release Notes:

- N/A
2024-06-25 13:41:55 +02:00
Bennet Bo Fenner
86cd87e993 indent guides: Fix edge case when line is folded (#13498)
https://github.com/zed-industries/zed/assets/53836821/6c79cdc8-c0e1-4f5e-807e-be4a4bde32c3



Release Notes:

- Fixed an edge case where some indent guides would disappear when a
folded line is the first visible line on screen
2024-06-25 12:00:40 +02:00
Tristan Hume
88000eb7e2 Fix bug with inline assist and indentation on empty lines (#13489)
Fix a minor bug when the inline assistant model spits out an empty line
with leading indentation on it. This happens sometimes with Claude 3.5
Sonnet and currently it causes the following line to have the wrong
indentation.

Release Notes:

- N/A
2024-06-25 10:11:21 +02:00
Matt Fellenz
ab5a462e0c Use likely-newer KeyPress serial for wl clipboard (#13483)
The KeyEnter serial will be too old if another client replaces the
selection before the user unfocuses and refocuses the window (i.e.,
triggers another KeyEnter event).
The KeyPress event is more likely to be new enough.

Release Notes:

- Fixed setting clipboard sometimes not working on wayland
([#13445](https://github.com/zed-industries/zed/issues/13445)).
2024-06-24 22:16:58 -06:00
Conrad Irwin
79430fc7d2 vim: Fix panic in search (#13487)
Release Notes:

- vim: Fixed a panic when searching after focusing search bar with mouse
2024-06-24 22:16:49 -06:00
Tristan Hume
f96e4ba84f Add "Select Enclosing Symbol" command (#13435)
I use this for a much faster workflow with inline assist when using fast
models.

Release Notes:

- Added "Select Enclosing Symbol" command based on tree-sitter outline.
Useful in combination with inline assist to rewrite a function.
2024-06-24 21:26:47 -06:00
SELO
7be1ffb9ec Add cross-compilation support from MacOS to Windows (#13382)
- Modify `build.rs` to use environment variables instead of `cfg`
directive to make cross-compilation to Windows possible
- Make `embed-resource` a global build-dependency for cross-compilation

Release Notes:

- N/A
2024-06-24 21:26:24 -06:00
Aaron Ruan
93a5d0ca29 Add limit to minimum window size (#13126)
Release Notes:

- Add a limit to the minimum window size on macOS.

Here's the minimum window before change:
<img width="121" alt="image"
src="https://github.com/zed-industries/zed/assets/38318044/9e907194-42e5-457e-91ea-96613426b479">

After change:
<img width="410" alt="image"
src="https://github.com/zed-industries/zed/assets/38318044/6e9c3057-9860-4f4b-9a73-c158ebac5ba9">
2024-06-24 21:26:13 -06:00
张小白
328d98dddc windows: Make IME input on Windows consistent with that on macOS + some small fixs (#13386)
### Before

![Screenshot 2024-06-22
180000](https://github.com/zed-industries/zed/assets/14981363/9efc3ccd-553c-4fe8-ada5-1b54f80bfbd5)

### After

![Screenshot 2024-06-22
175850](https://github.com/zed-industries/zed/assets/14981363/172675ca-9fda-45ae-bd97-59c33573a766)


Release Notes:

- N/A
2024-06-24 20:55:15 -06:00
张小白
76ab9e4d66 macOS: Fix IME action when deleting last char (#13385)
Closes #12862 


https://github.com/zed-industries/zed/assets/14981363/170b1206-5894-4b90-bd5c-79761073d8f2


Release Notes:

- Fixed deleting the last character during IME composition would
mistakenly delete other characters.(#12862)
2024-06-24 20:54:36 -06:00
Conrad Irwin
c477c12956 x11: Fix handling of shift key (#13485)
Fixes: #13306

Release Notes:

- N/A
2024-06-24 20:37:46 -06:00
Conrad Irwin
1ffd87b87e Fix dev server projects reconnect (#13484)
Release Notes:

- Improved handling of client reconnects for dev-servers (alpha group
only)
2024-06-24 20:36:05 -06:00
Mikayla Maki
df11b646da Fix Linux search issues (#13479)
In some rare cases, we wouldn't pick up .gitignore files in the right
order, causing performance issues for the project search and the file
finder

Release Notes:

- N/A
2024-06-24 13:46:21 -07:00
114 changed files with 3894 additions and 1329 deletions

View File

@@ -254,7 +254,7 @@ jobs:
target/aarch64-apple-darwin/release/Zed-aarch64.dmg
target/x86_64-apple-darwin/release/Zed-x86_64.dmg
target/release/Zed.dmg
body_file: target/release-notes.md
body_path: target/release-notes.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -41,7 +41,7 @@ We plan to set aside time each week to pair program with contributors on promisi
Zed is made up of several smaller crates - let's go over those you're most likely to interact with:
- [`gpui`](/crates/gpui) is a GPU-accelerated UI framework which provides all of the building blocks for Zed. **We recommend familiarizing yourself with the root level GPUI documentation**
- [`gpui`](/crates/gpui) is a GPU-accelerated UI framework which provides all of the building blocks for Zed. **We recommend familiarizing yourself with the root level GPUI documentation.**
- [`editor`](/crates/editor) contains the core `Editor` type that drives both the code editor and all various input fields within Zed. It also handles a display layer for LSP features such as Inlay Hints or code completions.
- [`project`](/crates/project) manages files and navigation within the filetree. It is also Zed's side of communication with LSP.
- [`workspace`](/crates/workspace) handles local state serialization and groups projects together.

418
Cargo.lock generated
View File

@@ -34,12 +34,6 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "adler32"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
[[package]]
name = "aes"
version = "0.8.4"
@@ -139,6 +133,12 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
[[package]]
name = "aligned-vec"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1"
[[package]]
name = "allocator-api2"
version = "0.2.16"
@@ -284,6 +284,17 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110"
[[package]]
name = "arg_enum_proc_macro"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.59",
]
[[package]]
name = "arrayref"
version = "0.3.7"
@@ -986,6 +997,29 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "av1-grain"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf"
dependencies = [
"anyhow",
"arrayvec",
"log",
"nom",
"num-rational",
"v_frame",
]
[[package]]
name = "avif-serialize"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "876c75a42f6364451a033496a14c44bffe41f5f4a8236f697391f11024e596d2"
dependencies = [
"arrayvec",
]
[[package]]
name = "aws-config"
version = "1.1.5"
@@ -1432,7 +1466,7 @@ dependencies = [
"cc",
"cfg-if",
"libc",
"miniz_oxide 0.7.1",
"miniz_oxide",
"object",
"rustc-demangle",
]
@@ -1555,6 +1589,12 @@ version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]]
name = "bit_field"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61"
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -1570,6 +1610,12 @@ dependencies = [
"serde",
]
[[package]]
name = "bitstream-io"
version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "415f8399438eb5e4b2f73ed3152a3448b98149dda642a957ee704e1daa5cf1d8"
[[package]]
name = "bitvec"
version = "1.0.1"
@@ -1751,6 +1797,12 @@ dependencies = [
"serde",
]
[[package]]
name = "built"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6a6c0b39c38fd754ac338b00a88066436389c0f029da5d37d1e01091d9b7c17"
[[package]]
name = "bumpalo"
version = "3.14.0"
@@ -1805,6 +1857,12 @@ version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "byteorder-lite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "bytes"
version = "0.4.12"
@@ -2033,6 +2091,16 @@ dependencies = [
"nom",
]
[[package]]
name = "cfg-expr"
version = "0.15.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
dependencies = [
"smallvec",
"target-lexicon",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
@@ -3238,16 +3306,6 @@ dependencies = [
"util",
]
[[package]]
name = "deflate"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174"
dependencies = [
"adler32",
"byteorder",
]
[[package]]
name = "deflate64"
version = "0.1.8"
@@ -3830,6 +3888,22 @@ dependencies = [
"libc",
]
[[package]]
name = "exr"
version = "1.72.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4"
dependencies = [
"bit_field",
"flume",
"half",
"lebe",
"miniz_oxide",
"rayon-core",
"smallvec",
"zune-inflate",
]
[[package]]
name = "extension"
version = "0.1.0"
@@ -4112,7 +4186,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010"
dependencies = [
"crc32fast",
"miniz_oxide 0.7.1",
"miniz_oxide",
]
[[package]]
@@ -4586,9 +4660,9 @@ dependencies = [
[[package]]
name = "gif"
version = "0.11.4"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06"
checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2"
dependencies = [
"color_quant",
"weezl",
@@ -4845,6 +4919,7 @@ dependencies = [
"taffy",
"thiserror",
"time",
"unicode-segmentation",
"usvg",
"util",
"uuid",
@@ -5017,6 +5092,12 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "heed"
version = "0.20.1"
@@ -5349,21 +5430,35 @@ dependencies = [
[[package]]
name = "image"
version = "0.23.14"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1"
checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11"
dependencies = [
"bytemuck",
"byteorder",
"color_quant",
"exr",
"gif",
"jpeg-decoder",
"num-iter",
"num-rational 0.3.2",
"image-webp",
"num-traits",
"png 0.16.8",
"scoped_threadpool",
"png",
"qoi",
"ravif",
"rayon",
"rgb",
"tiff",
"zune-core",
"zune-jpeg",
]
[[package]]
name = "image-webp"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d730b085583c4d789dfd07fdcf185be59501666a90c97c40162b37e4fdad272d"
dependencies = [
"byteorder-lite",
"thiserror",
]
[[package]]
@@ -5385,6 +5480,12 @@ version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284"
[[package]]
name = "imgref"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126"
[[package]]
name = "indexmap"
version = "1.9.3"
@@ -5498,6 +5599,17 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "interpolate_name"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.59",
]
[[package]]
name = "io-extras"
version = "0.18.1"
@@ -5699,12 +5811,9 @@ dependencies = [
[[package]]
name = "jpeg-decoder"
version = "0.1.22"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2"
dependencies = [
"rayon",
]
checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0"
[[package]]
name = "js-sys"
@@ -5953,12 +6062,29 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67"
[[package]]
name = "lebe"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
[[package]]
name = "libc"
version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]]
name = "libfuzzer-sys"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7"
dependencies = [
"arbitrary",
"cc",
"once_cell",
]
[[package]]
name = "libgit2-sys"
version = "0.17.0+1.8.1"
@@ -6151,6 +6277,15 @@ dependencies = [
"value-bag",
]
[[package]]
name = "loop9"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062"
dependencies = [
"imgref",
]
[[package]]
name = "lsp"
version = "0.1.0"
@@ -6326,6 +6461,16 @@ version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4"
[[package]]
name = "maybe-rayon"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519"
dependencies = [
"cfg-if",
"rayon",
]
[[package]]
name = "md-5"
version = "0.10.5"
@@ -6433,25 +6578,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435"
dependencies = [
"adler32",
]
[[package]]
name = "miniz_oxide"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b"
dependencies = [
"adler",
"autocfg",
]
[[package]]
name = "miniz_oxide"
version = "0.7.1"
@@ -6681,6 +6807,12 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "noop_proc_macro"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
[[package]]
name = "notifications"
version = "0.1.0"
@@ -6746,7 +6878,7 @@ dependencies = [
"num-complex",
"num-integer",
"num-iter",
"num-rational 0.4.1",
"num-rational",
"num-traits",
]
@@ -6822,6 +6954,17 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "num-derive"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.59",
]
[[package]]
name = "num-format"
version = "0.4.4"
@@ -6853,17 +6996,6 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.1"
@@ -6972,7 +7104,7 @@ dependencies = [
"jni 0.20.0",
"ndk",
"ndk-context",
"num-derive",
"num-derive 0.3.3",
"num-traits",
"oboe-sys",
]
@@ -7660,18 +7792,6 @@ dependencies = [
"plotters-backend",
]
[[package]]
name = "png"
version = "0.16.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6"
dependencies = [
"bitflags 1.3.2",
"crc32fast",
"deflate",
"miniz_oxide 0.3.7",
]
[[package]]
name = "png"
version = "0.17.13"
@@ -7682,7 +7802,7 @@ dependencies = [
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide 0.7.1",
"miniz_oxide",
]
[[package]]
@@ -8106,6 +8226,21 @@ dependencies = [
"unicase",
]
[[package]]
name = "qoi"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
dependencies = [
"bytemuck",
]
[[package]]
name = "quick-error"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quick-xml"
version = "0.30.0"
@@ -8229,6 +8364,56 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "977b1e897f9d764566891689e642653e5ed90c6895106acd005eb4c1d0203991"
[[package]]
name = "rav1e"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9"
dependencies = [
"arbitrary",
"arg_enum_proc_macro",
"arrayvec",
"av1-grain",
"bitstream-io",
"built",
"cfg-if",
"interpolate_name",
"itertools 0.12.1",
"libc",
"libfuzzer-sys",
"log",
"maybe-rayon",
"new_debug_unreachable",
"noop_proc_macro",
"num-derive 0.4.2",
"num-traits",
"once_cell",
"paste",
"profiling",
"rand 0.8.5",
"rand_chacha 0.3.1",
"simd_helpers",
"system-deps",
"thiserror",
"v_frame",
"wasm-bindgen",
]
[[package]]
name = "ravif"
version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc13288f5ab39e6d7c9d501759712e6969fcc9734220846fc9ed26cae2cc4234"
dependencies = [
"avif-serialize",
"imgref",
"loop9",
"quick-error",
"rav1e",
"rayon",
"rgb",
]
[[package]]
name = "raw-window-handle"
version = "0.5.2"
@@ -9028,12 +9213,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "scoped_threadpool"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8"
[[package]]
name = "scopeguard"
version = "1.2.0"
@@ -9560,6 +9739,15 @@ version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]]
name = "simd_helpers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6"
dependencies = [
"quote",
]
[[package]]
name = "simdutf8"
version = "0.1.4"
@@ -10368,6 +10556,19 @@ dependencies = [
"windows 0.52.0",
]
[[package]]
name = "system-deps"
version = "6.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
dependencies = [
"cfg-expr",
"heck 0.5.0",
"pkg-config",
"toml 0.8.10",
"version-compare",
]
[[package]]
name = "system-interface"
version = "0.27.1"
@@ -10707,12 +10908,12 @@ dependencies = [
[[package]]
name = "tiff"
version = "0.6.1"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437"
checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e"
dependencies = [
"flate2",
"jpeg-decoder",
"miniz_oxide 0.4.4",
"weezl",
]
@@ -10792,7 +10993,7 @@ dependencies = [
"bytemuck",
"cfg-if",
"log",
"png 0.17.13",
"png",
"tiny-skia-path",
]
@@ -11672,6 +11873,17 @@ dependencies = [
"sha1_smol",
]
[[package]]
name = "v_frame"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b"
dependencies = [
"aligned-vec",
"num-traits",
"wasm-bindgen",
]
[[package]]
name = "valuable"
version = "0.1.0"
@@ -11734,6 +11946,12 @@ dependencies = [
"workspace",
]
[[package]]
name = "version-compare"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b"
[[package]]
name = "version_check"
version = "0.9.4"
@@ -13332,7 +13550,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.142.0"
version = "0.143.0"
dependencies = [
"activity_indicator",
"anyhow",
@@ -13751,6 +13969,30 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "zune-core"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
[[package]]
name = "zune-inflate"
version = "0.2.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
dependencies = [
"simd-adler32",
]
[[package]]
name = "zune-jpeg"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec866b44a2a1fd6133d363f073ca1b179f438f99e7e5bfb1e33f7181facfe448"
dependencies = [
"zune-core",
]
[[package]]
name = "zvariant"
version = "4.0.2"

View File

@@ -308,7 +308,7 @@ heed = { version = "0.20.1", features = ["read-txn-no-tls"] }
hex = "0.4.3"
html5ever = "0.27.0"
ignore = "0.4.22"
image = "0.23"
image = "0.25.1"
indexmap = { version = "1.6.2", features = ["serde"] }
indoc = "1"
# We explicitly disable http2 support in isahc.

1
assets/icons/book.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-book"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/></svg>

After

Width:  |  Height:  |  Size: 289 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-book-copy"><path d="M2 16V4a2 2 0 0 1 2-2h11"/><path d="M5 14H4a2 2 0 1 0 0 4h1"/><path d="M22 18H11a2 2 0 1 0 0 4h11V6H11a2 2 0 0 0-2 2v12"/></svg>

After

Width:  |  Height:  |  Size: 351 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-book-plus"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/><path d="M9 10h6"/><path d="M12 7v6"/></svg>

After

Width:  |  Height:  |  Size: 332 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevrons-up-down"><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></svg>

After

Width:  |  Height:  |  Size: 276 B

1
assets/icons/font.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-type"><polyline points="4 7 4 4 20 4 20 7"/><line x1="9" x2="15" y1="20" y2="20"/><line x1="12" x2="12" y1="4" y2="20"/></svg>

After

Width:  |  Height:  |  Size: 329 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-a-large-small"><path d="M21 14h-5"/><path d="M16 16v-3.5a2.5 2.5 0 0 1 5 0V16"/><path d="M4.5 13h6"/><path d="m3 16 4.5-9 4.5 9"/></svg>

After

Width:  |  Height:  |  Size: 339 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bold"><path d="M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8"/></svg>

After

Width:  |  Height:  |  Size: 296 B

View File

@@ -0,0 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 13.6667H12" stroke="#B3B3B3" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 2.33333H12" stroke="#B3B3B3" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 11L8 5L11 11" stroke="#B3B3B3" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 9H10" stroke="#B3B3B3" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 539 B

1
assets/icons/visible.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-eye"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg>

After

Width:  |  Height:  |  Size: 301 B

View File

@@ -70,6 +70,14 @@
{
"context": "ProjectPanel",
"bindings": {
"a": "project_panel::NewFile",
"shift-a": "project_panel::NewDirectory",
"f2": "project_panel::Rename",
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
"shift-d": "project_panel::Duplicate",
"cmd-x": "project_panel::Cut",
"cmd-c": "project_panel::Copy",
"cmd-v": "project_panel::Paste",
"ctrl-[": "project_panel::CollapseSelectedEntry",
"ctrl-b": "project_panel::CollapseSelectedEntry",
"alt-b": "project_panel::CollapseSelectedEntry",

View File

@@ -152,7 +152,8 @@
// "focus": false
// }
// ],
"ctrl->": "assistant::QuoteSelection"
"ctrl->": "assistant::QuoteSelection",
"ctrl-alt-e": "editor::SelectEnclosingSymbol"
}
},
{
@@ -586,8 +587,9 @@
"alt-ctrl-shift-c": "project_panel::CopyRelativePath",
"f2": "project_panel::Rename",
"enter": "project_panel::Rename",
"backspace": "project_panel::Trash",
"delete": "project_panel::Trash",
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
"shift-delete": ["project_panel::Delete", { "skip_prompt": false }],
"delete": ["project_panel::Trash", { "skip_prompt": false }],
"ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
"alt-ctrl-r": "project_panel::RevealInFinder",

View File

@@ -188,7 +188,8 @@
"focus": false
}
],
"cmd->": "assistant::QuoteSelection"
"cmd->": "assistant::QuoteSelection",
"cmd-alt-e": "editor::SelectEnclosingSymbol"
}
},
{
@@ -604,6 +605,7 @@
"left": "project_panel::CollapseSelectedEntry",
"right": "project_panel::ExpandSelectedEntry",
"cmd-n": "project_panel::NewFile",
"cmd-d": "project_panel::Duplicate",
"alt-cmd-n": "project_panel::NewDirectory",
"cmd-x": "project_panel::Cut",
"cmd-c": "project_panel::Copy",
@@ -613,8 +615,9 @@
"enter": "project_panel::Rename",
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
"delete": ["project_panel::Trash", { "skip_prompt": false }],
"cmd-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"cmd-backspace": ["project_panel::Trash", { "skip_prompt": true }],
"cmd-delete": ["project_panel::Delete", { "skip_prompt": false }],
"cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"alt-cmd-r": "project_panel::RevealInFinder",
"alt-shift-f": "project_panel::NewSearchInDirectory",
"shift-down": "menu::SelectNext",

View File

@@ -78,6 +78,7 @@
"bindings": {
"cmd-shift-o": "file_finder::Toggle",
"cmd-shift-a": "command_palette::Toggle",
"shift shift": "command_palette::Toggle",
"cmd-alt-o": "project_symbols::Toggle",
"cmd-1": "workspace::ToggleLeftDock",
"cmd-6": "diagnostics::Deploy"
@@ -94,6 +95,10 @@
"context": "ProjectPanel",
"bindings": {
"enter": "project_panel::Open",
"cmd-backspace": ["project_panel::Trash", { "skip_prompt": false }],
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
"delete": ["project_panel::Trash", { "skip_prompt": false }],
"shift-delete": ["project_panel::Delete", { "skip_prompt": false }],
"shift-f6": "project_panel::Rename"
}
}

View File

@@ -87,7 +87,15 @@
},
{
"context": "ProjectPanel",
"bindings": {}
"bindings": {
"cmd-backspace": ["project_panel::Trash", { "skip_prompt": true }],
"cmd-d": "project_panel::Duplicate",
"cmd-n": "project_panel::NewFolder",
"return": "project_panel::Rename",
"cmd-c": "project_panel::Copy",
"cmd-v": "project_panel::Paste",
"cmd-alt-c": "project_panel::CopyPath"
}
},
{
"context": "Dock",

View File

@@ -688,7 +688,9 @@
// "TOML": ["Embargo.lock"]
// }
//
"file_types": {},
"file_types": {
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json"]
},
// The extensions that Zed should automatically install on startup.
//
// If you don't want any of these extensions, add this field to your settings

View File

@@ -1,5 +1,3 @@
version: "3.7"
services:
postgres:
image: postgres:15

View File

@@ -10,14 +10,14 @@ mod search;
mod slash_command;
mod streaming_diff;
pub use assistant_panel::AssistantPanel;
pub use assistant_panel::{AssistantPanel, AssistantPanelEvent};
use assistant_settings::{AnthropicModel, AssistantSettings, CloudModel, OllamaModel, OpenAiModel};
use assistant_slash_command::SlashCommandRegistry;
use client::{proto, Client};
use command_palette_hooks::CommandPaletteFilter;
pub(crate) use completion_provider::*;
pub(crate) use context_store::*;
use fs::Fs;
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
pub(crate) use inline_assistant::*;
pub(crate) use model_selector::*;
@@ -264,7 +264,7 @@ impl Assistant {
}
}
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, cx: &mut AppContext) {
cx.set_global(Assistant::default());
AssistantSettings::register(cx);
@@ -288,7 +288,7 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
assistant_slash_command::init(cx);
register_slash_commands(cx);
assistant_panel::init(cx);
inline_assistant::init(client.telemetry().clone(), cx);
inline_assistant::init(fs.clone(), client.telemetry().clone(), cx);
RustdocStore::init_global(cx);
CommandPaletteFilter::update_global(cx, |filter, _cx| {
@@ -324,6 +324,24 @@ fn register_slash_commands(cx: &mut AppContext) {
slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
}
pub fn humanize_token_count(count: usize) -> String {
match count {
0..=999 => count.to_string(),
1000..=9999 => {
let thousands = count / 1000;
let hundreds = (count % 1000 + 50) / 100;
if hundreds == 0 {
format!("{}k", thousands)
} else if hundreds == 10 {
format!("{}k", thousands + 1)
} else {
format!("{}.{}k", thousands, hundreds)
}
}
_ => format!("{}k", (count + 500) / 1000),
}
}
#[cfg(test)]
#[ctor::ctor]
fn init_logger() {

View File

@@ -1,5 +1,6 @@
use crate::{
assistant_settings::{AssistantDockPosition, AssistantSettings},
humanize_token_count,
prompt_library::open_prompt_library,
search::*,
slash_command::{
@@ -89,6 +90,10 @@ pub fn init(cx: &mut AppContext) {
.detach();
}
pub enum AssistantPanelEvent {
ContextEdited,
}
pub struct AssistantPanel {
workspace: WeakView<Workspace>,
width: Option<Pixels>,
@@ -360,11 +365,11 @@ impl AssistantPanel {
return;
}
let Some(assistant) = workspace.panel::<AssistantPanel>(cx) else {
let Some(assistant_panel) = workspace.panel::<AssistantPanel>(cx) else {
return;
};
let context_editor = assistant
let context_editor = assistant_panel
.read(cx)
.active_context_editor()
.and_then(|editor| {
@@ -391,25 +396,37 @@ impl AssistantPanel {
return;
};
if assistant.update(cx, |assistant, cx| assistant.is_authenticated(cx)) {
if assistant_panel.update(cx, |panel, cx| panel.is_authenticated(cx)) {
InlineAssistant::update_global(cx, |assistant, cx| {
assistant.assist(
&active_editor,
Some(cx.view().downgrade()),
include_context,
include_context.then_some(&assistant_panel),
cx,
)
})
} else {
let assistant = assistant.downgrade();
let assistant_panel = assistant_panel.downgrade();
cx.spawn(|workspace, mut cx| async move {
assistant
assistant_panel
.update(&mut cx, |assistant, cx| assistant.authenticate(cx))?
.await?;
if assistant.update(&mut cx, |assistant, cx| assistant.is_authenticated(cx))? {
if assistant_panel
.update(&mut cx, |assistant, cx| assistant.is_authenticated(cx))?
{
cx.update(|cx| {
let assistant_panel = if include_context {
assistant_panel.upgrade()
} else {
None
};
InlineAssistant::update_global(cx, |assistant, cx| {
assistant.assist(&active_editor, Some(workspace), include_context, cx)
assistant.assist(
&active_editor,
Some(workspace),
assistant_panel.as_ref(),
cx,
)
})
})?
} else {
@@ -460,7 +477,7 @@ impl AssistantPanel {
_subscriptions: subscriptions,
});
self.show_saved_contexts = false;
cx.emit(AssistantPanelEvent::ContextEdited);
cx.notify();
}
@@ -472,6 +489,7 @@ impl AssistantPanel {
) {
match event {
ContextEditorEvent::TabContentChanged => cx.notify(),
ContextEditorEvent::Edited => cx.emit(AssistantPanelEvent::ContextEdited),
}
}
@@ -863,18 +881,33 @@ impl AssistantPanel {
context: &Model<Context>,
cx: &mut ViewContext<Self>,
) -> Option<impl IntoElement> {
let remaining_tokens = context.read(cx).remaining_tokens(cx)?;
let remaining_tokens_color = if remaining_tokens <= 0 {
let model = CompletionProvider::global(cx).model();
let token_count = context.read(cx).token_count()?;
let max_token_count = model.max_token_count();
let remaining_tokens = max_token_count as isize - token_count as isize;
let token_count_color = if remaining_tokens <= 0 {
Color::Error
} else if remaining_tokens <= 500 {
} else if token_count as f32 / max_token_count as f32 >= 0.8 {
Color::Warning
} else {
Color::Muted
};
Some(
Label::new(remaining_tokens.to_string())
.size(LabelSize::Small)
.color(remaining_tokens_color),
h_flex()
.gap_0p5()
.child(
Label::new(humanize_token_count(token_count))
.size(LabelSize::Small)
.color(token_count_color),
)
.child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
.child(
Label::new(humanize_token_count(max_token_count))
.size(LabelSize::Small)
.color(Color::Muted),
),
)
}
}
@@ -978,6 +1011,7 @@ impl Panel for AssistantPanel {
}
impl EventEmitter<PanelEvent> for AssistantPanel {}
impl EventEmitter<AssistantPanelEvent> for AssistantPanel {}
impl FocusableView for AssistantPanel {
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
@@ -1538,11 +1572,6 @@ impl Context {
}
}
fn remaining_tokens(&self, cx: &AppContext) -> Option<isize> {
let model = CompletionProvider::global(cx).model();
Some(model.max_token_count() as isize - self.token_count? as isize)
}
fn completion_provider_changed(&mut self, cx: &mut ModelContext<Self>) {
self.count_remaining_tokens(cx);
}
@@ -2183,6 +2212,7 @@ struct PendingCompletion {
}
enum ContextEditorEvent {
Edited,
TabContentChanged,
}
@@ -2775,6 +2805,7 @@ impl ContextEditor {
EditorEvent::SelectionsChanged { .. } => {
self.scroll_position = self.cursor_scroll_position(cx);
}
EditorEvent::BufferEdited => cx.emit(ContextEditorEvent::Edited),
_ => {}
}
}

View File

@@ -169,6 +169,7 @@ pub enum AssistantProvider {
model: OpenAiModel,
api_url: String,
low_speed_timeout_in_seconds: Option<u64>,
available_models: Vec<OpenAiModel>,
},
Anthropic {
model: AnthropicModel,
@@ -188,6 +189,7 @@ impl Default for AssistantProvider {
model: OpenAiModel::default(),
api_url: open_ai::OPEN_AI_API_URL.into(),
low_speed_timeout_in_seconds: None,
available_models: Default::default(),
}
}
}
@@ -202,6 +204,7 @@ pub enum AssistantProviderContent {
default_model: Option<OpenAiModel>,
api_url: Option<String>,
low_speed_timeout_in_seconds: Option<u64>,
available_models: Option<Vec<OpenAiModel>>,
},
#[serde(rename = "anthropic")]
Anthropic {
@@ -272,6 +275,7 @@ impl AssistantSettingsContent {
default_model: settings.default_open_ai_model.clone(),
api_url: Some(open_ai_api_url.clone()),
low_speed_timeout_in_seconds: None,
available_models: Some(Default::default()),
})
} else {
settings.default_open_ai_model.clone().map(|open_ai_model| {
@@ -279,6 +283,7 @@ impl AssistantSettingsContent {
default_model: Some(open_ai_model),
api_url: None,
low_speed_timeout_in_seconds: None,
available_models: Some(Default::default()),
}
})
},
@@ -326,6 +331,14 @@ impl AssistantSettingsContent {
*model = Some(new_model);
}
}
Some(AssistantProviderContent::Ollama {
default_model: model,
..
}) => {
if let LanguageModel::Ollama(new_model) = new_model {
*model = Some(new_model);
}
}
provider => match new_model {
LanguageModel::Cloud(model) => {
*provider = Some(AssistantProviderContent::ZedDotDev {
@@ -337,6 +350,7 @@ impl AssistantSettingsContent {
default_model: Some(model),
api_url: None,
low_speed_timeout_in_seconds: None,
available_models: Some(Default::default()),
})
}
LanguageModel::Anthropic(model) => {
@@ -481,15 +495,18 @@ impl Settings for AssistantSettings {
model,
api_url,
low_speed_timeout_in_seconds,
available_models,
},
AssistantProviderContent::OpenAi {
default_model: model_override,
api_url: api_url_override,
low_speed_timeout_in_seconds: low_speed_timeout_in_seconds_override,
available_models: available_models_override,
},
) => {
merge(model, model_override);
merge(api_url, api_url_override);
merge(available_models, available_models_override);
if let Some(low_speed_timeout_in_seconds_override) =
low_speed_timeout_in_seconds_override
{
@@ -550,10 +567,12 @@ impl Settings for AssistantSettings {
default_model: model,
api_url,
low_speed_timeout_in_seconds,
available_models,
} => AssistantProvider::OpenAi {
model: model.unwrap_or_default(),
api_url: api_url.unwrap_or_else(|| open_ai::OPEN_AI_API_URL.into()),
low_speed_timeout_in_seconds,
available_models: available_models.unwrap_or_default(),
},
AssistantProviderContent::Anthropic {
default_model: model,
@@ -610,6 +629,7 @@ mod tests {
model: OpenAiModel::FourOmni,
api_url: open_ai::OPEN_AI_API_URL.into(),
low_speed_timeout_in_seconds: None,
available_models: Default::default(),
}
);
@@ -632,6 +652,7 @@ mod tests {
model: OpenAiModel::FourOmni,
api_url: "test-url".into(),
low_speed_timeout_in_seconds: None,
available_models: Default::default(),
}
);
SettingsStore::update_global(cx, |store, cx| {
@@ -652,6 +673,7 @@ mod tests {
model: OpenAiModel::Four,
api_url: open_ai::OPEN_AI_API_URL.into(),
low_speed_timeout_in_seconds: None,
available_models: Default::default(),
}
);

View File

@@ -24,6 +24,20 @@ use settings::{Settings, SettingsStore};
use std::sync::Arc;
use std::time::Duration;
/// Choose which model to use for openai provider.
/// If the model is not available, try to use the first available model, or fallback to the original model.
fn choose_openai_model(
model: &::open_ai::Model,
available_models: &[::open_ai::Model],
) -> ::open_ai::Model {
available_models
.iter()
.find(|&m| m == model)
.or_else(|| available_models.first())
.unwrap_or_else(|| model)
.clone()
}
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
let mut settings_version = 0;
let provider = match &AssistantSettings::get_global(cx).provider {
@@ -34,8 +48,9 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
model,
api_url,
low_speed_timeout_in_seconds,
available_models,
} => CompletionProvider::OpenAi(OpenAiCompletionProvider::new(
model.clone(),
choose_openai_model(model, available_models),
api_url.clone(),
client.http_client(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
@@ -77,10 +92,11 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
model,
api_url,
low_speed_timeout_in_seconds,
available_models,
},
) => {
provider.update(
model.clone(),
choose_openai_model(model, available_models),
api_url.clone(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
settings_version,
@@ -136,10 +152,11 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
model,
api_url,
low_speed_timeout_in_seconds,
available_models,
},
) => {
*provider = CompletionProvider::OpenAi(OpenAiCompletionProvider::new(
model.clone(),
choose_openai_model(model, available_models),
api_url.clone(),
client.http_client(),
low_speed_timeout_in_seconds.map(Duration::from_secs),
@@ -201,10 +218,10 @@ impl CompletionProvider {
cx.global::<Self>()
}
pub fn available_models(&self) -> Vec<LanguageModel> {
pub fn available_models(&self, cx: &AppContext) -> Vec<LanguageModel> {
match self {
CompletionProvider::OpenAi(provider) => provider
.available_models()
.available_models(cx)
.map(LanguageModel::OpenAi)
.collect(),
CompletionProvider::Anthropic(provider) => provider

View File

@@ -1,4 +1,5 @@
use crate::assistant_settings::CloudModel;
use crate::assistant_settings::{AssistantProvider, AssistantSettings};
use crate::{
assistant_settings::OpenAiModel, CompletionProvider, LanguageModel, LanguageModelRequest, Role,
};
@@ -56,8 +57,26 @@ impl OpenAiCompletionProvider {
self.settings_version = settings_version;
}
pub fn available_models(&self) -> impl Iterator<Item = OpenAiModel> {
OpenAiModel::iter()
pub fn available_models(&self, cx: &AppContext) -> impl Iterator<Item = OpenAiModel> {
if let AssistantProvider::OpenAi {
available_models, ..
} = &AssistantSettings::get_global(cx).provider
{
if !available_models.is_empty() {
// available_models is set, just return it
return available_models.clone().into_iter();
}
}
let available_models = if matches!(self.model, OpenAiModel::Custom { .. }) {
// available_models is not set but the default model is set to custom, only show custom
vec![self.model.clone()]
} else {
// default case, use all models except custom
OpenAiModel::iter()
.filter(|model| !matches!(model, OpenAiModel::Custom { .. }))
.collect()
};
available_models.into_iter()
}
pub fn settings_version(&self) -> usize {
@@ -213,7 +232,8 @@ pub fn count_open_ai_tokens(
| LanguageModel::Cloud(CloudModel::Claude3_5Sonnet)
| LanguageModel::Cloud(CloudModel::Claude3Opus)
| LanguageModel::Cloud(CloudModel::Claude3Sonnet)
| LanguageModel::Cloud(CloudModel::Claude3Haiku) => {
| LanguageModel::Cloud(CloudModel::Claude3Haiku)
| LanguageModel::OpenAi(OpenAiModel::Custom { .. }) => {
// 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

@@ -1,8 +1,9 @@
use crate::{
prompts::generate_content_prompt, AssistantPanel, CompletionProvider, Hunk,
LanguageModelRequest, LanguageModelRequestMessage, Role, StreamingDiff,
assistant_settings::AssistantSettings, humanize_token_count, prompts::generate_content_prompt,
AssistantPanel, AssistantPanelEvent, CompletionProvider, Hunk, LanguageModelRequest,
LanguageModelRequestMessage, Role, StreamingDiff,
};
use anyhow::{Context as _, Result};
use anyhow::{anyhow, Context as _, Result};
use client::telemetry::Telemetry;
use collections::{hash_map, HashMap, HashSet, VecDeque};
use editor::{
@@ -14,6 +15,7 @@ use editor::{
Anchor, AnchorRangeExt, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle,
ExcerptRange, GutterDimensions, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
};
use fs::Fs;
use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
use gpui::{
point, AppContext, EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight, Global,
@@ -24,7 +26,7 @@ use language::{Buffer, Point, Selection, TransactionId};
use multi_buffer::MultiBufferRow;
use parking_lot::Mutex;
use rope::Rope;
use settings::Settings;
use settings::{update_settings_file, Settings};
use similar::TextDiff;
use std::{
cmp, mem,
@@ -32,15 +34,15 @@ use std::{
pin::Pin,
sync::Arc,
task::{self, Poll},
time::Instant,
time::{Duration, Instant},
};
use theme::ThemeSettings;
use ui::{prelude::*, Tooltip};
use ui::{prelude::*, ContextMenu, PopoverMenu, Tooltip};
use util::RangeExt;
use workspace::{notifications::NotificationId, Toast, Workspace};
pub fn init(telemetry: Arc<Telemetry>, cx: &mut AppContext) {
cx.set_global(InlineAssistant::new(telemetry));
pub fn init(fs: Arc<dyn Fs>, telemetry: Arc<Telemetry>, cx: &mut AppContext) {
cx.set_global(InlineAssistant::new(fs, telemetry));
}
const PROMPT_HISTORY_MAX_LEN: usize = 20;
@@ -53,12 +55,13 @@ pub struct InlineAssistant {
assist_groups: HashMap<InlineAssistGroupId, InlineAssistGroup>,
prompt_history: VecDeque<String>,
telemetry: Option<Arc<Telemetry>>,
fs: Arc<dyn Fs>,
}
impl Global for InlineAssistant {}
impl InlineAssistant {
pub fn new(telemetry: Arc<Telemetry>) -> Self {
pub fn new(fs: Arc<dyn Fs>, telemetry: Arc<Telemetry>) -> Self {
Self {
next_assist_id: InlineAssistId::default(),
next_assist_group_id: InlineAssistGroupId::default(),
@@ -67,6 +70,7 @@ impl InlineAssistant {
assist_groups: HashMap::default(),
prompt_history: VecDeque::default(),
telemetry: Some(telemetry),
fs,
}
}
@@ -74,7 +78,7 @@ impl InlineAssistant {
&mut self,
editor: &View<Editor>,
workspace: Option<WeakView<Workspace>>,
include_context: bool,
assistant_panel: Option<&View<AssistantPanel>>,
cx: &mut WindowContext,
) {
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
@@ -151,7 +155,10 @@ impl InlineAssistant {
self.prompt_history.clone(),
prompt_buffer.clone(),
codegen.clone(),
editor,
assistant_panel,
workspace.clone(),
self.fs.clone(),
cx,
)
});
@@ -208,7 +215,7 @@ impl InlineAssistant {
InlineAssist::new(
assist_id,
assist_group_id,
include_context,
assistant_panel.is_some(),
editor,
&prompt_editor,
block_ids[0],
@@ -706,8 +713,6 @@ impl InlineAssistant {
return;
}
assist.codegen.update(cx, |codegen, cx| codegen.undo(cx));
let Some(user_prompt) = assist
.decorations
.as_ref()
@@ -716,115 +721,138 @@ impl InlineAssistant {
return;
};
let context = if assist.include_context {
assist.workspace.as_ref().and_then(|workspace| {
let workspace = workspace.upgrade()?.read(cx);
let assistant_panel = workspace.panel::<AssistantPanel>(cx)?;
assistant_panel.read(cx).active_context(cx)
})
} else {
None
};
let editor = if let Some(editor) = assist.editor.upgrade() {
editor
} else {
return;
};
let project_name = assist.workspace.as_ref().and_then(|workspace| {
let workspace = workspace.upgrade()?;
Some(
workspace
.read(cx)
.project()
.read(cx)
.worktree_root_names(cx)
.collect::<Vec<&str>>()
.join("/"),
)
});
self.prompt_history.retain(|prompt| *prompt != user_prompt);
self.prompt_history.push_back(user_prompt.clone());
if self.prompt_history.len() > PROMPT_HISTORY_MAX_LEN {
self.prompt_history.pop_front();
}
assist.codegen.update(cx, |codegen, cx| codegen.undo(cx));
let codegen = assist.codegen.clone();
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
let range = codegen.read(cx).range.clone();
let start = snapshot.point_to_buffer_offset(range.start);
let end = snapshot.point_to_buffer_offset(range.end);
let (buffer, range) = if let Some((start, end)) = start.zip(end) {
let (start_buffer, start_buffer_offset) = start;
let (end_buffer, end_buffer_offset) = end;
if start_buffer.remote_id() == end_buffer.remote_id() {
(start_buffer.clone(), start_buffer_offset..end_buffer_offset)
} else {
self.finish_assist(assist_id, false, cx);
return;
}
} else {
self.finish_assist(assist_id, false, cx);
return;
};
let language = buffer.language_at(range.start);
let language_name = if let Some(language) = language.as_ref() {
if Arc::ptr_eq(language, &language::PLAIN_TEXT) {
None
} else {
Some(language.name())
}
} else {
None
};
// Higher Temperature increases the randomness of model outputs.
// If Markdown or No Language is Known, increase the randomness for more creative output
// If Code, decrease temperature to get more deterministic outputs
let temperature = if let Some(language) = language_name.clone() {
if language.as_ref() == "Markdown" {
1.0
} else {
0.5
}
} else {
1.0
};
let prompt = cx.background_executor().spawn(async move {
let language_name = language_name.as_deref();
generate_content_prompt(user_prompt, language_name, buffer, range, project_name)
});
let mut messages = Vec::new();
if let Some(context) = context {
let request = context.read(cx).to_completion_request(cx);
messages = request.messages;
}
let model = CompletionProvider::global(cx).model();
let request = self.request_for_inline_assist(assist_id, cx);
cx.spawn(|mut cx| async move {
let prompt = prompt.await?;
let request = request.await?;
codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx))?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
fn request_for_inline_assist(
&self,
assist_id: InlineAssistId,
cx: &mut WindowContext,
) -> Task<Result<LanguageModelRequest>> {
cx.spawn(|mut cx| async move {
let (user_prompt, context_request, project_name, buffer, range, model) = cx
.read_global(|this: &InlineAssistant, cx: &WindowContext| {
let assist = this.assists.get(&assist_id).context("invalid assist")?;
let decorations = assist.decorations.as_ref().context("invalid assist")?;
let editor = assist.editor.upgrade().context("invalid assist")?;
let user_prompt = decorations.prompt_editor.read(cx).prompt(cx);
let context_request = if assist.include_context {
assist.workspace.as_ref().and_then(|workspace| {
let workspace = workspace.upgrade()?.read(cx);
let assistant_panel = workspace.panel::<AssistantPanel>(cx)?;
Some(
assistant_panel
.read(cx)
.active_context(cx)?
.read(cx)
.to_completion_request(cx),
)
})
} else {
None
};
let project_name = assist.workspace.as_ref().and_then(|workspace| {
let workspace = workspace.upgrade()?;
Some(
workspace
.read(cx)
.project()
.read(cx)
.worktree_root_names(cx)
.collect::<Vec<&str>>()
.join("/"),
)
});
let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
let range = assist.codegen.read(cx).range.clone();
let model = CompletionProvider::global(cx).model();
anyhow::Ok((
user_prompt,
context_request,
project_name,
buffer,
range,
model,
))
})??;
let language = buffer.language_at(range.start);
let language_name = if let Some(language) = language.as_ref() {
if Arc::ptr_eq(language, &language::PLAIN_TEXT) {
None
} else {
Some(language.name())
}
} else {
None
};
// Higher Temperature increases the randomness of model outputs.
// If Markdown or No Language is Known, increase the randomness for more creative output
// If Code, decrease temperature to get more deterministic outputs
let temperature = if let Some(language) = language_name.clone() {
if language.as_ref() == "Markdown" {
1.0
} else {
0.5
}
} else {
1.0
};
let prompt = cx
.background_executor()
.spawn(async move {
let language_name = language_name.as_deref();
let start = buffer.point_to_buffer_offset(range.start);
let end = buffer.point_to_buffer_offset(range.end);
let (buffer, range) = if let Some((start, end)) = start.zip(end) {
let (start_buffer, start_buffer_offset) = start;
let (end_buffer, end_buffer_offset) = end;
if start_buffer.remote_id() == end_buffer.remote_id() {
(start_buffer.clone(), start_buffer_offset..end_buffer_offset)
} else {
return Err(anyhow!("invalid transformation range"));
}
} else {
return Err(anyhow!("invalid transformation range"));
};
generate_content_prompt(user_prompt, language_name, buffer, range, project_name)
})
.await?;
let mut messages = Vec::new();
if let Some(context_request) = context_request {
messages = context_request.messages;
}
messages.push(LanguageModelRequestMessage {
role: Role::User,
content: prompt,
});
let request = LanguageModelRequest {
Ok(LanguageModelRequest {
model,
messages,
stop: vec!["|END|>".to_string()],
temperature,
};
codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx))?;
anyhow::Ok(())
})
})
.detach_and_log_err(cx);
}
fn stop_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
@@ -1142,6 +1170,7 @@ enum PromptEditorEvent {
struct PromptEditor {
id: InlineAssistId,
fs: Arc<dyn Fs>,
height_in_lines: u8,
editor: View<Editor>,
edited_since_done: bool,
@@ -1150,9 +1179,12 @@ struct PromptEditor {
prompt_history_ix: Option<usize>,
pending_prompt: String,
codegen: Model<Codegen>,
workspace: Option<WeakView<Workspace>>,
_codegen_subscription: Subscription,
editor_subscriptions: Vec<Subscription>,
pending_token_count: Task<Result<()>>,
token_count: Option<usize>,
_token_count_subscriptions: Vec<Subscription>,
workspace: Option<WeakView<Workspace>>,
}
impl EventEmitter<PromptEditorEvent> for PromptEditor {}
@@ -1160,6 +1192,7 @@ impl EventEmitter<PromptEditorEvent> for PromptEditor {}
impl Render for PromptEditor {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let gutter_dimensions = *self.gutter_dimensions.lock();
let fs = self.fs.clone();
let buttons = match &self.codegen.read(cx).status {
CodegenStatus::Idle => {
@@ -1245,85 +1278,101 @@ impl Render for PromptEditor {
}
};
v_flex().h_full().w_full().justify_end().child(
h_flex()
.bg(cx.theme().colors().editor_background)
.border_y_1()
.border_color(cx.theme().status().info_border)
.py_1p5()
.w_full()
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::move_up))
.on_action(cx.listener(Self::move_down))
.child(
h_flex()
.w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))
// .pr(gutter_dimensions.fold_area_width())
.justify_center()
.gap_2()
.children(self.workspace.clone().map(|workspace| {
IconButton::new("context", IconName::Context)
.size(ButtonSize::None)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.on_click({
let workspace = workspace.clone();
cx.listener(move |_, _, cx| {
workspace
.update(cx, |workspace, cx| {
workspace.focus_panel::<AssistantPanel>(cx);
})
.ok();
})
h_flex()
.bg(cx.theme().colors().editor_background)
.border_y_1()
.border_color(cx.theme().status().info_border)
.py_1p5()
.h_full()
.w_full()
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::move_up))
.on_action(cx.listener(Self::move_down))
.child(
h_flex()
.w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))
.justify_center()
.gap_2()
.child(
PopoverMenu::new("model-switcher")
.menu(move |cx| {
ContextMenu::build(cx, |mut menu, cx| {
for model in CompletionProvider::global(cx).available_models(cx)
{
menu = menu.custom_entry(
{
let model = model.clone();
move |_| {
Label::new(model.display_name())
.into_any_element()
}
},
{
let fs = fs.clone();
let model = model.clone();
move |cx| {
let model = model.clone();
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings| settings.set_model(model),
);
}
},
);
}
menu
})
.tooltip(move |cx| {
let token_count = workspace.upgrade().and_then(|workspace| {
let panel =
workspace.read(cx).panel::<AssistantPanel>(cx)?;
let context = panel.read(cx).active_context(cx)?;
context.read(cx).token_count()
});
if let Some(token_count) = token_count {
.into()
})
.trigger(
IconButton::new("context", IconName::Settings)
.size(ButtonSize::None)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.tooltip(move |cx| {
Tooltip::with_meta(
format!(
"{} Additional Context Tokens from Assistant",
token_count
"Using {}",
CompletionProvider::global(cx)
.model()
.display_name()
),
Some(&crate::ToggleFocus),
"Click to open…",
None,
"Click to Change Model",
cx,
)
} else {
Tooltip::for_action(
"Toggle Assistant Panel",
&crate::ToggleFocus,
cx,
)
}
})
}))
.children(
if let CodegenStatus::Error(error) = &self.codegen.read(cx).status {
let error_message = SharedString::from(error.to_string());
Some(
div()
.id("error")
.tooltip(move |cx| Tooltip::text(error_message.clone(), cx))
.child(
Icon::new(IconName::XCircle)
.size(IconSize::Small)
.color(Color::Error),
),
)
} else {
None
},
),
)
.child(div().flex_1().child(self.render_prompt_editor(cx)))
.child(h_flex().gap_2().pr_4().children(buttons)),
)
}),
)
.anchor(gpui::AnchorCorner::BottomRight),
)
.children(
if let CodegenStatus::Error(error) = &self.codegen.read(cx).status {
let error_message = SharedString::from(error.to_string());
Some(
div()
.id("error")
.tooltip(move |cx| Tooltip::text(error_message.clone(), cx))
.child(
Icon::new(IconName::XCircle)
.size(IconSize::Small)
.color(Color::Error),
),
)
} else {
None
},
),
)
.child(div().flex_1().child(self.render_prompt_editor(cx)))
.child(
h_flex()
.gap_2()
.pr_4()
.children(self.render_token_count(cx))
.children(buttons),
)
}
}
@@ -1336,13 +1385,17 @@ impl FocusableView for PromptEditor {
impl PromptEditor {
const MAX_LINES: u8 = 8;
#[allow(clippy::too_many_arguments)]
fn new(
id: InlineAssistId,
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
prompt_history: VecDeque<String>,
prompt_buffer: Model<MultiBuffer>,
codegen: Model<Codegen>,
parent_editor: &View<Editor>,
assistant_panel: Option<&View<AssistantPanel>>,
workspace: Option<WeakView<Workspace>>,
fs: Arc<dyn Fs>,
cx: &mut ViewContext<Self>,
) -> Self {
let prompt_editor = cx.new_view(|cx| {
@@ -1363,6 +1416,15 @@ impl PromptEditor {
editor.set_placeholder_text("Add a prompt…", cx);
editor
});
let mut token_count_subscriptions = Vec::new();
token_count_subscriptions
.push(cx.subscribe(parent_editor, Self::handle_parent_editor_event));
if let Some(assistant_panel) = assistant_panel {
token_count_subscriptions
.push(cx.subscribe(assistant_panel, Self::handle_assistant_panel_event));
}
let mut this = Self {
id,
height_in_lines: 1,
@@ -1375,9 +1437,14 @@ impl PromptEditor {
_codegen_subscription: cx.observe(&codegen, Self::handle_codegen_changed),
editor_subscriptions: Vec::new(),
codegen,
fs,
pending_token_count: Task::ready(Ok(())),
token_count: None,
_token_count_subscriptions: token_count_subscriptions,
workspace,
};
this.count_lines(cx);
this.count_tokens(cx);
this.subscribe_to_editor(cx);
this
}
@@ -1436,6 +1503,47 @@ impl PromptEditor {
}
}
fn handle_parent_editor_event(
&mut self,
_: View<Editor>,
event: &EditorEvent,
cx: &mut ViewContext<Self>,
) {
if let EditorEvent::BufferEdited { .. } = event {
self.count_tokens(cx);
}
}
fn handle_assistant_panel_event(
&mut self,
_: View<AssistantPanel>,
event: &AssistantPanelEvent,
cx: &mut ViewContext<Self>,
) {
let AssistantPanelEvent::ContextEdited { .. } = event;
self.count_tokens(cx);
}
fn count_tokens(&mut self, cx: &mut ViewContext<Self>) {
let assist_id = self.id;
self.pending_token_count = cx.spawn(|this, mut cx| async move {
cx.background_executor().timer(Duration::from_secs(1)).await;
let request = cx
.update_global(|inline_assistant: &mut InlineAssistant, cx| {
inline_assistant.request_for_inline_assist(assist_id, cx)
})?
.await?;
let token_count = cx
.update(|cx| CompletionProvider::global(cx).count_tokens(request, cx))?
.await?;
this.update(&mut cx, |this, cx| {
this.token_count = Some(token_count);
cx.notify();
})
})
}
fn handle_prompt_editor_changed(&mut self, _: View<Editor>, cx: &mut ViewContext<Self>) {
self.count_lines(cx);
}
@@ -1460,6 +1568,9 @@ impl PromptEditor {
self.edited_since_done = true;
cx.notify();
}
EditorEvent::BufferEdited => {
self.count_tokens(cx);
}
_ => {}
}
}
@@ -1551,6 +1662,63 @@ impl PromptEditor {
}
}
fn render_token_count(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
let model = CompletionProvider::global(cx).model();
let token_count = self.token_count?;
let max_token_count = model.max_token_count();
let remaining_tokens = max_token_count as isize - token_count as isize;
let token_count_color = if remaining_tokens <= 0 {
Color::Error
} else if token_count as f32 / max_token_count as f32 >= 0.8 {
Color::Warning
} else {
Color::Muted
};
let mut token_count = h_flex()
.id("token_count")
.gap_0p5()
.child(
Label::new(humanize_token_count(token_count))
.size(LabelSize::Small)
.color(token_count_color),
)
.child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
.child(
Label::new(humanize_token_count(max_token_count))
.size(LabelSize::Small)
.color(Color::Muted),
);
if let Some(workspace) = self.workspace.clone() {
token_count = token_count
.tooltip(|cx| {
Tooltip::with_meta(
"Tokens Used by Inline Assistant",
None,
"Click to Open Assistant Panel",
cx,
)
})
.cursor_pointer()
.on_mouse_down(gpui::MouseButton::Left, |_, cx| cx.stop_propagation())
.on_click(move |_, cx| {
cx.stop_propagation();
workspace
.update(cx, |workspace, cx| {
workspace.focus_panel::<AssistantPanel>(cx)
})
.ok();
});
} else {
token_count = token_count
.cursor_default()
.tooltip(|cx| Tooltip::text("Tokens Used by Inline Assistant", cx));
}
Some(token_count)
}
fn render_prompt_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
@@ -1893,6 +2061,11 @@ impl Codegen {
if lines.peek().is_some() {
hunks_tx.send(diff.push_new("\n")).await?;
if line_indent.is_none() {
// Don't write out the leading indentation in empty lines on the next line
// This is the case where the above if statement didn't clear the buffer
new_text.clear();
}
line_indent = None;
first_line = false;
}

View File

@@ -23,7 +23,7 @@ impl RenderOnce for ModelSelector {
.with_handle(self.handle)
.menu(move |cx| {
ContextMenu::build(cx, |mut menu, cx| {
for model in CompletionProvider::global(cx).available_models() {
for model in CompletionProvider::global(cx).available_models(cx) {
menu = menu.custom_entry(
{
let model = model.clone();

View File

@@ -6,16 +6,16 @@ use anyhow::{anyhow, Result};
use assistant_slash_command::SlashCommandRegistry;
use chrono::{DateTime, Utc};
use collections::HashMap;
use editor::{actions::Tab, CurrentLineHighlight, Editor, EditorEvent};
use editor::{actions::Tab, CurrentLineHighlight, Editor, EditorElement, EditorEvent, EditorStyle};
use futures::{
future::{self, BoxFuture, Shared},
FutureExt,
};
use fuzzy::StringMatchCandidate;
use gpui::{
actions, percentage, point, size, Animation, AnimationExt, AppContext, BackgroundExecutor,
Bounds, EventEmitter, Global, PromptLevel, ReadGlobal, Subscription, Task, TitlebarOptions,
Transformation, UpdateGlobal, View, WindowBounds, WindowHandle, WindowOptions,
actions, point, size, transparent_black, AppContext, BackgroundExecutor, Bounds, EventEmitter,
Global, HighlightStyle, PromptLevel, ReadGlobal, Subscription, Task, TextStyle,
TitlebarOptions, UpdateGlobal, View, WindowBounds, WindowHandle, WindowOptions,
};
use heed::{types::SerdeBincode, Database, RoTxn};
use language::{language_settings::SoftWrap, Buffer, LanguageRegistry};
@@ -109,12 +109,13 @@ pub struct PromptLibrary {
}
struct PromptEditor {
editor: View<Editor>,
title_editor: View<Editor>,
body_editor: View<Editor>,
token_count: Option<usize>,
pending_token_count: Task<Option<()>>,
next_body_to_save: Option<Rope>,
next_title_and_body_to_save: Option<(String, Rope)>,
pending_save: Option<Task<Option<()>>>,
_subscription: Subscription,
_subscriptions: Vec<Subscription>,
}
struct PromptPickerDelegate {
@@ -345,7 +346,8 @@ impl PromptLibrary {
let prompt_metadata = self.store.metadata(prompt_id).unwrap();
let prompt_editor = self.prompt_editors.get_mut(&prompt_id).unwrap();
let body = prompt_editor.editor.update(cx, |editor, cx| {
let title = prompt_editor.title_editor.read(cx).text(cx);
let body = prompt_editor.body_editor.update(cx, |editor, cx| {
editor
.buffer()
.read(cx)
@@ -359,20 +361,24 @@ impl PromptLibrary {
let store = self.store.clone();
let executor = cx.background_executor().clone();
prompt_editor.next_body_to_save = Some(body);
prompt_editor.next_title_and_body_to_save = Some((title, body));
if prompt_editor.pending_save.is_none() {
prompt_editor.pending_save = Some(cx.spawn(|this, mut cx| {
async move {
loop {
let next_body_to_save = this.update(&mut cx, |this, _| {
let title_and_body = this.update(&mut cx, |this, _| {
this.prompt_editors
.get_mut(&prompt_id)?
.next_body_to_save
.next_title_and_body_to_save
.take()
})?;
if let Some(body) = next_body_to_save {
let title = title_from_body(body.chars_at(0));
if let Some((title, body)) = title_and_body {
let title = if title.trim().is_empty() {
None
} else {
Some(SharedString::from(title))
};
store
.save(prompt_id, title, prompt_metadata.default, body)
.await
@@ -425,11 +431,11 @@ impl PromptLibrary {
if let Some(prompt_editor) = self.prompt_editors.get(&prompt_id) {
if focus {
prompt_editor
.editor
.body_editor
.update(cx, |editor, cx| editor.focus(cx));
}
self.set_active_prompt(Some(prompt_id), cx);
} else {
} else if let Some(prompt_metadata) = self.store.metadata(prompt_id) {
let language_registry = self.language_registry.clone();
let commands = SlashCommandRegistry::global(cx);
let prompt = self.store.load(prompt_id);
@@ -438,13 +444,20 @@ impl PromptLibrary {
let markdown = language_registry.language_for_name("Markdown").await;
this.update(&mut cx, |this, cx| match prompt {
Ok(prompt) => {
let buffer = cx.new_model(|cx| {
let mut buffer = Buffer::local(prompt, cx);
buffer.set_language(markdown.log_err(), cx);
buffer.set_language_registry(language_registry);
buffer
let title_editor = cx.new_view(|cx| {
let mut editor = Editor::auto_width(cx);
editor.set_placeholder_text("Untitled", cx);
editor.set_text(prompt_metadata.title.unwrap_or_default(), cx);
editor
});
let editor = cx.new_view(|cx| {
let body_editor = cx.new_view(|cx| {
let buffer = cx.new_model(|cx| {
let mut buffer = Buffer::local(prompt, cx);
buffer.set_language(markdown.log_err(), cx);
buffer.set_language_registry(language_registry);
buffer
});
let mut editor = Editor::for_buffer(buffer, None, cx);
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
editor.set_show_gutter(false, cx);
@@ -460,19 +473,24 @@ impl PromptLibrary {
}
editor
});
let _subscription =
cx.subscribe(&editor, move |this, _editor, event, cx| {
this.handle_prompt_editor_event(prompt_id, event, cx)
});
let _subscriptions = vec![
cx.subscribe(&title_editor, move |this, editor, event, cx| {
this.handle_prompt_title_editor_event(prompt_id, editor, event, cx)
}),
cx.subscribe(&body_editor, move |this, editor, event, cx| {
this.handle_prompt_body_editor_event(prompt_id, editor, event, cx)
}),
];
this.prompt_editors.insert(
prompt_id,
PromptEditor {
editor,
next_body_to_save: None,
title_editor,
body_editor,
next_title_and_body_to_save: None,
pending_save: None,
token_count: None,
pending_token_count: Task::ready(None),
_subscription,
_subscriptions,
},
);
this.set_active_prompt(Some(prompt_id), cx);
@@ -549,7 +567,7 @@ impl PromptLibrary {
fn focus_active_prompt(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
if let Some(active_prompt) = self.active_prompt_id {
self.prompt_editors[&active_prompt]
.editor
.body_editor
.update(cx, |editor, cx| editor.focus(cx));
cx.stop_propagation();
}
@@ -565,11 +583,11 @@ impl PromptLibrary {
return;
};
let prompt_editor = &self.prompt_editors[&active_prompt_id].editor;
let prompt_editor = &self.prompt_editors[&active_prompt_id].body_editor;
let provider = CompletionProvider::global(cx);
if provider.is_authenticated() {
InlineAssistant::update_global(cx, |assistant, cx| {
assistant.assist(&prompt_editor, None, false, cx)
assistant.assist(&prompt_editor, None, None, cx)
})
} else {
for window in cx.windows() {
@@ -589,50 +607,73 @@ impl PromptLibrary {
}
}
fn handle_prompt_editor_event(
fn move_down_from_title(&mut self, _: &editor::actions::MoveDown, cx: &mut ViewContext<Self>) {
if let Some(prompt_id) = self.active_prompt_id {
if let Some(prompt_editor) = self.prompt_editors.get(&prompt_id) {
cx.focus_view(&prompt_editor.body_editor);
}
}
}
fn move_up_from_body(&mut self, _: &editor::actions::MoveUp, cx: &mut ViewContext<Self>) {
if let Some(prompt_id) = self.active_prompt_id {
if let Some(prompt_editor) = self.prompt_editors.get(&prompt_id) {
cx.focus_view(&prompt_editor.title_editor);
}
}
}
fn handle_prompt_title_editor_event(
&mut self,
prompt_id: PromptId,
title_editor: View<Editor>,
event: &EditorEvent,
cx: &mut ViewContext<Self>,
) {
if let EditorEvent::BufferEdited = event {
let prompt_editor = self.prompt_editors.get(&prompt_id).unwrap();
let buffer = prompt_editor
.editor
.read(cx)
.buffer()
.read(cx)
.as_singleton()
.unwrap();
match event {
EditorEvent::BufferEdited => {
self.save_prompt(prompt_id, cx);
self.count_tokens(prompt_id, cx);
}
EditorEvent::Blurred => {
title_editor.update(cx, |title_editor, cx| {
title_editor.change_selections(None, cx, |selections| {
let cursor = selections.oldest_anchor().head();
selections.select_anchor_ranges([cursor..cursor]);
});
});
}
_ => {}
}
}
buffer.update(cx, |buffer, cx| {
let mut chars = buffer.chars_at(0);
match chars.next() {
Some('#') => {
if chars.next() != Some(' ') {
drop(chars);
buffer.edit([(1..1, " ")], None, cx);
}
}
Some(' ') => {
drop(chars);
buffer.edit([(0..0, "#")], None, cx);
}
_ => {
drop(chars);
buffer.edit([(0..0, "# ")], None, cx);
}
}
});
self.save_prompt(prompt_id, cx);
self.count_tokens(prompt_id, cx);
fn handle_prompt_body_editor_event(
&mut self,
prompt_id: PromptId,
body_editor: View<Editor>,
event: &EditorEvent,
cx: &mut ViewContext<Self>,
) {
match event {
EditorEvent::BufferEdited => {
self.save_prompt(prompt_id, cx);
self.count_tokens(prompt_id, cx);
}
EditorEvent::Blurred => {
body_editor.update(cx, |body_editor, cx| {
body_editor.change_selections(None, cx, |selections| {
let cursor = selections.oldest_anchor().head();
selections.select_anchor_ranges([cursor..cursor]);
});
});
}
_ => {}
}
}
fn count_tokens(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
if let Some(prompt) = self.prompt_editors.get_mut(&prompt_id) {
let editor = &prompt.editor.read(cx);
let editor = &prompt.body_editor.read(cx);
let buffer = &editor.buffer().read(cx).as_singleton().unwrap().read(cx);
let body = buffer.as_rope().clone();
prompt.pending_token_count = cx.spawn(|this, mut cx| {
@@ -708,122 +749,214 @@ impl PromptLibrary {
.flex_none()
.min_w_64()
.children(self.active_prompt_id.and_then(|prompt_id| {
let buffer_font = ThemeSettings::get_global(cx).buffer_font.family.clone();
let prompt_metadata = self.store.metadata(prompt_id)?;
let prompt_editor = &self.prompt_editors[&prompt_id];
let focus_handle = prompt_editor.editor.focus_handle(cx);
let focus_handle = prompt_editor.body_editor.focus_handle(cx);
let current_model = CompletionProvider::global(cx).model();
let token_count = prompt_editor.token_count.map(|count| count.to_string());
let settings = ThemeSettings::get_global(cx);
Some(
h_flex()
v_flex()
.id("prompt-editor-inner")
.size_full()
.items_start()
.relative()
.overflow_hidden()
.pl(Spacing::XXLarge.rems(cx))
.pt(Spacing::Large.rems(cx))
.on_click(cx.listener(move |_, _, cx| {
cx.focus(&focus_handle);
}))
.child(
div()
.on_action(cx.listener(Self::focus_picker))
.on_action(cx.listener(Self::inline_assist))
.flex_grow()
.h_full()
.pt(Spacing::XXLarge.rems(cx))
.pl(Spacing::XXLarge.rems(cx))
.child(prompt_editor.editor.clone()),
)
.child(
v_flex()
.w_12()
.py(Spacing::Large.rems(cx))
.justify_start()
.items_end()
.gap_1()
.child(h_flex().h_8().font_family(buffer_font).when_some_else(
token_count,
|tokens_ready, token_count| {
tokens_ready.pr_3().justify_end().child(
// This isn't actually a button, it just let's us easily add
// a tooltip to the token count.
Button::new("token_count", token_count.clone())
.style(ButtonStyle::Transparent)
.color(Color::Muted)
.tooltip(move |cx| {
Tooltip::with_meta(
format!("{} tokens", token_count,),
None,
format!(
"Model: {}",
current_model.display_name()
),
cx,
)
}),
)
},
|tokens_loading| {
tokens_loading.w_12().justify_center().child(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.color(Color::Muted)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(4)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(
percentage(delta),
))
},
),
)
},
))
h_flex()
.group("active-editor-header")
.pr(Spacing::XXLarge.rems(cx))
.pt(Spacing::XSmall.rems(cx))
.pb(Spacing::Large.rems(cx))
.justify_between()
.child(
h_flex().justify_center().w_12().h_8().child(
IconButton::new("toggle-default-prompt", IconName::Sparkle)
.style(ButtonStyle::Transparent)
.selected(prompt_metadata.default)
.selected_icon(IconName::SparkleFilled)
.icon_color(if prompt_metadata.default {
Color::Accent
} else {
Color::Muted
})
.shape(IconButtonShape::Square)
.tooltip(move |cx| {
Tooltip::text(
if prompt_metadata.default {
"Remove from Default Prompt"
} else {
"Add to Default Prompt"
},
cx,
h_flex().gap_1().child(
div()
.max_w_80()
.on_action(cx.listener(Self::move_down_from_title))
.border_1()
.border_color(transparent_black())
.rounded_md()
.group_hover("active-editor-header", |this| {
this.border_color(
cx.theme().colors().border_variant,
)
})
.on_click(|_, cx| {
cx.dispatch_action(Box::new(ToggleDefaultPrompt));
}),
.child(EditorElement::new(
&prompt_editor.title_editor,
EditorStyle {
background: cx.theme().system().transparent,
local_player: cx.theme().players().local(),
text: TextStyle {
color: cx
.theme()
.colors()
.editor_foreground,
font_family: settings
.ui_font
.family
.clone(),
font_features: settings
.ui_font
.features
.clone(),
font_size: HeadlineSize::Large
.size()
.into(),
font_weight: settings.ui_font.weight,
line_height: relative(
settings.buffer_line_height.value(),
),
..Default::default()
},
scrollbar_width: Pixels::ZERO,
syntax: cx.theme().syntax().clone(),
status: cx.theme().status().clone(),
inlay_hints_style: HighlightStyle {
color: Some(cx.theme().status().hint),
..HighlightStyle::default()
},
suggestions_style: HighlightStyle {
color: Some(cx.theme().status().predictive),
..HighlightStyle::default()
},
},
)),
),
)
.child(
h_flex().justify_center().w_12().h_8().child(
IconButton::new("delete-prompt", IconName::Trash)
.size(ButtonSize::Large)
.style(ButtonStyle::Transparent)
.shape(IconButtonShape::Square)
.tooltip(move |cx| {
Tooltip::for_action(
"Delete Prompt",
&DeletePrompt,
cx,
h_flex()
.h_full()
.child(
h_flex()
.h_full()
.gap(Spacing::XXLarge.rems(cx))
.child(div()),
)
.child(
h_flex()
.h_full()
.gap(Spacing::XXLarge.rems(cx))
.children(prompt_editor.token_count.map(
|token_count| {
let token_count: SharedString =
token_count.to_string().into();
let label_token_count: SharedString =
token_count.to_string().into();
h_flex()
.id("token_count")
.tooltip(move |cx| {
let token_count =
token_count.clone();
Tooltip::with_meta(
format!(
"{} tokens",
token_count.clone()
),
None,
format!(
"Model: {}",
current_model
.display_name()
),
cx,
)
})
.child(
Label::new(format!(
"{} tokens",
label_token_count.clone()
))
.color(Color::Muted),
)
},
))
.child(
IconButton::new(
"delete-prompt",
IconName::Trash,
)
.size(ButtonSize::Large)
.style(ButtonStyle::Transparent)
.shape(IconButtonShape::Square)
.size(ButtonSize::Large)
.tooltip(move |cx| {
Tooltip::for_action(
"Delete Prompt",
&DeletePrompt,
cx,
)
})
.on_click(|_, cx| {
cx.dispatch_action(Box::new(DeletePrompt));
}),
)
})
.on_click(|_, cx| {
cx.dispatch_action(Box::new(DeletePrompt));
}),
),
// .child(
// IconButton::new(
// "duplicate-prompt",
// IconName::BookCopy,
// )
// .size(ButtonSize::Large)
// .style(ButtonStyle::Transparent)
// .shape(IconButtonShape::Square)
// .size(ButtonSize::Large)
// .tooltip(move |cx| {
// Tooltip::for_action(
// "Duplicate Prompt",
// &gpui::NoAction,
// cx,
// )
// })
// .disabled(true),
// )
.child(
IconButton::new(
"toggle-default-prompt",
IconName::Sparkle,
)
.style(ButtonStyle::Transparent)
.selected(prompt_metadata.default)
.selected_icon(IconName::SparkleFilled)
.icon_color(if prompt_metadata.default {
Color::Accent
} else {
Color::Muted
})
.shape(IconButtonShape::Square)
.size(ButtonSize::Large)
.tooltip(move |cx| {
Tooltip::text(
if prompt_metadata.default {
"Remove from Default Prompt"
} else {
"Add to Default Prompt"
},
cx,
)
})
.on_click(|_, cx| {
cx.dispatch_action(Box::new(
ToggleDefaultPrompt,
));
}),
),
),
),
)
.child(
div()
.on_action(cx.listener(Self::focus_picker))
.on_action(cx.listener(Self::inline_assist))
.on_action(cx.listener(Self::move_up_from_body))
.flex_grow()
.h_full()
.child(prompt_editor.body_editor.clone()),
),
)
}))
@@ -1115,24 +1248,3 @@ pub struct GlobalPromptStore(
);
impl Global for GlobalPromptStore {}
fn title_from_body(body: impl IntoIterator<Item = char>) -> Option<SharedString> {
let mut chars = body.into_iter().take_while(|c| *c != '\n').peekable();
let mut level = 0;
while let Some('#') = chars.peek() {
level += 1;
chars.next();
}
if level > 0 {
let title = chars.collect::<String>().trim().to_string();
if title.is_empty() {
None
} else {
Some(title.into())
}
} else {
None
}
}

View File

@@ -6,118 +6,106 @@ pub fn generate_content_prompt(
language_name: Option<&str>,
buffer: BufferSnapshot,
range: Range<usize>,
project_name: Option<String>,
_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
"Here's a file of text that I'm going to ask you to make an edit to."
)?;
"Code"
"text"
}
Some(language_name) => {
writeln!(
prompt,
"Here's a file of {language_name} that I'm going to ask you to make an edit to."
)?;
"code"
}
};
if let Some(project_name) = project_name {
writeln!(
prompt,
"You are currently working inside the '{project_name}' project in code editor Zed."
)?;
}
writeln!(
prompt,
"The user has the following file open in the editor:"
)?;
const MAX_CTX: usize = 50000;
let mut is_truncated = false;
if range.is_empty() {
write!(prompt, "```")?;
if let Some(language_name) = language_name {
write!(prompt, "{language_name}")?;
}
for chunk in buffer.as_rope().chunks_in_range(0..range.start) {
prompt.push_str(chunk);
}
prompt.push_str("<|CURSOR|>");
for chunk in buffer.as_rope().chunks_in_range(range.start..buffer.len()) {
prompt.push_str(chunk);
}
if !prompt.ends_with('\n') {
prompt.push('\n');
}
writeln!(prompt, "```")?;
prompt.push('\n');
writeln!(
prompt,
"Assume the cursor is located where the `<|CURSOR|>` 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();
prompt.push_str("The point you'll need to insert at is marked with <insert_here></insert_here>.\n\n<document>");
} else {
write!(prompt, "```")?;
for chunk in buffer.as_rope().chunks() {
prompt.push_str(chunk);
}
if !prompt.ends_with('\n') {
prompt.push('\n');
}
writeln!(prompt, "```")?;
prompt.push('\n');
writeln!(
prompt,
"In particular, the following piece of text is selected:"
)?;
write!(prompt, "```")?;
if let Some(language_name) = language_name {
write!(prompt, "{language_name}")?;
}
prompt.push('\n');
prompt.push_str("The section you'll need to rewrite is marked with <rewrite_this></rewrite_this> tags.\n\n<document>");
}
// Include file content.
let before_range = 0..range.start;
let truncated_before = if before_range.len() > MAX_CTX {
is_truncated = true;
range.start - MAX_CTX..range.start
} else {
before_range
};
let mut non_rewrite_len = truncated_before.len();
for chunk in buffer.text_for_range(truncated_before) {
prompt.push_str(chunk);
}
if !range.is_empty() {
prompt.push_str("<rewrite_this>\n");
for chunk in buffer.text_for_range(range.clone()) {
prompt.push_str(chunk);
}
if !prompt.ends_with('\n') {
prompt.push('\n');
}
writeln!(prompt, "```")?;
prompt.push('\n');
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}, not the entire file."
)
.unwrap();
prompt.push_str("\n<rewrite_this>");
} else {
prompt.push_str("<insert_here></insert_here>");
}
let after_range = range.end..buffer.len();
let truncated_after = if after_range.len() > MAX_CTX {
is_truncated = true;
range.end..range.end + MAX_CTX
} else {
after_range
};
non_rewrite_len += truncated_after.len();
for chunk in buffer.text_for_range(truncated_after) {
prompt.push_str(chunk);
}
writeln!(prompt, "Never make remarks about the output.").unwrap();
writeln!(
prompt,
"Do not return anything else, except the generated {content_type}."
)
.unwrap();
write!(prompt, "</document>\n\n").unwrap();
if is_truncated {
writeln!(prompt, "The context around the relevant section has been truncated (possibly in the middle of a line) for brevity.\n")?;
}
if range.is_empty() {
writeln!(
prompt,
"You can't replace {content_type}, your answer will be inserted in place of the `<insert_here></insert_here>` tags. Don't include the insert_here tags in your output.",
)
.unwrap();
writeln!(
prompt,
"Generate {content_type} based on the following prompt:\n\n<prompt>\n{user_prompt}\n</prompt>",
)
.unwrap();
writeln!(prompt, "Match the indentation in the original file in the inserted {content_type}, don't include any indentation on blank lines.\n").unwrap();
prompt.push_str("Immediately start with the following format with no remarks:\n\n```\n{{INSERTED_CODE}}\n```");
} else {
writeln!(prompt, "Edit the section of {content_type} in <rewrite_this></rewrite_this> tags based on the following prompt:'").unwrap();
writeln!(prompt, "\n<prompt>\n{user_prompt}\n</prompt>\n").unwrap();
let rewrite_len = range.end - range.start;
if rewrite_len < 20000 && rewrite_len * 2 < non_rewrite_len {
writeln!(prompt, "And here's the section to rewrite based on that prompt again for reference:\n\n<rewrite_this>\n").unwrap();
for chunk in buffer.text_for_range(range.clone()) {
prompt.push_str(chunk);
}
writeln!(prompt, "\n</rewrite_this>\n").unwrap();
}
writeln!(prompt, "Only make changes that are necessary to fulfill the prompt, leave everything else as-is. All surrounding {content_type} will be preserved.\n").unwrap();
write!(
prompt,
"Start at the indentation level in the original file in the rewritten {content_type}. "
)
.unwrap();
prompt.push_str("Don't stop until you've rewritten the entire section, even if you have no more changes to make, always write out the whole section with no unnecessary elisions.");
prompt.push_str("\n\nImmediately start with the following format with no remarks:\n\n```\n{{REWRITTEN_CODE}}\n```");
}
Ok(prompt)
}

View File

@@ -196,23 +196,24 @@ mod linux {
impl Detect {
pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
let path = if let Some(path) = path {
path.to_path_buf().canonicalize()
path.to_path_buf().canonicalize()?
} else {
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("libexec").join("zed-editor").canonicalize() {
Ok(path) => Ok(path),
// 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),
},
}
}?;
// libexec is the standard, lib/zed is for Arch (and other non-libexec distros),
// ./zed is for the target directory in development builds.
let possible_locations =
["../libexec/zed-editor", "../lib/zed/zed-editor", "./zed"];
possible_locations
.iter()
.find_map(|p| dir.join(p).canonicalize().ok().filter(|path| path != &cli))
.ok_or_else(|| {
anyhow!("could not find any of: {}", possible_locations.join(", "))
})?
};
Ok(App(path))
}

View File

@@ -611,6 +611,7 @@ impl Telemetry {
let request_body = EventRequestBody {
installation_id: state.installation_id.as_deref().map(Into::into),
metrics_id: state.metrics_id.as_deref().map(Into::into),
session_id: state.session_id.clone(),
is_staff: state.is_staff,
app_version: state.app_version.clone(),

View File

@@ -87,51 +87,27 @@ impl Global {
}
pub fn observed_any(&self, other: &Self) -> bool {
let mut lhs = self.0.iter();
let mut rhs = other.0.iter();
loop {
if let Some(left) = lhs.next() {
if let Some(right) = rhs.next() {
if *right > 0 && left >= right {
return true;
}
} else {
return false;
}
} else {
return false;
}
}
self.0
.iter()
.zip(other.0.iter())
.any(|(left, right)| *right > 0 && left >= right)
}
pub fn observed_all(&self, other: &Self) -> bool {
let mut lhs = self.0.iter();
let mut rhs = other.0.iter();
loop {
if let Some(left) = lhs.next() {
if let Some(right) = rhs.next() {
if left < right {
return false;
}
} else {
return true;
}
} else {
return rhs.next().is_none();
}
}
self.0.iter().all(|left| match rhs.next() {
Some(right) => left >= right,
None => true,
}) && rhs.next().is_none()
}
pub fn changed_since(&self, other: &Self) -> bool {
if self.0.len() > other.0.len() {
return true;
}
for (left, right) in self.0.iter().zip(other.0.iter()) {
if left > right {
return true;
}
}
false
self.0.len() > other.0.len()
|| self
.0
.iter()
.zip(other.0.iter())
.any(|(left, right)| left > right)
}
pub fn iter(&self) -> impl Iterator<Item = Lamport> + '_ {

View File

@@ -664,6 +664,7 @@ where
#[derive(Serialize, Debug, clickhouse::Row)]
pub struct EditorEventRow {
installation_id: String,
metrics_id: String,
operation: String,
app_version: String,
file_extension: String,
@@ -713,6 +714,7 @@ impl EditorEventRow {
os_version: body.os_version.clone().unwrap_or_default(),
architecture: body.architecture.clone(),
installation_id: body.installation_id.clone().unwrap_or_default(),
metrics_id: body.metrics_id.clone().unwrap_or_default(),
session_id: body.session_id.clone(),
is_staff: body.is_staff,
time: time.timestamp_millis(),

View File

@@ -2583,14 +2583,13 @@ async fn rejoin_dev_server_projects(
)
.await?
};
notify_rejoined_projects(&mut rejoined_projects, &session)?;
response.send(proto::RejoinRemoteProjectsResponse {
rejoined_projects: rejoined_projects
.into_iter()
.iter()
.map(|project| project.to_proto())
.collect(),
})
})?;
notify_rejoined_projects(&mut rejoined_projects, &session)
}
async fn reconnect_dev_server(

View File

@@ -73,6 +73,7 @@ impl ConnectionPool {
pub fn reset(&mut self) {
self.connections.clear();
self.connected_users.clear();
self.connected_dev_servers.clear();
self.channels.clear();
}

View File

@@ -504,6 +504,29 @@ async fn test_dev_server_reconnect(
.unwrap();
}
#[gpui::test]
async fn test_dev_server_restart(cx1: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppContext) {
let (server, client1) = TestServer::start1(cx1).await;
let (_dev_server, remote_workspace) =
create_dev_server_project(&server, client1.app_state.clone(), cx1, cx2).await;
let cx = VisualTestContext::from_window(remote_workspace.into(), cx1).as_mut();
server.reset().await;
cx.run_until_parked();
cx.simulate_keystrokes("cmd-p 1 enter");
remote_workspace
.update(cx, |ws, cx| {
ws.active_item_as::<Editor>(cx)
.unwrap()
.update(cx, |ed, cx| {
assert_eq!(ed.text(cx).to_string(), "remote\nremote\nremote");
})
})
.unwrap();
}
#[gpui::test]
async fn test_create_dev_server_project_path_validation(
cx1: &mut gpui::TestAppContext,

View File

@@ -124,5 +124,6 @@ fn notification_window_options(
display_id: Some(screen.id()),
window_background: WindowBackgroundAppearance::default(),
app_id: Some(app_id.to_owned()),
window_min_size: None,
}
}

View File

@@ -268,6 +268,7 @@ gpui::actions!(
SelectAllMatches,
SelectDown,
SelectLargerSyntaxNode,
SelectEnclosingSymbol,
SelectLeft,
SelectLine,
SelectRight,

View File

@@ -720,8 +720,7 @@ impl DisplaySnapshot {
if let Some(severity) = chunk.diagnostic_severity {
// Omit underlines for HINT/INFO diagnostics on 'unnecessary' code.
if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary {
let diagnostic_color =
super::diagnostic_style(severity, true, &editor_style.status);
let diagnostic_color = super::diagnostic_style(severity, &editor_style.status);
diagnostic_highlight.underline = Some(UnderlineStyle {
color: Some(diagnostic_color),
thickness: 1.0.into(),
@@ -957,16 +956,18 @@ impl DisplaySnapshot {
return false;
}
for next_row in (buffer_row.0 + 1)..=max_row.0 {
let next_line_indent = self.line_indent_for_buffer_row(MultiBufferRow(next_row));
if next_line_indent.raw_len() > line_indent.raw_len() {
return true;
} else if !next_line_indent.is_line_blank() {
break;
}
}
false
(buffer_row.0 + 1..=max_row.0)
.find_map(|next_row| {
let next_line_indent = self.line_indent_for_buffer_row(MultiBufferRow(next_row));
if next_line_indent.raw_len() > line_indent.raw_len() {
Some(true)
} else if !next_line_indent.is_line_blank() {
Some(false)
} else {
None
}
})
.unwrap_or(false)
}
pub fn foldable_range(

View File

@@ -335,7 +335,7 @@ pub enum SelectMode {
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum EditorMode {
SingleLine,
SingleLine { auto_width: bool },
AutoHeight { max_lines: usize },
Full,
}
@@ -1580,7 +1580,13 @@ impl Editor {
pub fn single_line(cx: &mut ViewContext<Self>) -> Self {
let buffer = cx.new_model(|cx| Buffer::local("", cx));
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
Self::new(EditorMode::SingleLine, buffer, None, false, cx)
Self::new(
EditorMode::SingleLine { auto_width: false },
buffer,
None,
false,
cx,
)
}
pub fn multi_line(cx: &mut ViewContext<Self>) -> Self {
@@ -1589,6 +1595,18 @@ impl Editor {
Self::new(EditorMode::Full, buffer, None, false, cx)
}
pub fn auto_width(cx: &mut ViewContext<Self>) -> Self {
let buffer = cx.new_model(|cx| Buffer::local("", cx));
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
Self::new(
EditorMode::SingleLine { auto_width: true },
buffer,
None,
false,
cx,
)
}
pub fn auto_height(max_lines: usize, cx: &mut ViewContext<Self>) -> Self {
let buffer = cx.new_model(|cx| Buffer::local("", cx));
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
@@ -1701,8 +1719,8 @@ impl Editor {
let blink_manager = cx.new_model(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx));
let soft_wrap_mode_override =
(mode == EditorMode::SingleLine).then(|| language_settings::SoftWrap::PreferLine);
let soft_wrap_mode_override = matches!(mode, EditorMode::SingleLine { .. })
.then(|| language_settings::SoftWrap::PreferLine);
let mut project_subscriptions = Vec::new();
if mode == EditorMode::Full {
@@ -1749,7 +1767,7 @@ impl Editor {
.detach();
cx.on_blur(&focus_handle, Self::handle_blur).detach();
let show_indent_guides = if mode == EditorMode::SingleLine {
let show_indent_guides = if matches!(mode, EditorMode::SingleLine { .. }) {
Some(false)
} else {
None
@@ -1905,7 +1923,7 @@ impl Editor {
let mut key_context = KeyContext::new_with_defaults();
key_context.add("Editor");
let mode = match self.mode {
EditorMode::SingleLine => "single_line",
EditorMode::SingleLine { .. } => "single_line",
EditorMode::AutoHeight { .. } => "auto_height",
EditorMode::Full => "full",
};
@@ -2896,6 +2914,9 @@ impl Editor {
let start_offset = TO::to_offset(&range.start, &buffer_snapshot);
let end_offset = start_offset + end_difference;
let start_offset = start_offset + start_difference;
if start_offset > buffer_snapshot.len() || end_offset > buffer_snapshot.len() {
continue;
}
let start = buffer_snapshot.anchor_after(start_offset);
let end = buffer_snapshot.anchor_after(end_offset);
linked_edits
@@ -6660,7 +6681,7 @@ impl Editor {
return;
}
if matches!(self.mode, EditorMode::SingleLine) {
if matches!(self.mode, EditorMode::SingleLine { .. }) {
cx.propagate();
return;
}
@@ -6697,7 +6718,7 @@ impl Editor {
return;
}
if matches!(self.mode, EditorMode::SingleLine) {
if matches!(self.mode, EditorMode::SingleLine { .. }) {
cx.propagate();
return;
}
@@ -6728,7 +6749,7 @@ impl Editor {
return;
}
if matches!(self.mode, EditorMode::SingleLine) {
if matches!(self.mode, EditorMode::SingleLine { .. }) {
cx.propagate();
return;
}
@@ -6791,7 +6812,7 @@ impl Editor {
return;
}
if matches!(self.mode, EditorMode::SingleLine) {
if matches!(self.mode, EditorMode::SingleLine { .. }) {
cx.propagate();
return;
}
@@ -6839,7 +6860,7 @@ impl Editor {
pub fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext<Self>) {
self.take_rename(true, cx);
if self.mode == EditorMode::SingleLine {
if matches!(self.mode, EditorMode::SingleLine { .. }) {
cx.propagate();
return;
}
@@ -6900,7 +6921,7 @@ impl Editor {
return;
}
if matches!(self.mode, EditorMode::SingleLine) {
if matches!(self.mode, EditorMode::SingleLine { .. }) {
cx.propagate();
return;
}
@@ -7248,7 +7269,7 @@ impl Editor {
_: &MoveToStartOfParagraph,
cx: &mut ViewContext<Self>,
) {
if matches!(self.mode, EditorMode::SingleLine) {
if matches!(self.mode, EditorMode::SingleLine { .. }) {
cx.propagate();
return;
}
@@ -7268,7 +7289,7 @@ impl Editor {
_: &MoveToEndOfParagraph,
cx: &mut ViewContext<Self>,
) {
if matches!(self.mode, EditorMode::SingleLine) {
if matches!(self.mode, EditorMode::SingleLine { .. }) {
cx.propagate();
return;
}
@@ -7288,7 +7309,7 @@ impl Editor {
_: &SelectToStartOfParagraph,
cx: &mut ViewContext<Self>,
) {
if matches!(self.mode, EditorMode::SingleLine) {
if matches!(self.mode, EditorMode::SingleLine { .. }) {
cx.propagate();
return;
}
@@ -7308,7 +7329,7 @@ impl Editor {
_: &SelectToEndOfParagraph,
cx: &mut ViewContext<Self>,
) {
if matches!(self.mode, EditorMode::SingleLine) {
if matches!(self.mode, EditorMode::SingleLine { .. }) {
cx.propagate();
return;
}
@@ -7324,7 +7345,7 @@ impl Editor {
}
pub fn move_to_beginning(&mut self, _: &MoveToBeginning, cx: &mut ViewContext<Self>) {
if matches!(self.mode, EditorMode::SingleLine) {
if matches!(self.mode, EditorMode::SingleLine { .. }) {
cx.propagate();
return;
}
@@ -7344,7 +7365,7 @@ impl Editor {
}
pub fn move_to_end(&mut self, _: &MoveToEnd, cx: &mut ViewContext<Self>) {
if matches!(self.mode, EditorMode::SingleLine) {
if matches!(self.mode, EditorMode::SingleLine { .. }) {
cx.propagate();
return;
}
@@ -8203,7 +8224,7 @@ impl Editor {
let advance_downwards = action.advance_downwards
&& selections_on_single_row
&& !selections_selecting
&& this.mode != EditorMode::SingleLine;
&& !matches!(this.mode, EditorMode::SingleLine { .. });
if advance_downwards {
let snapshot = this.buffer.read(cx).snapshot(cx);
@@ -8226,6 +8247,58 @@ impl Editor {
});
}
pub fn select_enclosing_symbol(
&mut self,
_: &SelectEnclosingSymbol,
cx: &mut ViewContext<Self>,
) {
let buffer = self.buffer.read(cx).snapshot(cx);
let old_selections = self.selections.all::<usize>(cx).into_boxed_slice();
fn update_selection(
selection: &Selection<usize>,
buffer_snap: &MultiBufferSnapshot,
) -> Option<Selection<usize>> {
let cursor = selection.head();
let (_buffer_id, symbols) = buffer_snap.symbols_containing(cursor, None)?;
for symbol in symbols.iter().rev() {
let start = symbol.range.start.to_offset(&buffer_snap);
let end = symbol.range.end.to_offset(&buffer_snap);
let new_range = start..end;
if start < selection.start || end > selection.end {
return Some(Selection {
id: selection.id,
start: new_range.start,
end: new_range.end,
goal: SelectionGoal::None,
reversed: selection.reversed,
});
}
}
None
}
let mut selected_larger_symbol = false;
let new_selections = old_selections
.iter()
.map(|selection| match update_selection(selection, &buffer) {
Some(new_selection) => {
if new_selection.range() != selection.range() {
selected_larger_symbol = true;
}
new_selection
}
None => selection.clone(),
})
.collect::<Vec<_>>();
if selected_larger_symbol {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select(new_selections);
});
}
}
pub fn select_larger_syntax_node(
&mut self,
_: &SelectLargerSyntaxNode,
@@ -8743,13 +8816,7 @@ impl Editor {
let display_point = initial_point.to_display_point(snapshot);
let mut hunks = hunks
.map(|hunk| diff_hunk_to_display(&hunk, &snapshot))
.filter(|hunk| {
if is_wrapped {
true
} else {
!hunk.contains_display_row(display_point.row())
}
})
.filter(|hunk| is_wrapped || !hunk.contains_display_row(display_point.row()))
.dedup();
if let Some(hunk) = hunks.next() {
@@ -12027,7 +12094,7 @@ impl Render for Editor {
let settings = ThemeSettings::get_global(cx);
let text_style = match self.mode {
EditorMode::SingleLine | EditorMode::AutoHeight { .. } => TextStyle {
EditorMode::SingleLine { .. } | EditorMode::AutoHeight { .. } => TextStyle {
color: cx.theme().colors().editor_foreground,
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features.clone(),
@@ -12056,7 +12123,7 @@ impl Render for Editor {
};
let background = match self.mode {
EditorMode::SingleLine => cx.theme().system().transparent,
EditorMode::SingleLine { .. } => cx.theme().system().transparent,
EditorMode::AutoHeight { max_lines: _ } => cx.theme().system().transparent,
EditorMode::Full => cx.theme().colors().editor_background,
};
@@ -12320,6 +12387,7 @@ impl ViewInputHandler for Editor {
let font_id = cx.text_system().resolve_font(&style.text.font());
let font_size = style.text.font_size.to_pixels(cx.rem_size());
let line_height = style.text.line_height_in_pixels(cx.rem_size());
let em_width = cx
.text_system()
.typographic_bounds(font_id, font_size, 'm')
@@ -12447,7 +12515,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren
let group_id: SharedString = cx.block_id.to_string().into();
let mut text_style = cx.text_style().clone();
text_style.color = diagnostic_style(diagnostic.severity, true, cx.theme().status());
text_style.color = diagnostic_style(diagnostic.severity, cx.theme().status());
let theme_settings = ThemeSettings::get_global(cx);
text_style.font_family = theme_settings.buffer_font.family.clone();
text_style.font_style = theme_settings.buffer_font.style;
@@ -12543,25 +12611,19 @@ pub fn highlight_diagnostic_message(diagnostic: &Diagnostic) -> (SharedString, V
prev_offset = ix + 1;
if in_code_block {
code_ranges.push(prev_len..text_without_backticks.len());
in_code_block = false;
} else {
in_code_block = true;
}
in_code_block = !in_code_block;
}
(text_without_backticks.into(), code_ranges)
}
fn diagnostic_style(severity: DiagnosticSeverity, valid: bool, colors: &StatusColors) -> Hsla {
match (severity, valid) {
(DiagnosticSeverity::ERROR, true) => colors.error,
(DiagnosticSeverity::ERROR, false) => colors.error,
(DiagnosticSeverity::WARNING, true) => colors.warning,
(DiagnosticSeverity::WARNING, false) => colors.warning,
(DiagnosticSeverity::INFORMATION, true) => colors.info,
(DiagnosticSeverity::INFORMATION, false) => colors.info,
(DiagnosticSeverity::HINT, true) => colors.info,
(DiagnosticSeverity::HINT, false) => colors.info,
fn diagnostic_style(severity: DiagnosticSeverity, colors: &StatusColors) -> Hsla {
match severity {
DiagnosticSeverity::ERROR => colors.error,
DiagnosticSeverity::WARNING => colors.warning,
DiagnosticSeverity::INFORMATION => colors.info,
DiagnosticSeverity::HINT => colors.info,
_ => colors.ignored,
}
}

View File

@@ -276,6 +276,7 @@ impl EditorElement {
register_action(view, cx, Editor::toggle_comments);
register_action(view, cx, Editor::select_larger_syntax_node);
register_action(view, cx, Editor::select_smaller_syntax_node);
register_action(view, cx, Editor::select_enclosing_symbol);
register_action(view, cx, Editor::move_to_enclosing_bracket);
register_action(view, cx, Editor::undo_selection);
register_action(view, cx, Editor::redo_selection);
@@ -1830,10 +1831,10 @@ impl EditorElement {
}
fn layout_lines(
&self,
rows: Range<DisplayRow>,
line_number_layouts: &[Option<ShapedLine>],
snapshot: &EditorSnapshot,
style: &EditorStyle,
cx: &mut WindowContext,
) -> Vec<LineWithInvisibles> {
if rows.start >= rows.end {
@@ -1842,7 +1843,7 @@ impl EditorElement {
// Show the placeholder when the editor is empty
if snapshot.is_empty() {
let font_size = self.style.text.font_size.to_pixels(cx.rem_size());
let font_size = style.text.font_size.to_pixels(cx.rem_size());
let placeholder_color = cx.theme().colors().text_placeholder;
let placeholder_text = snapshot.placeholder_text();
@@ -1857,7 +1858,7 @@ impl EditorElement {
.filter_map(move |line| {
let run = TextRun {
len: line.len(),
font: self.style.text.font(),
font: style.text.font(),
color: placeholder_color,
background_color: None,
underline: Default::default(),
@@ -1876,10 +1877,10 @@ impl EditorElement {
})
.collect()
} else {
let chunks = snapshot.highlighted_chunks(rows.clone(), true, &self.style);
let chunks = snapshot.highlighted_chunks(rows.clone(), true, style);
LineWithInvisibles::from_chunks(
chunks,
&self.style.text,
&style.text,
MAX_LINE_LEN,
rows.len(),
line_number_layouts,
@@ -4474,7 +4475,7 @@ impl EditorElement {
// We currently use single-line and auto-height editors in UI contexts,
// so we don't want to scale everything with the buffer font size, as it
// ends up looking off.
EditorMode::SingleLine | EditorMode::AutoHeight { .. } => None,
EditorMode::SingleLine { .. } | EditorMode::AutoHeight { .. } => None,
}
}
}
@@ -4498,12 +4499,43 @@ impl Element for EditorElement {
editor.set_style(self.style.clone(), cx);
let layout_id = match editor.mode {
EditorMode::SingleLine => {
EditorMode::SingleLine { auto_width } => {
let rem_size = cx.rem_size();
let mut style = Style::default();
style.size.width = relative(1.).into();
style.size.height = self.style.text.line_height_in_pixels(rem_size).into();
cx.request_layout(style, None)
let height = self.style.text.line_height_in_pixels(rem_size);
if auto_width {
let editor_handle = cx.view().clone();
let style = self.style.clone();
cx.request_measured_layout(Style::default(), move |_, _, cx| {
let editor_snapshot =
editor_handle.update(cx, |editor, cx| editor.snapshot(cx));
let line = Self::layout_lines(
DisplayRow(0)..DisplayRow(1),
&[],
&editor_snapshot,
&style,
cx,
)
.pop()
.unwrap();
let font_id = cx.text_system().resolve_font(&style.text.font());
let font_size = style.text.font_size.to_pixels(cx.rem_size());
let em_width = cx
.text_system()
.typographic_bounds(font_id, font_size, 'm')
.unwrap()
.size
.width;
size(line.width + em_width, height)
})
} else {
let mut style = Style::default();
style.size.height = height.into();
style.size.width = relative(1.).into();
cx.request_layout(style, None)
}
}
EditorMode::AutoHeight { max_lines } => {
let editor_handle = cx.view().clone();
@@ -4762,8 +4794,13 @@ impl Element for EditorElement {
);
let mut max_visible_line_width = Pixels::ZERO;
let mut line_layouts =
self.layout_lines(start_row..end_row, &line_numbers, &snapshot, cx);
let mut line_layouts = Self::layout_lines(
start_row..end_row,
&line_numbers,
&snapshot,
&self.style,
cx,
);
for line_with_invisibles in &line_layouts {
if line_with_invisibles.width > max_visible_line_width {
max_visible_line_width = line_with_invisibles.width;
@@ -4791,16 +4828,43 @@ impl Element for EditorElement {
)
});
let scroll_pixel_position = point(
scroll_position.x * em_width,
scroll_position.y * line_height,
);
let start_buffer_row =
MultiBufferRow(start_anchor.to_point(&snapshot.buffer_snapshot).row);
let end_buffer_row =
MultiBufferRow(end_anchor.to_point(&snapshot.buffer_snapshot).row);
let scroll_max = point(
((scroll_width - text_hitbox.size.width) / em_width).max(0.0),
max_row.as_f32(),
);
self.editor.update(cx, |editor, cx| {
let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x);
let autoscrolled = if autoscroll_horizontally {
editor.autoscroll_horizontally(
start_row,
text_hitbox.size.width,
scroll_width,
em_width,
&line_layouts,
cx,
)
} else {
false
};
if clamped || autoscrolled {
snapshot = editor.snapshot(cx);
scroll_position = snapshot.scroll_position();
}
});
let scroll_pixel_position = point(
scroll_position.x * em_width,
scroll_position.y * line_height,
);
let indent_guides = self.layout_indent_guides(
content_origin,
text_hitbox.origin,
@@ -6064,7 +6128,7 @@ mod tests {
});
for editor_mode_without_invisibles in [
EditorMode::SingleLine,
EditorMode::SingleLine { auto_width: false },
EditorMode::AutoHeight { max_lines: 100 },
] {
let invisibles = collect_invisibles_from_new_editor(

View File

@@ -165,10 +165,16 @@ pub fn indent_guides_in_range(
.indent_guides_in_range(start_anchor..end_anchor, ignore_disabled_for_language, cx)
.into_iter()
.filter(|indent_guide| {
let start =
MultiBufferRow(indent_guide.multibuffer_row_range.start.0.saturating_sub(1));
// Filter out indent guides that are inside a fold
!snapshot.is_line_folded(MultiBufferRow(
indent_guide.multibuffer_row_range.start.0.saturating_sub(1),
))
let is_folded = snapshot.is_line_folded(start);
let line_indent = snapshot.line_indent_for_buffer_row(start);
let contained_in_fold =
line_indent.len(indent_guide.tab_size) <= indent_guide.indent_level();
!(is_folded && contained_in_fold)
})
.collect()
}

View File

@@ -455,7 +455,7 @@ impl Editor {
}
pub fn scroll_screen(&mut self, amount: &ScrollAmount, cx: &mut ViewContext<Self>) {
if matches!(self.mode, EditorMode::SingleLine) {
if matches!(self.mode, EditorMode::SingleLine { .. }) {
cx.propagate();
return;
}

View File

@@ -15,7 +15,7 @@ impl Editor {
return;
}
if matches!(self.mode, EditorMode::SingleLine) {
if matches!(self.mode, EditorMode::SingleLine { .. }) {
cx.propagate();
return;
}

View File

@@ -42,7 +42,7 @@ futures.workspace = true
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "5a5c4d4" }
gpui_macros.workspace = true
http.workspace = true
image = "0.23"
image = "0.25.1"
itertools.workspace = true
lazy_static.workspace = true
linkme = "0.3"
@@ -80,6 +80,10 @@ backtrace = "0.3"
collections = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] }
http = { workspace = true, features = ["test-support"] }
unicode-segmentation.workspace = true
[build-dependencies]
embed-resource = "2.4"
[target.'cfg(target_os = "macos")'.build-dependencies]
bindgen = "0.65.1"
@@ -143,9 +147,6 @@ windows.workspace = true
windows-core = "0.57"
clipboard-win = "3.1.1"
[target.'cfg(windows)'.build-dependencies]
embed-resource = "2.4"
[[example]]
name = "hello_world"
path = "examples/hello_world.rs"
@@ -157,3 +158,7 @@ path = "examples/image/image.rs"
[[example]]
name = "set_menus"
path = "examples/set_menus.rs"
[[example]]
name = "input"
path = "examples/input.rs"

View File

@@ -3,18 +3,25 @@
//TODO: consider generating shader code for WGSL
//TODO: deprecate "runtime-shaders" and "macos-blade"
fn main() {
#[cfg(target_os = "macos")]
macos::build();
use std::env;
#[cfg(target_os = "windows")]
{
let manifest = std::path::Path::new("resources/windows/gpui.manifest.xml");
let rc_file = std::path::Path::new("resources/windows/gpui.rc");
println!("cargo:rerun-if-changed={}", manifest.display());
println!("cargo:rerun-if-changed={}", rc_file.display());
embed_resource::compile(rc_file, embed_resource::NONE);
}
fn main() {
let target = env::var("CARGO_CFG_TARGET_OS");
match target.as_deref() {
Ok("macos") => {
#[cfg(target_os = "macos")]
macos::build();
}
Ok("windows") => {
let manifest = std::path::Path::new("resources/windows/gpui.manifest.xml");
let rc_file = std::path::Path::new("resources/windows/gpui.rc");
println!("cargo:rerun-if-changed={}", manifest.display());
println!("cargo:rerun-if-changed={}", rc_file.display());
embed_resource::compile(rc_file, embed_resource::NONE);
}
_ => (),
};
}
#[cfg(target_os = "macos")]

View File

@@ -0,0 +1,489 @@
use std::ops::Range;
use gpui::*;
use unicode_segmentation::*;
actions!(
text_input,
[
Backspace,
Delete,
Left,
Right,
SelectLeft,
SelectRight,
SelectAll,
Home,
End,
ShowCharacterPalette
]
);
struct TextInput {
focus_handle: FocusHandle,
content: SharedString,
selected_range: Range<usize>,
selection_reversed: bool,
marked_range: Option<Range<usize>>,
last_layout: Option<ShapedLine>,
}
impl TextInput {
fn left(&mut self, _: &Left, cx: &mut ViewContext<Self>) {
if self.selected_range.is_empty() {
self.move_to(self.previous_boundary(self.cursor_offset()), cx);
} else {
self.move_to(self.selected_range.start, cx)
}
}
fn right(&mut self, _: &Right, cx: &mut ViewContext<Self>) {
if self.selected_range.is_empty() {
self.move_to(self.next_boundary(self.selected_range.end), cx);
} else {
self.move_to(self.selected_range.end, cx)
}
}
fn select_left(&mut self, _: &SelectLeft, cx: &mut ViewContext<Self>) {
self.select_to(self.previous_boundary(self.cursor_offset()), cx);
}
fn select_right(&mut self, _: &SelectRight, cx: &mut ViewContext<Self>) {
self.select_to(self.next_boundary(self.cursor_offset()), cx);
}
fn select_all(&mut self, _: &SelectAll, cx: &mut ViewContext<Self>) {
self.move_to(0, cx);
self.select_to(self.content.len(), cx)
}
fn home(&mut self, _: &Home, cx: &mut ViewContext<Self>) {
self.move_to(0, cx);
}
fn end(&mut self, _: &End, cx: &mut ViewContext<Self>) {
self.move_to(self.content.len(), cx);
}
fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext<Self>) {
if self.selected_range.is_empty() {
self.select_to(self.previous_boundary(self.cursor_offset()), cx)
}
self.replace_text_in_range(None, "", cx)
}
fn delete(&mut self, _: &Delete, cx: &mut ViewContext<Self>) {
if self.selected_range.is_empty() {
self.select_to(self.next_boundary(self.cursor_offset()), cx)
}
self.replace_text_in_range(None, "", cx)
}
fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext<Self>) {
cx.show_character_palette();
}
fn move_to(&mut self, offset: usize, cx: &mut ViewContext<Self>) {
self.selected_range = offset..offset;
cx.notify()
}
fn cursor_offset(&self) -> usize {
if self.selection_reversed {
self.selected_range.start
} else {
self.selected_range.end
}
}
fn select_to(&mut self, offset: usize, cx: &mut ViewContext<Self>) {
if self.selection_reversed {
self.selected_range.start = offset
} else {
self.selected_range.end = offset
};
if self.selected_range.end < self.selected_range.start {
self.selection_reversed = !self.selection_reversed;
self.selected_range = self.selected_range.end..self.selected_range.start;
}
cx.notify()
}
fn offset_from_utf16(&self, offset: usize) -> usize {
let mut utf8_offset = 0;
let mut utf16_count = 0;
for ch in self.content.chars() {
if utf16_count >= offset {
break;
}
utf16_count += ch.len_utf16();
utf8_offset += ch.len_utf8();
}
utf8_offset
}
fn offset_to_utf16(&self, offset: usize) -> usize {
let mut utf16_offset = 0;
let mut utf8_count = 0;
for ch in self.content.chars() {
if utf8_count >= offset {
break;
}
utf8_count += ch.len_utf8();
utf16_offset += ch.len_utf16();
}
utf16_offset
}
fn range_to_utf16(&self, range: &Range<usize>) -> Range<usize> {
self.offset_to_utf16(range.start)..self.offset_to_utf16(range.end)
}
fn range_from_utf16(&self, range_utf16: &Range<usize>) -> Range<usize> {
self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end)
}
fn previous_boundary(&self, offset: usize) -> usize {
self.content
.grapheme_indices(true)
.rev()
.find_map(|(idx, _)| (idx < offset).then_some(idx))
.unwrap_or(0)
}
fn next_boundary(&self, offset: usize) -> usize {
self.content
.grapheme_indices(true)
.find_map(|(idx, _)| (idx > offset).then_some(idx))
.unwrap_or(self.content.len())
}
}
impl ViewInputHandler for TextInput {
fn text_for_range(
&mut self,
range_utf16: Range<usize>,
_cx: &mut ViewContext<Self>,
) -> Option<String> {
let range = self.range_from_utf16(&range_utf16);
Some(self.content[range].to_string())
}
fn selected_text_range(&mut self, _cx: &mut ViewContext<Self>) -> Option<Range<usize>> {
Some(self.range_to_utf16(&self.selected_range))
}
fn marked_text_range(&self, _cx: &mut ViewContext<Self>) -> Option<Range<usize>> {
self.marked_range
.as_ref()
.map(|range| self.range_to_utf16(range))
}
fn unmark_text(&mut self, _cx: &mut ViewContext<Self>) {
self.marked_range = None;
}
fn replace_text_in_range(
&mut self,
range_utf16: Option<Range<usize>>,
new_text: &str,
cx: &mut ViewContext<Self>,
) {
let range = range_utf16
.as_ref()
.map(|range_utf16| self.range_from_utf16(range_utf16))
.or(self.marked_range.clone())
.unwrap_or(self.selected_range.clone());
self.content =
(self.content[0..range.start].to_owned() + new_text + &self.content[range.end..])
.into();
self.selected_range = range.start + new_text.len()..range.start + new_text.len();
self.marked_range.take();
cx.notify();
}
fn replace_and_mark_text_in_range(
&mut self,
range_utf16: Option<Range<usize>>,
new_text: &str,
new_selected_range_utf16: Option<Range<usize>>,
cx: &mut ViewContext<Self>,
) {
let range = range_utf16
.as_ref()
.map(|range_utf16| self.range_from_utf16(range_utf16))
.or(self.marked_range.clone())
.unwrap_or(self.selected_range.clone());
self.content =
(self.content[0..range.start].to_owned() + new_text + &self.content[range.end..])
.into();
self.marked_range = Some(range.start..range.start + new_text.len());
self.selected_range = new_selected_range_utf16
.as_ref()
.map(|range_utf16| self.range_from_utf16(range_utf16))
.map(|new_range| new_range.start + range.start..new_range.end + range.end)
.unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len());
cx.notify();
}
fn bounds_for_range(
&mut self,
range_utf16: Range<usize>,
bounds: Bounds<Pixels>,
_cx: &mut ViewContext<Self>,
) -> Option<Bounds<Pixels>> {
let Some(last_layout) = self.last_layout.as_ref() else {
return None;
};
let range = self.range_from_utf16(&range_utf16);
Some(Bounds::from_corners(
point(
bounds.left() + last_layout.x_for_index(range.start),
bounds.top(),
),
point(
bounds.left() + last_layout.x_for_index(range.end),
bounds.bottom(),
),
))
}
}
struct TextElement {
input: View<TextInput>,
}
struct PrepaintState {
line: Option<ShapedLine>,
cursor: Option<PaintQuad>,
selection: Option<PaintQuad>,
}
impl IntoElement for TextElement {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Element for TextElement {
type RequestLayoutState = ();
type PrepaintState = PrepaintState;
fn id(&self) -> Option<ElementId> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
cx: &mut WindowContext,
) -> (LayoutId, Self::RequestLayoutState) {
let mut style = Style::default();
style.size.width = relative(1.).into();
style.size.height = cx.line_height().into();
(cx.request_layout(style, []), ())
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
cx: &mut WindowContext,
) -> Self::PrepaintState {
let input = self.input.read(cx);
let content = input.content.clone();
let selected_range = input.selected_range.clone();
let cursor = input.cursor_offset();
let style = cx.text_style();
let run = TextRun {
len: input.content.len(),
font: style.font(),
color: style.color,
background_color: None,
underline: None,
strikethrough: None,
};
let runs = if let Some(marked_range) = input.marked_range.as_ref() {
vec![
TextRun {
len: marked_range.start,
..run.clone()
},
TextRun {
len: marked_range.end - marked_range.start,
underline: Some(UnderlineStyle {
color: Some(run.color),
thickness: px(1.0),
wavy: false,
}),
..run.clone()
},
TextRun {
len: input.content.len() - marked_range.end,
..run.clone()
},
]
.into_iter()
.filter(|run| run.len > 0)
.collect()
} else {
vec![run]
};
let font_size = style.font_size.to_pixels(cx.rem_size());
let line = cx
.text_system()
.shape_line(content, font_size, &runs)
.unwrap();
let cursor_pos = line.x_for_index(cursor);
let (selection, cursor) = if selected_range.is_empty() {
(
None,
Some(fill(
Bounds::new(
point(bounds.left() + cursor_pos, bounds.top()),
size(px(2.), bounds.bottom() - bounds.top()),
),
gpui::blue(),
)),
)
} else {
(
Some(fill(
Bounds::from_corners(
point(
bounds.left() + line.x_for_index(selected_range.start),
bounds.top(),
),
point(
bounds.left() + line.x_for_index(selected_range.end),
bounds.bottom(),
),
),
rgba(0x3311FF30),
)),
None,
)
};
PrepaintState {
line: Some(line),
cursor,
selection,
}
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
prepaint: &mut Self::PrepaintState,
cx: &mut WindowContext,
) {
let focus_handle = self.input.read(cx).focus_handle.clone();
cx.handle_input(
&focus_handle,
ElementInputHandler::new(bounds, self.input.clone()),
);
if let Some(selection) = prepaint.selection.take() {
cx.paint_quad(selection)
}
let line = prepaint.line.take().unwrap();
line.paint(bounds.origin, cx.line_height(), cx).unwrap();
if let Some(cursor) = prepaint.cursor.take() {
cx.paint_quad(cursor);
}
self.input.update(cx, |input, _cx| {
input.last_layout = Some(line);
});
}
}
impl Render for TextInput {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.flex()
.key_context("TextInput")
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::backspace))
.on_action(cx.listener(Self::delete))
.on_action(cx.listener(Self::left))
.on_action(cx.listener(Self::right))
.on_action(cx.listener(Self::select_left))
.on_action(cx.listener(Self::select_right))
.on_action(cx.listener(Self::select_all))
.on_action(cx.listener(Self::home))
.on_action(cx.listener(Self::end))
.on_action(cx.listener(Self::show_character_palette))
.bg(rgb(0xeeeeee))
.size_full()
.line_height(px(30.))
.text_size(px(24.))
.child(
div()
.h(px(30. + 4. * 2.))
.w_full()
.p(px(4.))
.bg(white())
.child(TextElement {
input: cx.view().clone(),
}),
)
}
}
fn main() {
App::new().run(|cx: &mut AppContext| {
let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
cx.bind_keys([
KeyBinding::new("backspace", Backspace, None),
KeyBinding::new("delete", Delete, None),
KeyBinding::new("left", Left, None),
KeyBinding::new("right", Right, None),
KeyBinding::new("shift-left", SelectLeft, None),
KeyBinding::new("shift-right", SelectRight, None),
KeyBinding::new("cmd-a", SelectAll, None),
KeyBinding::new("home", Home, None),
KeyBinding::new("end", End, None),
KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, None),
]);
let window = cx
.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|cx| {
cx.new_view(|cx| TextInput {
focus_handle: cx.focus_handle(),
content: "".into(),
selected_range: 0..0,
selection_reversed: false,
marked_range: None,
last_layout: None,
})
},
)
.unwrap();
window
.update(cx, |view, cx| {
view.focus_handle.focus(cx);
cx.activate(true)
})
.unwrap();
});
}

View File

@@ -51,6 +51,7 @@ fn main() {
kind: WindowKind::PopUp,
is_movable: false,
app_id: None,
window_min_size: None,
}
};

View File

@@ -1,6 +1,6 @@
use crate::{size, DevicePixels, Result, SharedString, Size};
use image::{Bgra, ImageBuffer};
use image::RgbaImage;
use std::{
borrow::Cow,
fmt,
@@ -40,12 +40,12 @@ pub(crate) struct RenderImageParams {
pub struct ImageData {
/// The ID associated with this image
pub id: ImageId,
data: ImageBuffer<Bgra<u8>, Vec<u8>>,
data: RgbaImage,
}
impl ImageData {
/// Create a new image from the given data.
pub fn new(data: ImageBuffer<Bgra<u8>, Vec<u8>>) -> Self {
pub fn new(data: RgbaImage) -> Self {
static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
Self {

View File

@@ -384,7 +384,13 @@ impl Asset for Image {
};
let data = if let Ok(format) = image::guess_format(&bytes) {
let data = image::load_from_memory_with_format(&bytes, format)?.into_bgra8();
let mut data = image::load_from_memory_with_format(&bytes, format)?.into_rgba8();
// Convert from RGBA to BGRA.
for pixel in data.chunks_exact_mut(4) {
pixel.swap(0, 2);
}
ImageData::new(data)
} else {
let pixmap =

View File

@@ -2287,6 +2287,15 @@ impl Pixels {
pub fn abs(&self) -> Self {
Self(self.0.abs())
}
/// Returns the f64 value of `Pixels`.
///
/// # Returns
///
/// A f64 value of the `Pixels`.
pub fn to_f64(self) -> f64 {
self.0 as f64
}
}
impl Mul<Pixels> for Pixels {

View File

@@ -567,6 +567,9 @@ pub struct WindowOptions {
/// Application identifier of the window. Can by used by desktop environments to group applications together.
pub app_id: Option<String>,
/// Window minimum size
pub window_min_size: Option<Size<Pixels>>,
}
/// The variables that can be configured when creating a new window
@@ -594,6 +597,9 @@ pub(crate) struct WindowParams {
pub display_id: Option<DisplayId>,
pub window_background: WindowBackgroundAppearance,
#[cfg_attr(target_os = "linux", allow(dead_code))]
pub window_min_size: Option<Size<Pixels>>,
}
/// Represents the status of how a window should be opened.
@@ -642,6 +648,7 @@ impl Default for WindowOptions {
display_id: None,
window_background: WindowBackgroundAppearance::default(),
app_id: None,
window_min_size: None,
}
}
}

View File

@@ -94,6 +94,27 @@ impl Keystroke {
}
}
//Allow for the user to specify a keystroke modifier as the key itself
//This sets the `key` to the modifier, and disables the modifier
if key.is_none() {
if shift {
key = Some("shift".to_string());
shift = false;
} else if control {
key = Some("control".to_string());
control = false;
} else if alt {
key = Some("alt".to_string());
alt = false;
} else if platform {
key = Some("platform".to_string());
platform = false;
} else if function {
key = Some("function".to_string());
function = false;
}
}
let key = key.ok_or_else(|| anyhow!("Invalid keystroke `{}`", source))?;
Ok(Keystroke {
@@ -186,6 +207,10 @@ impl std::fmt::Display for Keystroke {
"right" => '→',
"tab" => '⇥',
"escape" => '⎋',
"shift" => '⇧',
"control" => '⌃',
"alt" => '⌥',
"platform" => '⌘',
key => {
if key.len() == 1 {
key.chars().next().unwrap().to_ascii_uppercase()
@@ -241,6 +266,15 @@ impl Modifiers {
}
}
/// How many modifier keys are pressed
pub fn number_of_modifiers(&self) -> u8 {
self.control as u8
+ self.alt as u8
+ self.shift as u8
+ self.platform as u8
+ self.function as u8
}
/// helper method for Modifiers with no modifiers
pub fn none() -> Modifiers {
Default::default()

View File

@@ -22,7 +22,7 @@ impl HeadlessClient {
pub(crate) fn new() -> Self {
let event_loop = EventLoop::try_new().unwrap();
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
let (common, main_receiver) = LinuxCommon::new(Box::new(event_loop.get_signal()));
let handle = event_loop.handle();

View File

@@ -3,6 +3,7 @@
use std::any::{type_name, Any};
use std::cell::{self, RefCell};
use std::env;
use std::ffi::OsString;
use std::fs::File;
use std::io::Read;
use std::ops::{Deref, DerefMut};
@@ -83,6 +84,16 @@ pub(crate) struct PlatformHandlers {
pub(crate) validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
}
pub trait QuitSignal {
fn quit(&mut self);
}
impl QuitSignal for LoopSignal {
fn quit(&mut self) {
self.stop();
}
}
pub(crate) struct LinuxCommon {
pub(crate) background_executor: BackgroundExecutor,
pub(crate) foreground_executor: ForegroundExecutor,
@@ -90,12 +101,12 @@ pub(crate) struct LinuxCommon {
pub(crate) appearance: WindowAppearance,
pub(crate) auto_hide_scrollbars: bool,
pub(crate) callbacks: PlatformHandlers,
pub(crate) signal: LoopSignal,
pub(crate) signal: Box<dyn QuitSignal>,
pub(crate) menus: Vec<OwnedMenu>,
}
impl LinuxCommon {
pub fn new(signal: LoopSignal) -> (Self, Channel<Runnable>) {
pub fn new(signal: Box<dyn QuitSignal>) -> (Self, Channel<Runnable>) {
let (main_sender, main_receiver) = calloop::channel::channel::<Runnable>();
let text_system = Arc::new(CosmicTextSystem::new());
let callbacks = PlatformHandlers::default();
@@ -145,7 +156,7 @@ impl<P: LinuxClient + 'static> Platform for P {
}
fn quit(&self) {
self.with_common(|common| common.signal.stop());
self.with_common(|common| common.signal.quit());
}
fn compositor_name(&self) -> &'static str {
@@ -508,6 +519,27 @@ pub(super) fn is_within_click_distance(a: Point<Pixels>, b: Point<Pixels>) -> bo
diff.x.abs() <= DOUBLE_CLICK_DISTANCE && diff.y.abs() <= DOUBLE_CLICK_DISTANCE
}
pub(super) fn get_xkb_compose_state(cx: &xkb::Context) -> Option<xkb::compose::State> {
let mut locales = Vec::default();
if let Some(locale) = std::env::var_os("LC_CTYPE") {
locales.push(locale);
}
locales.push(OsString::from("C"));
let mut state: Option<xkb::compose::State> = None;
for locale in locales {
if let Ok(table) =
xkb::compose::Table::new_from_locale(&cx, &locale, xkb::compose::COMPILE_NO_FLAGS)
{
state = Some(xkb::compose::State::new(
&table,
xkb::compose::STATE_NO_FLAGS,
));
break;
}
}
state
}
pub(super) unsafe fn read_fd(mut fd: FileDescriptor) -> Result<String> {
let mut file = File::from_raw_fd(fd.as_raw_fd());
@@ -583,19 +615,11 @@ impl Keystroke {
let key_utf8 = state.key_get_utf8(keycode);
let key_sym = state.key_get_one_sym(keycode);
// The logic here tries to replicate the logic in `../mac/events.rs`
// "Consumed" modifiers are modifiers that have been used to translate a key, for example
// pressing "shift" and "1" on US layout produces the key `!` but "consumes" the shift.
// Notes:
// - macOS gets the key character directly ("."), xkb gives us the key name ("period")
// - macOS logic removes consumed shift modifier for symbols: "{", not "shift-{"
// - macOS logic keeps consumed shift modifiers for letters: "shift-a", not "a" or "A"
let mut handle_consumed_modifiers = true;
let key = match key_sym {
Keysym::Return => "enter".to_owned(),
Keysym::Prior => "pageup".to_owned(),
Keysym::Next => "pagedown".to_owned(),
Keysym::ISO_Left_Tab => "tab".to_owned(),
Keysym::comma => ",".to_owned(),
Keysym::period => ".".to_owned(),
@@ -633,30 +657,22 @@ impl Keystroke {
Keysym::equal => "=".to_owned(),
Keysym::plus => "+".to_owned(),
Keysym::ISO_Left_Tab => {
handle_consumed_modifiers = false;
"tab".to_owned()
}
_ => {
handle_consumed_modifiers = false;
xkb::keysym_get_name(key_sym).to_lowercase()
}
_ => xkb::keysym_get_name(key_sym).to_lowercase(),
};
if modifiers.shift {
// we only include the shift for upper-case letters by convention,
// so don't include for numbers and symbols, but do include for
// tab/enter, etc.
if key.chars().count() == 1 && key_utf8 == key {
modifiers.shift = false;
}
}
// Ignore control characters (and DEL) for the purposes of ime_key
let ime_key =
(key_utf32 >= 32 && key_utf32 != 127 && !key_utf8.is_empty()).then_some(key_utf8);
if handle_consumed_modifiers {
let mod_shift_index = state.get_keymap().mod_get_index(xkb::MOD_NAME_SHIFT);
let is_shift_consumed = state.mod_index_is_consumed(keycode, mod_shift_index);
if modifiers.shift && is_shift_consumed {
modifiers.shift = false;
}
}
Keystroke {
modifiers,
key,

View File

@@ -1,5 +1,4 @@
use std::cell::{RefCell, RefMut};
use std::ffi::OsString;
use std::hash::Hash;
use std::os::fd::{AsRawFd, BorrowedFd};
use std::path::PathBuf;
@@ -65,7 +64,6 @@ use xkbcommon::xkb::{self, Keycode, KEYMAP_COMPILE_NO_FLAGS};
use super::super::{open_uri_internal, read_fd, DOUBLE_CLICK_INTERVAL};
use super::display::WaylandDisplay;
use super::window::{ImeInput, WaylandWindowStatePtr};
use crate::platform::linux::is_within_click_distance;
use crate::platform::linux::wayland::clipboard::{
Clipboard, DataOffer, FILE_LIST_MIME_TYPE, TEXT_MIME_TYPE,
};
@@ -74,6 +72,7 @@ use crate::platform::linux::wayland::serial::{SerialKind, SerialTracker};
use crate::platform::linux::wayland::window::WaylandWindow;
use crate::platform::linux::xdg_desktop_portal::{Event as XDPEvent, XDPEventSource};
use crate::platform::linux::LinuxClient;
use crate::platform::linux::{get_xkb_compose_state, is_within_click_distance};
use crate::platform::PlatformWindow;
use crate::{
point, px, size, Bounds, DevicePixels, FileDropEvent, ForegroundExecutor, MouseExitEvent, Size,
@@ -311,7 +310,7 @@ impl WaylandClientStatePtr {
}
}
if state.windows.is_empty() {
state.common.signal.stop();
state.common.signal.quit();
}
}
}
@@ -407,7 +406,7 @@ impl WaylandClient {
let event_loop = EventLoop::<WaylandClientStatePtr>::try_new().unwrap();
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
let (common, main_receiver) = LinuxCommon::new(Box::new(event_loop.get_signal()));
let handle = event_loop.handle();
handle
@@ -671,12 +670,12 @@ impl LinuxClient for WaylandClient {
return;
};
if state.mouse_focused_window.is_some() || state.keyboard_focused_window.is_some() {
let serial = state.serial_tracker.get(SerialKind::KeyEnter);
state.clipboard.set_primary(item);
let serial = state.serial_tracker.get(SerialKind::KeyPress);
let data_source = primary_selection_manager.create_source(&state.globals.qh, ());
data_source.offer(state.clipboard.self_mime());
data_source.offer(TEXT_MIME_TYPE.to_string());
primary_selection.set_selection(Some(&data_source), serial);
state.clipboard.set_primary(item.text);
}
}
@@ -689,35 +688,21 @@ impl LinuxClient for WaylandClient {
return;
};
if state.mouse_focused_window.is_some() || state.keyboard_focused_window.is_some() {
let serial = state.serial_tracker.get(SerialKind::KeyEnter);
state.clipboard.set(item);
let serial = state.serial_tracker.get(SerialKind::KeyPress);
let data_source = data_device_manager.create_data_source(&state.globals.qh, ());
data_source.offer(state.clipboard.self_mime());
data_source.offer(TEXT_MIME_TYPE.to_string());
data_device.set_selection(Some(&data_source), serial);
state.clipboard.set(item.text);
}
}
fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
self.0
.borrow_mut()
.clipboard
.read_primary()
.map(|s| crate::ClipboardItem {
text: s,
metadata: None,
})
self.0.borrow_mut().clipboard.read_primary()
}
fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
self.0
.borrow_mut()
.clipboard
.read()
.map(|s| crate::ClipboardItem {
text: s,
metadata: None,
})
self.0.borrow_mut().clipboard.read()
}
fn active_window(&self) -> Option<AnyWindowHandle> {
@@ -1068,21 +1053,8 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
.flatten()
.expect("Failed to create keymap")
};
let table = {
let locale = std::env::var_os("LC_CTYPE").unwrap_or(OsString::from("C"));
xkb::compose::Table::new_from_locale(
&xkb_context,
&locale,
xkb::compose::COMPILE_NO_FLAGS,
)
.log_err()
.unwrap()
};
state.keymap_state = Some(xkb::State::new(&keymap));
state.compose_state = Some(xkb::compose::State::new(
&table,
xkb::compose::STATE_NO_FLAGS,
));
state.compose_state = get_xkb_compose_state(&xkb_context);
}
wl_keyboard::Event::Enter {
serial, surface, ..
@@ -1162,6 +1134,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
compose.feed(keysym);
match compose.status() {
xkb::Status::Composing => {
keystroke.ime_key = None;
state.pre_edit_text =
compose.utf8().or(Keystroke::underlying_dead_key(keysym));
let pre_edit =
@@ -1174,7 +1147,9 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
xkb::Status::Composed => {
state.pre_edit_text.take();
keystroke.ime_key = compose.utf8();
keystroke.key = xkb::keysym_get_name(compose.keysym().unwrap());
if let Some(keysym) = compose.keysym() {
keystroke.key = xkb::keysym_get_name(keysym);
}
}
xkb::Status::Cancelled => {
let pre_edit = state.pre_edit_text.take();

View File

@@ -9,7 +9,7 @@ use filedescriptor::Pipe;
use wayland_client::{protocol::wl_data_offer::WlDataOffer, Connection};
use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1;
use crate::{platform::linux::platform::read_fd, WaylandClientStatePtr};
use crate::{platform::linux::platform::read_fd, ClipboardItem, WaylandClientStatePtr};
pub(crate) const TEXT_MIME_TYPE: &str = "text/plain;charset=utf-8";
pub(crate) const FILE_LIST_MIME_TYPE: &str = "text/uri-list";
@@ -23,13 +23,13 @@ pub(crate) struct Clipboard {
self_mime: String,
// Internal clipboard
contents: Option<String>,
primary_contents: Option<String>,
contents: Option<ClipboardItem>,
primary_contents: Option<ClipboardItem>,
// External clipboard
cached_read: Option<String>,
cached_read: Option<ClipboardItem>,
current_offer: Option<DataOffer<WlDataOffer>>,
cached_primary_read: Option<String>,
cached_primary_read: Option<ClipboardItem>,
current_primary_offer: Option<DataOffer<ZwpPrimarySelectionOfferV1>>,
}
@@ -89,12 +89,12 @@ impl Clipboard {
}
}
pub fn set(&mut self, text: String) {
self.contents = Some(text);
pub fn set(&mut self, item: ClipboardItem) {
self.contents = Some(item);
}
pub fn set_primary(&mut self, text: String) {
self.primary_contents = Some(text);
pub fn set_primary(&mut self, item: ClipboardItem) {
self.primary_contents = Some(item);
}
pub fn set_offer(&mut self, data_offer: Option<DataOffer<WlDataOffer>>) {
@@ -113,17 +113,17 @@ impl Clipboard {
pub fn send(&self, _mime_type: String, fd: OwnedFd) {
if let Some(contents) = &self.contents {
self.send_internal(fd, contents.as_bytes().to_owned());
self.send_internal(fd, contents.text.as_bytes().to_owned());
}
}
pub fn send_primary(&self, _mime_type: String, fd: OwnedFd) {
if let Some(primary_contents) = &self.primary_contents {
self.send_internal(fd, primary_contents.as_bytes().to_owned());
self.send_internal(fd, primary_contents.text.as_bytes().to_owned());
}
}
pub fn read(&mut self) -> Option<String> {
pub fn read(&mut self) -> Option<ClipboardItem> {
let offer = self.current_offer.clone()?;
if let Some(cached) = self.cached_read.clone() {
return Some(cached);
@@ -145,8 +145,8 @@ impl Clipboard {
match unsafe { read_fd(fd) } {
Ok(v) => {
self.cached_read = Some(v.clone());
Some(v)
self.cached_read = Some(ClipboardItem::new(v));
self.cached_read.clone()
}
Err(err) => {
log::error!("error reading clipboard pipe: {err:?}");
@@ -155,7 +155,7 @@ impl Clipboard {
}
}
pub fn read_primary(&mut self) -> Option<String> {
pub fn read_primary(&mut self) -> Option<ClipboardItem> {
let offer = self.current_primary_offer.clone()?;
if let Some(cached) = self.cached_primary_read.clone() {
return Some(cached);
@@ -177,8 +177,8 @@ impl Clipboard {
match unsafe { read_fd(fd) } {
Ok(v) => {
self.cached_primary_read = Some(v.clone());
Some(v)
self.cached_primary_read = Some(ClipboardItem::new(v.clone()));
self.cached_primary_read.clone()
}
Err(err) => {
log::error!("error reading clipboard pipe: {err:?}");

View File

@@ -1,20 +1,21 @@
use std::cell::RefCell;
use std::collections::HashSet;
use std::ffi::OsString;
use std::ops::Deref;
use std::rc::{Rc, Weak};
use std::time::{Duration, Instant};
use calloop::generic::{FdWrapper, Generic};
use calloop::{EventLoop, LoopHandle, RegistrationToken};
use anyhow::Context;
use async_task::Runnable;
use calloop::channel::Channel;
use collections::HashMap;
use util::ResultExt;
use futures::channel::oneshot;
use util::ResultExt;
use x11rb::connection::{Connection, RequestConnection};
use x11rb::cursor;
use x11rb::errors::ConnectionError;
use x11rb::protocol::randr::ConnectionExt as _;
use x11rb::protocol::xinput::ConnectionExt;
use x11rb::protocol::xkb::ConnectionExt as _;
use x11rb::protocol::xproto::{ChangeWindowAttributesAux, ConnectionExt as _};
@@ -29,13 +30,13 @@ use xkbcommon::xkb as xkbc;
use crate::platform::linux::LinuxClient;
use crate::platform::{LinuxCommon, PlatformWindow};
use crate::{
modifiers_from_xinput_info, point, px, AnyWindowHandle, Bounds, CursorStyle, DisplayId,
Keystroke, Modifiers, ModifiersChangedEvent, Pixels, PlatformDisplay, PlatformInput, Point,
ScrollDelta, Size, TouchPhase, WindowParams, X11Window,
modifiers_from_xinput_info, point, px, AnyWindowHandle, Bounds, ClipboardItem, CursorStyle,
DisplayId, Keystroke, Modifiers, ModifiersChangedEvent, Pixels, PlatformDisplay, PlatformInput,
Point, QuitSignal, ScrollDelta, Size, TouchPhase, WindowParams, X11Window,
};
use super::{
super::{open_uri_internal, SCROLL_LINES},
super::{get_xkb_compose_state, open_uri_internal, SCROLL_LINES},
X11Display, X11WindowStatePtr, XcbAtoms,
};
use super::{button_of_key, modifiers_from_state, pressed_button_from_mask};
@@ -48,7 +49,6 @@ pub(super) const XINPUT_MASTER_DEVICE: u16 = 1;
pub(crate) struct WindowRef {
window: X11WindowStatePtr,
refresh_event_token: RegistrationToken,
}
impl WindowRef {
@@ -96,9 +96,6 @@ impl From<xim::ClientError> for EventHandlerError {
}
pub struct X11ClientState {
pub(crate) loop_handle: LoopHandle<'static, X11Client>,
pub(crate) event_loop: Option<calloop::EventLoop<'static, X11Client>>,
pub(crate) last_click: Instant,
pub(crate) last_location: Point<Pixels>,
pub(crate) current_count: usize,
@@ -116,7 +113,7 @@ pub struct X11ClientState {
pub(crate) xim_handler: Option<XimHandler>,
pub modifiers: Modifiers,
pub(crate) compose_state: xkbc::compose::State,
pub(crate) compose_state: Option<xkbc::compose::State>,
pub(crate) pre_edit_text: Option<String>,
pub(crate) composing: bool,
pub(crate) cursor_handle: cursor::Handle,
@@ -129,6 +126,12 @@ pub struct X11ClientState {
pub(crate) common: LinuxCommon,
pub(crate) clipboard: x11_clipboard::Clipboard,
pub(crate) clipboard_item: Option<ClipboardItem>,
quit_signal_rx: oneshot::Receiver<()>,
runnables: Channel<Runnable>,
xdp_event_source: XDPEventSource,
}
#[derive(Clone)]
@@ -139,14 +142,39 @@ impl X11ClientStatePtr {
let client = X11Client(self.0.upgrade().expect("client already dropped"));
let mut state = client.0.borrow_mut();
if let Some(window_ref) = state.windows.remove(&x_window) {
state.loop_handle.remove(window_ref.refresh_event_token);
if state.windows.remove(&x_window).is_none() {
log::warn!(
"failed to remove X window {} from client state, does not exist",
x_window
);
}
state.cursor_styles.remove(&x_window);
if state.windows.is_empty() {
state.common.signal.stop();
state.common.signal.quit();
}
}
}
struct ChannelQuitSignal {
tx: Option<oneshot::Sender<()>>,
}
impl ChannelQuitSignal {
fn new() -> (Self, oneshot::Receiver<()>) {
let (tx, rx) = oneshot::channel::<()>();
let quit_signal = ChannelQuitSignal { tx: Some(tx) };
(quit_signal, rx)
}
}
impl QuitSignal for ChannelQuitSignal {
fn quit(&mut self) {
if let Some(tx) = self.tx.take() {
tx.send(()).log_err();
}
}
}
@@ -156,27 +184,9 @@ pub(crate) struct X11Client(Rc<RefCell<X11ClientState>>);
impl X11Client {
pub(crate) fn new() -> Self {
let event_loop = EventLoop::try_new().unwrap();
let (quit_signal, quit_signal_rx) = ChannelQuitSignal::new();
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
let handle = event_loop.handle();
handle
.insert_source(main_receiver, {
let handle = handle.clone();
move |event, _, _: &mut X11Client| {
if let calloop::channel::Event::Msg(runnable) = event {
// Insert the runnables as idle callbacks, so we make sure that user-input and X11
// events have higher priority and runnables are only worked off after the event
// callbacks.
handle.insert_idle(|_| {
runnable.run();
});
}
}
})
.unwrap();
let (common, runnables) = LinuxCommon::new(Box::new(quit_signal));
let (xcb_connection, x_root_index) = XCBConnection::connect(None).unwrap();
xcb_connection
@@ -249,18 +259,7 @@ impl X11Client {
);
xkbc::x11::state_new_from_device(&xkb_keymap, &xcb_connection, xkb_device_id)
};
let compose_state = {
let locale = std::env::var_os("LC_CTYPE").unwrap_or(OsString::from("C"));
let table = xkbc::compose::Table::new_from_locale(
&xkb_context,
&locale,
xkbc::compose::COMPILE_NO_FLAGS,
)
.log_err()
.unwrap();
xkbc::compose::State::new(&table, xkbc::compose::STATE_NO_FLAGS)
};
let compose_state = get_xkb_compose_state(&xkb_context);
let resource_database = x11rb::resource_manager::new_from_default(&xcb_connection).unwrap();
let scale_factor = resource_database
@@ -286,105 +285,16 @@ impl X11Client {
None
};
// Safety: Safe if xcb::Connection always returns a valid fd
let fd = unsafe { FdWrapper::new(Rc::clone(&xcb_connection)) };
handle
.insert_source(
Generic::new_with_error::<EventHandlerError>(
fd,
calloop::Interest::READ,
calloop::Mode::Level,
),
{
let xcb_connection = xcb_connection.clone();
move |_readiness, _, client| {
let mut events = Vec::new();
let mut windows_to_refresh = HashSet::new();
while let Some(event) = xcb_connection.poll_for_event()? {
if let Event::Expose(event) = event {
windows_to_refresh.insert(event.window);
} else {
events.push(event);
}
}
for window in windows_to_refresh.into_iter() {
if let Some(window) = client.get_window(window) {
window.refresh();
}
}
for event in events.into_iter() {
let mut state = client.0.borrow_mut();
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) {
Ok(handled) => handled,
Err(err) => {
log::error!("XIMClientError: {}", err);
false
}
};
let xim_callback_event = xim_handler.last_callback_event.take();
let mut state = client.0.borrow_mut();
state.ximc = Some(ximc);
state.xim_handler = Some(xim_handler);
drop(state);
if let Some(event) = xim_callback_event {
client.handle_xim_callback_event(event);
}
if xim_filtered {
continue;
}
if xim_connected {
client.xim_handle_event(event);
} else {
client.handle_event(event);
}
}
Ok(calloop::PostAction::Continue)
}
},
)
.expect("Failed to initialize x11 event source");
handle
.insert_source(XDPEventSource::new(&common.background_executor), {
move |event, _, client| match event {
XDPEvent::WindowAppearance(appearance) => {
client.with_common(|common| common.appearance = appearance);
for (_, window) in &mut client.0.borrow_mut().windows {
window.window.set_appearance(appearance);
}
}
XDPEvent::CursorTheme(_) | XDPEvent::CursorSize(_) => {
// noop, X11 manages this for us.
}
}
})
.unwrap();
let xdp_event_source = XDPEventSource::new(&common.background_executor);
X11Client(Rc::new(RefCell::new(X11ClientState {
modifiers: Modifiers::default(),
event_loop: Some(event_loop),
loop_handle: handle,
runnables,
xdp_event_source,
quit_signal_rx,
common,
modifiers: Modifiers::default(),
last_click: Instant::now(),
last_location: Point::new(px(0.0), px(0.0)),
current_count: 0,
@@ -400,7 +310,7 @@ impl X11Client {
ximc,
xim_handler,
compose_state: compose_state,
compose_state,
pre_edit_text: None,
composing: false,
@@ -413,6 +323,7 @@ impl X11Client {
scroll_y: None,
clipboard,
clipboard_item: None,
})))
}
@@ -478,6 +389,50 @@ impl X11Client {
.map(|window_reference| window_reference.window.clone())
}
fn handle_events(&self, events: Vec<Event>) {
for event in events.into_iter() {
let mut state = self.0.borrow_mut();
if state.ximc.is_none() || state.xim_handler.is_none() {
drop(state);
self.handle_event(event);
continue;
}
let mut ximc = state.ximc.take().unwrap();
let mut xim_handler = state.xim_handler.take().unwrap();
let xim_connected = xim_handler.connected;
drop(state);
let xim_filtered = match ximc.filter_event(&event, &mut xim_handler) {
Ok(handled) => handled,
Err(err) => {
log::error!("XIMClientError: {}", err);
false
}
};
let xim_callback_event = xim_handler.last_callback_event.take();
let mut state = self.0.borrow_mut();
state.ximc = Some(ximc);
state.xim_handler = Some(xim_handler);
drop(state);
if let Some(event) = xim_callback_event {
self.handle_xim_callback_event(event);
}
if xim_filtered {
continue;
}
if xim_connected {
self.xim_handle_event(event);
} else {
self.handle_event(event);
}
}
}
fn handle_event(&self, event: Event) -> Option<()> {
match event {
Event::ClientMessage(event) => {
@@ -524,7 +479,9 @@ impl X11Client {
window.set_focused(false);
let mut state = self.0.borrow_mut();
state.focused_window = None;
state.compose_state.reset();
if let Some(compose_state) = state.compose_state.as_mut() {
compose_state.reset();
}
state.pre_edit_text.take();
drop(state);
self.disable_ime();
@@ -570,37 +527,42 @@ impl X11Client {
if keysym.is_modifier_key() {
return Some(());
}
state.compose_state.feed(keysym);
match state.compose_state.status() {
xkbc::Status::Composed => {
state.pre_edit_text.take();
keystroke.ime_key = state.compose_state.utf8();
keystroke.key =
xkbc::keysym_get_name(state.compose_state.keysym().unwrap());
}
xkbc::Status::Composing => {
state.pre_edit_text = state
.compose_state
.utf8()
.or(crate::Keystroke::underlying_dead_key(keysym));
let pre_edit = state.pre_edit_text.clone().unwrap_or(String::default());
drop(state);
window.handle_ime_preedit(pre_edit);
state = self.0.borrow_mut();
}
xkbc::Status::Cancelled => {
let pre_edit = state.pre_edit_text.take();
drop(state);
if let Some(pre_edit) = pre_edit {
window.handle_ime_commit(pre_edit);
if let Some(mut compose_state) = state.compose_state.take() {
compose_state.feed(keysym);
match compose_state.status() {
xkbc::Status::Composed => {
state.pre_edit_text.take();
keystroke.ime_key = compose_state.utf8();
if let Some(keysym) = compose_state.keysym() {
keystroke.key = xkbc::keysym_get_name(keysym);
}
}
if let Some(current_key) = Keystroke::underlying_dead_key(keysym) {
window.handle_ime_preedit(current_key);
xkbc::Status::Composing => {
keystroke.ime_key = None;
state.pre_edit_text = compose_state
.utf8()
.or(crate::Keystroke::underlying_dead_key(keysym));
let pre_edit =
state.pre_edit_text.clone().unwrap_or(String::default());
drop(state);
window.handle_ime_preedit(pre_edit);
state = self.0.borrow_mut();
}
state = self.0.borrow_mut();
state.compose_state.feed(keysym);
xkbc::Status::Cancelled => {
let pre_edit = state.pre_edit_text.take();
drop(state);
if let Some(pre_edit) = pre_edit {
window.handle_ime_commit(pre_edit);
}
if let Some(current_key) = Keystroke::underlying_dead_key(keysym) {
window.handle_ime_preedit(current_key);
}
state = self.0.borrow_mut();
compose_state.feed(keysym);
}
_ => {}
}
_ => {}
state.compose_state = Some(compose_state);
}
keystroke
};
@@ -649,7 +611,9 @@ impl X11Client {
window.handle_ime_unmark();
state = self.0.borrow_mut();
} else if let Some(text) = state.pre_edit_text.take() {
state.compose_state.reset();
if let Some(compose_state) = state.compose_state.as_mut() {
compose_state.reset();
}
drop(state);
window.handle_ime_commit(text);
state = self.0.borrow_mut();
@@ -901,6 +865,39 @@ impl X11Client {
drop(state);
Some(())
}
fn send_window_expose_events(
&self,
x_windows: impl IntoIterator<Item = xproto::Window>,
) -> anyhow::Result<()> {
let state = self.0.borrow_mut();
for x_window in x_windows.into_iter() {
state
.xcb_connection
.send_event(
false,
x_window,
xproto::EventMask::EXPOSURE,
xproto::ExposeEvent {
response_type: xproto::EXPOSE_EVENT,
sequence: 0,
window: x_window,
x: 0,
y: 0,
width: 0,
height: 0,
count: 1,
},
)
.context("failed to send ExposeEvent for window")?;
}
state
.xcb_connection
.flush()
.context("failed to flush XCB connection after sending ExposeEvent")
}
}
impl LinuxClient for X11Client {
@@ -973,69 +970,8 @@ impl LinuxClient for X11Client {
state.common.appearance,
)?;
let screen_resources = state
.xcb_connection
.randr_get_screen_resources(x_window)
.unwrap()
.reply()
.expect("Could not find available screens");
let mode = screen_resources
.crtcs
.iter()
.find_map(|crtc| {
let crtc_info = state
.xcb_connection
.randr_get_crtc_info(*crtc, x11rb::CURRENT_TIME)
.ok()?
.reply()
.ok()?;
screen_resources
.modes
.iter()
.find(|m| m.id == crtc_info.mode)
})
.expect("Unable to find screen refresh rate");
let refresh_event_token = state
.loop_handle
.insert_source(calloop::timer::Timer::immediate(), {
let refresh_duration = mode_refresh_rate(mode);
move |mut instant, (), client| {
let state = client.0.borrow_mut();
state
.xcb_connection
.send_event(
false,
x_window,
xproto::EventMask::EXPOSURE,
xproto::ExposeEvent {
response_type: xproto::EXPOSE_EVENT,
sequence: 0,
window: x_window,
x: 0,
y: 0,
width: 0,
height: 0,
count: 1,
},
)
.unwrap();
let _ = state.xcb_connection.flush().unwrap();
// Take into account that some frames have been skipped
let now = Instant::now();
while instant < now {
instant += refresh_duration;
}
calloop::timer::TimeoutAction::ToInstant(instant)
}
})
.expect("Failed to initialize refresh timer");
let window_ref = WindowRef {
window: window.0.clone(),
refresh_event_token,
};
state.windows.insert(x_window, window_ref);
@@ -1097,7 +1033,7 @@ impl LinuxClient for X11Client {
}
fn write_to_clipboard(&self, item: crate::ClipboardItem) {
let state = self.0.borrow_mut();
let mut state = self.0.borrow_mut();
state
.clipboard
.store(
@@ -1106,6 +1042,7 @@ impl LinuxClient for X11Client {
item.text().as_bytes(),
)
.ok();
state.clipboard_item.replace(item);
}
fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
@@ -1127,6 +1064,20 @@ impl LinuxClient for X11Client {
fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
let state = self.0.borrow_mut();
// if the last copy was from this app, return our cached item
// which has metadata attached.
if state
.clipboard
.setter
.connection
.get_selection_owner(state.clipboard.setter.atoms.clipboard)
.ok()
.and_then(|r| r.reply().ok())
.map(|reply| reply.owner == state.clipboard.setter.window)
.unwrap_or(false)
{
return state.clipboard_item.clone();
}
state
.clipboard
.load(
@@ -1143,14 +1094,108 @@ impl LinuxClient for X11Client {
}
fn run(&self) {
let mut event_loop = self
.0
.borrow_mut()
.event_loop
.take()
.expect("App is already running");
loop {
{
let mut state = self.0.borrow_mut();
if let Ok(Some(())) = state.quit_signal_rx.try_recv() {
return;
}
}
event_loop.run(None, &mut self.clone(), |_| {}).log_err();
// Send expose events to windows that need refreshing
let mut windows_to_expose = HashSet::new();
{
let state = self.0.borrow_mut();
for (x_window, window_ref) in state.windows.iter() {
if window_ref.window.needs_refresh() {
windows_to_expose.insert(*x_window);
window_ref.window.set_refresh_queued(true);
}
}
}
let mut sleep = windows_to_expose.is_empty();
let _ = self.send_window_expose_events(windows_to_expose).log_err();
// Read all X11 events and then handle them in a batch
{
let mut events = Vec::new();
let mut windows_to_refresh = HashSet::new();
{
let state = self.0.borrow_mut();
while let Ok(Some(event)) = state.xcb_connection.poll_for_event() {
if let Event::Expose(event) = event {
windows_to_refresh.insert(event.window);
} else {
events.push(event);
}
}
}
sleep = !sleep && events.is_empty() && windows_to_refresh.is_empty();
// We prioritize Expose events so that a lot of input events don't hold up
// a render.
for window in windows_to_refresh.into_iter() {
if let Some(window) = self.get_window(window) {
window.refresh();
window.set_refresh_queued(false);
}
}
self.handle_events(events);
}
// Handle runnables
{
let mut state = self.0.borrow_mut();
let now = Instant::now();
while let Ok(runnable) = state.runnables.try_recv() {
drop(state);
runnable.run();
sleep = false;
if now.elapsed() >= Duration::from_millis(2) {
println!("ran runnables for over 2ms");
break;
}
state = self.0.borrow_mut();
}
}
// Handle XDG events
{
let mut state = self.0.borrow_mut();
while let Ok(event) = state.xdp_event_source.try_recv() {
drop(state);
sleep = false;
match event {
XDPEvent::WindowAppearance(appearance) => {
self.with_common(|common| common.appearance = appearance);
for (_, window) in &mut self.0.borrow_mut().windows {
window.window.set_appearance(appearance);
}
}
XDPEvent::CursorTheme(_) | XDPEvent::CursorSize(_) => {
// noop, X11 manages this for us.
}
};
state = self.0.borrow_mut();
}
}
// Sleep for a very short duration to prevent busy-waiting
// But only if we had nothing to do in this iteration.
if sleep {
std::thread::sleep(Duration::from_millis(1));
}
}
}
fn active_window(&self) -> Option<AnyWindowHandle> {
@@ -1164,15 +1209,6 @@ impl LinuxClient for X11Client {
}
}
// Adatpted from:
// https://docs.rs/winit/0.29.11/src/winit/platform_impl/linux/x11/monitor.rs.html#103-111
pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration {
let millihertz = mode.dot_clock as u64 * 1_000 / (mode.htotal as u64 * mode.vtotal as u64);
let micros = 1_000_000_000 / millihertz;
log::info!("Refreshing at {} micros", micros);
Duration::from_micros(micros)
}
fn fp3232_to_f32(value: xinput::Fp3232) -> f32 {
value.integral as f32 + value.frac as f32 / u32::MAX as f32
}

View File

@@ -14,6 +14,7 @@ use util::{maybe, ResultExt};
use x11rb::{
connection::Connection,
protocol::{
randr::{self, ConnectionExt as _},
xinput::{self, ConnectionExt as _},
xproto::{
self, ClientMessageEvent, ConnectionExt as _, EventMask, TranslateCoordinatesReply,
@@ -31,10 +32,10 @@ use std::{
ptr::NonNull,
rc::Rc,
sync::{self, Arc},
time::{Duration, Instant},
};
use super::{X11Display, XINPUT_MASTER_DEVICE};
x11rb::atom_manager! {
pub XcbAtoms: AtomsCookie {
UTF8_STRING,
@@ -159,6 +160,9 @@ pub struct Callbacks {
pub struct X11WindowState {
pub destroyed: bool,
pub refresh_rate: Duration,
refresh_queued: bool,
pub last_refresh_at: Option<Instant>,
client: X11ClientStatePtr,
executor: ForegroundExecutor,
atoms: XcbAtoms,
@@ -178,7 +182,7 @@ pub(crate) struct X11WindowStatePtr {
pub state: Rc<RefCell<X11WindowState>>,
pub(crate) callbacks: Rc<RefCell<Callbacks>>,
xcb_connection: Rc<XCBConnection>,
x_window: xproto::Window,
pub x_window: xproto::Window,
}
impl rwh::HasWindowHandle for RawWindow {
@@ -397,6 +401,31 @@ impl X11WindowState {
};
xcb_connection.map_window(x_window).unwrap();
let screen_resources = xcb_connection
.randr_get_screen_resources(x_window)
.unwrap()
.reply()
.expect("Could not find available screens");
let mode = screen_resources
.crtcs
.iter()
.find_map(|crtc| {
let crtc_info = xcb_connection
.randr_get_crtc_info(*crtc, x11rb::CURRENT_TIME)
.ok()?
.reply()
.ok()?;
screen_resources
.modes
.iter()
.find(|m| m.id == crtc_info.mode)
})
.expect("Unable to find screen refresh rate");
let refresh_rate = mode_refresh_rate(&mode);
Ok(Self {
client,
executor,
@@ -413,6 +442,9 @@ impl X11WindowState {
appearance,
handle,
destroyed: false,
refresh_rate,
refresh_queued: false,
last_refresh_at: None,
})
}
@@ -582,6 +614,10 @@ impl X11WindowStatePtr {
let mut cb = self.callbacks.borrow_mut();
if let Some(ref mut fun) = cb.request_frame {
fun();
self.state
.borrow_mut()
.last_refresh_at
.replace(Instant::now());
}
}
@@ -715,6 +751,23 @@ impl X11WindowStatePtr {
(fun)()
}
}
pub fn needs_refresh(&self) -> bool {
let state = self.state.borrow();
if state.refresh_queued {
return false;
}
let refresh_rate = state.refresh_rate;
state.last_refresh_at.map_or(false, |last_refresh_at| {
last_refresh_at.elapsed() >= refresh_rate
})
}
pub fn set_refresh_queued(&self, value: bool) {
self.state.borrow_mut().refresh_queued = value;
}
}
impl PlatformWindow for X11Window {
@@ -1028,3 +1081,16 @@ impl PlatformWindow for X11Window {
false
}
}
// Adapted from:
// https://docs.rs/winit/0.29.11/src/winit/platform_impl/linux/x11/monitor.rs.html#103-111
pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration {
if mode.dot_clock == 0 || mode.htotal == 0 || mode.vtotal == 0 {
return Duration::from_millis(16);
}
let millihertz = mode.dot_clock as u64 * 1_000 / (mode.htotal as u64 * mode.vtotal as u64);
let micros = 1_000_000_000 / millihertz;
log::info!("Refreshing at {} micros", micros);
Duration::from_micros(micros)
}

View File

@@ -2,6 +2,7 @@
//!
//! This module uses the [ashpd] crate
use anyhow::anyhow;
use ashpd::desktop::settings::{ColorScheme, Settings};
use calloop::channel::Channel;
use calloop::{EventSource, Poll, PostAction, Readiness, Token, TokenFactory};
@@ -98,6 +99,12 @@ impl XDPEventSource {
Self { channel }
}
pub fn try_recv(&self) -> anyhow::Result<Event> {
self.channel
.try_recv()
.map_err(|error| anyhow!("{}", error))
}
}
impl EventSource for XDPEventSource {

View File

@@ -344,6 +344,7 @@ struct MacWindowState {
// Whether the next left-mouse click is also the focusing click.
first_mouse: bool,
fullscreen_restore_bounds: Bounds<Pixels>,
ime_composing: bool,
}
impl MacWindowState {
@@ -504,6 +505,7 @@ impl MacWindow {
focus,
show,
display_id,
window_min_size,
}: WindowParams,
executor: ForegroundExecutor,
renderer_context: renderer::Context,
@@ -623,6 +625,7 @@ impl MacWindow {
external_files_dragged: false,
first_mouse: false,
fullscreen_restore_bounds: Bounds::default(),
ime_composing: false,
})));
(*native_window).set_ivar(
@@ -644,6 +647,13 @@ impl MacWindow {
native_window.setMovable_(is_movable as BOOL);
if let Some(window_min_size) = window_min_size {
native_window.setContentMinSize_(NSSize {
width: window_min_size.width.to_f64(),
height: window_min_size.height.to_f64(),
});
}
if titlebar.map_or(true, |titlebar| titlebar.appears_transparent) {
native_window.setTitlebarAppearsTransparent_(YES);
native_window.setTitleVisibility_(NSWindowTitleVisibility::NSWindowTitleHidden);
@@ -1234,6 +1244,7 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
let mut lock = window_state.lock();
let previous_keydown_inserted_text = lock.previous_keydown_inserted_text.take();
let mut last_inserts = lock.last_ime_inputs.take().unwrap();
let ime_composing = std::mem::take(&mut lock.ime_composing);
let mut callback = lock.event_callback.take();
drop(lock);
@@ -1248,7 +1259,8 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
let is_composing =
with_input_handler(this, |input_handler| input_handler.marked_text_range())
.flatten()
.is_some();
.is_some()
|| ime_composing;
if let Some((text, range)) = last_insert {
if !is_composing {
@@ -1660,9 +1672,13 @@ extern "C" fn first_rect_for_character_range(
range: NSRange,
_: id,
) -> NSRect {
let frame = unsafe {
let window = get_window_state(this).lock().native_window;
NSView::frame(window)
let frame: NSRect = unsafe {
let state = get_window_state(this);
let lock = state.lock();
let mut frame = NSWindow::frame(lock.native_window);
let content_layout_rect: CGRect = msg_send![lock.native_window, contentLayoutRect];
frame.origin.y -= frame.size.height - content_layout_rect.size.height;
frame
};
with_input_handler(this, |input_handler| {
input_handler.bounds_for_range(range.to_range()?)
@@ -1922,6 +1938,7 @@ fn send_to_input_handler(window: &Object, ime: ImeInput) {
input_handler.replace_text_in_range(range, &text)
}
ImeInput::SetMarkedText(text, range, marked_range) => {
lock.ime_composing = true;
drop(lock);
input_handler.replace_and_mark_text_in_range(range, &text, marked_range)
}

View File

@@ -267,14 +267,8 @@ fn handle_syskeydown_msg(
) -> Option<isize> {
// we need to call `DefWindowProcW`, or we will lose the system-wide `Alt+F4`, `Alt+{other keys}`
// shortcuts.
let Some(keystroke) = parse_syskeydown_msg_keystroke(wparam) else {
return None;
};
let mut lock = state_ptr.state.borrow_mut();
let Some(mut func) = lock.callbacks.input.take() else {
return None;
};
drop(lock);
let keystroke = parse_syskeydown_msg_keystroke(wparam)?;
let mut func = state_ptr.state.borrow_mut().callbacks.input.take()?;
let event = KeyDownEvent {
keystroke,
is_held: lparam.0 & (0x1 << 30) > 0,
@@ -292,14 +286,8 @@ fn handle_syskeydown_msg(
fn handle_syskeyup_msg(wparam: WPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
// we need to call `DefWindowProcW`, or we will lose the system-wide `Alt+F4`, `Alt+{other keys}`
// shortcuts.
let Some(keystroke) = parse_syskeydown_msg_keystroke(wparam) else {
return None;
};
let mut lock = state_ptr.state.borrow_mut();
let Some(mut func) = lock.callbacks.input.take() else {
return None;
};
drop(lock);
let keystroke = parse_syskeydown_msg_keystroke(wparam)?;
let mut func = state_ptr.state.borrow_mut().callbacks.input.take()?;
let event = KeyUpEvent { keystroke };
let result = if func(PlatformInput::KeyUp(event)).default_prevented {
Some(0)
@@ -614,35 +602,25 @@ fn handle_ime_composition(
) -> Option<isize> {
let mut ime_input = None;
if lparam.0 as u32 & GCS_COMPSTR.0 > 0 {
let Some((string, string_len)) = parse_ime_compostion_string(handle) else {
return None;
};
let mut lock = state_ptr.state.borrow_mut();
let Some(mut input_handler) = lock.input_handler.take() else {
return None;
};
drop(lock);
input_handler.replace_and_mark_text_in_range(None, string.as_str(), Some(0..string_len));
let (comp_string, string_len) = parse_ime_compostion_string(handle)?;
let mut input_handler = state_ptr.state.borrow_mut().input_handler.take()?;
input_handler.replace_and_mark_text_in_range(
None,
&comp_string,
Some(string_len..string_len),
);
state_ptr.state.borrow_mut().input_handler = Some(input_handler);
ime_input = Some(string);
ime_input = Some(comp_string);
}
if lparam.0 as u32 & GCS_CURSORPOS.0 > 0 {
let Some(ref comp_string) = ime_input else {
return None;
};
let comp_string = &ime_input?;
let caret_pos = retrieve_composition_cursor_position(handle);
let mut lock = state_ptr.state.borrow_mut();
let Some(mut input_handler) = lock.input_handler.take() else {
return None;
};
drop(lock);
input_handler.replace_and_mark_text_in_range(None, comp_string, Some(0..caret_pos));
let mut input_handler = state_ptr.state.borrow_mut().input_handler.take()?;
input_handler.replace_and_mark_text_in_range(None, comp_string, Some(caret_pos..caret_pos));
state_ptr.state.borrow_mut().input_handler = Some(input_handler);
}
if lparam.0 as u32 & GCS_RESULTSTR.0 > 0 {
let Some(comp_result) = parse_ime_compostion_result(handle) else {
return None;
};
let comp_result = parse_ime_compostion_result(handle)?;
let mut lock = state_ptr.state.borrow_mut();
let Some(mut input_handler) = lock.input_handler.take() else {
return Some(1);
@@ -663,11 +641,7 @@ fn handle_calc_client_size(
lparam: LPARAM,
state_ptr: Rc<WindowsWindowStatePtr>,
) -> Option<isize> {
if !state_ptr.hide_title_bar || state_ptr.state.borrow().is_fullscreen() {
return None;
}
if wparam.0 == 0 {
if !state_ptr.hide_title_bar || state_ptr.state.borrow().is_fullscreen() || wparam.0 == 0 {
return None;
}
@@ -1097,13 +1071,14 @@ fn parse_syskeydown_msg_keystroke(wparam: WPARAM) -> Option<Keystroke> {
VK_NEXT => "pagedown",
VK_ESCAPE => "escape",
VK_INSERT => "insert",
VK_DELETE => "delete",
_ => return basic_vkcode_to_string(vk_code, modifiers),
}
.to_owned();
Some(Keystroke {
modifiers,
key: key,
key,
ime_key: None,
})
}
@@ -1160,7 +1135,7 @@ fn parse_keydown_msg_keystroke(wparam: WPARAM) -> Option<KeystrokeOrModifier> {
Some(KeystrokeOrModifier::Keystroke(Keystroke {
modifiers,
key: key,
key,
ime_key: None,
}))
}

View File

@@ -58,7 +58,7 @@ impl<'de> serde::Deserialize<'de> for FontFeatures {
while let Some((key, value)) =
access.next_entry::<String, Option<FeatureValue>>()?
{
if key.len() != 4 && !key.is_ascii() {
if !is_valid_feature_tag(&key) {
log::error!("Incorrect font feature tag: {}", key);
continue;
}
@@ -142,3 +142,7 @@ impl schemars::JsonSchema for FontFeatures {
schema.into()
}
}
fn is_valid_feature_tag(tag: &str) -> bool {
tag.len() == 4 && tag.chars().all(|c| c.is_ascii_alphanumeric())
}

View File

@@ -549,6 +549,7 @@ pub struct Window {
pub(crate) focus: Option<FocusId>,
focus_enabled: bool,
pending_input: Option<PendingInput>,
pending_modifiers: Option<Modifiers>,
pending_input_observers: SubscriberSet<(), AnyObserver>,
prompt: Option<RenderablePromptHandle>,
}
@@ -631,6 +632,7 @@ impl Window {
display_id,
window_background,
app_id,
window_min_size,
} = options;
let bounds = window_bounds
@@ -647,6 +649,7 @@ impl Window {
show,
display_id,
window_background,
window_min_size,
},
)?;
let display_id = platform_window.display().map(|display| display.id());
@@ -821,6 +824,7 @@ impl Window {
focus: None,
focus_enabled: true,
pending_input: None,
pending_modifiers: None,
pending_input_observers: SubscriberSet::new(),
prompt: None,
})
@@ -3159,70 +3163,129 @@ impl<'a> WindowContext<'a> {
.dispatch_tree
.dispatch_path(node_id);
let mut bindings: SmallVec<[KeyBinding; 1]> = SmallVec::new();
let mut pending = false;
let mut keystroke: Option<Keystroke> = None;
if let Some(event) = event.downcast_ref::<ModifiersChangedEvent>() {
if let Some(previous) = self.window.pending_modifiers.take() {
if event.modifiers.number_of_modifiers() == 0 {
let key = match previous {
modifiers if modifiers.shift => Some("shift"),
modifiers if modifiers.control => Some("control"),
modifiers if modifiers.alt => Some("alt"),
modifiers if modifiers.platform => Some("platform"),
modifiers if modifiers.function => Some("function"),
_ => None,
};
if let Some(key) = key {
let key = Keystroke {
key: key.to_string(),
ime_key: None,
modifiers: Modifiers::default(),
};
let KeymatchResult {
bindings: modifier_bindings,
pending: pending_bindings,
} = self
.window
.rendered_frame
.dispatch_tree
.dispatch_key(&key, &dispatch_path);
keystroke = Some(key);
bindings = modifier_bindings;
pending = pending_bindings;
}
}
} else if event.modifiers.number_of_modifiers() == 1 {
self.window.pending_modifiers = Some(event.modifiers);
}
if keystroke.is_none() {
self.finish_dispatch_key_event(event, dispatch_path);
return;
}
}
if let Some(key_down_event) = event.downcast_ref::<KeyDownEvent>() {
let KeymatchResult { bindings, pending } = self
self.window.pending_modifiers.take();
let KeymatchResult {
bindings: key_down_bindings,
pending: key_down_pending,
} = self
.window
.rendered_frame
.dispatch_tree
.dispatch_key(&key_down_event.keystroke, &dispatch_path);
if pending {
let mut currently_pending = self.window.pending_input.take().unwrap_or_default();
if currently_pending.focus.is_some() && currently_pending.focus != self.window.focus
{
currently_pending = PendingInput::default();
}
currently_pending.focus = self.window.focus;
currently_pending
.keystrokes
.push(key_down_event.keystroke.clone());
for binding in bindings {
currently_pending.bindings.push(binding);
}
keystroke = Some(key_down_event.keystroke.clone());
currently_pending.timer = Some(self.spawn(|mut cx| async move {
cx.background_executor.timer(Duration::from_secs(1)).await;
cx.update(move |cx| {
cx.clear_pending_keystrokes();
let Some(currently_pending) = cx.window.pending_input.take() else {
return;
};
cx.pending_input_changed();
cx.replay_pending_input(currently_pending);
})
.log_err();
}));
bindings = key_down_bindings;
pending = key_down_pending;
}
self.window.pending_input = Some(currently_pending);
self.pending_input_changed();
self.propagate_event = false;
return;
} else if let Some(currently_pending) = self.window.pending_input.take() {
self.pending_input_changed();
if bindings
.iter()
.all(|binding| !currently_pending.used_by_binding(binding))
{
self.replay_pending_input(currently_pending)
}
if pending {
let mut currently_pending = self.window.pending_input.take().unwrap_or_default();
if currently_pending.focus.is_some() && currently_pending.focus != self.window.focus {
currently_pending = PendingInput::default();
}
if !bindings.is_empty() {
self.clear_pending_keystrokes();
currently_pending.focus = self.window.focus;
if let Some(keystroke) = keystroke {
currently_pending.keystrokes.push(keystroke.clone());
}
self.propagate_event = true;
for binding in bindings {
self.dispatch_action_on_node(node_id, binding.action.as_ref());
if !self.propagate_event {
self.dispatch_keystroke_observers(event, Some(binding.action));
return;
}
currently_pending.bindings.push(binding);
}
currently_pending.timer = Some(self.spawn(|mut cx| async move {
cx.background_executor.timer(Duration::from_secs(1)).await;
cx.update(move |cx| {
cx.clear_pending_keystrokes();
let Some(currently_pending) = cx.window.pending_input.take() else {
return;
};
cx.replay_pending_input(currently_pending);
cx.pending_input_changed();
})
.log_err();
}));
self.window.pending_input = Some(currently_pending);
self.pending_input_changed();
self.propagate_event = false;
return;
} else if let Some(currently_pending) = self.window.pending_input.take() {
self.pending_input_changed();
if bindings
.iter()
.all(|binding| !currently_pending.used_by_binding(binding))
{
self.replay_pending_input(currently_pending)
}
}
if !bindings.is_empty() {
self.clear_pending_keystrokes();
}
self.propagate_event = true;
for binding in bindings {
self.dispatch_action_on_node(node_id, binding.action.as_ref());
if !self.propagate_event {
self.dispatch_keystroke_observers(event, Some(binding.action));
return;
}
}
self.finish_dispatch_key_event(event, dispatch_path)
}
fn finish_dispatch_key_event(
&mut self,
event: &dyn Any,
dispatch_path: SmallVec<[DispatchNodeId; 32]>,
) {
self.dispatch_key_down_up_event(event, &dispatch_path);
if !self.propagate_event {
return;

View File

@@ -511,7 +511,7 @@ impl LanguageRegistry {
) -> impl Future<Output = Result<Arc<Language>>> {
let filename = path.file_name().and_then(|name| name.to_str());
let extension = path.extension_or_hidden_file_name();
let path_suffixes = [extension, filename];
let path_suffixes = [extension, filename, path.to_str()];
let empty = GlobSet::empty();
let rx = self.get_or_load_language(move |language_name, config| {

View File

@@ -662,6 +662,17 @@ impl settings::Settings for AllLanguageSettings {
.ok_or_else(Self::missing_default)?;
let mut file_types: HashMap<Arc<str>, GlobSet> = HashMap::default();
for (language, suffixes) in &default_value.file_types {
let mut builder = GlobSetBuilder::new();
for suffix in suffixes {
builder.add(Glob::new(suffix)?);
}
file_types.insert(language.clone(), builder.build()?);
}
for user_settings in sources.customizations() {
if let Some(copilot) = user_settings.features.as_ref().and_then(|f| f.copilot) {
copilot_enabled = Some(copilot);
@@ -701,6 +712,15 @@ impl settings::Settings for AllLanguageSettings {
for (language, suffixes) in &user_settings.file_types {
let mut builder = GlobSetBuilder::new();
let default_value = default_value.file_types.get(&language.clone());
// Merge the default value with the user's value.
if let Some(suffixes) = default_value {
for suffix in suffixes {
builder.add(Glob::new(suffix)?);
}
}
for suffix in suffixes {
builder.add(Glob::new(suffix)?);
}

View File

@@ -1,25 +1,32 @@
use anyhow::{anyhow, Result};
use anyhow::{anyhow, bail, Context, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
use collections::HashMap;
use feature_flags::FeatureFlagAppExt;
use futures::StreamExt;
use gpui::{AppContext, AsyncAppContext};
use http::github::{latest_github_release, GitHubLspBinaryVersion};
use language::{LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate};
use lsp::LanguageServerBinary;
use node_runtime::NodeRuntime;
use project::ContextProviderWithTasks;
use serde_json::{json, Value};
use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore};
use smol::fs;
use smol::{
fs::{self},
io::BufReader,
};
use std::{
any::Any,
env::consts,
ffi::OsString,
path::{Path, PathBuf},
str::FromStr,
sync::{Arc, OnceLock},
};
use task::{TaskTemplate, TaskTemplates, VariableName};
use util::{maybe, ResultExt};
use util::{fs::remove_matching, maybe, ResultExt};
const SERVER_PATH: &str =
"node_modules/vscode-langservers-extracted/bin/vscode-json-language-server";
@@ -206,7 +213,12 @@ impl LspAdapter for JsonLspAdapter {
}
fn language_ids(&self) -> HashMap<String, String> {
[("JSON".into(), "jsonc".into())].into_iter().collect()
[
("JSON".into(), "json".into()),
("JSONC".into(), "jsonc".into()),
]
.into_iter()
.collect()
}
}
@@ -251,3 +263,137 @@ fn schema_file_match(path: &Path) -> String {
.to_string()
.replace('\\', "/")
}
pub(super) struct NodeVersionAdapter;
#[async_trait(?Send)]
impl LspAdapter for NodeVersionAdapter {
fn name(&self) -> LanguageServerName {
LanguageServerName("package-version-server".into())
}
async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
let release = latest_github_release(
"zed-industries/package-version-server",
true,
false,
delegate.http_client(),
)
.await?;
let os = match consts::OS {
"macos" => "apple-darwin",
"linux" => "unknown-linux-gnu",
"windows" => "pc-windows-msvc",
other => bail!("Running on unsupported os: {other}"),
};
let suffix = if consts::OS == "windows" {
".zip"
} else {
".tar.gz"
};
let asset_name = format!("package-version-server-{}-{os}{suffix}", consts::ARCH);
let asset = release
.assets
.iter()
.find(|asset| asset.name == asset_name)
.with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
Ok(Box::new(GitHubLspBinaryVersion {
name: release.tag_name,
url: asset.browser_download_url.clone(),
}))
}
async fn fetch_server_binary(
&self,
latest_version: Box<dyn 'static + Send + Any>,
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = latest_version.downcast::<GitHubLspBinaryVersion>().unwrap();
let destination_path =
container_dir.join(format!("package-version-server-{}", version.name));
let destination_container_path =
container_dir.join(format!("package-version-server-{}-tmp", version.name));
if fs::metadata(&destination_path).await.is_err() {
let mut response = delegate
.http_client()
.get(&version.url, Default::default(), true)
.await
.map_err(|err| anyhow!("error downloading release: {}", err))?;
if version.url.ends_with(".zip") {
node_runtime::extract_zip(
&destination_container_path,
BufReader::new(response.body_mut()),
)
.await?;
} else if version.url.ends_with(".tar.gz") {
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
let archive = Archive::new(decompressed_bytes);
archive.unpack(&destination_container_path).await?;
}
fs::copy(
destination_container_path.join("package-version-server"),
&destination_path,
)
.await?;
// todo("windows")
#[cfg(not(windows))]
{
fs::set_permissions(
&destination_path,
<fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
)
.await?;
}
remove_matching(&container_dir, |entry| entry != destination_path).await;
}
Ok(LanguageServerBinary {
path: destination_path,
env: None,
arguments: Default::default(),
})
}
async fn cached_server_binary(
&self,
container_dir: PathBuf,
_delegate: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
get_cached_version_server_binary(container_dir).await
}
async fn installation_test_binary(
&self,
container_dir: PathBuf,
) -> Option<LanguageServerBinary> {
get_cached_version_server_binary(container_dir)
.await
.map(|mut binary| {
binary.arguments = vec!["--version".into()];
binary
})
}
}
async fn get_cached_version_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
maybe!(async {
let mut last = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {
last = Some(entry?.path());
}
anyhow::Ok(LanguageServerBinary {
path: last.ok_or_else(|| anyhow!("no cached binary"))?,
env: None,
arguments: Default::default(),
})
})
.await
.log_err()
}

View File

@@ -0,0 +1,3 @@
("[" @open "]" @close)
("{" @open "}" @close)
("\"" @open "\"" @close)

View File

@@ -0,0 +1,12 @@
name = "JSONC"
grammar = "jsonc"
path_suffixes = ["jsonc"]
line_comments = ["// "]
autoclose_before = ",]}"
brackets = [
{ start = "{", end = "}", close = true, newline = true },
{ start = "[", end = "]", close = true, newline = true },
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
]
tab_size = 2
prettier_parser_name = "jsonc"

View File

@@ -0,0 +1,14 @@
; Only produce one embedding for the entire file.
(document) @item
; Collapse arrays, except for the first object.
(array
"[" @keep
.
(object)? @keep
"]" @keep) @collapse
; Collapse string values (but not keys).
(pair value: (string
"\"" @keep
"\"" @keep) @collapse)

View File

@@ -0,0 +1,21 @@
(comment) @comment
(string) @string
(pair
key: (string) @property.json_key)
(number) @number
[
(true)
(false)
(null)
] @constant
[
"{"
"}"
"["
"]"
] @punctuation.bracket

View File

@@ -0,0 +1,2 @@
(array "]" @end) @indent
(object "}" @end) @indent

View File

@@ -0,0 +1,2 @@
(pair
key: (string (string_content) @name)) @item

View File

@@ -0,0 +1 @@
(string) @string

View File

@@ -0,0 +1,4 @@
(pair value: (number) @redact)
(pair value: (string) @redact)
(array (number) @redact)
(array (string) @redact)

View File

@@ -45,6 +45,7 @@ pub fn init(
("gowork", tree_sitter_gowork::language()),
("jsdoc", tree_sitter_jsdoc::language()),
("json", tree_sitter_json::language()),
("jsonc", tree_sitter_json::language()),
("markdown", tree_sitter_markdown::language()),
("proto", tree_sitter_proto::language()),
("python", tree_sitter_python::language()),
@@ -117,6 +118,17 @@ pub fn init(
language!(
"json",
vec![
Arc::new(json::JsonLspAdapter::new(
node_runtime.clone(),
languages.clone(),
)),
Arc::new(json::NodeVersionAdapter)
],
json_task_context()
);
language!(
"jsonc",
vec![Arc::new(json::JsonLspAdapter::new(
node_runtime.clone(),
languages.clone(),

View File

@@ -201,11 +201,18 @@ impl LspAdapter for RustLspAdapter {
completion: &lsp::CompletionItem,
language: &Arc<Language>,
) -> Option<CodeLabel> {
let detail = completion
.detail
.as_ref()
.or(completion
.label_details
.as_ref()
.and_then(|detail| detail.detail.as_ref()))
.map(ToOwned::to_owned);
match completion.kind {
Some(lsp::CompletionItemKind::FIELD) if completion.detail.is_some() => {
let detail = completion.detail.as_ref().unwrap();
Some(lsp::CompletionItemKind::FIELD) if detail.is_some() => {
let name = &completion.label;
let text = format!("{}: {}", name, detail);
let text = format!("{}: {}", name, detail.unwrap());
let source = Rope::from(format!("struct S {{ {} }}", text).as_str());
let runs = language.highlight_text(&source, 11..11 + text.len());
return Some(CodeLabel {
@@ -215,12 +222,11 @@ impl LspAdapter for RustLspAdapter {
});
}
Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE)
if completion.detail.is_some()
if detail.is_some()
&& completion.insert_text_format != Some(lsp::InsertTextFormat::SNIPPET) =>
{
let detail = completion.detail.as_ref().unwrap();
let name = &completion.label;
let text = format!("{}: {}", name, detail);
let text = format!("{}: {}", name, detail.unwrap());
let source = Rope::from(format!("let {} = ();", text).as_str());
let runs = language.highlight_text(&source, 4..4 + text.len());
return Some(CodeLabel {
@@ -230,12 +236,12 @@ impl LspAdapter for RustLspAdapter {
});
}
Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD)
if completion.detail.is_some() =>
if detail.is_some() =>
{
lazy_static! {
static ref REGEX: Regex = Regex::new("\\(…?\\)").unwrap();
}
let detail = completion.detail.as_ref().unwrap();
let detail = detail.unwrap();
const FUNCTION_PREFIXES: [&'static str; 2] = ["async fn", "fn"];
let prefix = FUNCTION_PREFIXES
.iter()
@@ -269,9 +275,14 @@ impl LspAdapter for RustLspAdapter {
_ => None,
};
let highlight_id = language.grammar()?.highlight_id_for_name(highlight_name?)?;
let mut label = CodeLabel::plain(completion.label.clone(), None);
let mut label = completion.label.clone();
if let Some(detail) = detail.filter(|detail| detail.starts_with(" (")) {
use std::fmt::Write;
write!(label, "{detail}").ok()?;
}
let mut label = CodeLabel::plain(label, None);
label.runs.push((
0..label.text.rfind('(').unwrap_or(label.text.len()),
0..label.text.rfind('(').unwrap_or(completion.label.len()),
highlight_id,
));
return Some(label);

View File

@@ -200,6 +200,7 @@ impl LspAdapter for TypeScriptLspAdapter {
) -> Result<Option<serde_json::Value>> {
Ok(Some(json!({
"provideFormatter": true,
"hostInfo": "zed",
"tsserver": {
"path": "node_modules/typescript/lib",
},

View File

@@ -162,47 +162,42 @@ impl LspAdapter for VtslsLspAdapter {
_: &Arc<dyn LspAdapterDelegate>,
) -> Result<Option<serde_json::Value>> {
Ok(Some(json!({
"typescript":
{
"typescript": {
"tsdk": "node_modules/typescript/lib",
"format": {
"enable": true
},
"inlayHints":{
"parameterNames":
{
"inlayHints": {
"parameterNames": {
"enabled": "all",
"suppressWhenArgumentMatchesName": false,
},
"parameterTypes":
{
"parameterTypes": {
"enabled": true
},
"variableTypes": {
"enabled": true,
"suppressWhenTypeMatchesName": false,
},
"propertyDeclarationTypes":{
"propertyDeclarationTypes": {
"enabled": true,
},
"functionLikeReturnTypes": {
"enabled": true,
},
"enumMemberValues":{
"enumMemberValues": {
"enabled": true,
}
}
},
"vtsls":
{"experimental": {
"completion": {
"enableServerSideFuzzyMatch": true,
"entriesLimit": 5000,
"vtsls": {
"experimental": {
"completion": {
"enableServerSideFuzzyMatch": true,
"entriesLimit": 5000,
}
}
}
}
})))
}
@@ -220,40 +215,36 @@ impl LspAdapter for VtslsLspAdapter {
"format": {
"enable": true
},
"inlayHints":{
"parameterNames":
{
"inlayHints": {
"parameterNames": {
"enabled": "all",
"suppressWhenArgumentMatchesName": false,
},
"parameterTypes":
{
"parameterTypes": {
"enabled": true
},
"variableTypes": {
"enabled": true,
"suppressWhenTypeMatchesName": false,
},
"propertyDeclarationTypes":{
"propertyDeclarationTypes": {
"enabled": true,
},
"functionLikeReturnTypes": {
"enabled": true,
},
"enumMemberValues":{
"enumMemberValues": {
"enabled": true,
}
}
},
"vtsls":
{"experimental": {
"completion": {
"enableServerSideFuzzyMatch": true,
"entriesLimit": 5000,
}
}
},
"vtsls": {
"experimental": {
"completion": {
"enableServerSideFuzzyMatch": true,
"entriesLimit": 5000,
}
}
}
}))
}

View File

@@ -1,6 +1,7 @@
mod archive;
use anyhow::{anyhow, bail, Context, Result};
pub use archive::extract_zip;
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use futures::AsyncReadExt;

View File

@@ -55,6 +55,8 @@ pub enum Model {
#[serde(rename = "gpt-4o", alias = "gpt-4o-2024-05-13")]
#[default]
FourOmni,
#[serde(rename = "custom")]
Custom { name: String, max_tokens: usize },
}
impl Model {
@@ -74,15 +76,17 @@ impl Model {
Self::Four => "gpt-4",
Self::FourTurbo => "gpt-4-turbo-preview",
Self::FourOmni => "gpt-4o",
Self::Custom { .. } => "custom",
}
}
pub fn display_name(&self) -> &'static str {
pub fn display_name(&self) -> &str {
match self {
Self::ThreePointFiveTurbo => "gpt-3.5-turbo",
Self::Four => "gpt-4",
Self::FourTurbo => "gpt-4-turbo",
Self::FourOmni => "gpt-4o",
Self::Custom { name, .. } => name,
}
}
@@ -92,12 +96,24 @@ impl Model {
Model::Four => 8192,
Model::FourTurbo => 128000,
Model::FourOmni => 128000,
Model::Custom { max_tokens, .. } => *max_tokens,
}
}
}
fn serialize_model<S>(model: &Model, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match model {
Model::Custom { name, .. } => serializer.serialize_str(name),
_ => serializer.serialize_str(model.id()),
}
}
#[derive(Debug, Serialize)]
pub struct Request {
#[serde(serialize_with = "serialize_model")]
pub model: Model,
pub messages: Vec<RequestMessage>,
pub stream: bool,

View File

@@ -8192,7 +8192,7 @@ impl Project {
}
};
if abs_path.ends_with(local_settings_file_relative_path()) {
if path.ends_with(local_settings_file_relative_path()) {
let settings_dir = Arc::from(
path.ancestors()
.nth(local_settings_file_relative_path().components().count())
@@ -8209,7 +8209,7 @@ impl Project {
},
)
});
} else if abs_path.ends_with(local_tasks_file_relative_path()) {
} else if path.ends_with(local_tasks_file_relative_path()) {
self.task_inventory().update(cx, |task_inventory, cx| {
if removed {
task_inventory.remove_local_static_source(&abs_path);
@@ -8229,7 +8229,7 @@ impl Project {
);
}
})
} else if abs_path.ends_with(local_vscode_tasks_file_relative_path()) {
} else if path.ends_with(local_vscode_tasks_file_relative_path()) {
self.task_inventory().update(cx, |task_inventory, cx| {
if removed {
task_inventory.remove_local_static_source(&abs_path);

View File

@@ -72,7 +72,7 @@ pub struct ProjectPanel {
width: Option<Pixels>,
pending_serialization: Task<Option<()>>,
show_scrollbar: bool,
is_dragging_scrollbar: Rc<Cell<bool>>,
scrollbar_drag_thumb_offset: Rc<Cell<Option<f32>>>,
hide_scrollbar_task: Option<Task<()>>,
}
@@ -289,7 +289,7 @@ impl ProjectPanel {
pending_serialization: Task::ready(None),
show_scrollbar: !Self::should_autohide_scrollbar(cx),
hide_scrollbar_task: None,
is_dragging_scrollbar: Default::default(),
scrollbar_drag_thumb_offset: Default::default(),
};
this.update_visible_entries(None, cx);
@@ -2231,7 +2231,7 @@ impl ProjectPanel {
let height = scroll_handle
.last_item_height
.filter(|_| self.show_scrollbar || self.is_dragging_scrollbar.get())?;
.filter(|_| self.show_scrollbar || self.scrollbar_drag_thumb_offset.get().is_some())?;
let total_list_length = height.0 as f64 * items_count as f64;
let current_offset = scroll_handle.base_handle.offset().y.0.min(0.).abs() as f64;
@@ -2270,7 +2270,7 @@ impl ProjectPanel {
.on_mouse_up(
MouseButton::Left,
cx.listener(|this, _, cx| {
if !this.is_dragging_scrollbar.get()
if this.scrollbar_drag_thumb_offset.get().is_none()
&& !this.focus_handle.contains_focused(cx)
{
this.hide_scrollbar(cx);
@@ -2293,7 +2293,7 @@ impl ProjectPanel {
.child(ProjectPanelScrollbar::new(
percentage as f32..end_offset as f32,
self.scroll_handle.clone(),
self.is_dragging_scrollbar.clone(),
self.scrollbar_drag_thumb_offset.clone(),
cx.view().clone().into(),
items_count,
)),

View File

@@ -9,7 +9,8 @@ use ui::{prelude::*, px, relative, IntoElement};
pub(crate) struct ProjectPanelScrollbar {
thumb: Range<f32>,
scroll: UniformListScrollHandle,
is_dragging_scrollbar: Rc<Cell<bool>>,
// If Some(), there's an active drag, offset by percentage from the top of thumb.
scrollbar_drag_state: Rc<Cell<Option<f32>>>,
item_count: usize,
view: AnyView,
}
@@ -18,14 +19,14 @@ impl ProjectPanelScrollbar {
pub(crate) fn new(
thumb: Range<f32>,
scroll: UniformListScrollHandle,
is_dragging_scrollbar: Rc<Cell<bool>>,
scrollbar_drag_state: Rc<Cell<Option<f32>>>,
view: AnyView,
item_count: usize,
) -> Self {
Self {
thumb,
scroll,
is_dragging_scrollbar,
scrollbar_drag_state,
item_count,
view,
}
@@ -97,7 +98,7 @@ impl gpui::Element for ProjectPanelScrollbar {
let item_count = self.item_count;
cx.on_mouse_event({
let scroll = self.scroll.clone();
let is_dragging = self.is_dragging_scrollbar.clone();
let is_dragging = self.scrollbar_drag_state.clone();
move |event: &MouseDownEvent, phase, _cx| {
if phase.bubble() && bounds.contains(&event.position) {
if !thumb_bounds.contains(&event.position) {
@@ -113,7 +114,9 @@ impl gpui::Element for ProjectPanelScrollbar {
.set_offset(point(px(0.), -max_offset * percentage));
}
} else {
is_dragging.set(true);
let thumb_top_offset =
(event.position.y - thumb_bounds.origin.y) / bounds.size.height;
is_dragging.set(Some(thumb_top_offset));
}
}
}
@@ -130,14 +133,15 @@ impl gpui::Element for ProjectPanelScrollbar {
}
}
});
let is_dragging = self.is_dragging_scrollbar.clone();
let drag_state = self.scrollbar_drag_state.clone();
let view_id = self.view.entity_id();
cx.on_mouse_event(move |event: &MouseMoveEvent, _, cx| {
if event.dragging() && is_dragging.get() {
if let Some(drag_state) = drag_state.get().filter(|_| event.dragging()) {
let scroll = scroll.0.borrow();
if let Some(last_height) = scroll.last_item_height {
let max_offset = item_count as f32 * last_height;
let percentage = (event.position.y - bounds.origin.y) / bounds.size.height;
let percentage =
(event.position.y - bounds.origin.y) / bounds.size.height - drag_state;
let percentage = percentage.min(1. - thumb_percentage_size);
scroll
@@ -146,13 +150,13 @@ impl gpui::Element for ProjectPanelScrollbar {
cx.notify(view_id);
}
} else {
is_dragging.set(false);
drag_state.set(None);
}
});
let is_dragging = self.is_dragging_scrollbar.clone();
let is_dragging = self.scrollbar_drag_state.clone();
cx.on_mouse_event(move |_event: &MouseUpEvent, phase, cx| {
if phase.bubble() {
is_dragging.set(false);
is_dragging.set(None);
cx.notify(view_id);
}
});

View File

@@ -60,7 +60,12 @@ impl ImageView {
let bytes = base64::decode(base64_encoded_data)?;
let format = image::guess_format(&bytes)?;
let data = image::load_from_memory_with_format(&bytes, format)?.into_bgra8();
let mut data = image::load_from_memory_with_format(&bytes, format)?.into_rgba8();
// Convert from RGBA to BGRA.
for pixel in data.chunks_exact_mut(4) {
pixel.swap(0, 2);
}
let height = data.height();
let width = data.width();

View File

@@ -31,6 +31,7 @@ pub enum ComponentStory {
OverflowScroll,
Picker,
Scroll,
Setting,
Tab,
TabBar,
Text,
@@ -64,6 +65,7 @@ impl ComponentStory {
Self::ListItem => cx.new_view(|_| ui::ListItemStory).into(),
Self::OverflowScroll => cx.new_view(|_| crate::stories::OverflowScrollStory).into(),
Self::Scroll => ScrollStory::view(cx).into(),
Self::Setting => cx.new_view(|cx| ui::SettingStory::init(cx)).into(),
Self::Text => TextStory::view(cx).into(),
Self::Tab => cx.new_view(|_| ui::TabStory).into(),
Self::TabBar => cx.new_view(|_| ui::TabBarStory).into(),

View File

@@ -5,6 +5,7 @@ use std::{fmt::Display, sync::Arc, time::Duration};
#[derive(Serialize, Deserialize, Debug)]
pub struct EventRequestBody {
pub installation_id: Option<String>,
pub metrics_id: Option<String>,
pub session_id: Option<String>,
pub is_staff: Option<bool>,
pub app_version: String,

View File

@@ -1,7 +1,9 @@
use gpui::{ClickEvent, WindowContext};
use gpui::{ClickEvent, CursorStyle, WindowContext};
/// A trait for elements that can be clicked. Enables the use of the `on_click` method.
pub trait Clickable {
/// Sets the click handler that will fire whenever the element is clicked.
fn on_click(self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self;
/// Sets the cursor style when hovering over the element.
fn cursor_style(self, cursor_style: CursorStyle) -> Self;
}

View File

@@ -4,6 +4,7 @@ mod checkbox;
mod context_menu;
mod disclosure;
mod divider;
mod dropdown_menu;
mod icon;
mod indicator;
mod keybinding;
@@ -14,6 +15,7 @@ mod popover;
mod popover_menu;
mod radio;
mod right_click_menu;
mod setting;
mod stack;
mod tab;
mod tab_bar;
@@ -30,6 +32,7 @@ pub use checkbox::*;
pub use context_menu::*;
pub use disclosure::*;
pub use divider::*;
use dropdown_menu::*;
pub use icon::*;
pub use indicator::*;
pub use keybinding::*;
@@ -40,6 +43,7 @@ pub use popover::*;
pub use popover_menu::*;
pub use radio::*;
pub use right_click_menu::*;
pub use setting::*;
pub use stack::*;
pub use tab::*;
pub use tab_bar::*;

View File

@@ -249,6 +249,11 @@ impl Clickable for Button {
self.base = self.base.on_click(handler);
self
}
fn cursor_style(mut self, cursor_style: gpui::CursorStyle) -> Self {
self.base = self.base.cursor_style(cursor_style);
self
}
}
impl FixedWidth for Button {

View File

@@ -1,4 +1,4 @@
use gpui::{relative, DefiniteLength, MouseButton};
use gpui::{relative, CursorStyle, DefiniteLength, MouseButton};
use gpui::{transparent_black, AnyElement, AnyView, ClickEvent, Hsla, Rems};
use smallvec::SmallVec;
@@ -344,6 +344,7 @@ pub struct ButtonLike {
size: ButtonSize,
rounding: Option<ButtonLikeRounding>,
tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
cursor_style: CursorStyle,
on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
children: SmallVec<[AnyElement; 2]>,
}
@@ -363,6 +364,7 @@ impl ButtonLike {
rounding: Some(ButtonLikeRounding::All),
tooltip: None,
children: SmallVec::new(),
cursor_style: CursorStyle::PointingHand,
on_click: None,
layer: None,
}
@@ -405,6 +407,11 @@ impl Clickable for ButtonLike {
self.on_click = Some(Box::new(handler));
self
}
fn cursor_style(mut self, cursor_style: CursorStyle) -> Self {
self.cursor_style = cursor_style;
self
}
}
impl FixedWidth for ButtonLike {

View File

@@ -86,6 +86,11 @@ impl Clickable for IconButton {
self.base = self.base.on_click(handler);
self
}
fn cursor_style(mut self, cursor_style: gpui::CursorStyle) -> Self {
self.base = self.base.cursor_style(cursor_style);
self
}
}
impl FixedWidth for IconButton {

View File

@@ -82,6 +82,11 @@ impl Clickable for ToggleButton {
self.base = self.base.on_click(handler);
self
}
fn cursor_style(mut self, cursor_style: gpui::CursorStyle) -> Self {
self.base = self.base.cursor_style(cursor_style);
self
}
}
impl ButtonCommon for ToggleButton {

View File

@@ -1,6 +1,6 @@
use std::sync::Arc;
use gpui::ClickEvent;
use gpui::{ClickEvent, CursorStyle};
use crate::{prelude::*, Color, IconButton, IconButtonShape, IconName, IconSize};
@@ -10,6 +10,7 @@ pub struct Disclosure {
is_open: bool,
selected: bool,
on_toggle: Option<Arc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
cursor_style: CursorStyle,
}
impl Disclosure {
@@ -19,6 +20,7 @@ impl Disclosure {
is_open,
selected: false,
on_toggle: None,
cursor_style: CursorStyle::PointingHand,
}
}
@@ -43,6 +45,11 @@ impl Clickable for Disclosure {
self.on_toggle = Some(Arc::new(handler));
self
}
fn cursor_style(mut self, cursor_style: gpui::CursorStyle) -> Self {
self.cursor_style = cursor_style;
self
}
}
impl RenderOnce for Disclosure {

View File

@@ -0,0 +1,85 @@
use crate::prelude::*;
/// !!don't use this yet it's not functional!!
///
/// pub crate until this is functional
///
/// just a placeholder for now for filling out the settings menu stories.
#[derive(Debug, Clone, IntoElement)]
pub(crate) struct DropdownMenu {
pub id: ElementId,
current_item: Option<SharedString>,
// items: Vec<SharedString>,
full_width: bool,
disabled: bool,
}
impl DropdownMenu {
pub fn new(id: impl Into<ElementId>, _cx: &WindowContext) -> Self {
Self {
id: id.into(),
current_item: None,
// items: Vec::new(),
full_width: false,
disabled: false,
}
}
pub fn current_item(mut self, current_item: Option<SharedString>) -> Self {
self.current_item = current_item;
self
}
pub fn full_width(mut self, full_width: bool) -> Self {
self.full_width = full_width;
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
}
impl RenderOnce for DropdownMenu {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let disabled = self.disabled;
h_flex()
.id(self.id)
.justify_between()
.rounded_md()
.bg(cx.theme().colors().editor_background)
.pl_2()
.pr_1p5()
.py_0p5()
.gap_2()
.min_w_20()
.when_else(
self.full_width,
|full_width| full_width.w_full(),
|auto_width| auto_width.flex_none().w_auto(),
)
.when_else(
disabled,
|disabled| disabled.cursor_not_allowed(),
|enabled| enabled.cursor_pointer(),
)
.child(
Label::new(self.current_item.unwrap_or("".into())).color(if disabled {
Color::Disabled
} else {
Color::Default
}),
)
.child(
Icon::new(IconName::ChevronUpDown)
.size(IconSize::XSmall)
.color(if disabled {
Color::Disabled
} else {
Color::Muted
}),
)
}
}

View File

@@ -97,12 +97,16 @@ pub enum IconName {
BellOff,
BellRing,
Bolt,
Book,
BookCopy,
BookPlus,
CaseSensitive,
Check,
ChevronDown,
ChevronLeft,
ChevronRight,
ChevronUp,
ChevronUpDown,
Close,
Code,
Collab,
@@ -138,6 +142,9 @@ pub enum IconName {
Folder,
FolderOpen,
FolderX,
Font,
FontSize,
FontWeight,
Github,
Hash,
HistoryRerun,
@@ -145,6 +152,7 @@ pub enum IconName {
IndicatorX,
InlayHint,
Library,
LineHeight,
Link,
ListTree,
MagicWand,
@@ -178,8 +186,8 @@ pub enum IconName {
RotateCw,
Save,
Screen,
SelectAll,
SearchSelection,
SelectAll,
Server,
Settings,
Shift,
@@ -209,6 +217,7 @@ pub enum IconName {
ZedAssistant,
ZedAssistantFilled,
ZedXCopilot,
Visible,
}
impl IconName {
@@ -221,6 +230,7 @@ impl IconName {
IconName::ArrowLeft => "icons/arrow_left.svg",
IconName::ArrowRight => "icons/arrow_right.svg",
IconName::ArrowUp => "icons/arrow_up.svg",
IconName::ArrowUpFromLine => "icons/arrow_up_from_line.svg",
IconName::ArrowUpRight => "icons/arrow_up_right.svg",
IconName::AtSign => "icons/at_sign.svg",
IconName::AudioOff => "icons/speaker_off.svg",
@@ -231,12 +241,16 @@ impl IconName {
IconName::BellOff => "icons/bell_off.svg",
IconName::BellRing => "icons/bell_ring.svg",
IconName::Bolt => "icons/bolt.svg",
IconName::Book => "icons/book.svg",
IconName::BookCopy => "icons/book_copy.svg",
IconName::BookPlus => "icons/book_plus.svg",
IconName::CaseSensitive => "icons/case_insensitive.svg",
IconName::Check => "icons/check.svg",
IconName::ChevronDown => "icons/chevron_down.svg",
IconName::ChevronLeft => "icons/chevron_left.svg",
IconName::ChevronRight => "icons/chevron_right.svg",
IconName::ChevronUp => "icons/chevron_up.svg",
IconName::ChevronUpDown => "icons/chevron_up_down.svg",
IconName::Close => "icons/x.svg",
IconName::Code => "icons/code.svg",
IconName::Collab => "icons/user_group_16.svg",
@@ -272,6 +286,9 @@ impl IconName {
IconName::Folder => "icons/file_icons/folder.svg",
IconName::FolderOpen => "icons/file_icons/folder_open.svg",
IconName::FolderX => "icons/stop_sharing.svg",
IconName::Font => "icons/font.svg",
IconName::FontSize => "icons/font_size.svg",
IconName::FontWeight => "icons/font_weight.svg",
IconName::Github => "icons/github.svg",
IconName::Hash => "icons/hash.svg",
IconName::HistoryRerun => "icons/history_rerun.svg",
@@ -279,6 +296,7 @@ impl IconName {
IconName::IndicatorX => "icons/indicator_x.svg",
IconName::InlayHint => "icons/inlay_hint.svg",
IconName::Library => "icons/library.svg",
IconName::LineHeight => "icons/line_height.svg",
IconName::Link => "icons/link.svg",
IconName::ListTree => "icons/list_tree.svg",
IconName::MagicWand => "icons/magic_wand.svg",
@@ -302,18 +320,18 @@ impl IconName {
IconName::Quote => "icons/quote.svg",
IconName::Regex => "icons/regex.svg",
IconName::Replace => "icons/replace.svg",
IconName::Reveal => "icons/reveal.svg",
IconName::ReplaceAll => "icons/replace_all.svg",
IconName::ReplaceNext => "icons/replace_next.svg",
IconName::ReplyArrowRight => "icons/reply_arrow_right.svg",
IconName::Rerun => "icons/rerun.svg",
IconName::Return => "icons/return.svg",
IconName::RotateCw => "icons/rotate_cw.svg",
IconName::Reveal => "icons/reveal.svg",
IconName::RotateCcw => "icons/rotate_ccw.svg",
IconName::RotateCw => "icons/rotate_cw.svg",
IconName::Save => "icons/save.svg",
IconName::Screen => "icons/desktop.svg",
IconName::SelectAll => "icons/select_all.svg",
IconName::SearchSelection => "icons/search_selection.svg",
IconName::SelectAll => "icons/select_all.svg",
IconName::Server => "icons/server.svg",
IconName::Settings => "icons/file_icons/settings.svg",
IconName::Shift => "icons/shift.svg",
@@ -343,7 +361,7 @@ impl IconName {
IconName::ZedAssistant => "icons/zed_assistant.svg",
IconName::ZedAssistantFilled => "icons/zed_assistant_filled.svg",
IconName::ZedXCopilot => "icons/zed_x_copilot.svg",
IconName::ArrowUpFromLine => "icons/arrow_up_from_line.svg",
IconName::Visible => "icons/visible.svg",
}
}
}

View File

@@ -30,7 +30,7 @@ impl KeyBinding {
Some(Self::new(key_binding))
}
fn icon_for_key(keystroke: &Keystroke) -> Option<IconName> {
fn icon_for_key(&self, keystroke: &Keystroke) -> Option<IconName> {
match keystroke.key.as_str() {
"left" => Some(IconName::ArrowLeft),
"right" => Some(IconName::ArrowRight),
@@ -45,6 +45,11 @@ impl KeyBinding {
"escape" => Some(IconName::Escape),
"pagedown" => Some(IconName::PageDown),
"pageup" => Some(IconName::PageUp),
"shift" if self.platform_style == PlatformStyle::Mac => Some(IconName::Shift),
"control" if self.platform_style == PlatformStyle::Mac => Some(IconName::Control),
"platform" if self.platform_style == PlatformStyle::Mac => Some(IconName::Command),
"function" if self.platform_style == PlatformStyle::Mac => Some(IconName::Control),
"alt" if self.platform_style == PlatformStyle::Mac => Some(IconName::Option),
_ => None,
}
}
@@ -80,7 +85,7 @@ impl RenderOnce for KeyBinding {
.gap(Spacing::Small.rems(cx))
.flex_none()
.children(self.key_binding.keystrokes().iter().map(|keystroke| {
let key_icon = Self::icon_for_key(keystroke);
let key_icon = self.icon_for_key(keystroke);
h_flex()
.flex_none()

View File

@@ -0,0 +1,351 @@
use crate::{prelude::*, Checkbox, ListHeader};
use super::DropdownMenu;
#[derive(PartialEq, Clone, Eq, Debug)]
pub enum ToggleType {
Checkbox,
// Switch,
}
impl From<ToggleType> for SettingType {
fn from(toggle_type: ToggleType) -> Self {
SettingType::Toggle(toggle_type)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InputType {
Text,
Number,
}
impl From<InputType> for SettingType {
fn from(input_type: InputType) -> Self {
SettingType::Input(input_type)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SecondarySettingType {
Dropdown,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SettingType {
Toggle(ToggleType),
ToggleAnd(SecondarySettingType),
Input(InputType),
Dropdown,
Range,
Unsupported,
}
#[derive(Debug, Clone, IntoElement)]
pub struct SettingsGroup {
pub name: String,
settings: Vec<SettingsItem>,
}
impl SettingsGroup {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
settings: Vec::new(),
}
}
pub fn add_setting(mut self, setting: SettingsItem) -> Self {
self.settings.push(setting);
self
}
}
impl RenderOnce for SettingsGroup {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
let empty_message = format!("No settings available for {}", self.name);
let header = ListHeader::new(self.name);
let settings = self.settings.clone().into_iter();
v_flex()
.p_1()
.gap_2()
.child(header)
.when(self.settings.len() == 0, |this| {
this.child(Label::new(empty_message))
})
.children(settings)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum SettingLayout {
Stacked,
AutoWidth,
FullLine,
FullLineJustified,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SettingId(pub SharedString);
impl From<SettingId> for ElementId {
fn from(id: SettingId) -> Self {
ElementId::Name(id.0)
}
}
impl From<&str> for SettingId {
fn from(id: &str) -> Self {
Self(id.to_string().into())
}
}
impl From<SharedString> for SettingId {
fn from(id: SharedString) -> Self {
Self(id)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SettingValue(pub SharedString);
impl From<SharedString> for SettingValue {
fn from(value: SharedString) -> Self {
Self(value)
}
}
impl From<String> for SettingValue {
fn from(value: String) -> Self {
Self(value.into())
}
}
impl From<bool> for SettingValue {
fn from(value: bool) -> Self {
Self(value.to_string().into())
}
}
impl From<SettingValue> for bool {
fn from(value: SettingValue) -> Self {
value.0 == "true"
}
}
#[derive(Debug, Clone, IntoElement)]
pub struct SettingsItem {
pub id: SettingId,
current_value: Option<SettingValue>,
disabled: bool,
hide_label: bool,
icon: Option<IconName>,
layout: SettingLayout,
name: SharedString,
// possible_values: Option<Vec<SettingValue>>,
setting_type: SettingType,
toggled: Option<bool>,
}
impl SettingsItem {
pub fn new(
id: impl Into<SettingId>,
name: SharedString,
setting_type: SettingType,
current_value: Option<SettingValue>,
) -> Self {
let toggled = match setting_type {
SettingType::Toggle(_) | SettingType::ToggleAnd(_) => Some(false),
_ => None,
};
Self {
id: id.into(),
current_value,
disabled: false,
hide_label: false,
icon: None,
layout: SettingLayout::FullLine,
name,
// possible_values: None,
setting_type,
toggled,
}
}
pub fn layout(mut self, layout: SettingLayout) -> Self {
self.layout = layout;
self
}
pub fn toggled(mut self, toggled: bool) -> Self {
self.toggled = Some(toggled);
self
}
// pub fn hide_label(mut self, hide_label: bool) -> Self {
// self.hide_label = hide_label;
// self
// }
pub fn icon(mut self, icon: IconName) -> Self {
self.icon = Some(icon);
self
}
// pub fn disabled(mut self, disabled: bool) -> Self {
// self.disabled = disabled;
// self
// }
}
impl RenderOnce for SettingsItem {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let id: ElementId = self.id.clone().into();
// When the setting is disabled or toggled off, we don't want any secondary elements to be interactable
let secondary_element_disabled = self.disabled || self.toggled == Some(false);
let full_width = match self.layout {
SettingLayout::FullLine | SettingLayout::FullLineJustified => true,
_ => false,
};
let hide_label = self.hide_label || self.icon.is_some();
let justified = match (self.layout.clone(), self.setting_type.clone()) {
(_, SettingType::ToggleAnd(_)) => true,
(SettingLayout::FullLineJustified, _) => true,
_ => false,
};
let (setting_type, current_value) = (self.setting_type.clone(), self.current_value.clone());
let current_string = if let Some(current_value) = current_value.clone() {
Some(current_value.0)
} else {
None
};
let toggleable = match setting_type {
SettingType::Toggle(_) => true,
SettingType::ToggleAnd(_) => true,
_ => false,
};
let setting_element = match setting_type {
SettingType::Toggle(_) => None,
SettingType::ToggleAnd(secondary_setting_type) => match secondary_setting_type {
SecondarySettingType::Dropdown => Some(
DropdownMenu::new(id.clone(), &cx)
.current_item(current_string)
.disabled(secondary_element_disabled)
.into_any_element(),
),
},
SettingType::Input(input_type) => match input_type {
InputType::Text => Some(div().child("text").into_any_element()),
InputType::Number => Some(div().child("number").into_any_element()),
},
SettingType::Dropdown => Some(
DropdownMenu::new(id.clone(), &cx)
.current_item(current_string)
.full_width(true)
.into_any_element(),
),
SettingType::Range => Some(div().child("range").into_any_element()),
SettingType::Unsupported => None,
};
let checkbox = Checkbox::new(
ElementId::Name(format!("toggle-{}", self.id.0).to_string().into()),
self.toggled.into(),
)
.disabled(self.disabled);
let toggle_element = match (toggleable, self.setting_type.clone()) {
(true, SettingType::Toggle(toggle_type)) => match toggle_type {
ToggleType::Checkbox => Some(checkbox.into_any_element()),
},
(true, SettingType::ToggleAnd(_)) => Some(checkbox.into_any_element()),
(_, _) => None,
};
let item = if self.layout == SettingLayout::Stacked {
v_flex()
} else {
h_flex()
};
item.id(id)
.gap_2()
.w_full()
.when_some(self.icon, |this, icon| {
this.child(div().px_0p5().child(Icon::new(icon).color(Color::Muted)))
})
.children(toggle_element)
.children(if hide_label {
None
} else {
Some(Label::new(self.name.clone()))
})
.when(justified, |this| this.child(div().flex_1().size_full()))
.child(
h_flex()
.when(full_width, |this| this.w_full())
.when(self.layout == SettingLayout::FullLineJustified, |this| {
this.justify_end()
})
.children(setting_element),
)
// help flex along when full width is disabled
//
// this probably isn't needed, but fighting with flex to
// get this right without inspection tools will be a pain
.when(!full_width, |this| this.child(div().size_full().flex_1()))
}
}
pub struct SettingsMenu {
name: SharedString,
groups: Vec<SettingsGroup>,
}
impl SettingsMenu {
pub fn new(name: impl Into<SharedString>) -> Self {
Self {
name: name.into(),
groups: Vec::new(),
}
}
pub fn add_group(mut self, group: SettingsGroup) -> Self {
self.groups.push(group);
self
}
}
impl Render for SettingsMenu {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let is_empty = self.groups.is_empty();
v_flex()
.id(ElementId::Name(self.name.clone()))
.elevation_2(cx)
.min_w_56()
.max_w_96()
.max_h_2_3()
.px_2()
.when_else(
is_empty,
|empty| empty.py_1(),
|not_empty| not_empty.pt_0().pb_1(),
)
.gap_1()
.when(is_empty, |this| {
this.child(Label::new("No settings found").color(Color::Muted))
})
.children(self.groups.clone())
}
}

View File

@@ -10,6 +10,7 @@ mod label;
mod list;
mod list_header;
mod list_item;
mod setting;
mod tab;
mod tab_bar;
mod title_bar;
@@ -28,6 +29,7 @@ pub use label::*;
pub use list::*;
pub use list_header::*;
pub use list_item::*;
pub use setting::*;
pub use tab::*;
pub use tab_bar::*;
pub use title_bar::*;

View File

@@ -0,0 +1,225 @@
use gpui::View;
use crate::prelude::*;
use crate::{
SecondarySettingType, SettingLayout, SettingType, SettingsGroup, SettingsItem, SettingsMenu,
ToggleType,
};
pub struct SettingStory {
menus: Vec<(SharedString, View<SettingsMenu>)>,
}
impl SettingStory {
pub fn new() -> Self {
Self { menus: Vec::new() }
}
pub fn init(cx: &mut ViewContext<Self>) -> Self {
let mut story = Self::new();
story.empty_menu(cx);
story.editor_example(cx);
story.menu_single_group(cx);
story
}
}
impl SettingStory {
pub fn empty_menu(&mut self, cx: &mut ViewContext<Self>) {
let menu = cx.new_view(|_cx| SettingsMenu::new("Empty Menu"));
self.menus.push(("Empty Menu".into(), menu));
}
pub fn menu_single_group(&mut self, cx: &mut ViewContext<Self>) {
let theme_setting = SettingsItem::new(
"theme-setting",
"Theme".into(),
SettingType::Dropdown,
Some(cx.theme().name.clone().into()),
)
.layout(SettingLayout::Stacked);
let high_contrast_setting = SettingsItem::new(
"theme-contrast",
"Use high contrast theme".into(),
SettingType::Toggle(ToggleType::Checkbox),
None,
)
.toggled(false);
let appearance_setting = SettingsItem::new(
"switch-appearance",
"Match system appearance".into(),
SettingType::ToggleAnd(SecondarySettingType::Dropdown),
Some("When Dark".to_string().into()),
)
.layout(SettingLayout::FullLineJustified);
let group = SettingsGroup::new("Appearance")
.add_setting(theme_setting)
.add_setting(appearance_setting)
.add_setting(high_contrast_setting);
let menu = cx.new_view(|_cx| SettingsMenu::new("Appearance").add_group(group));
self.menus.push(("Single Group".into(), menu));
}
pub fn editor_example(&mut self, cx: &mut ViewContext<Self>) {
let font_group = SettingsGroup::new("Font")
.add_setting(
SettingsItem::new(
"font-family",
"Font".into(),
SettingType::Dropdown,
Some("Berkeley Mono".to_string().into()),
)
.icon(IconName::Font)
.layout(SettingLayout::AutoWidth),
)
.add_setting(
SettingsItem::new(
"font-weifht",
"Font Weight".into(),
SettingType::Dropdown,
Some("400".to_string().into()),
)
.icon(IconName::FontWeight)
.layout(SettingLayout::AutoWidth),
)
.add_setting(
SettingsItem::new(
"font-size",
"Font Size".into(),
SettingType::Dropdown,
Some("14".to_string().into()),
)
.icon(IconName::FontSize)
.layout(SettingLayout::AutoWidth),
)
.add_setting(
SettingsItem::new(
"line-height",
"Line Height".into(),
SettingType::Dropdown,
Some("1.35".to_string().into()),
)
.icon(IconName::LineHeight)
.layout(SettingLayout::AutoWidth),
)
.add_setting(
SettingsItem::new(
"enable-ligatures",
"Enable Ligatures".into(),
SettingType::Toggle(ToggleType::Checkbox),
None,
)
.toggled(true),
);
let editor_group = SettingsGroup::new("Editor")
.add_setting(
SettingsItem::new(
"show-indent-guides",
"Indent Guides".into(),
SettingType::Toggle(ToggleType::Checkbox),
None,
)
.toggled(true),
)
.add_setting(
SettingsItem::new(
"show-git-blame",
"Git Blame".into(),
SettingType::Toggle(ToggleType::Checkbox),
None,
)
.toggled(false),
);
let gutter_group = SettingsGroup::new("Gutter")
.add_setting(
SettingsItem::new(
"enable-git-hunks",
"Show Git Hunks".into(),
SettingType::Toggle(ToggleType::Checkbox),
None,
)
.toggled(true),
)
.add_setting(
SettingsItem::new(
"show-line-numbers",
"Line Numbers".into(),
SettingType::ToggleAnd(SecondarySettingType::Dropdown),
Some("Ascending".to_string().into()),
)
.toggled(true)
.layout(SettingLayout::FullLineJustified),
);
let scrollbar_group = SettingsGroup::new("Scrollbar")
.add_setting(
SettingsItem::new(
"scrollbar-visibility",
"Show scrollbar when:".into(),
SettingType::Dropdown,
Some("Always Visible".to_string().into()),
)
.layout(SettingLayout::AutoWidth)
.icon(IconName::Visible),
)
.add_setting(
SettingsItem::new(
"show-diagnostic-markers",
"Diagnostic Markers".into(),
SettingType::Toggle(ToggleType::Checkbox),
None,
)
.toggled(true),
)
.add_setting(
SettingsItem::new(
"show-git-markers",
"Git Status Markers".into(),
SettingType::Toggle(ToggleType::Checkbox),
None,
)
.toggled(false),
)
.add_setting(
SettingsItem::new(
"show-selection-markers",
"Selection & Match Markers".into(),
SettingType::Toggle(ToggleType::Checkbox),
None,
)
.toggled(true),
);
let menu = cx.new_view(|_cx| {
SettingsMenu::new("Editor")
.add_group(font_group)
.add_group(editor_group)
.add_group(gutter_group)
.add_group(scrollbar_group)
});
self.menus.push(("Editor Example".into(), menu));
}
}
impl Render for SettingStory {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.bg(cx.theme().colors().background)
.text_color(cx.theme().colors().text)
.children(self.menus.iter().map(|(name, menu)| {
v_flex()
.p_2()
.gap_2()
.child(Headline::new(name.clone()).size(HeadlineSize::Medium))
.child(menu.clone())
}))
}
}

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