Compare commits

...

103 Commits

Author SHA1 Message Date
Antonio Scandurra
30239b3cc6 WIP 2024-02-26 17:53:01 +01:00
Antonio Scandurra
180421fe5a WIP 2024-02-26 15:21:43 +01:00
Antonio Scandurra
1a13995b8f Add support for group hovering, still need to wire it up 2024-02-26 13:44:56 +01:00
Antonio Scandurra
b6379a9177 Fix compile errors and render hovered quads correctly 2024-02-26 11:50:28 +01:00
Nathan Sobo
2cb041504b WIP: Start passing hover variants of quad primitives in paint_quad 2024-02-25 20:35:56 -07:00
Nathan Sobo
4d8dc79d7e Implement hover detection for scene elements
- When inserting primitives, optionally specify the hover state and whether the primitive is opaque
- After scene is completed, sort the bounds of all intersecting primitives in descending order (higher is on top)
- Walk through the intersecting bounds and consult the each intersecting primitive's metadata
    - Replace primitive with its hovered variant if present
    - Stop if the primitive was inserted with occludes_hover = true
2024-02-25 19:42:20 -07:00
Nathan Sobo
474c806331 Add BoundsTree::find_containing and try to start using it 2024-02-25 15:25:00 -07:00
Nathan Sobo
2db6ccd803 WIP: Replace primitives based on hover 2024-02-24 18:31:31 -07:00
Nathan Sobo
2d6a227258 Don't take an order in Scene::insert 2024-02-24 10:56:06 -07: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
Tung Hoang
f930969411 Allow removing workspaces from the "recent projects" modal (#7885)
<img width="492" alt="Screenshot 2024-02-19 at 10 59 01 AM"
src="https://github.com/zed-industries/zed/assets/1823955/922117f6-81c1-409d-938a-131bcec0f24c">

<img width="675" alt="Screenshot 2024-02-19 at 10 59 27 AM"
src="https://github.com/zed-industries/zed/assets/1823955/fefac68b-9a99-43bb-ac0c-724e7c622455">

Release Notes:

- Added a way to remove entries from the recent projects modal
([7426](https://github.com/zed-industries/zed/issues/7426)).
2024-02-22 01:30:02 +02:00
eyecreate
266bb62813 Update linux deps to include opensuse (#8127)
Release Notes:

- Added support for openSuse to Linux dependency installer script.
2024-02-21 15:01:33 -08:00
Conrad Irwin
6e897d9969 buf-version (#8154)
Release Notes:

- N/A
2024-02-21 15:29:11 -07:00
Ferdinand Theil
d90b052162 Update dependencies to include openssl (#8136)
Openssl is required by the `openssl-sys` crate. Trying to build zed on a
Fedora 39 workstation install. This is the error I got from the
compiler.

```
error: failed to run custom build command for `openssl-sys v0.9.93`

Caused by:
  process didn't exit successfully: `/home/dionysus/git/zed/target/debug/build/openssl-sys-9f784a7979d04ba8/build-script-main` (exit status: 101)
  --- stdout
  cargo:rerun-if-env-changed=X86_64_UNKNOWN_LINUX_GNU_OPENSSL_LIB_DIR
  X86_64_UNKNOWN_LINUX_GNU_OPENSSL_LIB_DIR unset
  cargo:rerun-if-env-changed=OPENSSL_LIB_DIR
  OPENSSL_LIB_DIR unset
  cargo:rerun-if-env-changed=X86_64_UNKNOWN_LINUX_GNU_OPENSSL_INCLUDE_DIR
  X86_64_UNKNOWN_LINUX_GNU_OPENSSL_INCLUDE_DIR unset
  cargo:rerun-if-env-changed=OPENSSL_INCLUDE_DIR
  OPENSSL_INCLUDE_DIR unset
  cargo:rerun-if-env-changed=X86_64_UNKNOWN_LINUX_GNU_OPENSSL_DIR
  X86_64_UNKNOWN_LINUX_GNU_OPENSSL_DIR unset
  cargo:rerun-if-env-changed=OPENSSL_DIR
  OPENSSL_DIR unset
  cargo:rerun-if-env-changed=OPENSSL_NO_PKG_CONFIG
  cargo:rerun-if-env-changed=PKG_CONFIG_x86_64-unknown-linux-gnu
  cargo:rerun-if-env-changed=PKG_CONFIG_x86_64_unknown_linux_gnu
  cargo:rerun-if-env-changed=HOST_PKG_CONFIG
  cargo:rerun-if-env-changed=PKG_CONFIG
  cargo:rerun-if-env-changed=OPENSSL_STATIC
  cargo:rerun-if-env-changed=OPENSSL_DYNAMIC
  cargo:rerun-if-env-changed=PKG_CONFIG_ALL_STATIC
  cargo:rerun-if-env-changed=PKG_CONFIG_ALL_DYNAMIC
  cargo:rerun-if-env-changed=PKG_CONFIG_PATH_x86_64-unknown-linux-gnu
  cargo:rerun-if-env-changed=PKG_CONFIG_PATH_x86_64_unknown_linux_gnu
  cargo:rerun-if-env-changed=HOST_PKG_CONFIG_PATH
  cargo:rerun-if-env-changed=PKG_CONFIG_PATH
  cargo:rerun-if-env-changed=PKG_CONFIG_LIBDIR_x86_64-unknown-linux-gnu
  cargo:rerun-if-env-changed=PKG_CONFIG_LIBDIR_x86_64_unknown_linux_gnu
  cargo:rerun-if-env-changed=HOST_PKG_CONFIG_LIBDIR
  cargo:rerun-if-env-changed=PKG_CONFIG_LIBDIR
  cargo:rerun-if-env-changed=PKG_CONFIG_SYSROOT_DIR_x86_64-unknown-linux-gnu
  cargo:rerun-if-env-changed=PKG_CONFIG_SYSROOT_DIR_x86_64_unknown_linux_gnu
  cargo:rerun-if-env-changed=HOST_PKG_CONFIG_SYSROOT_DIR
  cargo:rerun-if-env-changed=PKG_CONFIG_SYSROOT_DIR
  run pkg_config fail: `PKG_CONFIG_ALLOW_SYSTEM_CFLAGS="1" "pkg-config" "--libs" "--cflags" "openssl"` did not exit successfully: exit status: 1
  error: could not find system library 'openssl' required by the 'openssl-sys' crate

  --- stderr
  Package openssl was not found in the pkg-config search path.
  Perhaps you should add the directory containing `openssl.pc'
  to the PKG_CONFIG_PATH environment variable
  Package 'openssl', required by 'virtual:world', not found


  --- stderr
  thread 'main' panicked at /home/dionysus/.cargo/registry/src/index.crates.io-6f17d22bba15001f/openssl-sys-0.9.93/build/find_normal.rs:190:5:


  Could not find directory of OpenSSL installation, and this `-sys` crate cannot
  proceed without this knowledge. If OpenSSL is installed and this crate had
  trouble finding it,  you can set the `OPENSSL_DIR` environment variable for the
  compilation process.

  Make sure you also have the development packages of openssl installed.
  For example, `libssl-dev` on Ubuntu or `openssl-devel` on Fedora.

  If you're in a situation where you think the directory *should* be found
  automatically, please open a bug at https://github.com/sfackler/rust-openssl
  and include information about your system as well as this message.

  $HOST = x86_64-unknown-linux-gnu
  $TARGET = x86_64-unknown-linux-gnu
  openssl-sys = 0.9.93


  note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
warning: build failed, waiting for other jobs to finish...```

I didn't explicitly test this patch on arch or debian.


Release Notes:

- Added/Fixed/Improved ... ([#8135](https://github.com/zed-industries/zed/issues/8135)).
2024-02-21 13:57:39 -08:00
Bennet Bo Fenner
49a53e7654 titlebar: show placeholder when no project is selected (#8021)
When no project is selected, the recent project dropdown is displaying
an empty string, making the button basically impossible to click. This
PR adds a placeholder value for that case.

Here is what it looks like now:

![image](https://github.com/zed-industries/zed/assets/53836821/c831a1eb-722e-4189-8f8b-8b3039daf8f8)



Release Notes:

- Added placeholder to titlebar when no project is selected
2024-02-21 14:08:20 -07:00
Marshall Bowers
3220986fc9 Format docs with Prettier (#8134)
This PR formats the docs with Prettier.

Release Notes:

- N/A
2024-02-21 13:21:22 -05:00
Dzmitry Malyshau
9fcda5a5ac blade: quad render fast path (#8110)
Ported from #7231

Release Notes:
- N/A
2024-02-21 10:04:24 -08:00
Joseph T. Lyons
5e43290aa1 v0.125.x dev 2024-02-21 12:22:41 -05:00
Kirill Bulatov
f895d66d1c Make language server id more explicit in unhandled message logs (#8131)
Before:
```
[2024-02-21T18:55:55+02:00 INFO  language::language_registry] starting language server "eslint", path: "/Users/someonetoignore/Downloads/eslint-configs-demo", id: 2
[2024-02-21T18:55:56+02:00 INFO  lsp] 2 unhandled notification window/logMessage:
{
  "type": 3,
  "message": "ESLint server running in node v18.15.0"
}
[2024-02-21T18:55:56+02:00 INFO  lsp] 2 unhandled notification eslint/confirmESLintExecution:
{
  "scope": "local",
  "uri": "file:///Users/someonetoignore/Downloads/eslint-configs-demo/index.js",
  "libraryPath": "/Users/someonetoignore/Downloads/eslint-configs-demo/node_modules/eslint/lib/api.js"
}
```

After:
```
[2024-02-21T18:57:31+02:00 INFO  language::language_registry] starting language server "eslint", path: "/Users/someonetoignore/Downloads/eslint-configs-demo", id: 2
[2024-02-21T18:57:32+02:00 INFO  lsp] Language server with id 2 sent unhandled notification window/logMessage:
{
  "type": 3,
  "message": "ESLint server running in node v18.15.0"
}
[2024-02-21T18:57:32+02:00 INFO  project::prettier_support] Fetching default prettier and plugins: [("prettier-plugin-tailwindcss", "0.5.11"), ("prettier", "3.2.5")]
[2024-02-21T18:57:32+02:00 INFO  lsp] Language server with id 2 sent unhandled notification eslint/confirmESLintExecution:
{
  "scope": "local",
  "uri": "file:///Users/someonetoignore/Downloads/eslint-configs-demo/index.js",
  "libraryPath": "/Users/someonetoignore/Downloads/eslint-configs-demo/node_modules/eslint/lib/api.js"
}
```

We have to pass a name there too, but the problem here is that the
unhandled message callback is created very early, along with the binary,
but the server name is received from the LSP initialize response, which
is a totally separate piece of code.
I plan to refactor that code next, but so far, improve the logs at least
slightly.

Release Notes:

- N/A
2024-02-21 19:11:23 +02:00
Ivan Buryak
7bf16f263e Fix a bug when extension loading is failed after it's folder is viewed by MacOS finder (#8111)
Fixes #8096

# Bug description

I was experimenting with adding extensions and almost went crazy trying
to make my demo extension work. It appeared that I was copying files
with Finder that creates hidden `.DS_Store` files which interfered with
Zed's loading logic. It assumes that `languages/` directory contains
only directories and never files and so it crashes when meets
`.DS_Store`. This makes any extension stop working after it has been
viewed via Finder

# Change

Check if path is directory when loading extension languages (so it will
skip .DS_Store files)
2024-02-21 08:46:58 -08:00
Kyber
d3745a3931 Document new theme options (#7899)
Added documentation for
[#4970](https://github.com/zed-industries/zed/issues/4970), a feature
added in the latest update. Will need to modify `Default Settings` to
reflect the new default theme example.

Release Notes:

- N/A
2024-02-21 11:12:10 -05:00
Kirill Bulatov
0c939e5dfc Add task docs and default keybindings (#8123)
Also group task source modules together

Release Notes:

- N/A

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2024-02-21 16:43:56 +02:00
Piotr Osiewicz
b9151b9506 Runnables: remove version field from the format (#8118)
This changes the format of runnables slightly (the top-level object is
now a sequence, not a map).
The 2nd commit pulls in aliases from .zshrc and co.
Release Notes:

- N/A
2024-02-21 14:30:16 +01:00
Kirill Bulatov
2679457b02 Rename runnables into tasks (#8119)
Release Notes:

- N/A
2024-02-21 14:56:43 +02:00
Thorsten Ball
45e2c01773 Copilot: handle "ok" status message when no user is set (#8116)
In #6954 a user has trouble using copilot. We haven't gotten to the
bottom of the problem, but one problem is that apparently sometimes (I'm
going to find out when) copilot sends an `"OK"` status message without a
username. This is from the user's logs:

2024-02-20T15:28:41-03:00 [ERROR] failed to deserialize response from
language server: missing field `user`. Response from language server:
"{\"status\":\"OK\"}"

The official `copilot.vim` plugin handles this as if the user is not
authenticated (!= authorized):


1a284014d2/autoload/copilot.vim (L574-L579)

So that's what I'm doing here too.

Release Notes:

- Fixed wrong handling of Copilot sign-in status in rare cases.
2024-02-21 11:39:43 +01:00
Thorsten Ball
fd9823898f Tiny change: use consistent casing in log message (#8115)
Release Notes:

- N/A
2024-02-21 10:23:05 +01:00
Thorsten Ball
d5aba2795b Log when starting language servers (#8075)
This should help us debug more failures because we can now see what
exactly was started.

Release Notes:

- N/A

Co-authored-by: Nathan <nathan@zed.dev>
Co-authored-by: Max <max@zed.dev>
2024-02-21 10:11:41 +01:00
Joseph T. Lyons
92b2e5608b Fix crash when closing last zed window (#8102)
Fixes: #8100

Release Notes:

- N/A
2024-02-21 00:48:42 -05:00
Joseph T. Lyons
c58d72ea2b Improve automatic indentation in Gleam code files (#8098)
Release Notes:

- Improved automatic indentation in Gleam code files
([#7295](https://github.com/zed-industries/zed/issues/7295)).
2024-02-20 23:55:42 -05:00
Joseph T. Lyons
58a5a1eb8f Automatically indent the cursor when adding a newline after a { in Gleam code files (#8097)
Fixes: https://github.com/zed-industries/zed/issues/7295

Release Notes:

- Fixed a bug where adding a newline after a `{` would not automatically
indent the cursor in Gleam code files
([#7295](https://github.com/zed-industries/zed/issues/7295)).
2024-02-20 23:37:15 -05:00
gmorenz
cd640a87a9 Improve key handling on x11, sharing wayland implementation (#8094)
Makes keyboard shortcuts work on x11.

Release Notes:

- N/A
2024-02-20 16:04:52 -08:00
Kirill Bulatov
c97ecc7326 Add initial CI job for Windows target (#8088)
Clippy is disabled for now, due to many warnings in both `gpui` and
other code, see
https://github.com/zed-industries/zed/actions/runs/7980269779/job/21789529800
for more details.

Also, due to `#!/usr/bin/env bash` shebang in the `script/clippy`, it
starts in Windows CI with `shell: C:\Program Files\Git\bin\bash.EXE
-euxo pipefail {0}`

https://github.com/zed-industries/zed/actions/runs/7980269779/job/21789529800#step:4:3
It seems more appropriate to use PowerShell instead.

See `todo!("windows")` for all stubbed places currently.

Release Notes:

- N/A
2024-02-21 00:35:29 +02:00
Marshall Bowers
48f0f387f8 Update docs for building Zed (#8092)
This PR updates the docs for building Zed to fix the links in the
sidebar after the addition of the Linux-specific docs in #8083.

Release Notes:

- N/A
2024-02-20 17:34:13 -05:00
Piotr Osiewicz
2ec910f772 Runnables: Add oneshot runnables (#8061)
/cc @SomeoneToIgnore 
Fixes #7460 and partially addresses #7108 
Release Notes:

- N/A
2024-02-20 23:13:09 +01:00
N
8a73bc4c7d Vim: enable sending multiple keystrokes from custom keybinding (#7965)
Release Notes:

- Added `workspace::SendKeystrokes` to enable mapping from one key to a
sequence of others
([#7033](https://github.com/zed-industries/zed/issues/7033)).

Improves #7033. Big thank you to @ConradIrwin who did most of the heavy
lifting on this one.

This PR allows the user to send multiple keystrokes via custom
keybinding. For example, the following keybinding would go down four
lines and then right four characters.

```json
[
  {
    "context": "Editor && VimControl && !VimWaiting && !menu",
    "bindings": {
      "g z": [
        "workspace::SendKeystrokes",
        "j j j j l l l l"
      ],
    }
  }
]
```

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-02-20 15:01:45 -07:00
gmorenz
8f5d7db875 First pass at making a linux keymap (#8082)
Undoubtedly not perfect, but this should be something we can work off
of.

Note that matching keybindings with ctrl in them is currently broken on
linux (or at least x11). This keymap might just manage to be less useful
than using the macos one on linux until that is fixed... the proximate
cause of this is that the `key` field of the `Keystroke` struct looks
like `"\u{e}"` instead of `"n"` when `ctrl-n` is pressed.

Release Notes:

- N/A
2024-02-20 13:51:54 -08:00
Gabriel Dinner-David
389d26d974 Linux(Wayland): translate enter and pageup/down from keysym (#8089)
enter and pagedown/pageup weren't working now they do
Release Notes:
- N/A
2024-02-20 13:48:14 -08:00
Marshall Bowers
e580e2ff0a Update Cargo.lock (#8085)
This PR updates `Cargo.lock`, since it was missed in #8059.

Release Notes:

- N/A
2024-02-20 15:43:48 -05:00
Conrad Irwin
3d9503a454 Fix cx.windows() to return borrowed windows (#8086)
Fixes #8068

Release Notes:

- Fixed an error message when joining a project twice
([#8068](https://github.com/zed-industries/zed/issues/8068)).
2024-02-20 13:42:11 -07:00
Mikayla Maki
5c7cec9f85 Add linux to readme (#8083)
Release Notes:

- N/A
2024-02-20 12:02:51 -08:00
Dzmitry Malyshau
b028231aea linux/x11: disable Vulkan validation in Debug (#8044)
Turns out this validation requirement is confusing new users.

Release Notes:
- N/A
2024-02-20 11:14:13 -08:00
Andrew Lygin
f7d2cb1818 Project search bar layout improvements (#7963)
The PR matches project search layout with the recent changes in the
buffer project layout.

https://github.com/zed-industries/zed/assets/2101250/91b905ea-aed8-4740-9e60-67f3052885e2


Release Notes:

- Improve project search bar layout, match it with the buffer search bar ([7722](https://github.com/zed-industries/zed/issues/7722))
2024-02-20 21:07:01 +02:00
Robin Pfäffle
78dcd72790 Fix display of links in lists (markdown_preview) (#8073)
![markdown_preview](https://github.com/zed-industries/zed/assets/67913738/d8e4800f-d549-42e7-90b4-001d98aa39d2)

Release Notes:

- Fixed display of long links in lists not fully visible in markdown
preview.
2024-02-20 11:30:40 -07:00
Dzmitry Malyshau
d51a0b60be linux/x11: send XCB requests asynchronously (#8045)
With `send_and_check_request` we'd be blocking both the main loop and
the caller. `send_request` is only going to be blocking on the main loop
when processing the request.

Release Notes:
- N/A

Based on a flamegraph from `perf`/`hotspot`, we are spending 40% of time
redrawing, another 40% of time downloading stuff (i.e. rust toolchain),
and the rest on text rendering, layout and such. This is with Vulkan
Validation (see https://github.com/zed-industries/zed/pull/8044).

I'm also wondering if it would be better with #7758, but regardless we
should have no problem rendering at 60-120 fps and processing user
input. More follow-ups are expected here.
2024-02-20 10:20:49 -08:00
Marshall Bowers
8178d347b6 Change default Markdown tab size (#8080)
Following up to #8079, this PR changes the default Markdown tab size to
2 spaces.

This should produce less surprising formatting for lists when using
Prettier.

Release Notes:

- Changed default Markdown tab size to 2 spaces.
2024-02-20 13:18:40 -05:00
Marshall Bowers
33ecb424af Adjust tab size for Markdown (#8079)
This PR sets the `tab_size` for Markdown to 2 spaces.

This should prevent Prettier from adding a bunch of leading whitespace
when formatting Markdown lists.

Release Notes:

- N/A
2024-02-20 13:15:14 -05:00
Darren Schroeder
91b97387b6 bump tree-sitter-nu to latest (#8059)
This PR bumps the tree-sitter-nu commit to the latest supported by the
nushell team. It also includes the latest highlights.scm

Release Notes:

Bumped `nu` tree sitter dependency and highlights.scm
2024-02-20 12:05:09 -05:00
Julia
0a40a21c74 Timeout while waiting for server to shutdown and kill it 2024-02-20 11:47:13 -05:00
Conrad Irwin
b14d576349 Follower simplification (#8026)
Release Notes:

- Improved reliability of following
2024-02-20 09:41:37 -07:00
Philipp Schaffrath
db0eaca2e5 Rename scrollbar_thumb to be consistent with other style properties (#8004)
This small inconsistency was mentioned on the discord. This fixes it.

Release Notes:

- Themes: Renamed `scrollbar_thumb.background` to
`scrollbar.thumb.background` to be consistent with other style
properties.

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-02-20 11:26:09 -05:00
Thorsten Ball
80db468720 go: better logging if go install gopls fails (#8060)
Release Notes:

- Improved logging if installing `gopls` fails
2024-02-20 15:56:52 +01:00
Kirill Bulatov
0d2ad67b27 Add settings to configure terminal scroll limit (#8063)
Fixes https://github.com/zed-industries/zed/issues/7550
Also set maximum allowed to runnables' terminals.


Release Notes:

- Added settings to configure terminal scroll limit
([7550](https://github.com/zed-industries/zed/issues/7550))
2024-02-20 16:14:59 +02:00
Kirill Bulatov
7065d6c46d Use proper template for initial runnables config contents (#8064)
Release Notes:

- N/A
2024-02-20 16:14:50 +02:00
Hourann
6c714c13b3 Fix markdown preview heading overflows no wrap (#8052)
![Kapture 2024-02-20 at 18 27
15](https://github.com/zed-industries/zed/assets/8416130/87d4dcea-e2f0-44ba-88a4-06829dbb0e89)

Release Notes:

- Improved markdown preview wrapping ([#8047](https://github.com/zed-industries/zed/issues/8047)).
2024-02-20 15:18:42 +02:00
Kirill Bulatov
c54d6aff6c Properly ignore missing/empty runnables config 2024-02-20 15:02:35 +02:00
Kirill Bulatov
48a6fb9e84 Fix runnables-related hickups (#8058)
* never error on absent/empty runnables file
* always activate terminal tab on runnable (re)schedule

Release Notes:

- N/A
2024-02-20 14:54:19 +02:00
Ali Servet Donmez
e9f400a8bd rust-analyzer check command is check and not checkOnSave (#8054)
Reference: https://rust-analyzer.github.io/manual.html#configuration

Release Notes:

- N/A
2024-02-20 14:06:07 +02:00
Thorsten Ball
fc101c1fb3 Log when failed to deserialize response from language server (#8046)
This should probably help us debug when language servers don't start up
properly.

Release Notes:

- N/A
2024-02-20 10:16:00 +01:00
bbb651
4616d66e1d Download right language server binary for OS (#8040)
Release Notes:

- Download right language server binary for OS
2024-02-20 09:53:03 +02:00
Bennet Bo Fenner
3ef8a9910d chat: auto detect links (#8028)
@ConradIrwin here's our current implementation for auto detecting links
in the chat.
We also fixed an edge case where the close reply to preview button was
cut off (rendered off screen).

Release Notes:

- Added auto detection for links in the chat panel.

---------

Co-authored-by: Remco Smits <62463826+RemcoSmitsDev@users.noreply.github.com>
2024-02-19 21:49:47 -07:00
Ben Hamment
1e44bac418 Add Ruby method visibility in outline view (#7954)
Release Notes:

- Improved ([#7849
](https://github.com/zed-industries/zed/issues/7849)).

<img width="897" alt="image"
src="https://github.com/zed-industries/zed/assets/7274458/a2b0db84-1971-45c0-a5a2-68de651e342b">
2024-02-19 19:26:04 -07:00
Remco Smits
aad7761038 Add an indicator to the channel chat to see all the messages that you missed (#7781)
This pull requests add the following features:
- Show indicator before first unseen message
- Scroll to last unseen message

<img width="241" alt="Screenshot 2024-02-14 at 18 10 35"
src="https://github.com/zed-industries/zed/assets/62463826/ca396daf-7102-4eac-ae50-7d0b5ba9b6d5">


https://github.com/zed-industries/zed/assets/62463826/3a5c4afb-aea7-4e7b-98f6-515c027ef83b

### Questions: 
1. Should we hide the indicator after a couple of seconds? Now the
indicator will hide when you close/reopen the channel chat, because when
the last unseen channel message ID is not smaller than the last message
we will not show it.

Release Notes:
- Added unseen messages indicator for the channel chat.
2024-02-19 19:20:02 -07:00
bbb651
0422d43798 Linux: Add support for MouseButton::Navigate in GPUI (wayland and x11) (#7996)
Release Notes:

- N/A

Based on wgpu implementation (which I wrote).

---------

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2024-02-19 18:09:53 -08:00
Dzmitry Malyshau
b00b65b330 linux/x11: implement window focus (#8002)
Release Notes:
- N/A
2024-02-19 17:54:54 -08:00
Janrupf
fddb778e5f Enable server side decorations on wayland (#8037)
This PR enables server side decorations on Wayland if possible. This is
stopgap solution, so that the window can be moved, resized and dragged
on Wayland sessions at all.


![image](https://github.com/zed-industries/zed/assets/25827180/3dc9af53-76c0-4664-8746-ed6a6e5eafe7)

Since Wayland compositors can decide to force either mode (as in,
forcing server or client side decorations), this requires additional
handling in zed. Since zed doesn't provide any of that handling as of
now, as a temporary solution server side decorations are always
requested.
2024-02-19 17:53:31 -08:00
白山風露
77974a4367 Stubbing unix-dependent values on Windows (#8036)
Release Notes:

- N/A
2024-02-19 17:41:35 -08:00
白山風露
0037f0b2fd Avoid dependencies build errors on Windows (#7827)
This is a compilation of fixes for errors that appeared in dependent
crates in Windows.

- wezterm (zed-industries/wezterm#1)
- tree-sitter-svelte (Himujjal/tree-sitter-svelte#54)
- tree-sitter-uiua (shnarazk/tree-sitter-uiua#25)
- tree-sitter-haskell (I sent a PR, but upstream source is regenerated
and no longer errors.)

Release Notes:

- N/A
2024-02-19 16:44:24 -08:00
Kirill Bulatov
37f6a706cc Invalidate Linux build caches more agressively (#8031)
We run Linux CI on regular GitHub Action runners, which have ~30GB of
disk space. This is nothing for Rust builds and, due to Cargo.lock
perturbations, we tend to accumulate enough artifacts to fill the disk
entirely since `restore-keys` alowed to keep the cache for different
lockfiles.

Instead, try to invalidate the cache more aggressively (which will cost
us more frequent ~30min Linux CI runs) to see how this will work in
comparison.

Release Notes:

- N/A
2024-02-19 14:16:39 -08:00
234 changed files with 8000 additions and 3490 deletions

View File

@@ -64,6 +64,8 @@ jobs:
fi
- uses: bufbuild/buf-setup-action@v1
with:
version: v1.29.0
- uses: bufbuild/buf-breaking-action@v1
with:
input: "crates/rpc/proto/"
@@ -115,7 +117,6 @@ jobs:
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/rust-toolchain.toml') }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-${{ hashFiles('**/rust-toolchain.toml') }}-
- name: configure linux
shell: bash -euxo pipefail {0}
@@ -127,6 +128,36 @@ jobs:
- name: Build Zed
run: cargo build -p zed
# todo!(windows): Actually run the tests
windows_tests:
name: (Windows) Run Clippy and tests
runs-on: windows-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
- name: Restore from cache
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/rust-toolchain.toml') }}-${{ hashFiles('**/Cargo.lock') }}
# todo!(windows): Actually run clippy
#- name: cargo clippy
# shell: bash -euxo pipefail {0}
# run: script/clippy
- name: Build Zed
run: cargo build -p zed
bundle:
name: Bundle macOS app
runs-on:

View File

@@ -1,5 +1,9 @@
{
"languages": {
"Markdown": {
"tab_size": 2,
"formatter": "prettier"
},
"TOML": {
"formatter": "prettier",
"format_on_save": "off"

190
Cargo.lock generated
View File

@@ -631,6 +631,7 @@ dependencies = [
"async-global-executor",
"async-io 1.13.0",
"async-lock 2.8.0",
"async-process",
"crossbeam-utils",
"futures-channel",
"futures-core",
@@ -769,10 +770,12 @@ dependencies = [
"anyhow",
"client",
"db",
"editor",
"gpui",
"isahc",
"lazy_static",
"log",
"markdown_preview",
"menu",
"project",
"release_channel",
@@ -1348,7 +1351,7 @@ dependencies = [
"rustc-hash",
"shlex",
"syn 2.0.48",
"which",
"which 4.4.2",
]
[[package]]
@@ -1908,6 +1911,7 @@ dependencies = [
"async-recursion 0.3.2",
"async-tungstenite",
"chrono",
"clock",
"collections",
"db",
"feature_flags",
@@ -1945,6 +1949,8 @@ dependencies = [
name = "clock"
version = "0.1.0"
dependencies = [
"chrono",
"parking_lot 0.11.2",
"smallvec",
]
@@ -2090,6 +2096,7 @@ dependencies = [
"collections",
"db",
"editor",
"extensions_ui",
"feature_flags",
"feedback",
"futures 0.3.28",
@@ -2182,6 +2189,7 @@ dependencies = [
"language",
"menu",
"picker",
"postage",
"project",
"release_channel",
"serde",
@@ -2249,6 +2257,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-compression",
"async-std",
"async-tar",
"clock",
"collections",
@@ -3672,6 +3681,7 @@ dependencies = [
"text",
"time",
"util",
"windows-sys 0.52.0",
]
[[package]]
@@ -4115,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",
@@ -4337,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]]
@@ -5850,15 +5861,6 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "ntapi"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f"
dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "ntapi"
version = "0.4.1"
@@ -6858,14 +6860,33 @@ dependencies = [
[[package]]
name = "procinfo"
version = "0.1.0"
source = "git+https://github.com/zed-industries/wezterm?rev=5cd757e5f2eb039ed0c6bb6512223e69d5efc64d#5cd757e5f2eb039ed0c6bb6512223e69d5efc64d"
source = "git+https://github.com/zed-industries/wezterm?rev=0c13436f4fa8b126f46dd4a20106419b41666897#0c13436f4fa8b126f46dd4a20106419b41666897"
dependencies = [
"libc",
"log",
"ntapi 0.3.7",
"ntapi",
"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"
@@ -6904,7 +6925,6 @@ dependencies = [
"regex",
"release_channel",
"rpc",
"runnable",
"schemars",
"serde",
"serde_derive",
@@ -6914,6 +6934,7 @@ dependencies = [
"similar",
"smol",
"sum_tree",
"task",
"tempfile",
"terminal",
"text",
@@ -6921,6 +6942,7 @@ dependencies = [
"toml 0.8.10",
"unindent",
"util",
"which 6.0.0",
]
[[package]]
@@ -7030,7 +7052,7 @@ dependencies = [
"prost-types 0.9.0",
"regex",
"tempfile",
"which",
"which 4.4.2",
]
[[package]]
@@ -7322,6 +7344,7 @@ dependencies = [
"fuzzy",
"gpui",
"language",
"menu",
"ordered-float 2.10.0",
"picker",
"postage",
@@ -7562,6 +7585,7 @@ dependencies = [
"gpui",
"language",
"lazy_static",
"linkify",
"pulldown-cmark",
"smallvec",
"smol",
@@ -7761,48 +7785,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "runnable"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"futures 0.3.28",
"gpui",
"parking_lot 0.11.2",
"schemars",
"serde",
"serde_json",
"serde_json_lenient",
"settings",
"smol",
"util",
]
[[package]]
name = "runnables_ui"
version = "0.1.0"
dependencies = [
"anyhow",
"db",
"editor",
"fs",
"futures 0.3.28",
"fuzzy",
"gpui",
"log",
"picker",
"project",
"runnable",
"schemars",
"serde",
"serde_json",
"theme",
"ui",
"util",
"workspace",
]
[[package]]
name = "rusqlite"
version = "0.29.0"
@@ -9340,7 +9322,7 @@ dependencies = [
"cfg-if 1.0.0",
"core-foundation-sys 0.8.6",
"libc",
"ntapi 0.4.1",
"ntapi",
"once_cell",
"rayon",
"winapi 0.3.9",
@@ -9375,6 +9357,49 @@ version = "0.12.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69758bda2e78f098e4ccb393021a0963bb3442eac05f135c30f61b7370bbafae"
[[package]]
name = "task"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"futures 0.3.28",
"gpui",
"parking_lot 0.11.2",
"schemars",
"serde",
"serde_json",
"serde_json_lenient",
"settings",
"smol",
"util",
]
[[package]]
name = "tasks_ui"
version = "0.1.0"
dependencies = [
"anyhow",
"db",
"editor",
"fs",
"futures 0.3.28",
"fuzzy",
"gpui",
"log",
"menu",
"picker",
"project",
"schemars",
"serde",
"serde_json",
"task",
"theme",
"ui",
"util",
"workspace",
]
[[package]]
name = "tempfile"
version = "3.9.0"
@@ -9426,7 +9451,6 @@ dependencies = [
"ordered-float 2.10.0",
"procinfo",
"rand 0.8.5",
"runnable",
"schemars",
"serde",
"serde_derive",
@@ -9435,6 +9459,7 @@ dependencies = [
"shellexpand",
"smallvec",
"smol",
"task",
"theme",
"thiserror",
"util",
@@ -9461,7 +9486,6 @@ dependencies = [
"procinfo",
"project",
"rand 0.8.5",
"runnable",
"search",
"serde",
"serde_derive",
@@ -9470,6 +9494,7 @@ dependencies = [
"shellexpand",
"smallvec",
"smol",
"task",
"terminal",
"theme",
"thiserror",
@@ -10195,7 +10220,7 @@ dependencies = [
[[package]]
name = "tree-sitter-gitcommit"
version = "0.3.3"
source = "git+https://github.com/gbprod/tree-sitter-gitcommit#e8d9eda4e5ea0b08aa39d48dab0f6553058fbe0f"
source = "git+https://github.com/gbprod/tree-sitter-gitcommit#7c01af8d227b5344f62aade2ff00f19bd0c458ca"
dependencies = [
"cc",
"tree-sitter",
@@ -10249,7 +10274,7 @@ dependencies = [
[[package]]
name = "tree-sitter-haskell"
version = "0.14.0"
source = "git+https://github.com/tree-sitter/tree-sitter-haskell?rev=cf98de23e4285b8e6bcb57b050ef2326e2cc284b#cf98de23e4285b8e6bcb57b050ef2326e2cc284b"
source = "git+https://github.com/tree-sitter/tree-sitter-haskell?rev=8a99848fc734f9c4ea523b3f2a07df133cbbcec2#8a99848fc734f9c4ea523b3f2a07df133cbbcec2"
dependencies = [
"cc",
"tree-sitter",
@@ -10333,7 +10358,7 @@ dependencies = [
[[package]]
name = "tree-sitter-nu"
version = "0.0.1"
source = "git+https://github.com/nushell/tree-sitter-nu?rev=26bbaecda0039df4067861ab38ea8ea169f7f5aa#26bbaecda0039df4067861ab38ea8ea169f7f5aa"
source = "git+https://github.com/nushell/tree-sitter-nu?rev=7dd29f9616822e5fc259f5b4ae6c4ded9a71a132#7dd29f9616822e5fc259f5b4ae6c4ded9a71a132"
dependencies = [
"cc",
"tree-sitter",
@@ -10378,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",
@@ -10436,7 +10461,7 @@ dependencies = [
[[package]]
name = "tree-sitter-svelte"
version = "0.10.2"
source = "git+https://github.com/Himujjal/tree-sitter-svelte?rev=697bb515471871e85ff799ea57a76298a71a9cca#697bb515471871e85ff799ea57a76298a71a9cca"
source = "git+https://github.com/Himujjal/tree-sitter-svelte?rev=bd60db7d3d06f89b6ec3b287c9a6e9190b5564bd#bd60db7d3d06f89b6ec3b287c9a6e9190b5564bd"
dependencies = [
"cc",
"tree-sitter",
@@ -10462,8 +10487,8 @@ dependencies = [
[[package]]
name = "tree-sitter-uiua"
version = "0.3.3"
source = "git+https://github.com/shnarazk/tree-sitter-uiua?rev=9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2#9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2"
version = "0.10.0"
source = "git+https://github.com/shnarazk/tree-sitter-uiua?rev=21dc2db39494585bf29a3f86d5add6e9d11a22ba#21dc2db39494585bf29a3f86d5add6e9d11a22ba"
dependencies = [
"cc",
"tree-sitter",
@@ -10904,6 +10929,7 @@ dependencies = [
"project",
"regex",
"release_channel",
"schemars",
"search",
"serde",
"serde_derive",
@@ -11400,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"
@@ -11711,6 +11750,7 @@ dependencies = [
"bincode",
"call",
"client",
"clock",
"collections",
"db",
"derive_more",
@@ -11728,7 +11768,6 @@ dependencies = [
"parking_lot 0.11.2",
"postage",
"project",
"runnable",
"schemars",
"serde",
"serde_derive",
@@ -11736,6 +11775,7 @@ dependencies = [
"settings",
"smallvec",
"sqlez",
"task",
"terminal",
"theme",
"ui",
@@ -11915,7 +11955,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.124.0"
version = "0.125.0"
dependencies = [
"activity_indicator",
"ai",
@@ -11935,6 +11975,7 @@ dependencies = [
"chrono",
"cli",
"client",
"clock",
"collab_ui",
"collections",
"command_palette",
@@ -11978,6 +12019,7 @@ dependencies = [
"outline",
"parking_lot 0.11.2",
"postage",
"profiling",
"project",
"project_panel",
"project_symbols",
@@ -11989,8 +12031,6 @@ dependencies = [
"rope",
"rpc",
"rsa 0.4.0",
"runnable",
"runnables_ui",
"rust-embed",
"schemars",
"search",
@@ -12004,6 +12044,8 @@ dependencies = [
"smallvec",
"smol",
"sum_tree",
"task",
"tasks_ui",
"tempfile",
"terminal_view",
"text",

View File

@@ -63,8 +63,8 @@ members = [
"crates/rich_text",
"crates/rope",
"crates/rpc",
"crates/runnable",
"crates/runnables_ui",
"crates/task",
"crates/tasks_ui",
"crates/search",
"crates/semantic_index",
"crates/settings",
@@ -155,8 +155,8 @@ release_channel = { path = "crates/release_channel" }
rich_text = { path = "crates/rich_text" }
rope = { path = "crates/rope" }
rpc = { path = "crates/rpc" }
runnable = { path = "crates/runnable" }
runnables_ui = { path = "crates/runnables_ui" }
task = { path = "crates/task" }
tasks_ui = { path = "crates/tasks_ui" }
search = { path = "crates/search" }
semantic_index = { path = "crates/semantic_index" }
settings = { path = "crates/settings" }
@@ -199,9 +199,11 @@ indoc = "1"
# We explicitly disable a http2 support in isahc.
isahc = { version = "1.7.2", default-features = false, features = ["static-curl", "text-decoding"] }
lazy_static = "1.4.0"
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"
@@ -247,7 +249,7 @@ tree-sitter-glsl = { git = "https://github.com/theHamsta/tree-sitter-glsl", rev
tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" }
tree-sitter-gomod = { git = "https://github.com/camdencheek/tree-sitter-go-mod" }
tree-sitter-gowork = { git = "https://github.com/d1y/tree-sitter-go-work" }
tree-sitter-haskell = { git = "https://github.com/tree-sitter/tree-sitter-haskell", rev = "cf98de23e4285b8e6bcb57b050ef2326e2cc284b" }
tree-sitter-haskell = { git = "https://github.com/tree-sitter/tree-sitter-haskell", rev = "8a99848fc734f9c4ea523b3f2a07df133cbbcec2" }
tree-sitter-hcl = { git = "https://github.com/MichaHoffmann/tree-sitter-hcl", rev = "v1.1.0" }
tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "2e1348c3cf2c9323e87c2744796cf3f3868aa82a" }
tree-sitter-html = "0.19.0"
@@ -255,21 +257,21 @@ tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", re
tree-sitter-lua = "0.0.14"
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" }
tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "26bbaecda0039df4067861ab38ea8ea169f7f5aa" }
tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "7dd29f9616822e5fc259f5b4ae6c4ded9a71a132" }
tree-sitter-ocaml = { git = "https://github.com/tree-sitter/tree-sitter-ocaml", rev = "4abfdc1c7af2c6c77a370aee974627be1c285b3b" }
tree-sitter-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"
tree-sitter-rust = "0.20.3"
tree-sitter-scheme = { git = "https://github.com/6cdh/tree-sitter-scheme", rev = "af0fd1fa452cb2562dc7b5c8a8c55551c39273b9" }
tree-sitter-svelte = { git = "https://github.com/Himujjal/tree-sitter-svelte", rev = "697bb515471871e85ff799ea57a76298a71a9cca" }
tree-sitter-svelte = { git = "https://github.com/Himujjal/tree-sitter-svelte", rev = "bd60db7d3d06f89b6ec3b287c9a6e9190b5564bd" }
tree-sitter-toml = { git = "https://github.com/tree-sitter/tree-sitter-toml", rev = "342d9be207c2dba869b9967124c679b5e6fd0ebe" }
tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259" }
tree-sitter-uiua = { git = "https://github.com/shnarazk/tree-sitter-uiua", rev = "9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2" }
tree-sitter-uiua = { git = "https://github.com/shnarazk/tree-sitter-uiua", rev = "21dc2db39494585bf29a3f86d5add6e9d11a22ba" }
tree-sitter-vue = { git = "https://github.com/zed-industries/tree-sitter-vue", rev = "6608d9d60c386f19d80af7d8132322fa11199c42" }
tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "f545a41f57502e1b5ddf2a6668896c1b0620f930" }
tree-sitter-zig = { git = "https://github.com/maxxnino/tree-sitter-zig", rev = "0d08703e4c3f426ec61695d7617415fff97029bd" }
@@ -277,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

@@ -21,7 +21,8 @@ brew install zed
## Developing Zed
- [Building Zed](./docs/src/developing_zed__building_zed.md)
- [Building Zed for macOS](./docs/src/developing_zed__building_zed_macos.md)
- [Building Zed for Linux](./docs/src/developing_zed__building_zed_linux.md)
- [Running Collaboration Locally](./docs/src/developing_zed__local_collaboration.md)
## Contributing

View File

@@ -0,0 +1,561 @@
[
{
"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",
"ctrl-up": "menu::SelectFirst",
"ctrl-down": "menu::SelectLast",
"enter": "menu::Confirm",
"shift-f10": "menu::ShowContextMenu",
"ctrl-enter": "menu::SecondaryConfirm",
"escape": "menu::Cancel",
"ctrl-c": "menu::Cancel",
"ctrl-shift-w": "workspace::CloseWindow",
"shift-escape": "workspace::ToggleZoom",
"ctrl-o": "workspace::Open",
"ctrl-=": "zed::IncreaseBufferFontSize",
"ctrl-+": "zed::IncreaseBufferFontSize",
"ctrl--": "zed::DecreaseBufferFontSize",
"ctrl-0": "zed::ResetBufferFontSize",
"ctrl-,": "zed::OpenSettings",
"ctrl-q": "zed::Quit",
"ctrl-h": "zed::Hide",
"alt-ctrl-h": "zed::HideOthers",
"ctrl-m": "zed::Minimize",
"f11": "zed::ToggleFullScreen"
}
},
{
"context": "Editor",
"bindings": {
"escape": "editor::Cancel",
"backspace": "editor::Backspace",
"shift-backspace": "editor::Backspace",
"ctrl-h": "editor::Backspace",
"delete": "editor::Delete",
"ctrl-d": "editor::Delete",
"tab": "editor::Tab",
"shift-tab": "editor::TabPrev",
"ctrl-k": "editor::CutToEndOfLine",
"ctrl-t": "editor::Transpose",
"ctrl-backspace": "editor::DeleteToBeginningOfLine",
"ctrl-delete": "editor::DeleteToEndOfLine",
"alt-backspace": "editor::DeleteToPreviousWordStart",
"alt-delete": "editor::DeleteToNextWordEnd",
"alt-h": "editor::DeleteToPreviousWordStart",
"alt-d": "editor::DeleteToNextWordEnd",
"ctrl-x": "editor::Cut",
"ctrl-c": "editor::Copy",
"ctrl-v": "editor::Paste",
"ctrl-z": "editor::Undo",
"ctrl-shift-z": "editor::Redo",
"ctrl-y": "editor::Redo",
"up": "editor::MoveUp",
"ctrl-up": "editor::MoveToStartOfParagraph",
"pageup": "editor::PageUp",
"shift-pageup": "editor::MovePageUp",
"home": "editor::MoveToBeginningOfLine",
"down": "editor::MoveDown",
"ctrl-down": "editor::MoveToEndOfParagraph",
"pagedown": "editor::PageDown",
"shift-pagedown": "editor::MovePageDown",
"end": "editor::MoveToEndOfLine",
"left": "editor::MoveLeft",
"right": "editor::MoveRight",
"ctrl-p": "editor::MoveUp",
"ctrl-n": "editor::MoveDown",
"ctrl-b": "editor::MoveLeft",
"ctrl-f": "editor::MoveRight",
"ctrl-shift-l": "editor::NextScreen", // todo!(linux): What is this
"alt-left": "editor::MoveToPreviousWordStart",
"alt-b": "editor::MoveToPreviousWordStart",
"alt-right": "editor::MoveToNextWordEnd",
"alt-f": "editor::MoveToNextWordEnd",
"ctrl-e": "editor::MoveToEndOfLine",
"ctrl-home": "editor::MoveToBeginning",
"ctrl-=end": "editor::MoveToEnd",
"shift-up": "editor::SelectUp",
"ctrl-shift-p": "editor::SelectUp",
"shift-down": "editor::SelectDown",
"ctrl-shift-n": "editor::SelectDown",
"shift-left": "editor::SelectLeft",
"ctrl-shift-b": "editor::SelectLeft",
"shift-right": "editor::SelectRight",
"ctrl-shift-f": "editor::SelectRight",
"alt-shift-left": "editor::SelectToPreviousWordStart",
"alt-shift-b": "editor::SelectToPreviousWordStart",
"alt-shift-right": "editor::SelectToNextWordEnd",
"alt-shift-f": "editor::SelectToNextWordEnd",
"ctrl-shift-up": "editor::SelectToStartOfParagraph",
"ctrl-shift-down": "editor::SelectToEndOfParagraph",
"ctrl-shift-home": "editor::SelectToBeginning",
"ctrl-shift-end": "editor::SelectToEnd",
"ctrl-a": "editor::SelectAll",
"ctrl-l": "editor::SelectLine",
"ctrl-shift-i": "editor::Format",
"shift-home": [
"editor::SelectToBeginningOfLine",
{
"stop_at_soft_wraps": true
}
],
"shift-end": [
"editor::SelectToEndOfLine",
{
"stop_at_soft_wraps": true
}
],
"ctrl-shift-e": [
"editor::SelectToEndOfLine",
{
"stop_at_soft_wraps": true
}
]
}
},
{
"context": "Editor && mode == full",
"bindings": {
"enter": "editor::Newline",
"shift-enter": "editor::Newline",
"ctrl-shift-enter": "editor::NewlineAbove",
"ctrl-enter": "editor::NewlineBelow",
"alt-z": "editor::ToggleSoftWrap",
"ctrl-f": [
"buffer_search::Deploy",
{
"focus": true
}
],
"alt-\\": "copilot::Suggest",
"alt-]": "copilot::NextSuggestion",
"alt-[": "copilot::PreviousSuggestion",
"ctrl->": "assistant::QuoteSelection"
}
},
{
"context": "Editor && mode == auto_height",
"bindings": {
"ctrl-enter": "editor::Newline",
"shift-enter": "editor::Newline",
"ctrl-shift-enter": "editor::NewlineBelow"
}
},
{
"context": "AssistantPanel",
"bindings": {
"f3": "search::SelectNextMatch",
"shift-f3": "search::SelectPrevMatch"
}
},
{
"context": "ConversationEditor > Editor",
"bindings": {
"ctrl-enter": "assistant::Assist",
"ctrl-s": "workspace::Save",
"ctrl->": "assistant::QuoteSelection",
"shift-enter": "assistant::Split",
"ctrl-r": "assistant::CycleMessageRole"
}
},
{
"context": "BufferSearchBar",
"bindings": {
"escape": "buffer_search::Dismiss",
"tab": "buffer_search::FocusEditor",
"enter": "search::SelectNextMatch",
"shift-enter": "search::SelectPrevMatch",
"alt-enter": "search::SelectAllMatches",
"alt-tab": "search::CycleMode"
}
},
{
"context": "BufferSearchBar && in_replace",
"bindings": {
"enter": "search::ReplaceNext",
"ctrl-enter": "search::ReplaceAll"
}
},
{
"context": "BufferSearchBar && !in_replace > Editor",
"bindings": {
"up": "search::PreviousHistoryQuery",
"down": "search::NextHistoryQuery"
}
},
{
"context": "ProjectSearchBar",
"bindings": {
"escape": "project_search::ToggleFocus",
"alt-tab": "search::CycleMode",
"ctrl-shift-h": "search::ToggleReplace",
"ctrl-alt-g": "search::ActivateRegexMode",
"ctrl-alt-s": "search::ActivateSemanticMode",
"ctrl-alt-x": "search::ActivateTextMode"
}
},
{
"context": "ProjectSearchBar > Editor",
"bindings": {
"up": "search::PreviousHistoryQuery",
"down": "search::NextHistoryQuery"
}
},
{
"context": "ProjectSearchBar && in_replace",
"bindings": {
"enter": "search::ReplaceNext",
"ctrl-enter": "search::ReplaceAll"
}
},
{
"context": "ProjectSearchView",
"bindings": {
"escape": "project_search::ToggleFocus",
"alt-tab": "search::CycleMode",
"ctrl-shift-h": "search::ToggleReplace",
"ctrl-alt-g": "search::ActivateRegexMode",
"ctrl-alt-s": "search::ActivateSemanticMode",
"ctrl-alt-x": "search::ActivateTextMode"
}
},
{
"context": "Pane",
"bindings": {
"ctrl-{": "pane::ActivatePrevItem",
"ctrl-}": "pane::ActivateNextItem",
"ctrl-alt-left": "pane::ActivatePrevItem",
"ctrl-alt-right": "pane::ActivateNextItem",
"ctrl-w": "pane::CloseActiveItem",
"ctrl-alt-t": "pane::CloseInactiveItems",
"ctrl-alt-shift-w": "workspace::CloseInactiveTabsAndPanes",
"ctrl-k u": "pane::CloseCleanItems",
"ctrl-k ctrl-w": "pane::CloseAllItems",
"ctrl-f": "project_search::ToggleFocus",
"f3": "search::SelectNextMatch",
"shift-f3": "search::SelectPrevMatch",
"ctrl-shift-h": "search::ToggleReplace",
"alt-enter": "search::SelectAllMatches",
"ctrl-alt-c": "search::ToggleCaseSensitive",
"ctrl-alt-w": "search::ToggleWholeWord",
"alt-tab": "search::CycleMode",
"ctrl-alt-f": "project_search::ToggleFilters",
"ctrl-alt-g": "search::ActivateRegexMode",
"ctrl-alt-s": "search::ActivateSemanticMode",
"ctrl-alt-x": "search::ActivateTextMode"
}
},
// Bindings from VS Code
{
"context": "Editor",
"bindings": {
"ctrl-[": "editor::Outdent",
"ctrl-]": "editor::Indent",
"ctrl-alt-up": "editor::AddSelectionAbove",
"ctrl-alt-down": "editor::AddSelectionBelow",
"ctrl-d": [
"editor::SelectNext",
{
"replace_newest": false
}
],
"ctrl-shift-l": "editor::SelectAllMatches",
"ctrl-shift-d": [
"editor::SelectPrevious",
{
"replace_newest": false
}
],
"ctrl-k ctrl-d": [
"editor::SelectNext",
{
"replace_newest": true
}
],
"ctrl-k ctrl-shift-d": [
"editor::SelectPrevious",
{
"replace_newest": true
}
],
"ctrl-k ctrl-i": "editor::Hover",
"ctrl-/": [
"editor::ToggleComments",
{
"advance_downwards": false
}
],
"alt-up": "editor::SelectLargerSyntaxNode",
"alt-down": "editor::SelectSmallerSyntaxNode",
"ctrl-u": "editor::UndoSelection",
"ctrl-shift-u": "editor::RedoSelection",
"f8": "editor::GoToDiagnostic",
"shift-f8": "editor::GoToPrevDiagnostic",
"f2": "editor::Rename",
"f12": "editor::GoToDefinition",
"alt-f12": "editor::GoToDefinitionSplit",
"ctrl-f12": "editor::GoToTypeDefinition",
"ctrl-alt-f12": "editor::GoToTypeDefinitionSplit",
"alt-shift-f12": "editor::FindAllReferences",
"ctrl-m": "editor::MoveToEnclosingBracket",
"ctrl-alt-[": "editor::Fold",
"ctrl-alt-]": "editor::UnfoldLines",
"ctrl-space": "editor::ShowCompletions",
"ctrl-.": "editor::ToggleCodeActions",
"ctrl-alt-r": "editor::RevealInFinder",
"ctrl-alt-c": "editor::DisplayCursorNames"
}
},
{
"context": "Editor && mode == full",
"bindings": {
"ctrl-shift-o": "outline::Toggle",
"ctrl-g": "go_to_line::Toggle"
}
},
{
"context": "Pane",
"bindings": {
"ctrl-1": ["pane::ActivateItem", 0],
"ctrl-2": ["pane::ActivateItem", 1],
"ctrl-3": ["pane::ActivateItem", 2],
"ctrl-4": ["pane::ActivateItem", 3],
"ctrl-5": ["pane::ActivateItem", 4],
"ctrl-6": ["pane::ActivateItem", 5],
"ctrl-7": ["pane::ActivateItem", 6],
"ctrl-8": ["pane::ActivateItem", 7],
"ctrl-9": ["pane::ActivateItem", 8],
"ctrl-0": "pane::ActivateLastItem",
"ctrl--": "pane::GoBack",
"ctrl-_": "pane::GoForward",
"ctrl-shift-t": "pane::ReopenClosedItem",
"ctrl-shift-f": "project_search::ToggleFocus"
}
},
{
"context": "Workspace",
"bindings": {
"ctrl-alt-o": "projects::OpenRecent",
"ctrl-alt-b": "branches::OpenRecent",
"ctrl-~": "workspace::NewTerminal",
"ctrl-s": "workspace::Save",
"ctrl-shift-s": "workspace::SaveAs",
"ctrl-n": "workspace::NewFile",
"ctrl-shift-n": "workspace::NewWindow",
"ctrl-`": "terminal_panel::ToggleFocus",
"ctrl-1": ["workspace::ActivatePane", 0],
"ctrl-2": ["workspace::ActivatePane", 1],
"ctrl-3": ["workspace::ActivatePane", 2],
"ctrl-4": ["workspace::ActivatePane", 3],
"ctrl-5": ["workspace::ActivatePane", 4],
"ctrl-6": ["workspace::ActivatePane", 5],
"ctrl-7": ["workspace::ActivatePane", 6],
"ctrl-8": ["workspace::ActivatePane", 7],
"ctrl-9": ["workspace::ActivatePane", 8],
"ctrl-b": "workspace::ToggleLeftDock",
"ctrl-r": "workspace::ToggleRightDock",
"ctrl-j": "workspace::ToggleBottomDock",
"ctrl-alt-y": "workspace::CloseAllDocks",
"ctrl-shift-f": "pane::DeploySearch",
"ctrl-k ctrl-t": "theme_selector::Toggle",
"ctrl-k ctrl-s": "zed::OpenKeymap",
"ctrl-t": "project_symbols::Toggle",
"ctrl-p": "file_finder::Toggle",
"ctrl-shift-p": "command_palette::Toggle",
"ctrl-shift-m": "diagnostics::Deploy",
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-?": "assistant::ToggleFocus",
"ctrl-alt-s": "workspace::SaveAll",
"ctrl-k m": "language_selector::Toggle",
"escape": "workspace::Unfollow",
"ctrl-k ctrl-left": ["workspace::ActivatePaneInDirection", "Left"],
"ctrl-k ctrl-right": ["workspace::ActivatePaneInDirection", "Right"],
"ctrl-k ctrl-up": ["workspace::ActivatePaneInDirection", "Up"],
"ctrl-k ctrl-down": ["workspace::ActivatePaneInDirection", "Down"],
"ctrl-k shift-left": ["workspace::SwapPaneInDirection", "Left"],
"ctrl-k shift-right": ["workspace::SwapPaneInDirection", "Right"],
"ctrl-k shift-up": ["workspace::SwapPaneInDirection", "Up"],
"ctrl-k shift-down": ["workspace::SwapPaneInDirection", "Down"],
"alt-t": "task::Rerun",
"alt-shift-t": "task::Spawn"
}
},
// Bindings from Sublime Text
// todo!(linux) make sure these match linux bindings or remove above comment?
{
"context": "Editor",
"bindings": {
"ctrl-shift-k": "editor::DeleteLine",
"ctrl-shift-d": "editor::DuplicateLine",
"ctrl-j": "editor::JoinLines",
"ctrl-alt-up": "editor::MoveLineUp",
"ctrl-alt-down": "editor::MoveLineDown",
"ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
"ctrl-alt-h": "editor::DeleteToPreviousSubwordStart",
"ctrl-alt-delete": "editor::DeleteToNextSubwordEnd",
"ctrl-alt-d": "editor::DeleteToNextSubwordEnd",
"ctrl-alt-left": "editor::MoveToPreviousSubwordStart",
"ctrl-alt-b": "editor::MoveToPreviousSubwordStart",
"ctrl-alt-right": "editor::MoveToNextSubwordEnd",
"ctrl-alt-f": "editor::MoveToNextSubwordEnd",
"ctrl-alt-shift-left": "editor::SelectToPreviousSubwordStart",
"ctrl-alt-shift-b": "editor::SelectToPreviousSubwordStart",
"ctrl-alt-shift-right": "editor::SelectToNextSubwordEnd",
"ctrl-alt-shift-f": "editor::SelectToNextSubwordEnd"
}
},
// Bindings from Atom
// todo!(linux) make sure these match linux bindings or remove above comment?
{
"context": "Pane",
"bindings": {
"ctrl-k up": "pane::SplitUp",
"ctrl-k down": "pane::SplitDown",
"ctrl-k left": "pane::SplitLeft",
"ctrl-k right": "pane::SplitRight"
}
},
// Bindings that should be unified with bindings for more general actions
{
"context": "Editor && renaming",
"bindings": {
"enter": "editor::ConfirmRename"
}
},
{
"context": "Editor && showing_completions",
"bindings": {
"enter": "editor::ConfirmCompletion",
"tab": "editor::ConfirmCompletion"
}
},
{
"context": "Editor && showing_code_actions",
"bindings": {
"enter": "editor::ConfirmCodeAction"
}
},
{
"context": "Editor && (showing_code_actions || showing_completions)",
"bindings": {
"up": "editor::ContextMenuPrev",
"ctrl-p": "editor::ContextMenuPrev",
"down": "editor::ContextMenuNext",
"ctrl-n": "editor::ContextMenuNext",
"pageup": "editor::ContextMenuFirst",
"pagedown": "editor::ContextMenuLast"
}
},
// Custom bindings
{
"bindings": {
"ctrl-alt-shift-f": "workspace::FollowNextCollaborator",
// TODO: Move this to a dock open action
"ctrl-alt-c": "collab_panel::ToggleFocus",
"ctrl-alt-i": "zed::DebugElements",
"ctrl-:": "editor::ToggleInlayHints"
}
},
{
"context": "Editor && mode == full",
"bindings": {
"alt-enter": "editor::OpenExcerpts",
"ctrl-f8": "editor::GoToHunk",
"ctrl-shift-f8": "editor::GoToPrevHunk",
"ctrl-enter": "assistant::InlineAssist"
}
},
{
"context": "ProjectSearchBar && !in_replace",
"bindings": {
"ctrl-enter": "project_search::SearchInNew"
}
},
{
"context": "ProjectPanel",
"bindings": {
"left": "project_panel::CollapseSelectedEntry",
"right": "project_panel::ExpandSelectedEntry",
"ctrl-n": "project_panel::NewFile",
"ctrl-alt-n": "project_panel::NewDirectory",
"ctrl-x": "project_panel::Cut",
"ctrl-c": "project_panel::Copy",
"ctrl-v": "project_panel::Paste",
"ctrl-alt-c": "project_panel::CopyPath",
"ctrl-alt-shift-c": "project_panel::CopyRelativePath",
"f2": "project_panel::Rename",
"enter": "project_panel::Rename",
"backspace": "project_panel::Delete",
"ctrl-alt-r": "project_panel::RevealInFinder",
"alt-shift-f": "project_panel::NewSearchInDirectory"
}
},
{
"context": "ProjectPanel && not_editing",
"bindings": {
"space": "project_panel::Open"
}
},
{
"context": "CollabPanel && not_editing",
"bindings": {
"ctrl-backspace": "collab_panel::Remove",
"space": "menu::Confirm"
}
},
{
"context": "(CollabPanel && editing) > Editor",
"bindings": {
"space": "collab_panel::InsertSpace"
}
},
{
"context": "ChannelModal",
"bindings": {
"tab": "channel_modal::ToggleMode"
}
},
{
"context": "ChannelModal > Picker > Editor",
"bindings": {
"tab": "channel_modal::ToggleMode"
}
},
{
"context": "ChatPanel > MessageEditor",
"bindings": {
"escape": "chat_panel::CloseReplyPreview"
}
},
{
"context": "Terminal",
"bindings": {
"ctrl-alt-space": "terminal::ShowCharacterPalette",
"ctrl-shift-c": "terminal::Copy",
"ctrl-shift-v": "terminal::Paste",
"ctrl-k": "terminal::Clear",
// Some nice conveniences
"ctrl-backspace": ["terminal::SendText", "\u0015"],
"ctrl-right": ["terminal::SendText", "\u0005"],
"ctrl-left": ["terminal::SendText", "\u0001"],
// Terminal.app compatibility
"alt-left": ["terminal::SendText", "\u001bb"],
"alt-right": ["terminal::SendText", "\u001bf"],
// There are conflicting bindings for these keys in the global context.
// these bindings override them, remove at your own risk:
"up": ["terminal::SendKeystroke", "up"],
"pageup": ["terminal::SendKeystroke", "pageup"],
"down": ["terminal::SendKeystroke", "down"],
"pagedown": ["terminal::SendKeystroke", "pagedown"],
"escape": ["terminal::SendKeystroke", "escape"],
"enter": ["terminal::SendKeystroke", "enter"],
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"]
}
}
]

View File

@@ -423,7 +423,9 @@
"cmd-k shift-left": ["workspace::SwapPaneInDirection", "Left"],
"cmd-k shift-right": ["workspace::SwapPaneInDirection", "Right"],
"cmd-k shift-up": ["workspace::SwapPaneInDirection", "Up"],
"cmd-k shift-down": ["workspace::SwapPaneInDirection", "Down"]
"cmd-k shift-down": ["workspace::SwapPaneInDirection", "Down"],
"alt-t": "task::Rerun",
"alt-shift-t": "task::Spawn"
}
},
// Bindings from Sublime Text
@@ -529,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"
}
@@ -451,7 +466,10 @@
// Set the terminal's font family. If this option is not included,
// the terminal will default to matching the buffer's font family.
// "font_family": "Zed Mono",
// ---
// Sets the maximum number of lines in the terminal's scrollback buffer.
// Default: 10_000, maximum: 100_000 (all bigger values set will be treated as 100_000), 0 disables the scrolling.
// Existing terminals will not pick up this change until they are recreated.
// "max_scroll_history_lines": 10000,
},
// Difference settings for semantic_index
"semantic_index": {
@@ -491,6 +509,9 @@
"Elixir": {
"tab_size": 2
},
"Gleam": {
"tab_size": 2
},
"Go": {
"tab_size": 4,
"hard_tabs": true,
@@ -499,6 +520,7 @@
}
},
"Markdown": {
"tab_size": 2,
"soft_wrap": "preferred_line_length"
},
"JavaScript": {
@@ -540,14 +562,18 @@
"lsp": {
// Specify the LSP name as a key here.
// "rust-analyzer": {
// //These initialization options are merged into Zed's defaults
// // These initialization options are merged into Zed's defaults
// "initialization_options": {
// "checkOnSave": {
// "command": "clippy"
// "check": {
// "command": "clippy" // rust-analyzer.check.command (default: "check")
// }
// }
// }
},
// 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

@@ -1,19 +0,0 @@
// Static runnables configuration.
//
// Example:
// {
// "label": "human-readable label for UI",
// "command": "bash",
// // rest of the parameters are optional
// "args": ["-c", "for i in {1..10}; do echo \"Second $i\"; sleep 1; done"],
// // Env overrides for the command, will be appended to the terminal's environment from the settings.
// "env": {"foo": "bar"},
// // Current working directory to spawn the command into, defaults to current project root.
// "cwd": "/path/to/working/directory",
// // Whether to use a new terminal tab or reuse the existing one to spawn the process, defaults to `false`.
// "use_new_terminal": false,
// // Whether to allow multiple instances of the same runnable to be run, or rather wait for the existing ones to finish, defaults to `false`.
// "allow_concurrent_runs": false,
// },
//
{}

View File

@@ -0,0 +1,19 @@
// Static tasks configuration.
//
// Example:
[
{
"label": "Example task",
"command": "bash",
// rest of the parameters are optional
"args": ["-c", "for i in {1..5}; do echo \"Hello $i/5\"; sleep 1; done"],
// Env overrides for the command, will be appended to the terminal's environment from the settings.
"env": { "foo": "bar" },
// Current working directory to spawn the command into, defaults to current project root.
//"cwd": "/path/to/working/directory",
// Whether to use a new terminal tab or reuse the existing one to spawn the process, defaults to `false`.
"use_new_terminal": false,
// Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish, defaults to `false`.
"allow_concurrent_runs": false
}
]

View File

@@ -46,7 +46,7 @@
"panel.background": "#21242bff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#f7f7f84c",
"scrollbar.thumb.background": "#f7f7f84c",
"scrollbar.thumb.hover_background": "#252931ff",
"scrollbar.thumb.border": "#252931ff",
"scrollbar.track.background": "#00000000",

View File

@@ -46,7 +46,7 @@
"panel.background": "#221f26ff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#efecf44c",
"scrollbar.thumb.background": "#efecf44c",
"scrollbar.thumb.hover_background": "#332f38ff",
"scrollbar.thumb.border": "#332f38ff",
"scrollbar.track.background": "#00000000",
@@ -430,7 +430,7 @@
"panel.background": "#e6e3ebff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#19171c4c",
"scrollbar.thumb.background": "#19171c4c",
"scrollbar.thumb.hover_background": "#cbc8d1ff",
"scrollbar.thumb.border": "#cbc8d1ff",
"scrollbar.track.background": "#00000000",
@@ -814,7 +814,7 @@
"panel.background": "#262622ff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#fefbec4c",
"scrollbar.thumb.background": "#fefbec4c",
"scrollbar.thumb.hover_background": "#3b3933ff",
"scrollbar.thumb.border": "#3b3933ff",
"scrollbar.track.background": "#00000000",
@@ -1198,7 +1198,7 @@
"panel.background": "#eeebd7ff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#20201d4c",
"scrollbar.thumb.background": "#20201d4c",
"scrollbar.thumb.hover_background": "#d7d3beff",
"scrollbar.thumb.border": "#d7d3beff",
"scrollbar.track.background": "#00000000",
@@ -1582,7 +1582,7 @@
"panel.background": "#2c2b23ff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#f4f3ec4c",
"scrollbar.thumb.background": "#f4f3ec4c",
"scrollbar.thumb.hover_background": "#3c3b31ff",
"scrollbar.thumb.border": "#3c3b31ff",
"scrollbar.track.background": "#00000000",
@@ -1966,7 +1966,7 @@
"panel.background": "#ebeae3ff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#22221b4c",
"scrollbar.thumb.background": "#22221b4c",
"scrollbar.thumb.hover_background": "#d0cfc5ff",
"scrollbar.thumb.border": "#d0cfc5ff",
"scrollbar.track.background": "#00000000",
@@ -2350,7 +2350,7 @@
"panel.background": "#27211eff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#f0eeed4c",
"scrollbar.thumb.background": "#f0eeed4c",
"scrollbar.thumb.hover_background": "#3b3431ff",
"scrollbar.thumb.border": "#3b3431ff",
"scrollbar.track.background": "#00000000",
@@ -2734,7 +2734,7 @@
"panel.background": "#e9e6e4ff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#1b19184c",
"scrollbar.thumb.background": "#1b19184c",
"scrollbar.thumb.hover_background": "#d6d1cfff",
"scrollbar.thumb.border": "#d6d1cfff",
"scrollbar.track.background": "#00000000",
@@ -3118,7 +3118,7 @@
"panel.background": "#252025ff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#f7f3f74c",
"scrollbar.thumb.background": "#f7f3f74c",
"scrollbar.thumb.hover_background": "#393239ff",
"scrollbar.thumb.border": "#393239ff",
"scrollbar.track.background": "#00000000",
@@ -3502,7 +3502,7 @@
"panel.background": "#e0d5e0ff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#1b181b4c",
"scrollbar.thumb.background": "#1b181b4c",
"scrollbar.thumb.hover_background": "#ccbdccff",
"scrollbar.thumb.border": "#ccbdccff",
"scrollbar.track.background": "#00000000",
@@ -3886,7 +3886,7 @@
"panel.background": "#1c2529ff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#ebf8ff4c",
"scrollbar.thumb.background": "#ebf8ff4c",
"scrollbar.thumb.hover_background": "#2c3b42ff",
"scrollbar.thumb.border": "#2c3b42ff",
"scrollbar.track.background": "#00000000",
@@ -4270,7 +4270,7 @@
"panel.background": "#cdeaf9ff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#161b1d4c",
"scrollbar.thumb.background": "#161b1d4c",
"scrollbar.thumb.hover_background": "#b0d3e5ff",
"scrollbar.thumb.border": "#b0d3e5ff",
"scrollbar.track.background": "#00000000",
@@ -4654,7 +4654,7 @@
"panel.background": "#252020ff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#f4ecec4c",
"scrollbar.thumb.background": "#f4ecec4c",
"scrollbar.thumb.hover_background": "#352f2fff",
"scrollbar.thumb.border": "#352f2fff",
"scrollbar.track.background": "#00000000",
@@ -5038,7 +5038,7 @@
"panel.background": "#ebe3e3ff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#1b18184c",
"scrollbar.thumb.background": "#1b18184c",
"scrollbar.thumb.hover_background": "#cfc7c7ff",
"scrollbar.thumb.border": "#cfc7c7ff",
"scrollbar.track.background": "#00000000",
@@ -5422,7 +5422,7 @@
"panel.background": "#1f2621ff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#ecf4ee4c",
"scrollbar.thumb.background": "#ecf4ee4c",
"scrollbar.thumb.hover_background": "#2f3832ff",
"scrollbar.thumb.border": "#2f3832ff",
"scrollbar.track.background": "#00000000",
@@ -5806,7 +5806,7 @@
"panel.background": "#e3ebe6ff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#171c194c",
"scrollbar.thumb.background": "#171c194c",
"scrollbar.thumb.hover_background": "#c8d1cbff",
"scrollbar.thumb.border": "#c8d1cbff",
"scrollbar.track.background": "#00000000",
@@ -6190,7 +6190,7 @@
"panel.background": "#1f231fff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#f3faf34c",
"scrollbar.thumb.background": "#f3faf34c",
"scrollbar.thumb.hover_background": "#333b33ff",
"scrollbar.thumb.border": "#333b33ff",
"scrollbar.track.background": "#00000000",
@@ -6574,7 +6574,7 @@
"panel.background": "#daeedaff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#1315134c",
"scrollbar.thumb.background": "#1315134c",
"scrollbar.thumb.hover_background": "#bed7beff",
"scrollbar.thumb.border": "#bed7beff",
"scrollbar.track.background": "#00000000",
@@ -6958,7 +6958,7 @@
"panel.background": "#262f51ff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#f5f7ff4c",
"scrollbar.thumb.background": "#f5f7ff4c",
"scrollbar.thumb.hover_background": "#363f62ff",
"scrollbar.thumb.border": "#363f62ff",
"scrollbar.track.background": "#00000000",
@@ -7342,7 +7342,7 @@
"panel.background": "#e5e8f5ff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#2026464c",
"scrollbar.thumb.background": "#2026464c",
"scrollbar.thumb.hover_background": "#ccd0e1ff",
"scrollbar.thumb.border": "#ccd0e1ff",
"scrollbar.track.background": "#00000000",

View File

@@ -46,7 +46,7 @@
"panel.background": "#1f2127ff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#bfbdb64c",
"scrollbar.thumb.background": "#bfbdb64c",
"scrollbar.thumb.hover_background": "#2d2f34ff",
"scrollbar.thumb.border": "#2d2f34ff",
"scrollbar.track.background": "#00000000",
@@ -415,7 +415,7 @@
"panel.background": "#ececedff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#5c61664c",
"scrollbar.thumb.background": "#5c61664c",
"scrollbar.thumb.hover_background": "#dfe0e1ff",
"scrollbar.thumb.border": "#dfe0e1ff",
"scrollbar.track.background": "#00000000",
@@ -784,7 +784,7 @@
"panel.background": "#353944ff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#cccac24c",
"scrollbar.thumb.background": "#cccac24c",
"scrollbar.thumb.hover_background": "#43464fff",
"scrollbar.thumb.border": "#43464fff",
"scrollbar.track.background": "#00000000",

View File

@@ -46,7 +46,7 @@
"panel.background": "#3a3735ff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#fbf1c74c",
"scrollbar.thumb.background": "#fbf1c74c",
"scrollbar.thumb.hover_background": "#494340ff",
"scrollbar.thumb.border": "#494340ff",
"scrollbar.track.background": "#00000000",
@@ -420,7 +420,7 @@
"panel.background": "#393634ff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#fbf1c74c",
"scrollbar.thumb.background": "#fbf1c74c",
"scrollbar.thumb.hover_background": "#494340ff",
"scrollbar.thumb.border": "#494340ff",
"scrollbar.track.background": "#00000000",
@@ -794,7 +794,7 @@
"panel.background": "#3b3735ff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#fbf1c74c",
"scrollbar.thumb.background": "#fbf1c74c",
"scrollbar.thumb.hover_background": "#494340ff",
"scrollbar.thumb.border": "#494340ff",
"scrollbar.track.background": "#00000000",
@@ -1168,7 +1168,7 @@
"panel.background": "#ecddb4ff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#2828284c",
"scrollbar.thumb.background": "#2828284c",
"scrollbar.thumb.hover_background": "#ddcca7ff",
"scrollbar.thumb.border": "#ddcca7ff",
"scrollbar.track.background": "#00000000",
@@ -1542,7 +1542,7 @@
"panel.background": "#ecddb5ff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#2828284c",
"scrollbar.thumb.background": "#2828284c",
"scrollbar.thumb.hover_background": "#ddcca7ff",
"scrollbar.thumb.border": "#ddcca7ff",
"scrollbar.track.background": "#00000000",
@@ -1916,7 +1916,7 @@
"panel.background": "#ecdcb3ff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#2828284c",
"scrollbar.thumb.background": "#2828284c",
"scrollbar.thumb.hover_background": "#ddcca7ff",
"scrollbar.thumb.border": "#ddcca7ff",
"scrollbar.track.background": "#00000000",

View File

@@ -46,7 +46,7 @@
"panel.background": "#2f343eff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#c8ccd44c",
"scrollbar.thumb.background": "#c8ccd44c",
"scrollbar.thumb.hover_background": "#363c46ff",
"scrollbar.thumb.border": "#363c46ff",
"scrollbar.track.background": "#00000000",
@@ -420,7 +420,7 @@
"panel.background": "#ebebecff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#383a414c",
"scrollbar.thumb.background": "#383a414c",
"scrollbar.thumb.hover_background": "#dfdfe0ff",
"scrollbar.thumb.border": "#dfdfe0ff",
"scrollbar.track.background": "#00000000",

View File

@@ -46,7 +46,7 @@
"panel.background": "#1c1b2aff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#e0def44c",
"scrollbar.thumb.background": "#e0def44c",
"scrollbar.thumb.hover_background": "#232132ff",
"scrollbar.thumb.border": "#232132ff",
"scrollbar.track.background": "#00000000",
@@ -425,7 +425,7 @@
"panel.background": "#fef9f2ff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#5752794c",
"scrollbar.thumb.background": "#5752794c",
"scrollbar.thumb.hover_background": "#e5e0dfff",
"scrollbar.thumb.border": "#e5e0dfff",
"scrollbar.track.background": "#00000000",
@@ -804,7 +804,7 @@
"panel.background": "#28253cff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#e0def44c",
"scrollbar.thumb.background": "#e0def44c",
"scrollbar.thumb.hover_background": "#322f48ff",
"scrollbar.thumb.border": "#322f48ff",
"scrollbar.track.background": "#00000000",

View File

@@ -46,7 +46,7 @@
"panel.background": "#2b3038ff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#fdf4c14c",
"scrollbar.thumb.background": "#fdf4c14c",
"scrollbar.thumb.hover_background": "#313741ff",
"scrollbar.thumb.border": "#313741ff",
"scrollbar.track.background": "#00000000",

View File

@@ -46,7 +46,7 @@
"panel.background": "#04313bff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#fdf6e34c",
"scrollbar.thumb.background": "#fdf6e34c",
"scrollbar.thumb.hover_background": "#053541ff",
"scrollbar.thumb.border": "#053541ff",
"scrollbar.track.background": "#00000000",
@@ -415,7 +415,7 @@
"panel.background": "#f3eddaff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#002a354c",
"scrollbar.thumb.background": "#002a354c",
"scrollbar.thumb.hover_background": "#dcdacbff",
"scrollbar.thumb.border": "#dcdacbff",
"scrollbar.track.background": "#00000000",

View File

@@ -46,7 +46,7 @@
"panel.background": "#231f16ff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar_thumb.background": "#f8f5de4c",
"scrollbar.thumb.background": "#f8f5de4c",
"scrollbar.thumb.hover_background": "#29251bff",
"scrollbar.thumb.border": "#29251bff",
"scrollbar.track.background": "#00000000",

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,
@@ -348,6 +349,21 @@ impl ChannelStore {
.is_some_and(|state| state.has_new_messages())
}
pub fn last_acknowledge_message_id(&self, channel_id: ChannelId) -> Option<u64> {
self.channel_states.get(&channel_id).and_then(|state| {
if let Some(last_message_id) = state.latest_chat_message {
if state
.last_acknowledged_message_id()
.is_some_and(|id| id < last_message_id)
{
return state.last_acknowledged_message_id();
}
}
None
})
}
pub fn acknowledge_message_id(
&mut self,
channel_id: ChannelId,
@@ -1152,6 +1168,10 @@ impl ChannelState {
})
}
fn last_acknowledged_message_id(&self) -> Option<u64> {
self.observed_chat_message
}
fn acknowledge_message_id(&mut self, message_id: u64) {
let observed = self.observed_chat_message.get_or_insert(message_id);
*observed = (*observed).max(message_id);

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)
};
@@ -2088,21 +2109,16 @@ async fn update_followers(request: proto::UpdateFollowers, session: Session) ->
};
// For now, don't send view update messages back to that view's current leader.
let connection_id_to_omit = request.variant.as_ref().and_then(|variant| match variant {
let peer_id_to_omit = request.variant.as_ref().and_then(|variant| match variant {
proto::update_followers::Variant::UpdateView(payload) => payload.leader_id,
_ => None,
});
for follower_peer_id in request.follower_ids.iter().copied() {
let follower_connection_id = follower_peer_id.into();
if Some(follower_peer_id) != connection_id_to_omit
&& connection_ids.contains(&follower_connection_id)
{
session.peer.forward_send(
session.connection_id,
follower_connection_id,
request.clone(),
)?;
for connection_id in connection_ids.iter().cloned() {
if Some(connection_id.into()) != peer_id_to_omit && connection_id != session.connection_id {
session
.peer
.forward_send(session.connection_id, connection_id, request.clone())?;
}
}
Ok(())

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()),
@@ -273,7 +276,7 @@ impl TestServer {
collab_ui::init(&app_state, cx);
file_finder::init(cx);
menu::init();
settings::KeymapFile::load_asset("keymaps/default.json", cx).unwrap();
settings::KeymapFile::load_asset("keymaps/default-macos.json", cx).unwrap();
});
client

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

@@ -63,6 +63,7 @@ pub struct ChatPanel {
focus_handle: FocusHandle,
open_context_menu: Option<(u64, Subscription)>,
highlighted_message: Option<(u64, Task<()>)>,
last_acknowledged_message_id: Option<u64>,
}
#[derive(Serialize, Deserialize)]
@@ -126,6 +127,7 @@ impl ChatPanel {
focus_handle: cx.focus_handle(),
open_context_menu: None,
highlighted_message: None,
last_acknowledged_message_id: None,
};
if let Some(channel_id) = ActiveCall::global(cx)
@@ -281,6 +283,13 @@ impl ChatPanel {
fn acknowledge_last_message(&mut self, cx: &mut ViewContext<Self>) {
if self.active && self.is_scrolled_to_bottom {
if let Some((chat, _)) = &self.active_chat {
if let Some(channel_id) = self.channel_id(cx) {
self.last_acknowledged_message_id = self
.channel_store
.read(cx)
.last_acknowledge_message_id(channel_id);
}
chat.update(cx, |chat, cx| {
chat.acknowledge_last_message(cx);
});
@@ -362,9 +371,9 @@ impl ChatPanel {
.px_1()
.py_0p5()
.mb_1()
.overflow_hidden()
.child(
div()
.overflow_hidden()
.max_h_12()
.child(reply_to_message_body.element(body_element_id, cx)),
),
@@ -454,120 +463,145 @@ impl ChatPanel {
cx.theme().colors().panel_background
};
v_flex().w_full().relative().child(
div()
.bg(background)
.rounded_md()
.overflow_hidden()
.px_1()
.py_0p5()
.when(!is_continuation_from_previous, |this| {
this.mt_2().child(
h_flex()
.text_ui_sm()
.child(div().absolute().child(
Avatar::new(message.sender.avatar_uri.clone()).size(rems(1.)),
))
.child(
div()
.pl(cx.rem_size() + px(6.0))
.pr(px(8.0))
.font_weight(FontWeight::BOLD)
.child(Label::new(message.sender.github_login.clone())),
)
.child(
Label::new(format_timestamp(
OffsetDateTime::now_utc(),
message.timestamp,
self.local_timezone,
None,
v_flex()
.w_full()
.relative()
.child(
div()
.bg(background)
.rounded_md()
.overflow_hidden()
.px_1()
.py_0p5()
.when(!is_continuation_from_previous, |this| {
this.mt_2().child(
h_flex()
.text_ui_sm()
.child(div().absolute().child(
Avatar::new(message.sender.avatar_uri.clone()).size(rems(1.)),
))
.size(LabelSize::Small)
.color(Color::Muted),
),
.child(
div()
.pl(cx.rem_size() + px(6.0))
.pr(px(8.0))
.font_weight(FontWeight::BOLD)
.child(Label::new(message.sender.github_login.clone())),
)
.child(
Label::new(format_timestamp(
OffsetDateTime::now_utc(),
message.timestamp,
self.local_timezone,
None,
))
.size(LabelSize::Small)
.color(Color::Muted),
),
)
})
.when(
message.reply_to_message_id.is_some() && reply_to_message.is_none(),
|this| {
const MESSAGE_DELETED: &str = "Message has been deleted";
let body_text = StyledText::new(MESSAGE_DELETED).with_highlights(
&cx.text_style(),
vec![(
0..MESSAGE_DELETED.len(),
HighlightStyle {
font_style: Some(FontStyle::Italic),
..Default::default()
},
)],
);
this.child(
div()
.border_l_2()
.text_ui_xs()
.border_color(cx.theme().colors().border)
.px_1()
.py_0p5()
.child(body_text),
)
},
)
})
.when(
message.reply_to_message_id.is_some() && reply_to_message.is_none(),
|this| {
const MESSAGE_DELETED: &str = "Message has been deleted";
let body_text = StyledText::new(MESSAGE_DELETED).with_highlights(
&cx.text_style(),
vec![(
0..MESSAGE_DELETED.len(),
HighlightStyle {
font_style: Some(FontStyle::Italic),
..Default::default()
},
)],
);
this.child(
div()
.border_l_2()
.text_ui_xs()
.border_color(cx.theme().colors().border)
.px_1()
.py_0p5()
.child(body_text),
.when_some(reply_to_message, |el, reply_to_message| {
el.child(self.render_replied_to_message(
Some(message.id),
&reply_to_message,
cx,
))
})
.when(mentioning_you || replied_to_you, |this| this.my_0p5())
.map(|el| {
let text = self.markdown_data.entry(message.id).or_insert_with(|| {
Self::render_markdown_with_mentions(
&self.languages,
self.client.id(),
&message,
)
});
el.child(
v_flex()
.w_full()
.text_ui_sm()
.id(element_id)
.group("")
.child(text.element("body".into(), cx))
.child(
div()
.absolute()
.z_index(1)
.right_0()
.w_6()
.bg(background)
.when(!self.has_open_menu(message_id), |el| {
el.visible_on_hover("")
})
.when_some(message_id, |el, message_id| {
el.child(
popover_menu(("menu", message_id))
.trigger(IconButton::new(
("trigger", message_id),
IconName::Ellipsis,
))
.menu(move |cx| {
Some(Self::render_message_menu(
&this,
message_id,
can_delete_message,
cx,
))
}),
)
}),
),
)
},
)
.when_some(reply_to_message, |el, reply_to_message| {
el.child(self.render_replied_to_message(
Some(message.id),
&reply_to_message,
cx,
))
})
.when(mentioning_you || replied_to_you, |this| this.my_0p5())
.map(|el| {
let text = self.markdown_data.entry(message.id).or_insert_with(|| {
Self::render_markdown_with_mentions(
&self.languages,
self.client.id(),
&message,
)
});
el.child(
v_flex()
.w_full()
.text_ui_sm()
.id(element_id)
.group("")
.child(text.element("body".into(), cx))
}),
)
.when(
self.last_acknowledged_message_id
.is_some_and(|l| Some(l) == message_id),
|this| {
this.child(
h_flex()
.py_2()
.gap_1()
.items_center()
.child(div().w_full().h_0p5().bg(cx.theme().colors().border))
.child(
div()
.absolute()
.z_index(1)
.right_0()
.w_6()
.bg(background)
.when(!self.has_open_menu(message_id), |el| {
el.visible_on_hover("")
})
.when_some(message_id, |el, message_id| {
el.child(
popover_menu(("menu", message_id))
.trigger(IconButton::new(
("trigger", message_id),
IconName::Ellipsis,
))
.menu(move |cx| {
Some(Self::render_message_menu(
&this,
message_id,
can_delete_message,
cx,
))
}),
)
}),
),
.px_1()
.rounded_md()
.text_ui_xs()
.bg(cx.theme().colors().background)
.child("New messages"),
)
.child(div().w_full().h_0p5().bg(cx.theme().colors().border)),
)
}),
)
},
)
}
fn has_open_menu(&self, message_id: Option<u64>) -> bool {
@@ -682,8 +716,11 @@ impl ChatPanel {
cx.spawn(|this, mut cx| async move {
let chat = open_chat.await?;
this.update(&mut cx, |this, cx| {
let highlight_message_id = scroll_to_message_id;
let scroll_to_message_id = this.update(&mut cx, |this, cx| {
this.set_active_chat(chat.clone(), cx);
scroll_to_message_id.or_else(|| this.last_acknowledged_message_id)
})?;
if let Some(message_id) = scroll_to_message_id {
@@ -691,21 +728,22 @@ impl ChatPanel {
ChannelChat::load_history_since_message(chat.clone(), message_id, (*cx).clone())
.await
{
let task = cx.spawn({
let this = this.clone();
|mut cx| async move {
cx.background_executor().timer(Duration::from_secs(2)).await;
this.update(&mut cx, |this, cx| {
this.highlighted_message.take();
cx.notify();
})
.ok();
}
});
this.update(&mut cx, |this, cx| {
this.highlighted_message = Some((message_id, task));
if let Some(highlight_message_id) = highlight_message_id {
let task = cx.spawn({
|this, mut cx| async move {
cx.background_executor().timer(Duration::from_secs(2)).await;
this.update(&mut cx, |this, cx| {
this.highlighted_message.take();
cx.notify();
})
.ok();
}
});
this.highlighted_message = Some((highlight_message_id, task));
}
if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) {
this.message_list.scroll_to(ListOffset {
item_ix,
@@ -802,18 +840,21 @@ impl Render for ChatPanel {
el.when_some(reply_message, |el, reply_message| {
el.child(
div()
h_flex()
.when(!self.is_scrolled_to_bottom, |el| {
el.border_t_1().border_color(cx.theme().colors().border)
})
.flex()
.w_full()
.items_start()
.justify_between()
.overflow_hidden()
.items_start()
.py_1()
.px_2()
.bg(cx.theme().colors().background)
.child(self.render_replied_to_message(None, &reply_message, cx))
.child(
div().flex_shrink().overflow_hidden().child(
self.render_replied_to_message(None, &reply_message, cx),
),
)
.child(
IconButton::new("close-reply-preview", IconName::Close)
.shape(ui::IconButtonShape::Square)
@@ -1056,6 +1097,107 @@ mod tests {
);
}
#[gpui::test]
fn test_render_markdown_with_auto_detect_links() {
let language_registry = Arc::new(LanguageRegistry::test());
let message = channel::ChannelMessage {
id: ChannelMessageId::Saved(0),
body: "Here is a link https://zed.dev to zeds website".to_string(),
timestamp: OffsetDateTime::now_utc(),
sender: Arc::new(client::User {
github_login: "fgh".into(),
avatar_uri: "avatar_fgh".into(),
id: 103,
}),
nonce: 5,
mentions: Vec::new(),
reply_to_message_id: None,
};
let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message);
// Note that the "'" was replaced with due to smart punctuation.
let (body, ranges) =
marked_text_ranges("Here is a link «https://zed.dev» to zeds website", false);
assert_eq!(message.text, body);
assert_eq!(1, ranges.len());
assert_eq!(
message.highlights,
vec![(
ranges[0].clone(),
HighlightStyle {
underline: Some(gpui::UnderlineStyle {
thickness: 1.0.into(),
..Default::default()
}),
..Default::default()
}
.into()
),]
);
}
#[gpui::test]
fn test_render_markdown_with_auto_detect_links_and_additional_formatting() {
let language_registry = Arc::new(LanguageRegistry::test());
let message = channel::ChannelMessage {
id: ChannelMessageId::Saved(0),
body: "**Here is a link https://zed.dev to zeds website**".to_string(),
timestamp: OffsetDateTime::now_utc(),
sender: Arc::new(client::User {
github_login: "fgh".into(),
avatar_uri: "avatar_fgh".into(),
id: 103,
}),
nonce: 5,
mentions: Vec::new(),
reply_to_message_id: None,
};
let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message);
// Note that the "'" was replaced with due to smart punctuation.
let (body, ranges) = marked_text_ranges(
"«Here is a link »«https://zed.dev»« to zeds website»",
false,
);
assert_eq!(message.text, body);
assert_eq!(3, ranges.len());
assert_eq!(
message.highlights,
vec![
(
ranges[0].clone(),
HighlightStyle {
font_weight: Some(gpui::FontWeight::BOLD),
..Default::default()
}
.into()
),
(
ranges[1].clone(),
HighlightStyle {
font_weight: Some(gpui::FontWeight::BOLD),
underline: Some(gpui::UnderlineStyle {
thickness: 1.0.into(),
..Default::default()
}),
..Default::default()
}
.into()
),
(
ranges[2].clone(),
HighlightStyle {
font_weight: Some(gpui::FontWeight::BOLD),
..Default::default()
}
.into()
),
]
);
}
#[test]
fn test_format_locale() {
let reference = create_offset_datetime(1990, 4, 12, 16, 45, 0);

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()
}
@@ -2603,24 +2672,32 @@ fn render_tree_branch(is_last: bool, overdraw: bool, cx: &mut WindowContext) ->
let right = bounds.right();
let top = bounds.top();
cx.paint_quad(fill(
Bounds::from_corners(
point(start_x, top),
point(
start_x + thickness,
if is_last {
start_y
} else {
bounds.bottom() + if overdraw { px(1.) } else { px(0.) }
},
cx.paint_quad(
fill(
Bounds::from_corners(
point(start_x, top),
point(
start_x + thickness,
if is_last {
start_y
} else {
bounds.bottom() + if overdraw { px(1.) } else { px(0.) }
},
),
),
color,
),
color,
));
cx.paint_quad(fill(
Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
color,
));
None,
None,
);
cx.paint_quad(
fill(
Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
color,
),
None,
None,
);
})
.w(width)
.h(line_height)
@@ -2845,17 +2922,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)
@@ -421,14 +422,20 @@ impl CollabTitlebarItem {
worktree.root_name()
});
names.next().unwrap_or("")
names.next()
};
let is_project_selected = name.is_some();
let name = if let Some(name) = name {
util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH)
} else {
"Open recent project".to_string()
};
let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH);
let workspace = self.workspace.clone();
popover_menu("project_name_trigger")
.trigger(
Button::new("project_name_trigger", name)
.when(!is_project_selected, |b| b.color(Color::Muted))
.style(ButtonStyle::Subtle)
.label_size(LabelSize::Small)
.tooltip(move |cx| Tooltip::text("Recent Projects", cx)),
@@ -689,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())
@@ -714,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

@@ -28,6 +28,7 @@ ui.workspace = true
util.workspace = true
workspace.workspace = true
zed_actions.workspace = true
postage.workspace = true
[dev-dependencies]
ctor.workspace = true

View File

@@ -1,6 +1,7 @@
use std::{
cmp::{self, Reverse},
sync::Arc,
time::Duration,
};
use client::telemetry::Telemetry;
@@ -9,11 +10,12 @@ use copilot::CommandPaletteFilter;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
actions, Action, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Global,
ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
};
use picker::{Picker, PickerDelegate};
use release_channel::{parse_zed_link, ReleaseChannel};
use postage::{sink::Sink, stream::Stream};
use release_channel::parse_zed_link;
use ui::{h_flex, prelude::*, v_flex, HighlightedLabel, KeyBinding, ListItem, ListItemSpacing};
use util::ResultExt;
use workspace::{ModalView, Workspace};
@@ -119,6 +121,10 @@ pub struct CommandPaletteDelegate {
selected_ix: usize,
telemetry: Arc<Telemetry>,
previous_focus_handle: FocusHandle,
updating_matches: Option<(
Task<()>,
postage::dispatch::Receiver<(Vec<Command>, Vec<StringMatch>)>,
)>,
}
struct Command {
@@ -138,7 +144,7 @@ impl Clone for Command {
/// Hit count for each command in the palette.
/// We only account for commands triggered directly via command palette and not by e.g. keystrokes because
/// if a user already knows a keystroke for a command, they are unlikely to use a command palette to look for it.
#[derive(Default)]
#[derive(Default, Clone)]
struct HitCounts(HashMap<String, usize>);
impl Global for HitCounts {}
@@ -158,6 +164,66 @@ impl CommandPaletteDelegate {
selected_ix: 0,
telemetry,
previous_focus_handle,
updating_matches: None,
}
}
fn matches_updated(
&mut self,
query: String,
mut commands: Vec<Command>,
mut matches: Vec<StringMatch>,
cx: &mut ViewContext<Picker<Self>>,
) {
self.updating_matches.take();
let mut intercept_result =
if let Some(interceptor) = cx.try_global::<CommandPaletteInterceptor>() {
(interceptor.0)(&query, cx)
} else {
None
};
if parse_zed_link(&query).is_some() {
intercept_result = Some(CommandInterceptResult {
action: OpenZedUrl { url: query.clone() }.boxed_clone(),
string: query.clone(),
positions: vec![],
})
}
if let Some(CommandInterceptResult {
action,
string,
positions,
}) = intercept_result
{
if let Some(idx) = matches
.iter()
.position(|m| commands[m.candidate_id].action.type_id() == action.type_id())
{
matches.remove(idx);
}
commands.push(Command {
name: string.clone(),
action,
});
matches.insert(
0,
StringMatch {
candidate_id: commands.len() - 1,
string,
positions,
score: 0.0,
},
)
}
self.commands = commands;
self.matches = matches;
if self.matches.is_empty() {
self.selected_ix = 0;
} else {
self.selected_ix = cmp::min(self.selected_ix, self.matches.len() - 1);
}
}
}
@@ -186,113 +252,99 @@ impl PickerDelegate for CommandPaletteDelegate {
query: String,
cx: &mut ViewContext<Picker<Self>>,
) -> gpui::Task<()> {
let mut commands = self.all_commands.clone();
cx.spawn(move |picker, mut cx| async move {
cx.read_global::<HitCounts, _>(|hit_counts, _| {
let (mut tx, mut rx) = postage::dispatch::channel(1);
let task = cx.background_executor().spawn({
let mut commands = self.all_commands.clone();
let hit_counts = cx.global::<HitCounts>().clone();
let executor = cx.background_executor().clone();
let query = query.clone();
async move {
commands.sort_by_key(|action| {
(
Reverse(hit_counts.0.get(&action.name).cloned()),
action.name.clone(),
)
});
})
.ok();
let candidates = commands
.iter()
.enumerate()
.map(|(ix, command)| StringMatchCandidate {
id: ix,
string: command.name.to_string(),
char_bag: command.name.chars().collect(),
})
.collect::<Vec<_>>();
let mut matches = if query.is_empty() {
candidates
.into_iter()
let candidates = commands
.iter()
.enumerate()
.map(|(index, candidate)| StringMatch {
candidate_id: index,
string: candidate.string,
positions: Vec::new(),
score: 0.0,
.map(|(ix, command)| StringMatchCandidate {
id: ix,
string: command.name.to_string(),
char_bag: command.name.chars().collect(),
})
.collect()
} else {
fuzzy::match_strings(
&candidates,
&query,
true,
10000,
&Default::default(),
cx.background_executor().clone(),
)
.await
.collect::<Vec<_>>();
let matches = if query.is_empty() {
candidates
.into_iter()
.enumerate()
.map(|(index, candidate)| StringMatch {
candidate_id: index,
string: candidate.string,
positions: Vec::new(),
score: 0.0,
})
.collect()
} else {
let ret = fuzzy::match_strings(
&candidates,
&query,
true,
10000,
&Default::default(),
executor,
)
.await;
ret
};
tx.send((commands, matches)).await.log_err();
}
});
self.updating_matches = Some((task, rx.clone()));
cx.spawn(move |picker, mut cx| async move {
let Some((commands, matches)) = rx.recv().await else {
return;
};
let mut intercept_result = cx
.try_read_global(|interceptor: &CommandPaletteInterceptor, cx| {
(interceptor.0)(&query, cx)
})
.flatten();
let release_channel = cx
.update(|cx| ReleaseChannel::try_global(cx))
.ok()
.flatten();
if release_channel == Some(ReleaseChannel::Dev) {
if parse_zed_link(&query).is_some() {
intercept_result = Some(CommandInterceptResult {
action: OpenZedUrl { url: query.clone() }.boxed_clone(),
string: query.clone(),
positions: vec![],
})
}
}
if let Some(CommandInterceptResult {
action,
string,
positions,
}) = intercept_result
{
if let Some(idx) = matches
.iter()
.position(|m| commands[m.candidate_id].action.type_id() == action.type_id())
{
matches.remove(idx);
}
commands.push(Command {
name: string.clone(),
action,
});
matches.insert(
0,
StringMatch {
candidate_id: commands.len() - 1,
string,
positions,
score: 0.0,
},
)
}
picker
.update(&mut cx, |picker, _| {
let delegate = &mut picker.delegate;
delegate.commands = commands;
delegate.matches = matches;
if delegate.matches.is_empty() {
delegate.selected_ix = 0;
} else {
delegate.selected_ix =
cmp::min(delegate.selected_ix, delegate.matches.len() - 1);
}
.update(&mut cx, |picker, cx| {
picker
.delegate
.matches_updated(query, commands, matches, cx)
})
.log_err();
})
}
fn finalize_update_matches(
&mut self,
query: String,
duration: Duration,
cx: &mut ViewContext<Picker<Self>>,
) -> bool {
let Some((task, rx)) = self.updating_matches.take() else {
return true;
};
match cx
.background_executor()
.block_with_timeout(duration, rx.clone().recv())
{
Ok(Some((commands, matches))) => {
self.matches_updated(query, commands, matches, cx);
true
}
_ => {
self.updating_matches = Some((task, rx));
false
}
}
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
self.command_palette
.update(cx, |_, cx| cx.emit(DismissEvent))

View File

@@ -38,6 +38,9 @@ smol.workspace = true
theme.workspace = true
util.workspace = true
[target.'cfg(windows)'.dependencies]
async-std = { version = "1.12.0", features = ["unstable"] }
[dev-dependencies]
clock.workspace = true
collections = { workspace = true, features = ["test-support"] }

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(
@@ -512,7 +514,7 @@ impl Copilot {
.await?;
match sign_in {
request::SignInInitiateResult::AlreadySignedIn { user } => {
Ok(request::SignInStatus::Ok { user })
Ok(request::SignInStatus::Ok { user: Some(user) })
}
request::SignInInitiateResult::PromptUserDeviceFlow(flow) => {
this.update(&mut cx, |this, cx| {
@@ -920,7 +922,7 @@ impl Copilot {
if let Ok(server) = self.server.as_running() {
match lsp_status {
request::SignInStatus::Ok { .. }
request::SignInStatus::Ok { user: Some(_) }
| request::SignInStatus::MaybeOk { .. }
| request::SignInStatus::AlreadySignedIn { .. } => {
server.sign_in_status = SignInStatus::Authorized;
@@ -936,7 +938,7 @@ impl Copilot {
self.unregister_buffer(&buffer);
}
}
request::SignInStatus::NotSignedIn => {
request::SignInStatus::Ok { user: None } | request::SignInStatus::NotSignedIn => {
server.sign_in_status = SignInStatus::SignedOut;
for buffer in self.buffers.iter().cloned().collect::<Vec<_>>() {
self.unregister_buffer(&buffer);

View File

@@ -52,7 +52,7 @@ pub struct SignInConfirmParams {
pub enum SignInStatus {
#[serde(rename = "OK")]
Ok {
user: String,
user: Option<String>,
},
MaybeOk {
user: String,

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

@@ -40,7 +40,7 @@ indoc = "1.0.4"
itertools = "0.10"
language.workspace = true
lazy_static.workspace = true
linkify = "0.10.0"
linkify.workspace = true
log.workspace = true
lsp.workspace = true
multi_buffer.workspace = true

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);
@@ -630,8 +632,8 @@ impl EditorElement {
let scroll_top =
layout.position_map.snapshot.scroll_position().y * layout.position_map.line_height;
let gutter_bg = cx.theme().colors().editor_gutter_background;
cx.paint_quad(fill(gutter_bounds, gutter_bg));
cx.paint_quad(fill(text_bounds, self.style.background));
cx.paint_quad(fill(gutter_bounds, gutter_bg), None, None);
cx.paint_quad(fill(text_bounds, self.style.background), None, None);
if let EditorMode::Full = layout.mode {
let mut active_rows = layout.active_rows.iter().peekable();
@@ -655,7 +657,7 @@ impl EditorElement {
layout.position_map.line_height * (end_row - start_row + 1) as f32,
);
let active_line_bg = cx.theme().colors().editor_active_line_background;
cx.paint_quad(fill(Bounds { origin, size }, active_line_bg));
cx.paint_quad(fill(Bounds { origin, size }, active_line_bg), None, None);
}
}
@@ -671,7 +673,11 @@ impl EditorElement {
layout.position_map.line_height * highlighted_rows.len() as f32,
);
let highlighted_line_bg = cx.theme().colors().editor_highlighted_line_background;
cx.paint_quad(fill(Bounds { origin, size }, highlighted_line_bg));
cx.paint_quad(
fill(Bounds { origin, size }, highlighted_line_bg),
None,
None,
);
}
let scroll_left =
@@ -692,13 +698,17 @@ impl EditorElement {
} else {
cx.theme().colors().editor_wrap_guide
};
cx.paint_quad(fill(
Bounds {
origin: point(x, text_bounds.origin.y),
size: size(px(1.), text_bounds.size.height),
},
color,
));
cx.paint_quad(
fill(
Bounds {
origin: point(x, text_bounds.origin.y),
size: size(px(1.), text_bounds.size.height),
},
color,
),
None,
None,
);
}
}
}
@@ -714,20 +724,22 @@ 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;
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),
);
@@ -738,6 +750,7 @@ impl EditorElement {
cx.with_z_index(1, |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,
@@ -746,11 +759,12 @@ impl EditorElement {
let fold_indicator_size = fold_indicator.measure(available_space, cx);
let position = point(
bounds.size.width - layout.gutter_padding,
bounds.size.width - layout.gutter_dimensions.right_padding,
ix as f32 * line_height - (scroll_top % line_height),
);
let centering_offset = point(
(layout.gutter_padding + layout.gutter_margin - fold_indicator_size.width)
(layout.gutter_dimensions.right_padding + layout.gutter_dimensions.margin
- fold_indicator_size.width)
/ 2.,
(line_height - fold_indicator_size.height) / 2.,
);
@@ -760,6 +774,7 @@ impl EditorElement {
}
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,
@@ -770,7 +785,9 @@ impl EditorElement {
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.;
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);
@@ -795,13 +812,17 @@ impl EditorElement {
let highlight_origin = bounds.origin + point(-width, start_y);
let highlight_size = size(width * 2., end_y - start_y);
let highlight_bounds = Bounds::new(highlight_origin, highlight_size);
cx.paint_quad(quad(
highlight_bounds,
Corners::all(1. * line_height),
cx.theme().status().modified,
Edges::default(),
transparent_black(),
));
cx.paint_quad(
quad(
highlight_bounds,
Corners::all(1. * line_height),
cx.theme().status().modified,
Edges::default(),
transparent_black(),
),
None,
None,
);
continue;
}
@@ -828,13 +849,17 @@ impl EditorElement {
let highlight_origin = bounds.origin + point(-width, start_y);
let highlight_size = size(width * 2., end_y - start_y);
let highlight_bounds = Bounds::new(highlight_origin, highlight_size);
cx.paint_quad(quad(
highlight_bounds,
Corners::all(1. * line_height),
cx.theme().status().deleted,
Edges::default(),
transparent_black(),
));
cx.paint_quad(
quad(
highlight_bounds,
Corners::all(1. * line_height),
cx.theme().status().deleted,
Edges::default(),
transparent_black(),
),
None,
None,
);
continue;
}
@@ -868,13 +893,17 @@ impl EditorElement {
let highlight_origin = bounds.origin + point(-width, start_y);
let highlight_size = size(width * 2., end_y - start_y);
let highlight_bounds = Bounds::new(highlight_origin, highlight_size);
cx.paint_quad(quad(
highlight_bounds,
Corners::all(0.05 * line_height),
color,
Edges::default(),
transparent_black(),
));
cx.paint_quad(
quad(
highlight_bounds,
Corners::all(0.05 * line_height),
color,
Edges::default(),
transparent_black(),
),
None,
None,
);
}
}
@@ -885,7 +914,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
@@ -1154,7 +1184,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 +1211,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,18 +1366,22 @@ impl EditorElement {
let thumb_bounds = Bounds::from_corners(point(left, thumb_top), point(right, thumb_bottom));
if layout.show_scrollbars {
cx.paint_quad(quad(
track_bounds,
Corners::default(),
cx.theme().colors().scrollbar_track_background,
Edges {
top: Pixels::ZERO,
right: Pixels::ZERO,
bottom: Pixels::ZERO,
left: px(1.),
},
cx.theme().colors().scrollbar_track_border,
));
cx.paint_quad(
quad(
track_bounds,
Corners::default(),
cx.theme().colors().scrollbar_track_background,
Edges {
top: Pixels::ZERO,
right: Pixels::ZERO,
bottom: Pixels::ZERO,
left: px(1.),
},
cx.theme().colors().scrollbar_track_border,
),
None,
None,
);
let scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
if layout.is_singleton && scrollbar_settings.selections {
let start_anchor = Anchor::min();
@@ -1365,18 +1401,22 @@ impl EditorElement {
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(
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,
),
None,
None,
);
}
}
@@ -1403,18 +1443,22 @@ impl EditorElement {
}
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(
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,
),
None,
None,
);
}
}
@@ -1446,18 +1490,22 @@ impl EditorElement {
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,
));
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,
),
None,
None,
);
}
}
@@ -1504,33 +1552,41 @@ impl EditorElement {
DiagnosticSeverity::INFORMATION => cx.theme().status().info,
_ => cx.theme().status().hint,
};
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,
));
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,
),
None,
None,
);
}
}
cx.paint_quad(quad(
thumb_bounds,
Corners::default(),
cx.theme().colors().scrollbar_thumb_background,
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,
Corners::default(),
cx.theme().colors().scrollbar_thumb_background,
Edges {
top: Pixels::ZERO,
right: px(1.),
bottom: Pixels::ZERO,
left: px(1.),
},
cx.theme().colors().scrollbar_thumb_border,
),
None,
None,
);
}
let interactive_track_bounds = InteractiveBounds {
@@ -1817,7 +1873,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 +1921,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 +2019,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 +2278,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 +2315,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 +2338,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 +2363,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 +2441,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 +2473,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 +2516,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 +2621,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 +2689,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 +2706,7 @@ impl EditorElement {
});
}
(
scroll_width.max(fixed_block_max_width - gutter_width),
scroll_width.max(fixed_block_max_width - gutter_dimensions.width),
blocks,
)
}
@@ -3151,8 +3223,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]>,
@@ -3394,7 +3465,7 @@ impl Cursor {
})
}
cx.paint_quad(cursor);
cx.paint_quad(cursor, None, None);
if let Some(block_text) = &self.block_text {
block_text

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

@@ -147,7 +147,7 @@ impl EditorTestContext {
self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text));
let keystroke = Keystroke::parse(keystroke_text).unwrap();
self.cx.dispatch_keystroke(self.window, keystroke, false);
self.cx.dispatch_keystroke(self.window, keystroke);
keystroke_under_test_handle
}

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>,
@@ -651,6 +651,12 @@ impl ExtensionStore {
let Ok(relative_path) = language_path.strip_prefix(&extension_dir) else {
continue;
};
let Ok(Some(fs_metadata)) = fs.metadata(&language_path).await else {
continue;
};
if !fs_metadata.is_dir {
continue;
}
let config = fs.load(&language_path.join("config.toml")).await?;
let config = ::toml::from_str::<LanguageConfig>(&config)?;

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

@@ -11,6 +11,9 @@ use fsevent::StreamFlags;
#[cfg(not(target_os = "macos"))]
use notify::{Config, EventKind, Watcher};
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
use futures::{future::BoxFuture, Stream, StreamExt};
use git2::Repository as LibGitRepository;
use parking_lot::Mutex;
@@ -21,7 +24,6 @@ use std::io::Write;
use std::sync::Arc;
use std::{
io,
os::unix::fs::MetadataExt,
path::{Component, Path, PathBuf},
pin::Pin,
time::{Duration, SystemTime},
@@ -239,8 +241,15 @@ impl Fs for RealFs {
} else {
symlink_metadata
};
#[cfg(unix)]
let inode = metadata.ino();
#[cfg(windows)]
let inode = file_id(path).await?;
Ok(Some(Metadata {
inode: metadata.ino(),
inode,
mtime: metadata.modified().unwrap(),
is_symlink,
is_dir: metadata.file_type().is_dir(),
@@ -1327,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
@@ -105,7 +106,7 @@ ashpd = "0.7.0"
# todo!(linux) - Technically do not use `randr`, but it doesn't compile otherwise
xcb = { version = "1.3", features = ["as-raw-xcb-connection", "present", "randr", "xkb"] }
wayland-client= { version = "0.31.2" }
wayland-protocols = { version = "0.31.2", features = ["client", "staging"] }
wayland-protocols = { version = "0.31.2", features = ["client", "staging", "unstable"] }
wayland-backend = { version = "0.3.3", features = ["client_system"] }
xkbcommon = { version = "0.7", features = ["wayland", "x11"] }
as-raw-xcb-connection = "1"
@@ -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"

View File

@@ -94,7 +94,7 @@ fn generate_shader_bindings() -> PathBuf {
let mut builder = cbindgen::Builder::new();
let src_paths = [
crate_dir.join("src/scene.rs"),
crate_dir.join("src/scene/primitives.rs"),
crate_dir.join("src/geometry.rs"),
crate_dir.join("src/color.rs"),
crate_dir.join("src/window.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

@@ -39,7 +39,7 @@ use std::any::{Any, TypeId};
/// }
/// register_action!(Paste);
/// ```
pub trait Action: 'static {
pub trait Action: 'static + Send {
/// Clone the action into a new box
fn boxed_clone(&self) -> Box<dyn Action>;

View File

@@ -226,6 +226,7 @@ pub struct AppContext {
pub(crate) entities: EntityMap,
pub(crate) new_view_observers: SubscriberSet<TypeId, NewViewListener>,
pub(crate) windows: SlotMap<WindowId, Option<Window>>,
pub(crate) window_handles: FxHashMap<WindowId, AnyWindowHandle>,
pub(crate) keymap: Rc<RefCell<Keymap>>,
pub(crate) global_action_listeners:
FxHashMap<TypeId, Vec<Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Self)>>>,
@@ -285,6 +286,7 @@ impl AppContext {
globals_by_type: FxHashMap::default(),
entities,
new_view_observers: SubscriberSet::new(),
window_handles: FxHashMap::default(),
windows: SlotMap::with_key(),
keymap: Rc::new(RefCell::new(Keymap::default())),
global_action_listeners: FxHashMap::default(),
@@ -324,6 +326,7 @@ impl AppContext {
}
self.windows.clear();
self.window_handles.clear();
self.flush_effects();
let futures = futures::future::join_all(futures);
@@ -468,8 +471,8 @@ impl AppContext {
/// To find all windows of a given type, you could filter on
pub fn windows(&self) -> Vec<AnyWindowHandle> {
self.windows
.values()
.filter_map(|window| Some(window.as_ref()?.handle))
.keys()
.flat_map(|window_id| self.window_handles.get(&window_id).copied())
.collect()
}
@@ -492,6 +495,7 @@ impl AppContext {
let mut window = Window::new(handle.into(), options, cx);
let root_view = build_root_view(&mut WindowContext::new(cx, &mut window));
window.root_view.replace(root_view.into());
cx.window_handles.insert(id, window.handle);
cx.windows.get_mut(id).unwrap().replace(window);
handle
})
@@ -1239,12 +1243,13 @@ impl Context for AppContext {
.get_mut(handle.id)
.ok_or_else(|| anyhow!("window not found"))?
.take()
.unwrap();
.ok_or_else(|| anyhow!("window not found"))?;
let root_view = window.root_view.clone().unwrap();
let result = update(root_view, &mut WindowContext::new(cx, &mut window));
if window.removed {
cx.window_handles.remove(&handle.id);
cx.windows.remove(handle.id);
} else {
cx.windows

View File

@@ -335,7 +335,7 @@ impl TestAppContext {
.map(Keystroke::parse)
.map(Result::unwrap)
{
self.dispatch_keystroke(window, keystroke.into(), false);
self.dispatch_keystroke(window, keystroke.into());
}
self.background_executor.run_until_parked()
@@ -347,21 +347,16 @@ impl TestAppContext {
/// This will also run the background executor until it's parked.
pub fn simulate_input(&mut self, window: AnyWindowHandle, input: &str) {
for keystroke in input.split("").map(Keystroke::parse).map(Result::unwrap) {
self.dispatch_keystroke(window, keystroke.into(), false);
self.dispatch_keystroke(window, keystroke.into());
}
self.background_executor.run_until_parked()
}
/// dispatches a single Keystroke (see also `simulate_keystrokes` and `simulate_input`)
pub fn dispatch_keystroke(
&mut self,
window: AnyWindowHandle,
keystroke: Keystroke,
is_held: bool,
) {
self.test_window(window)
.simulate_keystroke(keystroke, is_held)
pub fn dispatch_keystroke(&mut self, window: AnyWindowHandle, keystroke: Keystroke) {
self.update_window(window, |_, cx| cx.dispatch_keystroke(keystroke))
.unwrap();
}
/// Returns the `TestWindow` backing the given handle.

View File

@@ -0,0 +1,363 @@
use crate::{Bounds, Half, Point};
use std::{
cmp,
fmt::Debug,
ops::{Add, Sub},
};
#[derive(Debug)]
pub struct BoundsTree<U, T>
where
U: Default + Clone + Debug,
T: Clone + Debug,
{
root: Option<usize>,
nodes: Vec<Node<U, T>>,
stack: Vec<usize>,
}
impl<U, T> BoundsTree<U, T>
where
U: Clone + Debug + PartialOrd + Add<U, Output = U> + Sub<Output = U> + Half + Default,
T: Clone + Debug,
{
pub fn clear(&mut self) {
self.root = None;
self.nodes.clear();
self.stack.clear();
}
pub fn insert(&mut self, new_bounds: Bounds<U>, payload: T) -> 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, payload, 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,
..
} = &mut self.nodes[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.nodes[left].bounds())
.half_perimeter();
let right_cost = new_bounds
.union(&self.nodes[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.nodes[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, payload, 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, .. } = &mut self.nodes[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_order: max_ordering,
..
} = &mut self.nodes[node_index]
else {
unreachable!()
};
*max_ordering = cmp::max(*max_ordering, ordering);
}
ordering
}
/// Finds all nodes whose bounds contain the given point and pushes their (bounds, payload) pairs onto the result vector.
pub(crate) fn find_containing(
&mut self,
point: &Point<U>,
result: &mut Vec<BoundsSearchResult<U, T>>,
) {
if let Some(mut index) = self.root {
self.stack.clear();
self.stack.push(index);
while let Some(current_index) = self.stack.pop() {
match &self.nodes[current_index] {
Node::Leaf {
bounds,
order,
data,
} => {
if bounds.contains(point) {
result.push(BoundsSearchResult {
bounds: bounds.clone(),
order: *order,
data: data.clone(),
});
}
}
Node::Internal {
left,
right,
bounds,
..
} => {
if bounds.contains(point) {
self.stack.push(*left);
self.stack.push(*right);
}
}
}
}
}
}
fn find_max_ordering(&self, index: usize, bounds: &Bounds<U>, mut max_ordering: u32) -> u32 {
match {
let this = &self;
&this.nodes[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_order: node_max_ordering,
..
} => {
if bounds.intersects(node_bounds) && max_ordering < *node_max_ordering {
let left_max_ordering = self.nodes[*left].max_ordering();
let right_max_ordering = self.nodes[*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>, payload: T, order: u32) -> usize {
self.nodes.push(Node::Leaf {
bounds,
data: payload,
order,
});
self.nodes.len() - 1
}
fn push_internal(&mut self, left: usize, right: usize) -> usize {
let left_node = &self.nodes[left];
let right_node = &self.nodes[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_order: max_ordering,
});
self.nodes.len() - 1
}
}
impl<U, T> Default for BoundsTree<U, T>
where
U: Default + Clone + Debug,
T: Clone + Debug,
{
fn default() -> Self {
BoundsTree {
root: None,
nodes: Vec::new(),
stack: Vec::new(),
}
}
}
#[derive(Debug, Clone)]
enum Node<U, T>
where
U: Clone + Default + Debug,
T: Clone + Debug,
{
Leaf {
bounds: Bounds<U>,
order: u32,
data: T,
},
Internal {
left: usize,
right: usize,
bounds: Bounds<U>,
max_order: u32,
},
}
impl<U, T> Node<U, T>
where
U: Clone + Default + Debug,
T: Clone + 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_order: max_ordering,
..
} => *max_ordering,
}
}
}
pub struct BoundsSearchResult<U: Clone + Default + Debug, T> {
pub bounds: Bounds<U>,
pub order: u32,
pub data: T,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Bounds, Point, Size};
#[test]
fn test_insert_and_find_containing() {
let mut tree = BoundsTree::<f32, String>::default();
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,
},
};
let bounds3 = Bounds {
origin: Point { x: 10.0, y: 10.0 },
size: Size {
width: 10.0,
height: 10.0,
},
};
// Insert bounds into the tree
tree.insert(bounds1.clone(), "Payload 1".to_string());
tree.insert(bounds2.clone(), "Payload 2".to_string());
tree.insert(bounds3.clone(), "Payload 3".to_string());
// Points for testing
let point_inside_bounds1 = Point { x: 1.0, y: 1.0 };
let point_inside_bounds1_and_2 = Point { x: 6.0, y: 6.0 };
let point_inside_bounds2_and_3 = Point { x: 12.0, y: 12.0 };
let point_outside_all_bounds = Point { x: 21.0, y: 21.0 };
assert!(!bounds1.contains(&point_inside_bounds2_and_3));
assert!(!bounds1.contains(&point_outside_all_bounds));
assert!(bounds2.contains(&point_inside_bounds1_and_2));
assert!(bounds2.contains(&point_inside_bounds2_and_3));
assert!(!bounds2.contains(&point_outside_all_bounds));
assert!(!bounds3.contains(&point_inside_bounds1));
assert!(bounds3.contains(&point_inside_bounds2_and_3));
assert!(!bounds3.contains(&point_outside_all_bounds));
// Test find_containing for different points
let mut result = Vec::new();
tree.find_containing(&point_inside_bounds1, &mut result);
assert_eq!(result.len(), 1);
assert_eq!(result[0].data, "Payload 1");
result.clear();
tree.find_containing(&point_inside_bounds1_and_2, &mut result);
assert_eq!(result.len(), 2);
assert!(result.iter().any(|r| r.data == "Payload 1"));
assert!(result.iter().any(|r| r.data == "Payload 2"));
result.clear();
tree.find_containing(&point_inside_bounds2_and_3, &mut result);
assert_eq!(result.len(), 2);
assert!(result.iter().any(|r| r.data == "Payload 2"));
assert!(result.iter().any(|r| r.data == "Payload 3"));
result.clear();
tree.find_containing(&point_outside_all_bounds, &mut result);
assert_eq!(result.len(), 0);
}
}

View File

@@ -338,6 +338,11 @@ impl Hsla {
self.a == 0.0
}
/// Returns true if the HSLA color is fully opaque, false otherwise.
pub fn is_opaque(&self) -> bool {
self.a == 1.0
}
/// Blends `other` on top of `self` based on `other`'s alpha value. The resulting color is a combination of `self`'s and `other`'s colors.
///
/// If `other`'s alpha value is 1.0 or greater, `other` color is fully opaque, thus `other` is returned as the output color.

View File

@@ -45,7 +45,7 @@ impl Element for Canvas {
}
fn paint(&mut self, bounds: Bounds<Pixels>, style: &mut Style, cx: &mut ElementContext) {
style.paint(bounds, cx, |cx| {
style.paint(bounds, None, None, cx, |cx| {
(self.paint_callback.take().unwrap())(&bounds, cx)
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -152,7 +152,7 @@ impl Element for Img {
let size = size(surface.width().into(), surface.height().into());
let new_bounds = preserve_aspect_ratio(bounds, size);
// TODO: Add support for corner_radii and grayscale.
cx.paint_surface(new_bounds, surface);
cx.paint_surface(new_bounds, surface, true);
}
};
});

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> {
@@ -2617,6 +2639,12 @@ pub trait Half {
fn half(&self) -> Self;
}
impl Half for i32 {
fn half(&self) -> Self {
self / 2
}
}
impl Half for f32 {
fn half(&self) -> Self {
self / 2.

View File

@@ -70,6 +70,7 @@ mod app;
mod arena;
mod assets;
mod bounds_tree;
mod color;
mod element;
mod elements;
@@ -117,6 +118,7 @@ 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::*;

View File

@@ -491,8 +491,8 @@ mod test {
.update(cx, |test_view, cx| cx.focus(&test_view.focus_handle))
.unwrap();
cx.dispatch_keystroke(*window, Keystroke::parse("a").unwrap(), false);
cx.dispatch_keystroke(*window, Keystroke::parse("ctrl-g").unwrap(), false);
cx.dispatch_keystroke(*window, Keystroke::parse("a").unwrap());
cx.dispatch_keystroke(*window, Keystroke::parse("ctrl-g").unwrap());
window
.update(cx, |test_view, _| {

View File

@@ -61,6 +61,10 @@ pub(crate) fn current_platform() -> Rc<dyn Platform> {
pub(crate) fn current_platform() -> Rc<dyn Platform> {
Rc::new(LinuxPlatform::new())
}
#[cfg(target_os = "windows")]
pub(crate) fn current_platform() -> Rc<dyn Platform> {
todo!("windows")
}
pub(crate) trait Platform: 'static {
fn background_executor(&self) -> BackgroundExecutor;
@@ -412,7 +416,7 @@ impl PlatformInputHandler {
.flatten()
}
pub(crate) fn flush_pending_input(&mut self, input: &str, cx: &mut WindowContext) {
pub(crate) fn dispatch_input(&mut self, input: &str, cx: &mut WindowContext) {
self.handler.replace_text_in_range(None, input, cx);
}
}

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

@@ -215,6 +215,15 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
}
let quad = b_quads[input.quad_id];
// Fast path when the quad is not rounded and doesn't have any border.
if (quad.corner_radii.top_left == 0.0 && quad.corner_radii.bottom_left == 0.0 &&
quad.corner_radii.top_right == 0.0 &&
quad.corner_radii.bottom_right == 0.0 && quad.border_widths.top == 0.0 &&
quad.border_widths.left == 0.0 && quad.border_widths.right == 0.0 &&
quad.border_widths.bottom == 0.0) {
return input.background_color;
}
let half_size = quad.bounds.size / 2.0;
let center = quad.bounds.origin + half_size;
let center_to_point = input.position.xy - center;

View File

@@ -3,6 +3,7 @@ mod client_dispatcher;
mod dispatcher;
mod platform;
mod text_system;
mod util;
mod wayland;
mod x11;

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

@@ -0,0 +1,30 @@
use xkbcommon::xkb::{self, Keycode, Keysym, State};
use crate::{Keystroke, Modifiers};
impl Keystroke {
pub(super) fn from_xkb(state: &State, modifiers: Modifiers, keycode: Keycode) -> Self {
let key_utf32 = state.key_get_utf32(keycode);
let key_utf8 = state.key_get_utf8(keycode);
let key_sym = state.key_get_one_sym(keycode);
let key = match key_sym {
Keysym::Return => "enter".to_owned(),
Keysym::Prior => "pageup".to_owned(),
Keysym::Next => "pagedown".to_owned(),
_ => xkb::keysym_get_name(key_sym).to_lowercase(),
};
// 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))
&& !key_utf8.is_empty())
.then_some(key_utf8);
Keystroke {
modifiers,
key,
ime_key,
}
}
}

View File

@@ -19,19 +19,21 @@ use wayland_protocols::wp::fractional_scale::v1::client::{
wp_fractional_scale_manager_v1, wp_fractional_scale_v1,
};
use wayland_protocols::wp::viewporter::client::{wp_viewport, wp_viewporter};
use wayland_protocols::xdg::decoration::zv1::client::{
zxdg_decoration_manager_v1, zxdg_toplevel_decoration_v1,
};
use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base};
use xkbcommon::xkb;
use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1;
use xkbcommon::xkb::{Keycode, KEYMAP_COMPILE_NO_FLAGS};
use xkbcommon::xkb::{self, Keycode, KEYMAP_COMPILE_NO_FLAGS};
use crate::platform::linux::client::Client;
use crate::platform::linux::wayland::window::WaylandWindow;
use crate::platform::linux::wayland::window::{WaylandDecorationState, WaylandWindow};
use crate::platform::{LinuxPlatformInner, PlatformWindow};
use crate::ScrollDelta;
use crate::{
platform::linux::wayland::window::WaylandWindowState, AnyWindowHandle, DisplayId, KeyDownEvent,
KeyUpEvent, Keystroke, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
Pixels, PlatformDisplay, PlatformInput, Point, ScrollWheelEvent, TouchPhase, WindowOptions,
NavigationDirection, Pixels, PlatformDisplay, PlatformInput, Point, ScrollDelta,
ScrollWheelEvent, TouchPhase, WindowOptions,
};
const MIN_KEYCODE: u32 = 8; // used to convert evdev scancode to xkb scancode
@@ -42,6 +44,7 @@ pub(crate) struct WaylandClientState {
wm_base: Option<xdg_wm_base::XdgWmBase>,
viewporter: Option<wp_viewporter::WpViewporter>,
fractional_scale_manager: Option<wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1>,
decoration_manager: Option<zxdg_decoration_manager_v1::ZxdgDecorationManagerV1>,
windows: Vec<(xdg_surface::XdgSurface, Rc<WaylandWindowState>)>,
platform_inner: Rc<LinuxPlatformInner>,
wl_seat: Option<wl_seat::WlSeat>,
@@ -70,6 +73,7 @@ impl WaylandClient {
wm_base: None,
viewporter: None,
fractional_scale_manager: None,
decoration_manager: None,
windows: Vec::new(),
platform_inner: Rc::clone(&linux_platform_inner),
wl_seat: None,
@@ -143,6 +147,31 @@ impl Client for WaylandClient {
let toplevel = xdg_surface.get_toplevel(&self.qh, ());
let wl_surface = Arc::new(wl_surface);
// Attempt to set up window decorations based on the requested configuration
//
// Note that wayland compositors may either not support decorations at all, or may
// support them but not allow clients to choose whether they are enabled or not.
// We attempt to account for these cases here.
if let Some(decoration_manager) = state.decoration_manager.as_ref() {
// The protocol for managing decorations is present at least, but that doesn't
// mean that the compositor will allow us to use it.
let decoration =
decoration_manager.get_toplevel_decoration(&toplevel, &self.qh, xdg_surface.id());
// todo!(linux) - options.titlebar is lacking information required for wayland.
// Especially, whether a titlebar is wanted in itself.
//
// Removing the titlebar also removes the entire window frame (ie. the ability to
// close, move and resize the window [snapping still works]). This needs additional
// handling in Zed, in order to implement drag handlers on a titlebar element.
//
// Since all of this handling is not present, we request server-side decorations
// for now as a stopgap solution.
decoration.set_mode(zxdg_toplevel_decoration_v1::Mode::ServerSide);
}
let viewport = state
.viewporter
.as_ref()
@@ -210,6 +239,17 @@ impl Dispatch<wl_registry::WlRegistry, ()> for WaylandClientState {
registry.bind::<wp_viewporter::WpViewporter, _, _>(name, 1, qh, ());
state.viewporter = Some(view_porter);
}
"zxdg_decoration_manager_v1" => {
// Unstable and optional
let decoration_manager = registry
.bind::<zxdg_decoration_manager_v1::ZxdgDecorationManagerV1, _, _>(
name,
1,
qh,
(),
);
state.decoration_manager = Some(decoration_manager);
}
_ => {}
};
}
@@ -222,6 +262,7 @@ delegate_noop!(WaylandClientState: ignore wl_shm::WlShm);
delegate_noop!(WaylandClientState: ignore wl_shm_pool::WlShmPool);
delegate_noop!(WaylandClientState: ignore wl_buffer::WlBuffer);
delegate_noop!(WaylandClientState: ignore wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1);
delegate_noop!(WaylandClientState: ignore zxdg_decoration_manager_v1::ZxdgDecorationManagerV1);
delegate_noop!(WaylandClientState: ignore wp_viewporter::WpViewporter);
delegate_noop!(WaylandClientState: ignore wp_viewport::WpViewport);
@@ -380,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,
@@ -411,55 +469,56 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientState {
} => {
let keymap_state = state.keymap_state.as_ref().unwrap();
let keycode = Keycode::from(key + MIN_KEYCODE);
let key_utf32 = keymap_state.key_get_utf32(keycode);
let key_utf8 = keymap_state.key_get_utf8(keycode);
let key_sym = keymap_state.key_get_one_sym(keycode);
let key = xkb::keysym_get_name(key_sym).to_lowercase();
// 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 focused_window = &state.keyboard_focused_window;
if let Some(focused_window) = focused_window {
match key_state {
wl_keyboard::KeyState::Pressed => {
focused_window.handle_input(PlatformInput::KeyDown(KeyDownEvent {
keystroke: Keystroke {
modifiers: state.modifiers,
key,
ime_key,
},
keystroke: Keystroke::from_xkb(
keymap_state,
state.modifiers,
keycode,
),
is_held: false, // todo!(linux)
}));
}
wl_keyboard::KeyState::Released => {
focused_window.handle_input(PlatformInput::KeyUp(KeyUpEvent {
keystroke: Keystroke {
modifiers: state.modifiers,
key,
ime_key,
},
keystroke: Keystroke::from_xkb(
keymap_state,
state.modifiers,
keycode,
),
}));
}
_ => {}
}
}
}
wl_keyboard::Event::Leave { .. } => {}
_ => {}
}
}
}
fn linux_button_to_gpui(button: u32) -> MouseButton {
match button {
0x110 => MouseButton::Left,
0x111 => MouseButton::Right,
0x112 => MouseButton::Middle,
_ => unimplemented!(), // todo!(linux)
}
fn linux_button_to_gpui(button: u32) -> Option<MouseButton> {
// These values are coming from <linux/input-event-codes.h>.
const BTN_LEFT: u32 = 0x110;
const BTN_RIGHT: u32 = 0x111;
const BTN_MIDDLE: u32 = 0x112;
const BTN_SIDE: u32 = 0x113;
const BTN_EXTRA: u32 = 0x114;
const BTN_FORWARD: u32 = 0x115;
const BTN_BACK: u32 = 0x116;
Some(match button {
BTN_LEFT => MouseButton::Left,
BTN_RIGHT => MouseButton::Right,
BTN_MIDDLE => MouseButton::Middle,
BTN_BACK | BTN_SIDE => MouseButton::Navigate(NavigationDirection::Back),
BTN_FORWARD | BTN_EXTRA => MouseButton::Navigate(NavigationDirection::Forward),
_ => return None,
})
}
impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientState {
@@ -513,16 +572,17 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientState {
..
} => {
let focused_window = &state.mouse_focused_window;
let mouse_location = &state.mouse_location;
if let (Some(focused_window), Some(mouse_location)) =
(focused_window, mouse_location)
let mouse_location = state.mouse_location;
let button = linux_button_to_gpui(button);
if let (Some(focused_window), Some(mouse_location), Some(button)) =
(focused_window, mouse_location, button)
{
match button_state {
wl_pointer::ButtonState::Pressed => {
state.button_pressed = Some(linux_button_to_gpui(button));
state.button_pressed = Some(button);
focused_window.handle_input(PlatformInput::MouseDown(MouseDownEvent {
button: linux_button_to_gpui(button),
position: *mouse_location,
button,
position: mouse_location,
modifiers: state.modifiers,
click_count: 1,
}));
@@ -530,8 +590,8 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientState {
wl_pointer::ButtonState::Released => {
state.button_pressed = None;
focused_window.handle_input(PlatformInput::MouseUp(MouseUpEvent {
button: linux_button_to_gpui(button),
position: *mouse_location,
button,
position: mouse_location,
modifiers: Modifiers::default(),
click_count: 1,
}));
@@ -614,3 +674,42 @@ impl Dispatch<wp_fractional_scale_v1::WpFractionalScaleV1, ObjectId> for Wayland
}
}
}
impl Dispatch<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1, ObjectId>
for WaylandClientState
{
fn event(
state: &mut Self,
_: &zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1,
event: zxdg_toplevel_decoration_v1::Event,
surface_id: &ObjectId,
_: &Connection,
_: &QueueHandle<Self>,
) {
if let zxdg_toplevel_decoration_v1::Event::Configure { mode, .. } = event {
for window in &state.windows {
if window.0.id() == *surface_id {
match mode {
WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ServerSide) => {
window
.1
.set_decoration_state(WaylandDecorationState::Server);
}
WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ClientSide) => {
window
.1
.set_decoration_state(WaylandDecorationState::Client);
}
WEnum::Value(_) => {
log::warn!("Unknown decoration mode");
}
WEnum::Unknown(v) => {
log::warn!("Unknown decoration mode: {}", v);
}
}
return;
}
}
}
}
}

View File

@@ -41,6 +41,7 @@ struct WaylandWindowInner {
bounds: Bounds<i32>,
scale: f32,
input_handler: Option<PlatformInputHandler>,
decoration_state: WaylandDecorationState,
}
struct RawWindow {
@@ -96,6 +97,9 @@ impl WaylandWindowInner {
bounds,
scale: 1.0,
input_handler: None,
// On wayland, decorations are by default provided by the client
decoration_state: WaylandDecorationState::Client,
}
}
}
@@ -187,6 +191,20 @@ impl WaylandWindowState {
self.set_size_and_scale(bounds.size.width, bounds.size.height, scale)
}
/// Notifies the window of the state of the decorations.
///
/// # Note
///
/// This API is indirectly called by the wayland compositor and
/// not meant to be called by a user who wishes to change the state
/// of the decorations. This is because the state of the decorations
/// is managed by the compositor and not the client.
pub fn set_decoration_state(&self, state: WaylandDecorationState) {
self.inner.lock().decoration_state = state;
log::trace!("Window decorations are now handled by {:?}", state);
// todo!(linux) - Handle this properly
}
pub fn close(&self) {
let mut callbacks = self.callbacks.lock();
if let Some(fun) = callbacks.close.take() {
@@ -210,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)]
@@ -331,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)>) {
@@ -377,3 +401,12 @@ impl PlatformWindow for WaylandWindow {
//todo!(linux)
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum WaylandDecorationState {
/// Decorations are to be provided by the client
Client,
/// Decorations are provided by the server
Server,
}

View File

@@ -11,7 +11,8 @@ use crate::platform::{
LinuxPlatformInner, PlatformWindow, X11Display, X11Window, X11WindowState, XcbAtoms,
};
use crate::{
AnyWindowHandle, Bounds, DisplayId, PlatformDisplay, PlatformInput, Point, Size, WindowOptions,
AnyWindowHandle, Bounds, DisplayId, PlatformDisplay, PlatformInput, Point, ScrollDelta, Size,
TouchPhase, WindowOptions,
};
pub(crate) struct X11ClientState {
@@ -70,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() {
@@ -106,42 +110,42 @@ impl Client for X11Client {
window.request_refresh();
}
xcb::Event::Present(xcb::present::Event::IdleNotify(_ev)) => {}
xcb::Event::X(x::Event::FocusIn(ev)) => {
let window = self.get_window(ev.event());
window.set_focused(true);
}
xcb::Event::X(x::Event::FocusOut(ev)) => {
let window = self.get_window(ev.event());
window.set_focused(false);
}
xcb::Event::X(x::Event::KeyPress(ev)) => {
let window = self.get_window(ev.event());
let modifiers = super::modifiers_from_state(ev.state());
let key = {
let keystroke = {
let code = ev.detail().into();
let mut state = self.state.lock();
let key = state.xkb.key_get_utf8(code);
let keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
state.xkb.update_key(code, xkb::KeyDirection::Down);
key
keystroke
};
window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent {
keystroke: crate::Keystroke {
modifiers,
key,
ime_key: None,
},
keystroke,
is_held: false,
}));
}
xcb::Event::X(x::Event::KeyRelease(ev)) => {
let window = self.get_window(ev.event());
let modifiers = super::modifiers_from_state(ev.state());
let key = {
let keystroke = {
let code = ev.detail().into();
let mut state = self.state.lock();
let key = state.xkb.key_get_utf8(code);
let keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
state.xkb.update_key(code, xkb::KeyDirection::Up);
key
keystroke
};
window.handle_input(PlatformInput::KeyUp(crate::KeyUpEvent {
keystroke: crate::Keystroke {
modifiers,
key,
ime_key: None,
},
}));
window.handle_input(PlatformInput::KeyUp(crate::KeyUpEvent { keystroke }));
}
xcb::Event::X(x::Event::ButtonPress(ev)) => {
let window = self.get_window(ev.event());
@@ -155,6 +159,15 @@ impl Client for X11Client {
modifiers,
click_count: 1,
}));
} else if ev.detail() >= 4 && ev.detail() <= 5 {
// https://stackoverflow.com/questions/15510472/scrollwheel-event-in-x11
let delta_x = if ev.detail() == 4 { 1.0 } else { -1.0 };
window.handle_input(PlatformInput::ScrollWheel(crate::ScrollWheelEvent {
position,
delta: ScrollDelta::Lines(Point::new(0.0, delta_x)),
modifiers,
touch_phase: TouchPhase::default(),
}));
} else {
log::warn!("Unknown button press: {ev:?}");
}
@@ -200,6 +213,7 @@ impl Client for X11Client {
_ => {}
}
profiling::scope!("Runnables");
if let Ok(runnable) = self.platform_inner.main_receiver.try_recv() {
runnable.run();
}
@@ -209,6 +223,7 @@ impl Client for X11Client {
fun();
}
}
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
let setup = self.xcb_connection.get_setup();
setup
@@ -220,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,14 @@
use xcb::x;
use crate::{Modifiers, MouseButton};
use crate::{Modifiers, MouseButton, NavigationDirection};
pub(crate) fn button_of_key(detail: x::Button) -> Option<MouseButton> {
Some(match detail {
1 => MouseButton::Left,
2 => MouseButton::Middle,
3 => MouseButton::Right,
8 => MouseButton::Navigate(NavigationDirection::Back),
9 => MouseButton::Navigate(NavigationDirection::Forward),
_ => return None,
})
}

View File

@@ -2,9 +2,9 @@
#![allow(unused)]
use crate::{
platform::blade::BladeRenderer, size, Bounds, GlobalPixels, Pixels, PlatformDisplay,
PlatformInput, PlatformInputHandler, PlatformWindow, Point, Size, WindowAppearance,
WindowBounds, WindowOptions, X11Display,
platform::blade::BladeRenderer, size, Bounds, GlobalPixels, Modifiers, Pixels, PlatformAtlas,
PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, PromptLevel,
Scene, Size, WindowAppearance, WindowBounds, WindowOptions, X11Display,
};
use blade_graphics as gpu;
use parking_lot::Mutex;
@@ -27,7 +27,7 @@ use std::{
#[derive(Default)]
struct Callbacks {
request_frame: Option<Box<dyn FnMut()>>,
input: Option<Box<dyn FnMut(crate::PlatformInput) -> bool>>,
input: Option<Box<dyn FnMut(PlatformInput) -> bool>>,
active_status_change: Option<Box<dyn FnMut(bool)>>,
resize: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
fullscreen: Option<Box<dyn FnMut(bool)>>,
@@ -155,6 +155,9 @@ impl X11WindowState {
x::Cw::EventMask(
x::EventMask::EXPOSURE
| x::EventMask::STRUCTURE_NOTIFY
| x::EventMask::ENTER_WINDOW
| x::EventMask::LEAVE_WINDOW
| x::EventMask::FOCUS_CHANGE
| x::EventMask::KEY_PRESS
| x::EventMask::KEY_RELEASE
| x::EventMask::BUTTON_PRESS
@@ -205,25 +208,21 @@ impl X11WindowState {
});
}
}
xcb_connection
.send_and_check_request(&x::ChangeProperty {
mode: x::PropMode::Replace,
window: x_window,
property: atoms.wm_protocols,
r#type: x::ATOM_ATOM,
data: &[atoms.wm_del_window],
})
.unwrap();
xcb_connection.send_request(&x::ChangeProperty {
mode: x::PropMode::Replace,
window: x_window,
property: atoms.wm_protocols,
r#type: x::ATOM_ATOM,
data: &[atoms.wm_del_window],
});
let fake_id = xcb_connection.generate_id();
xcb_connection
.send_and_check_request(&xcb::present::SelectInput {
eid: fake_id,
window: x_window,
//Note: also consider `IDLE_NOTIFY`
event_mask: xcb::present::EventMask::COMPLETE_NOTIFY,
})
.unwrap();
xcb_connection.send_request(&xcb::present::SelectInput {
eid: fake_id,
window: x_window,
//Note: also consider `IDLE_NOTIFY`
event_mask: xcb::present::EventMask::COMPLETE_NOTIFY,
});
xcb_connection.send_request(&x::MapWindow { window: x_window });
xcb_connection.flush().unwrap();
@@ -241,7 +240,7 @@ impl X11WindowState {
gpu::Context::init_windowed(
&raw,
gpu::ContextDesc {
validation: cfg!(debug_assertions),
validation: false,
capture: false,
},
)
@@ -321,15 +320,13 @@ impl X11WindowState {
}
pub fn request_refresh(&self) {
self.xcb_connection
.send_and_check_request(&xcb::present::NotifyMsc {
window: self.x_window,
serial: 0,
target_msc: 0,
divisor: 1,
remainder: 0,
})
.unwrap();
self.xcb_connection.send_request(&xcb::present::NotifyMsc {
window: self.x_window,
serial: 0,
target_msc: 0,
divisor: 1,
remainder: 0,
});
}
pub fn handle_input(&self, input: PlatformInput) {
@@ -341,10 +338,18 @@ impl X11WindowState {
if let PlatformInput::KeyDown(event) = input {
let mut inner = self.inner.lock();
if let Some(ref mut input_handler) = inner.input_handler {
input_handler.replace_text_in_range(None, &event.keystroke.key);
if let Some(ime_key) = &event.keystroke.ime_key {
input_handler.replace_text_in_range(None, ime_key);
}
}
}
}
pub fn set_focused(&self, focus: bool) {
if let Some(ref mut fun) = self.callbacks.lock().active_status_change {
fun(focus);
}
}
}
impl PlatformWindow for X11Window {
@@ -374,14 +379,20 @@ impl PlatformWindow for X11Window {
Rc::clone(&self.0.display)
}
//todo!(linux)
fn mouse_position(&self) -> Point<Pixels> {
Point::default()
let cookie = self.0.xcb_connection.send_request(&x::QueryPointer {
window: self.0.x_window,
});
let reply: x::QueryPointerReply = self.0.xcb_connection.wait_for_reply(cookie).unwrap();
Point::new(
(reply.root_x() as u32).into(),
(reply.root_y() as u32).into(),
)
}
//todo!(linux)
fn modifiers(&self) -> crate::Modifiers {
crate::Modifiers::default()
fn modifiers(&self) -> Modifiers {
Modifiers::default()
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
@@ -399,7 +410,7 @@ impl PlatformWindow for X11Window {
//todo!(linux)
fn prompt(
&self,
_level: crate::PromptLevel,
_level: PromptLevel,
_msg: &str,
_detail: Option<&str>,
_answers: &[&str],
@@ -456,7 +467,7 @@ impl PlatformWindow for X11Window {
self.0.callbacks.lock().request_frame = Some(callback);
}
fn on_input(&self, callback: Box<dyn FnMut(crate::PlatformInput) -> bool>) {
fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> bool>) {
self.0.callbacks.lock().input = Some(callback);
}
@@ -489,16 +500,16 @@ impl PlatformWindow for X11Window {
}
//todo!(linux)
fn is_topmost_for_position(&self, _position: crate::Point<Pixels>) -> bool {
fn is_topmost_for_position(&self, _position: Point<Pixels>) -> bool {
unimplemented!()
}
fn draw(&self, scene: &crate::Scene) {
fn draw(&self, scene: &Scene) {
let mut inner = self.0.inner.lock();
inner.renderer.draw(scene);
}
fn sprite_atlas(&self) -> sync::Arc<dyn crate::PlatformAtlas> {
fn sprite_atlas(&self) -> sync::Arc<dyn PlatformAtlas> {
let inner = self.0.inner.lock();
inner.renderer.sprite_atlas().clone()
}

View File

@@ -125,6 +125,9 @@ impl Platform for TestPlatform {
#[cfg(target_os = "macos")]
return Arc::new(crate::platform::mac::MacTextSystem::new());
#[cfg(target_os = "windows")]
todo!("windows")
}
fn run(&self, _on_finish_launching: Box<dyn FnOnce()>) {

View File

@@ -1,7 +1,7 @@
use crate::{
px, AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, KeyDownEvent, Keystroke,
Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow,
Point, Size, TestPlatform, TileId, WindowAppearance, WindowBounds, WindowOptions,
px, AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, Pixels, PlatformAtlas,
PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, Size,
TestPlatform, TileId, WindowAppearance, WindowBounds, WindowOptions,
};
use collections::HashMap;
use parking_lot::Mutex;
@@ -112,41 +112,6 @@ impl TestWindow {
self.0.lock().input_callback = Some(callback);
result
}
pub fn simulate_keystroke(&mut self, mut keystroke: Keystroke, is_held: bool) {
if keystroke.ime_key.is_none()
&& !keystroke.modifiers.command
&& !keystroke.modifiers.control
&& !keystroke.modifiers.function
{
keystroke.ime_key = Some(if keystroke.modifiers.shift {
keystroke.key.to_ascii_uppercase().clone()
} else {
keystroke.key.clone()
})
}
if self.simulate_input(PlatformInput::KeyDown(KeyDownEvent {
keystroke: keystroke.clone(),
is_held,
})) {
return;
}
let mut lock = self.0.lock();
let Some(mut input_handler) = lock.input_handler.take() else {
panic!(
"simulate_keystroke {:?} input event was not handled and there was no active input",
&keystroke
);
};
drop(lock);
if let Some(text) = keystroke.ime_key.as_ref() {
input_handler.replace_text_in_range(None, &text);
}
self.0.lock().input_handler = Some(input_handler);
}
}
impl PlatformWindow for TestWindow {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,345 @@
use crate::{
point, AtlasTile, Bounds, ContentMask, Corners, Edges, EntityId, Hsla, Pixels, Point,
ScaledPixels,
};
use std::fmt::Debug;
#[derive(Default, Debug, Clone, Eq, PartialEq)]
#[repr(C)]
pub(crate) struct Quad {
pub view_id: ViewId,
pub order: u32,
pub bounds: Bounds<ScaledPixels>,
pub content_mask: ContentMask<ScaledPixels>,
pub background: Hsla,
pub border_color: Hsla,
pub corner_radii: Corners<ScaledPixels>,
pub border_widths: Edges<ScaledPixels>,
}
impl Ord for Quad {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.order.cmp(&other.order)
}
}
impl PartialOrd for Quad {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
#[repr(C)]
pub(crate) struct Underline {
pub view_id: ViewId,
pub order: u32,
pub bounds: Bounds<ScaledPixels>,
pub content_mask: ContentMask<ScaledPixels>,
pub color: Hsla,
pub thickness: ScaledPixels,
pub wavy: bool,
}
impl Ord for Underline {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.order.cmp(&other.order)
}
}
impl PartialOrd for Underline {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
#[repr(C)]
pub(crate) struct Shadow {
pub view_id: ViewId,
pub order: u32,
pub bounds: Bounds<ScaledPixels>,
pub corner_radii: Corners<ScaledPixels>,
pub content_mask: ContentMask<ScaledPixels>,
pub color: Hsla,
pub blur_radius: ScaledPixels,
pub pad: u32, // align to 8 bytes
}
impl Ord for Shadow {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.order.cmp(&other.order)
}
}
impl PartialOrd for Shadow {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[repr(C)]
pub(crate) struct MonochromeSprite {
pub view_id: ViewId,
pub order: u32,
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))
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[repr(C)]
pub(crate) struct PolychromeSprite {
pub view_id: ViewId,
pub order: u32,
pub bounds: Bounds<ScaledPixels>,
pub content_mask: ContentMask<ScaledPixels>,
pub corner_radii: Corners<ScaledPixels>,
pub tile: AtlasTile,
pub grayscale: bool,
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))
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct Surface {
pub view_id: ViewId,
pub order: u32,
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))
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
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) id: PathId,
pub(crate) view_id: ViewId,
pub(crate) order: u32,
pub(crate) bounds: Bounds<P>,
pub(crate) content_mask: ContentMask<P>,
pub(crate) vertices: Vec<PathVertex<P>>,
pub(crate) color: Hsla,
pub(crate) start: Point<P>,
pub(crate) current: Point<P>,
pub(crate) contour_count: usize,
}
impl Path<Pixels> {
/// Create a new path with the given starting point.
pub fn new(start: Point<Pixels>) -> Self {
Self {
id: PathId(0),
view_id: ViewId::default(),
order: u32::default(),
vertices: Vec::new(),
start,
current: start,
bounds: Bounds {
origin: start,
size: Default::default(),
},
content_mask: Default::default(),
color: Default::default(),
contour_count: 0,
}
}
/// Scale this path by the given factor.
pub fn scale(&self, factor: f32) -> Path<ScaledPixels> {
Path {
id: self.id,
view_id: self.view_id,
order: self.order,
bounds: self.bounds.scale(factor),
content_mask: self.content_mask.scale(factor),
vertices: self
.vertices
.iter()
.map(|vertex| vertex.scale(factor))
.collect(),
start: self.start.map(|start| start.scale(factor)),
current: self.current.scale(factor),
contour_count: self.contour_count,
color: self.color,
}
}
/// Draw a straight line from the current point to the given point.
pub fn line_to(&mut self, to: Point<Pixels>) {
self.contour_count += 1;
if self.contour_count > 1 {
self.push_triangle(
(self.start, self.current, to),
(point(0., 1.), point(0., 1.), point(0., 1.)),
);
}
self.current = to;
}
/// Draw a curve from the current point to the given point, using the given control point.
pub fn curve_to(&mut self, to: Point<Pixels>, ctrl: Point<Pixels>) {
self.contour_count += 1;
if self.contour_count > 1 {
self.push_triangle(
(self.start, self.current, to),
(point(0., 1.), point(0., 1.), point(0., 1.)),
);
}
self.push_triangle(
(self.current, ctrl, to),
(point(0., 0.), point(0.5, 0.), point(1., 1.)),
);
self.current = to;
}
fn push_triangle(
&mut self,
xy: (Point<Pixels>, Point<Pixels>, Point<Pixels>),
st: (Point<f32>, Point<f32>, Point<f32>),
) {
self.bounds = self
.bounds
.union(&Bounds {
origin: xy.0,
size: Default::default(),
})
.union(&Bounds {
origin: xy.1,
size: Default::default(),
})
.union(&Bounds {
origin: xy.2,
size: Default::default(),
});
self.vertices.push(PathVertex {
xy_position: xy.0,
st_position: st.0,
content_mask: Default::default(),
});
self.vertices.push(PathVertex {
xy_position: xy.1,
st_position: st.1,
content_mask: Default::default(),
});
self.vertices.push(PathVertex {
xy_position: xy.2,
st_position: st.2,
content_mask: Default::default(),
});
}
}
impl Eq for Path<ScaledPixels> {}
impl PartialEq for Path<ScaledPixels> {
fn eq(&self, other: &Self) -> bool {
self.order == other.order
}
}
impl Ord for Path<ScaledPixels> {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.order.cmp(&other.order)
}
}
impl PartialOrd for Path<ScaledPixels> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
#[derive(Clone, Debug)]
#[repr(C)]
pub(crate) struct PathVertex<P: Clone + Default + Debug> {
pub(crate) xy_position: Point<P>,
pub(crate) st_position: Point<f32>,
pub(crate) content_mask: ContentMask<P>,
}
impl PathVertex<Pixels> {
pub fn scale(&self, factor: f32) -> PathVertex<ScaledPixels> {
PathVertex {
xy_position: self.xy_position.scale(factor),
st_position: self.st_position,
content_mask: self.content_mask.scale(factor),
}
}
}
#[allow(non_camel_case_types, unused)]
pub(crate) type PathVertex_ScaledPixels = PathVertex<ScaledPixels>;
#[derive(Default, Copy, Clone, Debug, Eq, PartialEq, Hash)]
#[repr(C)]
pub(crate) struct ViewId {
low_bits: u32,
high_bits: u32,
}
impl From<EntityId> for ViewId {
fn from(value: EntityId) -> Self {
let value = value.as_u64();
Self {
low_bits: value as u32,
high_bits: (value >> 32) as u32,
}
}
}
impl From<ViewId> for EntityId {
fn from(value: ViewId) -> Self {
let value = (value.low_bits as u64) | ((value.high_bits as u64) << 32);
value.into()
}
}

View File

@@ -383,10 +383,12 @@ impl Style {
}
}
/// Paints the background of an element styled with this style.
/// Paints the background of an element styled with this style, then calls the continuation function, then paints the border.
pub fn paint(
&self,
bounds: Bounds<Pixels>,
hover: Option<Self>,
group_hover: Option<(SharedString, Option<Self>)>,
cx: &mut ElementContext,
continuation: impl FnOnce(&mut ElementContext),
) {
@@ -397,7 +399,7 @@ impl Style {
#[cfg(debug_assertions)]
if self.debug || cx.has_global::<DebugBelow>() {
cx.paint_quad(crate::outline(bounds, crate::red()));
cx.paint_quad(crate::outline(bounds, crate::red()), None, None);
}
let rem_size = cx.rem_size();
@@ -410,101 +412,229 @@ impl Style {
);
});
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 named_hover_group = group_hover
.as_ref()
.map(|(group_name, _)| group_name.clone());
cx.with_hover_group(named_hover_group, |cx| {
let background_color = self.background_color();
let hover_background_color = hover
.as_ref()
.map(|hover_style| hover_style.background_color())
.unwrap_or_default();
let group_hover_background_color = group_hover
.as_ref()
.and_then(|(_, group_hover_style)| {
Some(group_hover_style.as_ref()?.background_color())
})
.unwrap_or_default();
cx.with_z_index(2, |cx| {
continuation(cx);
});
if !background_color.is_transparent()
|| !hover_background_color.is_transparent()
|| !group_hover_background_color.is_transparent()
{
cx.with_z_index(1, |cx| {
let corner_radii = self.corner_radii.to_pixels(bounds.size, rem_size);
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 mut border_color = background_color;
border_color.a = 0.;
let base_quad = quad(
bounds,
corner_radii,
background_color,
Edges::default(),
border_color,
);
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 hover_quad = if hover_background_color.is_transparent() {
None
} else {
let mut border_color = hover_background_color;
border_color.a = 0.;
Some(quad(
bounds,
corner_radii,
hover_background_color,
Edges::default(),
border_color,
))
};
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(),
);
let group_hover_quad = group_hover.as_ref().map(|(group_id, _)| {
let quad = if group_hover_background_color.is_transparent() {
None
} else {
let mut border_color = group_hover_background_color;
border_color.a = 0.;
Some(quad(
bounds,
corner_radii,
group_hover_background_color,
Edges::default(),
border_color,
))
};
(group_id.clone(), quad)
});
cx.with_content_mask(Some(ContentMask { bounds: top_bounds }), |cx| {
cx.paint_quad(quad.clone());
cx.paint_quad(base_quad, hover_quad, group_hover_quad);
});
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);
},
);
});
}
}
#[cfg(debug_assertions)]
if self.debug_below {
cx.remove_global::<DebugBelow>();
cx.with_z_index(2, |cx| {
continuation(cx);
});
let border_color = self.border_color();
let hover_border_color = hover
.as_ref()
.map(|hover_style| hover_style.border_color())
.unwrap_or_default();
let group_hover_border_color = group_hover
.as_ref()
.and_then(|(_, group_hover_style)| Some(group_hover_style.as_ref()?.border_color()))
.unwrap_or_default();
if self.border_widths.any(|width| !width.is_zero())
&& (!border_color.is_transparent()
|| !hover_border_color.is_transparent()
|| !group_hover_border_color.is_transparent())
{
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 = border_color;
background.a = 0.;
let border_quad = quad(
bounds,
corner_radii,
background,
border_widths,
border_color,
);
let hover_border_quad = if hover_border_color.is_transparent() {
None
} else {
let mut background = hover_border_color;
background.a = 0.;
Some(quad(
bounds,
corner_radii,
background,
border_widths,
hover_border_color,
))
};
let group_hover_border_quad = group_hover.as_ref().map(|(group_id, _)| {
let quad = if group_hover_border_color.is_transparent() {
None
} else {
let mut background = group_hover_border_color;
background.a = 0.;
Some(quad(
bounds,
corner_radii,
background,
border_widths,
group_hover_border_color,
))
};
(group_id.clone(), quad)
});
cx.with_content_mask(Some(ContentMask { bounds: top_bounds }), |cx| {
cx.paint_quad(
border_quad.clone(),
hover_border_quad.clone(),
group_hover_border_quad.clone(),
);
});
cx.with_content_mask(
Some(ContentMask {
bounds: right_bounds,
}),
|cx| {
cx.paint_quad(
border_quad.clone(),
hover_border_quad.clone(),
group_hover_border_quad.clone(),
);
},
);
cx.with_content_mask(
Some(ContentMask {
bounds: bottom_bounds,
}),
|cx| {
cx.paint_quad(
border_quad.clone(),
hover_border_quad.clone(),
group_hover_border_quad.clone(),
);
},
);
cx.with_content_mask(
Some(ContentMask {
bounds: left_bounds,
}),
|cx| {
cx.paint_quad(border_quad, hover_border_quad, group_hover_border_quad);
},
);
});
}
#[cfg(debug_assertions)]
if self.debug_below {
cx.remove_global::<DebugBelow>();
}
})
}
/// Returns the background color of the style based on the visibility.
/// If the visibility is `Visible`, it returns the background color of the style if set,
/// otherwise it returns the default color. If the visibility is `Hidden`, it returns
/// a transparent black color.
fn background_color(&self) -> Hsla {
match self.visibility {
Visibility::Visible => self
.background
.as_ref()
.and_then(Fill::color)
.unwrap_or_default(),
Visibility::Hidden => Hsla::transparent_black(),
}
}
fn is_border_visible(&self) -> bool {
self.border_color
.map_or(false, |color| !color.is_transparent())
&& self.border_widths.any(|length| !length.is_zero())
/// Returns the border color of the style based on the visibility.
/// If the visibility is `Visible`, it returns the border color of the style if set,
/// otherwise it returns the default color. If the visibility is `Hidden`, it returns
/// a transparent black color.
fn border_color(&self) -> Hsla {
match self.visibility {
Visibility::Visible => self.border_color.unwrap_or_default(),
Visibility::Hidden => Hsla::transparent_black(),
}
}
}

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