Compare commits

..

48 Commits

Author SHA1 Message Date
Max Brunsfeld
2e72b64547 Remove assistant2 SubmitMode 2024-05-09 20:50:25 -07:00
Conrad Irwin
45f12b9426 vim cl (#11641)
Release Notes:

- vim: Added support for the changelist. `g;` and `g,` to the
previous/next change
- vim: Added support for the `'.` mark
- vim: Added support for `gi` to resume the previous insert
2024-05-09 21:18:56 -06:00
Conrad Irwin
4f9ba28a25 linux cli (#11585)
- [x] Build out cli on linux
- [x] Add support for --dev-server-token sent by the CLI
- [x] Package cli into the .tar.gz
- [x] Link the cli to ~/.local/bin in install.sh

Release Notes:

- linux: Add cli support for managing zed
2024-05-09 21:08:49 -06:00
Conrad Irwin
0c2d71f1ac Remove 'Destructive' prompts (#11631)
While these would match how macOS handles this scenario, they crash on
Catalina, and require mouse clicks to interact.

cc @bennetbo



Release Notes:

- N/A
2024-05-09 18:52:09 -06:00
Zachiah Sawyer
901cb8b3d2 vim: Add basic mark support (#11507)
Release Notes:
- vim: Added support for buffer-local marks (`'a-'z`) and some builtin
marks `'<`,`'>`,`'[`,`']`, `'{`, `'}` and `^`. Global marks (`'A-'Z`),
and other builtin marks (`'0-'9`, `'(`, `')`, `''`, `'.`, `'"`) are not
yet implemented. (#5122)

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-05-09 18:51:19 -06:00
Kyle Kelley
9cef0ac869 Cleanup tool registry API surface (#11637)
Fast followups to #11629 

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
2024-05-09 16:43:27 -07:00
Kirill Bulatov
79b5556267 Remove a stray eprintln (#11635)
Release Notes:

- N/A
2024-05-10 02:27:55 +03:00
Marshall Bowers
dd67bda595 Update .mailmap (#11633)
This PR updates the `.mailmap` file to merge some commit authors using
multiple emails.

Release Notes:

- N/A
2024-05-09 19:03:34 -04:00
Kirill Bulatov
4762e52d31 Properly calculate expanded git diff hunk highlight ranges (#11632)
Closes https://github.com/zed-industries/zed/issues/11576

Release Notes:

- Fixed expanded diff hunks highlighting an extra row as added
([11576](https://github.com/zed-industries/zed/issues/11576))
2024-05-10 02:02:56 +03:00
Kyle Kelley
50c45c7897 Streaming tools (#11629)
Stream characters in for tool calls to allow rendering partial input.


https://github.com/zed-industries/zed/assets/836375/0f023a4b-9c46-4449-ae69-8b6bcab41673

Release Notes:

- N/A

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Marshall <marshall@zed.dev>
Co-authored-by: Max <max@zed.dev>
2024-05-09 15:57:14 -07:00
Marshall Bowers
27ed0f4273 assistant2: Render saved conversations inline instead of in a modal (#11630)
This PR reworks how saved conversations are rendered in the new
assistant panel.

Instead of rendering them in a modal we now display them in the panel
itself:

<img width="402" alt="Screenshot 2024-05-09 at 6 18 40 PM"
src="https://github.com/zed-industries/zed/assets/1486634/82decc04-cb31-4d83-a942-7e8426e02679">

Release Notes:

- N/A
2024-05-09 18:29:08 -04:00
Conrad Irwin
a3e75540af Reduce serializability of project delete (#11628)
This may reduce locks when deleting projects.

Release Notes:

- N/A
2024-05-09 16:17:13 -06:00
Conrad Irwin
aa5113cd92 vim: Support paste with count (#11621)
Fixes: #10842



Release Notes:

- vim: Fix pasting with a count (#10842)
2024-05-09 16:12:59 -06:00
Kirill Bulatov
bca639bda9 Use larger runners for Linux CI steps (#11574)
To speed up Linux CI builds, use a set of self-hosted Linux machines and
use them to run all slow CI steps for Linux: "tests", bundling and
nightly builds.

Also adds a set of dev icons as Linux bundling script required them for
`run-bundling`-tagged builds from regular PRs.
Same icons as for Preview were used, but, ideally, something different
could be created.

Release Notes:

- N/A
2024-05-10 00:44:31 +03:00
Piotr Osiewicz
bff1d8b142 task: Allow obtaining custom task variables from tree-sitter queries (#11624)
From now on, only top-level captures are treated as runnable tags and
the rest is appended to task context as custom environmental variables
(unless the name is prefixed with _, in which case the capture is
ignored). This is most likely gonna help with Pest-like test runners.



Release Notes:

- N/A

---------

Co-authored-by: Remco <djsmits12@gmail.com>
2024-05-09 23:38:18 +02:00
张小白
95e246ac1c windows: Better dispatcher (#11485)
This PR leverages a more modern Windows API to implement
`WindowsDispatcher`, aligning its implementation more closely with that
of the `macOS` platform. The following improvements have been made:

1. Similar to `macOS`, there is no longer a need to use `sender` and
`receiver` to dispatch a `Runnable` on the main thread.
2. There is also no longer a need to use an `Event` for synchronization.
3. Consistent with #7506 and #11269, `Runnable` is now executed with
high priority.

However, this PR raises the minimum Windows version requirement of
`GPUI` to Windows 10, specifically Windows 10 Fall Creators Update
(10.0.16299). However, the `alacritty_terminal` dependency in Zed relies
on `conPTY` on Windows, an API introduced in the Windows 10 Fall
Creators Update. Therefore, the impact of this PR on Zed should be
minimal. I'd like to hear your voices about this PR, especially about
the minimum Windows version bumping.

Release Notes:

- N/A
2024-05-09 14:24:57 -07:00
Andrew Lygin
ba25e371be Fix scrollbar markers for folded code (#11625)
There're two errors in scrollbar markers in the presence of folded code:

1. Some markers are not displayed (when the marked row numbers are
greater than the total displayed rows count after folding).
2. Code folding / unfolding doesn't trigger markers repainting.

This PR fixes both problems.

Release Notes:

- Fixed scrollbar markers for folded code.

The second problem (markers are repainted after I move the cursor, not
after folding):


https://github.com/zed-industries/zed/assets/2101250/57ed563d-186d-4497-98ab-d4f946416726
2024-05-09 14:23:21 -07:00
Marshall Bowers
c73ef1a5f3 assistant2: List saved conversations from disk (#11627)
This PR updates the saved conversation picker to use a list of
conversations retrieved from disk instead of the static placeholder
values.

Release Notes:

- N/A
2024-05-09 16:17:07 -04:00
Conrad Irwin
8b5a0cff10 vim: Fix e/E with inlay hints (#11616)
Co-Authored-By: Sergey <sergey.b@hey.com>
Fixes: #7046

Release Notes:

- vim: Fixes e/E with inlay hints (#7046)

Co-authored-by: Sergey <sergey.b@hey.com>
2024-05-09 13:45:45 -06:00
Piotr Osiewicz
f0af508ae5 Revert "chore: Bump taffy version to 0.4.3" (#11622)
Reverts zed-industries/zed#11606
2024-05-09 19:11:37 +02:00
Marshall Bowers
5fe4070501 docs: Fix copying code blocks (#11617)
This PR fixes copying code blocks in the docs.

The problem was that some of the elements we removed from the base
mdBook template were causing errors in the script, which prevented the
right event listeners from being registered for the copy button.

To remedy this, the elements have been restored, but are using `display:
none` so that they don't appear in the UI.

Resolves #11592.

Release Notes:

- N/A
2024-05-09 11:52:26 -04:00
Marshall Bowers
981a143e9b copilot: Update root path on Windows (#11613)
This PR updates the root path used by Copilot to be a validate path when
running on Windows.

Release Notes:

- N/A

Co-authored-by: Jason Lee <huacnlee@gmail.com>
2024-05-09 10:14:29 -04:00
Jason Lee
5e06ce4df3 Add zip extract support for Windows (#11156)
Release Notes:

- [x] Fixed install Node.js runtime and NPM lsp installation on Windows.
- [x] Update Node runtime command to execute on Windows with no window
popup.

Ref #9619, #9424

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-05-09 09:23:21 -04:00
Piotr Osiewicz
3bd53d0441 chore: Bump taffy version to 0.4.3 (#11606)
Taffy 0.4 has been released 2 months ago. We've been using an older
commit from their 0.4 development branch since November.
Compared to the commit we were pinned to, the following relevant changes
have been made:
-
563d5dcee7
-
64f8aa0fb1
-
70b35712a2

![image](https://github.com/zed-industries/zed/assets/24362066/ffdfae03-2743-496f-bb21-7aa38462178f)

Release Notes:

- N/A
2024-05-09 12:51:53 +02:00
Piotr Osiewicz
76535578e9 Task indicators in multibuffers (#11603)
Following #11487 the task indicators would no longer show up in
multibuffers.
Release Notes:

- N/A
2024-05-09 12:22:33 +02:00
Piotr Osiewicz
fdcedf15b7 editor: Do not show test indicators if a line is folded (#11599)
Originally reported by @RemcoSmitsDev



Release Notes:

- N/A
2024-05-09 11:43:50 +02:00
Piotr Osiewicz
bd6d385817 gpui: Pass Style by value to request_layout (#11597)
A minor thing I've spotted and decided to fix on the spot.
It was being cloned twice within the body of that function (one of which
was redundant even without this PR); now in most cases we go down from 2
clones to 0.
Release Notes:

- N/A
2024-05-09 11:38:53 +02:00
Antonio Scandurra
5df1481297 Introduce a new markdown crate (#11556)
This pull request introduces a new `markdown` crate which is capable of
parsing and rendering a Markdown source. One of the key additions is
that it enables text selection within a `Markdown` view. Eventually,
this will replace `RichText` but for now the goal is to use it in the
assistant revamped assistant in the spirit of making progress.

<img width="711" alt="image"
src="https://github.com/zed-industries/zed/assets/482957/b56c777b-e57c-42f9-95c1-3ada22f63a69">

Note that this pull request doesn't yet use the new markdown renderer in
`assistant2`. This is because we need to modify the assistant before
slotting in the new renderer and I wanted to merge this independently of
those changes.

Release Notes:

- N/A

---------

Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Alp <akeles@umd.edu>
Co-authored-by: Zachiah Sawyer <zachiah@proton.me>
2024-05-09 11:03:33 +02:00
Doy Bachtiar
ddaaaee973 docs: Fix a typo (#11588)
This PR fixes a typo in docs/src/development/debugging-crashes.md.

Release Notes:

- N/A
2024-05-08 20:16:02 -07:00
张小白
9772b7ac33 windows: Fix Zed freezing when resuming from sleep (#11589)
It seems that on the first frame after the system resumes from sleep,
`dcomp_vsync_fn` mistakenly detects the `timer_stop_event` triggering
and exits the loop.

Release Notes:

- N/A
2024-05-08 20:15:32 -07:00
CharlesChen0823
2e0811e113 windows: Improve platform clipboard (#11553)
I thought platform clipboard should share one ctx. and fixed in vim
mode, read from clipboard crash when using `unwrap`.

Release Notes:

- N/A
2024-05-08 16:09:13 -07:00
张小白
1b292d2fb3 Fix crash when the length of a line is greater than 1024 chars (#11536)
Close #11518 

Release Notes:

- N/A
2024-05-08 16:08:39 -07:00
Marshall Bowers
adecbd1815 Make Markdown default to "format_on_save": "off" (#11584)
This PR changes the Markdown language defaults to set `format_on_save`
to be `off`.

Prettier's Markdown formatting is a bit controversial for some people,
so we turn it off by default.

To restore the previous behavior, add the following to your settings:

```json
{
  "languages": {
    "Markdown": {
      "format_on_save": "on"
    }
  }
}
```


Release Notes:

- Changed the default `format_on_save` behavior for Markdown files to be
`off`.
2024-05-08 18:44:21 -04:00
Max Brunsfeld
a7aa2578e1 Implement serialization of assistant conversations, including tool calls and attachments (#11577)
Release Notes:

- N/A

---------

Co-authored-by: Kyle <kylek@zed.dev>
Co-authored-by: Marshall <marshall@zed.dev>
2024-05-08 17:52:15 -04:00
Conrad Irwin
24ffa0fcf3 Don't panic on failure to allocate an AtlasTile (#11579)
Release Notes:

- Fixed a panic in graphics allocation
2024-05-08 15:47:15 -06:00
Conrad Irwin
b0494d1c05 Pass hover position as an anchor (#11578)
It's too easily to accidentally pass a point from one snapshot into
another

Release Notes:

- Fixed a panic in show hover
2024-05-08 15:39:37 -06:00
Dzmitry Malyshau
a89dc8c42e blade: Switch to linear color space (#11534)
Release Notes:

- N/A

## What

Addresses a long-standing issue of doing the blending operations in sRGB
space. Currently, the input HSL colors are provided in sRGB space and
converted to linear in the vertex shader. Conversion back to sRGB, which
is required on most platforms today, happens at the very end of the
pipeline when writing into sRGB render target.

Note-1: in the future we may consider doing HSL -> sRGB -> Linear
transform on CPU before feeding into shaders. However, I don't expect
any significant difference here given that we are likely bound by fill
rate and pixel shaders, anyway.

Note-2: the graphics stack is programmed to detect if the platform
supports presenting in linear color space and avoids converting to sRGB
at the end in this case. However, on my Z13 laptop this isn't supported
by the RADV driver.

Closes #7684 
Closes #11462
@jansol please confirm if you can!

## Comparison

Screenshot of the Glazier theme before the change:

![glazier-old](https://github.com/zed-industries/zed/assets/107301/6a9552e1-0819-4a4e-8121-8d62ec012bf4)
Same theme after the change:

![glazier-new](https://github.com/zed-industries/zed/assets/107301/4e61c422-4a4b-4c4b-84a3-55680626d681)
2024-05-08 12:47:29 -07:00
Nate Butler
d103903229 Style header for assistant2 (#11570)
Release Notes:

- N/A
2024-05-08 14:17:07 -04:00
CharlesChen0823
ec3aabe2c2 windows: Fix crash when saving files to disk (#11547)
closes #11544, sorry for introduce this issue by pre pr.
Release Notes:

- N/A
2024-05-08 11:12:07 -07:00
张小白
4b98c35d68 windows: Let IME early return in vim mode (#11551)
This PR follows up #11387 , slightly changes the IME window behavior to
match macOS implementation.

Release Notes:

- N/A
2024-05-08 11:01:48 -07:00
张小白
5103995c32 windows: Fix incorrect font rendering (#11545)
Previously, `DirectWrite` had been following the text system
implementation on `macOS`, using the font's postscript name to
differentiate between different font faces. However, I noticed
occasional rendering issues, such as fonts incorrectly rendering as
italics. Later, I discovered that on `Windows`, the postscript name is
**not** unique. Surprisingly, even the same font can have different
postscript names on macOS and Windows! It's hard to believe! The
postscript name of a font face should be obtained from the same font
table. Why would the same font face have different names?

For example, the postscript name of ZedMono on `macOS` is
`Zed-Mono-Bold-Extended-Italic`, while on `Windows`, it is
`Zed-Mono-Extended`, missing weight and style information, leading to
incorrect rendering.

This PR introduces a `FontIdentifier` struct to uniquely identify font
faces.

Release Notes:

- N/A
2024-05-08 10:58:31 -07:00
张小白
fb4c6dbaa7 windows: Implement ResizeColumn and ResizeRow cursor style (#11533)
This PR follows up #11406

Release Notes:

- N/A
2024-05-08 10:57:09 -07:00
LoganDark
91c1716858 Fix horizontal scrolling direction on Windows (#11520)
As per Microsoft documentation, positive values scroll right, not left.
GPUI was incorrectly assuming it perfectly mirrored vertical scrolling.

Fixes #11515

Release Notes:

- N/A
2024-05-08 10:56:31 -07:00
Andrew Lygin
0933426e63 Editor tab bar settings (#7356)
This PR is another step to tabless editing (#6424, #4963). It adds
support for tab bar settings that allow the user to change its placement
or to hide completely.

Configuraton:

```json
"tab_bar": {
  "show": true
}
```

Placemnet options are "top", "bottom" and "no".

This PR intentionally doesn't affect tab bars of other panes (Terminal
for instance) to keep code changes small. I guess we'll do the rest in
separate PRs.

Release Notes:

- Added support for configuring the editor tab bar (part of #6424,
#4963).

---------

Co-authored-by: Mikayla <mikayla@zed.dev>
2024-05-08 10:54:48 -07:00
Kyle Kelley
689e4aef2f Render messages as early as possible to show progress (#11569)
This shows "Researching..." as placeholder text as early as possible so
that the user can see the model is working on reading/researching/etc.

This also adds on an `Option<Value>` to the `render_running` function so
that tools can hopefully render based on partially completed JSON (still
to come).

Release Notes:

- N/A
2024-05-08 10:24:51 -07:00
Thorsten Ball
dbebb40956 linux: Store binary path before restart to handle deleted binary file (#11568)
This fixes restart after updates not working on Linux.

On Linux we can't reliably get the binary path after an update, because
the original binary was deleted and the path will contain ` (deleted)`.

See: https://github.com/rust-lang/rust/issues/69343

We *could* strip ` (deleted)` off, but that feels nasty. So instead we
save the original binary path, before we do the installation, then
restart.

Later on, we can also change this to be a _new_ binary path returned by
the installers, which we then have to start.


Release Notes:

- N/A
2024-05-08 19:13:28 +02:00
Max Brunsfeld
d2cec0221b Run windows CI on our own GH-hosted windows runner (#11567)
It's a 16-core runner.

Release Notes:

- N/A
2024-05-08 10:09:43 -07:00
Joseph T. Lyons
724acaab61 v0.136.x dev 2024-05-08 12:05:45 -04:00
144 changed files with 5447 additions and 1877 deletions

View File

@@ -104,23 +104,19 @@ jobs:
# todo(linux): Actually run the tests
linux_tests:
name: (Linux) Run Clippy and tests
runs-on: ubuntu-latest
runs-on:
- self-hosted
- deploy
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Checkout repo
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
- name: Cache dependencies
uses: swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: configure linux
shell: bash -euxo pipefail {0}
run: script/linux
- name: cargo clippy
run: cargo xtask clippy
@@ -130,7 +126,7 @@ jobs:
# todo(windows): Actually run the tests
windows_tests:
name: (Windows) Run Clippy and tests
runs-on: windows-latest
runs-on: hosted-windows-1
steps:
- name: Checkout repo
uses: actions/checkout@v4
@@ -262,26 +258,25 @@ jobs:
bundle-linux:
name: Create a Linux bundle
runs-on: ubuntu-22.04 # keep the version fixed to avoid libc and dynamic linked library issues
runs-on:
- self-hosted
- deploy
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 }}
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Checkout repo
uses: actions/checkout@v4
with:
clean: false
submodules: "recursive"
- name: Cache dependencies
uses: swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Configure linux
shell: bash -euxo pipefail {0}
run: script/linux
- name: Limit target directory size
run: script/clear-target-dir-if-larger-than 100
- name: Determine version and release channel
if: ${{ startsWith(github.ref, 'refs/tags/v') }}

View File

@@ -96,7 +96,9 @@ jobs:
bundle-deb:
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
runs-on:
- self-hosted
- deploy
needs: tests
env:
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
@@ -109,14 +111,8 @@ jobs:
clean: false
submodules: "recursive"
- name: Cache dependencies
uses: swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Configure linux
shell: bash -euxo pipefail {0}
run: script/linux
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Set release channel to nightly
run: |

View File

@@ -15,8 +15,12 @@ Christian Bergschneider <christian.bergschneider@gmx.de>
Christian Bergschneider <christian.bergschneider@gmx.de> <magiclake@gmx.de>
Conrad Irwin <conrad@zed.dev>
Conrad Irwin <conrad@zed.dev> <conrad.irwin@gmail.com>
Fernando Tagawa <tagawafernando@gmail.com>
Fernando Tagawa <tagawafernando@gmail.com> <fernando.tagawa.gamail.com@gmail.com>
Greg Morenz <greg-morenz@droid.cafe>
Greg Morenz <greg-morenz@droid.cafe> <morenzg@gmail.com>
Ivan Žužak <izuzak@gmail.com>
Ivan Žužak <izuzak@gmail.com> <ivan.zuzak@github.com>
Joseph T. Lyons <JosephTLyons@gmail.com>
Joseph T. Lyons <JosephTLyons@gmail.com> <JosephTLyons@users.noreply.github.com>
Julia <floc@unpromptedtirade.com>
@@ -29,6 +33,9 @@ Kirill Bulatov <kirill@zed.dev>
Kirill Bulatov <kirill@zed.dev> <mail4score@gmail.com>
Kyle Caverly <kylebcaverly@gmail.com>
Kyle Caverly <kylebcaverly@gmail.com> <kyle@zed.dev>
LoganDark <contact@logandark.mozmail.com>
LoganDark <contact@logandark.mozmail.com> <git@logandark.mozmail.com>
LoganDark <contact@logandark.mozmail.com> <github@logandark.mozmail.com>
Marshall Bowers <elliott.codes@gmail.com>
Marshall Bowers <elliott.codes@gmail.com> <marshall@zed.dev>
Max Brunsfeld <maxbrunsfeld@gmail.com>
@@ -41,6 +48,8 @@ Nate Butler <iamnbutler@gmail.com> <nate@zed.dev>
Nathan Sobo <nathan@zed.dev>
Nathan Sobo <nathan@zed.dev> <nathan@warp.dev>
Nathan Sobo <nathan@zed.dev> <nathansobo@gmail.com>
Petros Amoiridis <petros@hey.com>
Petros Amoiridis <petros@hey.com> <petros@zed.dev>
Piotr Osiewicz <piotr@zed.dev>
Piotr Osiewicz <piotr@zed.dev> <24362066+osiewicz@users.noreply.github.com>
Robert Clover <git@clo4.net>

133
Cargo.lock generated
View File

@@ -377,6 +377,7 @@ dependencies = [
"anyhow",
"assets",
"assistant_tooling",
"chrono",
"client",
"collections",
"editor",
@@ -389,12 +390,12 @@ dependencies = [
"language",
"languages",
"log",
"nanoid",
"node_runtime",
"open_ai",
"picker",
"project",
"rand 0.8.5",
"regex",
"release_channel",
"rich_text",
"schemars",
@@ -417,12 +418,15 @@ dependencies = [
"collections",
"futures 0.3.28",
"gpui",
"log",
"project",
"repair_json",
"schemars",
"serde",
"serde_json",
"settings",
"sum_tree",
"ui",
"unindent",
"util",
]
@@ -482,6 +486,7 @@ version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a116f46a969224200a0a97f29cfd4c50e7534e4b4826bd23ea2c3c533039c82c"
dependencies = [
"deflate64",
"flate2",
"futures-core",
"futures-io",
@@ -815,6 +820,19 @@ dependencies = [
"tungstenite 0.16.0",
]
[[package]]
name = "async_zip"
version = "0.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b9f7252833d5ed4b00aa9604b563529dd5e11de9c23615de2dcdf91eb87b52"
dependencies = [
"async-compression",
"crc32fast",
"futures-lite 2.2.0",
"pin-project",
"thiserror",
]
[[package]]
name = "atoi"
version = "2.0.0"
@@ -1490,7 +1508,7 @@ dependencies = [
[[package]]
name = "blade-graphics"
version = "0.4.0"
source = "git+https://github.com/kvark/blade?rev=f5766863de9dcc092e90fdbbc5e0007a99e7f9bf#f5766863de9dcc092e90fdbbc5e0007a99e7f9bf"
source = "git+https://github.com/kvark/blade?rev=e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c#e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c"
dependencies = [
"ash",
"ash-window",
@@ -1520,7 +1538,7 @@ dependencies = [
[[package]]
name = "blade-macros"
version = "0.2.1"
source = "git+https://github.com/kvark/blade?rev=f5766863de9dcc092e90fdbbc5e0007a99e7f9bf#f5766863de9dcc092e90fdbbc5e0007a99e7f9bf"
source = "git+https://github.com/kvark/blade?rev=e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c#e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c"
dependencies = [
"proc-macro2",
"quote",
@@ -2101,7 +2119,11 @@ dependencies = [
"clap 4.4.4",
"core-foundation",
"core-services",
"exec",
"fork",
"ipc-channel",
"libc",
"once_cell",
"plist",
"release_channel",
"serde",
@@ -3110,6 +3132,12 @@ dependencies = [
"byteorder",
]
[[package]]
name = "deflate64"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83ace6c86376be0b6cdcf3fb41882e81d94b31587573d1cfa9d01cd06bba210d"
[[package]]
name = "der"
version = "0.6.1"
@@ -3551,6 +3579,17 @@ dependencies = [
"serde",
]
[[package]]
name = "errno"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
dependencies = [
"errno-dragonfly",
"libc",
"winapi",
]
[[package]]
name = "errno"
version = "0.3.8"
@@ -3561,6 +3600,16 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "errno-dragonfly"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "etagere"
version = "0.2.8"
@@ -3639,6 +3688,16 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "exec"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "886b70328cba8871bfc025858e1de4be16b1d5088f2ba50b57816f4210672615"
dependencies = [
"errno 0.2.8",
"libc",
]
[[package]]
name = "extension"
version = "0.1.0"
@@ -4039,6 +4098,15 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
[[package]]
name = "fork"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60e74d3423998a57e9d906e49252fb79eb4a04d5cdfe188fb1b7ff9fc076a8ed"
dependencies = [
"libc",
]
[[package]]
name = "form_urlencoded"
version = "1.2.1"
@@ -4764,7 +4832,6 @@ version = "0.1.0"
dependencies = [
"anyhow",
"client",
"ctrlc",
"fs",
"futures 0.3.28",
"gpui",
@@ -4776,6 +4843,7 @@ dependencies = [
"rpc",
"settings",
"shellexpand",
"signal-hook",
"util",
]
@@ -5973,6 +6041,27 @@ dependencies = [
"libc",
]
[[package]]
name = "markdown"
version = "0.1.0"
dependencies = [
"anyhow",
"assets",
"env_logger",
"futures 0.3.28",
"gpui",
"language",
"languages",
"linkify",
"log",
"node_runtime",
"pulldown-cmark",
"settings",
"theme",
"ui",
"util",
]
[[package]]
name = "markdown_preview"
version = "0.1.0"
@@ -6339,15 +6428,20 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-compression",
"async-std",
"async-tar",
"async-trait",
"async_zip",
"futures 0.3.28",
"log",
"semver",
"serde",
"serde_json",
"smol",
"tempfile",
"util",
"walkdir",
"windows 0.53.0",
]
[[package]]
@@ -8001,6 +8095,15 @@ dependencies = [
"bytecheck",
]
[[package]]
name = "repair_json"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee191e184125fe72cb59b74160e25584e3908f2aaa84cbda1e161347102aa15"
dependencies = [
"thiserror",
]
[[package]]
name = "reqwest"
version = "0.11.20"
@@ -8341,7 +8444,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06"
dependencies = [
"bitflags 1.3.2",
"errno",
"errno 0.3.8",
"io-lifetimes 1.0.11",
"libc",
"linux-raw-sys 0.3.8",
@@ -8355,7 +8458,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89"
dependencies = [
"bitflags 2.4.2",
"errno",
"errno 0.3.8",
"itoa",
"libc",
"linux-raw-sys 0.4.12",
@@ -8369,7 +8472,7 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a25c3aad9fc1424eb82c88087789a7d938e1829724f3e4043163baf0d13cfc12"
dependencies = [
"errno",
"errno 0.3.8",
"libc",
"rustix 0.38.32",
]
@@ -10136,18 +10239,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.48"
version = "1.0.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7"
checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.48"
version = "1.0.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35"
checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524"
dependencies = [
"proc-macro2",
"quote",
@@ -11293,9 +11396,9 @@ checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
[[package]]
name = "walkdir"
version = "2.4.0"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
@@ -12756,7 +12859,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.135.2"
version = "0.136.0"
dependencies = [
"activity_indicator",
"anyhow",
@@ -12773,7 +12876,6 @@ dependencies = [
"clap 4.4.4",
"cli",
"client",
"clock",
"collab_ui",
"collections",
"command_palette",
@@ -12804,6 +12906,7 @@ dependencies = [
"language_selector",
"language_tools",
"languages",
"libc",
"log",
"markdown_preview",
"menu",

View File

@@ -4,8 +4,8 @@ members = [
"crates/anthropic",
"crates/assets",
"crates/assistant",
"crates/assistant_tooling",
"crates/assistant2",
"crates/assistant_tooling",
"crates/audio",
"crates/auto_update",
"crates/breadcrumbs",
@@ -52,6 +52,7 @@ members = [
"crates/live_kit_client",
"crates/live_kit_server",
"crates/lsp",
"crates/markdown",
"crates/markdown_preview",
"crates/media",
"crates/menu",
@@ -192,6 +193,7 @@ languages = { path = "crates/languages" }
live_kit_client = { path = "crates/live_kit_client" }
live_kit_server = { path = "crates/live_kit_server" }
lsp = { path = "crates/lsp" }
markdown = { path = "crates/markdown" }
markdown_preview = { path = "crates/markdown_preview" }
media = { path = "crates/media" }
menu = { path = "crates/menu" }
@@ -225,7 +227,7 @@ snippet = { path = "crates/snippet" }
sqlez = { path = "crates/sqlez" }
sqlez_macros = { path = "crates/sqlez_macros" }
supermaven = { path = "crates/supermaven" }
supermaven_api = { path = "crates/supermaven_api"}
supermaven_api = { path = "crates/supermaven_api" }
story = { path = "crates/story" }
storybook = { path = "crates/storybook" }
sum_tree = { path = "crates/sum_tree" }
@@ -255,20 +257,23 @@ async-fs = "1.6"
async-recursion = "1.0.0"
async-tar = "0.4.2"
async-trait = "0.1"
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
bitflags = "2.4.2"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "f5766863de9dcc092e90fdbbc5e0007a99e7f9bf" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "f5766863de9dcc092e90fdbbc5e0007a99e7f9bf" }
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "e35b2d41f221a48b75f7cf2e78a81e7ecb7a383c" }
cap-std = "3.0"
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4.4", features = ["derive"] }
clickhouse = { version = "0.11.6" }
ctor = "0.2.6"
ctrlc = "3.4.4"
signal-hook = "0.3.17"
core-foundation = { version = "0.9.3" }
core-foundation-sys = "0.8.6"
derive_more = "0.99.17"
emojis = "0.6.1"
env_logger = "0.9"
exec = "0.3.1"
fork = "0.1.23"
futures = "0.3"
futures-batch = "0.6.1"
futures-lite = "1.13"
@@ -287,10 +292,12 @@ isahc = { version = "1.7.2", default-features = false, features = [
] }
itertools = "0.11.0"
lazy_static = "1.4.0"
libc = "0.2"
linkify = "0.10.0"
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
nanoid = "0.4"
nix = "0.28"
once_cell = "1.19.0"
ordered-float = "2.1.1"
palette = { version = "0.7.5", default-features = false, features = ["std"] }
parking_lot = "0.12.1"
@@ -304,6 +311,7 @@ pulldown-cmark = { version = "0.10.0", default-features = false }
rand = "0.8.5"
refineable = { path = "./crates/refineable" }
regex = "1.5"
repair_json = "0.1.0"
rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
rust-embed = { version = "8.0", features = ["include-exclude"] }
schemars = "0.8"
@@ -383,6 +391,8 @@ version = "0.53.0"
features = [
"implement",
"Foundation_Numerics",
"System",
"System_Threading",
"Wdk_System_SystemServices",
"Win32_Globalization",
"Win32_Graphics_Direct2D",
@@ -406,6 +416,7 @@ features = [
"Win32_System_SystemServices",
"Win32_System_Threading",
"Win32_System_Time",
"Win32_System_WinRT",
"Win32_UI_Controls",
"Win32_UI_HiDpi",
"Win32_UI_Input_Ime",

View File

@@ -117,6 +117,9 @@
}
}
],
"m": ["vim::PushOperator", "Mark"],
"'": ["vim::PushOperator", { "Jump": { "line": true } }],
"`": ["vim::PushOperator", { "Jump": { "line": false } }],
";": "vim::RepeatFind",
",": "vim::RepeatFindReversed",
"ctrl-o": "pane::GoBack",
@@ -237,6 +240,9 @@
],
"g ]": "editor::GoToDiagnostic",
"g [": "editor::GoToPrevDiagnostic",
"g i": ["workspace::SendKeystrokes", "` ^ i"],
"g ,": "vim::ChangeListNewer",
"g ;": "vim::ChangeListOlder",
"shift-h": "vim::WindowTop",
"shift-m": "vim::WindowMiddle",
"shift-l": "vim::WindowBottom",

View File

@@ -316,6 +316,8 @@
"autosave": "off",
// Settings related to the editor's tab bar.
"tab_bar": {
// Whether or not to show the tab bar in the editor
"show": true,
// Whether or not to show the navigation history buttons.
"show_nav_history_buttons": true
},
@@ -626,6 +628,9 @@
"Make": {
"hard_tabs": true
},
"Markdown": {
"format_on_save": "off"
},
"Prisma": {
"tab_size": 2
}

View File

@@ -281,11 +281,14 @@ impl ActivityIndicator {
message: "Installing Zed update…".to_string(),
on_click: None,
},
AutoUpdateStatus::Updated => Content {
AutoUpdateStatus::Updated { binary_path } => Content {
icon: None,
message: "Click to restart and update Zed".to_string(),
on_click: Some(Arc::new(|_, cx| {
workspace::restart(&Default::default(), cx)
on_click: Some(Arc::new({
let restart = workspace::Restart {
binary_path: Some(binary_path.clone()),
};
move |_, cx| workspace::restart(&restart, cx)
})),
},
AutoUpdateStatus::Errored => Content {

View File

@@ -19,6 +19,7 @@ stories = ["dep:story"]
anyhow.workspace = true
assistant_tooling.workspace = true
client.workspace = true
chrono.workspace = true
collections.workspace = true
editor.workspace = true
feature_flags.workspace = true
@@ -28,10 +29,10 @@ fuzzy.workspace = true
gpui.workspace = true
language.workspace = true
log.workspace = true
nanoid.workspace = true
open_ai.workspace = true
picker.workspace = true
project.workspace = true
regex.workspace = true
rich_text.workspace = true
schemars.workspace = true
semantic_index.workspace = true

View File

@@ -2,22 +2,18 @@ mod assistant_settings;
mod attachments;
mod completion_provider;
mod saved_conversation;
mod saved_conversation_picker;
mod saved_conversations;
mod tools;
pub mod ui;
use crate::saved_conversation::{SavedConversation, SavedMessage, SavedMessageRole};
use crate::saved_conversation_picker::SavedConversationPicker;
use crate::{
attachments::ActiveEditorAttachmentTool,
tools::{CreateBufferTool, ProjectIndexTool},
ui::UserOrAssistant,
};
use crate::saved_conversation::SavedConversationMetadata;
use crate::ui::UserOrAssistant;
use ::ui::{div, prelude::*, Color, Tooltip, ViewContext};
use anyhow::{Context, Result};
use assistant_tooling::{
AttachmentRegistry, ProjectContext, ToolFunctionCall, ToolRegistry, UserAttachment,
};
use attachments::ActiveEditorAttachmentTool;
use client::{proto, Client, UserStore};
use collections::HashMap;
use completion_provider::*;
@@ -32,11 +28,13 @@ use gpui::{
use language::{language_settings::SoftWrap, LanguageRegistry};
use open_ai::{FunctionContent, ToolCall, ToolCallContent};
use rich_text::RichText;
use saved_conversation::{SavedAssistantMessagePart, SavedChatMessage, SavedConversation};
use saved_conversations::SavedConversations;
use semantic_index::{CloudEmbeddingProvider, ProjectIndex, ProjectIndexDebugView, SemanticIndex};
use serde::{Deserialize, Serialize};
use settings::Settings;
use std::sync::Arc;
use tools::AnnotationTool;
use tools::{AnnotationTool, CreateBufferTool, ProjectIndexTool};
use ui::{ActiveFileButton, Composer, ProjectIndexButton};
use util::paths::CONVERSATIONS_DIR;
use util::{maybe, paths::EMBEDDINGS_DIR, ResultExt};
@@ -50,28 +48,9 @@ pub use assistant_settings::AssistantSettings;
const MAX_COMPLETION_CALLS_PER_SUBMISSION: usize = 5;
#[derive(Eq, PartialEq, Copy, Clone, Deserialize)]
pub struct Submit(SubmitMode);
pub struct Submit;
/// There are multiple different ways to submit a model request, represented by this enum.
#[derive(Eq, PartialEq, Copy, Clone, Deserialize)]
pub enum SubmitMode {
/// Only include the conversation.
Simple,
/// Send the current file as context.
CurrentFile,
/// Search the codebase and send relevant excerpts.
Codebase,
}
gpui::actions!(
assistant2,
[
Cancel,
ToggleFocus,
DebugProjectIndex,
ToggleSavedConversations
]
);
gpui::actions!(assistant2, [Cancel, ToggleFocus, DebugProjectIndex,]);
gpui::impl_actions!(assistant2, [Submit]);
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
@@ -111,8 +90,6 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
},
)
.detach();
cx.observe_new_views(SavedConversationPicker::register)
.detach();
}
pub fn enabled(cx: &AppContext) -> bool {
@@ -141,16 +118,13 @@ impl AssistantPanel {
let mut tool_registry = ToolRegistry::new();
tool_registry
.register(ProjectIndexTool::new(project_index.clone()), cx)
.register(ProjectIndexTool::new(project_index.clone()))
.unwrap();
tool_registry
.register(
CreateBufferTool::new(workspace.clone(), project.clone()),
cx,
)
.register(CreateBufferTool::new(workspace.clone(), project.clone()))
.unwrap();
tool_registry
.register(AnnotationTool::new(workspace.clone(), project.clone()), cx)
.register(AnnotationTool::new(workspace.clone(), project.clone()))
.unwrap();
let mut attachment_registry = AttachmentRegistry::new();
@@ -264,6 +238,8 @@ pub struct AssistantChat {
fs: Arc<dyn Fs>,
language_registry: Arc<LanguageRegistry>,
composer_editor: View<Editor>,
saved_conversations: View<SavedConversations>,
saved_conversations_open: bool,
project_index_button: View<ProjectIndexButton>,
active_file_button: Option<View<ActiveFileButton>>,
user_store: Model<UserStore>,
@@ -319,6 +295,24 @@ impl AssistantChat {
_ => None,
};
let saved_conversations = cx.new_view(|cx| SavedConversations::new(cx));
cx.spawn({
let fs = fs.clone();
let saved_conversations = saved_conversations.downgrade();
|_assistant_chat, mut cx| async move {
let saved_conversation_metadata = SavedConversationMetadata::list(fs).await?;
cx.update(|cx| {
saved_conversations.update(cx, |this, cx| {
this.init(saved_conversation_metadata, cx);
})
})??;
anyhow::Ok(())
}
})
.detach_and_log_err(cx);
Self {
model,
messages: Vec::new(),
@@ -328,6 +322,8 @@ impl AssistantChat {
editor.set_placeholder_text("Send a message…", cx);
editor
}),
saved_conversations,
saved_conversations_open: false,
list_state,
user_store,
fs,
@@ -359,6 +355,10 @@ impl AssistantChat {
})
}
fn toggle_saved_conversations(&mut self) {
self.saved_conversations_open = !self.saved_conversations_open;
}
fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
// If we're currently editing a message, cancel the edit.
if let Some(editing_message) = self.editing_message.take() {
@@ -380,7 +380,7 @@ impl AssistantChat {
cx.propagate();
}
fn submit(&mut self, Submit(mode): &Submit, cx: &mut ViewContext<Self>) {
fn submit(&mut self, _: &Submit, cx: &mut ViewContext<Self>) {
if let Some(focused_message_id) = self.focused_message_id(cx) {
self.truncate_messages(focused_message_id, cx);
self.pending_completion.take();
@@ -418,7 +418,6 @@ impl AssistantChat {
return;
}
let mode = *mode;
self.pending_completion = Some(cx.spawn(move |this, mut cx| async move {
let attachments_task = this.update(&mut cx, |this, cx| {
let attachment_registry = this.attachment_registry.clone();
@@ -443,14 +442,9 @@ impl AssistantChat {
})
.log_err();
Self::request_completion(
this.clone(),
mode,
MAX_COMPLETION_CALLS_PER_SUBMISSION,
&mut cx,
)
.await
.log_err();
Self::request_completion(this.clone(), MAX_COMPLETION_CALLS_PER_SUBMISSION, &mut cx)
.await
.log_err();
this.update(&mut cx, |this, _cx| {
this.pending_completion = None;
@@ -462,7 +456,6 @@ impl AssistantChat {
async fn request_completion(
this: WeakView<Self>,
mode: SubmitMode,
limit: usize,
cx: &mut AsyncWindowContext,
) -> Result<()> {
@@ -472,9 +465,7 @@ impl AssistantChat {
let (tool_definitions, model_name, messages) = this.update(cx, |this, cx| {
this.push_new_assistant_message(cx);
let definitions = if call_count < limit
&& matches!(mode, SubmitMode::Codebase | SubmitMode::Simple)
{
let definitions = if call_count < limit {
this.tool_registry.definitions()
} else {
Vec::new()
@@ -505,13 +496,11 @@ impl AssistantChat {
while let Some(delta) = stream.next().await {
let delta = delta?;
this.update(cx, |this, cx| {
if let Some(ChatMessage::Assistant(GroupedAssistantMessage {
messages,
..
})) = this.messages.last_mut()
if let Some(ChatMessage::Assistant(AssistantMessage { messages, .. })) =
this.messages.last_mut()
{
if messages.is_empty() {
messages.push(AssistantMessage {
messages.push(AssistantMessagePart {
body: RichText::default(),
tool_calls: Vec::new(),
})
@@ -523,25 +512,27 @@ impl AssistantChat {
body.push_str(content);
}
for tool_call in delta.tool_calls {
let index = tool_call.index as usize;
for tool_call_delta in delta.tool_calls {
let index = tool_call_delta.index as usize;
if index >= message.tool_calls.len() {
message.tool_calls.resize_with(index + 1, Default::default);
}
let call = &mut message.tool_calls[index];
let tool_call = &mut message.tool_calls[index];
if let Some(id) = &tool_call.id {
call.id.push_str(id);
if let Some(id) = &tool_call_delta.id {
tool_call.id.push_str(id);
}
match tool_call.variant {
Some(proto::tool_call_delta::Variant::Function(tool_call)) => {
if let Some(name) = &tool_call.name {
call.name.push_str(name);
}
if let Some(arguments) = &tool_call.arguments {
call.arguments.push_str(arguments);
}
match tool_call_delta.variant {
Some(proto::tool_call_delta::Variant::Function(
tool_call_delta,
)) => {
this.tool_registry.update_tool_call(
tool_call,
tool_call_delta.name.as_deref(),
tool_call_delta.arguments.as_deref(),
cx,
);
}
None => {}
}
@@ -562,7 +553,7 @@ impl AssistantChat {
let mut tool_tasks = Vec::new();
this.update(cx, |this, cx| {
if let Some(ChatMessage::Assistant(GroupedAssistantMessage {
if let Some(ChatMessage::Assistant(AssistantMessage {
error: message_error,
messages,
..
@@ -573,53 +564,39 @@ impl AssistantChat {
cx.notify();
} else {
if let Some(current_message) = messages.last_mut() {
for tool_call in current_message.tool_calls.iter() {
tool_tasks.push(this.tool_registry.call(tool_call, cx));
for tool_call in current_message.tool_calls.iter_mut() {
tool_tasks
.extend(this.tool_registry.execute_tool_call(tool_call, cx));
}
}
}
}
})?;
// This ends recursion on calling for responses after tools
if tool_tasks.is_empty() {
return Ok(());
}
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(GroupedAssistantMessage { messages, .. })) =
this.messages.last_mut()
{
if let Some(current_message) = messages.last_mut() {
current_message.tool_calls = tools;
cx.notify();
} else {
unreachable!()
}
}
})?;
join_all(tool_tasks.into_iter()).await;
}
}
fn push_new_assistant_message(&mut self, cx: &mut ViewContext<Self>) {
// If the last message is a grouped assistant message, add to the grouped message
if let Some(ChatMessage::Assistant(GroupedAssistantMessage { messages, .. })) =
if let Some(ChatMessage::Assistant(AssistantMessage { messages, .. })) =
self.messages.last_mut()
{
messages.push(AssistantMessage {
messages.push(AssistantMessagePart {
body: RichText::default(),
tool_calls: Vec::new(),
});
return;
}
let message = ChatMessage::Assistant(GroupedAssistantMessage {
let message = ChatMessage::Assistant(AssistantMessage {
id: self.next_message_id.post_inc(),
messages: vec![AssistantMessage {
messages: vec![AssistantMessagePart {
body: RichText::default(),
tool_calls: Vec::new(),
}],
@@ -668,40 +645,30 @@ impl AssistantChat {
*entry = !*entry;
}
fn new_conversation(&mut self, cx: &mut ViewContext<Self>) {
let messages = self
.messages
.drain(..)
.map(|message| {
let text = match &message {
ChatMessage::User(message) => message.body.read(cx).text(cx),
ChatMessage::Assistant(message) => message
.messages
.iter()
.map(|message| message.body.text.to_string())
.collect::<Vec<_>>()
.join("\n\n"),
};
SavedMessage {
id: message.id(),
role: match message {
ChatMessage::User(_) => SavedMessageRole::User,
ChatMessage::Assistant(_) => SavedMessageRole::Assistant,
},
text,
}
})
.collect::<Vec<_>>();
// Reset the chat for the new conversation.
fn reset(&mut self) {
self.messages.clear();
self.list_state.reset(0);
self.editing_message.take();
self.collapsed_messages.clear();
}
fn new_conversation(&mut self, cx: &mut ViewContext<Self>) {
let messages = std::mem::take(&mut self.messages)
.into_iter()
.map(|message| self.serialize_message(message, cx))
.collect::<Vec<_>>();
self.reset();
let title = messages
.first()
.map(|message| message.text.clone())
.map(|message| match message {
SavedChatMessage::User { body, .. } => body.clone(),
SavedChatMessage::Assistant { messages, .. } => messages
.first()
.map(|message| message.body.to_string())
.unwrap_or_default(),
})
.unwrap_or_else(|| "A conversation with the assistant.".to_string());
let saved_conversation = SavedConversation {
@@ -835,7 +802,7 @@ impl AssistantChat {
}
})
.into_any(),
ChatMessage::Assistant(GroupedAssistantMessage {
ChatMessage::Assistant(AssistantMessage {
id,
messages,
error,
@@ -856,7 +823,7 @@ impl AssistantChat {
let tools = message
.tool_calls
.iter()
.map(|tool_call| self.tool_registry.render_tool_call(tool_call, cx))
.filter_map(|tool_call| self.tool_registry.render_tool_call(tool_call, cx))
.collect::<Vec<AnyElement>>();
if !tools.is_empty() {
@@ -864,6 +831,10 @@ impl AssistantChat {
}
}
if message_elements.is_empty() {
message_elements.push(::ui::Label::new("Researching...").into_any_element())
}
div()
.when(is_first, |this| this.pt(padding))
.child(
@@ -896,6 +867,21 @@ impl AssistantChat {
let mut project_context = ProjectContext::new(project, fs);
let mut completion_messages = Vec::new();
completion_messages.push(CompletionMessage::System {
content: r#"
You are the assistant for the Zed code editor.
Your job is to help the user understand and modify their own code.
Use tools to retrieve the information needed to give answers that are
specific to the user's codebase. Do NOT give generic answers that are
not specific to the user's codebase.
Whenever possible, use tools to display code in the editor.
"#
.lines()
.map(|line| line.trim_start())
.filter(|line| !line.is_empty())
.collect(),
});
for message in &self.messages {
match message {
ChatMessage::User(UserMessage {
@@ -912,7 +898,7 @@ impl AssistantChat {
content: body.read(cx).text(cx),
});
}
ChatMessage::Assistant(GroupedAssistantMessage { messages, .. }) => {
ChatMessage::Assistant(AssistantMessage { messages, .. }) => {
for message in messages {
let body = message.body.clone();
@@ -941,13 +927,11 @@ impl AssistantChat {
for tool_call in &message.tool_calls {
// Every tool call _must_ have a result by ID, otherwise OpenAI will error.
let content = match &tool_call.result {
Some(result) => {
result.generate(&tool_call.name, &mut project_context, cx)
}
None => "".to_string(),
};
let content = self.tool_registry.content_for_tool_call(
tool_call,
&mut project_context,
cx,
);
completion_messages.push(CompletionMessage::Tool {
content,
tool_call_id: tool_call.id.clone(),
@@ -966,10 +950,53 @@ impl AssistantChat {
Ok(completion_messages)
})
}
fn serialize_message(
&self,
message: ChatMessage,
cx: &mut ViewContext<AssistantChat>,
) -> SavedChatMessage {
match message {
ChatMessage::User(message) => SavedChatMessage::User {
id: message.id,
body: message.body.read(cx).text(cx),
attachments: message
.attachments
.iter()
.map(|attachment| {
self.attachment_registry
.serialize_user_attachment(attachment)
})
.collect(),
},
ChatMessage::Assistant(message) => SavedChatMessage::Assistant {
id: message.id,
error: message.error,
messages: message
.messages
.iter()
.map(|message| SavedAssistantMessagePart {
body: message.body.text.clone(),
tool_calls: message
.tool_calls
.iter()
.filter_map(|tool_call| {
self.tool_registry
.serialize_tool_call(tool_call, cx)
.log_err()
})
.collect(),
})
.collect(),
},
}
}
}
impl Render for AssistantChat {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let header_height = Spacing::Small.rems(cx) * 2.0 + ButtonSize::Default.rems();
div()
.relative()
.flex_1()
@@ -978,23 +1005,60 @@ impl Render for AssistantChat {
.on_action(cx.listener(Self::submit))
.on_action(cx.listener(Self::cancel))
.text_color(Color::Default.color(cx))
.child(list(self.list_state.clone()).flex_1().pt(header_height))
.child(
h_flex()
.gap_2()
.absolute()
.top_0()
.justify_between()
.w_full()
.h(header_height)
.p(Spacing::Small.rems(cx))
.child(
Button::new("open-saved-conversations", "Saved Conversations").on_click(
|_event, cx| cx.dispatch_action(Box::new(ToggleSavedConversations)),
),
IconButton::new(
"toggle-saved-conversations",
if self.saved_conversations_open {
IconName::ChevronRight
} else {
IconName::ChevronLeft
},
)
.on_click(cx.listener(|this, _event, _cx| {
this.toggle_saved_conversations();
}))
.tooltip(move |cx| Tooltip::text("Switch Conversations", cx)),
)
.child(
IconButton::new("new-conversation", IconName::Plus)
.on_click(cx.listener(move |this, _event, cx| {
this.new_conversation(cx);
}))
.tooltip(move |cx| Tooltip::text("New Conversation", cx)),
h_flex()
.gap(Spacing::Large.rems(cx))
.child(
IconButton::new("new-conversation", IconName::Plus)
.on_click(cx.listener(move |this, _event, cx| {
this.new_conversation(cx);
}))
.tooltip(move |cx| Tooltip::text("New Conversation", cx)),
)
.child(
IconButton::new("assistant-menu", IconName::Menu)
.disabled(true)
.tooltip(move |cx| {
Tooltip::text(
"Coming soon Assistant settings & controls",
cx,
)
}),
),
),
)
.child(list(self.list_state.clone()).flex_1())
.when(self.saved_conversations_open, |element| {
element.child(
h_flex()
.absolute()
.top(header_height)
.w_full()
.child(self.saved_conversations.clone()),
)
})
.child(Composer::new(
self.composer_editor.clone(),
self.project_index_button.clone(),
@@ -1018,17 +1082,10 @@ impl MessageId {
enum ChatMessage {
User(UserMessage),
Assistant(GroupedAssistantMessage),
Assistant(AssistantMessage),
}
impl ChatMessage {
pub fn id(&self) -> MessageId {
match self {
ChatMessage::User(message) => message.id,
ChatMessage::Assistant(message) => message.id,
}
}
fn focus_handle(&self, cx: &AppContext) -> Option<FocusHandle> {
match self {
ChatMessage::User(UserMessage { body, .. }) => Some(body.focus_handle(cx)),
@@ -1038,18 +1095,18 @@ impl ChatMessage {
}
struct UserMessage {
id: MessageId,
body: View<Editor>,
attachments: Vec<UserAttachment>,
pub id: MessageId,
pub body: View<Editor>,
pub attachments: Vec<UserAttachment>,
}
struct AssistantMessagePart {
pub body: RichText,
pub tool_calls: Vec<ToolFunctionCall>,
}
struct AssistantMessage {
body: RichText,
tool_calls: Vec<ToolFunctionCall>,
}
struct GroupedAssistantMessage {
id: MessageId,
messages: Vec<AssistantMessage>,
error: Option<SharedString>,
pub id: MessageId,
pub messages: Vec<AssistantMessagePart>,
pub error: Option<SharedString>,
}

View File

@@ -1,64 +1,68 @@
use std::{path::PathBuf, sync::Arc};
use anyhow::{anyhow, Result};
use assistant_tooling::{LanguageModelAttachment, ProjectContext, ToolOutput};
use assistant_tooling::{AttachmentOutput, LanguageModelAttachment, ProjectContext};
use editor::Editor;
use gpui::{Render, Task, View, WeakModel, WeakView};
use language::Buffer;
use project::ProjectPath;
use serde::{Deserialize, Serialize};
use ui::{prelude::*, ButtonLike, Tooltip, WindowContext};
use util::maybe;
use workspace::Workspace;
#[derive(Serialize, Deserialize)]
pub struct ActiveEditorAttachment {
buffer: WeakModel<Buffer>,
path: Option<ProjectPath>,
#[serde(skip)]
buffer: Option<WeakModel<Buffer>>,
path: Option<PathBuf>,
}
pub struct FileAttachmentView {
output: Result<ActiveEditorAttachment>,
project_path: Option<ProjectPath>,
buffer: Option<WeakModel<Buffer>>,
error: Option<anyhow::Error>,
}
impl Render for FileAttachmentView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
match &self.output {
Ok(attachment) => {
let filename: SharedString = attachment
.path
.as_ref()
.and_then(|p| p.path.file_name()?.to_str())
.unwrap_or("Untitled")
.to_string()
.into();
// todo!(): make the button link to the actual file to open
ButtonLike::new("file-attachment")
.child(
h_flex()
.gap_1()
.bg(cx.theme().colors().editor_background)
.rounded_md()
.child(ui::Icon::new(IconName::File))
.child(filename.clone()),
)
.tooltip({
move |cx| Tooltip::with_meta("File Attached", None, filename.clone(), cx)
})
.into_any_element()
}
Err(err) => div().child(err.to_string()).into_any_element(),
if let Some(error) = &self.error {
return div().child(error.to_string()).into_any_element();
}
let filename: SharedString = self
.project_path
.as_ref()
.and_then(|p| p.path.file_name()?.to_str())
.unwrap_or("Untitled")
.to_string()
.into();
ButtonLike::new("file-attachment")
.child(
h_flex()
.gap_1()
.bg(cx.theme().colors().editor_background)
.rounded_md()
.child(ui::Icon::new(IconName::File))
.child(filename.clone()),
)
.tooltip(move |cx| Tooltip::with_meta("File Attached", None, filename.clone(), cx))
.into_any_element()
}
}
impl ToolOutput for FileAttachmentView {
impl AttachmentOutput for FileAttachmentView {
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String {
if let Ok(result) = &self.output {
if let Some(path) = &result.path {
project.add_file(path.clone());
return format!("current file: {}", path.path.display());
} else if let Some(buffer) = result.buffer.upgrade() {
return format!("current untitled buffer text:\n{}", buffer.read(cx).text());
}
if let Some(path) = &self.project_path {
project.add_file(path.clone());
return format!("current file: {}", path.path.display());
}
if let Some(buffer) = self.buffer.as_ref().and_then(|buffer| buffer.upgrade()) {
return format!("current untitled buffer text:\n{}", buffer.read(cx).text());
}
String::new()
}
}
@@ -77,6 +81,10 @@ impl LanguageModelAttachment for ActiveEditorAttachmentTool {
type Output = ActiveEditorAttachment;
type View = FileAttachmentView;
fn name(&self) -> Arc<str> {
"active-editor-attachment".into()
}
fn run(&self, cx: &mut WindowContext) -> Task<Result<ActiveEditorAttachment>> {
Task::ready(maybe!({
let active_buffer = self
@@ -91,13 +99,10 @@ impl LanguageModelAttachment for ActiveEditorAttachmentTool {
let buffer = active_buffer.read(cx);
if let Some(buffer) = buffer.as_singleton() {
let path =
project::File::from_dyn(buffer.read(cx).file()).map(|file| ProjectPath {
worktree_id: file.worktree_id(cx),
path: file.path.clone(),
});
let path = project::File::from_dyn(buffer.read(cx).file())
.and_then(|file| file.worktree.read(cx).absolutize(&file.path).ok());
return Ok(ActiveEditorAttachment {
buffer: buffer.downgrade(),
buffer: Some(buffer.downgrade()),
path,
});
} else {
@@ -106,7 +111,34 @@ impl LanguageModelAttachment for ActiveEditorAttachmentTool {
}))
}
fn view(output: Result<Self::Output>, cx: &mut WindowContext) -> View<Self::View> {
cx.new_view(|_cx| FileAttachmentView { output })
fn view(
&self,
output: Result<ActiveEditorAttachment>,
cx: &mut WindowContext,
) -> View<Self::View> {
let error;
let project_path;
let buffer;
match output {
Ok(output) => {
error = None;
let workspace = self.workspace.upgrade().unwrap();
let project = workspace.read(cx).project();
project_path = output
.path
.and_then(|path| project.read(cx).project_path_for_absolute_path(&path, cx));
buffer = output.buffer;
}
Err(err) => {
error = Some(err);
buffer = None;
project_path = None;
}
}
cx.new_view(|_cx| FileAttachmentView {
project_path,
buffer,
error,
})
}
}

View File

@@ -1,4 +1,16 @@
use std::cmp::Reverse;
use std::ffi::OsStr;
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::Result;
use assistant_tooling::{SavedToolFunctionCall, SavedUserAttachment};
use fs::Fs;
use futures::StreamExt;
use gpui::SharedString;
use regex::Regex;
use serde::{Deserialize, Serialize};
use util::paths::CONVERSATIONS_DIR;
use crate::MessageId;
@@ -8,42 +20,71 @@ pub struct SavedConversation {
pub version: String,
/// The title of the conversation, generated by the Assistant.
pub title: String,
pub messages: Vec<SavedMessage>,
pub messages: Vec<SavedChatMessage>,
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum SavedMessageRole {
User,
Assistant,
pub enum SavedChatMessage {
User {
id: MessageId,
body: String,
attachments: Vec<SavedUserAttachment>,
},
Assistant {
id: MessageId,
messages: Vec<SavedAssistantMessagePart>,
error: Option<SharedString>,
},
}
#[derive(Serialize, Deserialize)]
pub struct SavedMessage {
pub id: MessageId,
pub role: SavedMessageRole,
pub text: String,
pub struct SavedAssistantMessagePart {
pub body: SharedString,
pub tool_calls: Vec<SavedToolFunctionCall>,
}
/// Returns a list of placeholder conversations for mocking the UI.
///
/// Once we have real saved conversations to pull from we can use those instead.
pub fn placeholder_conversations() -> Vec<SavedConversation> {
vec![
SavedConversation {
version: "0.3.0".to_string(),
title: "How to get a list of exported functions in an Erlang module".to_string(),
messages: vec![],
},
SavedConversation {
version: "0.3.0".to_string(),
title: "7 wonders of the ancient world".to_string(),
messages: vec![],
},
SavedConversation {
version: "0.3.0".to_string(),
title: "Size difference between u8 and a reference to u8 in Rust".to_string(),
messages: vec![],
},
]
pub struct SavedConversationMetadata {
pub title: String,
pub path: PathBuf,
pub mtime: chrono::DateTime<chrono::Local>,
}
impl SavedConversationMetadata {
pub async fn list(fs: Arc<dyn Fs>) -> Result<Vec<Self>> {
fs.create_dir(&CONVERSATIONS_DIR).await?;
let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?;
let mut conversations = Vec::new();
while let Some(path) = paths.next().await {
let path = path?;
if path.extension() != Some(OsStr::new("json")) {
continue;
}
let pattern = r" - \d+.zed.\d.\d.\d.json$";
let re = Regex::new(pattern).unwrap();
let metadata = fs.metadata(&path).await?;
if let Some((file_name, metadata)) = path
.file_name()
.and_then(|name| name.to_str())
.zip(metadata)
{
// This is used to filter out conversations saved by the old assistant.
if !re.is_match(file_name) {
continue;
}
let title = re.replace(file_name, "");
conversations.push(Self {
title: title.into_owned(),
path,
mtime: metadata.mtime.into(),
});
}
}
conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime));
Ok(conversations)
}
}

View File

@@ -5,57 +5,66 @@ use gpui::{AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, V
use picker::{Picker, PickerDelegate};
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
use util::ResultExt;
use workspace::{ModalView, Workspace};
use crate::saved_conversation::{self, SavedConversation};
use crate::ToggleSavedConversations;
use crate::saved_conversation::SavedConversationMetadata;
pub struct SavedConversationPicker {
picker: View<Picker<SavedConversationPickerDelegate>>,
pub struct SavedConversations {
focus_handle: FocusHandle,
picker: Option<View<Picker<SavedConversationPickerDelegate>>>,
}
impl EventEmitter<DismissEvent> for SavedConversationPicker {}
impl EventEmitter<DismissEvent> for SavedConversations {}
impl ModalView for SavedConversationPicker {}
impl FocusableView for SavedConversationPicker {
impl FocusableView for SavedConversations {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx)
if let Some(picker) = self.picker.as_ref() {
picker.focus_handle(cx)
} else {
self.focus_handle.clone()
}
}
}
impl SavedConversationPicker {
pub fn register(workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>) {
workspace.register_action(|workspace, _: &ToggleSavedConversations, cx| {
workspace.toggle_modal(cx, move |cx| {
let delegate = SavedConversationPickerDelegate::new(cx.view().downgrade());
Self::new(delegate, cx)
});
});
impl SavedConversations {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
Self {
focus_handle: cx.focus_handle(),
picker: None,
}
}
pub fn new(delegate: SavedConversationPickerDelegate, cx: &mut ViewContext<Self>) -> Self {
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
Self { picker }
pub fn init(
&mut self,
saved_conversations: Vec<SavedConversationMetadata>,
cx: &mut ViewContext<Self>,
) {
let delegate =
SavedConversationPickerDelegate::new(cx.view().downgrade(), saved_conversations);
self.picker = Some(cx.new_view(|cx| Picker::uniform_list(delegate, cx).modal(false)));
}
}
impl Render for SavedConversationPicker {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
v_flex().w(rems(34.)).child(self.picker.clone())
impl Render for SavedConversations {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
v_flex()
.w_full()
.bg(cx.theme().colors().panel_background)
.children(self.picker.clone())
}
}
pub struct SavedConversationPickerDelegate {
view: WeakView<SavedConversationPicker>,
saved_conversations: Vec<SavedConversation>,
view: WeakView<SavedConversations>,
saved_conversations: Vec<SavedConversationMetadata>,
selected_index: usize,
matches: Vec<StringMatch>,
}
impl SavedConversationPickerDelegate {
pub fn new(weak_view: WeakView<SavedConversationPicker>) -> Self {
let saved_conversations = saved_conversation::placeholder_conversations();
pub fn new(
weak_view: WeakView<SavedConversations>,
saved_conversations: Vec<SavedConversationMetadata>,
) -> Self {
let matches = saved_conversations
.iter()
.map(|conversation| StringMatch {
@@ -176,7 +185,6 @@ impl PickerDelegate for SavedConversationPickerDelegate {
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.child(HighlightedLabel::new(

View File

@@ -4,9 +4,10 @@ use editor::{
display_map::{BlockContext, BlockDisposition, BlockProperties, BlockStyle},
Editor, MultiBuffer,
};
use gpui::{prelude::*, AnyElement, Model, Task, View, WeakView};
use futures::{channel::mpsc::UnboundedSender, StreamExt as _};
use gpui::{prelude::*, AnyElement, AsyncWindowContext, Model, Task, View, WeakView};
use language::ToPoint;
use project::{Project, ProjectPath};
use project::{search::SearchQuery, Project, ProjectPath};
use schemars::JsonSchema;
use serde::Deserialize;
use std::path::Path;
@@ -25,135 +26,177 @@ impl AnnotationTool {
}
}
#[derive(Debug, Deserialize, JsonSchema, Clone)]
#[derive(Default, Debug, Deserialize, JsonSchema, Clone)]
pub struct AnnotationInput {
/// Name for this set of annotations
#[serde(default = "default_title")]
title: String,
annotations: Vec<Annotation>,
/// Excerpts from the file to show to the user.
excerpts: Vec<Excerpt>,
}
fn default_title() -> String {
"Untitled".to_string()
}
#[derive(Debug, Deserialize, JsonSchema, Clone)]
struct Annotation {
struct Excerpt {
/// Path to the file
path: String,
/// Name of a symbol in the code
symbol_name: String,
/// Text to display near the symbol definition
text: String,
/// A short, distinctive string that appears in the file, used to define a location in the file.
text_passage: String,
/// Text to display above the code excerpt. All explanation of code should be included here.
annotation: String,
}
impl LanguageModelTool for AnnotationTool {
type Input = AnnotationInput;
type Output = String;
type View = AnnotationResultView;
fn name(&self) -> String {
"annotate_code".to_string()
"show_code_file_excerpts".to_string()
}
fn description(&self) -> String {
"Dynamically annotate symbols in the current codebase. Opens a buffer in a panel in their editor, to the side of the conversation. The annotations are shown in the editor as a block decoration.".to_string()
"
Show and explain code from the current project
Opens a buffer in a separate pane/tab, to the side of the conversation.
The annotations are shown in the editor as block decorations.
Many related excerpts can be shown at once.
"
.to_string()
}
fn execute(&self, input: &Self::Input, cx: &mut WindowContext) -> Task<Result<Self::Output>> {
let workspace = self.workspace.clone();
let project = self.project.clone();
let excerpts = input.annotations.clone();
let title = input.title.clone();
fn view(&self, cx: &mut WindowContext) -> View<Self::View> {
cx.new_view(|cx| {
let (tx, mut rx) = futures::channel::mpsc::unbounded();
cx.spawn(|view, mut cx| async move {
while let Some(excerpt) = rx.next().await {
AnnotationResultView::add_excerpt(view.clone(), excerpt, &mut cx).await?;
}
anyhow::Ok(())
})
.detach();
AnnotationResultView {
project: self.project.clone(),
workspace: self.workspace.clone(),
tx,
pending_excerpt: None,
added_editor_to_workspace: false,
editor: None,
error: None,
rendered_excerpt_count: 0,
}
})
}
}
pub struct AnnotationResultView {
workspace: WeakView<Workspace>,
project: Model<Project>,
pending_excerpt: Option<Excerpt>,
added_editor_to_workspace: bool,
editor: Option<View<Editor>>,
tx: UnboundedSender<Excerpt>,
error: Option<anyhow::Error>,
rendered_excerpt_count: usize,
}
impl AnnotationResultView {
async fn add_excerpt(
this: WeakView<Self>,
excerpt: Excerpt,
cx: &mut AsyncWindowContext,
) -> Result<()> {
let project = this.update(cx, |this, _cx| this.project.clone())?;
let worktree_id = project.update(cx, |project, cx| {
let worktree = project.worktrees().next()?;
let worktree_id = worktree.read(cx).id();
Some(worktree_id)
});
})?;
let worktree_id = if let Some(worktree_id) = worktree_id {
worktree_id
} else {
return Task::ready(Err(anyhow::anyhow!("No worktree found")));
return Err(anyhow::anyhow!("No worktree found"));
};
let buffer_tasks = project.update(cx, |project, cx| {
let excerpts = excerpts.clone();
excerpts
.iter()
.map(|excerpt| {
let project_path = ProjectPath {
worktree_id,
path: Path::new(&excerpt.path).into(),
};
project.open_buffer(project_path.clone(), cx)
let buffer_task = project.update(cx, |project, cx| {
project.open_buffer(
ProjectPath {
worktree_id,
path: Path::new(&excerpt.path).into(),
},
cx,
)
})?;
let buffer = match buffer_task.await {
Ok(buffer) => buffer,
Err(error) => {
return this.update(cx, |this, cx| {
this.error = Some(error);
cx.notify();
})
.collect::<Vec<_>>()
});
}
};
cx.spawn(move |mut cx| async move {
let buffers = futures::future::try_join_all(buffer_tasks).await?;
let snapshot = buffer.update(cx, |buffer, _cx| buffer.snapshot())?;
let query = SearchQuery::text(&excerpt.text_passage, false, false, false, vec![], vec![])?;
let matches = query.search(&snapshot, None).await;
let Some(first_match) = matches.first() else {
log::warn!(
"text {:?} does not appear in '{}'",
excerpt.text_passage,
excerpt.path
);
return Ok(());
};
let multibuffer = cx.new_model(|_cx| {
MultiBuffer::new(0, language::Capability::ReadWrite).with_title(title)
})?;
let editor =
cx.new_view(|cx| Editor::for_multibuffer(multibuffer, Some(project), cx))?;
this.update(cx, |this, cx| {
let mut start = first_match.start.to_point(&snapshot);
start.column = 0;
for (excerpt, buffer) in excerpts.iter().zip(buffers.iter()) {
let snapshot = buffer.update(&mut cx, |buffer, _cx| buffer.snapshot())?;
if let Some(editor) = &this.editor {
editor.update(cx, |editor, cx| {
let ranges = editor.buffer().update(cx, |multibuffer, cx| {
multibuffer.push_excerpts_with_context_lines(
buffer.clone(),
vec![start..start],
5,
cx,
)
});
if let Some(outline) = snapshot.outline(None) {
let matches = outline
.search(&excerpt.symbol_name, cx.background_executor().clone())
.await;
if let Some(mat) = matches.first() {
let item = &outline.items[mat.candidate_id];
let start = item.range.start.to_point(&snapshot);
editor.update(&mut cx, |editor, cx| {
let ranges = editor.buffer().update(cx, |multibuffer, cx| {
multibuffer.push_excerpts_with_context_lines(
buffer.clone(),
vec![start..start],
5,
cx,
)
});
let explanation = SharedString::from(excerpt.text.clone());
editor.insert_blocks(
[BlockProperties {
position: ranges[0].start,
height: 2,
style: BlockStyle::Fixed,
render: Box::new(move |cx| {
Self::render_note_block(&explanation, cx)
}),
disposition: BlockDisposition::Above,
}],
None,
cx,
);
})?;
}
let annotation = SharedString::from(excerpt.annotation);
editor.insert_blocks(
[BlockProperties {
position: ranges[0].start,
height: annotation.split('\n').count() as u8 + 1,
style: BlockStyle::Fixed,
render: Box::new(move |cx| Self::render_note_block(&annotation, cx)),
disposition: BlockDisposition::Above,
}],
None,
cx,
);
});
if !this.added_editor_to_workspace {
this.added_editor_to_workspace = true;
this.workspace
.update(cx, |workspace, cx| {
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, cx);
})
.log_err();
}
}
})?;
workspace
.update(&mut cx, |workspace, cx| {
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, cx);
})
.log_err();
anyhow::Ok("showed comments to users in a new view".into())
})
Ok(())
}
fn output_view(
_: Self::Input,
output: Result<Self::Output>,
cx: &mut WindowContext,
) -> View<Self::View> {
cx.new_view(|_cx| AnnotationResultView { output })
}
}
impl AnnotationTool {
fn render_note_block(explanation: &SharedString, cx: &mut BlockContext) -> AnyElement {
let anchor_x = cx.anchor_x;
let gutter_width = cx.gutter_dimensions.width;
@@ -179,24 +222,89 @@ impl AnnotationTool {
}
}
pub struct AnnotationResultView {
output: Result<String>,
}
impl Render for AnnotationResultView {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
match &self.output {
Ok(output) => div().child(output.clone().into_any_element()),
Err(error) => div().child(format!("failed to open path: {:?}", error)),
if let Some(error) = &self.error {
ui::Label::new(error.to_string()).into_any_element()
} else {
ui::Label::new(SharedString::from(format!(
"Opened a buffer with {} excerpts",
self.rendered_excerpt_count
)))
.into_any_element()
}
}
}
impl ToolOutput for AnnotationResultView {
fn generate(&self, _: &mut ProjectContext, _: &mut WindowContext) -> String {
match &self.output {
Ok(output) => output.clone(),
Err(err) => format!("Failed to create buffer: {err:?}"),
type Input = AnnotationInput;
type SerializedState = Option<String>;
fn generate(&self, _: &mut ProjectContext, _: &mut ViewContext<Self>) -> String {
if let Some(error) = &self.error {
format!("Failed to create buffer: {error:?}")
} else {
format!(
"opened {} excerpts in a buffer",
self.rendered_excerpt_count
)
}
}
fn set_input(&mut self, mut input: Self::Input, cx: &mut ViewContext<Self>) {
let editor = if let Some(editor) = &self.editor {
editor.clone()
} else {
let multibuffer = cx.new_model(|_cx| {
MultiBuffer::new(0, language::Capability::ReadWrite).with_title(String::new())
});
let editor = cx.new_view(|cx| {
Editor::for_multibuffer(multibuffer.clone(), Some(self.project.clone()), cx)
});
self.editor = Some(editor.clone());
editor
};
editor.update(cx, |editor, cx| {
editor.buffer().update(cx, |multibuffer, cx| {
if multibuffer.title(cx) != input.title {
multibuffer.set_title(input.title.clone(), cx);
}
});
self.pending_excerpt = input.excerpts.pop();
for excerpt in input.excerpts.iter().skip(self.rendered_excerpt_count) {
self.tx.unbounded_send(excerpt.clone()).ok();
}
self.rendered_excerpt_count = input.excerpts.len();
});
cx.notify();
}
fn execute(&mut self, _cx: &mut ViewContext<Self>) -> Task<Result<()>> {
if let Some(excerpt) = self.pending_excerpt.take() {
self.rendered_excerpt_count += 1;
self.tx.unbounded_send(excerpt.clone()).ok();
}
self.tx.close_channel();
Task::ready(Ok(()))
}
fn serialize(&self, _cx: &mut ViewContext<Self>) -> Self::SerializedState {
self.error.as_ref().map(|error| error.to_string())
}
fn deserialize(
&mut self,
output: Self::SerializedState,
_cx: &mut ViewContext<Self>,
) -> Result<()> {
if let Some(error_message) = output {
self.error = Some(anyhow::anyhow!("{}", error_message));
}
Ok(())
}
}

View File

@@ -1,4 +1,4 @@
use anyhow::Result;
use anyhow::{anyhow, Result};
use assistant_tooling::{LanguageModelTool, ProjectContext, ToolOutput};
use editor::Editor;
use gpui::{prelude::*, Model, Task, View, WeakView};
@@ -20,7 +20,7 @@ impl CreateBufferTool {
}
}
#[derive(Debug, Deserialize, JsonSchema)]
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct CreateBufferInput {
/// The contents of the buffer.
text: String,
@@ -32,25 +32,69 @@ pub struct CreateBufferInput {
}
impl LanguageModelTool for CreateBufferTool {
type Input = CreateBufferInput;
type Output = ();
type View = CreateBufferView;
fn name(&self) -> String {
"create_buffer".to_string()
"create_new_source_file".to_string()
}
fn description(&self) -> String {
"Create a new buffer in the current codebase".to_string()
"Create a new file in the current codebase. Only use this when generating new code, NOT when showing existing code from the project.".to_string()
}
fn execute(&self, input: &Self::Input, cx: &mut WindowContext) -> Task<Result<Self::Output>> {
fn view(&self, cx: &mut WindowContext) -> View<Self::View> {
cx.new_view(|_cx| CreateBufferView {
workspace: self.workspace.clone(),
project: self.project.clone(),
input: None,
error: None,
})
}
}
pub struct CreateBufferView {
workspace: WeakView<Workspace>,
project: Model<Project>,
input: Option<CreateBufferInput>,
error: Option<anyhow::Error>,
}
impl Render for CreateBufferView {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
div().child("Opening a buffer")
}
}
impl ToolOutput for CreateBufferView {
type Input = CreateBufferInput;
type SerializedState = ();
fn generate(&self, _project: &mut ProjectContext, _cx: &mut ViewContext<Self>) -> String {
let Some(input) = self.input.as_ref() else {
return "No input".to_string();
};
match &self.error {
None => format!("Created a new {} buffer", input.language),
Some(err) => format!("Failed to create buffer: {err:?}"),
}
}
fn set_input(&mut self, input: Self::Input, _cx: &mut ViewContext<Self>) {
self.input = Some(input);
}
fn execute(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
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 input = self.input.clone();
|_this, mut cx| async move {
let input = input.ok_or_else(|| anyhow!("no input"))?;
let text = input.text.clone();
let language_name = input.language.clone();
let language = cx
.update(|cx| {
project
@@ -86,34 +130,15 @@ impl LanguageModelTool for CreateBufferTool {
})
}
fn output_view(
input: Self::Input,
output: Result<Self::Output>,
cx: &mut WindowContext,
) -> View<Self::View> {
cx.new_view(|_cx| CreateBufferView {
language: input.language,
output,
})
}
}
pub struct CreateBufferView {
language: String,
output: Result<()>,
}
impl Render for CreateBufferView {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
div().child("Opening a buffer")
}
}
impl ToolOutput for CreateBufferView {
fn generate(&self, _: &mut ProjectContext, _: &mut WindowContext) -> String {
match &self.output {
Ok(_) => format!("Created a new {} buffer", self.language),
Err(err) => format!("Failed to create buffer: {err:?}"),
}
fn serialize(&self, _cx: &mut ViewContext<Self>) -> Self::SerializedState {
()
}
fn deserialize(
&mut self,
_output: Self::SerializedState,
_cx: &mut ViewContext<Self>,
) -> Result<()> {
Ok(())
}
}

View File

@@ -5,9 +5,9 @@ use gpui::{prelude::*, Model, Task};
use project::ProjectPath;
use schemars::JsonSchema;
use semantic_index::{ProjectIndex, Status};
use serde::Deserialize;
use std::{fmt::Write as _, ops::Range};
use ui::{div, prelude::*, CollapsibleContainer, Color, Icon, IconName, Label, WindowContext};
use serde::{Deserialize, Serialize};
use std::{fmt::Write as _, ops::Range, path::Path, sync::Arc};
use ui::{prelude::*, CollapsibleContainer, Color, Icon, IconName, Label, WindowContext};
const DEFAULT_SEARCH_LIMIT: usize = 20;
@@ -15,10 +15,26 @@ pub struct ProjectIndexTool {
project_index: Model<ProjectIndex>,
}
// 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(Default)]
enum ProjectIndexToolState {
#[default]
CollectingQuery,
Searching,
Error(anyhow::Error),
Finished {
excerpts: BTreeMap<ProjectPath, Vec<Range<usize>>>,
index_status: Status,
},
}
#[derive(Deserialize, JsonSchema)]
pub struct ProjectIndexView {
project_index: Model<ProjectIndex>,
input: CodebaseQuery,
expanded_header: bool,
state: ProjectIndexToolState,
}
#[derive(Default, Deserialize, JsonSchema)]
pub struct CodebaseQuery {
/// Semantic search query
query: String,
@@ -26,30 +42,19 @@ pub struct CodebaseQuery {
limit: Option<usize>,
}
pub struct ProjectIndexView {
input: CodebaseQuery,
output: Result<ProjectIndexOutput>,
element_id: ElementId,
expanded_header: bool,
#[derive(Serialize, Deserialize)]
pub struct SerializedState {
index_status: Status,
error_message: Option<String>,
worktrees: BTreeMap<Arc<Path>, WorktreeIndexOutput>,
}
pub struct ProjectIndexOutput {
status: Status,
excerpts: BTreeMap<ProjectPath, Vec<Range<usize>>>,
#[derive(Default, Serialize, Deserialize)]
struct WorktreeIndexOutput {
excerpts: BTreeMap<Arc<Path>, Vec<Range<usize>>>,
}
impl ProjectIndexView {
fn new(input: CodebaseQuery, output: Result<ProjectIndexOutput>) -> Self {
let element_id = ElementId::Name(nanoid::nanoid!().into());
Self {
input,
output,
element_id,
expanded_header: false,
}
}
fn toggle_header(&mut self, cx: &mut ViewContext<Self>) {
self.expanded_header = !self.expanded_header;
cx.notify();
@@ -60,81 +65,206 @@ 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));
let (header_text, content) = match &self.state {
ProjectIndexToolState::Error(error) => {
return format!("failed to search: {error:?}").into_any_element()
}
Ok(output) => output,
};
ProjectIndexToolState::CollectingQuery | ProjectIndexToolState::Searching => {
("Searching...".to_string(), div())
}
ProjectIndexToolState::Finished { excerpts, .. } => {
let file_count = excerpts.len();
let file_count = output.excerpts.len();
let header_text = format!(
"Read {} {}",
file_count,
if file_count == 1 { "file" } else { "files" }
);
let el = v_flex().gap_2().children(excerpts.keys().map(|path| {
h_flex().gap_2().child(Icon::new(IconName::File)).child(
Label::new(path.path.to_string_lossy().to_string()).color(Color::Muted),
)
}));
(header_text, el)
}
};
let header = h_flex()
.gap_2()
.child(Icon::new(IconName::File))
.child(format!(
"Read {} {}",
file_count,
if file_count == 1 { "file" } else { "files" }
));
.child(header_text);
v_flex().gap_3().child(
CollapsibleContainer::new(self.element_id.clone(), self.expanded_header)
.start_slot(header)
.on_click(cx.listener(move |this, _, cx| {
this.toggle_header(cx);
}))
.child(
v_flex()
.gap_3()
.p_3()
.child(
h_flex()
.gap_2()
.child(Icon::new(IconName::MagnifyingGlass))
.child(Label::new(format!("`{}`", query)).color(Color::Muted)),
)
.child(
v_flex()
.gap_2()
.children(output.excerpts.keys().map(|path| {
h_flex().gap_2().child(Icon::new(IconName::File)).child(
Label::new(path.path.to_string_lossy().to_string())
.color(Color::Muted),
)
})),
),
),
)
v_flex()
.gap_3()
.child(
CollapsibleContainer::new("collapsible-container", self.expanded_header)
.start_slot(header)
.on_click(cx.listener(move |this, _, cx| {
this.toggle_header(cx);
}))
.child(
v_flex()
.gap_3()
.p_3()
.child(
h_flex()
.gap_2()
.child(Icon::new(IconName::MagnifyingGlass))
.child(Label::new(format!("`{}`", query)).color(Color::Muted)),
)
.child(content),
),
)
.into_any_element()
}
}
impl ToolOutput for ProjectIndexView {
type Input = CodebaseQuery;
type SerializedState = SerializedState;
fn generate(
&self,
context: &mut assistant_tooling::ProjectContext,
_: &mut WindowContext,
_: &mut ViewContext<Self>,
) -> String {
match &self.output {
Ok(output) => {
match &self.state {
ProjectIndexToolState::CollectingQuery => String::new(),
ProjectIndexToolState::Searching => String::new(),
ProjectIndexToolState::Error(error) => format!("failed to search: {error:?}"),
ProjectIndexToolState::Finished {
excerpts,
index_status,
} => {
let mut body = "found results in the following paths:\n".to_string();
for (project_path, ranges) in &output.excerpts {
for (project_path, ranges) in excerpts {
context.add_excerpts(project_path.clone(), ranges);
writeln!(&mut body, "* {}", &project_path.path.display()).unwrap();
}
if output.status != Status::Idle {
if *index_status != Status::Idle {
body.push_str("Still indexing. Results may be incomplete.\n");
}
body
}
Err(err) => format!("Error: {}", err),
}
}
fn set_input(&mut self, input: Self::Input, cx: &mut ViewContext<Self>) {
self.input = input;
cx.notify();
}
fn execute(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
self.state = ProjectIndexToolState::Searching;
cx.notify();
let project_index = self.project_index.read(cx);
let index_status = project_index.status();
let search = project_index.search(
self.input.query.clone(),
self.input.limit.unwrap_or(DEFAULT_SEARCH_LIMIT),
cx,
);
cx.spawn(|this, mut cx| async move {
let search_result = search.await;
this.update(&mut cx, |this, cx| {
match search_result {
Ok(search_results) => {
let mut excerpts = BTreeMap::<ProjectPath, Vec<Range<usize>>>::new();
for search_result in search_results {
let project_path = ProjectPath {
worktree_id: search_result.worktree.read(cx).id(),
path: search_result.path,
};
excerpts
.entry(project_path)
.or_default()
.push(search_result.range);
}
this.state = ProjectIndexToolState::Finished {
excerpts,
index_status,
};
}
Err(error) => {
this.state = ProjectIndexToolState::Error(error);
}
}
cx.notify();
})
})
}
fn serialize(&self, cx: &mut ViewContext<Self>) -> Self::SerializedState {
let mut serialized = SerializedState {
error_message: None,
index_status: Status::Idle,
worktrees: Default::default(),
};
match &self.state {
ProjectIndexToolState::Error(err) => serialized.error_message = Some(err.to_string()),
ProjectIndexToolState::Finished {
excerpts,
index_status,
} => {
serialized.index_status = *index_status;
if let Some(project) = self.project_index.read(cx).project().upgrade() {
let project = project.read(cx);
for (project_path, excerpts) in excerpts {
if let Some(worktree) =
project.worktree_for_id(project_path.worktree_id, cx)
{
let worktree_path = worktree.read(cx).abs_path();
serialized
.worktrees
.entry(worktree_path)
.or_default()
.excerpts
.insert(project_path.path.clone(), excerpts.clone());
}
}
}
}
_ => {}
}
serialized
}
fn deserialize(
&mut self,
serialized: Self::SerializedState,
cx: &mut ViewContext<Self>,
) -> Result<()> {
if !serialized.worktrees.is_empty() {
let mut excerpts = BTreeMap::<ProjectPath, Vec<Range<usize>>>::new();
if let Some(project) = self.project_index.read(cx).project().upgrade() {
let project = project.read(cx);
for (worktree_path, worktree_state) in serialized.worktrees {
if let Some(worktree) = project
.worktrees()
.find(|worktree| worktree.read(cx).abs_path() == worktree_path)
{
let worktree_id = worktree.read(cx).id();
for (path, serialized_excerpts) in worktree_state.excerpts {
excerpts.insert(ProjectPath { worktree_id, path }, serialized_excerpts);
}
}
}
}
self.state = ProjectIndexToolState::Finished {
excerpts,
index_status: serialized.index_status,
};
}
cx.notify();
Ok(())
}
}
impl ProjectIndexTool {
@@ -144,8 +274,6 @@ impl ProjectIndexTool {
}
impl LanguageModelTool for ProjectIndexTool {
type Input = CodebaseQuery;
type Output = ProjectIndexOutput;
type View = ProjectIndexView;
fn name(&self) -> String {
@@ -156,54 +284,12 @@ impl LanguageModelTool for ProjectIndexTool {
"Semantic search against the user's current codebase, returning excerpts related to the query by computing a dot product against embeddings of code chunks in the code base 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 search = project_index.search(
query.query.clone(),
query.limit.unwrap_or(DEFAULT_SEARCH_LIMIT),
cx,
);
cx.spawn(|mut cx| async move {
let search_results = search.await?;
cx.update(|cx| {
let mut output = ProjectIndexOutput {
status,
excerpts: Default::default(),
};
for search_result in search_results {
let path = ProjectPath {
worktree_id: search_result.worktree.read(cx).id(),
path: search_result.path.clone(),
};
let excerpts_for_path = output.excerpts.entry(path).or_default();
let ix = match excerpts_for_path
.binary_search_by_key(&search_result.range.start, |r| r.start)
{
Ok(ix) | Err(ix) => ix,
};
excerpts_for_path.insert(ix, search_result.range);
}
output
})
fn view(&self, cx: &mut WindowContext) -> gpui::View<Self::View> {
cx.new_view(|_| ProjectIndexView {
state: ProjectIndexToolState::CollectingQuery,
input: Default::default(),
expanded_header: false,
project_index: self.project_index.clone(),
})
}
fn output_view(
input: Self::Input,
output: Result<Self::Output>,
cx: &mut WindowContext,
) -> gpui::View<Self::View> {
cx.new_view(|_cx| ProjectIndexView::new(input, output))
}
fn render_running(_: &mut WindowContext) -> impl IntoElement {
CollapsibleContainer::new(ElementId::Name(nanoid::nanoid!().into()), false)
.start_slot("Searching code base")
}
}

View File

@@ -48,68 +48,73 @@ impl RenderOnce for Composer {
fn render(mut self, cx: &mut WindowContext) -> impl IntoElement {
let font_size = TextSize::Default.rems(cx);
let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
let mut editor_border = cx.theme().colors().text;
editor_border.fade_out(0.90);
// Remove the extra 1px added by the border
let padding = Spacing::XLarge.rems(cx) - rems_from_px(1.);
h_flex()
.p(Spacing::Small.rems(cx))
.w_full()
.items_start()
.child(
v_flex().size_full().gap_1().child(
v_flex()
.w_full()
.p_3()
.bg(cx.theme().colors().editor_background)
.rounded_lg()
.child(
v_flex()
.justify_between()
.w_full()
.gap_2()
.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,
};
v_flex()
.w_full()
.rounded_lg()
.p(padding)
.border_1()
.border_color(editor_border)
.bg(cx.theme().colors().editor_background)
.child(
v_flex()
.justify_between()
.w_full()
.gap_2()
.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()
},
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(
h_flex()
.gap_2()
.child(self.render_tools(cx))
.child(Divider::vertical())
.child(self.render_attachment_tools(cx)),
),
)
})
.child(
h_flex()
.flex_none()
.gap_2()
.justify_between()
.w_full()
.child(
h_flex().gap_1().child(
h_flex()
.gap_2()
.child(self.render_tools(cx))
.child(Divider::vertical())
.child(self.render_attachment_tools(cx)),
),
)
.child(h_flex().gap_1().child(self.model_selector)),
),
),
),
.child(h_flex().gap_1().child(self.model_selector)),
),
),
)
}
}

View File

@@ -16,11 +16,14 @@ anyhow.workspace = true
collections.workspace = true
futures.workspace = true
gpui.workspace = true
log.workspace = true
project.workspace = true
repair_json.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
sum_tree.workspace = true
ui.workspace = true
util.workspace = true
[dev-dependencies]

View File

@@ -2,8 +2,12 @@ mod attachment_registry;
mod project_context;
mod tool_registry;
pub use attachment_registry::{AttachmentRegistry, LanguageModelAttachment, UserAttachment};
pub use attachment_registry::{
AttachmentOutput, AttachmentRegistry, LanguageModelAttachment, SavedUserAttachment,
UserAttachment,
};
pub use project_context::ProjectContext;
pub use tool_registry::{
LanguageModelTool, ToolFunctionCall, ToolFunctionDefinition, ToolOutput, ToolRegistry,
LanguageModelTool, SavedToolFunctionCall, ToolFunctionCall, ToolFunctionDefinition, ToolOutput,
ToolRegistry,
};

View File

@@ -1,8 +1,10 @@
use crate::{ProjectContext, ToolOutput};
use crate::ProjectContext;
use anyhow::{anyhow, Result};
use collections::HashMap;
use futures::future::join_all;
use gpui::{AnyView, Render, Task, View, WindowContext};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::value::RawValue;
use std::{
any::TypeId,
sync::{
@@ -16,25 +18,39 @@ pub struct AttachmentRegistry {
registered_attachments: HashMap<TypeId, RegisteredAttachment>,
}
pub trait AttachmentOutput {
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String;
}
pub trait LanguageModelAttachment {
type Output: 'static;
type View: Render + ToolOutput;
type Output: DeserializeOwned + Serialize + 'static;
type View: Render + AttachmentOutput;
fn name(&self) -> Arc<str>;
fn run(&self, cx: &mut WindowContext) -> Task<Result<Self::Output>>;
fn view(output: Result<Self::Output>, cx: &mut WindowContext) -> View<Self::View>;
fn view(&self, output: Result<Self::Output>, cx: &mut WindowContext) -> View<Self::View>;
}
/// A collected attachment from running an attachment tool
pub struct UserAttachment {
pub view: AnyView,
name: Arc<str>,
serialized_output: Result<Box<RawValue>, String>,
generate_fn: fn(AnyView, &mut ProjectContext, cx: &mut WindowContext) -> String,
}
#[derive(Serialize, Deserialize)]
pub struct SavedUserAttachment {
name: Arc<str>,
serialized_output: Result<Box<RawValue>, String>,
}
/// Internal representation of an attachment tool to allow us to treat them dynamically
struct RegisteredAttachment {
name: Arc<str>,
enabled: AtomicBool,
call: Box<dyn Fn(&mut WindowContext) -> Task<Result<UserAttachment>>>,
deserialize: Box<dyn Fn(&SavedUserAttachment, &mut WindowContext) -> Result<UserAttachment>>,
}
impl AttachmentRegistry {
@@ -45,24 +61,65 @@ impl AttachmentRegistry {
}
pub fn register<A: LanguageModelAttachment + 'static>(&mut self, attachment: A) {
let call = Box::new(move |cx: &mut WindowContext| {
let result = attachment.run(cx);
let attachment = Arc::new(attachment);
cx.spawn(move |mut cx| async move {
let result: Result<A::Output> = result.await;
let view = cx.update(|cx| A::view(result, cx))?;
let call = Box::new({
let attachment = attachment.clone();
move |cx: &mut WindowContext| {
let result = attachment.run(cx);
let attachment = attachment.clone();
cx.spawn(move |mut cx| async move {
let result: Result<A::Output> = result.await;
let serialized_output =
result
.as_ref()
.map_err(ToString::to_string)
.and_then(|output| {
Ok(RawValue::from_string(
serde_json::to_string(output).map_err(|e| e.to_string())?,
)
.unwrap())
});
let view = cx.update(|cx| attachment.view(result, cx))?;
Ok(UserAttachment {
name: attachment.name(),
view: view.into(),
generate_fn: generate::<A>,
serialized_output,
})
})
}
});
let deserialize = Box::new({
let attachment = attachment.clone();
move |saved_attachment: &SavedUserAttachment, cx: &mut WindowContext| {
let serialized_output = saved_attachment.serialized_output.clone();
let output = match &serialized_output {
Ok(serialized_output) => {
Ok(serde_json::from_str::<A::Output>(serialized_output.get())?)
}
Err(error) => Err(anyhow!("{error}")),
};
let view = attachment.view(output, cx).into();
Ok(UserAttachment {
view: view.into(),
name: saved_attachment.name.clone(),
view,
serialized_output,
generate_fn: generate::<A>,
})
})
}
});
self.registered_attachments.insert(
TypeId::of::<A>(),
RegisteredAttachment {
name: attachment.name(),
call,
deserialize,
enabled: AtomicBool::new(true),
},
);
@@ -134,6 +191,35 @@ impl AttachmentRegistry {
.collect())
})
}
pub fn serialize_user_attachment(
&self,
user_attachment: &UserAttachment,
) -> SavedUserAttachment {
SavedUserAttachment {
name: user_attachment.name.clone(),
serialized_output: user_attachment.serialized_output.clone(),
}
}
pub fn deserialize_user_attachment(
&self,
saved_user_attachment: SavedUserAttachment,
cx: &mut WindowContext,
) -> Result<UserAttachment> {
if let Some(registered_attachment) = self
.registered_attachments
.values()
.find(|attachment| attachment.name == saved_user_attachment.name)
{
(registered_attachment.deserialize)(&saved_user_attachment, cx)
} else {
Err(anyhow!(
"no attachment tool for name {}",
saved_user_attachment.name
))
}
}
}
impl UserAttachment {

View File

@@ -1,41 +1,67 @@
use crate::ProjectContext;
use anyhow::{anyhow, Result};
use gpui::{
div, AnyElement, AnyView, IntoElement, ParentElement, Render, Styled, Task, View, WindowContext,
};
use gpui::{AnyElement, AnyView, IntoElement, Render, Task, View, WindowContext};
use repair_json::repair;
use schemars::{schema::RootSchema, schema_for, JsonSchema};
use serde::Deserialize;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::value::RawValue;
use std::{
any::TypeId,
collections::HashMap,
fmt::Display,
mem,
sync::atomic::{AtomicBool, Ordering::SeqCst},
};
use crate::ProjectContext;
use ui::ViewContext;
pub struct ToolRegistry {
registered_tools: HashMap<String, RegisteredTool>,
}
#[derive(Default, Deserialize)]
#[derive(Default)]
pub struct ToolFunctionCall {
pub id: String,
pub name: String,
pub arguments: String,
#[serde(skip)]
pub result: Option<ToolFunctionCallResult>,
state: ToolFunctionCallState,
}
pub enum ToolFunctionCallResult {
#[derive(Default)]
enum ToolFunctionCallState {
#[default]
Initializing,
NoSuchTool,
ParsingFailed,
Finished {
view: AnyView,
generate_fn: fn(AnyView, &mut ProjectContext, &mut WindowContext) -> String,
},
KnownTool(Box<dyn ToolView>),
ExecutedTool(Box<dyn ToolView>),
}
#[derive(Clone)]
trait ToolView {
fn view(&self) -> AnyView;
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String;
fn try_set_input(&self, input: &str, cx: &mut WindowContext);
fn execute(&self, cx: &mut WindowContext) -> Task<Result<()>>;
fn serialize_output(&self, cx: &mut WindowContext) -> Result<Box<RawValue>>;
fn deserialize_output(&self, raw_value: &RawValue, cx: &mut WindowContext) -> Result<()>;
}
#[derive(Default, Serialize, Deserialize)]
pub struct SavedToolFunctionCall {
id: String,
name: String,
arguments: String,
state: SavedToolFunctionCallState,
}
#[derive(Default, Serialize, Deserialize)]
enum SavedToolFunctionCallState {
#[default]
Initializing,
NoSuchTool,
KnownTool,
ExecutedTool(Box<RawValue>),
}
#[derive(Clone, Debug, PartialEq)]
pub struct ToolFunctionDefinition {
pub name: String,
pub description: String,
@@ -43,14 +69,7 @@ pub struct ToolFunctionDefinition {
}
pub trait LanguageModelTool {
/// The input type that will be passed in to `execute` when the tool is called
/// by the language model.
type Input: for<'de> Deserialize<'de> + JsonSchema;
/// The output returned by executing the tool.
type Output: 'static;
type View: Render + ToolOutput;
type View: ToolOutput;
/// Returns the name of the tool.
///
@@ -66,7 +85,7 @@ pub trait LanguageModelTool {
/// 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);
let root_schema = schema_for!(<Self::View as ToolOutput>::Input);
ToolFunctionDefinition {
name: self.name(),
@@ -75,29 +94,34 @@ pub trait LanguageModelTool {
}
}
/// Executes the tool with the given input.
fn execute(&self, input: &Self::Input, cx: &mut WindowContext) -> Task<Result<Self::Output>>;
fn output_view(
input: Self::Input,
output: Result<Self::Output>,
cx: &mut WindowContext,
) -> View<Self::View>;
fn render_running(_cx: &mut WindowContext) -> impl IntoElement {
div()
}
/// A view of the output of running the tool, for displaying to the user.
fn view(&self, cx: &mut WindowContext) -> View<Self::View>;
}
pub trait ToolOutput: Sized {
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String;
pub trait ToolOutput: Render {
/// The input type that will be passed in to `execute` when the tool is called
/// by the language model.
type Input: DeserializeOwned + JsonSchema;
/// The output returned by executing the tool.
type SerializedState: DeserializeOwned + Serialize;
fn generate(&self, project: &mut ProjectContext, cx: &mut ViewContext<Self>) -> String;
fn set_input(&mut self, input: Self::Input, cx: &mut ViewContext<Self>);
fn execute(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>>;
fn serialize(&self, cx: &mut ViewContext<Self>) -> Self::SerializedState;
fn deserialize(
&mut self,
output: Self::SerializedState,
cx: &mut ViewContext<Self>,
) -> Result<()>;
}
struct RegisteredTool {
enabled: AtomicBool,
type_id: TypeId,
call: Box<dyn Fn(&ToolFunctionCall, &mut WindowContext) -> Task<Result<ToolFunctionCall>>>,
render_running: fn(&mut WindowContext) -> gpui::AnyElement,
build_view: Box<dyn Fn(&mut WindowContext) -> Box<dyn ToolView>>,
definition: ToolFunctionDefinition,
}
@@ -134,68 +158,141 @@ impl ToolRegistry {
.collect()
}
pub fn render_tool_call(
pub fn update_tool_call(
&self,
tool_call: &ToolFunctionCall,
call: &mut ToolFunctionCall,
name: Option<&str>,
arguments: Option<&str>,
cx: &mut WindowContext,
) -> AnyElement {
match &tool_call.result {
Some(result) => div()
.p_2()
.child(result.into_any_element(&tool_call.name))
.into_any_element(),
None => self
.registered_tools
.get(&tool_call.name)
.map(|tool| (tool.render_running)(cx))
.unwrap_or_else(|| div().into_any_element()),
) {
if let Some(name) = name {
call.name.push_str(name);
}
if let Some(arguments) = arguments {
if call.arguments.is_empty() {
if let Some(tool) = self.registered_tools.get(&call.name) {
let view = (tool.build_view)(cx);
call.state = ToolFunctionCallState::KnownTool(view);
} else {
call.state = ToolFunctionCallState::NoSuchTool;
}
}
call.arguments.push_str(arguments);
if let ToolFunctionCallState::KnownTool(view) = &call.state {
if let Ok(repaired_arguments) = repair(call.arguments.clone()) {
view.try_set_input(&repaired_arguments, cx)
}
}
}
}
pub fn register<T: 'static + LanguageModelTool>(
&mut self,
tool: T,
pub fn execute_tool_call(
&self,
tool_call: &mut ToolFunctionCall,
cx: &mut WindowContext,
) -> Option<Task<Result<()>>> {
if let ToolFunctionCallState::KnownTool(view) = mem::take(&mut tool_call.state) {
let task = view.execute(cx);
tool_call.state = ToolFunctionCallState::ExecutedTool(view);
Some(task)
} else {
None
}
}
pub fn render_tool_call(
&self,
tool_call: &ToolFunctionCall,
_cx: &mut WindowContext,
) -> Result<()> {
) -> Option<AnyElement> {
match &tool_call.state {
ToolFunctionCallState::NoSuchTool => {
Some(ui::Label::new("No such tool").into_any_element())
}
ToolFunctionCallState::Initializing => None,
ToolFunctionCallState::KnownTool(view) | ToolFunctionCallState::ExecutedTool(view) => {
Some(view.view().into_any_element())
}
}
}
pub fn content_for_tool_call(
&self,
tool_call: &ToolFunctionCall,
project_context: &mut ProjectContext,
cx: &mut WindowContext,
) -> String {
match &tool_call.state {
ToolFunctionCallState::Initializing => String::new(),
ToolFunctionCallState::NoSuchTool => {
format!("No such tool: {}", tool_call.name)
}
ToolFunctionCallState::KnownTool(view) | ToolFunctionCallState::ExecutedTool(view) => {
view.generate(project_context, cx)
}
}
}
pub fn serialize_tool_call(
&self,
call: &ToolFunctionCall,
cx: &mut WindowContext,
) -> Result<SavedToolFunctionCall> {
Ok(SavedToolFunctionCall {
id: call.id.clone(),
name: call.name.clone(),
arguments: call.arguments.clone(),
state: match &call.state {
ToolFunctionCallState::Initializing => SavedToolFunctionCallState::Initializing,
ToolFunctionCallState::NoSuchTool => SavedToolFunctionCallState::NoSuchTool,
ToolFunctionCallState::KnownTool(_) => SavedToolFunctionCallState::KnownTool,
ToolFunctionCallState::ExecutedTool(view) => {
SavedToolFunctionCallState::ExecutedTool(view.serialize_output(cx)?)
}
},
})
}
pub fn deserialize_tool_call(
&self,
call: &SavedToolFunctionCall,
cx: &mut WindowContext,
) -> Result<ToolFunctionCall> {
let Some(tool) = self.registered_tools.get(&call.name) else {
return Err(anyhow!("no such tool {}", call.name));
};
Ok(ToolFunctionCall {
id: call.id.clone(),
name: call.name.clone(),
arguments: call.arguments.clone(),
state: match &call.state {
SavedToolFunctionCallState::Initializing => ToolFunctionCallState::Initializing,
SavedToolFunctionCallState::NoSuchTool => ToolFunctionCallState::NoSuchTool,
SavedToolFunctionCallState::KnownTool => {
log::error!("Deserialized tool that had not executed");
let view = (tool.build_view)(cx);
view.try_set_input(&call.arguments, cx);
ToolFunctionCallState::KnownTool(view)
}
SavedToolFunctionCallState::ExecutedTool(output) => {
let view = (tool.build_view)(cx);
view.try_set_input(&call.arguments, cx);
view.deserialize_output(output, cx)?;
ToolFunctionCallState::ExecutedTool(view)
}
},
})
}
pub fn register<T: 'static + LanguageModelTool>(&mut self, tool: T) -> Result<()> {
let name = tool.name();
let registered_tool = RegisteredTool {
type_id: TypeId::of::<T>(),
definition: tool.definition(),
enabled: AtomicBool::new(true),
call: 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(Ok(ToolFunctionCall {
id,
name: name.clone(),
arguments,
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 view = cx.update(|cx| T::output_view(input, result, cx))?;
Ok(ToolFunctionCall {
id,
name: name.clone(),
arguments,
result: Some(ToolFunctionCallResult::Finished {
view: view.into(),
generate_fn: generate::<T>,
}),
})
})
},
),
render_running: render_running::<T>,
build_view: Box::new(move |cx: &mut WindowContext| Box::new(tool.view(cx))),
};
let previous = self.registered_tools.insert(name.clone(), registered_tool);
@@ -204,77 +301,40 @@ impl ToolRegistry {
}
return Ok(());
fn render_running<T: LanguageModelTool>(cx: &mut WindowContext) -> AnyElement {
T::render_running(cx).into_any_element()
}
fn generate<T: LanguageModelTool>(
view: AnyView,
project: &mut ProjectContext,
cx: &mut WindowContext,
) -> String {
view.downcast::<T::View>()
.unwrap()
.update(cx, |view, cx| T::View::generate(view, project, cx))
}
}
/// 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();
let tool = match self.registered_tools.get(&name) {
Some(tool) => tool,
None => {
let name = name.clone();
return Task::ready(Ok(ToolFunctionCall {
id,
name: name.clone(),
arguments,
result: Some(ToolFunctionCallResult::NoSuchTool),
}));
}
};
(tool.call)(tool_call, cx)
}
}
impl ToolFunctionCallResult {
pub fn generate(
&self,
name: &String,
project: &mut ProjectContext,
cx: &mut WindowContext,
) -> String {
match self {
ToolFunctionCallResult::NoSuchTool => format!("No tool for {name}"),
ToolFunctionCallResult::ParsingFailed => {
format!("Unable to parse arguments for {name}")
}
ToolFunctionCallResult::Finished { generate_fn, view } => {
(generate_fn)(view.clone(), project, cx)
}
impl<T: ToolOutput> ToolView for View<T> {
fn view(&self) -> AnyView {
self.clone().into()
}
fn generate(&self, project: &mut ProjectContext, cx: &mut WindowContext) -> String {
self.update(cx, |view, cx| view.generate(project, cx))
}
fn try_set_input(&self, input: &str, cx: &mut WindowContext) {
if let Ok(input) = serde_json::from_str::<T::Input>(input) {
self.update(cx, |view, cx| {
view.set_input(input, cx);
cx.notify();
});
}
}
fn into_any_element(&self, name: &String) -> AnyElement {
match self {
ToolFunctionCallResult::NoSuchTool => {
format!("Language Model attempted to call {name}").into_any_element()
}
ToolFunctionCallResult::ParsingFailed => {
format!("Language Model called {name} with bad arguments").into_any_element()
}
ToolFunctionCallResult::Finished { view, .. } => view.clone().into_any_element(),
}
fn execute(&self, cx: &mut WindowContext) -> Task<Result<()>> {
self.update(cx, |view, cx| view.execute(cx))
}
fn serialize_output(&self, cx: &mut WindowContext) -> Result<Box<RawValue>> {
let output = self.update(cx, |view, cx| view.serialize(cx));
Ok(RawValue::from_string(serde_json::to_string(&output)?)?)
}
fn deserialize_output(&self, output: &RawValue, cx: &mut WindowContext) -> Result<()> {
let state = serde_json::from_str::<T::SerializedState>(output.get())?;
self.update(cx, |view, cx| view.deserialize(state, cx))?;
Ok(())
}
}
@@ -293,7 +353,6 @@ mod test {
use super::*;
use gpui::{div, prelude::*, Render, TestAppContext};
use gpui::{EmptyView, View};
use schemars::schema_for;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::json;
@@ -304,10 +363,6 @@ mod test {
unit: String,
}
struct WeatherTool {
current_weather: WeatherResult,
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
struct WeatherResult {
location: String,
@@ -316,24 +371,81 @@ mod test {
}
struct WeatherView {
result: WeatherResult,
input: Option<WeatherQuery>,
result: Option<WeatherResult>,
// Fake API call
current_weather: WeatherResult,
}
#[derive(Clone, Serialize)]
struct WeatherTool {
current_weather: WeatherResult,
}
impl WeatherView {
fn new(current_weather: WeatherResult) -> Self {
Self {
input: None,
result: None,
current_weather,
}
}
}
impl Render for WeatherView {
fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
div().child(format!("temperature: {}", self.result.temperature))
match self.result {
Some(ref result) => div()
.child(format!("temperature: {}", result.temperature))
.into_any_element(),
None => div().child("Calculating weather...").into_any_element(),
}
}
}
impl ToolOutput for WeatherView {
fn generate(&self, _output: &mut ProjectContext, _cx: &mut WindowContext) -> String {
type Input = WeatherQuery;
type SerializedState = WeatherResult;
fn generate(&self, _output: &mut ProjectContext, _cx: &mut ViewContext<Self>) -> String {
serde_json::to_string(&self.result).unwrap()
}
fn set_input(&mut self, input: Self::Input, cx: &mut ViewContext<Self>) {
self.input = Some(input);
cx.notify();
}
fn execute(&mut self, _cx: &mut ViewContext<Self>) -> Task<Result<()>> {
let input = self.input.as_ref().unwrap();
let _location = input.location.clone();
let _unit = input.unit.clone();
let weather = self.current_weather.clone();
self.result = Some(weather);
Task::ready(Ok(()))
}
fn serialize(&self, _cx: &mut ViewContext<Self>) -> Self::SerializedState {
self.current_weather.clone()
}
fn deserialize(
&mut self,
output: Self::SerializedState,
_cx: &mut ViewContext<Self>,
) -> Result<()> {
self.current_weather = output;
Ok(())
}
}
impl LanguageModelTool for WeatherTool {
type Input = WeatherQuery;
type Output = WeatherResult;
type View = WeatherView;
fn name(&self) -> String {
@@ -344,88 +456,71 @@ mod test {
"Fetches the current weather for a given location.".to_string()
}
fn execute(
&self,
input: &Self::Input,
_cx: &mut WindowContext,
) -> Task<Result<Self::Output>> {
let _location = input.location.clone();
let _unit = input.unit.clone();
let weather = self.current_weather.clone();
Task::ready(Ok(weather))
}
fn output_view(
_input: Self::Input,
result: Result<Self::Output>,
cx: &mut WindowContext,
) -> View<Self::View> {
cx.new_view(|_cx| {
let result = result.unwrap();
WeatherView { result }
})
fn view(&self, cx: &mut WindowContext) -> View<Self::View> {
cx.new_view(|_cx| WeatherView::new(self.current_weather.clone()))
}
}
#[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 {
location: "San Francisco".to_string(),
temperature: 21.0,
unit: "Celsius".to_string(),
},
};
let tools = vec![tool.definition()];
assert_eq!(tools.len(), 1);
let expected = ToolFunctionDefinition {
name: "get_current_weather".to_string(),
description: "Fetches the current weather for a given location.".to_string(),
parameters: schema_for!(WeatherQuery),
};
assert_eq!(tools[0].name, expected.name);
assert_eq!(tools[0].description, expected.description);
let expected_schema = serde_json::to_value(&tools[0].parameters).unwrap();
assert_eq!(
expected_schema,
json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "WeatherQuery",
"type": "object",
"properties": {
"location": {
"type": "string"
},
"unit": {
"type": "string"
}
let mut registry = ToolRegistry::new();
registry
.register(WeatherTool {
current_weather: WeatherResult {
location: "San Francisco".to_string(),
temperature: 21.0,
unit: "Celsius".to_string(),
},
"required": ["location", "unit"]
})
.unwrap();
let definitions = registry.definitions();
assert_eq!(
definitions,
[ToolFunctionDefinition {
name: "get_current_weather".to_string(),
description: "Fetches the current weather for a given location.".to_string(),
parameters: serde_json::from_value(json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "WeatherQuery",
"type": "object",
"properties": {
"location": {
"type": "string"
},
"unit": {
"type": "string"
}
},
"required": ["location", "unit"]
}))
.unwrap(),
}]
);
let args = json!({
"location": "San Francisco",
"unit": "Celsius"
let mut call = ToolFunctionCall {
id: "the-id".to_string(),
name: "get_cur".to_string(),
..Default::default()
};
let task = cx.update(|cx| {
registry.update_tool_call(
&mut call,
Some("rent_weather"),
Some(r#"{"location": "San Francisco","#),
cx,
);
registry.update_tool_call(&mut call, None, Some(r#" "unit": "Celsius"}"#), cx);
registry.execute_tool_call(&mut call, cx).unwrap()
});
task.await.unwrap();
let query: WeatherQuery = serde_json::from_value(args).unwrap();
let result = cx.update(|cx| tool.execute(&query, cx)).await;
assert!(result.is_ok());
let result = result.unwrap();
assert_eq!(result, tool.current_weather);
match &call.state {
ToolFunctionCallState::ExecutedTool(_view) => {}
_ => panic!(),
}
}
}

View File

@@ -56,16 +56,22 @@ struct UpdateRequestBody {
telemetry: bool,
}
#[derive(Clone, Copy, PartialEq, Eq)]
#[derive(Clone, PartialEq, Eq)]
pub enum AutoUpdateStatus {
Idle,
Checking,
Downloading,
Installing,
Updated,
Updated { binary_path: PathBuf },
Errored,
}
impl AutoUpdateStatus {
pub fn is_updated(&self) -> bool {
matches!(self, Self::Updated { .. })
}
}
pub struct AutoUpdater {
status: AutoUpdateStatus,
current_version: SemanticVersion,
@@ -306,7 +312,7 @@ impl AutoUpdater {
}
pub fn poll(&mut self, cx: &mut ModelContext<Self>) {
if self.pending_poll.is_some() || self.status == AutoUpdateStatus::Updated {
if self.pending_poll.is_some() || self.status.is_updated() {
return;
}
@@ -328,7 +334,7 @@ impl AutoUpdater {
}
pub fn status(&self) -> AutoUpdateStatus {
self.status
self.status.clone()
}
pub fn dismiss_error(&mut self, cx: &mut ModelContext<Self>) {
@@ -404,6 +410,11 @@ impl AutoUpdater {
cx.notify();
})?;
// We store the path of our current binary, before we install, since installation might
// delete it. Once deleted, it's hard to get the path to our binary on Linux.
// So we cache it here, which allows us to then restart later on.
let binary_path = cx.update(|cx| cx.app_path())??;
match OS {
"macos" => install_release_macos(&temp_dir, downloaded_asset, &cx).await,
"linux" => install_release_linux(&temp_dir, downloaded_asset, &cx).await,
@@ -413,7 +424,7 @@ impl AutoUpdater {
this.update(&mut cx, |this, cx| {
this.set_should_show_update_notification(true, cx)
.detach_and_log_err(cx);
this.status = AutoUpdateStatus::Updated;
this.status = AutoUpdateStatus::Updated { binary_path };
cx.notify();
})?;

View File

@@ -19,11 +19,17 @@ path = "src/main.rs"
[dependencies]
anyhow.workspace = true
clap.workspace = true
libc.workspace = true
ipc-channel = "0.18"
once_cell.workspace = true
release_channel.workspace = true
serde.workspace = true
util.workspace = true
[target.'cfg(target_os = "linux")'.dependencies]
exec.workspace = true
fork.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation.workspace = true
core-services = "0.2"

View File

@@ -13,6 +13,7 @@ pub enum CliRequest {
paths: Vec<String>,
wait: bool,
open_new_workspace: Option<bool>,
dev_server_token: Option<String>,
},
}

View File

@@ -1,17 +1,21 @@
#![cfg_attr(any(target_os = "linux", target_os = "windows"), allow(dead_code))]
use anyhow::{anyhow, Context, Result};
use anyhow::{Context, Result};
use clap::Parser;
use cli::{CliRequest, CliResponse};
use serde::Deserialize;
use cli::{ipc::IpcOneShotServer, CliRequest, CliResponse, IpcHandshake};
use std::{
env,
ffi::OsStr,
fs,
env, fs,
path::{Path, PathBuf},
};
use util::paths::PathLikeWithPosition;
struct Detect;
trait InstalledApp {
fn zed_version_string(&self) -> String;
fn launch(&self, ipc_url: String) -> anyhow::Result<()>;
}
#[derive(Parser, Debug)]
#[command(name = "zed", disable_version_flag = true)]
struct Args {
@@ -33,9 +37,9 @@ struct Args {
/// Print Zed's version and the app path.
#[arg(short, long)]
version: bool,
/// Custom Zed.app path
#[arg(short, long)]
bundle_path: Option<PathBuf>,
/// Custom path to Zed.app or the zed binary
#[arg(long)]
zed: Option<PathBuf>,
/// Run zed in dev-server mode
#[arg(long)]
dev_server_token: Option<String>,
@@ -49,12 +53,6 @@ fn parse_path_with_position(
})
}
#[derive(Debug, Deserialize)]
struct InfoPlist {
#[serde(rename = "CFBundleShortVersionString")]
bundle_short_version_string: String,
}
fn main() -> Result<()> {
// Intercept version designators
#[cfg(target_os = "macos")]
@@ -68,14 +66,10 @@ fn main() -> Result<()> {
}
let args = Args::parse();
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]);
}
let app = Detect::detect(args.zed.as_deref()).context("Bundle detection")?;
if args.version {
println!("{}", bundle.zed_version_string());
println!("{}", app.zed_version_string());
return Ok(());
}
@@ -101,7 +95,14 @@ fn main() -> Result<()> {
paths.push(canonicalized.to_string(|path| path.display().to_string()))
}
let (tx, rx) = bundle.launch()?;
let (server, server_name) =
IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
let url = format!("zed-cli://{server_name}");
app.launch(url)?;
let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
let (tx, rx) = (handshake.requests, handshake.responses);
let open_new_workspace = if args.new {
Some(true)
} else if args.add {
@@ -114,6 +115,7 @@ fn main() -> Result<()> {
paths,
wait: args.wait,
open_new_workspace,
dev_server_token: args.dev_server_token,
})?;
while let Ok(response) = rx.recv() {
@@ -128,60 +130,125 @@ fn main() -> Result<()> {
Ok(())
}
enum Bundle {
App {
app_bundle: PathBuf,
plist: InfoPlist,
},
LocalPath {
executable: PathBuf,
plist: InfoPlist,
},
}
fn locate_bundle() -> Result<PathBuf> {
let cli_path = std::env::current_exe()?.canonicalize()?;
let mut app_path = cli_path.clone();
while app_path.extension() != Some(OsStr::new("app")) {
if !app_path.pop() {
return Err(anyhow!("cannot find app bundle containing {:?}", cli_path));
}
}
Ok(app_path)
}
#[cfg(target_os = "linux")]
mod linux {
use std::path::Path;
use std::{
env,
ffi::OsString,
io,
os::{
linux::net::SocketAddrExt,
unix::net::{SocketAddr, UnixDatagram},
},
path::{Path, PathBuf},
process, thread,
time::Duration,
};
use cli::{CliRequest, CliResponse};
use ipc_channel::ipc::{IpcReceiver, IpcSender};
use anyhow::anyhow;
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
use fork::Fork;
use once_cell::sync::Lazy;
use crate::{Bundle, InfoPlist};
use crate::{Detect, InstalledApp};
impl Bundle {
pub fn detect(_args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
unimplemented!()
static RELEASE_CHANNEL: Lazy<String> =
Lazy::new(|| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string());
struct App(PathBuf);
impl Detect {
pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
let path = if let Some(path) = path {
path.to_path_buf().canonicalize()
} else {
let cli = env::current_exe()?;
let dir = cli
.parent()
.ok_or_else(|| anyhow!("no parent path for cli"))?;
match dir.join("zed").canonicalize() {
Ok(path) => Ok(path),
// development builds have Zed capitalized
Err(e) => match dir.join("Zed").canonicalize() {
Ok(path) => Ok(path),
Err(_) => Err(e),
},
}
}?;
Ok(App(path))
}
}
impl InstalledApp for App {
fn zed_version_string(&self) -> String {
format!(
"Zed {}{} {}",
if *RELEASE_CHANNEL == "stable" {
"".to_string()
} else {
format!(" {} ", *RELEASE_CHANNEL)
},
option_env!("RELEASE_VERSION").unwrap_or_default(),
self.0.display(),
)
}
pub fn plist(&self) -> &InfoPlist {
unimplemented!()
fn launch(&self, ipc_url: String) -> anyhow::Result<()> {
let uid: u32 = unsafe { libc::getuid() };
let sock_addr =
SocketAddr::from_abstract_name(format!("zed-{}-{}", *RELEASE_CHANNEL, uid))?;
let sock = UnixDatagram::unbound()?;
if sock.connect_addr(&sock_addr).is_err() {
self.boot_background(ipc_url)?;
} else {
sock.send(ipc_url.as_bytes())?;
}
Ok(())
}
}
impl App {
fn boot_background(&self, ipc_url: String) -> anyhow::Result<()> {
let path = &self.0;
match fork::fork() {
Ok(Fork::Parent(_)) => Ok(()),
Ok(Fork::Child) => {
std::env::set_var(FORCE_CLI_MODE_ENV_VAR_NAME, "");
if let Err(_) = fork::setsid() {
eprintln!("failed to setsid: {}", std::io::Error::last_os_error());
process::exit(1);
}
if std::env::var("ZED_KEEP_FD").is_err() {
if let Err(_) = fork::close_fd() {
eprintln!("failed to close_fd: {}", std::io::Error::last_os_error());
}
}
let error =
exec::execvp(path.clone(), &[path.as_os_str(), &OsString::from(ipc_url)]);
// if exec succeeded, we never get here.
eprintln!("failed to exec {:?}: {}", path, error);
process::exit(1)
}
Err(_) => Err(anyhow!(io::Error::last_os_error())),
}
}
pub fn path(&self) -> &Path {
unimplemented!()
}
pub fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
unimplemented!()
}
pub fn spawn(&self, _args: Vec<String>) -> anyhow::Result<()> {
unimplemented!()
}
pub fn zed_version_string(&self) -> String {
unimplemented!()
fn wait_for_socket(
&self,
sock_addr: &SocketAddr,
sock: &mut UnixDatagram,
) -> Result<(), std::io::Error> {
for _ in 0..100 {
thread::sleep(Duration::from_millis(10));
if sock.connect_addr(&sock_addr).is_ok() {
return Ok(());
}
}
sock.connect_addr(&sock_addr)
}
}
}
@@ -189,59 +256,79 @@ mod linux {
// todo("windows")
#[cfg(target_os = "windows")]
mod windows {
use crate::{Detect, InstalledApp};
use std::path::Path;
use cli::{CliRequest, CliResponse};
use ipc_channel::ipc::{IpcReceiver, IpcSender};
use crate::{Bundle, InfoPlist};
impl Bundle {
pub fn detect(_args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
struct App;
impl InstalledApp for App {
fn zed_version_string(&self) -> String {
unimplemented!()
}
pub fn plist(&self) -> &InfoPlist {
fn launch(&self, _ipc_url: String) -> anyhow::Result<()> {
unimplemented!()
}
}
pub fn path(&self) -> &Path {
unimplemented!()
}
pub fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
unimplemented!()
}
pub fn spawn(&self, _args: Vec<String>) -> anyhow::Result<()> {
unimplemented!()
}
pub fn zed_version_string(&self) -> String {
unimplemented!()
impl Detect {
pub fn detect(_path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
Ok(App)
}
}
}
#[cfg(target_os = "macos")]
mod mac_os {
use anyhow::{Context, Result};
use anyhow::{anyhow, Context, Result};
use core_foundation::{
array::{CFArray, CFIndex},
string::kCFStringEncodingUTF8,
url::{CFURLCreateWithBytes, CFURL},
};
use core_services::{kLSLaunchDefaults, LSLaunchURLSpec, LSOpenFromURLSpec, TCFType};
use std::{fs, path::Path, process::Command, ptr};
use serde::Deserialize;
use std::{
ffi::OsStr,
fs,
path::{Path, PathBuf},
process::Command,
ptr,
};
use cli::{CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME};
use ipc_channel::ipc::{IpcOneShotServer, IpcReceiver, IpcSender};
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
use crate::{locate_bundle, Bundle, InfoPlist};
use crate::{Detect, InstalledApp};
impl Bundle {
pub fn detect(args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
let bundle_path = if let Some(bundle_path) = args_bundle_path {
#[derive(Debug, Deserialize)]
struct InfoPlist {
#[serde(rename = "CFBundleShortVersionString")]
bundle_short_version_string: String,
}
enum Bundle {
App {
app_bundle: PathBuf,
plist: InfoPlist,
},
LocalPath {
executable: PathBuf,
plist: InfoPlist,
},
}
fn locate_bundle() -> Result<PathBuf> {
let cli_path = std::env::current_exe()?.canonicalize()?;
let mut app_path = cli_path.clone();
while app_path.extension() != Some(OsStr::new("app")) {
if !app_path.pop() {
return Err(anyhow!("cannot find app bundle containing {:?}", cli_path));
}
}
Ok(app_path)
}
impl Detect {
pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
let bundle_path = if let Some(bundle_path) = path {
bundle_path
.canonicalize()
.with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))?
@@ -256,7 +343,7 @@ mod mac_os {
plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
format!("Reading *.app bundle plist file at {plist_path:?}")
})?;
Ok(Self::App {
Ok(Bundle::App {
app_bundle: bundle_path,
plist,
})
@@ -271,42 +358,27 @@ mod mac_os {
plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
format!("Reading dev bundle plist file at {plist_path:?}")
})?;
Ok(Self::LocalPath {
Ok(Bundle::LocalPath {
executable: bundle_path,
plist,
})
}
}
}
}
fn plist(&self) -> &InfoPlist {
match self {
Self::App { plist, .. } => plist,
Self::LocalPath { plist, .. } => plist,
}
impl InstalledApp for Bundle {
fn zed_version_string(&self) -> String {
let is_dev = matches!(self, Self::LocalPath { .. });
format!(
"Zed {}{} {}",
self.plist().bundle_short_version_string,
if is_dev { " (dev)" } else { "" },
self.path().display(),
)
}
fn path(&self) -> &Path {
match self {
Self::App { app_bundle, .. } => app_bundle,
Self::LocalPath { executable, .. } => executable,
}
}
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")?;
let url = format!("zed-cli://{server_name}");
fn launch(&self, url: String) -> anyhow::Result<()> {
match self {
Self::App { app_bundle, .. } => {
let app_path = app_bundle;
@@ -368,18 +440,23 @@ mod mac_os {
}
}
let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
Ok((handshake.requests, handshake.responses))
Ok(())
}
}
impl Bundle {
fn plist(&self) -> &InfoPlist {
match self {
Self::App { plist, .. } => plist,
Self::LocalPath { plist, .. } => plist,
}
}
pub fn zed_version_string(&self) -> String {
let is_dev = matches!(self, Self::LocalPath { .. });
format!(
"Zed {}{} {}",
self.plist().bundle_short_version_string,
if is_dev { " (dev)" } else { "" },
self.path().display(),
)
fn path(&self) -> &Path {
match self {
Self::App { app_bundle, .. } => app_bundle,
Self::LocalPath { executable, .. } => executable,
}
}
}

View File

@@ -27,7 +27,7 @@ futures.workspace = true
gpui.workspace = true
lazy_static.workspace = true
log.workspace = true
once_cell = "1.19.0"
once_cell.workspace = true
parking_lot.workspace = true
postage.workspace = true
rand.workspace = true

View File

@@ -415,7 +415,7 @@ impl Database {
if is_serialization_error(error) && prev_attempt_count < SLEEPS.len() {
let base_delay = SLEEPS[prev_attempt_count];
let randomized_delay = base_delay * self.rng.lock().await.gen_range(0.5..=2.0);
log::info!(
log::warn!(
"retrying transaction after serialization error. delay: {} ms.",
randomized_delay
);

View File

@@ -130,13 +130,21 @@ impl Database {
.await
}
pub async fn delete_project(&self, project_id: ProjectId) -> Result<()> {
self.weak_transaction(|tx| async move {
project::Entity::delete_by_id(project_id).exec(&*tx).await?;
Ok(())
})
.await
}
/// Unshares the given project.
pub async fn unshare_project(
&self,
project_id: ProjectId,
connection: ConnectionId,
user_id: Option<UserId>,
) -> Result<TransactionGuard<(Option<proto::Room>, Vec<ConnectionId>)>> {
) -> Result<TransactionGuard<(bool, Option<proto::Room>, Vec<ConnectionId>)>> {
self.project_transaction(project_id, |tx| async move {
let guest_connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
let project = project::Entity::find_by_id(project_id)
@@ -149,10 +157,7 @@ impl Database {
None
};
if project.host_connection()? == connection {
project::Entity::delete(project.into_active_model())
.exec(&*tx)
.await?;
return Ok((room, guest_connection_ids));
return Ok((true, room, guest_connection_ids));
}
if let Some(dev_server_project_id) = project.dev_server_project_id {
if let Some(user_id) = user_id {
@@ -169,7 +174,7 @@ impl Database {
})
.exec(&*tx)
.await?;
return Ok((room, guest_connection_ids));
return Ok((false, room, guest_connection_ids));
}
}

View File

@@ -2032,23 +2032,34 @@ async fn unshare_project_internal(
user_id: Option<UserId>,
session: &Session,
) -> Result<()> {
let (room, guest_connection_ids) = &*session
.db()
.await
.unshare_project(project_id, connection_id, user_id)
.await?;
let delete = {
let room_guard = session
.db()
.await
.unshare_project(project_id, connection_id, user_id)
.await?;
let message = proto::UnshareProject {
project_id: project_id.to_proto(),
let (delete, room, guest_connection_ids) = &*room_guard;
let message = proto::UnshareProject {
project_id: project_id.to_proto(),
};
broadcast(
Some(connection_id),
guest_connection_ids.iter().copied(),
|conn_id| session.peer.send(conn_id, message.clone()),
);
if let Some(room) = room {
room_updated(room, &session.peer);
}
*delete
};
broadcast(
Some(connection_id),
guest_connection_ids.iter().copied(),
|conn_id| session.peer.send(conn_id, message.clone()),
);
if let Some(room) = room {
room_updated(room, &session.peer);
if delete {
let db = session.db().await;
db.delete_project(project_id).await?;
}
Ok(())

View File

@@ -428,8 +428,10 @@ impl TestServer {
node_runtime: app_state.node_runtime.clone(),
},
cx,
);
});
)
})
.await
.unwrap();
TestClient {
app_state,

View File

@@ -677,7 +677,7 @@ impl CollabTitlebarItem {
client::Status::UpgradeRequired => {
let auto_updater = auto_update::AutoUpdater::get(cx);
let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
Some(AutoUpdateStatus::Updated) => "Please restart Zed to Collaborate",
Some(AutoUpdateStatus::Updated { .. }) => "Please restart Zed to Collaborate",
Some(AutoUpdateStatus::Installing)
| Some(AutoUpdateStatus::Downloading)
| Some(AutoUpdateStatus::Checking) => "Updating...",
@@ -691,7 +691,7 @@ impl CollabTitlebarItem {
.label_size(LabelSize::Small)
.on_click(|_, cx| {
if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
if auto_updater.read(cx).status() == AutoUpdateStatus::Updated {
if auto_updater.read(cx).status().is_updated() {
workspace::restart(&Default::default(), cx);
return;
}

View File

@@ -26,7 +26,7 @@ impl CollabNotification {
}
impl ParentElement for CollabNotification {
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
}
}

View File

@@ -429,11 +429,17 @@ impl Copilot {
env: None,
};
let root_path = if cfg!(target_os = "windows") {
Path::new("C:/")
} else {
Path::new("/")
};
let server = LanguageServer::new(
Arc::new(Mutex::new(None)),
new_server_id,
binary,
Path::new("/"),
root_path,
None,
cx.clone(),
)?;

View File

@@ -52,16 +52,11 @@ pub struct SelectToEndOfLine {
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct ToggleCodeActions {
// Display row from which the action was deployed.
#[serde(default)]
pub deployed_from_indicator: Option<u32>,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct ToggleTestRunner {
#[serde(default)]
pub deployed_from_row: Option<u32>,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct ConfirmCompletion {
#[serde(default)]

View File

@@ -477,6 +477,11 @@ impl DisplaySnapshot {
.to_inlay_offset(anchor.to_offset(&self.buffer_snapshot))
}
pub fn display_point_to_anchor(&self, point: DisplayPoint, bias: Bias) -> Anchor {
self.buffer_snapshot
.anchor_at(point.to_offset(&self, bias), bias)
}
fn display_point_to_inlay_point(&self, point: DisplayPoint, bias: Bias) -> InlayPoint {
let block_point = point.0;
let wrap_point = self.block_snapshot.to_wrap_point(block_point);
@@ -721,6 +726,10 @@ impl DisplaySnapshot {
DisplayPoint(clipped)
}
pub fn clip_ignoring_line_ends(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint {
DisplayPoint(self.block_snapshot.clip_point(point.0, bias))
}
pub fn clip_at_line_end(&self, point: DisplayPoint) -> DisplayPoint {
let mut point = point.0;
if point.column == self.line_len(point.row) {

View File

@@ -79,7 +79,6 @@ use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
pub use inline_completion_provider::*;
pub use items::MAX_TAB_TITLE_LEN;
use itertools::Itertools;
use language::Runnable;
use language::{
char_kind,
language_settings::{self, all_language_settings, InlayHintSettings},
@@ -87,7 +86,8 @@ use language::{
CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, OffsetRangeExt,
Point, Selection, SelectionGoal, TransactionId,
};
use task::{ResolvedTask, TaskTemplate};
use language::{Runnable, RunnableRange};
use task::{ResolvedTask, TaskTemplate, TaskVariables};
use hover_links::{HoverLink, HoveredLinkState, InlayHighlight};
use lsp::{DiagnosticSeverity, LanguageServerId};
@@ -404,6 +404,7 @@ struct RunnableTasks {
templates: Vec<(TaskSourceKind, TaskTemplate)>,
// We need the column at which the task context evaluation should take place.
column: u32,
extra_variables: HashMap<String, String>,
}
#[derive(Clone)]
@@ -504,7 +505,7 @@ pub struct Editor {
>,
last_bounds: Option<Bounds<Pixels>>,
expect_bounds_change: Option<Bounds<Pixels>>,
tasks: HashMap<u32, RunnableTasks>,
tasks: HashMap<(BufferId, u32), (usize, RunnableTasks)>,
tasks_update_task: Option<Task<()>>,
}
@@ -3839,7 +3840,7 @@ impl Editor {
}
}
drop(context_menu);
let snapshot = self.snapshot(cx);
let deployed_from_indicator = action.deployed_from_indicator;
let mut task = self.code_actions_task.take();
let action = action.clone();
@@ -3851,11 +3852,24 @@ impl Editor {
let spawned_test_task = this.update(&mut cx, |this, cx| {
if this.focus_handle.is_focused(cx) {
let buffer_row = action
let display_row = action
.deployed_from_indicator
.map(|row| {
DisplayPoint::new(row, 0)
.to_point(&snapshot.display_snapshot)
.row
})
.unwrap_or_else(|| this.selections.newest::<Point>(cx).head().row);
let tasks = this.tasks.get(&buffer_row).map(|t| Arc::new(t.to_owned()));
let (location, code_actions) = this
let (buffer, buffer_row) = snapshot
.buffer_snapshot
.buffer_line_for_row(display_row)
.and_then(|(buffer_snapshot, range)| {
this.buffer
.read(cx)
.buffer(buffer_snapshot.remote_id())
.map(|buffer| (buffer, range.start.row))
})?;
let (_, code_actions) = this
.available_code_actions
.clone()
.and_then(|(location, code_actions)| {
@@ -3869,25 +3883,20 @@ impl Editor {
}
})
.unzip();
let buffer_id = buffer.read(cx).remote_id();
let tasks = this
.tasks
.get(&(buffer_id, buffer_row))
.map(|t| Arc::new(t.to_owned()));
if tasks.is_none() && code_actions.is_none() {
return None;
}
let buffer = location.map(|location| location.buffer).or_else(|| {
let snapshot = this.snapshot(cx);
let (buffer_snapshot, _) =
snapshot.buffer_snapshot.buffer_line_for_row(buffer_row)?;
let buffer_id = buffer_snapshot.remote_id();
this.buffer().read(cx).buffer(buffer_id)
});
let Some(buffer) = buffer else {
return None;
};
this.completion_tasks.clear();
this.discard_inline_completion(cx);
let task_context = tasks.as_ref().zip(this.workspace.clone()).and_then(
|(tasks, (workspace, _))| {
let position = Point::new(buffer_row, tasks.column);
let position = Point::new(buffer_row, tasks.1.column);
let range_start = buffer.read(cx).anchor_at(position, Bias::Right);
let location = Location {
buffer: buffer.clone(),
@@ -3901,22 +3910,33 @@ impl Editor {
.flatten()
},
);
let tasks = tasks
.zip(task_context.as_ref())
.map(|(tasks, task_context)| {
Arc::new(ResolvedTasks {
templates: tasks
.templates
.iter()
.filter_map(|(kind, template)| {
template
.resolve_task(&kind.to_id_base(), &task_context)
.map(|task| (kind.clone(), task))
})
.collect(),
position: Point::new(buffer_row, tasks.column),
})
});
let tasks = tasks.zip(task_context).map(|(tasks, mut task_context)| {
// Fill in the environmental variables from the tree-sitter captures
let mut additional_task_variables = TaskVariables::default();
for (capture_name, value) in tasks.1.extra_variables.clone() {
additional_task_variables.insert(
task::VariableName::Custom(capture_name.into()),
value.clone(),
);
}
task_context
.task_variables
.extend(additional_task_variables);
Arc::new(ResolvedTasks {
templates: tasks
.1
.templates
.iter()
.filter_map(|(kind, template)| {
template
.resolve_task(&kind.to_id_base(), &task_context)
.map(|task| (kind.clone(), task))
})
.collect(),
position: Point::new(buffer_row, tasks.1.column),
})
});
let spawn_straight_away = tasks
.as_ref()
.map_or(false, |tasks| tasks.templates.len() == 1)
@@ -4505,8 +4525,8 @@ impl Editor {
self.tasks.clear()
}
fn insert_tasks(&mut self, row: u32, tasks: RunnableTasks) {
if let Some(_) = self.tasks.insert(row, tasks) {
fn insert_tasks(&mut self, key: (BufferId, u32), value: (usize, RunnableTasks)) {
if let Some(_) = self.tasks.insert(key, value) {
// This case should hopefully be rare, but just in case...
log::error!("multiple different run targets found on a single line, only the last target will be rendered")
}
@@ -4639,11 +4659,12 @@ impl Editor {
delta +=
snippet.text.len() as isize - insertion_range.len() as isize;
let start = ((insertion_start + tabstop_range.start) as usize)
.min(snapshot.len());
let end = ((insertion_start + tabstop_range.end) as usize)
.min(snapshot.len());
snapshot.anchor_before(start)..snapshot.anchor_after(end)
let start = snapshot.anchor_before(
(insertion_start + tabstop_range.start) as usize,
);
let end = snapshot
.anchor_after((insertion_start + tabstop_range.end) as usize);
start..end
})
})
.collect::<Vec<_>>();
@@ -7725,8 +7746,8 @@ impl Editor {
this.update(&mut cx, |this, _| {
this.clear_tasks();
for (row, tasks) in rows {
this.insert_tasks(row, tasks);
for (key, value) in rows {
this.insert_tasks(key, value);
}
})
.ok();
@@ -7735,32 +7756,47 @@ impl Editor {
fn fetch_runnable_ranges(
snapshot: &DisplaySnapshot,
range: Range<Anchor>,
) -> Vec<(Range<usize>, Runnable)> {
) -> Vec<language::RunnableRange> {
snapshot.buffer_snapshot.runnable_ranges(range).collect()
}
fn runnable_rows(
project: Model<Project>,
snapshot: DisplaySnapshot,
runnable_ranges: Vec<(Range<usize>, Runnable)>,
runnable_ranges: Vec<RunnableRange>,
mut cx: AsyncWindowContext,
) -> Vec<(u32, RunnableTasks)> {
) -> Vec<((BufferId, u32), (usize, RunnableTasks))> {
runnable_ranges
.into_iter()
.filter_map(|(multi_buffer_range, mut runnable)| {
.filter_map(|mut runnable| {
let (tasks, _) = cx
.update(|cx| Self::resolve_runnable(project.clone(), &mut runnable, cx))
.update(|cx| {
Self::resolve_runnable(project.clone(), &mut runnable.runnable, cx)
})
.ok()?;
if tasks.is_empty() {
return None;
}
let point = multi_buffer_range.start.to_point(&snapshot.buffer_snapshot);
let point = runnable.run_range.start.to_point(&snapshot.buffer_snapshot);
let row = snapshot
.buffer_snapshot
.buffer_line_for_row(point.row)?
.1
.start
.row;
Some((
point.row,
RunnableTasks {
templates: tasks,
column: point.column,
},
(runnable.buffer_id, row),
(
runnable.run_range.start,
RunnableTasks {
templates: tasks,
column: point.column,
extra_variables: runnable.extra_captures,
},
),
))
})
.collect()
@@ -9206,6 +9242,8 @@ impl Editor {
self.active_diagnostics = Some(active_diagnostics);
}
}
self.scrollbar_marker_state.dirty = true;
}
}
@@ -9239,6 +9277,7 @@ impl Editor {
}
cx.notify();
self.scrollbar_marker_state.dirty = true;
}
}
@@ -10114,6 +10153,7 @@ impl Editor {
predecessor,
excerpts,
} => {
self.tasks_update_task = Some(self.refresh_runnables(cx));
cx.emit(EditorEvent::ExcerptsAdded {
buffer: buffer.clone(),
predecessor: *predecessor,

View File

@@ -1406,17 +1406,24 @@ impl EditorElement {
};
editor
.tasks
.keys()
.map(|row| {
.iter()
.filter_map(|((_, row), (multibuffer_offset, _))| {
if snapshot.is_line_folded(*row) {
return None;
}
let display_row = snapshot
.buffer_snapshot
.offset_to_point(*multibuffer_offset)
.to_display_point(&snapshot.display_snapshot)
.row();
let button = editor.render_run_indicator(
&self.style,
Some(*row) == active_task_indicator_row,
*row,
display_row,
cx,
);
let display_row = Point::new(*row, 0)
.to_display_point(&snapshot.display_snapshot)
.row();
let button = prepaint_gutter_button(
button,
display_row,
@@ -1426,7 +1433,7 @@ impl EditorElement {
gutter_hitbox,
cx,
);
button
Some(button)
})
.collect_vec()
})
@@ -2859,7 +2866,6 @@ impl EditorElement {
let snapshot = layout.position_map.snapshot.clone();
let theme = cx.theme().clone();
let scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
let max_row = layout.max_row;
editor.scrollbar_marker_state.dirty = false;
editor.scrollbar_marker_state.pending_refresh =
@@ -2868,12 +2874,12 @@ impl EditorElement {
let scrollbar_markers = cx
.background_executor()
.spawn(async move {
let max_point = snapshot.display_snapshot.buffer_snapshot.max_point();
let mut marker_quads = Vec::new();
if scrollbar_settings.git_diff {
let marker_row_ranges = snapshot
.buffer_snapshot
.git_diff_hunks_in_range(0..max_row)
.git_diff_hunks_in_range(0..max_point.row)
.map(|hunk| {
let start_display_row =
Point::new(hunk.associated_range.start, 0)
@@ -2942,9 +2948,6 @@ impl EditorElement {
}
if scrollbar_settings.diagnostics {
let max_point =
snapshot.display_snapshot.buffer_snapshot.max_point();
let diagnostics = snapshot
.buffer_snapshot
.diagnostics_in_range::<_, Point>(
@@ -3661,7 +3664,7 @@ impl Element for EditorElement {
let mut style = Style::default();
style.size.width = relative(1.).into();
style.size.height = self.style.text.line_height_in_pixels(rem_size).into();
cx.request_layout(&style, None)
cx.request_layout(style, None)
}
EditorMode::AutoHeight { max_lines } => {
let editor_handle = cx.view().clone();
@@ -3685,7 +3688,7 @@ impl Element for EditorElement {
let mut style = Style::default();
style.size.width = relative(1.).into();
style.size.height = relative(1.).into();
cx.request_layout(&style, None)
cx.request_layout(style, None)
}
};
@@ -4032,20 +4035,25 @@ impl Element for EditorElement {
if gutter_settings.code_actions {
let newest_selection_point =
newest_selection_head.to_point(&snapshot.display_snapshot);
let has_test_indicator = self
.editor
.read(cx)
.tasks
.contains_key(&newest_selection_point.row);
if !has_test_indicator {
code_actions_indicator = self.layout_code_actions_indicator(
line_height,
newest_selection_head,
scroll_pixel_position,
&gutter_dimensions,
&gutter_hitbox,
cx,
);
let buffer = snapshot
.buffer_snapshot
.buffer_line_for_row(newest_selection_point.row);
if let Some((buffer, range)) = buffer {
let buffer_id = buffer.remote_id();
let row = range.start.row;
let has_test_indicator =
self.editor.read(cx).tasks.contains_key(&(buffer_id, row));
if !has_test_indicator {
code_actions_indicator = self.layout_code_actions_indicator(
line_height,
newest_selection_head,
scroll_pixel_position,
&gutter_dimensions,
&gutter_hitbox,
cx,
);
}
}
}
}
@@ -4145,7 +4153,6 @@ impl Element for EditorElement {
gutter_dimensions,
content_origin,
scrollbar_layout,
max_row,
active_rows,
highlighted_rows,
highlighted_ranges,
@@ -4278,7 +4285,6 @@ pub struct EditorLayout {
cursors: Vec<(DisplayPoint, Hsla)>,
visible_cursors: Vec<CursorLayout>,
selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,
max_row: u32,
code_actions_indicator: Option<AnyElement>,
test_indicators: Vec<AnyElement>,
fold_indicators: Vec<Option<AnyElement>>,
@@ -4495,20 +4501,26 @@ fn layout_line(
) -> Result<ShapedLine> {
let mut line = snapshot.line(row);
if line.len() > MAX_LINE_LEN {
let mut len = MAX_LINE_LEN;
while !line.is_char_boundary(len) {
len -= 1;
}
let len = {
let line_len = line.len();
if line_len > MAX_LINE_LEN {
let mut len = MAX_LINE_LEN;
while !line.is_char_boundary(len) {
len -= 1;
}
line.truncate(len);
}
line.truncate(len);
len
} else {
line_len
}
};
cx.text_system().shape_line(
line.into(),
style.text.font_size.to_pixels(cx.rem_size()),
&[TextRun {
len: snapshot.line_len(row) as usize,
len,
font: style.text.font(),
color: Hsla::default(),
background_color: None,

View File

@@ -6,7 +6,7 @@ use anyhow::Context;
use gpui::WindowContext;
use language::{BasicContextProvider, ContextProvider};
use project::{Location, WorktreeId};
use task::{TaskContext, TaskVariables};
use task::{TaskContext, TaskVariables, VariableName};
use util::ResultExt;
use workspace::Workspace;
@@ -79,7 +79,21 @@ pub(crate) fn task_context_with_editor(
buffer,
range: start..end,
};
task_context_for_location(workspace, location, cx)
task_context_for_location(workspace, location.clone(), cx).map(|mut task_context| {
for range in location
.buffer
.read(cx)
.snapshot()
.runnable_ranges(location.range)
{
for (capture_name, value) in range.extra_captures {
task_context
.task_variables
.insert(VariableName::Custom(capture_name.into()), value);
}
}
task_context
})
}
pub fn task_context(workspace: &Workspace, cx: &mut WindowContext<'_>) -> TaskContext {

View File

@@ -164,7 +164,7 @@ pub struct ExtensionIndexLanguageEntry {
actions!(zed, [ReloadExtensions]);
pub fn init(
fs: Arc<fs::RealFs>,
fs: Arc<dyn Fs>,
client: Arc<Client>,
node_runtime: Arc<dyn NodeRuntime>,
language_registry: Arc<LanguageRegistry>,

View File

@@ -23,7 +23,7 @@ impl ExtensionCard {
}
impl ParentElement for ExtensionCard {
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
}
}

View File

@@ -334,7 +334,7 @@ impl PickerDelegate for NewPathDelegate {
if exists {
self.should_dismiss = false;
let answer = cx.prompt(
gpui::PromptLevel::Destructive,
gpui::PromptLevel::Critical,
&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.",

View File

@@ -29,7 +29,7 @@ git.workspace = true
git2.workspace = true
serde.workspace = true
serde_json.workspace = true
libc = "0.2"
libc.workspace = true
time.workspace = true
gpui = { workspace = true, optional = true }

View File

@@ -642,8 +642,8 @@ impl AppContext {
}
/// Restart the application.
pub fn restart(&self) {
self.platform.restart()
pub fn restart(&self, binary_path: Option<PathBuf>) {
self.platform.restart(binary_path)
}
/// Returns the local timezone at the platform level.

View File

@@ -140,7 +140,7 @@ pub trait RenderOnce: 'static {
/// can accept any number of any kind of child elements
pub trait ParentElement {
/// Extend this element's children with the given child elements.
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>);
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>);
/// Add a single child element to this element.
fn child(mut self, child: impl IntoElement) -> Self
@@ -603,7 +603,7 @@ impl Element for Empty {
_id: Option<&GlobalElementId>,
cx: &mut WindowContext,
) -> (LayoutId, Self::RequestLayoutState) {
(cx.request_layout(&Style::default(), None), ())
(cx.request_layout(Style::default(), None), ())
}
fn prepaint(

View File

@@ -63,7 +63,7 @@ impl Anchored {
}
impl ParentElement for Anchored {
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
}
}
@@ -93,7 +93,7 @@ impl Element for Anchored {
..Style::default()
};
let layout_id = cx.request_layout(&anchored_style, child_layout_ids.iter().copied());
let layout_id = cx.request_layout(anchored_style, child_layout_ids.iter().copied());
(layout_id, AnchoredState { child_layout_ids })
}

View File

@@ -49,7 +49,7 @@ impl<T: 'static> Element for Canvas<T> {
) -> (crate::LayoutId, Self::RequestLayoutState) {
let mut style = Style::default();
style.refine(&self.style);
let layout_id = cx.request_layout(&style, []);
let layout_id = cx.request_layout(style.clone(), []);
(layout_id, style)
}

View File

@@ -1139,7 +1139,7 @@ impl Element for Div {
.iter_mut()
.map(|child| child.request_layout(cx))
.collect::<SmallVec<_>>();
cx.request_layout(&style, child_layout_ids.iter().copied())
cx.request_layout(style, child_layout_ids.iter().copied())
})
});
(layout_id, DivFrameState { child_layout_ids })
@@ -2337,7 +2337,7 @@ impl<E> ParentElement for Focusable<E>
where
E: ParentElement,
{
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.element.extend(elements)
}
}
@@ -2430,7 +2430,7 @@ impl<E> ParentElement for Stateful<E>
where
E: ParentElement,
{
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.element.extend(elements)
}
}

View File

@@ -262,7 +262,7 @@ impl Element for Img {
}
}
cx.request_layout(&style, [])
cx.request_layout(style, [])
});
(layout_id, ())
}

View File

@@ -766,7 +766,7 @@ impl Element for List {
let mut style = Style::default();
style.refine(&self.style);
cx.with_text_style(style.text_style().cloned(), |cx| {
cx.request_layout(&style, None)
cx.request_layout(style, None)
})
}
};

View File

@@ -51,7 +51,7 @@ impl Element for Svg {
) -> (LayoutId, Self::RequestLayoutState) {
let layout_id = self
.interactivity
.request_layout(global_id, cx, |style, cx| cx.request_layout(&style, None));
.request_layout(global_id, cx, |style, cx| cx.request_layout(style, None));
(layout_id, ())
}

View File

@@ -17,7 +17,7 @@ use std::{
use util::ResultExt;
impl Element for &'static str {
type RequestLayoutState = TextState;
type RequestLayoutState = TextLayout;
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
@@ -29,7 +29,7 @@ impl Element for &'static str {
_id: Option<&GlobalElementId>,
cx: &mut WindowContext,
) -> (LayoutId, Self::RequestLayoutState) {
let mut state = TextState::default();
let mut state = TextLayout::default();
let layout_id = state.layout(SharedString::from(*self), None, cx);
(layout_id, state)
}
@@ -37,21 +37,22 @@ impl Element for &'static str {
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_bounds: Bounds<Pixels>,
_text_state: &mut Self::RequestLayoutState,
bounds: Bounds<Pixels>,
text_layout: &mut Self::RequestLayoutState,
_cx: &mut WindowContext,
) {
text_layout.prepaint(bounds, self)
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
text_state: &mut TextState,
_bounds: Bounds<Pixels>,
text_layout: &mut TextLayout,
_: &mut (),
cx: &mut WindowContext,
) {
text_state.paint(bounds, self, cx)
text_layout.paint(self, cx)
}
}
@@ -72,7 +73,7 @@ impl IntoElement for String {
}
impl Element for SharedString {
type RequestLayoutState = TextState;
type RequestLayoutState = TextLayout;
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
@@ -86,7 +87,7 @@ impl Element for SharedString {
cx: &mut WindowContext,
) -> (LayoutId, Self::RequestLayoutState) {
let mut state = TextState::default();
let mut state = TextLayout::default();
let layout_id = state.layout(self.clone(), None, cx);
(layout_id, state)
}
@@ -94,22 +95,22 @@ impl Element for SharedString {
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_bounds: Bounds<Pixels>,
_text_state: &mut Self::RequestLayoutState,
bounds: Bounds<Pixels>,
text_layout: &mut Self::RequestLayoutState,
_cx: &mut WindowContext,
) {
text_layout.prepaint(bounds, self.as_ref())
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
text_state: &mut Self::RequestLayoutState,
_bounds: Bounds<Pixels>,
text_layout: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
cx: &mut WindowContext,
) {
let text_str: &str = self.as_ref();
text_state.paint(bounds, text_str, cx)
text_layout.paint(self.as_ref(), cx)
}
}
@@ -129,6 +130,7 @@ impl IntoElement for SharedString {
pub struct StyledText {
text: SharedString,
runs: Option<Vec<TextRun>>,
layout: TextLayout,
}
impl StyledText {
@@ -137,9 +139,15 @@ impl StyledText {
StyledText {
text: text.into(),
runs: None,
layout: TextLayout::default(),
}
}
/// todo!()
pub fn layout(&self) -> &TextLayout {
&self.layout
}
/// Set the styling attributes for the given text, as well as
/// as any ranges of text that have had their style customized.
pub fn with_highlights(
@@ -167,10 +175,16 @@ impl StyledText {
self.runs = Some(runs);
self
}
/// Set the text runs for this piece of text.
pub fn with_runs(mut self, runs: Vec<TextRun>) -> Self {
self.runs = Some(runs);
self
}
}
impl Element for StyledText {
type RequestLayoutState = TextState;
type RequestLayoutState = ();
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
@@ -184,29 +198,29 @@ impl Element for StyledText {
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)
let layout_id = self.layout.layout(self.text.clone(), self.runs.take(), cx);
(layout_id, ())
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_bounds: Bounds<Pixels>,
_state: &mut Self::RequestLayoutState,
bounds: Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
_cx: &mut WindowContext,
) {
self.layout.prepaint(bounds, &self.text)
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
text_state: &mut Self::RequestLayoutState,
_bounds: Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
cx: &mut WindowContext,
) {
text_state.paint(bounds, &self.text, cx)
self.layout.paint(&self.text, cx)
}
}
@@ -218,19 +232,20 @@ impl IntoElement for StyledText {
}
}
#[doc(hidden)]
/// todo!()
#[derive(Default, Clone)]
pub struct TextState(Arc<Mutex<Option<TextStateInner>>>);
pub struct TextLayout(Arc<Mutex<Option<TextLayoutInner>>>);
struct TextStateInner {
struct TextLayoutInner {
lines: SmallVec<[WrappedLine; 1]>,
line_height: Pixels,
wrap_width: Option<Pixels>,
size: Option<Size<Pixels>>,
bounds: Option<Bounds<Pixels>>,
}
impl TextState {
fn lock(&self) -> MutexGuard<Option<TextStateInner>> {
impl TextLayout {
fn lock(&self) -> MutexGuard<Option<TextLayoutInner>> {
self.0.lock()
}
@@ -265,11 +280,11 @@ impl TextState {
None
};
if let Some(text_state) = element_state.0.lock().as_ref() {
if text_state.size.is_some()
&& (wrap_width.is_none() || wrap_width == text_state.wrap_width)
if let Some(text_layout) = element_state.0.lock().as_ref() {
if text_layout.size.is_some()
&& (wrap_width.is_none() || wrap_width == text_layout.wrap_width)
{
return text_state.size.unwrap();
return text_layout.size.unwrap();
}
}
@@ -283,11 +298,12 @@ impl TextState {
)
.log_err()
else {
element_state.lock().replace(TextStateInner {
element_state.lock().replace(TextLayoutInner {
lines: Default::default(),
line_height,
wrap_width,
size: Some(Size::default()),
bounds: None,
});
return Size::default();
};
@@ -299,11 +315,12 @@ impl TextState {
size.width = size.width.max(line_size.width).ceil();
}
element_state.lock().replace(TextStateInner {
element_state.lock().replace(TextLayoutInner {
lines,
line_height,
wrap_width,
size: Some(size),
bounds: None,
});
size
@@ -313,12 +330,25 @@ impl TextState {
layout_id
}
fn paint(&mut self, bounds: Bounds<Pixels>, text: &str, cx: &mut WindowContext) {
fn prepaint(&mut self, bounds: Bounds<Pixels>, text: &str) {
let mut element_state = self.lock();
let element_state = element_state
.as_mut()
.ok_or_else(|| anyhow!("measurement has not been performed on {}", text))
.unwrap();
element_state.bounds = Some(bounds);
}
fn paint(&mut self, text: &str, cx: &mut WindowContext) {
let element_state = self.lock();
let element_state = element_state
.as_ref()
.ok_or_else(|| anyhow!("measurement has not been performed on {}", text))
.unwrap();
let bounds = element_state
.bounds
.ok_or_else(|| anyhow!("prepaint has not been performed on {:?}", text))
.unwrap();
let line_height = element_state.line_height;
let mut line_origin = bounds.origin;
@@ -328,15 +358,19 @@ impl TextState {
}
}
fn index_for_position(&self, bounds: Bounds<Pixels>, position: Point<Pixels>) -> Option<usize> {
if !bounds.contains(&position) {
return None;
}
/// todo!()
pub fn index_for_position(&self, mut position: Point<Pixels>) -> Result<usize, usize> {
let element_state = self.lock();
let element_state = element_state
.as_ref()
.expect("measurement has not been performed");
let bounds = element_state
.bounds
.expect("prepaint has not been performed");
if position.y < bounds.top() {
return Err(0);
}
let line_height = element_state.line_height;
let mut line_origin = bounds.origin;
@@ -348,14 +382,56 @@ impl TextState {
line_start_ix += line.len() + 1;
} else {
let position_within_line = position - line_origin;
let index_within_line =
line.index_for_position(position_within_line, line_height)?;
return Some(line_start_ix + index_within_line);
match line.index_for_position(position_within_line, line_height) {
Ok(index_within_line) => return Ok(line_start_ix + index_within_line),
Err(index_within_line) => return Err(line_start_ix + index_within_line),
}
}
}
Err(line_start_ix.saturating_sub(1))
}
/// todo!()
pub fn position_for_index(&self, index: usize) -> Option<Point<Pixels>> {
let element_state = self.lock();
let element_state = element_state
.as_ref()
.expect("measurement has not been performed");
let bounds = element_state
.bounds
.expect("prepaint has not been performed");
let line_height = element_state.line_height;
let mut line_origin = bounds.origin;
let mut line_start_ix = 0;
for line in &element_state.lines {
let line_end_ix = line_start_ix + line.len();
if index < line_start_ix {
break;
} else if index > line_end_ix {
line_origin.y += line.size(line_height).height;
line_start_ix = line_end_ix + 1;
continue;
} else {
let ix_within_line = index - line_start_ix;
return Some(line_origin + line.position_for_index(ix_within_line, line_height)?);
}
}
None
}
/// todo!()
pub fn bounds(&self) -> Bounds<Pixels> {
self.0.lock().as_ref().unwrap().bounds.unwrap()
}
/// todo!()
pub fn line_height(&self) -> Pixels {
self.0.lock().as_ref().unwrap().line_height
}
}
/// A text element that can be interacted with.
@@ -436,7 +512,7 @@ impl InteractiveText {
}
impl Element for InteractiveText {
type RequestLayoutState = TextState;
type RequestLayoutState = ();
type PrepaintState = Hitbox;
fn id(&self) -> Option<ElementId> {
@@ -484,17 +560,18 @@ impl Element for InteractiveText {
&mut self,
global_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
text_state: &mut Self::RequestLayoutState,
_: &mut Self::RequestLayoutState,
hitbox: &mut Hitbox,
cx: &mut WindowContext,
) {
let text_layout = self.text.layout().clone();
cx.with_element_state::<InteractiveTextState, _>(
global_id.unwrap(),
|interactive_state, cx| {
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) {
if let Some(ix) = text_layout.index_for_position(mouse_position).ok() {
if self
.clickable_ranges
.iter()
@@ -504,7 +581,7 @@ impl Element for InteractiveText {
}
}
let text_state = text_state.clone();
let text_layout = text_layout.clone();
let mouse_down = interactive_state.mouse_down_index.clone();
if let Some(mouse_down_index) = mouse_down.get() {
let hitbox = hitbox.clone();
@@ -512,7 +589,7 @@ impl Element for InteractiveText {
cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
if let Some(mouse_up_index) =
text_state.index_for_position(bounds, event.position)
text_layout.index_for_position(event.position).ok()
{
click_listener(
&clickable_ranges,
@@ -533,7 +610,7 @@ impl Element for InteractiveText {
cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
if let Some(mouse_down_index) =
text_state.index_for_position(bounds, event.position)
text_layout.index_for_position(event.position).ok()
{
mouse_down.set(Some(mouse_down_index));
cx.refresh();
@@ -546,12 +623,12 @@ impl Element for InteractiveText {
cx.on_mouse_event({
let mut hover_listener = self.hover_listener.take();
let hitbox = hitbox.clone();
let text_state = text_state.clone();
let text_layout = text_layout.clone();
let hovered_index = interactive_state.hovered_index.clone();
move |event: &MouseMoveEvent, phase, cx| {
if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
let current = hovered_index.get();
let updated = text_state.index_for_position(bounds, event.position);
let updated = text_layout.index_for_position(event.position).ok();
if current != updated {
hovered_index.set(updated);
if let Some(hover_listener) = hover_listener.as_ref() {
@@ -567,10 +644,10 @@ impl Element for InteractiveText {
let hitbox = hitbox.clone();
let active_tooltip = interactive_state.active_tooltip.clone();
let pending_mouse_down = interactive_state.mouse_down_index.clone();
let text_state = text_state.clone();
let text_layout = text_layout.clone();
cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
let position = text_state.index_for_position(bounds, event.position);
let position = text_layout.index_for_position(event.position).ok();
let is_hovered = position.is_some()
&& hitbox.is_hovered(cx)
&& pending_mouse_down.get().is_none();
@@ -621,7 +698,7 @@ impl Element for InteractiveText {
});
}
self.text.paint(None, bounds, text_state, &mut (), cx);
self.text.paint(None, bounds, &mut (), &mut (), cx);
((), interactive_state)
},

View File

@@ -98,7 +98,7 @@ pub(crate) trait Platform: 'static {
fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>);
fn quit(&self);
fn restart(&self);
fn restart(&self, binary_path: Option<PathBuf>);
fn activate(&self, ignoring_other_apps: bool);
fn hide(&self);
fn hide_other_apps(&self);
@@ -719,10 +719,6 @@ 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)

View File

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

View File

@@ -360,9 +360,7 @@ impl BladeRenderer {
size: config.size,
usage: gpu::TextureUsage::TARGET,
display_sync: gpu::DisplaySync::Recent,
//Note: this matches the original logic of the Metal backend,
// but ultimaterly we need to switch to `Linear`.
color_space: gpu::ColorSpace::Srgb,
color_space: gpu::ColorSpace::Linear,
allow_exclusive_full_screen: false,
transparent: config.transparent,
};

View File

@@ -88,6 +88,14 @@ fn distance_from_clip_rect(unit_vertex: vec2<f32>, bounds: Bounds, clip_bounds:
return distance_from_clip_rect_impl(position, clip_bounds);
}
// https://gamedev.stackexchange.com/questions/92015/optimized-linear-to-srgb-glsl
fn srgb_to_linear(srgb: vec3<f32>) -> vec3<f32> {
let cutoff = srgb < vec3<f32>(0.04045);
let higher = pow((srgb + vec3<f32>(0.055)) / vec3<f32>(1.055), vec3<f32>(2.4));
let lower = srgb / vec3<f32>(12.92);
return select(higher, lower, cutoff);
}
fn hsla_to_rgba(hsla: Hsla) -> vec4<f32> {
let h = hsla.h * 6.0; // Now, it's an angle but scaled in [0, 6) range
let s = hsla.s;
@@ -97,8 +105,7 @@ fn hsla_to_rgba(hsla: Hsla) -> vec4<f32> {
let c = (1.0 - abs(2.0 * l - 1.0)) * s;
let x = c * (1.0 - abs(h % 2.0 - 1.0));
let m = l - c / 2.0;
var color = vec4<f32>(m, m, m, a);
var color = vec3<f32>(m);
if (h >= 0.0 && h < 1.0) {
color.r += c;
@@ -120,7 +127,12 @@ fn hsla_to_rgba(hsla: Hsla) -> vec4<f32> {
color.b += x;
}
return color;
// Input colors are assumed to be in sRGB space,
// but blending and rendering needs to happen in linear space.
// The output will be converted to sRGB by either the target
// texture format or the swapchain color space.
let linear = srgb_to_linear(color);
return vec4<f32>(linear, a);
}
fn over(below: vec4<f32>, above: vec4<f32>) -> vec4<f32> {
@@ -181,7 +193,8 @@ fn quad_sdf(point: vec2<f32>, bounds: Bounds, corner_radii: Corners) -> f32 {
// target alpha compositing mode.
fn blend_color(color: vec4<f32>, alpha_factor: f32) -> vec4<f32> {
let alpha = color.a * alpha_factor;
return select(vec4<f32>(color.rgb, alpha), vec4<f32>(color.rgb, 1.0) * alpha, globals.premultiplied_alpha != 0u);
let multiplier = select(1.0, alpha, globals.premultiplied_alpha != 0u);
return vec4<f32>(color.rgb * multiplier, alpha);
}
// --- quads --- //

View File

@@ -136,17 +136,21 @@ impl<P: LinuxClient + 'static> Platform for P {
self.with_common(|common| common.signal.stop());
}
fn restart(&self) {
fn restart(&self, binary_path: Option<PathBuf>) {
use std::os::unix::process::CommandExt as _;
// get the process id of the current process
let app_pid = std::process::id().to_string();
// get the path to the executable
let app_path = match self.app_path() {
Ok(path) => path,
Err(err) => {
log::error!("Failed to get app path: {:?}", err);
return;
let app_path = if let Some(path) = binary_path {
path
} else {
match self.app_path() {
Ok(path) => path,
Err(err) => {
log::error!("Failed to get app path: {:?}", err);
return;
}
}
};

View File

@@ -396,7 +396,7 @@ impl Platform for MacPlatform {
}
}
fn restart(&self) {
fn restart(&self, _binary_path: Option<PathBuf>) {
use std::os::unix::process::CommandExt as _;
let app_pid = std::process::id().to_string();

View File

@@ -890,7 +890,7 @@ impl PlatformWindow for MacWindow {
let alert_style = match level {
PromptLevel::Info => 1,
PromptLevel::Warning => 0,
PromptLevel::Critical | PromptLevel::Destructive => 2,
PromptLevel::Critical => 2,
};
let _: () = msg_send![alert, setAlertStyle: alert_style];
let _: () = msg_send![alert, setMessageText: ns_string(msg)];
@@ -905,16 +905,10 @@ impl PlatformWindow for MacWindow {
{
let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)];
let _: () = msg_send![button, setTag: ix as NSInteger];
if level == PromptLevel::Destructive && answer != &"Cancel" {
let _: () = msg_send![button, setHasDestructiveAction: YES];
}
}
if let Some((ix, answer)) = latest_non_cancel_label {
let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)];
let _: () = msg_send![button, setTag: ix as NSInteger];
if level == PromptLevel::Destructive {
let _: () = msg_send![button, setHasDestructiveAction: YES];
}
}
let (done_tx, done_rx) = oneshot::channel();

View File

@@ -140,7 +140,7 @@ impl Platform for TestPlatform {
fn quit(&self) {}
fn restart(&self) {
fn restart(&self, _: Option<PathBuf>) {
unimplemented!()
}

View File

@@ -58,7 +58,14 @@ struct DirectWriteState {
custom_font_collection: IDWriteFontCollection1,
fonts: Vec<FontInfo>,
font_selections: HashMap<Font, FontId>,
font_id_by_postscript_name: HashMap<String, FontId>,
font_id_by_identifier: HashMap<FontIdentifier, FontId>,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
struct FontIdentifier {
postscript_name: String,
weight: i32,
style: i32,
}
impl DirectWriteComponent {
@@ -118,7 +125,7 @@ impl DirectWriteTextSystem {
custom_font_collection,
fonts: Vec::new(),
font_selections: HashMap::default(),
font_id_by_postscript_name: HashMap::default(),
font_id_by_identifier: HashMap::default(),
})))
}
}
@@ -269,8 +276,7 @@ impl DirectWriteState {
let Some(font_face) = font_face_ref.CreateFontFace().log_err() else {
continue;
};
let Some(postscript_name) = get_postscript_name(&font_face, &self.components.locale)
else {
let Some(identifier) = get_font_identifier(&font_face, &self.components.locale) else {
continue;
};
let is_emoji = font_face.IsColorFont().as_bool();
@@ -287,8 +293,7 @@ impl DirectWriteState {
};
let font_id = FontId(self.fonts.len());
self.fonts.push(font_info);
self.font_id_by_postscript_name
.insert(postscript_name, font_id);
self.font_id_by_identifier.insert(identifier, font_id);
return Some(font_id);
}
None
@@ -945,8 +950,8 @@ impl IDWriteTextRenderer_Impl for TextRenderer {
// This `cast()` action here should never fail since we are running on Win10+, and
// `IDWriteFontFace3` requires Win10
let font_face = &font_face.cast::<IDWriteFontFace3>().unwrap();
let Some((postscript_name, font_struct, is_emoji)) =
get_postscript_name_and_font(font_face, &self.locale)
let Some((font_identifier, font_struct, is_emoji)) =
get_font_identifier_and_font_struct(font_face, &self.locale)
else {
log::error!("none postscript name found");
return Ok(());
@@ -954,8 +959,8 @@ impl IDWriteTextRenderer_Impl for TextRenderer {
let font_id = if let Some(id) = context
.text_system
.font_id_by_postscript_name
.get(&postscript_name)
.font_id_by_identifier
.get(&font_identifier)
{
*id
} else {
@@ -1121,39 +1126,60 @@ fn get_font_names_from_collection(
}
}
unsafe fn get_postscript_name_and_font(
fn get_font_identifier_and_font_struct(
font_face: &IDWriteFontFace3,
locale: &str,
) -> Option<(String, Font, bool)> {
) -> Option<(FontIdentifier, Font, bool)> {
let Some(postscript_name) = get_postscript_name(font_face, locale) else {
return None;
};
let Some(localized_family_name) = font_face.GetFamilyNames().log_err() else {
let Some(localized_family_name) = (unsafe { font_face.GetFamilyNames().log_err() }) else {
return None;
};
let Some(family_name) = get_name(localized_family_name, locale) else {
return None;
};
let weight = unsafe { font_face.GetWeight() };
let style = unsafe { font_face.GetStyle() };
let identifier = FontIdentifier {
postscript_name,
weight: weight.0,
style: style.0,
};
let font_struct = Font {
family: family_name.into(),
features: FontFeatures::default(),
weight: font_face.GetWeight().into(),
style: font_face.GetStyle().into(),
weight: weight.into(),
style: style.into(),
};
let is_emoji = font_face.IsColorFont().as_bool();
Some((postscript_name, font_struct, is_emoji))
let is_emoji = unsafe { font_face.IsColorFont().as_bool() };
Some((identifier, font_struct, is_emoji))
}
unsafe fn get_postscript_name(font_face: &IDWriteFontFace3, locale: &str) -> Option<String> {
let mut info = std::mem::zeroed();
#[inline]
fn get_font_identifier(font_face: &IDWriteFontFace3, locale: &str) -> Option<FontIdentifier> {
let weight = unsafe { font_face.GetWeight().0 };
let style = unsafe { font_face.GetStyle().0 };
get_postscript_name(font_face, locale).map(|postscript_name| FontIdentifier {
postscript_name,
weight,
style,
})
}
#[inline]
fn get_postscript_name(font_face: &IDWriteFontFace3, locale: &str) -> Option<String> {
let mut info = None;
let mut exists = BOOL(0);
font_face
.GetInformationalStrings(
DWRITE_INFORMATIONAL_STRING_POSTSCRIPT_NAME,
&mut info,
&mut exists,
)
.log_err();
unsafe {
font_face
.GetInformationalStrings(
DWRITE_INFORMATIONAL_STRING_POSTSCRIPT_NAME,
&mut info,
&mut exists,
)
.log_err();
}
if !exists.as_bool() || info.is_none() {
return None;
}
@@ -1162,7 +1188,7 @@ unsafe fn get_postscript_name(font_face: &IDWriteFontFace3, locale: &str) -> Opt
}
// https://learn.microsoft.com/en-us/windows/win32/api/dwrite/ne-dwrite-dwrite_font_feature_tag
unsafe fn apply_font_features(
fn apply_font_features(
direct_write_features: &IDWriteTypography,
features: &FontFeatures,
) -> Result<()> {
@@ -1191,11 +1217,15 @@ unsafe fn apply_font_features(
continue;
}
direct_write_features.AddFontFeature(make_direct_write_feature(&tag, enable))?;
unsafe {
direct_write_features.AddFontFeature(make_direct_write_feature(&tag, enable))?;
}
}
unsafe {
direct_write_features.AddFontFeature(feature_liga)?;
direct_write_features.AddFontFeature(feature_clig)?;
direct_write_features.AddFontFeature(feature_calt)?;
}
direct_write_features.AddFontFeature(feature_liga)?;
direct_write_features.AddFontFeature(feature_clig)?;
direct_write_features.AddFontFeature(feature_calt)?;
Ok(())
}
@@ -1231,32 +1261,39 @@ fn make_direct_write_tag(tag_name: &str) -> DWRITE_FONT_FEATURE_TAG {
DWRITE_FONT_FEATURE_TAG(make_open_type_tag(tag_name))
}
unsafe fn get_name(string: IDWriteLocalizedStrings, locale: &str) -> Option<String> {
#[inline]
fn get_name(string: IDWriteLocalizedStrings, locale: &str) -> Option<String> {
let mut locale_name_index = 0u32;
let mut exists = BOOL(0);
string
.FindLocaleName(
&HSTRING::from(locale),
&mut locale_name_index,
&mut exists as _,
)
.log_err();
if !exists.as_bool() {
unsafe {
string
.FindLocaleName(
DEFAULT_LOCALE_NAME,
&mut locale_name_index as _,
&HSTRING::from(locale),
&mut locale_name_index,
&mut exists as _,
)
.log_err();
}
if !exists.as_bool() {
unsafe {
string
.FindLocaleName(
DEFAULT_LOCALE_NAME,
&mut locale_name_index as _,
&mut exists as _,
)
.log_err();
}
if !exists.as_bool() {
return None;
}
}
let name_length = string.GetStringLength(locale_name_index).unwrap() as usize;
let name_length = unsafe { string.GetStringLength(locale_name_index).unwrap() } as usize;
let mut name_vec = vec![0u16; name_length + 1];
string.GetString(locale_name_index, &mut name_vec).unwrap();
unsafe {
string.GetString(locale_name_index, &mut name_vec).unwrap();
}
Some(String::from_utf16_lossy(&name_vec[..name_length]))
}
@@ -1287,7 +1324,8 @@ fn get_system_ui_font_name() -> SharedString {
// Segoe UI is the Windows font intended for user interface text strings.
"Segoe UI".into()
} else {
String::from_utf16_lossy(&info.lfFaceName).into()
let font_name = String::from_utf16_lossy(&info.lfFaceName);
font_name.trim_matches(char::from(0)).to_owned().into()
};
log::info!("Use {} as UI font.", font_family);
font_family

View File

@@ -1,71 +1,97 @@
use std::{
sync::{
atomic::{AtomicIsize, Ordering},
Arc,
},
thread::{current, ThreadId},
time::Duration,
};
use async_task::Runnable;
use flume::Sender;
use parking::Parker;
use parking_lot::Mutex;
use windows::Win32::{Foundation::*, System::Threading::*};
use util::ResultExt;
use windows::{
Foundation::TimeSpan,
System::{
DispatcherQueue, DispatcherQueueController, DispatcherQueueHandler,
Threading::{
ThreadPool, ThreadPoolTimer, TimerElapsedHandler, WorkItemHandler, WorkItemOptions,
WorkItemPriority,
},
},
Win32::System::WinRT::{
CreateDispatcherQueueController, DispatcherQueueOptions, DQTAT_COM_NONE,
DQTYPE_THREAD_CURRENT,
},
};
use crate::{PlatformDispatcher, TaskLabel};
pub(crate) struct WindowsDispatcher {
threadpool: PTP_POOL,
main_sender: Sender<Runnable>,
controller: DispatcherQueueController,
main_queue: DispatcherQueue,
parker: Mutex<Parker>,
main_thread_id: ThreadId,
dispatch_event: HANDLE,
}
impl WindowsDispatcher {
pub(crate) fn new(main_sender: Sender<Runnable>, dispatch_event: HANDLE) -> Self {
let parker = Mutex::new(Parker::new());
let threadpool = unsafe {
let ret = CreateThreadpool(None);
if ret.0 == 0 {
panic!(
"unable to initialize a thread pool: {}",
std::io::Error::last_os_error()
);
}
// set minimum 1 thread in threadpool
let _ = SetThreadpoolThreadMinimum(ret, 1)
.inspect_err(|_| log::error!("unable to configure thread pool"));
unsafe impl Send for WindowsDispatcher {}
unsafe impl Sync for WindowsDispatcher {}
ret
impl WindowsDispatcher {
pub(crate) fn new() -> Self {
let controller = unsafe {
let options = DispatcherQueueOptions {
dwSize: std::mem::size_of::<DispatcherQueueOptions>() as u32,
threadType: DQTYPE_THREAD_CURRENT,
apartmentType: DQTAT_COM_NONE,
};
CreateDispatcherQueueController(options).unwrap()
};
let main_queue = controller.DispatcherQueue().unwrap();
let parker = Mutex::new(Parker::new());
let main_thread_id = current().id();
WindowsDispatcher {
threadpool,
main_sender,
controller,
main_queue,
parker,
main_thread_id,
dispatch_event,
}
}
fn dispatch_on_threadpool(&self, runnable: Runnable) {
unsafe {
let ptr = Box::into_raw(Box::new(runnable));
let environment = get_threadpool_environment(self.threadpool);
let Ok(work) =
CreateThreadpoolWork(Some(threadpool_runner), Some(ptr as _), Some(&environment))
.inspect_err(|_| {
log::error!(
"unable to dispatch work on thread pool: {}",
std::io::Error::last_os_error()
)
})
else {
return;
};
SubmitThreadpoolWork(work);
}
let handler = {
let mut task_wrapper = Some(runnable);
WorkItemHandler::new(move |_| {
task_wrapper.take().unwrap().run();
Ok(())
})
};
ThreadPool::RunWithPriorityAndOptionsAsync(
&handler,
WorkItemPriority::High,
WorkItemOptions::TimeSliced,
)
.log_err();
}
fn dispatch_on_threadpool_after(&self, runnable: Runnable, duration: Duration) {
let handler = {
let mut task_wrapper = Some(runnable);
TimerElapsedHandler::new(move |_| {
task_wrapper.take().unwrap().run();
Ok(())
})
};
let delay = TimeSpan {
// A time period expressed in 100-nanosecond units.
// 10,000,000 ticks per second
Duration: (duration.as_nanos() / 100) as i64,
};
ThreadPoolTimer::CreateTimer(&handler, delay).log_err();
}
}
impl Drop for WindowsDispatcher {
fn drop(&mut self) {
self.controller.ShutdownQueueAsync().log_err();
}
}
@@ -82,38 +108,18 @@ impl PlatformDispatcher for WindowsDispatcher {
}
fn dispatch_on_main_thread(&self, runnable: Runnable) {
self.main_sender
.send(runnable)
.inspect_err(|e| log::error!("Dispatch failed: {e}"))
.ok();
unsafe { SetEvent(self.dispatch_event) }.ok();
let handler = {
let mut task_wrapper = Some(runnable);
DispatcherQueueHandler::new(move || {
task_wrapper.take().unwrap().run();
Ok(())
})
};
self.main_queue.TryEnqueue(&handler).log_err();
}
fn dispatch_after(&self, duration: std::time::Duration, runnable: Runnable) {
if duration.as_millis() == 0 {
self.dispatch_on_threadpool(runnable);
return;
}
unsafe {
let mut handle = std::mem::zeroed();
let task = Arc::new(DelayedTask::new(runnable));
let _ = CreateTimerQueueTimer(
&mut handle,
None,
Some(timer_queue_runner),
Some(Arc::into_raw(task.clone()) as _),
duration.as_millis() as u32,
0,
WT_EXECUTEONLYONCE,
)
.inspect_err(|_| {
log::error!(
"unable to dispatch delayed task: {}",
std::io::Error::last_os_error()
)
});
task.raw_timer_handle.store(handle.0, Ordering::SeqCst);
}
fn dispatch_after(&self, duration: Duration, runnable: Runnable) {
self.dispatch_on_threadpool_after(runnable, duration);
}
fn tick(&self, _background_only: bool) -> bool {
@@ -128,48 +134,3 @@ impl PlatformDispatcher for WindowsDispatcher {
self.parker.lock().unparker()
}
}
extern "system" fn threadpool_runner(
_: PTP_CALLBACK_INSTANCE,
ptr: *mut std::ffi::c_void,
_: PTP_WORK,
) {
unsafe {
let runnable = Box::from_raw(ptr as *mut Runnable);
runnable.run();
}
}
unsafe extern "system" fn timer_queue_runner(ptr: *mut std::ffi::c_void, _: BOOLEAN) {
let task = Arc::from_raw(ptr as *mut DelayedTask);
task.runnable.lock().take().unwrap().run();
unsafe {
let timer = task.raw_timer_handle.load(Ordering::SeqCst);
let _ = DeleteTimerQueueTimer(None, HANDLE(timer), None);
}
}
struct DelayedTask {
runnable: Mutex<Option<Runnable>>,
raw_timer_handle: AtomicIsize,
}
impl DelayedTask {
pub fn new(runnable: Runnable) -> Self {
DelayedTask {
runnable: Mutex::new(Some(runnable)),
raw_timer_handle: AtomicIsize::new(0),
}
}
}
#[inline]
fn get_threadpool_environment(pool: PTP_POOL) -> TP_CALLBACK_ENVIRON_V3 {
TP_CALLBACK_ENVIRON_V3 {
Version: 3, // Win7+, otherwise this value should be 1
Pool: pool,
CallbackPriority: TP_CALLBACK_PRIORITY_NORMAL,
Size: std::mem::size_of::<TP_CALLBACK_ENVIRON_V3>() as _,
..Default::default()
}
}

View File

@@ -171,9 +171,6 @@ fn handle_timer_msg(
state_ptr: Rc<WindowsWindowStatePtr>,
) -> Option<isize> {
if wparam.0 == SIZE_MOVE_LOOP_TIMER_ID {
for runnable in state_ptr.main_receiver.drain() {
runnable.run();
}
handle_paint_msg(handle, state_ptr)
} else {
None
@@ -547,7 +544,7 @@ fn handle_mouse_horizontal_wheel_msg(
let wheel_scroll_chars = lock.mouse_wheel_settings.wheel_scroll_chars;
drop(lock);
let wheel_distance =
(wparam.signed_hiword() as f32 / WHEEL_DELTA as f32) * wheel_scroll_chars as f32;
(-wparam.signed_hiword() as f32 / WHEEL_DELTA as f32) * wheel_scroll_chars as f32;
let mut cursor_point = POINT {
x: lparam.signed_loword().into(),
y: lparam.signed_hiword().into(),
@@ -585,7 +582,10 @@ fn handle_ime_position(handle: HWND, state_ptr: Rc<WindowsWindowStatePtr>) -> Op
let scale_factor = lock.scale_factor;
drop(lock);
let caret_range = input_handler.selected_text_range().unwrap_or_default();
let Some(caret_range) = input_handler.selected_text_range() else {
state_ptr.state.borrow_mut().input_handler = Some(input_handler);
return Some(0);
};
let caret_position = input_handler.bounds_for_range(caret_range).unwrap();
state_ptr.state.borrow_mut().input_handler = Some(input_handler);
let config = CANDIDATEFORM {

View File

@@ -13,7 +13,6 @@ use std::{
use ::util::ResultExt;
use anyhow::{anyhow, Context, Result};
use async_task::Runnable;
use copypasta::{ClipboardContext, ClipboardProvider};
use futures::channel::oneshot::{self, Receiver};
use itertools::Itertools;
@@ -42,11 +41,9 @@ pub(crate) struct WindowsPlatform {
raw_window_handles: RwLock<SmallVec<[HWND; 4]>>,
// The below members will never change throughout the entire lifecycle of the app.
icon: HICON,
main_receiver: flume::Receiver<Runnable>,
background_executor: BackgroundExecutor,
foreground_executor: ForegroundExecutor,
text_system: Arc<dyn PlatformTextSystem>,
dispatch_event: OwnedHandle,
}
pub(crate) struct WindowsPlatformState {
@@ -85,10 +82,7 @@ impl WindowsPlatform {
unsafe {
OleInitialize(None).expect("unable to initialize Windows OLE");
}
let (main_sender, main_receiver) = flume::unbounded::<Runnable>();
let dispatch_event =
OwnedHandle::new(unsafe { CreateEventW(None, false, false, None) }.unwrap());
let dispatcher = Arc::new(WindowsDispatcher::new(main_sender, dispatch_event.to_raw()));
let dispatcher = Arc::new(WindowsDispatcher::new());
let background_executor = BackgroundExecutor::new(dispatcher.clone());
let foreground_executor = ForegroundExecutor::new(dispatcher);
let text_system = if let Some(direct_write) = DirectWriteTextSystem::new().log_err() {
@@ -106,18 +100,9 @@ impl WindowsPlatform {
state,
raw_window_handles,
icon,
main_receiver,
background_executor,
foreground_executor,
text_system,
dispatch_event,
}
}
#[inline]
fn run_foreground_tasks(&self) {
for runnable in self.main_receiver.drain() {
runnable.run();
}
}
@@ -201,7 +186,6 @@ impl Platform for WindowsPlatform {
fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>) {
on_finish_launching();
let dispatch_event = self.dispatch_event.to_raw();
let vsync_event = create_event().unwrap();
let timer_stop_event = create_event().unwrap();
let raw_timer_stop_event = timer_stop_event.to_raw();
@@ -209,7 +193,7 @@ impl Platform for WindowsPlatform {
'a: loop {
let wait_result = unsafe {
MsgWaitForMultipleObjects(
Some(&[vsync_event.to_raw(), dispatch_event]),
Some(&[vsync_event.to_raw()]),
false,
INFINITE,
QS_ALLINPUT,
@@ -221,12 +205,8 @@ impl Platform for WindowsPlatform {
WAIT_EVENT(0) => {
self.redraw_all();
}
// foreground tasks are dispatched
WAIT_EVENT(1) => {
self.run_foreground_tasks();
}
// Windows thread messages are posted
WAIT_EVENT(2) => {
WAIT_EVENT(1) => {
let mut msg = MSG::default();
unsafe {
while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
@@ -245,9 +225,6 @@ impl Platform for WindowsPlatform {
}
}
}
// foreground tasks may have been queued in the message handlers
self.run_foreground_tasks();
}
_ => {
log::error!("Something went wrong while waiting {:?}", wait_result);
@@ -268,7 +245,7 @@ impl Platform for WindowsPlatform {
.detach();
}
fn restart(&self) {
fn restart(&self, _: Option<PathBuf>) {
let pid = std::process::id();
let Some(app_path) = self.app_path().log_err() else {
return;
@@ -344,7 +321,6 @@ impl Platform for WindowsPlatform {
options,
self.icon,
self.foreground_executor.clone(),
self.main_receiver.clone(),
lock.settings.mouse_wheel_settings,
lock.current_cursor,
);
@@ -684,7 +660,7 @@ impl Platform for WindowsPlatform {
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
let mut ctx = ClipboardContext::new().unwrap();
let content = ctx.get_contents().unwrap();
let content = ctx.get_contents().ok()?;
Some(ClipboardItem {
text: content,
metadata: None,
@@ -813,8 +789,8 @@ unsafe fn show_savefile_dialog(directory: PathBuf) -> Result<IFileSaveDialog> {
fn begin_vsync_timer(vsync_event: HANDLE, timer_stop_event: OwnedHandle) {
let vsync_fn = select_vsync_fn();
std::thread::spawn(move || {
while vsync_fn(timer_stop_event.to_raw()) {
std::thread::spawn(move || loop {
if vsync_fn(timer_stop_event.to_raw()) {
if unsafe { SetEvent(vsync_event) }.log_err().is_none() {
break;
}

View File

@@ -118,12 +118,14 @@ pub(crate) fn load_cursor(style: CursorStyle) -> HCURSOR {
CursorStyle::IBeam | CursorStyle::IBeamCursorForVerticalLayout => (&IBEAM, IDC_IBEAM),
CursorStyle::Crosshair => (&CROSS, IDC_CROSS),
CursorStyle::PointingHand | CursorStyle::DragLink => (&HAND, IDC_HAND),
CursorStyle::ResizeLeft | CursorStyle::ResizeRight | CursorStyle::ResizeLeftRight => {
(&SIZEWE, IDC_SIZEWE)
}
CursorStyle::ResizeUp | CursorStyle::ResizeDown | CursorStyle::ResizeUpDown => {
(&SIZENS, IDC_SIZENS)
}
CursorStyle::ResizeLeft
| CursorStyle::ResizeRight
| CursorStyle::ResizeLeftRight
| CursorStyle::ResizeColumn => (&SIZEWE, IDC_SIZEWE),
CursorStyle::ResizeUp
| CursorStyle::ResizeDown
| CursorStyle::ResizeUpDown
| CursorStyle::ResizeRow => (&SIZENS, IDC_SIZENS),
CursorStyle::OperationNotAllowed => (&NO, IDC_NO),
_ => (&ARROW, IDC_ARROW),
};

View File

@@ -12,7 +12,6 @@ use std::{
use ::util::ResultExt;
use anyhow::Context;
use async_task::Runnable;
use futures::channel::oneshot::{self, Receiver};
use itertools::Itertools;
use raw_window_handle as rwh;
@@ -58,7 +57,6 @@ pub(crate) struct WindowsWindowStatePtr {
pub(crate) handle: AnyWindowHandle,
pub(crate) hide_title_bar: bool,
pub(crate) executor: ForegroundExecutor,
pub(crate) main_receiver: flume::Receiver<Runnable>,
}
impl WindowsWindowState {
@@ -208,7 +206,6 @@ impl WindowsWindowStatePtr {
handle: context.handle,
hide_title_bar: context.hide_title_bar,
executor: context.executor.clone(),
main_receiver: context.main_receiver.clone(),
})
}
}
@@ -232,7 +229,6 @@ struct WindowCreateContext {
display: WindowsDisplay,
transparent: bool,
executor: ForegroundExecutor,
main_receiver: flume::Receiver<Runnable>,
mouse_wheel_settings: MouseWheelSettings,
current_cursor: HCURSOR,
}
@@ -243,7 +239,6 @@ impl WindowsWindow {
params: WindowParams,
icon: HICON,
executor: ForegroundExecutor,
main_receiver: flume::Receiver<Runnable>,
mouse_wheel_settings: MouseWheelSettings,
current_cursor: HCURSOR,
) -> Self {
@@ -272,7 +267,6 @@ impl WindowsWindow {
display: WindowsDisplay::primary_monitor().unwrap(),
transparent: params.window_background != WindowBackgroundAppearance::Opaque,
executor,
main_receiver,
mouse_wheel_settings,
current_cursor,
};
@@ -443,7 +437,7 @@ impl PlatformWindow for WindowsWindow {
title = windows::core::w!("Warning");
main_icon = TD_WARNING_ICON;
}
crate::PromptLevel::Critical | crate::PromptLevel::Destructive => {
crate::PromptLevel::Critical => {
title = windows::core::w!("Critical");
main_icon = TD_ERROR_ICON;
}

View File

@@ -49,7 +49,7 @@ impl TaffyLayoutEngine {
pub fn request_layout(
&mut self,
style: &Style,
style: Style,
rem_size: Pixels,
children: &[LayoutId],
) -> LayoutId {
@@ -66,12 +66,11 @@ impl TaffyLayoutEngine {
.new_with_children(taffy_style, unsafe { std::mem::transmute(children) })
.expect(EXPECT_MESSAGE)
.into();
for child_id in children {
self.children_to_parents.insert(*child_id, parent_id);
}
self.children_to_parents
.extend(children.into_iter().map(|child_id| (*child_id, parent_id)));
parent_id
};
self.styles.insert(layout_id, style.clone());
self.styles.insert(layout_id, style);
layout_id
}
@@ -82,7 +81,6 @@ impl TaffyLayoutEngine {
measure: impl FnMut(Size<Option<Pixels>>, Size<AvailableSpace>, &mut WindowContext) -> Size<Pixels>
+ 'static,
) -> LayoutId {
let style = style.clone();
let taffy_style = style.to_taffy(rem_size);
let layout_id = self
@@ -91,7 +89,7 @@ impl TaffyLayoutEngine {
.expect(EXPECT_MESSAGE)
.into();
self.nodes_to_measure.insert(layout_id, Box::new(measure));
self.styles.insert(layout_id, style.clone());
self.styles.insert(layout_id, style);
layout_id
}

View File

@@ -1,4 +1,4 @@
use crate::{px, FontId, GlyphId, Pixels, PlatformTextSystem, Point, Size};
use crate::{point, px, FontId, GlyphId, Pixels, PlatformTextSystem, Point, Size};
use collections::FxHashMap;
use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
use smallvec::SmallVec;
@@ -254,39 +254,83 @@ impl WrappedLineLayout {
/// The index corresponding to a given position in this layout for the given line height.
pub fn index_for_position(
&self,
position: Point<Pixels>,
mut position: Point<Pixels>,
line_height: Pixels,
) -> Option<usize> {
) -> Result<usize, usize> {
let wrapped_line_ix = (position.y / line_height) as usize;
let wrapped_line_start_x = if wrapped_line_ix > 0 {
let wrapped_line_start_index;
let wrapped_line_start_x;
if wrapped_line_ix > 0 {
let Some(line_start_boundary) = self.wrap_boundaries.get(wrapped_line_ix - 1) else {
return None;
return Err(0);
};
let run = &self.unwrapped_layout.runs[line_start_boundary.run_ix];
run.glyphs[line_start_boundary.glyph_ix].position.x
let glyph = &run.glyphs[line_start_boundary.glyph_ix];
wrapped_line_start_index = glyph.index;
wrapped_line_start_x = glyph.position.x;
} else {
Pixels::ZERO
wrapped_line_start_index = 0;
wrapped_line_start_x = Pixels::ZERO;
};
let wrapped_line_end_x = if wrapped_line_ix < self.wrap_boundaries.len() {
let wrapped_line_end_index;
let wrapped_line_end_x;
if wrapped_line_ix < self.wrap_boundaries.len() {
let next_wrap_boundary_ix = wrapped_line_ix;
let next_wrap_boundary = self.wrap_boundaries[next_wrap_boundary_ix];
let run = &self.unwrapped_layout.runs[next_wrap_boundary.run_ix];
run.glyphs[next_wrap_boundary.glyph_ix].position.x
let glyph = &run.glyphs[next_wrap_boundary.glyph_ix];
wrapped_line_end_index = glyph.index;
wrapped_line_end_x = glyph.position.x;
} else {
self.unwrapped_layout.width
wrapped_line_end_index = self.unwrapped_layout.len;
wrapped_line_end_x = self.unwrapped_layout.width;
};
let mut position_in_unwrapped_line = position;
position_in_unwrapped_line.x += wrapped_line_start_x;
if position_in_unwrapped_line.x > wrapped_line_end_x {
None
if position_in_unwrapped_line.x < wrapped_line_start_x {
Err(wrapped_line_start_index)
} else if position_in_unwrapped_line.x >= wrapped_line_end_x {
Err(wrapped_line_end_index)
} else {
self.unwrapped_layout
Ok(self
.unwrapped_layout
.index_for_x(position_in_unwrapped_line.x)
.unwrap())
}
}
/// todo!()
pub fn position_for_index(&self, index: usize, line_height: Pixels) -> Option<Point<Pixels>> {
let mut line_start_ix = 0;
let mut line_end_indices = self
.wrap_boundaries
.iter()
.map(|wrap_boundary| {
let run = &self.unwrapped_layout.runs[wrap_boundary.run_ix];
let glyph = &run.glyphs[wrap_boundary.glyph_ix];
glyph.index
})
.chain([self.len()])
.enumerate();
for (ix, line_end_ix) in line_end_indices {
let line_y = ix as f32 * line_height;
if index < line_start_ix {
break;
} else if index > line_end_ix {
line_start_ix = line_end_ix;
continue;
} else {
let line_start_x = self.unwrapped_layout.x_for_index(line_start_ix);
let x = self.unwrapped_layout.x_for_index(index) - line_start_x;
return Some(point(x, line_y));
}
}
None
}
}
pub(crate) struct LineLayoutCache {

View File

@@ -296,7 +296,7 @@ impl Element for AnyView {
if let Some(style) = self.cached_style.as_ref() {
let mut root_style = Style::default();
root_style.refine(style);
let layout_id = cx.request_layout(&root_style, None);
let layout_id = cx.request_layout(root_style, None);
(layout_id, None)
} else {
let mut element = (self.render)(self, cx);

View File

@@ -2503,7 +2503,7 @@ impl<'a> WindowContext<'a> {
/// This method should only be called as part of the request_layout or prepaint phase of element drawing.
pub fn request_layout(
&mut self,
style: &Style,
style: Style,
children: impl IntoIterator<Item = LayoutId>,
) -> LayoutId {
debug_assert_eq!(

View File

@@ -15,7 +15,7 @@ doctest = false
[dependencies]
anyhow.workspace = true
client.workspace = true
ctrlc.workspace = true
signal-hook.workspace = true
gpui.workspace = true
log.workspace = true
rpc.workspace = true

View File

@@ -1,4 +1,4 @@
use anyhow::Result;
use anyhow::{anyhow, Result};
use client::DevServerProjectId;
use client::{user::UserStore, Client, ClientSettings};
use fs::Fs;
@@ -36,7 +36,7 @@ struct GlobalDevServer(Model<DevServer>);
impl Global for GlobalDevServer {}
pub fn init(client: Arc<Client>, app_state: AppState, cx: &mut AppContext) {
pub fn init(client: Arc<Client>, app_state: AppState, cx: &mut AppContext) -> Task<Result<()>> {
let dev_server = cx.new_model(|cx| DevServer::new(client.clone(), app_state, cx));
cx.set_global(GlobalDevServer(dev_server.clone()));
@@ -49,42 +49,36 @@ pub fn init(client: Arc<Client>, app_state: AppState, cx: &mut AppContext) {
});
});
// Set up a handler when the dev server is shut down by the user pressing Ctrl-C
let (tx, rx) = futures::channel::oneshot::channel();
set_ctrlc_handler(move || tx.send(()).log_err().unwrap()).log_err();
cx.spawn(|cx| async move {
rx.await.log_err();
log::info!("Received interrupt signal");
cx.update(|cx| cx.quit()).log_err();
})
.detach();
#[cfg(not(target_os = "windows"))]
{
use signal_hook::consts::{SIGINT, SIGTERM};
use signal_hook::iterator::Signals;
// Set up a handler when the dev server is shut down
// with ctrl-c or kill
let (tx, rx) = futures::channel::oneshot::channel();
let mut signals = Signals::new(&[SIGTERM, SIGINT]).unwrap();
std::thread::spawn({
move || {
if let Some(sig) = signals.forever().next() {
tx.send(sig).log_err();
}
}
});
cx.spawn(|cx| async move {
if let Ok(sig) = rx.await {
log::info!("received signal {sig:?}");
cx.update(|cx| cx.quit()).log_err();
}
})
.detach();
}
let server_url = ClientSettings::get_global(&cx).server_url.clone();
cx.spawn(|cx| async move {
match client.authenticate_and_connect(false, &cx).await {
Ok(_) => {
log::info!("Connected to {}", server_url);
}
Err(e) => {
log::error!("Error connecting to '{}': {}", server_url, e);
cx.update(|cx| cx.quit()).log_err();
}
}
})
.detach();
}
fn set_ctrlc_handler<F>(f: F) -> Result<(), ctrlc::Error>
where
F: FnOnce() + 'static + Send,
{
let f = std::sync::Mutex::new(Some(f));
ctrlc::set_handler(move || {
if let Ok(mut guard) = f.lock() {
let f = guard.take().expect("f can only be taken once");
f();
}
client
.authenticate_and_connect(false, &cx)
.await
.map_err(|e| anyhow!("Error connecting to '{}': {}", server_url, e))
})
}
@@ -186,7 +180,7 @@ impl DevServer {
let path_exists = fs.is_dir(path).await;
if !path_exists {
return Err(anyhow::anyhow!(ErrorCode::DevServerProjectPathDoesNotExist))?;
return Err(anyhow!(ErrorCode::DevServerProjectPathDoesNotExist))?;
}
Ok(proto::Ack {})

View File

@@ -13,6 +13,7 @@ use crate::{
SyntaxLayer, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatches,
SyntaxSnapshot, ToTreeSitterPoint,
},
task_context::RunnableRange,
LanguageScope, Outline, RunnableTag,
};
use anyhow::{anyhow, Context, Result};
@@ -2993,7 +2994,7 @@ impl BufferSnapshot {
pub fn runnable_ranges(
&self,
range: Range<Anchor>,
) -> impl Iterator<Item = (Range<usize>, Runnable)> + '_ {
) -> impl Iterator<Item = RunnableRange> + '_ {
let offset_range = range.start.to_offset(self)..range.end.to_offset(self);
let mut syntax_matches = self.syntax.matches(offset_range, self, |grammar| {
@@ -3007,31 +3008,49 @@ impl BufferSnapshot {
.collect::<Vec<_>>();
iter::from_fn(move || {
let test_range = syntax_matches
.peek()
.and_then(|mat| {
test_configs[mat.grammar_index].and_then(|test_configs| {
let tags = SmallVec::from_iter(mat.captures.iter().filter_map(|capture| {
test_configs.runnable_tags.get(&capture.index).cloned()
let test_range = syntax_matches.peek().and_then(|mat| {
test_configs[mat.grammar_index].and_then(|test_configs| {
let mut tags: SmallVec<[(Range<usize>, RunnableTag); 1]> =
SmallVec::from_iter(mat.captures.iter().filter_map(|capture| {
test_configs
.runnable_tags
.get(&capture.index)
.cloned()
.map(|tag_name| (capture.node.byte_range(), tag_name))
}));
if tags.is_empty() {
return None;
}
Some((
mat.captures
.iter()
.find(|capture| capture.index == test_configs.run_capture_ix)?,
Runnable {
tags,
language: mat.language,
buffer: self.remote_id(),
},
))
let maximum_range = tags
.iter()
.max_by_key(|(byte_range, _)| byte_range.len())
.map(|(range, _)| range)?
.clone();
tags.sort_by_key(|(range, _)| range == &maximum_range);
let split_point = tags.partition_point(|(range, _)| range != &maximum_range);
let (extra_captures, tags) = tags.split_at(split_point);
let extra_captures = extra_captures
.into_iter()
.map(|(range, name)| {
(
name.0.to_string(),
self.text_for_range(range.clone()).collect::<String>(),
)
})
.collect();
Some(RunnableRange {
run_range: mat
.captures
.iter()
.find(|capture| capture.index == test_configs.run_capture_ix)
.map(|mat| mat.node.byte_range())?,
runnable: Runnable {
tags: tags.into_iter().cloned().map(|(_, tag)| tag).collect(),
language: mat.language,
buffer: self.remote_id(),
},
extra_captures,
buffer_id: self.remote_id(),
})
})
.map(|(mat, test_tags)| (mat.node.byte_range(), test_tags));
});
syntax_matches.advance();
test_range
})

View File

@@ -57,7 +57,9 @@ use std::{
};
use syntax_map::{QueryCursorHandle, SyntaxSnapshot};
use task::RunnableTag;
pub use task_context::{BasicContextProvider, ContextProvider, ContextProviderWithTasks};
pub use task_context::{
BasicContextProvider, ContextProvider, ContextProviderWithTasks, RunnableRange,
};
use theme::SyntaxTheme;
use tree_sitter::{self, wasmtime, Query, QueryCursor, WasmStore};
use util::http::HttpClient;

View File

@@ -1,12 +1,19 @@
use std::path::Path;
use std::{ops::Range, path::Path};
use crate::Location;
use crate::{Location, Runnable};
use anyhow::Result;
use collections::HashMap;
use gpui::AppContext;
use task::{TaskTemplates, TaskVariables, VariableName};
use text::{Point, ToPoint};
use text::{BufferId, Point, ToPoint};
pub struct RunnableRange {
pub buffer_id: BufferId,
pub run_range: Range<usize>,
pub runnable: Runnable,
pub extra_captures: HashMap<String, String>,
}
/// Language Contexts are used by Zed tasks to extract information about the source file where the tasks are supposed to be scheduled from.
/// Multiple context providers may be used together: by default, Zed provides a base [`BasicContextProvider`] context that fills all non-custom [`VariableName`] variants.
///

View File

@@ -1,6 +1,6 @@
(
(attribute_item (attribute) @_attribute
(#match? @_attribute ".*test"))
(attribute_item (attribute) @attribute
(#match? @attribute ".*test"))
.
(function_item
name: (_) @run)

View File

@@ -0,0 +1,40 @@
[package]
name = "markdown"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/markdown.rs"
doctest = false
[features]
test-support = [
"gpui/test-support",
"util/test-support"
]
[dependencies]
anyhow.workspace = true
futures.workspace = true
gpui.workspace = true
language.workspace = true
linkify.workspace = true
log.workspace = true
pulldown-cmark.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
[dev-dependencies]
assets.workspace = true
env_logger.workspace = true
gpui = { workspace = true, features = ["test-support"] }
languages.workspace = true
node_runtime.workspace = true
settings = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] }

View File

@@ -0,0 +1,181 @@
use assets::Assets;
use gpui::{prelude::*, App, Task, View, WindowOptions};
use language::{language_settings::AllLanguageSettings, LanguageRegistry};
use markdown::{Markdown, MarkdownStyle};
use node_runtime::FakeNodeRuntime;
use settings::SettingsStore;
use std::sync::Arc;
use theme::LoadThemes;
use ui::prelude::*;
use ui::{div, WindowContext};
const MARKDOWN_EXAMPLE: &'static str = r#"
# Markdown Example Document
## Headings
Headings are created by adding one or more `#` symbols before your heading text. The number of `#` you use will determine the size of the heading.
## Emphasis
Emphasis can be added with italics or bold. *This text will be italic*. _This will also be italic_
## Lists
### Unordered Lists
Unordered lists use asterisks `*`, plus `+`, or minus `-` as list markers.
* Item 1
* Item 2
* Item 2a
* Item 2b
### Ordered Lists
Ordered lists use numbers followed by a period.
1. Item 1
2. Item 2
3. Item 3
1. Item 3a
2. Item 3b
## Links
Links are created using the format [http://zed.dev](https://zed.dev).
They can also be detected automatically, for example https://zed.dev/blog.
## Images
Images are like links, but with an exclamation mark `!` in front.
```todo!
![This is an image](/images/logo.png)
```
## Code
Inline `code` can be wrapped with backticks `` ` ``.
```markdown
Inline `code` has `back-ticks around` it.
```
Code blocks can be created by indenting lines by four spaces or with triple backticks ```.
```javascript
function test() {
console.log("notice the blank line before this function?");
}
```
## Blockquotes
Blockquotes are created with `>`.
> This is a blockquote.
## Horizontal Rules
Horizontal rules are created using three or more asterisks `***`, dashes `---`, or underscores `___`.
## Line breaks
This is a
\
line break!
---
Remember, markdown processors may have slight differences and extensions, so always refer to the specific documentation or guides relevant to your platform or editor for the best practices and additional features.
"#;
pub fn main() {
env_logger::init();
App::new().with_assets(Assets).run(|cx| {
let store = SettingsStore::test(cx);
cx.set_global(store);
language::init(cx);
SettingsStore::update(cx, |store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |_| {});
});
let node_runtime = FakeNodeRuntime::new();
let language_registry = Arc::new(LanguageRegistry::new(
Task::ready(()),
cx.background_executor().clone(),
));
languages::init(language_registry.clone(), node_runtime, cx);
theme::init(LoadThemes::JustBase, cx);
Assets.load_fonts(cx).unwrap();
cx.activate(true);
cx.open_window(WindowOptions::default(), |cx| {
cx.new_view(|cx| {
MarkdownExample::new(
MARKDOWN_EXAMPLE.to_string(),
MarkdownStyle {
code_block: gpui::TextStyleRefinement {
font_family: Some("Zed Mono".into()),
color: Some(cx.theme().colors().editor_foreground),
background_color: Some(cx.theme().colors().editor_background),
..Default::default()
},
inline_code: gpui::TextStyleRefinement {
font_family: Some("Zed Mono".into()),
// @nate: Could we add inline-code specific styles to the theme?
color: Some(cx.theme().colors().editor_foreground),
background_color: Some(cx.theme().colors().editor_background),
..Default::default()
},
rule_color: Color::Muted.color(cx),
block_quote_border_color: Color::Muted.color(cx),
block_quote: gpui::TextStyleRefinement {
color: Some(Color::Muted.color(cx)),
..Default::default()
},
link: gpui::TextStyleRefinement {
color: Some(Color::Accent.color(cx)),
underline: Some(gpui::UnderlineStyle {
thickness: px(1.),
color: Some(Color::Accent.color(cx)),
wavy: false,
}),
..Default::default()
},
syntax: cx.theme().syntax().clone(),
selection_background_color: {
let mut selection = cx.theme().players().local().selection;
selection.fade_out(0.7);
selection
},
},
language_registry,
cx,
)
})
});
});
}
struct MarkdownExample {
markdown: View<Markdown>,
}
impl MarkdownExample {
pub fn new(
text: String,
style: MarkdownStyle,
language_registry: Arc<LanguageRegistry>,
cx: &mut WindowContext,
) -> Self {
let markdown = cx.new_view(|cx| Markdown::new(text, style, language_registry, cx));
Self { markdown }
}
}
impl Render for MarkdownExample {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.id("markdown-example")
.debug_selector(|| "foo".into())
.relative()
.bg(gpui::white())
.size_full()
.p_4()
.overflow_y_scroll()
.child(self.markdown.clone())
}
}

View File

@@ -0,0 +1,902 @@
mod parser;
use crate::parser::CodeBlockKind;
use futures::FutureExt;
use gpui::{
point, quad, AnyElement, Bounds, CursorStyle, DispatchPhase, Edges, FontStyle, FontWeight,
GlobalElementId, Hitbox, Hsla, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent, Point,
Render, StrikethroughStyle, Style, StyledText, Task, TextLayout, TextRun, TextStyle,
TextStyleRefinement, View,
};
use language::{Language, LanguageRegistry, Rope};
use parser::{parse_markdown, MarkdownEvent, MarkdownTag, MarkdownTagEnd};
use std::{iter, mem, ops::Range, rc::Rc, sync::Arc};
use theme::SyntaxTheme;
use ui::prelude::*;
use util::{ResultExt, TryFutureExt};
#[derive(Clone)]
pub struct MarkdownStyle {
pub code_block: TextStyleRefinement,
pub inline_code: TextStyleRefinement,
pub block_quote: TextStyleRefinement,
pub link: TextStyleRefinement,
pub rule_color: Hsla,
pub block_quote_border_color: Hsla,
pub syntax: Arc<SyntaxTheme>,
pub selection_background_color: Hsla,
}
pub struct Markdown {
source: String,
selection: Selection,
pressed_link: Option<RenderedLink>,
autoscroll_request: Option<usize>,
style: MarkdownStyle,
parsed_markdown: ParsedMarkdown,
should_reparse: bool,
pending_parse: Option<Task<Option<()>>>,
language_registry: Arc<LanguageRegistry>,
}
impl Markdown {
pub fn new(
source: String,
style: MarkdownStyle,
language_registry: Arc<LanguageRegistry>,
cx: &mut ViewContext<Self>,
) -> Self {
let mut this = Self {
source,
selection: Selection::default(),
pressed_link: None,
autoscroll_request: None,
style,
should_reparse: false,
parsed_markdown: ParsedMarkdown::default(),
pending_parse: None,
language_registry,
};
this.parse(cx);
this
}
pub fn append(&mut self, text: &str, cx: &mut ViewContext<Self>) {
self.source.push_str(text);
self.parse(cx);
}
pub fn source(&self) -> &str {
&self.source
}
fn parse(&mut self, cx: &mut ViewContext<Self>) {
if self.source.is_empty() {
return;
}
if self.pending_parse.is_some() {
self.should_reparse = true;
return;
}
let text = self.source.clone();
let parsed = cx.background_executor().spawn(async move {
let text = SharedString::from(text);
let events = Arc::from(parse_markdown(text.as_ref()));
anyhow::Ok(ParsedMarkdown {
source: text,
events,
})
});
self.should_reparse = false;
self.pending_parse = Some(cx.spawn(|this, mut cx| {
async move {
let parsed = parsed.await?;
this.update(&mut cx, |this, cx| {
this.parsed_markdown = parsed;
this.pending_parse.take();
if this.should_reparse {
this.parse(cx);
}
cx.notify();
})
.ok();
anyhow::Ok(())
}
.log_err()
}));
}
}
impl Render for Markdown {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
MarkdownElement::new(
cx.view().clone(),
self.style.clone(),
self.language_registry.clone(),
)
}
}
#[derive(Copy, Clone, Default, Debug)]
struct Selection {
start: usize,
end: usize,
reversed: bool,
pending: bool,
}
impl Selection {
fn set_head(&mut self, head: usize) {
if head < self.tail() {
if !self.reversed {
self.end = self.start;
self.reversed = true;
}
self.start = head;
} else {
if self.reversed {
self.start = self.end;
self.reversed = false;
}
self.end = head;
}
}
fn tail(&self) -> usize {
if self.reversed {
self.end
} else {
self.start
}
}
}
#[derive(Clone)]
struct ParsedMarkdown {
source: SharedString,
events: Arc<[(Range<usize>, MarkdownEvent)]>,
}
impl Default for ParsedMarkdown {
fn default() -> Self {
Self {
source: SharedString::default(),
events: Arc::from([]),
}
}
}
pub struct MarkdownElement {
markdown: View<Markdown>,
style: MarkdownStyle,
language_registry: Arc<LanguageRegistry>,
}
impl MarkdownElement {
fn new(
markdown: View<Markdown>,
style: MarkdownStyle,
language_registry: Arc<LanguageRegistry>,
) -> Self {
Self {
markdown,
style,
language_registry,
}
}
fn load_language(&self, name: &str, cx: &mut WindowContext) -> Option<Arc<Language>> {
let language = self
.language_registry
.language_for_name(name)
.map(|language| language.ok())
.shared();
match language.clone().now_or_never() {
Some(language) => language,
None => {
let markdown = self.markdown.downgrade();
cx.spawn(|mut cx| async move {
language.await;
markdown.update(&mut cx, |_, cx| cx.notify())
})
.detach_and_log_err(cx);
None
}
}
}
fn paint_selection(
&mut self,
bounds: Bounds<Pixels>,
rendered_text: &RenderedText,
cx: &mut WindowContext,
) {
let selection = self.markdown.read(cx).selection;
let selection_start = rendered_text.position_for_source_index(selection.start);
let selection_end = rendered_text.position_for_source_index(selection.end);
if let Some(((start_position, start_line_height), (end_position, end_line_height))) =
selection_start.zip(selection_end)
{
if start_position.y == end_position.y {
cx.paint_quad(quad(
Bounds::from_corners(
start_position,
point(end_position.x, end_position.y + end_line_height),
),
Pixels::ZERO,
self.style.selection_background_color,
Edges::default(),
Hsla::transparent_black(),
));
} else {
cx.paint_quad(quad(
Bounds::from_corners(
start_position,
point(bounds.right(), start_position.y + start_line_height),
),
Pixels::ZERO,
self.style.selection_background_color,
Edges::default(),
Hsla::transparent_black(),
));
if end_position.y > start_position.y + start_line_height {
cx.paint_quad(quad(
Bounds::from_corners(
point(bounds.left(), start_position.y + start_line_height),
point(bounds.right(), end_position.y),
),
Pixels::ZERO,
self.style.selection_background_color,
Edges::default(),
Hsla::transparent_black(),
));
}
cx.paint_quad(quad(
Bounds::from_corners(
point(bounds.left(), end_position.y),
point(end_position.x, end_position.y + end_line_height),
),
Pixels::ZERO,
self.style.selection_background_color,
Edges::default(),
Hsla::transparent_black(),
));
}
}
}
fn paint_mouse_listeners(
&mut self,
hitbox: &Hitbox,
rendered_text: &RenderedText,
cx: &mut WindowContext,
) {
let is_hovering_link = hitbox.is_hovered(cx)
&& !self.markdown.read(cx).selection.pending
&& rendered_text
.link_for_position(cx.mouse_position())
.is_some();
if is_hovering_link {
cx.set_cursor_style(CursorStyle::PointingHand, hitbox);
} else {
cx.set_cursor_style(CursorStyle::IBeam, hitbox);
}
self.on_mouse_event(cx, {
let rendered_text = rendered_text.clone();
let hitbox = hitbox.clone();
move |markdown, event: &MouseDownEvent, phase, cx| {
if hitbox.is_hovered(cx) {
if phase.bubble() {
if let Some(link) = rendered_text.link_for_position(event.position) {
markdown.pressed_link = Some(link.clone());
} else {
let source_index =
match rendered_text.source_index_for_position(event.position) {
Ok(ix) | Err(ix) => ix,
};
markdown.selection = Selection {
start: source_index,
end: source_index,
reversed: false,
pending: true,
};
}
cx.notify();
}
} else if phase.capture() {
markdown.selection = Selection::default();
markdown.pressed_link = None;
cx.notify();
}
}
});
self.on_mouse_event(cx, {
let rendered_text = rendered_text.clone();
let hitbox = hitbox.clone();
let was_hovering_link = is_hovering_link;
move |markdown, event: &MouseMoveEvent, phase, cx| {
if phase.capture() {
return;
}
if markdown.selection.pending {
let source_index = match rendered_text.source_index_for_position(event.position)
{
Ok(ix) | Err(ix) => ix,
};
markdown.selection.set_head(source_index);
markdown.autoscroll_request = Some(source_index);
cx.notify();
} else {
let is_hovering_link = hitbox.is_hovered(cx)
&& rendered_text.link_for_position(event.position).is_some();
if is_hovering_link != was_hovering_link {
cx.notify();
}
}
}
});
self.on_mouse_event(cx, {
let rendered_text = rendered_text.clone();
move |markdown, event: &MouseUpEvent, phase, cx| {
if phase.bubble() {
if let Some(pressed_link) = markdown.pressed_link.take() {
if Some(&pressed_link) == rendered_text.link_for_position(event.position) {
cx.open_url(&pressed_link.destination_url);
}
}
} else {
if markdown.selection.pending {
markdown.selection.pending = false;
cx.notify();
}
}
}
});
}
fn autoscroll(&mut self, rendered_text: &RenderedText, cx: &mut WindowContext) -> Option<()> {
let autoscroll_index = self
.markdown
.update(cx, |markdown, _| markdown.autoscroll_request.take())?;
let (position, line_height) = rendered_text.position_for_source_index(autoscroll_index)?;
let text_style = cx.text_style();
let font_id = cx.text_system().resolve_font(&text_style.font());
let font_size = text_style.font_size.to_pixels(cx.rem_size());
let em_width = cx
.text_system()
.typographic_bounds(font_id, font_size, 'm')
.unwrap()
.size
.width;
cx.request_autoscroll(Bounds::from_corners(
point(position.x - 3. * em_width, position.y - 3. * line_height),
point(position.x + 3. * em_width, position.y + 3. * line_height),
));
Some(())
}
fn on_mouse_event<T: MouseEvent>(
&self,
cx: &mut WindowContext,
mut f: impl 'static + FnMut(&mut Markdown, &T, DispatchPhase, &mut ViewContext<Markdown>),
) {
cx.on_mouse_event({
let markdown = self.markdown.downgrade();
move |event, phase, cx| {
markdown
.update(cx, |markdown, cx| f(markdown, event, phase, cx))
.log_err();
}
});
}
}
impl Element for MarkdownElement {
type RequestLayoutState = RenderedMarkdown;
type PrepaintState = Hitbox;
fn id(&self) -> Option<ElementId> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
cx: &mut WindowContext,
) -> (gpui::LayoutId, Self::RequestLayoutState) {
let mut builder = MarkdownElementBuilder::new(cx.text_style(), self.style.syntax.clone());
let parsed_markdown = self.markdown.read(cx).parsed_markdown.clone();
for (range, event) in parsed_markdown.events.iter() {
match event {
MarkdownEvent::Start(tag) => {
match tag {
MarkdownTag::Paragraph => {
builder.push_div(div().mb_2().line_height(rems(1.3)));
}
MarkdownTag::Heading { level, .. } => {
let mut heading = div().mb_2();
heading = match level {
pulldown_cmark::HeadingLevel::H1 => heading.text_3xl(),
pulldown_cmark::HeadingLevel::H2 => heading.text_2xl(),
pulldown_cmark::HeadingLevel::H3 => heading.text_xl(),
pulldown_cmark::HeadingLevel::H4 => heading.text_lg(),
_ => heading,
};
builder.push_div(heading);
}
MarkdownTag::BlockQuote => {
builder.push_text_style(self.style.block_quote.clone());
builder.push_div(
div()
.pl_4()
.mb_2()
.border_l_4()
.border_color(self.style.block_quote_border_color),
);
}
MarkdownTag::CodeBlock(kind) => {
let language = if let CodeBlockKind::Fenced(language) = kind {
self.load_language(language.as_ref(), cx)
} else {
None
};
builder.push_code_block(language);
builder.push_text_style(self.style.code_block.clone());
builder.push_div(div().rounded_lg().p_4().mb_2().w_full().when_some(
self.style.code_block.background_color,
|div, color| div.bg(color),
));
}
MarkdownTag::HtmlBlock => builder.push_div(div()),
MarkdownTag::List(bullet_index) => {
builder.push_list(*bullet_index);
builder.push_div(div().pl_4());
}
MarkdownTag::Item => {
let bullet = if let Some(bullet_index) = builder.next_bullet_index() {
format!("{}.", bullet_index)
} else {
"".to_string()
};
builder.push_div(
div()
.h_flex()
.mb_2()
.line_height(rems(1.3))
.items_start()
.gap_1()
.child(bullet),
);
// Without `w_0`, text doesn't wrap to the width of the container.
builder.push_div(div().flex_1().w_0());
}
MarkdownTag::Emphasis => builder.push_text_style(TextStyleRefinement {
font_style: Some(FontStyle::Italic),
..Default::default()
}),
MarkdownTag::Strong => builder.push_text_style(TextStyleRefinement {
font_weight: Some(FontWeight::BOLD),
..Default::default()
}),
MarkdownTag::Strikethrough => {
builder.push_text_style(TextStyleRefinement {
strikethrough: Some(StrikethroughStyle {
thickness: px(1.),
color: None,
}),
..Default::default()
})
}
MarkdownTag::Link { dest_url, .. } => {
builder.push_link(dest_url.clone(), range.clone());
builder.push_text_style(self.style.link.clone())
}
_ => log::error!("unsupported markdown tag {:?}", tag),
}
}
MarkdownEvent::End(tag) => match tag {
MarkdownTagEnd::Paragraph => {
builder.pop_div();
}
MarkdownTagEnd::Heading(_) => builder.pop_div(),
MarkdownTagEnd::BlockQuote => {
builder.pop_text_style();
builder.pop_div()
}
MarkdownTagEnd::CodeBlock => {
builder.trim_trailing_newline();
builder.pop_div();
builder.pop_text_style();
builder.pop_code_block();
}
MarkdownTagEnd::HtmlBlock => builder.pop_div(),
MarkdownTagEnd::List(_) => {
builder.pop_list();
builder.pop_div();
}
MarkdownTagEnd::Item => {
builder.pop_div();
builder.pop_div();
}
MarkdownTagEnd::Emphasis => builder.pop_text_style(),
MarkdownTagEnd::Strong => builder.pop_text_style(),
MarkdownTagEnd::Strikethrough => builder.pop_text_style(),
MarkdownTagEnd::Link => builder.pop_text_style(),
_ => log::error!("unsupported markdown tag end: {:?}", tag),
},
MarkdownEvent::Text => {
builder.push_text(&parsed_markdown.source[range.clone()], range.start);
}
MarkdownEvent::Code => {
builder.push_text_style(self.style.inline_code.clone());
builder.push_text(&parsed_markdown.source[range.clone()], range.start);
builder.pop_text_style();
}
MarkdownEvent::Html => {
builder.push_text(&parsed_markdown.source[range.clone()], range.start);
}
MarkdownEvent::InlineHtml => {
builder.push_text(&parsed_markdown.source[range.clone()], range.start);
}
MarkdownEvent::Rule => {
builder.push_div(
div()
.border_b_1()
.my_2()
.border_color(self.style.rule_color),
);
builder.pop_div()
}
MarkdownEvent::SoftBreak => builder.push_text("\n", range.start),
MarkdownEvent::HardBreak => builder.push_text("\n", range.start),
_ => log::error!("unsupported markdown event {:?}", event),
}
}
let mut rendered_markdown = builder.build();
let child_layout_id = rendered_markdown.element.request_layout(cx);
let layout_id = cx.request_layout(Style::default(), [child_layout_id]);
(layout_id, rendered_markdown)
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
rendered_markdown: &mut Self::RequestLayoutState,
cx: &mut WindowContext,
) -> Self::PrepaintState {
let hitbox = cx.insert_hitbox(bounds, false);
rendered_markdown.element.prepaint(cx);
self.autoscroll(&rendered_markdown.text, cx);
hitbox
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
rendered_markdown: &mut Self::RequestLayoutState,
hitbox: &mut Self::PrepaintState,
cx: &mut WindowContext,
) {
self.paint_mouse_listeners(hitbox, &rendered_markdown.text, cx);
rendered_markdown.element.paint(cx);
self.paint_selection(bounds, &rendered_markdown.text, cx);
}
}
impl IntoElement for MarkdownElement {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
struct MarkdownElementBuilder {
div_stack: Vec<Div>,
rendered_lines: Vec<RenderedLine>,
pending_line: PendingLine,
rendered_links: Vec<RenderedLink>,
current_source_index: usize,
base_text_style: TextStyle,
text_style_stack: Vec<TextStyleRefinement>,
code_block_stack: Vec<Option<Arc<Language>>>,
list_stack: Vec<ListStackEntry>,
syntax_theme: Arc<SyntaxTheme>,
}
#[derive(Default)]
struct PendingLine {
text: String,
runs: Vec<TextRun>,
source_mappings: Vec<SourceMapping>,
}
struct ListStackEntry {
bullet_index: Option<u64>,
}
impl MarkdownElementBuilder {
fn new(base_text_style: TextStyle, syntax_theme: Arc<SyntaxTheme>) -> Self {
Self {
div_stack: vec![div().debug_selector(|| "inner".into())],
rendered_lines: Vec::new(),
pending_line: PendingLine::default(),
rendered_links: Vec::new(),
current_source_index: 0,
base_text_style,
text_style_stack: Vec::new(),
code_block_stack: Vec::new(),
list_stack: Vec::new(),
syntax_theme,
}
}
fn push_text_style(&mut self, style: TextStyleRefinement) {
self.text_style_stack.push(style);
}
fn text_style(&self) -> TextStyle {
let mut style = self.base_text_style.clone();
for refinement in &self.text_style_stack {
style.refine(refinement);
}
style
}
fn pop_text_style(&mut self) {
self.text_style_stack.pop();
}
fn push_div(&mut self, div: Div) {
self.flush_text();
self.div_stack.push(div);
}
fn pop_div(&mut self) {
self.flush_text();
let div = self.div_stack.pop().unwrap().into_any();
self.div_stack.last_mut().unwrap().extend(iter::once(div));
}
fn push_list(&mut self, bullet_index: Option<u64>) {
self.list_stack.push(ListStackEntry { bullet_index });
}
fn next_bullet_index(&mut self) -> Option<u64> {
self.list_stack.last_mut().and_then(|entry| {
let item_index = entry.bullet_index.as_mut()?;
*item_index += 1;
Some(*item_index - 1)
})
}
fn pop_list(&mut self) {
self.list_stack.pop();
}
fn push_code_block(&mut self, language: Option<Arc<Language>>) {
self.code_block_stack.push(language);
}
fn pop_code_block(&mut self) {
self.code_block_stack.pop();
}
fn push_link(&mut self, destination_url: SharedString, source_range: Range<usize>) {
self.rendered_links.push(RenderedLink {
source_range,
destination_url,
});
}
fn push_text(&mut self, text: &str, source_index: usize) {
self.pending_line.source_mappings.push(SourceMapping {
rendered_index: self.pending_line.text.len(),
source_index,
});
self.pending_line.text.push_str(text);
self.current_source_index = source_index + text.len();
if let Some(Some(language)) = self.code_block_stack.last() {
let mut offset = 0;
for (range, highlight_id) in language.highlight_text(&Rope::from(text), 0..text.len()) {
if range.start > offset {
self.pending_line
.runs
.push(self.text_style().to_run(range.start - offset));
}
let mut run_style = self.text_style();
if let Some(highlight) = highlight_id.style(&self.syntax_theme) {
run_style = run_style.highlight(highlight);
}
self.pending_line.runs.push(run_style.to_run(range.len()));
offset = range.end;
}
if offset < text.len() {
self.pending_line
.runs
.push(self.text_style().to_run(text.len() - offset));
}
} else {
self.pending_line
.runs
.push(self.text_style().to_run(text.len()));
}
}
fn trim_trailing_newline(&mut self) {
if self.pending_line.text.ends_with('\n') {
self.pending_line
.text
.truncate(self.pending_line.text.len() - 1);
self.pending_line.runs.last_mut().unwrap().len -= 1;
self.current_source_index -= 1;
}
}
fn flush_text(&mut self) {
let line = mem::take(&mut self.pending_line);
if line.text.is_empty() {
return;
}
let text = StyledText::new(line.text).with_runs(line.runs);
self.rendered_lines.push(RenderedLine {
layout: text.layout().clone(),
source_mappings: line.source_mappings,
source_end: self.current_source_index,
});
self.div_stack.last_mut().unwrap().extend([text.into_any()]);
}
fn build(mut self) -> RenderedMarkdown {
debug_assert_eq!(self.div_stack.len(), 1);
self.flush_text();
RenderedMarkdown {
element: self.div_stack.pop().unwrap().into_any(),
text: RenderedText {
lines: self.rendered_lines.into(),
links: self.rendered_links.into(),
},
}
}
}
struct RenderedLine {
layout: TextLayout,
source_mappings: Vec<SourceMapping>,
source_end: usize,
}
impl RenderedLine {
fn rendered_index_for_source_index(&self, source_index: usize) -> usize {
let mapping = match self
.source_mappings
.binary_search_by_key(&source_index, |probe| probe.source_index)
{
Ok(ix) => &self.source_mappings[ix],
Err(ix) => &self.source_mappings[ix - 1],
};
mapping.rendered_index + (source_index - mapping.source_index)
}
fn source_index_for_rendered_index(&self, rendered_index: usize) -> usize {
let mapping = match self
.source_mappings
.binary_search_by_key(&rendered_index, |probe| probe.rendered_index)
{
Ok(ix) => &self.source_mappings[ix],
Err(ix) => &self.source_mappings[ix - 1],
};
mapping.source_index + (rendered_index - mapping.rendered_index)
}
fn source_index_for_position(&self, position: Point<Pixels>) -> Result<usize, usize> {
let line_rendered_index;
let out_of_bounds;
match self.layout.index_for_position(position) {
Ok(ix) => {
line_rendered_index = ix;
out_of_bounds = false;
}
Err(ix) => {
line_rendered_index = ix;
out_of_bounds = true;
}
};
let source_index = self.source_index_for_rendered_index(line_rendered_index);
if out_of_bounds {
Err(source_index)
} else {
Ok(source_index)
}
}
}
#[derive(Copy, Clone, Debug, Default)]
struct SourceMapping {
rendered_index: usize,
source_index: usize,
}
pub struct RenderedMarkdown {
element: AnyElement,
text: RenderedText,
}
#[derive(Clone)]
struct RenderedText {
lines: Rc<[RenderedLine]>,
links: Rc<[RenderedLink]>,
}
#[derive(Clone, Eq, PartialEq)]
struct RenderedLink {
source_range: Range<usize>,
destination_url: SharedString,
}
impl RenderedText {
fn source_index_for_position(&self, position: Point<Pixels>) -> Result<usize, usize> {
let mut lines = self.lines.iter().peekable();
while let Some(line) = lines.next() {
let line_bounds = line.layout.bounds();
if position.y > line_bounds.bottom() {
if let Some(next_line) = lines.peek() {
if position.y < next_line.layout.bounds().top() {
return Err(line.source_end);
}
}
continue;
}
return line.source_index_for_position(position);
}
Err(self.lines.last().map_or(0, |line| line.source_end))
}
fn position_for_source_index(&self, source_index: usize) -> Option<(Point<Pixels>, Pixels)> {
for line in self.lines.iter() {
let line_source_start = line.source_mappings.first().unwrap().source_index;
if source_index < line_source_start {
break;
} else if source_index > line.source_end {
continue;
} else {
let line_height = line.layout.line_height();
let rendered_index_within_line = line.rendered_index_for_source_index(source_index);
let position = line.layout.position_for_index(rendered_index_within_line)?;
return Some((position, line_height));
}
}
None
}
fn link_for_position(&self, position: Point<Pixels>) -> Option<&RenderedLink> {
let source_index = self.source_index_for_position(position).ok()?;
self.links
.iter()
.find(|link| link.source_range.contains(&source_index))
}
}

View File

@@ -0,0 +1,274 @@
use gpui::SharedString;
use linkify::LinkFinder;
pub use pulldown_cmark::TagEnd as MarkdownTagEnd;
use pulldown_cmark::{Alignment, HeadingLevel, LinkType, MetadataBlockKind, Options, Parser};
use std::ops::Range;
pub fn parse_markdown(text: &str) -> Vec<(Range<usize>, MarkdownEvent)> {
let mut events = Vec::new();
let mut within_link = false;
for (pulldown_event, mut range) in Parser::new_ext(text, Options::all()).into_offset_iter() {
match pulldown_event {
pulldown_cmark::Event::Start(tag) => {
if let pulldown_cmark::Tag::Link { .. } = tag {
within_link = true;
}
events.push((range, MarkdownEvent::Start(tag.into())))
}
pulldown_cmark::Event::End(tag) => {
if let pulldown_cmark::TagEnd::Link = tag {
within_link = false;
}
events.push((range, MarkdownEvent::End(tag)));
}
pulldown_cmark::Event::Text(_) => {
// Automatically detect links in text if we're not already within a markdown
// link.
if !within_link {
let mut finder = LinkFinder::new();
finder.kinds(&[linkify::LinkKind::Url]);
let text_range = range.clone();
for link in finder.links(&text[text_range.clone()]) {
let link_range =
text_range.start + link.start()..text_range.start + link.end();
if link_range.start > range.start {
events.push((range.start..link_range.start, MarkdownEvent::Text));
}
events.push((
link_range.clone(),
MarkdownEvent::Start(MarkdownTag::Link {
link_type: LinkType::Autolink,
dest_url: SharedString::from(link.as_str().to_string()),
title: SharedString::default(),
id: SharedString::default(),
}),
));
events.push((link_range.clone(), MarkdownEvent::Text));
events.push((link_range.clone(), MarkdownEvent::End(MarkdownTagEnd::Link)));
range.start = link_range.end;
}
}
if range.start < range.end {
events.push((range, MarkdownEvent::Text));
}
}
pulldown_cmark::Event::Code(_) => {
range.start += 1;
range.end -= 1;
events.push((range, MarkdownEvent::Code))
}
pulldown_cmark::Event::Html(_) => events.push((range, MarkdownEvent::Html)),
pulldown_cmark::Event::InlineHtml(_) => events.push((range, MarkdownEvent::InlineHtml)),
pulldown_cmark::Event::FootnoteReference(_) => {
events.push((range, MarkdownEvent::FootnoteReference))
}
pulldown_cmark::Event::SoftBreak => events.push((range, MarkdownEvent::SoftBreak)),
pulldown_cmark::Event::HardBreak => events.push((range, MarkdownEvent::HardBreak)),
pulldown_cmark::Event::Rule => events.push((range, MarkdownEvent::Rule)),
pulldown_cmark::Event::TaskListMarker(checked) => {
events.push((range, MarkdownEvent::TaskListMarker(checked)))
}
}
}
events
}
/// A static-lifetime equivalent of pulldown_cmark::Event so we can cache the
/// parse result for rendering without resorting to unsafe lifetime coercion.
#[derive(Clone, Debug, PartialEq)]
pub enum MarkdownEvent {
/// Start of a tagged element. Events that are yielded after this event
/// and before its corresponding `End` event are inside this element.
/// Start and end events are guaranteed to be balanced.
Start(MarkdownTag),
/// End of a tagged element.
End(MarkdownTagEnd),
/// A text node.
Text,
/// An inline code node.
Code,
/// An HTML node.
Html,
/// An inline HTML node.
InlineHtml,
/// A reference to a footnote with given label, which may or may not be defined
/// by an event with a `Tag::FootnoteDefinition` tag. Definitions and references to them may
/// occur in any order.
FootnoteReference,
/// A soft line break.
SoftBreak,
/// A hard line break.
HardBreak,
/// A horizontal ruler.
Rule,
/// A task list marker, rendered as a checkbox in HTML. Contains a true when it is checked.
TaskListMarker(bool),
}
/// Tags for elements that can contain other elements.
#[derive(Clone, Debug, PartialEq)]
pub enum MarkdownTag {
/// A paragraph of text and other inline elements.
Paragraph,
/// A heading, with optional identifier, classes and custom attributes.
/// The identifier is prefixed with `#` and the last one in the attributes
/// list is chosen, classes are prefixed with `.` and custom attributes
/// have no prefix and can optionally have a value (`myattr` o `myattr=myvalue`).
Heading {
level: HeadingLevel,
id: Option<SharedString>,
classes: Vec<SharedString>,
/// The first item of the tuple is the attr and second one the value.
attrs: Vec<(SharedString, Option<SharedString>)>,
},
BlockQuote,
/// A code block.
CodeBlock(CodeBlockKind),
/// A HTML block.
HtmlBlock,
/// A list. If the list is ordered the field indicates the number of the first item.
/// Contains only list items.
List(Option<u64>), // TODO: add delim and tight for ast (not needed for html)
/// A list item.
Item,
/// A footnote definition. The value contained is the footnote's label by which it can
/// be referred to.
#[cfg_attr(feature = "serde", serde(borrow))]
FootnoteDefinition(SharedString),
/// A table. Contains a vector describing the text-alignment for each of its columns.
Table(Vec<Alignment>),
/// A table header. Contains only `TableCell`s. Note that the table body starts immediately
/// after the closure of the `TableHead` tag. There is no `TableBody` tag.
TableHead,
/// A table row. Is used both for header rows as body rows. Contains only `TableCell`s.
TableRow,
TableCell,
// span-level tags
Emphasis,
Strong,
Strikethrough,
/// A link.
Link {
link_type: LinkType,
dest_url: SharedString,
title: SharedString,
/// Identifier of reference links, e.g. `world` in the link `[hello][world]`.
id: SharedString,
},
/// An image. The first field is the link type, the second the destination URL and the third is a title,
/// the fourth is the link identifier.
Image {
link_type: LinkType,
dest_url: SharedString,
title: SharedString,
/// Identifier of reference links, e.g. `world` in the link `[hello][world]`.
id: SharedString,
},
/// A metadata block.
MetadataBlock(MetadataBlockKind),
}
#[derive(Clone, Debug, PartialEq)]
pub enum CodeBlockKind {
Indented,
/// The value contained in the tag describes the language of the code, which may be empty.
Fenced(SharedString),
}
impl From<pulldown_cmark::Tag<'_>> for MarkdownTag {
fn from(tag: pulldown_cmark::Tag) -> Self {
match tag {
pulldown_cmark::Tag::Paragraph => MarkdownTag::Paragraph,
pulldown_cmark::Tag::Heading {
level,
id,
classes,
attrs,
} => {
let id = id.map(|id| SharedString::from(id.into_string()));
let classes = classes
.into_iter()
.map(|c| SharedString::from(c.into_string()))
.collect();
let attrs = attrs
.into_iter()
.map(|(key, value)| {
(
SharedString::from(key.into_string()),
value.map(|v| SharedString::from(v.into_string())),
)
})
.collect();
MarkdownTag::Heading {
level,
id,
classes,
attrs,
}
}
pulldown_cmark::Tag::BlockQuote => MarkdownTag::BlockQuote,
pulldown_cmark::Tag::CodeBlock(kind) => match kind {
pulldown_cmark::CodeBlockKind::Indented => {
MarkdownTag::CodeBlock(CodeBlockKind::Indented)
}
pulldown_cmark::CodeBlockKind::Fenced(info) => MarkdownTag::CodeBlock(
CodeBlockKind::Fenced(SharedString::from(info.into_string())),
),
},
pulldown_cmark::Tag::List(start_number) => MarkdownTag::List(start_number),
pulldown_cmark::Tag::Item => MarkdownTag::Item,
pulldown_cmark::Tag::FootnoteDefinition(label) => {
MarkdownTag::FootnoteDefinition(SharedString::from(label.to_string()))
}
pulldown_cmark::Tag::Table(alignments) => MarkdownTag::Table(alignments),
pulldown_cmark::Tag::TableHead => MarkdownTag::TableHead,
pulldown_cmark::Tag::TableRow => MarkdownTag::TableRow,
pulldown_cmark::Tag::TableCell => MarkdownTag::TableCell,
pulldown_cmark::Tag::Emphasis => MarkdownTag::Emphasis,
pulldown_cmark::Tag::Strong => MarkdownTag::Strong,
pulldown_cmark::Tag::Strikethrough => MarkdownTag::Strikethrough,
pulldown_cmark::Tag::Link {
link_type,
dest_url,
title,
id,
} => MarkdownTag::Link {
link_type,
dest_url: SharedString::from(dest_url.into_string()),
title: SharedString::from(title.into_string()),
id: SharedString::from(id.into_string()),
},
pulldown_cmark::Tag::Image {
link_type,
dest_url,
title,
id,
} => MarkdownTag::Image {
link_type,
dest_url: SharedString::from(dest_url.into_string()),
title: SharedString::from(title.into_string()),
id: SharedString::from(id.into_string()),
},
pulldown_cmark::Tag::HtmlBlock => MarkdownTag::HtmlBlock,
pulldown_cmark::Tag::MetadataBlock(kind) => MarkdownTag::MetadataBlock(kind),
}
}
}

View File

@@ -13,7 +13,7 @@ use language::{
language_settings::{language_settings, LanguageSettings},
AutoindentMode, Buffer, BufferChunks, BufferSnapshot, Capability, CharKind, Chunk, CursorShape,
DiagnosticEntry, File, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16,
Outline, OutlineItem, Point, PointUtf16, Runnable, Selection, TextDimension, ToOffset as _,
Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _,
ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped,
};
use smallvec::SmallVec;
@@ -1603,6 +1603,11 @@ impl MultiBuffer {
"untitled".into()
}
pub fn set_title(&mut self, title: String, cx: &mut ModelContext<Self>) {
self.title = Some(title);
cx.notify();
}
#[cfg(any(test, feature = "test-support"))]
pub fn is_parsing(&self, cx: &AppContext) -> bool {
self.as_singleton().unwrap().read(cx).is_parsing()
@@ -3151,10 +3156,10 @@ impl MultiBufferSnapshot {
.redacted_ranges(excerpt.range.context.clone())
.map(move |mut redacted_range| {
// Re-base onto the excerpts coordinates in the multibuffer
redacted_range.start =
excerpt_offset + (redacted_range.start - excerpt_buffer_start);
redacted_range.end =
excerpt_offset + (redacted_range.end - excerpt_buffer_start);
redacted_range.start = excerpt_offset
+ redacted_range.start.saturating_sub(excerpt_buffer_start);
redacted_range.end = excerpt_offset
+ redacted_range.end.saturating_sub(excerpt_buffer_start);
redacted_range
})
@@ -3168,7 +3173,7 @@ impl MultiBufferSnapshot {
pub fn runnable_ranges(
&self,
range: Range<Anchor>,
) -> impl Iterator<Item = (Range<usize>, Runnable)> + '_ {
) -> impl Iterator<Item = language::RunnableRange> + '_ {
let range = range.start.to_offset(self)..range.end.to_offset(self);
self.excerpts_for_range(range.clone())
.flat_map(move |(excerpt, excerpt_offset)| {
@@ -3177,16 +3182,19 @@ impl MultiBufferSnapshot {
excerpt
.buffer
.runnable_ranges(excerpt.range.context.clone())
.map(move |(mut match_range, runnable)| {
.map(move |mut runnable| {
// Re-base onto the excerpts coordinates in the multibuffer
match_range.start =
excerpt_offset + (match_range.start - excerpt_buffer_start);
match_range.end = excerpt_offset + (match_range.end - excerpt_buffer_start);
(match_range, runnable)
runnable.run_range.start = excerpt_offset
+ runnable
.run_range
.start
.saturating_sub(excerpt_buffer_start);
runnable.run_range.end = excerpt_offset
+ runnable.run_range.end.saturating_sub(excerpt_buffer_start);
runnable
})
.skip_while(move |(match_range, _)| match_range.end < range.start)
.take_while(move |(match_range, _)| match_range.start < range.end)
.skip_while(move |runnable| runnable.run_range.end < range.start)
.take_while(move |runnable| runnable.run_range.start < range.end)
})
}

View File

@@ -12,15 +12,28 @@ workspace = true
path = "src/node_runtime.rs"
doctest = false
[features]
test-support = ["tempfile"]
[dependencies]
anyhow.workspace = true
async-compression.workspace = true
async-tar.workspace = true
async-trait.workspace = true
async_zip.workspace = true
futures.workspace = true
log.workspace = true
semver.workspace = true
serde.workspace = true
serde_json.workspace = true
smol.workspace = true
tempfile = { workspace = true, optional = true }
util.workspace = true
walkdir = "2.5.0"
windows.workspace = true
[target.'cfg(windows)'.dependencies]
async-std = { version = "1.12.0", features = ["unstable"] }
[dev-dependencies]
tempfile.workspace = true

View File

@@ -0,0 +1,118 @@
use std::path::Path;
use anyhow::Result;
use async_zip::base::read::stream::ZipFileReader;
use futures::{io::BufReader, AsyncRead};
pub async fn extract_zip<R: AsyncRead + Unpin>(destination: &Path, reader: R) -> Result<()> {
let mut reader = ZipFileReader::new(BufReader::new(reader));
let destination = &destination
.canonicalize()
.unwrap_or_else(|_| destination.to_path_buf());
while let Some(mut item) = reader.next_with_entry().await? {
let entry_reader = item.reader_mut();
let entry = entry_reader.entry();
let path = destination.join(entry.filename().as_str().unwrap());
if entry.dir().unwrap() {
std::fs::create_dir_all(&path)?;
} else {
let parent_dir = path.parent().expect("failed to get parent directory");
std::fs::create_dir_all(&parent_dir)?;
let mut file = smol::fs::File::create(&path).await?;
futures::io::copy(entry_reader, &mut file).await?;
}
reader = item.skip().await?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use async_zip::base::write::ZipFileWriter;
use async_zip::ZipEntryBuilder;
use futures::AsyncWriteExt;
use smol::io::Cursor;
use tempfile::TempDir;
use super::*;
async fn compress_zip(src_dir: &Path, dst: &Path) -> Result<()> {
let mut out = smol::fs::File::create(dst).await?;
let mut writer = ZipFileWriter::new(&mut out);
for entry in walkdir::WalkDir::new(src_dir) {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
continue;
}
let relative_path = path.strip_prefix(src_dir)?;
let data = smol::fs::read(&path).await?;
let filename = relative_path.display().to_string();
let builder = ZipEntryBuilder::new(filename.into(), async_zip::Compression::Deflate);
writer.write_entry_whole(builder, &data).await?;
}
writer.close().await?;
out.flush().await?;
Ok(())
}
#[track_caller]
fn assert_file_content(path: &Path, content: &str) {
assert!(path.exists(), "file not found: {:?}", path);
let actual = std::fs::read_to_string(path).unwrap();
assert_eq!(actual, content);
}
#[track_caller]
fn make_test_data() -> TempDir {
let dir = tempfile::tempdir().unwrap();
let dst = dir.path();
std::fs::write(&dst.join("test"), "Hello world.").unwrap();
std::fs::create_dir_all(&dst.join("foo/bar")).unwrap();
std::fs::write(&dst.join("foo/bar.txt"), "Foo bar.").unwrap();
std::fs::write(&dst.join("foo/dar.md"), "Bar dar.").unwrap();
std::fs::write(&dst.join("foo/bar/dar你好.txt"), "你好世界").unwrap();
dir
}
async fn read_archive(path: &PathBuf) -> impl AsyncRead + Unpin {
let data = smol::fs::read(&path).await.unwrap();
Cursor::new(data)
}
#[test]
fn test_extract_zip() {
let test_dir = make_test_data();
let zip_file = test_dir.path().join("test.zip");
smol::block_on(async {
compress_zip(&test_dir.path(), &zip_file).await.unwrap();
let reader = read_archive(&zip_file).await;
let dir = tempfile::tempdir().unwrap();
let dst = dir.path();
extract_zip(dst, reader).await.unwrap();
assert_file_content(&dst.join("test"), "Hello world.");
assert_file_content(&dst.join("foo/bar.txt"), "Foo bar.");
assert_file_content(&dst.join("foo/dar.md"), "Bar dar.");
assert_file_content(&dst.join("foo/bar/dar你好.txt"), "你好世界");
});
}
}

View File

@@ -1,10 +1,13 @@
mod archive;
use anyhow::{anyhow, bail, Context, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use futures::AsyncReadExt;
use semver::Version;
use serde::Deserialize;
use smol::{fs, io::BufReader, lock::Mutex, process::Command};
use smol::io::BufReader;
use smol::{fs, lock::Mutex, process::Command};
use std::io;
use std::process::{Output, Stdio};
use std::{
@@ -15,8 +18,26 @@ use std::{
use util::http::HttpClient;
use util::ResultExt;
#[cfg(windows)]
use smol::process::windows::CommandExt;
const VERSION: &str = "v18.15.0";
#[cfg(not(windows))]
const NODE_PATH: &str = "bin/node";
#[cfg(windows)]
const NODE_PATH: &str = "node.exe";
#[cfg(not(windows))]
const NPM_PATH: &str = "bin/npm";
#[cfg(windows)]
const NPM_PATH: &str = "node_modules/npm/bin/npm-cli.js";
enum ArchiveType {
TarGz,
Zip,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct NpmInfo {
@@ -119,10 +140,12 @@ impl RealNodeRuntime {
let folder_name = format!("node-{VERSION}-{os}-{arch}");
let node_containing_dir = util::paths::SUPPORT_DIR.join("node");
let node_dir = node_containing_dir.join(folder_name);
let node_binary = node_dir.join("bin/node");
let npm_file = node_dir.join("bin/npm");
let node_binary = node_dir.join(NODE_PATH);
let npm_file = node_dir.join(NPM_PATH);
let result = Command::new(&node_binary)
let mut command = Command::new(&node_binary);
command
.env_clear()
.arg(npm_file)
.arg("--version")
@@ -131,9 +154,12 @@ impl RealNodeRuntime {
.stderr(Stdio::null())
.args(["--cache".into(), node_dir.join("cache")])
.args(["--userconfig".into(), node_dir.join("blank_user_npmrc")])
.args(["--globalconfig".into(), node_dir.join("blank_global_npmrc")])
.status()
.await;
.args(["--globalconfig".into(), node_dir.join("blank_global_npmrc")]);
#[cfg(windows)]
command.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0);
let result = command.status().await;
let valid = matches!(result, Ok(status) if status.success());
if !valid {
@@ -142,7 +168,19 @@ impl RealNodeRuntime {
.await
.context("error creating node containing dir")?;
let file_name = format!("node-{VERSION}-{os}-{arch}.tar.gz");
let archive_type = match consts::OS {
"macos" | "linux" => ArchiveType::TarGz,
"windows" => ArchiveType::Zip,
other => bail!("Running on unsupported os: {other}"),
};
let file_name = format!(
"node-{VERSION}-{os}-{arch}.{extension}",
extension = match archive_type {
ArchiveType::TarGz => "tar.gz",
ArchiveType::Zip => "zip",
}
);
let url = format!("https://nodejs.org/dist/{VERSION}/{file_name}");
let mut response = self
.http
@@ -150,9 +188,15 @@ impl RealNodeRuntime {
.await
.context("error downloading Node binary tarball")?;
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
let archive = Archive::new(decompressed_bytes);
archive.unpack(&node_containing_dir).await?;
let body = response.body_mut();
match archive_type {
ArchiveType::TarGz => {
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
let archive = Archive::new(decompressed_bytes);
archive.unpack(&node_containing_dir).await?;
}
ArchiveType::Zip => archive::extract_zip(&node_containing_dir, body).await?,
}
}
// Note: Not in the `if !valid {}` so we can populate these for existing installations
@@ -168,7 +212,7 @@ impl RealNodeRuntime {
impl NodeRuntime for RealNodeRuntime {
async fn binary_path(&self) -> Result<PathBuf> {
let installation_path = self.install_if_needed().await?;
Ok(installation_path.join("bin/node"))
Ok(installation_path.join(NODE_PATH))
}
async fn run_npm_subcommand(
@@ -180,7 +224,13 @@ impl NodeRuntime for RealNodeRuntime {
let attempt = || async move {
let installation_path = self.install_if_needed().await?;
let mut env_path = installation_path.join("bin").into_os_string();
let node_binary = installation_path.join(NODE_PATH);
let npm_file = installation_path.join(NPM_PATH);
let mut env_path = node_binary
.parent()
.expect("invalid node binary path")
.to_path_buf();
if let Some(existing_path) = std::env::var_os("PATH") {
if !existing_path.is_empty() {
env_path.push(":");
@@ -188,9 +238,6 @@ impl NodeRuntime for RealNodeRuntime {
}
}
let node_binary = installation_path.join("bin/node");
let npm_file = installation_path.join("bin/npm");
if smol::fs::metadata(&node_binary).await.is_err() {
return Err(anyhow!("missing node binary file"));
}
@@ -219,6 +266,9 @@ impl NodeRuntime for RealNodeRuntime {
command.args(["--prefix".into(), directory.to_path_buf()]);
}
#[cfg(windows)]
command.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0);
command.output().await.map_err(|e| anyhow!("{e}"))
};
@@ -227,7 +277,8 @@ impl NodeRuntime for RealNodeRuntime {
output = attempt().await;
if output.is_err() {
return Err(anyhow!(
"failed to launch npm subcommand {subcommand} subcommand"
"failed to launch npm subcommand {subcommand} subcommand\nerr: {:?}",
output.err()
));
}
}

View File

@@ -7864,6 +7864,18 @@ impl Project {
})
}
pub fn project_path_for_absolute_path(
&self,
abs_path: &Path,
cx: &AppContext,
) -> Option<ProjectPath> {
self.find_local_worktree(abs_path, cx)
.map(|(worktree, relative_path)| ProjectPath {
worktree_id: worktree.read(cx).id(),
path: relative_path.into(),
})
}
pub fn get_workspace_root(
&self,
project_path: &ProjectPath,

View File

@@ -250,6 +250,7 @@ impl SearchQuery {
}
}
}
pub async fn search(
&self,
buffer: &BufferSnapshot,

View File

@@ -10,4 +10,4 @@ workspace = true
[dependencies]
gpui.workspace = true
once_cell = "1.19.0"
once_cell.workspace = true

View File

@@ -7,7 +7,8 @@ use std::{env, str::FromStr};
use gpui::{AppContext, Global, SemanticVersion};
use once_cell::sync::Lazy;
static RELEASE_CHANNEL_NAME: Lazy<String> = if cfg!(debug_assertions) {
/// stable | dev | nightly | preview
pub static RELEASE_CHANNEL_NAME: Lazy<String> = if cfg!(debug_assertions) {
Lazy::new(|| {
env::var("ZED_RELEASE_CHANNEL")
.unwrap_or_else(|_| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string())

View File

@@ -450,7 +450,7 @@ pub struct WorktreeSearchResult {
pub score: f32,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum Status {
Idle,
Loading,

View File

@@ -67,7 +67,7 @@ impl StoryContainer {
}
impl ParentElement for StoryContainer {
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
}
}
@@ -372,7 +372,7 @@ impl RenderOnce for StorySection {
}
impl ParentElement for StorySection {
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
}
}

View File

@@ -20,7 +20,7 @@ collections.workspace = true
dirs = "4.0.0"
futures.workspace = true
gpui.workspace = true
libc = "0.2"
libc.workspace = true
task.workspace = true
schemars.workspace = true
serde.workspace = true

View File

@@ -559,7 +559,7 @@ impl Element for TerminalElement {
.request_layout(global_id, cx, |mut style, cx| {
style.size.width = relative(1.).into();
style.size.height = relative(1.).into();
let layout_id = cx.request_layout(&style, None);
let layout_id = cx.request_layout(style, None);
layout_id
});

View File

@@ -74,6 +74,7 @@ impl TerminalPanel {
pane.set_can_split(false, cx);
pane.set_can_navigate(false, cx);
pane.display_nav_history_buttons(None);
pane.set_should_display_tab_bar(|_| true);
pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
h_flex()
.gap_2()

View File

@@ -407,7 +407,7 @@ impl VisibleOnHover for ButtonLike {
}
impl ParentElement for ButtonLike {
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
}
}

View File

@@ -89,7 +89,7 @@ impl LabelCommon for LabelLike {
}
impl ParentElement for LabelLike {
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
}
}

View File

@@ -40,7 +40,7 @@ impl List {
}
impl ParentElement for List {
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
}
}

View File

@@ -141,7 +141,7 @@ impl Selectable for ListItem {
}
impl ParentElement for ListItem {
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
}
}

View File

@@ -35,7 +35,7 @@ impl ModalHeader {
}
impl ParentElement for ModalHeader {
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
}
}
@@ -86,7 +86,7 @@ impl ModalContent {
}
impl ParentElement for ModalContent {
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
}
}
@@ -111,7 +111,7 @@ impl ModalRow {
}
impl ParentElement for ModalRow {
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
}
}

View File

@@ -74,7 +74,7 @@ impl Popover {
}
impl ParentElement for Popover {
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
}
}

View File

@@ -195,7 +195,7 @@ impl<M: ManagedView> Element for PopoverMenu<M> {
.map(|child_element| child_element.request_layout(cx));
let layout_id = cx.request_layout(
&gpui::Style::default(),
gpui::Style::default(),
menu_layout_id.into_iter().chain(child_layout_id),
);

View File

@@ -142,7 +142,7 @@ impl<M: ManagedView> Element for RightClickMenu<M> {
.map(|child_element| child_element.request_layout(cx));
let layout_id = cx.request_layout(
&gpui::Style::default(),
gpui::Style::default(),
menu_layout_id.into_iter().chain(child_layout_id),
);

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