Compare commits

..

20 Commits

Author SHA1 Message Date
Conrad Irwin
408fe83b90 linux:Add zed:// url support 2024-07-10 14:45:41 -06:00
Nathan Sobo
77b31d1845 Allow rpc_url to be assigned on Client with test-support feature (#13430)
Also, allow proto messages to be deserialized. This is to support
translating these messages JS types in a new server implementation based
on CloudFlare durable objects.

Release Notes:

- N/A
2024-07-10 13:36:22 -06:00
Ephram
15b8790a2c Update One Light modified color (#12143)
Release Notes:

- Changed the `modified` color on the one-light color theme to a more
readable value

The current color in the file tree for modified files is basically
unreadable in the default light mode
<img width="238" alt="image"
src="https://github.com/zed-industries/zed/assets/50590465/e553673f-1c24-41d9-b1b9-1dbbb7419d1e">

This change just changes the color to match that of the one-light theme
present in vscode
~~old proposal:
https://github.com/zed-industries/zed/assets/50590465/b4dc4030-bcd8-429a-84b8-2744e213e492~~
new proposal:
<img width="378" alt="image"
src="https://github.com/zed-industries/zed/assets/50590465/41c53019-8cf3-4927-9879-47937388cde8">

This does have a side-effect of changing the modified color on the side
in the editor, but personally I think this change is negligable.
2024-07-10 15:33:35 -04:00
Jason Lee
b693cbfcb7 Fix line wrap for CJK characters (#11296)
Release Notes:

- Fixed line wrap for CJK characters. 

## Demo


https://github.com/zed-industries/zed/assets/5518/c6695bb4-b170-4ce0-9a84-c36b051de438


![diff](https://github.com/zed-industries/zed/assets/5518/318bc815-1018-485c-aa16-49c775a9f402)

Fix issues: #4623 #11202

### Render case

```
## fr

Bien démarrer avec la documentation GitHub Découvrez comment commencer à créer, à livrer et à gérer des logiciels avec GitHub. Explorez nos produits, inscrivez-vous pour obtenir un compte et connectez-vous à la plus grande communauté de développement du monde.

## zh

GitHub 入门文档 了解如何开始构建、运输和维护具有 GitHub 的软件。 了解我们的产品,注册一个帐户,与世界上最大的发展社区建立联系。

## es

Documentación sobre la introducción a GitHub Aprende cómo comenzar a crear, enviar y mantener software con GitHub. Explora nuestros productos, regístrate para una cuenta y conéctate con la comunidad de desarrollo más grande del mundo.

## kr

GitHub 설명서 시작 GitHub를 사용하여 소프트웨어 빌드, 납품 및 유지 관리를 시작하는 방법을 알아봅니다. 제품을 탐색하고, 계정에 등록하고, 세계 최대의 개발 커뮤니티와 연결합니다.

## ja

GitHub の概要に関するドキュメント GitHub を使用してソフトウェアの構築、出荷、および保守を始める方法を学びます。 当社の製品を探索し、アカウントにサインアップして、世界最大の開発コミュニティと繋がりましょう。

## pt

Documentação de introdução ao GitHub Aprenda a começar a criar, enviar e manter um software com a GitHub. Explore nossos produtos, inscreva-se em uma conta e conecte-se com a maior comunidade de desenvolvimento do mundo.

## ru

Начало работы с документацией по GitHub Узнайте, как начать создание, доставку и обслуживание программного обеспечения с помощью GitHub. Изучите наши продукты, зарегистрируйте учетную запись и присоединитесь к крупнейшему в мире сообществу разработчиков.
```
2024-07-10 13:10:19 -06:00
Aidan Harris
73d7f70ff6 Flatpak fixes (#14083)
The Flatpak was failing to build because of AppStream metadata linting
errors. It also complained about the hyphen in the cid.

Release Notes:
 
     * N/A
2024-07-10 12:51:08 -06:00
Conrad Irwin
be5b7b2e70 Reduce the need to read the shell script to figure out what's going on (#14077)
Release Notes:

- N/A
2024-07-10 12:08:54 -06:00
Tim Whitbeck
4434353f73 docs: Correct ln command in linux install steps (#14078)
Release Notes:

- N/A
2024-07-10 12:08:42 -06:00
Nate Butler
95637a0320 Minor breadcrumb style updates (#14070)
Minor breadcrumb style updates

Before:

![CleanShot 2024-07-10 at 12 42
46@2x](https://github.com/zed-industries/zed/assets/1714999/9e1d4fb7-7549-4749-85f8-797f59d06c4d)

After:

![CleanShot 2024-07-10 at 12 42
36@2x](https://github.com/zed-industries/zed/assets/1714999/f448eb0a-deac-4a8e-b26e-67d559c4c679)


Release Notes:

- N/A
2024-07-10 14:04:17 -04:00
Thorsten Ball
ee623f77c1 linux/x11: Restore differentiation of mouse/keyboard focus (#13995)
This restores https://github.com/zed-industries/zed/pull/13943 which was
reverted in #13974 because it was possible to get in a state where focus
could not be restored on a window.

In this PR there's an additional change: `FocusIn` and `FocusOut` events
are always handled, even if the `event.mode` is not "NORMAL". In my
testing, `alt-tabbing` between windows didn't produce `FocusIn` and
`FocusOut` events when we had that check. Now, with the check removed,
it's possible to switch focus between two windows again with `alt-tab`.


Release Notes:

- N/A

---------

Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-07-10 19:54:26 +02:00
Conrad Irwin
c732865fc5 Build x86 linux too :/ (#14068)
Release Notes:

- N/A
2024-07-10 11:04:32 -06:00
sgj123456
8a659af82c gpui_macros: Enable extra-traits feature for syn (#14067)
Must enable extra-traits of syn feature to enable Debug trait of
Visibility

Release Notes:

- N/A

---------

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

Release Notes:

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

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

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

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


Release Notes:

- N/A

---------

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

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

/cc @maxdeviant , le clippy guru.

Release Notes:

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

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

## Before


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


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


## After

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


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


---

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

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

---

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

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

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

Display 1:

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


---

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

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

Display 2:

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

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

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


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

Release Notes:

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

Release Notes:

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

Release Notes:

- vim: `ctrl-m` now is equivalent to `enter` in editor.
2024-07-10 16:31:54 +02:00
Joseph T Lyons
f147722fe0 v0.145.x dev 2024-07-10 10:12:35 -04:00
95 changed files with 5492 additions and 3105 deletions

204
Cargo.lock generated
View File

@@ -87,7 +87,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6d1ea4484c8676f295307a4892d478c70ac8da1dbd8c7c10830a504b7f1022f"
dependencies = [
"base64 0.22.0",
"bitflags 2.6.0",
"bitflags 2.4.2",
"home",
"libc",
"log",
@@ -110,7 +110,7 @@ version = "0.24.1-dev"
source = "git+https://github.com/alacritty/alacritty?rev=cacdb5bb3b72bad2c729227537979d95af75978f#cacdb5bb3b72bad2c729227537979d95af75978f"
dependencies = [
"base64 0.22.0",
"bitflags 2.6.0",
"bitflags 2.4.2",
"home",
"libc",
"log",
@@ -341,9 +341,8 @@ dependencies = [
[[package]]
name = "ashpd"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfe7e0dd0ac5a401dc116ed9f9119cf9decc625600474cb41f0fc0a0050abc9a"
version = "0.9.0"
source = "git+https://github.com/bilelmoussaoui/ashpd?rev=29f2e1a#29f2e1a6f4b0911f504658f5f4630c02e01b13f2"
dependencies = [
"async-fs 2.1.1",
"async-net 2.0.0",
@@ -378,6 +377,7 @@ dependencies = [
"cargo_toml",
"chrono",
"client",
"clock",
"collections",
"command_palette_hooks",
"ctor",
@@ -420,6 +420,7 @@ dependencies = [
"telemetry_events",
"terminal",
"terminal_view",
"text",
"theme",
"tiktoken-rs",
"toml 0.8.10",
@@ -1582,16 +1583,7 @@ version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
dependencies = [
"bit-vec 0.6.3",
]
[[package]]
name = "bit-set"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0481a0e032742109b1133a095184ee93d88f3dc9e0d28a5d033dc77a073f44f"
dependencies = [
"bit-vec 0.7.0",
"bit-vec",
]
[[package]]
@@ -1600,12 +1592,6 @@ version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]]
name = "bit-vec"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2c54ff287cfc0a34f38a6b832ea1bd8e448a330b3e40a50859e6488bee07f22"
[[package]]
name = "bit_field"
version = "0.10.2"
@@ -1620,9 +1606,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.6.0"
version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
dependencies = [
"serde",
]
@@ -1648,11 +1634,11 @@ dependencies = [
[[package]]
name = "blade-graphics"
version = "0.4.0"
source = "git+https://github.com/zed-industries/blade?rev=a477c2008db27db0b9f745715e119b3ee7ab7818#a477c2008db27db0b9f745715e119b3ee7ab7818"
source = "git+https://github.com/kvark/blade?rev=21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7#21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7"
dependencies = [
"ash",
"ash-window",
"bitflags 2.6.0",
"bitflags 2.4.2",
"block",
"bytemuck",
"codespan-reporting",
@@ -1678,7 +1664,7 @@ dependencies = [
[[package]]
name = "blade-macros"
version = "0.2.1"
source = "git+https://github.com/zed-industries/blade?rev=a477c2008db27db0b9f745715e119b3ee7ab7818#a477c2008db27db0b9f745715e119b3ee7ab7818"
source = "git+https://github.com/kvark/blade?rev=21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7#21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7"
dependencies = [
"proc-macro2",
"quote",
@@ -1688,7 +1674,7 @@ dependencies = [
[[package]]
name = "blade-util"
version = "0.1.0"
source = "git+https://github.com/zed-industries/blade?rev=a477c2008db27db0b9f745715e119b3ee7ab7818#a477c2008db27db0b9f745715e119b3ee7ab7818"
source = "git+https://github.com/kvark/blade?rev=21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7#21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7"
dependencies = [
"blade-graphics",
"bytemuck",
@@ -1936,7 +1922,7 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.4.2",
"log",
"polling 3.3.2",
"rustix 0.38.32",
@@ -2421,6 +2407,7 @@ version = "0.1.0"
dependencies = [
"chrono",
"parking_lot",
"serde",
"smallvec",
]
@@ -2479,6 +2466,7 @@ version = "0.44.0"
dependencies = [
"anthropic",
"anyhow",
"assistant",
"async-trait",
"async-tungstenite",
"audio",
@@ -2871,7 +2859,7 @@ name = "cosmic-text"
version = "0.11.2"
source = "git+https://github.com/pop-os/cosmic-text?rev=542b20c#542b20ca4376a3b5de5fa629db1a4ace44e18e0c"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.4.2",
"fontdb",
"log",
"rangemap",
@@ -4024,7 +4012,7 @@ version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7493d4c459da9f84325ad297371a6b2b8a162800873a22e3b6b6512e61d18c05"
dependencies = [
"bit-set 0.5.3",
"bit-set",
"regex",
]
@@ -4081,7 +4069,7 @@ name = "feedback"
version = "0.1.0"
dependencies = [
"anyhow",
"bitflags 2.6.0",
"bitflags 2.4.2",
"client",
"db",
"editor",
@@ -4281,7 +4269,7 @@ checksum = "e32eac81c1135c1df01d4e6d4233c47ba11f6a6d07f33e0bba09d18797077770"
dependencies = [
"fontconfig-parser",
"log",
"memmap2",
"memmap2 0.9.4",
"slotmap",
"tinyvec",
"ttf-parser",
@@ -4415,7 +4403,7 @@ dependencies = [
name = "fsevent"
version = "0.1.0"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.4.2",
"core-foundation",
"fsevent-sys 3.1.0",
"parking_lot",
@@ -4726,7 +4714,7 @@ version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.4.2",
"libc",
"libgit2-sys",
"log",
@@ -4837,7 +4825,7 @@ version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.4.2",
"gpu-alloc-types",
]
@@ -4858,7 +4846,7 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.4.2",
]
[[package]]
@@ -4908,7 +4896,6 @@ dependencies = [
"num_cpus",
"objc",
"oo7",
"open",
"parking",
"parking_lot",
"pathfinder_geometry",
@@ -5116,7 +5103,7 @@ version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f7acb9683d7c7068aa46d47557bfa4e35a277964b350d9504a87b03610163fd"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.4.2",
"byteorder",
"heed-traits",
"heed-types",
@@ -5720,25 +5707,6 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6"
[[package]]
name = "is-docker"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
dependencies = [
"once_cell",
]
[[package]]
name = "is-wsl"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
dependencies = [
"is-docker",
"once_cell",
]
[[package]]
name = "isahc"
version = "1.7.2"
@@ -6556,6 +6524,15 @@ dependencies = [
"rustix 0.38.32",
]
[[package]]
name = "memmap2"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a5a03cefb0d953ec0be133036f14e109412fa594edc2f77227249db66cc3ed"
dependencies = [
"libc",
]
[[package]]
name = "memmap2"
version = "0.9.4"
@@ -6689,17 +6666,17 @@ checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a"
[[package]]
name = "naga"
version = "0.20.0"
source = "git+https://github.com/gfx-rs/wgpu?rev=425526828f738c95ec50b016c6a761bc00d2fb25#425526828f738c95ec50b016c6a761bc00d2fb25"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae585df4b6514cf8842ac0f1ab4992edc975892704835b549cf818dc0191249e"
dependencies = [
"arrayvec",
"bit-set 0.6.0",
"bitflags 2.6.0",
"cfg_aliases",
"bit-set",
"bitflags 2.4.2",
"codespan-reporting",
"hexf-parse",
"indexmap 2.2.6",
"log",
"num-traits",
"rustc-hash",
"spirv",
"termcolor",
@@ -6795,7 +6772,7 @@ version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.4.2",
"cfg-if",
"libc",
"memoffset",
@@ -6807,7 +6784,7 @@ version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.4.2",
"cfg-if",
"cfg_aliases",
"libc",
@@ -6876,7 +6853,7 @@ version = "6.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.4.2",
"crossbeam-channel",
"filetime",
"fsevent-sys 4.1.0",
@@ -7089,15 +7066,6 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "num_threads"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
dependencies = [
"libc",
]
[[package]]
name = "nvim-rs"
version = "0.6.0-pre"
@@ -7227,17 +7195,6 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "open"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61a877bf6abd716642a53ef1b89fb498923a4afca5c754f9050b4d081c05c4b3"
dependencies = [
"is-wsl",
"libc",
"pathdiff",
]
[[package]]
name = "open_ai"
version = "0.1.0"
@@ -7258,7 +7215,7 @@ version = "0.10.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.4.2",
"cfg-if",
"foreign-types 0.3.2",
"libc",
@@ -8272,7 +8229,7 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dce76ce678ffc8e5675b22aa1405de0b7037e2fdf8913fea40d1926c6fe1e6e7"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.4.2",
"memchr",
"unicase",
]
@@ -8317,6 +8274,7 @@ dependencies = [
"assistant",
"editor",
"gpui",
"repl",
"search",
"settings",
"ui",
@@ -9083,7 +9041,7 @@ version = "0.38.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.4.2",
"errno 0.3.8",
"itoa",
"libc",
@@ -9158,7 +9116,7 @@ version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.4.2",
"bytemuck",
"libm",
"smallvec",
@@ -9381,7 +9339,7 @@ version = "0.1.0"
dependencies = [
"any_vec",
"anyhow",
"bitflags 2.6.0",
"bitflags 2.4.2",
"client",
"collections",
"editor",
@@ -9809,13 +9767,13 @@ dependencies = [
[[package]]
name = "simplelog"
version = "0.12.2"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0"
checksum = "4bc0ffd69814a9b251d43afcabf96dad1b29f5028378056257be9e3fecc9f720"
dependencies = [
"chrono",
"log",
"termcolor",
"time",
]
[[package]]
@@ -9968,11 +9926,12 @@ dependencies = [
[[package]]
name = "spirv"
version = "0.3.0+sdk-1.3.268.0"
version = "0.2.0+1.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844"
checksum = "246bfa38fe3db3f1dfc8ca5a2cdeb7348c78be2112740cc0ec8ef18b6d94f830"
dependencies = [
"bitflags 2.6.0",
"bitflags 1.3.2",
"num-traits",
]
[[package]]
@@ -10148,7 +10107,7 @@ dependencies = [
"atoi",
"base64 0.21.7",
"bigdecimal",
"bitflags 2.6.0",
"bitflags 2.4.2",
"byteorder",
"bytes 1.5.0",
"chrono",
@@ -10195,7 +10154,7 @@ dependencies = [
"atoi",
"base64 0.21.7",
"bigdecimal",
"bitflags 2.6.0",
"bitflags 2.4.2",
"byteorder",
"chrono",
"crc",
@@ -10617,7 +10576,7 @@ version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aef1f9d4c1dbdd1cb3a63be9efd2f04d8ddbc919d46112982c76818ffc2f1a7"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.4.2",
"cap-fs-ext",
"cap-std",
"fd-lock",
@@ -10758,9 +10717,9 @@ dependencies = [
[[package]]
name = "termcolor"
version = "1.4.1"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
dependencies = [
"winapi-util",
]
@@ -10920,18 +10879,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.61"
version = "1.0.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.61"
version = "1.0.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524"
dependencies = [
"proc-macro2",
"quote",
@@ -10982,9 +10941,7 @@ checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
dependencies = [
"deranged",
"itoa",
"libc",
"num-conv",
"num_threads",
"powerfmt",
"serde",
"time-core",
@@ -11343,7 +11300,7 @@ version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.4.2",
"bytes 1.5.0",
"futures-core",
"futures-util",
@@ -12114,7 +12071,7 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40eb22ae96f050e0c0d6f7ce43feeae26c348fc4dea56928ca81537cfaa6188b"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.4.2",
"cursor-icon",
"log",
"serde",
@@ -12266,7 +12223,7 @@ version = "0.201.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84e5df6dba6c0d7fafc63a450f1738451ed7a0b52295d83e868218fa286bf708"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.4.2",
"indexmap 2.2.6",
"semver",
]
@@ -12533,7 +12490,7 @@ checksum = "371d828b6849ea06d598ae7dd1c316e8dd9e99b76f77d93d5886cb25c7f8e188"
dependencies = [
"anyhow",
"async-trait",
"bitflags 2.6.0",
"bitflags 2.4.2",
"bytes 1.5.0",
"cap-fs-ext",
"cap-net-ext",
@@ -12620,7 +12577,7 @@ version = "0.31.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82fb96ee935c2cea6668ccb470fb7771f6215d1691746c2d896b447a00ad3f1f"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.4.2",
"rustix 0.38.32",
"wayland-backend",
"wayland-scanner",
@@ -12643,7 +12600,7 @@ version = "0.31.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.4.2",
"wayland-backend",
"wayland-client",
"wayland-scanner",
@@ -12655,7 +12612,7 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.4.2",
"wayland-backend",
"wayland-client",
"wayland-protocols",
@@ -12774,7 +12731,7 @@ checksum = "ae1136a209614ace00b0c11f04dc7cf42540773be3b22eff6ad165110aba29c1"
dependencies = [
"anyhow",
"async-trait",
"bitflags 2.6.0",
"bitflags 2.4.2",
"thiserror",
"tracing",
"wasmtime",
@@ -13204,7 +13161,7 @@ version = "0.36.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9643b83820c0cd246ecabe5fa454dd04ba4fa67996369466d0747472d337346"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.4.2",
"windows-sys 0.52.0",
]
@@ -13223,7 +13180,7 @@ version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "288f992ea30e6b5c531b52cdd5f3be81c148554b09ea416f058d16556ba92c27"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.4.2",
"wit-bindgen-rt",
"wit-bindgen-rust-macro",
]
@@ -13279,7 +13236,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "421c0c848a0660a8c22e2fd217929a0191f14476b68962afd2af89fd22e39825"
dependencies = [
"anyhow",
"bitflags 2.6.0",
"bitflags 2.4.2",
"indexmap 2.2.6",
"log",
"serde",
@@ -13485,17 +13442,18 @@ name = "xim-parser"
version = "0.2.1"
source = "git+https://github.com/npmania/xim-rs?rev=27132caffc5b9bc9c432ca4afad184ab6e7c16af#27132caffc5b9bc9c432ca4afad184ab6e7c16af"
dependencies = [
"bitflags 2.6.0",
"bitflags 2.4.2",
]
[[package]]
name = "xkbcommon"
version = "0.7.0"
source = "git+https://github.com/ConradIrwin/xkbcommon-rs?rev=fcbb4612185cc129ceeff51d22f7fb51810a03b2#fcbb4612185cc129ceeff51d22f7fb51810a03b2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13867d259930edc7091a6c41b4ce6eee464328c6ff9659b7e4c668ca20d4c91e"
dependencies = [
"as-raw-xcb-connection",
"libc",
"memmap2",
"memmap2 0.8.0",
"xkeysym",
]
@@ -13628,7 +13586,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.144.3"
version = "0.145.0"
dependencies = [
"activity_indicator",
"anyhow",

View File

@@ -274,7 +274,7 @@ zed_actions = { path = "crates/zed_actions" }
alacritty_terminal = "0.23"
any_vec = "0.13"
anyhow = "1.0.57"
ashpd = "0.9.1"
ashpd = { git = "https://github.com/bilelmoussaoui/ashpd", rev = "29f2e1a" }
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
async-dispatcher = { version = "0.1" }
async-fs = "1.6"
@@ -284,10 +284,10 @@ async-trait = "0.1"
async-watch = "0.3.1"
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
base64 = "0.13"
bitflags = "2.6.0"
blade-graphics = { git = "https://github.com/zed-industries/blade", rev = "a477c2008db27db0b9f745715e119b3ee7ab7818" }
blade-macros = { git = "https://github.com/zed-industries/blade", rev = "a477c2008db27db0b9f745715e119b3ee7ab7818" }
blade-util = { git = "https://github.com/zed-industries/blade", rev = "a477c2008db27db0b9f745715e119b3ee7ab7818" }
bitflags = "2.4.2"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7" }
blade-util = { git = "https://github.com/kvark/blade", rev = "21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7" }
cap-std = "3.0"
cargo_toml = "0.20"
chrono = { version = "0.4", features = ["serde"] }
@@ -365,7 +365,6 @@ shellexpand = "2.1.0"
shlex = "1.3.0"
signal-hook = "0.3.17"
similar = "1.3"
simplelog = "0.12.2"
smallvec = { version = "1.6", features = ["union"] }
smol = "1.2"
strum = { version = "0.25.0", features = ["derive"] }
@@ -519,7 +518,7 @@ single_range_in_vec_init = "allow"
# There are a bunch of rules currently failing in the `style` group, so
# allow all of those, for now.
style = "allow"
style = { level = "allow", priority = -1 }
# Individual rules that have violations in the codebase:
almost_complete_range = "allow"

View File

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

After

Width:  |  Height:  |  Size: 1.4 KiB

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

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

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

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

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -397,7 +397,7 @@
"bindings": {
"ctrl-shift-k": "editor::DeleteLine",
"ctrl-shift-d": "editor::DuplicateLineDown",
"ctrl-shift-j": "editor::JoinLines",
"ctrl-j": "editor::JoinLines",
"ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
"ctrl-alt-h": "editor::DeleteToPreviousSubwordStart",
"ctrl-alt-delete": "editor::DeleteToNextSubwordEnd",

View File

@@ -569,7 +569,7 @@
}
},
{
"context": "Editor && jupyter",
"context": "Editor && jupyter && !ContextEditor",
"bindings": {
"cmd-enter": "repl::Run"
}

View File

@@ -27,7 +27,6 @@
"ctrl-,": "editor::GoToPrevHunk",
"cmd-k cmd-u": "editor::ConvertToUpperCase",
"cmd-k cmd-l": "editor::ConvertToLowerCase",
"cmd-shift-j": "editor::JoinLines",
"shift-alt-m": "markdown::OpenPreviewToTheSide",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd"

View File

@@ -17,6 +17,7 @@
"j": "vim::Down",
"down": "vim::Down",
"enter": "vim::NextLineStart",
"ctrl-m": "vim::NextLineStart",
"tab": "vim::Tab",
"shift-tab": "vim::Tab",
"k": "vim::Up",

View File

@@ -128,7 +128,14 @@
// The default number of lines to expand excerpts in the multibuffer by.
"expand_excerpt_lines": 3,
// Globs to match against file paths to determine if a file is private.
"private_files": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"],
"private_files": [
"**/.env*",
"**/*.pem",
"**/*.key",
"**/*.cert",
"**/*.crt",
"**/secrets.yml"
],
// Whether to use additional LSP queries to format (and amend) the code after
// every "trigger" symbol input, defined by LSP server capabilities.
"use_on_type_format": true,
@@ -707,12 +714,10 @@
}
},
"C": {
"format_on_save": "off",
"use_on_type_format": false
"format_on_save": "off"
},
"C++": {
"format_on_save": "off",
"use_on_type_format": false
"format_on_save": "off"
},
"CSS": {
"prettier": {
@@ -764,7 +769,6 @@
},
"Markdown": {
"format_on_save": "off",
"use_on_type_format": false,
"prettier": {
"allowed": true
}

View File

@@ -491,7 +491,7 @@
"info": "#5c78e2ff",
"info.background": "#e2e2faff",
"info.border": "#cbcdf6ff",
"modified": "#dec184ff",
"modified": "#a47a23ff",
"modified.background": "#faf2e6ff",
"modified.border": "#f4e7d1ff",
"predictive": "#9b9ec6ff",

View File

@@ -12,6 +12,14 @@ workspace = true
path = "src/assistant.rs"
doctest = false
[features]
test-support = [
"editor/test-support",
"language/test-support",
"project/test-support",
"text/test-support",
]
[dependencies]
anthropic = { workspace = true, features = ["schemars"] }
anyhow.workspace = true
@@ -21,6 +29,7 @@ breadcrumbs.workspace = true
cargo_toml.workspace = true
chrono.workspace = true
client.workspace = true
clock.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
editor.workspace = true
@@ -72,7 +81,9 @@ picker.workspace = true
ctor.workspace = true
editor = { workspace = true, features = ["test-support"] }
env_logger.workspace = true
language = { workspace = true, features = ["test-support"] }
log.workspace = true
project = { workspace = true, features = ["test-support"] }
rand.workspace = true
text = { workspace = true, features = ["test-support"] }
unindent.workspace = true

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,13 @@
mod anthropic;
mod cloud;
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
mod fake;
mod ollama;
mod open_ai;
pub use anthropic::*;
pub use cloud::*;
#[cfg(test)]
#[cfg(any(test, feature = "test-support"))]
pub use fake::*;
pub use ollama::*;
pub use open_ai::*;

View File

@@ -13,7 +13,6 @@ pub struct FakeCompletionProvider {
}
impl FakeCompletionProvider {
#[cfg(test)]
pub fn setup_test(cx: &mut AppContext) -> Self {
use crate::CompletionProvider;
use parking_lot::RwLock;

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -31,7 +31,6 @@ pub mod tabs_command;
pub mod term_command;
pub(crate) struct SlashCommandCompletionProvider {
commands: Arc<SlashCommandRegistry>,
cancel_flag: Mutex<Arc<AtomicBool>>,
editor: Option<WeakView<ContextEditor>>,
workspace: Option<WeakView<Workspace>>,
@@ -46,14 +45,12 @@ pub(crate) struct SlashCommandLine {
impl SlashCommandCompletionProvider {
pub fn new(
commands: Arc<SlashCommandRegistry>,
editor: Option<WeakView<ContextEditor>>,
workspace: Option<WeakView<Workspace>>,
) -> Self {
Self {
cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
editor,
commands,
workspace,
}
}
@@ -65,8 +62,8 @@ impl SlashCommandCompletionProvider {
name_range: Range<Anchor>,
cx: &mut WindowContext,
) -> Task<Result<Vec<project::Completion>>> {
let candidates = self
.commands
let commands = SlashCommandRegistry::global(cx);
let candidates = commands
.command_names()
.into_iter()
.enumerate()
@@ -76,7 +73,6 @@ impl SlashCommandCompletionProvider {
char_bag: def.as_ref().into(),
})
.collect::<Vec<_>>();
let commands = self.commands.clone();
let command_name = command_name.to_string();
let editor = self.editor.clone();
let workspace = self.workspace.clone();
@@ -155,7 +151,8 @@ impl SlashCommandCompletionProvider {
flag.store(true, SeqCst);
*flag = new_cancel_flag.clone();
if let Some(command) = self.commands.command(command_name) {
let commands = SlashCommandRegistry::global(cx);
if let Some(command) = commands.command(command_name) {
let completions = command.complete_argument(
argument,
new_cancel_flag.clone(),

View File

@@ -67,7 +67,7 @@ pub struct SlashCommandOutput {
pub run_commands_in_text: bool,
}
#[derive(Clone, Serialize, Deserialize)]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SlashCommandOutputSection<T> {
pub range: Range<T>,
pub icon: IconName,

View File

@@ -72,7 +72,7 @@ impl Render for Breadcrumbs {
.into_any()
});
let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || {
Label::new("").color(Color::Muted).into_any_element()
Label::new("").color(Color::Placeholder).into_any_element()
});
let breadcrumbs_stack = h_flex().gap_1().children(breadcrumbs);
@@ -83,7 +83,7 @@ impl Render for Breadcrumbs {
Some(editor) => element.child(
ButtonLike::new("toggle outline view")
.child(breadcrumbs_stack)
.style(ButtonStyle::Subtle)
.style(ButtonStyle::Transparent)
.on_click(move |_, cx| {
if let Some(editor) = editor.upgrade() {
outline::toggle(editor, &editor::actions::ToggleOutline, cx)

View File

@@ -217,6 +217,9 @@ pub struct Client {
>,
>,
>,
#[cfg(any(test, feature = "test-support"))]
rpc_url: RwLock<Option<Url>>,
}
#[derive(Error, Debug)]
@@ -527,6 +530,8 @@ impl Client {
authenticate: Default::default(),
#[cfg(any(test, feature = "test-support"))]
establish_connection: Default::default(),
#[cfg(any(test, feature = "test-support"))]
rpc_url: RwLock::default(),
})
}
@@ -584,6 +589,12 @@ impl Client {
self
}
#[cfg(any(test, feature = "test-support"))]
pub fn override_rpc_url(&self, url: Url) -> &Self {
*self.rpc_url.write() = Some(url);
self
}
pub fn global(cx: &AppContext) -> Arc<Self> {
cx.global::<GlobalClient>().0.clone()
}
@@ -1086,38 +1097,50 @@ impl Client {
self.establish_websocket_connection(credentials, cx)
}
async fn get_rpc_url(
fn rpc_url(
&self,
http: Arc<HttpClientWithUrl>,
release_channel: Option<ReleaseChannel>,
) -> Result<Url> {
if let Some(url) = &*ZED_RPC_URL {
return Url::parse(url).context("invalid rpc url");
}
) -> impl Future<Output = Result<Url>> {
#[cfg(any(test, feature = "test-support"))]
let url_override = self.rpc_url.read().clone();
let mut url = http.build_url("/rpc");
if let Some(preview_param) =
release_channel.and_then(|channel| channel.release_query_param())
{
url += "?";
url += preview_param;
}
let response = http.get(&url, Default::default(), false).await?;
let collab_url = if response.status().is_redirection() {
response
.headers()
.get("Location")
.ok_or_else(|| anyhow!("missing location header in /rpc response"))?
.to_str()
.map_err(EstablishConnectionError::other)?
.to_string()
} else {
Err(anyhow!(
"unexpected /rpc response status {}",
response.status()
))?
};
async move {
#[cfg(any(test, feature = "test-support"))]
if let Some(url) = url_override {
return Ok(url);
}
Url::parse(&collab_url).context("invalid rpc url")
if let Some(url) = &*ZED_RPC_URL {
return Url::parse(url).context("invalid rpc url");
}
let mut url = http.build_url("/rpc");
if let Some(preview_param) =
release_channel.and_then(|channel| channel.release_query_param())
{
url += "?";
url += preview_param;
}
let response = http.get(&url, Default::default(), false).await?;
let collab_url = if response.status().is_redirection() {
response
.headers()
.get("Location")
.ok_or_else(|| anyhow!("missing location header in /rpc response"))?
.to_str()
.map_err(EstablishConnectionError::other)?
.to_string()
} else {
Err(anyhow!(
"unexpected /rpc response status {}",
response.status()
))?
};
Url::parse(&collab_url).context("invalid rpc url")
}
}
fn establish_websocket_connection(
@@ -1144,8 +1167,9 @@ impl Client {
);
let http = self.http.clone();
let rpc_url = self.rpc_url(http, release_channel);
cx.background_executor().spawn(async move {
let mut rpc_url = Self::get_rpc_url(http, release_channel).await?;
let mut rpc_url = rpc_url.await?;
let rpc_host = rpc_url
.host_str()
.zip(rpc_url.port_or_known_default())
@@ -1186,6 +1210,7 @@ impl Client {
cx: &AsyncAppContext,
) -> Task<Result<Credentials>> {
let http = self.http.clone();
let this = self.clone();
cx.spawn(|cx| async move {
let background = cx.background_executor().clone();
@@ -1215,7 +1240,8 @@ impl Client {
{
eprintln!("authenticate as admin {login}, {token}");
return Self::authenticate_as_admin(http, login.clone(), token.clone())
return this
.authenticate_as_admin(http, login.clone(), token.clone())
.await;
}
@@ -1303,6 +1329,7 @@ impl Client {
}
async fn authenticate_as_admin(
self: &Arc<Self>,
http: Arc<HttpClientWithUrl>,
login: String,
mut api_token: String,
@@ -1319,7 +1346,7 @@ impl Client {
// Use the collab server's admin API to retrieve the id
// of the impersonated user.
let mut url = Self::get_rpc_url(http.clone(), None).await?;
let mut url = self.rpc_url(http.clone(), None).await?;
url.set_path("/user");
url.set_query(Some(&format!("github_login={login}")));
let request = Request::get(url.as_str())

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ use crate::{
},
};
use anyhow::{anyhow, Result};
use assistant::ContextStore;
use call::{room, ActiveCall, ParticipantLocation, Room};
use client::{User, RECEIVE_TIMEOUT};
use collections::{HashMap, HashSet};
@@ -6449,3 +6450,123 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
assert!(!pane.can_navigate_forward());
});
}
#[gpui::test(iterations = 10)]
async fn test_context_collaboration_with_reconnect(
executor: BackgroundExecutor,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
let mut server = TestServer::start(executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
client_a.fs().insert_tree("/a", Default::default()).await;
let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
// Client A sees that a guest has joined.
executor.run_until_parked();
project_a.read_with(cx_a, |project, _| {
assert_eq!(project.collaborators().len(), 1);
});
project_b.read_with(cx_b, |project, _| {
assert_eq!(project.collaborators().len(), 1);
});
let context_store_a = cx_a
.update(|cx| ContextStore::new(project_a.clone(), cx))
.await
.unwrap();
let context_store_b = cx_b
.update(|cx| ContextStore::new(project_b.clone(), cx))
.await
.unwrap();
// Client A creates a new context.
let context_a = context_store_a.update(cx_a, |store, cx| store.create(cx));
executor.run_until_parked();
// Client B retrieves host's contexts and joins one.
let context_b = context_store_b
.update(cx_b, |store, cx| {
let host_contexts = store.host_contexts().to_vec();
assert_eq!(host_contexts.len(), 1);
store.open_remote_context(host_contexts[0].id.clone(), cx)
})
.await
.unwrap();
// Host and guest make changes
context_a.update(cx_a, |context, cx| {
context.buffer().update(cx, |buffer, cx| {
buffer.edit([(0..0, "Host change\n")], None, cx)
})
});
context_b.update(cx_b, |context, cx| {
context.buffer().update(cx, |buffer, cx| {
buffer.edit([(0..0, "Guest change\n")], None, cx)
})
});
executor.run_until_parked();
assert_eq!(
context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()),
"Guest change\nHost change\n"
);
assert_eq!(
context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()),
"Guest change\nHost change\n"
);
// Disconnect client A and make some changes while disconnected.
server.disconnect_client(client_a.peer_id().unwrap());
server.forbid_connections();
context_a.update(cx_a, |context, cx| {
context.buffer().update(cx, |buffer, cx| {
buffer.edit([(0..0, "Host offline change\n")], None, cx)
})
});
context_b.update(cx_b, |context, cx| {
context.buffer().update(cx, |buffer, cx| {
buffer.edit([(0..0, "Guest offline change\n")], None, cx)
})
});
executor.run_until_parked();
assert_eq!(
context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()),
"Host offline change\nGuest change\nHost change\n"
);
assert_eq!(
context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()),
"Guest offline change\nGuest change\nHost change\n"
);
// Allow client A to reconnect and verify that contexts converge.
server.allow_connections();
executor.advance_clock(RECEIVE_TIMEOUT);
assert_eq!(
context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()),
"Guest offline change\nHost offline change\nGuest change\nHost change\n"
);
assert_eq!(
context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()),
"Guest offline change\nHost offline change\nGuest change\nHost change\n"
);
// Client A disconnects without being able to reconnect. Context B becomes readonly.
server.forbid_connections();
server.disconnect_client(client_a.peer_id().unwrap());
executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
context_b.read_with(cx_b, |context, cx| {
assert!(context.buffer().read(cx).read_only());
});
}

View File

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

View File

@@ -11093,6 +11093,7 @@ impl Editor {
if *singleton_buffer_edited {
if let Some(project) = &self.project {
let project = project.read(cx);
#[allow(clippy::mutable_key_type)]
let languages_affected = multibuffer
.read(cx)
.all_buffers()

View File

@@ -49,11 +49,13 @@ fn display_ranges<'a>(
.pending
.as_ref()
.map(|pending| &pending.selection);
selections
.disjoint
.iter()
.chain(pending)
.map(move |s| s.start.to_display_point(&display_map)..s.end.to_display_point(&display_map))
selections.disjoint.iter().chain(pending).map(move |s| {
if s.reversed {
s.end.to_display_point(&display_map)..s.start.to_display_point(&display_map)
} else {
s.start.to_display_point(&display_map)..s.end.to_display_point(&display_map)
}
})
}
pub fn deploy_context_menu(

View File

@@ -12,7 +12,7 @@ use editor::{Editor, EditorElement, EditorStyle};
use extension::{ExtensionManifest, ExtensionOperation, ExtensionStore};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
actions, uniform_list, AnyElement, AppContext, EventEmitter, Flatten, FocusableView, FontStyle,
actions, uniform_list, AnyElement, AppContext, EventEmitter, FocusableView, FontStyle,
InteractiveElement, KeyContext, ParentElement, Render, Styled, Task, TextStyle,
UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext,
};
@@ -24,6 +24,7 @@ use std::time::Duration;
use std::{ops::Range, sync::Arc};
use theme::ThemeSettings;
use ui::{prelude::*, ContextMenu, PopoverMenu, ToggleButton, Tooltip};
use util::ResultExt as _;
use workspace::item::TabContentParams;
use workspace::{
item::{Item, ItemEvent},
@@ -57,23 +58,9 @@ pub fn init(cx: &mut AppContext) {
multiple: false,
});
let workspace_handle = cx.view().downgrade();
cx.deref_mut()
.spawn(|mut cx| async move {
let extension_path =
match Flatten::flatten(prompt.await.map_err(|e| e.into())) {
Ok(Some(mut paths)) => paths.pop()?,
Ok(None) => return None,
Err(err) => {
workspace_handle
.update(&mut cx, |workspace, cx| {
workspace.show_portal_error(err.to_string(), cx);
})
.ok();
return None;
}
};
let extension_path = prompt.await.log_err()??.pop()?;
store
.update(&mut cx, |store, cx| {
store

View File

@@ -124,7 +124,6 @@ wayland-protocols = { version = "0.31.2", features = [
] }
wayland-protocols-plasma = { version = "0.2.0", features = ["client"] }
oo7 = "0.3.0"
open = "5.2.0"
filedescriptor = "0.8.2"
x11rb = { version = "0.13.0", features = [
"allow-unsafe-code",
@@ -135,10 +134,7 @@ x11rb = { version = "0.13.0", features = [
"resource_manager",
"sync",
] }
xkbcommon = { git = "https://github.com/ConradIrwin/xkbcommon-rs", rev = "fcbb4612185cc129ceeff51d22f7fb51810a03b2", features = [
"wayland",
"x11",
] }
xkbcommon = { version = "0.7", features = ["wayland", "x11"] }
xim = { git = "https://github.com/npmania/xim-rs", rev = "27132caffc5b9bc9c432ca4afad184ab6e7c16af", features = [
"x11rb-xcb",
"x11rb-client",

View File

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

View File

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

View File

@@ -612,11 +612,10 @@ impl AppContext {
/// Displays a platform modal for selecting paths.
/// When one or more paths are selected, they'll be relayed asynchronously via the returned oneshot channel.
/// If cancelled, a `None` will be relayed instead.
/// May return an error on Linux if the file picker couldn't be opened.
pub fn prompt_for_paths(
&self,
options: PathPromptOptions,
) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
self.platform.prompt_for_paths(options)
}
@@ -624,11 +623,7 @@ impl AppContext {
/// The provided directory will be used to set the initial location.
/// When a path is selected, it is relayed asynchronously via the returned oneshot channel.
/// If cancelled, a `None` will be relayed instead.
/// May return an error on Linux if the file picker couldn't be opened.
pub fn prompt_for_new_path(
&self,
directory: &Path,
) -> oneshot::Receiver<Result<Option<PathBuf>>> {
pub fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> {
self.platform.prompt_for_new_path(directory)
}

View File

@@ -137,8 +137,8 @@ pub(crate) trait Platform: 'static {
fn prompt_for_paths(
&self,
options: PathPromptOptions,
) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>>;
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Result<Option<PathBuf>>>;
) -> oneshot::Receiver<Option<Vec<PathBuf>>>;
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>>;
fn reveal_path(&self, path: &Path);
fn on_quit(&self, callback: Box<dyn FnMut()>);
@@ -318,6 +318,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
) -> Option<oneshot::Receiver<usize>>;
fn activate(&self);
fn is_active(&self) -> bool;
fn is_hovered(&self) -> bool;
fn set_title(&mut self, title: &str);
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance);
fn minimize(&self);
@@ -327,6 +328,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn on_request_frame(&self, callback: Box<dyn FnMut()>);
fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>);
fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>);
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
fn on_moved(&self, callback: Box<dyn FnMut()>);
fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);

View File

@@ -21,7 +21,6 @@ use std::{
use anyhow::anyhow;
use ashpd::desktop::file_chooser::{OpenFileRequest, SaveFileRequest};
use ashpd::desktop::open_uri::{OpenDirectoryRequest, OpenFileRequest as OpenUriRequest};
use ashpd::desktop::ResponseError;
use ashpd::{url, ActivationToken};
use async_task::Runnable;
use calloop::channel::Channel;
@@ -55,9 +54,6 @@ pub(crate) const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(400);
pub(crate) const DOUBLE_CLICK_DISTANCE: Pixels = px(5.0);
pub(crate) const KEYRING_LABEL: &str = "zed-github-account";
const FILE_PICKER_PORTAL_MISSING: &str =
"Couldn't open file picker due to missing xdg-desktop-portal implementation.";
pub trait LinuxClient {
fn compositor_name(&self) -> &'static str;
fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R;
@@ -260,7 +256,7 @@ impl<P: LinuxClient + 'static> Platform for P {
fn prompt_for_paths(
&self,
options: PathPromptOptions,
) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
let (done_tx, done_rx) = oneshot::channel();
self.foreground_executor()
.spawn(async move {
@@ -278,7 +274,7 @@ impl<P: LinuxClient + 'static> Platform for P {
}
};
let request = match OpenFileRequest::default()
let result = OpenFileRequest::default()
.modal(true)
.title(title)
.accept_label("Select")
@@ -286,68 +282,49 @@ impl<P: LinuxClient + 'static> Platform for P {
.directory(options.directories)
.send()
.await
{
Ok(request) => request,
Err(err) => {
let result = match err {
ashpd::Error::PortalNotFound(_) => anyhow!(FILE_PICKER_PORTAL_MISSING),
err => err.into(),
};
done_tx.send(Err(result));
return;
}
};
let result = match request.response() {
Ok(response) => Ok(Some(
.ok()
.and_then(|request| request.response().ok())
.and_then(|response| {
response
.uris()
.iter()
.filter_map(|uri| uri.to_file_path().ok())
.collect::<Vec<_>>(),
)),
Err(ashpd::Error::Response(ResponseError::Cancelled)) => Ok(None),
Err(e) => Err(e.into()),
};
.map(|uri| uri.to_file_path().ok())
.collect()
});
done_tx.send(result);
})
.detach();
done_rx
}
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Result<Option<PathBuf>>> {
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> {
let (done_tx, done_rx) = oneshot::channel();
let directory = directory.to_owned();
self.foreground_executor()
.spawn(async move {
let request = match SaveFileRequest::default()
let request = SaveFileRequest::default()
.modal(true)
.title("Select new path")
.accept_label("Accept")
.current_folder(directory)
.expect("pathbuf should not be nul terminated")
.send()
.await
{
Ok(request) => request,
Err(err) => {
let result = match err {
ashpd::Error::PortalNotFound(_) => anyhow!(FILE_PICKER_PORTAL_MISSING),
err => err.into(),
};
done_tx.send(Err(result));
return;
}
.current_folder(directory);
let result = if let Ok(request) = request {
request
.send()
.await
.ok()
.and_then(|request| request.response().ok())
.and_then(|response| {
response
.uris()
.first()
.and_then(|uri| uri.to_file_path().ok())
})
} else {
None
};
let result = match request.response() {
Ok(response) => Ok(response
.uris()
.first()
.and_then(|uri| uri.to_file_path().ok())),
Err(ashpd::Error::Response(ResponseError::Cancelled)) => Ok(None),
Err(e) => Err(e.into()),
};
done_tx.send(result);
})
.detach();
@@ -525,26 +502,11 @@ pub(super) fn open_uri_internal(
if let Some(uri) = url::Url::parse(uri).log_err() {
executor
.spawn(async move {
match OpenUriRequest::default()
.activation_token(activation_token.clone().map(ActivationToken::from))
OpenUriRequest::default()
.activation_token(activation_token.map(ActivationToken::from))
.send_uri(&uri)
.await
{
Ok(_) => return,
Err(e) => log::error!("Failed to open with dbus: {}", e),
}
for mut command in open::commands(uri.to_string()) {
if let Some(token) = activation_token.as_ref() {
command.env("XDG_ACTIVATION_TOKEN", token);
}
match command.spawn() {
Ok(_) => return,
Err(e) => {
log::error!("Failed to open with {:?}: {}", command.get_program(), e)
}
}
}
.log_err();
})
.detach();
}
@@ -557,20 +519,12 @@ pub(super) fn reveal_path_internal(
) {
executor
.spawn(async move {
if let Some(dir) = File::open(path.clone()).log_err() {
match OpenDirectoryRequest::default()
if let Some(dir) = File::open(path).log_err() {
OpenDirectoryRequest::default()
.activation_token(activation_token.map(ActivationToken::from))
.send(&dir.as_fd())
.await
{
Ok(_) => return,
Err(e) => log::error!("Failed to open with dbus: {}", e),
}
if path.is_dir() {
open::that_detached(path).log_err();
} else {
open::that_detached(path.parent().unwrap_or(Path::new(""))).log_err();
}
.log_err();
}
})
.detach();

View File

@@ -11,7 +11,6 @@ use calloop_wayland_source::WaylandSource;
use collections::HashMap;
use filedescriptor::Pipe;
use http::Url;
use smallvec::SmallVec;
use util::ResultExt;
use wayland_backend::client::ObjectId;
@@ -1404,6 +1403,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
if let Some(window) = get_window(&mut state, &surface.id()) {
state.mouse_focused_window = Some(window.clone());
if state.enter_token.is_some() {
state.enter_token = None;
}
@@ -1417,7 +1417,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
}
}
drop(state);
window.set_focused(true);
window.set_hovered(true);
}
}
wl_pointer::Event::Leave { .. } => {
@@ -1433,7 +1433,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
drop(state);
focused_window.handle_input(input);
focused_window.set_focused(false);
focused_window.set_hovered(false);
}
}
wl_pointer::Event::Motion {
@@ -1792,8 +1792,7 @@ impl Dispatch<wl_data_device::WlDataDevice, ()> for WaylandClientStatePtr {
let paths: SmallVec<[_; 2]> = file_list
.lines()
.filter_map(|path| Url::parse(path).log_err())
.filter_map(|url| url.to_file_path().log_err())
.map(|path| PathBuf::from(path.replace("file://", "")))
.collect();
let position = Point::new(x.into(), y.into());

View File

@@ -36,6 +36,7 @@ pub(crate) struct Callbacks {
request_frame: Option<Box<dyn FnMut()>>,
input: Option<Box<dyn FnMut(crate::PlatformInput) -> crate::DispatchEventResult>>,
active_status_change: Option<Box<dyn FnMut(bool)>>,
hover_status_change: Option<Box<dyn FnMut(bool)>>,
resize: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
moved: Option<Box<dyn FnMut()>>,
should_close: Option<Box<dyn FnMut() -> bool>>,
@@ -97,6 +98,7 @@ pub struct WaylandWindowState {
client: WaylandClientStatePtr,
handle: AnyWindowHandle,
active: bool,
hovered: bool,
in_progress_configure: Option<InProgressConfigure>,
in_progress_window_controls: Option<WindowControls>,
window_controls: WindowControls,
@@ -181,6 +183,7 @@ impl WaylandWindowState {
appearance,
handle,
active: false,
hovered: false,
in_progress_window_controls: None,
// Assume that we can do anything, unless told otherwise
window_controls: WindowControls {
@@ -700,6 +703,12 @@ impl WaylandWindowStatePtr {
}
}
pub fn set_hovered(&self, focus: bool) {
if let Some(ref mut fun) = self.callbacks.borrow_mut().hover_status_change {
fun(focus);
}
}
pub fn set_appearance(&mut self, appearance: WindowAppearance) {
self.state.borrow_mut().appearance = appearance;
@@ -845,6 +854,10 @@ impl PlatformWindow for WaylandWindow {
self.borrow().active
}
fn is_hovered(&self) -> bool {
self.borrow().hovered
}
fn set_title(&mut self, title: &str) {
self.borrow().toplevel.set_title(title.to_string());
}
@@ -899,6 +912,10 @@ impl PlatformWindow for WaylandWindow {
self.0.callbacks.borrow_mut().active_status_change = Some(callback);
}
fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>) {
self.0.callbacks.borrow_mut().hover_status_change = Some(callback);
}
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
self.0.callbacks.borrow_mut().resize = Some(callback);
}

View File

@@ -24,7 +24,7 @@ use x11rb::xcb_ffi::XCBConnection;
use xim::{x11rb::X11rbClient, Client};
use xim::{AttributeName, InputStyle};
use xkbc::x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION};
use xkbcommon::xkb::{self as xkbc, LayoutIndex, ModMask};
use xkbcommon::xkb as xkbc;
use crate::platform::linux::LinuxClient;
use crate::platform::{LinuxCommon, PlatformWindow};
@@ -94,13 +94,6 @@ impl From<xim::ClientError> for EventHandlerError {
}
}
#[derive(Debug, Default, Clone)]
struct XKBStateNotiy {
depressed_layout: LayoutIndex,
latched_layout: LayoutIndex,
locked_layout: LayoutIndex,
}
pub struct X11ClientState {
pub(crate) loop_handle: LoopHandle<'static, X11Client>,
pub(crate) event_loop: Option<calloop::EventLoop<'static, X11Client>>,
@@ -117,9 +110,9 @@ pub struct X11ClientState {
pub(crate) _resource_database: Database,
pub(crate) atoms: XcbAtoms,
pub(crate) windows: HashMap<xproto::Window, WindowRef>,
pub(crate) focused_window: Option<xproto::Window>,
pub(crate) mouse_focused_window: Option<xproto::Window>,
pub(crate) keyboard_focused_window: Option<xproto::Window>,
pub(crate) xkb: xkbc::State,
previous_xkb_state: XKBStateNotiy,
pub(crate) ximc: Option<X11rbClient<Rc<XCBConnection>>>,
pub(crate) xim_handler: Option<XimHandler>,
pub modifiers: Modifiers,
@@ -152,7 +145,12 @@ impl X11ClientStatePtr {
if let Some(window_ref) = state.windows.remove(&x_window) {
state.loop_handle.remove(window_ref.refresh_event_token);
}
if state.mouse_focused_window == Some(x_window) {
state.mouse_focused_window = None;
}
if state.keyboard_focused_window == Some(x_window) {
state.keyboard_focused_window = None;
}
state.cursor_styles.remove(&x_window);
if state.windows.is_empty() {
@@ -349,9 +347,9 @@ impl X11Client {
_resource_database: resource_database,
atoms,
windows: HashMap::default(),
focused_window: None,
mouse_focused_window: None,
keyboard_focused_window: None,
xkb: xkb_state,
previous_xkb_state: XKBStateNotiy::default(),
ximc,
xim_handler,
@@ -511,7 +509,7 @@ impl X11Client {
.push(AttributeName::ClientWindow, xim_handler.window)
.push(AttributeName::FocusWindow, xim_handler.window);
let window_id = state.focused_window;
let window_id = state.keyboard_focused_window;
drop(state);
if let Some(window_id) = window_id {
let window = self.get_window(window_id).unwrap();
@@ -595,17 +593,17 @@ impl X11Client {
}
Event::FocusIn(event) => {
let window = self.get_window(event.event)?;
window.set_focused(true);
window.set_active(true);
let mut state = self.0.borrow_mut();
state.focused_window = Some(event.event);
state.keyboard_focused_window = Some(event.event);
drop(state);
self.enable_ime();
}
Event::FocusOut(event) => {
let window = self.get_window(event.event)?;
window.set_focused(false);
window.set_active(false);
let mut state = self.0.borrow_mut();
state.focused_window = None;
state.keyboard_focused_window = None;
if let Some(compose_state) = state.compose_state.as_mut() {
compose_state.reset();
}
@@ -624,16 +622,12 @@ impl X11Client {
event.latched_group as u32,
event.locked_group.into(),
);
state.previous_xkb_state = XKBStateNotiy {
depressed_layout: event.base_group as u32,
latched_layout: event.latched_group as u32,
locked_layout: event.locked_group.into(),
};
let modifiers = Modifiers::from_xkb(&state.xkb);
if state.modifiers == modifiers {
drop(state);
} else {
let focused_window_id = state.focused_window?;
let focused_window_id = state.keyboard_focused_window?;
state.modifiers = modifiers;
drop(state);
@@ -650,18 +644,11 @@ impl X11Client {
let modifiers = modifiers_from_state(event.state);
state.modifiers = modifiers;
state.pre_ime_key_down.take();
let keystroke = {
let code = event.detail.into();
let xkb_state = state.previous_xkb_state.clone();
state.xkb.update_mask(
event.state.bits() as ModMask,
0,
0,
xkb_state.depressed_layout,
xkb_state.latched_layout,
xkb_state.locked_layout,
);
let mut keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
state.xkb.update_key(code, xkbc::KeyDirection::Down);
let keysym = state.xkb.key_get_one_sym(code);
if keysym.is_modifier_key() {
return Some(());
@@ -720,16 +707,8 @@ impl X11Client {
let keystroke = {
let code = event.detail.into();
let xkb_state = state.previous_xkb_state.clone();
state.xkb.update_mask(
event.state.bits() as ModMask,
0,
0,
xkb_state.depressed_layout,
xkb_state.latched_layout,
xkb_state.locked_layout,
);
let keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
state.xkb.update_key(code, xkbc::KeyDirection::Up);
let keysym = state.xkb.key_get_one_sym(code);
if keysym.is_modifier_key() {
return Some(());
@@ -899,12 +878,18 @@ impl X11Client {
valuator_idx += 1;
}
}
Event::XinputEnter(event) if event.mode == xinput::NotifyMode::NORMAL => {
let window = self.get_window(event.event)?;
window.set_hovered(true);
let mut state = self.0.borrow_mut();
state.mouse_focused_window = Some(event.event);
}
Event::XinputLeave(event) if event.mode == xinput::NotifyMode::NORMAL => {
self.0.borrow_mut().scroll_x = None; // Set last scroll to `None` so that a large delta isn't created if scrolling is done outside the window (the valuator is global)
self.0.borrow_mut().scroll_y = None;
let window = self.get_window(event.event)?;
let mut state = self.0.borrow_mut();
state.mouse_focused_window = None;
let pressed_button = pressed_button_from_mask(event.buttons[0]);
let position = point(
px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor),
@@ -914,11 +899,13 @@ impl X11Client {
state.modifiers = modifiers;
drop(state);
let window = self.get_window(event.event)?;
window.handle_input(PlatformInput::MouseExited(crate::MouseExitEvent {
pressed_button,
position,
modifiers,
}));
window.set_hovered(false);
}
_ => {}
};
@@ -975,17 +962,20 @@ impl X11Client {
fn xim_handle_commit(&self, window: xproto::Window, text: String) -> Option<()> {
let window = self.get_window(window).unwrap();
let mut state = self.0.borrow_mut();
let keystroke = state.pre_ime_key_down.take();
if !state.composing {
if let Some(keystroke) = state.pre_ime_key_down.take() {
drop(state);
window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent {
keystroke,
is_held: false,
}));
return Some(());
}
}
state.composing = false;
drop(state);
if let Some(mut keystroke) = keystroke {
keystroke.ime_key = Some(text.clone());
window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent {
keystroke,
is_held: false,
}));
}
window.handle_ime_commit(text);
Some(())
}
@@ -1165,7 +1155,7 @@ impl LinuxClient for X11Client {
fn set_cursor_style(&self, style: CursorStyle) {
let mut state = self.0.borrow_mut();
let Some(focused_window) = state.focused_window else {
let Some(focused_window) = state.mouse_focused_window else {
return;
};
let current_style = state
@@ -1179,13 +1169,10 @@ impl LinuxClient for X11Client {
let cursor = match state.cursor_cache.get(&style) {
Some(cursor) => *cursor,
None => {
let Some(cursor) = state
let cursor = state
.cursor_handle
.load_cursor(&state.xcb_connection, &style.to_icon_name())
.log_err()
else {
return;
};
.expect("failed to load cursor");
state.cursor_cache.insert(style, cursor);
cursor
}
@@ -1300,7 +1287,7 @@ impl LinuxClient for X11Client {
fn active_window(&self) -> Option<AnyWindowHandle> {
let state = self.0.borrow();
state.focused_window.and_then(|focused_window| {
state.keyboard_focused_window.and_then(|focused_window| {
state
.windows
.get(&focused_window)

View File

@@ -211,6 +211,7 @@ pub struct Callbacks {
request_frame: Option<Box<dyn FnMut()>>,
input: Option<Box<dyn FnMut(PlatformInput) -> crate::DispatchEventResult>>,
active_status_change: Option<Box<dyn FnMut(bool)>>,
hovered_status_change: Option<Box<dyn FnMut(bool)>>,
resize: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
moved: Option<Box<dyn FnMut()>>,
should_close: Option<Box<dyn FnMut() -> bool>>,
@@ -238,6 +239,7 @@ pub struct X11WindowState {
maximized_horizontal: bool,
hidden: bool,
active: bool,
hovered: bool,
fullscreen: bool,
client_side_decorations_supported: bool,
decorations: WindowDecorations,
@@ -451,6 +453,7 @@ impl X11WindowState {
xinput::XIEventMask::MOTION
| xinput::XIEventMask::BUTTON_PRESS
| xinput::XIEventMask::BUTTON_RELEASE
| xinput::XIEventMask::ENTER
| xinput::XIEventMask::LEAVE,
],
}],
@@ -507,6 +510,7 @@ impl X11WindowState {
atoms: *atoms,
input_handler: None,
active: false,
hovered: false,
fullscreen: false,
maximized_vertical: false,
maximized_horizontal: false,
@@ -777,6 +781,15 @@ impl X11WindowStatePtr {
state.hidden = true;
}
}
let hovered_window = self
.xcb_connection
.query_pointer(state.x_root_window)
.unwrap()
.reply()
.unwrap()
.child;
self.set_hovered(hovered_window == self.x_window);
}
pub fn close(&self) {
@@ -912,12 +925,18 @@ impl X11WindowStatePtr {
}
}
pub fn set_focused(&self, focus: bool) {
pub fn set_active(&self, focus: bool) {
if let Some(ref mut fun) = self.callbacks.borrow_mut().active_status_change {
fun(focus);
}
}
pub fn set_hovered(&self, focus: bool) {
if let Some(ref mut fun) = self.callbacks.borrow_mut().hovered_status_change {
fun(focus);
}
}
pub fn set_appearance(&mut self, appearance: WindowAppearance) {
let mut state = self.state.borrow_mut();
state.appearance = appearance;
@@ -1046,6 +1065,10 @@ impl PlatformWindow for X11Window {
self.0.state.borrow().active
}
fn is_hovered(&self) -> bool {
self.0.state.borrow().hovered
}
fn set_title(&mut self, title: &str) {
self.0
.xcb_connection
@@ -1162,6 +1185,10 @@ impl PlatformWindow for X11Window {
self.0.callbacks.borrow_mut().active_status_change = Some(callback);
}
fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>) {
self.0.callbacks.borrow_mut().hovered_status_change = Some(callback);
}
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
self.0.callbacks.borrow_mut().resize = Some(callback);
}

View File

@@ -602,7 +602,7 @@ impl Platform for MacPlatform {
fn prompt_for_paths(
&self,
options: PathPromptOptions,
) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
let (done_tx, done_rx) = oneshot::channel();
self.foreground_executor()
.spawn(async move {
@@ -632,7 +632,7 @@ impl Platform for MacPlatform {
};
if let Some(done_tx) = done_tx.take() {
let _ = done_tx.send(Ok(result));
let _ = done_tx.send(result);
}
});
let block = block.copy();
@@ -643,7 +643,7 @@ impl Platform for MacPlatform {
done_rx
}
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Result<Option<PathBuf>>> {
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> {
let directory = directory.to_owned();
let (done_tx, done_rx) = oneshot::channel();
self.foreground_executor()
@@ -665,7 +665,7 @@ impl Platform for MacPlatform {
}
if let Some(done_tx) = done_tx.take() {
let _ = done_tx.send(Ok(result));
let _ = done_tx.send(result);
}
});
let block = block.copy();

View File

@@ -452,7 +452,7 @@ impl MacWindowState {
let bounds = Bounds::new(
point(
px((window_frame.origin.x - screen_frame.origin.x) as f32),
px((window_frame.origin.y - screen_frame.origin.y) as f32),
px((window_frame.origin.y + screen_frame.origin.y) as f32),
),
size(
px(window_frame.size.width as f32),
@@ -546,7 +546,7 @@ impl MacWindow {
let count: u64 = cocoa::foundation::NSArray::count(screens);
for i in 0..count {
let screen = cocoa::foundation::NSArray::objectAtIndex(screens, i);
let frame = NSScreen::visibleFrame(screen);
let frame = NSScreen::frame(screen);
let display_id = display_id_for_screen(screen);
if display_id == display.0 {
screen_frame = Some(frame);
@@ -557,7 +557,7 @@ impl MacWindow {
let screen_frame = screen_frame.unwrap_or_else(|| {
let screen = NSScreen::mainScreen(nil);
target_screen = screen;
NSScreen::visibleFrame(screen)
NSScreen::frame(screen)
});
let window_rect = NSRect::new(
@@ -940,6 +940,11 @@ impl PlatformWindow for MacWindow {
unsafe { self.0.lock().native_window.isKeyWindow() == YES }
}
// is_hovered is unused on macOS. See WindowContext::is_window_hovered.
fn is_hovered(&self) -> bool {
false
}
fn set_title(&mut self, title: &str) {
unsafe {
let app = NSApplication::sharedApplication(nil);
@@ -1061,6 +1066,8 @@ impl PlatformWindow for MacWindow {
self.0.as_ref().lock().activate_callback = Some(callback);
}
fn on_hover_status_change(&self, _: Box<dyn FnMut(bool)>) {}
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
self.0.as_ref().lock().resize_callback = Some(callback);
}

View File

@@ -34,7 +34,7 @@ pub(crate) struct TestPlatform {
#[derive(Default)]
pub(crate) struct TestPrompts {
multiple_choice: VecDeque<oneshot::Sender<usize>>,
new_path: VecDeque<(PathBuf, oneshot::Sender<Result<Option<PathBuf>>>)>,
new_path: VecDeque<(PathBuf, oneshot::Sender<Option<PathBuf>>)>,
}
impl TestPlatform {
@@ -80,7 +80,7 @@ impl TestPlatform {
.new_path
.pop_front()
.expect("no pending new path prompt");
tx.send(Ok(select_path(&path))).ok();
tx.send(select_path(&path)).ok();
}
pub(crate) fn simulate_prompt_answer(&self, response_ix: usize) {
@@ -216,14 +216,14 @@ impl Platform for TestPlatform {
fn prompt_for_paths(
&self,
_options: crate::PathPromptOptions,
) -> oneshot::Receiver<Result<Option<Vec<std::path::PathBuf>>>> {
) -> oneshot::Receiver<Option<Vec<std::path::PathBuf>>> {
unimplemented!()
}
fn prompt_for_new_path(
&self,
directory: &std::path::Path,
) -> oneshot::Receiver<Result<Option<std::path::PathBuf>>> {
) -> oneshot::Receiver<Option<std::path::PathBuf>> {
let (tx, rx) = oneshot::channel();
self.prompts
.borrow_mut()

View File

@@ -23,6 +23,7 @@ pub(crate) struct TestWindowState {
pub(crate) should_close_handler: Option<Box<dyn FnMut() -> bool>>,
input_callback: Option<Box<dyn FnMut(PlatformInput) -> DispatchEventResult>>,
active_status_change_callback: Option<Box<dyn FnMut(bool)>>,
hover_status_change_callback: Option<Box<dyn FnMut(bool)>>,
resize_callback: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
moved_callback: Option<Box<dyn FnMut()>>,
input_handler: Option<PlatformInputHandler>,
@@ -66,6 +67,7 @@ impl TestWindow {
should_close_handler: None,
input_callback: None,
active_status_change_callback: None,
hover_status_change_callback: None,
resize_callback: None,
moved_callback: None,
input_handler: None,
@@ -182,6 +184,10 @@ impl PlatformWindow for TestWindow {
false
}
fn is_hovered(&self) -> bool {
false
}
fn set_title(&mut self, title: &str) {
self.0.lock().title = Some(title.to_owned());
}
@@ -225,6 +231,10 @@ impl PlatformWindow for TestWindow {
self.0.lock().active_status_change_callback = Some(callback)
}
fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>) {
self.0.lock().hover_status_change_callback = Some(callback)
}
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
self.0.lock().resize_callback = Some(callback)
}

View File

@@ -49,8 +49,7 @@ struct DirectWriteComponent {
struct GlyphRenderContext {
params: IDWriteRenderingParams3,
normal_dc_target: ID2D1DeviceContext4,
emoji_dc_target: ID2D1DeviceContext4,
dc_target: ID2D1DeviceContext4,
}
// All use of the IUnknown methods should be "thread-safe".
@@ -128,16 +127,7 @@ impl GlyphRenderContext {
DWRITE_RENDERING_MODE1_NATURAL_SYMMETRIC,
grid_fit_mode,
)?;
let normal_dc_target = {
let target = d2d1_factory.CreateDCRenderTarget(&get_render_target_property(
DXGI_FORMAT_A8_UNORM,
D2D1_ALPHA_MODE_STRAIGHT,
))?;
let target = target.cast::<ID2D1DeviceContext4>()?;
target.SetTextRenderingParams(&params);
target
};
let emoji_dc_target = {
let dc_target = {
let target = d2d1_factory.CreateDCRenderTarget(&get_render_target_property(
DXGI_FORMAT_B8G8R8A8_UNORM,
D2D1_ALPHA_MODE_PREMULTIPLIED,
@@ -147,11 +137,7 @@ impl GlyphRenderContext {
target
};
Ok(Self {
params,
normal_dc_target,
emoji_dc_target,
})
Ok(Self { params, dc_target })
}
}
}
@@ -571,11 +557,7 @@ impl DirectWriteState {
}
fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
let render_target = if params.is_emoji {
&self.components.render_context.emoji_dc_target
} else {
&self.components.render_context.normal_dc_target
};
let render_target = &self.components.render_context.dc_target;
unsafe {
render_target.SetUnitMode(D2D1_UNIT_MODE_DIPS);
render_target.SetDpi(96.0 * params.scale_factor, 96.0 * params.scale_factor);

View File

@@ -316,10 +316,7 @@ impl Platform for WindowsPlatform {
self.state.borrow_mut().callbacks.open_urls = Some(callback);
}
fn prompt_for_paths(
&self,
options: PathPromptOptions,
) -> Receiver<Result<Option<Vec<PathBuf>>>> {
fn prompt_for_paths(&self, options: PathPromptOptions) -> Receiver<Option<Vec<PathBuf>>> {
let (tx, rx) = oneshot::channel();
self.foreground_executor()
@@ -358,7 +355,7 @@ impl Platform for WindowsPlatform {
if hr.unwrap_err().code() == HRESULT(0x800704C7u32 as i32) {
// user canceled error
if let Some(tx) = tx.take() {
tx.send(Ok(None)).unwrap();
tx.send(None).unwrap();
}
return;
}
@@ -377,10 +374,10 @@ impl Platform for WindowsPlatform {
}
if let Some(tx) = tx.take() {
if paths.is_empty() {
tx.send(Ok(None)).unwrap();
if paths.len() == 0 {
tx.send(None).unwrap();
} else {
tx.send(Ok(Some(paths))).unwrap();
tx.send(Some(paths)).unwrap();
}
}
})
@@ -389,27 +386,27 @@ impl Platform for WindowsPlatform {
rx
}
fn prompt_for_new_path(&self, directory: &Path) -> Receiver<Result<Option<PathBuf>>> {
fn prompt_for_new_path(&self, directory: &Path) -> Receiver<Option<PathBuf>> {
let directory = directory.to_owned();
let (tx, rx) = oneshot::channel();
self.foreground_executor()
.spawn(async move {
unsafe {
let Ok(dialog) = show_savefile_dialog(directory) else {
let _ = tx.send(Ok(None));
let _ = tx.send(None);
return;
};
let Ok(_) = dialog.Show(None) else {
let _ = tx.send(Ok(None)); // user cancel
let _ = tx.send(None); // user cancel
return;
};
if let Ok(shell_item) = dialog.GetResult() {
if let Ok(file) = shell_item.GetDisplayName(SIGDN_FILESYSPATH) {
let _ = tx.send(Ok(Some(PathBuf::from(file.to_string().unwrap()))));
let _ = tx.send(Some(PathBuf::from(file.to_string().unwrap())));
return;
}
}
let _ = tx.send(Ok(None));
let _ = tx.send(None);
}
})
.detach();

View File

@@ -503,6 +503,11 @@ impl PlatformWindow for WindowsWindow {
self.0.hwnd == unsafe { GetActiveWindow() }
}
// is_hovered is unused on Windows. See WindowContext::is_window_hovered.
fn is_hovered(&self) -> bool {
false
}
fn set_title(&mut self, title: &str) {
unsafe { SetWindowTextW(self.0.hwnd, &HSTRING::from(title)) }
.inspect_err(|e| log::error!("Set title failed: {e}"))
@@ -604,6 +609,8 @@ impl PlatformWindow for WindowsWindow {
self.0.state.borrow_mut().callbacks.active_status_change = Some(callback);
}
fn on_hover_status_change(&self, _: Box<dyn FnMut(bool)>) {}
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
self.0.state.borrow_mut().callbacks.resize = Some(callback);
}

View File

@@ -658,26 +658,6 @@ impl Hash for RenderGlyphParams {
}
}
/// The parameters for rendering an emoji glyph.
#[derive(Clone, Debug, PartialEq)]
pub struct RenderEmojiParams {
pub(crate) font_id: FontId,
pub(crate) glyph_id: GlyphId,
pub(crate) font_size: Pixels,
pub(crate) scale_factor: f32,
}
impl Eq for RenderEmojiParams {}
impl Hash for RenderEmojiParams {
fn hash<H: Hasher>(&self, state: &mut H) {
self.font_id.0.hash(state);
self.glyph_id.0.hash(state);
self.font_size.0.to_bits().hash(state);
self.scale_factor.to_bits().hash(state);
}
}
/// The configuration details for identifying a specific font.
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct Font {

View File

@@ -49,9 +49,17 @@ impl LineWrapper {
continue;
}
if prev_c == ' ' && c != ' ' && first_non_whitespace_ix.is_some() {
last_candidate_ix = ix;
last_candidate_width = width;
if Self::is_word_char(c) {
if prev_c == ' ' && c != ' ' && first_non_whitespace_ix.is_some() {
last_candidate_ix = ix;
last_candidate_width = width;
}
} else {
// CJK may not be space separated, e.g.: `Hello world你好世界`
if c != ' ' && first_non_whitespace_ix.is_some() {
last_candidate_ix = ix;
last_candidate_width = width;
}
}
if c != ' ' && first_non_whitespace_ix.is_none() {
@@ -90,6 +98,31 @@ impl LineWrapper {
})
}
pub(crate) fn is_word_char(c: char) -> bool {
// ASCII alphanumeric characters, for English, numbers: `Hello123`, etc.
c.is_ascii_alphanumeric() ||
// Latin script in Unicode for French, German, Spanish, etc.
// Latin-1 Supplement
// https://en.wikipedia.org/wiki/Latin-1_Supplement
matches!(c, '\u{00C0}'..='\u{00FF}') ||
// Latin Extended-A
// https://en.wikipedia.org/wiki/Latin_Extended-A
matches!(c, '\u{0100}'..='\u{017F}') ||
// Latin Extended-B
// https://en.wikipedia.org/wiki/Latin_Extended-B
matches!(c, '\u{0180}'..='\u{024F}') ||
// Cyrillic for Russian, Ukrainian, etc.
// https://en.wikipedia.org/wiki/Cyrillic_script_in_Unicode
matches!(c, '\u{0400}'..='\u{04FF}') ||
// Some other known special characters that should be treated as word characters,
// e.g. `a-b`, `var_name`, `I'm`, '@mention`, `#hashtag`, `100%`, `3.1415`, `2^3`, `a~b`, etc.
matches!(c, '-' | '_' | '.' | '\'' | '$' | '%' | '@' | '#' | '^' | '~') ||
// Characters that used in URL, e.g. `https://github.com/zed-industries/zed?a=1&b=2` for better wrapping a long URL.
matches!(c, '/' | ':' | '?' | '&' | '=') ||
// `⋯` character is special used in Zed, to keep this at the end of the line.
matches!(c, '⋯')
}
#[inline(always)]
fn width_for_char(&mut self, c: char) -> Pixels {
if (c as u32) < 128 {
@@ -219,6 +252,59 @@ mod tests {
});
}
#[test]
fn test_is_word_char() {
#[track_caller]
fn assert_word(word: &str) {
for c in word.chars() {
assert!(LineWrapper::is_word_char(c), "assertion failed for '{}'", c);
}
}
#[track_caller]
fn assert_not_word(word: &str) {
let found = word.chars().any(|c| !LineWrapper::is_word_char(c));
assert!(found, "assertion failed for '{}'", word);
}
assert_word("Hello123");
assert_word("non-English");
assert_word("var_name");
assert_word("123456");
assert_word("3.1415");
assert_word("10^2");
assert_word("1~2");
assert_word("100%");
assert_word("@mention");
assert_word("#hashtag");
assert_word("$variable");
assert_word("more⋯");
// Space
assert_not_word("foo bar");
// URL case
assert_word("https://github.com/zed-industries/zed/");
assert_word("github.com");
assert_word("a=1&b=2");
// Latin-1 Supplement
assert_word("ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏ");
// Latin Extended-A
assert_word("ĀāĂ㥹ĆćĈĉĊċČčĎď");
// Latin Extended-B
assert_word("ƀƁƂƃƄƅƆƇƈƉƊƋƌƍƎƏ");
// Cyrillic
assert_word("АБВГДЕЖЗИЙКЛМНОП");
// non-word characters
assert_not_word("你好");
assert_not_word("안녕하세요");
assert_not_word("こんにちは");
assert_not_word("😀😁😂");
assert_not_word("()[]{}<>");
}
// For compatibility with the test macro
#[cfg(target_os = "macos")]
use crate as gpui;

View File

@@ -541,6 +541,7 @@ pub struct Window {
appearance: WindowAppearance,
appearance_observers: SubscriberSet<(), AnyObserver>,
active: Rc<Cell<bool>>,
hovered: Rc<Cell<bool>>,
pub(crate) dirty: Rc<Cell<bool>>,
pub(crate) needs_present: Rc<Cell<bool>>,
pub(crate) last_input_timestamp: Rc<Cell<Instant>>,
@@ -672,6 +673,7 @@ impl Window {
let text_system = Arc::new(WindowTextSystem::new(cx.text_system().clone()));
let dirty = Rc::new(Cell::new(true));
let active = Rc::new(Cell::new(platform_window.is_active()));
let hovered = Rc::new(Cell::new(platform_window.is_hovered()));
let needs_present = Rc::new(Cell::new(false));
let next_frame_callbacks: Rc<RefCell<Vec<FrameCallback>>> = Default::default();
let last_input_timestamp = Rc::new(Cell::new(Instant::now()));
@@ -778,7 +780,17 @@ impl Window {
.log_err();
}
}));
platform_window.on_hover_status_change(Box::new({
let mut cx = cx.to_async();
move |active| {
handle
.update(&mut cx, |_, cx| {
cx.window.hovered.set(active);
cx.refresh();
})
.log_err();
}
}));
platform_window.on_input({
let mut cx = cx.to_async();
Box::new(move |event| {
@@ -829,6 +841,7 @@ impl Window {
appearance,
appearance_observers: SubscriberSet::new(),
active,
hovered,
dirty,
needs_present,
last_input_timestamp,
@@ -1222,6 +1235,17 @@ impl<'a> WindowContext<'a> {
self.window.active.get()
}
/// Returns whether this window is considered to be the window
/// that currently owns the mouse cursor.
/// On mac, this is equivalent to `is_window_active`.
pub fn is_window_hovered(&self) -> bool {
if cfg!(target_os = "linux") {
self.window.hovered.get()
} else {
self.is_window_active()
}
}
/// Toggle zoom on the window.
pub fn zoom_window(&self) {
self.window.platform_window.zoom();
@@ -2980,7 +3004,7 @@ impl<'a> WindowContext<'a> {
fn reset_cursor_style(&self) {
// Set the cursor only if we're the active window.
if self.is_window_active() {
if self.is_window_hovered() {
let style = self
.window
.rendered_frame

View File

@@ -16,4 +16,4 @@ doctest = false
[dependencies]
proc-macro2 = "1.0.66"
quote = "1.0.9"
syn = { version = "1.0.72", features = ["full"] }
syn = { version = "1.0.72", features = ["full", "extra-traits"] }

View File

@@ -1903,6 +1903,10 @@ impl Buffer {
self.deferred_ops.insert(deferred_ops);
}
pub fn has_deferred_ops(&self) -> bool {
!self.deferred_ops.is_empty() || self.text.has_deferred_ops()
}
fn can_apply_op(&self, operation: &Operation) -> bool {
match operation {
Operation::Buffer(_) => {

View File

@@ -1,7 +1,7 @@
//! Handles conversions of `language` items to and from the [`rpc`] protocol.
use crate::{diagnostic_set::DiagnosticEntry, CursorShape, Diagnostic};
use anyhow::{anyhow, Result};
use anyhow::{anyhow, Context as _, Result};
use clock::ReplicaId;
use lsp::{DiagnosticSeverity, LanguageServerId};
use rpc::proto;
@@ -231,6 +231,21 @@ pub fn serialize_anchor(anchor: &Anchor) -> proto::Anchor {
}
}
pub fn serialize_anchor_range(range: Range<Anchor>) -> proto::AnchorRange {
proto::AnchorRange {
start: Some(serialize_anchor(&range.start)),
end: Some(serialize_anchor(&range.end)),
}
}
/// Deserializes an [`Range<Anchor>`] from the RPC representation.
pub fn deserialize_anchor_range(range: proto::AnchorRange) -> Result<Range<Anchor>> {
Ok(
deserialize_anchor(range.start.context("invalid anchor")?).context("invalid anchor")?
..deserialize_anchor(range.end.context("invalid anchor")?).context("invalid anchor")?,
)
}
// This behavior is currently copied in the collab database, for snapshotting channel notes
/// Deserializes an [`crate::Operation`] from the RPC representation.
pub fn deserialize_operation(message: proto::Operation) -> Result<crate::Operation> {

View File

@@ -1207,7 +1207,7 @@ fn get_injections(
language_registry: &Arc<LanguageRegistry>,
depth: usize,
changed_ranges: &[Range<usize>],
combined_injection_ranges: &mut HashMap<Arc<Language>, Vec<tree_sitter::Range>>,
combined_injection_ranges: &mut HashMap<LanguageId, (Arc<Language>, Vec<tree_sitter::Range>)>,
queue: &mut BinaryHeap<ParseStep>,
) {
let mut query_cursor = QueryCursorHandle::new();
@@ -1223,7 +1223,7 @@ fn get_injections(
.now_or_never()
.and_then(|language| language.ok())
{
combined_injection_ranges.insert(language, Vec::new());
combined_injection_ranges.insert(language.id, (language, Vec::new()));
}
}
}
@@ -1276,8 +1276,9 @@ fn get_injections(
if let Some(language) = language {
if combined {
combined_injection_ranges
.entry(language.clone())
.or_default()
.entry(language.id)
.or_insert_with(|| (language.clone(), vec![]))
.1
.extend(content_ranges);
} else {
queue.push(ParseStep {
@@ -1303,7 +1304,7 @@ fn get_injections(
}
}
for (language, mut included_ranges) in combined_injection_ranges.drain() {
for (_, (language, mut included_ranges)) in combined_injection_ranges.drain() {
included_ranges.sort_unstable_by(|a, b| {
Ord::cmp(&a.start_byte, &b.start_byte).then_with(|| Ord::cmp(&a.end_byte, &b.end_byte))
});

View File

@@ -57,7 +57,7 @@ gpui = { workspace = true, features = ["test-support"] }
live_kit_server.workspace = true
nanoid.workspace = true
sha2.workspace = true
simplelog.workspace = true
simplelog = "0.9"
[build-dependencies]
serde.workspace = true

View File

@@ -7,7 +7,7 @@ use anyhow::{anyhow, Context, Result};
use collections::HashMap;
use futures::{channel::oneshot, io::BufWriter, select, AsyncRead, AsyncWrite, Future, FutureExt};
use gpui::{AppContext, AsyncAppContext, BackgroundExecutor, Task};
use parking_lot::{Mutex, RwLock};
use parking_lot::Mutex;
use postage::{barrier, prelude::Stream};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::{json, value::RawValue, Value};
@@ -24,7 +24,6 @@ use std::{
ffi::OsString,
fmt,
io::Write,
ops::DerefMut,
path::PathBuf,
pin::Pin,
sync::{
@@ -70,7 +69,7 @@ pub struct LanguageServer {
next_id: AtomicI32,
outbound_tx: channel::Sender<String>,
name: Arc<str>,
capabilities: RwLock<ServerCapabilities>,
capabilities: ServerCapabilities,
code_action_kinds: Option<Vec<CodeActionKind>>,
notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
response_handlers: Arc<Mutex<Option<HashMap<RequestId, ResponseHandler>>>>,
@@ -641,19 +640,12 @@ impl LanguageServer {
..Default::default()
}),
formatting: Some(DynamicRegistrationClientCapabilities {
dynamic_registration: Some(true),
}),
range_formatting: Some(DynamicRegistrationClientCapabilities {
dynamic_registration: Some(true),
dynamic_registration: None,
}),
on_type_formatting: Some(DynamicRegistrationClientCapabilities {
dynamic_registration: Some(true),
dynamic_registration: None,
}),
synchronization: Some(TextDocumentSyncClientCapabilities {
did_save: Some(true),
..TextDocumentSyncClientCapabilities::default()
}),
..TextDocumentClientCapabilities::default()
..Default::default()
}),
experimental: Some(json!({
"serverStatusNotification": true,
@@ -684,7 +676,7 @@ impl LanguageServer {
if let Some(info) = response.server_info {
self.name = info.name.into();
}
self.capabilities = RwLock::new(response.capabilities);
self.capabilities = response.capabilities;
self.notify::<notification::Initialized>(InitializedParams {})?;
Ok(Arc::new(self))
@@ -899,12 +891,8 @@ impl LanguageServer {
}
/// Get the reported capabilities of the running language server.
pub fn capabilities(&self) -> ServerCapabilities {
self.capabilities.read().clone()
}
pub fn update_capabilities(&self, update: impl FnOnce(&mut ServerCapabilities)) {
update(self.capabilities.write().deref_mut());
pub fn capabilities(&self) -> &ServerCapabilities {
&self.capabilities
}
/// Get the id of the running language server.

View File

@@ -1622,7 +1622,11 @@ impl OutlinePanel {
ExcerptOutlines::Invalidated(_) => ExcerptOutlines::NotFetched,
ExcerptOutlines::NotFetched => ExcerptOutlines::NotFetched,
},
None => ExcerptOutlines::NotFetched,
None => {
new_collapsed_entries
.insert(CollapsedEntry::Excerpt(buffer_id, excerpt_id));
ExcerptOutlines::NotFetched
}
};
new_excerpts.entry(buffer_id).or_default().insert(
excerpt_id,
@@ -1670,6 +1674,11 @@ impl OutlinePanel {
.insert(CollapsedEntry::ExternalFile(buffer_id));
}
}
for excerpt_id in &excerpts {
new_collapsed_entries
.insert(CollapsedEntry::Excerpt(buffer_id, *excerpt_id));
}
}
if let Some(worktree) = worktree {

View File

@@ -84,7 +84,7 @@ impl Prettier {
path_to_check.pop();
}
let mut closest_package_json_path = None;
let mut project_path_with_prettier_dependency = None;
loop {
if installed_prettiers.contains(&path_to_check) {
log::debug!("Found prettier path {path_to_check:?} in installed prettiers");
@@ -92,44 +92,61 @@ impl Prettier {
} else if let Some(package_json_contents) =
read_package_json(fs, &path_to_check).await?
{
if has_prettier_in_node_modules(fs, &path_to_check).await? {
log::debug!("Found prettier path {path_to_check:?} in the node_modules");
return Ok(ControlFlow::Continue(Some(path_to_check)));
} else {
match &closest_package_json_path {
None => closest_package_json_path = Some(path_to_check.clone()),
Some(closest_package_json_path) => {
match package_json_contents.get("workspaces") {
Some(serde_json::Value::Array(workspaces)) => {
let subproject_path = closest_package_json_path.strip_prefix(&path_to_check).expect("traversing path parents, should be able to strip prefix");
if workspaces.iter().filter_map(|value| {
if let serde_json::Value::String(s) = value {
Some(s.clone())
} else {
log::warn!("Skipping non-string 'workspaces' value: {value:?}");
None
}
}).any(|workspace_definition| {
workspace_definition == subproject_path.to_string_lossy() || PathMatcher::new(&[workspace_definition]).ok().map_or(false, |path_matcher| path_matcher.is_match(subproject_path))
}) {
anyhow::ensure!(has_prettier_in_node_modules(fs, &path_to_check).await?, "Path {path_to_check:?} is the workspace root for project in {closest_package_json_path:?}, but it has no prettier installed");
log::info!("Found prettier path {path_to_check:?} in the workspace root for project in {closest_package_json_path:?}");
return Ok(ControlFlow::Continue(Some(path_to_check)));
} else {
log::warn!("Skipping path {path_to_check:?} workspace root with workspaces {workspaces:?} that have no prettier installed");
}
},
Some(unknown) => log::error!("Failed to parse workspaces for {path_to_check:?} from package.json, got {unknown:?}. Skipping."),
None => log::warn!("Skipping path {path_to_check:?} that has no prettier dependency and no workspaces section in its package.json"),
}
}
if has_prettier_in_package_json(&package_json_contents) {
if has_prettier_in_node_modules(fs, &path_to_check).await? {
log::debug!("Found prettier path {path_to_check:?} in both package.json and node_modules");
return Ok(ControlFlow::Continue(Some(path_to_check)));
} else if project_path_with_prettier_dependency.is_none() {
project_path_with_prettier_dependency = Some(path_to_check.clone());
}
} else {
match package_json_contents.get("workspaces") {
Some(serde_json::Value::Array(workspaces)) => {
match &project_path_with_prettier_dependency {
Some(project_path_with_prettier_dependency) => {
let subproject_path = project_path_with_prettier_dependency.strip_prefix(&path_to_check).expect("traversing path parents, should be able to strip prefix");
if workspaces.iter().filter_map(|value| {
if let serde_json::Value::String(s) = value {
Some(s.clone())
} else {
log::warn!("Skipping non-string 'workspaces' value: {value:?}");
None
}
}).any(|workspace_definition| {
if let Some(path_matcher) = PathMatcher::new(&[workspace_definition.clone()]).ok() {
path_matcher.is_match(subproject_path)
} else {
workspace_definition == subproject_path.to_string_lossy()
}
}) {
anyhow::ensure!(has_prettier_in_node_modules(fs, &path_to_check).await?, "Found prettier path {path_to_check:?} in the workspace root for project in {project_path_with_prettier_dependency:?}, but it's not installed into workspace root's node_modules");
log::info!("Found prettier path {path_to_check:?} in the workspace root for project in {project_path_with_prettier_dependency:?}");
return Ok(ControlFlow::Continue(Some(path_to_check)));
} else {
log::warn!("Skipping path {path_to_check:?} that has prettier in its 'node_modules' subdirectory, but is not included in its package.json workspaces {workspaces:?}");
}
}
None => {
log::warn!("Skipping path {path_to_check:?} that has prettier in its 'node_modules' subdirectory, but has no prettier in its package.json");
}
}
},
Some(unknown) => log::error!("Failed to parse workspaces for {path_to_check:?} from package.json, got {unknown:?}. Skipping."),
None => log::warn!("Skipping path {path_to_check:?} that has no prettier dependency and no workspaces section in its package.json"),
}
}
}
if !path_to_check.pop() {
log::debug!("Found no prettier in ancestors of {locate_from:?}");
return Ok(ControlFlow::Continue(None));
match project_path_with_prettier_dependency {
Some(closest_prettier_discovered) => {
anyhow::bail!("No prettier found in node_modules for ancestors of {locate_from:?}, but discovered prettier package.json dependency in {closest_prettier_discovered:?}")
}
None => {
log::debug!("Found no prettier in ancestors of {locate_from:?}");
return Ok(ControlFlow::Continue(None));
}
}
}
}
}
@@ -431,6 +448,22 @@ async fn read_package_json(
Ok(None)
}
fn has_prettier_in_package_json(
package_json_contents: &HashMap<String, serde_json::Value>,
) -> bool {
if let Some(serde_json::Value::Object(o)) = package_json_contents.get("dependencies") {
if o.contains_key(PRETTIER_PACKAGE_NAME) {
return true;
}
}
if let Some(serde_json::Value::Object(o)) = package_json_contents.get("devDependencies") {
if o.contains_key(PRETTIER_PACKAGE_NAME) {
return true;
}
}
false
}
enum Format {}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
@@ -515,36 +548,40 @@ mod tests {
)
.await;
assert_eq!(
Prettier::locate_prettier_installation(
fs.as_ref(),
&HashSet::default(),
Path::new("/root/.config/zed/settings.json"),
)
.await
.unwrap(),
ControlFlow::Continue(None),
"Should find no prettier for path hierarchy without it"
assert!(
matches!(
Prettier::locate_prettier_installation(
fs.as_ref(),
&HashSet::default(),
Path::new("/root/.config/zed/settings.json"),
)
.await,
Ok(ControlFlow::Continue(None))
),
"Should successfully find no prettier for path hierarchy without it"
);
assert_eq!(
Prettier::locate_prettier_installation(
fs.as_ref(),
&HashSet::default(),
Path::new("/root/work/project/src/index.js")
)
.await.unwrap(),
ControlFlow::Continue(Some(PathBuf::from("/root/work/project"))),
"Should successfully find a prettier for path hierarchy that has node_modules with prettier, but no package.json mentions of it"
assert!(
matches!(
Prettier::locate_prettier_installation(
fs.as_ref(),
&HashSet::default(),
Path::new("/root/work/project/src/index.js")
)
.await,
Ok(ControlFlow::Continue(None))
),
"Should successfully find no prettier for path hierarchy that has node_modules with prettier, but no package.json mentions of it"
);
assert_eq!(
Prettier::locate_prettier_installation(
fs.as_ref(),
&HashSet::default(),
Path::new("/root/work/project/node_modules/expect/build/print.js")
)
.await
.unwrap(),
ControlFlow::Break(()),
assert!(
matches!(
Prettier::locate_prettier_installation(
fs.as_ref(),
&HashSet::default(),
Path::new("/root/work/project/node_modules/expect/build/print.js")
)
.await,
Ok(ControlFlow::Break(()))
),
"Should not format files inside node_modules/"
);
}
@@ -654,17 +691,18 @@ mod tests {
)
.await;
assert_eq!(
Prettier::locate_prettier_installation(
fs.as_ref(),
&HashSet::default(),
Path::new("/root/work/web_blog/pages/[slug].tsx")
)
.await
.unwrap(),
ControlFlow::Continue(None),
"Should find no prettier when node_modules don't have it"
);
match Prettier::locate_prettier_installation(
fs.as_ref(),
&HashSet::default(),
Path::new("/root/work/web_blog/pages/[slug].tsx")
)
.await {
Ok(path) => panic!("Expected to fail for prettier in package.json but not in node_modules found, but got path {path:?}"),
Err(e) => {
let message = e.to_string();
assert!(message.contains("/root/work/web_blog"), "Error message should mention which project had prettier defined");
},
};
assert_eq!(
Prettier::locate_prettier_installation(

View File

@@ -2471,7 +2471,7 @@ impl LspCommand for InlayHints {
lsp_adapter.name.0.as_ref() == "typescript-language-server";
let hints = message.unwrap_or_default().into_iter().map(|lsp_hint| {
let resolve_state = if InlayHints::can_resolve_inlays(&lsp_server.capabilities()) {
let resolve_state = if InlayHints::can_resolve_inlays(lsp_server.capabilities()) {
ResolveState::CanResolve(lsp_server.server_id(), lsp_hint.data.clone())
} else {
ResolveState::Resolved

View File

@@ -355,6 +355,9 @@ pub enum Event {
},
CollaboratorJoined(proto::PeerId),
CollaboratorLeft(proto::PeerId),
HostReshared,
Reshared,
Rejoined,
RefreshInlayHints,
RevealInProjectPanel(ProjectEntryId),
SnippetEdit(BufferId, Vec<(lsp::Range, Snippet)>),
@@ -1716,6 +1719,7 @@ impl Project {
self.shared_buffers.clear();
self.set_collaborators_from_proto(message.collaborators, cx)?;
self.metadata_changed(cx);
cx.emit(Event::Reshared);
Ok(())
}
@@ -1753,6 +1757,7 @@ impl Project {
.collect();
self.enqueue_buffer_ordered_message(BufferOrderedMessage::Resync)
.unwrap();
cx.emit(Event::Rejoined);
cx.notify();
Ok(())
}
@@ -1805,9 +1810,11 @@ impl Project {
}
}
self.client.send(proto::UnshareProject {
project_id: remote_id,
})?;
self.client
.send(proto::UnshareProject {
project_id: remote_id,
})
.ok();
Ok(())
} else {
@@ -2853,21 +2860,15 @@ impl Project {
};
for (_, _, server) in self.language_servers_for_worktree(worktree_id) {
if let Some(include_text) = include_text(server.as_ref()) {
let text = if include_text {
Some(buffer.read(cx).text())
} else {
None
};
server
.notify::<lsp::notification::DidSaveTextDocument>(
lsp::DidSaveTextDocumentParams {
text_document: text_document.clone(),
text,
},
)
.log_err();
}
let text = include_text(server.as_ref()).then(|| buffer.read(cx).text());
server
.notify::<lsp::notification::DidSaveTextDocument>(
lsp::DidSaveTextDocumentParams {
text_document: text_document.clone(),
text,
},
)
.log_err();
}
for language_server_id in self.language_server_ids_for_buffer(buffer.read(cx), cx) {
@@ -3502,7 +3503,7 @@ impl Project {
}
async fn setup_pending_language_server(
project: WeakModel<Self>,
this: WeakModel<Self>,
override_options: Option<serde_json::Value>,
pending_server: PendingLanguageServer,
delegate: Arc<dyn LspAdapterDelegate>,
@@ -3521,7 +3522,7 @@ impl Project {
language_server
.on_notification::<lsp::notification::PublishDiagnostics, _>({
let adapter = adapter.clone();
let this = project.clone();
let this = this.clone();
move |mut params, mut cx| {
let adapter = adapter.clone();
if let Some(this) = this.upgrade() {
@@ -3575,7 +3576,7 @@ impl Project {
// to these requests when initializing.
language_server
.on_request::<lsp::request::WorkDoneProgressCreate, _, _>({
let this = project.clone();
let this = this.clone();
move |params, mut cx| {
let this = this.clone();
async move {
@@ -3596,103 +3597,20 @@ impl Project {
language_server
.on_request::<lsp::request::RegisterCapability, _, _>({
let project = project.clone();
let this = this.clone();
move |params, mut cx| {
let project = project.clone();
let this = this.clone();
async move {
for reg in params.registrations {
match reg.method.as_str() {
"workspace/didChangeWatchedFiles" => {
if let Some(options) = reg.register_options {
let options = serde_json::from_value(options)?;
project.update(&mut cx, |project, cx| {
project.on_lsp_did_change_watched_files(
server_id, &reg.id, options, cx,
);
})?;
}
if reg.method == "workspace/didChangeWatchedFiles" {
if let Some(options) = reg.register_options {
let options = serde_json::from_value(options)?;
this.update(&mut cx, |this, cx| {
this.on_lsp_did_change_watched_files(
server_id, &reg.id, options, cx,
);
})?;
}
"textDocument/rangeFormatting" => {
project.update(&mut cx, |project, _| {
if let Some(server) =
project.language_server_for_id(server_id)
{
let options = reg
.register_options
.map(|options| {
serde_json::from_value::<
lsp::DocumentRangeFormattingOptions,
>(
options
)
})
.transpose()?;
let provider = match options {
None => OneOf::Left(true),
Some(options) => OneOf::Right(options),
};
server.update_capabilities(|capabilities| {
capabilities.document_range_formatting_provider =
Some(provider);
})
}
anyhow::Ok(())
})??;
}
"textDocument/onTypeFormatting" => {
project.update(&mut cx, |project, _| {
if let Some(server) =
project.language_server_for_id(server_id)
{
let options = reg
.register_options
.map(|options| {
serde_json::from_value::<
lsp::DocumentOnTypeFormattingOptions,
>(
options
)
})
.transpose()?;
if let Some(options) = options {
server.update_capabilities(|capabilities| {
capabilities
.document_on_type_formatting_provider =
Some(options);
})
}
}
anyhow::Ok(())
})??;
}
"textDocument/formatting" => {
project.update(&mut cx, |project, _| {
if let Some(server) =
project.language_server_for_id(server_id)
{
let options = reg
.register_options
.map(|options| {
serde_json::from_value::<
lsp::DocumentFormattingOptions,
>(
options
)
})
.transpose()?;
let provider = match options {
None => OneOf::Left(true),
Some(options) => OneOf::Right(options),
};
server.update_capabilities(|capabilities| {
capabilities.document_formatting_provider =
Some(provider);
})
}
anyhow::Ok(())
})??;
}
_ => log::warn!("unhandled capability registration: {reg:?}"),
}
}
Ok(())
@@ -3703,55 +3621,17 @@ impl Project {
language_server
.on_request::<lsp::request::UnregisterCapability, _, _>({
let this = project.clone();
let this = this.clone();
move |params, mut cx| {
let project = this.clone();
let this = this.clone();
async move {
for unreg in params.unregisterations.iter() {
match unreg.method.as_str() {
"workspace/didChangeWatchedFiles" => {
project.update(&mut cx, |project, cx| {
project.on_lsp_unregister_did_change_watched_files(
server_id, &unreg.id, cx,
);
})?;
}
"textDocument/rangeFormatting" => {
project.update(&mut cx, |project, _| {
if let Some(server) =
project.language_server_for_id(server_id)
{
server.update_capabilities(|capabilities| {
capabilities.document_range_formatting_provider =
None
})
}
})?;
}
"textDocument/onTypeFormatting" => {
project.update(&mut cx, |project, _| {
if let Some(server) =
project.language_server_for_id(server_id)
{
server.update_capabilities(|capabilities| {
capabilities.document_on_type_formatting_provider =
None;
})
}
})?;
}
"textDocument/formatting" => {
project.update(&mut cx, |project, _| {
if let Some(server) =
project.language_server_for_id(server_id)
{
server.update_capabilities(|capabilities| {
capabilities.document_formatting_provider = None;
})
}
})?;
}
_ => log::warn!("unhandled capability unregistration: {unreg:?}"),
if unreg.method == "workspace/didChangeWatchedFiles" {
this.update(&mut cx, |this, cx| {
this.on_lsp_unregister_did_change_watched_files(
server_id, &unreg.id, cx,
);
})?;
}
}
Ok(())
@@ -3763,7 +3643,7 @@ impl Project {
language_server
.on_request::<lsp::request::ApplyWorkspaceEdit, _, _>({
let adapter = adapter.clone();
let this = project.clone();
let this = this.clone();
move |params, cx| {
Self::on_lsp_workspace_edit(
this.clone(),
@@ -3778,7 +3658,7 @@ impl Project {
language_server
.on_request::<lsp::request::InlayHintRefreshRequest, _, _>({
let this = project.clone();
let this = this.clone();
move |(), mut cx| {
let this = this.clone();
async move {
@@ -3797,7 +3677,7 @@ impl Project {
language_server
.on_request::<lsp::request::ShowMessageRequest, _, _>({
let this = project.clone();
let this = this.clone();
let name = name.to_string();
move |params, mut cx| {
let this = this.clone();
@@ -3836,7 +3716,7 @@ impl Project {
language_server
.on_notification::<ServerStatus, _>({
let this = project.clone();
let this = this.clone();
let name = name.to_string();
move |params, mut cx| {
let this = this.clone();
@@ -3879,7 +3759,7 @@ impl Project {
.detach();
language_server
.on_notification::<lsp::notification::ShowMessage, _>({
let this = project.clone();
let this = this.clone();
let name = name.to_string();
move |params, mut cx| {
let this = this.clone();
@@ -3906,7 +3786,7 @@ impl Project {
.detach();
language_server
.on_notification::<lsp::notification::Progress, _>(move |params, mut cx| {
if let Some(this) = project.upgrade() {
if let Some(this) = this.upgrade() {
this.update(&mut cx, |this, cx| {
this.on_lsp_progress(
params,
@@ -4226,6 +4106,7 @@ impl Project {
return;
}
#[allow(clippy::mutable_key_type)]
let language_server_lookup_info: HashSet<(Model<Worktree>, Arc<Language>)> = buffers
.into_iter()
.filter_map(|buffer| {
@@ -7153,7 +7034,7 @@ impl Project {
} else {
return Task::ready(Ok(hint));
};
if !InlayHints::can_resolve_inlays(&lang_server.capabilities()) {
if !InlayHints::can_resolve_inlays(lang_server.capabilities()) {
return Task::ready(Ok(hint));
}
@@ -7565,7 +7446,7 @@ impl Project {
let lsp_params = request.to_lsp(&file.abs_path(cx), buffer, &language_server, cx);
let status = request.status();
return cx.spawn(move |this, cx| async move {
if !request.check_capabilities(&language_server.capabilities()) {
if !request.check_capabilities(language_server.capabilities()) {
return Ok(Default::default());
}
@@ -7659,7 +7540,7 @@ impl Project {
let scope = position.and_then(|position| snapshot.language_scope_at(position));
let mut response_results = self
.language_servers_for_buffer(buffer.read(cx), cx)
.filter(|(_, server)| server_capabilities_check(&server.capabilities()))
.filter(|(_, server)| server_capabilities_check(server.capabilities()))
.filter(|(adapter, _)| {
scope
.as_ref()
@@ -8937,6 +8818,7 @@ impl Project {
.retain(|_, buffer| !matches!(buffer, OpenBuffer::Operations(_)));
this.enqueue_buffer_ordered_message(BufferOrderedMessage::Resync)
.unwrap();
cx.emit(Event::HostReshared);
}
cx.emit(Event::CollaboratorUpdated {
@@ -11185,6 +11067,7 @@ async fn populate_labels_for_symbols(
lsp_adapter: Option<Arc<CachedLspAdapter>>,
output: &mut Vec<Symbol>,
) {
#[allow(clippy::mutable_key_type)]
let mut symbols_by_language = HashMap::<Option<Arc<Language>>, Vec<CoreSymbol>>::default();
let mut unknown_path = None;
@@ -11840,27 +11723,20 @@ fn is_not_found_error(error: &anyhow::Error) -> bool {
.is_some_and(|err| err.kind() == io::ErrorKind::NotFound)
}
fn include_text(server: &lsp::LanguageServer) -> Option<bool> {
match server.capabilities().text_document_sync.as_ref()? {
lsp::TextDocumentSyncCapability::Kind(kind) => match kind {
&lsp::TextDocumentSyncKind::NONE => None,
&lsp::TextDocumentSyncKind::FULL => Some(true),
&lsp::TextDocumentSyncKind::INCREMENTAL => Some(false),
_ => None,
},
lsp::TextDocumentSyncCapability::Options(options) => match options.save.as_ref()? {
lsp::TextDocumentSyncSaveOptions::Supported(supported) => {
if *supported {
Some(true)
} else {
None
}
}
lsp::TextDocumentSyncSaveOptions::SaveOptions(save_options) => {
Some(save_options.include_text.unwrap_or(false))
}
},
}
fn include_text(server: &lsp::LanguageServer) -> bool {
server
.capabilities()
.text_document_sync
.as_ref()
.and_then(|sync| match sync {
lsp::TextDocumentSyncCapability::Kind(_) => None,
lsp::TextDocumentSyncCapability::Options(options) => options.save.as_ref(),
})
.and_then(|save_options| match save_options {
lsp::TextDocumentSyncSaveOptions::Supported(_) => None,
lsp::TextDocumentSyncSaveOptions::SaveOptions(options) => options.include_text,
})
.unwrap_or(false)
}
async fn load_shell_environment(dir: &Path) -> Result<HashMap<String, String>> {

View File

@@ -322,12 +322,6 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
trigger_characters: Some(vec![".".to_string(), "::".to_string()]),
..Default::default()
}),
text_document_sync: Some(lsp::TextDocumentSyncCapability::Options(
lsp::TextDocumentSyncOptions {
save: Some(lsp::TextDocumentSyncSaveOptions::Supported(true)),
..Default::default()
},
)),
..Default::default()
},
..Default::default()
@@ -342,12 +336,6 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
trigger_characters: Some(vec![":".to_string()]),
..Default::default()
}),
text_document_sync: Some(lsp::TextDocumentSyncCapability::Options(
lsp::TextDocumentSyncOptions {
save: Some(lsp::TextDocumentSyncSaveOptions::Supported(true)),
..Default::default()
},
)),
..Default::default()
},
..Default::default()

View File

@@ -444,11 +444,6 @@ mod test_inventory {
use super::{task_source_kind_preference, TaskSourceKind, UnboundedSender};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TestTask {
name: String,
}
pub(super) fn static_test_source(
task_names: impl IntoIterator<Item = String>,
updates: UnboundedSender<()>,

View File

@@ -1,7 +1,7 @@
fn main() {
let mut build = prost_build::Config::new();
build
.type_attribute(".", "#[derive(serde::Serialize)]")
.type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]")
.compile_protos(&["proto/zed.proto"], &["proto"])
.unwrap();
}

View File

@@ -255,7 +255,14 @@ message Envelope {
TaskTemplates task_templates = 206;
LinkedEditingRange linked_editing_range = 209;
LinkedEditingRangeResponse linked_editing_range_response = 210; // current max
LinkedEditingRangeResponse linked_editing_range_response = 210;
AdvertiseContexts advertise_contexts = 211;
OpenContext open_context = 212;
OpenContextResponse open_context_response = 213;
UpdateContext update_context = 214;
SynchronizeContexts synchronize_contexts = 215;
SynchronizeContextsResponse synchronize_contexts_response = 216; // current max
}
reserved 158 to 161;
@@ -2222,3 +2229,117 @@ message TaskSourceKind {
string name = 1;
}
}
message ContextMessageStatus {
oneof variant {
Done done = 1;
Pending pending = 2;
Error error = 3;
}
message Done {}
message Pending {}
message Error {
string message = 1;
}
}
message ContextMessage {
LamportTimestamp id = 1;
Anchor start = 2;
LanguageModelRole role = 3;
ContextMessageStatus status = 4;
}
message SlashCommandOutputSection {
AnchorRange range = 1;
string icon_name = 2;
string label = 3;
}
message ContextOperation {
oneof variant {
InsertMessage insert_message = 1;
UpdateMessage update_message = 2;
UpdateSummary update_summary = 3;
SlashCommandFinished slash_command_finished = 4;
BufferOperation buffer_operation = 5;
}
message InsertMessage {
ContextMessage message = 1;
repeated VectorClockEntry version = 2;
}
message UpdateMessage {
LamportTimestamp message_id = 1;
LanguageModelRole role = 2;
ContextMessageStatus status = 3;
LamportTimestamp timestamp = 4;
repeated VectorClockEntry version = 5;
}
message UpdateSummary {
string summary = 1;
bool done = 2;
LamportTimestamp timestamp = 3;
repeated VectorClockEntry version = 4;
}
message SlashCommandFinished {
LamportTimestamp id = 1;
AnchorRange output_range = 2;
repeated SlashCommandOutputSection sections = 3;
repeated VectorClockEntry version = 4;
}
message BufferOperation {
Operation operation = 1;
}
}
message Context {
repeated ContextOperation operations = 1;
}
message ContextMetadata {
string context_id = 1;
optional string summary = 2;
}
message AdvertiseContexts {
uint64 project_id = 1;
repeated ContextMetadata contexts = 2;
}
message OpenContext {
uint64 project_id = 1;
string context_id = 2;
}
message OpenContextResponse {
Context context = 1;
}
message UpdateContext {
uint64 project_id = 1;
string context_id = 2;
ContextOperation operation = 3;
}
message ContextVersion {
string context_id = 1;
repeated VectorClockEntry context_version = 2;
repeated VectorClockEntry buffer_version = 3;
}
message SynchronizeContexts {
uint64 project_id = 1;
repeated ContextVersion contexts = 2;
}
message SynchronizeContextsResponse {
repeated ContextVersion contexts = 1;
}

View File

@@ -8,7 +8,7 @@ pub use error::*;
pub use typed_envelope::*;
use collections::HashMap;
pub use prost::Message;
pub use prost::{DecodeError, Message};
use serde::Serialize;
use std::any::{Any, TypeId};
use std::time::Instant;
@@ -337,7 +337,13 @@ messages!(
(OpenNewBuffer, Foreground),
(RestartLanguageServers, Foreground),
(LinkedEditingRange, Background),
(LinkedEditingRangeResponse, Background)
(LinkedEditingRangeResponse, Background),
(AdvertiseContexts, Foreground),
(OpenContext, Foreground),
(OpenContextResponse, Foreground),
(UpdateContext, Foreground),
(SynchronizeContexts, Foreground),
(SynchronizeContextsResponse, Foreground),
);
request_messages!(
@@ -449,7 +455,9 @@ request_messages!(
(DeleteDevServerProject, Ack),
(RegenerateDevServerToken, RegenerateDevServerTokenResponse),
(RenameDevServer, Ack),
(RestartLanguageServers, Ack)
(RestartLanguageServers, Ack),
(OpenContext, OpenContextResponse),
(SynchronizeContexts, SynchronizeContextsResponse),
);
entity_messages!(
@@ -511,6 +519,10 @@ entity_messages!(
UpdateWorktree,
UpdateWorktreeSettings,
LspExtExpandMacro,
AdvertiseContexts,
OpenContext,
UpdateContext,
SynchronizeContexts,
);
entity_messages!(

View File

@@ -20,6 +20,7 @@ search.workspace = true
settings.workspace = true
ui.workspace = true
workspace.workspace = true
repl.workspace = true
[dev-dependencies]
editor = { workspace = true, features = ["test-support"] }

View File

@@ -20,8 +20,11 @@ use workspace::{
item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
};
mod repl_menu;
pub struct QuickActionBar {
buffer_search_bar: View<BufferSearchBar>,
repl_menu: Option<View<ContextMenu>>,
toggle_settings_menu: Option<View<ContextMenu>>,
toggle_selections_menu: Option<View<ContextMenu>>,
active_item: Option<Box<dyn ItemHandle>>,
@@ -40,6 +43,7 @@ impl QuickActionBar {
buffer_search_bar,
toggle_settings_menu: None,
toggle_selections_menu: None,
repl_menu: None,
active_item: None,
_inlay_hints_enabled_subscription: None,
workspace: workspace.weak_handle(),
@@ -290,9 +294,13 @@ impl Render for QuickActionBar {
.child(
h_flex()
.gap(Spacing::Medium.rems(cx))
.children(self.render_repl_menu(cx))
.children(editor_selections_dropdown)
.child(editor_settings_dropdown),
)
.when_some(self.repl_menu.as_ref(), |el, repl_menu| {
el.child(Self::render_menu_overlay(repl_menu))
})
.when_some(
self.toggle_settings_menu.as_ref(),
|el, toggle_settings_menu| {

View File

@@ -0,0 +1,116 @@
use gpui::AnyElement;
use repl::{
ExecutionState, JupyterSettings, Kernel, KernelSpecification, RuntimePanel, Session,
SessionSupport,
};
use ui::{prelude::*, ButtonLike, IconWithIndicator, IntoElement, Tooltip};
use crate::QuickActionBar;
const ZED_REPL_DOCUMENTATION: &str = "https://zed.dev/docs/repl";
impl QuickActionBar {
pub fn render_repl_menu(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
if !JupyterSettings::enabled(cx) {
return None;
}
let workspace = self.workspace.upgrade()?.read(cx);
let (editor, repl_panel) = if let (Some(editor), Some(repl_panel)) =
(self.active_editor(), workspace.panel::<RuntimePanel>(cx))
{
(editor, repl_panel)
} else {
return None;
};
let session = repl_panel.update(cx, |repl_panel, cx| {
repl_panel.session(editor.downgrade(), cx)
});
let session = match session {
SessionSupport::ActiveSession(session) => session.read(cx),
SessionSupport::Inactive(spec) => {
return self.render_repl_launch_menu(spec, cx);
}
SessionSupport::RequiresSetup(language) => {
return self.render_repl_setup(&language, cx);
}
SessionSupport::Unsupported => return None,
};
let kernel_name: SharedString = session.kernel_specification.name.clone().into();
let kernel_language: SharedString = session
.kernel_specification
.kernelspec
.language
.clone()
.into();
let tooltip = |session: &Session| match &session.kernel {
Kernel::RunningKernel(kernel) => match &kernel.execution_state {
ExecutionState::Idle => {
format!("Run code on {} ({})", kernel_name, kernel_language)
}
ExecutionState::Busy => format!("Interrupt {} ({})", kernel_name, kernel_language),
},
Kernel::StartingKernel(_) => format!("{} is starting", kernel_name),
Kernel::ErroredLaunch(e) => format!("Error with kernel {}: {}", kernel_name, e),
Kernel::ShuttingDown => format!("{} is shutting down", kernel_name),
Kernel::Shutdown => "Nothing running".to_string(),
};
let tooltip_text: SharedString = SharedString::from(tooltip(&session).clone());
let button = ButtonLike::new("toggle_repl_icon")
.child(
IconWithIndicator::new(Icon::new(IconName::Play), Some(session.kernel.dot()))
.indicator_border_color(Some(cx.theme().colors().border)),
)
.size(ButtonSize::Compact)
.style(ButtonStyle::Subtle)
.tooltip(move |cx| Tooltip::text(tooltip_text.clone(), cx))
.on_click(|_, cx| cx.dispatch_action(Box::new(repl::Run {})))
.on_click(|_, cx| cx.dispatch_action(Box::new(repl::Run {})))
.into_any_element();
Some(button)
}
pub fn render_repl_launch_menu(
&self,
kernel_specification: KernelSpecification,
_cx: &mut ViewContext<Self>,
) -> Option<AnyElement> {
let tooltip: SharedString =
SharedString::from(format!("Start REPL for {}", kernel_specification.name));
Some(
IconButton::new("toggle_repl_icon", IconName::Play)
.size(ButtonSize::Compact)
.icon_color(Color::Muted)
.style(ButtonStyle::Subtle)
.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
.on_click(|_, cx| cx.dispatch_action(Box::new(repl::Run {})))
.into_any_element(),
)
}
pub fn render_repl_setup(
&self,
language: &str,
_cx: &mut ViewContext<Self>,
) -> Option<AnyElement> {
let tooltip: SharedString = SharedString::from(format!("Setup Zed REPL for {}", language));
Some(
IconButton::new("toggle_repl_icon", IconName::Play)
.size(ButtonSize::Compact)
.icon_color(Color::Muted)
.style(ButtonStyle::Subtle)
.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
.on_click(|_, cx| cx.open_url(ZED_REPL_DOCUMENTATION))
.into_any_element(),
)
}
}

View File

@@ -82,7 +82,7 @@ pub enum Kernel {
}
impl Kernel {
pub fn dot(&mut self) -> Indicator {
pub fn dot(&self) -> Indicator {
match self {
Kernel::RunningKernel(kernel) => match kernel.execution_state {
ExecutionState::Idle => Indicator::dot().color(Color::Success),

View File

@@ -11,7 +11,11 @@ mod session;
mod stdio;
pub use jupyter_settings::JupyterSettings;
pub use runtime_panel::RuntimePanel;
pub use kernels::{Kernel, KernelSpecification};
pub use runtime_panel::Run;
pub use runtime_panel::{RuntimePanel, SessionSupport};
pub use runtimelib::ExecutionState;
pub use session::Session;
fn zed_dispatcher(cx: &mut AppContext) -> impl Dispatcher {
struct ZedDispatcher {

View File

@@ -241,6 +241,17 @@ impl RuntimePanel {
Some((selected_text, language_name, anchor_range))
}
pub fn language(
&self,
editor: WeakView<Editor>,
cx: &mut ViewContext<Self>,
) -> Option<Arc<str>> {
match self.snippet(editor, cx) {
Some((_, language, _)) => Some(language),
None => None,
}
}
pub fn refresh_kernelspecs(&mut self, cx: &mut ViewContext<Self>) -> Task<anyhow::Result<()>> {
let kernel_specifications = kernel_specifications(self.fs.clone());
cx.spawn(|this, mut cx| async move {
@@ -336,6 +347,50 @@ impl RuntimePanel {
}
}
pub enum SessionSupport {
ActiveSession(View<Session>),
Inactive(KernelSpecification),
RequiresSetup(String),
Unsupported,
}
impl RuntimePanel {
pub fn session(
&mut self,
editor: WeakView<Editor>,
cx: &mut ViewContext<Self>,
) -> SessionSupport {
let entity_id = editor.entity_id();
let session = self.sessions.get(&entity_id).cloned();
match session {
Some(session) => SessionSupport::ActiveSession(session),
None => {
let language = self.language(editor, cx);
let language = match language {
Some(language) => language,
None => return SessionSupport::Unsupported,
};
// Check for kernelspec
let kernelspec = self.kernelspec(&language, cx);
match kernelspec {
Some(kernelspec) => SessionSupport::Inactive(kernelspec),
None => {
let language: String = language.to_lowercase();
// If no kernelspec but language is one of typescript, python, r, or julia
// then we return RequiresSetup
match language.as_str() {
"typescript" | "python" => SessionSupport::RequiresSetup(language),
_ => SessionSupport::Unsupported,
}
}
}
}
}
}
}
impl Panel for RuntimePanel {
fn persistent_name() -> &'static str {
"RuntimePanel"

View File

@@ -22,11 +22,11 @@ use theme::{ActiveTheme, ThemeSettings};
use ui::{h_flex, prelude::*, v_flex, ButtonLike, ButtonStyle, Label};
pub struct Session {
editor: WeakView<Editor>,
kernel: Kernel,
pub editor: WeakView<Editor>,
pub kernel: Kernel,
blocks: HashMap<String, EditorBlock>,
messaging_task: Task<()>,
kernel_specification: KernelSpecification,
pub messaging_task: Task<()>,
pub kernel_specification: KernelSpecification,
}
struct EditorBlock {
@@ -310,7 +310,7 @@ impl Session {
}
}
fn interrupt(&mut self, cx: &mut ViewContext<Self>) {
pub fn interrupt(&mut self, cx: &mut ViewContext<Self>) {
match &mut self.kernel {
Kernel::RunningKernel(_kernel) => {
self.send(InterruptRequest {}.into(), cx).ok();
@@ -322,7 +322,7 @@ impl Session {
}
}
fn shutdown(&mut self, cx: &mut ViewContext<Self>) {
pub fn shutdown(&mut self, cx: &mut ViewContext<Self>) {
let kernel = std::mem::replace(&mut self.kernel, Kernel::ShuttingDown);
match kernel {

View File

@@ -29,7 +29,7 @@ picker.workspace = true
project.workspace = true
rust-embed.workspace = true
settings.workspace = true
simplelog.workspace = true
simplelog = "0.9"
story.workspace = true
strum = { version = "0.25.0", features = ["derive"] }
theme.workspace = true

View File

@@ -1,12 +1,15 @@
use std::fmt::Debug;
use clock::ReplicaId;
use collections::{BTreeMap, HashSet};
pub struct Network<T: Clone, R: rand::Rng> {
inboxes: std::collections::BTreeMap<ReplicaId, Vec<Envelope<T>>>,
all_messages: Vec<T>,
inboxes: BTreeMap<ReplicaId, Vec<Envelope<T>>>,
disconnected_peers: HashSet<ReplicaId>,
rng: R,
}
#[derive(Clone)]
#[derive(Clone, Debug)]
struct Envelope<T: Clone> {
message: T,
}
@@ -14,8 +17,8 @@ struct Envelope<T: Clone> {
impl<T: Clone, R: rand::Rng> Network<T, R> {
pub fn new(rng: R) -> Self {
Network {
inboxes: Default::default(),
all_messages: Vec::new(),
inboxes: BTreeMap::default(),
disconnected_peers: HashSet::default(),
rng,
}
}
@@ -24,6 +27,24 @@ impl<T: Clone, R: rand::Rng> Network<T, R> {
self.inboxes.insert(id, Vec::new());
}
pub fn disconnect_peer(&mut self, id: ReplicaId) {
self.disconnected_peers.insert(id);
self.inboxes.get_mut(&id).unwrap().clear();
}
pub fn reconnect_peer(&mut self, id: ReplicaId, replicate_from: ReplicaId) {
assert!(self.disconnected_peers.remove(&id));
self.replicate(replicate_from, id);
}
pub fn is_disconnected(&self, id: ReplicaId) -> bool {
self.disconnected_peers.contains(&id)
}
pub fn contains_disconnected_peers(&self) -> bool {
!self.disconnected_peers.is_empty()
}
pub fn replicate(&mut self, old_replica_id: ReplicaId, new_replica_id: ReplicaId) {
self.inboxes
.insert(new_replica_id, self.inboxes[&old_replica_id].clone());
@@ -34,8 +55,13 @@ impl<T: Clone, R: rand::Rng> Network<T, R> {
}
pub fn broadcast(&mut self, sender: ReplicaId, messages: Vec<T>) {
// Drop messages from disconnected peers.
if self.disconnected_peers.contains(&sender) {
return;
}
for (replica, inbox) in self.inboxes.iter_mut() {
if *replica != sender {
if *replica != sender && !self.disconnected_peers.contains(replica) {
for message in &messages {
// Insert one or more duplicates of this message, potentially *before* the previous
// message sent by this peer to simulate out-of-order delivery.
@@ -51,7 +77,6 @@ impl<T: Clone, R: rand::Rng> Network<T, R> {
}
}
}
self.all_messages.extend(messages);
}
pub fn has_unreceived(&self, receiver: ReplicaId) -> bool {

View File

@@ -1265,6 +1265,10 @@ impl Buffer {
}
}
pub fn has_deferred_ops(&self) -> bool {
!self.deferred_ops.is_empty()
}
pub fn peek_undo_stack(&self) -> Option<&HistoryEntry> {
self.history.undo_stack.last()
}

View File

@@ -20,7 +20,7 @@ schemars = { workspace = true, features = ["indexmap"] }
serde.workspace = true
serde_json.workspace = true
serde_json_lenient.workspace = true
simplelog.workspace= true
simplelog = "0.9"
strum = { version = "0.25.0", features = ["derive"] }
theme.workspace = true
vscode_theme = "0.2.0"

View File

@@ -12,7 +12,6 @@ use indexmap::IndexMap;
use log::LevelFilter;
use schemars::schema_for;
use serde::Deserialize;
use simplelog::ColorChoice;
use simplelog::{TermLogger, TerminalMode};
use theme::{Appearance, AppearanceContent, ThemeFamilyContent};
@@ -95,6 +94,11 @@ fn main() -> Result<()> {
let log_config = {
let mut config = simplelog::ConfigBuilder::new();
config
.set_level_color(log::Level::Trace, simplelog::Color::Cyan)
.set_level_color(log::Level::Info, simplelog::Color::Blue)
.set_level_color(log::Level::Warn, simplelog::Color::Yellow)
.set_level_color(log::Level::Error, simplelog::Color::Red);
if !args.warn_on_missing {
config.add_filter_ignore_str("theme_printer");
@@ -103,13 +107,8 @@ fn main() -> Result<()> {
config.build()
};
TermLogger::init(
LevelFilter::Trace,
log_config,
TerminalMode::Mixed,
ColorChoice::Auto,
)
.expect("could not initialize logger");
TermLogger::init(LevelFilter::Trace, log_config, TerminalMode::Mixed)
.expect("could not initialize logger");
if let Some(command) = args.command {
match command {

View File

@@ -1,6 +1,6 @@
use gpui::{svg, AnimationElement, Hsla, IntoElement, Rems, Transformation};
use serde::{Deserialize, Serialize};
use strum::EnumIter;
use strum::{EnumIter, EnumString, IntoStaticStr};
use crate::{prelude::*, Indicator};
@@ -90,7 +90,9 @@ impl IconSize {
}
}
#[derive(Debug, PartialEq, Copy, Clone, EnumIter, Serialize, Deserialize)]
#[derive(
Debug, Eq, PartialEq, Copy, Clone, EnumIter, EnumString, IntoStaticStr, Serialize, Deserialize,
)]
pub enum IconName {
Ai,
ArrowCircle,
@@ -158,11 +160,11 @@ pub enum IconName {
Font,
FontSize,
FontWeight,
Github,
GenericMinimize,
GenericMaximize,
GenericClose,
GenericMaximize,
GenericMinimize,
GenericRestore,
Github,
Hash,
HistoryRerun,
Indicator,
@@ -192,6 +194,10 @@ pub enum IconName {
PullRequest,
Quote,
Regex,
ReplPlay,
ReplOff,
ReplPause,
ReplNeutral,
Replace,
ReplaceAll,
ReplaceNext,
@@ -229,12 +235,12 @@ pub enum IconName {
Trash,
TriangleRight,
Update,
Visible,
WholeWord,
XCircle,
ZedAssistant,
ZedAssistantFilled,
ZedXCopilot,
Visible,
}
impl IconName {
@@ -306,11 +312,11 @@ impl IconName {
IconName::Font => "icons/font.svg",
IconName::FontSize => "icons/font_size.svg",
IconName::FontWeight => "icons/font_weight.svg",
IconName::Github => "icons/github.svg",
IconName::GenericMinimize => "icons/generic_minimize.svg",
IconName::GenericMaximize => "icons/generic_maximize.svg",
IconName::GenericClose => "icons/generic_close.svg",
IconName::GenericMaximize => "icons/generic_maximize.svg",
IconName::GenericMinimize => "icons/generic_minimize.svg",
IconName::GenericRestore => "icons/generic_restore.svg",
IconName::Github => "icons/github.svg",
IconName::Hash => "icons/hash.svg",
IconName::HistoryRerun => "icons/history_rerun.svg",
IconName::Indicator => "icons/indicator.svg",
@@ -340,6 +346,10 @@ impl IconName {
IconName::PullRequest => "icons/pull_request.svg",
IconName::Quote => "icons/quote.svg",
IconName::Regex => "icons/regex.svg",
IconName::ReplPlay => "icons/repl_play.svg",
IconName::ReplPause => "icons/repl_pause.svg",
IconName::ReplNeutral => "icons/repl_neutral.svg",
IconName::ReplOff => "icons/repl_off.svg",
IconName::Replace => "icons/replace.svg",
IconName::ReplaceAll => "icons/replace_all.svg",
IconName::ReplaceNext => "icons/replace_next.svg",
@@ -377,12 +387,12 @@ impl IconName {
IconName::Trash => "icons/trash.svg",
IconName::TriangleRight => "icons/triangle_right.svg",
IconName::Update => "icons/update.svg",
IconName::Visible => "icons/visible.svg",
IconName::WholeWord => "icons/word_search.svg",
IconName::XCircle => "icons/error.svg",
IconName::ZedAssistant => "icons/zed_assistant.svg",
IconName::ZedAssistantFilled => "icons/zed_assistant_filled.svg",
IconName::ZedXCopilot => "icons/zed_x_copilot.svg",
IconName::Visible => "icons/visible.svg",
}
}
}

View File

@@ -123,22 +123,20 @@ impl Render for WelcomePage {
.ok();
})),
)
.when(cfg!(target_os = "macos"), |el| {
el.child(
Button::new("install-cli", "Install the CLI")
.full_width()
.on_click(cx.listener(|this, _, cx| {
this.telemetry.report_app_event(
"welcome page: install cli".to_string(),
);
cx.app_mut()
.spawn(|cx| async move {
install_cli::install_cli(&cx).await
})
.detach_and_log_err(cx);
})),
)
})
.child(
Button::new("install-cli", "Install the CLI")
.full_width()
.on_click(cx.listener(|this, _, cx| {
this.telemetry.report_app_event(
"welcome page: install cli".to_string(),
);
cx.app_mut()
.spawn(|cx| async move {
install_cli::install_cli(&cx).await
})
.detach_and_log_err(cx);
})),
)
.child(
Button::new("sign-in-to-copilot", "Sign in to GitHub Copilot")
.full_width()

View File

@@ -160,23 +160,14 @@ impl Workspace {
self.show_notification(
NotificationId::unique::<WorkspaceErrorNotification>(),
cx,
|cx| cx.new_view(|_cx| ErrorMessagePrompt::new(format!("Error: {err:#}"))),
|cx| {
cx.new_view(|_cx| {
simple_message_notification::MessageNotification::new(format!("Error: {err:#}"))
})
},
);
}
pub fn show_portal_error(&mut self, err: String, cx: &mut ViewContext<Self>) {
struct PortalError;
self.show_notification(NotificationId::unique::<PortalError>(), cx, |cx| {
cx.new_view(|_cx| {
ErrorMessagePrompt::new(err.to_string()).with_link_button(
"See docs",
"https://zed.dev/docs/linux#i-cant-open-any-files",
)
})
});
}
pub fn dismiss_notification(&mut self, id: &NotificationId, cx: &mut ViewContext<Self>) {
self.dismiss_notification_internal(id, cx)
}
@@ -358,84 +349,6 @@ impl Render for LanguageServerPrompt {
impl EventEmitter<DismissEvent> for LanguageServerPrompt {}
pub struct ErrorMessagePrompt {
message: SharedString,
label_and_url_button: Option<(SharedString, SharedString)>,
}
impl ErrorMessagePrompt {
pub fn new<S>(message: S) -> Self
where
S: Into<SharedString>,
{
Self {
message: message.into(),
label_and_url_button: None,
}
}
pub fn with_link_button<S>(mut self, label: S, url: S) -> Self
where
S: Into<SharedString>,
{
self.label_and_url_button = Some((label.into(), url.into()));
self
}
}
impl Render for ErrorMessagePrompt {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
h_flex()
.id("error_message_prompt_notification")
.occlude()
.elevation_3(cx)
.items_start()
.justify_between()
.p_2()
.gap_2()
.w_full()
.child(
v_flex()
.w_full()
.child(
h_flex()
.w_full()
.justify_between()
.child(
svg()
.size(cx.text_style().font_size)
.flex_none()
.mr_2()
.mt(px(-2.0))
.map(|icon| {
icon.path(IconName::ExclamationTriangle.path())
.text_color(Color::Error.color(cx))
}),
)
.child(
ui::IconButton::new("close", ui::IconName::Close)
.on_click(cx.listener(|_, _, cx| cx.emit(gpui::DismissEvent))),
),
)
.child(
div()
.max_w_80()
.child(Label::new(self.message.clone()).size(LabelSize::Small)),
)
.when_some(self.label_and_url_button.clone(), |elm, (label, url)| {
elm.child(
div().mt_2().child(
ui::Button::new("error_message_prompt_notification_button", label)
.on_click(move |_, cx| cx.open_url(&url)),
),
)
}),
)
}
}
impl EventEmitter<DismissEvent> for ErrorMessagePrompt {}
pub mod simple_message_notification {
use gpui::{
div, DismissEvent, EventEmitter, InteractiveElement, ParentElement, Render, SharedString,

View File

@@ -30,10 +30,10 @@ use gpui::{
action_as, actions, canvas, impl_action_as, impl_actions, point, relative, size,
transparent_black, Action, AnyElement, AnyView, AnyWeakView, AppContext, AsyncAppContext,
AsyncWindowContext, Bounds, CursorStyle, Decorations, DragMoveEvent, Entity as _, EntityId,
EventEmitter, Flatten, FocusHandle, FocusableView, Global, Hsla, KeyContext, Keystroke,
ManagedView, Model, ModelContext, MouseButton, PathPromptOptions, Point, PromptLevel, Render,
ResizeEdge, Size, Stateful, Subscription, Task, Tiling, View, WeakView, WindowBounds,
WindowHandle, WindowOptions,
EventEmitter, FocusHandle, FocusableView, Global, Hsla, KeyContext, Keystroke, ManagedView,
Model, ModelContext, MouseButton, PathPromptOptions, Point, PromptLevel, Render, ResizeEdge,
Size, Stateful, Subscription, Task, Tiling, View, WeakView, WindowBounds, WindowHandle,
WindowOptions,
};
use item::{
FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
@@ -307,31 +307,13 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
if let Some(app_state) = app_state.upgrade() {
cx.spawn(move |cx| async move {
match Flatten::flatten(paths.await.map_err(|e| e.into())) {
Ok(Some(paths)) => {
cx.update(|cx| {
open_paths(&paths, app_state, OpenOptions::default(), cx)
.detach_and_log_err(cx)
})
.ok();
}
Ok(None) => {}
Err(err) => {
cx.update(|cx| {
if let Some(workspace_window) = cx
.active_window()
.and_then(|window| window.downcast::<Workspace>())
{
workspace_window
.update(cx, |workspace, cx| {
workspace.show_portal_error(err.to_string(), cx);
})
.ok();
}
})
.ok();
}
};
if let Some(paths) = paths.await.log_err().flatten() {
cx.update(|cx| {
open_paths(&paths, app_state, OpenOptions::default(), cx)
.detach_and_log_err(cx)
})
.ok();
}
})
.detach();
}
@@ -1314,15 +1296,7 @@ impl Workspace {
let (tx, rx) = oneshot::channel();
let abs_path = cx.prompt_for_new_path(&start_abs_path);
cx.spawn(|this, mut cx| async move {
let abs_path: Option<PathBuf> =
Flatten::flatten(abs_path.await.map_err(|e| e.into())).map_err(|err| {
this.update(&mut cx, |this, cx| {
this.show_portal_error(err.to_string(), cx);
})
.ok();
err
})?;
let abs_path = abs_path.await?;
let project_path = abs_path.and_then(|abs_path| {
this.update(&mut cx, |this, cx| {
this.project.update(cx, |project, cx| {
@@ -1611,16 +1585,8 @@ impl Workspace {
});
cx.spawn(|this, mut cx| async move {
let paths = match Flatten::flatten(paths.await.map_err(|e| e.into())) {
Ok(Some(paths)) => paths,
Ok(None) => return,
Err(err) => {
this.update(&mut cx, |this, cx| {
this.show_portal_error(err.to_string(), cx);
})
.ok();
return;
}
let Some(paths) = paths.await.log_err().flatten() else {
return;
};
if let Some(task) = this
@@ -1782,14 +1748,7 @@ impl Workspace {
multiple: true,
});
cx.spawn(|this, mut cx| async move {
let paths = Flatten::flatten(paths.await.map_err(|e| e.into())).map_err(|err| {
this.update(&mut cx, |this, cx| {
this.show_portal_error(err.to_string(), cx);
})
.ok();
err
})?;
if let Some(paths) = paths {
if let Some(paths) = paths.await.log_err().flatten() {
let results = this
.update(&mut cx, |this, cx| {
this.open_paths(paths, OpenVisible::All, None, cx)

View File

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
version = "0.144.3"
version = "0.145.0"
publish = false
license = "GPL-3.0-or-later"
authors = ["Zed Team <hi@zed.dev>"]
@@ -85,7 +85,7 @@ search.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
simplelog.workspace = true
simplelog = "0.9"
smol.workspace = true
snippet_provider.workspace = true
tab_switcher.workspace = true

View File

@@ -1 +1 @@
stable
dev

View File

@@ -54,27 +54,32 @@
<screenshots>
<screenshot type="default">
<caption>Zed with a large project open, showing language server and gitblame support</caption>
<image type="source" width="1122" height="859" xml:lang="en">https://zed.dev/img/flatpak/flatpak-1.png</image>
<image>https://zed.dev/img/flatpak/flatpak-1.png</image>
</screenshot>
<screenshot>
<caption>Zed with a file open and a channel message thread in the right sidebar</caption>
<image type="source" width="1122" height="859" xml:lang="en">https://zed.dev/img/flatpak/flatpak-2.png</image>
<image>https://zed.dev/img/flatpak/flatpak-2.png</image>
</screenshot>
<screenshot>
<caption>Example of a channel's shared document</caption>
<image type="source" width="1122" height="859" xml:lang="en">https://zed.dev/img/flatpak/flatpak-3.png</image>
<image>https://zed.dev/img/flatpak/flatpak-3.png</image>
</screenshot>
<screenshot>
<caption>Zed's extension list</caption>
<image type="source" width="1122" height="859" xml:lang="en">https://zed.dev/img/flatpak/flatpak-4.png</image>
<image>https://zed.dev/img/flatpak/flatpak-4.png</image>
</screenshot>
<screenshot>
<caption>Theme switcher UI and example theme</caption>
<image type="source" width="1122" height="859" xml:lang="en">https://zed.dev/img/flatpak/flatpak-5.png</image>
<image>https://zed.dev/img/flatpak/flatpak-5.png</image>
</screenshot>
</screenshots>
<releases>
@release_info@
@release_info@
<release version="0.0.0" date="1970-01-01">
<description>
<p>Dummy release to keep flatpak-builder AppStream metadata validation from complaining</p>
</description>
</release>
</releases>
</component>

View File

@@ -10,7 +10,7 @@ Exec=$APP_CLI $APP_ARGS
Icon=$APP_ICON
Categories=Utility;TextEditor;Development;IDE;
Keywords=zed;
MimeType=text/plain;inode/directory;
MimeType=text/plain;inode/directory;x-scheme-handler/zed;
[Desktop Action NewWorkspace]
Exec=$APP_CLI --new $APP_ARGS

View File

@@ -80,9 +80,7 @@ fn fail_to_open_window_async(e: anyhow::Error, cx: &mut AsyncAppContext) {
}
fn fail_to_open_window(e: anyhow::Error, _cx: &mut AppContext) {
eprintln!(
"Zed failed to open a window: {e:?}. See https://zed.dev/docs/linux for troubleshooting steps."
);
eprintln!("Zed failed to open a window: {e:?}");
#[cfg(not(target_os = "linux"))]
{
process::exit(1);
@@ -101,12 +99,7 @@ fn fail_to_open_window(e: anyhow::Error, _cx: &mut AppContext) {
.add_notification(
notification_id,
Notification::new("Zed failed to launch")
.body(Some(
format!(
"{e:?}. See https://zed.dev/docs/linux for troubleshooting steps."
)
.as_str(),
))
.body(Some(format!("{e:?}").as_str()))
.priority(Priority::High)
.icon(ashpd::desktop::Icon::with_names(&[
"dialog-question-symbolic",
@@ -715,8 +708,8 @@ fn init_logger() {
Ok(log_file) => {
let mut config_builder = ConfigBuilder::new();
config_builder.set_time_format_rfc3339();
config_builder.set_time_offset_to_local().log_err();
config_builder.set_time_format_str("%Y-%m-%dT%T%:z");
config_builder.set_time_to_local(true);
#[cfg(target_os = "linux")]
{

View File

@@ -22,6 +22,7 @@
- [Collaboration](./collaboration.md)
- [Tasks](./tasks.md)
- [Remote Development](./remote-development.md)
- [Repl](./repl.md)
# Language Support

View File

@@ -57,7 +57,7 @@ mkdir -p ~/.local
# extract zed to ~/.local/zed.app/
tar -xvf <path/to/download>.tar.gz -C ~/.local
# link the zed binary to ~/.local/bin (or another directory in your $PATH)
ln -sf ~/.local/bin/zed ~/.local/zed.app/bin/zed
ln -sf ~/.local/zed.app/bin/zed ~/.local/bin/zed
```
If you'd like integration with an XDG-compatible desktop environment, you will also need to install the `.desktop` file:

72
docs/src/repl.md Normal file
View File

@@ -0,0 +1,72 @@
# REPL
Read. Eval. Print. Loop.
<div class="warning">
This feature is in active development. Details may change. We're delighted to get feedback as the REPL feature evolves.
</div>
The built-in REPL for Zed allows you to run code interactively in your editor similarly to a notebook with your own text files.
<!-- TODO: Include GIF in action -->
To start using the REPL, add the following to your Zed `settings.json` to bring the power of [Jupyter kernels](https://docs.jupyter.org/en/latest/projects/kernels.html) to your editor:
```json
{
"jupyter": {
"enabled": true
}
}
```
After that, install any of the supported kernels:
* [Python](#python)
* [TypeScript via Deno](#deno)
## Python
### Global environment
To setup your current python to have an available kernel, run:
```
python -m ipykernel install --user
```
### Conda Environment
```
source activate myenv
conda install ipykernel
python -m ipykernel install --user --name myenv --display-name "Python (myenv)"
```
### Virtualenv with pip
```
source activate myenv
pip install ipykernel
python -m ipykernel install --user --name myenv --display-name "Python (myenv)"
```
## Deno
[Install Deno](https://docs.deno.com/runtime/manual/getting_started/installation/) and then install the Deno jupyter kernel:
```
deno jupyter --unstable --install
```
## Other languages
* [Julia](https://github.com/JuliaLang/IJulia.jl)
* R
- [Ark Kernel from Positron, formerly RStudio](https://github.com/posit-dev/ark)
- [Xeus-R](https://github.com/jupyter-xeus/xeus-r)
* [Scala](https://almond.sh/docs/quick-start-install)

View File

@@ -12,19 +12,19 @@ channel=$(<crates/zed/RELEASE_CHANNEL)
export CHANNEL="$channel"
export ARCHIVE="$archive"
if [[ "$channel" == "dev" ]]; then
export APP_ID="dev.zed.Zed-Dev"
export APP_ID="dev.zed.ZedDev"
export APP_NAME="Zed Devel"
export BRANDING_LIGHT="#99c1f1"
export BRANDING_DARK="#1a5fb4"
export ICON_FILE="app-icon-dev"
elif [[ "$channel" == "nightly" ]]; then
export APP_ID="dev.zed.Zed-Nightly"
export APP_ID="dev.zed.ZedNightly"
export APP_NAME="Zed Nightly"
export BRANDING_LIGHT="#e9aa6a"
export BRANDING_DARK="#1a5fb4"
export ICON_FILE="app-icon-nightly"
elif [[ "$channel" == "preview" ]]; then
export APP_ID="dev.zed.Zed-Preview"
export APP_ID="dev.zed.ZedPreview"
export APP_NAME="Zed Preview"
export BRANDING_LIGHT="#99c1f1"
export BRANDING_DARK="#1a5fb4"

View File

@@ -1,6 +1,10 @@
#!/usr/bin/env sh
set -eu
# Downloads the latest tarball from https://zed.dev/releases and unpacks it
# into ~/.local/. If you'd prefer to do this manually, instructions are at
# https://zed.dev/docs/linux.
main() {
platform="$(uname -s)"
arch="$(uname -m)"
@@ -11,7 +15,6 @@ main() {
platform="macos"
elif [ "$platform" = "Linux" ]; then
platform="linux"
channel="${ZED_CHANNEL:-preview}"
else
echo "Unsupported platform $platform"
exit 1