Compare commits

..

112 Commits

Author SHA1 Message Date
Nate Butler
d0649250a3 Add an initial set of captures for blockquotes 2024-04-17 10:48:51 -04:00
Thorsten Ball
d7becce9aa git: Only show inline git blame when editor is focused (#10680)
Release Notes:

- N/A
2024-04-17 13:28:11 +02:00
Thorsten Ball
62171387f6 Do not show tooltip for editor controls if clicked (#10679)
This avoids the tooltip showing up when the context menu is visible.

It fixes this:

![screenshot-2024-04-17-13 17
41@2x](https://github.com/zed-industries/zed/assets/1185253/373bb70e-9c7f-4b9f-a928-8206697c6039)


Release Notes:

- N/A
2024-04-17 13:20:47 +02:00
Thorsten Ball
47ad010901 Backport documentation for inline git blame (#10677)
Only noticed this when editing zed.dev.

Release Notes:

- N/A
2024-04-17 13:08:04 +02:00
Piotr Osiewicz
06987edadb project panel: Fix alignment of entries overflowing the panel. (#10676)
With file icons turned off, we still reserve space for an icon and make
it invisible. However, that space was marked as flex, which made it
shrink in case subsequent file name could not fit in the current width
of the project panel. Fixes #10622



https://github.com/zed-industries/zed/assets/24362066/d565a03a-3712-49d1-bf52-407e4508a8cf


Release Notes:


- Fixed project panel entries misalignment with narrow panel & file
icons turned off.
2024-04-17 12:54:56 +02:00
Thorsten Ball
1e1a2807db Document inline git blame (#10675)
Release Notes:

- N/A
2024-04-17 12:53:53 +02:00
Bennet Bo Fenner
9782dd342f docs: Sync with zed.dev version (#10674)
This PR brings the docs in line with the version we have on
https://zed.dev

Release Notes:

- N/A
2024-04-17 12:23:30 +02:00
Keith
535bcfad10 Update crates/ui/docs/hello-world.md TODO with explanation of SharedString usage (#10664)
Filled out a comment where there was a TODO to explain SharedString
usage.

Release Notes:

- N/A
2024-04-17 13:04:28 +03:00
Thorsten Ball
c76bacb974 Rename label to toggle inline git blame on/off (#10673)
cc @iamnbutler I think we should differentiate between inline blame and
the gutter blame.

Release Notes:

- N/A
2024-04-17 11:34:34 +02:00
Kirill Bulatov
20554d0296 Fix center element wrapper size (#10672)
Fixes
https://github.com/zed-industries/zed/pull/9754#pullrequestreview-2005401133
Fixes
https://github.com/zed-industries/zed/pull/9754#issuecomment-2060536590
Closes https://github.com/zed-industries/zed/pull/10669

* Updates the docs to use a proper max value for the centered layout
padding (0.4 instead of 0.45)
* Makes the `center` wrapper (`h_flex`) to be of size of the `center`
element always, to ensure terminal lines are displayed correctly

The letter fix is somewhat hacky: while it does the right thing right
now, it does not prevent us from future mistakes like these, and does
not explain why the bottom dock could be of one, smaller, height, and
its contents, the terminal pane/terminal element/something else would
think that it has a larger height, thus breaking the scrolling and
rendering.
cc @alygin if you're interested to solve another layout-related thing.

Release Notes:

- N/A
2024-04-17 12:34:18 +03:00
Thorsten Ball
2c78cf349b Regenerate git blame info when buffer's dirty bit changed (#10670)
This fixes the https://github.com/zed-industries/zed/issues/10583 by
regenerating the blame information after the
buffer is edited. It uses a debounce of 2seconds. Meaning that undone
deletions show up again after 2secs.

Release Notes:

- Fixed `git blame` data not handling the undoing of deletions
correctly.
([#10583](https://github.com/zed-industries/zed/issues/10583)).
2024-04-17 11:25:53 +02:00
Pedro Augusto da Silva Soares
c81eb419d4 Clear credentials state and delete keychain on SignOut request (#10558)
Release Notes:

- Fixed ([#4716](https://github.com/zed-industries/zed/issues/4716)).

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-04-16 19:57:38 -06:00
Conrad Irwin
c4e446f8a8 ./script/trigger-release (#10589)
Add `./script/trigger-release {nightly|stable|preview}`

This command can be run regardless of the state of your local git
repository, and it
either triggers a workflow run of `bump_patch_version.yml` (for
stable/preview) or
it force pushes the nightly tag.

Also add some docs on releases to explain all of this.

Release Notes:

- N/A
2024-04-16 19:32:51 -06:00
Max Brunsfeld
bc7eaa6cd5 Add links to jobs page in README and in app, under help menu (#10658)
Release Notes:

- N/A

Co-authored-by: Nathan <nathan@zed.dev>
2024-04-16 15:47:23 -07:00
Nate Butler
e93d554725 Add Editor Controls Menu to Tool Bar (#10655)
This PR adds an editor controls menu to the tool bar. This menu will be
used to contain controls that toggle visual features in the editor, like
toggling inlay hints, showing git status or blame, hiding the gutter,
hiding or showing elements in the tool bar, etc.

For the moment, this consolidates the new Inline Git Blame toggle and
the old Inlay Hints toggle. In the future it will contain additional
controls.

Before: 

![CleanShot - 2024-04-16 at 16 38
53@2x](https://github.com/zed-industries/zed/assets/1714999/249e353f-786a-4391-8d49-66dd61feff8a)

After:

![CleanShot - 2024-04-16 at 16 38
43@2x](https://github.com/zed-industries/zed/assets/1714999/5b3cf4d5-855a-4475-ac05-8474b6c94b7b)

---

Release Notes:

- Added an editor controls menu to the tool bar. This will contain
visual, editor-specific options like toggling inlay hints, showing git
status or blame, etc.
- Removed the top level inlay hint toggle from the tool bar due to the
above change.
- Added the ability to toggle inline git blame from the new editor
controls menu.

---------

Co-authored-by: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com>
2024-04-16 18:03:54 -04:00
Mikayla Maki
775539b3fa Fix order of migrations from #9754 (#10657)
This fixes a bug caused by mis-ordered database migration in #9754

Release Notes:

- N/A
2024-04-16 14:29:04 -07:00
Marshall Bowers
545319bced terraform: Bump to v0.0.2 (#10653)
This PR bumps the Terraform extension to v0.0.2.

Changes:

- #10641

Release Notes:

- N/A
2024-04-16 16:08:58 -04:00
Marshall Bowers
0b2de51c37 csharp: Bump to v0.0.2 (#10651)
This PR bumps the C# extension to v0.0.2.

Changes:

- #10638

Release Notes:

- N/A
2024-04-16 15:49:16 -04:00
Marshall Bowers
9a680dafc3 clojure: Bump to v0.0.2 (#10650)
This PR bumps the Clojure extension to v0.0.2.

Changes:

- #10636

Release Notes:

- N/A
2024-04-16 15:37:54 -04:00
Marshall Bowers
4c35cfaa69 gleam: Bump to v0.1.1 (#10648)
This PR bumps the Gleam extension to v0.1.1.

Changes:

- #10635

Release Notes:

- N/A
2024-04-16 15:21:14 -04:00
Kirill Bulatov
be2bf98529 Show task summary in its terminal after it stops running (#10615)
Based on https://github.com/alacritty/alacritty/issues/7795

Unknown error code commands (now includes the interrupted ones):

![image](https://github.com/zed-industries/zed/assets/2690773/801868bc-081c-453c-a353-233d4397bda9)

Successful command:

![image](https://github.com/zed-industries/zed/assets/2690773/874377c7-c967-4a6f-8a89-ec7bf398a8b3)

Unsuccessful command:

![image](https://github.com/zed-industries/zed/assets/2690773/6c99dc5d-d324-41e9-a71b-5d0bf705de27)

The "design", including wordings and special characters, is not final,
suggestions are welcome.
The main idea was to somehow distinguish the appended lines without
occupying extra vertical space.

Release Notes:

- Added task summary output into corresponding terminal tabs
2024-04-16 22:13:35 +03:00
Andrew Lygin
4eb1e65fbb Add centered layout support (#9754)
This PR implements the Centered Layout feature (#4685):
- Added the `toggle centered layout` action.
- The centered layout mode only takes effect when there's a single
central pane.
- The state of the centered layout toggle is saved / restored between
Zed restarts.
- The paddings are controlled by the `centered_layout` setting:

```json
"centered_layout": {
  "left_padding": 0.2,
  "right_padding": 0.2
}
```

This allows us to support both the VSCode-style (equal paddings) and
IntelliJ-style (only left padding in Zen mode).

Release Notes:

- Added support for Centered Layout
([#4685](https://github.com/zed-industries/zed/pull/9754)).


https://github.com/zed-industries/zed/assets/2101250/2d5b2a16-c248-48b5-9e8c-6f1219619398

Related Issues:

- Part of #4382
2024-04-16 12:12:41 -07:00
Marshall Bowers
52591905fb lua: Bump to v0.0.2
The previous v0.0.3 bump was in error, as we hadn't published a v0.0.2 yet.
2024-04-16 14:27:05 -04:00
Marshall Bowers
d2e83cc148 lua: Bump to v0.0.3 (#10646)
This PR bumps the Lua extension to v0.0.3.

Changes:

- #10639
- #10642

Release Notes:

- N/A
2024-04-16 14:12:41 -04:00
Marshall Bowers
f633460a8d zig: Bump to v0.1.1 (#10645)
This PR bumps the Zig extension to v0.1.1.

Changes:

- #10559
- #10634

Release Notes:

- N/A
2024-04-16 14:05:12 -04:00
Peter Tripp
9470a52b5d lua: Fix broken LuaLS download on x64 (#10642)
The changes in #10437 accidentally switched 'x64' to 'x86_64' which
breaks installs on linux x64, macos x64 and windows x64. This yields the
following error:

```
[2024-04-16T12:58:01-04:00 ERROR project] failed to start language server "lua-language-server": no asset found matching "lua-language-server-3.7.4-darwin-x86_64.tar.gz"
[2024-04-16T12:58:01-04:00 ERROR project] server stderr: Some("")
```

It's trying to download: 
`lua-language-server-3.7.4-darwin-x86_64.tar.gz`
which should be
`lua-language-server-3.7.4-darwin-x64.tar.gz`

See [LuaLS release
page](https://github.com/LuaLS/lua-language-server/releases/tag/3.6.25).

CC: @maxbrunsfeld

lua.rs before ef4c70c:

c6028f6651/crates/languages/src/lua.rs (L35)

lua.rs after:

5d7148bde1/extensions/lua/src/lua.rs (L49)

Release Notes:

- N/A
2024-04-16 13:48:45 -04:00
Marshall Bowers
fa0302f156 terraform: Don't cache user-installed terraform-ls (#10641)
This PR updates the Terraform extension to not cache the binary when it
is using the one on the $PATH.

Release Notes:

- N/A
2024-04-16 13:40:13 -04:00
Marshall Bowers
5d7148bde1 lua: Don't cache user-installed lua-language-server (#10639)
This PR updates the Lua extension to not cache the binary when it is
using the one on the $PATH.

Release Notes:

- N/A
2024-04-16 13:22:42 -04:00
Marshall Bowers
58991f332b csharp: Don't cache user-installed OmniSharp (#10638)
This PR updates the C# extension to not cache the binary when it is
using the one on the $PATH.

Release Notes:

- N/A
2024-04-16 13:22:36 -04:00
Marshall Bowers
9c569c8d95 zig: Rename cached_binary to cached_binary_path (#10637)
This PR renames the `cached_binary` field on the `ZigExtension` back to
`cached_binary_path` to make it match the other extensions.

Release Notes:

- N/A
2024-04-16 13:18:21 -04:00
Marshall Bowers
1ba0bf925b clojure: Don't cache user-installed clojure-lsp (#10636)
This PR updates the Clojure extension to not cache the binary when it is
using the one on the $PATH.

Release Notes:

- N/A
2024-04-16 13:12:08 -04:00
Marshall Bowers
53105ddd16 gleam: Don't cache user-installed gleam (#10635)
This PR updates the Gleam extension to not cache the binary when it is
using the one on the $PATH.

Release Notes:

- N/A
2024-04-16 13:11:58 -04:00
Thorsten Ball
210f8ebfed zig: Do not cache user-installed zls (#10634)
This was a bug introduced when moving to extensions: when we find a
binary in the user's project environment, we shouldn't cache that
globally since it might not work for other projects.

See also: https://github.com/zed-industries/zed/pull/10559

Release Notes:


- N/A
2024-04-16 19:05:11 +02:00
Thorsten Ball
c015b5c4cd Enable inline-git-blame by default (#10632)
Release Notes:

- N/A
2024-04-16 18:53:57 +02:00
Henrique Ferreiro
c1c8a74c7f Add ability to specify binary path/args for clangd (#10608)
This uses the language server settings added in #9293 to allow users to
specify the binary path and arguments with which to start up `clangd`.

Example user settings for `clangd`:

```json
{
  "lsp": {
    "clangd": {
      "binary": {
        "path": "/usr/bin/clangd",
        "arguments": ["--log=verbose"]
      },
    }
  }
}
```

Constraints:

* Right now this only allows ABSOLUTE paths.

Release Notes:

- Added ability to specify `clangd` binary `path` (must be absolute) and
`arguments` in user settings. Example: `{"lsp": {"clangd": {"binary":
{"path": "/usr/bin/clangd", "arguments": ["--log=verbose"] }}}}`
2024-04-16 18:17:15 +02:00
Peter Tripp
2f00fcbdf6 docs: use_autoclose (#10514)
Add some keywords (bracket, quote, etc)to the comments describing
`use_autoclose` preference in the settings json.

This setting took me a while to find -- so now it'll be more easily
searchable for others.

Release Notes:

- N/A
2024-04-16 18:17:04 +02:00
Thorsten Ball
5c5fb972d0 Handle setting git blame delay to 0 (#10626)
Release Notes:


- N/A
2024-04-16 17:25:24 +02:00
Piotr Osiewicz
7928095951 chore: parse cli args just once in zed crate (#10613)
Release Notes:

- N/A
2024-04-16 16:27:45 +02:00
Thorsten Ball
70c3ca4fdd Fix toggling of inline git blame when it's delayed (#10620)
Release Notes:

- N/A
2024-04-16 16:07:22 +02:00
Kirill Bulatov
d49271a112 Use project search action with the default keybinding in app menus (#10618)
Fixes https://github.com/zed-industries/zed/issues/10611 

Zed has `workspace::NewSearch` (without a default keybinding) and
`workspace::DeploySearch` (with the default keybinding for its
`DeploySearch::find()` form).

Use the one with the keybinding, as it's the whole point of the menu.

Release Notes:

- Fixed "Find In Project" app menu item having no keybinding
([10611](https://github.com/zed-industries/zed/issues/10611))
2024-04-16 16:47:30 +03:00
Piotr Osiewicz
e34c443331 lsp: Do not set error/result fields if they're missing. (#10610)
We were previously not conforming to LSP spec, as we were setting
**both** result field and error field on response fields, which could
confuse servers. Excerpt from the spec:
> * The result of a request. This member is REQUIRED on success.
> * This member MUST NOT exist if there was an error invoking the
method.

Fixes #10595

Release Notes:

- N/A
2024-04-16 14:27:12 +02:00
Kirill Bulatov
263023021d Show Zoom In/Out shortcuts in the labels (#10604)
Based on https://github.com/zed-industries/zed/discussions/10599 
Does the same as the assistant tab with the Zoom In/Out labels.


![image](https://github.com/zed-industries/zed/assets/2690773/afc59a3e-c3df-4fc8-bcaf-1d45a21aecf7)

Release Notes:

- Adjusted Zoom In/Out for Pane and Terminal Pane to show keybinding
labels
2024-04-16 14:00:25 +03:00
Thorsten Ball
7e1a184446 Fix Markdown code rendering in tooltips ignoring languages (#10607)
Some code blocks that are returned in tooltips (returned by language
servers, for example) use the language file extension as the language in
the the triple-backtick code blocks.

Example:

    ```rs
    fn rust_code() {}
    ```

    ```cpp
    fn rust_code() {}
    ```

Before this change we only looked up the language with the
`rs`/`cpp`/... being interpreted as the language name. Now we also treat
it as a possible file extension.



Release Notes:

- Fixed Markdown code blocks in tooltips not having correct language
highlighting.

Before:


![image](https://github.com/zed-industries/zed/assets/1185253/1f3870a6-467c-4e5f-9e49-1ff32240d10f)

After:

![screenshot-2024-04-16-12 43
39@2x](https://github.com/zed-industries/zed/assets/1185253/21a45ed5-825a-412d-9dc0-35a444fc64ba)

Co-authored-by: Bennet <bennetbo@gmx.de>
2024-04-16 12:49:35 +02:00
Thorsten Ball
c834ea75ef Fix --- in Markdown docs not rendered correctly (#10606)
This fixes #10511 by turning off the YAML metadata block rendering in
the Markdown parser.

`clangd` uses `---` as dividers, but our parser interpreted it as a YAML
metadata block, even though it didn't contain any valid YAML.

Example Markdown from `clangd`:

    ### instance-method `format`

    ---
    → `void`
    Parameters:
    - `const int &`
    - `const std::tm &`
    - `int & dest`

    ---
    ```cpp
    // In my_formatter_flag
    public: void format(const int &, const std::tm &, int &dest)
    ```

What's between the two `---` is *not* valid YAML. Neovim, too,
interprets these as dividers and renders them as such.

And since we don't handle any possible metadata anyway, we can turn off
the metadata handling, which causes the parser to interpret the `---` as
dividers.



Release Notes:

- Fixed Markdown returned by `clangd` being rendered the wrong way.
([#10511](https://github.com/zed-industries/zed/issues/10511)).

Before:

![screenshot-2024-04-16-12 32
15@2x](https://github.com/zed-industries/zed/assets/1185253/a268f106-9504-48aa-9744-42a7521de807)

After:

![screenshot-2024-04-16-12 33
02@2x](https://github.com/zed-industries/zed/assets/1185253/dd178a63-a075-48a9-85d9-565157a5b050)

Co-authored-by: Bennet <bennetbo@gmx.de>
2024-04-16 12:40:13 +02:00
Thorsten Ball
4d8cba2add Fix long git author names/emails overflowing blame tooltip (#10605)
This fixes https://github.com/zed-industries/zed/issues/10581.

Release Notes:

- N/A

Co-authored-by: Bennet <bennetbo@gmx.de>
2024-04-16 12:03:45 +02:00
Thorsten Ball
08aef198d5 Fix inline blame annotations handling wrapped lines (#10600)
Fixes inline blame not being displayed correctly for soft-wrapped lines.

(Can't find the ticket)
![screenshot-2024-04-16-10 50
29](https://github.com/zed-industries/zed/assets/1185253/e3ff9018-f796-469a-9d42-5997baf7d2f6)


Release Notes:

- N/A
2024-04-16 11:27:23 +02:00
Hans
2cfb1ffa77 add a setting to control show/hide terminal button for status bar (#10593)
Release Notes:

- Added a setting to show/hide the terminal button in the status bar:
`{"terminal": {"button": false}}` to hide it. (#10513)

---------

Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
2024-04-16 09:40:32 +02:00
Andrew Lygin
f3192b6fa6 Fix scrollbar marker settings (#10530)
Zed displays scrollbar markers of three types: git diffs, background
highlights and diagnostics. At the moment, the "background highlights"
markers are displayed for all the supported highlights:

- Occurences of the symbol under cursor.
- Search results.
- Scope boundaries (only works when a symbol is selected).
- Active hover popover position.

They all use the same color, which leads to confusing results. For
instance, in the following case I expect to see markers for the
`new_anchor` occurences in lines 43 and 47. But besides them, there're
also scope-markers for `ScrollAnchor` initialization block in lines 46
and 49, which makes me think that there're four places where
`new_anchor` appears.

<img width="740" alt="zed-scrollbar-markers"
src="https://github.com/zed-industries/zed/assets/2101250/78700e6b-fdd1-4c2f-beff-e564d8defc13">

Existing settings `selection` and `symbol_selection` in the `scrollbar`
section [don't work as
expected](https://github.com/zed-industries/zed/pull/10080#discussion_r1552325493),
which increases confusion.

This PR only leaves two types of bg-highlight-markers and provides
dedicated settings for them:

- Occurences of the symbol under cursor. Setting: `selected_symbol`,
default is `true`.
- Search results. Setting: `search_results`, default is `true`.

The `selection` and `symbol_selection` settings are not used anymore.

Release Notes:

- Breaking changes. Settings `selection` and `symbol_selection` in the
`scrollbar` section renamed to `search_results` and `selected_symbol`
respectively. Fixed the effect of these settings on which markers are
displayed on the scrollbar.

Optionally, include screenshots / media showcasing your addition that
can be included in the release notes.

- N/A

/cc @mrnugget
2024-04-16 09:21:22 +02:00
Conrad Irwin
33b9aca090 Add a bump patch version workflow (#10588)
I'd like to make it less clunky to release a new patch version of
preview/stable.

Release Notes:

- N/A
2024-04-15 20:31:12 -06:00
Kirill Bulatov
57b087e41e On label conflict, prefer resolved tasks with never templates (#10586)
This way, static (and other) templates that change, e.g. env vars, will
get the new template resolved task in the modal to run.

Release Notes:

- N/A
2024-04-16 03:31:33 +03:00
Kyle Kelley
2a9ce3cec3 Handle no active editor available (#10503)
Cleaned up a stray `.unwrap()`

Release Notes:

- N/A
2024-04-15 16:53:08 -07:00
Kyle Kelley
f5c2483423 Close the backticks in gpui geometry examples (#10579)
I noticed some of the examples in `crates/gpui/src/geometry.rs` were
missing ending triple backticks.

Release Notes:

- N/A
2024-04-15 16:52:19 -07:00
Mikayla Maki
4d314b2dd0 Increase the context in other multibuffers (#10582)
This moves the diagnostics and find all references to be in line with
the search pane. This also centralizes the constant into the editor code
base.

Release Notes:

- Increased diagnostic context to match the project search context.
2024-04-15 14:44:14 -07:00
Abdullah Alsigar
7a112b22ac dart: Use upstream tree-sitter-dart (#10552)
Update `tree-sitter-dart` to the upstream package, there was an issue
building Rust package which is resolved now.


Release Notes:


- N/A
2024-04-15 17:32:35 -04:00
Marshall Bowers
575eb792fb Remove stray word in CONTRIBUTING.md 2024-04-15 16:30:18 -04:00
Nathan Sobo
4f776f9ebe Don't make -l imply -d in bundle-mac script (#10438)
If you want a debug build, you can easily pass `-ld`.

Release Notes:

- N/A
2024-04-15 14:29:45 -06:00
Marshall Bowers
0c77e1ce45 Disable extension entries when the corresponding dev extension is installed (#10580)
This PR updates the extension list to disable remote entries when the
corresponding dev extension is installed.

Here is what this looks like:

<img width="1189" alt="Screenshot 2024-04-15 at 4 09 45 PM"
src="https://github.com/zed-industries/zed/assets/1486634/48bb61d4-bc85-4ca6-b233-716831dfa7d8">


Release Notes:

- Disabled extension entries when there is a development copy of that
extension installed.
2024-04-15 16:27:54 -04:00
Conrad Irwin
904b740e16 More vim-like regexes (#10577)
Fixes:  #10539

Release Notes:

- vim: Use `\<` `\>` instead of `\b`
2024-04-15 14:26:05 -06:00
Conrad Irwin
f2fc84ab44 Revert change to tracing (#10578)
Although we thought this fixed the bug, it just worked around it, and
runnign two copies of tracing in one app is a bad idea.

Simplify default RUST_LOG in development to avoid
 https://github.com/tokio-rs/tracing/issues/2927#issuecomment-2040080189



Release Notes:

- N/A
2024-04-15 14:00:56 -06:00
Conrad Irwin
fda3c91f16 bump libgit2 (#10561)
Although this probably doesn't fix anything by itself, it'll make it
easier to fix https://github.com/libgit2/libgit2/issues/6795

Release Notes:

- N/A
2024-04-15 13:21:30 -06:00
apricotbucket28
3eb8464d19 wayland: Improve cursor (#10516)
Fixes the cursor not updating when (a) switching windows from another
program via a shortcut and (b) when cursor updates were triggered by
something other than moving the mouse (e.g. when scrolling or pressing a
key).

Release Notes:

- N/A
2024-04-15 12:09:24 -07:00
张小白
58f57491b1 windows: Remove last_ime_input (#10506)
It seems that windows always report IME composition string, so there is
no need to store this string manually.

Release Notes:

- N/A
2024-04-15 12:08:38 -07:00
Marshall Bowers
3e44e97177 Install the latest compatible version of an extension when clicking "Install" (#10575)
This PR makes it so clicking "Install" will install the latest
compatible version of an extension instead of disabling the button when
the latest version is not compatible.

The "Upgrade" button will still be disabled when the latest version is
not compatible. We will also now display a tooltip to better indicate
why the button is disabled:

<img width="607" alt="Screenshot 2024-04-15 at 2 41 26 PM"
src="https://github.com/zed-industries/zed/assets/1486634/16ad516e-1c0c-4505-b994-158ea655641b">

Related to https://github.com/zed-industries/zed/issues/10509.

Release Notes:

- Changed the "Install" button for extensions to always install the
latest compatible version instead of becoming disabled when the latest
version of an extension is incompatible with the current Zed version.
2024-04-15 15:06:07 -04:00
Marshall Bowers
fda21232ae Indicate which extension version is installed when on an older version (#10574)
This PR adds an indicator to show what extension version is currently
installed when on an extension version that is not the latest.

<img width="1156" alt="Screenshot 2024-04-15 at 2 10 33 PM"
src="https://github.com/zed-industries/zed/assets/1486634/61c5e4cf-a0b8-48fc-8e52-f04f1c351794">

Release Notes:

- Added an indicator to show the currently-installed extension version
when not on the latest version.
2024-04-15 14:30:53 -04:00
Antonio Scandurra
57a736d74a Fuse iterator supplied to SumTree::from_iter (#10571)
This fixes an issue that could cause `from_iter` to never finish if the
underlying iterator restarted after returning `None` for the first time.

We only saw this in development but I wanna cherry-pick it to stable and
preview, just in case.

Release Notes:

- N/A

Co-authored-by: Kyle <kylek@zed.dev>
2024-04-15 20:09:43 +02:00
Max Brunsfeld
015e2ecd19 Remove built-in Nu support in favor of extension (#10570)
Release Notes:

- Removed built-in Nu language support in favor of an extension.

---------

Co-authored-by: Marshall <marshall@zed.dev>
2024-04-15 10:47:01 -07:00
Kyle Kelley
5037f466f6 Defer first update active buffer for conversation (#10564)
This fixes when the workspace is not actually available for a
`.read(cx)`.

Release Notes:

- Fix a panic when quoting a selection before the assistant panel has
been started

Co-authored-by: Conrad Irwin <conrad@zed.dev>
2024-04-15 11:11:02 -06:00
Marshall Bowers
f28fde5e58 Move constraints to query parameters for GET /extensions/:extension_id/download (#10562)
This PR fixes a bug where the constraints provided when downloading the
latest version of an extension were not being read properly.

These constraints are passed in the query string, but `collab` was
attempting to read them from the path.

This should fix https://github.com/zed-industries/zed/issues/10484, once
it is deployed.


Release Notes:

- N/A
2024-04-15 13:06:20 -04:00
Thorsten Ball
d1928f084e Remove TODO by defining constant (#10556)
Release Notes:

- N/A
2024-04-15 18:37:03 +02:00
Piotr Osiewicz
ad22bddffa diagnostics: Update diagnostics more eagerly (#10560)
Here comes a lenghty explanation for a short commit: We've had feedback
that our diagnostics tab often mismatches what's shown in the status
bar. E.g: https://x.com/fasterthanlime/status/1778764747732594753 Let's
dive into the lifetime of diagnostic tab first; it is actually spawned
*just once per workspace*, the first time you click on the diagnostics
status indicator. Even if you close this tab, we still reuse the same
object under the hood later on. This has upsides, as it means that you
can close a tab and then reopen it with your selections still in-tact
and so on. However, this also leads to the perceived staleness.
Crucially, the first time ever in a given session that you spawn the
diagnostics tab, the status bar counts match the content of a tab. That
is because we always call \`update_excerpts\` when we create diagnostics
tab for the first time, but later on we have severe constraints on when
we want to update the excerpts in diagnostics tab, mostly centered
around presence of selections in an editor... but, since we reuse the
diagnostic tab object under the hood, we're always gonna have at least
one selection in an editor sans the first time you open it. The end
result is that in order for diagnostic tab contents to be updated, we
have to get a "on-disk-diagnostics-finished" notification from language
server, which can take a long time.
Another example of this property manifesting itself is that if you fix a
diagnostic warning/error, it takes a while for diagnostic tab to reflect
it.

With this PR, I've afforded a bit of leniency in refreshing the contents
of that tab. The old check that discarded updates when diagnostics
editor had at least one selection has been updated to instead reject
multicursors; this is still overly conservative, as I'm not yet sure how
big of an issue is the cursor that's jumping around (as that's what the
selections constraint is supposed to prevent).



Release Notes:

- Fixed diagnostics tab showing outdated entries before the language
server is done with it's analysis.
2024-04-15 18:28:58 +02:00
Thorsten Ball
da0d968a2c zig: Use env if using zls from shell env (#10559)
This fixes the problem of the Zig extension picking up `zls` from the
shell env but `zls` then failing to launch because it cannot find `zig`.

Scenario in which this happens:
- `.envrc` in a project that sets `$PATH` up
- in that `$PATH` there's `zls` and `zig`
- Zed is started from Dock
- Project is opened
- Shell env from project directory is loaded and used to get to `zls`
- `zls` is then started, without that environment set on the process
- `zls` cannot find `zig`

Release Notes:

- N/A

Co-authored-by: Marshall <marshall@zed.dev>
2024-04-15 17:39:07 +02:00
Antonio Scandurra
200e36311c Intersect content mask with hitbox bounds only during hit test (#10554)
This fixes a bug that caused the editor to be rendered incorrectly when
its bounds extended outside the content mask. This is because the editor
uses the returned `Hitbox` bounds to determine the origin of its
elements.

With this commit, we will now store a new `content_mask` field within
the `Hitbox` struct which is captured when the hitbox is inserted. Then,
the content mask is applied on the fly when performing a hit test to
determine whether the hitbox is actually hovered.

Release Notes:

- N/A
2024-04-15 15:09:15 +02:00
Kirill Bulatov
db48c75231 Add basic bash and Python tasks (#10548)
Part of https://github.com/zed-industries/zed/issues/5141

* adds "run selection" and "run file" tasks for bash and Python.
* replaces newlines with `\n` symbols in the human-readable task labels
* properly escapes task command arguments when spawning the task in
terminal

Caveats:

* bash tasks will always use user's default shell to spawn the
selections, but they should rather respect the shebang line even if it's
not selected
* Python tasks will always use `python3` to spawn its tasks now, as
there's no proper mechanism in Zed to deal with different Python
executables

Release Notes:

- Added tasks for bash and Python to execute selections and open files
in terminal
2024-04-15 16:07:21 +03:00
Carter Olsen
1911a9f39b Add a setting to control the vertical and horizontal scroll sensitivity (#10244)
Some people (like myself) use touchpads for development and I find Zed's
default scroll sensitivity to be slower than I like. This change adds a
scroll sensitivity multiplier that allows users to customize the speed
of their scrolling.

Release Notes:

- Added a setting under "scroll_sensitivity" that allows user to control
the scroll sensitivity. This value acts as a multiplier for the
horizontal and vertical scroll speed.
2024-04-15 14:40:09 +02:00
Thorsten Ball
faebce8cd0 Inline git blame (#10398)
This adds so-called "inline git blame" to the editor that, when turned
on, shows `git blame` information about the current line inline:


![screenshot-2024-04-15-11 29
35@2x](https://github.com/zed-industries/zed/assets/1185253/21cef7be-3283-4556-a9f0-cc349c4e1d75)


When the inline information is hovered, a new tooltip appears that
contains more information on the current commit:


![screenshot-2024-04-15-11 28
24@2x](https://github.com/zed-industries/zed/assets/1185253/ee128460-f6a2-48c2-a70d-e03ff90a737f)

The commit message in this tooltip is rendered as Markdown, is
scrollable and clickable.

The tooltip is now also the tooltip used in the gutter:

![screenshot-2024-04-15-11 28
51@2x](https://github.com/zed-industries/zed/assets/1185253/42be3d63-91d0-4936-8183-570e024beabe)


## Settings

1. The inline git blame information can be turned on and off via
settings:
```json
{
  "git": {
    "inline_blame": {
      "enabled": true
    }
  }
}
```
2. Optionally, a delay can be configured. When a delay is set, the
inline blame information will only show up `x milliseconds` after a
cursor movement:
```json
{
  "git": {
    "inline_blame": {
      "enabled": true,
      "delay_ms": 600
    }
  }
}
```
3. It can also be turned on/off for the current buffer with `editor:
toggle git blame inline`.

## To be done in follow-up PRs

- [ ] Add link to pull request in tooltip
- [ ] Add avatars of users if possible

## Release notes

Release Notes:

- Added inline `git blame` information the editor. It can be turned on
in the settings with `{"git": { "inline_blame": "on" } }` for every
buffer or, temporarily for the current buffer, with `editor: toggle git
blame inline`.
2024-04-15 14:21:52 +02:00
Kirill Bulatov
573ba83034 Merge Zed task context providing logic (#10544)
Before, `tasks_ui` set most of the context with `SymbolContextProvider`
providing the symbol data part of the context. Now, there's a
`BasicContextProvider` that forms all standard Zed context and it
automatically serves as a base, with no need for other providers like
`RustContextProvider` to call it as before.

Also, stop adding `SelectedText` task variable into the context for
blank text selection.

Release Notes:

- N/A
2024-04-15 11:52:15 +03:00
Marshall Bowers
97c5cffbe3 Update contributing docs to point to extensions (#10537)
This PR updates the contributing docs to remove an outdated section
about extension support and instead point to the extension authoring
docs.

Release Notes:

- N/A
2024-04-14 19:31:19 -04:00
apricotbucket28
556ecd94c2 blade: Fix incorrect texture format (#10524)
Fixes image rendering
Closes https://github.com/zed-industries/zed/issues/10505

Before:

![image](https://github.com/zed-industries/zed/assets/71973804/3a903279-d631-4ca6-9f46-3065c7ed3073)


After:

![image](https://github.com/zed-industries/zed/assets/71973804/ab3a73e5-bf21-4df7-a9c1-a74bd1993a5b)


Release Notes:

- N/A
2024-04-14 11:46:31 -07:00
Mikayla Maki
3289188e0a linux: Simplify scrolling implementation (#10497)
This PR adjusts our scrolling implementation to delay the generation of
ScrollWheel events until we receive a complete frame.

Note that our implementation is still a bit off-spec, as we don't delay
any other kind of events. But it's been working so far on a variety of
compositors and the other events contain complete data; so I'll hold off
on that refactor for now.

Release Notes:

- N/A
2024-04-12 15:54:11 -07:00
Max Brunsfeld
5e4f707951 Change authors of lua extension 2024-04-12 15:15:40 -07:00
Conrad Irwin
5d7642d77d Update netrw bindings for preview panes (#10492)
Release Notes:

- N/A
2024-04-12 14:50:27 -06:00
Conrad Irwin
e64ecdc9ab Add missing block.copy() (#10496)
https://crates.io/crates/block implies this is necessary, and we're
still seeing segfaults in this method, so...

Release Notes:

- Fixed a panic when installing the CLI / registering for the zed://
protocol
2024-04-12 14:50:12 -06:00
Conrad Irwin
ba9c5929af Fix emojis when rendering with the system ui font (#10491)
Release Notes:

- N/A
2024-04-12 14:05:01 -06:00
Marshall Bowers
ad8dd1771a lua: Fix extension version (#10490)
This PR fixes the Lua extension version to be v0.0.1 instead of v0.1.0
for the initial release.

Release Notes:

- N/A
2024-04-12 15:38:26 -04:00
usr
cb6d0639db Windows: Fix crash when trying to copy nothing to clipboard (#10405)
Release Notes:

- N/A
2024-04-12 12:33:20 -07:00
Mikayla Maki
065f15e9a6 Use buffer font when rendering editor breadcrumbs and diagnostics (#10488)
Before:

<img width="592" alt="Screenshot 2024-04-12 at 12 00 00 PM"
src="https://github.com/zed-industries/zed/assets/2280405/3251743e-4f2c-4ca3-9bc5-88f37660f7b9">

After:

<img width="673" alt="Screenshot 2024-04-12 at 12 11 37 PM"
src="https://github.com/zed-industries/zed/assets/2280405/6a8ac597-261a-45d9-bf2a-a673b6f26b0e">


Release Notes:

- N/A
2024-04-12 12:29:00 -07:00
张小白
104558115f windows: Update WindowsDisplay::frequency() (#10476)
A subsequent update introduced the `HMONITOR` value to the
`WindowsDisplay` struct, eliminating the need for polling to retrieve
this value.

Release Notes:

- N/A
2024-04-12 12:19:49 -07:00
CharlesChen0823
4e6f24a841 Only emit resize event when size changed (#10419)
Currently, terminal will emit resize event every seconds, even if the
size not changed.
this PR fixed only emit resize event when size is changed.

Release Notes:

- N/A
2024-04-12 12:18:56 -07:00
Marshall Bowers
f3a78f613a Extract Vue extension (#10486)
This PR extracts Vue support into an extension and removes the built-in
C# support from Zed.

Release Notes:

- Removed built-in support for Vue, in favor of making it available as
an extension. The Vue extension will be suggested for download when you
open a `.vue` file.

---------

Co-authored-by: Max <max@zed.dev>
2024-04-12 14:39:27 -04:00
Yury Abykhodau
8bca9cea26 Fix Auto folded dirs performance issues (#8556)
Fixed auto folded dirs which caused significant performance issues #8476
(#7674)

Moved from iterating over snapshot entries to use `child_entries`
function from `worktree.rs` by making it public

@maxbrunsfeld 

Release Notes:

- Fixed a bug where project panel settings changes would not be applied
immediately.
- Added a `project_panel.auto_fold_dirs` setting which collapses the
nesting in the project panel when there is a chain of folders containing
a single folder.
<img width="288" alt="Screenshot 2024-04-12 at 11 10 58 AM"
src="https://github.com/zed-industries/zed/assets/2280405/efd61e75-026c-464d-ba4d-90db5f68bad3">

---------

Co-authored-by: Mikayla <mikayla@zed.dev>
2024-04-12 11:26:26 -07:00
Kirill Bulatov
28586060a1 Display more specific tasks above in the modal (#10485) 2024-04-12 20:19:11 +02:00
Kyle Kelley
49371b44cb Semantic Index (#10329)
This introduces semantic indexing in Zed based on chunking text from
files in the developer's workspace and creating vector embeddings using
an embedding model. As part of this, we've created an embeddings
provider trait that allows us to work with OpenAI, a local Ollama model,
or a Zed hosted embedding.

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

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

Remaining todo:

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


Release Notes:

- N/A

---------

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

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

After:

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

Release Notes:

- Fixed window title special characters display on X11.

---------

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

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

Release Notes:

- N/A

---------

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

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

Release Notes:

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

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

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

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

Release Notes:

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

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

Release Notes:

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

Release Notes:

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


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

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

Release Notes:

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

Release Notes:

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

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

Release Notes:

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


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

---

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


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

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


Release Notes:

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


Release Notes:

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

Release Notes:

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

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

Release Notes:

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

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

Release Notes:

- N/A

---------

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

Release notes:

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

Release Notes:

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

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

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

---------

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

Release Notes:

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

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

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

This commit introduces a new feature that displays the current partial
command in the vim mode, similar to the behavior in Vim plugin. This
helps users keep track of the commands they're entering.
2024-04-12 06:39:57 +02:00
194 changed files with 6787 additions and 2111 deletions

View File

@@ -0,0 +1,49 @@
name: bump_patch_version
on:
workflow_dispatch:
inputs:
branch:
description: "Branch name to run on"
required: true
concurrency:
# Allow only one workflow per any non-`main` branch.
group: ${{ github.workflow }}-${{ github.event.input.branch }}
cancel-in-progress: true
jobs:
bump_patch_version:
runs-on:
- self-hosted
- test
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
ref: ${{ github.event.inputs.branch }}
ssh-key: ${{ secrets.ZED_BOT_DEPLOY_KEY }}
- name: Bump Patch Version
run: |
set -eux
channel=$(cat crates/zed/RELEASE_CHANNEL)
tag_suffix=""
case $channel in
stable)
;;
preview)
tag_suffix="-pre"
;;
*)
echo "this must be run on either of stable|preview release branches" >&2
exit 1
;;
esac
which cargo-set-version > /dev/null || cargo install cargo-edit --features vendored-openssl
output=$(cargo set-version -p zed --bump patch 2>&1 | sed 's/.* //')
git commit -am "Bump to $output for @$GITHUB_ACTOR" --author "Zed Bot <hi@zed.dev>"
git tag v${output}${tag_suffix}
git push origin HEAD v${output}${tag_suffix}

View File

@@ -11,7 +11,7 @@ If you're looking for ideas about what to work on, check out:
- Our [public roadmap](https://zed.dev/roadmap) contains a rough outline of our near-term priorities for Zed.
- Our [top-ranking issues](https://github.com/zed-industries/zed/issues/5393) based on votes by the community.
Outside of a handful of extremely popular languages and themes, we are generally not looking to extend Zed's language or theme support by directly building them into Zed. We really want to build a plugin system to handle making the editor extensible going forward. If you are passionate about shipping new languages or themes we suggest contributing to the extension system to help us get there faster.
For adding themes or support for a new language to Zed, check out our [extension docs](https://github.com/zed-industries/extensions/blob/main/AUTHORING_EXTENSIONS.md).
## Proposing changes

346
Cargo.lock generated
View File

@@ -520,7 +520,7 @@ dependencies = [
"polling 3.3.2",
"rustix 0.38.32",
"slab",
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"tracing",
"windows-sys 0.52.0",
]
@@ -861,7 +861,7 @@ dependencies = [
"ring 0.17.7",
"time",
"tokio",
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"tracing",
"zeroize",
]
@@ -897,7 +897,7 @@ dependencies = [
"http-body",
"percent-encoding",
"pin-project-lite",
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"tracing",
"uuid",
]
@@ -926,7 +926,7 @@ dependencies = [
"once_cell",
"percent-encoding",
"regex-lite",
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"tracing",
"url",
]
@@ -949,7 +949,7 @@ dependencies = [
"http 0.2.9",
"once_cell",
"regex-lite",
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"tracing",
]
[[package]]
@@ -971,7 +971,7 @@ dependencies = [
"http 0.2.9",
"once_cell",
"regex-lite",
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"tracing",
]
[[package]]
@@ -994,7 +994,7 @@ dependencies = [
"http 0.2.9",
"once_cell",
"regex-lite",
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"tracing",
]
[[package]]
@@ -1022,7 +1022,7 @@ dependencies = [
"sha2 0.10.7",
"subtle",
"time",
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"tracing",
"zeroize",
]
@@ -1055,7 +1055,7 @@ dependencies = [
"pin-project-lite",
"sha1",
"sha2 0.10.7",
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"tracing",
]
[[package]]
@@ -1087,7 +1087,7 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"pin-utils",
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"tracing",
]
[[package]]
@@ -1131,7 +1131,7 @@ dependencies = [
"pin-utils",
"rustls",
"tokio",
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"tracing",
]
[[package]]
@@ -1146,7 +1146,7 @@ dependencies = [
"http 0.2.9",
"pin-project-lite",
"tokio",
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"tracing",
"zeroize",
]
@@ -1194,7 +1194,7 @@ dependencies = [
"aws-smithy-types",
"http 0.2.9",
"rustc_version",
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"tracing",
]
[[package]]
@@ -1517,7 +1517,7 @@ dependencies = [
"futures-io",
"futures-lite 2.2.0",
"piper",
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"tracing",
]
[[package]]
@@ -1578,17 +1578,6 @@ dependencies = [
"workspace",
]
[[package]]
name = "bromberg_sl2"
version = "0.6.0"
source = "git+https://github.com/zed-industries/bromberg_sl2?rev=950bc5482c216c395049ae33ae4501e08975f17f#950bc5482c216c395049ae33ae4501e08975f17f"
dependencies = [
"digest 0.9.0",
"lazy_static",
"rayon",
"seq-macro",
]
[[package]]
name = "bstr"
version = "1.6.2"
@@ -2286,7 +2275,7 @@ dependencies = [
"toml 0.8.10",
"tower",
"tower-http 0.4.4",
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"tracing",
"tracing-subscriber",
"unindent",
"util",
@@ -3276,6 +3265,15 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650"
[[package]]
name = "doxygen-rs"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "415b6ec780d34dcf624666747194393603d0373b7141eef01d12ee58881507d9"
dependencies = [
"phf",
]
[[package]]
name = "dwrote"
version = "0.11.0"
@@ -4096,6 +4094,17 @@ dependencies = [
"futures-util",
]
[[package]]
name = "futures-batch"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f444c45a1cb86f2a7e301469fd50a82084a60dadc25d94529a8312276ecb71a"
dependencies = [
"futures 0.3.28",
"futures-timer",
"pin-utils",
]
[[package]]
name = "futures-channel"
version = "0.3.30"
@@ -4191,6 +4200,12 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
[[package]]
name = "futures-timer"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
[[package]]
name = "futures-util"
version = "0.3.30"
@@ -4316,11 +4331,11 @@ dependencies = [
[[package]]
name = "git2"
version = "0.15.0"
version = "0.18.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2994bee4a3a6a51eb90c218523be382fd7ea09b16380b9312e9dbe955ff7c7d1"
checksum = "232e6a7bfe35766bf715e55a88b39a700596c0ccfd88cd3680b4cdb40d66ef70"
dependencies = [
"bitflags 1.3.2",
"bitflags 2.4.2",
"libc",
"libgit2-sys",
"log",
@@ -4558,7 +4573,7 @@ dependencies = [
"slab",
"tokio",
"tokio-util",
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"tracing",
]
[[package]]
@@ -4670,6 +4685,41 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "heed"
version = "0.20.0-alpha.9"
source = "git+https://github.com/meilisearch/heed?rev=036ac23f73a021894974b9adc815bc95b3e0482a#036ac23f73a021894974b9adc815bc95b3e0482a"
dependencies = [
"bitflags 2.4.2",
"byteorder",
"heed-traits",
"heed-types",
"libc",
"lmdb-master-sys",
"once_cell",
"page_size",
"serde",
"synchronoise",
"url",
]
[[package]]
name = "heed-traits"
version = "0.20.0-alpha.9"
source = "git+https://github.com/meilisearch/heed?rev=036ac23f73a021894974b9adc815bc95b3e0482a#036ac23f73a021894974b9adc815bc95b3e0482a"
[[package]]
name = "heed-types"
version = "0.20.0-alpha.9"
source = "git+https://github.com/meilisearch/heed?rev=036ac23f73a021894974b9adc815bc95b3e0482a#036ac23f73a021894974b9adc815bc95b3e0482a"
dependencies = [
"bincode",
"byteorder",
"heed-traits",
"serde",
"serde_json",
]
[[package]]
name = "hermit-abi"
version = "0.1.19"
@@ -4834,7 +4884,7 @@ dependencies = [
"socket2 0.4.9",
"tokio",
"tower-service",
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"tracing",
"want",
]
@@ -5149,7 +5199,7 @@ dependencies = [
"polling 2.8.0",
"slab",
"sluice",
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"tracing",
"tracing-futures",
"url",
"waker-fn",
@@ -5437,7 +5487,6 @@ dependencies = [
"log",
"lsp",
"node_runtime",
"parking_lot",
"project",
"regex",
"rope",
@@ -5463,19 +5512,16 @@ dependencies = [
"tree-sitter-go",
"tree-sitter-gomod",
"tree-sitter-gowork",
"tree-sitter-hcl",
"tree-sitter-heex",
"tree-sitter-jsdoc",
"tree-sitter-json 0.20.0",
"tree-sitter-markdown",
"tree-sitter-nu",
"tree-sitter-proto",
"tree-sitter-python",
"tree-sitter-regex",
"tree-sitter-ruby",
"tree-sitter-rust",
"tree-sitter-typescript",
"tree-sitter-vue",
"tree-sitter-yaml",
"unindent",
"util",
@@ -5517,9 +5563,9 @@ checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]]
name = "libgit2-sys"
version = "0.14.2+1.5.1"
version = "0.16.2+1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f3d95f6b51075fe9810a7ae22c7095f12b98005ab364d8544797a825ce946a4"
checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8"
dependencies = [
"cc",
"libc",
@@ -5676,6 +5722,16 @@ dependencies = [
"sha2 0.10.7",
]
[[package]]
name = "lmdb-master-sys"
version = "0.1.0"
source = "git+https://github.com/meilisearch/heed?rev=036ac23f73a021894974b9adc815bc95b3e0482a#036ac23f73a021894974b9adc815bc95b3e0482a"
dependencies = [
"cc",
"doxygen-rs",
"libc",
]
[[package]]
name = "lock_api"
version = "0.4.10"
@@ -6695,6 +6751,16 @@ dependencies = [
"sha2 0.10.7",
]
[[package]]
name = "page_size"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "palette"
version = "0.7.5"
@@ -6868,9 +6934,33 @@ version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
dependencies = [
"phf_macros",
"phf_shared",
]
[[package]]
name = "phf_generator"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
dependencies = [
"phf_shared",
"rand 0.8.5",
]
[[package]]
name = "phf_macros"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b"
dependencies = [
"phf_generator",
"phf_shared",
"proc-macro2",
"quote",
"syn 2.0.48",
]
[[package]]
name = "phf_shared"
version = "0.11.2"
@@ -7075,7 +7165,7 @@ dependencies = [
"concurrent-queue",
"pin-project-lite",
"rustix 0.38.32",
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"tracing",
"windows-sys 0.52.0",
]
@@ -7943,7 +8033,6 @@ name = "rope"
version = "0.1.0"
dependencies = [
"arrayvec",
"bromberg_sl2",
"criterion",
"gpui",
"log",
@@ -7980,7 +8069,7 @@ dependencies = [
"serde",
"serde_json",
"strum",
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"tracing",
"util",
"zstd",
]
@@ -8347,7 +8436,7 @@ dependencies = [
"strum",
"thiserror",
"time",
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"tracing",
"url",
"uuid",
]
@@ -8486,6 +8575,35 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58bf37232d3bb9a2c4e641ca2a11d83b5062066f88df7fed36c28772046d65ba"
[[package]]
name = "semantic_index"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"clock",
"collections",
"env_logger",
"fs",
"futures 0.3.28",
"futures-batch",
"gpui",
"heed",
"language",
"languages",
"log",
"open_ai",
"project",
"serde",
"serde_json",
"settings",
"sha2 0.10.7",
"smol",
"tempfile",
"util",
"worktree",
]
[[package]]
name = "semantic_version"
version = "0.1.0"
@@ -8500,12 +8618,6 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
[[package]]
name = "seq-macro"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a9f47faea3cad316faa914d013d24f471cd90bfca1a0c70f05a3f42c6441e99"
[[package]]
name = "serde"
version = "1.0.196"
@@ -9098,7 +9210,7 @@ dependencies = [
"time",
"tokio",
"tokio-stream",
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"tracing",
"url",
"uuid",
"webpki-roots",
@@ -9185,7 +9297,7 @@ dependencies = [
"stringprep",
"thiserror",
"time",
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"tracing",
"uuid",
"whoami",
]
@@ -9230,7 +9342,7 @@ dependencies = [
"stringprep",
"thiserror",
"time",
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"tracing",
"uuid",
"whoami",
]
@@ -9255,7 +9367,7 @@ dependencies = [
"serde",
"sqlx-core",
"time",
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"tracing",
"url",
"uuid",
]
@@ -9497,6 +9609,15 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]]
name = "synchronoise"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dbc01390fc626ce8d1cffe3376ded2b72a11bb70e1c75f404a210e4daa4def2"
dependencies = [
"crossbeam-queue",
]
[[package]]
name = "sys-locale"
version = "0.3.1"
@@ -9720,6 +9841,7 @@ dependencies = [
"serde_json",
"settings",
"shellexpand",
"shlex",
"smol",
"task",
"terminal",
@@ -10078,7 +10200,7 @@ dependencies = [
"futures-sink",
"pin-project-lite",
"tokio",
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"tracing",
]
[[package]]
@@ -10173,7 +10295,7 @@ dependencies = [
"tokio",
"tower-layer",
"tower-service",
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"tracing",
]
[[package]]
@@ -10210,7 +10332,7 @@ dependencies = [
"pin-project-lite",
"tower-layer",
"tower-service",
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"tracing",
]
[[package]]
@@ -10234,16 +10356,7 @@ dependencies = [
"log",
"pin-project-lite",
"tracing-attributes",
"tracing-core 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "tracing"
version = "0.1.40"
source = "git+https://github.com/tokio-rs/tracing?rev=tracing-subscriber-0.3.18#8b7a1dde69797b33ecfa20da71e72eb5e61f0b25"
dependencies = [
"pin-project-lite",
"tracing-core 0.1.32 (git+https://github.com/tokio-rs/tracing?rev=tracing-subscriber-0.3.18)",
"tracing-core",
]
[[package]]
@@ -10262,14 +10375,6 @@ name = "tracing-core"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
dependencies = [
"once_cell",
]
[[package]]
name = "tracing-core"
version = "0.1.32"
source = "git+https://github.com/tokio-rs/tracing?rev=tracing-subscriber-0.3.18#8b7a1dde69797b33ecfa20da71e72eb5e61f0b25"
dependencies = [
"once_cell",
"valuable",
@@ -10282,32 +10387,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2"
dependencies = [
"pin-project",
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"tracing",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "git+https://github.com/tokio-rs/tracing?rev=tracing-subscriber-0.3.18#8b7a1dde69797b33ecfa20da71e72eb5e61f0b25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core 0.1.32 (git+https://github.com/tokio-rs/tracing?rev=tracing-subscriber-0.3.18)",
"tracing-core",
]
[[package]]
name = "tracing-serde"
version = "0.1.3"
source = "git+https://github.com/tokio-rs/tracing?rev=tracing-subscriber-0.3.18#8b7a1dde69797b33ecfa20da71e72eb5e61f0b25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1"
dependencies = [
"serde",
"tracing-core 0.1.32 (git+https://github.com/tokio-rs/tracing?rev=tracing-subscriber-0.3.18)",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.18"
source = "git+https://github.com/tokio-rs/tracing?rev=tracing-subscriber-0.3.18#8b7a1dde69797b33ecfa20da71e72eb5e61f0b25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
dependencies = [
"matchers",
"nu-ansi-term",
@@ -10318,8 +10426,8 @@ dependencies = [
"sharded-slab",
"smallvec",
"thread_local",
"tracing 0.1.40 (git+https://github.com/tokio-rs/tracing?rev=tracing-subscriber-0.3.18)",
"tracing-core 0.1.32 (git+https://github.com/tokio-rs/tracing?rev=tracing-subscriber-0.3.18)",
"tracing",
"tracing-core",
"tracing-log",
"tracing-serde",
]
@@ -10417,15 +10525,6 @@ dependencies = [
"tree-sitter",
]
[[package]]
name = "tree-sitter-hcl"
version = "0.0.1"
source = "git+https://github.com/MichaHoffmann/tree-sitter-hcl?rev=v1.1.0#636dbe70301ecbab8f353c8c78b3406fe4f185f5"
dependencies = [
"cc",
"tree-sitter",
]
[[package]]
name = "tree-sitter-heex"
version = "0.0.1"
@@ -10482,15 +10581,6 @@ dependencies = [
"tree-sitter",
]
[[package]]
name = "tree-sitter-nu"
version = "0.0.1"
source = "git+https://github.com/nushell/tree-sitter-nu?rev=7dd29f9616822e5fc259f5b4ae6c4ded9a71a132#7dd29f9616822e5fc259f5b4ae6c4ded9a71a132"
dependencies = [
"cc",
"tree-sitter",
]
[[package]]
name = "tree-sitter-proto"
version = "0.0.2"
@@ -10549,15 +10639,6 @@ dependencies = [
"tree-sitter",
]
[[package]]
name = "tree-sitter-vue"
version = "0.0.1"
source = "git+https://github.com/zed-industries/tree-sitter-vue?rev=6608d9d60c386f19d80af7d8132322fa11199c42#6608d9d60c386f19d80af7d8132322fa11199c42"
dependencies = [
"cc",
"tree-sitter",
]
[[package]]
name = "tree-sitter-yaml"
version = "0.0.1"
@@ -11200,7 +11281,7 @@ dependencies = [
"anyhow",
"log",
"once_cell",
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"tracing",
"wasmtime",
"wasmtime-c-api-macros",
]
@@ -11412,7 +11493,7 @@ dependencies = [
"system-interface",
"thiserror",
"tokio",
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"tracing",
"url",
"wasmtime",
"wiggle",
@@ -11649,7 +11730,7 @@ dependencies = [
"async-trait",
"bitflags 2.4.2",
"thiserror",
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"tracing",
"wasmtime",
"wiggle-macro",
]
@@ -12405,7 +12486,7 @@ dependencies = [
"serde_repr",
"sha1",
"static_assertions",
"tracing 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"tracing",
"uds_windows",
"windows-sys 0.52.0",
"xdg-home",
@@ -12542,14 +12623,14 @@ dependencies = [
[[package]]
name = "zed_clojure"
version = "0.0.1"
version = "0.0.2"
dependencies = [
"zed_extension_api 0.0.4",
]
[[package]]
name = "zed_csharp"
version = "0.0.1"
version = "0.0.2"
dependencies = [
"zed_extension_api 0.0.4",
]
@@ -12591,15 +12672,6 @@ dependencies = [
"wit-bindgen",
]
[[package]]
name = "zed_extension_api"
version = "0.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5f4ae4e302a80591635ef9a236b35fde6fcc26cfd060e66fde4ba9f9fd394a1"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "zed_extension_api"
version = "0.0.6"
@@ -12622,7 +12694,7 @@ dependencies = [
[[package]]
name = "zed_gleam"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -12643,7 +12715,7 @@ dependencies = [
[[package]]
name = "zed_lua"
version = "0.0.1"
version = "0.0.2"
dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -12657,7 +12729,7 @@ dependencies = [
[[package]]
name = "zed_php"
version = "0.0.1"
version = "0.0.2"
dependencies = [
"zed_extension_api 0.0.4",
]
@@ -12684,10 +12756,17 @@ dependencies = [
]
[[package]]
name = "zed_toml"
name = "zed_terraform"
version = "0.0.2"
dependencies = [
"zed_extension_api 0.0.5",
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "zed_toml"
version = "0.1.0"
dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -12698,10 +12777,17 @@ dependencies = [
]
[[package]]
name = "zed_zig"
name = "zed_vue"
version = "0.0.1"
dependencies = [
"zed_extension_api 0.0.5",
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "zed_zig"
version = "0.1.1"
dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]

View File

@@ -73,6 +73,7 @@ members = [
"crates/task",
"crates/tasks_ui",
"crates/search",
"crates/semantic_index",
"crates/semantic_version",
"crates/settings",
"crates/snippet",
@@ -117,8 +118,10 @@ members = [
"extensions/prisma",
"extensions/purescript",
"extensions/svelte",
"extensions/terraform",
"extensions/toml",
"extensions/uiua",
"extensions/vue",
"extensions/zig",
"tooling/xtask",
@@ -252,9 +255,11 @@ derive_more = "0.99.17"
emojis = "0.6.1"
env_logger = "0.9"
futures = "0.3"
futures-batch = "0.6.1"
futures-lite = "1.13"
git2 = { version = "0.15", default-features = false }
git2 = { version = "0.18", default-features = false }
globset = "0.4"
heed = { git = "https://github.com/meilisearch/heed", rev = "036ac23f73a021894974b9adc815bc95b3e0482a", features = ["read-txn-no-tls"] }
hex = "0.4.3"
ignore = "0.4.22"
indoc = "1"
@@ -293,6 +298,7 @@ serde_json_lenient = { version = "0.1", features = [
] }
serde_repr = "0.1"
sha2 = "0.10"
shlex = "1.3"
shellexpand = "2.1.0"
smallvec = { version = "1.6", features = ["union"] }
smol = "1.2"
@@ -322,28 +328,24 @@ tree-sitter-embedded-template = "0.20.0"
tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" }
tree-sitter-gomod = { git = "https://github.com/camdencheek/tree-sitter-go-mod" }
tree-sitter-gowork = { git = "https://github.com/d1y/tree-sitter-go-work" }
tree-sitter-hcl = { git = "https://github.com/MichaHoffmann/tree-sitter-hcl", rev = "v1.1.0" }
rustc-demangle = "0.1.23"
tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "2e1348c3cf2c9323e87c2744796cf3f3868aa82a" }
tree-sitter-html = "0.19.0"
tree-sitter-jsdoc = { git = "https://github.com/tree-sitter/tree-sitter-jsdoc", ref = "6a6cf9e7341af32d8e2b2e24a37fbfebefc3dc55" }
tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "40a81c01a40ac48744e0c8ccabbaba1920441199" }
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "7dd29f9616822e5fc259f5b4ae6c4ded9a71a132" }
tree-sitter-proto = { git = "https://github.com/rewinfrey/tree-sitter-proto", rev = "36d54f288aee112f13a67b550ad32634d0c2cb52" }
tree-sitter-python = "0.20.2"
tree-sitter-regex = "0.20.0"
tree-sitter-ruby = "0.20.0"
tree-sitter-rust = "0.20.3"
tree-sitter-scheme = { git = "https://github.com/6cdh/tree-sitter-scheme", rev = "af0fd1fa452cb2562dc7b5c8a8c55551c39273b9" }
tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259" }
tree-sitter-vue = { git = "https://github.com/zed-industries/tree-sitter-vue", rev = "6608d9d60c386f19d80af7d8132322fa11199c42" }
tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "f545a41f57502e1b5ddf2a6668896c1b0620f930" }
unindent = "0.1.7"
unicase = "2.6"
unicode-segmentation = "1.10"
url = "2.2"
uuid = { version = "1.1.2", features = ["v4"] }
uuid = { version = "1.1.2", features = ["v4", "v5"] }
wasmparser = "0.201"
wasm-encoder = "0.201"
wasmtime = { version = "19.0.0", default-features = false, features = [

View File

@@ -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:-info} cargo run --package=collab serve
livekit: livekit-server --dev
blob_store: ./script/run-local-minio

View File

@@ -1,6 +1,6 @@
# Zed
[![CI](https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg)](https://github.com/zed-industries/ze34actions/workflows/ci.yml)
[![CI](https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg)](https://github.com/zed-industries/zed/actions/workflows/ci.yml)
Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter).
@@ -38,6 +38,8 @@ brew install zed-preview
See [CONTRIBUTING.md](./CONTRIBUTING.md) for ways you can contribute to Zed.
Also... we're hiring! Check out our [jobs](https://zed.dev/jobs) page for open roles.
## Licensing
License information for third party dependencies must be correctly provided for CI to pass.

3
assets/icons/sliders.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.36667 3.79167C5.53364 3.79167 4.85833 4.46697 4.85833 5.3C4.85833 6.13303 5.53364 6.80833 6.36667 6.80833C7.1997 6.80833 7.875 6.13303 7.875 5.3C7.875 4.46697 7.1997 3.79167 6.36667 3.79167ZM2.1 5.925H3.67944C3.9626 7.14732 5.05824 8.05833 6.36667 8.05833C7.67509 8.05833 8.77073 7.14732 9.05389 5.925H14.9C15.2452 5.925 15.525 5.64518 15.525 5.3C15.525 4.95482 15.2452 4.675 14.9 4.675H9.05389C8.77073 3.45268 7.67509 2.54167 6.36667 2.54167C5.05824 2.54167 3.9626 3.45268 3.67944 4.675H2.1C1.75482 4.675 1.475 4.95482 1.475 5.3C1.475 5.64518 1.75482 5.925 2.1 5.925ZM13.3206 12.325C13.0374 13.5473 11.9418 14.4583 10.6333 14.4583C9.32491 14.4583 8.22927 13.5473 7.94611 12.325H2.1C1.75482 12.325 1.475 12.0452 1.475 11.7C1.475 11.3548 1.75482 11.075 2.1 11.075H7.94611C8.22927 9.85268 9.32491 8.94167 10.6333 8.94167C11.9418 8.94167 13.0374 9.85268 13.3206 11.075H14.9C15.2452 11.075 15.525 11.3548 15.525 11.7C15.525 12.0452 15.2452 12.325 14.9 12.325H13.3206ZM9.125 11.7C9.125 10.867 9.8003 10.1917 10.6333 10.1917C11.4664 10.1917 12.1417 10.867 12.1417 11.7C12.1417 12.533 11.4664 13.2083 10.6333 13.2083C9.8003 13.2083 9.125 12.533 9.125 11.7Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -234,6 +234,8 @@
"displayLines": true
}
],
"g ]": "editor::GoToDiagnostic",
"g [": "editor::GoToPrevDiagnostic",
"shift-h": "vim::WindowTop",
"shift-m": "vim::WindowMiddle",
"shift-l": "vim::WindowBottom",
@@ -367,6 +369,15 @@
"< <": "vim::Outdent",
"ctrl-pagedown": "pane::ActivateNextItem",
"ctrl-pageup": "pane::ActivatePrevItem",
// tree-sitter related commands
"[ x": "editor::SelectLargerSyntaxNode",
"] x": "editor::SelectSmallerSyntaxNode"
}
},
{
"context": "Editor && vim_mode == visual && vim_operator == none && !VimWaiting",
"bindings": {
// tree-sitter related commands
"[ x": "editor::SelectLargerSyntaxNode",
"] x": "editor::SelectSmallerSyntaxNode"
}
@@ -532,6 +543,18 @@
]
}
},
{
"context": "Editor && vim_mode == normal",
"bindings": {
"g c c": "editor::ToggleComments"
}
},
{
"context": "Editor && vim_mode == visual",
"bindings": {
"g c": "editor::ToggleComments"
}
},
{
"context": "Editor && vim_mode == insert",
"bindings": {
@@ -590,17 +613,18 @@
"%": "project_panel::NewFile",
"/": "project_panel::NewSearchInDirectory",
"d": "project_panel::NewDirectory",
"enter": "project_panel::Open",
"enter": "project_panel::OpenPermanent",
"escape": "project_panel::ToggleFocus",
"h": "project_panel::CollapseSelectedEntry",
"j": "menu::SelectNext",
"k": "menu::SelectPrev",
"l": "project_panel::ExpandSelectedEntry",
"o": "project_panel::Open",
"o": "project_panel::OpenPermanent",
"shift-d": "project_panel::Delete",
"shift-r": "project_panel::Rename",
"t": "project_panel::Open",
"v": "project_panel::Open",
"t": "project_panel::OpenPermanent",
"v": "project_panel::OpenPermanent",
"p": "project_panel::Open",
"x": "project_panel::RevealInFinder"
}
}

View File

@@ -47,11 +47,20 @@
// The factor to grow the active pane by. Defaults to 1.0
// which gives the same size as all other panes.
"active_pane_magnification": 1.0,
// Centered layout related settings.
"centered_layout": {
// The relative width of the left padding of the central pane from the
// workspace when the centered layout is used.
"left_padding": 0.2,
// The relative width of the right padding of the central pane from the
// workspace when the centered layout is used.
"right_padding": 0.2
},
// The key to use for adding multiple cursors
// Currently "alt" or "cmd_or_ctrl" (also aliased as
// "cmd" and "ctrl") are supported.
"multi_cursor_modifier": "alt",
// Whether to enable vim modes and key bindings
// Whether to enable vim modes and key bindings.
"vim_mode": false,
// Whether to show the informational hover box when moving the mouse
// over symbols in the editor.
@@ -92,8 +101,9 @@
// Whether to use additional LSP queries to format (and amend) the code after
// every "trigger" symbol input, defined by LSP server capabilities.
"use_on_type_format": true,
// Whether to automatically type closing characters for you. For example,
// when you type (, Zed will automatically add a closing ) at the correct position.
// Whether to automatically add matching closing characters when typing
// opening parenthesis, bracket, brace, single or double quote characters.
// For example, when you type (, Zed will add a closing ) at the correct position.
"use_autoclose": true,
// Controls how the editor handles the autoclosed characters.
// When set to `false`(default), skipping over and auto-removing of the closing characters
@@ -145,10 +155,10 @@
"show": "auto",
// Whether to show git diff indicators in the scrollbar.
"git_diff": true,
// Whether to show selections in the scrollbar.
"selections": true,
// Whether to show symbols selections in the scrollbar.
"symbols_selections": true,
// Whether to show buffer search results in the scrollbar.
"search_results": true,
// Whether to show selected symbol occurrences in the scrollbar.
"selected_symbol": true,
// Whether to show diagnostic indicators in the scrollbar.
"diagnostics": true
},
@@ -171,6 +181,9 @@
},
// The number of lines to keep above/below the cursor when scrolling.
"vertical_scroll_margin": 3,
// Scroll sensitivity multiplier. This multiplier is applied
// to both the horizontal and vertical delta values while scrolling.
"scroll_sensitivity": 1.0,
"relative_line_numbers": false,
// When to populate a new search's query based on the text under the cursor.
// This setting can take the following three values:
@@ -214,7 +227,10 @@
// Whether to reveal it in the project panel automatically,
// when a corresponding project entry becomes active.
// Gitignored entries are never auto revealed.
"auto_reveal_entries": true
"auto_reveal_entries": true,
/// Whether to fold directories automatically
/// when a directory has only one directory inside.
"auto_fold_dirs": false
},
"collaboration_panel": {
// Whether to show the collaboration panel button in the status bar.
@@ -387,7 +403,15 @@
// "git_gutter": "tracked_files"
// 2. Hide the gutter
// "git_gutter": "hide"
"git_gutter": "tracked_files"
"git_gutter": "tracked_files",
// Control whether the git blame information is shown inline,
// in the currently focused line.
"inline_blame": {
"enabled": true
// Sets a delay after which the inline blame information is shown.
// Delay is restarted with every cursor movement.
// "delay_ms": 600
}
},
"copilot": {
// The set of glob patterns for which copilot should be disabled
@@ -476,6 +500,8 @@
// Whether or not selecting text in the terminal will automatically
// copy to the system clipboard.
"copy_on_select": false,
// Whether to show the terminal button in the status bar
"button": true,
// Any key-value pairs added to this list will be added to the terminal's
// environment. Use `:` to separate multiple values.
"env": {

View File

@@ -1119,8 +1119,8 @@ impl AssistantPanel {
)
.size_full()
.into_any_element()
} else {
let editor = self.active_conversation_editor().unwrap();
} else if let Some(editor) = self.active_conversation_editor() {
let editor = editor.clone();
let conversation = editor.read(cx).conversation.clone();
div()
.size_full()
@@ -1135,6 +1135,8 @@ impl AssistantPanel {
.children(self.render_remaining_tokens(&conversation, cx)),
)
.into_any_element()
} else {
div().into_any_element()
},
))
}
@@ -2065,7 +2067,7 @@ impl ConversationEditor {
workspace: workspace.downgrade(),
_subscriptions,
};
this.update_active_buffer(workspace, cx);
cx.defer(|this, cx| this.update_active_buffer(workspace, cx));
this.update_message_headers(cx);
this
}

View File

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

View File

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

View File

@@ -132,7 +132,7 @@ pub fn init(client: &Arc<Client>, cx: &mut AppContext) {
move |_: &SignOut, cx| {
if let Some(client) = client.upgrade() {
cx.spawn(|cx| async move {
client.disconnect(&cx);
client.sign_out(&cx).await;
})
.detach();
}
@@ -1250,6 +1250,15 @@ impl Client {
})
}
pub async fn sign_out(self: &Arc<Self>, cx: &AsyncAppContext) {
self.state.write().credentials = None;
self.disconnect(&cx);
if self.has_keychain_credentials(cx).await {
delete_credentials_from_keychain(cx).await.log_err();
}
}
pub fn disconnect(self: &Arc<Self>, cx: &AsyncAppContext) {
self.peer.teardown();
self.set_status(Status::SignedOut, cx);

View File

@@ -64,7 +64,7 @@ toml.workspace = true
tower = "0.4"
tower-http = { workspace = true, features = ["trace"] }
tracing = "0.1.40"
tracing-subscriber = { git = "https://github.com/tokio-rs/tracing", rev = "tracing-subscriber-0.3.18", features = ["env-filter", "json", "registry", "tracing-log"] } # workaround for https://github.com/tokio-rs/tracing/issues/2927
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json", "registry", "tracing-log"] } # workaround for https://github.com/tokio-rs/tracing/issues/2927
util.workspace = true
uuid.workspace = true

View File

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

View File

@@ -106,8 +106,12 @@ async fn get_extension_versions(
}
#[derive(Debug, Deserialize)]
struct DownloadLatestExtensionParams {
struct DownloadLatestExtensionPathParams {
extension_id: String,
}
#[derive(Debug, Deserialize)]
struct DownloadLatestExtensionQueryParams {
min_schema_version: Option<i32>,
max_schema_version: Option<i32>,
min_wasm_api_version: Option<SemanticVersion>,
@@ -116,13 +120,14 @@ struct DownloadLatestExtensionParams {
async fn download_latest_extension(
Extension(app): Extension<Arc<AppState>>,
Path(params): Path<DownloadLatestExtensionParams>,
Path(params): Path<DownloadLatestExtensionPathParams>,
Query(query): Query<DownloadLatestExtensionQueryParams>,
) -> Result<Redirect> {
let constraints = maybe!({
let min_schema_version = params.min_schema_version?;
let max_schema_version = params.max_schema_version?;
let min_wasm_api_version = params.min_wasm_api_version?;
let max_wasm_api_version = params.max_wasm_api_version?;
let min_schema_version = query.min_schema_version?;
let max_schema_version = query.max_schema_version?;
let min_wasm_api_version = query.min_wasm_api_version?;
let max_wasm_api_version = query.max_wasm_api_version?;
Some(ExtensionVersionConstraints {
schema_versions: min_schema_version..=max_schema_version,

View File

@@ -6,6 +6,7 @@ pub mod channels;
pub mod contacts;
pub mod contributors;
pub mod dev_servers;
pub mod embeddings;
pub mod extensions;
pub mod hosted_projects;
pub mod messages;

View File

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

View File

@@ -11,6 +11,7 @@ pub mod channel_message_mention;
pub mod contact;
pub mod contributor;
pub mod dev_server;
pub mod embedding;
pub mod extension;
pub mod extension_version;
pub mod feature_flag;

View File

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

View File

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

View File

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

View File

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

View File

@@ -32,6 +32,8 @@ use axum::{
use collections::{HashMap, HashSet};
pub use connection_pool::{ConnectionPool, ZedVersion};
use core::fmt::{self, Debug, Formatter};
use open_ai::{OpenAiEmbeddingModel, OPEN_AI_API_URL};
use sha2::Digest;
use futures::{
channel::oneshot,
@@ -568,6 +570,22 @@ impl Server {
app_state.config.google_ai_api_key.clone(),
)
})
})
.add_request_handler({
user_handler(move |request, response, session| {
get_cached_embeddings(request, response, session)
})
})
.add_request_handler({
let app_state = app_state.clone();
user_handler(move |request, response, session| {
compute_embeddings(
request,
response,
session,
app_state.config.openai_api_key.clone(),
)
})
});
Arc::new(server)
@@ -4021,8 +4039,6 @@ async fn complete_with_open_ai(
session: UserSession,
api_key: Arc<str>,
) -> Result<()> {
const OPEN_AI_API_URL: &str = "https://api.openai.com/v1";
let mut completion_stream = open_ai::stream_completion(
&session.http_client,
OPEN_AI_API_URL,
@@ -4276,6 +4292,128 @@ async fn count_tokens_with_language_model(
Ok(())
}
struct ComputeEmbeddingsRateLimit;
impl RateLimit for ComputeEmbeddingsRateLimit {
fn capacity() -> usize {
std::env::var("EMBED_TEXTS_RATE_LIMIT_PER_HOUR")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(120) // Picked arbitrarily
}
fn refill_duration() -> chrono::Duration {
chrono::Duration::hours(1)
}
fn db_name() -> &'static str {
"compute-embeddings"
}
}
async fn compute_embeddings(
request: proto::ComputeEmbeddings,
response: Response<proto::ComputeEmbeddings>,
session: UserSession,
api_key: Option<Arc<str>>,
) -> Result<()> {
let api_key = api_key.context("no OpenAI API key configured on the server")?;
authorize_access_to_language_models(&session).await?;
session
.rate_limiter
.check::<ComputeEmbeddingsRateLimit>(session.user_id())
.await?;
let embeddings = match request.model.as_str() {
"openai/text-embedding-3-small" => {
open_ai::embed(
&session.http_client,
OPEN_AI_API_URL,
&api_key,
OpenAiEmbeddingModel::TextEmbedding3Small,
request.texts.iter().map(|text| text.as_str()),
)
.await?
}
provider => return Err(anyhow!("unsupported embedding provider {:?}", provider))?,
};
let embeddings = request
.texts
.iter()
.map(|text| {
let mut hasher = sha2::Sha256::new();
hasher.update(text.as_bytes());
let result = hasher.finalize();
result.to_vec()
})
.zip(
embeddings
.data
.into_iter()
.map(|embedding| embedding.embedding),
)
.collect::<HashMap<_, _>>();
let db = session.db().await;
db.save_embeddings(&request.model, &embeddings)
.await
.context("failed to save embeddings")
.trace_err();
response.send(proto::ComputeEmbeddingsResponse {
embeddings: embeddings
.into_iter()
.map(|(digest, dimensions)| proto::Embedding { digest, dimensions })
.collect(),
})?;
Ok(())
}
struct GetCachedEmbeddingsRateLimit;
impl RateLimit for GetCachedEmbeddingsRateLimit {
fn capacity() -> usize {
std::env::var("EMBED_TEXTS_RATE_LIMIT_PER_HOUR")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(120) // Picked arbitrarily
}
fn refill_duration() -> chrono::Duration {
chrono::Duration::hours(1)
}
fn db_name() -> &'static str {
"get-cached-embeddings"
}
}
async fn get_cached_embeddings(
request: proto::GetCachedEmbeddings,
response: Response<proto::GetCachedEmbeddings>,
session: UserSession,
) -> Result<()> {
authorize_access_to_language_models(&session).await?;
session
.rate_limiter
.check::<GetCachedEmbeddingsRateLimit>(session.user_id())
.await?;
let db = session.db().await;
let embeddings = db.get_embeddings(&request.model, &request.digests).await?;
response.send(proto::GetCachedEmbeddingsResponse {
embeddings: embeddings
.into_iter()
.map(|(digest, dimensions)| proto::Embedding { digest, dimensions })
.collect(),
})?;
Ok(())
}
async fn authorize_access_to_language_models(session: &UserSession) -> Result<(), Error> {
let db = session.db().await;
let flags = db.get_user_flags(session.user_id()).await?;

View File

@@ -31,7 +31,7 @@ impl fmt::Display for ZedVersion {
impl ZedVersion {
pub fn can_collaborate(&self) -> bool {
self.0 >= SemanticVersion::new(0, 127, 3)
self.0 >= SemanticVersion::new(0, 129, 2)
}
}

View File

@@ -18,7 +18,10 @@ use language::{
language_settings::{AllLanguageSettings, InlayHintSettings},
FakeLspAdapter,
};
use project::SERVER_PROGRESS_DEBOUNCE_TIMEOUT;
use project::{
project_settings::{InlineBlameSettings, ProjectSettings},
SERVER_PROGRESS_DEBOUNCE_TIMEOUT,
};
use rpc::RECEIVE_TIMEOUT;
use serde_json::json;
use settings::SettingsStore;
@@ -1999,6 +2002,25 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
cx_a.update(editor::init);
cx_b.update(editor::init);
// Turn inline-blame-off by default so no state is transferred without us explicitly doing so
let inline_blame_off_settings = Some(InlineBlameSettings {
enabled: false,
delay_ms: None,
});
cx_a.update(|cx| {
cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings::<ProjectSettings>(cx, |settings| {
settings.git.inline_blame = inline_blame_off_settings;
});
});
});
cx_b.update(|cx| {
cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings::<ProjectSettings>(cx, |settings| {
settings.git.inline_blame = inline_blame_off_settings;
});
});
});
client_a
.fs()
@@ -2100,14 +2122,12 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
blame.update(cx, |blame, _| {
for (idx, entry) in entries.iter().flatten().enumerate() {
let details = blame.details_for_entry(entry).unwrap();
assert_eq!(details.message, format!("message for idx-{}", idx));
assert_eq!(
blame.permalink_for_entry(entry).unwrap().to_string(),
details.permalink.unwrap().to_string(),
format!("http://example.com/codehost/idx-{}", idx)
);
assert_eq!(
blame.message_for_entry(entry).unwrap(),
format!("message for idx-{}", idx)
);
}
});
});

View File

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

View File

@@ -531,6 +531,8 @@ impl ChatPanel {
&self.languages,
self.client.id(),
&message,
self.local_timezone,
cx,
)
});
el.child(
@@ -744,6 +746,8 @@ impl ChatPanel {
language_registry: &Arc<LanguageRegistry>,
current_user_id: u64,
message: &channel::ChannelMessage,
local_timezone: UtcOffset,
cx: &AppContext,
) -> RichText {
let mentions = message
.mentions
@@ -754,24 +758,39 @@ impl ChatPanel {
})
.collect::<Vec<_>>();
const MESSAGE_UPDATED: &str = " (edited)";
const MESSAGE_EDITED: &str = " (edited)";
let mut body = message.body.clone();
if message.edited_at.is_some() {
body.push_str(MESSAGE_UPDATED);
body.push_str(MESSAGE_EDITED);
}
let mut rich_text = rich_text::render_rich_text(body, &mentions, language_registry, None);
if message.edited_at.is_some() {
let range = (rich_text.text.len() - MESSAGE_EDITED.len())..rich_text.text.len();
rich_text.highlights.push((
(rich_text.text.len() - MESSAGE_UPDATED.len())..rich_text.text.len(),
range.clone(),
Highlight::Highlight(HighlightStyle {
fade_out: Some(0.8),
color: Some(cx.theme().colors().text_muted),
..Default::default()
}),
));
if let Some(edit_timestamp) = message.edited_at {
let edit_timestamp_text = time_format::format_localized_timestamp(
edit_timestamp,
OffsetDateTime::now_utc(),
local_timezone,
time_format::TimestampFormat::Absolute,
);
rich_text.custom_ranges.push(range);
rich_text.set_tooltip_builder_for_custom_ranges(move |_, _, cx| {
Some(Tooltip::text(edit_timestamp_text.clone(), cx))
})
}
}
rich_text
}
@@ -1176,7 +1195,13 @@ mod tests {
edited_at: None,
};
let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message);
let message = ChatPanel::render_markdown_with_mentions(
&language_registry,
102,
&message,
UtcOffset::UTC,
cx,
);
// Note that the "'" was replaced with due to smart punctuation.
let (body, ranges) = marked_text_ranges("«hi», «@abc», lets «call» «@fgh»", false);
@@ -1224,7 +1249,13 @@ mod tests {
edited_at: None,
};
let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message);
let message = ChatPanel::render_markdown_with_mentions(
&language_registry,
102,
&message,
UtcOffset::UTC,
cx,
);
// Note that the "'" was replaced with due to smart punctuation.
let (body, ranges) =
@@ -1265,7 +1296,13 @@ mod tests {
edited_at: None,
};
let message = ChatPanel::render_markdown_with_mentions(&language_registry, 102, &message);
let message = ChatPanel::render_markdown_with_mentions(
&language_registry,
102,
&message,
UtcOffset::UTC,
cx,
);
// Note that the "'" was replaced with due to smart punctuation.
let (body, ranges) = marked_text_ranges(

View File

@@ -557,6 +557,7 @@ mod tests {
use clock::FakeSystemClock;
use gpui::TestAppContext;
use language::{Language, LanguageConfig};
use project::Project;
use rpc::proto;
use settings::SettingsStore;
use util::{http::FakeHttpClient, test::marked_text_ranges};
@@ -630,6 +631,7 @@ mod tests {
let client = Client::new(clock, http.clone(), cx);
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
theme::init(theme::LoadThemes::JustBase, cx);
Project::init_settings(cx);
language::init(cx);
editor::init(cx);
client::init(&client, cx);

View File

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

View File

@@ -44,8 +44,6 @@ use workspace::{
actions!(diagnostics, [Deploy, ToggleWarnings]);
const CONTEXT_LINE_COUNT: u32 = 1;
pub fn init(cx: &mut AppContext) {
ProjectDiagnosticsSettings::register(cx);
cx.observe_new_views(ProjectDiagnosticsEditor::register)
@@ -63,6 +61,7 @@ struct ProjectDiagnosticsEditor {
paths_to_update: HashMap<LanguageServerId, HashSet<ProjectPath>>,
current_diagnostics: HashMap<LanguageServerId, HashSet<ProjectPath>>,
include_warnings: bool,
context: u32,
_subscriptions: Vec<Subscription>,
}
@@ -116,7 +115,8 @@ impl ProjectDiagnosticsEditor {
workspace.register_action(Self::deploy);
}
fn new(
fn new_with_context(
context: u32,
project_handle: Model<Project>,
workspace: WeakView<Workspace>,
cx: &mut ViewContext<Self>,
@@ -136,8 +136,15 @@ impl ProjectDiagnosticsEditor {
.entry(*language_server_id)
.or_default()
.insert(path.clone());
if this.editor.read(cx).selections.all::<usize>(cx).is_empty()
&& !this.is_dirty(cx)
if this.is_dirty(cx) {
return;
}
let selections = this.editor.read(cx).selections.all::<usize>(cx);
if selections.len() < 2
&& selections
.first()
.map_or(true, |selection| selection.end == selection.start)
{
this.update_excerpts(Some(*language_server_id), cx);
}
@@ -174,6 +181,7 @@ impl ProjectDiagnosticsEditor {
let summary = project.diagnostic_summary(false, cx);
let mut this = Self {
project: project_handle,
context,
summary,
workspace,
excerpts,
@@ -193,6 +201,19 @@ impl ProjectDiagnosticsEditor {
this
}
fn new(
project_handle: Model<Project>,
workspace: WeakView<Workspace>,
cx: &mut ViewContext<Self>,
) -> Self {
Self::new_with_context(
editor::DEFAULT_MULTIBUFFER_CONTEXT,
project_handle,
workspace,
cx,
)
}
fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
workspace.activate_item(&existing, cx);
@@ -423,18 +444,16 @@ impl ProjectDiagnosticsEditor {
let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
if let Some((range, start_ix)) = &mut pending_range {
if let Some(entry) = resolved_entry.as_ref() {
if entry.range.start.row
<= range.end.row + 1 + CONTEXT_LINE_COUNT * 2
{
if entry.range.start.row <= range.end.row + 1 + self.context * 2 {
range.end = range.end.max(entry.range.end);
continue;
}
}
let excerpt_start =
Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
Point::new(range.start.row.saturating_sub(self.context), 0);
let excerpt_end = snapshot.clip_point(
Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
Point::new(range.end.row + self.context, u32::MAX),
Bias::Left,
);
let excerpt_id = excerpts
@@ -1023,7 +1042,12 @@ mod tests {
// Open the project diagnostics view while there are already diagnostics.
let view = window.build_view(cx, |cx| {
ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
ProjectDiagnosticsEditor::new_with_context(
1,
project.clone(),
workspace.downgrade(),
cx,
)
});
view.next_notification(cx).await;
@@ -1333,7 +1357,12 @@ mod tests {
let workspace = window.root(cx).unwrap();
let view = window.build_view(cx, |cx| {
ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
ProjectDiagnosticsEditor::new_with_context(
1,
project.clone(),
workspace.downgrade(),
cx,
)
});
// Two language servers start updating diagnostics

View File

@@ -245,6 +245,7 @@ gpui::actions!(
Tab,
TabPrev,
ToggleGitBlame,
ToggleGitBlameInline,
ToggleInlayHints,
ToggleLineNumbers,
ToggleSoftWrap,

View File

@@ -129,6 +129,7 @@ use ui::{
Tooltip,
};
use util::{defer, maybe, post_inc, RangeExt, ResultExt, TryFutureExt};
use workspace::item::ItemHandle;
use workspace::notifications::NotificationId;
use workspace::Toast;
use workspace::{
@@ -137,6 +138,7 @@ use workspace::{
use crate::hover_links::find_url;
pub const DEFAULT_MULTIBUFFER_CONTEXT: u32 = 2;
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
const MAX_LINE_LEN: usize = 1024;
const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
@@ -154,7 +156,7 @@ pub fn render_parsed_markdown(
parsed: &language::ParsedMarkdown,
editor_style: &EditorStyle,
workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
cx: &mut WindowContext,
) -> InteractiveText {
let code_span_background_color = cx
.theme()
@@ -462,7 +464,10 @@ pub struct Editor {
editor_actions: Vec<Box<dyn Fn(&mut ViewContext<Self>)>>,
use_autoclose: bool,
auto_replace_emoji_shortcode: bool,
show_git_blame: bool,
show_git_blame_gutter: bool,
show_git_blame_inline: bool,
show_git_blame_inline_delay_task: Option<Task<()>>,
git_blame_inline_enabled: bool,
blame: Option<Model<GitBlame>>,
blame_subscription: Option<Subscription>,
custom_context_menu: Option<
@@ -471,13 +476,15 @@ pub struct Editor {
+ Fn(&mut Self, DisplayPoint, &mut ViewContext<Self>) -> Option<View<ui::ContextMenu>>,
>,
>,
last_bounds: Option<Bounds<Pixels>>,
expect_bounds_change: Option<Bounds<Pixels>>,
}
#[derive(Clone)]
pub struct EditorSnapshot {
pub mode: EditorMode,
show_gutter: bool,
show_git_blame: bool,
render_git_blame_gutter: bool,
pub display_snapshot: DisplaySnapshot,
pub placeholder_text: Option<Arc<str>>,
is_focused: bool,
@@ -1485,6 +1492,8 @@ impl Editor {
inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
gutter_hovered: false,
pixel_position_of_newest_cursor: None,
last_bounds: None,
expect_bounds_change: None,
gutter_width: Default::default(),
style: None,
show_cursor_names: false,
@@ -1493,7 +1502,10 @@ impl Editor {
vim_replace_map: Default::default(),
show_inline_completions: mode == EditorMode::Full,
custom_context_menu: None,
show_git_blame: false,
show_git_blame_gutter: false,
show_git_blame_inline: false,
show_git_blame_inline_delay_task: None,
git_blame_inline_enabled: ProjectSettings::get_global(cx).git.inline_blame_enabled(),
blame: None,
blame_subscription: None,
_subscriptions: vec![
@@ -1525,6 +1537,11 @@ impl Editor {
if mode == EditorMode::Full {
let should_auto_hide_scrollbars = cx.should_auto_hide_scrollbars();
cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars));
if this.git_blame_inline_enabled {
this.git_blame_inline_enabled = true;
this.start_git_blame_inline(false, cx);
}
}
this.report_editor_event("open", None, cx);
@@ -1641,10 +1658,7 @@ impl Editor {
EditorSnapshot {
mode: self.mode,
show_gutter: self.show_gutter,
show_git_blame: self
.blame
.as_ref()
.map_or(false, |blame| blame.read(cx).has_generated_entries()),
render_git_blame_gutter: self.render_git_blame_gutter(cx),
display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)),
scroll_anchor: self.scroll_manager.anchor(),
ongoing_scroll: self.scroll_manager.ongoing_scroll(),
@@ -1910,6 +1924,9 @@ impl Editor {
self.refresh_document_highlights(cx);
refresh_matching_bracket_highlights(self, cx);
self.discard_inline_completion(cx);
if self.git_blame_inline_enabled {
self.start_inline_blame_timer(cx);
}
}
self.blink_manager.update(cx, BlinkManager::pause_blinking);
@@ -3727,7 +3744,7 @@ impl Editor {
buffer
.edited_ranges_for_transaction::<usize>(transaction)
.collect(),
1,
DEFAULT_MULTIBUFFER_CONTEXT,
cx,
),
);
@@ -3789,6 +3806,22 @@ impl Editor {
None
}
fn start_inline_blame_timer(&mut self, cx: &mut ViewContext<Self>) {
if let Some(delay) = ProjectSettings::get_global(cx).git.inline_blame_delay() {
self.show_git_blame_inline = false;
self.show_git_blame_inline_delay_task = Some(cx.spawn(|this, mut cx| async move {
cx.background_executor().timer(delay).await;
this.update(&mut cx, |this, cx| {
this.show_git_blame_inline = true;
cx.notify();
})
.log_err();
}));
}
}
fn refresh_document_highlights(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
if self.pending_rename.is_some() {
return None;
@@ -7980,7 +8013,7 @@ impl Editor {
ranges_to_highlight.extend(multibuffer.push_excerpts_with_context_lines(
location.buffer.clone(),
ranges_for_buffer,
1,
DEFAULT_MULTIBUFFER_CONTEXT,
cx,
))
}
@@ -7998,11 +8031,16 @@ impl Editor {
cx,
);
});
let item = Box::new(editor);
if split {
workspace.split_item(SplitDirection::Right, Box::new(editor), cx);
workspace.split_item(SplitDirection::Right, item.clone(), cx);
} else {
workspace.add_item_to_active_pane(Box::new(editor), cx);
workspace.add_item_to_active_pane(item.clone(), cx);
}
workspace.active_pane().clone().update(cx, |pane, cx| {
let item_id = item.item_id();
pane.set_preview_item_id(Some(item_id), cx);
});
}
pub fn rename(&mut self, _: &Rename, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
@@ -8833,40 +8871,89 @@ impl Editor {
}
pub fn toggle_git_blame(&mut self, _: &ToggleGitBlame, cx: &mut ViewContext<Self>) {
if self.show_git_blame {
self.blame_subscription.take();
self.blame.take();
self.show_git_blame = false
} else {
if let Err(error) = self.show_git_blame_internal(cx) {
log::error!("failed to toggle on 'git blame': {}", error);
return;
}
self.show_git_blame = true
self.show_git_blame_gutter = !self.show_git_blame_gutter;
if self.show_git_blame_gutter && !self.has_blame_entries(cx) {
self.start_git_blame(true, cx);
}
cx.notify();
}
fn show_git_blame_internal(&mut self, cx: &mut ViewContext<Self>) -> Result<()> {
pub fn toggle_git_blame_inline(
&mut self,
_: &ToggleGitBlameInline,
cx: &mut ViewContext<Self>,
) {
self.toggle_git_blame_inline_internal(true, cx);
cx.notify();
}
pub fn git_blame_inline_enabled(&self) -> bool {
self.git_blame_inline_enabled
}
fn start_git_blame(&mut self, user_triggered: bool, cx: &mut ViewContext<Self>) {
if let Some(project) = self.project.as_ref() {
let Some(buffer) = self.buffer().read(cx).as_singleton() else {
anyhow::bail!("git blame not available in multi buffers")
return;
};
let project = project.clone();
let blame = cx.new_model(|cx| GitBlame::new(buffer, project, cx));
let blame = cx.new_model(|cx| GitBlame::new(buffer, project, user_triggered, cx));
self.blame_subscription = Some(cx.observe(&blame, |_, _, cx| cx.notify()));
self.blame = Some(blame);
}
}
Ok(())
fn toggle_git_blame_inline_internal(
&mut self,
user_triggered: bool,
cx: &mut ViewContext<Self>,
) {
if self.git_blame_inline_enabled {
self.git_blame_inline_enabled = false;
self.show_git_blame_inline = false;
self.show_git_blame_inline_delay_task.take();
} else {
self.git_blame_inline_enabled = true;
self.start_git_blame_inline(user_triggered, cx);
}
cx.notify();
}
fn start_git_blame_inline(&mut self, user_triggered: bool, cx: &mut ViewContext<Self>) {
self.start_git_blame(user_triggered, cx);
if ProjectSettings::get_global(cx)
.git
.inline_blame_delay()
.is_some()
{
self.start_inline_blame_timer(cx);
} else {
self.show_git_blame_inline = true
}
}
pub fn blame(&self) -> Option<&Model<GitBlame>> {
self.blame.as_ref()
}
pub fn render_git_blame_gutter(&mut self, cx: &mut WindowContext) -> bool {
self.show_git_blame_gutter && self.has_blame_entries(cx)
}
pub fn render_git_blame_inline(&mut self, cx: &mut WindowContext) -> bool {
self.focus_handle.is_focused(cx) && self.show_git_blame_inline && self.has_blame_entries(cx)
}
fn has_blame_entries(&self, cx: &mut WindowContext) -> bool {
self.blame()
.map_or(false, |blame| blame.read(cx).has_generated_entries())
}
fn get_permalink_to_line(&mut self, cx: &mut ViewContext<Self>) -> Result<url::Url> {
let (path, repo) = maybe!({
let project_handle = self.project.as_ref()?.clone();
@@ -9436,6 +9523,14 @@ impl Editor {
let editor_settings = EditorSettings::get_global(cx);
self.scroll_manager.vertical_scroll_margin = editor_settings.vertical_scroll_margin;
self.show_breadcrumbs = editor_settings.toolbar.breadcrumbs;
if self.mode == EditorMode::Full {
let inline_blame_enabled = ProjectSettings::get_global(cx).git.inline_blame_enabled();
if self.git_blame_inline_enabled != inline_blame_enabled {
self.toggle_git_blame_inline_internal(false, cx);
}
}
cx.notify();
}
@@ -9517,7 +9612,11 @@ impl Editor {
cx.spawn(|_, mut cx| async move {
let workspace = workspace.ok_or_else(|| anyhow!("cannot jump without workspace"))?;
let editor = workspace.update(&mut cx, |workspace, cx| {
workspace.open_path(path, None, true, cx)
// Reset the preview item id before opening the new item
workspace.active_pane().update(cx, |pane, cx| {
pane.set_preview_item_id(None, cx);
});
workspace.open_path_preview(path, None, true, true, cx)
})?;
let editor = editor
.await?
@@ -10044,7 +10143,7 @@ impl EditorSnapshot {
};
let git_blame_entries_width = self
.show_git_blame
.render_git_blame_gutter
.then_some(em_width * GIT_BLAME_GUTTER_WIDTH_CHARS);
let mut left_padding = git_blame_entries_width.unwrap_or(Pixels::ZERO);
@@ -10579,6 +10678,11 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren
let mut text_style = cx.text_style().clone();
text_style.color = diagnostic_style(diagnostic.severity, true, cx.theme().status());
let theme_settings = ThemeSettings::get_global(cx);
text_style.font_family = theme_settings.buffer_font.family.clone();
text_style.font_style = theme_settings.buffer_font.style;
text_style.font_features = theme_settings.buffer_font.features;
text_style.font_weight = theme_settings.buffer_font.weight;
let multi_line_diagnostic = diagnostic.message.contains('\n');

View File

@@ -15,6 +15,7 @@ pub struct EditorSettings {
pub scrollbar: Scrollbar,
pub gutter: Gutter,
pub vertical_scroll_margin: f32,
pub scroll_sensitivity: f32,
pub relative_line_numbers: bool,
pub seed_search_query_from_cursor: SeedQuerySetting,
pub multi_cursor_modifier: MultiCursorModifier,
@@ -57,8 +58,8 @@ pub struct Toolbar {
pub struct Scrollbar {
pub show: ShowScrollbar,
pub git_diff: bool,
pub selections: bool,
pub symbols_selections: bool,
pub selected_symbol: bool,
pub search_results: bool,
pub diagnostics: bool,
}
@@ -138,6 +139,11 @@ pub struct EditorSettingsContent {
///
/// Default: 3.
pub vertical_scroll_margin: Option<f32>,
/// Scroll sensitivity multiplier. This multiplier is applied
/// to both the horizontal and vertical delta values while scrolling.
///
/// Default: 1.0
pub scroll_sensitivity: Option<f32>,
/// Whether the line numbers on editors gutter are relative or not.
///
/// Default: false
@@ -178,7 +184,7 @@ pub struct ToolbarContent {
}
/// Scrollbar related settings
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct ScrollbarContent {
/// When to show the scrollbar in the editor.
///
@@ -188,14 +194,14 @@ pub struct ScrollbarContent {
///
/// Default: true
pub git_diff: Option<bool>,
/// Whether to show buffer search result markers in the scrollbar.
/// Whether to show buffer search result indicators in the scrollbar.
///
/// Default: true
pub selections: Option<bool>,
/// Whether to show symbols highlighted markers in the scrollbar.
pub search_results: Option<bool>,
/// Whether to show selected symbol occurrences in the scrollbar.
///
/// Default: true
pub symbols_selections: Option<bool>,
pub selected_symbol: Option<bool>,
/// Whether to show diagnostic indicators in the scrollbar.
///
/// Default: true

View File

@@ -4,7 +4,10 @@ use crate::{
TransformBlock,
},
editor_settings::{DoubleClickInMultibuffer, MultiCursorModifier, ShowScrollbar},
git::{blame::GitBlame, diff_hunk_to_display, DisplayDiffHunk},
git::{
blame::{CommitDetails, GitBlame},
diff_hunk_to_display, DisplayDiffHunk,
},
hover_popover::{
self, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
},
@@ -21,13 +24,13 @@ use collections::{BTreeMap, HashMap};
use git::{blame::BlameEntry, diff::DiffHunkStatus, Oid};
use gpui::{
anchored, deferred, div, fill, outline, point, px, quad, relative, size, svg,
transparent_black, Action, AnchorCorner, AnyElement, AnyView, AvailableSpace, Bounds,
ClipboardItem, ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element,
ElementContext, ElementInputHandler, Entity, Hitbox, Hsla, InteractiveElement, IntoElement,
transparent_black, Action, AnchorCorner, AnyElement, AvailableSpace, Bounds, ClipboardItem,
ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementContext,
ElementInputHandler, Entity, Hitbox, Hsla, InteractiveElement, IntoElement,
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size, Stateful,
StatefulInteractiveElement, Style, Styled, TextRun, TextStyle, TextStyleRefinement, View,
ViewContext, WindowContext,
ParentElement, Pixels, ScrollDelta, ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString,
Size, Stateful, StatefulInteractiveElement, Style, Styled, TextRun, TextStyle,
TextStyleRefinement, View, ViewContext, WeakView, WindowContext,
};
use itertools::Itertools;
use language::language_settings::ShowWhitespaceSetting;
@@ -49,11 +52,11 @@ use std::{
sync::Arc,
};
use sum_tree::Bias;
use theme::{ActiveTheme, PlayerColor};
use theme::{ActiveTheme, PlayerColor, ThemeSettings};
use ui::{h_flex, ButtonLike, ButtonStyle, ContextMenu, Tooltip};
use ui::{prelude::*, tooltip_container};
use util::ResultExt;
use workspace::item::Item;
use workspace::{item::Item, Workspace};
struct SelectionLayout {
head: DisplayPoint,
@@ -303,6 +306,7 @@ impl EditorElement {
register_action(view, cx, Editor::copy_permalink_to_line);
register_action(view, cx, Editor::open_permalink_to_line);
register_action(view, cx, Editor::toggle_git_blame);
register_action(view, cx, Editor::toggle_git_blame_inline);
register_action(view, cx, |editor, action, cx| {
if let Some(task) = editor.format(action, cx) {
task.detach_and_log_err(cx);
@@ -961,11 +965,11 @@ impl EditorElement {
// Git
(is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_git_diffs())
||
// Selections
(is_singleton && scrollbar_settings.selections && editor.has_background_highlights::<BufferSearchHighlights>())
// Buffer Search Results
(is_singleton && scrollbar_settings.search_results && editor.has_background_highlights::<BufferSearchHighlights>())
||
// Symbols Selections
(is_singleton && scrollbar_settings.symbols_selections && (editor.has_background_highlights::<DocumentHighlightRead>() || editor.has_background_highlights::<DocumentHighlightWrite>()))
// Selected Symbol Occurrences
(is_singleton && scrollbar_settings.selected_symbol && (editor.has_background_highlights::<DocumentHighlightRead>() || editor.has_background_highlights::<DocumentHighlightWrite>()))
||
// Diagnostics
(is_singleton && scrollbar_settings.diagnostics && snapshot.buffer_snapshot.has_diagnostics())
@@ -1092,6 +1096,63 @@ impl EditorElement {
.collect()
}
#[allow(clippy::too_many_arguments)]
fn layout_inline_blame(
&self,
display_row: u32,
display_snapshot: &DisplaySnapshot,
line_layout: &LineWithInvisibles,
em_width: Pixels,
content_origin: gpui::Point<Pixels>,
scroll_pixel_position: gpui::Point<Pixels>,
line_height: Pixels,
cx: &mut ElementContext,
) -> Option<AnyElement> {
if !self
.editor
.update(cx, |editor, cx| editor.render_git_blame_inline(cx))
{
return None;
}
let workspace = self
.editor
.read(cx)
.workspace
.as_ref()
.map(|(w, _)| w.clone());
let display_point = DisplayPoint::new(display_row, 0);
let buffer_row = display_point.to_point(display_snapshot).row;
let blame = self.editor.read(cx).blame.clone()?;
let blame_entry = blame
.update(cx, |blame, cx| {
blame.blame_for_rows([Some(buffer_row)], cx).next()
})
.flatten()?;
let mut element =
render_inline_blame_entry(&blame, blame_entry, &self.style, workspace, cx);
let start_y = content_origin.y
+ line_height * (display_row as f32 - scroll_pixel_position.y / line_height);
let start_x = {
const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 6.;
let line_width = line_layout.line.width;
content_origin.x + line_width + (em_width * INLINE_BLAME_PADDING_EM_WIDTHS)
};
let absolute_offset = point(start_x, start_y);
let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
element.layout(absolute_offset, available_space, cx);
Some(element)
}
#[allow(clippy::too_many_arguments)]
fn layout_blame_entries(
&self,
@@ -1103,10 +1164,14 @@ impl EditorElement {
max_width: Option<Pixels>,
cx: &mut ElementContext,
) -> Option<Vec<AnyElement>> {
let Some(blame) = self.editor.read(cx).blame.as_ref().cloned() else {
if !self
.editor
.update(cx, |editor, cx| editor.render_git_blame_gutter(cx))
{
return None;
};
}
let blame = self.editor.read(cx).blame.clone()?;
let blamed_rows: Vec<_> = blame.update(cx, |blame, cx| {
blame.blame_for_rows(buffer_rows, cx).collect()
});
@@ -1120,7 +1185,6 @@ impl EditorElement {
let start_x = em_width * 1;
let mut last_used_color: Option<(PlayerColor, Oid)> = None;
let text_style = &self.style.text;
let shaped_lines = blamed_rows
.into_iter()
@@ -1131,7 +1195,7 @@ impl EditorElement {
ix,
&blame,
blame_entry,
text_style,
&self.style,
&mut last_used_color,
self.editor.clone(),
cx,
@@ -2256,6 +2320,7 @@ impl EditorElement {
self.paint_lines(&invisible_display_ranges, layout, cx);
self.paint_redactions(layout, cx);
self.paint_cursors(layout, cx);
self.paint_inline_blame(layout, cx);
},
)
}
@@ -2559,10 +2624,14 @@ impl EditorElement {
for (background_highlight_id, (_, background_ranges)) in
background_highlights.iter()
{
if (*background_highlight_id
== TypeId::of::<BufferSearchHighlights>()
&& scrollbar_settings.selections)
|| scrollbar_settings.symbols_selections
let is_search_highlights = *background_highlight_id
== TypeId::of::<BufferSearchHighlights>();
let is_symbol_occurrences = *background_highlight_id
== TypeId::of::<DocumentHighlightRead>()
|| *background_highlight_id
== TypeId::of::<DocumentHighlightWrite>();
if (is_search_highlights && scrollbar_settings.search_results)
|| (is_symbol_occurrences && scrollbar_settings.selected_symbol)
{
let marker_row_ranges =
background_ranges.into_iter().map(|range| {
@@ -2730,6 +2799,14 @@ impl EditorElement {
})
}
fn paint_inline_blame(&mut self, layout: &mut EditorLayout, cx: &mut ElementContext) {
if let Some(mut inline_blame) = layout.inline_blame.take() {
cx.paint_layer(layout.text_hitbox.bounds, |cx| {
inline_blame.paint(cx);
})
}
}
fn paint_blocks(&mut self, layout: &mut EditorLayout, cx: &mut ElementContext) {
for mut block in layout.blocks.drain(..) {
block.element.paint(cx);
@@ -2749,6 +2826,10 @@ impl EditorElement {
let hitbox = layout.hitbox.clone();
let mut delta = ScrollDelta::default();
// Set a minimum scroll_sensitivity of 0.01 to make sure the user doesn't
// accidentally turn off their scrolling.
let scroll_sensitivity = EditorSettings::get_global(cx).scroll_sensitivity.max(0.01);
move |event: &ScrollWheelEvent, phase, cx| {
if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
delta = delta.coalesce(event.delta);
@@ -2773,8 +2854,11 @@ impl EditorElement {
};
let scroll_position = position_map.snapshot.scroll_position();
let x = (scroll_position.x * max_glyph_width - delta.x) / max_glyph_width;
let y = (scroll_position.y * line_height - delta.y) / line_height;
let x = (scroll_position.x * max_glyph_width
- (delta.x * scroll_sensitivity))
/ max_glyph_width;
let y = (scroll_position.y * line_height - (delta.y * scroll_sensitivity))
/ line_height;
let scroll_position =
point(x, y).clamp(&point(0., 0.), &position_map.scroll_max);
editor.scroll(scroll_position, axis, cx);
@@ -2894,11 +2978,194 @@ impl EditorElement {
}
}
fn render_inline_blame_entry(
blame: &gpui::Model<GitBlame>,
blame_entry: BlameEntry,
style: &EditorStyle,
workspace: Option<WeakView<Workspace>>,
cx: &mut ElementContext<'_>,
) -> AnyElement {
let relative_timestamp = blame_entry_relative_timestamp(&blame_entry, cx);
let author = blame_entry.author.as_deref().unwrap_or_default();
let text = format!("{}, {}", author, relative_timestamp);
let details = blame.read(cx).details_for_entry(&blame_entry);
let tooltip = cx.new_view(|_| BlameEntryTooltip::new(blame_entry, details, style, workspace));
h_flex()
.id("inline-blame")
.w_full()
.font(style.text.font().family)
.text_color(cx.theme().status().hint)
.line_height(style.text.line_height)
.child(Icon::new(IconName::FileGit).color(Color::Hint))
.child(text)
.gap_2()
.hoverable_tooltip(move |_| tooltip.clone().into())
.into_any()
}
fn blame_entry_timestamp(
blame_entry: &BlameEntry,
format: time_format::TimestampFormat,
cx: &WindowContext,
) -> String {
match blame_entry.author_offset_date_time() {
Ok(timestamp) => time_format::format_localized_timestamp(
timestamp,
time::OffsetDateTime::now_utc(),
cx.local_timezone(),
format,
),
Err(_) => "Error parsing date".to_string(),
}
}
fn blame_entry_relative_timestamp(blame_entry: &BlameEntry, cx: &WindowContext) -> String {
blame_entry_timestamp(blame_entry, time_format::TimestampFormat::Relative, cx)
}
fn blame_entry_absolute_timestamp(blame_entry: &BlameEntry, cx: &WindowContext) -> String {
blame_entry_timestamp(
blame_entry,
time_format::TimestampFormat::MediumAbsolute,
cx,
)
}
struct BlameEntryTooltip {
blame_entry: BlameEntry,
details: Option<CommitDetails>,
style: EditorStyle,
workspace: Option<WeakView<Workspace>>,
scroll_handle: ScrollHandle,
}
impl BlameEntryTooltip {
fn new(
blame_entry: BlameEntry,
details: Option<CommitDetails>,
style: &EditorStyle,
workspace: Option<WeakView<Workspace>>,
) -> Self {
Self {
style: style.clone(),
blame_entry,
details,
workspace,
scroll_handle: ScrollHandle::new(),
}
}
}
impl Render for BlameEntryTooltip {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let author = self
.blame_entry
.author
.clone()
.unwrap_or("<no name>".to_string());
let author_email = self.blame_entry.author_mail.clone();
let pretty_commit_id = format!("{}", self.blame_entry.sha);
let short_commit_id = pretty_commit_id.chars().take(6).collect::<String>();
let absolute_timestamp = blame_entry_absolute_timestamp(&self.blame_entry, cx);
let message = self
.details
.as_ref()
.map(|details| {
crate::render_parsed_markdown(
"blame-message",
&details.parsed_message,
&self.style,
self.workspace.clone(),
cx,
)
.into_any()
})
.unwrap_or("<no commit message>".into_any());
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size;
let message_max_height = cx.line_height() * 12 + (ui_font_size / 0.4);
tooltip_container(cx, move |this, cx| {
this.occlude()
.on_mouse_move(|_, cx| cx.stop_propagation())
.child(
v_flex()
.w(gpui::rems(30.))
.gap_4()
.child(
h_flex()
.gap_x_2()
.overflow_x_hidden()
.flex_wrap()
.child(author)
.when_some(author_email, |this, author_email| {
this.child(
div()
.text_color(cx.theme().colors().text_muted)
.child(author_email),
)
})
.pb_1()
.border_b_1()
.border_color(cx.theme().colors().border),
)
.child(
div()
.id("inline-blame-commit-message")
.occlude()
.child(message)
.max_h(message_max_height)
.overflow_y_scroll()
.track_scroll(&self.scroll_handle),
)
.child(
h_flex()
.text_color(cx.theme().colors().text_muted)
.w_full()
.justify_between()
.child(absolute_timestamp)
.child(
Button::new("commit-sha-button", short_commit_id.clone())
.style(ButtonStyle::Transparent)
.color(Color::Muted)
.icon(IconName::FileGit)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.disabled(
self.details.as_ref().map_or(true, |details| {
details.permalink.is_none()
}),
)
.when_some(
self.details
.as_ref()
.and_then(|details| details.permalink.clone()),
|this, url| {
this.on_click(move |_, cx| {
cx.stop_propagation();
cx.open_url(url.as_str())
})
},
),
),
),
)
})
}
}
fn render_blame_entry(
ix: usize,
blame: &gpui::Model<GitBlame>,
blame_entry: BlameEntry,
text_style: &TextStyle,
style: &EditorStyle,
last_used_color: &mut Option<(PlayerColor, Oid)>,
editor: View<Editor>,
cx: &mut ElementContext<'_>,
@@ -2918,29 +3185,26 @@ fn render_blame_entry(
};
last_used_color.replace((sha_color, blame_entry.sha));
let relative_timestamp = match blame_entry.author_offset_date_time() {
Ok(timestamp) => time_format::format_localized_timestamp(
timestamp,
time::OffsetDateTime::now_utc(),
cx.local_timezone(),
time_format::TimestampFormat::Relative,
),
Err(_) => "Error parsing date".to_string(),
};
let relative_timestamp = blame_entry_relative_timestamp(&blame_entry, cx);
let pretty_commit_id = format!("{}", blame_entry.sha);
let short_commit_id = pretty_commit_id.clone().chars().take(6).collect::<String>();
let short_commit_id = pretty_commit_id.chars().take(6).collect::<String>();
let author_name = blame_entry.author.as_deref().unwrap_or("<no name>");
let name = util::truncate_and_trailoff(author_name, 20);
let permalink = blame.read(cx).permalink_for_entry(&blame_entry);
let commit_message = blame.read(cx).message_for_entry(&blame_entry);
let details = blame.read(cx).details_for_entry(&blame_entry);
let workspace = editor.read(cx).workspace.as_ref().map(|(w, _)| w.clone());
let tooltip = cx.new_view(|_| {
BlameEntryTooltip::new(blame_entry.clone(), details.clone(), style, workspace)
});
h_flex()
.w_full()
.font(text_style.font().family)
.line_height(text_style.line_height)
.font(style.text.font().family)
.line_height(style.text.line_height)
.id(("blame", ix))
.children([
div()
@@ -2962,21 +3226,17 @@ fn render_blame_entry(
}
})
.hover(|style| style.bg(cx.theme().colors().element_hover))
.when_some(permalink, |this, url| {
let url = url.clone();
this.cursor_pointer().on_click(move |_, cx| {
cx.stop_propagation();
cx.open_url(url.as_str())
})
})
.tooltip(move |cx| {
BlameEntryTooltip::new(
sha_color.cursor,
commit_message.clone(),
blame_entry.clone(),
cx,
)
})
.when_some(
details.and_then(|details| details.permalink),
|this, url| {
let url = url.clone();
this.cursor_pointer().on_click(move |_, cx| {
cx.stop_propagation();
cx.open_url(url.as_str())
})
},
)
.hoverable_tooltip(move |_| tooltip.clone().into())
.into_any()
}
@@ -2999,84 +3259,6 @@ fn deploy_blame_entry_context_menu(
});
}
struct BlameEntryTooltip {
color: Hsla,
commit_message: Option<String>,
blame_entry: BlameEntry,
}
impl BlameEntryTooltip {
fn new(
color: Hsla,
commit_message: Option<String>,
blame_entry: BlameEntry,
cx: &mut WindowContext,
) -> AnyView {
cx.new_view(|_cx| Self {
color,
commit_message,
blame_entry,
})
.into()
}
}
impl Render for BlameEntryTooltip {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let author = self
.blame_entry
.author
.clone()
.unwrap_or("<no name>".to_string());
let author_email = self.blame_entry.author_mail.clone().unwrap_or_default();
let absolute_timestamp = match self.blame_entry.author_offset_date_time() {
Ok(timestamp) => time_format::format_localized_timestamp(
timestamp,
time::OffsetDateTime::now_utc(),
cx.local_timezone(),
time_format::TimestampFormat::Absolute,
),
Err(_) => "Error parsing date".to_string(),
};
let message = match &self.commit_message {
Some(message) => util::truncate_lines_and_trailoff(message, 15),
None => self.blame_entry.summary.clone().unwrap_or_default(),
};
let pretty_commit_id = format!("{}", self.blame_entry.sha);
tooltip_container(cx, move |this, cx| {
this.occlude()
.on_mouse_move(|_, cx| cx.stop_propagation())
.child(
v_flex()
.child(
h_flex()
.child(
div()
.text_color(cx.theme().colors().text_muted)
.child("Commit")
.pr_2(),
)
.child(
div().text_color(self.color).child(pretty_commit_id.clone()),
),
)
.child(
div()
.child(format!(
"{} {} - {}",
author, author_email, absolute_timestamp
))
.text_color(cx.theme().colors().text_muted),
)
.child(div().child(message)),
)
})
}
}
#[derive(Debug)]
pub(crate) struct LineWithInvisibles {
pub line: ShapedLine,
@@ -3205,13 +3387,9 @@ impl LineWithInvisibles {
let line_y =
line_height * (row as f32 - layout.position_map.scroll_pixel_position.y / line_height);
self.line
.paint(
content_origin + gpui::point(-layout.position_map.scroll_pixel_position.x, line_y),
line_height,
cx,
)
.log_err();
let line_origin =
content_origin + gpui::point(-layout.position_map.scroll_pixel_position.x, line_y);
self.line.paint(line_origin, line_height, cx).log_err();
self.draw_invisibles(
&selection_ranges,
@@ -3371,6 +3549,7 @@ impl Element for EditorElement {
let overscroll = size(em_width, px(0.));
snapshot = self.editor.update(cx, |editor, cx| {
editor.last_bounds = Some(bounds);
editor.gutter_width = gutter_dimensions.width;
editor.set_visible_line_count(bounds.size.height / line_height, cx);
@@ -3419,7 +3598,7 @@ impl Element for EditorElement {
let autoscroll_horizontally = self.editor.update(cx, |editor, cx| {
let autoscroll_horizontally =
editor.autoscroll_vertically(bounds.size.height, line_height, cx);
editor.autoscroll_vertically(bounds, line_height, cx);
snapshot = editor.snapshot(cx);
autoscroll_horizontally
});
@@ -3489,16 +3668,6 @@ impl Element for EditorElement {
let display_hunks = self.layout_git_gutters(start_row..end_row, &snapshot);
let blamed_display_rows = self.layout_blame_entries(
buffer_rows,
em_width,
scroll_position,
line_height,
&gutter_hitbox,
gutter_dimensions.git_blame_entries_width,
cx,
);
let mut max_visible_line_width = Pixels::ZERO;
let line_layouts =
self.layout_lines(start_row..end_row, &line_numbers, &snapshot, cx);
@@ -3527,6 +3696,39 @@ impl Element for EditorElement {
cx,
);
let scroll_pixel_position = point(
scroll_position.x * em_width,
scroll_position.y * line_height,
);
let mut inline_blame = None;
if let Some(newest_selection_head) = newest_selection_head {
let display_row = newest_selection_head.row();
if (start_row..end_row).contains(&display_row) {
let line_layout = &line_layouts[(display_row - start_row) as usize];
inline_blame = self.layout_inline_blame(
display_row,
&snapshot.display_snapshot,
line_layout,
em_width,
content_origin,
scroll_pixel_position,
line_height,
cx,
);
}
}
let blamed_display_rows = self.layout_blame_entries(
buffer_rows,
em_width,
scroll_position,
line_height,
&gutter_hitbox,
gutter_dimensions.git_blame_entries_width,
cx,
);
let scroll_max = point(
((scroll_width - text_hitbox.size.width) / em_width).max(0.0),
max_row as f32,
@@ -3554,11 +3756,6 @@ impl Element for EditorElement {
}
});
let scroll_pixel_position = point(
scroll_position.x * em_width,
scroll_position.y * line_height,
);
cx.with_element_id(Some("blocks"), |cx| {
self.layout_blocks(
&mut blocks,
@@ -3727,6 +3924,7 @@ impl Element for EditorElement {
line_numbers,
display_hunks,
blamed_display_rows,
inline_blame,
folds,
blocks,
cursors,
@@ -3814,6 +4012,7 @@ pub struct EditorLayout {
line_numbers: Vec<Option<ShapedLine>>,
display_hunks: Vec<DisplayDiffHunk>,
blamed_display_rows: Option<Vec<AnyElement>>,
inline_blame: Option<AnyElement>,
folds: Vec<FoldLayout>,
blocks: Vec<BlockLayout>,
highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,

View File

@@ -1,3 +1,5 @@
use std::{sync::Arc, time::Duration};
use anyhow::Result;
use collections::HashMap;
use git::{
@@ -5,7 +7,7 @@ use git::{
Oid,
};
use gpui::{Model, ModelContext, Subscription, Task};
use language::{Bias, Buffer, BufferSnapshot, Edit};
use language::{markdown, Bias, Buffer, BufferSnapshot, Edit, LanguageRegistry, ParsedMarkdown};
use project::{Item, Project};
use smallvec::SmallVec;
use sum_tree::SumTree;
@@ -44,23 +46,32 @@ impl<'a> sum_tree::Dimension<'a, GitBlameEntrySummary> for u32 {
}
}
#[derive(Clone, Debug)]
pub struct CommitDetails {
pub message: String,
pub parsed_message: ParsedMarkdown,
pub permalink: Option<Url>,
}
pub struct GitBlame {
project: Model<Project>,
buffer: Model<Buffer>,
entries: SumTree<GitBlameEntry>,
permalinks: HashMap<Oid, Url>,
messages: HashMap<Oid, String>,
commit_details: HashMap<Oid, CommitDetails>,
buffer_snapshot: BufferSnapshot,
buffer_edits: text::Subscription,
task: Task<Result<()>>,
generated: bool,
_refresh_subscription: Subscription,
user_triggered: bool,
regenerate_on_edit_task: Task<Result<()>>,
_regenerate_subscriptions: Vec<Subscription>,
}
impl GitBlame {
pub fn new(
buffer: Model<Buffer>,
project: Model<Project>,
user_triggered: bool,
cx: &mut ModelContext<Self>,
) -> Self {
let entries = SumTree::from_item(
@@ -71,7 +82,19 @@ impl GitBlame {
&(),
);
let refresh_subscription = cx.subscribe(&project, {
let buffer_subscriptions = cx.subscribe(&buffer, |this, buffer, event, cx| match event {
language::Event::DirtyChanged => {
if !buffer.read(cx).is_dirty() {
this.generate(cx);
}
}
language::Event::Edited => {
this.regenerate_on_edit(cx);
}
_ => {}
});
let project_subscription = cx.subscribe(&project, {
let buffer = buffer.clone();
move |this, _, event, cx| match event {
@@ -102,11 +125,12 @@ impl GitBlame {
buffer_snapshot,
entries,
buffer_edits,
permalinks: HashMap::default(),
messages: HashMap::default(),
user_triggered,
commit_details: HashMap::default(),
task: Task::ready(Ok(())),
generated: false,
_refresh_subscription: refresh_subscription,
regenerate_on_edit_task: Task::ready(Ok(())),
_regenerate_subscriptions: vec![buffer_subscriptions, project_subscription],
};
this.generate(cx);
this
@@ -116,12 +140,8 @@ impl GitBlame {
self.generated
}
pub fn permalink_for_entry(&self, entry: &BlameEntry) -> Option<Url> {
self.permalinks.get(&entry.sha).cloned()
}
pub fn message_for_entry(&self, entry: &BlameEntry) -> Option<String> {
self.messages.get(&entry.sha).cloned()
pub fn details_for_entry(&self, entry: &BlameEntry) -> Option<CommitDetails> {
self.commit_details.get(&entry.sha).cloned()
}
pub fn blame_for_rows<'a>(
@@ -254,9 +274,10 @@ impl GitBlame {
let buffer_edits = self.buffer.update(cx, |buffer, _| buffer.subscribe());
let snapshot = self.buffer.read(cx).snapshot();
let blame = self.project.read(cx).blame_buffer(&self.buffer, None, cx);
let languages = self.project.read(cx).languages().clone();
self.task = cx.spawn(|this, mut cx| async move {
let (entries, permalinks, messages) = cx
let result = cx
.background_executor()
.spawn({
let snapshot = snapshot.clone();
@@ -267,56 +288,133 @@ impl GitBlame {
messages,
} = blame.await?;
let mut current_row = 0;
let mut entries = SumTree::from_iter(
entries.into_iter().flat_map(|entry| {
let mut entries = SmallVec::<[GitBlameEntry; 2]>::new();
let entries = build_blame_entry_sum_tree(entries, snapshot.max_point().row);
let commit_details =
parse_commit_messages(messages, &permalinks, &languages).await;
if entry.range.start > current_row {
let skipped_rows = entry.range.start - current_row;
entries.push(GitBlameEntry {
rows: skipped_rows,
blame: None,
});
}
entries.push(GitBlameEntry {
rows: entry.range.len() as u32,
blame: Some(entry.clone()),
});
current_row = entry.range.end;
entries
}),
&(),
);
let max_row = snapshot.max_point().row;
if max_row >= current_row {
entries.push(
GitBlameEntry {
rows: (max_row + 1) - current_row,
blame: None,
},
&(),
);
}
anyhow::Ok((entries, permalinks, messages))
anyhow::Ok((entries, commit_details))
}
})
.await?;
.await;
this.update(&mut cx, |this, cx| {
this.buffer_edits = buffer_edits;
this.buffer_snapshot = snapshot;
this.entries = entries;
this.permalinks = permalinks;
this.messages = messages;
this.generated = true;
cx.notify();
this.update(&mut cx, |this, cx| match result {
Ok((entries, commit_details)) => {
this.buffer_edits = buffer_edits;
this.buffer_snapshot = snapshot;
this.entries = entries;
this.commit_details = commit_details;
this.generated = true;
cx.notify();
}
Err(error) => this.project.update(cx, |_, cx| {
if this.user_triggered {
log::error!("failed to get git blame data: {error:?}");
let notification = format!("{:#}", error).trim().to_string();
cx.emit(project::Event::Notification(notification));
} else {
// If we weren't triggered by a user, we just log errors in the background, instead of sending
// notifications.
// Except for `NoRepositoryError`, which can happen often if a user has inline-blame turned on
// and opens a non-git file.
if error.downcast_ref::<project::NoRepositoryError>().is_none() {
log::error!("failed to get git blame data: {error:?}");
}
}
}),
})
});
}
fn regenerate_on_edit(&mut self, cx: &mut ModelContext<Self>) {
self.regenerate_on_edit_task = cx.spawn(|this, mut cx| async move {
cx.background_executor()
.timer(REGENERATE_ON_EDIT_DEBOUNCE_INTERVAL)
.await;
this.update(&mut cx, |this, cx| {
this.generate(cx);
})
});
}
}
const REGENERATE_ON_EDIT_DEBOUNCE_INTERVAL: Duration = Duration::from_secs(2);
fn build_blame_entry_sum_tree(entries: Vec<BlameEntry>, max_row: u32) -> SumTree<GitBlameEntry> {
let mut current_row = 0;
let mut entries = SumTree::from_iter(
entries.into_iter().flat_map(|entry| {
let mut entries = SmallVec::<[GitBlameEntry; 2]>::new();
if entry.range.start > current_row {
let skipped_rows = entry.range.start - current_row;
entries.push(GitBlameEntry {
rows: skipped_rows,
blame: None,
});
}
entries.push(GitBlameEntry {
rows: entry.range.len() as u32,
blame: Some(entry.clone()),
});
current_row = entry.range.end;
entries
}),
&(),
);
if max_row >= current_row {
entries.push(
GitBlameEntry {
rows: (max_row + 1) - current_row,
blame: None,
},
&(),
);
}
entries
}
async fn parse_commit_messages(
messages: impl IntoIterator<Item = (Oid, String)>,
permalinks: &HashMap<Oid, Url>,
languages: &Arc<LanguageRegistry>,
) -> HashMap<Oid, CommitDetails> {
let mut commit_details = HashMap::default();
for (oid, message) in messages {
let parsed_message = parse_markdown(&message, &languages).await;
let permalink = permalinks.get(&oid).cloned();
commit_details.insert(
oid,
CommitDetails {
message,
parsed_message,
permalink,
},
);
}
commit_details
}
async fn parse_markdown(text: &str, language_registry: &Arc<LanguageRegistry>) -> ParsedMarkdown {
let mut parsed_message = ParsedMarkdown::default();
markdown::parse_markdown_block(
text,
language_registry,
None,
&mut parsed_message.text,
&mut parsed_message.highlights,
&mut parsed_message.region_ranges,
&mut parsed_message.regions,
)
.await;
parsed_message
}
#[cfg(test)]
@@ -359,6 +457,54 @@ mod tests {
});
}
#[gpui::test]
async fn test_blame_error_notifications(cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/my-repo",
json!({
".git": {},
"file.txt": r#"
irrelevant contents
"#
.unindent()
}),
)
.await;
// Creating a GitBlame without a corresponding blame state
// will result in an error.
let project = Project::test(fs, ["/my-repo".as_ref()], cx).await;
let buffer = project
.update(cx, |project, cx| {
project.open_local_buffer("/my-repo/file.txt", cx)
})
.await
.unwrap();
let blame = cx.new_model(|cx| GitBlame::new(buffer.clone(), project.clone(), true, cx));
let event = project.next_event(cx).await;
assert_eq!(
event,
project::Event::Notification(
"Failed to blame \"file.txt\": failed to get blame for \"file.txt\"".to_string()
)
);
blame.update(cx, |blame, cx| {
assert_eq!(
blame
.blame_for_rows((0..1).map(Some), cx)
.collect::<Vec<_>>(),
vec![None]
);
});
}
#[gpui::test]
async fn test_blame_for_rows(cx: &mut gpui::TestAppContext) {
init_test(cx);
@@ -408,7 +554,7 @@ mod tests {
.await
.unwrap();
let git_blame = cx.new_model(|cx| GitBlame::new(buffer.clone(), project, cx));
let git_blame = cx.new_model(|cx| GitBlame::new(buffer.clone(), project, false, cx));
cx.executor().run_until_parked();
@@ -488,7 +634,7 @@ mod tests {
.await
.unwrap();
let git_blame = cx.new_model(|cx| GitBlame::new(buffer.clone(), project, cx));
let git_blame = cx.new_model(|cx| GitBlame::new(buffer.clone(), project, false, cx));
cx.executor().run_until_parked();
@@ -637,7 +783,7 @@ mod tests {
.await
.unwrap();
let git_blame = cx.new_model(|cx| GitBlame::new(buffer.clone(), project, cx));
let git_blame = cx.new_model(|cx| GitBlame::new(buffer.clone(), project, false, cx));
cx.executor().run_until_parked();
git_blame.update(cx, |blame, cx| blame.check_invariants(cx));

View File

@@ -30,7 +30,7 @@ use std::{
sync::Arc,
};
use text::{BufferId, Selection};
use theme::Theme;
use theme::{Theme, ThemeSettings};
use ui::{h_flex, prelude::*, Label};
use util::{paths::PathExt, ResultExt, TryFutureExt};
use workspace::item::{BreadcrumbText, FollowEvent, FollowableItemHandle};
@@ -735,9 +735,8 @@ impl Item for Editor {
buffer
.update(&mut cx, |buffer, cx| {
let version = buffer.saved_version().clone();
let fingerprint = buffer.saved_version_fingerprint();
let mtime = buffer.saved_mtime();
buffer.did_save(version, fingerprint, mtime, cx);
buffer.did_save(version, mtime, cx);
})
.ok();
}
@@ -828,13 +827,18 @@ impl Item for Editor {
.map(|path| path.to_string_lossy().to_string())
.unwrap_or_else(|| "untitled".to_string());
let settings = ThemeSettings::get_global(cx);
let mut breadcrumbs = vec![BreadcrumbText {
text: filename,
highlights: None,
font: Some(settings.buffer_font.clone()),
}];
breadcrumbs.extend(symbols.into_iter().map(|symbol| BreadcrumbText {
text: symbol.text,
highlights: Some(symbol.highlight_ranges),
font: Some(settings.buffer_font.clone()),
}));
Some(breadcrumbs)
}
@@ -1167,6 +1171,10 @@ impl SearchableItem for Editor {
&self.buffer().read(cx).snapshot(cx),
)
}
fn search_bar_visibility_changed(&mut self, _visible: bool, _cx: &mut ViewContext<Self>) {
self.expect_bounds_change = self.last_bounds;
}
}
pub fn active_match_index(

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ use gpui::AsyncAppContext;
use language::{
CodeLabel, HighlightId, Language, LanguageServerName, LspAdapter, LspAdapterDelegate,
};
use lsp::LanguageServerBinary;
use lsp::{CodeActionKind, LanguageServerBinary};
use serde::Serialize;
use serde_json::Value;
use std::ops::Range;
@@ -129,6 +129,23 @@ impl LspAdapter for ExtensionLspAdapter {
None
}
fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
let code_action_kinds = self
.extension
.manifest
.language_servers
.get(&self.language_server_id)
.and_then(|server| server.code_action_kinds.clone());
code_action_kinds.or(Some(vec![
CodeActionKind::EMPTY,
CodeActionKind::QUICKFIX,
CodeActionKind::REFACTOR,
CodeActionKind::REFACTOR_EXTRACT,
CodeActionKind::SOURCE,
]))
}
fn language_ids(&self) -> HashMap<String, String> {
// TODO: The language IDs can be provided via the language server options
// in `extension.toml now but we're leaving these existing usages in place temporarily

View File

@@ -106,6 +106,8 @@ pub struct LanguageServerManifestEntry {
languages: Vec<Arc<str>>,
#[serde(default)]
pub language_ids: HashMap<String, String>,
#[serde(default)]
pub code_action_kinds: Option<Vec<lsp::CodeActionKind>>,
}
impl LanguageServerManifestEntry {

View File

@@ -4,15 +4,22 @@ use ui::prelude::*;
#[derive(IntoElement)]
pub struct ExtensionCard {
overridden_by_dev_extension: bool,
children: SmallVec<[AnyElement; 2]>,
}
impl ExtensionCard {
pub fn new() -> Self {
Self {
overridden_by_dev_extension: false,
children: SmallVec::new(),
}
}
pub fn overridden_by_dev_extension(mut self, overridden: bool) -> Self {
self.overridden_by_dev_extension = overridden;
self
}
}
impl ParentElement for ExtensionCard {
@@ -34,7 +41,24 @@ impl RenderOnce for ExtensionCard {
.border_1()
.border_color(cx.theme().colors().border)
.rounded_md()
.children(self.children),
.children(self.children)
.when(self.overridden_by_dev_extension, |card| {
card.child(
h_flex()
.absolute()
.top_0()
.left_0()
.occlude()
.size_full()
.items_center()
.justify_center()
.bg(theme::color_alpha(
cx.theme().colors().elevated_surface_background,
0.8,
))
.child(Label::new("Overridden by dev extension.")),
)
}),
)
}
}

View File

@@ -48,6 +48,7 @@ const SUGGESTIONS_BY_EXTENSION_ID: &[(&str, &[&str])] = &[
("lua", &["lua"]),
("make", &["Makefile"]),
("nix", &["nix"]),
("nu", &["nu"]),
("ocaml", &["ml", "mli"]),
("php", &["php"]),
("prisma", &["prisma"]),
@@ -59,7 +60,9 @@ const SUGGESTIONS_BY_EXTENSION_ID: &[(&str, &[&str])] = &[
("svelte", &["svelte"]),
("swift", &["swift"]),
("templ", &["templ"]),
("terraform", &["tf", "tfvars", "hcl"]),
("toml", &["Cargo.lock", "toml"]),
("vue", &["vue"]),
("wgsl", &["wgsl"]),
("zig", &["zig"]),
];

View File

@@ -190,6 +190,15 @@ impl ExtensionsPage {
}
}
/// Returns whether a dev extension currently exists for the extension with the given ID.
fn dev_extension_exists(extension_id: &str, cx: &mut ViewContext<Self>) -> bool {
let extension_store = ExtensionStore::global(cx).read(cx);
extension_store
.dev_extensions()
.any(|dev_extension| dev_extension.id.as_ref() == extension_id)
}
fn extension_status(extension_id: &str, cx: &mut ViewContext<Self>) -> ExtensionStatus {
let extension_store = ExtensionStore::global(cx).read(cx);
@@ -417,13 +426,21 @@ impl ExtensionsPage {
) -> ExtensionCard {
let this = cx.view().clone();
let status = Self::extension_status(&extension.id, cx);
let has_dev_extension = Self::dev_extension_exists(&extension.id, cx);
let extension_id = extension.id.clone();
let (install_or_uninstall_button, upgrade_button) =
self.buttons_for_entry(extension, &status, cx);
self.buttons_for_entry(extension, &status, has_dev_extension, cx);
let version = extension.manifest.version.clone();
let repository_url = extension.manifest.repository.clone();
let installed_version = match status {
ExtensionStatus::Installed(installed_version) => Some(installed_version),
_ => None,
};
ExtensionCard::new()
.overridden_by_dev_extension(has_dev_extension)
.child(
h_flex()
.justify_between()
@@ -435,9 +452,14 @@ impl ExtensionsPage {
Headline::new(extension.manifest.name.clone())
.size(HeadlineSize::Medium),
)
.child(
Headline::new(format!("v{}", extension.manifest.version))
.size(HeadlineSize::XSmall),
.child(Headline::new(format!("v{version}")).size(HeadlineSize::XSmall))
.children(
installed_version
.filter(|installed_version| *installed_version != version)
.map(|installed_version| {
Headline::new(format!("(v{installed_version} installed)",))
.size(HeadlineSize::XSmall)
}),
),
)
.child(
@@ -577,16 +599,24 @@ impl ExtensionsPage {
&self,
extension: &ExtensionMetadata,
status: &ExtensionStatus,
has_dev_extension: bool,
cx: &mut ViewContext<Self>,
) -> (Button, Option<Button>) {
let is_compatible = extension::is_version_compatible(&extension);
let disabled = !is_compatible;
if has_dev_extension {
// If we have a dev extension for the given extension, just treat it as uninstalled.
// The button here is a placeholder, as it won't be interactable anyways.
return (
Button::new(SharedString::from(extension.id.clone()), "Install"),
None,
);
}
match status.clone() {
ExtensionStatus::NotInstalled => (
Button::new(SharedString::from(extension.id.clone()), "Install")
.disabled(disabled)
.on_click(cx.listener({
Button::new(SharedString::from(extension.id.clone()), "Install").on_click(
cx.listener({
let extension_id = extension.id.clone();
move |this, _, cx| {
this.telemetry
@@ -595,7 +625,8 @@ impl ExtensionsPage {
store.install_latest_extension(extension_id.clone(), cx)
});
}
})),
}),
),
None,
),
ExtensionStatus::Installing => (
@@ -626,7 +657,20 @@ impl ExtensionsPage {
} else {
Some(
Button::new(SharedString::from(extension.id.clone()), "Upgrade")
.disabled(disabled)
.when(!is_compatible, |upgrade_button| {
upgrade_button.disabled(true).tooltip({
let version = extension.manifest.version.clone();
move |cx| {
Tooltip::text(
format!(
"v{version} is not compatible with this version of Zed.",
),
cx,
)
}
})
})
.disabled(!is_compatible)
.on_click(cx.listener({
let extension_id = extension.id.clone();
let version = extension.manifest.version.clone();

View File

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

View File

@@ -66,7 +66,7 @@ taffy = { git = "https://github.com/DioxusLabs/taffy", rev = "1876f72bee5e376023
thiserror.workspace = true
time.workspace = true
util.workspace = true
uuid = { version = "1.1.2", features = ["v4", "v5"] }
uuid.workspace = true
waker-fn = "1.1.0"
[dev-dependencies]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1257,6 +1257,7 @@ where
/// origin: Point { x: 15.0, y: 15.0 },
/// size: Size { width: 15.0, height: 30.0 },
/// });
/// ```
pub fn map<U>(&self, f: impl Fn(T) -> U) -> Bounds<U>
where
U: Clone + Default + Debug,
@@ -1283,6 +1284,7 @@ where
/// origin: Point { x: 15.0, y: 15.0 },
/// size: Size { width: 10.0, height: 20.0 },
/// });
/// ```
pub fn map_origin(self, f: impl Fn(Point<T>) -> Point<T>) -> Bounds<T> {
Bounds {
origin: f(self.origin),

View File

@@ -162,7 +162,7 @@ impl BladeAtlasState {
usage = gpu::TextureUsage::COPY | gpu::TextureUsage::RESOURCE;
}
AtlasTextureKind::Polychrome => {
format = gpu::TextureFormat::Rgba8Unorm;
format = gpu::TextureFormat::Bgra8Unorm;
usage = gpu::TextureUsage::COPY | gpu::TextureUsage::RESOURCE;
}
AtlasTextureKind::Path => {

View File

@@ -95,6 +95,7 @@ impl Globals {
pub(crate) struct WaylandClientState {
globals: Globals,
wl_pointer: Option<wl_pointer::WlPointer>,
// Surface to Window mapping
windows: HashMap<ObjectId, WaylandWindowStatePtr>,
// Output to scale mapping
@@ -103,9 +104,13 @@ pub(crate) struct WaylandClientState {
click: ClickState,
repeat: KeyRepeat,
modifiers: Modifiers,
scroll_direction: f64,
axis_source: AxisSource,
mouse_location: Option<Point<Pixels>>,
continuous_scroll_delta: Option<Point<Pixels>>,
discrete_scroll_delta: Option<Point<f32>>,
vertical_modifier: f32,
horizontal_modifier: f32,
scroll_event_received: bool,
enter_token: Option<()>,
button_pressed: Option<MouseButton>,
mouse_focused_window: Option<WaylandWindowStatePtr>,
@@ -164,20 +169,21 @@ impl WaylandClientStatePtr {
#[derive(Clone)]
pub struct WaylandClient(Rc<RefCell<WaylandClientState>>);
const WL_SEAT_MIN_VERSION: u32 = 4;
const WL_OUTPUT_VERSION: u32 = 2;
fn wl_seat_version(version: u32) -> u32 {
if version >= wl_pointer::EVT_AXIS_VALUE120_SINCE {
wl_pointer::EVT_AXIS_VALUE120_SINCE
} else if version >= WL_SEAT_MIN_VERSION {
WL_SEAT_MIN_VERSION
} else {
// We rely on the wl_pointer.frame event
const WL_SEAT_MIN_VERSION: u32 = 5;
const WL_SEAT_MAX_VERSION: u32 = 9;
if version < WL_SEAT_MIN_VERSION {
panic!(
"wl_seat below required version: {} < {}",
version, WL_SEAT_MIN_VERSION
);
}
version.clamp(WL_SEAT_MIN_VERSION, WL_SEAT_MAX_VERSION)
}
impl WaylandClient {
@@ -235,6 +241,7 @@ impl WaylandClient {
let mut state = Rc::new(RefCell::new(WaylandClientState {
globals,
wl_pointer: None,
output_scales: outputs,
windows: HashMap::default(),
common,
@@ -257,9 +264,13 @@ impl WaylandClient {
function: false,
platform: false,
},
scroll_direction: -1.0,
scroll_event_received: false,
axis_source: AxisSource::Wheel,
mouse_location: None,
continuous_scroll_delta: None,
discrete_scroll_delta: None,
vertical_modifier: -1.0,
horizontal_modifier: -1.0,
button_pressed: None,
mouse_focused_window: None,
keyboard_focused_window: None,
@@ -334,7 +345,15 @@ impl LinuxClient for WaylandClient {
}
.to_string();
self.0.borrow_mut().cursor_icon_name = cursor_icon_name;
let mut state = self.0.borrow_mut();
state.cursor_icon_name = cursor_icon_name.clone();
if state.mouse_focused_window.is_some() {
let wl_pointer = state
.wl_pointer
.clone()
.expect("window is focused by pointer");
state.cursor.set_icon(&wl_pointer, &cursor_icon_name);
}
}
fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R {
@@ -394,6 +413,7 @@ impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for WaylandClientStat
version,
} => match &interface[..] {
"wl_seat" => {
state.wl_pointer = None;
registry.bind::<wl_seat::WlSeat, _, _>(name, wl_seat_version(version), qh, ());
}
"wl_output" => {
@@ -573,7 +593,9 @@ impl Dispatch<wl_seat::WlSeat, ()> for WaylandClientStatePtr {
seat.get_keyboard(qh, ());
}
if capabilities.contains(wl_seat::Capability::Pointer) {
seat.get_pointer(qh, ());
let client = state.get_client();
let mut state = client.borrow_mut();
state.wl_pointer = Some(seat.get_pointer(qh, ()));
}
}
}
@@ -780,10 +802,11 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
if let Some(window) = get_window(&mut state, &surface.id()) {
state.enter_token = Some(());
state.mouse_focused_window = Some(window.clone());
state.cursor.mark_dirty();
state.cursor.set_serial_id(serial);
state
.cursor
.set_icon(&wl_pointer, Some(cursor_icon_name.as_str()));
.set_icon(&wl_pointer, cursor_icon_name.as_str());
drop(state);
window.set_focused(true);
}
@@ -814,9 +837,6 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
return;
}
state.mouse_location = Some(point(px(surface_x as f32), px(surface_y as f32)));
state
.cursor
.set_icon(&wl_pointer, Some(cursor_icon_name.as_str()));
if let Some(window) = state.mouse_focused_window.clone() {
let input = PlatformInput::MouseMove(MouseMoveEvent {
@@ -887,77 +907,137 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
_ => {}
}
}
wl_pointer::Event::AxisRelativeDirection {
direction: WEnum::Value(direction),
..
} => {
state.scroll_direction = match direction {
AxisRelativeDirection::Identical => -1.0,
AxisRelativeDirection::Inverted => 1.0,
_ => -1.0,
}
}
// Axis Events
wl_pointer::Event::AxisSource {
axis_source: WEnum::Value(axis_source),
} => {
state.axis_source = axis_source;
}
wl_pointer::Event::AxisValue120 {
axis: WEnum::Value(axis),
value120,
} => {
if let Some(focused_window) = state.mouse_focused_window.clone() {
let value = value120 as f64 * state.scroll_direction;
let input = PlatformInput::ScrollWheel(ScrollWheelEvent {
position: state.mouse_location.unwrap(),
delta: match axis {
wl_pointer::Axis::VerticalScroll => {
ScrollDelta::Pixels(point(px(0.0), px(value as f32)))
}
wl_pointer::Axis::HorizontalScroll => {
ScrollDelta::Pixels(point(px(value as f32), px(0.0)))
}
_ => unimplemented!(),
},
modifiers: state.modifiers,
touch_phase: TouchPhase::Moved,
});
drop(state);
focused_window.handle_input(input)
}
}
wl_pointer::Event::Axis {
time,
axis: WEnum::Value(axis),
value,
..
} => {
// We handle discrete scroll events with `AxisValue120`.
if wl_pointer.version() >= wl_pointer::EVT_AXIS_VALUE120_SINCE
&& state.axis_source == AxisSource::Wheel
{
return;
let axis_source = state.axis_source;
let axis_modifier = match axis {
wl_pointer::Axis::VerticalScroll => state.vertical_modifier,
wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier,
_ => 1.0,
};
let supports_relative_direction =
wl_pointer.version() >= wl_pointer::EVT_AXIS_RELATIVE_DIRECTION_SINCE;
state.scroll_event_received = true;
let scroll_delta = state
.continuous_scroll_delta
.get_or_insert(point(px(0.0), px(0.0)));
// TODO: Make nice feeling kinetic scrolling that integrates with the platform's scroll settings
let modifier = 3.0;
match axis {
wl_pointer::Axis::VerticalScroll => {
scroll_delta.y += px(value as f32 * modifier * axis_modifier);
}
wl_pointer::Axis::HorizontalScroll => {
scroll_delta.x += px(value as f32 * modifier * axis_modifier);
}
_ => unreachable!(),
}
if let Some(focused_window) = state.mouse_focused_window.clone() {
let value = value * state.scroll_direction;
}
wl_pointer::Event::AxisDiscrete {
axis: WEnum::Value(axis),
discrete,
} => {
state.scroll_event_received = true;
let axis_modifier = match axis {
wl_pointer::Axis::VerticalScroll => state.vertical_modifier,
wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier,
_ => 1.0,
};
let input = PlatformInput::ScrollWheel(ScrollWheelEvent {
position: state.mouse_location.unwrap(),
delta: match axis {
wl_pointer::Axis::VerticalScroll => {
ScrollDelta::Pixels(point(px(0.0), px(value as f32)))
}
wl_pointer::Axis::HorizontalScroll => {
ScrollDelta::Pixels(point(px(value as f32), px(0.0)))
}
_ => unimplemented!(),
},
modifiers: state.modifiers,
touch_phase: TouchPhase::Moved,
});
drop(state);
focused_window.handle_input(input)
// TODO: Make nice feeling kinetic scrolling that integrates with the platform's scroll settings
let modifier = 3.0;
let scroll_delta = state.discrete_scroll_delta.get_or_insert(point(0.0, 0.0));
match axis {
wl_pointer::Axis::VerticalScroll => {
scroll_delta.y += discrete as f32 * axis_modifier * modifier;
}
wl_pointer::Axis::HorizontalScroll => {
scroll_delta.x += discrete as f32 * axis_modifier * modifier;
}
_ => unreachable!(),
}
}
wl_pointer::Event::AxisRelativeDirection {
axis: WEnum::Value(axis),
direction: WEnum::Value(direction),
} => match (axis, direction) {
(wl_pointer::Axis::VerticalScroll, AxisRelativeDirection::Identical) => {
state.vertical_modifier = -1.0
}
(wl_pointer::Axis::VerticalScroll, AxisRelativeDirection::Inverted) => {
state.vertical_modifier = 1.0
}
(wl_pointer::Axis::HorizontalScroll, AxisRelativeDirection::Identical) => {
state.horizontal_modifier = -1.0
}
(wl_pointer::Axis::HorizontalScroll, AxisRelativeDirection::Inverted) => {
state.horizontal_modifier = 1.0
}
_ => unreachable!(),
},
wl_pointer::Event::AxisValue120 {
axis: WEnum::Value(axis),
value120,
} => {
state.scroll_event_received = true;
let axis_modifier = match axis {
wl_pointer::Axis::VerticalScroll => state.vertical_modifier,
wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier,
_ => unreachable!(),
};
let scroll_delta = state.discrete_scroll_delta.get_or_insert(point(0.0, 0.0));
let wheel_percent = value120 as f32 / 120.0;
match axis {
wl_pointer::Axis::VerticalScroll => {
scroll_delta.y += wheel_percent * axis_modifier;
}
wl_pointer::Axis::HorizontalScroll => {
scroll_delta.x += wheel_percent * axis_modifier;
}
_ => unreachable!(),
}
}
wl_pointer::Event::Frame => {
if state.scroll_event_received {
state.scroll_event_received = false;
let continuous = state.continuous_scroll_delta.take();
let discrete = state.discrete_scroll_delta.take();
if let Some(continuous) = continuous {
if let Some(window) = state.mouse_focused_window.clone() {
let input = PlatformInput::ScrollWheel(ScrollWheelEvent {
position: state.mouse_location.unwrap(),
delta: ScrollDelta::Pixels(continuous),
modifiers: state.modifiers,
touch_phase: TouchPhase::Moved,
});
drop(state);
window.handle_input(input);
}
} else if let Some(discrete) = discrete {
if let Some(window) = state.mouse_focused_window.clone() {
let input = PlatformInput::ScrollWheel(ScrollWheelEvent {
position: state.mouse_location.unwrap(),
delta: ScrollDelta::Lines(discrete),
modifiers: state.modifiers,
touch_phase: TouchPhase::Moved,
});
drop(state);
window.handle_input(input);
}
}
}
}
_ => {}

View File

@@ -8,7 +8,7 @@ use wayland_cursor::{CursorImageBuffer, CursorTheme};
pub(crate) struct Cursor {
theme: Option<CursorTheme>,
current_icon_name: String,
current_icon_name: Option<String>,
surface: WlSurface,
serial_id: u32,
}
@@ -24,19 +24,29 @@ impl Cursor {
pub fn new(connection: &Connection, globals: &Globals, size: u32) -> Self {
Self {
theme: CursorTheme::load(&connection, globals.shm.clone(), size).log_err(),
current_icon_name: "default".to_string(),
current_icon_name: None,
surface: globals.compositor.create_surface(&globals.qh, ()),
serial_id: 0,
}
}
pub fn mark_dirty(&mut self) {
self.current_icon_name = None;
}
pub fn set_serial_id(&mut self, serial_id: u32) {
self.serial_id = serial_id;
}
pub fn set_icon(&mut self, wl_pointer: &WlPointer, mut cursor_icon_name: Option<&str>) {
let mut cursor_icon_name = cursor_icon_name.unwrap_or("default");
if self.current_icon_name != cursor_icon_name {
pub fn set_icon(&mut self, wl_pointer: &WlPointer, mut cursor_icon_name: &str) {
let need_update = self
.current_icon_name
.as_ref()
.map_or(true, |current_icon_name| {
current_icon_name != cursor_icon_name
});
if need_update {
if let Some(theme) = &mut self.theme {
let mut buffer: Option<&CursorImageBuffer>;
@@ -68,7 +78,7 @@ impl Cursor {
self.surface.damage(0, 0, width as i32, height as i32);
self.surface.commit();
self.current_icon_name = cursor_icon_name.to_string();
self.current_icon_name = Some(cursor_icon_name.to_string());
}
} else {
log::warn!("Linux: Wayland: Unable to load cursor themes");

View File

@@ -33,8 +33,10 @@ use super::X11Display;
x11rb::atom_manager! {
pub XcbAtoms: AtomsCookie {
UTF8_STRING,
WM_PROTOCOLS,
WM_DELETE_WINDOW,
_NET_WM_NAME,
_NET_WM_STATE,
_NET_WM_STATE_MAXIMIZED_VERT,
_NET_WM_STATE_MAXIMIZED_HORZ,
@@ -76,7 +78,7 @@ pub struct Callbacks {
pub(crate) struct X11WindowState {
raw: RawWindow,
atoms: XcbAtoms,
bounds: Bounds<i32>,
scale_factor: f32,
renderer: BladeRenderer,
@@ -238,6 +240,7 @@ impl X11WindowState {
bounds: params.bounds.map(|v| v.0),
scale_factor: 1.0,
renderer: BladeRenderer::new(gpu, gpu_extent),
atoms: *atoms,
input_handler: None,
}
@@ -442,6 +445,16 @@ impl PlatformWindow for X11Window {
title.as_bytes(),
)
.unwrap();
self.xcb_connection
.change_property8(
xproto::PropMode::REPLACE,
self.x_window,
self.state.borrow().atoms._NET_WM_NAME,
self.state.borrow().atoms.UTF8_STRING,
title.as_bytes(),
)
.unwrap();
}
// todo(linux)

View File

@@ -570,6 +570,7 @@ impl Platform for MacPlatform {
let _ = done_tx.send(result);
}
});
let block = block.copy();
let _: () = msg_send![workspace, setDefaultApplicationAtURL: app toOpenURLsWithScheme: scheme completionHandler: block];
}

View File

@@ -334,7 +334,7 @@ impl MacTextSystemState {
self.postscript_names_by_font_id
.get(&font_id)
.map_or(false, |postscript_name| {
postscript_name == "AppleColorEmoji"
postscript_name == "AppleColorEmoji" || postscript_name == ".AppleColorEmojiUI"
})
}

View File

@@ -121,21 +121,18 @@ impl WindowsDisplay {
}
pub(crate) fn frequency(&self) -> Option<u32> {
available_monitors()
.get(self.display_id.0 as usize)
.and_then(|hmonitor| get_monitor_info(*hmonitor).ok())
.and_then(|info| {
let mut devmode = DEVMODEW::default();
unsafe {
EnumDisplaySettingsW(
PCWSTR(info.szDevice.as_ptr()),
ENUM_CURRENT_SETTINGS,
&mut devmode,
)
}
.as_bool()
.then(|| devmode.dmDisplayFrequency)
})
get_monitor_info(self.handle).ok().and_then(|info| {
let mut devmode = DEVMODEW::default();
unsafe {
EnumDisplaySettingsW(
PCWSTR(info.szDevice.as_ptr()),
ENUM_CURRENT_SETTINGS,
&mut devmode,
)
}
.as_bool()
.then(|| devmode.dmDisplayFrequency)
})
}
}

View File

@@ -687,8 +687,10 @@ impl Platform for WindowsPlatform {
}
fn write_to_clipboard(&self, item: ClipboardItem) {
let mut ctx = ClipboardContext::new().unwrap();
ctx.set_contents(item.text().to_owned()).unwrap();
if item.text.len() > 0 {
let mut ctx = ClipboardContext::new().unwrap();
ctx.set_contents(item.text().to_owned()).unwrap();
}
}
fn read_from_clipboard(&self) -> Option<ClipboardItem> {

View File

@@ -52,7 +52,6 @@ pub(crate) struct WindowsWindowInner {
pub(crate) handle: AnyWindowHandle,
hide_title_bar: bool,
display: RefCell<Rc<WindowsDisplay>>,
last_ime_input: RefCell<Option<String>>,
click_state: RefCell<ClickState>,
fullscreen: Cell<Option<StyleAndBounds>>,
}
@@ -114,7 +113,6 @@ impl WindowsWindowInner {
let renderer = RefCell::new(BladeRenderer::new(gpu, extent));
let callbacks = RefCell::new(Callbacks::default());
let display = RefCell::new(display);
let last_ime_input = RefCell::new(None);
let click_state = RefCell::new(ClickState::new());
let fullscreen = Cell::new(None);
Self {
@@ -129,7 +127,6 @@ impl WindowsWindowInner {
handle,
hide_title_bar,
display,
last_ime_input,
click_state,
fullscreen,
}
@@ -816,6 +813,7 @@ impl WindowsWindowInner {
}
fn handle_ime_composition(&self, lparam: LPARAM) -> Option<isize> {
let mut ime_input = None;
if lparam.0 as u32 & GCS_COMPSTR.0 > 0 {
let Some((string, string_len)) = self.parse_ime_compostion_string() else {
return None;
@@ -829,10 +827,10 @@ impl WindowsWindowInner {
Some(0..string_len),
);
self.input_handler.set(Some(input_handler));
*self.last_ime_input.borrow_mut() = Some(string);
ime_input = Some(string);
}
if lparam.0 as u32 & GCS_CURSORPOS.0 > 0 {
let Some(ref comp_string) = *self.last_ime_input.borrow() else {
let Some(ref comp_string) = ime_input else {
return None;
};
let caret_pos = self.retrieve_composition_cursor_position();
@@ -863,7 +861,6 @@ impl WindowsWindowInner {
};
input_handler.replace_text_in_range(None, &ime_char);
self.input_handler.set(Some(input_handler));
*self.last_ime_input.borrow_mut() = None;
self.invalidate_client_area();
Some(0)
}

View File

@@ -287,6 +287,8 @@ pub struct Window {
pub(crate) rendered_frame: Frame,
pub(crate) next_frame: Frame,
pub(crate) next_hitbox_id: HitboxId,
pub(crate) next_tooltip_id: TooltipId,
pub(crate) tooltip_bounds: Option<TooltipBounds>,
next_frame_callbacks: Rc<RefCell<Vec<FrameCallback>>>,
pub(crate) dirty_views: FxHashSet<EntityId>,
pub(crate) focus_handles: Arc<RwLock<SlotMap<FocusId, AtomicUsize>>>,
@@ -551,6 +553,8 @@ impl Window {
next_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
next_frame_callbacks,
next_hitbox_id: HitboxId::default(),
next_tooltip_id: TooltipId::default(),
tooltip_bounds: None,
dirty_views: FxHashSet::default(),
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
focus_listeners: SubscriberSet::new(),

View File

@@ -15,7 +15,7 @@
use std::{
any::{Any, TypeId},
borrow::{Borrow, BorrowMut, Cow},
mem,
cmp, mem,
ops::Range,
rc::Rc,
sync::Arc,
@@ -28,17 +28,18 @@ use futures::{future::Shared, FutureExt};
#[cfg(target_os = "macos")]
use media::core_video::CVImageBuffer;
use smallvec::SmallVec;
use util::post_inc;
use crate::{
hash, prelude::*, size, AnyElement, AnyTooltip, AppContext, Asset, AvailableSpace, Bounds,
BoxShadow, ContentMask, Corners, CursorStyle, DevicePixels, DispatchNodeId, DispatchPhase,
DispatchTree, DrawPhase, ElementId, ElementStateBox, EntityId, FocusHandle, FocusId, FontId,
GlobalElementId, GlyphId, Hsla, ImageData, InputHandler, IsZero, KeyContext, KeyEvent,
LayoutId, LineLayoutIndex, ModifiersChangedEvent, MonochromeSprite, MouseEvent, PaintQuad,
Path, Pixels, PlatformInputHandler, Point, PolychromeSprite, Quad, RenderGlyphParams,
RenderImageParams, RenderSvgParams, Scene, Shadow, SharedString, Size, StrikethroughStyle,
Style, Task, TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, Window,
WindowContext, SUBPIXEL_VARIANTS,
hash, point, prelude::*, px, size, AnyElement, AnyTooltip, AppContext, Asset, AvailableSpace,
Bounds, BoxShadow, ContentMask, Corners, CursorStyle, DevicePixels, DispatchNodeId,
DispatchPhase, DispatchTree, DrawPhase, ElementId, ElementStateBox, EntityId, FocusHandle,
FocusId, FontId, GlobalElementId, GlyphId, Hsla, ImageData, InputHandler, IsZero, KeyContext,
KeyEvent, LayoutId, LineLayoutIndex, ModifiersChangedEvent, MonochromeSprite, MouseEvent,
PaintQuad, Path, Pixels, PlatformInputHandler, Point, PolychromeSprite, Quad,
RenderGlyphParams, RenderImageParams, RenderSvgParams, Scene, Shadow, SharedString, Size,
StrikethroughStyle, Style, Task, TextStyleRefinement, TransformationMatrix, Underline,
UnderlineStyle, Window, WindowContext, SUBPIXEL_VARIANTS,
};
pub(crate) type AnyMouseListener =
@@ -65,11 +66,13 @@ impl HitboxId {
/// See [ElementContext::insert_hitbox] for more details.
#[derive(Clone, Debug, Deref)]
pub struct Hitbox {
/// A unique identifier for the hitbox
/// A unique identifier for the hitbox.
pub id: HitboxId,
/// The bounds of the hitbox
/// The bounds of the hitbox.
#[deref]
pub bounds: Bounds<Pixels>,
/// The content mask when the hitbox was inserted.
pub content_mask: ContentMask<Pixels>,
/// Whether the hitbox occludes other hitboxes inserted prior.
pub opaque: bool,
}
@@ -84,6 +87,33 @@ impl Hitbox {
#[derive(Default, Eq, PartialEq)]
pub(crate) struct HitTest(SmallVec<[HitboxId; 8]>);
/// An identifier for a tooltip.
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub struct TooltipId(usize);
impl TooltipId {
/// Checks if the tooltip is currently hovered.
pub fn is_hovered(&self, cx: &WindowContext) -> bool {
cx.window
.tooltip_bounds
.as_ref()
.map_or(false, |tooltip_bounds| {
tooltip_bounds.id == *self && tooltip_bounds.bounds.contains(&cx.mouse_position())
})
}
}
pub(crate) struct TooltipBounds {
id: TooltipId,
bounds: Bounds<Pixels>,
}
#[derive(Clone)]
pub(crate) struct TooltipRequest {
id: TooltipId,
tooltip: AnyTooltip,
}
pub(crate) struct DeferredDraw {
priority: usize,
parent_node: DispatchNodeId,
@@ -108,7 +138,7 @@ pub(crate) struct Frame {
pub(crate) content_mask_stack: Vec<ContentMask<Pixels>>,
pub(crate) element_offset_stack: Vec<Point<Pixels>>,
pub(crate) input_handlers: Vec<Option<PlatformInputHandler>>,
pub(crate) tooltip_requests: Vec<Option<AnyTooltip>>,
pub(crate) tooltip_requests: Vec<Option<TooltipRequest>>,
pub(crate) cursor_styles: Vec<CursorStyleRequest>,
#[cfg(any(test, feature = "test-support"))]
pub(crate) debug_bounds: FxHashMap<String, Bounds<Pixels>>,
@@ -173,7 +203,8 @@ impl Frame {
pub(crate) fn hit_test(&self, position: Point<Pixels>) -> HitTest {
let mut hit_test = HitTest::default();
for hitbox in self.hitboxes.iter().rev() {
if hitbox.bounds.contains(&position) {
let bounds = hitbox.bounds.intersect(&hitbox.content_mask.bounds);
if bounds.contains(&position) {
hit_test.0.push(hitbox.id);
if hitbox.opaque {
break;
@@ -364,6 +395,7 @@ impl<'a> VisualContext for ElementContext<'a> {
impl<'a> ElementContext<'a> {
pub(crate) fn draw_roots(&mut self) {
self.window.draw_phase = DrawPhase::Layout;
self.window.tooltip_bounds.take();
// Layout all root elements.
let mut root_element = self.window.root_view.as_ref().unwrap().clone().into_any();
@@ -388,14 +420,8 @@ impl<'a> ElementContext<'a> {
element.layout(offset, AvailableSpace::min_size(), self);
active_drag_element = Some(element);
self.app.active_drag = Some(active_drag);
} else if let Some(tooltip_request) =
self.window.next_frame.tooltip_requests.last().cloned()
{
let tooltip_request = tooltip_request.unwrap();
let mut element = tooltip_request.view.clone().into_any();
let offset = tooltip_request.cursor_offset;
element.layout(offset, AvailableSpace::min_size(), self);
tooltip_element = Some(element);
} else {
tooltip_element = self.layout_tooltip();
}
self.window.mouse_hit_test = self.window.next_frame.hit_test(self.window.mouse_position);
@@ -415,6 +441,52 @@ impl<'a> ElementContext<'a> {
}
}
fn layout_tooltip(&mut self) -> Option<AnyElement> {
let tooltip_request = self.window.next_frame.tooltip_requests.last().cloned()?;
let tooltip_request = tooltip_request.unwrap();
let mut element = tooltip_request.tooltip.view.clone().into_any();
let mouse_position = tooltip_request.tooltip.mouse_position;
let tooltip_size = element.measure(AvailableSpace::min_size(), self);
let mut tooltip_bounds = Bounds::new(mouse_position + point(px(1.), px(1.)), tooltip_size);
let window_bounds = Bounds {
origin: Point::default(),
size: self.viewport_size(),
};
if tooltip_bounds.right() > window_bounds.right() {
let new_x = mouse_position.x - tooltip_bounds.size.width - px(1.);
if new_x >= Pixels::ZERO {
tooltip_bounds.origin.x = new_x;
} else {
tooltip_bounds.origin.x = cmp::max(
Pixels::ZERO,
tooltip_bounds.origin.x - tooltip_bounds.right() - window_bounds.right(),
);
}
}
if tooltip_bounds.bottom() > window_bounds.bottom() {
let new_y = mouse_position.y - tooltip_bounds.size.height - px(1.);
if new_y >= Pixels::ZERO {
tooltip_bounds.origin.y = new_y;
} else {
tooltip_bounds.origin.y = cmp::max(
Pixels::ZERO,
tooltip_bounds.origin.y - tooltip_bounds.bottom() - window_bounds.bottom(),
);
}
}
self.with_absolute_element_offset(tooltip_bounds.origin, |cx| element.after_layout(cx));
self.window.tooltip_bounds = Some(TooltipBounds {
id: tooltip_request.id,
bounds: tooltip_bounds,
});
Some(element)
}
fn layout_deferred_draws(&mut self, deferred_draw_indices: &[usize]) {
assert_eq!(self.window.element_id_stack.len(), 0);
@@ -604,8 +676,13 @@ impl<'a> ElementContext<'a> {
}
/// Sets a tooltip to be rendered for the upcoming frame
pub fn set_tooltip(&mut self, tooltip: AnyTooltip) {
self.window.next_frame.tooltip_requests.push(Some(tooltip));
pub fn set_tooltip(&mut self, tooltip: AnyTooltip) -> TooltipId {
let id = TooltipId(post_inc(&mut self.window.next_tooltip_id.0));
self.window
.next_frame
.tooltip_requests
.push(Some(TooltipRequest { id, tooltip }));
id
}
/// Pushes the given element id onto the global stack and invokes the given closure
@@ -1330,7 +1407,8 @@ impl<'a> ElementContext<'a> {
window.next_hitbox_id.0 += 1;
let hitbox = Hitbox {
id,
bounds: bounds.intersect(&content_mask.bounds),
bounds,
content_mask,
opaque,
};
window.next_frame.hitboxes.push(hitbox.clone());

View File

@@ -45,9 +45,9 @@ use text::operation_queue::OperationQueue;
use text::*;
pub use text::{
Anchor, Bias, Buffer as TextBuffer, BufferId, BufferSnapshot as TextBufferSnapshot, Edit,
OffsetRangeExt, OffsetUtf16, Patch, Point, PointUtf16, Rope, RopeFingerprint, Selection,
SelectionGoal, Subscription, TextDimension, TextSummary, ToOffset, ToOffsetUtf16, ToPoint,
ToPointUtf16, Transaction, TransactionId, Unclipped,
OffsetRangeExt, OffsetUtf16, Patch, Point, PointUtf16, Rope, Selection, SelectionGoal,
Subscription, TextDimension, TextSummary, ToOffset, ToOffsetUtf16, ToPoint, ToPointUtf16,
Transaction, TransactionId, Unclipped,
};
use theme::SyntaxTheme;
#[cfg(any(test, feature = "test-support"))]
@@ -87,8 +87,6 @@ pub struct Buffer {
/// The version vector when this buffer was last loaded from
/// or saved to disk.
saved_version: clock::Global,
/// A hash of the current contents of the buffer's file.
file_fingerprint: RopeFingerprint,
transaction_depth: usize,
was_dirty_before_starting_transaction: Option<bool>,
reload_task: Option<Task<Result<()>>>,
@@ -379,7 +377,6 @@ pub trait LocalFile: File {
&self,
buffer_id: BufferId,
version: &clock::Global,
fingerprint: RopeFingerprint,
line_ending: LineEnding,
mtime: Option<SystemTime>,
cx: &mut AppContext,
@@ -562,7 +559,6 @@ impl Buffer {
diff_base: self.diff_base.as_ref().map(|h| h.to_string()),
line_ending: proto::serialize_line_ending(self.line_ending()) as i32,
saved_version: proto::serialize_version(&self.saved_version),
saved_version_fingerprint: proto::serialize_fingerprint(self.file_fingerprint),
saved_mtime: self.saved_mtime.map(|time| time.into()),
}
}
@@ -642,7 +638,6 @@ impl Buffer {
Self {
saved_mtime,
saved_version: buffer.version(),
file_fingerprint: buffer.as_rope().fingerprint(),
reload_task: None,
transaction_depth: 0,
was_dirty_before_starting_transaction: None,
@@ -717,11 +712,6 @@ impl Buffer {
&self.saved_version
}
/// The fingerprint of the buffer's text when the buffer was last saved or reloaded from disk.
pub fn saved_version_fingerprint(&self) -> RopeFingerprint {
self.file_fingerprint
}
/// The mtime of the buffer's file when the buffer was last saved or reloaded from disk.
pub fn saved_mtime(&self) -> Option<SystemTime> {
self.saved_mtime
@@ -754,13 +744,11 @@ impl Buffer {
pub fn did_save(
&mut self,
version: clock::Global,
fingerprint: RopeFingerprint,
mtime: Option<SystemTime>,
cx: &mut ModelContext<Self>,
) {
self.saved_version = version;
self.has_conflict = false;
self.file_fingerprint = fingerprint;
self.saved_mtime = mtime;
cx.emit(Event::Saved);
cx.notify();
@@ -792,13 +780,7 @@ impl Buffer {
this.apply_diff(diff, cx);
tx.send(this.finalize_last_transaction().cloned()).ok();
this.has_conflict = false;
this.did_reload(
this.version(),
this.as_rope().fingerprint(),
this.line_ending(),
new_mtime,
cx,
);
this.did_reload(this.version(), this.line_ending(), new_mtime, cx);
} else {
if !diff.edits.is_empty()
|| this
@@ -809,13 +791,7 @@ impl Buffer {
this.has_conflict = true;
}
this.did_reload(
prev_version,
Rope::text_fingerprint(&new_text),
this.line_ending(),
this.saved_mtime,
cx,
);
this.did_reload(prev_version, this.line_ending(), this.saved_mtime, cx);
}
this.reload_task.take();
@@ -828,20 +804,17 @@ impl Buffer {
pub fn did_reload(
&mut self,
version: clock::Global,
fingerprint: RopeFingerprint,
line_ending: LineEnding,
mtime: Option<SystemTime>,
cx: &mut ModelContext<Self>,
) {
self.saved_version = version;
self.file_fingerprint = fingerprint;
self.text.set_line_ending(line_ending);
self.saved_mtime = mtime;
if let Some(file) = self.file.as_ref().and_then(|f| f.as_local()) {
file.buffer_reloaded(
self.remote_id(),
&self.saved_version,
self.file_fingerprint,
self.line_ending(),
self.saved_mtime,
cx,

View File

@@ -56,7 +56,7 @@ use std::{
},
};
use syntax_map::SyntaxSnapshot;
pub use task_context::{ContextProvider, ContextProviderWithTasks, SymbolContextProvider};
pub use task_context::{BasicContextProvider, ContextProvider, ContextProviderWithTasks};
use theme::SyntaxTheme;
use tree_sitter::{self, wasmtime, Query, WasmStore};
use util::http::HttpClient;
@@ -72,7 +72,7 @@ pub use lsp::LanguageServerId;
pub use outline::{Outline, OutlineItem};
pub use syntax_map::{OwnedSyntaxLayer, SyntaxLayer};
pub use text::LineEnding;
pub use tree_sitter::{Parser, Tree};
pub use tree_sitter::{Node, Parser, Tree, TreeCursor};
use crate::language_settings::SoftWrap;
@@ -91,6 +91,16 @@ thread_local! {
};
}
pub fn with_parser<F, R>(func: F) -> R
where
F: FnOnce(&mut Parser) -> R,
{
PARSER.with(|parser| {
let mut parser = parser.borrow_mut();
func(&mut parser)
})
}
lazy_static! {
static ref NEXT_LANGUAGE_ID: AtomicUsize = Default::default();
static ref NEXT_GRAMMAR_ID: AtomicUsize = Default::default();

View File

@@ -5,10 +5,10 @@ use std::{ops::Range, path::PathBuf};
use crate::{HighlightId, Language, LanguageRegistry};
use gpui::{px, FontStyle, FontWeight, HighlightStyle, StrikethroughStyle, UnderlineStyle};
use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd};
use pulldown_cmark::{CodeBlockKind, Event, Parser, Tag, TagEnd};
/// Parsed Markdown content.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Default)]
pub struct ParsedMarkdown {
/// The Markdown text.
pub text: String,
@@ -165,7 +165,10 @@ pub async fn parse_markdown_block(
let mut current_language = None;
let mut list_stack = Vec::new();
for event in Parser::new_ext(markdown, Options::all()) {
let mut options = pulldown_cmark::Options::all();
options.remove(pulldown_cmark::Options::ENABLE_YAML_STYLE_METADATA_BLOCKS);
for event in Parser::new_ext(markdown, options) {
let prev_len = text.len();
match event {
Event::Text(t) => {
@@ -249,7 +252,7 @@ pub async fn parse_markdown_block(
new_paragraph(text, &mut list_stack);
current_language = if let CodeBlockKind::Fenced(language) = kind {
language_registry
.language_for_name(language.as_ref())
.language_for_name_or_extension(language.as_ref())
.await
.ok()
} else {
@@ -357,3 +360,35 @@ pub fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option<u64>, bool)
text.push_str(" ");
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_dividers() {
let input = r#"
### instance-method `format`
---
→ `void`
Parameters:
- `const int &`
- `const std::tm &`
- `int & dest`
---
```cpp
// In my_formatter_flag
public: void format(const int &, const std::tm &, int &dest)
```
"#;
let mut options = pulldown_cmark::Options::all();
options.remove(pulldown_cmark::Options::ENABLE_YAML_STYLE_METADATA_BLOCKS);
let parser = pulldown_cmark::Parser::new_ext(input, options);
for event in parser.into_iter() {
println!("{:?}", event);
}
}
}

View File

@@ -10,11 +10,6 @@ use text::*;
pub use proto::{BufferState, Operation};
/// Serializes a [`RopeFingerprint`] to be sent over RPC.
pub fn serialize_fingerprint(fingerprint: RopeFingerprint) -> String {
fingerprint.to_hex()
}
/// Deserializes a `[text::LineEnding]` from the RPC representation.
pub fn deserialize_line_ending(message: proto::LineEnding) -> text::LineEnding {
match message {

View File

@@ -1,34 +1,56 @@
use std::path::Path;
use crate::Location;
use anyhow::Result;
use gpui::AppContext;
use task::{TaskTemplates, TaskVariables, VariableName};
use text::{Point, ToPoint};
/// Language Contexts are used by Zed tasks to extract information about source file.
/// Language Contexts are used by Zed tasks to extract information about the source file where the tasks are supposed to be scheduled from.
/// Multiple context providers may be used together: by default, Zed provides a base [`BasicContextProvider`] context that fills all non-custom [`VariableName`] variants.
///
/// The context will be used to fill data for the tasks, and filter out the ones that do not have the variables required.
pub trait ContextProvider: Send + Sync {
fn build_context(&self, _: Location, _: &mut AppContext) -> Result<TaskVariables> {
/// Builds a specific context to be placed on top of the basic one (replacing all conflicting entries) and to be used for task resolving later.
fn build_context(
&self,
_: Option<&Path>,
_: &Location,
_: &mut AppContext,
) -> Result<TaskVariables> {
Ok(TaskVariables::default())
}
/// Provides all tasks, associated with the current language.
fn associated_tasks(&self) -> Option<TaskTemplates> {
None
}
// Determines whether the [`BasicContextProvider`] variables should be filled too (if `false`), or omitted (if `true`).
fn is_basic(&self) -> bool {
false
}
}
/// A context provider that finds out what symbol is currently focused in the buffer.
pub struct SymbolContextProvider;
/// A context provided that tries to provide values for all non-custom [`VariableName`] variants for a currently opened file.
/// Applied as a base for every custom [`ContextProvider`] unless explicitly oped out.
pub struct BasicContextProvider;
impl ContextProvider for BasicContextProvider {
fn is_basic(&self) -> bool {
true
}
impl ContextProvider for SymbolContextProvider {
fn build_context(
&self,
location: Location,
worktree_abs_path: Option<&Path>,
location: &Location,
cx: &mut AppContext,
) -> gpui::Result<TaskVariables> {
let symbols = location
.buffer
.read(cx)
.snapshot()
.symbols_containing(location.range.start, None);
) -> Result<TaskVariables> {
let buffer = location.buffer.read(cx);
let buffer_snapshot = buffer.snapshot();
let symbols = buffer_snapshot.symbols_containing(location.range.start, None);
let symbol = symbols.unwrap_or_default().last().map(|symbol| {
let range = symbol
.name_ranges
@@ -37,9 +59,40 @@ impl ContextProvider for SymbolContextProvider {
.unwrap_or(0..symbol.text.len());
symbol.text[range].to_string()
});
Ok(TaskVariables::from_iter(
Some(VariableName::Symbol).zip(symbol),
))
let current_file = buffer
.file()
.and_then(|file| file.as_local())
.map(|file| file.abs_path(cx).to_string_lossy().to_string());
let Point { row, column } = location.range.start.to_point(&buffer_snapshot);
let row = row + 1;
let column = column + 1;
let selected_text = buffer
.chars_for_range(location.range.clone())
.collect::<String>();
let mut task_variables = TaskVariables::from_iter([
(VariableName::Row, row.to_string()),
(VariableName::Column, column.to_string()),
]);
if let Some(symbol) = symbol {
task_variables.insert(VariableName::Symbol, symbol);
}
if !selected_text.trim().is_empty() {
task_variables.insert(VariableName::SelectedText, selected_text);
}
if let Some(path) = current_file {
task_variables.insert(VariableName::File, path);
}
if let Some(worktree_path) = worktree_abs_path {
task_variables.insert(
VariableName::WorktreeRoot,
worktree_path.to_string_lossy().to_string(),
);
}
Ok(task_variables)
}
}
@@ -61,7 +114,12 @@ impl ContextProvider for ContextProviderWithTasks {
Some(self.templates.clone())
}
fn build_context(&self, location: Location, cx: &mut AppContext) -> Result<TaskVariables> {
SymbolContextProvider.build_context(location, cx)
fn build_context(
&self,
worktree_abs_path: Option<&Path>,
location: &Location,
cx: &mut AppContext,
) -> Result<TaskVariables> {
BasicContextProvider.build_context(worktree_abs_path, location, cx)
}
}

View File

@@ -22,7 +22,6 @@ lazy_static.workspace = true
log.workspace = true
lsp.workspace = true
node_runtime.workspace = true
parking_lot.workspace = true
project.workspace = true
regex.workspace = true
rope.workspace = true
@@ -45,19 +44,16 @@ tree-sitter-embedded-template.workspace = true
tree-sitter-go.workspace = true
tree-sitter-gomod.workspace = true
tree-sitter-gowork.workspace = true
tree-sitter-hcl.workspace = true
tree-sitter-heex.workspace = true
tree-sitter-jsdoc.workspace = true
tree-sitter-json.workspace = true
tree-sitter-markdown.workspace = true
tree-sitter-nu.workspace = true
tree-sitter-proto.workspace = true
tree-sitter-python.workspace = true
tree-sitter-regex.workspace = true
tree-sitter-ruby.workspace = true
tree-sitter-rust.workspace = true
tree-sitter-typescript.workspace = true
tree-sitter-vue.workspace = true
tree-sitter-yaml.workspace = true
tree-sitter.workspace = true
util.workspace = true

View File

@@ -0,0 +1,18 @@
use language::ContextProviderWithTasks;
use task::{TaskTemplate, TaskTemplates, VariableName};
pub(super) fn bash_task_context() -> ContextProviderWithTasks {
ContextProviderWithTasks::new(TaskTemplates(vec![
TaskTemplate {
label: "execute selection".to_owned(),
command: VariableName::SelectedText.template_value(),
ignore_previously_resolved: true,
..TaskTemplate::default()
},
TaskTemplate {
label: format!("run '{}'", VariableName::File.template_value()),
command: VariableName::File.template_value(),
..TaskTemplate::default()
},
]))
}

View File

@@ -4,6 +4,8 @@ use futures::StreamExt;
use gpui::AsyncAppContext;
pub use language::*;
use lsp::LanguageServerBinary;
use project::project_settings::{BinarySettings, ProjectSettings};
use settings::Settings;
use smol::fs::{self, File};
use std::{any::Any, env::consts, path::PathBuf, sync::Arc};
use util::{
@@ -14,10 +16,51 @@ use util::{
pub struct CLspAdapter;
impl CLspAdapter {
const SERVER_NAME: &'static str = "clangd";
}
#[async_trait(?Send)]
impl super::LspAdapter for CLspAdapter {
fn name(&self) -> LanguageServerName {
LanguageServerName("clangd".into())
LanguageServerName(Self::SERVER_NAME.into())
}
async fn check_if_user_installed(
&self,
delegate: &dyn LspAdapterDelegate,
cx: &AsyncAppContext,
) -> Option<LanguageServerBinary> {
let configured_binary = cx.update(|cx| {
ProjectSettings::get_global(cx)
.lsp
.get(Self::SERVER_NAME)
.and_then(|s| s.binary.clone())
});
if let Ok(Some(BinarySettings {
path: Some(path),
arguments,
})) = configured_binary
{
Some(LanguageServerBinary {
path: path.into(),
arguments: arguments
.unwrap_or_default()
.iter()
.map(|arg| arg.into())
.collect(),
env: None,
})
} else {
let env = delegate.shell_env().await;
let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
Some(LanguageServerBinary {
path,
arguments: vec![],
env: Some(env),
})
}
}
async fn fetch_latest_server_version(
@@ -45,20 +88,6 @@ impl super::LspAdapter for CLspAdapter {
Ok(Box::new(version) as Box<_>)
}
async fn check_if_user_installed(
&self,
delegate: &dyn LspAdapterDelegate,
_: &AsyncAppContext,
) -> Option<LanguageServerBinary> {
let env = delegate.shell_env().await;
let path = delegate.which("clangd".as_ref()).await?;
Some(LanguageServerBinary {
path,
arguments: vec![],
env: Some(env),
})
}
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,

View File

@@ -88,7 +88,7 @@ impl super::LspAdapter for GoLspAdapter {
})
} else {
let env = delegate.shell_env().await;
let path = delegate.which("gopls".as_ref()).await?;
let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
Some(LanguageServerBinary {
path,
arguments: server_binary_arguments(),

View File

@@ -8,24 +8,25 @@ use smol::stream::StreamExt;
use std::{str, sync::Arc};
use util::{asset_str, ResultExt};
use crate::{elixir::elixir_task_context, rust::RustContextProvider};
use crate::{
bash::bash_task_context, elixir::elixir_task_context, python::python_task_context,
rust::RustContextProvider,
};
use self::{deno::DenoSettings, elixir::ElixirSettings};
mod bash;
mod c;
mod css;
mod deno;
mod elixir;
mod go;
mod json;
mod nu;
mod python;
mod ruby;
mod rust;
mod tailwind;
mod terraform;
mod typescript;
mod vue;
mod yaml;
// 1. Add tree-sitter-{language} parser to zed crate
@@ -63,12 +64,10 @@ pub fn init(
("go", tree_sitter_go::language()),
("gomod", tree_sitter_gomod::language()),
("gowork", tree_sitter_gowork::language()),
("hcl", tree_sitter_hcl::language()),
("heex", tree_sitter_heex::language()),
("jsdoc", tree_sitter_jsdoc::language()),
("json", tree_sitter_json::language()),
("markdown", tree_sitter_markdown::language()),
("nu", tree_sitter_nu::language()),
("proto", tree_sitter_proto::language()),
("python", tree_sitter_python::language()),
("regex", tree_sitter_regex::language()),
@@ -76,7 +75,6 @@ pub fn init(
("rust", tree_sitter_rust::language()),
("tsx", tree_sitter_typescript::language_tsx()),
("typescript", tree_sitter_typescript::language_typescript()),
("vue", tree_sitter_vue::language()),
("yaml", tree_sitter_yaml::language()),
]);
@@ -91,7 +89,7 @@ pub fn init(
Ok((
config.clone(),
load_queries($name),
Some(Arc::new(language::SymbolContextProvider)),
Some(Arc::new(language::BasicContextProvider)),
))
},
);
@@ -111,7 +109,7 @@ pub fn init(
Ok((
config.clone(),
load_queries($name),
Some(Arc::new(language::SymbolContextProvider)),
Some(Arc::new(language::BasicContextProvider)),
))
},
);
@@ -137,7 +135,7 @@ pub fn init(
);
};
}
language!("bash");
language!("bash", Vec::new(), bash_task_context());
language!("c", vec![Arc::new(c::CLspAdapter) as Arc<dyn LspAdapter>]);
language!("cpp", vec![Arc::new(c::CLspAdapter)]);
language!(
@@ -199,7 +197,8 @@ pub fn init(
"python",
vec![Arc::new(python::PythonLspAdapter::new(
node_runtime.clone(),
))]
))],
python_task_context()
);
language!(
"rust",
@@ -271,21 +270,7 @@ pub fn init(
"yaml",
vec![Arc::new(yaml::YamlLspAdapter::new(node_runtime.clone()))]
);
language!("nu", vec![Arc::new(nu::NuLanguageServer {})]);
language!(
"vue",
vec![
Arc::new(vue::VueLspAdapter::new(node_runtime.clone())),
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
]
);
language!("proto");
language!("terraform", vec![Arc::new(terraform::TerraformLspAdapter)]);
language!(
"terraform-vars",
vec![Arc::new(terraform::TerraformLspAdapter)]
);
language!("hcl", vec![]);
languages.register_secondary_lsp_adapter(
"Astro".into(),
@@ -303,6 +288,10 @@ pub fn init(
"Svelte".into(),
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
);
languages.register_secondary_lsp_adapter(
"Vue.js".into(),
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
);
let mut subscription = languages.subscribe();
let mut prev_language_settings = languages.language_settings();

View File

@@ -20,5 +20,28 @@
(info_string
(language) @text.literal))
; hack to deal with incorrect grammar parsing
(atx_heading
(block_quote_marker) @punctuation.block_quote_marker
)
; hack to deal with incorrect grammar parsing
(paragraph
(block_quote_marker) @punctuation.block_quote_marker
)
; hack to deal with incorrect grammar parsing
(list_item
(block_quote_marker) @punctuation.block_quote_marker
)
(block_quote
(block_quote_marker) @punctuation.block_quote_marker
)
(block_quote
(paragraph) @text.block_quote
)
(link_destination) @link_uri
(link_text) @link_text

View File

@@ -1,52 +0,0 @@
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
use lsp::LanguageServerBinary;
use std::{any::Any, path::PathBuf};
pub struct NuLanguageServer;
#[async_trait(?Send)]
impl LspAdapter for NuLanguageServer {
fn name(&self) -> LanguageServerName {
LanguageServerName("nu".into())
}
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Any + Send>> {
Ok(Box::new(()))
}
async fn fetch_server_binary(
&self,
_version: Box<dyn 'static + Send + Any>,
_container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
Err(anyhow!(
"nu v0.87.0 or greater must be installed and available in your $PATH"
))
}
async fn cached_server_binary(
&self,
_: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
Some(LanguageServerBinary {
path: "nu".into(),
env: None,
arguments: vec!["--lsp".into()],
})
}
fn can_be_reinstalled(&self) -> bool {
false
}
async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
None
}
}

View File

@@ -1,4 +0,0 @@
("(" @open ")" @close)
("[" @open "]" @close)
("{" @open "}" @close)
(parameter_pipes "|" @open "|" @close)

View File

@@ -1,10 +0,0 @@
name = "Nu"
grammar = "nu"
path_suffixes = ["nu"]
line_comments = ["# "]
autoclose_before = ";:.,=}])>` \n\t\""
brackets = [
{ start = "{", end = "}", close = true, newline = true },
{ start = "[", end = "]", close = true, newline = true },
{ start = "(", end = ")", close = true, newline = true },
]

View File

@@ -1,284 +0,0 @@
;;; ---
;;; keywords
[
"def"
"alias"
"export-env"
"export"
"extern"
"module"
"let"
"let-env"
"mut"
"const"
"hide-env"
"source"
"source-env"
"overlay"
"register"
"loop"
"while"
"error"
"do"
"if"
"else"
"try"
"catch"
"match"
"break"
"continue"
"return"
] @keyword
(hide_mod "hide" @keyword)
(decl_use "use" @keyword)
(ctrl_for
"for" @keyword
"in" @keyword
)
(overlay_list "list" @keyword.storage.modifier)
(overlay_hide "hide" @keyword.storage.modifier)
(overlay_new "new" @keyword.storage.modifier)
(overlay_use
"use" @keyword.storage.modifier
"as" @keyword
)
(ctrl_error "make" @keyword.storage.modifier)
;;; ---
;;; literals
(val_number) @constant.numeric
(val_duration
unit: [
"ns" "µs" "us" "ms" "sec" "min" "hr" "day" "wk"
] @variable.parameter
)
(val_filesize
unit: [
"b" "B"
"kb" "kB" "Kb" "KB"
"mb" "mB" "Mb" "MB"
"gb" "gB" "Gb" "GB"
"tb" "tB" "Tb" "TB"
"pb" "pB" "Pb" "PB"
"eb" "eB" "Eb" "EB"
"kib" "kiB" "kIB" "kIb" "Kib" "KIb" "KIB"
"mib" "miB" "mIB" "mIb" "Mib" "MIb" "MIB"
"gib" "giB" "gIB" "gIb" "Gib" "GIb" "GIB"
"tib" "tiB" "tIB" "tIb" "Tib" "TIb" "TIB"
"pib" "piB" "pIB" "pIb" "Pib" "PIb" "PIB"
"eib" "eiB" "eIB" "eIb" "Eib" "EIb" "EIB"
] @variable.parameter
)
(val_binary
[
"0b"
"0o"
"0x"
] @constant.numeric
"[" @punctuation.bracket
digit: [
"," @punctuation.delimiter
(hex_digit) @constant.number
]
"]" @punctuation.bracket
) @constant.numeric
(val_bool) @constant.builtin
(val_nothing) @constant.builtin
(val_string) @string
(val_date) @constant.number
(inter_escape_sequence) @constant.character.escape
(escape_sequence) @constant.character.escape
(val_interpolated [
"$\""
"$\'"
"\""
"\'"
] @string)
(unescaped_interpolated_content) @string
(escaped_interpolated_content) @string
(expr_interpolated ["(" ")"] @variable.parameter)
;;; ---
;;; operators
(expr_binary [
"+"
"-"
"*"
"/"
"mod"
"//"
"++"
"**"
"=="
"!="
"<"
"<="
">"
">="
"=~"
"!~"
"and"
"or"
"xor"
"bit-or"
"bit-xor"
"bit-and"
"bit-shl"
"bit-shr"
"in"
"not-in"
"starts-with"
"ends-with"
] @operator )
(where_command [
"+"
"-"
"*"
"/"
"mod"
"//"
"++"
"**"
"=="
"!="
"<"
"<="
">"
">="
"=~"
"!~"
"and"
"or"
"xor"
"bit-or"
"bit-xor"
"bit-and"
"bit-shl"
"bit-shr"
"in"
"not-in"
"starts-with"
"ends-with"
] @operator)
(assignment [
"="
"+="
"-="
"*="
"/="
"++="
] @operator)
(expr_unary ["not" "-"] @operator)
(val_range [
".."
"..="
"..<"
] @operator)
["=>" "=" "|"] @operator
[
"o>" "out>"
"e>" "err>"
"e+o>" "err+out>"
"o+e>" "out+err>"
] @special
;;; ---
;;; punctuation
[
","
";"
] @punctuation.delimiter
(param_short_flag "-" @punctuation.delimiter)
(param_long_flag ["--"] @punctuation.delimiter)
(long_flag ["--"] @punctuation.delimiter)
(param_rest "..." @punctuation.delimiter)
(param_type [":"] @punctuation.special)
(param_value ["="] @punctuation.special)
(param_cmd ["@"] @punctuation.special)
(param_opt ["?"] @punctuation.special)
[
"(" ")"
"{" "}"
"[" "]"
] @punctuation.bracket
(val_record
(record_entry ":" @punctuation.delimiter))
;;; ---
;;; identifiers
(param_rest
name: (_) @variable.parameter)
(param_opt
name: (_) @variable.parameter)
(parameter
param_name: (_) @variable.parameter)
(param_cmd
(cmd_identifier) @string)
(param_long_flag) @variable.parameter
(param_short_flag) @variable.parameter
(short_flag) @variable.parameter
(long_flag) @variable.parameter
(scope_pattern [(wild_card) @function])
(cmd_identifier) @function
(command
"^" @punctuation.delimiter
head: (_) @function
)
"where" @function
(path
["." "?"] @punctuation.delimiter
) @variable.parameter
(val_variable
"$" @variable.parameter
[
(identifier) @namespace
"in"
"nu"
"env"
"nothing"
] @special
)
;;; ---
;;; types
(flat_type) @type.builtin
(list_type
"list" @type.enum
["<" ">"] @punctuation.bracket
)
(collection_type
["record" "table"] @type.enum
"<" @punctuation.bracket
key: (_) @variable.parameter
["," ":"] @punctuation.delimiter
">" @punctuation.bracket
)
(shebang) @comment
(comment) @comment

View File

@@ -1,3 +0,0 @@
(_ "[" "]" @end) @indent
(_ "{" "}" @end) @indent
(_ "(" ")" @end) @indent

View File

@@ -1,6 +1,6 @@
use anyhow::Result;
use async_trait::async_trait;
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
use language::{ContextProviderWithTasks, LanguageServerName, LspAdapter, LspAdapterDelegate};
use lsp::LanguageServerBinary;
use node_runtime::NodeRuntime;
use std::{
@@ -9,6 +9,7 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
use task::{TaskTemplate, TaskTemplates, VariableName};
use util::ResultExt;
const SERVER_PATH: &str = "node_modules/pyright/langserver.index.js";
@@ -180,6 +181,30 @@ async fn get_cached_server_binary(
}
}
pub(super) fn python_task_context() -> ContextProviderWithTasks {
ContextProviderWithTasks::new(TaskTemplates(vec![
TaskTemplate {
label: "execute selection".to_owned(),
command: "python3".to_owned(),
args: vec![
"-c".to_owned(),
format!(
"exec(r'''{}''')",
VariableName::SelectedText.template_value()
),
],
ignore_previously_resolved: true,
..TaskTemplate::default()
},
TaskTemplate {
label: format!("run '{}'", VariableName::File.template_value()),
command: "python3".to_owned(),
args: vec![VariableName::File.template_value()],
..TaskTemplate::default()
},
]))
}
#[cfg(test)]
mod tests {
use gpui::{BorrowAppContext, Context, ModelContext, TestAppContext};

View File

@@ -331,25 +331,26 @@ const RUST_PACKAGE_TASK_VARIABLE: VariableName =
impl ContextProvider for RustContextProvider {
fn build_context(
&self,
location: Location,
_: Option<&Path>,
location: &Location,
cx: &mut gpui::AppContext,
) -> Result<TaskVariables> {
let mut context = SymbolContextProvider.build_context(location.clone(), cx)?;
let local_abs_path = location
.buffer
.read(cx)
.file()
.and_then(|file| Some(file.as_local()?.abs_path(cx)));
if let Some(package_name) = local_abs_path
.as_deref()
.and_then(|local_abs_path| local_abs_path.parent())
.and_then(human_readable_package_name)
{
context.insert(RUST_PACKAGE_TASK_VARIABLE.clone(), package_name);
}
Ok(context)
Ok(
if let Some(package_name) = local_abs_path
.as_deref()
.and_then(|local_abs_path| local_abs_path.parent())
.and_then(human_readable_package_name)
{
TaskVariables::from_iter(Some((RUST_PACKAGE_TASK_VARIABLE.clone(), package_name)))
} else {
TaskVariables::default()
},
)
}
fn associated_tasks(&self) -> Option<TaskTemplates> {

View File

@@ -1,181 +0,0 @@
use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
use collections::HashMap;
use futures::StreamExt;
pub use language::*;
use lsp::{CodeActionKind, LanguageServerBinary};
use smol::fs::{self, File};
use std::{any::Any, ffi::OsString, path::PathBuf};
use util::{
fs::remove_matching,
github::{latest_github_release, GitHubLspBinaryVersion},
maybe, ResultExt,
};
fn terraform_ls_binary_arguments() -> Vec<OsString> {
vec!["serve".into()]
}
pub struct TerraformLspAdapter;
#[async_trait(?Send)]
impl LspAdapter for TerraformLspAdapter {
fn name(&self) -> LanguageServerName {
LanguageServerName("terraform-ls".into())
}
async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
// TODO: maybe use release API instead
// https://api.releases.hashicorp.com/v1/releases/terraform-ls?limit=1
let release = latest_github_release(
"hashicorp/terraform-ls",
false,
false,
delegate.http_client(),
)
.await?;
Ok(Box::new(GitHubLspBinaryVersion {
name: release.tag_name,
url: Default::default(),
}))
}
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
let zip_path = container_dir.join(format!("terraform-ls_{}.zip", version.name));
let version_dir = container_dir.join(format!("terraform-ls_{}", version.name));
let binary_path = version_dir.join("terraform-ls");
let url = build_download_url(version.name)?;
if fs::metadata(&binary_path).await.is_err() {
let mut response = delegate
.http_client()
.get(&url, Default::default(), true)
.await
.context("error downloading release")?;
let mut file = File::create(&zip_path).await?;
if !response.status().is_success() {
Err(anyhow!(
"download failed with status {}",
response.status().to_string()
))?;
}
futures::io::copy(response.body_mut(), &mut file).await?;
let unzip_status = smol::process::Command::new("unzip")
.current_dir(&container_dir)
.arg(&zip_path)
.arg("-d")
.arg(&version_dir)
.output()
.await?
.status;
if !unzip_status.success() {
Err(anyhow!("failed to unzip Terraform LS archive"))?;
}
remove_matching(&container_dir, |entry| entry != version_dir).await;
}
Ok(LanguageServerBinary {
path: binary_path,
env: None,
arguments: terraform_ls_binary_arguments(),
})
}
async fn cached_server_binary(
&self,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
get_cached_server_binary(container_dir).await
}
async fn installation_test_binary(
&self,
container_dir: PathBuf,
) -> Option<LanguageServerBinary> {
get_cached_server_binary(container_dir)
.await
.map(|mut binary| {
binary.arguments = vec!["version".into()];
binary
})
}
fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
// TODO: file issue for server supported code actions
// TODO: reenable default actions / delete override
Some(vec![])
}
fn language_ids(&self) -> HashMap<String, String> {
HashMap::from_iter([
("Terraform".into(), "terraform".into()),
("Terraform Vars".into(), "terraform-vars".into()),
])
}
}
fn build_download_url(version: String) -> Result<String> {
let v = version.strip_prefix('v').unwrap_or(&version);
let os = match std::env::consts::OS {
"linux" => "linux",
"macos" => "darwin",
"win" => "windows",
_ => Err(anyhow!("unsupported OS {}", std::env::consts::OS))?,
}
.to_string();
let arch = match std::env::consts::ARCH {
"x86" => "386",
"x86_64" => "amd64",
"arm" => "arm",
"aarch64" => "arm64",
_ => Err(anyhow!("unsupported ARCH {}", std::env::consts::ARCH))?,
}
.to_string();
let url = format!(
"https://releases.hashicorp.com/terraform-ls/{v}/terraform-ls_{v}_{os}_{arch}.zip",
);
Ok(url)
}
async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
maybe!(async {
let mut last = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {
last = Some(entry?.path());
}
match last {
Some(path) if path.is_dir() => {
let binary = path.join("terraform-ls");
if fs::metadata(&binary).await.is_ok() {
return Ok(LanguageServerBinary {
path: binary,
env: None,
arguments: terraform_ls_binary_arguments(),
});
}
}
_ => {}
}
Err(anyhow!("no cached binary"))
})
.await
.log_err()
}

View File

@@ -1,242 +0,0 @@
use anyhow::{anyhow, ensure, Result};
use async_trait::async_trait;
use futures::StreamExt;
pub use language::*;
use lsp::{CodeActionKind, LanguageServerBinary};
use node_runtime::NodeRuntime;
use parking_lot::Mutex;
use smol::fs::{self};
use std::{
any::Any,
ffi::OsString,
path::{Path, PathBuf},
sync::Arc,
};
use util::{maybe, ResultExt};
pub struct VueLspVersion {
vue_version: String,
ts_version: String,
}
pub struct VueLspAdapter {
node: Arc<dyn NodeRuntime>,
typescript_install_path: Mutex<Option<PathBuf>>,
}
impl VueLspAdapter {
const SERVER_PATH: &'static str =
"node_modules/@vue/language-server/bin/vue-language-server.js";
// TODO: this can't be hardcoded, yet we have to figure out how to pass it in initialization_options.
const TYPESCRIPT_PATH: &'static str = "node_modules/typescript/lib";
pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
let typescript_install_path = Mutex::new(None);
Self {
node,
typescript_install_path,
}
}
}
#[async_trait(?Send)]
impl super::LspAdapter for VueLspAdapter {
fn name(&self) -> LanguageServerName {
LanguageServerName("vue-language-server".into())
}
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
Ok(Box::new(VueLspVersion {
// We hardcode the version to 1.8 since we do not support @vue/language-server 2.0 yet.
vue_version: "1.8".to_string(),
ts_version: self.node.npm_package_latest_version("typescript").await?,
}) as Box<_>)
}
async fn initialization_options(
self: Arc<Self>,
_: &Arc<dyn LspAdapterDelegate>,
) -> Result<Option<serde_json::Value>> {
let typescript_sdk_path = self.typescript_install_path.lock();
let typescript_sdk_path = typescript_sdk_path
.as_ref()
.expect("initialization_options called without a container_dir for typescript");
Ok(Some(serde_json::json!({
"typescript": {
"tsdk": typescript_sdk_path
}
})))
}
fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
// REFACTOR is explicitly disabled, as vue-lsp does not adhere to LSP protocol for code actions with these - it
// sends back a CodeAction with neither `command` nor `edits` fields set, which is against the spec.
Some(vec![
CodeActionKind::EMPTY,
CodeActionKind::QUICKFIX,
CodeActionKind::REFACTOR_REWRITE,
])
}
async fn fetch_server_binary(
&self,
latest_version: Box<dyn 'static + Send + Any>,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let latest_version = latest_version.downcast::<VueLspVersion>().unwrap();
let server_path = container_dir.join(Self::SERVER_PATH);
let ts_path = container_dir.join(Self::TYPESCRIPT_PATH);
let vue_package_name = "@vue/language-server";
let should_install_vue_language_server = self
.node
.should_install_npm_package(
vue_package_name,
&server_path,
&container_dir,
&latest_version.vue_version,
)
.await;
if should_install_vue_language_server {
self.node
.npm_install_packages(
&container_dir,
&[(vue_package_name, latest_version.vue_version.as_str())],
)
.await?;
}
ensure!(
fs::metadata(&server_path).await.is_ok(),
"@vue/language-server package installation failed"
);
let ts_package_name = "typescript";
let should_install_ts_language_server = self
.node
.should_install_npm_package(
ts_package_name,
&server_path,
&container_dir,
&latest_version.ts_version,
)
.await;
if should_install_ts_language_server {
self.node
.npm_install_packages(
&container_dir,
&[(ts_package_name, latest_version.ts_version.as_str())],
)
.await?;
}
ensure!(
fs::metadata(&ts_path).await.is_ok(),
"typescript for Vue package installation failed"
);
*self.typescript_install_path.lock() = Some(ts_path);
Ok(LanguageServerBinary {
path: self.node.binary_path().await?,
env: None,
arguments: vue_server_binary_arguments(&server_path),
})
}
async fn cached_server_binary(
&self,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
let (server, ts_path) = get_cached_server_binary(container_dir, self.node.clone()).await?;
*self.typescript_install_path.lock() = Some(ts_path);
Some(server)
}
async fn installation_test_binary(
&self,
container_dir: PathBuf,
) -> Option<LanguageServerBinary> {
let (server, ts_path) = get_cached_server_binary(container_dir, self.node.clone())
.await
.map(|(mut binary, ts_path)| {
binary.arguments = vec!["--help".into()];
(binary, ts_path)
})?;
*self.typescript_install_path.lock() = Some(ts_path);
Some(server)
}
async fn label_for_completion(
&self,
item: &lsp::CompletionItem,
language: &Arc<language::Language>,
) -> Option<language::CodeLabel> {
use lsp::CompletionItemKind as Kind;
let len = item.label.len();
let grammar = language.grammar()?;
let highlight_id = match item.kind? {
Kind::CLASS | Kind::INTERFACE => grammar.highlight_id_for_name("type"),
Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("tag"),
Kind::VARIABLE => grammar.highlight_id_for_name("type"),
Kind::KEYWORD => grammar.highlight_id_for_name("keyword"),
Kind::VALUE => grammar.highlight_id_for_name("tag"),
_ => None,
}?;
let text = match &item.detail {
Some(detail) => format!("{} {}", item.label, detail),
None => item.label.clone(),
};
Some(language::CodeLabel {
text,
runs: vec![(0..len, highlight_id)],
filter_range: 0..len,
})
}
}
fn vue_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
vec![server_path.into(), "--stdio".into()]
}
type TypescriptPath = PathBuf;
async fn get_cached_server_binary(
container_dir: PathBuf,
node: Arc<dyn NodeRuntime>,
) -> Option<(LanguageServerBinary, TypescriptPath)> {
maybe!(async {
let mut last_version_dir = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {
let entry = entry?;
if entry.file_type().await?.is_dir() {
last_version_dir = Some(entry.path());
}
}
let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
let server_path = last_version_dir.join(VueLspAdapter::SERVER_PATH);
let typescript_path = last_version_dir.join(VueLspAdapter::TYPESCRIPT_PATH);
if server_path.exists() && typescript_path.exists() {
Ok((
LanguageServerBinary {
path: node.binary_path().await?,
env: None,
arguments: vue_server_binary_arguments(&server_path),
},
typescript_path,
))
} else {
Err(anyhow!(
"missing executable in directory {:?}",
last_version_dir
))
}
})
.await
.log_err()
}

View File

@@ -138,8 +138,16 @@ struct AnyResponse<'a> {
struct Response<T> {
jsonrpc: &'static str,
id: RequestId,
result: Option<T>,
error: Option<Error>,
#[serde(flatten)]
value: LspResult<T>,
}
#[derive(Serialize)]
#[serde(rename_all = "snake_case")]
enum LspResult<T> {
#[serde(rename = "result")]
Ok(Option<T>),
Error(Option<Error>),
}
/// Language server protocol RPC notification message.
@@ -867,16 +875,14 @@ impl LanguageServer {
Ok(result) => Response {
jsonrpc: JSON_RPC_VERSION,
id,
result: Some(result),
error: None,
value: LspResult::Ok(Some(result)),
},
Err(error) => Response {
jsonrpc: JSON_RPC_VERSION,
id,
result: None,
error: Some(Error {
value: LspResult::Error(Some(Error {
message: error.to_string(),
}),
})),
},
};
if let Some(response) =
@@ -1503,4 +1509,27 @@ mod tests {
let expected_id = RequestId::Int(2);
assert_eq!(notification.id, Some(expected_id));
}
#[test]
fn test_serialize_has_no_nulls() {
// Ensure we're not setting both result and error variants. (ticket #10595)
let no_tag = Response::<u32> {
jsonrpc: "",
id: RequestId::Int(0),
value: LspResult::Ok(None),
};
assert_eq!(
serde_json::to_string(&no_tag).unwrap(),
"{\"jsonrpc\":\"\",\"id\":0,\"result\":null}"
);
let no_tag = Response::<u32> {
jsonrpc: "",
id: RequestId::Int(0),
value: LspResult::Error(None),
};
assert_eq!(
serde_json::to_string(&no_tag).unwrap(),
"{\"jsonrpc\":\"\",\"id\":0,\"error\":null}"
);
}
}

View File

@@ -1,9 +1,11 @@
use anyhow::{anyhow, Result};
use anyhow::{anyhow, Context, Result};
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, StreamExt};
use serde::{Deserialize, Serialize};
use std::convert::TryFrom;
use std::{convert::TryFrom, future::Future};
use util::http::{AsyncBody, HttpClient, Method, Request as HttpRequest};
pub const OPEN_AI_API_URL: &str = "https://api.openai.com/v1";
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Role {
@@ -188,3 +190,68 @@ pub async fn stream_completion(
}
}
}
#[derive(Copy, Clone, Serialize, Deserialize)]
pub enum OpenAiEmbeddingModel {
#[serde(rename = "text-embedding-3-small")]
TextEmbedding3Small,
#[serde(rename = "text-embedding-3-large")]
TextEmbedding3Large,
}
#[derive(Serialize)]
struct OpenAiEmbeddingRequest<'a> {
model: OpenAiEmbeddingModel,
input: Vec<&'a str>,
}
#[derive(Deserialize)]
pub struct OpenAiEmbeddingResponse {
pub data: Vec<OpenAiEmbedding>,
}
#[derive(Deserialize)]
pub struct OpenAiEmbedding {
pub embedding: Vec<f32>,
}
pub fn embed<'a>(
client: &dyn HttpClient,
api_url: &str,
api_key: &str,
model: OpenAiEmbeddingModel,
texts: impl IntoIterator<Item = &'a str>,
) -> impl 'static + Future<Output = Result<OpenAiEmbeddingResponse>> {
let uri = format!("{api_url}/embeddings");
let request = OpenAiEmbeddingRequest {
model,
input: texts.into_iter().collect(),
};
let body = AsyncBody::from(serde_json::to_string(&request).unwrap());
let request = HttpRequest::builder()
.method(Method::POST)
.uri(uri)
.header("Content-Type", "application/json")
.header("Authorization", format!("Bearer {}", api_key))
.body(body)
.map(|request| client.send(request));
async move {
let mut response = request?.await?;
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
if response.status().is_success() {
let response: OpenAiEmbeddingResponse =
serde_json::from_str(&body).context("failed to parse OpenAI embedding response")?;
Ok(response)
} else {
Err(anyhow!(
"error during embedding, status: {:?}, body: {:?}",
response.status(),
body
))
}
}
}

View File

@@ -97,7 +97,7 @@ use std::{
};
use task::static_source::{StaticSource, TrackedFile};
use terminals::Terminals;
use text::{Anchor, BufferId, RopeFingerprint};
use text::{Anchor, BufferId};
use util::{
debug_panic, defer,
http::{HttpClient, Url},
@@ -978,6 +978,50 @@ impl Project {
}
}
#[cfg(any(test, feature = "test-support"))]
pub async fn example(
root_paths: impl IntoIterator<Item = &Path>,
cx: &mut AsyncAppContext,
) -> Model<Project> {
use clock::FakeSystemClock;
let fs = Arc::new(RealFs::default());
let languages = LanguageRegistry::test(cx.background_executor().clone());
let clock = Arc::new(FakeSystemClock::default());
let http_client = util::http::FakeHttpClient::with_404_response();
let client = cx
.update(|cx| client::Client::new(clock, http_client.clone(), cx))
.unwrap();
let user_store = cx
.new_model(|cx| UserStore::new(client.clone(), cx))
.unwrap();
let project = cx
.update(|cx| {
Project::local(
client,
node_runtime::FakeNodeRuntime::new(),
user_store,
Arc::new(languages),
fs,
cx,
)
})
.unwrap();
for path in root_paths {
let (tree, _) = project
.update(cx, |project, cx| {
project.find_or_create_local_worktree(path, true, cx)
})
.unwrap()
.await
.unwrap();
tree.update(cx, |tree, _| tree.as_local().unwrap().scan_complete())
.unwrap()
.await;
}
project
}
#[cfg(any(test, feature = "test-support"))]
pub async fn test(
fs: Arc<dyn Fs>,
@@ -1146,6 +1190,10 @@ impl Project {
self.user_store.clone()
}
pub fn node_runtime(&self) -> Option<&Arc<dyn NodeRuntime>> {
self.node.as_ref()
}
pub fn opened_buffers(&self) -> Vec<Model<Buffer>> {
self.opened_buffers
.values()
@@ -7708,13 +7756,20 @@ impl Project {
.as_local()
.context("worktree was not local")?
.snapshot();
let (work_directory, repo) = worktree
.repository_and_work_directory_for_path(&buffer_project_path.path)
.context("failed to get repo for blamed buffer")?;
let repo_entry = worktree
.get_local_repo(&repo)
.context("failed to get repo for blamed buffer")?;
let (work_directory, repo) = match worktree
.repository_and_work_directory_for_path(&buffer_project_path.path)
{
Some(work_dir_repo) => work_dir_repo,
None => anyhow::bail!(NoRepositoryError {}),
};
let repo_entry = match worktree.get_local_repo(&repo) {
Some(repo_entry) => repo_entry,
None => anyhow::bail!(NoRepositoryError {}),
};
let repo = repo_entry.repo().clone();
let relative_path = buffer_project_path
.path
@@ -7725,7 +7780,6 @@ impl Project {
Some(version) => buffer.rope_for_version(&version).clone(),
None => buffer.as_rope().clone(),
};
let repo = repo_entry.repo().clone();
anyhow::Ok((repo, relative_path, content))
});
@@ -7734,6 +7788,7 @@ impl Project {
let (repo, relative_path, content) = blame_params?;
let lock = repo.lock();
lock.blame(&relative_path, content)
.with_context(|| format!("Failed to blame {relative_path:?}"))
})
} else {
let project_id = self.remote_id();
@@ -8525,7 +8580,6 @@ impl Project {
buffer_id: buffer_id.into(),
version: serialize_version(buffer.saved_version()),
mtime: buffer.saved_mtime().map(|time| time.into()),
fingerprint: language::proto::serialize_fingerprint(buffer.saved_version_fingerprint()),
})
}
@@ -8618,9 +8672,6 @@ impl Project {
buffer_id: buffer_id.into(),
version: language::proto::serialize_version(buffer.saved_version()),
mtime: buffer.saved_mtime().map(|time| time.into()),
fingerprint: language::proto::serialize_fingerprint(
buffer.saved_version_fingerprint(),
),
line_ending: language::proto::serialize_line_ending(
buffer.line_ending(),
) as i32,
@@ -9609,7 +9660,6 @@ impl Project {
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
let fingerprint = Default::default();
let version = deserialize_version(&envelope.payload.version);
let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
let mtime = envelope.payload.mtime.map(|time| time.into());
@@ -9622,7 +9672,7 @@ impl Project {
.or_else(|| this.incomplete_remote_buffers.get(&buffer_id).cloned());
if let Some(buffer) = buffer {
buffer.update(cx, |buffer, cx| {
buffer.did_save(version, fingerprint, mtime, cx);
buffer.did_save(version, mtime, cx);
});
}
Ok(())
@@ -9637,7 +9687,6 @@ impl Project {
) -> Result<()> {
let payload = envelope.payload;
let version = deserialize_version(&payload.version);
let fingerprint = RopeFingerprint::default();
let line_ending = deserialize_line_ending(
proto::LineEnding::from_i32(payload.line_ending)
.ok_or_else(|| anyhow!("missing line ending"))?,
@@ -9652,7 +9701,7 @@ impl Project {
.or_else(|| this.incomplete_remote_buffers.get(&buffer_id).cloned());
if let Some(buffer) = buffer {
buffer.update(cx, |buffer, cx| {
buffer.did_reload(version, fingerprint, line_ending, mtime, cx);
buffer.did_reload(version, line_ending, mtime, cx);
});
}
Ok(())
@@ -10739,3 +10788,14 @@ fn remove_empty_hover_blocks(mut hover: Hover) -> Option<Hover> {
Some(hover)
}
}
#[derive(Debug)]
pub struct NoRepositoryError {}
impl std::fmt::Display for NoRepositoryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "no git repository for worktree found")
}
}
impl std::error::Error for NoRepositoryError {}

View File

@@ -3,7 +3,7 @@ use gpui::AppContext;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use std::sync::Arc;
use std::{sync::Arc, time::Duration};
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct ProjectSettings {
@@ -29,6 +29,30 @@ pub struct GitSettings {
/// Default: tracked_files
pub git_gutter: Option<GitGutterSetting>,
pub gutter_debounce: Option<u64>,
/// Whether or not to show git blame data inline in
/// the currently focused line.
///
/// Default: off
pub inline_blame: Option<InlineBlameSettings>,
}
impl GitSettings {
pub fn inline_blame_enabled(&self) -> bool {
match self.inline_blame {
Some(InlineBlameSettings { enabled, .. }) => enabled,
_ => false,
}
}
pub fn inline_blame_delay(&self) -> Option<Duration> {
match self.inline_blame {
Some(InlineBlameSettings {
delay_ms: Some(delay_ms),
..
}) if delay_ms > 0 => Some(Duration::from_millis(delay_ms)),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
@@ -41,6 +65,26 @@ pub enum GitGutterSetting {
Hide,
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct InlineBlameSettings {
/// Whether or not to show git blame data inline in
/// the currently focused line.
///
/// Default: true
#[serde(default = "true_value")]
pub enabled: bool,
/// Whether to only show the inline blame information
/// after a delay once the cursor stops moving.
///
/// Default: 0
pub delay_ms: Option<u64>,
}
const fn true_value() -> bool {
true
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
pub struct BinarySettings {
pub path: Option<String>,

View File

@@ -14,6 +14,7 @@ use serde_json::json;
#[cfg(not(windows))]
use std::os;
use std::task::Poll;
use task::{TaskContext, TaskSource, TaskTemplate, TaskTemplates};
use unindent::Unindent as _;
use util::{assert_set_eq, paths::PathMatcher, test::temp_tree};
use worktree::WorktreeModelHandle as _;
@@ -125,8 +126,19 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
let worktree = project.update(cx, |project, _| project.worktrees().next().unwrap());
let task_context = TaskContext::default();
cx.executor().run_until_parked();
let workree_id = cx.update(|cx| {
project.update(cx, |project, cx| {
project.worktrees().next().unwrap().read(cx).id()
})
});
let global_task_source_kind = TaskSourceKind::Worktree {
id: workree_id,
abs_path: PathBuf::from("/the-root/.zed/tasks.json"),
id_base: "local_tasks_for_worktree",
};
cx.update(|cx| {
let tree = worktree.read(cx);
@@ -154,17 +166,29 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
assert_eq!(settings_a.tab_size.get(), 8);
assert_eq!(settings_b.tab_size.get(), 2);
let workree_id = project.update(cx, |project, cx| {
project.worktrees().next().unwrap().read(cx).id()
});
let all_tasks = project
.update(cx, |project, cx| {
project
.task_inventory()
.update(cx, |inventory, cx| inventory.list_tasks(None, None, cx))
project.task_inventory().update(cx, |inventory, cx| {
let (mut old, new) = inventory.used_and_current_resolved_tasks(
None,
Some(workree_id),
&task_context,
cx,
);
old.extend(new);
old
})
})
.into_iter()
.map(|(source_kind, task)| (source_kind, task.label))
.map(|(source_kind, task)| {
let resolved = task.resolved.unwrap();
(
source_kind,
task.resolved_label,
resolved.args,
resolved.env,
)
})
.collect::<Vec<_>>();
assert_eq!(
all_tasks,
@@ -172,24 +196,141 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
(
TaskSourceKind::Worktree {
id: workree_id,
abs_path: PathBuf::from("/the-root/.zed/tasks.json"),
abs_path: PathBuf::from("/the-root/b/.zed/tasks.json"),
id_base: "local_tasks_for_worktree",
},
"cargo check".to_string()
"cargo check".to_string(),
vec!["check".to_string()],
HashMap::default(),
),
(
global_task_source_kind.clone(),
"cargo check".to_string(),
vec!["check".to_string(), "--all".to_string()],
HashMap::default(),
),
]
);
});
project.update(cx, |project, cx| {
let inventory = project.task_inventory();
inventory.update(cx, |inventory, cx| {
let (mut old, new) = inventory.used_and_current_resolved_tasks(
None,
Some(workree_id),
&task_context,
cx,
);
old.extend(new);
let (_, resolved_task) = old
.into_iter()
.find(|(source_kind, _)| source_kind == &global_task_source_kind)
.expect("should have one global task");
inventory.task_scheduled(global_task_source_kind.clone(), resolved_task);
})
});
cx.update(|cx| {
let all_tasks = project
.update(cx, |project, cx| {
project.task_inventory().update(cx, |inventory, cx| {
inventory.remove_local_static_source(Path::new("/the-root/.zed/tasks.json"));
inventory.add_source(
global_task_source_kind.clone(),
|cx| {
cx.new_model(|_| {
let source = TestTaskSource {
tasks: TaskTemplates(vec![TaskTemplate {
label: "cargo check".to_string(),
command: "cargo".to_string(),
args: vec![
"check".to_string(),
"--all".to_string(),
"--all-targets".to_string(),
],
env: HashMap::from_iter(Some((
"RUSTFLAGS".to_string(),
"-Zunstable-options".to_string(),
))),
..TaskTemplate::default()
}]),
};
Box::new(source) as Box<_>
})
},
cx,
);
let (mut old, new) = inventory.used_and_current_resolved_tasks(
None,
Some(workree_id),
&task_context,
cx,
);
old.extend(new);
old
})
})
.into_iter()
.map(|(source_kind, task)| {
let resolved = task.resolved.unwrap();
(
source_kind,
task.resolved_label,
resolved.args,
resolved.env,
)
})
.collect::<Vec<_>>();
assert_eq!(
all_tasks,
vec![
(
TaskSourceKind::Worktree {
id: workree_id,
abs_path: PathBuf::from("/the-root/b/.zed/tasks.json"),
id_base: "local_tasks_for_worktree",
},
"cargo check".to_string()
"cargo check".to_string(),
vec!["check".to_string()],
HashMap::default(),
),
(
TaskSourceKind::Worktree {
id: workree_id,
abs_path: PathBuf::from("/the-root/.zed/tasks.json"),
id_base: "local_tasks_for_worktree",
},
"cargo check".to_string(),
vec![
"check".to_string(),
"--all".to_string(),
"--all-targets".to_string()
],
HashMap::from_iter(Some((
"RUSTFLAGS".to_string(),
"-Zunstable-options".to_string()
))),
),
]
);
});
}
struct TestTaskSource {
tasks: TaskTemplates,
}
impl TaskSource for TestTaskSource {
fn as_any(&mut self) -> &mut dyn std::any::Any {
self
}
fn tasks_to_schedule(&mut self, _: &mut ModelContext<Box<dyn TaskSource>>) -> TaskTemplates {
self.tasks.clone()
}
}
#[gpui::test]
async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
init_test(cx);
@@ -2661,7 +2802,7 @@ async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext)
)
.await
.unwrap();
worktree.next_event(cx);
worktree.next_event(cx).await;
// Change the buffer's file again. Depending on the random seed, the
// previous file change may still be in progress.
@@ -2672,7 +2813,7 @@ async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext)
)
.await
.unwrap();
worktree.next_event(cx);
worktree.next_event(cx).await;
cx.executor().run_until_parked();
let on_disk_text = fs.load(Path::new("/dir/file1")).await.unwrap();
@@ -2716,7 +2857,7 @@ async fn test_edit_buffer_while_it_reloads(cx: &mut gpui::TestAppContext) {
)
.await
.unwrap();
worktree.next_event(cx);
worktree.next_event(cx).await;
cx.executor()
.spawn(cx.executor().simulate_random_delay())
@@ -3122,12 +3263,7 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) {
&[language::Event::Edited, language::Event::DirtyChanged]
);
events.lock().clear();
buffer.did_save(
buffer.version(),
buffer.as_rope().fingerprint(),
buffer.file().unwrap().mtime(),
cx,
);
buffer.did_save(buffer.version(), buffer.file().unwrap().mtime(), cx);
});
// after saving, the buffer is not dirty, and emits a saved event.

View File

@@ -2,16 +2,16 @@
use std::{
any::TypeId,
cmp,
cmp::{self, Reverse},
path::{Path, PathBuf},
sync::Arc,
};
use collections::{HashMap, VecDeque};
use collections::{hash_map, HashMap, VecDeque};
use gpui::{AppContext, Context, Model, ModelContext, Subscription};
use itertools::{Either, Itertools};
use language::Language;
use task::{ResolvedTask, TaskContext, TaskId, TaskSource, TaskTemplate};
use task::{ResolvedTask, TaskContext, TaskId, TaskSource, TaskTemplate, VariableName};
use util::{post_inc, NumericPrefixWithSuffix};
use worktree::WorktreeId;
@@ -198,7 +198,7 @@ impl Inventory {
&self,
language: Option<Arc<Language>>,
worktree: Option<WorktreeId>,
task_context: TaskContext,
task_context: &TaskContext,
cx: &mut AppContext,
) -> (
Vec<(TaskSourceKind, ResolvedTask)>,
@@ -214,17 +214,22 @@ impl Inventory {
.flat_map(|task| Some((task_source_kind.as_ref()?, task)));
let mut lru_score = 0_u32;
let mut task_usage = self.last_scheduled_tasks.iter().rev().fold(
HashMap::default(),
|mut tasks, (task_source_kind, resolved_task)| {
tasks
.entry(&resolved_task.id)
.or_insert_with(|| (task_source_kind, resolved_task, post_inc(&mut lru_score)));
tasks
},
);
let mut task_usage = self
.last_scheduled_tasks
.iter()
.rev()
.filter(|(_, task)| !task.original_task().ignore_previously_resolved)
.fold(
HashMap::default(),
|mut tasks, (task_source_kind, resolved_task)| {
tasks.entry(&resolved_task.id).or_insert_with(|| {
(task_source_kind, resolved_task, post_inc(&mut lru_score))
});
tasks
},
);
let not_used_score = post_inc(&mut lru_score);
let current_resolved_tasks = self
let currently_resolved_tasks = self
.sources
.iter()
.filter(|source| {
@@ -242,7 +247,7 @@ impl Inventory {
.chain(language_tasks)
.filter_map(|(kind, task)| {
let id_base = kind.to_id_base();
Some((kind, task.resolve_task(&id_base, task_context.clone())?))
Some((kind, task.resolve_task(&id_base, task_context)?))
})
.map(|(kind, task)| {
let lru_score = task_usage
@@ -252,16 +257,55 @@ impl Inventory {
(kind.clone(), task, lru_score)
})
.collect::<Vec<_>>();
let previous_resolved_tasks = task_usage
let previously_spawned_tasks = task_usage
.into_iter()
.map(|(_, (kind, task, lru_score))| (kind.clone(), task.clone(), lru_score));
previous_resolved_tasks
.chain(current_resolved_tasks)
let mut tasks_by_label = HashMap::default();
tasks_by_label = previously_spawned_tasks.into_iter().fold(
tasks_by_label,
|mut tasks_by_label, (source, task, lru_score)| {
match tasks_by_label.entry((source, task.resolved_label.clone())) {
hash_map::Entry::Occupied(mut o) => {
let (_, previous_lru_score) = o.get();
if previous_lru_score >= &lru_score {
o.insert((task, lru_score));
}
}
hash_map::Entry::Vacant(v) => {
v.insert((task, lru_score));
}
}
tasks_by_label
},
);
tasks_by_label = currently_resolved_tasks.into_iter().fold(
tasks_by_label,
|mut tasks_by_label, (source, task, lru_score)| {
match tasks_by_label.entry((source, task.resolved_label.clone())) {
hash_map::Entry::Occupied(mut o) => {
let (previous_task, _) = o.get();
let new_template = task.original_task();
if new_template.ignore_previously_resolved
|| new_template != previous_task.original_task()
{
o.insert((task, lru_score));
}
}
hash_map::Entry::Vacant(v) => {
v.insert((task, lru_score));
}
}
tasks_by_label
},
);
tasks_by_label
.into_iter()
.map(|((kind, _), (task, lru_score))| (kind, task, lru_score))
.sorted_unstable_by(task_lru_comparator)
.unique_by(|(kind, task, _)| (kind.clone(), task.resolved_label.clone()))
.partition_map(|(kind, task, lru_index)| {
if lru_index < not_used_score {
.partition_map(|(kind, task, lru_score)| {
if lru_score < not_used_score {
Either::Left((kind, task))
} else {
Either::Right((kind, task))
@@ -299,22 +343,14 @@ fn task_lru_comparator(
(kind_b, task_b, lru_score_b): &(TaskSourceKind, ResolvedTask, u32),
) -> cmp::Ordering {
lru_score_a
// First, display recently used templates above all.
.cmp(&lru_score_b)
// Then, ensure more specific sources are displayed first.
.then(task_source_kind_preference(kind_a).cmp(&task_source_kind_preference(kind_b)))
.then(
kind_a
.worktree()
.is_none()
.cmp(&kind_b.worktree().is_none()),
)
.then(kind_a.worktree().cmp(&kind_b.worktree()))
.then(
kind_a
.abs_path()
.is_none()
.cmp(&kind_b.abs_path().is_none()),
)
.then(kind_a.abs_path().cmp(&kind_b.abs_path()))
// After that, display first more specific tasks, using more template variables.
// Bonus points for tasks with symbol variables.
.then(task_variables_preference(task_a).cmp(&task_variables_preference(task_b)))
// Finally, sort by the resolved label, but a bit more specifically, to avoid mixing letters and digits.
.then({
NumericPrefixWithSuffix::from_numeric_prefixed_str(&task_a.resolved_label)
.cmp(&NumericPrefixWithSuffix::from_numeric_prefixed_str(
@@ -333,6 +369,15 @@ fn task_source_kind_preference(kind: &TaskSourceKind) -> u32 {
}
}
fn task_variables_preference(task: &ResolvedTask) -> Reverse<usize> {
let task_variables = task.substituted_variables();
Reverse(if task_variables.contains(&VariableName::Symbol) {
task_variables.len() + 1
} else {
task_variables.len()
})
}
#[cfg(test)]
mod test_inventory {
use gpui::{AppContext, Context as _, Model, ModelContext, TestAppContext};
@@ -421,12 +466,12 @@ mod test_inventory {
let (used, current) = inventory.used_and_current_resolved_tasks(
None,
worktree,
TaskContext::default(),
&TaskContext::default(),
cx,
);
used.into_iter()
.chain(current)
.map(|(_, task)| task.original_task.label)
.map(|(_, task)| task.original_task().label.clone())
.collect()
})
}
@@ -445,7 +490,7 @@ mod test_inventory {
let id_base = task_source_kind.to_id_base();
inventory.task_scheduled(
task_source_kind.clone(),
task.resolve_task(&id_base, TaskContext::default())
task.resolve_task(&id_base, &TaskContext::default())
.unwrap_or_else(|| panic!("Failed to resolve task with name {task_name}")),
);
});
@@ -460,7 +505,7 @@ mod test_inventory {
let (used, current) = inventory.used_and_current_resolved_tasks(
None,
worktree,
TaskContext::default(),
&TaskContext::default(),
cx,
);
let mut all = used;
@@ -699,7 +744,7 @@ mod tests {
(
TaskSourceKind::AbsPath {
id_base: "test source",
abs_path: path_1.to_path_buf(),
abs_path: path_2.to_path_buf(),
},
common_name.to_string(),
),
@@ -713,7 +758,7 @@ mod tests {
(
TaskSourceKind::AbsPath {
id_base: "test source",
abs_path: path_2.to_path_buf(),
abs_path: path_1.to_path_buf(),
},
common_name.to_string(),
),

View File

@@ -44,6 +44,7 @@ impl Project {
.unwrap_or_else(|| Path::new(""));
let (spawn_task, shell) = if let Some(spawn_task) = spawn_task {
log::debug!("Spawning task: {spawn_task:?}");
env.extend(spawn_task.env);
// Activate minimal Python virtual environment
if let Some(python_settings) = &python_settings.as_option() {
@@ -52,7 +53,16 @@ impl Project {
(
Some(TaskState {
id: spawn_task.id,
full_label: spawn_task.full_label,
label: spawn_task.label,
command_label: spawn_task.args.iter().fold(
spawn_task.command.clone(),
|mut command_label, new_arg| {
command_label.push(' ');
command_label.push_str(new_arg);
command_label
},
),
status: TaskStatus::Running,
completion_rx,
}),

View File

@@ -1,6 +1,6 @@
mod project_panel_settings;
use client::{ErrorCode, ErrorExt};
use settings::Settings;
use settings::{Settings, SettingsStore};
use db::kvp::KEY_VALUE_STORE;
use editor::{actions::Cancel, items::entry_git_aware_label_color, scroll::Autoscroll, Editor};
@@ -24,6 +24,7 @@ use project_panel_settings::{ProjectPanelDockPosition, ProjectPanelSettings};
use serde::{Deserialize, Serialize};
use std::{
cmp::Ordering,
collections::HashSet,
ffi::OsStr,
ops::Range,
path::{Path, PathBuf},
@@ -50,6 +51,7 @@ pub struct ProjectPanel {
visible_entries: Vec<(WorktreeId, Vec<Entry>)>,
last_worktree_root_id: Option<ProjectEntryId>,
expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
unfolded_dir_ids: HashSet<ProjectEntryId>,
selection: Option<Selection>,
context_menu: Option<(View<ContextMenu>, Point<Pixels>, Subscription)>,
edit_state: Option<EditState>,
@@ -133,6 +135,8 @@ actions!(
OpenPermanent,
ToggleFocus,
NewSearchInDirectory,
UnfoldDirectory,
FoldDirectory,
]
);
@@ -235,6 +239,16 @@ impl ProjectPanel {
})
.detach();
let mut project_panel_settings = *ProjectPanelSettings::get_global(cx);
cx.observe_global::<SettingsStore>(move |_, cx| {
let new_settings = *ProjectPanelSettings::get_global(cx);
if project_panel_settings != new_settings {
project_panel_settings = new_settings;
cx.notify();
}
})
.detach();
let mut this = Self {
project: project.clone(),
fs: workspace.app_state().fs.clone(),
@@ -243,6 +257,7 @@ impl ProjectPanel {
visible_entries: Default::default(),
last_worktree_root_id: Default::default(),
expanded_dir_ids: Default::default(),
unfolded_dir_ids: Default::default(),
selection: None,
edit_state: None,
context_menu: None,
@@ -403,8 +418,11 @@ impl ProjectPanel {
});
if let Some((worktree, entry)) = self.selected_entry(cx) {
let auto_fold_dirs = ProjectPanelSettings::get_global(cx).auto_fold_dirs;
let is_root = Some(entry) == worktree.root_entry();
let is_dir = entry.is_dir();
let is_foldable = auto_fold_dirs && self.is_foldable(entry, worktree);
let is_unfoldable = auto_fold_dirs && self.is_unfoldable(entry, worktree);
let worktree_id = worktree.id();
let is_local = project.is_local();
let is_read_only = project.is_read_only();
@@ -430,6 +448,12 @@ impl ProjectPanel {
menu.separator()
.action("Find in Folder…", Box::new(NewSearchInDirectory))
})
.when(is_unfoldable, |menu| {
menu.action("Unfold Directory", Box::new(UnfoldDirectory))
})
.when(is_foldable, |menu| {
menu.action("Fold Directory", Box::new(FoldDirectory))
})
.separator()
.action("Cut", Box::new(Cut))
.action("Copy", Box::new(Copy))
@@ -482,6 +506,37 @@ impl ProjectPanel {
cx.notify();
}
fn is_unfoldable(&self, entry: &Entry, worktree: &Worktree) -> bool {
if !entry.is_dir() || self.unfolded_dir_ids.contains(&entry.id) {
return false;
}
if let Some(parent_path) = entry.path.parent() {
let snapshot = worktree.snapshot();
let mut child_entries = snapshot.child_entries(&parent_path);
if let Some(child) = child_entries.next() {
if child_entries.next().is_none() {
return child.kind.is_dir();
}
}
};
false
}
fn is_foldable(&self, entry: &Entry, worktree: &Worktree) -> bool {
if entry.is_dir() {
let snapshot = worktree.snapshot();
let mut child_entries = snapshot.child_entries(&entry.path);
if let Some(child) = child_entries.next() {
if child_entries.next().is_none() {
return child.kind.is_dir();
}
}
}
false
}
fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext<Self>) {
if let Some((worktree, entry)) = self.selected_entry(cx) {
if entry.is_dir() {
@@ -859,6 +914,59 @@ impl ProjectPanel {
});
}
fn unfold_directory(&mut self, _: &UnfoldDirectory, cx: &mut ViewContext<Self>) {
if let Some((worktree, entry)) = self.selected_entry(cx) {
self.unfolded_dir_ids.insert(entry.id);
let snapshot = worktree.snapshot();
let mut parent_path = entry.path.parent();
while let Some(path) = parent_path {
if let Some(parent_entry) = worktree.entry_for_path(path) {
let mut children_iter = snapshot.child_entries(path);
if children_iter.by_ref().take(2).count() > 1 {
break;
}
self.unfolded_dir_ids.insert(parent_entry.id);
parent_path = path.parent();
} else {
break;
}
}
self.update_visible_entries(None, cx);
self.autoscroll(cx);
cx.notify();
}
}
fn fold_directory(&mut self, _: &FoldDirectory, cx: &mut ViewContext<Self>) {
if let Some((worktree, entry)) = self.selected_entry(cx) {
self.unfolded_dir_ids.remove(&entry.id);
let snapshot = worktree.snapshot();
let mut path = &*entry.path;
loop {
let mut child_entries_iter = snapshot.child_entries(path);
if let Some(child) = child_entries_iter.next() {
if child_entries_iter.next().is_none() && child.is_dir() {
self.unfolded_dir_ids.remove(&child.id);
path = &*child.path;
} else {
break;
}
} else {
break;
}
}
self.update_visible_entries(None, cx);
self.autoscroll(cx);
cx.notify();
}
}
fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
if let Some(selection) = self.selection {
let (mut worktree_ix, mut entry_ix, _) =
@@ -1153,6 +1261,7 @@ impl ProjectPanel {
new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
cx: &mut ViewContext<Self>,
) {
let auto_collapse_dirs = ProjectPanelSettings::get_global(cx).auto_fold_dirs;
let project = self.project.read(cx);
self.last_worktree_root_id = project
.visible_worktrees(cx)
@@ -1194,8 +1303,25 @@ impl ProjectPanel {
let mut visible_worktree_entries = Vec::new();
let mut entry_iter = snapshot.entries(true);
while let Some(entry) = entry_iter.entry() {
if auto_collapse_dirs
&& entry.kind.is_dir()
&& !self.unfolded_dir_ids.contains(&entry.id)
{
if let Some(root_path) = snapshot.root_entry() {
let mut child_entries = snapshot.child_entries(&entry.path);
if let Some(child) = child_entries.next() {
if entry.path != root_path.path
&& child_entries.next().is_none()
&& child.kind.is_dir()
{
entry_iter.advance();
continue;
}
}
}
}
visible_worktree_entries.push(entry.clone());
if Some(entry.id) == new_entry_parent_id {
visible_worktree_entries.push(Entry {
@@ -1367,16 +1493,32 @@ impl ProjectPanel {
}
};
let mut details = EntryDetails {
filename: entry
let (depth, difference) = ProjectPanel::calculate_depth_and_difference(
entry,
visible_worktree_entries,
);
let filename = match difference {
diff if diff > 1 => entry
.path
.iter()
.skip(entry.path.components().count() - diff)
.collect::<PathBuf>()
.to_str()
.unwrap_or_default()
.to_string(),
_ => entry
.path
.file_name()
.unwrap_or(root_name)
.to_string_lossy()
.to_string(),
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| root_name.to_string_lossy().to_string()),
};
let mut details = EntryDetails {
filename,
icon,
path: entry.path.clone(),
depth: entry.path.components().count(),
depth,
kind: entry.kind,
is_ignored: entry.is_ignored,
is_expanded,
@@ -1420,6 +1562,45 @@ impl ProjectPanel {
}
}
fn calculate_depth_and_difference(
entry: &Entry,
visible_worktree_entries: &Vec<Entry>,
) -> (usize, usize) {
let visible_worktree_paths: HashSet<Arc<Path>> = visible_worktree_entries
.iter()
.map(|e| e.path.clone())
.collect();
let (depth, difference) = entry
.path
.ancestors()
.skip(1) // Skip the entry itself
.find_map(|ancestor| {
if visible_worktree_paths.contains(ancestor) {
let parent_entry = visible_worktree_entries
.iter()
.find(|&e| &*e.path == ancestor)
.unwrap();
let entry_path_components_count = entry.path.components().count();
let parent_path_components_count = parent_entry.path.components().count();
let difference = entry_path_components_count - parent_path_components_count;
let depth = parent_entry
.path
.ancestors()
.skip(1)
.filter(|ancestor| visible_worktree_paths.contains(*ancestor))
.count();
Some((depth + 1, difference))
} else {
None
}
})
.unwrap_or((0, 0));
(depth, difference)
}
fn render_entry(
&self,
entry_id: ProjectEntryId,
@@ -1461,7 +1642,10 @@ impl ProjectPanel {
.child(if let Some(icon) = &icon {
h_flex().child(Icon::from_path(icon.to_string()).color(filename_text_color))
} else {
h_flex().size(IconSize::default().rems()).invisible()
h_flex()
.size(IconSize::default().rems())
.invisible()
.flex_none()
})
.child(
if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) {
@@ -1572,6 +1756,8 @@ impl Render for ProjectPanel {
.on_action(cx.listener(Self::copy_path))
.on_action(cx.listener(Self::copy_relative_path))
.on_action(cx.listener(Self::new_search_in_directory))
.on_action(cx.listener(Self::unfold_directory))
.on_action(cx.listener(Self::fold_directory))
.when(!project.is_read_only(), |el| {
el.on_action(cx.listener(Self::new_file))
.on_action(cx.listener(Self::new_directory))
@@ -1983,6 +2169,125 @@ mod tests {
);
}
#[gpui::test]
async fn test_auto_collapse_dir_paths(cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor().clone());
fs.insert_tree(
"/root1",
json!({
"dir_1": {
"nested_dir_1": {
"nested_dir_2": {
"nested_dir_3": {
"file_a.java": "// File contents",
"file_b.java": "// File contents",
"file_c.java": "// File contents",
"nested_dir_4": {
"nested_dir_5": {
"file_d.java": "// File contents",
}
}
}
}
}
}
}),
)
.await;
fs.insert_tree(
"/root2",
json!({
"dir_2": {
"file_1.java": "// File contents",
}
}),
)
.await;
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let cx = &mut VisualTestContext::from_window(*workspace, cx);
cx.update(|cx| {
let settings = *ProjectPanelSettings::get_global(cx);
ProjectPanelSettings::override_global(
ProjectPanelSettings {
auto_fold_dirs: true,
..settings
},
cx,
);
});
let panel = workspace
.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
.unwrap();
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
&[
"v root1",
" > dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
"v root2",
" > dir_2",
]
);
toggle_expand_dir(
&panel,
"root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
cx,
);
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
&[
"v root1",
" v dir_1/nested_dir_1/nested_dir_2/nested_dir_3 <== selected",
" > nested_dir_4/nested_dir_5",
" file_a.java",
" file_b.java",
" file_c.java",
"v root2",
" > dir_2",
]
);
toggle_expand_dir(
&panel,
"root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3/nested_dir_4/nested_dir_5",
cx,
);
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
&[
"v root1",
" v dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
" v nested_dir_4/nested_dir_5 <== selected",
" file_d.java",
" file_a.java",
" file_b.java",
" file_c.java",
"v root2",
" > dir_2",
]
);
toggle_expand_dir(&panel, "root2/dir_2", cx);
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
&[
"v root1",
" v dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
" v nested_dir_4/nested_dir_5",
" file_d.java",
" file_a.java",
" file_b.java",
" file_c.java",
"v root2",
" v dir_2 <== selected",
" file_1.java",
]
);
}
#[gpui::test(iterations = 30)]
async fn test_editing_files(cx: &mut gpui::TestAppContext) {
init_test(cx);

View File

@@ -4,14 +4,14 @@ use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Copy, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ProjectPanelDockPosition {
Left,
Right,
}
#[derive(Deserialize, Debug)]
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
pub struct ProjectPanelSettings {
pub default_width: Pixels,
pub dock: ProjectPanelDockPosition,
@@ -20,6 +20,7 @@ pub struct ProjectPanelSettings {
pub git_status: bool,
pub indent_size: f32,
pub auto_reveal_entries: bool,
pub auto_fold_dirs: bool,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
@@ -54,6 +55,11 @@ pub struct ProjectPanelSettingsContent {
///
/// Default: true
pub auto_reveal_entries: Option<bool>,
/// Whether to fold directories automatically
/// when directory has only one directory inside.
///
/// Default: false
pub auto_fold_dirs: Option<bool>,
}
impl Settings for ProjectPanelSettings {

View File

@@ -3,18 +3,21 @@ use assistant::{AssistantPanel, InlineAssist};
use editor::{Editor, EditorSettings};
use gpui::{
Action, ClickEvent, ElementId, EventEmitter, InteractiveElement, ParentElement, Render, Styled,
Subscription, View, ViewContext, WeakView,
anchored, deferred, Action, AnchorCorner, ClickEvent, DismissEvent, ElementId, EventEmitter,
InteractiveElement, ParentElement, Render, Styled, Subscription, View, ViewContext, WeakView,
};
use search::{buffer_search, BufferSearchBar};
use settings::{Settings, SettingsStore};
use ui::{prelude::*, ButtonSize, ButtonStyle, IconButton, IconName, IconSize, Tooltip};
use ui::{
prelude::*, ButtonSize, ButtonStyle, ContextMenu, IconButton, IconName, IconSize, Tooltip,
};
use workspace::{
item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
};
pub struct QuickActionBar {
buffer_search_bar: View<BufferSearchBar>,
toggle_settings_menu: Option<View<ContextMenu>>,
active_item: Option<Box<dyn ItemHandle>>,
_inlay_hints_enabled_subscription: Option<Subscription>,
workspace: WeakView<Workspace>,
@@ -29,6 +32,7 @@ impl QuickActionBar {
) -> Self {
let mut this = Self {
buffer_search_bar,
toggle_settings_menu: None,
active_item: None,
_inlay_hints_enabled_subscription: None,
workspace: workspace.weak_handle(),
@@ -63,6 +67,17 @@ impl QuickActionBar {
ToolbarItemLocation::Hidden
}
}
fn render_menu_overlay(menu: &View<ContextMenu>) -> Div {
div().absolute().bottom_0().right_0().size_0().child(
deferred(
anchored()
.anchor(AnchorCorner::TopRight)
.child(menu.clone()),
)
.with_priority(1),
)
}
}
impl Render for QuickActionBar {
@@ -70,22 +85,6 @@ impl Render for QuickActionBar {
let Some(editor) = self.active_editor() else {
return div().id("empty quick action bar");
};
let inlay_hints_button = Some(QuickActionBarButton::new(
"toggle inlay hints",
IconName::InlayHint,
editor.read(cx).inlay_hints_enabled(),
Box::new(editor::actions::ToggleInlayHints),
"Toggle Inlay Hints",
{
let editor = editor.clone();
move |_, cx| {
editor.update(cx, |editor, cx| {
editor.toggle_inlay_hints(&editor::actions::ToggleInlayHints, cx);
});
}
},
))
.filter(|_| editor.read(cx).supports_inlay_hints(cx));
let search_button = Some(QuickActionBarButton::new(
"toggle buffer search",
@@ -122,14 +121,87 @@ impl Render for QuickActionBar {
},
);
let editor_settings_dropdown =
IconButton::new("toggle_editor_settings_icon", IconName::Sliders)
.size(ButtonSize::Compact)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.selected(self.toggle_settings_menu.is_some())
.on_click({
let editor = editor.clone();
cx.listener(move |quick_action_bar, _, cx| {
let inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
let supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx);
let git_blame_inline_enabled = editor.read(cx).git_blame_inline_enabled();
let menu = ContextMenu::build(cx, |mut menu, _| {
if supports_inlay_hints {
menu = menu.toggleable_entry(
"Show Inlay Hints",
inlay_hints_enabled,
Some(editor::actions::ToggleInlayHints.boxed_clone()),
{
let editor = editor.clone();
move |cx| {
editor.update(cx, |editor, cx| {
editor.toggle_inlay_hints(
&editor::actions::ToggleInlayHints,
cx,
);
});
}
},
);
}
menu = menu.toggleable_entry(
"Show Git Blame Inline",
git_blame_inline_enabled,
Some(editor::actions::ToggleGitBlameInline.boxed_clone()),
{
let editor = editor.clone();
move |cx| {
editor.update(cx, |editor, cx| {
editor.toggle_git_blame_inline(
&editor::actions::ToggleGitBlameInline,
cx,
)
});
}
},
);
menu
});
cx.subscribe(&menu, |quick_action_bar, _, _: &DismissEvent, _cx| {
quick_action_bar.toggle_settings_menu = None;
})
.detach();
quick_action_bar.toggle_settings_menu = Some(menu);
})
})
.when(self.toggle_settings_menu.is_none(), |this| {
this.tooltip(|cx| Tooltip::text("Editor Controls", cx))
});
h_flex()
.id("quick action bar")
.gap_2()
.children(inlay_hints_button)
.children(search_button)
.when(AssistantSettings::get_global(cx).button, |bar| {
bar.child(assistant_button)
})
.gap_3()
.child(
h_flex()
.gap_1p5()
.children(search_button)
.when(AssistantSettings::get_global(cx).button, |bar| {
bar.child(assistant_button)
}),
)
.child(editor_settings_dropdown)
.when_some(
self.toggle_settings_menu.as_ref(),
|el, toggle_settings_menu| {
el.child(Self::render_menu_overlay(toggle_settings_menu))
},
)
}
}

View File

@@ -1,7 +1,7 @@
use futures::FutureExt;
use gpui::{
AnyElement, ElementId, FontStyle, FontWeight, HighlightStyle, InteractiveText, IntoElement,
SharedString, StrikethroughStyle, StyledText, UnderlineStyle, WindowContext,
AnyElement, AnyView, ElementId, FontStyle, FontWeight, HighlightStyle, InteractiveText,
IntoElement, SharedString, StrikethroughStyle, StyledText, UnderlineStyle, WindowContext,
};
use language::{HighlightId, Language, LanguageRegistry};
use std::{ops::Range, sync::Arc};
@@ -31,12 +31,16 @@ impl From<HighlightId> for Highlight {
}
}
#[derive(Debug, Clone)]
#[derive(Clone)]
pub struct RichText {
pub text: SharedString,
pub highlights: Vec<(Range<usize>, Highlight)>,
pub link_ranges: Vec<Range<usize>>,
pub link_urls: Arc<[String]>,
pub custom_ranges: Vec<Range<usize>>,
custom_ranges_tooltip_fn:
Option<Arc<dyn Fn(usize, Range<usize>, &mut WindowContext) -> Option<AnyView>>>,
}
/// Allows one to specify extra links to the rendered markdown, which can be used
@@ -48,7 +52,14 @@ pub struct Mention {
}
impl RichText {
pub fn element(&self, id: ElementId, cx: &WindowContext) -> AnyElement {
pub fn set_tooltip_builder_for_custom_ranges(
&mut self,
f: impl Fn(usize, Range<usize>, &mut WindowContext) -> Option<AnyView> + 'static,
) {
self.custom_ranges_tooltip_fn = Some(Arc::new(f));
}
pub fn element(&self, id: ElementId, cx: &mut WindowContext) -> AnyElement {
let theme = cx.theme();
let code_background = theme.colors().surface_background;
@@ -111,12 +122,21 @@ impl RichText {
.tooltip({
let link_ranges = self.link_ranges.clone();
let link_urls = self.link_urls.clone();
let custom_tooltip_ranges = self.custom_ranges.clone();
let custom_tooltip_fn = self.custom_ranges_tooltip_fn.clone();
move |idx, cx| {
for (ix, range) in link_ranges.iter().enumerate() {
if range.contains(&idx) {
return Some(LinkPreview::new(&link_urls[ix], cx));
}
}
for range in &custom_tooltip_ranges {
if range.contains(&idx) {
if let Some(f) = &custom_tooltip_fn {
return f(idx, range.clone(), cx);
}
}
}
None
}
})
@@ -354,6 +374,8 @@ pub fn render_rich_text(
link_urls: link_urls.into(),
link_ranges,
highlights,
custom_ranges: Vec::new(),
custom_ranges_tooltip_fn: None,
}
}

View File

@@ -13,7 +13,6 @@ path = "src/rope.rs"
[dependencies]
arrayvec = "0.7.1"
bromberg_sl2 = { git = "https://github.com/zed-industries/bromberg_sl2", rev = "950bc5482c216c395049ae33ae4501e08975f17f" }
log.workspace = true
smallvec.workspace = true
sum_tree.workspace = true

View File

@@ -4,7 +4,6 @@ mod point_utf16;
mod unclipped;
use arrayvec::ArrayString;
use bromberg_sl2::HashMatrix;
use smallvec::SmallVec;
use std::{
cmp, fmt, io, mem,
@@ -26,12 +25,6 @@ const CHUNK_BASE: usize = 6;
#[cfg(not(test))]
const CHUNK_BASE: usize = 64;
/// Type alias to [`HashMatrix`], an implementation of a homomorphic hash function. Two [`Rope`] instances
/// containing the same text will produce the same fingerprint. This hash function is special in that
/// it allows us to hash individual chunks and aggregate them up the [`Rope`]'s tree, with the resulting
/// hash being equivalent to hashing all the text contained in the [`Rope`] at once.
pub type RopeFingerprint = HashMatrix;
#[derive(Clone, Default)]
pub struct Rope {
chunks: SumTree<Chunk>,
@@ -42,10 +35,6 @@ impl Rope {
Self::default()
}
pub fn text_fingerprint(text: &str) -> RopeFingerprint {
bromberg_sl2::hash_strict(text.as_bytes())
}
pub fn append(&mut self, rope: Rope) {
let mut chunks = rope.chunks.cursor::<()>();
chunks.next(&());
@@ -424,10 +413,6 @@ impl Rope {
self.clip_point(Point::new(row, u32::MAX), Bias::Left)
.column
}
pub fn fingerprint(&self) -> RopeFingerprint {
self.chunks.summary().fingerprint
}
}
impl<'a> From<&'a str> for Rope {
@@ -994,14 +979,12 @@ impl sum_tree::Item for Chunk {
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ChunkSummary {
text: TextSummary,
fingerprint: RopeFingerprint,
}
impl<'a> From<&'a str> for ChunkSummary {
fn from(text: &'a str) -> Self {
Self {
text: TextSummary::from(text),
fingerprint: Rope::text_fingerprint(text),
}
}
}
@@ -1011,7 +994,6 @@ impl sum_tree::Summary for ChunkSummary {
fn add_summary(&mut self, summary: &Self, _: &()) {
self.text += &summary.text;
self.fingerprint = self.fingerprint * summary.fingerprint;
}
}

View File

@@ -204,6 +204,11 @@ message Envelope {
LanguageModelResponse language_model_response = 167;
CountTokensWithLanguageModel count_tokens_with_language_model = 168;
CountTokensResponse count_tokens_response = 169;
GetCachedEmbeddings get_cached_embeddings = 189;
GetCachedEmbeddingsResponse get_cached_embeddings_response = 190;
ComputeEmbeddings compute_embeddings = 191;
ComputeEmbeddingsResponse compute_embeddings_response = 192; // current max
UpdateChannelMessage update_channel_message = 170;
ChannelMessageUpdate channel_message_update = 171;
@@ -216,7 +221,7 @@ message Envelope {
MultiLspQueryResponse multi_lsp_query_response = 176;
CreateRemoteProject create_remote_project = 177;
CreateRemoteProjectResponse create_remote_project_response = 188; // current max
CreateRemoteProjectResponse create_remote_project_response = 188;
CreateDevServer create_dev_server = 178;
CreateDevServerResponse create_dev_server_response = 179;
ShutdownDevServer shutdown_dev_server = 180;
@@ -750,7 +755,7 @@ message BufferSaved {
uint64 buffer_id = 2;
repeated VectorClockEntry version = 3;
Timestamp mtime = 4;
string fingerprint = 5;
reserved 5;
}
message BufferReloaded {
@@ -758,7 +763,7 @@ message BufferReloaded {
uint64 buffer_id = 2;
repeated VectorClockEntry version = 3;
Timestamp mtime = 4;
string fingerprint = 5;
reserved 5;
LineEnding line_ending = 6;
}
@@ -1605,7 +1610,7 @@ message BufferState {
optional string diff_base = 4;
LineEnding line_ending = 5;
repeated VectorClockEntry saved_version = 6;
string saved_version_fingerprint = 7;
reserved 7;
Timestamp saved_mtime = 8;
}
@@ -1892,6 +1897,29 @@ message CountTokensResponse {
uint32 token_count = 1;
}
message GetCachedEmbeddings {
string model = 1;
repeated bytes digests = 2;
}
message GetCachedEmbeddingsResponse {
repeated Embedding embeddings = 1;
}
message ComputeEmbeddings {
string model = 1;
repeated string texts = 2;
}
message ComputeEmbeddingsResponse {
repeated Embedding embeddings = 1;
}
message Embedding {
bytes digest = 1;
repeated float dimensions = 2;
}
message BlameBuffer {
uint64 project_id = 1;
uint64 buffer_id = 2;

View File

@@ -151,6 +151,8 @@ messages!(
(ChannelMessageSent, Foreground),
(ChannelMessageUpdate, Foreground),
(CompleteWithLanguageModel, Background),
(ComputeEmbeddings, Background),
(ComputeEmbeddingsResponse, Background),
(CopyProjectEntry, Foreground),
(CountTokensWithLanguageModel, Background),
(CountTokensResponse, Background),
@@ -174,6 +176,8 @@ messages!(
(FormatBuffers, Foreground),
(FormatBuffersResponse, Foreground),
(FuzzySearchUsers, Foreground),
(GetCachedEmbeddings, Background),
(GetCachedEmbeddingsResponse, Background),
(GetChannelMembers, Foreground),
(GetChannelMembersResponse, Foreground),
(GetChannelMessages, Background),
@@ -325,6 +329,7 @@ request_messages!(
(CancelCall, Ack),
(CopyProjectEntry, ProjectEntryResponse),
(CompleteWithLanguageModel, LanguageModelResponse),
(ComputeEmbeddings, ComputeEmbeddingsResponse),
(CountTokensWithLanguageModel, CountTokensResponse),
(CreateChannel, CreateChannelResponse),
(CreateProjectEntry, ProjectEntryResponse),
@@ -336,6 +341,7 @@ request_messages!(
(Follow, FollowResponse),
(FormatBuffers, FormatBuffersResponse),
(FuzzySearchUsers, UsersResponse),
(GetCachedEmbeddings, GetCachedEmbeddingsResponse),
(GetChannelMembers, GetChannelMembersResponse),
(GetChannelMessages, GetChannelMessagesResponse),
(GetChannelMessagesById, GetChannelMessagesResponse),

View File

@@ -272,28 +272,21 @@ impl Render for BufferSearchBar {
"Select previous match",
&SelectPrevMatch,
))
.when(!narrow_mode, |this| {
this.child(
h_flex()
.mx(rems_from_px(-4.0))
.min_w(rems_from_px(40.))
.justify_center()
.items_center()
.child(Label::new(match_text).color(
if self.active_match_index.is_some() {
Color::Default
} else {
Color::Disabled
},
)),
)
})
.child(render_nav_button(
ui::IconName::ChevronRight,
self.active_match_index.is_some(),
"Select next match",
&SelectNextMatch,
)),
))
.when(!narrow_mode, |this| {
this.child(h_flex().min_w(rems_from_px(40.)).child(
Label::new(match_text).color(if self.active_match_index.is_some() {
Color::Default
} else {
Color::Disabled
}),
))
}),
);
let replace_line = should_show_replace_input.then(|| {
@@ -531,6 +524,7 @@ impl BufferSearchBar {
}
}
if let Some(active_editor) = self.active_searchable_item.as_ref() {
active_editor.search_bar_visibility_changed(false, cx);
let handle = active_editor.focus_handle(cx);
cx.focus(&handle);
}
@@ -572,10 +566,12 @@ impl BufferSearchBar {
}
pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
if self.active_searchable_item.is_none() {
let Some(handle) = self.active_searchable_item.as_ref() else {
return false;
}
};
self.dismissed = false;
handle.search_bar_visibility_changed(true, cx);
cx.notify();
cx.emit(Event::UpdateLocation);
cx.emit(ToolbarItemEvent::ChangeLocation(
@@ -1100,6 +1096,7 @@ mod tests {
use editor::{DisplayPoint, Editor};
use gpui::{Context, Hsla, TestAppContext, VisualTestContext};
use language::Buffer;
use project::Project;
use smol::stream::StreamExt as _;
use unindent::Unindent as _;
@@ -1110,6 +1107,7 @@ mod tests {
editor::init(cx);
language::init(cx);
Project::init_settings(cx);
theme::init(theme::LoadThemes::JustBase, cx);
});
}
@@ -1868,8 +1866,7 @@ mod tests {
// Let's turn on regex mode.
search_bar
.update(cx, |search_bar, cx| {
search_bar.enable_search_option(SearchOptions::REGEX, cx);
search_bar.search("\\[([^\\]]+)\\]", None, cx)
search_bar.search("\\[([^\\]]+)\\]", Some(SearchOptions::REGEX), cx)
})
.await
.unwrap();
@@ -1892,8 +1889,11 @@ mod tests {
// Now with a whole-word twist.
search_bar
.update(cx, |search_bar, cx| {
search_bar.enable_search_option(SearchOptions::REGEX, cx);
search_bar.search("a\\w+s", Some(SearchOptions::WHOLE_WORD), cx)
search_bar.search(
"a\\w+s",
Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD),
cx,
)
})
.await
.unwrap();

View File

@@ -54,8 +54,6 @@ struct ActiveSettings(HashMap<WeakModel<Project>, ProjectSearchSettings>);
impl Global for ActiveSettings {}
const SEARCH_CONTEXT: u32 = 2;
pub fn init(cx: &mut AppContext) {
cx.set_global(ActiveSettings::default());
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
@@ -234,7 +232,7 @@ impl ProjectSearch {
excerpts.stream_excerpts_with_context_lines(
buffer,
ranges,
SEARCH_CONTEXT,
editor::DEFAULT_MULTIBUFFER_CONTEXT,
cx,
)
})
@@ -1437,20 +1435,6 @@ impl Render for ProjectSearchBar {
Tooltip::for_action("Go to previous match", &SelectPrevMatch, cx)
}),
)
.child(
h_flex()
.mx(rems_from_px(-4.0))
.min_w(rems_from_px(40.))
.justify_center()
.items_center()
.child(
Label::new(match_text).color(if search.active_match_index.is_some() {
Color::Default
} else {
Color::Disabled
}),
),
)
.child(
IconButton::new("project-search-next-match", IconName::ChevronRight)
.disabled(search.active_match_index.is_none())
@@ -1463,6 +1447,17 @@ impl Render for ProjectSearchBar {
}))
.tooltip(|cx| Tooltip::for_action("Go to next match", &SelectNextMatch, cx)),
)
.child(
h_flex()
.min_w(rems_from_px(40.))
.child(
Label::new(match_text).color(if search.active_match_index.is_some() {
Color::Default
} else {
Color::Disabled
}),
),
)
.when(limit_reached, |this| {
this.child(
div()

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