Compare commits

..

53 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
147 changed files with 4597 additions and 3640 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/"

View File

@@ -120,12 +120,6 @@ jobs:
export ZED_DO_CERTIFICATE_ID=$(doctl compute certificate list --format ID --no-header)
export ZED_IMAGE_ID="registry.digitalocean.com/zed/collab:${GITHUB_SHA}"
export ZED_SERVICE_NAME=collab
envsubst < crates/collab/k8s/collab.template.yml | kubectl apply -f -
kubectl -n "$ZED_KUBE_NAMESPACE" rollout status deployment/$ZED_SERVICE_NAME --watch
echo "deployed ${ZED_SERVICE_NAME} to ${ZED_KUBE_NAMESPACE}"
export ZED_SERVICE_NAME=api
envsubst < crates/collab/k8s/collab.template.yml | kubectl apply -f -
kubectl -n "$ZED_KUBE_NAMESPACE" rollout status deployment/$ZED_SERVICE_NAME --watch
echo "deployed ${ZED_SERVICE_NAME} to ${ZED_KUBE_NAMESPACE}"
kubectl -n "$ZED_KUBE_NAMESPACE" rollout status deployment/collab --watch
echo "deployed collab.template.yml to ${ZED_KUBE_NAMESPACE}"

150
Cargo.lock generated
View File

@@ -360,7 +360,6 @@ dependencies = [
"serde_json",
"settings",
"smol",
"telemetry_events",
"theme",
"tiktoken-rs",
"ui",
@@ -771,10 +770,12 @@ dependencies = [
"anyhow",
"client",
"db",
"editor",
"gpui",
"isahc",
"lazy_static",
"log",
"markdown_preview",
"menu",
"project",
"release_channel",
@@ -1188,7 +1189,7 @@ dependencies = [
"tokio",
"tokio-tungstenite",
"tower",
"tower-http 0.3.5",
"tower-http",
"tower-layer",
"tower-service",
]
@@ -1225,7 +1226,7 @@ dependencies = [
"serde_json",
"tokio",
"tower",
"tower-http 0.3.5",
"tower-http",
"tower-layer",
"tower-service",
]
@@ -1902,49 +1903,6 @@ dependencies = [
"util",
]
[[package]]
name = "clickhouse"
version = "0.11.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0875e527e299fc5f4faba42870bf199a39ab0bb2dbba1b8aef0a2151451130f"
dependencies = [
"bstr",
"bytes 1.5.0",
"clickhouse-derive",
"clickhouse-rs-cityhash-sys",
"futures 0.3.28",
"hyper",
"hyper-tls",
"lz4",
"sealed",
"serde",
"static_assertions",
"thiserror",
"tokio",
"url",
]
[[package]]
name = "clickhouse-derive"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18af5425854858c507eec70f7deb4d5d8cec4216fcb086283a78872387281ea5"
dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn 1.0.109",
]
[[package]]
name = "clickhouse-rs-cityhash-sys"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4baf9d4700a28d6cb600e17ed6ae2b43298a5245f1f76b4eab63027ebfd592b9"
dependencies = [
"cc",
]
[[package]]
name = "client"
version = "0.1.0"
@@ -1977,7 +1935,6 @@ dependencies = [
"smol",
"sum_tree",
"sysinfo",
"telemetry_events",
"tempfile",
"text",
"thiserror",
@@ -2063,7 +2020,6 @@ dependencies = [
"channel",
"chrono",
"clap 3.2.25",
"clickhouse",
"client",
"clock",
"collab_ui",
@@ -2078,7 +2034,6 @@ dependencies = [
"futures 0.3.28",
"git",
"gpui",
"hex",
"hyper",
"indoc",
"language",
@@ -2109,10 +2064,8 @@ dependencies = [
"serde_json",
"settings",
"sha-1 0.9.8",
"sha2 0.10.7",
"smallvec",
"sqlx",
"telemetry_events",
"text",
"theme",
"time",
@@ -2121,7 +2074,6 @@ dependencies = [
"toml 0.8.10",
"tonic",
"tower",
"tower-http 0.4.4",
"tracing",
"tracing-log",
"tracing-subscriber",
@@ -2144,6 +2096,7 @@ dependencies = [
"collections",
"db",
"editor",
"extensions_ui",
"feature_flags",
"feedback",
"futures 0.3.28",
@@ -3728,6 +3681,7 @@ dependencies = [
"text",
"time",
"util",
"windows-sys 0.52.0",
]
[[package]]
@@ -4171,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",
@@ -5305,26 +5260,6 @@ dependencies = [
"url",
]
[[package]]
name = "lz4"
version = "1.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e9e2dd86df36ce760a60f6ff6ad526f7ba1f14ba0356f8254fb6905e6494df1"
dependencies = [
"libc",
"lz4-sys",
]
[[package]]
name = "lz4-sys"
version = "1.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57d27b317e207b10f69f5e75494119e391a96f48861ae870d1da6edac98ca900"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "mac"
version = "0.1.1"
@@ -6933,6 +6868,25 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "profiling"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58"
dependencies = [
"profiling-procmacros",
]
[[package]]
name = "profiling-procmacros"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd"
dependencies = [
"quote",
"syn 2.0.48",
]
[[package]]
name = "project"
version = "0.1.0"
@@ -7390,6 +7344,7 @@ dependencies = [
"fuzzy",
"gpui",
"language",
"menu",
"ordered-float 2.10.0",
"picker",
"postage",
@@ -8246,18 +8201,6 @@ version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "sealed"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b5e421024b5e5edfbaa8e60ecf90bda9dbffc602dbb230e6028763f85f0c68c"
dependencies = [
"heck 0.3.3",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "search"
version = "0.1.0"
@@ -8605,9 +8548,9 @@ dependencies = [
[[package]]
name = "shlex"
version = "1.3.0"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380"
[[package]]
name = "signal-hook"
@@ -9457,14 +9400,6 @@ dependencies = [
"workspace",
]
[[package]]
name = "telemetry_events"
version = "0.1.0"
dependencies = [
"serde",
"util",
]
[[package]]
name = "tempfile"
version = "3.9.0"
@@ -10053,25 +9988,6 @@ dependencies = [
"tower-service",
]
[[package]]
name = "tower-http"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140"
dependencies = [
"bitflags 2.4.1",
"bytes 1.5.0",
"futures-core",
"futures-util",
"http 0.2.9",
"http-body",
"http-range-header",
"pin-project-lite 0.2.13",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-layer"
version = "0.3.2"
@@ -10487,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",
@@ -11013,6 +10929,7 @@ dependencies = [
"project",
"regex",
"release_channel",
"schemars",
"search",
"serde",
"serde_derive",
@@ -12038,7 +11955,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.124.6"
version = "0.125.0"
dependencies = [
"activity_indicator",
"ai",
@@ -12102,6 +12019,7 @@ dependencies = [
"outline",
"parking_lot 0.11.2",
"postage",
"profiling",
"project",
"project_panel",
"project_symbols",

View File

@@ -80,7 +80,6 @@ members = [
"crates/theme",
"crates/theme_importer",
"crates/theme_selector",
"crates/telemetry_events",
"crates/ui",
"crates/util",
"crates/vcs_menu",
@@ -173,7 +172,6 @@ text = { path = "crates/text" }
theme = { path = "crates/theme" }
theme_importer = { path = "crates/theme_importer" }
theme_selector = { path = "crates/theme_selector" }
telemetry_events = { path ="crates/telemetry_events" }
ui = { path = "crates/ui" }
util = { path = "crates/util" }
vcs_menu = { path = "crates/vcs_menu" }
@@ -191,14 +189,12 @@ blade-graphics = { git = "https://github.com/kvark/blade", rev = "e9d93a4d41f394
blade-macros = { git = "https://github.com/kvark/blade", rev = "e9d93a4d41f3946a03ffb76136290d6ccf7f2b80" }
blade-rwh = { package = "raw-window-handle", version = "0.5" }
chrono = { version = "0.4", features = ["serde"] }
clickhouse = { version = "0.11.6" }
ctor = "0.2.6"
derive_more = "0.99.17"
env_logger = "0.9"
futures = "0.3"
git2 = { version = "0.15", default-features = false }
globset = "0.4"
hex = "0.4.3"
indoc = "1"
# We explicitly disable a http2 support in isahc.
isahc = { version = "1.7.2", default-features = false, features = ["static-curl", "text-decoding"] }
@@ -207,6 +203,7 @@ linkify = "0.10.0"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
ordered-float = "2.1.1"
parking_lot = "0.11.1"
profiling = "1"
postage = { version = "0.5", features = ["futures-traits"] }
pretty_assertions = "1.3.0"
prost = "0.8"
@@ -223,7 +220,6 @@ serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
serde_json_lenient = { version = "0.1", features = ["preserve_order", "raw_value"] }
serde_repr = "0.1"
sha2 = "0.10"
smallvec = { version = "1.6", features = ["union"] }
smol = "1.2"
strum = { version = "0.25.0", features = ["derive"] }
@@ -233,7 +229,6 @@ thiserror = "1.0.29"
tiktoken-rs = "0.5.7"
time = { version = "0.3", features = ["serde", "serde-well-known", "formatting"] }
toml = "0.8"
tower-http = "0.4.4"
tree-sitter = { version = "0.20", features = ["wasm"] }
tree-sitter-astro = { git = "https://github.com/virchau13/tree-sitter-astro.git", rev = "e924787e12e8a03194f36a113290ac11d6dc10f3" }
tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "7331995b19b8f8aba2d5e26deb51d2195c18bc94" }
@@ -267,7 +262,7 @@ tree-sitter-ocaml = { git = "https://github.com/tree-sitter/tree-sitter-ocaml",
tree-sitter-php = "0.21.1"
tree-sitter-prisma-io = { git = "https://github.com/victorhqc/tree-sitter-prisma" }
tree-sitter-proto = { git = "https://github.com/rewinfrey/tree-sitter-proto", rev = "36d54f288aee112f13a67b550ad32634d0c2cb52" }
tree-sitter-purescript = { git = "https://github.com/ivanmoreau/tree-sitter-purescript", rev = "a37140f0c7034977b90faa73c94fcb8a5e45ed08" }
tree-sitter-purescript = { git = "https://github.com/postsolar/tree-sitter-purescript", rev = "v0.1.0" }
tree-sitter-python = "0.20.2"
tree-sitter-racket = { git = "https://github.com/zed-industries/tree-sitter-racket", rev = "eb010cf2c674c6fd9a6316a84e28ef90190fe51a" }
tree-sitter-ruby = "0.20.0"

View File

@@ -1,3 +1,3 @@
collab: RUST_LOG=${RUST_LOG:-warn,tower_http=info,collab=info} cargo run --package=collab serve
collab: RUST_LOG=${RUST_LOG:-warn,collab=info} cargo run --package=collab serve
livekit: livekit-server --dev
blob_store: MINIO_ROOT_USER=the-blob-store-access-key MINIO_ROOT_PASSWORD=the-blob-store-secret-key minio server .blob_store

View File

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

View File

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

View File

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

View File

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

View File

@@ -36,7 +36,6 @@ serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smol.workspace = true
telemetry_events.workspace = true
theme.workspace = true
tiktoken-rs.workspace = true
ui.workspace = true

View File

@@ -15,6 +15,7 @@ use ai::{
};
use anyhow::{anyhow, Result};
use chrono::{DateTime, Local};
use client::telemetry::AssistantKind;
use collections::{hash_map, HashMap, HashSet, VecDeque};
use editor::{
actions::{MoveDown, MoveUp},
@@ -51,7 +52,6 @@ use std::{
sync::Arc,
time::{Duration, Instant},
};
use telemetry_events::AssistantKind;
use theme::ThemeSettings;
use ui::{
prelude::*,
@@ -362,7 +362,7 @@ impl AssistantPanel {
move |cx: &mut BlockContext| {
measurements.set(BlockMeasurements {
anchor_x: cx.anchor_x,
gutter_width: cx.gutter_width,
gutter_width: cx.gutter_dimensions.width,
});
inline_assistant.clone().into_any_element()
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -41,10 +41,9 @@ schemars.workspace = true
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
sha2.workspace = true
sha2 = "0.10"
smol.workspace = true
sysinfo.workspace = true
telemetry_events.workspace = true
tempfile.workspace = true
thiserror.workspace = true
time.workspace = true

View File

@@ -46,7 +46,7 @@ use util::http::{HttpClient, ZedHttpClient};
use util::{ResultExt, TryFutureExt};
pub use rpc::*;
pub use telemetry_events::Event;
pub use telemetry::Event;
pub use user::*;
lazy_static! {

View File

@@ -8,6 +8,7 @@ use gpui::{AppContext, AppMetadata, BackgroundExecutor, Task};
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use release_channel::ReleaseChannel;
use serde::Serialize;
use settings::{Settings, SettingsStore};
use sha2::{Digest, Sha256};
use std::io::Write;
@@ -15,10 +16,6 @@ use std::{env, mem, path::PathBuf, sync::Arc, time::Duration};
use sysinfo::{
CpuRefreshKind, Pid, PidExt, ProcessExt, ProcessRefreshKind, RefreshKind, System, SystemExt,
};
use telemetry_events::{
ActionEvent, AppEvent, AssistantEvent, AssistantKind, CallEvent, CopilotEvent, CpuEvent,
EditEvent, EditorEvent, Event, EventRequestBody, EventWrapper, MemoryEvent, SettingEvent,
};
use tempfile::NamedTempFile;
use util::http::{self, HttpClient, Method, ZedHttpClient};
#[cfg(not(debug_assertions))]
@@ -38,7 +35,7 @@ struct TelemetryState {
settings: TelemetrySettings,
metrics_id: Option<Arc<str>>, // Per logged-in user
installation_id: Option<Arc<str>>, // Per app installation (different for dev, nightly, preview, and stable)
session_id: Option<String>, // Per app launch
session_id: Option<Arc<str>>, // Per app launch
release_channel: Option<&'static str>,
app_metadata: AppMetadata,
architecture: &'static str,
@@ -51,6 +48,93 @@ struct TelemetryState {
max_queue_size: usize,
}
#[derive(Serialize, Debug)]
struct EventRequestBody {
installation_id: Option<Arc<str>>,
session_id: Option<Arc<str>>,
is_staff: Option<bool>,
app_version: Option<String>,
os_name: &'static str,
os_version: Option<String>,
architecture: &'static str,
release_channel: Option<&'static str>,
events: Vec<EventWrapper>,
}
#[derive(Serialize, Debug)]
struct EventWrapper {
signed_in: bool,
#[serde(flatten)]
event: Event,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum AssistantKind {
Panel,
Inline,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(tag = "type")]
pub enum Event {
Editor {
operation: &'static str,
file_extension: Option<String>,
vim_mode: bool,
copilot_enabled: bool,
copilot_enabled_for_language: bool,
milliseconds_since_first_event: i64,
},
Copilot {
suggestion_id: Option<String>,
suggestion_accepted: bool,
file_extension: Option<String>,
milliseconds_since_first_event: i64,
},
Call {
operation: &'static str,
room_id: Option<u64>,
channel_id: Option<u64>,
milliseconds_since_first_event: i64,
},
Assistant {
conversation_id: Option<String>,
kind: AssistantKind,
model: &'static str,
milliseconds_since_first_event: i64,
},
Cpu {
usage_as_percentage: f32,
core_count: u32,
milliseconds_since_first_event: i64,
},
Memory {
memory_in_bytes: u64,
virtual_memory_in_bytes: u64,
milliseconds_since_first_event: i64,
},
App {
operation: String,
milliseconds_since_first_event: i64,
},
Setting {
setting: &'static str,
value: String,
milliseconds_since_first_event: i64,
},
Edit {
duration: i64,
environment: &'static str,
milliseconds_since_first_event: i64,
},
Action {
source: &'static str,
action: String,
milliseconds_since_first_event: i64,
},
}
#[cfg(debug_assertions)]
const MAX_QUEUE_LEN: usize = 5;
@@ -62,6 +146,7 @@ const FLUSH_INTERVAL: Duration = Duration::from_secs(1);
#[cfg(not(debug_assertions))]
const FLUSH_INTERVAL: Duration = Duration::from_secs(60 * 5);
static ZED_CLIENT_CHECKSUM_SEED: Lazy<Option<Vec<u8>>> = Lazy::new(|| {
option_env!("ZED_CLIENT_CHECKSUM_SEED")
.map(|s| s.as_bytes().into())
@@ -96,7 +181,7 @@ impl Telemetry {
log_file: None,
is_staff: None,
first_event_date_time: None,
event_coalescer: EventCoalescer::new(clock.clone()),
event_coalescer: EventCoalescer::new(),
max_queue_size: MAX_QUEUE_LEN,
}));
@@ -233,13 +318,15 @@ impl Telemetry {
copilot_enabled: bool,
copilot_enabled_for_language: bool,
) {
let event = Event::Editor(EditorEvent {
let event = Event::Editor {
file_extension,
vim_mode,
operation: operation.into(),
operation,
copilot_enabled,
copilot_enabled_for_language,
});
milliseconds_since_first_event: self
.milliseconds_since_first_event(self.clock.utc_now()),
};
self.report_event(event)
}
@@ -250,11 +337,13 @@ impl Telemetry {
suggestion_accepted: bool,
file_extension: Option<String>,
) {
let event = Event::Copilot(CopilotEvent {
let event = Event::Copilot {
suggestion_id,
suggestion_accepted,
file_extension,
});
milliseconds_since_first_event: self
.milliseconds_since_first_event(self.clock.utc_now()),
};
self.report_event(event)
}
@@ -265,11 +354,13 @@ impl Telemetry {
kind: AssistantKind,
model: &'static str,
) {
let event = Event::Assistant(AssistantEvent {
let event = Event::Assistant {
conversation_id,
kind,
model: model.to_string(),
});
model,
milliseconds_since_first_event: self
.milliseconds_since_first_event(self.clock.utc_now()),
};
self.report_event(event)
}
@@ -280,20 +371,24 @@ impl Telemetry {
room_id: Option<u64>,
channel_id: Option<u64>,
) {
let event = Event::Call(CallEvent {
operation: operation.to_string(),
let event = Event::Call {
operation,
room_id,
channel_id,
});
milliseconds_since_first_event: self
.milliseconds_since_first_event(self.clock.utc_now()),
};
self.report_event(event)
}
pub fn report_cpu_event(self: &Arc<Self>, usage_as_percentage: f32, core_count: u32) {
let event = Event::Cpu(CpuEvent {
let event = Event::Cpu {
usage_as_percentage,
core_count,
});
milliseconds_since_first_event: self
.milliseconds_since_first_event(self.clock.utc_now()),
};
self.report_event(event)
}
@@ -303,16 +398,22 @@ impl Telemetry {
memory_in_bytes: u64,
virtual_memory_in_bytes: u64,
) {
let event = Event::Memory(MemoryEvent {
let event = Event::Memory {
memory_in_bytes,
virtual_memory_in_bytes,
});
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) -> Event {
let event = Event::App(AppEvent { operation });
let event = Event::App {
operation,
milliseconds_since_first_event: self
.milliseconds_since_first_event(self.clock.utc_now()),
};
self.report_event(event.clone());
@@ -320,10 +421,12 @@ impl Telemetry {
}
pub fn report_setting_event(self: &Arc<Self>, setting: &'static str, value: String) {
let event = Event::Setting(SettingEvent {
setting: setting.to_string(),
let event = Event::Setting {
setting,
value,
});
milliseconds_since_first_event: self
.milliseconds_since_first_event(self.clock.utc_now()),
};
self.report_event(event)
}
@@ -334,24 +437,42 @@ impl Telemetry {
drop(state);
if let Some((start, end, environment)) = period_data {
let event = Event::Edit(EditEvent {
let event = Event::Edit {
duration: end.timestamp_millis() - start.timestamp_millis(),
environment: environment.to_string(),
});
environment,
milliseconds_since_first_event: self
.milliseconds_since_first_event(self.clock.utc_now()),
};
self.report_event(event);
}
}
pub fn report_action_event(self: &Arc<Self>, source: &'static str, action: String) {
let event = Event::Action(ActionEvent {
source: source.to_string(),
let event = Event::Action {
source,
action,
});
milliseconds_since_first_event: self
.milliseconds_since_first_event(self.clock.utc_now()),
};
self.report_event(event)
}
fn milliseconds_since_first_event(self: &Arc<Self>, date_time: DateTime<Utc>) -> i64 {
let mut state = self.state.lock();
match state.first_event_date_time {
Some(first_event_date_time) => {
date_time.timestamp_millis() - first_event_date_time.timestamp_millis()
}
None => {
state.first_event_date_time = Some(date_time);
0
}
}
}
fn report_event(self: &Arc<Self>, event: Event) {
let mut state = self.state.lock();
@@ -368,24 +489,8 @@ impl Telemetry {
}));
}
let date_time = self.clock.utc_now();
let milliseconds_since_first_event = match state.first_event_date_time {
Some(first_event_date_time) => {
date_time.timestamp_millis() - first_event_date_time.timestamp_millis()
}
None => {
state.first_event_date_time = Some(date_time);
0
}
};
let signed_in = state.metrics_id.is_some();
state.events_queue.push(EventWrapper {
signed_in,
milliseconds_since_first_event,
event,
});
state.events_queue.push(EventWrapper { signed_in, event });
if state.installation_id.is_some() {
if state.events_queue.len() >= state.max_queue_size {
@@ -440,22 +545,21 @@ impl Telemetry {
{
let state = this.state.lock();
let request_body = EventRequestBody {
installation_id: state.installation_id.as_deref().map(Into::into),
installation_id: state.installation_id.clone(),
session_id: state.session_id.clone(),
is_staff: state.is_staff.clone(),
app_version: state
.app_metadata
.app_version
.unwrap_or_default()
.to_string(),
os_name: state.app_metadata.os_name.to_string(),
.map(|version| version.to_string()),
os_name: state.app_metadata.os_name,
os_version: state
.app_metadata
.os_version
.map(|version| version.to_string()),
architecture: state.architecture.to_string(),
architecture: state.architecture,
release_channel: state.release_channel.map(Into::into),
release_channel: state.release_channel,
events,
};
json_bytes.clear();
@@ -474,7 +578,7 @@ impl Telemetry {
let request = http::Request::builder()
.method(Method::POST)
.uri(this.http_client.zed_api_url("/telemetry/events"))
.uri(&this.http_client.zed_url("/api/events"))
.header("Content-Type", "text/plain")
.header("x-zed-checksum", checksum)
.body(json_bytes.into());
@@ -523,9 +627,10 @@ mod tests {
let event = telemetry.report_app_event(operation.clone());
assert_eq!(
event,
Event::App(AppEvent {
Event::App {
operation: operation.clone(),
})
milliseconds_since_first_event: 0
}
);
assert_eq!(telemetry.state.lock().events_queue.len(), 1);
assert!(telemetry.state.lock().flush_events_task.is_some());
@@ -539,9 +644,10 @@ mod tests {
let event = telemetry.report_app_event(operation.clone());
assert_eq!(
event,
Event::App(AppEvent {
Event::App {
operation: operation.clone(),
})
milliseconds_since_first_event: 100
}
);
assert_eq!(telemetry.state.lock().events_queue.len(), 2);
assert!(telemetry.state.lock().flush_events_task.is_some());
@@ -555,9 +661,10 @@ mod tests {
let event = telemetry.report_app_event(operation.clone());
assert_eq!(
event,
Event::App(AppEvent {
Event::App {
operation: operation.clone(),
})
milliseconds_since_first_event: 200
}
);
assert_eq!(telemetry.state.lock().events_queue.len(), 3);
assert!(telemetry.state.lock().flush_events_task.is_some());
@@ -572,9 +679,10 @@ mod tests {
let event = telemetry.report_app_event(operation.clone());
assert_eq!(
event,
Event::App(AppEvent {
Event::App {
operation: operation.clone(),
})
milliseconds_since_first_event: 300
}
);
assert!(is_empty_state(&telemetry));
@@ -604,9 +712,10 @@ mod tests {
let event = telemetry.report_app_event(operation.clone());
assert_eq!(
event,
Event::App(AppEvent {
Event::App {
operation: operation.clone(),
})
milliseconds_since_first_event: 0
}
);
assert_eq!(telemetry.state.lock().events_queue.len(), 1);
assert!(telemetry.state.lock().flush_events_task.is_some());

View File

@@ -1,8 +1,5 @@
use std::sync::Arc;
use std::time;
use chrono::{DateTime, Duration, Utc};
use clock::SystemClock;
use std::time;
const COALESCE_TIMEOUT: time::Duration = time::Duration::from_secs(20);
const SIMULATED_DURATION_FOR_SINGLE_EVENT: time::Duration = time::Duration::from_millis(1);
@@ -15,20 +12,30 @@ struct PeriodData {
}
pub struct EventCoalescer {
clock: Arc<dyn SystemClock>,
state: Option<PeriodData>,
}
impl EventCoalescer {
pub fn new(clock: Arc<dyn SystemClock>) -> Self {
Self { clock, state: None }
pub fn new() -> Self {
Self { state: None }
}
pub fn log_event(
&mut self,
environment: &'static str,
) -> Option<(DateTime<Utc>, DateTime<Utc>, &'static str)> {
let log_time = self.clock.utc_now();
self.log_event_with_time(Utc::now(), environment)
}
// pub fn close_current_period(&mut self) -> Option<(DateTime<Utc>, DateTime<Utc>)> {
// self.environment.map(|env| self.log_event(env)).flatten()
// }
fn log_event_with_time(
&mut self,
log_time: DateTime<Utc>,
environment: &'static str,
) -> Option<(DateTime<Utc>, DateTime<Utc>, &'static str)> {
let coalesce_timeout = Duration::from_std(COALESCE_TIMEOUT).unwrap();
let Some(state) = &mut self.state else {
@@ -71,22 +78,18 @@ impl EventCoalescer {
#[cfg(test)]
mod tests {
use chrono::TimeZone;
use clock::FakeSystemClock;
use super::*;
#[test]
fn test_same_context_exceeding_timeout() {
let clock = Arc::new(FakeSystemClock::new(
Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(),
));
let environment_1 = "environment_1";
let mut event_coalescer = EventCoalescer::new(clock.clone());
let mut event_coalescer = EventCoalescer::new();
assert_eq!(event_coalescer.state, None);
let period_start = clock.utc_now();
let period_data = event_coalescer.log_event(environment_1);
let period_start = Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap();
let period_data = event_coalescer.log_event_with_time(period_start, environment_1);
assert_eq!(period_data, None);
assert_eq!(
@@ -99,12 +102,12 @@ mod tests {
);
let within_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT / 2).unwrap();
let mut period_end = period_start;
// Ensure that many calls within the timeout don't start a new period
for _ in 0..100 {
clock.advance(within_timeout_adjustment);
let period_data = event_coalescer.log_event(environment_1);
let period_end = clock.utc_now();
period_end += within_timeout_adjustment;
let period_data = event_coalescer.log_event_with_time(period_end, environment_1);
assert_eq!(period_data, None);
assert_eq!(
@@ -117,12 +120,10 @@ mod tests {
);
}
let period_end = clock.utc_now();
let exceed_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT * 2).unwrap();
// Logging an event exceeding the timeout should start a new period
clock.advance(exceed_timeout_adjustment);
let new_period_start = clock.utc_now();
let period_data = event_coalescer.log_event(environment_1);
let new_period_start = period_end + exceed_timeout_adjustment;
let period_data = event_coalescer.log_event_with_time(new_period_start, environment_1);
assert_eq!(period_data, Some((period_start, period_end, environment_1)));
assert_eq!(
@@ -137,16 +138,13 @@ mod tests {
#[test]
fn test_different_environment_under_timeout() {
let clock = Arc::new(FakeSystemClock::new(
Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(),
));
let environment_1 = "environment_1";
let mut event_coalescer = EventCoalescer::new(clock.clone());
let mut event_coalescer = EventCoalescer::new();
assert_eq!(event_coalescer.state, None);
let period_start = clock.utc_now();
let period_data = event_coalescer.log_event(environment_1);
let period_start = Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap();
let period_data = event_coalescer.log_event_with_time(period_start, environment_1);
assert_eq!(period_data, None);
assert_eq!(
@@ -159,9 +157,8 @@ mod tests {
);
let within_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT / 2).unwrap();
clock.advance(within_timeout_adjustment);
let period_end = clock.utc_now();
let period_data = event_coalescer.log_event(environment_1);
let period_end = period_start + within_timeout_adjustment;
let period_data = event_coalescer.log_event_with_time(period_end, environment_1);
assert_eq!(period_data, None);
assert_eq!(
@@ -173,12 +170,10 @@ mod tests {
})
);
clock.advance(within_timeout_adjustment);
// Logging an event within the timeout but with a different environment should start a new period
let period_end = clock.utc_now();
let period_end = period_end + within_timeout_adjustment;
let environment_2 = "environment_2";
let period_data = event_coalescer.log_event(environment_2);
let period_data = event_coalescer.log_event_with_time(period_end, environment_2);
assert_eq!(period_data, Some((period_start, period_end, environment_1)));
assert_eq!(
@@ -193,16 +188,13 @@ mod tests {
#[test]
fn test_switching_environment_while_within_timeout() {
let clock = Arc::new(FakeSystemClock::new(
Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(),
));
let environment_1 = "environment_1";
let mut event_coalescer = EventCoalescer::new(clock.clone());
let mut event_coalescer = EventCoalescer::new();
assert_eq!(event_coalescer.state, None);
let period_start = clock.utc_now();
let period_data = event_coalescer.log_event(environment_1);
let period_start = Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap();
let period_data = event_coalescer.log_event_with_time(period_start, environment_1);
assert_eq!(period_data, None);
assert_eq!(
@@ -215,10 +207,9 @@ mod tests {
);
let within_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT / 2).unwrap();
clock.advance(within_timeout_adjustment);
let period_end = clock.utc_now();
let period_end = period_start + within_timeout_adjustment;
let environment_2 = "environment_2";
let period_data = event_coalescer.log_event(environment_2);
let period_data = event_coalescer.log_event_with_time(period_end, environment_2);
assert_eq!(period_data, Some((period_start, period_end, environment_1)));
assert_eq!(
@@ -230,26 +221,22 @@ mod tests {
})
);
}
// 0 20 40 60
// |-------------------|-------------------|-------------------|-------------------
// |--------|----------env change
// |-------------------
// |period_start |period_end
// |new_period_start
// // 0 20 40 60
// // |-------------------|-------------------|-------------------|-------------------
// // |--------|----------env change
// // |-------------------
// // |period_start |period_end
// // |new_period_start
#[test]
fn test_switching_environment_while_exceeding_timeout() {
let clock = Arc::new(FakeSystemClock::new(
Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(),
));
let environment_1 = "environment_1";
let mut event_coalescer = EventCoalescer::new(clock.clone());
let mut event_coalescer = EventCoalescer::new();
assert_eq!(event_coalescer.state, None);
let period_start = clock.utc_now();
let period_data = event_coalescer.log_event(environment_1);
let period_start = Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap();
let period_data = event_coalescer.log_event_with_time(period_start, environment_1);
assert_eq!(period_data, None);
assert_eq!(
@@ -262,10 +249,9 @@ mod tests {
);
let exceed_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT * 2).unwrap();
clock.advance(exceed_timeout_adjustment);
let period_end = clock.utc_now();
let period_end = period_start + exceed_timeout_adjustment;
let environment_2 = "environment_2";
let period_data = event_coalescer.log_event(environment_2);
let period_data = event_coalescer.log_event_with_time(period_end, environment_2);
assert_eq!(
period_data,
@@ -284,7 +270,6 @@ mod tests {
})
);
}
// 0 20 40 60
// |-------------------|-------------------|-------------------|-------------------
// |--------|----------------------------------------env change

View File

@@ -12,12 +12,6 @@ BLOB_STORE_SECRET_KEY = "the-blob-store-secret-key"
BLOB_STORE_BUCKET = "the-extensions-bucket"
BLOB_STORE_URL = "http://127.0.0.1:9000"
BLOB_STORE_REGION = "the-region"
ZED_CLIENT_CHECKSUM_SEED = "development-checksum-seed"
# CLICKHOUSE_URL = ""
# CLICKHOUSE_USER = "default"
# CLICKHOUSE_PASSWORD = ""
# CLICKHOUSE_DATABASE = "default"
# RUST_LOG=info
# LOG_JSON=true

View File

@@ -25,12 +25,10 @@ base64 = "0.13"
chrono.workspace = true
clap = { version = "3.1", features = ["derive"], optional = true }
clock.workspace = true
clickhouse.workspace = true
collections.workspace = true
dashmap = "5.4"
envy = "0.4.2"
futures.workspace = true
hex.workspace = true
hyper = "0.14"
lazy_static.workspace = true
lipsum = { version = "0.8", optional = true }
@@ -50,10 +48,8 @@ serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
sha-1 = "0.9"
sha2.workspace = true
smallvec.workspace = true
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid", "any"] }
telemetry_events.workspace = true
text.workspace = true
time.workspace = true
tokio = { version = "1", features = ["full"] }
@@ -61,7 +57,6 @@ tokio-tungstenite = "0.17"
toml.workspace = true
tonic = "0.6"
tower = "0.4"
tower-http = { workspace = true, features = ["trace"] }
tracing = "0.1.34"
tracing-log = "0.1.3"
tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] }

View File

@@ -9,7 +9,7 @@ kind: Service
apiVersion: v1
metadata:
namespace: ${ZED_KUBE_NAMESPACE}
name: ${ZED_SERVICE_NAME}
name: collab
annotations:
service.beta.kubernetes.io/do-loadbalancer-tls-ports: "443"
service.beta.kubernetes.io/do-loadbalancer-certificate-id: ${ZED_DO_CERTIFICATE_ID}
@@ -17,7 +17,7 @@ metadata:
spec:
type: LoadBalancer
selector:
app: ${ZED_SERVICE_NAME}
app: collab
ports:
- name: web
protocol: TCP
@@ -29,17 +29,17 @@ apiVersion: apps/v1
kind: Deployment
metadata:
namespace: ${ZED_KUBE_NAMESPACE}
name: ${ZED_SERVICE_NAME}
name: collab
spec:
replicas: 1
selector:
matchLabels:
app: ${ZED_SERVICE_NAME}
app: collab
template:
metadata:
labels:
app: ${ZED_SERVICE_NAME}
app: collab
annotations:
ad.datadoghq.com/collab.check_names: |
["openmetrics"]
@@ -55,11 +55,10 @@ spec:
]
spec:
containers:
- name: ${ZED_SERVICE_NAME}
- name: collab
image: "${ZED_IMAGE_ID}"
args:
- serve
- ${ZED_SERVICE_NAME}
ports:
- containerPort: 8080
protocol: TCP
@@ -91,11 +90,6 @@ spec:
secretKeyRef:
name: api
key: token
- name: ZED_CLIENT_CHECKSUM_SEED
valueFrom:
secretKeyRef:
name: zed-client
key: checksum-seed
- name: LIVE_KIT_SERVER
valueFrom:
secretKeyRef:
@@ -136,26 +130,6 @@ spec:
secretKeyRef:
name: blob-store
key: bucket
- name: CLICKHOUSE_URL
valueFrom:
secretKeyRef:
name: clickhouse
key: url
- name: CLICKHOUSE_USER
valueFrom:
secretKeyRef:
name: clickhouse
key: user
- name: CLICKHOUSE_PASSWORD
valueFrom:
secretKeyRef:
name: clickhouse
key: password
- name: CLICKHOUSE_DATABASE
valueFrom:
secretKeyRef:
name: clickhouse
key: database
- name: INVITE_LINK_PREFIX
value: ${INVITE_LINK_PREFIX}
- name: RUST_BACKTRACE

View File

@@ -1,5 +1,4 @@
pub mod events;
pub mod extensions;
mod extensions;
use crate::{
auth,
@@ -25,7 +24,7 @@ use tracing::instrument;
pub use extensions::fetch_extensions_from_blob_store_periodically;
pub fn routes(rpc_server: Option<Arc<rpc::Server>>, state: Arc<AppState>) -> Router<Body> {
pub fn routes(rpc_server: Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body> {
Router::new()
.route("/user", get(get_authenticated_user))
.route("/users/:id/access_tokens", post(create_access_token))
@@ -33,6 +32,7 @@ pub fn routes(rpc_server: Option<Arc<rpc::Server>>, state: Arc<AppState>) -> Rou
.route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
.route("/contributors", get(get_contributors).post(add_contributor))
.route("/contributor", get(check_is_contributor))
.merge(extensions::router())
.layer(
ServiceBuilder::new()
.layer(Extension(state))
@@ -135,12 +135,8 @@ async fn trace_panic(panic: Json<Panic>) -> Result<()> {
}
async fn get_rpc_server_snapshot(
Extension(rpc_server): Extension<Option<Arc<rpc::Server>>>,
Extension(rpc_server): Extension<Arc<rpc::Server>>,
) -> Result<ErasedJson> {
let Some(rpc_server) = rpc_server else {
return Err(Error::Internal(anyhow!("rpc server is not available")));
};
Ok(ErasedJson::pretty(rpc_server.snapshot().await))
}

View File

@@ -1,805 +0,0 @@
use std::sync::Arc;
use anyhow::{anyhow, Context};
use axum::{
body::Bytes, headers::Header, http::HeaderName, routing::post, Extension, Router, TypedHeader,
};
use hyper::StatusCode;
use lazy_static::lazy_static;
use serde::{Serialize, Serializer};
use sha2::{Digest, Sha256};
use telemetry_events::{
ActionEvent, AppEvent, AssistantEvent, CallEvent, CopilotEvent, CpuEvent, EditEvent,
EditorEvent, Event, EventRequestBody, EventWrapper, MemoryEvent, SettingEvent,
};
use crate::{AppState, Error, Result};
pub fn router() -> Router {
Router::new().route("/telemetry/events", post(post_events))
}
lazy_static! {
static ref ZED_CHECKSUM_HEADER: HeaderName = HeaderName::from_static("x-zed-checksum");
static ref CLOUDFLARE_IP_COUNTRY_HEADER: HeaderName = HeaderName::from_static("cf-ipcountry");
}
pub struct ZedChecksumHeader(Vec<u8>);
impl Header for ZedChecksumHeader {
fn name() -> &'static HeaderName {
&ZED_CHECKSUM_HEADER
}
fn decode<'i, I>(values: &mut I) -> Result<Self, axum::headers::Error>
where
Self: Sized,
I: Iterator<Item = &'i axum::http::HeaderValue>,
{
let checksum = values
.next()
.ok_or_else(axum::headers::Error::invalid)?
.to_str()
.map_err(|_| axum::headers::Error::invalid())?;
let bytes = hex::decode(checksum).map_err(|_| axum::headers::Error::invalid())?;
Ok(Self(bytes))
}
fn encode<E: Extend<axum::http::HeaderValue>>(&self, _values: &mut E) {
unimplemented!()
}
}
pub struct CloudflareIpCountryHeader(String);
impl Header for CloudflareIpCountryHeader {
fn name() -> &'static HeaderName {
&CLOUDFLARE_IP_COUNTRY_HEADER
}
fn decode<'i, I>(values: &mut I) -> Result<Self, axum::headers::Error>
where
Self: Sized,
I: Iterator<Item = &'i axum::http::HeaderValue>,
{
let country_code = values
.next()
.ok_or_else(axum::headers::Error::invalid)?
.to_str()
.map_err(|_| axum::headers::Error::invalid())?;
Ok(Self(country_code.to_string()))
}
fn encode<E: Extend<axum::http::HeaderValue>>(&self, _values: &mut E) {
unimplemented!()
}
}
pub async fn post_events(
Extension(app): Extension<Arc<AppState>>,
TypedHeader(ZedChecksumHeader(checksum)): TypedHeader<ZedChecksumHeader>,
country_code_header: Option<TypedHeader<CloudflareIpCountryHeader>>,
body: Bytes,
) -> Result<()> {
let Some(clickhouse_client) = app.clickhouse_client.clone() else {
Err(Error::Http(
StatusCode::NOT_IMPLEMENTED,
"not supported".into(),
))?
};
let Some(checksum_seed) = app.config.zed_client_checksum_seed.as_ref() else {
return Err(Error::Http(
StatusCode::INTERNAL_SERVER_ERROR,
"events not enabled".into(),
))?;
};
let mut summer = Sha256::new();
summer.update(checksum_seed);
summer.update(&body);
summer.update(checksum_seed);
if &checksum[..] != &summer.finalize()[..] {
return Err(Error::Http(
StatusCode::BAD_REQUEST,
"invalid checksum".into(),
))?;
}
let request_body: telemetry_events::EventRequestBody =
serde_json::from_slice(&body).map_err(|err| {
log::error!("can't parse event json: {err}");
Error::Internal(anyhow!(err))
})?;
let mut to_upload = ToUpload::default();
let Some(last_event) = request_body.events.last() else {
return Err(Error::Http(StatusCode::BAD_REQUEST, "no events".into()))?;
};
let country_code = country_code_header.map(|h| h.0 .0);
let first_event_at = chrono::Utc::now()
- chrono::Duration::milliseconds(last_event.milliseconds_since_first_event);
for wrapper in &request_body.events {
match &wrapper.event {
Event::Editor(event) => to_upload.editor_events.push(EditorEventRow::from_event(
event.clone(),
&wrapper,
&request_body,
first_event_at,
country_code.clone(),
)),
Event::Copilot(event) => to_upload.copilot_events.push(CopilotEventRow::from_event(
event.clone(),
&wrapper,
&request_body,
first_event_at,
country_code.clone(),
)),
Event::Call(event) => to_upload.call_events.push(CallEventRow::from_event(
event.clone(),
&wrapper,
&request_body,
first_event_at,
)),
Event::Assistant(event) => {
to_upload
.assistant_events
.push(AssistantEventRow::from_event(
event.clone(),
&wrapper,
&request_body,
first_event_at,
))
}
Event::Cpu(event) => to_upload.cpu_events.push(CpuEventRow::from_event(
event.clone(),
&wrapper,
&request_body,
first_event_at,
)),
Event::Memory(event) => to_upload.memory_events.push(MemoryEventRow::from_event(
event.clone(),
&wrapper,
&request_body,
first_event_at,
)),
Event::App(event) => to_upload.app_events.push(AppEventRow::from_event(
event.clone(),
&wrapper,
&request_body,
first_event_at,
)),
Event::Setting(event) => to_upload.setting_events.push(SettingEventRow::from_event(
event.clone(),
&wrapper,
&request_body,
first_event_at,
)),
Event::Edit(event) => to_upload.edit_events.push(EditEventRow::from_event(
event.clone(),
&wrapper,
&request_body,
first_event_at,
)),
Event::Action(event) => to_upload.action_events.push(ActionEventRow::from_event(
event.clone(),
&wrapper,
&request_body,
first_event_at,
)),
}
}
to_upload
.upload(&clickhouse_client)
.await
.map_err(|err| Error::Internal(anyhow!(err)))?;
Ok(())
}
#[derive(Default)]
struct ToUpload {
editor_events: Vec<EditorEventRow>,
copilot_events: Vec<CopilotEventRow>,
assistant_events: Vec<AssistantEventRow>,
call_events: Vec<CallEventRow>,
cpu_events: Vec<CpuEventRow>,
memory_events: Vec<MemoryEventRow>,
app_events: Vec<AppEventRow>,
setting_events: Vec<SettingEventRow>,
edit_events: Vec<EditEventRow>,
action_events: Vec<ActionEventRow>,
}
impl ToUpload {
pub async fn upload(&self, clickhouse_client: &clickhouse::Client) -> anyhow::Result<()> {
Self::upload_to_table("editor_events", &self.editor_events, clickhouse_client)
.await
.with_context(|| format!("failed to upload to table 'editor_events'"))?;
Self::upload_to_table("copilot_events", &self.copilot_events, clickhouse_client)
.await
.with_context(|| format!("failed to upload to table 'copilot_events'"))?;
Self::upload_to_table(
"assistant_events",
&self.assistant_events,
clickhouse_client,
)
.await
.with_context(|| format!("failed to upload to table 'assistant_events'"))?;
Self::upload_to_table("call_events", &self.call_events, clickhouse_client)
.await
.with_context(|| format!("failed to upload to table 'call_events'"))?;
Self::upload_to_table("cpu_events", &self.cpu_events, clickhouse_client)
.await
.with_context(|| format!("failed to upload to table 'cpu_events'"))?;
Self::upload_to_table("memory_events", &self.memory_events, clickhouse_client)
.await
.with_context(|| format!("failed to upload to table 'memory_events'"))?;
Self::upload_to_table("app_events", &self.app_events, clickhouse_client)
.await
.with_context(|| format!("failed to upload to table 'app_events'"))?;
Self::upload_to_table("setting_events", &self.setting_events, clickhouse_client)
.await
.with_context(|| format!("failed to upload to table 'setting_events'"))?;
Self::upload_to_table("edit_events", &self.edit_events, clickhouse_client)
.await
.with_context(|| format!("failed to upload to table 'edit_events'"))?;
Self::upload_to_table("action_events", &self.action_events, clickhouse_client)
.await
.with_context(|| format!("failed to upload to table 'action_events'"))?;
Ok(())
}
async fn upload_to_table<T: clickhouse::Row + Serialize + std::fmt::Debug>(
table: &str,
rows: &[T],
clickhouse_client: &clickhouse::Client,
) -> anyhow::Result<()> {
if !rows.is_empty() {
let mut insert = clickhouse_client.insert(table)?;
for event in rows {
insert.write(event).await?;
}
insert.end().await?;
}
Ok(())
}
}
pub fn serialize_country_code<S>(country_code: &str, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if country_code.len() != 2 {
use serde::ser::Error;
return Err(S::Error::custom(
"country_code must be exactly 2 characters",
));
}
let country_code = country_code.as_bytes();
serializer.serialize_u16(((country_code[0] as u16) << 8) + country_code[1] as u16)
}
#[derive(Serialize, Debug, clickhouse::Row)]
pub struct EditorEventRow {
pub installation_id: String,
pub operation: String,
pub app_version: String,
pub file_extension: String,
pub os_name: String,
pub os_version: String,
pub release_channel: String,
pub signed_in: bool,
pub vim_mode: bool,
#[serde(serialize_with = "serialize_country_code")]
pub country_code: String,
pub region_code: String,
pub city: String,
pub time: i64,
pub copilot_enabled: bool,
pub copilot_enabled_for_language: bool,
pub historical_event: bool,
pub architecture: String,
pub is_staff: Option<bool>,
pub session_id: Option<String>,
pub major: Option<i32>,
pub minor: Option<i32>,
pub patch: Option<i32>,
}
impl EditorEventRow {
fn from_event(
event: EditorEvent,
wrapper: &EventWrapper,
body: &EventRequestBody,
first_event_at: chrono::DateTime<chrono::Utc>,
country_code: Option<String>,
) -> Self {
let semver = body.semver();
let time =
first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
Self {
app_version: body.app_version.clone(),
major: semver.map(|s| s.major as i32),
minor: semver.map(|s| s.minor as i32),
patch: semver.map(|s| s.patch as i32),
release_channel: body.release_channel.clone().unwrap_or_default(),
os_name: body.os_name.clone(),
os_version: body.os_version.clone().unwrap_or_default(),
architecture: body.architecture.clone(),
installation_id: body.installation_id.clone().unwrap_or_default(),
session_id: body.session_id.clone(),
is_staff: body.is_staff,
time: time.timestamp_millis(),
operation: event.operation,
file_extension: event.file_extension.unwrap_or_default(),
signed_in: wrapper.signed_in,
vim_mode: event.vim_mode,
copilot_enabled: event.copilot_enabled,
copilot_enabled_for_language: event.copilot_enabled_for_language,
country_code: country_code.unwrap_or("XX".to_string()),
region_code: "".to_string(),
city: "".to_string(),
historical_event: false,
}
}
}
#[derive(Serialize, Debug, clickhouse::Row)]
pub struct CopilotEventRow {
pub installation_id: String,
pub suggestion_id: String,
pub suggestion_accepted: bool,
pub app_version: String,
pub file_extension: String,
pub os_name: String,
pub os_version: String,
pub release_channel: String,
pub signed_in: bool,
#[serde(serialize_with = "serialize_country_code")]
pub country_code: String,
pub region_code: String,
pub city: String,
pub time: i64,
pub is_staff: Option<bool>,
pub session_id: Option<String>,
pub major: Option<i32>,
pub minor: Option<i32>,
pub patch: Option<i32>,
}
impl CopilotEventRow {
fn from_event(
event: CopilotEvent,
wrapper: &EventWrapper,
body: &EventRequestBody,
first_event_at: chrono::DateTime<chrono::Utc>,
country_code: Option<String>,
) -> Self {
let semver = body.semver();
let time =
first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
Self {
app_version: body.app_version.clone(),
major: semver.map(|s| s.major as i32),
minor: semver.map(|s| s.minor as i32),
patch: semver.map(|s| s.patch as i32),
release_channel: body.release_channel.clone().unwrap_or_default(),
os_name: body.os_name.clone(),
os_version: body.os_version.clone().unwrap_or_default(),
installation_id: body.installation_id.clone().unwrap_or_default(),
session_id: body.session_id.clone(),
is_staff: body.is_staff,
time: time.timestamp_millis(),
file_extension: event.file_extension.unwrap_or_default(),
signed_in: wrapper.signed_in,
country_code: country_code.unwrap_or("XX".to_string()),
region_code: "".to_string(),
city: "".to_string(),
suggestion_id: event.suggestion_id.unwrap_or_default(),
suggestion_accepted: event.suggestion_accepted,
}
}
}
#[derive(Serialize, Debug, clickhouse::Row)]
pub struct CallEventRow {
// AppInfoBase
app_version: String,
major: Option<i32>,
minor: Option<i32>,
patch: Option<i32>,
release_channel: String,
// ClientEventBase
installation_id: String,
session_id: Option<String>,
is_staff: Option<bool>,
time: i64,
// CallEventRow
operation: String,
room_id: Option<u64>,
channel_id: Option<u64>,
}
impl CallEventRow {
fn from_event(
event: CallEvent,
wrapper: &EventWrapper,
body: &EventRequestBody,
first_event_at: chrono::DateTime<chrono::Utc>,
) -> Self {
let semver = body.semver();
let time =
first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
Self {
app_version: body.app_version.clone(),
major: semver.map(|s| s.major as i32),
minor: semver.map(|s| s.minor as i32),
patch: semver.map(|s| s.patch as i32),
release_channel: body.release_channel.clone().unwrap_or_default(),
installation_id: body.installation_id.clone().unwrap_or_default(),
session_id: body.session_id.clone(),
is_staff: body.is_staff,
time: time.timestamp_millis(),
operation: event.operation,
room_id: event.room_id,
channel_id: event.channel_id,
}
}
}
#[derive(Serialize, Debug, clickhouse::Row)]
pub struct AssistantEventRow {
// AppInfoBase
app_version: String,
major: Option<i32>,
minor: Option<i32>,
patch: Option<i32>,
release_channel: String,
// ClientEventBase
installation_id: Option<String>,
session_id: Option<String>,
is_staff: Option<bool>,
time: i64,
// AssistantEventRow
conversation_id: String,
kind: String,
model: String,
}
impl AssistantEventRow {
fn from_event(
event: AssistantEvent,
wrapper: &EventWrapper,
body: &EventRequestBody,
first_event_at: chrono::DateTime<chrono::Utc>,
) -> Self {
let semver = body.semver();
let time =
first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
Self {
app_version: body.app_version.clone(),
major: semver.map(|s| s.major as i32),
minor: semver.map(|s| s.minor as i32),
patch: semver.map(|s| s.patch as i32),
release_channel: body.release_channel.clone().unwrap_or_default(),
installation_id: body.installation_id.clone(),
session_id: body.session_id.clone(),
is_staff: body.is_staff,
time: time.timestamp_millis(),
conversation_id: event.conversation_id.unwrap_or_default(),
kind: event.kind.to_string(),
model: event.model,
}
}
}
#[derive(Debug, clickhouse::Row, Serialize)]
pub struct CpuEventRow {
pub installation_id: Option<String>,
pub is_staff: Option<bool>,
pub usage_as_percentage: f32,
pub core_count: u32,
pub app_version: String,
pub release_channel: String,
pub time: i64,
pub session_id: Option<String>,
// pub normalized_cpu_usage: f64, MATERIALIZED
pub major: Option<i32>,
pub minor: Option<i32>,
pub patch: Option<i32>,
}
impl CpuEventRow {
fn from_event(
event: CpuEvent,
wrapper: &EventWrapper,
body: &EventRequestBody,
first_event_at: chrono::DateTime<chrono::Utc>,
) -> Self {
let semver = body.semver();
let time =
first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
Self {
app_version: body.app_version.clone(),
major: semver.map(|s| s.major as i32),
minor: semver.map(|s| s.minor as i32),
patch: semver.map(|s| s.patch as i32),
release_channel: body.release_channel.clone().unwrap_or_default(),
installation_id: body.installation_id.clone(),
session_id: body.session_id.clone(),
is_staff: body.is_staff,
time: time.timestamp_millis(),
usage_as_percentage: event.usage_as_percentage,
core_count: event.core_count,
}
}
}
#[derive(Serialize, Debug, clickhouse::Row)]
pub struct MemoryEventRow {
// AppInfoBase
app_version: String,
major: Option<i32>,
minor: Option<i32>,
patch: Option<i32>,
release_channel: String,
// ClientEventBase
installation_id: Option<String>,
session_id: Option<String>,
is_staff: Option<bool>,
time: i64,
// MemoryEventRow
memory_in_bytes: u64,
virtual_memory_in_bytes: u64,
}
impl MemoryEventRow {
fn from_event(
event: MemoryEvent,
wrapper: &EventWrapper,
body: &EventRequestBody,
first_event_at: chrono::DateTime<chrono::Utc>,
) -> Self {
let semver = body.semver();
let time =
first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
Self {
app_version: body.app_version.clone(),
major: semver.map(|s| s.major as i32),
minor: semver.map(|s| s.minor as i32),
patch: semver.map(|s| s.patch as i32),
release_channel: body.release_channel.clone().unwrap_or_default(),
installation_id: body.installation_id.clone(),
session_id: body.session_id.clone(),
is_staff: body.is_staff,
time: time.timestamp_millis(),
memory_in_bytes: event.memory_in_bytes,
virtual_memory_in_bytes: event.virtual_memory_in_bytes,
}
}
}
#[derive(Serialize, Debug, clickhouse::Row)]
pub struct AppEventRow {
// AppInfoBase
app_version: String,
major: Option<i32>,
minor: Option<i32>,
patch: Option<i32>,
release_channel: String,
// ClientEventBase
installation_id: Option<String>,
session_id: Option<String>,
is_staff: Option<bool>,
time: i64,
// AppEventRow
operation: String,
}
impl AppEventRow {
fn from_event(
event: AppEvent,
wrapper: &EventWrapper,
body: &EventRequestBody,
first_event_at: chrono::DateTime<chrono::Utc>,
) -> Self {
let semver = body.semver();
let time =
first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
Self {
app_version: body.app_version.clone(),
major: semver.map(|s| s.major as i32),
minor: semver.map(|s| s.minor as i32),
patch: semver.map(|s| s.patch as i32),
release_channel: body.release_channel.clone().unwrap_or_default(),
installation_id: body.installation_id.clone(),
session_id: body.session_id.clone(),
is_staff: body.is_staff,
time: time.timestamp_millis(),
operation: event.operation,
}
}
}
#[derive(Serialize, Debug, clickhouse::Row)]
pub struct SettingEventRow {
// AppInfoBase
app_version: String,
major: Option<i32>,
minor: Option<i32>,
patch: Option<i32>,
release_channel: String,
// ClientEventBase
installation_id: Option<String>,
session_id: Option<String>,
is_staff: Option<bool>,
time: i64,
// SettingEventRow
setting: String,
value: String,
}
impl SettingEventRow {
fn from_event(
event: SettingEvent,
wrapper: &EventWrapper,
body: &EventRequestBody,
first_event_at: chrono::DateTime<chrono::Utc>,
) -> Self {
let semver = body.semver();
let time =
first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
Self {
app_version: body.app_version.clone(),
major: semver.map(|s| s.major as i32),
minor: semver.map(|s| s.minor as i32),
patch: semver.map(|s| s.patch as i32),
release_channel: body.release_channel.clone().unwrap_or_default(),
installation_id: body.installation_id.clone(),
session_id: body.session_id.clone(),
is_staff: body.is_staff,
time: time.timestamp_millis(),
setting: event.setting,
value: event.value,
}
}
}
#[derive(Serialize, Debug, clickhouse::Row)]
pub struct EditEventRow {
// AppInfoBase
app_version: String,
major: Option<i32>,
minor: Option<i32>,
patch: Option<i32>,
release_channel: String,
// SystemInfoBase
os_name: String,
os_version: Option<String>,
architecture: String,
// ClientEventBase
installation_id: Option<String>,
// Note: This column name has a typo in the ClickHouse table.
#[serde(rename = "sesssion_id")]
session_id: Option<String>,
is_staff: Option<bool>,
time: i64,
// EditEventRow
period_start: i64,
period_end: i64,
environment: String,
}
impl EditEventRow {
fn from_event(
event: EditEvent,
wrapper: &EventWrapper,
body: &EventRequestBody,
first_event_at: chrono::DateTime<chrono::Utc>,
) -> Self {
let semver = body.semver();
let time =
first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
let period_start = time - chrono::Duration::milliseconds(event.duration);
let period_end = time;
Self {
app_version: body.app_version.clone(),
major: semver.map(|s| s.major as i32),
minor: semver.map(|s| s.minor as i32),
patch: semver.map(|s| s.patch as i32),
release_channel: body.release_channel.clone().unwrap_or_default(),
os_name: body.os_name.clone(),
os_version: body.os_version.clone(),
architecture: body.architecture.clone(),
installation_id: body.installation_id.clone(),
session_id: body.session_id.clone(),
is_staff: body.is_staff,
time: time.timestamp_millis(),
period_start: period_start.timestamp_millis(),
period_end: period_end.timestamp_millis(),
environment: event.environment,
}
}
}
#[derive(Serialize, Debug, clickhouse::Row)]
pub struct ActionEventRow {
// AppInfoBase
app_version: String,
major: Option<i32>,
minor: Option<i32>,
patch: Option<i32>,
release_channel: String,
// ClientEventBase
installation_id: Option<String>,
// Note: This column name has a typo in the ClickHouse table.
#[serde(rename = "sesssion_id")]
session_id: Option<String>,
is_staff: Option<bool>,
time: i64,
// ActionEventRow
source: String,
action: String,
}
impl ActionEventRow {
fn from_event(
event: ActionEvent,
wrapper: &EventWrapper,
body: &EventRequestBody,
first_event_at: chrono::DateTime<chrono::Utc>,
) -> Self {
let semver = body.semver();
let time =
first_event_at + chrono::Duration::milliseconds(wrapper.milliseconds_since_first_event);
Self {
app_version: body.app_version.clone(),
major: semver.map(|s| s.major as i32),
minor: semver.map(|s| s.minor as i32),
patch: semver.map(|s| s.patch as i32),
release_channel: body.release_channel.clone().unwrap_or_default(),
installation_id: body.installation_id.clone(),
session_id: body.session_id.clone(),
is_staff: body.is_staff,
time: time.timestamp_millis(),
source: event.source,
action: event.action,
}
}
}

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

@@ -58,24 +58,11 @@ impl From<serde_json::Error> for Error {
impl IntoResponse for Error {
fn into_response(self) -> axum::response::Response {
match self {
Error::Http(code, message) => {
log::error!("HTTP error {}: {}", code, &message);
(code, message).into_response()
}
Error::Http(code, message) => (code, message).into_response(),
Error::Database(error) => {
log::error!(
"HTTP error {}: {:?}",
StatusCode::INTERNAL_SERVER_ERROR,
&error
);
(StatusCode::INTERNAL_SERVER_ERROR, format!("{}", &error)).into_response()
}
Error::Internal(error) => {
log::error!(
"HTTP error {}: {:?}",
StatusCode::INTERNAL_SERVER_ERROR,
&error
);
(StatusCode::INTERNAL_SERVER_ERROR, format!("{}", &error)).into_response()
}
}
@@ -110,10 +97,6 @@ pub struct Config {
pub database_url: String,
pub database_max_connections: u32,
pub api_token: String,
pub clickhouse_url: Option<String>,
pub clickhouse_user: Option<String>,
pub clickhouse_password: Option<String>,
pub clickhouse_database: Option<String>,
pub invite_link_prefix: String,
pub live_kit_server: Option<String>,
pub live_kit_key: Option<String>,
@@ -126,7 +109,6 @@ pub struct Config {
pub blob_store_secret_key: Option<String>,
pub blob_store_bucket: Option<String>,
pub zed_environment: Arc<str>,
pub zed_client_checksum_seed: Option<String>,
}
impl Config {
@@ -145,7 +127,6 @@ pub struct AppState {
pub db: Arc<Database>,
pub live_kit_client: Option<Arc<dyn live_kit_server::api::Client>>,
pub blob_store_client: Option<aws_sdk_s3::Client>,
pub clickhouse_client: Option<clickhouse::Client>,
pub config: Config,
}
@@ -175,7 +156,6 @@ impl AppState {
db: Arc::new(db),
live_kit_client,
blob_store_client: build_blob_store_client(&config).await.log_err(),
clickhouse_client: build_clickhouse_client(&config).log_err(),
config,
};
Ok(Arc::new(this))
@@ -216,31 +196,3 @@ async fn build_blob_store_client(config: &Config) -> anyhow::Result<aws_sdk_s3::
Ok(aws_sdk_s3::Client::new(&s3_config))
}
fn build_clickhouse_client(config: &Config) -> anyhow::Result<clickhouse::Client> {
Ok(clickhouse::Client::default()
.with_url(
config
.clickhouse_url
.as_ref()
.ok_or_else(|| anyhow!("missing clickhouse_url"))?,
)
.with_user(
config
.clickhouse_user
.as_ref()
.ok_or_else(|| anyhow!("missing clickhouse_user"))?,
)
.with_password(
config
.clickhouse_password
.as_ref()
.ok_or_else(|| anyhow!("missing clickhouse_password"))?,
)
.with_database(
config
.clickhouse_database
.as_ref()
.ok_or_else(|| anyhow!("missing clickhouse_database"))?,
))
}

View File

@@ -1,11 +1,10 @@
use anyhow::anyhow;
use axum::{extract::MatchedPath, routing::get, Extension, Router};
use axum::{routing::get, Extension, Router};
use collab::{
api::fetch_extensions_from_blob_store_periodically, db, env, executor::Executor, AppState,
Config, MigrateConfig, Result,
};
use db::Database;
use hyper::Request;
use std::{
env::args,
net::{SocketAddr, TcpListener},
@@ -13,8 +12,6 @@ use std::{
sync::Arc,
};
use tokio::signal::unix::SignalKind;
use tower_http::trace::{self, TraceLayer};
use tracing::Level;
use tracing_log::LogTracer;
use tracing_subscriber::{filter::EnvFilter, fmt::format::JsonFields, Layer};
use util::ResultExt;
@@ -31,8 +28,7 @@ async fn main() -> Result<()> {
);
}
let mut args = args().skip(1);
match args.next().as_deref() {
match args().skip(1).next().as_deref() {
Some("version") => {
println!("collab v{} ({})", VERSION, REVISION.unwrap_or("unknown"));
}
@@ -40,17 +36,6 @@ async fn main() -> Result<()> {
run_migrations().await?;
}
Some("serve") => {
let (is_api, is_collab) = if let Some(next) = args.next() {
(next == "api", next == "collab")
} else {
(true, true)
};
if !is_api && !is_collab {
Err(anyhow!(
"usage: collab <version | migrate | serve [api|collab]>"
))?;
}
let config = envy::from_env::<Config>().expect("error loading config");
init_tracing(&config);
@@ -61,52 +46,22 @@ async fn main() -> Result<()> {
let listener = TcpListener::bind(&format!("0.0.0.0:{}", state.config.http_port))
.expect("failed to bind TCP listener");
let rpc_server = if is_collab {
let epoch = state
.db
.create_server(&state.config.zed_environment)
.await?;
let rpc_server =
collab::rpc::Server::new(epoch, state.clone(), Executor::Production);
rpc_server.start().await?;
let epoch = state
.db
.create_server(&state.config.zed_environment)
.await?;
let rpc_server = collab::rpc::Server::new(epoch, state.clone(), Executor::Production);
rpc_server.start().await?;
Some(rpc_server)
} else {
None
};
fetch_extensions_from_blob_store_periodically(state.clone(), Executor::Production);
if is_api {
fetch_extensions_from_blob_store_periodically(state.clone(), Executor::Production);
}
let mut app = collab::api::routes(rpc_server.clone(), state.clone());
if let Some(rpc_server) = rpc_server.clone() {
app = app.merge(collab::rpc::routes(rpc_server))
}
app = app
let app = collab::api::routes(rpc_server.clone(), state.clone())
.merge(collab::rpc::routes(rpc_server.clone()))
.merge(
Router::new()
.route("/", get(handle_root))
.route("/healthz", get(handle_liveness_probe))
.merge(collab::api::extensions::router())
.merge(collab::api::events::router())
.layer(Extension(state.clone())),
)
.layer(
TraceLayer::new_for_http()
.make_span_with(|request: &Request<_>| {
let matched_path = request
.extensions()
.get::<MatchedPath>()
.map(MatchedPath::as_str);
tracing::info_span!(
"http_request",
method = ?request.method(),
matched_path,
)
})
.on_response(trace::DefaultOnResponse::new().level(Level::INFO)),
);
axum::Server::from_tcp(listener)?
@@ -121,17 +76,12 @@ async fn main() -> Result<()> {
futures::pin_mut!(sigterm, sigint);
futures::future::select(sigterm, sigint).await;
tracing::info!("Received interrupt signal");
if let Some(rpc_server) = rpc_server {
rpc_server.teardown();
}
rpc_server.teardown();
})
.await?;
}
_ => {
Err(anyhow!(
"usage: collab <version | migrate | serve [api|collab]>"
))?;
Err(anyhow!("usage: collab <version | migrate | serve>"))?;
}
}
Ok(())

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
use crate::{
db::{tests::TestDb, NewUserParams, UserId},
executor::Executor,
rpc::{Server, CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
rpc::{Server, ZedVersion, CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
AppState, Config,
};
use anyhow::anyhow;
@@ -38,7 +38,7 @@ use std::{
Arc,
},
};
use util::http::FakeHttpClient;
use util::{http::FakeHttpClient, SemanticVersion};
use workspace::{Workspace, WorkspaceStore};
pub struct TestServer {
@@ -233,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()),
@@ -482,7 +483,6 @@ impl TestServer {
db: test_db.db().clone(),
live_kit_client: Some(Arc::new(fake_server.create_api_client())),
blob_store_client: None,
clickhouse_client: None,
config: Config {
http_port: 0,
database_url: "".into(),
@@ -500,11 +500,6 @@ impl TestServer {
blob_store_access_key: None,
blob_store_secret_key: None,
blob_store_bucket: None,
clickhouse_url: None,
clickhouse_user: None,
clickhouse_password: None,
clickhouse_database: None,
zed_client_checksum_seed: None,
},
})
}

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,9 +12,9 @@ use crate::{
mouse_context_menu,
scroll::scroll_amount::ScrollAmount,
CursorShape, DisplayPoint, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode,
EditorSettings, EditorSnapshot, EditorStyle, HalfPageDown, HalfPageUp, HoveredCursor, LineDown,
LineUp, OpenExcerpts, PageDown, PageUp, Point, SelectPhase, Selection, SoftWrap, ToPoint,
CURSORS_VISIBLE_FOR, MAX_LINE_LEN,
EditorSettings, EditorSnapshot, EditorStyle, GutterDimensions, HalfPageDown, HalfPageUp,
HoveredCursor, LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point, SelectPhase, Selection,
SoftWrap, ToPoint, CURSORS_VISIBLE_FOR, MAX_LINE_LEN,
};
use anyhow::Result;
use collections::{BTreeMap, HashMap};
@@ -260,6 +260,8 @@ impl EditorElement {
register_action(view, cx, Editor::go_to_prev_hunk);
register_action(view, cx, Editor::go_to_definition);
register_action(view, cx, Editor::go_to_definition_split);
register_action(view, cx, Editor::go_to_implementation);
register_action(view, cx, Editor::go_to_implementation_split);
register_action(view, cx, Editor::go_to_type_definition);
register_action(view, cx, Editor::go_to_type_definition_split);
register_action(view, cx, Editor::open_url);
@@ -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

@@ -1,6 +1,7 @@
use anyhow::{anyhow, bail, Context as _, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use client::ClientSettings;
use collections::{BTreeMap, HashSet};
use fs::{Fs, RemoveOptions};
use futures::channel::mpsc::unbounded;
@@ -12,6 +13,7 @@ use language::{
};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use settings::Settings as _;
use std::cmp::Ordering;
use std::{
ffi::OsStr,
@@ -20,7 +22,7 @@ use std::{
time::Duration,
};
use theme::{ThemeRegistry, ThemeSettings};
use util::http::{AsyncBody, ZedHttpClient};
use util::http::AsyncBody;
use util::TryFutureExt;
use util::{http::HttpClient, paths::EXTENSIONS_DIR, ResultExt};
@@ -32,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>,
@@ -55,7 +57,7 @@ pub enum ExtensionStatus {
pub struct ExtensionStore {
manifest: Arc<RwLock<Manifest>>,
fs: Arc<dyn Fs>,
http_client: Arc<ZedHttpClient>,
http_client: Arc<dyn HttpClient>,
extensions_dir: PathBuf,
extensions_being_installed: HashSet<Arc<str>>,
extensions_being_uninstalled: HashSet<Arc<str>>,
@@ -111,7 +113,7 @@ actions!(zed, [ReloadExtensions]);
pub fn init(
fs: Arc<fs::RealFs>,
http_client: Arc<ZedHttpClient>,
http_client: Arc<dyn HttpClient>,
language_registry: Arc<LanguageRegistry>,
theme_registry: Arc<ThemeRegistry>,
cx: &mut AppContext,
@@ -143,7 +145,7 @@ impl ExtensionStore {
pub fn new(
extensions_dir: PathBuf,
fs: Arc<dyn Fs>,
http_client: Arc<ZedHttpClient>,
http_client: Arc<dyn HttpClient>,
language_registry: Arc<LanguageRegistry>,
theme_registry: Arc<ThemeRegistry>,
cx: &mut ModelContext<Self>,
@@ -222,12 +224,14 @@ impl ExtensionStore {
search: Option<&str>,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<Extension>>> {
let url = self.http_client.zed_api_url(&format!(
"/extensions{query}",
let url = format!(
"{}/{}{query}",
ClientSettings::get_global(cx).server_url,
"api/extensions",
query = search
.map(|search| format!("?filter={search}"))
.unwrap_or_default()
));
);
let http_client = self.http_client.clone();
cx.spawn(move |_, _| async move {
let mut response = http_client.get(&url, AsyncBody::empty(), true).await?;
@@ -260,9 +264,10 @@ impl ExtensionStore {
cx: &mut ModelContext<Self>,
) {
log::info!("installing extension {extension_id} {version}");
let url = self
.http_client
.zed_api_url(&format!("/extensions/{extension_id}/{version}/download"));
let url = format!(
"{}/api/extensions/{extension_id}/{version}/download",
ClientSettings::get_global(cx).server_url
);
let extensions_dir = self.extensions_dir();
let http_client = self.http_client.clone();
@@ -401,10 +406,6 @@ impl ExtensionStore {
}));
for language_name in &languages_to_add {
if language_name.as_ref() == "Swift" {
continue;
}
let language = manifest.languages.get(language_name.as_ref()).unwrap();
let mut language_path = self.extensions_dir.clone();
language_path.extend([language.extension.as_ref(), language.path.as_path()]);

View File

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

View File

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

View File

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

View File

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

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

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

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

@@ -108,35 +108,6 @@ impl Keystroke {
ime_key,
})
}
/// Returns a new keystroke with the ime_key filled.
/// This is used for dispatch_keystroke where we want users to
/// be able to simulate typing "space", etc.
pub fn with_simulated_ime(mut self) -> Self {
if self.ime_key.is_none()
&& !self.modifiers.command
&& !self.modifiers.control
&& !self.modifiers.function
&& !self.modifiers.alt
{
self.ime_key = match self.key.as_str() {
"space" => Some(" ".into()),
"tab" => Some("\t".into()),
"enter" => Some("\n".into()),
"up" | "down" | "left" | "right" | "pageup" | "pagedown" | "home" | "end"
| "delete" | "escape" | "backspace" | "f1" | "f2" | "f3" | "f4" | "f5" | "f6"
| "f7" | "f8" | "f9" | "f10" | "f11" | "f12" => None,
key => {
if self.modifiers.shift {
Some(key.to_uppercase())
} else {
Some(key.into())
}
}
}
}
self
}
}
impl std::fmt::Display for Keystroke {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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(),
}
}
}

View File

@@ -130,13 +130,17 @@ fn paint_line(
if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
wraps.next();
if let Some((background_origin, background_color)) = current_background.as_mut() {
cx.paint_quad(fill(
Bounds {
origin: *background_origin,
size: size(glyph_origin.x - background_origin.x, line_height),
},
*background_color,
));
cx.paint_quad(
fill(
Bounds {
origin: *background_origin,
size: size(glyph_origin.x - background_origin.x, line_height),
},
*background_color,
),
None,
None,
);
background_origin.x = origin.x;
background_origin.y += line_height;
}
@@ -229,13 +233,17 @@ fn paint_line(
}
if let Some((background_origin, background_color)) = finished_background {
cx.paint_quad(fill(
Bounds {
origin: background_origin,
size: size(glyph_origin.x - background_origin.x, line_height),
},
background_color,
));
cx.paint_quad(
fill(
Bounds {
origin: background_origin,
size: size(glyph_origin.x - background_origin.x, line_height),
},
background_color,
),
None,
None,
);
}
if let Some((underline_origin, underline_style)) = finished_underline {
@@ -289,13 +297,17 @@ fn paint_line(
}
if let Some((background_origin, background_color)) = current_background.take() {
cx.paint_quad(fill(
Bounds {
origin: background_origin,
size: size(last_line_end_x - background_origin.x, line_height),
},
background_color,
));
cx.paint_quad(
fill(
Bounds {
origin: background_origin,
size: size(last_line_end_x - background_origin.x, line_height),
},
background_color,
),
None,
None,
);
}
if let Some((underline_start, underline_style)) = current_underline.take() {

View File

@@ -212,7 +212,8 @@ impl AnyView {
/// When using this method, the view's previous layout and paint will be recycled from the previous frame if [ViewContext::notify] has not been called since it was rendered.
/// The one exception is when [WindowContext::refresh] is called, in which case caching is ignored.
pub fn cached(mut self) -> Self {
self.cache = true;
// TODO!: ENABLE ME!
// self.cache = true;
self
}

View File

@@ -5,8 +5,8 @@ use crate::{
Global, GlobalElementId, Hsla, KeyBinding, KeyContext, KeyDownEvent, KeyMatch, KeymatchResult,
Keystroke, KeystrokeEvent, Model, ModelContext, Modifiers, MouseButton, MouseMoveEvent,
MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformWindow, Point,
PromptLevel, Render, ScaledPixels, SharedString, Size, SubscriberSet, Subscription,
TaffyLayoutEngine, Task, View, VisualContext, WeakView, WindowAppearance, WindowBounds,
PromptLevel, Quad, Render, ScaledPixels, SharedString, Size, SubscriberSet, Subscription,
TaffyLayoutEngine, Task, View, ViewId, VisualContext, WeakView, WindowAppearance, WindowBounds,
WindowOptions, WindowTextSystem,
};
use anyhow::{anyhow, Context as _, Result};
@@ -950,6 +950,7 @@ impl<'a> WindowContext<'a> {
/// Produces a new frame and assigns it to `rendered_frame`. To actually show
/// the contents of the new [Scene], use [present].
#[profiling::function]
pub fn draw(&mut self) {
self.window.dirty.set(false);
self.window.drawing = true;
@@ -1042,9 +1043,10 @@ impl<'a> WindowContext<'a> {
self.window.layout_engine.as_mut().unwrap().clear();
self.text_system()
.finish_frame(&self.window.next_frame.reused_views);
let mouse_position = self.window.mouse_position.scale(self.window.scale_factor);
self.window
.next_frame
.finish(&mut self.window.rendered_frame);
.finish(&mut self.window.rendered_frame, mouse_position);
ELEMENT_ARENA.with_borrow_mut(|element_arena| {
let percentage = (element_arena.len() as f32 / element_arena.capacity() as f32) * 100.;
if percentage >= 80. {
@@ -1092,17 +1094,29 @@ impl<'a> WindowContext<'a> {
self.window.needs_present.set(true);
}
#[profiling::function]
fn present(&self) {
self.window
.platform_window
.draw(&self.window.rendered_frame.scene);
self.window.needs_present.set(false);
profiling::finish_frame!();
}
/// Dispatch a given keystroke as though the user had typed it.
/// You can create a keystroke with Keystroke::parse("").
pub fn dispatch_keystroke(&mut self, keystroke: Keystroke) -> bool {
let keystroke = keystroke.with_simulated_ime();
pub fn dispatch_keystroke(&mut self, mut keystroke: Keystroke) -> 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_uppercase().clone()
} else {
keystroke.key.clone()
})
}
if self.dispatch_event(PlatformInput::KeyDown(KeyDownEvent {
keystroke: keystroke.clone(),
is_held: false,
@@ -1122,6 +1136,7 @@ impl<'a> WindowContext<'a> {
}
/// Dispatch a mouse or keyboard event on the window.
#[profiling::function]
pub fn dispatch_event(&mut self, event: PlatformInput) -> bool {
self.window.last_input_timestamp.set(Instant::now());
// Handlers may set this to false by calling `stop_propagation`.
@@ -2783,6 +2798,24 @@ impl PaintQuad {
..self
}
}
pub(crate) fn into_primitive(
self,
view_id: impl Into<ViewId>,
scale_factor: f32,
content_mask: ContentMask<Pixels>,
) -> Quad {
Quad {
view_id: view_id.into(),
order: 0,
bounds: self.bounds.scale(scale_factor),
content_mask: content_mask.scale(scale_factor),
background: self.background,
border_color: self.border_color,
corner_radii: self.corner_radii.scale(scale_factor),
border_widths: self.border_widths.scale(scale_factor),
}
}
}
/// Creates a quad with the given parameters.

View File

@@ -34,9 +34,9 @@ use crate::{
EntityId, FocusHandle, FocusId, FontId, GlobalElementId, GlyphId, Hsla, ImageData,
InputHandler, IsZero, KeyContext, KeyEvent, LayoutId, MonochromeSprite, MouseEvent, PaintQuad,
Path, Pixels, PlatformInputHandler, Point, PolychromeSprite, Quad, RenderGlyphParams,
RenderImageParams, RenderSvgParams, Scene, Shadow, SharedString, Size, StackingContext,
StackingOrder, StrikethroughStyle, Style, TextStyleRefinement, Underline, UnderlineStyle,
Window, WindowContext, SUBPIXEL_VARIANTS,
RenderImageParams, RenderSvgParams, ScaledPixels, Scene, Shadow, SharedString, Size,
StackingContext, StackingOrder, StrikethroughStyle, Style, TextStyleRefinement, Underline,
UnderlineStyle, Window, WindowContext, SUBPIXEL_VARIANTS,
};
type AnyMouseListener = Box<dyn FnMut(&dyn Any, DispatchPhase, &mut ElementContext) + 'static>;
@@ -124,7 +124,7 @@ impl Frame {
.unwrap_or_default()
}
pub(crate) fn finish(&mut self, prev_frame: &mut Self) {
pub(crate) fn finish(&mut self, prev_frame: &mut Self, mouse_position: Point<ScaledPixels>) {
// Reuse mouse listeners that didn't change since the last frame.
for (type_id, listeners) in &mut prev_frame.mouse_listeners {
let next_listeners = self.mouse_listeners.entry(*type_id).or_default();
@@ -157,7 +157,7 @@ impl Frame {
// Reuse geometry that didn't change since the last frame.
self.scene
.reuse_views(&self.reused_views, &mut prev_frame.scene);
self.scene.finish();
self.scene.finish(mouse_position);
}
}
@@ -651,6 +651,7 @@ impl<'a> ElementContext<'a> {
}
})
}
/// Paint one or more drop shadows into the scene for the next frame at the current z-index.
pub fn paint_shadows(
&mut self,
@@ -666,11 +667,9 @@ impl<'a> ElementContext<'a> {
let mut shadow_bounds = bounds;
shadow_bounds.origin += shadow.offset;
shadow_bounds.dilate(shadow.spread_radius);
window.next_frame.scene.insert(
&window.next_frame.z_index_stack,
window.next_frame.scene.insert_shadow(
Shadow {
view_id: view_id.into(),
layer_id: 0,
order: 0,
bounds: shadow_bounds.scale(scale_factor),
content_mask: content_mask.scale(scale_factor),
@@ -679,6 +678,8 @@ impl<'a> ElementContext<'a> {
blur_radius: shadow.blur_radius.scale(scale_factor),
pad: 0,
},
None,
None,
);
}
}
@@ -686,17 +687,20 @@ impl<'a> ElementContext<'a> {
/// Paint one or more quads into the scene for the next frame at the current stacking context.
/// Quads are colored rectangular regions with an optional background, border, and corner radius.
/// see [`fill`](crate::fill), [`outline`](crate::outline), and [`quad`](crate::quad) to construct this type.
pub fn paint_quad(&mut self, quad: PaintQuad) {
pub fn paint_quad(
&mut self,
quad: PaintQuad,
hover: Option<PaintQuad>,
group_hover: Option<(SharedString, Option<PaintQuad>)>,
) {
let scale_factor = self.scale_factor();
let content_mask = self.content_mask();
let view_id = self.parent_view_id();
let window = &mut *self.window;
window.next_frame.scene.insert(
&window.next_frame.z_index_stack,
window.next_frame.scene.insert_quad(
Quad {
view_id: view_id.into(),
layer_id: 0,
order: 0,
bounds: quad.bounds.scale(scale_factor),
content_mask: content_mask.scale(scale_factor),
@@ -705,6 +709,13 @@ impl<'a> ElementContext<'a> {
corner_radii: quad.corner_radii.scale(scale_factor),
border_widths: quad.border_widths.scale(scale_factor),
},
hover.map(|quad| quad.into_primitive(view_id, scale_factor, content_mask.clone())),
group_hover.map(|(group_id, quad)| {
(
group_id,
quad.map(|quad| quad.into_primitive(view_id, scale_factor, content_mask)),
)
}),
);
}
@@ -721,7 +732,7 @@ impl<'a> ElementContext<'a> {
window
.next_frame
.scene
.insert(&window.next_frame.z_index_stack, path.scale(scale_factor));
.insert_path(path.scale(scale_factor), None, None);
}
/// Paint an underline into the scene for the next frame at the current z-index.
@@ -745,11 +756,9 @@ impl<'a> ElementContext<'a> {
let view_id = self.parent_view_id();
let window = &mut *self.window;
window.next_frame.scene.insert(
&window.next_frame.z_index_stack,
window.next_frame.scene.insert_underline(
Underline {
view_id: view_id.into(),
layer_id: 0,
order: 0,
bounds: bounds.scale(scale_factor),
content_mask: content_mask.scale(scale_factor),
@@ -757,6 +766,8 @@ impl<'a> ElementContext<'a> {
thickness: style.thickness.scale(scale_factor),
wavy: style.wavy,
},
None,
None,
);
}
@@ -777,11 +788,9 @@ impl<'a> ElementContext<'a> {
let view_id = self.parent_view_id();
let window = &mut *self.window;
window.next_frame.scene.insert(
&window.next_frame.z_index_stack,
window.next_frame.scene.insert_underline(
Underline {
view_id: view_id.into(),
layer_id: 0,
order: 0,
bounds: bounds.scale(scale_factor),
content_mask: content_mask.scale(scale_factor),
@@ -789,6 +798,8 @@ impl<'a> ElementContext<'a> {
color: style.color.unwrap_or_default(),
wavy: false,
},
None,
None,
);
}
@@ -837,17 +848,17 @@ impl<'a> ElementContext<'a> {
let content_mask = self.content_mask().scale(scale_factor);
let view_id = self.parent_view_id();
let window = &mut *self.window;
window.next_frame.scene.insert(
&window.next_frame.z_index_stack,
window.next_frame.scene.insert_monochrome_sprite(
MonochromeSprite {
view_id: view_id.into(),
layer_id: 0,
order: 0,
bounds,
content_mask,
color,
tile,
},
None,
None,
);
}
Ok(())
@@ -895,11 +906,9 @@ impl<'a> ElementContext<'a> {
let view_id = self.parent_view_id();
let window = &mut *self.window;
window.next_frame.scene.insert(
&window.next_frame.z_index_stack,
window.next_frame.scene.insert_polychrome_sprite(
PolychromeSprite {
view_id: view_id.into(),
layer_id: 0,
order: 0,
bounds,
corner_radii: Default::default(),
@@ -908,6 +917,8 @@ impl<'a> ElementContext<'a> {
grayscale: false,
pad: 0,
},
None,
None,
);
}
Ok(())
@@ -941,17 +952,17 @@ impl<'a> ElementContext<'a> {
let view_id = self.parent_view_id();
let window = &mut *self.window;
window.next_frame.scene.insert(
&window.next_frame.z_index_stack,
window.next_frame.scene.insert_monochrome_sprite(
MonochromeSprite {
view_id: view_id.into(),
layer_id: 0,
order: 0,
bounds,
content_mask,
color,
tile,
},
None,
None,
);
Ok(())
@@ -980,11 +991,9 @@ impl<'a> ElementContext<'a> {
let view_id = self.parent_view_id();
let window = &mut *self.window;
window.next_frame.scene.insert(
&window.next_frame.z_index_stack,
window.next_frame.scene.insert_polychrome_sprite(
PolychromeSprite {
view_id: view_id.into(),
layer_id: 0,
order: 0,
bounds,
content_mask,
@@ -993,28 +1002,34 @@ impl<'a> ElementContext<'a> {
grayscale,
pad: 0,
},
None,
None,
);
Ok(())
}
/// Paint a surface into the scene for the next frame at the current z-index.
#[cfg(target_os = "macos")]
pub fn paint_surface(&mut self, bounds: Bounds<Pixels>, image_buffer: CVImageBuffer) {
pub fn paint_surface(
&mut self,
bounds: Bounds<Pixels>,
image_buffer: CVImageBuffer,
occludes_hover: bool,
) {
let scale_factor = self.scale_factor();
let bounds = bounds.scale(scale_factor);
let content_mask = self.content_mask().scale(scale_factor);
let view_id = self.parent_view_id();
let window = &mut *self.window;
window.next_frame.scene.insert(
&window.next_frame.z_index_stack,
window.next_frame.scene.insert_surface(
crate::Surface {
view_id: view_id.into(),
layer_id: 0,
order: 0,
bounds,
content_mask,
image_buffer,
},
occludes_hover,
);
}
@@ -1161,6 +1176,22 @@ impl<'a> ElementContext<'a> {
})
}
/// Invoke the given function with the given hover group id present on the hover stack.
/// This is a fairly low-level method used to paint hover effects for views that share
/// the same hover group.
pub fn with_hover_group<R>(
&mut self,
name: Option<SharedString>,
f: impl FnOnce(&mut Self) -> R,
) -> R {
let window = &mut self.window;
let group = window.next_frame.scene.hover_group(name);
window.hover_group_stack.push(group);
let result = f(self);
window.hover_group_stack.pop();
result
}
/// Sets an input handler, such as [`ElementInputHandler`][element_input_handler], which interfaces with the
/// platform to receive textual input with proper integration with concerns such
/// as IME interactions. This handler will be active for the upcoming frame until the following frame is

View File

@@ -399,8 +399,6 @@ fn box_suffixes() -> Vec<(&'static str, TokenStream2, &'static str)> {
("72", quote! { rems(18.) }, "288px (18rem)"),
("80", quote! { rems(20.) }, "320px (20rem)"),
("96", quote! { rems(24.) }, "384px (24rem)"),
("112", quote! { rems(28.) }, "448px (28rem)"),
("128", quote! { rems(32.) }, "512px (32rem)"),
("auto", quote! { auto() }, "Auto"),
("px", quote! { px(1.) }, "1px"),
("full", quote! { relative(1.) }, "100%"),

View File

@@ -1138,6 +1138,7 @@ impl LanguageServer {
document_formatting_provider: Some(OneOf::Left(true)),
document_range_formatting_provider: Some(OneOf::Left(true)),
definition_provider: Some(OneOf::Left(true)),
implementation_provider: Some(ImplementationProviderCapability::Simple(true)),
type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)),
..Default::default()
}

View File

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

View File

@@ -1,7 +1,7 @@
use editor::Editor;
use gpui::{
div, list, prelude::*, uniform_list, AnyElement, AppContext, DismissEvent, EventEmitter,
FocusHandle, FocusableView, Length, ListState, MouseButton, MouseDownEvent, Render, Task,
div, list, prelude::*, uniform_list, AnyElement, AppContext, ClickEvent, DismissEvent,
EventEmitter, FocusHandle, FocusableView, Length, ListState, Render, Task,
UniformListScrollHandle, View, ViewContext, WindowContext,
};
use std::{sync::Arc, time::Duration};
@@ -103,7 +103,7 @@ impl<D: PickerDelegate> Picker<D> {
let mut this = Self {
delegate,
editor,
element_container: Self::crate_element_container(is_uniform, cx),
element_container: Self::create_element_container(is_uniform, cx),
pending_update_matches: None,
confirm_on_update: None,
width: None,
@@ -117,7 +117,7 @@ impl<D: PickerDelegate> Picker<D> {
this
}
fn crate_element_container(is_uniform: bool, cx: &mut ViewContext<Self>) -> ElementContainer {
fn create_element_container(is_uniform: bool, cx: &mut ViewContext<Self>) -> ElementContainer {
if is_uniform {
ElementContainer::UniformList(UniformListScrollHandle::new())
} else {
@@ -311,12 +311,10 @@ impl<D: PickerDelegate> Picker<D> {
fn render_element(&self, cx: &mut ViewContext<Self>, ix: usize) -> impl IntoElement {
div()
.on_mouse_down(
MouseButton::Left,
cx.listener(move |this, event: &MouseDownEvent, cx| {
this.handle_click(ix, event.modifiers.command, cx)
}),
)
.id(("item", ix))
.on_click(cx.listener(move |this, event: &ClickEvent, cx| {
this.handle_click(ix, event.down.modifiers.command, cx)
}))
.children(
self.delegate
.render_match(ix, ix == self.delegate.selected_index(), cx),

View File

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

View File

@@ -228,7 +228,6 @@ pub struct LanguageServerPromptRequest {
pub level: PromptLevel,
pub message: String,
pub actions: Vec<MessageActionItem>,
pub lsp_name: String,
response_channel: Sender<MessageActionItem>,
}
@@ -3014,7 +3013,6 @@ impl Project {
cx.update(|cx| adapter.workspace_configuration(worktree_path, cx))?;
let language_server = pending_server.task.await?;
let name = language_server.name();
language_server
.on_notification::<lsp::notification::PublishDiagnostics, _>({
let adapter = adapter.clone();
@@ -3153,10 +3151,8 @@ impl Project {
language_server
.on_request::<lsp::request::ShowMessageRequest, _, _>({
let this = this.clone();
let name = name.to_string();
move |params, mut cx| {
let this = this.clone();
let name = name.to_string();
async move {
if let Some(actions) = params.actions {
let (tx, mut rx) = smol::channel::bounded(1);
@@ -3169,7 +3165,6 @@ impl Project {
message: params.message,
actions,
response_channel: tx,
lsp_name: name.clone(),
};
if let Ok(_) = this.update(&mut cx, |_, cx| {
@@ -3207,7 +3202,6 @@ impl Project {
}
})
.detach();
let mut initialization_options = adapter.adapter.initialization_options();
match (&mut initialization_options, override_options) {
(Some(initialization_options), Some(override_options)) => {
@@ -4652,6 +4646,7 @@ impl Project {
cx,
)
}
pub fn type_definition<T: ToPointUtf16>(
&self,
buffer: &Model<Buffer>,
@@ -4659,10 +4654,33 @@ impl Project {
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<LocationLink>>> {
let position = position.to_point_utf16(buffer.read(cx));
self.type_definition_impl(buffer, position, cx)
}
fn implementation_impl(
&self,
buffer: &Model<Buffer>,
position: PointUtf16,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<LocationLink>>> {
self.request_lsp(
buffer.clone(),
LanguageServerToQuery::Primary,
GetImplementation { position },
cx,
)
}
pub fn implementation<T: ToPointUtf16>(
&self,
buffer: &Model<Buffer>,
position: T,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<LocationLink>>> {
let position = position.to_point_utf16(buffer.read(cx));
self.implementation_impl(buffer, position, cx)
}
fn references_impl(
&self,
buffer: &Model<Buffer>,
@@ -9327,9 +9345,7 @@ impl LspAdapterDelegate for ProjectLspAdapterDelegate {
Ok(command_path) => Some((command_path, shell_env)),
Err(error) => {
log::warn!(
"failed to determine path for command {:?} in shell PATH {:?}: {error}",
command.to_string_lossy(),
shell_path.map(String::as_str).unwrap_or("")
"failed to determine path for command {:?} in env {shell_env:?}: {error}", command.to_string_lossy()
);
None
}

View File

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

View File

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

View File

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

View File

@@ -1,20 +1,19 @@
mod highlighted_workspace_location;
mod projects;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Result, Subscription, Task,
View, ViewContext, WeakView,
AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Result,
Subscription, Task, View, ViewContext, WeakView,
};
use highlighted_workspace_location::HighlightedWorkspaceLocation;
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
use std::sync::Arc;
use ui::{prelude::*, tooltip_container, HighlightedLabel, ListItem, ListItemSpacing};
use ui::{prelude::*, tooltip_container, HighlightedLabel, ListItem, ListItemSpacing, Tooltip};
use util::paths::PathExt;
use workspace::{ModalView, Workspace, WorkspaceLocation, WORKSPACE_DB};
use workspace::{ModalView, Workspace, WorkspaceId, WorkspaceLocation, WORKSPACE_DB};
pub use projects::OpenRecent;
gpui::actions!(projects, [OpenRecent]);
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(RecentProjects::register).detach();
@@ -45,13 +44,11 @@ impl RecentProjects {
let workspaces = WORKSPACE_DB
.recent_workspaces_on_disk()
.await
.unwrap_or_default()
.into_iter()
.map(|(_, location)| location)
.collect();
.unwrap_or_default();
this.update(&mut cx, move |this, cx| {
this.picker.update(cx, move |picker, cx| {
picker.delegate.workspace_locations = workspaces;
picker.delegate.workspaces = workspaces;
picker.update_matches(picker.query(cx), cx)
})
})
@@ -96,6 +93,7 @@ impl RecentProjects {
Ok(())
}))
}
pub fn open_popover(workspace: WeakView<Workspace>, cx: &mut WindowContext<'_>) -> View<Self> {
cx.new_view(|cx| Self::new(RecentProjectsDelegate::new(workspace, false), 20., cx))
}
@@ -124,20 +122,23 @@ impl Render for RecentProjects {
pub struct RecentProjectsDelegate {
workspace: WeakView<Workspace>,
workspace_locations: Vec<WorkspaceLocation>,
workspaces: Vec<(WorkspaceId, WorkspaceLocation)>,
selected_match_index: usize,
matches: Vec<StringMatch>,
render_paths: bool,
// Flag to reset index when there is a new query vs not reset index when user delete an item
reset_selected_match_index: bool,
}
impl RecentProjectsDelegate {
fn new(workspace: WeakView<Workspace>, render_paths: bool) -> Self {
Self {
workspace,
workspace_locations: vec![],
workspaces: vec![],
selected_match_index: 0,
matches: Default::default(),
render_paths,
reset_selected_match_index: true,
}
}
}
@@ -146,7 +147,11 @@ impl PickerDelegate for RecentProjectsDelegate {
type ListItem = ListItem;
fn placeholder_text(&self) -> Arc<str> {
"Search recent projects...".into()
Arc::from(format!(
"`{:?}` reuses the window, `{:?}` opens in new",
menu::Confirm,
menu::SecondaryConfirm,
))
}
fn match_count(&self) -> usize {
@@ -169,10 +174,10 @@ impl PickerDelegate for RecentProjectsDelegate {
let query = query.trim_start();
let smart_case = query.chars().any(|c| c.is_uppercase());
let candidates = self
.workspace_locations
.workspaces
.iter()
.enumerate()
.map(|(id, location)| {
.map(|(id, (_, location))| {
let combined_string = location
.paths()
.iter()
@@ -192,28 +197,40 @@ impl PickerDelegate for RecentProjectsDelegate {
));
self.matches.sort_unstable_by_key(|m| m.candidate_id);
self.selected_match_index = self
.matches
.iter()
.enumerate()
.rev()
.max_by_key(|(_, m)| OrderedFloat(m.score))
.map(|(ix, _)| ix)
.unwrap_or(0);
if self.reset_selected_match_index {
self.selected_match_index = self
.matches
.iter()
.enumerate()
.rev()
.max_by_key(|(_, m)| OrderedFloat(m.score))
.map(|(ix, _)| ix)
.unwrap_or(0);
}
self.reset_selected_match_index = true;
Task::ready(())
}
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
if let Some((selected_match, workspace)) = self
.matches
.get(self.selected_index())
.zip(self.workspace.upgrade())
{
let workspace_location = &self.workspace_locations[selected_match.candidate_id];
let (candidate_workspace_id, candidate_workspace_location) =
&self.workspaces[selected_match.candidate_id];
let replace_current_window = !secondary;
workspace
.update(cx, |workspace, cx| {
workspace
.open_workspace_for_paths(workspace_location.paths().as_ref().clone(), cx)
if workspace.database_id() != *candidate_workspace_id {
workspace.open_workspace_for_paths(
replace_current_window,
candidate_workspace_location.paths().as_ref().clone(),
cx,
)
} else {
Task::ready(Ok(()))
}
})
.detach_and_log_err(cx);
cx.emit(DismissEvent);
@@ -226,19 +243,18 @@ impl PickerDelegate for RecentProjectsDelegate {
&self,
ix: usize,
selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let Some(r#match) = self.matches.get(ix) else {
return None;
};
let highlighted_location = HighlightedWorkspaceLocation::new(
&r#match,
&self.workspace_locations[r#match.candidate_id],
);
let (workspace_id, location) = &self.workspaces[r#match.candidate_id];
let highlighted_location: HighlightedWorkspaceLocation =
HighlightedWorkspaceLocation::new(&r#match, location);
let tooltip_highlighted_location = highlighted_location.clone();
let is_current_workspace = self.is_current_workspace(*workspace_id, cx);
Some(
ListItem::new(ix)
.inset(true)
@@ -255,6 +271,27 @@ impl PickerDelegate for RecentProjectsDelegate {
}))
}),
)
.when(!is_current_workspace, |el| {
let delete_button = div()
.child(
IconButton::new("delete", IconName::Close)
.icon_size(IconSize::Small)
.on_click(cx.listener(move |this, _event, cx| {
cx.stop_propagation();
cx.prevent_default();
this.delegate.delete_recent_project(ix, cx)
}))
.tooltip(|cx| Tooltip::text("Delete From Recent Projects...", cx)),
)
.into_any_element();
if self.selected_index() == ix {
el.end_slot::<AnyElement>(delete_button)
} else {
el.end_hover_slot::<AnyElement>(delete_button)
}
})
.tooltip(move |cx| {
let tooltip_highlighted_location = tooltip_highlighted_location.clone();
cx.new_view(move |_| MatchTooltip {
@@ -266,6 +303,42 @@ impl PickerDelegate for RecentProjectsDelegate {
}
}
impl RecentProjectsDelegate {
fn delete_recent_project(&self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
if let Some(selected_match) = self.matches.get(ix) {
let (workspace_id, _) = self.workspaces[selected_match.candidate_id];
cx.spawn(move |this, mut cx| async move {
let _ = WORKSPACE_DB.delete_workspace_by_id(workspace_id).await;
let workspaces = WORKSPACE_DB
.recent_workspaces_on_disk()
.await
.unwrap_or_default();
this.update(&mut cx, move |picker, cx| {
picker.delegate.workspaces = workspaces;
picker.delegate.set_selected_index(ix - 1, cx);
picker.delegate.reset_selected_match_index = false;
picker.update_matches(picker.query(cx), cx)
})
})
.detach();
}
}
fn is_current_workspace(
&self,
workspace_id: WorkspaceId,
cx: &mut ViewContext<Picker<Self>>,
) -> bool {
if let Some(workspace) = self.workspace.upgrade() {
let workspace = workspace.read(cx);
if workspace_id == workspace.database_id() {
return true;
}
}
false
}
}
struct MatchTooltip {
highlighted_location: HighlightedWorkspaceLocation,
}

View File

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

View File

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

View File

@@ -116,20 +116,13 @@ pub fn update_settings_file<T: Settings>(
store.new_text_for_update::<T>(old_text, update)
})?;
let initial_path = paths::SETTINGS.as_path();
if !fs.is_file(initial_path).await {
fs.atomic_write(initial_path.to_path_buf(), new_text)
.await
.with_context(|| format!("Failed to write settings to file {:?}", initial_path))?;
} else {
let resolved_path = fs.canonicalize(initial_path).await.with_context(|| {
format!("Failed to canonicalize settings path {:?}", initial_path)
})?;
fs.atomic_write(resolved_path.clone(), new_text)
.await
.with_context(|| format!("Failed to write settings to file {:?}", resolved_path))?;
}
let resolved_path = fs
.canonicalize(initial_path)
.await
.with_context(|| format!("Failed to canonicalize settings path {:?}", initial_path))?;
fs.atomic_write(resolved_path.clone(), new_text)
.await
.with_context(|| format!("Failed to write settings to file {:?}", resolved_path))?;
anyhow::Ok(())
})
.detach_and_log_err(cx);

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +0,0 @@
[package]
name = "telemetry_events"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lib]
path = "src/telemetry_events.rs"
[dependencies]
serde.workspace = true
util.workspace = true

View File

@@ -1 +0,0 @@
../../LICENSE-GPL

View File

@@ -1,131 +0,0 @@
use std::fmt::Display;
use serde::{Deserialize, Serialize};
use util::SemanticVersion;
#[derive(Serialize, Deserialize, Debug)]
pub struct EventRequestBody {
pub installation_id: Option<String>,
pub session_id: Option<String>,
pub is_staff: Option<bool>,
pub app_version: String,
pub os_name: String,
pub os_version: Option<String>,
pub architecture: String,
pub release_channel: Option<String>,
pub events: Vec<EventWrapper>,
}
impl EventRequestBody {
pub fn semver(&self) -> Option<SemanticVersion> {
self.app_version.parse().ok()
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct EventWrapper {
pub signed_in: bool,
pub milliseconds_since_first_event: i64,
#[serde(flatten)]
pub event: Event,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AssistantKind {
Panel,
Inline,
}
impl Display for AssistantKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Panel => "panel",
Self::Inline => "inline",
}
)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Event {
Editor(EditorEvent),
Copilot(CopilotEvent),
Call(CallEvent),
Assistant(AssistantEvent),
Cpu(CpuEvent),
Memory(MemoryEvent),
App(AppEvent),
Setting(SettingEvent),
Edit(EditEvent),
Action(ActionEvent),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct EditorEvent {
pub operation: String,
pub file_extension: Option<String>,
pub vim_mode: bool,
pub copilot_enabled: bool,
pub copilot_enabled_for_language: bool,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CopilotEvent {
pub suggestion_id: Option<String>,
pub suggestion_accepted: bool,
pub file_extension: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CallEvent {
pub operation: String,
pub room_id: Option<u64>,
pub channel_id: Option<u64>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct AssistantEvent {
pub conversation_id: Option<String>,
pub kind: AssistantKind,
pub model: String,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CpuEvent {
pub usage_as_percentage: f32,
pub core_count: u32,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct MemoryEvent {
pub memory_in_bytes: u64,
pub virtual_memory_in_bytes: u64,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ActionEvent {
pub source: String,
pub action: String,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct EditEvent {
pub duration: i64,
pub environment: String,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct SettingEvent {
pub setting: String,
pub value: String,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct AppEvent {
pub operation: String,
}

View File

@@ -133,7 +133,7 @@ impl LayoutRect {
)
.into();
cx.paint_quad(fill(Bounds::new(position, size), self.color));
cx.paint_quad(fill(Bounds::new(position, size), self.color), None, None);
}
}
@@ -782,7 +782,7 @@ impl Element for TerminalElement {
) {
let mut layout = self.compute_layout(bounds, cx);
cx.paint_quad(fill(bounds, layout.background_color));
cx.paint_quad(fill(bounds, layout.background_color), None, None);
let origin = bounds.origin + Point::new(layout.gutter, px(0.));
let terminal_input_handler = TerminalInputHandler {

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