Compare commits

..

44 Commits

Author SHA1 Message Date
Peter Tripp
6e015998a7 chore: Add formatter for proto. Format zed.proto 2024-11-14 10:23:03 -05:00
Piotr Osiewicz
49eb865e8a python: Improve handling of triple-quoted strings (#20664)
Closes #13998

/cc @notpeter would you mind giving this branch a go to see if this is
pleasant to use? This impl is not quite what VSC has, but I think it
feels okay?

In this PR, the sequence goes as follows:
1st keypress: "|"
2nd keypress: ""|
3rd keypress: """|"""

Release Notes:

- Improved handling of triple-quote strings in Python.
2024-11-14 15:12:46 +01:00
Piotr Osiewicz
a650fe0d77 python: Fix detection of Poetry environments (#20669)
We were missing a .configure call, which let to discrepancies with PET
output.

Release Notes:

- Improved detection of Poetry-based environments
2024-11-14 14:54:53 +01:00
Danilo Leal
204a989758 assistant: Add tiny design tweaks to the patch block (#20636)
Adjusting really tiny things, most notably ensuring that the header text
doesn't overflow out of the block.

<img width="600" alt="Screenshot 2024-11-13 at 20 11 08"
src="https://github.com/user-attachments/assets/26656203-92c6-49e5-a732-bae010f96b2d">


Release Notes:

- N/A
2024-11-14 10:53:00 -03:00
Thorsten Ball
776cfe44d7 environment: Log stderr too if command fails to run (#20659)
Release Notes:

- N/A
2024-11-14 14:44:32 +01:00
Thorsten Ball
35798212c4 Update copilot to Copilot.vim 1.41.0 (#20520)
Release Notes:

- Update Copilot's underlying version to [Copilot.vim
1.41.0](8703812380)

Co-authored-by: Antonio <antonio@zed.dev>
2024-11-14 14:44:13 +01:00
Piotr Osiewicz
89f9a506f9 tasks: Add ability to query active toolchains for languages (#20667)
Closes #18649

Release Notes:

- Python tasks now use active toolchain to run.
2024-11-14 14:37:37 +01:00
Nate Butler
04ba75e2e5 Add ui::ContentGroup (#20666)
TL;DR our version of [HIG's
Box](https://developer.apple.com/design/human-interface-guidelines/boxes)

We can't use the name `Box` (because rust) or `ContentBox` (because
taffy/styles/css).

---

This PR introduces the `ContentGroup` component, a flexible container
inspired by HIG's `Box` component. It's designed to hold and organize
various UI elements with options to toggle borders and background fills.

**Example usage**:

```rust
ContentGroup::new()
    .flex_1()
    .items_center()
    .justify_center()
    .h_48()
    .child(Label::new("Flexible ContentBox"))
```

Here are some configurations:

- Default: Includes both border and fill.
- Borderless: No border for a clean look.
- Unfilled: No background fill for a transparent appearance.

**Preview**:

![CleanShot 2024-11-14 at 07 05
15@2x](https://github.com/user-attachments/assets/c838371e-e24f-46f0-94b4-43c078e8f14e)

---

_This PR was written by a large language model with input from the
author._

Release Notes:

- N/A
2024-11-14 08:25:48 -05:00
renovate[bot]
f7b4431659 Update Rust crate libc to v0.2.162 (#20625)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [libc](https://redirect.github.com/rust-lang/libc) |
workspace.dependencies | patch | `0.2.161` -> `0.2.162` |

---

### Release Notes

<details>
<summary>rust-lang/libc (libc)</summary>

###
[`v0.2.162`](https://redirect.github.com/rust-lang/libc/releases/tag/0.2.162)

[Compare
Source](https://redirect.github.com/rust-lang/libc/compare/0.2.161...0.2.162)

##### Added

- Android: fix the alignment of `uc_mcontext` on arm64
[#&#8203;3894](https://redirect.github.com/rust-lang/libc/pull/3894)
- Apple: add `host_cpu_load_info`
[#&#8203;3916](https://redirect.github.com/rust-lang/libc/pull/3916)
- ESP-IDF: add a time flag
[#&#8203;3993](https://redirect.github.com/rust-lang/libc/pull/3993)
- FreeBSD: add the `CLOSE_RANGE_CLOEXEC`
flag[#&#8203;3996](https://redirect.github.com/rust-lang/libc/pull/3996)
- FreeBSD: fix test errors regarding `__gregset_t`
[#&#8203;3995](https://redirect.github.com/rust-lang/libc/pull/3995)
- FreeBSD: fix tests on x86 FreeBSD 15
[#&#8203;3948](https://redirect.github.com/rust-lang/libc/pull/3948)
- FreeBSD: make `ucontext_t` and `mcontext_t` available on all
architectures
[#&#8203;3848](https://redirect.github.com/rust-lang/libc/pull/3848)
- Haiku: add `getentropy`
[#&#8203;3991](https://redirect.github.com/rust-lang/libc/pull/3991)
- Illumos: add `syncfs`
[#&#8203;3990](https://redirect.github.com/rust-lang/libc/pull/3990)
- Illumos: add some recently-added constants
[#&#8203;3999](https://redirect.github.com/rust-lang/libc/pull/3999)
- Linux: add `ioctl` flags
[#&#8203;3960](https://redirect.github.com/rust-lang/libc/pull/3960)
- Linux: add epoll busy polling parameters
[#&#8203;3922](https://redirect.github.com/rust-lang/libc/pull/3922)
- NuttX: add `pthread_[get/set]name_np`
[#&#8203;4003](https://redirect.github.com/rust-lang/libc/pull/4003)
- RTEMS: add `arc4random_buf`
[#&#8203;3989](https://redirect.github.com/rust-lang/libc/pull/3989)
- Trusty OS: add initial support
[#&#8203;3942](https://redirect.github.com/rust-lang/libc/pull/3942)
- WASIp2: expand socket support
[#&#8203;3981](https://redirect.github.com/rust-lang/libc/pull/3981)

##### Fixed

- Emscripten: don't pass `-lc`
[#&#8203;4002](https://redirect.github.com/rust-lang/libc/pull/4002)
- Hurd: change `st_fsid` field to `st_dev`
[#&#8203;3785](https://redirect.github.com/rust-lang/libc/pull/3785)
- Hurd: fix the definition of `utsname`
[#&#8203;3992](https://redirect.github.com/rust-lang/libc/pull/3992)
- Illumos/Solaris: fix `FNM_CASEFOLD` definition
[#&#8203;4004](https://redirect.github.com/rust-lang/libc/pull/4004)
- Solaris: fix all tests
[#&#8203;3864](https://redirect.github.com/rust-lang/libc/pull/3864)

##### Other

- CI: Add loongarch64
[#&#8203;4000](https://redirect.github.com/rust-lang/libc/pull/4000)
- CI: Check that semver files are sorted
[#&#8203;4018](https://redirect.github.com/rust-lang/libc/pull/4018)
- CI: Re-enable the FreeBSD 15 job
[#&#8203;3988](https://redirect.github.com/rust-lang/libc/pull/3988)
- Clean up imports and `extern crate` usage
[#&#8203;3897](https://redirect.github.com/rust-lang/libc/pull/3897)
- Convert `mode_t` constants to octal
[#&#8203;3634](https://redirect.github.com/rust-lang/libc/pull/3634)
- Remove the `wasm32-wasi` target that has been deleted upstream
[#&#8203;4013](https://redirect.github.com/rust-lang/libc/pull/4013)

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-14 12:07:38 +02:00
renovate[bot]
6b9eba2109 Update Rust crate linkme to v0.3.31 (#20626)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [linkme](https://redirect.github.com/dtolnay/linkme) | dependencies |
patch | `0.3.29` -> `0.3.31` |

---

### Release Notes

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

###
[`v0.3.31`](https://redirect.github.com/dtolnay/linkme/releases/tag/0.3.31)

[Compare
Source](https://redirect.github.com/dtolnay/linkme/compare/0.3.30...0.3.31)

- Prevent `ref_option_ref` pedantic clippy lint from triggering inside
generated code
([#&#8203;103](https://redirect.github.com/dtolnay/linkme/issues/103))
- Implement Debug for DistributedSlice
([#&#8203;105](https://redirect.github.com/dtolnay/linkme/issues/105))

###
[`v0.3.30`](https://redirect.github.com/dtolnay/linkme/releases/tag/0.3.30)

[Compare
Source](https://redirect.github.com/dtolnay/linkme/compare/0.3.29...0.3.30)

- Support Rust 2024 edition
([#&#8203;102](https://redirect.github.com/dtolnay/linkme/issues/102))

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-14 12:07:27 +02:00
renovate[bot]
58e3b788dc Update Rust crate mdbook to v0.4.42 (#20629)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [mdbook](https://redirect.github.com/rust-lang/mdBook) | dependencies
| patch | `0.4.40` -> `0.4.42` |

---

### Release Notes

<details>
<summary>rust-lang/mdBook (mdbook)</summary>

###
[`v0.4.42`](https://redirect.github.com/rust-lang/mdBook/blob/HEAD/CHANGELOG.md#mdBook-0442)

[Compare
Source](https://redirect.github.com/rust-lang/mdBook/compare/v0.4.41...v0.4.42)


[v0.4.41...v0.4.42](https://redirect.github.com/rust-lang/mdBook/compare/v0.4.41...v0.4.42)

##### Fixed

-   Fixed chapter list folding.
[#&#8203;2473](https://redirect.github.com/rust-lang/mdBook/pull/2473)

###
[`v0.4.41`](https://redirect.github.com/rust-lang/mdBook/blob/HEAD/CHANGELOG.md#mdBook-0441)

[Compare
Source](https://redirect.github.com/rust-lang/mdBook/compare/v0.4.40...v0.4.41)


[v0.4.40...v0.4.41](https://redirect.github.com/rust-lang/mdBook/compare/v0.4.40...v0.4.41)

**Note:** If you have a custom `index.hbs` theme file, you will need to
update it to the latest version.

##### Added

-   Added preliminary support for Rust 2024 edition.
[#&#8203;2398](https://redirect.github.com/rust-lang/mdBook/pull/2398)
-   Added a full example of the remove-emphasis preprocessor.
[#&#8203;2464](https://redirect.github.com/rust-lang/mdBook/pull/2464)

##### Changed

-   Adjusted styling of clipboard/play icons.
[#&#8203;2421](https://redirect.github.com/rust-lang/mdBook/pull/2421)
-   Updated to handlebars v6.
[#&#8203;2416](https://redirect.github.com/rust-lang/mdBook/pull/2416)
-   Attr and section rules now have specific code highlighting.
[#&#8203;2448](https://redirect.github.com/rust-lang/mdBook/pull/2448)
- The sidebar is now loaded from a common file, significantly reducing
the book size when there are many chapters.
[#&#8203;2414](https://redirect.github.com/rust-lang/mdBook/pull/2414)
-   Updated dependencies.
[#&#8203;2470](https://redirect.github.com/rust-lang/mdBook/pull/2470)

##### Fixed

-   Improved theme support when JavaScript is disabled.
[#&#8203;2454](https://redirect.github.com/rust-lang/mdBook/pull/2454)
-   Fixed broken themes when localStorage has an invalid theme id.
[#&#8203;2463](https://redirect.github.com/rust-lang/mdBook/pull/2463)
- Adjusted the line-height of superscripts (and footnotes) to avoid
adding extra space between lines.
[#&#8203;2465](https://redirect.github.com/rust-lang/mdBook/pull/2465)

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-14 12:07:17 +02:00
renovate[bot]
9fd971d8c9 Update Rust crate pulldown-cmark to v0.12.2 (#20630)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [pulldown-cmark](https://redirect.github.com/raphlinus/pulldown-cmark)
| workspace.dependencies | patch | `0.12.1` -> `0.12.2` |

---

### Release Notes

<details>
<summary>raphlinus/pulldown-cmark (pulldown-cmark)</summary>

###
[`v0.12.2`](https://redirect.github.com/pulldown-cmark/pulldown-cmark/releases/tag/v0.12.2):
0.12.2

[Compare
Source](https://redirect.github.com/raphlinus/pulldown-cmark/compare/v0.12.1...v0.12.2)

#### What's Changed

- Fix compiilation error in fuzzers by
[@&#8203;kdarkhan](https://redirect.github.com/kdarkhan) in
[https://github.com/pulldown-cmark/pulldown-cmark/pull/947](https://redirect.github.com/pulldown-cmark/pulldown-cmark/pull/947)
- Make `fuzz` dir part of the workspace by
[@&#8203;kdarkhan](https://redirect.github.com/kdarkhan) in
[https://github.com/pulldown-cmark/pulldown-cmark/pull/948](https://redirect.github.com/pulldown-cmark/pulldown-cmark/pull/948)
- Fix and improve `bench` by
[@&#8203;notriddle](https://redirect.github.com/notriddle) in
[https://github.com/pulldown-cmark/pulldown-cmark/pull/950](https://redirect.github.com/pulldown-cmark/pulldown-cmark/pull/950)
- Reuse a couple hash maps across blocks by
[@&#8203;notriddle](https://redirect.github.com/notriddle) in
[https://github.com/pulldown-cmark/pulldown-cmark/pull/951](https://redirect.github.com/pulldown-cmark/pulldown-cmark/pull/951)
- Reuse outer indent between item list, def list, and blockquote by
[@&#8203;notriddle](https://redirect.github.com/notriddle) in
[https://github.com/pulldown-cmark/pulldown-cmark/pull/952](https://redirect.github.com/pulldown-cmark/pulldown-cmark/pull/952)
- Add instructions on fixing fuzz build by
[@&#8203;kdarkhan](https://redirect.github.com/kdarkhan) in
[https://github.com/pulldown-cmark/pulldown-cmark/pull/953](https://redirect.github.com/pulldown-cmark/pulldown-cmark/pull/953)
- Account for definition list fixups while popping containers by
[@&#8203;notriddle](https://redirect.github.com/notriddle) in
[https://github.com/pulldown-cmark/pulldown-cmark/pull/954](https://redirect.github.com/pulldown-cmark/pulldown-cmark/pull/954)
- Use byte range instead of char count for delim run bounds by
[@&#8203;notriddle](https://redirect.github.com/notriddle) in
[https://github.com/pulldown-cmark/pulldown-cmark/pull/956](https://redirect.github.com/pulldown-cmark/pulldown-cmark/pull/956)
- CI improvements by
[@&#8203;kdarkhan](https://redirect.github.com/kdarkhan) in
[https://github.com/pulldown-cmark/pulldown-cmark/pull/955](https://redirect.github.com/pulldown-cmark/pulldown-cmark/pull/955)
- Fix a problem that causes multiple dt's to be parsed by
[@&#8203;notriddle](https://redirect.github.com/notriddle) in
[https://github.com/pulldown-cmark/pulldown-cmark/pull/958](https://redirect.github.com/pulldown-cmark/pulldown-cmark/pull/958)
- fix: emit `InlineHtml` for inline HTML inside blockquote instead of
`Html` by [@&#8203;rhysd](https://redirect.github.com/rhysd) in
[https://github.com/pulldown-cmark/pulldown-cmark/pull/961](https://redirect.github.com/pulldown-cmark/pulldown-cmark/pull/961)
- Complete the list of block item bodies by
[@&#8203;notriddle](https://redirect.github.com/notriddle) in
[https://github.com/pulldown-cmark/pulldown-cmark/pull/962](https://redirect.github.com/pulldown-cmark/pulldown-cmark/pull/962)
- Implement into_static for CowStr and Event in pulldown-cmark by
[@&#8203;Atreyagaurav](https://redirect.github.com/Atreyagaurav) in
[https://github.com/pulldown-cmark/pulldown-cmark/pull/967](https://redirect.github.com/pulldown-cmark/pulldown-cmark/pull/967)
- Enforce cargo fmt by
[@&#8203;ollpu](https://redirect.github.com/ollpu) in
[https://github.com/pulldown-cmark/pulldown-cmark/pull/971](https://redirect.github.com/pulldown-cmark/pulldown-cmark/pull/971)
- Respect line starts when trimming header endings by
[@&#8203;notriddle](https://redirect.github.com/notriddle) in
[https://github.com/pulldown-cmark/pulldown-cmark/pull/969](https://redirect.github.com/pulldown-cmark/pulldown-cmark/pull/969)

#### New Contributors

- [@&#8203;Atreyagaurav](https://redirect.github.com/Atreyagaurav) made
their first contribution in
[https://github.com/pulldown-cmark/pulldown-cmark/pull/967](https://redirect.github.com/pulldown-cmark/pulldown-cmark/pull/967)

**Full Changelog**:
https://github.com/pulldown-cmark/pulldown-cmark/compare/v0.12.1...v0.12.2

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-14 12:07:03 +02:00
renovate[bot]
cf7679e6a0 Update Rust crate clap to v4.5.21 (#20620)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [clap](https://redirect.github.com/clap-rs/clap) |
workspace.dependencies | patch | `4.5.20` -> `4.5.21` |

---

### Release Notes

<details>
<summary>clap-rs/clap (clap)</summary>

###
[`v4.5.21`](https://redirect.github.com/clap-rs/clap/blob/HEAD/CHANGELOG.md#4521---2024-11-13)

[Compare
Source](https://redirect.github.com/clap-rs/clap/compare/v4.5.20...v4.5.21)

##### Fixes

- *(parser)* Ensure defaults are filled in on error with
`ignore_errors(true)`

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-14 11:12:14 +02:00
renovate[bot]
07c0c54c28 Update Rust crate anyhow to v1.0.93 (#20619)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [anyhow](https://redirect.github.com/dtolnay/anyhow) |
workspace.dependencies | patch | `1.0.91` -> `1.0.93` |

---

### Release Notes

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

###
[`v1.0.93`](https://redirect.github.com/dtolnay/anyhow/releases/tag/1.0.93)

[Compare
Source](https://redirect.github.com/dtolnay/anyhow/compare/1.0.92...1.0.93)

-   Update dev-dependencies to `thiserror` v2

###
[`v1.0.92`](https://redirect.github.com/dtolnay/anyhow/releases/tag/1.0.92)

[Compare
Source](https://redirect.github.com/dtolnay/anyhow/compare/1.0.91...1.0.92)

- Support Rust 1.82's `&raw const` and `&raw mut` syntax inside
`ensure!`
([#&#8203;390](https://redirect.github.com/dtolnay/anyhow/issues/390))

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-14 11:11:52 +02:00
Max Brunsfeld
093c9cc87b Avoid creating occlusions for editor blocks, since these block mouse wheel events (#20649)
Just block mouse down events, and in the case of the inline assist
prompt, set the default cursor.

Release Notes:

- N/A

Co-authored-by: Richard <richard@zed.dev>
2024-11-13 21:02:54 -08:00
Bret Comnes
6b3c909155 Clarify rustup is requried (#20642)
Clarify that rustup is required to build developer extensions. Developer
extensions fail silently to the logs because rustup isn't found, even
when rust is installed.

Release Notes:

- N/A
2024-11-13 17:20:20 -08:00
Conrad Irwin
7e349e52b1 Don't try and run on armv5/6/7 (#20618)
Updates: #20523

Release Notes:

- SSH Remoting: correctly show an error when SSH'ing into a 32-bit arm
system
2024-11-13 16:18:53 -07:00
Max Brunsfeld
84d17fb191 Fix completions for non-built-in slash commands (#20632)
Release Notes:

- N/A

Co-authored-by: Marshall <marshall@zed.dev>
2024-11-13 15:11:50 -08:00
Max Brunsfeld
d3d408d47d Improve context server lifecycle management (#20622)
This optimizes and fixes bugs in our logic for maintaining a set of
running context servers, based on the combination of the user's
`context_servers` settings and their installed extensions.

Release Notes:

- N/A

---------

Co-authored-by: Marshall <marshall@zed.dev>
Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-11-13 13:55:06 -08:00
Conrad Irwin
6e477bbf56 Don't double-localize menu shortcuts (#20623)
Release Notes:

- Don't have macOS localize our menu shortcuts that we already
localized.
2024-11-13 13:56:56 -07:00
Conrad Irwin
3c2dcf50fa Don't send key equivalents to the input hanlder (#20621)
Release Notes:

- Fix `cmd-backtick` to change windows
2024-11-13 13:42:31 -07:00
David Soria Parra
a15f408f0c anthropic: Remove stable headers (#20595)
The tool and context length headers are now stable and no longer needed.

Release Notes:

- N/A
2024-11-13 15:04:37 -05:00
Axel Carlsson
b1cd9e4d24 vim: Add support for temporary normal mode (ctrl-o) within insert mode (#19454)
Support has been added for the ctrl-o command within insert mode. Ctrl-o
is used to partially enter normal mode for 1 motion to then return back
into insert mode.

Release Notes:

- vim: Added support for `ctrl-o` in insert mode to enter temporary
normal mode

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-11-13 12:44:41 -07:00
Marshall Bowers
254ce74036 Extract ExtensionSlashCommand to assistant_slash_command crate (#20617)
This PR extracts the `ExtensionSlashCommand` implementation to the
`assistant_slash_command` crate.

The slash command related methods have been added to the `Extension`
trait. We also create separate data types for the slash command data
within the `extension` crate so that we can talk about them without
depending on the `extension_host` or `assistant_slash_command`.

Release Notes:

- N/A
2024-11-13 14:34:58 -05:00
Kyle Kelley
b913cf2e02 Allow base64 encoded images to be decoded with or without padding (#20616)
The R kernel doesn't use base64 padding whereas the Python kernel (via
matplotlib) sometimes uses padding. We have to use the `base64` crate's
`Indifferent` mode.

/cherry-pick v0.161.x

Release Notes:

- N/A
2024-11-13 11:26:42 -08:00
Antonio Scandurra
92613a8904 Use replace blocks for patches (#20605)
Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
Co-authored-by: Richard <richard@zed.dev>
2024-11-13 18:55:23 +01:00
Conrad Irwin
96deabfb78 Deadkeys 2 (#20612)
Re-land of #20515 with less brokenness

In particular it turns out that for control, the .characters() method
returns the control code. This mostly didn't make a difference, except
when the control code matched tab/enter/escape (for
ctrl-y,ctrl-[/ctrl-c) as we interpreted the key incorrectly.

Secondly, we were setting IME key too aggressively. This led to (in vim
mode) cmd-shift-{ being interpreted as [, so vim would wait for a second
[ before letting you change tab.

Release Notes:

- N/A
2024-11-13 10:42:08 -07:00
Peter Tripp
ad31aacb7a Fix bad quote in script/determine-release-channel (#20613) 2024-11-13 12:41:50 -05:00
Marshall Bowers
a04c2ecff7 Decouple extension Worktree resource from LspAdapterDelegate (#20611)
This PR decouples the extension `Worktree` resource from the
`LspAdapterDelegate`.

We now have a `WorktreeDelegate` trait that corresponds to the methods
on the resource.

We then create a `WorktreeDelegateAdapter` that can wrap an
`LspAdapterDelegate` and implement the `WorktreeDelegate` trait.

Release Notes:

- N/A
2024-11-13 12:24:27 -05:00
Joseph T. Lyons
f96b29ca54 v0.163.x dev 2024-11-13 11:47:52 -05:00
Danilo Leal
9d2fc691de project panel: Don't change label color even when file has errors (#20600)
With this PR, Git status is now the only thing that can change an item's
label color. So, the summary of how status colors operate in the project
panel is:

- Item icon color is, by default, never changed, _not_ affected by
either diagnostics or Git status
  - This should become configurable in the near future, though
- However, a little x or triangle icon shows up on top of the file type
icon to display diagnostics status
- Label color is _not_ affected by diagnostics but it _is_ affected by
Git status

This aims to reduce color noise and clarify/simplify how each element is
affected.

Release Notes:

- N/A
2024-11-13 13:35:20 -03:00
Marshall Bowers
b084d53f8e Extract ExtensionIndexedDocsProvider to indexed_docs crate (#20607)
This PR extracts the `ExtensionIndexedDocsProvider` implementation to
the `indexed_docs` crate.

To achieve this, we introduce a new `Extension` trait that provides an
abstracted interface for calling an extension. This trait resides in the
`extension` crate, which has minimal dependencies and can be depended on
by other crates, like `indexed_docs`.

We're then able to implement the `ExtensionIndexedDocsProvider` without
having any knowledge of the Wasm-specific internals of the extension
system.

Release Notes:

- N/A
2024-11-13 11:19:55 -05:00
Askar
7832883c74 terminal: Fix detection of ignored python venv (#20227)
Closes #19227

Since items listed in `.gitignore` file are not included in a worktree,
python virtual environment cannot be detected until venv directory is
unfolded from project panel and forcefully added into worktree. I didn't
come up with anything better than scanning fs directly. I'm not sure how
it will affect remote development. if at all.

Release Notes:

- Fixed detection of `detect_venv.directories` ignored by a worktree
2024-11-13 17:19:25 +01:00
Peter Tripp
eb4e7472e6 Improve terminal.working_directory for non-project files (#18251) 2024-11-13 10:40:36 -05:00
Thorsten Ball
27dfb48a7b project panel: Fix entries being marked when switching between tabs (#20596)
This is a follow-up (or related to) #20412.

It fixes entries being marked when navigating between tabs with `cmd-[`
and `cmd-]`.

Turns out that deep in the bowels of the project panel, we check whether
a `shift` modifier was pressed - which is the case with `cmd-[` on a US
ANSI layout - and if so mark an entry.

I think that's a left-over, because all the other code paths that
select/reveal an entry mark it explicitly too.

Release Notes:

- Fixed entries in project panel being marked when navigating between
tabs with keybinding that uses `shift` modifier.
2024-11-13 15:32:19 +01:00
Peter Tripp
3a319e6cbe docs: Improve formatter docs. Examples for C/C++ (#20553) 2024-11-13 09:24:50 -05:00
Kirill Bulatov
84e47fb80b Fix context menus not cycling over its edge when selecting items (#20592) 2024-11-13 15:14:23 +01:00
Kirill Bulatov
7e82ca8082 Always allow rerunning the tasks using the tab button (#20591)
Release Notes:

- Improved task tabs to always rerun tasks on click
2024-11-13 14:57:30 +01:00
Danilo Leal
b44078781d Standardize button design in modal footers (#20585)
- Making sure this design and properties are the same across different
places
- No need for the `ButtonLike` here, we can use `Button` as it supports
`key_binding` and makes it for a cleaner code!
- Also, that ensures the binding is always to the right of the label,
which makes more sense
- Title-case the labels for consistency with other buttons across the
app

| File finder | Project finder |
|--------|--------|
| <img width="1136" alt="Screenshot 2024-11-13 at 09 21 06"
src="https://github.com/user-attachments/assets/dd051514-d873-4b65-a08f-af0920f2c010">
| <img width="1136" alt="Screenshot 2024-11-13 at 09 21 12"
src="https://github.com/user-attachments/assets/f958e3e7-4bfb-4752-839e-2bbc01334643">
|

Release Notes:

- N/A
2024-11-13 09:36:08 -03:00
Piotr Osiewicz
3b1f12af75 chore: Cleanup dev_server_projects leftover files (#20581)
Closes #ISSUE

Release Notes:

- N/A
2024-11-13 13:04:12 +01:00
Piotr Osiewicz
b8cf0a1ed1 chore: use codegen-units=1 for small crates (#20558)
These 500ms we'll save with this change will surely pay off.

Release Notes:

- N/A
2024-11-13 12:51:30 +01:00
Kirill Bulatov
3f224274da Restore the ability to navigate into project search input with the keyboard (#20579) 2024-11-13 12:45:30 +01:00
Kirill Bulatov
56cf32cb91 Add a way to use splits when opening in file finder (#20507) 2024-11-13 11:58:42 +01:00
Markus Wüstenberg
90ffd65a10 Document use of allow_concurrent_runs with long-running tasks (#20539) 2024-11-13 09:43:17 +01:00
116 changed files with 2985 additions and 2199 deletions

View File

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

654
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -562,6 +562,46 @@ rustybuzz = { opt-level = 3 }
ttf-parser = { opt-level = 3 }
wasmtime-cranelift = { opt-level = 3 }
wasmtime = { opt-level = 3 }
# Build single-source-file crates with cg=1 as it helps make `cargo build` of a whole workspace a bit faster
activity_indicator = {codegen-units = 1}
assets = {codegen-units = 1}
breadcrumbs = {codegen-units = 1}
collections = {codegen-units = 1}
command_palette = {codegen-units = 1}
command_palette_hooks = {codegen-units = 1}
evals = {codegen-units = 1}
extension_cli = {codegen-units = 1}
feature_flags = {codegen-units = 1}
file_icons = {codegen-units = 1}
fsevent = {codegen-units = 1}
image_viewer = {codegen-units = 1}
inline_completion_button = {codegen-units = 1}
install_cli = {codegen-units = 1}
journal = {codegen-units = 1}
menu = {codegen-units = 1}
notifications = {codegen-units = 1}
ollama = {codegen-units = 1}
outline = {codegen-units = 1}
paths = {codegen-units = 1}
prettier = {codegen-units = 1}
project_symbols = {codegen-units = 1}
refineable = {codegen-units = 1}
release_channel = {codegen-units = 1}
reqwest_client = {codegen-units = 1}
rich_text = {codegen-units = 1}
semantic_version = {codegen-units = 1}
session = {codegen-units = 1}
snippet = {codegen-units = 1}
snippets_ui = {codegen-units = 1}
sqlez_macros = {codegen-units = 1}
story = {codegen-units = 1}
supermaven_api = {codegen-units = 1}
telemetry_events = {codegen-units = 1}
theme_selector = {codegen-units = 1}
time_format = {codegen-units = 1}
ui_input = {codegen-units = 1}
vcs_menu = {codegen-units = 1}
zed_actions = {codegen-units = 1}
[profile.release]
debug = "limited"

View File

@@ -649,7 +649,19 @@
},
{
"context": "FileFinder",
"bindings": { "ctrl-shift-p": "file_finder::SelectPrev" }
"bindings": {
"ctrl-shift-p": "file_finder::SelectPrev",
"ctrl-k": "file_finder::OpenMenu"
}
},
{
"context": "FileFinder && menu_open",
"bindings": {
"u": "pane::SplitUp",
"d": "pane::SplitDown",
"l": "pane::SplitLeft",
"r": "pane::SplitRight"
}
},
{
"context": "TabSwitcher",

View File

@@ -649,7 +649,19 @@
},
{
"context": "FileFinder",
"bindings": { "cmd-shift-p": "file_finder::SelectPrev" }
"bindings": {
"cmd-shift-p": "file_finder::SelectPrev",
"cmd-k": "file_finder::OpenMenu"
}
},
{
"context": "FileFinder && menu_open",
"bindings": {
"u": "pane::SplitUp",
"d": "pane::SplitDown",
"l": "pane::SplitLeft",
"r": "pane::SplitRight"
}
},
{
"context": "TabSwitcher",

View File

@@ -304,7 +304,8 @@
"ctrl-q": ["vim::PushOperator", { "Literal": {} }],
"ctrl-shift-q": ["vim::PushOperator", { "Literal": {} }],
"ctrl-r": ["vim::PushOperator", "Register"],
"insert": "vim::ToggleReplace"
"insert": "vim::ToggleReplace",
"ctrl-o": "vim::TemporaryNormal"
}
},
{

View File

@@ -161,10 +161,7 @@ pub async fn complete(
.method(Method::POST)
.uri(uri)
.header("Anthropic-Version", "2023-06-01")
.header(
"Anthropic-Beta",
"tools-2024-04-04,prompt-caching-2024-07-31,max-tokens-3-5-sonnet-2024-07-15",
)
.header("Anthropic-Beta", "prompt-caching-2024-07-31")
.header("X-Api-Key", api_key)
.header("Content-Type", "application/json");

View File

@@ -1480,7 +1480,6 @@ struct ScrollPosition {
}
struct PatchViewState {
footer_block_id: CustomBlockId,
crease_id: CreaseId,
editor: Option<PatchEditorState>,
update_task: Option<Task<()>>,
@@ -1934,7 +1933,7 @@ impl ContextEditor {
);
});
Crease::new(
Crease::inline(
start..end,
placeholder,
fold_toggle("tool-use"),
@@ -2032,7 +2031,7 @@ impl ContextEditor {
let end = buffer
.anchor_in_excerpt(excerpt_id, command.source_range.end)
.unwrap();
Crease::new(start..end, placeholder, render_toggle, render_trailer)
Crease::inline(start..end, placeholder, render_toggle, render_trailer)
}),
cx,
);
@@ -2124,7 +2123,7 @@ impl ContextEditor {
let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
let crease = Crease::new(
let crease = Crease::inline(
start..end,
placeholder,
fold_toggle("tool-use"),
@@ -2192,18 +2191,14 @@ impl ContextEditor {
let crease_end = buffer
.anchor_in_excerpt(excerpt_id, invoked_slash_command.range.end)
.unwrap();
let fold_placeholder =
invoked_slash_command_fold_placeholder(command_id, context);
let crease_ids = editor.insert_creases(
[Crease::new(
crease_start..crease_end,
fold_placeholder.clone(),
fold_toggle("invoked-slash-command"),
|_row, _folded, _cx| Empty.into_any(),
)],
cx,
let crease = Crease::inline(
crease_start..crease_end,
invoked_slash_command_fold_placeholder(command_id, context),
fold_toggle("invoked-slash-command"),
|_row, _folded, _cx| Empty.into_any(),
);
editor.fold_ranges([(crease_start..crease_end, fold_placeholder)], false, cx);
let crease_ids = editor.insert_creases([crease.clone()], cx);
editor.fold_creases(vec![crease], false, cx);
entry.insert(crease_ids[0]);
} else {
cx.notify()
@@ -2225,23 +2220,32 @@ impl ContextEditor {
cx: &mut ViewContext<ContextEditor>,
) {
let this = cx.view().downgrade();
let mut removed_crease_ids = Vec::new();
let mut removed_block_ids = HashSet::default();
let mut editors_to_close = Vec::new();
for range in removed {
if let Some(state) = self.patches.remove(range) {
editors_to_close.extend(state.editor.and_then(|state| state.editor.upgrade()));
removed_block_ids.insert(state.footer_block_id);
removed_crease_ids.push(state.crease_id);
}
}
self.editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
let multibuffer = &snapshot.buffer_snapshot;
let (&excerpt_id, _, _) = multibuffer.as_singleton().unwrap();
let mut replaced_blocks = HashMap::default();
let mut removed_crease_ids = Vec::new();
let mut ranges_to_unfold: Vec<Range<Anchor>> = Vec::new();
for range in removed {
if let Some(state) = self.patches.remove(range) {
let patch_start = multibuffer
.anchor_in_excerpt(excerpt_id, range.start)
.unwrap();
let patch_end = multibuffer
.anchor_in_excerpt(excerpt_id, range.end)
.unwrap();
editors_to_close.extend(state.editor.and_then(|state| state.editor.upgrade()));
ranges_to_unfold.push(patch_start..patch_end);
removed_crease_ids.push(state.crease_id);
}
}
editor.unfold_ranges(&ranges_to_unfold, true, false, cx);
editor.remove_creases(removed_crease_ids, cx);
for range in updated {
let Some(patch) = self.context.read(cx).patch_for_range(&range, cx).cloned() else {
continue;
@@ -2254,19 +2258,21 @@ impl ContextEditor {
let patch_end = multibuffer
.anchor_in_excerpt(excerpt_id, patch.range.end)
.unwrap();
let render_block: RenderBlock = Box::new({
let render_block: RenderBlock = Arc::new({
let this = this.clone();
let patch_range = range.clone();
move |cx: &mut BlockContext<'_, '_>| {
let max_width = cx.max_width;
let gutter_width = cx.gutter_dimensions.full_width();
let block_id = cx.block_id;
let selected = cx.selected;
this.update(&mut **cx, |this, cx| {
this.render_patch_footer(
this.render_patch_block(
patch_range.clone(),
max_width,
gutter_width,
block_id,
selected,
cx,
)
})
@@ -2276,25 +2282,16 @@ impl ContextEditor {
}
});
let header_placeholder = FoldPlaceholder {
render: {
let this = this.clone();
let patch_range = range.clone();
Arc::new(move |fold_id, _range, cx| {
this.update(cx, |this, cx| {
this.render_patch_header(patch_range.clone(), fold_id, cx)
})
.ok()
.flatten()
.unwrap_or_else(|| Empty.into_any())
})
},
..Default::default()
};
let height = path_count as u32 + 1;
let crease = Crease::block(
patch_start..patch_end,
height,
BlockStyle::Flex,
render_block.clone(),
);
let should_refold;
if let Some(state) = self.patches.get_mut(&range) {
replaced_blocks.insert(state.footer_block_id, render_block);
if let Some(editor_state) = &state.editor {
if editor_state.opened_patch != patch {
state.update_task = Some({
@@ -2311,33 +2308,11 @@ impl ContextEditor {
should_refold =
snapshot.intersects_fold(patch_start.to_offset(&snapshot.buffer_snapshot));
} else {
let block_ids = editor.insert_blocks(
[BlockProperties {
height: path_count as u32 + 1,
style: BlockStyle::Flex,
render: render_block,
placement: BlockPlacement::Below(patch_start),
priority: 0,
}],
None,
cx,
);
let new_crease_ids = editor.insert_creases(
[Crease::new(
patch_start..patch_end,
header_placeholder.clone(),
fold_toggle("patch-header"),
|_, _, _| Empty.into_any_element(),
)],
cx,
);
let crease_id = editor.insert_creases([crease.clone()], cx)[0];
self.patches.insert(
range.clone(),
PatchViewState {
footer_block_id: block_ids[0],
crease_id: new_crease_ids[0],
crease_id,
editor: None,
update_task: None,
},
@@ -2348,13 +2323,9 @@ impl ContextEditor {
if should_refold {
editor.unfold_ranges(&[patch_start..patch_end], true, false, cx);
editor.fold_ranges([(patch_start..patch_end, header_placeholder)], false, cx);
editor.fold_creases(vec![crease], false, cx);
}
}
editor.remove_creases(removed_crease_ids, cx);
editor.remove_blocks(removed_block_ids, None, cx);
editor.replace_blocks(replaced_blocks, None, cx);
});
for editor in editors_to_close {
@@ -2385,7 +2356,7 @@ impl ContextEditor {
let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
buffer_rows_to_fold.insert(buffer_row);
creases.push(
Crease::new(
Crease::inline(
start..end,
FoldPlaceholder {
render: render_fold_icon_button(
@@ -2674,7 +2645,7 @@ impl ContextEditor {
let mut blocks_to_replace: HashMap<_, RenderBlock> = Default::default();
let render_block = |message: MessageMetadata| -> RenderBlock {
Box::new({
Arc::new({
let context = self.context.clone();
move |cx| {
@@ -3127,7 +3098,7 @@ impl ContextEditor {
crease_title,
cx.view().downgrade(),
);
let crease = Crease::new(
let crease = Crease::inline(
anchor_before..anchor_after,
fold_placeholder,
render_quote_selection_output_toggle,
@@ -3217,31 +3188,29 @@ impl ContextEditor {
&snapshot,
)
.filter_map(|crease| {
if let Some(metadata) = &crease.metadata {
let start = crease
.range
if let Crease::Inline {
range, metadata, ..
} = &crease
{
let metadata = metadata.as_ref()?;
let start = range
.start
.to_offset(&snapshot)
.saturating_sub(selection_start);
let end = crease
.range
let end = range
.end
.to_offset(&snapshot)
.saturating_sub(selection_start);
let range_relative_to_selection = start..end;
if range_relative_to_selection.is_empty() {
None
} else {
Some(SelectedCreaseMetadata {
if !range_relative_to_selection.is_empty() {
return Some(SelectedCreaseMetadata {
range_relative_to_selection,
crease: metadata.clone(),
})
});
}
} else {
None
}
None
})
.collect::<Vec<_>>()
}),
@@ -3322,7 +3291,7 @@ impl ContextEditor {
let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
buffer_rows_to_fold.insert(buffer_row);
Crease::new(
Crease::inline(
start..end,
FoldPlaceholder {
render: render_fold_icon_button(
@@ -3415,7 +3384,7 @@ impl ContextEditor {
placement: BlockPlacement::Above(anchor),
height: MAX_HEIGHT_IN_LINES,
style: BlockStyle::Sticky,
render: Box::new(move |cx| {
render: Arc::new(move |cx| {
let image_size = size_for_image(
&image,
size(
@@ -3472,33 +3441,13 @@ impl ContextEditor {
.unwrap_or_else(|| Cow::Borrowed(DEFAULT_TAB_TITLE))
}
fn render_patch_header(
&self,
range: Range<text::Anchor>,
_id: FoldId,
cx: &mut ViewContext<Self>,
) -> Option<AnyElement> {
let patch = self.context.read(cx).patch_for_range(&range, cx)?;
let theme = cx.theme().clone();
Some(
h_flex()
.px_1()
.py_0p5()
.border_b_1()
.border_color(theme.status().info_border)
.gap_1()
.child(Icon::new(IconName::Diff).size(IconSize::Small))
.child(Label::new(patch.title.clone()).size(LabelSize::Small))
.into_any(),
)
}
fn render_patch_footer(
fn render_patch_block(
&mut self,
range: Range<text::Anchor>,
max_width: Pixels,
gutter_width: Pixels,
id: BlockId,
selected: bool,
cx: &mut ViewContext<Self>,
) -> Option<AnyElement> {
let snapshot = self.editor.update(cx, |editor, cx| editor.snapshot(cx));
@@ -3509,10 +3458,7 @@ impl ContextEditor {
.anchor_in_excerpt(excerpt_id, range.start)
.unwrap();
if !snapshot.intersects_fold(anchor) {
return None;
}
let theme = cx.theme().clone();
let patch = self.context.read(cx).patch_for_range(&range, cx)?;
let paths = patch
.paths()
@@ -3522,9 +3468,18 @@ impl ContextEditor {
Some(
v_flex()
.id(id)
.pl(gutter_width)
.w(max_width)
.py_2()
.bg(theme.colors().editor_background)
.ml(gutter_width)
.pb_1()
.w(max_width - gutter_width)
.rounded_md()
.border_1()
.border_color(theme.colors().border_variant)
.overflow_hidden()
.hover(|style| style.border_color(theme.colors().text_accent))
.when(selected, |this| {
this.border_color(theme.colors().text_accent)
})
.cursor(CursorStyle::PointingHand)
.on_click(cx.listener(move |this, _, cx| {
this.editor.update(cx, |editor, cx| {
@@ -3534,24 +3489,60 @@ impl ContextEditor {
});
this.focus_active_patch(cx);
}))
.child(
div()
.px_2()
.py_1()
.overflow_hidden()
.text_ellipsis()
.border_b_1()
.border_color(theme.colors().border_variant)
.bg(theme.colors().element_background)
.child(
Label::new(patch.title.clone())
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.children(paths.into_iter().map(|path| {
h_flex()
.pl_1()
.gap_1()
.px_2()
.pt_1()
.gap_1p5()
.child(Icon::new(IconName::File).size(IconSize::Small))
.child(Label::new(path).size(LabelSize::Small))
}))
.when(patch.status == AssistantPatchStatus::Pending, |div| {
div.child(
Label::new("Generating")
.color(Color::Muted)
.size(LabelSize::Small)
.with_animation(
"pulsating-label",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.4, 1.)),
|label, delta| label.alpha(delta),
h_flex()
.pt_1()
.px_2()
.gap_1()
.child(
Icon::new(IconName::ArrowCircle)
.size(IconSize::XSmall)
.color(Color::Muted)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(percentage(
delta,
)))
},
),
)
.child(
Label::new("Generating…")
.color(Color::Muted)
.size(LabelSize::Small)
.with_animation(
"pulsating-label",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.4, 0.8)),
|label, delta| label.alpha(delta),
),
),
)
})

View File

@@ -8,9 +8,8 @@ use anyhow::{anyhow, Context as _, Result};
use client::{proto, telemetry::Telemetry, Client, TypedEnvelope};
use clock::ReplicaId;
use collections::HashMap;
use command_palette_hooks::CommandPaletteFilter;
use context_servers::manager::{ContextServerManager, ContextServerSettings};
use context_servers::{ContextServerFactoryRegistry, CONTEXT_SERVERS_NAMESPACE};
use context_servers::manager::ContextServerManager;
use context_servers::ContextServerFactoryRegistry;
use fs::Fs;
use futures::StreamExt;
use fuzzy::StringMatchCandidate;
@@ -22,7 +21,6 @@ use paths::contexts_dir;
use project::Project;
use regex::Regex;
use rpc::AnyProtoClient;
use settings::{Settings as _, SettingsStore};
use std::{
cmp::Reverse,
ffi::OsStr,
@@ -111,7 +109,11 @@ impl ContextStore {
let (mut events, _) = fs.watch(contexts_dir(), CONTEXT_WATCH_DURATION).await;
let this = cx.new_model(|cx: &mut ModelContext<Self>| {
let context_server_manager = cx.new_model(|_cx| ContextServerManager::new());
let context_server_factory_registry =
ContextServerFactoryRegistry::default_global(cx);
let context_server_manager = cx.new_model(|cx| {
ContextServerManager::new(context_server_factory_registry, project.clone(), cx)
});
let mut this = Self {
contexts: Vec::new(),
contexts_metadata: Vec::new(),
@@ -148,90 +150,16 @@ impl ContextStore {
this.handle_project_changed(project.clone(), cx);
this.synchronize_contexts(cx);
this.register_context_server_handlers(cx);
if project.read(cx).is_local() {
// TODO: At the time when we construct the `ContextStore` we may not have yet initialized the extensions.
// In order to register the context servers when the extension is loaded, we're periodically looping to
// see if there are context servers to register.
//
// I tried doing this in a subscription on the `ExtensionStore`, but it never seemed to fire.
//
// We should find a more elegant way to do this.
let context_server_factory_registry = ContextServerFactoryRegistry::global(cx);
cx.spawn(|context_store, mut cx| async move {
loop {
let mut servers_to_register = Vec::new();
for (_id, factory) in
context_server_factory_registry.context_server_factories()
{
if let Some(server) = factory(project.clone(), &cx).await.log_err()
{
servers_to_register.push(server);
}
}
let Some(_) = context_store
.update(&mut cx, |this, cx| {
this.context_server_manager.update(cx, |this, cx| {
for server in servers_to_register {
this.add_server(server, cx).detach_and_log_err(cx);
}
})
})
.log_err()
else {
break;
};
smol::Timer::after(Duration::from_millis(100)).await;
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
this
})?;
this.update(&mut cx, |this, cx| this.reload(cx))?
.await
.log_err();
this.update(&mut cx, |this, cx| {
this.watch_context_server_settings(cx);
})
.log_err();
Ok(this)
})
}
fn watch_context_server_settings(&self, cx: &mut ModelContext<Self>) {
cx.observe_global::<SettingsStore>(move |this, cx| {
this.context_server_manager.update(cx, |manager, cx| {
let location = this.project.read(cx).worktrees(cx).next().map(|worktree| {
settings::SettingsLocation {
worktree_id: worktree.read(cx).id(),
path: Path::new(""),
}
});
let settings = ContextServerSettings::get(location, cx);
manager.maintain_servers(settings, cx);
let has_any_context_servers = !manager.servers().is_empty();
CommandPaletteFilter::update_global(cx, |filter, _cx| {
if has_any_context_servers {
filter.show_namespace(CONTEXT_SERVERS_NAMESPACE);
} else {
filter.hide_namespace(CONTEXT_SERVERS_NAMESPACE);
}
});
})
})
.detach();
}
async fn handle_advertise_contexts(
this: Model<Self>,
envelope: TypedEnvelope<proto::AdvertiseContexts>,

View File

@@ -24,9 +24,9 @@ use futures::{
join, SinkExt, Stream, StreamExt,
};
use gpui::{
anchored, deferred, point, AnyElement, AppContext, ClickEvent, EventEmitter, FocusHandle,
FocusableView, FontWeight, Global, HighlightStyle, Model, ModelContext, Subscription, Task,
TextStyle, UpdateGlobal, View, ViewContext, WeakView, WindowContext,
anchored, deferred, point, AnyElement, AppContext, ClickEvent, CursorStyle, EventEmitter,
FocusHandle, FocusableView, FontWeight, Global, HighlightStyle, Model, ModelContext,
Subscription, Task, TextStyle, UpdateGlobal, View, ViewContext, WeakView, WindowContext,
};
use language::{Buffer, IndentKind, Point, Selection, TransactionId};
use language_model::{
@@ -460,7 +460,7 @@ impl InlineAssistant {
style: BlockStyle::Sticky,
placement: BlockPlacement::Below(range.end),
height: 0,
render: Box::new(|cx| {
render: Arc::new(|cx| {
v_flex()
.h_full()
.w_full()
@@ -1197,8 +1197,9 @@ impl InlineAssistant {
placement: BlockPlacement::Above(new_row),
height,
style: BlockStyle::Flex,
render: Box::new(move |cx| {
render: Arc::new(move |cx| {
div()
.block_mouse_down()
.bg(cx.theme().status().deleted_background)
.size_full()
.h(height as f32 * cx.line_height())
@@ -1317,7 +1318,7 @@ impl InlineAssistGroup {
fn build_assist_editor_renderer(editor: &View<PromptEditor>) -> RenderBlock {
let editor = editor.clone();
Box::new(move |cx: &mut BlockContext| {
Arc::new(move |cx: &mut BlockContext| {
*editor.read(cx).gutter_dimensions.lock() = *cx.gutter_dimensions;
editor.clone().into_any_element()
})
@@ -1480,6 +1481,8 @@ impl Render for PromptEditor {
h_flex()
.key_context("PromptEditor")
.bg(cx.theme().colors().editor_background)
.block_mouse_down()
.cursor(CursorStyle::Arrow)
.border_y_1()
.border_color(cx.theme().status().info_border)
.size_full()

View File

@@ -2,7 +2,7 @@ use crate::assistant_panel::ContextEditor;
use crate::SlashCommandWorkingSet;
use anyhow::Result;
use assistant_slash_command::AfterCompletion;
pub use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandRegistry};
pub use assistant_slash_command::{SlashCommand, SlashCommandOutput};
use editor::{CompletionProvider, Editor};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{AppContext, Model, Task, ViewContext, WeakView, WindowContext};
@@ -171,8 +171,7 @@ impl SlashCommandCompletionProvider {
let mut flag = self.cancel_flag.lock();
flag.store(true, SeqCst);
*flag = new_cancel_flag.clone();
let commands = SlashCommandRegistry::global(cx);
if let Some(command) = commands.command(command_name) {
if let Some(command) = self.slash_commands.command(command_name, cx) {
let completions = command.complete_argument(
arguments,
new_cancel_flag.clone(),

View File

@@ -27,7 +27,7 @@ pub struct ContextServerSlashCommand {
impl ContextServerSlashCommand {
pub fn new(
server_manager: Model<ContextServerManager>,
server: &Arc<dyn ContextServer>,
server: &Arc<ContextServer>,
prompt: Prompt,
) -> Self {
Self {

View File

@@ -13,8 +13,10 @@ path = "src/assistant_slash_command.rs"
[dependencies]
anyhow.workspace = true
async-trait.workspace = true
collections.workspace = true
derive_more.workspace = true
extension.workspace = true
futures.workspace = true
gpui.workspace = true
language.workspace = true
@@ -22,6 +24,7 @@ language_model.workspace = true
parking_lot.workspace = true
serde.workspace = true
serde_json.workspace = true
ui.workspace = true
workspace.workspace = true
[dev-dependencies]

View File

@@ -1,5 +1,8 @@
mod extension_slash_command;
mod slash_command_registry;
pub use crate::extension_slash_command::*;
pub use crate::slash_command_registry::*;
use anyhow::Result;
use futures::stream::{self, BoxStream};
use futures::StreamExt;
@@ -7,7 +10,6 @@ use gpui::{AnyElement, AppContext, ElementId, SharedString, Task, WeakView, Wind
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt};
pub use language_model::Role;
use serde::{Deserialize, Serialize};
pub use slash_command_registry::*;
use std::{
ops::Range,
sync::{atomic::AtomicBool, Arc},

View File

@@ -0,0 +1,143 @@
use std::path::PathBuf;
use std::sync::{atomic::AtomicBool, Arc};
use anyhow::Result;
use async_trait::async_trait;
use extension::{Extension, WorktreeDelegate};
use gpui::{Task, WeakView, WindowContext};
use language::{BufferSnapshot, LspAdapterDelegate};
use ui::prelude::*;
use workspace::Workspace;
use crate::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
SlashCommandResult,
};
/// An adapter that allows an [`LspAdapterDelegate`] to be used as a [`WorktreeDelegate`].
struct WorktreeDelegateAdapter(Arc<dyn LspAdapterDelegate>);
#[async_trait]
impl WorktreeDelegate for WorktreeDelegateAdapter {
fn id(&self) -> u64 {
self.0.worktree_id().to_proto()
}
fn root_path(&self) -> String {
self.0.worktree_root_path().to_string_lossy().to_string()
}
async fn read_text_file(&self, path: PathBuf) -> Result<String> {
self.0.read_text_file(path).await
}
async fn which(&self, binary_name: String) -> Option<String> {
self.0
.which(binary_name.as_ref())
.await
.map(|path| path.to_string_lossy().to_string())
}
async fn shell_env(&self) -> Vec<(String, String)> {
self.0.shell_env().await.into_iter().collect()
}
}
pub struct ExtensionSlashCommand {
extension: Arc<dyn Extension>,
command: extension::SlashCommand,
}
impl ExtensionSlashCommand {
pub fn new(extension: Arc<dyn Extension>, command: extension::SlashCommand) -> Self {
Self { extension, command }
}
}
impl SlashCommand for ExtensionSlashCommand {
fn name(&self) -> String {
self.command.name.clone()
}
fn description(&self) -> String {
self.command.description.clone()
}
fn menu_text(&self) -> String {
self.command.tooltip_text.clone()
}
fn requires_argument(&self) -> bool {
self.command.requires_argument
}
fn complete_argument(
self: Arc<Self>,
arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
let command = self.command.clone();
let arguments = arguments.to_owned();
cx.background_executor().spawn(async move {
let completions = self
.extension
.complete_slash_command_argument(command, arguments)
.await?;
anyhow::Ok(
completions
.into_iter()
.map(|completion| ArgumentCompletion {
label: completion.label.into(),
new_text: completion.new_text,
replace_previous_arguments: false,
after_completion: completion.run_command.into(),
})
.collect(),
)
})
}
fn run(
self: Arc<Self>,
arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
_workspace: WeakView<Workspace>,
delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
) -> Task<SlashCommandResult> {
let command = self.command.clone();
let arguments = arguments.to_owned();
let output = cx.background_executor().spawn(async move {
let delegate =
delegate.map(|delegate| Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _);
let output = self
.extension
.run_slash_command(command, arguments, delegate)
.await?;
anyhow::Ok(output)
});
cx.foreground_executor().spawn(async move {
let output = output.await?;
Ok(SlashCommandOutput {
text: output.text,
sections: output
.sections
.into_iter()
.map(|section| SlashCommandOutputSection {
range: section.range,
icon: IconName::Code,
label: section.label.into(),
metadata: None,
})
.collect(),
run_commands_in_text: false,
}
.to_event_stream())
})
}
}

View File

@@ -78,6 +78,7 @@ uuid.workspace = true
[dev-dependencies]
assistant = { workspace = true, features = ["test-support"] }
context_servers.workspace = true
async-trait.workspace = true
audio.workspace = true
call = { workspace = true, features = ["test-support"] }

View File

@@ -6486,6 +6486,8 @@ async fn test_context_collaboration_with_reconnect(
assert_eq!(project.collaborators().len(), 1);
});
cx_a.update(context_servers::init);
cx_b.update(context_servers::init);
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context_store_a = cx_a
.update(|cx| {

View File

@@ -39,11 +39,13 @@ impl CommandPaletteFilter {
}
/// Updates the global [`CommandPaletteFilter`] using the given closure.
pub fn update_global<F, R>(cx: &mut AppContext, update: F) -> R
pub fn update_global<F>(cx: &mut AppContext, update: F)
where
F: FnOnce(&mut Self, &mut AppContext) -> R,
F: FnOnce(&mut Self, &mut AppContext),
{
cx.update_global(|this: &mut GlobalCommandPaletteFilter, cx| update(&mut this.0, cx))
if cx.has_global::<GlobalCommandPaletteFilter>() {
cx.update_global(|this: &mut GlobalCommandPaletteFilter, cx| update(&mut this.0, cx))
}
}
/// Returns whether the given [`Action`] is hidden by the filter.

View File

@@ -13,7 +13,6 @@ path = "src/context_servers.rs"
[dependencies]
anyhow.workspace = true
async-trait.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
futures.workspace = true

View File

@@ -8,7 +8,6 @@ use command_palette_hooks::CommandPaletteFilter;
use gpui::{actions, AppContext};
use settings::Settings;
pub use crate::manager::ContextServer;
use crate::manager::ContextServerSettings;
pub use crate::registry::ContextServerFactoryRegistry;
@@ -19,7 +18,7 @@ pub const CONTEXT_SERVERS_NAMESPACE: &'static str = "context_servers";
pub fn init(cx: &mut AppContext) {
ContextServerSettings::register(cx);
ContextServerFactoryRegistry::global(cx);
ContextServerFactoryRegistry::default_global(cx);
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_namespace(CONTEXT_SERVERS_NAMESPACE);

View File

@@ -15,25 +15,23 @@
//! and react to changes in settings.
use std::path::Path;
use std::pin::Pin;
use std::sync::Arc;
use anyhow::{bail, Result};
use async_trait::async_trait;
use collections::{HashMap, HashSet};
use futures::{channel::mpsc, Future, FutureExt};
use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, Task};
use collections::HashMap;
use command_palette_hooks::CommandPaletteFilter;
use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, Subscription, Task, WeakModel};
use log;
use parking_lot::RwLock;
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources, SettingsStore};
use smol::stream::StreamExt;
use util::ResultExt as _;
use crate::{
client::{self, Client},
types, ContextServerFactoryRegistry,
types, ContextServerFactoryRegistry, CONTEXT_SERVERS_NAMESPACE,
};
#[derive(Deserialize, Serialize, Default, Clone, PartialEq, Eq, JsonSchema, Debug)]
@@ -68,25 +66,13 @@ impl Settings for ContextServerSettings {
}
}
#[async_trait(?Send)]
pub trait ContextServer: Send + Sync + 'static {
fn id(&self) -> Arc<str>;
fn config(&self) -> Arc<ServerConfig>;
fn client(&self) -> Option<Arc<crate::protocol::InitializedContextServerProtocol>>;
fn start<'a>(
self: Arc<Self>,
cx: &'a AsyncAppContext,
) -> Pin<Box<dyn 'a + Future<Output = Result<()>>>>;
fn stop(&self) -> Result<()>;
}
pub struct NativeContextServer {
pub struct ContextServer {
pub id: Arc<str>,
pub config: Arc<ServerConfig>,
pub client: RwLock<Option<Arc<crate::protocol::InitializedContextServerProtocol>>>,
}
impl NativeContextServer {
impl ContextServer {
pub fn new(id: Arc<str>, config: Arc<ServerConfig>) -> Self {
Self {
id,
@@ -94,61 +80,52 @@ impl NativeContextServer {
client: RwLock::new(None),
}
}
}
#[async_trait(?Send)]
impl ContextServer for NativeContextServer {
fn id(&self) -> Arc<str> {
pub fn id(&self) -> Arc<str> {
self.id.clone()
}
fn config(&self) -> Arc<ServerConfig> {
pub fn config(&self) -> Arc<ServerConfig> {
self.config.clone()
}
fn client(&self) -> Option<Arc<crate::protocol::InitializedContextServerProtocol>> {
pub fn client(&self) -> Option<Arc<crate::protocol::InitializedContextServerProtocol>> {
self.client.read().clone()
}
fn start<'a>(
self: Arc<Self>,
cx: &'a AsyncAppContext,
) -> Pin<Box<dyn 'a + Future<Output = Result<()>>>> {
async move {
log::info!("starting context server {}", self.id);
let Some(command) = &self.config.command else {
bail!("no command specified for server {}", self.id);
};
let client = Client::new(
client::ContextServerId(self.id.clone()),
client::ModelContextServerBinary {
executable: Path::new(&command.path).to_path_buf(),
args: command.args.clone(),
env: command.env.clone(),
},
cx.clone(),
)?;
pub async fn start(self: Arc<Self>, cx: &AsyncAppContext) -> Result<()> {
log::info!("starting context server {}", self.id);
let Some(command) = &self.config.command else {
bail!("no command specified for server {}", self.id);
};
let client = Client::new(
client::ContextServerId(self.id.clone()),
client::ModelContextServerBinary {
executable: Path::new(&command.path).to_path_buf(),
args: command.args.clone(),
env: command.env.clone(),
},
cx.clone(),
)?;
let protocol = crate::protocol::ModelContextProtocol::new(client);
let client_info = types::Implementation {
name: "Zed".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
};
let initialized_protocol = protocol.initialize(client_info).await?;
let protocol = crate::protocol::ModelContextProtocol::new(client);
let client_info = types::Implementation {
name: "Zed".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
};
let initialized_protocol = protocol.initialize(client_info).await?;
log::debug!(
"context server {} initialized: {:?}",
self.id,
initialized_protocol.initialize,
);
log::debug!(
"context server {} initialized: {:?}",
self.id,
initialized_protocol.initialize,
);
*self.client.write() = Some(Arc::new(initialized_protocol));
Ok(())
}
.boxed_local()
*self.client.write() = Some(Arc::new(initialized_protocol));
Ok(())
}
fn stop(&self) -> Result<()> {
pub fn stop(&self) -> Result<()> {
let mut client = self.client.write();
if let Some(protocol) = client.take() {
drop(protocol);
@@ -157,17 +134,13 @@ impl ContextServer for NativeContextServer {
}
}
/// A Context server manager manages the starting and stopping
/// of all servers. To obtain a server to interact with, a crate
/// must go through the `GlobalContextServerManager` which holds
/// a model to the ContextServerManager.
pub struct ContextServerManager {
servers: HashMap<Arc<str>, Arc<ContextServer>>,
project: Model<Project>,
servers: HashMap<Arc<str>, (ServerCommand, Arc<dyn ContextServer>)>,
pending_servers: HashSet<Arc<str>>,
registry: Model<ContextServerFactoryRegistry>,
_maintain_context_servers: Task<Result<()>>,
_subscriptions: [gpui::Subscription; 2],
update_servers_task: Option<Task<Result<()>>>,
needs_server_update: bool,
_subscriptions: Vec<Subscription>,
}
pub enum Event {
@@ -179,95 +152,64 @@ impl EventEmitter<Event> for ContextServerManager {}
impl ContextServerManager {
pub fn new(
project: Model<Project>,
registry: Model<ContextServerFactoryRegistry>,
project: Model<Project>,
cx: &mut ModelContext<Self>,
) -> Self {
let (tx, mut rx) = mpsc::unbounded::<()>();
Self {
project,
servers: Default::default(),
pending_servers: HashSet::default(),
_subscriptions: [
cx.observe(&registry, {
let tx = tx.clone();
move |_this, _, _cx| {
tx.unbounded_send(()).ok();
}
let mut this = Self {
_subscriptions: vec![
cx.observe(&registry, |this, _registry, cx| {
this.available_context_servers_changed(cx);
}),
cx.observe_global::<SettingsStore>({
let tx = tx.clone();
move |_this, _cx| {
tx.unbounded_send(()).ok();
}
cx.observe_global::<SettingsStore>(|this, cx| {
this.available_context_servers_changed(cx);
}),
],
project,
registry,
_maintain_context_servers: cx.spawn(|this, mut cx| async move {
while let Some(_) = rx.next().await {
this.update(&mut cx, |this, cx| {
this.registered_servers_changed(cx);
})?;
}
Ok(())
}),
}
}
pub fn add_server(
&mut self,
server: Arc<dyn ContextServer>,
cx: &ModelContext<Self>,
) -> Task<anyhow::Result<()>> {
let server_id = server.id();
if self.servers.contains_key(&server_id) || self.pending_servers.contains(&server_id) {
return Task::ready(Ok(()));
}
let task = {
let server_id = server_id.clone();
cx.spawn(|this, mut cx| async move {
server.clone().start(&cx).await?;
this.update(&mut cx, |this, cx| {
this.servers.insert(server_id.clone(), server);
this.pending_servers.remove(&server_id);
cx.emit(Event::ServerStarted {
server_id: server_id.clone(),
});
})?;
Ok(())
})
needs_server_update: false,
servers: HashMap::default(),
update_servers_task: None,
};
self.pending_servers.insert(server_id);
task
this.available_context_servers_changed(cx);
this
}
pub fn get_server(&self, id: &str) -> Option<Arc<dyn ContextServer>> {
self.servers.get(id).cloned()
fn available_context_servers_changed(&mut self, cx: &mut ModelContext<Self>) {
if self.update_servers_task.is_some() {
self.needs_server_update = true;
} else {
self.update_servers_task = Some(cx.spawn(|this, mut cx| async move {
this.update(&mut cx, |this, _| {
this.needs_server_update = false;
})?;
Self::maintain_servers(this.clone(), cx.clone()).await?;
this.update(&mut cx, |this, cx| {
let has_any_context_servers = !this.servers().is_empty();
if has_any_context_servers {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.show_namespace(CONTEXT_SERVERS_NAMESPACE);
});
}
this.update_servers_task.take();
if this.needs_server_update {
this.available_context_servers_changed(cx);
}
})?;
Ok(())
}));
}
}
pub fn remove_server(
&mut self,
id: &Arc<str>,
cx: &ModelContext<Self>,
) -> Task<anyhow::Result<()>> {
let id = id.clone();
cx.spawn(|this, mut cx| async move {
if let Some(server) =
this.update(&mut cx, |this, _cx| this.servers.remove(id.as_ref()))?
{
server.stop()?;
}
this.update(&mut cx, |this, cx| {
this.pending_servers.remove(id.as_ref());
cx.emit(Event::ServerStopped {
server_id: id.clone(),
})
})?;
Ok(())
})
pub fn get_server(&self, id: &str) -> Option<Arc<ContextServer>> {
self.servers
.get(id)
.filter(|server| server.client().is_some())
.cloned()
}
pub fn restart_server(
@@ -280,7 +222,7 @@ impl ContextServerManager {
if let Some(server) = this.update(&mut cx, |this, _cx| this.servers.remove(&id))? {
server.stop()?;
let config = server.config();
let new_server = Arc::new(NativeContextServer::new(id.clone(), config));
let new_server = Arc::new(ContextServer::new(id.clone(), config));
new_server.clone().start(&cx).await?;
this.update(&mut cx, |this, cx| {
this.servers.insert(id.clone(), new_server);
@@ -296,84 +238,83 @@ impl ContextServerManager {
})
}
pub fn servers(&self) -> Vec<Arc<dyn ContextServer>> {
self.servers.values().cloned().collect()
}
fn registered_servers_changed(
&mut self,
cx: &mut ModelContext<ContextServerManager>,
) -> Task<()> {
let worktree_id = self
.project
.read(cx)
.visible_worktrees(cx)
.next()
.map(|worktree| worktree.read(cx).id());
let settings = ContextServerSettings::get(
worktree_id.map(|worktree_id| settings::SettingsLocation {
worktree_id,
path: Path::new(""),
}),
cx,
);
let registry = self.registry.read(cx);
let mut settings_iter = settings.context_servers.iter().peekable();
let mut registry_iter = registry.context_servers.iter().peekable();
// loop {
// let mut setting_command = None;
// let mut registered_command = None;
// let mut server_id;
// match (settings_iter.peek(), registry_iter.peek()) {
// (None, None) => break,
// (None, Some((id, value))) => {
// server_id = id.clone();
// registered_command = value;
// }
// (Some(_), None) => continue,
// (Some(_), Some(_)) => continue,
// }
// }
}
pub fn maintain_servers(&mut self, settings: &ContextServerSettings, cx: &ModelContext<Self>) {
let current_servers = self
.servers()
.into_iter()
.map(|server| (server.id(), server.config()))
.collect::<HashMap<_, _>>();
let new_servers = settings
.context_servers
.iter()
.map(|(id, config)| (id.clone(), config.clone()))
.collect::<HashMap<_, _>>();
let servers_to_add = new_servers
.iter()
.filter(|(id, _)| !current_servers.contains_key(id.as_ref()))
.map(|(id, config)| (id.clone(), config.clone()))
.collect::<Vec<_>>();
let servers_to_remove = current_servers
.keys()
.filter(|id| !new_servers.contains_key(id.as_ref()))
pub fn servers(&self) -> Vec<Arc<ContextServer>> {
self.servers
.values()
.filter(|server| server.client().is_some())
.cloned()
.collect::<Vec<_>>();
.collect()
}
log::trace!("servers_to_add={:?}", servers_to_add);
for (id, config) in servers_to_add {
if config.command.is_some() {
let server = Arc::new(NativeContextServer::new(id, Arc::new(config)));
self.add_server(server, cx).detach_and_log_err(cx);
async fn maintain_servers(this: WeakModel<Self>, mut cx: AsyncAppContext) -> Result<()> {
let mut desired_servers = HashMap::default();
let (registry, project) = this.update(&mut cx, |this, cx| {
let location = this.project.read(cx).worktrees(cx).next().map(|worktree| {
settings::SettingsLocation {
worktree_id: worktree.read(cx).id(),
path: Path::new(""),
}
});
let settings = ContextServerSettings::get(location, cx);
desired_servers = settings.context_servers.clone();
(this.registry.clone(), this.project.clone())
})?;
for (id, factory) in
registry.read_with(&cx, |registry, _| registry.context_server_factories())?
{
let config = desired_servers.entry(id).or_default();
if config.command.is_none() {
if let Some(extension_command) = factory(project.clone(), &cx).await.log_err() {
config.command = Some(extension_command);
}
}
}
for id in servers_to_remove {
self.remove_server(&id, cx).detach_and_log_err(cx);
let mut servers_to_start = HashMap::default();
let mut servers_to_stop = HashMap::default();
this.update(&mut cx, |this, _cx| {
this.servers.retain(|id, server| {
if desired_servers.contains_key(id) {
true
} else {
servers_to_stop.insert(id.clone(), server.clone());
false
}
});
for (id, config) in desired_servers {
let existing_config = this.servers.get(&id).map(|server| server.config());
if existing_config.as_deref() != Some(&config) {
let config = Arc::new(config);
let server = Arc::new(ContextServer::new(id.clone(), config));
servers_to_start.insert(id.clone(), server.clone());
let old_server = this.servers.insert(id.clone(), server);
if let Some(old_server) = old_server {
servers_to_stop.insert(id, old_server);
}
}
}
})?;
for (id, server) in servers_to_stop {
server.stop().log_err();
this.update(&mut cx, |_, cx| {
cx.emit(Event::ServerStopped { server_id: id })
})?;
}
for (id, server) in servers_to_start {
if server.start(&cx).await.log_err().is_some() {
this.update(&mut cx, |_, cx| {
cx.emit(Event::ServerStarted { server_id: id })
})?;
}
}
Ok(())
}
}

View File

@@ -1,18 +1,14 @@
use std::sync::Arc;
use anyhow::Result;
use collections::BTreeMap;
use futures::future::BoxFuture;
use collections::HashMap;
use gpui::{AppContext, AsyncAppContext, Context, Global, Model, ReadGlobal, Task};
use project::Project;
use crate::manager::ServerCommand;
pub type ContextServerFactory = Arc<
dyn Fn(Model<Project>, &AsyncAppContext) -> BoxFuture<Result<ServerCommand>>
+ Send
+ Sync
+ 'static,
dyn Fn(Model<Project>, &AsyncAppContext) -> Task<Result<ServerCommand>> + Send + Sync + 'static,
>;
struct GlobalContextServerFactoryRegistry(Model<ContextServerFactoryRegistry>);
@@ -21,22 +17,29 @@ impl Global for GlobalContextServerFactoryRegistry {}
#[derive(Default)]
pub struct ContextServerFactoryRegistry {
pub context_servers: BTreeMap<Arc<str>, ContextServerFactory>,
context_servers: HashMap<Arc<str>, ContextServerFactory>,
}
impl ContextServerFactoryRegistry {
/// Returns the global [`ContextServerFactoryRegistry`].
pub fn global(cx: &mut AppContext) -> Model<Self> {
pub fn global(cx: &AppContext) -> Model<Self> {
GlobalContextServerFactoryRegistry::global(cx).0.clone()
}
/// Returns the global [`ContextServerFactoryRegistry`].
///
/// Inserts a default [`ContextServerFactoryRegistry`] if one does not yet exist.
pub fn default_global(cx: &mut AppContext) -> Model<Self> {
if !cx.has_global::<GlobalContextServerFactoryRegistry>() {
let registry = cx.new_model(|_| ContextServerFactoryRegistry::new());
let registry = cx.new_model(|_| Self::new());
cx.set_global(GlobalContextServerFactoryRegistry(registry));
}
GlobalContextServerFactoryRegistry::global(cx).0.clone()
cx.global::<GlobalContextServerFactoryRegistry>().0.clone()
}
pub fn new() -> Self {
Self {
context_servers: Default::default(),
context_servers: HashMap::default(),
}
}

View File

@@ -14,7 +14,7 @@ use gpui::{
actions, AppContext, AsyncAppContext, Context, Entity, EntityId, EventEmitter, Global, Model,
ModelContext, Task, WeakModel,
};
use http_client::github::latest_github_release;
use http_client::github::get_release_by_tag_name;
use http_client::HttpClient;
use language::{
language_settings::{all_language_settings, language_settings, InlineCompletionProvider},
@@ -989,12 +989,12 @@ async fn clear_copilot_dir() {
}
async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
const SERVER_PATH: &str = "dist/agent.js";
const SERVER_PATH: &str = "dist/language-server.js";
///Check for the latest copilot language server and download it if we haven't already
async fn fetch_latest(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
let release =
latest_github_release("zed-industries/copilot", true, false, http.clone()).await?;
get_release_by_tag_name("zed-industries/copilot", "v0.7.0", http.clone()).await?;
let version_dir = &paths::copilot_dir().join(format!("copilot-{}", release.tag_name));

View File

@@ -32,6 +32,7 @@ use std::{
cmp::Ordering,
mem,
ops::Range,
sync::Arc,
};
use theme::ActiveTheme;
pub use toolbar_controls::ToolbarControls;
@@ -790,10 +791,11 @@ const DIAGNOSTIC_HEADER: &str = "diagnostic header";
fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
let (message, code_ranges) = highlight_diagnostic_message(&diagnostic, None);
let message: SharedString = message;
Box::new(move |cx| {
Arc::new(move |cx| {
let highlight_style: HighlightStyle = cx.theme().colors().text_accent.into();
h_flex()
.id(DIAGNOSTIC_HEADER)
.block_mouse_down()
.h(2. * cx.line_height())
.pl_10()
.pr_5()

View File

@@ -36,7 +36,7 @@ use block_map::{BlockRow, BlockSnapshot};
use collections::{HashMap, HashSet};
pub use crease_map::*;
pub use fold_map::{Fold, FoldId, FoldPlaceholder, FoldPoint};
use fold_map::{FoldMap, FoldMapWriter, FoldOffset, FoldSnapshot};
use fold_map::{FoldMap, FoldSnapshot};
use gpui::{
AnyElement, Font, HighlightStyle, LineLayout, Model, ModelContext, Pixels, UnderlineStyle,
};
@@ -65,7 +65,7 @@ use std::{
};
use sum_tree::{Bias, TreeMap};
use tab_map::{TabMap, TabSnapshot};
use text::{Edit, LineIndent};
use text::LineIndent;
use ui::{div, px, IntoElement, ParentElement, SharedString, Styled, WindowContext};
use unicode_segmentation::UnicodeSegmentation;
use wrap_map::{WrapMap, WrapSnapshot};
@@ -197,22 +197,86 @@ impl DisplayMap {
other
.folds_in_range(0..other.buffer_snapshot.len())
.map(|fold| {
(
Crease::simple(
fold.range.to_offset(&other.buffer_snapshot),
fold.placeholder.clone(),
)
}),
})
.collect(),
cx,
);
}
/// Creates folds for the given ranges.
pub fn fold<T: ToOffset>(
/// Creates folds for the given creases.
pub fn fold<T: Clone + ToOffset>(
&mut self,
ranges: impl IntoIterator<Item = (Range<T>, FoldPlaceholder)>,
creases: Vec<Crease<T>>,
cx: &mut ModelContext<Self>,
) {
self.update_fold_map(cx, |fold_map| fold_map.fold(ranges))
let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.inlay_map.sync(buffer_snapshot.clone(), edits);
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
self.block_map.read(snapshot, edits);
let inline = creases.iter().filter_map(|crease| {
if let Crease::Inline {
range, placeholder, ..
} = crease
{
Some((range.clone(), placeholder.clone()))
} else {
None
}
});
let (snapshot, edits) = fold_map.fold(inline);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
let mut block_map = self.block_map.write(snapshot, edits);
let blocks = creases.into_iter().filter_map(|crease| {
if let Crease::Block {
range,
block_height,
render_block,
block_style,
block_priority,
..
} = crease
{
Some((
range,
render_block,
block_height,
block_style,
block_priority,
))
} else {
None
}
});
block_map.insert(
blocks
.into_iter()
.map(|(range, render, height, style, priority)| {
let start = buffer_snapshot.anchor_before(range.start);
let end = buffer_snapshot.anchor_after(range.end);
BlockProperties {
placement: BlockPlacement::Replace(start..end),
render,
height,
style,
priority,
}
}),
);
}
/// Removes any folds with the given ranges.
@@ -221,26 +285,6 @@ impl DisplayMap {
ranges: impl IntoIterator<Item = Range<T>>,
type_id: TypeId,
cx: &mut ModelContext<Self>,
) {
self.update_fold_map(cx, |fold_map| fold_map.remove_folds(ranges, type_id))
}
/// Removes any folds whose ranges intersect any of the given ranges.
pub fn unfold_intersecting<T: ToOffset>(
&mut self,
ranges: impl IntoIterator<Item = Range<T>>,
inclusive: bool,
cx: &mut ModelContext<Self>,
) {
self.update_fold_map(cx, |fold_map| {
fold_map.unfold_intersecting(ranges, inclusive)
})
}
fn update_fold_map(
&mut self,
cx: &mut ModelContext<Self>,
callback: impl FnOnce(&mut FoldMapWriter) -> (FoldSnapshot, Vec<Edit<FoldOffset>>),
) {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
@@ -252,17 +296,49 @@ impl DisplayMap {
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
self.block_map.read(snapshot, edits);
let (snapshot, edits) = callback(&mut fold_map);
let (snapshot, edits) = fold_map.remove_folds(ranges, type_id);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
self.block_map.write(snapshot, edits);
}
/// Removes any folds whose ranges intersect any of the given ranges.
pub fn unfold_intersecting<T: ToOffset>(
&mut self,
ranges: impl IntoIterator<Item = Range<T>>,
inclusive: bool,
cx: &mut ModelContext<Self>,
) {
let snapshot = self.buffer.read(cx).snapshot(cx);
let offset_ranges = ranges
.into_iter()
.map(|range| range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot))
.collect::<Vec<_>>();
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
self.block_map.read(snapshot, edits);
let (snapshot, edits) =
fold_map.unfold_intersecting(offset_ranges.iter().cloned(), inclusive);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
let mut block_map = self.block_map.write(snapshot, edits);
block_map.remove_intersecting_replace_blocks(offset_ranges, inclusive);
}
pub fn insert_creases(
&mut self,
creases: impl IntoIterator<Item = Crease>,
creases: impl IntoIterator<Item = Crease<Anchor>>,
cx: &mut ModelContext<Self>,
) -> Vec<CreaseId> {
let snapshot = self.buffer.read(cx).snapshot(cx);
@@ -596,7 +672,7 @@ impl DisplaySnapshot {
) -> impl Iterator<Item = Option<MultiBufferRow>> + '_ {
self.block_snapshot
.buffer_rows(BlockRow(start_row.0))
.map(|row| row.map(|row| MultiBufferRow(row.0)))
.map(|row| row.map(MultiBufferRow))
}
pub fn max_buffer_row(&self) -> MultiBufferRow {
@@ -987,7 +1063,12 @@ impl DisplaySnapshot {
}
pub fn is_line_folded(&self, buffer_row: MultiBufferRow) -> bool {
self.fold_snapshot.is_line_folded(buffer_row)
self.block_snapshot.is_line_replaced(buffer_row)
|| self.fold_snapshot.is_line_folded(buffer_row)
}
pub fn is_line_replaced(&self, buffer_row: MultiBufferRow) -> bool {
self.block_snapshot.is_line_replaced(buffer_row)
}
pub fn is_block_line(&self, display_row: DisplayRow) -> bool {
@@ -1061,19 +1142,42 @@ impl DisplaySnapshot {
.unwrap_or(false)
}
pub fn foldable_range(
&self,
buffer_row: MultiBufferRow,
) -> Option<(Range<Point>, FoldPlaceholder)> {
pub fn crease_for_buffer_row(&self, buffer_row: MultiBufferRow) -> Option<Crease<Point>> {
let start = MultiBufferPoint::new(buffer_row.0, self.buffer_snapshot.line_len(buffer_row));
if let Some(crease) = self
.crease_snapshot
.query_row(buffer_row, &self.buffer_snapshot)
{
Some((
crease.range.to_point(&self.buffer_snapshot),
crease.placeholder.clone(),
))
match crease {
Crease::Inline {
range,
placeholder,
render_toggle,
render_trailer,
metadata,
} => Some(Crease::Inline {
range: range.to_point(&self.buffer_snapshot),
placeholder: placeholder.clone(),
render_toggle: render_toggle.clone(),
render_trailer: render_trailer.clone(),
metadata: metadata.clone(),
}),
Crease::Block {
range,
block_height,
block_style,
render_block,
block_priority,
render_toggle,
} => Some(Crease::Block {
range: range.to_point(&self.buffer_snapshot),
block_height: *block_height,
block_style: *block_style,
render_block: render_block.clone(),
block_priority: *block_priority,
render_toggle: render_toggle.clone(),
}),
}
} else if self.starts_indent(MultiBufferRow(start.row))
&& !self.is_line_folded(MultiBufferRow(start.row))
{
@@ -1110,7 +1214,13 @@ impl DisplaySnapshot {
.line_len(MultiBufferRow(row_before_line_breaks.row)),
);
Some((start..row_before_line_breaks, self.fold_placeholder.clone()))
Some(Crease::Inline {
range: start..row_before_line_breaks,
placeholder: self.fold_placeholder.clone(),
render_toggle: None,
render_trailer: None,
metadata: None,
})
} else {
None
}
@@ -1418,7 +1528,7 @@ pub mod tests {
placement,
style: BlockStyle::Fixed,
height,
render: Box::new(|_| div().into_any()),
render: Arc::new(|_| div().into_any()),
priority,
}
})
@@ -1457,7 +1567,8 @@ pub mod tests {
map.fold(
ranges
.into_iter()
.map(|range| (range, FoldPlaceholder::test())),
.map(|range| Crease::simple(range, FoldPlaceholder::test()))
.collect(),
cx,
);
});
@@ -1832,7 +1943,7 @@ pub mod tests {
map.update(cx, |map, cx| {
map.fold(
vec![(
vec![Crease::simple(
MultiBufferPoint::new(0, 6)..MultiBufferPoint::new(3, 2),
FoldPlaceholder::test(),
)],
@@ -1922,7 +2033,7 @@ pub mod tests {
),
height: 1,
style: BlockStyle::Sticky,
render: Box::new(|_| div().into_any()),
render: Arc::new(|_| div().into_any()),
priority: 0,
}],
cx,
@@ -2028,7 +2139,7 @@ pub mod tests {
),
height: 1,
style: BlockStyle::Sticky,
render: Box::new(|_| div().into_any()),
render: Arc::new(|_| div().into_any()),
priority: 0,
}],
cx,
@@ -2104,7 +2215,7 @@ pub mod tests {
),
height: 4,
style: BlockStyle::Fixed,
render: Box::new(|_| div().into_any()),
render: Arc::new(|_| div().into_any()),
priority: 0,
}],
cx,
@@ -2253,7 +2364,7 @@ pub mod tests {
map.update(cx, |map, cx| {
map.fold(
vec![(
vec![Crease::simple(
MultiBufferPoint::new(0, 6)..MultiBufferPoint::new(3, 2),
FoldPlaceholder::test(),
)],
@@ -2452,7 +2563,7 @@ pub mod tests {
snapshot.anchor_before(Point::new(2, 0))..snapshot.anchor_after(Point::new(3, 3));
map.crease_map.insert(
[Crease::new(
[Crease::inline(
range,
FoldPlaceholder::test(),
|_row, _status, _toggle, _cx| div(),

View File

@@ -7,7 +7,7 @@ use collections::{Bound, HashMap, HashSet};
use gpui::{AnyElement, EntityId, Pixels, WindowContext};
use language::{Chunk, Patch, Point};
use multi_buffer::{
Anchor, ExcerptId, ExcerptInfo, MultiBufferRow, MultiBufferSnapshot, ToPoint as _,
Anchor, ExcerptId, ExcerptInfo, MultiBufferRow, MultiBufferSnapshot, ToOffset, ToPoint as _,
};
use parking_lot::Mutex;
use std::{
@@ -77,7 +77,7 @@ pub struct BlockRow(pub(super) u32);
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
struct WrapRow(u32);
pub type RenderBlock = Box<dyn Send + FnMut(&mut BlockContext) -> AnyElement>;
pub type RenderBlock = Arc<dyn Send + Sync + Fn(&mut BlockContext) -> AnyElement>;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum BlockPlacement<T> {
@@ -352,6 +352,13 @@ impl Block {
Block::ExcerptBoundary { next_excerpt, .. } => next_excerpt.is_none(),
}
}
fn is_replacement(&self) -> bool {
match self {
Block::Custom(block) => matches!(block.placement, BlockPlacement::Replace(_)),
Block::ExcerptBoundary { .. } => false,
}
}
}
impl Debug for Block {
@@ -1119,6 +1126,64 @@ impl<'a> BlockMapWriter<'a> {
.retain(|id, _| !block_ids.contains(id));
self.0.sync(wrap_snapshot, edits);
}
pub fn remove_intersecting_replace_blocks<T>(
&mut self,
ranges: impl IntoIterator<Item = Range<T>>,
inclusive: bool,
) where
T: ToOffset,
{
let wrap_snapshot = self.0.wrap_snapshot.borrow();
let mut blocks_to_remove = HashSet::default();
for range in ranges {
let range = range.start.to_offset(wrap_snapshot.buffer_snapshot())
..range.end.to_offset(wrap_snapshot.buffer_snapshot());
for block in self.blocks_intersecting_buffer_range(range, inclusive) {
if matches!(block.placement, BlockPlacement::Replace(_)) {
blocks_to_remove.insert(block.id);
}
}
}
drop(wrap_snapshot);
self.remove(blocks_to_remove);
}
fn blocks_intersecting_buffer_range(
&self,
range: Range<usize>,
inclusive: bool,
) -> &[Arc<CustomBlock>] {
let wrap_snapshot = self.0.wrap_snapshot.borrow();
let buffer = wrap_snapshot.buffer_snapshot();
let start_block_ix = match self.0.custom_blocks.binary_search_by(|probe| {
probe
.end()
.to_offset(buffer)
.cmp(&range.start)
.then(if inclusive {
Ordering::Greater
} else {
Ordering::Less
})
}) {
Ok(ix) | Err(ix) => ix,
};
let end_block_ix = match self.0.custom_blocks.binary_search_by(|probe| {
probe
.start()
.to_offset(buffer)
.cmp(&range.end)
.then(if inclusive {
Ordering::Less
} else {
Ordering::Greater
})
}) {
Ok(ix) | Err(ix) => ix,
};
&self.0.custom_blocks[start_block_ix..end_block_ix]
}
}
impl BlockSnapshot {
@@ -1298,6 +1363,21 @@ impl BlockSnapshot {
cursor.item().map_or(false, |t| t.block.is_some())
}
pub(super) fn is_line_replaced(&self, row: MultiBufferRow) -> bool {
let wrap_point = self
.wrap_snapshot
.make_wrap_point(Point::new(row.0, 0), Bias::Left);
let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>(&());
cursor.seek(&WrapRow(wrap_point.row()), Bias::Right, &());
cursor.item().map_or(false, |transform| {
if let Some(Block::Custom(block)) = transform.block.as_ref() {
matches!(block.placement, BlockPlacement::Replace(_))
} else {
false
}
})
}
pub fn clip_point(&self, point: BlockPoint, bias: Bias) -> BlockPoint {
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());
cursor.seek(&BlockRow(point.row), Bias::Right, &());
@@ -1515,7 +1595,7 @@ impl<'a> Iterator for BlockChunks<'a> {
}
impl<'a> Iterator for BlockBufferRows<'a> {
type Item = Option<BlockRow>;
type Item = Option<u32>;
fn next(&mut self) -> Option<Self::Item> {
if self.started {
@@ -1538,16 +1618,25 @@ impl<'a> Iterator for BlockBufferRows<'a> {
}
}
if self.transforms.item()?.block.is_none() {
let transform = self.transforms.item()?;
if transform
.block
.as_ref()
.map_or(true, |block| block.is_replacement())
{
self.input_buffer_rows.seek(self.transforms.start().1 .0);
}
}
let transform = self.transforms.item()?;
if transform.block.is_some() {
Some(None)
if let Some(block) = transform.block.as_ref() {
if block.is_replacement() && self.transforms.start().0 == self.output_row {
Some(self.input_buffer_rows.next().unwrap())
} else {
Some(None)
}
} else {
Some(self.input_buffer_rows.next().unwrap().map(BlockRow))
Some(self.input_buffer_rows.next().unwrap())
}
}
}
@@ -1709,21 +1798,21 @@ mod tests {
style: BlockStyle::Fixed,
placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(1, 0))),
height: 1,
render: Box::new(|_| div().into_any()),
render: Arc::new(|_| div().into_any()),
priority: 0,
},
BlockProperties {
style: BlockStyle::Fixed,
placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(1, 2))),
height: 2,
render: Box::new(|_| div().into_any()),
render: Arc::new(|_| div().into_any()),
priority: 0,
},
BlockProperties {
style: BlockStyle::Fixed,
placement: BlockPlacement::Below(buffer_snapshot.anchor_after(Point::new(3, 3))),
height: 3,
render: Box::new(|_| div().into_any()),
render: Arc::new(|_| div().into_any()),
priority: 0,
},
]);
@@ -1821,10 +1910,7 @@ mod tests {
);
assert_eq!(
snapshot
.buffer_rows(BlockRow(0))
.map(|row| row.map(|r| r.0))
.collect::<Vec<_>>(),
snapshot.buffer_rows(BlockRow(0)).collect::<Vec<_>>(),
&[
Some(0),
None,
@@ -1960,21 +2046,21 @@ mod tests {
style: BlockStyle::Fixed,
placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(1, 0))),
height: 1,
render: Box::new(|_| div().into_any()),
render: Arc::new(|_| div().into_any()),
priority: 0,
},
BlockProperties {
style: BlockStyle::Fixed,
placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(1, 2))),
height: 2,
render: Box::new(|_| div().into_any()),
render: Arc::new(|_| div().into_any()),
priority: 0,
},
BlockProperties {
style: BlockStyle::Fixed,
placement: BlockPlacement::Below(buffer_snapshot.anchor_after(Point::new(3, 3))),
height: 3,
render: Box::new(|_| div().into_any()),
render: Arc::new(|_| div().into_any()),
priority: 0,
},
]);
@@ -2062,14 +2148,14 @@ mod tests {
BlockProperties {
style: BlockStyle::Fixed,
placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(1, 12))),
render: Box::new(|_| div().into_any()),
render: Arc::new(|_| div().into_any()),
height: 1,
priority: 0,
},
BlockProperties {
style: BlockStyle::Fixed,
placement: BlockPlacement::Below(buffer_snapshot.anchor_after(Point::new(1, 1))),
render: Box::new(|_| div().into_any()),
render: Arc::new(|_| div().into_any()),
height: 1,
priority: 0,
},
@@ -2109,7 +2195,7 @@ mod tests {
..buffer_snapshot.anchor_before(Point::new(3, 1)),
),
height: 4,
render: Box::new(|_| div().into_any()),
render: Arc::new(|_| div().into_any()),
priority: 0,
}]);
@@ -2162,14 +2248,14 @@ mod tests {
style: BlockStyle::Fixed,
placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(1, 3))),
height: 1,
render: Box::new(|_| div().into_any()),
render: Arc::new(|_| div().into_any()),
priority: 0,
},
BlockProperties {
style: BlockStyle::Fixed,
placement: BlockPlacement::Below(buffer_snapshot.anchor_after(Point::new(6, 2))),
height: 1,
render: Box::new(|_| div().into_any()),
render: Arc::new(|_| div().into_any()),
priority: 0,
},
]);
@@ -2183,21 +2269,21 @@ mod tests {
style: BlockStyle::Fixed,
placement: BlockPlacement::Below(buffer_snapshot.anchor_after(Point::new(1, 3))),
height: 1,
render: Box::new(|_| div().into_any()),
render: Arc::new(|_| div().into_any()),
priority: 0,
},
BlockProperties {
style: BlockStyle::Fixed,
placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(2, 1))),
height: 1,
render: Box::new(|_| div().into_any()),
render: Arc::new(|_| div().into_any()),
priority: 0,
},
BlockProperties {
style: BlockStyle::Fixed,
placement: BlockPlacement::Above(buffer_snapshot.anchor_after(Point::new(6, 1))),
height: 1,
render: Box::new(|_| div().into_any()),
render: Arc::new(|_| div().into_any()),
priority: 0,
},
]);
@@ -2302,7 +2388,7 @@ mod tests {
style: BlockStyle::Fixed,
placement,
height,
render: Box::new(|_| div().into_any()),
render: Arc::new(|_| div().into_any()),
priority: 0,
}
})
@@ -2321,7 +2407,7 @@ mod tests {
placement: props.placement.clone(),
height: props.height,
style: props.style,
render: Box::new(|_| div().into_any()),
render: Arc::new(|_| div().into_any()),
priority: 0,
}));
}
@@ -2409,6 +2495,7 @@ mod tests {
let mut expected_buffer_rows = Vec::new();
let mut expected_text = String::new();
let mut expected_block_positions = Vec::new();
let mut expected_replaced_buffer_rows = HashSet::default();
let input_text = wraps_snapshot.text();
// Loop over the input lines, creating (N - 1) empty lines for
@@ -2422,6 +2509,9 @@ mod tests {
let mut block_row = 0;
while let Some((wrap_row, input_line)) = input_text_lines.next() {
let wrap_row = wrap_row as u32;
let multibuffer_row = wraps_snapshot
.to_point(WrapPoint::new(wrap_row, 0), Bias::Left)
.row;
// Create empty lines for the above block
while let Some((placement, block)) = sorted_blocks_iter.peek() {
@@ -2451,30 +2541,33 @@ mod tests {
{
if wrap_row >= replace_range.start.0 {
is_in_replace_block = true;
if wrap_row == replace_range.start.0 {
expected_buffer_rows.push(input_buffer_rows[multibuffer_row as usize]);
}
if wrap_row == replace_range.end.0 {
expected_block_positions.push((block_row, block.id()));
if block.height() > 0 {
let text = "\n".repeat((block.height() - 1) as usize);
if block_row > 0 {
expected_text.push('\n');
}
expected_text.push_str(&text);
for _ in 0..block.height() {
expected_buffer_rows.push(None);
}
block_row += block.height();
let text = "\n".repeat((block.height() - 1) as usize);
if block_row > 0 {
expected_text.push('\n');
}
expected_text.push_str(&text);
for _ in 1..block.height() {
expected_buffer_rows.push(None);
}
block_row += block.height();
sorted_blocks_iter.next();
}
}
}
if !is_in_replace_block {
let buffer_row = input_buffer_rows[wraps_snapshot
.to_point(WrapPoint::new(wrap_row, 0), Bias::Left)
.row as usize];
if is_in_replace_block {
expected_replaced_buffer_rows.insert(MultiBufferRow(multibuffer_row));
} else {
let buffer_row = input_buffer_rows[multibuffer_row as usize];
let soft_wrapped = wraps_snapshot
.to_tab_point(WrapPoint::new(wrap_row, 0))
.column()
@@ -2543,9 +2636,10 @@ mod tests {
assert_eq!(
blocks_snapshot
.buffer_rows(BlockRow(start_row as u32))
.map(|row| row.map(|r| r.0))
.collect::<Vec<_>>(),
&expected_buffer_rows[start_row..]
&expected_buffer_rows[start_row..],
"incorrect buffer_rows starting at row {:?}",
start_row
);
}
@@ -2666,6 +2760,16 @@ mod tests {
block_point.column += c.len_utf8() as u32;
}
}
for buffer_row in 0..=buffer_snapshot.max_point().row {
let buffer_row = MultiBufferRow(buffer_row);
assert_eq!(
blocks_snapshot.is_line_replaced(buffer_row),
expected_replaced_buffer_rows.contains(&buffer_row),
"incorrect is_line_replaced({:?})",
buffer_row
);
}
}
}

View File

@@ -2,12 +2,12 @@ use collections::HashMap;
use gpui::{AnyElement, IntoElement};
use multi_buffer::{Anchor, AnchorRangeExt, MultiBufferRow, MultiBufferSnapshot, ToPoint};
use serde::{Deserialize, Serialize};
use std::{cmp::Ordering, ops::Range, sync::Arc};
use std::{cmp::Ordering, fmt::Debug, ops::Range, sync::Arc};
use sum_tree::{Bias, SeekTarget, SumTree};
use text::Point;
use ui::{IconName, SharedString, WindowContext};
use crate::FoldPlaceholder;
use crate::{BlockStyle, FoldPlaceholder, RenderBlock};
#[derive(Copy, Clone, Default, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)]
pub struct CreaseId(usize);
@@ -45,15 +45,15 @@ impl CreaseSnapshot {
&'a self,
row: MultiBufferRow,
snapshot: &'a MultiBufferSnapshot,
) -> Option<&'a Crease> {
) -> Option<&'a Crease<Anchor>> {
let start = snapshot.anchor_before(Point::new(row.0, 0));
let mut cursor = self.creases.cursor::<ItemSummary>(snapshot);
cursor.seek(&start, Bias::Left, snapshot);
while let Some(item) = cursor.item() {
match Ord::cmp(&item.crease.range.start.to_point(snapshot).row, &row.0) {
match Ord::cmp(&item.crease.range().start.to_point(snapshot).row, &row.0) {
Ordering::Less => cursor.next(snapshot),
Ordering::Equal => {
if item.crease.range.start.is_valid(snapshot) {
if item.crease.range().start.is_valid(snapshot) {
return Some(&item.crease);
} else {
cursor.next(snapshot);
@@ -69,7 +69,7 @@ impl CreaseSnapshot {
&'a self,
range: Range<MultiBufferRow>,
snapshot: &'a MultiBufferSnapshot,
) -> impl 'a + Iterator<Item = &'a Crease> {
) -> impl 'a + Iterator<Item = &'a Crease<Anchor>> {
let start = snapshot.anchor_before(Point::new(range.start.0, 0));
let mut cursor = self.creases.cursor::<ItemSummary>(snapshot);
cursor.seek(&start, Bias::Left, snapshot);
@@ -77,8 +77,9 @@ impl CreaseSnapshot {
std::iter::from_fn(move || {
while let Some(item) = cursor.item() {
cursor.next(snapshot);
let crease_start = item.crease.range.start.to_point(snapshot);
let crease_end = item.crease.range.end.to_point(snapshot);
let crease_range = item.crease.range();
let crease_start = crease_range.start.to_point(snapshot);
let crease_end = crease_range.end.to_point(snapshot);
if crease_end.row > range.end.0 {
continue;
}
@@ -99,8 +100,9 @@ impl CreaseSnapshot {
cursor.next(snapshot);
while let Some(item) = cursor.item() {
let start_point = item.crease.range.start.to_point(snapshot);
let end_point = item.crease.range.end.to_point(snapshot);
let crease_range = item.crease.range();
let start_point = crease_range.start.to_point(snapshot);
let end_point = crease_range.end.to_point(snapshot);
results.push((item.id, start_point..end_point));
cursor.next(snapshot);
}
@@ -123,12 +125,22 @@ type RenderTrailerFn =
Arc<dyn Send + Sync + Fn(MultiBufferRow, bool, &mut WindowContext) -> AnyElement>;
#[derive(Clone)]
pub struct Crease {
pub range: Range<Anchor>,
pub placeholder: FoldPlaceholder,
pub render_toggle: RenderToggleFn,
pub render_trailer: RenderTrailerFn,
pub metadata: Option<CreaseMetadata>,
pub enum Crease<T> {
Inline {
range: Range<T>,
placeholder: FoldPlaceholder,
render_toggle: Option<RenderToggleFn>,
render_trailer: Option<RenderTrailerFn>,
metadata: Option<CreaseMetadata>,
},
Block {
range: Range<T>,
block_height: u32,
block_style: BlockStyle,
render_block: RenderBlock,
block_priority: usize,
render_toggle: Option<RenderToggleFn>,
},
}
/// Metadata about a [`Crease`], that is used for serialization.
@@ -138,9 +150,30 @@ pub struct CreaseMetadata {
pub label: SharedString,
}
impl Crease {
pub fn new<RenderToggle, ToggleElement, RenderTrailer, TrailerElement>(
range: Range<Anchor>,
impl<T> Crease<T> {
pub fn simple(range: Range<T>, placeholder: FoldPlaceholder) -> Self {
Crease::Inline {
range,
placeholder,
render_toggle: None,
render_trailer: None,
metadata: None,
}
}
pub fn block(range: Range<T>, height: u32, style: BlockStyle, render: RenderBlock) -> Self {
Self::Block {
range,
block_height: height,
block_style: style,
render_block: render,
block_priority: 0,
render_toggle: None,
}
}
pub fn inline<RenderToggle, ToggleElement, RenderTrailer, TrailerElement>(
range: Range<T>,
placeholder: FoldPlaceholder,
render_toggle: RenderToggle,
render_trailer: RenderTrailer,
@@ -164,37 +197,76 @@ impl Crease {
+ 'static,
TrailerElement: IntoElement,
{
Crease {
Crease::Inline {
range,
placeholder,
render_toggle: Arc::new(move |row, folded, toggle, cx| {
render_toggle: Some(Arc::new(move |row, folded, toggle, cx| {
render_toggle(row, folded, toggle, cx).into_any_element()
}),
render_trailer: Arc::new(move |row, folded, cx| {
})),
render_trailer: Some(Arc::new(move |row, folded, cx| {
render_trailer(row, folded, cx).into_any_element()
}),
})),
metadata: None,
}
}
pub fn with_metadata(mut self, metadata: CreaseMetadata) -> Self {
self.metadata = Some(metadata);
self
pub fn with_metadata(self, metadata: CreaseMetadata) -> Self {
match self {
Crease::Inline {
range,
placeholder,
render_toggle,
render_trailer,
..
} => Crease::Inline {
range,
placeholder,
render_toggle,
render_trailer,
metadata: Some(metadata),
},
Crease::Block { .. } => self,
}
}
pub fn range(&self) -> &Range<T> {
match self {
Crease::Inline { range, .. } => range,
Crease::Block { range, .. } => range,
}
}
}
impl std::fmt::Debug for Crease {
impl<T> std::fmt::Debug for Crease<T>
where
T: Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Crease")
.field("range", &self.range)
.finish()
match self {
Crease::Inline {
range, metadata, ..
} => f
.debug_struct("Crease::Inline")
.field("range", range)
.field("metadata", metadata)
.finish_non_exhaustive(),
Crease::Block {
range,
block_height,
..
} => f
.debug_struct("Crease::Block")
.field("range", range)
.field("height", block_height)
.finish_non_exhaustive(),
}
}
}
#[derive(Clone, Debug)]
struct CreaseItem {
id: CreaseId,
crease: Crease,
crease: Crease<Anchor>,
}
impl CreaseMap {
@@ -204,7 +276,7 @@ impl CreaseMap {
pub fn insert(
&mut self,
creases: impl IntoIterator<Item = Crease>,
creases: impl IntoIterator<Item = Crease<Anchor>>,
snapshot: &MultiBufferSnapshot,
) -> Vec<CreaseId> {
let mut new_ids = Vec::new();
@@ -212,11 +284,12 @@ impl CreaseMap {
let mut new_creases = SumTree::new(snapshot);
let mut cursor = self.snapshot.creases.cursor::<ItemSummary>(snapshot);
for crease in creases {
new_creases.append(cursor.slice(&crease.range, Bias::Left, snapshot), snapshot);
let crease_range = crease.range().clone();
new_creases.append(cursor.slice(&crease_range, Bias::Left, snapshot), snapshot);
let id = self.next_id;
self.next_id.0 += 1;
self.id_to_range.insert(id, crease.range.clone());
self.id_to_range.insert(id, crease_range);
new_creases.push(CreaseItem { crease, id }, snapshot);
new_ids.push(id);
}
@@ -293,7 +366,7 @@ impl sum_tree::Item for CreaseItem {
fn summary(&self, _cx: &MultiBufferSnapshot) -> Self::Summary {
ItemSummary {
range: self.crease.range.clone(),
range: self.crease.range().clone(),
}
}
}
@@ -326,13 +399,13 @@ mod test {
// Insert creases
let creases = [
Crease::new(
Crease::inline(
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(1, 5)),
FoldPlaceholder::test(),
|_row, _folded, _toggle, _cx| div(),
|_row, _folded, _cx| div(),
),
Crease::new(
Crease::inline(
snapshot.anchor_before(Point::new(3, 0))..snapshot.anchor_after(Point::new(3, 5)),
FoldPlaceholder::test(),
|_row, _folded, _toggle, _cx| div(),
@@ -372,19 +445,19 @@ mod test {
let mut crease_map = CreaseMap::new(&snapshot);
let creases = [
Crease::new(
Crease::inline(
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(1, 5)),
FoldPlaceholder::test(),
|_row, _folded, _toggle, _cx| div(),
|_row, _folded, _cx| div(),
),
Crease::new(
Crease::inline(
snapshot.anchor_before(Point::new(3, 0))..snapshot.anchor_after(Point::new(3, 5)),
FoldPlaceholder::test(),
|_row, _folded, _toggle, _cx| div(),
|_row, _folded, _cx| div(),
),
Crease::new(
Crease::inline(
snapshot.anchor_before(Point::new(5, 0))..snapshot.anchor_after(Point::new(5, 5)),
FoldPlaceholder::test(),
|_row, _folded, _toggle, _cx| div(),
@@ -402,12 +475,12 @@ mod test {
let range = MultiBufferRow(2)..MultiBufferRow(5);
let creases: Vec<_> = crease_snapshot.creases_in_range(range, &snapshot).collect();
assert_eq!(creases.len(), 1);
assert_eq!(creases[0].range.start.to_point(&snapshot).row, 3);
assert_eq!(creases[0].range().start.to_point(&snapshot).row, 3);
let range = MultiBufferRow(0)..MultiBufferRow(2);
let creases: Vec<_> = crease_snapshot.creases_in_range(range, &snapshot).collect();
assert_eq!(creases.len(), 1);
assert_eq!(creases[0].range.start.to_point(&snapshot).row, 1);
assert_eq!(creases[0].range().start.to_point(&snapshot).row, 1);
let range = MultiBufferRow(6)..MultiBufferRow(7);
let creases: Vec<_> = crease_snapshot.creases_in_range(range, &snapshot).collect();

View File

@@ -6779,7 +6779,7 @@ impl Editor {
let mut edits = Vec::new();
let mut unfold_ranges = Vec::new();
let mut refold_ranges = Vec::new();
let mut refold_creases = Vec::new();
let selections = self.selections.all::<Point>(cx);
let mut selections = selections.iter().peekable();
@@ -6854,7 +6854,7 @@ impl Editor {
let mut end = fold.range.end.to_point(&buffer);
start.row -= row_delta;
end.row -= row_delta;
refold_ranges.push((start..end, fold.placeholder.clone()));
refold_creases.push(Crease::simple(start..end, fold.placeholder.clone()));
}
}
}
@@ -6870,7 +6870,7 @@ impl Editor {
buffer.edit([(range, text)], None, cx);
}
});
this.fold_ranges(refold_ranges, true, cx);
this.fold_creases(refold_creases, true, cx);
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select(new_selections);
})
@@ -6883,7 +6883,7 @@ impl Editor {
let mut edits = Vec::new();
let mut unfold_ranges = Vec::new();
let mut refold_ranges = Vec::new();
let mut refold_creases = Vec::new();
let selections = self.selections.all::<Point>(cx);
let mut selections = selections.iter().peekable();
@@ -6948,7 +6948,7 @@ impl Editor {
let mut end = fold.range.end.to_point(&buffer);
start.row += row_delta;
end.row += row_delta;
refold_ranges.push((start..end, fold.placeholder.clone()));
refold_creases.push(Crease::simple(start..end, fold.placeholder.clone()));
}
}
}
@@ -6964,7 +6964,7 @@ impl Editor {
buffer.edit([(range, text)], None, cx);
}
});
this.fold_ranges(refold_ranges, true, cx);
this.fold_creases(refold_creases, true, cx);
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
});
}
@@ -10421,7 +10421,7 @@ impl Editor {
style: BlockStyle::Flex,
placement: BlockPlacement::Below(range.start),
height: 1,
render: Box::new({
render: Arc::new({
let rename_editor = rename_editor.clone();
move |cx: &mut BlockContext| {
let mut text_style = cx.editor_style.text.clone();
@@ -10431,6 +10431,7 @@ impl Editor {
text_style = text_style.highlight(highlight_style);
}
div()
.block_mouse_down()
.pl(cx.anchor_x)
.child(EditorElement::new(
&rename_editor,
@@ -10894,7 +10895,7 @@ impl Editor {
}
pub fn fold(&mut self, _: &actions::Fold, cx: &mut ViewContext<Self>) {
let mut fold_ranges = Vec::new();
let mut to_fold = Vec::new();
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let selections = self.selections.all_adjusted(cx);
@@ -10906,12 +10907,10 @@ impl Editor {
let mut found = false;
let mut row = range.start.row;
while row <= range.end.row {
if let Some((foldable_range, fold_text)) =
{ display_map.foldable_range(MultiBufferRow(row)) }
{
if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) {
found = true;
row = foldable_range.end.row + 1;
fold_ranges.push((foldable_range, fold_text));
row = crease.range().end.row + 1;
to_fold.push(crease);
} else {
row += 1
}
@@ -10922,11 +10921,9 @@ impl Editor {
}
for row in (0..=range.start.row).rev() {
if let Some((foldable_range, fold_text)) =
display_map.foldable_range(MultiBufferRow(row))
{
if foldable_range.end.row >= buffer_start_row {
fold_ranges.push((foldable_range, fold_text));
if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) {
if crease.range().end.row >= buffer_start_row {
to_fold.push(crease);
if row <= range.start.row {
break;
}
@@ -10935,26 +10932,29 @@ impl Editor {
}
}
self.fold_ranges(fold_ranges, true, cx);
self.fold_creases(to_fold, true, cx);
}
fn fold_at_level(&mut self, fold_at: &FoldAtLevel, cx: &mut ViewContext<Self>) {
let fold_at_level = fold_at.level;
let snapshot = self.buffer.read(cx).snapshot(cx);
let mut fold_ranges = Vec::new();
let mut to_fold = Vec::new();
let mut stack = vec![(0, snapshot.max_buffer_row().0, 1)];
while let Some((mut start_row, end_row, current_level)) = stack.pop() {
while start_row < end_row {
match self.snapshot(cx).foldable_range(MultiBufferRow(start_row)) {
Some(foldable_range) => {
let nested_start_row = foldable_range.0.start.row + 1;
let nested_end_row = foldable_range.0.end.row;
match self
.snapshot(cx)
.crease_for_buffer_row(MultiBufferRow(start_row))
{
Some(crease) => {
let nested_start_row = crease.range().start.row + 1;
let nested_end_row = crease.range().end.row;
if current_level < fold_at_level {
stack.push((nested_start_row, nested_end_row, current_level + 1));
} else if current_level == fold_at_level {
fold_ranges.push(foldable_range);
to_fold.push(crease);
}
start_row = nested_end_row + 1;
@@ -10964,7 +10964,7 @@ impl Editor {
}
}
self.fold_ranges(fold_ranges, true, cx);
self.fold_creases(to_fold, true, cx);
}
pub fn fold_all(&mut self, _: &actions::FoldAll, cx: &mut ViewContext<Self>) {
@@ -10972,16 +10972,18 @@ impl Editor {
let snapshot = self.buffer.read(cx).snapshot(cx);
for row in 0..snapshot.max_buffer_row().0 {
if let Some(foldable_range) = self.snapshot(cx).foldable_range(MultiBufferRow(row)) {
if let Some(foldable_range) =
self.snapshot(cx).crease_for_buffer_row(MultiBufferRow(row))
{
fold_ranges.push(foldable_range);
}
}
self.fold_ranges(fold_ranges, true, cx);
self.fold_creases(fold_ranges, true, cx);
}
pub fn fold_recursive(&mut self, _: &actions::FoldRecursive, cx: &mut ViewContext<Self>) {
let mut fold_ranges = Vec::new();
let mut to_fold = Vec::new();
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let selections = self.selections.all_adjusted(cx);
@@ -10992,11 +10994,9 @@ impl Editor {
if range.start.row != range.end.row {
let mut found = false;
for row in range.start.row..=range.end.row {
if let Some((foldable_range, fold_text)) =
{ display_map.foldable_range(MultiBufferRow(row)) }
{
if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) {
found = true;
fold_ranges.push((foldable_range, fold_text));
to_fold.push(crease);
}
}
if found {
@@ -11005,11 +11005,9 @@ impl Editor {
}
for row in (0..=range.start.row).rev() {
if let Some((foldable_range, fold_text)) =
display_map.foldable_range(MultiBufferRow(row))
{
if foldable_range.end.row >= buffer_start_row {
fold_ranges.push((foldable_range, fold_text));
if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) {
if crease.range().end.row >= buffer_start_row {
to_fold.push(crease);
} else {
break;
}
@@ -11017,21 +11015,21 @@ impl Editor {
}
}
self.fold_ranges(fold_ranges, true, cx);
self.fold_creases(to_fold, true, cx);
}
pub fn fold_at(&mut self, fold_at: &FoldAt, cx: &mut ViewContext<Self>) {
let buffer_row = fold_at.buffer_row;
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
if let Some((fold_range, placeholder)) = display_map.foldable_range(buffer_row) {
if let Some(crease) = display_map.crease_for_buffer_row(buffer_row) {
let autoscroll = self
.selections
.all::<Point>(cx)
.iter()
.any(|selection| fold_range.overlaps(&selection.range()));
.any(|selection| crease.range().overlaps(&selection.range()));
self.fold_ranges([(fold_range, placeholder)], autoscroll, cx);
self.fold_creases(vec![crease], autoscroll, cx);
}
}
@@ -11092,81 +11090,78 @@ impl Editor {
pub fn unfold_all(&mut self, _: &actions::UnfoldAll, cx: &mut ViewContext<Self>) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
self.unfold_ranges(
&[Point::zero()..display_map.max_point().to_point(&display_map)],
true,
true,
cx,
);
self.unfold_ranges(&[0..display_map.buffer_snapshot.len()], true, true, cx);
}
pub fn fold_selected_ranges(&mut self, _: &FoldSelectedRanges, cx: &mut ViewContext<Self>) {
let selections = self.selections.all::<Point>(cx);
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let line_mode = self.selections.line_mode;
let ranges = selections.into_iter().map(|s| {
if line_mode {
let start = Point::new(s.start.row, 0);
let end = Point::new(
s.end.row,
display_map
.buffer_snapshot
.line_len(MultiBufferRow(s.end.row)),
);
(start..end, display_map.fold_placeholder.clone())
} else {
(s.start..s.end, display_map.fold_placeholder.clone())
}
});
self.fold_ranges(ranges, true, cx);
let ranges = selections
.into_iter()
.map(|s| {
if line_mode {
let start = Point::new(s.start.row, 0);
let end = Point::new(
s.end.row,
display_map
.buffer_snapshot
.line_len(MultiBufferRow(s.end.row)),
);
Crease::simple(start..end, display_map.fold_placeholder.clone())
} else {
Crease::simple(s.start..s.end, display_map.fold_placeholder.clone())
}
})
.collect::<Vec<_>>();
self.fold_creases(ranges, true, cx);
}
pub fn fold_ranges<T: ToOffset + Clone>(
pub fn fold_creases<T: ToOffset + Clone>(
&mut self,
ranges: impl IntoIterator<Item = (Range<T>, FoldPlaceholder)>,
creases: Vec<Crease<T>>,
auto_scroll: bool,
cx: &mut ViewContext<Self>,
) {
let mut fold_ranges = Vec::new();
if creases.is_empty() {
return;
}
let mut buffers_affected = HashMap::default();
let multi_buffer = self.buffer().read(cx);
for (fold_range, fold_text) in ranges {
for crease in &creases {
if let Some((_, buffer, _)) =
multi_buffer.excerpt_containing(fold_range.start.clone(), cx)
multi_buffer.excerpt_containing(crease.range().start.clone(), cx)
{
buffers_affected.insert(buffer.read(cx).remote_id(), buffer);
};
fold_ranges.push((fold_range, fold_text));
}
let mut ranges = fold_ranges.into_iter().peekable();
if ranges.peek().is_some() {
self.display_map.update(cx, |map, cx| map.fold(ranges, cx));
self.display_map.update(cx, |map, cx| map.fold(creases, cx));
if auto_scroll {
self.request_autoscroll(Autoscroll::fit(), cx);
}
for buffer in buffers_affected.into_values() {
self.sync_expanded_diff_hunks(buffer, cx);
}
cx.notify();
if let Some(active_diagnostics) = self.active_diagnostics.take() {
// Clear diagnostics block when folding a range that contains it.
let snapshot = self.snapshot(cx);
if snapshot.intersects_fold(active_diagnostics.primary_range.start) {
drop(snapshot);
self.active_diagnostics = Some(active_diagnostics);
self.dismiss_diagnostics(cx);
} else {
self.active_diagnostics = Some(active_diagnostics);
}
}
self.scrollbar_marker_state.dirty = true;
if auto_scroll {
self.request_autoscroll(Autoscroll::fit(), cx);
}
for buffer in buffers_affected.into_values() {
self.sync_expanded_diff_hunks(buffer, cx);
}
cx.notify();
if let Some(active_diagnostics) = self.active_diagnostics.take() {
// Clear diagnostics block when folding a range that contains it.
let snapshot = self.snapshot(cx);
if snapshot.intersects_fold(active_diagnostics.primary_range.start) {
drop(snapshot);
self.active_diagnostics = Some(active_diagnostics);
self.dismiss_diagnostics(cx);
} else {
self.active_diagnostics = Some(active_diagnostics);
}
}
self.scrollbar_marker_state.dirty = true;
}
/// Removes any folds whose ranges intersect any of the given ranges.
@@ -11215,6 +11210,7 @@ impl Editor {
}
self.display_map.update(cx, update);
if auto_scroll {
self.request_autoscroll(Autoscroll::fit(), cx);
}
@@ -11317,7 +11313,7 @@ impl Editor {
pub fn insert_creases(
&mut self,
creases: impl IntoIterator<Item = Crease>,
creases: impl IntoIterator<Item = Crease<Anchor>>,
cx: &mut ViewContext<Self>,
) -> Vec<CreaseId> {
self.display_map
@@ -14056,7 +14052,7 @@ impl EditorSnapshot {
}
}
pub fn render_fold_toggle(
pub fn render_crease_toggle(
&self,
buffer_row: MultiBufferRow,
row_contains_cursor: bool,
@@ -14064,34 +14060,38 @@ impl EditorSnapshot {
cx: &mut WindowContext,
) -> Option<AnyElement> {
let folded = self.is_line_folded(buffer_row);
let mut is_foldable = false;
if let Some(crease) = self
.crease_snapshot
.query_row(buffer_row, &self.buffer_snapshot)
{
let toggle_callback = Arc::new(move |folded, cx: &mut WindowContext| {
if folded {
editor.update(cx, |editor, cx| {
editor.fold_at(&crate::FoldAt { buffer_row }, cx)
});
} else {
editor.update(cx, |editor, cx| {
editor.unfold_at(&crate::UnfoldAt { buffer_row }, cx)
});
is_foldable = true;
match crease {
Crease::Inline { render_toggle, .. } | Crease::Block { render_toggle, .. } => {
if let Some(render_toggle) = render_toggle {
let toggle_callback = Arc::new(move |folded, cx: &mut WindowContext| {
if folded {
editor.update(cx, |editor, cx| {
editor.fold_at(&crate::FoldAt { buffer_row }, cx)
});
} else {
editor.update(cx, |editor, cx| {
editor.unfold_at(&crate::UnfoldAt { buffer_row }, cx)
});
}
});
return Some((render_toggle)(buffer_row, folded, toggle_callback, cx));
}
}
});
}
}
Some((crease.render_toggle)(
buffer_row,
folded,
toggle_callback,
cx,
))
} else if folded
|| (self.starts_indent(buffer_row) && (row_contains_cursor || self.gutter_hovered))
{
is_foldable |= self.starts_indent(buffer_row);
if folded || (is_foldable && (row_contains_cursor || self.gutter_hovered)) {
Some(
Disclosure::new(("indent-fold-indicator", buffer_row.0), !folded)
Disclosure::new(("gutter_crease", buffer_row.0), !folded)
.selected(folded)
.on_click(cx.listener_for(&editor, move |this, _e, cx| {
if folded {
@@ -14113,10 +14113,15 @@ impl EditorSnapshot {
cx: &mut WindowContext,
) -> Option<AnyElement> {
let folded = self.is_line_folded(buffer_row);
let crease = self
if let Crease::Inline { render_trailer, .. } = self
.crease_snapshot
.query_row(buffer_row, &self.buffer_snapshot)?;
Some((crease.render_trailer)(buffer_row, folded, cx))
.query_row(buffer_row, &self.buffer_snapshot)?
{
let render_trailer = render_trailer.as_ref()?;
Some(render_trailer(buffer_row, folded, cx))
} else {
None
}
}
}
@@ -14621,7 +14626,7 @@ pub fn diagnostic_block_renderer(
let (text_without_backticks, code_ranges) =
highlight_diagnostic_message(&diagnostic, max_message_rows);
Box::new(move |cx: &mut BlockContext| {
Arc::new(move |cx: &mut BlockContext| {
let group_id: SharedString = cx.block_id.to_string().into();
let mut text_style = cx.text_style().clone();
@@ -14676,6 +14681,7 @@ pub fn diagnostic_block_renderer(
.group(group_id.clone())
.relative()
.size_full()
.block_mouse_down()
.pl(cx.gutter_dimensions.width)
.w(cx.max_width - cx.gutter_dimensions.full_width())
.child(

View File

@@ -596,10 +596,10 @@ fn test_clone(cx: &mut TestAppContext) {
_ = editor.update(cx, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges(selection_ranges.clone()));
editor.fold_ranges(
[
(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
editor.fold_creases(
vec![
Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
],
true,
cx,
@@ -1283,11 +1283,11 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
assert_eq!('α'.len_utf8(), 2);
_ = view.update(cx, |view, cx| {
view.fold_ranges(
view.fold_creases(
vec![
(Point::new(0, 6)..Point::new(0, 12), FoldPlaceholder::test()),
(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
Crease::simple(Point::new(0, 6)..Point::new(0, 12), FoldPlaceholder::test()),
Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
],
true,
cx,
@@ -3875,11 +3875,11 @@ fn test_move_line_up_down(cx: &mut TestAppContext) {
build_editor(buffer, cx)
});
_ = view.update(cx, |view, cx| {
view.fold_ranges(
view.fold_creases(
vec![
(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
],
true,
cx,
@@ -3980,7 +3980,7 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
style: BlockStyle::Fixed,
placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
height: 1,
render: Box::new(|_| div().into_any()),
render: Arc::new(|_| div().into_any()),
priority: 0,
}],
Some(Autoscroll::fit()),
@@ -4022,7 +4022,7 @@ async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
placement,
height: 4,
style: BlockStyle::Sticky,
render: Box::new(|_| gpui::div().into_any_element()),
render: Arc::new(|_| gpui::div().into_any_element()),
priority: 0,
}],
None,
@@ -4717,11 +4717,11 @@ fn test_split_selection_into_lines(cx: &mut TestAppContext) {
build_editor(buffer, cx)
});
_ = view.update(cx, |view, cx| {
view.fold_ranges(
view.fold_creases(
vec![
(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
],
true,
cx,
@@ -5398,13 +5398,13 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
// Ensure that we keep expanding the selection if the larger selection starts or ends within
// a fold.
editor.update(cx, |view, cx| {
view.fold_ranges(
view.fold_creases(
vec![
(
Crease::simple(
Point::new(0, 21)..Point::new(0, 24),
FoldPlaceholder::test(),
),
(
Crease::simple(
Point::new(3, 20)..Point::new(3, 22),
FoldPlaceholder::test(),
),
@@ -13139,7 +13139,7 @@ fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
callback: Arc<dyn Fn(bool, &mut WindowContext) + Send + Sync>,
}
let crease = Crease::new(
let crease = Crease::inline(
range,
FoldPlaceholder::test(),
{
@@ -13158,7 +13158,8 @@ fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
editor.insert_creases(Some(crease), cx);
let snapshot = editor.snapshot(cx);
let _div = snapshot.render_fold_toggle(MultiBufferRow(1), false, cx.view().clone(), cx);
let _div =
snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.view().clone(), cx);
snapshot
})
.unwrap();

View File

@@ -1227,9 +1227,9 @@ impl EditorElement {
}
#[allow(clippy::too_many_arguments)]
fn prepaint_gutter_fold_toggles(
fn prepaint_crease_toggles(
&self,
toggles: &mut [Option<AnyElement>],
crease_toggles: &mut [Option<AnyElement>],
line_height: Pixels,
gutter_dimensions: &GutterDimensions,
gutter_settings: crate::editor_settings::Gutter,
@@ -1237,25 +1237,25 @@ impl EditorElement {
gutter_hitbox: &Hitbox,
cx: &mut WindowContext,
) {
for (ix, fold_indicator) in toggles.iter_mut().enumerate() {
if let Some(fold_indicator) = fold_indicator {
for (ix, crease_toggle) in crease_toggles.iter_mut().enumerate() {
if let Some(crease_toggle) = crease_toggle {
debug_assert!(gutter_settings.folds);
let available_space = size(
AvailableSpace::MinContent,
AvailableSpace::Definite(line_height * 0.55),
);
let fold_indicator_size = fold_indicator.layout_as_root(available_space, cx);
let crease_toggle_size = crease_toggle.layout_as_root(available_space, cx);
let position = point(
gutter_dimensions.width - gutter_dimensions.right_padding,
ix as f32 * line_height - (scroll_pixel_position.y % line_height),
);
let centering_offset = point(
(gutter_dimensions.fold_area_width() - fold_indicator_size.width) / 2.,
(line_height - fold_indicator_size.height) / 2.,
(gutter_dimensions.fold_area_width() - crease_toggle_size.width) / 2.,
(line_height - crease_toggle_size.height) / 2.,
);
let origin = gutter_hitbox.origin + position + centering_offset;
fold_indicator.prepaint_as_root(origin, available_space, cx);
crease_toggle.prepaint_as_root(origin, available_space, cx);
}
}
}
@@ -1915,7 +1915,7 @@ impl EditorElement {
.collect()
}
fn layout_gutter_fold_toggles(
fn layout_crease_toggles(
&self,
rows: Range<DisplayRow>,
buffer_rows: impl IntoIterator<Item = Option<MultiBufferRow>>,
@@ -1934,7 +1934,7 @@ impl EditorElement {
if let Some(multibuffer_row) = row {
let display_row = DisplayRow(rows.start.0 + ix as u32);
let active = active_rows.contains_key(&display_row);
snapshot.render_fold_toggle(
snapshot.render_crease_toggle(
multibuffer_row,
active,
self.editor.clone(),
@@ -2122,9 +2122,7 @@ impl EditorElement {
max_width: text_hitbox.size.width.max(*scroll_width),
editor_style: &self.style,
}))
.cursor(CursorStyle::Arrow)
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
.into_any_element()
.into_any()
}
Block::ExcerptBoundary {
@@ -3354,9 +3352,9 @@ impl EditorElement {
fn paint_gutter_indicators(&self, layout: &mut EditorLayout, cx: &mut WindowContext) {
cx.paint_layer(layout.gutter_hitbox.bounds, |cx| {
cx.with_element_namespace("gutter_fold_toggles", |cx| {
for fold_indicator in layout.gutter_fold_toggles.iter_mut().flatten() {
fold_indicator.paint(cx);
cx.with_element_namespace("crease_toggles", |cx| {
for crease_toggle in layout.crease_toggles.iter_mut().flatten() {
crease_toggle.paint(cx);
}
});
@@ -5167,16 +5165,15 @@ impl Element for EditorElement {
cx,
);
let mut gutter_fold_toggles =
cx.with_element_namespace("gutter_fold_toggles", |cx| {
self.layout_gutter_fold_toggles(
start_row..end_row,
buffer_rows.iter().copied(),
&active_rows,
&snapshot,
cx,
)
});
let mut crease_toggles = cx.with_element_namespace("crease_toggles", |cx| {
self.layout_crease_toggles(
start_row..end_row,
buffer_rows.iter().copied(),
&active_rows,
&snapshot,
cx,
)
});
let crease_trailers = cx.with_element_namespace("crease_trailers", |cx| {
self.layout_crease_trailers(buffer_rows.iter().copied(), &snapshot, cx)
});
@@ -5556,9 +5553,9 @@ impl Element for EditorElement {
let mouse_context_menu =
self.layout_mouse_context_menu(&snapshot, start_row..end_row, cx);
cx.with_element_namespace("gutter_fold_toggles", |cx| {
self.prepaint_gutter_fold_toggles(
&mut gutter_fold_toggles,
cx.with_element_namespace("crease_toggles", |cx| {
self.prepaint_crease_toggles(
&mut crease_toggles,
line_height,
&gutter_dimensions,
gutter_settings,
@@ -5638,7 +5635,7 @@ impl Element for EditorElement {
mouse_context_menu,
test_indicators,
code_actions_indicator,
gutter_fold_toggles,
crease_toggles,
crease_trailers,
tab_invisible,
space_invisible,
@@ -5671,7 +5668,6 @@ impl Element for EditorElement {
line_height: Some(self.style.text.line_height),
..Default::default()
};
let mouse_position = cx.mouse_position();
let hovered_hunk = layout
.display_hunks
.iter()
@@ -5685,7 +5681,7 @@ impl Element for EditorElement {
} => {
if hunk_hitbox
.as_ref()
.map(|hitbox| hitbox.contains(&mouse_position))
.map(|hitbox| hitbox.is_hovered(cx))
.unwrap_or(false)
{
Some(HoveredHunk {
@@ -5778,7 +5774,7 @@ pub struct EditorLayout {
selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,
code_actions_indicator: Option<AnyElement>,
test_indicators: Vec<AnyElement>,
gutter_fold_toggles: Vec<Option<AnyElement>>,
crease_toggles: Vec<Option<AnyElement>>,
crease_trailers: Vec<Option<CreaseTrailerLayout>>,
mouse_context_menu: Option<AnyElement>,
tab_invisible: ShapedLine,
@@ -6623,7 +6619,7 @@ mod tests {
style: BlockStyle::Fixed,
placement: BlockPlacement::Above(Anchor::min()),
height: 3,
render: Box::new(|cx| div().h(3. * cx.line_height()).into_any()),
render: Arc::new(|cx| div().h(3. * cx.line_height()).into_any()),
priority: 0,
}],
None,

View File

@@ -425,7 +425,7 @@ impl Editor {
height: 1,
style: BlockStyle::Sticky,
priority: 0,
render: Box::new({
render: Arc::new({
let editor = cx.view().clone();
let hunk = hunk.clone();
@@ -435,6 +435,7 @@ impl Editor {
h_flex()
.id(cx.block_id)
.block_mouse_down()
.h(cx.line_height())
.w_full()
.border_t_1()
@@ -707,12 +708,13 @@ impl Editor {
height,
style: BlockStyle::Flex,
priority: 0,
render: Box::new(move |cx| {
render: Arc::new(move |cx| {
let width = EditorElement::diff_hunk_strip_width(cx.line_height());
let gutter_dimensions = editor.read(cx.context).gutter_dimensions;
h_flex()
.id(cx.block_id)
.block_mouse_down()
.bg(deleted_hunk_color)
.h(height as f32 * cx.line_height())
.w_full()

View File

@@ -15,9 +15,11 @@ path = "src/extension.rs"
anyhow.workspace = true
async-compression.workspace = true
async-tar.workspace = true
async-trait.workspace = true
collections.workspace = true
fs.workspace = true
futures.workspace = true
gpui.workspace = true
http_client.workspace = true
language.workspace = true
log.workspace = true

View File

@@ -1,10 +1,61 @@
pub mod extension_builder;
mod extension_manifest;
mod slash_command;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::{anyhow, bail, Context as _, Result};
use async_trait::async_trait;
use gpui::Task;
use semantic_version::SemanticVersion;
pub use crate::extension_manifest::*;
pub use crate::slash_command::*;
#[async_trait]
pub trait WorktreeDelegate: Send + Sync + 'static {
fn id(&self) -> u64;
fn root_path(&self) -> String;
async fn read_text_file(&self, path: PathBuf) -> Result<String>;
async fn which(&self, binary_name: String) -> Option<String>;
async fn shell_env(&self) -> Vec<(String, String)>;
}
pub trait KeyValueStoreDelegate: Send + Sync + 'static {
fn insert(&self, key: String, docs: String) -> Task<Result<()>>;
}
#[async_trait]
pub trait Extension: Send + Sync + 'static {
/// Returns the [`ExtensionManifest`] for this extension.
fn manifest(&self) -> Arc<ExtensionManifest>;
/// Returns the path to this extension's working directory.
fn work_dir(&self) -> Arc<Path>;
async fn complete_slash_command_argument(
&self,
command: SlashCommand,
arguments: Vec<String>,
) -> Result<Vec<SlashCommandArgumentCompletion>>;
async fn run_slash_command(
&self,
command: SlashCommand,
arguments: Vec<String>,
resource: Option<Arc<dyn WorktreeDelegate>>,
) -> Result<SlashCommandOutput>;
async fn suggest_docs_packages(&self, provider: Arc<str>) -> Result<Vec<String>>;
async fn index_docs(
&self,
provider: Arc<str>,
package_name: Arc<str>,
kv_store: Arc<dyn KeyValueStoreDelegate>,
) -> Result<()>;
}
pub fn parse_wasm_extension_version(
extension_id: &str,

View File

@@ -0,0 +1,43 @@
use std::ops::Range;
/// A slash command for use in the Assistant.
#[derive(Debug, Clone)]
pub struct SlashCommand {
/// The name of the slash command.
pub name: String,
/// The description of the slash command.
pub description: String,
/// The tooltip text to display for the run button.
pub tooltip_text: String,
/// Whether this slash command requires an argument.
pub requires_argument: bool,
}
/// The output of a slash command.
#[derive(Debug, Clone)]
pub struct SlashCommandOutput {
/// The text produced by the slash command.
pub text: String,
/// The list of sections to show in the slash command placeholder.
pub sections: Vec<SlashCommandOutputSection>,
}
/// A section in the slash command output.
#[derive(Debug, Clone)]
pub struct SlashCommandOutputSection {
/// The range this section occupies.
pub range: Range<usize>,
/// The label to display in the placeholder for this section.
pub label: String,
}
/// A completion for a slash command argument.
#[derive(Debug, Clone)]
pub struct SlashCommandArgumentCompletion {
/// The label to display for this completion.
pub label: String,
/// The new text that should be inserted into the command when this completion is accepted.
pub new_text: String,
/// Whether the command should be run when accepting this completion.
pub run_command: bool,
}

View File

@@ -9,6 +9,7 @@ use async_tar::Archive;
use client::{telemetry::Telemetry, Client, ExtensionMetadata, GetExtensionsResponse};
use collections::{btree_map, BTreeMap, HashSet};
use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder};
use extension::Extension;
pub use extension::ExtensionManifest;
use fs::{Fs, RemoveOptions};
use futures::{
@@ -90,10 +91,6 @@ pub fn is_version_compatible(
true
}
pub trait DocsDatabase: Send + Sync + 'static {
fn insert(&self, key: String, docs: String) -> Task<Result<()>>;
}
pub trait ExtensionRegistrationHooks: Send + Sync + 'static {
fn remove_user_themes(&self, _themes: Vec<SharedString>) {}
@@ -135,9 +132,8 @@ pub trait ExtensionRegistrationHooks: Send + Sync + 'static {
fn register_slash_command(
&self,
_slash_command: wit::SlashCommand,
_extension: WasmExtension,
_host: Arc<WasmHost>,
_extension: Arc<dyn Extension>,
_command: extension::SlashCommand,
) {
}
@@ -145,17 +141,11 @@ pub trait ExtensionRegistrationHooks: Send + Sync + 'static {
&self,
_id: Arc<str>,
_extension: WasmExtension,
_host: Arc<WasmHost>,
_cx: &mut AppContext,
) {
}
fn register_docs_provider(
&self,
_extension: WasmExtension,
_host: Arc<WasmHost>,
_provider_id: Arc<str>,
) {
}
fn register_docs_provider(&self, _extension: Arc<dyn Extension>, _provider_id: Arc<str>) {}
fn register_snippets(&self, _path: &PathBuf, _snippet_contents: &str) -> Result<()> {
Ok(())
@@ -1238,6 +1228,8 @@ impl ExtensionStore {
this.reload_complete_senders.clear();
for (manifest, wasm_extension) in &wasm_extensions {
let extension = Arc::new(wasm_extension.clone());
for (language_server_id, language_server_config) in &manifest.language_servers {
for language in language_server_config.languages() {
this.registration_hooks.register_lsp_adapter(
@@ -1257,7 +1249,8 @@ impl ExtensionStore {
for (slash_command_name, slash_command) in &manifest.slash_commands {
this.registration_hooks.register_slash_command(
crate::wit::SlashCommand {
extension.clone(),
extension::SlashCommand {
name: slash_command_name.to_string(),
description: slash_command.description.to_string(),
// We don't currently expose this as a configurable option, as it currently drives
@@ -1266,8 +1259,6 @@ impl ExtensionStore {
tooltip_text: String::new(),
requires_argument: slash_command.requires_argument,
},
wasm_extension.clone(),
this.wasm_host.clone(),
);
}
@@ -1275,16 +1266,13 @@ impl ExtensionStore {
this.registration_hooks.register_context_server(
id.clone(),
wasm_extension.clone(),
this.wasm_host.clone(),
cx,
);
}
for (provider_id, _provider) in &manifest.indexed_docs_providers {
this.registration_hooks.register_docs_provider(
wasm_extension.clone(),
this.wasm_host.clone(),
provider_id.clone(),
);
this.registration_hooks
.register_docs_provider(extension.clone(), provider_id.clone());
}
}

View File

@@ -5,6 +5,7 @@ use crate::wasm_host::{
use anyhow::{anyhow, Context, Result};
use async_trait::async_trait;
use collections::HashMap;
use extension::WorktreeDelegate;
use futures::{Future, FutureExt};
use gpui::AsyncAppContext;
use language::{
@@ -18,6 +19,35 @@ use std::{any::Any, path::PathBuf, pin::Pin, sync::Arc};
use util::{maybe, ResultExt};
use wasmtime_wasi::WasiView as _;
/// An adapter that allows an [`LspAdapterDelegate`] to be used as a [`WorktreeDelegate`].
pub struct WorktreeDelegateAdapter(pub Arc<dyn LspAdapterDelegate>);
#[async_trait]
impl WorktreeDelegate for WorktreeDelegateAdapter {
fn id(&self) -> u64 {
self.0.worktree_id().to_proto()
}
fn root_path(&self) -> String {
self.0.worktree_root_path().to_string_lossy().to_string()
}
async fn read_text_file(&self, path: PathBuf) -> Result<String> {
self.0.read_text_file(path).await
}
async fn which(&self, binary_name: String) -> Option<String> {
self.0
.which(binary_name.as_ref())
.await
.map(|path| path.to_string_lossy().to_string())
}
async fn shell_env(&self) -> Vec<(String, String)> {
self.0.shell_env().await.into_iter().collect()
}
}
pub struct ExtensionLspAdapter {
pub(crate) extension: WasmExtension,
pub(crate) language_server_id: LanguageServerName,
@@ -45,6 +75,7 @@ impl LspAdapter for ExtensionLspAdapter {
let this = self.clone();
|extension, store| {
async move {
let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _;
let resource = store.data_mut().table().push(delegate)?;
let command = extension
.call_language_server_command(
@@ -166,6 +197,7 @@ impl LspAdapter for ExtensionLspAdapter {
let this = self.clone();
|extension, store| {
async move {
let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _;
let resource = store.data_mut().table().push(delegate)?;
let options = extension
.call_language_server_initialization_options(
@@ -204,6 +236,7 @@ impl LspAdapter for ExtensionLspAdapter {
let this = self.clone();
|extension, store| {
async move {
let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _;
let resource = store.data_mut().table().push(delegate)?;
let options = extension
.call_language_server_workspace_configuration(

View File

@@ -2,6 +2,11 @@ pub mod wit;
use crate::{ExtensionManifest, ExtensionRegistrationHooks};
use anyhow::{anyhow, bail, Context as _, Result};
use async_trait::async_trait;
use extension::{
KeyValueStoreDelegate, SlashCommand, SlashCommandArgumentCompletion, SlashCommandOutput,
WorktreeDelegate,
};
use fs::{normalize_path, Fs};
use futures::future::LocalBoxFuture;
use futures::{
@@ -25,9 +30,9 @@ use wasmtime::{
component::{Component, ResourceTable},
Engine, Store,
};
use wasmtime_wasi as wasi;
use wasmtime_wasi::{self as wasi, WasiView};
use wit::Extension;
pub use wit::{ExtensionProject, SlashCommand};
pub use wit::ExtensionProject;
pub struct WasmHost {
engine: Engine,
@@ -45,10 +50,108 @@ pub struct WasmHost {
pub struct WasmExtension {
tx: UnboundedSender<ExtensionCall>,
pub manifest: Arc<ExtensionManifest>,
pub work_dir: Arc<Path>,
#[allow(unused)]
pub zed_api_version: SemanticVersion,
}
#[async_trait]
impl extension::Extension for WasmExtension {
fn manifest(&self) -> Arc<ExtensionManifest> {
self.manifest.clone()
}
fn work_dir(&self) -> Arc<Path> {
self.work_dir.clone()
}
async fn complete_slash_command_argument(
&self,
command: SlashCommand,
arguments: Vec<String>,
) -> Result<Vec<SlashCommandArgumentCompletion>> {
self.call(|extension, store| {
async move {
let completions = extension
.call_complete_slash_command_argument(store, &command.into(), &arguments)
.await?
.map_err(|err| anyhow!("{err}"))?;
Ok(completions.into_iter().map(Into::into).collect())
}
.boxed()
})
.await
}
async fn run_slash_command(
&self,
command: SlashCommand,
arguments: Vec<String>,
delegate: Option<Arc<dyn WorktreeDelegate>>,
) -> Result<SlashCommandOutput> {
self.call(|extension, store| {
async move {
let resource = if let Some(delegate) = delegate {
Some(store.data_mut().table().push(delegate)?)
} else {
None
};
let output = extension
.call_run_slash_command(store, &command.into(), &arguments, resource)
.await?
.map_err(|err| anyhow!("{err}"))?;
Ok(output.into())
}
.boxed()
})
.await
}
async fn suggest_docs_packages(&self, provider: Arc<str>) -> Result<Vec<String>> {
self.call(|extension, store| {
async move {
let packages = extension
.call_suggest_docs_packages(store, provider.as_ref())
.await?
.map_err(|err| anyhow!("{err:?}"))?;
Ok(packages)
}
.boxed()
})
.await
}
async fn index_docs(
&self,
provider: Arc<str>,
package_name: Arc<str>,
kv_store: Arc<dyn KeyValueStoreDelegate>,
) -> Result<()> {
self.call(|extension, store| {
async move {
let kv_store_resource = store.data_mut().table().push(kv_store)?;
extension
.call_index_docs(
store,
provider.as_ref(),
package_name.as_ref(),
kv_store_resource,
)
.await?
.map_err(|err| anyhow!("{err:?}"))?;
anyhow::Ok(())
}
.boxed()
})
.await
}
}
pub struct WasmState {
manifest: Arc<ExtensionManifest>,
pub table: ResourceTable,
@@ -152,6 +255,7 @@ impl WasmHost {
Ok(WasmExtension {
manifest: manifest.clone(),
work_dir: this.work_dir.clone().into(),
tx,
zed_api_version,
})

View File

@@ -3,15 +3,13 @@ mod since_v0_0_4;
mod since_v0_0_6;
mod since_v0_1_0;
mod since_v0_2_0;
use extension::{KeyValueStoreDelegate, WorktreeDelegate};
use lsp::LanguageServerName;
use release_channel::ReleaseChannel;
use since_v0_2_0 as latest;
use crate::DocsDatabase;
use super::{wasm_engine, WasmState};
use anyhow::{anyhow, Context, Result};
use language::LspAdapterDelegate;
use semantic_version::SemanticVersion;
use std::{ops::RangeInclusive, sync::Arc};
use wasmtime::{
@@ -153,7 +151,7 @@ impl Extension {
store: &mut Store<WasmState>,
language_server_id: &LanguageServerName,
config: &LanguageServerConfig,
resource: Resource<Arc<dyn LspAdapterDelegate>>,
resource: Resource<Arc<dyn WorktreeDelegate>>,
) -> Result<Result<Command, String>> {
match self {
Extension::V020(ext) => {
@@ -184,7 +182,7 @@ impl Extension {
store: &mut Store<WasmState>,
language_server_id: &LanguageServerName,
config: &LanguageServerConfig,
resource: Resource<Arc<dyn LspAdapterDelegate>>,
resource: Resource<Arc<dyn WorktreeDelegate>>,
) -> Result<Result<Option<String>, String>> {
match self {
Extension::V020(ext) => {
@@ -230,7 +228,7 @@ impl Extension {
&self,
store: &mut Store<WasmState>,
language_server_id: &LanguageServerName,
resource: Resource<Arc<dyn LspAdapterDelegate>>,
resource: Resource<Arc<dyn WorktreeDelegate>>,
) -> Result<Result<Option<String>, String>> {
match self {
Extension::V020(ext) => {
@@ -367,7 +365,7 @@ impl Extension {
store: &mut Store<WasmState>,
command: &SlashCommand,
arguments: &[String],
resource: Option<Resource<Arc<dyn LspAdapterDelegate>>>,
resource: Option<Resource<Arc<dyn WorktreeDelegate>>>,
) -> Result<Result<SlashCommandOutput, String>> {
match self {
Extension::V020(ext) => {
@@ -422,15 +420,15 @@ impl Extension {
store: &mut Store<WasmState>,
provider: &str,
package_name: &str,
database: Resource<Arc<dyn DocsDatabase>>,
kv_store: Resource<Arc<dyn KeyValueStoreDelegate>>,
) -> Result<Result<(), String>> {
match self {
Extension::V020(ext) => {
ext.call_index_docs(store, provider, package_name, database)
ext.call_index_docs(store, provider, package_name, kv_store)
.await
}
Extension::V010(ext) => {
ext.call_index_docs(store, provider, package_name, database)
ext.call_index_docs(store, provider, package_name, kv_store)
.await
}
Extension::V001(_) | Extension::V004(_) | Extension::V006(_) => {

View File

@@ -3,7 +3,8 @@ use crate::wasm_host::wit::since_v0_0_4;
use crate::wasm_host::WasmState;
use anyhow::Result;
use async_trait::async_trait;
use language::{LanguageServerBinaryStatus, LspAdapterDelegate};
use extension::WorktreeDelegate;
use language::LanguageServerBinaryStatus;
use semantic_version::SemanticVersion;
use std::sync::{Arc, OnceLock};
use wasmtime::component::{Linker, Resource};
@@ -21,7 +22,7 @@ wasmtime::component::bindgen!({
},
});
pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
pub type ExtensionWorktree = Arc<dyn WorktreeDelegate>;
pub fn linker() -> &'static Linker<WasmState> {
static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
@@ -62,7 +63,7 @@ impl From<Command> for latest::Command {
impl HostWorktree for WasmState {
async fn read_text_file(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
delegate: Resource<Arc<dyn WorktreeDelegate>>,
path: String,
) -> wasmtime::Result<Result<String, String>> {
latest::HostWorktree::read_text_file(self, delegate, path).await
@@ -70,14 +71,14 @@ impl HostWorktree for WasmState {
async fn shell_env(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
delegate: Resource<Arc<dyn WorktreeDelegate>>,
) -> wasmtime::Result<EnvVars> {
latest::HostWorktree::shell_env(self, delegate).await
}
async fn which(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
delegate: Resource<Arc<dyn WorktreeDelegate>>,
binary_name: String,
) -> wasmtime::Result<Option<String>> {
latest::HostWorktree::which(self, delegate, binary_name).await

View File

@@ -2,7 +2,7 @@ use super::latest;
use crate::wasm_host::WasmState;
use anyhow::Result;
use async_trait::async_trait;
use language::LspAdapterDelegate;
use extension::WorktreeDelegate;
use semantic_version::SemanticVersion;
use std::sync::{Arc, OnceLock};
use wasmtime::component::{Linker, Resource};
@@ -20,7 +20,7 @@ wasmtime::component::bindgen!({
},
});
pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
pub type ExtensionWorktree = Arc<dyn WorktreeDelegate>;
pub fn linker() -> &'static Linker<WasmState> {
static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
@@ -71,7 +71,7 @@ impl From<Command> for latest::Command {
impl HostWorktree for WasmState {
async fn read_text_file(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
delegate: Resource<Arc<dyn WorktreeDelegate>>,
path: String,
) -> wasmtime::Result<Result<String, String>> {
latest::HostWorktree::read_text_file(self, delegate, path).await
@@ -79,14 +79,14 @@ impl HostWorktree for WasmState {
async fn shell_env(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
delegate: Resource<Arc<dyn WorktreeDelegate>>,
) -> wasmtime::Result<EnvVars> {
latest::HostWorktree::shell_env(self, delegate).await
}
async fn which(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
delegate: Resource<Arc<dyn WorktreeDelegate>>,
binary_name: String,
) -> wasmtime::Result<Option<String>> {
latest::HostWorktree::which(self, delegate, binary_name).await

View File

@@ -2,7 +2,7 @@ use super::{latest, since_v0_1_0};
use crate::wasm_host::WasmState;
use anyhow::Result;
use async_trait::async_trait;
use language::LspAdapterDelegate;
use extension::WorktreeDelegate;
use semantic_version::SemanticVersion;
use std::sync::{Arc, OnceLock};
use wasmtime::component::{Linker, Resource};
@@ -26,7 +26,7 @@ mod settings {
include!(concat!(env!("OUT_DIR"), "/since_v0.0.6/settings.rs"));
}
pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
pub type ExtensionWorktree = Arc<dyn WorktreeDelegate>;
pub fn linker() -> &'static Linker<WasmState> {
static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
@@ -113,23 +113,20 @@ impl From<CodeLabel> for latest::CodeLabel {
#[async_trait]
impl HostWorktree for WasmState {
async fn id(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
) -> wasmtime::Result<u64> {
async fn id(&mut self, delegate: Resource<Arc<dyn WorktreeDelegate>>) -> wasmtime::Result<u64> {
latest::HostWorktree::id(self, delegate).await
}
async fn root_path(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
delegate: Resource<Arc<dyn WorktreeDelegate>>,
) -> wasmtime::Result<String> {
latest::HostWorktree::root_path(self, delegate).await
}
async fn read_text_file(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
delegate: Resource<Arc<dyn WorktreeDelegate>>,
path: String,
) -> wasmtime::Result<Result<String, String>> {
latest::HostWorktree::read_text_file(self, delegate, path).await
@@ -137,14 +134,14 @@ impl HostWorktree for WasmState {
async fn shell_env(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
delegate: Resource<Arc<dyn WorktreeDelegate>>,
) -> wasmtime::Result<EnvVars> {
latest::HostWorktree::shell_env(self, delegate).await
}
async fn which(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
delegate: Resource<Arc<dyn WorktreeDelegate>>,
binary_name: String,
) -> wasmtime::Result<Option<String>> {
latest::HostWorktree::which(self, delegate, binary_name).await

View File

@@ -1,17 +1,15 @@
use crate::wasm_host::{wit::ToWasmtimeResult, WasmState};
use crate::DocsDatabase;
use ::http_client::{AsyncBody, HttpRequestExt};
use ::settings::{Settings, WorktreeId};
use anyhow::{anyhow, bail, Context, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
use extension::{KeyValueStoreDelegate, WorktreeDelegate};
use futures::{io::BufReader, FutureExt as _};
use futures::{lock::Mutex, AsyncReadExt};
use language::LanguageName;
use language::{
language_settings::AllLanguageSettings, LanguageServerBinaryStatus, LspAdapterDelegate,
};
use language::{language_settings::AllLanguageSettings, LanguageServerBinaryStatus};
use project::project_settings::ProjectSettings;
use semantic_version::SemanticVersion;
use std::{
@@ -47,8 +45,8 @@ mod settings {
include!(concat!(env!("OUT_DIR"), "/since_v0.1.0/settings.rs"));
}
pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
pub type ExtensionKeyValueStore = Arc<dyn DocsDatabase>;
pub type ExtensionWorktree = Arc<dyn WorktreeDelegate>;
pub type ExtensionKeyValueStore = Arc<dyn KeyValueStoreDelegate>;
pub type ExtensionHttpResponseStream = Arc<Mutex<::http_client::Response<AsyncBody>>>;
pub fn linker() -> &'static Linker<WasmState> {
@@ -251,52 +249,38 @@ impl HostKeyValueStore for WasmState {
#[async_trait]
impl HostWorktree for WasmState {
async fn id(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
) -> wasmtime::Result<u64> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.worktree_id().to_proto())
async fn id(&mut self, delegate: Resource<Arc<dyn WorktreeDelegate>>) -> wasmtime::Result<u64> {
latest::HostWorktree::id(self, delegate).await
}
async fn root_path(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
delegate: Resource<Arc<dyn WorktreeDelegate>>,
) -> wasmtime::Result<String> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.worktree_root_path().to_string_lossy().to_string())
latest::HostWorktree::root_path(self, delegate).await
}
async fn read_text_file(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
delegate: Resource<Arc<dyn WorktreeDelegate>>,
path: String,
) -> wasmtime::Result<Result<String, String>> {
let delegate = self.table.get(&delegate)?;
Ok(delegate
.read_text_file(path.into())
.await
.map_err(|error| error.to_string()))
latest::HostWorktree::read_text_file(self, delegate, path).await
}
async fn shell_env(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
delegate: Resource<Arc<dyn WorktreeDelegate>>,
) -> wasmtime::Result<EnvVars> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.shell_env().await.into_iter().collect())
latest::HostWorktree::shell_env(self, delegate).await
}
async fn which(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
delegate: Resource<Arc<dyn WorktreeDelegate>>,
binary_name: String,
) -> wasmtime::Result<Option<String>> {
let delegate = self.table.get(&delegate)?;
Ok(delegate
.which(binary_name.as_ref())
.await
.map(|path| path.to_string_lossy().to_string()))
latest::HostWorktree::which(self, delegate, binary_name).await
}
fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {

View File

@@ -1,5 +1,5 @@
use crate::wasm_host::wit::since_v0_2_0::slash_command::SlashCommandOutputSection;
use crate::wasm_host::{wit::ToWasmtimeResult, WasmState};
use crate::DocsDatabase;
use ::http_client::{AsyncBody, HttpRequestExt};
use ::settings::{Settings, WorktreeId};
use anyhow::{anyhow, bail, Context, Result};
@@ -7,12 +7,10 @@ use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
use context_servers::manager::ContextServerSettings;
use extension::{KeyValueStoreDelegate, WorktreeDelegate};
use futures::{io::BufReader, FutureExt as _};
use futures::{lock::Mutex, AsyncReadExt};
use language::{
language_settings::AllLanguageSettings, LanguageName, LanguageServerBinaryStatus,
LspAdapterDelegate,
};
use language::{language_settings::AllLanguageSettings, LanguageName, LanguageServerBinaryStatus};
use project::project_settings::ProjectSettings;
use semantic_version::SemanticVersion;
use std::{
@@ -44,8 +42,8 @@ mod settings {
include!(concat!(env!("OUT_DIR"), "/since_v0.2.0/settings.rs"));
}
pub type ExtensionWorktree = Arc<dyn LspAdapterDelegate>;
pub type ExtensionKeyValueStore = Arc<dyn DocsDatabase>;
pub type ExtensionWorktree = Arc<dyn WorktreeDelegate>;
pub type ExtensionKeyValueStore = Arc<dyn KeyValueStoreDelegate>;
pub type ExtensionHttpResponseStream = Arc<Mutex<::http_client::Response<AsyncBody>>>;
pub struct ExtensionProject {
@@ -57,6 +55,45 @@ pub fn linker() -> &'static Linker<WasmState> {
LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker))
}
impl From<extension::SlashCommand> for SlashCommand {
fn from(value: extension::SlashCommand) -> Self {
Self {
name: value.name,
description: value.description,
tooltip_text: value.tooltip_text,
requires_argument: value.requires_argument,
}
}
}
impl From<SlashCommandOutput> for extension::SlashCommandOutput {
fn from(value: SlashCommandOutput) -> Self {
Self {
text: value.text,
sections: value.sections.into_iter().map(Into::into).collect(),
}
}
}
impl From<SlashCommandOutputSection> for extension::SlashCommandOutputSection {
fn from(value: SlashCommandOutputSection) -> Self {
Self {
range: value.range.start as usize..value.range.end as usize,
label: value.label,
}
}
}
impl From<SlashCommandArgumentCompletion> for extension::SlashCommandArgumentCompletion {
fn from(value: SlashCommandArgumentCompletion) -> Self {
Self {
label: value.label,
new_text: value.new_text,
run_command: value.run_command,
}
}
}
#[async_trait]
impl HostKeyValueStore for WasmState {
async fn insert(
@@ -93,25 +130,22 @@ impl HostProject for WasmState {
#[async_trait]
impl HostWorktree for WasmState {
async fn id(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
) -> wasmtime::Result<u64> {
async fn id(&mut self, delegate: Resource<Arc<dyn WorktreeDelegate>>) -> wasmtime::Result<u64> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.worktree_id().to_proto())
Ok(delegate.id())
}
async fn root_path(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
delegate: Resource<Arc<dyn WorktreeDelegate>>,
) -> wasmtime::Result<String> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.worktree_root_path().to_string_lossy().to_string())
Ok(delegate.root_path())
}
async fn read_text_file(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
delegate: Resource<Arc<dyn WorktreeDelegate>>,
path: String,
) -> wasmtime::Result<Result<String, String>> {
let delegate = self.table.get(&delegate)?;
@@ -123,7 +157,7 @@ impl HostWorktree for WasmState {
async fn shell_env(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
delegate: Resource<Arc<dyn WorktreeDelegate>>,
) -> wasmtime::Result<EnvVars> {
let delegate = self.table.get(&delegate)?;
Ok(delegate.shell_env().await.into_iter().collect())
@@ -131,14 +165,11 @@ impl HostWorktree for WasmState {
async fn which(
&mut self,
delegate: Resource<Arc<dyn LspAdapterDelegate>>,
delegate: Resource<Arc<dyn WorktreeDelegate>>,
binary_name: String,
) -> wasmtime::Result<Option<String>> {
let delegate = self.table.get(&delegate)?;
Ok(delegate
.which(binary_name.as_ref())
.await
.map(|path| path.to_string_lossy().to_string()))
Ok(delegate.which(binary_name).await)
}
fn drop(&mut self, _worktree: Resource<Worktree>) -> Result<()> {

View File

@@ -17,12 +17,12 @@ test-support = []
[dependencies]
anyhow.workspace = true
assistant_slash_command.workspace = true
async-trait.workspace = true
client.workspace = true
collections.workspace = true
context_servers.workspace = true
db.workspace = true
editor.workspace = true
extension.workspace = true
extension_host.workspace = true
fs.workspace = true
futures.workspace = true
@@ -30,6 +30,7 @@ fuzzy.workspace = true
gpui.workspace = true
indexed_docs.workspace = true
language.workspace = true
log.workspace = true
lsp.workspace = true
num-format.workspace = true
picker.workspace = true

View File

@@ -1,97 +0,0 @@
use std::pin::Pin;
use std::sync::Arc;
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use context_servers::manager::{NativeContextServer, ServerCommand, ServerConfig};
use context_servers::protocol::InitializedContextServerProtocol;
use context_servers::ContextServer;
use extension_host::wasm_host::{ExtensionProject, WasmExtension, WasmHost};
use futures::{Future, FutureExt};
use gpui::{AsyncAppContext, Model};
use project::Project;
use wasmtime_wasi::WasiView as _;
pub struct ExtensionContextServer {
#[allow(unused)]
pub(crate) extension: WasmExtension,
#[allow(unused)]
pub(crate) host: Arc<WasmHost>,
id: Arc<str>,
context_server: Arc<NativeContextServer>,
}
impl ExtensionContextServer {
pub async fn new(
extension: WasmExtension,
host: Arc<WasmHost>,
id: Arc<str>,
project: Model<Project>,
mut cx: AsyncAppContext,
) -> Result<Self> {
let extension_project = project.update(&mut cx, |project, cx| ExtensionProject {
worktree_ids: project
.visible_worktrees(cx)
.map(|worktree| worktree.read(cx).id().to_proto())
.collect(),
})?;
let command = extension
.call({
let id = id.clone();
|extension, store| {
async move {
let project = store.data_mut().table().push(extension_project)?;
let command = extension
.call_context_server_command(store, id.clone(), project)
.await?
.map_err(|e| anyhow!("{}", e))?;
anyhow::Ok(command)
}
.boxed()
}
})
.await?;
let config = Arc::new(ServerConfig {
settings: None,
command: Some(ServerCommand {
path: command.command,
args: command.args,
env: Some(command.env.into_iter().collect()),
}),
});
anyhow::Ok(Self {
extension,
host,
id: id.clone(),
context_server: Arc::new(NativeContextServer::new(id, config)),
})
}
}
#[async_trait(?Send)]
impl ContextServer for ExtensionContextServer {
fn id(&self) -> Arc<str> {
self.id.clone()
}
fn config(&self) -> Arc<ServerConfig> {
self.context_server.config()
}
fn client(&self) -> Option<Arc<InitializedContextServerProtocol>> {
self.context_server.client()
}
fn start<'a>(
self: Arc<Self>,
cx: &'a AsyncAppContext,
) -> Pin<Box<dyn 'a + Future<Output = Result<()>>>> {
self.context_server.clone().start(cx)
}
fn stop(&self) -> Result<()> {
self.context_server.stop()
}
}

View File

@@ -1,79 +0,0 @@
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use futures::FutureExt;
use indexed_docs::{IndexedDocsDatabase, IndexedDocsProvider, PackageName, ProviderId};
use wasmtime_wasi::WasiView;
use extension_host::wasm_host::{WasmExtension, WasmHost};
pub struct ExtensionIndexedDocsProvider {
pub(crate) extension: WasmExtension,
pub(crate) host: Arc<WasmHost>,
pub(crate) id: ProviderId,
}
#[async_trait]
impl IndexedDocsProvider for ExtensionIndexedDocsProvider {
fn id(&self) -> ProviderId {
self.id.clone()
}
fn database_path(&self) -> PathBuf {
let mut database_path = self.host.work_dir.clone();
database_path.push(self.extension.manifest.id.as_ref());
database_path.push("docs");
database_path.push(format!("{}.0.mdb", self.id));
database_path
}
async fn suggest_packages(&self) -> Result<Vec<PackageName>> {
self.extension
.call({
let id = self.id.clone();
|extension, store| {
async move {
let packages = extension
.call_suggest_docs_packages(store, id.as_ref())
.await?
.map_err(|err| anyhow!("{err:?}"))?;
Ok(packages
.into_iter()
.map(|package| PackageName::from(package.as_str()))
.collect())
}
.boxed()
}
})
.await
}
async fn index(&self, package: PackageName, database: Arc<IndexedDocsDatabase>) -> Result<()> {
self.extension
.call({
let id = self.id.clone();
|extension, store| {
async move {
let database_resource = store.data_mut().table().push(database as _)?;
extension
.call_index_docs(
store,
id.as_ref(),
package.as_ref(),
database_resource,
)
.await?
.map_err(|err| anyhow!("{err:?}"))?;
anyhow::Ok(())
}
.boxed()
}
})
.await
}
}

View File

@@ -1,20 +1,21 @@
use std::{path::PathBuf, sync::Arc};
use anyhow::Result;
use assistant_slash_command::SlashCommandRegistry;
use anyhow::{anyhow, Result};
use assistant_slash_command::{ExtensionSlashCommand, SlashCommandRegistry};
use context_servers::manager::ServerCommand;
use context_servers::ContextServerFactoryRegistry;
use db::smol::future::FutureExt as _;
use extension::Extension;
use extension_host::wasm_host::ExtensionProject;
use extension_host::{extension_lsp_adapter::ExtensionLspAdapter, wasm_host};
use fs::Fs;
use futures::FutureExt;
use gpui::{AppContext, BackgroundExecutor, Task};
use indexed_docs::{IndexedDocsRegistry, ProviderId};
use gpui::{AppContext, BackgroundExecutor, Model, Task};
use indexed_docs::{ExtensionIndexedDocsProvider, IndexedDocsRegistry, ProviderId};
use language::{LanguageRegistry, LanguageServerBinaryStatus, LoadedLanguage};
use snippet_provider::SnippetRegistry;
use theme::{ThemeRegistry, ThemeSettings};
use ui::SharedString;
use crate::extension_context_server::ExtensionContextServer;
use crate::{extension_indexed_docs_provider, extension_slash_command::ExtensionSlashCommand};
use wasmtime_wasi::WasiView as _;
pub struct ConcreteExtensionRegistrationHooks {
slash_command_registry: Arc<SlashCommandRegistry>,
@@ -22,7 +23,7 @@ pub struct ConcreteExtensionRegistrationHooks {
indexed_docs_registry: Arc<IndexedDocsRegistry>,
snippet_registry: Arc<SnippetRegistry>,
language_registry: Arc<LanguageRegistry>,
context_server_factory_registry: Arc<ContextServerFactoryRegistry>,
context_server_factory_registry: Model<ContextServerFactoryRegistry>,
executor: BackgroundExecutor,
}
@@ -33,7 +34,7 @@ impl ConcreteExtensionRegistrationHooks {
indexed_docs_registry: Arc<IndexedDocsRegistry>,
snippet_registry: Arc<SnippetRegistry>,
language_registry: Arc<LanguageRegistry>,
context_server_factory_registry: Arc<ContextServerFactoryRegistry>,
context_server_factory_registry: Model<ContextServerFactoryRegistry>,
cx: &AppContext,
) -> Arc<dyn extension_host::ExtensionRegistrationHooks> {
Arc::new(Self {
@@ -61,75 +62,85 @@ impl extension_host::ExtensionRegistrationHooks for ConcreteExtensionRegistratio
fn register_slash_command(
&self,
command: wasm_host::SlashCommand,
extension: wasm_host::WasmExtension,
host: Arc<wasm_host::WasmHost>,
extension: Arc<dyn Extension>,
command: extension::SlashCommand,
) {
self.slash_command_registry.register_command(
ExtensionSlashCommand {
command,
extension,
host,
},
false,
)
self.slash_command_registry
.register_command(ExtensionSlashCommand::new(extension, command), false)
}
fn register_context_server(
&self,
id: Arc<str>,
extension: wasm_host::WasmExtension,
host: Arc<wasm_host::WasmHost>,
cx: &mut AppContext,
) {
self.context_server_factory_registry
.register_server_factory(
id.clone(),
Arc::new(move |project, cx| {
async move {
let extension_project =
project.update(&mut cx, |project, cx| ExtensionProject {
worktree_ids: project
.visible_worktrees(cx)
.map(|worktree| worktree.read(cx).id().to_proto())
.collect(),
})?;
let command = extension
.call({
let id = id.clone();
|extension, store| {
async move {
let project =
store.data_mut().table().push(extension_project)?;
let command = extension
.call_context_server_command(store, id.clone(), project)
.await?
.map_err(|e| anyhow!("{}", e))?;
anyhow::Ok(command)
}
.boxed()
}
.update(cx, |registry, _| {
registry.register_server_factory(
id.clone(),
Arc::new({
move |project, cx| {
log::info!(
"loading command for context server {id} from extension {}",
extension.manifest.id
);
let id = id.clone();
let extension = extension.clone();
cx.spawn(|mut cx| async move {
let extension_project =
project.update(&mut cx, |project, cx| ExtensionProject {
worktree_ids: project
.visible_worktrees(cx)
.map(|worktree| worktree.read(cx).id().to_proto())
.collect(),
})?;
let command = extension
.call({
let id = id.clone();
|extension, store| {
async move {
let project = store
.data_mut()
.table()
.push(extension_project)?;
let command = extension
.call_context_server_command(
store,
id.clone(),
project,
)
.await?
.map_err(|e| anyhow!("{}", e))?;
anyhow::Ok(command)
}
.boxed()
}
})
.await?;
log::info!("loaded command for context server {id}: {command:?}");
Ok(ServerCommand {
path: command.command,
args: command.args,
env: Some(command.env.into_iter().collect()),
})
})
.await?;
Ok(command)
}
.boxed()
}),
);
}
}),
)
});
}
fn register_docs_provider(
&self,
extension: wasm_host::WasmExtension,
host: Arc<wasm_host::WasmHost>,
provider_id: Arc<str>,
) {
self.indexed_docs_registry.register_provider(Box::new(
extension_indexed_docs_provider::ExtensionIndexedDocsProvider {
fn register_docs_provider(&self, extension: Arc<dyn Extension>, provider_id: Arc<str>) {
self.indexed_docs_registry
.register_provider(Box::new(ExtensionIndexedDocsProvider::new(
extension,
host,
id: ProviderId(provider_id),
},
));
ProviderId(provider_id),
)));
}
fn register_snippets(&self, path: &PathBuf, snippet_contents: &str) -> Result<()> {

View File

@@ -1,135 +0,0 @@
use std::sync::{atomic::AtomicBool, Arc};
use anyhow::{anyhow, Result};
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
SlashCommandResult,
};
use futures::FutureExt as _;
use gpui::{Task, WeakView, WindowContext};
use language::{BufferSnapshot, LspAdapterDelegate};
use ui::prelude::*;
use wasmtime_wasi::WasiView;
use workspace::Workspace;
use extension_host::wasm_host::{WasmExtension, WasmHost};
pub struct ExtensionSlashCommand {
pub(crate) extension: WasmExtension,
#[allow(unused)]
pub(crate) host: Arc<WasmHost>,
pub(crate) command: extension_host::wasm_host::SlashCommand,
}
impl SlashCommand for ExtensionSlashCommand {
fn name(&self) -> String {
self.command.name.clone()
}
fn description(&self) -> String {
self.command.description.clone()
}
fn menu_text(&self) -> String {
self.command.tooltip_text.clone()
}
fn requires_argument(&self) -> bool {
self.command.requires_argument
}
fn complete_argument(
self: Arc<Self>,
arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
let arguments = arguments.to_owned();
cx.background_executor().spawn(async move {
self.extension
.call({
let this = self.clone();
move |extension, store| {
async move {
let completions = extension
.call_complete_slash_command_argument(
store,
&this.command,
&arguments,
)
.await?
.map_err(|e| anyhow!("{}", e))?;
anyhow::Ok(
completions
.into_iter()
.map(|completion| ArgumentCompletion {
label: completion.label.into(),
new_text: completion.new_text,
replace_previous_arguments: false,
after_completion: completion.run_command.into(),
})
.collect(),
)
}
.boxed()
}
})
.await
})
}
fn run(
self: Arc<Self>,
arguments: &[String],
_context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
_context_buffer: BufferSnapshot,
_workspace: WeakView<Workspace>,
delegate: Option<Arc<dyn LspAdapterDelegate>>,
cx: &mut WindowContext,
) -> Task<SlashCommandResult> {
let arguments = arguments.to_owned();
let output = cx.background_executor().spawn(async move {
self.extension
.call({
let this = self.clone();
move |extension, store| {
async move {
let resource = if let Some(delegate) = delegate {
Some(store.data_mut().table().push(delegate)?)
} else {
None
};
let output = extension
.call_run_slash_command(store, &this.command, &arguments, resource)
.await?
.map_err(|e| anyhow!("{}", e))?;
anyhow::Ok(output)
}
.boxed()
}
})
.await
});
cx.foreground_executor().spawn(async move {
let output = output.await?;
Ok(SlashCommandOutput {
text: output.text,
sections: output
.sections
.into_iter()
.map(|section| SlashCommandOutputSection {
range: section.range.into(),
icon: IconName::Code,
label: section.label.into(),
metadata: None,
})
.collect(),
run_commands_in_text: false,
}
.to_event_stream())
})
}
}

View File

@@ -268,7 +268,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
let slash_command_registry = SlashCommandRegistry::new();
let indexed_docs_registry = Arc::new(IndexedDocsRegistry::new(cx.executor()));
let snippet_registry = Arc::new(SnippetRegistry::new());
let context_server_factory_registry = ContextServerFactoryRegistry::new();
let context_server_factory_registry = cx.new_model(|_| ContextServerFactoryRegistry::new());
let node_runtime = NodeRuntime::unavailable();
let store = cx.new_model(|cx| {
@@ -508,7 +508,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
let slash_command_registry = SlashCommandRegistry::new();
let indexed_docs_registry = Arc::new(IndexedDocsRegistry::new(cx.executor()));
let snippet_registry = Arc::new(SnippetRegistry::new());
let context_server_factory_registry = ContextServerFactoryRegistry::new();
let context_server_factory_registry = cx.new_model(|_| ContextServerFactoryRegistry::new());
let node_runtime = NodeRuntime::unavailable();
let mut status_updates = language_registry.language_server_binary_statuses();

View File

@@ -1,8 +1,5 @@
mod components;
mod extension_context_server;
mod extension_indexed_docs_provider;
mod extension_registration_hooks;
mod extension_slash_command;
mod extension_suggest;
mod extension_version_selector;

View File

@@ -15,8 +15,8 @@ use file_icons::FileIcons;
use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
use gpui::{
actions, rems, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle,
FocusableView, Model, Modifiers, ModifiersChangedEvent, ParentElement, Render, Styled, Task,
View, ViewContext, VisualContext, WeakView,
FocusableView, KeyContext, Model, Modifiers, ModifiersChangedEvent, ParentElement, Render,
Styled, Task, View, ViewContext, VisualContext, WeakView,
};
use new_path_prompt::NewPathPrompt;
use open_path_prompt::OpenPathPrompt;
@@ -32,16 +32,30 @@ use std::{
},
};
use text::Point;
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
use ui::{
prelude::*, ContextMenu, HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, PopoverMenu,
PopoverMenuHandle,
};
use util::{paths::PathWithPosition, post_inc, ResultExt};
use workspace::{item::PreviewTabsSettings, notifications::NotifyResultExt, ModalView, Workspace};
use workspace::{
item::PreviewTabsSettings, notifications::NotifyResultExt, pane, ModalView, SplitDirection,
Workspace,
};
actions!(file_finder, [SelectPrev]);
actions!(file_finder, [SelectPrev, OpenMenu]);
impl ModalView for FileFinder {}
impl ModalView for FileFinder {
fn on_before_dismiss(&mut self, cx: &mut ViewContext<Self>) -> workspace::DismissDecision {
let submenu_focused = self.picker.update(cx, |picker, cx| {
picker.delegate.popover_menu_handle.is_focused(cx)
});
workspace::DismissDecision::Dismiss(!submenu_focused)
}
}
pub struct FileFinder {
picker: View<Picker<FileFinderDelegate>>,
picker_focus_handle: FocusHandle,
init_modifiers: Option<Modifiers>,
}
@@ -142,8 +156,14 @@ impl FileFinder {
}
fn new(delegate: FileFinderDelegate, cx: &mut ViewContext<Self>) -> Self {
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
let picker_focus_handle = picker.focus_handle(cx);
picker.update(cx, |picker, _| {
picker.delegate.focus_handle = picker_focus_handle.clone();
});
Self {
picker: cx.new_view(|cx| Picker::uniform_list(delegate, cx)),
picker,
picker_focus_handle,
init_modifiers: cx.modifiers().modified().then_some(cx.modifiers()),
}
}
@@ -168,23 +188,85 @@ impl FileFinder {
self.init_modifiers = Some(cx.modifiers());
cx.dispatch_action(Box::new(menu::SelectPrev));
}
fn handle_open_menu(&mut self, _: &OpenMenu, cx: &mut ViewContext<Self>) {
self.picker.update(cx, |picker, cx| {
let menu_handle = &picker.delegate.popover_menu_handle;
if !menu_handle.is_deployed() {
menu_handle.show(cx);
}
});
}
fn go_to_file_split_left(&mut self, _: &pane::SplitLeft, cx: &mut ViewContext<Self>) {
self.go_to_file_split_inner(SplitDirection::Left, cx)
}
fn go_to_file_split_right(&mut self, _: &pane::SplitRight, cx: &mut ViewContext<Self>) {
self.go_to_file_split_inner(SplitDirection::Right, cx)
}
fn go_to_file_split_up(&mut self, _: &pane::SplitUp, cx: &mut ViewContext<Self>) {
self.go_to_file_split_inner(SplitDirection::Up, cx)
}
fn go_to_file_split_down(&mut self, _: &pane::SplitDown, cx: &mut ViewContext<Self>) {
self.go_to_file_split_inner(SplitDirection::Down, cx)
}
fn go_to_file_split_inner(
&mut self,
split_direction: SplitDirection,
cx: &mut ViewContext<Self>,
) {
self.picker.update(cx, |picker, cx| {
let delegate = &mut picker.delegate;
if let Some(workspace) = delegate.workspace.upgrade() {
if let Some(m) = delegate.matches.get(delegate.selected_index()) {
let path = match &m {
Match::History { path, .. } => {
let worktree_id = path.project.worktree_id;
ProjectPath {
worktree_id,
path: Arc::clone(&path.project.path),
}
}
Match::Search(m) => ProjectPath {
worktree_id: WorktreeId::from_usize(m.0.worktree_id),
path: m.0.path.clone(),
},
};
let open_task = workspace.update(cx, move |workspace, cx| {
workspace.split_path_preview(path, false, Some(split_direction), cx)
});
open_task.detach_and_log_err(cx);
}
}
})
}
}
impl EventEmitter<DismissEvent> for FileFinder {}
impl FocusableView for FileFinder {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx)
fn focus_handle(&self, _: &AppContext) -> FocusHandle {
self.picker_focus_handle.clone()
}
}
impl Render for FileFinder {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let key_context = self.picker.read(cx).delegate.key_context(cx);
v_flex()
.key_context("FileFinder")
.key_context(key_context)
.w(rems(34.))
.on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
.on_action(cx.listener(Self::handle_select_prev))
.on_action(cx.listener(Self::handle_open_menu))
.on_action(cx.listener(Self::go_to_file_split_left))
.on_action(cx.listener(Self::go_to_file_split_right))
.on_action(cx.listener(Self::go_to_file_split_up))
.on_action(cx.listener(Self::go_to_file_split_down))
.child(self.picker.clone())
}
}
@@ -205,6 +287,8 @@ pub struct FileFinderDelegate {
history_items: Vec<FoundPath>,
separate_history: bool,
first_update: bool,
popover_menu_handle: PopoverMenuHandle<ContextMenu>,
focus_handle: FocusHandle,
}
/// Use a custom ordering for file finder: the regular one
@@ -533,6 +617,8 @@ impl FileFinderDelegate {
history_items,
separate_history,
first_update: true,
popover_menu_handle: PopoverMenuHandle::default(),
focus_handle: cx.focus_handle(),
}
}
@@ -845,6 +931,15 @@ impl FileFinderDelegate {
0
}
fn key_context(&self, cx: &WindowContext) -> KeyContext {
let mut key_context = KeyContext::new_with_defaults();
key_context.add("FileFinder");
if self.popover_menu_handle.is_focused(cx) {
key_context.add("menu_open");
}
key_context
}
}
impl PickerDelegate for FileFinderDelegate {
@@ -958,7 +1053,7 @@ impl PickerDelegate for FileFinderDelegate {
let allow_preview =
PreviewTabsSettings::get_global(cx).enable_preview_from_file_finder;
if secondary {
workspace.split_path_preview(project_path, allow_preview, cx)
workspace.split_path_preview(project_path, allow_preview, None, cx)
} else {
workspace.open_path_preview(
project_path,
@@ -1125,6 +1220,49 @@ impl PickerDelegate for FileFinderDelegate {
),
)
}
fn render_footer(&self, cx: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
Some(
h_flex()
.w_full()
.p_2()
.gap_2()
.justify_end()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.child(
Button::new("open-selection", "Open")
.key_binding(KeyBinding::for_action(&menu::Confirm, cx))
.on_click(|_, cx| cx.dispatch_action(menu::Confirm.boxed_clone())),
)
.child(
PopoverMenu::new("menu-popover")
.with_handle(self.popover_menu_handle.clone())
.attach(gpui::AnchorCorner::TopRight)
.anchor(gpui::AnchorCorner::BottomRight)
.trigger(
Button::new("actions-trigger", "Split Options")
.selected_label_color(Color::Accent)
.key_binding(KeyBinding::for_action_in(
&OpenMenu,
&self.focus_handle,
cx,
)),
)
.menu({
move |cx| {
Some(ContextMenu::build(cx, move |menu, _| {
menu.action("Split Left", pane::SplitLeft.boxed_clone())
.action("Split Right", pane::SplitRight.boxed_clone())
.action("Split Up", pane::SplitUp.boxed_clone())
.action("Split Down", pane::SplitDown.boxed_clone())
}))
}
}),
)
.into_any(),
)
}
}
#[cfg(test)]

View File

@@ -580,9 +580,9 @@ impl Render for InputExample {
.children(self.recent_keystrokes.iter().rev().map(|ks| {
format!(
"{:} {}",
ks,
ks.unparse(),
if let Some(ime_key) = ks.ime_key.as_ref() {
format!("-> {}", ime_key)
format!("-> {:?}", ime_key)
} else {
"".to_owned()
}

View File

@@ -511,7 +511,7 @@ impl Interactivity {
}
/// Block the mouse from interacting with this element or any of its children
/// The imperative API equivalent to [`InteractiveElement::block_mouse`]
/// The imperative API equivalent to [`InteractiveElement::occlude`]
pub fn occlude_mouse(&mut self) {
self.occlude_mouse = true;
}
@@ -874,11 +874,17 @@ pub trait InteractiveElement: Sized {
}
/// Block the mouse from interacting with this element or any of its children
/// The fluent API equivalent to [`Interactivity::block_mouse`]
/// The fluent API equivalent to [`Interactivity::occlude_mouse`]
fn occlude(mut self) -> Self {
self.interactivity().occlude_mouse();
self
}
/// Block the mouse from interacting with this element or any of its children
/// The fluent API equivalent to [`Interactivity::occlude_mouse`]
fn block_mouse_down(mut self) -> Self {
self.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
}
}
/// A trait for elements that want to use the standard GPUI interactivity features

View File

@@ -688,6 +688,11 @@ impl PlatformInputHandler {
.flatten()
}
#[allow(dead_code)]
fn apple_press_and_hold_enabled(&mut self) -> bool {
self.handler.apple_press_and_hold_enabled()
}
pub(crate) fn dispatch_input(&mut self, input: &str, cx: &mut WindowContext) {
self.handler.replace_text_in_range(None, input, cx);
}
@@ -785,6 +790,15 @@ pub trait InputHandler: 'static {
range_utf16: Range<usize>,
cx: &mut WindowContext,
) -> Option<Bounds<Pixels>>;
/// Allows a given input context to opt into getting raw key repeats instead of
/// sending these to the platform.
/// TODO: Ideally we should be able to set ApplePressAndHoldEnabled in NSUserDefaults
/// (which is how iTerm does it) but it doesn't seem to work for me.
#[allow(dead_code)]
fn apple_press_and_hold_enabled(&mut self) -> bool {
true
}
}
/// The variables that can be configured when creating a new window

View File

@@ -34,6 +34,7 @@ impl Keystroke {
{
let ime_modifiers = Modifiers {
control: self.modifiers.control,
platform: self.modifiers.platform,
..Default::default()
};
@@ -124,6 +125,9 @@ impl Keystroke {
/// Produces a representation of this key that Parse can understand.
pub fn unparse(&self) -> String {
let mut str = String::new();
if self.modifiers.function {
str.push_str("fn-");
}
if self.modifiers.control {
str.push_str("ctrl-");
}

View File

@@ -1,20 +1,20 @@
use crate::{
platform::mac::NSStringExt, point, px, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers,
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent,
MouseUpEvent, NavigationDirection, Pixels, PlatformInput, ScrollDelta, ScrollWheelEvent,
TouchPhase,
platform::mac::{
kTISPropertyUnicodeKeyLayoutData, LMGetKbdType, NSStringExt,
TISCopyCurrentKeyboardLayoutInputSource, TISGetInputSourceProperty, UCKeyTranslate,
},
point, px, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton,
MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels,
PlatformInput, ScrollDelta, ScrollWheelEvent, TouchPhase,
};
use cocoa::{
appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType},
base::{id, YES},
};
use core_graphics::{
event::{CGEvent, CGEventFlags, CGKeyCode},
event_source::{CGEventSource, CGEventSourceStateID},
};
use metal::foreign_types::ForeignType as _;
use objc::{class, msg_send, sel, sel_impl};
use std::{borrow::Cow, mem, ptr, sync::Once};
use core_foundation::data::{CFDataGetBytePtr, CFDataRef};
use core_graphics::event::CGKeyCode;
use objc::{msg_send, sel, sel_impl};
use std::{borrow::Cow, ffi::c_void};
const BACKSPACE_KEY: u16 = 0x7f;
const SPACE_KEY: u16 = b' ' as u16;
@@ -24,24 +24,6 @@ const ESCAPE_KEY: u16 = 0x1b;
const TAB_KEY: u16 = 0x09;
const SHIFT_TAB_KEY: u16 = 0x19;
fn synthesize_keyboard_event(code: CGKeyCode) -> CGEvent {
static mut EVENT_SOURCE: core_graphics::sys::CGEventSourceRef = ptr::null_mut();
static INIT_EVENT_SOURCE: Once = Once::new();
INIT_EVENT_SOURCE.call_once(|| {
let source = CGEventSource::new(CGEventSourceStateID::Private).unwrap();
unsafe {
EVENT_SOURCE = source.as_ptr();
};
mem::forget(source);
});
let source = unsafe { core_graphics::event_source::CGEventSource::from_ptr(EVENT_SOURCE) };
let event = CGEvent::new_keyboard_event(source.clone(), code, true).unwrap();
mem::forget(source);
event
}
pub fn key_to_native(key: &str) -> Cow<str> {
use cocoa::appkit::*;
let code = match key {
@@ -259,8 +241,12 @@ impl PlatformInput {
unsafe fn parse_keystroke(native_event: id) -> Keystroke {
use cocoa::appkit::*;
let mut chars_ignoring_modifiers = chars_for_modified_key(native_event.keyCode(), false, false);
let first_char = chars_ignoring_modifiers.chars().next().map(|ch| ch as u16);
let mut characters = native_event
.charactersIgnoringModifiers()
.to_str()
.to_string();
let mut ime_key = None;
let first_char = characters.chars().next().map(|ch| ch as u16);
let modifiers = native_event.modifierFlags();
let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask);
@@ -313,7 +299,7 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
_ => {
// Cases to test when modifying this:
//
// qwerty key | none | cmd | cmd-shift
// qwerty key | none | cmd | cmd-shift
// * Armenian s | ս | cmd-s | cmd-shift-s (layout is non-ASCII, so we use cmd layout)
// * Dvorak+QWERTY s | o | cmd-s | cmd-shift-s (layout switches on cmd)
// * Ukrainian+QWERTY s | с | cmd-s | cmd-shift-s (macOS reports cmd-s instead of cmd-S)
@@ -321,12 +307,17 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
// * Norwegian 7 | 7 | cmd-7 | cmd-/ (macOS reports cmd-shift-7 instead of cmd-/)
// * Russian 7 | 7 | cmd-7 | cmd-& (shift-7 is . but when cmd is down, should use cmd layout)
// * German QWERTZ ; | ö | cmd-ö | cmd-Ö (Zed's shift special case only applies to a-z)
let mut chars_with_shift = chars_for_modified_key(native_event.keyCode(), false, true);
//
let mut chars_ignoring_modifiers =
chars_for_modified_key(native_event.keyCode(), NO_MOD);
let mut chars_with_shift = chars_for_modified_key(native_event.keyCode(), SHIFT_MOD);
let always_use_cmd_layout = always_use_command_layout();
// Handle Dvorak+QWERTY / Russian / Armeniam
if command || always_use_command_layout() {
let chars_with_cmd = chars_for_modified_key(native_event.keyCode(), true, false);
let chars_with_both = chars_for_modified_key(native_event.keyCode(), true, true);
if command || always_use_cmd_layout {
let chars_with_cmd = chars_for_modified_key(native_event.keyCode(), CMD_MOD);
let chars_with_both =
chars_for_modified_key(native_event.keyCode(), CMD_MOD | SHIFT_MOD);
// We don't do this in the case that the shifted command key generates
// the same character as the unshifted command key (Norwegian, e.g.)
@@ -341,14 +332,34 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
chars_ignoring_modifiers = chars_with_cmd;
}
if shift && chars_ignoring_modifiers == chars_with_shift.to_ascii_lowercase() {
let mut key = if shift
&& chars_ignoring_modifiers
.chars()
.all(|c| c.is_ascii_lowercase())
{
chars_ignoring_modifiers
} else if shift {
shift = false;
chars_with_shift
} else {
chars_ignoring_modifiers
}
};
if always_use_cmd_layout || alt {
let mut mods = NO_MOD;
if shift {
mods |= SHIFT_MOD;
}
if alt {
mods |= OPTION_MOD;
}
let alt_key = chars_for_modified_key(native_event.keyCode(), mods);
if alt_key != key {
ime_key = Some(alt_key);
}
};
key
}
};
@@ -361,50 +372,83 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
function,
},
key,
ime_key: None,
ime_key,
}
}
fn always_use_command_layout() -> bool {
// look at the key to the right of "tab" ('a' in QWERTY)
// if it produces a non-ASCII character, but with command held produces ASCII,
// we default to the command layout for our keyboard system.
let event = synthesize_keyboard_event(0);
let without_cmd = unsafe {
let event: id = msg_send![class!(NSEvent), eventWithCGEvent: &*event];
event.characters().to_str().to_string()
};
if without_cmd.is_ascii() {
if chars_for_modified_key(0, NO_MOD).is_ascii() {
return false;
}
event.set_flags(CGEventFlags::CGEventFlagCommand);
let with_cmd = unsafe {
let event: id = msg_send![class!(NSEvent), eventWithCGEvent: &*event];
event.characters().to_str().to_string()
};
with_cmd.is_ascii()
chars_for_modified_key(0, CMD_MOD).is_ascii()
}
fn chars_for_modified_key(code: CGKeyCode, cmd: bool, shift: bool) -> String {
// Ideally, we would use `[NSEvent charactersByApplyingModifiers]` but that
// always returns an empty string with certain keyboards, e.g. Japanese. Synthesizing
// an event with the given flags instead lets us access `characters`, which always
// returns a valid string.
let event = synthesize_keyboard_event(code);
const NO_MOD: u32 = 0;
const CMD_MOD: u32 = 1;
const SHIFT_MOD: u32 = 2;
const OPTION_MOD: u32 = 8;
let mut flags = CGEventFlags::empty();
if cmd {
flags |= CGEventFlags::CGEventFlagCommand;
fn chars_for_modified_key(code: CGKeyCode, modifiers: u32) -> String {
// Values from: https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.6.sdk/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h#L126
// shifted >> 8 for UCKeyTranslate
const CG_SPACE_KEY: u16 = 49;
// https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.6.sdk/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/Headers/UnicodeUtilities.h#L278
#[allow(non_upper_case_globals)]
const kUCKeyActionDown: u16 = 0;
#[allow(non_upper_case_globals)]
const kUCKeyTranslateNoDeadKeysMask: u32 = 0;
let keyboard_type = unsafe { LMGetKbdType() as u32 };
const BUFFER_SIZE: usize = 4;
let mut dead_key_state = 0;
let mut buffer: [u16; BUFFER_SIZE] = [0; BUFFER_SIZE];
let mut buffer_size: usize = 0;
let keyboard = unsafe { TISCopyCurrentKeyboardLayoutInputSource() };
if keyboard.is_null() {
return "".to_string();
}
if shift {
flags |= CGEventFlags::CGEventFlagShift;
let layout_data = unsafe {
TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData as *const c_void)
as CFDataRef
};
if layout_data.is_null() {
unsafe {
let _: () = msg_send![keyboard, release];
}
return "".to_string();
}
event.set_flags(flags);
let keyboard_layout = unsafe { CFDataGetBytePtr(layout_data) };
unsafe {
let event: id = msg_send![class!(NSEvent), eventWithCGEvent: &*event];
event.characters().to_str().to_string()
UCKeyTranslate(
keyboard_layout as *const c_void,
code,
kUCKeyActionDown,
modifiers,
keyboard_type,
kUCKeyTranslateNoDeadKeysMask,
&mut dead_key_state,
BUFFER_SIZE,
&mut buffer_size as *mut usize,
&mut buffer as *mut u16,
);
if dead_key_state != 0 {
UCKeyTranslate(
keyboard_layout as *const c_void,
CG_SPACE_KEY,
kUCKeyActionDown,
modifiers,
keyboard_type,
kUCKeyTranslateNoDeadKeysMask,
&mut dead_key_state,
BUFFER_SIZE,
&mut buffer_size as *mut usize,
&mut buffer as *mut u16,
);
}
let _: () = msg_send![keyboard, release];
}
String::from_utf16(&buffer[..buffer_size]).unwrap_or_default()
}

View File

@@ -19,7 +19,7 @@ use cocoa::{
NSPasteboardTypePNG, NSPasteboardTypeRTF, NSPasteboardTypeRTFD, NSPasteboardTypeString,
NSPasteboardTypeTIFF, NSSavePanel, NSWindow,
},
base::{id, nil, selector, BOOL, YES},
base::{id, nil, selector, BOOL, NO, YES},
foundation::{
NSArray, NSAutoreleasePool, NSBundle, NSData, NSInteger, NSProcessInfo, NSRange, NSString,
NSUInteger, NSURL,
@@ -343,6 +343,8 @@ impl MacPlatform {
ns_string(key_to_native(&keystroke.key).as_ref()),
)
.autorelease();
let _: () =
msg_send![item, setAllowsAutomaticKeyEquivalentLocalization: NO];
item.setKeyEquivalentModifierMask_(mask);
}
// For multi-keystroke bindings, render the keystroke as part of the title.
@@ -1448,13 +1450,27 @@ unsafe fn ns_url_to_path(url: id) -> Result<PathBuf> {
#[link(name = "Carbon", kind = "framework")]
extern "C" {
fn TISCopyCurrentKeyboardLayoutInputSource() -> *mut Object;
fn TISGetInputSourceProperty(
pub(super) fn TISCopyCurrentKeyboardLayoutInputSource() -> *mut Object;
pub(super) fn TISGetInputSourceProperty(
inputSource: *mut Object,
propertyKey: *const c_void,
) -> *mut Object;
pub static kTISPropertyInputSourceID: CFStringRef;
pub(super) fn UCKeyTranslate(
keyLayoutPtr: *const ::std::os::raw::c_void,
virtualKeyCode: u16,
keyAction: u16,
modifierKeyState: u32,
keyboardType: u32,
keyTranslateOptions: u32,
deadKeyState: *mut u32,
maxStringLength: usize,
actualStringLength: *mut usize,
unicodeString: *mut u16,
) -> u32;
pub(super) fn LMGetKbdType() -> u16;
pub(super) static kTISPropertyUnicodeKeyLayoutData: CFStringRef;
pub(super) static kTISPropertyInputSourceID: CFStringRef;
}
mod security {

View File

@@ -38,7 +38,6 @@ use std::{
cell::Cell,
ffi::{c_void, CStr},
mem,
ops::Range,
path::PathBuf,
ptr::{self, NonNull},
rc::Rc,
@@ -310,14 +309,6 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C
decl.register()
}
#[allow(clippy::enum_variant_names)]
#[derive(Clone, Debug)]
enum ImeInput {
InsertText(String, Option<Range<usize>>),
SetMarkedText(String, Option<Range<usize>>, Option<Range<usize>>),
UnmarkText,
}
struct MacWindowState {
handle: AnyWindowHandle,
executor: ForegroundExecutor,
@@ -338,14 +329,11 @@ struct MacWindowState {
synthetic_drag_counter: usize,
traffic_light_position: Option<Point<Pixels>>,
previous_modifiers_changed_event: Option<PlatformInput>,
// State tracking what the IME did after the last request
last_ime_inputs: Option<SmallVec<[(String, Option<Range<usize>>); 1]>>,
previous_keydown_inserted_text: Option<String>,
keystroke_for_do_command: Option<Keystroke>,
external_files_dragged: bool,
// Whether the next left-mouse click is also the focusing click.
first_mouse: bool,
fullscreen_restore_bounds: Bounds<Pixels>,
ime_composing: bool,
}
impl MacWindowState {
@@ -619,12 +607,10 @@ impl MacWindow {
.as_ref()
.and_then(|titlebar| titlebar.traffic_light_position),
previous_modifiers_changed_event: None,
last_ime_inputs: None,
previous_keydown_inserted_text: None,
keystroke_for_do_command: None,
external_files_dragged: false,
first_mouse: false,
fullscreen_restore_bounds: Bounds::default(),
ime_composing: false,
})));
(*native_window).set_ivar(
@@ -1226,9 +1212,9 @@ extern "C" fn handle_key_down(this: &Object, _: Sel, native_event: id) {
// Brazilian layout:
// - `" space` should create an unmarked quote
// - `" backspace` should delete the marked quote
// - `" "`should create an unmarked quote and a second marked quote
// - `" up` should insert a quote, unmark it, and move up one line
// - `" cmd-down` should insert a quote, unmark it, and move to the end of the file
// - NOTE: The current implementation does not move the selection to the end of the file
// - `cmd-ctrl-space` and clicking on an emoji should type it
// Czech (QWERTY) layout:
// - in vim mode `option-4` should go to end of line (same as $)
@@ -1241,95 +1227,86 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
let window_height = lock.content_size().height;
let event = unsafe { PlatformInput::from_native(native_event, Some(window_height)) };
if let Some(PlatformInput::KeyDown(mut event)) = event {
// For certain keystrokes, macOS will first dispatch a "key equivalent" event.
// If that event isn't handled, it will then dispatch a "key down" event. GPUI
// makes no distinction between these two types of events, so we need to ignore
// the "key down" event if we've already just processed its "key equivalent" version.
if key_equivalent {
lock.last_key_equivalent = Some(event.clone());
} else if lock.last_key_equivalent.take().as_ref() == Some(&event) {
return NO;
let Some(PlatformInput::KeyDown(mut event)) = event else {
return NO;
};
// For certain keystrokes, macOS will first dispatch a "key equivalent" event.
// If that event isn't handled, it will then dispatch a "key down" event. GPUI
// makes no distinction between these two types of events, so we need to ignore
// the "key down" event if we've already just processed its "key equivalent" version.
if key_equivalent {
lock.last_key_equivalent = Some(event.clone());
} else if lock.last_key_equivalent.take().as_ref() == Some(&event) {
return NO;
}
drop(lock);
let is_composing = with_input_handler(this, |input_handler| input_handler.marked_text_range())
.flatten()
.is_some();
// If we're composing, send the key to the input handler first;
// otherwise we only send to the input handler if we don't have a matching binding.
// The input handler may call `do_command_by_selector` if it doesn't know how to handle
// a key. If it does so, it will return YES so we won't send the key twice.
if is_composing || event.keystroke.key.is_empty() {
window_state.as_ref().lock().keystroke_for_do_command = Some(event.keystroke.clone());
let handled: BOOL = unsafe {
let input_context: id = msg_send![this, inputContext];
msg_send![input_context, handleEvent: native_event]
};
window_state.as_ref().lock().keystroke_for_do_command.take();
if handled == YES {
return YES;
}
let keydown = event.keystroke.clone();
let fn_modifier = keydown.modifiers.function;
lock.last_ime_inputs = Some(Default::default());
drop(lock);
let mut callback = window_state.as_ref().lock().event_callback.take();
let handled: BOOL = if let Some(callback) = callback.as_mut() {
!callback(PlatformInput::KeyDown(event)).propagate as BOOL
} else {
NO
};
window_state.as_ref().lock().event_callback = callback;
return handled as BOOL;
}
// Send the event to the input context for IME handling, unless the `fn` modifier is
// being pressed.
// this will call back into `insert_text`, etc.
if !fn_modifier {
unsafe {
let input_context: id = msg_send![this, inputContext];
let _: BOOL = msg_send![input_context, handleEvent: native_event];
}
}
let mut handled = false;
let mut lock = window_state.lock();
let previous_keydown_inserted_text = lock.previous_keydown_inserted_text.take();
let mut last_inserts = lock.last_ime_inputs.take().unwrap();
let ime_composing = std::mem::take(&mut lock.ime_composing);
let mut callback = lock.event_callback.take();
drop(lock);
let last_insert = last_inserts.pop();
// on a brazilian keyboard typing `"` and then hitting `up` will cause two IME
// events, one to unmark the quote, and one to send the up arrow.
for (text, range) in last_inserts {
send_to_input_handler(this, ImeInput::InsertText(text, range));
}
let is_composing =
with_input_handler(this, |input_handler| input_handler.marked_text_range())
.flatten()
.is_some()
|| ime_composing;
if let Some((text, range)) = last_insert {
if !is_composing {
window_state.lock().previous_keydown_inserted_text = Some(text.clone());
if let Some(callback) = callback.as_mut() {
event.keystroke.ime_key = Some(text.clone());
handled = !callback(PlatformInput::KeyDown(event)).propagate;
}
}
if !handled {
handled = true;
send_to_input_handler(this, ImeInput::InsertText(text, range));
}
} else if !is_composing {
let is_held = event.is_held;
if let Some(callback) = callback.as_mut() {
handled = !callback(PlatformInput::KeyDown(event)).propagate;
}
if !handled && is_held {
if let Some(text) = previous_keydown_inserted_text {
// macOS IME is a bit funky, and even when you've told it there's nothing to
// enter it will still swallow certain keys (e.g. 'f', 'j') and not others
// (e.g. 'n'). This is a problem for certain kinds of views, like the terminal.
with_input_handler(this, |input_handler| {
if input_handler.selected_text_range(false).is_none() {
handled = true;
input_handler.replace_text_in_range(None, &text)
}
});
window_state.lock().previous_keydown_inserted_text = Some(text);
}
}
}
window_state.lock().event_callback = callback;
handled as BOOL
let mut callback = window_state.as_ref().lock().event_callback.take();
let handled = if let Some(callback) = callback.as_mut() {
!callback(PlatformInput::KeyDown(event.clone())).propagate as BOOL
} else {
NO
};
window_state.as_ref().lock().event_callback = callback;
if handled == YES {
return YES;
}
if event.is_held {
let handled = with_input_handler(&this, |input_handler| {
if !input_handler.apple_press_and_hold_enabled() {
input_handler.replace_text_in_range(
None,
&event.keystroke.ime_key.unwrap_or(event.keystroke.key),
);
return YES;
}
NO
});
if handled == Some(YES) {
return YES;
}
}
// Don't send key equivalents to the input handler,
// or macOS shortcuts like cmd-` will stop working.
if key_equivalent {
return NO;
}
unsafe {
let input_context: id = msg_send![this, inputContext];
msg_send![input_context, handleEvent: native_event]
}
}
@@ -1741,10 +1718,9 @@ extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NS
let text = text.to_str();
let replacement_range = replacement_range.to_range();
send_to_input_handler(
this,
ImeInput::InsertText(text.to_string(), replacement_range),
);
with_input_handler(this, |input_handler| {
input_handler.replace_text_in_range(replacement_range, &text)
});
}
}
@@ -1766,15 +1742,13 @@ extern "C" fn set_marked_text(
let selected_range = selected_range.to_range();
let replacement_range = replacement_range.to_range();
let text = text.to_str();
send_to_input_handler(
this,
ImeInput::SetMarkedText(text.to_string(), replacement_range, selected_range),
);
with_input_handler(this, |input_handler| {
input_handler.replace_and_mark_text_in_range(replacement_range, &text, selected_range)
});
}
}
extern "C" fn unmark_text(this: &Object, _: Sel) {
send_to_input_handler(this, ImeInput::UnmarkText);
with_input_handler(this, |input_handler| input_handler.unmark_text());
}
extern "C" fn attributed_substring_for_proposed_range(
@@ -1800,7 +1774,24 @@ extern "C" fn attributed_substring_for_proposed_range(
.unwrap_or(nil)
}
extern "C" fn do_command_by_selector(_: &Object, _: Sel, _: Sel) {}
// We ignore which selector it asks us to do because the user may have
// bound the shortcut to something else.
extern "C" fn do_command_by_selector(this: &Object, _: Sel, _: Sel) {
let state = unsafe { get_window_state(this) };
let mut lock = state.as_ref().lock();
let keystroke = lock.keystroke_for_do_command.take();
let mut event_callback = lock.event_callback.take();
drop(lock);
if let Some((keystroke, mut callback)) = keystroke.zip(event_callback.as_mut()) {
(callback)(PlatformInput::KeyDown(KeyDownEvent {
keystroke,
is_held: false,
}));
}
state.as_ref().lock().event_callback = event_callback;
}
extern "C" fn view_did_change_effective_appearance(this: &Object, _: Sel) {
unsafe {
@@ -1950,43 +1941,6 @@ where
}
}
fn send_to_input_handler(window: &Object, ime: ImeInput) {
unsafe {
let window_state = get_window_state(window);
let mut lock = window_state.lock();
if let Some(mut input_handler) = lock.input_handler.take() {
match ime {
ImeInput::InsertText(text, range) => {
if let Some(ime_input) = lock.last_ime_inputs.as_mut() {
ime_input.push((text, range));
lock.input_handler = Some(input_handler);
return;
}
drop(lock);
input_handler.replace_text_in_range(range, &text)
}
ImeInput::SetMarkedText(text, range, marked_range) => {
lock.ime_composing = true;
drop(lock);
input_handler.replace_and_mark_text_in_range(range, &text, marked_range)
}
ImeInput::UnmarkText => {
drop(lock);
input_handler.unmark_text()
}
}
window_state.lock().input_handler = Some(input_handler);
} else {
if let ImeInput::InsertText(text, range) = ime {
if let Some(ime_input) = lock.last_ime_inputs.as_mut() {
ime_input.push((text, range));
}
}
}
}
}
unsafe fn display_id_for_screen(screen: id) -> CGDirectDisplayID {
let device_description = NSScreen::deviceDescription(screen);
let screen_number_key: id = NSString::alloc(nil).init_str("NSScreenNumber");

View File

@@ -17,6 +17,7 @@ async-trait.workspace = true
cargo_metadata.workspace = true
collections.workspace = true
derive_more.workspace = true
extension.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
@@ -30,7 +31,6 @@ paths.workspace = true
serde.workspace = true
strum.workspace = true
util.workspace = true
extension_host.workspace = true
[dev-dependencies]
indoc.workspace = true

View File

@@ -0,0 +1,53 @@
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::Result;
use async_trait::async_trait;
use extension::Extension;
use crate::{IndexedDocsDatabase, IndexedDocsProvider, PackageName, ProviderId};
pub struct ExtensionIndexedDocsProvider {
extension: Arc<dyn Extension>,
id: ProviderId,
}
impl ExtensionIndexedDocsProvider {
pub fn new(extension: Arc<dyn Extension>, id: ProviderId) -> Self {
Self { extension, id }
}
}
#[async_trait]
impl IndexedDocsProvider for ExtensionIndexedDocsProvider {
fn id(&self) -> ProviderId {
self.id.clone()
}
fn database_path(&self) -> PathBuf {
let mut database_path = PathBuf::from(self.extension.work_dir().as_ref());
database_path.push(self.extension.manifest().id.as_ref());
database_path.push("docs");
database_path.push(format!("{}.0.mdb", self.id));
database_path
}
async fn suggest_packages(&self) -> Result<Vec<PackageName>> {
let packages = self
.extension
.suggest_docs_packages(self.id.0.clone())
.await?;
Ok(packages
.into_iter()
.map(|package| PackageName::from(package.as_str()))
.collect())
}
async fn index(&self, package: PackageName, database: Arc<IndexedDocsDatabase>) -> Result<()> {
self.extension
.index_docs(self.id.0.clone(), package.as_ref().into(), database)
.await
}
}

View File

@@ -1,7 +1,9 @@
mod extension_indexed_docs_provider;
mod providers;
mod registry;
mod store;
pub use crate::extension_indexed_docs_provider::ExtensionIndexedDocsProvider;
pub use crate::providers::rustdoc::*;
pub use crate::registry::*;
pub use crate::store::*;

View File

@@ -2,7 +2,6 @@ mod item;
mod to_markdown;
use cargo_metadata::MetadataCommand;
use extension_host::DocsDatabase;
use futures::future::BoxFuture;
pub use item::*;
use parking_lot::RwLock;
@@ -209,7 +208,7 @@ impl IndexedDocsProvider for DocsDotRsProvider {
async fn index_rustdoc(
package: PackageName,
database: Arc<dyn DocsDatabase>,
database: Arc<IndexedDocsDatabase>,
fetch_page: impl Fn(&PackageName, Option<&RustdocItem>) -> BoxFuture<'static, Result<Option<String>>>
+ Send
+ Sync,

View File

@@ -324,10 +324,8 @@ impl IndexedDocsDatabase {
Ok(any)
})
}
}
impl extension_host::DocsDatabase for IndexedDocsDatabase {
fn insert(&self, key: String, docs: String) -> Task<Result<()>> {
pub fn insert(&self, key: String, docs: String) -> Task<Result<()>> {
let env = self.env.clone();
let entries = self.entries;
@@ -339,3 +337,9 @@ impl extension_host::DocsDatabase for IndexedDocsDatabase {
})
}
}
impl extension::KeyValueStoreDelegate for IndexedDocsDatabase {
fn insert(&self, key: String, docs: String) -> Task<Result<()>> {
IndexedDocsDatabase::insert(&self, key, docs)
}
}

View File

@@ -1,10 +1,10 @@
use std::{ops::Range, sync::Arc};
use crate::{Location, Runnable};
use crate::{LanguageToolchainStore, Location, Runnable};
use anyhow::Result;
use collections::HashMap;
use gpui::AppContext;
use gpui::{AppContext, Task};
use task::{TaskTemplates, TaskVariables};
use text::BufferId;
@@ -25,10 +25,11 @@ pub trait ContextProvider: Send + Sync {
&self,
_variables: &TaskVariables,
_location: &Location,
_project_env: Option<&HashMap<String, String>>,
_project_env: Option<HashMap<String, String>>,
_toolchains: Arc<dyn LanguageToolchainStore>,
_cx: &mut AppContext,
) -> Result<TaskVariables> {
Ok(TaskVariables::default())
) -> Task<Result<TaskVariables>> {
Task::ready(Ok(TaskVariables::default()))
}
/// Provides all tasks, associated with the current language.

View File

@@ -418,9 +418,10 @@ impl ContextProvider for GoContextProvider {
&self,
variables: &TaskVariables,
location: &Location,
_: Option<&HashMap<String, String>>,
_: Option<HashMap<String, String>>,
_: Arc<dyn LanguageToolchainStore>,
cx: &mut gpui::AppContext,
) -> Result<TaskVariables> {
) -> Task<Result<TaskVariables>> {
let local_abs_path = location
.buffer
.read(cx)
@@ -468,7 +469,7 @@ impl ContextProvider for GoContextProvider {
let go_subtest_variable = extract_subtest_name(_subtest_name.unwrap_or(""))
.map(|subtest_name| (GO_SUBTEST_NAME_TASK_VARIABLE.clone(), subtest_name));
Ok(TaskVariables::from_iter(
Task::ready(Ok(TaskVariables::from_iter(
[
go_package_variable,
go_subtest_variable,
@@ -476,7 +477,7 @@ impl ContextProvider for GoContextProvider {
]
.into_iter()
.flatten(),
))
)))
}
fn associated_tasks(

View File

@@ -2,8 +2,8 @@ use anyhow::ensure;
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use collections::HashMap;
use gpui::AppContext;
use gpui::AsyncAppContext;
use gpui::{AppContext, Task};
use language::LanguageName;
use language::LanguageToolchainStore;
use language::Toolchain;
@@ -267,14 +267,17 @@ pub(crate) struct PythonContextProvider;
const PYTHON_UNITTEST_TARGET_TASK_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("PYTHON_UNITTEST_TARGET"));
const PYTHON_ACTIVE_TOOLCHAIN_PATH: VariableName =
VariableName::Custom(Cow::Borrowed("PYTHON_ACTIVE_ZED_TOOLCHAIN"));
impl ContextProvider for PythonContextProvider {
fn build_context(
&self,
variables: &task::TaskVariables,
_location: &project::Location,
_: Option<&HashMap<String, String>>,
_cx: &mut gpui::AppContext,
) -> Result<task::TaskVariables> {
location: &project::Location,
_: Option<HashMap<String, String>>,
toolchains: Arc<dyn LanguageToolchainStore>,
cx: &mut gpui::AppContext,
) -> Task<Result<task::TaskVariables>> {
let python_module_name = python_module_name_from_relative_path(
variables.get(&VariableName::RelativeFile).unwrap_or(""),
);
@@ -290,15 +293,26 @@ impl ContextProvider for PythonContextProvider {
}
(Some(class_name), None) => format!("{}.{}", python_module_name, class_name),
(None, None) => python_module_name,
(None, Some(_)) => return Ok(task::TaskVariables::default()), // should never happen, a TestCase class is the unit of testing
(None, Some(_)) => return Task::ready(Ok(task::TaskVariables::default())), // should never happen, a TestCase class is the unit of testing
};
let unittest_target = (
PYTHON_UNITTEST_TARGET_TASK_VARIABLE.clone(),
unittest_target_str,
);
Ok(task::TaskVariables::from_iter([unittest_target]))
let worktree_id = location.buffer.read(cx).file().map(|f| f.worktree_id(cx));
cx.spawn(move |mut cx| async move {
let active_toolchain = if let Some(worktree_id) = worktree_id {
toolchains
.active_toolchain(worktree_id, "Python".into(), &mut cx)
.await
.map_or_else(|| "python3".to_owned(), |toolchain| toolchain.path.into())
} else {
String::from("python3")
};
let toolchain = (PYTHON_ACTIVE_TOOLCHAIN_PATH, active_toolchain);
Ok(task::TaskVariables::from_iter([unittest_target, toolchain]))
})
}
fn associated_tasks(
@@ -309,19 +323,19 @@ impl ContextProvider for PythonContextProvider {
Some(TaskTemplates(vec![
TaskTemplate {
label: "execute selection".to_owned(),
command: "python3".to_owned(),
command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
args: vec!["-c".to_owned(), VariableName::SelectedText.template_value()],
..TaskTemplate::default()
},
TaskTemplate {
label: format!("run '{}'", VariableName::File.template_value()),
command: "python3".to_owned(),
command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
args: vec![VariableName::File.template_value()],
..TaskTemplate::default()
},
TaskTemplate {
label: format!("unittest '{}'", VariableName::File.template_value()),
command: "python3".to_owned(),
command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
args: vec![
"-m".to_owned(),
"unittest".to_owned(),
@@ -331,7 +345,7 @@ impl ContextProvider for PythonContextProvider {
},
TaskTemplate {
label: "unittest $ZED_CUSTOM_PYTHON_UNITTEST_TARGET".to_owned(),
command: "python3".to_owned(),
command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
args: vec![
"-m".to_owned(),
"unittest".to_owned(),
@@ -399,6 +413,10 @@ impl ToolchainLister for PythonToolchainProvider {
);
let mut config = Configuration::default();
config.workspace_directories = Some(vec![worktree_root]);
for locator in locators.iter() {
locator.configure(&config);
}
let reporter = pet_reporter::collect::create_reporter();
pet::find::find_and_report_envs(&reporter, config, &locators, &environment, None);

View File

@@ -5,6 +5,8 @@ first_line_pattern = '^#!.*\bpython[0-9.]*\b'
line_comments = ["# "]
autoclose_before = ";:.,=}])>"
brackets = [
{ start = "\"\"\"", end = "\"\"\"", close = true, newline = false, not_in = ["string"] },
{ start = "'''", end = "'''", close = true, newline = false, not_in = ["string"] },
{ start = "{", end = "}", close = true, newline = true },
{ start = "[", end = "]", close = true, newline = true },
{ start = "(", end = ")", close = true, newline = true },

View File

@@ -3,7 +3,7 @@ use async_compression::futures::bufread::GzipDecoder;
use async_trait::async_trait;
use collections::HashMap;
use futures::{io::BufReader, StreamExt};
use gpui::{AppContext, AsyncAppContext};
use gpui::{AppContext, AsyncAppContext, Task};
use http_client::github::AssetKind;
use http_client::github::{latest_github_release, GitHubLspBinaryVersion};
pub use language::*;
@@ -424,9 +424,10 @@ impl ContextProvider for RustContextProvider {
&self,
task_variables: &TaskVariables,
location: &Location,
project_env: Option<&HashMap<String, String>>,
project_env: Option<HashMap<String, String>>,
_: Arc<dyn LanguageToolchainStore>,
cx: &mut gpui::AppContext,
) -> Result<TaskVariables> {
) -> Task<Result<TaskVariables>> {
let local_abs_path = location
.buffer
.read(cx)
@@ -440,27 +441,27 @@ impl ContextProvider for RustContextProvider {
.is_some();
if is_main_function {
if let Some((package_name, bin_name)) = local_abs_path
.and_then(|path| package_name_and_bin_name_from_abs_path(path, project_env))
{
return Ok(TaskVariables::from_iter([
if let Some((package_name, bin_name)) = local_abs_path.and_then(|path| {
package_name_and_bin_name_from_abs_path(path, project_env.as_ref())
}) {
return Task::ready(Ok(TaskVariables::from_iter([
(RUST_PACKAGE_TASK_VARIABLE.clone(), package_name),
(RUST_BIN_NAME_TASK_VARIABLE.clone(), bin_name),
]));
])));
}
}
if let Some(package_name) = local_abs_path
.and_then(|local_abs_path| local_abs_path.parent())
.and_then(|path| human_readable_package_name(path, project_env))
.and_then(|path| human_readable_package_name(path, project_env.as_ref()))
{
return Ok(TaskVariables::from_iter([(
return Task::ready(Ok(TaskVariables::from_iter([(
RUST_PACKAGE_TASK_VARIABLE.clone(),
package_name,
)]));
)])));
}
Ok(TaskVariables::default())
Task::ready(Ok(TaskVariables::default()))
}
fn associated_tasks(

View File

@@ -125,7 +125,7 @@ pub struct MultiBufferDiffHunk {
pub type MultiBufferPoint = Point;
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq, serde::Deserialize)]
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq, Hash, serde::Deserialize)]
#[serde(transparent)]
pub struct MultiBufferRow(pub u32);

View File

@@ -312,7 +312,12 @@ async fn load_shell_environment(
let stdout = String::from_utf8_lossy(&output.stdout);
let Some(env_output_start) = stdout.find(marker) else {
log::error!("failed to parse output of `env` command in login shell: {stdout}");
let stderr = String::from_utf8_lossy(&output.stderr);
log::error!(
"failed to parse output of `env` command in login shell. stdout: {:?}, stderr: {:?}",
stdout,
stderr
);
return message("Failed to parse stdout of env command. See logs for the output");
};

View File

@@ -82,6 +82,7 @@ use std::{
use task_store::TaskStore;
use terminals::Terminals;
use text::{Anchor, BufferId};
use toolchain_store::EmptyToolchainStore;
use util::{paths::compare_paths, ResultExt as _};
use worktree::{CreatedEntry, Snapshot, Traversal};
use worktree_store::{WorktreeStore, WorktreeStoreEvent};
@@ -626,12 +627,20 @@ impl Project {
});
let environment = ProjectEnvironment::new(&worktree_store, env, cx);
let toolchain_store = cx.new_model(|cx| {
ToolchainStore::local(
languages.clone(),
worktree_store.clone(),
environment.clone(),
cx,
)
});
let task_store = cx.new_model(|cx| {
TaskStore::local(
fs.clone(),
buffer_store.downgrade(),
worktree_store.clone(),
toolchain_store.read(cx).as_language_toolchain_store(),
environment.clone(),
cx,
)
@@ -647,14 +656,7 @@ impl Project {
});
cx.subscribe(&settings_observer, Self::on_settings_observer_event)
.detach();
let toolchain_store = cx.new_model(|cx| {
ToolchainStore::local(
languages.clone(),
worktree_store.clone(),
environment.clone(),
cx,
)
});
let lsp_store = cx.new_model(|cx| {
LspStore::new_local(
buffer_store.clone(),
@@ -749,12 +751,15 @@ impl Project {
});
cx.subscribe(&buffer_store, Self::on_buffer_store_event)
.detach();
let toolchain_store = cx.new_model(|cx| {
ToolchainStore::remote(SSH_PROJECT_ID, ssh.read(cx).proto_client(), cx)
});
let task_store = cx.new_model(|cx| {
TaskStore::remote(
fs.clone(),
buffer_store.downgrade(),
worktree_store.clone(),
toolchain_store.read(cx).as_language_toolchain_store(),
ssh.read(cx).proto_client(),
SSH_PROJECT_ID,
cx,
@@ -768,14 +773,12 @@ impl Project {
.detach();
let environment = ProjectEnvironment::new(&worktree_store, None, cx);
let toolchain_store = Some(cx.new_model(|cx| {
ToolchainStore::remote(SSH_PROJECT_ID, ssh.read(cx).proto_client(), cx)
}));
let lsp_store = cx.new_model(|cx| {
LspStore::new_remote(
buffer_store.clone(),
worktree_store.clone(),
toolchain_store.clone(),
Some(toolchain_store.clone()),
languages.clone(),
ssh_proto.clone(),
SSH_PROJECT_ID,
@@ -835,7 +838,7 @@ impl Project {
search_included_history: Self::new_search_history(),
search_excluded_history: Self::new_search_history(),
toolchain_store,
toolchain_store: Some(toolchain_store),
};
let ssh = ssh.read(cx);
@@ -963,6 +966,7 @@ impl Project {
fs.clone(),
buffer_store.downgrade(),
worktree_store.clone(),
Arc::new(EmptyToolchainStore),
client.clone().into(),
remote_id,
cx,

View File

@@ -10,9 +10,9 @@ use std::{
use anyhow::{Context, Result};
use collections::{HashMap, HashSet, VecDeque};
use gpui::{AppContext, Context as _, Model};
use gpui::{AppContext, Context as _, Model, Task};
use itertools::Itertools;
use language::{ContextProvider, File, Language, Location};
use language::{ContextProvider, File, Language, LanguageToolchainStore, Location};
use settings::{parse_json_with_comments, SettingsLocation};
use task::{
ResolvedTask, TaskContext, TaskId, TaskTemplate, TaskTemplates, TaskVariables, VariableName,
@@ -431,15 +431,15 @@ impl BasicContextProvider {
Self { worktree_store }
}
}
impl ContextProvider for BasicContextProvider {
fn build_context(
&self,
_: &TaskVariables,
location: &Location,
_: Option<&HashMap<String, String>>,
_: Option<HashMap<String, String>>,
_: Arc<dyn LanguageToolchainStore>,
cx: &mut AppContext,
) -> Result<TaskVariables> {
) -> Task<Result<TaskVariables>> {
let buffer = location.buffer.read(cx);
let buffer_snapshot = buffer.snapshot();
let symbols = buffer_snapshot.symbols_containing(location.range.start, None);
@@ -517,7 +517,7 @@ impl ContextProvider for BasicContextProvider {
task_variables.insert(VariableName::File, path_as_string);
}
Ok(task_variables)
Task::ready(Ok(task_variables))
}
}

View File

@@ -7,7 +7,7 @@ use futures::StreamExt as _;
use gpui::{AppContext, AsyncAppContext, EventEmitter, Model, ModelContext, Task, WeakModel};
use language::{
proto::{deserialize_anchor, serialize_anchor},
ContextProvider as _, Location,
ContextProvider as _, LanguageToolchainStore, Location,
};
use rpc::{proto, AnyProtoClient, TypedEnvelope};
use settings::{watch_config_file, SettingsLocation};
@@ -20,6 +20,7 @@ use crate::{
ProjectEnvironment,
};
#[expect(clippy::large_enum_variant)]
pub enum TaskStore {
Functional(StoreState),
Noop,
@@ -30,6 +31,7 @@ pub struct StoreState {
task_inventory: Model<Inventory>,
buffer_store: WeakModel<BufferStore>,
worktree_store: Model<WorktreeStore>,
toolchain_store: Arc<dyn LanguageToolchainStore>,
_global_task_config_watcher: Task<()>,
}
@@ -155,6 +157,7 @@ impl TaskStore {
fs: Arc<dyn Fs>,
buffer_store: WeakModel<BufferStore>,
worktree_store: Model<WorktreeStore>,
toolchain_store: Arc<dyn LanguageToolchainStore>,
environment: Model<ProjectEnvironment>,
cx: &mut ModelContext<'_, Self>,
) -> Self {
@@ -165,6 +168,7 @@ impl TaskStore {
},
task_inventory: Inventory::new(cx),
buffer_store,
toolchain_store,
worktree_store,
_global_task_config_watcher: Self::subscribe_to_global_task_file_changes(fs, cx),
})
@@ -174,6 +178,7 @@ impl TaskStore {
fs: Arc<dyn Fs>,
buffer_store: WeakModel<BufferStore>,
worktree_store: Model<WorktreeStore>,
toolchain_store: Arc<dyn LanguageToolchainStore>,
upstream_client: AnyProtoClient,
project_id: u64,
cx: &mut ModelContext<'_, Self>,
@@ -185,6 +190,7 @@ impl TaskStore {
},
task_inventory: Inventory::new(cx),
buffer_store,
toolchain_store,
worktree_store,
_global_task_config_watcher: Self::subscribe_to_global_task_file_changes(fs, cx),
})
@@ -200,6 +206,7 @@ impl TaskStore {
TaskStore::Functional(state) => match &state.mode {
StoreMode::Local { environment, .. } => local_task_context_for_location(
state.worktree_store.clone(),
state.toolchain_store.clone(),
environment.clone(),
captured_variables,
location,
@@ -210,10 +217,11 @@ impl TaskStore {
project_id,
} => remote_task_context_for_location(
*project_id,
upstream_client,
upstream_client.clone(),
state.worktree_store.clone(),
captured_variables,
location,
state.toolchain_store.clone(),
cx,
),
},
@@ -314,6 +322,7 @@ impl TaskStore {
fn local_task_context_for_location(
worktree_store: Model<WorktreeStore>,
toolchain_store: Arc<dyn LanguageToolchainStore>,
environment: Model<ProjectEnvironment>,
captured_variables: TaskVariables,
location: Location,
@@ -338,14 +347,15 @@ fn local_task_context_for_location(
combine_task_variables(
captured_variables,
location,
project_env.as_ref(),
project_env.clone(),
BasicContextProvider::new(worktree_store),
toolchain_store,
cx,
)
.log_err()
})
.ok()
.flatten()?;
.ok()?
.await
.log_err()?;
// Remove all custom entries starting with _, as they're not intended for use by the end user.
task_variables.sweep();
@@ -359,32 +369,46 @@ fn local_task_context_for_location(
fn remote_task_context_for_location(
project_id: u64,
upstream_client: &AnyProtoClient,
upstream_client: AnyProtoClient,
worktree_store: Model<WorktreeStore>,
captured_variables: TaskVariables,
location: Location,
toolchain_store: Arc<dyn LanguageToolchainStore>,
cx: &mut AppContext,
) -> Task<Option<TaskContext>> {
// We need to gather a client context, as the headless one may lack certain information (e.g. tree-sitter parsing is disabled there, so symbols are not available).
let mut remote_context = BasicContextProvider::new(worktree_store)
.build_context(&TaskVariables::default(), &location, None, cx)
.log_err()
.unwrap_or_default();
remote_context.extend(captured_variables);
cx.spawn(|cx| async move {
// We need to gather a client context, as the headless one may lack certain information (e.g. tree-sitter parsing is disabled there, so symbols are not available).
let mut remote_context = cx
.update(|cx| {
BasicContextProvider::new(worktree_store).build_context(
&TaskVariables::default(),
&location,
None,
toolchain_store,
cx,
)
})
.ok()?
.await
.log_err()
.unwrap_or_default();
remote_context.extend(captured_variables);
let context_task = upstream_client.request(proto::TaskContextForLocation {
project_id,
location: Some(proto::Location {
buffer_id: location.buffer.read(cx).remote_id().into(),
start: Some(serialize_anchor(&location.range.start)),
end: Some(serialize_anchor(&location.range.end)),
}),
task_variables: remote_context
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect(),
});
cx.spawn(|_| async move {
let buffer_id = cx
.update(|cx| location.buffer.read(cx).remote_id().to_proto())
.ok()?;
let context_task = upstream_client.request(proto::TaskContextForLocation {
project_id,
location: Some(proto::Location {
buffer_id,
start: Some(serialize_anchor(&location.range.start)),
end: Some(serialize_anchor(&location.range.end)),
}),
task_variables: remote_context
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect(),
});
let task_context = context_task.await.log_err()?;
Some(TaskContext {
cwd: task_context.cwd.map(PathBuf::from),
@@ -409,25 +433,45 @@ fn remote_task_context_for_location(
fn combine_task_variables(
mut captured_variables: TaskVariables,
location: Location,
project_env: Option<&HashMap<String, String>>,
project_env: Option<HashMap<String, String>>,
baseline: BasicContextProvider,
toolchain_store: Arc<dyn LanguageToolchainStore>,
cx: &mut AppContext,
) -> anyhow::Result<TaskVariables> {
) -> Task<anyhow::Result<TaskVariables>> {
let language_context_provider = location
.buffer
.read(cx)
.language()
.and_then(|language| language.context_provider());
let baseline = baseline
.build_context(&captured_variables, &location, project_env, cx)
.context("building basic default context")?;
captured_variables.extend(baseline);
if let Some(provider) = language_context_provider {
captured_variables.extend(
provider
.build_context(&captured_variables, &location, project_env, cx)
cx.spawn(move |cx| async move {
let baseline = cx
.update(|cx| {
baseline.build_context(
&captured_variables,
&location,
project_env.clone(),
toolchain_store.clone(),
cx,
)
})?
.await
.context("building basic default context")?;
captured_variables.extend(baseline);
if let Some(provider) = language_context_provider {
captured_variables.extend(
cx.update(|cx| {
provider.build_context(
&captured_variables,
&location,
project_env,
toolchain_store,
cx,
)
})?
.await
.context("building provider context")?,
);
}
Ok(captured_variables)
);
}
Ok(captured_variables)
})
}

View File

@@ -46,12 +46,16 @@ impl Project {
let worktree = self
.active_entry()
.and_then(|entry_id| self.worktree_for_entry(entry_id, cx))
.or_else(|| self.worktrees(cx).next())?;
let worktree = worktree.read(cx);
if !worktree.root_entry()?.is_dir() {
return None;
}
Some(worktree.abs_path().to_path_buf())
.into_iter()
.chain(self.worktrees(cx))
.find_map(|tree| {
let worktree = tree.read(cx);
worktree
.root_entry()
.filter(|entry| entry.is_dir())
.map(|_| worktree.abs_path().to_path_buf())
});
worktree
}
pub fn first_project_directory(&self, cx: &AppContext) -> Option<PathBuf> {
@@ -272,6 +276,18 @@ impl Project {
cx: &AppContext,
) -> Option<PathBuf> {
let venv_settings = settings.detect_venv.as_option()?;
if let Some(path) = self.find_venv_in_worktree(abs_path, &venv_settings, cx) {
return Some(path);
}
self.find_venv_on_filesystem(abs_path, &venv_settings, cx)
}
fn find_venv_in_worktree(
&self,
abs_path: &Path,
venv_settings: &terminal_settings::VenvSettingsContent,
cx: &AppContext,
) -> Option<PathBuf> {
let bin_dir_name = match std::env::consts::OS {
"windows" => "Scripts",
_ => "bin",
@@ -279,7 +295,7 @@ impl Project {
venv_settings
.directories
.iter()
.map(|virtual_environment_name| abs_path.join(virtual_environment_name))
.map(|name| abs_path.join(name))
.find(|venv_path| {
let bin_path = venv_path.join(bin_dir_name);
self.find_worktree(&bin_path, cx)
@@ -290,6 +306,32 @@ impl Project {
})
}
fn find_venv_on_filesystem(
&self,
abs_path: &Path,
venv_settings: &terminal_settings::VenvSettingsContent,
cx: &AppContext,
) -> Option<PathBuf> {
let (worktree, _) = self.find_worktree(abs_path, cx)?;
let fs = worktree.read(cx).as_local()?.fs();
let bin_dir_name = match std::env::consts::OS {
"windows" => "Scripts",
_ => "bin",
};
venv_settings
.directories
.iter()
.map(|name| abs_path.join(name))
.find(|venv_path| {
let bin_path = venv_path.join(bin_dir_name);
// One-time synchronous check is acceptable for terminal/task initialization
smol::block_on(fs.metadata(&bin_path))
.ok()
.flatten()
.map_or(false, |meta| meta.is_dir)
})
}
fn python_activate_command(
&self,
venv_base_directory: &Path,

View File

@@ -194,7 +194,7 @@ impl ToolchainStore {
groups,
})
}
pub(crate) fn as_language_toolchain_store(&self) -> Arc<dyn LanguageToolchainStore> {
pub fn as_language_toolchain_store(&self) -> Arc<dyn LanguageToolchainStore> {
match &self.0 {
ToolchainStoreInner::Local(local, _) => Arc::new(LocalStore(local.downgrade())),
ToolchainStoreInner::Remote(remote) => Arc::new(RemoteStore(remote.downgrade())),

View File

@@ -2145,12 +2145,6 @@ impl ProjectPanel {
worktree_id,
entry_id,
});
if cx.modifiers().shift {
self.marked_entries.insert(SelectedEntry {
worktree_id,
entry_id,
});
}
}
}
@@ -2458,14 +2452,8 @@ impl ProjectPanel {
.get(&(*worktree_id, entry.path.to_path_buf()))
.cloned();
let filename_text_color = if entry.kind.is_file()
&& diagnostic_severity
.map_or(false, |severity| severity == DiagnosticSeverity::ERROR)
{
Color::Error
} else {
entry_git_aware_label_color(status, entry.is_ignored, is_marked)
};
let filename_text_color =
entry_git_aware_label_color(status, entry.is_ignored, is_marked);
let mut details = EntryDetails {
filename,

View File

@@ -644,10 +644,10 @@ message UpdateChannelBufferCollaborators {
}
message GetDefinition {
uint64 project_id = 1;
uint64 buffer_id = 2;
Anchor position = 3;
repeated VectorClockEntry version = 4;
uint64 project_id = 1;
uint64 buffer_id = 2;
Anchor position = 3;
repeated VectorClockEntry version = 4;
}
message GetDefinitionResponse {
@@ -655,10 +655,10 @@ message GetDefinitionResponse {
}
message GetDeclaration {
uint64 project_id = 1;
uint64 buffer_id = 2;
Anchor position = 3;
repeated VectorClockEntry version = 4;
uint64 project_id = 1;
uint64 buffer_id = 2;
Anchor position = 3;
repeated VectorClockEntry version = 4;
}
message GetDeclarationResponse {
@@ -666,43 +666,43 @@ message GetDeclarationResponse {
}
message GetTypeDefinition {
uint64 project_id = 1;
uint64 buffer_id = 2;
Anchor position = 3;
repeated VectorClockEntry version = 4;
}
uint64 project_id = 1;
uint64 buffer_id = 2;
Anchor position = 3;
repeated VectorClockEntry version = 4;
}
message GetTypeDefinitionResponse {
repeated LocationLink links = 1;
}
message GetImplementation {
uint64 project_id = 1;
uint64 buffer_id = 2;
Anchor position = 3;
repeated VectorClockEntry version = 4;
}
uint64 project_id = 1;
uint64 buffer_id = 2;
Anchor position = 3;
repeated VectorClockEntry version = 4;
}
message GetImplementationResponse {
repeated LocationLink links = 1;
}
message GetReferences {
uint64 project_id = 1;
uint64 buffer_id = 2;
Anchor position = 3;
repeated VectorClockEntry version = 4;
}
uint64 project_id = 1;
uint64 buffer_id = 2;
Anchor position = 3;
repeated VectorClockEntry version = 4;
}
message GetReferencesResponse {
repeated Location locations = 1;
}
message GetDocumentHighlights {
uint64 project_id = 1;
uint64 buffer_id = 2;
Anchor position = 3;
repeated VectorClockEntry version = 4;
}
uint64 project_id = 1;
uint64 buffer_id = 2;
Anchor position = 3;
repeated VectorClockEntry version = 4;
}
message GetDocumentHighlightsResponse {
repeated DocumentHighlight highlights = 1;
@@ -1041,7 +1041,6 @@ message OnTypeFormattingResponse {
Transaction transaction = 1;
}
message LinkedEditingRange {
uint64 project_id = 1;
uint64 buffer_id = 2;

View File

@@ -23,9 +23,7 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
use ui::{
prelude::*, tooltip_container, ButtonLike, KeyBinding, ListItem, ListItemSpacing, Tooltip,
};
use ui::{prelude::*, tooltip_container, KeyBinding, ListItem, ListItemSpacing, Tooltip};
use util::{paths::PathExt, ResultExt};
use workspace::{
CloseIntent, ModalView, OpenOptions, SerializedWorkspaceLocation, Workspace, WorkspaceId,
@@ -471,27 +469,20 @@ impl PickerDelegate for RecentProjectsDelegate {
fn render_footer(&self, cx: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
Some(
h_flex()
.border_t_1()
.py_2()
.pr_2()
.border_color(cx.theme().colors().border_variant)
.w_full()
.p_2()
.gap_2()
.justify_end()
.gap_4()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.child(
ButtonLike::new("remote")
.when_some(KeyBinding::for_action(&OpenRemote, cx), |button, key| {
button.child(key)
})
.child(Label::new("Open Remote Folder…").color(Color::Muted))
Button::new("remote", "Open Remote Folder")
.key_binding(KeyBinding::for_action(&OpenRemote, cx))
.on_click(|_, cx| cx.dispatch_action(OpenRemote.boxed_clone())),
)
.child(
ButtonLike::new("local")
.when_some(
KeyBinding::for_action(&workspace::Open, cx),
|button, key| button.child(key),
)
.child(Label::new("Open Local Folder…").color(Color::Muted))
Button::new("local", "Open Local Folder")
.key_binding(KeyBinding::for_action(&workspace::Open, cx))
.on_click(|_, cx| cx.dispatch_action(workspace::Open.boxed_clone())),
)
.into_any(),

View File

@@ -1553,9 +1553,13 @@ impl SshRemoteConnection {
"Prebuilt remote servers are not yet available for {os:?}. See https://zed.dev/docs/remote-development"
))?,
};
let arch = if arch.starts_with("arm") || arch.starts_with("aarch64") {
// exclude armv5,6,7 as they are 32-bit.
let arch = if arch.starts_with("armv8")
|| arch.starts_with("armv9")
|| arch.starts_with("aarch64")
{
"aarch64"
} else if arch.starts_with("x86") || arch.starts_with("i686") {
} else if arch.starts_with("x86") {
"x86_64"
} else {
Err(anyhow!(

View File

@@ -85,13 +85,22 @@ impl HeadlessProject {
cx,
)
});
let environment = project::ProjectEnvironment::new(&worktree_store, None, cx);
let toolchain_store = cx.new_model(|cx| {
ToolchainStore::local(
languages.clone(),
worktree_store.clone(),
environment.clone(),
cx,
)
});
let task_store = cx.new_model(|cx| {
let mut task_store = TaskStore::local(
fs.clone(),
buffer_store.downgrade(),
worktree_store.clone(),
toolchain_store.read(cx).as_language_toolchain_store(),
environment.clone(),
cx,
);
@@ -108,14 +117,7 @@ impl HeadlessProject {
observer.shared(SSH_PROJECT_ID, session.clone().into(), cx);
observer
});
let toolchain_store = cx.new_model(|cx| {
ToolchainStore::local(
languages.clone(),
worktree_store.clone(),
environment.clone(),
cx,
)
});
let lsp_store = cx.new_model(|cx| {
let mut lsp_store = LspStore::new_local(
buffer_store.clone(),

View File

@@ -1,5 +1,9 @@
use anyhow::Result;
use base64::prelude::*;
use base64::{
alphabet,
engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig},
Engine as _,
};
use gpui::{img, ClipboardItem, Image, ImageFormat, Pixels, RenderImage, WindowContext};
use std::sync::Arc;
use ui::{div, prelude::*, IntoElement, Styled};
@@ -14,11 +18,18 @@ pub struct ImageView {
image: Arc<RenderImage>,
}
pub const STANDARD_INDIFFERENT: GeneralPurpose = GeneralPurpose::new(
&alphabet::STANDARD,
GeneralPurposeConfig::new()
.with_encode_padding(false)
.with_decode_padding_mode(DecodePaddingMode::Indifferent),
);
impl ImageView {
pub fn from(base64_encoded_data: &str) -> Result<Self> {
let filtered =
base64_encoded_data.replace(&[' ', '\n', '\t', '\r', '\x0b', '\x0c'][..], "");
let bytes = BASE64_STANDARD_NO_PAD.decode(filtered)?;
let bytes = STANDARD_INDIFFERENT.decode(filtered)?;
let format = image::guess_format(&bytes)?;

View File

@@ -121,7 +121,7 @@ impl EditorBlock {
execution_view: View<ExecutionView>,
on_close: CloseBlockFn,
) -> RenderBlock {
let render = move |cx: &mut BlockContext| {
Arc::new(move |cx: &mut BlockContext| {
let execution_view = execution_view.clone();
let text_style = crate::outputs::plain::text_style(cx);
@@ -163,6 +163,7 @@ impl EditorBlock {
div()
.id(cx.block_id)
.block_mouse_down()
.flex()
.items_start()
.min_h(text_line_height)
@@ -186,9 +187,7 @@ impl EditorBlock {
.child(execution_view),
)
.into_any_element()
};
Box::new(render)
})
}
}

View File

@@ -657,6 +657,7 @@ impl BufferSearchBar {
return true;
}
cx.propagate();
false
}

View File

@@ -1,7 +1,7 @@
use crate::{
BufferSearchBar, FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext,
SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleIncludeIgnored,
ToggleRegex, ToggleReplace, ToggleWholeWord,
buffer_search::Deploy, BufferSearchBar, FocusSearch, NextHistoryQuery, PreviousHistoryQuery,
ReplaceAll, ReplaceNext, SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive,
ToggleIncludeIgnored, ToggleRegex, ToggleReplace, ToggleWholeWord,
};
use collections::{HashMap, HashSet};
use editor::{
@@ -58,6 +58,9 @@ impl Global for ActiveSettings {}
pub fn init(cx: &mut AppContext) {
cx.set_global(ActiveSettings::default());
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
register_workspace_action(workspace, move |search_bar, _: &Deploy, cx| {
search_bar.focus_search(cx);
});
register_workspace_action(workspace, move |search_bar, _: &FocusSearch, cx| {
search_bar.focus_search(cx);
});

View File

@@ -1044,6 +1044,10 @@ impl InputHandler for TerminalInputHandler {
) -> Option<Bounds<Pixels>> {
self.cursor_bounds
}
fn apple_press_and_hold_enabled(&mut self) -> bool {
false
}
}
pub fn is_blank(cell: &IndexedCell) -> bool {

View File

@@ -1046,14 +1046,20 @@ impl Item for TerminalView {
.on_click(move |_, cx| {
cx.dispatch_action(Box::new(tasks_ui::Rerun {
task_id: Some(task_id.clone()),
..tasks_ui::Rerun::default()
allow_concurrent_runs: Some(true),
use_new_terminal: Some(false),
reevaluate_context: false,
}));
})
};
let (icon, icon_color, rerun_button) = match terminal.task() {
Some(terminal_task) => match &terminal_task.status {
TaskStatus::Running => (IconName::Play, Color::Disabled, None),
TaskStatus::Running => (
IconName::Play,
Color::Disabled,
Some(rerun_button(terminal_task.id.clone())),
),
TaskStatus::Unknown => (
IconName::Warning,
Color::Warning,
@@ -1465,7 +1471,7 @@ mod tests {
});
}
// Active entry with a work tree, worktree is a file -> home_dir()
// Active entry with a work tree, worktree is a file -> worktree_folder()
#[gpui::test]
async fn active_entry_worktree_is_file(cx: &mut TestAppContext) {
let (project, workspace) = init_test(cx).await;
@@ -1481,7 +1487,7 @@ mod tests {
assert!(active_entry.is_some());
let res = default_working_directory(workspace, cx);
assert_eq!(res, None);
assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
let res = first_project_directory(workspace, cx);
assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
});

View File

@@ -1,6 +1,7 @@
mod avatar;
mod button;
mod checkbox;
mod content_group;
mod context_menu;
mod disclosure;
mod divider;
@@ -36,6 +37,7 @@ mod stories;
pub use avatar::*;
pub use button::*;
pub use checkbox::*;
pub use content_group::*;
pub use context_menu::*;
pub use disclosure::*;
pub use divider::*;

View File

@@ -0,0 +1,135 @@
use crate::prelude::*;
use gpui::{AnyElement, IntoElement, ParentElement, StyleRefinement, Styled};
use smallvec::SmallVec;
/// Creates a new [ContentGroup].
pub fn content_group() -> ContentGroup {
ContentGroup::new()
}
/// A [ContentGroup] that vertically stacks its children.
///
/// This is a convenience function that simply combines [`ContentGroup`] and [`v_flex`](crate::v_flex).
pub fn v_group() -> ContentGroup {
content_group().v_flex()
}
/// Creates a new horizontal [ContentGroup].
///
/// This is a convenience function that simply combines [`ContentGroup`] and [`h_flex`](crate::h_flex).
pub fn h_group() -> ContentGroup {
content_group().h_flex()
}
/// A flexible container component that can hold other elements.
#[derive(IntoElement)]
pub struct ContentGroup {
base: Div,
border: bool,
fill: bool,
children: SmallVec<[AnyElement; 2]>,
}
impl ContentGroup {
/// Creates a new [ContentBox].
pub fn new() -> Self {
Self {
base: div(),
border: true,
fill: true,
children: SmallVec::new(),
}
}
/// Removes the border from the [ContentBox].
pub fn borderless(mut self) -> Self {
self.border = false;
self
}
/// Removes the background fill from the [ContentBox].
pub fn unfilled(mut self) -> Self {
self.fill = false;
self
}
}
impl ParentElement for ContentGroup {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
}
}
impl Styled for ContentGroup {
fn style(&mut self) -> &mut StyleRefinement {
self.base.style()
}
}
impl RenderOnce for ContentGroup {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
// TODO:
// Baked in padding will make scrollable views inside of content boxes awkward.
//
// Do we make the padding optional, or do we push to use a different component?
self.base
.when(self.fill, |this| {
this.bg(cx.theme().colors().text.opacity(0.05))
})
.when(self.border, |this| {
this.border_1().border_color(cx.theme().colors().border)
})
.rounded_md()
.p_2()
.children(self.children)
}
}
impl ComponentPreview for ContentGroup {
fn description() -> impl Into<Option<&'static str>> {
"A flexible container component that can hold other elements. It can be customized with or without a border and background fill."
}
fn example_label_side() -> ExampleLabelSide {
ExampleLabelSide::Bottom
}
fn examples(_: &WindowContext) -> Vec<ComponentExampleGroup<Self>> {
vec![example_group(vec![
single_example(
"Default",
ContentGroup::new()
.flex_1()
.items_center()
.justify_center()
.h_48()
.child(Label::new("Default ContentBox")),
)
.grow(),
single_example(
"Without Border",
ContentGroup::new()
.flex_1()
.items_center()
.justify_center()
.h_48()
.borderless()
.child(Label::new("Borderless ContentBox")),
)
.grow(),
single_example(
"Without Fill",
ContentGroup::new()
.flex_1()
.items_center()
.justify_center()
.h_48()
.unfilled()
.child(Label::new("Unfilled ContentBox")),
)
.grow(),
])
.grow()]
}
}

View File

@@ -262,29 +262,38 @@ impl ContextMenu {
fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
if let Some(ix) = self.selected_index {
for (ix, item) in self.items.iter().enumerate().skip(ix + 1) {
if item.is_selectable() {
self.selected_index = Some(ix);
cx.notify();
break;
let next_index = ix + 1;
if self.items.len() <= next_index {
self.select_first(&SelectFirst, cx);
} else {
for (ix, item) in self.items.iter().enumerate().skip(next_index) {
if item.is_selectable() {
self.selected_index = Some(ix);
cx.notify();
break;
}
}
}
} else {
self.select_first(&Default::default(), cx);
self.select_first(&SelectFirst, cx);
}
}
pub fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
if let Some(ix) = self.selected_index {
for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
if item.is_selectable() {
self.selected_index = Some(ix);
cx.notify();
break;
if ix == 0 {
self.handle_select_last(&SelectLast, cx);
} else {
for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
if item.is_selectable() {
self.selected_index = Some(ix);
cx.notify();
break;
}
}
}
} else {
self.handle_select_last(&Default::default(), cx);
self.handle_select_last(&SelectLast, cx);
}
}

View File

@@ -66,7 +66,7 @@ impl<M: ManagedView> PopoverMenuHandle<M> {
.map_or(false, |state| state.menu.borrow().as_ref().is_some())
}
pub fn is_focused(&self, cx: &mut WindowContext) -> bool {
pub fn is_focused(&self, cx: &WindowContext) -> bool {
self.0.borrow().as_ref().map_or(false, |state| {
state
.menu

View File

@@ -16,7 +16,7 @@ pub use crate::traits::selectable::*;
pub use crate::traits::styled_ext::*;
pub use crate::traits::visible_on_hover::*;
pub use crate::DynamicSpacing;
pub use crate::{h_flex, v_flex};
pub use crate::{h_flex, h_group, v_flex, v_group};
pub use crate::{Button, ButtonSize, ButtonStyle, IconButton, SelectableButton};
pub use crate::{ButtonCommon, Color};
pub use crate::{Headline, HeadlineSize};

View File

@@ -32,6 +32,10 @@ pub trait ComponentPreview: IntoElement {
fn examples(_cx: &WindowContext) -> Vec<ComponentExampleGroup<Self>>;
fn custom_example(_cx: &WindowContext) -> impl Into<Option<AnyElement>> {
None::<AnyElement>
}
fn component_previews(cx: &WindowContext) -> Vec<AnyElement> {
Self::examples(cx)
.into_iter()
@@ -47,7 +51,8 @@ pub trait ComponentPreview: IntoElement {
let description = Self::description().into();
v_flex()
.gap_3()
.w_full()
.gap_6()
.p_4()
.border_1()
.border_color(cx.theme().colors().border)
@@ -73,18 +78,23 @@ pub trait ComponentPreview: IntoElement {
)
}),
)
.when_some(Self::custom_example(cx).into(), |this, custom_example| {
this.child(custom_example)
})
.children(Self::component_previews(cx))
.into_any_element()
}
fn render_example_group(group: ComponentExampleGroup<Self>) -> AnyElement {
v_flex()
.gap_2()
.gap_6()
.when(group.grow, |this| this.w_full().flex_1())
.when_some(group.title, |this, title| {
this.child(Label::new(title).size(LabelSize::Small))
})
.child(
h_flex()
.w_full()
.gap_6()
.children(group.examples.into_iter().map(Self::render_example))
.into_any_element(),
@@ -103,6 +113,7 @@ pub trait ComponentPreview: IntoElement {
};
base.gap_1()
.when(example.grow, |this| this.flex_1())
.child(example.element)
.child(
Label::new(example.variant_name)
@@ -117,6 +128,7 @@ pub trait ComponentPreview: IntoElement {
pub struct ComponentExample<T> {
variant_name: SharedString,
element: T,
grow: bool,
}
impl<T> ComponentExample<T> {
@@ -125,14 +137,22 @@ impl<T> ComponentExample<T> {
Self {
variant_name: variant_name.into(),
element: example,
grow: false,
}
}
/// Set the example to grow to fill the available horizontal space.
pub fn grow(mut self) -> Self {
self.grow = true;
self
}
}
/// A group of component examples.
pub struct ComponentExampleGroup<T> {
pub title: Option<SharedString>,
pub examples: Vec<ComponentExample<T>>,
pub grow: bool,
}
impl<T> ComponentExampleGroup<T> {
@@ -141,15 +161,24 @@ impl<T> ComponentExampleGroup<T> {
Self {
title: None,
examples,
grow: false,
}
}
/// Create a new group of examples with the given title.
pub fn with_title(title: impl Into<SharedString>, examples: Vec<ComponentExample<T>>) -> Self {
Self {
title: Some(title.into()),
examples,
grow: false,
}
}
/// Set the group to grow to fill the available horizontal space.
pub fn grow(mut self) -> Self {
self.grow = true;
self
}
}
/// Create a single example

View File

@@ -3,10 +3,11 @@ use editor::{scroll::Autoscroll, Bias, Editor};
use gpui::{actions, Action, ViewContext};
use language::SelectionGoal;
actions!(vim, [NormalBefore]);
actions!(vim, [NormalBefore, TemporaryNormal]);
pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
Vim::action(editor, cx, Vim::normal_before);
Vim::action(editor, cx, Vim::temporary_normal);
}
impl Vim {
@@ -35,6 +36,11 @@ impl Vim {
self.repeat(true, cx)
}
fn temporary_normal(&mut self, _: &TemporaryNormal, cx: &mut ViewContext<Self>) {
self.switch_mode(Mode::Normal, true, cx);
self.temp_mode = true;
}
}
#[cfg(test)]

View File

@@ -88,12 +88,19 @@ impl Render for ModeIndicator {
return div().into_any();
};
let vim_readable = vim.read(cx);
let mode = if vim_readable.temp_mode {
format!("(insert) {}", vim_readable.mode)
} else {
vim_readable.mode.to_string()
};
let current_operators_description = self.current_operators_description(vim.clone(), cx);
let pending = self
.pending_keys
.as_ref()
.unwrap_or(&current_operators_description);
Label::new(format!("{} -- {} --", pending, vim.read(cx).mode))
Label::new(format!("{} -- {} --", pending, mode))
.size(LabelSize::Small)
.line_height_style(LineHeightStyle::UiLabel)
.into_any_element()

View File

@@ -185,6 +185,8 @@ impl Vim {
error!("Unexpected normal mode motion operator: {:?}", operator)
}
}
// Exit temporary normal mode (if active).
self.exit_temporary_normal(cx);
}
pub fn normal_object(&mut self, object: Object, cx: &mut ViewContext<Self>) {
@@ -483,6 +485,12 @@ impl Vim {
});
});
}
fn exit_temporary_normal(&mut self, cx: &mut ViewContext<Self>) {
if self.temp_mode {
self.switch_mode(Mode::Insert, true, cx);
}
}
}
#[cfg(test)]
mod test {

View File

@@ -176,7 +176,7 @@ impl Vim {
.0;
}
cursor = movement::indented_line_beginning(map, cursor, true);
} else if !is_multiline {
} else if !is_multiline && !vim.temp_mode {
cursor = movement::saturating_left(map, cursor)
}
cursors.push(cursor);

View File

@@ -3,6 +3,7 @@ use std::{cell::RefCell, rc::Rc};
use crate::{
insert::NormalBefore,
motion::Motion,
normal::InsertBefore,
state::{Mode, Operator, RecordedSelection, ReplayableAction, VimGlobals},
Vim,
};
@@ -308,6 +309,11 @@ impl Vim {
actions.push(ReplayableAction::Action(EndRepeat.boxed_clone()));
if self.temp_mode {
self.temp_mode = false;
actions.push(ReplayableAction::Action(InsertBefore.boxed_clone()));
}
let globals = Vim::globals(cx);
globals.dot_replaying = true;
let mut replayer = globals.replayer.get_or_insert_with(Replayer::new).clone();

View File

@@ -139,6 +139,11 @@ impl Vim {
options |= SearchOptions::REGEX;
}
search_bar.set_search_options(options, cx);
let prior_mode = if self.temp_mode {
Mode::Insert
} else {
self.mode
};
self.search = SearchState {
direction,
@@ -146,7 +151,7 @@ impl Vim {
initial_query: query,
prior_selections,
prior_operator: self.operator_stack.last().cloned(),
prior_mode: self.mode,
prior_mode,
}
});
}

View File

@@ -42,6 +42,7 @@ impl Vim {
});
});
});
self.exit_temporary_normal(cx);
}
pub fn yank_object(&mut self, object: Object, around: bool, cx: &mut ViewContext<Self>) {
@@ -65,6 +66,7 @@ impl Vim {
});
});
});
self.exit_temporary_normal(cx);
}
pub fn yank_selections_content(

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