Compare commits

...

124 Commits

Author SHA1 Message Date
Conrad Irwin
284ad179d7 Don't auto-start during install flow 2024-04-30 16:09:19 -06:00
Conrad Irwin
30b55279b5 Bundle linux preview releases too 2024-04-30 16:04:42 -06:00
Conrad Irwin
28bcc95468 installer (#11229)
Release Notes:

- N/A

---------

Co-authored-by: Mikayla <mikayla@zed.dev>
2024-04-30 16:01:07 -06:00
Marshall Bowers
d55b637b7e assistant2: Fix the height of collapsed messages (#11230)
This PR fixes the height of collapsed messages, addressing the
associated TODO comment.

Release Notes:

- N/A
2024-04-30 17:44:51 -04:00
Marshall Bowers
f2a1226e18 assistant2: Setup storybook (#11228)
This PR sets up the `assistant2` crate with the storybook so that UI
elements can be iterated on in isolation.

To start, we have some stories for the `ChatMessage` component:

```sh
cargo run -p storybook -- components/assistant_chat_message
```

<img width="1233" alt="Screenshot 2024-04-30 at 5 20 03 PM"
src="https://github.com/zed-industries/zed/assets/1486634/510967ea-0e9b-4fa9-94fb-421ee74bcc45">

Release Notes:

- N/A
2024-04-30 17:33:32 -04:00
Marshall Bowers
96b1fc4650 assistant2: Align chat messages with composer (#11227)
This PR adds some additional spacing to the composer so it aligns with
the chat messages.

Release Notes:

- N/A
2024-04-30 17:00:23 -04:00
Conrad Irwin
1d4814e5b6 installer (#11224)
Release Notes:

- N/A

---------

Co-authored-by: Mikayla <mikayla@zed.dev>
2024-04-30 14:25:12 -06:00
Conrad Irwin
cb7350174e Don't allow dropping files on remote projects (#11218)
Release Notes:

- Fixed a panic when a remote participant dropped a local file on your
project
2024-04-30 14:24:47 -06:00
Marshall Bowers
f11a7811f3 assistant2: Use composer for editing inline messages (#11222)
This PR updates the assistant to render historical user messages the
same as ones from the assistant.

Double-clicking on a message will open a composer inline for editing.
Pressing `Esc` will cancel the edit.

We don't yet restore the previous state of the message upon canceling.

<img width="401" alt="Screenshot 2024-04-30 at 4 04 01 PM"
src="https://github.com/zed-industries/zed/assets/1486634/5f253fa8-6578-4054-be30-c495e326d700">

<img width="401" alt="Screenshot 2024-04-30 at 4 04 28 PM"
src="https://github.com/zed-industries/zed/assets/1486634/edf25cea-d97e-44d2-8772-3690eac017a4">


Release Notes:

- N/A
2024-04-30 16:12:20 -04:00
Marshall Bowers
e8ee0131f1 Remove unused assistant prompt (#11221)
This PR removes an unused assistant prompt.

Release Notes:

- N/A

Co-authored-by: Nathan <nathan@zed.dev>
2024-04-30 15:32:11 -04:00
Max Brunsfeld
38b9d5cc36 Fix some semantic index issues (#11216)
* [x] Fixed an issue where embeddings would be assigned incorrectly to
files if a subset of embedding batches failed
* [x] Added a command to debug which paths are present in the semantic
index
* [x] Determine why so many paths are often missing from the semantic
index
* we erroring out if an embedding batch contained multiple texts that
were the same, which can happen if a worktree contains multiple copies
of the same text (e.g. a license).

Release Notes:

- N/A

---------

Co-authored-by: Marshall <marshall@zed.dev>
Co-authored-by: Nathan <nathan@zed.dev>
Co-authored-by: Kyle <kylek@zed.dev>
Co-authored-by: Kyle Kelley <rgbkrk@gmail.com>
2024-04-30 10:55:38 -07:00
Marshall Bowers
d01428e69c assistant2: Add create buffer tool (#11219)
This PR adds a new tool to the `assistant2` crate that allows the
assistant to create a new buffer with some content.

Release Notes:

- N/A

---------

Co-authored-by: Nathan <nathan@zed.dev>
2024-04-30 13:43:25 -04:00
Piotr Osiewicz
ada2791fa3 editor: Move code actions menu closer to the indicator when it is deployed via click (#11214)
Before:

![image](https://github.com/zed-industries/zed/assets/24362066/98d633a7-c982-4522-b4dc-b944b70b8081)

After: 

![image](https://github.com/zed-industries/zed/assets/24362066/79931e12-0e6c-4ece-b734-5af7d02f7e50)

Release Notes:

- N/A
2024-04-30 18:51:20 +02:00
Marshall Bowers
713c314d67 Clean up doc comments on LanguageModelTool trait (#11217)
This PR cleans up the doc comments on the `LanguageModelTool` trait to
follow Rust documentation conventions.

Release Notes:

- N/A
2024-04-30 12:32:59 -04:00
Marshall Bowers
e3de440715 assistant2: Restructure tools in preparation for adding more (#11213)
This PR does a slight restructuring of how tools are defined in the
`assistant2` crate to make it more amenable to adding more tools in the
near future.

Release Notes:

- N/A
2024-04-30 11:12:44 -04:00
Thorsten Ball
d743c19fe2 Add script to package Linux binary into archive (#11165)
Release Notes:

- N/A
2024-04-30 14:56:48 +02:00
Thorsten Ball
73d0600ad2 project: Set completion to undocumented if text empty (#11207)
I think the previous code was missing a `return` in there because it
always overwrote the `completion.documentation` field, even if the
`text.is_empty()` is true.

Release Notes:

- N/A
2024-04-30 14:56:22 +02:00
Stanislav Alekseev
f96cab286c Add avatar support for codeberg in git blame (#10991)
Release Notes:

- Added support for avatars in git blame for repositories hosted on
codeberg

<img width="1144" alt="Screenshot 2024-04-25 at 16 45 22"
src="https://github.com/zed-industries/zed/assets/43210583/d44770d8-44ea-4c6b-a1c0-ac2d1d49408f">

Questions:
- Should we move git stuff like `Commit`, `Author`, etc outside of
hosting-specific files (I don't think so, as other hostings can have
different stuff)
- Should we also add support for self hosted forgejo instances or should
it be a different PR?
2024-04-30 12:57:10 +02:00
Conrad Irwin
8152e0676f Allow triggering preview and stable simultaneously (#11201)
Release Notes:


- N/A
2024-04-29 20:55:50 -06:00
Conrad Irwin
62c12cd549 Reduce frequency of workspace save (#11183)
Co-Authored-By: Mikayla <mikayla@zed.dev>

Release Notes:


- N/A

---------

Co-authored-by: Mikayla <mikayla@zed.dev>
2024-04-29 20:27:01 -06:00
Hans
4fad96b179 Add yss operation support for vim surrounds: (#11084)
The Motion::CurrentLine operation will contain the newline of the
current line, so we need to deal with this edge case



Release Notes:

- N/A
2024-04-29 19:38:14 -06:00
Conrad Irwin
ad44237467 Fix panic in SVG rendering (#11196)
Release Notes:

- Fixed a panic in SVG rendering
2024-04-29 19:03:37 -06:00
Danny Hua
bc736265be support vim replace command with range (#10709)
Release Notes:

- Added support for line ranges in vim replace commands #9428


- not supporting anything other than bare line numbers right now
- ~need to figure out how to show range in question in search bar~
@ConradIrwin implemented showing a highlight of the selected range for a
short direction instead
- ~tests lol~
2024-04-29 18:49:30 -06:00
Conrad Irwin
0697b417a0 Hang diagnostics (#11190)
Release Notes:

- Added diagnostics for main-thread hangs on macOS. These are only
enabled if you've opted into diagnostics.

---------

Co-authored-by: Mikayla <mikayla@zed.dev>
2024-04-29 17:13:28 -07:00
Marshall Bowers
04cd8dd0f2 assistant2: Add the ability to collapse chat messages (#11194)
This PR adds the ability to collapse/uncollapse chat messages.

I think the spacing might be a little off with the collapsed
calculations, so we'll need to look into that.


https://github.com/zed-industries/zed/assets/1486634/4009c831-b44e-4b30-85ed-0266cb5b8a26

Release Notes:

- N/A
2024-04-29 19:25:58 -04:00
shimataro
ce643e6bef Add Gentoo support to Linux dependency script (#11141)
This PR adds dependencies for Gentoo Linux.

Release Notes:

- N/A
2024-04-29 16:08:22 -07:00
apricotbucket28
6ab9c3c3ab x11: HiDPI support (#11140)
Fixes https://github.com/zed-industries/zed/issues/11121

Release Notes:

- N/A
2024-04-29 16:07:54 -07:00
apricotbucket28
a765535557 wayland: Implement cursor-shape-v1 (#11106)
Fixes wrong cursor icons and sizes on KDE 6 (and possibly other
compositors)

Gnome still doesn't support this protocol, so to fix cursor settings
there we'll need to read `gsettings`.

Before:

![image](https://github.com/zed-industries/zed/assets/71973804/f0c3dc1b-2bed-43f7-b579-df6c9c0b547f)

After:

![image](https://github.com/zed-industries/zed/assets/71973804/6de639f9-653d-4896-80ca-b7c69641eded)


Release Notes:

- N/A
2024-04-29 16:06:18 -07:00
Marshall Bowers
089ea7852d assistant2: Use ChatMessage component to render chat messages (#11193)
This PR updates the new assistant panel to use the `ChatMessage`
component to render its chat messages.

This also lays the foundation for collapsing the messages, though that
has yet to be wired up.

Adapted from the work on the `assistant-chat-ui` branch.

Release Notes:

- N/A
2024-04-29 18:47:16 -04:00
Marshall Bowers
ae650342ce assistant2: Add headers to chat messages (#11191)
This PR adds headers to the chat messages in the new assistant panel.

Adapted from the work on the `assistant-chat-ui` branch.

Release Notes:

- N/A
2024-04-29 18:21:43 -04:00
Max Brunsfeld
1c09b69384 Pin message composer to the bottom of the new assistant panel (#11186)
Release Notes:

- N/A

---------

Co-authored-by: Marshall <marshall@zed.dev>
Co-authored-by: Nate <nate@zed.dev>
Co-authored-by: Kyle <kylek@zed.dev>
Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-04-29 14:26:19 -07:00
Nate Butler
f842d19b0b Remove extra space right of traffic lights on macOS (#11176)
This PR balances the left and right spacing around the traffic lights on
macOS.

Release Notes:

- Minor UI changes to the titlebar
2024-04-29 16:43:21 -04:00
Marshall Bowers
d633a0da78 gpui: Fix Global trait (#11187)
This PR restores the `Global` trait's status as a marker trait.

This was the original intent from #7095, when it was added, that had
been lost in #9777.

The purpose of the `Global` trait is to statically convey what types can
and can't be accessed as `Global` state, as well as provide a way of
restricting access to said globals.

For example, in the case of the `ThemeRegistry` we have a private
`GlobalThemeRegistry` that is marked as `Global`:
91b3c24ed3/crates/theme/src/registry.rs (L25-L34)

We're then able to permit reading the `ThemeRegistry` from the
`GlobalThemeRegistry` via a custom getter, while still restricting which
callers are able to mutate the global:
91b3c24ed3/crates/theme/src/registry.rs (L46-L61)

Release Notes:

- N/A
2024-04-29 16:37:37 -04:00
Piotr Osiewicz
91b3c24ed3 editor: Clear diagnostics when folding a range that contains it (#11167)
Fixes #4659 

Release Notes:

- Fixed active diagnostic in editor showing up when it's line is in a
folded range.
2024-04-29 20:50:14 +02:00
Bennet Bo Fenner
20625e98ad preview tabs: Allow replacing preview tab when using code navigation (#10730)
This PR adds support for replacing the current preview tab when using
GoToDefinition. Previously a tab, that was navigated away from, was
converted into a permanent tab and the new tab was opened as preview.

Without `enable_preview_from_code_navigation`:


https://github.com/zed-industries/zed/assets/53836821/99840724-d6ff-4738-a9c4-ee71a0001634

With `enable_preview_from_code_navigation`:


https://github.com/zed-industries/zed/assets/53836821/8c60efcb-d597-40bf-b08b-13faf5a289b6

Note: In the future I would like to improve support for the navigation
history, because right now tabs that are not "normal" project items,
e.g. FindAllReferences cannot be reopened

Release Notes:

- Added support for replacing the current preview tab when using code
navigation (`enable_preview_from_code_navigation`)
2024-04-29 20:47:01 +02:00
Marshall Bowers
9ff847753e Register ESLint as an available language server (#11178)
This PR adds the ability for the ESLint language server (`eslint`) to be
controlled by the `language_servers` setting.

Now in your settings you can indicate that the ESLint language server
should be used for a given language, even if that language does not have
the ESLint language server registered for it already:

```json
{
  "languages": {
    "My Language": {
      "language_servers": ["eslint", "..."]
    }
  }
}
```

Release Notes:

- N/A
2024-04-29 13:18:30 -04:00
Owen Law
ec95605fec XI2 Smooth Scrolling for X11 - Attempt 2 (#11110)
This should have fixed the problems that some users were reporting with
https://github.com/zed-industries/zed/pull/10695 .

The problem was with devices which send more than one valuator axis in a
single event whereas the original PR assumed there would only ever be
one axis per event. This version also does away with the complicated
device selection and instead just uses the master pointer device, which
automatically uses all sub-pointers.

Edit: Confirmed working for one of the user's which the first attempt
was broken for.

Release Notes:

- Added smooth scrolling for X11 on Linux
- Added horizontal scrolling for X11 on Linux
2024-04-29 09:40:42 -07:00
Jakob Hellermann
ff8e7f91c1 Fix wayland keyrepeat: use modifier-independant keycode instead of keysym (#11095)
fix bug introduced by #11052



https://github.com/zed-industries/zed/pull/11052#issuecomment-2080475491
the keysym for `a` pressed with or without shift is different, so
keyrepeat was never ended for `Shift-A`.

Release Notes:

- N/A
2024-04-29 09:33:53 -07:00
apricotbucket28
2614215090 wayland: Fix crash on wayfire (#11081)
It is a protocol violation to attach a buffer before acknowledging in
xdg_surface::configure.

Release Notes:

- N/A
2024-04-29 09:33:11 -07:00
Jakob Hellermann
2386ae9f0e Set appid/wmclass for zed window (#10909)
fixes https://github.com/zed-industries/zed/issues/9132

By setting the app id, window managers like `sway` can apply custom
configuration like `for_window [app_id="zed"] floating enable`.
Tested using `wlprop`/`hyprctl activewindow` for wayland, `xprop` for
x11.


Release Notes:

- Zed now sets the window app id / class, which can be used e.g. in
window managers like `sway`/`i3` to define custom rules
2024-04-29 09:27:25 -07:00
Conrad Irwin
5674ba2a49 Validate buffer_id of anchors in is_valid (#11170)
Release Notes:

- Fixes diagnostic panic better (follow up from #11066)
2024-04-29 10:24:04 -06:00
William Viktorsson
95118c6568 Restore previous workspace on application resume (#10813)
Addresses #10812 

Release Notes:

- Launching an empty already-running Zed application now behaves like a
regular startup and respects the user `resume_on_startup` setting.
([#10812](https://github.com/zed-industries/zed/issues/10812)).

See attached showcase which highlights how the previous project can now
be re-opened through both "quit" and "close window".

This has a noticeable performance benefit on startup/project resume
time.

This should also make the behaviour of closing/opening an application
consistent between macOS/Linux/Windows.


https://github.com/zed-industries/zed/assets/22855292/9c37ba31-ce0a-4c3d-940d-a56e3347e64a
2024-04-29 09:56:23 -06:00
jansol
35c3af7fd0 gpui/mac: Disable shadows on non-opaque windows (#10896)
The culprit behind ghost images in transparent windows and bad
performance with blurred windows turns out to be one and the same:
window shadows. The simplest and most popular fix appears to be to
simply disable shadows on non-opaque windows so let's just do that.

Disabling shadows on a window that is already visible however leaves the
shadow on screen, detached from the window, until a full screen effect
such as exposé or a virtual desktop switch wipes it clean. There does
not seem to be any known solution to this, and it does not affect
windows created after switching to a transparent theme so this is a good
enough compromise for now.



Release Notes:

- Fixed ghostly artifacts in transparent window backgrounds.
- Fixed sluggishness with blurred window backgrounds.
2024-04-29 11:04:11 -04:00
Thorsten Ball
5ef75919f0 git: Do not log error if repository has no commits (#11163)
This is a follow-up to #10685 which started to hide these errors instead
of displaying them to the user.

But the errors are still noisy and not actionable, so we hide them
instead.

Release Notes:

- Removed error message being logged when `git blame` is run in a
repository without commits.

Co-authored-by: Bennet <bennetbo@gmx.de>
2024-04-29 16:00:29 +02:00
Bennet Bo Fenner
9746f4f267 Fix relative line numbers (#11161)
Closes #11105

Release Notes:

- Fixed rendering of relative line numbers in editor
([#11105](https://github.com/zed-industries/zed/issues/11105)).

Co-authored-by: Thorsten <thorsten@zed.dev>
2024-04-29 15:54:40 +02:00
Thorsten Ball
9693e394f7 Remove unused fields (#11158)
Release Notes:


- N/A
2024-04-29 15:34:34 +02:00
Bennet Bo Fenner
45d217f6e0 Set destructive action only when prompt level is set to Destructive (#11154)
Oversight from #11015, where we added `PromptLevel::Destructive`, which
should be used when a prompt performs a "destructive" action (e.g.
deleting a file). However, we accidentally set `setHasDestructiveAction`
to `true` regardless of which prompt level would be specified

Release Notes:

- N/A
2024-04-29 12:12:11 +02:00
William Villeneuve
ca187c8386 Allow users to configure ESLint's rulesCustomizations settings (#11135)
https://github.com/zed-industries/zed/assets/2072378/18f0bb28-0546-4234-a11f-39af6c9fcc12

`rulesCustomizations` is an array of rule severity overrides. Globs can
be used to apply default severities for multiple rules. See
[docs](553e632fb4/README.md (L312-L333))
& [type
definitions](553e632fb4/%24shared/settings.ts (L168))

Example Zed `settings.json` to override all eslint errors to warnings:
```jsonc
{
    "lsp": {
        "eslint": {
            "settings": {
                "rulesCustomizations": [
                    // set all eslint errors/warnings to show as warnings
                    { "rule": "*", "severity": "warn" }
                ]
            }
        }
    }
}
```


Release Notes:

- Added support for configuring ESLint's `rulesCustomizations` settings,
ie. `{"lsp": {"eslint": {"settings": {"rulesCustomizations": [{"rule":
"*", "severity": "warn"}]}}}}`
2024-04-29 11:41:48 +03:00
d1y
f72cf2afe3 Highlight reference types directive in TypeScript (#11039)
Before:

<img width="443" alt="image"
src="https://github.com/zed-industries/zed/assets/45585937/a8037edb-ed5b-4b12-92b4-2d409442ad4b">


After:

<img width="429" alt="image"
src="https://github.com/zed-industries/zed/assets/45585937/1fd66523-d667-4041-952f-c1125e7feab9">


Release Notes:

- Add typescript reference types directive highlighted(#11001)
2024-04-29 11:38:53 +03:00
Thorsten Ball
8a79535b84 vim: Document jk (#11150)
Release Notes:


- N/A
2024-04-29 07:27:35 +02:00
Joseph T. Lyons
d4ec68b9ab Add hpp file icon (#11149)
Release Notes:

- Added `.hpp` file icon.
2024-04-29 01:08:59 -04:00
CharlesChen0823
c8a496ec4b Windows: Fix direct_write crash when clicking an empty line (#11117)
Windows platform direct_write using `LineLayout::default` indicates
`font_size` will be 0. Zed crashed when click empty line.

Release Notes:
- N/A
2024-04-28 21:37:45 -07:00
Michael Angerman
c826ad2f82 gpui: Improve the image example (#11111)
### fix cropping problem

Prior to these changes the images were being cropped so you never
actually saw
the full image but you had to use your mouse to make the window bigger
to see
both the text and the images...

### activate
 
```rust
cx.activate(true);
```
was not in place so the window did not appear when you ran the example

### No longer need to Ctrl-c to quit the example

Now you can hit *Cmd-q* to quit out of the example instead of having to
*Ctrl-c* in your
terminal where you fired off the example

Release Notes:

- N/A
2024-04-28 20:59:21 -07:00
Mikayla Maki
f458f90673 Fix bugs from recent PRs (#11147)
Fix missed delete in project panel
Fix blinking scrollbar from selections-in-scrollbar change

Release Notes:

- N/A (nightly only)
2024-04-28 20:56:17 -07:00
Nathan Sobo
39fb1d567d Incorporate ElementId as part of the Element::id trait method and expose GlobalId (#11101)
We're planning to associate "selection sources" with global element ids
to allow arbitrary UI text to be selected in GPUI. Previously, global
ids were not exposed outside the framework and we entangled management
of the element id stack with element state access. This was more
acceptable when element state was the only place we used global element
ids, but now that we're planning to use them more places, it makes sense
to deal with element identity as a first-class part of the element
system. We now ensure that the stack of element ids which forms the
current global element id is correctly managed in every phase of element
layout and paint and make the global id available to each element
method. In a subsequent PR, we'll use the global element id as part of
implementing arbitrary selection for UI text.

Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
2024-04-28 13:59:21 -06:00
Marshall Bowers
8b55494351 Properly register new icons (#11132)
This PR fixes some issues with the icons added in #11116, where they
weren't being registered properly.

Release Notes:

- N/A
2024-04-28 10:43:10 -04:00
Derek Briggs
32e6424543 Icons for js, c, cc, h, jsx, and tsx file types (#11116)
Release Notes:
Add Icons for react (jsx/tsx), C, and C++ files

![CleanShot 2024-04-27 at 21 06
26@2x](https://github.com/zed-industries/zed/assets/1648941/3f23c058-e0c1-4aed-bfdd-0a1a9c8d7203)

Release Notes:

- Added icons for JS, React, C, and C++
2024-04-27 21:24:54 -06:00
Mikayla Maki
d2569afe66 Switch from delete file by default to trash file by default (#10875)
TODO:

- [x] Don't immediately seg fault
- [x] Implement for directories 
- [x] Add cmd-delete to remove files
- [ ] ~~Add setting for trash vs. delete~~ You can just use keybindings
to change the behavior.

fixes https://github.com/zed-industries/zed/issues/7228
fixes https://github.com/zed-industries/zed/issues/5094

Release Notes:

- Added a new `project_panel::Trash` action and changed the default
behavior for `backspace` and `delete` in the project panel to send a
file to the systems trash, instead of permanently deleting it
([#7228](https://github.com/zed-industries/zed/issues/7228),
[#5094](https://github.com/zed-industries/zed/issues/5094)). The
original behavior can be restored by adding the following section to
your keybindings:

```json5
[
// ...Other keybindings...
  {
    "context": "ProjectPanel",
    "bindings": {
        "backspace": "project_panel::Delete",
        "delete": "project_panel::Delete",
    }
  }
]
2024-04-26 17:43:50 -07:00
blufony
5dbd23f6b0 vim: add keybinding to jump to parent directory in project panel (#11078)
Release Notes:

- vim: Support `-` to go to `parent directory` of the project panel.

As mentioned in the comments of #11073 this adds support for netrw's `-`
to go to the parent directory in the project panel. Again tested on
linux only.


- N/A
2024-04-26 18:22:15 -06:00
Max Brunsfeld
b7d9aeb29d Semantic index progress (#11071)
Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Kyle <kylek@zed.dev>
Co-authored-by: Marshall <marshall@zed.dev>
Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-04-26 17:06:05 -07:00
Andrew Lygin
1aa9c868d4 Scrollbar markers for cursors (#10816)
How it looks:

https://github.com/zed-industries/zed/assets/2101250/f564111c-1019-4442-b8a6-de338e12b12e

This PR adds cursor markers to the scrollbar. They work similar to
VSCode:

1. A cursor marker takes the whole scrollbar width.
2. It's always 2px high.
3. It uses the player's `cursor` color, so it may be helpful in the
collaboration mode.

There's a setting to switch cursor markers on/off:

```json
{
  "scrollbar": {
    "cursors": true
  }
}
```

Implementation details:

- Unlike other markers, cursor markers are displayed synchronously.
Otherwise they don't feel smooth and sometimes freez on prolonged
up/down navigation.
- Cursor markers are automatically switched off when it's more than 100
of them.
- The minimum (non-cursor) marker height is now 5px. It allows the user
to see other markers under the cursor marker.
- The way the minimum height is imposed on markers has changed a bit to
keep consistency between markers of different types.
- Selected symbol markers use less vibrant color (`info` faded out a
little).

Release Notes:

- Added displaying of cursor markers in the scrollbar. They can be
switched on/off by the `scrollbar.cursors` setting.
2024-04-27 02:26:42 +03:00
Conrad Irwin
848bb97ba7 release notes vN (#11077)
Make it more likely the draft release notes work...

Release Notes:

- N/A
2024-04-26 17:18:17 -06:00
Max Brunsfeld
8e925bf58f Don't panic when a tree-sitter parse fails (#11076)
Fixes
https://zed-industries.slack.com/archives/C04S6T1T7TQ/p1714162894982749

Release Notes:
* Fixed a crash that could happen if an error occurred in a parser
provided by an extension.

Co-authored-by: Conrad <conrad@zed.dev>
2024-04-26 16:59:35 -06:00
blufony
adcaa211ec Add keybindings to jump to first / last file in project panel (#11073)
Release Notes:
- vim: Support `g g`/`G` to go to top/bottom of the project panel.

In vim type environments i usually expect to be able to jump to the top
and bottom and i was confused as to why that wasn't possible in the
project panel. So i added it. If anyone using different keymaps also
thinks this might be useful i would be happy to add other defaults. I
think for vim mode it is the most useful though, because you tend to not
use your mouse in vim mode.

This is my first contribution to any rust project, but it seemed like a
good starting point.
The function select_last() is inspired by select_first.
I was also thinking about adding a bigger jump keybinding, that would
jump for example 5 entries up / down, with vim default keybindings "{" /
"}".
FYI: I tested this on linux only and don't have access to any macos
machines.


- N/A
2024-04-26 16:35:57 -06:00
Jakob Hellermann
393b16d226 Fix Wayland keyrepeat getting cancelled by unrelated keyup (#11052)
fixes #11048

## Problem
in the situation `press right`, `press left`, `release right` the
following happens right now:

- `keypressed right`, `current_keysym` is set to `right`
- `keypressed left`, `current_keysym` is set to `left`

the repeat timer runs asynchronously and emits keyrepeats since
`current_keysym.is_some()`

- `keyreleased right`, `current_keysym` is set to None

the repeat timer no longer emits keyrepeats

- `keyreleased left`, this is where `current_keysym` should actually be
set to None.

## Solution
Only reset `current_keysym` if the released key matches the last pressed
key.

Release Notes:

- N/A
2024-04-26 14:07:05 -07:00
Akilan Elango
7bd18fa653 Sync maximized state from top-level configure event for a wayland window (#11003)
* Otherwise is_maximized always returns `true`



Release Notes:

- Fixed maximized state. Tested with a dummy maximize/restore button
with the `zoom()` (not implemented yet). Without the right `maximized`,
in toggle zoom function is not possible to call `set_maximized()` or
`unset_maximized()`.

```rust
    fn zoom(&self) {
      if self.is_maximized() {
        self.borrow_mut().toplevel.unset_maximized();
      } else {
        self.borrow_mut().toplevel.set_maximized();
      }
    }
```
2024-04-26 14:03:19 -07:00
张小白
11dc3c2582 windows: Support all OpenType font features (#10756)
Release Notes:

- Added support for all `OpenType` font features to DirectWrite.



https://github.com/zed-industries/zed/assets/14981363/cb2848cd-9178-4d87-881a-54dc646b2b61

---------

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2024-04-26 13:58:12 -07:00
张小白
268cb948a7 windows: Move manifest file to gpui (#11036)
This is a follow up of #10810 , `embed-resource` crate uses a different
method to link the manifest file, so this makes moving manifest file to
`gpui` possible.

Now, examples can run as expected:
![Screenshot 2024-04-26
111559](https://github.com/zed-industries/zed/assets/14981363/bb040690-8129-490b-83b3-0a7d3cbd4953)

TODO:
- [ ] check if it builds with gnu toolchain

Release Notes:

- N/A
2024-04-26 13:56:48 -07:00
张小白
6a915e349c windows: Fix panicking on startup (#11028)
### Connection:
Closes #10954 

Release Notes:

- N/A
2024-04-26 13:55:41 -07:00
apricotbucket28
70d03e4841 x11: Fix window close (#11008)
Fixes https://github.com/zed-industries/zed/issues/10483 on X11

Also calls the `should_close` callback before closing the window (needed
for the "Do you want to save?" dialog).

Release Notes:

- N/A
2024-04-26 13:53:49 -07:00
DissolveDZ
b1eb0291dc Re-add README.md which might have been deleted by mistake (#11067)
Release Notes:

- N/A
2024-04-26 16:04:25 -04:00
Conrad Irwin
e0644de90e Fix panic in Diagnostics (#11066)
cc @maxbrunsfeld

Release Notes:

- Fixed a panic in populating diagnostics
2024-04-26 14:04:18 -06:00
Bennet Bo Fenner
9329ef1d78 markdown preview: Break up list items into individual blocks (#10852)
Fixes a panic related to rendering checkboxes, see #10824.

Currently we are rendering a list into a single block, meaning the whole
block has to be rendered when it is visible on screen. This would lead
to performance problems when a single list block contained a lot of
items (especially if it contained checkboxes). This PR splits up list
items into separate blocks, meaning only the actual visible list items
on screen get rendered, instead of the whole list.
A nice side effect of the refactoring is, that you can actually click on
individual list items now:


https://github.com/zed-industries/zed/assets/53836821/5ef4200c-bd85-4e96-a8bf-e0c8b452f762

Release Notes:

- Improved rendering performance of list elements inside the markdown
preview

---------

Co-authored-by: Remco <djsmits12@gmail.com>
2024-04-26 21:34:45 +02:00
Conrad Irwin
664f779eb4 new path picker (#11015)
Still TODO:

* Disable the new save-as for local projects
* Wire up sending the new path to the remote server

Release Notes:

- Added the ability to "Save-as" in remote projects

---------

Co-authored-by: Nathan <nathan@zed.dev>
Co-authored-by: Bennet <bennetbo@gmx.de>
2024-04-26 13:25:25 -06:00
Bennet Bo Fenner
314b723292 remote projects: Allow reusing window (#11058)
Release Notes:

- Allow reusing the window when opening a remote project from the recent
projects picker
- Fixed an issue, which would not let you rejoin a remote project after
disconnecting from it for the first time

---------

Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Remco <djsmits12@gmail.com>
2024-04-26 21:04:34 +02:00
Tim Masliuchenko
1af1a9e8b3 Toggle tasks modal in task::Rerun, when no tasks have been scheduled (#11059)
Currently, when no tasks have been scheduled, the `task::Rerun` action
does nothing.
This PR adds a fallback, so when no tasks have been scheduled so far the
`task::Rerun` action toggles the tasks modal

https://github.com/zed-industries/zed/assets/471335/72f7a71e-cfa8-49db-a295-fb05b2e7c905

Release Notes:

- Improved the `task::Rerun` action to toggle the tasks modal when no
tasks have been scheduled so far
2024-04-26 17:56:34 +02:00
Jakob Hellermann
8006f69513 Fix Cargo.toml typo ref -> rev (#11047)
Release Notes:

- N/A
2024-04-26 08:31:04 -04:00
Piotr Osiewicz
bacc92333a tasks: Fix divider position in modal (#11049)
The divider between templates and recent runs is constant, regardless of
the currently used filter string; this can lead to situations where an
user can remove the predefined task, which isn't good at all.

Additionally, in this PR I've made it so that recent runs always show up
before task templates in filtered list.

Release Notes:

- Fixed position of list divider in task modal.
2024-04-26 14:29:16 +02:00
Conrad Irwin
eb7bd0b98a Use fewer fancy cursors even for vim users (#11041)
Release Notes:

- N/A
2024-04-26 09:42:21 +02:00
Conrad Irwin
7f229dc202 Remove unread notes indicator for now (#11035)
I'd like to add something back here, but it's more distracting than
helpful today.

Fixes: #10887

Release Notes:

- Removed channel notes unread indicator

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-04-25 23:50:31 -04:00
Conrad Irwin
03d0b68f0c Fix panic in rename selections (#11033)
cc @someonetoignore

Release Notes:

- Fixed a panic when renaming with a selection (preview only)
2024-04-25 21:29:56 -06:00
Hans
5c2f27a501 Fix VIM cw on last character of a word doesn't work as expected: (#10963)
At the moment, using the default expand_selection seems to do the job
well, without the need for some additional logic, which may also make
the code a little clearer, Fix #10945



Release Notes:


- N/A
2024-04-25 21:09:06 -06:00
Conrad Irwin
d9d509a2bb Send installation id with crashes (#11032)
This will let us prioritize crashes that affect many users.

Release Notes:

- N/A
2024-04-25 21:07:06 -06:00
Marshall Bowers
a4ad3bcc08 Hoist nanoid to workspace-level (#11029)
This PR hoists `nanoid` up to a workspace dependency.

Release Notes:

- N/A
2024-04-25 22:37:40 -04:00
Conrad Irwin
6d7332e80c Fix panic in vim search (#11022)
Release Notes:

- vim: Fixed a panic when searching
2024-04-25 20:32:15 -06:00
Marshall Bowers
1b614ef63b Add an Assistant example that can interact with the filesystem (#11027)
This PR adds a new Assistant example that is able to interact with the
filesystem using a tool.

Release Notes:

- N/A
2024-04-25 22:21:18 -04:00
Hendrik Sollich
604857ed2e vim: Increment search right (#10866)
Hi there, nice editor!
Here's my attempt at fixing #10865.

Thanks

Release Notes:

-vim: Fix ctrl+a when cursor is on a decimal point
([#10865](https://github.com/zed-industries/zed/issues/10865)).

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-04-25 19:47:52 -06:00
DissolveDZ
d9eb3c4b35 vim: Fix hollow cursor being offset when selecting text (#11000)
Fixed the cursor selection being offset, the hollow cursor was being
displayed fine when not having text selected that's why it might not
have been noticed at first.

Release Notes:
- N/A

Improved:
0d6fb08b67
2024-04-25 19:47:12 -06:00
Marshall Bowers
f8beda0704 Rename chat_with_functions to use snake_case (#11020)
This PR renames the `chat-with-functions.rs` example to use snake_case
for the filename, as is convention.

Release Notes:

- N/A
2024-04-25 21:43:02 -04:00
Max Brunsfeld
40fe5275cf Rework project diagnostics to prevent showing inconsistent state (#10922)
For a long time, we've had problems where diagnostics can end up showing
up inconsistently in different views. This PR is my attempt to prevent
that, and to simplify the system in the process. There are some UX
changes.

Diagnostic behaviors that have *not* changed:

* In-buffer diagnostics update immediately when LSPs send diagnostics
updates.
* The diagnostic counts in the status bar indicator also update
immediately.

Diagnostic behaviors that this PR changes:

* [x] The tab title for the project diagnostics view now simply shows
the same counts as the status bar indicator - the project's current
totals. Previously, this tab title showed something slightly different -
the numbers of diagnostics *currently shown* in the diagnostics view's
excerpts. But it was pretty confusing that you could sometimes see two
different diagnostic counts.
* [x] The project diagnostics view **never** updates its excerpts while
the user might be in the middle of typing it that view, unless the user
expressed an intent for the excerpts to update (by e.g. saving the
buffer). This was the behavior we originally implemented, but has
changed a few times since then, in attempts to fix other issues. I've
restored that invariant.

    Times when the excerpts will update:
     * diagnostics are updated while the diagnostics view is not focused
     * the user changes focus away from the diagnostics view
* the language server sends a `work done progress end` message for its
disk-based diagnostics token (i.e. cargo check finishes)
* the user saves a buffer associated with a language server, and then a
debounce timer expires

* [x] The project diagnostics view indicates when its diagnostics are
stale. States:
* when diagnostics have been updated while the diagnostics view was
focused:
        * the indicator shows a 'refresh' icon
        * clicking the indicator updates the excerpts
* when diagnostics have been updated, but a file has been saved, so that
the diagnostics will soon update, the indicator is disabled

With these UX changes, the only 'complex' part of the our diagnostics
presentation is the Project Diagnostics view's excerpt management,
because it needs to implement the deferred updates in order to avoid
disrupting the user while they may be typing. I want to take some steps
to reduce the potential for bugs in this view.

* [x] Reduce the amount of state that the view uses, and simplify its
implementation
* [x] Add a randomized test that checks the invariant that a mutated
diagnostics view matches a freshly computed diagnostics view


##  Release Notes

- Reworked the project diagnostics view:
- Fixed an issue where the project diagnostics view could update its
excerpts while you were typing in it.
- Fixed bugs where the project diagnostics view could show the wrong
excerpts.
- Changed the diagnostics view to always update its excerpts eagerly
when not focused.
- Added an indicator to the project diagnostics view's toolbar, showing
when diagnostics have been changed.

---------

Co-authored-by: Richard Feldman <oss@rtfeldman.com>
2024-04-25 18:12:15 -07:00
Kyle Kelley
cf2272a949 Always submit function definitions in Simple mode too (#11016)
Switches Assistant2 to always provide functions. It's up to the model to
choose to use them. At a later point, the `Submit::Codebase` should
change the `tool_choice` to `query_codebase` rather than `auto`. For
now, I think this will improve the experience for folks testing.

Release Notes:

- N/A
2024-04-25 16:34:07 -07:00
Nate Butler
366d7e7728 Break typography styles out of StyledExt (#11013)
- Centralizes typography-related UI styles and methods in
`styles/typography.rs`
- Breaks the typography-related styles out of `StyledExt`. This means we
add a `StyledTypography` trait – this should more or less be an
invisible change as we publish it in the prelude.
- adds the ability to easily grab the UI or Buffer font sizes
(`ui_font_size`, `buffer_font_size`) with `TextSize::UI`,
`TextSize::Editor`

Release Notes:

- N/A
2024-04-25 17:42:53 -04:00
Michael Angerman
4c780568bc storybook: Fix Backspace in Auto Height Editor and Picker stories (#11011)
Currently in the *Auto Height Editor* story the backspace key is not
working when you type into the Editor.

The same thing is true for the *Picker* story as one is not able to
backspace...

By adding an entry in the keymap file

```rust
assets/keymaps/storybook.json
```

both of these issues are solved...

Release Notes:

- N/A
2024-04-25 17:37:47 -04:00
Marshall Bowers
7af96a15fe Fix typo in comment 2024-04-25 17:30:47 -04:00
Marshall Bowers
3eac581a62 Allow controlling Tailwind via the language_servers setting (#11012)
This PR adds the ability for the Tailwind language server
(`tailwindcss-language-server`) to be controlled by the
`language_servers` setting.

Now in your settings you can indicate that the Tailwind language server
should be used for a given language, even if that language does not have
the Tailwind language server registered for it already:

```json
{
  "languages": {
    "My Language": {
      "language_servers": ["tailwindcss-language-server", "..."]
    }
  }
}
```

Release Notes:

- N/A
2024-04-25 17:29:47 -04:00
Conrad Irwin
c833a7e662 Don't use fancy cursors for non-vim people (#11010)
Release Notes:

- N/A
2024-04-25 14:21:01 -06:00
Kyle Kelley
f176e8f0e4 Accept Views on LanguageModelTools (#10956)
Creates a `ToolView` trait to allow interactivity. This brings expanding
and collapsing to the excerpts from project index searches.

Release Notes:

- N/A

---------

Co-authored-by: Nathan <nathan@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2024-04-25 13:03:43 -07:00
Marshall Bowers
7005f0b424 Remove outdated instructions for adding languages (#11005)
This PR removes the outdated comment regarding adding languages to Zed.

New languages should be added as extensions.

Release Notes:

- N/A
2024-04-25 14:30:31 -04:00
James Thurley
d3f6ca7a1e Add @operator, @lifetime and @punctuation.delimiters captures for Rust (#10885)
Adds additional captures for theming rust code.

I'm uncertain about whether `#` belongs in the `@operator` capture, but
I didn't see a more appropriate capture name in my brief hunt in other
files. It is the prefix of an `attribute_item`.. suggestions welcome.

Release Notes:

- Added `@operator`, `@lifetime` and `@punctuation.delimiter` captures
to Rust highlights file.
2024-04-25 14:20:20 -04:00
Marshall Bowers
544bd490ac Extract Elixir extension (#10948)
This PR extracts Elixir support into an extension and removes the
built-in Elixir support from Zed.

As part of this, [Lexical](https://github.com/lexical-lsp/lexical) has
been added as an available language server for Elixir.

Since the Elixir extension provides three different language servers,
you'll need to use the `language_servers` setting to select the one you
want to use:

#### Elixir LS

```json
{
  "languages": {
    "Elixir": {
      "language_servers": [ "elixir-ls", "!next-ls", "!lexical", "..."]
    }
  }
}
```

#### Next LS

```json
{
  "languages": {
    "Elixir": {
      "language_servers": [ "next-ls", "!elixir-ls", "!lexical", "..."]
    }
  }
}
```

#### Lexical

```json
{
  "languages": {
    "Elixir": {
      "language_servers": [ "lexical", "!elixir-ls", "!next-ls", "..."]
    }
  }
}
```

These can either go in your user settings or your project settings.

Release Notes:

- Removed built-in support for Elixir, in favor of making it available
as an extension.
2024-04-25 13:59:14 -04:00
Jason Lee
7065da2b98 Fix project-panel double click file support on Windows (#10917)
Release Notes:

- Fixed project panel double click to force open file on Windows.

Ref issue: #10898 

@bennetbo 

I saw you was also used `event.down.click_count` in Markdown Preview.


7dccbd8e3b (diff-c8d1735cb347ea08d03198df112343ec50a74de8d50414a6f3be6c6d674c6d19R161)

And this also used in other place:

<img width="870" alt="image"
src="https://github.com/zed-industries/zed/assets/5518/b844e700-b95f-4cd2-987f-9e4305ebdd7c">

## Test demo after updated

Looks like it is no side effect

![2024-04-24 10 17
45](https://github.com/zed-industries/zed/assets/5518/0df4cf06-7448-4014-9df2-f2608a5f5314)
2024-04-25 10:01:24 -07:00
Thorsten Ball
0d6fb08b67 vim: set cursor to hollow-block if editor loses focus (#10995)
This has been bugging me for a while now. Finally figured out how to do
it.

Release Notes:

- Fixed cursor in Vim mode not changing into a hollow block when editor
loses focus.


Demo: 


https://github.com/zed-industries/zed/assets/1185253/c7585282-156d-4ab2-b516-eb1940d6d0d3
2024-04-25 17:56:53 +02:00
Marshall Bowers
3ce4ff94ae Update Cargo.lock 2024-04-25 11:17:25 -04:00
Congyu
21022f1644 Fix cmd+click find all references fallback not working in Vim mode (#10684)
Exclude go-to-definition links returned by LSP that points to the
current cursor position. This fixes #10392 . Related PR #9243 .

The previous implementation first performs go-to-definition, and if the
selected text covers the clicked position, it figures out that it is
already clicking on a definition, and should instead look for
references.

However, the selected range does not necessarily cover the clicked
position after clicking on a definition, as in VIM mode.

After the PR, if cmd+click on definitions, the definition links would be
an empty list, so no go-to-definition is performed, and
find-all-references is performed instead.

Release Notes:

- Fixed #10392 , now `cmd+click`ing to find all references works in vim
mode.
2024-04-25 09:07:14 -06:00
Antonio Scandurra
11bcfea6d2 Fix single-line editors not working anymore (#10994)
This was introduced with #10979 and was caused by a missing call to
`cx.set_view_id` in `EditorElement`, which is necessary when rendering
`EditorElement` manually instead of via a view.

Release Notes:

- N/A
2024-04-25 17:04:20 +02:00
Jakob Hellermann
1cd34fdd9c Recognize PKGBUILD as bash script (#10946)
[PKGBUILD] is a file used in the build system of arch linux, and it is
basically just a bash script with special functions.


Release Notes:

- Changed `PKGBUILD` files to be recognized as bash.
2024-04-25 09:44:52 -04:00
Antonio Scandurra
530224527d Allow pressing escape to cancel the current assistant generation (#10987)
If the assistant has already emitted some text, we will leave the
assistant message but maintain the cursor on the previous user message,
so that the user can easily discard the message by submitting again.

If no output was emitted yet, we simply delete the empty assistant
message.

Release Notes:

- N/A
2024-04-25 15:12:58 +02:00
Marshall Bowers
0de2636324 Revert "Changed cmd+w with no open tabs to close window (#10740)" (#10986)
This PR reverts #10740, as it makes it too easy to close Zed
accidentally.

Quitting Zed when you don't mean to is disruptive and can break your
flow. This is even more the case when you're collaborating.

Therefore, we shouldn't make it easy to quit Zed when you don't mean to.

If we want to bring back this behavior it needs to have a corresponding
setting that should, in my opinion, be **off** by default. Additionally,
someone made the good point that this behavior should not be present on
Linux or Windows.

This reverts commit 5102e37a5b.

Release Notes:

- Changed `cmd-w` with no open tabs to no longer close the window
(preview-only).
2024-04-25 09:10:02 -04:00
Thorsten Ball
7ec963664e git blame: Do not try to blame buffer if it has no file (#10985)
Release Notes:

- Fixed error messages being logged due to inline git blame not working
on an empty buffer that hasn't been saved yet.
2024-04-25 15:02:19 +02:00
Thorsten Ball
019821d62c eslint: register as language server for Vue.js (#10983)
This fixes #9934 and does two things:

1. It registers ESLint as a secondary language server for Vue.js files
(`.vue`)
2. It registers ESLint as a _secondary_ (instead of primary) language
server for TypeScript, JavaScript and TSX.

The second point because I don't see any reason why ESLint should be
registered as a primary language server for these languages. I read
through the code in `project.rs` that uses the primary language server
and I don't think there will be any differences to how it previously
worked.

I also manually tested ESLint support in a Vue.js project, a Next.js
project and a plain old JS project — still works in all three.

Release Notes:

- Added ESLint support for Vue.js files by starting it as a language
server on `.vue` files.
([#9934](https://github.com/zed-industries/zed/issues/9934)).
2024-04-25 14:49:07 +02:00
Thorsten Ball
bb213b6e37 Fix keybinding errors on Linux (#10982)
These showed up as error messages. One of them has been removed and the
other two have changed names.



Release Notes:

- N/A
2024-04-25 13:44:24 +02:00
Antonio Scandurra
6a7761e620 Merge ElementContext into WindowContext (#10979)
The new `ElementContext` was originally introduced to ensure the element
APIs could only be used inside of elements. Unfortunately, there were
many places where some of those APIs needed to be used, so
`WindowContext::with_element_context` was introduced, which defeated the
original safety purposes of having a specific context for elements.

This pull request merges `ElementContext` into `WindowContext` and adds
(debug) runtime checks to APIs that can only be used during certain
phases of element drawing.

Release Notes:

- N/A

---------

Co-authored-by: Nathan Sobo <nathan@zed.dev>
2024-04-25 12:54:39 +02:00
Thorsten Ball
031580f4dc git: Fix inline blame moving on horizontal scroll (#10974)
This fixes the behaviour reported by @mikayla-maki.

## Before


https://github.com/zed-industries/zed/assets/1185253/35aa4e6d-295b-4050-b5cc-cab0f91b27e1


## After


https://github.com/zed-industries/zed/assets/1185253/a17cbc9c-fc2c-43d6-918b-1205b327507b

## Release notes

Release Notes:

- Fixed inline git blame information moving when horizontally scrolling.
2024-04-25 11:29:29 +02:00
Hans
1a27016123 Improve logic for obtaining surrounds range in Vim mode (#10938)
now correctly retrieves range in cases where escape characters are
present. Fixed #10827


Release Notes:

- vim: Fix logic for finding surrounding quotes to ignore escaped
characters (#10827)
2024-04-24 21:19:15 -06:00
Conrad Irwin
d1425603f6 Fix misalignment of vim mode indicator (#10962)
Credit-to: @elkowar

New is the top
<img width="220" alt="Screenshot 2024-04-24 at 19 00 48"
src="https://github.com/zed-industries/zed/assets/94272/9d917bf1-e175-494d-9653-757d15584921">

Release Notes:

- N/A
2024-04-24 19:17:10 -06:00
Marshall Bowers
583a662ddc Fix issues with drafting release notes in CI (#10955)
This PR addresses some issues I ran into with the way we draft release
notes in CI when doing builds.

The first issue I encountered was that `script/draft-release-notes` was
failing, seemingly due to CI doing a shallow Git checkout and not having
all of the tags available in order to compare then. This was addressed
by setting the `fetch-depth` during the Git checkout.

The second issue is that (in my opinion) we shouldn't fail the build if
drafting release notes fails. After well, we're doing it as a
convenience to ourselves, and it isn't a mandatory part of the build.
This was addressed by making any failures in
`script/draft-release-notes` not fail the CI step as a whole.

These changes were already applied to the `v0.133.x` branch.

Release Notes:

- N/A
2024-04-24 18:56:36 -04:00
Conrad Irwin
64617a0ede Read settings in headless mode (#10950)
Release Notes:

- N/A
2024-04-24 16:06:36 -06:00
Marshall Bowers
b673494f4d Restore the previous styles for single-line editors (#10951)
This PR fixes a bug introduced in #10870 that caused editors used as
single-line inputs to have the wrong text style.

If this change was intentional for something relating to the new
Assistant panel, we'll need to figure out a way to change it without
breaking these other usages.

### Before

<img width="589" alt="Screenshot 2024-04-24 at 5 35 36 PM"
src="https://github.com/zed-industries/zed/assets/1486634/31624cfd-75d1-4771-9402-c14ef9e9483e">

<img width="326" alt="Screenshot 2024-04-24 at 5 35 46 PM"
src="https://github.com/zed-industries/zed/assets/1486634/1b76a3ef-7205-49ee-b391-7609f90461bd">

### After

<img width="588" alt="Screenshot 2024-04-24 at 5 36 14 PM"
src="https://github.com/zed-industries/zed/assets/1486634/9d550ee2-80c0-4afb-9b45-a2956471c546">

<img width="260" alt="Screenshot 2024-04-24 at 5 36 31 PM"
src="https://github.com/zed-industries/zed/assets/1486634/63240f27-1679-45d5-b39c-016860ff9683">

Release Notes:

- Fixed a bug where some inputs were using the wrong font style
(preview-only).
2024-04-24 17:47:25 -04:00
Dzmitry Malyshau
53f67a8241 Update blade with transparency and exclusive fullscreen fixes (#10880)
Release Notes:

- N/A

Picks up https://github.com/kvark/blade/pull/113 and a bunch of other
fixes.
Should prevent the exclusive full-screen on Vulkan - related to #9728
cc @kazatsuyu 

Note: this PR doesn't enable transparency, this is left to follow-up
2024-04-24 13:02:11 -07:00
张小白
06d2d9da5f windows: Let the OS decide which font to use as the UI font (#10877)
On my computer, I get `Yahei UI`, which makes sense since I'm using a
Chinese operating system, and `Yahei UI` includes Chinese codepoints. On
an English operating system, `Segoe UI` should be used instead.

Edit: I also choose to use the UI font selected by the system as the
fallback font, rather than hard-coding the `Arial` font.

Release Notes:

- N/A
2024-04-24 13:00:25 -07:00
Maksim Bondarenkov
9e88155a48 Use winresource instead of embed-manifest (#10810)
use winresource for crates/zed and crates/storybook. tested on
`x86_64-pc-windows-gnu`. on `x86_64-pc-windows-msvc` I receive a error
message, that looks like a problem with my machine
 
Release Notes:

- N/A
2024-04-24 12:59:18 -07:00
Conrad Irwin
048fc7ad09 Allow cli to accept --dev-server-token (#10944)
Release Notes:

- N/A
2024-04-24 13:15:19 -06:00
Marshall Bowers
bd77232f65 dart: Bump to v0.0.2 (#10940)
This PR bumps the Dart extension to v0.0.2.

Changes:

- https://github.com/zed-industries/zed/pull/8347
- https://github.com/zed-industries/zed/pull/10552

Release Notes:

- N/A
2024-04-24 13:03:56 -04:00
Joseph T. Lyons
facd04c902 v0.134.x dev 2024-04-24 12:46:30 -04:00
242 changed files with 11531 additions and 7258 deletions

View File

@@ -9,7 +9,7 @@ on:
concurrency:
# Allow only one workflow per any non-`main` branch.
group: ${{ github.workflow }}-${{ github.event.input.branch }}
group: ${{ github.workflow }}-${{ inputs.branch }}
cancel-in-progress: true
jobs:

View File

@@ -173,6 +173,11 @@ jobs:
- name: Checkout repo
uses: actions/checkout@v4
with:
# We need to fetch more than one commit so that `script/draft-release-notes`
# is able to diff between the current and previous tag.
#
# 25 was chosen arbitrarily.
fetch-depth: 25
clean: false
submodules: "recursive"
@@ -206,7 +211,8 @@ jobs:
exit 1
fi
mkdir -p target/
script/draft-release-notes "$version" "$channel" > target/release-notes.md
# Ignore any errors that occur while drafting release notes to not fail the build.
script/draft-release-notes "$version" "$channel" > target/release-notes.md || true
- name: Generate license file
run: script/generate-licenses
@@ -254,15 +260,13 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
bundle-deb:
name: Create a *.deb Linux bundle
bundle-linux:
name: Create a Linux bundle
runs-on: ubuntu-22.04 # keep the version fixed to avoid libc and dynamic linked library issues
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
needs: [linux_tests]
env:
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
steps:
- name: Checkout repo
uses: actions/checkout@v4
@@ -306,28 +310,26 @@ jobs:
exit 1
fi
# TODO linux : Find a way to add licenses to the final bundle
# - name: Generate license file
# run: script/generate-licenses
- name: Generate license file
run: script/generate-licenses
- name: Create Linux *.deb bundle
- name: Create and upload Linux .tar.gz bundle
run: script/bundle-linux
- name: Upload app bundle to workflow run if main branch or specific label
- name: Upload Linux bundle to workflow run if main branch or specific label
uses: actions/upload-artifact@v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.deb
path: target/release/*.deb
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.tar.gz
path: zed-*.tar.gz
# TODO linux : make it stable enough to be uploaded as a release
# - uses: softprops/action-gh-release@v1
# name: Upload app bundle to release
# if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }}
# with:
# draft: true
# prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
# files: target/release/Zed.dmg
# body: ""
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload app bundle to release
uses: softprops/action-gh-release@v1
if: ${{ env.RELEASE_CHANNEL == 'preview' }}
with:
draft: true
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
files: target/release/zed-linux-x86_64.tar.gz
body: ""
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -94,7 +94,7 @@ jobs:
run: script/upload-nightly macos
bundle-deb:
name: Create a *.deb Linux bundle
name: Create a Linux *.tar.gz bundle
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-22.04 # keep the version fixed to avoid libc and dynamic linked library issues
needs: tests
@@ -125,12 +125,11 @@ jobs:
echo "Publishing version: ${version} on release channel nightly"
echo "nightly" > crates/zed/RELEASE_CHANNEL
# TODO linux : find a way to add licenses to the final bundle
# - name: Generate license file
# run: script/generate-licenses
- name: Generate license file
run: script/generate-licenses
- name: Create Linux *.deb bundle
- name: Create Linux .tar.gz bundle
run: script/bundle-linux
- name: Upload Zed Nightly
run: script/upload-nightly linux-deb
run: script/upload-nightly linux-targz

119
Cargo.lock generated
View File

@@ -284,21 +284,21 @@ checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
[[package]]
name = "ash"
version = "0.37.3+1.3.251"
version = "0.38.0+1.3.281"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39e9c3835d686b0a6084ab4234fcd1b07dbf6e4767dce60874b12356a25ecd4a"
checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f"
dependencies = [
"libloading 0.7.4",
"libloading 0.8.0",
]
[[package]]
name = "ash-window"
version = "0.12.0"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b912285a7c29f3a8f87ca6f55afc48768624e5e33ec17dbd2f2075903f5e35ab"
checksum = "52bca67b61cb81e5553babde81b8211f713cb6db79766f80168f3e5f40ea6c82"
dependencies = [
"ash",
"raw-window-handle 0.5.2",
"raw-window-handle 0.6.0",
"raw-window-metal",
]
@@ -379,9 +379,11 @@ dependencies = [
"assets",
"assistant_tooling",
"client",
"collections",
"editor",
"env_logger",
"feature_flags",
"fs",
"futures 0.3.28",
"gpui",
"language",
@@ -399,6 +401,7 @@ dependencies = [
"serde",
"serde_json",
"settings",
"story",
"theme",
"ui",
"util",
@@ -1479,7 +1482,7 @@ dependencies = [
[[package]]
name = "blade-graphics"
version = "0.4.0"
source = "git+https://github.com/kvark/blade?rev=810ec594358aafea29a4a3d8ab601d25292b2ce4#810ec594358aafea29a4a3d8ab601d25292b2ce4"
source = "git+https://github.com/kvark/blade?rev=e82eec97691c3acdb43494484be60d661edfebf3#e82eec97691c3acdb43494484be60d661edfebf3"
dependencies = [
"ash",
"ash-window",
@@ -1500,7 +1503,7 @@ dependencies = [
"mint",
"naga",
"objc",
"raw-window-handle 0.5.2",
"raw-window-handle 0.6.0",
"slab",
"wasm-bindgen",
"web-sys",
@@ -1509,7 +1512,7 @@ dependencies = [
[[package]]
name = "blade-macros"
version = "0.2.1"
source = "git+https://github.com/kvark/blade?rev=810ec594358aafea29a4a3d8ab601d25292b2ce4#810ec594358aafea29a4a3d8ab601d25292b2ce4"
source = "git+https://github.com/kvark/blade?rev=e82eec97691c3acdb43494484be60d661edfebf3#e82eec97691c3acdb43494484be60d661edfebf3"
dependencies = [
"proc-macro2",
"quote",
@@ -3181,13 +3184,17 @@ dependencies = [
"anyhow",
"client",
"collections",
"ctor",
"editor",
"env_logger",
"futures 0.3.28",
"gpui",
"language",
"log",
"lsp",
"pretty_assertions",
"project",
"rand 0.8.5",
"schemars",
"serde",
"serde_json",
@@ -3434,10 +3441,18 @@ dependencies = [
]
[[package]]
name = "embed-manifest"
version = "1.4.0"
name = "embed-resource"
version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41cd446c890d6bed1d8b53acef5f240069ebef91d6fae7c5f52efe61fe8b5eae"
checksum = "c6985554d0688b687c5cb73898a34fbe3ad6c24c58c238a4d91d5e840670ee9d"
dependencies = [
"cc",
"memchr",
"rustc_version",
"toml 0.8.10",
"vswhom",
"winreg 0.52.0",
]
[[package]]
name = "emojis"
@@ -3816,6 +3831,7 @@ dependencies = [
"ctor",
"editor",
"env_logger",
"futures 0.3.28",
"fuzzy",
"gpui",
"itertools 0.11.0",
@@ -4051,6 +4067,7 @@ dependencies = [
"anyhow",
"async-tar",
"async-trait",
"cocoa",
"collections",
"fsevent",
"futures 0.3.28",
@@ -4060,6 +4077,7 @@ dependencies = [
"lazy_static",
"libc",
"notify",
"objc",
"parking_lot",
"rope",
"serde",
@@ -4492,9 +4510,9 @@ dependencies = [
[[package]]
name = "gpu-alloc-ash"
version = "0.6.0"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2424bc9be88170e1a56e57c25d3d0e2dfdd22e8f328e892786aeb4da1415732"
checksum = "cbda7a18a29bc98c2e0de0435c347df935bf59489935d0cbd0b73f1679b6f79a"
dependencies = [
"ash",
"gpu-alloc-types",
@@ -4536,6 +4554,7 @@ dependencies = [
"cosmic-text",
"ctor",
"derive_more",
"embed-resource",
"env_logger",
"etagere",
"filedescriptor",
@@ -4561,7 +4580,6 @@ dependencies = [
"postage",
"profiling",
"rand 0.8.5",
"raw-window-handle 0.5.2",
"raw-window-handle 0.6.0",
"refineable",
"resvg",
@@ -4724,6 +4742,7 @@ dependencies = [
"project",
"rpc",
"settings",
"shellexpand",
"util",
]
@@ -5552,12 +5571,9 @@ dependencies = [
"regex",
"rope",
"rust-embed",
"schemars",
"serde",
"serde_derive",
"serde_json",
"settings",
"shellexpand",
"smol",
"task",
"text",
@@ -5568,12 +5584,10 @@ dependencies = [
"tree-sitter-c",
"tree-sitter-cpp",
"tree-sitter-css",
"tree-sitter-elixir",
"tree-sitter-embedded-template",
"tree-sitter-go",
"tree-sitter-gomod",
"tree-sitter-gowork",
"tree-sitter-heex",
"tree-sitter-jsdoc",
"tree-sitter-json 0.20.0",
"tree-sitter-markdown",
@@ -5907,6 +5921,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-recursion 1.0.5",
"collections",
"editor",
"gpui",
"language",
@@ -5963,9 +5978,9 @@ dependencies = [
[[package]]
name = "memchr"
version = "2.6.3"
version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c"
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
[[package]]
name = "memfd"
@@ -7731,14 +7746,14 @@ checksum = "42a9830a0e1b9fb145ebb365b8bc4ccd75f290f98c0247deafbbe2c75cefb544"
[[package]]
name = "raw-window-metal"
version = "0.3.2"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac4ea493258d54c24cb46aa9345d099e58e2ea3f30dd63667fc54fc892f18e76"
checksum = "76e8caa82e31bb98fee12fa8f051c94a6aa36b07cddb03f0d4fc558988360ff1"
dependencies = [
"cocoa",
"core-graphics",
"objc",
"raw-window-handle 0.5.2",
"raw-window-handle 0.6.0",
]
[[package]]
@@ -7974,7 +7989,7 @@ dependencies = [
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"winreg",
"winreg 0.50.0",
]
[[package]]
@@ -8678,6 +8693,7 @@ dependencies = [
"languages",
"log",
"open_ai",
"parking_lot",
"project",
"serde",
"serde_json",
@@ -9483,12 +9499,12 @@ name = "storybook"
version = "0.1.0"
dependencies = [
"anyhow",
"assistant2",
"clap 4.4.4",
"collab_ui",
"ctrlc",
"dialoguer",
"editor",
"embed-manifest",
"fuzzy",
"gpui",
"indoc",
@@ -10519,7 +10535,7 @@ dependencies = [
[[package]]
name = "tree-sitter"
version = "0.20.100"
source = "git+https://github.com/tree-sitter/tree-sitter?rev=7f21c3b98c0749ac192da67a0d65dfe3eabc4a63#7f21c3b98c0749ac192da67a0d65dfe3eabc4a63"
source = "git+https://github.com/tree-sitter/tree-sitter?rev=7b4894ba2ae81b988846676f54c0988d4027ef4f#7b4894ba2ae81b988846676f54c0988d4027ef4f"
dependencies = [
"cc",
"regex",
@@ -10631,7 +10647,7 @@ dependencies = [
[[package]]
name = "tree-sitter-jsdoc"
version = "0.20.0"
source = "git+https://github.com/tree-sitter/tree-sitter-jsdoc#6a6cf9e7341af32d8e2b2e24a37fbfebefc3dc55"
source = "git+https://github.com/tree-sitter/tree-sitter-jsdoc?rev=6a6cf9e7341af32d8e2b2e24a37fbfebefc3dc55#6a6cf9e7341af32d8e2b2e24a37fbfebefc3dc55"
dependencies = [
"cc",
"tree-sitter",
@@ -11101,6 +11117,7 @@ dependencies = [
"futures 0.3.28",
"gpui",
"indoc",
"itertools 0.11.0",
"language",
"log",
"lsp",
@@ -11136,6 +11153,26 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
[[package]]
name = "vswhom"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b"
dependencies = [
"libc",
"vswhom-sys",
]
[[package]]
name = "vswhom-sys"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3b17ae1f6c8a2b28506cd96d412eebf83b4a0ff2cbefeeb952f2f9dfa44ba18"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "vte"
version = "0.13.0"
@@ -12209,6 +12246,16 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "winreg"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
dependencies = [
"cfg-if",
"windows-sys 0.48.0",
]
[[package]]
name = "winresource"
version = "0.1.17"
@@ -12607,7 +12654,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.133.0"
version = "0.134.0"
dependencies = [
"activity_indicator",
"anyhow",
@@ -12633,7 +12680,6 @@ dependencies = [
"db",
"diagnostics",
"editor",
"embed-manifest",
"env_logger",
"extension",
"extensions_ui",
@@ -12657,6 +12703,7 @@ dependencies = [
"markdown_preview",
"menu",
"mimalloc",
"nix 0.28.0",
"node_runtime",
"notifications",
"outline",
@@ -12679,6 +12726,7 @@ dependencies = [
"tab_switcher",
"task",
"tasks_ui",
"telemetry_events",
"terminal_view",
"theme",
"theme_selector",
@@ -12724,7 +12772,7 @@ dependencies = [
[[package]]
name = "zed_dart"
version = "0.0.1"
version = "0.0.2"
dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -12736,6 +12784,13 @@ dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "zed_elixir"
version = "0.0.1"
dependencies = [
"zed_extension_api 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "zed_elm"
version = "0.0.1"

View File

@@ -110,6 +110,7 @@ members = [
"extensions/csharp",
"extensions/dart",
"extensions/deno",
"extensions/elixir",
"extensions/elm",
"extensions/emmet",
"extensions/erlang",
@@ -249,9 +250,8 @@ async-recursion = "1.0.0"
async-tar = "0.4.2"
async-trait = "0.1"
bitflags = "2.4.2"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "810ec594358aafea29a4a3d8ab601d25292b2ce4" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "810ec594358aafea29a4a3d8ab601d25292b2ce4" }
blade-rwh = { package = "raw-window-handle", version = "0.5" }
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e82eec97691c3acdb43494484be60d661edfebf3" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "e82eec97691c3acdb43494484be60d661edfebf3" }
cap-std = "3.0"
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4.4", features = ["derive"] }
@@ -283,6 +283,8 @@ itertools = "0.11.0"
lazy_static = "1.4.0"
linkify = "0.10.0"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
nanoid = "0.4"
nix = "0.28"
ordered-float = "2.1.1"
palette = { version = "0.7.5", default-features = false, features = ["std"] }
parking_lot = "0.12.1"
@@ -341,7 +343,7 @@ tree-sitter-gowork = { git = "https://github.com/d1y/tree-sitter-go-work" }
rustc-demangle = "0.1.23"
tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "2e1348c3cf2c9323e87c2744796cf3f3868aa82a" }
tree-sitter-html = "0.19.0"
tree-sitter-jsdoc = { git = "https://github.com/tree-sitter/tree-sitter-jsdoc", ref = "6a6cf9e7341af32d8e2b2e24a37fbfebefc3dc55" }
tree-sitter-jsdoc = { git = "https://github.com/tree-sitter/tree-sitter-jsdoc", rev = "6a6cf9e7341af32d8e2b2e24a37fbfebefc3dc55" }
tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "40a81c01a40ac48744e0c8ccabbaba1920441199" }
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
tree-sitter-proto = { git = "https://github.com/rewinfrey/tree-sitter-proto", rev = "36d54f288aee112f13a67b550ad32634d0c2cb52" }
@@ -407,7 +409,7 @@ features = [
]
[patch.crates-io]
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "7f21c3b98c0749ac192da67a0d65dfe3eabc4a63" }
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "7b4894ba2ae81b988846676f54c0988d4027ef4f" }
# Workaround for a broken nightly build of gpui: See #7644 and revisit once 0.5.3 is released.
pathfinder_simd = { git = "https://github.com/servo/pathfinder.git", rev = "30419d07660dc11a21e42ef4a7fa329600cff152" }

102
README.md
View File

@@ -1,51 +1,51 @@
# Zed
[![CI](https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg)](https://github.com/zed-industries/zed/actions/workflows/ci.yml)
Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter).
## Installation
You can [download](https://zed.dev/download) Zed today for macOS (v10.15+).
Support for additional platforms is on our [roadmap](https://zed.dev/roadmap):
- Linux ([tracking issue](https://github.com/zed-industries/zed/issues/7015))
- Windows ([tracking issue](https://github.com/zed-industries/zed/issues/5394))
- Web ([tracking issue](https://github.com/zed-industries/zed/issues/5396))
For macOS users, you can also install Zed using [Homebrew](https://brew.sh/):
```sh
brew install --cask zed
```
Alternatively, to install the Preview release:
```sh
brew tap homebrew/cask-versions
brew install zed-preview
```
## Developing Zed
- [Building Zed for macOS](./docs/src/developing_zed__building_zed_macos.md)
- [Building Zed for Linux](./docs/src/developing_zed__building_zed_linux.md)
- [Building Zed for Windows](./docs/src/developing_zed__building_zed_windows.md)
- [Running Collaboration Locally](./docs/src/developing_zed__local_collaboration.md)
## Contributing
See [CONTRIBUTING.md](./CONTRIBUTING.md) for ways you can contribute to Zed.
Also... we're hiring! Check out our [jobs](https://zed.dev/jobs) page for open roles.
## Licensing
License information for third party dependencies must be correctly provided for CI to pass.
We use [`cargo-about`](https://github.com/EmbarkStudios/cargo-about) to automatically comply with open source licenses. If CI is failing, check the following:
- Is it showing a `no license specified` error for a crate you've created? If so, add `publish = false` under `[package]` in your crate's Cargo.toml.
- Is the error `failed to satisfy license requirements` for a dependency? If so, first determine what license the project has and whether this system is sufficient to comply with this license's requirements. If you're unsure, ask a lawyer. Once you've verified that this system is acceptable add the license's SPDX identifier to the `accepted` array in `script/licenses/zed-licenses.toml`.
- Is `cargo-about` unable to find the license for a dependency? If so, add a clarification field at the end of `script/licenses/zed-licenses.toml`, as specified in the [cargo-about book](https://embarkstudios.github.io/cargo-about/cli/generate/config.html#crate-configuration).
# Zed
[![CI](https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg)](https://github.com/zed-industries/zed/actions/workflows/ci.yml)
Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter).
## Installation
You can [download](https://zed.dev/download) Zed today for macOS (v10.15+).
Support for additional platforms is on our [roadmap](https://zed.dev/roadmap):
- Linux ([tracking issue](https://github.com/zed-industries/zed/issues/7015))
- Windows ([tracking issue](https://github.com/zed-industries/zed/issues/5394))
- Web ([tracking issue](https://github.com/zed-industries/zed/issues/5396))
For macOS users, you can also install Zed using [Homebrew](https://brew.sh/):
```sh
brew install --cask zed
```
Alternatively, to install the Preview release:
```sh
brew tap homebrew/cask-versions
brew install zed-preview
```
## Developing Zed
- [Building Zed for macOS](./docs/src/developing_zed__building_zed_macos.md)
- [Building Zed for Linux](./docs/src/developing_zed__building_zed_linux.md)
- [Building Zed for Windows](./docs/src/developing_zed__building_zed_windows.md)
- [Running Collaboration Locally](./docs/src/developing_zed__local_collaboration.md)
## Contributing
See [CONTRIBUTING.md](./CONTRIBUTING.md) for ways you can contribute to Zed.
Also... we're hiring! Check out our [jobs](https://zed.dev/jobs) page for open roles.
## Licensing
License information for third party dependencies must be correctly provided for CI to pass.
We use [`cargo-about`](https://github.com/EmbarkStudios/cargo-about) to automatically comply with open source licenses. If CI is failing, check the following:
- Is it showing a `no license specified` error for a crate you've created? If so, add `publish = false` under `[package]` in your crate's Cargo.toml.
- Is the error `failed to satisfy license requirements` for a dependency? If so, first determine what license the project has and whether this system is sufficient to comply with this license's requirements. If you're unsure, ask a lawyer. Once you've verified that this system is acceptable add the license's SPDX identifier to the `accepted` array in `script/licenses/zed-licenses.toml`.
- Is `cargo-about` unable to find the license for a dependency? If so, add a clarification field at the end of `script/licenses/zed-licenses.toml`, as specified in the [cargo-about book](https://embarkstudios.github.io/cargo-about/cli/generate/config.html#crate-configuration).

View File

@@ -1,6 +0,0 @@
User input begins on a line starting with /.
Don't apologize ever.
Never say "I apologize".
Use simple language and don't flatter the users.
Keep it short.
Risk being rude.

View File

@@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1791_43)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.8473 4.79901C13.7531 4.63587 13.6232 4.4934 13.4803 4.4109L8.51958 1.54683C8.23382 1.38181 7.76613 1.38181 7.48037 1.54683L2.51961 4.4109C2.2338 4.57587 2 4.98089 2 5.31089V11.0391C2 11.2041 2.05864 11.3879 2.15283 11.551C2.24699 11.7141 2.37691 11.8566 2.51977 11.939L7.48053 14.8031C7.7663 14.9681 8.23398 14.9681 8.51974 14.8031L13.4805 11.939C13.6234 11.8565 13.7532 11.714 13.8473 11.5509C13.9415 11.3878 14 11.204 14 11.039V5.31085C14 5.14583 13.9415 4.96211 13.8473 4.79901ZM4 8.175C4 10.3806 5.79438 12.175 7.99998 12.175C9.42327 12.175 10.7506 11.4091 11.464 10.1761L9.73295 9.17441C9.37586 9.79162 8.71182 10.175 7.99998 10.175C6.89716 10.175 5.99999 9.27778 5.99999 8.175C5.99999 7.07218 6.89716 6.17501 7.99998 6.17501C8.71174 6.17501 9.37578 6.55838 9.73284 7.17548L11.4639 6.17375C10.7505 4.9409 9.42319 4.17502 7.99998 4.17502C5.79438 4.17502 4 5.9694 4 8.175Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_1791_43">
<rect width="11.9999" height="13.5039" fill="white" transform="translate(2 1.42307)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1791_60)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.4803 4.4109C13.6232 4.4934 13.7531 4.63587 13.8473 4.79901C13.9415 4.96211 14 5.14583 14 5.31085V5.78958H12.8036V7.50745H11.0857V8.99892H12.8036V10.7168H14V11.039C14 11.204 13.9415 11.3878 13.8473 11.5509C13.7532 11.714 13.6234 11.8565 13.4805 11.939L8.51974 14.8031C8.23398 14.9681 7.7663 14.9681 7.48053 14.8031L2.51977 11.939C2.37691 11.8566 2.24699 11.7141 2.15283 11.551C2.05864 11.3879 2 11.2041 2 11.0391V5.31089C2 4.98089 2.2338 4.57587 2.51961 4.4109L7.48037 1.54683C7.76613 1.38181 8.23382 1.38181 8.51958 1.54683L13.4803 4.4109ZM7.99998 12.175C5.79438 12.175 4 10.3806 4 8.175C4 5.9694 5.79438 4.17502 7.99998 4.17502C9.42319 4.17502 10.7505 4.9409 11.4639 6.17375L9.73284 7.17548C9.37578 6.55838 8.71174 6.17501 7.99998 6.17501C6.89716 6.17501 5.99999 7.07218 5.99999 8.175C5.99999 9.27778 6.89716 10.175 7.99998 10.175C8.71182 10.175 9.37586 9.79162 9.73295 9.17441L11.464 10.1761C10.7506 11.4091 9.42327 12.175 7.99998 12.175Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_1791_60">
<rect width="11.9999" height="13.5039" fill="white" transform="translate(2 1.42307)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -19,11 +19,11 @@
"bash_profile": "terminal",
"bashrc": "terminal",
"bmp": "image",
"c": "code",
"cc": "code",
"cjs": "code",
"c": "c",
"cc": "cpp",
"cjs": "javascript",
"conf": "settings",
"cpp": "code",
"cpp": "cpp",
"css": "css",
"csv": "storage",
"cts": "typescript",
@@ -58,7 +58,8 @@
"gitmodules": "vcs",
"go": "go",
"graphql": "graphql",
"h": "code",
"h": "c",
"hpp": "cpp",
"handlebars": "code",
"hbs": "template",
"heex": "elixir",
@@ -77,7 +78,8 @@
"jp2": "image",
"jpeg": "image",
"jpg": "image",
"js": "code",
"js": "javascript",
"jsx": "react",
"json": "storage",
"jsonc": "storage",
"jxl": "image",
@@ -95,7 +97,7 @@
"mdx": "document",
"metadata": "code",
"mkv": "video",
"mjs": "code",
"mjs": "javascript",
"mka": "audio",
"ml": "ocaml",
"mli": "ocaml",
@@ -152,7 +154,7 @@
"ts": "typescript",
"tsv": "storage",
"ttf": "font",
"tsx": "code",
"tsx": "react",
"txt": "document",
"tcl": "tcl",
"vue": "vue",
@@ -195,6 +197,12 @@
"collapsed_folder": {
"icon": "icons/file_icons/folder.svg"
},
"c": {
"icon": "icons/file_icons/c.svg"
},
"cpp": {
"icon": "icons/file_icons/cpp.svg"
},
"css": {
"icon": "icons/file_icons/css.svg"
},
@@ -255,6 +263,9 @@
"java": {
"icon": "icons/file_icons/java.svg"
},
"javascript": {
"icon": "icons/file_icons/javascript.svg"
},
"kotlin": {
"icon": "icons/file_icons/kotlin.svg"
},
@@ -291,15 +302,18 @@
"python": {
"icon": "icons/file_icons/python.svg"
},
"r": {
"icon": "icons/file_icons/r.svg"
},
"react": {
"icon": "icons/file_icons/react.svg"
},
"ruby": {
"icon": "icons/file_icons/ruby.svg"
},
"rust": {
"icon": "icons/file_icons/rust.svg"
},
"r": {
"icon": "icons/file_icons/r.svg"
},
"settings": {
"icon": "icons/file_icons/settings.svg"
},

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 2C2.89543 2 2 2.89543 2 4V12C2 13.1046 2.89543 14 4 14H12C13.1046 14 14 13.1046 14 12V4C14 2.89543 13.1046 2 12 2H4ZM7.26917 6.80584H6.04784V10.8808C6.04784 11.0672 6.02025 11.2241 5.96508 11.3516C5.90991 11.4791 5.82906 11.5761 5.72253 11.6427C5.61789 11.7074 5.48948 11.7397 5.33729 11.7397C5.19271 11.7397 5.06715 11.7112 4.96062 11.6541C4.85599 11.5951 4.77323 11.5114 4.71235 11.403C4.65338 11.2926 4.62199 11.1604 4.61819 11.0063H3.38829C3.38639 11.3944 3.46914 11.7169 3.63655 11.9737C3.80396 12.2286 4.03035 12.4189 4.31571 12.5444C4.60297 12.6681 4.92257 12.7299 5.27451 12.7299C5.67021 12.7299 6.0174 12.6547 6.31607 12.5045C6.61475 12.3542 6.84779 12.1402 7.0152 11.8624C7.18452 11.5847 7.26917 11.2574 7.26917 10.8808V6.80584ZM11.1672 7.95013C11.3403 8.07759 11.4383 8.25641 11.4611 8.4866H12.6453C12.6396 8.13846 12.5464 7.83218 12.3657 7.56775C12.185 7.30331 11.9319 7.0969 11.6066 6.94852C11.2832 6.80013 10.9046 6.72594 10.4709 6.72594C10.0448 6.72594 9.66429 6.80013 9.32947 6.94852C8.99464 7.0969 8.73116 7.30331 8.53902 7.56775C8.34878 7.83218 8.25461 8.14132 8.25652 8.49516C8.25461 8.92701 8.39634 9.27039 8.6817 9.52531C8.96706 9.78023 9.3561 9.96762 9.84882 10.0875L10.4852 10.2473C10.6982 10.2986 10.878 10.3557 11.0245 10.4185C11.1729 10.4813 11.2851 10.5574 11.3612 10.6468C11.4392 10.7362 11.4782 10.8465 11.4782 10.9778C11.4782 11.1186 11.4354 11.2432 11.3498 11.3516C11.2642 11.46 11.1434 11.5447 10.9874 11.6056C10.8333 11.6665 10.6516 11.6969 10.4424 11.6969C10.2293 11.6969 10.0381 11.6646 9.86879 11.5999C9.70138 11.5333 9.56727 11.4353 9.46644 11.306C9.36751 11.1747 9.31139 11.0111 9.29808 10.8151H8.10242C8.11193 11.2356 8.21371 11.5885 8.40776 11.8738C8.6037 12.1573 8.87574 12.3713 9.22388 12.5159C9.57392 12.6605 9.98484 12.7327 10.4566 12.7327C10.9322 12.7327 11.3384 12.6614 11.6751 12.5187C12.0137 12.3741 12.2725 12.1715 12.4513 11.9109C12.632 11.6484 12.7233 11.3383 12.7252 10.9806C12.7233 10.7371 12.6786 10.5212 12.5911 10.3329C12.5055 10.1445 12.3847 9.98093 12.2287 9.84206C12.0727 9.70319 11.8882 9.58619 11.6751 9.49107C11.4621 9.39595 11.2281 9.31985 10.9731 9.26278L10.4481 9.13722C10.3206 9.10869 10.2008 9.07444 10.0885 9.03449C9.97628 8.99264 9.87736 8.94413 9.79175 8.88896C9.70614 8.83189 9.63861 8.76435 9.58914 8.68635C9.54158 8.60836 9.51971 8.51704 9.52351 8.41241C9.52351 8.28685 9.55966 8.17461 9.63195 8.07569C9.70614 7.97676 9.81267 7.89971 9.95155 7.84455C10.0904 7.78747 10.2607 7.75894 10.4623 7.75894C10.7591 7.75894 10.9941 7.82267 11.1672 7.95013Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.99752 9.14577C8.62961 9.14577 9.14201 8.63336 9.14201 8.00127C9.14201 7.36919 8.62961 6.85678 7.99752 6.85678C7.36543 6.85678 6.85303 7.36919 6.85303 8.00127C6.85303 8.63336 7.36543 9.14577 7.99752 9.14577Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.37507 12.5467C5.35849 12.5371 5.26876 12.4764 5.21215 12.1996C5.15576 11.924 5.15423 11.5219 5.24075 11.0018C5.25336 10.926 5.2677 10.8485 5.28376 10.7695C5.61535 10.8289 5.96088 10.8775 6.31735 10.9146C6.52765 11.2048 6.74254 11.4797 6.9598 11.7371C6.8994 11.7906 6.83947 11.8417 6.7801 11.8906C6.37292 12.2255 6.02397 12.4252 5.75706 12.5142C5.48905 12.6036 5.39166 12.5562 5.37507 12.5467ZM4.63463 8.00002C4.48846 7.67278 4.35781 7.34921 4.24347 7.03232C4.16699 7.05793 4.09271 7.08426 4.0207 7.11126C3.52701 7.29639 3.17959 7.49875 2.96906 7.6854C2.75767 7.87282 2.75 7.98085 2.75 8C2.75 8.01916 2.75767 8.12719 2.96906 8.31461C3.17959 8.50126 3.52701 8.70361 4.0207 8.88875C4.09271 8.91575 4.167 8.94208 4.24348 8.96769C4.35782 8.65081 4.48846 8.32725 4.63463 8.00002ZM3.49402 5.70677C3.6016 5.66643 3.71247 5.6276 3.82645 5.59035C3.80173 5.47305 3.77992 5.35765 3.76108 5.24434C3.65732 4.62055 3.63633 4.01923 3.74257 3.49981C3.84858 2.98153 4.10358 2.45543 4.62507 2.15435C5.14656 1.85326 5.72968 1.89548 6.23152 2.06281C6.73448 2.23051 7.24474 2.54935 7.73308 2.95111C7.82179 3.02409 7.91084 3.10068 8.00007 3.18075C8.0893 3.10068 8.17835 3.02409 8.26706 2.9511C8.7554 2.54935 9.26566 2.23051 9.76862 2.06281C10.2705 1.89548 10.8536 1.85326 11.3751 2.15435C11.8966 2.45543 12.1516 2.98153 12.2576 3.49981C12.3638 4.01923 12.3428 4.62055 12.2391 5.24434C12.2202 5.35766 12.1984 5.47308 12.1737 5.59039C12.2876 5.62763 12.3984 5.66644 12.506 5.70677C13.0981 5.9288 13.6293 6.21129 14.026 6.56301C14.4219 6.91396 14.75 7.39783 14.75 8C14.75 8.60218 14.4219 9.08605 14.026 9.437C13.6293 9.78872 13.0981 10.0712 12.506 10.2932C12.3984 10.3336 12.2876 10.3724 12.1737 10.4096C12.1984 10.5269 12.2202 10.6424 12.2391 10.7557C12.3428 11.3795 12.3638 11.9808 12.2576 12.5002C12.1516 13.0185 11.8966 13.5446 11.3751 13.8457C10.8536 14.1468 10.2705 14.1046 9.76862 13.9372C9.26566 13.7695 8.7554 13.4507 8.26706 13.0489C8.17835 12.976 8.08931 12.8994 8.00007 12.8193C7.91084 12.8994 7.82179 12.976 7.73308 13.0489C7.24474 13.4507 6.73448 13.7695 6.23152 13.9372C5.72968 14.1046 5.14657 14.1468 4.62507 13.8457C4.10358 13.5446 3.84859 13.0185 3.74257 12.5002C3.63633 11.9808 3.65732 11.3795 3.76108 10.7557C3.77993 10.6424 3.80174 10.527 3.82646 10.4097C3.71248 10.3724 3.6016 10.3336 3.49402 10.2932C2.90192 10.0712 2.37066 9.78872 1.97395 9.437C1.57812 9.08605 1.25 8.60218 1.25 8C1.25 7.39783 1.57812 6.91396 1.97395 6.56301C2.37066 6.21129 2.90192 5.9288 3.49402 5.70677ZM9.22005 11.8906C9.16067 11.8417 9.10075 11.7906 9.04034 11.7371C9.25761 11.4797 9.4725 11.2048 9.68281 10.9145C10.0393 10.8775 10.3848 10.8289 10.7164 10.7695C10.7324 10.8485 10.7468 10.926 10.7594 11.0018C10.8459 11.5219 10.8444 11.924 10.788 12.1996C10.7314 12.4764 10.6417 12.5371 10.6251 12.5467C10.6085 12.5562 10.5111 12.6036 10.2431 12.5142C9.97617 12.4252 9.62722 12.2255 9.22005 11.8906ZM6.31737 5.08544C6.52766 4.79525 6.74254 4.52034 6.9598 4.26289C6.89939 4.20948 6.83947 4.15832 6.7801 4.10948C6.37292 3.77449 6.02397 3.57479 5.75706 3.4858C5.48905 3.39644 5.39165 3.44381 5.37507 3.45339C5.35849 3.46296 5.26876 3.52362 5.21214 3.80041C5.15576 4.07605 5.15423 4.4781 5.24075 4.99822C5.25336 5.07405 5.2677 5.15152 5.28375 5.23053C5.61535 5.1711 5.96089 5.12247 6.31737 5.08544ZM9.04034 4.26289C9.2576 4.52034 9.47249 4.79526 9.68278 5.08546C10.0393 5.12248 10.3848 5.17112 10.7164 5.23055C10.7324 5.15154 10.7468 5.07406 10.7594 4.99822C10.8459 4.4781 10.8444 4.07605 10.788 3.80041C10.7314 3.52362 10.6417 3.46296 10.6251 3.45339C10.6085 3.44381 10.5111 3.39644 10.2431 3.4858C9.97617 3.57479 9.62722 3.77449 9.22005 4.10947C9.16067 4.15832 9.10074 4.20948 9.04034 4.26289ZM11.3655 8.00002C11.5117 8.32723 11.6423 8.65078 11.7566 8.96765C11.8331 8.94205 11.9073 8.91574 11.9793 8.88875C12.473 8.70361 12.8204 8.50126 13.0309 8.31461C13.2423 8.12719 13.25 8.01916 13.25 8C13.25 7.98085 13.2423 7.87282 13.0309 7.6854C12.8204 7.49875 12.473 7.29639 11.9793 7.11126C11.9073 7.08427 11.8331 7.05796 11.7567 7.03237C11.6423 7.34924 11.5117 7.6728 11.3655 8.00002ZM7.99752 10.1458C9.18189 10.1458 10.142 9.18565 10.142 8.00127C10.142 6.8169 9.18189 5.85678 7.99752 5.85678C6.81315 5.85678 5.85303 6.8169 5.85303 8.00127C5.85303 9.18564 6.81315 10.1458 7.99752 10.1458Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@@ -297,13 +297,8 @@
"ctrl-shift-k": "editor::DeleteLine",
"alt-up": "editor::MoveLineUp",
"alt-down": "editor::MoveLineDown",
"ctrl-alt-shift-up": [
"editor::DuplicateLine",
{
"move_upwards": true
}
],
"ctrl-alt-shift-down": "editor::DuplicateLine",
"ctrl-alt-shift-up": "editor::DuplicateLineUp",
"ctrl-alt-shift-down": "editor::DuplicateLineDown",
"ctrl-shift-left": "editor::SelectToPreviousWordStart",
"ctrl-shift-right": "editor::SelectToNextWordEnd",
"ctrl-shift-up": "editor::SelectLargerSyntaxNode", //todo(linux) tmp keybinding
@@ -554,8 +549,8 @@
"alt-ctrl-shift-c": "project_panel::CopyRelativePath",
"f2": "project_panel::Rename",
"enter": "project_panel::Rename",
"backspace": "project_panel::Delete",
"delete": "project_panel::Delete",
"backspace": "project_panel::Trash",
"delete": "project_panel::Trash",
"ctrl-backspace": ["project_panel::Delete", { "skip_prompt": true }],
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": true }],
"alt-ctrl-r": "project_panel::RevealInFinder",
@@ -593,12 +588,6 @@
"tab": "channel_modal::ToggleMode"
}
},
{
"context": "ChatPanel > MessageEditor",
"bindings": {
"escape": "chat_panel::CloseReplyPreview"
}
},
{
"context": "FileFinder",
"bindings": { "ctrl-shift-p": "file_finder::SelectPrev" }

View File

@@ -212,7 +212,8 @@
"context": "AssistantChat > Editor", // Used in the assistant2 crate
"bindings": {
"enter": ["assistant2::Submit", "Simple"],
"cmd-enter": ["assistant2::Submit", "Codebase"]
"cmd-enter": ["assistant2::Submit", "Codebase"],
"escape": "assistant2::Cancel"
}
},
{
@@ -575,8 +576,8 @@
"alt-cmd-shift-c": "project_panel::CopyRelativePath",
"f2": "project_panel::Rename",
"enter": "project_panel::Rename",
"backspace": "project_panel::Delete",
"delete": "project_panel::Delete",
"backspace": "project_panel::Trash",
"delete": "project_panel::Trash",
"cmd-backspace": ["project_panel::Delete", { "skip_prompt": true }],
"cmd-delete": ["project_panel::Delete", { "skip_prompt": true }],
"alt-cmd-r": "project_panel::RevealInFinder",

View File

@@ -17,7 +17,11 @@
"cmd-enter": "menu::SecondaryConfirm",
"escape": "menu::Cancel",
"ctrl-c": "menu::Cancel",
"cmd-q": "storybook::Quit"
"cmd-q": "storybook::Quit",
"backspace": "editor::Backspace",
"delete": "editor::Delete",
"left": "editor::MoveLeft",
"right": "editor::MoveRight"
}
}
]

View File

@@ -435,6 +435,12 @@
]
}
},
{
"context": "Editor && vim_operator == ys",
"bindings": {
"s": "vim::CurrentLine"
}
},
{
"context": "Editor && VimObject",
"bindings": {
@@ -625,7 +631,10 @@
"t": "project_panel::OpenPermanent",
"v": "project_panel::OpenPermanent",
"p": "project_panel::Open",
"x": "project_panel::RevealInFinder"
"x": "project_panel::RevealInFinder",
"shift-g": "menu::SelectLast",
"g g": "menu::SelectFirst",
"-": "project_panel::SelectParent"
}
}
]

View File

@@ -155,6 +155,8 @@
// 4. Never show the scrollbar:
// "never"
"show": "auto",
// Whether to show cursor positions in the scrollbar.
"cursors": true,
// Whether to show git diff indicators in the scrollbar.
"git_diff": true,
// Whether to show buffer search results in the scrollbar.
@@ -329,8 +331,10 @@
// when you switch to another file unless you explicitly pin them.
// This is useful for quickly viewing files without cluttering your workspace.
"enabled": true,
// Whether to open files in preview mode when selected from the file finder.
"enable_preview_from_file_finder": false
// Whether to open tabs in preview mode when selected from the file finder.
"enable_preview_from_file_finder": false,
// Whether a preview tab gets replaced when code navigation is used to navigate away from the tab.
"enable_preview_from_code_navigation": false
},
// Whether or not to remove any trailing whitespace from lines of a buffer
// before saving it.
@@ -555,27 +559,6 @@
// Existing terminals will not pick up this change until they are recreated.
// "max_scroll_history_lines": 10000,
},
// Settings specific to our elixir integration
"elixir": {
// Change the LSP zed uses for elixir.
// Note that changing this setting requires a restart of Zed
// to take effect.
//
// May take 3 values:
// 1. Use the standard ElixirLS, this is the default
// "lsp": "elixir_ls"
// 2. Use the experimental NextLs
// "lsp": "next_ls",
// 3. Use a language server installed locally on your machine:
// "lsp": {
// "local": {
// "path": "~/next-ls/bin/start",
// "arguments": ["--stdio"]
// }
// },
//
"lsp": "elixir_ls"
},
"code_actions_on_format": {},
// An object whose keys are language names, and whose values
// are arrays of filenames or extensions of files that should

View File

@@ -99,6 +99,7 @@ impl ActivityIndicator {
Box::new(
cx.new_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)),
),
None,
cx,
);
}

View File

@@ -2873,7 +2873,7 @@ impl InlineAssistant {
cx.theme().colors().text
},
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features,
font_features: settings.ui_font.features.clone(),
font_size: rems(0.875).into(),
font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal,

View File

@@ -241,7 +241,7 @@ impl AuthenticationPrompt {
let text_style = TextStyle {
color: cx.theme().colors().text,
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features,
font_features: settings.ui_font.features.clone(),
font_size: rems(0.875).into(),
font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal,

View File

@@ -5,37 +5,42 @@ edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/assistant2.rs"
[[example]]
name = "assistant_example"
path = "examples/assistant_example.rs"
crate-type = ["bin"]
[features]
default = []
stories = ["dep:story"]
[dependencies]
anyhow.workspace = true
assistant_tooling.workspace = true
client.workspace = true
collections.workspace = true
editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
futures.workspace = true
gpui.workspace = true
language.workspace = true
log.workspace = true
nanoid.workspace = true
open_ai.workspace = true
project.workspace = true
rich_text.workspace = true
semantic_index.workspace = true
schemars.workspace = true
semantic_index.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
story = { workspace = true, optional = true }
theme.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
nanoid = "0.4"
[dev-dependencies]
assets.workspace = true
@@ -52,6 +57,3 @@ settings = { workspace = true, features = ["test-support"] }
theme = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] }
workspace = { workspace = true, features = ["test-support"] }
[lints]
workspace = true

View File

@@ -1,129 +0,0 @@
use anyhow::Context as _;
use assets::Assets;
use assistant2::{tools::ProjectIndexTool, AssistantPanel};
use assistant_tooling::ToolRegistry;
use client::Client;
use gpui::{actions, App, AppContext, KeyBinding, Task, View, WindowOptions};
use language::LanguageRegistry;
use project::Project;
use semantic_index::{OpenAiEmbeddingModel, OpenAiEmbeddingProvider, SemanticIndex};
use settings::{KeymapFile, DEFAULT_KEYMAP_PATH};
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use theme::LoadThemes;
use ui::{div, prelude::*, Render};
use util::{http::HttpClientWithUrl, ResultExt as _};
actions!(example, [Quit]);
fn main() {
let args: Vec<String> = std::env::args().collect();
env_logger::init();
App::new().with_assets(Assets).run(|cx| {
cx.bind_keys(Some(KeyBinding::new("cmd-q", Quit, None)));
cx.on_action(|_: &Quit, cx: &mut AppContext| {
cx.quit();
});
if args.len() < 2 {
eprintln!(
"Usage: cargo run --example assistant_example -p assistant2 -- <project_path>"
);
cx.quit();
return;
}
settings::init(cx);
language::init(cx);
Project::init_settings(cx);
editor::init(cx);
theme::init(LoadThemes::JustBase, cx);
Assets.load_fonts(cx).unwrap();
KeymapFile::load_asset(DEFAULT_KEYMAP_PATH, cx).unwrap();
client::init_settings(cx);
release_channel::init("0.130.0", cx);
let client = Client::production(cx);
{
let client = client.clone();
cx.spawn(|cx| async move { client.authenticate_and_connect(false, &cx).await })
.detach_and_log_err(cx);
}
assistant2::init(client.clone(), cx);
let language_registry = Arc::new(LanguageRegistry::new(
Task::ready(()),
cx.background_executor().clone(),
));
let node_runtime = node_runtime::RealNodeRuntime::new(client.http_client());
languages::init(language_registry.clone(), node_runtime, cx);
let http = Arc::new(HttpClientWithUrl::new("http://localhost:11434"));
let api_key = std::env::var("OPENAI_API_KEY").expect("OPENAI_API_KEY not set");
let embedding_provider = OpenAiEmbeddingProvider::new(
http.clone(),
OpenAiEmbeddingModel::TextEmbedding3Small,
open_ai::OPEN_AI_API_URL.to_string(),
api_key,
);
cx.spawn(|mut cx| async move {
let mut semantic_index = SemanticIndex::new(
PathBuf::from("/tmp/semantic-index-db.mdb"),
Arc::new(embedding_provider),
&mut cx,
)
.await?;
let project_path = Path::new(&args[1]);
let project = Project::example([project_path], &mut cx).await;
cx.update(|cx| {
let fs = project.read(cx).fs().clone();
let project_index = semantic_index.project_index(project.clone(), cx);
let mut tool_registry = ToolRegistry::new();
tool_registry
.register(ProjectIndexTool::new(project_index.clone(), fs.clone()))
.context("failed to register ProjectIndexTool")
.log_err();
let tool_registry = Arc::new(tool_registry);
cx.open_window(WindowOptions::default(), |cx| {
cx.new_view(|cx| Example::new(language_registry, tool_registry, cx))
});
cx.activate(true);
})
})
.detach_and_log_err(cx);
})
}
struct Example {
assistant_panel: View<AssistantPanel>,
}
impl Example {
fn new(
language_registry: Arc<LanguageRegistry>,
tool_registry: Arc<ToolRegistry>,
cx: &mut ViewContext<Self>,
) -> Self {
Self {
assistant_panel: cx
.new_view(|cx| AssistantPanel::new(language_registry, tool_registry, cx)),
}
}
}
impl Render for Example {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl ui::prelude::IntoElement {
div().size_full().child(self.assistant_panel.clone())
}
}

View File

@@ -1,218 +0,0 @@
use anyhow::Context as _;
use assets::Assets;
use assistant2::AssistantPanel;
use assistant_tooling::{LanguageModelTool, ToolRegistry};
use client::Client;
use gpui::{actions, AnyElement, App, AppContext, KeyBinding, Task, View, WindowOptions};
use language::LanguageRegistry;
use project::Project;
use rand::Rng;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{KeymapFile, DEFAULT_KEYMAP_PATH};
use std::sync::Arc;
use theme::LoadThemes;
use ui::{div, prelude::*, Render};
use util::ResultExt as _;
actions!(example, [Quit]);
struct RollDiceTool {}
impl RollDiceTool {
fn new() -> Self {
Self {}
}
}
#[derive(Serialize, Deserialize, JsonSchema, Clone)]
#[serde(rename_all = "snake_case")]
enum Die {
D6 = 6,
D20 = 20,
}
impl Die {
fn into_str(&self) -> &'static str {
match self {
Die::D6 => "d6",
Die::D20 => "d20",
}
}
}
#[derive(Serialize, Deserialize, JsonSchema, Clone)]
struct DiceParams {
/// The number of dice to roll.
num_dice: u8,
/// Which die to roll. Defaults to a d6 if not provided.
die_type: Option<Die>,
}
#[derive(Serialize, Deserialize)]
struct DieRoll {
die: Die,
roll: u8,
}
impl DieRoll {
fn render(&self) -> AnyElement {
match self.die {
Die::D6 => {
let face = match self.roll {
6 => div().child(""),
5 => div().child(""),
4 => div().child(""),
3 => div().child(""),
2 => div().child(""),
1 => div().child(""),
_ => div().child("😅"),
};
face.text_3xl().into_any_element()
}
_ => div()
.child(format!("{}", self.roll))
.text_3xl()
.into_any_element(),
}
}
}
#[derive(Serialize, Deserialize)]
struct DiceRoll {
rolls: Vec<DieRoll>,
}
impl LanguageModelTool for RollDiceTool {
type Input = DiceParams;
type Output = DiceRoll;
fn name(&self) -> String {
"roll_dice".to_string()
}
fn description(&self) -> String {
"Rolls N many dice and returns the results.".to_string()
}
fn execute(&self, input: &Self::Input, _cx: &AppContext) -> Task<gpui::Result<Self::Output>> {
let rolls = (0..input.num_dice)
.map(|_| {
let die_type = input.die_type.as_ref().unwrap_or(&Die::D6).clone();
DieRoll {
die: die_type.clone(),
roll: rand::thread_rng().gen_range(1..=die_type as u8),
}
})
.collect();
return Task::ready(Ok(DiceRoll { rolls }));
}
fn render(
_tool_call_id: &str,
_input: &Self::Input,
output: &Self::Output,
_cx: &mut WindowContext,
) -> gpui::AnyElement {
h_flex()
.children(
output
.rolls
.iter()
.map(|roll| div().p_2().child(roll.render())),
)
.into_any_element()
}
fn format(_input: &Self::Input, output: &Self::Output) -> String {
let mut result = String::new();
for roll in &output.rolls {
let die = &roll.die;
result.push_str(&format!("{}: {}\n", die.into_str(), roll.roll));
}
result
}
}
fn main() {
env_logger::init();
App::new().with_assets(Assets).run(|cx| {
cx.bind_keys(Some(KeyBinding::new("cmd-q", Quit, None)));
cx.on_action(|_: &Quit, cx: &mut AppContext| {
cx.quit();
});
settings::init(cx);
language::init(cx);
Project::init_settings(cx);
editor::init(cx);
theme::init(LoadThemes::JustBase, cx);
Assets.load_fonts(cx).unwrap();
KeymapFile::load_asset(DEFAULT_KEYMAP_PATH, cx).unwrap();
client::init_settings(cx);
release_channel::init("0.130.0", cx);
let client = Client::production(cx);
{
let client = client.clone();
cx.spawn(|cx| async move { client.authenticate_and_connect(false, &cx).await })
.detach_and_log_err(cx);
}
assistant2::init(client.clone(), cx);
let language_registry = Arc::new(LanguageRegistry::new(
Task::ready(()),
cx.background_executor().clone(),
));
let node_runtime = node_runtime::RealNodeRuntime::new(client.http_client());
languages::init(language_registry.clone(), node_runtime, cx);
cx.spawn(|cx| async move {
cx.update(|cx| {
let mut tool_registry = ToolRegistry::new();
tool_registry
.register(RollDiceTool::new())
.context("failed to register DummyTool")
.log_err();
let tool_registry = Arc::new(tool_registry);
println!("Tools registered");
for definition in tool_registry.definitions() {
println!("{}", definition);
}
cx.open_window(WindowOptions::default(), |cx| {
cx.new_view(|cx| Example::new(language_registry, tool_registry, cx))
});
cx.activate(true);
})
})
.detach_and_log_err(cx);
})
}
struct Example {
assistant_panel: View<AssistantPanel>,
}
impl Example {
fn new(
language_registry: Arc<LanguageRegistry>,
tool_registry: Arc<ToolRegistry>,
cx: &mut ViewContext<Self>,
) -> Self {
Self {
assistant_panel: cx
.new_view(|cx| AssistantPanel::new(language_registry, tool_registry, cx)),
}
}
}
impl Render for Example {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl ui::prelude::IntoElement {
div().size_full().child(self.assistant_panel.clone())
}
}

View File

@@ -0,0 +1,378 @@
//! This example creates a basic Chat UI with a function for rolling a die.
use anyhow::{Context as _, Result};
use assets::Assets;
use assistant2::AssistantPanel;
use assistant_tooling::{LanguageModelTool, ToolRegistry};
use client::{Client, UserStore};
use fs::Fs;
use futures::StreamExt as _;
use gpui::{actions, AnyElement, App, AppContext, KeyBinding, Model, Task, View, WindowOptions};
use language::LanguageRegistry;
use project::Project;
use rand::Rng;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{KeymapFile, DEFAULT_KEYMAP_PATH};
use std::{path::PathBuf, sync::Arc};
use theme::LoadThemes;
use ui::{div, prelude::*, Render};
use util::ResultExt as _;
actions!(example, [Quit]);
struct RollDiceTool {}
impl RollDiceTool {
fn new() -> Self {
Self {}
}
}
#[derive(Serialize, Deserialize, JsonSchema, Clone)]
#[serde(rename_all = "snake_case")]
enum Die {
D6 = 6,
D20 = 20,
}
impl Die {
fn into_str(&self) -> &'static str {
match self {
Die::D6 => "d6",
Die::D20 => "d20",
}
}
}
#[derive(Serialize, Deserialize, JsonSchema, Clone)]
struct DiceParams {
/// The number of dice to roll.
num_dice: u8,
/// Which die to roll. Defaults to a d6 if not provided.
die_type: Option<Die>,
}
#[derive(Serialize, Deserialize)]
struct DieRoll {
die: Die,
roll: u8,
}
impl DieRoll {
fn render(&self) -> AnyElement {
match self.die {
Die::D6 => {
let face = match self.roll {
6 => div().child(""),
5 => div().child(""),
4 => div().child(""),
3 => div().child(""),
2 => div().child(""),
1 => div().child(""),
_ => div().child("😅"),
};
face.text_3xl().into_any_element()
}
_ => div()
.child(format!("{}", self.roll))
.text_3xl()
.into_any_element(),
}
}
}
#[derive(Serialize, Deserialize)]
struct DiceRoll {
rolls: Vec<DieRoll>,
}
pub struct DiceView {
result: Result<DiceRoll>,
}
impl Render for DiceView {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
let output = match &self.result {
Ok(output) => output,
Err(_) => return "Somehow dice failed 🎲".into_any_element(),
};
h_flex()
.children(
output
.rolls
.iter()
.map(|roll| div().p_2().child(roll.render())),
)
.into_any_element()
}
}
impl LanguageModelTool for RollDiceTool {
type Input = DiceParams;
type Output = DiceRoll;
type View = DiceView;
fn name(&self) -> String {
"roll_dice".to_string()
}
fn description(&self) -> String {
"Rolls N many dice and returns the results.".to_string()
}
fn execute(
&self,
input: &Self::Input,
_cx: &mut WindowContext,
) -> Task<gpui::Result<Self::Output>> {
let rolls = (0..input.num_dice)
.map(|_| {
let die_type = input.die_type.as_ref().unwrap_or(&Die::D6).clone();
DieRoll {
die: die_type.clone(),
roll: rand::thread_rng().gen_range(1..=die_type as u8),
}
})
.collect();
return Task::ready(Ok(DiceRoll { rolls }));
}
fn output_view(
_tool_call_id: String,
_input: Self::Input,
result: Result<Self::Output>,
cx: &mut WindowContext,
) -> gpui::View<Self::View> {
cx.new_view(|_cx| DiceView { result })
}
fn format(_: &Self::Input, output: &Result<Self::Output>) -> String {
let output = match output {
Ok(output) => output,
Err(_) => return "Somehow dice failed 🎲".to_string(),
};
let mut result = String::new();
for roll in &output.rolls {
let die = &roll.die;
result.push_str(&format!("{}: {}\n", die.into_str(), roll.roll));
}
result
}
}
struct FileBrowserTool {
fs: Arc<dyn Fs>,
root_dir: PathBuf,
}
impl FileBrowserTool {
fn new(fs: Arc<dyn Fs>, root_dir: PathBuf) -> Self {
Self { fs, root_dir }
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
struct FileBrowserParams {
command: FileBrowserCommand,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
enum FileBrowserCommand {
Ls { path: PathBuf },
Cat { path: PathBuf },
}
#[derive(Serialize, Deserialize)]
enum FileBrowserOutput {
Ls { entries: Vec<String> },
Cat { content: String },
}
pub struct FileBrowserView {
result: Result<FileBrowserOutput>,
}
impl Render for FileBrowserView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let Ok(output) = self.result.as_ref() else {
return h_flex().child("Failed to perform operation");
};
match output {
FileBrowserOutput::Ls { entries } => v_flex().children(
entries
.into_iter()
.map(|entry| h_flex().text_ui(cx).child(entry.clone())),
),
FileBrowserOutput::Cat { content } => h_flex().child(content.clone()),
}
}
}
impl LanguageModelTool for FileBrowserTool {
type Input = FileBrowserParams;
type Output = FileBrowserOutput;
type View = FileBrowserView;
fn name(&self) -> String {
"file_browser".to_string()
}
fn description(&self) -> String {
"A tool for browsing the filesystem.".to_string()
}
fn execute(
&self,
input: &Self::Input,
cx: &mut WindowContext,
) -> Task<gpui::Result<Self::Output>> {
cx.spawn({
let fs = self.fs.clone();
let root_dir = self.root_dir.clone();
let input = input.clone();
|_cx| async move {
match input.command {
FileBrowserCommand::Ls { path } => {
let path = root_dir.join(path);
let mut output = fs.read_dir(&path).await?;
let mut entries = Vec::new();
while let Some(entry) = output.next().await {
let entry = entry?;
entries.push(entry.display().to_string());
}
Ok(FileBrowserOutput::Ls { entries })
}
FileBrowserCommand::Cat { path } => {
let path = root_dir.join(path);
let output = fs.load(&path).await?;
Ok(FileBrowserOutput::Cat { content: output })
}
}
}
})
}
fn output_view(
_tool_call_id: String,
_input: Self::Input,
result: Result<Self::Output>,
cx: &mut WindowContext,
) -> gpui::View<Self::View> {
cx.new_view(|_cx| FileBrowserView { result })
}
fn format(_input: &Self::Input, output: &Result<Self::Output>) -> String {
let Ok(output) = output else {
return "Failed to perform command: {input:?}".to_string();
};
match output {
FileBrowserOutput::Ls { entries } => entries.join("\n"),
FileBrowserOutput::Cat { content } => content.to_owned(),
}
}
}
fn main() {
env_logger::init();
App::new().with_assets(Assets).run(|cx| {
cx.bind_keys(Some(KeyBinding::new("cmd-q", Quit, None)));
cx.on_action(|_: &Quit, cx: &mut AppContext| {
cx.quit();
});
settings::init(cx);
language::init(cx);
Project::init_settings(cx);
editor::init(cx);
theme::init(LoadThemes::JustBase, cx);
Assets.load_fonts(cx).unwrap();
KeymapFile::load_asset(DEFAULT_KEYMAP_PATH, cx).unwrap();
client::init_settings(cx);
release_channel::init("0.130.0", cx);
let client = Client::production(cx);
{
let client = client.clone();
cx.spawn(|cx| async move { client.authenticate_and_connect(false, &cx).await })
.detach_and_log_err(cx);
}
assistant2::init(client.clone(), cx);
let language_registry = Arc::new(LanguageRegistry::new(
Task::ready(()),
cx.background_executor().clone(),
));
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
let node_runtime = node_runtime::RealNodeRuntime::new(client.http_client());
languages::init(language_registry.clone(), node_runtime, cx);
cx.spawn(|cx| async move {
cx.update(|cx| {
let fs = Arc::new(fs::RealFs::new(None));
let cwd = std::env::current_dir().expect("Failed to get current working directory");
cx.open_window(WindowOptions::default(), |cx| {
let mut tool_registry = ToolRegistry::new();
tool_registry
.register(RollDiceTool::new(), cx)
.context("failed to register DummyTool")
.log_err();
tool_registry
.register(FileBrowserTool::new(fs, cwd), cx)
.context("failed to register FileBrowserTool")
.log_err();
let tool_registry = Arc::new(tool_registry);
println!("Tools registered");
for definition in tool_registry.definitions() {
println!("{}", definition);
}
cx.new_view(|cx| Example::new(language_registry, tool_registry, user_store, cx))
});
cx.activate(true);
})
})
.detach_and_log_err(cx);
})
}
struct Example {
assistant_panel: View<AssistantPanel>,
}
impl Example {
fn new(
language_registry: Arc<LanguageRegistry>,
tool_registry: Arc<ToolRegistry>,
user_store: Model<UserStore>,
cx: &mut ViewContext<Self>,
) -> Self {
Self {
assistant_panel: cx.new_view(|cx| {
AssistantPanel::new(language_registry, tool_registry, user_store, None, cx)
}),
}
}
}
impl Render for Example {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl ui::prelude::IntoElement {
div().size_full().child(self.assistant_panel.clone())
}
}

View File

@@ -1,29 +1,29 @@
mod assistant_settings;
mod completion_provider;
pub mod tools;
mod tools;
pub mod ui;
use ::ui::{div, prelude::*, Color, ViewContext};
use anyhow::{Context, Result};
use assistant_tooling::{ToolFunctionCall, ToolRegistry};
use client::{proto, Client};
use client::{proto, Client, UserStore};
use collections::HashMap;
use completion_provider::*;
use editor::Editor;
use feature_flags::FeatureFlagAppExt as _;
use futures::{channel::oneshot, future::join_all, Future, FutureExt, StreamExt};
use futures::{future::join_all, StreamExt};
use gpui::{
list, prelude::*, AnyElement, AppContext, AsyncWindowContext, EventEmitter, FocusHandle,
FocusableView, Global, ListAlignment, ListState, Model, Render, Task, View, WeakView,
list, AnyElement, AppContext, AsyncWindowContext, ClickEvent, EventEmitter, FocusHandle,
FocusableView, ListAlignment, ListState, Model, Render, Task, View, WeakView,
};
use language::{language_settings::SoftWrap, LanguageRegistry};
use open_ai::{FunctionContent, ToolCall, ToolCallContent};
use project::Fs;
use rich_text::RichText;
use semantic_index::{CloudEmbeddingProvider, ProjectIndex, SemanticIndex};
use serde::Deserialize;
use settings::Settings;
use std::{cmp, sync::Arc};
use theme::ThemeSettings;
use tools::ProjectIndexTool;
use ui::{popover_menu, prelude::*, ButtonLike, CollapsibleContainer, Color, ContextMenu, Tooltip};
use std::sync::Arc;
use ui::Composer;
use util::{paths::EMBEDDINGS_DIR, ResultExt};
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
@@ -32,9 +32,10 @@ use workspace::{
pub use assistant_settings::AssistantSettings;
const MAX_COMPLETION_CALLS_PER_SUBMISSION: usize = 5;
use crate::tools::{CreateBufferTool, ProjectIndexTool};
use crate::ui::UserOrAssistant;
// gpui::actions!(assistant, [Submit]);
const MAX_COMPLETION_CALLS_PER_SUBMISSION: usize = 5;
#[derive(Eq, PartialEq, Copy, Clone, Deserialize)]
pub struct Submit(SubmitMode);
@@ -50,7 +51,7 @@ pub enum SubmitMode {
Codebase,
}
gpui::actions!(assistant2, [ToggleFocus]);
gpui::actions!(assistant2, [Cancel, ToggleFocus, DebugProjectIndex]);
gpui::impl_actions!(assistant2, [Submit]);
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
@@ -104,6 +105,8 @@ impl AssistantPanel {
(workspace.app_state().clone(), workspace.project().clone())
})?;
let user_store = app_state.user_store.clone();
cx.new_view(|cx| {
// todo!("this will panic if the semantic index failed to load or has not loaded yet")
let project_index = cx.update_global(|semantic_index: &mut SemanticIndex, cx| {
@@ -112,16 +115,29 @@ impl AssistantPanel {
let mut tool_registry = ToolRegistry::new();
tool_registry
.register(ProjectIndexTool::new(
project_index.clone(),
app_state.fs.clone(),
))
.register(
ProjectIndexTool::new(project_index.clone(), app_state.fs.clone()),
cx,
)
.context("failed to register ProjectIndexTool")
.log_err();
tool_registry
.register(
CreateBufferTool::new(workspace.clone(), project.clone()),
cx,
)
.context("failed to register CreateBufferTool")
.log_err();
let tool_registry = Arc::new(tool_registry);
Self::new(app_state.languages.clone(), tool_registry, cx)
Self::new(
app_state.languages.clone(),
tool_registry,
user_store,
Some(project_index),
cx,
)
})
})
}
@@ -129,10 +145,18 @@ impl AssistantPanel {
pub fn new(
language_registry: Arc<LanguageRegistry>,
tool_registry: Arc<ToolRegistry>,
user_store: Model<UserStore>,
project_index: Option<Model<ProjectIndex>>,
cx: &mut ViewContext<Self>,
) -> Self {
let chat = cx.new_view(|cx| {
AssistantChat::new(language_registry.clone(), tool_registry.clone(), cx)
AssistantChat::new(
language_registry.clone(),
tool_registry.clone(),
user_store,
project_index,
cx,
)
});
Self { width: None, chat }
@@ -177,7 +201,7 @@ impl Panel for AssistantPanel {
cx.notify();
}
fn icon(&self, _cx: &WindowContext) -> Option<ui::IconName> {
fn icon(&self, _cx: &WindowContext) -> Option<::ui::IconName> {
Some(IconName::Ai)
}
@@ -194,30 +218,31 @@ impl EventEmitter<PanelEvent> for AssistantPanel {}
impl FocusableView for AssistantPanel {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.chat
.read(cx)
.messages
.iter()
.rev()
.find_map(|msg| msg.focus_handle(cx))
.expect("no user message in chat")
self.chat.read(cx).composer_editor.read(cx).focus_handle(cx)
}
}
struct AssistantChat {
pub struct AssistantChat {
model: String,
messages: Vec<ChatMessage>,
list_state: ListState,
language_registry: Arc<LanguageRegistry>,
composer_editor: View<Editor>,
user_store: Model<UserStore>,
next_message_id: MessageId,
collapsed_messages: HashMap<MessageId, bool>,
editing_message_id: Option<MessageId>,
pending_completion: Option<Task<()>>,
tool_registry: Arc<ToolRegistry>,
project_index: Option<Model<ProjectIndex>>,
}
impl AssistantChat {
fn new(
language_registry: Arc<LanguageRegistry>,
tool_registry: Arc<ToolRegistry>,
user_store: Model<UserStore>,
project_index: Option<Model<ProjectIndex>>,
cx: &mut ViewContext<Self>,
) -> Self {
let model = CompletionProvider::get(cx).default_model();
@@ -232,17 +257,25 @@ impl AssistantChat {
},
);
let mut this = Self {
Self {
model,
messages: Vec::new(),
composer_editor: cx.new_view(|cx| {
let mut editor = Editor::auto_height(80, cx);
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
editor.set_placeholder_text("Type a message to the assistant", cx);
editor
}),
list_state,
user_store,
language_registry,
project_index,
next_message_id: MessageId(0),
editing_message_id: None,
collapsed_messages: HashMap::default(),
pending_completion: None,
tool_registry,
};
this.push_new_user_message(true, cx);
this
}
}
fn focused_message_id(&self, cx: &WindowContext) -> Option<MessageId> {
@@ -256,13 +289,49 @@ impl AssistantChat {
})
}
fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
// If we're currently editing a message, cancel the edit.
self.editing_message_id.take();
if self.pending_completion.take().is_none() {
cx.propagate();
return;
}
if let Some(ChatMessage::Assistant(message)) = self.messages.last() {
if message.body.text.is_empty() {
self.pop_message(cx);
}
}
}
fn submit(&mut self, Submit(mode): &Submit, cx: &mut ViewContext<Self>) {
let Some(focused_message_id) = self.focused_message_id(cx) else {
// Don't allow multiple concurrent completions.
if self.pending_completion.is_some() {
cx.propagate();
return;
}
if let Some(focused_message_id) = self.focused_message_id(cx) {
self.truncate_messages(focused_message_id, cx);
} else if self.composer_editor.focus_handle(cx).is_focused(cx) {
let message = self.composer_editor.update(cx, |composer_editor, cx| {
let text = composer_editor.text(cx);
let id = self.next_message_id.post_inc();
let body = cx.new_view(|cx| {
let mut editor = Editor::auto_height(80, cx);
editor.set_text(text, cx);
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
editor
});
composer_editor.clear(cx);
ChatMessage::User(UserMessage { id, body })
});
self.push_message(message, cx);
} else {
log::error!("unexpected state: no user message editor is focused.");
return;
};
self.truncate_messages(focused_message_id, cx);
}
let mode = *mode;
self.pending_completion = Some(cx.spawn(move |this, mut cx| async move {
@@ -276,18 +345,27 @@ impl AssistantChat {
.log_err();
this.update(&mut cx, |this, cx| {
let focus = this
.user_message(focused_message_id)
.body
.focus_handle(cx)
.contains_focused(cx);
this.push_new_user_message(focus, cx);
let composer_focus_handle = this.composer_editor.focus_handle(cx);
cx.focus(&composer_focus_handle);
this.pending_completion = None;
})
.context("Failed to push new user message")
.log_err();
}));
}
fn can_submit(&self) -> bool {
self.pending_completion.is_none()
}
fn debug_project_index(&mut self, _: &DebugProjectIndex, cx: &mut ViewContext<Self>) {
if let Some(index) = &self.project_index {
index.update(cx, |project_index, cx| {
project_index.debug(cx).detach_and_log_err(cx)
});
}
}
async fn request_completion(
this: WeakView<Self>,
mode: SubmitMode,
@@ -300,7 +378,8 @@ impl AssistantChat {
let completion = this.update(cx, |this, cx| {
this.push_new_assistant_message(cx);
let definitions = if call_count < limit && matches!(mode, SubmitMode::Codebase)
let definitions = if call_count < limit
&& matches!(mode, SubmitMode::Codebase | SubmitMode::Simple)
{
this.tool_registry.definitions()
} else {
@@ -308,9 +387,11 @@ impl AssistantChat {
};
call_count += 1;
let messages = this.completion_messages(cx);
CompletionProvider::get(cx).complete(
this.model.clone(),
this.completion_messages(cx),
messages,
Vec::new(),
1.0,
definitions,
@@ -393,6 +474,10 @@ impl AssistantChat {
}
let tools = join_all(tool_tasks.into_iter()).await;
// If the WindowContext went away for any tool's view we don't include it
// especially since the below call would fail for the same reason.
let tools = tools.into_iter().filter_map(|tool| tool.ok()).collect();
this.update(cx, |this, cx| {
if let Some(ChatMessage::Assistant(AssistantMessage { tool_calls, .. })) =
this.messages.last_mut()
@@ -404,36 +489,6 @@ impl AssistantChat {
}
}
fn user_message(&mut self, message_id: MessageId) -> &mut UserMessage {
self.messages
.iter_mut()
.find_map(|message| match message {
ChatMessage::User(user_message) if user_message.id == message_id => {
Some(user_message)
}
_ => None,
})
.expect("User message not found")
}
fn push_new_user_message(&mut self, focus: bool, cx: &mut ViewContext<Self>) {
let id = self.next_message_id.post_inc();
let body = cx.new_view(|cx| {
let mut editor = Editor::auto_height(80, cx);
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
if focus {
cx.focus_self();
}
editor
});
let message = ChatMessage::User(UserMessage {
id,
body,
contexts: Vec::new(),
});
self.push_message(message, cx);
}
fn push_new_assistant_message(&mut self, cx: &mut ViewContext<Self>) {
let message = ChatMessage::Assistant(AssistantMessage {
id: self.next_message_id.post_inc(),
@@ -453,6 +508,17 @@ impl AssistantChat {
cx.notify();
}
fn pop_message(&mut self, cx: &mut ViewContext<Self>) {
if self.messages.is_empty() {
return;
}
self.messages.pop();
self.list_state
.splice(self.messages.len()..self.messages.len() + 1, 0);
cx.notify();
}
fn truncate_messages(&mut self, last_message_id: MessageId, cx: &mut ViewContext<Self>) {
if let Some(index) = self.messages.iter().position(|message| match message {
ChatMessage::User(message) => message.id == last_message_id,
@@ -464,6 +530,15 @@ impl AssistantChat {
}
}
fn is_message_collapsed(&self, id: &MessageId) -> bool {
self.collapsed_messages.get(id).copied().unwrap_or_default()
}
fn toggle_message_collapsed(&mut self, id: MessageId) {
let entry = self.collapsed_messages.entry(id).or_insert(false);
*entry = !*entry;
}
fn render_error(
&self,
error: Option<SharedString>,
@@ -493,22 +568,53 @@ impl AssistantChat {
let is_last = ix == self.messages.len() - 1;
match &self.messages[ix] {
ChatMessage::User(UserMessage {
body,
contexts: _contexts,
..
}) => div()
ChatMessage::User(UserMessage { id, body }) => div()
.id(SharedString::from(format!("message-{}-container", id.0)))
.when(!is_last, |element| element.mb_2())
.child(div().p_2().child(Label::new("You").color(Color::Default)))
.child(
div()
.on_action(cx.listener(Self::submit))
.p_2()
.text_color(cx.theme().colors().editor_foreground)
.font(ThemeSettings::get_global(cx).buffer_font.clone())
.bg(cx.theme().colors().editor_background)
.child(body.clone()), // .children(contexts.iter().map(|context| context.render(cx))),
)
.map(|element| {
if self.editing_message_id.as_ref() == Some(id) {
element.child(Composer::new(
body.clone(),
self.user_store.read(cx).current_user(),
self.can_submit(),
self.tool_registry.clone(),
crate::ui::ModelSelector::new(
cx.view().downgrade(),
self.model.clone(),
)
.into_any_element(),
))
} else {
element
.on_click(cx.listener({
let id = *id;
move |assistant_chat, event: &ClickEvent, _cx| {
if event.up.click_count == 2 {
assistant_chat.editing_message_id = Some(id);
}
}
}))
.child(crate::ui::ChatMessage::new(
*id,
UserOrAssistant::User(self.user_store.read(cx).current_user()),
Some(
RichText::new(
body.read(cx).text(cx),
&[],
&self.language_registry,
)
.element(ElementId::from(id.0), cx),
),
self.is_message_collapsed(id),
Box::new(cx.listener({
let id = *id;
move |assistant_chat, _event, _cx| {
assistant_chat.toggle_message_collapsed(id)
}
})),
))
}
})
.into_any(),
ChatMessage::Assistant(AssistantMessage {
id,
@@ -517,29 +623,40 @@ impl AssistantChat {
tool_calls,
..
}) => {
let assistant_body = if body.text.is_empty() && !tool_calls.is_empty() {
div()
let assistant_body = if body.text.is_empty() {
None
} else {
div().p_2().child(body.element(ElementId::from(id.0), cx))
Some(
div()
.p_2()
.child(body.element(ElementId::from(id.0), cx))
.into_any_element(),
)
};
div()
.when(!is_last, |element| element.mb_2())
.child(
div()
.p_2()
.child(Label::new("Assistant").color(Color::Modified)),
)
.child(assistant_body)
.child(crate::ui::ChatMessage::new(
*id,
UserOrAssistant::Assistant,
assistant_body,
self.is_message_collapsed(id),
Box::new(cx.listener({
let id = *id;
move |assistant_chat, _event, _cx| {
assistant_chat.toggle_message_collapsed(id)
}
})),
))
// TODO: Should the errors and tool calls get passed into `ChatMessage`?
.child(self.render_error(error.clone(), ix, cx))
.children(tool_calls.iter().map(|tool_call| {
let result = &tool_call.result;
let name = tool_call.name.clone();
match result {
Some(result) => div()
.p_2()
.child(result.render(&name, &tool_call.id, cx))
.into_any(),
Some(result) => {
div().p_2().child(result.into_any_element(&name)).into_any()
}
None => div()
.p_2()
.child(Label::new(name).color(Color::Modified))
@@ -552,16 +669,16 @@ impl AssistantChat {
}
}
fn completion_messages(&self, cx: &WindowContext) -> Vec<CompletionMessage> {
fn completion_messages(&self, cx: &mut WindowContext) -> Vec<CompletionMessage> {
let mut completion_messages = Vec::new();
for message in &self.messages {
match message {
ChatMessage::User(UserMessage { body, contexts, .. }) => {
// setup context for model
contexts.iter().for_each(|context| {
completion_messages.extend(context.completion_messages(cx))
});
ChatMessage::User(UserMessage { body, .. }) => {
// When we re-introduce contexts like active file, we'll inject them here instead of relying on the model to request them
// contexts.iter().for_each(|context| {
// completion_messages.extend(context.completion_messages(cx))
// });
// Show user's message last so that the assistant is grounded in the user's request
completion_messages.push(CompletionMessage::User {
@@ -615,59 +732,6 @@ impl AssistantChat {
completion_messages
}
fn render_model_dropdown(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let this = cx.view().downgrade();
div().h_flex().justify_end().child(
div().w_32().child(
popover_menu("user-menu")
.menu(move |cx| {
ContextMenu::build(cx, |mut menu, cx| {
for model in CompletionProvider::get(cx).available_models() {
menu = menu.custom_entry(
{
let model = model.clone();
move |_| Label::new(model.clone()).into_any_element()
},
{
let this = this.clone();
move |cx| {
_ = this.update(cx, |this, cx| {
this.model = model.clone();
cx.notify();
});
}
},
);
}
menu
})
.into()
})
.trigger(
ButtonLike::new("active-model")
.child(
h_flex()
.w_full()
.gap_0p5()
.child(
div()
.overflow_x_hidden()
.flex_grow()
.whitespace_nowrap()
.child(Label::new(self.model.clone())),
)
.child(div().child(
Icon::new(IconName::ChevronDown).color(Color::Muted),
)),
)
.style(ButtonStyle::Subtle)
.tooltip(move |cx| Tooltip::text("Change Model", cx)),
)
.anchor(gpui::AnchorCorner::TopRight),
),
)
}
}
impl Render for AssistantChat {
@@ -677,14 +741,24 @@ impl Render for AssistantChat {
.flex_1()
.v_flex()
.key_context("AssistantChat")
.on_action(cx.listener(Self::submit))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::debug_project_index))
.text_color(Color::Default.color(cx))
.child(self.render_model_dropdown(cx))
.child(list(self.list_state.clone()).flex_1())
.child(Composer::new(
self.composer_editor.clone(),
self.user_store.read(cx).current_user(),
self.can_submit(),
self.tool_registry.clone(),
crate::ui::ModelSelector::new(cx.view().downgrade(), self.model.clone())
.into_any_element(),
))
}
}
#[derive(Copy, Clone, Eq, PartialEq)]
struct MessageId(usize);
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub struct MessageId(usize);
impl MessageId {
fn post_inc(&mut self) -> Self {
@@ -711,7 +785,6 @@ impl ChatMessage {
struct UserMessage {
id: MessageId,
body: View<Editor>,
contexts: Vec<AssistantContext>,
}
struct AssistantMessage {
@@ -720,211 +793,3 @@ struct AssistantMessage {
tool_calls: Vec<ToolFunctionCall>,
error: Option<SharedString>,
}
// Since we're swapping out for direct query usage, we might not need to use this injected context
// It will be useful though for when the user _definitely_ wants the model to see a specific file,
// query, error, etc.
#[allow(dead_code)]
enum AssistantContext {
Codebase(View<CodebaseContext>),
}
#[allow(dead_code)]
struct CodebaseExcerpt {
element_id: ElementId,
path: SharedString,
text: SharedString,
score: f32,
expanded: bool,
}
impl AssistantContext {
#[allow(dead_code)]
fn render(&self, _cx: &mut ViewContext<AssistantChat>) -> AnyElement {
match self {
AssistantContext::Codebase(context) => context.clone().into_any_element(),
}
}
fn completion_messages(&self, cx: &WindowContext) -> Vec<CompletionMessage> {
match self {
AssistantContext::Codebase(context) => context.read(cx).completion_messages(),
}
}
}
enum CodebaseContext {
Pending { _task: Task<()> },
Done(Result<Vec<CodebaseExcerpt>>),
}
impl CodebaseContext {
fn toggle_expanded(&mut self, element_id: ElementId, cx: &mut ViewContext<Self>) {
if let CodebaseContext::Done(Ok(excerpts)) = self {
if let Some(excerpt) = excerpts
.iter_mut()
.find(|excerpt| excerpt.element_id == element_id)
{
excerpt.expanded = !excerpt.expanded;
cx.notify();
}
}
}
}
impl Render for CodebaseContext {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
match self {
CodebaseContext::Pending { .. } => div()
.h_flex()
.items_center()
.gap_1()
.child(Icon::new(IconName::Ai).color(Color::Muted).into_element())
.child("Searching codebase..."),
CodebaseContext::Done(Ok(excerpts)) => {
div()
.v_flex()
.gap_2()
.children(excerpts.iter().map(|excerpt| {
let expanded = excerpt.expanded;
let element_id = excerpt.element_id.clone();
CollapsibleContainer::new(element_id.clone(), expanded)
.start_slot(
h_flex()
.gap_1()
.child(Icon::new(IconName::File).color(Color::Muted))
.child(Label::new(excerpt.path.clone()).color(Color::Muted)),
)
.on_click(cx.listener(move |this, _, cx| {
this.toggle_expanded(element_id.clone(), cx);
}))
.child(
div()
.p_2()
.rounded_md()
.bg(cx.theme().colors().editor_background)
.child(
excerpt.text.clone(), // todo!(): Show as an editor block
),
)
}))
}
CodebaseContext::Done(Err(error)) => div().child(error.to_string()),
}
}
}
impl CodebaseContext {
#[allow(dead_code)]
fn new(
query: impl 'static + Future<Output = Result<String>>,
populated: oneshot::Sender<bool>,
project_index: Model<ProjectIndex>,
fs: Arc<dyn Fs>,
cx: &mut ViewContext<Self>,
) -> Self {
let query = query.boxed_local();
let _task = cx.spawn(|this, mut cx| async move {
let result = async {
let query = query.await?;
let results = this
.update(&mut cx, |_this, cx| {
project_index.read(cx).search(&query, 16, cx)
})?
.await;
let excerpts = results.into_iter().map(|result| {
let abs_path = result
.worktree
.read_with(&cx, |worktree, _| worktree.abs_path().join(&result.path));
let fs = fs.clone();
async move {
let path = result.path.clone();
let text = fs.load(&abs_path?).await?;
// todo!("what should we do with stale ranges?");
let range = cmp::min(result.range.start, text.len())
..cmp::min(result.range.end, text.len());
let text = SharedString::from(text[range].to_string());
anyhow::Ok(CodebaseExcerpt {
element_id: ElementId::Name(nanoid::nanoid!().into()),
path: path.to_string_lossy().to_string().into(),
text,
score: result.score,
expanded: false,
})
}
});
anyhow::Ok(
futures::future::join_all(excerpts)
.await
.into_iter()
.filter_map(|result| result.log_err())
.collect(),
)
}
.await;
this.update(&mut cx, |this, cx| {
this.populate(result, populated, cx);
})
.ok();
});
Self::Pending { _task }
}
#[allow(dead_code)]
fn populate(
&mut self,
result: Result<Vec<CodebaseExcerpt>>,
populated: oneshot::Sender<bool>,
cx: &mut ViewContext<Self>,
) {
let success = result.is_ok();
*self = Self::Done(result);
populated.send(success).ok();
cx.notify();
}
fn completion_messages(&self) -> Vec<CompletionMessage> {
// One system message for the whole batch of excerpts:
// Semantic search results for user query:
//
// Excerpt from $path:
// ~~~
// `text`
// ~~~
//
// Excerpt from $path:
match self {
CodebaseContext::Done(Ok(excerpts)) => {
if excerpts.is_empty() {
return Vec::new();
}
let mut body = "Semantic search results for user query:\n".to_string();
for excerpt in excerpts {
body.push_str("Excerpt from ");
body.push_str(excerpt.path.as_ref());
body.push_str(", score ");
body.push_str(&excerpt.score.to_string());
body.push_str(":\n");
body.push_str("~~~\n");
body.push_str(excerpt.text.as_ref());
body.push_str("~~~\n");
}
vec![CompletionMessage::System { content: body }]
}
_ => vec![],
}
}
}

View File

@@ -2,7 +2,7 @@ use anyhow::Result;
use assistant_tooling::ToolFunctionDefinition;
use client::{proto, Client};
use futures::{future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
use gpui::Global;
use gpui::{AppContext, Global};
use std::sync::Arc;
pub use open_ai::RequestMessage as CompletionMessage;
@@ -11,6 +11,10 @@ pub use open_ai::RequestMessage as CompletionMessage;
pub struct CompletionProvider(Arc<dyn CompletionProviderBackend>);
impl CompletionProvider {
pub fn get(cx: &AppContext) -> &Self {
cx.global::<CompletionProvider>()
}
pub fn new(backend: impl CompletionProviderBackend) -> Self {
Self(Arc::new(backend))
}

View File

@@ -1,176 +1,5 @@
use anyhow::Result;
use assistant_tooling::LanguageModelTool;
use gpui::{prelude::*, AnyElement, AppContext, Model, Task};
use project::Fs;
use schemars::JsonSchema;
use semantic_index::ProjectIndex;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use ui::{
div, prelude::*, CollapsibleContainer, Color, Icon, IconName, Label, SharedString,
WindowContext,
};
use util::ResultExt as _;
mod create_buffer;
mod project_index;
const DEFAULT_SEARCH_LIMIT: usize = 20;
#[derive(Serialize, Clone)]
pub struct CodebaseExcerpt {
path: SharedString,
text: SharedString,
score: f32,
}
// Note: Comments on a `LanguageModelTool::Input` become descriptions on the generated JSON schema as shown to the language model.
// Any changes or deletions to the `CodebaseQuery` comments will change model behavior.
#[derive(Deserialize, JsonSchema)]
pub struct CodebaseQuery {
/// Semantic search query
query: String,
/// Maximum number of results to return, defaults to 20
limit: Option<usize>,
}
pub struct ProjectIndexTool {
project_index: Model<ProjectIndex>,
fs: Arc<dyn Fs>,
}
impl ProjectIndexTool {
pub fn new(project_index: Model<ProjectIndex>, fs: Arc<dyn Fs>) -> Self {
// TODO: setup a better description based on the user's current codebase.
Self { project_index, fs }
}
}
impl LanguageModelTool for ProjectIndexTool {
type Input = CodebaseQuery;
type Output = Vec<CodebaseExcerpt>;
fn name(&self) -> String {
"query_codebase".to_string()
}
fn description(&self) -> String {
"Semantic search against the user's current codebase, returning excerpts related to the query by computing a dot product against embeddings of chunks and an embedding of the query".to_string()
}
fn execute(&self, query: &Self::Input, cx: &AppContext) -> Task<Result<Self::Output>> {
let project_index = self.project_index.read(cx);
let results = project_index.search(
query.query.as_str(),
query.limit.unwrap_or(DEFAULT_SEARCH_LIMIT),
cx,
);
let fs = self.fs.clone();
cx.spawn(|cx| async move {
let results = results.await;
let excerpts = results.into_iter().map(|result| {
let abs_path = result
.worktree
.read_with(&cx, |worktree, _| worktree.abs_path().join(&result.path));
let fs = fs.clone();
async move {
let path = result.path.clone();
let text = fs.load(&abs_path?).await?;
let mut start = result.range.start;
let mut end = result.range.end.min(text.len());
while !text.is_char_boundary(start) {
start += 1;
}
while !text.is_char_boundary(end) {
end -= 1;
}
anyhow::Ok(CodebaseExcerpt {
path: path.to_string_lossy().to_string().into(),
text: SharedString::from(text[start..end].to_string()),
score: result.score,
})
}
});
let excerpts = futures::future::join_all(excerpts)
.await
.into_iter()
.filter_map(|result| result.log_err())
.collect();
anyhow::Ok(excerpts)
})
}
fn render(
_tool_call_id: &str,
input: &Self::Input,
excerpts: &Self::Output,
cx: &mut WindowContext,
) -> AnyElement {
let query = input.query.clone();
div()
.v_flex()
.gap_2()
.child(
div()
.p_2()
.rounded_md()
.bg(cx.theme().colors().editor_background)
.child(
h_flex()
.child(Label::new("Query: ").color(Color::Modified))
.child(Label::new(query).color(Color::Muted)),
),
)
.children(excerpts.iter().map(|excerpt| {
// This render doesn't have state/model, so we can't use the listener
// let expanded = excerpt.expanded;
// let element_id = excerpt.element_id.clone();
let element_id = ElementId::Name(nanoid::nanoid!().into());
let expanded = false;
CollapsibleContainer::new(element_id.clone(), expanded)
.start_slot(
h_flex()
.gap_1()
.child(Icon::new(IconName::File).color(Color::Muted))
.child(Label::new(excerpt.path.clone()).color(Color::Muted)),
)
// .on_click(cx.listener(move |this, _, cx| {
// this.toggle_expanded(element_id.clone(), cx);
// }))
.child(
div()
.p_2()
.rounded_md()
.bg(cx.theme().colors().editor_background)
.child(
excerpt.text.clone(), // todo!(): Show as an editor block
),
)
}))
.into_any_element()
}
fn format(_input: &Self::Input, excerpts: &Self::Output) -> String {
let mut body = "Semantic search results:\n".to_string();
for excerpt in excerpts {
body.push_str("Excerpt from ");
body.push_str(excerpt.path.as_ref());
body.push_str(", score ");
body.push_str(&excerpt.score.to_string());
body.push_str(":\n");
body.push_str("~~~\n");
body.push_str(excerpt.text.as_ref());
body.push_str("~~~\n");
}
body
}
}
pub use create_buffer::*;
pub use project_index::*;

View File

@@ -0,0 +1,111 @@
use anyhow::Result;
use assistant_tooling::LanguageModelTool;
use editor::Editor;
use gpui::{prelude::*, Model, Task, View, WeakView};
use project::Project;
use schemars::JsonSchema;
use serde::Deserialize;
use ui::prelude::*;
use util::ResultExt;
use workspace::Workspace;
pub struct CreateBufferTool {
workspace: WeakView<Workspace>,
project: Model<Project>,
}
impl CreateBufferTool {
pub fn new(workspace: WeakView<Workspace>, project: Model<Project>) -> Self {
Self { workspace, project }
}
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct CreateBufferInput {
/// The contents of the buffer.
text: String,
/// The name of the language to use for the buffer.
///
/// This should be a human-readable name, like "Rust", "JavaScript", or "Python".
language: String,
}
pub struct CreateBufferOutput {}
impl LanguageModelTool for CreateBufferTool {
type Input = CreateBufferInput;
type Output = CreateBufferOutput;
type View = CreateBufferView;
fn name(&self) -> String {
"create_buffer".to_string()
}
fn description(&self) -> String {
"Create a new buffer in the current codebase".to_string()
}
fn execute(&self, input: &Self::Input, cx: &mut WindowContext) -> Task<Result<Self::Output>> {
cx.spawn({
let workspace = self.workspace.clone();
let project = self.project.clone();
let text = input.text.clone();
let language_name = input.language.clone();
|mut cx| async move {
let language = cx
.update(|cx| {
project
.read(cx)
.languages()
.language_for_name(&language_name)
})?
.await?;
let buffer = cx.update(|cx| {
project.update(cx, |project, cx| {
project.create_buffer(&text, Some(language), cx)
})
})??;
workspace
.update(&mut cx, |workspace, cx| {
workspace.add_item_to_active_pane(
Box::new(
cx.new_view(|cx| Editor::for_buffer(buffer, Some(project), cx)),
),
None,
cx,
);
})
.log_err();
Ok(CreateBufferOutput {})
}
})
}
fn format(input: &Self::Input, output: &Result<Self::Output>) -> String {
match output {
Ok(_) => format!("Created a new {} buffer", input.language),
Err(err) => format!("Failed to create buffer: {err:?}"),
}
}
fn output_view(
_tool_call_id: String,
_input: Self::Input,
_output: Result<Self::Output>,
cx: &mut WindowContext,
) -> View<Self::View> {
cx.new_view(|_cx| CreateBufferView {})
}
}
pub struct CreateBufferView {}
impl Render for CreateBufferView {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
div().child("Opening a buffer")
}
}

View File

@@ -0,0 +1,267 @@
use anyhow::Result;
use assistant_tooling::LanguageModelTool;
use gpui::{prelude::*, AnyView, Model, Task};
use project::Fs;
use schemars::JsonSchema;
use semantic_index::{ProjectIndex, Status};
use serde::Deserialize;
use std::sync::Arc;
use ui::{
div, prelude::*, CollapsibleContainer, Color, Icon, IconName, Label, SharedString,
WindowContext,
};
use util::ResultExt as _;
const DEFAULT_SEARCH_LIMIT: usize = 20;
#[derive(Clone)]
pub struct CodebaseExcerpt {
path: SharedString,
text: SharedString,
score: f32,
element_id: ElementId,
expanded: bool,
}
// Note: Comments on a `LanguageModelTool::Input` become descriptions on the generated JSON schema as shown to the language model.
// Any changes or deletions to the `CodebaseQuery` comments will change model behavior.
#[derive(Deserialize, JsonSchema)]
pub struct CodebaseQuery {
/// Semantic search query
query: String,
/// Maximum number of results to return, defaults to 20
limit: Option<usize>,
}
pub struct ProjectIndexView {
input: CodebaseQuery,
output: Result<ProjectIndexOutput>,
}
impl ProjectIndexView {
fn toggle_expanded(&mut self, element_id: ElementId, cx: &mut ViewContext<Self>) {
if let Ok(output) = &mut self.output {
if let Some(excerpt) = output
.excerpts
.iter_mut()
.find(|excerpt| excerpt.element_id == element_id)
{
excerpt.expanded = !excerpt.expanded;
cx.notify();
}
}
}
}
impl Render for ProjectIndexView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let query = self.input.query.clone();
let result = &self.output;
let output = match result {
Err(err) => {
return div().child(Label::new(format!("Error: {}", err)).color(Color::Error));
}
Ok(output) => output,
};
div()
.v_flex()
.gap_2()
.child(
div()
.p_2()
.rounded_md()
.bg(cx.theme().colors().editor_background)
.child(
h_flex()
.child(Label::new("Query: ").color(Color::Modified))
.child(Label::new(query).color(Color::Muted)),
),
)
.children(output.excerpts.iter().map(|excerpt| {
let element_id = excerpt.element_id.clone();
let expanded = excerpt.expanded;
CollapsibleContainer::new(element_id.clone(), expanded)
.start_slot(
h_flex()
.gap_1()
.child(Icon::new(IconName::File).color(Color::Muted))
.child(Label::new(excerpt.path.clone()).color(Color::Muted)),
)
.on_click(cx.listener(move |this, _, cx| {
this.toggle_expanded(element_id.clone(), cx);
}))
.child(
div()
.p_2()
.rounded_md()
.bg(cx.theme().colors().editor_background)
.child(excerpt.text.clone()),
)
}))
}
}
pub struct ProjectIndexTool {
project_index: Model<ProjectIndex>,
fs: Arc<dyn Fs>,
}
pub struct ProjectIndexOutput {
excerpts: Vec<CodebaseExcerpt>,
status: Status,
}
impl ProjectIndexTool {
pub fn new(project_index: Model<ProjectIndex>, fs: Arc<dyn Fs>) -> Self {
// Listen for project index status and update the ProjectIndexTool directly
// TODO: setup a better description based on the user's current codebase.
Self { project_index, fs }
}
}
impl LanguageModelTool for ProjectIndexTool {
type Input = CodebaseQuery;
type Output = ProjectIndexOutput;
type View = ProjectIndexView;
fn name(&self) -> String {
"query_codebase".to_string()
}
fn description(&self) -> String {
"Semantic search against the user's current codebase, returning excerpts related to the query by computing a dot product against embeddings of chunks and an embedding of the query".to_string()
}
fn execute(&self, query: &Self::Input, cx: &mut WindowContext) -> Task<Result<Self::Output>> {
let project_index = self.project_index.read(cx);
let status = project_index.status();
let results = project_index.search(
query.query.as_str(),
query.limit.unwrap_or(DEFAULT_SEARCH_LIMIT),
cx,
);
let fs = self.fs.clone();
cx.spawn(|cx| async move {
let results = results.await;
let excerpts = results.into_iter().map(|result| {
let abs_path = result
.worktree
.read_with(&cx, |worktree, _| worktree.abs_path().join(&result.path));
let fs = fs.clone();
async move {
let path = result.path.clone();
let text = fs.load(&abs_path?).await?;
let mut start = result.range.start;
let mut end = result.range.end.min(text.len());
while !text.is_char_boundary(start) {
start += 1;
}
while !text.is_char_boundary(end) {
end -= 1;
}
anyhow::Ok(CodebaseExcerpt {
element_id: ElementId::Name(nanoid::nanoid!().into()),
expanded: false,
path: path.to_string_lossy().to_string().into(),
text: SharedString::from(text[start..end].to_string()),
score: result.score,
})
}
});
let excerpts = futures::future::join_all(excerpts)
.await
.into_iter()
.filter_map(|result| result.log_err())
.collect();
anyhow::Ok(ProjectIndexOutput { excerpts, status })
})
}
fn output_view(
_tool_call_id: String,
input: Self::Input,
output: Result<Self::Output>,
cx: &mut WindowContext,
) -> gpui::View<Self::View> {
cx.new_view(|_cx| ProjectIndexView { input, output })
}
fn status_view(&self, cx: &mut WindowContext) -> Option<AnyView> {
Some(
cx.new_view(|cx| ProjectIndexStatusView::new(self.project_index.clone(), cx))
.into(),
)
}
fn format(_input: &Self::Input, output: &Result<Self::Output>) -> String {
match &output {
Ok(output) => {
let mut body = "Semantic search results:\n".to_string();
if output.status != Status::Idle {
body.push_str("Still indexing. Results may be incomplete.\n");
}
if output.excerpts.is_empty() {
body.push_str("No results found");
return body;
}
for excerpt in &output.excerpts {
body.push_str("Excerpt from ");
body.push_str(excerpt.path.as_ref());
body.push_str(", score ");
body.push_str(&excerpt.score.to_string());
body.push_str(":\n");
body.push_str("~~~\n");
body.push_str(excerpt.text.as_ref());
body.push_str("~~~\n");
}
body
}
Err(err) => format!("Error: {}", err),
}
}
}
struct ProjectIndexStatusView {
project_index: Model<ProjectIndex>,
}
impl ProjectIndexStatusView {
pub fn new(project_index: Model<ProjectIndex>, cx: &mut ViewContext<Self>) -> Self {
cx.subscribe(&project_index, |_this, _, _status: &Status, cx| {
cx.notify();
})
.detach();
Self { project_index }
}
}
impl Render for ProjectIndexStatusView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let status = self.project_index.read(cx).status();
h_flex().gap_2().map(|element| match status {
Status::Idle => element.child(Label::new("Project index ready")),
Status::Loading => element.child(Label::new("Project index loading...")),
Status::Scanning { remaining_count } => element.child(Label::new(format!(
"Project index scanning: {remaining_count} remaining..."
))),
})
}
}

View File

@@ -0,0 +1,11 @@
mod chat_message;
mod composer;
#[cfg(feature = "stories")]
mod stories;
pub use chat_message::*;
pub use composer::*;
#[cfg(feature = "stories")]
pub use stories::*;

View File

@@ -0,0 +1,134 @@
use std::sync::Arc;
use client::User;
use gpui::{AnyElement, ClickEvent};
use ui::{prelude::*, Avatar};
use crate::MessageId;
pub enum UserOrAssistant {
User(Option<Arc<User>>),
Assistant,
}
#[derive(IntoElement)]
pub struct ChatMessage {
id: MessageId,
player: UserOrAssistant,
message: Option<AnyElement>,
collapsed: bool,
on_collapse_handle_click: Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>,
}
impl ChatMessage {
pub fn new(
id: MessageId,
player: UserOrAssistant,
message: Option<AnyElement>,
collapsed: bool,
on_collapse_handle_click: Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>,
) -> Self {
Self {
id,
player,
message,
collapsed,
on_collapse_handle_click,
}
}
}
impl RenderOnce for ChatMessage {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let collapse_handle_id = SharedString::from(format!("{}_collapse_handle", self.id.0));
let collapse_handle = h_flex()
.id(collapse_handle_id.clone())
.group(collapse_handle_id.clone())
.flex_none()
.justify_center()
.w_1()
.mx_2()
.h_full()
.on_click(self.on_collapse_handle_click)
.child(
div()
.w_px()
.h_full()
.rounded_lg()
.overflow_hidden()
.bg(cx.theme().colors().element_background)
.group_hover(collapse_handle_id, |this| {
this.bg(cx.theme().colors().element_hover)
}),
);
let content_padding = rems(1.);
// Clamp the message height to exactly 1.5 lines when collapsed.
let collapsed_height = content_padding.to_pixels(cx.rem_size()) + cx.line_height() * 1.5;
let content = self.message.map(|message| {
div()
.overflow_hidden()
.w_full()
.p(content_padding)
.rounded_lg()
.when(self.collapsed, |this| this.h(collapsed_height))
.bg(cx.theme().colors().surface_background)
.child(message)
});
v_flex()
.gap_1()
.child(ChatMessageHeader::new(self.player))
.child(h_flex().gap_3().child(collapse_handle).children(content))
}
}
#[derive(IntoElement)]
struct ChatMessageHeader {
player: UserOrAssistant,
contexts: Vec<()>,
}
impl ChatMessageHeader {
fn new(player: UserOrAssistant) -> Self {
Self {
player,
contexts: Vec::new(),
}
}
}
impl RenderOnce for ChatMessageHeader {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
let (username, avatar_uri) = match self.player {
UserOrAssistant::Assistant => (
"Assistant".into(),
Some("https://zed.dev/assistant_avatar.png".into()),
),
UserOrAssistant::User(Some(user)) => {
(user.github_login.clone(), Some(user.avatar_uri.clone()))
}
UserOrAssistant::User(None) => ("You".into(), None),
};
h_flex()
.justify_between()
.child(
h_flex()
.gap_3()
.map(|this| {
let avatar_size = rems(20.0 / 16.0);
if let Some(avatar_uri) = avatar_uri {
this.child(Avatar::new(avatar_uri).size(avatar_size))
} else {
this.child(div().size(avatar_size))
}
})
.child(Label::new(username).color(Color::Default)),
)
.child(div().when(!self.contexts.is_empty(), |this| {
this.child(Label::new(self.contexts.len().to_string()).color(Color::Muted))
}))
}
}

View File

@@ -0,0 +1,220 @@
use assistant_tooling::ToolRegistry;
use client::User;
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{AnyElement, FontStyle, FontWeight, TextStyle, View, WeakView, WhiteSpace};
use settings::Settings;
use std::sync::Arc;
use theme::ThemeSettings;
use ui::{popover_menu, prelude::*, Avatar, ButtonLike, ContextMenu, Tooltip};
use crate::{AssistantChat, CompletionProvider, Submit, SubmitMode};
#[derive(IntoElement)]
pub struct Composer {
editor: View<Editor>,
player: Option<Arc<User>>,
can_submit: bool,
tool_registry: Arc<ToolRegistry>,
model_selector: AnyElement,
}
impl Composer {
pub fn new(
editor: View<Editor>,
player: Option<Arc<User>>,
can_submit: bool,
tool_registry: Arc<ToolRegistry>,
model_selector: AnyElement,
) -> Self {
Self {
editor,
player,
can_submit,
tool_registry,
model_selector,
}
}
}
impl RenderOnce for Composer {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let mut player_avatar = div().size(rems(20.0 / 16.0)).into_any_element();
if let Some(player) = self.player.clone() {
player_avatar = Avatar::new(player.avatar_uri.clone())
.size(rems(20.0 / 16.0))
.into_any_element();
}
let font_size = rems(0.875);
let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
h_flex()
.w_full()
.items_start()
.mt_4()
.gap_3()
.child(player_avatar)
.child(
v_flex()
.size_full()
.gap_1()
.pr_4()
.child(
v_flex()
.w_full()
.p_4()
.bg(cx.theme().colors().editor_background)
.rounded_lg()
.child(
v_flex()
.justify_between()
.w_full()
.gap_1()
.min_h(line_height * 4 + px(74.0))
.child({
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: cx.theme().colors().editor_foreground,
font_family: settings.buffer_font.family.clone(),
font_features: settings.buffer_font.features.clone(),
font_size: font_size.into(),
font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal,
line_height: line_height.into(),
background_color: None,
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
};
EditorElement::new(
&self.editor,
EditorStyle {
background: cx.theme().colors().editor_background,
local_player: cx.theme().players().local(),
text: text_style,
..Default::default()
},
)
})
.child(
h_flex()
.flex_none()
.gap_2()
.justify_between()
.w_full()
.child(
h_flex().gap_1().child(
// IconButton/button
// Toggle - if enabled, .selected(true).selected_style(IconButtonStyle::Filled)
//
// match status
// Tooltip::with_meta("some label explaining project index + status", "click to enable")
IconButton::new(
"add-context",
IconName::FileDoc,
)
.icon_color(Color::Muted),
), // .child(
// IconButton::new(
// "add-context",
// IconName::Plus,
// )
// .icon_color(Color::Muted),
// ),
)
.child(
Button::new("send-button", "Send")
.style(ButtonStyle::Filled)
.disabled(!self.can_submit)
.on_click(|_, cx| {
cx.dispatch_action(Box::new(Submit(
SubmitMode::Codebase,
)))
})
.tooltip(|cx| {
Tooltip::for_action(
"Submit message",
&Submit(SubmitMode::Codebase),
cx,
)
}),
),
),
),
)
.child(
h_flex()
.w_full()
.justify_between()
.child(self.model_selector)
.children(self.tool_registry.status_views().iter().cloned()),
),
)
}
}
#[derive(IntoElement)]
pub struct ModelSelector {
assistant_chat: WeakView<AssistantChat>,
model: String,
}
impl ModelSelector {
pub fn new(assistant_chat: WeakView<AssistantChat>, model: String) -> Self {
Self {
assistant_chat,
model,
}
}
}
impl RenderOnce for ModelSelector {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
popover_menu("model-switcher")
.menu(move |cx| {
ContextMenu::build(cx, |mut menu, cx| {
for model in CompletionProvider::get(cx).available_models() {
menu = menu.custom_entry(
{
let model = model.clone();
move |_| Label::new(model.clone()).into_any_element()
},
{
let assistant_chat = self.assistant_chat.clone();
move |cx| {
_ = assistant_chat.update(cx, |assistant_chat, cx| {
assistant_chat.model = model.clone();
cx.notify();
});
}
},
);
}
menu
})
.into()
})
.trigger(
ButtonLike::new("active-model")
.child(
h_flex()
.w_full()
.gap_0p5()
.child(
div()
.overflow_x_hidden()
.flex_grow()
.whitespace_nowrap()
.child(Label::new(self.model)),
)
.child(
div().child(Icon::new(IconName::ChevronDown).color(Color::Muted)),
),
)
.style(ButtonStyle::Subtle)
.tooltip(move |cx| Tooltip::text("Change Model", cx)),
)
.anchor(gpui::AnchorCorner::BottomRight)
}
}

View File

@@ -0,0 +1,3 @@
mod chat_message;
pub use chat_message::*;

View File

@@ -0,0 +1,101 @@
use std::sync::Arc;
use client::User;
use story::{StoryContainer, StoryItem, StorySection};
use ui::prelude::*;
use crate::ui::{ChatMessage, UserOrAssistant};
use crate::MessageId;
pub struct ChatMessageStory;
impl Render for ChatMessageStory {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
let user_1 = Arc::new(User {
id: 12345,
github_login: "iamnbutler".into(),
avatar_uri: "https://avatars.githubusercontent.com/u/1714999?v=4".into(),
});
StoryContainer::new(
"ChatMessage Story",
"crates/assistant2/src/ui/stories/chat_message.rs",
)
.child(
StorySection::new()
.child(StoryItem::new(
"User chat message",
ChatMessage::new(
MessageId(0),
UserOrAssistant::User(Some(user_1.clone())),
Some(div().child("What can I do here?").into_any_element()),
false,
Box::new(|_, _| {}),
),
))
.child(StoryItem::new(
"User chat message (collapsed)",
ChatMessage::new(
MessageId(0),
UserOrAssistant::User(Some(user_1.clone())),
Some(div().child("What can I do here?").into_any_element()),
true,
Box::new(|_, _| {}),
),
)),
)
.child(
StorySection::new()
.child(StoryItem::new(
"Assistant chat message",
ChatMessage::new(
MessageId(0),
UserOrAssistant::Assistant,
Some(div().child("You can talk to me!").into_any_element()),
false,
Box::new(|_, _| {}),
),
))
.child(StoryItem::new(
"Assistant chat message (collapsed)",
ChatMessage::new(
MessageId(0),
UserOrAssistant::Assistant,
Some(div().child(MULTI_LINE_MESSAGE).into_any_element()),
true,
Box::new(|_, _| {}),
),
)),
)
.child(
StorySection::new().child(StoryItem::new(
"Conversation between user and assistant",
v_flex()
.gap_2()
.child(ChatMessage::new(
MessageId(0),
UserOrAssistant::User(Some(user_1.clone())),
Some(div().child("What is Rust??").into_any_element()),
false,
Box::new(|_, _| {}),
))
.child(ChatMessage::new(
MessageId(0),
UserOrAssistant::Assistant,
Some(div().child("Rust is a multi-paradigm programming language focused on performance and safety").into_any_element()),
false,
Box::new(|_, _| {}),
))
.child(ChatMessage::new(
MessageId(0),
UserOrAssistant::User(Some(user_1)),
Some(div().child("Sounds pretty cool!").into_any_element()),
false,
Box::new(|_, _| {}),
)),
)),
)
}
}
const MULTI_LINE_MESSAGE: &str = "In 2010, the movies nominated for the 82nd Academy Awards, for films released in 2009, were as follows. Note that 2010 nominees were announced for the ceremony happening in that year, but they honor movies from the previous year";

View File

@@ -1,14 +1,18 @@
use anyhow::{anyhow, Result};
use gpui::{AnyElement, AppContext, Task, WindowContext};
use std::{any::Any, collections::HashMap};
use gpui::{AnyView, Task, WindowContext};
use std::collections::HashMap;
use crate::tool::{
LanguageModelTool, ToolFunctionCall, ToolFunctionCallResult, ToolFunctionDefinition,
};
pub struct ToolRegistry {
tools: HashMap<String, Box<dyn Fn(&ToolFunctionCall, &AppContext) -> Task<ToolFunctionCall>>>,
tools: HashMap<
String,
Box<dyn Fn(&ToolFunctionCall, &mut WindowContext) -> Task<Result<ToolFunctionCall>>>,
>,
definitions: Vec<ToolFunctionDefinition>,
status_views: Vec<AnyView>,
}
impl ToolRegistry {
@@ -16,6 +20,7 @@ impl ToolRegistry {
Self {
tools: HashMap::new(),
definitions: Vec::new(),
status_views: Vec::new(),
}
}
@@ -23,78 +28,55 @@ impl ToolRegistry {
&self.definitions
}
pub fn register<T: 'static + LanguageModelTool>(&mut self, tool: T) -> Result<()> {
fn render<T: 'static + LanguageModelTool>(
tool_call_id: &str,
input: &Box<dyn Any>,
output: &Box<dyn Any>,
cx: &mut WindowContext,
) -> AnyElement {
T::render(
tool_call_id,
input.as_ref().downcast_ref::<T::Input>().unwrap(),
output.as_ref().downcast_ref::<T::Output>().unwrap(),
cx,
)
}
fn format<T: 'static + LanguageModelTool>(
input: &Box<dyn Any>,
output: &Box<dyn Any>,
) -> String {
T::format(
input.as_ref().downcast_ref::<T::Input>().unwrap(),
output.as_ref().downcast_ref::<T::Output>().unwrap(),
)
}
pub fn register<T: 'static + LanguageModelTool>(
&mut self,
tool: T,
cx: &mut WindowContext,
) -> Result<()> {
self.definitions.push(tool.definition());
if let Some(tool_view) = tool.status_view(cx) {
self.status_views.push(tool_view);
}
let name = tool.name();
let previous = self.tools.insert(
name.clone(),
Box::new(move |tool_call: &ToolFunctionCall, cx: &AppContext| {
let name = tool_call.name.clone();
let arguments = tool_call.arguments.clone();
let id = tool_call.id.clone();
// registry.call(tool_call, cx)
Box::new(
move |tool_call: &ToolFunctionCall, cx: &mut WindowContext| {
let name = tool_call.name.clone();
let arguments = tool_call.arguments.clone();
let id = tool_call.id.clone();
let Ok(input) = serde_json::from_str::<T::Input>(arguments.as_str()) else {
return Task::ready(ToolFunctionCall {
id,
name: name.clone(),
arguments,
result: Some(ToolFunctionCallResult::ParsingFailed),
});
};
let result = tool.execute(&input, cx);
cx.spawn(move |_cx| async move {
match result.await {
Ok(result) => {
let result: T::Output = result;
ToolFunctionCall {
id,
name: name.clone(),
arguments,
result: Some(ToolFunctionCallResult::Finished {
input: Box::new(input),
output: Box::new(result),
render_fn: render::<T>,
format_fn: format::<T>,
}),
}
}
Err(_error) => ToolFunctionCall {
let Ok(input) = serde_json::from_str::<T::Input>(arguments.as_str()) else {
return Task::ready(Ok(ToolFunctionCall {
id,
name: name.clone(),
arguments,
result: Some(ToolFunctionCallResult::ExecutionFailed {
input: Box::new(input),
result: Some(ToolFunctionCallResult::ParsingFailed),
}));
};
let result = tool.execute(&input, cx);
cx.spawn(move |mut cx| async move {
let result: Result<T::Output> = result.await;
let for_model = T::format(&input, &result);
let view = cx.update(|cx| T::output_view(id.clone(), input, result, cx))?;
Ok(ToolFunctionCall {
id,
name: name.clone(),
arguments,
result: Some(ToolFunctionCallResult::Finished {
view: view.into(),
for_model,
}),
},
}
})
}),
})
})
},
),
);
if previous.is_some() {
@@ -104,7 +86,12 @@ impl ToolRegistry {
Ok(())
}
pub fn call(&self, tool_call: &ToolFunctionCall, cx: &AppContext) -> Task<ToolFunctionCall> {
/// Task yields an error if the window for the given WindowContext is closed before the task completes.
pub fn call(
&self,
tool_call: &ToolFunctionCall,
cx: &mut WindowContext,
) -> Task<Result<ToolFunctionCall>> {
let name = tool_call.name.clone();
let arguments = tool_call.arguments.clone();
let id = tool_call.id.clone();
@@ -113,27 +100,29 @@ impl ToolRegistry {
Some(tool) => tool,
None => {
let name = name.clone();
return Task::ready(ToolFunctionCall {
return Task::ready(Ok(ToolFunctionCall {
id,
name: name.clone(),
arguments,
result: Some(ToolFunctionCallResult::NoSuchTool),
});
}));
}
};
tool(tool_call, cx)
}
pub fn status_views(&self) -> &[AnyView] {
&self.status_views
}
}
#[cfg(test)]
mod test {
use super::*;
use gpui::{div, prelude::*, Render, TestAppContext};
use gpui::{EmptyView, View};
use schemars::schema_for;
use gpui::{div, AnyElement, Element, ParentElement, TestAppContext, WindowContext};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::json;
@@ -155,9 +144,20 @@ mod test {
unit: String,
}
struct WeatherView {
result: WeatherResult,
}
impl Render for WeatherView {
fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
div().child(format!("temperature: {}", self.result.temperature))
}
}
impl LanguageModelTool for WeatherTool {
type Input = WeatherQuery;
type Output = WeatherResult;
type View = WeatherView;
fn name(&self) -> String {
"get_current_weather".to_string()
@@ -167,7 +167,11 @@ mod test {
"Fetches the current weather for a given location.".to_string()
}
fn execute(&self, input: &WeatherQuery, _cx: &AppContext) -> Task<Result<Self::Output>> {
fn execute(
&self,
input: &Self::Input,
_cx: &mut WindowContext,
) -> Task<Result<Self::Output>> {
let _location = input.location.clone();
let _unit = input.unit.clone();
@@ -176,71 +180,27 @@ mod test {
Task::ready(Ok(weather))
}
fn render(
_tool_call_id: &str,
_input: &Self::Input,
output: &Self::Output,
_cx: &mut WindowContext,
) -> AnyElement {
div()
.child(format!(
"The current temperature in {} is {} {}",
output.location, output.temperature, output.unit
))
.into_any()
}
fn format(_input: &Self::Input, output: &Self::Output) -> String {
format!(
"The current temperature in {} is {} {}",
output.location, output.temperature, output.unit
)
}
}
#[gpui::test]
async fn test_function_registry(cx: &mut TestAppContext) {
cx.background_executor.run_until_parked();
let mut registry = ToolRegistry::new();
let tool = WeatherTool {
current_weather: WeatherResult {
location: "San Francisco".to_string(),
temperature: 21.0,
unit: "Celsius".to_string(),
},
};
registry.register(tool).unwrap();
let _result = cx
.update(|cx| {
registry.call(
&ToolFunctionCall {
name: "get_current_weather".to_string(),
arguments: r#"{ "location": "San Francisco", "unit": "Celsius" }"#
.to_string(),
id: "test-123".to_string(),
result: None,
},
cx,
)
fn output_view(
_tool_call_id: String,
_input: Self::Input,
result: Result<Self::Output>,
cx: &mut WindowContext,
) -> View<Self::View> {
cx.new_view(|_cx| {
let result = result.unwrap();
WeatherView { result }
})
.await;
}
// assert!(result.is_ok());
// let result = result.unwrap();
// let expected = r#"{"location":"San Francisco","temperature":21.0,"unit":"Celsius"}"#;
// todo!(): Put this back in after the interface is stabilized
// assert_eq!(result, expected);
fn format(_: &Self::Input, output: &Result<Self::Output>) -> String {
serde_json::to_string(&output.as_ref().unwrap()).unwrap()
}
}
#[gpui::test]
async fn test_openai_weather_example(cx: &mut TestAppContext) {
cx.background_executor.run_until_parked();
let (_, cx) = cx.add_window_view(|_cx| EmptyView);
let tool = WeatherTool {
current_weather: WeatherResult {

View File

@@ -1,11 +1,8 @@
use anyhow::Result;
use gpui::{div, AnyElement, AppContext, Element, ParentElement as _, Task, WindowContext};
use gpui::{AnyElement, AnyView, IntoElement as _, Render, Task, View, WindowContext};
use schemars::{schema::RootSchema, schema_for, JsonSchema};
use serde::Deserialize;
use std::{
any::Any,
fmt::{Debug, Display},
};
use std::fmt::Display;
#[derive(Default, Deserialize)]
pub struct ToolFunctionCall {
@@ -19,71 +16,29 @@ pub struct ToolFunctionCall {
pub enum ToolFunctionCallResult {
NoSuchTool,
ParsingFailed,
ExecutionFailed {
input: Box<dyn Any>,
},
Finished {
input: Box<dyn Any>,
output: Box<dyn Any>,
render_fn: fn(
// tool_call_id
&str,
// LanguageModelTool::Input
&Box<dyn Any>,
// LanguageModelTool::Output
&Box<dyn Any>,
&mut WindowContext,
) -> AnyElement,
format_fn: fn(
// LanguageModelTool::Input
&Box<dyn Any>,
// LanguageModelTool::Output
&Box<dyn Any>,
) -> String,
},
Finished { for_model: String, view: AnyView },
}
impl ToolFunctionCallResult {
pub fn render(
&self,
tool_name: &str,
tool_call_id: &str,
cx: &mut WindowContext,
) -> AnyElement {
pub fn format(&self, name: &String) -> String {
match self {
ToolFunctionCallResult::NoSuchTool => {
div().child(format!("no such tool {tool_name}")).into_any()
ToolFunctionCallResult::NoSuchTool => format!("No tool for {name}"),
ToolFunctionCallResult::ParsingFailed => {
format!("Unable to parse arguments for {name}")
}
ToolFunctionCallResult::ParsingFailed => div()
.child(format!("failed to parse input for tool {tool_name}"))
.into_any(),
ToolFunctionCallResult::ExecutionFailed { .. } => div()
.child(format!("failed to execute tool {tool_name}"))
.into_any(),
ToolFunctionCallResult::Finished {
input,
output,
render_fn,
..
} => render_fn(tool_call_id, input, output, cx),
ToolFunctionCallResult::Finished { for_model, .. } => for_model.clone(),
}
}
pub fn format(&self, tool: &str) -> String {
pub fn into_any_element(&self, name: &String) -> AnyElement {
match self {
ToolFunctionCallResult::NoSuchTool => format!("no such tool {tool}"),
ToolFunctionCallResult::NoSuchTool => {
format!("Language Model attempted to call {name}").into_any_element()
}
ToolFunctionCallResult::ParsingFailed => {
format!("failed to parse input for tool {tool}")
format!("Language Model called {name} with bad arguments").into_any_element()
}
ToolFunctionCallResult::ExecutionFailed { input: _input } => {
format!("failed to execute tool {tool}")
}
ToolFunctionCallResult::Finished {
input,
output,
format_fn,
..
} => format_fn(input, output),
ToolFunctionCallResult::Finished { view, .. } => view.clone().into_any_element(),
}
}
}
@@ -105,19 +60,6 @@ impl Display for ToolFunctionDefinition {
}
}
impl Debug for ToolFunctionDefinition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let schema = serde_json::to_string(&self.parameters).ok();
let schema = schema.unwrap_or("None".to_string());
f.debug_struct("ToolFunctionDefinition")
.field("name", &self.name)
.field("description", &self.description)
.field("parameters", &schema)
.finish()
}
}
pub trait LanguageModelTool {
/// The input type that will be passed in to `execute` when the tool is called
/// by the language model.
@@ -126,16 +68,21 @@ pub trait LanguageModelTool {
/// The output returned by executing the tool.
type Output: 'static;
/// The name of the tool is exposed to the language model to allow
/// the model to pick which tools to use. As this name is used to
/// identify the tool within a tool registry, it should be unique.
type View: Render;
/// Returns the name of the tool.
///
/// This name is exposed to the language model to allow the model to pick
/// which tools to use. As this name is used to identify the tool within a
/// tool registry, it should be unique.
fn name(&self) -> String;
/// A description of the tool that can be used to _prompt_ the model
/// as to what the tool does.
/// Returns the description of the tool.
///
/// This can be used to _prompt_ the model as to what the tool does.
fn description(&self) -> String;
/// The OpenAI Function definition for the tool, for direct use with OpenAI's API.
/// Returns the OpenAI Function definition for the tool, for direct use with OpenAI's API.
fn definition(&self) -> ToolFunctionDefinition {
let root_schema = schema_for!(Self::Input);
@@ -146,15 +93,19 @@ pub trait LanguageModelTool {
}
}
/// Execute the tool
fn execute(&self, input: &Self::Input, cx: &AppContext) -> Task<Result<Self::Output>>;
/// Executes the tool with the given input.
fn execute(&self, input: &Self::Input, cx: &mut WindowContext) -> Task<Result<Self::Output>>;
fn render(
tool_call_id: &str,
input: &Self::Input,
output: &Self::Output,
fn format(input: &Self::Input, output: &Result<Self::Output>) -> String;
fn output_view(
tool_call_id: String,
input: Self::Input,
output: Result<Self::Output>,
cx: &mut WindowContext,
) -> AnyElement;
) -> View<Self::View>;
fn format(input: &Self::Input, output: &Self::Output) -> String;
fn status_view(&self, _cx: &mut WindowContext) -> Option<AnyView> {
None
}
}

View File

@@ -243,7 +243,7 @@ fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Wo
Some(tab_description),
cx,
);
workspace.add_item_to_active_pane(Box::new(view.clone()), cx);
workspace.add_item_to_active_pane(Box::new(view.clone()), None, cx);
cx.notify();
})
.log_err();

View File

@@ -33,7 +33,7 @@ impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
impl Render for Breadcrumbs {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
const MAX_SEGMENTS: usize = 12;
let element = h_flex().text_ui();
let element = h_flex().text_ui(cx);
let Some(active_item) = self.active_item.as_ref() else {
return element;
};

View File

@@ -36,6 +36,9 @@ struct Args {
/// Custom Zed.app path
#[arg(short, long)]
bundle_path: Option<PathBuf>,
/// Run zed in dev-server mode
#[arg(long)]
dev_server_token: Option<String>,
}
fn parse_path_with_position(
@@ -67,6 +70,10 @@ fn main() -> Result<()> {
let bundle = Bundle::detect(args.bundle_path.as_deref()).context("Bundle detection")?;
if let Some(dev_server_token) = args.dev_server_token {
return bundle.spawn(vec!["--dev-server-token".into(), dev_server_token]);
}
if args.version {
println!("{}", bundle.zed_version_string());
return Ok(());
@@ -169,6 +176,10 @@ mod linux {
unimplemented!()
}
pub fn spawn(&self, _args: Vec<String>) -> anyhow::Result<()> {
unimplemented!()
}
pub fn zed_version_string(&self) -> String {
unimplemented!()
}
@@ -202,6 +213,10 @@ mod windows {
unimplemented!()
}
pub fn spawn(&self, _args: Vec<String>) -> anyhow::Result<()> {
unimplemented!()
}
pub fn zed_version_string(&self) -> String {
unimplemented!()
}
@@ -217,7 +232,7 @@ mod mac_os {
url::{CFURLCreateWithBytes, CFURL},
};
use core_services::{kLSLaunchDefaults, LSLaunchURLSpec, LSOpenFromURLSpec, TCFType};
use std::{fs, path::Path, ptr};
use std::{fs, path::Path, process::Command, ptr};
use cli::{CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME};
use ipc_channel::ipc::{IpcOneShotServer, IpcReceiver, IpcSender};
@@ -278,6 +293,15 @@ mod mac_os {
}
}
pub fn spawn(&self, args: Vec<String>) -> Result<()> {
let path = match self {
Self::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed"),
Self::LocalPath { executable, .. } => executable.clone(),
};
Command::new(path).args(args).status()?;
Ok(())
}
pub fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
let (server, server_name) =
IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
@@ -358,12 +382,12 @@ mod mac_os {
)
}
}
pub(super) fn spawn_channel_cli(
channel: release_channel::ReleaseChannel,
leftover_args: Vec<String>,
) -> Result<()> {
use anyhow::bail;
use std::process::Command;
let app_id_prompt = format!("id of app \"{}\"", channel.display_name());
let app_id_output = Command::new("osascript")

View File

@@ -421,7 +421,7 @@ impl Telemetry {
return;
}
let Some(checksum_seed) = &*ZED_CLIENT_CHECKSUM_SEED else {
if ZED_CLIENT_CHECKSUM_SEED.is_none() {
return;
};
@@ -466,15 +466,9 @@ impl Telemetry {
serde_json::to_writer(&mut json_bytes, &request_body)?;
}
let mut summer = Sha256::new();
summer.update(checksum_seed);
summer.update(&json_bytes);
summer.update(checksum_seed);
let mut checksum = String::new();
for byte in summer.finalize().as_slice() {
use std::fmt::Write;
write!(&mut checksum, "{:02x}", byte).unwrap();
}
let Some(checksum) = calculate_json_checksum(&json_bytes) else {
return Ok(());
};
let request = http::Request::builder()
.method(Method::POST)
@@ -657,3 +651,21 @@ mod tests {
&& telemetry.state.lock().first_event_date_time.is_none()
}
}
pub fn calculate_json_checksum(json: &impl AsRef<[u8]>) -> Option<String> {
let Some(checksum_seed) = &*ZED_CLIENT_CHECKSUM_SEED else {
return None;
};
let mut summer = Sha256::new();
summer.update(checksum_seed);
summer.update(&json);
summer.update(checksum_seed);
let mut checksum = String::new();
for byte in summer.finalize().as_slice() {
use std::fmt::Write;
write!(&mut checksum, "{:02x}", byte).unwrap();
}
Some(checksum)
}

View File

@@ -37,7 +37,7 @@ google_ai.workspace = true
hex.workspace = true
live_kit_server.workspace = true
log.workspace = true
nanoid = "0.4"
nanoid.workspace = true
open_ai.workspace = true
parking_lot.workspace = true
prometheus = "0.13"

View File

@@ -18,11 +18,15 @@ use telemetry_events::{
ActionEvent, AppEvent, AssistantEvent, CallEvent, CopilotEvent, CpuEvent, EditEvent,
EditorEvent, Event, EventRequestBody, EventWrapper, ExtensionEvent, MemoryEvent, SettingEvent,
};
use uuid::Uuid;
static CRASH_REPORTS_BUCKET: &str = "zed-crash-reports";
pub fn router() -> Router {
Router::new()
.route("/telemetry/events", post(post_events))
.route("/telemetry/crashes", post(post_crash))
.route("/telemetry/hangs", post(post_hang))
}
pub struct ZedChecksumHeader(Vec<u8>);
@@ -85,8 +89,6 @@ pub async fn post_crash(
headers: HeaderMap,
body: Bytes,
) -> Result<()> {
static CRASH_REPORTS_BUCKET: &str = "zed-crash-reports";
let report = IpsFile::parse(&body)?;
let version_threshold = SemanticVersion::new(0, 123, 0);
@@ -136,6 +138,13 @@ pub async fn post_crash(
.get("x-zed-panicked-on")
.and_then(|h| h.to_str().ok())
.and_then(|s| s.parse().ok());
let installation_id = headers
.get("x-zed-installation-id")
.and_then(|h| h.to_str().ok())
.map(|s| s.to_string())
.unwrap_or_default();
let mut recent_panic = None;
if let Some(recent_panic_on) = recent_panic_on {
@@ -160,6 +169,7 @@ pub async fn post_crash(
os_version = %report.header.os_version,
bundle_id = %report.header.bundle_id,
incident_id = %report.header.incident_id,
installation_id = %installation_id,
description = %description,
backtrace = %summary,
"crash report");
@@ -214,6 +224,107 @@ pub async fn post_crash(
Ok(())
}
pub async fn post_hang(
Extension(app): Extension<Arc<AppState>>,
TypedHeader(ZedChecksumHeader(checksum)): TypedHeader<ZedChecksumHeader>,
body: Bytes,
) -> Result<()> {
let Some(expected) = calculate_json_checksum(app.clone(), &body) else {
return Err(Error::Http(
StatusCode::INTERNAL_SERVER_ERROR,
"events not enabled".into(),
))?;
};
if checksum != expected {
return Err(Error::Http(
StatusCode::BAD_REQUEST,
"invalid checksum".into(),
))?;
}
let incident_id = Uuid::new_v4().to_string();
// dump JSON into S3 so we can get frame offsets if we need to.
if let Some(blob_store_client) = app.blob_store_client.as_ref() {
blob_store_client
.put_object()
.bucket(CRASH_REPORTS_BUCKET)
.key(incident_id.clone() + ".hang.json")
.acl(aws_sdk_s3::types::ObjectCannedAcl::PublicRead)
.body(ByteStream::from(body.to_vec()))
.send()
.await
.map_err(|e| log::error!("Failed to upload crash: {}", e))
.ok();
}
let report: telemetry_events::HangReport = serde_json::from_slice(&body).map_err(|err| {
log::error!("can't parse report json: {err}");
Error::Internal(anyhow!(err))
})?;
let mut backtrace = "Possible hang detected on main threadL".to_string();
let unknown = "<unknown>".to_string();
for frame in report.backtrace.iter() {
backtrace.push_str(&format!("\n{}", frame.symbols.first().unwrap_or(&unknown)));
}
tracing::error!(
service = "client",
version = %report.app_version.unwrap_or_default().to_string(),
os_name = %report.os_name,
os_version = report.os_version.unwrap_or_default().to_string(),
incident_id = %incident_id,
installation_id = %report.installation_id.unwrap_or_default(),
backtrace = %backtrace,
"hang report");
if let Some(slack_panics_webhook) = app.config.slack_panics_webhook.clone() {
let payload = slack::WebhookBody::new(|w| {
w.add_section(|s| s.text(slack::Text::markdown("Possible Hang".to_string())))
.add_section(|s| {
s.add_field(slack::Text::markdown(format!(
"*Version:*\n {} ",
report.app_version.unwrap_or_default()
)))
.add_field({
let hostname = app.config.blob_store_url.clone().unwrap_or_default();
let hostname = hostname.strip_prefix("https://").unwrap_or_else(|| {
hostname.strip_prefix("http://").unwrap_or_default()
});
slack::Text::markdown(format!(
"*Incident:*\n<https://{}.{}/{}.hang.json|{}…>",
CRASH_REPORTS_BUCKET,
hostname,
incident_id,
incident_id.chars().take(8).collect::<String>(),
))
})
})
.add_rich_text(|r| r.add_preformatted(|p| p.add_text(backtrace)))
});
let payload_json = serde_json::to_string(&payload).map_err(|err| {
log::error!("Failed to serialize payload to JSON: {err}");
Error::Internal(anyhow!(err))
})?;
reqwest::Client::new()
.post(slack_panics_webhook)
.header("Content-Type", "application/json")
.body(payload_json)
.send()
.await
.map_err(|err| {
log::error!("Failed to send payload to Slack: {err}");
Error::Internal(anyhow!(err))
})?;
}
Ok(())
}
pub async fn post_events(
Extension(app): Extension<Arc<AppState>>,
TypedHeader(ZedChecksumHeader(checksum)): TypedHeader<ZedChecksumHeader>,
@@ -227,19 +338,14 @@ pub async fn post_events(
))?
};
let Some(checksum_seed) = app.config.zed_client_checksum_seed.as_ref() else {
let Some(expected) = calculate_json_checksum(app.clone(), &body) else {
return Err(Error::Http(
StatusCode::INTERNAL_SERVER_ERROR,
"events not enabled".into(),
))?;
};
let mut summer = Sha256::new();
summer.update(checksum_seed);
summer.update(&body);
summer.update(checksum_seed);
if &checksum != &summer.finalize()[..] {
if checksum != expected {
return Err(Error::Http(
StatusCode::BAD_REQUEST,
"invalid checksum".into(),
@@ -1053,3 +1159,15 @@ impl ActionEventRow {
}
}
}
pub fn calculate_json_checksum(app: Arc<AppState>, json: &impl AsRef<[u8]>) -> Option<Vec<u8>> {
let Some(checksum_seed) = app.config.zed_client_checksum_seed.as_ref() else {
return None;
};
let mut summer = Sha256::new();
summer.update(checksum_seed);
summer.update(&json);
summer.update(checksum_seed);
Some(summer.finalize().into_iter().collect())
}

View File

@@ -70,6 +70,7 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC
workspace::join_remote_project(
projects[0].project_id.unwrap(),
client.app_state.clone(),
None,
cx,
)
})
@@ -205,7 +206,12 @@ async fn create_remote_project(
let projects = store.remote_projects();
assert_eq!(projects.len(), 1);
assert_eq!(projects[0].path, "/remote");
workspace::join_remote_project(projects[0].project_id.unwrap(), client_app_state, cx)
workspace::join_remote_project(
projects[0].project_id.unwrap(),
client_app_state,
None,
cx,
)
})
.await
.unwrap();
@@ -301,6 +307,7 @@ async fn test_dev_server_reconnect(
workspace::join_remote_project(
projects[0].project_id.unwrap(),
client2.app_state.clone(),
None,
cx,
)
})
@@ -359,3 +366,35 @@ async fn test_create_remote_project_path_validation(
ErrorCode::RemoteProjectPathDoesNotExist
));
}
#[gpui::test]
async fn test_save_as_remote(cx1: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppContext) {
let (server, client1) = TestServer::start1(cx1).await;
// Creating a project with a path that does exist should not fail
let (dev_server, remote_workspace) =
create_remote_project(&server, client1.app_state.clone(), cx1, cx2).await;
let mut cx = VisualTestContext::from_window(remote_workspace.into(), cx1);
cx.simulate_keystrokes("cmd-p 1 enter");
cx.simulate_keystrokes("cmd-shift-s");
cx.simulate_input("2.txt");
cx.simulate_keystrokes("enter");
cx.executor().run_until_parked();
let title = remote_workspace
.update(&mut cx, |ws, cx| {
ws.active_item(cx).unwrap().tab_description(0, &cx).unwrap()
})
.unwrap();
assert_eq!(title, "2.txt");
let path = Path::new("/remote/2.txt");
assert_eq!(
dev_server.fs().load(&path).await.unwrap(),
"remote\nremote\nremote"
);
}

View File

@@ -310,7 +310,7 @@ async fn test_basic_following(
let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| {
let editor =
cx.new_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
workspace.add_item_to_active_pane(Box::new(editor.clone()), cx);
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, cx);
editor
});
executor.run_until_parked();

View File

@@ -2468,7 +2468,12 @@ async fn test_propagate_saves_and_fs_changes(
});
project_a
.update(cx_a, |project, cx| {
project.save_buffer_as(new_buffer_a.clone(), "/a/file3.rs".into(), cx)
let path = ProjectPath {
path: Arc::from(Path::new("file3.rs")),
worktree_id: worktree_a.read(cx).id(),
};
project.save_buffer_as(new_buffer_a.clone(), path, cx)
})
.await
.unwrap();
@@ -3185,7 +3190,7 @@ async fn test_fs_operations(
project_b
.update(cx_b, |project, cx| {
project.delete_entry(dir_entry.id, cx).unwrap()
project.delete_entry(dir_entry.id, false, cx).unwrap()
})
.await
.unwrap();
@@ -3213,7 +3218,7 @@ async fn test_fs_operations(
project_b
.update(cx_b, |project, cx| {
project.delete_entry(entry.id, cx).unwrap()
project.delete_entry(entry.id, false, cx).unwrap()
})
.await
.unwrap();

View File

@@ -315,7 +315,7 @@ impl ChatPanel {
None => {
return div().child(
h_flex()
.text_ui_xs()
.text_ui_xs(cx)
.my_0p5()
.px_0p5()
.gap_x_1()
@@ -350,7 +350,7 @@ impl ChatPanel {
div().child(
h_flex()
.id(message_element_id)
.text_ui_xs()
.text_ui_xs(cx)
.my_0p5()
.px_0p5()
.gap_x_1()
@@ -495,7 +495,7 @@ impl ChatPanel {
|this| {
this.child(
h_flex()
.text_ui_sm()
.text_ui_sm(cx)
.child(
div().absolute().child(
Avatar::new(message.sender.avatar_uri.clone())
@@ -539,7 +539,7 @@ impl ChatPanel {
el.child(
v_flex()
.w_full()
.text_ui_sm()
.text_ui_sm(cx)
.id(element_id)
.child(text.element("body".into(), cx)),
)
@@ -562,7 +562,7 @@ impl ChatPanel {
div()
.px_1()
.rounded_md()
.text_ui_xs()
.text_ui_xs(cx)
.bg(cx.theme().colors().background)
.child("New messages"),
)
@@ -1003,7 +1003,7 @@ impl Render for ChatPanel {
el.child(
h_flex()
.px_2()
.text_ui_xs()
.text_ui_xs(cx)
.justify_between()
.border_t_1()
.border_color(cx.theme().colors().border)

View File

@@ -18,7 +18,7 @@ use project::{search::SearchQuery, Completion};
use settings::Settings;
use std::{ops::Range, sync::Arc, time::Duration};
use theme::ThemeSettings;
use ui::{prelude::*, UiTextSize};
use ui::{prelude::*, TextSize};
use crate::panel_settings::MessageEditorSettings;
@@ -522,8 +522,8 @@ impl Render for MessageEditor {
cx.theme().colors().text
},
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features,
font_size: UiTextSize::Small.rems().into(),
font_features: settings.ui_font.features.clone(),
font_size: TextSize::Small.rems(cx).into(),
font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal,
line_height: relative(1.3),

View File

@@ -2171,7 +2171,7 @@ impl CollabPanel {
cx.theme().colors().text
},
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features,
font_features: settings.ui_font.features.clone(),
font_size: rems(0.875).into(),
font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal,
@@ -2970,6 +2970,7 @@ impl Render for DraggedChannelView {
struct JoinChannelTooltip {
channel_store: Model<ChannelStore>,
channel_id: ChannelId,
#[allow(unused)]
has_notes_notification: bool,
}
@@ -2983,12 +2984,6 @@ impl Render for JoinChannelTooltip {
container
.child(Label::new("Join channel"))
.children(self.has_notes_notification.then(|| {
h_flex()
.gap_2()
.child(Indicator::dot().color(Color::Info))
.child(Label::new("Unread notes"))
}))
.children(participants.iter().map(|participant| {
h_flex()
.gap_2()

View File

@@ -122,5 +122,6 @@ fn notification_window_options(
display_id: Some(screen.id()),
fullscreen: false,
window_background: WindowBackgroundAppearance::default(),
app_id: Some("dev.zed.Zed".to_owned()),
}
}

View File

@@ -34,7 +34,7 @@ impl ParentElement for CollabNotification {
impl RenderOnce for CollabNotification {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
h_flex()
.text_ui()
.text_ui(cx)
.justify_between()
.size_full()
.overflow_hidden()

View File

@@ -477,7 +477,7 @@ mod tests {
});
workspace.update(cx, |workspace, cx| {
workspace.add_item_to_active_pane(Box::new(editor.clone()), cx);
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, cx);
editor.update(cx, |editor, cx| editor.focus(cx))
});

View File

@@ -15,13 +15,16 @@ doctest = false
[dependencies]
anyhow.workspace = true
collections.workspace = true
ctor.workspace = true
editor.workspace = true
env_logger.workspace = true
futures.workspace = true
gpui.workspace = true
language.workspace = true
log.workspace = true
lsp.workspace = true
project.workspace = true
rand.workspace = true
schemars.workspace = true
serde.workspace = true
settings.workspace = true
@@ -40,3 +43,4 @@ serde_json.workspace = true
theme = { workspace = true, features = ["test-support"] }
unindent.workspace = true
workspace = { workspace = true, features = ["test-support"] }
pretty_assertions.workspace = true

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,11 @@
use std::time::Duration;
use collections::HashSet;
use editor::Editor;
use gpui::{
percentage, rems, Animation, AnimationExt, EventEmitter, IntoElement, ParentElement, Render,
Styled, Subscription, Transformation, View, ViewContext, WeakView,
};
use language::Diagnostic;
use lsp::LanguageServerId;
use ui::{h_flex, prelude::*, Button, ButtonLike, Color, Icon, IconName, Label, Tooltip};
use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
@@ -18,7 +16,6 @@ pub struct DiagnosticIndicator {
active_editor: Option<WeakView<Editor>>,
workspace: WeakView<Workspace>,
current_diagnostic: Option<Diagnostic>,
in_progress_checks: HashSet<LanguageServerId>,
_observe_active_editor: Option<Subscription>,
}
@@ -64,7 +61,20 @@ impl Render for DiagnosticIndicator {
.child(Label::new(warning_count.to_string()).size(LabelSize::Small)),
};
let status = if !self.in_progress_checks.is_empty() {
let has_in_progress_checks = self
.workspace
.upgrade()
.and_then(|workspace| {
workspace
.read(cx)
.project()
.read(cx)
.language_servers_running_disk_based_diagnostics()
.next()
})
.is_some();
let status = if has_in_progress_checks {
Some(
h_flex()
.gap_2()
@@ -126,15 +136,13 @@ impl DiagnosticIndicator {
pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
let project = workspace.project();
cx.subscribe(project, |this, project, event, cx| match event {
project::Event::DiskBasedDiagnosticsStarted { language_server_id } => {
this.in_progress_checks.insert(*language_server_id);
project::Event::DiskBasedDiagnosticsStarted { .. } => {
cx.notify();
}
project::Event::DiskBasedDiagnosticsFinished { language_server_id }
| project::Event::LanguageServerRemoved(language_server_id) => {
project::Event::DiskBasedDiagnosticsFinished { .. }
| project::Event::LanguageServerRemoved(_) => {
this.summary = project.read(cx).diagnostic_summary(false, cx);
this.in_progress_checks.remove(language_server_id);
cx.notify();
}
@@ -149,10 +157,6 @@ impl DiagnosticIndicator {
Self {
summary: project.read(cx).diagnostic_summary(false, cx),
in_progress_checks: project
.read(cx)
.language_servers_running_disk_based_diagnostics()
.collect(),
active_editor: None,
workspace: workspace.weak_handle(),
current_diagnostic: None,

View File

@@ -1,5 +1,5 @@
use crate::ProjectDiagnosticsEditor;
use gpui::{div, EventEmitter, ParentElement, Render, ViewContext, WeakView};
use gpui::{EventEmitter, ParentElement, Render, ViewContext, WeakView};
use ui::prelude::*;
use ui::{IconButton, IconName, Tooltip};
use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
@@ -10,12 +10,23 @@ pub struct ToolbarControls {
impl Render for ToolbarControls {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let include_warnings = self
.editor
.as_ref()
.and_then(|editor| editor.upgrade())
.map(|editor| editor.read(cx).include_warnings)
.unwrap_or(false);
let mut include_warnings = false;
let mut has_stale_excerpts = false;
let mut is_updating = false;
if let Some(editor) = self.editor.as_ref().and_then(|editor| editor.upgrade()) {
let editor = editor.read(cx);
include_warnings = editor.include_warnings;
has_stale_excerpts = !editor.paths_to_update.is_empty();
is_updating = editor.update_paths_tx.len() > 0
|| editor
.project
.read(cx)
.language_servers_running_disk_based_diagnostics()
.next()
.is_some();
}
let tooltip = if include_warnings {
"Exclude Warnings"
@@ -23,17 +34,37 @@ impl Render for ToolbarControls {
"Include Warnings"
};
div().child(
IconButton::new("toggle-warnings", IconName::ExclamationTriangle)
.tooltip(move |cx| Tooltip::text(tooltip, cx))
.on_click(cx.listener(|this, _, cx| {
if let Some(editor) = this.editor.as_ref().and_then(|editor| editor.upgrade()) {
editor.update(cx, |editor, cx| {
editor.toggle_warnings(&Default::default(), cx);
});
}
})),
)
h_flex()
.when(has_stale_excerpts, |div| {
div.child(
IconButton::new("update-excerpts", IconName::Update)
.icon_color(Color::Info)
.disabled(is_updating)
.tooltip(move |cx| Tooltip::text("Update excerpts", cx))
.on_click(cx.listener(|this, _, cx| {
if let Some(editor) =
this.editor.as_ref().and_then(|editor| editor.upgrade())
{
editor.update(cx, |editor, _| {
editor.enqueue_update_stale_excerpts(None);
});
}
})),
)
})
.child(
IconButton::new("toggle-warnings", IconName::ExclamationTriangle)
.tooltip(move |cx| Tooltip::text(tooltip, cx))
.on_click(cx.listener(|this, _, cx| {
if let Some(editor) =
this.editor.as_ref().and_then(|editor| editor.upgrade())
{
editor.update(cx, |editor, cx| {
editor.toggle_warnings(&Default::default(), cx);
});
}
})),
)
}
}

View File

@@ -39,17 +39,15 @@ impl<'a> CommitAvatar<'a> {
let avatar_url = CommitAvatarAsset::new(remote.clone(), self.sha);
let element = cx.with_element_context(|cx| {
match cx.use_cached_asset::<CommitAvatarAsset>(&avatar_url) {
// Loading or no avatar found
None | Some(None) => Icon::new(IconName::Person)
.color(Color::Muted)
.into_element()
.into_any(),
// Found
Some(Some(url)) => Avatar::new(url.to_string()).into_element().into_any(),
}
});
let element = match cx.use_cached_asset::<CommitAvatarAsset>(&avatar_url) {
// Loading or no avatar found
None | Some(None) => Icon::new(IconName::Person)
.color(Color::Muted)
.into_element()
.into_any(),
// Found
Some(Some(url)) => Avatar::new(url.to_string()).into_element().into_any(),
};
Some(element)
}
}

View File

@@ -4,7 +4,7 @@ use super::{
};
use crate::{EditorStyle, GutterDimensions};
use collections::{Bound, HashMap, HashSet};
use gpui::{AnyElement, ElementContext, Pixels};
use gpui::{AnyElement, Pixels, WindowContext};
use language::{BufferSnapshot, Chunk, Patch, Point};
use multi_buffer::{Anchor, ExcerptId, ExcerptRange, ToPoint as _};
use parking_lot::Mutex;
@@ -82,7 +82,7 @@ pub enum BlockStyle {
}
pub struct BlockContext<'a, 'b> {
pub context: &'b mut ElementContext<'a>,
pub context: &'b mut WindowContext<'a>,
pub anchor_x: Pixels,
pub max_width: Pixels,
pub gutter_dimensions: &'b GutterDimensions,
@@ -934,7 +934,7 @@ impl BlockDisposition {
}
impl<'a> Deref for BlockContext<'a, '_> {
type Target = ElementContext<'a>;
type Target = WindowContext<'a>;
fn deref(&self) -> &Self::Target {
self.context

View File

@@ -130,7 +130,7 @@ use ui::{
Tooltip,
};
use util::{defer, maybe, post_inc, RangeExt, ResultExt, TryFutureExt};
use workspace::item::ItemHandle;
use workspace::item::{ItemHandle, PreviewTabsSettings};
use workspace::notifications::NotificationId;
use workspace::{
searchable::SearchEvent, ItemNavHistory, SplitDirection, ViewId, Workspace, WorkspaceId,
@@ -278,6 +278,8 @@ pub fn init(cx: &mut AppContext) {
});
}
pub struct SearchWithinRange;
trait InvalidationRegion {
fn ranges(&self) -> &[Range<Anchor>];
}
@@ -762,10 +764,10 @@ impl ContextMenu {
max_height: Pixels,
workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> (DisplayPoint, AnyElement) {
) -> (ContextMenuOrigin, AnyElement) {
match self {
ContextMenu::Completions(menu) => (
cursor_position,
ContextMenuOrigin::EditorPoint(cursor_position),
menu.render(style, max_height, workspace, cx),
),
ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, max_height, cx),
@@ -773,6 +775,11 @@ impl ContextMenu {
}
}
enum ContextMenuOrigin {
EditorPoint(DisplayPoint),
GutterIndicator(u32),
}
#[derive(Clone)]
struct CompletionsMenu {
id: CompletionId,
@@ -1206,11 +1213,11 @@ impl CodeActionsMenu {
fn render(
&self,
mut cursor_position: DisplayPoint,
cursor_position: DisplayPoint,
_style: &EditorStyle,
max_height: Pixels,
cx: &mut ViewContext<Editor>,
) -> (DisplayPoint, AnyElement) {
) -> (ContextMenuOrigin, AnyElement) {
let actions = self.actions.clone();
let selected_item = self.selected_item;
@@ -1275,10 +1282,11 @@ impl CodeActionsMenu {
)
.into_any_element();
if self.deployed_from_indicator {
*cursor_position.column_mut() = 0;
}
let cursor_position = if self.deployed_from_indicator {
ContextMenuOrigin::GutterIndicator(cursor_position.row())
} else {
ContextMenuOrigin::EditorPoint(cursor_position)
};
(cursor_position, element)
}
}
@@ -1610,6 +1618,7 @@ impl Editor {
{
workspace.add_item_to_active_pane(
Box::new(cx.new_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx))),
None,
cx,
);
}
@@ -3781,7 +3790,7 @@ impl Editor {
let project = workspace.project().clone();
let editor =
cx.new_view(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), cx));
workspace.add_item_to_active_pane(Box::new(editor.clone()), cx);
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, cx);
editor.update(cx, |editor, cx| {
editor.highlight_background::<Self>(
&ranges_to_highlight,
@@ -4244,13 +4253,13 @@ impl Editor {
.map_or(false, |menu| menu.visible())
}
pub fn render_context_menu(
fn render_context_menu(
&self,
cursor_position: DisplayPoint,
style: &EditorStyle,
max_height: Pixels,
cx: &mut ViewContext<Editor>,
) -> Option<(DisplayPoint, AnyElement)> {
) -> Option<(ContextMenuOrigin, AnyElement)> {
self.context_menu.read().as_ref().map(|menu| {
menu.render(
cursor_position,
@@ -7526,13 +7535,14 @@ impl Editor {
} else {
selection.head()
};
let snapshot = self.snapshot(cx);
loop {
let mut diagnostics = if direction == Direction::Prev {
buffer.diagnostics_in_range::<_, usize>(0..search_start, true)
} else {
buffer.diagnostics_in_range::<_, usize>(search_start..buffer.len(), false)
};
}
.filter(|diagnostic| !snapshot.intersects_fold(diagnostic.range.start));
let group = diagnostics.find_map(|entry| {
if entry.diagnostic.is_primary
&& entry.diagnostic.severity <= DiagnosticSeverity::WARNING
@@ -7745,7 +7755,13 @@ impl Editor {
.update(&mut cx, |editor, cx| {
editor.navigate_to_hover_links(
Some(kind),
definitions.into_iter().map(HoverLink::Text).collect(),
definitions
.into_iter()
.filter(|location| {
hover_links::exclude_link_to_position(&buffer, &head, location, cx)
})
.map(HoverLink::Text)
.collect::<Vec<_>>(),
split,
cx,
)
@@ -8096,14 +8112,23 @@ impl Editor {
cx,
);
});
let item = Box::new(editor);
let item_id = item.item_id();
if split {
workspace.split_item(SplitDirection::Right, item.clone(), cx);
} else {
workspace.add_item_to_active_pane(item.clone(), cx);
let destination_index = workspace.active_pane().update(cx, |pane, cx| {
if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation {
pane.close_current_preview_item(cx)
} else {
None
}
});
workspace.add_item_to_active_pane(item.clone(), destination_index, cx);
}
workspace.active_pane().clone().update(cx, |pane, cx| {
let item_id = item.item_id();
workspace.active_pane().update(cx, |pane, cx| {
pane.set_preview_item_id(Some(item_id), cx);
});
}
@@ -8200,9 +8225,13 @@ impl Editor {
cursor_offset_in_rename_range_end..cursor_offset_in_rename_range
}
};
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges([rename_selection_range]);
});
if rename_selection_range.end > old_name.len() {
editor.select_all(&SelectAll, cx);
} else {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges([rename_selection_range]);
});
}
editor
});
@@ -8473,6 +8502,7 @@ impl Editor {
fn activate_diagnostics(&mut self, group_id: usize, cx: &mut ViewContext<Self>) -> bool {
self.dismiss_diagnostics(cx);
let snapshot = self.snapshot(cx);
self.active_diagnostics = self.display_map.update(cx, |display_map, cx| {
let buffer = self.buffer.read(cx).snapshot(cx);
@@ -8481,7 +8511,13 @@ impl Editor {
let mut group_end = Point::zero();
let diagnostic_group = buffer
.diagnostic_group::<Point>(group_id)
.map(|entry| {
.filter_map(|entry| {
if snapshot.is_line_folded(entry.range.start.row)
&& (entry.range.start.row == entry.range.end.row
|| snapshot.is_line_folded(entry.range.end.row))
{
return None;
}
if entry.range.end > group_end {
group_end = entry.range.end;
}
@@ -8489,7 +8525,7 @@ impl Editor {
primary_range = Some(entry.range.clone());
primary_message = Some(entry.diagnostic.message.clone());
}
entry
Some(entry)
})
.collect::<Vec<_>>();
let primary_range = primary_range?;
@@ -8718,6 +8754,18 @@ impl Editor {
}
cx.notify();
if let Some(active_diagnostics) = self.active_diagnostics.take() {
// Clear diagnostics block when folding a range that contains it.
let snapshot = self.snapshot(cx);
if snapshot.intersects_fold(active_diagnostics.primary_range.start) {
drop(snapshot);
self.active_diagnostics = Some(active_diagnostics);
self.dismiss_diagnostics(cx);
} else {
self.active_diagnostics = Some(active_diagnostics);
}
}
}
}
@@ -8982,6 +9030,10 @@ impl Editor {
return;
};
if buffer.read(cx).file().is_none() {
return;
}
let project = project.clone();
let blame = cx.new_model(|cx| GitBlame::new(buffer, project, user_triggered, cx));
self.blame_subscription = Some(cx.observe(&blame, |_, _, cx| cx.notify()));
@@ -9220,6 +9272,18 @@ impl Editor {
)
}
pub fn set_search_within_ranges(
&mut self,
ranges: &[Range<Anchor>],
cx: &mut ViewContext<Self>,
) {
self.highlight_background::<SearchWithinRange>(
ranges,
|colors| colors.editor_document_highlight_read_background,
cx,
)
}
pub fn highlight_background<T: 'static>(
&mut self,
ranges: &[Range<Anchor>],
@@ -10323,11 +10387,23 @@ impl Render for Editor {
let settings = ThemeSettings::get_global(cx);
let text_style = match self.mode {
EditorMode::SingleLine | EditorMode::AutoHeight { .. } => cx.text_style(),
EditorMode::SingleLine | EditorMode::AutoHeight { .. } => TextStyle {
color: cx.theme().colors().editor_foreground,
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features.clone(),
font_size: rems(0.875).into(),
font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal,
line_height: relative(settings.buffer_line_height.value()),
background_color: None,
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
},
EditorMode::Full => TextStyle {
color: cx.theme().colors().editor_foreground,
font_family: settings.buffer_font.family.clone(),
font_features: settings.buffer_font.features,
font_features: settings.buffer_font.features.clone(),
font_size: settings.buffer_font_size(cx).into(),
font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal,
@@ -10752,7 +10828,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren
let theme_settings = ThemeSettings::get_global(cx);
text_style.font_family = theme_settings.buffer_font.family.clone();
text_style.font_style = theme_settings.buffer_font.style;
text_style.font_features = theme_settings.buffer_font.features;
text_style.font_features = theme_settings.buffer_font.features.clone();
text_style.font_weight = theme_settings.buffer_font.weight;
let multi_line_diagnostic = diagnostic.message.contains('\n');

View File

@@ -61,6 +61,7 @@ pub struct Scrollbar {
pub selected_symbol: bool,
pub search_results: bool,
pub diagnostics: bool,
pub cursors: bool,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@@ -206,6 +207,10 @@ pub struct ScrollbarContent {
///
/// Default: true
pub diagnostics: Option<bool>,
/// Whether to show cursor positions in the scrollbar.
///
/// Default: true
pub cursors: Option<bool>,
}
/// Gutter related settings

View File

@@ -9161,7 +9161,7 @@ async fn test_mutlibuffer_in_navigation_history(cx: &mut gpui::TestAppContext) {
workspace.active_item(cx).is_none(),
"active item should be None before the first item is added"
);
workspace.add_item_to_active_pane(Box::new(multi_buffer_editor.clone()), cx);
workspace.add_item_to_active_pane(Box::new(multi_buffer_editor.clone()), None, cx);
let active_item = workspace
.active_item(cx)
.expect("should have an active item after adding the multi buffer");

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@ use crate::{
Anchor, Editor, EditorSnapshot, FindAllReferences, GoToDefinition, GoToTypeDefinition, InlayId,
PointForPosition, SelectPhase,
};
use gpui::{px, AsyncWindowContext, Model, Modifiers, Task, ViewContext};
use gpui::{px, AppContext, AsyncWindowContext, Model, Modifiers, Task, ViewContext};
use language::{Bias, ToOffset};
use linkify::{LinkFinder, LinkKind};
use lsp::LanguageServerId;
@@ -11,8 +11,7 @@ use project::{
HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, LocationLink,
ResolveState,
};
use std::{cmp, ops::Range};
use text::Point;
use std::ops::Range;
use theme::ActiveTheme as _;
use util::{maybe, ResultExt, TryFutureExt};
@@ -85,6 +84,25 @@ impl TriggerPoint {
}
}
pub fn exclude_link_to_position(
buffer: &Model<language::Buffer>,
current_position: &text::Anchor,
location: &LocationLink,
cx: &AppContext,
) -> bool {
// Exclude definition links that points back to cursor position.
// (i.e., currently cursor upon definition).
let snapshot = buffer.read(cx).snapshot();
!(buffer == &location.target.buffer
&& current_position
.bias_right(&snapshot)
.cmp(&location.target.range.start, &snapshot)
.is_ge()
&& current_position
.cmp(&location.target.range.end, &snapshot)
.is_le())
}
impl Editor {
pub(crate) fn update_hovered_link(
&mut self,
@@ -132,28 +150,12 @@ impl Editor {
modifiers: Modifiers,
cx: &mut ViewContext<Editor>,
) {
let selection_before_revealing = self.selections.newest::<Point>(cx);
let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
let before_revealing_head = selection_before_revealing.head();
let before_revealing_tail = selection_before_revealing.tail();
let before_revealing = match before_revealing_tail.cmp(&before_revealing_head) {
cmp::Ordering::Equal | cmp::Ordering::Less => {
multi_buffer_snapshot.anchor_after(before_revealing_head)
..multi_buffer_snapshot.anchor_before(before_revealing_tail)
}
cmp::Ordering::Greater => {
multi_buffer_snapshot.anchor_before(before_revealing_tail)
..multi_buffer_snapshot.anchor_after(before_revealing_head)
}
};
drop(multi_buffer_snapshot);
let reveal_task = self.cmd_click_reveal_task(point, modifiers, cx);
cx.spawn(|editor, mut cx| async move {
let definition_revealed = reveal_task.await.log_err().unwrap_or(false);
let find_references = editor
.update(&mut cx, |editor, cx| {
if definition_revealed && revealed_elsewhere(editor, before_revealing, cx) {
if definition_revealed {
return None;
}
editor.find_all_references(&FindAllReferences, cx)
@@ -180,12 +182,30 @@ impl Editor {
cx.focus(&self.focus_handle);
}
return self.navigate_to_hover_links(
None,
hovered_link_state.links,
modifiers.alt,
cx,
);
// exclude links pointing back to the current anchor
let current_position = point
.next_valid
.to_point(&self.snapshot(cx).display_snapshot);
let Some((buffer, anchor)) = self
.buffer()
.read(cx)
.text_anchor_for_position(current_position, cx)
else {
return Task::ready(Ok(false));
};
let links = hovered_link_state
.links
.into_iter()
.filter(|link| {
if let HoverLink::Text(location) = link {
exclude_link_to_position(&buffer, &anchor, location, cx)
} else {
true
}
})
.collect();
return self.navigate_to_hover_links(None, links, modifiers.alt, cx);
}
}
@@ -212,46 +232,6 @@ impl Editor {
}
}
fn revealed_elsewhere(
editor: &mut Editor,
before_revealing: Range<Anchor>,
cx: &mut ViewContext<'_, Editor>,
) -> bool {
let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
let selection_after_revealing = editor.selections.newest::<Point>(cx);
let after_revealing_head = selection_after_revealing.head();
let after_revealing_tail = selection_after_revealing.tail();
let after_revealing = match after_revealing_tail.cmp(&after_revealing_head) {
cmp::Ordering::Equal | cmp::Ordering::Less => {
multi_buffer_snapshot.anchor_after(after_revealing_tail)
..multi_buffer_snapshot.anchor_before(after_revealing_head)
}
cmp::Ordering::Greater => {
multi_buffer_snapshot.anchor_after(after_revealing_head)
..multi_buffer_snapshot.anchor_before(after_revealing_tail)
}
};
let before_intersects_after_range = (before_revealing
.start
.cmp(&after_revealing.start, &multi_buffer_snapshot)
.is_ge()
&& before_revealing
.start
.cmp(&after_revealing.end, &multi_buffer_snapshot)
.is_le())
|| (before_revealing
.end
.cmp(&after_revealing.start, &multi_buffer_snapshot)
.is_ge()
&& before_revealing
.end
.cmp(&after_revealing.end, &multi_buffer_snapshot)
.is_le());
!before_intersects_after_range
}
pub fn update_inlay_link_and_hover_points(
snapshot: &EditorSnapshot,
point_for_position: PointForPosition,

View File

@@ -1,7 +1,7 @@
use crate::{
editor_settings::SeedQuerySetting, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll,
Editor, EditorEvent, EditorSettings, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot,
NavigationData, ToPoint as _,
NavigationData, SearchWithinRange, ToPoint as _,
};
use anyhow::{anyhow, Context as _, Result};
use collections::HashSet;
@@ -16,17 +16,19 @@ use language::{
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, OffsetRangeExt,
Point, SelectionGoal,
};
use multi_buffer::AnchorRangeExt;
use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath};
use rpc::proto::{self, update_view, PeerId};
use settings::Settings;
use workspace::item::{ItemSettings, TabContentParams};
use std::{
any::TypeId,
borrow::Cow,
cmp::{self, Ordering},
iter,
ops::Range,
path::{Path, PathBuf},
path::Path,
sync::Arc,
};
use text::{BufferId, Selection};
@@ -750,7 +752,7 @@ impl Item for Editor {
fn save_as(
&mut self,
project: Model<Project>,
abs_path: PathBuf,
path: ProjectPath,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
let buffer = self
@@ -759,14 +761,13 @@ impl Item for Editor {
.as_singleton()
.expect("cannot call save_as on an excerpt list");
let file_extension = abs_path
let file_extension = path
.path
.extension()
.map(|a| a.to_string_lossy().to_string());
self.report_editor_event("save", file_extension, cx);
project.update(cx, |project, cx| {
project.save_buffer_as(buffer, abs_path, cx)
})
project.update(cx, |project, cx| project.save_buffer_as(buffer, path, cx))
}
fn reload(&mut self, project: Model<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
@@ -1000,6 +1001,10 @@ impl SearchableItem for Editor {
);
}
fn has_filtered_search_ranges(&mut self) -> bool {
self.has_background_highlights::<SearchWithinRange>()
}
fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
let setting = EditorSettings::get_global(cx).seed_search_query_from_cursor;
let snapshot = &self.snapshot(cx).buffer_snapshot;
@@ -1124,18 +1129,37 @@ impl SearchableItem for Editor {
cx: &mut ViewContext<Self>,
) -> Task<Vec<Range<Anchor>>> {
let buffer = self.buffer().read(cx).snapshot(cx);
let search_within_ranges = self
.background_highlights
.get(&TypeId::of::<SearchWithinRange>())
.map(|(_color, ranges)| {
ranges
.iter()
.map(|range| range.to_offset(&buffer))
.collect::<Vec<_>>()
});
cx.background_executor().spawn(async move {
let mut ranges = Vec::new();
if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() {
ranges.extend(
query
.search(excerpt_buffer, None)
.await
.into_iter()
.map(|range| {
buffer.anchor_after(range.start)..buffer.anchor_before(range.end)
}),
);
if let Some(search_within_ranges) = search_within_ranges {
for range in search_within_ranges {
let offset = range.start;
ranges.extend(
query
.search(excerpt_buffer, Some(range))
.await
.into_iter()
.map(|range| {
buffer.anchor_after(range.start + offset)
..buffer.anchor_before(range.end + offset)
}),
);
}
} else {
ranges.extend(query.search(excerpt_buffer, None).await.into_iter().map(
|range| buffer.anchor_after(range.start)..buffer.anchor_before(range.end),
));
}
} else {
for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) {
let excerpt_range = excerpt.range.context.to_offset(&excerpt.buffer);

View File

@@ -106,6 +106,7 @@ pub fn expand_macro_recursively(
});
workspace.add_item_to_active_pane(
Box::new(cx.new_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx))),
None,
cx,
);
})

View File

@@ -21,6 +21,7 @@ const SUGGESTIONS_BY_EXTENSION_ID: &[(&str, &[&str])] = &[
("dart", &["dart"]),
("dockerfile", &["Dockerfile"]),
("elisp", &["el"]),
("elixir", &["ex", "exs", "heex"]),
("elm", &["elm"]),
("erlang", &["erl", "hrl"]),
("fish", &["fish"]),

View File

@@ -45,7 +45,7 @@ pub fn init(cx: &mut AppContext) {
workspace.activate_item(&existing, cx);
} else {
let extensions_page = ExtensionsPage::new(workspace, cx);
workspace.add_item_to_active_pane(Box::new(extensions_page), cx)
workspace.add_item_to_active_pane(Box::new(extensions_page), None, cx)
}
})
.register_action(move |_, _: &InstallDevExtension, cx| {
@@ -739,7 +739,7 @@ impl ExtensionsPage {
cx.theme().colors().text
},
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features,
font_features: settings.ui_font.features.clone(),
font_size: rems(0.875).into(),
font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal,

View File

@@ -16,6 +16,7 @@ doctest = false
anyhow.workspace = true
collections.workspace = true
editor.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
itertools = "0.11"

View File

@@ -1,6 +1,8 @@
#[cfg(test)]
mod file_finder_tests;
mod new_path_prompt;
use collections::{HashMap, HashSet};
use editor::{scroll::Autoscroll, Bias, Editor};
use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
@@ -10,6 +12,7 @@ use gpui::{
ViewContext, VisualContext, WeakView,
};
use itertools::Itertools;
use new_path_prompt::NewPathPrompt;
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
use settings::Settings;
@@ -37,6 +40,7 @@ pub struct FileFinder {
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(FileFinder::register).detach();
cx.observe_new_views(NewPathPrompt::register).detach();
}
impl FileFinder {
@@ -454,6 +458,7 @@ impl FileFinderDelegate {
.root_entry()
.map_or(false, |entry| entry.is_ignored),
include_root_name,
directories_only: false,
}
})
.collect::<Vec<_>>();

View File

@@ -0,0 +1,463 @@
use futures::channel::oneshot;
use fuzzy::PathMatch;
use gpui::{HighlightStyle, Model, StyledText};
use picker::{Picker, PickerDelegate};
use project::{Entry, PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
use std::{
path::PathBuf,
sync::{
atomic::{self, AtomicBool},
Arc,
},
};
use ui::{highlight_ranges, prelude::*, LabelLike, ListItemSpacing};
use ui::{ListItem, ViewContext};
use util::ResultExt;
use workspace::Workspace;
pub(crate) struct NewPathPrompt;
#[derive(Debug, Clone)]
struct Match {
path_match: Option<PathMatch>,
suffix: Option<String>,
}
impl Match {
fn entry<'a>(&'a self, project: &'a Project, cx: &'a WindowContext) -> Option<&'a Entry> {
if let Some(suffix) = &self.suffix {
let (worktree, path) = if let Some(path_match) = &self.path_match {
(
project.worktree_for_id(WorktreeId::from_usize(path_match.worktree_id), cx),
path_match.path.join(suffix),
)
} else {
(project.worktrees().next(), PathBuf::from(suffix))
};
worktree.and_then(|worktree| worktree.read(cx).entry_for_path(path))
} else if let Some(path_match) = &self.path_match {
let worktree =
project.worktree_for_id(WorktreeId::from_usize(path_match.worktree_id), cx)?;
worktree.read(cx).entry_for_path(path_match.path.as_ref())
} else {
None
}
}
fn is_dir(&self, project: &Project, cx: &WindowContext) -> bool {
self.entry(project, cx).is_some_and(|e| e.is_dir())
|| self.suffix.as_ref().is_some_and(|s| s.ends_with('/'))
}
fn relative_path(&self) -> String {
if let Some(path_match) = &self.path_match {
if let Some(suffix) = &self.suffix {
format!(
"{}/{}",
path_match.path.to_string_lossy(),
suffix.trim_end_matches('/')
)
} else {
path_match.path.to_string_lossy().to_string()
}
} else if let Some(suffix) = &self.suffix {
suffix.trim_end_matches('/').to_string()
} else {
"".to_string()
}
}
fn project_path(&self, project: &Project, cx: &WindowContext) -> Option<ProjectPath> {
let worktree_id = if let Some(path_match) = &self.path_match {
WorktreeId::from_usize(path_match.worktree_id)
} else {
project.worktrees().next()?.read(cx).id()
};
let path = PathBuf::from(self.relative_path());
Some(ProjectPath {
worktree_id,
path: Arc::from(path),
})
}
fn existing_prefix(&self, project: &Project, cx: &WindowContext) -> Option<PathBuf> {
let worktree = project.worktrees().next()?.read(cx);
let mut prefix = PathBuf::new();
let parts = self.suffix.as_ref()?.split('/');
for part in parts {
if worktree.entry_for_path(prefix.join(&part)).is_none() {
return Some(prefix);
}
prefix = prefix.join(part);
}
None
}
fn styled_text(&self, project: &Project, cx: &WindowContext) -> StyledText {
let mut text = "./".to_string();
let mut highlights = Vec::new();
let mut offset = text.as_bytes().len();
let separator = '/';
let dir_indicator = "[…]";
if let Some(path_match) = &self.path_match {
text.push_str(&path_match.path.to_string_lossy());
for (range, style) in highlight_ranges(
&path_match.path.to_string_lossy(),
&path_match.positions,
gpui::HighlightStyle::color(Color::Accent.color(cx)),
) {
highlights.push((range.start + offset..range.end + offset, style))
}
text.push(separator);
offset = text.as_bytes().len();
if let Some(suffix) = &self.suffix {
text.push_str(suffix);
let entry = self.entry(project, cx);
let color = if let Some(entry) = entry {
if entry.is_dir() {
Color::Accent
} else {
Color::Conflict
}
} else {
Color::Created
};
highlights.push((
offset..offset + suffix.as_bytes().len(),
HighlightStyle::color(color.color(cx)),
));
offset += suffix.as_bytes().len();
if entry.is_some_and(|e| e.is_dir()) {
text.push(separator);
offset += separator.len_utf8();
text.push_str(dir_indicator);
highlights.push((
offset..offset + dir_indicator.bytes().len(),
HighlightStyle::color(Color::Muted.color(cx)),
));
}
} else {
text.push_str(dir_indicator);
highlights.push((
offset..offset + dir_indicator.bytes().len(),
HighlightStyle::color(Color::Muted.color(cx)),
))
}
} else if let Some(suffix) = &self.suffix {
text.push_str(suffix);
let existing_prefix_len = self
.existing_prefix(project, cx)
.map(|prefix| prefix.to_string_lossy().as_bytes().len())
.unwrap_or(0);
if existing_prefix_len > 0 {
highlights.push((
offset..offset + existing_prefix_len,
HighlightStyle::color(Color::Accent.color(cx)),
));
}
highlights.push((
offset + existing_prefix_len..offset + suffix.as_bytes().len(),
HighlightStyle::color(if self.entry(project, cx).is_some() {
Color::Conflict.color(cx)
} else {
Color::Created.color(cx)
}),
));
offset += suffix.as_bytes().len();
if suffix.ends_with('/') {
text.push_str(dir_indicator);
highlights.push((
offset..offset + dir_indicator.bytes().len(),
HighlightStyle::color(Color::Muted.color(cx)),
));
}
}
StyledText::new(text).with_highlights(&cx.text_style().clone(), highlights)
}
}
pub struct NewPathDelegate {
project: Model<Project>,
tx: Option<oneshot::Sender<Option<ProjectPath>>>,
selected_index: usize,
matches: Vec<Match>,
last_selected_dir: Option<String>,
cancel_flag: Arc<AtomicBool>,
should_dismiss: bool,
}
impl NewPathPrompt {
pub(crate) fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
if workspace.project().read(cx).is_remote() {
workspace.set_prompt_for_new_path(Box::new(|workspace, cx| {
let (tx, rx) = futures::channel::oneshot::channel();
Self::prompt_for_new_path(workspace, tx, cx);
rx
}));
}
}
fn prompt_for_new_path(
workspace: &mut Workspace,
tx: oneshot::Sender<Option<ProjectPath>>,
cx: &mut ViewContext<Workspace>,
) {
let project = workspace.project().clone();
workspace.toggle_modal(cx, |cx| {
let delegate = NewPathDelegate {
project,
tx: Some(tx),
selected_index: 0,
matches: vec![],
cancel_flag: Arc::new(AtomicBool::new(false)),
last_selected_dir: None,
should_dismiss: true,
};
Picker::uniform_list(delegate, cx).width(rems(34.))
});
}
}
impl PickerDelegate for NewPathDelegate {
type ListItem = ui::ListItem;
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<picker::Picker<Self>>) {
self.selected_index = ix;
cx.notify();
}
fn update_matches(
&mut self,
query: String,
cx: &mut ViewContext<picker::Picker<Self>>,
) -> gpui::Task<()> {
let query = query.trim().trim_start_matches('/');
let (dir, suffix) = if let Some(index) = query.rfind('/') {
let suffix = if index + 1 < query.len() {
Some(query[index + 1..].to_string())
} else {
None
};
(query[0..index].to_string(), suffix)
} else {
(query.to_string(), None)
};
let worktrees = self
.project
.read(cx)
.visible_worktrees(cx)
.collect::<Vec<_>>();
let include_root_name = worktrees.len() > 1;
let candidate_sets = worktrees
.into_iter()
.map(|worktree| {
let worktree = worktree.read(cx);
PathMatchCandidateSet {
snapshot: worktree.snapshot(),
include_ignored: worktree
.root_entry()
.map_or(false, |entry| entry.is_ignored),
include_root_name,
directories_only: true,
}
})
.collect::<Vec<_>>();
self.cancel_flag.store(true, atomic::Ordering::Relaxed);
self.cancel_flag = Arc::new(AtomicBool::new(false));
let cancel_flag = self.cancel_flag.clone();
let query = query.to_string();
let prefix = dir.clone();
cx.spawn(|picker, mut cx| async move {
let matches = fuzzy::match_path_sets(
candidate_sets.as_slice(),
&dir,
None,
false,
100,
&cancel_flag,
cx.background_executor().clone(),
)
.await;
let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
if did_cancel {
return;
}
picker
.update(&mut cx, |picker, cx| {
picker
.delegate
.set_search_matches(query, prefix, suffix, matches, cx)
})
.log_err();
})
}
fn confirm_update_query(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<String> {
let m = self.matches.get(self.selected_index)?;
if m.is_dir(self.project.read(cx), cx) {
let path = m.relative_path();
self.last_selected_dir = Some(path.clone());
Some(format!("{}/", path))
} else {
None
}
}
fn confirm(&mut self, _: bool, cx: &mut ViewContext<picker::Picker<Self>>) {
let Some(m) = self.matches.get(self.selected_index) else {
return;
};
let exists = m.entry(self.project.read(cx), cx).is_some();
if exists {
self.should_dismiss = false;
let answer = cx.prompt(
gpui::PromptLevel::Destructive,
&format!("{} already exists. Do you want to replace it?", m.relative_path()),
Some(
"A file or folder with the same name already eixsts. Replacing it will overwrite its current contents.",
),
&["Replace", "Cancel"],
);
let m = m.clone();
cx.spawn(|picker, mut cx| async move {
let answer = answer.await.ok();
picker
.update(&mut cx, |picker, cx| {
picker.delegate.should_dismiss = true;
if answer != Some(0) {
return;
}
if let Some(path) = m.project_path(picker.delegate.project.read(cx), cx) {
if let Some(tx) = picker.delegate.tx.take() {
tx.send(Some(path)).ok();
}
}
cx.emit(gpui::DismissEvent);
})
.ok();
})
.detach();
return;
}
if let Some(path) = m.project_path(self.project.read(cx), cx) {
if let Some(tx) = self.tx.take() {
tx.send(Some(path)).ok();
}
}
cx.emit(gpui::DismissEvent);
}
fn should_dismiss(&self) -> bool {
self.should_dismiss
}
fn dismissed(&mut self, cx: &mut ViewContext<picker::Picker<Self>>) {
if let Some(tx) = self.tx.take() {
tx.send(None).ok();
}
cx.emit(gpui::DismissEvent)
}
fn render_match(
&self,
ix: usize,
selected: bool,
cx: &mut ViewContext<picker::Picker<Self>>,
) -> Option<Self::ListItem> {
let m = self.matches.get(ix)?;
Some(
ListItem::new(ix)
.spacing(ListItemSpacing::Sparse)
.inset(true)
.selected(selected)
.child(LabelLike::new().child(m.styled_text(self.project.read(cx), cx))),
)
}
fn no_matches_text(&self, _cx: &mut WindowContext) -> SharedString {
"Type a path...".into()
}
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
Arc::from("[directory/]filename.ext")
}
}
impl NewPathDelegate {
fn set_search_matches(
&mut self,
query: String,
prefix: String,
suffix: Option<String>,
matches: Vec<PathMatch>,
cx: &mut ViewContext<Picker<Self>>,
) {
cx.notify();
if query.is_empty() {
self.matches = vec![];
return;
}
let mut directory_exists = false;
self.matches = matches
.into_iter()
.map(|m| {
if m.path.as_ref().to_string_lossy() == prefix {
directory_exists = true
}
Match {
path_match: Some(m),
suffix: suffix.clone(),
}
})
.collect();
if !directory_exists {
if suffix.is_none()
|| self
.last_selected_dir
.as_ref()
.is_some_and(|d| query.starts_with(d))
{
self.matches.insert(
0,
Match {
path_match: None,
suffix: Some(query.clone()),
},
)
} else {
self.matches.push(Match {
path_match: None,
suffix: Some(query.clone()),
})
}
}
}
}

View File

@@ -31,6 +31,10 @@ pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
}
impl FileIcons {
pub fn get(cx: &AppContext) -> &Self {
cx.global::<FileIcons>()
}
pub fn new(assets: impl AssetSource) -> Self {
assets
.load("icons/file_icons/file_types.json")

View File

@@ -36,6 +36,9 @@ gpui = { workspace = true, optional = true }
[target.'cfg(target_os = "macos")'.dependencies]
fsevent.workspace = true
objc = "0.2"
cocoa = "0.25"
[target.'cfg(not(target_os = "macos"))'.dependencies]
notify = "6.1.1"

View File

@@ -49,7 +49,13 @@ pub trait Fs: Send + Sync {
async fn copy_file(&self, source: &Path, target: &Path, options: CopyOptions) -> Result<()>;
async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>;
async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>;
async fn trash_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
self.remove_dir(path, options).await
}
async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>;
async fn trash_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
self.remove_file(path, options).await
}
async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>>;
async fn load(&self, path: &Path) -> Result<String>;
async fn atomic_write(&self, path: PathBuf, text: String) -> Result<()>;
@@ -237,6 +243,33 @@ impl Fs for RealFs {
}
}
#[cfg(target_os = "macos")]
async fn trash_file(&self, path: &Path, _options: RemoveOptions) -> Result<()> {
use cocoa::{
base::{id, nil},
foundation::{NSAutoreleasePool, NSString},
};
use objc::{class, msg_send, sel, sel_impl};
unsafe {
unsafe fn ns_string(string: &str) -> id {
NSString::alloc(nil).init_str(string).autorelease()
}
let url: id = msg_send![class!(NSURL), fileURLWithPath: ns_string(path.to_string_lossy().as_ref())];
let array: id = msg_send![class!(NSArray), arrayWithObject: url];
let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace];
let _: id = msg_send![workspace, recycleURLs: array completionHandler: nil];
}
Ok(())
}
#[cfg(target_os = "macos")]
async fn trash_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
self.trash_file(path, options).await
}
async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>> {
Ok(Box::new(std::fs::File::open(path)?))
}
@@ -714,6 +747,15 @@ impl FakeFs {
Ok(())
}
pub fn read_file_sync(&self, path: impl AsRef<Path>) -> Result<Vec<u8>> {
let path = path.as_ref();
let path = normalize_path(path);
let state = self.state.lock();
let entry = state.read_path(&path)?;
let entry = entry.lock();
entry.file_content(&path).cloned()
}
async fn load_internal(&self, path: impl AsRef<Path>) -> Result<Vec<u8>> {
let path = path.as_ref();
let path = normalize_path(path);

View File

@@ -8,6 +8,10 @@ use std::process::Command;
use std::os::windows::process::CommandExt;
pub fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result<HashMap<Oid, String>> {
if shas.is_empty() {
return Ok(HashMap::default());
}
const MARKER: &'static str = "<MARKER>";
let mut command = Command::new("git");

View File

@@ -3,7 +3,7 @@ use std::{ops::Range, sync::Arc};
use anyhow::Result;
use url::Url;
use util::{github, http::HttpClient};
use util::{codeberg, github, http::HttpClient};
use crate::Oid;
@@ -59,7 +59,7 @@ impl HostingProvider {
pub fn supports_avatars(&self) -> bool {
match self {
HostingProvider::Github => true,
HostingProvider::Github | HostingProvider::Codeberg => true,
_ => false,
}
}
@@ -71,24 +71,27 @@ impl HostingProvider {
commit: Oid,
client: Arc<dyn HttpClient>,
) -> Result<Option<Url>> {
match self {
Ok(match self {
HostingProvider::Github => {
let commit = commit.to_string();
let author =
github::fetch_github_commit_author(repo_owner, repo, &commit, &client).await?;
let url = if let Some(author) = author {
let mut url = Url::parse(&author.avatar_url)?;
url.set_query(Some("size=128"));
Some(url)
} else {
None
};
Ok(url)
github::fetch_github_commit_author(repo_owner, repo, &commit, &client)
.await?
.map(|author| -> Result<Url, url::ParseError> {
let mut url = Url::parse(&author.avatar_url)?;
url.set_query(Some("size=128"));
Ok(url)
})
.transpose()
}
HostingProvider::Codeberg => {
let commit = commit.to_string();
codeberg::fetch_codeberg_commit_author(repo_owner, repo, &commit, &client)
.await?
.map(|author| Url::parse(&author.avatar_url))
.transpose()
}
_ => Ok(None),
}
}?)
}
}

View File

@@ -14,7 +14,7 @@ workspace = true
default = []
test-support = ["backtrace", "collections/test-support", "util/test-support"]
runtime_shaders = []
macos-blade = ["blade-graphics", "blade-macros", "blade-rwh", "bytemuck"]
macos-blade = ["blade-graphics", "blade-macros", "bytemuck"]
[lib]
path = "src/gpui.rs"
@@ -26,7 +26,6 @@ async-task = "4.7"
backtrace = { version = "0.3", optional = true }
blade-graphics = { workspace = true, optional = true }
blade-macros = { workspace = true, optional = true }
blade-rwh = { workspace = true, optional = true }
bytemuck = { version = "1", optional = true }
collections.workspace = true
ctor.workspace = true
@@ -95,7 +94,6 @@ flume = "0.11"
#TODO: use these on all platforms
blade-graphics.workspace = true
blade-macros.workspace = true
blade-rwh.workspace = true
bytemuck = "1"
cosmic-text = "0.11.2"
copypasta = "0.10.1"
@@ -116,12 +114,21 @@ wayland-protocols = { version = "0.31.2", features = [
oo7 = "0.3.0"
open = "5.1.2"
filedescriptor = "0.8.2"
x11rb = { version = "0.13.0", features = ["allow-unsafe-code", "xkb", "randr"] }
x11rb = { version = "0.13.0", features = [
"allow-unsafe-code",
"xkb",
"randr",
"xinput",
"resource_manager",
] }
xkbcommon = { version = "0.7", features = ["wayland", "x11"] }
[target.'cfg(windows)'.dependencies]
windows.workspace = true
[target.'cfg(windows)'.build-dependencies]
embed-resource = "2.4"
[[example]]
name = "hello_world"
path = "examples/hello_world.rs"

View File

@@ -6,6 +6,15 @@
fn main() {
#[cfg(target_os = "macos")]
macos::build();
#[cfg(target_os = "windows")]
{
let manifest = std::path::Path::new("resources/windows/gpui.manifest.xml");
let rc_file = std::path::Path::new("resources/windows/gpui.rc");
println!("cargo:rerun-if-changed={}", manifest.display());
println!("cargo:rerun-if-changed={}", rc_file.display());
embed_resource::compile(rc_file, embed_resource::NONE);
}
}
#[cfg(target_os = "macos")]

View File

@@ -58,11 +58,36 @@ impl Render for ImageShowcase {
}
}
actions!(image, [Quit]);
fn main() {
env_logger::init();
App::new().run(|cx: &mut AppContext| {
cx.open_window(WindowOptions::default(), |cx| {
cx.activate(true);
cx.on_action(|_: &Quit, cx| cx.quit());
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
cx.set_menus(vec![Menu {
name: "Image",
items: vec![MenuItem::action("Quit", Quit)],
}]);
let window_options = WindowOptions {
titlebar: Some(TitlebarOptions {
title: Some(SharedString::from("Image Example")),
appears_transparent: false,
..Default::default()
}),
bounds: Some(Bounds {
size: size(px(1100.), px(600.)).into(),
origin: Point::new(DevicePixels::from(200), DevicePixels::from(200)),
}),
..Default::default()
};
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("examples/image/app-icon.png").unwrap()),

View File

@@ -54,6 +54,7 @@ fn main() {
kind: WindowKind::PopUp,
is_movable: false,
fullscreen: false,
app_id: None,
}
};

View File

@@ -0,0 +1,2 @@
#define RT_MANIFEST 24
1 RT_MANIFEST "resources/windows/gpui.manifest.xml"

View File

@@ -1,10 +1,11 @@
use crate::{
Action, AnyElement, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
AvailableSpace, BackgroundExecutor, BorrowAppContext, Bounds, ClipboardItem, Context, Empty,
Entity, EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke, Model, ModelContext,
Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
Pixels, Platform, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform, TestWindow,
TextSystem, View, ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions,
Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, AvailableSpace,
BackgroundExecutor, BorrowAppContext, Bounds, ClipboardItem, Context, DrawPhase, Drawable,
Element, Empty, Entity, EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke, Model,
ModelContext, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent,
MouseUpEvent, Pixels, Platform, Point, Render, Result, Size, Task, TestDispatcher,
TestPlatform, TestWindow, TextSystem, View, ViewContext, VisualContext, WindowContext,
WindowHandle, WindowOptions,
};
use anyhow::{anyhow, bail};
use futures::{channel::oneshot, Stream, StreamExt};
@@ -217,7 +218,7 @@ impl TestAppContext {
/// Adds a new window, and returns its root view and a `VisualTestContext` which can be used
/// as a `WindowContext` for the rest of the test. Typically you would shadow this context with
/// the returned one. `let (view, cx) = cx.add_window_view(...);`
pub fn add_window_view<F, V>(&mut self, build_window: F) -> (View<V>, &mut VisualTestContext)
pub fn add_window_view<F, V>(&mut self, build_root_view: F) -> (View<V>, &mut VisualTestContext)
where
F: FnOnce(&mut ViewContext<V>) -> V,
V: 'static + Render,
@@ -229,7 +230,7 @@ impl TestAppContext {
bounds: Some(bounds),
..Default::default()
},
|cx| cx.new_view(build_window),
|cx| cx.new_view(build_root_view),
);
drop(cx);
let view = window.root_view(self).unwrap();
@@ -725,21 +726,28 @@ impl VisualTestContext {
}
/// Draw an element to the window. Useful for simulating events or actions
pub fn draw(
pub fn draw<E>(
&mut self,
origin: Point<Pixels>,
space: Size<AvailableSpace>,
f: impl FnOnce(&mut WindowContext) -> AnyElement,
) {
space: impl Into<Size<AvailableSpace>>,
f: impl FnOnce(&mut WindowContext) -> E,
) -> (E::RequestLayoutState, E::PrepaintState)
where
E: Element,
{
self.update(|cx| {
cx.with_element_context(|cx| {
let mut element = f(cx);
element.layout_as_root(space, cx);
cx.with_absolute_element_offset(origin, |cx| element.prepaint(cx));
element.paint(cx);
});
cx.window.draw_phase = DrawPhase::Prepaint;
let mut element = Drawable::new(f(cx));
element.layout_as_root(space.into(), cx);
cx.with_absolute_element_offset(origin, |cx| element.prepaint(cx));
cx.window.draw_phase = DrawPhase::Paint;
let (request_layout_state, prepaint_state) = element.paint(cx);
cx.window.draw_phase = DrawPhase::None;
cx.refresh();
(request_layout_state, prepaint_state)
})
}

View File

@@ -20,7 +20,7 @@ pub trait AssetSource: 'static + Send + Sync {
impl AssetSource for () {
fn load(&self, path: &str) -> Result<Cow<'static, [u8]>> {
Err(anyhow!(
"get called on empty asset provider with \"{}\"",
"load called on empty asset provider with \"{}\"",
path
))
}

View File

@@ -32,12 +32,12 @@
//! your own custom layout algorithm or rendering a code editor.
use crate::{
util::FluentBuilder, ArenaBox, AvailableSpace, Bounds, DispatchNodeId, ElementContext,
ElementId, LayoutId, Pixels, Point, Size, ViewContext, WindowContext, ELEMENT_ARENA,
util::FluentBuilder, ArenaBox, AvailableSpace, Bounds, DispatchNodeId, ElementId, LayoutId,
Pixels, Point, Size, Style, ViewContext, WindowContext, ELEMENT_ARENA,
};
use derive_more::{Deref, DerefMut};
pub(crate) use smallvec::SmallVec;
use std::{any::Any, fmt::Debug, mem, ops::DerefMut};
use std::{any::Any, fmt::Debug, mem};
/// Implemented by types that participate in laying out and painting the contents of a window.
/// Elements form a tree and are laid out according to web-based layout rules, as implemented by Taffy.
@@ -52,27 +52,40 @@ pub trait Element: 'static + IntoElement {
/// provided to [`Element::paint`].
type PrepaintState: 'static;
/// If this element has a unique identifier, return it here. This is used to track elements across frames, and
/// will cause a GlobalElementId to be passed to the request_layout, prepaint, and paint methods.
///
/// The global id can in turn be used to access state that's connected to an element with the same id across
/// frames. This id must be unique among children of the first containing element with an id.
fn id(&self) -> Option<ElementId>;
/// Before an element can be painted, we need to know where it's going to be and how big it is.
/// Use this method to request a layout from Taffy and initialize the element's state.
fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState);
fn request_layout(
&mut self,
id: Option<&GlobalElementId>,
cx: &mut WindowContext,
) -> (LayoutId, Self::RequestLayoutState);
/// After laying out an element, we need to commit its bounds to the current frame for hitbox
/// purposes. The state argument is the same state that was returned from [`Element::request_layout()`].
fn prepaint(
&mut self,
id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
cx: &mut ElementContext,
cx: &mut WindowContext,
) -> Self::PrepaintState;
/// Once layout has been completed, this method will be called to paint the element to the screen.
/// The state argument is the same state that was returned from [`Element::request_layout()`].
fn paint(
&mut self,
id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
prepaint: &mut Self::PrepaintState,
cx: &mut ElementContext,
cx: &mut WindowContext,
);
/// Convert this element into a dynamically-typed [`AnyElement`].
@@ -164,27 +177,37 @@ impl<C: RenderOnce> Element for Component<C> {
type RequestLayoutState = AnyElement;
type PrepaintState = ();
fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) {
let mut element = self
.0
.take()
.unwrap()
.render(cx.deref_mut())
.into_any_element();
fn id(&self) -> Option<ElementId> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
cx: &mut WindowContext,
) -> (LayoutId, Self::RequestLayoutState) {
let mut element = self.0.take().unwrap().render(cx).into_any_element();
let layout_id = element.request_layout(cx);
(layout_id, element)
}
fn prepaint(&mut self, _: Bounds<Pixels>, element: &mut AnyElement, cx: &mut ElementContext) {
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_: Bounds<Pixels>,
element: &mut AnyElement,
cx: &mut WindowContext,
) {
element.prepaint(cx);
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_: Bounds<Pixels>,
element: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
cx: &mut ElementContext,
cx: &mut WindowContext,
) {
element.paint(cx)
}
@@ -199,22 +222,22 @@ impl<C: RenderOnce> IntoElement for Component<C> {
}
/// A globally unique identifier for an element, used to track state across frames.
#[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)]
pub(crate) struct GlobalElementId(SmallVec<[ElementId; 32]>);
#[derive(Deref, DerefMut, Default, Debug, Eq, PartialEq, Hash)]
pub struct GlobalElementId(pub(crate) SmallVec<[ElementId; 32]>);
trait ElementObject {
fn inner_element(&mut self) -> &mut dyn Any;
fn request_layout(&mut self, cx: &mut ElementContext) -> LayoutId;
fn request_layout(&mut self, cx: &mut WindowContext) -> LayoutId;
fn prepaint(&mut self, cx: &mut ElementContext);
fn prepaint(&mut self, cx: &mut WindowContext);
fn paint(&mut self, cx: &mut ElementContext);
fn paint(&mut self, cx: &mut WindowContext);
fn layout_as_root(
&mut self,
available_space: Size<AvailableSpace>,
cx: &mut ElementContext,
cx: &mut WindowContext,
) -> Size<Pixels>;
}
@@ -229,17 +252,20 @@ pub struct Drawable<E: Element> {
enum ElementDrawPhase<RequestLayoutState, PrepaintState> {
#[default]
Start,
RequestLayoutState {
RequestLayout {
layout_id: LayoutId,
global_id: Option<GlobalElementId>,
request_layout: RequestLayoutState,
},
LayoutComputed {
layout_id: LayoutId,
global_id: Option<GlobalElementId>,
available_space: Size<AvailableSpace>,
request_layout: RequestLayoutState,
},
PrepaintState {
Prepaint {
node_id: DispatchNodeId,
global_id: Option<GlobalElementId>,
bounds: Bounds<Pixels>,
request_layout: RequestLayoutState,
prepaint: PrepaintState,
@@ -249,19 +275,31 @@ enum ElementDrawPhase<RequestLayoutState, PrepaintState> {
/// A wrapper around an implementer of [`Element`] that allows it to be drawn in a window.
impl<E: Element> Drawable<E> {
fn new(element: E) -> Self {
pub(crate) fn new(element: E) -> Self {
Drawable {
element,
phase: ElementDrawPhase::Start,
}
}
fn request_layout(&mut self, cx: &mut ElementContext) -> LayoutId {
fn request_layout(&mut self, cx: &mut WindowContext) -> LayoutId {
match mem::take(&mut self.phase) {
ElementDrawPhase::Start => {
let (layout_id, request_layout) = self.element.request_layout(cx);
self.phase = ElementDrawPhase::RequestLayoutState {
let global_id = self.element.id().map(|element_id| {
cx.window.element_id_stack.push(element_id);
GlobalElementId(cx.window.element_id_stack.clone())
});
let (layout_id, request_layout) =
self.element.request_layout(global_id.as_ref(), cx);
if global_id.is_some() {
cx.window.element_id_stack.pop();
}
self.phase = ElementDrawPhase::RequestLayout {
layout_id,
global_id,
request_layout,
};
layout_id
@@ -270,68 +308,104 @@ impl<E: Element> Drawable<E> {
}
}
fn prepaint(&mut self, cx: &mut ElementContext) {
pub(crate) fn prepaint(&mut self, cx: &mut WindowContext) {
match mem::take(&mut self.phase) {
ElementDrawPhase::RequestLayoutState {
ElementDrawPhase::RequestLayout {
layout_id,
global_id,
mut request_layout,
}
| ElementDrawPhase::LayoutComputed {
layout_id,
global_id,
mut request_layout,
..
} => {
if let Some(element_id) = self.element.id() {
cx.window.element_id_stack.push(element_id);
debug_assert_eq!(global_id.as_ref().unwrap().0, cx.window.element_id_stack);
}
let bounds = cx.layout_bounds(layout_id);
let node_id = cx.window.next_frame.dispatch_tree.push_node();
let prepaint = self.element.prepaint(bounds, &mut request_layout, cx);
self.phase = ElementDrawPhase::PrepaintState {
let prepaint =
self.element
.prepaint(global_id.as_ref(), bounds, &mut request_layout, cx);
cx.window.next_frame.dispatch_tree.pop_node();
if global_id.is_some() {
cx.window.element_id_stack.pop();
}
self.phase = ElementDrawPhase::Prepaint {
node_id,
global_id,
bounds,
request_layout,
prepaint,
};
cx.window.next_frame.dispatch_tree.pop_node();
}
_ => panic!("must call request_layout before prepaint"),
}
}
fn paint(&mut self, cx: &mut ElementContext) -> E::RequestLayoutState {
pub(crate) fn paint(
&mut self,
cx: &mut WindowContext,
) -> (E::RequestLayoutState, E::PrepaintState) {
match mem::take(&mut self.phase) {
ElementDrawPhase::PrepaintState {
ElementDrawPhase::Prepaint {
node_id,
global_id,
bounds,
mut request_layout,
mut prepaint,
..
} => {
if let Some(element_id) = self.element.id() {
cx.window.element_id_stack.push(element_id);
debug_assert_eq!(global_id.as_ref().unwrap().0, cx.window.element_id_stack);
}
cx.window.next_frame.dispatch_tree.set_active_node(node_id);
self.element
.paint(bounds, &mut request_layout, &mut prepaint, cx);
self.element.paint(
global_id.as_ref(),
bounds,
&mut request_layout,
&mut prepaint,
cx,
);
if global_id.is_some() {
cx.window.element_id_stack.pop();
}
self.phase = ElementDrawPhase::Painted;
request_layout
(request_layout, prepaint)
}
_ => panic!("must call prepaint before paint"),
}
}
fn layout_as_root(
pub(crate) fn layout_as_root(
&mut self,
available_space: Size<AvailableSpace>,
cx: &mut ElementContext,
cx: &mut WindowContext,
) -> Size<Pixels> {
if matches!(&self.phase, ElementDrawPhase::Start) {
self.request_layout(cx);
}
let layout_id = match mem::take(&mut self.phase) {
ElementDrawPhase::RequestLayoutState {
ElementDrawPhase::RequestLayout {
layout_id,
global_id,
request_layout,
} => {
cx.compute_layout(layout_id, available_space);
self.phase = ElementDrawPhase::LayoutComputed {
layout_id,
global_id,
available_space,
request_layout,
};
@@ -339,6 +413,7 @@ impl<E: Element> Drawable<E> {
}
ElementDrawPhase::LayoutComputed {
layout_id,
global_id,
available_space: prev_available_space,
request_layout,
} => {
@@ -347,6 +422,7 @@ impl<E: Element> Drawable<E> {
}
self.phase = ElementDrawPhase::LayoutComputed {
layout_id,
global_id,
available_space,
request_layout,
};
@@ -368,22 +444,22 @@ where
&mut self.element
}
fn request_layout(&mut self, cx: &mut ElementContext) -> LayoutId {
fn request_layout(&mut self, cx: &mut WindowContext) -> LayoutId {
Drawable::request_layout(self, cx)
}
fn prepaint(&mut self, cx: &mut ElementContext) {
fn prepaint(&mut self, cx: &mut WindowContext) {
Drawable::prepaint(self, cx);
}
fn paint(&mut self, cx: &mut ElementContext) {
fn paint(&mut self, cx: &mut WindowContext) {
Drawable::paint(self, cx);
}
fn layout_as_root(
&mut self,
available_space: Size<AvailableSpace>,
cx: &mut ElementContext,
cx: &mut WindowContext,
) -> Size<Pixels> {
Drawable::layout_as_root(self, available_space, cx)
}
@@ -411,18 +487,18 @@ impl AnyElement {
/// Request the layout ID of the element stored in this `AnyElement`.
/// Used for laying out child elements in a parent element.
pub fn request_layout(&mut self, cx: &mut ElementContext) -> LayoutId {
pub fn request_layout(&mut self, cx: &mut WindowContext) -> LayoutId {
self.0.request_layout(cx)
}
/// Prepares the element to be painted by storing its bounds, giving it a chance to draw hitboxes and
/// request autoscroll before the final paint pass is confirmed.
pub fn prepaint(&mut self, cx: &mut ElementContext) {
pub fn prepaint(&mut self, cx: &mut WindowContext) {
self.0.prepaint(cx)
}
/// Paints the element stored in this `AnyElement`.
pub fn paint(&mut self, cx: &mut ElementContext) {
pub fn paint(&mut self, cx: &mut WindowContext) {
self.0.paint(cx)
}
@@ -430,13 +506,13 @@ impl AnyElement {
pub fn layout_as_root(
&mut self,
available_space: Size<AvailableSpace>,
cx: &mut ElementContext,
cx: &mut WindowContext,
) -> Size<Pixels> {
self.0.layout_as_root(available_space, cx)
}
/// Prepaints this element at the given absolute origin.
pub fn prepaint_at(&mut self, origin: Point<Pixels>, cx: &mut ElementContext) {
pub fn prepaint_at(&mut self, origin: Point<Pixels>, cx: &mut WindowContext) {
cx.with_absolute_element_offset(origin, |cx| self.0.prepaint(cx));
}
@@ -445,7 +521,7 @@ impl AnyElement {
&mut self,
origin: Point<Pixels>,
available_space: Size<AvailableSpace>,
cx: &mut ElementContext,
cx: &mut WindowContext,
) {
self.layout_as_root(available_space, cx);
cx.with_absolute_element_offset(origin, |cx| self.0.prepaint(cx));
@@ -456,26 +532,36 @@ impl Element for AnyElement {
type RequestLayoutState = ();
type PrepaintState = ();
fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) {
fn id(&self) -> Option<ElementId> {
None
}
fn request_layout(
&mut self,
_: Option<&GlobalElementId>,
cx: &mut WindowContext,
) -> (LayoutId, Self::RequestLayoutState) {
let layout_id = self.request_layout(cx);
(layout_id, ())
}
fn prepaint(
&mut self,
_: Option<&GlobalElementId>,
_: Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
cx: &mut ElementContext,
cx: &mut WindowContext,
) {
self.prepaint(cx)
}
fn paint(
&mut self,
_: Option<&GlobalElementId>,
_: Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
cx: &mut ElementContext,
cx: &mut WindowContext,
) {
self.paint(cx)
}
@@ -508,24 +594,34 @@ impl Element for Empty {
type RequestLayoutState = ();
type PrepaintState = ();
fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) {
(cx.request_layout(&crate::Style::default(), None), ())
fn id(&self) -> Option<ElementId> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
cx: &mut WindowContext,
) -> (LayoutId, Self::RequestLayoutState) {
(cx.request_layout(&Style::default(), None), ())
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_bounds: Bounds<Pixels>,
_state: &mut Self::RequestLayoutState,
_cx: &mut ElementContext,
_cx: &mut WindowContext,
) {
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
_prepaint: &mut Self::PrepaintState,
_cx: &mut ElementContext,
_cx: &mut WindowContext,
) {
}
}

View File

@@ -2,8 +2,8 @@ use smallvec::SmallVec;
use taffy::style::{Display, Position};
use crate::{
point, AnyElement, Bounds, Element, ElementContext, IntoElement, LayoutId, ParentElement,
Pixels, Point, Size, Style,
point, AnyElement, Bounds, Element, GlobalElementId, IntoElement, LayoutId, ParentElement,
Pixels, Point, Size, Style, WindowContext,
};
/// The state that the anchored element element uses to track its children.
@@ -72,9 +72,14 @@ impl Element for Anchored {
type RequestLayoutState = AnchoredState;
type PrepaintState = ();
fn id(&self) -> Option<crate::ElementId> {
None
}
fn request_layout(
&mut self,
cx: &mut ElementContext,
_id: Option<&GlobalElementId>,
cx: &mut WindowContext,
) -> (crate::LayoutId, Self::RequestLayoutState) {
let child_layout_ids = self
.children
@@ -95,9 +100,10 @@ impl Element for Anchored {
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
cx: &mut ElementContext,
cx: &mut WindowContext,
) {
if request_layout.child_layout_ids.is_empty() {
return;
@@ -177,10 +183,11 @@ impl Element for Anchored {
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_bounds: crate::Bounds<crate::Pixels>,
_request_layout: &mut Self::RequestLayoutState,
_prepaint: &mut Self::PrepaintState,
cx: &mut ElementContext,
cx: &mut WindowContext,
) {
for child in &mut self.children {
child.paint(cx);

View File

@@ -1,6 +1,6 @@
use std::time::{Duration, Instant};
use crate::{AnyElement, Element, ElementId, IntoElement};
use crate::{AnyElement, Element, ElementId, GlobalElementId, IntoElement};
pub use easing::*;
@@ -86,15 +86,19 @@ struct AnimationState {
impl<E: IntoElement + 'static> Element for AnimationElement<E> {
type RequestLayoutState = AnyElement;
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn request_layout(
&mut self,
cx: &mut crate::ElementContext,
global_id: Option<&GlobalElementId>,
cx: &mut crate::WindowContext,
) -> (crate::LayoutId, Self::RequestLayoutState) {
cx.with_element_state(Some(self.id.clone()), |state, cx| {
let state = state.unwrap().unwrap_or_else(|| AnimationState {
cx.with_element_state(global_id.unwrap(), |state, cx| {
let state = state.unwrap_or_else(|| AnimationState {
start: Instant::now(),
});
let mut delta =
@@ -130,25 +134,27 @@ impl<E: IntoElement + 'static> Element for AnimationElement<E> {
})
}
((element.request_layout(cx), element), Some(state))
((element.request_layout(cx), element), state)
})
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_bounds: crate::Bounds<crate::Pixels>,
element: &mut Self::RequestLayoutState,
cx: &mut crate::ElementContext,
cx: &mut crate::WindowContext,
) -> Self::PrepaintState {
element.prepaint(cx);
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_bounds: crate::Bounds<crate::Pixels>,
element: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
cx: &mut crate::ElementContext,
cx: &mut crate::WindowContext,
) {
element.paint(cx);
}

View File

@@ -1,12 +1,15 @@
use refineable::Refineable as _;
use crate::{Bounds, Element, ElementContext, IntoElement, Pixels, Style, StyleRefinement, Styled};
use crate::{
Bounds, Element, ElementId, GlobalElementId, IntoElement, Pixels, Style, StyleRefinement,
Styled, WindowContext,
};
/// Construct a canvas element with the given paint callback.
/// Useful for adding short term custom drawing to a view.
pub fn canvas<T>(
prepaint: impl 'static + FnOnce(Bounds<Pixels>, &mut ElementContext) -> T,
paint: impl 'static + FnOnce(Bounds<Pixels>, T, &mut ElementContext),
prepaint: impl 'static + FnOnce(Bounds<Pixels>, &mut WindowContext) -> T,
paint: impl 'static + FnOnce(Bounds<Pixels>, T, &mut WindowContext),
) -> Canvas<T> {
Canvas {
prepaint: Some(Box::new(prepaint)),
@@ -18,8 +21,8 @@ pub fn canvas<T>(
/// A canvas element, meant for accessing the low level paint API without defining a whole
/// custom element
pub struct Canvas<T> {
prepaint: Option<Box<dyn FnOnce(Bounds<Pixels>, &mut ElementContext) -> T>>,
paint: Option<Box<dyn FnOnce(Bounds<Pixels>, T, &mut ElementContext)>>,
prepaint: Option<Box<dyn FnOnce(Bounds<Pixels>, &mut WindowContext) -> T>>,
paint: Option<Box<dyn FnOnce(Bounds<Pixels>, T, &mut WindowContext)>>,
style: StyleRefinement,
}
@@ -35,9 +38,14 @@ impl<T: 'static> Element for Canvas<T> {
type RequestLayoutState = Style;
type PrepaintState = Option<T>;
fn id(&self) -> Option<ElementId> {
None
}
fn request_layout(
&mut self,
cx: &mut ElementContext,
_id: Option<&GlobalElementId>,
cx: &mut WindowContext,
) -> (crate::LayoutId, Self::RequestLayoutState) {
let mut style = Style::default();
style.refine(&self.style);
@@ -47,19 +55,21 @@ impl<T: 'static> Element for Canvas<T> {
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
_request_layout: &mut Style,
cx: &mut ElementContext,
cx: &mut WindowContext,
) -> Option<T> {
Some(self.prepaint.take().unwrap()(bounds, cx))
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
style: &mut Style,
prepaint: &mut Self::PrepaintState,
cx: &mut ElementContext,
cx: &mut WindowContext,
) {
let prepaint = prepaint.take().unwrap();
style.paint(bounds, cx, |cx| {

View File

@@ -1,4 +1,6 @@
use crate::{AnyElement, Bounds, Element, ElementContext, IntoElement, LayoutId, Pixels};
use crate::{
AnyElement, Bounds, Element, GlobalElementId, IntoElement, LayoutId, Pixels, WindowContext,
};
/// Builds a `Deferred` element, which delays the layout and paint of its child.
pub fn deferred(child: impl IntoElement) -> Deferred {
@@ -29,16 +31,25 @@ impl Element for Deferred {
type RequestLayoutState = ();
type PrepaintState = ();
fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, ()) {
fn id(&self) -> Option<crate::ElementId> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
cx: &mut WindowContext,
) -> (LayoutId, ()) {
let layout_id = self.child.as_mut().unwrap().request_layout(cx);
(layout_id, ())
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
cx: &mut ElementContext,
cx: &mut WindowContext,
) {
let child = self.child.take().unwrap();
let element_offset = cx.element_offset();
@@ -47,10 +58,11 @@ impl Element for Deferred {
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
_prepaint: &mut Self::PrepaintState,
_cx: &mut ElementContext,
_cx: &mut WindowContext,
) {
}
}

View File

@@ -17,7 +17,7 @@
use crate::{
point, px, size, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, AppContext, Bounds,
ClickEvent, DispatchPhase, Element, ElementContext, ElementId, FocusHandle, Global, Hitbox,
ClickEvent, DispatchPhase, Element, ElementId, FocusHandle, Global, GlobalElementId, Hitbox,
HitboxId, IntoElement, IsZero, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId,
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
ParentElement, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style,
@@ -1123,26 +1123,37 @@ impl Element for Div {
type RequestLayoutState = DivFrameState;
type PrepaintState = Option<Hitbox>;
fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) {
fn id(&self) -> Option<ElementId> {
self.interactivity.element_id.clone()
}
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
cx: &mut WindowContext,
) -> (LayoutId, Self::RequestLayoutState) {
let mut child_layout_ids = SmallVec::new();
let layout_id = self.interactivity.request_layout(cx, |style, cx| {
cx.with_text_style(style.text_style().cloned(), |cx| {
child_layout_ids = self
.children
.iter_mut()
.map(|child| child.request_layout(cx))
.collect::<SmallVec<_>>();
cx.request_layout(&style, child_layout_ids.iter().copied())
})
});
let layout_id = self
.interactivity
.request_layout(global_id, cx, |style, cx| {
cx.with_text_style(style.text_style().cloned(), |cx| {
child_layout_ids = self
.children
.iter_mut()
.map(|child| child.request_layout(cx))
.collect::<SmallVec<_>>();
cx.request_layout(&style, child_layout_ids.iter().copied())
})
});
(layout_id, DivFrameState { child_layout_ids })
}
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
cx: &mut ElementContext,
cx: &mut WindowContext,
) -> Option<Hitbox> {
let mut child_min = point(Pixels::MAX, Pixels::MAX);
let mut child_max = Point::default();
@@ -1178,6 +1189,7 @@ impl Element for Div {
};
self.interactivity.prepaint(
global_id,
bounds,
content_size,
cx,
@@ -1194,13 +1206,14 @@ impl Element for Div {
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
hitbox: &mut Option<Hitbox>,
cx: &mut ElementContext,
cx: &mut WindowContext,
) {
self.interactivity
.paint(bounds, hitbox.as_ref(), cx, |_style, cx| {
.paint(global_id, bounds, hitbox.as_ref(), cx, |_style, cx| {
for child in &mut self.children {
child.paint(cx);
}
@@ -1220,7 +1233,7 @@ impl IntoElement for Div {
/// interactivity in the `Div` element.
#[derive(Default)]
pub struct Interactivity {
/// The element ID of the element
/// The element ID of the element. In id is required to support a stateful subset of the interactivity such as on_click.
pub element_id: Option<ElementId>,
/// Whether the element was clicked. This will only be present after layout.
pub active: Option<bool>,
@@ -1276,11 +1289,12 @@ impl Interactivity {
/// Layout this element according to this interactivity state's configured styles
pub fn request_layout(
&mut self,
cx: &mut ElementContext,
f: impl FnOnce(Style, &mut ElementContext) -> LayoutId,
global_id: Option<&GlobalElementId>,
cx: &mut WindowContext,
f: impl FnOnce(Style, &mut WindowContext) -> LayoutId,
) -> LayoutId {
cx.with_element_state::<InteractiveElementState, _>(
self.element_id.clone(),
cx.with_optional_element_state::<InteractiveElementState, _>(
global_id,
|element_state, cx| {
let mut element_state =
element_state.map(|element_state| element_state.unwrap_or_default());
@@ -1339,14 +1353,15 @@ impl Interactivity {
/// Commit the bounds of this element according to this interactivity state's configured styles.
pub fn prepaint<R>(
&mut self,
global_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
content_size: Size<Pixels>,
cx: &mut ElementContext,
f: impl FnOnce(&Style, Point<Pixels>, Option<Hitbox>, &mut ElementContext) -> R,
cx: &mut WindowContext,
f: impl FnOnce(&Style, Point<Pixels>, Option<Hitbox>, &mut WindowContext) -> R,
) -> R {
self.content_size = content_size;
cx.with_element_state::<InteractiveElementState, _>(
self.element_id.clone(),
cx.with_optional_element_state::<InteractiveElementState, _>(
global_id,
|element_state, cx| {
let mut element_state =
element_state.map(|element_state| element_state.unwrap_or_default());
@@ -1406,7 +1421,7 @@ impl Interactivity {
&mut self,
bounds: Bounds<Pixels>,
style: &Style,
cx: &mut ElementContext,
cx: &mut WindowContext,
) -> Point<Pixels> {
if let Some(scroll_offset) = self.scroll_offset.as_ref() {
if let Some(scroll_handle) = &self.tracked_scroll_handle {
@@ -1454,14 +1469,15 @@ impl Interactivity {
/// with the current scroll offset
pub fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
hitbox: Option<&Hitbox>,
cx: &mut ElementContext,
f: impl FnOnce(&Style, &mut ElementContext),
cx: &mut WindowContext,
f: impl FnOnce(&Style, &mut WindowContext),
) {
self.hovered = hitbox.map(|hitbox| hitbox.is_hovered(cx));
cx.with_element_state::<InteractiveElementState, _>(
self.element_id.clone(),
cx.with_optional_element_state::<InteractiveElementState, _>(
global_id,
|element_state, cx| {
let mut element_state =
element_state.map(|element_state| element_state.unwrap_or_default());
@@ -1482,12 +1498,12 @@ impl Interactivity {
return ((), element_state);
}
style.paint(bounds, cx, |cx: &mut ElementContext| {
style.paint(bounds, cx, |cx: &mut WindowContext| {
cx.with_text_style(style.text_style().cloned(), |cx| {
cx.with_content_mask(style.overflow_mask(bounds, cx.rem_size()), |cx| {
if let Some(hitbox) = hitbox {
#[cfg(debug_assertions)]
self.paint_debug_info(hitbox, &style, cx);
self.paint_debug_info(global_id, hitbox, &style, cx);
if !cx.has_active_drag() {
if let Some(mouse_cursor) = style.mouse_cursor {
@@ -1521,16 +1537,22 @@ impl Interactivity {
}
#[cfg(debug_assertions)]
fn paint_debug_info(&mut self, hitbox: &Hitbox, style: &Style, cx: &mut ElementContext) {
if self.element_id.is_some()
fn paint_debug_info(
&mut self,
global_id: Option<&GlobalElementId>,
hitbox: &Hitbox,
style: &Style,
cx: &mut WindowContext,
) {
if global_id.is_some()
&& (style.debug || style.debug_below || cx.has_global::<crate::DebugBelow>())
&& hitbox.is_hovered(cx)
{
const FONT_SIZE: crate::Pixels = crate::Pixels(10.);
let element_id = format!("{:?}", self.element_id.as_ref().unwrap());
let element_id = format!("{:?}", global_id.unwrap());
let str_len = element_id.len();
let render_debug_text = |cx: &mut ElementContext| {
let render_debug_text = |cx: &mut WindowContext| {
if let Some(text) = cx
.text_system()
.shape_text(
@@ -1629,7 +1651,7 @@ impl Interactivity {
&mut self,
hitbox: &Hitbox,
element_state: Option<&mut InteractiveElementState>,
cx: &mut ElementContext,
cx: &mut WindowContext,
) {
// If this element can be focused, register a mouse down listener
// that will automatically transfer focus when hitting the element.
@@ -1712,11 +1734,11 @@ impl Interactivity {
let mut can_drop = true;
if let Some(predicate) = &can_drop_predicate {
can_drop = predicate(drag.value.as_ref(), cx.deref_mut());
can_drop = predicate(drag.value.as_ref(), cx);
}
if can_drop {
listener(drag.value.as_ref(), cx.deref_mut());
listener(drag.value.as_ref(), cx);
cx.refresh();
cx.stop_propagation();
}
@@ -1840,7 +1862,7 @@ impl Interactivity {
*was_hovered = is_hovered;
drop(was_hovered);
hover_listener(&is_hovered, cx.deref_mut());
hover_listener(&is_hovered, cx);
}
});
}
@@ -1969,7 +1991,7 @@ impl Interactivity {
}
}
fn paint_keyboard_listeners(&mut self, cx: &mut ElementContext) {
fn paint_keyboard_listeners(&mut self, cx: &mut WindowContext) {
let key_down_listeners = mem::take(&mut self.key_down_listeners);
let key_up_listeners = mem::take(&mut self.key_up_listeners);
let modifiers_changed_listeners = mem::take(&mut self.modifiers_changed_listeners);
@@ -2004,7 +2026,7 @@ impl Interactivity {
}
}
fn paint_hover_group_handler(&self, cx: &mut ElementContext) {
fn paint_hover_group_handler(&self, cx: &mut WindowContext) {
let group_hitbox = self
.group_hover_style
.as_ref()
@@ -2021,7 +2043,7 @@ impl Interactivity {
}
}
fn paint_scroll_listener(&self, hitbox: &Hitbox, style: &Style, cx: &mut ElementContext) {
fn paint_scroll_listener(&self, hitbox: &Hitbox, style: &Style, cx: &mut WindowContext) {
if let Some(scroll_offset) = self.scroll_offset.clone() {
let overflow = style.overflow;
let line_height = cx.line_height();
@@ -2064,8 +2086,13 @@ impl Interactivity {
}
/// Compute the visual style for this element, based on the current bounds and the element's state.
pub fn compute_style(&self, hitbox: Option<&Hitbox>, cx: &mut ElementContext) -> Style {
cx.with_element_state(self.element_id.clone(), |element_state, cx| {
pub fn compute_style(
&self,
global_id: Option<&GlobalElementId>,
hitbox: Option<&Hitbox>,
cx: &mut WindowContext,
) -> Style {
cx.with_optional_element_state(global_id, |element_state, cx| {
let mut element_state =
element_state.map(|element_state| element_state.unwrap_or_default());
let style = self.compute_style_internal(hitbox, element_state.as_mut(), cx);
@@ -2078,7 +2105,7 @@ impl Interactivity {
&self,
hitbox: Option<&Hitbox>,
element_state: Option<&mut InteractiveElementState>,
cx: &mut ElementContext,
cx: &mut WindowContext,
) -> Style {
let mut style = Style::default();
style.refine(&self.base_style);
@@ -2119,7 +2146,7 @@ impl Interactivity {
if let Some(drag) = cx.active_drag.take() {
let mut can_drop = true;
if let Some(can_drop_predicate) = &self.can_drop_predicate {
can_drop = can_drop_predicate(drag.value.as_ref(), cx.deref_mut());
can_drop = can_drop_predicate(drag.value.as_ref(), cx);
}
if can_drop {
@@ -2264,27 +2291,37 @@ where
type RequestLayoutState = E::RequestLayoutState;
type PrepaintState = E::PrepaintState;
fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) {
self.element.request_layout(cx)
fn id(&self) -> Option<ElementId> {
self.element.id()
}
fn request_layout(
&mut self,
id: Option<&GlobalElementId>,
cx: &mut WindowContext,
) -> (LayoutId, Self::RequestLayoutState) {
self.element.request_layout(id, cx)
}
fn prepaint(
&mut self,
id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
state: &mut Self::RequestLayoutState,
cx: &mut ElementContext,
cx: &mut WindowContext,
) -> E::PrepaintState {
self.element.prepaint(bounds, state, cx)
self.element.prepaint(id, bounds, state, cx)
}
fn paint(
&mut self,
id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
prepaint: &mut Self::PrepaintState,
cx: &mut ElementContext,
cx: &mut WindowContext,
) {
self.element.paint(bounds, request_layout, prepaint, cx)
self.element.paint(id, bounds, request_layout, prepaint, cx)
}
}
@@ -2347,27 +2384,37 @@ where
type RequestLayoutState = E::RequestLayoutState;
type PrepaintState = E::PrepaintState;
fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) {
self.element.request_layout(cx)
fn id(&self) -> Option<ElementId> {
self.element.id()
}
fn request_layout(
&mut self,
id: Option<&GlobalElementId>,
cx: &mut WindowContext,
) -> (LayoutId, Self::RequestLayoutState) {
self.element.request_layout(id, cx)
}
fn prepaint(
&mut self,
id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
state: &mut Self::RequestLayoutState,
cx: &mut ElementContext,
cx: &mut WindowContext,
) -> E::PrepaintState {
self.element.prepaint(bounds, state, cx)
self.element.prepaint(id, bounds, state, cx)
}
fn paint(
&mut self,
id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
prepaint: &mut Self::PrepaintState,
cx: &mut ElementContext,
cx: &mut WindowContext,
) {
self.element.paint(bounds, request_layout, prepaint, cx);
self.element.paint(id, bounds, request_layout, prepaint, cx);
}
}

View File

@@ -4,8 +4,9 @@ use std::sync::Arc;
use crate::{
point, px, size, AbsoluteLength, Asset, Bounds, DefiniteLength, DevicePixels, Element,
ElementContext, Hitbox, ImageData, InteractiveElement, Interactivity, IntoElement, LayoutId,
Length, Pixels, SharedUri, Size, StyleRefinement, Styled, SvgSize, UriOrPath, WindowContext,
ElementId, GlobalElementId, Hitbox, ImageData, InteractiveElement, Interactivity, IntoElement,
LayoutId, Length, Pixels, SharedUri, Size, StyleRefinement, Styled, SvgSize, UriOrPath,
WindowContext,
};
use futures::{AsyncReadExt, Future};
use image::{ImageBuffer, ImageError};
@@ -232,50 +233,62 @@ impl Element for Img {
type RequestLayoutState = ();
type PrepaintState = Option<Hitbox>;
fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) {
let layout_id = self.interactivity.request_layout(cx, |mut style, cx| {
if let Some(data) = self.source.data(cx) {
let image_size = data.size();
match (style.size.width, style.size.height) {
(Length::Auto, Length::Auto) => {
style.size = Size {
width: Length::Definite(DefiniteLength::Absolute(
AbsoluteLength::Pixels(px(image_size.width.0 as f32)),
)),
height: Length::Definite(DefiniteLength::Absolute(
AbsoluteLength::Pixels(px(image_size.height.0 as f32)),
)),
}
}
_ => {}
}
}
fn id(&self) -> Option<ElementId> {
self.interactivity.element_id.clone()
}
cx.request_layout(&style, [])
});
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
cx: &mut WindowContext,
) -> (LayoutId, Self::RequestLayoutState) {
let layout_id = self
.interactivity
.request_layout(global_id, cx, |mut style, cx| {
if let Some(data) = self.source.data(cx) {
let image_size = data.size();
match (style.size.width, style.size.height) {
(Length::Auto, Length::Auto) => {
style.size = Size {
width: Length::Definite(DefiniteLength::Absolute(
AbsoluteLength::Pixels(px(image_size.width.0 as f32)),
)),
height: Length::Definite(DefiniteLength::Absolute(
AbsoluteLength::Pixels(px(image_size.height.0 as f32)),
)),
}
}
_ => {}
}
}
cx.request_layout(&style, [])
});
(layout_id, ())
}
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
cx: &mut ElementContext,
cx: &mut WindowContext,
) -> Option<Hitbox> {
self.interactivity
.prepaint(bounds, bounds.size, cx, |_, _, hitbox, _| hitbox)
.prepaint(global_id, bounds, bounds.size, cx, |_, _, hitbox, _| hitbox)
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
hitbox: &mut Self::PrepaintState,
cx: &mut ElementContext,
cx: &mut WindowContext,
) {
let source = self.source.clone();
self.interactivity
.paint(bounds, hitbox.as_ref(), cx, |style, cx| {
.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.data(cx) {
@@ -319,7 +332,7 @@ impl InteractiveElement for Img {
}
impl ImageSource {
fn data(&self, cx: &mut ElementContext) -> Option<Arc<ImageData>> {
fn data(&self, cx: &mut WindowContext) -> Option<Arc<ImageData>> {
match self {
ImageSource::Uri(_) | ImageSource::File(_) => {
let uri_or_path: UriOrPath = match self {

View File

@@ -8,7 +8,7 @@
use crate::{
point, px, size, AnyElement, AvailableSpace, Bounds, ContentMask, DispatchPhase, Edges,
Element, ElementContext, FocusHandle, Hitbox, IntoElement, Pixels, Point, ScrollWheelEvent,
Element, FocusHandle, GlobalElementId, Hitbox, IntoElement, Pixels, Point, ScrollWheelEvent,
Size, Style, StyleRefinement, Styled, WindowContext,
};
use collections::VecDeque;
@@ -434,7 +434,7 @@ impl StateInner {
available_width: Option<Pixels>,
available_height: Pixels,
padding: &Edges<Pixels>,
cx: &mut ElementContext,
cx: &mut WindowContext,
) -> LayoutItemsResponse {
let old_items = self.items.clone();
let mut measured_items = VecDeque::new();
@@ -609,7 +609,7 @@ impl StateInner {
bounds: Bounds<Pixels>,
padding: Edges<Pixels>,
autoscroll: bool,
cx: &mut ElementContext,
cx: &mut WindowContext,
) -> Result<LayoutItemsResponse, ListOffset> {
cx.transact(|cx| {
let mut layout_response =
@@ -704,9 +704,14 @@ impl Element for List {
type RequestLayoutState = ();
type PrepaintState = ListPrepaintState;
fn id(&self) -> Option<crate::ElementId> {
None
}
fn request_layout(
&mut self,
cx: &mut crate::ElementContext,
_id: Option<&GlobalElementId>,
cx: &mut crate::WindowContext,
) -> (crate::LayoutId, Self::RequestLayoutState) {
let layout_id = match self.sizing_behavior {
ListSizingBehavior::Infer => {
@@ -770,9 +775,10 @@ impl Element for List {
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
cx: &mut ElementContext,
cx: &mut WindowContext,
) -> ListPrepaintState {
let state = &mut *self.state.0.borrow_mut();
state.reset = false;
@@ -812,10 +818,11 @@ impl Element for List {
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
bounds: Bounds<crate::Pixels>,
_: &mut Self::RequestLayoutState,
prepaint: &mut Self::PrepaintState,
cx: &mut crate::ElementContext,
cx: &mut crate::WindowContext,
) {
cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
for item in &mut prepaint.layout.item_layouts {
@@ -951,11 +958,9 @@ mod test {
});
// Paint
cx.draw(
point(px(0.), px(0.)),
size(px(100.), px(20.)).into(),
|_| list(state.clone()).w_full().h_full().into_any(),
);
cx.draw(point(px(0.), px(0.)), size(px(100.), px(20.)), |_| {
list(state.clone()).w_full().h_full()
});
// Reset
state.reset(5);

View File

@@ -1,7 +1,7 @@
use crate::{
geometry::Negate as _, point, px, radians, size, Bounds, Element, ElementContext, Hitbox,
geometry::Negate as _, point, px, radians, size, Bounds, Element, GlobalElementId, Hitbox,
InteractiveElement, Interactivity, IntoElement, LayoutId, Pixels, Point, Radians, SharedString,
Size, StyleRefinement, Styled, TransformationMatrix,
Size, StyleRefinement, Styled, TransformationMatrix, WindowContext,
};
use util::ResultExt;
@@ -40,34 +40,44 @@ impl Element for Svg {
type RequestLayoutState = ();
type PrepaintState = Option<Hitbox>;
fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) {
fn id(&self) -> Option<crate::ElementId> {
self.interactivity.element_id.clone()
}
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
cx: &mut WindowContext,
) -> (LayoutId, Self::RequestLayoutState) {
let layout_id = self
.interactivity
.request_layout(cx, |style, cx| cx.request_layout(&style, None));
.request_layout(global_id, cx, |style, cx| cx.request_layout(&style, None));
(layout_id, ())
}
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
cx: &mut ElementContext,
cx: &mut WindowContext,
) -> Option<Hitbox> {
self.interactivity
.prepaint(bounds, bounds.size, cx, |_, _, hitbox, _| hitbox)
.prepaint(global_id, bounds, bounds.size, cx, |_, _, hitbox, _| hitbox)
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
hitbox: &mut Option<Hitbox>,
cx: &mut ElementContext,
cx: &mut WindowContext,
) where
Self: Sized,
{
self.interactivity
.paint(bounds, hitbox.as_ref(), cx, |style, cx| {
.paint(global_id, bounds, hitbox.as_ref(), cx, |style, cx| {
if let Some((path, color)) = self.path.as_ref().zip(style.text.color) {
let transformation = self
.transformation

View File

@@ -1,5 +1,5 @@
use crate::{
ActiveTooltip, AnyTooltip, AnyView, Bounds, DispatchPhase, Element, ElementContext, ElementId,
ActiveTooltip, AnyTooltip, AnyView, Bounds, DispatchPhase, Element, ElementId, GlobalElementId,
HighlightStyle, Hitbox, IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
Pixels, Point, SharedString, Size, TextRun, TextStyle, WhiteSpace, WindowContext, WrappedLine,
TOOLTIP_DELAY,
@@ -20,7 +20,15 @@ impl Element for &'static str {
type RequestLayoutState = TextState;
type PrepaintState = ();
fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) {
fn id(&self) -> Option<ElementId> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
cx: &mut WindowContext,
) -> (LayoutId, Self::RequestLayoutState) {
let mut state = TextState::default();
let layout_id = state.layout(SharedString::from(*self), None, cx);
(layout_id, state)
@@ -28,18 +36,20 @@ impl Element for &'static str {
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_bounds: Bounds<Pixels>,
_text_state: &mut Self::RequestLayoutState,
_cx: &mut ElementContext,
_cx: &mut WindowContext,
) {
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
text_state: &mut TextState,
_: &mut (),
cx: &mut ElementContext,
cx: &mut WindowContext,
) {
text_state.paint(bounds, self, cx)
}
@@ -65,7 +75,17 @@ impl Element for SharedString {
type RequestLayoutState = TextState;
type PrepaintState = ();
fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) {
fn id(&self) -> Option<ElementId> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
cx: &mut WindowContext,
) -> (LayoutId, Self::RequestLayoutState) {
let mut state = TextState::default();
let layout_id = state.layout(self.clone(), None, cx);
(layout_id, state)
@@ -73,18 +93,20 @@ impl Element for SharedString {
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_bounds: Bounds<Pixels>,
_text_state: &mut Self::RequestLayoutState,
_cx: &mut ElementContext,
_cx: &mut WindowContext,
) {
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
text_state: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
cx: &mut ElementContext,
cx: &mut WindowContext,
) {
let text_str: &str = self.as_ref();
text_state.paint(bounds, text_str, cx)
@@ -151,7 +173,17 @@ impl Element for StyledText {
type RequestLayoutState = TextState;
type PrepaintState = ();
fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) {
fn id(&self) -> Option<ElementId> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
cx: &mut WindowContext,
) -> (LayoutId, Self::RequestLayoutState) {
let mut state = TextState::default();
let layout_id = state.layout(self.text.clone(), self.runs.take(), cx);
(layout_id, state)
@@ -159,18 +191,20 @@ impl Element for StyledText {
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_bounds: Bounds<Pixels>,
_state: &mut Self::RequestLayoutState,
_cx: &mut ElementContext,
_cx: &mut WindowContext,
) {
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
text_state: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
cx: &mut ElementContext,
cx: &mut WindowContext,
) {
text_state.paint(bounds, &self.text, cx)
}
@@ -204,7 +238,7 @@ impl TextState {
&mut self,
text: SharedString,
runs: Option<Vec<TextRun>>,
cx: &mut ElementContext,
cx: &mut WindowContext,
) -> LayoutId {
let text_style = cx.text_style();
let font_size = text_style.font_size.to_pixels(cx.rem_size());
@@ -279,7 +313,7 @@ impl TextState {
layout_id
}
fn paint(&mut self, bounds: Bounds<Pixels>, text: &str, cx: &mut ElementContext) {
fn paint(&mut self, bounds: Bounds<Pixels>, text: &str, cx: &mut WindowContext) {
let element_state = self.lock();
let element_state = element_state
.as_ref()
@@ -405,18 +439,27 @@ impl Element for InteractiveText {
type RequestLayoutState = TextState;
type PrepaintState = Hitbox;
fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) {
self.text.request_layout(cx)
fn id(&self) -> Option<ElementId> {
Some(self.element_id.clone())
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
cx: &mut WindowContext,
) -> (LayoutId, Self::RequestLayoutState) {
self.text.request_layout(None, cx)
}
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
state: &mut Self::RequestLayoutState,
cx: &mut ElementContext,
cx: &mut WindowContext,
) -> Hitbox {
cx.with_element_state::<InteractiveTextState, _>(
Some(self.element_id.clone()),
cx.with_optional_element_state::<InteractiveTextState, _>(
global_id,
|interactive_state, cx| {
let interactive_state = interactive_state
.map(|interactive_state| interactive_state.unwrap_or_default());
@@ -430,7 +473,7 @@ impl Element for InteractiveText {
}
}
self.text.prepaint(bounds, state, cx);
self.text.prepaint(None, bounds, state, cx);
let hitbox = cx.insert_hitbox(bounds, false);
(hitbox, interactive_state)
},
@@ -439,15 +482,16 @@ impl Element for InteractiveText {
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
text_state: &mut Self::RequestLayoutState,
hitbox: &mut Hitbox,
cx: &mut ElementContext,
cx: &mut WindowContext,
) {
cx.with_element_state::<InteractiveTextState, _>(
Some(self.element_id.clone()),
global_id.unwrap(),
|interactive_state, cx| {
let mut interactive_state = interactive_state.unwrap().unwrap_or_default();
let mut interactive_state = interactive_state.unwrap_or_default();
if let Some(click_listener) = self.click_listener.take() {
let mouse_position = cx.mouse_position();
if let Some(ix) = text_state.index_for_position(bounds, mouse_position) {
@@ -577,9 +621,9 @@ impl Element for InteractiveText {
});
}
self.text.paint(bounds, text_state, &mut (), cx);
self.text.paint(None, bounds, text_state, &mut (), cx);
((), Some(interactive_state))
((), interactive_state)
},
);
}

View File

@@ -5,9 +5,9 @@
//! elements with uniform height.
use crate::{
point, px, size, AnyElement, AvailableSpace, Bounds, ContentMask, Element, ElementContext,
ElementId, Hitbox, InteractiveElement, Interactivity, IntoElement, LayoutId, Pixels, Render,
ScrollHandle, Size, StyleRefinement, Styled, View, ViewContext, WindowContext,
point, px, size, AnyElement, AvailableSpace, Bounds, ContentMask, Element, ElementId,
GlobalElementId, Hitbox, InteractiveElement, Interactivity, IntoElement, LayoutId, Pixels,
Render, ScrollHandle, Size, StyleRefinement, Styled, View, ViewContext, WindowContext,
};
use smallvec::SmallVec;
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
@@ -107,26 +107,38 @@ impl Element for UniformList {
type RequestLayoutState = UniformListFrameState;
type PrepaintState = Option<Hitbox>;
fn request_layout(&mut self, cx: &mut ElementContext) -> (LayoutId, Self::RequestLayoutState) {
fn id(&self) -> Option<ElementId> {
self.interactivity.element_id.clone()
}
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
cx: &mut WindowContext,
) -> (LayoutId, Self::RequestLayoutState) {
let max_items = self.item_count;
let item_size = self.measure_item(None, cx);
let layout_id = self.interactivity.request_layout(cx, |style, cx| {
cx.request_measured_layout(style, move |known_dimensions, available_space, _cx| {
let desired_height = item_size.height * max_items;
let width = known_dimensions
.width
.unwrap_or(match available_space.width {
AvailableSpace::Definite(x) => x,
AvailableSpace::MinContent | AvailableSpace::MaxContent => item_size.width,
});
let layout_id = self
.interactivity
.request_layout(global_id, cx, |style, cx| {
cx.request_measured_layout(style, move |known_dimensions, available_space, _cx| {
let desired_height = item_size.height * max_items;
let width = known_dimensions
.width
.unwrap_or(match available_space.width {
AvailableSpace::Definite(x) => x,
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
item_size.width
}
});
let height = match available_space.height {
AvailableSpace::Definite(height) => desired_height.min(height),
AvailableSpace::MinContent | AvailableSpace::MaxContent => desired_height,
};
size(width, height)
})
});
let height = match available_space.height {
AvailableSpace::Definite(height) => desired_height.min(height),
AvailableSpace::MinContent | AvailableSpace::MaxContent => desired_height,
};
size(width, height)
})
});
(
layout_id,
@@ -139,11 +151,12 @@ impl Element for UniformList {
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
frame_state: &mut Self::RequestLayoutState,
cx: &mut ElementContext,
cx: &mut WindowContext,
) -> Option<Hitbox> {
let style = self.interactivity.compute_style(None, cx);
let style = self.interactivity.compute_style(global_id, None, cx);
let border = style.border_widths.to_pixels(cx.rem_size());
let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
@@ -167,6 +180,7 @@ impl Element for UniformList {
.and_then(|handle| handle.deferred_scroll_to_item.take());
self.interactivity.prepaint(
global_id,
bounds,
content_size,
cx,
@@ -236,13 +250,14 @@ impl Element for UniformList {
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
bounds: Bounds<crate::Pixels>,
request_layout: &mut Self::RequestLayoutState,
hitbox: &mut Option<Hitbox>,
cx: &mut ElementContext,
cx: &mut WindowContext,
) {
self.interactivity
.paint(bounds, hitbox.as_ref(), cx, |_, cx| {
.paint(global_id, bounds, hitbox.as_ref(), cx, |_, cx| {
for item in &mut request_layout.items {
item.paint(cx);
}
@@ -265,7 +280,7 @@ impl UniformList {
self
}
fn measure_item(&self, list_width: Option<Pixels>, cx: &mut ElementContext) -> Size<Pixels> {
fn measure_item(&self, list_width: Option<Pixels>, cx: &mut WindowContext) -> Size<Pixels> {
if self.item_count == 0 {
return Size::default();
}

View File

@@ -330,19 +330,12 @@ impl<T> Flatten<T> for Result<T> {
/// A marker trait for types that can be stored in GPUI's global state.
///
/// This trait exists to provide type-safe access to globals by restricting
/// the scope from which they can be accessed. For instance, the actual type
/// that implements [`Global`] can be private, with public accessor functions
/// that enforce correct usage.
///
/// Implement this on types you want to store in the context as a global.
pub trait Global: 'static + Sized {
/// Access the global of the implementing type. Panics if a global for that type has not been assigned.
fn get(cx: &AppContext) -> &Self {
cx.global()
}
/// Updates the global of the implementing type with a closure. Unlike `global_mut`, this method provides
/// your closure with mutable access to the `AppContext` and the global simultaneously.
fn update<C, R>(cx: &mut C, f: impl FnOnce(&mut Self, &mut C) -> R) -> R
where
C: BorrowAppContext,
{
cx.update_global(f)
}
pub trait Global: 'static {
// This trait is intentionally left empty, by virtue of being a marker trait.
}

View File

@@ -50,9 +50,8 @@
/// KeyBinding::new("cmd-k left", pane::SplitLeft, Some("Pane"))
///
use crate::{
Action, ActionRegistry, DispatchPhase, ElementContext, EntityId, FocusId, KeyBinding,
KeyContext, Keymap, KeymatchResult, Keystroke, KeystrokeMatcher, ModifiersChangedEvent,
WindowContext,
Action, ActionRegistry, DispatchPhase, EntityId, FocusId, KeyBinding, KeyContext, Keymap,
KeymatchResult, Keystroke, KeystrokeMatcher, ModifiersChangedEvent, WindowContext,
};
use collections::FxHashMap;
use smallvec::SmallVec;
@@ -107,8 +106,8 @@ impl ReusedSubtree {
}
}
type KeyListener = Rc<dyn Fn(&dyn Any, DispatchPhase, &mut ElementContext)>;
type ModifiersChangedListener = Rc<dyn Fn(&ModifiersChangedEvent, &mut ElementContext)>;
type KeyListener = Rc<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>;
type ModifiersChangedListener = Rc<dyn Fn(&ModifiersChangedEvent, &mut WindowContext)>;
#[derive(Clone)]
pub(crate) struct DispatchActionListener {

View File

@@ -209,6 +209,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn activate(&self);
fn is_active(&self) -> bool;
fn set_title(&mut self, title: &str);
fn set_app_id(&mut self, app_id: &str);
fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance);
fn set_edited(&mut self, edited: bool);
fn show_character_palette(&self);
@@ -557,6 +558,9 @@ pub struct WindowOptions {
/// The appearance of the window background.
pub window_background: WindowBackgroundAppearance,
/// Application identifier of the window. Can by used by desktop environments to group applications together.
pub app_id: Option<String>,
}
/// The variables that can be configured when creating a new window
@@ -599,6 +603,7 @@ impl Default for WindowOptions {
display_id: None,
fullscreen: false,
window_background: WindowBackgroundAppearance::default(),
app_id: None,
}
}
}
@@ -693,7 +698,7 @@ pub struct PathPromptOptions {
}
/// What kind of prompt styling to show
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum PromptLevel {
/// A prompt that is shown when the user should be notified of something
Info,
@@ -703,10 +708,14 @@ pub enum PromptLevel {
/// A prompt that is shown when a critical problem has occurred
Critical,
/// A prompt that is shown when asking the user to confirm a potentially destructive action
/// (overwriting a file for example)
Destructive,
}
/// The style of the cursor (pointer)
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum CursorStyle {
/// The default cursor
Arrow,

View File

@@ -12,7 +12,7 @@ use collections::HashMap;
#[cfg(target_os = "macos")]
use media::core_video::CVMetalTextureCache;
#[cfg(target_os = "macos")]
use std::ffi::c_void;
use std::{ffi::c_void, ptr::NonNull};
use blade_graphics as gpu;
use std::{mem, sync::Arc};
@@ -25,35 +25,32 @@ pub type Renderer = BladeRenderer;
#[cfg(target_os = "macos")]
pub unsafe fn new_renderer(
_context: self::Context,
native_window: *mut c_void,
_native_window: *mut c_void,
native_view: *mut c_void,
bounds: crate::Size<f32>,
) -> Renderer {
use raw_window_handle as rwh;
struct RawWindow {
window: *mut c_void,
view: *mut c_void,
}
unsafe impl blade_rwh::HasRawWindowHandle for RawWindow {
fn raw_window_handle(&self) -> blade_rwh::RawWindowHandle {
let mut wh = blade_rwh::AppKitWindowHandle::empty();
wh.ns_window = self.window;
wh.ns_view = self.view;
wh.into()
impl rwh::HasWindowHandle for RawWindow {
fn window_handle(&self) -> Result<rwh::WindowHandle, rwh::HandleError> {
let view = NonNull::new(self.view).unwrap();
let handle = rwh::AppKitWindowHandle::new(view);
Ok(unsafe { rwh::WindowHandle::borrow_raw(handle.into()) })
}
}
unsafe impl blade_rwh::HasRawDisplayHandle for RawWindow {
fn raw_display_handle(&self) -> blade_rwh::RawDisplayHandle {
let dh = blade_rwh::AppKitDisplayHandle::empty();
dh.into()
impl rwh::HasDisplayHandle for RawWindow {
fn display_handle(&self) -> Result<rwh::DisplayHandle, rwh::HandleError> {
let handle = rwh::AppKitDisplayHandle::new();
Ok(unsafe { rwh::DisplayHandle::borrow_raw(handle.into()) })
}
}
let gpu = Arc::new(
gpu::Context::init_windowed(
&RawWindow {
window: native_window as *mut _,
view: native_view as *mut _,
},
gpu::ContextDesc {
@@ -184,7 +181,7 @@ struct BladePipelines {
}
impl BladePipelines {
fn new(gpu: &gpu::Context, surface_format: gpu::TextureFormat) -> Self {
fn new(gpu: &gpu::Context, surface_info: gpu::SurfaceInfo) -> Self {
use gpu::ShaderData as _;
let shader = gpu.create_shader(gpu::ShaderDesc {
@@ -216,7 +213,7 @@ impl BladePipelines {
depth_stencil: None,
fragment: shader.at("fs_quad"),
color_targets: &[gpu::ColorTargetState {
format: surface_format,
format: surface_info.format,
blend: Some(gpu::BlendState::ALPHA_BLENDING),
write_mask: gpu::ColorWrites::default(),
}],
@@ -233,7 +230,7 @@ impl BladePipelines {
depth_stencil: None,
fragment: shader.at("fs_shadow"),
color_targets: &[gpu::ColorTargetState {
format: surface_format,
format: surface_info.format,
blend: Some(gpu::BlendState::ALPHA_BLENDING),
write_mask: gpu::ColorWrites::default(),
}],
@@ -267,7 +264,7 @@ impl BladePipelines {
depth_stencil: None,
fragment: shader.at("fs_path"),
color_targets: &[gpu::ColorTargetState {
format: surface_format,
format: surface_info.format,
blend: Some(gpu::BlendState::ALPHA_BLENDING),
write_mask: gpu::ColorWrites::default(),
}],
@@ -284,7 +281,7 @@ impl BladePipelines {
depth_stencil: None,
fragment: shader.at("fs_underline"),
color_targets: &[gpu::ColorTargetState {
format: surface_format,
format: surface_info.format,
blend: Some(gpu::BlendState::ALPHA_BLENDING),
write_mask: gpu::ColorWrites::default(),
}],
@@ -301,7 +298,7 @@ impl BladePipelines {
depth_stencil: None,
fragment: shader.at("fs_mono_sprite"),
color_targets: &[gpu::ColorTargetState {
format: surface_format,
format: surface_info.format,
blend: Some(gpu::BlendState::ALPHA_BLENDING),
write_mask: gpu::ColorWrites::default(),
}],
@@ -318,7 +315,7 @@ impl BladePipelines {
depth_stencil: None,
fragment: shader.at("fs_poly_sprite"),
color_targets: &[gpu::ColorTargetState {
format: surface_format,
format: surface_info.format,
blend: Some(gpu::BlendState::ALPHA_BLENDING),
write_mask: gpu::ColorWrites::default(),
}],
@@ -335,7 +332,7 @@ impl BladePipelines {
depth_stencil: None,
fragment: shader.at("fs_surface"),
color_targets: &[gpu::ColorTargetState {
format: surface_format,
format: surface_info.format,
blend: Some(gpu::BlendState::ALPHA_BLENDING),
write_mask: gpu::ColorWrites::default(),
}],
@@ -367,16 +364,18 @@ impl BladeRenderer {
//Note: this matches the original logic of the Metal backend,
// but ultimaterly we need to switch to `Linear`.
color_space: gpu::ColorSpace::Srgb,
allow_exclusive_full_screen: false,
transparent: false,
}
}
pub fn new(gpu: Arc<gpu::Context>, size: gpu::Extent) -> Self {
let surface_format = gpu.resize(Self::make_surface_config(size));
let surface_info = gpu.resize(Self::make_surface_config(size));
let command_encoder = gpu.create_command_encoder(gpu::CommandEncoderDesc {
name: "main",
buffer_count: 2,
});
let pipelines = BladePipelines::new(&gpu, surface_format);
let pipelines = BladePipelines::new(&gpu, surface_info);
let instance_belt = BladeBelt::new(BladeBeltDescriptor {
memory: gpu::Memory::Shared,
min_chunk_size: 0x1000,

View File

@@ -90,7 +90,7 @@ impl PlatformTextSystem for CosmicTextSystem {
let candidates = if let Some(font_ids) = state.font_ids_by_family_cache.get(&font.family) {
font_ids.as_slice()
} else {
let font_ids = state.load_family(&font.family, font.features)?;
let font_ids = state.load_family(&font.family, &font.features)?;
state
.font_ids_by_family_cache
.insert(font.family.clone(), font_ids);
@@ -211,7 +211,7 @@ impl CosmicTextSystemState {
fn load_family(
&mut self,
name: &str,
_features: FontFeatures,
_features: &FontFeatures,
) -> Result<SmallVec<[FontId; 4]>> {
// TODO: Determine the proper system UI font.
let name = if name == ".SystemUIFont" {

View File

@@ -28,6 +28,7 @@ use futures::channel::oneshot;
use parking_lot::Mutex;
use time::UtcOffset;
use wayland_client::Connection;
use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape;
use xkbcommon::xkb::{self, Keycode, Keysym, State};
use crate::platform::linux::wayland::WaylandClient;
@@ -501,6 +502,58 @@ pub(super) unsafe fn read_fd(mut fd: FileDescriptor) -> Result<String> {
Ok(result)
}
impl CursorStyle {
pub(super) fn to_shape(&self) -> Shape {
match self {
CursorStyle::Arrow => Shape::Default,
CursorStyle::IBeam => Shape::Text,
CursorStyle::Crosshair => Shape::Crosshair,
CursorStyle::ClosedHand => Shape::Grabbing,
CursorStyle::OpenHand => Shape::Grab,
CursorStyle::PointingHand => Shape::Pointer,
CursorStyle::ResizeLeft => Shape::WResize,
CursorStyle::ResizeRight => Shape::EResize,
CursorStyle::ResizeLeftRight => Shape::EwResize,
CursorStyle::ResizeUp => Shape::NResize,
CursorStyle::ResizeDown => Shape::SResize,
CursorStyle::ResizeUpDown => Shape::NsResize,
CursorStyle::DisappearingItem => Shape::Grabbing, // todo(linux) - couldn't find equivalent icon in linux
CursorStyle::IBeamCursorForVerticalLayout => Shape::VerticalText,
CursorStyle::OperationNotAllowed => Shape::NotAllowed,
CursorStyle::DragLink => Shape::Alias,
CursorStyle::DragCopy => Shape::Copy,
CursorStyle::ContextualMenu => Shape::ContextMenu,
}
}
pub(super) fn to_icon_name(&self) -> String {
// Based on cursor names from https://gitlab.gnome.org/GNOME/adwaita-icon-theme (GNOME)
// and https://github.com/KDE/breeze (KDE). Both of them seem to be also derived from
// Web CSS cursor names: https://developer.mozilla.org/en-US/docs/Web/CSS/cursor#values
match self {
CursorStyle::Arrow => "arrow",
CursorStyle::IBeam => "text",
CursorStyle::Crosshair => "crosshair",
CursorStyle::ClosedHand => "grabbing",
CursorStyle::OpenHand => "grab",
CursorStyle::PointingHand => "pointer",
CursorStyle::ResizeLeft => "w-resize",
CursorStyle::ResizeRight => "e-resize",
CursorStyle::ResizeLeftRight => "ew-resize",
CursorStyle::ResizeUp => "n-resize",
CursorStyle::ResizeDown => "s-resize",
CursorStyle::ResizeUpDown => "ns-resize",
CursorStyle::DisappearingItem => "grabbing", // todo(linux) - couldn't find equivalent icon in linux
CursorStyle::IBeamCursorForVerticalLayout => "vertical-text",
CursorStyle::OperationNotAllowed => "not-allowed",
CursorStyle::DragLink => "alias",
CursorStyle::DragCopy => "copy",
CursorStyle::ContextualMenu => "context-menu",
}
.to_string()
}
}
impl Keystroke {
pub(super) fn from_xkb(state: &State, modifiers: Modifiers, keycode: Keycode) -> Self {
let mut modifiers = modifiers;

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