Compare commits

..

115 Commits

Author SHA1 Message Date
Antonio Scandurra
02701b49f2 WIP 2024-10-31 15:53:24 +01:00
Antonio Scandurra
b338475855 WIP 2024-10-31 15:02:21 +01:00
Antonio Scandurra
a27369c42c WIP: start using replace blocks for patches 2024-10-31 15:02:06 +01:00
Antonio Scandurra
d4099eed3a Introduce a new selected field to BlockContext 2024-10-31 15:01:43 +01:00
Thorsten Ball
5b6401519b activity indicator: Reset formatting failure on click (#20029)
Release Notes:

- N/A
2024-10-31 14:33:36 +01:00
Thorsten Ball
293e080f03 tasks: Add editor: Spawn Nearest Task action (#19901)
This spawns the runnable task that that's closest to the cursor.

One thing missing right now is that it doesn't find tasks that are
attached to non-outline symbols, such as subtests in Go.

Release Notes:

- Added a new reveal option for tasks: `"no_focus"`. If used, the tasks
terminal panel will be opened and shown, but not focused.
- Added a new `editor: spawn nearest task` action that spawns the task
with a run indicator icon nearest to the cursor. It can be configured to
also use a `reveal` strategy. Example:
```json
{
  "context": "EmptyPane || SharedScreen || vim_mode == normal",
  "bindings": {
    ", r t": ["editor::SpawnNearestTask", { "reveal": "no_focus" }],
  }
}
```


Demo:



https://github.com/user-attachments/assets/0d1818f0-7ae4-4200-8c3e-0ed47550c298

---------

Co-authored-by: Bennet <bennet@zed.dev>
2024-10-31 14:25:57 +01:00
Auf keinen Fall Jens
633b665379 Option to insert comment character(s) at the beginning of the line(s) (#19746)
Closes #19459


This PR adds the optional setting to insert comment character(s) at the
beginning of the line(s) instead of after the indentation. It can be
enabled via keybindings:

```
"ctrl-/": ["editor::ToggleComments", { "ignore_indent": true }]
```

As suggested by @notpeter in #19459, this is implemented in
`toggle_comments` (editor.rs) taking the existing `advance_downwards`
option as example.

There's also a test case for the setting, which mimics the test case for
the regular comment toggling behavior.

---

I am not entirely happy with the name `ignore_indent`. The default would
be a double negative now `ignore_indent=false`. A positive wording would
probably easier to understand, but I could not think of anything
concise. `insert_at_line_start` or just `at_line_start` might work, but
didn't convince me either. That said, I am happy to change the name if
there are better ideas.

---

Release Notes:

- Added optional setting to insert comment character(s) at the beginning
of the line(s) instead of after the indentation. It can be used by
changing the default mapping to toggle comments like this: `"ctrl-/":
["editor::ToggleComments", { "ignore_indent": true }]`
2024-10-31 09:39:57 +01:00
Thorsten Ball
7fd334fddb proto: Remove unused UpdateUserSettings message (#20005)
Release Notes:

- N/A
2024-10-31 09:36:18 +01:00
Thorsten Ball
10226a3992 docs: Document inline blame options (#20006)
Release Notes:

- N/A
2024-10-31 09:36:05 +01:00
Peter Tripp
383e868af0 docs: SSH no longer requires Zed Preview (#20003) 2024-10-30 23:28:13 -04:00
Conrad Irwin
40802d91d4 SSH installation refactor (#19991)
This also cleans up logic for deciding how to do things.

Release Notes:

- Remoting: If downloading the binary on the remote fails, fall back to
uploading it.

---------

Co-authored-by: Mikayala <mikayla@zed.dev>
2024-10-30 16:20:11 -07:00
Danilo Leal
6d5784daa6 Adjust design of the slash command picker (#19973)
This PR removes the quote selection icon button from the footer and adds
it in the picker, and adds an icon field to each command entry. Final
result looks like:


https://github.com/user-attachments/assets/d177f1c1-b6f6-4652-9434-f6291b279e34

Release Notes:

- N/A
2024-10-30 19:42:42 -03:00
Conrad Irwin
f80eb264fb Robustify download on remote (#19983)
Closes #19976
Closes #19972

We now prefer curl to wget (as it supports socks5:// proxies) and pass
-f to
curl so it fails; and use sh instead of bash, which should have more
consistent
behaviour across systems

Release Notes:

- SSH Remoting: make downloading binary on remote more reliable.

---------

Co-authored-by: Will <will@zed.dev>
2024-10-30 15:17:50 -07:00
Conrad Irwin
3d956ca68b Fail download if download fails (#19990)
Co-Authored-By: Mikayla <mikayla@zed.dev>

Release Notes:

- Remoting: Fixes a bug where we could cache an HTML error page as a
binary

Co-authored-by: Mikayla <mikayla@zed.dev>
2024-10-30 14:50:41 -07:00
Kyle Kelley
7ce131aaf8 Trim whitespace from base64 encoded image data before decoding it (#19977)
Closes #17956
Closes #16330

This fix is for both REPL (released) and notebook (unreleased)

<img width="1210" alt="image"
src="https://github.com/user-attachments/assets/bd046f0f-3ad1-4c25-b3cb-114e008c2a69">

Release Notes:

- Fixed image support in REPL for certain versions of matplotlib that
included preceding and/or trailing whitespace in the base64 image data
2024-10-30 12:32:17 -07:00
Jen Stehlik
60be47d115 Update Gleam icon (#19978)
Improves upon: https://github.com/zed-industries/zed/pull/19887

Implements the feedback by @PixelJanitor to make the icon follow the
design guidelines.

Release Notes:

- Improved Gleam icon
2024-10-30 15:29:32 -04:00
Conrad Irwin
bd187883da Migration to remove dev servers (#19639)
Depends on #19638

Release Notes:

- None
2024-10-30 11:55:55 -06:00
Conrad Irwin
4f9217bca0 Support zed://ssh (#19970)
Closes: #15070

Release Notes:

- Added support for `zed://ssh/<connnection>/<path>`
2024-10-30 11:28:25 -06:00
Conrad Irwin
ce5222f1df Add KeyContextView (#19872)
Release Notes:

- Added `cmd-shift-p debug: Open Key Context View` to help debug custom
key bindings



https://github.com/user-attachments/assets/de273c97-5b27-45aa-9ff1-f943b0ed7dfe
2024-10-30 11:26:54 -06:00
Kirill Bulatov
cf7b0c8971 Add scrollbars to outline panel (#19969)
Part of https://github.com/zed-industries/zed/issues/15324


![image](https://github.com/user-attachments/assets/4f32d585-9bd2-46be-8234-3658a71906ee)

Repeats the approach used in the project panel.

Release Notes:

- Added scrollbars to outline panel

---------

Co-authored-by: Nate Butler <nate@zed.dev>
2024-10-30 19:09:14 +02:00
renovate[bot]
7bc4cb9868 Update Rust crate hyper to v0.14.31 (#19323)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [hyper](https://hyper.rs)
([source](https://redirect.github.com/hyperium/hyper)) |
workspace.dependencies | patch | `0.14.30` -> `0.14.31` |

---

### Release Notes

<details>
<summary>hyperium/hyper (hyper)</summary>

###
[`v0.14.31`](https://redirect.github.com/hyperium/hyper/releases/tag/v0.14.31)

[Compare
Source](https://redirect.github.com/hyperium/hyper/compare/v0.14.30...v0.14.31)

#### Bug Fixes

- **http1:** improve performance of parsing sequentially partial
messages
([97b595e](97b595e589))

</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:eyJjcmVhdGVkSW5WZXIiOiIzOC4xMjAuMSIsInVwZGF0ZWRJblZlciI6IjM4LjEyMC4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-30 12:11:07 -04:00
Gherman
f84f3ffeb7 docs: Add linkedProjects section to Rust docs (#19954)
Related to #19897

Adds a section about multi-project workspaces and how to configure
rust-analyzer to diagnose them even if the cargo workspace does not list
them

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-10-30 11:43:44 -04:00
Richard Feldman
c564a4a26c Require /file or /tab when using Suggest Edits (#19960)
Now if you try to do Suggest Edits without a file context, you see this
(and it doesn't run the query).

<img width="635" alt="Screenshot 2024-10-30 at 10 51 24 AM"
src="https://github.com/user-attachments/assets/a3997ba6-98a9-4bfa-81b6-1d8579c26fd7">


Release Notes:

- N/A

---------

Co-authored-by: Antonio <antonio@zed.dev>
2024-10-30 11:38:43 -04:00
Marshall Bowers
515fd7b75f git_hosting_providers: Fix support for GitLab remotes containing subgroups (#19962)
This PR fixes the support for GitLab remote URLs containing subgroups.

Reported in
https://github.com/zed-industries/zed/issues/18012#issuecomment-2446206256.

Release Notes:

- N/A
2024-10-30 11:16:44 -04:00
Peter Tripp
662a4440cc v0.161.x dev 2024-10-30 11:06:39 -04:00
Marshall Bowers
5dee43b05c dart: Extract to zed-extensions/dart repository (#19959)
This PR extracts the Dart extension to the
[zed-extensions/dart](https://github.com/zed-extensions/dart)
repository.

Release Notes:

- N/A
2024-10-30 11:00:06 -04:00
Antonio Scandurra
c8003c0697 Take a mutable context when resolving selections (#19948)
This is a behavior-preserving change, but lays the groundwork for
expanding selections when the cursor lands inside of a "replace" block.

Release Notes:

- N/A
2024-10-30 15:21:51 +01:00
Lukas Geiger
83e2889d63 Fix notebook cell-height when soft-wrapping lines (#19933) 2024-10-30 07:12:32 -07:00
Kirill Bulatov
d49cd0019f Log prettier errors on failures (#19951)
Closes https://github.com/zed-industries/zed/issues/11987

Release Notes:

- Fixed prettier not reporting failures in the status panel on
formatting and installation errors
2024-10-30 14:49:47 +02:00
Thorsten Ball
0ba40bdfb8 remote dev: Always upload binary in development mode (#19953)
Release Notes:

- N/A
2024-10-30 13:41:28 +01:00
Thorsten Ball
f6cd97f6fd remote dev: Allow canceling language server work in editor (#19946)
Release Notes:

- Added ability to cancel language server work in remote development.

Demo:



https://github.com/user-attachments/assets/c9ca91a5-617f-4886-a458-87c563c5a247
2024-10-30 13:27:11 +01:00
Thorsten Ball
774a8bf039 inline blame: Fix default setting for inline blame (#19943)
Follow-up to #19759. Fixes the default value. cc @pjtatlow 😄 

Release Notes:

- N/A
2024-10-30 11:40:04 +01:00
Antonio Scandurra
4431ef1870 Speed up point translation in the Rope (#19913)
This pull request introduces an index of Unicode codepoints, newlines
and UTF-16 codepoints.

Benchmarks worth a thousand words:

```
push/4096               time:   [467.06 µs 470.07 µs 473.24 µs]
                        thrpt:  [8.2543 MiB/s 8.3100 MiB/s 8.3635 MiB/s]
                 change:
                        time:   [-4.1462% -3.0990% -2.0527%] (p = 0.00 < 0.05)
                        thrpt:  [+2.0957% +3.1981% +4.3255%]
                        Performance has improved.
Found 3 outliers among 100 measurements (3.00%)
  1 (1.00%) low mild
  2 (2.00%) high mild
push/65536              time:   [1.4650 ms 1.4796 ms 1.4922 ms]
                        thrpt:  [41.885 MiB/s 42.242 MiB/s 42.664 MiB/s]
                 change:
                        time:   [-3.2871% -2.3489% -1.4555%] (p = 0.00 < 0.05)
                        thrpt:  [+1.4770% +2.4054% +3.3988%]
                        Performance has improved.
Found 6 outliers among 100 measurements (6.00%)
  3 (3.00%) low severe
  3 (3.00%) low mild

append/4096             time:   [729.00 ns 730.57 ns 732.14 ns]
                        thrpt:  [5.2103 GiB/s 5.2215 GiB/s 5.2327 GiB/s]
                 change:
                        time:   [-81.884% -81.836% -81.790%] (p = 0.00 < 0.05)
                        thrpt:  [+449.16% +450.53% +452.01%]
                        Performance has improved.
Found 11 outliers among 100 measurements (11.00%)
  3 (3.00%) low mild
  6 (6.00%) high mild
  2 (2.00%) high severe
append/65536            time:   [504.44 ns 505.58 ns 506.77 ns]
                        thrpt:  [120.44 GiB/s 120.72 GiB/s 121.00 GiB/s]
                 change:
                        time:   [-94.833% -94.807% -94.782%] (p = 0.00 < 0.05)
                        thrpt:  [+1816.3% +1825.8% +1835.5%]
                        Performance has improved.
Found 4 outliers among 100 measurements (4.00%)
  3 (3.00%) high mild
  1 (1.00%) high severe

slice/4096              time:   [29.661 µs 29.733 µs 29.816 µs]
                        thrpt:  [131.01 MiB/s 131.38 MiB/s 131.70 MiB/s]
                 change:
                        time:   [-48.833% -48.533% -48.230%] (p = 0.00 < 0.05)
                        thrpt:  [+93.161% +94.298% +95.440%]
                        Performance has improved.
slice/65536             time:   [588.00 µs 590.22 µs 592.17 µs]
                        thrpt:  [105.54 MiB/s 105.89 MiB/s 106.29 MiB/s]
                 change:
                        time:   [-45.599% -45.347% -45.099%] (p = 0.00 < 0.05)
                        thrpt:  [+82.147% +82.971% +83.821%]
                        Performance has improved.
Found 2 outliers among 100 measurements (2.00%)
  1 (1.00%) low severe
  1 (1.00%) high mild

bytes_in_range/4096     time:   [3.8630 µs 3.8811 µs 3.8994 µs]
                        thrpt:  [1001.8 MiB/s 1006.5 MiB/s 1011.2 MiB/s]
                 change:
                        time:   [+0.0600% +0.6000% +1.1833%] (p = 0.03 < 0.05)
                        thrpt:  [-1.1695% -0.5964% -0.0600%]
                        Change within noise threshold.
bytes_in_range/65536    time:   [98.178 µs 98.545 µs 98.931 µs]
                        thrpt:  [631.75 MiB/s 634.23 MiB/s 636.60 MiB/s]
                 change:
                        time:   [-0.6513% +0.7537% +2.2265%] (p = 0.30 > 0.05)
                        thrpt:  [-2.1780% -0.7481% +0.6555%]
                        No change in performance detected.
Found 11 outliers among 100 measurements (11.00%)
  8 (8.00%) high mild
  3 (3.00%) high severe

chars/4096              time:   [878.91 ns 879.45 ns 880.06 ns]
                        thrpt:  [4.3346 GiB/s 4.3376 GiB/s 4.3403 GiB/s]
                 change:
                        time:   [+9.1679% +9.4000% +9.6304%] (p = 0.00 < 0.05)
                        thrpt:  [-8.7844% -8.5923% -8.3979%]
                        Performance has regressed.
Found 8 outliers among 100 measurements (8.00%)
  1 (1.00%) low severe
  1 (1.00%) low mild
  3 (3.00%) high mild
  3 (3.00%) high severe
chars/65536             time:   [15.615 µs 15.691 µs 15.757 µs]
                        thrpt:  [3.8735 GiB/s 3.8899 GiB/s 3.9087 GiB/s]
                 change:
                        time:   [+5.4902% +5.9345% +6.4044%] (p = 0.00 < 0.05)
                        thrpt:  [-6.0190% -5.6021% -5.2045%]
                        Performance has regressed.
Found 2 outliers among 100 measurements (2.00%)
  2 (2.00%) low mild

clip_point/4096         time:   [29.677 µs 29.835 µs 30.019 µs]
                        thrpt:  [130.13 MiB/s 130.93 MiB/s 131.63 MiB/s]
                 change:
                        time:   [-46.306% -45.866% -45.436%] (p = 0.00 < 0.05)
                        thrpt:  [+83.272% +84.728% +86.240%]
                        Performance has improved.
Found 11 outliers among 100 measurements (11.00%)
  3 (3.00%) high mild
  8 (8.00%) high severe
clip_point/65536        time:   [1.5933 ms 1.6116 ms 1.6311 ms]
                        thrpt:  [38.318 MiB/s 38.782 MiB/s 39.226 MiB/s]
                 change:
                        time:   [-30.388% -29.598% -28.717%] (p = 0.00 < 0.05)
                        thrpt:  [+40.286% +42.040% +43.653%]
                        Performance has improved.
Found 3 outliers among 100 measurements (3.00%)
  3 (3.00%) high mild


running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 7 filtered out; finished in 0.00s

point_to_offset/4096    time:   [14.493 µs 14.591 µs 14.707 µs]
                        thrpt:  [265.61 MiB/s 267.72 MiB/s 269.52 MiB/s]
                 change:
                        time:   [-71.990% -71.787% -71.588%] (p = 0.00 < 0.05)
                        thrpt:  [+251.96% +254.45% +257.01%]
                        Performance has improved.
Found 9 outliers among 100 measurements (9.00%)
  5 (5.00%) high mild
  4 (4.00%) high severe
point_to_offset/65536   time:   [700.72 µs 713.75 µs 727.26 µs]
                        thrpt:  [85.939 MiB/s 87.566 MiB/s 89.194 MiB/s]
                 change:
                        time:   [-61.778% -61.015% -60.256%] (p = 0.00 < 0.05)
                        thrpt:  [+151.61% +156.51% +161.63%]
                        Performance has improved.
```

Calling `Rope::chars` got slightly slower but I don't think it's a big
issue (we don't really call `chars` for an entire `Rope`).

In a future pull request, I want to use the tab index (which we're not
yet using) and the char index to make `TabMap` a lot faster.

Release Notes:

- N/A
2024-10-30 10:59:03 +01:00
Mikayla Maki
b3f0ba1430 Implement panic reporting saving and uploads (#19932)
TODO: 
- [x] check that the app version is well formatted for zed.dev

Release Notes:

- N/A

---------

Co-authored-by: Trace <violet.white.batt@gmail.com>
2024-10-29 23:54:00 -07:00
Nate Butler
a5f52f0f04 Use theme families to refine user themes (#19936)
This PR changes the way we load user themes into the ThemeRegistry. 

Rather than directly pass a theme family's themes to
`insert_user_themes`, instead we use the new `refine_theme_family ` and
`ThemeFamily::refine_theme`.

This PR should have net zero change to themes today, but sets up
enabling theme variables. We need to do it this way so each theme has
access to it's family when it is refined.

Release Notes:

- N/A
2024-10-29 22:30:58 -04:00
Nate Butler
63524a2354 Add missing full-size styles for panes (#19935)
As we don't use scrolling flex layouts directly in panes that often, the
methods that would normally be applied to containers that should fill
the space weren't applied here.

Should help un-stuck #19872's layout issue, but I'm merging this change
separately in case it creates some other layout issue in panes.

Release Notes:

- N/A
2024-10-29 21:16:45 -04:00
Marshall Bowers
90edb7189f git_hosting_providers: Clean up tests (#19927)
This PR cleans up the tests for the various Git hosting providers.

These tests had rotted a bit over time, to the point that some of them
weren't even testing what they claimed anymore.

Release Notes:

- N/A
2024-10-29 17:24:32 -04:00
Max Brunsfeld
518f6b529b Fix missing diagnostic and text highlights after blocks (#19920)
Release Notes:

- Fixed an issue where diagnostic underlines and certain text highlights
were not rendered correctly below block decorations such as the inline
assistant prompt.

Co-authored-by: Antonio <antonio@zed.dev>
Co-authored-by: Richard <richard@zed.dev>
2024-10-29 13:47:43 -07:00
Conrad Irwin
fb97e462de Better handle interrupted connections for shared SSH (#19925)
Co-Authored-By: Mikayla <mikayla@zed.dev>
2024-10-29 16:43:34 -04:00
Marshall Bowers
5b7fa05a87 Make Git remote URL parsing more robust (#19924)
This PR improves the parsing of Git remote URLs in order to make
features that depend on them more robust.

Previously we were just treating these as plain strings and doing
one-off shotgun parsing to massage them into the right format. This
meant that we weren't accounting for edge cases in URL structure.

One of these cases was HTTPS Git URLs containing a username, which can
arise when using GitHub Enterprise (see
https://github.com/zed-industries/zed/issues/11160).

We now have a `RemoteUrl` typed to represent a parsed Git remote URL and
use the `Url` parser to parse it.

Release Notes:

- Improved the parsing of Git remote URLs to support additional
scenarios.
2024-10-29 16:19:05 -04:00
Conrad Irwin
d310a1269f SSH Remoting: Fix diagnostic summary syncing (#19923)
Co-Authored-By: Mikayla <mikayla@zed.dev>

Release Notes:

- SSH Remoting: Fix diagnostics summary over collab

Co-authored-by: Mikayla <mikayla@zed.dev>
2024-10-29 13:02:32 -07:00
Mikayla Maki
9818835c9d Fix the log spam from the BlameBuffer request (#19921)
Release Notes:

- N/A
2024-10-29 13:02:21 -07:00
Mikayla Maki
f3b7f5944d Fix a rare crash on startup (#19922)
Release Notes:

- Fixed a rare crash that could happen when certain SQL statements are
prepared
2024-10-29 12:30:55 -07:00
Conrad Irwin
fc5cde9434 Fix quotes in Rust (#19914)
Release Notes:

- (preview only) Fixed quote-autoclose in Rust
2024-10-29 12:35:34 -06:00
Kyle Kelley
6ea4662326 Initial Notebook UI structure (#19756)
This is the start of a notebook UI for Zed. 

`🔔 Note: This won't be useable yet when it is merged! Read below. 🔔`

This is going to be behind a feature flag so that we can merge this
initial PR and then make follow up PRs. Release notes will be produced
in a future PR.

Minimum checklist for merging this:

* [x] All functionality behind the `notebooks` feature flag (with env
var opt out)
* [x] Open notebook files in the workspace
* [x] Remove the "Open Notebook" button from title bar
* [x] Incorporate text style refinements for cell editors
* [x] Rely on `nbformat` crate for parsing the notebook into our
in-memory format
* [x] Move notebook to a `gpui::List`
* [x] Hook up output rendering


Release Notes:

- N/A

---------

Co-authored-by: Nate Butler <iamnbutler@gmail.com>
Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
2024-10-29 11:30:07 -07:00
Joseph T. Lyons
9d12308d06 Fix Julia icon extension lookup (#19916)
Release Notes:

- Fixed a bug where the Julia icon was not displayed for Julia files.
2024-10-29 14:07:54 -04:00
Richard Feldman
21137d2ba7 Delete /workflow (#19900)
This a separate PR from https://github.com/zed-industries/zed/pull/19705
so we can revert it more easily if we want it back later.

Release Notes:

- Added "Suggest Edit" button to the assistant panel if
`"enable_experimental_live_diffs": true` is set in the `"assistant"`
section of `settings.json`. This button takes the place of the previous
`/workflow` command, but it is experimental and may change!

---------

Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
2024-10-29 13:57:33 -04:00
Conrad Irwin
273cb1921f Fix wrong UpdateWorktree chunk size being used in release mode (#19912)
Release Notes:

- Fixed slowness when collaborating

Co-authored-by: Thorsten <thorsten@zed.dev>
2024-10-29 11:22:41 -06:00
Nathan Sobo
cfa20ff221 Sketch in assistant edit button (#19705)
Add an edit button to the assistant. This is totally hacked in for now,
just to see how this would feel rendered simply in the UI.

![CleanShot 2024-10-24 at 16 26
14@2x](https://github.com/user-attachments/assets/e630d078-78b7-42d7-93f1-cf61c00bd20e)

cc @as-cii @danilo-leal 

Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Co-authored-by: Richard Feldman <oss@rtfeldman.com>
2024-10-29 13:21:10 -04:00
Mikayla Maki
759d136fe6 Update a few doc comments (#19911)
Release Notes:

- N/A
2024-10-29 10:09:49 -07:00
Marshall Bowers
322aa41ad6 Add support for self-hosted GitLab instances for Git permalinks (#19909)
This PR adds support for self-hosted GitLab instances when generating
Git permalinks.

If the `origin` Git remote contains `gitlab` in the URL hostname we will
then attempt to register it as a self-hosted GitLab instance.

A note on this: I don't think relying on specific keywords is going to
be a suitable long-term solution to detection. In reality the
self-hosted instance could be hosted anywhere (e.g.,
`vcs.my-company.com`), so we will ultimately need a way to have the user
indicate which Git provider they are using (perhaps via a setting).

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

Release Notes:

- Added support for self-hosted GitLab instances when generating Git
permalinks.
- The instance URL must have `gitlab` somewhere in the host in order to
be recognized.
2024-10-29 12:31:51 -04:00
Max Brunsfeld
3e2f1d733c Fix horizontal scroll caused by diagnostic block width error (#19856)
Previously, when scrolling the diagnostics view with the mouse, we'd get
a spurious horizontal scroll (even if the content was not overflowing
horizontally) due to an error in the widths of the diagnostic blocks.

Release Notes:

- Fixed an issue where the project diagnostics view spuriously allowed
horizontal scrolling by a small amount.
2024-10-29 09:16:38 -07:00
Michael Sloan
3fed738d2f Use same logic for skipping single instance check on Linux as on Mac/Win (#19446)
Release Notes:

- Linux: Now skips check which exits with "zed is already running" when
in development mode or when run with `zed-local`, matching the behavior
on Mac and Windows

Co-authored-by: Nathan Sobo <nathan@zed.dev>
2024-10-29 09:12:34 -07:00
Kirill Bulatov
5893e85708 Ensure shared ssh project propagates buffer changes to all participants (#19907)
Fixed the bug when shared ssh project did not account for client
changing things in their buffers.
Also ensures Prettier formatting workflow works for both ssh project
owner and ssh project clients.

Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad@zed.dev>
2024-10-29 17:24:10 +02:00
Joseph T. Lyons
1356665ed3 Update community links page url (#19899)
See:

- https://github.com/zed-industries/zed.dev/pull/786

Release Notes:

- N/A
2024-10-29 10:13:26 -04:00
Jen Stehlik
9739da8de3 Add Gleam icon (#19887)
I took a shot at creating an icon version of the Gleam logo in response
to https://github.com/zed-industries/zed/pull/19529

Release Notes:

- Added an icon for Gleam files.


![image](https://github.com/user-attachments/assets/97432ded-342f-4d87-8eb2-dc9145513d8c)

<img width="231" alt="Screenshot 2024-10-29 at 9 46 33 AM"
src="https://github.com/user-attachments/assets/c957c98f-3da0-4b92-bc21-2a5adca1daa3">

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-10-29 09:54:21 -04:00
Joseph T. Lyons
249c8a4d96 Remove community content from docs and point to zed.dev (#19895)
The community content now lives on zed.dev, discoverable via the navbar
`resources` menu.

See:

- https://github.com/zed-industries/zed.dev/pull/783

Release Notes:

- N/A
2024-10-29 09:44:58 -04:00
Thorsten Ball
f919fa92de remote servers: Fix title from alpha to beta (#19889)
Discussed this in Slack yesterday. We use `beta` because that's what we
use in the docs as well.

Release Notes:

- N/A
2024-10-29 13:38:30 +01:00
Bennet Bo Fenner
21b58643fa vsc menu: Fix issue when switching branch while non-visible worktree is open (#19888)
Fixes a regression introduced in #19755

<img width="935" alt="Screenshot 2024-10-29 at 12 13 04"
src="https://github.com/user-attachments/assets/7699b8da-631d-4932-89a8-bc5d7f2546f1">

Co-Authored-by: Thorsten <thorsten@zed.dev>

Release Notes:

- Fixed an issue where the branch switcher would show an error, when
opening a file outside of the project

Co-authored-by: Thorsten <thorsten@zed.dev>
2024-10-29 12:27:30 +01:00
Bennet Bo Fenner
6a0bcca9ec ssh remoting: Hide share button while connecting to project (#19885)
Co-Authored-by: Thorsten <thorsten@zed.dev>

Release Notes:

- N/A

Co-authored-by: Thorsten <thorsten@zed.dev>
2024-10-29 12:13:21 +01:00
PJ Tatlow
84328c303b Include commit summary in inline Git blame (#19759)
Closes #19758

Release Notes:

- Added feature to show commit summary as part of the inline Git blame

---------

Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
2024-10-29 11:35:31 +01:00
Bennet Bo Fenner
f7b2b41df9 ssh remoting: Check nightly version correctly by comparing commit SHA (#19884)
This ensures that we detect if a new nightly version of the remote
server is available.
Previously we would always mark a version as matching if they had the
same semantic version.
However, for nightly versions we also need to check if they have the
same commit SHA.

Co-Authored-by: Thorsten <thorsten@zed.dev>

Release Notes:

- N/A

---------

Co-authored-by: Thorsten <thorsten@zed.dev>
2024-10-29 11:32:55 +01:00
Bennet Bo Fenner
7a6b6435c4 languages: Enable grammar loading when compiling with test feature (#19881)
This ensures that `cargo tests -p languages` will not fail with a
confusing error message.

Follow up to #19821

We opted to check the `test` feature flag instead of defining a runtime
flag, because we only want to include the `tree-sitter-*` dependencies
in some cases, which is not possible with a runtime flag.

Co-Authored-by: Thorsten <thorsten@zed.dev>

Release Notes:

- N/A

Co-authored-by: Thorsten <thorsten@zed.dev>
2024-10-29 11:00:44 +01:00
Bennet Bo Fenner
bdb54decdc ssh remoting: Show the host's GitHub name in the titlebar when sharing an SSH project (#19844)
The name (GitHub name) of the host was not displayed when sharing an ssh
project.

Previously we assumed that the a collaborator is a host if the
`replica_id` of the collaborator was `0`,
but for ssh project the `replica_id` is actually `1`.

<img width="329" alt="Screenshot 2024-10-28 at 18 16 30"
src="https://github.com/user-attachments/assets/c0151e12-a96f-4f38-aec1-4ed5475a9eaf">


Co-Authored-by: Thorsten <thorsten@zed.dev>

Release Notes:

- N/A

---------

Co-authored-by: Thorsten <thorsten@zed.dev>
2024-10-29 09:52:54 +01:00
Bennet Bo Fenner
b5c41eeb98 Future-proof indent guides settings for panels (#19878)
This PR ensures that we do not have to break the indent guides settings
for the project/outline panel. In the future we might want to have a
more granular way to control when to show indent guides, or control
other indent guide properties, like its width.

Release Notes:

- N/A
2024-10-29 09:52:36 +01:00
Conrad Irwin
719a7f7890 Fix block cursor on graphemes (#19867)
Release Notes:

- Fixed block cursor rendering only first char of multii-char graphemes.
2024-10-28 21:05:24 -06:00
Joseph T. Lyons
1b84fee708 restore editor::UnfoldRecursive binding (#19865) 2024-10-28 22:32:59 -04:00
Conrad Irwin
58e5d4ff02 Reland invisibles (#19846)
Release Notes:

- Show invisibles in the editor

Relands #19298 

Trying to quantify a performance impact, it doesn't seem to impact much
visible in Instruments or in a micro-benchmark of Editor#layout_lines.
We're still taking a few hundred micro-seconds (+/- a lot) every time.
The ascii file has just ascii, where as the cc file has one control
character per line.

<img width="1055" alt="Screenshot 2024-10-28 at 12 14 53"
src="https://github.com/user-attachments/assets/1c382063-bb19-4e92-bbba-ed5e7c02309f">
<img width="1020" alt="Screenshot 2024-10-28 at 12 15 07"
src="https://github.com/user-attachments/assets/1789f65e-5f83-4c32-be47-7748c62c3703">
2024-10-28 20:27:09 -06:00
Mikayla Maki
85ff03cde0 Add more context to the save new file path picker (#19863)
Release Notes:

- N/A

Co-authored-by: Conrad <conrad@zed.dev>
2024-10-28 17:21:41 -07:00
Conrad Irwin
a3f0bb4547 SSH Remoting: Document manual binary management (#19862)
Release Notes:

- N/A
2024-10-28 17:10:08 -06:00
Marshall Bowers
93b20008e0 Add support for Doxygen doc comments in C++ (#19858)
This PR adds support for Doxygen-style doc comments in C++.

<img width="962" alt="Screenshot 2024-10-28 at 5 38 34 PM"
src="https://github.com/user-attachments/assets/57d0fa4b-07c1-4b71-899a-fba78e822e8f">

https://www.doxygen.nl/manual/docblocks.html

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

Release Notes:

- C++: Added support for Doxygen-style doc comments starting with `/// `
or `//! `.
2024-10-28 17:44:23 -04:00
ClanEver
188a893fd0 python: Enhance syntax highlighting for type hints (#18185)
Release Notes:

- Python: Improved syntax highlighting for type hints.

# Before

![image](https://github.com/user-attachments/assets/876a69ab-a572-4d1b-af99-e6f85f249ea6)

# After

![image](https://github.com/user-attachments/assets/4fb98a9b-bc5d-4799-b535-057047884383)

---
Why manual recursion?
- Due to tree-sitter grammar not supporting recursion in query
(https://github.com/tree-sitter-grammars/tree-sitter-lua/issues/24),
currently only manual recursion is possible (refer to
https://github.com/projekt0n/github-nvim-theme/pull/250/files).

<br/>

Unable to highlight when simple structures appear before complex
structures, example:
```python
def t() -> str | dict[int, dict[int, dict[int, str]]]:
    pass
```
Because complex structures are parsed as `subscript` rather than
`generic_type` by tree-sitter in this case ☹

<br/>

Related:

- https://github.com/zed-industries/zed/issues/14715
- [Union Type (Python
Doc)](https://docs.python.org/3/library/stdtypes.html#union-type)
- [Type parameter lists (Python
Doc)](https://docs.python.org/3/reference/compound_stmts.html#type-parameter-lists)

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-10-28 16:56:59 -04:00
Piotr Osiewicz
052b746fbd language_selector: Fix debug_assert firing off on context menu creation for LSP view (#19854)
Closes #ISSUE

Release Notes:

- N/A
2024-10-28 21:55:38 +01:00
Mikayla Maki
80f89059aa Fix mouse clicks on remote-open-folder UI (#19851)
Also change Zed's standard style to use
`.track_focus(&self.focus_handle(cx))`, instead of
`.track_focus(&self.focus_handle)`, to catch these kinds of errors more
easily in the future.

Release Notes:

- N/A

---------

Co-authored-by: Conrad <conrad@zed.dev>
2024-10-28 12:55:55 -07:00
Mikayla Maki
826d83edfe Fix backtrace spam on remote server (#19850)
Release Notes:

- N/A

Co-authored-by: conrad <conrad@zed.dev>
2024-10-28 12:28:42 -07:00
Joseph T. Lyons
f5d5fab2c8 Improve fold_at_level performance (#19845)
Just spotted a tiny error that was causing us to continue looking for
nested folds 1 layer deeper than any fold already found at the target
level. We shouldn't continue to seek for a deeper fold after the fold at
the target level is found.

Tested on a debug build and used `editor.rs` as the source material:

```
Old

Level 1 fold:
[crates/editor/src/editor.rs:10777:9] counter = 2806
[crates/editor/src/editor.rs:10778:9] time_elapsed = 320.570792ms

Level 2 fold:
[crates/editor/src/editor.rs:10777:9] counter = 5615
[crates/editor/src/editor.rs:10778:9] time_elapsed = 497.4305ms

Level 3 fold:
[crates/editor/src/editor.rs:10777:9] counter = 7528
[crates/editor/src/editor.rs:10778:9] time_elapsed = 619.818334ms

New

Level 1 fold:
[crates/editor/src/editor.rs:10776:9] counter = 543
[crates/editor/src/editor.rs:10777:9] time_elapsed = 139.115625ms

Level 2 fold:
[crates/editor/src/editor.rs:10776:9] counter = 2806
[crates/editor/src/editor.rs:10777:9] time_elapsed = 312.560416ms

Level 3 fold:
[crates/editor/src/editor.rs:10776:9] counter = 5615
[crates/editor/src/editor.rs:10777:9] time_elapsed = 498.873292ms
```

Release Notes:

- N/A
2024-10-28 13:37:28 -04:00
Thorsten Ball
fab2f22a89 remote project: Fix project reference leak when waiting for prompt reply (#19838)
When the language server gave us a prompt and we'd close the window, we
wouldn't release the `project` until the next `flush_effects` call that
came in when opening a window.

With this change, we no longer hold a strong reference to the project in
the future. Fixes the leak and makes sure we clean up the SSH connection
when closing a window.

Release Notes:

- N/A

Co-authored-by: Bennet <bennet@zed.dev>
2024-10-28 17:07:30 +01:00
Marshall Bowers
a451bcc3c4 collab: Exempt staff from LLM usage limits (#19836)
This PR updates the usage limit check to exempt Zed staff members from
usage limits.

We previously had some affordances for the rate limits, but hadn't yet
updated it for the usage-based billing.

Release Notes:

- N/A
2024-10-28 11:45:18 -04:00
Marshall Bowers
5e9ff3e313 dart: Bump to v0.1.2 (#19835)
This PR bumps the Dart extension to v0.1.2.

Changes:

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

Release Notes:

- N/A
2024-10-28 11:36:44 -04:00
Thorsten Ball
cc81f19c68 remote server: Fix error log about inability to open buffer (#19824)
Turns out that we used client-side `fs` to check whether something is a
directory or not, which obviously doesn't work with SSH projects.

Release Notes:

- N/A

---------

Co-authored-by: Bennet <bennet@zed.dev>
2024-10-28 16:35:37 +01:00
Ömer Sinan Ağacan
5e89fba681 dart: Add support for documentation comments (#19592)
Closes #19590

Release Notes:

- N/A
---

I'm unable to test this because rebuilding Zed with the changes does not
seem to use the changes. If maintainers could let me know how to test
these changes I'd like to verify that this really fixes #19590.

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-10-28 11:20:04 -04:00
Thorsten Ball
67eb652bf1 remote servers: Always dismiss modal (#19831)
We display the errors in another window anyway and if the connection
takes a while it looks like a bug that the modal stays open.

Release Notes:

- N/A

Co-authored-by: Bennet <bennet@zed.dev>
2024-10-28 16:12:37 +01:00
Bennet Bo Fenner
e0ea9a9ab5 Remove leftover comments from previous PR (#19820)
Co-Authored-by: Thorsten <thorsten@zed.dev>

Removes some leftover comments from #19766 

Release Notes:

- N/A

Co-authored-by: Thorsten <thorsten@zed.dev>
2024-10-28 16:00:38 +01:00
xdBronch
ff29a34298 zig: Account for doctests in outline (#19776)
zig has a feature called
[doctests](https://ziglang.org/documentation/master/#Doctests) where
instead of providing a string as the name of a test you use an
identifier so that the test is "tied" to it and can be used in
documentation. this wasnt accounted for so any tests using this were
unnamed in the outline

Release Notes:

- N/A
2024-10-28 10:49:40 -04:00
Thorsten Ball
6686f66949 ollama: Ensure only single task fetches models (#19830)
Before this change, we'd see a ton of requests from the Ollama provider
trying to fetch models:

```
[2024-10-28T15:00:52+01:00 DEBUG reqwest::connect] starting new connection: http://localhost:11434/
[2024-10-28T15:00:52+01:00 DEBUG reqwest::connect] starting new connection: http://localhost:11434/
[2024-10-28T15:00:52+01:00 DEBUG reqwest::connect] starting new connection: http://localhost:11434/
[2024-10-28T15:00:52+01:00 DEBUG reqwest::connect] starting new connection: http://localhost:11434/
[2024-10-28T15:00:52+01:00 DEBUG reqwest::connect] starting new connection: http://localhost:11434/
[2024-10-28T15:00:52+01:00 DEBUG reqwest::connect] starting new connection: http://localhost:11434/
[2024-10-28T15:00:52+01:00 DEBUG reqwest::connect] starting new connection: http://localhost:11434/
[2024-10-28T15:00:52+01:00 DEBUG reqwest::connect] starting new connection: http://localhost:11434/
[2024-10-28T15:00:52+01:00 DEBUG reqwest::connect] starting new connection: http://localhost:11434/
[2024-10-28T15:00:52+01:00 DEBUG reqwest::connect] starting new connection: https://api.zed.dev/
[2024-10-28T15:00:52+01:00 DEBUG reqwest::connect] starting new connection: http://localhost:11434/
[2024-10-28T15:00:52+01:00 DEBUG reqwest::connect] starting new connection: http://localhost:11434/
[2024-10-28T15:00:52+01:00 DEBUG reqwest::connect] starting new connection: http://localhost:11434/
[2024-10-28T15:00:52+01:00 DEBUG reqwest::connect] starting new connection: http://localhost:11434/
[2024-10-28T15:00:52+01:00 DEBUG reqwest::connect] starting new connection: http://localhost:11434/
[2024-10-28T15:00:52+01:00 DEBUG reqwest::connect] starting new connection: http://localhost:11434/
[2024-10-28T15:00:52+01:00 DEBUG reqwest::connect] starting new connection: http://localhost:11434/
```

Turns out we'd send a request on *every* change to settings.

Now, with this change, we only send a single request.

Release Notes:

- N/A

Co-authored-by: Bennet <bennet@zed.dev>
2024-10-28 15:40:50 +01:00
David Soria Parra
8a96ea25c4 context_servers: Support tools (#19548)
This PR depends on #19547 

This PR adds support for tools from context servers. Context servers are
free to expose tools that Zed can pass to models. When called by the
model, Zed forwards the request to context servers. This allows for some
interesting techniques. Context servers can easily expose tools such as
querying local databases, reading or writing local files, reading
resources over authenticated APIs (e.g. kubernetes, asana, etc).

This is currently experimental. 

Things to discuss
* I want to still add a confirm dialog asking people if a server is
allows to use the tool. Should do this or just use the tool and assume
trustworthyness of context servers?
* Can we add tool use behind a local setting flag?

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
2024-10-28 10:37:58 -04:00
Piotr Osiewicz
cdddb4d360 Add language toolchains (#19576)
This PR adds support for selecting toolchains for a given language (e.g.
Rust toolchains or Python virtual environments) with support for SSH
projects provided out of the box. For Python we piggy-back off of
[PET](https://github.com/microsoft/python-environment-tools), a library
maintained by Microsoft.
Closes #16421
Closes #7646

Release Notes:

- Added toolchain selector to the status bar (with initial support for
Python virtual environments)
2024-10-28 15:34:03 +01:00
Thorsten Ball
03bd95405b docs: Add diagram to remote development docs (#19827)
Release Notes:

- N/A
2024-10-28 14:14:51 +01:00
Kirill Bulatov
177dfdf900 Declare RUSTFLAGS env var for all CI jobs (#19826)
Follow-up of https://github.com/zed-industries/zed/pull/19149 

Makes RUSTFLAGS propagation uniform, to ensure all `cargo ...` jobs get
the same RUSTFLAGS env set.

Release Notes:

- N/A
2024-10-28 14:53:40 +02:00
Thorsten Ball
2ab0b3b819 remote server: Fix language servers not starting (#19821)
PR #19653 change the code in this diff, which lead to the remote_server
binary trying to load language grammars, which in turn failed, and
stopped languages from being loaded correctly.

That then lead to language servers not starting up.

This change reintroduces what #19653 removed, so that we don't load the
grammar on the remote_server, by ignoring the grammar name from the
config. The tests still all work.


Release Notes:

- N/A

Co-authored-by: Bennet <bennet@zed.dev>
2024-10-28 11:02:46 +01:00
Bennet Bo Fenner
888fec9299 outline panel: Add indent guides (#19719)
See #12673

| File | Search |
|--------|--------|
| <img width="302" alt="image"
src="https://github.com/user-attachments/assets/44b8d5f9-8446-41b5-8c0f-e438050f0ac9">
| <img width="301" alt="image"
src="https://github.com/user-attachments/assets/a2e6f77b-6d3b-4f1c-8fcb-16bd35274807">
|



Release Notes:

- Added indent guides to the outline panel
2024-10-28 09:54:18 +01:00
Bennet Bo Fenner
e86b096b92 docs: Add indent_guides setting to project panel docs (#19819)
Follow up to #18260 

Release Notes:

- N/A
2024-10-28 09:45:19 +01:00
Mikayla Maki
ffe36c9beb Remove hosted projects (#19754)
Release Notes:

- N/A
2024-10-27 19:44:21 -07:00
Kirill Bulatov
2d16d2d036 Fixed outline panel panicking on filtering (#19811)
Closes https://github.com/zed-industries/zed/issues/19732

Release Notes:

- Fixed outline panel panicking on filtering
([#19732](https://github.com/zed-industries/zed/issues/19732))
2024-10-28 02:31:02 +02:00
Mikayla Maki
c69da2df70 Add support for git branches on remote projects (#19755)
Release Notes:

- Fixed a bug where the branch switcher could not be used remotely.
2024-10-27 15:50:54 -07:00
CharlesChen0823
5506669b06 windows: Fix more windows platform test (#19802)
Release Notes:

- N/A

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2024-10-27 19:15:23 +02:00
Bennet Bo Fenner
b13940720a markdown preview: Ignore inline HTML tags in text (#19804)
Follow up to #19785

This PR ensures that we explicitly ignore inline HTML tags so that we
can still extract the text between the tags and show them to the user

Release Notes:

- N/A
2024-10-27 14:34:59 +01:00
Peter Tripp
db61711753 ci: Don't run GitHub Actions workflows on forks (#19789)
- Closes: https://github.com/zed-industries/zed/issues/19351

Release Notes:

- N/A
2024-10-27 09:04:52 -04:00
Joseph T. Lyons
c12a9f2673 Add fold_at_level test (#19800) 2024-10-26 21:57:55 -04:00
Kirill Bulatov
2e32f1c8a1 Restore horizontal scrollbar checks (#19767)
Closes https://github.com/zed-industries/zed/issues/19637

Follow-up of https://github.com/zed-industries/zed/pull/18927 , restores
the condition that removed the horizontal scrollbar when panel's items
are not long enough.

Release Notes:

- Fixed horizontal scrollbar not being hidden
([#19637](https://github.com/zed-industries/zed/issues/19637))
2024-10-26 21:57:22 +03:00
Bennet Bo Fenner
03a1c8d2b8 markdown preview: Fix infinite loop in parser when parsing list items (#19785)
Release Notes:

- Fixed an issue with the markdown parser when opening a markdown
preview file that contained HTML tags inside a list item
2024-10-26 14:59:46 +02:00
Kirill Bulatov
d7a277607b Fix a few Windows tests (#19773) 2024-10-26 03:32:22 +03:00
Thorsten Ball
fc8a72cdd8 WIP: ssh remoting: Add upload_binary field to SshConnections (#19748)
This removes the old `remote_server { "download_binary_on_host": bool }`
field and replaces it with a `upload_binary: bool` on every
`ssh_connection`.


@ConradIrwin it compiles, it connects, but I haven't tested it really
yet

Release Notes:

- N/A

---------

Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-10-25 17:32:54 -06:00
Kirill Bulatov
1acebb3c47 Remove another false-positive Danger message (#19769)
Follow-up of https://github.com/zed-industries/zed/pull/19151

Ignores any URLs aftrer `Release Notes:` (if present) and after
`Follow-up of` and `Part of` words.


Release Notes:

- N/A
2024-10-26 01:55:46 +03:00
Conrad Irwin
78ed0c9312 vim: Copy comment to new lines with o/O (#19766)
Co-Authored-By: Kurt Wolf <kurtwolfbuilds@gmail.com>

Closes: #4691

Closes #ISSUE

Release Notes:

- vim: o/O now respect `extend_comment_on_newline`
2024-10-25 16:47:44 -06:00
Conrad Irwin
98d2e5fe73 Quote fixes (#19765)
Closes #19372

Release Notes:

- Fixed autoclosing quotes when the string is already open.
- Added autoclosing of rust multiline strings

---------

Co-authored-by: Kurt Wolf <kurtwolfbuilds@gmail.com>
2024-10-25 16:28:08 -06:00
Max Brunsfeld
4325819075 Fix more failure cases of assistant edits (#19653)
* Make `description` optional (since we describe it as optional in the
prompt, and we're currently not showing it)
* Fix fuzzy location bug that neglected the cost of deleting prefixes of
the query.
* Make auto-indent work for single-line edits. Previously, auto-indent
would not occur when overwriting a single line (without inserting or
deleting a newline)

Release Notes:

- N/A
2024-10-25 14:30:34 -07:00
Marshall Bowers
c19c89e6df collab: Include checkout_complete query parameter after checking out (#19763)
This PR updates the checkout flow to include the `?checkout_complete=1`
query parameter after successfully checking out.

We'll use this on the account page to adapt the UI accordingly.

Release Notes:

- N/A
2024-10-25 16:29:20 -04:00
Joseph T. Lyons
507929cb79 Add editor: fold at level <level> commands (#19750)
Closes https://github.com/zed-industries/zed/issues/5142

Note that I only moved the cursor to the top of the file so it wouldn't
jump - the commands work no matter where you are in the file.


https://github.com/user-attachments/assets/78c74ca6-5c17-477c-b5d1-97c5665e44b0

Also, is VS Code doing this right thing here? or is it busted?


https://github.com/user-attachments/assets/8c503b50-9671-4221-b9f8-1e692fe8cd9a

Release Notes:

- Added `editor: fold at level <level>` commands. macOS: `cmd-k,
cmd-<number>`, Linux: `ctrl-k, ctrl-<number>`.

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2024-10-25 16:08:37 -04:00
Max Brunsfeld
7d0a7aff44 Fix condition for re-using highlights when seeking buffer chunks iterator (#19760)
Fixes a syntax highlighting regression introduced in
https://github.com/zed-industries/zed/pull/19531, which caused syntax
highlighting to be missing after any block.

Release Notes:

- N/A
2024-10-25 12:37:03 -07:00
Kirill Bulatov
92ba18342c Properly deserialize active pane in the workspace (#19744)
Without setting the active pane metadata, no center pane events are
emitted on start before the pane is focused manually, which breaks
deserialization of other components like outline panel, which should
show the active pane's active item outlines on start.

Release Notes:

- N/A

Co-authored-by: Thorsten Ball <thorsten@zed.dev>
2024-10-25 22:04:09 +03:00
Kirill Bulatov
6de5ace116 Update outline panel representation when a theme is changed (#19747)
Release Notes:

- N/A
2024-10-25 22:04:02 +03:00
Richard Feldman
c9db1b9a7b Add keybindings for accepting hunks (#19749)
I went with Cmd-Shift-Y on macOS (Ctrl-Shift-Y on Linux) for "yes accept
this individual hunk" - both are currently unused.

I went with Cmd-Shift-A on macOS (Ctrl-Alt-A on Linux) for "accept all
hunks" - both are unused. (Ctrl-Shift-A on Linux was taken, as is
Ctrl-Alt-Y, so although the pairing of Ctrl-Shift-Y and Ctrl-Alt-A isn't
necessarily obvious, the letters seem intuitive - "yes" and "all" - and
those key combinations don't conflict with anything.)

Release Notes:

- Added keybindings for applying hunks in Proposed Changes
<img width="247" alt="Screenshot 2024-10-25 at 12 47 00 PM"
src="https://github.com/user-attachments/assets/d6355621-ba80-4ee2-8918-b7239a4d29be">
2024-10-25 14:10:04 -04:00
Nathan Sobo
24cb694494 Update placeholder text with key bindings to focus context panel and navigate history (#19447)
Hopefully, this will help people understand how easy it is to add
context to an inline transformation.

![CleanShot 2024-10-18 at 22 41
00@2x](https://github.com/user-attachments/assets/c09c1d89-3df2-4079-9849-9de7ac63c003)

@as-cii @maxdeviant @rtfeldman could somebody update this to display the
actual correct key bindings and ship it. I have them hard coded for now.

Release Notes:

- Updated placeholder text with key bindings to focus context panel and
navigate history.

---------

Co-authored-by: Richard Feldman <oss@rtfeldman.com>
2024-10-25 14:09:21 -04:00
Conrad Irwin
85bdd9329b Revert "Show invisibles in editor (#19298)" (#19752)
Closes: #19714

This reverts commit 6dcec47235.

Release Notes:

- (preview only) Fixes a crash when rendering invisibles
2024-10-25 11:59:22 -06:00
283 changed files with 13715 additions and 5663 deletions

View File

@@ -25,8 +25,194 @@ env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: 1
RUSTFLAGS: "-D warnings"
jobs:
migration_checks:
name: Check Postgres and Protobuf migrations, mergability
if: github.repository_owner == 'zed-industries'
timeout-minutes: 60
runs-on:
- self-hosted
- test
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
fetch-depth: 0 # fetch full history
- name: Remove untracked files
run: git clean -df
- name: Find modified migrations
shell: bash -euxo pipefail {0}
run: |
export SQUAWK_GITHUB_TOKEN=${{ github.token }}
. ./script/squawk
- name: Ensure fresh merge
shell: bash -euxo pipefail {0}
run: |
if [ -z "$GITHUB_BASE_REF" ];
then
echo "BUF_BASE_BRANCH=$(git merge-base origin/main HEAD)" >> $GITHUB_ENV
else
git checkout -B temp
git merge -q origin/$GITHUB_BASE_REF -m "merge main into temp"
echo "BUF_BASE_BRANCH=$GITHUB_BASE_REF" >> $GITHUB_ENV
fi
- uses: bufbuild/buf-setup-action@v1
with:
version: v1.29.0
- uses: bufbuild/buf-breaking-action@v1
with:
input: "crates/proto/proto/"
against: "https://github.com/${GITHUB_REPOSITORY}.git#branch=${BUF_BASE_BRANCH},subdir=crates/proto/proto/"
style:
timeout-minutes: 60
name: Check formatting and spelling
if: github.repository_owner == 'zed-industries'
runs-on:
- buildjet-8vcpu-ubuntu-2204
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Run style checks
uses: ./.github/actions/check_style
- name: Check for typos
uses: crate-ci/typos@8e6a4285bcbde632c5d79900a7779746e8b7ea3f # v1.24.6
with:
config: ./typos.toml
macos_tests:
timeout-minutes: 60
name: (macOS) Run Clippy and tests
if: github.repository_owner == 'zed-industries'
runs-on:
- self-hosted
- test
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: cargo clippy
run: ./script/clippy
- name: Check unused dependencies
uses: bnjbvr/cargo-machete@main
- name: Check licenses
run: |
script/check-licenses
script/generate-licenses /tmp/zed_licenses_output
- name: Run tests
uses: ./.github/actions/run_tests
- name: Build collab
run: cargo build -p collab
- name: Build other binaries and features
run: |
cargo build --workspace --bins --all-features
cargo check -p gpui --features "macos-blade"
cargo build -p remote_server
linux_tests:
timeout-minutes: 60
name: (Linux) Run Clippy and tests
if: github.repository_owner == 'zed-industries'
runs-on:
- buildjet-16vcpu-ubuntu-2204
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Cache dependencies
uses: swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-provider: "buildjet"
- name: Install Linux dependencies
run: ./script/linux
- name: cargo clippy
run: ./script/clippy
- name: Run tests
uses: ./.github/actions/run_tests
- name: Build Zed
run: cargo build -p zed
build_remote_server:
timeout-minutes: 60
name: (Linux) Build Remote Server
if: github.repository_owner == 'zed-industries'
runs-on:
- buildjet-16vcpu-ubuntu-2204
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Cache dependencies
uses: swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-provider: "buildjet"
- name: Install Clang & Mold
run: ./script/remote-server && ./script/install-mold 2.34.0
- name: Build Remote Server
run: cargo build -p remote_server
# todo(windows): Actually run the tests
windows_tests:
timeout-minutes: 60
name: (Windows) Run Clippy and tests
if: github.repository_owner == 'zed-industries'
runs-on: hosted-windows-1
steps:
# more info here:- https://github.com/rust-lang/cargo/issues/13020
- name: Enable longer pathnames for git
run: git config --system core.longpaths true
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Cache dependencies
uses: swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-provider: "github"
- name: cargo clippy
# Windows can't run shell scripts, so we need to use `cargo xtask`.
run: cargo xtask clippy
- name: Build Zed
run: cargo build
bundle-mac:
timeout-minutes: 60
name: Create a macOS bundle
@@ -34,6 +220,7 @@ jobs:
- self-hosted
- bundle
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
needs: [macos_tests]
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
@@ -109,6 +296,7 @@ jobs:
- uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
name: Upload app bundle to release
if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }}
with:
draft: true
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
@@ -121,3 +309,97 @@ jobs:
body_path: target/release-notes.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
bundle-linux:
timeout-minutes: 60
name: Create a Linux bundle
runs-on:
- buildjet-16vcpu-ubuntu-2004
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
needs: [linux_tests]
env:
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Install Linux dependencies
run: ./script/linux && ./script/install-mold 2.34.0
- name: Determine version and release channel
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
run: |
# This exports RELEASE_CHANNEL into env (GITHUB_ENV)
script/determine-release-channel
- name: Create Linux .tar.gz bundle
run: script/bundle-linux
- name: Upload Linux bundle to workflow run if main branch or specific label
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.tar.gz
path: target/release/zed-*.tar.gz
- name: Upload app bundle to release
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
with:
draft: true
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
files: |
target/zed-remote-server-linux-x86_64.gz
target/release/zed-linux-x86_64.tar.gz
body: ""
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
bundle-linux-aarch64: # this runs on ubuntu22.04
timeout-minutes: 60
name: Create arm64 Linux bundle
runs-on:
- buildjet-16vcpu-ubuntu-2204-arm
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
needs: [linux_tests]
env:
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Install Linux dependencies
run: ./script/linux
- name: Determine version and release channel
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
run: |
# This exports RELEASE_CHANNEL into env (GITHUB_ENV)
script/determine-release-channel
- name: Create and upload Linux .tar.gz bundle
run: script/bundle-linux
- name: Upload Linux bundle to workflow run if main branch or specific label
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-aarch64-unknown-linux-gnu.tar.gz
path: target/release/zed-*.tar.gz
- name: Upload app bundle to release
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
with:
draft: true
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
files: |
target/zed-remote-server-linux-aarch64.gz
target/release/zed-linux-aarch64.tar.gz
body: ""
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,3 +1,3 @@
# Code of Conduct
The Code of Conduct for this repository can be found online at [zed.dev/docs/code-of-conduct](https://zed.dev/docs/code-of-conduct).
The Code of Conduct for this repository can be found online at [zed.dev/code-of-conduct](https://zed.dev/code-of-conduct).

View File

@@ -2,7 +2,7 @@
Thanks for your interest in contributing to Zed, the collaborative platform that is also a code editor!
All activity in Zed forums is subject to our [Code of Conduct](https://zed.dev/docs/code-of-conduct). Additionally, contributors must sign our [Contributor License Agreement](https://zed.dev/cla) before their contributions can be merged.
All activity in Zed forums is subject to our [Code of Conduct](https://zed.dev/code-of-conduct). Additionally, contributors must sign our [Contributor License Agreement](https://zed.dev/cla) before their contributions can be merged.
## Contribution ideas

582
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -117,6 +117,7 @@ members = [
"crates/theme_selector",
"crates/time_format",
"crates/title_bar",
"crates/toolchain_selector",
"crates/ui",
"crates/ui_input",
"crates/ui_macros",
@@ -137,7 +138,6 @@ members = [
"extensions/astro",
"extensions/clojure",
"extensions/csharp",
"extensions/dart",
"extensions/deno",
"extensions/elixir",
"extensions/elm",
@@ -290,6 +290,7 @@ theme_importer = { path = "crates/theme_importer" }
theme_selector = { path = "crates/theme_selector" }
time_format = { path = "crates/time_format" }
title_bar = { path = "crates/title_bar" }
toolchain_selector = { path = "crates/toolchain_selector" }
ui = { path = "crates/ui" }
ui_input = { path = "crates/ui_input" }
ui_macros = { path = "crates/ui_macros" }
@@ -369,6 +370,7 @@ linkify = "0.10.0"
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
markup5ever_rcdom = "0.3.0"
nanoid = "0.4"
nbformat = "0.3.1"
nix = "0.29"
num-format = "0.4.4"
once_cell = "1.19.0"
@@ -376,6 +378,11 @@ ordered-float = "2.1.1"
palette = { version = "0.7.5", default-features = false, features = ["std"] }
parking_lot = "0.12.1"
pathdiff = "0.2"
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
postage = { version = "0.5", features = ["futures-traits"] }
pretty_assertions = "1.3.0"
profiling = "1"
@@ -384,6 +391,7 @@ prost-build = "0.9"
prost-types = "0.9"
pulldown-cmark = { version = "0.12.0", default-features = false }
rand = "0.8.5"
rayon = "1.8"
regex = "1.5"
repair_json = "0.1.0"
reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "fd110f6998da16bbca97b6dddda9be7827c50e29", default-features = false, features = [
@@ -395,7 +403,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "fd110f
"stream",
] }
rsa = "0.9.6"
runtimelib = { version = "0.15", default-features = false, features = [
runtimelib = { version = "0.16.0", default-features = false, features = [
"async-dispatcher-runtime",
] }
rustc-demangle = "0.1.23"
@@ -464,7 +472,7 @@ tree-sitter-typescript = "0.23"
tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "baff0b51c64ef6a1fb1f8390f3ad6015b83ec13a" }
unicase = "2.6"
unindent = "0.1.7"
unicode-segmentation = "1.11"
unicode-segmentation = "1.10"
url = "2.2"
uuid = { version = "1.1.2", features = ["v4", "v5", "serde"] }
wasmparser = "0.215"

View File

@@ -58,6 +58,7 @@
"gitignore": "vcs",
"gitkeep": "vcs",
"gitmodules": "vcs",
"gleam": "gleam",
"go": "go",
"gql": "graphql",
"graphql": "graphql",
@@ -83,6 +84,7 @@
"j2k": "image",
"java": "java",
"jfif": "image",
"jl": "julia",
"jp2": "image",
"jpeg": "image",
"jpg": "image",
@@ -90,7 +92,6 @@
"json": "storage",
"jsonc": "storage",
"jsx": "react",
"julia": "julia",
"jxl": "image",
"kt": "kotlin",
"ldf": "storage",
@@ -264,6 +265,9 @@
"fsharp": {
"icon": "icons/file_icons/fsharp.svg"
},
"gleam": {
"icon": "icons/file_icons/gleam.svg"
},
"go": {
"icon": "icons/file_icons/go.svg"
},

View File

@@ -0,0 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.3848 9.30444C7.3848 9.30444 7.53254 10.2646 8.53248 10.0882C9.53242 9.91193 9.36378 8.95549 9.36378 8.95549" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.54155 5.54157C6.12355 4.90104 6.01688 2.62541 7.22875 2.3985C8.44063 2.17158 9.19097 4.33148 9.91982 4.6814C10.6487 5.03133 12.8517 4.3028 13.4381 5.38734C14.0244 6.47188 12.1395 7.95973 12.026 8.64088C11.9126 9.32203 13.3614 11.2416 12.4675 12.1701C11.5736 13.0986 9.73005 11.7545 8.90486 11.8834C8.07966 12.0123 6.79244 13.9095 5.67367 13.3502C4.55491 12.7909 5.16702 10.5455 4.82437 9.87612C4.48171 9.20673 2.34028 8.54978 2.4525 7.35049C2.56471 6.15121 4.95956 6.1821 5.54155 5.54157Z" stroke="#FF7676" stroke-opacity="0.52" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.54155 5.54157C6.12355 4.90104 6.01688 2.62541 7.22875 2.3985C8.44063 2.17158 9.19097 4.33148 9.91982 4.6814C10.6487 5.03133 12.8517 4.3028 13.4381 5.38734C14.0244 6.47188 12.1395 7.95973 12.026 8.64088C11.9126 9.32203 13.3614 11.2416 12.4675 12.1701C11.5736 13.0986 9.73005 11.7545 8.90486 11.8834C8.07966 12.0123 6.79244 13.9095 5.67367 13.3502C4.55491 12.7909 5.16702 10.5455 4.82437 9.87612C4.48171 9.20673 2.34028 8.54978 2.4525 7.35049C2.56471 6.15121 4.95956 6.1821 5.54155 5.54157Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="6.25098" cy="7.75" r="0.75" fill="black"/>
<circle cx="10.1035" cy="7.25" r="0.75" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

7
assets/icons/list_x.svg Normal file
View File

@@ -0,0 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.33333 8H3" stroke="#FBF1C7" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.6667 4H3" stroke="#FBF1C7" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.6667 12H3" stroke="#FBF1C7" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.6667 6.66663L11 9.33329" stroke="#FBF1C7" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11 6.66663L13.6667 9.33329" stroke="#FBF1C7" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 579 B

1
assets/icons/wand.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-wand"><path d="M15 4V2"/><path d="M15 16v-2"/><path d="M8 9h2"/><path d="M20 9h2"/><path d="M17.8 11.8 19 13"/><path d="M15 9h.01"/><path d="M17.8 6.2 19 5"/><path d="m3 21 9-9"/><path d="M12.2 6.2 11 5"/></svg>

After

Width:  |  Height:  |  Size: 414 B

View File

@@ -313,6 +313,15 @@
"ctrl-k ctrl-l": "editor::ToggleFold",
"ctrl-k ctrl-[": "editor::FoldRecursive",
"ctrl-k ctrl-]": "editor::UnfoldRecursive",
"ctrl-k ctrl-1": ["editor::FoldAtLevel", { "level": 1 }],
"ctrl-k ctrl-2": ["editor::FoldAtLevel", { "level": 2 }],
"ctrl-k ctrl-3": ["editor::FoldAtLevel", { "level": 3 }],
"ctrl-k ctrl-4": ["editor::FoldAtLevel", { "level": 4 }],
"ctrl-k ctrl-5": ["editor::FoldAtLevel", { "level": 5 }],
"ctrl-k ctrl-6": ["editor::FoldAtLevel", { "level": 6 }],
"ctrl-k ctrl-7": ["editor::FoldAtLevel", { "level": 7 }],
"ctrl-k ctrl-8": ["editor::FoldAtLevel", { "level": 8 }],
"ctrl-k ctrl-9": ["editor::FoldAtLevel", { "level": 9 }],
"ctrl-k ctrl-0": "editor::FoldAll",
"ctrl-k ctrl-j": "editor::UnfoldAll",
"ctrl-space": "editor::ShowCompletions",
@@ -505,6 +514,13 @@
"ctrl-enter": "assistant::InlineAssist"
}
},
{
"context": "ProposedChangesEditor",
"bindings": {
"ctrl-shift-y": "editor::ApplyDiffHunk",
"ctrl-alt-a": "editor::ApplyAllDiffHunks"
}
},
{
"context": "Editor && jupyter && !ContextEditor",
"bindings": {
@@ -516,6 +532,7 @@
"context": "ContextEditor > Editor",
"bindings": {
"ctrl-enter": "assistant::Assist",
"ctrl-shift-enter": "assistant::Edit",
"ctrl-s": "workspace::Save",
"ctrl->": "assistant::QuoteSelection",
"ctrl-<": "assistant::InsertIntoEditor",

View File

@@ -201,6 +201,7 @@
"context": "ContextEditor > Editor",
"bindings": {
"cmd-enter": "assistant::Assist",
"cmd-shift-enter": "assistant::Edit",
"cmd-s": "workspace::Save",
"cmd->": "assistant::QuoteSelection",
"cmd-<": "assistant::InsertIntoEditor",
@@ -350,6 +351,15 @@
"cmd-k cmd-l": "editor::ToggleFold",
"cmd-k cmd-[": "editor::FoldRecursive",
"cmd-k cmd-]": "editor::UnfoldRecursive",
"cmd-k cmd-1": ["editor::FoldAtLevel", { "level": 1 }],
"cmd-k cmd-2": ["editor::FoldAtLevel", { "level": 2 }],
"cmd-k cmd-3": ["editor::FoldAtLevel", { "level": 3 }],
"cmd-k cmd-4": ["editor::FoldAtLevel", { "level": 4 }],
"cmd-k cmd-5": ["editor::FoldAtLevel", { "level": 5 }],
"cmd-k cmd-6": ["editor::FoldAtLevel", { "level": 6 }],
"cmd-k cmd-7": ["editor::FoldAtLevel", { "level": 7 }],
"cmd-k cmd-8": ["editor::FoldAtLevel", { "level": 8 }],
"cmd-k cmd-9": ["editor::FoldAtLevel", { "level": 9 }],
"cmd-k cmd-0": "editor::FoldAll",
"cmd-k cmd-j": "editor::UnfoldAll",
"ctrl-space": "editor::ShowCompletions",
@@ -538,6 +548,13 @@
"ctrl-enter": "assistant::InlineAssist"
}
},
{
"context": "ProposedChangesEditor",
"bindings": {
"cmd-shift-y": "editor::ApplyDiffHunk",
"cmd-shift-a": "editor::ApplyAllDiffHunks"
}
},
{
"context": "PromptEditor",
"bindings": {

View File

@@ -88,7 +88,6 @@ origin: (f64, f64),
<edit>
<path>src/shapes/rectangle.rs</path>
<description>Update the Rectangle's new function to take an origin parameter</description>
<operation>update</operation>
<old_text>
fn new(width: f64, height: f64) -> Self {
@@ -117,7 +116,6 @@ pub struct Circle {
<edit>
<path>src/shapes/circle.rs</path>
<description>Update the Circle's new function to take an origin parameter</description>
<operation>update</operation>
<old_text>
fn new(radius: f64) -> Self {
@@ -134,7 +132,6 @@ fn new(origin: (f64, f64), radius: f64) -> Self {
<edit>
<path>src/shapes/rectangle.rs</path>
<description>Add an import for the std::fmt module</description>
<operation>insert_before</operation>
<old_text>
struct Rectangle {
@@ -147,7 +144,10 @@ use std::fmt;
<edit>
<path>src/shapes/rectangle.rs</path>
<description>Add a Display implementation for Rectangle</description>
<description>
Add a manual Display implementation for Rectangle.
Currently, this is the same as a derived Display implementation.
</description>
<operation>insert_after</operation>
<old_text>
Rectangle { width, height }
@@ -169,7 +169,6 @@ impl fmt::Display for Rectangle {
<edit>
<path>src/shapes/circle.rs</path>
<description>Add an import for the `std::fmt` module</description>
<operation>insert_before</operation>
<old_text>
struct Circle {
@@ -181,7 +180,6 @@ use std::fmt;
<edit>
<path>src/shapes/circle.rs</path>
<description>Add a Display implementation for Circle</description>
<operation>insert_after</operation>
<old_text>
Circle { radius }

View File

@@ -346,8 +346,6 @@
"git_status": true,
// Amount of indentation for nested items.
"indent_size": 20,
// Whether to show indent guides in the project panel.
"indent_guides": true,
// Whether to reveal it in the project panel automatically,
// when a corresponding project entry becomes active.
// Gitignored entries are never auto revealed.
@@ -371,6 +369,17 @@
/// 5. Never show the scrollbar:
/// "never"
"show": null
},
// Settings related to indent guides in the project panel.
"indent_guides": {
// When to show indent guides in the project panel.
// This setting can take two values:
//
// 1. Always show indent guides:
// "always"
// 2. Never show indent guides:
// "never"
"show": "always"
}
},
"outline_panel": {
@@ -394,7 +403,35 @@
"auto_reveal_entries": true,
/// Whether to fold directories automatically
/// when a directory has only one directory inside.
"auto_fold_dirs": true
"auto_fold_dirs": true,
// Settings related to indent guides in the outline panel.
"indent_guides": {
// When to show indent guides in the outline panel.
// This setting can take two values:
//
// 1. Always show indent guides:
// "always"
// 2. Never show indent guides:
// "never"
"show": "always"
},
/// Scrollbar-related settings
"scrollbar": {
/// When to show the scrollbar in the project panel.
/// This setting can take four values:
///
/// 1. null (default): Inherit editor settings
/// 2. Show the scrollbar if there's important information or
/// follow the system's configured behavior (default):
/// "auto"
/// 3. Match the system's configured behavior:
/// "system"
/// 4. Always show the scrollbar:
/// "always"
/// 5. Never show the scrollbar:
/// "never"
"show": null
}
},
"collaboration_panel": {
// Whether to show the collaboration panel button in the status bar.
@@ -615,6 +652,12 @@
// Sets a delay after which the inline blame information is shown.
// Delay is restarted with every cursor movement.
// "delay_ms": 600
//
// Whether or not do display the git commit summary on the same line.
// "show_commit_summary": false
//
// The minimum column number to show the inline blame information at
// "min_column": 0
}
},
// Configuration for how direnv configuration should be loaded. May take 2 values:
@@ -777,6 +820,7 @@
"tasks": {
"variables": {}
},
"toolchain": { "name": "default", "path": "default" },
// An object whose keys are language names, and whose values
// are arrays of filenames or extensions of files that should
// use those languages.

View File

@@ -16,6 +16,7 @@
"allow_concurrent_runs": false,
// What to do with the terminal pane and tab, after the command was started:
// * `always` — always show the terminal pane, add and focus the corresponding task's tab in it (default)
// * `no_focus` — always show the terminal pane, add/reuse the task's tab there, but don't focus it
// * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there
"reveal": "always",
// What to do with the terminal pane and tab, after the command had finished:

View File

@@ -23,6 +23,7 @@ language.workspace = true
project.workspace = true
smallvec.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
[dev-dependencies]

View File

@@ -13,7 +13,8 @@ use language::{
use project::{EnvironmentErrorMessage, LanguageServerProgress, Project, WorktreeId};
use smallvec::SmallVec;
use std::{cmp::Reverse, fmt::Write, sync::Arc, time::Duration};
use ui::{prelude::*, ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle};
use ui::{prelude::*, ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip};
use util::truncate_and_trailoff;
use workspace::{item::ItemHandle, StatusItemView, Workspace};
actions!(activity_indicator, [ShowErrorMessage]);
@@ -351,7 +352,10 @@ impl ActivityIndicator {
.into_any_element(),
),
message: format!("Formatting failed: {}. Click to see logs.", failure),
on_click: Some(Arc::new(|_, cx| {
on_click: Some(Arc::new(|indicator, cx| {
indicator.project.update(cx, |project, cx| {
project.reset_last_formatting_failure(cx);
});
cx.dispatch_action(Box::new(workspace::OpenLog));
})),
});
@@ -446,6 +450,8 @@ impl ActivityIndicator {
impl EventEmitter<Event> for ActivityIndicator {}
const MAX_MESSAGE_LEN: usize = 50;
impl Render for ActivityIndicator {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let result = h_flex()
@@ -456,6 +462,7 @@ impl Render for ActivityIndicator {
return result;
};
let this = cx.view().downgrade();
let truncate_content = content.message.len() > MAX_MESSAGE_LEN;
result.gap_2().child(
PopoverMenu::new("activity-indicator-popover")
.trigger(
@@ -464,7 +471,21 @@ impl Render for ActivityIndicator {
.id("activity-indicator-status")
.gap_2()
.children(content.icon)
.child(Label::new(content.message).size(LabelSize::Small))
.map(|button| {
if truncate_content {
button
.child(
Label::new(truncate_and_trailoff(
&content.message,
MAX_MESSAGE_LEN,
))
.size(LabelSize::Small),
)
.tooltip(move |cx| Tooltip::text(&content.message, cx))
} else {
button.child(Label::new(content.message).size(LabelSize::Small))
}
})
.when_some(content.on_click, |this, handler| {
this.on_click(cx.listener(move |this, _, cx| {
handler(this, cx);

View File

@@ -41,12 +41,10 @@ use prompts::PromptLoadingParams;
use semantic_index::{CloudEmbeddingProvider, SemanticDb};
use serde::{Deserialize, Serialize};
use settings::{update_settings_file, Settings, SettingsStore};
use slash_command::workflow_command::WorkflowSlashCommand;
use slash_command::{
auto_command, cargo_workspace_command, context_server_command, default_command, delta_command,
diagnostics_command, docs_command, fetch_command, file_command, now_command, project_command,
prompt_command, search_command, symbols_command, tab_command, terminal_command,
workflow_command,
};
use std::path::PathBuf;
use std::sync::Arc;
@@ -59,6 +57,7 @@ actions!(
assistant,
[
Assist,
Edit,
Split,
CopyCode,
CycleMessageRole,
@@ -298,25 +297,64 @@ fn register_context_server_handlers(cx: &mut AppContext) {
return;
};
if let Some(prompts) = protocol.list_prompts().await.log_err() {
for prompt in prompts
.into_iter()
.filter(context_server_command::acceptable_prompt)
{
log::info!(
"registering context server command: {:?}",
prompt.name
);
context_server_registry.register_command(
server.id.clone(),
prompt.name.as_str(),
);
slash_command_registry.register_command(
context_server_command::ContextServerSlashCommand::new(
&server, prompt,
),
true,
);
if protocol.capable(context_servers::protocol::ServerCapability::Prompts) {
if let Some(prompts) = protocol.list_prompts().await.log_err() {
for prompt in prompts
.into_iter()
.filter(context_server_command::acceptable_prompt)
{
log::info!(
"registering context server command: {:?}",
prompt.name
);
context_server_registry.register_command(
server.id.clone(),
prompt.name.as_str(),
);
slash_command_registry.register_command(
context_server_command::ContextServerSlashCommand::new(
&server, prompt,
),
true,
);
}
}
}
})
.detach();
}
},
);
cx.update_model(
&manager,
|manager: &mut context_servers::manager::ContextServerManager, cx| {
let tool_registry = ToolRegistry::global(cx);
let context_server_registry = ContextServerRegistry::global(cx);
if let Some(server) = manager.get_server(server_id) {
cx.spawn(|_, _| async move {
let Some(protocol) = server.client.read().clone() else {
return;
};
if protocol.capable(context_servers::protocol::ServerCapability::Tools) {
if let Some(tools) = protocol.list_tools().await.log_err() {
for tool in tools.tools {
log::info!(
"registering context server tool: {:?}",
tool.name
);
context_server_registry.register_tool(
server.id.clone(),
tool.name.as_str(),
);
tool_registry.register_tool(
tools::context_server_tool::ContextServerTool::new(
server.id.clone(),
tool
),
);
}
}
}
})
@@ -334,6 +372,14 @@ fn register_context_server_handlers(cx: &mut AppContext) {
context_server_registry.unregister_command(&server_id, &command_name);
}
}
if let Some(tools) = context_server_registry.get_tools(server_id) {
let tool_registry = ToolRegistry::global(cx);
for tool_name in tools {
tool_registry.unregister_tool_by_name(&tool_name);
context_server_registry.unregister_tool(&server_id, &tool_name);
}
}
}
},
)
@@ -397,22 +443,6 @@ fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut
slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
if let Some(prompt_builder) = prompt_builder {
cx.observe_global::<SettingsStore>({
let slash_command_registry = slash_command_registry.clone();
let prompt_builder = prompt_builder.clone();
move |cx| {
if AssistantSettings::get_global(cx).are_live_diffs_enabled(cx) {
slash_command_registry.register_command(
workflow_command::WorkflowSlashCommand::new(prompt_builder.clone()),
true,
);
} else {
slash_command_registry.unregister_command_by_name(WorkflowSlashCommand::NAME);
}
}
})
.detach();
cx.observe_flag::<project_command::ProjectSlashCommandFeatureFlag, _>({
let slash_command_registry = slash_command_registry.clone();
move |is_enabled, _cx| {

View File

@@ -13,10 +13,11 @@ use crate::{
terminal_inline_assistant::TerminalInlineAssistant,
Assist, AssistantPatch, AssistantPatchStatus, CacheStatus, ConfirmCommand, Content, Context,
ContextEvent, ContextId, ContextStore, ContextStoreEvent, CopyCode, CycleMessageRole,
DeployHistory, DeployPromptLibrary, InlineAssistant, InsertDraggedFiles, InsertIntoEditor,
Message, MessageId, MessageMetadata, MessageStatus, ModelPickerDelegate, ModelSelector,
NewContext, PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection,
RemoteContextMetadata, SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector,
DeployHistory, DeployPromptLibrary, Edit, InlineAssistant, InsertDraggedFiles,
InsertIntoEditor, Message, MessageId, MessageMetadata, MessageStatus, ModelPickerDelegate,
ModelSelector, NewContext, PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection,
RemoteContextMetadata, RequestType, SavedContextMetadata, Split, ToggleFocus,
ToggleModelSelector,
};
use anyhow::Result;
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
@@ -72,12 +73,11 @@ use std::{
};
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
use text::SelectionGoal;
use ui::TintColor;
use ui::{
prelude::*,
utils::{format_distance_from_now, DateTimeType},
Avatar, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding, ListItem,
ListItemSpacing, PopoverMenu, PopoverMenuHandle, Tooltip,
ListItemSpacing, PopoverMenu, PopoverMenuHandle, TintColor, Tooltip,
};
use util::{maybe, ResultExt};
use workspace::{
@@ -1446,8 +1446,8 @@ struct ScrollPosition {
}
struct PatchViewState {
footer_block_id: CustomBlockId,
crease_id: CreaseId,
block_id: CustomBlockId,
// crease_id: CreaseId,
editor: Option<PatchEditorState>,
update_task: Option<Task<()>>,
}
@@ -1461,6 +1461,7 @@ type MessageHeader = MessageMetadata;
#[derive(Clone)]
enum AssistError {
FileRequired,
PaymentRequired,
MaxMonthlySpendReached,
Message(SharedString),
@@ -1588,23 +1589,11 @@ impl ContextEditor {
}
fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
let provider = LanguageModelRegistry::read_global(cx).active_provider();
if provider
.as_ref()
.map_or(false, |provider| provider.must_accept_terms(cx))
{
self.show_accept_terms = true;
cx.notify();
return;
}
self.send_to_model(RequestType::Chat, cx);
}
if self.focus_active_patch(cx) {
return;
}
self.last_error = None;
self.send_to_model(cx);
cx.notify();
fn edit(&mut self, _: &Edit, cx: &mut ViewContext<Self>) {
self.send_to_model(RequestType::SuggestEdits, cx);
}
fn focus_active_patch(&mut self, cx: &mut ViewContext<Self>) -> bool {
@@ -1622,8 +1611,30 @@ impl ContextEditor {
false
}
fn send_to_model(&mut self, cx: &mut ViewContext<Self>) {
if let Some(user_message) = self.context.update(cx, |context, cx| context.assist(cx)) {
fn send_to_model(&mut self, request_type: RequestType, cx: &mut ViewContext<Self>) {
let provider = LanguageModelRegistry::read_global(cx).active_provider();
if provider
.as_ref()
.map_or(false, |provider| provider.must_accept_terms(cx))
{
self.show_accept_terms = true;
cx.notify();
return;
}
if self.focus_active_patch(cx) {
return;
}
self.last_error = None;
if request_type == RequestType::SuggestEdits && !self.context.read(cx).contains_files(cx) {
self.last_error = Some(AssistError::FileRequired);
cx.notify();
} else if let Some(user_message) = self
.context
.update(cx, |context, cx| context.assist(request_type, cx))
{
let new_selection = {
let cursor = user_message
.start
@@ -1640,6 +1651,8 @@ impl ContextEditor {
// Avoid scrolling to the new cursor position so the assistant's output is stable.
cx.defer(|this, _| this.scroll_position = None);
}
cx.notify();
}
fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
@@ -1667,8 +1680,10 @@ impl ContextEditor {
});
}
fn cursors(&self, cx: &AppContext) -> Vec<usize> {
let selections = self.editor.read(cx).selections.all::<usize>(cx);
fn cursors(&self, cx: &mut WindowContext) -> Vec<usize> {
let selections = self
.editor
.update(cx, |editor, cx| editor.selections.all::<usize>(cx));
selections
.into_iter()
.map(|selection| selection.head())
@@ -2158,8 +2173,8 @@ impl ContextEditor {
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);
removed_block_ids.insert(state.block_id);
// removed_crease_ids.push(state.crease_id);
}
}
@@ -2188,12 +2203,14 @@ impl ContextEditor {
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(
patch_range.clone(),
max_width,
gutter_width,
block_id,
selected,
cx,
)
})
@@ -2203,26 +2220,9 @@ 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())
})
},
constrain_width: false,
merge_adjacent: false,
};
let should_refold;
if let Some(state) = self.patches.get_mut(&range) {
replaced_blocks.insert(state.footer_block_id, render_block);
replaced_blocks.insert(state.block_id, render_block);
if let Some(editor_state) = &state.editor {
if editor_state.opened_patch != patch {
state.update_task = Some({
@@ -2242,30 +2242,30 @@ impl ContextEditor {
let block_ids = editor.insert_blocks(
[BlockProperties {
height: path_count as u32 + 1,
style: BlockStyle::Flex,
style: BlockStyle::Fixed,
render: render_block,
placement: BlockPlacement::Below(patch_start),
placement: BlockPlacement::Replace(patch_start..patch_end),
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 new_crease_ids = editor.insert_creases(
// [Crease::new(
// patch_start..patch_end,
// header_placeholder.clone(),
// fold_toggle("patch-header"),
// |_, _, _| Empty.into_any_element(),
// )],
// cx,
// );
self.patches.insert(
range.clone(),
PatchViewState {
footer_block_id: block_ids[0],
crease_id: new_crease_ids[0],
block_id: block_ids[0],
// crease_id: new_crease_ids[0],
editor: None,
update_task: None,
},
@@ -2275,8 +2275,8 @@ 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.unfold_ranges([patch_start..patch_end], true, false, cx);
// editor.fold_ranges([(patch_start..patch_end, header_placeholder)], false, cx);
}
}
@@ -2375,7 +2375,9 @@ impl ContextEditor {
}
fn update_active_patch(&mut self, cx: &mut ViewContext<Self>) {
let newest_cursor = self.editor.read(cx).selections.newest::<Point>(cx).head();
let newest_cursor = self.editor.update(cx, |editor, cx| {
editor.selections.newest::<Point>(cx).head()
});
let context = self.context.read(cx);
let new_patch = context.patch_containing(newest_cursor, cx).cloned();
@@ -2782,39 +2784,40 @@ impl ContextEditor {
) -> Option<(String, bool)> {
const CODE_FENCE_DELIMITER: &'static str = "```";
let context_editor = context_editor_view.read(cx).editor.read(cx);
let context_editor = context_editor_view.read(cx).editor.clone();
context_editor.update(cx, |context_editor, cx| {
if context_editor.selections.newest::<Point>(cx).is_empty() {
let snapshot = context_editor.buffer().read(cx).snapshot(cx);
let (_, _, snapshot) = snapshot.as_singleton()?;
if context_editor.selections.newest::<Point>(cx).is_empty() {
let snapshot = context_editor.buffer().read(cx).snapshot(cx);
let (_, _, snapshot) = snapshot.as_singleton()?;
let head = context_editor.selections.newest::<Point>(cx).head();
let offset = snapshot.point_to_offset(head);
let head = context_editor.selections.newest::<Point>(cx).head();
let offset = snapshot.point_to_offset(head);
let surrounding_code_block_range = find_surrounding_code_block(snapshot, offset)?;
let mut text = snapshot
.text_for_range(surrounding_code_block_range)
.collect::<String>();
let surrounding_code_block_range = find_surrounding_code_block(snapshot, offset)?;
let mut text = snapshot
.text_for_range(surrounding_code_block_range)
.collect::<String>();
// If there is no newline trailing the closing three-backticks, then
// tree-sitter-md extends the range of the content node to include
// the backticks.
if text.ends_with(CODE_FENCE_DELIMITER) {
text.drain((text.len() - CODE_FENCE_DELIMITER.len())..);
}
// If there is no newline trailing the closing three-backticks, then
// tree-sitter-md extends the range of the content node to include
// the backticks.
if text.ends_with(CODE_FENCE_DELIMITER) {
text.drain((text.len() - CODE_FENCE_DELIMITER.len())..);
(!text.is_empty()).then_some((text, true))
} else {
let anchor = context_editor.selections.newest_anchor();
let text = context_editor
.buffer()
.read(cx)
.read(cx)
.text_for_range(anchor.range())
.collect::<String>();
(!text.is_empty()).then_some((text, false))
}
(!text.is_empty()).then_some((text, true))
} else {
let anchor = context_editor.selections.newest_anchor();
let text = context_editor
.buffer()
.read(cx)
.read(cx)
.text_for_range(anchor.range())
.collect::<String>();
(!text.is_empty()).then_some((text, false))
}
})
}
fn insert_selection(
@@ -3431,33 +3434,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(
&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));
@@ -3468,10 +3451,6 @@ impl ContextEditor {
.anchor_in_excerpt(excerpt_id, range.start)
.unwrap();
if !snapshot.intersects_fold(anchor) {
return None;
}
let patch = self.context.read(cx).patch_for_range(&range, cx)?;
let paths = patch
.paths()
@@ -3480,10 +3459,18 @@ impl ContextEditor {
Some(
v_flex()
.bg(cx.theme().colors().editor_background)
.border_1()
.border_color(if selected {
cx.theme().colors().border_focused
} else {
cx.theme().colors().border
})
.id(id)
.pl(gutter_width)
.w(max_width)
.py_2()
.ml(gutter_width)
.p_2()
.rounded_md()
.min_h(cx.line_height() * 3.)
.cursor(CursorStyle::PointingHand)
.on_click(cx.listener(move |this, _, cx| {
this.editor.update(cx, |editor, cx| {
@@ -3493,6 +3480,7 @@ impl ContextEditor {
});
this.focus_active_patch(cx);
}))
.child(Label::new(patch.title.clone()))
.children(paths.into_iter().map(|path| {
h_flex()
.pl_1()
@@ -3644,7 +3632,13 @@ impl ContextEditor {
button.tooltip(move |_| tooltip.clone())
})
.layer(ElevationIndex::ModalSurface)
.child(Label::new("Send"))
.child(Label::new(
if AssistantSettings::get_global(cx).are_live_diffs_enabled(cx) {
"Chat"
} else {
"Send"
},
))
.children(
KeyBinding::for_action_in(&Assist, &focus_handle, cx)
.map(|binding| binding.into_any_element()),
@@ -3654,6 +3648,57 @@ impl ContextEditor {
})
}
fn render_edit_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle(cx).clone();
let (style, tooltip) = match token_state(&self.context, cx) {
Some(TokenState::NoTokensLeft { .. }) => (
ButtonStyle::Tinted(TintColor::Negative),
Some(Tooltip::text("Token limit reached", cx)),
),
Some(TokenState::HasMoreTokens {
over_warn_threshold,
..
}) => {
let (style, tooltip) = if over_warn_threshold {
(
ButtonStyle::Tinted(TintColor::Warning),
Some(Tooltip::text("Token limit is close to exhaustion", cx)),
)
} else {
(ButtonStyle::Filled, None)
};
(style, tooltip)
}
None => (ButtonStyle::Filled, None),
};
let provider = LanguageModelRegistry::read_global(cx).active_provider();
let has_configuration_error = configuration_error(cx).is_some();
let needs_to_accept_terms = self.show_accept_terms
&& provider
.as_ref()
.map_or(false, |provider| provider.must_accept_terms(cx));
let disabled = has_configuration_error || needs_to_accept_terms;
ButtonLike::new("edit_button")
.disabled(disabled)
.style(style)
.when_some(tooltip, |button, tooltip| {
button.tooltip(move |_| tooltip.clone())
})
.layer(ElevationIndex::ModalSurface)
.child(Label::new("Suggest Edits"))
.children(
KeyBinding::for_action_in(&Edit, &focus_handle, cx)
.map(|binding| binding.into_any_element()),
)
.on_click(move |_event, cx| {
focus_handle.dispatch_action(&Edit, cx);
})
}
fn render_last_error(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
let last_error = self.last_error.as_ref()?;
@@ -3668,6 +3713,7 @@ impl ContextEditor {
.elevation_2(cx)
.occlude()
.child(match last_error {
AssistError::FileRequired => self.render_file_required_error(cx),
AssistError::PaymentRequired => self.render_payment_required_error(cx),
AssistError::MaxMonthlySpendReached => {
self.render_max_monthly_spend_reached_error(cx)
@@ -3680,6 +3726,41 @@ impl ContextEditor {
)
}
fn render_file_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
v_flex()
.gap_0p5()
.child(
h_flex()
.gap_1p5()
.items_center()
.child(Icon::new(IconName::Warning).color(Color::Warning))
.child(
Label::new("Suggest Edits needs a file to edit").weight(FontWeight::MEDIUM),
),
)
.child(
div()
.id("error-message")
.max_h_24()
.overflow_y_scroll()
.child(Label::new(
"To include files, type /file or /tab in your prompt.",
)),
)
.child(
h_flex()
.justify_end()
.mt_1()
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, cx| {
this.last_error = None;
cx.notify();
},
))),
)
.into_any()
}
fn render_payment_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
@@ -3894,13 +3975,7 @@ impl Render for ContextEditor {
} else {
None
};
let focus_handle = self
.workspace
.update(cx, |workspace, cx| {
Some(workspace.active_item_as::<Editor>(cx)?.focus_handle(cx))
})
.ok()
.flatten();
v_flex()
.key_context("ContextEditor")
.capture_action(cx.listener(ContextEditor::cancel))
@@ -3910,6 +3985,7 @@ impl Render for ContextEditor {
.capture_action(cx.listener(ContextEditor::paste))
.capture_action(cx.listener(ContextEditor::cycle_message_role))
.capture_action(cx.listener(ContextEditor::confirm_command))
.on_action(cx.listener(ContextEditor::edit))
.on_action(cx.listener(ContextEditor::assist))
.on_action(cx.listener(ContextEditor::split))
.size_full()
@@ -3947,34 +4023,27 @@ impl Render for ContextEditor {
.child(
h_flex()
.gap_1()
.child(render_inject_context_menu(cx.view().downgrade(), cx))
.child(
IconButton::new("quote-button", IconName::Quote)
.icon_size(IconSize::Small)
.on_click(|_, cx| {
cx.dispatch_action(QuoteSelection.boxed_clone());
})
.tooltip(move |cx| {
cx.new_view(|cx| {
Tooltip::new("Insert Selection").key_binding(
focus_handle.as_ref().and_then(|handle| {
KeyBinding::for_action_in(
&QuoteSelection,
&handle,
cx,
)
}),
)
})
.into()
}),
),
.child(render_inject_context_menu(cx.view().downgrade(), cx)),
)
.child(
h_flex()
.w_full()
.justify_end()
.child(div().child(self.render_send_button(cx))),
.when(
AssistantSettings::get_global(cx).are_live_diffs_enabled(cx),
|buttons| {
buttons
.items_center()
.gap_1p5()
.child(self.render_edit_button(cx))
.child(
Label::new("or")
.size(LabelSize::Small)
.color(Color::Muted),
)
},
)
.child(self.render_send_button(cx)),
),
),
)
@@ -4249,6 +4318,7 @@ fn render_inject_context_menu(
Button::new("trigger", "Add Context")
.icon(IconName::Plus)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.tooltip(|cx| Tooltip::text("Type / to insert via keyboard", cx)),
)
@@ -4423,7 +4493,7 @@ impl Render for ContextEditorToolbarItem {
.w_full()
.justify_between()
.gap_2()
.child(Label::new("Insert Context"))
.child(Label::new("Add Context"))
.child(Label::new("/ command").color(Color::Muted))
.into_any()
},
@@ -4447,7 +4517,7 @@ impl Render for ContextEditorToolbarItem {
}
},
)
.action("Insert Selection", QuoteSelection.boxed_clone())
.action("Add Selection", QuoteSelection.boxed_clone())
}))
}
}),
@@ -4707,7 +4777,7 @@ impl Render for ConfigurationView {
let mut element = v_flex()
.id("assistant-configuration-view")
.track_focus(&self.focus_handle)
.track_focus(&self.focus_handle(cx))
.bg(cx.theme().colors().editor_background)
.size_full()
.overflow_y_scroll()

View File

@@ -2,8 +2,9 @@
mod context_tests;
use crate::{
prompts::PromptBuilder, slash_command::SlashCommandLine, AssistantEdit, AssistantPatch,
AssistantPatchStatus, MessageId, MessageStatus,
prompts::PromptBuilder,
slash_command::{file_command::FileCommandMetadata, SlashCommandLine},
AssistantEdit, AssistantPatch, AssistantPatchStatus, MessageId, MessageStatus,
};
use anyhow::{anyhow, Context as _, Result};
use assistant_slash_command::{
@@ -66,6 +67,14 @@ impl ContextId {
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RequestType {
/// Request a normal chat response from the model.
Chat,
/// Add a preamble to the message, which tells the model to return a structured response that suggests edits.
SuggestEdits,
}
#[derive(Clone, Debug)]
pub enum ContextOperation {
InsertMessage {
@@ -981,6 +990,20 @@ impl Context {
&self.slash_command_output_sections
}
pub fn contains_files(&self, cx: &AppContext) -> bool {
let buffer = self.buffer.read(cx);
self.slash_command_output_sections.iter().any(|section| {
section.is_valid(buffer)
&& section
.metadata
.as_ref()
.and_then(|metadata| {
serde_json::from_value::<FileCommandMetadata>(metadata.clone()).ok()
})
.is_some()
})
}
pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> {
self.pending_tool_uses_by_id.values().collect()
}
@@ -1028,7 +1051,7 @@ impl Context {
}
pub(crate) fn count_remaining_tokens(&mut self, cx: &mut ModelContext<Self>) {
let request = self.to_completion_request(cx);
let request = self.to_completion_request(RequestType::SuggestEdits, cx); // Conservatively assume SuggestEdits, since it takes more tokens.
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
return;
};
@@ -1171,7 +1194,7 @@ impl Context {
}
let request = {
let mut req = self.to_completion_request(cx);
let mut req = self.to_completion_request(RequestType::Chat, cx);
// Skip the last message because it's likely to change and
// therefore would be a waste to cache.
req.messages.pop();
@@ -1859,7 +1882,11 @@ impl Context {
})
}
pub fn assist(&mut self, cx: &mut ModelContext<Self>) -> Option<MessageAnchor> {
pub fn assist(
&mut self,
request_type: RequestType,
cx: &mut ModelContext<Self>,
) -> Option<MessageAnchor> {
let model_registry = LanguageModelRegistry::read_global(cx);
let provider = model_registry.active_provider()?;
let model = model_registry.active_model()?;
@@ -1872,7 +1899,7 @@ impl Context {
// Compute which messages to cache, including the last one.
self.mark_cache_anchors(&model.cache_configuration(), false, cx);
let mut request = self.to_completion_request(cx);
let mut request = self.to_completion_request(request_type, cx);
if cx.has_flag::<ToolUseFeatureFlag>() {
let tool_registry = ToolRegistry::global(cx);
@@ -2074,7 +2101,11 @@ impl Context {
Some(user_message)
}
pub fn to_completion_request(&self, cx: &AppContext) -> LanguageModelRequest {
pub fn to_completion_request(
&self,
request_type: RequestType,
cx: &AppContext,
) -> LanguageModelRequest {
let buffer = self.buffer.read(cx);
let mut contents = self.contents(cx).peekable();
@@ -2163,6 +2194,25 @@ impl Context {
completion_request.messages.push(request_message);
}
if let RequestType::SuggestEdits = request_type {
if let Ok(preamble) = self.prompt_builder.generate_workflow_prompt() {
let last_elem_index = completion_request.messages.len();
completion_request
.messages
.push(LanguageModelRequestMessage {
role: Role::User,
content: vec![MessageContent::Text(preamble)],
cache: false,
});
// The preamble message should be sent right before the last actual user message.
completion_request
.messages
.swap(last_elem_index, last_elem_index.saturating_sub(1));
}
}
completion_request
}
@@ -2477,7 +2527,7 @@ impl Context {
return;
}
let mut request = self.to_completion_request(cx);
let mut request = self.to_completion_request(RequestType::Chat, cx);
request.messages.push(LanguageModelRequestMessage {
role: Role::User,
content: vec![

View File

@@ -636,7 +636,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
kind: AssistantEditKind::InsertAfter {
old_text: "fn one".into(),
new_text: "fn two() {}".into(),
description: "add a `two` function".into(),
description: Some("add a `two` function".into()),
},
}]],
cx,
@@ -690,7 +690,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
kind: AssistantEditKind::InsertAfter {
old_text: "fn zero".into(),
new_text: "fn two() {}".into(),
description: "add a `two` function".into(),
description: Some("add a `two` function".into()),
},
}]],
cx,
@@ -754,7 +754,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
kind: AssistantEditKind::InsertAfter {
old_text: "fn zero".into(),
new_text: "fn two() {}".into(),
description: "add a `two` function".into(),
description: Some("add a `two` function".into()),
},
}]],
cx,
@@ -798,7 +798,7 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
kind: AssistantEditKind::InsertAfter {
old_text: "fn zero".into(),
new_text: "fn two() {}".into(),
description: "add a `two` function".into(),
description: Some("add a `two` function".into()),
},
}]],
cx,

View File

@@ -1,7 +1,7 @@
use crate::{
assistant_settings::AssistantSettings, humanize_token_count, prompts::PromptBuilder,
AssistantPanel, AssistantPanelEvent, CharOperation, CycleNextInlineAssist,
CyclePreviousInlineAssist, LineDiff, LineOperation, ModelSelector, StreamingDiff,
CyclePreviousInlineAssist, LineDiff, LineOperation, ModelSelector, RequestType, StreamingDiff,
};
use anyhow::{anyhow, Context as _, Result};
use client::{telemetry::Telemetry, ErrorExt};
@@ -54,7 +54,7 @@ use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
use terminal_view::terminal_panel::TerminalPanel;
use text::{OffsetRangeExt, ToPoint as _};
use theme::ThemeSettings;
use ui::{prelude::*, CheckboxWithLabel, IconButtonShape, Popover, Tooltip};
use ui::{prelude::*, text_for_action, CheckboxWithLabel, IconButtonShape, Popover, Tooltip};
use util::{RangeExt, ResultExt};
use workspace::{notifications::NotificationId, ItemHandle, Toast, Workspace};
@@ -189,11 +189,16 @@ impl InlineAssistant {
initial_prompt: Option<String>,
cx: &mut WindowContext,
) {
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
let (snapshot, initial_selections) = editor.update(cx, |editor, cx| {
(
editor.buffer().read(cx).snapshot(cx),
editor.selections.all::<Point>(cx),
)
});
let mut selections = Vec::<Selection<Point>>::new();
let mut newest_selection = None;
for mut selection in editor.read(cx).selections.all::<Point>(cx) {
for mut selection in initial_selections {
if selection.end > selection.start {
selection.start.column = 0;
// If the selection ends at the start of the line, we don't want to include it.
@@ -566,10 +571,13 @@ impl InlineAssistant {
return;
};
let editor = editor.read(cx);
if editor.selections.count() == 1 {
let selection = editor.selections.newest::<usize>(cx);
let buffer = editor.buffer().read(cx).snapshot(cx);
if editor.read(cx).selections.count() == 1 {
let (selection, buffer) = editor.update(cx, |editor, cx| {
(
editor.selections.newest::<usize>(cx),
editor.buffer().read(cx).snapshot(cx),
)
});
for assist_id in &editor_assists.assist_ids {
let assist = &self.assists[assist_id];
let assist_range = assist.range.to_offset(&buffer);
@@ -594,10 +602,13 @@ impl InlineAssistant {
return;
};
let editor = editor.read(cx);
if editor.selections.count() == 1 {
let selection = editor.selections.newest::<usize>(cx);
let buffer = editor.buffer().read(cx).snapshot(cx);
if editor.read(cx).selections.count() == 1 {
let (selection, buffer) = editor.update(cx, |editor, cx| {
(
editor.selections.newest::<usize>(cx),
editor.buffer().read(cx).snapshot(cx),
)
});
let mut closest_assist_fallback = None;
for assist_id in &editor_assists.assist_ids {
let assist = &self.assists[assist_id];
@@ -1596,7 +1607,7 @@ impl PromptEditor {
// always show the cursor (even when it isn't focused) because
// typing in one will make what you typed appear in all of them.
editor.set_show_cursor_when_unfocused(true, cx);
editor.set_placeholder_text("Add a prompt…", cx);
editor.set_placeholder_text(Self::placeholder_text(codegen.read(cx), cx), cx);
editor
});
@@ -1653,6 +1664,7 @@ impl PromptEditor {
self.editor = cx.new_view(|cx| {
let mut editor = Editor::auto_height(Self::MAX_LINES as usize, cx);
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
editor.set_placeholder_text(Self::placeholder_text(self.codegen.read(cx), cx), cx);
editor.set_placeholder_text("Add a prompt…", cx);
editor.set_text(prompt, cx);
if focus {
@@ -1663,6 +1675,20 @@ impl PromptEditor {
self.subscribe_to_editor(cx);
}
fn placeholder_text(codegen: &Codegen, cx: &WindowContext) -> String {
let context_keybinding = text_for_action(&crate::ToggleFocus, cx)
.map(|keybinding| format!("{keybinding} for context"))
.unwrap_or_default();
let action = if codegen.is_insertion {
"Generate"
} else {
"Transform"
};
format!("{action}{context_keybinding} • ↓↑ for history")
}
fn prompt(&self, cx: &AppContext) -> String {
self.editor.read(cx).text(cx)
}
@@ -2219,7 +2245,7 @@ impl InlineAssist {
.read(cx)
.active_context(cx)?
.read(cx)
.to_completion_request(cx),
.to_completion_request(RequestType::Chat, cx),
)
} else {
None
@@ -2260,6 +2286,7 @@ pub struct Codegen {
initial_transaction_id: Option<TransactionId>,
telemetry: Option<Arc<Telemetry>>,
builder: Arc<PromptBuilder>,
is_insertion: bool,
}
impl Codegen {
@@ -2282,6 +2309,7 @@ impl Codegen {
)
});
let mut this = Self {
is_insertion: range.to_offset(&buffer.read(cx).snapshot(cx)).is_empty(),
alternatives: vec![codegen],
active_alternative: 0,
seen_alternatives: HashSet::default(),
@@ -2683,7 +2711,7 @@ impl CodegenAlternative {
let prompt = self
.builder
.generate_content_prompt(user_prompt, language_name, buffer, range)
.generate_inline_transformation_prompt(user_prompt, language_name, buffer, range)
.map_err(|e| anyhow::anyhow!("Failed to generate content prompt: {}", e))?;
let mut messages = Vec::new();

View File

@@ -33,21 +33,21 @@ pub enum AssistantEditKind {
Update {
old_text: String,
new_text: String,
description: String,
description: Option<String>,
},
Create {
new_text: String,
description: String,
description: Option<String>,
},
InsertBefore {
old_text: String,
new_text: String,
description: String,
description: Option<String>,
},
InsertAfter {
old_text: String,
new_text: String,
description: String,
description: Option<String>,
},
Delete {
old_text: String,
@@ -86,19 +86,37 @@ enum SearchDirection {
Diagonal,
}
// A measure of the currently quality of an in-progress fuzzy search.
//
// Uses 60 bits to store a numeric cost, and 4 bits to store the preceding
// operation in the search.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct SearchState {
score: u32,
cost: u32,
direction: SearchDirection,
}
impl SearchState {
fn new(score: u32, direction: SearchDirection) -> Self {
Self { score, direction }
fn new(cost: u32, direction: SearchDirection) -> Self {
Self { cost, direction }
}
}
struct SearchMatrix {
cols: usize,
data: Vec<SearchState>,
}
impl SearchMatrix {
fn new(rows: usize, cols: usize) -> Self {
SearchMatrix {
cols,
data: vec![SearchState::new(0, SearchDirection::Diagonal); rows * cols],
}
}
fn get(&self, row: usize, col: usize) -> SearchState {
self.data[row * self.cols + col]
}
fn set(&mut self, row: usize, col: usize, cost: SearchState) {
self.data[row * self.cols + col] = cost;
}
}
@@ -187,23 +205,23 @@ impl AssistantEdit {
"update" => AssistantEditKind::Update {
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
description: description.ok_or_else(|| anyhow!("missing description"))?,
description,
},
"insert_before" => AssistantEditKind::InsertBefore {
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
description: description.ok_or_else(|| anyhow!("missing description"))?,
description,
},
"insert_after" => AssistantEditKind::InsertAfter {
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
description: description.ok_or_else(|| anyhow!("missing description"))?,
description,
},
"delete" => AssistantEditKind::Delete {
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
},
"create" => AssistantEditKind::Create {
description: description.ok_or_else(|| anyhow!("missing description"))?,
description,
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
},
_ => Err(anyhow!("unknown operation {operation:?}"))?,
@@ -264,7 +282,7 @@ impl AssistantEditKind {
ResolvedEdit {
range,
new_text,
description: Some(description),
description,
}
}
Self::Create {
@@ -272,7 +290,7 @@ impl AssistantEditKind {
description,
} => ResolvedEdit {
range: text::Anchor::MIN..text::Anchor::MAX,
description: Some(description),
description,
new_text,
},
Self::InsertBefore {
@@ -285,7 +303,7 @@ impl AssistantEditKind {
ResolvedEdit {
range: range.start..range.start,
new_text,
description: Some(description),
description,
}
}
Self::InsertAfter {
@@ -298,7 +316,7 @@ impl AssistantEditKind {
ResolvedEdit {
range: range.end..range.end,
new_text,
description: Some(description),
description,
}
}
Self::Delete { old_text } => {
@@ -314,44 +332,29 @@ impl AssistantEditKind {
fn resolve_location(buffer: &text::BufferSnapshot, search_query: &str) -> Range<text::Anchor> {
const INSERTION_COST: u32 = 3;
const DELETION_COST: u32 = 10;
const WHITESPACE_INSERTION_COST: u32 = 1;
const DELETION_COST: u32 = 3;
const WHITESPACE_DELETION_COST: u32 = 1;
const EQUALITY_BONUS: u32 = 5;
struct Matrix {
cols: usize,
data: Vec<SearchState>,
}
impl Matrix {
fn new(rows: usize, cols: usize) -> Self {
Matrix {
cols,
data: vec![SearchState::new(0, SearchDirection::Diagonal); rows * cols],
}
}
fn get(&self, row: usize, col: usize) -> SearchState {
self.data[row * self.cols + col]
}
fn set(&mut self, row: usize, col: usize, cost: SearchState) {
self.data[row * self.cols + col] = cost;
}
}
let buffer_len = buffer.len();
let query_len = search_query.len();
let mut matrix = Matrix::new(query_len + 1, buffer_len + 1);
let mut matrix = SearchMatrix::new(query_len + 1, buffer_len + 1);
let mut leading_deletion_cost = 0_u32;
for (row, query_byte) in search_query.bytes().enumerate() {
let deletion_cost = if query_byte.is_ascii_whitespace() {
WHITESPACE_DELETION_COST
} else {
DELETION_COST
};
leading_deletion_cost = leading_deletion_cost.saturating_add(deletion_cost);
matrix.set(
row + 1,
0,
SearchState::new(leading_deletion_cost, SearchDirection::Diagonal),
);
for (col, buffer_byte) in buffer.bytes_in_range(0..buffer.len()).flatten().enumerate() {
let deletion_cost = if query_byte.is_ascii_whitespace() {
WHITESPACE_DELETION_COST
} else {
DELETION_COST
};
let insertion_cost = if buffer_byte.is_ascii_whitespace() {
WHITESPACE_INSERTION_COST
} else {
@@ -359,38 +362,35 @@ impl AssistantEditKind {
};
let up = SearchState::new(
matrix.get(row, col + 1).score.saturating_sub(deletion_cost),
matrix.get(row, col + 1).cost.saturating_add(deletion_cost),
SearchDirection::Up,
);
let left = SearchState::new(
matrix
.get(row + 1, col)
.score
.saturating_sub(insertion_cost),
matrix.get(row + 1, col).cost.saturating_add(insertion_cost),
SearchDirection::Left,
);
let diagonal = SearchState::new(
if query_byte == *buffer_byte {
matrix.get(row, col).score.saturating_add(EQUALITY_BONUS)
matrix.get(row, col).cost
} else {
matrix
.get(row, col)
.score
.saturating_sub(deletion_cost + insertion_cost)
.cost
.saturating_add(deletion_cost + insertion_cost)
},
SearchDirection::Diagonal,
);
matrix.set(row + 1, col + 1, up.max(left).max(diagonal));
matrix.set(row + 1, col + 1, up.min(left).min(diagonal));
}
}
// Traceback to find the best match
let mut best_buffer_end = buffer_len;
let mut best_score = 0;
let mut best_cost = u32::MAX;
for col in 1..=buffer_len {
let score = matrix.get(query_len, col).score;
if score > best_score {
best_score = score;
let cost = matrix.get(query_len, col).cost;
if cost < best_cost {
best_cost = cost;
best_buffer_end = col;
}
}
@@ -560,89 +560,84 @@ mod tests {
language_settings::AllLanguageSettings, Language, LanguageConfig, LanguageMatcher,
};
use settings::SettingsStore;
use text::{OffsetRangeExt, Point};
use ui::BorrowAppContext;
use unindent::Unindent as _;
use util::test::{generate_marked_text, marked_text_ranges};
#[gpui::test]
fn test_resolve_location(cx: &mut AppContext) {
{
let buffer = cx.new_model(|cx| {
Buffer::local(
concat!(
" Lorem\n",
" ipsum\n",
" dolor sit amet\n",
" consecteur",
),
cx,
)
});
let snapshot = buffer.read(cx).snapshot();
assert_eq!(
AssistantEditKind::resolve_location(&snapshot, "ipsum\ndolor").to_point(&snapshot),
Point::new(1, 0)..Point::new(2, 18)
);
}
assert_location_resolution(
concat!(
" Lorem\n",
"« ipsum\n",
" dolor sit amet»\n",
" consecteur",
),
"ipsum\ndolor",
cx,
);
{
let buffer = cx.new_model(|cx| {
Buffer::local(
concat!(
"fn foo1(a: usize) -> usize {\n",
" 40\n",
"}\n",
"\n",
"fn foo2(b: usize) -> usize {\n",
" 42\n",
"}\n",
),
cx,
)
});
let snapshot = buffer.read(cx).snapshot();
assert_eq!(
AssistantEditKind::resolve_location(&snapshot, "fn foo1(b: usize) {\n40\n}")
.to_point(&snapshot),
Point::new(0, 0)..Point::new(2, 1)
);
}
assert_location_resolution(
&"
«fn foo1(a: usize) -> usize {
40
{
let buffer = cx.new_model(|cx| {
Buffer::local(
concat!(
"fn main() {\n",
" Foo\n",
" .bar()\n",
" .baz()\n",
" .qux()\n",
"}\n",
"\n",
"fn foo2(b: usize) -> usize {\n",
" 42\n",
"}\n",
),
cx,
)
});
let snapshot = buffer.read(cx).snapshot();
assert_eq!(
AssistantEditKind::resolve_location(&snapshot, "Foo.bar.baz.qux()")
.to_point(&snapshot),
Point::new(1, 0)..Point::new(4, 14)
);
}
fn foo2(b: usize) -> usize {
42
}
"
.unindent(),
"fn foo1(b: usize) {\n40\n}",
cx,
);
assert_location_resolution(
&"
fn main() {
« Foo
.bar()
.baz()
.qux()»
}
fn foo2(b: usize) -> usize {
42
}
"
.unindent(),
"Foo.bar.baz.qux()",
cx,
);
assert_location_resolution(
&"
class Something {
one() { return 1; }
« two() { return 2222; }
three() { return 333; }
four() { return 4444; }
five() { return 5555; }
six() { return 6666; }
» seven() { return 7; }
eight() { return 8; }
}
"
.unindent(),
&"
two() { return 2222; }
four() { return 4444; }
five() { return 5555; }
six() { return 6666; }
"
.unindent(),
cx,
);
}
#[gpui::test]
fn test_resolve_edits(cx: &mut AppContext) {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
cx.update_global::<SettingsStore, _>(|settings, cx| {
settings.update_user_settings::<AllLanguageSettings>(cx, |_| {});
});
init_test(cx);
assert_edits(
"
@@ -675,7 +670,7 @@ mod tests {
last_name: String,
"
.unindent(),
description: "".into(),
description: None,
},
AssistantEditKind::Update {
old_text: "
@@ -690,7 +685,7 @@ mod tests {
}
"
.unindent(),
description: "".into(),
description: None,
},
],
"
@@ -734,7 +729,7 @@ mod tests {
qux();
}"
.unindent(),
description: "implement bar".into(),
description: Some("implement bar".into()),
},
AssistantEditKind::Update {
old_text: "
@@ -747,7 +742,7 @@ mod tests {
bar();
}"
.unindent(),
description: "call bar in foo".into(),
description: Some("call bar in foo".into()),
},
AssistantEditKind::InsertAfter {
old_text: "
@@ -762,7 +757,7 @@ mod tests {
}
"
.unindent(),
description: "implement qux".into(),
description: Some("implement qux".into()),
},
],
"
@@ -814,7 +809,7 @@ mod tests {
}
"
.unindent(),
description: "pick better number".into(),
description: None,
},
AssistantEditKind::Update {
old_text: "
@@ -829,7 +824,7 @@ mod tests {
}
"
.unindent(),
description: "pick better number".into(),
description: None,
},
AssistantEditKind::Update {
old_text: "
@@ -844,7 +839,7 @@ mod tests {
}
"
.unindent(),
description: "pick better number".into(),
description: None,
},
],
"
@@ -865,6 +860,69 @@ mod tests {
.unindent(),
cx,
);
assert_edits(
"
impl Person {
fn set_name(&mut self, name: String) {
self.name = name;
}
fn name(&self) -> String {
return self.name;
}
}
"
.unindent(),
vec![
AssistantEditKind::Update {
old_text: "self.name = name;".unindent(),
new_text: "self._name = name;".unindent(),
description: None,
},
AssistantEditKind::Update {
old_text: "return self.name;\n".unindent(),
new_text: "return self._name;\n".unindent(),
description: None,
},
],
"
impl Person {
fn set_name(&mut self, name: String) {
self._name = name;
}
fn name(&self) -> String {
return self._name;
}
}
"
.unindent(),
cx,
);
}
fn init_test(cx: &mut AppContext) {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
cx.update_global::<SettingsStore, _>(|settings, cx| {
settings.update_user_settings::<AllLanguageSettings>(cx, |_| {});
});
}
#[track_caller]
fn assert_location_resolution(
text_with_expected_range: &str,
query: &str,
cx: &mut AppContext,
) {
let (text, _) = marked_text_ranges(text_with_expected_range, false);
let buffer = cx.new_model(|cx| Buffer::local(text.clone(), cx));
let snapshot = buffer.read(cx).snapshot();
let range = AssistantEditKind::resolve_location(&snapshot, query).to_offset(&snapshot);
let text_with_actual_range = generate_marked_text(&text, &[range], false);
pretty_assertions::assert_eq!(text_with_actual_range, text_with_expected_range);
}
#[track_caller]

View File

@@ -204,7 +204,7 @@ impl PromptBuilder {
Ok(())
}
pub fn generate_content_prompt(
pub fn generate_inline_transformation_prompt(
&self,
user_prompt: String,
language_name: Option<&LanguageName>,
@@ -311,7 +311,7 @@ impl PromptBuilder {
}
pub fn generate_workflow_prompt(&self) -> Result<String, RenderError> {
self.handlebars.lock().render("edit_workflow", &())
self.handlebars.lock().render("suggest_edits", &())
}
pub fn generate_project_slash_command_prompt(

View File

@@ -34,7 +34,6 @@ pub mod search_command;
pub mod symbols_command;
pub mod tab_command;
pub mod terminal_command;
pub mod workflow_command;
pub(crate) struct SlashCommandCompletionProvider {
cancel_flag: Mutex<Arc<AtomicBool>>,

View File

@@ -14,7 +14,7 @@ use language_model::{
use semantic_index::{FileSummary, SemanticDb};
use smol::channel;
use std::sync::{atomic::AtomicBool, Arc};
use ui::{BorrowAppContext, WindowContext};
use ui::{prelude::*, BorrowAppContext, WindowContext};
use util::ResultExt;
use workspace::Workspace;
@@ -37,6 +37,10 @@ impl SlashCommand for AutoCommand {
"Automatically infer what context to add".into()
}
fn icon(&self) -> IconName {
IconName::Wand
}
fn menu_text(&self) -> String {
self.description()
}

View File

@@ -10,6 +10,7 @@ use gpui::{Task, WeakView, WindowContext};
use language::{BufferSnapshot, LspAdapterDelegate};
use std::sync::{atomic::AtomicBool, Arc};
use text::OffsetRangeExt;
use ui::prelude::*;
use workspace::Workspace;
pub(crate) struct DeltaSlashCommand;
@@ -27,6 +28,10 @@ impl SlashCommand for DeltaSlashCommand {
self.description()
}
fn icon(&self) -> IconName {
IconName::Diff
}
fn requires_argument(&self) -> bool {
false
}

View File

@@ -98,6 +98,10 @@ impl SlashCommand for DiagnosticsSlashCommand {
"Insert diagnostics".into()
}
fn icon(&self) -> IconName {
IconName::XCircle
}
fn menu_text(&self) -> String {
self.description()
}

View File

@@ -117,7 +117,7 @@ impl SlashCommand for FileSlashCommand {
}
fn description(&self) -> String {
"Insert file".into()
"Insert file and/or directory".into()
}
fn menu_text(&self) -> String {
@@ -128,6 +128,10 @@ impl SlashCommand for FileSlashCommand {
true
}
fn icon(&self) -> IconName {
IconName::File
}
fn complete_argument(
self: Arc<Self>,
arguments: &[String],

View File

@@ -24,7 +24,8 @@ use std::{
ops::DerefMut,
sync::{atomic::AtomicBool, Arc},
};
use ui::{BorrowAppContext as _, IconName};
use ui::prelude::*;
use workspace::Workspace;
pub struct ProjectSlashCommand {
@@ -50,6 +51,10 @@ impl SlashCommand for ProjectSlashCommand {
"Generate a semantic search based on context".into()
}
fn icon(&self) -> IconName {
IconName::Folder
}
fn menu_text(&self) -> String {
self.description()
}

View File

@@ -21,6 +21,10 @@ impl SlashCommand for PromptSlashCommand {
"Insert prompt from library".into()
}
fn icon(&self) -> IconName {
IconName::Library
}
fn menu_text(&self) -> String {
self.description()
}

View File

@@ -38,6 +38,10 @@ impl SlashCommand for SearchSlashCommand {
"Search your project semantically".into()
}
fn icon(&self) -> IconName {
IconName::SearchCode
}
fn menu_text(&self) -> String {
self.description()
}

View File

@@ -22,6 +22,10 @@ impl SlashCommand for OutlineSlashCommand {
"Insert symbols for active tab".into()
}
fn icon(&self) -> IconName {
IconName::ListTree
}
fn menu_text(&self) -> String {
self.description()
}

View File

@@ -12,7 +12,7 @@ use std::{
path::PathBuf,
sync::{atomic::AtomicBool, Arc},
};
use ui::{ActiveTheme, WindowContext};
use ui::{prelude::*, ActiveTheme, WindowContext};
use util::ResultExt;
use workspace::Workspace;
@@ -31,6 +31,10 @@ impl SlashCommand for TabSlashCommand {
"Insert open tabs (active tab by default)".to_owned()
}
fn icon(&self) -> IconName {
IconName::FileTree
}
fn menu_text(&self) -> String {
self.description()
}

View File

@@ -33,6 +33,10 @@ impl SlashCommand for TerminalSlashCommand {
"Insert terminal output".into()
}
fn icon(&self) -> IconName {
IconName::Terminal
}
fn menu_text(&self) -> String {
self.description()
}

View File

@@ -1,82 +0,0 @@
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use anyhow::Result;
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
SlashCommandResult,
};
use gpui::{Task, WeakView};
use language::{BufferSnapshot, LspAdapterDelegate};
use ui::prelude::*;
use workspace::Workspace;
use crate::prompts::PromptBuilder;
pub(crate) struct WorkflowSlashCommand {
prompt_builder: Arc<PromptBuilder>,
}
impl WorkflowSlashCommand {
pub const NAME: &'static str = "workflow";
pub fn new(prompt_builder: Arc<PromptBuilder>) -> Self {
Self { prompt_builder }
}
}
impl SlashCommand for WorkflowSlashCommand {
fn name(&self) -> String {
Self::NAME.into()
}
fn description(&self) -> String {
"Insert prompt to opt into the edit workflow".into()
}
fn menu_text(&self) -> String {
self.description()
}
fn requires_argument(&self) -> bool {
false
}
fn complete_argument(
self: Arc<Self>,
_arguments: &[String],
_cancel: Arc<AtomicBool>,
_workspace: Option<WeakView<Workspace>>,
_cx: &mut WindowContext,
) -> Task<Result<Vec<ArgumentCompletion>>> {
Task::ready(Ok(Vec::new()))
}
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 prompt_builder = self.prompt_builder.clone();
cx.spawn(|_cx| async move {
let text = prompt_builder.generate_workflow_prompt()?;
let range = 0..text.len();
Ok(SlashCommandOutput {
text,
sections: vec![SlashCommandOutputSection {
range,
icon: IconName::Route,
label: "Workflow".into(),
metadata: None,
}],
run_commands_in_text: false,
}
.to_event_stream())
})
}
}

View File

@@ -1,19 +1,13 @@
use std::sync::Arc;
use assistant_slash_command::SlashCommandRegistry;
use gpui::AnyElement;
use gpui::DismissEvent;
use gpui::WeakView;
use picker::PickerEditorPosition;
use ui::ListItemSpacing;
use gpui::SharedString;
use gpui::Task;
use picker::{Picker, PickerDelegate};
use ui::{prelude::*, ListItem, PopoverMenu, PopoverTrigger};
use gpui::{AnyElement, DismissEvent, SharedString, Task, WeakView};
use picker::{Picker, PickerDelegate, PickerEditorPosition};
use ui::{prelude::*, KeyBinding, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger};
use crate::assistant_panel::ContextEditor;
use crate::QuoteSelection;
#[derive(IntoElement)]
pub(super) struct SlashCommandSelector<T: PopoverTrigger> {
@@ -27,6 +21,7 @@ struct SlashCommandInfo {
name: SharedString,
description: SharedString,
args: Option<SharedString>,
icon: IconName,
}
#[derive(Clone)]
@@ -37,6 +32,7 @@ enum SlashCommandEntry {
renderer: fn(&mut WindowContext<'_>) -> AnyElement,
on_confirm: fn(&mut WindowContext<'_>),
},
QuoteButton,
}
impl AsRef<str> for SlashCommandEntry {
@@ -44,6 +40,7 @@ impl AsRef<str> for SlashCommandEntry {
match self {
SlashCommandEntry::Info(SlashCommandInfo { name, .. })
| SlashCommandEntry::Advert { name, .. } => name,
SlashCommandEntry::QuoteButton => "Quote Selection",
}
}
}
@@ -145,16 +142,23 @@ impl PickerDelegate for SlashCommandDelegate {
}
ret
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
if let Some(command) = self.filtered_commands.get(self.selected_index) {
if let SlashCommandEntry::Info(info) = command {
self.active_context_editor
.update(cx, |context_editor, cx| {
context_editor.insert_command(&info.name, cx)
})
.ok();
} else if let SlashCommandEntry::Advert { on_confirm, .. } = command {
on_confirm(cx);
match command {
SlashCommandEntry::Info(info) => {
self.active_context_editor
.update(cx, |context_editor, cx| {
context_editor.insert_command(&info.name, cx)
})
.ok();
}
SlashCommandEntry::QuoteButton => {
cx.dispatch_action(Box::new(QuoteSelection));
}
SlashCommandEntry::Advert { on_confirm, .. } => {
on_confirm(cx);
}
}
cx.emit(DismissEvent);
}
@@ -181,46 +185,78 @@ impl PickerDelegate for SlashCommandDelegate {
.spacing(ListItemSpacing::Dense)
.selected(selected)
.child(
h_flex()
v_flex()
.group(format!("command-entry-label-{ix}"))
.w_full()
.min_w(px(250.))
.child(
v_flex()
.child(
h_flex()
.child(div().font_buffer(cx).child({
let mut label = format!("/{}", info.name);
if let Some(args) =
info.args.as_ref().filter(|_| selected)
{
label.push_str(&args);
}
Label::new(label).size(LabelSize::Small)
}))
.children(info.args.clone().filter(|_| !selected).map(
|args| {
div()
.font_buffer(cx)
.child(
Label::new(args)
.size(LabelSize::Small)
.color(Color::Muted),
)
.visible_on_hover(format!(
"command-entry-label-{ix}"
))
},
)),
)
.child(
Label::new(info.description.clone())
.size(LabelSize::Small)
.color(Color::Muted),
),
h_flex()
.gap_1p5()
.child(Icon::new(info.icon).size(IconSize::XSmall))
.child(div().font_buffer(cx).child({
let mut label = format!("{}", info.name);
if let Some(args) = info.args.as_ref().filter(|_| selected)
{
label.push_str(&args);
}
Label::new(label).size(LabelSize::Small)
}))
.children(info.args.clone().filter(|_| !selected).map(
|args| {
div()
.font_buffer(cx)
.child(
Label::new(args)
.size(LabelSize::Small)
.color(Color::Muted),
)
.visible_on_hover(format!(
"command-entry-label-{ix}"
))
},
)),
)
.child(
Label::new(info.description.clone())
.size(LabelSize::Small)
.color(Color::Muted),
),
),
),
SlashCommandEntry::QuoteButton => {
let focus = cx.focus_handle();
let key_binding = KeyBinding::for_action_in(&QuoteSelection, &focus, cx);
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Dense)
.selected(selected)
.child(
v_flex()
.child(
h_flex()
.gap_1p5()
.child(Icon::new(IconName::Quote).size(IconSize::XSmall))
.child(
div().font_buffer(cx).child(
Label::new("selection").size(LabelSize::Small),
),
),
)
.child(
h_flex()
.gap_1p5()
.child(
Label::new("Insert editor selection")
.color(Color::Muted)
.size(LabelSize::Small),
)
.children(key_binding.map(|kb| kb.render(cx))),
),
),
)
}
SlashCommandEntry::Advert { renderer, .. } => Some(
ListItem::new(ix)
.inset(true)
@@ -251,31 +287,50 @@ impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
name: command_name.into(),
description: menu_text,
args,
icon: command.icon(),
}))
})
.chain([SlashCommandEntry::Advert {
name: "create-your-command".into(),
renderer: |cx| {
v_flex()
.child(
h_flex()
.font_buffer(cx)
.items_center()
.gap_1()
.child(div().font_buffer(cx).child(
Label::new("create-your-command").size(LabelSize::Small),
))
.child(Icon::new(IconName::ArrowUpRight).size(IconSize::XSmall)),
)
.child(
Label::new("Learn how to create a custom command")
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element()
.chain([
SlashCommandEntry::Advert {
name: "create-your-command".into(),
renderer: |cx| {
v_flex()
.w_full()
.child(
h_flex()
.w_full()
.font_buffer(cx)
.items_center()
.justify_between()
.child(
h_flex()
.items_center()
.gap_1p5()
.child(Icon::new(IconName::Plus).size(IconSize::XSmall))
.child(
div().font_buffer(cx).child(
Label::new("create-your-command")
.size(LabelSize::Small),
),
),
)
.child(
Icon::new(IconName::ArrowUpRight)
.size(IconSize::XSmall)
.color(Color::Muted),
),
)
.child(
Label::new("Create your custom command")
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element()
},
on_confirm: |cx| cx.open_url("https://zed.dev/docs/extensions/slash-commands"),
},
on_confirm: |cx| cx.open_url("https://zed.dev/docs/extensions/slash-commands"),
}])
SlashCommandEntry::QuoteButton,
])
.collect::<Vec<_>>();
let delegate = SlashCommandDelegate {

View File

@@ -1,6 +1,6 @@
use crate::{
humanize_token_count, prompts::PromptBuilder, AssistantPanel, AssistantPanelEvent,
ModelSelector, DEFAULT_CONTEXT_LINES,
ModelSelector, RequestType, DEFAULT_CONTEXT_LINES,
};
use anyhow::{Context as _, Result};
use client::telemetry::Telemetry;
@@ -251,7 +251,7 @@ impl TerminalInlineAssistant {
.read(cx)
.active_context(cx)?
.read(cx)
.to_completion_request(cx),
.to_completion_request(RequestType::Chat, cx),
)
})
} else {

View File

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

View File

@@ -0,0 +1,82 @@
use anyhow::{anyhow, bail};
use assistant_tool::Tool;
use context_servers::manager::ContextServerManager;
use context_servers::types;
use gpui::Task;
pub struct ContextServerTool {
server_id: String,
tool: types::Tool,
}
impl ContextServerTool {
pub fn new(server_id: impl Into<String>, tool: types::Tool) -> Self {
Self {
server_id: server_id.into(),
tool,
}
}
}
impl Tool for ContextServerTool {
fn name(&self) -> String {
self.tool.name.clone()
}
fn description(&self) -> String {
self.tool.description.clone().unwrap_or_default()
}
fn input_schema(&self) -> serde_json::Value {
match &self.tool.input_schema {
serde_json::Value::Null => {
serde_json::json!({ "type": "object", "properties": [] })
}
serde_json::Value::Object(map) if map.is_empty() => {
serde_json::json!({ "type": "object", "properties": [] })
}
_ => self.tool.input_schema.clone(),
}
}
fn run(
self: std::sync::Arc<Self>,
input: serde_json::Value,
_workspace: gpui::WeakView<workspace::Workspace>,
cx: &mut ui::WindowContext,
) -> gpui::Task<gpui::Result<String>> {
let manager = ContextServerManager::global(cx);
let manager = manager.read(cx);
if let Some(server) = manager.get_server(&self.server_id) {
cx.foreground_executor().spawn({
let tool_name = self.tool.name.clone();
async move {
let Some(protocol) = server.client.read().clone() else {
bail!("Context server not initialized");
};
let arguments = if let serde_json::Value::Object(map) = input {
Some(map.into_iter().collect())
} else {
None
};
log::trace!(
"Running tool: {} with arguments: {:?}",
tool_name,
arguments
);
let response = protocol.run_tool(tool_name, arguments).await?;
let tool_result = match response.tool_result {
serde_json::Value::String(s) => s,
_ => serde_json::to_string(&response.tool_result)?,
};
Ok(tool_result)
}
})
} else {
Task::ready(Err(anyhow!("Context server not found")))
}
}
}

View File

@@ -62,6 +62,9 @@ pub type SlashCommandResult = Result<BoxStream<'static, Result<SlashCommandEvent
pub trait SlashCommand: 'static + Send + Sync {
fn name(&self) -> String;
fn icon(&self) -> IconName {
IconName::Slash
}
fn label(&self, _cx: &AppContext) -> CodeLabel {
CodeLabel::plain(self.name(), None)
}

View File

@@ -84,9 +84,9 @@ pub struct AutoUpdater {
}
#[derive(Deserialize)]
struct JsonRelease {
version: String,
url: String,
pub struct JsonRelease {
pub version: String,
pub url: String,
}
struct MacOsUnmounter {
@@ -482,7 +482,7 @@ impl AutoUpdater {
release_channel: ReleaseChannel,
version: Option<SemanticVersion>,
cx: &mut AsyncAppContext,
) -> Result<(String, String)> {
) -> Result<(JsonRelease, String)> {
let this = cx.update(|cx| {
cx.default_global::<GlobalAutoUpdate>()
.0
@@ -504,7 +504,7 @@ impl AutoUpdater {
let update_request_body = build_remote_server_update_request_body(cx)?;
let body = serde_json::to_string(&update_request_body)?;
Ok((release.url, body))
Ok((release, body))
}
async fn get_release(
@@ -686,6 +686,12 @@ async fn download_remote_server_binary(
let request_body = AsyncBody::from(serde_json::to_string(&update_request_body)?);
let mut response = client.get(&release.url, request_body, true).await?;
if !response.status().is_success() {
return Err(anyhow!(
"failed to download remote server release: {:?}",
response.status()
));
}
smol::io::copy(response.body_mut(), &mut temp_file).await?;
smol::fs::rename(&temp, &target_path).await?;

View File

@@ -3,7 +3,7 @@ mod channel_index;
use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat, ChannelMessage};
use anyhow::{anyhow, Result};
use channel_index::ChannelIndex;
use client::{ChannelId, Client, ClientSettings, ProjectId, Subscription, User, UserId, UserStore};
use client::{ChannelId, Client, ClientSettings, Subscription, User, UserId, UserStore};
use collections::{hash_map, HashMap, HashSet};
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
use gpui::{
@@ -33,30 +33,11 @@ struct NotesVersion {
version: clock::Global,
}
#[derive(Debug, Clone)]
pub struct HostedProject {
project_id: ProjectId,
channel_id: ChannelId,
name: SharedString,
_visibility: proto::ChannelVisibility,
}
impl From<proto::HostedProject> for HostedProject {
fn from(project: proto::HostedProject) -> Self {
Self {
project_id: ProjectId(project.project_id),
channel_id: ChannelId(project.channel_id),
_visibility: project.visibility(),
name: project.name.into(),
}
}
}
pub struct ChannelStore {
pub channel_index: ChannelIndex,
channel_invitations: Vec<Arc<Channel>>,
channel_participants: HashMap<ChannelId, Vec<Arc<User>>>,
channel_states: HashMap<ChannelId, ChannelState>,
hosted_projects: HashMap<ProjectId, HostedProject>,
outgoing_invites: HashSet<(ChannelId, UserId)>,
update_channels_tx: mpsc::UnboundedSender<proto::UpdateChannels>,
opened_buffers: HashMap<ChannelId, OpenedModelHandle<ChannelBuffer>>,
@@ -85,7 +66,6 @@ pub struct ChannelState {
observed_notes_version: NotesVersion,
observed_chat_message: Option<u64>,
role: Option<ChannelRole>,
projects: HashSet<ProjectId>,
}
impl Channel {
@@ -216,7 +196,6 @@ impl ChannelStore {
channel_invitations: Vec::default(),
channel_index: ChannelIndex::default(),
channel_participants: Default::default(),
hosted_projects: Default::default(),
outgoing_invites: Default::default(),
opened_buffers: Default::default(),
opened_chats: Default::default(),
@@ -316,19 +295,6 @@ impl ChannelStore {
self.channel_index.by_id().get(&channel_id)
}
pub fn projects_for_id(&self, channel_id: ChannelId) -> Vec<(SharedString, ProjectId)> {
let mut projects: Vec<(SharedString, ProjectId)> = self
.channel_states
.get(&channel_id)
.map(|state| state.projects.clone())
.unwrap_or_default()
.into_iter()
.flat_map(|id| Some((self.hosted_projects.get(&id)?.name.clone(), id)))
.collect();
projects.sort();
projects
}
pub fn has_open_channel_buffer(&self, channel_id: ChannelId, _cx: &AppContext) -> bool {
if let Some(buffer) = self.opened_buffers.get(&channel_id) {
if let OpenedModelHandle::Open(buffer) = buffer {
@@ -1102,9 +1068,7 @@ impl ChannelStore {
let channels_changed = !payload.channels.is_empty()
|| !payload.delete_channels.is_empty()
|| !payload.latest_channel_message_ids.is_empty()
|| !payload.latest_channel_buffer_versions.is_empty()
|| !payload.hosted_projects.is_empty()
|| !payload.deleted_hosted_projects.is_empty();
|| !payload.latest_channel_buffer_versions.is_empty();
if channels_changed {
if !payload.delete_channels.is_empty() {
@@ -1161,34 +1125,6 @@ impl ChannelStore {
.or_default()
.update_latest_message_id(latest_channel_message.message_id);
}
for hosted_project in payload.hosted_projects {
let hosted_project: HostedProject = hosted_project.into();
if let Some(old_project) = self
.hosted_projects
.insert(hosted_project.project_id, hosted_project.clone())
{
self.channel_states
.entry(old_project.channel_id)
.or_default()
.remove_hosted_project(old_project.project_id);
}
self.channel_states
.entry(hosted_project.channel_id)
.or_default()
.add_hosted_project(hosted_project.project_id);
}
for hosted_project_id in payload.deleted_hosted_projects {
let hosted_project_id = ProjectId(hosted_project_id);
if let Some(old_project) = self.hosted_projects.remove(&hosted_project_id) {
self.channel_states
.entry(old_project.channel_id)
.or_default()
.remove_hosted_project(old_project.project_id);
}
}
}
cx.notify();
@@ -1295,12 +1231,4 @@ impl ChannelState {
};
}
}
fn add_hosted_project(&mut self, project_id: ProjectId) {
self.projects.insert(project_id);
}
fn remove_hosted_project(&mut self, project_id: ProjectId) {
self.projects.remove(&project_id);
}
}

View File

@@ -48,6 +48,7 @@ pub struct Collaborator {
pub peer_id: proto::PeerId,
pub replica_id: ReplicaId,
pub user_id: UserId,
pub is_host: bool,
}
impl PartialOrd for User {
@@ -824,6 +825,7 @@ impl Collaborator {
peer_id: message.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?,
replica_id: message.replica_id as ReplicaId,
user_id: message.user_id as UserId,
is_host: message.is_host,
})
}
}

View File

@@ -52,9 +52,7 @@ CREATE TABLE "projects" (
"host_user_id" INTEGER REFERENCES users (id),
"host_connection_id" INTEGER,
"host_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE,
"unregistered" BOOLEAN NOT NULL DEFAULT FALSE,
"hosted_project_id" INTEGER REFERENCES hosted_projects (id),
"dev_server_project_id" INTEGER REFERENCES dev_server_projects(id)
"unregistered" BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE INDEX "index_projects_on_host_connection_server_id" ON "projects" ("host_connection_server_id");
CREATE INDEX "index_projects_on_host_connection_id_and_host_connection_server_id" ON "projects" ("host_connection_id", "host_connection_server_id");
@@ -399,30 +397,6 @@ CREATE TABLE rate_buckets (
);
CREATE INDEX idx_user_id_rate_limit ON rate_buckets (user_id, rate_limit_name);
CREATE TABLE hosted_projects (
id INTEGER PRIMARY KEY AUTOINCREMENT,
channel_id INTEGER NOT NULL REFERENCES channels(id),
name TEXT NOT NULL,
visibility TEXT NOT NULL,
deleted_at TIMESTAMP NULL
);
CREATE INDEX idx_hosted_projects_on_channel_id ON hosted_projects (channel_id);
CREATE UNIQUE INDEX uix_hosted_projects_on_channel_id_and_name ON hosted_projects (channel_id, name) WHERE (deleted_at IS NULL);
CREATE TABLE dev_servers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id),
name TEXT NOT NULL,
ssh_connection_string TEXT,
hashed_token TEXT NOT NULL
);
CREATE TABLE dev_server_projects (
id INTEGER PRIMARY KEY AUTOINCREMENT,
dev_server_id INTEGER NOT NULL REFERENCES dev_servers(id),
paths TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS billing_preferences (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,

View File

@@ -0,0 +1,6 @@
ALTER TABLE projects DROP COLUMN dev_server_project_id;
ALTER TABLE projects DROP COLUMN hosted_project_id;
DROP TABLE hosted_projects;
DROP TABLE dev_server_projects;
DROP TABLE dev_servers;

View File

@@ -252,7 +252,10 @@ async fn create_billing_subscription(
let default_model = llm_db.model(rpc::LanguageModelProvider::Anthropic, "claude-3-5-sonnet")?;
let stripe_model = stripe_billing.register_model(default_model).await?;
let success_url = format!("{}/account", app.config.zed_dot_dev_url());
let success_url = format!(
"{}/account?checkout_complete=1",
app.config.zed_dot_dev_url()
);
let checkout_session_url = stripe_billing
.checkout(customer_id, &user.github_login, &stripe_model, &success_url)
.await?;

View File

@@ -617,7 +617,6 @@ pub struct ChannelsForUser {
pub channels: Vec<Channel>,
pub channel_memberships: Vec<channel_member::Model>,
pub channel_participants: HashMap<ChannelId, Vec<UserId>>,
pub hosted_projects: Vec<proto::HostedProject>,
pub invited_channels: Vec<Channel>,
pub observed_buffer_versions: Vec<proto::ChannelBufferVersion>,
@@ -741,6 +740,7 @@ impl ProjectCollaborator {
peer_id: Some(self.connection_id.into()),
replica_id: self.replica_id.0 as u32,
user_id: self.user_id.to_proto(),
is_host: self.is_host,
}
}
}

View File

@@ -10,7 +10,6 @@ pub mod contacts;
pub mod contributors;
pub mod embeddings;
pub mod extensions;
pub mod hosted_projects;
pub mod messages;
pub mod notifications;
pub mod processed_stripe_events;

View File

@@ -116,6 +116,7 @@ impl Database {
peer_id: Some(collaborator.connection().into()),
user_id: collaborator.user_id.to_proto(),
replica_id: collaborator.replica_id.0 as u32,
is_host: false,
})
.collect(),
})
@@ -222,6 +223,7 @@ impl Database {
peer_id: Some(collaborator.connection().into()),
user_id: collaborator.user_id.to_proto(),
replica_id: collaborator.replica_id.0 as u32,
is_host: false,
})
.collect(),
},
@@ -257,6 +259,7 @@ impl Database {
peer_id: Some(db_collaborator.connection().into()),
replica_id: db_collaborator.replica_id.0 as u32,
user_id: db_collaborator.user_id.to_proto(),
is_host: false,
})
} else {
collaborator_ids_to_remove.push(db_collaborator.id);
@@ -385,6 +388,7 @@ impl Database {
peer_id: Some(connection.into()),
replica_id: row.replica_id.0 as u32,
user_id: row.user_id.to_proto(),
is_host: false,
});
}

View File

@@ -615,15 +615,10 @@ impl Database {
.observed_channel_messages(&channel_ids, user_id, tx)
.await?;
let hosted_projects = self
.get_hosted_projects(&channel_ids, &roles_by_channel_id, tx)
.await?;
Ok(ChannelsForUser {
channel_memberships,
channels,
invited_channels,
hosted_projects,
channel_participants,
latest_buffer_versions,
latest_channel_messages,

View File

@@ -1,85 +0,0 @@
use rpc::{proto, ErrorCode};
use super::*;
impl Database {
pub async fn get_hosted_projects(
&self,
channel_ids: &[ChannelId],
roles: &HashMap<ChannelId, ChannelRole>,
tx: &DatabaseTransaction,
) -> Result<Vec<proto::HostedProject>> {
let projects = hosted_project::Entity::find()
.find_also_related(project::Entity)
.filter(hosted_project::Column::ChannelId.is_in(channel_ids.iter().map(|id| id.0)))
.all(tx)
.await?
.into_iter()
.flat_map(|(hosted_project, project)| {
if hosted_project.deleted_at.is_some() {
return None;
}
match hosted_project.visibility {
ChannelVisibility::Public => {}
ChannelVisibility::Members => {
let is_visible = roles
.get(&hosted_project.channel_id)
.map(|role| role.can_see_all_descendants())
.unwrap_or(false);
if !is_visible {
return None;
}
}
};
Some(proto::HostedProject {
project_id: project?.id.to_proto(),
channel_id: hosted_project.channel_id.to_proto(),
name: hosted_project.name.clone(),
visibility: hosted_project.visibility.into(),
})
})
.collect();
Ok(projects)
}
pub async fn get_hosted_project(
&self,
hosted_project_id: HostedProjectId,
user_id: UserId,
tx: &DatabaseTransaction,
) -> Result<(hosted_project::Model, ChannelRole)> {
let project = hosted_project::Entity::find_by_id(hosted_project_id)
.one(tx)
.await?
.ok_or_else(|| anyhow!(ErrorCode::NoSuchProject))?;
let channel = channel::Entity::find_by_id(project.channel_id)
.one(tx)
.await?
.ok_or_else(|| anyhow!(ErrorCode::NoSuchChannel))?;
let role = match project.visibility {
ChannelVisibility::Public => {
self.check_user_is_channel_participant(&channel, user_id, tx)
.await?
}
ChannelVisibility::Members => {
self.check_user_is_channel_member(&channel, user_id, tx)
.await?
}
};
Ok((project, role))
}
pub async fn is_hosted_project(&self, project_id: ProjectId) -> Result<bool> {
self.transaction(|tx| async move {
Ok(project::Entity::find_by_id(project_id)
.one(&*tx)
.await?
.map(|project| project.hosted_project_id.is_some())
.ok_or_else(|| anyhow!(ErrorCode::NoSuchProject))?)
})
.await
}
}

View File

@@ -68,7 +68,6 @@ impl Database {
connection.owner_id as i32,
))),
id: ActiveValue::NotSet,
hosted_project_id: ActiveValue::Set(None),
}
.insert(&*tx)
.await?;
@@ -536,39 +535,6 @@ impl Database {
.await
}
/// Adds the given connection to the specified hosted project
pub async fn join_hosted_project(
&self,
id: ProjectId,
user_id: UserId,
connection: ConnectionId,
) -> Result<(Project, ReplicaId)> {
self.transaction(|tx| async move {
let (project, hosted_project) = project::Entity::find_by_id(id)
.find_also_related(hosted_project::Entity)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("hosted project is no longer shared"))?;
let Some(hosted_project) = hosted_project else {
return Err(anyhow!("project is not hosted"))?;
};
let channel = channel::Entity::find_by_id(hosted_project.channel_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such channel"))?;
let role = self
.check_user_is_channel_participant(&channel, user_id, &tx)
.await?;
self.join_project_internal(project, user_id, connection, role, &tx)
.await
})
.await
}
pub async fn get_project(&self, id: ProjectId) -> Result<project::Model> {
self.transaction(|tx| async move {
Ok(project::Entity::find_by_id(id)
@@ -784,49 +750,6 @@ impl Database {
Ok((project, replica_id as ReplicaId))
}
pub async fn leave_hosted_project(
&self,
project_id: ProjectId,
connection: ConnectionId,
) -> Result<LeftProject> {
self.transaction(|tx| async move {
let result = project_collaborator::Entity::delete_many()
.filter(
Condition::all()
.add(project_collaborator::Column::ProjectId.eq(project_id))
.add(project_collaborator::Column::ConnectionId.eq(connection.id as i32))
.add(
project_collaborator::Column::ConnectionServerId
.eq(connection.owner_id as i32),
),
)
.exec(&*tx)
.await?;
if result.rows_affected == 0 {
return Err(anyhow!("not in the project"))?;
}
let project = project::Entity::find_by_id(project_id)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such project"))?;
let collaborators = project
.find_related(project_collaborator::Entity)
.all(&*tx)
.await?;
let connection_ids = collaborators
.into_iter()
.map(|collaborator| collaborator.connection())
.collect();
Ok(LeftProject {
id: project.id,
connection_ids,
should_unshare: false,
})
})
.await
}
/// Removes the given connection from the specified project.
pub async fn leave_project(
&self,

View File

@@ -18,7 +18,6 @@ pub mod extension;
pub mod extension_version;
pub mod feature_flag;
pub mod follower;
pub mod hosted_project;
pub mod language_server;
pub mod notification;
pub mod notification_kind;

View File

@@ -1,27 +0,0 @@
use crate::db::{ChannelId, ChannelVisibility, HostedProjectId};
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "hosted_projects")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: HostedProjectId,
pub channel_id: ChannelId,
pub name: String,
pub visibility: ChannelVisibility,
pub deleted_at: Option<DateTime>,
}
impl ActiveModelBehavior for ActiveModel {}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_one = "super::project::Entity")]
Project,
}
impl Related<super::project::Entity> for Entity {
fn to() -> RelationDef {
Relation::Project.def()
}
}

View File

@@ -1,4 +1,4 @@
use crate::db::{HostedProjectId, ProjectId, Result, RoomId, ServerId, UserId};
use crate::db::{ProjectId, Result, RoomId, ServerId, UserId};
use anyhow::anyhow;
use rpc::ConnectionId;
use sea_orm::entity::prelude::*;
@@ -12,7 +12,6 @@ pub struct Model {
pub host_user_id: Option<UserId>,
pub host_connection_id: Option<i32>,
pub host_connection_server_id: Option<ServerId>,
pub hosted_project_id: Option<HostedProjectId>,
}
impl Model {
@@ -50,12 +49,6 @@ pub enum Relation {
Collaborators,
#[sea_orm(has_many = "super::language_server::Entity")]
LanguageServers,
#[sea_orm(
belongs_to = "super::hosted_project::Entity",
from = "Column::HostedProjectId",
to = "super::hosted_project::Column::Id"
)]
HostedProject,
}
impl Related<super::user::Entity> for Entity {
@@ -88,10 +81,4 @@ impl Related<super::language_server::Entity> for Entity {
}
}
impl Related<super::hosted_project::Entity> for Entity {
fn to() -> RelationDef {
Relation::HostedProject.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -121,11 +121,13 @@ async fn test_channel_buffers(db: &Arc<Database>) {
user_id: a_id.to_proto(),
peer_id: Some(rpc::proto::PeerId { id: 1, owner_id }),
replica_id: 0,
is_host: false,
},
rpc::proto::Collaborator {
user_id: b_id.to_proto(),
peer_id: Some(rpc::proto::PeerId { id: 2, owner_id }),
replica_id: 1,
is_host: false,
}
]
);

View File

@@ -449,6 +449,10 @@ async fn check_usage_limit(
model_name: &str,
claims: &LlmTokenClaims,
) -> Result<()> {
if claims.is_staff {
return Ok(());
}
let model = state.db.model(provider, model_name)?;
let usage = state
.db
@@ -513,11 +517,6 @@ async fn check_usage_limit(
];
for (used, limit, usage_measure) in checks {
// Temporarily bypass rate-limiting for staff members.
if claims.is_staff {
continue;
}
if used > limit {
let resource = match usage_measure {
UsageMeasure::RequestsPerMinute => "requests_per_minute",

View File

@@ -287,7 +287,6 @@ impl Server {
.add_request_handler(share_project)
.add_message_handler(unshare_project)
.add_request_handler(join_project)
.add_request_handler(join_hosted_project)
.add_message_handler(leave_project)
.add_request_handler(update_project)
.add_request_handler(update_worktree)
@@ -308,6 +307,8 @@ impl Server {
.add_request_handler(forward_read_only_project_request::<proto::InlayHints>)
.add_request_handler(forward_read_only_project_request::<proto::ResolveInlayHint>)
.add_request_handler(forward_read_only_project_request::<proto::OpenBufferByPath>)
.add_request_handler(forward_read_only_project_request::<proto::GitBranches>)
.add_request_handler(forward_mutating_project_request::<proto::UpdateGitBranch>)
.add_request_handler(forward_mutating_project_request::<proto::GetCompletions>)
.add_request_handler(
forward_mutating_project_request::<proto::ApplyCompletionAdditionalEdits>,
@@ -1793,11 +1794,6 @@ impl JoinProjectInternalResponse for Response<proto::JoinProject> {
Response::<proto::JoinProject>::send(self, result)
}
}
impl JoinProjectInternalResponse for Response<proto::JoinHostedProject> {
fn send(self, result: proto::JoinProjectResponse) -> Result<()> {
Response::<proto::JoinHostedProject>::send(self, result)
}
}
fn join_project_internal(
response: impl JoinProjectInternalResponse,
@@ -1831,6 +1827,7 @@ fn join_project_internal(
peer_id: Some(session.connection_id.into()),
replica_id: replica_id.0 as u32,
user_id: guest_user_id.to_proto(),
is_host: false,
}),
};
@@ -1921,11 +1918,6 @@ async fn leave_project(request: proto::LeaveProject, session: Session) -> Result
let sender_id = session.connection_id;
let project_id = ProjectId::from_proto(request.project_id);
let db = session.db().await;
if db.is_hosted_project(project_id).await? {
let project = db.leave_hosted_project(project_id, sender_id).await?;
project_left(&project, &session);
return Ok(());
}
let (room, project) = &*db.leave_project(project_id, sender_id).await?;
tracing::info!(
@@ -1941,24 +1933,6 @@ async fn leave_project(request: proto::LeaveProject, session: Session) -> Result
Ok(())
}
async fn join_hosted_project(
request: proto::JoinHostedProject,
response: Response<proto::JoinHostedProject>,
session: Session,
) -> Result<()> {
let (mut project, replica_id) = session
.db()
.await
.join_hosted_project(
ProjectId(request.project_id as i32),
session.user_id(),
session.connection_id,
)
.await?;
join_project_internal(response, session, &mut project, &replica_id)
}
/// Updates other participants with changes to the project
async fn update_project(
request: proto::UpdateProject,
@@ -4200,7 +4174,6 @@ fn build_channels_update(channels: ChannelsForUser) -> proto::UpdateChannels {
update.channel_invitations.push(channel.to_proto());
}
update.hosted_projects = channels.hosted_projects;
update
}

View File

@@ -1978,6 +1978,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
enabled: false,
delay_ms: None,
min_column: None,
show_commit_summary: false,
});
cx_a.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {

View File

@@ -1957,9 +1957,10 @@ async fn test_following_to_channel_notes_without_a_shared_project(
});
channel_notes_1_b.update(cx_b, |notes, cx| {
assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
let editor = notes.editor.read(cx);
assert_eq!(editor.text(cx), "Hello from A.");
assert_eq!(editor.selections.ranges::<usize>(cx), &[3..4]);
notes.editor.update(cx, |editor, cx| {
assert_eq!(editor.text(cx), "Hello from A.");
assert_eq!(editor.selections.ranges::<usize>(cx), &[3..4]);
})
});
// Client A opens the notes for channel 2.

View File

@@ -21,8 +21,8 @@ use language::{
language_settings::{
AllLanguageSettings, Formatter, FormatterList, PrettierSettings, SelectedFormatter,
},
tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, LanguageConfig,
LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope,
tree_sitter_rust, tree_sitter_typescript, Diagnostic, DiagnosticEntry, FakeLspAdapter,
Language, LanguageConfig, LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope,
};
use live_kit_client::MacOSDisplay;
use lsp::LanguageServerId;
@@ -4461,7 +4461,7 @@ async fn test_prettier_formatting_buffer(
},
..Default::default()
},
Some(tree_sitter_rust::LANGUAGE.into()),
Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
)));
let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
"TypeScript",
@@ -6575,3 +6575,95 @@ async fn test_context_collaboration_with_reconnect(
assert!(context.buffer().read(cx).read_only());
});
}
#[gpui::test]
async fn test_remote_git_branches(
executor: BackgroundExecutor,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
let mut server = TestServer::start(executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
client_a
.fs()
.insert_tree("/project", serde_json::json!({ ".git":{} }))
.await;
let branches = ["main", "dev", "feature-1"];
client_a
.fs()
.insert_branches(Path::new("/project/.git"), &branches);
let (project_a, worktree_id) = client_a.build_local_project("/project", cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let project_b = client_b.join_remote_project(project_id, cx_b).await;
let root_path = ProjectPath::root_path(worktree_id);
// Client A sees that a guest has joined.
executor.run_until_parked();
let branches_b = cx_b
.update(|cx| project_b.update(cx, |project, cx| project.branches(root_path.clone(), cx)))
.await
.unwrap();
let new_branch = branches[2];
let branches_b = branches_b
.into_iter()
.map(|branch| branch.name)
.collect::<Vec<_>>();
assert_eq!(&branches_b, &branches);
cx_b.update(|cx| {
project_b.update(cx, |project, cx| {
project.update_or_create_branch(root_path.clone(), new_branch.to_string(), cx)
})
})
.await
.unwrap();
executor.run_until_parked();
let host_branch = cx_a.update(|cx| {
project_a.update(cx, |project, cx| {
project.worktree_store().update(cx, |worktree_store, cx| {
worktree_store
.current_branch(root_path.clone(), cx)
.unwrap()
})
})
});
assert_eq!(host_branch.as_ref(), branches[2]);
// Also try creating a new branch
cx_b.update(|cx| {
project_b.update(cx, |project, cx| {
project.update_or_create_branch(root_path.clone(), "totally-new-branch".to_string(), cx)
})
})
.await
.unwrap();
executor.run_until_parked();
let host_branch = cx_a.update(|cx| {
project_a.update(cx, |project, cx| {
project.worktree_store().update(cx, |worktree_store, cx| {
worktree_store.current_branch(root_path, cx).unwrap()
})
})
});
assert_eq!(host_branch.as_ref(), "totally-new-branch");
}

View File

@@ -1,14 +1,27 @@
use crate::tests::TestServer;
use call::ActiveCall;
use collections::HashSet;
use fs::{FakeFs, Fs as _};
use gpui::{Context as _, TestAppContext};
use futures::StreamExt as _;
use gpui::{BackgroundExecutor, Context as _, TestAppContext, UpdateGlobal as _};
use http_client::BlockedHttpClient;
use language::{language_settings::language_settings, LanguageRegistry};
use language::{
language_settings::{
language_settings, AllLanguageSettings, Formatter, FormatterList, PrettierSettings,
SelectedFormatter,
},
tree_sitter_typescript, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher,
LanguageRegistry,
};
use node_runtime::NodeRuntime;
use project::ProjectPath;
use project::{
lsp_store::{FormatTarget, FormatTrigger},
ProjectPath,
};
use remote::SshRemoteClient;
use remote_server::{HeadlessAppState, HeadlessProject};
use serde_json::json;
use settings::SettingsStore;
use std::{path::Path, sync::Arc};
#[gpui::test(iterations = 10)]
@@ -174,3 +187,311 @@ async fn test_sharing_an_ssh_remote_project(
);
});
}
#[gpui::test]
async fn test_ssh_collaboration_git_branches(
executor: BackgroundExecutor,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
server_cx: &mut TestAppContext,
) {
cx_a.set_name("a");
cx_b.set_name("b");
server_cx.set_name("server");
let mut server = TestServer::start(executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
// Set up project on remote FS
let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
let remote_fs = FakeFs::new(server_cx.executor());
remote_fs
.insert_tree("/project", serde_json::json!({ ".git":{} }))
.await;
let branches = ["main", "dev", "feature-1"];
remote_fs.insert_branches(Path::new("/project/.git"), &branches);
// User A connects to the remote project via SSH.
server_cx.update(HeadlessProject::init);
let remote_http_client = Arc::new(BlockedHttpClient);
let node = NodeRuntime::unavailable();
let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
let headless_project = server_cx.new_model(|cx| {
client::init_settings(cx);
HeadlessProject::new(
HeadlessAppState {
session: server_ssh,
fs: remote_fs.clone(),
http_client: remote_http_client,
node_runtime: node,
languages,
},
cx,
)
});
let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
let (project_a, worktree_id) = client_a
.build_ssh_project("/project", client_ssh, cx_a)
.await;
// While the SSH worktree is being scanned, user A shares the remote project.
let active_call_a = cx_a.read(ActiveCall::global);
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
// User B joins the project.
let project_b = client_b.join_remote_project(project_id, cx_b).await;
// Give client A sometime to see that B has joined, and that the headless server
// has some git repositories
executor.run_until_parked();
let root_path = ProjectPath::root_path(worktree_id);
let branches_b = cx_b
.update(|cx| project_b.update(cx, |project, cx| project.branches(root_path.clone(), cx)))
.await
.unwrap();
let new_branch = branches[2];
let branches_b = branches_b
.into_iter()
.map(|branch| branch.name)
.collect::<Vec<_>>();
assert_eq!(&branches_b, &branches);
cx_b.update(|cx| {
project_b.update(cx, |project, cx| {
project.update_or_create_branch(root_path.clone(), new_branch.to_string(), cx)
})
})
.await
.unwrap();
executor.run_until_parked();
let server_branch = server_cx.update(|cx| {
headless_project.update(cx, |headless_project, cx| {
headless_project
.worktree_store
.update(cx, |worktree_store, cx| {
worktree_store
.current_branch(root_path.clone(), cx)
.unwrap()
})
})
});
assert_eq!(server_branch.as_ref(), branches[2]);
// Also try creating a new branch
cx_b.update(|cx| {
project_b.update(cx, |project, cx| {
project.update_or_create_branch(root_path.clone(), "totally-new-branch".to_string(), cx)
})
})
.await
.unwrap();
executor.run_until_parked();
let server_branch = server_cx.update(|cx| {
headless_project.update(cx, |headless_project, cx| {
headless_project
.worktree_store
.update(cx, |worktree_store, cx| {
worktree_store.current_branch(root_path, cx).unwrap()
})
})
});
assert_eq!(server_branch.as_ref(), "totally-new-branch");
}
#[gpui::test]
async fn test_ssh_collaboration_formatting_with_prettier(
executor: BackgroundExecutor,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
server_cx: &mut TestAppContext,
) {
cx_a.set_name("a");
cx_b.set_name("b");
server_cx.set_name("server");
let mut server = TestServer::start(executor.clone()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
let remote_fs = FakeFs::new(server_cx.executor());
let buffer_text = "let one = \"two\"";
let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
remote_fs
.insert_tree("/project", serde_json::json!({ "a.ts": buffer_text }))
.await;
let test_plugin = "test_plugin";
let ts_lang = Arc::new(Language::new(
LanguageConfig {
name: "TypeScript".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["ts".to_string()],
..LanguageMatcher::default()
},
..LanguageConfig::default()
},
Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
));
client_a.language_registry().add(ts_lang.clone());
client_b.language_registry().add(ts_lang.clone());
let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
let mut fake_language_servers = languages.register_fake_lsp(
"TypeScript",
FakeLspAdapter {
prettier_plugins: vec![test_plugin],
..Default::default()
},
);
// User A connects to the remote project via SSH.
server_cx.update(HeadlessProject::init);
let remote_http_client = Arc::new(BlockedHttpClient);
let _headless_project = server_cx.new_model(|cx| {
client::init_settings(cx);
HeadlessProject::new(
HeadlessAppState {
session: server_ssh,
fs: remote_fs.clone(),
http_client: remote_http_client,
node_runtime: NodeRuntime::unavailable(),
languages,
},
cx,
)
});
let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
let (project_a, worktree_id) = client_a
.build_ssh_project("/project", client_ssh, cx_a)
.await;
// While the SSH worktree is being scanned, user A shares the remote project.
let active_call_a = cx_a.read(ActiveCall::global);
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
// User B joins the project.
let project_b = client_b.join_remote_project(project_id, cx_b).await;
executor.run_until_parked();
// Opens the buffer and formats it
let buffer_b = project_b
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx))
.await
.expect("user B opens buffer for formatting");
cx_a.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
file.defaults.formatter = Some(SelectedFormatter::Auto);
file.defaults.prettier = Some(PrettierSettings {
allowed: true,
..PrettierSettings::default()
});
});
});
});
cx_b.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
vec![Formatter::LanguageServer { name: None }].into(),
)));
file.defaults.prettier = Some(PrettierSettings {
allowed: true,
..PrettierSettings::default()
});
});
});
});
let fake_language_server = fake_language_servers.next().await.unwrap();
fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
panic!(
"Unexpected: prettier should be preferred since it's enabled and language supports it"
)
});
project_b
.update(cx_b, |project, cx| {
project.format(
HashSet::from_iter([buffer_b.clone()]),
true,
FormatTrigger::Save,
FormatTarget::Buffer,
cx,
)
})
.await
.unwrap();
executor.run_until_parked();
assert_eq!(
buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
buffer_text.to_string() + "\n" + prettier_format_suffix,
"Prettier formatting was not applied to client buffer after client's request"
);
// User A opens and formats the same buffer too
let buffer_a = project_a
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx))
.await
.expect("user A opens buffer for formatting");
cx_a.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
file.defaults.formatter = Some(SelectedFormatter::Auto);
file.defaults.prettier = Some(PrettierSettings {
allowed: true,
..PrettierSettings::default()
});
});
});
});
project_a
.update(cx_a, |project, cx| {
project.format(
HashSet::from_iter([buffer_a.clone()]),
true,
FormatTrigger::Manual,
FormatTarget::Buffer,
cx,
)
})
.await
.unwrap();
executor.run_until_parked();
assert_eq!(
buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix,
"Prettier formatting was not applied to client buffer after host's request"
);
}

View File

@@ -5,7 +5,7 @@ use self::channel_modal::ChannelModal;
use crate::{channel_view::ChannelView, chat_panel::ChatPanel, CollaborationPanelSettings};
use call::ActiveCall;
use channel::{Channel, ChannelEvent, ChannelStore};
use client::{ChannelId, Client, Contact, ProjectId, User, UserStore};
use client::{ChannelId, Client, Contact, User, UserStore};
use contact_finder::ContactFinder;
use db::kvp::KEY_VALUE_STORE;
use editor::{Editor, EditorElement, EditorStyle};
@@ -182,10 +182,6 @@ enum ListEntry {
ChannelEditor {
depth: usize,
},
HostedProject {
id: ProjectId,
name: SharedString,
},
Contact {
contact: Arc<Contact>,
calling: bool,
@@ -566,7 +562,6 @@ impl CollabPanel {
}
}
let hosted_projects = channel_store.projects_for_id(channel.id);
let has_children = channel_store
.channel_at_index(mat.candidate_id + 1)
.map_or(false, |next_channel| {
@@ -600,10 +595,6 @@ impl CollabPanel {
});
}
}
for (name, id) in hosted_projects {
self.entries.push(ListEntry::HostedProject { id, name });
}
}
}
@@ -1029,40 +1020,6 @@ impl CollabPanel {
.tooltip(move |cx| Tooltip::text("Open Chat", cx))
}
fn render_channel_project(
&self,
id: ProjectId,
name: &SharedString,
is_selected: bool,
cx: &mut ViewContext<Self>,
) -> impl IntoElement {
ListItem::new(ElementId::NamedInteger(
"channel-project".into(),
id.0 as usize,
))
.indent_level(2)
.indent_step_size(px(20.))
.selected(is_selected)
.on_click(cx.listener(move |this, _, cx| {
if let Some(workspace) = this.workspace.upgrade() {
let app_state = workspace.read(cx).app_state().clone();
workspace::join_hosted_project(id, app_state, cx).detach_and_prompt_err(
"Failed to open project",
cx,
|_, _| None,
)
}
}))
.start_slot(
h_flex()
.relative()
.gap_1()
.child(IconButton::new(0, IconName::FileTree)),
)
.child(Label::new(name.clone()))
.tooltip(move |cx| Tooltip::text("Open Project", cx))
}
fn has_subchannels(&self, ix: usize) -> bool {
self.entries.get(ix).map_or(false, |entry| {
if let ListEntry::Channel { has_children, .. } = entry {
@@ -1538,12 +1495,6 @@ impl CollabPanel {
ListEntry::ChannelChat { channel_id } => {
self.join_channel_chat(*channel_id, cx)
}
ListEntry::HostedProject {
id: _id,
name: _name,
} => {
// todo()
}
ListEntry::OutgoingRequest(_) => {}
ListEntry::ChannelEditor { .. } => {}
}
@@ -2157,10 +2108,6 @@ impl CollabPanel {
ListEntry::ChannelChat { channel_id } => self
.render_channel_chat(*channel_id, is_selected, cx)
.into_any_element(),
ListEntry::HostedProject { id, name } => self
.render_channel_project(*id, name, is_selected, cx)
.into_any_element(),
}
}
@@ -2779,7 +2726,7 @@ impl Render for CollabPanel {
.on_action(cx.listener(CollabPanel::collapse_selected_channel))
.on_action(cx.listener(CollabPanel::expand_selected_channel))
.on_action(cx.listener(CollabPanel::start_move_selected_channel))
.track_focus(&self.focus_handle)
.track_focus(&self.focus_handle(cx))
.size_full()
.child(if self.user_store.read(cx).current_user().is_none() {
self.render_signed_out(cx)
@@ -2898,11 +2845,6 @@ impl PartialEq for ListEntry {
return channel_1.id == channel_2.id;
}
}
ListEntry::HostedProject { id, .. } => {
if let ListEntry::HostedProject { id: other_id, .. } = other {
return id == other_id;
}
}
ListEntry::ChannelNotes { channel_id } => {
if let ListEntry::ChannelNotes {
channel_id: other_id,

View File

@@ -180,6 +180,39 @@ impl InitializedContextServerProtocol {
Ok(completion)
}
/// List MCP tools.
pub async fn list_tools(&self) -> Result<types::ListToolsResponse> {
self.check_capability(ServerCapability::Tools)?;
let response = self
.inner
.request::<types::ListToolsResponse>(types::RequestType::ListTools.as_str(), ())
.await?;
Ok(response)
}
/// Executes a tool with the given arguments
pub async fn run_tool<P: AsRef<str>>(
&self,
tool: P,
arguments: Option<HashMap<String, serde_json::Value>>,
) -> Result<types::CallToolResponse> {
self.check_capability(ServerCapability::Tools)?;
let params = types::CallToolParams {
name: tool.as_ref().to_string(),
arguments,
};
let response: types::CallToolResponse = self
.inner
.request(types::RequestType::CallTool.as_str(), params)
.await?;
Ok(response)
}
}
impl InitializedContextServerProtocol {

View File

@@ -9,7 +9,8 @@ struct GlobalContextServerRegistry(Arc<ContextServerRegistry>);
impl Global for GlobalContextServerRegistry {}
pub struct ContextServerRegistry {
registry: RwLock<HashMap<String, Vec<Arc<str>>>>,
command_registry: RwLock<HashMap<String, Vec<Arc<str>>>>,
tool_registry: RwLock<HashMap<String, Vec<Arc<str>>>>,
}
impl ContextServerRegistry {
@@ -20,13 +21,14 @@ impl ContextServerRegistry {
pub fn register(cx: &mut AppContext) {
cx.set_global(GlobalContextServerRegistry(Arc::new(
ContextServerRegistry {
registry: RwLock::new(HashMap::default()),
command_registry: RwLock::new(HashMap::default()),
tool_registry: RwLock::new(HashMap::default()),
},
)))
}
pub fn register_command(&self, server_id: String, command_name: &str) {
let mut registry = self.registry.write();
let mut registry = self.command_registry.write();
registry
.entry(server_id)
.or_default()
@@ -34,14 +36,34 @@ impl ContextServerRegistry {
}
pub fn unregister_command(&self, server_id: &str, command_name: &str) {
let mut registry = self.registry.write();
let mut registry = self.command_registry.write();
if let Some(commands) = registry.get_mut(server_id) {
commands.retain(|name| name.as_ref() != command_name);
}
}
pub fn get_commands(&self, server_id: &str) -> Option<Vec<Arc<str>>> {
let registry = self.registry.read();
let registry = self.command_registry.read();
registry.get(server_id).cloned()
}
pub fn register_tool(&self, server_id: String, tool_name: &str) {
let mut registry = self.tool_registry.write();
registry
.entry(server_id)
.or_default()
.push(tool_name.into());
}
pub fn unregister_tool(&self, server_id: &str, tool_name: &str) {
let mut registry = self.tool_registry.write();
if let Some(tools) = registry.get_mut(server_id) {
tools.retain(|name| name.as_ref() != tool_name);
}
}
pub fn get_tools(&self, server_id: &str) -> Option<Vec<Arc<str>>> {
let registry = self.tool_registry.read();
registry.get(server_id).cloned()
}
}

View File

@@ -16,6 +16,8 @@ pub enum RequestType {
PromptsList,
CompletionComplete,
Ping,
ListTools,
ListResourceTemplates,
}
impl RequestType {
@@ -32,6 +34,8 @@ impl RequestType {
RequestType::PromptsList => "prompts/list",
RequestType::CompletionComplete => "completion/complete",
RequestType::Ping => "ping",
RequestType::ListTools => "tools/list",
RequestType::ListResourceTemplates => "resources/templates/list",
}
}
}
@@ -402,3 +406,17 @@ pub struct Completion {
pub values: Vec<String>,
pub total: CompletionTotal,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CallToolResponse {
pub tool_result: serde_json::Value,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListToolsResponse {
pub tools: Vec<Tool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
}

View File

@@ -185,7 +185,7 @@ impl Render for CopilotCodeVerification {
v_flex()
.id("copilot code verification")
.track_focus(&self.focus_handle)
.track_focus(&self.focus_handle(cx))
.elevation_3(cx)
.w_96()
.items_center()

View File

@@ -101,7 +101,7 @@ impl Render for ProjectDiagnosticsEditor {
};
div()
.track_focus(&self.focus_handle)
.track_focus(&self.focus_handle(cx))
.when(self.path_states.is_empty(), |el| {
el.key_context("EmptyPane")
})

View File

@@ -986,6 +986,7 @@ fn editor_blocks(
em_width: px(0.),
max_width: px(0.),
block_id,
selected: false,
editor_style: &editor::EditorStyle::default(),
});
let element = element.downcast_mut::<Stateful<Div>>().unwrap();

View File

@@ -136,11 +136,12 @@ impl DiagnosticIndicator {
}
fn update(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
let editor = editor.read(cx);
let buffer = editor.buffer().read(cx);
let cursor_position = editor.selections.newest::<usize>(cx).head();
let (buffer, cursor_position) = editor.update(cx, |editor, cx| {
let buffer = editor.buffer().read(cx).snapshot(cx);
let cursor_position = editor.selections.newest::<usize>(cx).head();
(buffer, cursor_position)
});
let new_diagnostic = buffer
.snapshot(cx)
.diagnostics_in_range::<_, usize>(cursor_position..cursor_position, false)
.filter(|entry| !entry.range.is_empty())
.min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))

View File

@@ -76,12 +76,12 @@ theme.workspace = true
tree-sitter-html = { workspace = true, optional = true }
tree-sitter-rust = { workspace = true, optional = true }
tree-sitter-typescript = { workspace = true, optional = true }
unicode-segmentation.workspace = true
unindent = { workspace = true, optional = true }
ui.workspace = true
url.workspace = true
util.workspace = true
workspace.workspace = true
unicode-segmentation.workspace = true
[dev-dependencies]
ctor.workspace = true

View File

@@ -80,6 +80,8 @@ pub struct ConfirmCodeAction {
pub struct ToggleComments {
#[serde(default)]
pub advance_downwards: bool,
#[serde(default)]
pub ignore_indent: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
@@ -153,6 +155,17 @@ pub struct DeleteToPreviousWordStart {
pub ignore_newlines: bool,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct FoldAtLevel {
pub level: u32,
}
#[derive(PartialEq, Clone, Deserialize, Default)]
pub struct SpawnNearestTask {
#[serde(default)]
pub reveal: task::RevealStrategy,
}
impl_actions!(
editor,
[
@@ -178,10 +191,12 @@ impl_actions!(
SelectToBeginningOfLine,
SelectToEndOfLine,
SelectUpByLines,
SpawnNearestTask,
ShowCompletions,
ToggleCodeActions,
ToggleComments,
UnfoldAt,
FoldAtLevel
]
);
@@ -193,6 +208,7 @@ gpui::actions!(
AcceptPartialInlineCompletion,
AddSelectionAbove,
AddSelectionBelow,
ApplyAllDiffHunks,
ApplyDiffHunk,
Backspace,
Cancel,

View File

@@ -8,7 +8,7 @@
//! of several smaller structures that form a hierarchy (starting at the bottom):
//! - [`InlayMap`] that decides where the [`Inlay`]s should be displayed.
//! - [`FoldMap`] that decides where the fold indicators should be; it also tracks parts of a source file that are currently folded.
//! - [`CharMap`] that replaces tabs and non-printable characters
//! - [`TabMap`] that keeps track of hard tabs in a buffer.
//! - [`WrapMap`] that handles soft wrapping.
//! - [`BlockMap`] that tracks custom blocks such as diagnostics that should be displayed within buffer.
//! - [`DisplayMap`] that adds background highlights to the regions of text.
@@ -18,11 +18,11 @@
//! [EditorElement]: crate::element::EditorElement
mod block_map;
mod char_map;
mod crease_map;
mod fold_map;
mod inlay_map;
mod invisibles;
pub(crate) mod invisibles;
mod tab_map;
mod wrap_map;
use crate::{
@@ -33,7 +33,6 @@ pub use block_map::{
BlockPlacement, BlockPoint, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
};
use block_map::{BlockRow, BlockSnapshot};
use char_map::{CharMap, CharSnapshot};
use collections::{HashMap, HashSet};
pub use crease_map::*;
pub use fold_map::{Fold, FoldId, FoldPlaceholder, FoldPoint};
@@ -44,7 +43,7 @@ use gpui::{
pub(crate) use inlay_map::Inlay;
use inlay_map::{InlayMap, InlaySnapshot};
pub use inlay_map::{InlayOffset, InlayPoint};
pub use invisibles::is_invisible;
use invisibles::{is_invisible, replacement};
use language::{
language_settings::language_settings, ChunkRenderer, OffsetUtf16, Point,
Subscription as BufferSubscription,
@@ -59,13 +58,15 @@ use std::{
any::TypeId,
borrow::Cow,
fmt::Debug,
iter,
num::NonZeroU32,
ops::{Add, Range, Sub},
sync::Arc,
};
use sum_tree::{Bias, TreeMap};
use tab_map::{TabMap, TabSnapshot};
use text::LineIndent;
use ui::{px, WindowContext};
use ui::{div, px, IntoElement, ParentElement, SharedString, Styled, WindowContext};
use unicode_segmentation::UnicodeSegmentation;
use wrap_map::{WrapMap, WrapSnapshot};
@@ -97,7 +98,7 @@ pub struct DisplayMap {
/// Decides where the fold indicators should be and tracks parts of a source file that are currently folded.
fold_map: FoldMap,
/// Keeps track of hard tabs in a buffer.
char_map: CharMap,
tab_map: TabMap,
/// Handles soft wrapping.
wrap_map: Model<WrapMap>,
/// Tracks custom blocks such as diagnostics that should be displayed within buffer.
@@ -134,7 +135,7 @@ impl DisplayMap {
let crease_map = CreaseMap::new(&buffer_snapshot);
let (inlay_map, snapshot) = InlayMap::new(buffer_snapshot);
let (fold_map, snapshot) = FoldMap::new(snapshot);
let (char_map, snapshot) = CharMap::new(snapshot, tab_size);
let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
let (wrap_map, snapshot) = WrapMap::new(snapshot, font, font_size, wrap_width, cx);
let block_map = BlockMap::new(
snapshot,
@@ -151,7 +152,7 @@ impl DisplayMap {
buffer_subscription,
fold_map,
inlay_map,
char_map,
tab_map,
wrap_map,
block_map,
crease_map,
@@ -169,17 +170,17 @@ impl DisplayMap {
let (inlay_snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits);
let (fold_snapshot, edits) = self.fold_map.read(inlay_snapshot.clone(), edits);
let tab_size = Self::tab_size(&self.buffer, cx);
let (char_snapshot, edits) = self.char_map.sync(fold_snapshot.clone(), edits, tab_size);
let (tab_snapshot, edits) = self.tab_map.sync(fold_snapshot.clone(), edits, tab_size);
let (wrap_snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(char_snapshot.clone(), edits, cx));
.update(cx, |map, cx| map.sync(tab_snapshot.clone(), edits, cx));
let block_snapshot = self.block_map.read(wrap_snapshot.clone(), edits).snapshot;
DisplaySnapshot {
buffer_snapshot: self.buffer.read(cx).snapshot(cx),
fold_snapshot,
inlay_snapshot,
char_snapshot,
tab_snapshot,
wrap_snapshot,
block_snapshot,
crease_snapshot: self.crease_map.snapshot(),
@@ -215,13 +216,13 @@ impl DisplayMap {
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.char_map.sync(snapshot, edits, tab_size);
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.fold(ranges);
let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
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));
@@ -239,13 +240,13 @@ impl DisplayMap {
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.char_map.sync(snapshot, edits, tab_size);
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(ranges, inclusive);
let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
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));
@@ -280,7 +281,7 @@ impl DisplayMap {
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
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));
@@ -298,7 +299,7 @@ impl DisplayMap {
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
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));
@@ -316,7 +317,7 @@ impl DisplayMap {
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
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));
@@ -334,7 +335,7 @@ impl DisplayMap {
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
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));
@@ -410,7 +411,7 @@ impl DisplayMap {
let (snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
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));
@@ -418,7 +419,7 @@ impl DisplayMap {
let (snapshot, edits) = self.inlay_map.splice(to_remove, to_insert);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.char_map.sync(snapshot, edits, tab_size);
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));
@@ -464,13 +465,105 @@ pub struct HighlightedChunk<'a> {
pub renderer: Option<ChunkRenderer>,
}
impl<'a> HighlightedChunk<'a> {
fn highlight_invisibles(
self,
editor_style: &'a EditorStyle,
) -> impl Iterator<Item = Self> + 'a {
let mut chars = self.text.chars().peekable();
let mut text = self.text;
let style = self.style;
let is_tab = self.is_tab;
let renderer = self.renderer;
iter::from_fn(move || {
let mut prefix_len = 0;
while let Some(&ch) = chars.peek() {
if !is_invisible(ch) {
prefix_len += ch.len_utf8();
chars.next();
continue;
}
if prefix_len > 0 {
let (prefix, suffix) = text.split_at(prefix_len);
text = suffix;
return Some(HighlightedChunk {
text: prefix,
style,
is_tab,
renderer: renderer.clone(),
});
}
chars.next();
let (prefix, suffix) = text.split_at(ch.len_utf8());
text = suffix;
if let Some(replacement) = replacement(ch) {
let background = editor_style.status.hint_background;
let underline = editor_style.status.hint;
return Some(HighlightedChunk {
text: prefix,
style: None,
is_tab: false,
renderer: Some(ChunkRenderer {
render: Arc::new(move |_| {
div()
.child(replacement)
.bg(background)
.text_decoration_1()
.text_decoration_color(underline)
.into_any_element()
}),
constrain_width: false,
}),
});
} else {
let invisible_highlight = HighlightStyle {
background_color: Some(editor_style.status.hint_background),
underline: Some(UnderlineStyle {
color: Some(editor_style.status.hint),
thickness: px(1.),
wavy: false,
}),
..Default::default()
};
let invisible_style = if let Some(mut style) = style {
style.highlight(invisible_highlight);
style
} else {
invisible_highlight
};
return Some(HighlightedChunk {
text: prefix,
style: Some(invisible_style),
is_tab: false,
renderer: renderer.clone(),
});
}
}
if !text.is_empty() {
let remainder = text;
text = "";
Some(HighlightedChunk {
text: remainder,
style,
is_tab,
renderer: renderer.clone(),
})
} else {
None
}
})
}
}
#[derive(Clone)]
pub struct DisplaySnapshot {
pub buffer_snapshot: MultiBufferSnapshot,
pub fold_snapshot: FoldSnapshot,
pub crease_snapshot: CreaseSnapshot,
inlay_snapshot: InlaySnapshot,
char_snapshot: CharSnapshot,
tab_snapshot: TabSnapshot,
wrap_snapshot: WrapSnapshot,
block_snapshot: BlockSnapshot,
text_highlights: TextHighlights,
@@ -570,8 +663,8 @@ impl DisplaySnapshot {
fn point_to_display_point(&self, point: MultiBufferPoint, bias: Bias) -> DisplayPoint {
let inlay_point = self.inlay_snapshot.to_inlay_point(point);
let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
let char_point = self.char_snapshot.to_char_point(fold_point);
let wrap_point = self.wrap_snapshot.char_point_to_wrap_point(char_point);
let tab_point = self.tab_snapshot.to_tab_point(fold_point);
let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point);
let block_point = self.block_snapshot.to_block_point(wrap_point);
DisplayPoint(block_point)
}
@@ -599,21 +692,21 @@ impl DisplaySnapshot {
fn display_point_to_inlay_point(&self, point: DisplayPoint, bias: Bias) -> InlayPoint {
let block_point = point.0;
let wrap_point = self.block_snapshot.to_wrap_point(block_point);
let char_point = self.wrap_snapshot.to_char_point(wrap_point);
let fold_point = self.char_snapshot.to_fold_point(char_point, bias).0;
let tab_point = self.wrap_snapshot.to_tab_point(wrap_point);
let fold_point = self.tab_snapshot.to_fold_point(tab_point, bias).0;
fold_point.to_inlay_point(&self.fold_snapshot)
}
pub fn display_point_to_fold_point(&self, point: DisplayPoint, bias: Bias) -> FoldPoint {
let block_point = point.0;
let wrap_point = self.block_snapshot.to_wrap_point(block_point);
let char_point = self.wrap_snapshot.to_char_point(wrap_point);
self.char_snapshot.to_fold_point(char_point, bias).0
let tab_point = self.wrap_snapshot.to_tab_point(wrap_point);
self.tab_snapshot.to_fold_point(tab_point, bias).0
}
pub fn fold_point_to_display_point(&self, fold_point: FoldPoint) -> DisplayPoint {
let char_point = self.char_snapshot.to_char_point(fold_point);
let wrap_point = self.wrap_snapshot.char_point_to_wrap_point(char_point);
let tab_point = self.tab_snapshot.to_tab_point(fold_point);
let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point);
let block_point = self.block_snapshot.to_block_point(wrap_point);
DisplayPoint(block_point)
}
@@ -678,7 +771,7 @@ impl DisplaySnapshot {
suggestion: Some(editor_style.suggestions_style),
},
)
.map(|chunk| {
.flat_map(|chunk| {
let mut highlight_style = chunk
.syntax_highlight_id
.and_then(|id| id.style(&editor_style.syntax));
@@ -691,23 +784,6 @@ impl DisplaySnapshot {
}
}
if chunk.is_invisible {
let invisible_highlight = HighlightStyle {
background_color: Some(editor_style.status.hint_background),
underline: Some(UnderlineStyle {
color: Some(editor_style.status.hint),
thickness: px(1.),
wavy: false,
}),
..Default::default()
};
if let Some(highlight_style) = highlight_style.as_mut() {
highlight_style.highlight(invisible_highlight);
} else {
highlight_style = Some(invisible_highlight);
}
}
let mut diagnostic_highlight = HighlightStyle::default();
if chunk.is_unnecessary {
@@ -738,6 +814,7 @@ impl DisplaySnapshot {
is_tab: chunk.is_tab,
renderer: chunk.renderer,
}
.highlight_invisibles(editor_style)
})
}
@@ -804,9 +881,8 @@ impl DisplaySnapshot {
layout_line.closest_index_for_x(x) as u32
}
pub fn grapheme_at(&self, mut point: DisplayPoint) -> Option<String> {
pub fn grapheme_at(&self, mut point: DisplayPoint) -> Option<SharedString> {
point = DisplayPoint(self.block_snapshot.clip_point(point.0, Bias::Left));
let chars = self
.text_chunks(point.row())
.flat_map(str::chars)
@@ -827,12 +903,15 @@ impl DisplaySnapshot {
!end
}
});
chars
.collect::<String>()
.graphemes(true)
.next()
.map(|s| s.to_owned())
chars.collect::<String>().graphemes(true).next().map(|s| {
if let Some(invisible) = s.chars().next().filter(|&c| is_invisible(c)) {
replacement(invisible).unwrap_or(s).to_owned().into()
} else if s == "\n" {
" ".into()
} else {
s.to_owned().into()
}
})
}
pub fn buffer_chars_at(&self, mut offset: usize) -> impl Iterator<Item = (char, usize)> + '_ {
@@ -863,6 +942,14 @@ impl DisplaySnapshot {
DisplayPoint(clipped)
}
pub fn clip_point_2(&self, point: DisplayPoint, bias: Bias, skip_blocks: bool) -> DisplayPoint {
let mut clipped = self.block_snapshot.clip_point_2(point.0, bias, skip_blocks);
if self.clip_at_line_ends {
clipped = self.clip_at_line_end(DisplayPoint(clipped)).0
}
DisplayPoint(clipped)
}
pub fn clip_ignoring_line_ends(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint {
DisplayPoint(self.block_snapshot.clip_point(point.0, bias))
}
@@ -1144,8 +1231,8 @@ impl DisplayPoint {
pub fn to_offset(self, map: &DisplaySnapshot, bias: Bias) -> usize {
let wrap_point = map.block_snapshot.to_wrap_point(self.0);
let char_point = map.wrap_snapshot.to_char_point(wrap_point);
let fold_point = map.char_snapshot.to_fold_point(char_point, bias).0;
let tab_point = map.wrap_snapshot.to_tab_point(wrap_point);
let fold_point = map.tab_snapshot.to_fold_point(tab_point, bias).0;
let inlay_point = fold_point.to_inlay_point(&map.fold_snapshot);
map.inlay_snapshot
.to_buffer_offset(map.inlay_snapshot.to_offset(inlay_point))
@@ -1181,17 +1268,23 @@ pub mod tests {
use super::*;
use crate::{movement, test::marked_display_snapshot};
use block_map::BlockPlacement;
use gpui::{div, font, observe, px, AppContext, BorrowAppContext, Context, Element, Hsla};
use gpui::{
div, font, observe, px, AppContext, BorrowAppContext, Context, Element, Hsla, Rgba,
};
use language::{
language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
Buffer, Language, LanguageConfig, LanguageMatcher,
Buffer, Diagnostic, DiagnosticEntry, DiagnosticSet, Language, LanguageConfig,
LanguageMatcher,
};
use lsp::LanguageServerId;
use project::Project;
use rand::{prelude::*, Rng};
use settings::SettingsStore;
use smol::stream::StreamExt;
use std::{env, sync::Arc};
use text::PointUtf16;
use theme::{LoadThemes, SyntaxTheme};
use unindent::Unindent as _;
use util::test::{marked_text_ranges, sample_text};
use Bias::*;
@@ -1253,7 +1346,7 @@ pub mod tests {
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text());
log::info!("fold text: {:?}", snapshot.fold_snapshot.text());
log::info!("char text: {:?}", snapshot.char_snapshot.text());
log::info!("tab text: {:?}", snapshot.tab_snapshot.text());
log::info!("wrap text: {:?}", snapshot.wrap_snapshot.text());
log::info!("block text: {:?}", snapshot.block_snapshot.text());
log::info!("display text: {:?}", snapshot.text());
@@ -1368,7 +1461,7 @@ pub mod tests {
fold_count = snapshot.fold_count();
log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text());
log::info!("fold text: {:?}", snapshot.fold_snapshot.text());
log::info!("char text: {:?}", snapshot.char_snapshot.text());
log::info!("tab text: {:?}", snapshot.tab_snapshot.text());
log::info!("wrap text: {:?}", snapshot.wrap_snapshot.text());
log::info!("block text: {:?}", snapshot.block_snapshot.text());
log::info!("display text: {:?}", snapshot.text());
@@ -1648,8 +1741,6 @@ pub mod tests {
#[gpui::test]
async fn test_chunks(cx: &mut gpui::TestAppContext) {
use unindent::Unindent as _;
let text = r#"
fn outer() {}
@@ -1746,12 +1837,229 @@ pub mod tests {
);
}
#[gpui::test]
async fn test_chunks_with_syntax_highlighting_across_blocks(cx: &mut gpui::TestAppContext) {
cx.background_executor
.set_block_on_ticks(usize::MAX..=usize::MAX);
let text = r#"
const A: &str = "
one
two
three
";
const B: &str = "four";
"#
.unindent();
let theme = SyntaxTheme::new_test(vec![
("string", Hsla::red()),
("punctuation", Hsla::blue()),
("keyword", Hsla::green()),
]);
let language = Arc::new(
Language::new(
LanguageConfig {
name: "Rust".into(),
..Default::default()
},
Some(tree_sitter_rust::LANGUAGE.into()),
)
.with_highlights_query(
r#"
(string_literal) @string
"const" @keyword
[":" ";"] @punctuation
"#,
)
.unwrap(),
);
language.set_theme(&theme);
cx.update(|cx| init_test(cx, |_| {}));
let buffer = cx.new_model(|cx| Buffer::local(text, cx).with_language(language, cx));
cx.condition(&buffer, |buf, _| !buf.is_parsing()).await;
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
let map = cx.new_model(|cx| {
DisplayMap::new(
buffer,
font("Courier"),
px(16.0),
None,
true,
1,
1,
0,
FoldPlaceholder::test(),
cx,
)
});
// Insert a block in the middle of a multi-line string literal
map.update(cx, |map, cx| {
map.insert_blocks(
[BlockProperties {
placement: BlockPlacement::Below(
buffer_snapshot.anchor_before(Point::new(1, 0)),
),
height: 1,
style: BlockStyle::Sticky,
render: Box::new(|_| div().into_any()),
priority: 0,
}],
cx,
)
});
pretty_assertions::assert_eq!(
cx.update(|cx| syntax_chunks(DisplayRow(0)..DisplayRow(7), &map, &theme, cx)),
[
("const".into(), Some(Hsla::green())),
(" A".into(), None),
(":".into(), Some(Hsla::blue())),
(" &str = ".into(), None),
("\"\n one\n".into(), Some(Hsla::red())),
("\n".into(), None),
(" two\n three\n\"".into(), Some(Hsla::red())),
(";".into(), Some(Hsla::blue())),
("\n".into(), None),
("const".into(), Some(Hsla::green())),
(" B".into(), None),
(":".into(), Some(Hsla::blue())),
(" &str = ".into(), None),
("\"four\"".into(), Some(Hsla::red())),
(";".into(), Some(Hsla::blue())),
("\n".into(), None),
]
);
}
#[gpui::test]
async fn test_chunks_with_diagnostics_across_blocks(cx: &mut gpui::TestAppContext) {
cx.background_executor
.set_block_on_ticks(usize::MAX..=usize::MAX);
let text = r#"
struct A {
b: usize;
}
const c: usize = 1;
"#
.unindent();
cx.update(|cx| init_test(cx, |_| {}));
let buffer = cx.new_model(|cx| Buffer::local(text, cx));
buffer.update(cx, |buffer, cx| {
buffer.update_diagnostics(
LanguageServerId(0),
DiagnosticSet::new(
[DiagnosticEntry {
range: PointUtf16::new(0, 0)..PointUtf16::new(2, 1),
diagnostic: Diagnostic {
severity: DiagnosticSeverity::ERROR,
group_id: 1,
message: "hi".into(),
..Default::default()
},
}],
buffer,
),
cx,
)
});
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
let map = cx.new_model(|cx| {
DisplayMap::new(
buffer,
font("Courier"),
px(16.0),
None,
true,
1,
1,
0,
FoldPlaceholder::test(),
cx,
)
});
let black = gpui::black().to_rgb();
let red = gpui::red().to_rgb();
// Insert a block in the middle of a multi-line diagnostic.
map.update(cx, |map, cx| {
map.highlight_text(
TypeId::of::<usize>(),
vec![
buffer_snapshot.anchor_before(Point::new(3, 9))
..buffer_snapshot.anchor_after(Point::new(3, 14)),
buffer_snapshot.anchor_before(Point::new(3, 17))
..buffer_snapshot.anchor_after(Point::new(3, 18)),
],
red.into(),
);
map.insert_blocks(
[BlockProperties {
placement: BlockPlacement::Below(
buffer_snapshot.anchor_before(Point::new(1, 0)),
),
height: 1,
style: BlockStyle::Sticky,
render: Box::new(|_| div().into_any()),
priority: 0,
}],
cx,
)
});
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
let mut chunks = Vec::<(String, Option<DiagnosticSeverity>, Rgba)>::new();
for chunk in snapshot.chunks(DisplayRow(0)..DisplayRow(5), true, Default::default()) {
let color = chunk
.highlight_style
.and_then(|style| style.color)
.map_or(black, |color| color.to_rgb());
if let Some((last_chunk, last_severity, last_color)) = chunks.last_mut() {
if *last_severity == chunk.diagnostic_severity && *last_color == color {
last_chunk.push_str(chunk.text);
continue;
}
}
chunks.push((chunk.text.to_string(), chunk.diagnostic_severity, color));
}
assert_eq!(
chunks,
[
(
"struct A {\n b: usize;\n".into(),
Some(DiagnosticSeverity::ERROR),
black
),
("\n".into(), None, black),
("}".into(), Some(DiagnosticSeverity::ERROR), black),
("\nconst c: ".into(), None, black),
("usize".into(), None, red),
(" = ".into(), None, black),
("1".into(), None, red),
(";\n".into(), None, black),
]
);
}
// todo(linux) fails due to pixel differences in text rendering
#[cfg(target_os = "macos")]
#[gpui::test]
async fn test_chunks_with_soft_wrapping(cx: &mut gpui::TestAppContext) {
use unindent::Unindent as _;
cx.background_executor
.set_block_on_ticks(usize::MAX..=usize::MAX);

View File

@@ -265,6 +265,7 @@ pub struct BlockContext<'a, 'b> {
pub em_width: Pixels,
pub line_height: Pixels,
pub block_id: BlockId,
pub selected: bool,
pub editor_style: &'b EditorStyle,
}
@@ -1297,6 +1298,68 @@ impl BlockSnapshot {
cursor.item().map_or(false, |t| t.block.is_some())
}
pub fn clip_point_2(&self, point: BlockPoint, bias: Bias, skip_blocks: bool) -> BlockPoint {
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());
cursor.seek(&BlockRow(point.row), Bias::Right, &());
let max_input_row = WrapRow(self.transforms.summary().input_rows);
let mut search_left =
(bias == Bias::Left && cursor.start().1 .0 > 0) || cursor.end(&()).1 == max_input_row;
let mut reversed = false;
loop {
if let Some(transform) = cursor.item() {
let (output_start_row, input_start_row) = cursor.start();
let (output_end_row, input_end_row) = cursor.end(&());
let output_start = Point::new(output_start_row.0, 0);
let output_end = Point::new(output_end_row.0, 0);
let input_start = Point::new(input_start_row.0, 0);
let input_end = Point::new(input_end_row.0, 0);
match transform.block.as_ref() {
Some(Block::Custom(block))
if matches!(block.placement, BlockPlacement::Replace(_)) =>
{
if bias == Bias::Left {
return BlockPoint(output_start);
} else {
return BlockPoint(Point::new(output_end.row - 1, 0));
}
}
None => {
let input_point = if point.row >= output_end_row.0 {
let line_len = self.wrap_snapshot.line_len(input_end_row.0 - 1);
self.wrap_snapshot
.clip_point(WrapPoint::new(input_end_row.0 - 1, line_len), bias)
} else {
let output_overshoot = point.0.saturating_sub(output_start);
self.wrap_snapshot
.clip_point(WrapPoint(input_start + output_overshoot), bias)
};
if (input_start..input_end).contains(&input_point.0) {
let input_overshoot = input_point.0.saturating_sub(input_start);
return BlockPoint(output_start + input_overshoot);
}
}
_ => {}
}
if search_left {
cursor.prev(&());
} else {
cursor.next(&());
}
} else if reversed {
return self.max_point();
} else {
reversed = true;
search_left = !search_left;
cursor.seek(&BlockRow(point.row), Bias::Right, &());
}
}
}
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, &());
@@ -1666,7 +1729,7 @@ fn offset_for_row(s: &str, target: u32) -> (u32, usize) {
mod tests {
use super::*;
use crate::display_map::{
char_map::CharMap, fold_map::FoldMap, inlay_map::InlayMap, wrap_map::WrapMap,
fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap, wrap_map::WrapMap,
};
use gpui::{div, font, px, AppContext, Context as _, Element};
use language::{Buffer, Capability};
@@ -1701,9 +1764,9 @@ mod tests {
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (mut char_map, char_snapshot) = CharMap::new(fold_snapshot, 1.try_into().unwrap());
let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap());
let (wrap_map, wraps_snapshot) =
cx.update(|cx| WrapMap::new(char_snapshot, font("Helvetica"), px(14.0), None, cx));
cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx));
let mut block_map = BlockMap::new(wraps_snapshot.clone(), true, 1, 1, 1);
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
@@ -1851,10 +1914,10 @@ mod tests {
let (inlay_snapshot, inlay_edits) =
inlay_map.sync(buffer_snapshot, subscription.consume().into_inner());
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
let (char_snapshot, tab_edits) =
char_map.sync(fold_snapshot, fold_edits, 4.try_into().unwrap());
let (tab_snapshot, tab_edits) =
tab_map.sync(fold_snapshot, fold_edits, 4.try_into().unwrap());
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(char_snapshot, tab_edits, cx)
wrap_map.sync(tab_snapshot, tab_edits, cx)
});
let snapshot = block_map.read(wraps_snapshot, wrap_edits);
assert_eq!(snapshot.text(), "aaa\n\nb!!!\n\n\nbb\nccc\nddd\n\n\n");
@@ -1914,9 +1977,8 @@ mod tests {
let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(multi_buffer_snapshot.clone());
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, char_snapshot) = CharMap::new(fold_snapshot, 4.try_into().unwrap());
let (_, wraps_snapshot) =
WrapMap::new(char_snapshot, font, font_size, Some(wrap_width), cx);
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
let (_, wraps_snapshot) = WrapMap::new(tab_snapshot, font, font_size, Some(wrap_width), cx);
let block_map = BlockMap::new(wraps_snapshot.clone(), true, 1, 1, 1);
let snapshot = block_map.read(wraps_snapshot, Default::default());
@@ -1953,9 +2015,9 @@ mod tests {
let _subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let (_inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_char_map, char_snapshot) = CharMap::new(fold_snapshot, 1.try_into().unwrap());
let (_tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap());
let (_wrap_map, wraps_snapshot) =
cx.update(|cx| WrapMap::new(char_snapshot, font("Helvetica"), px(14.0), None, cx));
cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx));
let mut block_map = BlockMap::new(wraps_snapshot.clone(), false, 1, 1, 0);
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
@@ -2055,15 +2117,9 @@ mod tests {
let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx));
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, char_snapshot) = CharMap::new(fold_snapshot, 4.try_into().unwrap());
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
let (_, wraps_snapshot) = cx.update(|cx| {
WrapMap::new(
char_snapshot,
font("Helvetica"),
px(14.0),
Some(px(60.)),
cx,
)
WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), Some(px(60.)), cx)
});
let mut block_map = BlockMap::new(wraps_snapshot.clone(), true, 1, 1, 0);
@@ -2106,7 +2162,7 @@ mod tests {
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
let tab_size = 1.try_into().unwrap();
let (mut tab_map, tab_snapshot) = CharMap::new(fold_snapshot, tab_size);
let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, tab_size);
let (wrap_map, wraps_snapshot) =
cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx));
let mut block_map = BlockMap::new(wraps_snapshot.clone(), false, 1, 1, 0);
@@ -2257,9 +2313,9 @@ mod tests {
let mut buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx));
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (mut char_map, char_snapshot) = CharMap::new(fold_snapshot, 4.try_into().unwrap());
let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
let (wrap_map, wraps_snapshot) = cx
.update(|cx| WrapMap::new(char_snapshot, font("Helvetica"), font_size, wrap_width, cx));
.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), font_size, wrap_width, cx));
let mut block_map = BlockMap::new(
wraps_snapshot,
true,
@@ -2321,10 +2377,10 @@ mod tests {
let (inlay_snapshot, inlay_edits) =
inlay_map.sync(buffer_snapshot.clone(), vec![]);
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
let (char_snapshot, tab_edits) =
char_map.sync(fold_snapshot, fold_edits, tab_size);
let (tab_snapshot, tab_edits) =
tab_map.sync(fold_snapshot, fold_edits, tab_size);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(char_snapshot, tab_edits, cx)
wrap_map.sync(tab_snapshot, tab_edits, cx)
});
let mut block_map = block_map.write(wraps_snapshot, wrap_edits);
block_map.insert(block_properties.iter().map(|props| BlockProperties {
@@ -2346,10 +2402,10 @@ mod tests {
let (inlay_snapshot, inlay_edits) =
inlay_map.sync(buffer_snapshot.clone(), vec![]);
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
let (char_snapshot, tab_edits) =
char_map.sync(fold_snapshot, fold_edits, tab_size);
let (tab_snapshot, tab_edits) =
tab_map.sync(fold_snapshot, fold_edits, tab_size);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(char_snapshot, tab_edits, cx)
wrap_map.sync(tab_snapshot, tab_edits, cx)
});
let mut block_map = block_map.write(wraps_snapshot, wrap_edits);
block_map.remove(block_ids_to_remove);
@@ -2369,9 +2425,9 @@ mod tests {
let (inlay_snapshot, inlay_edits) =
inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
let (char_snapshot, tab_edits) = char_map.sync(fold_snapshot, fold_edits, tab_size);
let (tab_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(char_snapshot, tab_edits, cx)
wrap_map.sync(tab_snapshot, tab_edits, cx)
});
let blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits);
assert_eq!(
@@ -2486,7 +2542,7 @@ mod tests {
.row as usize];
let soft_wrapped = wraps_snapshot
.to_char_point(WrapPoint::new(wrap_row, 0))
.to_tab_point(WrapPoint::new(wrap_row, 0))
.column()
> 0;
expected_buffer_rows.push(if soft_wrapped { None } else { buffer_row });

View File

@@ -255,6 +255,22 @@ impl<'a> InlayChunks<'a> {
self.buffer_chunk = None;
self.output_offset = new_range.start;
self.max_output_offset = new_range.end;
let mut highlight_endpoints = Vec::new();
if let Some(text_highlights) = self.highlights.text_highlights {
if !text_highlights.is_empty() {
self.snapshot.apply_text_highlights(
&mut self.transforms,
&new_range,
text_highlights,
&mut highlight_endpoints,
);
self.transforms.seek(&new_range.start, Bias::Right, &());
highlight_endpoints.sort();
}
}
self.highlight_endpoints = highlight_endpoints.into_iter().peekable();
self.active_highlights.clear();
}
pub fn offset(&self) -> InlayOffset {

View File

@@ -1,11 +1,7 @@
use std::sync::LazyLock;
use collections::HashMap;
// Invisibility in a Unicode context is not well defined, so we have to guess.
//
// We highlight all ASCII control codes, and unicode whitespace because they are likely
// confused with a normal space (U+0020).
// confused with an ASCII space in a programming context (U+0020).
//
// We also highlight the handful of blank non-space characters:
// U+2800 BRAILLE PATTERN BLANK - Category: So
@@ -38,62 +34,38 @@ pub fn is_invisible(c: char) -> bool {
if c <= '\u{1f}' {
c != '\t' && c != '\n' && c != '\r'
} else if c >= '\u{7f}' {
c <= '\u{9f}' || c.is_whitespace() || contains(c, &FORMAT) || contains(c, &OTHER)
c <= '\u{9f}'
|| (c.is_whitespace() && c != IDEOGRAPHIC_SPACE)
|| contains(c, &FORMAT)
|| contains(c, &OTHER)
} else {
false
}
}
// ASCII control characters have fancy unicode glyphs, everything else
// is replaced by a space - unless it is used in combining characters in
// which case we need to leave it in the string.
pub(crate) fn replacement(c: char) -> Option<&'static str> {
if !is_invisible(c) {
return None;
}
if c <= '\x7f' {
REPLACEMENTS.get(&c).copied()
if c <= '\x1f' {
Some(C0_SYMBOLS[c as usize])
} else if c == '\x7f' {
Some(DEL)
} else if contains(c, &PRESERVE) {
None
} else {
Some(" ")
Some("\u{2007}") // fixed width space
}
}
// IDEOGRAPHIC SPACE is common alongside Chinese and other wide character sets.
// We don't highlight this for now (as it already shows up wide in the editor),
// but could if we tracked state in the classifier.
const IDEOGRAPHIC_SPACE: char = '\u{3000}';
const REPLACEMENTS: LazyLock<HashMap<char, &'static str>> = LazyLock::new(|| {
[
('\x00', ""),
('\x01', ""),
('\x02', ""),
('\x03', ""),
('\x04', ""),
('\x05', ""),
('\x06', ""),
('\x07', ""),
('\x08', ""),
('\x0B', ""),
('\x0C', ""),
('\x0D', ""),
('\x0E', ""),
('\x0F', ""),
('\x10', ""),
('\x11', ""),
('\x12', ""),
('\x13', ""),
('\x14', ""),
('\x15', ""),
('\x16', ""),
('\x17', ""),
('\x18', ""),
('\x19', ""),
('\x1A', ""),
('\x1B', ""),
('\x1C', ""),
('\x1D', ""),
('\x1E', ""),
('\x1F', ""),
('\u{007F}', ""),
]
.into_iter()
.collect()
});
const C0_SYMBOLS: &'static [&'static str] = &[
"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "",
"", "", "", "", "", "", "", "", "", "", "", "", "",
];
const DEL: &'static str = "";
// generated using ucd-generate: ucd-generate general-category --include Format --chars ucd-16.0.0
pub const FORMAT: &'static [(char, char)] = &[

View File

@@ -1,6 +1,5 @@
use super::{
fold_map::{self, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot},
invisibles::{is_invisible, replacement},
Highlights,
};
use language::{Chunk, Point};
@@ -10,14 +9,14 @@ use sum_tree::Bias;
const MAX_EXPANSION_COLUMN: u32 = 256;
/// Keeps track of hard tabs and non-printable characters in a text buffer.
/// Keeps track of hard tabs in a text buffer.
///
/// See the [`display_map` module documentation](crate::display_map) for more information.
pub struct CharMap(CharSnapshot);
pub struct TabMap(TabSnapshot);
impl CharMap {
pub fn new(fold_snapshot: FoldSnapshot, tab_size: NonZeroU32) -> (Self, CharSnapshot) {
let snapshot = CharSnapshot {
impl TabMap {
pub fn new(fold_snapshot: FoldSnapshot, tab_size: NonZeroU32) -> (Self, TabSnapshot) {
let snapshot = TabSnapshot {
fold_snapshot,
tab_size,
max_expansion_column: MAX_EXPANSION_COLUMN,
@@ -27,7 +26,7 @@ impl CharMap {
}
#[cfg(test)]
pub fn set_max_expansion_column(&mut self, column: u32) -> CharSnapshot {
pub fn set_max_expansion_column(&mut self, column: u32) -> TabSnapshot {
self.0.max_expansion_column = column;
self.0.clone()
}
@@ -37,9 +36,9 @@ impl CharMap {
fold_snapshot: FoldSnapshot,
mut fold_edits: Vec<FoldEdit>,
tab_size: NonZeroU32,
) -> (CharSnapshot, Vec<TabEdit>) {
) -> (TabSnapshot, Vec<TabEdit>) {
let old_snapshot = &mut self.0;
let mut new_snapshot = CharSnapshot {
let mut new_snapshot = TabSnapshot {
fold_snapshot,
tab_size,
max_expansion_column: old_snapshot.max_expansion_column,
@@ -138,15 +137,15 @@ impl CharMap {
let new_start = fold_edit.new.start.to_point(&new_snapshot.fold_snapshot);
let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot);
tab_edits.push(TabEdit {
old: old_snapshot.to_char_point(old_start)..old_snapshot.to_char_point(old_end),
new: new_snapshot.to_char_point(new_start)..new_snapshot.to_char_point(new_end),
old: old_snapshot.to_tab_point(old_start)..old_snapshot.to_tab_point(old_end),
new: new_snapshot.to_tab_point(new_start)..new_snapshot.to_tab_point(new_end),
});
}
} else {
new_snapshot.version += 1;
tab_edits.push(TabEdit {
old: CharPoint::zero()..old_snapshot.max_point(),
new: CharPoint::zero()..new_snapshot.max_point(),
old: TabPoint::zero()..old_snapshot.max_point(),
new: TabPoint::zero()..new_snapshot.max_point(),
});
}
@@ -156,14 +155,14 @@ impl CharMap {
}
#[derive(Clone)]
pub struct CharSnapshot {
pub struct TabSnapshot {
pub fold_snapshot: FoldSnapshot,
pub tab_size: NonZeroU32,
pub max_expansion_column: u32,
pub version: usize,
}
impl CharSnapshot {
impl TabSnapshot {
pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
&self.fold_snapshot.inlay_snapshot.buffer
}
@@ -171,7 +170,7 @@ impl CharSnapshot {
pub fn line_len(&self, row: u32) -> u32 {
let max_point = self.max_point();
if row < max_point.row() {
self.to_char_point(FoldPoint::new(row, self.fold_snapshot.line_len(row)))
self.to_tab_point(FoldPoint::new(row, self.fold_snapshot.line_len(row)))
.0
.column
} else {
@@ -180,10 +179,10 @@ impl CharSnapshot {
}
pub fn text_summary(&self) -> TextSummary {
self.text_summary_for_range(CharPoint::zero()..self.max_point())
self.text_summary_for_range(TabPoint::zero()..self.max_point())
}
pub fn text_summary_for_range(&self, range: Range<CharPoint>) -> TextSummary {
pub fn text_summary_for_range(&self, range: Range<TabPoint>) -> TextSummary {
let input_start = self.to_fold_point(range.start, Bias::Left).0;
let input_end = self.to_fold_point(range.end, Bias::Right).0;
let input_summary = self
@@ -212,7 +211,7 @@ impl CharSnapshot {
} else {
for _ in self
.chunks(
CharPoint::new(range.end.row(), 0)..range.end,
TabPoint::new(range.end.row(), 0)..range.end,
false,
Highlights::default(),
)
@@ -233,7 +232,7 @@ impl CharSnapshot {
pub fn chunks<'a>(
&'a self,
range: Range<CharPoint>,
range: Range<TabPoint>,
language_aware: bool,
highlights: Highlights<'a>,
) -> TabChunks<'a> {
@@ -280,7 +279,7 @@ impl CharSnapshot {
#[cfg(test)]
pub fn text(&self) -> String {
self.chunks(
CharPoint::zero()..self.max_point(),
TabPoint::zero()..self.max_point(),
false,
Highlights::default(),
)
@@ -288,24 +287,24 @@ impl CharSnapshot {
.collect()
}
pub fn max_point(&self) -> CharPoint {
self.to_char_point(self.fold_snapshot.max_point())
pub fn max_point(&self) -> TabPoint {
self.to_tab_point(self.fold_snapshot.max_point())
}
pub fn clip_point(&self, point: CharPoint, bias: Bias) -> CharPoint {
self.to_char_point(
pub fn clip_point(&self, point: TabPoint, bias: Bias) -> TabPoint {
self.to_tab_point(
self.fold_snapshot
.clip_point(self.to_fold_point(point, bias).0, bias),
)
}
pub fn to_char_point(&self, input: FoldPoint) -> CharPoint {
pub fn to_tab_point(&self, input: FoldPoint) -> TabPoint {
let chars = self.fold_snapshot.chars_at(FoldPoint::new(input.row(), 0));
let expanded = self.expand_tabs(chars, input.column());
CharPoint::new(input.row(), expanded)
TabPoint::new(input.row(), expanded)
}
pub fn to_fold_point(&self, output: CharPoint, bias: Bias) -> (FoldPoint, u32, u32) {
pub fn to_fold_point(&self, output: TabPoint, bias: Bias) -> (FoldPoint, u32, u32) {
let chars = self.fold_snapshot.chars_at(FoldPoint::new(output.row(), 0));
let expanded = output.column();
let (collapsed, expanded_char_column, to_next_stop) =
@@ -317,13 +316,13 @@ impl CharSnapshot {
)
}
pub fn make_char_point(&self, point: Point, bias: Bias) -> CharPoint {
pub fn make_tab_point(&self, point: Point, bias: Bias) -> TabPoint {
let inlay_point = self.fold_snapshot.inlay_snapshot.to_inlay_point(point);
let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
self.to_char_point(fold_point)
self.to_tab_point(fold_point)
}
pub fn to_point(&self, point: CharPoint, bias: Bias) -> Point {
pub fn to_point(&self, point: TabPoint, bias: Bias) -> Point {
let fold_point = self.to_fold_point(point, bias).0;
let inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
self.fold_snapshot
@@ -346,9 +345,6 @@ impl CharSnapshot {
let tab_len = tab_size - expanded_chars % tab_size;
expanded_bytes += tab_len;
expanded_chars += tab_len;
} else if let Some(replacement) = replacement(c) {
expanded_chars += replacement.chars().count() as u32;
expanded_bytes += replacement.len() as u32;
} else {
expanded_bytes += c.len_utf8() as u32;
expanded_chars += 1;
@@ -388,9 +384,6 @@ impl CharSnapshot {
Bias::Right => (collapsed_bytes + 1, expanded_chars, 0),
};
}
} else if let Some(replacement) = replacement(c) {
expanded_chars += replacement.chars().count() as u32;
expanded_bytes += replacement.len() as u32;
} else {
expanded_chars += 1;
expanded_bytes += c.len_utf8() as u32;
@@ -412,9 +405,9 @@ impl CharSnapshot {
}
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
pub struct CharPoint(pub Point);
pub struct TabPoint(pub Point);
impl CharPoint {
impl TabPoint {
pub fn new(row: u32, column: u32) -> Self {
Self(Point::new(row, column))
}
@@ -432,13 +425,13 @@ impl CharPoint {
}
}
impl From<Point> for CharPoint {
impl From<Point> for TabPoint {
fn from(point: Point) -> Self {
Self(point)
}
}
pub type TabEdit = text::Edit<CharPoint>;
pub type TabEdit = text::Edit<TabPoint>;
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct TextSummary {
@@ -493,7 +486,7 @@ impl<'a> std::ops::AddAssign<&'a Self> for TextSummary {
const SPACES: &str = " ";
pub struct TabChunks<'a> {
snapshot: &'a CharSnapshot,
snapshot: &'a TabSnapshot,
fold_chunks: FoldChunks<'a>,
chunk: Chunk<'a>,
column: u32,
@@ -506,7 +499,7 @@ pub struct TabChunks<'a> {
}
impl<'a> TabChunks<'a> {
pub(crate) fn seek(&mut self, range: Range<CharPoint>) {
pub(crate) fn seek(&mut self, range: Range<TabPoint>) {
let (input_start, expanded_char_column, to_next_stop) =
self.snapshot.to_fold_point(range.start, Bias::Left);
let input_column = input_start.column();
@@ -591,37 +584,6 @@ impl<'a> Iterator for TabChunks<'a> {
self.input_column = 0;
self.output_position += Point::new(1, 0);
}
_ if is_invisible(c) => {
if ix > 0 {
let (prefix, suffix) = self.chunk.text.split_at(ix);
self.chunk.text = suffix;
return Some(Chunk {
text: prefix,
is_invisible: false,
..self.chunk.clone()
});
}
let c_len = c.len_utf8();
let replacement = replacement(c).unwrap_or(&self.chunk.text[..c_len]);
if self.chunk.text.len() >= c_len {
self.chunk.text = &self.chunk.text[c_len..];
} else {
self.chunk.text = "";
}
let len = replacement.chars().count() as u32;
let next_output_position = cmp::min(
self.output_position + Point::new(0, len),
self.max_output_position,
);
self.column += len;
self.input_column += 1;
self.output_position = next_output_position;
return Some(Chunk {
text: replacement,
is_invisible: true,
..self.chunk.clone()
});
}
_ => {
self.column += 1;
if !self.inside_leading_tab {
@@ -651,11 +613,11 @@ mod tests {
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, char_snapshot) = CharMap::new(fold_snapshot, 4.try_into().unwrap());
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
assert_eq!(char_snapshot.expand_tabs("\t".chars(), 0), 0);
assert_eq!(char_snapshot.expand_tabs("\t".chars(), 1), 4);
assert_eq!(char_snapshot.expand_tabs("\ta".chars(), 2), 5);
assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 0), 0);
assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 1), 4);
assert_eq!(tab_snapshot.expand_tabs("\ta".chars(), 2), 5);
}
#[gpui::test]
@@ -668,16 +630,16 @@ mod tests {
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, mut char_snapshot) = CharMap::new(fold_snapshot, 4.try_into().unwrap());
let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
char_snapshot.max_expansion_column = max_expansion_column;
assert_eq!(char_snapshot.text(), output);
tab_snapshot.max_expansion_column = max_expansion_column;
assert_eq!(tab_snapshot.text(), output);
for (ix, c) in input.char_indices() {
assert_eq!(
char_snapshot
tab_snapshot
.chunks(
CharPoint::new(0, ix as u32)..char_snapshot.max_point(),
TabPoint::new(0, ix as u32)..tab_snapshot.max_point(),
false,
Highlights::default(),
)
@@ -691,13 +653,13 @@ mod tests {
let input_point = Point::new(0, ix as u32);
let output_point = Point::new(0, output.find(c).unwrap() as u32);
assert_eq!(
char_snapshot.to_char_point(FoldPoint(input_point)),
CharPoint(output_point),
"to_char_point({input_point:?})"
tab_snapshot.to_tab_point(FoldPoint(input_point)),
TabPoint(output_point),
"to_tab_point({input_point:?})"
);
assert_eq!(
char_snapshot
.to_fold_point(CharPoint(output_point), Bias::Left)
tab_snapshot
.to_fold_point(TabPoint(output_point), Bias::Left)
.0,
FoldPoint(input_point),
"to_fold_point({output_point:?})"
@@ -715,10 +677,10 @@ mod tests {
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, mut char_snapshot) = CharMap::new(fold_snapshot, 4.try_into().unwrap());
let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
char_snapshot.max_expansion_column = max_expansion_column;
assert_eq!(char_snapshot.text(), input);
tab_snapshot.max_expansion_column = max_expansion_column;
assert_eq!(tab_snapshot.text(), input);
}
#[gpui::test]
@@ -729,10 +691,10 @@ mod tests {
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, char_snapshot) = CharMap::new(fold_snapshot, 4.try_into().unwrap());
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
assert_eq!(
chunks(&char_snapshot, CharPoint::zero()),
chunks(&tab_snapshot, TabPoint::zero()),
vec![
(" ".to_string(), true),
(" ".to_string(), false),
@@ -741,7 +703,7 @@ mod tests {
]
);
assert_eq!(
chunks(&char_snapshot, CharPoint::new(0, 2)),
chunks(&tab_snapshot, TabPoint::new(0, 2)),
vec![
(" ".to_string(), true),
(" ".to_string(), false),
@@ -750,7 +712,7 @@ mod tests {
]
);
fn chunks(snapshot: &CharSnapshot, start: CharPoint) -> Vec<(String, bool)> {
fn chunks(snapshot: &TabSnapshot, start: TabPoint) -> Vec<(String, bool)> {
let mut chunks = Vec::new();
let mut was_tab = false;
let mut text = String::new();
@@ -796,12 +758,12 @@ mod tests {
let (inlay_snapshot, _) = inlay_map.randomly_mutate(&mut 0, &mut rng);
log::info!("InlayMap text: {:?}", inlay_snapshot.text());
let (mut char_map, _) = CharMap::new(fold_snapshot.clone(), tab_size);
let tabs_snapshot = char_map.set_max_expansion_column(32);
let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
let tabs_snapshot = tab_map.set_max_expansion_column(32);
let text = text::Rope::from(tabs_snapshot.text().as_str());
log::info!(
"CharMap text (tab size: {}): {:?}",
"TabMap text (tab size: {}): {:?}",
tab_size,
tabs_snapshot.text(),
);
@@ -809,11 +771,11 @@ mod tests {
for _ in 0..5 {
let end_row = rng.gen_range(0..=text.max_point().row);
let end_column = rng.gen_range(0..=text.line_len(end_row));
let mut end = CharPoint(text.clip_point(Point::new(end_row, end_column), Bias::Right));
let mut end = TabPoint(text.clip_point(Point::new(end_row, end_column), Bias::Right));
let start_row = rng.gen_range(0..=text.max_point().row);
let start_column = rng.gen_range(0..=text.line_len(start_row));
let mut start =
CharPoint(text.clip_point(Point::new(start_row, start_column), Bias::Left));
TabPoint(text.clip_point(Point::new(start_row, start_column), Bias::Left));
if start > end {
mem::swap(&mut start, &mut end);
}

View File

@@ -1,6 +1,6 @@
use super::{
char_map::{self, CharPoint, CharSnapshot, TabEdit},
fold_map::FoldBufferRows,
tab_map::{self, TabEdit, TabPoint, TabSnapshot},
Highlights,
};
use gpui::{AppContext, Context, Font, LineWrapper, Model, ModelContext, Pixels, Task};
@@ -12,7 +12,7 @@ use std::{cmp, collections::VecDeque, mem, ops::Range, time::Duration};
use sum_tree::{Bias, Cursor, SumTree};
use text::Patch;
pub use super::char_map::TextSummary;
pub use super::tab_map::TextSummary;
pub type WrapEdit = text::Edit<u32>;
/// Handles soft wrapping of text.
@@ -20,7 +20,7 @@ pub type WrapEdit = text::Edit<u32>;
/// See the [`display_map` module documentation](crate::display_map) for more information.
pub struct WrapMap {
snapshot: WrapSnapshot,
pending_edits: VecDeque<(CharSnapshot, Vec<TabEdit>)>,
pending_edits: VecDeque<(TabSnapshot, Vec<TabEdit>)>,
interpolated_edits: Patch<u32>,
edits_since_sync: Patch<u32>,
wrap_width: Option<Pixels>,
@@ -30,7 +30,7 @@ pub struct WrapMap {
#[derive(Clone)]
pub struct WrapSnapshot {
char_snapshot: CharSnapshot,
tab_snapshot: TabSnapshot,
transforms: SumTree<Transform>,
interpolated: bool,
}
@@ -51,11 +51,11 @@ struct TransformSummary {
pub struct WrapPoint(pub Point);
pub struct WrapChunks<'a> {
input_chunks: char_map::TabChunks<'a>,
input_chunks: tab_map::TabChunks<'a>,
input_chunk: Chunk<'a>,
output_position: WrapPoint,
max_output_row: u32,
transforms: Cursor<'a, Transform, (WrapPoint, CharPoint)>,
transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>,
snapshot: &'a WrapSnapshot,
}
@@ -66,7 +66,7 @@ pub struct WrapBufferRows<'a> {
output_row: u32,
soft_wrapped: bool,
max_output_row: u32,
transforms: Cursor<'a, Transform, (WrapPoint, CharPoint)>,
transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>,
}
impl<'a> WrapBufferRows<'a> {
@@ -86,7 +86,7 @@ impl<'a> WrapBufferRows<'a> {
impl WrapMap {
pub fn new(
char_snapshot: CharSnapshot,
tab_snapshot: TabSnapshot,
font: Font,
font_size: Pixels,
wrap_width: Option<Pixels>,
@@ -99,7 +99,7 @@ impl WrapMap {
pending_edits: Default::default(),
interpolated_edits: Default::default(),
edits_since_sync: Default::default(),
snapshot: WrapSnapshot::new(char_snapshot),
snapshot: WrapSnapshot::new(tab_snapshot),
background_task: None,
};
this.set_wrap_width(wrap_width, cx);
@@ -117,17 +117,17 @@ impl WrapMap {
pub fn sync(
&mut self,
char_snapshot: CharSnapshot,
tab_snapshot: TabSnapshot,
edits: Vec<TabEdit>,
cx: &mut ModelContext<Self>,
) -> (WrapSnapshot, Patch<u32>) {
if self.wrap_width.is_some() {
self.pending_edits.push_back((char_snapshot, edits));
self.pending_edits.push_back((tab_snapshot, edits));
self.flush_edits(cx);
} else {
self.edits_since_sync = self
.edits_since_sync
.compose(self.snapshot.interpolate(char_snapshot, &edits));
.compose(self.snapshot.interpolate(tab_snapshot, &edits));
self.snapshot.interpolated = false;
}
@@ -177,11 +177,11 @@ impl WrapMap {
let (font, font_size) = self.font_with_size.clone();
let task = cx.background_executor().spawn(async move {
let mut line_wrapper = text_system.line_wrapper(font, font_size);
let char_snapshot = new_snapshot.char_snapshot.clone();
let range = CharPoint::zero()..char_snapshot.max_point();
let tab_snapshot = new_snapshot.tab_snapshot.clone();
let range = TabPoint::zero()..tab_snapshot.max_point();
let edits = new_snapshot
.update(
char_snapshot,
tab_snapshot,
&[TabEdit {
old: range.clone(),
new: range.clone(),
@@ -221,7 +221,7 @@ impl WrapMap {
} else {
let old_rows = self.snapshot.transforms.summary().output.lines.row + 1;
self.snapshot.transforms = SumTree::default();
let summary = self.snapshot.char_snapshot.text_summary();
let summary = self.snapshot.tab_snapshot.text_summary();
if !summary.lines.is_zero() {
self.snapshot
.transforms
@@ -239,8 +239,8 @@ impl WrapMap {
fn flush_edits(&mut self, cx: &mut ModelContext<Self>) {
if !self.snapshot.interpolated {
let mut to_remove_len = 0;
for (char_snapshot, _) in &self.pending_edits {
if char_snapshot.version <= self.snapshot.char_snapshot.version {
for (tab_snapshot, _) in &self.pending_edits {
if tab_snapshot.version <= self.snapshot.tab_snapshot.version {
to_remove_len += 1;
} else {
break;
@@ -262,9 +262,9 @@ impl WrapMap {
let update_task = cx.background_executor().spawn(async move {
let mut edits = Patch::default();
let mut line_wrapper = text_system.line_wrapper(font, font_size);
for (char_snapshot, tab_edits) in pending_edits {
for (tab_snapshot, tab_edits) in pending_edits {
let wrap_edits = snapshot
.update(char_snapshot, &tab_edits, wrap_width, &mut line_wrapper)
.update(tab_snapshot, &tab_edits, wrap_width, &mut line_wrapper)
.await;
edits = edits.compose(&wrap_edits);
}
@@ -301,11 +301,11 @@ impl WrapMap {
let was_interpolated = self.snapshot.interpolated;
let mut to_remove_len = 0;
for (char_snapshot, edits) in &self.pending_edits {
if char_snapshot.version <= self.snapshot.char_snapshot.version {
for (tab_snapshot, edits) in &self.pending_edits {
if tab_snapshot.version <= self.snapshot.tab_snapshot.version {
to_remove_len += 1;
} else {
let interpolated_edits = self.snapshot.interpolate(char_snapshot.clone(), edits);
let interpolated_edits = self.snapshot.interpolate(tab_snapshot.clone(), edits);
self.edits_since_sync = self.edits_since_sync.compose(&interpolated_edits);
self.interpolated_edits = self.interpolated_edits.compose(&interpolated_edits);
}
@@ -318,49 +318,45 @@ impl WrapMap {
}
impl WrapSnapshot {
fn new(char_snapshot: CharSnapshot) -> Self {
fn new(tab_snapshot: TabSnapshot) -> Self {
let mut transforms = SumTree::default();
let extent = char_snapshot.text_summary();
let extent = tab_snapshot.text_summary();
if !extent.lines.is_zero() {
transforms.push(Transform::isomorphic(extent), &());
}
Self {
transforms,
char_snapshot,
tab_snapshot,
interpolated: true,
}
}
pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
self.char_snapshot.buffer_snapshot()
self.tab_snapshot.buffer_snapshot()
}
fn interpolate(
&mut self,
new_char_snapshot: CharSnapshot,
tab_edits: &[TabEdit],
) -> Patch<u32> {
fn interpolate(&mut self, new_tab_snapshot: TabSnapshot, tab_edits: &[TabEdit]) -> Patch<u32> {
let mut new_transforms;
if tab_edits.is_empty() {
new_transforms = self.transforms.clone();
} else {
let mut old_cursor = self.transforms.cursor::<CharPoint>(&());
let mut old_cursor = self.transforms.cursor::<TabPoint>(&());
let mut tab_edits_iter = tab_edits.iter().peekable();
new_transforms =
old_cursor.slice(&tab_edits_iter.peek().unwrap().old.start, Bias::Right, &());
while let Some(edit) = tab_edits_iter.next() {
if edit.new.start > CharPoint::from(new_transforms.summary().input.lines) {
let summary = new_char_snapshot.text_summary_for_range(
CharPoint::from(new_transforms.summary().input.lines)..edit.new.start,
if edit.new.start > TabPoint::from(new_transforms.summary().input.lines) {
let summary = new_tab_snapshot.text_summary_for_range(
TabPoint::from(new_transforms.summary().input.lines)..edit.new.start,
);
new_transforms.push_or_extend(Transform::isomorphic(summary));
}
if !edit.new.is_empty() {
new_transforms.push_or_extend(Transform::isomorphic(
new_char_snapshot.text_summary_for_range(edit.new.clone()),
new_tab_snapshot.text_summary_for_range(edit.new.clone()),
));
}
@@ -369,7 +365,7 @@ impl WrapSnapshot {
if next_edit.old.start > old_cursor.end(&()) {
if old_cursor.end(&()) > edit.old.end {
let summary = self
.char_snapshot
.tab_snapshot
.text_summary_for_range(edit.old.end..old_cursor.end(&()));
new_transforms.push_or_extend(Transform::isomorphic(summary));
}
@@ -383,7 +379,7 @@ impl WrapSnapshot {
} else {
if old_cursor.end(&()) > edit.old.end {
let summary = self
.char_snapshot
.tab_snapshot
.text_summary_for_range(edit.old.end..old_cursor.end(&()));
new_transforms.push_or_extend(Transform::isomorphic(summary));
}
@@ -396,7 +392,7 @@ impl WrapSnapshot {
let old_snapshot = mem::replace(
self,
WrapSnapshot {
char_snapshot: new_char_snapshot,
tab_snapshot: new_tab_snapshot,
transforms: new_transforms,
interpolated: true,
},
@@ -407,7 +403,7 @@ impl WrapSnapshot {
async fn update(
&mut self,
new_char_snapshot: CharSnapshot,
new_tab_snapshot: TabSnapshot,
tab_edits: &[TabEdit],
wrap_width: Pixels,
line_wrapper: &mut LineWrapper,
@@ -444,27 +440,27 @@ impl WrapSnapshot {
new_transforms = self.transforms.clone();
} else {
let mut row_edits = row_edits.into_iter().peekable();
let mut old_cursor = self.transforms.cursor::<CharPoint>(&());
let mut old_cursor = self.transforms.cursor::<TabPoint>(&());
new_transforms = old_cursor.slice(
&CharPoint::new(row_edits.peek().unwrap().old_rows.start, 0),
&TabPoint::new(row_edits.peek().unwrap().old_rows.start, 0),
Bias::Right,
&(),
);
while let Some(edit) = row_edits.next() {
if edit.new_rows.start > new_transforms.summary().input.lines.row {
let summary = new_char_snapshot.text_summary_for_range(
CharPoint(new_transforms.summary().input.lines)
..CharPoint::new(edit.new_rows.start, 0),
let summary = new_tab_snapshot.text_summary_for_range(
TabPoint(new_transforms.summary().input.lines)
..TabPoint::new(edit.new_rows.start, 0),
);
new_transforms.push_or_extend(Transform::isomorphic(summary));
}
let mut line = String::new();
let mut remaining = None;
let mut chunks = new_char_snapshot.chunks(
CharPoint::new(edit.new_rows.start, 0)..new_char_snapshot.max_point(),
let mut chunks = new_tab_snapshot.chunks(
TabPoint::new(edit.new_rows.start, 0)..new_tab_snapshot.max_point(),
false,
Highlights::default(),
);
@@ -511,19 +507,19 @@ impl WrapSnapshot {
}
new_transforms.extend(edit_transforms, &());
old_cursor.seek_forward(&CharPoint::new(edit.old_rows.end, 0), Bias::Right, &());
old_cursor.seek_forward(&TabPoint::new(edit.old_rows.end, 0), Bias::Right, &());
if let Some(next_edit) = row_edits.peek() {
if next_edit.old_rows.start > old_cursor.end(&()).row() {
if old_cursor.end(&()) > CharPoint::new(edit.old_rows.end, 0) {
let summary = self.char_snapshot.text_summary_for_range(
CharPoint::new(edit.old_rows.end, 0)..old_cursor.end(&()),
if old_cursor.end(&()) > TabPoint::new(edit.old_rows.end, 0) {
let summary = self.tab_snapshot.text_summary_for_range(
TabPoint::new(edit.old_rows.end, 0)..old_cursor.end(&()),
);
new_transforms.push_or_extend(Transform::isomorphic(summary));
}
old_cursor.next(&());
new_transforms.append(
old_cursor.slice(
&CharPoint::new(next_edit.old_rows.start, 0),
&TabPoint::new(next_edit.old_rows.start, 0),
Bias::Right,
&(),
),
@@ -531,9 +527,9 @@ impl WrapSnapshot {
);
}
} else {
if old_cursor.end(&()) > CharPoint::new(edit.old_rows.end, 0) {
let summary = self.char_snapshot.text_summary_for_range(
CharPoint::new(edit.old_rows.end, 0)..old_cursor.end(&()),
if old_cursor.end(&()) > TabPoint::new(edit.old_rows.end, 0) {
let summary = self.tab_snapshot.text_summary_for_range(
TabPoint::new(edit.old_rows.end, 0)..old_cursor.end(&()),
);
new_transforms.push_or_extend(Transform::isomorphic(summary));
}
@@ -546,7 +542,7 @@ impl WrapSnapshot {
let old_snapshot = mem::replace(
self,
WrapSnapshot {
char_snapshot: new_char_snapshot,
tab_snapshot: new_tab_snapshot,
transforms: new_transforms,
interpolated: false,
},
@@ -599,17 +595,17 @@ impl WrapSnapshot {
) -> WrapChunks<'a> {
let output_start = WrapPoint::new(rows.start, 0);
let output_end = WrapPoint::new(rows.end, 0);
let mut transforms = self.transforms.cursor::<(WrapPoint, CharPoint)>(&());
let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
transforms.seek(&output_start, Bias::Right, &());
let mut input_start = CharPoint(transforms.start().1 .0);
let mut input_start = TabPoint(transforms.start().1 .0);
if transforms.item().map_or(false, |t| t.is_isomorphic()) {
input_start.0 += output_start.0 - transforms.start().0 .0;
}
let input_end = self
.to_char_point(output_end)
.min(self.char_snapshot.max_point());
.to_tab_point(output_end)
.min(self.tab_snapshot.max_point());
WrapChunks {
input_chunks: self.char_snapshot.chunks(
input_chunks: self.tab_snapshot.chunks(
input_start..input_end,
language_aware,
highlights,
@@ -627,7 +623,7 @@ impl WrapSnapshot {
}
pub fn line_len(&self, row: u32) -> u32 {
let mut cursor = self.transforms.cursor::<(WrapPoint, CharPoint)>(&());
let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Left, &());
if cursor
.item()
@@ -635,7 +631,7 @@ impl WrapSnapshot {
{
let overshoot = row - cursor.start().0.row();
let tab_row = cursor.start().1.row() + overshoot;
let tab_line_len = self.char_snapshot.line_len(tab_row);
let tab_line_len = self.tab_snapshot.line_len(tab_row);
if overshoot == 0 {
cursor.start().0.column() + (tab_line_len - cursor.start().1.column())
} else {
@@ -652,17 +648,15 @@ impl WrapSnapshot {
let start = WrapPoint::new(rows.start, 0);
let end = WrapPoint::new(rows.end, 0);
let mut cursor = self.transforms.cursor::<(WrapPoint, CharPoint)>(&());
let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
cursor.seek(&start, Bias::Right, &());
if let Some(transform) = cursor.item() {
let start_in_transform = start.0 - cursor.start().0 .0;
let end_in_transform = cmp::min(end, cursor.end(&()).0).0 - cursor.start().0 .0;
if transform.is_isomorphic() {
let char_start = CharPoint(cursor.start().1 .0 + start_in_transform);
let char_end = CharPoint(cursor.start().1 .0 + end_in_transform);
summary += &self
.char_snapshot
.text_summary_for_range(char_start..char_end);
let tab_start = TabPoint(cursor.start().1 .0 + start_in_transform);
let tab_end = TabPoint(cursor.start().1 .0 + end_in_transform);
summary += &self.tab_snapshot.text_summary_for_range(tab_start..tab_end);
} else {
debug_assert_eq!(start_in_transform.row, end_in_transform.row);
let indent_len = end_in_transform.column - start_in_transform.column;
@@ -687,9 +681,9 @@ impl WrapSnapshot {
let end_in_transform = end.0 - cursor.start().0 .0;
if transform.is_isomorphic() {
let char_start = cursor.start().1;
let char_end = CharPoint(char_start.0 + end_in_transform);
let char_end = TabPoint(char_start.0 + end_in_transform);
summary += &self
.char_snapshot
.tab_snapshot
.text_summary_for_range(char_start..char_end);
} else {
debug_assert_eq!(end_in_transform, Point::new(1, 0));
@@ -724,14 +718,14 @@ impl WrapSnapshot {
}
pub fn buffer_rows(&self, start_row: u32) -> WrapBufferRows {
let mut transforms = self.transforms.cursor::<(WrapPoint, CharPoint)>(&());
let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left, &());
let mut input_row = transforms.start().1.row();
if transforms.item().map_or(false, |t| t.is_isomorphic()) {
input_row += start_row - transforms.start().0.row();
}
let soft_wrapped = transforms.item().map_or(false, |t| !t.is_isomorphic());
let mut input_buffer_rows = self.char_snapshot.buffer_rows(input_row);
let mut input_buffer_rows = self.tab_snapshot.buffer_rows(input_row);
let input_buffer_row = input_buffer_rows.next().unwrap();
WrapBufferRows {
transforms,
@@ -743,26 +737,26 @@ impl WrapSnapshot {
}
}
pub fn to_char_point(&self, point: WrapPoint) -> CharPoint {
let mut cursor = self.transforms.cursor::<(WrapPoint, CharPoint)>(&());
pub fn to_tab_point(&self, point: WrapPoint) -> TabPoint {
let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
cursor.seek(&point, Bias::Right, &());
let mut char_point = cursor.start().1 .0;
let mut tab_point = cursor.start().1 .0;
if cursor.item().map_or(false, |t| t.is_isomorphic()) {
char_point += point.0 - cursor.start().0 .0;
tab_point += point.0 - cursor.start().0 .0;
}
CharPoint(char_point)
TabPoint(tab_point)
}
pub fn to_point(&self, point: WrapPoint, bias: Bias) -> Point {
self.char_snapshot.to_point(self.to_char_point(point), bias)
self.tab_snapshot.to_point(self.to_tab_point(point), bias)
}
pub fn make_wrap_point(&self, point: Point, bias: Bias) -> WrapPoint {
self.char_point_to_wrap_point(self.char_snapshot.make_char_point(point, bias))
self.tab_point_to_wrap_point(self.tab_snapshot.make_tab_point(point, bias))
}
pub fn char_point_to_wrap_point(&self, point: CharPoint) -> WrapPoint {
let mut cursor = self.transforms.cursor::<(CharPoint, WrapPoint)>(&());
pub fn tab_point_to_wrap_point(&self, point: TabPoint) -> WrapPoint {
let mut cursor = self.transforms.cursor::<(TabPoint, WrapPoint)>(&());
cursor.seek(&point, Bias::Right, &());
WrapPoint(cursor.start().1 .0 + (point.0 - cursor.start().0 .0))
}
@@ -777,10 +771,7 @@ impl WrapSnapshot {
}
}
self.char_point_to_wrap_point(
self.char_snapshot
.clip_point(self.to_char_point(point), bias),
)
self.tab_point_to_wrap_point(self.tab_snapshot.clip_point(self.to_tab_point(point), bias))
}
pub fn prev_row_boundary(&self, mut point: WrapPoint) -> u32 {
@@ -790,7 +781,7 @@ impl WrapSnapshot {
*point.column_mut() = 0;
let mut cursor = self.transforms.cursor::<(WrapPoint, CharPoint)>(&());
let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
cursor.seek(&point, Bias::Right, &());
if cursor.item().is_none() {
cursor.prev(&());
@@ -810,7 +801,7 @@ impl WrapSnapshot {
pub fn next_row_boundary(&self, mut point: WrapPoint) -> Option<u32> {
point.0 += Point::new(1, 0);
let mut cursor = self.transforms.cursor::<(WrapPoint, CharPoint)>(&());
let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
cursor.seek(&point, Bias::Right, &());
while let Some(transform) = cursor.item() {
if transform.is_isomorphic() && cursor.start().1.column() == 0 {
@@ -842,8 +833,8 @@ impl WrapSnapshot {
#[cfg(test)]
{
assert_eq!(
CharPoint::from(self.transforms.summary().input.lines),
self.char_snapshot.max_point()
TabPoint::from(self.transforms.summary().input.lines),
self.tab_snapshot.max_point()
);
{
@@ -856,18 +847,18 @@ impl WrapSnapshot {
}
let text = language::Rope::from(self.text().as_str());
let mut input_buffer_rows = self.char_snapshot.buffer_rows(0);
let mut input_buffer_rows = self.tab_snapshot.buffer_rows(0);
let mut expected_buffer_rows = Vec::new();
let mut prev_tab_row = 0;
for display_row in 0..=self.max_point().row() {
let char_point = self.to_char_point(WrapPoint::new(display_row, 0));
if char_point.row() == prev_tab_row && display_row != 0 {
let tab_point = self.to_tab_point(WrapPoint::new(display_row, 0));
if tab_point.row() == prev_tab_row && display_row != 0 {
expected_buffer_rows.push(None);
} else {
expected_buffer_rows.push(input_buffer_rows.next().unwrap());
}
prev_tab_row = char_point.row();
prev_tab_row = tab_point.row();
assert_eq!(self.line_len(display_row), text.line_len(display_row));
}
@@ -889,14 +880,14 @@ impl<'a> WrapChunks<'a> {
let output_start = WrapPoint::new(rows.start, 0);
let output_end = WrapPoint::new(rows.end, 0);
self.transforms.seek(&output_start, Bias::Right, &());
let mut input_start = CharPoint(self.transforms.start().1 .0);
let mut input_start = TabPoint(self.transforms.start().1 .0);
if self.transforms.item().map_or(false, |t| t.is_isomorphic()) {
input_start.0 += output_start.0 - self.transforms.start().0 .0;
}
let input_end = self
.snapshot
.to_char_point(output_end)
.min(self.snapshot.char_snapshot.max_point());
.to_tab_point(output_end)
.min(self.snapshot.tab_snapshot.max_point());
self.input_chunks.seek(input_start..input_end);
self.input_chunk = Chunk::default();
self.output_position = output_start;
@@ -951,11 +942,13 @@ impl<'a> Iterator for WrapChunks<'a> {
} else {
*self.output_position.column_mut() += char_len as u32;
}
if self.output_position >= transform_end {
self.transforms.next(&());
break;
}
}
let (prefix, suffix) = self.input_chunk.text.split_at(input_len);
self.input_chunk.text = suffix;
Some(Chunk {
@@ -1110,7 +1103,7 @@ impl sum_tree::Summary for TransformSummary {
}
}
impl<'a> sum_tree::Dimension<'a, TransformSummary> for CharPoint {
impl<'a> sum_tree::Dimension<'a, TransformSummary> for TabPoint {
fn zero(_cx: &()) -> Self {
Default::default()
}
@@ -1120,7 +1113,7 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for CharPoint {
}
}
impl<'a> sum_tree::SeekTarget<'a, TransformSummary, TransformSummary> for CharPoint {
impl<'a> sum_tree::SeekTarget<'a, TransformSummary, TransformSummary> for TabPoint {
fn cmp(&self, cursor_location: &TransformSummary, _: &()) -> std::cmp::Ordering {
Ord::cmp(&self.0, &cursor_location.input.lines)
}
@@ -1168,7 +1161,7 @@ fn consolidate_wrap_edits(edits: Vec<WrapEdit>) -> Vec<WrapEdit> {
mod tests {
use super::*;
use crate::{
display_map::{char_map::CharMap, fold_map::FoldMap, inlay_map::InlayMap},
display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap},
MultiBuffer,
};
use gpui::{font, px, test::observe};
@@ -1220,9 +1213,9 @@ mod tests {
log::info!("InlayMap text: {:?}", inlay_snapshot.text());
let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot.clone());
log::info!("FoldMap text: {:?}", fold_snapshot.text());
let (mut char_map, _) = CharMap::new(fold_snapshot.clone(), tab_size);
let tabs_snapshot = char_map.set_max_expansion_column(32);
log::info!("CharMap text: {:?}", tabs_snapshot.text());
let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
let tabs_snapshot = tab_map.set_max_expansion_column(32);
log::info!("TabMap text: {:?}", tabs_snapshot.text());
let mut line_wrapper = text_system.line_wrapper(font.clone(), font_size);
let unwrapped_text = tabs_snapshot.text();
@@ -1268,7 +1261,7 @@ mod tests {
20..=39 => {
for (fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
let (tabs_snapshot, tab_edits) =
char_map.sync(fold_snapshot, fold_edits, tab_size);
tab_map.sync(fold_snapshot, fold_edits, tab_size);
let (mut snapshot, wrap_edits) =
wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
snapshot.check_invariants();
@@ -1281,7 +1274,7 @@ mod tests {
inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
let (tabs_snapshot, tab_edits) =
char_map.sync(fold_snapshot, fold_edits, tab_size);
tab_map.sync(fold_snapshot, fold_edits, tab_size);
let (mut snapshot, wrap_edits) =
wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
snapshot.check_invariants();
@@ -1305,8 +1298,8 @@ mod tests {
log::info!("InlayMap text: {:?}", inlay_snapshot.text());
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
log::info!("FoldMap text: {:?}", fold_snapshot.text());
let (tabs_snapshot, tab_edits) = char_map.sync(fold_snapshot, fold_edits, tab_size);
log::info!("CharMap text: {:?}", tabs_snapshot.text());
let (tabs_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
log::info!("TabMap text: {:?}", tabs_snapshot.text());
let unwrapped_text = tabs_snapshot.text();
let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
@@ -1352,7 +1345,7 @@ mod tests {
if tab_size.get() == 1
|| !wrapped_snapshot
.char_snapshot
.tab_snapshot
.fold_snapshot
.text()
.contains('\t')

View File

@@ -502,6 +502,19 @@ struct RunnableTasks {
context_range: Range<BufferOffset>,
}
impl RunnableTasks {
fn resolve<'a>(
&'a self,
cx: &'a task::TaskContext,
) -> impl Iterator<Item = (TaskSourceKind, ResolvedTask)> + 'a {
self.templates.iter().filter_map(|(kind, template)| {
template
.resolve_task(&kind.to_id_base(), cx)
.map(|task| (kind.clone(), task))
})
}
}
#[derive(Clone)]
struct ResolvedTasks {
templates: SmallVec<[(TaskSourceKind, ResolvedTask); 1]>,
@@ -3244,9 +3257,21 @@ impl Editor {
}
if enabled && pair.start.ends_with(text.as_ref()) {
bracket_pair = Some(pair.clone());
is_bracket_pair_start = true;
break;
let prefix_len = pair.start.len() - text.len();
let preceding_text_matches_prefix = prefix_len == 0
|| (selection.start.column >= (prefix_len as u32)
&& snapshot.contains_str_at(
Point::new(
selection.start.row,
selection.start.column - (prefix_len as u32),
),
&pair.start[..prefix_len],
));
if preceding_text_matches_prefix {
bracket_pair = Some(pair.clone());
is_bracket_pair_start = true;
break;
}
}
if pair.end.as_str() == text.as_ref() {
bracket_pair = Some(pair.clone());
@@ -3263,8 +3288,6 @@ impl Editor {
self.use_auto_surround && snapshot_settings.use_auto_surround;
if selection.is_empty() {
if is_bracket_pair_start {
let prefix_len = bracket_pair.start.len() - text.len();
// If the inserted text is a suffix of an opening bracket and the
// selection is preceded by the rest of the opening bracket, then
// insert the closing bracket.
@@ -3272,20 +3295,25 @@ impl Editor {
.chars_at(selection.start)
.next()
.map_or(true, |c| scope.should_autoclose_before(c));
let preceding_text_matches_prefix = prefix_len == 0
|| (selection.start.column >= (prefix_len as u32)
&& snapshot.contains_str_at(
Point::new(
selection.start.row,
selection.start.column - (prefix_len as u32),
),
&bracket_pair.start[..prefix_len],
));
let is_closing_quote = if bracket_pair.end == bracket_pair.start
&& bracket_pair.start.len() == 1
{
let target = bracket_pair.start.chars().next().unwrap();
let current_line_count = snapshot
.reversed_chars_at(selection.start)
.take_while(|&c| c != '\n')
.filter(|&c| c == target)
.count();
current_line_count % 2 == 1
} else {
false
};
if autoclose
&& bracket_pair.close
&& following_text_allows_autoclose
&& preceding_text_matches_prefix
&& !is_closing_quote
{
let anchor = snapshot.anchor_before(selection.end);
new_selections.push((selection.map(|_| anchor), text.len()));
@@ -4708,29 +4736,7 @@ impl Editor {
.as_ref()
.zip(editor.project.clone())
.map(|(tasks, project)| {
let position = Point::new(buffer_row, tasks.column);
let range_start = buffer.read(cx).anchor_at(position, Bias::Right);
let location = Location {
buffer: buffer.clone(),
range: range_start..range_start,
};
// Fill in the environmental variables from the tree-sitter captures
let mut captured_task_variables = TaskVariables::default();
for (capture_name, value) in tasks.extra_variables.clone() {
captured_task_variables.insert(
task::VariableName::Custom(capture_name.into()),
value.clone(),
);
}
project.update(cx, |project, cx| {
project.task_store().update(cx, |task_store, cx| {
task_store.task_context_for_location(
captured_task_variables,
location,
cx,
)
})
})
Self::build_tasks_context(&project, &buffer, buffer_row, tasks, cx)
});
Some(cx.spawn(|editor, mut cx| async move {
@@ -4741,15 +4747,7 @@ impl Editor {
let resolved_tasks =
tasks.zip(task_context).map(|(tasks, task_context)| {
Arc::new(ResolvedTasks {
templates: tasks
.templates
.iter()
.filter_map(|(kind, template)| {
template
.resolve_task(&kind.to_id_base(), &task_context)
.map(|task| (kind.clone(), task))
})
.collect(),
templates: tasks.resolve(&task_context).collect(),
position: snapshot.buffer_snapshot.anchor_before(Point::new(
multibuffer_point.row,
tasks.column,
@@ -5455,6 +5453,132 @@ impl Editor {
}
}
fn build_tasks_context(
project: &Model<Project>,
buffer: &Model<Buffer>,
buffer_row: u32,
tasks: &Arc<RunnableTasks>,
cx: &mut ViewContext<Self>,
) -> Task<Option<task::TaskContext>> {
let position = Point::new(buffer_row, tasks.column);
let range_start = buffer.read(cx).anchor_at(position, Bias::Right);
let location = Location {
buffer: buffer.clone(),
range: range_start..range_start,
};
// Fill in the environmental variables from the tree-sitter captures
let mut captured_task_variables = TaskVariables::default();
for (capture_name, value) in tasks.extra_variables.clone() {
captured_task_variables.insert(
task::VariableName::Custom(capture_name.into()),
value.clone(),
);
}
project.update(cx, |project, cx| {
project.task_store().update(cx, |task_store, cx| {
task_store.task_context_for_location(captured_task_variables, location, cx)
})
})
}
pub fn spawn_nearest_task(&mut self, action: &SpawnNearestTask, cx: &mut ViewContext<Self>) {
let Some((workspace, _)) = self.workspace.clone() else {
return;
};
let Some(project) = self.project.clone() else {
return;
};
// Try to find a closest, enclosing node using tree-sitter that has a
// task
let Some((buffer, buffer_row, tasks)) = self
.find_enclosing_node_task(cx)
// Or find the task that's closest in row-distance.
.or_else(|| self.find_closest_task(cx))
else {
return;
};
let reveal_strategy = action.reveal;
let task_context = Self::build_tasks_context(&project, &buffer, buffer_row, &tasks, cx);
cx.spawn(|_, mut cx| async move {
let context = task_context.await?;
let (task_source_kind, mut resolved_task) = tasks.resolve(&context).next()?;
let resolved = resolved_task.resolved.as_mut()?;
resolved.reveal = reveal_strategy;
workspace
.update(&mut cx, |workspace, cx| {
workspace::tasks::schedule_resolved_task(
workspace,
task_source_kind,
resolved_task,
false,
cx,
);
})
.ok()
})
.detach();
}
fn find_closest_task(
&mut self,
cx: &mut ViewContext<Self>,
) -> Option<(Model<Buffer>, u32, Arc<RunnableTasks>)> {
let cursor_row = self.selections.newest_adjusted(cx).head().row;
let ((buffer_id, row), tasks) = self
.tasks
.iter()
.min_by_key(|((_, row), _)| cursor_row.abs_diff(*row))?;
let buffer = self.buffer.read(cx).buffer(*buffer_id)?;
let tasks = Arc::new(tasks.to_owned());
Some((buffer, *row, tasks))
}
fn find_enclosing_node_task(
&mut self,
cx: &mut ViewContext<Self>,
) -> Option<(Model<Buffer>, u32, Arc<RunnableTasks>)> {
let snapshot = self.buffer.read(cx).snapshot(cx);
let offset = self.selections.newest::<usize>(cx).head();
let excerpt = snapshot.excerpt_containing(offset..offset)?;
let buffer_id = excerpt.buffer().remote_id();
let layer = excerpt.buffer().syntax_layer_at(offset)?;
let mut cursor = layer.node().walk();
while cursor.goto_first_child_for_byte(offset).is_some() {
if cursor.node().end_byte() == offset {
cursor.goto_next_sibling();
}
}
// Ascend to the smallest ancestor that contains the range and has a task.
loop {
let node = cursor.node();
let node_range = node.byte_range();
let symbol_start_row = excerpt.buffer().offset_to_point(node.start_byte()).row;
// Check if this node contains our offset
if node_range.start <= offset && node_range.end >= offset {
// If it contains offset, check for task
if let Some(tasks) = self.tasks.get(&(buffer_id, symbol_start_row)) {
let buffer = self.buffer.read(cx).buffer(buffer_id)?;
return Some((buffer, symbol_start_row, Arc::new(tasks.to_owned())));
}
}
if !cursor.goto_parent() {
break;
}
}
None
}
fn render_run_indicator(
&self,
_style: &EditorStyle,
@@ -7339,11 +7463,12 @@ impl Editor {
if !selection.is_empty() && !line_mode {
selection.goal = SelectionGoal::None;
}
let (cursor, goal) = movement::up(
let (cursor, goal) = movement::up2(
map,
selection.start,
selection.goal,
false,
false,
text_layout_details,
);
selection.collapse_to(cursor, goal);
@@ -7504,8 +7629,16 @@ impl Editor {
pub fn select_up(&mut self, _: &SelectUp, cx: &mut ViewContext<Self>) {
let text_layout_details = &self.text_layout_details(cx);
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_heads_with(|map, head, goal| {
movement::up(map, head, goal, false, text_layout_details)
s.move_with(|map, selection| {
let (head, goal) = movement::up2(
map,
selection.head(),
selection.goal,
false,
!selection.reversed,
text_layout_details,
);
selection.set_head(head, goal);
})
})
}
@@ -8649,14 +8782,22 @@ impl Editor {
let snapshot = this.buffer.read(cx).read(cx);
let empty_str: Arc<str> = Arc::default();
let mut suffixes_inserted = Vec::new();
let ignore_indent = action.ignore_indent;
fn comment_prefix_range(
snapshot: &MultiBufferSnapshot,
row: MultiBufferRow,
comment_prefix: &str,
comment_prefix_whitespace: &str,
ignore_indent: bool,
) -> Range<Point> {
let start = Point::new(row.0, snapshot.indent_size_for_line(row).len);
let indent_size = if ignore_indent {
0
} else {
snapshot.indent_size_for_line(row).len
};
let start = Point::new(row.0, indent_size);
let mut line_bytes = snapshot
.bytes_in_range(start..snapshot.max_point())
@@ -8752,7 +8893,16 @@ impl Editor {
}
// If the language has line comments, toggle those.
let full_comment_prefixes = language.line_comment_prefixes();
let mut full_comment_prefixes = language.line_comment_prefixes().to_vec();
// If ignore_indent is set, trim spaces from the right side of all full_comment_prefixes
if ignore_indent {
full_comment_prefixes = full_comment_prefixes
.into_iter()
.map(|s| Arc::from(s.trim_end()))
.collect();
}
if !full_comment_prefixes.is_empty() {
let first_prefix = full_comment_prefixes
.first()
@@ -8779,6 +8929,7 @@ impl Editor {
row,
&prefix[..trimmed_prefix_len],
&prefix[trimmed_prefix_len..],
ignore_indent,
)
})
.max_by_key(|range| range.end.column - range.start.column)
@@ -8819,6 +8970,7 @@ impl Editor {
start_row,
comment_prefix,
comment_prefix_whitespace,
ignore_indent,
);
let suffix_range = comment_suffix_range(
snapshot.deref(),
@@ -9614,8 +9766,8 @@ impl Editor {
let Some(provider) = self.semantics_provider.clone() else {
return Task::ready(Ok(Navigated::No));
};
let buffer = self.buffer.read(cx);
let head = self.selections.newest::<usize>(cx).head();
let buffer = self.buffer.read(cx);
let (buffer, head) = if let Some(text_anchor) = buffer.text_anchor_for_position(head, cx) {
text_anchor
} else {
@@ -9922,8 +10074,8 @@ impl Editor {
_: &FindAllReferences,
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<Navigated>>> {
let multi_buffer = self.buffer.read(cx);
let selection = self.selections.newest::<usize>(cx);
let multi_buffer = self.buffer.read(cx);
let head = selection.head();
let multi_buffer_snapshot = multi_buffer.snapshot(cx);
@@ -10330,8 +10482,9 @@ impl Editor {
self.show_local_selections = true;
if moving_cursor {
let rename_editor = rename.editor.read(cx);
let cursor_in_rename_editor = rename_editor.selections.newest::<usize>(cx).head();
let cursor_in_rename_editor = rename.editor.update(cx, |editor, cx| {
editor.selections.newest::<usize>(cx).head()
});
// Update the selection to match the position of the selection inside
// the rename editor.
@@ -10445,7 +10598,7 @@ impl Editor {
fn cancel_language_server_work(
&mut self,
_: &CancelLanguageServerWork,
_: &actions::CancelLanguageServerWork,
cx: &mut ViewContext<Self>,
) {
if let Some(project) = self.project.clone() {
@@ -10728,15 +10881,42 @@ impl Editor {
self.fold_ranges(fold_ranges, 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 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;
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);
}
start_row = nested_end_row + 1;
}
None => start_row += 1,
}
}
}
self.fold_ranges(fold_ranges, true, cx);
}
pub fn fold_all(&mut self, _: &actions::FoldAll, cx: &mut ViewContext<Self>) {
let mut fold_ranges = Vec::new();
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let snapshot = self.buffer.read(cx).snapshot(cx);
for row in 0..display_map.max_buffer_row().0 {
if let Some((foldable_range, fold_text)) =
display_map.foldable_range(MultiBufferRow(row))
{
fold_ranges.push((foldable_range, fold_text));
for row in 0..snapshot.max_buffer_row().0 {
if let Some(foldable_range) = self.snapshot(cx).foldable_range(MultiBufferRow(row)) {
fold_ranges.push(foldable_range);
}
}
@@ -11550,9 +11730,9 @@ impl Editor {
}
pub fn copy_file_location(&mut self, _: &CopyFileLocation, cx: &mut ViewContext<Self>) {
let selection = self.selections.newest::<Point>(cx).start.row + 1;
if let Some(file) = self.target_file(cx) {
if let Some(path) = file.path().to_str() {
let selection = self.selections.newest::<Point>(cx).start.row + 1;
cx.write_to_clipboard(ClipboardItem::new_string(format!("{path}:{selection}")));
}
}
@@ -12328,9 +12508,10 @@ impl Editor {
return;
};
let selections = self.selections.all::<usize>(cx);
let buffer = self.buffer.read(cx);
let mut new_selections_by_buffer = HashMap::default();
for selection in self.selections.all::<usize>(cx) {
for selection in selections {
for (buffer, range, _) in
buffer.range_to_buffer_ranges(selection.start..selection.end, cx)
{
@@ -12375,6 +12556,7 @@ impl Editor {
}
fn open_excerpts_common(&mut self, split: bool, cx: &mut ViewContext<Self>) {
let selections = self.selections.all::<usize>(cx);
let buffer = self.buffer.read(cx);
if buffer.is_singleton() {
cx.propagate();
@@ -12387,7 +12569,7 @@ impl Editor {
};
let mut new_selections_by_buffer = HashMap::default();
for selection in self.selections.all::<usize>(cx) {
for selection in selections {
for (mut buffer_handle, mut range, _) in
buffer.range_to_buffer_ranges(selection.range(), cx)
{
@@ -12503,7 +12685,7 @@ impl Editor {
fn selection_replacement_ranges(
&self,
range: Range<OffsetUtf16>,
cx: &AppContext,
cx: &mut AppContext,
) -> Vec<Range<OffsetUtf16>> {
let selections = self.selections.all::<OffsetUtf16>(cx);
let newest_selection = selections
@@ -14146,7 +14328,7 @@ pub fn diagnostic_block_renderer(
.relative()
.size_full()
.pl(cx.gutter_dimensions.width)
.w(cx.max_width + cx.gutter_dimensions.width)
.w(cx.max_width - cx.gutter_dimensions.full_width())
.child(
div()
.flex()

View File

@@ -1080,6 +1080,112 @@ fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
});
}
#[gpui::test]
fn test_fold_at_level(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let view = cx.add_window(|cx| {
let buffer = MultiBuffer::build_simple(
&"
class Foo:
# Hello!
def a():
print(1)
def b():
print(2)
class Bar:
# World!
def a():
print(1)
def b():
print(2)
"
.unindent(),
cx,
);
build_editor(buffer.clone(), cx)
});
_ = view.update(cx, |view, cx| {
view.fold_at_level(&FoldAtLevel { level: 2 }, cx);
assert_eq!(
view.display_text(cx),
"
class Foo:
# Hello!
def a():⋯
def b():⋯
class Bar:
# World!
def a():⋯
def b():⋯
"
.unindent(),
);
view.fold_at_level(&FoldAtLevel { level: 1 }, cx);
assert_eq!(
view.display_text(cx),
"
class Foo:⋯
class Bar:⋯
"
.unindent(),
);
view.unfold_all(&UnfoldAll, cx);
view.fold_at_level(&FoldAtLevel { level: 0 }, cx);
assert_eq!(
view.display_text(cx),
"
class Foo:
# Hello!
def a():
print(1)
def b():
print(2)
class Bar:
# World!
def a():
print(1)
def b():
print(2)
"
.unindent(),
);
assert_eq!(view.display_text(cx), view.buffer.read(cx).read(cx).text());
});
}
#[gpui::test]
fn test_move_cursor(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -8427,6 +8533,131 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
"});
}
#[gpui::test]
async fn test_toggle_comment_ignore_indent(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let language = Arc::new(Language::new(
LanguageConfig {
line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
..Default::default()
},
Some(tree_sitter_rust::LANGUAGE.into()),
));
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
let toggle_comments = &ToggleComments {
advance_downwards: false,
ignore_indent: true,
};
// If multiple selections intersect a line, the line is only toggled once.
cx.set_state(indoc! {"
fn a() {
// «b();
// c();
// ˇ» d();
}
"});
cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx));
cx.assert_editor_state(indoc! {"
fn a() {
«b();
c();
ˇ» d();
}
"});
// The comment prefix is inserted at the beginning of each line
cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx));
cx.assert_editor_state(indoc! {"
fn a() {
// «b();
// c();
// ˇ» d();
}
"});
// If a selection ends at the beginning of a line, that line is not toggled.
cx.set_selections_state(indoc! {"
fn a() {
// b();
// «c();
ˇ»// d();
}
"});
cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx));
cx.assert_editor_state(indoc! {"
fn a() {
// b();
«c();
ˇ»// d();
}
"});
// If a selection span a single line and is empty, the line is toggled.
cx.set_state(indoc! {"
fn a() {
a();
b();
ˇ
}
"});
cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx));
cx.assert_editor_state(indoc! {"
fn a() {
a();
b();
//ˇ
}
"});
// If a selection span multiple lines, empty lines are not toggled.
cx.set_state(indoc! {"
fn a() {
«a();
c();ˇ»
}
"});
cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx));
cx.assert_editor_state(indoc! {"
fn a() {
// «a();
// c();ˇ»
}
"});
// If a selection includes multiple comment prefixes, all lines are uncommented.
cx.set_state(indoc! {"
fn a() {
// «a();
/// b();
//! c();ˇ»
}
"});
cx.update_editor(|e, cx| e.toggle_comments(toggle_comments, cx));
cx.assert_editor_state(indoc! {"
fn a() {
«a();
b();
c();ˇ»
}
"});
}
#[gpui::test]
async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
@@ -8448,6 +8679,7 @@ async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext)
let toggle_comments = &ToggleComments {
advance_downwards: true,
ignore_indent: false,
};
// Single cursor on one line -> advance
@@ -13098,6 +13330,89 @@ async fn test_goto_definition_with_find_all_references_fallback(cx: &mut gpui::T
});
}
#[gpui::test]
async fn test_find_enclosing_node_with_task(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let language = Arc::new(Language::new(
LanguageConfig::default(),
Some(tree_sitter_rust::LANGUAGE.into()),
));
let text = r#"
#[cfg(test)]
mod tests() {
#[test]
fn runnable_1() {
let a = 1;
}
#[test]
fn runnable_2() {
let a = 1;
let b = 2;
}
}
"#
.unindent();
let fs = FakeFs::new(cx.executor());
fs.insert_file("/file.rs", Default::default()).await;
let project = Project::test(fs, ["/a".as_ref()], cx).await;
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
let buffer = cx.new_model(|cx| Buffer::local(text, cx).with_language(language, cx));
let multi_buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer.clone(), cx));
let editor = cx.new_view(|cx| {
Editor::new(
EditorMode::Full,
multi_buffer,
Some(project.clone()),
true,
cx,
)
});
editor.update(cx, |editor, cx| {
editor.tasks.insert(
(buffer.read(cx).remote_id(), 3),
RunnableTasks {
templates: vec![],
offset: MultiBufferOffset(43),
column: 0,
extra_variables: HashMap::default(),
context_range: BufferOffset(43)..BufferOffset(85),
},
);
editor.tasks.insert(
(buffer.read(cx).remote_id(), 8),
RunnableTasks {
templates: vec![],
offset: MultiBufferOffset(86),
column: 0,
extra_variables: HashMap::default(),
context_range: BufferOffset(86)..BufferOffset(191),
},
);
// Test finding task when cursor is inside function body
editor.change_selections(None, cx, |s| {
s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
});
let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
// Test finding task when cursor is on function name
editor.change_selections(None, cx, |s| {
s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
});
let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
assert_eq!(row, 8, "Should find task when cursor is on function name");
});
}
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
point..point

View File

@@ -337,6 +337,7 @@ impl EditorElement {
register_action(view, cx, Editor::open_url);
register_action(view, cx, Editor::open_file);
register_action(view, cx, Editor::fold);
register_action(view, cx, Editor::fold_at_level);
register_action(view, cx, Editor::fold_all);
register_action(view, cx, Editor::fold_at);
register_action(view, cx, Editor::fold_recursive);
@@ -445,9 +446,11 @@ impl EditorElement {
register_action(view, cx, Editor::accept_inline_completion);
register_action(view, cx, Editor::revert_file);
register_action(view, cx, Editor::revert_selected_hunks);
register_action(view, cx, Editor::apply_all_diff_hunks);
register_action(view, cx, Editor::apply_selected_diff_hunks);
register_action(view, cx, Editor::open_active_item_in_terminal);
register_action(view, cx, Editor::reload_file)
register_action(view, cx, Editor::reload_file);
register_action(view, cx, Editor::spawn_nearest_task);
}
fn register_key_listeners(&self, cx: &mut WindowContext, layout: &EditorLayout) {
@@ -822,129 +825,131 @@ impl EditorElement {
let mut selections: Vec<(PlayerColor, Vec<SelectionLayout>)> = Vec::new();
let mut active_rows = BTreeMap::new();
let mut newest_selection_head = None;
let editor = self.editor.read(cx);
self.editor.update(cx, |editor, cx| {
if editor.show_local_selections {
let mut local_selections: Vec<Selection<Point>> = editor
.selections
.disjoint_in_range(start_anchor..end_anchor, cx);
local_selections.extend(editor.selections.pending(cx));
let mut layouts = Vec::new();
let newest = editor.selections.newest(cx);
for selection in local_selections.drain(..) {
let is_empty = selection.start == selection.end;
let is_newest = selection == newest;
if editor.show_local_selections {
let mut local_selections: Vec<Selection<Point>> = editor
.selections
.disjoint_in_range(start_anchor..end_anchor, cx);
local_selections.extend(editor.selections.pending(cx));
let mut layouts = Vec::new();
let newest = editor.selections.newest(cx);
for selection in local_selections.drain(..) {
let is_empty = selection.start == selection.end;
let is_newest = selection == newest;
let layout = SelectionLayout::new(
selection,
editor.selections.line_mode,
editor.cursor_shape,
&snapshot.display_snapshot,
is_newest,
editor.leader_peer_id.is_none(),
None,
);
if is_newest {
newest_selection_head = Some(layout.head);
}
let layout = SelectionLayout::new(
selection,
editor.selections.line_mode,
editor.cursor_shape,
&snapshot.display_snapshot,
is_newest,
editor.leader_peer_id.is_none(),
None,
);
if is_newest {
newest_selection_head = Some(layout.head);
for row in cmp::max(layout.active_rows.start.0, start_row.0)
..=cmp::min(layout.active_rows.end.0, end_row.0)
{
let contains_non_empty_selection =
active_rows.entry(DisplayRow(row)).or_insert(!is_empty);
*contains_non_empty_selection |= !is_empty;
}
layouts.push(layout);
}
for row in cmp::max(layout.active_rows.start.0, start_row.0)
..=cmp::min(layout.active_rows.end.0, end_row.0)
{
let contains_non_empty_selection =
active_rows.entry(DisplayRow(row)).or_insert(!is_empty);
*contains_non_empty_selection |= !is_empty;
}
layouts.push(layout);
let player = if editor.read_only(cx) {
cx.theme().players().read_only()
} else {
self.style.local_player
};
selections.push((player, layouts));
}
let player = if editor.read_only(cx) {
cx.theme().players().read_only()
} else {
self.style.local_player
};
selections.push((player, layouts));
}
if let Some(collaboration_hub) = &editor.collaboration_hub {
// When following someone, render the local selections in their color.
if let Some(leader_id) = editor.leader_peer_id {
if let Some(collaborator) = collaboration_hub.collaborators(cx).get(&leader_id) {
if let Some(participant_index) = collaboration_hub
.user_participant_indices(cx)
.get(&collaborator.user_id)
if let Some(collaboration_hub) = &editor.collaboration_hub {
// When following someone, render the local selections in their color.
if let Some(leader_id) = editor.leader_peer_id {
if let Some(collaborator) = collaboration_hub.collaborators(cx).get(&leader_id)
{
if let Some((local_selection_style, _)) = selections.first_mut() {
*local_selection_style = cx
.theme()
.players()
.color_for_participant(participant_index.0);
if let Some(participant_index) = collaboration_hub
.user_participant_indices(cx)
.get(&collaborator.user_id)
{
if let Some((local_selection_style, _)) = selections.first_mut() {
*local_selection_style = cx
.theme()
.players()
.color_for_participant(participant_index.0);
}
}
}
}
}
let mut remote_selections = HashMap::default();
for selection in snapshot.remote_selections_in_range(
&(start_anchor..end_anchor),
collaboration_hub.as_ref(),
cx,
) {
let selection_style = Self::get_participant_color(selection.participant_index, cx);
let mut remote_selections = HashMap::default();
for selection in snapshot.remote_selections_in_range(
&(start_anchor..end_anchor),
collaboration_hub.as_ref(),
cx,
) {
let selection_style =
Self::get_participant_color(selection.participant_index, cx);
// Don't re-render the leader's selections, since the local selections
// match theirs.
if Some(selection.peer_id) == editor.leader_peer_id {
continue;
// Don't re-render the leader's selections, since the local selections
// match theirs.
if Some(selection.peer_id) == editor.leader_peer_id {
continue;
}
let key = HoveredCursor {
replica_id: selection.replica_id,
selection_id: selection.selection.id,
};
let is_shown =
editor.show_cursor_names || editor.hovered_cursors.contains_key(&key);
remote_selections
.entry(selection.replica_id)
.or_insert((selection_style, Vec::new()))
.1
.push(SelectionLayout::new(
selection.selection,
selection.line_mode,
selection.cursor_shape,
&snapshot.display_snapshot,
false,
false,
if is_shown { selection.user_name } else { None },
));
}
let key = HoveredCursor {
replica_id: selection.replica_id,
selection_id: selection.selection.id,
selections.extend(remote_selections.into_values());
} else if !editor.is_focused(cx) && editor.show_cursor_when_unfocused {
let player = if editor.read_only(cx) {
cx.theme().players().read_only()
} else {
self.style.local_player
};
let is_shown =
editor.show_cursor_names || editor.hovered_cursors.contains_key(&key);
remote_selections
.entry(selection.replica_id)
.or_insert((selection_style, Vec::new()))
.1
.push(SelectionLayout::new(
selection.selection,
selection.line_mode,
selection.cursor_shape,
&snapshot.display_snapshot,
false,
false,
if is_shown { selection.user_name } else { None },
));
let layouts = snapshot
.buffer_snapshot
.selections_in_range(&(start_anchor..end_anchor), true)
.map(move |(_, line_mode, cursor_shape, selection)| {
SelectionLayout::new(
selection,
line_mode,
cursor_shape,
&snapshot.display_snapshot,
false,
false,
None,
)
})
.collect::<Vec<_>>();
selections.push((player, layouts));
}
selections.extend(remote_selections.into_values());
} else if !editor.is_focused(cx) && editor.show_cursor_when_unfocused {
let player = if editor.read_only(cx) {
cx.theme().players().read_only()
} else {
self.style.local_player
};
let layouts = snapshot
.buffer_snapshot
.selections_in_range(&(start_anchor..end_anchor), true)
.map(move |(_, line_mode, cursor_shape, selection)| {
SelectionLayout::new(
selection,
line_mode,
cursor_shape,
&snapshot.display_snapshot,
false,
false,
None,
)
})
.collect::<Vec<_>>();
selections.push((player, layouts));
}
});
(selections, active_rows, newest_selection_head)
}
@@ -1030,18 +1035,13 @@ impl EditorElement {
.or_else(|| {
if cursor_column == 0 {
snapshot.placeholder_text().and_then(|s| {
s.graphemes(true).next().map(|s| s.to_owned())
s.graphemes(true).next().map(|s| s.to_string().into())
})
} else {
None
}
})
.and_then(|grapheme| {
let text = if grapheme == "\n" {
SharedString::from(" ")
} else {
SharedString::from(grapheme)
};
.and_then(|text| {
let len = text.len();
let font = cursor_row_layout
@@ -1851,23 +1851,25 @@ impl EditorElement {
return Vec::new();
}
let editor = self.editor.read(cx);
let newest_selection_head = newest_selection_head.unwrap_or_else(|| {
let newest = editor.selections.newest::<Point>(cx);
SelectionLayout::new(
newest,
editor.selections.line_mode,
editor.cursor_shape,
&snapshot.display_snapshot,
true,
true,
None,
)
.head
let (newest_selection_head, is_relative) = self.editor.update(cx, |editor, cx| {
let newest_selection_head = newest_selection_head.unwrap_or_else(|| {
let newest = editor.selections.newest::<Point>(cx);
SelectionLayout::new(
newest,
editor.selections.line_mode,
editor.cursor_shape,
&snapshot.display_snapshot,
true,
true,
None,
)
.head
});
let is_relative = editor.should_use_relative_line_numbers(cx);
(newest_selection_head, is_relative)
});
let font_size = self.style.text.font_size.to_pixels(cx.rem_size());
let is_relative = editor.should_use_relative_line_numbers(cx);
let relative_to = if is_relative {
Some(newest_selection_head.row())
} else {
@@ -2066,6 +2068,7 @@ impl EditorElement {
editor_width: Pixels,
scroll_width: &mut Pixels,
resized_blocks: &mut HashMap<CustomBlockId, u32>,
active_rows: &BTreeMap<DisplayRow, bool>,
cx: &mut WindowContext,
) -> (AnyElement, Size<Pixels>) {
let mut element = match block {
@@ -2092,6 +2095,7 @@ impl EditorElement {
line_height,
em_width,
block_id,
selected: active_rows.contains_key(&block_row_start),
max_width: text_hitbox.size.width.max(*scroll_width),
editor_style: &self.style,
}))
@@ -2429,6 +2433,7 @@ impl EditorElement {
text_x: Pixels,
line_height: Pixels,
line_layouts: &[LineWithInvisibles],
active_rows: &BTreeMap<DisplayRow, bool>,
cx: &mut WindowContext,
) -> Result<Vec<BlockLayout>, HashMap<CustomBlockId, u32>> {
let (fixed_blocks, non_fixed_blocks) = snapshot
@@ -2465,6 +2470,7 @@ impl EditorElement {
editor_width,
scroll_width,
&mut resized_blocks,
active_rows,
cx,
);
fixed_block_max_width = fixed_block_max_width.max(element_size.width + em_width);
@@ -2509,6 +2515,7 @@ impl EditorElement {
editor_width,
scroll_width,
&mut resized_blocks,
active_rows,
cx,
);
@@ -2554,6 +2561,7 @@ impl EditorElement {
editor_width,
scroll_width,
&mut resized_blocks,
active_rows,
cx,
);
@@ -4156,7 +4164,16 @@ fn render_inline_blame_entry(
let relative_timestamp = blame_entry_relative_timestamp(&blame_entry);
let author = blame_entry.author.as_deref().unwrap_or_default();
let text = format!("{}, {}", author, relative_timestamp);
let summary_enabled = ProjectSettings::get_global(cx)
.git
.show_inline_commit_summary();
let text = match blame_entry.summary.as_ref() {
Some(summary) if summary_enabled => {
format!("{}, {} - {}", author, relative_timestamp, summary)
}
_ => format!("{}, {}", author, relative_timestamp),
};
let details = blame.read(cx).details_for_entry(&blame_entry);
@@ -5165,6 +5182,7 @@ impl Element for EditorElement {
gutter_dimensions.full_width(),
line_height,
&line_layouts,
&active_rows,
cx,
)
});

View File

@@ -368,12 +368,15 @@ impl GitBlame {
.spawn({
let snapshot = snapshot.clone();
async move {
let Blame {
let Some(Blame {
entries,
permalinks,
messages,
remote_url,
} = blame.await?;
}) = blame.await?
else {
return Ok(None);
};
let entries = build_blame_entry_sum_tree(entries, snapshot.max_point().row);
let commit_details = parse_commit_messages(
@@ -385,13 +388,16 @@ impl GitBlame {
)
.await;
anyhow::Ok((entries, commit_details))
anyhow::Ok(Some((entries, commit_details)))
}
})
.await;
this.update(&mut cx, |this, cx| match result {
Ok((entries, commit_details)) => {
Ok(None) => {
// Nothing to do, e.g. no repository found
}
Ok(Some((entries, commit_details))) => {
this.buffer_edits = buffer_edits;
this.buffer_snapshot = snapshot;
this.entries = entries;
@@ -410,11 +416,7 @@ impl GitBlame {
} else {
// If we weren't triggered by a user, we just log errors in the background, instead of sending
// notifications.
// Except for `NoRepositoryError`, which can happen often if a user has inline-blame turned on
// and opens a non-git file.
if error.downcast_ref::<project::NoRepositoryError>().is_none() {
log::error!("failed to get git blame data: {error:?}");
}
log::error!("failed to get git blame data: {error:?}");
}
}),
})

View File

@@ -706,10 +706,11 @@ pub(crate) async fn find_file(
) -> Option<ResolvedPath> {
project
.update(cx, |project, cx| {
project.resolve_existing_file_path(&candidate_file_path, buffer, cx)
project.resolve_path_in_buffer(&candidate_file_path, buffer, cx)
})
.ok()?
.await
.filter(|s| s.is_file())
}
if let Some(existing_path) = check_path(&candidate_file_path, &project, buffer, cx).await {
@@ -1612,4 +1613,46 @@ mod tests {
assert_eq!(file_path.to_str().unwrap(), "/root/dir/file2.rs");
});
}
#[gpui::test]
async fn test_hover_directories(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
..Default::default()
},
cx,
)
.await;
// Insert a new file
let fs = cx.update_workspace(|workspace, cx| workspace.project().read(cx).fs().clone());
fs.as_fake()
.insert_file("/root/dir/file2.rs", "This is file2.rs".as_bytes().to_vec())
.await;
cx.set_state(indoc! {"
You can't open ../diˇr because it's a directory.
"});
// File does not exist
let screen_coord = cx.pixel_position(indoc! {"
You can't open ../diˇr because it's a directory.
"});
cx.simulate_mouse_move(screen_coord, None, Modifiers::secondary_key());
// No highlight
cx.update_editor(|editor, cx| {
assert!(editor
.snapshot(cx)
.text_highlight_ranges::<HoveredLinkState>()
.unwrap_or_default()
.1
.is_empty());
});
// Does not open the directory
cx.simulate_click(screen_coord, Modifiers::secondary_key());
cx.update_workspace(|workspace, cx| assert_eq!(workspace.items(cx).count(), 1));
}
}

View File

@@ -1,7 +1,6 @@
use crate::{
display_map::{InlayOffset, ToDisplayPoint},
display_map::{invisibles::is_invisible, InlayOffset, ToDisplayPoint},
hover_links::{InlayHighlight, RangeInEditor},
is_invisible,
scroll::ScrollAmount,
Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings, EditorSnapshot,
Hover, RangeToAnchorExt,
@@ -200,6 +199,7 @@ fn show_hover(
if editor.pending_rename.is_some() {
return None;
}
let snapshot = editor.snapshot(cx);
let (buffer, buffer_position) = editor
@@ -280,7 +280,6 @@ fn show_hover(
range: entry.range.to_anchors(&snapshot.buffer_snapshot),
})
});
if let Some(invisible) = snapshot
.buffer_snapshot
.chars_at(anchor)
@@ -324,6 +323,7 @@ fn show_hover(
}
None => local_diagnostic.diagnostic.message.clone(),
};
let mut border_color: Option<Hsla> = None;
let mut background_color: Option<Hsla> = None;
@@ -379,6 +379,7 @@ fn show_hover(
Markdown::new_text(text, markdown_style.clone(), None, cx, None)
})
.ok();
Some(DiagnosticPopover {
local_diagnostic,
primary_diagnostic,
@@ -466,6 +467,7 @@ fn show_hover(
cx.notify();
cx.refresh();
})?;
anyhow::Ok(())
}
.log_err()

View File

@@ -16,10 +16,10 @@ use util::RangeExt;
use workspace::Item;
use crate::{
editor_settings::CurrentLineHighlight, hunk_status, hunks_for_selections, ApplyDiffHunk,
BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight, DisplayRow,
DisplaySnapshot, Editor, EditorElement, ExpandAllHunkDiffs, GoToHunk, GoToPrevHunk, RevertFile,
RevertSelectedHunks, ToDisplayPoint, ToggleHunkDiff,
editor_settings::CurrentLineHighlight, hunk_status, hunks_for_selections, ApplyAllDiffHunks,
ApplyDiffHunk, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight,
DisplayRow, DisplaySnapshot, Editor, EditorElement, ExpandAllHunkDiffs, GoToHunk, GoToPrevHunk,
RevertFile, RevertSelectedHunks, ToDisplayPoint, ToggleHunkDiff,
};
#[derive(Debug, Clone)]
@@ -352,7 +352,11 @@ impl Editor {
None
}
pub(crate) fn apply_all_diff_hunks(&mut self, cx: &mut ViewContext<Self>) {
pub(crate) fn apply_all_diff_hunks(
&mut self,
_: &ApplyAllDiffHunks,
cx: &mut ViewContext<Self>,
) {
let buffers = self.buffer.read(cx).all_buffers();
for branch_buffer in buffers {
branch_buffer.update(cx, |branch_buffer, cx| {

View File

@@ -41,9 +41,9 @@ pub(super) fn refresh_linked_ranges(this: &mut Editor, cx: &mut ViewContext<Edit
return None;
}
let project = this.project.clone()?;
let selections = this.selections.all::<usize>(cx);
let buffer = this.buffer.read(cx);
let mut applicable_selections = vec![];
let selections = this.selections.all::<usize>(cx);
let snapshot = buffer.snapshot(cx);
for selection in selections {
let cursor_position = selection.head();

View File

@@ -76,6 +76,26 @@ pub fn saturating_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> Displ
map.clip_point(point, Bias::Right)
}
/// Returns a display point for the preceding displayed line (which might be a soft-wrapped line).
pub fn up2(
map: &DisplaySnapshot,
start: DisplayPoint,
goal: SelectionGoal,
preserve_column_at_start: bool,
skip_replace_blocks: bool,
text_layout_details: &TextLayoutDetails,
) -> (DisplayPoint, SelectionGoal) {
up_by_rows2(
map,
start,
1,
goal,
preserve_column_at_start,
skip_replace_blocks,
text_layout_details,
)
}
/// Returns a display point for the preceding displayed line (which might be a soft-wrapped line).
pub fn up(
map: &DisplaySnapshot,
@@ -112,6 +132,26 @@ pub fn down(
)
}
/// Returns a display point for the next displayed line (which might be a soft-wrapped line).
pub fn down2(
map: &DisplaySnapshot,
start: DisplayPoint,
goal: SelectionGoal,
preserve_column_at_end: bool,
skip_replace_blocks: bool,
text_layout_details: &TextLayoutDetails,
) -> (DisplayPoint, SelectionGoal) {
down_by_rows2(
map,
start,
1,
goal,
preserve_column_at_end,
skip_replace_blocks,
text_layout_details,
)
}
pub(crate) fn up_by_rows(
map: &DisplaySnapshot,
start: DisplayPoint,
@@ -151,6 +191,46 @@ pub(crate) fn up_by_rows(
)
}
pub(crate) fn up_by_rows2(
map: &DisplaySnapshot,
start: DisplayPoint,
row_count: u32,
goal: SelectionGoal,
preserve_column_at_start: bool,
skip_replace_blocks: bool,
text_layout_details: &TextLayoutDetails,
) -> (DisplayPoint, SelectionGoal) {
let mut goal_x = match goal {
SelectionGoal::HorizontalPosition(x) => x.into(),
SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
SelectionGoal::HorizontalRange { end, .. } => end.into(),
_ => map.x_for_display_point(start, text_layout_details),
};
let prev_row = DisplayRow(start.row().0.saturating_sub(row_count));
let mut point = map.clip_point(
DisplayPoint::new(prev_row, map.line_len(prev_row)),
Bias::Left,
);
if point.row() < start.row() {
*point.column_mut() = map.display_column_for_x(point.row(), goal_x, text_layout_details)
} else if preserve_column_at_start {
return (start, goal);
} else {
point = DisplayPoint::new(DisplayRow(0), 0);
goal_x = px(0.);
}
let mut clipped_point = map.clip_point_2(point, Bias::Left, skip_replace_blocks);
if clipped_point.row() < point.row() {
clipped_point = map.clip_point_2(point, Bias::Right, skip_replace_blocks);
}
(
clipped_point,
SelectionGoal::HorizontalPosition(goal_x.into()),
)
}
pub(crate) fn down_by_rows(
map: &DisplaySnapshot,
start: DisplayPoint,
@@ -187,6 +267,43 @@ pub(crate) fn down_by_rows(
)
}
pub(crate) fn down_by_rows2(
map: &DisplaySnapshot,
start: DisplayPoint,
row_count: u32,
goal: SelectionGoal,
preserve_column_at_end: bool,
skip_replace_blocks: bool,
text_layout_details: &TextLayoutDetails,
) -> (DisplayPoint, SelectionGoal) {
let mut goal_x = match goal {
SelectionGoal::HorizontalPosition(x) => x.into(),
SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
SelectionGoal::HorizontalRange { end, .. } => end.into(),
_ => map.x_for_display_point(start, text_layout_details),
};
let new_row = DisplayRow(start.row().0 + row_count);
let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right);
if point.row() > start.row() {
*point.column_mut() = map.display_column_for_x(point.row(), goal_x, text_layout_details)
} else if preserve_column_at_end {
return (start, goal);
} else {
point = map.max_point();
goal_x = map.x_for_display_point(point, text_layout_details)
}
let mut clipped_point = map.clip_point_2(point, Bias::Right, skip_replace_blocks);
if clipped_point.row() > point.row() {
clipped_point = map.clip_point_2(point, Bias::Left, skip_replace_blocks);
}
(
clipped_point,
SelectionGoal::HorizontalPosition(goal_x.into()),
)
}
/// Returns a position of the start of line.
/// If `stop_at_soft_boundaries` is true, the returned position is that of the
/// displayed line (e.g. it could actually be in the middle of a text line if that line is soft-wrapped).

View File

@@ -1,4 +1,4 @@
use crate::{Editor, EditorEvent, SemanticsProvider};
use crate::{ApplyAllDiffHunks, Editor, EditorEvent, SemanticsProvider};
use collections::HashSet;
use futures::{channel::mpsc, future::join_all};
use gpui::{AppContext, EventEmitter, FocusableView, Model, Render, Subscription, Task, View};
@@ -8,7 +8,7 @@ use project::Project;
use smol::stream::StreamExt;
use std::{any::TypeId, ops::Range, rc::Rc, time::Duration};
use text::ToOffset;
use ui::prelude::*;
use ui::{prelude::*, ButtonLike, KeyBinding};
use workspace::{
searchable::SearchableItemHandle, Item, ItemHandle as _, ToolbarItemEvent, ToolbarItemLocation,
ToolbarItemView, Workspace,
@@ -232,7 +232,10 @@ impl ProposedChangesEditor {
impl Render for ProposedChangesEditor {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
self.editor.clone()
div()
.size_full()
.key_context("ProposedChangesEditor")
.child(self.editor.clone())
}
}
@@ -331,17 +334,21 @@ impl ProposedChangesEditorToolbar {
}
impl Render for ProposedChangesEditorToolbar {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
let editor = self.current_editor.clone();
Button::new("apply-changes", "Apply All").on_click(move |_, cx| {
if let Some(editor) = &editor {
editor.update(cx, |editor, cx| {
editor.editor.update(cx, |editor, cx| {
editor.apply_all_diff_hunks(cx);
})
});
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let button_like = ButtonLike::new("apply-changes").child(Label::new("Apply All"));
match &self.current_editor {
Some(editor) => {
let focus_handle = editor.focus_handle(cx);
let keybinding = KeyBinding::for_action_in(&ApplyAllDiffHunks, &focus_handle, cx)
.map(|binding| binding.into_any_element());
button_like.children(keybinding).on_click({
move |_event, cx| focus_handle.dispatch_action(&ApplyAllDiffHunks, cx)
})
}
})
None => button_like.disabled(true),
}
}
}

View File

@@ -8,14 +8,14 @@ use std::{
use collections::HashMap;
use gpui::{AppContext, Model, Pixels};
use itertools::Itertools;
use language::{Bias, Point, Selection, SelectionGoal, TextDimension, ToPoint};
use language::{Bias, Point, Selection, SelectionGoal, TextDimension};
use util::post_inc;
use crate::{
display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
movement::TextLayoutDetails,
Anchor, DisplayPoint, DisplayRow, ExcerptId, MultiBuffer, MultiBufferSnapshot, SelectMode,
ToOffset,
ToOffset, ToPoint,
};
#[derive(Debug, Clone)]
@@ -96,7 +96,7 @@ impl SelectionsCollection {
pub fn pending<D: TextDimension + Ord + Sub<D, Output = D>>(
&self,
cx: &AppContext,
cx: &mut AppContext,
) -> Option<Selection<D>> {
self.pending_anchor()
.as_ref()
@@ -107,7 +107,7 @@ impl SelectionsCollection {
self.pending.as_ref().map(|pending| pending.mode.clone())
}
pub fn all<'a, D>(&self, cx: &AppContext) -> Vec<Selection<D>>
pub fn all<'a, D>(&self, cx: &mut AppContext) -> Vec<Selection<D>>
where
D: 'a + TextDimension + Ord + Sub<D, Output = D>,
{
@@ -194,7 +194,7 @@ impl SelectionsCollection {
pub fn disjoint_in_range<'a, D>(
&self,
range: Range<Anchor>,
cx: &AppContext,
cx: &mut AppContext,
) -> Vec<Selection<D>>
where
D: 'a + TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug,
@@ -239,9 +239,10 @@ impl SelectionsCollection {
pub fn newest<D: TextDimension + Ord + Sub<D, Output = D>>(
&self,
cx: &AppContext,
cx: &mut AppContext,
) -> Selection<D> {
resolve(self.newest_anchor(), &self.buffer(cx))
let buffer = self.buffer(cx);
self.newest_anchor().map(|p| p.summary::<D>(&buffer))
}
pub fn newest_display(&self, cx: &mut AppContext) -> Selection<DisplayPoint> {
@@ -262,9 +263,10 @@ impl SelectionsCollection {
pub fn oldest<D: TextDimension + Ord + Sub<D, Output = D>>(
&self,
cx: &AppContext,
cx: &mut AppContext,
) -> Selection<D> {
resolve(self.oldest_anchor(), &self.buffer(cx))
let buffer = self.buffer(cx);
self.oldest_anchor().map(|p| p.summary::<D>(&buffer))
}
pub fn first_anchor(&self) -> Selection<Anchor> {
@@ -276,14 +278,14 @@ impl SelectionsCollection {
pub fn first<D: TextDimension + Ord + Sub<D, Output = D>>(
&self,
cx: &AppContext,
cx: &mut AppContext,
) -> Selection<D> {
self.all(cx).first().unwrap().clone()
}
pub fn last<D: TextDimension + Ord + Sub<D, Output = D>>(
&self,
cx: &AppContext,
cx: &mut AppContext,
) -> Selection<D> {
self.all(cx).last().unwrap().clone()
}
@@ -298,7 +300,7 @@ impl SelectionsCollection {
#[cfg(any(test, feature = "test-support"))]
pub fn ranges<D: TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug>(
&self,
cx: &AppContext,
cx: &mut AppContext,
) -> Vec<Range<D>> {
self.all::<D>(cx)
.iter()
@@ -475,7 +477,7 @@ impl<'a> MutableSelectionsCollection<'a> {
where
T: 'a + ToOffset + ToPoint + TextDimension + Ord + Sub<T, Output = T> + std::marker::Copy,
{
let mut selections = self.all(self.cx);
let mut selections = self.collection.all(self.cx);
let mut start = range.start.to_offset(&self.buffer());
let mut end = range.end.to_offset(&self.buffer());
let reversed = if start > end {
@@ -649,6 +651,7 @@ impl<'a> MutableSelectionsCollection<'a> {
let mut changed = false;
let display_map = self.display_map();
let selections = self
.collection
.all::<Point>(self.cx)
.into_iter()
.map(|selection| {
@@ -676,6 +679,7 @@ impl<'a> MutableSelectionsCollection<'a> {
let mut changed = false;
let snapshot = self.buffer().clone();
let selections = self
.collection
.all::<usize>(self.cx)
.into_iter()
.map(|selection| {
@@ -869,10 +873,3 @@ where
goal: s.goal,
})
}
fn resolve<D: TextDimension + Ord + Sub<D, Output = D>>(
selection: &Selection<Anchor>,
buffer: &MultiBufferSnapshot,
) -> Selection<D> {
selection.map(|p| p.summary::<D>(buffer))
}

View File

@@ -1,6 +1,7 @@
use std::{
borrow::Cow,
ops::{Deref, DerefMut, Range},
path::Path,
sync::Arc,
};
@@ -66,10 +67,12 @@ impl EditorLspTestContext {
);
language_registry.add(Arc::new(language));
let root = Self::root_path();
app_state
.fs
.as_fake()
.insert_tree("/root", json!({ "dir": { file_name.clone(): "" }}))
.insert_tree(root, json!({ "dir": { file_name.clone(): "" }}))
.await;
let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
@@ -79,7 +82,7 @@ impl EditorLspTestContext {
let mut cx = VisualTestContext::from_window(*window.deref(), cx);
project
.update(&mut cx, |project, cx| {
project.find_or_create_worktree("/root", true, cx)
project.find_or_create_worktree(root, true, cx)
})
.await
.unwrap();
@@ -108,7 +111,7 @@ impl EditorLspTestContext {
},
lsp,
workspace,
buffer_lsp_url: lsp::Url::from_file_path(format!("/root/dir/{file_name}")).unwrap(),
buffer_lsp_url: lsp::Url::from_file_path(root.join("dir").join(file_name)).unwrap(),
}
}
@@ -123,6 +126,7 @@ impl EditorLspTestContext {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
line_comments: vec!["// ".into(), "/// ".into(), "//! ".into()],
..Default::default()
},
Some(tree_sitter_rust::LANGUAGE.into()),
@@ -309,6 +313,16 @@ impl EditorLspTestContext {
pub fn notify<T: notification::Notification>(&self, params: T::Params) {
self.lsp.notify::<T>(params);
}
#[cfg(target_os = "windows")]
fn root_path() -> &'static Path {
Path::new("C:\\root")
}
#[cfg(not(target_os = "windows"))]
fn root_path() -> &'static Path {
Path::new("/root")
}
}
impl Deref for EditorLspTestContext {

View File

@@ -17,6 +17,7 @@ use project::{FakeFs, Project};
use std::{
any::TypeId,
ops::{Deref, DerefMut, Range},
path::Path,
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
@@ -42,17 +43,18 @@ impl EditorTestContext {
pub async fn new(cx: &mut gpui::TestAppContext) -> EditorTestContext {
let fs = FakeFs::new(cx.executor());
// fs.insert_file("/file", "".to_owned()).await;
let root = Self::root_path();
fs.insert_tree(
"/root",
root,
serde_json::json!({
"file": "",
}),
)
.await;
let project = Project::test(fs, ["/root".as_ref()], cx).await;
let project = Project::test(fs, [root], cx).await;
let buffer = project
.update(cx, |project, cx| {
project.open_local_buffer("/root/file", cx)
project.open_local_buffer(root.join("file"), cx)
})
.await
.unwrap();
@@ -71,6 +73,16 @@ impl EditorTestContext {
}
}
#[cfg(target_os = "windows")]
fn root_path() -> &'static Path {
Path::new("C:\\root")
}
#[cfg(not(target_os = "windows"))]
fn root_path() -> &'static Path {
Path::new("/root")
}
pub async fn for_editor(editor: WindowHandle<Editor>, cx: &mut gpui::TestAppContext) -> Self {
let editor_view = editor.root_view(cx).unwrap();
Self {

View File

@@ -8,7 +8,8 @@ use collections::HashMap;
use futures::{Future, FutureExt};
use gpui::AsyncAppContext;
use language::{
CodeLabel, HighlightId, Language, LanguageServerName, LspAdapter, LspAdapterDelegate,
CodeLabel, HighlightId, Language, LanguageServerName, LanguageToolchainStore, LspAdapter,
LspAdapterDelegate,
};
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions};
use serde::Serialize;
@@ -194,6 +195,7 @@ impl LspAdapter for ExtensionLspAdapter {
async fn workspace_configuration(
self: Arc<Self>,
delegate: &Arc<dyn LspAdapterDelegate>,
_: Arc<dyn LanguageToolchainStore>,
_cx: &mut AsyncAppContext,
) -> Result<Value> {
let delegate = delegate.clone();

View File

@@ -37,7 +37,7 @@ use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
use indexed_docs::{IndexedDocsRegistry, ProviderId};
use language::{
LanguageConfig, LanguageMatcher, LanguageName, LanguageQueries, LanguageRegistry,
QUERY_FILENAME_PREFIXES,
LoadedLanguage, QUERY_FILENAME_PREFIXES,
};
use node_runtime::NodeRuntime;
use project::ContextProviderWithTasks;
@@ -1102,14 +1102,21 @@ impl ExtensionStore {
let config = std::fs::read_to_string(language_path.join("config.toml"))?;
let config: LanguageConfig = ::toml::from_str(&config)?;
let queries = load_plugin_queries(&language_path);
let tasks = std::fs::read_to_string(language_path.join("tasks.json"))
.ok()
.and_then(|contents| {
let definitions = serde_json_lenient::from_str(&contents).log_err()?;
Some(Arc::new(ContextProviderWithTasks::new(definitions)) as Arc<_>)
});
let context_provider =
std::fs::read_to_string(language_path.join("tasks.json"))
.ok()
.and_then(|contents| {
let definitions =
serde_json_lenient::from_str(&contents).log_err()?;
Some(Arc::new(ContextProviderWithTasks::new(definitions)) as Arc<_>)
});
Ok((config, queries, tasks))
Ok(LoadedLanguage {
config,
queries,
context_provider,
toolchain_provider: None,
})
},
);
}

View File

@@ -59,6 +59,12 @@ impl FeatureFlag for ZedPro {
const NAME: &'static str = "zed-pro";
}
pub struct NotebookFeatureFlag;
impl FeatureFlag for NotebookFeatureFlag {
const NAME: &'static str = "notebooks";
}
pub struct AutoCommand {}
impl FeatureFlag for AutoCommand {
const NAME: &'static str = "auto-command";

View File

@@ -790,9 +790,9 @@ impl FileFinderDelegate {
let mut path_matches = Vec::new();
let abs_file_exists = if let Ok(task) = project.update(&mut cx, |this, cx| {
this.abs_file_path_exists(query.path_query(), cx)
this.resolve_abs_file_path(query.path_query(), cx)
}) {
task.await
task.await.is_some()
} else {
false
};

View File

@@ -4,7 +4,7 @@ use gpui::{HighlightStyle, Model, StyledText};
use picker::{Picker, PickerDelegate};
use project::{Entry, PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
use std::{
path::PathBuf,
path::{Path, PathBuf},
sync::{
atomic::{self, AtomicBool},
Arc,
@@ -254,6 +254,7 @@ impl PickerDelegate for NewPathDelegate {
.trim()
.trim_start_matches("./")
.trim_start_matches('/');
let (dir, suffix) = if let Some(index) = query.rfind('/') {
let suffix = if index + 1 < query.len() {
Some(query[index + 1..].to_string())
@@ -317,6 +318,14 @@ impl PickerDelegate for NewPathDelegate {
})
}
fn confirm_completion(
&mut self,
_: String,
cx: &mut ViewContext<Picker<Self>>,
) -> Option<String> {
self.confirm_update_query(cx)
}
fn confirm_update_query(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<String> {
let m = self.matches.get(self.selected_index)?;
if m.is_dir(self.project.read(cx), cx) {
@@ -422,7 +431,32 @@ impl NewPathDelegate {
) {
cx.notify();
if query.is_empty() {
self.matches = vec![];
self.matches = self
.project
.read(cx)
.worktrees(cx)
.flat_map(|worktree| {
let worktree_id = worktree.read(cx).id();
worktree
.read(cx)
.child_entries(Path::new(""))
.filter_map(move |entry| {
entry.is_dir().then(|| Match {
path_match: Some(PathMatch {
score: 1.0,
positions: Default::default(),
worktree_id: worktree_id.to_usize(),
path: entry.path.clone(),
path_prefix: "".into(),
is_dir: entry.is_dir(),
distance_to_relative_ancestor: 0,
}),
suffix: None,
})
})
})
.collect();
return;
}

View File

@@ -220,7 +220,11 @@ impl PickerDelegate for OpenPathDelegate {
})
}
fn confirm_completion(&self, query: String) -> Option<String> {
fn confirm_completion(
&mut self,
query: String,
_: &mut ViewContext<Picker<Self>>,
) -> Option<String> {
Some(
maybe!({
let m = self.matches.get(self.selected_index)?;

View File

@@ -813,6 +813,7 @@ struct FakeFsState {
root: Arc<Mutex<FakeFsEntry>>,
next_inode: u64,
next_mtime: SystemTime,
git_event_tx: smol::channel::Sender<PathBuf>,
event_txs: Vec<smol::channel::Sender<Vec<PathEvent>>>,
events_paused: bool,
buffered_events: Vec<PathEvent>,
@@ -865,14 +866,22 @@ impl FakeFsState {
let mut entry_stack = Vec::new();
'outer: loop {
let mut path_components = path.components().peekable();
let mut prefix = None;
while let Some(component) = path_components.next() {
match component {
Component::Prefix(_) => panic!("prefix paths aren't supported"),
Component::Prefix(prefix_component) => prefix = Some(prefix_component),
Component::RootDir => {
entry_stack.clear();
entry_stack.push(self.root.clone());
canonical_path.clear();
canonical_path.push("/");
match prefix {
Some(prefix_component) => {
canonical_path = PathBuf::from(prefix_component.as_os_str());
// Prefixes like `C:\\` are represented without their trailing slash, so we have to re-add it.
canonical_path.push(std::path::MAIN_SEPARATOR_STR);
}
None => canonical_path = PathBuf::from(std::path::MAIN_SEPARATOR_STR),
}
}
Component::CurDir => {}
Component::ParentDir => {
@@ -894,7 +903,7 @@ impl FakeFsState {
}
}
entry_stack.push(entry.clone());
canonical_path.push(name);
canonical_path = canonical_path.join(name);
} else {
return None;
}
@@ -956,9 +965,15 @@ pub static FS_DOT_GIT: std::sync::LazyLock<&'static OsStr> =
#[cfg(any(test, feature = "test-support"))]
impl FakeFs {
/// We need to use something large enough for Windows and Unix to consider this a new file.
/// https://doc.rust-lang.org/nightly/std/time/struct.SystemTime.html#platform-specific-behavior
const SYSTEMTIME_INTERVAL: u64 = 100;
pub fn new(executor: gpui::BackgroundExecutor) -> Arc<Self> {
Arc::new(Self {
executor,
let (tx, mut rx) = smol::channel::bounded::<PathBuf>(10);
let this = Arc::new(Self {
executor: executor.clone(),
state: Mutex::new(FakeFsState {
root: Arc::new(Mutex::new(FakeFsEntry::Dir {
inode: 0,
@@ -967,6 +982,7 @@ impl FakeFs {
entries: Default::default(),
git_repo_state: None,
})),
git_event_tx: tx,
next_mtime: SystemTime::UNIX_EPOCH,
next_inode: 1,
event_txs: Default::default(),
@@ -975,7 +991,22 @@ impl FakeFs {
read_dir_call_count: 0,
metadata_call_count: 0,
}),
})
});
executor.spawn({
let this = this.clone();
async move {
while let Some(git_event) = rx.next().await {
if let Some(mut state) = this.state.try_lock() {
state.emit_event([(git_event, None)]);
} else {
panic!("Failed to lock file system state, this execution would have caused a test hang");
}
}
}
}).detach();
this
}
pub fn set_next_mtime(&self, next_mtime: SystemTime) {
@@ -989,7 +1020,7 @@ impl FakeFs {
let new_mtime = state.next_mtime;
let new_inode = state.next_inode;
state.next_inode += 1;
state.next_mtime += Duration::from_nanos(1);
state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL);
state
.write_path(path, move |entry| {
match entry {
@@ -1042,7 +1073,7 @@ impl FakeFs {
let inode = state.next_inode;
let mtime = state.next_mtime;
state.next_inode += 1;
state.next_mtime += Duration::from_nanos(1);
state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL);
let file = Arc::new(Mutex::new(FakeFsEntry::File {
inode,
mtime,
@@ -1169,7 +1200,12 @@ impl FakeFs {
let mut entry = entry.lock();
if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
let repo_state = git_repo_state.get_or_insert_with(Default::default);
let repo_state = git_repo_state.get_or_insert_with(|| {
Arc::new(Mutex::new(FakeGitRepositoryState::new(
dot_git.to_path_buf(),
state.git_event_tx.clone(),
)))
});
let mut repo_state = repo_state.lock();
f(&mut repo_state);
@@ -1184,7 +1220,22 @@ impl FakeFs {
pub fn set_branch_name(&self, dot_git: &Path, branch: Option<impl Into<String>>) {
self.with_git_state(dot_git, true, |state| {
state.branch_name = branch.map(Into::into)
let branch = branch.map(Into::into);
state.branches.extend(branch.clone());
state.current_branch_name = branch.map(Into::into)
})
}
pub fn insert_branches(&self, dot_git: &Path, branches: &[&str]) {
self.with_git_state(dot_git, true, |state| {
if let Some(first) = branches.first() {
if state.current_branch_name.is_none() {
state.current_branch_name = Some(first.to_string())
}
}
state
.branches
.extend(branches.iter().map(ToString::to_string));
})
}
@@ -1384,15 +1435,16 @@ impl Fs for FakeFs {
let mut created_dirs = Vec::new();
let mut cur_path = PathBuf::new();
for component in path.components() {
let mut state = self.state.lock();
let should_skip = matches!(component, Component::Prefix(..) | Component::RootDir);
cur_path.push(component);
if cur_path == Path::new("/") {
if should_skip {
continue;
}
let mut state = self.state.lock();
let inode = state.next_inode;
let mtime = state.next_mtime;
state.next_mtime += Duration::from_nanos(1);
state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL);
state.next_inode += 1;
state.write_path(&cur_path, |entry| {
entry.or_insert_with(|| {
@@ -1418,7 +1470,7 @@ impl Fs for FakeFs {
let mut state = self.state.lock();
let inode = state.next_inode;
let mtime = state.next_mtime;
state.next_mtime += Duration::from_nanos(1);
state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL);
state.next_inode += 1;
let file = Arc::new(Mutex::new(FakeFsEntry::File {
inode,
@@ -1553,7 +1605,7 @@ impl Fs for FakeFs {
let mut state = self.state.lock();
let mtime = state.next_mtime;
let inode = util::post_inc(&mut state.next_inode);
state.next_mtime += Duration::from_nanos(1);
state.next_mtime += Duration::from_nanos(Self::SYSTEMTIME_INTERVAL);
let source_entry = state.read_path(&source)?;
let content = source_entry.lock().file_content(&source)?.clone();
let mut kind = Some(PathEventKind::Created);
@@ -1823,7 +1875,12 @@ impl Fs for FakeFs {
let mut entry = entry.lock();
if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
let state = git_repo_state
.get_or_insert_with(|| Arc::new(Mutex::new(FakeGitRepositoryState::default())))
.get_or_insert_with(|| {
Arc::new(Mutex::new(FakeGitRepositoryState::new(
abs_dot_git.to_path_buf(),
state.git_event_tx.clone(),
)))
})
.clone();
Some(git::repository::FakeGitRepository::open(state))
} else {

View File

@@ -1,4 +1,10 @@
pub mod blame;
pub mod commit;
pub mod diff;
mod hosting_provider;
mod remote;
pub mod repository;
pub mod status;
use anyhow::{anyhow, Context, Result};
use serde::{Deserialize, Serialize};
@@ -7,15 +13,9 @@ use std::fmt;
use std::str::FromStr;
use std::sync::LazyLock;
pub use git2 as libgit;
pub use crate::hosting_provider::*;
pub mod blame;
pub mod commit;
pub mod diff;
pub mod repository;
pub mod status;
pub use crate::remote::*;
pub use git2 as libgit;
pub static DOT_GIT: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new(".git"));
pub static COOKIES: LazyLock<&'static OsStr> = LazyLock::new(|| OsStr::new("cookies"));

View File

@@ -69,7 +69,7 @@ pub trait GitHostingProvider {
/// Returns a formatted range of line numbers to be placed in a permalink URL.
fn format_line_numbers(&self, start_line: u32, end_line: u32) -> String;
fn parse_remote_url<'a>(&self, url: &'a str) -> Option<ParsedGitRemote<'a>>;
fn parse_remote_url(&self, url: &str) -> Option<ParsedGitRemote>;
fn extract_pull_request(
&self,
@@ -111,6 +111,12 @@ impl GitHostingProviderRegistry {
cx.global::<GlobalGitHostingProviderRegistry>().0.clone()
}
/// Returns the global [`GitHostingProviderRegistry`], if one is set.
pub fn try_global(cx: &AppContext) -> Option<Arc<Self>> {
cx.try_global::<GlobalGitHostingProviderRegistry>()
.map(|registry| registry.0.clone())
}
/// Returns the global [`GitHostingProviderRegistry`].
///
/// Inserts a default [`GitHostingProviderRegistry`] if one does not yet exist.
@@ -153,10 +159,10 @@ impl GitHostingProviderRegistry {
}
}
#[derive(Debug)]
pub struct ParsedGitRemote<'a> {
pub owner: &'a str,
pub repo: &'a str,
#[derive(Debug, PartialEq)]
pub struct ParsedGitRemote {
pub owner: Arc<str>,
pub repo: Arc<str>,
}
pub fn parse_git_remote_url(

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