Compare commits

..

21 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
Daniel Schmidt
e1a6efa609 go: Quote targeting expression on runnables (#14055)
Release Notes:

- Go: fix test runnables in fish shell.
2024-07-10 16:02:29 +02:00
77 changed files with 5126 additions and 2466 deletions

View File

@@ -319,7 +319,6 @@ jobs:
- name: Upload app bundle to release
uses: softprops/action-gh-release@v1
if: ${{ env.RELEASE_CHANNEL == 'preview' }}
with:
draft: true
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}

7
Cargo.lock generated
View File

@@ -377,6 +377,7 @@ dependencies = [
"cargo_toml",
"chrono",
"client",
"clock",
"collections",
"command_palette_hooks",
"ctor",
@@ -419,6 +420,7 @@ dependencies = [
"telemetry_events",
"terminal",
"terminal_view",
"text",
"theme",
"tiktoken-rs",
"toml 0.8.10",
@@ -2405,6 +2407,7 @@ version = "0.1.0"
dependencies = [
"chrono",
"parking_lot",
"serde",
"smallvec",
]
@@ -2463,6 +2466,7 @@ version = "0.44.0"
dependencies = [
"anthropic",
"anyhow",
"assistant",
"async-trait",
"async-tungstenite",
"audio",
@@ -8270,6 +8274,7 @@ dependencies = [
"assistant",
"editor",
"gpui",
"repl",
"search",
"settings",
"ui",
@@ -13581,7 +13586,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.144.0"
version = "0.145.0"
dependencies = [
"activity_indicator",
"anyhow",

View File

@@ -518,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

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

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

@@ -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

@@ -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

@@ -193,16 +193,6 @@ impl TextInput {
.find_map(|(idx, _)| (idx > offset).then_some(idx))
.unwrap_or(self.content.len())
}
fn reset(&mut self) {
self.content = "".into();
self.selected_range = 0..0;
self.selection_reversed = false;
self.marked_range = None;
self.last_layout = None;
self.last_bounds = None;
self.is_selecting = false;
}
}
impl ViewInputHandler for TextInput {
@@ -325,7 +315,6 @@ impl Element for TextElement {
None
}
#[profiling::function]
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
@@ -337,7 +326,6 @@ impl Element for TextElement {
(cx.request_layout(style, []), ())
}
#[profiling::function]
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
@@ -428,7 +416,6 @@ impl Element for TextElement {
}
}
#[profiling::function]
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
@@ -443,14 +430,12 @@ impl Element for TextElement {
ElementInputHandler::new(bounds, self.input.clone()),
);
if let Some(selection) = prepaint.selection.take() {
profiling::scope!("paint_quad selection");
cx.paint_quad(selection)
}
let line = prepaint.line.take().unwrap();
line.paint(bounds.origin, cx.line_height(), cx).unwrap();
if let Some(cursor) = prepaint.cursor.take() {
profiling::scope!("paint_quad cursor");
cx.paint_quad(cursor);
}
self.input.update(cx, |input, _cx| {
@@ -461,7 +446,6 @@ impl Element for TextElement {
}
impl Render for TextInput {
#[profiling::function]
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.flex()
@@ -510,48 +494,13 @@ struct InputExample {
recent_keystrokes: Vec<Keystroke>,
}
impl InputExample {
fn on_reset_click(&mut self, _: &MouseUpEvent, cx: &mut ViewContext<Self>) {
self.recent_keystrokes.clear();
self.text_input
.update(cx, |text_input, _cx| text_input.reset());
cx.notify();
}
}
impl Render for InputExample {
#[profiling::function]
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let num_keystrokes = self.recent_keystrokes.len();
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
div()
.bg(rgb(0xaaaaaa))
.flex()
.flex_col()
.size_full()
.child(
div()
.bg(white())
.border_b_1()
.border_color(black())
.flex()
.flex_row()
.justify_between()
.child(format!("Keystrokes: {}", num_keystrokes))
.child(
div()
.border_1()
.border_color(black())
.px_2()
.bg(yellow())
.child("Reset")
.hover(|style| {
style
.bg(yellow().blend(opaque_grey(0.5, 0.5)))
.cursor_pointer()
})
.on_mouse_up(MouseButton::Left, cx.listener(Self::on_reset_click)),
),
)
.child(self.text_input.clone())
.children(self.recent_keystrokes.iter().rev().map(|ks| {
format!(

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

@@ -387,7 +387,6 @@ impl<E: Element> Drawable<E> {
}
}
#[profiling::function]
pub(crate) fn layout_as_root(
&mut self,
available_space: Size<AvailableSpace>,
@@ -504,7 +503,6 @@ impl AnyElement {
}
/// Performs layout for this element within the given available space and returns its size.
#[profiling::function]
pub fn layout_as_root(
&mut self,
available_space: Size<AvailableSpace>,
@@ -519,7 +517,6 @@ impl AnyElement {
}
/// Performs layout on this element in the available space, then prepaints it at the given absolute origin.
#[profiling::function]
pub fn prepaint_as_root(
&mut self,
origin: Point<Pixels>,
@@ -527,10 +524,7 @@ impl AnyElement {
cx: &mut WindowContext,
) {
self.layout_as_root(available_space, cx);
cx.with_absolute_element_offset(origin, |cx| {
profiling::scope!("with_absolute_element_offset prepaint");
self.0.prepaint(cx)
});
cx.with_absolute_element_offset(origin, |cx| self.0.prepaint(cx));
}
}

View File

@@ -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

@@ -524,7 +524,6 @@ impl BladeRenderer {
self.gpu.destroy_command_encoder(&mut self.command_encoder);
}
#[profiling::function]
pub fn draw(&mut self, scene: &Scene) {
self.command_encoder.start();
self.atlas.before_frame(&mut self.command_encoder);

View File

@@ -38,8 +38,6 @@ impl LinuxDispatcher {
.map(|i| {
let receiver = background_receiver.clone();
std::thread::spawn(move || {
let thread_name = format!("background-{}", i);
profiling::register_thread!(&thread_name);
for runnable in receiver {
let start = Instant::now();
@@ -57,8 +55,6 @@ impl LinuxDispatcher {
let (timer_sender, timer_channel) = calloop::channel::channel::<TimerAfter>();
let timer_thread = std::thread::spawn(|| {
profiling::register_thread!("timer-thread");
let mut event_loop: EventLoop<()> =
EventLoop::try_new().expect("Failed to initialize timer loop!");

View File

@@ -1403,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;
}
@@ -1416,7 +1417,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
}
}
drop(state);
window.set_focused(true);
window.set_hovered(true);
}
}
wl_pointer::Event::Leave { .. } => {
@@ -1432,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 {

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

@@ -110,7 +110,8 @@ 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,
pub(crate) ximc: Option<X11rbClient<Rc<XCBConnection>>>,
pub(crate) xim_handler: Option<XimHandler>,
@@ -144,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() {
@@ -341,7 +347,8 @@ impl X11Client {
_resource_database: resource_database,
atoms,
windows: HashMap::default(),
focused_window: None,
mouse_focused_window: None,
keyboard_focused_window: None,
xkb: xkb_state,
ximc,
xim_handler,
@@ -502,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();
@@ -586,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();
}
@@ -620,7 +627,7 @@ impl X11Client {
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);
@@ -871,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),
@@ -886,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);
}
_ => {}
};
@@ -1140,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
@@ -1272,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);
}
@@ -1182,7 +1209,6 @@ impl PlatformWindow for X11Window {
self.0.callbacks.borrow_mut().appearance_changed = Some(callback);
}
#[profiling::function]
fn draw(&self, scene: &Scene) {
let mut inner = self.0.state.borrow_mut();
inner.renderer.draw(scene);

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

@@ -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

@@ -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

@@ -142,7 +142,6 @@ impl TaffyLayoutEngine {
Ok(edges)
}
#[profiling::function]
pub fn compute_layout(
&mut self,
id: LayoutId,
@@ -162,7 +161,6 @@ impl TaffyLayoutEngine {
//
if !self.computed_layouts.insert(id) {
profiling::scope!("compute layout stack extension");
let mut stack = SmallVec::<[LayoutId; 64]>::new();
stack.push(id);
while let Some(id) = stack.pop() {
@@ -183,8 +181,6 @@ impl TaffyLayoutEngine {
id.into(),
available_space.into(),
|known_dimensions, available_space, node_id, _context| {
profiling::scope!("measure function");
let Some(measure) = self.nodes_to_measure.get_mut(&node_id.into()) else {
return taffy::geometry::Size::default();
};
@@ -194,10 +190,7 @@ impl TaffyLayoutEngine {
height: known_dimensions.height.map(Pixels),
};
{
profiling::scope!("calling measure");
measure(known_dimensions, available_space.into(), cx).into()
}
measure(known_dimensions, available_space.into(), cx).into()
},
)
.expect(EXPECT_MESSAGE);

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

@@ -453,48 +453,17 @@ impl Frame {
}
}
#[profiling::function]
pub(crate) fn clear(&mut self) {
{
profiling::scope!("element_states clear");
self.element_states.clear();
}
{
profiling::scope!("accessed_element_states clear");
self.accessed_element_states.clear();
}
{
profiling::scope!("mouse_listeners clear");
self.mouse_listeners.clear();
}
{
profiling::scope!("dispatch_tree clear");
self.dispatch_tree.clear();
}
{
profiling::scope!("scene clear");
self.scene.clear();
}
{
profiling::scope!("input handlers clear");
self.input_handlers.clear();
}
{
profiling::scope!("tooltip_requests clear");
self.tooltip_requests.clear();
}
{
profiling::scope!("cursor styles clear");
self.cursor_styles.clear();
}
{
profiling::scope!("hitboxes clear");
self.hitboxes.clear();
}
{
profiling::scope!("deferred draws clear");
self.deferred_draws.clear();
}
self.element_states.clear();
self.accessed_element_states.clear();
self.mouse_listeners.clear();
self.dispatch_tree.clear();
self.scene.clear();
self.input_handlers.clear();
self.tooltip_requests.clear();
self.cursor_styles.clear();
self.hitboxes.clear();
self.deferred_draws.clear();
}
pub(crate) fn hit_test(&self, position: Point<Pixels>) -> HitTest {
@@ -572,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>>,
@@ -703,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()));
@@ -726,7 +697,6 @@ impl Window {
}
}));
platform_window.on_request_frame(Box::new({
profiling::scope!("on_request_frame");
let mut cx = cx.to_async();
let dirty = dirty.clone();
let active = active.clone();
@@ -810,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| {
@@ -861,6 +841,7 @@ impl Window {
appearance,
appearance_observers: SubscriberSet::new(),
active,
hovered,
dirty,
needs_present,
last_input_timestamp,
@@ -1254,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();
@@ -1461,7 +1453,6 @@ impl<'a> WindowContext<'a> {
.next_frame
.finish(&mut self.window.rendered_frame);
ELEMENT_ARENA.with_borrow_mut(|element_arena| {
profiling::scope!("element area clear");
let percentage = (element_arena.len() as f32 / element_arena.capacity() as f32) * 100.;
if percentage >= 80. {
log::warn!("elevated element arena occupation: {}.", percentage);
@@ -1472,47 +1463,37 @@ impl<'a> WindowContext<'a> {
self.window.draw_phase = DrawPhase::Focus;
let previous_focus_path = self.window.rendered_frame.focus_path();
let previous_window_active = self.window.rendered_frame.window_active;
{
profiling::scope!("swapping frames");
mem::swap(&mut self.window.rendered_frame, &mut self.window.next_frame);
}
{
profiling::scope!("clearing next frame");
self.window.next_frame.clear();
}
mem::swap(&mut self.window.rendered_frame, &mut self.window.next_frame);
self.window.next_frame.clear();
let current_focus_path = self.window.rendered_frame.focus_path();
let current_window_active = self.window.rendered_frame.window_active;
if previous_focus_path != current_focus_path
|| previous_window_active != current_window_active
{
profiling::scope!("updating focus path");
let current_focus_path = self.window.rendered_frame.focus_path();
let current_window_active = self.window.rendered_frame.window_active;
if previous_focus_path != current_focus_path
|| previous_window_active != current_window_active
{
if !previous_focus_path.is_empty() && current_focus_path.is_empty() {
self.window
.focus_lost_listeners
.clone()
.retain(&(), |listener| listener(self));
}
let event = WindowFocusEvent {
previous_focus_path: if previous_window_active {
previous_focus_path
} else {
Default::default()
},
current_focus_path: if current_window_active {
current_focus_path
} else {
Default::default()
},
};
if !previous_focus_path.is_empty() && current_focus_path.is_empty() {
self.window
.focus_listeners
.focus_lost_listeners
.clone()
.retain(&(), |listener| listener(&event, self));
.retain(&(), |listener| listener(self));
}
let event = WindowFocusEvent {
previous_focus_path: if previous_window_active {
previous_focus_path
} else {
Default::default()
},
current_focus_path: if current_window_active {
current_focus_path
} else {
Default::default()
},
};
self.window
.focus_listeners
.clone()
.retain(&(), |listener| listener(&event, self));
}
self.reset_cursor_style();
@@ -1530,7 +1511,6 @@ impl<'a> WindowContext<'a> {
profiling::finish_frame!();
}
#[profiling::function]
fn draw_roots(&mut self) {
self.window.draw_phase = DrawPhase::Prepaint;
self.window.tooltip_bounds.take();
@@ -1898,7 +1878,6 @@ impl<'a> WindowContext<'a> {
/// Updates the global element offset based on the given offset. This is used to implement
/// drag handles and other manual painting of elements. This method should only be called during
/// the prepaint phase of element drawing.
#[profiling::function]
pub fn with_absolute_element_offset<R>(
&mut self,
offset: Point<Pixels>,
@@ -2764,7 +2743,6 @@ impl<'a> WindowContext<'a> {
/// After calling it, you can request the bounds of the given layout node id or any descendant.
///
/// This method should only be called as part of the prepaint phase of element drawing.
#[profiling::function]
pub fn compute_layout(&mut self, layout_id: LayoutId, available_space: Size<AvailableSpace>) {
debug_assert_eq!(
self.window.draw_phase,
@@ -2773,10 +2751,7 @@ impl<'a> WindowContext<'a> {
);
let mut layout_engine = self.window.layout_engine.take().unwrap();
{
profiling::scope!("layout_engine compute_layout");
layout_engine.compute_layout(layout_id, available_space, self);
}
layout_engine.compute_layout(layout_id, available_space, self);
self.window.layout_engine = Some(layout_engine);
}
@@ -3029,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

@@ -518,7 +518,7 @@ impl ContextProvider for GoContextProvider {
"test".into(),
GO_PACKAGE_TASK_VARIABLE.template_value(),
"-run".into(),
format!("^{}$", VariableName::Symbol.template_value(),),
format!("'^{}$'", VariableName::Symbol.template_value(),),
],
tags: vec!["go-test".to_owned()],
..TaskTemplate::default()
@@ -549,7 +549,7 @@ impl ContextProvider for GoContextProvider {
"-v".into(),
"-run".into(),
format!(
"^{}$/^{}$",
"'^{}$/^{}$'",
VariableName::Symbol.template_value(),
GO_SUBTEST_NAME_TASK_VARIABLE.template_value(),
),
@@ -570,7 +570,7 @@ impl ContextProvider for GoContextProvider {
"-benchmem".into(),
"-run=^$".into(),
"-bench".into(),
format!("^{}$", VariableName::Symbol.template_value()),
format!("'^{}$'", VariableName::Symbol.template_value()),
],
tags: vec!["go-benchmark".to_owned()],
..TaskTemplate::default()

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 {
@@ -4099,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| {
@@ -8810,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 {
@@ -11058,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;

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

@@ -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

@@ -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

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
version = "0.144.0"
version = "0.145.0"
publish = false
license = "GPL-3.0-or-later"
authors = ["Zed Team <hi@zed.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

@@ -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