Compare commits

..

114 Commits

Author SHA1 Message Date
Richard Feldman
c6724d598e Render our code edit tool use differently 2024-11-20 11:42:09 -05:00
Richard Feldman
61a516e95f Replace assistant XML parsing with tool use 2024-11-20 11:03:37 -05:00
Richard Feldman
eb1754a091 Use tool call with Suggest Edits 2024-11-20 10:42:08 -05:00
Richard Feldman
2386595de5 wip 2024-11-20 01:01:36 -05:00
Richard Feldman
b36ed56443 wip 2024-11-20 00:49:22 -05:00
Richard Feldman
1b72c5402d Revert "Try cusotomizing RootSchema (decided against this approach)"
This reverts commit e82987db19286a18779f9e2e9d634d9cf98672ee.
2024-11-20 00:45:50 -05:00
Richard Feldman
a143fdc630 Try cusotomizing RootSchema (decided against this approach) 2024-11-20 00:43:08 -05:00
Richard Feldman
1e9666649e Add preliminary tool use for code edits 2024-11-20 00:39:59 -05:00
Conrad Irwin
3c57a4071c vim: Fix jj to exit insert mode (#20890)
Release Notes:

- (Preview only) fixed binding `jj` to exit insert mode
2024-11-19 20:00:11 -07:00
Conrad Irwin
ad6a07e574 Remove comments from discord release announcements (#20888)
Release Notes:

- N/A
2024-11-19 20:00:03 -07:00
Conrad Irwin
c2668bc953 Fix draft-releaase-notes (#20885)
Turns out this was broken because (a) we didn't have tags fetched,
and (b) because the gh-release action we use is buggy.

Release Notes:

- N/A
2024-11-19 19:08:33 -07:00
Conrad Irwin
705a06c3dd Send Country/OS/Version amplitude style (#20884)
Release Notes:

- N/A
2024-11-19 16:38:14 -07:00
Conrad Irwin
f77b6ab79c Fix space repeating in terminal (#20877)
This is broken because of the way we try to emulate macOS's
ApplePressAndHoldEnabled.

Release Notes:

- Fixed holding down space in the terminal (preview only)
2024-11-19 13:43:24 -07:00
Conrad Irwin
ea5131ce0a Country Code To Snowflake (#20875)
Release Notes:

- N/A

---------

Co-authored-by: Nathan Sobo <nathan@zed.dev>
2024-11-19 12:52:00 -07:00
Peter Tripp
1c2b3ad782 Add editor::SelectAllMatches to SublimeText base keymap (#20866)
`alt-f3` on Linux
`ctrl-cmd-g` on MacOS

Co-authored-by: Roman Seidelsohn <rseidelsohn@gmail.com>
2024-11-19 14:33:35 -05:00
Conrad Irwin
496dae968b Remove old CPU/Memory events (#20865)
Release Notes:

- Telemetry: stop reporting CPU/RAM on a timer
2024-11-19 12:25:16 -07:00
Piotr Osiewicz
5c6565a9e0 editor: Use completion filter_range for fuzzy matching (#20869)
Fixes regression from #13958

Closes #20868

Release Notes:

- N/A
2024-11-19 19:49:36 +01:00
Jaagup Averin
7853e32f80 python: Highlight attribute docstrings (#20763)
Adds more docstring highlights missing from #20486.
[PEP257](https://peps.python.org/pep-0257/) defines attribute docstrings
as
> String literals occurring immediately after a simple assignment at the
top level of a module, class, or __init__ method are called “attribute
docstrings”.

This PR adds `@string.doc` for such cases.
Before:

![Screenshot_20241116_162257](https://github.com/user-attachments/assets/6b471cff-717e-4755-9291-d596da927dc6)
After:

![Screenshot_20241116_162457](https://github.com/user-attachments/assets/96674157-9c86-45b6-8ce9-e433ca0ae8ea)

Release Notes:

- Added Python syntax highlighting for attribute docstrings.

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2024-11-19 18:53:36 +01:00
Marshall Bowers
f5cbfa718e assistant: Fix evaluating slash commands in slash command output (like /default) (#20864)
This PR fixes an issue where slash commands in the output of other slash
commands were not being evaluated when configured to do so.

Closes https://github.com/zed-industries/zed/issues/20820.

Release Notes:

- Fixed slash commands from other slash commands (like `/default`) not
being evaluated (Preview only).
2024-11-19 11:20:30 -05:00
Conrad Irwin
6a2c712990 Use Instant not chrono for telemetry (#20756)
We occasionally see dates in the future appearing in our telemetry. One
hypothesis is that this is caused by a clock change while Zed is running
causing date math based on chrono to be incorrect.

Instant *should* be a more stable source of relative timestamps.

Release Notes:

- N/A
2024-11-19 08:23:12 -07:00
Egor Krugletsov
9454f0f1c7 clangd: Use Url::to_file_path() to get actual file path for header/source (#20856)
Using `Url::path()` seems fine on POSIX systems as it will leave forward
slash (given hostname is empty). On Windows it will result in error.

Release Notes:

- N/A
2024-11-19 15:49:21 +02:00
Julian de Ruiter
5b0c15d8c4 Add pytest-based test discovery and runnables for Python (#18824)
Closes  #12080, #18649.

Screenshot:

<img width="1499" alt="image"
src="https://github.com/user-attachments/assets/2644c2fc-19cf-4d2c-a992-5c56cb22deed">

Still in progress:

1. I'd like to add configuration options for selecting a Python test
runner (either pytest or unittest) so that users can explicitly choose
which runner they'd like to use for running their tests. This preference
has to be configured as unittest-style tests can also be run by pytest,
meaning we can't rely on auto-discovery to choose the desired test
runner.
2. I'd like to add venv auto-discovery similar to the feature currently
provided by the terminal using detect_venv.
3. Unit tests.

Unfortunately I'm struggling a bit with how to add settings in the
appropriate location (e.g. Python language settings). Can anyone provide
me with some pointers and/or examples on how to either add extra
settings or to re-use the existing ones?

My rust programming level is OK-ish but I'm not very familiar with the
Zed project structure and could use some help.

I'm also open for pair programming as mentioned on the website if that
helps!

Release Notes:

- Added pytest-based test discovery and runnables for Python.
- Adds a configurable option for switching between unittest and pytest
as a test runner under Python language settings. Set "TASK_RUNNER" to
"unittest" under task settings for Python if you wish to use unittest to
run Python tasks; the default is pytest.

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2024-11-19 14:34:56 +01:00
Bennet Bo Fenner
aae39071ef editor: Show hints for using AI features on empty lines (#20824)
Co-Authored-by: Thorsten <thorsten@zed.dev>
Co-Authored-by: Antonio <antonio@zed.dev>

Screenshot:

![screenshot-2024-11-18-17 11
08@2x](https://github.com/user-attachments/assets/610fd7db-7476-4b9b-9465-a3d55df12340)

TODO:
- [x] docs

Release Notes:

- Added inline hints that guide users on how to invoke the inline
assistant and open the assistant panel. (These hints can be disabled by
setting `{"assistant": {"show_hints": false}}`.)

---------

Co-authored-by: Thorsten <thorsten@zed.dev>
Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
2024-11-19 09:41:44 +01:00
Nate Butler
a35b73e63e Revert "remove usages of theme::color_alpha"
This reverts commit c0d11be75f.
2024-11-19 00:24:48 -05:00
Nate Butler
c0d11be75f remove usages of theme::color_alpha 2024-11-19 00:24:37 -05:00
uncenter
0e26d22fea Add HTML injections for markdown (#20527)
Closes https://github.com/zed-industries/extensions/issues/1588.

| Before | After |
| --- | --- |
| ![CleanShot 2024-11-11 at 22 48
43](https://github.com/user-attachments/assets/9470e6a8-6a37-4b8f-8daa-5c8c5ed2bb17)
| ![CleanShot 2024-11-11 at 22 49
43](https://github.com/user-attachments/assets/f2b858d0-9274-4332-b30e-61c13ac347c6)
|

Release Notes:

- Added HTML injections for markdown syntax highlighting
2024-11-18 20:19:24 -07:00
Kyle Kelley
bd0f197415 Create RunningKernel trait to allow for native and remote jupyter kernels (#20842)
Starts setting up a `RunningKernel` trait to make the remote kernel
implementation easy to get started with. No release notes until this is
all hooked up.

Release Notes:

- N/A
2024-11-18 18:12:23 -08:00
Peter Tripp
343c88574a Improve file_types in default.json (#20429)
Detect .env.* as Shell Script
Move non glob json/jsonc/toml file_types into langauges/*/config.toml
2024-11-18 19:56:45 -05:00
Conrad Irwin
e7a0890086 Don't call setAllowsAutomaticKeyEquivalentLocalization on Big Sur (#20844)
Closes #20821

Release Notes:

- Fixed a crash on Big Sur (preview only)
2024-11-18 16:47:36 -07:00
Conrad Irwin
d4c5c0f05e Don't render invisibles with elements (#20841)
Turns out that in the case you have a somehow valid utf-8 file that
contains almost all ascii control characters, we run out of element
arena space.

Fixes: #20652

Release Notes:

- Fixed a crash when opening a file containing a very large number of
ascii control characters on one line.
2024-11-18 16:47:25 -07:00
lord
f0c7e62adc Leave goal_x unchanged when moving by rows past the start or end of the document (#20705)
Perhaps this was intentional behavior, but if not, I've attempted to
write this hacky fix — I noticed using the vertical arrow keys to move
past the document start/end would reset the goal_x to either zero (for
moving upwards) or the line width (for moving downwards). This change
makes Zed match most native text fields (at least on macOS) which leave
goal_x unchanged, even when hitting the end of the document.

I tested this change manually. Would be happy to add automatic tests for
it too, but couldn't find any existing cursor movement tests.

Release Notes:

- Behavior when moving vertically past the start or end of a document
now matches native text fields; it no longer resets the selection goal
2024-11-18 16:32:43 -07:00
Marshall Bowers
80d50f56f3 collab: Add feature flag to bypass account age check (#20843)
This PR adds a `bypass-account-age-check` feature flag that can be used
to bypass the minimum account age check.

Release Notes:

- N/A
2024-11-18 18:20:32 -05:00
Carroll Wainwright
fb6c987e3e python: Improve function syntax highlighting (#20487)
Release Notes:

- Differentiate between function and method calls and definitions.
`function.definition` matches the highlight for e.g. rust,
`function.call` is new.
- Likewise differentiate between class calls and class definitions.
- Better highlighting of function decorators (the `@` symbol is
punctuation, and now the decorator itself has a `function.decorator`
tag)
- Make `cls` a special variable (like `self`)
- Add `ellipsis` as a built-in constant

Note that most themes do not currently make use of the
`function.definition` tags, and none make use of the
`type.class.definition` tag. Hopefully more themes will pick this up.

*Before:*
<img width="248" alt="image"
src="https://github.com/user-attachments/assets/550ccd3d-594c-413a-b543-ef9caf39eee1">


*After:*
<img width="245" alt="image"
src="https://github.com/user-attachments/assets/47aa43b1-006b-4f9f-9029-510880f390ea">
2024-11-19 00:05:39 +01:00
Michael Sloan
b4c2f29c8b Remove use of current File for new buffers that never have File (#20832)
`create_buffer` calls `Buffer::local` which sets `file` to `None`
[here](f12981db32/crates/language/src/buffer.rs (L629)).
So there's no point in then immediately attempting to update maps that
rely on `file` being present.

Release Notes:

- N/A
2024-11-18 14:30:38 -08:00
Peter Tripp
8666ec95ba ssh: Fix SSH to mac remotes (#20838)
Restore ability to SSH to macOS arm remotes (`uname -m` on mac == `arm64`).
Fix regression introduced in https://github.com/zed-industries/zed/pull/20618
2024-11-18 17:17:24 -05:00
Anthony Eid
889aac9c03 Snippet choices (#13958)
Closes: #12739

Release Notes:

Solves #12739 by
- Enable snippet parsing to successfully parse snippets with choices
- Show completion menu when tabbing to a snippet variable with multiple
choices

Todo:
 - [x] Parse snippet choices
- [x] Open completion menu when tabbing to a snippet variable with
several choices (Thank you Piotr)
- [x] Get snippet choices to reappear when tabbing back to a previous
tabstop in a snippet
 - [x] add snippet unit tests
- [x] Add fuzzy search to snippet choice completion menu & update
completion menu based on choices
 - [x] add completion menu unit tests

Current State:

Using these custom snippets

```json
  "my snippet": {
      "prefix": "log",
      "body": ["type ${1|i32, u32|} = $2"],
      "description": "Expand `log` to `console.log()`"
  },
  "my snippet2": {
      "prefix": "snip",
      "body": [
        "type ${1|i,i8,i16,i64,i32|} ${2|test,test_again,test_final|} = $3"
      ],
      "description": "snippet choice tester"
    }
```

Using snippet choices:



https://github.com/user-attachments/assets/d29fb1a2-7632-4071-944f-daeaa243e3ac

---------

Co-authored-by: Piotr Osiewicz <piotr@zed.dev>
Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2024-11-18 22:56:34 +01:00
Peter Tripp
5b9916e34b ci: Add shellcheck for scripts (#20631)
Fixes shellcheck errors in script/*
Adds a couple trailing newlines.
Adds `script/shellcheck-scripts` and associated CI machinery.
Current set ultra-conservative, does not output warnings, only errors.
2024-11-18 16:41:22 -05:00
Peter Tripp
5b317f60df Improve install-cmake script (#20836)
- Don't output junk to stderr when cmake unavailable
- Kitware PPA does not include up to date bins for all distros (e.g.
Ubuntu 24 only has 3.30.2 although 3.30.4 has been out for a while) so
don't try to force install a specific version. Take the best we can get.
2024-11-18 16:39:57 -05:00
Marshall Bowers
e2552b9add collab: Bypass account age check for users with active LLM subscriptions (#20837)
This PR makes it so users with an active LLM subscription can bypass the
account age check.

Release Notes:

- N/A
2024-11-18 16:37:28 -05:00
Danilo Leal
37899187c6 Adjust file finder width configuration (#20819)
Follow up to: https://github.com/zed-industries/zed/pull/18682

This PR tweaks the setting value, so it's clear we're referring to
`max-width`, meaning the width will change up to a specific value
depending on the available window size. Then, it also makes `Small` the
default value, which, in practice, makes the modal size the same as it
was before the original PR linked above.

Release Notes:

- N/A

---------

Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
2024-11-18 16:32:16 -03:00
Michael Sloan
d265e44209 Don't treat absence of a file on fs as conflict for new files from CLI (#20828)
Closes #20827

Release Notes:

- Fixes bug where save for new files created via CLI would report a
conflict and ask about overwriting.
2024-11-18 10:55:44 -08:00
Peter Tripp
f12981db32 docs: Add Language extension config TBDs (To Be Documented) (#20829)
Release Notes:

- N/A
2024-11-18 13:31:24 -05:00
Michael Sloan
d99f5fe83e Add File.disk_state enum to clarify filesystem states (#20776)
Motivation for this is to make things more understandable while figuring
out #20775.

This is intended to be a refactoring that does not affect behavior, but
there are a few tricky spots:

* Previously `File.mtime()` (now `File.disk_state().mtime()`) would
return last known modification time for deleted files. Looking at uses,
I believe this will not affect anything. If there are behavior changes
here I believe they would be improvements.

* `BufferEvent::DirtyChanged` is now only emitted if dirtiness actually
changed, rather than if it may have changed. This should only be an
efficiency improvement.

Release Notes:

- N/A

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2024-11-18 10:30:08 -08:00
Marshall Bowers
df1d0dec0a ocaml: Extract to zed-extensions/ocaml repository (#20825)
This PR extracts the OCaml extensions to the
[zed-extensions/ocaml](https://github.com/zed-extensions/ocaml)
repository.

Release Notes:

- N/A
2024-11-18 11:30:45 -05:00
Marshall Bowers
ad94ad511a ocaml: Bump to v0.1.1 (#20823)
This PR bumps the OCaml extension to v0.1.1.

Changes:

- https://github.com/zed-industries/zed/pull/20700

Release Notes:

- N/A
2024-11-18 11:02:32 -05:00
Danilo Leal
0e7770a9a2 theme: Add color darken function (#20746)
This PR adds a `darken` function that allows to reduce the lightness of
a color by a certain factor. This popped up as I wanted to add hover
styles to tinted-colors buttons.

Release Notes:

- N/A
2024-11-18 12:44:49 -03:00
Danilo Leal
3f905d57e5 assistant: Adjust title summarization prompt (#20822)
Meant to avoid the excessive use of "Here's a concise 3-7 word title..."
and "Title:" instances we've been seeing lately.
Follow up to: https://github.com/zed-industries/zed/pull/19530

Release Notes:

- Improve prompt for generating title summaries, avoiding preambles
2024-11-18 12:44:06 -03:00
Peter Tripp
f01a86c644 Support for Back/Forward multimedia keys (#20695)
- Added Support for Back/Forward multimedia keys (Linux)
2024-11-18 09:36:08 -05:00
Peter Tripp
5fd7afb9da docs: More language extension config.toml key documentation (#20818)
Release Notes:

- N/A
2024-11-18 09:23:29 -05:00
Michael Sloan
9260abafba Use HashMap<ProjectEntryId, Entry> instead of HashSet<Entry> in outline_panel (#20780)
Came across this because I noticed that `Entry` implements `Hash`, which
was surprising to me. I believe that `ProjectEntryId` should be unique
and so it seems better to dedupe based on this.

Release Notes:

- N/A
2024-11-18 14:38:31 +02:00
Kirill Bulatov
d92166f9f6 Revert "Use livekit's Rust SDK instead of their swift SDK (#13343)" (#20809)
Issues found:

* audio does not work well with various set-ups using USB
* switching audio during initial join may leave the client with no audio
at all
* audio streaming is done on the main thread, beachballing certain
set-ups
* worse screenshare quality (seems that there's no dynamic scaling
anymore, compared to the Swift SDK)

This reverts commit 1235d0808e.

Release Notes:

- N/A
2024-11-18 11:43:53 +02:00
moshyfawn
59a355da74 docs: Update Svelte extension link (#20804)
Closes #20768

Release Notes:

- N/A
2024-11-18 09:51:47 +02:00
Nathan Sobo
ee207ab77e Map "feature upsell" events to the new "Noun Verbed" format (#20787)
Release Notes:

- N/A
2024-11-17 07:38:30 -07:00
Isaac Donaldson
31566cb5a0 Add width setting for the file finder (#18682)
This PR adds the ability to adjust the width of the file finder popup. I
found when searching my projects the default width was not always wide
enough and there was no option to change it.

It allows values `small`, `medium` (default), `large`, `xlarge`, and
`full`

Release Notes:

- Added a setting to adjust the width of the file finder modal


Example Setting:
```json
  "file_finder": {
    "modal_width": "medium"
  },
```

Screenshots can be found in the comments below.
2024-11-16 20:52:43 +02:00
Lu Wan
2d3476530e lsp: Retrieve links to documentation for the given symbol (#19233)
Closes #18924 

Release Notes:

- Added an `editor:OpenDocs` action to open links to documentation via
rust-analyzer
2024-11-16 20:23:49 +02:00
Nathan Sobo
f9990b42fa Send events to Snowflake in the format they're expected by Amplitude (#20765)
This will allow us to use the events table directly in Amplitude, which
lets us use the newer event ingestion flow that detects changes to the
table. Otherwise we'll need a transformation.

I think Amplitude's API is probably a pretty good example to follow for
the raw event schema, even if we don't end up using their product. They
also recommend a "Noun Verbed" format for naming events, so I think we
should go with this. This will help us be consistent and encourage the
author of events to think more clearly about what event they're
reporting.

cc @ConradIrwin 

Release Notes:

- N/A
2024-11-16 09:58:19 -07:00
Siddharth M. Bhatia
97e9137cb7 Update references of Ollama Llama 3.1 to model Llama 3.2 (#20757)
Release Notes:

- N/A
2024-11-16 11:18:53 -05:00
Jason Lee
932c7e23c8 gpui: Fix SVG color render, when color have alpha (#20537)
Release Notes:

- N/A


## Demo

- [Source
SVG](https://github.com/user-attachments/assets/1c681e01-baba-4613-a3e7-ea5cb3015406)
click here open in browser.

| Before | After |
| --- | --- |
| <img width="1212" alt="image"
src="https://github.com/user-attachments/assets/ba323b13-538b-4a34-bb64-9dcf490aface">
| <img width="1212" alt="image"
src="https://github.com/user-attachments/assets/4635926a-843e-426d-89a1-4e9b4f4cc37e">
|

---------

Co-authored-by: Floyd Wang <gassnake999@gmail.com>
2024-11-16 16:53:57 +02:00
Aaron Ruan
65a9c8d994 Dynamic tab bar height (#19076)
Tracking issue: https://github.com/zed-industries/zed/issues/18078

Release Notes:

- Change tab bar height according to `ui-density`

---------

Signed-off-by: Aaron Ruan <aaron212cn@outlook.com>
2024-11-16 13:48:25 +02:00
狐狸
33f09bad60 Highlight ? and : in ternary expressions as operator in JavaScript, TypeScript, and TSX (#20573)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2024-11-16 13:42:10 +02:00
Jason Lee
792c1e4710 gpui: Add paint_path example (#20499)
Release Notes:

- N/A

---

```
cargo run -p gpui --example painting
```

I added this demo to verify the detailed support of Path drawing in
GPUI.

Because of, when we actually used GPUI to draw a 2D line chart, we found
that the straight line Path#line_to did not support `anti-aliasing`, and
the drawn line looked very bad.

As shown in the demo image, if we zoom in on the image, we can clearly
see that all the lines are jagged.

I read and tried to make some appropriate adjustments to the functions
in Path, but since I have no experience in the graphics field, I still
cannot achieve anti-aliasing support so far.

I don't know if I used it wrong somewhere. I checked `curve_to` and
found that the curves drawn have anti-aliasing effects, as shown in the
arc part of the figure below.

<img width="1136" alt="image"
src="https://github.com/user-attachments/assets/4dfb7603-e746-43e9-b737-cff56b56329f">
2024-11-16 13:36:13 +02:00
Gowtham K
b421ffafb5 Windows: Add transparency effect (#20400)
Release Notes:

- Added Transparency effect for Windows #19405 


![image](https://github.com/user-attachments/assets/b0750020-5a89-48c9-b26e-13b30874cf8d)


![image](https://github.com/user-attachments/assets/80609a14-b8c3-4159-b909-1e61f4c3eac3)

---------

Co-authored-by: thedeveloper-sharath <35845141+thedeveloper-sharath@users.noreply.github.com>
2024-11-16 13:24:30 +02:00
SweetPPro
21c785ede4 Add more common Prettier plugin base paths (#20758)
Closes #19024

Release Notes:
- Added some more common Prettier plugin base paths
2024-11-16 13:20:52 +02:00
Mikayla Maki
516f7b3642 Add Loading and Fallback States to Image Elements (via StyledImage) (#20371)
@iamnbutler edit:

This pull request enhances the image element by introducing the ability
to display loading and fallback states.

Changes:

- Implemented the loading and fallback states for image elements using
`.with_loading` and `.with_fallback` respectively.
- Introduced the `StyledImage` trait and `ImageStyle` to enable a fluent
API for changing image styles across image types (`Img`,
`Stateful<Img>`, etc).

Example Usage:

```rust
fn loading_element() -> impl IntoElement {
    div().size_full().flex_none().p_0p5().rounded_sm().child(
        div().size_full().with_animation(
            "loading-bg",
            Animation::new(Duration::from_secs(3))
                .repeat()
                .with_easing(pulsating_between(0.04, 0.24)),
            move |this, delta| this.bg(black().opacity(delta)),
        ),
    )
}

fn fallback_element() -> impl IntoElement {
    let fallback_color: Hsla = black().opacity(0.5);

    div().size_full().flex_none().p_0p5().child(
        div()
            .size_full()
            .flex()
            .items_center()
            .justify_center()
            .rounded_sm()
            .text_sm()
            .text_color(fallback_color)
            .border_1()
            .border_color(fallback_color)
            .child("?"),
    )
}

impl Render for ImageLoadingExample {
    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
        img("some/image/path")
            .id("image-1")
            .with_fallback(|| Self::fallback_element().into_any_element())
            .with_loading(|| Self::loading_element().into_any_element())
    }
}
```

Note:

An `Img` must have an `id` to be able to add a loading state.

Release Notes:

- N/A

---------

Co-authored-by: nate <nate@zed.dev>
Co-authored-by: michael <michael@zed.dev>
Co-authored-by: Nate Butler <iamnbutler@gmail.com>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
2024-11-15 19:12:01 -08:00
Michael Sloan
f34877334e Fix tab strikethrough logic (#20755)
This fix was in downstream commits before splitting out #20711, should
have tested locally before merging.

Release Notes:

- N/A
2024-11-15 17:14:49 -07:00
Peter Tripp
6e296eb4b6 ssh: Use openbsd nc on macOS (#20751)
Co-authored-by: Conrad Irwin <conrad@zed.dev>
2024-11-15 17:27:45 -05:00
Wes Higbee
4c8c6c08fe docs: Fix assistant prompt_overrides template directory (#20434)
Update docs to reflect the correct path for prompt handlebars templates.
Link to git repo for prompts rather than including an out of date version inline.

Co-authored-by: Peter Tripp <peter@zed.dev>
2024-11-15 17:26:50 -05:00
Michael Sloan
050ce919ba Rename "Overwrite" to "Save" for prompt about recreating a deleted file (#20750)
Release Notes:

- N/A
2024-11-15 15:12:40 -07:00
Michael Sloan
369828f51c Require save confirmation and prevent autosave for deleted files (#20742)
* `has_conflict` will now return true if the file has been deleted on
disk.  This is for treating multi-buffers as conflicted, and also
blocks auto-save.

* `has_deleted_file` is added so that the single-file buffer save can
specifically mention the delete conflict. This does not yet handle
discard (#20745).

Closes #9101
Closes #9568
Closes #20462

Release Notes:

- Improved handling of externally deleted files: auto-save will be
disabled, multibuffers will treat this as a save conflict, and single
buffers will ask for restore confirmation.

Co-authored-by: Conrad <conrad@zed.dev>
2024-11-15 15:01:16 -07:00
Conrad Irwin
ac5ecf5487 Don't log every value (#20744)
Release Notes:

- N/A
2024-11-15 14:36:58 -07:00
Max Brunsfeld
1235d0808e Use livekit's Rust SDK instead of their swift SDK (#13343)
See https://github.com/livekit/rust-sdks/pull/355

Todo:

* [x] make `call` / `live_kit_client` crates use the livekit rust sdk
* [x] create a fake version of livekit rust API for integration tests
* [x] capture local audio
* [x] play remote audio
* [x] capture local video tracks
* [x] play remote video tracks
* [x] tests passing
* bugs
* [x] deafening does not work
(https://github.com/livekit/rust-sdks/issues/359)
* [x] mute and speaking status are not replicated properly:
(https://github.com/livekit/rust-sdks/issues/358)
* [x] **linux** - crash due to symbol conflict between WebRTC's
BoringSSL and libcurl's openssl
(https://github.com/livekit/rust-sdks/issues/89)
* [x] **linux** - libwebrtc-sys adds undesired dependencies on `libGL`
and `libXext`
* [x] **windows** - linker error, maybe related to the C++ stdlib
(https://github.com/livekit/rust-sdks/issues/364)
        ```
libwebrtc_sys-54978c6ad5066a35.rlib(video_frame.obj) : error LNK2038:
mismatch detected for 'RuntimeLibrary': value 'MT_StaticRelease' doesn't
match value 'MD_DynamicRelease' in
libtree_sitter_yaml-df6b0adf8f009e8f.rlib(2e40c9e35e9506f4-scanner.o)
        ```
    * [x] audio problems

Release Notes:

- Switch from Swift to Rust LiveKit SDK 🦀

---------

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
Co-authored-by: Conrad Irwin <conrad@zed.dev>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
Co-authored-by: Michael Sloan <michael@zed.dev>
2024-11-15 23:18:50 +02:00
Conrad Irwin
6ff69faf37 Start to send data to Snowflake too (#20698)
This PR adds support for sending telemetry events to AWS Kinesis.

In our AWS account we now have three new things:
* The [Kinesis data
stream](https://us-east-1.console.aws.amazon.com/kinesis/home?region=us-east-1#/streams/details/zed-telemetry/monitoring)
that we will actually write to.
* A [Firehose for
Axiom](https://us-east-1.console.aws.amazon.com/firehose/home?region=us-east-1#/details/telemetry-to-axiom/monitoring)
that sends events from that stream to Axiom for ad-hoc queries over
recent data.
* A [Firehose for
Snowflake](https://us-east-1.console.aws.amazon.com/firehose/home?region=us-east-1#/details/telemetry-to-snowflake/monitoring)
that sends events from that stream to Snowflake for long-term retention.
This Firehose also backs up data into an S3 bucket in case we want to
change how the system works in the future.

In a follow-up PR, we'll add support for ad-hoc telemetry events; and
slowly move away from the current Clickhouse defined schemas; though we
won't move off click house until we have what we need in Snowflake.

Co-Authored-By: Nathan <nathan@zed.dev>

Release Notes:

- N/A
2024-11-15 12:58:00 -07:00
renovate[bot]
f449e8d3d3 Migrate Renovate config (#20741)
The Renovate config in this repository needs migrating. Typically this
is because one or more configuration options you are using have been
renamed.

You don't need to merge this PR right away, because Renovate will
continue to migrate these fields internally each time it runs. But later
some of these fields may be fully deprecated and the migrations removed.
So it's a good idea to merge this migration PR soon.





🔕 **Ignore**: Close this PR and you won't be reminded about config
migration again, but one day your current config may no longer be valid.

 Got questions? Does something look wrong to you? Please don't hesitate
to [request help
here](https://redirect.github.com/renovatebot/renovate/discussions).


---

Release Notes:

- N/A

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-11-15 14:37:39 -05:00
Marshall Bowers
da09cbd055 assistant: Show more details for assist errors (#20740)
This PR updates the Assistant to show more detailed error messages when
the user encounters an assist error.

Here are some examples:

<img width="415" alt="Screenshot 2024-11-15 at 1 47 03 PM"
src="https://github.com/user-attachments/assets/5e7c5d5f-bd78-4af3-86ed-af4c6712770f">

<img width="417" alt="Screenshot 2024-11-15 at 2 11 14 PM"
src="https://github.com/user-attachments/assets/02cb659b-1239-4e24-865f-3a512703a94f">

The notification will scroll if the error lines overflow the set maximum
height.

Release Notes:

- Updated the Assistant to show more details in error cases.
2024-11-15 14:23:46 -05:00
Caleb Heydon
4327459d2a Fix Rust LSP adapter on FreeBSD (#20736)
This PR fixes the Rust LSP adapter on FreeBSD. This issue was creating
build errors.

Release Notes:

- Fixed Rust LSP adapter on FreeBSD
2024-11-15 11:03:55 -07:00
Michael Sloan
cc601bd770 Use strikethrough style in label implementation (#20735)
Release Notes:

- N/A

---------

Co-authored-by: Richard Feldman <richard@zed.dev>
Co-authored-by: Marshall Bowers <marshall@zed.dev>
2024-11-15 10:57:23 -07:00
Marshall Bowers
c491b75e07 assistant: Fix panic when using /project (#20733)
This PR fixes a panic when using `/project` (which is staff-flagged).

We weren't initializing the `SemanticDb` global if the
`project-slash-command` feature flag was enabled.

Closes #20563.

Release Notes:

- N/A
2024-11-15 12:25:29 -05:00
Marshall Bowers
3420ebb428 util: Remove unused code (#20734)
This PR removes the `with_clone` macro from `util`, as it wasn't used
(and isn't needed).

Release Notes:

- N/A
2024-11-15 12:25:18 -05:00
Marshall Bowers
b23d72ec4f gpui: Clean up Styled doc comments (#20731)
This PR cleans up the doc comments on the `Styled` trait to make them
more consistent.

Release Notes:

- N/A
2024-11-15 11:27:49 -05:00
Peter Tripp
e25a03cd3c docs: Variable escaping in tasks (#20730) 2024-11-15 11:14:30 -05:00
Marshall Bowers
9e8ff3f198 Update .mailmap (#20729)
This PR updates the `.mailmap` file to merge some more commit authors.

Release Notes:

- N/A
2024-11-15 11:05:41 -05:00
Marshall Bowers
6d80d5b74b gpui: Add line_through method to Styled (#20728)
This PR adds a `.line_through` method to the `Styled` trait that mirrors
the corresponding Tailwind class.

Release Notes:

- N/A
2024-11-15 10:51:09 -05:00
TOULAR
7137bdee02 Fix scrollbar not always being on top (#20665)
Set the elevation of the scrollbar to 1 borderless, so that the blue
outline is no longer above the scrollbar.

Closes #19875

Release Notes:

- N/A

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2024-11-15 16:47:17 +01:00
Kirill Bulatov
98403aa994 Disable signature help shown by default (#20726)
Closes https://github.com/zed-industries/zed/issues/20725

Stop showing the pop-up that gets an issue open every now and then.

Release Notes:

- Stopped showing signature help after completions by default
2024-11-15 17:19:11 +02:00
lord
794ad1af75 ocaml: Improve highlighting and bracketing (#20700)
Some small improvements to OCaml. Would happily split these into smaller
changes, discard anything, etc.

Before:
<img width="441" alt="before"
src="https://github.com/user-attachments/assets/2fb82b03-0d45-4c59-a94d-6f48d634fe19">

After:
<img width="448" alt="after"
src="https://github.com/user-attachments/assets/aa232d8f-4b1b-48f8-93e2-2147de37a20d">

OCaml highlighting and bracketing improvements
    - Fixed bug where `<` was automatically closed with `>`.
    - Typing `{|` now automatically closes with `|}`
- Type variables are now colored with `variable.special` instead of
`variable`.
- Argument names in function declarations and application are now
colored with `label` instead of `property`, even if they are punned.
- `[@@` and `[%` in macros are now colored as bracket punctuation to
match the closing `]`, rather than colored as `attribute`

Release Notes:

- N/A
2024-11-15 10:17:46 -05:00
Conrad Irwin
4b1f0c033b html: Bump to v0.1.4 (#20692)
Changes:

- https://github.com/zed-industries/zed/pull/20536

Release Notes:

- N/A
2024-11-15 10:10:25 -05:00
Danilo Leal
3796b4a55c project panel: Update decoration icon active color (#20723)
Just so that the icon decoration knockout color matches the background
of a selected/market item.

<img width="600" alt="Screenshot 2024-11-15 at 10 50 24"
src="https://github.com/user-attachments/assets/0037fe5a-f42d-47e8-8559-97ca11ff2d97">

Release Notes:

- N/A
2024-11-15 11:48:26 -03:00
Piotr Osiewicz
8c02929710 pane: Fix rough edges around pinning of dropped project entries (#20722)
Closes #20485

Release Notes:

- Fixed quirks around dropping project entries into tab bar which
might've led to tabs being pinned sometimes.
2024-11-15 13:51:40 +01:00
Kirill Bulatov
1e14697bb6 Fix Linux rust-analyzer downloads in Preview (#20718)
Follow-up of https://github.com/zed-industries/zed/pull/20408

Release Notes:

- (Preview) Fixed broken rust-analyzer downloads
2024-11-15 11:57:54 +02:00
Adam Wolff
f619a872b5 python: Prefer conda environments that match CONDA_PREFIX (#20702)
Release Notes:

- Improved Python toolchain selection for conda environments
2024-11-15 09:12:35 +01:00
Michael Sloan
c03f5b351b Use strikethrough on tab label to indicate file deletion (#20711)
Closes #5364

Release Notes:

- Added indication of deleted files. Files deleted outside of Zed will
have a strikethrough in the title of the tab.
2024-11-15 00:39:09 -07:00
Conrad Irwin
a8df0642a8 vim: Allow :cpplink for CopyPermalinkToLine (#20707)
Release Notes:

- vim: Added `:<range>cpplink` to copy a permanent git link to the
highlighted range to the clipboard
2024-11-14 23:44:40 -07:00
Thorsten Ball
aee01f2c50 assistant: Remove low_speed_timeout (#20681)
This removes the `low_speed_timeout` setting from all providers as a
response to issue #19509.

Reason being that the original `low_speed_timeout` was only as part of
#9913 because users wanted to _get rid of timeouts_. They wanted to bump
the default timeout from 5sec to a lot more.

Then, in the meantime, the meaning of `low_speed_timeout` changed in
#19055 and was changed to a normal `timeout`, which is a different thing
and breaks slower LLMs that don't reply with a complete response in the
configured timeout.

So we figured: let's remove the whole thing and replace it with a
default _connect_ timeout to make sure that we can connect to a server
in 10s, but then give the server as long as it wants to complete its
response.

Closes #19509

Release Notes:

- Removed the `low_speed_timeout` setting from LLM provider settings,
since it was only used to _increase_ the timeout to give LLMs more time,
but since we don't have any other use for it, we simply remove the
setting to give LLMs as long as they need.

---------

Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Peter Tripp <peter@zed.dev>
2024-11-15 07:37:31 +01:00
chottolabs
c9546070ac docs: Update Zig Tree-sitter grammar link (#20708)
lines up with
https://github.com/zed-industries/zed/blob/main/extensions/zig/extension.toml

Release Notes:

- N/A
2024-11-15 07:45:36 +02:00
Marshall Bowers
1855a312d0 Use Extension trait in ExtensionLspAdapter (#20704)
This PR updates the `ExtensionLspAdapter` to go through the `Extension`
trait for interacting with extensions rather than going through the
`WasmHost` directly.

Release Notes:

- N/A
2024-11-14 20:44:57 -05:00
Dairon M.
332b33716a erlang: Update tree-sitter grammar (#20699)
erlang: Update tree-sitter grammar for new OTP 27 features:
- -moduledoc and -doc attributes
- Sigils
- Triple quoted strings

<img width="717" alt="Screenshot 2024-11-14 at 5 18 08 PM"
src="https://github.com/user-attachments/assets/24812b17-4e64-47f3-a6ab-6bc7260cd53f">

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-11-14 18:10:00 -05:00
Conrad Irwin
acf25324be Delete a.html (#20691)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2024-11-14 13:00:03 -07:00
hrou0003
f0882f44a7 vim: Enable % to jump between tags (#20536)
Closes #12986

Release Notes:

- Enable `%` to jump between pairs of tags

---------

Co-authored-by: Harrison <hrouillard@sfi.com.au>
2024-11-14 12:41:53 -07:00
Harsh Narayan Jha
189a034e71 docs: Document exec flags for GDScript (#20688)
While looking up the GDScript extension docs, I noticed that the
original extension repo mentions of `{line}:{col}` placeholders too in
addition to `{project} {file}` that the Zed docs suggest adding.

This PR Improves the docs to add those missing options to the suggested
flags.
2024-11-14 14:28:25 -05:00
Conrad Irwin
7f52071513 Use the project env when running LSPs (#20641)
This change ensures we always run LSPs with the project environment (in
addition to any overrides they provide). This helps ensure the
environment is
set correctly on remotes where we don't load the login shell environment
and
assign it to the current process.

Also fixed the go language to use the project env to find the go
command.

Release Notes:

- Improved environment variable handling for SSH remotes
2024-11-14 12:26:55 -07:00
Piotr Osiewicz
56c93be4de project panel: Fix rendering of groups of dragged project panel entries (#20686)
This PR introduces a new parameter for `on_drag` in gpui, which is an
offset from the element origin to the mouse event origin.

Release Notes:

- Fixed rendering of dragged project panel entries
2024-11-14 19:29:18 +01:00
Marshall Bowers
43999c47e1 client: Remove unneeded return (#20685)
This PR removes an unneeded `return` that was introduced in #19928.

Release Notes:

- N/A
2024-11-14 13:16:55 -05:00
David Soria Parra
690a725667 context_servers: Upgrade protocol to version 2024-11-05 (#20615)
This updates context servers to the most recent version

Release Notes:

- N/A

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-11-14 13:03:30 -05:00
Marshall Bowers
b5ce8e7aa5 zed_extension_api: Release v0.2.0 (#20683)
This PR releases v0.2.0 of the Zed extension API.

Support for this version of the extension API will land in Zed v0.162.x.

Release Notes:

- N/A
2024-11-14 12:44:10 -05:00
Marshall Bowers
d177a1d4e5 Move ExtensionStore tests back to extension_host (#20682)
This PR moves the tests for the `ExtensionStore` back into the
`extension_host` crate.

We now have a separate `TestExtensionRegistrationHooks` to use in the
test that implements the minimal required functionality needed for the
tests. This means that we can depend on the `theme` crate only in the
tests.

Release Notes:

- N/A
2024-11-14 12:09:41 -05:00
renovate[bot]
5d17cfab31 Update Rust crate wasmtime to v24.0.2 [SECURITY] (#20614)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [wasmtime](https://redirect.github.com/bytecodealliance/wasmtime) |
workspace.dependencies | patch | `24.0.1` -> `24.0.2` |

### GitHub Vulnerability Alerts

####
[CVE-2024-51745](https://redirect.github.com/bytecodealliance/wasmtime/security/advisories/GHSA-c2f5-jxjv-2hh8)

### Impact

Wasmtime's filesystem sandbox implementation on Windows blocks access to
special device filenames such as "COM1", "COM2", "LPT0", "LPT1", and so
on, however it did not block access to the special device filenames
which use superscript digits, such as "COM¹", "COM²", "LPT⁰", "LPT¹",
and so on. Untrusted Wasm programs that are given access to any
filesystem directory could bypass the sandbox and access devices through
those special device filenames with superscript digits, and through them
gain access peripheral devices connected to the computer, or network
resources mapped to those devices. This can include modems, printers,
network printers, and any other device connected to a serial or parallel
port, including emulated USB serial ports.

### Patches

Patch releases for Wasmtime have been issued as 24.0.2, 25.0.3, and
26.0.1. Users of Wasmtime 23.0.x and prior are recommended to upgrade to
one of these patched versions.

### Workarounds

There are no known workarounds for this issue. Affected Windows users
are recommended to upgrade.

### References

- [Microsoft's
documentation](https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions)
of the special device filenames
 - [ISO-8859-1](https://en.wikipedia.org/wiki/ISO/IEC_8859-1)
- [The original PR reporting the
issue](https://redirect.github.com/bytecodealliance/cap-std/pull/371)

---

### Release Notes

<details>
<summary>bytecodealliance/wasmtime (wasmtime)</summary>

###
[`v24.0.2`](https://redirect.github.com/bytecodealliance/wasmtime/releases/tag/v24.0.2)

[Compare
Source](https://redirect.github.com/bytecodealliance/wasmtime/compare/v24.0.1...v24.0.2)

#### 24.0.2

Released 2024-11-05.

##### Fixed

- Update to cap-std 3.4.1, for
[#&#8203;9559](https://redirect.github.com/bytecodealliance/wasmtime/issues/9559),
which fixes a wasi-filesystem sandbox
    escape on Windows.

[CVE-2024-51745](https://redirect.github.com/bytecodealliance/wasmtime/security/advisories/GHSA-c2f5-jxjv-2hh8).

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "" in timezone America/New_York,
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xMS41IiwidXBkYXRlZEluVmVyIjoiMzkuMTEuNSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-14 18:35:31 +02:00
renovate[bot]
404ddeebc5 Update Rust crate serde_json to v1.0.132 (#20638)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [serde_json](https://redirect.github.com/serde-rs/json) | dependencies
| patch | `1.0.128` -> `1.0.132` |
| [serde_json](https://redirect.github.com/serde-rs/json) |
workspace.dependencies | patch | `1.0.128` -> `1.0.132` |

---

### Release Notes

<details>
<summary>serde-rs/json (serde_json)</summary>

###
[`v1.0.132`](https://redirect.github.com/serde-rs/json/releases/tag/1.0.132)

[Compare
Source](https://redirect.github.com/serde-rs/json/compare/1.0.131...1.0.132)

- Improve binary size and compile time for JSON array and JSON object
deserialization by about 50%
([#&#8203;1205](https://redirect.github.com/serde-rs/json/issues/1205))
- Improve performance of JSON array and JSON object deserialization by
about 8%
([#&#8203;1206](https://redirect.github.com/serde-rs/json/issues/1206))

###
[`v1.0.131`](https://redirect.github.com/serde-rs/json/releases/tag/1.0.131)

[Compare
Source](https://redirect.github.com/serde-rs/json/compare/1.0.130...1.0.131)

- Implement Deserializer and IntoDeserializer for `Map<String, Value>`
and `&Map<String, Value>`
([#&#8203;1135](https://redirect.github.com/serde-rs/json/issues/1135),
thanks [@&#8203;swlynch99](https://redirect.github.com/swlynch99))

###
[`v1.0.130`](https://redirect.github.com/serde-rs/json/releases/tag/1.0.130)

[Compare
Source](https://redirect.github.com/serde-rs/json/compare/1.0.129...1.0.130)

- Support converting and deserializing `Number` from i128 and u128
([#&#8203;1141](https://redirect.github.com/serde-rs/json/issues/1141),
thanks [@&#8203;druide](https://redirect.github.com/druide))

###
[`v1.0.129`](https://redirect.github.com/serde-rs/json/releases/tag/1.0.129)

[Compare
Source](https://redirect.github.com/serde-rs/json/compare/1.0.128...1.0.129)

- Add
[`serde_json::Map::sort_keys`](https://docs.rs/serde_json/1/serde_json/struct.Map.html#method.sort_keys)
and
[`serde_json::Value::sort_all_objects`](https://docs.rs/serde_json/1/serde_json/enum.Value.html#method.sort_all_objects)
([#&#8203;1199](https://redirect.github.com/serde-rs/json/issues/1199))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these
updates again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xMS41IiwidXBkYXRlZEluVmVyIjoiMzkuMTEuNSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-14 18:35:13 +02:00
renovate[bot]
ad370ed986 Update Rust crate sys-locale to v0.3.2 (#20639)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [sys-locale](https://redirect.github.com/1Password/sys-locale) |
workspace.dependencies | patch | `0.3.1` -> `0.3.2` |

---

### Release Notes

<details>
<summary>1Password/sys-locale (sys-locale)</summary>

###
[`v0.3.2`](https://redirect.github.com/1Password/sys-locale/releases/tag/v0.3.2)

[Compare
Source](https://redirect.github.com/1Password/sys-locale/compare/v0.3.1...v0.3.2)

#### What's Changed

##### Added

- Support for all other Apple targets, such as watchOS and tvOS by
[@&#8203;complexspaces](https://redirect.github.com/complexspaces) in
[https://github.com/1Password/sys-locale/pull/38](https://redirect.github.com/1Password/sys-locale/pull/38).
- Support for ignoring POSIX modifiers in UNIX locales with them present
by [@&#8203;pasabanov](https://redirect.github.com/pasabanov) in
[https://github.com/1Password/sys-locale/pull/33](https://redirect.github.com/1Password/sys-locale/pull/33).
    -   Parsing support/recognition may come at a later date.
- Support for returning a list of user locales on Linux/BSD UNIX
platforms by [@&#8203;pasabanov](https://redirect.github.com/pasabanov)
in
[https://github.com/1Password/sys-locale/pull/35](https://redirect.github.com/1Password/sys-locale/pull/35).

##### Fixed

- No longer use `LC_CTYPE` when determining the locale; the crate now
uses `LC_MESSAGES` in its place by
[@&#8203;pasabanov](https://redirect.github.com/pasabanov) in
[https://github.com/1Password/sys-locale/pull/35](https://redirect.github.com/1Password/sys-locale/pull/35).
- Skip empty locale environment variables on UNIX platforms by
[@&#8203;complexspaces](https://redirect.github.com/complexspaces) in
[https://github.com/1Password/sys-locale/pull/29](https://redirect.github.com/1Password/sys-locale/pull/29).
- Corrected types mentioned and improved the public API documentation by
[@&#8203;pasabanov](https://redirect.github.com/pasabanov) in
[https://github.com/1Password/sys-locale/pull/37](https://redirect.github.com/1Password/sys-locale/pull/37).

##### Changed

- Improved crate download size by excluding unused directories and files
by [@&#8203;pasabanov](https://redirect.github.com/pasabanov).
- Very slight improvement to locale fetching performance on Windows by
[@&#8203;complexspaces](https://redirect.github.com/complexspaces) in
[https://github.com/1Password/sys-locale/pull/29](https://redirect.github.com/1Password/sys-locale/pull/29).
- Increased MSRV to Rust 1.56, which is 3 years old as of this release
by [@&#8203;complexspaces](https://redirect.github.com/complexspaces).

#### New Contributors

- [@&#8203;pasabanov](https://redirect.github.com/pasabanov) made their
first contribution in
[https://github.com/1Password/sys-locale/pull/30](https://redirect.github.com/1Password/sys-locale/pull/30)

**Full Changelog**:
https://github.com/1Password/sys-locale/compare/v0.3.1...v0.3.2

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xMS41IiwidXBkYXRlZEluVmVyIjoiMzkuMTEuNSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-14 18:35:05 +02:00
renovate[bot]
ced9045591 Update Rust crate thiserror to v1.0.69 (#20643)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [thiserror](https://redirect.github.com/dtolnay/thiserror) |
workspace.dependencies | patch | `1.0.64` -> `1.0.69` |

---

### Release Notes

<details>
<summary>dtolnay/thiserror (thiserror)</summary>

###
[`v1.0.69`](https://redirect.github.com/dtolnay/thiserror/releases/tag/1.0.69)

[Compare
Source](https://redirect.github.com/dtolnay/thiserror/compare/1.0.68...1.0.69)

-   Backport 2.0.2 fixes

###
[`v1.0.68`](https://redirect.github.com/dtolnay/thiserror/releases/tag/1.0.68)

[Compare
Source](https://redirect.github.com/dtolnay/thiserror/compare/1.0.67...1.0.68)

- Handle incomplete expressions more robustly in format arguments, such
as while code is being typed
([#&#8203;341](https://redirect.github.com/dtolnay/thiserror/issues/341),
[#&#8203;344](https://redirect.github.com/dtolnay/thiserror/issues/344))

###
[`v1.0.67`](https://redirect.github.com/dtolnay/thiserror/releases/tag/1.0.67)

[Compare
Source](https://redirect.github.com/dtolnay/thiserror/compare/1.0.66...1.0.67)

- Improve expression syntax support inside format arguments
([#&#8203;335](https://redirect.github.com/dtolnay/thiserror/issues/335),
[#&#8203;337](https://redirect.github.com/dtolnay/thiserror/issues/337),
[#&#8203;339](https://redirect.github.com/dtolnay/thiserror/issues/339),
[#&#8203;340](https://redirect.github.com/dtolnay/thiserror/issues/340))

###
[`v1.0.66`](https://redirect.github.com/dtolnay/thiserror/releases/tag/1.0.66)

[Compare
Source](https://redirect.github.com/dtolnay/thiserror/compare/1.0.65...1.0.66)

- Improve compile error on malformed format attribute
([#&#8203;327](https://redirect.github.com/dtolnay/thiserror/issues/327))

###
[`v1.0.65`](https://redirect.github.com/dtolnay/thiserror/releases/tag/1.0.65)

[Compare
Source](https://redirect.github.com/dtolnay/thiserror/compare/1.0.64...1.0.65)

- Ensure OUT_DIR is left with deterministic contents after build script
execution
([#&#8203;325](https://redirect.github.com/dtolnay/thiserror/issues/325))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xMS41IiwidXBkYXRlZEluVmVyIjoiMzkuMTEuNSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-14 18:34:58 +02:00
Kirill Bulatov
0d9bcbba25 Use vim-like keybindings for splitting out of the file finder (#20680)
Follow-up of https://github.com/zed-industries/zed/pull/20507

Release Notes:

- (breaking Preview) Adjusted file finder split keybindings to be less
conflicting

Co-authored-by: Conrad Irwin <conrad@zed.dev>
2024-11-14 18:32:04 +02:00
Peter Tripp
c650ba4e72 docs: Proto formatter example (#20677) 2024-11-14 11:25:37 -05:00
Marshall Bowers
5fab3ca5ba Format workspace Cargo.toml (#20679)
This PR does some formatting of the workspace `Cargo.toml`.

Release Notes:

- N/A
2024-11-14 11:10:58 -05:00
zachcp
621a200d2f docs: Remove duplicate text in the Clojure page (#20635) 2024-11-14 10:36:43 -05:00
Shiny
2544fad8a4 Fix tab switch behavior for the Sublime Text keymap (#20547)
Don't override tab switching behavior, default has correct behavior.
2024-11-14 09:50:29 -05:00
224 changed files with 5687 additions and 3740 deletions

View File

@@ -244,6 +244,7 @@ jobs:
#
# 25 was chosen arbitrarily.
fetch-depth: 25
fetch-tags: true
clean: false
- name: Limit target directory size
@@ -261,6 +262,9 @@ jobs:
mkdir -p target/
# Ignore any errors that occur while drafting release notes to not fail the build.
script/draft-release-notes "$RELEASE_VERSION" "$RELEASE_CHANNEL" > target/release-notes.md || true
script/create-draft-release target/release-notes.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Generate license file
run: script/generate-licenses
@@ -306,7 +310,6 @@ jobs:
target/aarch64-apple-darwin/release/Zed-aarch64.dmg
target/x86_64-apple-darwin/release/Zed-x86_64.dmg
target/release/Zed.dmg
body_path: target/release-notes.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -353,7 +356,6 @@ jobs:
files: |
target/zed-remote-server-linux-x86_64.gz
target/release/zed-linux-x86_64.tar.gz
body: ""
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -400,6 +402,5 @@ jobs:
files: |
target/zed-remote-server-linux-aarch64.gz
target/release/zed-linux-aarch64.tar.gz
body: ""
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

21
.github/workflows/script_checks.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: Script
on:
pull_request:
paths:
- "script/**"
push:
branches:
- main
jobs:
shellcheck:
name: "ShellCheck Scripts"
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Shellcheck ./scripts
run: |
./script/shellcheck-scripts error

View File

@@ -22,10 +22,14 @@ Antonio Scandurra <me@as-cii.com> <antonio@zed.dev>
Bennet Bo Fenner <bennet@zed.dev>
Bennet Bo Fenner <bennet@zed.dev> <53836821+bennetbo@users.noreply.github.com>
Bennet Bo Fenner <bennet@zed.dev> <bennetbo@gmx.de>
Boris Cherny <boris@anthropic.com>
Boris Cherny <boris@anthropic.com> <boris@performancejs.com>
Chris Hayes <chris+git@hayes.software>
Christian Bergschneider <christian.bergschneider@gmx.de>
Christian Bergschneider <christian.bergschneider@gmx.de> <magiclake@gmx.de>
Conrad Irwin <conrad@zed.dev>
Conrad Irwin <conrad@zed.dev> <conrad.irwin@gmail.com>
Dairon Medina <dairon.medina@gmail.com>
Danilo Leal <danilo@zed.dev>
Danilo Leal <danilo@zed.dev> <67129314+danilo-leal@users.noreply.github.com>
Evren Sen <nervenes@icloud.com>
@@ -35,6 +39,7 @@ Fernando Tagawa <tagawafernando@gmail.com>
Fernando Tagawa <tagawafernando@gmail.com> <fernando.tagawa.gamail.com@gmail.com>
Greg Morenz <greg-morenz@droid.cafe>
Greg Morenz <greg-morenz@droid.cafe> <morenzg@gmail.com>
Ihnat Aŭtuška <autushka.ihnat@gmail.com>
Ivan Žužak <izuzak@gmail.com>
Ivan Žužak <izuzak@gmail.com> <ivan.zuzak@github.com>
Joseph T. Lyons <JosephTLyons@gmail.com>
@@ -61,10 +66,13 @@ Max Brunsfeld <maxbrunsfeld@gmail.com> <max@zed.dev>
Max Linke <maxlinke88@gmail.com>
Max Linke <maxlinke88@gmail.com> <kain88-de@users.noreply.github.com>
Michael Sloan <michael@zed.dev>
Michael Sloan <michael@zed.dev> <mgsloan@gmail.com>
Michael Sloan <michael@zed.dev> <mgsloan@google.com>
Mikayla Maki <mikayla@zed.dev>
Mikayla Maki <mikayla@zed.dev> <mikayla.c.maki@gmail.com>
Mikayla Maki <mikayla@zed.dev> <mikayla.c.maki@icloud.com>
Muhammad Talal Anwar <mail@talal.io>
Muhammad Talal Anwar <mail@talal.io> <talalanwar@outlook.com>
Nate Butler <iamnbutler@gmail.com>
Nate Butler <iamnbutler@gmail.com> <nate@zed.dev>
Nathan Sobo <nathan@zed.dev>
@@ -88,7 +96,11 @@ Robert Clover <git@clo4.net>
Robert Clover <git@clo4.net> <robert@clover.gdn>
Roy Williams <roy.williams.iii@gmail.com>
Roy Williams <roy.williams.iii@gmail.com> <roy@anthropic.com>
Sebastijan Kelnerič <sebastijan.kelneric@sebba.dev>
Sebastijan Kelnerič <sebastijan.kelneric@sebba.dev> <sebastijan.kelneric@vichava.com>
Sergey Onufrienko <sergey@onufrienko.com>
Shish <webmaster@shishnet.org>
Shish <webmaster@shishnet.org> <shish@shishnet.org>
Thorben Kröger <dev@thorben.net>
Thorben Kröger <dev@thorben.net> <thorben.kroeger@hexagon.com>
Thorsten Ball <thorsten@zed.dev>

View File

@@ -30,15 +30,6 @@
"tab_size": 2,
"formatter": "prettier"
},
"Proto": {
"tab_size": 4,
"formatter": {
"external": {
"command": "clang-format",
"arguments": ["-style={IndentWidth: 4, ColumnLimit: 0}"]
}
}
},
"Rust": {
"tasks": {
"variables": {

281
Cargo.lock generated
View File

@@ -263,9 +263,9 @@ checksum = "34cd60c5e3152cef0a592f1b296f1cc93715d89d2551d85315828c3a09575ff4"
[[package]]
name = "anyhow"
version = "1.0.93"
version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775"
checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6"
[[package]]
name = "approx"
@@ -549,9 +549,9 @@ dependencies = [
[[package]]
name = "async-compression"
version = "0.4.17"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cb8f1d480b0ea3783ab015936d2a55c87e219676f0c0b7dec61494043f21857"
checksum = "7e614738943d3f68c628ae3dbce7c3daffb196665f82f8c8ea6b65de73c79429"
dependencies = [
"deflate64",
"flate2",
@@ -859,7 +859,7 @@ dependencies = [
"chrono",
"futures-util",
"http-types",
"hyper 0.14.31",
"hyper 0.14.30",
"hyper-rustls 0.24.2",
"serde",
"serde_json",
@@ -915,6 +915,20 @@ dependencies = [
"syn 2.0.76",
]
[[package]]
name = "async-tungstenite"
version = "0.22.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce01ac37fdc85f10a43c43bc582cbd566720357011578a935761075f898baf58"
dependencies = [
"async-std",
"futures-io",
"futures-util",
"log",
"pin-project-lite",
"tungstenite 0.19.0",
]
[[package]]
name = "async-tungstenite"
version = "0.28.0"
@@ -1099,9 +1113,9 @@ dependencies = [
[[package]]
name = "aws-runtime"
version = "1.4.2"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2424565416eef55906f9f8cece2072b6b6a76075e3ff81483ebe938a89a4c05f"
checksum = "a10d5c055aa540164d9561a0e2e74ad30f0dcf7393c3a92f6733ddf9c5762468"
dependencies = [
"aws-credential-types",
"aws-sigv4",
@@ -1123,6 +1137,28 @@ dependencies = [
"uuid",
]
[[package]]
name = "aws-sdk-kinesis"
version = "1.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad48026d3d53881146469b36358d633f1b8c9ad6eb3033f348600f981f2f449b"
dependencies = [
"aws-credential-types",
"aws-runtime",
"aws-smithy-async",
"aws-smithy-http",
"aws-smithy-json",
"aws-smithy-runtime",
"aws-smithy-runtime-api",
"aws-smithy-types",
"aws-types",
"bytes 1.7.2",
"http 0.2.12",
"once_cell",
"regex-lite",
"tracing",
]
[[package]]
name = "aws-sdk-s3"
version = "1.47.0"
@@ -1227,9 +1263,9 @@ dependencies = [
[[package]]
name = "aws-sigv4"
version = "1.2.3"
version = "1.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5df1b0fa6be58efe9d4ccc257df0a53b89cd8909e86591a13ca54817c87517be"
checksum = "5619742a0d8f253be760bfbb8e8e8368c69e3587e4637af5754e488a611499b1"
dependencies = [
"aws-credential-types",
"aws-smithy-eventstream",
@@ -1288,9 +1324,9 @@ dependencies = [
[[package]]
name = "aws-smithy-eventstream"
version = "0.60.4"
version = "0.60.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6363078f927f612b970edf9d1903ef5cef9a64d1e8423525ebb1f0a1633c858"
checksum = "cef7d0a272725f87e51ba2bf89f8c21e4df61b9e49ae1ac367a6d69916ef7c90"
dependencies = [
"aws-smithy-types",
"bytes 1.7.2",
@@ -1299,9 +1335,9 @@ dependencies = [
[[package]]
name = "aws-smithy-http"
version = "0.60.10"
version = "0.60.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01dbcb6e2588fd64cfb6d7529661b06466419e4c54ed1c62d6510d2d0350a728"
checksum = "5c8bc3e8fdc6b8d07d976e301c02fe553f72a39b7a9fea820e023268467d7ab6"
dependencies = [
"aws-smithy-eventstream",
"aws-smithy-runtime-api",
@@ -1339,9 +1375,9 @@ dependencies = [
[[package]]
name = "aws-smithy-runtime"
version = "1.7.1"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1ce695746394772e7000b39fe073095db6d45a862d0767dd5ad0ac0d7f8eb87"
checksum = "be28bd063fa91fd871d131fc8b68d7cd4c5fa0869bea68daca50dcb1cbd76be2"
dependencies = [
"aws-smithy-async",
"aws-smithy-http",
@@ -1354,7 +1390,7 @@ dependencies = [
"http-body 0.4.6",
"http-body 1.0.1",
"httparse",
"hyper 0.14.31",
"hyper 0.14.30",
"hyper-rustls 0.24.2",
"once_cell",
"pin-project-lite",
@@ -1366,9 +1402,9 @@ dependencies = [
[[package]]
name = "aws-smithy-runtime-api"
version = "1.7.2"
version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e086682a53d3aa241192aa110fa8dfce98f2f5ac2ead0de84d41582c7e8fdb96"
checksum = "92165296a47a812b267b4f41032ff8069ab7ff783696d217f0994a0d7ab585cd"
dependencies = [
"aws-smithy-async",
"aws-smithy-types",
@@ -1383,9 +1419,9 @@ dependencies = [
[[package]]
name = "aws-smithy-types"
version = "1.2.4"
version = "1.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "273dcdfd762fae3e1650b8024624e7cd50e484e37abdab73a7a706188ad34543"
checksum = "4fbd94a32b3a7d55d3806fe27d98d3ad393050439dd05eb53ece36ec5e3d3510"
dependencies = [
"base64-simd",
"bytes 1.7.2",
@@ -1445,7 +1481,7 @@ dependencies = [
"headers",
"http 0.2.12",
"http-body 0.4.6",
"hyper 0.14.31",
"hyper 0.14.30",
"itoa",
"matchit",
"memchr",
@@ -2304,9 +2340,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.21"
version = "4.5.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f"
checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
dependencies = [
"clap_builder",
"clap_derive",
@@ -2314,9 +2350,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.21"
version = "4.5.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec"
checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
dependencies = [
"anstream",
"anstyle",
@@ -2385,7 +2421,7 @@ dependencies = [
"clickhouse-derive",
"clickhouse-rs-cityhash-sys",
"futures 0.3.30",
"hyper 0.14.31",
"hyper 0.14.30",
"hyper-tls",
"lz4",
"sealed",
@@ -2424,7 +2460,7 @@ dependencies = [
"anyhow",
"async-native-tls",
"async-recursion 0.3.2",
"async-tungstenite",
"async-tungstenite 0.28.0",
"chrono",
"clock",
"cocoa 0.26.0",
@@ -2449,7 +2485,6 @@ dependencies = [
"settings",
"sha2",
"smol",
"sysinfo",
"telemetry_events",
"text",
"thiserror",
@@ -2466,7 +2501,6 @@ dependencies = [
name = "clock"
version = "0.1.0"
dependencies = [
"chrono",
"parking_lot",
"serde",
"smallvec",
@@ -2557,9 +2591,10 @@ dependencies = [
"assistant",
"async-stripe",
"async-trait",
"async-tungstenite",
"async-tungstenite 0.28.0",
"audio",
"aws-config",
"aws-sdk-kinesis",
"aws-sdk-s3",
"axum",
"axum-extra",
@@ -2588,7 +2623,7 @@ dependencies = [
"gpui",
"hex",
"http_client",
"hyper 0.14.31",
"hyper 0.14.30",
"indoc",
"jsonwebtoken",
"language",
@@ -4189,6 +4224,7 @@ dependencies = [
"serde_json_lenient",
"settings",
"task",
"theme",
"toml 0.8.19",
"url",
"util",
@@ -4203,36 +4239,26 @@ version = "0.1.0"
dependencies = [
"anyhow",
"assistant_slash_command",
"async-compression",
"async-tar",
"client",
"collections",
"context_servers",
"ctor",
"db",
"editor",
"env_logger 0.11.5",
"extension",
"extension_host",
"fs",
"futures 0.3.30",
"fuzzy",
"gpui",
"http_client",
"indexed_docs",
"language",
"log",
"lsp",
"node_runtime",
"num-format",
"parking_lot",
"picker",
"project",
"release_channel",
"reqwest_client",
"semantic_version",
"serde",
"serde_json",
"settings",
"smallvec",
"snippet_provider",
@@ -4455,9 +4481,9 @@ checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d"
[[package]]
name = "flume"
version = "0.11.1"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181"
dependencies = [
"futures-core",
"futures-sink",
@@ -5274,12 +5300,11 @@ dependencies = [
[[package]]
name = "handlebars"
version = "6.2.0"
version = "5.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd4ccde012831f9a071a637b0d4e31df31c0f6c525784b35ae76a9ac6bc1e315"
checksum = "d08485b96a0e6393e9e4d1b8d48cf74ad6c063cd905eb33f42c1ce3f0377539b"
dependencies = [
"log",
"num-order",
"pest",
"pest_derive",
"serde",
@@ -5638,9 +5663,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hyper"
version = "0.14.31"
version = "0.14.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85"
checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9"
dependencies = [
"bytes 1.7.2",
"futures-channel",
@@ -5653,7 +5678,7 @@ dependencies = [
"httpdate",
"itoa",
"pin-project-lite",
"socket2 0.4.10",
"socket2 0.5.7",
"tokio",
"tower-service",
"tracing",
@@ -5688,7 +5713,7 @@ checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
dependencies = [
"futures-util",
"http 0.2.12",
"hyper 0.14.31",
"hyper 0.14.30",
"log",
"rustls 0.21.12",
"rustls-native-certs 0.6.3",
@@ -5721,7 +5746,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
"bytes 1.7.2",
"hyper 0.14.31",
"hyper 0.14.30",
"native-tls",
"tokio",
"tokio-native-tls",
@@ -6227,10 +6252,28 @@ dependencies = [
]
[[package]]
name = "jupyter-serde"
version = "0.4.0"
name = "jupyter-protocol"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd71aa17c4fa65e6d7536ab2728881a41f8feb2ee5841c2240516c3c3d65d8b3"
checksum = "5f3e9d36f282f7e0400de20921d283121a97c5a5a6db2c1bb0c0853defff9934"
dependencies = [
"anyhow",
"async-trait",
"bytes 1.7.2",
"chrono",
"futures 0.3.30",
"jupyter-serde",
"rand 0.8.5",
"serde",
"serde_json",
"uuid",
]
[[package]]
name = "jupyter-serde"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11adb69edaf2eb03d5e84249f68f870dd03d4c8f955314b5a32b2db5798e9b9a"
dependencies = [
"anyhow",
"serde",
@@ -6239,6 +6282,24 @@ dependencies = [
"uuid",
]
[[package]]
name = "jupyter-websocket-client"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d315d037789a652084877b0919615e937d2f2e877b01aa4ba8fcc1ab07cb58b"
dependencies = [
"anyhow",
"async-trait",
"async-tungstenite 0.22.2",
"futures 0.3.30",
"jupyter-protocol",
"jupyter-serde",
"serde",
"serde_json",
"url",
"uuid",
]
[[package]]
name = "khronos-egl"
version = "6.0.0"
@@ -6313,7 +6374,7 @@ dependencies = [
"parking_lot",
"postage",
"pretty_assertions",
"pulldown-cmark 0.12.2",
"pulldown-cmark 0.12.1",
"rand 0.8.5",
"regex",
"rpc",
@@ -6518,9 +6579,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
[[package]]
name = "libc"
version = "0.2.162"
version = "0.2.159"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398"
checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
[[package]]
name = "libdbus-sys"
@@ -6562,7 +6623,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
dependencies = [
"cfg-if",
"windows-targets 0.48.5",
"windows-targets 0.52.6",
]
[[package]]
@@ -6626,18 +6687,18 @@ dependencies = [
[[package]]
name = "linkme"
version = "0.3.31"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "566336154b9e58a4f055f6dd4cbab62c7dc0826ce3c0a04e63b2d2ecd784cdae"
checksum = "3c943daedff228392b791b33bba32e75737756e80a613e32e246c6ce9cbab20a"
dependencies = [
"linkme-impl",
]
[[package]]
name = "linkme-impl"
version = "0.3.31"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edbe595006d355eaf9ae11db92707d4338cd2384d16866131cc1afdbdd35d8d9"
checksum = "cb26336e6dc7cc76e7927d2c9e7e3bb376d7af65a6f56a0b16c47d18a9b1abc5"
dependencies = [
"proc-macro2",
"quote",
@@ -6843,7 +6904,7 @@ dependencies = [
"linkify",
"log",
"node_runtime",
"pulldown-cmark 0.12.2",
"pulldown-cmark 0.12.1",
"settings",
"theme",
"ui",
@@ -6863,7 +6924,7 @@ dependencies = [
"linkify",
"log",
"pretty_assertions",
"pulldown-cmark 0.12.2",
"pulldown-cmark 0.12.1",
"settings",
"theme",
"ui",
@@ -6938,9 +6999,9 @@ dependencies = [
[[package]]
name = "mdbook"
version = "0.4.42"
version = "0.4.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7624879735513024d323e7267a0b3a7176aceb0db537939beb4ee31d9e8945e3"
checksum = "b45a38e19bd200220ef07c892b0157ad3d2365e5b5a267ca01ad12182491eea5"
dependencies = [
"ammonia",
"anyhow",
@@ -6950,7 +7011,7 @@ dependencies = [
"elasticlunr-rs",
"env_logger 0.11.5",
"futures-util",
"handlebars 6.2.0",
"handlebars 5.1.2",
"ignore",
"log",
"memchr",
@@ -7228,9 +7289,9 @@ dependencies = [
[[package]]
name = "nbformat"
version = "0.5.0"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9ffb2ca556072f114bcaf2ca01dde7f1bc8a4946097dd804cb5a22d8af7d6df"
checksum = "187de1b1f1430353ef9b5208096d84f7bf089ee1593f14213d122b7fbb1f3dee"
dependencies = [
"anyhow",
"chrono",
@@ -7504,21 +7565,6 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-modular"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f"
[[package]]
name = "num-order"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6"
dependencies = [
"num-modular",
]
[[package]]
name = "num-rational"
version = "0.4.2"
@@ -8020,9 +8066,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pathdiff"
version = "0.2.2"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d61c5ce1153ab5b689d0c074c4e7fc613e942dfb7dd9eea5ab202d2ad91fe361"
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
[[package]]
name = "pathfinder_geometry"
@@ -8897,27 +8943,27 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.89"
version = "1.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a"
dependencies = [
"unicode-ident",
]
[[package]]
name = "profiling"
version = "1.0.16"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d"
checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58"
dependencies = [
"profiling-procmacros",
]
[[package]]
name = "profiling-procmacros"
version = "1.0.16"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30"
checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd"
dependencies = [
"quote",
"syn 2.0.76",
@@ -9164,9 +9210,9 @@ dependencies = [
[[package]]
name = "pulldown-cmark"
version = "0.12.2"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14"
checksum = "666f0f59e259aea2d72e6012290c09877a780935cc3c18b1ceded41f3890d59c"
dependencies = [
"bitflags 2.6.0",
"memchr",
@@ -9641,6 +9687,7 @@ dependencies = [
"tempfile",
"thiserror",
"util",
"which 6.0.3",
]
[[package]]
@@ -9717,6 +9764,8 @@ dependencies = [
"http_client",
"image",
"indoc",
"jupyter-protocol",
"jupyter-websocket-client",
"language",
"languages",
"log",
@@ -9759,7 +9808,7 @@ dependencies = [
"h2 0.3.26",
"http 0.2.12",
"http-body 0.4.6",
"hyper 0.14.31",
"hyper 0.14.30",
"hyper-tls",
"ipnet",
"js-sys",
@@ -9890,7 +9939,7 @@ dependencies = [
"gpui",
"language",
"linkify",
"pulldown-cmark 0.12.2",
"pulldown-cmark 0.12.1",
"theme",
"ui",
"util",
@@ -10001,7 +10050,7 @@ name = "rpc"
version = "0.1.0"
dependencies = [
"anyhow",
"async-tungstenite",
"async-tungstenite 0.28.0",
"base64 0.22.1",
"chrono",
"collections",
@@ -10043,9 +10092,9 @@ dependencies = [
[[package]]
name = "runtimelib"
version = "0.19.0"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe23ba9967355bbb1be2fb9a8e51bd239ffdf9c791fad5a9b765122ee2bde2e4"
checksum = "2db079f82c110e25c3202d20c7cd29dcbfa93d96de7c5bb8bb6f294f477567cf"
dependencies = [
"anyhow",
"async-dispatcher",
@@ -10057,8 +10106,8 @@ dependencies = [
"dirs 5.0.1",
"futures 0.3.30",
"glob",
"jupyter-protocol",
"jupyter-serde",
"rand 0.8.5",
"ring",
"serde",
"serde_json",
@@ -13000,6 +13049,25 @@ version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8"
[[package]]
name = "tungstenite"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15fba1a6d6bb030745759a9a2a588bfe8490fc8b4751a277db3a0be1c9ebbf67"
dependencies = [
"byteorder",
"bytes 1.7.2",
"data-encoding",
"http 0.2.12",
"httparse",
"log",
"rand 0.8.5",
"sha1",
"thiserror",
"url",
"utf-8",
]
[[package]]
name = "tungstenite"
version = "0.20.1"
@@ -13524,7 +13592,7 @@ dependencies = [
"futures-util",
"headers",
"http 0.2.12",
"hyper 0.14.31",
"hyper 0.14.30",
"log",
"mime",
"mime_guess",
@@ -14237,7 +14305,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.48.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -15319,7 +15387,7 @@ dependencies = [
[[package]]
name = "zed_html"
version = "0.1.3"
version = "0.1.4"
dependencies = [
"zed_extension_api 0.1.0",
]
@@ -15331,13 +15399,6 @@ dependencies = [
"zed_extension_api 0.1.0",
]
[[package]]
name = "zed_ocaml"
version = "0.1.0"
dependencies = [
"zed_extension_api 0.1.0",
]
[[package]]
name = "zed_php"
version = "0.2.2"

View File

@@ -148,7 +148,6 @@ members = [
"extensions/haskell",
"extensions/html",
"extensions/lua",
"extensions/ocaml",
"extensions/php",
"extensions/perplexity",
"extensions/prisma",
@@ -369,12 +368,14 @@ indexmap = { version = "1.6.2", features = ["serde"] }
indoc = "2"
itertools = "0.13.0"
jsonwebtoken = "9.3"
jupyter-protocol = { version = "0.2.0" }
jupyter-websocket-client = { version = "0.4.1" }
libc = "0.2"
linkify = "0.10.0"
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
markup5ever_rcdom = "0.3.0"
nanoid = "0.4"
nbformat = "0.5.0"
nbformat = "0.6.0"
nix = "0.29"
num-format = "0.4.4"
once_cell = "1.19.0"
@@ -389,7 +390,7 @@ pet-core = { git = "https://github.com/microsoft/python-environment-tools.git",
pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
postage = { version = "0.5", features = ["futures-traits"] }
pretty_assertions = "1.3.0"
pretty_assertions = { version = "1.3.0", features = ["unstable"] }
profiling = "1"
prost = "0.9"
prost-build = "0.9"
@@ -408,7 +409,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "fd110f
"stream",
] }
rsa = "0.9.6"
runtimelib = { version = "0.19.0", default-features = false, features = [
runtimelib = { version = "0.21.0", default-features = false, features = [
"async-dispatcher-runtime",
] }
rustc-demangle = "0.1.23"
@@ -563,45 +564,45 @@ ttf-parser = { opt-level = 3 }
wasmtime-cranelift = { opt-level = 3 }
wasmtime = { opt-level = 3 }
# Build single-source-file crates with cg=1 as it helps make `cargo build` of a whole workspace a bit faster
activity_indicator = {codegen-units = 1}
assets = {codegen-units = 1}
breadcrumbs = {codegen-units = 1}
collections = {codegen-units = 1}
command_palette = {codegen-units = 1}
command_palette_hooks = {codegen-units = 1}
evals = {codegen-units = 1}
extension_cli = {codegen-units = 1}
feature_flags = {codegen-units = 1}
file_icons = {codegen-units = 1}
fsevent = {codegen-units = 1}
image_viewer = {codegen-units = 1}
inline_completion_button = {codegen-units = 1}
install_cli = {codegen-units = 1}
journal = {codegen-units = 1}
menu = {codegen-units = 1}
notifications = {codegen-units = 1}
ollama = {codegen-units = 1}
outline = {codegen-units = 1}
paths = {codegen-units = 1}
prettier = {codegen-units = 1}
project_symbols = {codegen-units = 1}
refineable = {codegen-units = 1}
release_channel = {codegen-units = 1}
reqwest_client = {codegen-units = 1}
rich_text = {codegen-units = 1}
semantic_version = {codegen-units = 1}
session = {codegen-units = 1}
snippet = {codegen-units = 1}
snippets_ui = {codegen-units = 1}
sqlez_macros = {codegen-units = 1}
story = {codegen-units = 1}
supermaven_api = {codegen-units = 1}
telemetry_events = {codegen-units = 1}
theme_selector = {codegen-units = 1}
time_format = {codegen-units = 1}
ui_input = {codegen-units = 1}
vcs_menu = {codegen-units = 1}
zed_actions = {codegen-units = 1}
activity_indicator = { codegen-units = 1 }
assets = { codegen-units = 1 }
breadcrumbs = { codegen-units = 1 }
collections = { codegen-units = 1 }
command_palette = { codegen-units = 1 }
command_palette_hooks = { codegen-units = 1 }
evals = { codegen-units = 1 }
extension_cli = { codegen-units = 1 }
feature_flags = { codegen-units = 1 }
file_icons = { codegen-units = 1 }
fsevent = { codegen-units = 1 }
image_viewer = { codegen-units = 1 }
inline_completion_button = { codegen-units = 1 }
install_cli = { codegen-units = 1 }
journal = { codegen-units = 1 }
menu = { codegen-units = 1 }
notifications = { codegen-units = 1 }
ollama = { codegen-units = 1 }
outline = { codegen-units = 1 }
paths = { codegen-units = 1 }
prettier = { codegen-units = 1 }
project_symbols = { codegen-units = 1 }
refineable = { codegen-units = 1 }
release_channel = { codegen-units = 1 }
reqwest_client = { codegen-units = 1 }
rich_text = { codegen-units = 1 }
semantic_version = { codegen-units = 1 }
session = { codegen-units = 1 }
snippet = { codegen-units = 1 }
snippets_ui = { codegen-units = 1 }
sqlez_macros = { codegen-units = 1 }
story = { codegen-units = 1 }
supermaven_api = { codegen-units = 1 }
telemetry_events = { codegen-units = 1 }
theme_selector = { codegen-units = 1 }
time_format = { codegen-units = 1 }
ui_input = { codegen-units = 1 }
vcs_menu = { codegen-units = 1 }
zed_actions = { codegen-units = 1 }
[profile.release]
debug = "limited"

View File

@@ -251,6 +251,8 @@
"ctrl-pagedown": "pane::ActivateNextItem",
"ctrl-shift-pageup": "pane::SwapItemLeft",
"ctrl-shift-pagedown": "pane::SwapItemRight",
"back": "pane::GoBack",
"forward": "pane::GoForward",
"ctrl-w": "pane::CloseActiveItem",
"ctrl-f4": "pane::CloseActiveItem",
"alt-ctrl-t": ["pane::CloseInactiveItems", { "close_pinned": false }],
@@ -648,19 +650,23 @@
}
},
{
"context": "FileFinder",
"context": "FileFinder && !menu_open",
"bindings": {
"ctrl-shift-p": "file_finder::SelectPrev",
"ctrl-k": "file_finder::OpenMenu"
"ctrl": "file_finder::OpenMenu",
"ctrl-j": "pane::SplitDown",
"ctrl-k": "pane::SplitUp",
"ctrl-h": "pane::SplitLeft",
"ctrl-l": "pane::SplitRight"
}
},
{
"context": "FileFinder && menu_open",
"bindings": {
"u": "pane::SplitUp",
"d": "pane::SplitDown",
"l": "pane::SplitLeft",
"r": "pane::SplitRight"
"j": "pane::SplitDown",
"k": "pane::SplitUp",
"h": "pane::SplitLeft",
"l": "pane::SplitRight"
}
},
{

View File

@@ -648,19 +648,23 @@
}
},
{
"context": "FileFinder",
"context": "FileFinder && !menu_open",
"bindings": {
"cmd-shift-p": "file_finder::SelectPrev",
"cmd-k": "file_finder::OpenMenu"
"cmd": "file_finder::OpenMenu",
"cmd-j": "pane::SplitDown",
"cmd-k": "pane::SplitUp",
"cmd-h": "pane::SplitLeft",
"cmd-l": "pane::SplitRight"
}
},
{
"context": "FileFinder && menu_open",
"bindings": {
"u": "pane::SplitUp",
"d": "pane::SplitDown",
"l": "pane::SplitLeft",
"r": "pane::SplitRight"
"j": "pane::SplitDown",
"k": "pane::SplitUp",
"h": "pane::SplitLeft",
"l": "pane::SplitRight"
}
},
{

View File

@@ -4,9 +4,7 @@
"ctrl-shift-[": "pane::ActivatePrevItem",
"ctrl-shift-]": "pane::ActivateNextItem",
"ctrl-pageup": "pane::ActivatePrevItem",
"ctrl-pagedown": "pane::ActivateNextItem",
"ctrl-tab": "pane::ActivateNextItem",
"ctrl-shift-tab": "pane::ActivatePrevItem"
"ctrl-pagedown": "pane::ActivateNextItem"
}
},
{
@@ -18,6 +16,7 @@
"ctrl-shift-l": "editor::SplitSelectionIntoLines",
"ctrl-shift-a": "editor::SelectLargerSyntaxNode",
"ctrl-shift-d": "editor::DuplicateLineDown",
"alt-f3": "editor::SelectAllMatches", // find_all_under
"f12": "editor::GoToDefinition",
"ctrl-f12": "editor::GoToDefinitionSplit",
"shift-f12": "editor::FindAllReferences",

View File

@@ -4,9 +4,7 @@
"cmd-shift-[": "pane::ActivatePrevItem",
"cmd-shift-]": "pane::ActivateNextItem",
"ctrl-pageup": "pane::ActivatePrevItem",
"ctrl-pagedown": "pane::ActivateNextItem",
"ctrl-tab": "pane::ActivateNextItem",
"ctrl-shift-tab": "pane::ActivatePrevItem"
"ctrl-pagedown": "pane::ActivateNextItem"
}
},
{
@@ -21,6 +19,7 @@
"cmd-shift-l": "editor::SplitSelectionIntoLines",
"cmd-shift-a": "editor::SelectLargerSyntaxNode",
"cmd-shift-d": "editor::DuplicateLineDown",
"ctrl-cmd-g": "editor::SelectAllMatches", // find_all_under
"shift-f12": "editor::FindAllReferences",
"alt-cmd-down": "editor::GoToDefinition",
"ctrl-alt-cmd-down": "editor::GoToDefinitionSplit",

View File

@@ -1,206 +0,0 @@
<task_description>
The user of a code editor wants to make a change to their codebase.
You must describe the change using the following XML structure:
- <patch> - A group of related code changes.
Child tags:
- <title> (required) - A high-level description of the changes. This should be as short
as possible, possibly using common abbreviations.
- <edit> (1 or more) - An edit to make at a particular range within a file.
Includes the following child tags:
- <path> (required) - The path to the file that will be changed.
- <description> (optional) - An arbitrarily-long comment that describes the purpose
of this edit.
- <old_text> (optional) - An excerpt from the file's current contents that uniquely
identifies a range within the file where the edit should occur. If this tag is not
specified, then the entire file will be used as the range.
- <new_text> (required) - The new text to insert into the file.
- <operation> (required) - The type of change that should occur at the given range
of the file. Must be one of the following values:
- `update`: Replaces the entire range with the new text.
- `insert_before`: Inserts the new text before the range.
- `insert_after`: Inserts new text after the range.
- `create`: Creates a new file with the given path and the new text.
- `delete`: Deletes the specified range from the file.
<guidelines>
- Never provide multiple edits whose ranges intersect each other. Instead, merge them into one edit.
- Prefer multiple edits to smaller, disjoint ranges, rather than one edit to a larger range.
- There's no need to escape angle brackets within XML tags.
- Always ensure imports are added if you're referencing symbols that are not in scope.
</guidelines>
Here are some concrete examples.
<example>
<message role="user">
```rs src/shapes.rs
pub mod rectangle;
pub mod circle;
```
```rs src/shapes/rectangle.rs
pub struct Rectangle {
width: f64,
height: f64,
}
impl Rectangle {
pub fn new(width: f64, height: f64) -> Self {
Rectangle { width, height }
}
}
```
```rs src/shapes/circle.rs
pub struct Circle {
radius: f64,
}
impl Circle {
pub fn new(radius: f64) -> Self {
Circle { radius }
}
}
```
Update all shapes to store their origin as an (x, y) tuple and implement Display.
</message>
<message role="assistant">
We'll need to update both the rectangle and circle modules.
<patch>
<title>Add origins and display impls to shapes</title>
<edit>
<path>src/shapes/rectangle.rs</path>
<description>Add the origin field to Rectangle struct</description>
<operation>insert_after</operation>
<old_text>
pub struct Rectangle {
</old_text>
<new_text>
origin: (f64, f64),
</new_text>
</edit>
<edit>
<path>src/shapes/rectangle.rs</path>
<operation>update</operation>
<old_text>
fn new(width: f64, height: f64) -> Self {
Rectangle { width, height }
}
</old_text>
<new_text>
fn new(origin: (f64, f64), width: f64, height: f64) -> Self {
Rectangle { origin, width, height }
}
</new_text>
</edit>
<edit>
<path>src/shapes/circle.rs</path>
<description>Add the origin field to Circle struct</description>
<operation>insert_after</operation>
<old_text>
pub struct Circle {
radius: f64,
</old_text>
<new_text>
origin: (f64, f64),
</new_text>
</edit>
<edit>
<path>src/shapes/circle.rs</path>
<operation>update</operation>
<old_text>
fn new(radius: f64) -> Self {
Circle { radius }
}
</old_text>
<new_text>
fn new(origin: (f64, f64), radius: f64) -> Self {
Circle { origin, radius }
}
</new_text>
</edit>
</step>
<edit>
<path>src/shapes/rectangle.rs</path>
<operation>insert_before</operation>
<old_text>
struct Rectangle {
</old_text>
<new_text>
use std::fmt;
</new_text>
</edit>
<edit>
<path>src/shapes/rectangle.rs</path>
<description>
Add a manual Display implementation for Rectangle.
Currently, this is the same as a derived Display implementation.
</description>
<operation>insert_after</operation>
<old_text>
Rectangle { width, height }
}
}
</old_text>
<new_text>
impl fmt::Display for Rectangle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.format_struct(f, "Rectangle")
.field("origin", &self.origin)
.field("width", &self.width)
.field("height", &self.height)
.finish()
}
}
</new_text>
</edit>
<edit>
<path>src/shapes/circle.rs</path>
<operation>insert_before</operation>
<old_text>
struct Circle {
</old_text>
<new_text>
use std::fmt;
</new_text>
</edit>
<edit>
<path>src/shapes/circle.rs</path>
<operation>insert_after</operation>
<old_text>
Circle { radius }
}
}
</old_text>
<new_text>
impl fmt::Display for Rectangle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.format_struct(f, "Rectangle")
.field("origin", &self.origin)
.field("width", &self.width)
.field("height", &self.height)
.finish()
}
}
</new_text>
</edit>
</patch>
</message>
</example>
</task_description>

View File

@@ -157,7 +157,7 @@
"auto_signature_help": false,
/// Whether to show the signature help after completion or a bracket pair inserted.
/// If `auto_signature_help` is enabled, this setting will be treated as enabled also.
"show_signature_help_after_edits": true,
"show_signature_help_after_edits": false,
// Whether to show wrap guides (vertical rulers) in the editor.
// Setting this to true will show a guide at the 'preferred_line_length' value
// if 'soft_wrap' is set to 'preferred_line_length', and will show any
@@ -490,6 +490,9 @@
"version": "2",
// Whether the assistant is enabled.
"enabled": true,
// Whether to show inline hints showing the keybindings to use the inline assistant and the
// assistant panel.
"show_hints": true,
// Whether to show the assistant panel button in the status bar.
"button": true,
// Where to dock the assistant panel. Can be 'left', 'right' or 'bottom'.
@@ -580,7 +583,23 @@
// Settings related to the file finder.
"file_finder": {
// Whether to show file icons in the file finder.
"file_icons": true
"file_icons": true,
// Determines how much space the file finder can take up in relation to the available window width.
// There are 5 possible width values:
//
// 1. Small: This value is essentially a fixed width.
// "modal_width": "small"
// 2. Medium:
// "modal_width": "medium"
// 3. Large:
// "modal_width": "large"
// 4. Extra Large:
// "modal_width": "xlarge"
// 5. Fullscreen: This value removes any horizontal padding, as it consumes the whole viewport width.
// "modal_width": "full"
//
// Default: small
"modal_max_width": "small"
},
// Whether or not to remove any trailing whitespace from lines of a buffer
// before saving it.
@@ -857,15 +876,8 @@
//
"file_types": {
"Plain Text": ["txt"],
"JSON": ["flake.lock"],
"JSONC": [
"**/.zed/**/*.json",
"**/zed/**/*.json",
"**/Zed/**/*.json",
"tsconfig.json",
"pyrightconfig.json"
],
"TOML": ["uv.lock"]
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json"],
"Shell Script": [".env.*"]
},
/// By default use a recent system version of node, or install our own.
/// You can override this to use a version of node that is not in $PATH with:
@@ -1053,13 +1065,11 @@
"api_url": "https://generativelanguage.googleapis.com"
},
"ollama": {
"api_url": "http://localhost:11434",
"low_speed_timeout_in_seconds": 60
"api_url": "http://localhost:11434"
},
"openai": {
"version": "1",
"api_url": "https://api.openai.com/v1",
"low_speed_timeout_in_seconds": 600
"api_url": "https://api.openai.com/v1"
}
},
// Zed's Prettier integration settings.

View File

@@ -1,14 +1,12 @@
mod supported_countries;
use std::time::Duration;
use std::{pin::Pin, str::FromStr};
use anyhow::{anyhow, Context, Result};
use chrono::{DateTime, Utc};
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt};
use http_client::http::{HeaderMap, HeaderValue};
use http_client::{AsyncBody, HttpClient, HttpRequestExt, Method, Request as HttpRequest};
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
use serde::{Deserialize, Serialize};
use std::{pin::Pin, str::FromStr};
use strum::{EnumIter, EnumString};
use thiserror::Error;
use util::ResultExt as _;
@@ -207,9 +205,8 @@ pub async fn stream_completion(
api_url: &str,
api_key: &str,
request: Request,
low_speed_timeout: Option<Duration>,
) -> Result<BoxStream<'static, Result<Event, AnthropicError>>, AnthropicError> {
stream_completion_with_rate_limit_info(client, api_url, api_key, request, low_speed_timeout)
stream_completion_with_rate_limit_info(client, api_url, api_key, request)
.await
.map(|output| output.0)
}
@@ -261,7 +258,6 @@ pub async fn stream_completion_with_rate_limit_info(
api_url: &str,
api_key: &str,
request: Request,
low_speed_timeout: Option<Duration>,
) -> Result<
(
BoxStream<'static, Result<Event, AnthropicError>>,
@@ -274,7 +270,7 @@ pub async fn stream_completion_with_rate_limit_info(
stream: true,
};
let uri = format!("{api_url}/v1/messages");
let mut request_builder = HttpRequest::builder()
let request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(uri)
.header("Anthropic-Version", "2023-06-01")
@@ -284,9 +280,6 @@ pub async fn stream_completion_with_rate_limit_info(
)
.header("X-Api-Key", api_key)
.header("Content-Type", "application/json");
if let Some(low_speed_timeout) = low_speed_timeout {
request_builder = request_builder.read_timeout(low_speed_timeout);
}
let serialized_request =
serde_json::to_string(&request).context("failed to serialize request")?;
let request = request_builder

View File

@@ -18,6 +18,7 @@ mod terminal_inline_assistant;
mod tool_working_set;
mod tools;
use crate::slash_command::project_command::ProjectSlashCommandFeatureFlag;
pub use crate::slash_command_working_set::{SlashCommandId, SlashCommandWorkingSet};
pub use crate::tool_working_set::{ToolId, ToolWorkingSet};
pub use assistant_panel::{AssistantPanel, AssistantPanelEvent};
@@ -215,23 +216,32 @@ pub fn init(
});
}
if cx.has_flag::<SearchSlashCommandFeatureFlag>() {
cx.spawn(|mut cx| {
let client = client.clone();
async move {
let embedding_provider = CloudEmbeddingProvider::new(client.clone());
let semantic_index = SemanticDb::new(
paths::embeddings_dir().join("semantic-index-db.0.mdb"),
Arc::new(embedding_provider),
&mut cx,
)
.await?;
cx.spawn(|mut cx| {
let client = client.clone();
async move {
let is_search_slash_command_enabled = cx
.update(|cx| cx.wait_for_flag::<SearchSlashCommandFeatureFlag>())?
.await;
let is_project_slash_command_enabled = cx
.update(|cx| cx.wait_for_flag::<ProjectSlashCommandFeatureFlag>())?
.await;
cx.update(|cx| cx.set_global(semantic_index))
if !is_search_slash_command_enabled && !is_project_slash_command_enabled {
return Ok(());
}
})
.detach();
}
let embedding_provider = CloudEmbeddingProvider::new(client.clone());
let semantic_index = SemanticDb::new(
paths::embeddings_dir().join("semantic-index-db.0.mdb"),
Arc::new(embedding_provider),
&mut cx,
)
.await?;
cx.update(|cx| cx.set_global(semantic_index))
}
})
.detach();
context_store::init(&client.clone().into());
prompt_library::init(cx);

View File

@@ -1,5 +1,6 @@
use crate::slash_command::file_command::codeblock_fence_for_path;
use crate::slash_command_working_set::SlashCommandWorkingSet;
use crate::tools::code_edits_tool::{CodeEditsTool, CodeEditsToolInput};
use crate::ToolWorkingSet;
use crate::{
assistant_settings::{AssistantDockPosition, AssistantSettings},
@@ -1898,47 +1899,111 @@ impl ContextEditor {
let creases = new_tool_uses
.iter()
.map(|tool_use| {
let placeholder = FoldPlaceholder {
render: render_fold_icon_button(
cx.view().downgrade(),
IconName::PocketKnife,
tool_use.name.clone().into(),
),
..Default::default()
};
let render_trailer =
move |_row, _unfold, _cx: &mut WindowContext| Empty.into_any();
if &tool_use.name == CodeEditsTool::TOOL_NAME {
// If this is a Code Edit tool,
match serde_json::from_value::<CodeEditsToolInput>(
tool_use.input.clone(),
) {
Ok(CodeEditsToolInput { title, edits }) => {
let placeholder = FoldPlaceholder {
render: render_fold_icon_button(
cx.view().downgrade(),
IconName::Sparkle,
title.into(),
),
..Default::default()
};
let render_trailer =
move |_row, _unfold, _cx: &mut WindowContext| {
Empty.into_any()
};
let start = buffer
.anchor_in_excerpt(excerpt_id, tool_use.source_range.start)
.unwrap();
let end = buffer
.anchor_in_excerpt(excerpt_id, tool_use.source_range.end)
.unwrap();
let start = buffer
.anchor_in_excerpt(
excerpt_id,
tool_use.source_range.start,
)
.unwrap();
let end = buffer
.anchor_in_excerpt(
excerpt_id,
tool_use.source_range.end,
)
.unwrap();
let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
buffer_rows_to_fold.insert(buffer_row);
let buffer_row =
MultiBufferRow(start.to_point(&buffer).row);
buffer_rows_to_fold.insert(buffer_row);
self.context.update(cx, |context, cx| {
context.insert_content(
Content::ToolUse {
range: tool_use.source_range.clone(),
tool_use: LanguageModelToolUse {
id: tool_use.id.to_string(),
name: tool_use.name.clone(),
input: tool_use.input.clone(),
self.context.update(cx, |context, cx| {
context.insert_content(
Content::ToolUse {
range: tool_use.source_range.clone(),
tool_use: LanguageModelToolUse {
id: tool_use.id.to_string(),
name: tool_use.name.clone(),
input: tool_use.input.clone(),
},
},
cx,
);
});
Crease::inline(
start..end,
placeholder,
fold_toggle("tool-use"),
render_trailer,
)
}
Err(json_err) => {
// TODO gracefully handle malformed JSON (should distinguish from "errored out" vs "not done streaming yet")
todo!();
}
}
} else {
let placeholder = FoldPlaceholder {
render: render_fold_icon_button(
cx.view().downgrade(),
IconName::PocketKnife,
tool_use.name.clone().into(),
),
..Default::default()
};
let render_trailer =
move |_row, _unfold, _cx: &mut WindowContext| Empty.into_any();
let start = buffer
.anchor_in_excerpt(excerpt_id, tool_use.source_range.start)
.unwrap();
let end = buffer
.anchor_in_excerpt(excerpt_id, tool_use.source_range.end)
.unwrap();
let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
buffer_rows_to_fold.insert(buffer_row);
self.context.update(cx, |context, cx| {
context.insert_content(
Content::ToolUse {
range: tool_use.source_range.clone(),
tool_use: LanguageModelToolUse {
id: tool_use.id.to_string(),
name: tool_use.name.clone(),
input: tool_use.input.clone(),
},
},
},
cx,
);
});
cx,
);
});
Crease::inline(
start..end,
placeholder,
fold_toggle("tool-use"),
render_trailer,
)
Crease::inline(
start..end,
placeholder,
fold_toggle("tool-use"),
render_trailer,
)
}
})
.collect::<Vec<_>>();
@@ -2050,30 +2115,6 @@ impl ContextEditor {
ContextEvent::SlashCommandOutputSectionAdded { section } => {
self.insert_slash_command_output_sections([section.clone()], false, cx);
}
ContextEvent::SlashCommandFinished {
output_range: _output_range,
run_commands_in_ranges,
} => {
for range in run_commands_in_ranges {
let commands = self.context.update(cx, |context, cx| {
context.reparse(cx);
context
.pending_commands_for_range(range.clone(), cx)
.to_vec()
});
for command in commands {
self.run_command(
command.source_range,
&command.name,
&command.arguments,
false,
self.workspace.clone(),
cx,
);
}
}
}
ContextEvent::UsePendingTools => {
let pending_tool_uses = self
.context
@@ -2152,6 +2193,37 @@ impl ContextEditor {
command_id: InvokedSlashCommandId,
cx: &mut ViewContext<Self>,
) {
if let Some(invoked_slash_command) =
self.context.read(cx).invoked_slash_command(&command_id)
{
if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status {
let run_commands_in_ranges = invoked_slash_command
.run_commands_in_ranges
.iter()
.cloned()
.collect::<Vec<_>>();
for range in run_commands_in_ranges {
let commands = self.context.update(cx, |context, cx| {
context.reparse(cx);
context
.pending_commands_for_range(range.clone(), cx)
.to_vec()
});
for command in commands {
self.run_command(
command.source_range,
&command.name,
&command.arguments,
false,
self.workspace.clone(),
cx,
);
}
}
}
}
self.editor.update(cx, |editor, cx| {
if let Some(invoked_slash_command) =
self.context.read(cx).invoked_slash_command(&command_id)
@@ -3333,7 +3405,8 @@ impl ContextEditor {
self.context.update(cx, |context, cx| {
for image in images {
let Some(render_image) = image.to_image_data(cx).log_err() else {
let Some(render_image) = image.to_image_data(cx.svg_renderer()).log_err()
else {
continue;
};
let image_id = image.id();
@@ -3920,7 +3993,7 @@ impl ContextEditor {
.child(
div()
.id("error-message")
.max_h_24()
.max_h_32()
.overflow_y_scroll()
.child(Label::new(error_message.clone())),
)

View File

@@ -35,20 +35,17 @@ pub enum AssistantProviderContentV1 {
OpenAi {
default_model: Option<OpenAiModel>,
api_url: Option<String>,
low_speed_timeout_in_seconds: Option<u64>,
available_models: Option<Vec<OpenAiModel>>,
},
#[serde(rename = "anthropic")]
Anthropic {
default_model: Option<AnthropicModel>,
api_url: Option<String>,
low_speed_timeout_in_seconds: Option<u64>,
},
#[serde(rename = "ollama")]
Ollama {
default_model: Option<OllamaModel>,
api_url: Option<String>,
low_speed_timeout_in_seconds: Option<u64>,
},
}
@@ -63,6 +60,7 @@ pub struct AssistantSettings {
pub inline_alternatives: Vec<LanguageModelSelection>,
pub using_outdated_settings_version: bool,
pub enable_experimental_live_diffs: bool,
pub show_hints: bool,
}
impl AssistantSettings {
@@ -115,47 +113,41 @@ impl AssistantSettingsContent {
if let VersionedAssistantSettingsContent::V1(settings) = settings {
if let Some(provider) = settings.provider.clone() {
match provider {
AssistantProviderContentV1::Anthropic {
api_url,
low_speed_timeout_in_seconds,
..
} => update_settings_file::<AllLanguageModelSettings>(
fs,
cx,
move |content, _| {
if content.anthropic.is_none() {
content.anthropic = Some(AnthropicSettingsContent::Versioned(
VersionedAnthropicSettingsContent::V1(
AnthropicSettingsContentV1 {
api_url,
low_speed_timeout_in_seconds,
available_models: None,
},
),
));
}
},
),
AssistantProviderContentV1::Ollama {
api_url,
low_speed_timeout_in_seconds,
..
} => update_settings_file::<AllLanguageModelSettings>(
fs,
cx,
move |content, _| {
if content.ollama.is_none() {
content.ollama = Some(OllamaSettingsContent {
api_url,
low_speed_timeout_in_seconds,
available_models: None,
});
}
},
),
AssistantProviderContentV1::Anthropic { api_url, .. } => {
update_settings_file::<AllLanguageModelSettings>(
fs,
cx,
move |content, _| {
if content.anthropic.is_none() {
content.anthropic =
Some(AnthropicSettingsContent::Versioned(
VersionedAnthropicSettingsContent::V1(
AnthropicSettingsContentV1 {
api_url,
available_models: None,
},
),
));
}
},
)
}
AssistantProviderContentV1::Ollama { api_url, .. } => {
update_settings_file::<AllLanguageModelSettings>(
fs,
cx,
move |content, _| {
if content.ollama.is_none() {
content.ollama = Some(OllamaSettingsContent {
api_url,
available_models: None,
});
}
},
)
}
AssistantProviderContentV1::OpenAi {
api_url,
low_speed_timeout_in_seconds,
available_models,
..
} => update_settings_file::<AllLanguageModelSettings>(
@@ -188,7 +180,6 @@ impl AssistantSettingsContent {
VersionedOpenAiSettingsContent::V1(
OpenAiSettingsContentV1 {
api_url,
low_speed_timeout_in_seconds,
available_models,
},
),
@@ -212,6 +203,7 @@ impl AssistantSettingsContent {
AssistantSettingsContent::Versioned(settings) => match settings {
VersionedAssistantSettingsContent::V1(settings) => AssistantSettingsContentV2 {
enabled: settings.enabled,
show_hints: None,
button: settings.button,
dock: settings.dock,
default_width: settings.default_width,
@@ -252,6 +244,7 @@ impl AssistantSettingsContent {
},
AssistantSettingsContent::Legacy(settings) => AssistantSettingsContentV2 {
enabled: None,
show_hints: None,
button: settings.button,
dock: settings.dock,
default_width: settings.default_width,
@@ -298,54 +291,41 @@ impl AssistantSettingsContent {
log::warn!("attempted to set zed.dev model on outdated settings");
}
"anthropic" => {
let (api_url, low_speed_timeout_in_seconds) = match &settings.provider {
Some(AssistantProviderContentV1::Anthropic {
api_url,
low_speed_timeout_in_seconds,
..
}) => (api_url.clone(), *low_speed_timeout_in_seconds),
_ => (None, None),
let api_url = match &settings.provider {
Some(AssistantProviderContentV1::Anthropic { api_url, .. }) => {
api_url.clone()
}
_ => None,
};
settings.provider = Some(AssistantProviderContentV1::Anthropic {
default_model: AnthropicModel::from_id(&model).ok(),
api_url,
low_speed_timeout_in_seconds,
});
}
"ollama" => {
let (api_url, low_speed_timeout_in_seconds) = match &settings.provider {
Some(AssistantProviderContentV1::Ollama {
api_url,
low_speed_timeout_in_seconds,
..
}) => (api_url.clone(), *low_speed_timeout_in_seconds),
_ => (None, None),
let api_url = match &settings.provider {
Some(AssistantProviderContentV1::Ollama { api_url, .. }) => {
api_url.clone()
}
_ => None,
};
settings.provider = Some(AssistantProviderContentV1::Ollama {
default_model: Some(ollama::Model::new(&model, None, None)),
api_url,
low_speed_timeout_in_seconds,
});
}
"openai" => {
let (api_url, low_speed_timeout_in_seconds, available_models) =
match &settings.provider {
Some(AssistantProviderContentV1::OpenAi {
api_url,
low_speed_timeout_in_seconds,
available_models,
..
}) => (
api_url.clone(),
*low_speed_timeout_in_seconds,
available_models.clone(),
),
_ => (None, None, None),
};
let (api_url, available_models) = match &settings.provider {
Some(AssistantProviderContentV1::OpenAi {
api_url,
available_models,
..
}) => (api_url.clone(), available_models.clone()),
_ => (None, None),
};
settings.provider = Some(AssistantProviderContentV1::OpenAi {
default_model: OpenAiModel::from_id(&model).ok(),
api_url,
low_speed_timeout_in_seconds,
available_models,
});
}
@@ -377,6 +357,7 @@ impl Default for VersionedAssistantSettingsContent {
fn default() -> Self {
Self::V2(AssistantSettingsContentV2 {
enabled: None,
show_hints: None,
button: None,
dock: None,
default_width: None,
@@ -394,6 +375,11 @@ pub struct AssistantSettingsContentV2 {
///
/// Default: true
enabled: Option<bool>,
/// Whether to show inline hints that show keybindings for inline assistant
/// and assistant panel.
///
/// Default: true
show_hints: Option<bool>,
/// Whether to show the assistant panel button in the status bar.
///
/// Default: true
@@ -528,6 +514,7 @@ impl Settings for AssistantSettings {
let value = value.upgrade();
merge(&mut settings.enabled, value.enabled);
merge(&mut settings.show_hints, value.show_hints);
merge(&mut settings.button, value.button);
merge(&mut settings.dock, value.dock);
merge(
@@ -598,6 +585,7 @@ mod tests {
}),
inline_alternatives: None,
enabled: None,
show_hints: None,
button: None,
dock: None,
default_width: None,

View File

@@ -6,12 +6,14 @@ use crate::ToolWorkingSet;
use crate::{
prompts::PromptBuilder,
slash_command::{file_command::FileCommandMetadata, SlashCommandLine},
AssistantEdit, AssistantPatch, AssistantPatchStatus, MessageId, MessageStatus,
tools::code_edits_tool::CodeEditsTool,
AssistantPatch, MessageId, MessageStatus,
};
use anyhow::{anyhow, Context as _, Result};
use assistant_slash_command::{
SlashCommandContent, SlashCommandEvent, SlashCommandOutputSection, SlashCommandResult,
};
use assistant_tool::Tool;
use client::{self, proto, telemetry::Telemetry};
use clock::ReplicaId;
use collections::{HashMap, HashSet};
@@ -43,7 +45,6 @@ use std::{
iter, mem,
ops::Range,
path::{Path, PathBuf},
str::FromStr as _,
sync::Arc,
time::{Duration, Instant},
};
@@ -381,10 +382,6 @@ pub enum ContextEvent {
SlashCommandOutputSectionAdded {
section: SlashCommandOutputSection<language::Anchor>,
},
SlashCommandFinished {
output_range: Range<language::Anchor>,
run_commands_in_ranges: Vec<Range<language::Anchor>>,
},
UsePendingTools,
ToolFinished {
tool_use_id: Arc<str>,
@@ -565,7 +562,6 @@ pub struct Context {
telemetry: Option<Arc<Telemetry>>,
language_registry: Arc<LanguageRegistry>,
patches: Vec<AssistantPatch>,
xml_tags: Vec<XmlTag>,
project: Option<Model<Project>>,
prompt_builder: Arc<PromptBuilder>,
}
@@ -674,7 +670,6 @@ impl Context {
slash_commands,
tools,
patches: Vec::new(),
xml_tags: Vec::new(),
prompt_builder,
};
@@ -916,6 +911,7 @@ impl Context {
InvokedSlashCommand {
name: name.into(),
range: output_range,
run_commands_in_ranges: Vec::new(),
status: InvokedSlashCommandStatus::Running(Task::ready(())),
transaction: None,
timestamp: id.0,
@@ -965,7 +961,6 @@ impl Context {
}
if !changed_messages.is_empty() {
self.message_roles_updated(changed_messages, cx);
cx.emit(ContextEvent::MessagesEdited);
cx.notify();
}
@@ -1389,8 +1384,6 @@ impl Context {
let mut removed_parsed_slash_command_ranges = Vec::new();
let mut updated_parsed_slash_commands = Vec::new();
let mut removed_patches = Vec::new();
let mut updated_patches = Vec::new();
while let Some(mut row_range) = row_ranges.next() {
while let Some(next_row_range) = row_ranges.peek() {
if row_range.end >= next_row_range.start {
@@ -1415,13 +1408,6 @@ impl Context {
cx,
);
self.invalidate_pending_slash_commands(&buffer, cx);
self.reparse_patches_in_range(
start..end,
&buffer,
&mut updated_patches,
&mut removed_patches,
cx,
);
}
if !updated_parsed_slash_commands.is_empty()
@@ -1432,13 +1418,6 @@ impl Context {
updated: updated_parsed_slash_commands,
});
}
if !updated_patches.is_empty() || !removed_patches.is_empty() {
cx.emit(ContextEvent::PatchesUpdated {
removed: removed_patches,
updated: updated_patches,
});
}
}
fn reparse_slash_commands_in_range(
@@ -1529,267 +1508,6 @@ impl Context {
}
}
fn reparse_patches_in_range(
&mut self,
range: Range<text::Anchor>,
buffer: &BufferSnapshot,
updated: &mut Vec<Range<text::Anchor>>,
removed: &mut Vec<Range<text::Anchor>>,
cx: &mut ModelContext<Self>,
) {
// Rebuild the XML tags in the edited range.
let intersecting_tags_range =
self.indices_intersecting_buffer_range(&self.xml_tags, range.clone(), cx);
let new_tags = self.parse_xml_tags_in_range(buffer, range.clone(), cx);
self.xml_tags
.splice(intersecting_tags_range.clone(), new_tags);
// Find which patches intersect the changed range.
let intersecting_patches_range =
self.indices_intersecting_buffer_range(&self.patches, range.clone(), cx);
// Reparse all tags after the last unchanged patch before the change.
let mut tags_start_ix = 0;
if let Some(preceding_unchanged_patch) =
self.patches[..intersecting_patches_range.start].last()
{
tags_start_ix = match self.xml_tags.binary_search_by(|tag| {
tag.range
.start
.cmp(&preceding_unchanged_patch.range.end, buffer)
.then(Ordering::Less)
}) {
Ok(ix) | Err(ix) => ix,
};
}
// Rebuild the patches in the range.
let new_patches = self.parse_patches(tags_start_ix, range.end, buffer, cx);
updated.extend(new_patches.iter().map(|patch| patch.range.clone()));
let removed_patches = self.patches.splice(intersecting_patches_range, new_patches);
removed.extend(
removed_patches
.map(|patch| patch.range)
.filter(|range| !updated.contains(&range)),
);
}
fn parse_xml_tags_in_range(
&self,
buffer: &BufferSnapshot,
range: Range<text::Anchor>,
cx: &AppContext,
) -> Vec<XmlTag> {
let mut messages = self.messages(cx).peekable();
let mut tags = Vec::new();
let mut lines = buffer.text_for_range(range).lines();
let mut offset = lines.offset();
while let Some(line) = lines.next() {
while let Some(message) = messages.peek() {
if offset < message.offset_range.end {
break;
} else {
messages.next();
}
}
let is_assistant_message = messages
.peek()
.map_or(false, |message| message.role == Role::Assistant);
if is_assistant_message {
for (start_ix, _) in line.match_indices('<') {
let mut name_start_ix = start_ix + 1;
let closing_bracket_ix = line[start_ix..].find('>').map(|i| start_ix + i);
if let Some(closing_bracket_ix) = closing_bracket_ix {
let end_ix = closing_bracket_ix + 1;
let mut is_open_tag = true;
if line[name_start_ix..closing_bracket_ix].starts_with('/') {
name_start_ix += 1;
is_open_tag = false;
}
let tag_inner = &line[name_start_ix..closing_bracket_ix];
let tag_name_len = tag_inner
.find(|c: char| c.is_whitespace())
.unwrap_or(tag_inner.len());
if let Ok(kind) = XmlTagKind::from_str(&tag_inner[..tag_name_len]) {
tags.push(XmlTag {
range: buffer.anchor_after(offset + start_ix)
..buffer.anchor_before(offset + end_ix),
is_open_tag,
kind,
});
};
}
}
}
offset = lines.offset();
}
tags
}
fn parse_patches(
&mut self,
tags_start_ix: usize,
buffer_end: text::Anchor,
buffer: &BufferSnapshot,
cx: &AppContext,
) -> Vec<AssistantPatch> {
let mut new_patches = Vec::new();
let mut pending_patch = None;
let mut patch_tag_depth = 0;
let mut tags = self.xml_tags[tags_start_ix..].iter().peekable();
'tags: while let Some(tag) = tags.next() {
if tag.range.start.cmp(&buffer_end, buffer).is_gt() && patch_tag_depth == 0 {
break;
}
if tag.kind == XmlTagKind::Patch && tag.is_open_tag {
patch_tag_depth += 1;
let patch_start = tag.range.start;
let mut edits = Vec::<Result<AssistantEdit>>::new();
let mut patch = AssistantPatch {
range: patch_start..patch_start,
title: String::new().into(),
edits: Default::default(),
status: crate::AssistantPatchStatus::Pending,
};
while let Some(tag) = tags.next() {
if tag.kind == XmlTagKind::Patch && !tag.is_open_tag {
patch_tag_depth -= 1;
if patch_tag_depth == 0 {
patch.range.end = tag.range.end;
// Include the line immediately after this <patch> tag if it's empty.
let patch_end_offset = patch.range.end.to_offset(buffer);
let mut patch_end_chars = buffer.chars_at(patch_end_offset);
if patch_end_chars.next() == Some('\n')
&& patch_end_chars.next().map_or(true, |ch| ch == '\n')
{
let messages = self.messages_for_offsets(
[patch_end_offset, patch_end_offset + 1],
cx,
);
if messages.len() == 1 {
patch.range.end = buffer.anchor_before(patch_end_offset + 1);
}
}
edits.sort_unstable_by(|a, b| {
if let (Ok(a), Ok(b)) = (a, b) {
a.path.cmp(&b.path)
} else {
Ordering::Equal
}
});
patch.edits = edits.into();
patch.status = AssistantPatchStatus::Ready;
new_patches.push(patch);
continue 'tags;
}
}
if tag.kind == XmlTagKind::Title && tag.is_open_tag {
let content_start = tag.range.end;
while let Some(tag) = tags.next() {
if tag.kind == XmlTagKind::Title && !tag.is_open_tag {
let content_end = tag.range.start;
patch.title =
trimmed_text_in_range(buffer, content_start..content_end)
.into();
break;
}
}
}
if tag.kind == XmlTagKind::Edit && tag.is_open_tag {
let mut path = None;
let mut old_text = None;
let mut new_text = None;
let mut operation = None;
let mut description = None;
while let Some(tag) = tags.next() {
if tag.kind == XmlTagKind::Edit && !tag.is_open_tag {
edits.push(AssistantEdit::new(
path,
operation,
old_text,
new_text,
description,
));
break;
}
if tag.is_open_tag
&& [
XmlTagKind::Path,
XmlTagKind::OldText,
XmlTagKind::NewText,
XmlTagKind::Operation,
XmlTagKind::Description,
]
.contains(&tag.kind)
{
let kind = tag.kind;
let content_start = tag.range.end;
if let Some(tag) = tags.peek() {
if tag.kind == kind && !tag.is_open_tag {
let tag = tags.next().unwrap();
let content_end = tag.range.start;
let content = trimmed_text_in_range(
buffer,
content_start..content_end,
);
match kind {
XmlTagKind::Path => path = Some(content),
XmlTagKind::Operation => operation = Some(content),
XmlTagKind::OldText => {
old_text = Some(content).filter(|s| !s.is_empty())
}
XmlTagKind::NewText => {
new_text = Some(content).filter(|s| !s.is_empty())
}
XmlTagKind::Description => {
description =
Some(content).filter(|s| !s.is_empty())
}
_ => {}
}
}
}
}
}
}
}
patch.edits = edits.into();
pending_patch = Some(patch);
}
}
if let Some(mut pending_patch) = pending_patch {
let patch_start = pending_patch.range.start.to_offset(buffer);
if let Some(message) = self.message_for_offset(patch_start, cx) {
if message.anchor_range.end == text::Anchor::MAX {
pending_patch.range.end = text::Anchor::MAX;
} else {
let message_end = buffer.anchor_after(message.offset_range.end - 1);
pending_patch.range.end = message_end;
}
} else {
pending_patch.range.end = text::Anchor::MAX;
}
new_patches.push(pending_patch);
}
new_patches
}
pub fn pending_command_for_position(
&mut self,
position: language::Anchor,
@@ -1914,7 +1632,6 @@ impl Context {
}
let mut pending_section_stack: Vec<PendingSection> = Vec::new();
let mut run_commands_in_ranges: Vec<Range<language::Anchor>> = Vec::new();
let mut last_role: Option<Role> = None;
let mut last_section_range = None;
@@ -1980,7 +1697,13 @@ impl Context {
let end = this.buffer.read(cx).anchor_before(insert_position);
if run_commands_in_text {
run_commands_in_ranges.push(start..end);
if let Some(invoked_slash_command) =
this.invoked_slash_commands.get_mut(&command_id)
{
invoked_slash_command
.run_commands_in_ranges
.push(start..end);
}
}
}
SlashCommandEvent::EndSection => {
@@ -2100,6 +1823,7 @@ impl Context {
InvokedSlashCommand {
name: name.to_string().into(),
range: command_range.clone(),
run_commands_in_ranges: Vec::new(),
status: InvokedSlashCommandStatus::Running(insert_output_task),
transaction: Some(first_transaction),
timestamp: command_id.0,
@@ -2383,7 +2107,11 @@ impl Context {
});
Some(error.to_string())
} else {
let error_message = error.to_string().trim().to_string();
let error_message = error
.chain()
.map(|err| err.to_string())
.collect::<Vec<_>>()
.join("\n");
cx.emit(ContextEvent::ShowAssistError(SharedString::from(
error_message.clone(),
)));
@@ -2464,9 +2192,22 @@ impl Context {
}
}
let tools = if let RequestType::SuggestEdits = request_type {
vec![{
let tool = CodeEditsTool;
LanguageModelRequestTool {
name: tool.name(),
description: tool.description(),
input_schema: tool.input_schema(),
}
}]
} else {
Vec::new()
};
let mut completion_request = LanguageModelRequest {
messages: Vec::new(),
tools: Vec::new(),
tools,
stop: Vec::new(),
temperature: None,
};
@@ -2539,25 +2280,6 @@ impl Context {
completion_request.messages.push(request_message);
}
if let RequestType::SuggestEdits = request_type {
if let Ok(preamble) = self.prompt_builder.generate_suggest_edits_prompt() {
let last_elem_index = completion_request.messages.len();
completion_request
.messages
.push(LanguageModelRequestMessage {
role: Role::User,
content: vec![MessageContent::Text(preamble)],
cache: false,
});
// The preamble message should be sent right before the last actual user message.
completion_request
.messages
.swap(last_elem_index, last_elem_index.saturating_sub(1));
}
}
completion_request
}
@@ -2581,28 +2303,6 @@ impl Context {
self.update_metadata(*id, cx, |metadata| metadata.role = role);
}
}
self.message_roles_updated(ids, cx);
}
fn message_roles_updated(&mut self, ids: HashSet<MessageId>, cx: &mut ModelContext<Self>) {
let mut ranges = Vec::new();
for message in self.messages(cx) {
if ids.contains(&message.id) {
ranges.push(message.anchor_range.clone());
}
}
let buffer = self.buffer.read(cx).text_snapshot();
let mut updated = Vec::new();
let mut removed = Vec::new();
for range in ranges {
self.reparse_patches_in_range(range, &buffer, &mut updated, &mut removed, cx);
}
if !updated.is_empty() || !removed.is_empty() {
cx.emit(ContextEvent::PatchesUpdated { removed, updated })
}
}
pub fn update_metadata(
@@ -2887,7 +2587,7 @@ impl Context {
request.messages.push(LanguageModelRequestMessage {
role: Role::User,
content: vec![
"Generate a concise 3-7 word title for this conversation, omitting punctuation"
"Generate a concise 3-7 word title for this conversation, omitting punctuation. Go straight to the title, without any preamble and prefix like `Here's a concise suggestion:...` or `Title:`"
.into(),
],
cache: false,
@@ -3172,6 +2872,7 @@ pub struct ParsedSlashCommand {
pub struct InvokedSlashCommand {
pub name: SharedString,
pub range: Range<language::Anchor>,
pub run_commands_in_ranges: Vec<Range<language::Anchor>>,
pub status: InvokedSlashCommandStatus,
pub transaction: Option<language::TransactionId>,
timestamp: clock::Lamport,

View File

@@ -1,11 +1,11 @@
use super::{AssistantEdit, MessageCacheMetadata};
use super::MessageCacheMetadata;
use crate::slash_command_working_set::SlashCommandWorkingSet;
use crate::ToolWorkingSet;
use crate::{
assistant_panel, prompt_library, slash_command::file_command, AssistantEditKind, CacheStatus,
Context, ContextEvent, ContextId, ContextOperation, InvokedSlashCommandId, MessageId,
MessageStatus, PromptBuilder,
};
use crate::{AssistantEdit, ToolWorkingSet};
use anyhow::Result;
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent, SlashCommandOutput,

View File

@@ -310,10 +310,6 @@ impl PromptBuilder {
.render("terminal_assistant_prompt", &context)
}
pub fn generate_suggest_edits_prompt(&self) -> Result<String, RenderError> {
self.handlebars.lock().render("suggest_edits", &())
}
pub fn generate_project_slash_command_prompt(
&self,
context_buffer: String,

View File

@@ -152,7 +152,7 @@ impl SlashCommand for ContextServerSlashCommand {
if result
.messages
.iter()
.any(|msg| !matches!(msg.role, context_servers::types::SamplingRole::User))
.any(|msg| !matches!(msg.role, context_servers::types::Role::User))
{
return Err(anyhow!(
"Prompt contains non-user roles, which is not supported"
@@ -164,7 +164,7 @@ impl SlashCommand for ContextServerSlashCommand {
.messages
.into_iter()
.filter_map(|msg| match msg.content {
context_servers::types::SamplingContent::Text { text } => Some(text),
context_servers::types::MessageContent::Text { text } => Some(text),
_ => None,
})
.collect::<Vec<String>>()

View File

@@ -69,6 +69,10 @@ impl SlashCommand for DefaultSlashCommand {
text.push('\n');
}
if !text.ends_with('\n') {
text.push('\n');
}
Ok(SlashCommandOutput {
sections: vec![SlashCommandOutputSection {
range: 0..text.len(),

View File

@@ -1,2 +1,3 @@
pub mod code_edits_tool;
pub mod context_server_tool;
pub mod now_tool;

View File

@@ -0,0 +1,86 @@
use std::sync::Arc;
use anyhow::{anyhow, Result};
use assistant_tool::Tool;
use gpui::{Task, WeakView, WindowContext};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CodeEditsToolInput {
/// A high-level description of the code changes. This should be as short as possible, possibly using common abbreviations.
pub title: String,
/// An array of edits to be applied.
pub edits: Vec<Edit>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct Edit {
/// The path to the file that this edit will change.
pub path: String,
/// An arbitrarily-long comment that describes the purpose of this edit.
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// An excerpt from the file's current contents that uniquely identifies a range within the file where the edit should occur.
#[serde(skip_serializing_if = "Option::is_none")]
pub old_text: Option<String>,
/// The new text to insert into the file.
pub new_text: String,
/// The type of change that should occur at the given range of the file.
pub operation: Operation,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Operation {
/// Replaces the entire range with the new text.
Update,
/// Inserts the new text before the range.
InsertBefore,
/// Inserts new text after the range.
InsertAfter,
/// Creates a new file with the given path and the new text.
Create,
/// Deletes the specified range from the file.
Delete,
}
pub struct CodeEditsTool;
impl CodeEditsTool {
pub const TOOL_NAME: &str = "zed_code_edits";
}
impl Tool for CodeEditsTool {
fn name(&self) -> String {
Self::TOOL_NAME.to_string()
}
fn description(&self) -> String {
// Anthropic's best practices for tool descriptions:
// https://docs.anthropic.com/en/docs/build-with-claude/tool-use#best-practices-for-tool-definitions
include_str!("edit_tool_description.txt").to_string()
}
fn input_schema(&self) -> serde_json::Value {
let schema = schemars::schema_for!(CodeEditsToolInput);
serde_json::to_value(&schema).unwrap()
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_workspace: WeakView<workspace::Workspace>,
_cx: &mut WindowContext,
) -> Task<Result<String>> {
let input: CodeEditsToolInput = match serde_json::from_value(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))),
};
let text = format!("The tool returned {:?}.", input);
Task::ready(Ok(text))
}
}

View File

@@ -74,11 +74,21 @@ impl Tool for ContextServerTool {
);
let response = protocol.run_tool(tool_name, arguments).await?;
let tool_result = match response.tool_result {
serde_json::Value::String(s) => s,
_ => serde_json::to_string(&response.tool_result)?,
};
Ok(tool_result)
let mut result = String::new();
for content in response.content {
match content {
types::ToolResponseContent::Text { text } => {
result.push_str(&text);
}
types::ToolResponseContent::Image { .. } => {
log::warn!("Ignoring image content from tool response");
}
types::ToolResponseContent::Resource { .. } => {
log::warn!("Ignoring resource content from tool response");
}
}
}
Ok(result)
}
})
} else {

View File

@@ -0,0 +1,15 @@
Describes the specific code changes that should be made to the files in a code base, based on the request the user made.
It should be used when the user requests making changes to the code base, but not when the user is asking an question about information
(including when asking for information about the code base) rather than requesting a change.
The tool will return an array of patches, each of which represents some related modifications to the code base.
Each patch contains a high-level summary of the changes (which will be displayed in the code editor),
as well as an array of specific edits to be made to specific individual files. The code editor will apply each of those edits to the code base, or not, at the discretion of the user of the editor.
Within each patch, the tool will never return multiple edits whose ranges intersect each other. Instead, it will merge them into one edit.
On the other hand, for ranges that do not intersect each other, the tool will prefer multiple edits to smaller ranges over one edit to a larger range.
Whenever edits reference symbols that would be out of scope, the tool will always include earlier edits which add any necessary imports to bring those symbols into scope.
The overall goal is that if the user of the code editor accepts all edits within all patches, the code will end up in a correct state, and
will successfully build and run without any further modifications from the user. It will also have correctly effected the changes to the code base that the user originally requested.

View File

@@ -343,7 +343,7 @@ fn init_test(cx: &mut AppContext) -> Model<ChannelStore> {
release_channel::init(SemanticVersion::default(), cx);
client::init_settings(cx);
let clock = Arc::new(FakeSystemClock::default());
let clock = Arc::new(FakeSystemClock::new());
let http = FakeHttpClient::with_404_response();
let client = Client::new(clock, http.clone(), cx);
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));

View File

@@ -42,7 +42,6 @@ serde_json.workspace = true
settings.workspace = true
sha2.workspace = true
smol.workspace = true
sysinfo.workspace = true
telemetry_events.workspace = true
text.workspace = true
thiserror.workspace = true

View File

@@ -1780,7 +1780,7 @@ mod tests {
let user_id = 5;
let client = cx.update(|cx| {
Client::new(
Arc::new(FakeSystemClock::default()),
Arc::new(FakeSystemClock::new()),
FakeHttpClient::with_404_response(),
cx,
)
@@ -1821,7 +1821,7 @@ mod tests {
let user_id = 5;
let client = cx.update(|cx| {
Client::new(
Arc::new(FakeSystemClock::default()),
Arc::new(FakeSystemClock::new()),
FakeHttpClient::with_404_response(),
cx,
)
@@ -1900,7 +1900,7 @@ mod tests {
let dropped_auth_count = Arc::new(Mutex::new(0));
let client = cx.update(|cx| {
Client::new(
Arc::new(FakeSystemClock::default()),
Arc::new(FakeSystemClock::new()),
FakeHttpClient::with_404_response(),
cx,
)
@@ -1943,7 +1943,7 @@ mod tests {
let user_id = 5;
let client = cx.update(|cx| {
Client::new(
Arc::new(FakeSystemClock::default()),
Arc::new(FakeSystemClock::new()),
FakeHttpClient::with_404_response(),
cx,
)
@@ -2003,7 +2003,7 @@ mod tests {
let user_id = 5;
let client = cx.update(|cx| {
Client::new(
Arc::new(FakeSystemClock::default()),
Arc::new(FakeSystemClock::new()),
FakeHttpClient::with_404_response(),
cx,
)
@@ -2038,7 +2038,7 @@ mod tests {
let user_id = 5;
let client = cx.update(|cx| {
Client::new(
Arc::new(FakeSystemClock::default()),
Arc::new(FakeSystemClock::new()),
FakeHttpClient::with_404_response(),
cx,
)

View File

@@ -2,7 +2,6 @@ mod event_coalescer;
use crate::{ChannelId, TelemetrySettings};
use anyhow::Result;
use chrono::{DateTime, Utc};
use clock::SystemClock;
use collections::{HashMap, HashSet};
use futures::Future;
@@ -15,12 +14,11 @@ use settings::{Settings, SettingsStore};
use sha2::{Digest, Sha256};
use std::fs::File;
use std::io::Write;
use std::time::Instant;
use std::{env, mem, path::PathBuf, sync::Arc, time::Duration};
use sysinfo::{CpuRefreshKind, Pid, ProcessRefreshKind, RefreshKind, System};
use telemetry_events::{
ActionEvent, AppEvent, AssistantEvent, CallEvent, CpuEvent, EditEvent, EditorEvent, Event,
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, MemoryEvent, ReplEvent,
SettingEvent,
ActionEvent, AppEvent, AssistantEvent, CallEvent, EditEvent, EditorEvent, Event,
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, ReplEvent, SettingEvent,
};
use util::{ResultExt, TryFutureExt};
use worktree::{UpdatedEntriesSet, WorktreeId};
@@ -46,7 +44,7 @@ struct TelemetryState {
flush_events_task: Option<Task<()>>,
log_file: Option<File>,
is_staff: Option<bool>,
first_event_date_time: Option<DateTime<Utc>>,
first_event_date_time: Option<Instant>,
event_coalescer: EventCoalescer,
max_queue_size: usize,
worktree_id_map: WorktreeIdMap,
@@ -293,55 +291,13 @@ impl Telemetry {
state.session_id = Some(session_id);
state.app_version = release_channel::AppVersion::global(cx).to_string();
state.os_name = os_name();
drop(state);
let this = self.clone();
cx.background_executor()
.spawn(async move {
let mut system = System::new_with_specifics(
RefreshKind::new().with_cpu(CpuRefreshKind::everything()),
);
let refresh_kind = ProcessRefreshKind::new().with_cpu().with_memory();
let current_process = Pid::from_u32(std::process::id());
system.refresh_processes_specifics(
sysinfo::ProcessesToUpdate::Some(&[current_process]),
refresh_kind,
);
// Waiting some amount of time before the first query is important to get a reasonable value
// https://docs.rs/sysinfo/0.29.10/sysinfo/trait.ProcessExt.html#tymethod.cpu_usage
const DURATION_BETWEEN_SYSTEM_EVENTS: Duration = Duration::from_secs(4 * 60);
loop {
smol::Timer::after(DURATION_BETWEEN_SYSTEM_EVENTS).await;
let current_process = Pid::from_u32(std::process::id());
system.refresh_processes_specifics(
sysinfo::ProcessesToUpdate::Some(&[current_process]),
refresh_kind,
);
let Some(process) = system.process(current_process) else {
log::error!(
"Failed to find own process {current_process:?} in system process table"
);
// TODO: Fire an error telemetry event
return;
};
this.report_memory_event(process.memory(), process.virtual_memory());
this.report_cpu_event(process.cpu_usage(), system.cpus().len() as u32);
}
})
.detach();
}
pub fn metrics_enabled(self: &Arc<Self>) -> bool {
let state = self.state.lock();
let enabled = state.settings.metrics;
drop(state);
return enabled;
enabled
}
pub fn set_authenticated_user_info(
@@ -416,28 +372,6 @@ impl Telemetry {
self.report_event(event)
}
pub fn report_cpu_event(self: &Arc<Self>, usage_as_percentage: f32, core_count: u32) {
let event = Event::Cpu(CpuEvent {
usage_as_percentage,
core_count,
});
self.report_event(event)
}
pub fn report_memory_event(
self: &Arc<Self>,
memory_in_bytes: u64,
virtual_memory_in_bytes: u64,
) {
let event = Event::Memory(MemoryEvent {
memory_in_bytes,
virtual_memory_in_bytes,
});
self.report_event(event)
}
pub fn report_app_event(self: &Arc<Self>, operation: String) -> Event {
let event = Event::App(AppEvent { operation });
@@ -469,7 +403,10 @@ impl Telemetry {
if let Some((start, end, environment)) = period_data {
let event = Event::Edit(EditEvent {
duration: end.timestamp_millis() - start.timestamp_millis(),
duration: end
.saturating_duration_since(start)
.min(Duration::from_secs(60 * 60 * 24))
.as_millis() as i64,
environment: environment.to_string(),
is_via_ssh,
});
@@ -567,9 +504,10 @@ impl Telemetry {
let date_time = self.clock.utc_now();
let milliseconds_since_first_event = match state.first_event_date_time {
Some(first_event_date_time) => {
date_time.timestamp_millis() - first_event_date_time.timestamp_millis()
}
Some(first_event_date_time) => date_time
.saturating_duration_since(first_event_date_time)
.min(Duration::from_secs(60 * 60 * 24))
.as_millis() as i64,
None => {
state.first_event_date_time = Some(date_time);
0
@@ -702,7 +640,6 @@ pub fn calculate_json_checksum(json: &impl AsRef<[u8]>) -> Option<String> {
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
use clock::FakeSystemClock;
use gpui::TestAppContext;
use http_client::FakeHttpClient;
@@ -710,9 +647,7 @@ mod tests {
#[gpui::test]
fn test_telemetry_flush_on_max_queue_size(cx: &mut TestAppContext) {
init_test(cx);
let clock = Arc::new(FakeSystemClock::new(
Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap(),
));
let clock = Arc::new(FakeSystemClock::new());
let http = FakeHttpClient::with_200_response();
let system_id = Some("system_id".to_string());
let installation_id = Some("installation_id".to_string());
@@ -743,7 +678,7 @@ mod tests {
Some(first_date_time)
);
clock.advance(chrono::Duration::milliseconds(100));
clock.advance(Duration::from_millis(100));
let event = telemetry.report_app_event(operation.clone());
assert_eq!(
@@ -759,7 +694,7 @@ mod tests {
Some(first_date_time)
);
clock.advance(chrono::Duration::milliseconds(100));
clock.advance(Duration::from_millis(100));
let event = telemetry.report_app_event(operation.clone());
assert_eq!(
@@ -775,7 +710,7 @@ mod tests {
Some(first_date_time)
);
clock.advance(chrono::Duration::milliseconds(100));
clock.advance(Duration::from_millis(100));
// Adding a 4th event should cause a flush
let event = telemetry.report_app_event(operation.clone());
@@ -796,9 +731,7 @@ mod tests {
cx: &mut TestAppContext,
) {
init_test(cx);
let clock = Arc::new(FakeSystemClock::new(
Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap(),
));
let clock = Arc::new(FakeSystemClock::new());
let http = FakeHttpClient::with_200_response();
let system_id = Some("system_id".to_string());
let installation_id = Some("installation_id".to_string());

View File

@@ -1,7 +1,6 @@
use std::sync::Arc;
use std::time;
use std::{sync::Arc, time::Instant};
use chrono::{DateTime, Duration, Utc};
use clock::SystemClock;
const COALESCE_TIMEOUT: time::Duration = time::Duration::from_secs(20);
@@ -10,8 +9,8 @@ const SIMULATED_DURATION_FOR_SINGLE_EVENT: time::Duration = time::Duration::from
#[derive(Debug, PartialEq)]
struct PeriodData {
environment: &'static str,
start: DateTime<Utc>,
end: Option<DateTime<Utc>>,
start: Instant,
end: Option<Instant>,
}
pub struct EventCoalescer {
@@ -27,9 +26,8 @@ impl EventCoalescer {
pub fn log_event(
&mut self,
environment: &'static str,
) -> Option<(DateTime<Utc>, DateTime<Utc>, &'static str)> {
) -> Option<(Instant, Instant, &'static str)> {
let log_time = self.clock.utc_now();
let coalesce_timeout = Duration::from_std(COALESCE_TIMEOUT).unwrap();
let Some(state) = &mut self.state else {
self.state = Some(PeriodData {
@@ -43,7 +41,7 @@ impl EventCoalescer {
let period_end = state
.end
.unwrap_or(state.start + SIMULATED_DURATION_FOR_SINGLE_EVENT);
let within_timeout = log_time - period_end < coalesce_timeout;
let within_timeout = log_time - period_end < COALESCE_TIMEOUT;
let environment_is_same = state.environment == environment;
let should_coaelesce = !within_timeout || !environment_is_same;
@@ -70,16 +68,13 @@ impl EventCoalescer {
#[cfg(test)]
mod tests {
use chrono::TimeZone;
use clock::FakeSystemClock;
use super::*;
#[test]
fn test_same_context_exceeding_timeout() {
let clock = Arc::new(FakeSystemClock::new(
Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(),
));
let clock = Arc::new(FakeSystemClock::new());
let environment_1 = "environment_1";
let mut event_coalescer = EventCoalescer::new(clock.clone());
@@ -98,7 +93,7 @@ mod tests {
})
);
let within_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT / 2).unwrap();
let within_timeout_adjustment = COALESCE_TIMEOUT / 2;
// Ensure that many calls within the timeout don't start a new period
for _ in 0..100 {
@@ -118,7 +113,7 @@ mod tests {
}
let period_end = clock.utc_now();
let exceed_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT * 2).unwrap();
let exceed_timeout_adjustment = COALESCE_TIMEOUT * 2;
// Logging an event exceeding the timeout should start a new period
clock.advance(exceed_timeout_adjustment);
let new_period_start = clock.utc_now();
@@ -137,9 +132,7 @@ mod tests {
#[test]
fn test_different_environment_under_timeout() {
let clock = Arc::new(FakeSystemClock::new(
Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(),
));
let clock = Arc::new(FakeSystemClock::new());
let environment_1 = "environment_1";
let mut event_coalescer = EventCoalescer::new(clock.clone());
@@ -158,7 +151,7 @@ mod tests {
})
);
let within_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT / 2).unwrap();
let within_timeout_adjustment = COALESCE_TIMEOUT / 2;
clock.advance(within_timeout_adjustment);
let period_end = clock.utc_now();
let period_data = event_coalescer.log_event(environment_1);
@@ -193,9 +186,7 @@ mod tests {
#[test]
fn test_switching_environment_while_within_timeout() {
let clock = Arc::new(FakeSystemClock::new(
Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(),
));
let clock = Arc::new(FakeSystemClock::new());
let environment_1 = "environment_1";
let mut event_coalescer = EventCoalescer::new(clock.clone());
@@ -214,7 +205,7 @@ mod tests {
})
);
let within_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT / 2).unwrap();
let within_timeout_adjustment = COALESCE_TIMEOUT / 2;
clock.advance(within_timeout_adjustment);
let period_end = clock.utc_now();
let environment_2 = "environment_2";
@@ -240,9 +231,7 @@ mod tests {
#[test]
fn test_switching_environment_while_exceeding_timeout() {
let clock = Arc::new(FakeSystemClock::new(
Utc.with_ymd_and_hms(1990, 4, 12, 0, 0, 0).unwrap(),
));
let clock = Arc::new(FakeSystemClock::new());
let environment_1 = "environment_1";
let mut event_coalescer = EventCoalescer::new(clock.clone());
@@ -261,7 +250,7 @@ mod tests {
})
);
let exceed_timeout_adjustment = Duration::from_std(COALESCE_TIMEOUT * 2).unwrap();
let exceed_timeout_adjustment = COALESCE_TIMEOUT * 2;
clock.advance(exceed_timeout_adjustment);
let period_end = clock.utc_now();
let environment_2 = "environment_2";

View File

@@ -16,7 +16,6 @@ doctest = false
test-support = ["dep:parking_lot"]
[dependencies]
chrono.workspace = true
parking_lot = { workspace = true, optional = true }
serde.workspace = true
smallvec.workspace = true

View File

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

View File

@@ -24,6 +24,7 @@ async-stripe.workspace = true
async-tungstenite.workspace = true
aws-config = { version = "1.1.5" }
aws-sdk-s3 = { version = "1.15.0" }
aws-sdk-kinesis = "1.51.0"
axum = { version = "0.6", features = ["json", "headers", "ws"] }
axum-extra = { version = "0.4", features = ["erased-json"] }
base64.workspace = true

View File

@@ -174,6 +174,31 @@ spec:
secretKeyRef:
name: blob-store
key: bucket
- name: KINESIS_ACCESS_KEY
valueFrom:
secretKeyRef:
name: kinesis
key: access_key
- name: KINESIS_SECRET_KEY
valueFrom:
secretKeyRef:
name: kinesis
key: secret_key
- name: KINESIS_STREAM
valueFrom:
secretKeyRef:
name: kinesis
key: stream
- name: KINESIS_REGION
valueFrom:
secretKeyRef:
name: kinesis
key: region
- name: BLOB_STORE_BUCKET
valueFrom:
secretKeyRef:
name: blob-store
key: bucket
- name: CLICKHOUSE_URL
valueFrom:
secretKeyRef:

View File

@@ -11,9 +11,11 @@ use axum::{
routing::post,
Extension, Router, TypedHeader,
};
use chrono::Duration;
use rpc::ExtensionMetadata;
use semantic_version::SemanticVersion;
use serde::{Serialize, Serializer};
use serde::{Deserialize, Serialize, Serializer};
use serde_json::json;
use sha2::{Digest, Sha256};
use std::sync::{Arc, OnceLock};
use telemetry_events::{
@@ -21,6 +23,7 @@ use telemetry_events::{
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, MemoryEvent, Panic,
ReplEvent, SettingEvent,
};
use util::ResultExt;
use uuid::Uuid;
const CRASH_REPORTS_BUCKET: &str = "zed-crash-reports";
@@ -388,13 +391,6 @@ pub async fn post_events(
country_code_header: Option<TypedHeader<CloudflareIpCountryHeader>>,
body: Bytes,
) -> Result<()> {
let Some(clickhouse_client) = app.clickhouse_client.clone() else {
Err(Error::http(
StatusCode::NOT_IMPLEMENTED,
"not supported".into(),
))?
};
let Some(expected) = calculate_json_checksum(app.clone(), &body) else {
return Err(Error::http(
StatusCode::INTERNAL_SERVER_ERROR,
@@ -416,6 +412,34 @@ pub async fn post_events(
};
let country_code = country_code_header.map(|h| h.to_string());
let first_event_at = chrono::Utc::now()
- chrono::Duration::milliseconds(last_event.milliseconds_since_first_event);
if let Some(kinesis_client) = app.kinesis_client.clone() {
if let Some(stream) = app.config.kinesis_stream.clone() {
let mut request = kinesis_client.put_records().stream_name(stream);
for row in for_snowflake(request_body.clone(), first_event_at, country_code.clone()) {
if let Some(data) = serde_json::to_vec(&row).log_err() {
request = request.records(
aws_sdk_kinesis::types::PutRecordsRequestEntry::builder()
.partition_key(request_body.system_id.clone().unwrap_or_default())
.data(data.into())
.build()
.unwrap(),
);
}
}
request.send().await.log_err();
}
};
let Some(clickhouse_client) = app.clickhouse_client.clone() else {
Err(Error::http(
StatusCode::NOT_IMPLEMENTED,
"not supported".into(),
))?
};
let first_event_at = chrono::Utc::now()
- chrono::Duration::milliseconds(last_event.milliseconds_since_first_event);
@@ -459,20 +483,7 @@ pub async fn post_events(
checksum_matched,
))
}
Event::Cpu(event) => to_upload.cpu_events.push(CpuEventRow::from_event(
event.clone(),
wrapper,
&request_body,
first_event_at,
checksum_matched,
)),
Event::Memory(event) => to_upload.memory_events.push(MemoryEventRow::from_event(
event.clone(),
wrapper,
&request_body,
first_event_at,
checksum_matched,
)),
Event::Cpu(_) | Event::Memory(_) => continue,
Event::App(event) => to_upload.app_events.push(AppEventRow::from_event(
event.clone(),
wrapper,
@@ -923,6 +934,7 @@ pub struct CpuEventRow {
}
impl CpuEventRow {
#[allow(unused)]
fn from_event(
event: CpuEvent,
wrapper: &EventWrapper,
@@ -977,6 +989,7 @@ pub struct MemoryEventRow {
}
impl MemoryEventRow {
#[allow(unused)]
fn from_event(
event: MemoryEvent,
wrapper: &EventWrapper,
@@ -1364,3 +1377,259 @@ pub fn calculate_json_checksum(app: Arc<AppState>, json: &impl AsRef<[u8]>) -> O
summer.update(checksum_seed);
Some(summer.finalize().into_iter().collect())
}
fn for_snowflake(
body: EventRequestBody,
first_event_at: chrono::DateTime<chrono::Utc>,
country_code: Option<String>,
) -> impl Iterator<Item = SnowflakeRow> {
body.events.into_iter().flat_map(move |event| {
let timestamp =
first_event_at + Duration::milliseconds(event.milliseconds_since_first_event);
let (event_type, mut event_properties) = match &event.event {
Event::Editor(e) => (
match e.operation.as_str() {
"open" => "Editor Opened".to_string(),
"save" => "Editor Saved".to_string(),
_ => format!("Unknown Editor Event: {}", e.operation),
},
serde_json::to_value(e).unwrap(),
),
Event::InlineCompletion(e) => (
format!(
"Inline Completion {}",
if e.suggestion_accepted {
"Accepted"
} else {
"Discarded"
}
),
serde_json::to_value(e).unwrap(),
),
Event::Call(e) => {
let event_type = match e.operation.trim() {
"unshare project" => "Project Unshared".to_string(),
"open channel notes" => "Channel Notes Opened".to_string(),
"share project" => "Project Shared".to_string(),
"join channel" => "Channel Joined".to_string(),
"hang up" => "Call Ended".to_string(),
"accept incoming" => "Incoming Call Accepted".to_string(),
"invite" => "Participant Invited".to_string(),
"disable microphone" => "Microphone Disabled".to_string(),
"enable microphone" => "Microphone Enabled".to_string(),
"enable screen share" => "Screen Share Enabled".to_string(),
"disable screen share" => "Screen Share Disabled".to_string(),
"decline incoming" => "Incoming Call Declined".to_string(),
"enable camera" => "Camera Enabled".to_string(),
"disable camera" => "Camera Disabled".to_string(),
_ => format!("Unknown Call Event: {}", e.operation),
};
(event_type, serde_json::to_value(e).unwrap())
}
Event::Assistant(e) => (
match e.phase {
telemetry_events::AssistantPhase::Response => "Assistant Responded".to_string(),
telemetry_events::AssistantPhase::Invoked => "Assistant Invoked".to_string(),
telemetry_events::AssistantPhase::Accepted => {
"Assistant Response Accepted".to_string()
}
telemetry_events::AssistantPhase::Rejected => {
"Assistant Response Rejected".to_string()
}
},
serde_json::to_value(e).unwrap(),
),
Event::Cpu(_) | Event::Memory(_) => return None,
Event::App(e) => {
let mut properties = json!({});
let event_type = match e.operation.trim() {
"extensions: install extension" => "Extension Installed".to_string(),
"open" => "App Opened".to_string(),
"project search: open" => "Project Search Opened".to_string(),
"first open" => {
properties["is_first_open"] = json!(true);
"App First Opened".to_string()
}
"extensions: uninstall extension" => "Extension Uninstalled".to_string(),
"welcome page: close" => "Welcome Page Closed".to_string(),
"open project" => {
properties["is_first_time"] = json!(false);
"Project Opened".to_string()
}
"welcome page: install cli" => "CLI Installed".to_string(),
"project diagnostics: open" => "Project Diagnostics Opened".to_string(),
"extensions page: open" => "Extensions Page Opened".to_string(),
"welcome page: change theme" => "Welcome Theme Changed".to_string(),
"welcome page: toggle metric telemetry" => {
properties["enabled"] = json!(false);
"Welcome Telemetry Toggled".to_string()
}
"welcome page: change keymap" => "Keymap Changed".to_string(),
"welcome page: toggle vim" => {
properties["enabled"] = json!(false);
"Welcome Vim Mode Toggled".to_string()
}
"welcome page: sign in to copilot" => "Welcome Copilot Signed In".to_string(),
"welcome page: toggle diagnostic telemetry" => {
"Welcome Telemetry Toggled".to_string()
}
"welcome page: open" => "Welcome Page Opened".to_string(),
"close" => "App Closed".to_string(),
"markdown preview: open" => "Markdown Preview Opened".to_string(),
"welcome page: open extensions" => "Extensions Page Opened".to_string(),
"open node project" | "open pnpm project" | "open yarn project" => {
properties["project_type"] = json!("node");
properties["is_first_time"] = json!(false);
"Project Opened".to_string()
}
"repl sessions: open" => "REPL Session Started".to_string(),
"welcome page: toggle helix" => {
properties["enabled"] = json!(false);
"Helix Mode Toggled".to_string()
}
"welcome page: edit settings" => {
properties["changed_settings"] = json!([]);
"Settings Edited".to_string()
}
"welcome page: view docs" => "Documentation Viewed".to_string(),
"open ssh project" => {
properties["is_first_time"] = json!(false);
"SSH Project Opened".to_string()
}
"create ssh server" => "SSH Server Created".to_string(),
"create ssh project" => "SSH Project Created".to_string(),
"first open for release channel" => {
properties["is_first_for_channel"] = json!(true);
"App First Opened For Release Channel".to_string()
}
"feature upsell: toggle vim" => {
properties["source"] = json!("Feature Upsell");
"Vim Mode Toggled".to_string()
}
_ => e
.operation
.strip_prefix("feature upsell: viewed docs (")
.and_then(|s| s.strip_suffix(')'))
.map_or_else(
|| format!("Unknown App Event: {}", e.operation),
|docs_url| {
properties["url"] = json!(docs_url);
properties["source"] = json!("Feature Upsell");
"Documentation Viewed".to_string()
},
),
};
(event_type, properties)
}
Event::Setting(e) => (
"Settings Changed".to_string(),
serde_json::to_value(e).unwrap(),
),
Event::Extension(e) => (
"Extension Loaded".to_string(),
serde_json::to_value(e).unwrap(),
),
Event::Edit(e) => (
"Editor Edited".to_string(),
serde_json::to_value(e).unwrap(),
),
Event::Action(e) => (
"Action Invoked".to_string(),
serde_json::to_value(e).unwrap(),
),
Event::Repl(e) => (
"Kernel Status Changed".to_string(),
serde_json::to_value(e).unwrap(),
),
};
if let serde_json::Value::Object(ref mut map) = event_properties {
map.insert("app_version".to_string(), body.app_version.clone().into());
map.insert("os_name".to_string(), body.os_name.clone().into());
map.insert("os_version".to_string(), body.os_version.clone().into());
map.insert("architecture".to_string(), body.architecture.clone().into());
map.insert(
"release_channel".to_string(),
body.release_channel.clone().into(),
);
map.insert("signed_in".to_string(), event.signed_in.into());
if let Some(country_code) = country_code.as_ref() {
map.insert("country_code".to_string(), country_code.clone().into());
}
}
let user_properties = Some(serde_json::json!({
"is_staff": body.is_staff,
"Country": country_code.clone(),
"OS": format!("{} {}", body.os_name, body.os_version.clone().unwrap_or_default()),
"Version": body.app_version.clone(),
}));
Some(SnowflakeRow {
time: timestamp,
user_id: body.metrics_id.clone(),
device_id: body.system_id.clone(),
event_type,
event_properties,
user_properties,
insert_id: Some(Uuid::new_v4().to_string()),
})
})
}
#[derive(Serialize, Deserialize)]
struct SnowflakeRow {
pub time: chrono::DateTime<chrono::Utc>,
pub user_id: Option<String>,
pub device_id: Option<String>,
pub event_type: String,
pub event_properties: serde_json::Value,
pub user_properties: Option<serde_json::Value>,
pub insert_id: Option<String>,
}
#[derive(Serialize, Deserialize)]
struct SnowflakeData {
/// Identifier unique to each Zed installation (differs for stable, preview, dev)
pub installation_id: Option<String>,
/// Identifier unique to each logged in Zed user (randomly generated on first sign in)
/// Identifier unique to each Zed session (differs for each time you open Zed)
pub session_id: Option<String>,
pub metrics_id: Option<String>,
/// True for Zed staff, otherwise false
pub is_staff: Option<bool>,
/// Zed version number
pub app_version: String,
pub os_name: String,
pub os_version: Option<String>,
pub architecture: String,
/// Zed release channel (stable, preview, dev)
pub release_channel: Option<String>,
pub signed_in: bool,
#[serde(flatten)]
pub editor_event: Option<EditorEvent>,
#[serde(flatten)]
pub inline_completion_event: Option<InlineCompletionEvent>,
#[serde(flatten)]
pub call_event: Option<CallEvent>,
#[serde(flatten)]
pub assistant_event: Option<AssistantEvent>,
#[serde(flatten)]
pub cpu_event: Option<CpuEvent>,
#[serde(flatten)]
pub memory_event: Option<MemoryEvent>,
#[serde(flatten)]
pub app_event: Option<AppEvent>,
#[serde(flatten)]
pub setting_event: Option<SettingEvent>,
#[serde(flatten)]
pub extension_event: Option<ExtensionEvent>,
#[serde(flatten)]
pub edit_event: Option<EditEvent>,
#[serde(flatten)]
pub repl_event: Option<ReplEvent>,
#[serde(flatten)]
pub action_event: Option<ActionEvent>,
}

View File

@@ -170,6 +170,10 @@ pub struct Config {
pub blob_store_access_key: Option<String>,
pub blob_store_secret_key: Option<String>,
pub blob_store_bucket: Option<String>,
pub kinesis_region: Option<String>,
pub kinesis_stream: Option<String>,
pub kinesis_access_key: Option<String>,
pub kinesis_secret_key: Option<String>,
pub zed_environment: Arc<str>,
pub openai_api_key: Option<Arc<str>>,
pub google_ai_api_key: Option<Arc<str>>,
@@ -238,6 +242,10 @@ impl Config {
stripe_api_key: None,
supermaven_admin_api_key: None,
user_backfiller_github_access_token: None,
kinesis_region: None,
kinesis_access_key: None,
kinesis_secret_key: None,
kinesis_stream: None,
}
}
}
@@ -276,6 +284,7 @@ pub struct AppState {
pub rate_limiter: Arc<RateLimiter>,
pub executor: Executor,
pub clickhouse_client: Option<::clickhouse::Client>,
pub kinesis_client: Option<::aws_sdk_kinesis::Client>,
pub config: Config,
}
@@ -332,6 +341,11 @@ impl AppState {
.clickhouse_url
.as_ref()
.and_then(|_| build_clickhouse_client(&config).log_err()),
kinesis_client: if config.kinesis_access_key.is_some() {
build_kinesis_client(&config).await.log_err()
} else {
None
},
config,
};
Ok(Arc::new(this))
@@ -381,6 +395,35 @@ async fn build_blob_store_client(config: &Config) -> anyhow::Result<aws_sdk_s3::
Ok(aws_sdk_s3::Client::new(&s3_config))
}
async fn build_kinesis_client(config: &Config) -> anyhow::Result<aws_sdk_kinesis::Client> {
let keys = aws_sdk_s3::config::Credentials::new(
config
.kinesis_access_key
.clone()
.ok_or_else(|| anyhow!("missing kinesis_access_key"))?,
config
.kinesis_secret_key
.clone()
.ok_or_else(|| anyhow!("missing kinesis_secret_key"))?,
None,
None,
"env",
);
let kinesis_config = aws_config::defaults(BehaviorVersion::latest())
.region(Region::new(
config
.kinesis_region
.clone()
.ok_or_else(|| anyhow!("missing blob_store_region"))?,
))
.credentials_provider(keys)
.load()
.await;
Ok(aws_sdk_kinesis::Client::new(&kinesis_config))
}
fn build_clickhouse_client(config: &Config) -> anyhow::Result<::clickhouse::Client> {
Ok(::clickhouse::Client::default()
.with_url(

View File

@@ -267,7 +267,6 @@ async fn perform_completion(
anthropic::ANTHROPIC_API_URL,
api_key,
request,
None,
)
.await
.map_err(|err| match err {
@@ -357,7 +356,6 @@ async fn perform_completion(
open_ai::OPEN_AI_API_URL,
api_key,
serde_json::from_str(params.provider_request.get())?,
None,
)
.await?;
@@ -390,7 +388,6 @@ async fn perform_completion(
google_ai::API_URL,
api_key,
serde_json::from_str(params.provider_request.get())?,
None,
)
.await?;

View File

@@ -3621,7 +3621,6 @@ async fn count_language_model_tokens(
google_ai::API_URL,
api_key,
serde_json::from_str(&request.request)?,
None,
)
.await?
}
@@ -4031,12 +4030,18 @@ async fn get_llm_api_token(
Err(anyhow!("terms of service not accepted"))?
}
let mut account_created_at = user.created_at;
if let Some(github_created_at) = user.github_user_created_at {
account_created_at = account_created_at.min(github_created_at);
}
if Utc::now().naive_utc() - account_created_at < MIN_ACCOUNT_AGE_FOR_LLM_USE {
Err(anyhow!("account too young"))?
let has_llm_subscription = session.has_llm_subscription(&db).await?;
let bypass_account_age_check =
has_llm_subscription || flags.iter().any(|flag| flag == "bypass-account-age-check");
if !bypass_account_age_check {
let mut account_created_at = user.created_at;
if let Some(github_created_at) = user.github_user_created_at {
account_created_at = account_created_at.min(github_created_at);
}
if Utc::now().naive_utc() - account_created_at < MIN_ACCOUNT_AGE_FOR_LLM_USE {
Err(anyhow!("account too young"))?
}
}
let billing_preferences = db.get_billing_preferences(user.id).await?;
@@ -4046,7 +4051,7 @@ async fn get_llm_api_token(
session.is_staff(),
billing_preferences,
has_llm_closed_beta_feature_flag,
session.has_llm_subscription(&db).await?,
has_llm_subscription,
session.current_plan(&db).await?,
&session.app_state.config,
)?;

View File

@@ -1323,11 +1323,8 @@ impl RandomizedTest for ProjectCollaborationTest {
match (host_file, guest_file) {
(Some(host_file), Some(guest_file)) => {
assert_eq!(guest_file.path(), host_file.path());
assert_eq!(guest_file.is_deleted(), host_file.is_deleted());
assert_eq!(
guest_file.mtime(),
host_file.mtime(),
"guest {} mtime does not match host {} for path {:?} in project {}",
assert_eq!(guest_file.disk_state(), host_file.disk_state(),
"guest {} disk_state does not match host {} for path {:?} in project {}",
guest_user_id,
host_user_id,
guest_file.path(),

View File

@@ -168,7 +168,7 @@ impl TestServer {
client::init_settings(cx);
});
let clock = Arc::new(FakeSystemClock::default());
let clock = Arc::new(FakeSystemClock::new());
let http = FakeHttpClient::with_404_response();
let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await
{
@@ -512,6 +512,7 @@ impl TestServer {
rate_limiter: Arc::new(RateLimiter::new(test_db.db().clone())),
executor,
clickhouse_client: None,
kinesis_client: None,
config: Config {
http_port: 0,
database_url: "".into(),
@@ -550,6 +551,10 @@ impl TestServer {
stripe_api_key: None,
supermaven_admin_api_key: None,
user_backfiller_github_access_token: None,
kinesis_region: None,
kinesis_stream: None,
kinesis_access_key: None,
kinesis_secret_key: None,
},
})
}

View File

@@ -23,7 +23,7 @@ use std::{sync::Arc, time::Duration};
use time::{OffsetDateTime, UtcOffset};
use ui::{
prelude::*, Avatar, Button, ContextMenu, IconButton, IconName, KeyBinding, Label, PopoverMenu,
TabBar, Tooltip,
Tab, TabBar, Tooltip,
};
use util::{ResultExt, TryFutureExt};
use workspace::{
@@ -939,7 +939,7 @@ impl Render for ChatPanel {
TabBar::new("chat_header").child(
h_flex()
.w_full()
.h(rems(ui::Tab::CONTAINER_HEIGHT_IN_REMS))
.h(Tab::container_height(cx))
.px_2()
.child(Label::new(
self.active_chat

View File

@@ -2521,7 +2521,7 @@ impl CollabPanel {
.flex()
.w_full()
.when(!channel.is_root_channel(), |el| {
el.on_drag(channel.clone(), move |channel, cx| {
el.on_drag(channel.clone(), move |channel, _, cx| {
cx.new_view(|_| DraggedChannelView {
channel: channel.clone(),
width,

View File

@@ -19,7 +19,9 @@ use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
use std::{sync::Arc, time::Duration};
use time::{OffsetDateTime, UtcOffset};
use ui::{h_flex, prelude::*, v_flex, Avatar, Button, Icon, IconButton, IconName, Label, Tooltip};
use ui::{
h_flex, prelude::*, v_flex, Avatar, Button, Icon, IconButton, IconName, Label, Tab, Tooltip,
};
use util::{ResultExt, TryFutureExt};
use workspace::notifications::NotificationId;
use workspace::{
@@ -588,7 +590,7 @@ impl Render for NotificationPanel {
.px_2()
.py_1()
// Match the height of the tab bar so they line up.
.h(rems(ui::Tab::CONTAINER_HEIGHT_IN_REMS))
.h(Tab::container_height(cx))
.border_b_1()
.border_color(cx.theme().colors().border)
.child(Label::new("Notifications"))

View File

@@ -25,6 +25,13 @@ use util::TryFutureExt;
const JSON_RPC_VERSION: &str = "2.0";
const REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
// Standard JSON-RPC error codes
pub const PARSE_ERROR: i32 = -32700;
pub const INVALID_REQUEST: i32 = -32600;
pub const METHOD_NOT_FOUND: i32 = -32601;
pub const INVALID_PARAMS: i32 = -32602;
pub const INTERNAL_ERROR: i32 = -32603;
type ResponseHandler = Box<dyn Send + FnOnce(Result<String, Error>)>;
type NotificationHandler = Box<dyn Send + FnMut(Value, AsyncAppContext)>;

View File

@@ -11,8 +11,6 @@ use collections::HashMap;
use crate::client::Client;
use crate::types;
const PROTOCOL_VERSION: &str = "2024-10-07";
pub struct ModelContextProtocol {
inner: Client,
}
@@ -23,10 +21,9 @@ impl ModelContextProtocol {
}
fn supported_protocols() -> Vec<types::ProtocolVersion> {
vec![
types::ProtocolVersion::VersionString(PROTOCOL_VERSION.to_string()),
types::ProtocolVersion::VersionNumber(1),
]
vec![types::ProtocolVersion(
types::LATEST_PROTOCOL_VERSION.to_string(),
)]
}
pub async fn initialize(
@@ -34,11 +31,13 @@ impl ModelContextProtocol {
client_info: types::Implementation,
) -> Result<InitializedContextServerProtocol> {
let params = types::InitializeParams {
protocol_version: types::ProtocolVersion::VersionString(PROTOCOL_VERSION.to_string()),
protocol_version: types::ProtocolVersion(types::LATEST_PROTOCOL_VERSION.to_string()),
capabilities: types::ClientCapabilities {
experimental: None,
sampling: None,
roots: None,
},
meta: None,
client_info,
};
@@ -148,6 +147,7 @@ impl InitializedContextServerProtocol {
let params = types::PromptsGetParams {
name: prompt.as_ref().to_string(),
arguments: Some(arguments),
meta: None,
};
let response: types::PromptsGetResponse = self
@@ -170,6 +170,7 @@ impl InitializedContextServerProtocol {
name: argument.into(),
value: value.into(),
},
meta: None,
};
let result: types::CompletionCompleteResponse = self
.inner
@@ -210,6 +211,7 @@ impl InitializedContextServerProtocol {
let params = types::CallToolParams {
name: tool.as_ref().to_string(),
arguments,
meta: None,
};
let response: types::CallToolResponse = self

View File

@@ -2,8 +2,8 @@ use collections::HashMap;
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub const LATEST_PROTOCOL_VERSION: &str = "2024-11-05";
pub enum RequestType {
Initialize,
CallTool,
@@ -18,6 +18,7 @@ pub enum RequestType {
Ping,
ListTools,
ListResourceTemplates,
ListRoots,
}
impl RequestType {
@@ -36,16 +37,14 @@ impl RequestType {
RequestType::Ping => "ping",
RequestType::ListTools => "tools/list",
RequestType::ListResourceTemplates => "resources/templates/list",
RequestType::ListRoots => "roots/list",
}
}
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ProtocolVersion {
VersionString(String),
VersionNumber(u32),
}
#[serde(transparent)]
pub struct ProtocolVersion(pub String);
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
@@ -53,6 +52,8 @@ pub struct InitializeParams {
pub protocol_version: ProtocolVersion,
pub capabilities: ClientCapabilities,
pub client_info: Implementation,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize)]
@@ -61,30 +62,40 @@ pub struct CallToolParams {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<HashMap<String, serde_json::Value>>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourcesUnsubscribeParams {
pub uri: Url,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourcesSubscribeParams {
pub uri: Url,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourcesReadParams {
pub uri: Url,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LoggingSetLevelParams {
pub level: LoggingLevel,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize)]
@@ -93,6 +104,8 @@ pub struct PromptsGetParams {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<HashMap<String, String>>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize)]
@@ -100,6 +113,8 @@ pub struct PromptsGetParams {
pub struct CompletionCompleteParams {
pub r#ref: CompletionReference,
pub argument: CompletionArgument,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize)]
@@ -145,12 +160,16 @@ pub struct InitializeResponse {
pub protocol_version: ProtocolVersion,
pub capabilities: ServerCapabilities,
pub server_info: Implementation,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourcesReadResponse {
pub contents: Vec<ResourceContent>,
pub contents: Vec<ResourceContents>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Deserialize)]
@@ -159,29 +178,39 @@ pub struct ResourcesListResponse {
pub resources: Vec<Resource>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SamplingMessage {
pub role: Role,
pub content: MessageContent,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SamplingMessage {
pub role: SamplingRole,
pub content: SamplingContent,
pub struct PromptMessage {
pub role: Role,
pub content: MessageContent,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SamplingRole {
pub enum Role {
User,
Assistant,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum SamplingContent {
pub enum MessageContent {
#[serde(rename = "text")]
Text { text: String },
#[serde(rename = "image")]
Image { data: String, mime_type: String },
#[serde(rename = "resource")]
Resource { resource: ResourceContents },
}
#[derive(Debug, Deserialize)]
@@ -189,7 +218,9 @@ pub enum SamplingContent {
pub struct PromptsGetResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub messages: Vec<SamplingMessage>,
pub messages: Vec<PromptMessage>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Deserialize)]
@@ -198,12 +229,16 @@ pub struct PromptsListResponse {
pub prompts: Vec<Prompt>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionCompleteResponse {
pub completion: CompletionResult,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Deserialize)]
@@ -214,6 +249,8 @@ pub struct CompletionResult {
pub total: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub has_more: Option<bool>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Deserialize, Serialize)]
@@ -243,6 +280,8 @@ pub struct ClientCapabilities {
pub experimental: Option<HashMap<String, serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sampling: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub roots: Option<RootsCapabilities>,
}
#[derive(Debug, Serialize, Deserialize)]
@@ -283,6 +322,13 @@ pub struct ToolsCapabilities {
pub list_changed: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RootsCapabilities {
#[serde(skip_serializing_if = "Option::is_none")]
pub list_changed: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Tool {
@@ -312,14 +358,28 @@ pub struct Resource {
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourceContent {
pub struct ResourceContents {
pub uri: Url,
#[serde(skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TextResourceContents {
pub uri: Url,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
pub mime_type: Option<String>,
pub text: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BlobResourceContents {
pub uri: Url,
#[serde(skip_serializing_if = "Option::is_none")]
pub blob: Option<String>,
pub mime_type: Option<String>,
pub blob: String,
}
#[derive(Debug, Serialize, Deserialize)]
@@ -338,8 +398,32 @@ pub struct ResourceTemplate {
pub enum LoggingLevel {
Debug,
Info,
Notice,
Warning,
Error,
Critical,
Alert,
Emergency,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ModelPreferences {
#[serde(skip_serializing_if = "Option::is_none")]
pub hints: Option<Vec<ModelHint>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cost_priority: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub speed_priority: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub intelligence_priority: Option<f64>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ModelHint {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
#[derive(Debug, Serialize)]
@@ -352,6 +436,7 @@ pub enum NotificationType {
ResourcesListChanged,
ToolsListChanged,
PromptsListChanged,
RootsListChanged,
}
impl NotificationType {
@@ -364,6 +449,7 @@ impl NotificationType {
NotificationType::ResourcesListChanged => "notifications/resources/list_changed",
NotificationType::ToolsListChanged => "notifications/tools/list_changed",
NotificationType::PromptsListChanged => "notifications/prompts/list_changed",
NotificationType::RootsListChanged => "notifications/roots/list_changed",
}
}
}
@@ -373,6 +459,14 @@ impl NotificationType {
pub enum ClientNotification {
Initialized,
Progress(ProgressParams),
RootsListChanged,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ProgressToken {
String(String),
Number(f64),
}
#[derive(Debug, Serialize)]
@@ -382,10 +476,10 @@ pub struct ProgressParams {
pub progress: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub total: Option<f64>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
pub type ProgressToken = String;
pub enum CompletionTotal {
Exact(u32),
HasMore,
@@ -410,7 +504,22 @@ pub struct Completion {
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CallToolResponse {
pub tool_result: serde_json::Value,
pub content: Vec<ToolResponseContent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_error: Option<bool>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ToolResponseContent {
#[serde(rename = "text")]
Text { text: String },
#[serde(rename = "image")]
Image { data: String, mime_type: String },
#[serde(rename = "resource")]
Resource { resource: ResourceContents },
}
#[derive(Debug, Deserialize)]
@@ -419,4 +528,22 @@ pub struct ListToolsResponse {
pub tools: Vec<Tool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListRootsResponse {
pub roots: Vec<Root>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Root {
pub uri: Url,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}

View File

@@ -1229,8 +1229,10 @@ mod tests {
Some(self)
}
fn mtime(&self) -> Option<std::time::SystemTime> {
unimplemented!()
fn disk_state(&self) -> language::DiskState {
language::DiskState::Present {
mtime: std::time::UNIX_EPOCH,
}
}
fn path(&self) -> &Arc<Path> {
@@ -1245,10 +1247,6 @@ mod tests {
unimplemented!()
}
fn is_deleted(&self) -> bool {
unimplemented!()
}
fn as_any(&self) -> &dyn std::any::Any {
unimplemented!()
}

View File

@@ -1,13 +1,13 @@
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::OnceLock;
use std::{sync::Arc, time::Duration};
use anyhow::{anyhow, Result};
use chrono::DateTime;
use fs::Fs;
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, StreamExt};
use gpui::{AppContext, AsyncAppContext, Global};
use http_client::{AsyncBody, HttpClient, HttpRequestExt, Method, Request as HttpRequest};
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
use paths::home_dir;
use serde::{Deserialize, Serialize};
use settings::watch_config_file;
@@ -254,7 +254,6 @@ impl CopilotChat {
pub async fn stream_completion(
request: Request,
low_speed_timeout: Option<Duration>,
mut cx: AsyncAppContext,
) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
let Some(this) = cx.update(|cx| Self::global(cx)).ok().flatten() else {
@@ -274,8 +273,7 @@ impl CopilotChat {
let token = match api_token {
Some(api_token) if api_token.remaining_seconds() > 5 * 60 => api_token.clone(),
_ => {
let token =
request_api_token(&oauth_token, client.clone(), low_speed_timeout).await?;
let token = request_api_token(&oauth_token, client.clone()).await?;
this.update(&mut cx, |this, cx| {
this.api_token = Some(token.clone());
cx.notify();
@@ -284,25 +282,17 @@ impl CopilotChat {
}
};
stream_completion(client.clone(), token.api_key, request, low_speed_timeout).await
stream_completion(client.clone(), token.api_key, request).await
}
}
async fn request_api_token(
oauth_token: &str,
client: Arc<dyn HttpClient>,
low_speed_timeout: Option<Duration>,
) -> Result<ApiToken> {
let mut request_builder = HttpRequest::builder()
async fn request_api_token(oauth_token: &str, client: Arc<dyn HttpClient>) -> Result<ApiToken> {
let request_builder = HttpRequest::builder()
.method(Method::GET)
.uri(COPILOT_CHAT_AUTH_URL)
.header("Authorization", format!("token {}", oauth_token))
.header("Accept", "application/json");
if let Some(low_speed_timeout) = low_speed_timeout {
request_builder = request_builder.read_timeout(low_speed_timeout);
}
let request = request_builder.body(AsyncBody::empty())?;
let mut response = client.send(request).await?;
@@ -340,9 +330,8 @@ async fn stream_completion(
client: Arc<dyn HttpClient>,
api_key: String,
request: Request,
low_speed_timeout: Option<Duration>,
) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
let mut request_builder = HttpRequest::builder()
let request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(COPILOT_CHAT_COMPLETION_URL)
.header(
@@ -356,9 +345,6 @@ async fn stream_completion(
.header("Content-Type", "application/json")
.header("Copilot-Integration-Id", "vscode-chat");
if let Some(low_speed_timeout) = low_speed_timeout {
request_builder = request_builder.read_timeout(low_speed_timeout);
}
let is_streaming = request.stream;
let request = request_builder.body(AsyncBody::from(serde_json::to_string(&request)?))?;

View File

@@ -727,6 +727,10 @@ impl Item for ProjectDiagnosticsEditor {
self.excerpts.read(cx).is_dirty(cx)
}
fn has_deleted_file(&self, cx: &AppContext) -> bool {
self.excerpts.read(cx).has_deleted_file(cx)
}
fn has_conflict(&self, cx: &AppContext) -> bool {
self.excerpts.read(cx).has_conflict(cx)
}

View File

@@ -297,6 +297,7 @@ gpui::actions!(
OpenExcerptsSplit,
OpenProposedChangesEditor,
OpenFile,
OpenDocs,
OpenPermalinkToLine,
OpenUrl,
Outdent,

View File

@@ -1,5 +1,3 @@
use std::path::PathBuf;
use anyhow::Context as _;
use gpui::{View, ViewContext, WindowContext};
use language::Language;
@@ -54,9 +52,9 @@ pub fn switch_source_header(
cx.spawn(|_editor, mut cx| async move {
let switch_source_header = switch_source_header_task
.await
.with_context(|| format!("Switch source/header LSP request for path \"{}\" failed", source_file))?;
.with_context(|| format!("Switch source/header LSP request for path \"{source_file}\" failed"))?;
if switch_source_header.0.is_empty() {
log::info!("Clangd returned an empty string when requesting to switch source/header from \"{}\"", source_file);
log::info!("Clangd returned an empty string when requesting to switch source/header from \"{source_file}\"" );
return Ok(());
}
@@ -67,14 +65,17 @@ pub fn switch_source_header(
)
})?;
let path = goto.to_file_path().map_err(|()| {
anyhow::anyhow!("URL conversion to file path failed for \"{goto}\"")
})?;
workspace
.update(&mut cx, |workspace, view_cx| {
workspace.open_abs_path(PathBuf::from(goto.path()), false, view_cx)
workspace.open_abs_path(path, false, view_cx)
})
.with_context(|| {
format!(
"Switch source/header could not open \"{}\" in workspace",
goto.path()
"Switch source/header could not open \"{goto}\" in workspace"
)
})?
.await

View File

@@ -5,6 +5,7 @@ use gpui::{Task, ViewContext};
use crate::Editor;
#[derive(Debug)]
pub struct DebouncedDelay {
task: Option<Task<()>>,
cancel_channel: Option<oneshot::Sender<()>>,

View File

@@ -66,7 +66,7 @@ use std::{
use sum_tree::{Bias, TreeMap};
use tab_map::{TabMap, TabSnapshot};
use text::LineIndent;
use ui::{div, px, IntoElement, ParentElement, SharedString, Styled, WindowContext};
use ui::{px, SharedString, WindowContext};
use unicode_segmentation::UnicodeSegmentation;
use wrap_map::{WrapMap, WrapSnapshot};
@@ -541,11 +541,17 @@ pub struct HighlightStyles {
pub suggestion: Option<HighlightStyle>,
}
#[derive(Clone)]
pub enum ChunkReplacement {
Renderer(ChunkRenderer),
Str(SharedString),
}
pub struct HighlightedChunk<'a> {
pub text: &'a str,
pub style: Option<HighlightStyle>,
pub is_tab: bool,
pub renderer: Option<ChunkRenderer>,
pub replacement: Option<ChunkReplacement>,
}
impl<'a> HighlightedChunk<'a> {
@@ -557,7 +563,7 @@ impl<'a> HighlightedChunk<'a> {
let mut text = self.text;
let style = self.style;
let is_tab = self.is_tab;
let renderer = self.renderer;
let renderer = self.replacement;
iter::from_fn(move || {
let mut prefix_len = 0;
while let Some(&ch) = chars.peek() {
@@ -573,30 +579,33 @@ impl<'a> HighlightedChunk<'a> {
text: prefix,
style,
is_tab,
renderer: renderer.clone(),
replacement: renderer.clone(),
});
}
chars.next();
let (prefix, suffix) = text.split_at(ch.len_utf8());
text = suffix;
if let Some(replacement) = replacement(ch) {
let background = editor_style.status.hint_background;
let underline = editor_style.status.hint;
let invisible_highlight = HighlightStyle {
background_color: Some(editor_style.status.hint_background),
underline: Some(UnderlineStyle {
color: Some(editor_style.status.hint),
thickness: px(1.),
wavy: false,
}),
..Default::default()
};
let invisible_style = if let Some(mut style) = style {
style.highlight(invisible_highlight);
style
} else {
invisible_highlight
};
return Some(HighlightedChunk {
text: prefix,
style: None,
style: Some(invisible_style),
is_tab: false,
renderer: Some(ChunkRenderer {
render: Arc::new(move |_| {
div()
.child(replacement)
.bg(background)
.text_decoration_1()
.text_decoration_color(underline)
.into_any_element()
}),
constrain_width: false,
}),
replacement: Some(ChunkReplacement::Str(replacement.into())),
});
} else {
let invisible_highlight = HighlightStyle {
@@ -619,7 +628,7 @@ impl<'a> HighlightedChunk<'a> {
text: prefix,
style: Some(invisible_style),
is_tab: false,
renderer: renderer.clone(),
replacement: renderer.clone(),
});
}
}
@@ -631,7 +640,7 @@ impl<'a> HighlightedChunk<'a> {
text: remainder,
style,
is_tab,
renderer: renderer.clone(),
replacement: renderer.clone(),
})
} else {
None
@@ -895,7 +904,7 @@ impl DisplaySnapshot {
text: chunk.text,
style: highlight_style,
is_tab: chunk.is_tab,
renderer: chunk.renderer,
replacement: chunk.renderer.map(ChunkReplacement::Renderer),
}
.highlight_invisibles(editor_style)
})

View File

@@ -540,6 +540,15 @@ pub enum IsVimMode {
No,
}
pub trait ActiveLineTrailerProvider {
fn render_active_line_trailer(
&mut self,
style: &EditorStyle,
focus_handle: &FocusHandle,
cx: &mut WindowContext,
) -> Option<AnyElement>;
}
/// Zed's primary text input `View`, allowing users to edit a [`MultiBuffer`]
///
/// See the [module level documentation](self) for more information.
@@ -667,6 +676,7 @@ pub struct Editor {
next_scroll_position: NextScrollCursorCenterTopBottom,
addons: HashMap<TypeId, Box<dyn Addon>>,
_scroll_cursor_center_top_bottom_task: Task<()>,
active_line_trailer_provider: Option<Box<dyn ActiveLineTrailerProvider>>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
@@ -883,6 +893,7 @@ struct AutocloseRegion {
struct SnippetState {
ranges: Vec<Vec<Range<Anchor>>>,
active_index: usize,
choices: Vec<Option<Vec<String>>>,
}
#[doc(hidden)]
@@ -1000,7 +1011,7 @@ enum ContextMenuOrigin {
GutterIndicator(DisplayRow),
}
#[derive(Clone)]
#[derive(Clone, Debug)]
struct CompletionsMenu {
id: CompletionId,
sort_completions: bool,
@@ -1011,10 +1022,105 @@ struct CompletionsMenu {
matches: Arc<[StringMatch]>,
selected_item: usize,
scroll_handle: UniformListScrollHandle,
selected_completion_documentation_resolve_debounce: Arc<Mutex<DebouncedDelay>>,
selected_completion_documentation_resolve_debounce: Option<Arc<Mutex<DebouncedDelay>>>,
}
impl CompletionsMenu {
fn new(
id: CompletionId,
sort_completions: bool,
initial_position: Anchor,
buffer: Model<Buffer>,
completions: Box<[Completion]>,
) -> Self {
let match_candidates = completions
.iter()
.enumerate()
.map(|(id, completion)| {
StringMatchCandidate::new(
id,
completion.label.text[completion.label.filter_range.clone()].into(),
)
})
.collect();
Self {
id,
sort_completions,
initial_position,
buffer,
completions: Arc::new(RwLock::new(completions)),
match_candidates,
matches: Vec::new().into(),
selected_item: 0,
scroll_handle: UniformListScrollHandle::new(),
selected_completion_documentation_resolve_debounce: Some(Arc::new(Mutex::new(
DebouncedDelay::new(),
))),
}
}
fn new_snippet_choices(
id: CompletionId,
sort_completions: bool,
choices: &Vec<String>,
selection: Range<Anchor>,
buffer: Model<Buffer>,
) -> Self {
let completions = choices
.iter()
.map(|choice| Completion {
old_range: selection.start.text_anchor..selection.end.text_anchor,
new_text: choice.to_string(),
label: CodeLabel {
text: choice.to_string(),
runs: Default::default(),
filter_range: Default::default(),
},
server_id: LanguageServerId(usize::MAX),
documentation: None,
lsp_completion: Default::default(),
confirm: None,
})
.collect();
let match_candidates = choices
.iter()
.enumerate()
.map(|(id, completion)| StringMatchCandidate::new(id, completion.to_string()))
.collect();
let matches = choices
.iter()
.enumerate()
.map(|(id, completion)| StringMatch {
candidate_id: id,
score: 1.,
positions: vec![],
string: completion.clone(),
})
.collect();
Self {
id,
sort_completions,
initial_position: selection.start,
buffer,
completions: Arc::new(RwLock::new(completions)),
match_candidates,
matches,
selected_item: 0,
scroll_handle: UniformListScrollHandle::new(),
selected_completion_documentation_resolve_debounce: Some(Arc::new(Mutex::new(
DebouncedDelay::new(),
))),
}
}
fn suppress_documentation_resolution(mut self) -> Self {
self.selected_completion_documentation_resolve_debounce
.take();
self
}
fn select_first(
&mut self,
provider: Option<&dyn CompletionProvider>,
@@ -1115,6 +1221,12 @@ impl CompletionsMenu {
let Some(provider) = provider else {
return;
};
let Some(documentation_resolve) = self
.selected_completion_documentation_resolve_debounce
.as_ref()
else {
return;
};
let resolve_task = provider.resolve_completions(
self.buffer.clone(),
@@ -1127,15 +1239,13 @@ impl CompletionsMenu {
EditorSettings::get_global(cx).completion_documentation_secondary_query_debounce;
let delay = Duration::from_millis(delay_ms);
self.selected_completion_documentation_resolve_debounce
.lock()
.fire_new(delay, cx, |_, cx| {
cx.spawn(move |this, mut cx| async move {
if let Some(true) = resolve_task.await.log_err() {
this.update(&mut cx, |_, cx| cx.notify()).ok();
}
})
});
documentation_resolve.lock().fire_new(delay, cx, |_, cx| {
cx.spawn(move |this, mut cx| async move {
if let Some(true) = resolve_task.await.log_err() {
this.update(&mut cx, |_, cx| cx.notify()).ok();
}
})
});
}
fn visible(&self) -> bool {
@@ -1418,6 +1528,7 @@ impl CompletionsMenu {
}
}
#[derive(Clone)]
struct AvailableCodeAction {
excerpt_id: ExcerptId,
action: CodeAction,
@@ -2104,6 +2215,7 @@ impl Editor {
addons: HashMap::default(),
_scroll_cursor_center_top_bottom_task: Task::ready(()),
text_style_refinement: None,
active_line_trailer_provider: None,
};
this.tasks_update_task = Some(this.refresh_runnables(cx));
this._subscriptions.extend(project_subscriptions);
@@ -2392,6 +2504,16 @@ impl Editor {
self.refresh_inline_completion(false, false, cx);
}
pub fn set_active_line_trailer_provider<T>(
&mut self,
provider: Option<T>,
_cx: &mut ViewContext<Self>,
) where
T: ActiveLineTrailerProvider + 'static,
{
self.active_line_trailer_provider = provider.map(|provider| Box::new(provider) as Box<_>);
}
pub fn placeholder_text(&self, _cx: &WindowContext) -> Option<&str> {
self.placeholder_text.as_deref()
}
@@ -4386,6 +4508,10 @@ impl Editor {
return;
};
if !self.snippet_stack.is_empty() && self.context_menu.read().as_ref().is_some() {
return;
}
let position = self.selections.newest_anchor().head();
let (buffer, buffer_position) =
if let Some(output) = self.buffer.read(cx).text_anchor_for_position(position, cx) {
@@ -4431,30 +4557,13 @@ impl Editor {
})?;
let completions = completions.await.log_err();
let menu = if let Some(completions) = completions {
let mut menu = CompletionsMenu {
let mut menu = CompletionsMenu::new(
id,
sort_completions,
initial_position: position,
match_candidates: completions
.iter()
.enumerate()
.map(|(id, completion)| {
StringMatchCandidate::new(
id,
completion.label.text[completion.label.filter_range.clone()]
.into(),
)
})
.collect(),
buffer: buffer.clone(),
completions: Arc::new(RwLock::new(completions.into())),
matches: Vec::new().into(),
selected_item: 0,
scroll_handle: UniformListScrollHandle::new(),
selected_completion_documentation_resolve_debounce: Arc::new(Mutex::new(
DebouncedDelay::new(),
)),
};
position,
buffer.clone(),
completions.into(),
);
menu.filter(query.as_deref(), cx.background_executor().clone())
.await;
@@ -4657,7 +4766,11 @@ impl Editor {
self.transact(cx, |this, cx| {
if let Some(mut snippet) = snippet {
snippet.text = text.to_string();
for tabstop in snippet.tabstops.iter_mut().flatten() {
for tabstop in snippet
.tabstops
.iter_mut()
.flat_map(|tabstop| tabstop.ranges.iter_mut())
{
tabstop.start -= common_prefix_len as isize;
tabstop.end -= common_prefix_len as isize;
}
@@ -5693,6 +5806,27 @@ impl Editor {
context_menu
}
fn show_snippet_choices(
&mut self,
choices: &Vec<String>,
selection: Range<Anchor>,
cx: &mut ViewContext<Self>,
) {
if selection.start.buffer_id.is_none() {
return;
}
let buffer_id = selection.start.buffer_id.unwrap();
let buffer = self.buffer().read(cx).buffer(buffer_id);
let id = post_inc(&mut self.next_completion_id);
if let Some(buffer) = buffer {
*self.context_menu.write() = Some(ContextMenu::Completions(
CompletionsMenu::new_snippet_choices(id, true, choices, selection, buffer)
.suppress_documentation_resolution(),
));
}
}
pub fn insert_snippet(
&mut self,
insertion_ranges: &[Range<usize>],
@@ -5702,6 +5836,7 @@ impl Editor {
struct Tabstop<T> {
is_end_tabstop: bool,
ranges: Vec<Range<T>>,
choices: Option<Vec<String>>,
}
let tabstops = self.buffer.update(cx, |buffer, cx| {
@@ -5721,10 +5856,11 @@ impl Editor {
.tabstops
.iter()
.map(|tabstop| {
let is_end_tabstop = tabstop.first().map_or(false, |tabstop| {
let is_end_tabstop = tabstop.ranges.first().map_or(false, |tabstop| {
tabstop.is_empty() && tabstop.start == snippet.text.len() as isize
});
let mut tabstop_ranges = tabstop
.ranges
.iter()
.flat_map(|tabstop_range| {
let mut delta = 0_isize;
@@ -5746,6 +5882,7 @@ impl Editor {
Tabstop {
is_end_tabstop,
ranges: tabstop_ranges,
choices: tabstop.choices.clone(),
}
})
.collect::<Vec<_>>()
@@ -5755,16 +5892,29 @@ impl Editor {
s.select_ranges(tabstop.ranges.iter().cloned());
});
if let Some(choices) = &tabstop.choices {
if let Some(selection) = tabstop.ranges.first() {
self.show_snippet_choices(choices, selection.clone(), cx)
}
}
// If we're already at the last tabstop and it's at the end of the snippet,
// we're done, we don't need to keep the state around.
if !tabstop.is_end_tabstop {
let choices = tabstops
.iter()
.map(|tabstop| tabstop.choices.clone())
.collect();
let ranges = tabstops
.into_iter()
.map(|tabstop| tabstop.ranges)
.collect::<Vec<_>>();
self.snippet_stack.push(SnippetState {
active_index: 0,
ranges,
choices,
});
}
@@ -5839,6 +5989,13 @@ impl Editor {
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_anchor_ranges(current_ranges.iter().cloned())
});
if let Some(choices) = &snippet.choices[snippet.active_index] {
if let Some(selection) = current_ranges.first() {
self.show_snippet_choices(&choices, selection.clone(), cx);
}
}
// If snippet state is not at the last tabstop, push it back on the stack
if snippet.active_index + 1 < snippet.ranges.len() {
self.snippet_stack.push(snippet);
@@ -11713,6 +11870,21 @@ impl Editor {
&& self.has_blame_entries(cx)
}
pub fn render_active_line_trailer(
&mut self,
style: &EditorStyle,
cx: &mut WindowContext,
) -> Option<AnyElement> {
if !self.newest_selection_head_on_empty_line(cx) || self.has_active_inline_completion(cx) {
return None;
}
let focus_handle = self.focus_handle.clone();
self.active_line_trailer_provider
.as_mut()?
.render_active_line_trailer(style, &focus_handle, cx)
}
fn has_blame_entries(&self, cx: &mut WindowContext) -> bool {
self.blame()
.map_or(false, |blame| blame.read(cx).has_generated_entries())

View File

@@ -279,7 +279,7 @@ pub struct EditorSettingsContent {
/// Whether to show the signature help pop-up after completions or bracket pairs inserted.
///
/// Default: true
/// Default: false
pub show_signature_help_after_edits: Option<bool>,
/// Jupyter REPL settings.

View File

@@ -1398,6 +1398,15 @@ fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
view.change_selections(None, cx, |s| {
s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
});
// moving above start of document should move selection to start of document,
// but the next move down should still be at the original goal_x
view.move_up(&MoveUp, cx);
assert_eq!(
view.selections.display_ranges(cx),
&[empty_range(0, "".len())]
);
view.move_down(&MoveDown, cx);
assert_eq!(
view.selections.display_ranges(cx),
@@ -1422,6 +1431,25 @@ fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
&[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
);
// moving past end of document should not change goal_x
view.move_down(&MoveDown, cx);
assert_eq!(
view.selections.display_ranges(cx),
&[empty_range(5, "".len())]
);
view.move_down(&MoveDown, cx);
assert_eq!(
view.selections.display_ranges(cx),
&[empty_range(5, "".len())]
);
view.move_up(&MoveUp, cx);
assert_eq!(
view.selections.display_ranges(cx),
&[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
);
view.move_up(&MoveUp, cx);
assert_eq!(
view.selections.display_ranges(cx),
@@ -6551,6 +6579,45 @@ async fn test_auto_replace_emoji_shortcode(cx: &mut gpui::TestAppContext) {
});
}
#[gpui::test]
async fn test_snippet_placeholder_choices(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let (text, insertion_ranges) = marked_text_ranges(
indoc! {"
ˇ
"},
false,
);
let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
_ = editor.update(cx, |editor, cx| {
let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
editor
.insert_snippet(&insertion_ranges, snippet, cx)
.unwrap();
fn assert(editor: &mut Editor, cx: &mut ViewContext<Editor>, marked_text: &str) {
let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
assert_eq!(editor.text(cx), expected_text);
assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
}
assert(
editor,
cx,
indoc! {"
type «» =•
"},
);
assert!(editor.context_menu_visible(), "There should be a matches");
});
}
#[gpui::test]
async fn test_snippets(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});

View File

@@ -16,8 +16,8 @@ use crate::{
items::BufferSearchHighlights,
mouse_context_menu::{self, MenuPosition, MouseContextMenu},
scroll::scroll_amount::ScrollAmount,
BlockId, CodeActionsMenu, CursorShape, CustomBlockId, DisplayPoint, DisplayRow,
DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, EditorSettings,
BlockId, ChunkReplacement, CodeActionsMenu, CursorShape, CustomBlockId, DisplayPoint,
DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, EditorSettings,
EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GutterDimensions, HalfPageDown,
HalfPageUp, HandleInput, HoveredCursor, HoveredHunk, JumpData, LineDown, LineUp, OpenExcerpts,
PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, Selection, SoftWrap, ToPoint,
@@ -34,8 +34,8 @@ use gpui::{
FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Length,
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size,
StatefulInteractiveElement, Style, Styled, TextRun, TextStyle, TextStyleRefinement, View,
ViewContext, WeakView, WindowContext,
StatefulInteractiveElement, Style, Styled, TextRun, TextStyleRefinement, View, ViewContext,
WeakView, WindowContext,
};
use gpui::{ClickEvent, Subscription};
use itertools::Itertools;
@@ -1412,7 +1412,7 @@ impl EditorElement {
}
#[allow(clippy::too_many_arguments)]
fn layout_inline_blame(
fn layout_active_line_trailer(
&self,
display_row: DisplayRow,
display_snapshot: &DisplaySnapshot,
@@ -1424,61 +1424,71 @@ impl EditorElement {
line_height: Pixels,
cx: &mut WindowContext,
) -> Option<AnyElement> {
if !self
let render_inline_blame = self
.editor
.update(cx, |editor, cx| editor.render_git_blame_inline(cx))
{
return None;
}
.update(cx, |editor, cx| editor.render_git_blame_inline(cx));
if render_inline_blame {
let workspace = self
.editor
.read(cx)
.workspace
.as_ref()
.map(|(w, _)| w.clone());
let workspace = self
.editor
.read(cx)
.workspace
.as_ref()
.map(|(w, _)| w.clone());
let display_point = DisplayPoint::new(display_row, 0);
let buffer_row = MultiBufferRow(display_point.to_point(display_snapshot).row);
let display_point = DisplayPoint::new(display_row, 0);
let buffer_row = MultiBufferRow(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 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 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_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 start_x = {
const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 6.;
let line_end = if let Some(crease_trailer) = crease_trailer {
crease_trailer.bounds.right()
} else {
content_origin.x - scroll_pixel_position.x + line_layout.width
};
let padded_line_end = line_end + em_width * INLINE_BLAME_PADDING_EM_WIDTHS;
let line_end = if let Some(crease_trailer) = crease_trailer {
crease_trailer.bounds.right()
} else {
content_origin.x - scroll_pixel_position.x + line_layout.width
let min_column_in_pixels = ProjectSettings::get_global(cx)
.git
.inline_blame
.and_then(|settings| settings.min_column)
.map(|col| self.column_pixels(col as usize, cx))
.unwrap_or(px(0.));
let min_start = content_origin.x - scroll_pixel_position.x + min_column_in_pixels;
cmp::max(padded_line_end, min_start)
};
let padded_line_end = line_end + em_width * INLINE_BLAME_PADDING_EM_WIDTHS;
let min_column_in_pixels = ProjectSettings::get_global(cx)
.git
.inline_blame
.and_then(|settings| settings.min_column)
.map(|col| self.column_pixels(col as usize, cx))
.unwrap_or(px(0.));
let min_start = content_origin.x - scroll_pixel_position.x + min_column_in_pixels;
let absolute_offset = point(start_x, start_y);
element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), cx);
cmp::max(padded_line_end, min_start)
};
Some(element)
} else if let Some(mut element) = self.editor.update(cx, |editor, cx| {
editor.render_active_line_trailer(&self.style, cx)
}) {
let start_y = content_origin.y
+ line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height);
let start_x = content_origin.x - scroll_pixel_position.x + em_width;
let absolute_offset = point(start_x, start_y);
element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), cx);
let absolute_offset = point(start_x, start_y);
element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), cx);
Some(element)
Some(element)
} else {
None
}
}
#[allow(clippy::too_many_arguments)]
@@ -2019,7 +2029,7 @@ impl EditorElement {
let chunks = snapshot.highlighted_chunks(rows.clone(), true, style);
LineWithInvisibles::from_chunks(
chunks,
&style.text,
&style,
MAX_LINE_LEN,
rows.len(),
snapshot.mode,
@@ -3454,7 +3464,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);
self.paint_active_line_trailer(layout, cx);
cx.with_element_namespace("crease_trailers", |cx| {
for trailer in layout.crease_trailers.iter_mut().flatten() {
trailer.element.paint(cx);
@@ -3936,10 +3946,10 @@ impl EditorElement {
}
}
fn paint_inline_blame(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) {
if let Some(mut inline_blame) = layout.inline_blame.take() {
fn paint_active_line_trailer(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) {
if let Some(mut element) = layout.active_line_trailer.take() {
cx.paint_layer(layout.text_hitbox.bounds, |cx| {
inline_blame.paint(cx);
element.paint(cx);
})
}
}
@@ -4372,7 +4382,7 @@ impl LineWithInvisibles {
#[allow(clippy::too_many_arguments)]
fn from_chunks<'a>(
chunks: impl Iterator<Item = HighlightedChunk<'a>>,
text_style: &TextStyle,
editor_style: &EditorStyle,
max_line_len: usize,
max_line_count: usize,
editor_mode: EditorMode,
@@ -4380,6 +4390,7 @@ impl LineWithInvisibles {
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
cx: &mut WindowContext,
) -> Vec<Self> {
let text_style = &editor_style.text;
let mut layouts = Vec::with_capacity(max_line_count);
let mut fragments: SmallVec<[LineFragment; 1]> = SmallVec::new();
let mut line = String::new();
@@ -4398,9 +4409,9 @@ impl LineWithInvisibles {
text: "\n",
style: None,
is_tab: false,
renderer: None,
replacement: None,
}]) {
if let Some(renderer) = highlighted_chunk.renderer {
if let Some(replacement) = highlighted_chunk.replacement {
if !line.is_empty() {
let shaped_line = cx
.text_system()
@@ -4413,42 +4424,71 @@ impl LineWithInvisibles {
styles.clear();
}
let available_width = if renderer.constrain_width {
let chunk = if highlighted_chunk.text == ellipsis.as_ref() {
ellipsis.clone()
} else {
SharedString::from(Arc::from(highlighted_chunk.text))
};
let shaped_line = cx
.text_system()
.shape_line(
chunk,
font_size,
&[text_style.to_run(highlighted_chunk.text.len())],
)
.unwrap();
AvailableSpace::Definite(shaped_line.width)
} else {
AvailableSpace::MinContent
};
match replacement {
ChunkReplacement::Renderer(renderer) => {
let available_width = if renderer.constrain_width {
let chunk = if highlighted_chunk.text == ellipsis.as_ref() {
ellipsis.clone()
} else {
SharedString::from(Arc::from(highlighted_chunk.text))
};
let shaped_line = cx
.text_system()
.shape_line(
chunk,
font_size,
&[text_style.to_run(highlighted_chunk.text.len())],
)
.unwrap();
AvailableSpace::Definite(shaped_line.width)
} else {
AvailableSpace::MinContent
};
let mut element = (renderer.render)(&mut ChunkRendererContext {
context: cx,
max_width: text_width,
});
let line_height = text_style.line_height_in_pixels(cx.rem_size());
let size = element.layout_as_root(
size(available_width, AvailableSpace::Definite(line_height)),
cx,
);
let mut element = (renderer.render)(&mut ChunkRendererContext {
context: cx,
max_width: text_width,
});
let line_height = text_style.line_height_in_pixels(cx.rem_size());
let size = element.layout_as_root(
size(available_width, AvailableSpace::Definite(line_height)),
cx,
);
width += size.width;
len += highlighted_chunk.text.len();
fragments.push(LineFragment::Element {
element: Some(element),
size,
len: highlighted_chunk.text.len(),
});
width += size.width;
len += highlighted_chunk.text.len();
fragments.push(LineFragment::Element {
element: Some(element),
size,
len: highlighted_chunk.text.len(),
});
}
ChunkReplacement::Str(x) => {
let text_style = if let Some(style) = highlighted_chunk.style {
Cow::Owned(text_style.clone().highlight(style))
} else {
Cow::Borrowed(text_style)
};
let run = TextRun {
len: x.len(),
font: text_style.font(),
color: text_style.color,
background_color: text_style.background_color,
underline: text_style.underline,
strikethrough: text_style.strikethrough,
};
let line_layout = cx
.text_system()
.shape_line(x, font_size, &[run])
.unwrap()
.with_len(highlighted_chunk.text.len());
width += line_layout.width;
len += highlighted_chunk.text.len();
fragments.push(LineFragment::Text(line_layout))
}
}
} else {
for (ix, mut line_chunk) in highlighted_chunk.text.split('\n').enumerate() {
if ix > 0 {
@@ -5301,14 +5341,14 @@ impl Element for EditorElement {
)
});
let mut inline_blame = None;
let mut active_line_trailer = 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_ix = display_row.minus(start_row) as usize;
let line_layout = &line_layouts[line_ix];
let crease_trailer_layout = crease_trailers[line_ix].as_ref();
inline_blame = self.layout_inline_blame(
active_line_trailer = self.layout_active_line_trailer(
display_row,
&snapshot.display_snapshot,
line_layout,
@@ -5627,7 +5667,7 @@ impl Element for EditorElement {
line_elements,
line_numbers,
blamed_display_rows,
inline_blame,
active_line_trailer,
blocks,
cursors,
visible_cursors,
@@ -5764,7 +5804,7 @@ pub struct EditorLayout {
line_numbers: Vec<Option<ShapedLine>>,
display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>,
blamed_display_rows: Option<Vec<AnyElement>>,
inline_blame: Option<AnyElement>,
active_line_trailer: Option<AnyElement>,
blocks: Vec<BlockLayout>,
highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
highlighted_gutter_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
@@ -5992,7 +6032,7 @@ fn layout_line(
let chunks = snapshot.highlighted_chunks(row..row + DisplayRow(1), true, style);
LineWithInvisibles::from_chunks(
chunks,
&style.text,
&style,
MAX_LINE_LEN,
1,
snapshot.mode,

View File

@@ -16,7 +16,8 @@ use gpui::{
VisualContext, WeakView, WindowContext,
};
use language::{
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, Point, SelectionGoal,
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, DiskState, Point,
SelectionGoal,
};
use lsp::DiagnosticSeverity;
use multi_buffer::AnchorRangeExt;
@@ -635,12 +636,21 @@ impl Item for Editor {
Some(util::truncate_and_trailoff(description, MAX_TAB_TITLE_LEN))
});
// Whether the file was saved in the past but is now deleted.
let was_deleted: bool = self
.buffer()
.read(cx)
.as_singleton()
.and_then(|buffer| buffer.read(cx).file())
.map_or(false, |file| file.disk_state() == DiskState::Deleted);
h_flex()
.gap_2()
.child(
Label::new(self.title(cx).to_string())
.color(label_color)
.italic(params.preview),
.italic(params.preview)
.strikethrough(was_deleted),
)
.when_some(description, |this, description| {
this.child(
@@ -700,6 +710,10 @@ impl Item for Editor {
self.buffer().read(cx).read(cx).is_dirty()
}
fn has_deleted_file(&self, cx: &AppContext) -> bool {
self.buffer().read(cx).read(cx).has_deleted_file()
}
fn has_conflict(&self, cx: &AppContext) -> bool {
self.buffer().read(cx).read(cx).has_conflict()
}

View File

@@ -3,7 +3,7 @@
use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
use crate::{scroll::ScrollAnchor, CharKind, DisplayRow, EditorStyle, RowExt, ToOffset, ToPoint};
use gpui::{px, Pixels, WindowTextSystem};
use gpui::{Pixels, WindowTextSystem};
use language::Point;
use multi_buffer::{MultiBufferRow, MultiBufferSnapshot};
use serde::Deserialize;
@@ -120,7 +120,7 @@ pub(crate) fn up_by_rows(
preserve_column_at_start: bool,
text_layout_details: &TextLayoutDetails,
) -> (DisplayPoint, SelectionGoal) {
let mut goal_x = match goal {
let goal_x = match goal {
SelectionGoal::HorizontalPosition(x) => x.into(),
SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
SelectionGoal::HorizontalRange { end, .. } => end.into(),
@@ -138,7 +138,6 @@ pub(crate) fn up_by_rows(
return (start, goal);
} else {
point = DisplayPoint::new(DisplayRow(0), 0);
goal_x = px(0.);
}
let mut clipped_point = map.clip_point(point, Bias::Left);
@@ -159,7 +158,7 @@ pub(crate) fn down_by_rows(
preserve_column_at_end: bool,
text_layout_details: &TextLayoutDetails,
) -> (DisplayPoint, SelectionGoal) {
let mut goal_x = match goal {
let goal_x = match goal {
SelectionGoal::HorizontalPosition(x) => x.into(),
SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
SelectionGoal::HorizontalRange { end, .. } => end.into(),
@@ -174,7 +173,6 @@ pub(crate) fn down_by_rows(
return (start, goal);
} else {
point = map.max_point();
goal_x = map.x_for_display_point(point, text_layout_details)
}
let mut clipped_point = map.clip_point(point, Bias::Right);
@@ -610,7 +608,7 @@ mod tests {
test::{editor_test_context::EditorTestContext, marked_display_snapshot},
Buffer, DisplayMap, DisplayRow, ExcerptRange, FoldPlaceholder, InlayId, MultiBuffer,
};
use gpui::{font, Context as _};
use gpui::{font, px, Context as _};
use language::Capability;
use project::Project;
use settings::SettingsStore;
@@ -977,7 +975,7 @@ mod tests {
),
(
DisplayPoint::new(DisplayRow(2), 0),
SelectionGoal::HorizontalPosition(0.0)
SelectionGoal::HorizontalPosition(col_2_x.0),
),
);
assert_eq!(
@@ -990,7 +988,7 @@ mod tests {
),
(
DisplayPoint::new(DisplayRow(2), 0),
SelectionGoal::HorizontalPosition(0.0)
SelectionGoal::HorizontalPosition(0.0),
),
);
@@ -1059,7 +1057,7 @@ mod tests {
let max_point_x = snapshot
.x_for_display_point(DisplayPoint::new(DisplayRow(7), 2), &text_layout_details);
// Can't move down off the end
// Can't move down off the end, and attempting to do so leaves the selection goal unchanged
assert_eq!(
down(
&snapshot,
@@ -1070,7 +1068,7 @@ mod tests {
),
(
DisplayPoint::new(DisplayRow(7), 2),
SelectionGoal::HorizontalPosition(max_point_x.0)
SelectionGoal::HorizontalPosition(0.0)
),
);
assert_eq!(

View File

@@ -1,3 +1,5 @@
use std::{fs, path::Path};
use anyhow::Context as _;
use gpui::{Context, View, ViewContext, VisualContext, WindowContext};
use language::Language;
@@ -7,7 +9,7 @@ use text::ToPointUtf16;
use crate::{
element::register_action, lsp_ext::find_specific_language_server_in_selection, Editor,
ExpandMacroRecursively,
ExpandMacroRecursively, OpenDocs,
};
const RUST_ANALYZER_NAME: &str = "rust-analyzer";
@@ -24,6 +26,7 @@ pub fn apply_related_actions(editor: &View<Editor>, cx: &mut WindowContext) {
.is_some()
{
register_action(editor, cx, expand_macro_recursively);
register_action(editor, cx, open_docs);
}
}
@@ -94,3 +97,64 @@ pub fn expand_macro_recursively(
})
.detach_and_log_err(cx);
}
pub fn open_docs(editor: &mut Editor, _: &OpenDocs, cx: &mut ViewContext<'_, Editor>) {
if editor.selections.count() == 0 {
return;
}
let Some(project) = &editor.project else {
return;
};
let Some(workspace) = editor.workspace() else {
return;
};
let Some((trigger_anchor, _rust_language, server_to_query, buffer)) =
find_specific_language_server_in_selection(
editor,
cx,
is_rust_language,
RUST_ANALYZER_NAME,
)
else {
return;
};
let project = project.clone();
let buffer_snapshot = buffer.read(cx).snapshot();
let position = trigger_anchor.text_anchor.to_point_utf16(&buffer_snapshot);
let open_docs_task = project.update(cx, |project, cx| {
project.request_lsp(
buffer,
project::LanguageServerToQuery::Other(server_to_query),
project::lsp_ext_command::OpenDocs { position },
cx,
)
});
cx.spawn(|_editor, mut cx| async move {
let docs_urls = open_docs_task.await.context("open docs")?;
if docs_urls.is_empty() {
log::debug!("Empty docs urls for position {position:?}");
return Ok(());
} else {
log::debug!("{:?}", docs_urls);
}
workspace.update(&mut cx, |_workspace, cx| {
// Check if the local document exists, otherwise fallback to the online document.
// Open with the default browser.
if let Some(local_url) = docs_urls.local {
if fs::metadata(Path::new(&local_url[8..])).is_ok() {
cx.open_url(&local_url);
return;
}
}
if let Some(web_url) = docs_urls.web {
cx.open_url(&web_url);
}
})
})
.detach_and_log_err(cx);
}

View File

@@ -234,7 +234,16 @@ impl EditorLspTestContext {
..Default::default()
},
Some(tree_sitter_html::language()),
);
)
.with_queries(LanguageQueries {
brackets: Some(Cow::from(indoc! {r#"
("<" @open "/>" @close)
("</" @open ">" @close)
("<" @open ">" @close)
("\"" @open "\"" @close)"#})),
..Default::default()
})
.expect("Could not parse queries");
Self::new(language, Default::default(), cx).await
}

View File

@@ -1,17 +1,20 @@
pub mod extension_builder;
mod extension_manifest;
mod slash_command;
mod types;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use ::lsp::LanguageServerName;
use anyhow::{anyhow, bail, Context as _, Result};
use async_trait::async_trait;
use fs::normalize_path;
use gpui::Task;
use language::LanguageName;
use semantic_version::SemanticVersion;
pub use crate::extension_manifest::*;
pub use crate::slash_command::*;
pub use crate::types::*;
#[async_trait]
pub trait WorktreeDelegate: Send + Sync + 'static {
@@ -34,6 +37,43 @@ pub trait Extension: Send + Sync + 'static {
/// Returns the path to this extension's working directory.
fn work_dir(&self) -> Arc<Path>;
/// Returns a path relative to this extension's working directory.
fn path_from_extension(&self, path: &Path) -> PathBuf {
normalize_path(&self.work_dir().join(path))
}
async fn language_server_command(
&self,
language_server_id: LanguageServerName,
language_name: LanguageName,
worktree: Arc<dyn WorktreeDelegate>,
) -> Result<Command>;
async fn language_server_initialization_options(
&self,
language_server_id: LanguageServerName,
language_name: LanguageName,
worktree: Arc<dyn WorktreeDelegate>,
) -> Result<Option<String>>;
async fn language_server_workspace_configuration(
&self,
language_server_id: LanguageServerName,
worktree: Arc<dyn WorktreeDelegate>,
) -> Result<Option<String>>;
async fn labels_for_completions(
&self,
language_server_id: LanguageServerName,
completions: Vec<Completion>,
) -> Result<Vec<Option<CodeLabel>>>;
async fn labels_for_symbols(
&self,
language_server_id: LanguageServerName,
symbols: Vec<Symbol>,
) -> Result<Vec<Option<CodeLabel>>>;
async fn complete_slash_command_argument(
&self,
command: SlashCommand,
@@ -44,7 +84,7 @@ pub trait Extension: Send + Sync + 'static {
&self,
command: SlashCommand,
arguments: Vec<String>,
resource: Option<Arc<dyn WorktreeDelegate>>,
worktree: Option<Arc<dyn WorktreeDelegate>>,
) -> Result<SlashCommandOutput>;
async fn suggest_docs_packages(&self, provider: Arc<str>) -> Result<Vec<String>>;

View File

@@ -0,0 +1,49 @@
mod lsp;
mod slash_command;
use std::ops::Range;
pub use lsp::*;
pub use slash_command::*;
/// A list of environment variables.
pub type EnvVars = Vec<(String, String)>;
/// A command.
pub struct Command {
/// The command to execute.
pub command: String,
/// The arguments to pass to the command.
pub args: Vec<String>,
/// The environment variables to set for the command.
pub env: EnvVars,
}
/// A label containing some code.
#[derive(Debug, Clone)]
pub struct CodeLabel {
/// The source code to parse with Tree-sitter.
pub code: String,
/// The spans to display in the label.
pub spans: Vec<CodeLabelSpan>,
/// The range of the displayed label to include when filtering.
pub filter_range: Range<usize>,
}
/// A span within a code label.
#[derive(Debug, Clone)]
pub enum CodeLabelSpan {
/// A range into the parsed code.
CodeRange(Range<usize>),
/// A span containing a code literal.
Literal(CodeLabelSpanLiteral),
}
/// A span containing a code literal.
#[derive(Debug, Clone)]
pub struct CodeLabelSpanLiteral {
/// The literal text.
pub text: String,
/// The name of the highlight to use for this literal.
pub highlight_name: Option<String>,
}

View File

@@ -0,0 +1,96 @@
use std::option::Option;
/// An LSP completion.
#[derive(Debug, Clone)]
pub struct Completion {
pub label: String,
pub label_details: Option<CompletionLabelDetails>,
pub detail: Option<String>,
pub kind: Option<CompletionKind>,
pub insert_text_format: Option<InsertTextFormat>,
}
/// The kind of an LSP completion.
#[derive(Debug, Clone, Copy)]
pub enum CompletionKind {
Text,
Method,
Function,
Constructor,
Field,
Variable,
Class,
Interface,
Module,
Property,
Unit,
Value,
Enum,
Keyword,
Snippet,
Color,
File,
Reference,
Folder,
EnumMember,
Constant,
Struct,
Event,
Operator,
TypeParameter,
Other(i32),
}
/// Label details for an LSP completion.
#[derive(Debug, Clone)]
pub struct CompletionLabelDetails {
pub detail: Option<String>,
pub description: Option<String>,
}
/// Defines how to interpret the insert text in a completion item.
#[derive(Debug, Clone, Copy)]
pub enum InsertTextFormat {
PlainText,
Snippet,
Other(i32),
}
/// An LSP symbol.
#[derive(Debug, Clone)]
pub struct Symbol {
pub kind: SymbolKind,
pub name: String,
}
/// The kind of an LSP symbol.
#[derive(Debug, Clone, Copy)]
pub enum SymbolKind {
File,
Module,
Namespace,
Package,
Class,
Method,
Property,
Field,
Constructor,
Enum,
Interface,
Function,
Variable,
Constant,
String,
Number,
Boolean,
Array,
Object,
Key,
Null,
EnumMember,
Struct,
Event,
Operator,
TypeParameter,
Other(i32),
}

View File

@@ -8,9 +8,6 @@ keywords = ["zed", "extension"]
edition = "2021"
license = "Apache-2.0"
# Remove when we're ready to publish v0.2.0.
publish = false
[lints]
workspace = true

View File

@@ -63,6 +63,7 @@ Here is the compatibility of the `zed_extension_api` with versions of Zed:
| Zed version | `zed_extension_api` version |
| ----------- | --------------------------- |
| `0.162.x` | `0.0.1` - `0.2.0` |
| `0.149.x` | `0.0.1` - `0.1.0` |
| `0.131.x` | `0.0.1` - `0.0.6` |
| `0.130.x` | `0.0.1` - `0.0.5` |

View File

@@ -58,3 +58,4 @@ language = { workspace = true, features = ["test-support"] }
parking_lot.workspace = true
project = { workspace = true, features = ["test-support"] }
reqwest_client.workspace = true
theme = { workspace = true, features = ["test-support"] }

View File

@@ -2,7 +2,10 @@ pub mod extension_lsp_adapter;
pub mod extension_settings;
pub mod wasm_host;
use crate::{extension_lsp_adapter::ExtensionLspAdapter, wasm_host::wit};
#[cfg(test)]
mod extension_store_test;
use crate::extension_lsp_adapter::ExtensionLspAdapter;
use anyhow::{anyhow, bail, Context as _, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
@@ -1235,13 +1238,9 @@ impl ExtensionStore {
this.registration_hooks.register_lsp_adapter(
language.clone(),
ExtensionLspAdapter {
extension: wasm_extension.clone(),
host: this.wasm_host.clone(),
extension: extension.clone(),
language_server_id: language_server_id.clone(),
config: wit::LanguageServerConfig {
name: language_server_id.0.to_string(),
language_name: language.to_string(),
},
language_name: language.clone(),
},
);
}

View File

@@ -1,15 +1,12 @@
use crate::wasm_host::{
wit::{self, LanguageServerConfig},
WasmExtension, WasmHost,
};
use anyhow::{anyhow, Context, Result};
use anyhow::{Context, Result};
use async_trait::async_trait;
use collections::HashMap;
use extension::WorktreeDelegate;
use extension::{Extension, WorktreeDelegate};
use futures::{Future, FutureExt};
use gpui::AsyncAppContext;
use language::{
CodeLabel, HighlightId, Language, LanguageToolchainStore, LspAdapter, LspAdapterDelegate,
CodeLabel, HighlightId, Language, LanguageName, LanguageToolchainStore, LspAdapter,
LspAdapterDelegate,
};
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerName};
use serde::Serialize;
@@ -17,7 +14,6 @@ use serde_json::Value;
use std::ops::Range;
use std::{any::Any, path::PathBuf, pin::Pin, sync::Arc};
use util::{maybe, ResultExt};
use wasmtime_wasi::WasiView as _;
/// An adapter that allows an [`LspAdapterDelegate`] to be used as a [`WorktreeDelegate`].
pub struct WorktreeDelegateAdapter(pub Arc<dyn LspAdapterDelegate>);
@@ -49,16 +45,15 @@ impl WorktreeDelegate for WorktreeDelegateAdapter {
}
pub struct ExtensionLspAdapter {
pub(crate) extension: WasmExtension,
pub(crate) extension: Arc<dyn Extension>,
pub(crate) language_server_id: LanguageServerName,
pub(crate) config: LanguageServerConfig,
pub(crate) host: Arc<WasmHost>,
pub(crate) language_name: LanguageName,
}
#[async_trait(?Send)]
impl LspAdapter for ExtensionLspAdapter {
fn name(&self) -> LanguageServerName {
LanguageServerName(self.config.name.clone().into())
self.language_server_id.clone()
}
fn get_language_server_command<'a>(
@@ -69,33 +64,17 @@ impl LspAdapter for ExtensionLspAdapter {
_: &'a mut AsyncAppContext,
) -> Pin<Box<dyn 'a + Future<Output = Result<LanguageServerBinary>>>> {
async move {
let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _;
let command = self
.extension
.call({
let this = self.clone();
|extension, store| {
async move {
let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _;
let resource = store.data_mut().table().push(delegate)?;
let command = extension
.call_language_server_command(
store,
&this.language_server_id,
&this.config,
resource,
)
.await?
.map_err(|e| anyhow!("{}", e))?;
anyhow::Ok(command)
}
.boxed()
}
})
.language_server_command(
self.language_server_id.clone(),
self.language_name.clone(),
delegate,
)
.await?;
let path = self
.host
.path_from_extension(&self.extension.manifest.id, command.command.as_ref());
let path = self.extension.path_from_extension(command.command.as_ref());
// TODO: This should now be done via the `zed::make_file_executable` function in
// Zed extension API, but we're leaving these existing usages in place temporarily
@@ -104,8 +83,8 @@ impl LspAdapter for ExtensionLspAdapter {
// We can remove once the following extension versions no longer see any use:
// - toml@0.0.2
// - zig@0.0.1
if ["toml", "zig"].contains(&self.extension.manifest.id.as_ref())
&& path.starts_with(&self.host.work_dir)
if ["toml", "zig"].contains(&self.extension.manifest().id.as_ref())
&& path.starts_with(&self.extension.work_dir())
{
#[cfg(not(windows))]
{
@@ -153,7 +132,7 @@ impl LspAdapter for ExtensionLspAdapter {
fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
let code_action_kinds = self
.extension
.manifest
.manifest()
.language_servers
.get(&self.language_server_id)
.and_then(|server| server.code_action_kinds.clone());
@@ -174,14 +153,14 @@ impl LspAdapter for ExtensionLspAdapter {
//
// We can remove once the following extension versions no longer see any use:
// - php@0.0.1
if self.extension.manifest.id.as_ref() == "php" {
if self.extension.manifest().id.as_ref() == "php" {
return HashMap::from_iter([("PHP".into(), "php".into())]);
}
self.extension
.manifest
.manifest()
.language_servers
.get(&LanguageServerName(self.config.name.clone().into()))
.get(&self.language_server_id)
.map(|server| server.language_ids.clone())
.unwrap_or_default()
}
@@ -190,29 +169,14 @@ impl LspAdapter for ExtensionLspAdapter {
self: Arc<Self>,
delegate: &Arc<dyn LspAdapterDelegate>,
) -> Result<Option<serde_json::Value>> {
let delegate = delegate.clone();
let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _;
let json_options = self
.extension
.call({
let this = self.clone();
|extension, store| {
async move {
let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _;
let resource = store.data_mut().table().push(delegate)?;
let options = extension
.call_language_server_initialization_options(
store,
&this.language_server_id,
&this.config,
resource,
)
.await?
.map_err(|e| anyhow!("{}", e))?;
anyhow::Ok(options)
}
.boxed()
}
})
.language_server_initialization_options(
self.language_server_id.clone(),
self.language_name.clone(),
delegate,
)
.await?;
Ok(if let Some(json_options) = json_options {
serde_json::from_str(&json_options).with_context(|| {
@@ -229,32 +193,14 @@ impl LspAdapter for ExtensionLspAdapter {
_: Arc<dyn LanguageToolchainStore>,
_cx: &mut AsyncAppContext,
) -> Result<Value> {
let delegate = delegate.clone();
let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _;
let json_options: Option<String> = self
.extension
.call({
let this = self.clone();
|extension, store| {
async move {
let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _;
let resource = store.data_mut().table().push(delegate)?;
let options = extension
.call_language_server_workspace_configuration(
store,
&this.language_server_id,
resource,
)
.await?
.map_err(|e| anyhow!("{}", e))?;
anyhow::Ok(options)
}
.boxed()
}
})
.language_server_workspace_configuration(self.language_server_id.clone(), delegate)
.await?;
Ok(if let Some(json_options) = json_options {
serde_json::from_str(&json_options).with_context(|| {
format!("failed to parse initialization_options from extension: {json_options}")
format!("failed to parse workspace_configuration from extension: {json_options}")
})?
} else {
serde_json::json!({})
@@ -268,30 +214,16 @@ impl LspAdapter for ExtensionLspAdapter {
) -> Result<Vec<Option<CodeLabel>>> {
let completions = completions
.iter()
.map(|completion| wit::Completion::from(completion.clone()))
.cloned()
.map(lsp_completion_to_extension)
.collect::<Vec<_>>();
let labels = self
.extension
.call({
let this = self.clone();
|extension, store| {
async move {
extension
.call_labels_for_completions(
store,
&this.language_server_id,
completions,
)
.await?
.map_err(|e| anyhow!("{}", e))
}
.boxed()
}
})
.labels_for_completions(self.language_server_id.clone(), completions)
.await?;
Ok(labels_from_wit(labels, language))
Ok(labels_from_extension(labels, language))
}
async fn labels_for_symbols(
@@ -302,34 +234,29 @@ impl LspAdapter for ExtensionLspAdapter {
let symbols = symbols
.iter()
.cloned()
.map(|(name, kind)| wit::Symbol {
.map(|(name, kind)| extension::Symbol {
name,
kind: kind.into(),
kind: lsp_symbol_kind_to_extension(kind),
})
.collect::<Vec<_>>();
let labels = self
.extension
.call({
let this = self.clone();
|extension, store| {
async move {
extension
.call_labels_for_symbols(store, &this.language_server_id, symbols)
.await?
.map_err(|e| anyhow!("{}", e))
}
.boxed()
}
})
.labels_for_symbols(self.language_server_id.clone(), symbols)
.await?;
Ok(labels_from_wit(labels, language))
Ok(labels_from_extension(
labels
.into_iter()
.map(|label| label.map(Into::into))
.collect(),
language,
))
}
}
fn labels_from_wit(
labels: Vec<Option<wit::CodeLabel>>,
fn labels_from_extension(
labels: Vec<Option<extension::CodeLabel>>,
language: &Arc<Language>,
) -> Vec<Option<CodeLabel>> {
labels
@@ -347,7 +274,7 @@ fn labels_from_wit(
}
fn build_code_label(
label: &wit::CodeLabel,
label: &extension::CodeLabel,
parsed_runs: &[(Range<usize>, HighlightId)],
language: &Arc<Language>,
) -> Option<CodeLabel> {
@@ -356,8 +283,7 @@ fn build_code_label(
for span in &label.spans {
match span {
wit::CodeLabelSpan::CodeRange(range) => {
let range = Range::from(*range);
extension::CodeLabelSpan::CodeRange(range) => {
let code_span = &label.code.get(range.clone())?;
let mut input_ix = range.start;
let mut output_ix = text.len();
@@ -383,7 +309,7 @@ fn build_code_label(
text.push_str(code_span);
}
wit::CodeLabelSpan::Literal(span) => {
extension::CodeLabelSpan::Literal(span) => {
let highlight_id = language
.grammar()
.zip(span.highlight_name.as_ref())
@@ -398,7 +324,7 @@ fn build_code_label(
}
}
let filter_range = Range::from(label.filter_range);
let filter_range = label.filter_range.clone();
text.get(filter_range.clone())?;
Some(CodeLabel {
text,
@@ -407,109 +333,101 @@ fn build_code_label(
})
}
impl From<wit::Range> for Range<usize> {
fn from(range: wit::Range) -> Self {
let start = range.start as usize;
let end = range.end as usize;
start..end
fn lsp_completion_to_extension(value: lsp::CompletionItem) -> extension::Completion {
extension::Completion {
label: value.label,
label_details: value
.label_details
.map(lsp_completion_item_label_details_to_extension),
detail: value.detail,
kind: value.kind.map(lsp_completion_item_kind_to_extension),
insert_text_format: value
.insert_text_format
.map(lsp_insert_text_format_to_extension),
}
}
impl From<lsp::CompletionItem> for wit::Completion {
fn from(value: lsp::CompletionItem) -> Self {
Self {
label: value.label,
label_details: value.label_details.map(Into::into),
detail: value.detail,
kind: value.kind.map(Into::into),
insert_text_format: value.insert_text_format.map(Into::into),
}
fn lsp_completion_item_label_details_to_extension(
value: lsp::CompletionItemLabelDetails,
) -> extension::CompletionLabelDetails {
extension::CompletionLabelDetails {
detail: value.detail,
description: value.description,
}
}
impl From<lsp::CompletionItemLabelDetails> for wit::CompletionLabelDetails {
fn from(value: lsp::CompletionItemLabelDetails) -> Self {
Self {
detail: value.detail,
description: value.description,
}
fn lsp_completion_item_kind_to_extension(
value: lsp::CompletionItemKind,
) -> extension::CompletionKind {
match value {
lsp::CompletionItemKind::TEXT => extension::CompletionKind::Text,
lsp::CompletionItemKind::METHOD => extension::CompletionKind::Method,
lsp::CompletionItemKind::FUNCTION => extension::CompletionKind::Function,
lsp::CompletionItemKind::CONSTRUCTOR => extension::CompletionKind::Constructor,
lsp::CompletionItemKind::FIELD => extension::CompletionKind::Field,
lsp::CompletionItemKind::VARIABLE => extension::CompletionKind::Variable,
lsp::CompletionItemKind::CLASS => extension::CompletionKind::Class,
lsp::CompletionItemKind::INTERFACE => extension::CompletionKind::Interface,
lsp::CompletionItemKind::MODULE => extension::CompletionKind::Module,
lsp::CompletionItemKind::PROPERTY => extension::CompletionKind::Property,
lsp::CompletionItemKind::UNIT => extension::CompletionKind::Unit,
lsp::CompletionItemKind::VALUE => extension::CompletionKind::Value,
lsp::CompletionItemKind::ENUM => extension::CompletionKind::Enum,
lsp::CompletionItemKind::KEYWORD => extension::CompletionKind::Keyword,
lsp::CompletionItemKind::SNIPPET => extension::CompletionKind::Snippet,
lsp::CompletionItemKind::COLOR => extension::CompletionKind::Color,
lsp::CompletionItemKind::FILE => extension::CompletionKind::File,
lsp::CompletionItemKind::REFERENCE => extension::CompletionKind::Reference,
lsp::CompletionItemKind::FOLDER => extension::CompletionKind::Folder,
lsp::CompletionItemKind::ENUM_MEMBER => extension::CompletionKind::EnumMember,
lsp::CompletionItemKind::CONSTANT => extension::CompletionKind::Constant,
lsp::CompletionItemKind::STRUCT => extension::CompletionKind::Struct,
lsp::CompletionItemKind::EVENT => extension::CompletionKind::Event,
lsp::CompletionItemKind::OPERATOR => extension::CompletionKind::Operator,
lsp::CompletionItemKind::TYPE_PARAMETER => extension::CompletionKind::TypeParameter,
_ => extension::CompletionKind::Other(extract_int(value)),
}
}
impl From<lsp::CompletionItemKind> for wit::CompletionKind {
fn from(value: lsp::CompletionItemKind) -> Self {
match value {
lsp::CompletionItemKind::TEXT => Self::Text,
lsp::CompletionItemKind::METHOD => Self::Method,
lsp::CompletionItemKind::FUNCTION => Self::Function,
lsp::CompletionItemKind::CONSTRUCTOR => Self::Constructor,
lsp::CompletionItemKind::FIELD => Self::Field,
lsp::CompletionItemKind::VARIABLE => Self::Variable,
lsp::CompletionItemKind::CLASS => Self::Class,
lsp::CompletionItemKind::INTERFACE => Self::Interface,
lsp::CompletionItemKind::MODULE => Self::Module,
lsp::CompletionItemKind::PROPERTY => Self::Property,
lsp::CompletionItemKind::UNIT => Self::Unit,
lsp::CompletionItemKind::VALUE => Self::Value,
lsp::CompletionItemKind::ENUM => Self::Enum,
lsp::CompletionItemKind::KEYWORD => Self::Keyword,
lsp::CompletionItemKind::SNIPPET => Self::Snippet,
lsp::CompletionItemKind::COLOR => Self::Color,
lsp::CompletionItemKind::FILE => Self::File,
lsp::CompletionItemKind::REFERENCE => Self::Reference,
lsp::CompletionItemKind::FOLDER => Self::Folder,
lsp::CompletionItemKind::ENUM_MEMBER => Self::EnumMember,
lsp::CompletionItemKind::CONSTANT => Self::Constant,
lsp::CompletionItemKind::STRUCT => Self::Struct,
lsp::CompletionItemKind::EVENT => Self::Event,
lsp::CompletionItemKind::OPERATOR => Self::Operator,
lsp::CompletionItemKind::TYPE_PARAMETER => Self::TypeParameter,
_ => Self::Other(extract_int(value)),
}
fn lsp_insert_text_format_to_extension(
value: lsp::InsertTextFormat,
) -> extension::InsertTextFormat {
match value {
lsp::InsertTextFormat::PLAIN_TEXT => extension::InsertTextFormat::PlainText,
lsp::InsertTextFormat::SNIPPET => extension::InsertTextFormat::Snippet,
_ => extension::InsertTextFormat::Other(extract_int(value)),
}
}
impl From<lsp::InsertTextFormat> for wit::InsertTextFormat {
fn from(value: lsp::InsertTextFormat) -> Self {
match value {
lsp::InsertTextFormat::PLAIN_TEXT => Self::PlainText,
lsp::InsertTextFormat::SNIPPET => Self::Snippet,
_ => Self::Other(extract_int(value)),
}
}
}
impl From<lsp::SymbolKind> for wit::SymbolKind {
fn from(value: lsp::SymbolKind) -> Self {
match value {
lsp::SymbolKind::FILE => Self::File,
lsp::SymbolKind::MODULE => Self::Module,
lsp::SymbolKind::NAMESPACE => Self::Namespace,
lsp::SymbolKind::PACKAGE => Self::Package,
lsp::SymbolKind::CLASS => Self::Class,
lsp::SymbolKind::METHOD => Self::Method,
lsp::SymbolKind::PROPERTY => Self::Property,
lsp::SymbolKind::FIELD => Self::Field,
lsp::SymbolKind::CONSTRUCTOR => Self::Constructor,
lsp::SymbolKind::ENUM => Self::Enum,
lsp::SymbolKind::INTERFACE => Self::Interface,
lsp::SymbolKind::FUNCTION => Self::Function,
lsp::SymbolKind::VARIABLE => Self::Variable,
lsp::SymbolKind::CONSTANT => Self::Constant,
lsp::SymbolKind::STRING => Self::String,
lsp::SymbolKind::NUMBER => Self::Number,
lsp::SymbolKind::BOOLEAN => Self::Boolean,
lsp::SymbolKind::ARRAY => Self::Array,
lsp::SymbolKind::OBJECT => Self::Object,
lsp::SymbolKind::KEY => Self::Key,
lsp::SymbolKind::NULL => Self::Null,
lsp::SymbolKind::ENUM_MEMBER => Self::EnumMember,
lsp::SymbolKind::STRUCT => Self::Struct,
lsp::SymbolKind::EVENT => Self::Event,
lsp::SymbolKind::OPERATOR => Self::Operator,
lsp::SymbolKind::TYPE_PARAMETER => Self::TypeParameter,
_ => Self::Other(extract_int(value)),
}
fn lsp_symbol_kind_to_extension(value: lsp::SymbolKind) -> extension::SymbolKind {
match value {
lsp::SymbolKind::FILE => extension::SymbolKind::File,
lsp::SymbolKind::MODULE => extension::SymbolKind::Module,
lsp::SymbolKind::NAMESPACE => extension::SymbolKind::Namespace,
lsp::SymbolKind::PACKAGE => extension::SymbolKind::Package,
lsp::SymbolKind::CLASS => extension::SymbolKind::Class,
lsp::SymbolKind::METHOD => extension::SymbolKind::Method,
lsp::SymbolKind::PROPERTY => extension::SymbolKind::Property,
lsp::SymbolKind::FIELD => extension::SymbolKind::Field,
lsp::SymbolKind::CONSTRUCTOR => extension::SymbolKind::Constructor,
lsp::SymbolKind::ENUM => extension::SymbolKind::Enum,
lsp::SymbolKind::INTERFACE => extension::SymbolKind::Interface,
lsp::SymbolKind::FUNCTION => extension::SymbolKind::Function,
lsp::SymbolKind::VARIABLE => extension::SymbolKind::Variable,
lsp::SymbolKind::CONSTANT => extension::SymbolKind::Constant,
lsp::SymbolKind::STRING => extension::SymbolKind::String,
lsp::SymbolKind::NUMBER => extension::SymbolKind::Number,
lsp::SymbolKind::BOOLEAN => extension::SymbolKind::Boolean,
lsp::SymbolKind::ARRAY => extension::SymbolKind::Array,
lsp::SymbolKind::OBJECT => extension::SymbolKind::Object,
lsp::SymbolKind::KEY => extension::SymbolKind::Key,
lsp::SymbolKind::NULL => extension::SymbolKind::Null,
lsp::SymbolKind::ENUM_MEMBER => extension::SymbolKind::EnumMember,
lsp::SymbolKind::STRUCT => extension::SymbolKind::Struct,
lsp::SymbolKind::EVENT => extension::SymbolKind::Event,
lsp::SymbolKind::OPERATOR => extension::SymbolKind::Operator,
lsp::SymbolKind::TYPE_PARAMETER => extension::SymbolKind::TypeParameter,
_ => extension::SymbolKind::Other(extract_int(value)),
}
}
@@ -536,21 +454,14 @@ fn test_build_code_label() {
.collect::<Vec<_>>();
let label = build_code_label(
&wit::CodeLabel {
&extension::CodeLabel {
spans: vec![
wit::CodeLabelSpan::CodeRange(wit::Range {
start: code.find("pqrs").unwrap() as u32,
end: code.len() as u32,
}),
wit::CodeLabelSpan::CodeRange(wit::Range {
start: code.find(": fn").unwrap() as u32,
end: code.find(" = ").unwrap() as u32,
}),
extension::CodeLabelSpan::CodeRange(code.find("pqrs").unwrap()..code.len()),
extension::CodeLabelSpan::CodeRange(
code.find(": fn").unwrap()..code.find(" = ").unwrap(),
),
],
filter_range: wit::Range {
start: 0,
end: "pqrs.tuv".len() as u32,
},
filter_range: 0.."pqrs.tuv".len(),
code,
},
&code_runs,
@@ -588,21 +499,14 @@ fn test_build_code_label_with_invalid_ranges() {
// A span uses a code range that is invalid because it starts inside of
// a multi-byte character.
let label = build_code_label(
&wit::CodeLabel {
&extension::CodeLabel {
spans: vec![
wit::CodeLabelSpan::CodeRange(wit::Range {
start: code.find('B').unwrap() as u32,
end: code.find(" = ").unwrap() as u32,
}),
wit::CodeLabelSpan::CodeRange(wit::Range {
start: code.find('🏀').unwrap() as u32 + 1,
end: code.len() as u32,
}),
extension::CodeLabelSpan::CodeRange(
code.find('B').unwrap()..code.find(" = ").unwrap(),
),
extension::CodeLabelSpan::CodeRange((code.find('🏀').unwrap() + 1)..code.len()),
],
filter_range: wit::Range {
start: 0,
end: "B".len() as u32,
},
filter_range: 0.."B".len(),
code,
},
&code_runs,
@@ -612,12 +516,14 @@ fn test_build_code_label_with_invalid_ranges() {
// Filter range extends beyond actual text
let label = build_code_label(
&wit::CodeLabel {
spans: vec![wit::CodeLabelSpan::Literal(wit::CodeLabelSpanLiteral {
text: "abc".into(),
highlight_name: Some("type".into()),
})],
filter_range: wit::Range { start: 0, end: 5 },
&extension::CodeLabel {
spans: vec![extension::CodeLabelSpan::Literal(
extension::CodeLabelSpanLiteral {
text: "abc".into(),
highlight_name: Some("type".into()),
},
)],
filter_range: 0..5,
code: String::new(),
},
&code_runs,

View File

@@ -1,20 +1,17 @@
use assistant_slash_command::SlashCommandRegistry;
use crate::extension_lsp_adapter::ExtensionLspAdapter;
use crate::{
Event, ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry,
ExtensionIndexThemeEntry, ExtensionManifest, ExtensionSettings, ExtensionStore,
GrammarManifestEntry, SchemaVersion, RELOAD_DEBOUNCE_DURATION,
};
use anyhow::Result;
use async_compression::futures::bufread::GzipEncoder;
use collections::BTreeMap;
use context_servers::ContextServerFactoryRegistry;
use extension_host::ExtensionSettings;
use extension_host::SchemaVersion;
use extension_host::{
Event, ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry,
ExtensionIndexThemeEntry, ExtensionManifest, ExtensionStore, GrammarManifestEntry,
RELOAD_DEBOUNCE_DURATION,
};
use fs::{FakeFs, Fs, RealFs};
use futures::{io::BufReader, AsyncReadExt, StreamExt};
use gpui::{Context, SemanticVersion, TestAppContext};
use gpui::{BackgroundExecutor, Context, SemanticVersion, SharedString, Task, TestAppContext};
use http_client::{FakeHttpClient, Response};
use indexed_docs::IndexedDocsRegistry;
use language::{LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus};
use language::{LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus, LoadedLanguage};
use lsp::LanguageServerName;
use node_runtime::NodeRuntime;
use parking_lot::Mutex;
@@ -23,7 +20,6 @@ use release_channel::AppVersion;
use reqwest_client::ReqwestClient;
use serde_json::json;
use settings::{Settings as _, SettingsStore};
use snippet_provider::SnippetRegistry;
use std::{
ffi::OsString,
path::{Path, PathBuf},
@@ -32,6 +28,84 @@ use std::{
use theme::ThemeRegistry;
use util::test::temp_tree;
use crate::ExtensionRegistrationHooks;
struct TestExtensionRegistrationHooks {
executor: BackgroundExecutor,
language_registry: Arc<LanguageRegistry>,
theme_registry: Arc<ThemeRegistry>,
}
impl ExtensionRegistrationHooks for TestExtensionRegistrationHooks {
fn list_theme_names(&self, path: PathBuf, fs: Arc<dyn Fs>) -> Task<Result<Vec<String>>> {
self.executor.spawn(async move {
let themes = theme::read_user_theme(&path, fs).await?;
Ok(themes.themes.into_iter().map(|theme| theme.name).collect())
})
}
fn load_user_theme(&self, theme_path: PathBuf, fs: Arc<dyn fs::Fs>) -> Task<Result<()>> {
let theme_registry = self.theme_registry.clone();
self.executor
.spawn(async move { theme_registry.load_user_theme(&theme_path, fs).await })
}
fn remove_user_themes(&self, themes: Vec<SharedString>) {
self.theme_registry.remove_user_themes(&themes);
}
fn register_language(
&self,
language: language::LanguageName,
grammar: Option<Arc<str>>,
matcher: language::LanguageMatcher,
load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>,
) {
self.language_registry
.register_language(language, grammar, matcher, load)
}
fn remove_languages(
&self,
languages_to_remove: &[language::LanguageName],
grammars_to_remove: &[Arc<str>],
) {
self.language_registry
.remove_languages(&languages_to_remove, &grammars_to_remove);
}
fn register_wasm_grammars(&self, grammars: Vec<(Arc<str>, PathBuf)>) {
self.language_registry.register_wasm_grammars(grammars)
}
fn register_lsp_adapter(
&self,
language_name: language::LanguageName,
adapter: ExtensionLspAdapter,
) {
self.language_registry
.register_lsp_adapter(language_name, Arc::new(adapter));
}
fn update_lsp_status(
&self,
server_name: lsp::LanguageServerName,
status: LanguageServerBinaryStatus,
) {
self.language_registry
.update_lsp_status(server_name, status);
}
fn remove_lsp_adapter(
&self,
language_name: &language::LanguageName,
server_name: &lsp::LanguageServerName,
) {
self.language_registry
.remove_lsp_adapter(language_name, server_name);
}
}
#[cfg(test)]
#[ctor::ctor]
fn init_logger() {
@@ -265,27 +339,18 @@ async fn test_extension_store(cx: &mut TestAppContext) {
let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
let slash_command_registry = SlashCommandRegistry::new();
let indexed_docs_registry = Arc::new(IndexedDocsRegistry::new(cx.executor()));
let snippet_registry = Arc::new(SnippetRegistry::new());
let context_server_factory_registry = cx.new_model(|_| ContextServerFactoryRegistry::new());
let registration_hooks = Arc::new(TestExtensionRegistrationHooks {
executor: cx.executor(),
language_registry: language_registry.clone(),
theme_registry: theme_registry.clone(),
});
let node_runtime = NodeRuntime::unavailable();
let store = cx.new_model(|cx| {
let extension_registration_hooks = crate::ConcreteExtensionRegistrationHooks::new(
theme_registry.clone(),
slash_command_registry.clone(),
indexed_docs_registry.clone(),
snippet_registry.clone(),
language_registry.clone(),
context_server_factory_registry.clone(),
cx,
);
ExtensionStore::new(
PathBuf::from("/the-extension-dir"),
None,
extension_registration_hooks,
registration_hooks.clone(),
fs.clone(),
http_client.clone(),
http_client.clone(),
@@ -407,20 +472,10 @@ async fn test_extension_store(cx: &mut TestAppContext) {
// Create new extension store, as if Zed were restarting.
drop(store);
let store = cx.new_model(|cx| {
let extension_api = crate::ConcreteExtensionRegistrationHooks::new(
theme_registry.clone(),
slash_command_registry,
indexed_docs_registry,
snippet_registry,
language_registry.clone(),
context_server_factory_registry.clone(),
cx,
);
ExtensionStore::new(
PathBuf::from("/the-extension-dir"),
None,
extension_api,
registration_hooks,
fs.clone(),
http_client.clone(),
http_client.clone(),
@@ -505,10 +560,11 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
let slash_command_registry = SlashCommandRegistry::new();
let indexed_docs_registry = Arc::new(IndexedDocsRegistry::new(cx.executor()));
let snippet_registry = Arc::new(SnippetRegistry::new());
let context_server_factory_registry = cx.new_model(|_| ContextServerFactoryRegistry::new());
let registration_hooks = Arc::new(TestExtensionRegistrationHooks {
executor: cx.executor(),
language_registry: language_registry.clone(),
theme_registry: theme_registry.clone(),
});
let node_runtime = NodeRuntime::unavailable();
let mut status_updates = language_registry.language_server_binary_statuses();
@@ -599,19 +655,10 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
Arc::new(ReqwestClient::user_agent(&user_agent).expect("Could not create HTTP client"));
let extension_store = cx.new_model(|cx| {
let extension_api = crate::ConcreteExtensionRegistrationHooks::new(
theme_registry.clone(),
slash_command_registry,
indexed_docs_registry,
snippet_registry,
language_registry.clone(),
context_server_factory_registry.clone(),
cx,
);
ExtensionStore::new(
extensions_dir.clone(),
Some(cache_dir),
extension_api,
registration_hooks,
fs.clone(),
extension_client.clone(),
builder_client,
@@ -626,7 +673,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
let executor = cx.executor();
let _task = cx.executor().spawn(async move {
while let Some(event) = events.next().await {
if let extension_host::Event::StartedReloading = event {
if let Event::StartedReloading = event {
executor.advance_clock(RELOAD_DEBOUNCE_DURATION);
}
}

View File

@@ -4,8 +4,8 @@ use crate::{ExtensionManifest, ExtensionRegistrationHooks};
use anyhow::{anyhow, bail, Context as _, Result};
use async_trait::async_trait;
use extension::{
KeyValueStoreDelegate, SlashCommand, SlashCommandArgumentCompletion, SlashCommandOutput,
WorktreeDelegate,
CodeLabel, Command, Completion, KeyValueStoreDelegate, SlashCommand,
SlashCommandArgumentCompletion, SlashCommandOutput, Symbol, WorktreeDelegate,
};
use fs::{normalize_path, Fs};
use futures::future::LocalBoxFuture;
@@ -19,6 +19,8 @@ use futures::{
};
use gpui::{AppContext, AsyncAppContext, BackgroundExecutor, Task};
use http_client::HttpClient;
use language::LanguageName;
use lsp::LanguageServerName;
use node_runtime::NodeRuntime;
use release_channel::ReleaseChannel;
use semantic_version::SemanticVersion;
@@ -65,6 +67,132 @@ impl extension::Extension for WasmExtension {
self.work_dir.clone()
}
async fn language_server_command(
&self,
language_server_id: LanguageServerName,
language_name: LanguageName,
worktree: Arc<dyn WorktreeDelegate>,
) -> Result<Command> {
self.call(|extension, store| {
async move {
let resource = store.data_mut().table().push(worktree)?;
let command = extension
.call_language_server_command(
store,
&language_server_id,
&language_name,
resource,
)
.await?
.map_err(|err| anyhow!("{err}"))?;
Ok(command.into())
}
.boxed()
})
.await
}
async fn language_server_initialization_options(
&self,
language_server_id: LanguageServerName,
language_name: LanguageName,
worktree: Arc<dyn WorktreeDelegate>,
) -> Result<Option<String>> {
self.call(|extension, store| {
async move {
let resource = store.data_mut().table().push(worktree)?;
let options = extension
.call_language_server_initialization_options(
store,
&language_server_id,
&language_name,
resource,
)
.await?
.map_err(|err| anyhow!("{err}"))?;
anyhow::Ok(options)
}
.boxed()
})
.await
}
async fn language_server_workspace_configuration(
&self,
language_server_id: LanguageServerName,
worktree: Arc<dyn WorktreeDelegate>,
) -> Result<Option<String>> {
self.call(|extension, store| {
async move {
let resource = store.data_mut().table().push(worktree)?;
let options = extension
.call_language_server_workspace_configuration(
store,
&language_server_id,
resource,
)
.await?
.map_err(|err| anyhow!("{err}"))?;
anyhow::Ok(options)
}
.boxed()
})
.await
}
async fn labels_for_completions(
&self,
language_server_id: LanguageServerName,
completions: Vec<Completion>,
) -> Result<Vec<Option<CodeLabel>>> {
self.call(|extension, store| {
async move {
let labels = extension
.call_labels_for_completions(
store,
&language_server_id,
completions.into_iter().map(Into::into).collect(),
)
.await?
.map_err(|err| anyhow!("{err}"))?;
Ok(labels
.into_iter()
.map(|label| label.map(Into::into))
.collect())
}
.boxed()
})
.await
}
async fn labels_for_symbols(
&self,
language_server_id: LanguageServerName,
symbols: Vec<Symbol>,
) -> Result<Vec<Option<CodeLabel>>> {
self.call(|extension, store| {
async move {
let labels = extension
.call_labels_for_symbols(
store,
&language_server_id,
symbols.into_iter().map(Into::into).collect(),
)
.await?
.map_err(|err| anyhow!("{err}"))?;
Ok(labels
.into_iter()
.map(|label| label.map(Into::into))
.collect())
}
.boxed()
})
.await
}
async fn complete_slash_command_argument(
&self,
command: SlashCommand,
@@ -255,7 +383,7 @@ impl WasmHost {
Ok(WasmExtension {
manifest: manifest.clone(),
work_dir: this.work_dir.clone().into(),
work_dir: this.work_dir.join(manifest.id.as_ref()).into(),
tx,
zed_api_version,
})
@@ -286,11 +414,6 @@ impl WasmHost {
.build())
}
pub fn path_from_extension(&self, id: &Arc<str>, path: &Path) -> PathBuf {
let extension_work_dir = self.work_dir.join(id.as_ref());
normalize_path(&extension_work_dir.join(path))
}
pub fn writeable_path_from_extension(&self, id: &Arc<str>, path: &Path) -> Result<PathBuf> {
let extension_work_dir = self.work_dir.join(id.as_ref());
let path = normalize_path(&extension_work_dir.join(path));

View File

@@ -4,6 +4,7 @@ mod since_v0_0_6;
mod since_v0_1_0;
mod since_v0_2_0;
use extension::{KeyValueStoreDelegate, WorktreeDelegate};
use language::LanguageName;
use lsp::LanguageServerName;
use release_channel::ReleaseChannel;
use since_v0_2_0 as latest;
@@ -57,12 +58,35 @@ pub fn wasm_api_version_range(release_channel: ReleaseChannel) -> RangeInclusive
let max_version = match release_channel {
ReleaseChannel::Dev | ReleaseChannel::Nightly => latest::MAX_VERSION,
ReleaseChannel::Stable | ReleaseChannel::Preview => since_v0_1_0::MAX_VERSION,
ReleaseChannel::Stable | ReleaseChannel::Preview => latest::MAX_VERSION,
};
since_v0_0_1::MIN_VERSION..=max_version
}
/// Authorizes access to use unreleased versions of the Wasm API, based on the provided [`ReleaseChannel`].
///
/// Note: If there isn't currently an unreleased Wasm API version this function may be unused. Don't delete it!
pub fn authorize_access_to_unreleased_wasm_api_version(
release_channel: ReleaseChannel,
) -> Result<()> {
let allow_unreleased_version = match release_channel {
ReleaseChannel::Dev | ReleaseChannel::Nightly => true,
ReleaseChannel::Stable | ReleaseChannel::Preview => {
// We always allow the latest in tests so that the extension tests pass on release branches.
cfg!(any(test, feature = "test-support"))
}
};
if !allow_unreleased_version {
Err(anyhow!(
"unreleased versions of the extension API can only be used on development builds of Zed"
))?;
}
Ok(())
}
pub enum Extension {
V020(since_v0_2_0::Extension),
V010(since_v0_1_0::Extension),
@@ -78,20 +102,10 @@ impl Extension {
version: SemanticVersion,
component: &Component,
) -> Result<Self> {
// Note: The release channel can be used to stage a new version of the extension API.
let _ = release_channel;
if version >= latest::MIN_VERSION {
// Note: The release channel can be used to stage a new version of the extension API.
// We always allow the latest in tests so that the extension tests pass on release branches.
let allow_latest_version = match release_channel {
ReleaseChannel::Dev | ReleaseChannel::Nightly => true,
ReleaseChannel::Stable | ReleaseChannel::Preview => {
cfg!(any(test, feature = "test-support"))
}
};
if !allow_latest_version {
Err(anyhow!(
"unreleased versions of the extension API can only be used on development builds of Zed"
))?;
}
let extension =
latest::Extension::instantiate_async(store, component, latest::linker())
.await
@@ -150,7 +164,7 @@ impl Extension {
&self,
store: &mut Store<WasmState>,
language_server_id: &LanguageServerName,
config: &LanguageServerConfig,
language_name: &LanguageName,
resource: Resource<Arc<dyn WorktreeDelegate>>,
) -> Result<Result<Command, String>> {
match self {
@@ -167,11 +181,26 @@ impl Extension {
.await?
.map(|command| command.into())),
Extension::V004(ext) => Ok(ext
.call_language_server_command(store, config, resource)
.call_language_server_command(
store,
&LanguageServerConfig {
name: language_server_id.0.to_string(),
language_name: language_name.to_string(),
},
resource,
)
.await?
.map(|command| command.into())),
Extension::V001(ext) => Ok(ext
.call_language_server_command(store, &config.clone().into(), resource)
.call_language_server_command(
store,
&LanguageServerConfig {
name: language_server_id.0.to_string(),
language_name: language_name.to_string(),
}
.into(),
resource,
)
.await?
.map(|command| command.into())),
}
@@ -181,7 +210,7 @@ impl Extension {
&self,
store: &mut Store<WasmState>,
language_server_id: &LanguageServerName,
config: &LanguageServerConfig,
language_name: &LanguageName,
resource: Resource<Arc<dyn WorktreeDelegate>>,
) -> Result<Result<Option<String>, String>> {
match self {
@@ -210,13 +239,24 @@ impl Extension {
.await
}
Extension::V004(ext) => {
ext.call_language_server_initialization_options(store, config, resource)
.await
ext.call_language_server_initialization_options(
store,
&LanguageServerConfig {
name: language_server_id.0.to_string(),
language_name: language_name.to_string(),
},
resource,
)
.await
}
Extension::V001(ext) => {
ext.call_language_server_initialization_options(
store,
&config.clone().into(),
&LanguageServerConfig {
name: language_server_id.0.to_string(),
language_name: language_name.to_string(),
}
.into(),
resource,
)
.await

View File

@@ -22,7 +22,6 @@ use wasmtime::component::{Linker, Resource};
use super::latest;
pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 1, 0);
pub const MAX_VERSION: SemanticVersion = SemanticVersion::new(0, 1, 0);
wasmtime::component::bindgen!({
async: true,

View File

@@ -1,4 +1,5 @@
use crate::wasm_host::wit::since_v0_2_0::slash_command::SlashCommandOutputSection;
use crate::wasm_host::wit::{CompletionKind, CompletionLabelDetails, InsertTextFormat, SymbolKind};
use crate::wasm_host::{wit::ToWasmtimeResult, WasmState};
use ::http_client::{AsyncBody, HttpRequestExt};
use ::settings::{Settings, WorktreeId};
@@ -55,6 +56,159 @@ pub fn linker() -> &'static Linker<WasmState> {
LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker))
}
impl From<Range> for std::ops::Range<usize> {
fn from(range: Range) -> Self {
let start = range.start as usize;
let end = range.end as usize;
start..end
}
}
impl From<Command> for extension::Command {
fn from(value: Command) -> Self {
Self {
command: value.command,
args: value.args,
env: value.env,
}
}
}
impl From<CodeLabel> for extension::CodeLabel {
fn from(value: CodeLabel) -> Self {
Self {
code: value.code,
spans: value.spans.into_iter().map(Into::into).collect(),
filter_range: value.filter_range.into(),
}
}
}
impl From<CodeLabelSpan> for extension::CodeLabelSpan {
fn from(value: CodeLabelSpan) -> Self {
match value {
CodeLabelSpan::CodeRange(range) => Self::CodeRange(range.into()),
CodeLabelSpan::Literal(literal) => Self::Literal(literal.into()),
}
}
}
impl From<CodeLabelSpanLiteral> for extension::CodeLabelSpanLiteral {
fn from(value: CodeLabelSpanLiteral) -> Self {
Self {
text: value.text,
highlight_name: value.highlight_name,
}
}
}
impl From<extension::Completion> for Completion {
fn from(value: extension::Completion) -> Self {
Self {
label: value.label,
label_details: value.label_details.map(Into::into),
detail: value.detail,
kind: value.kind.map(Into::into),
insert_text_format: value.insert_text_format.map(Into::into),
}
}
}
impl From<extension::CompletionLabelDetails> for CompletionLabelDetails {
fn from(value: extension::CompletionLabelDetails) -> Self {
Self {
detail: value.detail,
description: value.description,
}
}
}
impl From<extension::CompletionKind> for CompletionKind {
fn from(value: extension::CompletionKind) -> Self {
match value {
extension::CompletionKind::Text => Self::Text,
extension::CompletionKind::Method => Self::Method,
extension::CompletionKind::Function => Self::Function,
extension::CompletionKind::Constructor => Self::Constructor,
extension::CompletionKind::Field => Self::Field,
extension::CompletionKind::Variable => Self::Variable,
extension::CompletionKind::Class => Self::Class,
extension::CompletionKind::Interface => Self::Interface,
extension::CompletionKind::Module => Self::Module,
extension::CompletionKind::Property => Self::Property,
extension::CompletionKind::Unit => Self::Unit,
extension::CompletionKind::Value => Self::Value,
extension::CompletionKind::Enum => Self::Enum,
extension::CompletionKind::Keyword => Self::Keyword,
extension::CompletionKind::Snippet => Self::Snippet,
extension::CompletionKind::Color => Self::Color,
extension::CompletionKind::File => Self::File,
extension::CompletionKind::Reference => Self::Reference,
extension::CompletionKind::Folder => Self::Folder,
extension::CompletionKind::EnumMember => Self::EnumMember,
extension::CompletionKind::Constant => Self::Constant,
extension::CompletionKind::Struct => Self::Struct,
extension::CompletionKind::Event => Self::Event,
extension::CompletionKind::Operator => Self::Operator,
extension::CompletionKind::TypeParameter => Self::TypeParameter,
extension::CompletionKind::Other(value) => Self::Other(value),
}
}
}
impl From<extension::InsertTextFormat> for InsertTextFormat {
fn from(value: extension::InsertTextFormat) -> Self {
match value {
extension::InsertTextFormat::PlainText => Self::PlainText,
extension::InsertTextFormat::Snippet => Self::Snippet,
extension::InsertTextFormat::Other(value) => Self::Other(value),
}
}
}
impl From<extension::Symbol> for Symbol {
fn from(value: extension::Symbol) -> Self {
Self {
kind: value.kind.into(),
name: value.name,
}
}
}
impl From<extension::SymbolKind> for SymbolKind {
fn from(value: extension::SymbolKind) -> Self {
match value {
extension::SymbolKind::File => Self::File,
extension::SymbolKind::Module => Self::Module,
extension::SymbolKind::Namespace => Self::Namespace,
extension::SymbolKind::Package => Self::Package,
extension::SymbolKind::Class => Self::Class,
extension::SymbolKind::Method => Self::Method,
extension::SymbolKind::Property => Self::Property,
extension::SymbolKind::Field => Self::Field,
extension::SymbolKind::Constructor => Self::Constructor,
extension::SymbolKind::Enum => Self::Enum,
extension::SymbolKind::Interface => Self::Interface,
extension::SymbolKind::Function => Self::Function,
extension::SymbolKind::Variable => Self::Variable,
extension::SymbolKind::Constant => Self::Constant,
extension::SymbolKind::String => Self::String,
extension::SymbolKind::Number => Self::Number,
extension::SymbolKind::Boolean => Self::Boolean,
extension::SymbolKind::Array => Self::Array,
extension::SymbolKind::Object => Self::Object,
extension::SymbolKind::Key => Self::Key,
extension::SymbolKind::Null => Self::Null,
extension::SymbolKind::EnumMember => Self::EnumMember,
extension::SymbolKind::Struct => Self::Struct,
extension::SymbolKind::Event => Self::Event,
extension::SymbolKind::Operator => Self::Operator,
extension::SymbolKind::TypeParameter => Self::TypeParameter,
extension::SymbolKind::Other(value) => Self::Other(value),
}
}
}
impl From<extension::SlashCommand> for SlashCommand {
fn from(value: extension::SlashCommand) -> Self {
Self {

View File

@@ -11,9 +11,6 @@ workspace = true
[lib]
path = "src/extensions_ui.rs"
[features]
test-support = []
[dependencies]
anyhow.workspace = true
assistant_slash_command.workspace = true
@@ -25,7 +22,6 @@ editor.workspace = true
extension.workspace = true
extension_host.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
indexed_docs.workspace = true
@@ -50,21 +46,4 @@ wasmtime-wasi.workspace = true
workspace.workspace = true
[dev-dependencies]
async-compression.workspace = true
async-tar.workspace = true
ctor.workspace = true
editor = { workspace = true, features = ["test-support"] }
env_logger.workspace = true
extension_host = {workspace = true, features = ["test-support"] }
fs = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
http_client.workspace = true
indexed_docs.workspace = true
language = { workspace = true, features = ["test-support"] }
lsp.workspace = true
node_runtime.workspace = true
parking_lot.workspace = true
project = { workspace = true, features = ["test-support"] }
reqwest_client.workspace = true
serde_json.workspace = true
workspace = { workspace = true, features = ["test-support"] }

View File

@@ -3,9 +3,6 @@ mod extension_registration_hooks;
mod extension_suggest;
mod extension_version_selector;
#[cfg(test)]
mod extension_store_test;
pub use extension_registration_hooks::ConcreteExtensionRegistrationHooks;
use std::ops::DerefMut;

View File

@@ -10,11 +10,11 @@ pub use open_path_prompt::OpenPathDelegate;
use collections::HashMap;
use editor::{scroll::Autoscroll, Bias, Editor};
use file_finder_settings::FileFinderSettings;
use file_finder_settings::{FileFinderSettings, FileFinderWidth};
use file_icons::FileIcons;
use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
use gpui::{
actions, rems, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle,
actions, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle,
FocusableView, KeyContext, Model, Modifiers, ModifiersChangedEvent, ParentElement, Render,
Styled, Task, View, ViewContext, VisualContext, WeakView,
};
@@ -244,6 +244,22 @@ impl FileFinder {
}
})
}
pub fn modal_max_width(
width_setting: Option<FileFinderWidth>,
cx: &mut ViewContext<Self>,
) -> Pixels {
let window_width = cx.viewport_size().width;
let small_width = Pixels(545.);
match width_setting {
None | Some(FileFinderWidth::Small) => small_width,
Some(FileFinderWidth::Full) => window_width,
Some(FileFinderWidth::XLarge) => (window_width - Pixels(512.)).max(small_width),
Some(FileFinderWidth::Large) => (window_width - Pixels(768.)).max(small_width),
Some(FileFinderWidth::Medium) => (window_width - Pixels(1024.)).max(small_width),
}
}
}
impl EventEmitter<DismissEvent> for FileFinder {}
@@ -257,9 +273,13 @@ impl FocusableView for FileFinder {
impl Render for FileFinder {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let key_context = self.picker.read(cx).delegate.key_context(cx);
let file_finder_settings = FileFinderSettings::get_global(cx);
let modal_max_width = Self::modal_max_width(file_finder_settings.modal_max_width, cx);
v_flex()
.key_context(key_context)
.w(rems(34.))
.w(modal_max_width)
.on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
.on_action(cx.listener(Self::handle_select_prev))
.on_action(cx.listener(Self::handle_open_menu))

View File

@@ -6,6 +6,7 @@ use settings::{Settings, SettingsSources};
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
pub struct FileFinderSettings {
pub file_icons: bool,
pub modal_max_width: Option<FileFinderWidth>,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
@@ -14,6 +15,10 @@ pub struct FileFinderSettingsContent {
///
/// Default: true
pub file_icons: Option<bool>,
/// Determines how much space the file finder can take up in relation to the available window width.
///
/// Default: small
pub modal_max_width: Option<FileFinderWidth>,
}
impl Settings for FileFinderSettings {
@@ -25,3 +30,14 @@ impl Settings for FileFinderSettings {
sources.json_merge()
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum FileFinderWidth {
#[default]
Small,
Medium,
Large,
XLarge,
Full,
}

View File

@@ -2,9 +2,8 @@ mod supported_countries;
use anyhow::{anyhow, Result};
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt};
use http_client::{AsyncBody, HttpClient, HttpRequestExt, Method, Request as HttpRequest};
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
use serde::{Deserialize, Serialize};
use std::time::Duration;
pub use supported_countries::*;
@@ -15,7 +14,6 @@ pub async fn stream_generate_content(
api_url: &str,
api_key: &str,
mut request: GenerateContentRequest,
low_speed_timeout: Option<Duration>,
) -> Result<BoxStream<'static, Result<GenerateContentResponse>>> {
let uri = format!(
"{api_url}/v1beta/models/{model}:streamGenerateContent?alt=sse&key={api_key}",
@@ -23,15 +21,11 @@ pub async fn stream_generate_content(
);
request.model.clear();
let mut request_builder = HttpRequest::builder()
let request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(uri)
.header("Content-Type", "application/json");
if let Some(low_speed_timeout) = low_speed_timeout {
request_builder = request_builder.read_timeout(low_speed_timeout);
};
let request = request_builder.body(AsyncBody::from(serde_json::to_string(&request)?))?;
let mut response = client.send(request).await?;
if response.status().is_success() {
@@ -70,7 +64,6 @@ pub async fn count_tokens(
api_url: &str,
api_key: &str,
request: CountTokensRequest,
low_speed_timeout: Option<Duration>,
) -> Result<CountTokensResponse> {
let uri = format!(
"{}/v1beta/models/gemini-pro:countTokens?key={}",
@@ -78,15 +71,11 @@ pub async fn count_tokens(
);
let request = serde_json::to_string(&request)?;
let mut request_builder = HttpRequest::builder()
let request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(&uri)
.header("Content-Type", "application/json");
if let Some(low_speed_timeout) = low_speed_timeout {
request_builder = request_builder.read_timeout(low_speed_timeout);
}
let http_request = request_builder.body(AsyncBody::from(request))?;
let mut response = client.send(http_request).await?;
let mut text = String::new();

View File

@@ -1,6 +1,4 @@
use gpui::{
div, img, prelude::*, App, AppContext, ImageSource, Render, ViewContext, WindowOptions,
};
use gpui::{div, img, prelude::*, App, AppContext, Render, ViewContext, WindowOptions};
use std::path::PathBuf;
struct GifViewer {
@@ -16,7 +14,7 @@ impl GifViewer {
impl Render for GifViewer {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
div().size_full().child(
img(ImageSource::File(self.gif_path.clone().into()))
img(self.gif_path.clone())
.size_full()
.object_fit(gpui::ObjectFit::Contain)
.id("gif"),

View File

@@ -6,8 +6,8 @@
<circle cx="240" cy="100" r="30" stroke="#dc2626" />
<circle cx="380" cy="100" r="20" stroke="#d97706" />
<circle cx="380" cy="240" r="30" stroke="#06b6d4" />
<circle cx="100" cy="240" r="30" stroke="#3b82f6" />
<circle cx="100" cy="240" r="30" stroke="#3b82f666" />
<circle cx="240" cy="380" r="30" stroke="#7c3aed" />
<circle cx="380" cy="380" r="20" stroke="#c026d3" />
<circle cx="100" cy="380" r="20" stroke="#e11d48" />
</svg>
<circle cx="100" cy="380" r="20" stroke="#e11d4866" />
</svg>

Before

Width:  |  Height:  |  Size: 741 B

After

Width:  |  Height:  |  Size: 746 B

View File

@@ -61,7 +61,7 @@ impl RenderOnce for ImageContainer {
}
struct ImageShowcase {
local_resource: Arc<PathBuf>,
local_resource: Arc<std::path::Path>,
remote_resource: SharedUri,
asset_resource: SharedString,
}
@@ -153,9 +153,10 @@ fn main() {
cx.open_window(window_options, |cx| {
cx.new_view(|_cx| ImageShowcase {
// Relative path to your root project path
local_resource: Arc::new(
PathBuf::from_str("crates/gpui/examples/image/app-icon.png").unwrap(),
),
local_resource: PathBuf::from_str("crates/gpui/examples/image/app-icon.png")
.unwrap()
.into(),
remote_resource: "https://picsum.photos/512/512".into(),
asset_resource: "image/color.svg".into(),

View File

@@ -0,0 +1,214 @@
use std::{path::Path, sync::Arc, time::Duration};
use anyhow::anyhow;
use gpui::{
black, div, img, prelude::*, pulsating_between, px, red, size, Animation, AnimationExt, App,
AppContext, Asset, AssetLogger, AssetSource, Bounds, Hsla, ImageAssetLoader, ImageCacheError,
ImgResourceLoader, Length, Pixels, RenderImage, Resource, SharedString, ViewContext,
WindowBounds, WindowContext, WindowOptions, LOADING_DELAY,
};
struct Assets {}
impl AssetSource for Assets {
fn load(&self, path: &str) -> anyhow::Result<Option<std::borrow::Cow<'static, [u8]>>> {
std::fs::read(path)
.map(Into::into)
.map_err(Into::into)
.map(Some)
}
fn list(&self, path: &str) -> anyhow::Result<Vec<SharedString>> {
Ok(std::fs::read_dir(path)?
.filter_map(|entry| {
Some(SharedString::from(
entry.ok()?.path().to_string_lossy().to_string(),
))
})
.collect::<Vec<_>>())
}
}
const IMAGE: &str = "examples/image/app-icon.png";
#[derive(Copy, Clone, Hash)]
struct LoadImageParameters {
timeout: Duration,
fail: bool,
}
struct LoadImageWithParameters {}
impl Asset for LoadImageWithParameters {
type Source = LoadImageParameters;
type Output = Result<Arc<RenderImage>, ImageCacheError>;
fn load(
parameters: Self::Source,
cx: &mut AppContext,
) -> impl std::future::Future<Output = Self::Output> + Send + 'static {
let timer = cx.background_executor().timer(parameters.timeout);
let data = AssetLogger::<ImageAssetLoader>::load(
Resource::Path(Path::new(IMAGE).to_path_buf().into()),
cx,
);
async move {
timer.await;
if parameters.fail {
log::error!("Intentionally failed to load image");
Err(anyhow!("Failed to load image").into())
} else {
data.await
}
}
}
}
struct ImageLoadingExample {}
impl ImageLoadingExample {
fn loading_element() -> impl IntoElement {
div().size_full().flex_none().p_0p5().rounded_sm().child(
div().size_full().with_animation(
"loading-bg",
Animation::new(Duration::from_secs(3))
.repeat()
.with_easing(pulsating_between(0.04, 0.24)),
move |this, delta| this.bg(black().opacity(delta)),
),
)
}
fn fallback_element() -> impl IntoElement {
let fallback_color: Hsla = black().opacity(0.5);
div().size_full().flex_none().p_0p5().child(
div()
.size_full()
.flex()
.items_center()
.justify_center()
.rounded_sm()
.text_sm()
.text_color(fallback_color)
.border_1()
.border_color(fallback_color)
.child("?"),
)
}
}
impl Render for ImageLoadingExample {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
div().flex().flex_col().size_full().justify_around().child(
div().flex().flex_row().w_full().justify_around().child(
div()
.flex()
.bg(gpui::white())
.size(Length::Definite(Pixels(300.0).into()))
.justify_center()
.items_center()
.child({
let image_source = LoadImageParameters {
timeout: LOADING_DELAY.saturating_sub(Duration::from_millis(25)),
fail: false,
};
// Load within the 'loading delay', should not show loading fallback
img(move |cx: &mut WindowContext| {
cx.use_asset::<LoadImageWithParameters>(&image_source)
})
.id("image-1")
.border_1()
.size_12()
.with_fallback(|| Self::fallback_element().into_any_element())
.border_color(red())
.with_loading(|| Self::loading_element().into_any_element())
.on_click(move |_, cx| {
cx.remove_asset::<LoadImageWithParameters>(&image_source);
})
})
.child({
// Load after a long delay
let image_source = LoadImageParameters {
timeout: Duration::from_secs(5),
fail: false,
};
img(move |cx: &mut WindowContext| {
cx.use_asset::<LoadImageWithParameters>(&image_source)
})
.id("image-2")
.with_fallback(|| Self::fallback_element().into_any_element())
.with_loading(|| Self::loading_element().into_any_element())
.size_12()
.border_1()
.border_color(red())
.on_click(move |_, cx| {
cx.remove_asset::<LoadImageWithParameters>(&image_source);
})
})
.child({
// Fail to load image after a long delay
let image_source = LoadImageParameters {
timeout: Duration::from_secs(5),
fail: true,
};
// Fail to load after a long delay
img(move |cx: &mut WindowContext| {
cx.use_asset::<LoadImageWithParameters>(&image_source)
})
.id("image-3")
.with_fallback(|| Self::fallback_element().into_any_element())
.with_loading(|| Self::loading_element().into_any_element())
.size_12()
.border_1()
.border_color(red())
.on_click(move |_, cx| {
cx.remove_asset::<LoadImageWithParameters>(&image_source);
})
})
.child({
// Ensure that the normal image loader doesn't spam logs
let image_source = Path::new(
"this/file/really/shouldn't/exist/or/won't/be/an/image/I/hope",
)
.to_path_buf();
img(image_source.clone())
.id("image-1")
.border_1()
.size_12()
.with_fallback(|| Self::fallback_element().into_any_element())
.border_color(red())
.with_loading(|| Self::loading_element().into_any_element())
.on_click(move |_, cx| {
cx.remove_asset::<ImgResourceLoader>(&image_source.clone().into());
})
}),
),
)
}
}
fn main() {
env_logger::init();
App::new()
.with_assets(Assets {})
.run(|cx: &mut AppContext| {
let options = WindowOptions {
window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
None,
size(px(300.), Pixels(300.)),
cx,
))),
..Default::default()
};
cx.open_window(options, |cx| {
cx.activate(false);
cx.new_view(|_cx| ImageLoadingExample {})
})
.unwrap();
});
}

View File

@@ -0,0 +1,199 @@
use gpui::{
canvas, div, point, prelude::*, px, size, App, AppContext, Bounds, MouseDownEvent, Path,
Pixels, Point, Render, ViewContext, WindowOptions,
};
struct PaintingViewer {
default_lines: Vec<Path<Pixels>>,
lines: Vec<Vec<Point<Pixels>>>,
start: Point<Pixels>,
_painting: bool,
}
impl PaintingViewer {
fn new() -> Self {
let mut lines = vec![];
// draw a line
let mut path = Path::new(point(px(50.), px(180.)));
path.line_to(point(px(100.), px(120.)));
// go back to close the path
path.line_to(point(px(100.), px(121.)));
path.line_to(point(px(50.), px(181.)));
lines.push(path);
// draw a lightening bolt ⚡
let mut path = Path::new(point(px(150.), px(200.)));
path.line_to(point(px(200.), px(125.)));
path.line_to(point(px(200.), px(175.)));
path.line_to(point(px(250.), px(100.)));
lines.push(path);
// draw a ⭐
let mut path = Path::new(point(px(350.), px(100.)));
path.line_to(point(px(370.), px(160.)));
path.line_to(point(px(430.), px(160.)));
path.line_to(point(px(380.), px(200.)));
path.line_to(point(px(400.), px(260.)));
path.line_to(point(px(350.), px(220.)));
path.line_to(point(px(300.), px(260.)));
path.line_to(point(px(320.), px(200.)));
path.line_to(point(px(270.), px(160.)));
path.line_to(point(px(330.), px(160.)));
path.line_to(point(px(350.), px(100.)));
lines.push(path);
let square_bounds = Bounds {
origin: point(px(450.), px(100.)),
size: size(px(200.), px(80.)),
};
let height = square_bounds.size.height;
let horizontal_offset = height;
let vertical_offset = px(30.);
let mut path = Path::new(square_bounds.lower_left());
path.curve_to(
square_bounds.origin + point(horizontal_offset, vertical_offset),
square_bounds.origin + point(px(0.0), vertical_offset),
);
path.line_to(square_bounds.upper_right() + point(-horizontal_offset, vertical_offset));
path.curve_to(
square_bounds.lower_right(),
square_bounds.upper_right() + point(px(0.0), vertical_offset),
);
path.line_to(square_bounds.lower_left());
lines.push(path);
Self {
default_lines: lines.clone(),
lines: vec![],
start: point(px(0.), px(0.)),
_painting: false,
}
}
fn clear(&mut self, cx: &mut ViewContext<Self>) {
self.lines.clear();
cx.notify();
}
}
impl Render for PaintingViewer {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let default_lines = self.default_lines.clone();
let lines = self.lines.clone();
div()
.font_family(".SystemUIFont")
.bg(gpui::white())
.size_full()
.p_4()
.flex()
.flex_col()
.child(
div()
.flex()
.gap_2()
.justify_between()
.items_center()
.child("Mouse down any point and drag to draw lines (Hold on shift key to draw straight lines)")
.child(
div()
.id("clear")
.child("Clean up")
.bg(gpui::black())
.text_color(gpui::white())
.active(|this| this.opacity(0.8))
.flex()
.px_3()
.py_1()
.on_click(cx.listener(|this, _, cx| {
this.clear(cx);
})),
),
)
.child(
div()
.size_full()
.child(
canvas(
move |_, _| {},
move |_, _, cx| {
const STROKE_WIDTH: Pixels = px(2.0);
for path in default_lines {
cx.paint_path(path, gpui::black());
}
for points in lines {
let mut path = Path::new(points[0]);
for p in points.iter().skip(1) {
path.line_to(*p);
}
let mut last = points.last().unwrap();
for p in points.iter().rev() {
let mut offset_x = px(0.);
if last.x == p.x {
offset_x = STROKE_WIDTH;
}
path.line_to(point(p.x + offset_x, p.y + STROKE_WIDTH));
last = p;
}
cx.paint_path(path, gpui::black());
}
},
)
.size_full(),
)
.on_mouse_down(
gpui::MouseButton::Left,
cx.listener(|this, ev: &MouseDownEvent, _| {
this._painting = true;
this.start = ev.position;
let path = vec![ev.position];
this.lines.push(path);
}),
)
.on_mouse_move(cx.listener(|this, ev: &gpui::MouseMoveEvent, cx| {
if !this._painting {
return;
}
let is_shifted = ev.modifiers.shift;
let mut pos = ev.position;
// When holding shift, draw a straight line
if is_shifted {
let dx = pos.x - this.start.x;
let dy = pos.y - this.start.y;
if dx.abs() > dy.abs() {
pos.y = this.start.y;
} else {
pos.x = this.start.x;
}
}
if let Some(path) = this.lines.last_mut() {
path.push(pos);
}
cx.notify();
}))
.on_mouse_up(
gpui::MouseButton::Left,
cx.listener(|this, _, _| {
this._painting = false;
}),
),
)
}
}
fn main() {
App::new().run(|cx: &mut AppContext| {
cx.open_window(
WindowOptions {
focus: true,
..Default::default()
},
|cx| cx.new_view(|_| PaintingViewer::new()),
)
.unwrap();
cx.activate(true);
});
}

View File

@@ -740,7 +740,7 @@ impl AppContext {
}
/// Returns the SVG renderer GPUI uses
pub(crate) fn svg_renderer(&self) -> SvgRenderer {
pub fn svg_renderer(&self) -> SvgRenderer {
self.svg_renderer.clone()
}
@@ -1362,7 +1362,7 @@ impl AppContext {
}
/// Remove an asset from GPUI's cache
pub fn remove_cached_asset<A: Asset + 'static>(&mut self, source: &A::Source) {
pub fn remove_asset<A: Asset>(&mut self, source: &A::Source) {
let asset_id = (TypeId::of::<A>(), hash(source));
self.loading_assets.remove(&asset_id);
}
@@ -1371,12 +1371,7 @@ impl AppContext {
///
/// Note that the multiple calls to this method will only result in one `Asset::load` call at a
/// time, and the results of this call will be cached
///
/// This asset will not be cached by default, see [Self::use_cached_asset]
pub fn fetch_asset<A: Asset + 'static>(
&mut self,
source: &A::Source,
) -> (Shared<Task<A::Output>>, bool) {
pub fn fetch_asset<A: Asset>(&mut self, source: &A::Source) -> (Shared<Task<A::Output>>, bool) {
let asset_id = (TypeId::of::<A>(), hash(source));
let mut is_first = false;
let task = self

View File

@@ -1,30 +1,43 @@
use crate::{AppContext, SharedString, SharedUri};
use futures::Future;
use std::fmt::Debug;
use std::hash::{Hash, Hasher};
use std::path::PathBuf;
use std::marker::PhantomData;
use std::path::{Path, PathBuf};
use std::sync::Arc;
/// An enum representing
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub(crate) enum UriOrPath {
pub enum Resource {
/// This resource is at a given URI
Uri(SharedUri),
Path(Arc<PathBuf>),
/// This resource is at a given path in the file system
Path(Arc<Path>),
/// This resource is embedded in the application binary
Embedded(SharedString),
}
impl From<SharedUri> for UriOrPath {
impl From<SharedUri> for Resource {
fn from(value: SharedUri) -> Self {
Self::Uri(value)
}
}
impl From<Arc<PathBuf>> for UriOrPath {
fn from(value: Arc<PathBuf>) -> Self {
impl From<PathBuf> for Resource {
fn from(value: PathBuf) -> Self {
Self::Path(value.into())
}
}
impl From<Arc<Path>> for Resource {
fn from(value: Arc<Path>) -> Self {
Self::Path(value)
}
}
/// A trait for asynchronous asset loading.
pub trait Asset {
pub trait Asset: 'static {
/// The source of the asset.
type Source: Clone + Hash + Send;
@@ -38,6 +51,31 @@ pub trait Asset {
) -> impl Future<Output = Self::Output> + Send + 'static;
}
/// An asset Loader that logs whatever passes through it
pub enum AssetLogger<T> {
#[doc(hidden)]
_Phantom(PhantomData<T>, &'static dyn crate::seal::Sealed),
}
impl<R: Clone + Send, E: Clone + Send + std::error::Error, T: Asset<Output = Result<R, E>>> Asset
for AssetLogger<T>
{
type Source = T::Source;
type Output = T::Output;
fn load(
source: Self::Source,
cx: &mut AppContext,
) -> impl Future<Output = Self::Output> + Send + 'static {
let load = T::load(source, cx);
async {
load.await
.inspect_err(|e| log::error!("Failed to load asset: {}", e))
}
}
}
/// Use a quick, non-cryptographically secure hash function to get an identifier from data
pub fn hash<T: Hash>(data: &T) -> u64 {
let mut hasher = collections::FxHasher::default();

View File

@@ -22,6 +22,17 @@ pub fn rgba(hex: u32) -> Rgba {
Rgba { r, g, b, a }
}
/// Swap from RGBA with premultiplied alpha to BGRA
pub(crate) fn swap_rgba_pa_to_bgra(color: &mut [u8]) {
color.swap(0, 2);
if color[3] > 0 {
let a = color[3] as f32 / 255.;
color[0] = (color[0] as f32 / a) as u8;
color[1] = (color[1] as f32 / a) as u8;
color[2] = (color[2] as f32 / a) as u8;
}
}
/// An RGBA color
#[derive(PartialEq, Clone, Copy, Default)]
pub struct Rgba {

View File

@@ -443,7 +443,7 @@ impl Interactivity {
pub fn on_drag<T, W>(
&mut self,
value: T,
constructor: impl Fn(&T, &mut WindowContext) -> View<W> + 'static,
constructor: impl Fn(&T, Point<Pixels>, &mut WindowContext) -> View<W> + 'static,
) where
Self: Sized,
T: 'static,
@@ -455,7 +455,9 @@ impl Interactivity {
);
self.drag_listener = Some((
Box::new(value),
Box::new(move |value, cx| constructor(value.downcast_ref().unwrap(), cx).into()),
Box::new(move |value, offset, cx| {
constructor(value.downcast_ref().unwrap(), offset, cx).into()
}),
));
}
@@ -966,14 +968,15 @@ pub trait StatefulInteractiveElement: InteractiveElement {
/// On drag initiation, this callback will be used to create a new view to render the dragged value for a
/// drag and drop operation. This API should also be used as the equivalent of 'on drag start' with
/// the [`Self::on_drag_move`] API
/// the [`Self::on_drag_move`] API.
/// The callback also has access to the offset of triggering click from the origin of parent element.
/// The fluent API equivalent to [`Interactivity::on_drag`]
///
/// See [`ViewContext::listener`](crate::ViewContext::listener) to get access to a view's state from this callback.
fn on_drag<T, W>(
mut self,
value: T,
constructor: impl Fn(&T, &mut WindowContext) -> View<W> + 'static,
constructor: impl Fn(&T, Point<Pixels>, &mut WindowContext) -> View<W> + 'static,
) -> Self
where
Self: Sized,
@@ -1056,7 +1059,8 @@ pub(crate) type ScrollWheelListener =
pub(crate) type ClickListener = Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>;
pub(crate) type DragListener = Box<dyn Fn(&dyn Any, &mut WindowContext) -> AnyView + 'static>;
pub(crate) type DragListener =
Box<dyn Fn(&dyn Any, Point<Pixels>, &mut WindowContext) -> AnyView + 'static>;
type DropListener = Box<dyn Fn(&dyn Any, &mut WindowContext) + 'static>;
@@ -1818,7 +1822,8 @@ impl Interactivity {
if let Some((drag_value, drag_listener)) = drag_listener.take() {
*clicked_state.borrow_mut() = ElementClickedState::default();
let cursor_offset = event.position - hitbox.origin;
let drag = (drag_listener)(drag_value.as_ref(), cx);
let drag =
(drag_listener)(drag_value.as_ref(), cursor_offset, cx);
cx.active_drag = Some(AnyDrag {
view: drag,
value: drag_value,
@@ -2378,7 +2383,7 @@ where
/// A wrapper around an element that can store state, produced after assigning an ElementId.
pub struct Stateful<E> {
element: E,
pub(crate) element: E,
}
impl<E> Styled for Stateful<E>

View File

@@ -1,9 +1,11 @@
use crate::{
px, AbsoluteLength, AppContext, Asset, Bounds, DefiniteLength, Element, ElementId,
GlobalElementId, Hitbox, Image, InteractiveElement, Interactivity, IntoElement, LayoutId,
Length, ObjectFit, Pixels, RenderImage, SharedString, SharedUri, StyleRefinement, Styled,
SvgSize, UriOrPath, WindowContext,
px, swap_rgba_pa_to_bgra, AbsoluteLength, AnyElement, AppContext, Asset, AssetLogger, Bounds,
DefiniteLength, Element, ElementId, GlobalElementId, Hitbox, Image, InteractiveElement,
Interactivity, IntoElement, LayoutId, Length, ObjectFit, Pixels, RenderImage, Resource,
SharedString, SharedUri, StyleRefinement, Styled, SvgSize, Task, WindowContext,
};
use anyhow::{anyhow, Result};
use futures::{AsyncReadExt, Future};
use image::{
codecs::gif::GifDecoder, AnimationDecoder, Frame, ImageBuffer, ImageError, ImageFormat,
@@ -11,45 +13,56 @@ use image::{
use smallvec::SmallVec;
use std::{
fs,
io::Cursor,
path::PathBuf,
io::{self, Cursor},
ops::{Deref, DerefMut},
path::{Path, PathBuf},
str::FromStr,
sync::Arc,
time::{Duration, Instant},
};
use thiserror::Error;
use util::ResultExt;
use super::{FocusableElement, Stateful, StatefulInteractiveElement};
/// The delay before showing the loading state.
pub const LOADING_DELAY: Duration = Duration::from_millis(200);
/// A type alias to the resource loader that the `img()` element uses.
///
/// Note: that this is only for Resources, like URLs or file paths.
/// Custom loaders, or external images will not use this asset loader
pub type ImgResourceLoader = AssetLogger<ImageAssetLoader>;
/// A source of image content.
#[derive(Clone, Debug, PartialEq, Eq)]
#[derive(Clone)]
pub enum ImageSource {
/// Image content will be loaded from provided URI at render time.
Uri(SharedUri),
/// Image content will be loaded from the provided file at render time.
File(Arc<PathBuf>),
/// The image content will be loaded from some resource location
Resource(Resource),
/// Cached image data
Render(Arc<RenderImage>),
/// Cached image data
Image(Arc<Image>),
/// Image content will be loaded from Asset at render time.
Embedded(SharedString),
/// A custom loading function to use
Custom(Arc<dyn Fn(&mut WindowContext) -> Option<Result<Arc<RenderImage>, ImageCacheError>>>),
}
fn is_uri(uri: &str) -> bool {
uri.contains("://")
http_client::Uri::from_str(uri).is_ok()
}
impl From<SharedUri> for ImageSource {
fn from(value: SharedUri) -> Self {
Self::Uri(value)
Self::Resource(Resource::Uri(value))
}
}
impl From<&'static str> for ImageSource {
fn from(s: &'static str) -> Self {
impl<'a> From<&'a str> for ImageSource {
fn from(s: &'a str) -> Self {
if is_uri(s) {
Self::Uri(s.into())
Self::Resource(Resource::Uri(s.to_string().into()))
} else {
Self::Embedded(s.into())
Self::Resource(Resource::Embedded(s.to_string().into()))
}
}
}
@@ -57,32 +70,34 @@ impl From<&'static str> for ImageSource {
impl From<String> for ImageSource {
fn from(s: String) -> Self {
if is_uri(&s) {
Self::Uri(s.into())
Self::Resource(Resource::Uri(s.into()))
} else {
Self::Embedded(s.into())
Self::Resource(Resource::Embedded(s.into()))
}
}
}
impl From<SharedString> for ImageSource {
fn from(s: SharedString) -> Self {
if is_uri(&s) {
Self::Uri(s.into())
} else {
Self::Embedded(s)
}
s.as_ref().into()
}
}
impl From<Arc<PathBuf>> for ImageSource {
fn from(value: Arc<PathBuf>) -> Self {
Self::File(value)
impl From<&Path> for ImageSource {
fn from(value: &Path) -> Self {
Self::Resource(value.to_path_buf().into())
}
}
impl From<Arc<Path>> for ImageSource {
fn from(value: Arc<Path>) -> Self {
Self::Resource(value.into())
}
}
impl From<PathBuf> for ImageSource {
fn from(value: PathBuf) -> Self {
Self::File(value.into())
Self::Resource(value.into())
}
}
@@ -98,12 +113,80 @@ impl From<Arc<Image>> for ImageSource {
}
}
impl<F: Fn(&mut WindowContext) -> Option<Result<Arc<RenderImage>, ImageCacheError>> + 'static>
From<F> for ImageSource
{
fn from(value: F) -> Self {
Self::Custom(Arc::new(value))
}
}
/// The style of an image element.
pub struct ImageStyle {
grayscale: bool,
object_fit: ObjectFit,
loading: Option<Box<dyn Fn() -> AnyElement>>,
fallback: Option<Box<dyn Fn() -> AnyElement>>,
}
impl Default for ImageStyle {
fn default() -> Self {
Self {
grayscale: false,
object_fit: ObjectFit::Contain,
loading: None,
fallback: None,
}
}
}
/// Style an image element.
pub trait StyledImage: Sized {
/// Get a mutable [ImageStyle] from the element.
fn image_style(&mut self) -> &mut ImageStyle;
/// Set the image to be displayed in grayscale.
fn grayscale(mut self, grayscale: bool) -> Self {
self.image_style().grayscale = grayscale;
self
}
/// Set the object fit for the image.
fn object_fit(mut self, object_fit: ObjectFit) -> Self {
self.image_style().object_fit = object_fit;
self
}
/// Set the object fit for the image.
fn with_fallback(mut self, fallback: impl Fn() -> AnyElement + 'static) -> Self {
self.image_style().fallback = Some(Box::new(fallback));
self
}
/// Set the object fit for the image.
fn with_loading(mut self, loading: impl Fn() -> AnyElement + 'static) -> Self {
self.image_style().loading = Some(Box::new(loading));
self
}
}
impl StyledImage for Img {
fn image_style(&mut self) -> &mut ImageStyle {
&mut self.style
}
}
impl StyledImage for Stateful<Img> {
fn image_style(&mut self) -> &mut ImageStyle {
&mut self.element.style
}
}
/// An image element.
pub struct Img {
interactivity: Interactivity,
source: ImageSource,
grayscale: bool,
object_fit: ObjectFit,
style: ImageStyle,
}
/// Create a new image element.
@@ -111,8 +194,7 @@ pub fn img(source: impl Into<ImageSource>) -> Img {
Img {
interactivity: Interactivity::default(),
source: source.into(),
grayscale: false,
object_fit: ObjectFit::Contain,
style: ImageStyle::default(),
}
}
@@ -125,16 +207,19 @@ impl Img {
"hdr", "exr", "pbm", "pam", "ppm", "pgm", "ff", "farbfeld", "qoi", "svg",
]
}
}
/// Set the image to be displayed in grayscale.
pub fn grayscale(mut self, grayscale: bool) -> Self {
self.grayscale = grayscale;
self
impl Deref for Stateful<Img> {
type Target = Img;
fn deref(&self) -> &Self::Target {
&self.element
}
/// Set the object fit for the image.
pub fn object_fit(mut self, object_fit: ObjectFit) -> Self {
self.object_fit = object_fit;
self
}
impl DerefMut for Stateful<Img> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.element
}
}
@@ -142,10 +227,17 @@ impl Img {
struct ImgState {
frame_index: usize,
last_frame_time: Option<Instant>,
started_loading: Option<(Instant, Task<()>)>,
}
/// The image layout state between frames
pub struct ImgLayoutState {
frame_index: usize,
replacement: Option<AnyElement>,
}
impl Element for Img {
type RequestLayoutState = usize;
type RequestLayoutState = ImgLayoutState;
type PrepaintState = Option<Hitbox>;
fn id(&self) -> Option<ElementId> {
@@ -157,11 +249,17 @@ impl Element for Img {
global_id: Option<&GlobalElementId>,
cx: &mut WindowContext,
) -> (LayoutId, Self::RequestLayoutState) {
let mut layout_state = ImgLayoutState {
frame_index: 0,
replacement: None,
};
cx.with_optional_element_state(global_id, |state, cx| {
let mut state = state.map(|state| {
state.unwrap_or(ImgState {
frame_index: 0,
last_frame_time: None,
started_loading: None,
})
});
@@ -170,64 +268,105 @@ impl Element for Img {
let layout_id = self
.interactivity
.request_layout(global_id, cx, |mut style, cx| {
if let Some(data) = self.source.use_data(cx) {
if let Some(state) = &mut state {
let frame_count = data.frame_count();
if frame_count > 1 {
let current_time = Instant::now();
if let Some(last_frame_time) = state.last_frame_time {
let elapsed = current_time - last_frame_time;
let frame_duration =
Duration::from(data.delay(state.frame_index));
let mut replacement_id = None;
if elapsed >= frame_duration {
state.frame_index = (state.frame_index + 1) % frame_count;
state.last_frame_time =
Some(current_time - (elapsed - frame_duration));
match self.source.use_data(cx) {
Some(Ok(data)) => {
if let Some(state) = &mut state {
let frame_count = data.frame_count();
if frame_count > 1 {
let current_time = Instant::now();
if let Some(last_frame_time) = state.last_frame_time {
let elapsed = current_time - last_frame_time;
let frame_duration =
Duration::from(data.delay(state.frame_index));
if elapsed >= frame_duration {
state.frame_index =
(state.frame_index + 1) % frame_count;
state.last_frame_time =
Some(current_time - (elapsed - frame_duration));
}
} else {
state.last_frame_time = Some(current_time);
}
}
state.started_loading = None;
}
let image_size = data.size(frame_index);
if let Length::Auto = style.size.width {
style.size.width = match style.size.height {
Length::Definite(DefiniteLength::Absolute(
AbsoluteLength::Pixels(height),
)) => Length::Definite(
px(image_size.width.0 as f32 * height.0
/ image_size.height.0 as f32)
.into(),
),
_ => Length::Definite(px(image_size.width.0 as f32).into()),
};
}
if let Length::Auto = style.size.height {
style.size.height = match style.size.width {
Length::Definite(DefiniteLength::Absolute(
AbsoluteLength::Pixels(width),
)) => Length::Definite(
px(image_size.height.0 as f32 * width.0
/ image_size.width.0 as f32)
.into(),
),
_ => Length::Definite(px(image_size.height.0 as f32).into()),
};
}
if global_id.is_some() && data.frame_count() > 1 {
cx.request_animation_frame();
}
}
Some(_err) => {
if let Some(fallback) = self.style.fallback.as_ref() {
let mut element = fallback();
replacement_id = Some(element.request_layout(cx));
layout_state.replacement = Some(element);
}
if let Some(state) = &mut state {
state.started_loading = None;
}
}
None => {
if let Some(state) = &mut state {
if let Some((started_loading, _)) = state.started_loading {
if started_loading.elapsed() > LOADING_DELAY {
if let Some(loading) = self.style.loading.as_ref() {
let mut element = loading();
replacement_id = Some(element.request_layout(cx));
layout_state.replacement = Some(element);
}
}
} else {
state.last_frame_time = Some(current_time);
let parent_view_id = cx.parent_view_id();
let task = cx.spawn(|mut cx| async move {
cx.background_executor().timer(LOADING_DELAY).await;
cx.update(|cx| {
cx.notify(parent_view_id);
})
.ok();
});
state.started_loading = Some((Instant::now(), task));
}
}
}
let image_size = data.size(frame_index);
if let Length::Auto = style.size.width {
style.size.width = match style.size.height {
Length::Definite(DefiniteLength::Absolute(
AbsoluteLength::Pixels(height),
)) => Length::Definite(
px(image_size.width.0 as f32 * height.0
/ image_size.height.0 as f32)
.into(),
),
_ => Length::Definite(px(image_size.width.0 as f32).into()),
};
}
if let Length::Auto = style.size.height {
style.size.height = match style.size.width {
Length::Definite(DefiniteLength::Absolute(
AbsoluteLength::Pixels(width),
)) => Length::Definite(
px(image_size.height.0 as f32 * width.0
/ image_size.width.0 as f32)
.into(),
),
_ => Length::Definite(px(image_size.height.0 as f32).into()),
};
}
if global_id.is_some() && data.frame_count() > 1 {
cx.request_animation_frame();
}
}
cx.request_layout(style, [])
cx.request_layout(style, replacement_id)
});
((layout_id, frame_index), state)
layout_state.frame_index = frame_index;
((layout_id, layout_state), state)
})
}
@@ -235,18 +374,24 @@ impl Element for Img {
&mut self,
global_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
request_layout: &mut Self::RequestLayoutState,
cx: &mut WindowContext,
) -> Option<Hitbox> {
) -> Self::PrepaintState {
self.interactivity
.prepaint(global_id, bounds, bounds.size, cx, |_, _, hitbox, _| hitbox)
.prepaint(global_id, bounds, bounds.size, cx, |_, _, hitbox, cx| {
if let Some(replacement) = &mut request_layout.replacement {
replacement.prepaint(cx);
}
hitbox
})
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
frame_index: &mut Self::RequestLayoutState,
layout_state: &mut Self::RequestLayoutState,
hitbox: &mut Self::PrepaintState,
cx: &mut WindowContext,
) {
@@ -255,29 +400,26 @@ impl Element for Img {
.paint(global_id, bounds, hitbox.as_ref(), cx, |style, cx| {
let corner_radii = style.corner_radii.to_pixels(bounds.size, cx.rem_size());
if let Some(data) = source.use_data(cx) {
let new_bounds = self.object_fit.get_bounds(bounds, data.size(*frame_index));
if let Some(Ok(data)) = source.use_data(cx) {
let new_bounds = self
.style
.object_fit
.get_bounds(bounds, data.size(layout_state.frame_index));
cx.paint_image(
new_bounds,
corner_radii,
data.clone(),
*frame_index,
self.grayscale,
layout_state.frame_index,
self.style.grayscale,
)
.log_err();
} else if let Some(replacement) = &mut layout_state.replacement {
replacement.paint(cx);
}
})
}
}
impl IntoElement for Img {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Styled for Img {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.interactivity.base_style
@@ -290,41 +432,28 @@ impl InteractiveElement for Img {
}
}
impl ImageSource {
pub(crate) fn use_data(&self, cx: &mut WindowContext) -> Option<Arc<RenderImage>> {
match self {
ImageSource::Uri(_) | ImageSource::Embedded(_) | ImageSource::File(_) => {
let uri_or_path: UriOrPath = match self {
ImageSource::Uri(uri) => uri.clone().into(),
ImageSource::File(path) => path.clone().into(),
ImageSource::Embedded(path) => UriOrPath::Embedded(path.clone()),
_ => unreachable!(),
};
impl IntoElement for Img {
type Element = Self;
cx.use_asset::<ImageAsset>(&uri_or_path)?.log_err()
}
ImageSource::Render(data) => Some(data.to_owned()),
ImageSource::Image(data) => cx.use_asset::<ImageDecoder>(data)?.log_err(),
}
fn into_element(self) -> Self::Element {
self
}
}
/// Fetch the data associated with this source, using GPUI's asset caching
pub async fn data(&self, cx: &mut AppContext) -> Option<Arc<RenderImage>> {
impl FocusableElement for Img {}
impl StatefulInteractiveElement for Img {}
impl ImageSource {
pub(crate) fn use_data(
&self,
cx: &mut WindowContext,
) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
match self {
ImageSource::Uri(_) | ImageSource::Embedded(_) | ImageSource::File(_) => {
let uri_or_path: UriOrPath = match self {
ImageSource::Uri(uri) => uri.clone().into(),
ImageSource::File(path) => path.clone().into(),
ImageSource::Embedded(path) => UriOrPath::Embedded(path.clone()),
_ => unreachable!(),
};
cx.fetch_asset::<ImageAsset>(&uri_or_path).0.await.log_err()
}
ImageSource::Render(data) => Some(data.to_owned()),
ImageSource::Image(data) => cx.fetch_asset::<ImageDecoder>(data).0.await.log_err(),
ImageSource::Resource(resource) => cx.use_asset::<ImgResourceLoader>(&resource),
ImageSource::Custom(loading_fn) => loading_fn(cx),
ImageSource::Render(data) => Some(Ok(data.to_owned())),
ImageSource::Image(data) => cx.use_asset::<AssetLogger<ImageDecoder>>(data),
}
}
}
@@ -334,22 +463,23 @@ enum ImageDecoder {}
impl Asset for ImageDecoder {
type Source = Arc<Image>;
type Output = Result<Arc<RenderImage>, Arc<anyhow::Error>>;
type Output = Result<Arc<RenderImage>, ImageCacheError>;
fn load(
source: Self::Source,
cx: &mut AppContext,
) -> impl Future<Output = Self::Output> + Send + 'static {
let result = source.to_image_data(cx).map_err(Arc::new);
async { result }
let renderer = cx.svg_renderer();
async move { source.to_image_data(renderer).map_err(Into::into) }
}
}
/// An image loader for the GPUI asset system
#[derive(Clone)]
enum ImageAsset {}
pub enum ImageAssetLoader {}
impl Asset for ImageAsset {
type Source = UriOrPath;
impl Asset for ImageAssetLoader {
type Source = Resource;
type Output = Result<Arc<RenderImage>, ImageCacheError>;
fn load(
@@ -363,12 +493,12 @@ impl Asset for ImageAsset {
let asset_source = cx.asset_source().clone();
async move {
let bytes = match source.clone() {
UriOrPath::Path(uri) => fs::read(uri.as_ref())?,
UriOrPath::Uri(uri) => {
Resource::Path(uri) => fs::read(uri.as_ref())?,
Resource::Uri(uri) => {
let mut response = client
.get(uri.as_ref(), ().into(), true)
.await
.map_err(|e| ImageCacheError::Client(Arc::new(e)))?;
.map_err(|e| anyhow!(e))?;
let mut body = Vec::new();
response.body_mut().read_to_end(&mut body).await?;
if !response.status().is_success() {
@@ -383,13 +513,13 @@ impl Asset for ImageAsset {
}
body
}
UriOrPath::Embedded(path) => {
Resource::Embedded(path) => {
let data = asset_source.load(&path).ok().flatten();
if let Some(data) = data {
data.to_vec()
} else {
return Err(ImageCacheError::Asset(
format!("not found: {}", path).into(),
format!("Embedded resource not found: {}", path).into(),
));
}
}
@@ -434,9 +564,8 @@ impl Asset for ImageAsset {
let mut buffer =
ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take()).unwrap();
// Convert from RGBA to BGRA.
for pixel in buffer.chunks_exact_mut(4) {
pixel.swap(0, 2);
swap_rgba_pa_to_bgra(pixel);
}
RenderImage::new(SmallVec::from_elem(Frame::new(buffer), 1))
@@ -450,9 +579,9 @@ impl Asset for ImageAsset {
/// An error that can occur when interacting with the image cache.
#[derive(Debug, Error, Clone)]
pub enum ImageCacheError {
/// An error that occurred while fetching an image from a remote source.
#[error("http error: {0}")]
Client(#[from] Arc<anyhow::Error>),
/// Some other kind of error occurred
#[error("error: {0}")]
Other(#[from] Arc<anyhow::Error>),
/// An error that occurred while reading the image from disk.
#[error("IO error: {0}")]
Io(Arc<std::io::Error>),
@@ -477,20 +606,26 @@ pub enum ImageCacheError {
Usvg(Arc<usvg::Error>),
}
impl From<std::io::Error> for ImageCacheError {
fn from(error: std::io::Error) -> Self {
Self::Io(Arc::new(error))
impl From<anyhow::Error> for ImageCacheError {
fn from(value: anyhow::Error) -> Self {
Self::Other(Arc::new(value))
}
}
impl From<ImageError> for ImageCacheError {
fn from(error: ImageError) -> Self {
Self::Image(Arc::new(error))
impl From<io::Error> for ImageCacheError {
fn from(value: io::Error) -> Self {
Self::Io(Arc::new(value))
}
}
impl From<usvg::Error> for ImageCacheError {
fn from(error: usvg::Error) -> Self {
Self::Usvg(Arc::new(error))
fn from(value: usvg::Error) -> Self {
Self::Usvg(Arc::new(value))
}
}
impl From<image::ImageError> for ImageCacheError {
fn from(value: image::ImageError) -> Self {
Self::Image(Arc::new(value))
}
}

View File

@@ -27,11 +27,11 @@ mod test;
mod windows;
use crate::{
point, Action, AnyWindowHandle, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds,
DevicePixels, DispatchEventResult, Font, FontId, FontMetrics, FontRun, ForegroundExecutor,
GPUSpecs, GlyphId, ImageSource, Keymap, LineLayout, Pixels, PlatformInput, Point,
RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, ScaledPixels, Scene,
SharedString, Size, SvgSize, Task, TaskLabel, WindowContext, DEFAULT_WINDOW_SIZE,
point, Action, AnyWindowHandle, AsyncWindowContext, BackgroundExecutor, Bounds, DevicePixels,
DispatchEventResult, Font, FontId, FontMetrics, FontRun, ForegroundExecutor, GPUSpecs, GlyphId,
ImageSource, Keymap, LineLayout, Pixels, PlatformInput, Point, RenderGlyphParams, RenderImage,
RenderImageParams, RenderSvgParams, ScaledPixels, Scene, SharedString, Size, SvgRenderer,
SvgSize, Task, TaskLabel, WindowContext, DEFAULT_WINDOW_SIZE,
};
use anyhow::{anyhow, Result};
use async_task::Runnable;
@@ -1264,11 +1264,13 @@ impl Image {
/// Use the GPUI `use_asset` API to make this image renderable
pub fn use_render_image(self: Arc<Self>, cx: &mut WindowContext) -> Option<Arc<RenderImage>> {
ImageSource::Image(self).use_data(cx)
ImageSource::Image(self)
.use_data(cx)
.and_then(|result| result.ok())
}
/// Convert the clipboard image to an `ImageData` object.
pub fn to_image_data(&self, cx: &AppContext) -> Result<Arc<RenderImage>> {
pub fn to_image_data(&self, svg_renderer: SvgRenderer) -> Result<Arc<RenderImage>> {
fn frames_for_image(
bytes: &[u8],
format: image::ImageFormat,
@@ -1305,10 +1307,7 @@ impl Image {
ImageFormat::Bmp => frames_for_image(&self.bytes, image::ImageFormat::Bmp)?,
ImageFormat::Tiff => frames_for_image(&self.bytes, image::ImageFormat::Tiff)?,
ImageFormat::Svg => {
// TODO: Fix this
let pixmap = cx
.svg_renderer()
.render_pixmap(&self.bytes, SvgSize::ScaleFactor(1.0))?;
let pixmap = svg_renderer.render_pixmap(&self.bytes, SvgSize::ScaleFactor(1.0))?;
let buffer =
image::ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take())

View File

@@ -222,6 +222,8 @@ fn is_printable_key(key: &str) -> bool {
| "insert"
| "home"
| "end"
| "back"
| "forward"
| "escape"
)
}

View File

@@ -684,6 +684,8 @@ impl Keystroke {
Keysym::ISO_Left_Tab => "tab".to_owned(),
Keysym::KP_Prior => "pageup".to_owned(),
Keysym::KP_Next => "pagedown".to_owned(),
Keysym::XF86_Back => "back".to_owned(),
Keysym::XF86_Forward => "forward".to_owned(),
Keysym::comma => ",".to_owned(),
Keysym::period => ".".to_owned(),

View File

@@ -260,7 +260,10 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
#[allow(non_upper_case_globals)]
let key = match first_char {
Some(SPACE_KEY) => "space".to_string(),
Some(SPACE_KEY) => {
ime_key = Some(" ".to_string());
"space".to_string()
}
Some(BACKSPACE_KEY) => "backspace".to_string(),
Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => "enter".to_string(),
Some(ESCAPE_KEY) => "escape".to_string(),

View File

@@ -343,8 +343,10 @@ impl MacPlatform {
ns_string(key_to_native(&keystroke.key).as_ref()),
)
.autorelease();
let _: () =
msg_send![item, setAllowsAutomaticKeyEquivalentLocalization: NO];
if MacPlatform::os_version().unwrap() >= SemanticVersion::new(12, 0, 0) {
let _: () =
msg_send![item, setAllowsAutomaticKeyEquivalentLocalization: NO];
}
item.setKeyEquivalentModifierMask_(mask);
}
// For multi-keystroke bindings, render the keystroke as part of the title.

View File

@@ -1,7 +1,8 @@
use crate::{
point, px, size, Bounds, DevicePixels, Font, FontFallbacks, FontFeatures, FontId, FontMetrics,
FontRun, FontStyle, FontWeight, GlyphId, LineLayout, Pixels, PlatformTextSystem, Point,
RenderGlyphParams, Result, ShapedGlyph, ShapedRun, SharedString, Size, SUBPIXEL_VARIANTS,
point, px, size, swap_rgba_pa_to_bgra, Bounds, DevicePixels, Font, FontFallbacks, FontFeatures,
FontId, FontMetrics, FontRun, FontStyle, FontWeight, GlyphId, LineLayout, Pixels,
PlatformTextSystem, Point, RenderGlyphParams, Result, ShapedGlyph, ShapedRun, SharedString,
Size, SUBPIXEL_VARIANTS,
};
use anyhow::anyhow;
use cocoa::appkit::CGFloat;
@@ -418,11 +419,7 @@ impl MacTextSystemState {
if params.is_emoji {
// Convert from RGBA with premultiplied alpha to BGRA with straight alpha.
for pixel in bytes.chunks_exact_mut(4) {
pixel.swap(0, 2);
let a = pixel[3] as f32 / 255.;
pixel[0] = (pixel[0] as f32 / a) as u8;
pixel[1] = (pixel[1] as f32 / a) as u8;
pixel[2] = (pixel[2] as f32 / a) as u8;
swap_rgba_pa_to_bgra(pixel);
}
}

View File

@@ -1160,6 +1160,8 @@ fn parse_syskeydown_msg_keystroke(wparam: WPARAM) -> Option<Keystroke> {
VK_END => "end",
VK_PRIOR => "pageup",
VK_NEXT => "pagedown",
VK_BROWSER_BACK => "back",
VK_BROWSER_FORWARD => "forward",
VK_ESCAPE => "escape",
VK_INSERT => "insert",
VK_DELETE => "delete",
@@ -1196,6 +1198,8 @@ fn parse_keydown_msg_keystroke(wparam: WPARAM) -> Option<KeystrokeOrModifier> {
VK_END => "end",
VK_PRIOR => "pageup",
VK_NEXT => "pagedown",
VK_BROWSER_BACK => "back",
VK_BROWSER_FORWARD => "forward",
VK_ESCAPE => "escape",
VK_INSERT => "insert",
VK_DELETE => "delete",

View File

@@ -647,11 +647,47 @@ impl PlatformWindow for WindowsWindow {
}
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
self.0
.state
.borrow_mut()
let mut window_state = self.0.state.borrow_mut();
window_state
.renderer
.update_transparency(background_appearance != WindowBackgroundAppearance::Opaque);
let mut version = unsafe { std::mem::zeroed() };
let status = unsafe { windows::Wdk::System::SystemServices::RtlGetVersion(&mut version) };
if status.is_ok() {
if background_appearance == WindowBackgroundAppearance::Blurred {
if version.dwBuildNumber >= 17763 {
set_window_composition_attribute(window_state.hwnd, Some((0, 0, 0, 10)), 4);
}
} else {
if version.dwBuildNumber >= 17763 {
set_window_composition_attribute(window_state.hwnd, None, 0);
}
}
//Transparent effect might cause some flickering and performance issues due `WS_EX_COMPOSITED` is enabled
//if `WS_EX_COMPOSITED` is removed the window instance won't initiate
if background_appearance == WindowBackgroundAppearance::Transparent {
unsafe {
let current_style = GetWindowLongW(window_state.hwnd, GWL_EXSTYLE);
SetWindowLongW(
window_state.hwnd,
GWL_EXSTYLE,
current_style | WS_EX_LAYERED.0 as i32 | WS_EX_COMPOSITED.0 as i32,
);
SetLayeredWindowAttributes(window_state.hwnd, COLORREF(0), 225, LWA_ALPHA)
.inspect_err(|e| log::error!("Unable to set window to transparent: {e}"))
.ok();
};
} else {
unsafe {
let current_style = GetWindowLongW(window_state.hwnd, GWL_EXSTYLE);
SetWindowLongW(
window_state.hwnd,
GWL_EXSTYLE,
current_style & !WS_EX_LAYERED.0 as i32 & !WS_EX_COMPOSITED.0 as i32,
);
}
}
}
}
fn minimize(&self) {
@@ -932,6 +968,23 @@ struct StyleAndBounds {
cy: i32,
}
#[repr(C)]
struct WINDOWCOMPOSITIONATTRIBDATA {
attrib: u32,
pv_data: *mut std::ffi::c_void,
cb_data: usize,
}
#[repr(C)]
struct AccentPolicy {
accent_state: u32,
accent_flags: u32,
gradient_color: u32,
animation_id: u32,
}
type Color = (u8, u8, u8, u8);
#[derive(Debug, Default, Clone, Copy)]
pub(crate) struct WindowBorderOffset {
width_offset: i32,
@@ -1136,6 +1189,44 @@ fn retrieve_window_placement(
Ok(placement)
}
fn set_window_composition_attribute(hwnd: HWND, color: Option<Color>, state: u32) {
unsafe {
type SetWindowCompositionAttributeType =
unsafe extern "system" fn(HWND, *mut WINDOWCOMPOSITIONATTRIBDATA) -> BOOL;
let module_name = PCSTR::from_raw("user32.dll\0".as_ptr());
let user32 = GetModuleHandleA(module_name);
if user32.is_ok() {
let func_name = PCSTR::from_raw("SetWindowCompositionAttribute\0".as_ptr());
let set_window_composition_attribute: SetWindowCompositionAttributeType =
std::mem::transmute(GetProcAddress(user32.unwrap(), func_name));
let mut color = color.unwrap_or_default();
let is_acrylic = state == 4;
if is_acrylic && color.3 == 0 {
color.3 = 1;
}
let accent = AccentPolicy {
accent_state: state,
accent_flags: if is_acrylic { 0 } else { 2 },
gradient_color: (color.0 as u32)
| ((color.1 as u32) << 8)
| ((color.2 as u32) << 16)
| (color.3 as u32) << 24,
animation_id: 0,
};
let mut data = WINDOWCOMPOSITIONATTRIBDATA {
attrib: 0x13,
pv_data: &accent as *const _ as *mut _,
cb_data: std::mem::size_of::<AccentPolicy>(),
};
let _ = set_window_composition_attribute(hwnd, &mut data as *mut _ as _);
} else {
let _ = user32
.inspect_err(|e| log::error!("Error getting module: {e}"))
.ok();
}
}
}
mod windows_renderer {
use std::{num::NonZeroIsize, sync::Arc};

View File

@@ -5,5 +5,5 @@
pub use crate::{
util::FluentBuilder, BorrowAppContext, BorrowWindow, Context, Element, FocusableElement,
InteractiveElement, IntoElement, ParentElement, Refineable, Render, RenderOnce,
StatefulInteractiveElement, Styled, VisualContext,
StatefulInteractiveElement, Styled, StyledImage, VisualContext,
};

View File

@@ -1,7 +1,7 @@
use crate::{
self as gpui, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle, DefiniteLength,
Fill, FlexDirection, FlexWrap, Font, FontStyle, FontWeight, Hsla, JustifyContent, Length,
SharedString, StyleRefinement, WhiteSpace,
SharedString, StrikethroughStyle, StyleRefinement, WhiteSpace,
};
use crate::{TextStyleRefinement, Truncate};
pub use gpui_macros::{
@@ -12,7 +12,7 @@ pub use gpui_macros::{
use taffy::style::{AlignContent, Display};
/// A trait for elements that can be styled.
/// Use this to opt-in to a CSS-like styling API.
/// Use this to opt-in to a utility CSS-like styling API.
pub trait Styled: Sized {
/// Returns a reference to the style memory of this element.
fn style(&mut self) -> &mut StyleRefinement;
@@ -323,19 +323,23 @@ pub trait Styled: Sized {
self
}
/// Get the text style that has been configured on this element.
/// Returns a mutable reference to the text style that has been configured on this element.
fn text_style(&mut self) -> &mut Option<TextStyleRefinement> {
let style: &mut StyleRefinement = self.style();
&mut style.text
}
/// Set the text color of this element, this value cascades to its child elements.
/// Sets the text color of this element.
///
/// This value cascades to its child elements.
fn text_color(mut self, color: impl Into<Hsla>) -> Self {
self.text_style().get_or_insert_with(Default::default).color = Some(color.into());
self
}
/// Set the font weight of this element, this value cascades to its child elements.
/// Sets the font weight of this element
///
/// This value cascades to its child elements.
fn font_weight(mut self, weight: FontWeight) -> Self {
self.text_style()
.get_or_insert_with(Default::default)
@@ -343,7 +347,9 @@ pub trait Styled: Sized {
self
}
/// Set the background color of this element, this value cascades to its child elements.
/// Sets the background color of this element.
///
/// This value cascades to its child elements.
fn text_bg(mut self, bg: impl Into<Hsla>) -> Self {
self.text_style()
.get_or_insert_with(Default::default)
@@ -351,7 +357,9 @@ pub trait Styled: Sized {
self
}
/// Set the text size of this element, this value cascades to its child elements.
/// Sets the text size of this element.
///
/// This value cascades to its child elements.
fn text_size(mut self, size: impl Into<AbsoluteLength>) -> Self {
self.text_style()
.get_or_insert_with(Default::default)
@@ -359,8 +367,8 @@ pub trait Styled: Sized {
self
}
/// Set the text size to 'extra small',
/// see the [Tailwind Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size)
/// Sets the text size to 'extra small'.
/// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size)
fn text_xs(mut self) -> Self {
self.text_style()
.get_or_insert_with(Default::default)
@@ -368,8 +376,8 @@ pub trait Styled: Sized {
self
}
/// Set the text size to 'small',
/// see the [Tailwind Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size)
/// Sets the text size to 'small'.
/// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size)
fn text_sm(mut self) -> Self {
self.text_style()
.get_or_insert_with(Default::default)
@@ -377,7 +385,8 @@ pub trait Styled: Sized {
self
}
/// Reset the text styling for this element and its children.
/// Sets the text size to 'base'.
/// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size)
fn text_base(mut self) -> Self {
self.text_style()
.get_or_insert_with(Default::default)
@@ -385,8 +394,8 @@ pub trait Styled: Sized {
self
}
/// Set the text size to 'large',
/// see the [Tailwind Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size)
/// Sets the text size to 'large'.
/// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size)
fn text_lg(mut self) -> Self {
self.text_style()
.get_or_insert_with(Default::default)
@@ -394,8 +403,8 @@ pub trait Styled: Sized {
self
}
/// Set the text size to 'extra large',
/// see the [Tailwind Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size)
/// Sets the text size to 'extra large'.
/// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size)
fn text_xl(mut self) -> Self {
self.text_style()
.get_or_insert_with(Default::default)
@@ -403,8 +412,8 @@ pub trait Styled: Sized {
self
}
/// Set the text size to 'extra-extra large',
/// see the [Tailwind Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size)
/// Sets the text size to 'extra extra large'.
/// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size)
fn text_2xl(mut self) -> Self {
self.text_style()
.get_or_insert_with(Default::default)
@@ -412,8 +421,8 @@ pub trait Styled: Sized {
self
}
/// Set the text size to 'extra-extra-extra large',
/// see the [Tailwind Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size)
/// Sets the text size to 'extra extra extra large'.
/// [Docs](https://tailwindcss.com/docs/font-size#setting-the-font-size)
fn text_3xl(mut self) -> Self {
self.text_style()
.get_or_insert_with(Default::default)
@@ -421,17 +430,8 @@ pub trait Styled: Sized {
self
}
/// Set the font style to 'non-italic',
/// see the [Tailwind Docs](https://tailwindcss.com/docs/font-style#italicizing-text)
fn non_italic(mut self) -> Self {
self.text_style()
.get_or_insert_with(Default::default)
.font_style = Some(FontStyle::Normal);
self
}
/// Set the font style to 'italic',
/// see the [Tailwind Docs](https://tailwindcss.com/docs/font-style#italicizing-text)
/// Sets the font style of the element to italic.
/// [Docs](https://tailwindcss.com/docs/font-style#italicizing-text)
fn italic(mut self) -> Self {
self.text_style()
.get_or_insert_with(Default::default)
@@ -439,7 +439,29 @@ pub trait Styled: Sized {
self
}
/// Remove the text decoration on this element, this value cascades to its child elements.
/// Sets the font style of the element to normal (not italic).
/// [Docs](https://tailwindcss.com/docs/font-style#italicizing-text)
fn not_italic(mut self) -> Self {
self.text_style()
.get_or_insert_with(Default::default)
.font_style = Some(FontStyle::Normal);
self
}
/// Sets the decoration of the text to have a line through it.
/// [Docs](https://tailwindcss.com/docs/text-decoration#setting-the-text-decoration)
fn line_through(mut self) -> Self {
let style = self.text_style().get_or_insert_with(Default::default);
style.strikethrough = Some(StrikethroughStyle {
thickness: px(1.),
..Default::default()
});
self
}
/// Removes the text decoration on this element.
///
/// This value cascades to its child elements.
fn text_decoration_none(mut self) -> Self {
self.text_style()
.get_or_insert_with(Default::default)
@@ -447,7 +469,7 @@ pub trait Styled: Sized {
self
}
/// Set the color for the underline on this element
/// Sets the color for the underline on this element
fn text_decoration_color(mut self, color: impl Into<Hsla>) -> Self {
let style = self.text_style().get_or_insert_with(Default::default);
let underline = style.underline.get_or_insert_with(Default::default);
@@ -455,7 +477,8 @@ pub trait Styled: Sized {
self
}
/// Set the underline to a solid line
/// Sets the text decoration style to a solid line.
/// [Docs](https://tailwindcss.com/docs/text-decoration-style)
fn text_decoration_solid(mut self) -> Self {
let style = self.text_style().get_or_insert_with(Default::default);
let underline = style.underline.get_or_insert_with(Default::default);
@@ -463,7 +486,8 @@ pub trait Styled: Sized {
self
}
/// Set the underline to a wavy line
/// Sets the text decoration style to a wavy line.
/// [Docs](https://tailwindcss.com/docs/text-decoration-style)
fn text_decoration_wavy(mut self) -> Self {
let style = self.text_style().get_or_insert_with(Default::default);
let underline = style.underline.get_or_insert_with(Default::default);
@@ -471,7 +495,8 @@ pub trait Styled: Sized {
self
}
/// Set the underline to be 0 thickness, see the [Tailwind Docs](https://tailwindcss.com/docs/text-decoration-thickness)
/// Sets the text decoration to be 0px thick.
/// [Docs](https://tailwindcss.com/docs/text-decoration-thickness)
fn text_decoration_0(mut self) -> Self {
let style = self.text_style().get_or_insert_with(Default::default);
let underline = style.underline.get_or_insert_with(Default::default);
@@ -479,7 +504,8 @@ pub trait Styled: Sized {
self
}
/// Set the underline to be 1px thick, see the [Tailwind Docs](https://tailwindcss.com/docs/text-decoration-thickness)
/// Sets the text decoration to be 1px thick.
/// [Docs](https://tailwindcss.com/docs/text-decoration-thickness)
fn text_decoration_1(mut self) -> Self {
let style = self.text_style().get_or_insert_with(Default::default);
let underline = style.underline.get_or_insert_with(Default::default);
@@ -487,7 +513,8 @@ pub trait Styled: Sized {
self
}
/// Set the underline to be 2px thick, see the [Tailwind Docs](https://tailwindcss.com/docs/text-decoration-thickness)
/// Sets the text decoration to be 2px thick.
/// [Docs](https://tailwindcss.com/docs/text-decoration-thickness)
fn text_decoration_2(mut self) -> Self {
let style = self.text_style().get_or_insert_with(Default::default);
let underline = style.underline.get_or_insert_with(Default::default);
@@ -495,7 +522,8 @@ pub trait Styled: Sized {
self
}
/// Set the underline to be 4px thick, see the [Tailwind Docs](https://tailwindcss.com/docs/text-decoration-thickness)
/// Sets the text decoration to be 4px thick.
/// [Docs](https://tailwindcss.com/docs/text-decoration-thickness)
fn text_decoration_4(mut self) -> Self {
let style = self.text_style().get_or_insert_with(Default::default);
let underline = style.underline.get_or_insert_with(Default::default);
@@ -503,7 +531,8 @@ pub trait Styled: Sized {
self
}
/// Set the underline to be 8px thick, see the [Tailwind Docs](https://tailwindcss.com/docs/text-decoration-thickness)
/// Sets the text decoration to be 8px thick.
/// [Docs](https://tailwindcss.com/docs/text-decoration-thickness)
fn text_decoration_8(mut self) -> Self {
let style = self.text_style().get_or_insert_with(Default::default);
let underline = style.underline.get_or_insert_with(Default::default);
@@ -511,7 +540,7 @@ pub trait Styled: Sized {
self
}
/// Change the font family on this element and its children.
/// Sets the font family of this element and its children.
fn font_family(mut self, family_name: impl Into<SharedString>) -> Self {
self.text_style()
.get_or_insert_with(Default::default)
@@ -519,7 +548,7 @@ pub trait Styled: Sized {
self
}
/// Change the font of this element and its children.
/// Sets the font of this element and its children.
fn font(mut self, font: Font) -> Self {
let Font {
family,
@@ -539,7 +568,7 @@ pub trait Styled: Sized {
self
}
/// Set the line height on this element and its children.
/// Sets the line height of this element and its children.
fn line_height(mut self, line_height: impl Into<DefiniteLength>) -> Self {
self.text_style()
.get_or_insert_with(Default::default)
@@ -547,20 +576,20 @@ pub trait Styled: Sized {
self
}
/// Set opacity on this element and its children.
/// Sets the opacity of this element and its children.
fn opacity(mut self, opacity: f32) -> Self {
self.style().opacity = Some(opacity);
self
}
/// Draw a debug border around this element.
/// Draws a debug border around this element.
#[cfg(debug_assertions)]
fn debug(mut self) -> Self {
self.style().debug = Some(true);
self
}
/// Draw a debug border on all conforming elements below this element.
/// Draws a debug border on all conforming elements below this element.
#[cfg(debug_assertions)]
fn debug_below(mut self) -> Self {
self.style().debug_below = Some(true);

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