Compare commits

..

87 Commits

Author SHA1 Message Date
Mikayla
9eb92d4599 Make project panel settings live update 2024-04-12 11:17:51 -07:00
Mikayla
88a1070b13 Merge branch 'main' into auto-folded-dirs 2024-04-12 10:54:55 -07:00
Mikayla
60e1ffbce5 Default auto-folding to off, restore previous tests 2024-04-12 10:41:51 -07:00
Kyle Kelley
49371b44cb Semantic Index (#10329)
This introduces semantic indexing in Zed based on chunking text from
files in the developer's workspace and creating vector embeddings using
an embedding model. As part of this, we've created an embeddings
provider trait that allows us to work with OpenAI, a local Ollama model,
or a Zed hosted embedding.

The semantic index is built by breaking down text for known
(programming) languages into manageable chunks that are smaller than the
max token size. Each chunk is then fed to a language model to create a
high dimensional vector which is then normalized to a unit vector to
allow fast comparison with other vectors with a simple dot product.
Alongside the vector, we store the path of the file and the range within
the document where the vector was sourced from.

Zed will soon grok contextual similarity across different text snippets,
allowing for natural language search beyond keyword matching. This is
being put together both for human-based search as well as providing
results to Large Language Models to allow them to refine how they help
developers.

Remaining todo:

* [x] Change `provider` to `model` within the zed hosted embeddings
database (as its currently a combo of the provider and the model in one
name)


Release Notes:

- N/A

---------

Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Conrad Irwin <conrad@zed.dev>
Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
Co-authored-by: Antonio <antonio@zed.dev>
2024-04-12 11:40:59 -06:00
Maxime Forveille
4b40e83b8b gpui: Fix window title special characters display on X11 (#9994)
Before:

![image](https://github.com/zed-industries/zed/assets/13511978/f12a144a-5c41-44e9-8422-aa73ea54fb9c)

After:

![image](https://github.com/zed-industries/zed/assets/13511978/45e9b701-77a8-4e63-9481-dab895a347f7)

Release Notes:

- Fixed window title special characters display on X11.

---------

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2024-04-12 09:49:31 -07:00
Conrad Irwin
dffddaec4c Revert "Revert "language: Remove buffer fingerprinting (#9007)"" (#9671)
This reverts commit caed275fbf.

NOTE: this should not be merged until #9668 is on stable and the
`ZedVersion#can_collaborate` is updated to exclude all clients without
that change.

Release Notes:

- N/A

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2024-04-12 18:40:35 +02:00
Marshall Bowers
a4d6c5da7c toml: Bump to v0.1.0 (#10482)
This PR bumps the TOML extension to v0.1.0.

This version of the extension has been updated to use v0.0.6 of the
`zed_extension_api`.

Release Notes:

- N/A
2024-04-12 12:39:43 -04:00
Hans
3ea17248c8 Adjust left movement when soft_wrap mode is used (#10464)
Release Notes:

- Added/Fixed #10350
2024-04-12 10:36:31 -06:00
Marshall Bowers
e0e1103228 zig: Bump to v0.1.0 (#10481)
This PR bumps the Zig extension to v0.1.0.

This version of the extension has been updated to use v0.0.6 of the
`zed_extension_api`.

It also adds support for treating `.zig.zon` files as Zig files
(#10012).

Release Notes:

- N/A
2024-04-12 12:30:29 -04:00
Marshall Bowers
65c9e7d3d1 php: Bump to v0.0.2 (#10480)
This PR bumps the PHP extension to v0.0.2.

This version of the PHP extension adds the `language_ids` mappings from
#10053.

Release Notes:

- N/A
2024-04-12 12:19:01 -04:00
Marshall Bowers
b5b872656b Extract Terraform extension (#10479)
This PR extracts Terraform support into an extension and removes the
built-in Terraform support from Zed.

Release Notes:

- Removed built-in support for Terraform, in favor of making it
available as
an extension. The Terraform extension will be suggested for download
when you
open a `.tf`, `.tfvars`, or `.hcl` file.
2024-04-12 11:49:49 -04:00
Bennet Bo Fenner
f4d9a97195 preview tabs: Support find all references (#10470)
`FindAllReferences` will now open a preview tab, jumping to a definition
will also open a preview tab.


https://github.com/zed-industries/zed/assets/53836821/fa3db1fd-ccb3-4559-b3d2-b1fe57f86481

Note: One thing I would like to improve here is also adding support for
reopening `FindAllReferences` using the navigation history. As of now
the navigation history is lacking support for reopening items other then
project files, which needs to be implemented first.

Release Notes:

- N/A
2024-04-12 17:22:12 +02:00
Bennet Bo Fenner
7b01a29f5a preview tabs: Fix tab selection getting out of sync (#10478)
There was an edge case where the project panel selection would not be
updated when opening a lot of tabs quickly using the preview tab
feature.
I spent way too long debugging this, thankfully @ConradIrwin spotted it
in like 5 minutes 🎉

Release Notes:

- N/A
2024-04-12 17:20:30 +02:00
张小白
04e89c4c51 Use workspace uuid (#10475)
Release Notes:

- N/A
2024-04-12 10:53:10 -04:00
Conrad Irwin
0ab5a524b0 Fix overlap (#10474)
Although I liked the symmetry of the count in the middle of the arrows,
it's
tricky to make the buttons not occlude the count on hover, so go back to
this arrangement.

Release Notes:

- N/A
2024-04-12 08:25:09 -06:00
Bennet Bo Fenner
cd5ddfe34b chat panel: Add timestamp in tooltip to edited message (#10444)
Hovering over the `(edited)` text inside a message displays a tooltip
with the timestamp of when the message was last edited:


![image](https://github.com/zed-industries/zed/assets/53836821/be6d68c2-7447-42bc-bd5e-7a9053b3c980)

---

Also removed the `fade_out` style for the `(edited)` text, as this was
causing tooltips to fade out as well:


![image](https://github.com/zed-industries/zed/assets/53836821/91d3cf6a-db58-4e1d-b257-663b2ce1aca4)

Instead it uses `theme().text_muted` now.


Release Notes:

- Hovering over an edited message now displays a tooltip revealing the
timestamp of the last edit.
2024-04-12 14:26:41 +02:00
Bennet Bo Fenner
0a4c3488dd Fix typo in README (#10471)
Fixes a typo in the README which I believe was accidentally committed
yesterday in #10459


Release Notes:

- N/A
2024-04-12 14:18:54 +02:00
Piotr Osiewicz
a1cbc23fee task: use full task label to distinguish a terminal (#10469)
Spotted by @SomeoneToIgnore, in #10468 I've used a shortened task label,
which might lead to collisions.

Release Notes:

- N/A
2024-04-12 13:25:46 +02:00
Piotr Osiewicz
298e9c9387 task: Allow Rerun action to override properties of task being reran (#10468)
For example:
```
"alt-t": [
    "task::Rerun",
     { "reevaluate_context": true, "allow_concurrent_runs": true }
],
```
Overriding `allow_concurrent_runs` to `true` by itself should terminate
current instance of the task, if there's any.

This PR also fixes task deduplication in terminal panel to use expanded
label and not the id, which depends on task context. It kinda aligns
with how task rerun worked prior to #10341 . That's omitted in the
release notes though, as it's not in Preview yet.

Release Notes:

- `Task::Rerun` action can now override `allow_concurrent_runs` and
`use_new_terminal` properties of the task that is being reran.
2024-04-12 12:44:50 +02:00
Thorsten Ball
6e1ba7e936 Allow hovering over tooltips in git blame sidebar (#10466)
This introduces a new API on `StatefulInteractiveElement` to create a
tooltip that can be hovered, scrolled inside, and clicked:
`.hoverable_tooltip`.

Right now we only use it in the `git blame` gutter, but the plan is to
use the new hover/click/scroll behavior in #10398 to introduce new
git-blame-tooltips.

Release Notes:

- N/A

---------

Co-authored-by: Antonio <antonio@zed.dev>
2024-04-12 11:47:32 +02:00
Thorsten Ball
bc0c2e0cae Extend Vim default keybindings (#10461)
This implements some of #10457.

Release notes:

- Added `g c c` and `g c` to Vim keybindings to toggle comments in
normal and visual mode respectively.
- Added `g ]` and `g [` to Vim keybindings to go to next and previous
diagnostic error.
- Changed `[ x` and `] x` (which select larger/smaller syntax node) in
Vim mode to also work in visual mode.
2024-04-12 08:05:38 +02:00
Mehmet Efe Akça
29a50573a9 Add git blame error reporting with notification (#10408)
<img width="1035" alt="Screenshot 2024-04-11 at 13 13 44"
src="https://github.com/zed-industries/zed/assets/13402668/cd0e96a0-41c6-4757-8840-97d15a75c511">

Release Notes:

- Added a notification to show possible `git blame` errors if it fails to run.

Caveats:
- ~git blame now executes in foreground
executor  (required since the Fut is !Send)~

TODOs:
- After a failed toggle, the app thinks the blame
is shown. This means toggling again will do nothing
instead of retrying. (Caused by editor.show_git_blame
being set to true before the git blame is generated)
- ~(Maybe) Trim error?~ Done

---------

Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
2024-04-12 07:20:34 +02:00
Conrad Irwin
08786fa7bf Make BufferSearch less wide (#10459)
This also adds some "responsiveness" so that UI elements are hidden
before everything has to be occluded

Release Notes:

- Improved search UI. It now works in narrower panes, and avoids
scrolling the editor on open.

<img width="899" alt="Screenshot 2024-04-11 at 21 33 17"
src="https://github.com/zed-industries/zed/assets/94272/44b95d4f-08d6-4c40-a175-0e594402ca01">
<img width="508" alt="Screenshot 2024-04-11 at 21 33 45"
src="https://github.com/zed-industries/zed/assets/94272/baf4638d-427b-43e6-ad67-13d43f0f18a2">
<img width="361" alt="Screenshot 2024-04-11 at 21 34 00"
src="https://github.com/zed-industries/zed/assets/94272/ff60b561-2f77-49c0-9df7-e26227fe9225">
<img width="348" alt="Screenshot 2024-04-11 at 21 37 03"
src="https://github.com/zed-industries/zed/assets/94272/a2a700a2-ce99-41bd-bf47-9b14d7082b0e">
2024-04-11 23:07:29 -06:00
Hans
f2d61f3ea5 Add feature to display commands for vim mode (#10349)
Release Notes:

- Added the current operator stack to the Vim status bar at the bottom
of the editor. #4447

This commit introduces a new feature that displays the current partial
command in the vim mode, similar to the behavior in Vim plugin. This
helps users keep track of the commands they're entering.
2024-04-12 06:39:57 +02:00
Conrad Irwin
98533079e4 Use buffer font for search (#10455)
It's wierd to type code/regex in the UI font

Release Notes:

- Continue to use buffer font for search
2024-04-11 22:15:12 -06:00
Nate Butler
27ba165046 Organize Project Panel context menus (#10456)
This design polish PR brings the project panel context menu into better
alignment with other editors, better follows system patterns and
identifies focus shifting actions with the `…` indicator (like adding a
new folder to a project, which will open a modal window.)

## Before & After:

**Root level**

![CleanShot - 2024-04-11 at 22 40
53@2x](https://github.com/zed-industries/zed/assets/1714999/aa103d14-0747-4be9-acbf-1c3ed0542a15)

**Folder level**

![CleanShot - 2024-04-11 at 22 43
45@2x](https://github.com/zed-industries/zed/assets/1714999/180224f2-26d1-45bd-8f78-822f46068a6d)

**File level**

![CleanShot - 2024-04-11 at 22 44
56@2x](https://github.com/zed-industries/zed/assets/1714999/67edd0ae-bcb6-4920-a480-c4d50c6bccfa)

Note: That double divider in the after has been fixed 😅

---

Release Notes:

- Improved ordering and organization of context menus in the project
panel to bring them closer to those in similar applications.
2024-04-11 22:52:49 -04:00
Hans
32806b8320 vim: Don’t allow edits in the read-only state (#10404)
Fix #8423 

Release Notes:

- vim: Fixed vim-surround motions editing read-only buffer
(preview-only)
2024-04-11 18:19:49 -06:00
CharlesChen0823
3ab9700155 Windows: Add missing delete key (#10422)
Add miss delete key in windows platform.
Release Notes:

- N/A
2024-04-11 16:20:28 -07:00
Mikayla Maki
9d96ae6e78 Redo linux state again (#10452)
With the recent Linux rewrite, I attempted to simplify the number of
wrapper structs involved in the Linux code, following the macOS code as
an example. Unfortunately, I missed a vital component: pointers to the
platform state, held by platform data structures. As we hold all of the
platform data structures on Linux, this PR reintroduces a wrapper around
the internal state of both the platform and the window. This allows us
to close and drop windows correctly.

This PR also fixes a performance problem introduced by:
https://github.com/zed-industries/zed/pull/10343, where each configure
request would add a new frame callback quickly saturating the main
thread and slowing everything down.

Release Notes:
- N/A
2024-04-11 16:12:14 -07:00
Max Brunsfeld
8d7f5eab79 Extract Ocaml language support into an extension (#10450)
Release Notes:

- Extracted Ocaml language support into an extension

---------

Co-authored-by: Marshall <marshall@zed.dev>
2024-04-11 15:20:19 -07:00
Conrad Irwin
f6c85b28d5 WIP: remoting (#10085)
Release Notes:

- Added private alpha support for remote development. Please reach out to hi@zed.dev if you'd like to be part of shaping this feature.
2024-04-11 15:36:35 -06:00
Bennet Bo Fenner
ea4419076e Add preview tabs (#9125)
This PR implements the preview tabs feature from VSCode.
More details and thanks for the head start of the implementation here
#6782.

Here is what I have observed from using the vscode implementation ([x]
-> already implemented):
- [x] Single click on project file opens tab as preview
- [x] Double click on item in project panel opens tab as permanent
- [x] Double click on the tab makes it permanent
- [x] Navigating away from the tab makes the tab permanent and the new
tab is shown as preview (e.g. GoToReference)
- [x] Existing preview tab is reused when opening a new tab
- [x] Dragging tab to the same/another panel makes the tab permanent
- [x] Opening a tab from the file finder makes the tab permanent
- [x] Editing a preview tab will make the tab permanent
- [x] Using the space key in the project panel opens the tab as preview
- [x] Handle navigation history correctly (restore a preview tab as
preview as well)
- [x] Restore preview tabs after restarting
- [x] Support opening files from file finder in preview mode (vscode:
"Enable Preview From Quick Open")
 
I need to do some more testing of the vscode implementation, there might
be other behaviors/workflows which im not aware of that open an item as
preview/make them permanent.

Showcase:


https://github.com/zed-industries/zed/assets/53836821/9be16515-c740-4905-bea1-88871112ef86


TODOs
- [x] Provide `enable_preview_tabs` setting
- [x] Write some tests
- [x] How should we handle this in collaboration mode (have not tested
the behavior so far)
- [x] Keyboard driven usage (probably need workspace commands)
- [x] Register `TogglePreviewTab` only when setting enabled?
- [x] Render preview tabs in tab switcher as italic
- [x] Render preview tabs in image viewer as italic
- [x] Should this be enabled by default (it is the default behavior in
VSCode)?
- [x] Docs

Future improvements (out of scope for now):
- Support preview mode for find all references and possibly other
multibuffers (VSCode: "Enable Preview From Code Navigation")


Release Notes:

- Added preview tabs
([#4922](https://github.com/zed-industries/zed/issues/4922)).

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-04-11 23:09:12 +02:00
Kyle Kelley
edb1ea2433 Do not show image viewer for SVGs (#10435)
Absent some ability to toggle between viewing and editing a file, I
think it would be best to get a fix out quick for people to edit SVGs as
text files.

Release Notes:

- Fixed editing of SVG images by disabling it from the image viewer
([#10403](https://github.com/zed-industries/zed/issues/10403)).
2024-04-11 13:27:32 -07:00
Marshall Bowers
86aa352ad9 Remove leftover commented-out code (#10445)
This PR removes some commented-out code that was left over from #10430.

Release Notes:

- N/A
2024-04-11 16:08:27 -04:00
Max Brunsfeld
253aa28375 Extract Scheme and Racket language support into extensions (#10442)
Release Notes:

- Extracted Scheme and Racket language support into extensions.

---------

Co-authored-by: Marshall <marshall@zed.dev>
2024-04-11 12:45:46 -07:00
Piotr Osiewicz
165d6b9edb task: Fix variable substitution for free variables (#10434)
Fixes regression from https://github.com/zed-industries/zed/pull/10341
where it was not possible to use non-zed environmental variables (e.g.
$PATH) in task definitions.

No release note, as this didn't land on Preview yet.
Release Notes:

- N/A
2024-04-11 21:15:33 +02:00
Max Brunsfeld
0ac31302d3 Remove built-in Nix support (#10439)
Release Notes:

- Removed built-in Nix support, now that there is a Nix extension.

Co-authored-by: Marshall <marshall@zed.dev>
2024-04-11 11:44:08 -07:00
Max Brunsfeld
176f440158 Extract lua language support into an extension (#10437)
Release Notes:

- Extracted lua language support into an extension, and improved Lua
highlighting and completion label styling.

---------

Co-authored-by: Marshall <marshall@zed.dev>
2024-04-11 11:32:10 -07:00
Sai Gokula Krishnan
c6028f6651 Fix Dart syntax highlighting issue (#8347)
Release Notes:
* Improved Dart syntax highlighting
([#8151](https://github.com/zed-industries/zed/pull/8347#8151))

Before:
<img width="1246" alt="Before"
src="https://github.com/zed-industries/zed/assets/25414681/d9143fe8-c474-40bb-afbd-56fef16edab3">

After:
<img width="1249" alt="After"
src="https://github.com/zed-industries/zed/assets/25414681/4b103c35-fb24-4693-8b9e-3dd494036c9f">


(Shameless plug, since I build the theme)
Theme: The Dark Side
2024-04-11 11:30:36 -07:00
Max Brunsfeld
c38f72d194 Extract GLSL language support into an extension (#10433)
Release Notes:

- Extracted GLSL language support into an extension.

---------

Co-authored-by: Marshall <marshall@zed.dev>
Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-04-11 13:52:10 -04:00
Max Brunsfeld
47f698d5a3 Validate content-length of downloaded extension tar gz files (#10430)
Release Notes:

- Fixed a bug where extension installation would appear to succeed even
if the download did not complete due to network interruptions
([#10330](https://github.com/zed-industries/zed/issues/10330)).

Co-authored-by: Marshall <marshall@zed.dev>
2024-04-11 10:24:09 -07:00
Max Brunsfeld
bcd2ca6196 Extract Elm language into an extension (#10432)
Release Notes:

- Extracted Elm language support into an extension

Co-authored-by: Marshall <marshall@zed.dev>
2024-04-11 10:23:49 -07:00
Bennet Bo Fenner
78d6beee80 Fix invisible chat icons (#10406)
As of #10393 some icons in the chat were invisible, looking at the icons
I noticed that the viewport was actually 800x800, I scaled that down to
16x16 and now they work fine again.

Also remove the `reply_arrow_left` icon because it is not used at all.

Thanks to @RemcoSmitsDev for noticing.

I don't have any expertise in svg's, so if something is off about the
svg markup reach out to me.

Release Notes:

- N/A
2024-04-11 18:36:58 +02:00
Marshall Bowers
2d21f6debf emmet: Bump to v0.0.2 (#10426)
This PR bumps the Emmet extension to v0.0.2.

This version of the Emmet extension adds Emmet support for PHP and ERB
files.

Release Notes:

- N/A
2024-04-11 11:56:25 -04:00
Nate Butler
837b7111b3 Update TextField (#10415)
This PR makes some simple updates to the TextField api and update it's
styling.

Release Notes:

- N/A
2024-04-11 10:03:36 -04:00
Piotr Osiewicz
ea165e134d gpui-macros: Hide autogenerated action types/functions (#10417)
I've found it a bit annoying that autogenerated code shows up in
completions when working on Zed, so I've moved them into an associated
function of a struct we're "implementing" Action on. That way, neither a
generated function nor a static show up in completions.

Note that this change only affects Zed codebase! I'm pushing it up right
after last Preview to give it some time on Nightly.
Before:

![image](https://github.com/zed-industries/zed/assets/24362066/7343201f-f05b-4342-a9f7-97f002d88a48)

![image](https://github.com/zed-industries/zed/assets/24362066/e67f9dcb-e090-40e0-873c-e51bd39e0c7e)

After:

![image](https://github.com/zed-industries/zed/assets/24362066/0704211a-73f5-4f12-8583-9e47f092e5b7)

![image](https://github.com/zed-industries/zed/assets/24362066/c6fa00f5-fd8f-4f06-8be7-b74acedccd7c)


Release Notes:

- N/A
2024-04-11 15:38:47 +02:00
Tatsuya Kyushima
15758c10bf docs: Fix installation command via Homebrew (#10416)
When I installed zed by running `brew install zed` following
`README.md`, [brimdata/zed](https://github.com/brimdata/zed) was
installed.
By running `brew install --cask zed`, zed was installed properly.
It seems like `--cask` option is needed.

### Environments:
- OS:
    - macOS 13.4
- Homebrew version: 
    ```sh
    Homebrew 4.2.17-40-gef1c54e
Homebrew/homebrew-core (git revision 0f61f2950ec; last commit
2024-04-03)
Homebrew/homebrew-cask (git revision 40c0a17ee0; last commit 2024-04-03)
    ```

Release Notes:

- N/A
2024-04-11 09:12:23 -04:00
Piotr Osiewicz
2f616fe8eb workspace: Add restore_on_startup setting that allows always opening an empty Zed instance (#10366)
Fixes #7694
The new setting accepts "last_workspace" (default) and "none" as
options.

In a follow-up PR I'll add a new option that re-launches all of the Zed
windows and not just the last one.

Release Notes:

- Added `restore_on_startup` option, accepting `last_workspace`
(default) and `none` options. With `none`, new Zed instances will not
restore workspaces that were open last.
2024-04-11 13:27:27 +02:00
Bennet Bo Fenner
fef0516f5b markdown preview: Allow toggling checkbox by click (#10364)
Release Notes:

- Added support for toggling a checkbox in markdown preview by clicking
on it (cmd+click)
([#5226](https://github.com/zed-industries/zed/issues/5226)).

---------

Co-authored-by: Remco Smits <62463826+RemcoSmitsDev@users.noreply.github.com>
2024-04-11 13:09:21 +02:00
Joseph T. Lyons
eb6f7c1240 Remove if-not-else patterns (#10402) 2024-04-11 03:48:06 -04:00
Remco Smits
36a87d0f5c Add allow to click on the reply preview to go to the message (#10357)
Release Notes:

- Added support for scrolling to the message you are replying to when
clicking on the reply preview
([#10028](https://github.com/zed-industries/zed/issues/10028)).

Co-authored-by: Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com>
2024-04-11 09:36:00 +02:00
Joseph T. Lyons
43c115a747 Use set literal notation 2024-04-11 00:43:56 -04:00
Joseph T. Lyons
859c5279c4 Allow arrow keys to be used in tab switcher (#10396)
In addition to `ctrl-tab` and `ctrl-shift-tab`, VS Code allows changing
the selection in the tab switcher via the up and down arrow keys.

Release Notes:

- Added bindings to allow `up` and `down` arrow keys to be used while
the tab switcher is open.
2024-04-11 00:11:48 -04:00
Marshall Bowers
13c14d9b96 Proxy Danger requests through a proxy service (#10395)
This PR updates Danger to proxy its requests to GitHub through a proxy
service.

## Motivation

Currently Danger is not able to run on PRs opened from forks of Zed.

This is due to GitHub Actions' security policies. Forks are not able to
see any of the repository secrets, and the built-in
`secrets.GITHUB_TOKEN` has its permissions
[restricted](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token)
to only reads when running on forks.

I asked around on the Danger repo, and some big projects
(DefinitelyTyped) are working around this by using a publicly-listed
(although slightly obfuscated) token:
https://github.com/danger/danger-js/issues/918#issuecomment-2048629487.

While this approach is _probably_ okay given the limited scope and
permissions of the GitHub token, I would still prefer a solution that
avoids disclosing the token at all.

## Explanation

I ended up writing a small proxy service, [Danger
Proxy](https://github.com/maxdeviant/danger-proxy), that can be used to
provide Danger with the ability to make authenticated GitHub requests,
but without disclosing the token.

From the README:

> Danger Proxy will:
>
> - Proxy all requests to `/github/*` to the GitHub API. The provided
GitHub API token will be used for authentication.
> - Restrict requests to the list of repositories specified in the
`ALLOWED_REPOS` environment variable.
> - Restrict requests to the subset of the GitHub API that Danger
requires.

I have an instance of this service deployed to
[danger-proxy.fly.dev](https://danger-proxy.fly.dev/).

Release Notes:

- N/A
2024-04-11 00:01:20 -04:00
Conrad Irwin
3b68665277 Update resvg to fix panic (#10393)
This bumps us up a *long* way on the resvg/usvg crate, to fix a panic

Release Notes:

- Fixed a panic when rendering certain malformed SVGs
2024-04-10 20:12:05 -06:00
Mehmet Efe Akça
339b29ef17 Add open vim keymap command (#9953)
Release Notes:

- Added a `vim: open default keymap` command to show the default Vim
keymap ([#8593](https://github.com/zed-industries/zed/issues/8593)).

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-04-10 19:03:13 -04:00
Kirill Bulatov
d1ad96782c Rework task modal (#10341)
New list (used tasks are above the separator line, sorted by the usage
recency), then all language tasks, then project-local and global tasks
are listed.
Note that there are two test tasks (for `test_name_1` and `test_name_2`
functions) that are created from the same task template:
<img width="563" alt="Screenshot 2024-04-10 at 01 00 46"
src="https://github.com/zed-industries/zed/assets/2690773/7455a82f-2af2-47bf-99bd-d9c5a36e64ab">

Tasks are deduplicated by labels, with the used tasks left in case of
the conflict with the new tasks from the template:
<img width="555" alt="Screenshot 2024-04-10 at 01 01 06"
src="https://github.com/zed-industries/zed/assets/2690773/8f5a249e-abec-46ef-a991-08c6d0348648">

Regular recent tasks can be now removed too:
<img width="565" alt="Screenshot 2024-04-10 at 01 00 55"
src="https://github.com/zed-industries/zed/assets/2690773/0976b8fe-b5d7-4d2a-953d-1d8b1f216192">

When the caret is in the place where no function symbol could be
retrieved, no cargo tests for function are listed in tasks:
<img width="556" alt="image"
src="https://github.com/zed-industries/zed/assets/2690773/df30feba-fe27-4645-8be9-02afc70f02da">


Part of https://github.com/zed-industries/zed/issues/10132
Reworks the task code to simplify it and enable proper task labels.

* removes `trait Task`, renames `Definition` into `TaskTemplate` and use
that instead of `Arc<dyn Task>` everywhere
* implement more generic `TaskId` generation that depends on the
`TaskContext` and `TaskTemplate`
* remove `TaskId` out of the template and only create it after
"resolving" the template into the `ResolvedTask`: this way, task
templates, task state (`TaskContext`) and task "result" (resolved state)
are clearly separated and are not mixed
* implement the logic for filtering out non-related language tasks and
tasks that have non-resolved Zed task variables
* rework Zed template-vs-resolved-task display in modal: now all reruns
and recently used tasks are resolved tasks with "fixed" context (unless
configured otherwise in the task json) that are always shown, and Zed
can add on top tasks with different context that are derived from the
same template as the used, resolved tasks
* sort the tasks list better, showing more specific and least recently
used tasks higher
* shows a separator between used and unused tasks, allow removing the
used tasks same as the oneshot ones
* remote the Oneshot task source as redundant: all oneshot tasks are now
stored in the inventory's history
* when reusing the tasks as query in the modal, paste the expanded task
label now, show trimmed resolved label in the modal
* adjusts Rust and Elixir task labels to be more descriptive and closer
to bash scripts

Release Notes:

- Improved task modal ordering, run and deletion capabilities
2024-04-11 02:02:04 +03:00
Jonathan Green
b0eda77d73 Add a few tests to cover other folder names (#10356)
Added additional tests to cover other folder names in regards to
[#10193](https://github.com/zed-industries/zed/issues/10193) and
[#9729](https://github.com/zed-industries/zed/issues/9729).

I had a similar issue but with folder names like '2.5' and '2.5_backup'.
I didn't see test coverage for those kinds of file names, so wanted to
add them.

![2024-04-10 09 07
03](https://github.com/zed-industries/zed/assets/127535196/eef26ce2-b0c7-4b1f-98ed-426ce1df0af2)

Release Notes:

- N/A

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-04-10 18:47:53 -04:00
Kirill Bulatov
fd3ee5a9d0 Use proper Workspace references when querying for tasks by name (#10383)
Closes https://github.com/zed-industries/zed/issues/10380

Release Notes:

- Fixed Zed panicking when running tasks via a keybinding
([10380](https://github.com/zed-industries/zed/issues/10380))
2024-04-11 00:42:17 +03:00
Marshall Bowers
8cbdd9e0fa Refactor workspace notifications to use explicit NotificationId type (#10342)
This PR reworks the way workspace notifications are identified to use a
new `NotificationId` type.

A `NotificationId` is bound to a given type that is used as a unique
identifier. Generally this will be a unit struct that can be used to
uniquely identify this notification.

A `NotificationId` can also accept an optional `ElementId` in order to
distinguish between different notifications of the same type.

This system avoids the issue we had previously of selecting `usize` IDs
somewhat arbitrarily and running the risk of having two independent
notifications collide (and thus interfere with each other).

This also fixes a bug where multiple suggestion notifications for the
same extension could be live at once

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

Release Notes:

- Fixed a bug where multiple extension suggestions for the same
extension could be shown at once
([#10320](https://github.com/zed-industries/zed/issues/10320)).

---------

Co-authored-by: Max <max@zed.dev>
2024-04-10 17:21:23 -04:00
Joel Selvaraj
322f68f3d6 linux: wayland: fix cursor set_icon (#10374)
Release Notes:

- Partially (Wayland implementation) Fixed
[#10124](https://github.com/zed-industries/zed/issues/10124)).

The recent refactor of the linux gpui implementation
(https://github.com/zed-industries/zed/pull/10227) broke the wayland
cursor update logic by setting the cursor icon as `None`. Fix it by
setting the `cursor_icon_name`.
2024-04-10 12:59:33 -07:00
Marshall Bowers
195f9d9b24 haskell: Bump to v0.1.0 (#10378)
This PR bumps the Haskell extension to v0.1.0.

This version of the Haskell extension brings improved labels for
symbols:

<img width="569" alt="Screenshot 2024-04-10 at 3 38 01 PM"
src="https://github.com/zed-industries/zed/assets/1486634/759fa52f-7b85-4395-abfd-070138201162">

Release Notes:

- N/A
2024-04-10 15:42:46 -04:00
Marshall Bowers
3a6e0bb9b6 gleam: Bump to v0.1.0 (#10376)
This PR bumps the Gleam extension to v0.1.0.

This version of the Gleam extension brings improved completion labels:

<img width="572" alt="Screenshot 2024-04-10 at 3 30 25 PM"
src="https://github.com/zed-industries/zed/assets/1486634/afca4563-c520-4f01-949f-2c8095769751">

Release Notes:

- N/A
2024-04-10 15:33:22 -04:00
张小白
fdddbfc179 Fix caret movement issue for some special characters (#10198)
Currently in Zed, certain characters require pressing the key twice to
move the caret through that character. For example: "❤️" and "y̆".

The reason for this is as follows:

Currently, Zed uses `chars` to distinguish different characters, and
calling `chars` on `y̆` will yield two `char` values: `y` and `\u{306}`,
and calling `chars` on `❤️` will yield two `char` values: `❤` and
`\u{fe0f}`.

Therefore, consider the following scenario (where ^ represents the
caret):

- what we see: ❤️ ^
- the actual buffer: ❤ \u{fe0f} ^

After pressing the left arrow key once:

- what we see: ❤️ ^
- the actual buffer: ❤ ^ \u{fe0f}

After pressing the left arrow key again:
- what we see: ^ ❤️
- the actual buffer: ^ ❤ \u{fe0f}

Thus, two left arrow key presses are needed to move the caret, and this
PR fixes this bug (or this is actually a feature?).

I have tried to keep the scope of code modifications as minimal as
possible. In this PR, Zed handles such characters as follows:

- what we see: ❤️ ^
- the actual buffer: ❤ \u{fe0f} ^

After pressing the left arrow key once:

- what we see: ^ ❤️
- the actual buffer: ^ ❤ \u{fe0f}

Or after pressing the delete key:

- what we see: ^
- the actual buffer: ^

Please note that currently, different platforms and software handle
these special characters differently, and even the same software may
handle these characters differently in different situations. For
example, in my testing on Chrome on macOS, GitHub treats `y̆` as a
single character, just like in this PR; however, in Rust Playground,
`y̆` is treated as two characters, and pressing the delete key does not
delete the entire `y̆` character, but instead deletes `\u{306}` to yield
the character `y`. And they both treat `❤️` as a single character,
pressing the delete key will delete the entire `❤️` character.

This PR is based on the principle of making changes with the smallest
impact on the code, and I think that deleting the entire character with
the delete key is more intuitive.

Release Notes:

- Fix caret movement issue for some special characters

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Thorsten <thorsten@zed.dev>
Co-authored-by: Bennet <bennetbo@gmx.de>
2024-04-10 13:01:25 -06:00
张小白
3648d79ddb Reduce the frequency writing window size and position information to the database (#10315)
Release Notes:

- N/A
2024-04-10 12:56:51 -06:00
Marshall Bowers
081e9b9a60 Fix panic when loading malformed Wasm files (#10370)
This PR fixes a potential panic that could occur when loading malformed
Wasm files.

We now use the `parse_wasm_extension_version` function that was
previously used just to extract the Zed extension API version from the
Wasm bytes as a pre-validation step. By parsing the entirety of the Wasm
file here instead of returning as soon as we find the version, the
invalid Wasm bytes are now surfaced as an `Err` instead of a panic.

We were able to replicate the panic using the following test:

```rs
#[gpui::test]
async fn test_bad_wasm(cx: &mut TestAppContext) {
    init_test(cx);

    let wasm_host = cx.update(|cx| {
        WasmHost::new(
            FakeFs::new(cx.background_executor().clone()),
            FakeHttpClient::with_200_response(),
            FakeNodeRuntime::new(),
            Arc::new(LanguageRegistry::test(cx.background_executor().clone())),
            PathBuf::from("/the/work/dir".to_string()),
            cx,
        )
    });

    let mut wasm_bytes = std::fs::read("/Users/maxdeviant/Library/Application Support/Zed/extensions/installed/dart/extension.wasm").unwrap();

    // This is the error message we were seeing in the stack trace:
    // range end index 267037 out of range for slice of length 253952

    dbg!(&wasm_bytes.len());

    // Truncate the bytes to the same point:
    wasm_bytes.truncate(253952);

    std::fs::write("/tmp/bad-extension.wasm", wasm_bytes.clone()).unwrap();

    let manifest = Arc::new(ExtensionManifest {
        id: "the-extension".into(),
        name: "The Extension".into(),
        version: "0.0.1".into(),
        schema_version: SchemaVersion(1),
        description: Default::default(),
        repository: Default::default(),
        authors: Default::default(),
        lib: LibManifestEntry {
            kind: None,
            version: None,
        },
        themes: Default::default(),
        languages: Default::default(),
        grammars: Default::default(),
        language_servers: Default::default(),
    });

    // 💥
    let result = wasm_host
        .load_extension(wasm_bytes, manifest, cx.executor())
        .await;
    dbg!(result.map(|_| ()));
```



Release Notes:

- Fixed a crash that could occur when loading malformed Wasm extensions
([#10352](https://github.com/zed-industries/zed/issues/10352)).

---------

Co-authored-by: Max <max@zed.dev>
2024-04-10 14:13:43 -04:00
Marshall Bowers
3cf93dfcf6 Revert "Update GITHUB_TOKEN environment variable for Danger (#10365)"
Danger doesn't appear to work with PRs from forks: https://github.com/danger/danger-js/issues/918

Will need to research this some more.

This reverts commit 53d0cc6146.
2024-04-10 13:29:54 -04:00
Marshall Bowers
53d0cc6146 Update GITHUB_TOKEN environment variable for Danger (#10365)
This PR updates the `GITHUB_TOKEN` environment variable that we use for
Danger so that it should (hopefully) be able to run for PRs of external
contributors.

Followed the instructions outlined
[here](https://danger.systems/js/guides/getting_started#setting-up-danger-to-run-on-your-ci).

Danger comments will now by left by the @zed-industries-bot:

<img width="951" alt="Screenshot 2024-04-10 at 1 07 15 PM"
src="https://github.com/zed-industries/zed/assets/1486634/d28cd537-9626-47df-8878-75a778824ef4">

Release Notes:

- N/A
2024-04-10 13:09:27 -04:00
Nate Butler
03d853d344 Introduce TextField by adding the ui_text_field crate (#10361)
There hasn't been a componentized way to create inputs or text fields
thus far due to the innate circular dependency between the `ui` and
`editor` crates. To bypass this issue we are introducing a new
`ui_text_field` crate to specifically handle this component.

`TextField` provides the ability to add stacked or inline labels, as
well as applies a standard visual style to inputs.

Example:

![CleanShot - 2024-04-10 at 11 22
13@2x](https://github.com/zed-industries/zed/assets/1714999/9bf5fc40-5024-4d01-9a8b-fb76f67d7e6e)

We'll continue to evolve this component in the near future and start
using it in the app once we've built out the needed functionality.

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-04-10 11:53:25 -04:00
Joseph T. Lyons
d03f1c4cab v0.132.x dev 2024-04-10 11:45:05 -04:00
Abykhodau Yury
b82dda64e1 Merge github.com:zed-industries/zed into auto-folded-dirs 2024-04-03 21:49:18 +03:00
Abykhodau Yury
dfbf6b9ee9 Improve performance of project panel 2024-04-03 21:49:13 +03:00
Abykhodau Yury
dbd2a247db fix Snapshot import issue 2024-03-28 23:19:51 +02:00
Abykhodau Yury
da4af5a651 Merge github.com:zed-industries/zed into auto-folded-dirs 2024-03-28 20:44:56 +02:00
Abykhodau Yury
e9c33bd819 Delete collecting the vector in is_foldable check 2024-03-28 20:41:41 +02:00
Abykhodau Yury
3dd6a7c6e5 Merge branch 'main' of github.com:zed-industries/zed into auto-folded-dirs 2024-02-28 22:10:32 +02:00
Abykhodau Yury
b946797390 Solve performance issues with project panel auto folding dirs 2024-02-28 22:10:21 +02:00
Abykhodau Yury
fd32cc4679 Merge branch 'main' of github.com:zed-industries/zed into auto-folded-dirs 2024-02-26 10:58:26 +02:00
Abykhodau Yury
4d5509be6b Refactor project panel tests according to changes in auto folding of directories 2024-02-26 10:56:56 +02:00
Abykhodau Yury
17cea27bbc Add support of fold/unfold directory functionality 2024-02-25 22:39:16 +02:00
Abykhodau Yury
fe4b744ae9 Refactor project_panel to get rid of redundant state 2024-02-25 14:46:45 +02:00
Abykhodau Yury
a5b8b5fdb3 Adjust tests after adding functioality of auto collapsed dir paths 2024-02-12 11:00:06 +02:00
Abykhodau Yury
4275283201 Merge branch 'main' of github.com:zed-industries/zed into auto-folded-dirs 2024-02-12 08:33:07 +02:00
Abykhodau Yury
6de4aa5990 Adding test for auto collapsing dir paths 2024-02-11 18:32:03 +02:00
Abykhodau Yury
b375001228 Add 'auto_collapse_dirs' parameter to project_panel settings 2024-02-10 21:36:25 +02:00
Abykhodau Yury
00dd254ba9 Add support of fold/unfold action for directories 2024-02-10 19:26:01 +02:00
Abykhodau Yury
00d8a92326 Add support for collapsed paths in nested directories within the project panel 2024-02-10 15:00:02 +02:00
300 changed files with 12386 additions and 4456 deletions

View File

@@ -32,4 +32,10 @@ jobs:
- name: Run Danger
run: pnpm run --dir script/danger danger ci
env:
GITHUB_TOKEN: ${{ github.token }}
# This GitHub token is not used, but the value needs to be here to prevent
# Danger from throwing an error.
GITHUB_TOKEN: "not_a_real_token"
# All requests are instead proxied through an instance of
# https://github.com/maxdeviant/danger-proxy that allows Danger to securely
# authenticate with GitHub while still being able to run on PRs from forks.
DANGER_GITHUB_API_BASE_URL: "https://danger-proxy.fly.dev/github"

View File

@@ -0,0 +1,7 @@
[
{
"label": "clippy",
"command": "cargo",
"args": ["xtask", "clippy"]
}
]

View File

@@ -2,8 +2,6 @@
Thanks for your interest in contributing to Zed, the collaborative platform that is also a code editor!
We want to avoid anyone spending time on a pull request that may not be accepted, so we suggest you discuss your ideas with the team and community before starting on major changes. Bug fixes, however, are almost always welcome.
All activity in Zed forums is subject to our [Code of Conduct](https://zed.dev/docs/code-of-conduct). Additionally, contributors must sign our [Contributor License Agreement](https://zed.dev/cla) before their contributions can be merged.
## Contribution ideas

750
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -38,6 +38,7 @@ members = [
"crates/google_ai",
"crates/gpui",
"crates/gpui_macros",
"crates/headless",
"crates/image_viewer",
"crates/install_cli",
"crates/journal",
@@ -72,6 +73,7 @@ members = [
"crates/task",
"crates/tasks_ui",
"crates/search",
"crates/semantic_index",
"crates/semantic_version",
"crates/settings",
"crates/snippet",
@@ -90,6 +92,7 @@ members = [
"crates/telemetry_events",
"crates/time_format",
"crates/ui",
"crates/ui_text_field",
"crates/util",
"crates/vcs_menu",
"crates/vim",
@@ -103,15 +106,19 @@ members = [
"extensions/clojure",
"extensions/csharp",
"extensions/dart",
"extensions/elm",
"extensions/emmet",
"extensions/erlang",
"extensions/gleam",
"extensions/haskell",
"extensions/html",
"extensions/lua",
"extensions/ocaml",
"extensions/php",
"extensions/prisma",
"extensions/purescript",
"extensions/svelte",
"extensions/terraform",
"extensions/toml",
"extensions/uiua",
"extensions/zig",
@@ -161,6 +168,7 @@ go_to_line = { path = "crates/go_to_line" }
google_ai = { path = "crates/google_ai" }
gpui = { path = "crates/gpui" }
gpui_macros = { path = "crates/gpui_macros" }
headless = { path = "crates/headless" }
install_cli = { path = "crates/install_cli" }
image_viewer = { path = "crates/image_viewer" }
journal = { path = "crates/journal" }
@@ -214,6 +222,7 @@ theme_selector = { path = "crates/theme_selector" }
telemetry_events = { path = "crates/telemetry_events" }
time_format = { path = "crates/time_format" }
ui = { path = "crates/ui" }
ui_text_field = { path = "crates/ui_text_field" }
util = { path = "crates/util" }
vcs_menu = { path = "crates/vcs_menu" }
vim = { path = "crates/vim" }
@@ -238,15 +247,18 @@ chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4.4", features = ["derive"] }
clickhouse = { version = "0.11.6" }
ctor = "0.2.6"
ctrlc = "3.4.4"
core-foundation = { version = "0.9.3" }
core-foundation-sys = "0.8.6"
derive_more = "0.99.17"
emojis = "0.6.1"
env_logger = "0.9"
futures = "0.3"
futures-batch = "0.6.1"
futures-lite = "1.13"
git2 = { version = "0.15", default-features = false }
globset = "0.4"
heed = { git = "https://github.com/meilisearch/heed", rev = "036ac23f73a021894974b9adc815bc95b3e0482a", features = ["read-txn-no-tls"] }
hex = "0.4.3"
ignore = "0.4.22"
indoc = "1"
@@ -310,26 +322,19 @@ tree-sitter-c = "0.20.1"
tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "f44509141e7e483323d2ec178f2d2e6c0fc041c1" }
tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" }
tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "a2861e88a730287a60c11ea9299c033c7d076e30" }
tree-sitter-elm = { git = "https://github.com/elm-tooling/tree-sitter-elm", rev = "692c50c0b961364c40299e73c1306aecb5d20f40" }
tree-sitter-embedded-template = "0.20.0"
tree-sitter-glsl = { git = "https://github.com/theHamsta/tree-sitter-glsl", rev = "2a56fb7bc8bb03a1892b4741279dd0a8758b7fb3" }
tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" }
tree-sitter-gomod = { git = "https://github.com/camdencheek/tree-sitter-go-mod" }
tree-sitter-gowork = { git = "https://github.com/d1y/tree-sitter-go-work" }
tree-sitter-hcl = { git = "https://github.com/MichaHoffmann/tree-sitter-hcl", rev = "v1.1.0" }
rustc-demangle = "0.1.23"
tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "2e1348c3cf2c9323e87c2744796cf3f3868aa82a" }
tree-sitter-html = "0.19.0"
tree-sitter-jsdoc = { git = "https://github.com/tree-sitter/tree-sitter-jsdoc", ref = "6a6cf9e7341af32d8e2b2e24a37fbfebefc3dc55" }
tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "40a81c01a40ac48744e0c8ccabbaba1920441199" }
tree-sitter-lua = "0.0.14"
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" }
tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "7dd29f9616822e5fc259f5b4ae6c4ded9a71a132" }
tree-sitter-ocaml = { git = "https://github.com/tree-sitter/tree-sitter-ocaml", rev = "4abfdc1c7af2c6c77a370aee974627be1c285b3b" }
tree-sitter-proto = { git = "https://github.com/rewinfrey/tree-sitter-proto", rev = "36d54f288aee112f13a67b550ad32634d0c2cb52" }
tree-sitter-python = "0.20.2"
tree-sitter-racket = { git = "https://github.com/zed-industries/tree-sitter-racket", rev = "eb010cf2c674c6fd9a6316a84e28ef90190fe51a" }
tree-sitter-regex = "0.20.0"
tree-sitter-ruby = "0.20.0"
tree-sitter-rust = "0.20.3"
@@ -339,8 +344,9 @@ tree-sitter-vue = { git = "https://github.com/zed-industries/tree-sitter-vue", r
tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "f545a41f57502e1b5ddf2a6668896c1b0620f930" }
unindent = "0.1.7"
unicase = "2.6"
unicode-segmentation = "1.10"
url = "2.2"
uuid = { version = "1.1.2", features = ["v4"] }
uuid = { version = "1.1.2", features = ["v4", "v5"] }
wasmparser = "0.201"
wasm-encoder = "0.201"
wasmtime = { version = "19.0.0", default-features = false, features = [

View File

@@ -17,7 +17,7 @@ Support for additional platforms is on our [roadmap](https://zed.dev/roadmap):
For macOS users, you can also install Zed using [Homebrew](https://brew.sh/):
```sh
brew install zed
brew install --cask zed
```
Alternatively, to install the Preview release:

View File

@@ -1,4 +1,3 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 10L21 7L17 3L14 6M18 10L8 20H4V16L14 6M18 10L14 6" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="m12 6.668 2-2L11.332 2l-2 2M12 6.668l-6.668 6.664H2.668v-2.664L9.332 4M12 6.668 9.332 4" stroke="black" stroke-width="1" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 379 B

After

Width:  |  Height:  |  Size: 239 B

4
assets/icons/regex.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="4" cy="11" r="1" fill="#787D87"/>
<path d="M9 2.5V5M9 5V7.5M9 5H11.5M9 5H6.5M9 5L10.6667 3.33333M9 5L7.33333 6.6667M9 5L10.6667 6.6667M9 5L7.33333 3.33333" stroke="#787D87" stroke-width="1.25" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 333 B

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 17V15.8C20 14.1198 20 13.2798 19.673 12.638C19.3854 12.0735 18.9265 11.6146 18.362 11.327C17.7202 11 16.8802 11 15.2 11H4M4 11L8 7M4 11L8 15" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 468 B

View File

@@ -1,56 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg
width="800px"
height="800px"
viewBox="0 0 24 24"
fill="none"
version="1.1"
id="svg1"
sodipodi:docname="reply-svgrepo-com.svg"
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#505050"
showgrid="false"
inkscape:zoom="0.39996789"
inkscape:cx="435.03492"
inkscape:cy="417.53351"
inkscape:window-width="1440"
inkscape:window-height="847"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<g
id="SVGRepo_bgCarrier"
stroke-width="0" />
<g
id="SVGRepo_tracerCarrier"
stroke-linecap="round"
stroke-linejoin="round" />
<g
id="SVGRepo_iconCarrier"
transform="matrix(-1,0,0,1,24.001548,0)">
<path
d="M 20,17 V 15.8 C 20,14.1198 20,13.2798 19.673,12.638 19.3854,12.0735 18.9265,11.6146 18.362,11.327 17.7202,11 16.8802,11 15.2,11 H 4 m 0,0 4,-4 m -4,4 4,4"
stroke="#000000"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
id="path1" />
</g>
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.668 11.332v-.797c0-1.12 0-1.683.219-2.11.191-.374.496-.683.87-.874.43-.219.99-.219 2.11-.219h7.469m0 0-2.668-2.664m2.668 2.664L10.668 10" stroke="black" stroke-width="1.33334" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 296 B

View File

@@ -1,5 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.5 7V9.5M9.5 12V9.5M12 9.5H9.5M7 9.5H9.5M9.5 9.5L11.1667 7.83333M9.5 9.5L7.83333 11.1667M9.5 9.5L11.1667 11.1667M9.5 9.5L7.83333 7.83333" stroke="#11181C" stroke-width="1.25" stroke-linecap="round"/>
<path d="M2.19366 3.84943C2.19188 4.26418 2.32864 4.59864 2.60673 4.84707C2.88052 5.09166 3.25136 5.26933 3.71609 5.3824C3.71616 5.38242 3.71623 5.38243 3.7163 5.38245L4.30919 5.53134L4.30919 5.53134L4.30965 5.53145C4.50649 5.57891 4.67124 5.63133 4.80447 5.68843L4.80469 5.68852C4.93838 5.74508 5.03564 5.81206 5.10001 5.8877L5.10001 5.8877L5.10041 5.88816C5.16432 5.96142 5.19716 6.05222 5.19716 6.16389C5.19716 6.28412 5.1609 6.38933 5.0882 6.48141C5.01496 6.57418 4.91031 6.64838 4.77141 6.70259L4.77121 6.70266C4.63472 6.75659 4.47185 6.7843 4.28146 6.7843C4.08801 6.7843 3.91607 6.75496 3.76491 6.69726C3.61654 6.6382 3.49924 6.55209 3.41132 6.43942C3.3502 6.35821 3.30747 6.26204 3.28375 6.14992C3.26238 6.04888 3.1772 5.96225 3.06518 5.96225H2.26366C2.14682 5.96225 2.04842 6.05919 2.0592 6.18012C2.08842 6.50802 2.1826 6.79102 2.34331 7.02735L2.34352 7.02767C2.53217 7.30057 2.79377 7.50587 3.12633 7.64399L3.12642 7.64402C3.46009 7.78185 3.84993 7.85 4.29476 7.85C4.74293 7.85 5.12859 7.7828 5.45023 7.64651L5.45036 7.64646C5.77328 7.50857 6.02259 7.31417 6.19551 7.06217C6.37037 6.80817 6.4579 6.50901 6.45972 6.16682L6.45972 6.16616C6.4579 5.9333 6.41513 5.72482 6.33012 5.54178C6.2474 5.35987 6.13061 5.20175 5.98007 5.06773C5.83038 4.93448 5.65389 4.82273 5.4511 4.7322C5.24919 4.64206 5.02795 4.57016 4.78757 4.51632L4.29841 4.39935L4.29841 4.39934L4.29771 4.39919C4.18081 4.37301 4.07116 4.34168 3.9687 4.30523C3.86715 4.26734 3.77847 4.22375 3.70232 4.17471C3.62796 4.12508 3.57037 4.06717 3.52849 4.00124C3.49012 3.93815 3.47157 3.86312 3.47481 3.77407L3.47484 3.77407V3.77225C3.47484 3.66563 3.50527 3.57146 3.56612 3.48808C3.6287 3.40475 3.71977 3.33801 3.84235 3.28931L3.84235 3.28932L3.84289 3.28909C3.96465 3.23906 4.1165 3.21304 4.30008 3.21304C4.57006 3.21304 4.77746 3.27105 4.92754 3.38154C5.04235 3.46608 5.11838 3.57594 5.15673 3.71259C5.18352 3.80802 5.26636 3.89142 5.37611 3.89142H6.17259C6.28852 3.89142 6.38806 3.7953 6.37515 3.67382C6.34686 3.4077 6.26051 3.16831 6.1158 2.95658C5.94159 2.70169 5.6982 2.50368 5.38762 2.36201L5.36687 2.4075M2.19366 3.84943C2.19187 3.51004 2.28242 3.21139 2.46644 2.9556L2.46658 2.9554C2.65148 2.70093 2.90447 2.50326 3.22368 2.36179C3.54316 2.2202 3.90494 2.15 4.30807 2.15C4.71809 2.15 5.07841 2.22014 5.38773 2.36206L5.36687 2.4075M2.19366 3.84943C2.19366 3.84951 2.19366 3.84959 2.19366 3.84967L2.24366 3.8494L2.19366 3.84918C2.19366 3.84926 2.19366 3.84935 2.19366 3.84943ZM5.36687 2.4075C5.06537 2.26917 4.71244 2.2 4.30807 2.2C3.91079 2.2 3.55608 2.26917 3.24394 2.4075C2.93179 2.54584 2.68616 2.73827 2.50703 2.9848L3.82389 3.24285L3.82389 3.24285C3.95336 3.18964 4.11209 3.16304 4.30008 3.16304C4.57676 3.16304 4.79579 3.22245 4.95718 3.34128C5.08094 3.43239 5.1635 3.55166 5.20487 3.69908C5.2271 3.77827 5.29386 3.84142 5.37611 3.84142H6.17259C6.26198 3.84142 6.33488 3.76799 6.32543 3.6791C6.29797 3.4208 6.21433 3.18936 6.07452 2.9848C5.90603 2.73827 5.67015 2.54584 5.36687 2.4075ZM4.78958 6.74917C4.64593 6.80592 4.47655 6.8343 4.28146 6.8343C4.08283 6.8343 3.90458 6.80415 3.74674 6.74384C3.59067 6.68177 3.46563 6.59043 3.37163 6.46983L4.78958 6.74917ZM4.78958 6.74917C4.93502 6.69241 5.04764 6.61349 5.12745 6.5124M4.78958 6.74917L5.12745 6.5124M5.12745 6.5124C5.20726 6.4113 5.24716 6.29514 5.24716 6.16389M5.12745 6.5124L5.24716 6.16389M5.24716 6.16389C5.24716 6.04152 5.2108 5.93865 5.13809 5.85529L5.24716 6.16389Z" fill="#687076" stroke="#687076" stroke-width="0.1"/>
<path d="M9.5 7V9.5M9.5 9.5V12M9.5 9.5H12M9.5 9.5H7M9.5 9.5L11.1667 7.83333M9.5 9.5L7.83333 11.1667M9.5 9.5L11.1667 11.1667M9.5 9.5L7.83333 7.83333" stroke="#687076" stroke-width="1.25" stroke-linecap="round"/>
<path d="M2.19368 3.84945C2.1919 4.2642 2.32866 4.59866 2.60675 4.84709C2.88054 5.09168 3.25138 5.26935 3.71611 5.38242L4.30921 5.53136C4.50605 5.57882 4.67126 5.63135 4.80449 5.68845C4.93818 5.74501 5.03566 5.81208 5.10003 5.88772C5.16394 5.96098 5.19718 6.05224 5.19718 6.16391C5.19718 6.28414 5.16092 6.38935 5.08822 6.48143C5.01498 6.5742 4.91033 6.6484 4.77143 6.70261C4.63494 6.75654 4.47187 6.78432 4.28148 6.78432C4.08803 6.78432 3.91609 6.75498 3.76493 6.69728C3.61656 6.63822 3.49926 6.55211 3.41134 6.43944C3.35022 6.35823 3.30749 6.26206 3.28377 6.14994C3.2624 6.0489 3.17722 5.96227 3.0652 5.96227H2.26368C2.14684 5.96227 2.04844 6.05921 2.05922 6.18014C2.08844 6.50804 2.18262 6.79104 2.34333 7.02737C2.53198 7.30027 2.79379 7.50589 3.12635 7.64401C3.46002 7.78184 3.84995 7.85002 4.29478 7.85002C4.74295 7.85002 5.12861 7.78282 5.45025 7.64653C5.77317 7.50864 6.02261 7.31419 6.19553 7.06219C6.37039 6.80819 6.45792 6.50903 6.45974 6.16684C6.45792 5.93398 6.41515 5.72484 6.33014 5.5418C6.24742 5.35989 6.13063 5.20177 5.98009 5.06775C5.8304 4.9345 5.65391 4.82275 5.45112 4.73222C5.24921 4.64208 5.02797 4.57018 4.78759 4.51634L4.29843 4.39937C4.18153 4.37319 4.07118 4.3417 3.96872 4.30525C3.86717 4.26736 3.77849 4.22377 3.70234 4.17473C3.62798 4.1251 3.57039 4.06719 3.52851 4.00126C3.49014 3.93817 3.47159 3.86314 3.47483 3.77409L3.47486 3.77227C3.47486 3.66565 3.50529 3.57148 3.56614 3.4881C3.62872 3.40477 3.71979 3.33803 3.84237 3.28933C3.96413 3.2393 4.11652 3.21306 4.3001 3.21306C4.57008 3.21306 4.77748 3.27107 4.92756 3.38156C5.04237 3.4661 5.1184 3.57596 5.15675 3.71261C5.18354 3.80804 5.26638 3.89144 5.37613 3.89144H6.17261C6.28854 3.89144 6.38808 3.79532 6.37517 3.67384C6.34688 3.40772 6.26053 3.16833 6.11582 2.9566C5.94161 2.70171 5.69822 2.5037 5.38764 2.36203L5.36689 2.40752M2.19368 3.84945C2.19189 3.51006 2.28244 3.21141 2.46646 2.95562C2.65136 2.70115 2.90449 2.50328 3.2237 2.36181C3.54318 2.22022 3.90496 2.15002 4.30809 2.15002C4.71811 2.15002 5.07832 2.22011 5.38764 2.36203L5.36689 2.40752M4.7896 6.74919C4.93504 6.69243 5.04766 6.61351 5.12747 6.51242ZM4.7896 6.74919L5.12747 6.51242ZM5.12747 6.51242C5.20728 6.41132 5.24718 6.29516 5.24718 6.16391ZM5.12747 6.51242L5.24718 6.16391ZM5.24718 6.16391C5.24718 6.04154 5.21082 5.93867 5.13811 5.85531L5.24718 6.16391Z" fill="#687076"/>
<path d="M2.19368 3.84945C2.1919 4.2642 2.32866 4.59866 2.60675 4.84709C2.88054 5.09168 3.25138 5.26935 3.71611 5.38242L4.30921 5.53136C4.50605 5.57882 4.67126 5.63135 4.80449 5.68845C4.93818 5.74501 5.03566 5.81208 5.10003 5.88772C5.16394 5.96098 5.19718 6.05224 5.19718 6.16391C5.19718 6.28414 5.16092 6.38935 5.08822 6.48143C5.01498 6.5742 4.91033 6.6484 4.77143 6.70261C4.63494 6.75654 4.47187 6.78432 4.28148 6.78432C4.08803 6.78432 3.91609 6.75498 3.76493 6.69728C3.61656 6.63822 3.49926 6.55211 3.41134 6.43944C3.35022 6.35823 3.30749 6.26206 3.28377 6.14994C3.2624 6.0489 3.17722 5.96227 3.0652 5.96227H2.26368C2.14684 5.96227 2.04844 6.05921 2.05922 6.18014C2.08844 6.50804 2.18262 6.79104 2.34333 7.02737C2.53198 7.30027 2.79379 7.50589 3.12635 7.64401C3.46002 7.78184 3.84995 7.85002 4.29478 7.85002C4.74295 7.85002 5.12861 7.78282 5.45025 7.64653C5.77317 7.50864 6.02261 7.31419 6.19553 7.06219C6.37039 6.80819 6.45792 6.50903 6.45974 6.16684C6.45792 5.93398 6.41515 5.72484 6.33014 5.5418C6.24742 5.35989 6.13063 5.20177 5.98009 5.06775C5.8304 4.9345 5.65391 4.82275 5.45112 4.73222C5.24921 4.64208 5.02797 4.57018 4.78759 4.51634L4.29843 4.39937C4.18153 4.37319 4.07118 4.3417 3.96872 4.30525C3.86717 4.26736 3.77849 4.22377 3.70234 4.17473C3.62798 4.1251 3.57039 4.06719 3.52851 4.00126C3.49014 3.93817 3.47159 3.86314 3.47483 3.77409L3.47486 3.77227C3.47486 3.66565 3.50529 3.57148 3.56614 3.4881C3.62872 3.40477 3.71979 3.33803 3.84237 3.28933C3.96413 3.2393 4.11652 3.21306 4.3001 3.21306C4.57008 3.21306 4.77748 3.27107 4.92756 3.38156C5.04237 3.4661 5.1184 3.57596 5.15675 3.71261C5.18354 3.80804 5.26638 3.89144 5.37613 3.89144H6.17261C6.28854 3.89144 6.38808 3.79532 6.37517 3.67384C6.34688 3.40772 6.26053 3.16833 6.11582 2.9566C5.94161 2.70171 5.69822 2.5037 5.38764 2.36203M2.19368 3.84945C2.19189 3.51006 2.28244 3.21141 2.46646 2.95562C2.65136 2.70115 2.90449 2.50328 3.2237 2.36181C3.54318 2.22022 3.90496 2.15002 4.30809 2.15002C4.71811 2.15002 5.07832 2.22011 5.38764 2.36203M2.19368 3.84945L2.24368 3.84942M5.38764 2.36203L5.36689 2.40752M5.36689 2.40752C5.06539 2.26919 4.71246 2.20002 4.30809 2.20002C3.91081 2.20002 3.5561 2.26919 3.24396 2.40752C2.93181 2.54586 2.68618 2.73829 2.50705 2.98482M5.36689 2.40752C5.67017 2.54586 5.90605 2.73829 6.07454 2.98482C6.21435 3.18938 6.29799 3.42082 6.32545 3.67912C6.3349 3.76801 6.262 3.84144 6.17261 3.84144H5.37613C5.29388 3.84144 5.22712 3.77829 5.20489 3.6991C5.16352 3.55168 5.08096 3.43241 4.9572 3.3413C4.79581 3.22247 4.57678 3.16306 4.3001 3.16306M4.7896 6.74919C4.64595 6.80594 4.47657 6.83432 4.28148 6.83432C4.08285 6.83432 3.9046 6.80417 3.74676 6.74386C3.59069 6.68179 3.46565 6.59045 3.37165 6.46985M4.7896 6.74919C4.93504 6.69243 5.04766 6.61351 5.12747 6.51242M4.7896 6.74919L5.12747 6.51242M5.12747 6.51242C5.20728 6.41132 5.24718 6.29516 5.24718 6.16391M5.12747 6.51242L5.24718 6.16391M5.24718 6.16391C5.24718 6.04154 5.21082 5.93867 5.13811 5.85531L5.24718 6.16391Z" stroke="#687076" stroke-width="0.1"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

5
assets/icons/server.svg Normal file
View File

@@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.99993 6.85713C11.1558 6.85713 13.7142 5.83379 13.7142 4.57142C13.7142 3.30905 11.1558 2.28571 7.99993 2.28571C4.84402 2.28571 2.28564 3.30905 2.28564 4.57142C2.28564 5.83379 4.84402 6.85713 7.99993 6.85713Z" fill="black" stroke="black" stroke-width="1.5"/>
<path d="M13.7142 4.57141V11.4286C13.7142 12.691 11.1558 13.7143 7.99993 13.7143C4.84402 13.7143 2.28564 12.691 2.28564 11.4286V4.57141" stroke="black" stroke-width="1.5"/>
<path d="M13.7142 8C13.7142 9.26237 11.1558 10.2857 7.99993 10.2857C4.84402 10.2857 2.28564 9.26237 2.28564 8" stroke="black" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 692 B

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

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash-2"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>

After

Width:  |  Height:  |  Size: 409 B

View File

@@ -212,7 +212,6 @@
"enter": "search::SelectNextMatch",
"shift-enter": "search::SelectPrevMatch",
"alt-enter": "search::SelectAllMatches",
"alt-tab": "search::CycleMode",
"ctrl-f": "search::FocusSearch",
"ctrl-h": "search::ToggleReplace"
}
@@ -235,11 +234,10 @@
"context": "ProjectSearchBar",
"bindings": {
"escape": "project_search::ToggleFocus",
"alt-tab": "search::CycleMode",
"ctrl-shift-f": "search::FocusSearch",
"ctrl-shift-h": "search::ToggleReplace",
"alt-ctrl-g": "search::ActivateRegexMode",
"alt-ctrl-x": "search::ActivateTextMode"
"alt-ctrl-g": "search::ToggleRegex",
"alt-ctrl-x": "search::ToggleRegex"
}
},
{
@@ -260,10 +258,9 @@
"context": "ProjectSearchView",
"bindings": {
"escape": "project_search::ToggleFocus",
"alt-tab": "search::CycleMode",
"ctrl-shift-h": "search::ToggleReplace",
"alt-ctrl-g": "search::ActivateRegexMode",
"alt-ctrl-x": "search::ActivateTextMode"
"alt-ctrl-g": "search::ToggleRegex",
"alt-ctrl-x": "search::ToggleRegex"
}
},
{
@@ -283,10 +280,10 @@
"alt-enter": "search::SelectAllMatches",
"alt-c": "search::ToggleCaseSensitive",
"alt-w": "search::ToggleWholeWord",
"alt-r": "search::CycleMode",
"alt-r": "search::ToggleRegex",
"alt-ctrl-f": "project_search::ToggleFilters",
"ctrl-alt-shift-r": "search::ActivateRegexMode",
"ctrl-alt-shift-x": "search::ActivateTextMode"
"ctrl-alt-shift-r": "search::ToggleRegex",
"ctrl-alt-shift-x": "search::ToggleRegex"
}
},
// Bindings from VS Code
@@ -608,6 +605,8 @@
{
"context": "TabSwitcher",
"bindings": {
"ctrl-up": "menu::SelectPrev",
"ctrl-down": "menu::SelectNext",
"ctrl-shift-tab": "menu::SelectPrev",
"ctrl-backspace": "tab_switcher::CloseSelectedItem"
}

View File

@@ -233,7 +233,6 @@
"enter": "search::SelectNextMatch",
"shift-enter": "search::SelectPrevMatch",
"alt-enter": "search::SelectAllMatches",
"alt-tab": "search::CycleMode",
"cmd-f": "search::FocusSearch",
"cmd-alt-f": "search::ToggleReplace"
}
@@ -256,11 +255,10 @@
"context": "ProjectSearchBar",
"bindings": {
"escape": "project_search::ToggleFocus",
"alt-tab": "search::CycleMode",
"cmd-shift-f": "search::FocusSearch",
"cmd-shift-h": "search::ToggleReplace",
"alt-cmd-g": "search::ActivateRegexMode",
"alt-cmd-x": "search::ActivateTextMode"
"alt-cmd-g": "search::ToggleRegex",
"alt-cmd-x": "search::ToggleRegex"
}
},
{
@@ -281,10 +279,9 @@
"context": "ProjectSearchView",
"bindings": {
"escape": "project_search::ToggleFocus",
"alt-tab": "search::CycleMode",
"cmd-shift-h": "search::ToggleReplace",
"alt-cmd-g": "search::ActivateRegexMode",
"alt-cmd-x": "search::ActivateTextMode"
"alt-cmd-g": "search::ToggleRegex",
"alt-cmd-x": "search::ToggleRegex"
}
},
{
@@ -306,10 +303,9 @@
"alt-enter": "search::SelectAllMatches",
"alt-cmd-c": "search::ToggleCaseSensitive",
"alt-cmd-w": "search::ToggleWholeWord",
"alt-tab": "search::CycleMode",
"alt-cmd-f": "project_search::ToggleFilters",
"alt-cmd-g": "search::ActivateRegexMode",
"alt-cmd-x": "search::ActivateTextMode"
"alt-cmd-g": "search::ToggleRegex",
"alt-cmd-x": "search::ToggleRegex"
}
},
// Bindings from VS Code
@@ -617,6 +613,8 @@
{
"context": "TabSwitcher",
"bindings": {
"ctrl-up": "menu::SelectPrev",
"ctrl-down": "menu::SelectNext",
"ctrl-shift-tab": "menu::SelectPrev",
"ctrl-backspace": "tab_switcher::CloseSelectedItem"
}

View File

@@ -234,6 +234,8 @@
"displayLines": true
}
],
"g ]": "editor::GoToDiagnostic",
"g [": "editor::GoToPrevDiagnostic",
"shift-h": "vim::WindowTop",
"shift-m": "vim::WindowMiddle",
"shift-l": "vim::WindowBottom",
@@ -367,6 +369,15 @@
"< <": "vim::Outdent",
"ctrl-pagedown": "pane::ActivateNextItem",
"ctrl-pageup": "pane::ActivatePrevItem",
// tree-sitter related commands
"[ x": "editor::SelectLargerSyntaxNode",
"] x": "editor::SelectSmallerSyntaxNode"
}
},
{
"context": "Editor && vim_mode == visual && vim_operator == none && !VimWaiting",
"bindings": {
// tree-sitter related commands
"[ x": "editor::SelectLargerSyntaxNode",
"] x": "editor::SelectSmallerSyntaxNode"
}
@@ -532,6 +543,18 @@
]
}
},
{
"context": "Editor && vim_mode == normal",
"bindings": {
"g c c": "editor::ToggleComments"
}
},
{
"context": "Editor && vim_mode == visual",
"bindings": {
"g c": "editor::ToggleComments"
}
},
{
"context": "Editor && vim_mode == insert",
"bindings": {

View File

@@ -58,6 +58,8 @@
"hover_popover_enabled": true,
// Whether to confirm before quitting Zed.
"confirm_quit": false,
// Whether to restore last closed project when fresh Zed instance is opened.
"restore_on_startup": "last_workspace",
// Whether the cursor blinks in the editor.
"cursor_blink": true,
// Whether to pop the completions menu while typing in an editor without
@@ -212,7 +214,10 @@
// Whether to reveal it in the project panel automatically,
// when a corresponding project entry becomes active.
// Gitignored entries are never auto revealed.
"auto_reveal_entries": true
"auto_reveal_entries": true,
/// Whether to fold directories automatically
/// when a directory has only one directory inside.
"auto_fold_dirs": false
},
"collaboration_panel": {
// Whether to show the collaboration panel button in the status bar.
@@ -296,6 +301,16 @@
// Position of the close button on the editor tabs.
"close_position": "right"
},
// Settings related to preview tabs.
"preview_tabs": {
// Whether preview tabs should be enabled.
// Preview tabs allow you to open files in preview mode, where they close automatically
// when you switch to another file unless you explicitly pin them.
// This is useful for quickly viewing files without cluttering your workspace.
"enabled": true,
// Whether to open files in preview mode when selected from the file finder.
"enable_preview_from_file_finder": false
},
// Whether or not to remove any trailing whitespace from lines of a buffer
// before saving it.
"remove_trailing_whitespace_on_save": true,

View File

@@ -46,6 +46,7 @@ use ui::{
};
use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt};
use uuid::Uuid;
use workspace::notifications::NotificationId;
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
searchable::Direction,
@@ -418,10 +419,14 @@ impl AssistantPanel {
if pending_assist.inline_assistant.is_none() {
if let Some(workspace) = this.workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
workspace.show_toast(
Toast::new(inline_assist_id, error),
cx,
);
struct InlineAssistantError;
let id =
NotificationId::identified::<InlineAssistantError>(
inline_assist_id,
);
workspace.show_toast(Toast::new(id, error), cx);
})
}
@@ -620,10 +625,10 @@ impl AssistantPanel {
// If Markdown or No Language is Known, increase the randomness for more creative output
// If Code, decrease temperature to get more deterministic outputs
let temperature = if let Some(language) = language_name.clone() {
if language.as_ref() != "Markdown" {
0.5
} else {
if language.as_ref() == "Markdown" {
1.0
} else {
0.5
}
} else {
1.0
@@ -696,7 +701,7 @@ impl AssistantPanel {
} else {
editor.highlight_background::<PendingInlineAssist>(
&background_ranges,
|theme| theme.editor_active_line_background, // todo!("use the appropriate color")
|theme| theme.editor_active_line_background, // TODO use the appropriate color
cx,
);
}
@@ -2060,7 +2065,7 @@ impl ConversationEditor {
workspace: workspace.downgrade(),
_subscriptions,
};
cx.defer(|this, cx| this.update_active_buffer(workspace, cx));
this.update_active_buffer(workspace, cx);
this.update_message_headers(cx);
this
}

View File

@@ -32,6 +32,7 @@ use util::{
http::{HttpClient, HttpClientWithUrl},
ResultExt,
};
use workspace::notifications::NotificationId;
use workspace::Workspace;
const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
@@ -262,9 +263,11 @@ pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> {
let should_show_notification = should_show_notification.await?;
if should_show_notification {
workspace.update(&mut cx, |workspace, cx| {
workspace.show_notification(0, cx, |cx| {
cx.new_view(|_| UpdateNotification::new(version))
});
workspace.show_notification(
NotificationId::unique::<UpdateNotification>(),
cx,
|cx| cx.new_view(|_| UpdateNotification::new(version)),
);
updater
.read(cx)
.set_should_show_update_notification(false, cx)

View File

@@ -52,19 +52,12 @@ impl Render for Breadcrumbs {
Some(BreadcrumbText {
text: "".into(),
highlights: None,
font: None,
}),
);
}
let highlighted_segments = segments.into_iter().map(|segment| {
let mut text_style = cx.text_style();
if let Some(font) = segment.font {
text_style.font_family = font.family;
text_style.font_features = font.features;
text_style.font_style = font.style;
text_style.font_weight = font.weight;
}
text_style.color = Color::Muted.color(cx);
StyledText::new(segment.text.replace('\n', ""))

View File

@@ -432,7 +432,9 @@ impl ActiveCall {
room: Option<Model<Room>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if room.as_ref() != self.room.as_ref().map(|room| &room.0) {
if room.as_ref() == self.room.as_ref().map(|room| &room.0) {
Task::ready(Ok(()))
} else {
cx.notify();
if let Some(room) = room {
if room.read(cx).status().is_offline() {
@@ -462,8 +464,6 @@ impl ActiveCall {
self.room = None;
Task::ready(Ok(()))
}
} else {
Task::ready(Ok(()))
}
}

View File

@@ -1182,7 +1182,7 @@ impl Room {
cx.emit(Event::RemoteProjectJoined { project_id: id });
cx.spawn(move |this, mut cx| async move {
let project =
Project::remote(id, client, user_store, language_registry, fs, cx.clone()).await?;
Project::in_room(id, client, user_store, language_registry, fs, cx.clone()).await?;
this.update(&mut cx, |this, cx| {
this.joined_projects.retain(|project| {

View File

@@ -11,7 +11,9 @@ pub use channel_chat::{
mentions_to_proto, ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId,
MessageParams,
};
pub use channel_store::{Channel, ChannelEvent, ChannelMembership, ChannelStore};
pub use channel_store::{
Channel, ChannelEvent, ChannelMembership, ChannelStore, DevServer, RemoteProject,
};
#[cfg(test)]
mod channel_store_tests;

View File

@@ -3,7 +3,10 @@ mod channel_index;
use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat, ChannelMessage};
use anyhow::{anyhow, Result};
use channel_index::ChannelIndex;
use client::{ChannelId, Client, ClientSettings, ProjectId, Subscription, User, UserId, UserStore};
use client::{
ChannelId, Client, ClientSettings, DevServerId, ProjectId, RemoteProjectId, Subscription, User,
UserId, UserStore,
};
use collections::{hash_map, HashMap, HashSet};
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
use gpui::{
@@ -12,7 +15,7 @@ use gpui::{
};
use language::Capability;
use rpc::{
proto::{self, ChannelRole, ChannelVisibility},
proto::{self, ChannelRole, ChannelVisibility, DevServerStatus},
TypedEnvelope,
};
use settings::Settings;
@@ -40,7 +43,6 @@ pub struct HostedProject {
name: SharedString,
_visibility: proto::ChannelVisibility,
}
impl From<proto::HostedProject> for HostedProject {
fn from(project: proto::HostedProject) -> Self {
Self {
@@ -52,12 +54,56 @@ impl From<proto::HostedProject> for HostedProject {
}
}
#[derive(Debug, Clone)]
pub struct RemoteProject {
pub id: RemoteProjectId,
pub project_id: Option<ProjectId>,
pub channel_id: ChannelId,
pub name: SharedString,
pub path: SharedString,
pub dev_server_id: DevServerId,
}
impl From<proto::RemoteProject> for RemoteProject {
fn from(project: proto::RemoteProject) -> Self {
Self {
id: RemoteProjectId(project.id),
project_id: project.project_id.map(|id| ProjectId(id)),
channel_id: ChannelId(project.channel_id),
name: project.name.into(),
path: project.path.into(),
dev_server_id: DevServerId(project.dev_server_id),
}
}
}
#[derive(Debug, Clone)]
pub struct DevServer {
pub id: DevServerId,
pub channel_id: ChannelId,
pub name: SharedString,
pub status: DevServerStatus,
}
impl From<proto::DevServer> for DevServer {
fn from(dev_server: proto::DevServer) -> Self {
Self {
id: DevServerId(dev_server.dev_server_id),
channel_id: ChannelId(dev_server.channel_id),
status: dev_server.status(),
name: dev_server.name.into(),
}
}
}
pub struct ChannelStore {
pub channel_index: ChannelIndex,
channel_invitations: Vec<Arc<Channel>>,
channel_participants: HashMap<ChannelId, Vec<Arc<User>>>,
channel_states: HashMap<ChannelId, ChannelState>,
hosted_projects: HashMap<ProjectId, HostedProject>,
remote_projects: HashMap<RemoteProjectId, RemoteProject>,
dev_servers: HashMap<DevServerId, DevServer>,
outgoing_invites: HashSet<(ChannelId, UserId)>,
update_channels_tx: mpsc::UnboundedSender<proto::UpdateChannels>,
@@ -87,6 +133,8 @@ pub struct ChannelState {
observed_chat_message: Option<u64>,
role: Option<ChannelRole>,
projects: HashSet<ProjectId>,
dev_servers: HashSet<DevServerId>,
remote_projects: HashSet<RemoteProjectId>,
}
impl Channel {
@@ -217,6 +265,8 @@ impl ChannelStore {
channel_index: ChannelIndex::default(),
channel_participants: Default::default(),
hosted_projects: Default::default(),
remote_projects: Default::default(),
dev_servers: Default::default(),
outgoing_invites: Default::default(),
opened_buffers: Default::default(),
opened_chats: Default::default(),
@@ -316,6 +366,40 @@ impl ChannelStore {
projects
}
pub fn dev_servers_for_id(&self, channel_id: ChannelId) -> Vec<DevServer> {
let mut dev_servers: Vec<DevServer> = self
.channel_states
.get(&channel_id)
.map(|state| state.dev_servers.clone())
.unwrap_or_default()
.into_iter()
.flat_map(|id| self.dev_servers.get(&id).cloned())
.collect();
dev_servers.sort_by_key(|s| (s.name.clone(), s.id));
dev_servers
}
pub fn find_dev_server_by_id(&self, id: DevServerId) -> Option<&DevServer> {
self.dev_servers.get(&id)
}
pub fn find_remote_project_by_id(&self, id: RemoteProjectId) -> Option<&RemoteProject> {
self.remote_projects.get(&id)
}
pub fn remote_projects_for_id(&self, channel_id: ChannelId) -> Vec<RemoteProject> {
let mut remote_projects: Vec<RemoteProject> = self
.channel_states
.get(&channel_id)
.map(|state| state.remote_projects.clone())
.unwrap_or_default()
.into_iter()
.flat_map(|id| self.remote_projects.get(&id).cloned())
.collect();
remote_projects.sort_by_key(|p| (p.name.clone(), p.id));
remote_projects
}
pub fn has_open_channel_buffer(&self, channel_id: ChannelId, _cx: &AppContext) -> bool {
if let Some(buffer) = self.opened_buffers.get(&channel_id) {
if let OpenedModelHandle::Open(buffer) = buffer {
@@ -818,6 +902,45 @@ impl ChannelStore {
})
}
pub fn create_remote_project(
&mut self,
channel_id: ChannelId,
dev_server_id: DevServerId,
name: String,
path: String,
cx: &mut ModelContext<Self>,
) -> Task<Result<proto::CreateRemoteProjectResponse>> {
let client = self.client.clone();
cx.background_executor().spawn(async move {
client
.request(proto::CreateRemoteProject {
channel_id: channel_id.0,
dev_server_id: dev_server_id.0,
name,
path,
})
.await
})
}
pub fn create_dev_server(
&mut self,
channel_id: ChannelId,
name: String,
cx: &mut ModelContext<Self>,
) -> Task<Result<proto::CreateDevServerResponse>> {
let client = self.client.clone();
cx.background_executor().spawn(async move {
let result = client
.request(proto::CreateDevServer {
channel_id: channel_id.0,
name,
})
.await?;
Ok(result)
})
}
pub fn get_channel_member_details(
&self,
channel_id: ChannelId,
@@ -1098,7 +1221,11 @@ impl ChannelStore {
|| !payload.latest_channel_message_ids.is_empty()
|| !payload.latest_channel_buffer_versions.is_empty()
|| !payload.hosted_projects.is_empty()
|| !payload.deleted_hosted_projects.is_empty();
|| !payload.deleted_hosted_projects.is_empty()
|| !payload.dev_servers.is_empty()
|| !payload.deleted_dev_servers.is_empty()
|| !payload.remote_projects.is_empty()
|| !payload.deleted_remote_projects.is_empty();
if channels_changed {
if !payload.delete_channels.is_empty() {
@@ -1186,6 +1313,60 @@ impl ChannelStore {
.remove_hosted_project(old_project.project_id);
}
}
for remote_project in payload.remote_projects {
let remote_project: RemoteProject = remote_project.into();
if let Some(old_remote_project) = self
.remote_projects
.insert(remote_project.id, remote_project.clone())
{
self.channel_states
.entry(old_remote_project.channel_id)
.or_default()
.remove_remote_project(old_remote_project.id);
}
self.channel_states
.entry(remote_project.channel_id)
.or_default()
.add_remote_project(remote_project.id);
}
for remote_project_id in payload.deleted_remote_projects {
let remote_project_id = RemoteProjectId(remote_project_id);
if let Some(old_project) = self.remote_projects.remove(&remote_project_id) {
self.channel_states
.entry(old_project.channel_id)
.or_default()
.remove_remote_project(old_project.id);
}
}
for dev_server in payload.dev_servers {
let dev_server: DevServer = dev_server.into();
if let Some(old_server) = self.dev_servers.insert(dev_server.id, dev_server.clone())
{
self.channel_states
.entry(old_server.channel_id)
.or_default()
.remove_dev_server(old_server.id);
}
self.channel_states
.entry(dev_server.channel_id)
.or_default()
.add_dev_server(dev_server.id);
}
for dev_server_id in payload.deleted_dev_servers {
let dev_server_id = DevServerId(dev_server_id);
if let Some(old_server) = self.dev_servers.remove(&dev_server_id) {
self.channel_states
.entry(old_server.channel_id)
.or_default()
.remove_dev_server(old_server.id);
}
}
}
cx.notify();
@@ -1300,4 +1481,20 @@ impl ChannelState {
fn remove_hosted_project(&mut self, project_id: ProjectId) {
self.projects.remove(&project_id);
}
fn add_remote_project(&mut self, remote_project_id: RemoteProjectId) {
self.remote_projects.insert(remote_project_id);
}
fn remove_remote_project(&mut self, remote_project_id: RemoteProjectId) {
self.remote_projects.remove(&remote_project_id);
}
fn add_dev_server(&mut self, dev_server_id: DevServerId) {
self.dev_servers.insert(dev_server_id);
}
fn remove_dev_server(&mut self, dev_server_id: DevServerId) {
self.dev_servers.remove(&dev_server_id);
}
}

View File

@@ -264,7 +264,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
);
assert_eq!(
channel.next_event(cx),
channel.next_event(cx).await,
ChannelChatEvent::MessagesUpdated {
old_range: 2..2,
new_count: 1,
@@ -317,7 +317,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
);
assert_eq!(
channel.next_event(cx),
channel.next_event(cx).await,
ChannelChatEvent::MessagesUpdated {
old_range: 0..0,
new_count: 2,

View File

@@ -759,8 +759,9 @@ impl Client {
read_credentials_from_keychain(cx).await.is_some()
}
pub fn set_dev_server_token(&self, token: DevServerToken) {
pub fn set_dev_server_token(&self, token: DevServerToken) -> &Self {
self.state.write().credentials = Some(Credentials::DevServer { token });
self
}
#[async_recursion(?Send)]

View File

@@ -27,6 +27,12 @@ impl std::fmt::Display for ChannelId {
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub struct ProjectId(pub u64);
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub struct DevServerId(pub u64);
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub struct RemoteProjectId(pub u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ParticipantIndex(pub u32);

View File

@@ -1,5 +1,5 @@
DATABASE_URL = "postgres://postgres@localhost/zed"
# DATABASE_URL = "sqlite:////home/zed/.config/zed/db.sqlite3?mode=rwc"
# DATABASE_URL = "sqlite:////root/0/zed/db.sqlite3?mode=rwc"
DATABASE_MAX_CONNECTIONS = 5
HTTP_PORT = 8080
API_TOKEN = "secret"

View File

@@ -63,8 +63,8 @@ tokio.workspace = true
toml.workspace = true
tower = "0.4"
tower-http = { workspace = true, features = ["trace"] }
tracing = "0.1.34"
tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json", "registry", "tracing-log"] }
tracing = "0.1.40"
tracing-subscriber = { git = "https://github.com/tokio-rs/tracing", rev = "tracing-subscriber-0.3.18", features = ["env-filter", "json", "registry", "tracing-log"] } # workaround for https://github.com/tokio-rs/tracing/issues/2927
util.workspace = true
uuid.workspace = true
@@ -102,3 +102,4 @@ theme.workspace = true
unindent.workspace = true
util.workspace = true
workspace = { workspace = true, features = ["test-support"] }
headless.workspace = true

View File

@@ -45,12 +45,13 @@ CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id");
CREATE TABLE "projects" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"room_id" INTEGER REFERENCES rooms (id) ON DELETE CASCADE NOT NULL,
"room_id" INTEGER REFERENCES rooms (id) ON DELETE CASCADE,
"host_user_id" INTEGER REFERENCES users (id),
"host_connection_id" INTEGER,
"host_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE,
"unregistered" BOOLEAN NOT NULL DEFAULT FALSE,
"hosted_project_id" INTEGER REFERENCES hosted_projects (id)
"hosted_project_id" INTEGER REFERENCES hosted_projects (id),
"remote_project_id" INTEGER REFERENCES remote_projects(id)
);
CREATE INDEX "index_projects_on_host_connection_server_id" ON "projects" ("host_connection_server_id");
CREATE INDEX "index_projects_on_host_connection_id_and_host_connection_server_id" ON "projects" ("host_connection_id", "host_connection_server_id");
@@ -397,7 +398,9 @@ CREATE TABLE hosted_projects (
channel_id INTEGER NOT NULL REFERENCES channels(id),
name TEXT NOT NULL,
visibility TEXT NOT NULL,
deleted_at TIMESTAMP NULL
deleted_at TIMESTAMP NULL,
dev_server_id INTEGER REFERENCES dev_servers(id),
dev_server_path TEXT
);
CREATE INDEX idx_hosted_projects_on_channel_id ON hosted_projects (channel_id);
CREATE UNIQUE INDEX uix_hosted_projects_on_channel_id_and_name ON hosted_projects (channel_id, name) WHERE (deleted_at IS NULL);
@@ -409,3 +412,13 @@ CREATE TABLE dev_servers (
hashed_token TEXT NOT NULL
);
CREATE INDEX idx_dev_servers_on_channel_id ON dev_servers (channel_id);
CREATE TABLE remote_projects (
id INTEGER PRIMARY KEY AUTOINCREMENT,
channel_id INTEGER NOT NULL REFERENCES channels(id),
dev_server_id INTEGER NOT NULL REFERENCES dev_servers(id),
name TEXT NOT NULL,
path TEXT NOT NULL
);
ALTER TABLE hosted_projects ADD COLUMN remote_project_id INTEGER REFERENCES remote_projects(id);

View File

@@ -0,0 +1,9 @@
CREATE TABLE remote_projects (
id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
channel_id INT NOT NULL REFERENCES channels(id),
dev_server_id INT NOT NULL REFERENCES dev_servers(id),
name TEXT NOT NULL,
path TEXT NOT NULL
);
ALTER TABLE projects ADD COLUMN remote_project_id INTEGER REFERENCES remote_projects(id);

View File

@@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS "embeddings" (
"model" TEXT,
"digest" BYTEA,
"dimensions" FLOAT4[1536],
"retrieved_at" TIMESTAMP NOT NULL DEFAULT now(),
PRIMARY KEY ("model", "digest")
);
CREATE INDEX IF NOT EXISTS "idx_retrieved_at_on_embeddings" ON "embeddings" ("retrieved_at");

View File

@@ -10,6 +10,7 @@ use axum::{
response::IntoResponse,
};
use prometheus::{exponential_buckets, register_histogram, Histogram};
pub use rpc::auth::random_token;
use scrypt::{
password_hash::{PasswordHash, PasswordVerifier},
Scrypt,
@@ -152,7 +153,7 @@ pub async fn create_access_token(
/// Hashing prevents anyone with access to the database being able to login.
/// As the token is randomly generated, we don't need to worry about scrypt-style
/// protection.
fn hash_access_token(token: &str) -> String {
pub fn hash_access_token(token: &str) -> String {
let digest = sha2::Sha256::digest(token);
format!(
"$sha256${}",
@@ -230,18 +231,15 @@ pub async fn verify_access_token(
})
}
// a dev_server_token has the format <id>.<base64>. This is to make them
// relatively easy to copy/paste around.
pub fn generate_dev_server_token(id: usize, access_token: String) -> String {
format!("{}.{}", id, access_token)
}
pub async fn verify_dev_server_token(
dev_server_token: &str,
db: &Arc<Database>,
) -> anyhow::Result<dev_server::Model> {
let mut parts = dev_server_token.splitn(2, '.');
let id = DevServerId(parts.next().unwrap_or_default().parse()?);
let token = parts
.next()
.ok_or_else(|| anyhow!("invalid dev server token format"))?;
let (id, token) = split_dev_server_token(dev_server_token)?;
let token_hash = hash_access_token(&token);
let server = db.get_dev_server(id).await?;
@@ -257,6 +255,17 @@ pub async fn verify_dev_server_token(
}
}
// a dev_server_token has the format <id>.<base64>. This is to make them
// relatively easy to copy/paste around.
pub fn split_dev_server_token(dev_server_token: &str) -> anyhow::Result<(DevServerId, &str)> {
let mut parts = dev_server_token.splitn(2, '.');
let id = DevServerId(parts.next().unwrap_or_default().parse()?);
let token = parts
.next()
.ok_or_else(|| anyhow!("invalid dev server token format"))?;
Ok((id, token))
}
#[cfg(test)]
mod test {
use rand::thread_rng;

View File

@@ -56,6 +56,7 @@ pub struct Database {
options: ConnectOptions,
pool: DatabaseConnection,
rooms: DashMap<RoomId, Arc<Mutex<()>>>,
projects: DashMap<ProjectId, Arc<Mutex<()>>>,
rng: Mutex<StdRng>,
executor: Executor,
notification_kinds_by_id: HashMap<NotificationKindId, &'static str>,
@@ -74,6 +75,7 @@ impl Database {
options: options.clone(),
pool: sea_orm::Database::connect(options).await?,
rooms: DashMap::with_capacity(16384),
projects: DashMap::with_capacity(16384),
rng: Mutex::new(StdRng::seed_from_u64(0)),
notification_kinds_by_id: HashMap::default(),
notification_kinds_by_name: HashMap::default(),
@@ -86,6 +88,7 @@ impl Database {
#[cfg(test)]
pub fn reset(&self) {
self.rooms.clear();
self.projects.clear();
}
/// Runs the database migrations.
@@ -190,7 +193,10 @@ impl Database {
}
/// The same as room_transaction, but if you need to only optionally return a Room.
async fn optional_room_transaction<F, Fut, T>(&self, f: F) -> Result<Option<RoomGuard<T>>>
async fn optional_room_transaction<F, Fut, T>(
&self,
f: F,
) -> Result<Option<TransactionGuard<T>>>
where
F: Send + Fn(TransactionHandle) -> Fut,
Fut: Send + Future<Output = Result<Option<(RoomId, T)>>>,
@@ -205,7 +211,7 @@ impl Database {
let _guard = lock.lock_owned().await;
match tx.commit().await.map_err(Into::into) {
Ok(()) => {
return Ok(Some(RoomGuard {
return Ok(Some(TransactionGuard {
data,
_guard,
_not_send: PhantomData,
@@ -240,10 +246,63 @@ impl Database {
self.run(body).await
}
async fn project_transaction<F, Fut, T>(
&self,
project_id: ProjectId,
f: F,
) -> Result<TransactionGuard<T>>
where
F: Send + Fn(TransactionHandle) -> Fut,
Fut: Send + Future<Output = Result<T>>,
{
let room_id = Database::room_id_for_project(&self, project_id).await?;
let body = async {
let mut i = 0;
loop {
let lock = if let Some(room_id) = room_id {
self.rooms.entry(room_id).or_default().clone()
} else {
self.projects.entry(project_id).or_default().clone()
};
let _guard = lock.lock_owned().await;
let (tx, result) = self.with_transaction(&f).await?;
match result {
Ok(data) => match tx.commit().await.map_err(Into::into) {
Ok(()) => {
return Ok(TransactionGuard {
data,
_guard,
_not_send: PhantomData,
});
}
Err(error) => {
if !self.retry_on_serialization_error(&error, i).await {
return Err(error);
}
}
},
Err(error) => {
tx.rollback().await?;
if !self.retry_on_serialization_error(&error, i).await {
return Err(error);
}
}
}
i += 1;
}
};
self.run(body).await
}
/// room_transaction runs the block in a transaction. It returns a RoomGuard, that keeps
/// the database locked until it is dropped. This ensures that updates sent to clients are
/// properly serialized with respect to database changes.
async fn room_transaction<F, Fut, T>(&self, room_id: RoomId, f: F) -> Result<RoomGuard<T>>
async fn room_transaction<F, Fut, T>(
&self,
room_id: RoomId,
f: F,
) -> Result<TransactionGuard<T>>
where
F: Send + Fn(TransactionHandle) -> Fut,
Fut: Send + Future<Output = Result<T>>,
@@ -257,7 +316,7 @@ impl Database {
match result {
Ok(data) => match tx.commit().await.map_err(Into::into) {
Ok(()) => {
return Ok(RoomGuard {
return Ok(TransactionGuard {
data,
_guard,
_not_send: PhantomData,
@@ -399,15 +458,16 @@ impl Deref for TransactionHandle {
}
}
/// [`RoomGuard`] keeps a database transaction alive until it is dropped.
/// so that updates to rooms are serialized.
pub struct RoomGuard<T> {
/// [`TransactionGuard`] keeps a database transaction alive until it is dropped.
/// It wraps data that depends on the state of the database and prevents an additional
/// transaction from starting that would invalidate that data.
pub struct TransactionGuard<T> {
data: T,
_guard: OwnedMutexGuard<()>,
_not_send: PhantomData<Rc<()>>,
}
impl<T> Deref for RoomGuard<T> {
impl<T> Deref for TransactionGuard<T> {
type Target = T;
fn deref(&self) -> &T {
@@ -415,13 +475,13 @@ impl<T> Deref for RoomGuard<T> {
}
}
impl<T> DerefMut for RoomGuard<T> {
impl<T> DerefMut for TransactionGuard<T> {
fn deref_mut(&mut self) -> &mut T {
&mut self.data
}
}
impl<T> RoomGuard<T> {
impl<T> TransactionGuard<T> {
/// Returns the inner value of the guard.
pub fn into_inner(self) -> T {
self.data
@@ -518,6 +578,7 @@ pub struct MembershipUpdated {
/// The result of setting a member's role.
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum SetMemberRoleResult {
InviteUpdated(Channel),
MembershipUpdated(MembershipUpdated),
@@ -594,6 +655,8 @@ pub struct ChannelsForUser {
pub channel_memberships: Vec<channel_member::Model>,
pub channel_participants: HashMap<ChannelId, Vec<UserId>>,
pub hosted_projects: Vec<proto::HostedProject>,
pub dev_servers: Vec<dev_server::Model>,
pub remote_projects: Vec<proto::RemoteProject>,
pub observed_buffer_versions: Vec<proto::ChannelBufferVersion>,
pub observed_channel_messages: Vec<proto::ChannelMessageId>,
@@ -635,6 +698,30 @@ pub struct RejoinedProject {
pub language_servers: Vec<proto::LanguageServer>,
}
impl RejoinedProject {
pub fn to_proto(&self) -> proto::RejoinedProject {
proto::RejoinedProject {
id: self.id.to_proto(),
worktrees: self
.worktrees
.iter()
.map(|worktree| proto::WorktreeMetadata {
id: worktree.id,
root_name: worktree.root_name.clone(),
visible: worktree.visible,
abs_path: worktree.abs_path.clone(),
})
.collect(),
collaborators: self
.collaborators
.iter()
.map(|collaborator| collaborator.to_proto())
.collect(),
language_servers: self.language_servers.clone(),
}
}
}
#[derive(Debug)]
pub struct RejoinedWorktree {
pub id: u64,

View File

@@ -84,6 +84,7 @@ id_type!(NotificationId);
id_type!(NotificationKindId);
id_type!(ProjectCollaboratorId);
id_type!(ProjectId);
id_type!(RemoteProjectId);
id_type!(ReplicaId);
id_type!(RoomId);
id_type!(RoomParticipantId);
@@ -270,3 +271,18 @@ impl Into<i32> for ChannelVisibility {
proto.into()
}
}
#[derive(Copy, Clone, Debug, Serialize, PartialEq)]
pub enum PrincipalId {
UserId(UserId),
DevServerId(DevServerId),
}
/// Indicate whether a [Buffer] has permissions to edit.
#[derive(PartialEq, Clone, Copy, Debug)]
pub enum Capability {
/// The buffer is a mutable replica.
ReadWrite,
/// The buffer is a read-only replica.
ReadOnly,
}

View File

@@ -6,12 +6,14 @@ pub mod channels;
pub mod contacts;
pub mod contributors;
pub mod dev_servers;
pub mod embeddings;
pub mod extensions;
pub mod hosted_projects;
pub mod messages;
pub mod notifications;
pub mod projects;
pub mod rate_buckets;
pub mod remote_projects;
pub mod rooms;
pub mod servers;
pub mod users;

View File

@@ -640,10 +640,15 @@ impl Database {
.get_hosted_projects(&channel_ids, &roles_by_channel_id, tx)
.await?;
let dev_servers = self.get_dev_servers(&channel_ids, tx).await?;
let remote_projects = self.get_remote_projects(&channel_ids, tx).await?;
Ok(ChannelsForUser {
channel_memberships,
channels,
hosted_projects,
dev_servers,
remote_projects,
channel_participants,
latest_buffer_versions,
latest_channel_messages,

View File

@@ -1,6 +1,6 @@
use sea_orm::EntityTrait;
use sea_orm::{ActiveValue, ColumnTrait, DatabaseTransaction, EntityTrait, QueryFilter};
use super::{dev_server, Database, DevServerId};
use super::{channel, dev_server, ChannelId, Database, DevServerId, UserId};
impl Database {
pub async fn get_dev_server(
@@ -15,4 +15,42 @@ impl Database {
})
.await
}
pub async fn get_dev_servers(
&self,
channel_ids: &Vec<ChannelId>,
tx: &DatabaseTransaction,
) -> crate::Result<Vec<dev_server::Model>> {
let servers = dev_server::Entity::find()
.filter(dev_server::Column::ChannelId.is_in(channel_ids.iter().map(|id| id.0)))
.all(tx)
.await?;
Ok(servers)
}
pub async fn create_dev_server(
&self,
channel_id: ChannelId,
name: &str,
hashed_access_token: &str,
user_id: UserId,
) -> crate::Result<(channel::Model, dev_server::Model)> {
self.transaction(|tx| async move {
let channel = self.get_channel_internal(channel_id, &tx).await?;
self.check_user_is_channel_admin(&channel, user_id, &tx)
.await?;
let dev_server = dev_server::Entity::insert(dev_server::ActiveModel {
id: ActiveValue::NotSet,
hashed_token: ActiveValue::Set(hashed_access_token.to_string()),
channel_id: ActiveValue::Set(channel_id),
name: ActiveValue::Set(name.to_string()),
})
.exec_with_returning(&*tx)
.await?;
Ok((channel, dev_server))
})
.await
}
}

View File

@@ -0,0 +1,94 @@
use super::*;
use time::Duration;
use time::OffsetDateTime;
impl Database {
pub async fn get_embeddings(
&self,
model: &str,
digests: &[Vec<u8>],
) -> Result<HashMap<Vec<u8>, Vec<f32>>> {
self.weak_transaction(|tx| async move {
let embeddings = {
let mut db_embeddings = embedding::Entity::find()
.filter(
embedding::Column::Model.eq(model).and(
embedding::Column::Digest
.is_in(digests.iter().map(|digest| digest.as_slice())),
),
)
.stream(&*tx)
.await?;
let mut embeddings = HashMap::default();
while let Some(db_embedding) = db_embeddings.next().await {
let db_embedding = db_embedding?;
embeddings.insert(db_embedding.digest, db_embedding.dimensions);
}
embeddings
};
if !embeddings.is_empty() {
let now = OffsetDateTime::now_utc();
let retrieved_at = PrimitiveDateTime::new(now.date(), now.time());
embedding::Entity::update_many()
.filter(
embedding::Column::Digest
.is_in(embeddings.keys().map(|digest| digest.as_slice())),
)
.col_expr(embedding::Column::RetrievedAt, Expr::value(retrieved_at))
.exec(&*tx)
.await?;
}
Ok(embeddings)
})
.await
}
pub async fn save_embeddings(
&self,
model: &str,
embeddings: &HashMap<Vec<u8>, Vec<f32>>,
) -> Result<()> {
self.weak_transaction(|tx| async move {
embedding::Entity::insert_many(embeddings.iter().map(|(digest, dimensions)| {
let now_offset_datetime = OffsetDateTime::now_utc();
let retrieved_at =
PrimitiveDateTime::new(now_offset_datetime.date(), now_offset_datetime.time());
embedding::ActiveModel {
model: ActiveValue::set(model.to_string()),
digest: ActiveValue::set(digest.clone()),
dimensions: ActiveValue::set(dimensions.clone()),
retrieved_at: ActiveValue::set(retrieved_at),
}
}))
.on_conflict(
OnConflict::columns([embedding::Column::Model, embedding::Column::Digest])
.do_nothing()
.to_owned(),
)
.exec_without_returning(&*tx)
.await?;
Ok(())
})
.await
}
pub async fn purge_old_embeddings(&self) -> Result<()> {
self.weak_transaction(|tx| async move {
embedding::Entity::delete_many()
.filter(
embedding::Column::RetrievedAt
.lte(OffsetDateTime::now_utc() - Duration::days(60)),
)
.exec(&*tx)
.await?;
Ok(())
})
.await
}
}

View File

@@ -1,3 +1,5 @@
use util::ResultExt;
use super::*;
impl Database {
@@ -28,7 +30,7 @@ impl Database {
room_id: RoomId,
connection: ConnectionId,
worktrees: &[proto::WorktreeMetadata],
) -> Result<RoomGuard<(ProjectId, proto::Room)>> {
) -> Result<TransactionGuard<(ProjectId, proto::Room)>> {
self.room_transaction(room_id, |tx| async move {
let participant = room_participant::Entity::find()
.filter(
@@ -65,6 +67,7 @@ impl Database {
))),
id: ActiveValue::NotSet,
hosted_project_id: ActiveValue::Set(None),
remote_project_id: ActiveValue::Set(None),
}
.insert(&*tx)
.await?;
@@ -108,20 +111,22 @@ impl Database {
&self,
project_id: ProjectId,
connection: ConnectionId,
) -> Result<RoomGuard<(proto::Room, Vec<ConnectionId>)>> {
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
) -> Result<TransactionGuard<(Option<proto::Room>, Vec<ConnectionId>)>> {
self.project_transaction(project_id, |tx| async move {
let guest_connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
let project = project::Entity::find_by_id(project_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("project not found"))?;
if project.host_connection()? == connection {
let room = if let Some(room_id) = project.room_id {
Some(self.get_room(room_id, &tx).await?)
} else {
None
};
project::Entity::delete(project.into_active_model())
.exec(&*tx)
.await?;
let room = self.get_room(room_id, &tx).await?;
Ok((room, guest_connection_ids))
} else {
Err(anyhow!("cannot unshare a project hosted by another user"))?
@@ -136,9 +141,8 @@ impl Database {
project_id: ProjectId,
connection: ConnectionId,
worktrees: &[proto::WorktreeMetadata],
) -> Result<RoomGuard<(proto::Room, Vec<ConnectionId>)>> {
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
) -> Result<TransactionGuard<(Option<proto::Room>, Vec<ConnectionId>)>> {
self.project_transaction(project_id, |tx| async move {
let project = project::Entity::find_by_id(project_id)
.filter(
Condition::all()
@@ -154,12 +158,14 @@ impl Database {
self.update_project_worktrees(project.id, worktrees, &tx)
.await?;
let room_id = project
.room_id
.ok_or_else(|| anyhow!("project not in a room"))?;
let guest_connection_ids = self.project_guest_connection_ids(project.id, &tx).await?;
let room = self.get_room(room_id, &tx).await?;
let room = if let Some(room_id) = project.room_id {
Some(self.get_room(room_id, &tx).await?)
} else {
None
};
Ok((room, guest_connection_ids))
})
.await
@@ -204,11 +210,10 @@ impl Database {
&self,
update: &proto::UpdateWorktree,
connection: ConnectionId,
) -> Result<RoomGuard<Vec<ConnectionId>>> {
) -> Result<TransactionGuard<Vec<ConnectionId>>> {
let project_id = ProjectId::from_proto(update.project_id);
let worktree_id = update.worktree_id as i64;
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
self.project_transaction(project_id, |tx| async move {
// Ensure the update comes from the host.
let _project = project::Entity::find_by_id(project_id)
.filter(
@@ -360,11 +365,10 @@ impl Database {
&self,
update: &proto::UpdateDiagnosticSummary,
connection: ConnectionId,
) -> Result<RoomGuard<Vec<ConnectionId>>> {
) -> Result<TransactionGuard<Vec<ConnectionId>>> {
let project_id = ProjectId::from_proto(update.project_id);
let worktree_id = update.worktree_id as i64;
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
self.project_transaction(project_id, |tx| async move {
let summary = update
.summary
.as_ref()
@@ -415,10 +419,9 @@ impl Database {
&self,
update: &proto::StartLanguageServer,
connection: ConnectionId,
) -> Result<RoomGuard<Vec<ConnectionId>>> {
) -> Result<TransactionGuard<Vec<ConnectionId>>> {
let project_id = ProjectId::from_proto(update.project_id);
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
self.project_transaction(project_id, |tx| async move {
let server = update
.server
.as_ref()
@@ -461,10 +464,9 @@ impl Database {
&self,
update: &proto::UpdateWorktreeSettings,
connection: ConnectionId,
) -> Result<RoomGuard<Vec<ConnectionId>>> {
) -> Result<TransactionGuard<Vec<ConnectionId>>> {
let project_id = ProjectId::from_proto(update.project_id);
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
self.project_transaction(project_id, |tx| async move {
// Ensure the update comes from the host.
let project = project::Entity::find_by_id(project_id)
.one(&*tx)
@@ -542,46 +544,36 @@ impl Database {
.await
}
pub async fn get_project(&self, id: ProjectId) -> Result<project::Model> {
self.transaction(|tx| async move {
Ok(project::Entity::find_by_id(id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?)
})
.await
}
/// Adds the given connection to the specified project
/// in the current room.
pub async fn join_project_in_room(
pub async fn join_project(
&self,
project_id: ProjectId,
connection: ConnectionId,
) -> Result<RoomGuard<(Project, ReplicaId)>> {
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
let participant = room_participant::Entity::find()
.filter(
Condition::all()
.add(
room_participant::Column::AnsweringConnectionId
.eq(connection.id as i32),
)
.add(
room_participant::Column::AnsweringConnectionServerId
.eq(connection.owner_id as i32),
),
user_id: UserId,
) -> Result<TransactionGuard<(Project, ReplicaId)>> {
self.project_transaction(project_id, |tx| async move {
let (project, role) = self
.access_project(
project_id,
connection,
PrincipalId::UserId(user_id),
Capability::ReadOnly,
&tx,
)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("must join a room first"))?;
let project = project::Entity::find_by_id(project_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?;
if project.room_id != Some(participant.room_id) {
return Err(anyhow!("no such project"))?;
}
self.join_project_internal(
project,
participant.user_id,
connection,
participant.role.unwrap_or(ChannelRole::Member),
&tx,
)
.await
.await?;
self.join_project_internal(project, user_id, connection, role, &tx)
.await
})
.await
}
@@ -814,9 +806,8 @@ impl Database {
&self,
project_id: ProjectId,
connection: ConnectionId,
) -> Result<RoomGuard<(proto::Room, LeftProject)>> {
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
) -> Result<TransactionGuard<(Option<proto::Room>, LeftProject)>> {
self.project_transaction(project_id, |tx| async move {
let result = project_collaborator::Entity::delete_many()
.filter(
Condition::all()
@@ -871,7 +862,12 @@ impl Database {
.exec(&*tx)
.await?;
let room = self.get_room(room_id, &tx).await?;
let room = if let Some(room_id) = project.room_id {
Some(self.get_room(room_id, &tx).await?)
} else {
None
};
let left_project = LeftProject {
id: project_id,
host_user_id: project.host_user_id,
@@ -888,17 +884,15 @@ impl Database {
project_id: ProjectId,
connection_id: ConnectionId,
) -> Result<()> {
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
project_collaborator::Entity::find()
self.project_transaction(project_id, |tx| async move {
project::Entity::find()
.filter(
Condition::all()
.add(project_collaborator::Column::ProjectId.eq(project_id))
.add(project_collaborator::Column::IsHost.eq(true))
.add(project_collaborator::Column::ConnectionId.eq(connection_id.id))
.add(project::Column::Id.eq(project_id))
.add(project::Column::HostConnectionId.eq(Some(connection_id.id as i32)))
.add(
project_collaborator::Column::ConnectionServerId
.eq(connection_id.owner_id),
project::Column::HostConnectionServerId
.eq(Some(connection_id.owner_id as i32)),
),
)
.one(&*tx)
@@ -911,39 +905,90 @@ impl Database {
.map(|guard| guard.into_inner())
}
/// Returns the current project if the given user is authorized to access it with the specified capability.
pub async fn access_project(
&self,
project_id: ProjectId,
connection_id: ConnectionId,
principal_id: PrincipalId,
capability: Capability,
tx: &DatabaseTransaction,
) -> Result<(project::Model, ChannelRole)> {
let (project, remote_project) = project::Entity::find_by_id(project_id)
.find_also_related(remote_project::Entity)
.one(tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?;
let user_id = match principal_id {
PrincipalId::DevServerId(_) => {
if project
.host_connection()
.is_ok_and(|connection| connection == connection_id)
{
return Ok((project, ChannelRole::Admin));
}
return Err(anyhow!("not the project host"))?;
}
PrincipalId::UserId(user_id) => user_id,
};
let role = if let Some(remote_project) = remote_project {
let channel = channel::Entity::find_by_id(remote_project.channel_id)
.one(tx)
.await?
.ok_or_else(|| anyhow!("no such channel"))?;
self.check_user_is_channel_participant(&channel, user_id, &tx)
.await?
} else if let Some(room_id) = project.room_id {
// what's the users role?
let current_participant = room_participant::Entity::find()
.filter(room_participant::Column::RoomId.eq(room_id))
.filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.id))
.one(tx)
.await?
.ok_or_else(|| anyhow!("no such room"))?;
current_participant.role.unwrap_or(ChannelRole::Guest)
} else {
return Err(anyhow!("not authorized to read projects"))?;
};
match capability {
Capability::ReadWrite => {
if !role.can_edit_projects() {
return Err(anyhow!("not authorized to edit projects"))?;
}
}
Capability::ReadOnly => {
if !role.can_read_projects() {
return Err(anyhow!("not authorized to read projects"))?;
}
}
}
Ok((project, role))
}
/// Returns the host connection for a read-only request to join a shared project.
pub async fn host_for_read_only_project_request(
&self,
project_id: ProjectId,
connection_id: ConnectionId,
user_id: UserId,
) -> Result<ConnectionId> {
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
let current_participant = room_participant::Entity::find()
.filter(room_participant::Column::RoomId.eq(room_id))
.filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.id))
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such room"))?;
if !current_participant
.role
.map_or(false, |role| role.can_read_projects())
{
Err(anyhow!("not authorized to read projects"))?;
}
let host = project_collaborator::Entity::find()
.filter(
project_collaborator::Column::ProjectId
.eq(project_id)
.and(project_collaborator::Column::IsHost.eq(true)),
self.project_transaction(project_id, |tx| async move {
let (project, _) = self
.access_project(
project_id,
connection_id,
PrincipalId::UserId(user_id),
Capability::ReadOnly,
&tx,
)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("failed to read project host"))?;
Ok(host.connection())
.await?;
project.host_connection()
})
.await
.map(|guard| guard.into_inner())
@@ -954,83 +999,56 @@ impl Database {
&self,
project_id: ProjectId,
connection_id: ConnectionId,
user_id: UserId,
) -> Result<ConnectionId> {
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
let current_participant = room_participant::Entity::find()
.filter(room_participant::Column::RoomId.eq(room_id))
.filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.id))
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such room"))?;
if !current_participant
.role
.map_or(false, |role| role.can_edit_projects())
{
Err(anyhow!("not authorized to edit projects"))?;
}
let host = project_collaborator::Entity::find()
.filter(
project_collaborator::Column::ProjectId
.eq(project_id)
.and(project_collaborator::Column::IsHost.eq(true)),
self.project_transaction(project_id, |tx| async move {
let (project, _) = self
.access_project(
project_id,
connection_id,
PrincipalId::UserId(user_id),
Capability::ReadWrite,
&tx,
)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("failed to read project host"))?;
Ok(host.connection())
.await?;
project.host_connection()
})
.await
.map(|guard| guard.into_inner())
}
pub async fn project_collaborators_for_buffer_update(
pub async fn connections_for_buffer_update(
&self,
project_id: ProjectId,
principal_id: PrincipalId,
connection_id: ConnectionId,
requires_write: bool,
) -> Result<RoomGuard<Vec<ProjectCollaborator>>> {
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
let current_participant = room_participant::Entity::find()
.filter(room_participant::Column::RoomId.eq(room_id))
.filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.id))
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such room"))?;
capability: Capability,
) -> Result<TransactionGuard<(ConnectionId, Vec<ConnectionId>)>> {
self.project_transaction(project_id, |tx| async move {
// Authorize
let (project, _) = self
.access_project(project_id, connection_id, principal_id, capability, &tx)
.await?;
if requires_write
&& !current_participant
.role
.map_or(false, |role| role.can_edit_projects())
{
Err(anyhow!("not authorized to edit projects"))?;
}
let host_connection_id = project.host_connection()?;
let collaborators = project_collaborator::Entity::find()
.filter(project_collaborator::Column::ProjectId.eq(project_id))
.all(&*tx)
.await?
.into_iter()
.map(|collaborator| ProjectCollaborator {
connection_id: collaborator.connection(),
user_id: collaborator.user_id,
replica_id: collaborator.replica_id,
is_host: collaborator.is_host,
})
.collect::<Vec<_>>();
.await?;
if collaborators
.iter()
.any(|collaborator| collaborator.connection_id == connection_id)
{
Ok(collaborators)
} else {
Err(anyhow!("no such project"))?
}
let guest_connection_ids = collaborators
.into_iter()
.filter_map(|collaborator| {
if collaborator.is_host {
None
} else {
Some(collaborator.connection())
}
})
.collect();
Ok((host_connection_id, guest_connection_ids))
})
.await
}
@@ -1043,24 +1061,39 @@ impl Database {
&self,
project_id: ProjectId,
connection_id: ConnectionId,
) -> Result<RoomGuard<HashSet<ConnectionId>>> {
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
exclude_dev_server: bool,
) -> Result<TransactionGuard<HashSet<ConnectionId>>> {
self.project_transaction(project_id, |tx| async move {
let project = project::Entity::find_by_id(project_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?;
let mut collaborators = project_collaborator::Entity::find()
.filter(project_collaborator::Column::ProjectId.eq(project_id))
.stream(&*tx)
.await?;
let mut connection_ids = HashSet::default();
if let Some(host_connection) = project.host_connection().log_err() {
if !exclude_dev_server {
connection_ids.insert(host_connection);
}
}
while let Some(collaborator) = collaborators.next().await {
let collaborator = collaborator?;
connection_ids.insert(collaborator.connection());
}
if connection_ids.contains(&connection_id) {
if connection_ids.contains(&connection_id)
|| Some(connection_id) == project.host_connection().ok()
{
Ok(connection_ids)
} else {
Err(anyhow!("no such project"))?
Err(anyhow!(
"can only send project updates to a project you're in"
))?
}
})
.await
@@ -1089,15 +1122,12 @@ impl Database {
}
/// Returns the [`RoomId`] for the given project.
pub async fn room_id_for_project(&self, project_id: ProjectId) -> Result<RoomId> {
pub async fn room_id_for_project(&self, project_id: ProjectId) -> Result<Option<RoomId>> {
self.transaction(|tx| async move {
let project = project::Entity::find_by_id(project_id)
Ok(project::Entity::find_by_id(project_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("project {} not found", project_id))?;
Ok(project
.room_id
.ok_or_else(|| anyhow!("project not in room"))?)
.and_then(|project| project.room_id))
})
.await
}
@@ -1142,7 +1172,7 @@ impl Database {
project_id: ProjectId,
leader_connection: ConnectionId,
follower_connection: ConnectionId,
) -> Result<RoomGuard<proto::Room>> {
) -> Result<TransactionGuard<proto::Room>> {
self.room_transaction(room_id, |tx| async move {
follower::ActiveModel {
room_id: ActiveValue::set(room_id),
@@ -1173,7 +1203,7 @@ impl Database {
project_id: ProjectId,
leader_connection: ConnectionId,
follower_connection: ConnectionId,
) -> Result<RoomGuard<proto::Room>> {
) -> Result<TransactionGuard<proto::Room>> {
self.room_transaction(room_id, |tx| async move {
follower::Entity::delete_many()
.filter(

View File

@@ -0,0 +1,261 @@
use anyhow::anyhow;
use rpc::{proto, ConnectionId};
use sea_orm::{
ActiveModelTrait, ActiveValue, ColumnTrait, Condition, DatabaseTransaction, EntityTrait,
ModelTrait, QueryFilter,
};
use crate::db::ProjectId;
use super::{
channel, project, project_collaborator, remote_project, worktree, ChannelId, Database,
DevServerId, RejoinedProject, RemoteProjectId, ResharedProject, ServerId, UserId,
};
impl Database {
pub async fn get_remote_project(
&self,
remote_project_id: RemoteProjectId,
) -> crate::Result<remote_project::Model> {
self.transaction(|tx| async move {
Ok(remote_project::Entity::find_by_id(remote_project_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no remote project with id {}", remote_project_id))?)
})
.await
}
pub async fn get_remote_projects(
&self,
channel_ids: &Vec<ChannelId>,
tx: &DatabaseTransaction,
) -> crate::Result<Vec<proto::RemoteProject>> {
let servers = remote_project::Entity::find()
.filter(remote_project::Column::ChannelId.is_in(channel_ids.iter().map(|id| id.0)))
.find_also_related(project::Entity)
.all(tx)
.await?;
Ok(servers
.into_iter()
.map(|(remote_project, project)| proto::RemoteProject {
id: remote_project.id.to_proto(),
project_id: project.map(|p| p.id.to_proto()),
channel_id: remote_project.channel_id.to_proto(),
name: remote_project.name,
dev_server_id: remote_project.dev_server_id.to_proto(),
path: remote_project.path,
})
.collect())
}
pub async fn get_remote_projects_for_dev_server(
&self,
dev_server_id: DevServerId,
) -> crate::Result<Vec<proto::RemoteProject>> {
self.transaction(|tx| async move {
let servers = remote_project::Entity::find()
.filter(remote_project::Column::DevServerId.eq(dev_server_id))
.find_also_related(project::Entity)
.all(&*tx)
.await?;
Ok(servers
.into_iter()
.map(|(remote_project, project)| proto::RemoteProject {
id: remote_project.id.to_proto(),
project_id: project.map(|p| p.id.to_proto()),
channel_id: remote_project.channel_id.to_proto(),
name: remote_project.name,
dev_server_id: remote_project.dev_server_id.to_proto(),
path: remote_project.path,
})
.collect())
})
.await
}
pub async fn get_stale_dev_server_projects(
&self,
connection: ConnectionId,
) -> crate::Result<Vec<ProjectId>> {
self.transaction(|tx| async move {
let projects = project::Entity::find()
.filter(
Condition::all()
.add(project::Column::HostConnectionId.eq(connection.id))
.add(project::Column::HostConnectionServerId.eq(connection.owner_id)),
)
.all(&*tx)
.await?;
Ok(projects.into_iter().map(|p| p.id).collect())
})
.await
}
pub async fn create_remote_project(
&self,
channel_id: ChannelId,
dev_server_id: DevServerId,
name: &str,
path: &str,
user_id: UserId,
) -> crate::Result<(channel::Model, remote_project::Model)> {
self.transaction(|tx| async move {
let channel = self.get_channel_internal(channel_id, &tx).await?;
self.check_user_is_channel_admin(&channel, user_id, &tx)
.await?;
let project = remote_project::Entity::insert(remote_project::ActiveModel {
name: ActiveValue::Set(name.to_string()),
id: ActiveValue::NotSet,
channel_id: ActiveValue::Set(channel_id),
dev_server_id: ActiveValue::Set(dev_server_id),
path: ActiveValue::Set(path.to_string()),
})
.exec_with_returning(&*tx)
.await?;
Ok((channel, project))
})
.await
}
pub async fn share_remote_project(
&self,
remote_project_id: RemoteProjectId,
dev_server_id: DevServerId,
connection: ConnectionId,
worktrees: &[proto::WorktreeMetadata],
) -> crate::Result<proto::RemoteProject> {
self.transaction(|tx| async move {
let remote_project = remote_project::Entity::find_by_id(remote_project_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no remote project with id {}", remote_project_id))?;
if remote_project.dev_server_id != dev_server_id {
return Err(anyhow!("remote project shared from wrong server"))?;
}
let project = project::ActiveModel {
room_id: ActiveValue::Set(None),
host_user_id: ActiveValue::Set(None),
host_connection_id: ActiveValue::set(Some(connection.id as i32)),
host_connection_server_id: ActiveValue::set(Some(ServerId(
connection.owner_id as i32,
))),
id: ActiveValue::NotSet,
hosted_project_id: ActiveValue::Set(None),
remote_project_id: ActiveValue::Set(Some(remote_project_id)),
}
.insert(&*tx)
.await?;
if !worktrees.is_empty() {
worktree::Entity::insert_many(worktrees.iter().map(|worktree| {
worktree::ActiveModel {
id: ActiveValue::set(worktree.id as i64),
project_id: ActiveValue::set(project.id),
abs_path: ActiveValue::set(worktree.abs_path.clone()),
root_name: ActiveValue::set(worktree.root_name.clone()),
visible: ActiveValue::set(worktree.visible),
scan_id: ActiveValue::set(0),
completed_scan_id: ActiveValue::set(0),
}
}))
.exec(&*tx)
.await?;
}
Ok(remote_project.to_proto(Some(project)))
})
.await
}
pub async fn reshare_remote_projects(
&self,
reshared_projects: &Vec<proto::UpdateProject>,
dev_server_id: DevServerId,
connection: ConnectionId,
) -> crate::Result<Vec<ResharedProject>> {
// todo!() project_transaction? (maybe we can make the lock per-dev-server instead of per-project?)
self.transaction(|tx| async move {
let mut ret = Vec::new();
for reshared_project in reshared_projects {
let project_id = ProjectId::from_proto(reshared_project.project_id);
let (project, remote_project) = project::Entity::find_by_id(project_id)
.find_also_related(remote_project::Entity)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("project does not exist"))?;
if remote_project.map(|rp| rp.dev_server_id) != Some(dev_server_id) {
return Err(anyhow!("remote project reshared from wrong server"))?;
}
let Ok(old_connection_id) = project.host_connection() else {
return Err(anyhow!("remote project was not shared"))?;
};
project::Entity::update(project::ActiveModel {
id: ActiveValue::set(project_id),
host_connection_id: ActiveValue::set(Some(connection.id as i32)),
host_connection_server_id: ActiveValue::set(Some(ServerId(
connection.owner_id as i32,
))),
..Default::default()
})
.exec(&*tx)
.await?;
let collaborators = project
.find_related(project_collaborator::Entity)
.all(&*tx)
.await?;
self.update_project_worktrees(project_id, &reshared_project.worktrees, &tx)
.await?;
ret.push(super::ResharedProject {
id: project_id,
old_connection_id,
collaborators: collaborators
.iter()
.map(|collaborator| super::ProjectCollaborator {
connection_id: collaborator.connection(),
user_id: collaborator.user_id,
replica_id: collaborator.replica_id,
is_host: collaborator.is_host,
})
.collect(),
worktrees: reshared_project.worktrees.clone(),
});
}
Ok(ret)
})
.await
}
pub async fn rejoin_remote_projects(
&self,
rejoined_projects: &Vec<proto::RejoinProject>,
user_id: UserId,
connection_id: ConnectionId,
) -> crate::Result<Vec<RejoinedProject>> {
// todo!() project_transaction? (maybe we can make the lock per-dev-server instead of per-project?)
self.transaction(|tx| async move {
let mut ret = Vec::new();
for rejoined_project in rejoined_projects {
if let Some(project) = self
.rejoin_project_internal(&tx, rejoined_project, user_id, connection_id)
.await?
{
ret.push(project);
}
}
Ok(ret)
})
.await
}
}

View File

@@ -6,7 +6,7 @@ impl Database {
&self,
room_id: RoomId,
new_server_id: ServerId,
) -> Result<RoomGuard<RefreshedRoom>> {
) -> Result<TransactionGuard<RefreshedRoom>> {
self.room_transaction(room_id, |tx| async move {
let stale_participant_filter = Condition::all()
.add(room_participant::Column::RoomId.eq(room_id))
@@ -149,7 +149,7 @@ impl Database {
calling_connection: ConnectionId,
called_user_id: UserId,
initial_project_id: Option<ProjectId>,
) -> Result<RoomGuard<(proto::Room, proto::IncomingCall)>> {
) -> Result<TransactionGuard<(proto::Room, proto::IncomingCall)>> {
self.room_transaction(room_id, |tx| async move {
let caller = room_participant::Entity::find()
.filter(
@@ -201,7 +201,7 @@ impl Database {
&self,
room_id: RoomId,
called_user_id: UserId,
) -> Result<RoomGuard<proto::Room>> {
) -> Result<TransactionGuard<proto::Room>> {
self.room_transaction(room_id, |tx| async move {
room_participant::Entity::delete_many()
.filter(
@@ -221,7 +221,7 @@ impl Database {
&self,
expected_room_id: Option<RoomId>,
user_id: UserId,
) -> Result<Option<RoomGuard<proto::Room>>> {
) -> Result<Option<TransactionGuard<proto::Room>>> {
self.optional_room_transaction(|tx| async move {
let mut filter = Condition::all()
.add(room_participant::Column::UserId.eq(user_id))
@@ -258,7 +258,7 @@ impl Database {
room_id: RoomId,
calling_connection: ConnectionId,
called_user_id: UserId,
) -> Result<RoomGuard<proto::Room>> {
) -> Result<TransactionGuard<proto::Room>> {
self.room_transaction(room_id, |tx| async move {
let participant = room_participant::Entity::find()
.filter(
@@ -294,7 +294,7 @@ impl Database {
room_id: RoomId,
user_id: UserId,
connection: ConnectionId,
) -> Result<RoomGuard<JoinRoom>> {
) -> Result<TransactionGuard<JoinRoom>> {
self.room_transaction(room_id, |tx| async move {
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
enum QueryChannelId {
@@ -472,7 +472,7 @@ impl Database {
rejoin_room: proto::RejoinRoom,
user_id: UserId,
connection: ConnectionId,
) -> Result<RoomGuard<RejoinedRoom>> {
) -> Result<TransactionGuard<RejoinedRoom>> {
let room_id = RoomId::from_proto(rejoin_room.id);
self.room_transaction(room_id, |tx| async {
let tx = tx;
@@ -572,180 +572,12 @@ impl Database {
let mut rejoined_projects = Vec::new();
for rejoined_project in &rejoin_room.rejoined_projects {
let project_id = ProjectId::from_proto(rejoined_project.id);
let Some(project) = project::Entity::find_by_id(project_id).one(&*tx).await? else {
continue;
};
let mut worktrees = Vec::new();
let db_worktrees = project.find_related(worktree::Entity).all(&*tx).await?;
for db_worktree in db_worktrees {
let mut worktree = RejoinedWorktree {
id: db_worktree.id as u64,
abs_path: db_worktree.abs_path,
root_name: db_worktree.root_name,
visible: db_worktree.visible,
updated_entries: Default::default(),
removed_entries: Default::default(),
updated_repositories: Default::default(),
removed_repositories: Default::default(),
diagnostic_summaries: Default::default(),
settings_files: Default::default(),
scan_id: db_worktree.scan_id as u64,
completed_scan_id: db_worktree.completed_scan_id as u64,
};
let rejoined_worktree = rejoined_project
.worktrees
.iter()
.find(|worktree| worktree.id == db_worktree.id as u64);
// File entries
{
let entry_filter = if let Some(rejoined_worktree) = rejoined_worktree {
worktree_entry::Column::ScanId.gt(rejoined_worktree.scan_id)
} else {
worktree_entry::Column::IsDeleted.eq(false)
};
let mut db_entries = worktree_entry::Entity::find()
.filter(
Condition::all()
.add(worktree_entry::Column::ProjectId.eq(project.id))
.add(worktree_entry::Column::WorktreeId.eq(worktree.id))
.add(entry_filter),
)
.stream(&*tx)
.await?;
while let Some(db_entry) = db_entries.next().await {
let db_entry = db_entry?;
if db_entry.is_deleted {
worktree.removed_entries.push(db_entry.id as u64);
} else {
worktree.updated_entries.push(proto::Entry {
id: db_entry.id as u64,
is_dir: db_entry.is_dir,
path: db_entry.path,
inode: db_entry.inode as u64,
mtime: Some(proto::Timestamp {
seconds: db_entry.mtime_seconds as u64,
nanos: db_entry.mtime_nanos as u32,
}),
is_symlink: db_entry.is_symlink,
is_ignored: db_entry.is_ignored,
is_external: db_entry.is_external,
git_status: db_entry.git_status.map(|status| status as i32),
});
}
}
}
// Repository Entries
{
let repository_entry_filter =
if let Some(rejoined_worktree) = rejoined_worktree {
worktree_repository::Column::ScanId.gt(rejoined_worktree.scan_id)
} else {
worktree_repository::Column::IsDeleted.eq(false)
};
let mut db_repositories = worktree_repository::Entity::find()
.filter(
Condition::all()
.add(worktree_repository::Column::ProjectId.eq(project.id))
.add(worktree_repository::Column::WorktreeId.eq(worktree.id))
.add(repository_entry_filter),
)
.stream(&*tx)
.await?;
while let Some(db_repository) = db_repositories.next().await {
let db_repository = db_repository?;
if db_repository.is_deleted {
worktree
.removed_repositories
.push(db_repository.work_directory_id as u64);
} else {
worktree.updated_repositories.push(proto::RepositoryEntry {
work_directory_id: db_repository.work_directory_id as u64,
branch: db_repository.branch,
});
}
}
}
worktrees.push(worktree);
}
let language_servers = project
.find_related(language_server::Entity)
.all(&*tx)
if let Some(rejoined_project) = self
.rejoin_project_internal(&tx, rejoined_project, user_id, connection)
.await?
.into_iter()
.map(|language_server| proto::LanguageServer {
id: language_server.id as u64,
name: language_server.name,
})
.collect::<Vec<_>>();
{
let mut db_settings_files = worktree_settings_file::Entity::find()
.filter(worktree_settings_file::Column::ProjectId.eq(project_id))
.stream(&*tx)
.await?;
while let Some(db_settings_file) = db_settings_files.next().await {
let db_settings_file = db_settings_file?;
if let Some(worktree) = worktrees
.iter_mut()
.find(|w| w.id == db_settings_file.worktree_id as u64)
{
worktree.settings_files.push(WorktreeSettingsFile {
path: db_settings_file.path,
content: db_settings_file.content,
});
}
}
rejoined_projects.push(rejoined_project);
}
let mut collaborators = project
.find_related(project_collaborator::Entity)
.all(&*tx)
.await?;
let self_collaborator = if let Some(self_collaborator_ix) = collaborators
.iter()
.position(|collaborator| collaborator.user_id == user_id)
{
collaborators.swap_remove(self_collaborator_ix)
} else {
continue;
};
let old_connection_id = self_collaborator.connection();
project_collaborator::Entity::update(project_collaborator::ActiveModel {
connection_id: ActiveValue::set(connection.id as i32),
connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)),
..self_collaborator.into_active_model()
})
.exec(&*tx)
.await?;
let collaborators = collaborators
.into_iter()
.map(|collaborator| ProjectCollaborator {
connection_id: collaborator.connection(),
user_id: collaborator.user_id,
replica_id: collaborator.replica_id,
is_host: collaborator.is_host,
})
.collect::<Vec<_>>();
rejoined_projects.push(RejoinedProject {
id: project_id,
old_connection_id,
collaborators,
worktrees,
language_servers,
});
}
let (channel, room) = self.get_channel_room(room_id, &tx).await?;
@@ -760,10 +592,192 @@ impl Database {
.await
}
pub async fn rejoin_project_internal(
&self,
tx: &DatabaseTransaction,
rejoined_project: &proto::RejoinProject,
user_id: UserId,
connection: ConnectionId,
) -> Result<Option<RejoinedProject>> {
let project_id = ProjectId::from_proto(rejoined_project.id);
let Some(project) = project::Entity::find_by_id(project_id).one(tx).await? else {
return Ok(None);
};
let mut worktrees = Vec::new();
let db_worktrees = project.find_related(worktree::Entity).all(tx).await?;
for db_worktree in db_worktrees {
let mut worktree = RejoinedWorktree {
id: db_worktree.id as u64,
abs_path: db_worktree.abs_path,
root_name: db_worktree.root_name,
visible: db_worktree.visible,
updated_entries: Default::default(),
removed_entries: Default::default(),
updated_repositories: Default::default(),
removed_repositories: Default::default(),
diagnostic_summaries: Default::default(),
settings_files: Default::default(),
scan_id: db_worktree.scan_id as u64,
completed_scan_id: db_worktree.completed_scan_id as u64,
};
let rejoined_worktree = rejoined_project
.worktrees
.iter()
.find(|worktree| worktree.id == db_worktree.id as u64);
// File entries
{
let entry_filter = if let Some(rejoined_worktree) = rejoined_worktree {
worktree_entry::Column::ScanId.gt(rejoined_worktree.scan_id)
} else {
worktree_entry::Column::IsDeleted.eq(false)
};
let mut db_entries = worktree_entry::Entity::find()
.filter(
Condition::all()
.add(worktree_entry::Column::ProjectId.eq(project.id))
.add(worktree_entry::Column::WorktreeId.eq(worktree.id))
.add(entry_filter),
)
.stream(tx)
.await?;
while let Some(db_entry) = db_entries.next().await {
let db_entry = db_entry?;
if db_entry.is_deleted {
worktree.removed_entries.push(db_entry.id as u64);
} else {
worktree.updated_entries.push(proto::Entry {
id: db_entry.id as u64,
is_dir: db_entry.is_dir,
path: db_entry.path,
inode: db_entry.inode as u64,
mtime: Some(proto::Timestamp {
seconds: db_entry.mtime_seconds as u64,
nanos: db_entry.mtime_nanos as u32,
}),
is_symlink: db_entry.is_symlink,
is_ignored: db_entry.is_ignored,
is_external: db_entry.is_external,
git_status: db_entry.git_status.map(|status| status as i32),
});
}
}
}
// Repository Entries
{
let repository_entry_filter = if let Some(rejoined_worktree) = rejoined_worktree {
worktree_repository::Column::ScanId.gt(rejoined_worktree.scan_id)
} else {
worktree_repository::Column::IsDeleted.eq(false)
};
let mut db_repositories = worktree_repository::Entity::find()
.filter(
Condition::all()
.add(worktree_repository::Column::ProjectId.eq(project.id))
.add(worktree_repository::Column::WorktreeId.eq(worktree.id))
.add(repository_entry_filter),
)
.stream(tx)
.await?;
while let Some(db_repository) = db_repositories.next().await {
let db_repository = db_repository?;
if db_repository.is_deleted {
worktree
.removed_repositories
.push(db_repository.work_directory_id as u64);
} else {
worktree.updated_repositories.push(proto::RepositoryEntry {
work_directory_id: db_repository.work_directory_id as u64,
branch: db_repository.branch,
});
}
}
}
worktrees.push(worktree);
}
let language_servers = project
.find_related(language_server::Entity)
.all(tx)
.await?
.into_iter()
.map(|language_server| proto::LanguageServer {
id: language_server.id as u64,
name: language_server.name,
})
.collect::<Vec<_>>();
{
let mut db_settings_files = worktree_settings_file::Entity::find()
.filter(worktree_settings_file::Column::ProjectId.eq(project_id))
.stream(tx)
.await?;
while let Some(db_settings_file) = db_settings_files.next().await {
let db_settings_file = db_settings_file?;
if let Some(worktree) = worktrees
.iter_mut()
.find(|w| w.id == db_settings_file.worktree_id as u64)
{
worktree.settings_files.push(WorktreeSettingsFile {
path: db_settings_file.path,
content: db_settings_file.content,
});
}
}
}
let mut collaborators = project
.find_related(project_collaborator::Entity)
.all(tx)
.await?;
let self_collaborator = if let Some(self_collaborator_ix) = collaborators
.iter()
.position(|collaborator| collaborator.user_id == user_id)
{
collaborators.swap_remove(self_collaborator_ix)
} else {
return Ok(None);
};
let old_connection_id = self_collaborator.connection();
project_collaborator::Entity::update(project_collaborator::ActiveModel {
connection_id: ActiveValue::set(connection.id as i32),
connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)),
..self_collaborator.into_active_model()
})
.exec(tx)
.await?;
let collaborators = collaborators
.into_iter()
.map(|collaborator| ProjectCollaborator {
connection_id: collaborator.connection(),
user_id: collaborator.user_id,
replica_id: collaborator.replica_id,
is_host: collaborator.is_host,
})
.collect::<Vec<_>>();
return Ok(Some(RejoinedProject {
id: project_id,
old_connection_id,
collaborators,
worktrees,
language_servers,
}));
}
pub async fn leave_room(
&self,
connection: ConnectionId,
) -> Result<Option<RoomGuard<LeftRoom>>> {
) -> Result<Option<TransactionGuard<LeftRoom>>> {
self.optional_room_transaction(|tx| async move {
let leaving_participant = room_participant::Entity::find()
.filter(
@@ -935,7 +949,7 @@ impl Database {
room_id: RoomId,
connection: ConnectionId,
location: proto::ParticipantLocation,
) -> Result<RoomGuard<proto::Room>> {
) -> Result<TransactionGuard<proto::Room>> {
self.room_transaction(room_id, |tx| async {
let tx = tx;
let location_kind;
@@ -997,7 +1011,7 @@ impl Database {
room_id: RoomId,
user_id: UserId,
role: ChannelRole,
) -> Result<RoomGuard<proto::Room>> {
) -> Result<TransactionGuard<proto::Room>> {
self.room_transaction(room_id, |tx| async move {
room_participant::Entity::find()
.filter(
@@ -1150,7 +1164,7 @@ impl Database {
&self,
room_id: RoomId,
connection_id: ConnectionId,
) -> Result<RoomGuard<HashSet<ConnectionId>>> {
) -> Result<TransactionGuard<HashSet<ConnectionId>>> {
self.room_transaction(room_id, |tx| async move {
let mut participants = room_participant::Entity::find()
.filter(room_participant::Column::RoomId.eq(room_id))

View File

@@ -11,6 +11,7 @@ pub mod channel_message_mention;
pub mod contact;
pub mod contributor;
pub mod dev_server;
pub mod embedding;
pub mod extension;
pub mod extension_version;
pub mod feature_flag;
@@ -24,6 +25,7 @@ pub mod observed_channel_messages;
pub mod project;
pub mod project_collaborator;
pub mod rate_buckets;
pub mod remote_project;
pub mod room;
pub mod room_participant;
pub mod server;

View File

@@ -1,4 +1,5 @@
use crate::db::{ChannelId, DevServerId};
use rpc::proto;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
@@ -15,3 +16,14 @@ impl ActiveModelBehavior for ActiveModel {}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl Model {
pub fn to_proto(&self, status: proto::DevServerStatus) -> proto::DevServer {
proto::DevServer {
dev_server_id: self.id.to_proto(),
channel_id: self.channel_id.to_proto(),
name: self.name.clone(),
status: status as i32,
}
}
}

View File

@@ -0,0 +1,18 @@
use sea_orm::entity::prelude::*;
use time::PrimitiveDateTime;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "embeddings")]
pub struct Model {
#[sea_orm(primary_key)]
pub model: String,
#[sea_orm(primary_key)]
pub digest: Vec<u8>,
pub dimensions: Vec<f32>,
pub retrieved_at: PrimitiveDateTime,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -1,4 +1,4 @@
use crate::db::{HostedProjectId, ProjectId, Result, RoomId, ServerId, UserId};
use crate::db::{HostedProjectId, ProjectId, RemoteProjectId, Result, RoomId, ServerId, UserId};
use anyhow::anyhow;
use rpc::ConnectionId;
use sea_orm::entity::prelude::*;
@@ -13,6 +13,7 @@ pub struct Model {
pub host_connection_id: Option<i32>,
pub host_connection_server_id: Option<ServerId>,
pub hosted_project_id: Option<HostedProjectId>,
pub remote_project_id: Option<RemoteProjectId>,
}
impl Model {
@@ -56,6 +57,12 @@ pub enum Relation {
to = "super::hosted_project::Column::Id"
)]
HostedProject,
#[sea_orm(
belongs_to = "super::remote_project::Entity",
from = "Column::RemoteProjectId",
to = "super::remote_project::Column::Id"
)]
RemoteProject,
}
impl Related<super::user::Entity> for Entity {
@@ -94,4 +101,10 @@ impl Related<super::hosted_project::Entity> for Entity {
}
}
impl Related<super::remote_project::Entity> for Entity {
fn to() -> RelationDef {
Relation::RemoteProject.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,42 @@
use super::project;
use crate::db::{ChannelId, DevServerId, RemoteProjectId};
use rpc::proto;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "remote_projects")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: RemoteProjectId,
pub channel_id: ChannelId,
pub dev_server_id: DevServerId,
pub name: String,
pub path: String,
}
impl ActiveModelBehavior for ActiveModel {}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_one = "super::project::Entity")]
Project,
}
impl Related<super::project::Entity> for Entity {
fn to() -> RelationDef {
Relation::Project.def()
}
}
impl Model {
pub fn to_proto(&self, project: Option<project::Model>) -> proto::RemoteProject {
proto::RemoteProject {
id: self.id.to_proto(),
project_id: project.map(|p| p.id.to_proto()),
channel_id: self.channel_id.to_proto(),
dev_server_id: self.dev_server_id.to_proto(),
name: self.name.clone(),
path: self.path.clone(),
}
}
}

View File

@@ -2,6 +2,7 @@ mod buffer_tests;
mod channel_tests;
mod contributor_tests;
mod db_tests;
mod embedding_tests;
mod extension_tests;
mod feature_flag_tests;
mod message_tests;

View File

@@ -0,0 +1,84 @@
use super::TestDb;
use crate::db::embedding;
use collections::HashMap;
use sea_orm::{sea_query::Expr, ColumnTrait, EntityTrait, QueryFilter};
use std::ops::Sub;
use time::{Duration, OffsetDateTime, PrimitiveDateTime};
// SQLite does not support array arguments, so we only test this against a real postgres instance
#[gpui::test]
async fn test_get_embeddings_postgres(cx: &mut gpui::TestAppContext) {
let test_db = TestDb::postgres(cx.executor().clone());
let db = test_db.db();
let provider = "test_model";
let digest1 = vec![1, 2, 3];
let digest2 = vec![4, 5, 6];
let embeddings = HashMap::from_iter([
(digest1.clone(), vec![0.1, 0.2, 0.3]),
(digest2.clone(), vec![0.4, 0.5, 0.6]),
]);
// Save embeddings
db.save_embeddings(provider, &embeddings).await.unwrap();
// Retrieve embeddings
let retrieved_embeddings = db
.get_embeddings(provider, &[digest1.clone(), digest2.clone()])
.await
.unwrap();
assert_eq!(retrieved_embeddings.len(), 2);
assert!(retrieved_embeddings.contains_key(&digest1));
assert!(retrieved_embeddings.contains_key(&digest2));
// Check if the retrieved embeddings are correct
assert_eq!(retrieved_embeddings[&digest1], vec![0.1, 0.2, 0.3]);
assert_eq!(retrieved_embeddings[&digest2], vec![0.4, 0.5, 0.6]);
}
#[gpui::test]
async fn test_purge_old_embeddings(cx: &mut gpui::TestAppContext) {
let test_db = TestDb::postgres(cx.executor().clone());
let db = test_db.db();
let model = "test_model";
let digest = vec![7, 8, 9];
let embeddings = HashMap::from_iter([(digest.clone(), vec![0.7, 0.8, 0.9])]);
// Save old embeddings
db.save_embeddings(model, &embeddings).await.unwrap();
// Reach into the DB and change the retrieved at to be > 60 days
db.weak_transaction(|tx| {
let digest = digest.clone();
async move {
let sixty_days_ago = OffsetDateTime::now_utc().sub(Duration::days(61));
let retrieved_at = PrimitiveDateTime::new(sixty_days_ago.date(), sixty_days_ago.time());
embedding::Entity::update_many()
.filter(
embedding::Column::Model
.eq(model)
.and(embedding::Column::Digest.eq(digest)),
)
.col_expr(embedding::Column::RetrievedAt, Expr::value(retrieved_at))
.exec(&*tx)
.await
.unwrap();
Ok(())
}
})
.await
.unwrap();
// Purge old embeddings
db.purge_old_embeddings().await.unwrap();
// Try to retrieve the purged embeddings
let retrieved_embeddings = db.get_embeddings(model, &[digest.clone()]).await.unwrap();
assert!(
retrieved_embeddings.is_empty(),
"Old embeddings should have been purged"
);
}

View File

@@ -6,8 +6,8 @@ use axum::{
Extension, Router,
};
use collab::{
api::fetch_extensions_from_blob_store_periodically, db, env, executor::Executor, AppState,
Config, RateLimiter, Result,
api::fetch_extensions_from_blob_store_periodically, db, env, executor::Executor,
rpc::ResultExt, AppState, Config, RateLimiter, Result,
};
use db::Database;
use std::{
@@ -23,7 +23,7 @@ use tower_http::trace::TraceLayer;
use tracing_subscriber::{
filter::EnvFilter, fmt::format::JsonFields, util::SubscriberInitExt, Layer,
};
use util::ResultExt;
use util::ResultExt as _;
const VERSION: &str = env!("CARGO_PKG_VERSION");
const REVISION: Option<&'static str> = option_env!("GITHUB_SHA");
@@ -90,6 +90,7 @@ async fn main() -> Result<()> {
};
if is_collab {
state.db.purge_old_embeddings().await.trace_err();
RateLimiter::save_periodically(state.rate_limiter.clone(), state.executor.clone());
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
use crate::db::{ChannelId, ChannelRole, UserId};
use crate::db::{ChannelId, ChannelRole, DevServerId, PrincipalId, UserId};
use anyhow::{anyhow, Result};
use collections::{BTreeMap, HashMap, HashSet};
use rpc::ConnectionId;
use rpc::{proto, ConnectionId};
use semantic_version::SemanticVersion;
use serde::Serialize;
use std::fmt;
@@ -10,12 +10,13 @@ use tracing::instrument;
#[derive(Default, Serialize)]
pub struct ConnectionPool {
connections: BTreeMap<ConnectionId, Connection>,
connected_users: BTreeMap<UserId, ConnectedUser>,
connected_users: BTreeMap<UserId, ConnectedPrincipal>,
connected_dev_servers: BTreeMap<DevServerId, ConnectionId>,
channels: ChannelPool,
}
#[derive(Default, Serialize)]
struct ConnectedUser {
struct ConnectedPrincipal {
connection_ids: HashSet<ConnectionId>,
}
@@ -30,13 +31,13 @@ impl fmt::Display for ZedVersion {
impl ZedVersion {
pub fn can_collaborate(&self) -> bool {
self.0 >= SemanticVersion::new(0, 127, 3)
self.0 >= SemanticVersion::new(0, 129, 2)
}
}
#[derive(Serialize)]
pub struct Connection {
pub user_id: UserId,
pub principal_id: PrincipalId,
pub admin: bool,
pub zed_version: ZedVersion,
}
@@ -59,7 +60,7 @@ impl ConnectionPool {
self.connections.insert(
connection_id,
Connection {
user_id,
principal_id: PrincipalId::UserId(user_id),
admin,
zed_version,
},
@@ -68,6 +69,25 @@ impl ConnectionPool {
connected_user.connection_ids.insert(connection_id);
}
pub fn add_dev_server(
&mut self,
connection_id: ConnectionId,
dev_server_id: DevServerId,
zed_version: ZedVersion,
) {
self.connections.insert(
connection_id,
Connection {
principal_id: PrincipalId::DevServerId(dev_server_id),
admin: false,
zed_version,
},
);
self.connected_dev_servers
.insert(dev_server_id, connection_id);
}
#[instrument(skip(self))]
pub fn remove_connection(&mut self, connection_id: ConnectionId) -> Result<()> {
let connection = self
@@ -75,12 +95,18 @@ impl ConnectionPool {
.get_mut(&connection_id)
.ok_or_else(|| anyhow!("no such connection"))?;
let user_id = connection.user_id;
let connected_user = self.connected_users.get_mut(&user_id).unwrap();
connected_user.connection_ids.remove(&connection_id);
if connected_user.connection_ids.is_empty() {
self.connected_users.remove(&user_id);
self.channels.remove_user(&user_id);
match connection.principal_id {
PrincipalId::UserId(user_id) => {
let connected_user = self.connected_users.get_mut(&user_id).unwrap();
connected_user.connection_ids.remove(&connection_id);
if connected_user.connection_ids.is_empty() {
self.connected_users.remove(&user_id);
self.channels.remove_user(&user_id);
}
}
PrincipalId::DevServerId(dev_server_id) => {
self.connected_dev_servers.remove(&dev_server_id);
}
}
self.connections.remove(&connection_id).unwrap();
Ok(())
@@ -110,6 +136,18 @@ impl ConnectionPool {
.copied()
}
pub fn dev_server_status(&self, dev_server_id: DevServerId) -> proto::DevServerStatus {
if self.dev_server_connection_id(dev_server_id).is_some() {
proto::DevServerStatus::Online
} else {
proto::DevServerStatus::Offline
}
}
pub fn dev_server_connection_id(&self, dev_server_id: DevServerId) -> Option<ConnectionId> {
self.connected_dev_servers.get(&dev_server_id).copied()
}
pub fn channel_user_ids(
&self,
channel_id: ChannelId,
@@ -154,22 +192,39 @@ impl ConnectionPool {
#[cfg(test)]
pub fn check_invariants(&self) {
for (connection_id, connection) in &self.connections {
assert!(self
.connected_users
.get(&connection.user_id)
.unwrap()
.connection_ids
.contains(connection_id));
match &connection.principal_id {
PrincipalId::UserId(user_id) => {
assert!(self
.connected_users
.get(user_id)
.unwrap()
.connection_ids
.contains(connection_id));
}
PrincipalId::DevServerId(dev_server_id) => {
assert_eq!(
self.connected_dev_servers.get(&dev_server_id).unwrap(),
connection_id
);
}
}
}
for (user_id, state) in &self.connected_users {
for connection_id in &state.connection_ids {
assert_eq!(
self.connections.get(connection_id).unwrap().user_id,
*user_id
self.connections.get(connection_id).unwrap().principal_id,
PrincipalId::UserId(*user_id)
);
}
}
for (dev_server_id, connection_id) in &self.connected_dev_servers {
assert_eq!(
self.connections.get(connection_id).unwrap().principal_id,
PrincipalId::DevServerId(*dev_server_id)
);
}
}
}

View File

@@ -8,6 +8,7 @@ mod channel_buffer_tests;
mod channel_guest_tests;
mod channel_message_tests;
mod channel_tests;
mod dev_server_tests;
mod editor_tests;
mod following_tests;
mod integration_tests;

View File

@@ -0,0 +1,110 @@
use std::path::Path;
use editor::Editor;
use fs::Fs;
use gpui::VisualTestContext;
use rpc::proto::DevServerStatus;
use serde_json::json;
use crate::tests::TestServer;
#[gpui::test]
async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppContext) {
let (server, client) = TestServer::start1(cx).await;
let channel_id = server
.make_channel("test", None, (&client, cx), &mut [])
.await;
let resp = client
.channel_store()
.update(cx, |store, cx| {
store.create_dev_server(channel_id, "server-1".to_string(), cx)
})
.await
.unwrap();
client.channel_store().update(cx, |store, _| {
assert_eq!(store.dev_servers_for_id(channel_id).len(), 1);
assert_eq!(store.dev_servers_for_id(channel_id)[0].name, "server-1");
assert_eq!(
store.dev_servers_for_id(channel_id)[0].status,
DevServerStatus::Offline
);
});
let dev_server = server.create_dev_server(resp.access_token, cx2).await;
cx.executor().run_until_parked();
client.channel_store().update(cx, |store, _| {
assert_eq!(
store.dev_servers_for_id(channel_id)[0].status,
DevServerStatus::Online
);
});
dev_server
.fs()
.insert_tree(
"/remote",
json!({
"1.txt": "remote\nremote\nremote",
"2.js": "function two() { return 2; }",
"3.rs": "mod test",
}),
)
.await;
client
.channel_store()
.update(cx, |store, cx| {
store.create_remote_project(
channel_id,
client::DevServerId(resp.dev_server_id),
"project-1".to_string(),
"/remote".to_string(),
cx,
)
})
.await
.unwrap();
cx.executor().run_until_parked();
let remote_workspace = client
.channel_store()
.update(cx, |store, cx| {
let projects = store.remote_projects_for_id(channel_id);
assert_eq!(projects.len(), 1);
assert_eq!(projects[0].name, "project-1");
workspace::join_remote_project(
projects[0].project_id.unwrap(),
client.app_state.clone(),
cx,
)
})
.await
.unwrap();
cx.executor().run_until_parked();
let cx2 = VisualTestContext::from_window(remote_workspace.into(), cx).as_mut();
cx2.simulate_keystrokes("cmd-p 1 enter");
let editor = remote_workspace
.update(cx2, |ws, cx| {
ws.active_item_as::<Editor>(cx).unwrap().clone()
})
.unwrap();
editor.update(cx2, |ed, cx| {
assert_eq!(ed.text(cx).to_string(), "remote\nremote\nremote");
});
cx2.simulate_input("wow!");
cx2.simulate_keystrokes("cmd-s");
let content = dev_server
.fs()
.load(&Path::new("/remote/1.txt"))
.await
.unwrap();
assert_eq!(content, "wow!remote\nremote\nremote\n");
}

View File

@@ -42,6 +42,7 @@ use std::{
time::Duration,
};
use unindent::Unindent as _;
use workspace::Pane;
#[ctor::ctor]
fn init_logger() {
@@ -3759,7 +3760,7 @@ async fn test_leaving_project(
// Client B can't join the project, unless they re-join the room.
cx_b.spawn(|cx| {
Project::remote(
Project::in_room(
project_id,
client_b.app_state.client.clone(),
client_b.user_store().clone(),
@@ -6127,3 +6128,269 @@ async fn test_join_after_restart(cx1: &mut TestAppContext, cx2: &mut TestAppCont
let client2 = server.create_client(cx2, "user_a").await;
join_channel(channel2, &client2, cx2).await.unwrap();
}
#[gpui::test]
async fn test_preview_tabs(cx: &mut TestAppContext) {
let (_server, client) = TestServer::start1(cx).await;
let (workspace, cx) = client.build_test_workspace(cx).await;
let project = workspace.update(cx, |workspace, _| workspace.project().clone());
let worktree_id = project.update(cx, |project, cx| {
project.worktrees().next().unwrap().read(cx).id()
});
let path_1 = ProjectPath {
worktree_id,
path: Path::new("1.txt").into(),
};
let path_2 = ProjectPath {
worktree_id,
path: Path::new("2.js").into(),
};
let path_3 = ProjectPath {
worktree_id,
path: Path::new("3.rs").into(),
};
let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
let get_path = |pane: &Pane, idx: usize, cx: &AppContext| {
pane.item_for_index(idx).unwrap().project_path(cx).unwrap()
};
// Opening item 3 as a "permanent" tab
workspace
.update(cx, |workspace, cx| {
workspace.open_path(path_3.clone(), None, false, cx)
})
.await
.unwrap();
pane.update(cx, |pane, cx| {
assert_eq!(pane.items_len(), 1);
assert_eq!(get_path(pane, 0, cx), path_3.clone());
assert_eq!(pane.preview_item_id(), None);
assert!(!pane.can_navigate_backward());
assert!(!pane.can_navigate_forward());
});
// Open item 1 as preview
workspace
.update(cx, |workspace, cx| {
workspace.open_path_preview(path_1.clone(), None, true, true, cx)
})
.await
.unwrap();
pane.update(cx, |pane, cx| {
assert_eq!(pane.items_len(), 2);
assert_eq!(get_path(pane, 0, cx), path_3.clone());
assert_eq!(get_path(pane, 1, cx), path_1.clone());
assert_eq!(
pane.preview_item_id(),
Some(pane.items().nth(1).unwrap().item_id())
);
assert!(pane.can_navigate_backward());
assert!(!pane.can_navigate_forward());
});
// Open item 2 as preview
workspace
.update(cx, |workspace, cx| {
workspace.open_path_preview(path_2.clone(), None, true, true, cx)
})
.await
.unwrap();
pane.update(cx, |pane, cx| {
assert_eq!(pane.items_len(), 2);
assert_eq!(get_path(pane, 0, cx), path_3.clone());
assert_eq!(get_path(pane, 1, cx), path_2.clone());
assert_eq!(
pane.preview_item_id(),
Some(pane.items().nth(1).unwrap().item_id())
);
assert!(pane.can_navigate_backward());
assert!(!pane.can_navigate_forward());
});
// Going back should show item 1 as preview
workspace
.update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
.await
.unwrap();
pane.update(cx, |pane, cx| {
assert_eq!(pane.items_len(), 2);
assert_eq!(get_path(pane, 0, cx), path_3.clone());
assert_eq!(get_path(pane, 1, cx), path_1.clone());
assert_eq!(
pane.preview_item_id(),
Some(pane.items().nth(1).unwrap().item_id())
);
assert!(pane.can_navigate_backward());
assert!(pane.can_navigate_forward());
});
// Closing item 1
pane.update(cx, |pane, cx| {
pane.close_item_by_id(
pane.active_item().unwrap().item_id(),
workspace::SaveIntent::Skip,
cx,
)
})
.await
.unwrap();
pane.update(cx, |pane, cx| {
assert_eq!(pane.items_len(), 1);
assert_eq!(get_path(pane, 0, cx), path_3.clone());
assert_eq!(pane.preview_item_id(), None);
assert!(pane.can_navigate_backward());
assert!(!pane.can_navigate_forward());
});
// Going back should show item 1 as preview
workspace
.update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
.await
.unwrap();
pane.update(cx, |pane, cx| {
assert_eq!(pane.items_len(), 2);
assert_eq!(get_path(pane, 0, cx), path_3.clone());
assert_eq!(get_path(pane, 1, cx), path_1.clone());
assert_eq!(
pane.preview_item_id(),
Some(pane.items().nth(1).unwrap().item_id())
);
assert!(pane.can_navigate_backward());
assert!(pane.can_navigate_forward());
});
// Close permanent tab
pane.update(cx, |pane, cx| {
let id = pane.items().nth(0).unwrap().item_id();
pane.close_item_by_id(id, workspace::SaveIntent::Skip, cx)
})
.await
.unwrap();
pane.update(cx, |pane, cx| {
assert_eq!(pane.items_len(), 1);
assert_eq!(get_path(pane, 0, cx), path_1.clone());
assert_eq!(
pane.preview_item_id(),
Some(pane.items().nth(0).unwrap().item_id())
);
assert!(pane.can_navigate_backward());
assert!(pane.can_navigate_forward());
});
// Split pane to the right
pane.update(cx, |pane, cx| {
pane.split(workspace::SplitDirection::Right, cx);
});
let right_pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
pane.update(cx, |pane, cx| {
assert_eq!(pane.items_len(), 1);
assert_eq!(get_path(pane, 0, cx), path_1.clone());
assert_eq!(
pane.preview_item_id(),
Some(pane.items().nth(0).unwrap().item_id())
);
assert!(pane.can_navigate_backward());
assert!(pane.can_navigate_forward());
});
right_pane.update(cx, |pane, cx| {
assert_eq!(pane.items_len(), 1);
assert_eq!(get_path(pane, 0, cx), path_1.clone());
assert_eq!(pane.preview_item_id(), None);
assert!(!pane.can_navigate_backward());
assert!(!pane.can_navigate_forward());
});
// Open item 2 as preview in right pane
workspace
.update(cx, |workspace, cx| {
workspace.open_path_preview(path_2.clone(), None, true, true, cx)
})
.await
.unwrap();
pane.update(cx, |pane, cx| {
assert_eq!(pane.items_len(), 1);
assert_eq!(get_path(pane, 0, cx), path_1.clone());
assert_eq!(
pane.preview_item_id(),
Some(pane.items().nth(0).unwrap().item_id())
);
assert!(pane.can_navigate_backward());
assert!(pane.can_navigate_forward());
});
right_pane.update(cx, |pane, cx| {
assert_eq!(pane.items_len(), 2);
assert_eq!(get_path(pane, 0, cx), path_1.clone());
assert_eq!(get_path(pane, 1, cx), path_2.clone());
assert_eq!(
pane.preview_item_id(),
Some(pane.items().nth(1).unwrap().item_id())
);
assert!(pane.can_navigate_backward());
assert!(!pane.can_navigate_forward());
});
// Focus left pane
workspace.update(cx, |workspace, cx| {
workspace.activate_pane_in_direction(workspace::SplitDirection::Left, cx)
});
// Open item 2 as preview in left pane
workspace
.update(cx, |workspace, cx| {
workspace.open_path_preview(path_2.clone(), None, true, true, cx)
})
.await
.unwrap();
pane.update(cx, |pane, cx| {
assert_eq!(pane.items_len(), 1);
assert_eq!(get_path(pane, 0, cx), path_2.clone());
assert_eq!(
pane.preview_item_id(),
Some(pane.items().nth(0).unwrap().item_id())
);
assert!(pane.can_navigate_backward());
assert!(!pane.can_navigate_forward());
});
right_pane.update(cx, |pane, cx| {
assert_eq!(pane.items_len(), 2);
assert_eq!(get_path(pane, 0, cx), path_1.clone());
assert_eq!(get_path(pane, 1, cx), path_2.clone());
assert_eq!(
pane.preview_item_id(),
Some(pane.items().nth(1).unwrap().item_id())
);
assert!(pane.can_navigate_backward());
assert!(!pane.can_navigate_forward());
});
}

View File

@@ -1347,13 +1347,11 @@ impl RandomizedTest for ProjectCollaborationTest {
client.username
);
let host_saved_version_fingerprint =
host_buffer.read_with(host_cx, |b, _| b.saved_version_fingerprint());
let guest_saved_version_fingerprint =
guest_buffer.read_with(client_cx, |b, _| b.saved_version_fingerprint());
let host_is_dirty = host_buffer.read_with(host_cx, |b, _| b.is_dirty());
let guest_is_dirty = guest_buffer.read_with(client_cx, |b, _| b.is_dirty());
assert_eq!(
guest_saved_version_fingerprint, host_saved_version_fingerprint,
"guest {} saved fingerprint does not match host's for path {path:?} in project {project_id}",
guest_is_dirty, host_is_dirty,
"guest {} dirty state does not match host's for path {path:?} in project {project_id}",
client.username
);

View File

@@ -1,4 +1,5 @@
use crate::{
auth::split_dev_server_token,
db::{tests::TestDb, NewUserParams, UserId},
executor::Executor,
rpc::{Principal, Server, ZedVersion, CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
@@ -302,6 +303,130 @@ impl TestServer {
client
}
pub async fn create_dev_server(
&self,
access_token: String,
cx: &mut TestAppContext,
) -> TestClient {
cx.update(|cx| {
if cx.has_global::<SettingsStore>() {
panic!("Same cx used to create two test clients")
}
let settings = SettingsStore::test(cx);
cx.set_global(settings);
release_channel::init("0.0.0", cx);
client::init_settings(cx);
});
let (dev_server_id, _) = split_dev_server_token(&access_token).unwrap();
let clock = Arc::new(FakeSystemClock::default());
let http = FakeHttpClient::with_404_response();
let mut client = cx.update(|cx| Client::new(clock, http.clone(), cx));
let server = self.server.clone();
let db = self.app_state.db.clone();
let connection_killers = self.connection_killers.clone();
let forbid_connections = self.forbid_connections.clone();
Arc::get_mut(&mut client)
.unwrap()
.set_id(1)
.set_dev_server_token(client::DevServerToken(access_token.clone()))
.override_establish_connection(move |credentials, cx| {
assert_eq!(
credentials,
&Credentials::DevServer {
token: client::DevServerToken(access_token.to_string())
}
);
let server = server.clone();
let db = db.clone();
let connection_killers = connection_killers.clone();
let forbid_connections = forbid_connections.clone();
cx.spawn(move |cx| async move {
if forbid_connections.load(SeqCst) {
Err(EstablishConnectionError::other(anyhow!(
"server is forbidding connections"
)))
} else {
let (client_conn, server_conn, killed) =
Connection::in_memory(cx.background_executor().clone());
let (connection_id_tx, connection_id_rx) = oneshot::channel();
let dev_server = db
.get_dev_server(dev_server_id)
.await
.expect("retrieving dev_server failed");
cx.background_executor()
.spawn(server.handle_connection(
server_conn,
"dev-server".to_string(),
Principal::DevServer(dev_server),
ZedVersion(SemanticVersion::new(1, 0, 0)),
Some(connection_id_tx),
Executor::Deterministic(cx.background_executor().clone()),
))
.detach();
let connection_id = connection_id_rx.await.map_err(|e| {
EstablishConnectionError::Other(anyhow!(
"{} (is server shutting down?)",
e
))
})?;
connection_killers
.lock()
.insert(connection_id.into(), killed);
Ok(client_conn)
}
})
});
let fs = FakeFs::new(cx.executor());
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
let app_state = Arc::new(workspace::AppState {
client: client.clone(),
user_store: user_store.clone(),
workspace_store,
languages: language_registry,
fs: fs.clone(),
build_window_options: |_, _| Default::default(),
node_runtime: FakeNodeRuntime::new(),
});
cx.update(|cx| {
theme::init(theme::LoadThemes::JustBase, cx);
Project::init(&client, cx);
client::init(&client, cx);
language::init(cx);
editor::init(cx);
workspace::init(app_state.clone(), cx);
call::init(client.clone(), user_store.clone(), cx);
channel::init(&client, user_store.clone(), cx);
notifications::init(client.clone(), user_store, cx);
collab_ui::init(&app_state, cx);
file_finder::init(cx);
menu::init();
headless::init(
client.clone(),
headless::AppState {
languages: app_state.languages.clone(),
user_store: app_state.user_store.clone(),
fs: fs.clone(),
node_runtime: app_state.node_runtime.clone(),
},
cx,
);
});
TestClient {
app_state,
username: "dev-server".to_string(),
channel_store: cx.read(ChannelStore::global).clone(),
notification_store: cx.read(NotificationStore::global).clone(),
state: Default::default(),
}
}
pub fn disconnect_client(&self, peer_id: PeerId) {
self.connection_killers
.lock()

View File

@@ -39,6 +39,7 @@ db.workspace = true
editor.workspace = true
emojis.workspace = true
extensions_ui.workspace = true
feature_flags.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true

View File

@@ -22,8 +22,9 @@ use std::{
};
use ui::{prelude::*, Label};
use util::ResultExt;
use workspace::notifications::NotificationId;
use workspace::{
item::{FollowableItem, Item, ItemEvent, ItemHandle},
item::{FollowableItem, Item, ItemEvent, ItemHandle, TabContentParams},
register_followable_item,
searchable::SearchableItemHandle,
ItemNavHistory, Pane, SaveIntent, Toast, ViewId, Workspace, WorkspaceId,
@@ -269,7 +270,15 @@ impl ChannelView {
cx.write_to_clipboard(ClipboardItem::new(link));
self.workspace
.update(cx, |workspace, cx| {
workspace.show_toast(Toast::new(0, "Link copied to clipboard"), cx);
struct CopyLinkForPositionToast;
workspace.show_toast(
Toast::new(
NotificationId::unique::<CopyLinkForPositionToast>(),
"Link copied to clipboard",
),
cx,
);
})
.ok();
}
@@ -365,7 +374,7 @@ impl Item for ChannelView {
}
}
fn tab_content(&self, _: Option<usize>, selected: bool, cx: &WindowContext) -> AnyElement {
fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement {
let label = if let Some(channel) = self.channel(cx) {
match (
self.channel_buffer.read(cx).buffer().read(cx).read_only(),
@@ -379,7 +388,7 @@ impl Item for ChannelView {
"channel notes (disconnected)".to_string()
};
Label::new(label)
.color(if selected {
.color(if params.selected {
Color::Default
} else {
Color::Muted

View File

@@ -531,6 +531,8 @@ impl ChatPanel {
&self.languages,
self.client.id(),
&message,
self.local_timezone,
cx,
)
});
el.child(
@@ -744,6 +746,8 @@ impl ChatPanel {
language_registry: &Arc<LanguageRegistry>,
current_user_id: u64,
message: &channel::ChannelMessage,
local_timezone: UtcOffset,
cx: &AppContext,
) -> RichText {
let mentions = message
.mentions
@@ -754,24 +758,39 @@ impl ChatPanel {
})
.collect::<Vec<_>>();
const MESSAGE_UPDATED: &str = " (edited)";
const MESSAGE_EDITED: &str = " (edited)";
let mut body = message.body.clone();
if message.edited_at.is_some() {
body.push_str(MESSAGE_UPDATED);
body.push_str(MESSAGE_EDITED);
}
let mut rich_text = rich_text::render_rich_text(body, &mentions, language_registry, None);
if message.edited_at.is_some() {
let range = (rich_text.text.len() - MESSAGE_EDITED.len())..rich_text.text.len();
rich_text.highlights.push((
(rich_text.text.len() - MESSAGE_UPDATED.len())..rich_text.text.len(),
range.clone(),
Highlight::Highlight(HighlightStyle {
fade_out: Some(0.8),
color: Some(cx.theme().colors().text_muted),
..Default::default()
}),
));
if let Some(edit_timestamp) = message.edited_at {
let edit_timestamp_text = time_format::format_localized_timestamp(
edit_timestamp,
OffsetDateTime::now_utc(),
local_timezone,
time_format::TimestampFormat::Absolute,
);
rich_text.custom_ranges.push(range);
rich_text.set_tooltip_builder_for_custom_ranges(move |_, _, cx| {
Some(Tooltip::text(edit_timestamp_text.clone(), cx))
})
}
}
rich_text
}
@@ -913,6 +932,10 @@ impl ChatPanel {
impl Render for ChatPanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let channel_id = self
.active_chat
.as_ref()
.map(|(c, _)| c.read(cx).channel_id);
let message_editor = self.message_editor.read(cx);
let reply_to_message_id = message_editor.reply_to_message_id();
@@ -1022,6 +1045,7 @@ impl Render for ChatPanel {
.child(
div().flex_shrink().overflow_hidden().child(
h_flex()
.id(("reply-preview", reply_to_message_id))
.child(Label::new("Replying to ").size(LabelSize::Small))
.child(
div().font_weight(FontWeight::BOLD).child(
@@ -1031,7 +1055,20 @@ impl Render for ChatPanel {
))
.size(LabelSize::Small),
),
),
)
.when_some(channel_id, |this, channel_id| {
this.cursor_pointer().on_click(cx.listener(
move |chat_panel, _, cx| {
chat_panel
.select_channel(
channel_id,
reply_to_message_id.into(),
cx,
)
.detach_and_log_err(cx)
},
))
}),
),
)
.child(
@@ -1158,7 +1195,13 @@ mod tests {
edited_at: None,
};
let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message);
let message = ChatPanel::render_markdown_with_mentions(
&language_registry,
102,
&message,
UtcOffset::UTC,
cx,
);
// Note that the "'" was replaced with due to smart punctuation.
let (body, ranges) = marked_text_ranges("«hi», «@abc», lets «call» «@fgh»", false);
@@ -1206,7 +1249,13 @@ mod tests {
edited_at: None,
};
let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message);
let message = ChatPanel::render_markdown_with_mentions(
&language_registry,
102,
&message,
UtcOffset::UTC,
cx,
);
// Note that the "'" was replaced with due to smart punctuation.
let (body, ranges) =
@@ -1247,7 +1296,13 @@ mod tests {
edited_at: None,
};
let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message);
let message = ChatPanel::render_markdown_with_mentions(
&language_registry,
102,
&message,
UtcOffset::UTC,
cx,
);
// Note that the "'" was replaced with due to smart punctuation.
let (body, ranges) = marked_text_ranges(

View File

@@ -1,17 +1,20 @@
mod channel_modal;
mod contact_finder;
mod dev_server_modal;
use self::channel_modal::ChannelModal;
use self::dev_server_modal::DevServerModal;
use crate::{
channel_view::ChannelView, chat_panel::ChatPanel, face_pile::FacePile,
CollaborationPanelSettings,
};
use call::ActiveCall;
use channel::{Channel, ChannelEvent, ChannelStore};
use channel::{Channel, ChannelEvent, ChannelStore, RemoteProject};
use client::{ChannelId, Client, Contact, ProjectId, User, UserStore};
use contact_finder::ContactFinder;
use db::kvp::KEY_VALUE_STORE;
use editor::{Editor, EditorElement, EditorStyle};
use feature_flags::{self, FeatureFlagAppExt};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
actions, anchored, canvas, deferred, div, fill, list, point, prelude::*, px, AnyElement,
@@ -24,7 +27,7 @@ use gpui::{
use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrev};
use project::{Fs, Project};
use rpc::{
proto::{self, ChannelVisibility, PeerId},
proto::{self, ChannelVisibility, DevServerStatus, PeerId},
ErrorCode, ErrorExt,
};
use serde_derive::{Deserialize, Serialize};
@@ -188,6 +191,7 @@ enum ListEntry {
id: ProjectId,
name: SharedString,
},
RemoteProject(channel::RemoteProject),
Contact {
contact: Arc<Contact>,
calling: bool,
@@ -278,10 +282,23 @@ impl CollabPanel {
.push(cx.observe(&this.user_store, |this, _, cx| {
this.update_entries(true, cx)
}));
this.subscriptions
.push(cx.observe(&this.channel_store, |this, _, cx| {
let mut has_opened = false;
this.subscriptions.push(cx.observe(
&this.channel_store,
move |this, channel_store, cx| {
if !has_opened {
if !channel_store
.read(cx)
.dev_servers_for_id(ChannelId(1))
.is_empty()
{
this.manage_remote_projects(ChannelId(1), cx);
has_opened = true;
}
}
this.update_entries(true, cx)
}));
},
));
this.subscriptions
.push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));
this.subscriptions.push(cx.subscribe(
@@ -569,6 +586,7 @@ impl CollabPanel {
}
let hosted_projects = channel_store.projects_for_id(channel.id);
let remote_projects = channel_store.remote_projects_for_id(channel.id);
let has_children = channel_store
.channel_at_index(mat.candidate_id + 1)
.map_or(false, |next_channel| {
@@ -604,7 +622,13 @@ impl CollabPanel {
}
for (name, id) in hosted_projects {
self.entries.push(ListEntry::HostedProject { id, name })
self.entries.push(ListEntry::HostedProject { id, name });
}
if cx.has_flag::<feature_flags::Remoting>() {
for remote_project in remote_projects {
self.entries.push(ListEntry::RemoteProject(remote_project));
}
}
}
}
@@ -1065,6 +1089,59 @@ impl CollabPanel {
.tooltip(move |cx| Tooltip::text("Open Project", cx))
}
fn render_remote_project(
&self,
remote_project: &RemoteProject,
is_selected: bool,
cx: &mut ViewContext<Self>,
) -> impl IntoElement {
let id = remote_project.id;
let name = remote_project.name.clone();
let maybe_project_id = remote_project.project_id;
let dev_server = self
.channel_store
.read(cx)
.find_dev_server_by_id(remote_project.dev_server_id);
let tooltip_text = SharedString::from(match dev_server {
Some(dev_server) => format!("Open Remote Project ({})", dev_server.name),
None => "Open Remote Project".to_string(),
});
let dev_server_is_online = dev_server.map(|s| s.status) == Some(DevServerStatus::Online);
let dev_server_text_color = if dev_server_is_online {
Color::Default
} else {
Color::Disabled
};
ListItem::new(ElementId::NamedInteger(
"remote-project".into(),
id.0 as usize,
))
.indent_level(2)
.indent_step_size(px(20.))
.selected(is_selected)
.on_click(cx.listener(move |this, _, cx| {
//TODO display error message if dev server is offline
if dev_server_is_online {
if let Some(project_id) = maybe_project_id {
this.join_remote_project(project_id, cx);
}
}
}))
.start_slot(
h_flex()
.relative()
.gap_1()
.child(IconButton::new(0, IconName::FileTree).icon_color(dev_server_text_color)),
)
.child(Label::new(name.clone()).color(dev_server_text_color))
.tooltip(move |cx| Tooltip::text(tooltip_text.clone(), cx))
}
fn has_subchannels(&self, ix: usize) -> bool {
self.entries.get(ix).map_or(false, |entry| {
if let ListEntry::Channel { has_children, .. } = entry {
@@ -1266,11 +1343,24 @@ impl CollabPanel {
}
if self.channel_store.read(cx).is_root_channel(channel_id) {
context_menu = context_menu.separator().entry(
"Manage Members",
None,
cx.handler_for(&this, move |this, cx| this.manage_members(channel_id, cx)),
)
context_menu = context_menu
.separator()
.entry(
"Manage Members",
None,
cx.handler_for(&this, move |this, cx| {
this.manage_members(channel_id, cx)
}),
)
.when(cx.has_flag::<feature_flags::Remoting>(), |context_menu| {
context_menu.entry(
"Manage Remote Projects",
None,
cx.handler_for(&this, move |this, cx| {
this.manage_remote_projects(channel_id, cx)
}),
)
})
} else {
context_menu = context_menu.entry(
"Move this channel",
@@ -1534,6 +1624,11 @@ impl CollabPanel {
} => {
// todo()
}
ListEntry::RemoteProject(project) => {
if let Some(project_id) = project.project_id {
self.join_remote_project(project_id, cx)
}
}
ListEntry::OutgoingRequest(_) => {}
ListEntry::ChannelEditor { .. } => {}
@@ -1706,6 +1801,18 @@ impl CollabPanel {
self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, cx);
}
fn manage_remote_projects(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
let channel_store = self.channel_store.clone();
let Some(workspace) = self.workspace.upgrade() else {
return;
};
workspace.update(cx, |workspace, cx| {
workspace.toggle_modal(cx, |cx| {
DevServerModal::new(channel_store.clone(), channel_id, cx)
});
});
}
fn remove_selected_channel(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
if let Some(channel) = self.selected_channel() {
self.remove_channel(channel.id, cx)
@@ -2006,6 +2113,18 @@ impl CollabPanel {
.detach_and_prompt_err("Failed to join channel", cx, |_, _| None)
}
fn join_remote_project(&mut self, project_id: ProjectId, cx: &mut ViewContext<Self>) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let app_state = workspace.read(cx).app_state().clone();
workspace::join_remote_project(project_id, app_state, cx).detach_and_prompt_err(
"Failed to join project",
cx,
|_, _| None,
)
}
fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
let Some(workspace) = self.workspace.upgrade() else {
return;
@@ -2141,6 +2260,9 @@ impl CollabPanel {
ListEntry::HostedProject { id, name } => self
.render_channel_project(*id, name, is_selected, cx)
.into_any_element(),
ListEntry::RemoteProject(remote_project) => self
.render_remote_project(remote_project, is_selected, cx)
.into_any_element(),
}
}
@@ -2519,7 +2641,9 @@ impl CollabPanel {
const FACEPILE_LIMIT: usize = 3;
let participants = self.channel_store.read(cx).channel_participants(channel_id);
let face_pile = if !participants.is_empty() {
let face_pile = if participants.is_empty() {
None
} else {
let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
let result = FacePile::new(
participants
@@ -2540,8 +2664,6 @@ impl CollabPanel {
);
Some(result)
} else {
None
};
let width = self.width.unwrap_or(px(240.));
@@ -2883,6 +3005,11 @@ impl PartialEq for ListEntry {
return id == other_id;
}
}
ListEntry::RemoteProject(project) => {
if let ListEntry::RemoteProject(other) = other {
return project.id == other.id;
}
}
ListEntry::ChannelNotes { channel_id } => {
if let ListEntry::ChannelNotes {
channel_id: other_id,

View File

@@ -0,0 +1,622 @@
use channel::{ChannelStore, DevServer, RemoteProject};
use client::{ChannelId, DevServerId, RemoteProjectId};
use editor::Editor;
use gpui::{
AppContext, ClipboardItem, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
ScrollHandle, Task, View, ViewContext,
};
use rpc::proto::{self, CreateDevServerResponse, DevServerStatus};
use ui::{prelude::*, Indicator, List, ListHeader, ModalContent, ModalHeader, Tooltip};
use util::ResultExt;
use workspace::ModalView;
pub struct DevServerModal {
mode: Mode,
focus_handle: FocusHandle,
scroll_handle: ScrollHandle,
channel_store: Model<ChannelStore>,
channel_id: ChannelId,
remote_project_name_editor: View<Editor>,
remote_project_path_editor: View<Editor>,
dev_server_name_editor: View<Editor>,
_subscriptions: [gpui::Subscription; 2],
}
#[derive(Default)]
struct CreateDevServer {
creating: Option<Task<()>>,
dev_server: Option<CreateDevServerResponse>,
}
struct CreateRemoteProject {
dev_server_id: DevServerId,
creating: Option<Task<()>>,
remote_project: Option<proto::RemoteProject>,
}
enum Mode {
Default,
CreateRemoteProject(CreateRemoteProject),
CreateDevServer(CreateDevServer),
}
impl DevServerModal {
pub fn new(
channel_store: Model<ChannelStore>,
channel_id: ChannelId,
cx: &mut ViewContext<Self>,
) -> Self {
let name_editor = cx.new_view(|cx| Editor::single_line(cx));
let path_editor = cx.new_view(|cx| Editor::single_line(cx));
let dev_server_name_editor = cx.new_view(|cx| {
let mut editor = Editor::single_line(cx);
editor.set_placeholder_text("Dev server name", cx);
editor
});
let focus_handle = cx.focus_handle();
let subscriptions = [
cx.observe(&channel_store, |_, _, cx| {
cx.notify();
}),
cx.on_focus_out(&focus_handle, |_, _cx| { /* cx.emit(DismissEvent) */ }),
];
Self {
mode: Mode::Default,
focus_handle,
scroll_handle: ScrollHandle::new(),
channel_store,
channel_id,
remote_project_name_editor: name_editor,
remote_project_path_editor: path_editor,
dev_server_name_editor,
_subscriptions: subscriptions,
}
}
pub fn create_remote_project(
&mut self,
dev_server_id: DevServerId,
cx: &mut ViewContext<Self>,
) {
let channel_id = self.channel_id;
let name = self
.remote_project_name_editor
.read(cx)
.text(cx)
.trim()
.to_string();
let path = self
.remote_project_path_editor
.read(cx)
.text(cx)
.trim()
.to_string();
if name == "" {
return;
}
if path == "" {
return;
}
let create = self.channel_store.update(cx, |store, cx| {
store.create_remote_project(channel_id, dev_server_id, name, path, cx)
});
let task = cx.spawn(|this, mut cx| async move {
let result = create.await;
if let Err(e) = &result {
cx.prompt(
gpui::PromptLevel::Critical,
"Failed to create project",
Some(&format!("{:?}. Please try again.", e)),
&["Ok"],
)
.await
.log_err();
}
this.update(&mut cx, |this, _| {
this.mode = Mode::CreateRemoteProject(CreateRemoteProject {
dev_server_id,
creating: None,
remote_project: result.ok().and_then(|r| r.remote_project),
});
})
.log_err();
});
self.mode = Mode::CreateRemoteProject(CreateRemoteProject {
dev_server_id,
creating: Some(task),
remote_project: None,
});
}
pub fn create_dev_server(&mut self, cx: &mut ViewContext<Self>) {
let name = self
.dev_server_name_editor
.read(cx)
.text(cx)
.trim()
.to_string();
if name == "" {
return;
}
let dev_server = self.channel_store.update(cx, |store, cx| {
store.create_dev_server(self.channel_id, name.clone(), cx)
});
let task = cx.spawn(|this, mut cx| async move {
match dev_server.await {
Ok(dev_server) => {
this.update(&mut cx, |this, _| {
this.mode = Mode::CreateDevServer(CreateDevServer {
creating: None,
dev_server: Some(dev_server),
});
})
.log_err();
}
Err(e) => {
cx.prompt(
gpui::PromptLevel::Critical,
"Failed to create server",
Some(&format!("{:?}. Please try again.", e)),
&["Ok"],
)
.await
.log_err();
this.update(&mut cx, |this, _| {
this.mode = Mode::CreateDevServer(Default::default());
})
.log_err();
}
}
});
self.mode = Mode::CreateDevServer(CreateDevServer {
creating: Some(task),
dev_server: None,
});
cx.notify()
}
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
match self.mode {
Mode::Default => cx.emit(DismissEvent),
Mode::CreateRemoteProject(_) | Mode::CreateDevServer(_) => {
self.mode = Mode::Default;
cx.notify();
}
}
}
fn render_dev_server(
&mut self,
dev_server: &DevServer,
cx: &mut ViewContext<Self>,
) -> impl IntoElement {
let channel_store = self.channel_store.read(cx);
let dev_server_id = dev_server.id;
let status = dev_server.status;
v_flex()
.w_full()
.child(
h_flex()
.group("dev-server")
.justify_between()
.child(
h_flex()
.gap_2()
.child(
div()
.id(("status", dev_server.id.0))
.relative()
.child(Icon::new(IconName::Server).size(IconSize::Small))
.child(
div().absolute().bottom_0().left(rems_from_px(8.0)).child(
Indicator::dot().color(match status {
DevServerStatus::Online => Color::Created,
DevServerStatus::Offline => Color::Deleted,
}),
),
)
.tooltip(move |cx| {
Tooltip::text(
match status {
DevServerStatus::Online => "Online",
DevServerStatus::Offline => "Offline",
},
cx,
)
}),
)
.child(dev_server.name.clone())
.child(
h_flex()
.visible_on_hover("dev-server")
.gap_1()
.child(
IconButton::new("edit-dev-server", IconName::Pencil)
.disabled(true) //TODO implement this on the collab side
.tooltip(|cx| {
Tooltip::text("Coming Soon - Edit dev server", cx)
}),
)
.child(
IconButton::new("remove-dev-server", IconName::Trash)
.disabled(true) //TODO implement this on the collab side
.tooltip(|cx| {
Tooltip::text("Coming Soon - Remove dev server", cx)
}),
),
),
)
.child(
h_flex().gap_1().child(
IconButton::new("add-remote-project", IconName::Plus)
.tooltip(|cx| Tooltip::text("Add a remote project", cx))
.on_click(cx.listener(move |this, _, cx| {
this.mode = Mode::CreateRemoteProject(CreateRemoteProject {
dev_server_id,
creating: None,
remote_project: None,
});
cx.notify();
})),
),
),
)
.child(
v_flex()
.w_full()
.bg(cx.theme().colors().title_bar_background)
.border()
.border_color(cx.theme().colors().border_variant)
.rounded_md()
.my_1()
.py_0p5()
.px_3()
.child(
List::new().empty_message("No projects.").children(
channel_store
.remote_projects_for_id(dev_server.channel_id)
.iter()
.filter_map(|remote_project| {
if remote_project.dev_server_id == dev_server.id {
Some(self.render_remote_project(remote_project, cx))
} else {
None
}
}),
),
),
)
// .child(div().ml_8().child(
// Button::new(("add-project", dev_server_id.0), "Add Project").on_click(cx.listener(
// move |this, _, cx| {
// this.mode = Mode::CreateRemoteProject(CreateRemoteProject {
// dev_server_id,
// creating: None,
// remote_project: None,
// });
// cx.notify();
// },
// )),
// ))
}
fn render_remote_project(
&mut self,
project: &RemoteProject,
_: &mut ViewContext<Self>,
) -> impl IntoElement {
h_flex()
.gap_2()
.child(Icon::new(IconName::FileTree))
.child(Label::new(project.name.clone()))
.child(Label::new(format!("({})", project.path.clone())).color(Color::Muted))
}
fn render_create_dev_server(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let Mode::CreateDevServer(CreateDevServer {
creating,
dev_server,
}) = &self.mode
else {
unreachable!()
};
self.dev_server_name_editor.update(cx, |editor, _| {
editor.set_read_only(creating.is_some() || dev_server.is_some())
});
v_flex()
.px_1()
.pt_0p5()
.gap_px()
.child(
v_flex().py_0p5().px_1().child(
h_flex()
.px_1()
.py_0p5()
.child(
IconButton::new("back", IconName::ArrowLeft)
.style(ButtonStyle::Transparent)
.on_click(cx.listener(|this, _: &gpui::ClickEvent, cx| {
this.mode = Mode::Default;
cx.notify();
})),
)
.child(Headline::new("Register dev server")),
),
)
.child(
h_flex()
.ml_5()
.gap_2()
.child("Name")
.child(self.dev_server_name_editor.clone())
.on_action(
cx.listener(|this, _: &menu::Confirm, cx| this.create_dev_server(cx)),
)
.when(creating.is_none() && dev_server.is_none(), |div| {
div.child(
Button::new("create-dev-server", "Create").on_click(cx.listener(
move |this, _, cx| {
this.create_dev_server(cx);
},
)),
)
})
.when(creating.is_some() && dev_server.is_none(), |div| {
div.child(Button::new("create-dev-server", "Creating...").disabled(true))
}),
)
.when_some(dev_server.clone(), |div, dev_server| {
let channel_store = self.channel_store.read(cx);
let status = channel_store
.find_dev_server_by_id(DevServerId(dev_server.dev_server_id))
.map(|server| server.status)
.unwrap_or(DevServerStatus::Offline);
let instructions = SharedString::from(format!(
"zed --dev-server-token {}",
dev_server.access_token
));
div.child(
v_flex()
.ml_8()
.gap_2()
.child(Label::new(format!(
"Please log into `{}` and run:",
dev_server.name
)))
.child(instructions.clone())
.child(
IconButton::new("copy-access-token", IconName::Copy)
.on_click(cx.listener(move |_, _, cx| {
cx.write_to_clipboard(ClipboardItem::new(
instructions.to_string(),
))
}))
.icon_size(IconSize::Small)
.tooltip(|cx| Tooltip::text("Copy access token", cx)),
)
.when(status == DevServerStatus::Offline, |this| {
this.child(Label::new("Waiting for connection..."))
})
.when(status == DevServerStatus::Online, |this| {
this.child(Label::new("Connection established! 🎊")).child(
Button::new("done", "Done").on_click(cx.listener(|this, _, cx| {
this.mode = Mode::Default;
cx.notify();
})),
)
}),
)
})
}
fn render_default(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let channel_store = self.channel_store.read(cx);
let dev_servers = channel_store.dev_servers_for_id(self.channel_id);
// let dev_servers = Vec::new();
v_flex()
.id("scroll-container")
.h_full()
.overflow_y_scroll()
.track_scroll(&self.scroll_handle)
.px_1()
.pt_0p5()
.gap_px()
.child(
ModalHeader::new("Manage Remote Project")
.child(Headline::new("Remote Projects").size(HeadlineSize::Small)),
)
.child(
ModalContent::new().child(
List::new()
.empty_message("No dev servers registered.")
.header(Some(
ListHeader::new("Dev Servers").end_slot(
Button::new("register-dev-server-button", "New Server")
.icon(IconName::Plus)
.icon_position(IconPosition::Start)
.tooltip(|cx| Tooltip::text("Register a new dev server", cx))
.on_click(cx.listener(|this, _, cx| {
this.mode = Mode::CreateDevServer(Default::default());
this.dev_server_name_editor
.read(cx)
.focus_handle(cx)
.focus(cx);
cx.notify();
})),
),
))
.children(dev_servers.iter().map(|dev_server| {
self.render_dev_server(dev_server, cx).into_any_element()
})),
),
)
}
fn render_create_project(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let Mode::CreateRemoteProject(CreateRemoteProject {
dev_server_id,
creating,
remote_project,
}) = &self.mode
else {
unreachable!()
};
let channel_store = self.channel_store.read(cx);
let (dev_server_name, dev_server_status) = channel_store
.find_dev_server_by_id(*dev_server_id)
.map(|server| (server.name.clone(), server.status))
.unwrap_or((SharedString::from(""), DevServerStatus::Offline));
v_flex()
.px_1()
.pt_0p5()
.gap_px()
.child(
ModalHeader::new("Manage Remote Project")
.child(Headline::new("Manage Remote Projects")),
)
.child(
h_flex()
.py_0p5()
.px_1()
.child(div().px_1().py_0p5().child(
IconButton::new("back", IconName::ArrowLeft).on_click(cx.listener(
|this, _, cx| {
this.mode = Mode::Default;
cx.notify()
},
)),
))
.child("Add Project..."),
)
.child(
h_flex()
.ml_5()
.gap_2()
.child(
div()
.id(("status", dev_server_id.0))
.relative()
.child(Icon::new(IconName::Server))
.child(div().absolute().bottom_0().left(rems_from_px(12.0)).child(
Indicator::dot().color(match dev_server_status {
DevServerStatus::Online => Color::Created,
DevServerStatus::Offline => Color::Deleted,
}),
))
.tooltip(move |cx| {
Tooltip::text(
match dev_server_status {
DevServerStatus::Online => "Online",
DevServerStatus::Offline => "Offline",
},
cx,
)
}),
)
.child(dev_server_name.clone()),
)
.child(
h_flex()
.ml_5()
.gap_2()
.child("Name")
.child(self.remote_project_name_editor.clone())
.on_action(cx.listener(|this, _: &menu::Confirm, cx| {
cx.focus_view(&this.remote_project_path_editor)
})),
)
.child(
h_flex()
.ml_5()
.gap_2()
.child("Path")
.child(self.remote_project_path_editor.clone())
.on_action(
cx.listener(|this, _: &menu::Confirm, cx| this.create_dev_server(cx)),
)
.when(creating.is_none() && remote_project.is_none(), |div| {
div.child(Button::new("create-remote-server", "Create").on_click({
let dev_server_id = *dev_server_id;
cx.listener(move |this, _, cx| {
this.create_remote_project(dev_server_id, cx)
})
}))
})
.when(creating.is_some(), |div| {
div.child(Button::new("create-dev-server", "Creating...").disabled(true))
}),
)
.when_some(remote_project.clone(), |div, remote_project| {
let channel_store = self.channel_store.read(cx);
let status = channel_store
.find_remote_project_by_id(RemoteProjectId(remote_project.id))
.map(|project| {
if project.project_id.is_some() {
DevServerStatus::Online
} else {
DevServerStatus::Offline
}
})
.unwrap_or(DevServerStatus::Offline);
div.child(
v_flex()
.ml_5()
.ml_8()
.gap_2()
.when(status == DevServerStatus::Offline, |this| {
this.child(Label::new("Waiting for project..."))
})
.when(status == DevServerStatus::Online, |this| {
this.child(Label::new("Project online! 🎊")).child(
Button::new("done", "Done").on_click(cx.listener(|this, _, cx| {
this.mode = Mode::Default;
cx.notify();
})),
)
}),
)
})
}
}
impl ModalView for DevServerModal {}
impl FocusableView for DevServerModal {
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
impl EventEmitter<DismissEvent> for DevServerModal {}
impl Render for DevServerModal {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.track_focus(&self.focus_handle)
.elevation_3(cx)
.key_context("DevServerModal")
.on_action(cx.listener(Self::cancel))
.pb_4()
.w(rems(34.))
.min_h(rems(20.))
.max_h(rems(40.))
.child(match &self.mode {
Mode::Default => self.render_default(cx).into_any_element(),
Mode::CreateRemoteProject(_) => self.render_create_project(cx).into_any_element(),
Mode::CreateDevServer(_) => self.render_create_dev_server(cx).into_any_element(),
})
}
}

View File

@@ -21,6 +21,7 @@ use std::{sync::Arc, time::Duration};
use time::{OffsetDateTime, UtcOffset};
use ui::{h_flex, prelude::*, v_flex, Avatar, Button, Icon, IconButton, IconName, Label, Tooltip};
use util::{ResultExt, TryFutureExt};
use workspace::notifications::NotificationId;
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
Workspace,
@@ -534,8 +535,10 @@ impl NotificationPanel {
self.workspace
.update(cx, |workspace, cx| {
workspace.dismiss_notification::<NotificationToast>(0, cx);
workspace.show_notification(0, cx, |cx| {
let id = NotificationId::unique::<NotificationToast>();
workspace.dismiss_notification(&id, cx);
workspace.show_notification(id, cx, |cx| {
let workspace = cx.view().downgrade();
cx.new_view(|_| NotificationToast {
notification_id,
@@ -554,7 +557,8 @@ impl NotificationPanel {
self.current_notification_toast.take();
self.workspace
.update(cx, |workspace, cx| {
workspace.dismiss_notification::<NotificationToast>(0, cx)
let id = NotificationId::unique::<NotificationToast>();
workspace.dismiss_notification(&id, cx)
})
.ok();
}

View File

@@ -1255,7 +1255,6 @@ mod tests {
&self,
_: BufferId,
_: &clock::Global,
_: language::RopeFingerprint,
_: language::LineEnding,
_: Option<std::time::SystemTime>,
_: &mut AppContext,

View File

@@ -14,6 +14,7 @@ use language::{
use settings::{update_settings_file, Settings, SettingsStore};
use std::{path::Path, sync::Arc};
use util::{paths, ResultExt};
use workspace::notifications::NotificationId;
use workspace::{
create_and_open_local_file,
item::ItemHandle,
@@ -25,8 +26,10 @@ use workspace::{
use zed_actions::OpenBrowser;
const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
const COPILOT_STARTING_TOAST_ID: usize = 1337;
const COPILOT_ERROR_TOAST_ID: usize = 1338;
struct CopilotStartingToast;
struct CopilotErrorToast;
pub struct CopilotButton {
editor_subscription: Option<(Subscription, usize)>,
@@ -74,7 +77,7 @@ impl Render for CopilotButton {
.update(cx, |workspace, cx| {
workspace.show_toast(
Toast::new(
COPILOT_ERROR_TOAST_ID,
NotificationId::unique::<CopilotErrorToast>(),
format!("Copilot can't be started: {}", e),
)
.on_click(
@@ -350,7 +353,10 @@ pub fn initiate_sign_in(cx: &mut WindowContext) {
let Ok(workspace) = workspace.update(cx, |workspace, cx| {
workspace.show_toast(
Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot is starting..."),
Toast::new(
NotificationId::unique::<CopilotStartingToast>(),
"Copilot is starting...",
),
cx,
);
workspace.weak_handle()
@@ -364,11 +370,17 @@ pub fn initiate_sign_in(cx: &mut WindowContext) {
workspace
.update(&mut cx, |workspace, cx| match copilot.read(cx).status() {
Status::Authorized => workspace.show_toast(
Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot has started!"),
Toast::new(
NotificationId::unique::<CopilotStartingToast>(),
"Copilot has started!",
),
cx,
),
_ => {
workspace.dismiss_toast(COPILOT_STARTING_TOAST_ID, cx);
workspace.dismiss_toast(
&NotificationId::unique::<CopilotStartingToast>(),
cx,
);
copilot
.update(cx, |copilot, cx| copilot.sign_in(cx))
.detach_and_log_err(cx);

View File

@@ -38,7 +38,7 @@ pub use toolbar_controls::ToolbarControls;
use ui::{h_flex, prelude::*, Icon, IconName, Label};
use util::TryFutureExt;
use workspace::{
item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},
ItemNavHistory, Pane, ToolbarItemLocation, Workspace,
};
@@ -645,10 +645,10 @@ impl Item for ProjectDiagnosticsEditor {
Some("Project Diagnostics".into())
}
fn tab_content(&self, _detail: Option<usize>, selected: bool, _: &WindowContext) -> AnyElement {
fn tab_content(&self, params: TabContentParams, _: &WindowContext) -> AnyElement {
if self.summary.error_count == 0 && self.summary.warning_count == 0 {
Label::new("No problems")
.color(if selected {
.color(if params.selected {
Color::Default
} else {
Color::Muted
@@ -663,7 +663,7 @@ impl Item for ProjectDiagnosticsEditor {
.gap_1()
.child(Icon::new(IconName::XCircle).color(Color::Error))
.child(Label::new(self.summary.error_count.to_string()).color(
if selected {
if params.selected {
Color::Default
} else {
Color::Muted
@@ -677,7 +677,7 @@ impl Item for ProjectDiagnosticsEditor {
.gap_1()
.child(Icon::new(IconName::ExclamationTriangle).color(Color::Warning))
.child(Label::new(self.summary.warning_count.to_string()).color(
if selected {
if params.selected {
Color::Default
} else {
Color::Muted

View File

@@ -233,11 +233,11 @@ impl FoldMap {
}
pub fn set_ellipses_color(&mut self, color: Hsla) -> bool {
if self.ellipses_color != Some(color) {
if self.ellipses_color == Some(color) {
false
} else {
self.ellipses_color = Some(color);
true
} else {
false
}
}

View File

@@ -126,12 +126,12 @@ impl WrapMap {
) -> bool {
let font_with_size = (font, font_size);
if font_with_size != self.font_with_size {
if font_with_size == self.font_with_size {
false
} else {
self.font_with_size = font_with_size;
self.rewrap(cx);
true
} else {
false
}
}

View File

@@ -129,6 +129,8 @@ use ui::{
Tooltip,
};
use util::{defer, maybe, post_inc, RangeExt, ResultExt, TryFutureExt};
use workspace::item::ItemHandle;
use workspace::notifications::NotificationId;
use workspace::Toast;
use workspace::{
searchable::SearchEvent, ItemNavHistory, SplitDirection, ViewId, Workspace, WorkspaceId,
@@ -470,6 +472,8 @@ pub struct Editor {
+ Fn(&mut Self, DisplayPoint, &mut ViewContext<Self>) -> Option<View<ui::ContextMenu>>,
>,
>,
last_bounds: Option<Bounds<Pixels>>,
expect_bounds_change: Option<Bounds<Pixels>>,
}
#[derive(Clone)]
@@ -1484,6 +1488,8 @@ impl Editor {
inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
gutter_hovered: false,
pixel_position_of_newest_cursor: None,
last_bounds: None,
expect_bounds_change: None,
gutter_width: Default::default(),
style: None,
show_cursor_names: false,
@@ -7997,11 +8003,16 @@ impl Editor {
cx,
);
});
let item = Box::new(editor);
if split {
workspace.split_item(SplitDirection::Right, Box::new(editor), cx);
workspace.split_item(SplitDirection::Right, item.clone(), cx);
} else {
workspace.add_item_to_active_pane(Box::new(editor), cx);
workspace.add_item_to_active_pane(item.clone(), cx);
}
workspace.active_pane().clone().update(cx, |pane, cx| {
let item_id = item.item_id();
pane.set_preview_item_id(Some(item_id), cx);
});
}
pub fn rename(&mut self, _: &Rename, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
@@ -8832,16 +8843,16 @@ impl Editor {
}
pub fn toggle_git_blame(&mut self, _: &ToggleGitBlame, cx: &mut ViewContext<Self>) {
if !self.show_git_blame {
if self.show_git_blame {
self.blame_subscription.take();
self.blame.take();
self.show_git_blame = false
} else {
if let Err(error) = self.show_git_blame_internal(cx) {
log::error!("failed to toggle on 'git blame': {}", error);
return;
}
self.show_git_blame = true
} else {
self.blame_subscription.take();
self.blame.take();
self.show_git_blame = false
}
cx.notify();
@@ -8922,7 +8933,12 @@ impl Editor {
if let Some(workspace) = self.workspace() {
workspace.update(cx, |workspace, cx| {
workspace.show_toast(Toast::new(0x156a5f9ee, message), cx)
struct CopyPermalinkToLine;
workspace.show_toast(
Toast::new(NotificationId::unique::<CopyPermalinkToLine>(), message),
cx,
)
})
}
}
@@ -8943,7 +8959,12 @@ impl Editor {
if let Some(workspace) = self.workspace() {
workspace.update(cx, |workspace, cx| {
workspace.show_toast(Toast::new(0x45a8978, message), cx)
struct OpenPermalinkToLine;
workspace.show_toast(
Toast::new(NotificationId::unique::<OpenPermalinkToLine>(), message),
cx,
)
})
}
}
@@ -9506,7 +9527,11 @@ impl Editor {
cx.spawn(|_, mut cx| async move {
let workspace = workspace.ok_or_else(|| anyhow!("cannot jump without workspace"))?;
let editor = workspace.update(&mut cx, |workspace, cx| {
workspace.open_path(path, None, true, cx)
// Reset the preview item id before opening the new item
workspace.active_pane().update(cx, |pane, cx| {
pane.set_preview_item_id(None, cx);
});
workspace.open_path_preview(path, None, true, true, cx)
})?;
let editor = editor
.await?
@@ -10568,11 +10593,6 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren
let mut text_style = cx.text_style().clone();
text_style.color = diagnostic_style(diagnostic.severity, true, cx.theme().status());
let theme_settings = ThemeSettings::get_global(cx);
text_style.font_family = theme_settings.buffer_font.family.clone();
text_style.font_style = theme_settings.buffer_font.style;
text_style.font_features = theme_settings.buffer_font.features;
text_style.font_weight = theme_settings.buffer_font.weight;
let multi_line_diagnostic = diagnostic.message.contains('\n');

View File

@@ -2969,7 +2969,7 @@ fn render_blame_entry(
cx.open_url(url.as_str())
})
})
.tooltip(move |cx| {
.hoverable_tooltip(move |cx| {
BlameEntryTooltip::new(
sha_color.cursor,
commit_message.clone(),
@@ -3371,6 +3371,7 @@ impl Element for EditorElement {
let overscroll = size(em_width, px(0.));
snapshot = self.editor.update(cx, |editor, cx| {
editor.last_bounds = Some(bounds);
editor.gutter_width = gutter_dimensions.width;
editor.set_visible_line_count(bounds.size.height / line_height, cx);
@@ -3419,7 +3420,7 @@ impl Element for EditorElement {
let autoscroll_horizontally = self.editor.update(cx, |editor, cx| {
let autoscroll_horizontally =
editor.autoscroll_vertically(bounds.size.height, line_height, cx);
editor.autoscroll_vertically(bounds, line_height, cx);
snapshot = editor.snapshot(cx);
autoscroll_horizontally
});

View File

@@ -256,7 +256,7 @@ impl GitBlame {
let blame = self.project.read(cx).blame_buffer(&self.buffer, None, cx);
self.task = cx.spawn(|this, mut cx| async move {
let (entries, permalinks, messages) = cx
let result = cx
.background_executor()
.spawn({
let snapshot = snapshot.clone();
@@ -304,16 +304,23 @@ impl GitBlame {
anyhow::Ok((entries, permalinks, messages))
}
})
.await?;
.await;
this.update(&mut cx, |this, cx| {
this.buffer_edits = buffer_edits;
this.buffer_snapshot = snapshot;
this.entries = entries;
this.permalinks = permalinks;
this.messages = messages;
this.generated = true;
cx.notify();
this.update(&mut cx, |this, cx| match result {
Ok((entries, permalinks, messages)) => {
this.buffer_edits = buffer_edits;
this.buffer_snapshot = snapshot;
this.entries = entries;
this.permalinks = permalinks;
this.messages = messages;
this.generated = true;
cx.notify();
}
Err(error) => this.project.update(cx, |_, cx| {
log::error!("failed to get git blame data: {error:?}");
let notification = format!("{:#}", error).trim().to_string();
cx.emit(project::Event::Notification(notification));
}),
})
});
}
@@ -359,6 +366,54 @@ mod tests {
});
}
#[gpui::test]
async fn test_blame_error_notifications(cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/my-repo",
json!({
".git": {},
"file.txt": r#"
irrelevant contents
"#
.unindent()
}),
)
.await;
// Creating a GitBlame without a corresponding blame state
// will result in an error.
let project = Project::test(fs, ["/my-repo".as_ref()], cx).await;
let buffer = project
.update(cx, |project, cx| {
project.open_local_buffer("/my-repo/file.txt", cx)
})
.await
.unwrap();
let blame = cx.new_model(|cx| GitBlame::new(buffer.clone(), project.clone(), cx));
let event = project.next_event(cx).await;
assert_eq!(
event,
project::Event::Notification(
"Failed to blame \"file.txt\": failed to get blame for \"file.txt\"".to_string()
)
);
blame.update(cx, |blame, cx| {
assert_eq!(
blame
.blame_for_rows((0..1).map(Some), cx)
.collect::<Vec<_>>(),
vec![None]
);
});
}
#[gpui::test]
async fn test_blame_for_rows(cx: &mut gpui::TestAppContext) {
init_test(cx);

View File

@@ -238,7 +238,9 @@ fn show_hover(
let task = cx.spawn(|this, mut cx| {
async move {
// If we need to delay, delay a set amount initially before making the lsp request
let delay = if !ignore_timeout {
let delay = if ignore_timeout {
None
} else {
// Construct delay task to wait for later
let total_delay = Some(
cx.background_executor()
@@ -249,8 +251,6 @@ fn show_hover(
.timer(Duration::from_millis(HOVER_REQUEST_DELAY_MILLIS))
.await;
total_delay
} else {
None
};
// query the LSP for hover info

View File

@@ -19,7 +19,7 @@ use project::repository::GitFileStatus;
use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath};
use rpc::proto::{self, update_view, PeerId};
use settings::Settings;
use workspace::item::ItemSettings;
use workspace::item::{ItemSettings, TabContentParams};
use std::{
borrow::Cow,
@@ -30,7 +30,7 @@ use std::{
sync::Arc,
};
use text::{BufferId, Selection};
use theme::{Theme, ThemeSettings};
use theme::Theme;
use ui::{h_flex, prelude::*, Label};
use util::{paths::PathExt, ResultExt, TryFutureExt};
use workspace::item::{BreadcrumbText, FollowEvent, FollowableItemHandle};
@@ -594,7 +594,7 @@ impl Item for Editor {
Some(path.to_string_lossy().to_string().into())
}
fn tab_content(&self, detail: Option<usize>, selected: bool, cx: &WindowContext) -> AnyElement {
fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement {
let label_color = if ItemSettings::get_global(cx).git_status {
self.buffer()
.read(cx)
@@ -602,14 +602,14 @@ impl Item for Editor {
.and_then(|buffer| buffer.read(cx).project_path(cx))
.and_then(|path| self.project.as_ref()?.read(cx).entry_for_path(&path, cx))
.map(|entry| {
entry_git_aware_label_color(entry.git_status, entry.is_ignored, selected)
entry_git_aware_label_color(entry.git_status, entry.is_ignored, params.selected)
})
.unwrap_or_else(|| entry_label_color(selected))
.unwrap_or_else(|| entry_label_color(params.selected))
} else {
entry_label_color(selected)
entry_label_color(params.selected)
};
let description = detail.and_then(|detail| {
let description = params.detail.and_then(|detail| {
let path = path_for_buffer(&self.buffer, detail, false, cx)?;
let description = path.to_string_lossy();
let description = description.trim();
@@ -623,7 +623,11 @@ impl Item for Editor {
h_flex()
.gap_2()
.child(Label::new(self.title(cx).to_string()).color(label_color))
.child(
Label::new(self.title(cx).to_string())
.color(label_color)
.italic(params.preview),
)
.when_some(description, |this, description| {
this.child(
Label::new(description)
@@ -731,9 +735,8 @@ impl Item for Editor {
buffer
.update(&mut cx, |buffer, cx| {
let version = buffer.saved_version().clone();
let fingerprint = buffer.saved_version_fingerprint();
let mtime = buffer.saved_mtime();
buffer.did_save(version, fingerprint, mtime, cx);
buffer.did_save(version, mtime, cx);
})
.ok();
}
@@ -824,18 +827,13 @@ impl Item for Editor {
.map(|path| path.to_string_lossy().to_string())
.unwrap_or_else(|| "untitled".to_string());
let settings = ThemeSettings::get_global(cx);
let mut breadcrumbs = vec![BreadcrumbText {
text: filename,
highlights: None,
font: Some(settings.buffer_font.clone()),
}];
breadcrumbs.extend(symbols.into_iter().map(|symbol| BreadcrumbText {
text: symbol.text,
highlights: Some(symbol.highlight_ranges),
font: Some(settings.buffer_font.clone()),
}));
Some(breadcrumbs)
}
@@ -1168,6 +1166,10 @@ impl SearchableItem for Editor {
&self.buffer().read(cx).snapshot(cx),
)
}
fn search_bar_visibility_changed(&mut self, _visible: bool, _cx: &mut ViewContext<Self>) {
self.expect_bounds_change = self.last_bounds;
}
}
pub fn active_match_index(

View File

@@ -47,6 +47,12 @@ pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
pub fn saturating_left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
if point.column() > 0 {
*point.column_mut() -= 1;
} else if point.column() == 0 {
// If the current sofr_wrap mode is used, the column corresponding to the display is 0,
// which does not necessarily mean that the actual beginning of a paragraph
if map.display_point_to_fold_point(point, Bias::Left).column() > 0 {
return left(map, point);
}
}
map.clip_point(point, Bias::Left)
}

View File

@@ -45,11 +45,11 @@ impl ScrollAnchor {
pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> gpui::Point<f32> {
let mut scroll_position = self.offset;
if self.anchor != Anchor::min() {
if self.anchor == Anchor::min() {
scroll_position.y = 0.;
} else {
let scroll_top = self.anchor.to_display_point(snapshot).row() as f32;
scroll_position.y = scroll_top + scroll_position.y;
} else {
scroll_position.y = 0.;
}
scroll_position
}

View File

@@ -1,6 +1,6 @@
use std::{cmp, f32};
use gpui::{px, Pixels, ViewContext};
use gpui::{px, Bounds, Pixels, ViewContext};
use language::Point;
use crate::{display_map::ToDisplayPoint, Editor, EditorMode, LineWithInvisibles};
@@ -63,13 +63,23 @@ impl AutoscrollStrategy {
impl Editor {
pub fn autoscroll_vertically(
&mut self,
viewport_height: Pixels,
bounds: Bounds<Pixels>,
line_height: Pixels,
cx: &mut ViewContext<Editor>,
) -> bool {
let viewport_height = bounds.size.height;
let visible_lines = viewport_height / line_height;
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let mut scroll_position = self.scroll_manager.scroll_position(&display_map);
let original_y = scroll_position.y;
if let Some(last_bounds) = self.expect_bounds_change.take() {
if scroll_position.y != 0. {
scroll_position.y += (bounds.top() - last_bounds.top()) / line_height;
if scroll_position.y < 0. {
scroll_position.y = 0.;
}
}
}
let max_scroll_top = if matches!(self.mode, EditorMode::AutoHeight { .. }) {
(display_map.max_point().row() as f32 - visible_lines + 1.).max(0.)
} else {
@@ -77,6 +87,9 @@ impl Editor {
};
if scroll_position.y > max_scroll_top {
scroll_position.y = max_scroll_top;
}
if original_y != scroll_position.y {
self.set_scroll_position(scroll_position, cx);
}

View File

@@ -10,7 +10,7 @@ use gpui::AsyncAppContext;
use language::{
CodeLabel, HighlightId, Language, LanguageServerName, LspAdapter, LspAdapterDelegate,
};
use lsp::LanguageServerBinary;
use lsp::{CodeActionKind, LanguageServerBinary};
use serde::Serialize;
use serde_json::Value;
use std::ops::Range;
@@ -129,6 +129,24 @@ impl LspAdapter for ExtensionLspAdapter {
None
}
fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
if self.extension.manifest.id.as_ref() == "terraform" {
// This is taken from the original Terraform implementation, including
// the TODOs:
// TODO: file issue for server supported code actions
// TODO: reenable default actions / delete override
return Some(vec![]);
}
Some(vec![
CodeActionKind::EMPTY,
CodeActionKind::QUICKFIX,
CodeActionKind::REFACTOR,
CodeActionKind::REFACTOR_EXTRACT,
CodeActionKind::SOURCE,
])
}
fn language_ids(&self) -> HashMap<String, String> {
// TODO: The language IDs can be provided via the language server options
// in `extension.toml now but we're leaving these existing usages in place temporarily
@@ -295,10 +313,10 @@ fn labels_from_wit(
.into_iter()
.map(|label| {
let label = label?;
let runs = if !label.code.is_empty() {
language.highlight_text(&label.code.as_str().into(), 0..label.code.len())
} else {
let runs = if label.code.is_empty() {
Vec::new()
} else {
language.highlight_text(&label.code.as_str().into(), 0..label.code.len())
};
build_code_label(&label, &runs, &language)
})

View File

@@ -612,17 +612,16 @@ impl ExtensionStore {
.and_then(|value| value.to_str().ok()?.parse::<usize>().ok());
let mut body = BufReader::new(response.body_mut());
let mut tgz_bytes = Vec::new();
body.read_to_end(&mut tgz_bytes).await?;
let mut tar_gz_bytes = Vec::new();
body.read_to_end(&mut tar_gz_bytes).await?;
if let Some(content_length) = content_length {
let actual_len = tgz_bytes.len();
let actual_len = tar_gz_bytes.len();
if content_length != actual_len {
bail!("downloaded extension size {actual_len} does not match content length {content_length}");
}
}
let decompressed_bytes = GzipDecoder::new(BufReader::new(tgz_bytes.as_slice()));
// let decompressed_bytes = GzipDecoder::new(BufReader::new(tgz_bytes));
let decompressed_bytes = GzipDecoder::new(BufReader::new(tar_gz_bytes.as_slice()));
let archive = Archive::new(decompressed_bytes);
archive.unpack(extension_dir).await?;
this.update(&mut cx, |this, cx| {

View File

@@ -5,66 +5,78 @@ use std::sync::{Arc, OnceLock};
use db::kvp::KEY_VALUE_STORE;
use editor::Editor;
use extension::ExtensionStore;
use gpui::{Entity, Model, VisualContext};
use gpui::{Model, VisualContext};
use language::Buffer;
use ui::ViewContext;
use workspace::{notifications::simple_message_notification, Workspace};
use ui::{SharedString, ViewContext};
use workspace::{
notifications::{simple_message_notification, NotificationId},
Workspace,
};
const SUGGESTIONS_BY_EXTENSION_ID: &[(&str, &[&str])] = &[
("astro", &["astro"]),
("beancount", &["beancount"]),
("clojure", &["bb", "clj", "cljc", "cljs", "edn"]),
("csharp", &["cs"]),
("dart", &["dart"]),
("dockerfile", &["Dockerfile"]),
("elisp", &["el"]),
("elm", &["elm"]),
("erlang", &["erl", "hrl"]),
("fish", &["fish"]),
(
"git-firefly",
&[
".gitconfig",
".gitignore",
"COMMIT_EDITMSG",
"EDIT_DESCRIPTION",
"MERGE_MSG",
"NOTES_EDITMSG",
"TAG_EDITMSG",
"git-rebase-todo",
],
),
("gleam", &["gleam"]),
("glsl", &["vert", "frag"]),
("graphql", &["gql", "graphql"]),
("haskell", &["hs"]),
("html", &["htm", "html", "shtml"]),
("java", &["java"]),
("kotlin", &["kt"]),
("latex", &["tex"]),
("lua", &["lua"]),
("make", &["Makefile"]),
("nix", &["nix"]),
("ocaml", &["ml", "mli"]),
("php", &["php"]),
("prisma", &["prisma"]),
("purescript", &["purs"]),
("r", &["r", "R"]),
("racket", &["rkt"]),
("sql", &["sql"]),
("scheme", &["scm"]),
("svelte", &["svelte"]),
("swift", &["swift"]),
("templ", &["templ"]),
("terraform", &["tf", "tfvars", "hcl"]),
("toml", &["Cargo.lock", "toml"]),
("wgsl", &["wgsl"]),
("zig", &["zig"]),
];
fn suggested_extensions() -> &'static HashMap<&'static str, Arc<str>> {
static SUGGESTED: OnceLock<HashMap<&str, Arc<str>>> = OnceLock::new();
SUGGESTED.get_or_init(|| {
[
("astro", "astro"),
("beancount", "beancount"),
("clojure", "bb"),
("clojure", "clj"),
("clojure", "cljc"),
("clojure", "cljs"),
("clojure", "edn"),
("csharp", "cs"),
("dart", "dart"),
("dockerfile", "Dockerfile"),
("elisp", "el"),
("erlang", "erl"),
("erlang", "hrl"),
("fish", "fish"),
("git-firefly", ".gitconfig"),
("git-firefly", ".gitignore"),
("git-firefly", "COMMIT_EDITMSG"),
("git-firefly", "EDIT_DESCRIPTION"),
("git-firefly", "MERGE_MSG"),
("git-firefly", "NOTES_EDITMSG"),
("git-firefly", "TAG_EDITMSG"),
("git-firefly", "git-rebase-todo"),
("gleam", "gleam"),
("graphql", "gql"),
("graphql", "graphql"),
("haskell", "hs"),
("html", "htm"),
("html", "html"),
("html", "shtml"),
("java", "java"),
("kotlin", "kt"),
("latex", "tex"),
("make", "Makefile"),
("nix", "nix"),
("php", "php"),
("prisma", "prisma"),
("purescript", "purs"),
("r", "r"),
("r", "R"),
("sql", "sql"),
("svelte", "svelte"),
("swift", "swift"),
("templ", "templ"),
("toml", "Cargo.lock"),
("toml", "toml"),
("wgsl", "wgsl"),
("zig", "zig"),
]
.into_iter()
.map(|(name, file)| (file, name.into()))
.collect()
static SUGGESTIONS_BY_PATH_SUFFIX: OnceLock<HashMap<&str, Arc<str>>> = OnceLock::new();
SUGGESTIONS_BY_PATH_SUFFIX.get_or_init(|| {
SUGGESTIONS_BY_EXTENSION_ID
.into_iter()
.flat_map(|(name, path_suffixes)| {
let name = Arc::<str>::from(*name);
path_suffixes
.into_iter()
.map(move |suffix| (*suffix, name.clone()))
})
.collect()
})
}
@@ -140,7 +152,13 @@ pub(crate) fn suggest(buffer: Model<Buffer>, cx: &mut ViewContext<Workspace>) {
return;
}
workspace.show_notification(buffer.entity_id().as_u64() as usize, cx, |cx| {
struct ExtensionSuggestionNotification;
let notification_id = NotificationId::identified::<ExtensionSuggestionNotification>(
SharedString::from(extension_id.clone()),
);
workspace.show_notification(notification_id, cx, |cx| {
cx.new_view(move |_cx| {
simple_message_notification::MessageNotification::new(format!(
"Do you want to install the recommended '{}' extension for '{}' files?",

View File

@@ -23,6 +23,7 @@ use std::{ops::Range, sync::Arc};
use theme::ThemeSettings;
use ui::{popover_menu, prelude::*, ContextMenu, ToggleButton, Tooltip};
use util::ResultExt as _;
use workspace::item::TabContentParams;
use workspace::{
item::{Item, ItemEvent},
Workspace, WorkspaceId,
@@ -925,9 +926,9 @@ impl FocusableView for ExtensionsPage {
impl Item for ExtensionsPage {
type Event = ItemEvent;
fn tab_content(&self, _: Option<usize>, selected: bool, _: &WindowContext) -> AnyElement {
fn tab_content(&self, params: TabContentParams, _: &WindowContext) -> AnyElement {
Label::new("Extensions")
.color(if selected {
.color(if params.selected {
Color::Default
} else {
Color::Muted

View File

@@ -18,6 +18,11 @@ pub trait FeatureFlag {
const NAME: &'static str;
}
pub struct Remoting {}
impl FeatureFlag for Remoting {
const NAME: &'static str = "remoting";
}
pub trait FeatureFlagViewExt<V: 'static> {
fn observe_flag<T: FeatureFlag, F>(&mut self, callback: F) -> Subscription
where

View File

@@ -17,6 +17,7 @@ use regex::Regex;
use serde_derive::Serialize;
use ui::{prelude::*, Button, ButtonStyle, IconPosition, Tooltip};
use util::{http::HttpClient, ResultExt};
use workspace::notifications::NotificationId;
use workspace::{DismissDecision, ModalView, Toast, Workspace};
use crate::{system_specs::SystemSpecs, GiveFeedback, OpenZedRepo};
@@ -127,11 +128,11 @@ impl FeedbackModal {
let is_local_project = project.read(cx).is_local();
if !is_local_project {
const TOAST_ID: usize = 0xdeadbeef;
struct FeedbackInRemoteProject;
workspace.show_toast(
Toast::new(
TOAST_ID,
NotificationId::unique::<FeedbackInRemoteProject>(),
"You can only submit feedback in your own project.",
),
cx,

View File

@@ -22,6 +22,7 @@ itertools = "0.11"
menu.workspace = true
picker.workspace = true
project.workspace = true
settings.workspace = true
text.workspace = true
theme.workspace = true
ui.workspace = true

View File

@@ -12,6 +12,7 @@ use gpui::{
use itertools::Itertools;
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
use settings::Settings;
use std::{
cmp,
path::{Path, PathBuf},
@@ -23,7 +24,7 @@ use std::{
use text::Point;
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
use util::{paths::PathLikeWithPosition, post_inc, ResultExt};
use workspace::{ModalView, Workspace};
use workspace::{item::PreviewTabsSettings, ModalView, Workspace};
actions!(file_finder, [Toggle, SelectPrev]);
@@ -782,13 +783,24 @@ impl PickerDelegate for FileFinderDelegate {
if let Some(m) = self.matches.get(self.selected_index()) {
if let Some(workspace) = self.workspace.upgrade() {
let open_task = workspace.update(cx, move |workspace, cx| {
let split_or_open = |workspace: &mut Workspace, project_path, cx| {
if secondary {
workspace.split_path(project_path, cx)
} else {
workspace.open_path(project_path, None, true, cx)
}
};
let split_or_open =
|workspace: &mut Workspace,
project_path,
cx: &mut ViewContext<Workspace>| {
let allow_preview =
PreviewTabsSettings::get_global(cx).enable_preview_from_file_finder;
if secondary {
workspace.split_path_preview(project_path, allow_preview, cx)
} else {
workspace.open_path_preview(
project_path,
None,
true,
allow_preview,
cx,
)
}
};
match m {
Match::History(history_match, _) => {
let worktree_id = history_match.project.worktree_id;

View File

@@ -78,6 +78,7 @@ fn run_git_blame(
.arg(path.as_os_str())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| anyhow!("Failed to start git blame process: {}", e))?;

View File

@@ -12,11 +12,7 @@ workspace = true
[features]
default = []
test-support = [
"backtrace",
"collections/test-support",
"util/test-support",
]
test-support = ["backtrace", "collections/test-support", "util/test-support"]
runtime_shaders = []
macos-blade = ["blade-graphics", "blade-macros", "blade-rwh", "bytemuck"]
@@ -54,9 +50,8 @@ profiling.workspace = true
rand.workspace = true
raw-window-handle = "0.6"
refineable.workspace = true
resvg = "0.14"
tiny-skia = "0.5"
usvg = { version = "0.14", features = [] }
resvg = { version = "0.41.0", default-features = false }
usvg = { version = "0.41.0", default-features = false }
schemars.workspace = true
seahash = "4.1"
semantic_version.workspace = true
@@ -71,7 +66,7 @@ taffy = { git = "https://github.com/DioxusLabs/taffy", rev = "1876f72bee5e376023
thiserror.workspace = true
time.workspace = true
util.workspace = true
uuid = { version = "1.1.2", features = ["v4", "v5"] }
uuid.workspace = true
waker-fn = "1.1.0"
[dev-dependencies]

View File

@@ -182,7 +182,9 @@ impl ActionRegistry {
macro_rules! actions {
($namespace:path, [ $($name:ident),* $(,)? ]) => {
$(
/// The `$name` action see [`gpui::actions!`]
#[doc = "The `"]
#[doc = stringify!($name)]
#[doc = "` action, see [`gpui::actions!`]"]
#[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, ::std::fmt::Debug, gpui::private::serde_derive::Deserialize)]
#[serde(crate = "gpui::private::serde")]
pub struct $name;

View File

@@ -1416,8 +1416,8 @@ pub struct AnyTooltip {
/// The view used to display the tooltip
pub view: AnyView,
/// The offset from the cursor to use, relative to the parent view
pub cursor_offset: Point<Pixels>,
/// The absolute position of the mouse when the tooltip was deployed.
pub mouse_position: Point<Pixels>,
}
/// A keystroke event, and potentially the associated action

View File

@@ -1,10 +1,12 @@
use crate::{
AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, BorrowAppContext, Context,
DismissEvent, FocusableView, ForegroundExecutor, Global, Model, ModelContext, Render,
Reservation, Result, Task, View, ViewContext, VisualContext, WindowContext, WindowHandle,
DismissEvent, FocusableView, ForegroundExecutor, Global, Model, ModelContext, PromptLevel,
Render, Reservation, Result, Task, View, ViewContext, VisualContext, WindowContext,
WindowHandle,
};
use anyhow::{anyhow, Context as _};
use derive_more::{Deref, DerefMut};
use futures::channel::oneshot;
use std::{future::Future, rc::Weak};
/// An async-friendly version of [AppContext] with a static lifetime so it can be held across `await` points in async code.
@@ -285,6 +287,21 @@ impl AsyncWindowContext {
{
self.foreground_executor.spawn(f(self.clone()))
}
/// Present a platform dialog.
/// The provided message will be presented, along with buttons for each answer.
/// When a button is clicked, the returned Receiver will receive the index of the clicked button.
pub fn prompt(
&mut self,
level: PromptLevel,
message: &str,
detail: Option<&str>,
answers: &[&str],
) -> oneshot::Receiver<usize> {
self.window
.update(self, |_, cx| cx.prompt(level, message, detail, answers))
.unwrap_or_else(|_| oneshot::channel().1)
}
}
impl Context for AsyncWindowContext {

View File

@@ -7,7 +7,7 @@ use crate::{
TextSystem, View, ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions,
};
use anyhow::{anyhow, bail};
use futures::{Stream, StreamExt};
use futures::{channel::oneshot, Stream, StreamExt};
use std::{cell::RefCell, future::Future, ops::Deref, rc::Rc, sync::Arc, time::Duration};
/// A TestAppContext is provided to tests created with `#[gpui::test]`, it provides
@@ -479,31 +479,26 @@ impl TestAppContext {
impl<T: 'static> Model<T> {
/// Block until the next event is emitted by the model, then return it.
pub fn next_event<Evt>(&self, cx: &mut TestAppContext) -> Evt
pub fn next_event<Event>(&self, cx: &mut TestAppContext) -> impl Future<Output = Event>
where
Evt: Send + Clone + 'static,
T: EventEmitter<Evt>,
Event: Send + Clone + 'static,
T: EventEmitter<Event>,
{
let (tx, mut rx) = futures::channel::mpsc::unbounded();
let _subscription = self.update(cx, |_, cx| {
let (tx, mut rx) = oneshot::channel();
let mut tx = Some(tx);
let subscription = self.update(cx, |_, cx| {
cx.subscribe(self, move |_, _, event, _| {
tx.unbounded_send(event.clone()).ok();
if let Some(tx) = tx.take() {
_ = tx.send(event.clone());
}
})
});
// Run other tasks until the event is emitted.
loop {
match rx.try_next() {
Ok(Some(event)) => return event,
Ok(None) => panic!("model was dropped"),
Err(_) => {
if !cx.executor().tick() {
break;
}
}
}
async move {
let event = rx.await.expect("no event emitted");
drop(subscription);
event
}
panic!("no event received")
}
/// Returns a future that resolves when the model notifies.

View File

@@ -21,7 +21,7 @@ use crate::{
HitboxId, IntoElement, IsZero, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId,
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
ParentElement, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style,
StyleRefinement, Styled, Task, View, Visibility, WindowContext,
StyleRefinement, Styled, Task, TooltipId, View, Visibility, WindowContext,
};
use collections::HashMap;
use refineable::Refineable;
@@ -483,7 +483,29 @@ impl Interactivity {
self.tooltip_builder.is_none(),
"calling tooltip more than once on the same element is not supported"
);
self.tooltip_builder = Some(Rc::new(build_tooltip));
self.tooltip_builder = Some(TooltipBuilder {
build: Rc::new(build_tooltip),
hoverable: false,
});
}
/// Use the given callback to construct a new tooltip view when the mouse hovers over this element.
/// The tooltip itself is also hoverable and won't disappear when the user moves the mouse into
/// the tooltip. The imperative API equivalent to [`InteractiveElement::hoverable_tooltip`]
pub fn hoverable_tooltip(
&mut self,
build_tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static,
) where
Self: Sized,
{
debug_assert!(
self.tooltip_builder.is_none(),
"calling tooltip more than once on the same element is not supported"
);
self.tooltip_builder = Some(TooltipBuilder {
build: Rc::new(build_tooltip),
hoverable: true,
});
}
/// Block the mouse from interacting with this element or any of its children
@@ -973,6 +995,20 @@ pub trait StatefulInteractiveElement: InteractiveElement {
self.interactivity().tooltip(build_tooltip);
self
}
/// Use the given callback to construct a new tooltip view when the mouse hovers over this element.
/// The tooltip itself is also hoverable and won't disappear when the user moves the mouse into
/// the tooltip. The fluent API equivalent to [`Interactivity::hoverable_tooltip`]
fn hoverable_tooltip(
mut self,
build_tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static,
) -> Self
where
Self: Sized,
{
self.interactivity().hoverable_tooltip(build_tooltip);
self
}
}
/// A trait for providing focus related APIs to interactive elements
@@ -1015,7 +1051,10 @@ type DropListener = Box<dyn Fn(&dyn Any, &mut WindowContext) + 'static>;
type CanDropPredicate = Box<dyn Fn(&dyn Any, &mut WindowContext) -> bool + 'static>;
pub(crate) type TooltipBuilder = Rc<dyn Fn(&mut WindowContext) -> AnyView + 'static>;
pub(crate) struct TooltipBuilder {
build: Rc<dyn Fn(&mut WindowContext) -> AnyView + 'static>,
hoverable: bool,
}
pub(crate) type KeyDownListener =
Box<dyn Fn(&KeyDownEvent, DispatchPhase, &mut WindowContext) + 'static>;
@@ -1188,6 +1227,7 @@ pub struct Interactivity {
/// Whether the element was hovered. This will only be present after paint if an hitbox
/// was created for the interactive element.
pub hovered: Option<bool>,
pub(crate) tooltip_id: Option<TooltipId>,
pub(crate) content_size: Size<Pixels>,
pub(crate) key_context: Option<KeyContext>,
pub(crate) focusable: bool,
@@ -1321,7 +1361,7 @@ impl Interactivity {
if let Some(active_tooltip) = element_state.active_tooltip.as_ref() {
if let Some(active_tooltip) = active_tooltip.borrow().as_ref() {
if let Some(tooltip) = active_tooltip.tooltip.clone() {
cx.set_tooltip(tooltip);
self.tooltip_id = Some(cx.set_tooltip(tooltip));
}
}
}
@@ -1806,6 +1846,7 @@ impl Interactivity {
}
if let Some(tooltip_builder) = self.tooltip_builder.take() {
let tooltip_is_hoverable = tooltip_builder.hoverable;
let active_tooltip = element_state
.active_tooltip
.get_or_insert_with(Default::default)
@@ -1818,11 +1859,17 @@ impl Interactivity {
cx.on_mouse_event({
let active_tooltip = active_tooltip.clone();
let hitbox = hitbox.clone();
let tooltip_id = self.tooltip_id;
move |_: &MouseMoveEvent, phase, cx| {
let is_hovered =
pending_mouse_down.borrow().is_none() && hitbox.is_hovered(cx);
if !is_hovered {
active_tooltip.borrow_mut().take();
let tooltip_is_hovered =
tooltip_id.map_or(false, |tooltip_id| tooltip_id.is_hovered(cx));
if !is_hovered && (!tooltip_is_hoverable || !tooltip_is_hovered) {
if active_tooltip.borrow_mut().take().is_some() {
cx.refresh();
}
return;
}
@@ -1833,15 +1880,14 @@ impl Interactivity {
if active_tooltip.borrow().is_none() {
let task = cx.spawn({
let active_tooltip = active_tooltip.clone();
let tooltip_builder = tooltip_builder.clone();
let build_tooltip = tooltip_builder.build.clone();
move |mut cx| async move {
cx.background_executor().timer(TOOLTIP_DELAY).await;
cx.update(|cx| {
active_tooltip.borrow_mut().replace(ActiveTooltip {
tooltip: Some(AnyTooltip {
view: tooltip_builder(cx),
cursor_offset: cx.mouse_position(),
view: build_tooltip(cx),
mouse_position: cx.mouse_position(),
}),
_task: None,
});
@@ -1860,15 +1906,30 @@ impl Interactivity {
cx.on_mouse_event({
let active_tooltip = active_tooltip.clone();
move |_: &MouseDownEvent, _, _| {
active_tooltip.borrow_mut().take();
let tooltip_id = self.tooltip_id;
move |_: &MouseDownEvent, _, cx| {
let tooltip_is_hovered =
tooltip_id.map_or(false, |tooltip_id| tooltip_id.is_hovered(cx));
if !tooltip_is_hoverable || !tooltip_is_hovered {
if active_tooltip.borrow_mut().take().is_some() {
cx.refresh();
}
}
}
});
cx.on_mouse_event({
let active_tooltip = active_tooltip.clone();
move |_: &ScrollWheelEvent, _, _| {
active_tooltip.borrow_mut().take();
let tooltip_id = self.tooltip_id;
move |_: &ScrollWheelEvent, _, cx| {
let tooltip_is_hovered =
tooltip_id.map_or(false, |tooltip_id| tooltip_id.is_hovered(cx));
if !tooltip_is_hoverable || !tooltip_is_hovered {
if active_tooltip.borrow_mut().take().is_some() {
cx.refresh();
}
}
}
})
}
@@ -2381,6 +2442,11 @@ impl ScrollHandle {
}
}
/// Return the bounds into which this child is painted
pub fn bounds(&self) -> Bounds<Pixels> {
self.0.borrow().bounds
}
/// Get the bounds for a specific child.
pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> {
self.0.borrow().child_bounds.get(ix).cloned()

View File

@@ -553,7 +553,7 @@ impl Element for InteractiveText {
ActiveTooltip {
tooltip: Some(AnyTooltip {
view: tooltip,
cursor_offset: cx.mouse_position(),
mouse_position: cx.mouse_position(),
}),
_task: None,
}

View File

@@ -372,7 +372,7 @@ impl BackgroundExecutor {
self.dispatcher.as_test().unwrap().rng()
}
/// How many CPUs are available to the dispatcher
/// How many CPUs are available to the dispatcher.
pub fn num_cpus(&self) -> usize {
num_cpus::get()
}
@@ -440,6 +440,11 @@ impl<'a> Scope<'a> {
}
}
/// How many CPUs are available to the dispatcher.
pub fn num_cpus(&self) -> usize {
self.executor.num_cpus()
}
/// Spawn a future into this scope.
pub fn spawn<F>(&mut self, f: F)
where

View File

@@ -260,11 +260,11 @@ impl DispatchTree {
{
let source_node_id = DispatchNodeId(source_node_id);
while let Some(source_ancestor) = source_stack.last() {
if source_node.parent != Some(*source_ancestor) {
if source_node.parent == Some(*source_ancestor) {
break;
} else {
source_stack.pop();
self.pop_node();
} else {
break;
}
}

View File

@@ -73,12 +73,17 @@ pub(crate) fn current_platform() -> Rc<dyn Platform> {
#[cfg(target_os = "linux")]
pub(crate) fn current_platform() -> Rc<dyn Platform> {
let wayland_display = std::env::var_os("WAYLAND_DISPLAY");
let x11_display = std::env::var_os("DISPLAY");
let use_wayland = wayland_display.is_some_and(|display| !display.is_empty());
let use_x11 = x11_display.is_some_and(|display| !display.is_empty());
if use_wayland {
Rc::new(WaylandClient::new())
} else {
} else if use_x11 {
Rc::new(X11Client::new())
} else {
Rc::new(HeadlessClient::new())
}
}
// todo("windows")

View File

@@ -2,11 +2,13 @@
#![allow(unused)]
mod dispatcher;
mod headless;
mod platform;
mod wayland;
mod x11;
pub(crate) use dispatcher::*;
pub(crate) use headless::*;
pub(crate) use platform::*;
pub(crate) use wayland::*;
pub(crate) use x11::*;

View File

@@ -0,0 +1,3 @@
mod client;
pub(crate) use client::*;

View File

@@ -0,0 +1,98 @@
use std::cell::RefCell;
use std::ops::Deref;
use std::rc::Rc;
use std::time::{Duration, Instant};
use calloop::{EventLoop, LoopHandle};
use collections::HashMap;
use util::ResultExt;
use crate::platform::linux::LinuxClient;
use crate::platform::{LinuxCommon, PlatformWindow};
use crate::{
px, AnyWindowHandle, Bounds, CursorStyle, DisplayId, Modifiers, ModifiersChangedEvent, Pixels,
PlatformDisplay, PlatformInput, Point, ScrollDelta, Size, TouchPhase, WindowParams,
};
use calloop::{
generic::{FdWrapper, Generic},
RegistrationToken,
};
pub struct HeadlessClientState {
pub(crate) loop_handle: LoopHandle<'static, HeadlessClient>,
pub(crate) event_loop: Option<calloop::EventLoop<'static, HeadlessClient>>,
pub(crate) common: LinuxCommon,
}
#[derive(Clone)]
pub(crate) struct HeadlessClient(Rc<RefCell<HeadlessClientState>>);
impl HeadlessClient {
pub(crate) fn new() -> Self {
let event_loop = EventLoop::try_new().unwrap();
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
let handle = event_loop.handle();
handle.insert_source(main_receiver, |event, _, _: &mut HeadlessClient| {
if let calloop::channel::Event::Msg(runnable) = event {
runnable.run();
}
});
HeadlessClient(Rc::new(RefCell::new(HeadlessClientState {
event_loop: Some(event_loop),
loop_handle: handle,
common,
})))
}
}
impl LinuxClient for HeadlessClient {
fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R {
f(&mut self.0.borrow_mut().common)
}
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
vec![]
}
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
None
}
fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
None
}
fn open_window(
&self,
_handle: AnyWindowHandle,
params: WindowParams,
) -> Box<dyn PlatformWindow> {
unimplemented!()
}
//todo(linux)
fn set_cursor_style(&self, _style: CursorStyle) {}
fn write_to_clipboard(&self, item: crate::ClipboardItem) {}
fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
None
}
fn run(&self) {
let mut event_loop = self
.0
.borrow_mut()
.event_loop
.take()
.expect("App is already running");
event_loop.run(None, &mut self.clone(), |_| {}).log_err();
}
}

View File

@@ -45,63 +45,6 @@ pub(crate) const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(400);
pub(crate) const DOUBLE_CLICK_DISTANCE: Pixels = px(5.0);
pub(crate) const KEYRING_LABEL: &str = "zed-github-account";
pub struct RcRefCell<T>(Rc<RefCell<T>>);
impl<T> RcRefCell<T> {
pub fn new(value: T) -> Self {
RcRefCell(Rc::new(RefCell::new(value)))
}
#[inline]
#[track_caller]
pub fn borrow_mut(&self) -> std::cell::RefMut<'_, T> {
#[cfg(debug_assertions)]
{
if option_env!("TRACK_BORROW_MUT").is_some() {
eprintln!(
"borrow_mut-ing {} at {}",
type_name::<T>(),
Location::caller()
);
}
}
self.0.borrow_mut()
}
#[inline]
#[track_caller]
pub fn borrow(&self) -> std::cell::Ref<'_, T> {
#[cfg(debug_assertions)]
{
if option_env!("TRACK_BORROW_MUT").is_some() {
eprintln!("borrow-ing {} at {}", type_name::<T>(), Location::caller());
}
}
self.0.borrow()
}
}
impl<T> Deref for RcRefCell<T> {
type Target = Rc<RefCell<T>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> DerefMut for RcRefCell<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<T> Clone for RcRefCell<T> {
fn clone(&self) -> Self {
RcRefCell(self.0.clone())
}
}
pub trait LinuxClient {
fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R;
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;

View File

@@ -1,7 +1,8 @@
use std::cell::RefCell;
use std::rc::Rc;
use std::cell::{RefCell, RefMut};
use std::rc::{Rc, Weak};
use std::time::{Duration, Instant};
use async_task::Runnable;
use calloop::timer::{TimeoutAction, Timer};
use calloop::{EventLoop, LoopHandle};
use calloop_wayland_source::WaylandSource;
@@ -35,12 +36,13 @@ use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1;
use xkbcommon::xkb::{self, Keycode, KEYMAP_COMPILE_NO_FLAGS};
use super::super::DOUBLE_CLICK_INTERVAL;
use super::window::{WaylandWindowState, WaylandWindowStatePtr};
use crate::platform::linux::is_within_click_distance;
use crate::platform::linux::wayland::cursor::Cursor;
use crate::platform::linux::wayland::window::WaylandWindow;
use crate::platform::linux::LinuxClient;
use crate::platform::PlatformWindow;
use crate::{point, px, MouseExitEvent};
use crate::{point, px, ForegroundExecutor, MouseExitEvent};
use crate::{
AnyWindowHandle, CursorStyle, DisplayId, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers,
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
@@ -52,9 +54,9 @@ use crate::{LinuxCommon, WindowParams};
/// Used to convert evdev scancode to xkb scancode
const MIN_KEYCODE: u32 = 8;
#[derive(Debug, Clone)]
#[derive(Clone)]
pub struct Globals {
pub qh: QueueHandle<WaylandClient>,
pub qh: QueueHandle<WaylandClientStatePtr>,
pub compositor: wl_compositor::WlCompositor,
pub wm_base: xdg_wm_base::XdgWmBase,
pub shm: wl_shm::WlShm,
@@ -62,10 +64,15 @@ pub struct Globals {
pub fractional_scale_manager:
Option<wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1>,
pub decoration_manager: Option<zxdg_decoration_manager_v1::ZxdgDecorationManagerV1>,
pub executor: ForegroundExecutor,
}
impl Globals {
fn new(globals: GlobalList, qh: QueueHandle<WaylandClient>) -> Self {
fn new(
globals: GlobalList,
executor: ForegroundExecutor,
qh: QueueHandle<WaylandClientStatePtr>,
) -> Self {
Globals {
compositor: globals
.bind(
@@ -80,6 +87,7 @@ impl Globals {
viewporter: globals.bind(&qh, 1..=1, ()).ok(),
fractional_scale_manager: globals.bind(&qh, 1..=1, ()).ok(),
decoration_manager: globals.bind(&qh, 1..=1, ()).ok(),
executor,
qh,
}
}
@@ -88,7 +96,7 @@ impl Globals {
pub(crate) struct WaylandClientState {
globals: Globals,
// Surface to Window mapping
windows: HashMap<ObjectId, WaylandWindow>,
windows: HashMap<ObjectId, WaylandWindowStatePtr>,
// Output to scale mapping
output_scales: HashMap<ObjectId, i32>,
keymap_state: Option<xkb::State>,
@@ -100,14 +108,14 @@ pub(crate) struct WaylandClientState {
mouse_location: Option<Point<Pixels>>,
enter_token: Option<()>,
button_pressed: Option<MouseButton>,
mouse_focused_window: Option<WaylandWindow>,
keyboard_focused_window: Option<WaylandWindow>,
loop_handle: LoopHandle<'static, WaylandClient>,
mouse_focused_window: Option<WaylandWindowStatePtr>,
keyboard_focused_window: Option<WaylandWindowStatePtr>,
loop_handle: LoopHandle<'static, WaylandClientStatePtr>,
cursor_icon_name: String,
cursor: Cursor,
clipboard: Clipboard,
primary: Primary,
event_loop: Option<EventLoop<'static, WaylandClient>>,
event_loop: Option<EventLoop<'static, WaylandClientStatePtr>>,
common: LinuxCommon,
}
@@ -124,6 +132,35 @@ pub(crate) struct KeyRepeat {
current_keysym: Option<xkb::Keysym>,
}
/// This struct is required to conform to Rust's orphan rules, so we can dispatch on the state but hand the
/// window to GPUI.
#[derive(Clone)]
pub struct WaylandClientStatePtr(Weak<RefCell<WaylandClientState>>);
impl WaylandClientStatePtr {
fn get_client(&self) -> Rc<RefCell<WaylandClientState>> {
self.0
.upgrade()
.expect("The pointer should always be valid when dispatching in wayland")
}
pub fn drop_window(&self, surface_id: &ObjectId) {
let mut client = self.get_client();
let mut state = client.borrow_mut();
let closed_window = state.windows.remove(surface_id).unwrap();
if let Some(window) = state.mouse_focused_window.take() {
if !window.ptr_eq(&closed_window) {
state.mouse_focused_window = Some(window);
}
}
if let Some(window) = state.keyboard_focused_window.take() {
if !window.ptr_eq(&closed_window) {
state.mouse_focused_window = Some(window);
}
}
}
}
#[derive(Clone)]
pub struct WaylandClient(Rc<RefCell<WaylandClientState>>);
@@ -147,7 +184,8 @@ impl WaylandClient {
pub(crate) fn new() -> Self {
let conn = Connection::connect_to_env().unwrap();
let (globals, mut event_queue) = registry_queue_init::<WaylandClient>(&conn).unwrap();
let (globals, mut event_queue) =
registry_queue_init::<WaylandClientStatePtr>(&conn).unwrap();
let qh = event_queue.handle();
let mut outputs = HashMap::default();
@@ -179,19 +217,19 @@ impl WaylandClient {
let display = conn.backend().display_ptr() as *mut std::ffi::c_void;
let (primary, clipboard) = unsafe { create_clipboards_from_external(display) };
let event_loop = EventLoop::try_new().unwrap();
let event_loop = EventLoop::<WaylandClientStatePtr>::try_new().unwrap();
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
let handle = event_loop.handle();
handle.insert_source(main_receiver, |event, _, _: &mut WaylandClient| {
handle.insert_source(main_receiver, |event, _, _: &mut WaylandClientStatePtr| {
if let calloop::channel::Event::Msg(runnable) = event {
runnable.run();
}
});
let globals = Globals::new(globals, qh);
let globals = Globals::new(globals, common.foreground_executor.clone(), qh);
let cursor = Cursor::new(&conn, &globals, 24);
@@ -260,8 +298,12 @@ impl LinuxClient for WaylandClient {
) -> Box<dyn PlatformWindow> {
let mut state = self.0.borrow_mut();
let (window, surface_id) = WaylandWindow::new(state.globals.clone(), params);
state.windows.insert(surface_id, window.clone());
let (window, surface_id) = WaylandWindow::new(
state.globals.clone(),
WaylandClientStatePtr(Rc::downgrade(&self.0)),
params,
);
state.windows.insert(surface_id, window.0.clone());
Box::new(window)
}
@@ -307,7 +349,13 @@ impl LinuxClient for WaylandClient {
.take()
.expect("App is already running");
event_loop.run(None, &mut self.clone(), |_| {}).log_err();
event_loop
.run(
None,
&mut WaylandClientStatePtr(Rc::downgrade(&self.0)),
|_| {},
)
.log_err();
}
fn write_to_clipboard(&self, item: crate::ClipboardItem) {
@@ -327,16 +375,18 @@ impl LinuxClient for WaylandClient {
}
}
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for WaylandClient {
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for WaylandClientStatePtr {
fn event(
state: &mut Self,
this: &mut Self,
registry: &wl_registry::WlRegistry,
event: wl_registry::Event,
_: &GlobalListContents,
_: &Connection,
qh: &QueueHandle<Self>,
) {
let mut state = state.0.borrow_mut();
let mut client = this.get_client();
let mut state = client.borrow_mut();
match event {
wl_registry::Event::Global {
name,
@@ -360,50 +410,60 @@ impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for WaylandClient {
}
}
delegate_noop!(WaylandClient: ignore wl_compositor::WlCompositor);
delegate_noop!(WaylandClient: ignore wl_shm::WlShm);
delegate_noop!(WaylandClient: ignore wl_shm_pool::WlShmPool);
delegate_noop!(WaylandClient: ignore wl_buffer::WlBuffer);
delegate_noop!(WaylandClient: ignore wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1);
delegate_noop!(WaylandClient: ignore zxdg_decoration_manager_v1::ZxdgDecorationManagerV1);
delegate_noop!(WaylandClient: ignore wp_viewporter::WpViewporter);
delegate_noop!(WaylandClient: ignore wp_viewport::WpViewport);
delegate_noop!(WaylandClientStatePtr: ignore wl_compositor::WlCompositor);
delegate_noop!(WaylandClientStatePtr: ignore wl_shm::WlShm);
delegate_noop!(WaylandClientStatePtr: ignore wl_shm_pool::WlShmPool);
delegate_noop!(WaylandClientStatePtr: ignore wl_buffer::WlBuffer);
delegate_noop!(WaylandClientStatePtr: ignore wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1);
delegate_noop!(WaylandClientStatePtr: ignore zxdg_decoration_manager_v1::ZxdgDecorationManagerV1);
delegate_noop!(WaylandClientStatePtr: ignore wp_viewporter::WpViewporter);
delegate_noop!(WaylandClientStatePtr: ignore wp_viewport::WpViewport);
impl Dispatch<WlCallback, ObjectId> for WaylandClient {
impl Dispatch<WlCallback, ObjectId> for WaylandClientStatePtr {
fn event(
this: &mut WaylandClient,
state: &mut WaylandClientStatePtr,
_: &wl_callback::WlCallback,
event: wl_callback::Event,
surface_id: &ObjectId,
_: &Connection,
qh: &QueueHandle<Self>,
) {
let state = this.0.borrow_mut();
let Some(window) = state.windows.get(surface_id).cloned() else {
let client = state.get_client();
let mut state = client.borrow_mut();
let Some(window) = get_window(&mut state, surface_id) else {
return;
};
drop(state);
match event {
wl_callback::Event::Done { callback_data } => {
window.frame();
window.frame(true);
}
_ => {}
}
}
}
impl Dispatch<wl_surface::WlSurface, ()> for WaylandClient {
fn get_window(
mut state: &mut RefMut<WaylandClientState>,
surface_id: &ObjectId,
) -> Option<WaylandWindowStatePtr> {
state.windows.get(surface_id).cloned()
}
impl Dispatch<wl_surface::WlSurface, ()> for WaylandClientStatePtr {
fn event(
state: &mut Self,
this: &mut Self,
surface: &wl_surface::WlSurface,
event: <wl_surface::WlSurface as Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
let mut state = state.0.borrow_mut();
let Some(window) = state.windows.get(&surface.id()).cloned() else {
let mut client = this.get_client();
let mut state = client.borrow_mut();
let Some(window) = get_window(&mut state, &surface.id()) else {
return;
};
let scales = state.output_scales.clone();
@@ -413,16 +473,18 @@ impl Dispatch<wl_surface::WlSurface, ()> for WaylandClient {
}
}
impl Dispatch<wl_output::WlOutput, ()> for WaylandClient {
impl Dispatch<wl_output::WlOutput, ()> for WaylandClientStatePtr {
fn event(
state: &mut Self,
this: &mut Self,
output: &wl_output::WlOutput,
event: <wl_output::WlOutput as Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
let mut state = state.0.borrow_mut();
let mut client = this.get_client();
let mut state = client.borrow_mut();
let Some(mut output_scale) = state.output_scales.get_mut(&output.id()) else {
return;
};
@@ -436,7 +498,7 @@ impl Dispatch<wl_output::WlOutput, ()> for WaylandClient {
}
}
impl Dispatch<xdg_surface::XdgSurface, ObjectId> for WaylandClient {
impl Dispatch<xdg_surface::XdgSurface, ObjectId> for WaylandClientStatePtr {
fn event(
state: &mut Self,
xdg_surface: &xdg_surface::XdgSurface,
@@ -445,17 +507,17 @@ impl Dispatch<xdg_surface::XdgSurface, ObjectId> for WaylandClient {
_: &Connection,
_: &QueueHandle<Self>,
) {
let mut state = state.0.borrow_mut();
let Some(window) = state.windows.get(surface_id).cloned() else {
let client = state.get_client();
let mut state = client.borrow_mut();
let Some(window) = get_window(&mut state, surface_id) else {
return;
};
drop(state);
window.handle_xdg_surface_event(event);
}
}
impl Dispatch<xdg_toplevel::XdgToplevel, ObjectId> for WaylandClient {
impl Dispatch<xdg_toplevel::XdgToplevel, ObjectId> for WaylandClientStatePtr {
fn event(
this: &mut Self,
xdg_toplevel: &xdg_toplevel::XdgToplevel,
@@ -464,9 +526,9 @@ impl Dispatch<xdg_toplevel::XdgToplevel, ObjectId> for WaylandClient {
_: &Connection,
_: &QueueHandle<Self>,
) {
let mut state = this.0.borrow_mut();
let Some(window) = state.windows.get(surface_id).cloned() else {
let client = this.get_client();
let mut state = client.borrow_mut();
let Some(window) = get_window(&mut state, surface_id) else {
return;
};
@@ -474,13 +536,12 @@ impl Dispatch<xdg_toplevel::XdgToplevel, ObjectId> for WaylandClient {
let should_close = window.handle_toplevel_event(event);
if should_close {
let mut state = this.0.borrow_mut();
state.windows.remove(surface_id);
this.drop_window(surface_id);
}
}
}
impl Dispatch<xdg_wm_base::XdgWmBase, ()> for WaylandClient {
impl Dispatch<xdg_wm_base::XdgWmBase, ()> for WaylandClientStatePtr {
fn event(
_: &mut Self,
wm_base: &xdg_wm_base::XdgWmBase,
@@ -495,7 +556,7 @@ impl Dispatch<xdg_wm_base::XdgWmBase, ()> for WaylandClient {
}
}
impl Dispatch<wl_seat::WlSeat, ()> for WaylandClient {
impl Dispatch<wl_seat::WlSeat, ()> for WaylandClientStatePtr {
fn event(
state: &mut Self,
seat: &wl_seat::WlSeat,
@@ -518,7 +579,7 @@ impl Dispatch<wl_seat::WlSeat, ()> for WaylandClient {
}
}
impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClient {
impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
fn event(
this: &mut Self,
keyboard: &wl_keyboard::WlKeyboard,
@@ -527,7 +588,8 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClient {
conn: &Connection,
qh: &QueueHandle<Self>,
) {
let mut state = this.0.borrow_mut();
let mut client = this.get_client();
let mut state = client.borrow_mut();
match event {
wl_keyboard::Event::RepeatInfo { rate, delay } => {
state.repeat.characters_per_second = rate as u32;
@@ -559,7 +621,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClient {
state.keymap_state = Some(xkb::State::new(&keymap));
}
wl_keyboard::Event::Enter { surface, .. } => {
state.keyboard_focused_window = state.windows.get(&surface.id()).cloned();
state.keyboard_focused_window = get_window(&mut state, &surface.id());
if let Some(window) = state.keyboard_focused_window.clone() {
drop(state);
@@ -567,7 +629,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClient {
}
}
wl_keyboard::Event::Leave { surface, .. } => {
let keyboard_focused_window = state.windows.get(&surface.id()).cloned();
let keyboard_focused_window = get_window(&mut state, &surface.id());
state.keyboard_focused_window = None;
if let Some(window) = keyboard_focused_window {
@@ -629,8 +691,9 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClient {
.loop_handle
.insert_source(Timer::from_duration(state.repeat.delay), {
let input = input.clone();
move |event, _metadata, client| {
let state = client.0.borrow_mut();
move |event, _metadata, this| {
let mut client = this.get_client();
let mut state = client.borrow_mut();
let is_repeating = id == state.repeat.current_id
&& state.repeat.current_keysym.is_some()
&& state.keyboard_focused_window.is_some();
@@ -691,16 +754,18 @@ fn linux_button_to_gpui(button: u32) -> Option<MouseButton> {
})
}
impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClient {
impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
fn event(
client: &mut Self,
this: &mut Self,
wl_pointer: &wl_pointer::WlPointer,
event: wl_pointer::Event,
data: &(),
conn: &Connection,
qh: &QueueHandle<Self>,
) {
let mut state = client.0.borrow_mut();
let mut client = this.get_client();
let mut state = client.borrow_mut();
let cursor_icon_name = state.cursor_icon_name.clone();
match event {
wl_pointer::Event::Enter {
@@ -712,11 +777,13 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClient {
} => {
state.mouse_location = Some(point(px(surface_x as f32), px(surface_y as f32)));
if let Some(window) = state.windows.get(&surface.id()).cloned() {
if let Some(window) = get_window(&mut state, &surface.id()) {
state.enter_token = Some(());
state.mouse_focused_window = Some(window.clone());
state.cursor.set_serial_id(serial);
state.cursor.set_icon(&wl_pointer, None);
state
.cursor
.set_icon(&wl_pointer, Some(cursor_icon_name.as_str()));
drop(state);
window.set_focused(true);
}
@@ -747,7 +814,9 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClient {
return;
}
state.mouse_location = Some(point(px(surface_x as f32), px(surface_y as f32)));
state.cursor.set_icon(&wl_pointer, None);
state
.cursor
.set_icon(&wl_pointer, Some(cursor_icon_name.as_str()));
if let Some(window) = state.mouse_focused_window.clone() {
let input = PlatformInput::MouseMove(MouseMoveEvent {
@@ -896,18 +965,19 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClient {
}
}
impl Dispatch<wp_fractional_scale_v1::WpFractionalScaleV1, ObjectId> for WaylandClient {
impl Dispatch<wp_fractional_scale_v1::WpFractionalScaleV1, ObjectId> for WaylandClientStatePtr {
fn event(
state: &mut Self,
this: &mut Self,
_: &wp_fractional_scale_v1::WpFractionalScaleV1,
event: <wp_fractional_scale_v1::WpFractionalScaleV1 as Proxy>::Event,
surface_id: &ObjectId,
_: &Connection,
_: &QueueHandle<Self>,
) {
let mut state = state.0.borrow_mut();
let client = this.get_client();
let mut state = client.borrow_mut();
let Some(window) = state.windows.get(surface_id).cloned() else {
let Some(window) = get_window(&mut state, surface_id) else {
return;
};
@@ -916,18 +986,20 @@ impl Dispatch<wp_fractional_scale_v1::WpFractionalScaleV1, ObjectId> for Wayland
}
}
impl Dispatch<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1, ObjectId> for WaylandClient {
impl Dispatch<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1, ObjectId>
for WaylandClientStatePtr
{
fn event(
state: &mut Self,
this: &mut Self,
_: &zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1,
event: zxdg_toplevel_decoration_v1::Event,
surface_id: &ObjectId,
_: &Connection,
_: &QueueHandle<Self>,
) {
let mut state = state.0.borrow_mut();
let Some(window) = state.windows.get(surface_id).cloned() else {
let client = this.get_client();
let mut state = client.borrow_mut();
let Some(window) = get_window(&mut state, surface_id) else {
return;
};

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