Compare commits

..

39 Commits

Author SHA1 Message Date
Nathan Sobo
74ed2f7f70 Reverse div child paint order 2024-02-23 19:37:44 -07:00
Antonio Scandurra
eba5db3a6e Finish flipping paint order in EditorElement 2024-02-23 18:54:08 +01:00
Antonio Scandurra
cd6bdd8b1c WIP: start painting elements front-to-back
Co-Authored-By: Nathan <nathan@zed.dev>
2024-02-23 18:22:03 +01:00
Antonio Scandurra
a3ce933b04 Use BoundsTree in scene
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2024-02-23 17:51:06 +01:00
Antonio Scandurra
816c48b7d6 Introduce a BoundsTree structure
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2024-02-23 17:33:22 +01:00
Thorsten Ball
42ac9880c6 Detect and possibly use user-installed gopls / zls language servers (#8188)
After a lot of back-and-forth, this is a small attempt to implement
solutions (1) and (3) in
https://github.com/zed-industries/zed/issues/7902. The goal is to have a
minimal change that helps users get started with Zed, until we have
extensions ready.

Release Notes:

- Added detection of user-installed `gopls` to Go language server
adapter. If a user has `gopls` in `$PATH` when opening a worktree, it
will be used.
- Added detection of user-installed `zls` to Zig language server
adapter. If a user has `zls` in `$PATH` when opening a worktree, it will
be used.

Example:

I don't have `go` installed globally, but I do have `gopls`:

```
~ $ which go
go not found
~ $ which gopls
/Users/thorstenball/code/go/bin/gopls
```

But I do have `go` in a project's directory:

```
~/tmp/go-testing φ which go
/Users/thorstenball/.local/share/mise/installs/go/1.21.5/go/bin/go
~/tmp/go-testing φ which gopls
/Users/thorstenball/code/go/bin/gopls
```

With current Zed when I run `zed ~/tmp/go-testing`, I'd get the dreaded
error:

![screenshot-2024-02-23-11 14
08@2x](https://github.com/zed-industries/zed/assets/1185253/822ea59b-c63e-4102-a50e-75501cc4e0e3)

But with the changes in this PR, it works:

```
[2024-02-23T11:14:42+01:00 INFO  language::language_registry] starting language server "gopls", path: "/Users/thorstenball/tmp/go-testing", id: 1
[2024-02-23T11:14:42+01:00 INFO  language::language_registry] found user-installed language server for Go. path: "/Users/thorstenball/code/go/bin/gopls", arguments: ["-mode=stdio"]
[2024-02-23T11:14:42+01:00 INFO  lsp] starting language server. binary path: "/Users/thorstenball/code/go/bin/gopls", working directory: "/Users/thorstenball/tmp/go-testing", args: ["-mode=stdio"]
```

---------

Co-authored-by: Antonio <antonio@zed.dev>
2024-02-23 13:39:14 +01:00
postsolar
65318cb6ac Re-enable PureScript on Linux and Windows (#8252)
Relevant PRs:
- https://github.com/zed-industries/zed/pull/7543
- https://github.com/zed-industries/zed/pull/7827

Release Notes:

- Fixed build issues with PureScript on Windows and Linux
2024-02-23 13:19:36 +02:00
Kirill Bulatov
71557f3eb3 Adjust "recent projects" modal behavior to allow opening projects in both current and new window (#8267)
![image](https://github.com/zed-industries/zed/assets/2690773/7a0927e8-f32a-4502-8a8a-c7f8e5f325bb)

Fixes https://github.com/zed-industries/zed/issues/7419 by changing the
way "recent projects" modal confirm actions work:
* `menu::Confirm` now reuses the current window when opening a recent
project
* `menu::SecondaryConfirm` now opens a recent project in the new window 
* neither confirm tries to open the current project anymore
* modal's placeholder is adjusted to emphasize this behavior

Release Notes:

- Added a way to open recent projects in the new window
2024-02-23 13:17:31 +02:00
Kirill Bulatov
a588f674db Ensure default prettier installs correctly when certain FS entries are missing (#8261)
Fixes https://github.com/zed-industries/zed/issues/7865

* bind default prettier (re)installation decision to
`prettier_server.js` existence
* ensure the `prettier_server.js` file is created last, after all
default prettier packages installed
* ensure that default prettier directory exists before installing the
packages
* reinstall default prettier if the `prettier_server.js` file is
different from what Zed expects

Release Notes:

- Fixed incorrect default prettier installation process
2024-02-23 12:25:56 +02:00
Rom Grk
50dd38bd02 Linux: adjust docs for building (#8246)
Improve docs & remove `vulkan-validation-layers` from the dependencies.
2024-02-22 22:56:18 -08:00
Conrad Irwin
caa156ab13 Fix a panic in the assistant panel (#8244)
Release Notes:

- Fixed a panic in the assistant panel when the app is shutting down.
2024-02-22 22:42:32 -07:00
Felipe
a82f4857f4 Add settings to control gutter elements (#7665)
The current gutter was a bit too big for my taste, so I added some
settings to change which visual elements are shown, including being able
to hide the gutter completely.

This should help with the following issues: #4963, #4382, #7422

New settings:
```json5
"gutter": {
    "line_numbers": true, // Whether line numbers are shown
    "buttons": true // Whether the code action/folding buttons are shown
}
```

The existing `git.git_gutter` setting is also taken into account when
calculating the width of the gutter.

We could also separate the display of the code action and folding
buttons into separate settings, let me know if that is desirable.

## Screenshots:

- Everything on (`gutter.line_numbers`, `gutter.buttons`,
`git.git_gutter`):
<img width="434" alt="SCR-20240210-trfb"
src="https://github.com/zed-industries/zed/assets/17355488/bcc55311-6e1d-4c22-8c43-4f364637959b">

- Only line numbers and git gutter (`gutter.line_numbers`,
`git.git_gutter`):
<img width="406" alt="SCR-20240210-trhm"
src="https://github.com/zed-industries/zed/assets/17355488/0a0e074d-64d0-437c-851b-55560d5a6c6b">

- Only git gutter (`git.git_gutter`):
<img width="356" alt="SCR-20240210-trkb"
src="https://github.com/zed-industries/zed/assets/17355488/7ebdb38d-93a5-4e38-b008-beabf355510d">

- Only git gutter and buttons (`git.git_gutter`, `gutter.buttons`):
<img width="356" alt="SCR-20240210-txyo"
src="https://github.com/zed-industries/zed/assets/17355488/e2c92c05-cc30-43bc-9399-09ea5e376e1b">


- Nothing:
<img width="350" alt="SCR-20240210-trne"
src="https://github.com/zed-industries/zed/assets/17355488/e0cd301d-c3e0-4b31-ac69-997515928b5a">



## Release Notes:
- Added settings to control the display of gutter visual elements. `"gutter": {"line_numbers": true,    "code_actions": true,    "folds": true}` ([#8041](https://github.com/zed-industries/zed/issues/8041))  ([#7422](https://github.com/zed-industries/zed/issues/7422))
```

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-02-22 20:37:13 -07:00
Marshall Bowers
0de8672044 Add SystemClock (#8239)
This PR adds a `SystemClock` trait for abstracting away the system
clock.

This allows us to swap out the real system clock with a
`FakeSystemClock` in the tests, thus allowing the fake passage of time.

We're using this in `Telemetry` to better mock the clock for testing
purposes.

Release Notes:

- N/A
2024-02-22 22:28:08 -05:00
Joseph T. Lyons
cc8e3c2286 Show more extensions (#8234)
This is a bandaid fix for:
https://github.com/zed-industries/zed/issues/8228.

Release Notes:

- N/A
2024-02-22 19:51:03 -05:00
Kristján Oddsson
347f68887f Support ESLint flat configs (#8109)
Not available before the new eslint language server version is released, but prepares the ground for it.

## Further reading

- https://eslint.org/docs/latest/use/configure/configuration-files-new
- https://github.com/microsoft/vscode-eslint?tab=readme-ov-file#settings-options

Release Notes:

- Added ESLint flat config support
([#7271](https://github.com/zed-industries/zed/issues/7271))
2024-02-22 22:22:31 +02:00
Small White
a475d8640f Introduce file_id on Windows (#8130)
Added a function `file_id` to get the file id on windows, which is
similar to inode on unix.

Release Notes:

- N/A
2024-02-22 11:22:12 -08:00
Dzmitry Malyshau
991c9ec441 Integrate profiling into gpui (#8176)
[Profiling](https://crates.io/crates/profiling) crate allows easy
integration with various profiler tools. The best thing is - annotations
compile to nothing unless you request a specific feature.

For example, I used this command to enable Tracy support:
```bash
cargo run --features profiling/profile-with-tracy
```
At the same time I had Tracy tool open and waiting for connection. It
gathered nice stats from the run:

![zed-profiler](https://github.com/zed-industries/zed/assets/107301/5233045d-078c-4ad8-8b00-7ae55cf94ebb)


Release Notes:
- N/A
2024-02-22 10:59:52 -08:00
Conrad Irwin
250df707bf Tidy up indicators in collab panel (#8214)
Move away from columns of icons towards the "changed" info dot we used
for files.

Secondary actions for chat/notes still show up (if you're lucky) on
hover.

Co-Authored-By: Marshall <marshall@zed.dev>



Release Notes:

- Improved design of collab panel

---------

Co-authored-by: Marshall <marshall@zed.dev>
Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-02-22 13:30:43 -05:00
Michael Angerman
ba6b319046 Add an app_menu to Storybook which enables quitting with cmd-q (#8166)
I really think storybook is a cool standalone app but there are some
usability issues that are getting in the way of making this a fun tool
to use.

Currently it is not easy to gracefully exit out of storybook.

In fact even trying to Ctrl-c out of storybook seems currently broken to
me...

So the only real way to exit out of storybook is to kill the process
after a Ctrl-z.

This PR attempts to make this much easier by adding a simple app_menu
with a menu item called quit along with the ability to *Cmd-q* out of
storybook as well...

Both the menu item quit and *Cmd-q* gracefully exit storybook.

There are still a bunch of issues with storybook which I plan on
addressing in future PR's but this is a start and something that to me
is the highest priority to make storybook more functional and easy to
use moving forward.

One of my longer term goals of storybook is to have it be a nice stand
alone application similar to
[Loungy](https://github.com/MatthiasGrandl/Loungy) which can be used as
a nice tutorial application for how to develop a real world *gpui* app.

For that reason I added a *assets/keymaps/storybook.json* file as well.
2024-02-22 12:51:40 -05:00
Rom Grk
bd94a0e921 Wayland: implement focus events (#8170)
Implements keyboard focus in/out events.

This also enables vim mode to work on wayland, which is only activated
when an editor gains focus.
2024-02-22 09:51:09 -08:00
apricotbucket28
40bbd0031d linux: fix reveal_path for files (#8162)
Fixes 'Reveal in Finder' opening files instead of showing them in the
file explorer.
Tested on Fedora KDE 39.

Release Notes:

- N/A
2024-02-22 09:49:36 -08:00
Rom Grk
946f4a312a Wayland: avoid replacing text with empty string (#8103)
Fix an issue where the `ime_key` is sometimes an empty string, and
pressing a keystroke replaces the selected text.

E.g. select some text, press `Escape`: selected text is deleted.
2024-02-22 09:48:15 -08:00
Marshall Bowers
af06063d31 Add checkbox to only show installed extensions (#8208)
This PR adds a checkbox to the extensions view to allow filtering to
just extensions that are installed:

<img width="1408" alt="Screenshot 2024-02-22 at 12 05 40 PM"
src="https://github.com/zed-industries/zed/assets/1486634/b5e82941-53be-432e-bfe5-fec7fd0959c5">

Release Notes:

- Added a checkbox to the extensions view to only show installed
extensions.
2024-02-22 12:16:02 -05:00
Mahdy M. Karam
5c4f3c0cea Add option to either use system clipboard or vim clipboard (#7936)
Release Notes:

- vim: Added a setting to control default clipboard behaviour. `{"vim":
{"use_system_clipboard": "never"}}` disables writing to the clipboard.
`"on_yank"` writes to the system clipboard only on yank, and `"always"`
preserves the current behavior. ([#4390
](https://github.com/zed-industries/zed/issues/4390))

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-02-22 10:12:29 -07:00
Conrad Irwin
c6826a61a0 talkers (#8158)
Release Notes:

- Added an "Unmute" action for guests in calls. This lets them use the
mic, but not edit projects.
2024-02-22 10:07:36 -07:00
Piotr Osiewicz
fa2c92d190 Editor: tweak label for "Go to implementation" tabs (#8201)
No release notes as this is a followup to #7890 
Release Notes:

- N/A
2024-02-22 17:06:25 +01:00
Conrad Irwin
20b10fdca9 Add ./script/symbolicate (#8165)
This lets you get a readable backtrace from an .ips file of a crash
report.

Release Notes:

- N/A
2024-02-22 08:50:39 -07:00
Kirill Bulatov
4f40d3c801 Require prerelease eslint version (#8197)
Fixes https://github.com/zed-industries/zed/issues/7650

Release Notes:

- Fixed eslint diagnostics not showing up due to old eslint version used
2024-02-22 16:33:08 +02:00
Leon Huston
b716035d02 Editor: support go to implementation (#7890)
Release Notes:

- Added "Go to implementation" support in editor.
2024-02-22 14:22:04 +01:00
Piotr Osiewicz
94bc216bbd worktree: Do not scan for .gitignore files beyond project root. (#8189)
This has been fixed and reported before (and got lost in gpui1->gpui2
transition);
https://github.com/zed-industries/zed/issues/5749#issuecomment-1959217319

Release Notes:

- Fixed .gitignore files beyond the first .git directory being respected
by the worktree (zed-industries/zed#5749).
2024-02-22 13:16:19 +01:00
Ngô Quốc Đạt
95d5ea7edc Add "Extensions" item to user menu (#8183)
<img width="274" alt="Screenshot 2024-02-22 at 18 12 52"
src="https://github.com/zed-industries/zed/assets/56961917/9057d1be-bedb-474a-a663-c53d62366f26">

Release Note:

- Add "Extensions" menu item to the UI
2024-02-22 14:01:20 +02:00
Jason Lee
aff858bd00 Added a cmd-backspace keybinding to delete files in the project panel. (#8163)
Fixes #7228

Release Notes:

- Added a `cmd-backspace` keybinding to delete files in the project panel ([7228](https://github.com/zed-industries/zed/issues/7228))
2024-02-22 12:59:01 +02:00
Thorsten Ball
583d85cf66 Do not add empty tasks to inventory (#8180)
I ran into this when trying out which keybindings work and accidentally
added empty tasks. They get then added to the task inventory and
displayed in the picker.

Release Notes:

- Fixed empty tasks being added to the list of tasks when using `task:
spawn`

---------

Co-authored-by: Kirill <kirill@zed.dev>
2024-02-22 12:21:19 +02:00
Xinzhao Xu
36586b77ec gpui: use a separate image in the image example and remove unnecessary examples (#8181)
Follow-up of https://github.com/zed-industries/zed/pull/8174#issuecomment-1959031659,
Fixes image example and removes window-less "noop" example.

<img width="1975" alt="Screenshot 2024-02-22 at 17 34 15"
src="https://github.com/zed-industries/zed/assets/9134003/060d8484-63b6-415a-9f06-189542422457">

Release Notes:
- N/A
2024-02-22 11:47:24 +02:00
Robin Pfäffle
587788b9a0 Update docs for inlay hints (#8178)
Follow up of #7943. 
Updates the docs for inlay hints as they are incorrect (for Svelte).

Release Notes:

- N/A
2024-02-22 11:40:49 +02:00
Xinzhao Xu
6f36527bc6 gpui: add example sections in Cargo.toml (#8174)
So that we can run the example simply by `cargo run --example hello_world`

Release Notes:
- N/A
2024-02-22 11:18:34 +02:00
Hans
aa34e306f7 Fix removal of brackets inserted by auto-close when using snippets (#7265)
Release Notes:

- Fixed auto-inserted brackets (or quotes) not being removed when they
were inserted as part of a snippet.
([#4605](https://github.com/zed-industries/issues/4605))

---------

Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
2024-02-22 10:09:10 +01:00
Joseph T. Lyons
e5d971f4c7 Add url preview tooltip to repository link in extensions view (#8179)
Because the `repository` url field is defined via the user's
`extension.json` file, a user could insert a malicious link. I want to
be able to preview repository urls before clicking the button.

Release Notes:

- Add url preview tooltip to repository link in extensions view.
2024-02-22 03:46:01 -05:00
Joseph T. Lyons
38c3a93f0c Add action to open release notes locally (#8173)
Fixes: https://github.com/zed-industries/zed/issues/5019

zed.dev PR: https://github.com/zed-industries/zed.dev/pull/562

I've been wanting to be able to open release notes in Zed for awhile,
but was blocked on having a rendered Markdown view. Now that that is
mostly there, I think we can add this. I have not removed the `auto
update: view release notes` action, since the Markdown render view
doesn't support displaying media yet. I've opted to just add a new
action: `auto update: view release notes locally`. I'd imagine that in
the future, once the rendered view supports media, we could remove `view
release notes` and `view release notes locally` could replace it.
Clicking the toast that normally is presented on update
(https://github.com/zed-industries/zed/issues/7597) would show the notes
locally.

The action works for stable and preview as expected; for dev and
nightly, it just pulls the latest stable, for testing purposes.

I changed the way the markdown rendered view works by allowing a tab
description to be passed in.

For files that have a name, it will use `Preview <name>`:

<img width="1496" alt="SCR-20240222-byyz"
src="https://github.com/zed-industries/zed/assets/19867440/a0ef34e5-bd6d-4b0c-a684-9b09d350aec4">

For untitled files, it defaults back to `Markdown preview`:

<img width="1496" alt="SCR-20240222-byip"
src="https://github.com/zed-industries/zed/assets/19867440/2ba3f336-6198-4dce-8867-cf0e45f2c646">

Release Notes:

- Added a `zed: view release notes locally` action
([#5019](https://github.com/zed-industries/zed/issues/5019)).


https://github.com/zed-industries/zed/assets/19867440/af324f9c-e7a4-4434-adff-7fe0f8ccc7ff
2024-02-22 02:20:06 -05:00
153 changed files with 3996 additions and 1998 deletions

64
Cargo.lock generated
View File

@@ -770,10 +770,12 @@ dependencies = [
"anyhow",
"client",
"db",
"editor",
"gpui",
"isahc",
"lazy_static",
"log",
"markdown_preview",
"menu",
"project",
"release_channel",
@@ -1349,7 +1351,7 @@ dependencies = [
"rustc-hash",
"shlex",
"syn 2.0.48",
"which",
"which 4.4.2",
]
[[package]]
@@ -1909,6 +1911,7 @@ dependencies = [
"async-recursion 0.3.2",
"async-tungstenite",
"chrono",
"clock",
"collections",
"db",
"feature_flags",
@@ -1946,6 +1949,8 @@ dependencies = [
name = "clock"
version = "0.1.0"
dependencies = [
"chrono",
"parking_lot 0.11.2",
"smallvec",
]
@@ -2091,6 +2096,7 @@ dependencies = [
"collections",
"db",
"editor",
"extensions_ui",
"feature_flags",
"feedback",
"futures 0.3.28",
@@ -3675,6 +3681,7 @@ dependencies = [
"text",
"time",
"util",
"windows-sys 0.52.0",
]
[[package]]
@@ -4118,6 +4125,7 @@ dependencies = [
"pathfinder_geometry",
"png",
"postage",
"profiling",
"rand 0.8.5",
"raw-window-handle 0.5.2",
"raw-window-handle 0.6.0",
@@ -4340,11 +4348,11 @@ dependencies = [
[[package]]
name = "home"
version = "0.5.5"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb"
checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
dependencies = [
"windows-sys 0.48.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -6860,6 +6868,25 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "profiling"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58"
dependencies = [
"profiling-procmacros",
]
[[package]]
name = "profiling-procmacros"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd"
dependencies = [
"quote",
"syn 2.0.48",
]
[[package]]
name = "project"
version = "0.1.0"
@@ -6915,6 +6942,7 @@ dependencies = [
"toml 0.8.10",
"unindent",
"util",
"which 6.0.0",
]
[[package]]
@@ -7024,7 +7052,7 @@ dependencies = [
"prost-types 0.9.0",
"regex",
"tempfile",
"which",
"which 4.4.2",
]
[[package]]
@@ -7316,6 +7344,7 @@ dependencies = [
"fuzzy",
"gpui",
"language",
"menu",
"ordered-float 2.10.0",
"picker",
"postage",
@@ -8519,9 +8548,9 @@ dependencies = [
[[package]]
name = "shlex"
version = "1.3.0"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380"
[[package]]
name = "signal-hook"
@@ -10374,8 +10403,8 @@ dependencies = [
[[package]]
name = "tree-sitter-purescript"
version = "1.0.0"
source = "git+https://github.com/ivanmoreau/tree-sitter-purescript?rev=a37140f0c7034977b90faa73c94fcb8a5e45ed08#a37140f0c7034977b90faa73c94fcb8a5e45ed08"
version = "0.1.0"
source = "git+https://github.com/postsolar/tree-sitter-purescript?rev=v0.1.0#0554811a512b9cec08b5a83ce9096eb22da18213"
dependencies = [
"cc",
"tree-sitter",
@@ -10900,6 +10929,7 @@ dependencies = [
"project",
"regex",
"release_channel",
"schemars",
"search",
"serde",
"serde_derive",
@@ -11396,6 +11426,19 @@ dependencies = [
"rustix 0.38.30",
]
[[package]]
name = "which"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fa5e0c10bf77f44aac573e498d1a82d5fbd5e91f6fc0a99e7be4b38e85e101c"
dependencies = [
"either",
"home",
"once_cell",
"rustix 0.38.30",
"windows-sys 0.52.0",
]
[[package]]
name = "whoami"
version = "1.4.1"
@@ -11707,6 +11750,7 @@ dependencies = [
"bincode",
"call",
"client",
"clock",
"collections",
"db",
"derive_more",
@@ -11931,6 +11975,7 @@ dependencies = [
"chrono",
"cli",
"client",
"clock",
"collab_ui",
"collections",
"command_palette",
@@ -11974,6 +12019,7 @@ dependencies = [
"outline",
"parking_lot 0.11.2",
"postage",
"profiling",
"project",
"project_panel",
"project_symbols",

View File

@@ -203,6 +203,7 @@ linkify = "0.10.0"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
ordered-float = "2.1.1"
parking_lot = "0.11.1"
profiling = "1"
postage = { version = "0.5", features = ["futures-traits"] }
pretty_assertions = "1.3.0"
prost = "0.8"
@@ -261,7 +262,7 @@ tree-sitter-ocaml = { git = "https://github.com/tree-sitter/tree-sitter-ocaml",
tree-sitter-php = "0.21.1"
tree-sitter-prisma-io = { git = "https://github.com/victorhqc/tree-sitter-prisma" }
tree-sitter-proto = { git = "https://github.com/rewinfrey/tree-sitter-proto", rev = "36d54f288aee112f13a67b550ad32634d0c2cb52" }
tree-sitter-purescript = { git = "https://github.com/ivanmoreau/tree-sitter-purescript", rev = "a37140f0c7034977b90faa73c94fcb8a5e45ed08" }
tree-sitter-purescript = { git = "https://github.com/postsolar/tree-sitter-purescript", rev = "v0.1.0" }
tree-sitter-python = "0.20.2"
tree-sitter-racket = { git = "https://github.com/zed-industries/tree-sitter-racket", rev = "eb010cf2c674c6fd9a6316a84e28ef90190fe51a" }
tree-sitter-ruby = "0.20.0"
@@ -278,6 +279,7 @@ unindent = "0.1.7"
url = "2.2"
uuid = { version = "1.1.2", features = ["v4"] }
wasmtime = "16"
which = "6.0.0"
sys-locale = "0.3.1"
[patch.crates-io]

View File

@@ -531,7 +531,8 @@
"alt-cmd-shift-c": "project_panel::CopyRelativePath",
"f2": "project_panel::Rename",
"enter": "project_panel::Rename",
"backspace": "project_panel::Delete",
"delete": "project_panel::Delete",
"cmd-backspace": "project_panel::Delete",
"alt-cmd-r": "project_panel::RevealInFinder",
"alt-shift-f": "project_panel::NewSearchInDirectory"
}

View File

@@ -0,0 +1,23 @@
[
// Standard macOS bindings
{
"bindings": {
"up": "menu::SelectPrev",
"pageup": "menu::SelectFirst",
"shift-pageup": "menu::SelectFirst",
"ctrl-p": "menu::SelectPrev",
"down": "menu::SelectNext",
"pagedown": "menu::SelectLast",
"shift-pagedown": "menu::SelectFirst",
"ctrl-n": "menu::SelectNext",
"cmd-up": "menu::SelectFirst",
"cmd-down": "menu::SelectLast",
"enter": "menu::Confirm",
"ctrl-enter": "menu::ShowContextMenu",
"cmd-enter": "menu::SecondaryConfirm",
"escape": "menu::Cancel",
"ctrl-c": "menu::Cancel",
"cmd-q": "storybook::Quit"
}
}
]

View File

@@ -101,8 +101,14 @@
"ctrl-o": "pane::GoBack",
"ctrl-i": "pane::GoForward",
"ctrl-]": "editor::GoToDefinition",
"escape": ["vim::SwitchMode", "Normal"],
"ctrl-[": ["vim::SwitchMode", "Normal"],
"escape": [
"vim::SwitchMode",
"Normal"
],
"ctrl-[": [
"vim::SwitchMode",
"Normal"
],
"v": "vim::ToggleVisual",
"shift-v": "vim::ToggleVisualLine",
"ctrl-v": "vim::ToggleVisualBlock",
@@ -235,36 +241,123 @@
}
],
// Count support
"1": ["vim::Number", 1],
"2": ["vim::Number", 2],
"3": ["vim::Number", 3],
"4": ["vim::Number", 4],
"5": ["vim::Number", 5],
"6": ["vim::Number", 6],
"7": ["vim::Number", 7],
"8": ["vim::Number", 8],
"9": ["vim::Number", 9],
"1": [
"vim::Number",
1
],
"2": [
"vim::Number",
2
],
"3": [
"vim::Number",
3
],
"4": [
"vim::Number",
4
],
"5": [
"vim::Number",
5
],
"6": [
"vim::Number",
6
],
"7": [
"vim::Number",
7
],
"8": [
"vim::Number",
8
],
"9": [
"vim::Number",
9
],
// window related commands (ctrl-w X)
"ctrl-w left": ["workspace::ActivatePaneInDirection", "Left"],
"ctrl-w right": ["workspace::ActivatePaneInDirection", "Right"],
"ctrl-w up": ["workspace::ActivatePaneInDirection", "Up"],
"ctrl-w down": ["workspace::ActivatePaneInDirection", "Down"],
"ctrl-w h": ["workspace::ActivatePaneInDirection", "Left"],
"ctrl-w l": ["workspace::ActivatePaneInDirection", "Right"],
"ctrl-w k": ["workspace::ActivatePaneInDirection", "Up"],
"ctrl-w j": ["workspace::ActivatePaneInDirection", "Down"],
"ctrl-w ctrl-h": ["workspace::ActivatePaneInDirection", "Left"],
"ctrl-w ctrl-l": ["workspace::ActivatePaneInDirection", "Right"],
"ctrl-w ctrl-k": ["workspace::ActivatePaneInDirection", "Up"],
"ctrl-w ctrl-j": ["workspace::ActivatePaneInDirection", "Down"],
"ctrl-w shift-left": ["workspace::SwapPaneInDirection", "Left"],
"ctrl-w shift-right": ["workspace::SwapPaneInDirection", "Right"],
"ctrl-w shift-up": ["workspace::SwapPaneInDirection", "Up"],
"ctrl-w shift-down": ["workspace::SwapPaneInDirection", "Down"],
"ctrl-w shift-h": ["workspace::SwapPaneInDirection", "Left"],
"ctrl-w shift-l": ["workspace::SwapPaneInDirection", "Right"],
"ctrl-w shift-k": ["workspace::SwapPaneInDirection", "Up"],
"ctrl-w shift-j": ["workspace::SwapPaneInDirection", "Down"],
"ctrl-w left": [
"workspace::ActivatePaneInDirection",
"Left"
],
"ctrl-w right": [
"workspace::ActivatePaneInDirection",
"Right"
],
"ctrl-w up": [
"workspace::ActivatePaneInDirection",
"Up"
],
"ctrl-w down": [
"workspace::ActivatePaneInDirection",
"Down"
],
"ctrl-w h": [
"workspace::ActivatePaneInDirection",
"Left"
],
"ctrl-w l": [
"workspace::ActivatePaneInDirection",
"Right"
],
"ctrl-w k": [
"workspace::ActivatePaneInDirection",
"Up"
],
"ctrl-w j": [
"workspace::ActivatePaneInDirection",
"Down"
],
"ctrl-w ctrl-h": [
"workspace::ActivatePaneInDirection",
"Left"
],
"ctrl-w ctrl-l": [
"workspace::ActivatePaneInDirection",
"Right"
],
"ctrl-w ctrl-k": [
"workspace::ActivatePaneInDirection",
"Up"
],
"ctrl-w ctrl-j": [
"workspace::ActivatePaneInDirection",
"Down"
],
"ctrl-w shift-left": [
"workspace::SwapPaneInDirection",
"Left"
],
"ctrl-w shift-right": [
"workspace::SwapPaneInDirection",
"Right"
],
"ctrl-w shift-up": [
"workspace::SwapPaneInDirection",
"Up"
],
"ctrl-w shift-down": [
"workspace::SwapPaneInDirection",
"Down"
],
"ctrl-w shift-h": [
"workspace::SwapPaneInDirection",
"Left"
],
"ctrl-w shift-l": [
"workspace::SwapPaneInDirection",
"Right"
],
"ctrl-w shift-k": [
"workspace::SwapPaneInDirection",
"Up"
],
"ctrl-w shift-j": [
"workspace::SwapPaneInDirection",
"Down"
],
"ctrl-w g t": "pane::ActivateNextItem",
"ctrl-w ctrl-g t": "pane::ActivateNextItem",
"ctrl-w g shift-t": "pane::ActivatePrevItem",
@@ -286,8 +379,14 @@
"ctrl-w ctrl-q": "pane::CloseAllItems",
"ctrl-w o": "workspace::CloseInactiveTabsAndPanes",
"ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes",
"ctrl-w n": ["workspace::NewFileInDirection", "Up"],
"ctrl-w ctrl-n": ["workspace::NewFileInDirection", "Up"],
"ctrl-w n": [
"workspace::NewFileInDirection",
"Up"
],
"ctrl-w ctrl-n": [
"workspace::NewFileInDirection",
"Up"
],
"-": "pane::RevealInProjectPanel"
}
},
@@ -303,12 +402,21 @@
"context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
"bindings": {
".": "vim::Repeat",
"c": ["vim::PushOperator", "Change"],
"c": [
"vim::PushOperator",
"Change"
],
"shift-c": "vim::ChangeToEndOfLine",
"d": ["vim::PushOperator", "Delete"],
"d": [
"vim::PushOperator",
"Delete"
],
"shift-d": "vim::DeleteToEndOfLine",
"shift-j": "vim::JoinLines",
"y": ["vim::PushOperator", "Yank"],
"y": [
"vim::PushOperator",
"Yank"
],
"shift-y": "vim::YankLine",
"i": "vim::InsertBefore",
"shift-i": "vim::InsertFirstNonWhitespace",
@@ -339,7 +447,10 @@
],
"*": "vim::MoveToNext",
"#": "vim::MoveToPrev",
"r": ["vim::PushOperator", "Replace"],
"r": [
"vim::PushOperator",
"Replace"
],
"s": "vim::Substitute",
"shift-s": "vim::SubstituteLine",
"> >": "editor::Indent",
@@ -351,7 +462,10 @@
{
"context": "Editor && VimCount",
"bindings": {
"0": ["vim::Number", 0]
"0": [
"vim::Number",
0
]
}
},
{
@@ -454,10 +568,22 @@
"shift-i": "vim::InsertBefore",
"shift-a": "vim::InsertAfter",
"shift-j": "vim::JoinLines",
"r": ["vim::PushOperator", "Replace"],
"ctrl-c": ["vim::SwitchMode", "Normal"],
"escape": ["vim::SwitchMode", "Normal"],
"ctrl-[": ["vim::SwitchMode", "Normal"],
"r": [
"vim::PushOperator",
"Replace"
],
"ctrl-c": [
"vim::SwitchMode",
"Normal"
],
"escape": [
"vim::SwitchMode",
"Normal"
],
"ctrl-[": [
"vim::SwitchMode",
"Normal"
],
">": "editor::Indent",
"<": "editor::Outdent",
"i": [
@@ -498,8 +624,14 @@
"bindings": {
"tab": "vim::Tab",
"enter": "vim::Enter",
"escape": ["vim::SwitchMode", "Normal"],
"ctrl-[": ["vim::SwitchMode", "Normal"]
"escape": [
"vim::SwitchMode",
"Normal"
],
"ctrl-[": [
"vim::SwitchMode",
"Normal"
]
}
},
{

View File

@@ -140,6 +140,14 @@
// Whether to show diagnostic indicators in the scrollbar.
"diagnostics": true
},
"gutter": {
// Whether to show line numbers in the gutter.
"line_numbers": true,
// Whether to show code action buttons in the gutter.
"code_actions": true,
// Whether to show fold buttons in the gutter.
"folds": true
},
// The number of lines to keep above/below the cursor when scrolling.
"vertical_scroll_margin": 3,
"relative_line_numbers": false,
@@ -331,7 +339,9 @@
"copilot": {
// The set of glob patterns for which copilot should be disabled
// in any matching file.
"disabled_globs": [".env"]
"disabled_globs": [
".env"
]
},
// Settings specific to journaling
"journal": {
@@ -440,7 +450,12 @@
// Default directories to search for virtual environments, relative
// to the current working directory. We recommend overriding this
// in your project's settings, rather than globally.
"directories": [".env", "env", ".venv", "venv"],
"directories": [
".env",
"env",
".venv",
"venv"
],
// Can also be 'csh', 'fish', and `nushell`
"activate_script": "default"
}
@@ -555,6 +570,10 @@
// }
// }
},
// Vim settings
"vim": {
"use_system_clipboard": "always"
},
// The server to connect to. If the environment variable
// ZED_SERVER_URL is set, it will override this setting.
"server_url": "https://zed.dev",

View File

@@ -122,16 +122,13 @@ impl AssistantPanel {
.await
.log_err()
.unwrap_or_default();
let (api_url, model_name) = cx
.update(|cx| {
let settings = AssistantSettings::get_global(cx);
(
settings.openai_api_url.clone(),
settings.default_open_ai_model.full_name().to_string(),
)
})
.log_err()
.unwrap();
let (api_url, model_name) = cx.update(|cx| {
let settings = AssistantSettings::get_global(cx);
(
settings.openai_api_url.clone(),
settings.default_open_ai_model.full_name().to_string(),
)
})?;
let completion_provider = OpenAiCompletionProvider::new(
api_url,
model_name,
@@ -365,7 +362,7 @@ impl AssistantPanel {
move |cx: &mut BlockContext| {
measurements.set(BlockMeasurements {
anchor_x: cx.anchor_x,
gutter_width: cx.gutter_width,
gutter_width: cx.gutter_dimensions.width,
});
inline_assistant.clone().into_any_element()
}

View File

@@ -13,10 +13,12 @@ doctest = false
anyhow.workspace = true
client.workspace = true
db.workspace = true
editor.workspace = true
gpui.workspace = true
isahc.workspace = true
lazy_static.workspace = true
log.workspace = true
markdown_preview.workspace = true
menu.workspace = true
project.workspace = true
release_channel.workspace = true

View File

@@ -4,12 +4,14 @@ use anyhow::{anyhow, Context, Result};
use client::{Client, TelemetrySettings, ZED_APP_PATH};
use db::kvp::KEY_VALUE_STORE;
use db::RELEASE_CHANNEL;
use editor::{Editor, MultiBuffer};
use gpui::{
actions, AppContext, AsyncAppContext, Context as _, Global, Model, ModelContext,
SemanticVersion, Task, ViewContext, VisualContext, WindowContext,
SemanticVersion, SharedString, Task, View, ViewContext, VisualContext, WindowContext,
};
use isahc::AsyncBody;
use markdown_preview::markdown_preview_view::MarkdownPreviewView;
use schemars::JsonSchema;
use serde::Deserialize;
use serde_derive::Serialize;
@@ -26,13 +28,24 @@ use std::{
time::Duration,
};
use update_notification::UpdateNotification;
use util::http::{HttpClient, ZedHttpClient};
use util::{
http::{HttpClient, ZedHttpClient},
ResultExt,
};
use workspace::Workspace;
const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes]);
actions!(
auto_update,
[
Check,
DismissErrorMessage,
ViewReleaseNotes,
ViewReleaseNotesLocally
]
);
#[derive(Serialize)]
struct UpdateRequestBody {
@@ -96,6 +109,12 @@ struct GlobalAutoUpdate(Option<Model<AutoUpdater>>);
impl Global for GlobalAutoUpdate {}
#[derive(Deserialize)]
struct ReleaseNotesBody {
title: String,
release_notes: String,
}
pub fn init(http_client: Arc<ZedHttpClient>, cx: &mut AppContext) {
AutoUpdateSetting::register(cx);
@@ -105,6 +124,10 @@ pub fn init(http_client: Arc<ZedHttpClient>, cx: &mut AppContext) {
workspace.register_action(|_, action, cx| {
view_release_notes(action, cx);
});
workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, cx| {
view_release_notes_locally(workspace, cx);
});
})
.detach();
@@ -165,6 +188,71 @@ pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) -> Option<(
None
}
fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
let release_channel = ReleaseChannel::global(cx);
let version = env!("CARGO_PKG_VERSION");
let client = client::Client::global(cx).http_client();
let url = client.zed_url(&format!(
"/api/release_notes/{}/{}",
release_channel.dev_name(),
version
));
let markdown = workspace
.app_state()
.languages
.language_for_name("Markdown");
workspace
.with_local_workspace(cx, move |_, cx| {
cx.spawn(|workspace, mut cx| async move {
let markdown = markdown.await.log_err();
let response = client.get(&url, Default::default(), true).await;
let Some(mut response) = response.log_err() else {
return;
};
let mut body = Vec::new();
response.body_mut().read_to_end(&mut body).await.ok();
let body: serde_json::Result<ReleaseNotesBody> =
serde_json::from_slice(body.as_slice());
if let Ok(body) = body {
workspace
.update(&mut cx, |workspace, cx| {
let project = workspace.project().clone();
let buffer = project
.update(cx, |project, cx| project.create_buffer("", markdown, cx))
.expect("creating buffers on a local workspace always succeeds");
buffer.update(cx, |buffer, cx| {
buffer.edit([(0..0, body.release_notes)], None, cx)
});
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let tab_description = SharedString::from(body.title.to_string());
let editor = cx
.new_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx));
let workspace_handle = workspace.weak_handle();
let view: View<MarkdownPreviewView> = MarkdownPreviewView::new(
editor,
workspace_handle,
Some(tab_description),
cx,
);
workspace.add_item(Box::new(view.clone()), cx);
cx.notify();
})
.log_err();
}
})
.detach();
})
.detach();
}
pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> {
let updater = AutoUpdater::get(cx)?;
let version = updater.read(cx).current_version;

View File

@@ -156,7 +156,7 @@ impl Room {
cx.spawn(|this, mut cx| async move {
connect.await?;
this.update(&mut cx, |this, cx| {
if !this.read_only() {
if this.can_use_microphone() {
if let Some(live_kit) = &this.live_kit {
if !live_kit.muted_by_user && !live_kit.deafened {
return this.share_microphone(cx);
@@ -1322,11 +1322,6 @@ impl Room {
})
}
pub fn read_only(&self) -> bool {
!(self.local_participant().role == proto::ChannelRole::Member
|| self.local_participant().role == proto::ChannelRole::Admin)
}
pub fn is_speaking(&self) -> bool {
self.live_kit
.as_ref()
@@ -1337,6 +1332,22 @@ impl Room {
self.live_kit.as_ref().map(|live_kit| live_kit.deafened)
}
pub fn can_use_microphone(&self) -> bool {
use proto::ChannelRole::*;
match self.local_participant.role {
Admin | Member | Talker => true,
Guest | Banned => false,
}
}
pub fn can_share_projects(&self) -> bool {
use proto::ChannelRole::*;
match self.local_participant.role {
Admin | Member => true,
Guest | Banned | Talker => false,
}
}
#[track_caller]
pub fn share_microphone(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if self.status.is_offline() {

View File

@@ -120,7 +120,8 @@ impl ChannelMembership {
proto::ChannelRole::Admin => 0,
proto::ChannelRole::Member => 1,
proto::ChannelRole::Banned => 2,
proto::ChannelRole::Guest => 3,
proto::ChannelRole::Talker => 3,
proto::ChannelRole::Guest => 4,
},
kind_order: match self.kind {
proto::channel_member::Kind::Member => 0,

View File

@@ -2,6 +2,7 @@ use crate::channel_chat::ChannelChatEvent;
use super::*;
use client::{test::FakeServer, Client, UserStore};
use clock::FakeSystemClock;
use gpui::{AppContext, Context, Model, TestAppContext};
use rpc::proto::{self};
use settings::SettingsStore;
@@ -337,8 +338,9 @@ fn init_test(cx: &mut AppContext) -> Model<ChannelStore> {
release_channel::init("0.0.0", cx);
client::init_settings(cx);
let clock = Arc::new(FakeSystemClock::default());
let http = FakeHttpClient::with_404_response();
let client = Client::new(http.clone(), cx);
let client = Client::new(clock, http.clone(), cx);
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
client::init(&client, cx);

View File

@@ -10,10 +10,11 @@ path = "src/client.rs"
doctest = false
[features]
test-support = ["collections/test-support", "gpui/test-support", "rpc/test-support"]
test-support = ["clock/test-support", "collections/test-support", "gpui/test-support", "rpc/test-support"]
[dependencies]
chrono = { version = "0.4", features = ["serde"] }
clock.workspace = true
collections.workspace = true
db.workspace = true
gpui.workspace = true
@@ -51,6 +52,7 @@ uuid.workspace = true
url.workspace = true
[dev-dependencies]
clock = { workspace = true, features = ["test-support"] }
collections = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
rpc = { workspace = true, features = ["test-support"] }

View File

@@ -10,6 +10,7 @@ use async_tungstenite::tungstenite::{
error::Error as WebsocketError,
http::{Request, StatusCode},
};
use clock::SystemClock;
use collections::HashMap;
use futures::{
channel::oneshot, future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt,
@@ -421,11 +422,15 @@ impl settings::Settings for TelemetrySettings {
}
impl Client {
pub fn new(http: Arc<ZedHttpClient>, cx: &mut AppContext) -> Arc<Self> {
pub fn new(
clock: Arc<dyn SystemClock>,
http: Arc<ZedHttpClient>,
cx: &mut AppContext,
) -> Arc<Self> {
let client = Arc::new(Self {
id: AtomicU64::new(0),
peer: Peer::new(0),
telemetry: Telemetry::new(http.clone(), cx),
telemetry: Telemetry::new(clock, http.clone(), cx),
http,
state: Default::default(),
@@ -1455,6 +1460,7 @@ mod tests {
use super::*;
use crate::test::FakeServer;
use clock::FakeSystemClock;
use gpui::{BackgroundExecutor, Context, TestAppContext};
use parking_lot::Mutex;
use settings::SettingsStore;
@@ -1465,7 +1471,13 @@ mod tests {
async fn test_reconnection(cx: &mut TestAppContext) {
init_test(cx);
let user_id = 5;
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let client = cx.update(|cx| {
Client::new(
Arc::new(FakeSystemClock::default()),
FakeHttpClient::with_404_response(),
cx,
)
});
let server = FakeServer::for_client(user_id, &client, cx).await;
let mut status = client.status();
assert!(matches!(
@@ -1500,7 +1512,13 @@ mod tests {
async fn test_connection_timeout(executor: BackgroundExecutor, cx: &mut TestAppContext) {
init_test(cx);
let user_id = 5;
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let client = cx.update(|cx| {
Client::new(
Arc::new(FakeSystemClock::default()),
FakeHttpClient::with_404_response(),
cx,
)
});
let mut status = client.status();
// Time out when client tries to connect.
@@ -1573,7 +1591,13 @@ mod tests {
init_test(cx);
let auth_count = Arc::new(Mutex::new(0));
let dropped_auth_count = Arc::new(Mutex::new(0));
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let client = cx.update(|cx| {
Client::new(
Arc::new(FakeSystemClock::default()),
FakeHttpClient::with_404_response(),
cx,
)
});
client.override_authenticate({
let auth_count = auth_count.clone();
let dropped_auth_count = dropped_auth_count.clone();
@@ -1621,7 +1645,13 @@ mod tests {
async fn test_subscribing_to_entity(cx: &mut TestAppContext) {
init_test(cx);
let user_id = 5;
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let client = cx.update(|cx| {
Client::new(
Arc::new(FakeSystemClock::default()),
FakeHttpClient::with_404_response(),
cx,
)
});
let server = FakeServer::for_client(user_id, &client, cx).await;
let (done_tx1, mut done_rx1) = smol::channel::unbounded();
@@ -1675,7 +1705,13 @@ mod tests {
async fn test_subscribing_after_dropping_subscription(cx: &mut TestAppContext) {
init_test(cx);
let user_id = 5;
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let client = cx.update(|cx| {
Client::new(
Arc::new(FakeSystemClock::default()),
FakeHttpClient::with_404_response(),
cx,
)
});
let server = FakeServer::for_client(user_id, &client, cx).await;
let model = cx.new_model(|_| TestModel::default());
@@ -1704,7 +1740,13 @@ mod tests {
async fn test_dropping_subscription_in_handler(cx: &mut TestAppContext) {
init_test(cx);
let user_id = 5;
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let client = cx.update(|cx| {
Client::new(
Arc::new(FakeSystemClock::default()),
FakeHttpClient::with_404_response(),
cx,
)
});
let server = FakeServer::for_client(user_id, &client, cx).await;
let model = cx.new_model(|_| TestModel::default());

View File

@@ -2,6 +2,7 @@ mod event_coalescer;
use crate::TelemetrySettings;
use chrono::{DateTime, Utc};
use clock::SystemClock;
use futures::Future;
use gpui::{AppContext, AppMetadata, BackgroundExecutor, Task};
use once_cell::sync::Lazy;
@@ -24,6 +25,7 @@ use util::TryFutureExt;
use self::event_coalescer::EventCoalescer;
pub struct Telemetry {
clock: Arc<dyn SystemClock>,
http_client: Arc<ZedHttpClient>,
executor: BackgroundExecutor,
state: Arc<Mutex<TelemetryState>>,
@@ -156,7 +158,11 @@ static ZED_CLIENT_CHECKSUM_SEED: Lazy<Option<Vec<u8>>> = Lazy::new(|| {
});
impl Telemetry {
pub fn new(client: Arc<ZedHttpClient>, cx: &mut AppContext) -> Arc<Self> {
pub fn new(
clock: Arc<dyn SystemClock>,
client: Arc<ZedHttpClient>,
cx: &mut AppContext,
) -> Arc<Self> {
let release_channel =
ReleaseChannel::try_global(cx).map(|release_channel| release_channel.display_name());
@@ -205,6 +211,7 @@ impl Telemetry {
// TODO: Replace all hardware stuff with nested SystemSpecs json
let this = Arc::new(Self {
clock,
http_client: client,
executor: cx.background_executor().clone(),
state,
@@ -317,7 +324,8 @@ impl Telemetry {
operation,
copilot_enabled,
copilot_enabled_for_language,
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
milliseconds_since_first_event: self
.milliseconds_since_first_event(self.clock.utc_now()),
};
self.report_event(event)
@@ -333,7 +341,8 @@ impl Telemetry {
suggestion_id,
suggestion_accepted,
file_extension,
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
milliseconds_since_first_event: self
.milliseconds_since_first_event(self.clock.utc_now()),
};
self.report_event(event)
@@ -349,7 +358,8 @@ impl Telemetry {
conversation_id,
kind,
model,
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
milliseconds_since_first_event: self
.milliseconds_since_first_event(self.clock.utc_now()),
};
self.report_event(event)
@@ -365,7 +375,8 @@ impl Telemetry {
operation,
room_id,
channel_id,
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
milliseconds_since_first_event: self
.milliseconds_since_first_event(self.clock.utc_now()),
};
self.report_event(event)
@@ -375,7 +386,8 @@ impl Telemetry {
let event = Event::Cpu {
usage_as_percentage,
core_count,
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
milliseconds_since_first_event: self
.milliseconds_since_first_event(self.clock.utc_now()),
};
self.report_event(event)
@@ -389,24 +401,18 @@ impl Telemetry {
let event = Event::Memory {
memory_in_bytes,
virtual_memory_in_bytes,
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
milliseconds_since_first_event: self
.milliseconds_since_first_event(self.clock.utc_now()),
};
self.report_event(event)
}
pub fn report_app_event(self: &Arc<Self>, operation: String) {
self.report_app_event_with_date_time(operation, Utc::now());
}
fn report_app_event_with_date_time(
self: &Arc<Self>,
operation: String,
date_time: DateTime<Utc>,
) -> Event {
pub fn report_app_event(self: &Arc<Self>, operation: String) -> Event {
let event = Event::App {
operation,
milliseconds_since_first_event: self.milliseconds_since_first_event(date_time),
milliseconds_since_first_event: self
.milliseconds_since_first_event(self.clock.utc_now()),
};
self.report_event(event.clone());
@@ -418,7 +424,8 @@ impl Telemetry {
let event = Event::Setting {
setting,
value,
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
milliseconds_since_first_event: self
.milliseconds_since_first_event(self.clock.utc_now()),
};
self.report_event(event)
@@ -433,7 +440,8 @@ impl Telemetry {
let event = Event::Edit {
duration: end.timestamp_millis() - start.timestamp_millis(),
environment,
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
milliseconds_since_first_event: self
.milliseconds_since_first_event(self.clock.utc_now()),
};
self.report_event(event);
@@ -444,7 +452,8 @@ impl Telemetry {
let event = Event::Action {
source,
action,
milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
milliseconds_since_first_event: self
.milliseconds_since_first_event(self.clock.utc_now()),
};
self.report_event(event)
@@ -590,29 +599,32 @@ impl Telemetry {
mod tests {
use super::*;
use chrono::TimeZone;
use clock::FakeSystemClock;
use gpui::TestAppContext;
use util::http::FakeHttpClient;
#[gpui::test]
fn test_telemetry_flush_on_max_queue_size(cx: &mut TestAppContext) {
init_test(cx);
let clock = Arc::new(FakeSystemClock::new(
Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap(),
));
let http = FakeHttpClient::with_200_response();
let installation_id = Some("installation_id".to_string());
let session_id = "session_id".to_string();
cx.update(|cx| {
let telemetry = Telemetry::new(http, cx);
let telemetry = Telemetry::new(clock.clone(), http, cx);
telemetry.state.lock().max_queue_size = 4;
telemetry.start(installation_id, session_id, cx);
assert!(is_empty_state(&telemetry));
let first_date_time = Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap();
let first_date_time = clock.utc_now();
let operation = "test".to_string();
let event =
telemetry.report_app_event_with_date_time(operation.clone(), first_date_time);
let event = telemetry.report_app_event(operation.clone());
assert_eq!(
event,
Event::App {
@@ -627,9 +639,9 @@ mod tests {
Some(first_date_time)
);
let mut date_time = first_date_time + chrono::Duration::milliseconds(100);
clock.advance(chrono::Duration::milliseconds(100));
let event = telemetry.report_app_event_with_date_time(operation.clone(), date_time);
let event = telemetry.report_app_event(operation.clone());
assert_eq!(
event,
Event::App {
@@ -644,9 +656,9 @@ mod tests {
Some(first_date_time)
);
date_time += chrono::Duration::milliseconds(100);
clock.advance(chrono::Duration::milliseconds(100));
let event = telemetry.report_app_event_with_date_time(operation.clone(), date_time);
let event = telemetry.report_app_event(operation.clone());
assert_eq!(
event,
Event::App {
@@ -661,10 +673,10 @@ mod tests {
Some(first_date_time)
);
date_time += chrono::Duration::milliseconds(100);
clock.advance(chrono::Duration::milliseconds(100));
// Adding a 4th event should cause a flush
let event = telemetry.report_app_event_with_date_time(operation.clone(), date_time);
let event = telemetry.report_app_event(operation.clone());
assert_eq!(
event,
Event::App {
@@ -680,22 +692,24 @@ mod tests {
#[gpui::test]
async fn test_connection_timeout(executor: BackgroundExecutor, cx: &mut TestAppContext) {
init_test(cx);
let clock = Arc::new(FakeSystemClock::new(
Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap(),
));
let http = FakeHttpClient::with_200_response();
let installation_id = Some("installation_id".to_string());
let session_id = "session_id".to_string();
cx.update(|cx| {
let telemetry = Telemetry::new(http, cx);
let telemetry = Telemetry::new(clock.clone(), http, cx);
telemetry.state.lock().max_queue_size = 4;
telemetry.start(installation_id, session_id, cx);
assert!(is_empty_state(&telemetry));
let first_date_time = Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap();
let first_date_time = clock.utc_now();
let operation = "test".to_string();
let event =
telemetry.report_app_event_with_date_time(operation.clone(), first_date_time);
let event = telemetry.report_app_event(operation.clone());
assert_eq!(
event,
Event::App {

View File

@@ -9,5 +9,10 @@ license = "GPL-3.0-or-later"
path = "src/clock.rs"
doctest = false
[features]
test-support = ["dep:parking_lot"]
[dependencies]
chrono.workspace = true
parking_lot = { workspace = true, optional = true }
smallvec.workspace = true

View File

@@ -1,13 +1,17 @@
mod system_clock;
use smallvec::SmallVec;
use std::{
cmp::{self, Ordering},
fmt, iter,
};
/// A unique identifier for each distributed node
pub use system_clock::*;
/// A unique identifier for each distributed node.
pub type ReplicaId = u16;
/// A [Lamport sequence number](https://en.wikipedia.org/wiki/Lamport_timestamp),
/// A [Lamport sequence number](https://en.wikipedia.org/wiki/Lamport_timestamp).
pub type Seq = u32;
/// A [Lamport timestamp](https://en.wikipedia.org/wiki/Lamport_timestamp),
@@ -18,7 +22,7 @@ pub struct Lamport {
pub value: Seq,
}
/// A [vector clock](https://en.wikipedia.org/wiki/Vector_clock)
/// A [vector clock](https://en.wikipedia.org/wiki/Vector_clock).
#[derive(Clone, Default, Hash, Eq, PartialEq)]
pub struct Global(SmallVec<[u32; 8]>);

View File

@@ -0,0 +1,59 @@
use chrono::{DateTime, Utc};
pub trait SystemClock: Send + Sync {
/// Returns the current date and time in UTC.
fn utc_now(&self) -> DateTime<Utc>;
}
pub struct RealSystemClock;
impl SystemClock for RealSystemClock {
fn utc_now(&self) -> DateTime<Utc> {
Utc::now()
}
}
#[cfg(any(test, feature = "test-support"))]
pub struct FakeSystemClockState {
now: DateTime<Utc>,
}
#[cfg(any(test, feature = "test-support"))]
pub struct FakeSystemClock {
// Use an unfair lock to ensure tests are deterministic.
state: parking_lot::Mutex<FakeSystemClockState>,
}
#[cfg(any(test, feature = "test-support"))]
impl Default for FakeSystemClock {
fn default() -> Self {
Self::new(Utc::now())
}
}
#[cfg(any(test, feature = "test-support"))]
impl FakeSystemClock {
pub fn new(now: DateTime<Utc>) -> Self {
let state = FakeSystemClockState { now };
Self {
state: parking_lot::Mutex::new(state),
}
}
pub fn set_now(&self, now: DateTime<Utc>) {
self.state.lock().now = now;
}
/// Advances the [`FakeSystemClock`] by the specified [`Duration`](chrono::Duration).
pub fn advance(&self, duration: chrono::Duration) {
self.state.lock().now += duration;
}
}
#[cfg(any(test, feature = "test-support"))]
impl SystemClock for FakeSystemClock {
fn utc_now(&self) -> DateTime<Utc> {
self.state.lock().now
}
}

View File

@@ -56,7 +56,7 @@ async fn get_extensions(
Extension(app): Extension<Arc<AppState>>,
Query(params): Query<GetExtensionsParams>,
) -> Result<Json<GetExtensionsResponse>> {
let extensions = app.db.get_extensions(params.filter.as_deref(), 30).await?;
let extensions = app.db.get_extensions(params.filter.as_deref(), 500).await?;
Ok(Json(GetExtensionsResponse { data: extensions }))
}

View File

@@ -100,8 +100,12 @@ pub enum ChannelRole {
#[sea_orm(string_value = "member")]
#[default]
Member,
/// Talker can read, but not write.
/// They can use microphones and the channel chat
#[sea_orm(string_value = "talker")]
Talker,
/// Guest can read, but not write.
/// (thought they can use the channel chat)
/// They can not use microphones but can use the chat.
#[sea_orm(string_value = "guest")]
Guest,
/// Banned may not read.
@@ -114,8 +118,9 @@ impl ChannelRole {
pub fn should_override(&self, other: Self) -> bool {
use ChannelRole::*;
match self {
Admin => matches!(other, Member | Banned | Guest),
Member => matches!(other, Banned | Guest),
Admin => matches!(other, Member | Banned | Talker | Guest),
Member => matches!(other, Banned | Talker | Guest),
Talker => matches!(other, Guest),
Banned => matches!(other, Guest),
Guest => false,
}
@@ -134,7 +139,7 @@ impl ChannelRole {
use ChannelRole::*;
match self {
Admin | Member => true,
Guest => visibility == ChannelVisibility::Public,
Guest | Talker => visibility == ChannelVisibility::Public,
Banned => false,
}
}
@@ -144,7 +149,7 @@ impl ChannelRole {
use ChannelRole::*;
match self {
Admin | Member => true,
Guest | Banned => false,
Guest | Talker | Banned => false,
}
}
@@ -152,16 +157,16 @@ impl ChannelRole {
pub fn can_only_see_public_descendants(&self) -> bool {
use ChannelRole::*;
match self {
Guest => true,
Guest | Talker => true,
Admin | Member | Banned => false,
}
}
/// True if the role can share screen/microphone/projects into rooms.
pub fn can_publish_to_rooms(&self) -> bool {
pub fn can_use_microphone(&self) -> bool {
use ChannelRole::*;
match self {
Admin | Member => true,
Admin | Member | Talker => true,
Guest | Banned => false,
}
}
@@ -171,7 +176,7 @@ impl ChannelRole {
use ChannelRole::*;
match self {
Admin | Member => true,
Guest | Banned => false,
Talker | Guest | Banned => false,
}
}
@@ -179,7 +184,7 @@ impl ChannelRole {
pub fn can_read_projects(&self) -> bool {
use ChannelRole::*;
match self {
Admin | Member | Guest => true,
Admin | Member | Guest | Talker => true,
Banned => false,
}
}
@@ -188,7 +193,7 @@ impl ChannelRole {
use ChannelRole::*;
match self {
Admin | Member => true,
Banned | Guest => false,
Banned | Guest | Talker => false,
}
}
}
@@ -198,6 +203,7 @@ impl From<proto::ChannelRole> for ChannelRole {
match value {
proto::ChannelRole::Admin => ChannelRole::Admin,
proto::ChannelRole::Member => ChannelRole::Member,
proto::ChannelRole::Talker => ChannelRole::Talker,
proto::ChannelRole::Guest => ChannelRole::Guest,
proto::ChannelRole::Banned => ChannelRole::Banned,
}
@@ -209,6 +215,7 @@ impl Into<proto::ChannelRole> for ChannelRole {
match self {
ChannelRole::Admin => proto::ChannelRole::Admin,
ChannelRole::Member => proto::ChannelRole::Member,
ChannelRole::Talker => proto::ChannelRole::Talker,
ChannelRole::Guest => proto::ChannelRole::Guest,
ChannelRole::Banned => proto::ChannelRole::Banned,
}

View File

@@ -795,6 +795,7 @@ impl Database {
match role {
Some(ChannelRole::Admin) => Ok(role.unwrap()),
Some(ChannelRole::Member)
| Some(ChannelRole::Talker)
| Some(ChannelRole::Banned)
| Some(ChannelRole::Guest)
| None => Err(anyhow!(
@@ -813,7 +814,10 @@ impl Database {
let channel_role = self.channel_role_for_user(channel, user_id, tx).await?;
match channel_role {
Some(ChannelRole::Admin) | Some(ChannelRole::Member) => Ok(channel_role.unwrap()),
Some(ChannelRole::Banned) | Some(ChannelRole::Guest) | None => Err(anyhow!(
Some(ChannelRole::Banned)
| Some(ChannelRole::Guest)
| Some(ChannelRole::Talker)
| None => Err(anyhow!(
"user is not a channel member or channel does not exist"
))?,
}
@@ -828,9 +832,10 @@ impl Database {
) -> Result<ChannelRole> {
let role = self.channel_role_for_user(channel, user_id, tx).await?;
match role {
Some(ChannelRole::Admin) | Some(ChannelRole::Member) | Some(ChannelRole::Guest) => {
Ok(role.unwrap())
}
Some(ChannelRole::Admin)
| Some(ChannelRole::Member)
| Some(ChannelRole::Guest)
| Some(ChannelRole::Talker) => Ok(role.unwrap()),
Some(ChannelRole::Banned) | None => Err(anyhow!(
"user is not a channel participant or channel does not exist"
))?,

View File

@@ -51,7 +51,7 @@ impl Database {
if !participant
.role
.unwrap_or(ChannelRole::Member)
.can_publish_to_rooms()
.can_edit_projects()
{
return Err(anyhow!("guests cannot share projects"))?;
}

View File

@@ -169,7 +169,7 @@ impl Database {
let called_user_role = match caller.role.unwrap_or(ChannelRole::Member) {
ChannelRole::Admin | ChannelRole::Member => ChannelRole::Member,
ChannelRole::Guest => ChannelRole::Guest,
ChannelRole::Guest | ChannelRole::Talker => ChannelRole::Guest,
ChannelRole::Banned => return Err(anyhow!("banned users cannot invite").into()),
};

View File

@@ -28,7 +28,7 @@ use axum::{
Extension, Router, TypedHeader,
};
use collections::{HashMap, HashSet};
pub use connection_pool::ConnectionPool;
pub use connection_pool::{ConnectionPool, ZedVersion};
use futures::{
channel::oneshot,
future::{self, BoxFuture},
@@ -558,6 +558,7 @@ impl Server {
connection: Connection,
address: String,
user: User,
zed_version: ZedVersion,
impersonator: Option<User>,
mut send_connection_id: Option<oneshot::Sender<ConnectionId>>,
executor: Executor,
@@ -599,7 +600,7 @@ impl Server {
{
let mut pool = this.connection_pool.lock();
pool.add_connection(connection_id, user_id, user.admin);
pool.add_connection(connection_id, user_id, user.admin, zed_version);
this.peer.send(connection_id, build_initial_contacts_update(contacts, &pool))?;
this.peer.send(connection_id, build_update_user_channels(&channels_for_user))?;
this.peer.send(connection_id, build_channels_update(
@@ -879,17 +880,20 @@ pub async fn handle_websocket_request(
.into_response();
}
// the first version of zed that sent this header was 0.121.x
if let Some(version) = app_version_header.map(|header| header.0 .0) {
// 0.123.0 was a nightly version with incompatible collab changes
// that were reverted.
if version == "0.123.0".parse().unwrap() {
return (
StatusCode::UPGRADE_REQUIRED,
"client must be upgraded".to_string(),
)
.into_response();
}
let Some(version) = app_version_header.map(|header| ZedVersion(header.0 .0)) else {
return (
StatusCode::UPGRADE_REQUIRED,
"no version header found".to_string(),
)
.into_response();
};
if !version.is_supported() {
return (
StatusCode::UPGRADE_REQUIRED,
"client must be upgraded".to_string(),
)
.into_response();
}
let socket_address = socket_address.to_string();
@@ -906,6 +910,7 @@ pub async fn handle_websocket_request(
connection,
socket_address,
user,
version,
impersonator.0,
None,
Executor::Production,
@@ -1311,6 +1316,22 @@ async fn set_room_participant_role(
response: Response<proto::SetRoomParticipantRole>,
session: Session,
) -> Result<()> {
let user_id = UserId::from_proto(request.user_id);
let role = ChannelRole::from(request.role());
if role == ChannelRole::Talker {
let pool = session.connection_pool().await;
for connection in pool.user_connections(user_id) {
if !connection.zed_version.supports_talker_role() {
Err(anyhow!(
"This user is on zed {} which does not support unmute",
connection.zed_version
))?;
}
}
}
let (live_kit_room, can_publish) = {
let room = session
.db()
@@ -1318,13 +1339,13 @@ async fn set_room_participant_role(
.set_room_participant_role(
session.user_id,
RoomId::from_proto(request.room_id),
UserId::from_proto(request.user_id),
ChannelRole::from(request.role()),
user_id,
role,
)
.await?;
let live_kit_room = room.live_kit_room.clone();
let can_publish = ChannelRole::from(request.role()).can_publish_to_rooms();
let can_publish = ChannelRole::from(request.role()).can_use_microphone();
room_updated(&room, &session.peer);
(live_kit_room, can_publish)
};

View File

@@ -4,6 +4,7 @@ use collections::{BTreeMap, HashSet};
use rpc::ConnectionId;
use serde::Serialize;
use tracing::instrument;
use util::SemanticVersion;
#[derive(Default, Serialize)]
pub struct ConnectionPool {
@@ -16,10 +17,30 @@ struct ConnectedUser {
connection_ids: HashSet<ConnectionId>,
}
#[derive(Debug, Serialize)]
pub struct ZedVersion(pub SemanticVersion);
use std::fmt;
impl fmt::Display for ZedVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl ZedVersion {
pub fn is_supported(&self) -> bool {
self.0 != SemanticVersion::new(0, 123, 0)
}
pub fn supports_talker_role(&self) -> bool {
self.0 >= SemanticVersion::new(0, 125, 0)
}
}
#[derive(Serialize)]
pub struct Connection {
pub user_id: UserId,
pub admin: bool,
pub zed_version: ZedVersion,
}
impl ConnectionPool {
@@ -29,9 +50,21 @@ impl ConnectionPool {
}
#[instrument(skip(self))]
pub fn add_connection(&mut self, connection_id: ConnectionId, user_id: UserId, admin: bool) {
self.connections
.insert(connection_id, Connection { user_id, admin });
pub fn add_connection(
&mut self,
connection_id: ConnectionId,
user_id: UserId,
admin: bool,
zed_version: ZedVersion,
) {
self.connections.insert(
connection_id,
Connection {
user_id,
admin,
zed_version,
},
);
let connected_user = self.connected_users.entry(user_id).or_default();
connected_user.connection_ids.insert(connection_id);
}
@@ -57,6 +90,19 @@ impl ConnectionPool {
self.connections.values()
}
pub fn user_connections(&self, user_id: UserId) -> impl Iterator<Item = &Connection> + '_ {
self.connected_users
.get(&user_id)
.into_iter()
.map(|state| {
state
.connection_ids
.iter()
.flat_map(|cid| self.connections.get(cid))
})
.flatten()
}
pub fn user_connection_ids(&self, user_id: UserId) -> impl Iterator<Item = ConnectionId> + '_ {
self.connected_users
.get(&user_id)

View File

@@ -104,7 +104,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
});
assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
assert!(editor_b.update(cx_b, |e, cx| e.read_only(cx)));
assert!(room_b.read_with(cx_b, |room, _| room.read_only()));
assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
assert!(room_b
.update(cx_b, |room, cx| room.share_microphone(cx))
.await
@@ -130,7 +130,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
assert!(editor_b.update(cx_b, |editor, cx| !editor.read_only(cx)));
// B sees themselves as muted, and can unmute.
assert!(room_b.read_with(cx_b, |room, _| !room.read_only()));
assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
room_b.read_with(cx_b, |room, _| assert!(room.is_muted()));
room_b.update(cx_b, |room, cx| room.toggle_mute(cx));
cx_a.run_until_parked();
@@ -223,7 +223,7 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
let room_b = cx_b
.read(ActiveCall::global)
.update(cx_b, |call, _| call.room().unwrap().clone());
assert!(room_b.read_with(cx_b, |room, _| room.read_only()));
assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
// A tries to grant write access to B, but cannot because B has not
// yet signed the zed CLA.
@@ -240,7 +240,26 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
.await
.unwrap_err();
cx_a.run_until_parked();
assert!(room_b.read_with(cx_b, |room, _| room.read_only()));
assert!(room_b.read_with(cx_b, |room, _| !room.can_share_projects()));
assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
// A tries to grant write access to B, but cannot because B has not
// yet signed the zed CLA.
active_call_a
.update(cx_a, |call, cx| {
call.room().unwrap().update(cx, |room, cx| {
room.set_participant_role(
client_b.user_id().unwrap(),
proto::ChannelRole::Talker,
cx,
)
})
})
.await
.unwrap();
cx_a.run_until_parked();
assert!(room_b.read_with(cx_b, |room, _| !room.can_share_projects()));
assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
// User B signs the zed CLA.
server
@@ -264,5 +283,6 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
.await
.unwrap();
cx_a.run_until_parked();
assert!(room_b.read_with(cx_b, |room, _| !room.read_only()));
assert!(room_b.read_with(cx_b, |room, _| room.can_share_projects()));
assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
}

View File

@@ -1,7 +1,7 @@
use crate::{
db::{tests::TestDb, NewUserParams, UserId},
executor::Executor,
rpc::{Server, CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
rpc::{Server, ZedVersion, CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
AppState, Config,
};
use anyhow::anyhow;
@@ -10,6 +10,7 @@ use channel::{ChannelBuffer, ChannelStore};
use client::{
self, proto::PeerId, Client, Connection, Credentials, EstablishConnectionError, UserStore,
};
use clock::FakeSystemClock;
use collab_ui::channel_view::ChannelView;
use collections::{HashMap, HashSet};
use fs::FakeFs;
@@ -37,7 +38,7 @@ use std::{
Arc,
},
};
use util::http::FakeHttpClient;
use util::{http::FakeHttpClient, SemanticVersion};
use workspace::{Workspace, WorkspaceStore};
pub struct TestServer {
@@ -163,6 +164,7 @@ impl TestServer {
client::init_settings(cx);
});
let clock = Arc::new(FakeSystemClock::default());
let http = FakeHttpClient::with_404_response();
let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await
{
@@ -185,7 +187,7 @@ impl TestServer {
.user_id
};
let client_name = name.to_string();
let mut client = cx.update(|cx| Client::new(http.clone(), cx));
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();
@@ -231,6 +233,7 @@ impl TestServer {
server_conn,
client_name,
user,
ZedVersion(SemanticVersion::new(1, 0, 0)),
None,
Some(connection_id_tx),
Executor::Deterministic(cx.background_executor().clone()),

View File

@@ -34,6 +34,7 @@ clock.workspace = true
collections.workspace = true
db.workspace = true
editor.workspace = true
extensions_ui.workspace = true
feature_flags.workspace = true
feedback.workspace = true
futures.workspace = true

View File

@@ -385,6 +385,7 @@ impl Render for MessageEditor {
mod tests {
use super::*;
use client::{Client, User, UserStore};
use clock::FakeSystemClock;
use gpui::TestAppContext;
use language::{Language, LanguageConfig};
use rpc::proto;
@@ -455,8 +456,9 @@ mod tests {
let settings = SettingsStore::test(cx);
cx.set_global(settings);
let clock = Arc::new(FakeSystemClock::default());
let http = FakeHttpClient::with_404_response();
let client = Client::new(http.clone(), cx);
let client = Client::new(clock, http.clone(), cx);
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
theme::init(theme::LoadThemes::JustBase, cx);
language::init(cx);

View File

@@ -34,7 +34,7 @@ use std::{mem, sync::Arc};
use theme::{ActiveTheme, ThemeSettings};
use ui::{
prelude::*, tooltip_container, Avatar, AvatarAvailabilityIndicator, Button, Color, ContextMenu,
Icon, IconButton, IconName, IconSize, Label, ListHeader, ListItem, Tooltip,
Icon, IconButton, IconName, IconSize, Indicator, Label, ListHeader, ListItem, Tooltip,
};
use util::{maybe, ResultExt, TryFutureExt};
use workspace::{
@@ -854,6 +854,10 @@ impl CollabPanel {
.into_any_element()
} else if role == proto::ChannelRole::Guest {
Label::new("Guest").color(Color::Muted).into_any_element()
} else if role == proto::ChannelRole::Talker {
Label::new("Mic only")
.color(Color::Muted)
.into_any_element()
} else {
div().into_any_element()
})
@@ -959,6 +963,8 @@ impl CollabPanel {
is_selected: bool,
cx: &mut ViewContext<Self>,
) -> impl IntoElement {
let channel_store = self.channel_store.read(cx);
let has_channel_buffer_changed = channel_store.has_channel_buffer_changed(channel_id);
ListItem::new("channel-notes")
.selected(is_selected)
.on_click(cx.listener(move |this, _, cx| {
@@ -966,9 +972,19 @@ impl CollabPanel {
}))
.start_slot(
h_flex()
.relative()
.gap_1()
.child(render_tree_branch(false, true, cx))
.child(IconButton::new(0, IconName::File)),
.child(IconButton::new(0, IconName::File))
.children(has_channel_buffer_changed.then(|| {
div()
.w_1p5()
.z_index(1)
.absolute()
.right(px(2.))
.top(px(2.))
.child(Indicator::dot().color(Color::Info))
})),
)
.child(Label::new("notes"))
.tooltip(move |cx| Tooltip::text("Open Channel Notes", cx))
@@ -980,6 +996,8 @@ impl CollabPanel {
is_selected: bool,
cx: &mut ViewContext<Self>,
) -> impl IntoElement {
let channel_store = self.channel_store.read(cx);
let has_messages_notification = channel_store.has_new_messages(channel_id);
ListItem::new("channel-chat")
.selected(is_selected)
.on_click(cx.listener(move |this, _, cx| {
@@ -987,9 +1005,19 @@ impl CollabPanel {
}))
.start_slot(
h_flex()
.relative()
.gap_1()
.child(render_tree_branch(false, false, cx))
.child(IconButton::new(0, IconName::MessageBubbles)),
.child(IconButton::new(0, IconName::MessageBubbles))
.children(has_messages_notification.then(|| {
div()
.w_1p5()
.z_index(1)
.absolute()
.right(px(2.))
.top(px(4.))
.child(Indicator::dot().color(Color::Info))
})),
)
.child(Label::new("chat"))
.tooltip(move |cx| Tooltip::text("Open Chat", cx))
@@ -1013,13 +1041,38 @@ impl CollabPanel {
cx: &mut ViewContext<Self>,
) {
let this = cx.view().clone();
if !(role == proto::ChannelRole::Guest || role == proto::ChannelRole::Member) {
if !(role == proto::ChannelRole::Guest
|| role == proto::ChannelRole::Talker
|| role == proto::ChannelRole::Member)
{
return;
}
let context_menu = ContextMenu::build(cx, |context_menu, cx| {
let context_menu = ContextMenu::build(cx, |mut context_menu, cx| {
if role == proto::ChannelRole::Guest {
context_menu.entry(
context_menu = context_menu.entry(
"Grant Mic Access",
None,
cx.handler_for(&this, move |_, cx| {
ActiveCall::global(cx)
.update(cx, |call, cx| {
let Some(room) = call.room() else {
return Task::ready(Ok(()));
};
room.update(cx, |room, cx| {
room.set_participant_role(
user_id,
proto::ChannelRole::Talker,
cx,
)
})
})
.detach_and_prompt_err("Failed to grant mic access", cx, |_, _| None)
}),
);
}
if role == proto::ChannelRole::Guest || role == proto::ChannelRole::Talker {
context_menu = context_menu.entry(
"Grant Write Access",
None,
cx.handler_for(&this, move |_, cx| {
@@ -1043,10 +1096,16 @@ impl CollabPanel {
}
})
}),
)
} else if role == proto::ChannelRole::Member {
context_menu.entry(
"Revoke Write Access",
);
}
if role == proto::ChannelRole::Member || role == proto::ChannelRole::Talker {
let label = if role == proto::ChannelRole::Talker {
"Mute"
} else {
"Revoke Access"
};
context_menu = context_menu.entry(
label,
None,
cx.handler_for(&this, move |_, cx| {
ActiveCall::global(cx)
@@ -1062,12 +1121,12 @@ impl CollabPanel {
)
})
})
.detach_and_prompt_err("Failed to revoke write access", cx, |_, _| None)
.detach_and_prompt_err("Failed to revoke access", cx, |_, _| None)
}),
)
} else {
unreachable!()
);
}
context_menu
});
cx.focus_view(&context_menu);
@@ -2490,13 +2549,26 @@ impl CollabPanel {
},
))
.start_slot(
Icon::new(if is_public {
IconName::Public
} else {
IconName::Hash
})
.size(IconSize::Small)
.color(Color::Muted),
div()
.relative()
.child(
Icon::new(if is_public {
IconName::Public
} else {
IconName::Hash
})
.size(IconSize::Small)
.color(Color::Muted),
)
.children(has_notes_notification.then(|| {
div()
.w_1p5()
.z_index(1)
.absolute()
.right(px(-1.))
.top(px(-1.))
.child(Indicator::dot().color(Color::Info))
})),
)
.child(
h_flex()
@@ -2530,9 +2602,7 @@ impl CollabPanel {
this.join_channel_chat(channel_id, cx)
}))
.tooltip(|cx| Tooltip::text("Open channel chat", cx))
.when(!has_messages_notification, |this| {
this.visible_on_hover("")
}),
.visible_on_hover(""),
)
.child(
IconButton::new("channel_notes", IconName::File)
@@ -2548,9 +2618,7 @@ impl CollabPanel {
this.open_channel_notes(channel_id, cx)
}))
.tooltip(|cx| Tooltip::text("Open channel notes", cx))
.when(!has_notes_notification, |this| {
this.visible_on_hover("")
}),
.visible_on_hover(""),
),
),
)
@@ -2560,6 +2628,7 @@ impl CollabPanel {
cx.new_view(|_| JoinChannelTooltip {
channel_store: channel_store.clone(),
channel_id,
has_notes_notification,
})
.into()
}
@@ -2845,17 +2914,25 @@ impl Render for DraggedChannelView {
struct JoinChannelTooltip {
channel_store: Model<ChannelStore>,
channel_id: ChannelId,
has_notes_notification: bool,
}
impl Render for JoinChannelTooltip {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
tooltip_container(cx, |div, cx| {
tooltip_container(cx, |container, cx| {
let participants = self
.channel_store
.read(cx)
.channel_participants(self.channel_id);
div.child(Label::new("Join Channel"))
container
.child(Label::new("Join channel"))
.children(self.has_notes_notification.then(|| {
h_flex()
.gap_2()
.child(Indicator::dot().color(Color::Info))
.child(Label::new("Unread notes"))
}))
.children(participants.iter().map(|participant| {
h_flex()
.gap_2()

View File

@@ -187,9 +187,10 @@ impl Render for CollabTitlebarItem {
let is_muted = room.is_muted();
let is_deafened = room.is_deafened().unwrap_or(false);
let is_screen_sharing = room.is_screen_sharing();
let read_only = room.read_only();
let can_use_microphone = room.can_use_microphone();
let can_share_projects = room.can_share_projects();
this.when(is_local && !read_only, |this| {
this.when(is_local && can_share_projects, |this| {
this.child(
Button::new(
"toggle_sharing",
@@ -235,7 +236,7 @@ impl Render for CollabTitlebarItem {
)
.pr_2(),
)
.when(!read_only, |this| {
.when(can_use_microphone, |this| {
this.child(
IconButton::new(
"mute-microphone",
@@ -276,7 +277,7 @@ impl Render for CollabTitlebarItem {
.icon_size(IconSize::Small)
.selected(is_deafened)
.tooltip(move |cx| {
if !read_only {
if can_use_microphone {
Tooltip::with_meta(
"Deafen Audio",
None,
@@ -289,7 +290,7 @@ impl Render for CollabTitlebarItem {
})
.on_click(move |_, cx| crate::toggle_deafen(&Default::default(), cx)),
)
.when(!read_only, |this| {
.when(can_share_projects, |this| {
this.child(
IconButton::new("screen-share", ui::IconName::Screen)
.style(ButtonStyle::Subtle)
@@ -695,6 +696,7 @@ impl CollabTitlebarItem {
.menu(|cx| {
ContextMenu::build(cx, |menu, _| {
menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
.action("Extensions", extensions_ui::Extensions.boxed_clone())
.action("Theme", theme_selector::Toggle.boxed_clone())
.separator()
.action("Share Feedback", feedback::GiveFeedback.boxed_clone())
@@ -720,6 +722,7 @@ impl CollabTitlebarItem {
ContextMenu::build(cx, |menu, _| {
menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
.action("Theme", theme_selector::Toggle.boxed_clone())
.action("Extensions", extensions_ui::Extensions.boxed_clone())
.separator()
.action("Share Feedback", feedback::GiveFeedback.boxed_clone())
})

View File

@@ -428,6 +428,8 @@ impl Copilot {
let binary = LanguageServerBinary {
path: node_path,
arguments,
// TODO: We could set HTTP_PROXY etc here and fix the copilot issue.
env: None,
};
let server = LanguageServer::new(

View File

@@ -885,7 +885,7 @@ mod tests {
use super::*;
use editor::{
display_map::{BlockContext, TransformBlock},
DisplayPoint,
DisplayPoint, GutterDimensions,
};
use gpui::{px, TestAppContext, VisualTestContext, WindowContext};
use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
@@ -1599,8 +1599,7 @@ mod tests {
.render(&mut BlockContext {
context: cx,
anchor_x: px(0.),
gutter_padding: px(0.),
gutter_width: px(0.),
gutter_dimensions: &GutterDimensions::default(),
line_height: px(0.),
em_width: px(0.),
max_width: px(0.),

View File

@@ -165,6 +165,8 @@ gpui::actions!(
GoToPrevHunk,
GoToTypeDefinition,
GoToTypeDefinitionSplit,
GoToImplementation,
GoToImplementationSplit,
OpenUrl,
HalfPageDown,
HalfPageUp,

View File

@@ -2,7 +2,7 @@ use super::{
wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot},
Highlights,
};
use crate::{Anchor, Editor, EditorStyle, ExcerptId, ExcerptRange, ToPoint as _};
use crate::{Anchor, Editor, EditorStyle, ExcerptId, ExcerptRange, GutterDimensions, ToPoint as _};
use collections::{Bound, HashMap, HashSet};
use gpui::{AnyElement, ElementContext, Pixels, View};
use language::{BufferSnapshot, Chunk, Patch, Point};
@@ -88,8 +88,7 @@ pub struct BlockContext<'a, 'b> {
pub view: View<Editor>,
pub anchor_x: Pixels,
pub max_width: Pixels,
pub gutter_width: Pixels,
pub gutter_padding: Pixels,
pub gutter_dimensions: &'b GutterDimensions,
pub em_width: Pixels,
pub line_height: Pixels,
pub block_id: usize,

View File

@@ -88,6 +88,7 @@ pub use multi_buffer::{
};
use ordered_float::OrderedFloat;
use parking_lot::{Mutex, RwLock};
use project::project_settings::{GitGutterSetting, ProjectSettings};
use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction};
use rand::prelude::*;
use rpc::proto::*;
@@ -443,7 +444,8 @@ pub struct EditorSnapshot {
}
pub struct GutterDimensions {
pub padding: Pixels,
pub left_padding: Pixels,
pub right_padding: Pixels,
pub width: Pixels,
pub margin: Pixels,
}
@@ -451,7 +453,8 @@ pub struct GutterDimensions {
impl Default for GutterDimensions {
fn default() -> Self {
Self {
padding: Pixels::ZERO,
left_padding: Pixels::ZERO,
right_padding: Pixels::ZERO,
width: Pixels::ZERO,
margin: Pixels::ZERO,
}
@@ -1346,6 +1349,7 @@ pub(crate) struct NavigationData {
enum GotoDefinitionKind {
Symbol,
Type,
Implementation,
}
#[derive(Debug, Clone)]
@@ -4057,7 +4061,8 @@ impl Editor {
if self.available_code_actions.is_some() {
Some(
IconButton::new("code_actions_indicator", ui::IconName::Bolt)
.icon_size(IconSize::Small)
.icon_size(IconSize::XSmall)
.size(ui::ButtonSize::None)
.icon_color(Color::Muted)
.selected(is_active)
.on_click(cx.listener(|editor, _e, cx| {
@@ -4206,8 +4211,43 @@ impl Editor {
active_index: 0,
ranges: tabstops,
});
}
// Check whether the just-entered snippet ends with an auto-closable bracket.
if self.autoclose_regions.is_empty() {
let snapshot = self.buffer.read(cx).snapshot(cx);
for selection in &mut self.selections.all::<Point>(cx) {
let selection_head = selection.head();
let Some(scope) = snapshot.language_scope_at(selection_head) else {
continue;
};
let mut bracket_pair = None;
let next_chars = snapshot.chars_at(selection_head).collect::<String>();
let prev_chars = snapshot
.reversed_chars_at(selection_head)
.collect::<String>();
for (pair, enabled) in scope.brackets() {
if enabled
&& pair.close
&& prev_chars.starts_with(pair.start.as_str())
&& next_chars.starts_with(pair.end.as_str())
{
bracket_pair = Some(pair.clone());
break;
}
}
if let Some(pair) = bracket_pair {
let start = snapshot.anchor_after(selection_head);
let end = snapshot.anchor_after(selection_head);
self.autoclose_regions.push(AutocloseRegion {
selection_id: selection.id,
range: start..end,
pair,
});
}
}
}
}
Ok(())
}
@@ -7317,6 +7357,18 @@ impl Editor {
self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, false, cx);
}
pub fn go_to_implementation(&mut self, _: &GoToImplementation, cx: &mut ViewContext<Self>) {
self.go_to_definition_of_kind(GotoDefinitionKind::Implementation, false, cx);
}
pub fn go_to_implementation_split(
&mut self,
_: &GoToImplementationSplit,
cx: &mut ViewContext<Self>,
) {
self.go_to_definition_of_kind(GotoDefinitionKind::Implementation, true, cx);
}
pub fn go_to_type_definition(&mut self, _: &GoToTypeDefinition, cx: &mut ViewContext<Self>) {
self.go_to_definition_of_kind(GotoDefinitionKind::Type, false, cx);
}
@@ -7354,12 +7406,14 @@ impl Editor {
let definitions = project.update(cx, |project, cx| match kind {
GotoDefinitionKind::Symbol => project.definition(&buffer, head, cx),
GotoDefinitionKind::Type => project.type_definition(&buffer, head, cx),
GotoDefinitionKind::Implementation => project.implementation(&buffer, head, cx),
});
cx.spawn(|editor, mut cx| async move {
let definitions = definitions.await?;
editor.update(&mut cx, |editor, cx| {
editor.navigate_to_hover_links(
Some(kind),
definitions.into_iter().map(HoverLink::Text).collect(),
split,
cx,
@@ -7392,8 +7446,9 @@ impl Editor {
.detach();
}
pub fn navigate_to_hover_links(
pub(crate) fn navigate_to_hover_links(
&mut self,
kind: Option<GotoDefinitionKind>,
mut definitions: Vec<HoverLink>,
split: bool,
cx: &mut ViewContext<Editor>,
@@ -7462,13 +7517,18 @@ impl Editor {
cx.spawn(|editor, mut cx| async move {
let (title, location_tasks, workspace) = editor
.update(&mut cx, |editor, cx| {
let tab_kind = match kind {
Some(GotoDefinitionKind::Implementation) => "Implementations",
_ => "Definitions",
};
let title = definitions
.iter()
.find_map(|definition| match definition {
HoverLink::Text(link) => link.origin.as_ref().map(|origin| {
let buffer = origin.buffer.read(cx);
format!(
"Definitions for {}",
"{} for {}",
tab_kind,
buffer
.text_for_range(origin.range.clone())
.collect::<String>()
@@ -7477,7 +7537,7 @@ impl Editor {
HoverLink::InlayHint(_, _) => None,
HoverLink::Url(_) => None,
})
.unwrap_or("Definitions".to_string());
.unwrap_or(tab_kind.to_string());
let location_tasks = definitions
.into_iter()
.map(|definition| match definition {
@@ -9580,23 +9640,50 @@ impl EditorSnapshot {
max_line_number_width: Pixels,
cx: &AppContext,
) -> GutterDimensions {
if self.show_gutter {
let descent = cx.text_system().descent(font_id, font_size);
let gutter_padding_factor = 4.0;
let gutter_padding = (em_width * gutter_padding_factor).round();
if !self.show_gutter {
return GutterDimensions::default();
}
let descent = cx.text_system().descent(font_id, font_size);
let show_git_gutter = matches!(
ProjectSettings::get_global(cx).git.git_gutter,
Some(GitGutterSetting::TrackedFiles)
);
let gutter_settings = EditorSettings::get_global(cx).gutter;
let line_gutter_width = if gutter_settings.line_numbers {
// Avoid flicker-like gutter resizes when the line number gains another digit and only resize the gutter on files with N*10^5 lines.
let min_width_for_number_on_gutter = em_width * 4.0;
let gutter_width =
max_line_number_width.max(min_width_for_number_on_gutter) + gutter_padding * 2.0;
let gutter_margin = -descent;
GutterDimensions {
padding: gutter_padding,
width: gutter_width,
margin: gutter_margin,
}
max_line_number_width.max(min_width_for_number_on_gutter)
} else {
GutterDimensions::default()
0.0.into()
};
let left_padding = if gutter_settings.code_actions {
em_width * 3.0
} else if show_git_gutter && gutter_settings.line_numbers {
em_width * 2.0
} else if show_git_gutter || gutter_settings.line_numbers {
em_width
} else {
px(0.)
};
let right_padding = if gutter_settings.folds && gutter_settings.line_numbers {
em_width * 4.0
} else if gutter_settings.folds {
em_width * 3.0
} else if gutter_settings.line_numbers {
em_width
} else {
px(0.)
};
GutterDimensions {
left_padding,
right_padding,
width: line_gutter_width + left_padding + right_padding,
margin: -descent,
}
}
}
@@ -10103,9 +10190,14 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren
.group(group_id.clone())
.relative()
.size_full()
.pl(cx.gutter_width)
.w(cx.max_width + cx.gutter_width)
.child(div().flex().w(cx.anchor_x - cx.gutter_width).flex_shrink())
.pl(cx.gutter_dimensions.width)
.w(cx.max_width + cx.gutter_dimensions.width)
.child(
div()
.flex()
.w(cx.anchor_x - cx.gutter_dimensions.width)
.flex_shrink(),
)
.child(div().flex().flex_shrink_0().child(
StyledText::new(text_without_backticks.clone()).with_highlights(
&text_style,

View File

@@ -12,6 +12,7 @@ pub struct EditorSettings {
pub use_on_type_format: bool,
pub toolbar: Toolbar,
pub scrollbar: Scrollbar,
pub gutter: Gutter,
pub vertical_scroll_margin: f32,
pub relative_line_numbers: bool,
pub seed_search_query_from_cursor: SeedQuerySetting,
@@ -45,6 +46,13 @@ pub struct Scrollbar {
pub diagnostics: bool,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct Gutter {
pub line_numbers: bool,
pub code_actions: bool,
pub folds: bool,
}
/// When to show the scrollbar in the editor.
///
/// Default: auto
@@ -97,6 +105,8 @@ pub struct EditorSettingsContent {
pub toolbar: Option<ToolbarContent>,
/// Scrollbar related settings
pub scrollbar: Option<ScrollbarContent>,
/// Gutter related settings
pub gutter: Option<GutterContent>,
/// The number of lines to keep above/below the cursor when auto-scrolling.
///
@@ -157,6 +167,23 @@ pub struct ScrollbarContent {
pub diagnostics: Option<bool>,
}
/// Gutter related settings
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct GutterContent {
/// Whether to show line numbers in the gutter.
///
/// Default: true
pub line_numbers: Option<bool>,
/// Whether to show code action buttons in the gutter.
///
/// Default: true
pub code_actions: Option<bool>,
/// Whether to show fold buttons in the gutter.
///
/// Default: true
pub folds: Option<bool>,
}
impl Settings for EditorSettings {
const KEY: Option<&'static str> = None;

View File

@@ -12,9 +12,9 @@ use crate::{
mouse_context_menu,
scroll::scroll_amount::ScrollAmount,
CursorShape, DisplayPoint, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode,
EditorSettings, EditorSnapshot, EditorStyle, HalfPageDown, HalfPageUp, HoveredCursor, LineDown,
LineUp, OpenExcerpts, PageDown, PageUp, Point, SelectPhase, Selection, SoftWrap, ToPoint,
CURSORS_VISIBLE_FOR, MAX_LINE_LEN,
EditorSettings, EditorSnapshot, EditorStyle, GutterDimensions, HalfPageDown, HalfPageUp,
HoveredCursor, LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point, SelectPhase, Selection,
SoftWrap, ToPoint, CURSORS_VISIBLE_FOR, MAX_LINE_LEN,
};
use anyhow::Result;
use collections::{BTreeMap, HashMap};
@@ -260,6 +260,8 @@ impl EditorElement {
register_action(view, cx, Editor::go_to_prev_hunk);
register_action(view, cx, Editor::go_to_definition);
register_action(view, cx, Editor::go_to_definition_split);
register_action(view, cx, Editor::go_to_implementation);
register_action(view, cx, Editor::go_to_implementation_split);
register_action(view, cx, Editor::go_to_type_definition);
register_action(view, cx, Editor::go_to_type_definition_split);
register_action(view, cx, Editor::open_url);
@@ -714,68 +716,73 @@ impl EditorElement {
let scroll_position = layout.position_map.snapshot.scroll_position();
let scroll_top = scroll_position.y * line_height;
let show_gutter = matches!(
let show_git_gutter = matches!(
ProjectSettings::get_global(cx).git.git_gutter,
Some(GitGutterSetting::TrackedFiles)
);
if show_gutter {
if show_git_gutter {
Self::paint_diff_hunks(bounds, layout, cx);
}
let gutter_settings = EditorSettings::get_global(cx).gutter;
if let Some(indicator) = layout.code_actions_indicator.take() {
debug_assert!(gutter_settings.code_actions);
let mut button = indicator.button.into_any_element();
let available_space = size(
AvailableSpace::MinContent,
AvailableSpace::Definite(line_height),
);
let indicator_size = button.measure(available_space, cx);
let mut x = Pixels::ZERO;
let mut y = indicator.row as f32 * line_height - scroll_top;
// Center indicator.
x += (layout.gutter_dimensions.margin + layout.gutter_dimensions.left_padding
- indicator_size.width)
/ 2.;
y += (line_height - indicator_size.height) / 2.;
button.draw(bounds.origin + point(x, y), available_space, cx);
}
for (ix, fold_indicator) in layout.fold_indicators.drain(..).enumerate() {
if let Some(fold_indicator) = fold_indicator {
debug_assert!(gutter_settings.folds);
let mut fold_indicator = fold_indicator.into_any_element();
let available_space = size(
AvailableSpace::MinContent,
AvailableSpace::Definite(line_height * 0.55),
);
let fold_indicator_size = fold_indicator.measure(available_space, cx);
let position = point(
bounds.size.width - layout.gutter_dimensions.right_padding,
ix as f32 * line_height - (scroll_top % line_height),
);
let centering_offset = point(
(layout.gutter_dimensions.right_padding + layout.gutter_dimensions.margin
- fold_indicator_size.width)
/ 2.,
(line_height - fold_indicator_size.height) / 2.,
);
let origin = bounds.origin + position + centering_offset;
fold_indicator.draw(origin, available_space, cx);
}
}
for (ix, line) in layout.line_numbers.iter().enumerate() {
if let Some(line) = line {
let line_origin = bounds.origin
+ point(
bounds.size.width - line.width - layout.gutter_padding,
bounds.size.width - line.width - layout.gutter_dimensions.right_padding,
ix as f32 * line_height - (scroll_top % line_height),
);
line.paint(line_origin, line_height, cx).log_err();
}
}
cx.with_z_index(1, |cx| {
for (ix, fold_indicator) in layout.fold_indicators.drain(..).enumerate() {
if let Some(fold_indicator) = fold_indicator {
let mut fold_indicator = fold_indicator.into_any_element();
let available_space = size(
AvailableSpace::MinContent,
AvailableSpace::Definite(line_height * 0.55),
);
let fold_indicator_size = fold_indicator.measure(available_space, cx);
let position = point(
bounds.size.width - layout.gutter_padding,
ix as f32 * line_height - (scroll_top % line_height),
);
let centering_offset = point(
(layout.gutter_padding + layout.gutter_margin - fold_indicator_size.width)
/ 2.,
(line_height - fold_indicator_size.height) / 2.,
);
let origin = bounds.origin + position + centering_offset;
fold_indicator.draw(origin, available_space, cx);
}
}
if let Some(indicator) = layout.code_actions_indicator.take() {
let mut button = indicator.button.into_any_element();
let available_space = size(
AvailableSpace::MinContent,
AvailableSpace::Definite(line_height),
);
let indicator_size = button.measure(available_space, cx);
let mut x = Pixels::ZERO;
let mut y = indicator.row as f32 * line_height - scroll_top;
// Center indicator.
x += ((layout.gutter_padding + layout.gutter_margin) - indicator_size.width) / 2.;
y += (line_height - indicator_size.height) / 2.;
button.draw(bounds.origin + point(x, y), available_space, cx);
}
});
}
fn paint_diff_hunks(bounds: Bounds<Pixels>, layout: &LayoutState, cx: &mut ElementContext) {
@@ -885,7 +892,8 @@ impl EditorElement {
cx: &mut ElementContext,
) {
let start_row = layout.visible_display_row_range.start;
let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO);
let content_origin =
text_bounds.origin + point(layout.gutter_dimensions.margin, Pixels::ZERO);
let line_end_overshoot = 0.15 * layout.position_map.line_height;
let whitespace_setting = self
.editor
@@ -918,6 +926,153 @@ impl EditorElement {
}
}
let mut invisible_display_ranges = SmallVec::<[Range<DisplayPoint>; 32]>::new();
for (participant_ix, (player_color, selections)) in
layout.selections.iter().enumerate()
{
for selection in selections {
if selection.is_local && !selection.range.is_empty() {
invisible_display_ranges.push(selection.range.clone());
}
if !selection.is_local || self.editor.read(cx).show_local_cursors(cx) {
let cursor_position = selection.head;
if layout
.visible_display_row_range
.contains(&cursor_position.row())
{
let cursor_row_layout = &layout.position_map.line_layouts
[(cursor_position.row() - start_row) as usize]
.line;
let cursor_column = cursor_position.column() as usize;
let cursor_character_x =
cursor_row_layout.x_for_index(cursor_column);
let mut block_width = cursor_row_layout
.x_for_index(cursor_column + 1)
- cursor_character_x;
if block_width == Pixels::ZERO {
block_width = layout.position_map.em_width;
}
let block_text = if let CursorShape::Block = selection.cursor_shape
{
layout
.position_map
.snapshot
.chars_at(cursor_position)
.next()
.and_then(|(character, _)| {
let text = if character == '\n' {
SharedString::from(" ")
} else {
SharedString::from(character.to_string())
};
let len = text.len();
cx.text_system()
.shape_line(
text,
cursor_row_layout.font_size,
&[TextRun {
len,
font: self.style.text.font(),
color: self.style.background,
background_color: None,
strikethrough: None,
underline: None,
}],
)
.log_err()
})
} else {
None
};
let x = cursor_character_x - layout.position_map.scroll_position.x;
let y = cursor_position.row() as f32
* layout.position_map.line_height
- layout.position_map.scroll_position.y;
if selection.is_newest {
self.editor.update(cx, |editor, _| {
editor.pixel_position_of_newest_cursor = Some(point(
text_bounds.origin.x + x + block_width / 2.,
text_bounds.origin.y
+ y
+ layout.position_map.line_height / 2.,
))
});
}
let cursor = Cursor {
color: player_color.cursor,
block_width,
origin: point(x, y),
line_height: layout.position_map.line_height,
shape: selection.cursor_shape,
block_text,
cursor_name: selection.user_name.clone().map(|name| {
CursorName {
string: name,
color: self.style.background,
is_top_row: cursor_position.row() == 0,
z_index: (participant_ix % 256).try_into().unwrap(),
}
}),
};
cursor.paint(content_origin, cx);
}
}
}
}
self.paint_redactions(text_bounds, &layout, cx);
for (ix, line_with_invisibles) in
layout.position_map.line_layouts.iter().enumerate()
{
let row = start_row + ix as u32;
line_with_invisibles.draw(
layout,
row,
content_origin,
whitespace_setting,
&invisible_display_ranges,
cx,
)
}
let corner_radius = 0.15 * layout.position_map.line_height;
for (player_color, selections) in &layout.selections {
for selection in selections.into_iter() {
self.paint_highlighted_range(
selection.range.clone(),
player_color.selection,
corner_radius,
corner_radius * 2.,
layout,
content_origin,
text_bounds,
cx,
);
if selection.is_local && !selection.range.is_empty() {
invisible_display_ranges.push(selection.range.clone());
}
}
}
for (range, color) in &layout.highlighted_ranges {
self.paint_highlighted_range(
range.clone(),
*color,
Pixels::ZERO,
line_end_overshoot,
layout,
content_origin,
text_bounds,
cx,
);
}
let fold_corner_radius = 0.15 * layout.position_map.line_height;
cx.with_element_id(Some("folds"), |cx| {
let snapshot = &layout.position_map.snapshot;
@@ -998,152 +1153,6 @@ impl EditorElement {
);
}
});
for (range, color) in &layout.highlighted_ranges {
self.paint_highlighted_range(
range.clone(),
*color,
Pixels::ZERO,
line_end_overshoot,
layout,
content_origin,
text_bounds,
cx,
);
}
let mut cursors = SmallVec::<[Cursor; 32]>::new();
let corner_radius = 0.15 * layout.position_map.line_height;
let mut invisible_display_ranges = SmallVec::<[Range<DisplayPoint>; 32]>::new();
for (participant_ix, (player_color, selections)) in
layout.selections.iter().enumerate()
{
for selection in selections.into_iter() {
self.paint_highlighted_range(
selection.range.clone(),
player_color.selection,
corner_radius,
corner_radius * 2.,
layout,
content_origin,
text_bounds,
cx,
);
if selection.is_local && !selection.range.is_empty() {
invisible_display_ranges.push(selection.range.clone());
}
if !selection.is_local || self.editor.read(cx).show_local_cursors(cx) {
let cursor_position = selection.head;
if layout
.visible_display_row_range
.contains(&cursor_position.row())
{
let cursor_row_layout = &layout.position_map.line_layouts
[(cursor_position.row() - start_row) as usize]
.line;
let cursor_column = cursor_position.column() as usize;
let cursor_character_x =
cursor_row_layout.x_for_index(cursor_column);
let mut block_width = cursor_row_layout
.x_for_index(cursor_column + 1)
- cursor_character_x;
if block_width == Pixels::ZERO {
block_width = layout.position_map.em_width;
}
let block_text = if let CursorShape::Block = selection.cursor_shape
{
layout
.position_map
.snapshot
.chars_at(cursor_position)
.next()
.and_then(|(character, _)| {
let text = if character == '\n' {
SharedString::from(" ")
} else {
SharedString::from(character.to_string())
};
let len = text.len();
cx.text_system()
.shape_line(
text,
cursor_row_layout.font_size,
&[TextRun {
len,
font: self.style.text.font(),
color: self.style.background,
background_color: None,
strikethrough: None,
underline: None,
}],
)
.log_err()
})
} else {
None
};
let x = cursor_character_x - layout.position_map.scroll_position.x;
let y = cursor_position.row() as f32
* layout.position_map.line_height
- layout.position_map.scroll_position.y;
if selection.is_newest {
self.editor.update(cx, |editor, _| {
editor.pixel_position_of_newest_cursor = Some(point(
text_bounds.origin.x + x + block_width / 2.,
text_bounds.origin.y
+ y
+ layout.position_map.line_height / 2.,
))
});
}
cursors.push(Cursor {
color: player_color.cursor,
block_width,
origin: point(x, y),
line_height: layout.position_map.line_height,
shape: selection.cursor_shape,
block_text,
cursor_name: selection.user_name.clone().map(|name| {
CursorName {
string: name,
color: self.style.background,
is_top_row: cursor_position.row() == 0,
z_index: (participant_ix % 256).try_into().unwrap(),
}
}),
});
}
}
}
}
for (ix, line_with_invisibles) in
layout.position_map.line_layouts.iter().enumerate()
{
let row = start_row + ix as u32;
line_with_invisibles.draw(
layout,
row,
content_origin,
whitespace_setting,
&invisible_display_ranges,
cx,
)
}
cx.with_z_index(0, |cx| self.paint_redactions(text_bounds, &layout, cx));
cx.with_z_index(1, |cx| {
for cursor in cursors {
cursor.paint(content_origin, cx);
}
});
},
)
}
@@ -1154,7 +1163,8 @@ impl EditorElement {
layout: &LayoutState,
cx: &mut ElementContext,
) {
let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO);
let content_origin =
text_bounds.origin + point(layout.gutter_dimensions.margin, Pixels::ZERO);
let line_end_overshoot = layout.line_end_overshoot();
// A softer than perfect black
@@ -1180,7 +1190,8 @@ impl EditorElement {
layout: &mut LayoutState,
cx: &mut ElementContext,
) {
let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO);
let content_origin =
text_bounds.origin + point(layout.gutter_dimensions.margin, Pixels::ZERO);
let start_row = layout.visible_display_row_range.start;
if let Some((position, mut context_menu)) = layout.context_menu.take() {
let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
@@ -1334,132 +1345,20 @@ impl EditorElement {
let thumb_bounds = Bounds::from_corners(point(left, thumb_top), point(right, thumb_bottom));
if layout.show_scrollbars {
let scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
cx.paint_quad(quad(
track_bounds,
thumb_bounds,
Corners::default(),
cx.theme().colors().scrollbar_track_background,
cx.theme().colors().scrollbar_thumb_background,
Edges {
top: Pixels::ZERO,
right: Pixels::ZERO,
right: px(1.),
bottom: Pixels::ZERO,
left: px(1.),
},
cx.theme().colors().scrollbar_track_border,
cx.theme().colors().scrollbar_thumb_border,
));
let scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
if layout.is_singleton && scrollbar_settings.selections {
let start_anchor = Anchor::min();
let end_anchor = Anchor::max();
let background_ranges = self
.editor
.read(cx)
.background_highlight_row_ranges::<BufferSearchHighlights>(
start_anchor..end_anchor,
&layout.position_map.snapshot,
50000,
);
for range in background_ranges {
let start_y = y_for_row(range.start().row() as f32);
let mut end_y = y_for_row(range.end().row() as f32);
if end_y - start_y < px(1.) {
end_y = start_y + px(1.);
}
let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y));
cx.paint_quad(quad(
bounds,
Corners::default(),
cx.theme().status().info,
Edges {
top: Pixels::ZERO,
right: px(1.),
bottom: Pixels::ZERO,
left: px(1.),
},
cx.theme().colors().scrollbar_thumb_border,
));
}
}
if layout.is_singleton && scrollbar_settings.symbols_selections {
let selection_ranges = self.editor.read(cx).background_highlights_in_range(
Anchor::min()..Anchor::max(),
&layout.position_map.snapshot,
cx.theme().colors(),
);
for hunk in selection_ranges {
let start_display = Point::new(hunk.0.start.row(), 0)
.to_display_point(&layout.position_map.snapshot.display_snapshot);
let end_display = Point::new(hunk.0.end.row(), 0)
.to_display_point(&layout.position_map.snapshot.display_snapshot);
let start_y = y_for_row(start_display.row() as f32);
let mut end_y = if hunk.0.start == hunk.0.end {
y_for_row((end_display.row() + 1) as f32)
} else {
y_for_row((end_display.row()) as f32)
};
if end_y - start_y < px(1.) {
end_y = start_y + px(1.);
}
let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y));
cx.paint_quad(quad(
bounds,
Corners::default(),
cx.theme().status().info,
Edges {
top: Pixels::ZERO,
right: px(1.),
bottom: Pixels::ZERO,
left: px(1.),
},
cx.theme().colors().scrollbar_thumb_border,
));
}
}
if layout.is_singleton && scrollbar_settings.git_diff {
for hunk in layout
.position_map
.snapshot
.buffer_snapshot
.git_diff_hunks_in_range(0..(max_row.floor() as u32))
{
let start_display = Point::new(hunk.buffer_range.start, 0)
.to_display_point(&layout.position_map.snapshot.display_snapshot);
let end_display = Point::new(hunk.buffer_range.end, 0)
.to_display_point(&layout.position_map.snapshot.display_snapshot);
let start_y = y_for_row(start_display.row() as f32);
let mut end_y = if hunk.buffer_range.start == hunk.buffer_range.end {
y_for_row((end_display.row() + 1) as f32)
} else {
y_for_row((end_display.row()) as f32)
};
if end_y - start_y < px(1.) {
end_y = start_y + px(1.);
}
let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y));
let color = match hunk.status() {
DiffHunkStatus::Added => cx.theme().status().created,
DiffHunkStatus::Modified => cx.theme().status().modified,
DiffHunkStatus::Removed => cx.theme().status().deleted,
};
cx.paint_quad(quad(
bounds,
Corners::default(),
color,
Edges {
top: Pixels::ZERO,
right: px(1.),
bottom: Pixels::ZERO,
left: px(1.),
},
cx.theme().colors().scrollbar_thumb_border,
));
}
}
if layout.is_singleton && scrollbar_settings.diagnostics {
let max_point = layout
@@ -1519,17 +1418,131 @@ impl EditorElement {
}
}
if layout.is_singleton && scrollbar_settings.git_diff {
for hunk in layout
.position_map
.snapshot
.buffer_snapshot
.git_diff_hunks_in_range(0..(max_row.floor() as u32))
{
let start_display = Point::new(hunk.buffer_range.start, 0)
.to_display_point(&layout.position_map.snapshot.display_snapshot);
let end_display = Point::new(hunk.buffer_range.end, 0)
.to_display_point(&layout.position_map.snapshot.display_snapshot);
let start_y = y_for_row(start_display.row() as f32);
let mut end_y = if hunk.buffer_range.start == hunk.buffer_range.end {
y_for_row((end_display.row() + 1) as f32)
} else {
y_for_row((end_display.row()) as f32)
};
if end_y - start_y < px(1.) {
end_y = start_y + px(1.);
}
let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y));
let color = match hunk.status() {
DiffHunkStatus::Added => cx.theme().status().created,
DiffHunkStatus::Modified => cx.theme().status().modified,
DiffHunkStatus::Removed => cx.theme().status().deleted,
};
cx.paint_quad(quad(
bounds,
Corners::default(),
color,
Edges {
top: Pixels::ZERO,
right: px(1.),
bottom: Pixels::ZERO,
left: px(1.),
},
cx.theme().colors().scrollbar_thumb_border,
));
}
}
if layout.is_singleton && scrollbar_settings.symbols_selections {
let selection_ranges = self.editor.read(cx).background_highlights_in_range(
Anchor::min()..Anchor::max(),
&layout.position_map.snapshot,
cx.theme().colors(),
);
for hunk in selection_ranges {
let start_display = Point::new(hunk.0.start.row(), 0)
.to_display_point(&layout.position_map.snapshot.display_snapshot);
let end_display = Point::new(hunk.0.end.row(), 0)
.to_display_point(&layout.position_map.snapshot.display_snapshot);
let start_y = y_for_row(start_display.row() as f32);
let mut end_y = if hunk.0.start == hunk.0.end {
y_for_row((end_display.row() + 1) as f32)
} else {
y_for_row((end_display.row()) as f32)
};
if end_y - start_y < px(1.) {
end_y = start_y + px(1.);
}
let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y));
cx.paint_quad(quad(
bounds,
Corners::default(),
cx.theme().status().info,
Edges {
top: Pixels::ZERO,
right: px(1.),
bottom: Pixels::ZERO,
left: px(1.),
},
cx.theme().colors().scrollbar_thumb_border,
));
}
}
if layout.is_singleton && scrollbar_settings.selections {
let start_anchor = Anchor::min();
let end_anchor = Anchor::max();
let background_ranges = self
.editor
.read(cx)
.background_highlight_row_ranges::<BufferSearchHighlights>(
start_anchor..end_anchor,
&layout.position_map.snapshot,
50000,
);
for range in background_ranges {
let start_y = y_for_row(range.start().row() as f32);
let mut end_y = y_for_row(range.end().row() as f32);
if end_y - start_y < px(1.) {
end_y = start_y + px(1.);
}
let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y));
cx.paint_quad(quad(
bounds,
Corners::default(),
cx.theme().status().info,
Edges {
top: Pixels::ZERO,
right: px(1.),
bottom: Pixels::ZERO,
left: px(1.),
},
cx.theme().colors().scrollbar_thumb_border,
));
}
}
cx.paint_quad(quad(
thumb_bounds,
track_bounds,
Corners::default(),
cx.theme().colors().scrollbar_thumb_background,
cx.theme().colors().scrollbar_track_background,
Edges {
top: Pixels::ZERO,
right: px(1.),
right: Pixels::ZERO,
bottom: Pixels::ZERO,
left: px(1.),
},
cx.theme().colors().scrollbar_thumb_border,
cx.theme().colors().scrollbar_track_border,
));
}
@@ -1817,7 +1830,10 @@ impl EditorElement {
Vec<Option<(FoldStatus, BufferRow, bool)>>,
) {
let font_size = self.style.text.font_size.to_pixels(cx.rem_size());
let include_line_numbers = snapshot.mode == EditorMode::Full;
let include_line_numbers =
EditorSettings::get_global(cx).gutter.line_numbers && snapshot.mode == EditorMode::Full;
let include_fold_statuses =
EditorSettings::get_global(cx).gutter.folds && snapshot.mode == EditorMode::Full;
let mut shaped_line_numbers = Vec::with_capacity(rows.len());
let mut fold_statuses = Vec::with_capacity(rows.len());
let mut line_number = String::new();
@@ -1862,6 +1878,8 @@ impl EditorElement {
.shape_line(line_number.clone().into(), font_size, &[run])
.unwrap();
shaped_line_numbers.push(Some(shaped_line));
}
if include_fold_statuses {
fold_statuses.push(
is_singleton
.then(|| {
@@ -1958,7 +1976,13 @@ impl EditorElement {
.unwrap()
.width;
let gutter_dimensions = snapshot.gutter_dimensions(font_id, font_size, em_width, self.max_line_number_width(&snapshot, cx), cx);
let gutter_dimensions = snapshot.gutter_dimensions(
font_id,
font_size,
em_width,
self.max_line_number_width(&snapshot, cx),
cx,
);
editor.gutter_width = gutter_dimensions.width;
@@ -2211,8 +2235,7 @@ impl EditorElement {
bounds.size.width,
scroll_width,
text_width,
gutter_dimensions.padding,
gutter_dimensions.width,
&gutter_dimensions,
em_width,
gutter_dimensions.width + gutter_dimensions.margin,
line_height,
@@ -2249,6 +2272,8 @@ impl EditorElement {
snapshot = editor.snapshot(cx);
}
let gutter_settings = EditorSettings::get_global(cx).gutter;
let mut context_menu = None;
let mut code_actions_indicator = None;
if let Some(newest_selection_head) = newest_selection_head {
@@ -2270,12 +2295,14 @@ impl EditorElement {
Some(crate::ContextMenu::CodeActions(_))
);
code_actions_indicator = editor
.render_code_actions_indicator(&style, active, cx)
.map(|element| CodeActionsIndicator {
row: newest_selection_head.row(),
button: element,
});
if gutter_settings.code_actions {
code_actions_indicator = editor
.render_code_actions_indicator(&style, active, cx)
.map(|element| CodeActionsIndicator {
row: newest_selection_head.row(),
button: element,
});
}
}
}
@@ -2293,29 +2320,32 @@ impl EditorElement {
None
} else {
editor.hover_state.render(
&snapshot,
&style,
visible_rows,
max_size,
editor.workspace.as_ref().map(|(w, _)| w.clone()),
cx,
)
&snapshot,
&style,
visible_rows,
max_size,
editor.workspace.as_ref().map(|(w, _)| w.clone()),
cx,
)
};
let editor_view = cx.view().clone();
let fold_indicators = cx.with_element_context(|cx| {
cx.with_element_id(Some("gutter_fold_indicators"), |_cx| {
editor.render_fold_indicators(
fold_statuses,
&style,
editor.gutter_hovered,
line_height,
gutter_dimensions.margin,
editor_view,
)
})
});
let fold_indicators = if gutter_settings.folds {
cx.with_element_context(|cx| {
cx.with_element_id(Some("gutter_fold_indicators"), |_cx| {
editor.render_fold_indicators(
fold_statuses,
&style,
editor.gutter_hovered,
line_height,
gutter_dimensions.margin,
editor_view,
)
})
})
} else {
Vec::new()
};
let invisible_symbol_font_size = font_size / 2.;
let tab_invisible = cx
@@ -2368,13 +2398,12 @@ impl EditorElement {
visible_display_row_range: start_row..end_row,
wrap_guides,
gutter_size,
gutter_padding: gutter_dimensions.padding,
gutter_dimensions,
text_size,
scrollbar_row_range,
show_scrollbars,
is_singleton,
max_row,
gutter_margin: gutter_dimensions.margin,
active_rows,
highlighted_rows,
highlighted_ranges,
@@ -2401,8 +2430,7 @@ impl EditorElement {
editor_width: Pixels,
scroll_width: Pixels,
text_width: Pixels,
gutter_padding: Pixels,
gutter_width: Pixels,
gutter_dimensions: &GutterDimensions,
em_width: Pixels,
text_x: Pixels,
line_height: Pixels,
@@ -2445,9 +2473,8 @@ impl EditorElement {
block.render(&mut BlockContext {
context: cx,
anchor_x,
gutter_padding,
gutter_dimensions,
line_height,
gutter_width,
em_width,
block_id,
max_width: scroll_width.max(text_width),
@@ -2551,12 +2578,14 @@ impl EditorElement {
h_flex()
.id(("collapsed context", block_id))
.size_full()
.gap(gutter_padding)
.gap(gutter_dimensions.left_padding + gutter_dimensions.right_padding)
.child(
h_flex()
.justify_end()
.flex_none()
.w(gutter_width - gutter_padding)
.w(gutter_dimensions.width
- (gutter_dimensions.left_padding
+ gutter_dimensions.right_padding))
.h_full()
.text_buffer(cx)
.text_color(cx.theme().colors().editor_line_number)
@@ -2617,7 +2646,7 @@ impl EditorElement {
BlockStyle::Sticky => editor_width,
BlockStyle::Flex => editor_width
.max(fixed_block_max_width)
.max(gutter_width + scroll_width),
.max(gutter_dimensions.width + scroll_width),
BlockStyle::Fixed => unreachable!(),
};
let available_space = size(
@@ -2634,7 +2663,7 @@ impl EditorElement {
});
}
(
scroll_width.max(fixed_block_max_width - gutter_width),
scroll_width.max(fixed_block_max_width - gutter_dimensions.width),
blocks,
)
}
@@ -3098,34 +3127,25 @@ impl Element for EditorElement {
ElementInputHandler::new(bounds, self.editor.clone()),
);
self.paint_background(gutter_bounds, text_bounds, &layout, cx);
self.paint_scrollbar(bounds, &mut layout, cx);
self.paint_overlays(text_bounds, &mut layout, cx);
if !layout.blocks.is_empty() {
cx.with_element_id(Some("editor_blocks"), |cx| {
self.paint_blocks(bounds, &mut layout, cx);
});
}
self.paint_mouse_listeners(
bounds,
gutter_bounds,
text_bounds,
&layout,
cx,
);
self.paint_text(text_bounds, &mut layout, cx);
if layout.gutter_size.width > Pixels::ZERO {
self.paint_gutter(gutter_bounds, &mut layout, cx);
}
self.paint_text(text_bounds, &mut layout, cx);
cx.with_z_index(0, |cx| {
self.paint_mouse_listeners(
bounds,
gutter_bounds,
text_bounds,
&layout,
cx,
);
});
if !layout.blocks.is_empty() {
cx.with_z_index(0, |cx| {
cx.with_element_id(Some("editor_blocks"), |cx| {
self.paint_blocks(bounds, &mut layout, cx);
});
})
}
cx.with_z_index(1, |cx| {
self.paint_overlays(text_bounds, &mut layout, cx);
});
cx.with_z_index(2, |cx| self.paint_scrollbar(bounds, &mut layout, cx));
self.paint_background(gutter_bounds, text_bounds, &layout, cx);
});
})
},
@@ -3151,8 +3171,7 @@ type BufferRow = u32;
pub struct LayoutState {
position_map: Arc<PositionMap>,
gutter_size: Size<Pixels>,
gutter_padding: Pixels,
gutter_margin: Pixels,
gutter_dimensions: GutterDimensions,
text_size: gpui::Size<Pixels>,
mode: EditorMode,
wrap_guides: SmallVec<[(Pixels, bool); 2]>,

View File

@@ -138,7 +138,7 @@ impl Editor {
cx.focus(&self.focus_handle);
}
self.navigate_to_hover_links(hovered_link_state.links, modifiers.alt, cx);
self.navigate_to_hover_links(None, hovered_link_state.links, modifiers.alt, cx);
return;
}
}

View File

@@ -1,6 +1,6 @@
use crate::{
DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToTypeDefinition,
Rename, RevealInFinder, SelectMode, ToggleCodeActions,
DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToImplementation,
GoToTypeDefinition, Rename, RevealInFinder, SelectMode, ToggleCodeActions,
};
use gpui::{DismissEvent, Pixels, Point, Subscription, View, ViewContext};
@@ -48,6 +48,7 @@ pub fn deploy_context_menu(
menu.action("Rename Symbol", Box::new(Rename))
.action("Go to Definition", Box::new(GoToDefinition))
.action("Go to Type Definition", Box::new(GoToTypeDefinition))
.action("Go to Implementation", Box::new(GoToImplementation))
.action("Find All References", Box::new(FindAllReferences))
.action(
"Code Actions",

View File

@@ -34,7 +34,7 @@ pub struct ExtensionsApiResponse {
pub data: Vec<Extension>,
}
#[derive(Deserialize)]
#[derive(Clone, Deserialize)]
pub struct Extension {
pub id: Arc<str>,
pub version: Arc<str>,

View File

@@ -11,7 +11,7 @@ use settings::Settings;
use std::time::Duration;
use std::{ops::Range, sync::Arc};
use theme::ThemeSettings;
use ui::prelude::*;
use ui::{prelude::*, CheckboxWithLabel, Tooltip};
use workspace::{
item::{Item, ItemEvent},
@@ -34,7 +34,8 @@ pub struct ExtensionsPage {
list: UniformListScrollHandle,
telemetry: Arc<Telemetry>,
is_fetching_extensions: bool,
extensions_entries: Vec<Extension>,
is_only_showing_installed_extensions: bool,
extension_entries: Vec<Extension>,
query_editor: View<Editor>,
query_contains_error: bool,
_subscription: gpui::Subscription,
@@ -54,7 +55,8 @@ impl ExtensionsPage {
list: UniformListScrollHandle::new(),
telemetry: workspace.client().telemetry().clone(),
is_fetching_extensions: false,
extensions_entries: Vec::new(),
is_only_showing_installed_extensions: false,
extension_entries: Vec::new(),
query_contains_error: false,
extension_fetch_task: None,
_subscription: subscription,
@@ -65,6 +67,24 @@ impl ExtensionsPage {
})
}
fn filtered_extension_entries(&self, cx: &mut ViewContext<Self>) -> Vec<Extension> {
let extension_store = ExtensionStore::global(cx).read(cx);
self.extension_entries
.iter()
.filter(|extension| {
if self.is_only_showing_installed_extensions {
let status = extension_store.extension_status(&extension.id);
matches!(status, ExtensionStatus::Installed(_))
} else {
true
}
})
.cloned()
.collect::<Vec<_>>()
}
fn install_extension(
&self,
extension_id: Arc<str>,
@@ -94,7 +114,7 @@ impl ExtensionsPage {
let fetch_result = extensions.await;
match fetch_result {
Ok(extensions) => this.update(&mut cx, |this, cx| {
this.extensions_entries = extensions;
this.extension_entries = extensions;
this.is_fetching_extensions = false;
cx.notify();
}),
@@ -113,7 +133,7 @@ impl ExtensionsPage {
}
fn render_extensions(&mut self, range: Range<usize>, cx: &mut ViewContext<Self>) -> Vec<Div> {
self.extensions_entries[range]
self.filtered_extension_entries(cx)[range]
.iter()
.map(|extension| self.render_entry(extension, cx))
.collect()
@@ -195,6 +215,7 @@ impl ExtensionsPage {
.color(Color::Accent);
let repository_url = extension.repository.clone();
let tooltip_text = Tooltip::text(repository_url.clone(), cx);
div().w_full().child(
v_flex()
@@ -269,7 +290,8 @@ impl ExtensionsPage {
.style(ButtonStyle::Filled)
.on_click(cx.listener(move |_, _, cx| {
cx.open_url(&repository_url);
})),
}))
.tooltip(move |_| tooltip_text.clone()),
),
),
)
@@ -379,10 +401,32 @@ impl ExtensionsPage {
Some(search)
}
}
fn render_empty_state(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let has_search = self.search_query(cx).is_some();
let message = if self.is_fetching_extensions {
"Loading extensions..."
} else if self.is_only_showing_installed_extensions {
if has_search {
"No installed extensions that match your search."
} else {
"No installed extensions."
}
} else {
if has_search {
"No extensions that match your search."
} else {
"No extensions."
}
};
Label::new(message)
}
}
impl Render for ExtensionsPage {
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
v_flex()
.size_full()
.p_4()
@@ -393,25 +437,39 @@ impl Render for ExtensionsPage {
.w_full()
.child(Headline::new("Extensions").size(HeadlineSize::XLarge)),
)
.child(h_flex().w_56().child(self.render_search(cx)))
.child(
h_flex()
.w_full()
.gap_2()
.child(h_flex().child(self.render_search(cx)))
.child(CheckboxWithLabel::new(
"installed",
Label::new("Only show installed"),
if self.is_only_showing_installed_extensions {
Selection::Selected
} else {
Selection::Unselected
},
cx.listener(|this, selection, _cx| {
this.is_only_showing_installed_extensions = match selection {
Selection::Selected => true,
Selection::Unselected => false,
Selection::Indeterminate => return,
}
}),
)),
)
.child(v_flex().size_full().overflow_y_hidden().map(|this| {
if self.extensions_entries.is_empty() {
let message = if self.is_fetching_extensions {
"Loading extensions..."
} else if self.search_query(cx).is_some() {
"No extensions that match your search."
} else {
"No extensions."
};
return this.child(Label::new(message));
let entries = self.filtered_extension_entries(cx);
if entries.is_empty() {
return this.child(self.render_empty_state(cx));
}
this.child(
canvas({
let view = cx.view().clone();
let scroll_handle = self.list.clone();
let item_count = self.extensions_entries.len();
let item_count = entries.len();
move |bounds, cx| {
uniform_list::<_, Div, _>(
view,

View File

@@ -32,11 +32,17 @@ log.workspace = true
libc = "0.2"
time.workspace = true
gpui = { workspace = true, optional = true}
gpui = { workspace = true, optional = true }
[target.'cfg(not(target_os = "macos"))'.dependencies]
notify = "6.1.1"
[target.'cfg(target_os = "windows")'.dependencies]
windows-sys = { version = "0.52", features = [
"Win32_Foundation",
"Win32_Storage_FileSystem",
] }
[dev-dependencies]
gpui = { workspace = true, features = ["test-support"] }

View File

@@ -245,9 +245,8 @@ impl Fs for RealFs {
#[cfg(unix)]
let inode = metadata.ino();
// todo!("windows")
#[cfg(windows)]
let inode = 0;
let inode = file_id(path).await?;
Ok(Some(Metadata {
inode,
@@ -1337,6 +1336,41 @@ pub fn copy_recursive<'a>(
.boxed()
}
// todo!(windows)
// can we get file id not open the file twice?
// https://github.com/rust-lang/rust/issues/63010
#[cfg(target_os = "windows")]
async fn file_id(path: impl AsRef<Path>) -> Result<u64> {
use std::os::windows::io::AsRawHandle;
use smol::fs::windows::OpenOptionsExt;
use windows_sys::Win32::{
Foundation::HANDLE,
Storage::FileSystem::{
GetFileInformationByHandle, BY_HANDLE_FILE_INFORMATION, FILE_FLAG_BACKUP_SEMANTICS,
},
};
let file = smol::fs::OpenOptions::new()
.read(true)
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS)
.open(path)
.await?;
let mut info: BY_HANDLE_FILE_INFORMATION = unsafe { std::mem::zeroed() };
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfileinformationbyhandle
// This function supports Windows XP+
smol::unblock(move || {
let ret = unsafe { GetFileInformationByHandle(file.as_raw_handle() as HANDLE, &mut info) };
if ret == 0 {
return Err(anyhow!(format!("{}", std::io::Error::last_os_error())));
};
Ok(((info.nFileIndexHigh as u64) << 32) | (info.nFileIndexLow as u64))
})
.await
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -51,6 +51,7 @@ parking = "2.0.0"
parking_lot.workspace = true
pathfinder_geometry = "0.5"
postage.workspace = true
profiling.workspace = true
rand.workspace = true
raw-window-handle = "0.6"
refineable.workspace = true
@@ -115,3 +116,11 @@ blade-macros.workspace = true
blade-rwh.workspace = true
bytemuck = "1"
cosmic-text = "0.10.0"
[[example]]
name = "hello_world"
path = "examples/hello_world.rs"
[[example]]
name = "image"
path = "examples/image/image.rs"

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

View File

@@ -64,9 +64,8 @@ fn main() {
App::new().run(|cx: &mut AppContext| {
cx.open_window(WindowOptions::default(), |cx| {
cx.new_view(|_cx| ImageShowcase {
local_resource: Arc::new(
PathBuf::from_str("crates/zed/resources/app-icon.png").unwrap(),
),
// Relative path to your root project path
local_resource: Arc::new(PathBuf::from_str("examples/image/app-icon.png").unwrap()),
remote_resource: "https://picsum.photos/512/512".into(),
})
});

View File

@@ -1106,8 +1106,14 @@ impl AppContext {
for window in self.windows() {
window
.update(self, |_, cx| {
cx.window.rendered_frame.clear_pending_keystrokes();
cx.window.next_frame.clear_pending_keystrokes();
cx.window
.rendered_frame
.dispatch_tree
.clear_pending_keystrokes();
cx.window
.next_frame
.dispatch_tree
.clear_pending_keystrokes();
})
.ok();
}

View File

@@ -0,0 +1,435 @@
use crate::{Bounds, Half};
use std::{
cmp,
fmt::Debug,
ops::{Add, Sub},
};
#[derive(Debug, Default)]
pub struct BoundsTree<U: Default + Clone + Debug> {
root: Option<usize>,
nodes: Vec<Node<U>>,
stack: Vec<usize>,
}
impl<U> BoundsTree<U>
where
U: Clone + Debug + PartialOrd + Add<U, Output = U> + Sub<Output = U> + Half + Default,
{
pub fn new() -> Self {
BoundsTree::default()
}
pub fn clear(&mut self) {
self.root = None;
self.nodes.clear();
self.stack.clear();
}
pub fn insert(&mut self, new_bounds: Bounds<U>) -> u32 {
// If the tree is empty, make the root the new leaf.
if self.root.is_none() {
let new_node = self.push_leaf(new_bounds, 1);
self.root = Some(new_node);
return 1;
}
// Search for the best place to add the new leaf based on heuristics.
let mut max_intersecting_ordering = 0;
let mut index = self.root.unwrap();
while let Node::Internal {
left,
right,
bounds: node_bounds,
..
} = self.node_mut(index)
{
let left = *left;
let right = *right;
*node_bounds = node_bounds.union(&new_bounds);
self.stack.push(index);
// Descend to the best-fit child, based on which one would increase
// the surface area the least. This attempts to keep the tree balanced
// in terms of surface area. If there is an intersection with the other child,
// add its keys to the intersections vector.
let left_cost = new_bounds.union(self.node(left).bounds()).half_perimeter();
let right_cost = new_bounds.union(self.node(right).bounds()).half_perimeter();
if left_cost < right_cost {
max_intersecting_ordering =
self.find_max_ordering(right, &new_bounds, max_intersecting_ordering);
index = left;
} else {
max_intersecting_ordering =
self.find_max_ordering(left, &new_bounds, max_intersecting_ordering);
index = right;
}
}
// We've found a leaf ('index' now refers to a leaf node).
// We'll insert a new parent node above the leaf and attach our new leaf to it.
let sibling = index;
// Check for collision with the located leaf node
let Node::Leaf {
bounds: sibling_bounds,
order: sibling_ordering,
..
} = self.node(index)
else {
unreachable!();
};
if sibling_bounds.intersects(&new_bounds) {
max_intersecting_ordering = cmp::max(max_intersecting_ordering, *sibling_ordering);
}
let ordering = max_intersecting_ordering + 1;
let new_node = self.push_leaf(new_bounds, ordering);
let new_parent = self.push_internal(sibling, new_node);
// If there was an old parent, we need to update its children indices.
if let Some(old_parent) = self.stack.last().copied() {
let Node::Internal { left, right, .. } = self.node_mut(old_parent) else {
unreachable!();
};
if *left == sibling {
*left = new_parent;
} else {
*right = new_parent;
}
} else {
// If the old parent was the root, the new parent is the new root.
self.root = Some(new_parent);
}
for node_index in self.stack.drain(..) {
let Node::Internal { max_ordering, .. } = &mut self.nodes[node_index] else {
unreachable!()
};
*max_ordering = cmp::max(*max_ordering, ordering);
}
ordering
}
fn find_max_ordering(&self, index: usize, bounds: &Bounds<U>, mut max_ordering: u32) -> u32 {
match self.node(index) {
Node::Leaf {
bounds: node_bounds,
order: ordering,
..
} => {
if bounds.intersects(node_bounds) {
max_ordering = cmp::max(*ordering, max_ordering);
}
}
Node::Internal {
left,
right,
bounds: node_bounds,
max_ordering: node_max_ordering,
..
} => {
if bounds.intersects(node_bounds) && max_ordering < *node_max_ordering {
let left_max_ordering = self.node(*left).max_ordering();
let right_max_ordering = self.node(*right).max_ordering();
if left_max_ordering > right_max_ordering {
max_ordering = self.find_max_ordering(*left, bounds, max_ordering);
max_ordering = self.find_max_ordering(*right, bounds, max_ordering);
} else {
max_ordering = self.find_max_ordering(*right, bounds, max_ordering);
max_ordering = self.find_max_ordering(*left, bounds, max_ordering);
}
}
}
}
max_ordering
}
fn push_leaf(&mut self, bounds: Bounds<U>, order: u32) -> usize {
self.nodes.push(Node::Leaf { bounds, order });
self.nodes.len() - 1
}
fn push_internal(&mut self, left: usize, right: usize) -> usize {
let left_node = self.node(left);
let right_node = self.node(right);
let new_bounds = left_node.bounds().union(right_node.bounds());
let max_ordering = cmp::max(left_node.max_ordering(), right_node.max_ordering());
self.nodes.push(Node::Internal {
bounds: new_bounds,
left,
right,
max_ordering,
});
self.nodes.len() - 1
}
#[inline(always)]
fn node(&self, index: usize) -> &Node<U> {
&self.nodes[index]
}
#[inline(always)]
fn node_mut(&mut self, index: usize) -> &mut Node<U> {
&mut self.nodes[index]
}
}
#[derive(Default, Debug, Clone, PartialEq)]
pub struct Primitive<U: Clone + Default + Debug> {
bounds: Bounds<U>,
order: u32,
}
#[derive(Debug)]
enum Node<U: Clone + Default + Debug> {
Leaf {
bounds: Bounds<U>,
order: u32,
},
Internal {
left: usize,
right: usize,
bounds: Bounds<U>,
max_ordering: u32,
},
}
impl<U> Node<U>
where
U: Clone + Default + Debug,
{
fn bounds(&self) -> &Bounds<U> {
match self {
Node::Leaf { bounds, .. } => bounds,
Node::Internal { bounds, .. } => bounds,
}
}
fn max_ordering(&self) -> u32 {
match self {
Node::Leaf {
order: ordering, ..
} => *ordering,
Node::Internal { max_ordering, .. } => *max_ordering,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Point, Size};
use rand::{Rng, SeedableRng};
use std::{fs, path::Path};
#[test]
fn test_bounds_insertion_with_two_bounds() {
let mut tree = BoundsTree::new();
let bounds1 = Bounds {
origin: Point { x: 0.0, y: 0.0 },
size: Size {
width: 10.0,
height: 10.0,
},
};
let bounds2 = Bounds {
origin: Point { x: 5.0, y: 5.0 },
size: Size {
width: 10.0,
height: 10.0,
},
};
// Insert the first Bounds.
assert_eq!(tree.insert(bounds1), 1);
// Insert the second Bounds, which overlaps with the first.
assert_eq!(tree.insert(bounds2), 2);
}
#[test]
fn test_adjacent_bounds() {
let mut tree = BoundsTree::new();
let bounds1 = Bounds {
origin: Point { x: 0.0, y: 0.0 },
size: Size {
width: 10.0,
height: 10.0,
},
};
let bounds2 = Bounds {
origin: Point { x: 10.0, y: 0.0 },
size: Size {
width: 10.0,
height: 10.0,
},
};
// Insert the first bounds.
assert_eq!(tree.insert(bounds1), 1);
// Insert the second bounds, which is adjacent to the first but not overlapping.
assert_eq!(tree.insert(bounds2), 1);
}
#[test]
fn test_random_iterations() {
let max_bounds = 100;
let mut actual_intersections: Vec<usize> = Vec::new();
for seed in 1..=1000 {
// let seed = 44;
let debug = false;
if debug {
let svg_path = Path::new("./svg");
if svg_path.exists() {
fs::remove_dir_all("./svg").unwrap();
}
fs::create_dir_all("./svg").unwrap();
}
dbg!(seed);
let mut tree = BoundsTree::new();
let mut rng = rand::rngs::StdRng::seed_from_u64(seed as u64);
let mut expected_quads: Vec<Primitive<f32>> = Vec::new();
let mut insert_time = std::time::Duration::ZERO;
// Insert a random number of random Bounds into the tree.
let num_bounds = rng.gen_range(1..=max_bounds);
for quad_id in 0..num_bounds {
let min_x: f32 = rng.gen_range(-100.0..100.0);
let min_y: f32 = rng.gen_range(-100.0..100.0);
let max_x: f32 = rng.gen_range(min_x..min_x + 50.0);
let max_y: f32 = rng.gen_range(min_y..min_y + 50.0);
let bounds = Bounds {
origin: Point { x: min_x, y: min_y },
size: Size {
width: max_x - min_x,
height: max_y - min_y,
},
};
let expected_ordering = expected_quads
.iter()
.filter_map(|quad| {
(quad.bounds.origin.x < bounds.origin.x + bounds.size.width
&& quad.bounds.origin.x + quad.bounds.size.width > bounds.origin.x
&& quad.bounds.origin.y < bounds.origin.y + bounds.size.height
&& quad.bounds.origin.y + quad.bounds.size.height > bounds.origin.y)
.then_some(quad.order)
})
.max()
.unwrap_or(0)
+ 1;
expected_quads.push(Primitive {
bounds,
order: expected_ordering,
});
if debug {
println!("inserting {} with Bounds: {:?}", quad_id, bounds);
draw_bounds(
format!("./svg/expected_bounds_after_{}.svg", quad_id),
&expected_quads,
);
}
// Insert the Bounds into the tree and collect intersections.
actual_intersections.clear();
let t0 = std::time::Instant::now();
let actual_ordering = tree.insert(bounds);
insert_time += t0.elapsed();
assert_eq!(actual_ordering, expected_ordering);
if debug {
tree.draw(format!("./svg/bounds_tree_after_{}.svg", quad_id));
}
}
}
}
fn draw_bounds(svg_path: impl AsRef<Path>, bounds: &[Primitive<f32>]) {
let mut svg_content = String::from(
r#"<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="-100 -100 200 200" style="border:1px solid black;">"#,
);
for quad in bounds {
svg_content.push_str(&format!(
r#"<rect x="{}" y="{}" width="{}" height="{}" style="fill:none;stroke:black;stroke-width:1" />"#,
quad.bounds.origin.x,
quad.bounds.origin.y,
quad.bounds.size.width,
quad.bounds.size.height
));
svg_content.push_str(&format!(
r#"<text x="{}" y="{}" font-size="3" text-anchor="middle" alignment-baseline="central"></text>"#,
quad.bounds.origin.x + quad.bounds.size.width / 2.0,
quad.bounds.origin.y + quad.bounds.size.height / 2.0,
));
}
svg_content.push_str("</svg>");
fs::write(svg_path, &svg_content).unwrap();
}
impl BoundsTree<f32> {
fn draw(&self, svg_path: impl AsRef<std::path::Path>) {
let root_bounds = self.node(self.root.unwrap()).bounds();
let mut svg_content = format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" version="1.1" style="border:1px solid black;" viewBox="{} {} {} {}">"#,
root_bounds.origin.x,
root_bounds.origin.y,
root_bounds.size.width,
root_bounds.size.height
);
fn draw_node(svg_content: &mut String, nodes: &[Node<f32>], index: usize) {
match &nodes[index] {
Node::Internal {
bounds,
left,
right,
..
} => {
svg_content.push_str(&format!(
r#"<rect x="{}" y="{}" width="{}" height="{}" style="fill:rgba({},{},{},0.1);stroke:rgba({},{},{},1);stroke-width:1" />"#,
bounds.origin.x,
bounds.origin.y,
bounds.size.width,
bounds.size.height,
(index * 50) % 255, // Red component
(index * 120) % 255, // Green component
(index * 180) % 255, // Blue component
(index * 50) % 255, // Red stroke
(index * 120) % 255, // Green stroke
(index * 180) % 255 // Blue stroke
));
draw_node(svg_content, nodes, *left);
draw_node(svg_content, nodes, *right);
}
Node::Leaf { bounds, .. } => {
svg_content.push_str(&format!(
r#"<rect x="{}" y="{}" width="{}" height="{}" style="fill:none;stroke:black;stroke-width:1" />"#,
bounds.origin.x,
bounds.origin.y,
bounds.size.width,
bounds.size.height
));
}
}
}
if let Some(root) = self.root {
draw_node(&mut svg_content, &self.nodes, root);
}
svg_content.push_str("</svg>");
std::fs::write(svg_path, &svg_content).unwrap();
}
}
}

View File

@@ -1127,7 +1127,7 @@ impl Element for Div {
cx,
|_style, scroll_offset, cx| {
cx.with_element_offset(scroll_offset, |cx| {
for child in &mut self.children {
for child in self.children.iter_mut().rev() {
child.paint(cx);
}
})

View File

@@ -1,457 +0,0 @@
use crate::{
Action, ActionRegistry, AnyTooltip, Bounds, ContentMask, CursorStyle, DispatchPhase,
ElementContext, EntityId, FocusId, GlobalElementId, KeyBinding, KeyContext, KeyEvent, Keymap,
KeymatchResult, Keystroke, KeystrokeMatcher, MouseEvent, Pixels, PlatformInputHandler,
Primitive, Scene, SceneIndex, SmallVec, WindowContext,
};
use collections::FxHashMap;
use std::{
any::{Any, TypeId},
cell::RefCell,
iter,
ops::Range,
rc::Rc,
};
// pub(crate) struct Frame {
// pub(crate) window_active: bool,
// #[cfg(any(test, feature = "test-support"))]
// pub(crate) debug_bounds: FxHashMap<String, Bounds<Pixels>>,
// }
pub struct Frame {
elements: Vec<PaintedElement>,
pub(crate) scene: Scene,
focus: Option<FocusId>,
pub(crate) window_active: bool,
mouse_listeners: Vec<AnyMouseListener>,
key_listeners: Vec<KeyListener>,
action_listeners: Vec<ActionListener>,
element_states: FxHashMap<GlobalElementId, ElementStateBox>,
element_stack: Vec<PaintedElementId>,
context_stack: Vec<KeyContext>,
content_mask_stack: Vec<ContentMask<Pixels>>,
focusable_node_ids: FxHashMap<FocusId, PaintedElementId>,
view_node_ids: FxHashMap<EntityId, PaintedElementId>,
keystroke_matchers: FxHashMap<SmallVec<[KeyContext; 4]>, KeystrokeMatcher>,
keymap: Rc<RefCell<Keymap>>,
action_registry: Rc<ActionRegistry>,
}
impl Frame {
pub fn new(keymap: Rc<RefCell<Keymap>>, action_registry: Rc<ActionRegistry>) -> Self {
Frame {
keymap,
action_registry,
elements: Vec::new(),
scene: Scene::default(),
focus: None,
window_active: false,
mouse_listeners: Vec::new(),
key_listeners: Vec::new(),
action_listeners: Vec::new(),
element_states: FxHashMap::default(),
element_stack: Vec::new(),
context_stack: Vec::new(),
content_mask_stack: Vec::new(),
focusable_node_ids: FxHashMap::default(),
view_node_ids: FxHashMap::default(),
keystroke_matchers: FxHashMap::default(),
}
}
pub fn clear(&mut self) {
self.elements.clear();
self.scene.clear();
self.focus = None;
self.mouse_listeners.clear();
self.key_listeners.clear();
self.action_listeners.clear();
self.element_states.clear();
self.element_stack.clear();
self.context_stack.clear();
self.content_mask_stack.clear();
self.focusable_node_ids.clear();
self.view_node_ids.clear();
self.keystroke_matchers.clear();
}
pub fn clear_pending_keystrokes(&mut self) {
self.keystroke_matchers.clear();
}
/// Preserve keystroke matchers from previous frames to support multi-stroke
/// bindings across multiple frames.
pub fn preserve_pending_keystrokes(
&mut self,
prev_frame: &mut Self,
focus_id: Option<FocusId>,
) {
self.context_stack.clear();
for element in self.dispatch_path(focus_id) {
if let Some(context) = element.key_context.clone() {
self.context_stack.push(context);
}
if let Some((context_stack, matcher)) = prev_frame
.keystroke_matchers
.remove_entry(self.context_stack.as_slice())
{
self.keystroke_matchers.insert(context_stack, matcher);
}
}
}
pub fn set_focus(&mut self, focus_id: Option<FocusId>) {
self.focus = focus_id;
}
pub fn set_window_active(&mut self, active: bool) {
self.window_active = active;
}
pub fn window_active(&self) -> bool {
self.window_active
}
pub fn focus_contains(&self, parent: FocusId, child: FocusId) -> bool {
if parent == child {
return true;
}
if let Some(parent_node_id) = self.focusable_node_ids.get(&parent) {
let mut current_node_id = self.focusable_node_ids.get(&child).copied();
while let Some(node_id) = current_node_id {
if node_id == *parent_node_id {
return true;
}
current_node_id = self.elements[node_id.0].parent;
}
}
false
}
pub fn focus_path(&self) -> SmallVec<[FocusId; 8]> {
let Some(focus_id) = self.focus else {
return SmallVec::new();
};
let mut focus_path = self
.dispatch_path(Some(focus_id))
.flat_map(|element| element.focus_id)
.collect::<SmallVec<[FocusId; 8]>>();
focus_path.reverse(); // Reverse the path so it goes from the root to the focused node.
focus_path
}
pub fn view_path(&self, view_id: EntityId) -> SmallVec<[EntityId; 8]> {
let Some(element_id) = self.view_node_ids.get(&view_id) else {
return SmallVec::new();
};
let mut view_path = self
.ancestors(Some(*element_id))
.flat_map(|element| element.view_id)
.collect::<SmallVec<[EntityId; 8]>>();
view_path.reverse(); // Reverse the path so it goes from the root to the focused node.
view_path
}
pub fn action_dispatch_path(&self, focus_id: Option<FocusId>) -> SmallVec<[ActionListener; 8]> {
let mut action_dispatch_path = self
.dispatch_path(focus_id)
.flat_map(|element| {
self.action_listeners[element.action_listeners.clone()]
.iter()
.cloned()
})
.collect::<SmallVec<[ActionListener; 8]>>();
action_dispatch_path.reverse(); // Reverse the path so it goes from the root to the focused node.
action_dispatch_path
}
pub fn key_dispatch_path(&self, focus_id: Option<FocusId>) -> SmallVec<[KeyListener; 8]> {
let mut key_dispatch_path: SmallVec<[KeyListener; 8]> = self
.dispatch_path(focus_id)
.flat_map(|element| {
self.key_listeners[element.key_listeners.clone()]
.iter()
.cloned()
})
.collect::<SmallVec<[KeyListener; 8]>>();
key_dispatch_path.reverse(); // Reverse the path so it goes from the root to the focused node.
key_dispatch_path
}
pub fn available_actions(&self, focus_id: Option<FocusId>) -> Vec<Box<dyn Action>> {
let mut actions = Vec::<Box<dyn Action>>::new();
for ActionListener { action_type, .. } in self.action_dispatch_path(focus_id) {
if let Err(ix) = actions.binary_search_by_key(&action_type, |a| a.as_any().type_id()) {
// Intentionally silence these errors without logging.
// If an action cannot be built by default, it's not available.
let action = self.action_registry.build_action_type(&action_type).ok();
if let Some(action) = action {
actions.insert(ix, action);
}
}
}
actions
}
pub fn bindings_for_action(
&self,
action: &dyn Action,
focus_id: Option<FocusId>,
) -> Vec<KeyBinding> {
let context_stack = self
.dispatch_path(focus_id)
.flat_map(|element| element.key_context.clone())
.collect::<SmallVec<[KeyContext; 8]>>();
let keymap = self.keymap.borrow();
keymap
.bindings_for_action(action)
.filter(|binding| {
for i in 0..context_stack.len() {
let context = &context_stack[0..=i];
if keymap.binding_enabled(binding, context) {
return true;
}
}
false
})
.cloned()
.collect()
}
pub fn is_action_available(&self, action: &dyn Action, focus_id: Option<FocusId>) -> bool {
for element in self.dispatch_path(focus_id) {
if self.action_listeners[element.action_listeners.clone()]
.iter()
.any(|listener| listener.action_type == action.as_any().type_id())
{
return true;
}
}
false
}
pub fn match_keystroke(
&mut self,
keystroke: &Keystroke,
focus_id: Option<FocusId>,
) -> KeymatchResult {
let mut bindings = SmallVec::<[KeyBinding; 1]>::new();
let mut pending = false;
let mut context_stack: SmallVec<[KeyContext; 4]> = SmallVec::new();
for element in self.dispatch_path(focus_id) {
if let Some(context) = element.key_context.clone() {
context_stack.push(context);
}
}
while !context_stack.is_empty() {
let keystroke_matcher = self
.keystroke_matchers
.entry(context_stack.clone())
.or_insert_with(|| KeystrokeMatcher::new(self.keymap.clone()));
let result = keystroke_matcher.match_keystroke(keystroke, &context_stack);
if result.pending && !pending && !bindings.is_empty() {
context_stack.pop();
continue;
}
pending = result.pending || pending;
for new_binding in result.bindings {
match bindings
.iter()
.position(|el| el.keystrokes.len() < new_binding.keystrokes.len())
{
Some(idx) => {
bindings.insert(idx, new_binding);
}
None => bindings.push(new_binding),
}
}
context_stack.pop();
}
KeymatchResult { bindings, pending }
}
pub fn has_pending_keystrokes(&self) -> bool {
self.keystroke_matchers
.iter()
.any(|(_, matcher)| matcher.has_pending_keystrokes())
}
fn dispatch_path(&self, focus_id: Option<FocusId>) -> impl Iterator<Item = &PaintedElement> {
let mut current_node_id = focus_id
.and_then(|focus_id| self.focusable_node_ids.get(&focus_id).copied())
.or_else(|| self.elements.is_empty().then(|| PaintedElementId(0)));
self.ancestors(current_node_id)
}
fn ancestors(
&self,
mut current_node_id: Option<PaintedElementId>,
) -> impl Iterator<Item = &PaintedElement> {
iter::from_fn(move || {
let node_id = current_node_id?;
current_node_id = self.elements[node_id.0].parent;
Some(&self.elements[node_id.0])
})
}
pub fn push_element(&mut self) {
let parent = self.element_stack.last().copied();
let element_id = PaintedElementId(self.elements.len());
let scene_index = self.scene.current_index();
self.elements.push(PaintedElement {
parent,
scene_primitives: scene_index.clone()..scene_index,
mouse_listeners: self.mouse_listeners.len()..self.mouse_listeners.len(),
key_listeners: self.key_listeners.len()..self.key_listeners.len(),
action_listeners: self.action_listeners.len()..self.action_listeners.len(),
..Default::default()
});
self.element_stack.push(element_id);
}
pub fn pop_element(&mut self) {
let element = &self.elements[self.active_element_id().0];
if element.key_context.is_some() {
self.context_stack.pop();
}
self.element_stack.pop();
}
pub fn set_key_context(&mut self, context: KeyContext) {
let element_id = self.active_element_id();
self.elements[element_id.0].key_context = Some(context.clone());
self.context_stack.push(context);
}
pub fn set_focus_id(&mut self, focus_id: FocusId) {
let element_id = self.active_element_id();
self.elements[element_id.0].focus_id = Some(focus_id);
self.focusable_node_ids.insert(focus_id, element_id);
}
pub fn set_view_id(&mut self, view_id: EntityId) {
let element_id = self.active_element_id();
self.elements[element_id.0].view_id = Some(view_id);
self.view_node_ids.insert(view_id, element_id);
}
pub fn paint_primitive<P: Into<Primitive>>(&mut self, build_primitive: impl FnOnce(u32) -> P) {
self.scene.paint_primitive(build_primitive);
let element_id = self.active_element_id();
self.elements[element_id.0].scene_primitives.end = self.scene.current_index();
}
pub fn on_mouse_event<E: MouseEvent>(
&mut self,
mut listener: impl 'static + FnMut(&E, DispatchPhase, &mut WindowContext),
) {
self.mouse_listeners.push(Rc::new(move |event, phase, cx| {
if let Some(event) = event.downcast_ref::<E>() {
listener(event, phase, cx);
}
}));
self.active_element().mouse_listeners.end += 1;
}
pub fn on_key_event<E: KeyEvent>(
&mut self,
listener: impl Fn(&E, DispatchPhase, &mut WindowContext) + 'static,
) {
self.key_listeners.push(Rc::new(|event, phase, cx| {
if let Some(event) = event.downcast_ref::<E>() {
listener(event, phase, cx);
}
}));
self.active_element().key_listeners.end += 1;
}
pub fn on_action(
&mut self,
action_type: TypeId,
listener: Rc<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext) + 'static>,
) {
self.action_listeners.push(ActionListener {
action_type,
listener: Rc::new(|event, phase, cx| listener(event, phase, cx)),
});
self.active_element().action_listeners.end += 1;
}
pub fn set_input_handler(&mut self, handler: Option<PlatformInputHandler>) {
self.active_element().input_handler = handler;
}
pub fn set_tooltip(&mut self, tooltip: Option<AnyTooltip>) {
self.active_element().tooltip = tooltip;
}
pub fn set_cursor_style(&mut self, cursor_style: Option<CursorStyle>) {
self.active_element().cursor_style = cursor_style;
}
fn active_element_id(&self) -> PaintedElementId {
self.element_stack
.last()
.copied()
.expect("There should be an active element")
}
fn active_element(&mut self) -> &mut PaintedElement {
let element_id = self.active_element_id();
&mut self.elements[element_id.0]
}
}
#[derive(Default)]
struct PaintedElement {
id: Option<GlobalElementId>,
bounds: Bounds<Pixels>,
content_mask: ContentMask<Pixels>,
opaque: bool,
scene_primitives: Range<SceneIndex>,
mouse_listeners: Range<usize>,
key_listeners: Range<usize>,
action_listeners: Range<usize>,
input_handler: Option<PlatformInputHandler>,
tooltip: Option<AnyTooltip>,
cursor_style: Option<CursorStyle>,
key_context: Option<KeyContext>,
focus_id: Option<FocusId>,
view_id: Option<EntityId>,
parent: Option<PaintedElementId>,
}
pub(crate) struct ElementStateBox {
pub(crate) inner: Box<dyn Any>,
pub(crate) parent_view_id: EntityId,
#[cfg(debug_assertions)]
pub(crate) type_name: &'static str,
}
#[derive(Copy, Clone, Eq, PartialEq)]
struct PaintedElementId(usize);
type AnyMouseListener = Rc<dyn Fn(&dyn Any, DispatchPhase, &mut ElementContext) + 'static>;
type KeyListener = Rc<dyn Fn(&dyn Any, DispatchPhase, &mut ElementContext)>;
#[derive(Clone)]
pub(crate) struct ActionListener {
pub(crate) action_type: TypeId,
pub(crate) listener: Rc<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>,
}

View File

@@ -828,6 +828,28 @@ where
y: self.origin.y.clone() + self.size.height.clone().half(),
}
}
/// Calculates the half perimeter of a rectangle defined by the bounds.
///
/// The half perimeter is calculated as the sum of the width and the height of the rectangle.
/// This method is generic over the type `T` which must implement the `Sub` trait to allow
/// calculation of the width and height from the bounds' origin and size, as well as the `Add` trait
/// to sum the width and height for the half perimeter.
///
/// # Examples
///
/// ```
/// # use zed::{Bounds, Point, Size};
/// let bounds = Bounds {
/// origin: Point { x: 0, y: 0 },
/// size: Size { width: 10, height: 20 },
/// };
/// let half_perimeter = bounds.half_perimeter();
/// assert_eq!(half_perimeter, 30);
/// ```
pub fn half_perimeter(&self) -> T {
self.size.width.clone() + self.size.height.clone()
}
}
impl<T: Clone + Default + Debug + PartialOrd + Add<T, Output = T> + Sub<Output = T>> Bounds<T> {

View File

@@ -70,11 +70,11 @@ mod app;
mod arena;
mod assets;
mod bounds_tree;
mod color;
mod element;
mod elements;
mod executor;
mod frame;
mod geometry;
mod image_cache;
mod input;
@@ -118,12 +118,12 @@ pub use anyhow::Result;
pub use app::*;
pub(crate) use arena::*;
pub use assets::*;
pub(crate) use bounds_tree::*;
pub use color::*;
pub use ctor::ctor;
pub use element::*;
pub use elements::*;
pub use executor::*;
pub use frame::*;
pub use geometry::*;
pub use gpui_macros::{register_action, test, IntoElement, Render};
use image_cache::*;

View File

@@ -68,22 +68,26 @@ impl<V: 'static> ElementInputHandler<V> {
}
impl<V: ViewInputHandler> InputHandler for ElementInputHandler<V> {
fn selected_text_range(&self, cx: &mut WindowContext) -> Option<Range<usize>> {
fn selected_text_range(&mut self, cx: &mut WindowContext) -> Option<Range<usize>> {
self.view
.update(cx, |view, cx| view.selected_text_range(cx))
}
fn marked_text_range(&self, cx: &mut WindowContext) -> Option<Range<usize>> {
fn marked_text_range(&mut self, cx: &mut WindowContext) -> Option<Range<usize>> {
self.view.update(cx, |view, cx| view.marked_text_range(cx))
}
fn text_for_range(&self, range_utf16: Range<usize>, cx: &mut WindowContext) -> Option<String> {
fn text_for_range(
&mut self,
range_utf16: Range<usize>,
cx: &mut WindowContext,
) -> Option<String> {
self.view
.update(cx, |view, cx| view.text_for_range(range_utf16, cx))
}
fn replace_text_in_range(
&self,
&mut self,
replacement_range: Option<Range<usize>>,
text: &str,
cx: &mut WindowContext,
@@ -94,7 +98,7 @@ impl<V: ViewInputHandler> InputHandler for ElementInputHandler<V> {
}
fn replace_and_mark_text_in_range(
&self,
&mut self,
range_utf16: Option<Range<usize>>,
new_text: &str,
new_selected_range: Option<Range<usize>>,
@@ -105,12 +109,12 @@ impl<V: ViewInputHandler> InputHandler for ElementInputHandler<V> {
});
}
fn unmark_text(&self, cx: &mut WindowContext) {
fn unmark_text(&mut self, cx: &mut WindowContext) {
self.view.update(cx, |view, cx| view.unmark_text(cx));
}
fn bounds_for_range(
&self,
&mut self,
range_utf16: Range<usize>,
cx: &mut WindowContext,
) -> Option<Bounds<Pixels>> {

View File

@@ -108,10 +108,6 @@ impl DispatchTree {
}
}
pub fn len(&self) -> usize {
self.nodes.len()
}
pub fn clear(&mut self) {
self.node_stack.clear();
self.context_stack.clear();

View File

@@ -430,26 +430,30 @@ pub trait InputHandler: 'static {
/// Corresponds to [selectedRange()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438242-selectedrange)
///
/// Return value is in terms of UTF-16 characters, from 0 to the length of the document
fn selected_text_range(&self, cx: &mut WindowContext) -> Option<Range<usize>>;
fn selected_text_range(&mut self, cx: &mut WindowContext) -> Option<Range<usize>>;
/// Get the range of the currently marked text, if any
/// Corresponds to [markedRange()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438250-markedrange)
///
/// Return value is in terms of UTF-16 characters, from 0 to the length of the document
fn marked_text_range(&self, cx: &mut WindowContext) -> Option<Range<usize>>;
fn marked_text_range(&mut self, cx: &mut WindowContext) -> Option<Range<usize>>;
/// Get the text for the given document range in UTF-16 characters
/// Corresponds to [attributedSubstring(forProposedRange: actualRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438238-attributedsubstring)
///
/// range_utf16 is in terms of UTF-16 characters
fn text_for_range(&self, range_utf16: Range<usize>, cx: &mut WindowContext) -> Option<String>;
fn text_for_range(
&mut self,
range_utf16: Range<usize>,
cx: &mut WindowContext,
) -> Option<String>;
/// Replace the text in the given document range with the given text
/// Corresponds to [insertText(_:replacementRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438258-inserttext)
///
/// replacement_range is in terms of UTF-16 characters
fn replace_text_in_range(
&self,
&mut self,
replacement_range: Option<Range<usize>>,
text: &str,
cx: &mut WindowContext,
@@ -462,7 +466,7 @@ pub trait InputHandler: 'static {
/// range_utf16 is in terms of UTF-16 characters
/// new_selected_range is in terms of UTF-16 characters
fn replace_and_mark_text_in_range(
&self,
&mut self,
range_utf16: Option<Range<usize>>,
new_text: &str,
new_selected_range: Option<Range<usize>>,
@@ -471,14 +475,14 @@ pub trait InputHandler: 'static {
/// Remove the IME 'composing' state from the document
/// Corresponds to [unmarkText()](https://developer.apple.com/documentation/appkit/nstextinputclient/1438239-unmarktext)
fn unmark_text(&self, cx: &mut WindowContext);
fn unmark_text(&mut self, cx: &mut WindowContext);
/// Get the bounds of the given document range in screen coordinates
/// Corresponds to [firstRect(forCharacterRange:actualRange:)](https://developer.apple.com/documentation/appkit/nstextinputclient/1438240-firstrect)
///
/// This is used for positioning the IME candidate window
fn bounds_for_range(
&self,
&mut self,
range_utf16: Range<usize>,
cx: &mut WindowContext,
) -> Option<Bounds<Pixels>>;

View File

@@ -117,6 +117,7 @@ impl PlatformAtlas for BladeAtlas {
if let Some(tile) = lock.tiles_by_key.get(key) {
Ok(tile.clone())
} else {
profiling::scope!("new tile");
let (size, bytes) = build()?;
let tile = lock.allocate(size, key.texture_kind());
lock.upload_texture(tile.texture_id, tile.bounds, &bytes);

View File

@@ -39,6 +39,7 @@ impl BladeBelt {
}
}
#[profiling::function]
pub fn alloc(&mut self, size: u64, gpu: &gpu::Context) -> gpu::BufferPiece {
for &mut (ref rb, ref mut offset) in self.active.iter_mut() {
let aligned = offset.next_multiple_of(self.desc.alignment);

View File

@@ -444,6 +444,7 @@ impl BladeRenderer {
self.gpu.metal_layer().unwrap().as_ptr()
}
#[profiling::function]
fn rasterize_paths(&mut self, paths: &[Path<ScaledPixels>]) {
self.path_tiles.clear();
let mut vertices_by_texture_id = HashMap::default();
@@ -506,7 +507,10 @@ impl BladeRenderer {
}
pub fn draw(&mut self, scene: &Scene) {
let frame = self.gpu.acquire_frame();
let frame = {
profiling::scope!("acquire frame");
self.gpu.acquire_frame()
};
self.command_encoder.start();
self.command_encoder.init_texture(frame.texture());
@@ -529,6 +533,7 @@ impl BladeRenderer {
}],
depth_stencil: None,
}) {
profiling::scope!("render pass");
for batch in scene.batches() {
match batch {
PrimitiveBatch::Quads(quads) => {
@@ -718,6 +723,7 @@ impl BladeRenderer {
self.command_encoder.present(frame);
let sync_point = self.gpu.submit(&mut self.command_encoder);
profiling::scope!("finish");
self.instance_belt.flush(&sync_point);
self.atlas.after_frame(&sync_point);
self.atlas.clear_textures(AtlasTextureKind::Path);

View File

@@ -33,6 +33,7 @@ impl LinuxDispatcher {
) -> Self {
let (background_sender, background_receiver) = flume::unbounded::<Runnable>();
let background_thread = thread::spawn(move || {
profiling::register_thread!("background");
for runnable in background_receiver {
let _ignore_panic = panic::catch_unwind(|| runnable.run());
}

View File

@@ -294,7 +294,13 @@ impl Platform for LinuxPlatform {
}
fn reveal_path(&self, path: &Path) {
open::that(path);
if path.is_dir() {
open::that(path);
return;
}
// If `path` is a file, the system may try to open it in a text editor
let dir = path.parent().unwrap_or(Path::new(""));
open::that(dir);
}
fn on_become_active(&self, callback: Box<dyn FnMut()>) {

View File

@@ -17,7 +17,9 @@ impl Keystroke {
// Ignore control characters (and DEL) for the purposes of ime_key,
// but if key_utf32 is 0 then assume it isn't one
let ime_key = (key_utf32 == 0 || (key_utf32 >= 32 && key_utf32 != 127)).then_some(key_utf8);
let ime_key = ((key_utf32 == 0 || (key_utf32 >= 32 && key_utf32 != 127))
&& !key_utf8.is_empty())
.then_some(key_utf8);
Keystroke {
modifiers,

View File

@@ -421,12 +421,29 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientState {
state.keymap_state = Some(xkb::State::new(&keymap));
}
wl_keyboard::Event::Enter { surface, .. } => {
for window in &state.windows {
if window.1.surface.id() == surface.id() {
state.keyboard_focused_window = Some(Rc::clone(&window.1));
}
state.keyboard_focused_window = state
.windows
.iter()
.find(|&w| w.1.surface.id() == surface.id())
.map(|w| w.1.clone());
if let Some(window) = &state.keyboard_focused_window {
window.set_focused(true);
}
}
wl_keyboard::Event::Leave { surface, .. } => {
let keyboard_focused_window = state
.windows
.iter()
.find(|&w| w.1.surface.id() == surface.id())
.map(|w| w.1.clone());
if let Some(window) = keyboard_focused_window {
window.set_focused(false);
}
state.keyboard_focused_window = None;
}
wl_keyboard::Event::Modifiers {
mods_depressed,
mods_latched,
@@ -479,7 +496,6 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientState {
}
}
}
wl_keyboard::Event::Leave { .. } => {}
_ => {}
}
}

View File

@@ -228,6 +228,12 @@ impl WaylandWindowState {
}
}
}
pub fn set_focused(&self, focus: bool) {
if let Some(ref mut fun) = self.callbacks.lock().active_status_change {
fun(focus);
}
}
}
#[derive(Clone)]
@@ -349,7 +355,7 @@ impl PlatformWindow for WaylandWindow {
}
fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>) {
//todo!(linux)
self.0.callbacks.lock().active_status_change = Some(callback);
}
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {

View File

@@ -71,7 +71,10 @@ impl Client for X11Client {
// into window functions as they may invoke callbacks that need
// to immediately access the platform (self).
while !self.platform_inner.state.lock().quit_requested {
let event = self.xcb_connection.wait_for_event().unwrap();
let event = {
profiling::scope!("Wait for event");
self.xcb_connection.wait_for_event().unwrap()
};
match event {
xcb::Event::X(x::Event::ClientMessage(ev)) => {
if let x::ClientMessageData::Data32([atom, ..]) = ev.data() {
@@ -210,6 +213,7 @@ impl Client for X11Client {
_ => {}
}
profiling::scope!("Runnables");
if let Ok(runnable) = self.platform_inner.main_receiver.try_recv() {
runnable.run();
}
@@ -219,6 +223,7 @@ impl Client for X11Client {
fun();
}
}
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
let setup = self.xcb_connection.get_setup();
setup
@@ -230,6 +235,7 @@ impl Client for X11Client {
})
.collect()
}
fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
Some(Rc::new(X11Display::new(&self.xcb_connection, id.0 as i32)))
}

View File

@@ -1,12 +1,16 @@
use crate::{
point, AtlasTextureId, AtlasTile, Bounds, ContentMask, Corners, Edges, EntityId, Hsla, Pixels,
Point, ScaledPixels,
point, AtlasTextureId, AtlasTile, Bounds, BoundsTree, ContentMask, Corners, Edges, EntityId,
Hsla, Pixels, Point, ScaledPixels, StackingOrder,
};
use collections::{BTreeMap, FxHashSet};
use std::{fmt::Debug, iter::Peekable, slice};
#[allow(non_camel_case_types, unused)]
pub(crate) type PathVertex_ScaledPixels = PathVertex<ScaledPixels>;
pub(crate) type LayerId = u32;
pub(crate) type DrawOrder = u32;
#[derive(Default, Copy, Clone, Debug, Eq, PartialEq, Hash)]
#[repr(C)]
pub(crate) struct ViewId {
@@ -31,18 +35,6 @@ impl From<ViewId> for EntityId {
}
}
#[derive(Clone, Default)]
/// An index into all the geometry in a `Scene` at a point in time.
pub(crate) struct SceneIndex {
pub(crate) shadows: usize,
pub(crate) quads: usize,
pub(crate) paths: usize,
pub(crate) underlines: usize,
pub(crate) monochrome_sprites: usize,
pub(crate) polychrome_sprites: usize,
pub(crate) surfaces: usize,
}
#[derive(Default)]
pub(crate) struct Scene {
pub(crate) shadows: Vec<Shadow>,
@@ -52,7 +44,7 @@ pub(crate) struct Scene {
pub(crate) monochrome_sprites: Vec<MonochromeSprite>,
pub(crate) polychrome_sprites: Vec<PolychromeSprite>,
pub(crate) surfaces: Vec<Surface>,
pub(crate) primitive_count: u32,
bounds_tree: BoundsTree<ScaledPixels>,
}
impl Scene {
@@ -64,19 +56,7 @@ impl Scene {
self.monochrome_sprites.clear();
self.polychrome_sprites.clear();
self.surfaces.clear();
self.primitive_count = 0;
}
pub fn current_index(&self) -> SceneIndex {
SceneIndex {
shadows: self.shadows.len(),
quads: self.quads.len(),
paths: self.paths.len(),
underlines: self.underlines.len(),
monochrome_sprites: self.monochrome_sprites.len(),
polychrome_sprites: self.polychrome_sprites.len(),
surfaces: self.surfaces.len(),
}
self.bounds_tree.clear();
}
pub fn paths(&self) -> &[Path<ScaledPixels>] {
@@ -109,72 +89,120 @@ impl Scene {
}
}
pub(crate) fn paint_primitive<T: Into<Primitive>>(
pub(crate) fn insert(
&mut self,
build_primitive: impl FnOnce(u32) -> T,
) {
let primitive = build_primitive(self.primitive_count).into();
order: &StackingOrder,
primitive: impl Into<Primitive>,
) -> Option<u32> {
let primitive = primitive.into();
let clipped_bounds = primitive
.bounds()
.intersect(&primitive.content_mask().bounds);
if clipped_bounds.size.width <= ScaledPixels(0.)
|| clipped_bounds.size.height <= ScaledPixels(0.)
{
return;
return None;
}
let order = u32::MAX - self.bounds_tree.insert(clipped_bounds);
match primitive {
Primitive::Shadow(mut shadow) => {
shadow.order = order;
self.shadows.push(shadow);
}
Primitive::Quad(mut quad) => {
quad.order = order;
self.quads.push(quad);
}
Primitive::Path(mut path) => {
path.order = order;
path.id = PathId(self.paths.len());
self.paths.push(path);
}
Primitive::Underline(mut underline) => {
underline.order = order;
self.underlines.push(underline);
}
Primitive::MonochromeSprite(mut sprite) => {
sprite.order = order;
self.monochrome_sprites.push(sprite);
}
Primitive::PolychromeSprite(mut sprite) => {
sprite.order = order;
self.polychrome_sprites.push(sprite);
}
Primitive::Surface(mut surface) => {
surface.order = order;
self.surfaces.push(surface);
}
}
self.primitive_count += 1;
Some(order)
}
pub fn reuse_subscene(&mut self, prev_scene: &mut Self, start: SceneIndex, end: SceneIndex) {
self.shadows
.extend(prev_scene.shadows.drain(start.shadows..end.shadows));
self.quads
.extend(prev_scene.quads.drain(start.quads..end.quads));
self.paths
.extend(prev_scene.paths.drain(start.paths..end.paths));
self.underlines.extend(
prev_scene
.underlines
.drain(start.underlines..end.underlines),
);
self.monochrome_sprites.extend(
prev_scene
.monochrome_sprites
.drain(start.monochrome_sprites..end.monochrome_sprites),
);
self.polychrome_sprites.extend(
prev_scene
.polychrome_sprites
.drain(start.polychrome_sprites..end.polychrome_sprites),
);
self.surfaces
.extend(prev_scene.surfaces.drain(start.surfaces..end.surfaces));
pub fn reuse_views(&mut self, views: &FxHashSet<EntityId>, prev_scene: &mut Self) {
// todo!()
// for shadow in prev_scene.shadows.drain(..) {
// if views.contains(&shadow.view_id.into()) {
// let order = &prev_scene.orders_by_layer[&shadow.layer_id];
// self.insert(order, shadow);
// }
// }
// for quad in prev_scene.quads.drain(..) {
// if views.contains(&quad.view_id.into()) {
// let order = &prev_scene.orders_by_layer[&quad.layer_id];
// self.insert(order, quad);
// }
// }
// for path in prev_scene.paths.drain(..) {
// if views.contains(&path.view_id.into()) {
// let order = &prev_scene.orders_by_layer[&path.layer_id];
// self.insert(order, path);
// }
// }
// for underline in prev_scene.underlines.drain(..) {
// if views.contains(&underline.view_id.into()) {
// let order = &prev_scene.orders_by_layer[&underline.layer_id];
// self.insert(order, underline);
// }
// }
// for sprite in prev_scene.monochrome_sprites.drain(..) {
// if views.contains(&sprite.view_id.into()) {
// let order = &prev_scene.orders_by_layer[&sprite.layer_id];
// self.insert(order, sprite);
// }
// }
// for sprite in prev_scene.polychrome_sprites.drain(..) {
// if views.contains(&sprite.view_id.into()) {
// let order = &prev_scene.orders_by_layer[&sprite.layer_id];
// self.insert(order, sprite);
// }
// }
// for surface in prev_scene.surfaces.drain(..) {
// if views.contains(&surface.view_id.into()) {
// let order = &prev_scene.orders_by_layer[&surface.layer_id];
// self.insert(order, surface);
// }
// }
}
pub fn finish(&mut self) {
self.shadows.sort_unstable_by_key(|shadow| shadow.order);
self.quads.sort_unstable_by_key(|quad| quad.order);
self.paths.sort_unstable_by_key(|path| path.order);
self.underlines
.sort_unstable_by_key(|underline| underline.order);
self.monochrome_sprites
.sort_unstable_by_key(|sprite| sprite.order);
self.polychrome_sprites
.sort_unstable_by_key(|sprite| sprite.order);
self.surfaces.sort_unstable_by_key(|surface| surface.order);
}
}
@@ -208,31 +236,25 @@ impl<'a> Iterator for BatchIterator<'a> {
fn next(&mut self) -> Option<Self::Item> {
let mut orders_and_kinds = [
(
self.shadows_iter.peek().map(|s| s.draw_order),
self.shadows_iter.peek().map(|s| s.order),
PrimitiveKind::Shadow,
),
(self.quads_iter.peek().map(|q| q.order), PrimitiveKind::Quad),
(self.paths_iter.peek().map(|q| q.order), PrimitiveKind::Path),
(
self.quads_iter.peek().map(|q| q.draw_order),
PrimitiveKind::Quad,
),
(
self.paths_iter.peek().map(|q| q.draw_order),
PrimitiveKind::Path,
),
(
self.underlines_iter.peek().map(|u| u.draw_order),
self.underlines_iter.peek().map(|u| u.order),
PrimitiveKind::Underline,
),
(
self.monochrome_sprites_iter.peek().map(|s| s.draw_order),
self.monochrome_sprites_iter.peek().map(|s| s.order),
PrimitiveKind::MonochromeSprite,
),
(
self.polychrome_sprites_iter.peek().map(|s| s.draw_order),
self.polychrome_sprites_iter.peek().map(|s| s.order),
PrimitiveKind::PolychromeSprite,
),
(
self.surfaces_iter.peek().map(|s| s.draw_order),
self.surfaces_iter.peek().map(|s| s.order),
PrimitiveKind::Surface,
),
];
@@ -253,7 +275,7 @@ impl<'a> Iterator for BatchIterator<'a> {
self.shadows_iter.next();
while self
.shadows_iter
.next_if(|shadow| (shadow.draw_order, batch_kind) < max_order_and_kind)
.next_if(|shadow| (shadow.order, batch_kind) < max_order_and_kind)
.is_some()
{
shadows_end += 1;
@@ -269,7 +291,7 @@ impl<'a> Iterator for BatchIterator<'a> {
self.quads_iter.next();
while self
.quads_iter
.next_if(|quad| (quad.draw_order, batch_kind) < max_order_and_kind)
.next_if(|quad| (quad.order, batch_kind) < max_order_and_kind)
.is_some()
{
quads_end += 1;
@@ -283,7 +305,7 @@ impl<'a> Iterator for BatchIterator<'a> {
self.paths_iter.next();
while self
.paths_iter
.next_if(|path| (path.draw_order, batch_kind) < max_order_and_kind)
.next_if(|path| (path.order, batch_kind) < max_order_and_kind)
.is_some()
{
paths_end += 1;
@@ -297,7 +319,7 @@ impl<'a> Iterator for BatchIterator<'a> {
self.underlines_iter.next();
while self
.underlines_iter
.next_if(|underline| (underline.draw_order, batch_kind) < max_order_and_kind)
.next_if(|underline| (underline.order, batch_kind) < max_order_and_kind)
.is_some()
{
underlines_end += 1;
@@ -315,7 +337,7 @@ impl<'a> Iterator for BatchIterator<'a> {
while self
.monochrome_sprites_iter
.next_if(|sprite| {
(sprite.draw_order, batch_kind) < max_order_and_kind
(sprite.order, batch_kind) < max_order_and_kind
&& sprite.tile.texture_id == texture_id
})
.is_some()
@@ -336,7 +358,7 @@ impl<'a> Iterator for BatchIterator<'a> {
while self
.polychrome_sprites_iter
.next_if(|sprite| {
(sprite.draw_order, batch_kind) < max_order_and_kind
(sprite.order, batch_kind) < max_order_and_kind
&& sprite.tile.texture_id == texture_id
})
.is_some()
@@ -355,7 +377,7 @@ impl<'a> Iterator for BatchIterator<'a> {
self.surfaces_iter.next();
while self
.surfaces_iter
.next_if(|surface| (surface.draw_order, batch_kind) < max_order_and_kind)
.next_if(|surface| (surface.order, batch_kind) < max_order_and_kind)
.is_some()
{
surfaces_end += 1;
@@ -437,7 +459,9 @@ pub(crate) enum PrimitiveBatch<'a> {
#[derive(Default, Debug, Clone, Eq, PartialEq)]
#[repr(C)]
pub(crate) struct Quad {
pub draw_order: u32,
pub view_id: ViewId,
pub layer_id: LayerId,
pub order: DrawOrder,
pub bounds: Bounds<ScaledPixels>,
pub content_mask: ContentMask<ScaledPixels>,
pub background: Hsla,
@@ -448,7 +472,7 @@ pub(crate) struct Quad {
impl Ord for Quad {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.draw_order.cmp(&other.draw_order)
self.order.cmp(&other.order)
}
}
@@ -467,7 +491,9 @@ impl From<Quad> for Primitive {
#[derive(Debug, Clone, Eq, PartialEq)]
#[repr(C)]
pub(crate) struct Underline {
pub draw_order: u32,
pub view_id: ViewId,
pub layer_id: LayerId,
pub order: DrawOrder,
pub bounds: Bounds<ScaledPixels>,
pub content_mask: ContentMask<ScaledPixels>,
pub color: Hsla,
@@ -477,7 +503,7 @@ pub(crate) struct Underline {
impl Ord for Underline {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.draw_order.cmp(&other.draw_order)
self.order.cmp(&other.order)
}
}
@@ -496,7 +522,9 @@ impl From<Underline> for Primitive {
#[derive(Debug, Clone, Eq, PartialEq)]
#[repr(C)]
pub(crate) struct Shadow {
pub draw_order: u32,
pub view_id: ViewId,
pub layer_id: LayerId,
pub order: DrawOrder,
pub bounds: Bounds<ScaledPixels>,
pub corner_radii: Corners<ScaledPixels>,
pub content_mask: ContentMask<ScaledPixels>,
@@ -507,7 +535,7 @@ pub(crate) struct Shadow {
impl Ord for Shadow {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.draw_order.cmp(&other.draw_order)
self.order.cmp(&other.order)
}
}
@@ -526,13 +554,30 @@ impl From<Shadow> for Primitive {
#[derive(Clone, Debug, Eq, PartialEq)]
#[repr(C)]
pub(crate) struct MonochromeSprite {
pub draw_order: u32,
pub view_id: ViewId,
pub layer_id: LayerId,
pub order: DrawOrder,
pub bounds: Bounds<ScaledPixels>,
pub content_mask: ContentMask<ScaledPixels>,
pub color: Hsla,
pub tile: AtlasTile,
}
impl Ord for MonochromeSprite {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match self.order.cmp(&other.order) {
std::cmp::Ordering::Equal => self.tile.tile_id.cmp(&other.tile.tile_id),
order => order,
}
}
}
impl PartialOrd for MonochromeSprite {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl From<MonochromeSprite> for Primitive {
fn from(sprite: MonochromeSprite) -> Self {
Primitive::MonochromeSprite(sprite)
@@ -542,7 +587,9 @@ impl From<MonochromeSprite> for Primitive {
#[derive(Clone, Debug, Eq, PartialEq)]
#[repr(C)]
pub(crate) struct PolychromeSprite {
pub draw_order: u32,
pub view_id: ViewId,
pub layer_id: LayerId,
pub order: DrawOrder,
pub bounds: Bounds<ScaledPixels>,
pub content_mask: ContentMask<ScaledPixels>,
pub corner_radii: Corners<ScaledPixels>,
@@ -551,6 +598,21 @@ pub(crate) struct PolychromeSprite {
pub pad: u32, // align to 8 bytes
}
impl Ord for PolychromeSprite {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match self.order.cmp(&other.order) {
std::cmp::Ordering::Equal => self.tile.tile_id.cmp(&other.tile.tile_id),
order => order,
}
}
}
impl PartialOrd for PolychromeSprite {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl From<PolychromeSprite> for Primitive {
fn from(sprite: PolychromeSprite) -> Self {
Primitive::PolychromeSprite(sprite)
@@ -559,13 +621,27 @@ impl From<PolychromeSprite> for Primitive {
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct Surface {
pub draw_order: u32,
pub view_id: ViewId,
pub layer_id: LayerId,
pub order: DrawOrder,
pub bounds: Bounds<ScaledPixels>,
pub content_mask: ContentMask<ScaledPixels>,
#[cfg(target_os = "macos")]
pub image_buffer: media::core_video::CVImageBuffer,
}
impl Ord for Surface {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.order.cmp(&other.order)
}
}
impl PartialOrd for Surface {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl From<Surface> for Primitive {
fn from(surface: Surface) -> Self {
Primitive::Surface(surface)
@@ -578,8 +654,10 @@ pub(crate) struct PathId(pub(crate) usize);
/// A line made up of a series of vertices and control points.
#[derive(Debug)]
pub struct Path<P: Clone + Default + Debug> {
pub(crate) draw_order: u32,
pub(crate) id: PathId,
pub(crate) view_id: ViewId,
layer_id: LayerId,
order: DrawOrder,
pub(crate) bounds: Bounds<P>,
pub(crate) content_mask: ContentMask<P>,
pub(crate) vertices: Vec<PathVertex<P>>,
@@ -593,8 +671,10 @@ impl Path<Pixels> {
/// Create a new path with the given starting point.
pub fn new(start: Point<Pixels>) -> Self {
Self {
draw_order: 0,
id: PathId(0),
view_id: ViewId::default(),
layer_id: LayerId::default(),
order: DrawOrder::default(),
vertices: Vec::new(),
start,
current: start,
@@ -611,8 +691,10 @@ impl Path<Pixels> {
/// Scale this path by the given factor.
pub fn scale(&self, factor: f32) -> Path<ScaledPixels> {
Path {
draw_order: self.draw_order,
id: self.id,
view_id: self.view_id,
layer_id: self.layer_id,
order: self.order,
bounds: self.bounds.scale(factor),
content_mask: self.content_mask.scale(factor),
vertices: self
@@ -698,13 +780,13 @@ impl Eq for Path<ScaledPixels> {}
impl PartialEq for Path<ScaledPixels> {
fn eq(&self, other: &Self) -> bool {
self.draw_order == other.draw_order
self.order == other.order
}
}
impl Ord for Path<ScaledPixels> {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.draw_order.cmp(&other.draw_order)
self.order.cmp(&other.order)
}
}

View File

@@ -402,98 +402,88 @@ impl Style {
let rem_size = cx.rem_size();
cx.with_z_index(0, |cx| {
cx.paint_shadows(
bounds,
self.corner_radii.to_pixels(bounds.size, rem_size),
&self.box_shadow,
if self.is_border_visible() {
let corner_radii = self.corner_radii.to_pixels(bounds.size, rem_size);
let border_widths = self.border_widths.to_pixels(rem_size);
let max_border_width = border_widths.max();
let max_corner_radius = corner_radii.max();
let top_bounds = Bounds::from_corners(
bounds.origin,
bounds.upper_right() + point(Pixels::ZERO, max_border_width.max(max_corner_radius)),
);
});
let bottom_bounds = Bounds::from_corners(
bounds.lower_left() - point(Pixels::ZERO, max_border_width.max(max_corner_radius)),
bounds.lower_right(),
);
let left_bounds = Bounds::from_corners(
top_bounds.lower_left(),
bottom_bounds.origin + point(max_border_width, Pixels::ZERO),
);
let right_bounds = Bounds::from_corners(
top_bounds.lower_right() - point(max_border_width, Pixels::ZERO),
bottom_bounds.upper_right(),
);
let mut background = self.border_color.unwrap_or_default();
background.a = 0.;
let quad = quad(
bounds,
corner_radii,
background,
border_widths,
self.border_color.unwrap_or_default(),
);
cx.with_content_mask(Some(ContentMask { bounds: top_bounds }), |cx| {
cx.paint_quad(quad.clone());
});
cx.with_content_mask(
Some(ContentMask {
bounds: right_bounds,
}),
|cx| {
cx.paint_quad(quad.clone());
},
);
cx.with_content_mask(
Some(ContentMask {
bounds: bottom_bounds,
}),
|cx| {
cx.paint_quad(quad.clone());
},
);
cx.with_content_mask(
Some(ContentMask {
bounds: left_bounds,
}),
|cx| {
cx.paint_quad(quad);
},
);
}
continuation(cx);
let background_color = self.background.as_ref().and_then(Fill::color);
if background_color.map_or(false, |color| !color.is_transparent()) {
cx.with_z_index(1, |cx| {
let mut border_color = background_color.unwrap_or_default();
border_color.a = 0.;
cx.paint_quad(quad(
bounds,
self.corner_radii.to_pixels(bounds.size, rem_size),
background_color.unwrap_or_default(),
Edges::default(),
border_color,
));
});
let mut border_color = background_color.unwrap_or_default();
border_color.a = 0.;
cx.paint_quad(quad(
bounds,
self.corner_radii.to_pixels(bounds.size, rem_size),
background_color.unwrap_or_default(),
Edges::default(),
border_color,
));
}
cx.with_z_index(2, |cx| {
continuation(cx);
});
if self.is_border_visible() {
cx.with_z_index(3, |cx| {
let corner_radii = self.corner_radii.to_pixels(bounds.size, rem_size);
let border_widths = self.border_widths.to_pixels(rem_size);
let max_border_width = border_widths.max();
let max_corner_radius = corner_radii.max();
let top_bounds = Bounds::from_corners(
bounds.origin,
bounds.upper_right()
+ point(Pixels::ZERO, max_border_width.max(max_corner_radius)),
);
let bottom_bounds = Bounds::from_corners(
bounds.lower_left()
- point(Pixels::ZERO, max_border_width.max(max_corner_radius)),
bounds.lower_right(),
);
let left_bounds = Bounds::from_corners(
top_bounds.lower_left(),
bottom_bounds.origin + point(max_border_width, Pixels::ZERO),
);
let right_bounds = Bounds::from_corners(
top_bounds.lower_right() - point(max_border_width, Pixels::ZERO),
bottom_bounds.upper_right(),
);
let mut background = self.border_color.unwrap_or_default();
background.a = 0.;
let quad = quad(
bounds,
corner_radii,
background,
border_widths,
self.border_color.unwrap_or_default(),
);
cx.with_content_mask(Some(ContentMask { bounds: top_bounds }), |cx| {
cx.paint_quad(quad.clone());
});
cx.with_content_mask(
Some(ContentMask {
bounds: right_bounds,
}),
|cx| {
cx.paint_quad(quad.clone());
},
);
cx.with_content_mask(
Some(ContentMask {
bounds: bottom_bounds,
}),
|cx| {
cx.paint_quad(quad.clone());
},
);
cx.with_content_mask(
Some(ContentMask {
bounds: left_bounds,
}),
|cx| {
cx.paint_quad(quad);
},
);
});
}
cx.paint_shadows(
bounds,
self.corner_radii.to_pixels(bounds.size, rem_size),
&self.box_shadow,
);
#[cfg(debug_assertions)]
if self.debug_below {

View File

@@ -1,15 +1,14 @@
use crate::{
seal::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, Bounds,
ContentMask, Element, ElementContext, ElementId, Entity, EntityId, Flatten, FocusHandle,
FocusableView, FrameIndex, IntoElement, LayoutId, Model, Pixels, Point, Render, Size,
StackingOrder, Style, TextStyle, ViewContext, VisualContext, WeakModel,
FocusableView, IntoElement, LayoutId, Model, Pixels, Point, Render, Size, StackingOrder, Style,
TextStyle, ViewContext, VisualContext, WeakModel,
};
use anyhow::{Context, Result};
use std::{
any::{type_name, TypeId},
fmt,
hash::{Hash, Hasher},
ops::Range,
};
/// A view is a piece of state that can be presented on screen by implementing the [Render] trait.
@@ -25,16 +24,15 @@ impl<V> Sealed for View<V> {}
pub struct AnyViewState {
root_style: Style,
next_stacking_order_id: u16,
cache_state: Option<ViewCacheState>,
cache_key: Option<ViewCacheKey>,
element: Option<AnyElement>,
}
struct ViewCacheState {
struct ViewCacheKey {
bounds: Bounds<Pixels>,
stacking_order: StackingOrder,
content_mask: ContentMask<Pixels>,
text_style: TextStyle,
subframe_range: Range<FrameIndex>,
}
impl<V: 'static> Entity<V> for View<V> {
@@ -214,7 +212,8 @@ impl AnyView {
/// When using this method, the view's previous layout and paint will be recycled from the previous frame if [ViewContext::notify] has not been called since it was rendered.
/// The one exception is when [WindowContext::refresh] is called, in which case caching is ignored.
pub fn cached(mut self) -> Self {
self.cache = true;
// TODO!: ENABLE ME!
// self.cache = true;
self
}
@@ -299,7 +298,7 @@ impl Element for AnyView {
let state = AnyViewState {
root_style,
next_stacking_order_id: 0,
cache_state: None,
cache_key: None,
element: Some(element),
};
(layout_id, state)
@@ -313,25 +312,23 @@ impl Element for AnyView {
return;
}
if let Some(cache_state) = state.cache_state.as_mut() {
if cache_state.bounds == bounds
&& cache_state.content_mask == cx.content_mask()
&& cache_state.stacking_order == *cx.stacking_order()
&& cache_state.text_style == cx.text_style()
if let Some(cache_key) = state.cache_key.as_mut() {
if cache_key.bounds == bounds
&& cache_key.content_mask == cx.content_mask()
&& cache_key.stacking_order == *cx.stacking_order()
&& cache_key.text_style == cx.text_style()
{
cx.reuse_view(cache_state.subframe_range.clone());
cx.reuse_view(state.next_stacking_order_id);
return;
}
}
let subframe_start = cx.window.next_frame.current_index();
if let Some(mut element) = state.element.take() {
element.paint(cx);
} else {
let mut element = (self.request_layout)(self, cx).1;
element.draw(bounds.origin, bounds.size.into(), cx);
}
let subframe_end = cx.window.next_frame.current_index();
state.next_stacking_order_id = cx
.window
@@ -340,12 +337,11 @@ impl Element for AnyView {
.last()
.copied()
.unwrap();
state.cache_state = Some(ViewCacheState {
state.cache_key = Some(ViewCacheKey {
bounds,
stacking_order: cx.stacking_order().clone(),
content_mask: cx.content_mask(),
text_style: cx.text_style(),
subframe_range: subframe_start..subframe_end,
});
})
}

View File

@@ -1,8 +1,8 @@
use crate::{
frame::ActionListener, px, size, transparent_black, Action, AnyDrag, AnyView, AppContext,
Arena, AsyncWindowContext, AvailableSpace, Bounds, Context, Corners, CursorStyle, DisplayId,
Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, Flatten, Frame, Global,
GlobalElementId, Hsla, KeyBinding, KeyContext, KeyDownEvent, KeyMatch, KeymatchResult,
px, size, transparent_black, Action, AnyDrag, AnyView, AppContext, Arena, AsyncWindowContext,
AvailableSpace, Bounds, Context, Corners, CursorStyle, DispatchActionListener, DispatchNodeId,
DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, Flatten,
Global, GlobalElementId, Hsla, KeyBinding, KeyContext, KeyDownEvent, KeyMatch, KeymatchResult,
Keystroke, KeystrokeEvent, Model, ModelContext, Modifiers, MouseButton, MouseMoveEvent,
MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformWindow, Point,
PromptLevel, Render, ScaledPixels, SharedString, Size, SubscriberSet, Subscription,
@@ -126,7 +126,10 @@ impl FocusId {
/// Obtains whether this handle contains the given handle in the most recently rendered frame.
pub(crate) fn contains(&self, other: Self, cx: &WindowContext) -> bool {
cx.window.rendered_frame.focus_contains(*self, other)
cx.window
.rendered_frame
.dispatch_tree
.focus_contains(*self, other)
}
}
@@ -311,6 +314,13 @@ impl PendingInput {
}
}
pub(crate) struct ElementStateBox {
pub(crate) inner: Box<dyn Any>,
pub(crate) parent_view_id: EntityId,
#[cfg(debug_assertions)]
pub(crate) type_name: &'static str,
}
impl Window {
pub(crate) fn new(
handle: AnyWindowHandle,
@@ -439,8 +449,8 @@ impl Window {
layout_engine: Some(TaffyLayoutEngine::new()),
root_view: None,
element_id_stack: GlobalElementId::default(),
rendered_frame: Frame::new(cx.keymap.clone(), cx.actions.clone()),
next_frame: Frame::new(cx.keymap.clone(), cx.actions.clone()),
rendered_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
next_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
next_frame_callbacks,
dirty_views: FxHashSet::default(),
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
@@ -551,7 +561,10 @@ impl<'a> WindowContext<'a> {
}
self.window.focus = Some(handle.id);
self.window.rendered_frame.clear_pending_keystrokes();
self.window
.rendered_frame
.dispatch_tree
.clear_pending_keystrokes();
self.refresh();
}
@@ -578,10 +591,20 @@ impl<'a> WindowContext<'a> {
/// Dispatch the given action on the currently focused element.
pub fn dispatch_action(&mut self, action: Box<dyn Action>) {
let focus_id = self.window.focus;
let focus_handle = self.focused();
self.defer(move |cx| {
let node_id = focus_handle
.and_then(|handle| {
cx.window
.rendered_frame
.dispatch_tree
.focusable_node_id(handle.id)
})
.unwrap_or_else(|| cx.window.rendered_frame.dispatch_tree.root_node_id());
cx.propagate_event = true;
cx.dispatch_action_on(focus_id, action);
cx.dispatch_action_on_node(node_id, action);
})
}
@@ -609,8 +632,14 @@ impl<'a> WindowContext<'a> {
}
pub(crate) fn clear_pending_keystrokes(&mut self) {
self.window.rendered_frame.clear_pending_keystrokes();
self.window.next_frame.clear_pending_keystrokes();
self.window
.rendered_frame
.dispatch_tree
.clear_pending_keystrokes();
self.window
.next_frame
.dispatch_tree
.clear_pending_keystrokes();
}
/// Schedules the given function to be run at the end of the current effect cycle, allowing entities
@@ -805,9 +834,19 @@ impl<'a> WindowContext<'a> {
/// Determine whether the given action is available along the dispatch path to the currently focused element.
pub fn is_action_available(&self, action: &dyn Action) -> bool {
let target = self
.focused()
.and_then(|focused_handle| {
self.window
.rendered_frame
.dispatch_tree
.focusable_node_id(focused_handle.id)
})
.unwrap_or_else(|| self.window.rendered_frame.dispatch_tree.root_node_id());
self.window
.rendered_frame
.is_action_available(action, self.window.focus)
.dispatch_tree
.is_action_available(action, target)
}
/// The position of the mouse relative to the window.
@@ -824,38 +863,36 @@ impl<'a> WindowContext<'a> {
/// on top of the given level. Layers who are extensions of the queried layer
/// are not considered to be on top of queried layer.
pub fn was_top_layer(&self, point: &Point<Pixels>, layer: &StackingOrder) -> bool {
todo!()
// Precondition: the depth map is ordered from topmost to bottomost.
// // Precondition: the depth map is ordered from topmost to bottomost.
for (opaque_layer, _, bounds) in self.window.rendered_frame.depth_map.iter() {
if layer >= opaque_layer {
// The queried layer is either above or is the same as the this opaque layer.
// Anything after this point is guaranteed to be below the queried layer.
return true;
}
// for (opaque_layer, _, bounds) in self.window.rendered_frame.depth_map.iter() {
// if layer >= opaque_layer {
// // The queried layer is either above or is the same as the this opaque layer.
// // Anything after this point is guaranteed to be below the queried layer.
// return true;
// }
if !bounds.contains(point) {
// This opaque layer is above the queried layer but it doesn't contain
// the given position, so we can ignore it even if it's above.
continue;
}
// if !bounds.contains(point) {
// // This opaque layer is above the queried layer but it doesn't contain
// // the given position, so we can ignore it even if it's above.
// continue;
// }
// At this point, we've established that this opaque layer is on top of the queried layer
// and contains the position:
// If neither the opaque layer or the queried layer is an extension of the other then
// we know they are on different stacking orders, and return false.
let is_on_same_layer = opaque_layer
.iter()
.zip(layer.iter())
.all(|(a, b)| a.z_index == b.z_index);
// // At this point, we've established that this opaque layer is on top of the queried layer
// // and contains the position:
// // If neither the opaque layer or the queried layer is an extension of the other then
// // we know they are on different stacking orders, and return false.
// let is_on_same_layer = opaque_layer
// .iter()
// .zip(layer.iter())
// .all(|(a, b)| a.z_index == b.z_index);
if !is_on_same_layer {
return false;
}
}
// if !is_on_same_layer {
// return false;
// }
// }
// true
true
}
pub(crate) fn was_top_layer_under_active_drag(
@@ -863,61 +900,67 @@ impl<'a> WindowContext<'a> {
point: &Point<Pixels>,
layer: &StackingOrder,
) -> bool {
todo!()
// Precondition: the depth map is ordered from topmost to bottomost.
// for (opaque_layer, _, bounds) in self.window.rendered_frame.depth_map.iter() {
// if layer >= opaque_layer {
// // The queried layer is either above or is the same as the this opaque layer.
// // Anything after this point is guaranteed to be below the queried layer.
// return true;
// }
for (opaque_layer, _, bounds) in self.window.rendered_frame.depth_map.iter() {
if layer >= opaque_layer {
// The queried layer is either above or is the same as the this opaque layer.
// Anything after this point is guaranteed to be below the queried layer.
return true;
}
// if !bounds.contains(point) {
// // This opaque layer is above the queried layer but it doesn't contain
// // the given position, so we can ignore it even if it's above.
// continue;
// }
if !bounds.contains(point) {
// This opaque layer is above the queried layer but it doesn't contain
// the given position, so we can ignore it even if it's above.
continue;
}
// // All normal content is rendered with a base z-index of 0, we know that if the root of this opaque layer
// // equals `ACTIVE_DRAG_Z_INDEX` then it must be the drag layer and we can ignore it as we are
// // looking to see if the queried layer was the topmost underneath the drag layer.
// if opaque_layer
// .first()
// .map(|c| c.z_index == ACTIVE_DRAG_Z_INDEX)
// .unwrap_or(false)
// {
// continue;
// }
// All normal content is rendered with a base z-index of 0, we know that if the root of this opaque layer
// equals `ACTIVE_DRAG_Z_INDEX` then it must be the drag layer and we can ignore it as we are
// looking to see if the queried layer was the topmost underneath the drag layer.
if opaque_layer
.first()
.map(|c| c.z_index == ACTIVE_DRAG_Z_INDEX)
.unwrap_or(false)
{
continue;
}
// // At this point, we've established that this opaque layer is on top of the queried layer
// // and contains the position:
// // If neither the opaque layer or the queried layer is an extension of the other then
// // we know they are on different stacking orders, and return false.
// let is_on_same_layer = opaque_layer
// .iter()
// .zip(layer.iter())
// .all(|(a, b)| a.z_index == b.z_index);
// At this point, we've established that this opaque layer is on top of the queried layer
// and contains the position:
// If neither the opaque layer or the queried layer is an extension of the other then
// we know they are on different stacking orders, and return false.
let is_on_same_layer = opaque_layer
.iter()
.zip(layer.iter())
.all(|(a, b)| a.z_index == b.z_index);
// if !is_on_same_layer {
// return false;
// }
// }
if !is_on_same_layer {
return false;
}
}
// true
true
}
/// Called during painting to get the current stacking order.
pub fn stacking_order(&self) -> &StackingOrder {
todo!()
&self.window.next_frame.z_index_stack
}
/// Produces a new frame and assigns it to `rendered_frame`. To actually show
/// the contents of the new [Scene], use [present].
#[profiling::function]
pub fn draw(&mut self) {
self.window.dirty.set(false);
self.window.drawing = true;
if let Some(requested_handler) = self.window.rendered_frame.requested_input_handler.as_mut()
{
let input_handler = self.window.platform_window.take_input_handler();
requested_handler.handler = input_handler;
}
let root_view = self.window.root_view.take().unwrap();
self.with_element_context(|cx| {
cx.with_z_index(0, |cx| {
@@ -925,7 +968,7 @@ impl<'a> WindowContext<'a> {
// We need to use cx.cx here so we can utilize borrow splitting
for (action_type, action_listeners) in &cx.cx.app.global_action_listeners {
for action_listener in action_listeners.iter().cloned() {
cx.cx.window.next_frame.on_action(
cx.cx.window.next_frame.dispatch_tree.on_action(
*action_type,
Rc::new(
move |action: &dyn Any, phase, cx: &mut WindowContext<'_>| {
@@ -968,14 +1011,15 @@ impl<'a> WindowContext<'a> {
}
self.window.dirty_views.clear();
self.window.next_frame.preserve_pending_keystrokes(
&mut self.window.rendered_frame.dispatch_tree,
self.window.focus,
);
self.window.next_frame.set_focus(self.window.focus);
self.window
.next_frame
.set_window_active(self.window.active.get());
.dispatch_tree
.preserve_pending_keystrokes(
&mut self.window.rendered_frame.dispatch_tree,
self.window.focus,
);
self.window.next_frame.focus = self.window.focus;
self.window.next_frame.window_active = self.window.active.get();
self.window.root_view = Some(root_view);
// Set the cursor only if we're the active window.
@@ -999,10 +1043,9 @@ impl<'a> WindowContext<'a> {
self.window.layout_engine.as_mut().unwrap().clear();
self.text_system()
.finish_frame(&self.window.next_frame.reused_views);
// todo!()
// self.window
// .next_frame
// .finish(&mut self.window.rendered_frame);
self.window
.next_frame
.finish(&mut self.window.rendered_frame);
ELEMENT_ARENA.with_borrow_mut(|element_arena| {
let percentage = (element_arena.len() as f32 / element_arena.capacity() as f32) * 100.;
if percentage >= 80. {
@@ -1050,11 +1093,13 @@ impl<'a> WindowContext<'a> {
self.window.needs_present.set(true);
}
#[profiling::function]
fn present(&self) {
self.window
.platform_window
.draw(&self.window.rendered_frame.scene);
self.window.needs_present.set(false);
profiling::finish_frame!();
}
/// Dispatch a given keystroke as though the user had typed it.
@@ -1090,6 +1135,7 @@ impl<'a> WindowContext<'a> {
}
/// Dispatch a mouse or keyboard event on the window.
#[profiling::function]
pub fn dispatch_event(&mut self, event: PlatformInput) -> bool {
self.window.last_input_timestamp.set(Instant::now());
// Handlers may set this to false by calling `stop_propagation`.
@@ -1184,54 +1230,56 @@ impl<'a> WindowContext<'a> {
}
fn dispatch_mouse_event(&mut self, event: &dyn Any) {
todo!()
// if let Some(mut handlers) = self
// .window
// .rendered_frame
// .mouse_listeners
// .remove(&event.type_id())
// {
// // Capture phase, events bubble from back to front. Handlers for this phase are used for
// // special purposes, such as detecting events outside of a given Bounds.
// for (_, _, handler) in &mut handlers {
// self.with_element_context(|cx| {
// handler(event, DispatchPhase::Capture, cx);
// });
// if !self.app.propagate_event {
// break;
// }
// }
if let Some(mut handlers) = self
.window
.rendered_frame
.mouse_listeners
.remove(&event.type_id())
{
// Because handlers may add other handlers, we sort every time.
handlers.sort_by(|(a, _, _), (b, _, _)| a.cmp(b));
// // Bubble phase, where most normal handlers do their work.
// if self.app.propagate_event {
// for (_, _, handler) in handlers.iter_mut().rev() {
// self.with_element_context(|cx| {
// handler(event, DispatchPhase::Bubble, cx);
// });
// if !self.app.propagate_event {
// break;
// }
// }
// }
// Capture phase, events bubble from back to front. Handlers for this phase are used for
// special purposes, such as detecting events outside of a given Bounds.
for (_, _, handler) in &mut handlers {
self.with_element_context(|cx| {
handler(event, DispatchPhase::Capture, cx);
});
if !self.app.propagate_event {
break;
}
}
// self.window
// .rendered_frame
// .mouse_listeners
// .insert(event.type_id(), handlers);
// }
// Bubble phase, where most normal handlers do their work.
if self.app.propagate_event {
for (_, _, handler) in handlers.iter_mut().rev() {
self.with_element_context(|cx| {
handler(event, DispatchPhase::Bubble, cx);
});
if !self.app.propagate_event {
break;
}
}
}
// if self.app.propagate_event && self.has_active_drag() {
// if event.is::<MouseMoveEvent>() {
// // If this was a mouse move event, redraw the window so that the
// // active drag can follow the mouse cursor.
// self.refresh();
// } else if event.is::<MouseUpEvent>() {
// // If this was a mouse up event, cancel the active drag and redraw
// // the window.
// self.active_drag = None;
// self.refresh();
// }
// }
self.window
.rendered_frame
.mouse_listeners
.insert(event.type_id(), handlers);
}
if self.app.propagate_event && self.has_active_drag() {
if event.is::<MouseMoveEvent>() {
// If this was a mouse move event, redraw the window so that the
// active drag can follow the mouse cursor.
self.refresh();
} else if event.is::<MouseUpEvent>() {
// If this was a mouse up event, cancel the active drag and redraw
// the window.
self.active_drag = None;
self.refresh();
}
}
}
fn dispatch_key_event(&mut self, event: &dyn Any) {
@@ -1239,14 +1287,29 @@ impl<'a> WindowContext<'a> {
self.draw();
}
let focus_id = self.window.focus;
let dispatch_path = self.window.rendered_frame.key_dispatch_path(focus_id);
let node_id = self
.window
.focus
.and_then(|focus_id| {
self.window
.rendered_frame
.dispatch_tree
.focusable_node_id(focus_id)
})
.unwrap_or_else(|| self.window.rendered_frame.dispatch_tree.root_node_id());
let dispatch_path = self
.window
.rendered_frame
.dispatch_tree
.dispatch_path(node_id);
if let Some(key_down_event) = event.downcast_ref::<KeyDownEvent>() {
let KeymatchResult { bindings, pending } = self
.window
.rendered_frame
.match_keystroke(&key_down_event.keystroke, focus_id);
.dispatch_tree
.dispatch_key(&key_down_event.keystroke, &dispatch_path);
if pending {
let mut currently_pending = self.window.pending_input.take().unwrap_or_default();
@@ -1276,11 +1339,8 @@ impl<'a> WindowContext<'a> {
self.window.pending_input = Some(currently_pending);
self.propagate_event = false;
return;
}
if let Some(currently_pending) = self.window.pending_input.take() {
} else if let Some(currently_pending) = self.window.pending_input.take() {
if bindings
.iter()
.all(|binding| !currently_pending.used_by_binding(binding))
@@ -1295,7 +1355,7 @@ impl<'a> WindowContext<'a> {
self.propagate_event = true;
for binding in bindings {
self.dispatch_action_on(focus_id, binding.action.boxed_clone());
self.dispatch_action_on_node(node_id, binding.action.boxed_clone());
if !self.propagate_event {
self.dispatch_keystroke_observers(event, Some(binding.action));
return;
@@ -1303,7 +1363,7 @@ impl<'a> WindowContext<'a> {
}
}
self.dispatch_key_down_up_event(event, focus_id);
self.dispatch_key_down_up_event(event, &dispatch_path);
if !self.propagate_event {
return;
}
@@ -1311,37 +1371,61 @@ impl<'a> WindowContext<'a> {
self.dispatch_keystroke_observers(event, None);
}
fn dispatch_key_down_up_event(&mut self, event: &dyn Any, focus_id: Option<FocusId>) {
fn dispatch_key_down_up_event(
&mut self,
event: &dyn Any,
dispatch_path: &SmallVec<[DispatchNodeId; 32]>,
) {
// Capture phase
let key_dispatch_path = self.window.rendered_frame.key_dispatch_path(focus_id);
for key_listener in &key_dispatch_path {
self.with_element_context(|cx| {
key_listener(event, DispatchPhase::Capture, cx);
});
if !self.propagate_event {
return;
for node_id in dispatch_path {
let node = self.window.rendered_frame.dispatch_tree.node(*node_id);
for key_listener in node.key_listeners.clone() {
self.with_element_context(|cx| {
key_listener(event, DispatchPhase::Capture, cx);
});
if !self.propagate_event {
return;
}
}
}
// Bubble phase
for key_listener in key_dispatch_path.iter().rev() {
self.with_element_context(|cx| {
key_listener(event, DispatchPhase::Bubble, cx);
});
if !self.propagate_event {
return;
for node_id in dispatch_path.iter().rev() {
// Handle low level key events
let node = self.window.rendered_frame.dispatch_tree.node(*node_id);
for key_listener in node.key_listeners.clone() {
self.with_element_context(|cx| {
key_listener(event, DispatchPhase::Bubble, cx);
});
if !self.propagate_event {
return;
}
}
}
}
/// Determine whether a potential multi-stroke key binding is in progress on this window.
pub fn has_pending_keystrokes(&self) -> bool {
self.window.rendered_frame.has_pending_keystrokes()
self.window
.rendered_frame
.dispatch_tree
.has_pending_keystrokes()
}
fn replay_pending_input(&mut self, currently_pending: PendingInput) {
let focus_id = self.window.focus;
if focus_id != currently_pending.focus {
let node_id = self
.window
.focus
.and_then(|focus_id| {
self.window
.rendered_frame
.dispatch_tree
.focusable_node_id(focus_id)
})
.unwrap_or_else(|| self.window.rendered_frame.dispatch_tree.root_node_id());
if self.window.focus != currently_pending.focus {
return;
}
@@ -1349,19 +1433,25 @@ impl<'a> WindowContext<'a> {
self.propagate_event = true;
for binding in currently_pending.bindings {
self.dispatch_action_on(focus_id, binding.action.boxed_clone());
self.dispatch_action_on_node(node_id, binding.action.boxed_clone());
if !self.propagate_event {
return;
}
}
let dispatch_path = self
.window
.rendered_frame
.dispatch_tree
.dispatch_path(node_id);
for keystroke in currently_pending.keystrokes {
let event = KeyDownEvent {
keystroke,
is_held: false,
};
self.dispatch_key_down_up_event(&event, focus_id);
self.dispatch_key_down_up_event(&event, &dispatch_path);
if !self.propagate_event {
return;
}
@@ -1375,43 +1465,52 @@ impl<'a> WindowContext<'a> {
}
}
fn dispatch_action_on(&mut self, focus_id: Option<FocusId>, action: Box<dyn Action>) {
let dispatch_path = self.window.rendered_frame.action_dispatch_path(focus_id);
fn dispatch_action_on_node(&mut self, node_id: DispatchNodeId, action: Box<dyn Action>) {
let dispatch_path = self
.window
.rendered_frame
.dispatch_tree
.dispatch_path(node_id);
// Capture phase
for ActionListener {
action_type,
listener,
} in &dispatch_path
{
let any_action = action.as_any();
if *action_type == any_action.type_id() {
self.with_element_context(|cx| {
listener(any_action, DispatchPhase::Capture, cx);
});
for node_id in &dispatch_path {
let node = self.window.rendered_frame.dispatch_tree.node(*node_id);
for DispatchActionListener {
action_type,
listener,
} in node.action_listeners.clone()
{
let any_action = action.as_any();
if action_type == any_action.type_id() {
self.with_element_context(|cx| {
listener(any_action, DispatchPhase::Capture, cx);
});
if !self.propagate_event {
return;
if !self.propagate_event {
return;
}
}
}
}
// Bubble phase
for ActionListener {
action_type,
listener,
} in dispatch_path.iter().rev()
{
let any_action = action.as_any();
if *action_type == any_action.type_id() {
self.propagate_event = false; // Actions stop propagation by default during the bubble phase
for node_id in dispatch_path.iter().rev() {
let node = self.window.rendered_frame.dispatch_tree.node(*node_id);
for DispatchActionListener {
action_type,
listener,
} in node.action_listeners.clone()
{
let any_action = action.as_any();
if action_type == any_action.type_id() {
self.propagate_event = false; // Actions stop propagation by default during the bubble phase
self.with_element_context(|cx| {
listener(any_action, DispatchPhase::Bubble, cx);
});
self.with_element_context(|cx| {
listener(any_action, DispatchPhase::Bubble, cx);
});
if !self.propagate_event {
return;
if !self.propagate_event {
return;
}
}
}
}
@@ -1472,16 +1571,32 @@ impl<'a> WindowContext<'a> {
/// Returns all available actions for the focused element.
pub fn available_actions(&self) -> Vec<Box<dyn Action>> {
let node_id = self
.window
.focus
.and_then(|focus_id| {
self.window
.rendered_frame
.dispatch_tree
.focusable_node_id(focus_id)
})
.unwrap_or_else(|| self.window.rendered_frame.dispatch_tree.root_node_id());
self.window
.rendered_frame
.available_actions(self.window.focus)
.dispatch_tree
.available_actions(node_id)
}
/// Returns key bindings that invoke the given action on the currently focused element.
pub fn bindings_for_action(&self, action: &dyn Action) -> Vec<KeyBinding> {
self.window
.rendered_frame
.bindings_for_action(action, self.window.focus)
.dispatch_tree
.bindings_for_action(
action,
&self.window.rendered_frame.dispatch_tree.context_stack,
)
}
/// Returns any bindings that would invoke the given action on the given focus handle if it were focused.
@@ -1490,9 +1605,17 @@ impl<'a> WindowContext<'a> {
action: &dyn Action,
focus_handle: &FocusHandle,
) -> Vec<KeyBinding> {
self.window
.rendered_frame
.bindings_for_action(action, Some(focus_handle.id))
let dispatch_tree = &self.window.rendered_frame.dispatch_tree;
let Some(node_id) = dispatch_tree.focusable_node_id(focus_handle.id) else {
return vec![];
};
let context_stack: Vec<_> = dispatch_tree
.dispatch_path(node_id)
.into_iter()
.filter_map(|node_id| dispatch_tree.node(node_id).context.clone())
.collect();
dispatch_tree.bindings_for_action(action, &context_stack)
}
/// Returns a generic event listener that invokes the given listener with the view and context associated with the given view handle.
@@ -1528,6 +1651,15 @@ impl<'a> WindowContext<'a> {
.on_should_close(Box::new(move || this.update(|cx| f(cx)).unwrap_or(true)))
}
pub(crate) fn parent_view_id(&self) -> EntityId {
*self
.window
.next_frame
.view_stack
.last()
.expect("a view should always be on the stack while drawing")
}
/// Register an action listener on the window for the next frame. The type of action
/// is determined by the first parameter of the given listener. When the next frame is rendered
/// the listener will be cleared.
@@ -1541,6 +1673,7 @@ impl<'a> WindowContext<'a> {
) {
self.window
.next_frame
.dispatch_tree
.on_action(action_type, Rc::new(listener));
}
}
@@ -1952,6 +2085,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
for view_id in self
.window
.rendered_frame
.dispatch_tree
.view_path(self.view.entity_id())
.into_iter()
.rev()

View File

@@ -16,13 +16,12 @@ use std::{
any::{Any, TypeId},
borrow::{Borrow, BorrowMut, Cow},
mem,
ops::Range,
rc::Rc,
sync::Arc,
};
use anyhow::Result;
use collections::FxHashMap;
use collections::{FxHashMap, FxHashSet};
use derive_more::{Deref, DerefMut};
#[cfg(target_os = "macos")]
use media::core_video::CVImageBuffer;
@@ -35,135 +34,132 @@ use crate::{
EntityId, FocusHandle, FocusId, FontId, GlobalElementId, GlyphId, Hsla, ImageData,
InputHandler, IsZero, KeyContext, KeyEvent, LayoutId, MonochromeSprite, MouseEvent, PaintQuad,
Path, Pixels, PlatformInputHandler, Point, PolychromeSprite, Quad, RenderGlyphParams,
RenderImageParams, RenderSvgParams, Scene, SceneIndex, Shadow, SharedString, Size,
StackingContext, StackingOrder, StrikethroughStyle, Style, TextStyleRefinement, Underline,
UnderlineStyle, Window, WindowContext, SUBPIXEL_VARIANTS,
RenderImageParams, RenderSvgParams, Scene, Shadow, SharedString, Size, StackingContext,
StackingOrder, StrikethroughStyle, Style, TextStyleRefinement, Underline, UnderlineStyle,
Window, WindowContext, SUBPIXEL_VARIANTS,
};
type AnyMouseListener = Box<dyn FnMut(&dyn Any, DispatchPhase, &mut ElementContext) + 'static>;
pub(crate) struct RequestedInputHandler {
pub(crate) view_id: EntityId,
pub(crate) handler: Option<PlatformInputHandler>,
}
pub(crate) struct TooltipRequest {
pub(crate) view_id: EntityId,
pub(crate) tooltip: AnyTooltip,
}
/// Identifies a moment in time during construction of a frame. Used for reusing cached subsets of a previous frame.
#[derive(Clone)]
pub(crate) struct FrameIndex {
mouse_listeners: usize,
dispatch_nodes: usize,
scene_index: SceneIndex,
pub(crate) struct Frame {
pub(crate) focus: Option<FocusId>,
pub(crate) window_active: bool,
pub(crate) element_states: FxHashMap<GlobalElementId, ElementStateBox>,
pub(crate) mouse_listeners: FxHashMap<TypeId, Vec<(StackingOrder, EntityId, AnyMouseListener)>>,
pub(crate) dispatch_tree: DispatchTree,
pub(crate) scene: Scene,
pub(crate) depth_map: Vec<(StackingOrder, EntityId, Bounds<Pixels>)>,
pub(crate) z_index_stack: StackingOrder,
pub(crate) next_stacking_order_ids: Vec<u16>,
pub(crate) next_root_z_index: u16,
pub(crate) content_mask_stack: Vec<ContentMask<Pixels>>,
pub(crate) element_offset_stack: Vec<Point<Pixels>>,
pub(crate) requested_input_handler: Option<RequestedInputHandler>,
pub(crate) tooltip_request: Option<TooltipRequest>,
pub(crate) cursor_styles: FxHashMap<EntityId, CursorStyle>,
pub(crate) requested_cursor_style: Option<CursorStyle>,
pub(crate) view_stack: Vec<EntityId>,
pub(crate) reused_views: FxHashSet<EntityId>,
#[cfg(any(test, feature = "test-support"))]
pub(crate) debug_bounds: FxHashMap<String, Bounds<Pixels>>,
}
// pub(crate) struct Frame {
// pub(crate) focus: Option<FocusId>,
// pub(crate) window_active: bool,
// pub(crate) element_states: FxHashMap<GlobalElementId, ElementStateBox>,
// pub(crate) mouse_listeners: FxHashMap<TypeId, Vec<(StackingOrder, EntityId, AnyMouseListener)>>,
// pub(crate) input_handlers: Vec<PlatformInputHandler>,
// pub(crate) dispatch_tree: DispatchTree,
// pub(crate) scene: Scene,
// pub(crate) depth_map: Vec<(StackingOrder, EntityId, Bounds<Pixels>)>,
// pub(crate) z_index_stack: StackingOrder,
// pub(crate) next_stacking_order_ids: Vec<u16>,
// pub(crate) next_root_z_index: u16,
// pub(crate) content_mask_stack: Vec<ContentMask<Pixels>>,
// pub(crate) element_offset_stack: Vec<Point<Pixels>>,
// pub(crate) tooltip_request: Option<TooltipRequest>,
// pub(crate) cursor_styles: FxHashMap<EntityId, CursorStyle>,
// pub(crate) requested_cursor_style: Option<CursorStyle>,
// pub(crate) view_stack: Vec<EntityId>,
impl Frame {
pub(crate) fn new(dispatch_tree: DispatchTree) -> Self {
Frame {
focus: None,
window_active: false,
element_states: FxHashMap::default(),
mouse_listeners: FxHashMap::default(),
dispatch_tree,
scene: Scene::default(),
depth_map: Vec::new(),
z_index_stack: StackingOrder::default(),
next_stacking_order_ids: vec![0],
next_root_z_index: 0,
content_mask_stack: Vec::new(),
element_offset_stack: Vec::new(),
requested_input_handler: None,
tooltip_request: None,
cursor_styles: FxHashMap::default(),
requested_cursor_style: None,
view_stack: Vec::new(),
reused_views: FxHashSet::default(),
// #[cfg(any(test, feature = "test-support"))]
// pub(crate) debug_bounds: FxHashMap<String, Bounds<Pixels>>,
// }
#[cfg(any(test, feature = "test-support"))]
debug_bounds: FxHashMap::default(),
}
}
// impl Frame {
// pub(crate) fn new(dispatch_tree: DispatchTree) -> Self {
// Frame {
// focus: None,
// window_active: false,
// element_states: FxHashMap::default(),
// mouse_listeners: FxHashMap::default(),
// input_handlers: Vec::new(),
// dispatch_tree,
// scene: Scene::default(),
// depth_map: Vec::new(),
// z_index_stack: StackingOrder::default(),
// next_stacking_order_ids: vec![0],
// next_root_z_index: 0,
// content_mask_stack: Vec::new(),
// element_offset_stack: Vec::new(),
// tooltip_request: None,
// cursor_styles: FxHashMap::default(),
// requested_cursor_style: None,
// view_stack: Vec::new(),
pub(crate) fn clear(&mut self) {
self.element_states.clear();
self.mouse_listeners.values_mut().for_each(Vec::clear);
self.dispatch_tree.clear();
self.depth_map.clear();
self.next_stacking_order_ids = vec![0];
self.next_root_z_index = 0;
self.reused_views.clear();
self.scene.clear();
self.requested_input_handler.take();
self.tooltip_request.take();
self.cursor_styles.clear();
self.requested_cursor_style.take();
debug_assert_eq!(self.view_stack.len(), 0);
}
// #[cfg(any(test, feature = "test-support"))]
// debug_bounds: FxHashMap::default(),
// }
// }
pub(crate) fn focus_path(&self) -> SmallVec<[FocusId; 8]> {
self.focus
.map(|focus_id| self.dispatch_tree.focus_path(focus_id))
.unwrap_or_default()
}
// pub(crate) fn clear(&mut self) {
// self.element_states.clear();
// self.mouse_listeners.values_mut().for_each(Vec::clear);
// self.input_handlers.clear();
// self.dispatch_tree.clear();
// self.depth_map.clear();
// self.next_stacking_order_ids = vec![0];
// self.next_root_z_index = 0;
// self.scene.clear();
// self.tooltip_request.take();
// self.cursor_styles.clear();
// self.requested_cursor_style.take();
// debug_assert_eq!(self.view_stack.len(), 0);
// }
pub(crate) fn finish(&mut self, prev_frame: &mut Self) {
// Reuse mouse listeners that didn't change since the last frame.
for (type_id, listeners) in &mut prev_frame.mouse_listeners {
let next_listeners = self.mouse_listeners.entry(*type_id).or_default();
for (order, view_id, listener) in listeners.drain(..) {
if self.reused_views.contains(&view_id) {
next_listeners.push((order, view_id, listener));
}
}
}
// pub(crate) fn focus_path(&self) -> SmallVec<[FocusId; 8]> {
// self.focus
// .map(|focus_id| self.dispatch_tree.focus_path(focus_id))
// .unwrap_or_default()
// }
// Reuse entries in the depth map that didn't change since the last frame.
for (order, view_id, bounds) in prev_frame.depth_map.drain(..) {
if self.reused_views.contains(&view_id) {
match self
.depth_map
.binary_search_by(|(level, _, _)| order.cmp(level))
{
Ok(i) | Err(i) => self.depth_map.insert(i, (order, view_id, bounds)),
}
}
}
// pub(crate) fn current_index(&self) -> FrameIndex {
// FrameIndex {
// scene_index: self.scene.current_index(),
// mouse_listeners: self.mouse_listeners.len(),
// dispatch_nodes: self.dispatch_tree.len(),
// }
// }
// Retain element states for views that didn't change since the last frame.
for (element_id, state) in prev_frame.element_states.drain() {
if self.reused_views.contains(&state.parent_view_id) {
self.element_states.entry(element_id).or_insert(state);
}
}
// pub(crate) fn finish(&mut self, prev_frame: &mut Self) {
// // Reuse mouse listeners that didn't change since the last frame.
// for (type_id, listeners) in &mut prev_frame.mouse_listeners {
// let next_listeners = self.mouse_listeners.entry(*type_id).or_default();
// for (order, view_id, listener) in listeners.drain(..) {
// if self.reused_views.contains(&view_id) {
// next_listeners.push((order, view_id, listener));
// }
// }
// }
// // Reuse entries in the depth map that didn't change since the last frame.
// for (order, view_id, bounds) in prev_frame.depth_map.drain(..) {
// if self.reused_views.contains(&view_id) {
// match self
// .depth_map
// .binary_search_by(|(level, _, _)| order.cmp(level))
// {
// Ok(i) | Err(i) => self.depth_map.insert(i, (order, view_id, bounds)),
// }
// }
// }
// // Retain element states for views that didn't change since the last frame.
// for (element_id, state) in prev_frame.element_states.drain() {
// if self.reused_views.contains(&state.parent_view_id) {
// self.element_states.entry(element_id).or_insert(state);
// }
// }
// }
// }
// Reuse geometry that didn't change since the last frame.
self.scene
.reuse_views(&self.reused_views, &mut prev_frame.scene);
self.scene.finish();
}
}
/// This context is used for assisting in the implementation of the element trait
#[derive(Deref, DerefMut)]
@@ -314,9 +310,8 @@ impl<'a> VisualContext for ElementContext<'a> {
}
impl<'a> ElementContext<'a> {
pub(crate) fn reuse_view(&mut self, subframe_range: Range<FrameIndex>) {
pub(crate) fn reuse_view(&mut self, next_stacking_order_id: u16) {
let view_id = self.parent_view_id();
let grafted_view_ids = self
.cx
.window
@@ -656,7 +651,6 @@ impl<'a> ElementContext<'a> {
}
})
}
/// Paint one or more drop shadows into the scene for the next frame at the current z-index.
pub fn paint_shadows(
&mut self,
@@ -672,18 +666,20 @@ impl<'a> ElementContext<'a> {
let mut shadow_bounds = bounds;
shadow_bounds.origin += shadow.offset;
shadow_bounds.dilate(shadow.spread_radius);
window
.next_frame
.scene
.paint_primitive(|draw_order| Shadow {
draw_order,
window.next_frame.scene.insert(
&window.next_frame.z_index_stack,
Shadow {
view_id: view_id.into(),
layer_id: 0,
order: 0,
bounds: shadow_bounds.scale(scale_factor),
content_mask: content_mask.scale(scale_factor),
corner_radii: corner_radii.scale(scale_factor),
color: shadow.color,
blur_radius: shadow.blur_radius.scale(scale_factor),
pad: 0,
});
},
);
}
}
@@ -696,15 +692,20 @@ impl<'a> ElementContext<'a> {
let view_id = self.parent_view_id();
let window = &mut *self.window;
window.next_frame.scene.paint_primitive(|draw_order| Quad {
draw_order,
bounds: quad.bounds.scale(scale_factor),
content_mask: content_mask.scale(scale_factor),
background: quad.background,
border_color: quad.border_color,
corner_radii: quad.corner_radii.scale(scale_factor),
border_widths: quad.border_widths.scale(scale_factor),
});
window.next_frame.scene.insert(
&window.next_frame.z_index_stack,
Quad {
view_id: view_id.into(),
layer_id: 0,
order: 0,
bounds: quad.bounds.scale(scale_factor),
content_mask: content_mask.scale(scale_factor),
background: quad.background,
border_color: quad.border_color,
corner_radii: quad.corner_radii.scale(scale_factor),
border_widths: quad.border_widths.scale(scale_factor),
},
);
}
/// Paint the given `Path` into the scene for the next frame at the current z-index.
@@ -715,11 +716,12 @@ impl<'a> ElementContext<'a> {
path.content_mask = content_mask;
path.color = color.into();
path.view_id = view_id.into();
let window = &mut *self.window;
window.next_frame.scene.paint_primitive(|draw_order| {
path.draw_order = draw_order;
path.scale(scale_factor)
});
window
.next_frame
.scene
.insert(&window.next_frame.z_index_stack, path.scale(scale_factor));
}
/// Paint an underline into the scene for the next frame at the current z-index.
@@ -743,17 +745,19 @@ impl<'a> ElementContext<'a> {
let view_id = self.parent_view_id();
let window = &mut *self.window;
window
.next_frame
.scene
.paint_primitive(|draw_order| Underline {
draw_order,
window.next_frame.scene.insert(
&window.next_frame.z_index_stack,
Underline {
view_id: view_id.into(),
layer_id: 0,
order: 0,
bounds: bounds.scale(scale_factor),
content_mask: content_mask.scale(scale_factor),
color: style.color.unwrap_or_default(),
thickness: style.thickness.scale(scale_factor),
wavy: style.wavy,
});
},
);
}
/// Paint a strikethrough into the scene for the next frame at the current z-index.
@@ -773,17 +777,19 @@ impl<'a> ElementContext<'a> {
let view_id = self.parent_view_id();
let window = &mut *self.window;
window
.next_frame
.scene
.paint_primitive(|draw_order| Underline {
draw_order,
window.next_frame.scene.insert(
&window.next_frame.z_index_stack,
Underline {
view_id: view_id.into(),
layer_id: 0,
order: 0,
bounds: bounds.scale(scale_factor),
content_mask: content_mask.scale(scale_factor),
thickness: style.thickness.scale(scale_factor),
color: style.color.unwrap_or_default(),
wavy: false,
});
},
);
}
/// Paints a monochrome (non-emoji) glyph into the scene for the next frame at the current z-index.
@@ -831,16 +837,18 @@ impl<'a> ElementContext<'a> {
let content_mask = self.content_mask().scale(scale_factor);
let view_id = self.parent_view_id();
let window = &mut *self.window;
window
.next_frame
.scene
.paint_primitive(|draw_order| MonochromeSprite {
draw_order,
window.next_frame.scene.insert(
&window.next_frame.z_index_stack,
MonochromeSprite {
view_id: view_id.into(),
layer_id: 0,
order: 0,
bounds,
content_mask,
color,
tile,
});
},
);
}
Ok(())
}
@@ -887,18 +895,20 @@ impl<'a> ElementContext<'a> {
let view_id = self.parent_view_id();
let window = &mut *self.window;
window
.next_frame
.scene
.paint_primitive(|draw_order| PolychromeSprite {
draw_order,
window.next_frame.scene.insert(
&window.next_frame.z_index_stack,
PolychromeSprite {
view_id: view_id.into(),
layer_id: 0,
order: 0,
bounds,
corner_radii: Default::default(),
content_mask,
tile,
grayscale: false,
pad: 0,
});
},
);
}
Ok(())
}
@@ -931,16 +941,18 @@ impl<'a> ElementContext<'a> {
let view_id = self.parent_view_id();
let window = &mut *self.window;
window
.next_frame
.scene
.paint_primitive(|draw_order| MonochromeSprite {
draw_order,
window.next_frame.scene.insert(
&window.next_frame.z_index_stack,
MonochromeSprite {
view_id: view_id.into(),
layer_id: 0,
order: 0,
bounds,
content_mask,
color,
tile,
});
},
);
Ok(())
}
@@ -968,18 +980,20 @@ impl<'a> ElementContext<'a> {
let view_id = self.parent_view_id();
let window = &mut *self.window;
window
.next_frame
.scene
.paint_primitive(|draw_order| PolychromeSprite {
draw_order,
window.next_frame.scene.insert(
&window.next_frame.z_index_stack,
PolychromeSprite {
view_id: view_id.into(),
layer_id: 0,
order: 0,
bounds,
content_mask,
corner_radii,
tile,
grayscale,
pad: 0,
});
},
);
Ok(())
}
@@ -991,15 +1005,17 @@ impl<'a> ElementContext<'a> {
let content_mask = self.content_mask().scale(scale_factor);
let view_id = self.parent_view_id();
let window = &mut *self.window;
window
.next_frame
.scene
.paint_primitive(|draw_order| crate::Surface {
draw_order,
window.next_frame.scene.insert(
&window.next_frame.z_index_stack,
crate::Surface {
view_id: view_id.into(),
layer_id: 0,
order: 0,
bounds,
content_mask,
image_buffer,
});
},
);
}
#[must_use]

View File

@@ -38,6 +38,7 @@ use serde_json::Value;
use std::{
any::Any,
cell::RefCell,
ffi::OsString,
fmt::Debug,
hash::Hash,
mem,
@@ -140,6 +141,14 @@ impl CachedLspAdapter {
})
}
pub fn check_if_user_installed(
&self,
delegate: &Arc<dyn LspAdapterDelegate>,
cx: &mut AsyncAppContext,
) -> Option<Task<Option<LanguageServerBinary>>> {
self.adapter.check_if_user_installed(delegate, cx)
}
pub async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,
@@ -240,6 +249,11 @@ impl CachedLspAdapter {
pub trait LspAdapterDelegate: Send + Sync {
fn show_notification(&self, message: &str, cx: &mut AppContext);
fn http_client(&self) -> Arc<dyn HttpClient>;
fn which_command(
&self,
command: OsString,
cx: &AppContext,
) -> Task<Option<(PathBuf, HashMap<String, String>)>>;
}
#[async_trait]
@@ -248,6 +262,14 @@ pub trait LspAdapter: 'static + Send + Sync {
fn short_name(&self) -> &'static str;
fn check_if_user_installed(
&self,
_: &Arc<dyn LspAdapterDelegate>,
_: &mut AsyncAppContext,
) -> Option<Task<Option<LanguageServerBinary>>> {
None
}
async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,

View File

@@ -558,34 +558,41 @@ impl LanguageRegistry {
let task = {
let container_dir = container_dir.clone();
cx.spawn(move |mut cx| async move {
login_shell_env_loaded.await;
// First we check whether the adapter can give us a user-installed binary.
// If so, we do *not* want to cache that, because each worktree might give us a different
// binary:
//
// worktree 1: user-installed at `.bin/gopls`
// worktree 2: user-installed at `~/bin/gopls`
// worktree 3: no gopls found in PATH -> fallback to Zed installation
//
// We only want to cache when we fall back to the global one,
// because we don't want to download and overwrite our global one
// for each worktree we might have open.
let entry = this
.lsp_binary_paths
.lock()
.entry(adapter.name.clone())
.or_insert_with(|| {
let adapter = adapter.clone();
let language = language.clone();
let delegate = delegate.clone();
cx.spawn(|cx| {
get_binary(
adapter,
language,
delegate,
container_dir,
lsp_binary_statuses,
cx,
)
.map_err(Arc::new)
})
.shared()
})
.clone();
let user_binary_task = check_user_installed_binary(
adapter.clone(),
language.clone(),
delegate.clone(),
&mut cx,
);
let binary = if let Some(user_binary) = user_binary_task.await {
user_binary
} else {
// If we want to install a binary globally, we need to wait for
// the login shell to be set on our process.
login_shell_env_loaded.await;
let binary = match entry.await {
Ok(binary) => binary,
Err(err) => anyhow::bail!("{err}"),
get_or_install_binary(
this,
&adapter,
language,
&delegate,
&cx,
container_dir,
lsp_binary_statuses,
)
.await?
};
if let Some(task) = adapter.will_start_server(&delegate, &mut cx) {
@@ -724,6 +731,62 @@ impl LspBinaryStatusSender {
}
}
async fn check_user_installed_binary(
adapter: Arc<CachedLspAdapter>,
language: Arc<Language>,
delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut AsyncAppContext,
) -> Option<LanguageServerBinary> {
let Some(task) = adapter.check_if_user_installed(&delegate, cx) else {
return None;
};
task.await.and_then(|binary| {
log::info!(
"found user-installed language server for {}. path: {:?}, arguments: {:?}",
language.name(),
binary.path,
binary.arguments
);
Some(binary)
})
}
async fn get_or_install_binary(
registry: Arc<LanguageRegistry>,
adapter: &Arc<CachedLspAdapter>,
language: Arc<Language>,
delegate: &Arc<dyn LspAdapterDelegate>,
cx: &AsyncAppContext,
container_dir: Arc<Path>,
lsp_binary_statuses: LspBinaryStatusSender,
) -> Result<LanguageServerBinary> {
let entry = registry
.lsp_binary_paths
.lock()
.entry(adapter.name.clone())
.or_insert_with(|| {
let adapter = adapter.clone();
let language = language.clone();
let delegate = delegate.clone();
cx.spawn(|cx| {
get_binary(
adapter,
language,
delegate,
container_dir,
lsp_binary_statuses,
cx,
)
.map_err(Arc::new)
})
.shared()
})
.clone();
entry.await.map_err(|err| anyhow!("{:?}", err))
}
async fn get_binary(
adapter: Arc<CachedLspAdapter>,
language: Arc<Language>,
@@ -757,15 +820,20 @@ async fn get_binary(
.await
{
statuses.send(language.clone(), LanguageServerBinaryStatus::Cached);
return Ok(binary);
} else {
statuses.send(
language.clone(),
LanguageServerBinaryStatus::Failed {
error: format!("{:?}", error),
},
log::info!(
"failed to fetch newest version of language server {:?}. falling back to using {:?}",
adapter.name,
binary.path.display()
);
return Ok(binary);
}
statuses.send(
language.clone(),
LanguageServerBinaryStatus::Failed {
error: format!("{:?}", error),
},
);
}
binary
@@ -779,14 +847,23 @@ async fn fetch_latest_binary(
lsp_binary_statuses_tx: LspBinaryStatusSender,
) -> Result<LanguageServerBinary> {
let container_dir: Arc<Path> = container_dir.into();
lsp_binary_statuses_tx.send(
language.clone(),
LanguageServerBinaryStatus::CheckingForUpdate,
);
log::info!(
"querying GitHub for latest version of language server {:?}",
adapter.name.0
);
let version_info = adapter.fetch_latest_server_version(delegate).await?;
lsp_binary_statuses_tx.send(language.clone(), LanguageServerBinaryStatus::Downloading);
log::info!(
"checking if Zed already installed or fetching version for language server {:?}",
adapter.name.0
);
let binary = adapter
.fetch_server_binary(version_info, container_dir.to_path_buf(), delegate)
.await?;

View File

@@ -55,6 +55,7 @@ pub enum IoKind {
pub struct LanguageServerBinary {
pub path: PathBuf,
pub arguments: Vec<OsString>,
pub env: Option<HashMap<String, String>>,
}
/// A running language server process.
@@ -189,6 +190,7 @@ impl LanguageServer {
let mut server = process::Command::new(&binary.path)
.current_dir(working_dir)
.args(binary.arguments)
.envs(binary.env.unwrap_or_default())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
@@ -1136,6 +1138,7 @@ impl LanguageServer {
document_formatting_provider: Some(OneOf::Left(true)),
document_range_formatting_provider: Some(OneOf::Left(true)),
definition_provider: Some(OneOf::Left(true)),
implementation_provider: Some(ImplementationProviderCapability::Simple(true)),
type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)),
..Default::default()
}

View File

@@ -6,7 +6,7 @@ use gpui::{
IntoElement, ListState, ParentElement, Render, Styled, View, ViewContext, WeakView,
};
use ui::prelude::*;
use workspace::item::Item;
use workspace::item::{Item, ItemHandle};
use workspace::Workspace;
use crate::{
@@ -22,6 +22,7 @@ pub struct MarkdownPreviewView {
contents: ParsedMarkdown,
selected_block: usize,
list_state: ListState,
tab_description: String,
}
impl MarkdownPreviewView {
@@ -34,8 +35,9 @@ impl MarkdownPreviewView {
if let Some(editor) = workspace.active_item_as::<Editor>(cx) {
let workspace_handle = workspace.weak_handle();
let tab_description = editor.tab_description(0, cx);
let view: View<MarkdownPreviewView> =
MarkdownPreviewView::new(editor, workspace_handle, cx);
MarkdownPreviewView::new(editor, workspace_handle, tab_description, cx);
workspace.split_item(workspace::SplitDirection::Right, Box::new(view.clone()), cx);
cx.notify();
}
@@ -45,6 +47,7 @@ impl MarkdownPreviewView {
pub fn new(
active_editor: View<Editor>,
workspace: WeakView<Workspace>,
tab_description: Option<SharedString>,
cx: &mut ViewContext<Workspace>,
) -> View<Self> {
cx.new_view(|cx: &mut ViewContext<Self>| {
@@ -119,12 +122,17 @@ impl MarkdownPreviewView {
},
);
let tab_description = tab_description
.map(|tab_description| format!("Preview {}", tab_description))
.unwrap_or("Markdown preview".to_string());
Self {
selected_block: 0,
focus_handle: cx.focus_handle(),
workspace,
contents,
list_state,
tab_description: tab_description.into(),
}
})
}
@@ -188,11 +196,13 @@ impl Item for MarkdownPreviewView {
} else {
Color::Muted
}))
.child(Label::new("Markdown preview").color(if selected {
Color::Default
} else {
Color::Muted
}))
.child(
Label::new(self.tab_description.to_string()).color(if selected {
Color::Default
} else {
Color::Muted
}),
)
.into_any()
}

View File

@@ -192,6 +192,7 @@ impl Prettier {
LanguageServerBinary {
path: node_path,
arguments: vec![prettier_server.into(), prettier_dir.as_path().into()],
env: None,
},
Path::new("/"),
None,

View File

@@ -65,6 +65,7 @@ text.workspace = true
thiserror.workspace = true
toml.workspace = true
util.workspace = true
which.workspace = true
[dev-dependencies]
client = { workspace = true, features = ["test-support"] }

View File

@@ -105,6 +105,10 @@ pub(crate) struct GetTypeDefinition {
pub position: PointUtf16,
}
pub(crate) struct GetImplementation {
pub position: PointUtf16,
}
pub(crate) struct GetReferences {
pub position: PointUtf16,
}
@@ -492,6 +496,99 @@ impl LspCommand for GetDefinition {
}
}
#[async_trait(?Send)]
impl LspCommand for GetImplementation {
type Response = Vec<LocationLink>;
type LspRequest = lsp::request::GotoImplementation;
type ProtoRequest = proto::GetImplementation;
fn to_lsp(
&self,
path: &Path,
_: &Buffer,
_: &Arc<LanguageServer>,
_: &AppContext,
) -> lsp::GotoImplementationParams {
lsp::GotoImplementationParams {
text_document_position_params: lsp::TextDocumentPositionParams {
text_document: lsp::TextDocumentIdentifier {
uri: lsp::Url::from_file_path(path).unwrap(),
},
position: point_to_lsp(self.position),
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
}
}
async fn response_from_lsp(
self,
message: Option<lsp::GotoImplementationResponse>,
project: Model<Project>,
buffer: Model<Buffer>,
server_id: LanguageServerId,
cx: AsyncAppContext,
) -> Result<Vec<LocationLink>> {
location_links_from_lsp(message, project, buffer, server_id, cx).await
}
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetImplementation {
proto::GetImplementation {
project_id,
buffer_id: buffer.remote_id().into(),
position: Some(language::proto::serialize_anchor(
&buffer.anchor_before(self.position),
)),
version: serialize_version(&buffer.version()),
}
}
async fn from_proto(
message: proto::GetImplementation,
_: Model<Project>,
buffer: Model<Buffer>,
mut cx: AsyncAppContext,
) -> Result<Self> {
let position = message
.position
.and_then(deserialize_anchor)
.ok_or_else(|| anyhow!("invalid position"))?;
buffer
.update(&mut cx, |buffer, _| {
buffer.wait_for_version(deserialize_version(&message.version))
})?
.await?;
Ok(Self {
position: buffer.update(&mut cx, |buffer, _| position.to_point_utf16(buffer))?,
})
}
fn response_to_proto(
response: Vec<LocationLink>,
project: &mut Project,
peer_id: PeerId,
_: &clock::Global,
cx: &mut AppContext,
) -> proto::GetImplementationResponse {
let links = location_links_to_proto(response, project, peer_id, cx);
proto::GetImplementationResponse { links }
}
async fn response_from_proto(
self,
message: proto::GetImplementationResponse,
project: Model<Project>,
_: Model<Buffer>,
cx: AsyncAppContext,
) -> Result<Vec<LocationLink>> {
location_links_from_proto(message.links, project, cx).await
}
fn buffer_id_from_proto(message: &proto::GetImplementation) -> Result<BufferId> {
BufferId::new(message.buffer_id)
}
}
#[async_trait(?Send)]
impl LspCommand for GetTypeDefinition {
type Response = Vec<LocationLink>;

View File

@@ -70,9 +70,14 @@ pub(super) async fn format_with_prettier(
match prettier.format(buffer, buffer_path, cx).await {
Ok(new_diff) => return Some(FormatOperation::Prettier(new_diff)),
Err(e) => {
log::error!(
"Prettier instance from {prettier_path:?} failed to format a buffer: {e:#}"
);
match prettier_path {
Some(prettier_path) => log::error!(
"Prettier instance from path {prettier_path:?} failed to format a buffer: {e:#}"
),
None => log::error!(
"Default prettier instance failed to format a buffer: {e:#}"
),
}
}
}
}
@@ -366,6 +371,7 @@ fn register_new_prettier(
}
async fn install_prettier_packages(
fs: &dyn Fs,
plugins_to_install: HashSet<&'static str>,
node: Arc<dyn NodeRuntime>,
) -> anyhow::Result<()> {
@@ -385,18 +391,32 @@ async fn install_prettier_packages(
.await
.context("fetching latest npm versions")?;
log::info!("Fetching default prettier and plugins: {packages_to_versions:?}");
let default_prettier_dir = DEFAULT_PRETTIER_DIR.as_path();
match fs.metadata(default_prettier_dir).await.with_context(|| {
format!("fetching FS metadata for default prettier dir {default_prettier_dir:?}")
})? {
Some(prettier_dir_metadata) => anyhow::ensure!(
prettier_dir_metadata.is_dir,
"default prettier dir {default_prettier_dir:?} is not a directory"
),
None => fs
.create_dir(default_prettier_dir)
.await
.with_context(|| format!("creating default prettier dir {default_prettier_dir:?}"))?,
}
log::info!("Installing default prettier and plugins: {packages_to_versions:?}");
let borrowed_packages = packages_to_versions
.iter()
.map(|(package, version)| (package.as_str(), version.as_str()))
.collect::<Vec<_>>();
node.npm_install_packages(DEFAULT_PRETTIER_DIR.as_path(), &borrowed_packages)
node.npm_install_packages(default_prettier_dir, &borrowed_packages)
.await
.context("fetching formatter packages")?;
anyhow::Ok(())
}
async fn save_prettier_server_file(fs: &dyn Fs) -> Result<(), anyhow::Error> {
async fn save_prettier_server_file(fs: &dyn Fs) -> anyhow::Result<()> {
let prettier_wrapper_path = DEFAULT_PRETTIER_DIR.join(prettier::PRETTIER_SERVER_FILE);
fs.save(
&prettier_wrapper_path,
@@ -413,6 +433,17 @@ async fn save_prettier_server_file(fs: &dyn Fs) -> Result<(), anyhow::Error> {
Ok(())
}
async fn should_write_prettier_server_file(fs: &dyn Fs) -> bool {
let prettier_wrapper_path = DEFAULT_PRETTIER_DIR.join(prettier::PRETTIER_SERVER_FILE);
if !fs.is_file(&prettier_wrapper_path).await {
return true;
}
let Ok(prettier_server_file_contents) = fs.load(&prettier_wrapper_path).await else {
return true;
};
prettier_server_file_contents != prettier::PRETTIER_SERVER_JS
}
impl Project {
pub fn update_prettier_settings(
&self,
@@ -623,6 +654,7 @@ impl Project {
_cx: &mut ModelContext<Self>,
) {
// suppress unused code warnings
let _ = should_write_prettier_server_file;
let _ = install_prettier_packages;
let _ = save_prettier_server_file;
@@ -643,7 +675,6 @@ impl Project {
let Some(node) = self.node.as_ref().cloned() else {
return;
};
log::info!("Initializing default prettier with plugins {new_plugins:?}");
let fs = Arc::clone(&self.fs);
let locate_prettier_installation = match worktree.and_then(|worktree_id| {
self.worktree_for_id(worktree_id, cx)
@@ -689,6 +720,7 @@ impl Project {
}
};
log::info!("Initializing default prettier with plugins {new_plugins:?}");
let plugins_to_install = new_plugins.clone();
let fs = Arc::clone(&self.fs);
let new_installation_task = cx
@@ -703,7 +735,7 @@ impl Project {
if prettier_path.is_some() {
new_plugins.clear();
}
let mut needs_install = false;
let mut needs_install = should_write_prettier_server_file(fs.as_ref()).await;
if let Some(previous_installation_task) = previous_installation_task {
if let Err(e) = previous_installation_task.await {
log::error!("Failed to install default prettier: {e:#}");
@@ -744,8 +776,10 @@ impl Project {
let installed_plugins = new_plugins.clone();
cx.background_executor()
.spawn(async move {
install_prettier_packages(fs.as_ref(), new_plugins, node).await?;
// Save the server file last, so the reinstall need could be determined by the absence of the file.
save_prettier_server_file(fs.as_ref()).await?;
install_prettier_packages(new_plugins, node).await
anyhow::Ok(())
})
.await
.context("prettier & plugins install")

View File

@@ -71,6 +71,8 @@ use smol::lock::Semaphore;
use std::{
cmp::{self, Ordering},
convert::TryInto,
env,
ffi::OsString,
hash::Hash,
mem,
num::NonZeroU32,
@@ -504,11 +506,6 @@ pub enum FormatTrigger {
Manual,
}
struct ProjectLspAdapterDelegate {
project: Model<Project>,
http_client: Arc<dyn HttpClient>,
}
// Currently, formatting operations are represented differently depending on
// whether they come from a language server or an external command.
enum FormatOperation {
@@ -853,10 +850,13 @@ impl Project {
root_paths: impl IntoIterator<Item = &Path>,
cx: &mut gpui::TestAppContext,
) -> Model<Project> {
use clock::FakeSystemClock;
let mut languages = LanguageRegistry::test();
languages.set_executor(cx.executor());
let clock = Arc::new(FakeSystemClock::default());
let http_client = util::http::FakeHttpClient::with_404_response();
let client = cx.update(|cx| client::Client::new(http_client.clone(), cx));
let client = cx.update(|cx| client::Client::new(clock, http_client.clone(), cx));
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
let project = cx.update(|cx| {
Project::local(
@@ -2800,7 +2800,7 @@ impl Project {
fn start_language_server(
&mut self,
worktree: &Model<Worktree>,
worktree_handle: &Model<Worktree>,
adapter: Arc<CachedLspAdapter>,
language: Arc<Language>,
cx: &mut ModelContext<Self>,
@@ -2809,7 +2809,7 @@ impl Project {
return;
}
let worktree = worktree.read(cx);
let worktree = worktree_handle.read(cx);
let worktree_id = worktree.id();
let worktree_path = worktree.abs_path();
let key = (worktree_id, adapter.name.clone());
@@ -2823,7 +2823,7 @@ impl Project {
language.clone(),
adapter.clone(),
Arc::clone(&worktree_path),
ProjectLspAdapterDelegate::new(self, cx),
ProjectLspAdapterDelegate::new(self, worktree_handle, cx),
cx,
) {
Some(pending_server) => pending_server,
@@ -4646,6 +4646,7 @@ impl Project {
cx,
)
}
pub fn type_definition<T: ToPointUtf16>(
&self,
buffer: &Model<Buffer>,
@@ -4653,10 +4654,33 @@ impl Project {
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<LocationLink>>> {
let position = position.to_point_utf16(buffer.read(cx));
self.type_definition_impl(buffer, position, cx)
}
fn implementation_impl(
&self,
buffer: &Model<Buffer>,
position: PointUtf16,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<LocationLink>>> {
self.request_lsp(
buffer.clone(),
LanguageServerToQuery::Primary,
GetImplementation { position },
cx,
)
}
pub fn implementation<T: ToPointUtf16>(
&self,
buffer: &Model<Buffer>,
position: T,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<LocationLink>>> {
let position = position.to_point_utf16(buffer.read(cx));
self.implementation_impl(buffer, position, cx)
}
fn references_impl(
&self,
buffer: &Model<Buffer>,
@@ -9271,10 +9295,17 @@ impl<P: AsRef<Path>> From<(WorktreeId, P)> for ProjectPath {
}
}
struct ProjectLspAdapterDelegate {
project: Model<Project>,
worktree: Model<Worktree>,
http_client: Arc<dyn HttpClient>,
}
impl ProjectLspAdapterDelegate {
fn new(project: &Project, cx: &ModelContext<Project>) -> Arc<Self> {
fn new(project: &Project, worktree: &Model<Worktree>, cx: &ModelContext<Project>) -> Arc<Self> {
Arc::new(Self {
project: cx.handle(),
worktree: worktree.clone(),
http_client: project.client.http_client(),
})
}
@@ -9289,6 +9320,41 @@ impl LspAdapterDelegate for ProjectLspAdapterDelegate {
fn http_client(&self) -> Arc<dyn HttpClient> {
self.http_client.clone()
}
fn which_command(
&self,
command: OsString,
cx: &AppContext,
) -> Task<Option<(PathBuf, HashMap<String, String>)>> {
let worktree_abs_path = self.worktree.read(cx).abs_path();
let command = command.to_owned();
cx.background_executor().spawn(async move {
let shell_env = load_shell_environment(&worktree_abs_path)
.await
.with_context(|| {
format!(
"failed to determine load login shell environment in {worktree_abs_path:?}"
)
})
.log_err();
if let Some(shell_env) = shell_env {
let shell_path = shell_env.get("PATH");
match which::which_in(&command, shell_path, &worktree_abs_path) {
Ok(command_path) => Some((command_path, shell_env)),
Err(error) => {
log::warn!(
"failed to determine path for command {:?} in env {shell_env:?}: {error}", command.to_string_lossy()
);
None
}
}
} else {
None
}
})
}
}
fn serialize_symbol(symbol: &Symbol) -> proto::Symbol {
@@ -9396,3 +9462,55 @@ fn include_text(server: &lsp::LanguageServer) -> bool {
})
.unwrap_or(false)
}
async fn load_shell_environment(dir: &Path) -> Result<HashMap<String, String>> {
let marker = "ZED_SHELL_START";
let shell = env::var("SHELL").context(
"SHELL environment variable is not assigned so we can't source login environment variables",
)?;
let output = smol::process::Command::new(&shell)
.args([
"-i",
"-c",
// What we're doing here is to spawn a shell and then `cd` into
// the project directory to get the env in there as if the user
// `cd`'d into it. We do that because tools like direnv, asdf, ...
// hook into `cd` and only set up the env after that.
//
// The `exit 0` is the result of hours of debugging, trying to find out
// why running this command here, without `exit 0`, would mess
// up signal process for our process so that `ctrl-c` doesn't work
// anymore.
// We still don't know why `$SHELL -l -i -c '/usr/bin/env -0'` would
// do that, but it does, and `exit 0` helps.
&format!("cd {dir:?}; echo {marker}; /usr/bin/env -0; exit 0;"),
])
.output()
.await
.context("failed to spawn login shell to source login environment variables")?;
anyhow::ensure!(
output.status.success(),
"login shell exited with error {:?}",
output.status
);
let stdout = String::from_utf8_lossy(&output.stdout);
let env_output_start = stdout.find(marker).ok_or_else(|| {
anyhow!(
"failed to parse output of `env` command in login shell: {}",
stdout
)
})?;
let mut parsed_env = HashMap::default();
let env_output = &stdout[env_output_start + marker.len()..];
for line in env_output.split_terminator('\0') {
if let Some(separator_index) = line.find('=') {
let key = line[..separator_index].to_string();
let value = line[separator_index + 1..].to_string();
parsed_env.insert(key, value);
}
}
Ok(parsed_env)
}

View File

@@ -2252,11 +2252,16 @@ impl LocalSnapshot {
fn ignore_stack_for_abs_path(&self, abs_path: &Path, is_dir: bool) -> Arc<IgnoreStack> {
let mut new_ignores = Vec::new();
for ancestor in abs_path.ancestors().skip(1) {
if let Some((ignore, _)) = self.ignores_by_parent_abs_path.get(ancestor) {
new_ignores.push((ancestor, Some(ignore.clone())));
} else {
new_ignores.push((ancestor, None));
for (index, ancestor) in abs_path.ancestors().enumerate() {
if index > 0 {
if let Some((ignore, _)) = self.ignores_by_parent_abs_path.get(ancestor) {
new_ignores.push((ancestor, Some(ignore.clone())));
} else {
new_ignores.push((ancestor, None));
}
}
if ancestor.join(&*DOT_GIT).is_dir() {
break;
}
}
@@ -3319,14 +3324,21 @@ impl BackgroundScanner {
// Populate ignores above the root.
let root_abs_path = self.state.lock().snapshot.abs_path.clone();
for ancestor in root_abs_path.ancestors().skip(1) {
if let Ok(ignore) = build_gitignore(&ancestor.join(&*GITIGNORE), self.fs.as_ref()).await
{
self.state
.lock()
.snapshot
.ignores_by_parent_abs_path
.insert(ancestor.into(), (ignore.into(), false));
for (index, ancestor) in root_abs_path.ancestors().enumerate() {
if index != 0 {
if let Ok(ignore) =
build_gitignore(&ancestor.join(&*GITIGNORE), self.fs.as_ref()).await
{
self.state
.lock()
.snapshot
.ignores_by_parent_abs_path
.insert(ancestor.into(), (ignore.into(), false));
}
}
if ancestor.join(&*DOT_GIT).is_dir() {
// Reached root of git repository.
break;
}
}

View File

@@ -5,6 +5,7 @@ use crate::{
};
use anyhow::Result;
use client::Client;
use clock::FakeSystemClock;
use fs::{repository::GitFileStatus, FakeFs, Fs, RealFs, RemoveOptions};
use git::GITIGNORE;
use gpui::{ModelContext, Task, TestAppContext};
@@ -1263,7 +1264,13 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
init_test(cx);
cx.executor().allow_parking();
let client_fake = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let client_fake = cx.update(|cx| {
Client::new(
Arc::new(FakeSystemClock::default()),
FakeHttpClient::with_404_response(),
cx,
)
});
let fs_fake = FakeFs::new(cx.background_executor.clone());
fs_fake
@@ -1304,7 +1311,13 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
});
let client_real = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let client_real = cx.update(|cx| {
Client::new(
Arc::new(FakeSystemClock::default()),
FakeHttpClient::with_404_response(),
cx,
)
});
let fs_real = Arc::new(RealFs);
let temp_root = temp_tree(json!({
@@ -2396,8 +2409,9 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
}
fn build_client(cx: &mut TestAppContext) -> Arc<Client> {
let clock = Arc::new(FakeSystemClock::default());
let http_client = FakeHttpClient::with_404_response();
cx.update(|cx| Client::new(http_client, cx))
cx.update(|cx| Client::new(clock, http_client, cx))
}
#[track_caller]

View File

@@ -15,6 +15,7 @@ futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
language.workspace = true
menu.workspace = true
ordered-float.workspace = true
picker.workspace = true
postage.workspace = true

View File

@@ -1 +0,0 @@
gpui::actions!(projects, [OpenRecent]);

View File

@@ -1,5 +1,4 @@
mod highlighted_workspace_location;
mod projects;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
@@ -14,7 +13,7 @@ use ui::{prelude::*, tooltip_container, HighlightedLabel, ListItem, ListItemSpac
use util::paths::PathExt;
use workspace::{ModalView, Workspace, WorkspaceId, WorkspaceLocation, WORKSPACE_DB};
pub use projects::OpenRecent;
gpui::actions!(projects, [OpenRecent]);
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(RecentProjects::register).detach();
@@ -94,6 +93,7 @@ impl RecentProjects {
Ok(())
}))
}
pub fn open_popover(workspace: WeakView<Workspace>, cx: &mut WindowContext<'_>) -> View<Self> {
cx.new_view(|cx| Self::new(RecentProjectsDelegate::new(workspace, false), 20., cx))
}
@@ -147,7 +147,11 @@ impl PickerDelegate for RecentProjectsDelegate {
type ListItem = ListItem;
fn placeholder_text(&self) -> Arc<str> {
"Search recent projects...".into()
Arc::from(format!(
"`{:?}` reuses the window, `{:?}` opens in new",
menu::Confirm,
menu::SecondaryConfirm,
))
}
fn match_count(&self) -> usize {
@@ -207,17 +211,26 @@ impl PickerDelegate for RecentProjectsDelegate {
Task::ready(())
}
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
if let Some((selected_match, workspace)) = self
.matches
.get(self.selected_index())
.zip(self.workspace.upgrade())
{
let (_, workspace_location) = &self.workspaces[selected_match.candidate_id];
let (candidate_workspace_id, candidate_workspace_location) =
&self.workspaces[selected_match.candidate_id];
let replace_current_window = !secondary;
workspace
.update(cx, |workspace, cx| {
workspace
.open_workspace_for_paths(workspace_location.paths().as_ref().clone(), cx)
if workspace.database_id() != *candidate_workspace_id {
workspace.open_workspace_for_paths(
replace_current_window,
candidate_workspace_location.paths().as_ref().clone(),
cx,
)
} else {
Task::ready(Ok(()))
}
})
.detach_and_log_err(cx);
cx.emit(DismissEvent);

View File

@@ -12,6 +12,14 @@ message Envelope {
uint32 id = 1;
optional uint32 responding_to = 2;
optional PeerId original_sender_id = 3;
/*
When you are adding a new message type, instead of adding it in semantic order
and bumping the message ID's of everything that follows, add it at the end of the
file and bump the max number. See this
https://github.com/zed-industries/zed/pull/7890#discussion_r1496621823
*/
oneof payload {
Hello hello = 4;
Ack ack = 5;
@@ -48,6 +56,7 @@ message Envelope {
GetDefinitionResponse get_definition_response = 33;
GetTypeDefinition get_type_definition = 34;
GetTypeDefinitionResponse get_type_definition_response = 35;
GetReferences get_references = 36;
GetReferencesResponse get_references_response = 37;
GetDocumentHighlights get_document_highlights = 38;
@@ -183,7 +192,10 @@ message Envelope {
LspExtExpandMacroResponse lsp_ext_expand_macro_response = 155;
SetRoomParticipantRole set_room_participant_role = 156;
UpdateUserChannels update_user_channels = 157;
UpdateUserChannels update_user_channels = 157;
GetImplementation get_implementation = 162;
GetImplementationResponse get_implementation_response = 163;
}
reserved 158 to 161;
@@ -503,6 +515,16 @@ message GetTypeDefinition {
message GetTypeDefinitionResponse {
repeated LocationLink links = 1;
}
message GetImplementation {
uint64 project_id = 1;
uint64 buffer_id = 2;
Anchor position = 3;
repeated VectorClockEntry version = 4;
}
message GetImplementationResponse {
repeated LocationLink links = 1;
}
message GetReferences {
uint64 project_id = 1;
@@ -1085,6 +1107,7 @@ enum ChannelRole {
Member = 1;
Guest = 2;
Banned = 3;
Talker = 4;
}
message SetChannelMemberRole {

View File

@@ -192,6 +192,8 @@ messages!(
(GetReferencesResponse, Background),
(GetTypeDefinition, Background),
(GetTypeDefinitionResponse, Background),
(GetImplementation, Background),
(GetImplementationResponse, Background),
(GetUsers, Foreground),
(Hello, Foreground),
(IncomingCall, Foreground),
@@ -312,6 +314,7 @@ request_messages!(
(GetCodeActions, GetCodeActionsResponse),
(GetCompletions, GetCompletionsResponse),
(GetDefinition, GetDefinitionResponse),
(GetImplementation, GetImplementationResponse),
(GetDocumentHighlights, GetDocumentHighlightsResponse),
(GetHover, GetHoverResponse),
(GetNotifications, GetNotificationsResponse),
@@ -388,6 +391,7 @@ entity_messages!(
GetCodeActions,
GetCompletions,
GetDefinition,
GetImplementation,
GetDocumentHighlights,
GetHover,
GetProjectSymbols,

View File

@@ -0,0 +1,2 @@
use gpui::actions;
actions!(storybook, [Quit]);

View File

@@ -0,0 +1,10 @@
use gpui::{Menu, MenuItem};
pub fn app_menus() -> Vec<Menu<'static>> {
use crate::actions::Quit;
vec![Menu {
name: "Storybook",
items: vec![MenuItem::action("Quit", Quit)],
}]
}

View File

@@ -1,3 +1,5 @@
mod actions;
mod app_menus;
mod assets;
mod stories;
mod story_selector;
@@ -9,14 +11,16 @@ use gpui::{
WindowOptions,
};
use log::LevelFilter;
use settings::{default_settings, Settings, SettingsStore};
use settings::{default_settings, KeymapFile, Settings, SettingsStore};
use simplelog::SimpleLogger;
use strum::IntoEnumIterator;
use theme::{ThemeRegistry, ThemeSettings};
use ui::prelude::*;
use crate::app_menus::app_menus;
use crate::assets::Assets;
use crate::story_selector::{ComponentStory, StorySelector};
use actions::Quit;
pub use indoc::indoc;
#[derive(Parser)]
@@ -37,6 +41,7 @@ fn main() {
SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
menu::init();
let args = Args::parse();
let story_selector = args.story.clone().unwrap_or_else(|| {
@@ -78,6 +83,9 @@ fn main() {
language::init(cx);
editor::init(cx);
init(cx);
load_storybook_keymap(cx);
cx.set_menus(app_menus());
let _window = cx.open_window(
WindowOptions {
@@ -133,3 +141,19 @@ fn load_embedded_fonts(cx: &AppContext) -> gpui::Result<()> {
cx.text_system().add_fonts(embedded_fonts)
}
fn load_storybook_keymap(cx: &mut AppContext) {
KeymapFile::load_asset("keymaps/storybook.json", cx).unwrap();
}
pub fn init(cx: &mut AppContext) {
cx.on_action(quit);
}
fn quit(_: &Quit, cx: &mut AppContext) {
cx.spawn(|cx| async move {
cx.update(|cx| cx.quit())?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}

View File

@@ -180,16 +180,21 @@ impl PickerDelegate for TasksModalDelegate {
fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<picker::Picker<Self>>) {
let current_match_index = self.selected_index();
let Some(task) = secondary
.then(|| self.spawn_oneshot(cx))
.flatten()
.or_else(|| {
self.matches.get(current_match_index).map(|current_match| {
let ix = current_match.candidate_id;
self.candidates[ix].clone()
})
let task = if secondary {
if !self.last_prompt.trim().is_empty() {
self.spawn_oneshot(cx)
} else {
None
}
} else {
self.matches.get(current_match_index).map(|current_match| {
let ix = current_match.candidate_id;
self.candidates[ix].clone()
})
else {
};
let Some(task) = task else {
return;
};

View File

@@ -14,9 +14,18 @@ pub struct SemanticVersion {
pub patch: usize,
}
impl SemanticVersion {
pub fn new(major: usize, minor: usize, patch: usize) -> Self {
Self {
major,
minor,
patch,
}
}
}
impl FromStr for SemanticVersion {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
let mut components = s.trim().split('.');
let major = components

View File

@@ -39,6 +39,7 @@ tokio = { version = "1.15", "optional" = true }
ui.workspace = true
workspace.workspace = true
zed_actions.workspace = true
schemars.workspace = true
[dev-dependencies]
editor = { workspace = true, features = ["test-support"] }

View File

@@ -15,7 +15,7 @@ fn normal_before(_: &mut Workspace, action: &NormalBefore, cx: &mut ViewContext<
let count = vim.take_count(cx).unwrap_or(1);
vim.stop_recording_immediately(action.boxed_clone());
if count <= 1 || vim.workspace_state.replaying {
vim.update_active_editor(cx, |editor, cx| {
vim.update_active_editor(cx, |_, editor, cx| {
editor.cancel(&Default::default(), cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, mut cursor, _| {

View File

@@ -119,7 +119,7 @@ pub(crate) fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace
times -= 1;
}
vim.update_active_editor(cx, |editor, cx| {
vim.update_active_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| {
for _ in 0..times {
editor.join_lines(&Default::default(), cx)
@@ -182,7 +182,7 @@ pub(crate) fn move_cursor(
times: Option<usize>,
cx: &mut WindowContext,
) {
vim.update_active_editor(cx, |editor, cx| {
vim.update_active_editor(cx, |_, editor, cx| {
let text_layout_details = editor.text_layout_details(cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, cursor, goal| {
@@ -198,7 +198,7 @@ fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspa
Vim::update(cx, |vim, cx| {
vim.start_recording(cx);
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |editor, cx| {
vim.update_active_editor(cx, |_, editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, cursor, _| (right(map, cursor, 1), SelectionGoal::None));
});
@@ -221,7 +221,7 @@ fn insert_first_non_whitespace(
Vim::update(cx, |vim, cx| {
vim.start_recording(cx);
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |editor, cx| {
vim.update_active_editor(cx, |_, editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, cursor, _| {
(
@@ -238,7 +238,7 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte
Vim::update(cx, |vim, cx| {
vim.start_recording(cx);
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |editor, cx| {
vim.update_active_editor(cx, |_, editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, cursor, _| {
(next_line_end(map, cursor, 1), SelectionGoal::None)
@@ -252,7 +252,7 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex
Vim::update(cx, |vim, cx| {
vim.start_recording(cx);
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |editor, cx| {
vim.update_active_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| {
let (map, old_selections) = editor.selections.all_display(cx);
let selection_start_rows: HashSet<u32> = old_selections
@@ -285,7 +285,7 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
Vim::update(cx, |vim, cx| {
vim.start_recording(cx);
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |editor, cx| {
vim.update_active_editor(cx, |_, editor, cx| {
let text_layout_details = editor.text_layout_details(cx);
editor.transact(cx, |editor, cx| {
let (map, old_selections) = editor.selections.all_display(cx);
@@ -330,7 +330,7 @@ fn yank_line(_: &mut Workspace, _: &YankLine, cx: &mut ViewContext<Workspace>) {
pub(crate) fn normal_replace(text: Arc<str>, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
vim.stop_recording();
vim.update_active_editor(cx, |editor, cx| {
vim.update_active_editor(cx, |_, editor, cx| {
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
let (map, display_selections) = editor.selections.all_display(cx);

View File

@@ -40,7 +40,7 @@ where
Vim::update(cx, |vim, cx| {
vim.record_current_action(cx);
let count = vim.take_count(cx).unwrap_or(1) as u32;
vim.update_active_editor(cx, |editor, cx| {
vim.update_active_editor(cx, |vim, editor, cx| {
let mut ranges = Vec::new();
let mut cursor_positions = Vec::new();
let snapshot = editor.buffer().read(cx).snapshot(cx);

View File

@@ -24,7 +24,7 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
| Motion::Backspace
| Motion::StartOfLine { .. }
);
vim.update_active_editor(cx, |editor, cx| {
vim.update_active_editor(cx, |vim, editor, cx| {
let text_layout_details = editor.text_layout_details(cx);
editor.transact(cx, |editor, cx| {
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
@@ -45,7 +45,7 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
};
});
});
copy_selections_content(editor, motion.linewise(), cx);
copy_selections_content(vim, editor, motion.linewise(), cx);
editor.insert("", cx);
});
});
@@ -59,7 +59,7 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) {
let mut objects_found = false;
vim.update_active_editor(cx, |editor, cx| {
vim.update_active_editor(cx, |vim, editor, cx| {
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
editor.set_clip_at_line_ends(false, cx);
editor.transact(cx, |editor, cx| {
@@ -69,7 +69,7 @@ pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Windo
});
});
if objects_found {
copy_selections_content(editor, false, cx);
copy_selections_content(vim, editor, false, cx);
editor.insert("", cx);
}
});

View File

@@ -6,7 +6,7 @@ use language::Point;
pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
vim.stop_recording();
vim.update_active_editor(cx, |editor, cx| {
vim.update_active_editor(cx, |vim, editor, cx| {
let text_layout_details = editor.text_layout_details(cx);
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
@@ -39,7 +39,7 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
}
});
});
copy_selections_content(editor, motion.linewise(), cx);
copy_selections_content(vim, editor, motion.linewise(), cx);
editor.insert("", cx);
// Fixup cursor position after the deletion
@@ -62,7 +62,7 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) {
vim.stop_recording();
vim.update_active_editor(cx, |editor, cx| {
vim.update_active_editor(cx, |vim, editor, cx| {
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
// Emulates behavior in vim where if we expanded backwards to include a newline
@@ -98,7 +98,7 @@ pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Windo
}
});
});
copy_selections_content(editor, false, cx);
copy_selections_content(vim, editor, false, cx);
editor.insert("", cx);
// Fixup cursor position after the deletion

View File

@@ -44,7 +44,7 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
}
fn increment(vim: &mut Vim, mut delta: i32, step: i32, cx: &mut WindowContext) {
vim.update_active_editor(cx, |editor, cx| {
vim.update_active_editor(cx, |vim, editor, cx| {
let mut edits = Vec::new();
let mut new_anchors = Vec::new();

View File

@@ -1,14 +1,15 @@
use std::{borrow::Cow, cmp};
use std::cmp;
use editor::{
display_map::ToDisplayPoint, movement, scroll::Autoscroll, ClipboardSelection, DisplayPoint,
};
use gpui::{impl_actions, ViewContext};
use gpui::{impl_actions, AppContext, ViewContext};
use language::{Bias, SelectionGoal};
use serde::Deserialize;
use settings::Settings;
use workspace::Workspace;
use crate::{state::Mode, utils::copy_selections_content, Vim};
use crate::{state::Mode, utils::copy_selections_content, UseSystemClipboard, Vim, VimSettings};
#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
@@ -25,34 +26,60 @@ pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>
workspace.register_action(paste);
}
fn system_clipboard_is_newer(vim: &Vim, cx: &mut AppContext) -> bool {
cx.read_from_clipboard().is_some_and(|item| {
if let Some(last_state) = vim.workspace_state.registers.get(".system.") {
last_state != item.text()
} else {
true
}
})
}
fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.record_current_action(cx);
vim.update_active_editor(cx, |editor, cx| {
vim.update_active_editor(cx, |vim, editor, cx| {
let text_layout_details = editor.text_layout_details(cx);
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
let Some(item) = cx.read_from_clipboard() else {
return;
};
let clipboard_text = Cow::Borrowed(item.text());
let (clipboard_text, clipboard_selections): (String, Option<_>) =
if VimSettings::get_global(cx).use_system_clipboard == UseSystemClipboard::Never
|| VimSettings::get_global(cx).use_system_clipboard
== UseSystemClipboard::OnYank
&& !system_clipboard_is_newer(vim, cx)
{
(
vim.workspace_state
.registers
.get("\"")
.cloned()
.unwrap_or_else(|| "".to_string()),
None,
)
} else {
if let Some(item) = cx.read_from_clipboard() {
let clipboard_selections = item
.metadata::<Vec<ClipboardSelection>>()
.filter(|clipboard_selections| {
clipboard_selections.len() > 1
&& vim.state().mode != Mode::VisualLine
});
(item.text().clone(), clipboard_selections)
} else {
("".into(), None)
}
};
if clipboard_text.is_empty() {
return;
}
if !action.preserve_clipboard && vim.state().mode.is_visual() {
copy_selections_content(editor, vim.state().mode == Mode::VisualLine, cx);
copy_selections_content(vim, editor, vim.state().mode == Mode::VisualLine, cx);
}
// if we are copying from multi-cursor (of visual block mode), we want
// to
let clipboard_selections =
item.metadata::<Vec<ClipboardSelection>>()
.filter(|clipboard_selections| {
clipboard_selections.len() > 1 && vim.state().mode != Mode::VisualLine
});
let (display_map, current_selections) = editor.selections.all_adjusted_display(cx);
// unlike zed, if you have a multi-cursor selection from vim block mode,
@@ -201,8 +228,11 @@ mod test {
use crate::{
state::Mode,
test::{NeovimBackedTestContext, VimTestContext},
UseSystemClipboard, VimSettings,
};
use gpui::ClipboardItem;
use indoc::indoc;
use settings::SettingsStore;
#[gpui::test]
async fn test_paste(cx: &mut gpui::TestAppContext) {
@@ -291,6 +321,103 @@ mod test {
.await;
}
#[gpui::test]
async fn test_yank_system_clipboard_never(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings::<VimSettings>(cx, |s| {
s.use_system_clipboard = Some(UseSystemClipboard::Never)
});
});
cx.set_state(
indoc! {"
The quick brown
fox jˇumps over
the lazy dog"},
Mode::Normal,
);
cx.simulate_keystrokes(["v", "i", "w", "y"]);
cx.assert_state(
indoc! {"
The quick brown
fox ˇjumps over
the lazy dog"},
Mode::Normal,
);
cx.simulate_keystroke("p");
cx.assert_state(
indoc! {"
The quick brown
fox jjumpˇsumps over
the lazy dog"},
Mode::Normal,
);
assert_eq!(cx.read_from_clipboard(), None);
}
#[gpui::test]
async fn test_yank_system_clipboard_on_yank(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings::<VimSettings>(cx, |s| {
s.use_system_clipboard = Some(UseSystemClipboard::OnYank)
});
});
// copy in visual mode
cx.set_state(
indoc! {"
The quick brown
fox jˇumps over
the lazy dog"},
Mode::Normal,
);
cx.simulate_keystrokes(["v", "i", "w", "y"]);
cx.assert_state(
indoc! {"
The quick brown
fox ˇjumps over
the lazy dog"},
Mode::Normal,
);
cx.simulate_keystroke("p");
cx.assert_state(
indoc! {"
The quick brown
fox jjumpˇsumps over
the lazy dog"},
Mode::Normal,
);
assert_eq!(
cx.read_from_clipboard().map(|item| item.text().clone()),
Some("jumps".into())
);
cx.simulate_keystrokes(["d", "d", "p"]);
cx.assert_state(
indoc! {"
The quick brown
the lazy dog
ˇfox jjumpsumps over"},
Mode::Normal,
);
assert_eq!(
cx.read_from_clipboard().map(|item| item.text().clone()),
Some("jumps".into())
);
cx.write_to_clipboard(ClipboardItem::new("test-copy".to_string()));
cx.simulate_keystroke("shift-p");
cx.assert_state(
indoc! {"
The quick brown
the lazy dog
test-copˇyfox jjumpsumps over"},
Mode::Normal,
);
}
#[gpui::test]
async fn test_paste_visual(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;

View File

@@ -52,7 +52,7 @@ fn scroll(
) {
Vim::update(cx, |vim, cx| {
let amount = by(vim.take_count(cx).map(|c| c as f32));
vim.update_active_editor(cx, |editor, cx| {
vim.update_active_editor(cx, |_, editor, cx| {
scroll_editor(editor, move_cursor, &amount, cx)
});
})

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