Compare commits

..

141 Commits

Author SHA1 Message Date
Conrad Irwin
51ab619a84 TEMP 2024-12-18 17:10:19 -07:00
Max Brunsfeld
848df0a887 WIP 2024-12-18 14:51:54 -08:00
Max Brunsfeld
c3734400af Start separating multi-buffer into ExcerptMap and DiffMap 2024-12-17 21:57:58 -08:00
Conrad Irwin
df81a91bf9 WIPWIPWIPWIPW 2024-12-17 15:33:38 -07:00
Conrad Irwin
7adcf903cc fix merge conflicts 2024-12-17 14:55:25 -07:00
Conrad Irwin
062b4a2dd8 Merge branch 'main' into new-diff-map 2024-12-17 14:46:59 -07:00
Nathan Sobo
81c118d67d Store focus handles in AppContext instead of Window (#22158)
Previously, each window stored its own collection of focus handles. This
meant that to create a focus handle, you needed to have access to a
Window. I'm working on a simplification to gpui's context types that
removes `WindowContext` and `ViewContext` in favor of passing a window
reference explicitly when rendering or handling events. You'll still
need a window to manipulate focus, but it will be helpful to be able to
create focus handles without a window.

cc @mgsloan 

Release Notes:

- N/A
2024-12-17 14:41:00 -07:00
Marshall Bowers
e1ca5ed836 zed: Fix formatting in workspace initialization (#22152)
This PR fixes some formatting issues in the workspace initialization
code that stemmed from certain constructs causing `rustfmt` to bail out
of the formatting.

The bulk of the content of `initialize_workspace` has been factored out
into functions, as having nested closures within closures seems to be
the primary cause of `rustfmt` being unhappy.

Release Notes:

- N/A
2024-12-17 16:34:52 -05:00
Peter Tripp
fa1b1c6aff docs: Fix indentation of JSON example lsp settings (#22162) 2024-12-17 16:09:13 -05:00
Conrad Irwin
2cca833d7b Add cmp 2024-12-17 12:50:36 -07:00
Peter Tripp
0511f9268b Add tooltip for Markdown Preview copy code button (#22057) 2024-12-17 14:46:38 -05:00
Marshall Bowers
70f82f84c6 telemetry: Fix license symlink (#22153)
This PR fixes the `LICENSE-GPL` symlink in the `telemetry` crate.

Release Notes:

- N/A
2024-12-17 14:29:04 -05:00
Conrad Irwin
0b3715ae44 Add DisplayAnchor 2024-12-17 12:25:44 -07:00
Marshall Bowers
1c4868979d Allow the use of both Assistants when in the assistant2 feature flag (#22150)
This PR makes it so both Assistant panels are visible when in the
`assistant2` feature flag.

This way folks can continue using Assistant1 if Assistant2 isn't meeting
their needs.

Right now they are shown as two different panels shown in the status bar
(although using the same icon), but this is just a temporary state until
we can surface the Assistant1 functionality in Assistant2 somehow.

Note that the inline assist will always use the Assistant2 panel.

Release Notes:

- N/A
2024-12-17 14:23:57 -05:00
Danilo Leal
c86cf2c3e1 zeta: Refine visuals for the completion popover (#22142)
Most notably, trying out a different icon and adding the "Completion"
word to the side of the provider name.

<img width="800" alt="Screenshot 2024-12-17 at 13 04 55"
src="https://github.com/user-attachments/assets/6dcaa699-f358-4242-9812-e1668f426207"
/>

Release Notes:

- N/A
2024-12-17 15:45:58 -03:00
Conrad Irwin
7425d242bc Add telemetry::event! (#22146)
CC @JosephTLyons

Release Notes:

- N/A
2024-12-17 11:39:18 -07:00
Marshall Bowers
b17f2089a2 assistant2: Sketch in directory context picker (#22148)
This PR sketches in the structure for the directory context picker.

Waiting on implementing the actual behavior until we fix the issues with
the file context picker.

Release Notes:

- N/A
2024-12-17 13:02:46 -05:00
Marshall Bowers
68e3d79847 assistant2: Use ContextKind to match on ContextPicker entries (#22143)
This PR updates the `ContextPicker` entries to match on the
`ContextKind` instead of using strings.

Release Notes:

- N/A
2024-12-17 11:43:27 -05:00
Carlos Kieliszewski
ed3e647ed7 editor: Add horizontal scrollbar (#19495)
![editor_scrollbars](https://github.com/user-attachments/assets/76c26776-8fe4-47f8-9c79-9add7d7d2151)

Closes #4427 

Release Notes:

- Added a horizontal scrollbar to the editor panel
- Added `axis` option to `scrollbar` in the Zed configuration, which can
forcefully disable either the horizontal or vertical scrollbar
- Added `horizontal_scroll_margin` equivalent to
`vertical_scroll_margin` in the Zed configuration

Rough Edges:

This feature seems mostly stable from my testing. I've been using a
development build for about a week with no issues. Any feedback would be
appreciated. There are a few things to note as well:

1. Scrolling to the lower right occasionally causes scrollbar clipping
on my end, but it isn't consistent and it isn't major. Some more testing
would definitely be a good idea. [FIXED]
2. Documentation may need to be modified
3. I added an `AxisPair` type to the `editor` crate to manage values
that have a horizontal and vertical variant. I'm not sure if that's the
optimal way to do it, but I didn't see a good alternative. The `Point`
type would technically work, but it may cause confusion.

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2024-12-17 17:24:59 +01:00
Marshall Bowers
6fa5a17586 assistant2: Only add context if it is non-empty (#22141)
This PR fixes an issue where we were attaching empty message content
even if there was no context.

Release Notes:

- N/A
2024-12-17 11:09:10 -05:00
Marshall Bowers
a6b717b97b assistant2: Add spacing between historical thread entries (#22140)
This PR adds spacing between entries in the historical threads view:

<img width="1371" alt="Screenshot 2024-12-17 at 10 51 26 AM"
src="https://github.com/user-attachments/assets/f66d69e9-eb59-4c76-b3b5-c4d60190c3cc"
/>

Release Notes:

- N/A
2024-12-17 11:03:09 -05:00
Thorsten Ball
228c89a78a Show inline documentation in menu even if documentation is disabled (#22137)
This makes inline completions show up in the completion menu even if the
user has set `"show_completion_documentation": false` in their settings,
because there is no other way to show the Zeta completion.

Follow-up to #22093

Release Notes:

- N/A

Co-authored-by: Danilo <danilo@zed.dev>
2024-12-17 14:52:08 +01:00
Danilo Leal
3978937457 Add stray visual refinements (#22125)
In different parts of the app, but all of them can be seen in the
screenshot below:

<img width="800" alt="Screenshot 2024-12-16 at 21 31 51"
src="https://github.com/user-attachments/assets/79c0ad5e-4e4c-469d-93a1-fd4e707d4aaa"
/>

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2024-12-17 09:40:16 -03:00
Thorsten Ball
95334cb0ad Show inline completions inside the completion menu if both are available (#22093)
Screenshot:

![screenshot-2024-12-17-11 53
41@2x](https://github.com/user-attachments/assets/bace3d20-7175-4833-9326-7b859166c0e8)

Demo:


https://github.com/user-attachments/assets/70197042-4785-4e45-80fd-29d12e68333f



(Note for Joseph/Peter: this supersedes
https://github.com/zed-industries/zed/pull/22069)

Release Notes:
- Changed inline completions to show up inside the normal completions in
case LSP and inline-completions are available. In that case, the inline
completion will be the first entry in the menu and can be selected with
`<tab>`.

---------

Co-authored-by: Bennet <bennet@zed.dev>
Co-authored-by: Danilo <danilo@zed.dev>
2024-12-17 13:34:25 +01:00
Kirill Bulatov
cc56ed7a88 Focus terminal when creating one after activating the terminal panel (#22133)
Follow-up of https://github.com/zed-industries/zed/pull/22082

Release Notes:

- N/A
2024-12-17 14:19:13 +02:00
lihsai0
4878b9bbed docs: Fix shortcut for select a smaller syntax node (#22135)
Release Notes:

- N/A
2024-12-17 14:13:10 +02:00
Ignat S.
e1bc48c554 Workspace move editor actions (#21760)
Closes #20205

Release Notes:

- Added `MoveItemToPane` and `MoveItemToPaneInDirection` actions

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2024-12-17 13:39:36 +02:00
Thorsten Ball
9082a006d6 Revert "vim: Don't dismiss inline completion when switching to normal mode (#22075)" (#22131)
This reverts commit 38c0aa303e from
#22075.

Release Notes:

- N/A
2024-12-17 09:24:33 +01:00
mgsloan@gmail.com
ebf6804afd Misc improvements to Bounds in gpui geometry
* Makes `dilate` and `inset` return.

* Implements `Add<Point<T>>` and `Sub<Point<T>>`.

* Makes some trait constraints more precise.
2024-12-17 00:10:10 -07:00
Michael Sloan
a062c0f1bc Improve code context menu layout position esp visual stability (#22102)
* Now decides whether the menu is above or below the target position
before rendering it. This causes its position to no longer vary
depending on the length of completions

* When the text area is height constrained (< 12) lines, now chooses the
side which has the most space. Before it would always display above if
height constrained below.

* Misc code cleanups

Release Notes:

- Improved completions menu layout to be more stable and use available
space better.
2024-12-16 23:17:36 -07:00
Conrad Irwin
2b68817ed8 Fix test_{addition,deletion,modification}_reverts 2024-12-16 23:11:16 -07:00
Michael Sloan
fc5a810408 Add Corner to geometry and make names of corner methods consistent (#22119)
Release Notes:

- N/A
2024-12-16 22:57:15 -07:00
Conrad Irwin
d1239f6cdb fix test_edits_around_expanded_deletion_hunks 2024-12-16 22:35:09 -07:00
Conrad Irwin
d600c58d3c fix test toggle diff hunks (again) 2024-12-16 22:06:41 -07:00
Marshall Bowers
3052fc2565 Use the same InlineAssist action between both assistant and assistant2 (#22126)
This PR makes it so `assistant` and `assistant2` both use the same
action for inline assist (`zed_actions::InlineAssist`).

This makes it so the keybindings to deploy the inline assist seamlessly
swap based on the feature flag without needing to rebind them.

One minor caveat: if you're using `assistant2` the action name in the
command palette will be `assistant: inline assist`.

Release Notes:

- N/A
2024-12-16 23:57:07 -05:00
Conrad Irwin
c160ce6a24 handle diff chunks with no trailing newline 2024-12-16 21:31:41 -07:00
Marshall Bowers
80431e5518 assistant2: Add keybinding to toggle ContextPicker (#22124)
This PR adds an action and associated keybinding
(<kbd>Cmd+Shift+A</kbd>) to toggle the context picker.

This allows for adding context via the keyboard.

Release Notes:

- N/A
2024-12-16 20:20:32 -05:00
Max Brunsfeld
9450de73b3 Remove more of the old DiffMap 2024-12-16 16:48:03 -08:00
Danilo Leal
28087934d1 assistant2: Add text_ellipsis to the PastThread label (#22117)
To treat the case for whenever the assistant panel gets small.

<img width="360" alt="Screenshot 2024-12-16 at 20 12 34"
src="https://github.com/user-attachments/assets/5426a5b9-9baf-41e3-a2c6-2f997378c994"
/>

Release Notes:

- N/A
2024-12-16 21:43:12 -03:00
Max Brunsfeld
5cc3bd1585 Fix collapsing of hunks when base text changes
Co-authored-by: Conrad <conrad@zed.dev>
2024-12-16 16:34:33 -08:00
Max Brunsfeld
87d50ee54c Fix handling of modified empty lines in assert_state_with_diff helper 2024-12-16 16:32:27 -08:00
Michael Sloan
5558b04223 Misc geometry cleanup (#22123)
Release Notes:

- N/A
2024-12-16 17:30:07 -07:00
Marshall Bowers
0ca0433912 assistant2: Add keybinding to toggle LanguageModelSelector (#22122)
This PR adds a keybinding to toggle the `LanguageModelSelector` in
Assistant2.

Release Notes:

- N/A
2024-12-16 19:19:24 -05:00
Marshall Bowers
d11deff3c2 ui: Round hover styles for ListItems with outlined set (#22120)
This PR makes `ListItem`s with `outlined` set use the same rounding for
their hover state to ensure that the hover background doesn't bleed
outside of the outline.

Release Notes:

- N/A
2024-12-16 18:43:16 -05:00
Marshall Bowers
8e71e46867 ui: Add text_ellipsis method to Labels (#22118)
This PR adds a `text_ellipsis` method to `Label`s.

This can be used to truncate the text with an ellipsis without needing
to wrap the `Label` in another element.

Release Notes:

- N/A
2024-12-16 18:43:01 -05:00
Max Brunsfeld
2f5ea92fe1 Fix bugs in DiffMap::sync
Co-authored-by: Conrad <conrad@zed.dev>
2024-12-16 15:30:45 -08:00
Michael Sloan
ac24f074df Use Popover and ListItem in code actions menu (#22112)
This is good for code sharing but also sets up #22102 for making
assumptions about popover y padding.

Release Notes:

- N/A
2024-12-16 16:24:38 -07:00
Marshall Bowers
ccf2a60039 assistant2: Persist model selector changes (#22116)
This PR makes the language model selector in the Assistant2 panel
persist the model changes to the settings.

Release Notes:

- N/A
2024-12-16 18:00:40 -05:00
Kirill Bulatov
db2aa0bca5 Use a proper color for the folded buffer border selection 2024-12-17 01:00:31 +02:00
Max Brunsfeld
2625719ca2 Put DiffMap before InlayMap in DisplayMap
Co-authored-by: Conrad <conrad@zed.dev>
2024-12-16 14:58:43 -08:00
Marshall Bowers
373854be46 assistant2: Uniquely identify remove buttons on ContextPills (#22115)
This PR ensures that the remove buttons on the `ContextPill`s are
uniquely identified.

Release Notes:

- N/A
2024-12-16 17:57:05 -05:00
Lukas Geiger
eb74332e96 extensions_ui: Add Cython as a suggested extension (#22053)
This suggest the [Cython
extension](https://github.com/lgeiger/zed-cython) for syntax
highlighting of Cython files.

Release Notes:

- Suggest Cython extension for syntax highlighting of `.pyx`, `.pxd` and
`.pxi` files
2024-12-16 17:54:55 -05:00
Marshall Bowers
1932c04b84 assistant2: Add ability to resize the panel (#22113)
This PR adds the ability to resize the Assistant2 panel.

Release Notes:

- N/A
2024-12-16 17:35:56 -05:00
Kirill Bulatov
97d9567188 Show a brighter border around folded blocks with selections (#22114)
Follow-up of https://github.com/zed-industries/zed/pull/22046

Properly [un]fold blocks based on the selections

<img width="1728" alt="image"
src="https://github.com/user-attachments/assets/73f319ee-3005-4a3b-95ee-4c6deb5cd0b8"
/>


Release Notes:

- N/A
2024-12-17 00:33:43 +02:00
Danilo Leal
53c8b48647 assistant2: Add stray visual adjustments (#22111)
Mostly minor tweaks to make it closer to the prototype. More to come.

| With message | Empty state |
|--------|--------|
| <img width="1420" alt="Screenshot 2024-12-16 at 18 59 40"
src="https://github.com/user-attachments/assets/5df791bf-577a-4f01-9045-80568604099f"
/> | <img width="1420" alt="Screenshot 2024-12-16 at 18 59 33"
src="https://github.com/user-attachments/assets/adbf1673-3040-4b2b-8d65-f8b38a83c1d0"
/> |

Release Notes:

- N/A
2024-12-16 19:23:42 -03:00
Conrad Irwin
88b4628c00 rewrite diff map to not depend on inlay map 2024-12-16 14:38:39 -07:00
Marshall Bowers
92fb38acb6 assistant2: Wire up context for terminal inline assist (#22108)
This PR updates up the context picker for the terminal's inline assist.

Release Notes:

- N/A

Co-authored-by: Richard <richard@zed.dev>
Co-authored-by: Agus <agus@zed.dev>
2024-12-16 16:22:16 -05:00
uncenter
84392fbc2f docs: Use preferred languages instead of deprecated language field (#22107)
Ref
https://github.com/zed-industries/zed/issues/21994#issuecomment-2545988779,
`language` is already deprecated but was still suggested in the docs
here.

Release Notes:

- N/A
2024-12-16 16:11:32 -05:00
Kirill Bulatov
91fdb5d2a9 Return back the logic for indent guides check (#22095)
Follow-up of https://github.com/zed-industries/zed/pull/22046

Release Notes:

- N/A

Co-authored-by: Bennet <bennet@zed.dev>
2024-12-16 22:53:14 +02:00
Max Brunsfeld
8127decd2d Extract logic around custom text highlights out of InlayChunks iterator (#22104)
This is a pure refactor, extracted from
https://github.com/zed-industries/zed/tree/new-diff-map

Release Notes:

- N/A

Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Agus <agus@zed.dev>
2024-12-16 12:51:17 -08:00
Marshall Bowers
4bf005ef52 assistant2: Wire up context picker with inline assist (#22106)
This PR wire up the context picker with the inline assist.

UI is not finalized.

Release Notes:

- N/A

---------

Co-authored-by: Richard <richard@zed.dev>
Co-authored-by: Agus <agus@zed.dev>
2024-12-16 15:46:28 -05:00
uncenter
082469e173 docs: Use rev instead of commit for extension grammars (#22105)
`rev` is the preferred key
2024-12-16 15:40:02 -05:00
Max Brunsfeld
241c7a6645 Extract logic around custom text highlights out of InlayChunks iterator
Co-Authored-By: Conrad <conrad@zed.dev>
Co-Authored-By: Agus <agus@zed.dev>
2024-12-16 11:56:25 -08:00
Danilo Leal
ec741d61ed assistant2: Adjust thread history list item visuals (#21998)
Most notably, adding the `outlined` property in the `ListItem`
component.

<img width="800" alt="Screenshot 2024-12-13 at 20 35 39"
src="https://github.com/user-attachments/assets/adac4463-66f9-4b5e-b1c0-93c34f068dc4"
/>

Release Notes:

- N/A
2024-12-16 16:42:09 -03:00
Nate Butler
426f94b310 git_ui: Update todos (#22100)
`todo!()` -> `TODO`

Release Notes:

- N/A
2024-12-16 13:39:40 -05:00
Max Brunsfeld
f6aa7df14f Make EditorTestContext::assert_state_with_diff use new DiffMap
Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Agus <agus@zed.dev>
2024-12-16 10:34:45 -08:00
Marshall Bowers
eff61ee764 assistant2: Remove WeakView<Workspace> optionality for inline assist (#22099)
This PR removes the optionality for the `WeakView<Workspace>` that we
pass to the inline assist.

This was always `Some` in practice, so it seems we don't need to have it
be an `Option`.

Release Notes:

- N/A
2024-12-16 13:26:11 -05:00
Marshall Bowers
caefdcd7f1 assistant2: Factor out ContextStrip (#22096)
This PR factors a `ContextStrip` view out of the `MessageEditor` so that
we can use it in other places.

Release Notes:

- N/A
2024-12-16 12:45:01 -05:00
Kirill Bulatov
ff2ad63037 Allow splitting terminal items in the central pane group (#22088)
Follow-up of https://github.com/zed-industries/zed/pull/22004
Closes https://github.com/zed-industries/zed/issues/22078

Release Notes:

- Fixed splitting terminal items in the center
2024-12-16 19:23:01 +02:00
Marshall Bowers
88f7942f11 assistant2: Add support for referencing other threads as context (#22092)
This PR adds the ability to reference other threads as context:

<img width="1159" alt="Screenshot 2024-12-16 at 11 29 54 AM"
src="https://github.com/user-attachments/assets/bb8a24ff-56d3-4406-ab8c-6657e65d8c70"
/>

<img width="1159" alt="Screenshot 2024-12-16 at 11 29 35 AM"
src="https://github.com/user-attachments/assets/7a02ebda-a2f5-40e9-9dd4-1bb029cb1c43"
/>


Release Notes:

- N/A
2024-12-16 11:50:57 -05:00
uncenter
188c55c8a6 docs: Fix context_servers key for example extension manifest (#22079)
Pretty sure this isn't meant to be kebab-case.

Release Notes:

- N/A
2024-12-16 11:00:26 -05:00
Danilo Leal
2562b488b1 Refine interaction in foldable multibuffer header (#22084)
- Ensuring that the fold button is big enough to avoid clicking on the
header as a whole (and then moving to the actual file)
- Adding tooltips to the fold button
- Refining the container structure so that the tooltip for the folder
button and the header click don't overlap
- Adding keybindings to tooltips


https://github.com/user-attachments/assets/82284b59-3025-4d6d-b916-ad4d1ecdb119

Release Notes:

- N/A
2024-12-16 12:54:06 -03:00
Kirill Bulatov
bc113e4b51 Move task centering code closer to user input (#22082)
Follow-up of https://github.com/zed-industries/zed/pull/22004 

* Reuse center terminals for tasks, when requested
* Extend task templates with `RevealTarget`, moving it from
`TaskSpawnTarget` into the core library
* Use `reveal_target` instead of `target` to avoid misinterpretations in
the task template context
* Do not expose `SpawnInTerminal` to user interface, avoid it
implementing `Serialize` and `Deserialize`
* Remove `NewCenterTask` action, extending `task::Spawn` interface
instead
* Do not require any extra unrelated parameters during task resolution,
instead, use task overrides on the resolved tasks on the modal side
* Add keybindings for opening the task modal in the
`RevealTarget::Center` mode

Release Notes:

- N/A
2024-12-16 16:15:58 +02:00
Thorsten Ball
ea012075fc Trigger completions even if inline completion is visible (#22077)
This is related to #22069 and #21858: before both of these PRs, we would
only ever show inline completions OR completions, never both at the same
time.

Now we show both at the same, but we still had this piece of logic here,
that prevented non-inline completions from showing up if there was
already an inline completion.

With this change, it's possible to get LSP completions without having to
dismiss inline completions before.

Release Notes:

- Inline completions (Copilot, Supermaven, ...) don't stop other
completions from showing up anymore. Both can now be visible at the same
time.

---------

Co-authored-by: Bennet <bennet@zed.dev>
2024-12-16 15:10:33 +01:00
Helge Mahrt
ce727fbc07 workspace: Fix doc comments (#22063)
Happened to see that the doc comments here were not correct while
implementing something else.

Release Notes:

- N/A
2024-12-16 08:27:14 -05:00
tims
62b3acee5f Project panel: Deselect entries on remaining blank space click + Remove hover color for selected entries (#22073)
Closes #22072

Clicking on the remaining space now allows a single click to deselect
all selected items. Check the issue for a preview of the current state
and how it works in VSCode.

Bonus: I found the hover color on selected items to be distracting. When
I have many entries selected and hover over them, it becomes hard to
tell if a particular entry is selected while the mouse pointer is on it.
This PR removes hover coloring for selected entries, mimicking how
VSCode handles it.

This PR:
<img
src="https://github.com/user-attachments/assets/9c4b20fc-df93-4868-b7fe-4045433e85b2"
alt="zed" width="450px" />

Release Notes:

- Clicking on empty space in the Project Panel now deselects all
selected items.
2024-12-16 14:05:54 +01:00
Thorsten Ball
38c0aa303e vim: Don't dismiss inline completion when switching to normal mode (#22075)
I'm not sure about this yet.

On one hand: it's nice that the completion doesn't just disappear when I
hit escape because I was typing and in the flow.

On the other hand: no other inline completion provider keeps the
suggestion when leaving insert mode.

I'm going to merge this so we can get it into nightly and try it out for
the next couple of days. cc @ConradIrwin

Release Notes:

- vim: Do not dismiss inline completions when leaving insert/replace
mode with `<esc>`.
2024-12-16 11:23:20 +01:00
Bennet Bo Fenner
040d9ae222 zeta: Prevent diff popover from going offscreen (#22070)
https://github.com/user-attachments/assets/4ce806f1-d790-41d0-9825-e68055281446

Release Notes:

- N/A
2024-12-16 11:22:20 +01:00
Thorsten Ball
d135ec2b73 completions: Restore tab behavior when both visible (#22069)
This reverts part of #21858 by changing how `tab` works again:

- If both, completions and inline completions, are visible, then `tab`
accepts the completion and `shif-tab` the inline completion.
- If only one of them is shown, then `tab` accepts it.

I'm not a fan of this solution, but I think it's a short-term fix that
avoids breaking people's `tab` muscle memory.

Release Notes:

- (These release notes invalidate the release notes contained in:
https://github.com/zed-industries/zed/pull/21858)
- Changed how inline completions (Copilot, Supermaven, ...) and normal
completions (from language servers) interact. Zed will now also show
inline completions when the completion menu is visible. The user can
accept the inline completion with `<shift-tab>` and the active entry in
the completion menu with `<tab>`.
2024-12-16 11:02:54 +01:00
Michael Sloan
a94afbc062 Switch from Arc/RwLock to Rc/RefCell for CodeContextMenu (#22035)
`CodeContextMenu` is always accessed on one thread, so only `Rc`s and
`Rc<RefCell<_>>` are needed. There should be tiny performance benefits
from this. The main benefit of this is that when seeing code accessing a
`RwLock` it would be reasonable to wonder whether it will block. The
only potential downside is the potential for panics due to overlapping
borrows of the RefCells. I think this is an acceptable risk because most
errors of this nature will be local or will be caught by clippy via the
check for holding a RefCell reference over an `await`.

Release Notes:

- N/A
2024-12-16 01:50:21 -07:00
Michael Sloan
7b721efe2c Stop mutating completion match state + reject fuzzy match text change (#22061)
This fixes #21837, where CompletionsMenu fuzzy match positions were
desynchronized from completion label text. The solution is to not mutate
`match_candidates` and instead offset the highlight positions in the
rendering code.

This solution requires that the fuzzy match text not change on
completion resolution. This is a property we want anyway, since fuzzy
match text changing means items unexpectedly changing position in the
menu.

What happened:

* #21521 updated completion resolution to modify labels on resolution.

- This interacted poorly with the code
[here](341e65e122/crates/editor/src/code_context_menus.rs (L604)),
where the fuzzy match results are updated to include the full label, and
the fuzzy match result positions are offset to be in the correct place.
The fuzzy mach positions were now invalid because they were based on the
old text.

* #21705 caused completion resolution to occur more frequently. Before
this only the selected item was being resolved. This caused the panic
due to invalid positions to happen much more frequently.

Closes #21837

Release Notes:

- N/A
2024-12-16 01:21:26 -07:00
Peter Tripp
18b6d142c3 docs: Suggest installing PHP to use PHP (#22058) 2024-12-16 01:21:20 -05:00
Michael Sloan
53c9af3e61 Add and use CodeLabel::filter_text() (#22054)
Release Notes:

- N/A
2024-12-15 22:24:41 -07:00
Max Brunsfeld
472fee6b62 When buffer is edited, compute DiffMap edits using deltas 2024-12-15 20:30:18 -08:00
Kirill Bulatov
af50261ae2 Allow folding buffers inside multi buffers (#22046)
Closes https://github.com/zed-industries/zed/issues/4925


https://github.com/user-attachments/assets/e7b87375-893f-41ae-a2d9-d501499e40d1


Allows to fold any buffer inside multi buffers, either by clicking the
chevron icon on the header, or by using
`editor::Fold`/`editor::UnfoldLines`/`editor::ToggleFold`/`editor::FoldAll`
and `editor::UnfoldAll` actions inside the multi buffer (those were noop
there before).

Every fold has a fake line inside it, so it's possible to navigate into
that via the keyboard and unfold it with the corresponding editor
action.

The state is synchronized with the outline panel state: any fold inside
multi buffer folds the corresponding file entry; any file entry fold
inside the outline panel folds the corresponding buffer inside the multi
buffer, any directory fold inside the outline panel folds the
corresponding buffers inside the multi buffer for each nested file entry
in the panel.


Release Notes:

- Added a possibility to fold buffers inside multi buffers

---------

Co-authored-by: Antonio Scandurra <antonio@zed.dev>
Co-authored-by: Max Brunsfeld <max@zed.dev>
Co-authored-by: Cole Miller <cole@zed.dev>
2024-12-16 00:32:07 +02:00
Michael Sloan
f64fcedabb Fix fuzzy string match invariant check (#22032)
Version in #21983 only handled out of range issues rather than utf-8
boundary issues (thanks to @s3bba for pointing this out)

Release Notes:

- N/A
2024-12-15 01:15:22 -07:00
Michael Sloan
7e6233d70f Remove an unnecessary clone in get_permalink_to_line (#22027)
Release Notes:

- N/A
2024-12-14 15:52:39 -07:00
Michael Sloan
d459f010b6 Rename GitRepository.path() to GitRepository.dot_git_dir() (#22026)
Release Notes:

- N/A
2024-12-14 15:30:56 -07:00
Michael Sloan
25970650a7 Improve StringMatchCandidate::new interface (#22011)
Release Notes:

- N/A
2024-12-14 13:35:36 -07:00
Kirill Bulatov
9daa426e93 Fix terminal pane tabs arrangement and closing (#22013)
* Fixes the inability to drag and drop terminal tabs to reorder them;
fixed incorrect terminal tab move on drag and drop into existing pane
(follow-up of https://github.com/zed-industries/zed/pull/21238)
* Fixes save dialogue appearing when on closing terminal tabs with
running tasks (follow-up of
https://github.com/zed-industries/zed/pull/21374)

Release Notes:

- Fixed terminal pane tabs arrangement and closing
2024-12-14 16:13:04 +02:00
Michael Sloan
6e1cc5dad3 Remove Task::get_ready method I added, which is unusable in practice (#22012)
Does seem like such a mechanism should be possible, but not yet sure how
to define it.

Release Notes:

- N/A
2024-12-14 03:21:41 -07:00
Michael Sloan
c5fe6ef100 Hide the implementation of Task (#22009)
The `Option<T>` within `Ready` is confusing and using `None` for it can
cause crashes. There was actually one instance of this!

Release Notes:

- N/A
2024-12-14 02:52:22 -07:00
Kirill Bulatov
1ac60289fe Fix the compilation (#22010)
https://github.com/zed-industries/zed/pull/21706 was merged after
https://github.com/zed-industries/zed/pull/22004 and the CI missed that.

Release Notes:

- N/A
2024-12-14 10:02:46 +02:00
IViktorov
cbc226597c Prefer project (worktree) tasks to language/global tasks in task::Spawn (#21706)
`Inventory::list_tasks()` in `project` crate now is ordered by task
types. Worktree tasks comes first, language tasks second and global
tasks last.
That leads to `spawn_task_with_name()` from `task_ui` crate will find
worktree task first, so it's possible to override global tasks at
project level.

* `Inventory::templates_from_settings()` splitted to
`Inventory::global_templates_from_settings()` and
`Inventory::worktree_templates_from_settings()`.

* In tests function `list_tasks()` renamed to
`list_tasks_sorted_by_last_used()`, because it call's
`Inventory::used_and_current_resolved_tasks()`. Also added
`list_tasks()` which calls `Inventory::list_tasks()`.

Closes #20987 

Release Notes:

- Fix task::Spawn to search for task name in project tasks first.
2024-12-14 09:30:48 +02:00
Aaron Feickert
ff2d20780f Add setting for hover delay (#22006)
This PR adds a new `hover_popover_delay` setting that allows the user to
specify how long to wait before showing informational hover boxes. It
defaults to the existing delay.

Release Notes:

- Added a setting to control the delay for informational hover boxes
2024-12-13 20:34:16 -08:00
Ulysse Buonomo
cd5d8b4173 settings: Add max tabs option (#18933)
Add a `max_tabs` option to the settings that ensure no more than this
amount of tabs are open in a pane. If set to `null`, there is no limit.

Closes #4784

Release Notes:

- Added a `max_tabs` option to cap the maximum number of open tabs.
2024-12-13 20:32:55 -08:00
Wes Higbee
0be7cf8ea8 Show restart transformation button after successful inline assist (#20439)
When using inline assist, after successfully generating a transformation
it's not possible to generate a new transformation. Currently, you have
to modify the prompt (i.e. add a `<SPACE>` and hit `<ENTER>`) to
regenerate.

So, I changed the restart button to be visible after a successful
transformation. And in that case I map it to a different keyboard
shortcut because `menu::Confirm` is mapped to accept the current
suggestion.

Now, I can invoke a series of transforms back to back until I get what I
want!

It might also be desired to keep the accept button visible after
modifying the prompt (before submitting it). In that case we'll need to
remap accept to an alternate key (perhaps the same alt-shift-enter I am
using for restart. That wouldn't be too insane to remember. But maybe
someone has a better idea.

I don't care what the shortcut is, I just want the ability to regenerate
without adding/deleting spaces.

## Before

**Two choices** after a suggestions is presented. Also, a great example
of why I would want to regenerate the suggestion, it left some tokens
`<rewrite_this>`!

![CleanShot 2024-12-13 at 00 34
09](https://github.com/user-attachments/assets/3c1786ca-8ec5-48e2-b3dd-64de36e61f6a)

## After

**Three choices** after a suggestion is presented, the circular icon is
for regenerate, just like you see if you modify the prompt text.
![CleanShot 2024-12-13 at 00 37
58](https://github.com/user-attachments/assets/ceda300f-0851-48bf-ad3a-be44308c734e)


## Release Notes:

- Added Restart Button to Inline Assistant When Prompt Is Unchanged

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2024-12-13 20:31:54 -08:00
Mikayla Maki
4f96706161 Add the ability for tasks to target the center pane (#22004)
Closes #20060
Closes #20720
Closes #19873
Closes #9445

Release Notes:

- Fixed a bug where tasks would be spawned with their working directory
set to a file in some cases
- Added the ability to spawn tasks in the center pane, when spawning
from a keybinding:

```json5
[
  {
    // Assuming you have a task labeled "echo hello"
    "ctrl--": [
      "task::Spawn",
      { "task_name": "echo hello", "target": "center" }
    ]
  }
]
```
2024-12-13 19:39:46 -08:00
Brian Tan
85c3aec6e7 vim: Maintain block cursor for navigating/non-modifying operators (#21502)
The cursor shape now only changes to underline for operators that modify
text (like: delete, change, yank) while maintaining block shape for
navigation operators (like: find, till).

This better matches Vim/Nvim's behavior where the cursor only changes
shape when an operator that will modify text is pending.

Release Notes:

- vim: Improved cursor shape behavior to better match Vim
2024-12-13 19:06:18 -07:00
Max Brunsfeld
093c3d3652 Add failing unit test for editing inside of an insertion hunk 2024-12-13 17:49:15 -08:00
Max Brunsfeld
1f28437c82 Sync display map immediately when updating diff state 2024-12-13 17:32:39 -08:00
Max Brunsfeld
2266b3cb96 Perform the same range expansion in has_expanded_diff_hunks_in_ranges 2024-12-13 17:26:23 -08:00
Max Brunsfeld
4369890646 Move logic for defining edits outside of recompute_transforms 2024-12-13 17:17:57 -08:00
Max Brunsfeld
82fbe27c08 Fix some problems with expanding and collapsing diff hunks
Co-authored-by: Conrad <conrad@zed.dev>
2024-12-13 16:42:07 -08:00
Marshall Bowers
901dbedf8d assistant2: Refine context pickers (#21996)
This PR adds some visual refinements to the context pickers in
Assistant2.

<img width="1159" alt="Screenshot 2024-12-13 at 5 11 24 PM"
src="https://github.com/user-attachments/assets/f85ce87f-6800-4fc2-8a10-8ec3232d30e9"
/>

<img width="1159" alt="Screenshot 2024-12-13 at 5 11 31 PM"
src="https://github.com/user-attachments/assets/9b13c76d-cb7c-4441-a855-1ec4de685e0c"
/>

Release Notes:

- N/A
2024-12-13 17:26:10 -05:00
Piotr Osiewicz
99dc85e6ec Format code/fix broken CI build (#21997)
In #21981 the CI didn't run for whatever reason. I've merged based off
of CI state alone and that led to CI breaking on main.

Release Notes:

- N/A
2024-12-13 17:25:54 -05:00
Peter Tripp
735849e201 Improve Editor::DuplicateSelection (#21976)
Improves the new `Editor::DuplicateSelection` @CharlesChen0823 added in
https://github.com/zed-industries/zed/pull/21154.

- Merge `duplicate_selection` and `duplicate_line` into single function.
- Add keyboard shortcuts to JetBrains and SublimeText keymaps.
- If the selection is empty (e.g. just a cursor) make
`Editor::DuplicateSelection` fallback to being the same as
`Editor::DuplicateLineDown`.
- Tested with multiple cursors and for multiple selections.

| Editor      | Action              | macOS       | Linux        |
| ----------- | ------------------- | ----------- | ------------ |
| VSCode      | Duplicate Selection |             |              |
| JetBrains   | Duplicate Selection | cmd-d       | ctrl-d       |
| XCode       | Duplicate           | cmd-d       | N/A          |
| SublimeText | duplicate_line      | cmd-shift-d | ctrl-shift-d |

This matches behavior of the `duplicate` functionality in all other
major editors, with one exception: other editors change the selection so
that the newly duplicated object, current Zed behavior leaves the
original selection unchanged (TODO?)
2024-12-13 16:32:33 -05:00
Silvano Cerza
06edcd18be Fix running Python commands that include paths with spaces (#21981)
This PR fixes running Python commands that include paths with spaces by
wrapping python commands and their arguments in quotation marks.

I fixed this only in Python as I noticed this while trying to run
`pytest` in Zed.

Probably this is not the best approach as it doesn't fix other languages
too, though I don't know enough about the codebase to fix it like that.
I'm not even sure if it's actually feasible right now.

I didn't add tests for this either as I couldn't really understand how
to easily to that, I tried to look at other languages but couldn't find
one that tests their `ContextProvider` directly.

Release Notes:

- Fix running Python commands that include paths with spaces

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2024-12-13 22:31:08 +01:00
Peter Tripp
e53c1a8ee3 ci: GitHub Actions Runner Cleanup (#21993)
- Only run top issues on zed-industries/zed
- Move Nightly ARM build to BuildJet
2024-12-13 16:30:53 -05:00
Joseph T. Lyons
421974f923 Use consistent casing for provider name in telemetry (#21991)
Release Notes:

- N/A
2024-12-13 16:13:47 -05:00
Conrad Irwin
41c5e45ca2 Fix bugs in DiffMap's handling of edits and diff updates 2024-12-13 12:54:08 -08:00
Marshall Bowers
c57cc35b03 assistant2: Add ability to fetch URLs as context (#21988)
This PR adds the ability to fetch URLs as context in Assistant2.

In the picker we use the search area as an input for the user to enter
the URL they wish to fetch:

<img width="1159" alt="Screenshot 2024-12-13 at 2 45 41 PM"
src="https://github.com/user-attachments/assets/b3b20648-2c22-4509-b592-d0291d25b202"
/>

<img width="1159" alt="Screenshot 2024-12-13 at 2 45 47 PM"
src="https://github.com/user-attachments/assets/7e6bab2d-2731-467f-9781-130c6e4ea5cf"
/>

Release Notes:

- N/A
2024-12-13 15:03:55 -05:00
Max Brunsfeld
243d7c6d5a WIP - Failing diff map test for hunks staying expanded on diff updates 2024-12-12 17:58:10 -08:00
Max Brunsfeld
9025fb226a Update more editor rendering to use the new DiffMap 2024-12-12 17:33:04 -08:00
Max Brunsfeld
07eb573a2c Highlight background of expanded hunks
Co-authored-by: Michael <michael@zed.dev>
2024-12-12 16:51:50 -08:00
Max Brunsfeld
b552e32b18 Replace buffer rows iterators with row info iterators that also give git status
Co-authored-by: Michael <michael@zed.dev>
2024-12-12 16:40:56 -08:00
Max Brunsfeld
9b526a4d42 Start work on providing git statuses for each line of diff map
Co-authored-by: Michael <michael@zed.dev>
2024-12-12 15:50:27 -08:00
Max Brunsfeld
cc4ecacc04 Fix dropping of ChangeSet in DiffMap
Co-authored-by: Michael <michael@zed.dev>
2024-12-12 14:51:19 -08:00
Max Brunsfeld
dc814cb3af Start integrating the new DiffMap into the editor
Co-authored-by: Conrad <conrad@zed.dev>
2024-12-12 14:26:58 -08:00
Max Brunsfeld
ad87dfe908 Get more editor tests passing
Correctly handle multiple edits to DiffMap

Co-authored-by: Conrad <conrad@zed.dev>
2024-12-12 12:52:29 -08:00
Max Brunsfeld
99edc9583c Fix DiffMap::buffer_rows to handle last empty line of buffer
Co-authored-by: Conrad <conrad@zed.dev>
2024-12-12 11:55:37 -08:00
Max Brunsfeld
c015553b1f Get assertions about DiffMap::chunks passing
Co-authored-by: Conrad <conrad@zed.dev>
2024-12-12 10:41:40 -08:00
Max Brunsfeld
e53d8bb8d2 wip 2024-12-12 09:59:37 -08:00
Conrad Irwin
6e724c1168 seek on DiffMapBufferRows 2024-12-11 23:43:29 -07:00
Conrad Irwin
a8a9177878 implement text_summary_for_range 2024-12-11 23:12:58 -07:00
Conrad Irwin
9dc5ebab85 better naming? 2024-12-11 22:13:28 -07:00
Conrad Irwin
2630dfdd23 implement clip_point 2024-12-11 22:04:12 -07:00
Conrad Irwin
20ec116932 Implement to_offset/to_point 2024-12-11 21:33:07 -07:00
Max Brunsfeld
dac0a07770 Merge branch 'main' into new-diff-map 2024-12-11 17:10:46 -08:00
Max Brunsfeld
61e42b986a Integrate DiffMap into the DisplayMap 2024-12-11 17:10:34 -08:00
Max Brunsfeld
a20715d663 Start implementing DiffMap::buffer_rows 2024-12-11 11:36:44 -08:00
Max Brunsfeld
33833533bd Get DiffMap::sync working for a basic test
Co-authored-by: Cole <cole@zed.dev>
2024-12-10 17:19:29 -08:00
Max Brunsfeld
023f9239b4 Restructure DiffMap::sync to prepare for handling buffer edits
Co-authored-by: Cole <cole@zed.dev>
2024-12-10 16:30:47 -08:00
Max Brunsfeld
946ae93f68 Compute diff map edits in a single pass through the old and new trees
Co-authored-by: Cole <cole@zed.dev>
2024-12-09 16:26:16 -08:00
Max Brunsfeld
011880c48c Start work on expanding and collapsing hunks by range
Co-authored-by: Cole <cole@zed.dev>
2024-12-09 11:55:13 -08:00
Max Brunsfeld
440b632b09 Start work on expanding and collapsing hunks 2024-12-05 17:58:53 -08:00
Max Brunsfeld
d0a8b0f8b1 Start work on returning edits from DiffMap::sync
Co-authored-by: Conrad <conrad@zed.dev>
2024-12-05 15:38:55 -08:00
Max Brunsfeld
09082d103e Implement DiffMap::set_all_hunks_expanded
Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Mikayla <mikayla@zed.dev>
2024-12-05 14:37:16 -08:00
Max Brunsfeld
90c9ad4fd6 Get DiffMapChunks working to some degree
Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Mikayla <mikayla@zed.dev>
2024-12-05 14:30:59 -08:00
Max Brunsfeld
67ffe999e5 Start work on DiffMapSnapshot::chunks
Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Cole <cole@zed.dev>
2024-12-05 12:39:04 -08:00
Max Brunsfeld
159c2239cc Stub in the structure of a DiffMap to go in the display map stack
Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Cole <cole@zed.dev>
2024-12-04 16:49:53 -08:00
206 changed files with 20317 additions and 5290 deletions

View File

@@ -8,7 +8,7 @@ on:
jobs:
update_top_ranking_issues:
runs-on: ubuntu-latest
if: github.repository_owner == 'zed-industries'
if: github.repository == 'zed-industries/zed'
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Set up uv

View File

@@ -8,7 +8,7 @@ on:
jobs:
update_top_ranking_issues:
runs-on: ubuntu-latest
if: github.repository_owner == 'zed-industries'
if: github.repository == 'zed-industries/zed'
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Set up uv

View File

@@ -140,7 +140,7 @@ jobs:
name: Create a Linux *.tar.gz bundle for ARM
if: github.repository_owner == 'zed-industries'
runs-on:
- hosted-linux-arm-1
- buildjet-16vcpu-ubuntu-2204-arm
needs: tests
env:
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}

23
Cargo.lock generated
View File

@@ -472,6 +472,8 @@ dependencies = [
"fuzzy",
"gpui",
"handlebars 4.5.0",
"html_to_markdown",
"http_client",
"indoc",
"language",
"language_model",
@@ -2545,6 +2547,7 @@ dependencies = [
"settings",
"sha2",
"smol",
"telemetry",
"telemetry_events",
"text",
"thiserror 1.0.69",
@@ -2766,7 +2769,6 @@ dependencies = [
"language",
"menu",
"notifications",
"parking_lot",
"picker",
"pretty_assertions",
"project",
@@ -2840,6 +2842,7 @@ dependencies = [
"serde",
"serde_json",
"settings",
"telemetry",
"theme",
"ui",
"util",
@@ -3937,6 +3940,7 @@ dependencies = [
"snippet",
"sum_tree",
"task",
"telemetry",
"tempfile",
"text",
"theme",
@@ -4372,6 +4376,7 @@ dependencies = [
"serde_json_lenient",
"settings",
"task",
"telemetry",
"tempfile",
"theme",
"theme_extension",
@@ -7697,11 +7702,13 @@ dependencies = [
"ctor",
"env_logger 0.11.5",
"futures 0.3.31",
"git",
"gpui",
"itertools 0.13.0",
"language",
"log",
"parking_lot",
"project",
"rand 0.8.5",
"serde",
"settings",
@@ -10398,6 +10405,7 @@ dependencies = [
"serde_json",
"settings",
"smol",
"telemetry",
"terminal",
"terminal_view",
"theme",
@@ -12633,6 +12641,7 @@ dependencies = [
"sha2",
"shellexpand 2.1.2",
"util",
"zed_actions",
]
[[package]]
@@ -12661,12 +12670,23 @@ dependencies = [
"zed_actions",
]
[[package]]
name = "telemetry"
version = "0.1.0"
dependencies = [
"futures 0.3.31",
"serde",
"serde_json",
"telemetry_events",
]
[[package]]
name = "telemetry_events"
version = "0.1.0"
dependencies = [
"semantic_version",
"serde",
"serde_json",
]
[[package]]
@@ -16105,6 +16125,7 @@ name = "zed_actions"
version = "0.1.0"
dependencies = [
"gpui",
"schemars",
"serde",
]

View File

@@ -117,6 +117,7 @@ members = [
"crates/tab_switcher",
"crates/task",
"crates/tasks_ui",
"crates/telemetry",
"crates/telemetry_events",
"crates/terminal",
"crates/terminal_view",
@@ -305,6 +306,7 @@ supermaven_api = { path = "crates/supermaven_api" }
tab_switcher = { path = "crates/tab_switcher" }
task = { path = "crates/task" }
tasks_ui = { path = "crates/tasks_ui" }
telemetry = { path = "crates/telemetry" }
telemetry_events = { path = "crates/telemetry_events" }
terminal = { path = "crates/terminal" }
terminal_view = { path = "crates/terminal_view" }

View File

@@ -1,5 +1,5 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.5 6C1.5 6.89002 1.76392 7.76004 2.25839 8.50007C2.75285 9.24009 3.45566 9.81686 4.27792 10.1575C5.10019 10.4981 6.00499 10.5872 6.87791 10.4135C7.75082 10.2399 8.55264 9.81132 9.18198 9.18198C9.81132 8.55264 10.2399 7.75082 10.4135 6.87791C10.5872 6.00499 10.4981 5.10019 10.1575 4.27792C9.81686 3.45566 9.24009 2.75285 8.50007 2.25839C7.76004 1.76392 6.89002 1.5 6 1.5C4.74198 1.50473 3.53448 1.99561 2.63 2.87L1.5 4" stroke="#919081" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1.5 1.5V4H4" stroke="#919081" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 3.5V6L8 7" stroke="#919081" stroke-linecap="round" stroke-linejoin="round"/>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 8C2 9.18669 2.35189 10.3467 3.01118 11.3334C3.67047 12.3201 4.60754 13.0892 5.7039 13.5433C6.80026 13.9974 8.00666 14.1162 9.17054 13.8847C10.3344 13.6532 11.4035 13.0818 12.2426 12.2426C13.0818 11.4035 13.6532 10.3344 13.8847 9.17054C14.1162 8.00666 13.9974 6.80026 13.5433 5.7039C13.0892 4.60754 12.3201 3.67047 11.3334 3.01118C10.3467 2.35189 9.18669 2 8 2C6.32263 2.00631 4.71265 2.66082 3.50667 3.82667L2 5.33333" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 2V5.33333H5.33333" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 5V8.5L10 9.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 778 B

After

Width:  |  Height:  |  Size: 840 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-circle-more"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/><path d="M8 12h.01"/><path d="M12 12h.01"/><path d="M16 12h.01"/></svg>

After

Width:  |  Height:  |  Size: 337 B

View File

@@ -1,3 +1,4 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.5 0.875C5.49797 0.875 3.875 2.49797 3.875 4.5C3.875 6.15288 4.98124 7.54738 6.49373 7.98351C5.2997 8.12901 4.27557 8.55134 3.50407 9.31167C2.52216 10.2794 2.02502 11.72 2.02502 13.5999C2.02502 13.8623 2.23769 14.0749 2.50002 14.0749C2.76236 14.0749 2.97502 13.8623 2.97502 13.5999C2.97502 11.8799 3.42786 10.7206 4.17091 9.9883C4.91536 9.25463 6.02674 8.87499 7.49995 8.87499C8.97317 8.87499 10.0846 9.25463 10.8291 9.98831C11.5721 10.7206 12.025 11.8799 12.025 13.5999C12.025 13.8623 12.2376 14.0749 12.5 14.0749C12.7623 14.075 12.975 13.8623 12.975 13.6C12.975 11.72 12.4778 10.2794 11.4959 9.31166C10.7244 8.55135 9.70025 8.12903 8.50625 7.98352C10.0187 7.5474 11.125 6.15289 11.125 4.5C11.125 2.49797 9.50203 0.875 7.5 0.875ZM4.825 4.5C4.825 3.02264 6.02264 1.825 7.5 1.825C8.97736 1.825 10.175 3.02264 10.175 4.5C10.175 5.97736 8.97736 7.175 7.5 7.175C6.02264 7.175 4.825 5.97736 4.825 4.5Z" fill="black"/>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6666 14V12.6667C12.6666 11.9594 12.3856 11.2811 11.8855 10.781C11.3854 10.281 10.7072 10 9.99992 10H5.99992C5.29267 10 4.6144 10.281 4.1143 10.781C3.6142 11.2811 3.33325 11.9594 3.33325 12.6667V14" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.99992 7.33333C9.47268 7.33333 10.6666 6.13943 10.6666 4.66667C10.6666 3.19391 9.47268 2 7.99992 2C6.52716 2 5.33325 3.19391 5.33325 4.66667C5.33325 6.13943 6.52716 7.33333 7.99992 7.33333Z" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 690 B

View File

@@ -1,8 +1,4 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M8 2.75C8 2.47386 7.77614 2.25 7.5 2.25C7.22386 2.25 7 2.47386 7 2.75V7H2.75C2.47386 7 2.25 7.22386 2.25 7.5C2.25 7.77614 2.47386 8 2.75 8H7V12.25C7 12.5261 7.22386 12.75 7.5 12.75C7.77614 12.75 8 12.5261 8 12.25V8H12.25C12.5261 8 12.75 7.77614 12.75 7.5C12.75 7.22386 12.5261 7 12.25 7H8V2.75Z"
fill="currentColor"
/>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.33325 8H12.6666" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 3.33333V12.6667" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 491 B

After

Width:  |  Height:  |  Size: 327 B

View File

@@ -1 +1,4 @@
<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-rotate-cw"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 6.5L9.99556 4.21778C9.27778 3.5 8.12 3 7 3C6.20888 3 5.43552 3.2346 4.77772 3.67412C4.11992 4.11365 3.60723 4.73836 3.30448 5.46927C3.00173 6.20017 2.92252 7.00444 3.07686 7.78036C3.2312 8.55628 3.61216 9.26902 4.17157 9.82842C4.73098 10.3878 5.44372 10.7688 6.21964 10.9231C6.99556 11.0775 7.79983 10.9983 8.53073 10.6955C8.88113 10.5504 9.20712 10.357 9.5 10.1225" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 4V6.5H9.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 303 B

After

Width:  |  Height:  |  Size: 673 B

View File

@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.16089 10.2476L3.99598 10.3784C4.61244 10.4749 5.05269 11.0395 5.00728 11.6755L4.94576 12.5377C4.92784 12.789 5.06165 13.0255 5.28326 13.1348L5.90091 13.4391C6.12253 13.5485 6.38717 13.5075 6.56817 13.3371L7.1888 12.7505C7.64641 12.3178 8.35245 12.3178 8.81059 12.7505L9.43121 13.3371C9.61222 13.5081 9.87629 13.5485 10.0985 13.4391L10.7173 13.1341C10.9384 13.0255 11.0716 12.7895 11.0537 12.539L10.9921 11.6755C10.9467 11.0395 11.3869 10.4749 12.0033 10.3784L12.8385 10.2476C13.0817 10.2097 13.2776 10.0233 13.3325 9.77768L13.4848 9.09455C13.5398 8.8489 13.4425 8.59408 13.2393 8.45229L12.5422 7.96404C12.0279 7.60355 11.8708 6.89963 12.1814 6.34659L12.6025 5.59745C12.7249 5.3793 12.7047 5.10616 12.5511 4.9094L12.1241 4.36128C11.9706 4.16451 11.7149 4.08325 11.4795 4.15719L10.6719 4.41016C10.0752 4.59714 9.43903 4.28367 9.20962 3.69035L8.90017 2.88803C8.80937 2.65339 8.58777 2.4994 8.34108 2.5L7.65649 2.50184C7.40979 2.50244 7.1888 2.65766 7.09921 2.89291L6.79751 3.68607C6.57053 4.28307 5.93138 4.59898 5.33284 4.41077L4.49178 4.1468C4.25583 4.07225 3.99897 4.15413 3.84545 4.35212L3.42133 4.90084C3.26781 5.09943 3.2493 5.37319 3.37414 5.59133L3.80483 6.34232C4.12201 6.89591 3.96671 7.60659 3.44941 7.96897L2.76065 8.45169C2.55756 8.59408 2.4602 8.84891 2.51516 9.09393L2.66747 9.77708C2.72184 10.0233 2.91777 10.2097 3.16089 10.2476Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.41432 6.83576C8.63332 6.05481 7.36676 6.05476 6.58575 6.83571C5.8048 7.61672 5.80476 8.88327 6.58571 9.66427C7.36671 10.4452 8.63326 10.4452 9.41426 9.66432C10.1952 8.88332 10.1952 7.61676 9.41432 6.83576Z" fill="black"/>
<path d="M8.14667 1.33334H7.85333C7.49971 1.33334 7.16057 1.47382 6.91053 1.72387C6.66048 1.97392 6.52 2.31305 6.52 2.66668V2.78668C6.51976 3.02049 6.45804 3.25014 6.34103 3.45257C6.22401 3.655 6.05583 3.8231 5.85333 3.94001L5.56667 4.10668C5.36398 4.2237 5.13405 4.28531 4.9 4.28531C4.66595 4.28531 4.43603 4.2237 4.23333 4.10668L4.13333 4.05334C3.82738 3.87685 3.46389 3.82897 3.12267 3.92022C2.78145 4.01146 2.49037 4.23437 2.31333 4.54001L2.16667 4.79334C1.99018 5.0993 1.9423 5.46279 2.03354 5.80401C2.12478 6.14523 2.34769 6.43631 2.65333 6.61334L2.75333 6.68001C2.95485 6.79635 3.12241 6.9634 3.23937 7.16456C3.35632 7.36573 3.4186 7.59399 3.42 7.82668V8.16668C3.42093 8.40162 3.35977 8.63265 3.2427 8.83635C3.12563 9.04005 2.95681 9.2092 2.75333 9.32668L2.65333 9.38668C2.34769 9.56371 2.12478 9.85479 2.03354 10.196C1.9423 10.5372 1.99018 10.9007 2.16667 11.2067L2.31333 11.46C2.49037 11.7657 2.78145 11.9886 3.12267 12.0798C3.46389 12.171 3.82738 12.1232 4.13333 11.9467L4.23333 11.8933C4.43603 11.7763 4.66595 11.7147 4.9 11.7147C5.13405 11.7147 5.36398 11.7763 5.56667 11.8933L5.85333 12.06C6.05583 12.1769 6.22401 12.345 6.34103 12.5475C6.45804 12.7499 6.51976 12.9795 6.52 13.2133V13.3333C6.52 13.687 6.66048 14.0261 6.91053 14.2762C7.16057 14.5262 7.49971 14.6667 7.85333 14.6667H8.14667C8.50029 14.6667 8.83943 14.5262 9.08948 14.2762C9.33953 14.0261 9.48 13.687 9.48 13.3333V13.2133C9.48024 12.9795 9.54196 12.7499 9.65898 12.5475C9.77599 12.345 9.94418 12.1769 10.1467 12.06L10.4333 11.8933C10.636 11.7763 10.866 11.7147 11.1 11.7147C11.3341 11.7147 11.564 11.7763 11.7667 11.8933L11.8667 11.9467C12.1726 12.1232 12.5361 12.171 12.8773 12.0798C13.2186 11.9886 13.5096 11.7657 13.6867 11.46L13.8333 11.2C14.0098 10.8941 14.0577 10.5306 13.9665 10.1893C13.8752 9.84812 13.6523 9.55704 13.3467 9.38001L13.2467 9.32668C13.0432 9.2092 12.8744 9.04005 12.7573 8.83635C12.6402 8.63265 12.5791 8.40162 12.58 8.16668V7.83334C12.5791 7.5984 12.6402 7.36738 12.7573 7.16367C12.8744 6.95997 13.0432 6.79082 13.2467 6.67334L13.3467 6.61334C13.6523 6.43631 13.8752 6.14523 13.9665 5.80401C14.0577 5.46279 14.0098 5.0993 13.8333 4.79334L13.6867 4.54001C13.5096 4.23437 13.2186 4.01146 12.8773 3.92022C12.5361 3.82897 12.1726 3.87685 11.8667 4.05334L11.7667 4.10668C11.564 4.2237 11.3341 4.28531 11.1 4.28531C10.866 4.28531 10.636 4.2237 10.4333 4.10668L10.1467 3.94001C9.94418 3.8231 9.77599 3.655 9.65898 3.45257C9.54196 3.25014 9.48024 3.02049 9.48 2.78668V2.66668C9.48 2.31305 9.33953 1.97392 9.08948 1.72387C8.83943 1.47382 8.50029 1.33334 8.14667 1.33334Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 10C9.10457 10 10 9.10457 10 8C10 6.89543 9.10457 6 8 6C6.89543 6 6 6.89543 6 8C6 9.10457 6.89543 10 8 10Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -1,4 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 8.9V11C5.93097 11 5.06903 11 3 11V10.4L8 5.6V5H3V7.1" stroke="black" stroke-width="1.5"/>
<path d="M11 5L13 8L11 11" stroke="black" stroke-width="1.5"/>
<path d="M7 8.9V11C5.34478 11 4.65522 11 3 11V10.4L7 5.6V5H3V7.1" stroke="black" stroke-width="1.5"/>
<path d="M12 5L14 8L12 11" stroke="black" stroke-width="1.5"/>
<path d="M10 6.5L11 8L10 9.5" stroke="black" stroke-width="1.5"/>
</svg>

Before

Width:  |  Height:  |  Size: 268 B

After

Width:  |  Height:  |  Size: 334 B

View File

@@ -17,6 +17,7 @@
"escape": "menu::Cancel",
"ctrl-escape": "menu::Cancel",
"ctrl-c": "menu::Cancel",
"alt-shift-enter": "menu::Restart",
"alt-enter": ["picker::ConfirmInput", { "secondary": false }],
"ctrl-alt-enter": ["picker::ConfirmInput", { "secondary": true }],
"ctrl-shift-w": "workspace::CloseWindow",
@@ -425,7 +426,10 @@
"ctrl-shift-r": "task::Rerun",
"ctrl-alt-r": "task::Rerun",
"alt-t": "task::Rerun",
"alt-shift-t": "task::Spawn"
"alt-shift-t": "task::Spawn",
"alt-shift-r": ["task::Spawn", { "reveal_target": "center" }]
// also possible to spawn tasks by name:
// "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
}
},
// Bindings from Sublime Text
@@ -470,18 +474,12 @@
"context": "Editor && showing_completions",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmCompletion"
}
},
{
"context": "Editor && !inline_completion && showing_completions",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmCompletion",
"tab": "editor::ComposeCompletion"
}
},
{
"context": "Editor && inline_completion",
"context": "Editor && inline_completion && !showing_completions",
"use_key_equivalents": true,
"bindings": {
"tab": "editor::AcceptInlineCompletion"

View File

@@ -24,6 +24,7 @@
"cmd-escape": "menu::Cancel",
"ctrl-escape": "menu::Cancel",
"ctrl-c": "menu::Cancel",
"alt-shift-enter": "menu::Restart",
"cmd-shift-w": "workspace::CloseWindow",
"shift-escape": "workspace::ToggleZoom",
"cmd-o": "workspace::Open",
@@ -223,7 +224,9 @@
"use_key_equivalents": true,
"bindings": {
"cmd-n": "assistant2::NewThread",
"cmd-shift-h": "assistant2::OpenHistory"
"cmd-shift-h": "assistant2::OpenHistory",
"cmd-shift-m": "assistant2::ToggleModelSelector",
"cmd-shift-a": "assistant2::ToggleContextPicker"
}
},
{
@@ -494,8 +497,9 @@
"bindings": {
"cmd-shift-r": "task::Spawn",
"cmd-alt-r": "task::Rerun",
"alt-t": "task::Spawn",
"alt-shift-t": "task::Spawn"
"ctrl-alt-shift-r": ["task::Spawn", { "reveal_target": "center" }]
// also possible to spawn tasks by name:
// "foo-bar": ["task_name::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
}
},
// Bindings from Sublime Text
@@ -541,18 +545,12 @@
"context": "Editor && showing_completions",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmCompletion"
}
},
{
"context": "Editor && !inline_completion && showing_completions",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmCompletion",
"tab": "editor::ComposeCompletion"
}
},
{
"context": "Editor && inline_completion",
"context": "Editor && inline_completion && !showing_completions",
"use_key_equivalents": true,
"bindings": {
"tab": "editor::AcceptInlineCompletion"
@@ -612,6 +610,7 @@
"context": "PromptEditor",
"use_key_equivalents": true,
"bindings": {
"cmd-shift-a": "assistant2::ToggleContextPicker",
"ctrl-[": "assistant::CyclePreviousInlineAssist",
"ctrl-]": "assistant::CycleNextInlineAssist"
}

View File

@@ -12,7 +12,7 @@
"ctrl->": "zed::IncreaseBufferFontSize",
"ctrl-<": "zed::DecreaseBufferFontSize",
"ctrl-shift-j": "editor::JoinLines",
"ctrl-d": "editor::DuplicateLineDown",
"ctrl-d": "editor::DuplicateSelection",
"ctrl-y": "editor::DeleteLine",
"ctrl-m": "editor::ScrollCursorCenter",
"ctrl-pagedown": "editor::MovePageDown",

View File

@@ -4,19 +4,41 @@
"ctrl-shift-[": "pane::ActivatePrevItem",
"ctrl-shift-]": "pane::ActivateNextItem",
"ctrl-pageup": "pane::ActivatePrevItem",
"ctrl-pagedown": "pane::ActivateNextItem"
"ctrl-pagedown": "pane::ActivateNextItem",
"ctrl-1": ["workspace::ActivatePane", 0],
"ctrl-2": ["workspace::ActivatePane", 1],
"ctrl-3": ["workspace::ActivatePane", 2],
"ctrl-4": ["workspace::ActivatePane", 3],
"ctrl-5": ["workspace::ActivatePane", 4],
"ctrl-6": ["workspace::ActivatePane", 5],
"ctrl-7": ["workspace::ActivatePane", 6],
"ctrl-8": ["workspace::ActivatePane", 7],
"ctrl-9": ["workspace::ActivatePane", 8],
"ctrl-shift-1": ["workspace::MoveItemToPane", { "destination": 0, "focus": true }],
"ctrl-shift-2": ["workspace::MoveItemToPane", { "destination": 1 }],
"ctrl-shift-3": ["workspace::MoveItemToPane", { "destination": 2 }],
"ctrl-shift-4": ["workspace::MoveItemToPane", { "destination": 3 }],
"ctrl-shift-5": ["workspace::MoveItemToPane", { "destination": 4 }],
"ctrl-shift-6": ["workspace::MoveItemToPane", { "destination": 5 }],
"ctrl-shift-7": ["workspace::MoveItemToPane", { "destination": 6 }],
"ctrl-shift-8": ["workspace::MoveItemToPane", { "destination": 7 }],
"ctrl-shift-9": ["workspace::MoveItemToPane", { "destination": 8 }]
}
},
{
"context": "Editor",
"bindings": {
"ctrl-alt-up": "editor::AddSelectionAbove",
"ctrl-alt-down": "editor::AddSelectionBelow",
"ctrl-shift-up": "editor::MoveLineUp",
"ctrl-shift-down": "editor::MoveLineDown",
"ctrl-shift-m": "editor::SelectLargerSyntaxNode",
"ctrl-shift-l": "editor::SplitSelectionIntoLines",
"ctrl-shift-a": "editor::SelectLargerSyntaxNode",
"ctrl-shift-d": "editor::DuplicateLineDown",
"ctrl-shift-d": "editor::DuplicateSelection",
"alt-f3": "editor::SelectAllMatches", // find_all_under
"f9": "editor::SortLinesCaseSensitive",
"ctrl-f9": "editor::SortLinesCaseInsensitive",
"f12": "editor::GoToDefinition",
"ctrl-f12": "editor::GoToDefinitionSplit",
"shift-f12": "editor::FindAllReferences",

View File

@@ -11,7 +11,7 @@
"ctrl->": "zed::IncreaseBufferFontSize",
"ctrl-<": "zed::DecreaseBufferFontSize",
"ctrl-shift-j": "editor::JoinLines",
"cmd-d": "editor::DuplicateLineDown",
"cmd-d": "editor::DuplicateSelection",
"cmd-backspace": "editor::DeleteLine",
"cmd-pagedown": "editor::MovePageDown",
"cmd-pageup": "editor::MovePageUp",

View File

@@ -4,7 +4,25 @@
"cmd-shift-[": "pane::ActivatePrevItem",
"cmd-shift-]": "pane::ActivateNextItem",
"ctrl-pageup": "pane::ActivatePrevItem",
"ctrl-pagedown": "pane::ActivateNextItem"
"ctrl-pagedown": "pane::ActivateNextItem",
"ctrl-1": ["workspace::ActivatePane", 0],
"ctrl-2": ["workspace::ActivatePane", 1],
"ctrl-3": ["workspace::ActivatePane", 2],
"ctrl-4": ["workspace::ActivatePane", 3],
"ctrl-5": ["workspace::ActivatePane", 4],
"ctrl-6": ["workspace::ActivatePane", 5],
"ctrl-7": ["workspace::ActivatePane", 6],
"ctrl-8": ["workspace::ActivatePane", 7],
"ctrl-9": ["workspace::ActivatePane", 8],
"ctrl-shift-1": ["workspace::MoveItemToPane", { "destination": 0, "focus": true }],
"ctrl-shift-2": ["workspace::MoveItemToPane", { "destination": 1 }],
"ctrl-shift-3": ["workspace::MoveItemToPane", { "destination": 2 }],
"ctrl-shift-4": ["workspace::MoveItemToPane", { "destination": 3 }],
"ctrl-shift-5": ["workspace::MoveItemToPane", { "destination": 4 }],
"ctrl-shift-6": ["workspace::MoveItemToPane", { "destination": 5 }],
"ctrl-shift-7": ["workspace::MoveItemToPane", { "destination": 6 }],
"ctrl-shift-8": ["workspace::MoveItemToPane", { "destination": 7 }],
"ctrl-shift-9": ["workspace::MoveItemToPane", { "destination": 8 }]
}
},
{
@@ -18,8 +36,10 @@
"ctrl-shift-m": "editor::SelectLargerSyntaxNode",
"cmd-shift-l": "editor::SplitSelectionIntoLines",
"cmd-shift-a": "editor::SelectLargerSyntaxNode",
"cmd-shift-d": "editor::DuplicateLineDown",
"cmd-shift-d": "editor::DuplicateSelection",
"ctrl-cmd-g": "editor::SelectAllMatches", // find_all_under
"f5": "editor::SortLinesCaseSensitive",
"ctrl-f5": "editor::SortLinesCaseInsensitive",
"shift-f12": "editor::FindAllReferences",
"alt-cmd-down": "editor::GoToDefinition",
"ctrl-alt-cmd-down": "editor::GoToDefinitionSplit",

View File

@@ -101,6 +101,8 @@
// Whether to show the informational hover box when moving the mouse
// over symbols in the editor.
"hover_popover_enabled": true,
// Time to wait before showing the informational hover box
"hover_popover_delay": 350,
// Whether to confirm before quitting Zed.
"confirm_quit": false,
// Whether to restore last closed project when fresh Zed instance is opened.
@@ -252,7 +254,14 @@
// Whether to show selected symbol occurrences in the scrollbar.
"selected_symbol": true,
// Whether to show diagnostic indicators in the scrollbar.
"diagnostics": true
"diagnostics": true,
/// Forcefully enable or disable the scrollbar for each axis
"axes": {
/// When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings.
"horizontal": true,
/// When false, forcefully disables the vertical scrollbar. Otherwise, obey other settings.
"vertical": true
}
},
// Enable middle-click paste on Linux.
"middle_click_paste": true,
@@ -302,6 +311,8 @@
"vertical_scroll_margin": 3,
// Whether to scroll when clicking near the edge of the visible text area.
"autoscroll_on_clicks": false,
// The number of characters to keep on either side when scrolling with the mouse
"horizontal_scroll_margin": 5,
// Scroll sensitivity multiplier. This multiplier is applied
// to both the horizontal and vertical delta values while scrolling.
"scroll_sensitivity": 1.0,
@@ -552,6 +563,8 @@
// 4. Save when idle for a certain amount of time:
// "autosave": { "after_delay": {"milliseconds": 500} },
"autosave": "off",
// Maximum number of tabs per pane. Unset for unlimited.
"max_tabs": null,
// Settings related to the editor's tab bar.
"tab_bar": {
// Whether or not to show the tab bar in the editor

View File

@@ -15,10 +15,14 @@
// Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish, defaults to `false`.
"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
// * `always` — always show the task's pane, and focus the corresponding tab in it (default)
// * `no_focus` — always show the task's pane, add the task's tab in it, but don't focus it
// * `never` — do not alter focus, but still add/reuse the task's tab in its pane
"reveal": "always",
// Where to place the task's terminal item after starting the task:
// * `dock` — in the terminal dock, "regular" terminal items' place (default)
// * `center` — in the central pane group, "main" editor area
"reveal_target": "dock",
// What to do with the terminal pane and tab, after the command had finished:
// * `never` — Do nothing when the command finishes (default)
// * `always` — always hide the terminal tab, hide the pane also if it was the last tab in it

View File

@@ -9,7 +9,7 @@
"style": {
"border": "#464b57ff",
"border.variant": "#363c46ff",
"border.focused": "#293b5bff",
"border.focused": "#47679eff",
"border.selected": "#293b5bff",
"border.transparent": "#00000000",
"border.disabled": "#414754ff",
@@ -384,7 +384,7 @@
"style": {
"border": "#c9c9caff",
"border.variant": "#dfdfe0ff",
"border.focused": "#cbcdf6ff",
"border.focused": "#7d82e8ff",
"border.selected": "#cbcdf6ff",
"border.transparent": "#00000000",
"border.disabled": "#d3d3d4ff",

View File

@@ -493,7 +493,7 @@ impl Render for ActivityIndicator {
}),
),
)
.anchor(gpui::AnchorCorner::BottomLeft)
.anchor(gpui::Corner::BottomLeft)
.menu(move |cx| {
let strong_this = this.upgrade()?;
let mut has_work = false;

View File

@@ -108,7 +108,6 @@ pub fn init(cx: &mut AppContext) {
workspace.toggle_panel_focus::<AssistantPanel>(cx);
})
.register_action(AssistantPanel::inline_assist)
.register_action(ContextEditor::quote_selection)
.register_action(ContextEditor::insert_selection)
.register_action(ContextEditor::copy_code)

View File

@@ -716,7 +716,7 @@ impl ContextStore {
let candidates = metadata
.iter()
.enumerate()
.map(|(id, metadata)| StringMatchCandidate::new(id, metadata.title.clone()))
.map(|(id, metadata)| StringMatchCandidate::new(id, &metadata.title))
.collect::<Vec<_>>();
let matches = fuzzy::match_strings(
&candidates,

View File

@@ -47,6 +47,7 @@ use std::{
iter, mem,
ops::{Range, RangeInclusive},
pin::Pin,
rc::Rc,
sync::Arc,
task::{self, Poll},
time::{Duration, Instant},
@@ -56,7 +57,7 @@ use terminal_view::terminal_panel::TerminalPanel;
use text::{OffsetRangeExt, ToPoint as _};
use theme::ThemeSettings;
use ui::{
prelude::*, text_for_action, ToggleWithLabel, IconButtonShape, KeyBinding, Popover, Tooltip,
prelude::*, text_for_action, CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, Tooltip,
};
use util::{RangeExt, ResultExt};
use workspace::{notifications::NotificationId, ItemHandle, Toast, Workspace};
@@ -174,7 +175,7 @@ impl InlineAssistant {
if let Some(editor) = item.act_as::<Editor>(cx) {
editor.update(cx, |editor, cx| {
editor.push_code_action_provider(
Arc::new(AssistantCodeActionProvider {
Rc::new(AssistantCodeActionProvider {
editor: cx.view().downgrade(),
workspace: workspace.downgrade(),
}),
@@ -1441,6 +1442,15 @@ impl Render for PromptEditor {
]
}
CodegenStatus::Error(_) | CodegenStatus::Done => {
let must_rerun =
self.edited_since_done || matches!(status, CodegenStatus::Error(_));
// when accept button isn't visible, then restart maps to confirm
// when accept button is visible, then restart must be mapped to an alternate keyboard shortcut
let restart_key: &dyn gpui::Action = if must_rerun {
&menu::Confirm
} else {
&menu::Restart
};
vec![
IconButton::new("cancel", IconName::Close)
.icon_color(Color::Muted)
@@ -1450,23 +1460,22 @@ impl Render for PromptEditor {
cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)),
)
.into_any_element(),
if self.edited_since_done || matches!(status, CodegenStatus::Error(_)) {
IconButton::new("restart", IconName::RotateCw)
.icon_color(Color::Info)
.shape(IconButtonShape::Square)
.tooltip(|cx| {
Tooltip::with_meta(
"Restart Transformation",
Some(&menu::Confirm),
"Changes will be discarded",
cx,
)
})
.on_click(cx.listener(|_, _, cx| {
cx.emit(PromptEditorEvent::StartRequested);
}))
.into_any_element()
} else {
IconButton::new("restart", IconName::RotateCw)
.icon_color(Color::Muted)
.shape(IconButtonShape::Square)
.tooltip(|cx| {
Tooltip::with_meta(
"Regenerate Transformation",
Some(restart_key),
"Current change will be discarded",
cx,
)
})
.on_click(cx.listener(|_, _, cx| {
cx.emit(PromptEditorEvent::StartRequested);
}))
.into_any_element(),
if !must_rerun {
IconButton::new("confirm", IconName::Check)
.icon_color(Color::Info)
.shape(IconButtonShape::Square)
@@ -1475,6 +1484,8 @@ impl Render for PromptEditor {
cx.emit(PromptEditorEvent::ConfirmRequested);
}))
.into_any_element()
} else {
div().into_any_element()
},
]
}
@@ -1491,6 +1502,7 @@ impl Render for PromptEditor {
.py(cx.line_height() / 2.5)
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::restart))
.on_action(cx.listener(Self::move_up))
.on_action(cx.listener(Self::move_down))
.capture_action(cx.listener(Self::cycle_prev))
@@ -1544,7 +1556,7 @@ impl Render for PromptEditor {
anchored()
.position_mode(gpui::AnchoredPositionMode::Local)
.position(point(px(0.), px(24.)))
.anchor(gpui::AnchorCorner::TopLeft)
.anchor(gpui::Corner::TopLeft)
.child(self.render_rate_limit_notice(cx)),
)
})),
@@ -1837,6 +1849,10 @@ impl PromptEditor {
}
}
fn restart(&mut self, _: &menu::Restart, cx: &mut ViewContext<Self>) {
cx.emit(PromptEditorEvent::StartRequested);
}
fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
match self.codegen.read(cx).status(cx) {
CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
@@ -2129,7 +2145,7 @@ impl PromptEditor {
.child(
h_flex()
.justify_between()
.child(ToggleWithLabel::new(
.child(CheckboxWithLabel::new(
"dont-show-again",
Label::new("Don't show again"),
if dismissed_rate_limit_notice() {

View File

@@ -1439,10 +1439,7 @@ impl PromptStore {
.iter()
.enumerate()
.filter_map(|(ix, metadata)| {
Some(StringMatchCandidate::new(
ix,
metadata.title.as_ref()?.to_string(),
))
Some(StringMatchCandidate::new(ix, metadata.title.as_ref()?))
})
.collect::<Vec<_>>();
let matches = fuzzy::match_strings(

View File

@@ -7,11 +7,13 @@ use editor::{CompletionProvider, Editor};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{AppContext, Model, Task, ViewContext, WeakView, WindowContext};
use language::{Anchor, Buffer, CodeLabel, Documentation, HighlightId, LanguageServerId, ToPoint};
use parking_lot::{Mutex, RwLock};
use parking_lot::Mutex;
use project::CompletionIntent;
use rope::Point;
use std::{
cell::RefCell,
ops::Range,
rc::Rc,
sync::{
atomic::{AtomicBool, Ordering::SeqCst},
Arc,
@@ -78,11 +80,7 @@ impl SlashCommandCompletionProvider {
.command_names(cx)
.into_iter()
.enumerate()
.map(|(ix, def)| StringMatchCandidate {
id: ix,
string: def.to_string(),
char_bag: def.as_ref().into(),
})
.map(|(ix, def)| StringMatchCandidate::new(ix, &def))
.collect::<Vec<_>>();
let command_name = command_name.to_string();
let editor = self.editor.clone();
@@ -326,7 +324,7 @@ impl CompletionProvider for SlashCommandCompletionProvider {
&self,
_: Model<Buffer>,
_: Vec<usize>,
_: Arc<RwLock<Box<[project::Completion]>>>,
_: Rc<RefCell<Box<[project::Completion]>>>,
_: &mut ViewContext<Editor>,
) -> Task<Result<bool>> {
Task::ready(Ok(true))

View File

@@ -218,10 +218,7 @@ impl Options {
}
fn match_candidates_for_args() -> [StringMatchCandidate; 1] {
[StringMatchCandidate::new(
0,
INCLUDE_WARNINGS_ARGUMENT.to_string(),
)]
[StringMatchCandidate::new(0, INCLUDE_WARNINGS_ARGUMENT)]
}
}

View File

@@ -249,11 +249,7 @@ fn tab_items_for_queries(
.enumerate()
.filter_map(|(id, (full_path, ..))| {
let path_string = full_path.as_deref()?.to_string_lossy().to_string();
Some(fuzzy::StringMatchCandidate {
id,
char_bag: path_string.as_str().into(),
string: path_string,
})
Some(fuzzy::StringMatchCandidate::new(id, &path_string))
})
.collect::<Vec<_>>();
let mut processed_matches = HashSet::default();

View File

@@ -217,11 +217,10 @@ impl PickerDelegate for SlashCommandDelegate {
)),
)
.child(
div().overflow_hidden().text_ellipsis().child(
Label::new(info.description.clone())
.size(LabelSize::Small)
.color(Color::Muted),
),
Label::new(info.description.clone())
.size(LabelSize::Small)
.color(Color::Muted)
.text_ellipsis(),
),
),
),
@@ -317,8 +316,8 @@ impl<T: PopoverTrigger> RenderOnce for SlashCommandSelector<T> {
PopoverMenu::new("model-switcher")
.menu(move |_cx| Some(picker_view.clone()))
.trigger(self.trigger)
.attach(gpui::AnchorCorner::TopLeft)
.anchor(gpui::AnchorCorner::BottomLeft)
.attach(gpui::Corner::TopLeft)
.anchor(gpui::Corner::BottomLeft)
.offset(gpui::Point {
x: px(0.0),
y: px(-16.0),

View File

@@ -31,6 +31,8 @@ futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
handlebars.workspace = true
html_to_markdown.workspace = true
http_client.workspace = true
language.workspace = true
language_model.workspace = true
language_model_selector.workspace = true

View File

@@ -213,26 +213,33 @@ impl ActiveThread {
div()
.id(("message-container", ix))
.p_2()
.py_1()
.px_2()
.child(
v_flex()
.border_1()
.border_color(cx.theme().colors().border_variant)
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().editor_background)
.rounded_md()
.child(
h_flex()
.justify_between()
.p_1p5()
.py_1()
.px_2()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.child(
h_flex()
.gap_2()
.child(Icon::new(role_icon).size(IconSize::Small))
.child(Label::new(role_name).size(LabelSize::Small)),
.gap_1p5()
.child(
Icon::new(role_icon)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(Label::new(role_name).size(LabelSize::XSmall)),
),
)
.child(v_flex().p_1p5().text_ui(cx).child(markdown.clone()))
.child(v_flex().px_2().py_1().text_ui(cx).child(markdown.clone()))
.when_some(context, |parent, context| {
parent.child(
h_flex().flex_wrap().gap_2().p_1p5().children(
@@ -249,6 +256,6 @@ impl ActiveThread {
impl Render for ActiveThread {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
list(self.list_state.clone()).flex_1()
list(self.list_state.clone()).flex_1().py_1()
}
}

View File

@@ -3,6 +3,8 @@ mod assistant_panel;
mod assistant_settings;
mod context;
mod context_picker;
mod context_store;
mod context_strip;
mod inline_assistant;
mod message_editor;
mod prompts;
@@ -15,7 +17,6 @@ mod ui;
use std::sync::Arc;
use assistant_settings::AssistantSettings;
use client::Client;
use command_palette_hooks::CommandPaletteFilter;
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt};
@@ -26,16 +27,18 @@ use settings::Settings as _;
use util::ResultExt;
pub use crate::assistant_panel::AssistantPanel;
use crate::assistant_settings::AssistantSettings;
pub use crate::inline_assistant::InlineAssistant;
actions!(
assistant2,
[
ToggleFocus,
NewThread,
ToggleContextPicker,
ToggleModelSelector,
OpenHistory,
Chat,
ToggleInlineAssist,
CycleNextInlineAssist,
CyclePreviousInlineAssist
]
@@ -75,8 +78,6 @@ pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, stdout_is_a_pty: bool, cx: &mu
}
fn feature_gate_assistant2_actions(cx: &mut AppContext) {
const ASSISTANT1_NAMESPACE: &str = "assistant";
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_namespace(NAMESPACE);
});
@@ -85,12 +86,10 @@ fn feature_gate_assistant2_actions(cx: &mut AppContext) {
if is_enabled {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.show_namespace(NAMESPACE);
filter.hide_namespace(ASSISTANT1_NAMESPACE);
});
} else {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_namespace(NAMESPACE);
filter.show_namespace(ASSISTANT1_NAMESPACE);
});
}
})

View File

@@ -3,18 +3,21 @@ use std::sync::Arc;
use anyhow::Result;
use assistant_tool::ToolWorkingSet;
use client::zed_urls;
use fs::Fs;
use gpui::{
prelude::*, px, svg, Action, AnyElement, AppContext, AsyncWindowContext, EventEmitter,
FocusHandle, FocusableView, FontWeight, Model, Pixels, Task, View, ViewContext, WeakView,
WindowContext,
};
use language::LanguageRegistry;
use settings::Settings;
use time::UtcOffset;
use ui::{prelude::*, Divider, IconButtonShape, KeyBinding, Tab, Tooltip};
use ui::{prelude::*, KeyBinding, Tab, Tooltip};
use workspace::dock::{DockPosition, Panel, PanelEvent};
use workspace::Workspace;
use crate::active_thread::ActiveThread;
use crate::assistant_settings::{AssistantDockPosition, AssistantSettings};
use crate::message_editor::MessageEditor;
use crate::thread::{ThreadError, ThreadId};
use crate::thread_history::{PastThread, ThreadHistory};
@@ -39,6 +42,7 @@ enum ActiveView {
pub struct AssistantPanel {
workspace: WeakView<Workspace>,
fs: Arc<dyn Fs>,
language_registry: Arc<LanguageRegistry>,
thread_store: Model<ThreadStore>,
thread: View<ActiveThread>,
@@ -47,6 +51,8 @@ pub struct AssistantPanel {
local_timezone: UtcOffset,
active_view: ActiveView,
history: View<ThreadHistory>,
width: Option<Pixels>,
height: Option<Pixels>,
}
impl AssistantPanel {
@@ -76,6 +82,7 @@ impl AssistantPanel {
cx: &mut ViewContext<Self>,
) -> Self {
let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
let fs = workspace.app_state().fs.clone();
let language_registry = workspace.project().read(cx).languages().clone();
let workspace = workspace.weak_handle();
let weak_self = cx.view().downgrade();
@@ -83,6 +90,7 @@ impl AssistantPanel {
Self {
active_view: ActiveView::Thread,
workspace: workspace.clone(),
fs: fs.clone(),
language_registry: language_registry.clone(),
thread_store: thread_store.clone(),
thread: cx.new_view(|cx| {
@@ -94,13 +102,23 @@ impl AssistantPanel {
cx,
)
}),
message_editor: cx.new_view(|cx| MessageEditor::new(workspace, thread.clone(), cx)),
message_editor: cx.new_view(|cx| {
MessageEditor::new(
fs.clone(),
workspace,
thread_store.downgrade(),
thread.clone(),
cx,
)
}),
tools,
local_timezone: UtcOffset::from_whole_seconds(
chrono::Local::now().offset().local_minus_utc(),
)
.unwrap(),
history: cx.new_view(|cx| ThreadHistory::new(weak_self, thread_store, cx)),
width: None,
height: None,
}
}
@@ -108,6 +126,10 @@ impl AssistantPanel {
self.local_timezone
}
pub(crate) fn thread_store(&self) -> &Model<ThreadStore> {
&self.thread_store
}
fn new_thread(&mut self, cx: &mut ViewContext<Self>) {
let thread = self
.thread_store
@@ -123,8 +145,15 @@ impl AssistantPanel {
cx,
)
});
self.message_editor =
cx.new_view(|cx| MessageEditor::new(self.workspace.clone(), thread, cx));
self.message_editor = cx.new_view(|cx| {
MessageEditor::new(
self.fs.clone(),
self.workspace.clone(),
self.thread_store.downgrade(),
thread,
cx,
)
});
self.message_editor.focus_handle(cx).focus(cx);
}
@@ -146,8 +175,15 @@ impl AssistantPanel {
cx,
)
});
self.message_editor =
cx.new_view(|cx| MessageEditor::new(self.workspace.clone(), thread, cx));
self.message_editor = cx.new_view(|cx| {
MessageEditor::new(
self.fs.clone(),
self.workspace.clone(),
self.thread_store.downgrade(),
thread,
cx,
)
});
self.message_editor.focus_handle(cx).focus(cx);
}
@@ -181,13 +217,38 @@ impl Panel for AssistantPanel {
true
}
fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext<Self>) {}
fn size(&self, _cx: &WindowContext) -> Pixels {
px(640.)
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
settings::update_settings_file::<AssistantSettings>(
self.fs.clone(),
cx,
move |settings, _| {
let dock = match position {
DockPosition::Left => AssistantDockPosition::Left,
DockPosition::Bottom => AssistantDockPosition::Bottom,
DockPosition::Right => AssistantDockPosition::Right,
};
settings.set_dock(dock);
},
);
}
fn set_size(&mut self, _size: Option<Pixels>, _cx: &mut ViewContext<Self>) {}
fn size(&self, cx: &WindowContext) -> Pixels {
let settings = AssistantSettings::get_global(cx);
match self.position(cx) {
DockPosition::Left | DockPosition::Right => {
self.width.unwrap_or(settings.default_width)
}
DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
}
}
fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
match self.position(cx) {
DockPosition::Left | DockPosition::Right => self.width = size,
DockPosition::Bottom => self.height = size,
}
cx.notify();
}
fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
@@ -220,15 +281,17 @@ impl AssistantPanel {
.px(DynamicSpacing::Base08.rems(cx))
.bg(cx.theme().colors().tab_bar_background)
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.border_color(cx.theme().colors().border)
.child(h_flex().children(self.thread.read(cx).summary(cx).map(Label::new)))
.child(
h_flex()
.gap(DynamicSpacing::Base08.rems(cx))
.child(Divider::vertical())
.h_full()
.pl_1()
.border_l_1()
.border_color(cx.theme().colors().border)
.gap(DynamicSpacing::Base02.rems(cx))
.child(
IconButton::new("new-thread", IconName::Plus)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.tooltip({
@@ -248,7 +311,6 @@ impl AssistantPanel {
)
.child(
IconButton::new("open-history", IconName::HistoryRerun)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.tooltip({
@@ -268,7 +330,6 @@ impl AssistantPanel {
)
.child(
IconButton::new("configure-assistant", IconName::Settings)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.tooltip(move |cx| Tooltip::text("Configure Assistant", cx))
@@ -294,7 +355,6 @@ impl AssistantPanel {
v_flex()
.gap_2()
.mx_auto()
.child(
v_flex().w_full().child(
svg()
@@ -309,13 +369,14 @@ impl AssistantPanel {
.when(!recent_threads.is_empty(), |parent| {
parent
.child(
h_flex()
.w_full()
.justify_center()
.child(Label::new("Recent Threads:").size(LabelSize::Small)),
h_flex().w_full().justify_center().child(
Label::new("Recent Threads:")
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.child(
v_flex().gap_2().children(
v_flex().mx_auto().w_4_5().gap_2().children(
recent_threads
.into_iter()
.map(|thread| PastThread::new(thread, cx.view().downgrade())),
@@ -522,7 +583,7 @@ impl Render for AssistantPanel {
.child(
h_flex()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.border_color(cx.theme().colors().border)
.child(self.message_editor.clone()),
)
.children(self.render_last_error(cx)),

View File

@@ -157,6 +157,22 @@ impl AssistantSettingsContent {
}
}
pub fn set_dock(&mut self, dock: AssistantDockPosition) {
match self {
AssistantSettingsContent::Versioned(settings) => match settings {
VersionedAssistantSettingsContent::V1(settings) => {
settings.dock = Some(dock);
}
VersionedAssistantSettingsContent::V2(settings) => {
settings.dock = Some(dock);
}
},
AssistantSettingsContent::Legacy(settings) => {
settings.dock = Some(dock);
}
}
}
pub fn set_model(&mut self, language_model: Arc<dyn LanguageModel>) {
let model = language_model.id().0.to_string();
let provider = language_model.provider_id().0.to_string();

View File

@@ -1,4 +1,5 @@
use gpui::SharedString;
use language_model::{LanguageModelRequestMessage, MessageContent};
use serde::{Deserialize, Serialize};
use util::post_inc;
@@ -23,4 +24,67 @@ pub struct Context {
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ContextKind {
File,
Directory,
FetchedUrl,
Thread,
}
pub fn attach_context_to_message(
message: &mut LanguageModelRequestMessage,
context: impl IntoIterator<Item = Context>,
) {
let mut file_context = String::new();
let mut directory_context = String::new();
let mut fetch_context = String::new();
let mut thread_context = String::new();
for context in context.into_iter() {
match context.kind {
ContextKind::File => {
file_context.push_str(&context.text);
file_context.push('\n');
}
ContextKind::Directory => {
directory_context.push_str(&context.text);
directory_context.push('\n');
}
ContextKind::FetchedUrl => {
fetch_context.push_str(&context.name);
fetch_context.push('\n');
fetch_context.push_str(&context.text);
fetch_context.push('\n');
}
ContextKind::Thread => {
thread_context.push_str(&context.name);
thread_context.push('\n');
thread_context.push_str(&context.text);
thread_context.push('\n');
}
}
}
let mut context_text = String::new();
if !file_context.is_empty() {
context_text.push_str("The following files are available:\n");
context_text.push_str(&file_context);
}
if !directory_context.is_empty() {
context_text.push_str("The following directories are available:\n");
context_text.push_str(&directory_context);
}
if !fetch_context.is_empty() {
context_text.push_str("The following fetched results are available\n");
context_text.push_str(&fetch_context);
}
if !thread_context.is_empty() {
context_text.push_str("The following previous conversation threads are available\n");
context_text.push_str(&thread_context);
}
if !context_text.is_empty() {
message.content.push(MessageContent::Text(context_text));
}
}

View File

@@ -1,23 +1,34 @@
mod directory_context_picker;
mod fetch_context_picker;
mod file_context_picker;
mod thread_context_picker;
use std::sync::Arc;
use gpui::{
AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, SharedString, Task, View,
WeakView,
WeakModel, WeakView,
};
use picker::{Picker, PickerDelegate};
use ui::{prelude::*, ListItem, ListItemSpacing, Tooltip};
use ui::{prelude::*, ListItem, ListItemSpacing};
use util::ResultExt;
use workspace::Workspace;
use crate::context::ContextKind;
use crate::context_picker::directory_context_picker::DirectoryContextPicker;
use crate::context_picker::fetch_context_picker::FetchContextPicker;
use crate::context_picker::file_context_picker::FileContextPicker;
use crate::message_editor::MessageEditor;
use crate::context_picker::thread_context_picker::ThreadContextPicker;
use crate::context_store::ContextStore;
use crate::thread_store::ThreadStore;
#[derive(Debug, Clone)]
enum ContextPickerMode {
Default,
File(View<FileContextPicker>),
Directory(View<DirectoryContextPicker>),
Fetch(View<FetchContextPicker>),
Thread(View<ThreadContextPicker>),
}
pub(super) struct ContextPicker {
@@ -28,30 +39,42 @@ pub(super) struct ContextPicker {
impl ContextPicker {
pub fn new(
workspace: WeakView<Workspace>,
message_editor: WeakView<MessageEditor>,
thread_store: Option<WeakModel<ThreadStore>>,
context_store: WeakModel<ContextStore>,
cx: &mut ViewContext<Self>,
) -> Self {
let mut entries = vec![
ContextPickerEntry {
name: "File".into(),
kind: ContextKind::File,
icon: IconName::File,
},
ContextPickerEntry {
name: "Folder".into(),
kind: ContextKind::Directory,
icon: IconName::Folder,
},
ContextPickerEntry {
name: "Fetch".into(),
kind: ContextKind::FetchedUrl,
icon: IconName::Globe,
},
];
if thread_store.is_some() {
entries.push(ContextPickerEntry {
name: "Thread".into(),
kind: ContextKind::Thread,
icon: IconName::MessageCircle,
});
}
let delegate = ContextPickerDelegate {
context_picker: cx.view().downgrade(),
workspace: workspace.clone(),
message_editor: message_editor.clone(),
entries: vec![
ContextPickerEntry {
name: "directory".into(),
description: "Insert any directory".into(),
icon: IconName::Folder,
},
ContextPickerEntry {
name: "file".into(),
description: "Insert any file".into(),
icon: IconName::File,
},
ContextPickerEntry {
name: "web".into(),
description: "Fetch content from URL".into(),
icon: IconName::Globe,
},
],
workspace,
thread_store,
context_store,
entries,
selected_ix: 0,
};
@@ -77,30 +100,42 @@ impl FocusableView for ContextPicker {
match &self.mode {
ContextPickerMode::Default => self.picker.focus_handle(cx),
ContextPickerMode::File(file_picker) => file_picker.focus_handle(cx),
ContextPickerMode::Directory(directory_picker) => directory_picker.focus_handle(cx),
ContextPickerMode::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
ContextPickerMode::Thread(thread_picker) => thread_picker.focus_handle(cx),
}
}
}
impl Render for ContextPicker {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
v_flex().min_w(px(400.)).map(|parent| match &self.mode {
ContextPickerMode::Default => parent.child(self.picker.clone()),
ContextPickerMode::File(file_picker) => parent.child(file_picker.clone()),
})
v_flex()
.w(px(400.))
.min_w(px(400.))
.map(|parent| match &self.mode {
ContextPickerMode::Default => parent.child(self.picker.clone()),
ContextPickerMode::File(file_picker) => parent.child(file_picker.clone()),
ContextPickerMode::Directory(directory_picker) => {
parent.child(directory_picker.clone())
}
ContextPickerMode::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
ContextPickerMode::Thread(thread_picker) => parent.child(thread_picker.clone()),
})
}
}
#[derive(Clone)]
struct ContextPickerEntry {
name: SharedString,
description: SharedString,
kind: ContextKind,
icon: IconName,
}
pub(crate) struct ContextPickerDelegate {
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
message_editor: WeakView<MessageEditor>,
thread_store: Option<WeakModel<ThreadStore>>,
context_store: WeakModel<ContextStore>,
entries: Vec<ContextPickerEntry>,
selected_ix: usize,
}
@@ -133,18 +168,49 @@ impl PickerDelegate for ContextPickerDelegate {
if let Some(entry) = self.entries.get(self.selected_ix) {
self.context_picker
.update(cx, |this, cx| {
match entry.name.to_string().as_str() {
"file" => {
match entry.kind {
ContextKind::File => {
this.mode = ContextPickerMode::File(cx.new_view(|cx| {
FileContextPicker::new(
self.context_picker.clone(),
self.workspace.clone(),
self.message_editor.clone(),
self.context_store.clone(),
cx,
)
}));
}
_ => {}
ContextKind::Directory => {
this.mode = ContextPickerMode::Directory(cx.new_view(|cx| {
DirectoryContextPicker::new(
self.context_picker.clone(),
self.workspace.clone(),
self.context_store.clone(),
cx,
)
}));
}
ContextKind::FetchedUrl => {
this.mode = ContextPickerMode::Fetch(cx.new_view(|cx| {
FetchContextPicker::new(
self.context_picker.clone(),
self.workspace.clone(),
self.context_store.clone(),
cx,
)
}));
}
ContextKind::Thread => {
if let Some(thread_store) = self.thread_store.as_ref() {
this.mode = ContextPickerMode::Thread(cx.new_view(|cx| {
ThreadContextPicker::new(
thread_store.clone(),
self.context_picker.clone(),
self.context_store.clone(),
cx,
)
}));
}
}
}
cx.focus_self();
@@ -157,7 +223,10 @@ impl PickerDelegate for ContextPickerDelegate {
self.context_picker
.update(cx, |this, cx| match this.mode {
ContextPickerMode::Default => cx.emit(DismissEvent),
ContextPickerMode::File(_) => {}
ContextPickerMode::File(_)
| ContextPickerMode::Directory(_)
| ContextPickerMode::Fetch(_)
| ContextPickerMode::Thread(_) => {}
})
.log_err();
}
@@ -175,34 +244,13 @@ impl PickerDelegate for ContextPickerDelegate {
.inset(true)
.spacing(ListItemSpacing::Dense)
.toggle_state(selected)
.tooltip({
let description = entry.description.clone();
move |cx| cx.new_view(|_cx| Tooltip::new(description.clone())).into()
})
.child(
v_flex()
.group(format!("context-entry-label-{ix}"))
.w_full()
.py_0p5()
h_flex()
.min_w(px(250.))
.max_w(px(400.))
.child(
h_flex()
.gap_1p5()
.child(Icon::new(entry.icon).size(IconSize::XSmall))
.child(
Label::new(entry.name.clone())
.single_line()
.size(LabelSize::Small),
),
)
.child(
div().overflow_hidden().text_ellipsis().child(
Label::new(entry.description.clone())
.size(LabelSize::Small)
.color(Color::Muted),
),
),
.gap_2()
.child(Icon::new(entry.icon).size(IconSize::Small))
.child(Label::new(entry.name.clone()).single_line()),
),
)
}

View File

@@ -0,0 +1,117 @@
// TODO: Remove this once we've implemented the functionality.
#![allow(unused)]
use std::sync::Arc;
use fuzzy::PathMatch;
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, WorktreeId};
use ui::{prelude::*, ListItem};
use util::ResultExt as _;
use workspace::Workspace;
use crate::context_picker::ContextPicker;
use crate::context_store::ContextStore;
pub struct DirectoryContextPicker {
picker: View<Picker<DirectoryContextPickerDelegate>>,
}
impl DirectoryContextPicker {
pub fn new(
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_store: WeakModel<ContextStore>,
cx: &mut ViewContext<Self>,
) -> Self {
let delegate =
DirectoryContextPickerDelegate::new(context_picker, workspace, context_store);
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
Self { picker }
}
}
impl FocusableView for DirectoryContextPicker {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for DirectoryContextPicker {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
self.picker.clone()
}
}
pub struct DirectoryContextPickerDelegate {
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_store: WeakModel<ContextStore>,
matches: Vec<PathMatch>,
selected_index: usize,
}
impl DirectoryContextPickerDelegate {
pub fn new(
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_store: WeakModel<ContextStore>,
) -> Self {
Self {
context_picker,
workspace,
context_store,
matches: Vec::new(),
selected_index: 0,
}
}
}
impl PickerDelegate for DirectoryContextPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
self.selected_index = ix;
}
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
"Search folders…".into()
}
fn update_matches(&mut self, _query: String, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
// TODO: Implement this once we fix the issues with the file context picker.
Task::ready(())
}
fn confirm(&mut self, _secondary: bool, _cx: &mut ViewContext<Picker<Self>>) {
// TODO: Implement this once we fix the issues with the file context picker.
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
self.context_picker
.update(cx, |this, cx| {
this.reset_mode();
cx.emit(DismissEvent);
})
.ok();
}
fn render_match(
&self,
_ix: usize,
_selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
None
}
}

View File

@@ -0,0 +1,225 @@
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
use anyhow::{bail, Context as _, Result};
use futures::AsyncReadExt as _;
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
use http_client::{AsyncBody, HttpClientWithUrl};
use picker::{Picker, PickerDelegate};
use ui::{prelude::*, ListItem, ViewContext};
use workspace::Workspace;
use crate::context::ContextKind;
use crate::context_picker::ContextPicker;
use crate::context_store::ContextStore;
pub struct FetchContextPicker {
picker: View<Picker<FetchContextPickerDelegate>>,
}
impl FetchContextPicker {
pub fn new(
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_store: WeakModel<ContextStore>,
cx: &mut ViewContext<Self>,
) -> Self {
let delegate = FetchContextPickerDelegate::new(context_picker, workspace, context_store);
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
Self { picker }
}
}
impl FocusableView for FetchContextPicker {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for FetchContextPicker {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
self.picker.clone()
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
enum ContentType {
Html,
Plaintext,
Json,
}
pub struct FetchContextPickerDelegate {
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_store: WeakModel<ContextStore>,
url: String,
}
impl FetchContextPickerDelegate {
pub fn new(
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
context_store: WeakModel<ContextStore>,
) -> Self {
FetchContextPickerDelegate {
context_picker,
workspace,
context_store,
url: String::new(),
}
}
async fn build_message(http_client: Arc<HttpClientWithUrl>, url: &str) -> Result<String> {
let mut url = url.to_owned();
if !url.starts_with("https://") && !url.starts_with("http://") {
url = format!("https://{url}");
}
let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
let mut body = Vec::new();
response
.body_mut()
.read_to_end(&mut body)
.await
.context("error reading response body")?;
if response.status().is_client_error() {
let text = String::from_utf8_lossy(body.as_slice());
bail!(
"status error {}, response: {text:?}",
response.status().as_u16()
);
}
let Some(content_type) = response.headers().get("content-type") else {
bail!("missing Content-Type header");
};
let content_type = content_type
.to_str()
.context("invalid Content-Type header")?;
let content_type = match content_type {
"text/html" => ContentType::Html,
"text/plain" => ContentType::Plaintext,
"application/json" => ContentType::Json,
_ => ContentType::Html,
};
match content_type {
ContentType::Html => {
let mut handlers: Vec<TagHandler> = vec![
Rc::new(RefCell::new(markdown::WebpageChromeRemover)),
Rc::new(RefCell::new(markdown::ParagraphHandler)),
Rc::new(RefCell::new(markdown::HeadingHandler)),
Rc::new(RefCell::new(markdown::ListHandler)),
Rc::new(RefCell::new(markdown::TableHandler::new())),
Rc::new(RefCell::new(markdown::StyledTextHandler)),
];
if url.contains("wikipedia.org") {
use html_to_markdown::structure::wikipedia;
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
handlers.push(Rc::new(
RefCell::new(wikipedia::WikipediaCodeHandler::new()),
));
} else {
handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
}
convert_html_to_markdown(&body[..], &mut handlers)
}
ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
ContentType::Json => {
let json: serde_json::Value = serde_json::from_slice(&body)?;
Ok(format!(
"```json\n{}\n```",
serde_json::to_string_pretty(&json)?
))
}
}
}
}
impl PickerDelegate for FetchContextPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
if self.url.is_empty() {
0
} else {
1
}
}
fn no_matches_text(&self, _cx: &mut WindowContext) -> SharedString {
"Enter the URL that you would like to fetch".into()
}
fn selected_index(&self) -> usize {
0
}
fn set_selected_index(&mut self, _ix: usize, _cx: &mut ViewContext<Picker<Self>>) {}
fn placeholder_text(&self, _cx: &mut ui::WindowContext) -> Arc<str> {
"Enter a URL…".into()
}
fn update_matches(&mut self, query: String, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
self.url = query;
Task::ready(())
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let http_client = workspace.read(cx).client().http_client().clone();
let url = self.url.clone();
cx.spawn(|this, mut cx| async move {
let text = Self::build_message(http_client, &url).await?;
this.update(&mut cx, |this, cx| {
this.delegate
.context_store
.update(cx, |context_store, _cx| {
context_store.insert_context(ContextKind::FetchedUrl, url, text);
})
})??;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
self.context_picker
.update(cx, |this, cx| {
this.reset_mode();
cx.emit(DismissEvent);
})
.ok();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
Some(
ListItem::new(ix)
.inset(true)
.toggle_state(selected)
.child(Label::new(self.url.clone())),
)
}
}

View File

@@ -5,16 +5,16 @@ use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use fuzzy::PathMatch;
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakView};
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, WorktreeId};
use ui::{prelude::*, ListItem, ListItemSpacing};
use ui::{prelude::*, ListItem};
use util::ResultExt as _;
use workspace::Workspace;
use crate::context::ContextKind;
use crate::context_picker::ContextPicker;
use crate::message_editor::MessageEditor;
use crate::context_store::ContextStore;
pub struct FileContextPicker {
picker: View<Picker<FileContextPickerDelegate>>,
@@ -24,10 +24,10 @@ impl FileContextPicker {
pub fn new(
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
message_editor: WeakView<MessageEditor>,
context_store: WeakModel<ContextStore>,
cx: &mut ViewContext<Self>,
) -> Self {
let delegate = FileContextPickerDelegate::new(context_picker, workspace, message_editor);
let delegate = FileContextPickerDelegate::new(context_picker, workspace, context_store);
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
Self { picker }
@@ -49,7 +49,7 @@ impl Render for FileContextPicker {
pub struct FileContextPickerDelegate {
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
message_editor: WeakView<MessageEditor>,
context_store: WeakModel<ContextStore>,
matches: Vec<PathMatch>,
selected_index: usize,
}
@@ -58,12 +58,12 @@ impl FileContextPickerDelegate {
pub fn new(
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
message_editor: WeakView<MessageEditor>,
context_store: WeakModel<ContextStore>,
) -> Self {
Self {
context_picker,
workspace,
message_editor,
context_store,
matches: Vec::new(),
selected_index: 0,
}
@@ -214,24 +214,22 @@ impl PickerDelegate for FileContextPickerDelegate {
let buffer = open_buffer_task.await?;
this.update(&mut cx, |this, cx| {
this.delegate
.message_editor
.update(cx, |message_editor, cx| {
let mut text = String::new();
text.push_str(&codeblock_fence_for_path(Some(&path), None));
text.push_str(&buffer.read(cx).text());
if !text.ends_with('\n') {
text.push('\n');
}
this.delegate.context_store.update(cx, |context_store, cx| {
let mut text = String::new();
text.push_str(&codeblock_fence_for_path(Some(&path), None));
text.push_str(&buffer.read(cx).text());
if !text.ends_with('\n') {
text.push('\n');
}
text.push_str("```\n");
text.push_str("```\n");
message_editor.insert_context(
ContextKind::File,
path.to_string_lossy().to_string(),
text,
);
})
context_store.insert_context(
ContextKind::File,
path.to_string_lossy().to_string(),
text,
);
})
})??;
anyhow::Ok(())
@@ -245,7 +243,7 @@ impl PickerDelegate for FileContextPickerDelegate {
this.reset_mode();
cx.emit(DismissEvent);
})
.log_err();
.ok();
}
fn render_match(
@@ -254,14 +252,29 @@ impl PickerDelegate for FileContextPickerDelegate {
selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let mat = &self.matches[ix];
let path_match = &self.matches[ix];
let file_name = path_match
.path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let directory = path_match
.path
.parent()
.map(|directory| format!("{}/", directory.to_string_lossy()));
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(mat.path.to_string_lossy().to_string()),
ListItem::new(ix).inset(true).toggle_state(selected).child(
h_flex()
.gap_2()
.child(Label::new(file_name))
.children(directory.map(|directory| {
Label::new(directory)
.size(LabelSize::Small)
.color(Color::Muted)
})),
),
)
}
}

View File

@@ -0,0 +1,209 @@
use std::sync::Arc;
use fuzzy::StringMatchCandidate;
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
use picker::{Picker, PickerDelegate};
use ui::{prelude::*, ListItem};
use crate::context::ContextKind;
use crate::context_picker::ContextPicker;
use crate::context_store;
use crate::thread::ThreadId;
use crate::thread_store::ThreadStore;
pub struct ThreadContextPicker {
picker: View<Picker<ThreadContextPickerDelegate>>,
}
impl ThreadContextPicker {
pub fn new(
thread_store: WeakModel<ThreadStore>,
context_picker: WeakView<ContextPicker>,
context_store: WeakModel<context_store::ContextStore>,
cx: &mut ViewContext<Self>,
) -> Self {
let delegate =
ThreadContextPickerDelegate::new(thread_store, context_picker, context_store);
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
ThreadContextPicker { picker }
}
}
impl FocusableView for ThreadContextPicker {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for ThreadContextPicker {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
self.picker.clone()
}
}
#[derive(Debug, Clone)]
struct ThreadContextEntry {
id: ThreadId,
summary: SharedString,
}
pub struct ThreadContextPickerDelegate {
thread_store: WeakModel<ThreadStore>,
context_picker: WeakView<ContextPicker>,
context_store: WeakModel<context_store::ContextStore>,
matches: Vec<ThreadContextEntry>,
selected_index: usize,
}
impl ThreadContextPickerDelegate {
pub fn new(
thread_store: WeakModel<ThreadStore>,
context_picker: WeakView<ContextPicker>,
context_store: WeakModel<context_store::ContextStore>,
) -> Self {
ThreadContextPickerDelegate {
thread_store,
context_picker,
context_store,
matches: Vec::new(),
selected_index: 0,
}
}
}
impl PickerDelegate for ThreadContextPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
self.selected_index = ix;
}
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
"Search threads…".into()
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
let Ok(threads) = self.thread_store.update(cx, |this, cx| {
this.threads(cx)
.into_iter()
.map(|thread| {
const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Thread");
let id = thread.read(cx).id().clone();
let summary = thread.read(cx).summary().unwrap_or(DEFAULT_SUMMARY);
ThreadContextEntry { id, summary }
})
.collect::<Vec<_>>()
}) else {
return Task::ready(());
};
let executor = cx.background_executor().clone();
let search_task = cx.background_executor().spawn(async move {
if query.is_empty() {
threads
} else {
let candidates = threads
.iter()
.enumerate()
.map(|(id, thread)| StringMatchCandidate::new(id, &thread.summary))
.collect::<Vec<_>>();
let matches = fuzzy::match_strings(
&candidates,
&query,
false,
100,
&Default::default(),
executor,
)
.await;
matches
.into_iter()
.map(|mat| threads[mat.candidate_id].clone())
.collect()
}
});
cx.spawn(|this, mut cx| async move {
let matches = search_task.await;
this.update(&mut cx, |this, cx| {
this.delegate.matches = matches;
this.delegate.selected_index = 0;
cx.notify();
})
.ok();
})
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
let entry = &self.matches[self.selected_index];
let Some(thread_store) = self.thread_store.upgrade() else {
return;
};
let Some(thread) = thread_store.update(cx, |this, cx| this.open_thread(&entry.id, cx))
else {
return;
};
self.context_store
.update(cx, |context_store, cx| {
let text = thread.update(cx, |thread, _cx| {
let mut text = String::new();
for message in thread.messages() {
text.push_str(match message.role {
language_model::Role::User => "User:",
language_model::Role::Assistant => "Assistant:",
language_model::Role::System => "System:",
});
text.push('\n');
text.push_str(&message.text);
text.push('\n');
}
text
});
context_store.insert_context(ContextKind::Thread, entry.summary.clone(), text);
})
.ok();
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
self.context_picker
.update(cx, |this, cx| {
this.reset_mode();
cx.emit(DismissEvent);
})
.ok();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let thread = &self.matches[ix];
Some(
ListItem::new(ix)
.inset(true)
.toggle_state(selected)
.child(thread.summary.clone()),
)
}
}

View File

@@ -0,0 +1,47 @@
use gpui::SharedString;
use crate::context::{Context, ContextId, ContextKind};
pub struct ContextStore {
context: Vec<Context>,
next_context_id: ContextId,
}
impl ContextStore {
pub fn new() -> Self {
Self {
context: Vec::new(),
next_context_id: ContextId(0),
}
}
pub fn context(&self) -> &Vec<Context> {
&self.context
}
pub fn drain(&mut self) -> Vec<Context> {
self.context.drain(..).collect()
}
pub fn clear(&mut self) {
self.context.clear();
}
pub fn insert_context(
&mut self,
kind: ContextKind,
name: impl Into<SharedString>,
text: impl Into<SharedString>,
) {
self.context.push(Context {
id: self.next_context_id.post_inc(),
name: name.into(),
kind,
text: text.into(),
});
}
pub fn remove_context(&mut self, id: &ContextId) {
self.context.retain(|context| context.id != *id);
}
}

View File

@@ -0,0 +1,105 @@
use std::rc::Rc;
use gpui::{FocusHandle, Model, View, WeakModel, WeakView};
use ui::{prelude::*, PopoverMenu, PopoverMenuHandle, Tooltip};
use workspace::Workspace;
use crate::context_picker::ContextPicker;
use crate::context_store::ContextStore;
use crate::thread_store::ThreadStore;
use crate::ui::ContextPill;
use crate::ToggleContextPicker;
pub struct ContextStrip {
context_store: Model<ContextStore>,
context_picker: View<ContextPicker>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
focus_handle: FocusHandle,
}
impl ContextStrip {
pub fn new(
context_store: Model<ContextStore>,
workspace: WeakView<Workspace>,
thread_store: Option<WeakModel<ThreadStore>>,
focus_handle: FocusHandle,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
cx: &mut ViewContext<Self>,
) -> Self {
Self {
context_store: context_store.clone(),
context_picker: cx.new_view(|cx| {
ContextPicker::new(
workspace.clone(),
thread_store.clone(),
context_store.downgrade(),
cx,
)
}),
context_picker_menu_handle,
focus_handle,
}
}
}
impl Render for ContextStrip {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let context = self.context_store.read(cx).context();
let context_picker = self.context_picker.clone();
let focus_handle = self.focus_handle.clone();
h_flex()
.flex_wrap()
.gap_1()
.child(
PopoverMenu::new("context-picker")
.menu(move |_cx| Some(context_picker.clone()))
.trigger(
IconButton::new("add-context", IconName::Plus)
.icon_size(IconSize::Small)
.style(ui::ButtonStyle::Filled)
.tooltip(move |cx| {
Tooltip::for_action_in(
"Add Context",
&ToggleContextPicker,
&focus_handle,
cx,
)
}),
)
.attach(gpui::Corner::TopLeft)
.anchor(gpui::Corner::BottomLeft)
.offset(gpui::Point {
x: px(0.0),
y: px(-16.0),
})
.with_handle(self.context_picker_menu_handle.clone()),
)
.children(context.iter().map(|context| {
ContextPill::new(context.clone()).on_remove({
let context = context.clone();
let context_store = self.context_store.clone();
Rc::new(cx.listener(move |_this, _event, cx| {
context_store.update(cx, |this, _cx| {
this.remove_context(&context.id);
});
cx.notify();
}))
})
}))
.when(!context.is_empty(), |parent| {
parent.child(
IconButton::new("remove-all-context", IconName::Eraser)
.icon_size(IconSize::Small)
.tooltip(move |cx| Tooltip::text("Remove All Context", cx))
.on_click({
let context_store = self.context_store.clone();
cx.listener(move |_this, _event, cx| {
context_store.update(cx, |this, _cx| this.clear());
cx.notify();
})
}),
)
})
}
}

View File

@@ -1,10 +1,16 @@
use crate::context::attach_context_to_message;
use crate::context_picker::ContextPicker;
use crate::context_store::ContextStore;
use crate::context_strip::ContextStrip;
use crate::thread_store::ThreadStore;
use crate::{
assistant_settings::AssistantSettings,
prompts::PromptBuilder,
streaming_diff::{CharOperation, LineDiff, LineOperation, StreamingDiff},
terminal_inline_assistant::TerminalInlineAssistant,
CycleNextInlineAssist, CyclePreviousInlineAssist, ToggleInlineAssist,
CycleNextInlineAssist, CyclePreviousInlineAssist,
};
use crate::{AssistantPanel, ToggleContextPicker};
use anyhow::{Context as _, Result};
use client::{telemetry::Telemetry, ErrorExt};
use collections::{hash_map, HashMap, HashSet, VecDeque};
@@ -24,7 +30,8 @@ use futures::{channel::mpsc, future::LocalBoxFuture, join, SinkExt, Stream, Stre
use gpui::{
anchored, deferred, point, AnyElement, AppContext, ClickEvent, CursorStyle, EventEmitter,
FocusHandle, FocusableView, FontWeight, Global, HighlightStyle, Model, ModelContext,
Subscription, Task, TextStyle, UpdateGlobal, View, ViewContext, WeakView, WindowContext,
Subscription, Task, TextStyle, UpdateGlobal, View, ViewContext, WeakModel, WeakView,
WindowContext,
};
use language::{Buffer, IndentKind, Point, Selection, TransactionId};
use language_model::{
@@ -45,6 +52,7 @@ use std::{
iter, mem,
ops::{Range, RangeInclusive},
pin::Pin,
rc::Rc,
sync::Arc,
task::{self, Poll},
time::Instant,
@@ -53,7 +61,9 @@ use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
use text::{OffsetRangeExt, ToPoint as _};
use theme::ThemeSettings;
use ui::{prelude::*, ToggleWithLabel, IconButtonShape, KeyBinding, Popover, Tooltip};
use ui::{
prelude::*, CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, PopoverMenuHandle, Tooltip,
};
use util::{RangeExt, ResultExt};
use workspace::{dock::Panel, ShowConfiguration};
use workspace::{notifications::NotificationId, ItemHandle, Toast, Workspace};
@@ -65,9 +75,7 @@ pub fn init(
cx: &mut AppContext,
) {
cx.set_global(InlineAssistant::new(fs, prompt_builder, telemetry));
cx.observe_new_views(|workspace: &mut Workspace, cx| {
workspace.register_action(InlineAssistant::toggle_inline_assist);
cx.observe_new_views(|_workspace: &mut Workspace, cx| {
let workspace = cx.view().clone();
InlineAssistant::update_global(cx, |inline_assistant, cx| {
inline_assistant.register_workspace(&workspace, cx)
@@ -177,10 +185,16 @@ impl InlineAssistant {
) {
if let Some(editor) = item.act_as::<Editor>(cx) {
editor.update(cx, |editor, cx| {
let thread_store = workspace
.read(cx)
.panel::<AssistantPanel>(cx)
.map(|assistant_panel| assistant_panel.read(cx).thread_store().downgrade());
editor.push_code_action_provider(
Arc::new(AssistantCodeActionProvider {
Rc::new(AssistantCodeActionProvider {
editor: cx.view().downgrade(),
workspace: workspace.downgrade(),
thread_store,
}),
cx,
);
@@ -188,9 +202,9 @@ impl InlineAssistant {
}
}
pub fn toggle_inline_assist(
pub fn inline_assist(
workspace: &mut Workspace,
_action: &ToggleInlineAssist,
_action: &zed_actions::InlineAssist,
cx: &mut ViewContext<Workspace>,
) {
let settings = AssistantSettings::get_global(cx);
@@ -208,15 +222,19 @@ impl InlineAssistant {
.map_or(false, |provider| provider.is_authenticated(cx))
};
let thread_store = workspace
.panel::<AssistantPanel>(cx)
.map(|assistant_panel| assistant_panel.read(cx).thread_store().downgrade());
let handle_assist = |cx: &mut ViewContext<Workspace>| match inline_assist_target {
InlineAssistTarget::Editor(active_editor) => {
InlineAssistant::update_global(cx, |assistant, cx| {
assistant.assist(&active_editor, Some(cx.view().downgrade()), cx)
assistant.assist(&active_editor, cx.view().downgrade(), thread_store, cx)
})
}
InlineAssistTarget::Terminal(active_terminal) => {
TerminalInlineAssistant::update_global(cx, |assistant, cx| {
assistant.assist(&active_terminal, Some(cx.view().downgrade()), cx)
assistant.assist(&active_terminal, cx.view().downgrade(), thread_store, cx)
})
}
};
@@ -263,7 +281,8 @@ impl InlineAssistant {
pub fn assist(
&mut self,
editor: &View<Editor>,
workspace: Option<WeakView<Workspace>>,
workspace: WeakView<Workspace>,
thread_store: Option<WeakModel<ThreadStore>>,
cx: &mut WindowContext,
) {
let (snapshot, initial_selections) = editor.update(cx, |editor, cx| {
@@ -342,11 +361,13 @@ impl InlineAssistant {
let mut assist_to_focus = None;
for range in codegen_ranges {
let assist_id = self.next_assist_id.post_inc();
let context_store = cx.new_model(|_cx| ContextStore::new());
let codegen = cx.new_model(|cx| {
Codegen::new(
editor.read(cx).buffer().clone(),
range.clone(),
None,
context_store.clone(),
self.telemetry.clone(),
self.prompt_builder.clone(),
cx,
@@ -362,6 +383,9 @@ impl InlineAssistant {
prompt_buffer.clone(),
codegen.clone(),
self.fs.clone(),
context_store,
workspace.clone(),
thread_store.clone(),
cx,
)
});
@@ -428,7 +452,8 @@ impl InlineAssistant {
initial_prompt: String,
initial_transaction_id: Option<TransactionId>,
focus: bool,
workspace: Option<WeakView<Workspace>>,
workspace: WeakView<Workspace>,
thread_store: Option<WeakModel<ThreadStore>>,
cx: &mut WindowContext,
) -> InlineAssistId {
let assist_group_id = self.next_assist_group_id.post_inc();
@@ -444,11 +469,14 @@ impl InlineAssistant {
range.end = range.end.bias_right(&snapshot);
}
let context_store = cx.new_model(|_cx| ContextStore::new());
let codegen = cx.new_model(|cx| {
Codegen::new(
editor.read(cx).buffer().clone(),
range.clone(),
initial_transaction_id,
context_store.clone(),
self.telemetry.clone(),
self.prompt_builder.clone(),
cx,
@@ -464,6 +492,9 @@ impl InlineAssistant {
prompt_buffer.clone(),
codegen.clone(),
self.fs.clone(),
context_store,
workspace.clone(),
thread_store,
cx,
)
});
@@ -1455,6 +1486,8 @@ enum PromptEditorEvent {
struct PromptEditor {
id: InlineAssistId,
editor: View<Editor>,
context_strip: View<ContextStrip>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
language_model_selector: View<LanguageModelSelector>,
edited_since_done: bool,
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
@@ -1472,11 +1505,7 @@ impl EventEmitter<PromptEditorEvent> for PromptEditor {}
impl Render for PromptEditor {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let gutter_dimensions = *self.gutter_dimensions.lock();
let mut buttons = vec![Button::new("add-context", "Add Context")
.style(ButtonStyle::Filled)
.icon(IconName::Plus)
.icon_position(IconPosition::Start)
.into_any_element()];
let mut buttons = Vec::new();
let codegen = self.codegen.read(cx);
if codegen.alternative_count(cx) > 1 {
buttons.push(self.render_cycle_controls(cx));
@@ -1569,91 +1598,115 @@ impl Render for PromptEditor {
}
});
h_flex()
.key_context("PromptEditor")
.bg(cx.theme().colors().editor_background)
.block_mouse_down()
.cursor(CursorStyle::Arrow)
v_flex()
.border_y_1()
.border_color(cx.theme().status().info_border)
.size_full()
.py(cx.line_height() / 2.5)
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::move_up))
.on_action(cx.listener(Self::move_down))
.capture_action(cx.listener(Self::cycle_prev))
.capture_action(cx.listener(Self::cycle_next))
.child(
h_flex()
.w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))
.justify_center()
.gap_2()
.child(LanguageModelSelectorPopoverMenu::new(
self.language_model_selector.clone(),
IconButton::new("context", IconName::SettingsAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.tooltip(move |cx| {
Tooltip::with_meta(
format!(
"Using {}",
LanguageModelRegistry::read_global(cx)
.active_model()
.map(|model| model.name().0)
.unwrap_or_else(|| "No model selected".into()),
),
None,
"Change Model",
cx,
)
}),
))
.map(|el| {
let CodegenStatus::Error(error) = self.codegen.read(cx).status(cx) else {
return el;
};
let error_message = SharedString::from(error.to_string());
if error.error_code() == proto::ErrorCode::RateLimitExceeded
&& cx.has_flag::<ZedPro>()
{
el.child(
v_flex()
.child(
IconButton::new("rate-limit-error", IconName::XCircle)
.toggle_state(self.show_rate_limit_notice)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.on_click(cx.listener(Self::toggle_rate_limit_notice)),
)
.children(self.show_rate_limit_notice.then(|| {
deferred(
anchored()
.position_mode(gpui::AnchoredPositionMode::Local)
.position(point(px(0.), px(24.)))
.anchor(gpui::AnchorCorner::TopLeft)
.child(self.render_rate_limit_notice(cx)),
.key_context("PromptEditor")
.bg(cx.theme().colors().editor_background)
.block_mouse_down()
.cursor(CursorStyle::Arrow)
.on_action(cx.listener(Self::toggle_context_picker))
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::move_up))
.on_action(cx.listener(Self::move_down))
.capture_action(cx.listener(Self::cycle_prev))
.capture_action(cx.listener(Self::cycle_next))
.child(
h_flex()
.w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))
.justify_center()
.gap_2()
.child(LanguageModelSelectorPopoverMenu::new(
self.language_model_selector.clone(),
IconButton::new("context", IconName::SettingsAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.tooltip(move |cx| {
Tooltip::with_meta(
format!(
"Using {}",
LanguageModelRegistry::read_global(cx)
.active_model()
.map(|model| model.name().0)
.unwrap_or_else(|| "No model selected".into()),
),
None,
"Change Model",
cx,
)
})),
)
} else {
el.child(
div()
.id("error")
.tooltip(move |cx| Tooltip::text(error_message.clone(), cx))
.child(
Icon::new(IconName::XCircle)
.size(IconSize::Small)
.color(Color::Error),
),
)
}
}),
}),
))
.map(|el| {
let CodegenStatus::Error(error) = self.codegen.read(cx).status(cx)
else {
return el;
};
let error_message = SharedString::from(error.to_string());
if error.error_code() == proto::ErrorCode::RateLimitExceeded
&& cx.has_flag::<ZedPro>()
{
el.child(
v_flex()
.child(
IconButton::new(
"rate-limit-error",
IconName::XCircle,
)
.toggle_state(self.show_rate_limit_notice)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.on_click(
cx.listener(Self::toggle_rate_limit_notice),
),
)
.children(self.show_rate_limit_notice.then(|| {
deferred(
anchored()
.position_mode(
gpui::AnchoredPositionMode::Local,
)
.position(point(px(0.), px(24.)))
.anchor(gpui::Corner::TopLeft)
.child(self.render_rate_limit_notice(cx)),
)
})),
)
} else {
el.child(
div()
.id("error")
.tooltip(move |cx| {
Tooltip::text(error_message.clone(), cx)
})
.child(
Icon::new(IconName::XCircle)
.size(IconSize::Small)
.color(Color::Error),
),
)
}
}),
)
.child(div().flex_1().child(self.render_editor(cx)))
.child(h_flex().gap_2().pr_6().children(buttons)),
)
.child(
h_flex()
.child(
h_flex()
.w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))
.justify_center()
.gap_2(),
)
.child(self.context_strip.clone()),
)
.child(div().flex_1().child(self.render_editor(cx)))
.child(h_flex().gap_2().pr_6().children(buttons))
}
}
@@ -1674,6 +1727,9 @@ impl PromptEditor {
prompt_buffer: Model<MultiBuffer>,
codegen: Model<Codegen>,
fs: Arc<dyn Fs>,
context_store: Model<ContextStore>,
workspace: WeakView<Workspace>,
thread_store: Option<WeakModel<ThreadStore>>,
cx: &mut ViewContext<Self>,
) -> Self {
let prompt_editor = cx.new_view(|cx| {
@@ -1694,10 +1750,22 @@ impl PromptEditor {
editor.set_placeholder_text(Self::placeholder_text(codegen.read(cx)), cx);
editor
});
let context_picker_menu_handle = PopoverMenuHandle::default();
let mut this = Self {
id,
editor: prompt_editor,
editor: prompt_editor.clone(),
context_strip: cx.new_view(|cx| {
ContextStrip::new(
context_store,
workspace.clone(),
thread_store.clone(),
prompt_editor.focus_handle(cx),
context_picker_menu_handle.clone(),
cx,
)
}),
context_picker_menu_handle,
language_model_selector: cx.new_view(|cx| {
let fs = fs.clone();
LanguageModelSelector::new(
@@ -1855,6 +1923,10 @@ impl PromptEditor {
}
}
fn toggle_context_picker(&mut self, _: &ToggleContextPicker, cx: &mut ViewContext<Self>) {
self.context_picker_menu_handle.toggle(cx);
}
fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
match self.codegen.read(cx).status(cx) {
CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
@@ -2061,7 +2133,7 @@ impl PromptEditor {
.child(
h_flex()
.justify_between()
.child(ToggleWithLabel::new(
.child(CheckboxWithLabel::new(
"dont-show-again",
Label::new("Don't show again"),
if dismissed_rate_limit_notice() {
@@ -2165,7 +2237,7 @@ pub struct InlineAssist {
decorations: Option<InlineAssistDecorations>,
codegen: Model<Codegen>,
_subscriptions: Vec<Subscription>,
workspace: Option<WeakView<Workspace>>,
workspace: WeakView<Workspace>,
}
impl InlineAssist {
@@ -2179,7 +2251,7 @@ impl InlineAssist {
end_block_id: CustomBlockId,
range: Range<Anchor>,
codegen: Model<Codegen>,
workspace: Option<WeakView<Workspace>>,
workspace: WeakView<Workspace>,
cx: &mut WindowContext,
) -> Self {
let prompt_editor_focus_handle = prompt_editor.focus_handle(cx);
@@ -2239,11 +2311,7 @@ impl InlineAssist {
if let CodegenStatus::Error(error) = codegen.read(cx).status(cx) {
if assist.decorations.is_none() {
if let Some(workspace) = assist
.workspace
.as_ref()
.and_then(|workspace| workspace.upgrade())
{
if let Some(workspace) = assist.workspace.upgrade() {
let error = format!("Inline assistant error: {}", error);
workspace.update(cx, |workspace, cx| {
struct InlineAssistantError;
@@ -2296,6 +2364,7 @@ pub struct Codegen {
buffer: Model<MultiBuffer>,
range: Range<Anchor>,
initial_transaction_id: Option<TransactionId>,
context_store: Model<ContextStore>,
telemetry: Arc<Telemetry>,
builder: Arc<PromptBuilder>,
is_insertion: bool,
@@ -2306,6 +2375,7 @@ impl Codegen {
buffer: Model<MultiBuffer>,
range: Range<Anchor>,
initial_transaction_id: Option<TransactionId>,
context_store: Model<ContextStore>,
telemetry: Arc<Telemetry>,
builder: Arc<PromptBuilder>,
cx: &mut ModelContext<Self>,
@@ -2315,6 +2385,7 @@ impl Codegen {
buffer.clone(),
range.clone(),
false,
Some(context_store.clone()),
Some(telemetry.clone()),
builder.clone(),
cx,
@@ -2329,6 +2400,7 @@ impl Codegen {
buffer,
range,
initial_transaction_id,
context_store,
telemetry,
builder,
};
@@ -2401,6 +2473,7 @@ impl Codegen {
self.buffer.clone(),
self.range.clone(),
false,
Some(self.context_store.clone()),
Some(self.telemetry.clone()),
self.builder.clone(),
cx,
@@ -2480,6 +2553,7 @@ pub struct CodegenAlternative {
status: CodegenStatus,
generation: Task<()>,
diff: Diff,
context_store: Option<Model<ContextStore>>,
telemetry: Option<Arc<Telemetry>>,
_subscription: gpui::Subscription,
builder: Arc<PromptBuilder>,
@@ -2518,6 +2592,7 @@ impl CodegenAlternative {
buffer: Model<MultiBuffer>,
range: Range<Anchor>,
active: bool,
context_store: Option<Model<ContextStore>>,
telemetry: Option<Arc<Telemetry>>,
builder: Arc<PromptBuilder>,
cx: &mut ModelContext<Self>,
@@ -2555,6 +2630,7 @@ impl CodegenAlternative {
status: CodegenStatus::Idle,
generation: Task::ready(()),
diff: Diff::default(),
context_store,
telemetry,
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
builder,
@@ -2640,7 +2716,11 @@ impl CodegenAlternative {
Ok(())
}
fn build_request(&self, user_prompt: String, cx: &AppContext) -> Result<LanguageModelRequest> {
fn build_request(
&self,
user_prompt: String,
cx: &mut AppContext,
) -> Result<LanguageModelRequest> {
let buffer = self.buffer.read(cx).snapshot(cx);
let language = buffer.language_at(self.range.start);
let language_name = if let Some(language) = language.as_ref() {
@@ -2673,15 +2753,24 @@ impl CodegenAlternative {
.generate_inline_transformation_prompt(user_prompt, language_name, buffer, range)
.map_err(|e| anyhow::anyhow!("Failed to generate content prompt: {}", e))?;
let mut request_message = LanguageModelRequestMessage {
role: Role::User,
content: Vec::new(),
cache: false,
};
if let Some(context_store) = &self.context_store {
let context = context_store.update(cx, |this, _cx| this.context().clone());
attach_context_to_message(&mut request_message, context);
}
request_message.content.push(prompt.into());
Ok(LanguageModelRequest {
tools: Vec::new(),
stop: Vec::new(),
temperature: None,
messages: vec![LanguageModelRequestMessage {
role: Role::User,
content: vec![prompt.into()],
cache: false,
}],
messages: vec![request_message],
})
}
@@ -3276,6 +3365,7 @@ where
struct AssistantCodeActionProvider {
editor: WeakView<Editor>,
workspace: WeakView<Workspace>,
thread_store: Option<WeakModel<ThreadStore>>,
}
impl CodeActionProvider for AssistantCodeActionProvider {
@@ -3340,6 +3430,7 @@ impl CodeActionProvider for AssistantCodeActionProvider {
) -> Task<Result<ProjectTransaction>> {
let editor = self.editor.clone();
let workspace = self.workspace.clone();
let thread_store = self.thread_store.clone();
cx.spawn(|mut cx| async move {
let editor = editor.upgrade().context("editor was released")?;
let range = editor
@@ -3386,7 +3477,8 @@ impl CodeActionProvider for AssistantCodeActionProvider {
"Fix Diagnostics".into(),
None,
true,
Some(workspace),
workspace,
thread_store,
cx,
);
assistant.start_assist(assist_id, cx);
@@ -3472,6 +3564,7 @@ mod tests {
range.clone(),
true,
None,
None,
prompt_builder,
cx,
)
@@ -3536,6 +3629,7 @@ mod tests {
range.clone(),
true,
None,
None,
prompt_builder,
cx,
)
@@ -3603,6 +3697,7 @@ mod tests {
range.clone(),
true,
None,
None,
prompt_builder,
cx,
)
@@ -3669,6 +3764,7 @@ mod tests {
range.clone(),
true,
None,
None,
prompt_builder,
cx,
)
@@ -3724,6 +3820,7 @@ mod tests {
range.clone(),
false,
None,
None,
prompt_builder,
cx,
)

View File

@@ -1,77 +1,95 @@
use std::rc::Rc;
use std::sync::Arc;
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{AppContext, FocusableView, Model, TextStyle, View, WeakView};
use fs::Fs;
use gpui::{AppContext, FocusableView, Model, TextStyle, View, WeakModel, WeakView};
use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use settings::Settings;
use settings::{update_settings_file, Settings};
use theme::ThemeSettings;
use ui::{
prelude::*, ButtonLike, ToggleWithLabel, ElevationIndex, IconButtonShape, KeyBinding,
PopoverMenu, PopoverMenuHandle, Tooltip,
prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, KeyBinding, PopoverMenuHandle,
Tooltip,
};
use workspace::Workspace;
use crate::context::{Context, ContextId, ContextKind};
use crate::assistant_settings::AssistantSettings;
use crate::context_picker::ContextPicker;
use crate::context_store::ContextStore;
use crate::context_strip::ContextStrip;
use crate::thread::{RequestKind, Thread};
use crate::ui::ContextPill;
use crate::{Chat, ToggleModelSelector};
use crate::thread_store::ThreadStore;
use crate::{Chat, ToggleContextPicker, ToggleModelSelector};
pub struct MessageEditor {
thread: Model<Thread>,
editor: View<Editor>,
context: Vec<Context>,
next_context_id: ContextId,
context_picker: View<ContextPicker>,
pub(crate) context_picker_handle: PopoverMenuHandle<ContextPicker>,
context_store: Model<ContextStore>,
context_strip: View<ContextStrip>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
language_model_selector: View<LanguageModelSelector>,
language_model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
use_tools: bool,
}
impl MessageEditor {
pub fn new(
fs: Arc<dyn Fs>,
workspace: WeakView<Workspace>,
thread_store: WeakModel<ThreadStore>,
thread: Model<Thread>,
cx: &mut ViewContext<Self>,
) -> Self {
let weak_self = cx.view().downgrade();
let context_store = cx.new_model(|_cx| ContextStore::new());
let context_picker_menu_handle = PopoverMenuHandle::default();
let editor = cx.new_view(|cx| {
let mut editor = Editor::auto_height(80, cx);
editor.set_placeholder_text("Ask anything, @ to add context", cx);
editor.set_show_indent_guides(false, cx);
editor
});
Self {
thread,
editor: cx.new_view(|cx| {
let mut editor = Editor::auto_height(80, cx);
editor.set_placeholder_text("Ask anything or type @ to add context", cx);
editor
editor: editor.clone(),
context_store: context_store.clone(),
context_strip: cx.new_view(|cx| {
ContextStrip::new(
context_store,
workspace.clone(),
Some(thread_store.clone()),
editor.focus_handle(cx),
context_picker_menu_handle.clone(),
cx,
)
}),
context: Vec::new(),
next_context_id: ContextId(0),
context_picker: cx.new_view(|cx| ContextPicker::new(workspace.clone(), weak_self, cx)),
context_picker_handle: PopoverMenuHandle::default(),
context_picker_menu_handle,
language_model_selector: cx.new_view(|cx| {
let fs = fs.clone();
LanguageModelSelector::new(
|model, _cx| {
println!("Selected {:?}", model.name());
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _cx| settings.set_model(model.clone()),
);
},
cx,
)
}),
language_model_selector_menu_handle: PopoverMenuHandle::default(),
use_tools: false,
}
}
pub fn insert_context(
&mut self,
kind: ContextKind,
name: impl Into<SharedString>,
text: impl Into<SharedString>,
) {
self.context.push(Context {
id: self.next_context_id.post_inc(),
name: name.into(),
kind,
text: text.into(),
});
fn toggle_model_selector(&mut self, _: &ToggleModelSelector, cx: &mut ViewContext<Self>) {
self.language_model_selector_menu_handle.toggle(cx);
}
fn toggle_context_picker(&mut self, _: &ToggleContextPicker, cx: &mut ViewContext<Self>) {
self.context_picker_menu_handle.toggle(cx);
}
fn chat(&mut self, _: &Chat, cx: &mut ViewContext<Self>) {
@@ -100,7 +118,7 @@ impl MessageEditor {
editor.clear(cx);
text
});
let context = self.context.drain(..).collect::<Vec<_>>();
let context = self.context_store.update(cx, |this, _cx| this.drain());
self.thread.update(cx, |thread, cx| {
thread.insert_user_message(user_message, context, cx);
@@ -126,8 +144,8 @@ impl MessageEditor {
}
fn render_language_model_selector(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
let active_model = LanguageModelRegistry::read_global(cx).active_model();
let focus_handle = self.language_model_selector.focus_handle(cx).clone();
LanguageModelSelectorPopoverMenu::new(
self.language_model_selector.clone(),
@@ -142,16 +160,8 @@ impl MessageEditor {
.overflow_x_hidden()
.flex_grow()
.whitespace_nowrap()
.child(match (active_provider, active_model) {
(Some(provider), Some(model)) => h_flex()
.gap_1()
.child(
Icon::new(
model.icon().unwrap_or_else(|| provider.icon()),
)
.color(Color::Muted)
.size(IconSize::XSmall),
)
.child(match active_model {
Some(model) => h_flex()
.child(
Label::new(model.name().0)
.size(LabelSize::Small)
@@ -170,8 +180,11 @@ impl MessageEditor {
.size(IconSize::XSmall),
),
)
.tooltip(move |cx| Tooltip::for_action("Change Model", &ToggleModelSelector, cx)),
.tooltip(move |cx| {
Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
}),
)
.with_handle(self.language_model_selector_menu_handle.clone())
}
}
@@ -184,60 +197,21 @@ impl FocusableView for MessageEditor {
impl Render for MessageEditor {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let font_size = TextSize::Default.rems(cx);
let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
let line_height = font_size.to_pixels(cx.rem_size()) * 1.5;
let focus_handle = self.editor.focus_handle(cx);
let context_picker = self.context_picker.clone();
let bg_color = cx.theme().colors().editor_background;
v_flex()
.key_context("MessageEditor")
.on_action(cx.listener(Self::chat))
.on_action(cx.listener(Self::toggle_model_selector))
.on_action(cx.listener(Self::toggle_context_picker))
.size_full()
.gap_2()
.p_2()
.bg(cx.theme().colors().editor_background)
.child(
h_flex()
.flex_wrap()
.gap_2()
.child(
PopoverMenu::new("context-picker")
.menu(move |_cx| Some(context_picker.clone()))
.trigger(
IconButton::new("add-context", IconName::Plus)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small),
)
.attach(gpui::AnchorCorner::TopLeft)
.anchor(gpui::AnchorCorner::BottomLeft)
.offset(gpui::Point {
x: px(0.0),
y: px(-16.0),
})
.with_handle(self.context_picker_handle.clone()),
)
.children(self.context.iter().map(|context| {
ContextPill::new(context.clone()).on_remove({
let context = context.clone();
Rc::new(cx.listener(move |this, _event, cx| {
this.context.retain(|other| other.id != context.id);
cx.notify();
}))
})
}))
.when(!self.context.is_empty(), |parent| {
parent.child(
IconButton::new("remove-all-context", IconName::Eraser)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.tooltip(move |cx| Tooltip::text("Remove All Context", cx))
.on_click(cx.listener(|this, _event, cx| {
this.context.clear();
cx.notify();
})),
)
}),
)
.child({
.bg(bg_color)
.child(self.context_strip.clone())
.child(div().id("thread_editor").overflow_y_scroll().h_12().child({
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: cx.theme().colors().editor_foreground,
@@ -252,17 +226,17 @@ impl Render for MessageEditor {
EditorElement::new(
&self.editor,
EditorStyle {
background: cx.theme().colors().editor_background,
background: bg_color,
local_player: cx.theme().players().local(),
text: text_style,
..Default::default()
},
)
})
}))
.child(
h_flex()
.justify_between()
.child(h_flex().gap_2().child(ToggleWithLabel::new(
.child(CheckboxWithLabel::new(
"use-tools",
Label::new("Tools"),
self.use_tools.into(),
@@ -272,10 +246,10 @@ impl Render for MessageEditor {
ToggleState::Unselected | ToggleState::Indeterminate => false,
};
}),
)))
))
.child(
h_flex()
.gap_2()
.gap_1()
.child(self.render_language_model_selector(cx))
.child(
ButtonLike::new("chat")

View File

@@ -1,5 +1,11 @@
use crate::assistant_settings::AssistantSettings;
use crate::context::attach_context_to_message;
use crate::context_picker::ContextPicker;
use crate::context_store::ContextStore;
use crate::context_strip::ContextStrip;
use crate::prompts::PromptBuilder;
use crate::thread_store::ThreadStore;
use crate::ToggleContextPicker;
use anyhow::{Context as _, Result};
use client::telemetry::Telemetry;
use collections::{HashMap, VecDeque};
@@ -11,7 +17,7 @@ use fs::Fs;
use futures::{channel::mpsc, SinkExt, StreamExt};
use gpui::{
AppContext, Context, EventEmitter, FocusHandle, FocusableView, Global, Model, ModelContext,
Subscription, Task, TextStyle, UpdateGlobal, View, WeakView,
Subscription, Task, TextStyle, UpdateGlobal, View, WeakModel, WeakView,
};
use language::Buffer;
use language_model::{
@@ -25,7 +31,7 @@ use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
use terminal::Terminal;
use terminal_view::TerminalView;
use theme::ThemeSettings;
use ui::{prelude::*, text_for_action, IconButtonShape, Tooltip};
use ui::{prelude::*, text_for_action, IconButtonShape, PopoverMenuHandle, Tooltip};
use util::ResultExt;
use workspace::{notifications::NotificationId, Toast, Workspace};
@@ -82,7 +88,8 @@ impl TerminalInlineAssistant {
pub fn assist(
&mut self,
terminal_view: &View<TerminalView>,
workspace: Option<WeakView<Workspace>>,
workspace: WeakView<Workspace>,
thread_store: Option<WeakModel<ThreadStore>>,
cx: &mut WindowContext,
) {
let terminal = terminal_view.read(cx).terminal().clone();
@@ -90,6 +97,7 @@ impl TerminalInlineAssistant {
let prompt_buffer = cx.new_model(|cx| {
MultiBuffer::singleton(cx.new_model(|cx| Buffer::local(String::new(), cx)), cx)
});
let context_store = cx.new_model(|_cx| ContextStore::new());
let codegen = cx.new_model(|_| Codegen::new(terminal, self.telemetry.clone()));
let prompt_editor = cx.new_view(|cx| {
@@ -99,6 +107,9 @@ impl TerminalInlineAssistant {
prompt_buffer.clone(),
codegen,
self.fs.clone(),
context_store.clone(),
workspace.clone(),
thread_store.clone(),
cx,
)
});
@@ -116,6 +127,7 @@ impl TerminalInlineAssistant {
terminal_view,
prompt_editor,
workspace.clone(),
context_store,
cx,
);
@@ -246,12 +258,21 @@ impl TerminalInlineAssistant {
&latest_output,
)?;
let mut request_message = LanguageModelRequestMessage {
role: Role::User,
content: vec![],
cache: false,
};
let context = assist
.context_store
.update(cx, |this, _cx| this.context().clone());
attach_context_to_message(&mut request_message, context);
request_message.content.push(prompt.into());
Ok(LanguageModelRequest {
messages: vec![LanguageModelRequestMessage {
role: Role::User,
content: vec![prompt.into()],
cache: false,
}],
messages: vec![request_message],
tools: Vec::new(),
stop: Vec::new(),
temperature: None,
@@ -361,7 +382,8 @@ struct TerminalInlineAssist {
terminal: WeakView<TerminalView>,
prompt_editor: Option<View<PromptEditor>>,
codegen: Model<Codegen>,
workspace: Option<WeakView<Workspace>>,
workspace: WeakView<Workspace>,
context_store: Model<ContextStore>,
_subscriptions: Vec<Subscription>,
}
@@ -370,7 +392,8 @@ impl TerminalInlineAssist {
assist_id: TerminalInlineAssistId,
terminal: &View<TerminalView>,
prompt_editor: View<PromptEditor>,
workspace: Option<WeakView<Workspace>>,
workspace: WeakView<Workspace>,
context_store: Model<ContextStore>,
cx: &mut WindowContext,
) -> Self {
let codegen = prompt_editor.read(cx).codegen.clone();
@@ -379,6 +402,7 @@ impl TerminalInlineAssist {
prompt_editor: Some(prompt_editor.clone()),
codegen: codegen.clone(),
workspace: workspace.clone(),
context_store,
_subscriptions: vec![
cx.subscribe(&prompt_editor, |prompt_editor, event, cx| {
TerminalInlineAssistant::update_global(cx, |this, cx| {
@@ -396,11 +420,7 @@ impl TerminalInlineAssist {
if let CodegenStatus::Error(error) = &codegen.read(cx).status {
if assist.prompt_editor.is_none() {
if let Some(workspace) = assist
.workspace
.as_ref()
.and_then(|workspace| workspace.upgrade())
{
if let Some(workspace) = assist.workspace.upgrade() {
let error =
format!("Terminal inline assistant error: {}", error);
workspace.update(cx, |workspace, cx| {
@@ -441,6 +461,8 @@ struct PromptEditor {
id: TerminalInlineAssistId,
height_in_lines: u8,
editor: View<Editor>,
context_strip: View<ContextStrip>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
language_model_selector: View<LanguageModelSelector>,
edited_since_done: bool,
prompt_history: VecDeque<String>,
@@ -456,11 +478,7 @@ impl EventEmitter<PromptEditorEvent> for PromptEditor {}
impl Render for PromptEditor {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let status = &self.codegen.read(cx).status;
let mut buttons = vec![Button::new("add-context", "Add Context")
.style(ButtonStyle::Filled)
.icon(IconName::Plus)
.icon_position(IconPosition::Start)
.into_any_element()];
let mut buttons = Vec::new();
buttons.extend(match status {
CodegenStatus::Idle => vec![
@@ -558,64 +576,71 @@ impl Render for PromptEditor {
}
});
h_flex()
.bg(cx.theme().colors().editor_background)
v_flex()
.border_y_1()
.border_color(cx.theme().status().info_border)
.py_2()
.h_full()
.w_full()
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::secondary_confirm))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::move_up))
.on_action(cx.listener(Self::move_down))
.size_full()
.child(
h_flex()
.w_12()
.justify_center()
.gap_2()
.child(LanguageModelSelectorPopoverMenu::new(
self.language_model_selector.clone(),
IconButton::new("context", IconName::SettingsAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.tooltip(move |cx| {
Tooltip::with_meta(
format!(
"Using {}",
LanguageModelRegistry::read_global(cx)
.active_model()
.map(|model| model.name().0)
.unwrap_or_else(|| "No model selected".into()),
),
None,
"Change Model",
cx,
)
}),
))
.children(
if let CodegenStatus::Error(error) = &self.codegen.read(cx).status {
let error_message = SharedString::from(error.to_string());
Some(
div()
.id("error")
.tooltip(move |cx| Tooltip::text(error_message.clone(), cx))
.child(
Icon::new(IconName::XCircle)
.size(IconSize::Small)
.color(Color::Error),
),
)
} else {
None
},
),
.key_context("PromptEditor")
.bg(cx.theme().colors().editor_background)
.on_action(cx.listener(Self::toggle_context_picker))
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::secondary_confirm))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::move_up))
.on_action(cx.listener(Self::move_down))
.child(
h_flex()
.w_12()
.justify_center()
.gap_2()
.child(LanguageModelSelectorPopoverMenu::new(
self.language_model_selector.clone(),
IconButton::new("context", IconName::SettingsAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.tooltip(move |cx| {
Tooltip::with_meta(
format!(
"Using {}",
LanguageModelRegistry::read_global(cx)
.active_model()
.map(|model| model.name().0)
.unwrap_or_else(|| "No model selected".into()),
),
None,
"Change Model",
cx,
)
}),
))
.children(
if let CodegenStatus::Error(error) = &self.codegen.read(cx).status {
let error_message = SharedString::from(error.to_string());
Some(
div()
.id("error")
.tooltip(move |cx| {
Tooltip::text(error_message.clone(), cx)
})
.child(
Icon::new(IconName::XCircle)
.size(IconSize::Small)
.color(Color::Error),
),
)
} else {
None
},
),
)
.child(div().flex_1().child(self.render_prompt_editor(cx)))
.child(h_flex().gap_1().pr_4().children(buttons)),
)
.child(div().flex_1().child(self.render_prompt_editor(cx)))
.child(h_flex().gap_1().pr_4().children(buttons))
.child(h_flex().child(self.context_strip.clone()))
}
}
@@ -635,6 +660,9 @@ impl PromptEditor {
prompt_buffer: Model<MultiBuffer>,
codegen: Model<Codegen>,
fs: Arc<dyn Fs>,
context_store: Model<ContextStore>,
workspace: WeakView<Workspace>,
thread_store: Option<WeakModel<ThreadStore>>,
cx: &mut ViewContext<Self>,
) -> Self {
let prompt_editor = cx.new_view(|cx| {
@@ -651,11 +679,23 @@ impl PromptEditor {
editor.set_placeholder_text(Self::placeholder_text(cx), cx);
editor
});
let context_picker_menu_handle = PopoverMenuHandle::default();
let mut this = Self {
id,
height_in_lines: 1,
editor: prompt_editor,
editor: prompt_editor.clone(),
context_strip: cx.new_view(|cx| {
ContextStrip::new(
context_store,
workspace.clone(),
thread_store.clone(),
prompt_editor.focus_handle(cx),
context_picker_menu_handle.clone(),
cx,
)
}),
context_picker_menu_handle,
language_model_selector: cx.new_view(|cx| {
let fs = fs.clone();
LanguageModelSelector::new(
@@ -764,6 +804,10 @@ impl PromptEditor {
}
}
fn toggle_context_picker(&mut self, _: &ToggleContextPicker, cx: &mut ViewContext<Self>) {
self.context_picker_menu_handle.toggle(cx);
}
fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
match &self.codegen.read(cx).status {
CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {

View File

@@ -17,7 +17,7 @@ use serde::{Deserialize, Serialize};
use util::{post_inc, TryFutureExt as _};
use uuid::Uuid;
use crate::context::{Context, ContextKind};
use crate::context::{attach_context_to_message, Context};
#[derive(Debug, Clone, Copy)]
pub enum RequestKind {
@@ -192,26 +192,7 @@ impl Thread {
}
if let Some(context) = self.context_for_message(message.id) {
let mut file_context = String::new();
for context in context.iter() {
match context.kind {
ContextKind::File => {
file_context.push_str(&context.text);
file_context.push_str("\n");
}
}
}
let mut context_text = String::new();
if !file_context.is_empty() {
context_text.push_str("The following files are available:\n");
context_text.push_str(&file_context);
}
request_message
.content
.push(MessageContent::Text(context_text))
attach_context_to_message(&mut request_message, context.clone());
}
if !message.text.is_empty() {

View File

@@ -2,7 +2,7 @@ use gpui::{
uniform_list, AppContext, FocusHandle, FocusableView, Model, UniformListScrollHandle, WeakView,
};
use time::{OffsetDateTime, UtcOffset};
use ui::{prelude::*, IconButtonShape, ListItem};
use ui::{prelude::*, IconButtonShape, ListItem, ListItemSpacing, Tooltip};
use crate::thread::Thread;
use crate::thread_store::ThreadStore;
@@ -66,10 +66,10 @@ impl Render for ThreadHistory {
threads[range]
.iter()
.map(|thread| {
PastThread::new(
h_flex().w_full().pb_1().child(PastThread::new(
thread.clone(),
history.assistant_panel.clone(),
)
))
})
.collect()
},
@@ -117,17 +117,29 @@ impl RenderOnce for PastThread {
.unwrap_or(UtcOffset::UTC),
time_format::TimestampFormat::EnhancedAbsolute,
);
ListItem::new(("past-thread", self.thread.entity_id()))
.start_slot(Icon::new(IconName::MessageBubbles))
.child(Label::new(summary))
.outlined()
.start_slot(
Icon::new(IconName::MessageCircle)
.size(IconSize::Small)
.color(Color::Muted),
)
.spacing(ListItemSpacing::Sparse)
.child(Label::new(summary).size(LabelSize::Small).text_ellipsis())
.end_slot(
h_flex()
.gap_2()
.child(Label::new(thread_timestamp).color(Color::Disabled))
.child(
Label::new(thread_timestamp)
.color(Color::Disabled)
.size(LabelSize::Small),
)
.child(
IconButton::new("delete", IconName::TrashAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.tooltip(|cx| Tooltip::text("Delete Thread", cx))
.on_click({
let assistant_panel = self.assistant_panel.clone();
let id = id.clone();

View File

@@ -29,14 +29,17 @@ impl RenderOnce for ContextPill {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
h_flex()
.gap_1()
.px_1()
.pl_1p5()
.pr_0p5()
.pb(px(1.))
.border_1()
.border_color(cx.theme().colors().border)
.border_color(cx.theme().colors().border.opacity(0.5))
.bg(cx.theme().colors().element_background)
.rounded_md()
.child(Label::new(self.context.name.clone()).size(LabelSize::Small))
.when_some(self.on_remove, |parent, on_remove| {
parent.child(
IconButton::new("remove", IconName::Close)
IconButton::new(("remove", self.context.id.0), IconName::Close)
.shape(IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.on_click({

View File

@@ -51,6 +51,7 @@ tokio-socks = { version = "0.5.2", default-features = false, features = ["future
url.workspace = true
util.workspace = true
worktree.workspace = true
telemetry.workspace = true
[dev-dependencies]
clock = { workspace = true, features = ["test-support"] }

View File

@@ -4,7 +4,8 @@ use crate::{ChannelId, TelemetrySettings};
use anyhow::Result;
use clock::SystemClock;
use collections::{HashMap, HashSet};
use futures::Future;
use futures::channel::mpsc;
use futures::{Future, StreamExt};
use gpui::{AppContext, BackgroundExecutor, Task};
use http_client::{self, AsyncBody, HttpClient, HttpClientWithUrl, Method, Request};
use once_cell::sync::Lazy;
@@ -17,9 +18,8 @@ use std::io::Write;
use std::time::Instant;
use std::{env, mem, path::PathBuf, sync::Arc, time::Duration};
use telemetry_events::{
ActionEvent, AppEvent, AssistantEvent, CallEvent, EditEvent, EditorEvent, Event,
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, InlineCompletionRating,
InlineCompletionRatingEvent, ReplEvent, SettingEvent,
AppEvent, AssistantEvent, CallEvent, EditEvent, Event, EventRequestBody, EventWrapper,
InlineCompletionEvent, InlineCompletionRating, InlineCompletionRatingEvent, SettingEvent,
};
use util::{ResultExt, TryFutureExt};
use worktree::{UpdatedEntriesSet, WorktreeId};
@@ -245,7 +245,6 @@ impl Telemetry {
})
.detach();
// TODO: Replace all hardware stuff with nested SystemSpecs json
let this = Arc::new(Self {
clock,
http_client: client,
@@ -253,6 +252,21 @@ impl Telemetry {
state,
});
let (tx, mut rx) = mpsc::unbounded();
::telemetry::init(tx);
cx.background_executor()
.spawn({
let this = Arc::downgrade(&this);
async move {
while let Some(event) = rx.next().await {
let Some(state) = this.upgrade() else { break };
state.report_event(Event::Flexible(event))
}
}
})
.detach();
// We should only ever have one instance of Telemetry, leak the subscription to keep it alive
// rather than store in TelemetryState, complicating spawn as subscriptions are not Send
std::mem::forget(cx.on_app_quit({
@@ -320,27 +334,6 @@ impl Telemetry {
drop(state);
}
pub fn report_editor_event(
self: &Arc<Self>,
file_extension: Option<String>,
vim_mode: bool,
operation: &'static str,
copilot_enabled: bool,
copilot_enabled_for_language: bool,
is_via_ssh: bool,
) {
let event = Event::Editor(EditorEvent {
file_extension,
vim_mode,
operation: operation.into(),
copilot_enabled,
copilot_enabled_for_language,
is_via_ssh,
});
self.report_event(event)
}
pub fn report_inline_completion_event(
self: &Arc<Self>,
provider: String,
@@ -410,13 +403,6 @@ impl Telemetry {
self.report_event(event)
}
pub fn report_extension_event(self: &Arc<Self>, extension_id: Arc<str>, version: Arc<str>) {
self.report_event(Event::Extension(ExtensionEvent {
extension_id,
version,
}))
}
pub fn log_edit_event(self: &Arc<Self>, environment: &'static str, is_via_ssh: bool) {
let mut state = self.state.lock();
let period_data = state.event_coalescer.log_event(environment);
@@ -436,15 +422,6 @@ impl Telemetry {
}
}
pub fn report_action_event(self: &Arc<Self>, source: &'static str, action: String) {
let event = Event::Action(ActionEvent {
source: source.to_string(),
action,
});
self.report_event(event)
}
pub fn report_discovered_project_events(
self: &Arc<Self>,
worktree_id: WorktreeId,
@@ -491,21 +468,6 @@ impl Telemetry {
}
}
pub fn report_repl_event(
self: &Arc<Self>,
kernel_language: String,
kernel_status: String,
repl_session_id: String,
) {
let event = Event::Repl(ReplEvent {
kernel_language,
kernel_status,
repl_session_id,
});
self.report_event(event)
}
fn report_event(self: &Arc<Self>, event: Event) {
let mut state = self.state.lock();

View File

@@ -610,6 +610,10 @@ fn for_snowflake(
"Kernel Status Changed".to_string(),
serde_json::to_value(e).unwrap(),
),
Event::Flexible(e) => (
e.event_type.clone(),
serde_json::to_value(&e.event_properties).unwrap(),
),
};
if let serde_json::Value::Object(ref mut map) = event_properties {

View File

@@ -10,7 +10,7 @@ use editor::{
ToggleCodeActions, Undo,
},
test::editor_test_context::{AssertionContextManager, EditorTestContext},
Editor,
Editor, RowInfo,
};
use fs::Fs;
use futures::StreamExt;
@@ -20,7 +20,6 @@ use language::{
language_settings::{AllLanguageSettings, InlayHintSettings},
FakeLspAdapter,
};
use multi_buffer::MultiBufferRow;
use project::{
project_settings::{InlineBlameSettings, ProjectSettings},
SERVER_PROGRESS_THROTTLE_TIMEOUT,
@@ -2075,7 +2074,15 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
let blame = editor_b.blame().expect("editor_b should have blame now");
let entries = blame.update(cx, |blame, cx| {
blame
.blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
.blame_for_rows(
&(0..4)
.map(|row| RowInfo {
buffer_row: Some(row),
..Default::default()
})
.collect::<Vec<_>>(),
cx,
)
.collect::<Vec<_>>()
});
@@ -2114,7 +2121,15 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
let blame = editor_b.blame().expect("editor_b should have blame now");
let entries = blame.update(cx, |blame, cx| {
blame
.blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
.blame_for_rows(
&(0..4)
.map(|row| RowInfo {
buffer_row: Some(row),
..Default::default()
})
.collect::<Vec<_>>(),
cx,
)
.collect::<Vec<_>>()
});
@@ -2141,7 +2156,15 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
let blame = editor_b.blame().expect("editor_b should have blame now");
let entries = blame.update(cx, |blame, cx| {
blame
.blame_for_rows((0..4).map(MultiBufferRow).map(Some), cx)
.blame_for_rows(
&(0..4)
.map(|row| RowInfo {
buffer_row: Some(row),
..Default::default()
})
.collect::<Vec<_>>(),
cx,
)
.collect::<Vec<_>>()
});

View File

@@ -44,7 +44,6 @@ gpui.workspace = true
language.workspace = true
menu.workspace = true
notifications.workspace = true
parking_lot.workspace = true
picker.workspace = true
project.workspace = true
release_channel.workspace = true

View File

@@ -12,10 +12,15 @@ use language::{
language_settings::SoftWrap, Anchor, Buffer, BufferSnapshot, CodeLabel, LanguageRegistry,
LanguageServerId, ToOffset,
};
use parking_lot::RwLock;
use project::{search::SearchQuery, Completion};
use settings::Settings;
use std::{ops::Range, sync::Arc, sync::LazyLock, time::Duration};
use std::{
cell::RefCell,
ops::Range,
rc::Rc,
sync::{Arc, LazyLock},
time::Duration,
};
use theme::ThemeSettings;
use ui::{prelude::*, TextSize};
@@ -68,7 +73,7 @@ impl CompletionProvider for MessageEditorCompletionProvider {
&self,
_buffer: Model<Buffer>,
_completion_indices: Vec<usize>,
_completions: Arc<RwLock<Box<[Completion]>>>,
_completions: Rc<RefCell<Box<[Completion]>>>,
_cx: &mut ViewContext<Editor>,
) -> Task<anyhow::Result<bool>> {
Task::ready(Ok(false))
@@ -381,11 +386,7 @@ impl MessageEditor {
let candidates = names
.into_iter()
.map(|user| StringMatchCandidate {
id: 0,
string: user.clone(),
char_bag: user.chars().collect(),
})
.map(|user| StringMatchCandidate::new(0, &user))
.collect::<Vec<_>>();
Some((start_anchor, query, candidates))
@@ -401,11 +402,7 @@ impl MessageEditor {
LazyLock::new(|| {
let emojis = emojis::iter()
.flat_map(|s| s.shortcodes())
.map(|emoji| StringMatchCandidate {
id: 0,
string: emoji.to_string(),
char_bag: emoji.chars().collect(),
})
.map(|emoji| StringMatchCandidate::new(0, emoji))
.collect::<Vec<_>>();
emojis
});

View File

@@ -393,11 +393,8 @@ impl CollabPanel {
// Populate the active user.
if let Some(user) = user_store.current_user() {
self.match_candidates.clear();
self.match_candidates.push(StringMatchCandidate {
id: 0,
string: user.github_login.clone(),
char_bag: user.github_login.chars().collect(),
});
self.match_candidates
.push(StringMatchCandidate::new(0, &user.github_login));
let matches = executor.block(match_strings(
&self.match_candidates,
&query,
@@ -436,11 +433,10 @@ impl CollabPanel {
self.match_candidates.clear();
self.match_candidates
.extend(room.remote_participants().values().map(|participant| {
StringMatchCandidate {
id: participant.user.id as usize,
string: participant.user.github_login.clone(),
char_bag: participant.user.github_login.chars().collect(),
}
StringMatchCandidate::new(
participant.user.id as usize,
&participant.user.github_login,
)
}));
let mut matches = executor.block(match_strings(
&self.match_candidates,
@@ -489,10 +485,8 @@ impl CollabPanel {
self.match_candidates.clear();
self.match_candidates
.extend(room.pending_participants().iter().enumerate().map(
|(id, participant)| StringMatchCandidate {
id,
string: participant.github_login.clone(),
char_bag: participant.github_login.chars().collect(),
|(id, participant)| {
StringMatchCandidate::new(id, &participant.github_login)
},
));
let matches = executor.block(match_strings(
@@ -519,17 +513,12 @@ impl CollabPanel {
if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
self.match_candidates.clear();
self.match_candidates
.extend(
channel_store
.ordered_channels()
.enumerate()
.map(|(ix, (_, channel))| StringMatchCandidate {
id: ix,
string: channel.name.clone().into(),
char_bag: channel.name.chars().collect(),
}),
);
self.match_candidates.extend(
channel_store
.ordered_channels()
.enumerate()
.map(|(ix, (_, channel))| StringMatchCandidate::new(ix, &channel.name)),
);
let matches = executor.block(match_strings(
&self.match_candidates,
&query,
@@ -600,14 +589,12 @@ impl CollabPanel {
let channel_invites = channel_store.channel_invitations();
if !channel_invites.is_empty() {
self.match_candidates.clear();
self.match_candidates
.extend(channel_invites.iter().enumerate().map(|(ix, channel)| {
StringMatchCandidate {
id: ix,
string: channel.name.clone().into(),
char_bag: channel.name.chars().collect(),
}
}));
self.match_candidates.extend(
channel_invites
.iter()
.enumerate()
.map(|(ix, channel)| StringMatchCandidate::new(ix, &channel.name)),
);
let matches = executor.block(match_strings(
&self.match_candidates,
&query,
@@ -637,17 +624,12 @@ impl CollabPanel {
let incoming = user_store.incoming_contact_requests();
if !incoming.is_empty() {
self.match_candidates.clear();
self.match_candidates
.extend(
incoming
.iter()
.enumerate()
.map(|(ix, user)| StringMatchCandidate {
id: ix,
string: user.github_login.clone(),
char_bag: user.github_login.chars().collect(),
}),
);
self.match_candidates.extend(
incoming
.iter()
.enumerate()
.map(|(ix, user)| StringMatchCandidate::new(ix, &user.github_login)),
);
let matches = executor.block(match_strings(
&self.match_candidates,
&query,
@@ -666,17 +648,12 @@ impl CollabPanel {
let outgoing = user_store.outgoing_contact_requests();
if !outgoing.is_empty() {
self.match_candidates.clear();
self.match_candidates
.extend(
outgoing
.iter()
.enumerate()
.map(|(ix, user)| StringMatchCandidate {
id: ix,
string: user.github_login.clone(),
char_bag: user.github_login.chars().collect(),
}),
);
self.match_candidates.extend(
outgoing
.iter()
.enumerate()
.map(|(ix, user)| StringMatchCandidate::new(ix, &user.github_login)),
);
let matches = executor.block(match_strings(
&self.match_candidates,
&query,
@@ -703,17 +680,12 @@ impl CollabPanel {
let contacts = user_store.contacts();
if !contacts.is_empty() {
self.match_candidates.clear();
self.match_candidates
.extend(
contacts
.iter()
.enumerate()
.map(|(ix, contact)| StringMatchCandidate {
id: ix,
string: contact.user.github_login.clone(),
char_bag: contact.user.github_login.chars().collect(),
}),
);
self.match_candidates.extend(
contacts
.iter()
.enumerate()
.map(|(ix, contact)| StringMatchCandidate::new(ix, &contact.user.github_login)),
);
let matches = executor.block(match_strings(
&self.match_candidates,
@@ -2736,7 +2708,7 @@ impl Render for CollabPanel {
deferred(
anchored()
.position(*position)
.anchor(gpui::AnchorCorner::TopLeft)
.anchor(gpui::Corner::TopLeft)
.child(menu.clone()),
)
.with_priority(1)

View File

@@ -11,7 +11,7 @@ use gpui::{
};
use picker::{Picker, PickerDelegate};
use std::sync::Arc;
use ui::{prelude::*, Avatar, ToggleWithLabel, ContextMenu, ListItem, ListItemSpacing};
use ui::{prelude::*, Avatar, CheckboxWithLabel, ContextMenu, ListItem, ListItemSpacing};
use util::TryFutureExt;
use workspace::{notifications::DetachAndPromptErr, ModalView};
@@ -155,7 +155,7 @@ impl Render for ChannelModal {
.h(rems_from_px(22.))
.justify_between()
.line_height(rems(1.25))
.child(ToggleWithLabel::new(
.child(CheckboxWithLabel::new(
"is-public",
Label::new("Public").size(LabelSize::Small),
if visibility == ChannelVisibility::Public {
@@ -272,11 +272,7 @@ impl PickerDelegate for ChannelModalDelegate {
self.match_candidates.clear();
self.match_candidates
.extend(self.members.iter().enumerate().map(|(id, member)| {
StringMatchCandidate {
id,
string: member.user.github_login.clone(),
char_bag: member.user.github_login.chars().collect(),
}
StringMatchCandidate::new(id, &member.user.github_login)
}));
let matches = cx.background_executor().block(match_strings(
@@ -413,7 +409,7 @@ impl PickerDelegate for ChannelModalDelegate {
Some(
deferred(
anchored()
.anchor(gpui::AnchorCorner::TopRight)
.anchor(gpui::Corner::TopRight)
.child(menu.clone()),
)
.with_priority(1),

View File

@@ -44,7 +44,7 @@ fn notification_window_options(
let notification_margin_height = px(-48.);
let bounds = gpui::Bounds::<Pixels> {
origin: screen.bounds().upper_right()
origin: screen.bounds().top_right()
- point(
size.width + notification_margin_width,
notification_margin_height,

View File

@@ -25,6 +25,7 @@ settings.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
telemetry.workspace = true
workspace.workspace = true
zed_actions.workspace = true

View File

@@ -4,7 +4,7 @@ use std::{
time::Duration,
};
use client::{parse_zed_link, telemetry::Telemetry};
use client::parse_zed_link;
use collections::HashMap;
use command_palette_hooks::{
CommandInterceptResult, CommandPaletteFilter, CommandPaletteInterceptor,
@@ -63,18 +63,12 @@ impl CommandPalette {
let Some(previous_focus_handle) = cx.focused() else {
return;
};
let telemetry = workspace.client().telemetry().clone();
workspace.toggle_modal(cx, move |cx| {
CommandPalette::new(previous_focus_handle, telemetry, query, cx)
CommandPalette::new(previous_focus_handle, query, cx)
});
}
fn new(
previous_focus_handle: FocusHandle,
telemetry: Arc<Telemetry>,
query: &str,
cx: &mut ViewContext<Self>,
) -> Self {
fn new(previous_focus_handle: FocusHandle, query: &str, cx: &mut ViewContext<Self>) -> Self {
let filter = CommandPaletteFilter::try_global(cx);
let commands = cx
@@ -92,12 +86,8 @@ impl CommandPalette {
})
.collect();
let delegate = CommandPaletteDelegate::new(
cx.view().downgrade(),
commands,
telemetry,
previous_focus_handle,
);
let delegate =
CommandPaletteDelegate::new(cx.view().downgrade(), commands, previous_focus_handle);
let picker = cx.new_view(|cx| {
let picker = Picker::uniform_list(delegate, cx);
@@ -133,7 +123,6 @@ pub struct CommandPaletteDelegate {
commands: Vec<Command>,
matches: Vec<StringMatch>,
selected_ix: usize,
telemetry: Arc<Telemetry>,
previous_focus_handle: FocusHandle,
updating_matches: Option<(
Task<()>,
@@ -167,7 +156,6 @@ impl CommandPaletteDelegate {
fn new(
command_palette: WeakView<CommandPalette>,
commands: Vec<Command>,
telemetry: Arc<Telemetry>,
previous_focus_handle: FocusHandle,
) -> Self {
Self {
@@ -176,7 +164,6 @@ impl CommandPaletteDelegate {
matches: vec![],
commands,
selected_ix: 0,
telemetry,
previous_focus_handle,
updating_matches: None,
}
@@ -283,11 +270,7 @@ impl PickerDelegate for CommandPaletteDelegate {
let candidates = commands
.iter()
.enumerate()
.map(|(ix, command)| StringMatchCandidate {
id: ix,
string: command.name.to_string(),
char_bag: command.name.chars().collect(),
})
.map(|(ix, command)| StringMatchCandidate::new(ix, &command.name))
.collect::<Vec<_>>();
let matches = if query.is_empty() {
candidates
@@ -371,9 +354,11 @@ impl PickerDelegate for CommandPaletteDelegate {
let action_ix = self.matches[self.selected_ix].candidate_id;
let command = self.commands.swap_remove(action_ix);
self.telemetry
.report_action_event("command palette", command.name.clone());
telemetry::event!(
"Action Invoked",
source = "command palette",
action = command.name
);
self.matches.clear();
self.commands.clear();
HitCounts::update_global(cx, |hit_counts, _cx| {

View File

@@ -19,6 +19,9 @@ pub fn init(cx: &mut AppContext) {
pub struct CommandPaletteFilter {
hidden_namespaces: HashSet<&'static str>,
hidden_action_types: HashSet<TypeId>,
/// Actions that have explicitly been shown. These should be shown even if
/// they are in a hidden namespace.
shown_action_types: HashSet<TypeId>,
}
#[derive(Deref, DerefMut, Default)]
@@ -53,6 +56,11 @@ impl CommandPaletteFilter {
let name = action.name();
let namespace = name.split("::").next().unwrap_or("malformed action name");
// If this action has specifically been shown then it should be visible.
if self.shown_action_types.contains(&action.type_id()) {
return false;
}
self.hidden_namespaces.contains(namespace)
|| self.hidden_action_types.contains(&action.type_id())
}
@@ -69,12 +77,16 @@ impl CommandPaletteFilter {
/// Hides all actions with the given types.
pub fn hide_action_types(&mut self, action_types: &[TypeId]) {
self.hidden_action_types.extend(action_types);
for action_type in action_types {
self.hidden_action_types.insert(*action_type);
self.shown_action_types.remove(action_type);
}
}
/// Shows all actions with the given types.
pub fn show_action_types<'a>(&mut self, action_types: impl Iterator<Item = &'a TypeId>) {
for action_type in action_types {
self.shown_action_types.insert(*action_type);
self.hidden_action_types.remove(action_type);
}
}

View File

@@ -55,6 +55,10 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
"copilot"
}
fn display_name() -> &'static str {
"Copilot"
}
fn is_enabled(
&self,
buffer: &Model<Buffer>,
@@ -324,10 +328,15 @@ mod tests {
cx.update_editor(|editor, cx| {
// We want to show both: the inline completion and the completion menu
assert!(editor.context_menu_visible());
assert!(editor.context_menu_contains_inline_completion());
assert!(editor.has_active_inline_completion());
// Since we have both, the copilot suggestion is not shown inline
assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
assert_eq!(editor.display_text(cx), "one.\ntwo\nthree\n");
// Confirming a completion inserts it and hides the context menu, without showing
// Confirming a non-copilot completion inserts it and hides the context menu, without showing
// the copilot suggestion afterwards.
editor.context_menu_next(&Default::default(), cx);
editor
.confirm_completion(&Default::default(), cx)
.unwrap()
@@ -338,13 +347,14 @@ mod tests {
assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n");
});
// Reset editor and test that accepting completions works
// Reset editor and only return copilot suggestions
cx.set_state(indoc! {"
oneˇ
two
three
"});
cx.simulate_keystroke(".");
drop(handle_completion_request(
&mut cx,
indoc! {"
@@ -352,7 +362,7 @@ mod tests {
two
three
"},
vec!["completion_a", "completion_b"],
vec![],
));
handle_copilot_completion_request(
&copilot_lsp,
@@ -365,19 +375,32 @@ mod tests {
);
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(editor.context_menu_visible());
assert!(!editor.context_menu_visible());
assert!(editor.has_active_inline_completion());
// Since only the copilot is available, it's shown inline
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
});
// Ensure existing inline completion is interpolated when inserting again.
cx.simulate_keystroke("c");
drop(handle_completion_request(
&mut cx,
indoc! {"
one.c|<>
two
three
"},
vec!["completion_a", "completion_b"],
));
executor.run_until_parked();
cx.update_editor(|editor, cx| {
assert!(!editor.context_menu_visible());
// Since we have an LSP completion too, the inline completion is
// shown in the menu now
assert!(editor.context_menu_visible());
assert!(editor.context_menu_contains_inline_completion());
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
});
@@ -393,6 +416,14 @@ mod tests {
);
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(editor.context_menu_visible());
assert!(editor.has_active_inline_completion());
assert!(editor.context_menu_contains_inline_completion());
assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
// Canceling should first hide the menu and make Copilot suggestion visible.
editor.cancel(&Default::default(), cx);
assert!(!editor.context_menu_visible());
assert!(editor.has_active_inline_completion());
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
@@ -897,8 +928,8 @@ mod tests {
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, cx| {
assert!(editor.context_menu_visible());
assert!(editor.context_menu_contains_inline_completion());
assert!(editor.has_active_inline_completion(),);
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
assert_eq!(editor.text(cx), "one\ntwo.\nthree\n");
});
}

View File

@@ -1050,6 +1050,7 @@ fn editor_blocks(
.ok()?
}
Block::FoldedBuffer { .. } => FILE_HEADER.into(),
Block::ExcerptBoundary {
starts_new_buffer, ..
} => {

View File

@@ -72,6 +72,7 @@ smol.workspace = true
snippet.workspace = true
sum_tree.workspace = true
task.workspace = true
telemetry.workspace = true
text.workspace = true
time.workspace = true
time_format.workspace = true

View File

@@ -1,24 +1,20 @@
use std::{cell::Cell, cmp::Reverse, ops::Range, sync::Arc};
use std::cell::RefCell;
use std::{cell::Cell, cmp::Reverse, ops::Range, rc::Rc};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
div, px, uniform_list, AnyElement, BackgroundExecutor, Div, FontWeight, ListSizingBehavior,
Model, MouseButton, Pixels, ScrollStrategy, SharedString, StrikethroughStyle, StyledText,
UniformListScrollHandle, ViewContext, WeakView,
Model, ScrollStrategy, SharedString, StrikethroughStyle, StyledText, UniformListScrollHandle,
ViewContext, WeakView,
};
use language::Buffer;
use language::{CodeLabel, Documentation};
use lsp::LanguageServerId;
use multi_buffer::{Anchor, ExcerptId};
use ordered_float::OrderedFloat;
use parking_lot::RwLock;
use project::{CodeAction, Completion, TaskSourceKind};
use task::ResolvedTask;
use ui::{
h_flex, ActiveTheme as _, Color, FluentBuilder as _, InteractiveElement as _, IntoElement,
Label, LabelCommon as _, LabelSize, ListItem, ParentElement as _, Popover,
StatefulInteractiveElement as _, Styled, StyledExt as _, Toggleable as _,
};
use ui::{prelude::*, Color, IntoElement, ListItem, Popover, Styled};
use util::ResultExt as _;
use workspace::Workspace;
@@ -28,6 +24,7 @@ use crate::{
render_parsed_markdown, split_words, styled_runs_for_code_label, CodeActionProvider,
CompletionId, CompletionProvider, DisplayRow, Editor, EditorStyle, ResolvedTasks,
};
use crate::{AcceptInlineCompletion, InlineCompletionMenuHint, InlineCompletionText};
pub enum CodeContextMenu {
Completions(CompletionsMenu),
@@ -106,22 +103,24 @@ impl CodeContextMenu {
}
}
pub fn origin(&self, cursor_position: DisplayPoint) -> ContextMenuOrigin {
match self {
CodeContextMenu::Completions(menu) => menu.origin(cursor_position),
CodeContextMenu::CodeActions(menu) => menu.origin(cursor_position),
}
}
pub fn render(
&self,
cursor_position: DisplayPoint,
style: &EditorStyle,
max_height: Pixels,
max_height_in_lines: u32,
workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> (ContextMenuOrigin, AnyElement) {
) -> AnyElement {
match self {
CodeContextMenu::Completions(menu) => (
ContextMenuOrigin::EditorPoint(cursor_position),
menu.render(style, max_height, workspace, cx),
),
CodeContextMenu::CodeActions(menu) => {
menu.render(cursor_position, style, max_height, cx)
CodeContextMenu::Completions(menu) => {
menu.render(style, max_height_in_lines, workspace, cx)
}
CodeContextMenu::CodeActions(menu) => menu.render(style, max_height_in_lines, cx),
}
}
}
@@ -137,9 +136,9 @@ pub struct CompletionsMenu {
sort_completions: bool,
pub initial_position: Anchor,
pub buffer: Model<Buffer>,
pub completions: Arc<RwLock<Box<[Completion]>>>,
match_candidates: Arc<[StringMatchCandidate]>,
pub matches: Arc<[StringMatch]>,
pub completions: Rc<RefCell<Box<[Completion]>>>,
match_candidates: Rc<[StringMatchCandidate]>,
pub entries: Rc<[CompletionEntry]>,
pub selected_item: usize,
scroll_handle: UniformListScrollHandle,
resolve_completions: bool,
@@ -147,6 +146,12 @@ pub struct CompletionsMenu {
show_completion_documentation: bool,
}
#[derive(Clone, Debug)]
pub(crate) enum CompletionEntry {
Match(StringMatch),
InlineCompletionHint(InlineCompletionMenuHint),
}
impl CompletionsMenu {
pub fn new(
id: CompletionId,
@@ -160,12 +165,7 @@ impl CompletionsMenu {
let match_candidates = completions
.iter()
.enumerate()
.map(|(id, completion)| {
StringMatchCandidate::new(
id,
completion.label.text[completion.label.filter_range.clone()].into(),
)
})
.map(|(id, completion)| StringMatchCandidate::new(id, &completion.label.filter_text()))
.collect();
Self {
@@ -174,9 +174,9 @@ impl CompletionsMenu {
initial_position,
buffer,
show_completion_documentation,
completions: Arc::new(RwLock::new(completions)),
completions: RefCell::new(completions).into(),
match_candidates,
matches: Vec::new().into(),
entries: Vec::new().into(),
selected_item: 0,
scroll_handle: UniformListScrollHandle::new(),
resolve_completions: true,
@@ -211,16 +211,18 @@ impl CompletionsMenu {
let match_candidates = choices
.iter()
.enumerate()
.map(|(id, completion)| StringMatchCandidate::new(id, completion.to_string()))
.map(|(id, completion)| StringMatchCandidate::new(id, &completion))
.collect();
let matches = choices
let entries = choices
.iter()
.enumerate()
.map(|(id, completion)| StringMatch {
candidate_id: id,
score: 1.,
positions: vec![],
string: completion.clone(),
.map(|(id, completion)| {
CompletionEntry::Match(StringMatch {
candidate_id: id,
score: 1.,
positions: vec![],
string: completion.clone(),
})
})
.collect();
Self {
@@ -228,9 +230,9 @@ impl CompletionsMenu {
sort_completions,
initial_position: selection.start,
buffer,
completions: Arc::new(RwLock::new(completions)),
completions: RefCell::new(completions).into(),
match_candidates,
matches,
entries,
selected_item: 0,
scroll_handle: UniformListScrollHandle::new(),
resolve_completions: false,
@@ -259,7 +261,7 @@ impl CompletionsMenu {
if self.selected_item > 0 {
self.selected_item -= 1;
} else {
self.selected_item = self.matches.len() - 1;
self.selected_item = self.entries.len() - 1;
}
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
@@ -272,7 +274,7 @@ impl CompletionsMenu {
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
if self.selected_item + 1 < self.matches.len() {
if self.selected_item + 1 < self.entries.len() {
self.selected_item += 1;
} else {
self.selected_item = 0;
@@ -288,13 +290,33 @@ impl CompletionsMenu {
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
self.selected_item = self.matches.len() - 1;
self.selected_item = self.entries.len() - 1;
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
self.resolve_selected_completion(provider, cx);
cx.notify();
}
pub fn show_inline_completion_hint(&mut self, hint: InlineCompletionMenuHint) {
let hint = CompletionEntry::InlineCompletionHint(hint);
self.entries = match self.entries.first() {
Some(CompletionEntry::InlineCompletionHint { .. }) => {
let mut entries = Vec::from(&*self.entries);
entries[0] = hint;
entries
}
_ => {
let mut entries = Vec::with_capacity(self.entries.len() + 1);
entries.push(hint);
entries.extend_from_slice(&self.entries);
entries
}
}
.into();
self.selected_item = 0;
}
pub fn resolve_selected_completion(
&mut self,
provider: Option<&dyn CompletionProvider>,
@@ -307,86 +329,114 @@ impl CompletionsMenu {
return;
};
let completion_index = self.matches[self.selected_item].candidate_id;
let resolve_task = provider.resolve_completions(
self.buffer.clone(),
vec![completion_index],
self.completions.clone(),
cx,
);
match &self.entries[self.selected_item] {
CompletionEntry::Match(entry) => {
let completion_index = entry.candidate_id;
let resolve_task = provider.resolve_completions(
self.buffer.clone(),
vec![completion_index],
self.completions.clone(),
cx,
);
cx.spawn(move |editor, mut cx| async move {
if let Some(true) = resolve_task.await.log_err() {
editor.update(&mut cx, |_, cx| cx.notify()).ok();
cx.spawn(move |editor, mut cx| async move {
if let Some(true) = resolve_task.await.log_err() {
editor.update(&mut cx, |_, cx| cx.notify()).ok();
}
})
.detach();
}
})
.detach();
CompletionEntry::InlineCompletionHint { .. } => {}
}
}
fn visible(&self) -> bool {
!self.matches.is_empty()
pub fn visible(&self) -> bool {
!self.entries.is_empty()
}
fn origin(&self, cursor_position: DisplayPoint) -> ContextMenuOrigin {
ContextMenuOrigin::EditorPoint(cursor_position)
}
fn render(
&self,
style: &EditorStyle,
max_height: Pixels,
max_height_in_lines: u32,
workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> AnyElement {
let max_height = max_height_in_lines as f32 * cx.line_height();
let completions = self.completions.borrow_mut();
let show_completion_documentation = self.show_completion_documentation;
let widest_completion_ix = self
.matches
.entries
.iter()
.enumerate()
.max_by_key(|(_, mat)| {
let completions = self.completions.read();
let completion = &completions[mat.candidate_id];
let documentation = &completion.documentation;
.max_by_key(|(_, mat)| match mat {
CompletionEntry::Match(mat) => {
let completion = &completions[mat.candidate_id];
let documentation = &completion.documentation;
let mut len = completion.label.text.chars().count();
if let Some(Documentation::SingleLine(text)) = documentation {
if show_completion_documentation {
len += text.chars().count();
let mut len = completion.label.text.chars().count();
if let Some(Documentation::SingleLine(text)) = documentation {
if show_completion_documentation {
len += text.chars().count();
}
}
}
len
len
}
CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint {
provider_name,
..
}) => provider_name.len(),
})
.map(|(ix, _)| ix);
let completions = self.completions.clone();
let matches = self.matches.clone();
let selected_item = self.selected_item;
let style = style.clone();
let multiline_docs = if show_completion_documentation {
let mat = &self.matches[selected_item];
match &self.completions.read()[mat.candidate_id].documentation {
Some(Documentation::MultiLinePlainText(text)) => {
Some(div().child(SharedString::from(text.clone())))
let multiline_docs = match &self.entries[selected_item] {
CompletionEntry::Match(mat) if show_completion_documentation => {
match &completions[mat.candidate_id].documentation {
Some(Documentation::MultiLinePlainText(text)) => {
Some(div().child(SharedString::from(text.clone())))
}
Some(Documentation::MultiLineMarkdown(parsed)) if !parsed.text.is_empty() => {
Some(div().child(render_parsed_markdown(
"completions_markdown",
parsed,
&style,
workspace,
cx,
)))
}
Some(Documentation::Undocumented) if self.aside_was_displayed.get() => {
Some(div().child("No documentation"))
}
_ => None,
}
Some(Documentation::MultiLineMarkdown(parsed)) if !parsed.text.is_empty() => {
Some(div().child(render_parsed_markdown(
"completions_markdown",
parsed,
&style,
workspace,
cx,
)))
}
Some(Documentation::Undocumented) if self.aside_was_displayed.get() => {
Some(div().child("No documentation"))
}
_ => None,
}
} else {
None
CompletionEntry::InlineCompletionHint(hint) => Some(match &hint.text {
InlineCompletionText::Edit { text, highlights } => div()
.my_1()
.rounded(px(6.))
.bg(cx.theme().colors().editor_background)
.border_1()
.border_color(cx.theme().colors().border_variant)
.child(
gpui::StyledText::new(text.clone())
.with_highlights(&style.text, highlights.clone()),
),
InlineCompletionText::Move(text) => div().child(text.clone()),
}),
_ => None,
};
let aside_contents = if let Some(multiline_docs) = multiline_docs {
Some(multiline_docs)
} else if self.aside_was_displayed.get() {
} else if show_completion_documentation && self.aside_was_displayed.get() {
Some(div().child("Fetching documentation..."))
} else {
None
@@ -399,102 +449,141 @@ impl CompletionsMenu {
.flex_1()
.px_1p5()
.py_1()
.min_w(px(260.))
.max_w(px(640.))
.w(px(500.))
.w(px(450.))
.overflow_y_scroll()
.occlude()
});
drop(completions);
let completions = self.completions.clone();
let matches = self.entries.clone();
let list = uniform_list(
cx.view().clone(),
"completions",
matches.len(),
move |_editor, range, cx| {
let start_ix = range.start;
let completions_guard = completions.read();
let completions_guard = completions.borrow_mut();
matches[range]
.iter()
.enumerate()
.map(|(ix, mat)| {
let item_ix = start_ix + ix;
let candidate_id = mat.candidate_id;
let completion = &completions_guard[candidate_id];
match mat {
CompletionEntry::Match(mat) => {
let candidate_id = mat.candidate_id;
let completion = &completions_guard[candidate_id];
let documentation = if show_completion_documentation {
&completion.documentation
} else {
&None
};
let highlights = gpui::combine_highlights(
mat.ranges().map(|range| (range, FontWeight::BOLD.into())),
styled_runs_for_code_label(&completion.label, &style.syntax).map(
|(range, mut highlight)| {
// Ignore font weight for syntax highlighting, as we'll use it
// for fuzzy matches.
highlight.font_weight = None;
if completion.lsp_completion.deprecated.unwrap_or(false) {
highlight.strikethrough = Some(StrikethroughStyle {
thickness: 1.0.into(),
..Default::default()
});
highlight.color = Some(cx.theme().colors().text_muted);
}
(range, highlight)
},
),
);
let completion_label = StyledText::new(completion.label.text.clone())
.with_highlights(&style.text, highlights);
let documentation_label =
if let Some(Documentation::SingleLine(text)) = documentation {
if text.trim().is_empty() {
None
let documentation = if show_completion_documentation {
&completion.documentation
} else {
Some(
Label::new(text.clone())
.ml_4()
.size(LabelSize::Small)
.color(Color::Muted),
&None
};
let filter_start = completion.label.filter_range.start;
let highlights = gpui::combine_highlights(
mat.ranges().map(|range| {
(
filter_start + range.start..filter_start + range.end,
FontWeight::BOLD.into(),
)
}),
styled_runs_for_code_label(&completion.label, &style.syntax)
.map(|(range, mut highlight)| {
// Ignore font weight for syntax highlighting, as we'll use it
// for fuzzy matches.
highlight.font_weight = None;
if completion.lsp_completion.deprecated.unwrap_or(false)
{
highlight.strikethrough =
Some(StrikethroughStyle {
thickness: 1.0.into(),
..Default::default()
});
highlight.color =
Some(cx.theme().colors().text_muted);
}
(range, highlight)
}),
);
let completion_label =
StyledText::new(completion.label.text.clone())
.with_highlights(&style.text, highlights);
let documentation_label =
if let Some(Documentation::SingleLine(text)) = documentation {
if text.trim().is_empty() {
None
} else {
Some(
Label::new(text.clone())
.ml_4()
.size(LabelSize::Small)
.color(Color::Muted),
)
}
} else {
None
};
let color_swatch = completion
.color()
.map(|color| div().size_4().bg(color).rounded_sm());
div().min_w(px(220.)).max_w(px(540.)).child(
ListItem::new(mat.candidate_id)
.inset(true)
.toggle_state(item_ix == selected_item)
.on_click(cx.listener(move |editor, _event, cx| {
cx.stop_propagation();
if let Some(task) = editor.confirm_completion(
&ConfirmCompletion {
item_ix: Some(item_ix),
},
cx,
) {
task.detach_and_log_err(cx)
}
}))
.start_slot::<Div>(color_swatch)
.child(h_flex().overflow_hidden().child(completion_label))
.end_slot::<Label>(documentation_label),
)
}
CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint {
provider_name,
..
}) => div().min_w(px(250.)).max_w(px(500.)).child(
ListItem::new("inline-completion")
.inset(true)
.toggle_state(item_ix == selected_item)
.start_slot(Icon::new(IconName::ZedPredict))
.child(
StyledText::new(format!(
"{} Completion",
SharedString::new_static(provider_name)
))
.with_highlights(&style.text, None),
)
}
} else {
None
};
let color_swatch = completion
.color()
.map(|color| div().size_4().bg(color).rounded_sm());
div().min_w(px(220.)).max_w(px(540.)).child(
ListItem::new(mat.candidate_id)
.inset(true)
.toggle_state(item_ix == selected_item)
.on_click(cx.listener(move |editor, _event, cx| {
cx.stop_propagation();
if let Some(task) = editor.confirm_completion(
&ConfirmCompletion {
item_ix: Some(item_ix),
},
cx,
) {
task.detach_and_log_err(cx)
}
}))
.start_slot::<Div>(color_swatch)
.child(h_flex().overflow_hidden().child(completion_label))
.end_slot::<Label>(documentation_label),
)
.on_click(cx.listener(move |editor, _event, cx| {
cx.stop_propagation();
editor.accept_inline_completion(
&AcceptInlineCompletion {},
cx,
);
})),
),
}
})
.collect()
},
)
.occlude()
.max_h(max_height)
.max_h(max_height_in_lines as f32 * cx.line_height())
.track_scroll(self.scroll_handle.clone())
.with_width_from_item(widest_completion_ix)
.with_sizing_behavior(ListSizingBehavior::Infer);
@@ -547,7 +636,7 @@ impl CompletionsMenu {
}
}
let completions = self.completions.read();
let completions = self.completions.borrow_mut();
if self.sort_completions {
matches.sort_unstable_by_key(|mat| {
// We do want to strike a balance here between what the language server tells us
@@ -599,17 +688,14 @@ impl CompletionsMenu {
}
});
}
for mat in &mut matches {
let completion = &completions[mat.candidate_id];
mat.string.clone_from(&completion.label.text);
for position in &mut mat.positions {
*position += completion.label.filter_range.start;
}
}
drop(completions);
self.matches = matches.into();
let mut new_entries: Vec<_> = matches.into_iter().map(CompletionEntry::Match).collect();
if let Some(CompletionEntry::InlineCompletionHint(hint)) = self.entries.first() {
new_entries.insert(0, CompletionEntry::InlineCompletionHint(hint.clone()));
}
self.entries = new_entries.into();
self.selected_item = 0;
}
}
@@ -618,13 +704,13 @@ impl CompletionsMenu {
pub struct AvailableCodeAction {
pub excerpt_id: ExcerptId,
pub action: CodeAction,
pub provider: Arc<dyn CodeActionProvider>,
pub provider: Rc<dyn CodeActionProvider>,
}
#[derive(Clone)]
pub struct CodeActionContents {
pub tasks: Option<Arc<ResolvedTasks>>,
pub actions: Option<Arc<[AvailableCodeAction]>>,
pub tasks: Option<Rc<ResolvedTasks>>,
pub actions: Option<Rc<[AvailableCodeAction]>>,
}
impl CodeActionContents {
@@ -709,7 +795,7 @@ pub enum CodeActionsItem {
CodeAction {
excerpt_id: ExcerptId,
action: CodeAction,
provider: Arc<dyn CodeActionProvider>,
provider: Rc<dyn CodeActionProvider>,
},
}
@@ -785,16 +871,23 @@ impl CodeActionsMenu {
!self.actions.is_empty()
}
fn origin(&self, cursor_position: DisplayPoint) -> ContextMenuOrigin {
if let Some(row) = self.deployed_from_indicator {
ContextMenuOrigin::GutterIndicator(row)
} else {
ContextMenuOrigin::EditorPoint(cursor_position)
}
}
fn render(
&self,
cursor_position: DisplayPoint,
_style: &EditorStyle,
max_height: Pixels,
max_height_in_lines: u32,
cx: &mut ViewContext<Editor>,
) -> (ContextMenuOrigin, AnyElement) {
) -> AnyElement {
let actions = self.actions.clone();
let selected_item = self.selected_item;
let element = uniform_list(
let list = uniform_list(
cx.view().clone(),
"code_actions_menu",
self.actions.len(),
@@ -806,27 +899,14 @@ impl CodeActionsMenu {
.enumerate()
.map(|(ix, action)| {
let item_ix = range.start + ix;
let selected = selected_item == item_ix;
let selected = item_ix == selected_item;
let colors = cx.theme().colors();
div()
.px_1()
.rounded_md()
.text_color(colors.text)
.when(selected, |style| {
style
.bg(colors.element_active)
.text_color(colors.text_accent)
})
.hover(|style| {
style
.bg(colors.element_hover)
.text_color(colors.text_accent)
})
.whitespace_nowrap()
.when_some(action.as_code_action(), |this, action| {
this.on_mouse_down(
MouseButton::Left,
cx.listener(move |editor, _, cx| {
div().min_w(px(220.)).max_w(px(540.)).child(
ListItem::new(item_ix)
.inset(true)
.toggle_state(selected)
.when_some(action.as_code_action(), |this, action| {
this.on_click(cx.listener(move |editor, _, cx| {
cx.stop_propagation();
if let Some(task) = editor.confirm_code_action(
&ConfirmCodeAction {
@@ -836,17 +916,21 @@ impl CodeActionsMenu {
) {
task.detach_and_log_err(cx)
}
}),
)
// TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
.child(SharedString::from(
action.lsp_action.title.replace("\n", ""),
))
})
.when_some(action.as_task(), |this, task| {
this.on_mouse_down(
MouseButton::Left,
cx.listener(move |editor, _, cx| {
}))
.child(
h_flex()
.overflow_hidden()
.child(
// TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
action.lsp_action.title.replace("\n", ""),
)
.when(selected, |this| {
this.text_color(colors.text_accent)
}),
)
})
.when_some(action.as_task(), |this, task| {
this.on_click(cx.listener(move |editor, _, cx| {
cx.stop_propagation();
if let Some(task) = editor.confirm_code_action(
&ConfirmCodeAction {
@@ -856,18 +940,23 @@ impl CodeActionsMenu {
) {
task.detach_and_log_err(cx)
}
}),
)
.child(SharedString::from(task.resolved_label.replace("\n", "")))
})
}))
.child(
h_flex()
.overflow_hidden()
.child(task.resolved_label.replace("\n", ""))
.when(selected, |this| {
this.text_color(colors.text_accent)
}),
)
}),
)
})
.collect()
},
)
.elevation_1(cx)
.p_1()
.max_h(max_height)
.occlude()
.max_h(max_height_in_lines as f32 * cx.line_height())
.track_scroll(self.scroll_handle.clone())
.with_width_from_item(
self.actions
@@ -881,15 +970,8 @@ impl CodeActionsMenu {
})
.map(|(ix, _)| ix),
)
.with_sizing_behavior(ListSizingBehavior::Infer)
.into_any_element();
.with_sizing_behavior(ListSizingBehavior::Infer);
let cursor_position = if let Some(row) = self.deployed_from_indicator {
ContextMenuOrigin::GutterIndicator(row)
} else {
ContextMenuOrigin::EditorPoint(cursor_position)
};
(cursor_position, element)
Popover::new().child(list).into_any_element()
}
}

View File

@@ -19,6 +19,8 @@
mod block_map;
mod crease_map;
mod custom_highlights;
mod diff_map;
mod fold_map;
mod inlay_map;
pub(crate) mod invisibles;
@@ -29,16 +31,18 @@ use crate::{
hover_links::InlayHighlight, movement::TextLayoutDetails, EditorStyle, InlayId, RowExt,
};
pub use block_map::{
Block, BlockBufferRows, BlockChunks as DisplayChunks, BlockContext, BlockId, BlockMap,
BlockPlacement, BlockPoint, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
Block, BlockChunks as DisplayChunks, BlockContext, BlockId, BlockMap, BlockPlacement,
BlockPoint, BlockProperties, BlockRows, BlockStyle, CustomBlockId, RenderBlock,
};
use block_map::{BlockRow, BlockSnapshot};
use collections::{HashMap, HashSet};
pub use crease_map::*;
use diff_map::{DiffMap, DiffMapSnapshot, DiffOffset, DiffPoint};
pub use fold_map::{Fold, FoldId, FoldPlaceholder, FoldPoint};
use fold_map::{FoldMap, FoldSnapshot};
use gpui::{
AnyElement, Font, HighlightStyle, LineLayout, Model, ModelContext, Pixels, UnderlineStyle,
AnyElement, AppContext, Font, HighlightStyle, LineLayout, Model, ModelContext, Pixels,
UnderlineStyle,
};
pub(crate) use inlay_map::Inlay;
use inlay_map::{InlayMap, InlaySnapshot};
@@ -50,9 +54,10 @@ use language::{
};
use lsp::DiagnosticSeverity;
use multi_buffer::{
Anchor, AnchorRangeExt, MultiBuffer, MultiBufferPoint, MultiBufferRow, MultiBufferSnapshot,
ToOffset, ToPoint,
Anchor, AnchorRangeExt, MultiBuffer, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow,
MultiBufferSnapshot, RowInfo, ToOffset, ToPoint,
};
use project::buffer_store::BufferChangeSet;
use serde::Deserialize;
use std::{
any::TypeId,
@@ -65,7 +70,7 @@ use std::{
};
use sum_tree::{Bias, TreeMap};
use tab_map::{TabMap, TabSnapshot};
use text::LineIndent;
use text::{BufferId, LineIndent};
use ui::{px, SharedString, WindowContext};
use unicode_segmentation::UnicodeSegmentation;
use wrap_map::{WrapMap, WrapSnapshot};
@@ -95,6 +100,8 @@ pub struct DisplayMap {
buffer_subscription: BufferSubscription,
/// Decides where the [`Inlay`]s should be displayed.
inlay_map: InlayMap,
/// Decides where diff hunks should be.
diff_map: Model<DiffMap>,
/// 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.
@@ -133,7 +140,8 @@ impl DisplayMap {
let tab_size = Self::tab_size(&buffer, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let crease_map = CreaseMap::new(&buffer_snapshot);
let (inlay_map, snapshot) = InlayMap::new(buffer_snapshot);
let (diff_map, snapshot) = DiffMap::new(buffer.clone(), cx);
let (inlay_map, snapshot) = InlayMap::new(snapshot);
let (fold_map, snapshot) = FoldMap::new(snapshot);
let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
let (wrap_map, snapshot) = WrapMap::new(snapshot, font, font_size, wrap_width, cx);
@@ -152,6 +160,7 @@ impl DisplayMap {
buffer_subscription,
fold_map,
inlay_map,
diff_map,
tab_map,
wrap_map,
block_map,
@@ -167,7 +176,10 @@ impl DisplayMap {
pub fn snapshot(&mut self, cx: &mut ModelContext<Self>) -> DisplaySnapshot {
let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let (inlay_snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits);
let (diff_snapshot, edits) = self.diff_map.update(cx, |diff_map, cx| {
diff_map.sync(buffer_snapshot.clone(), edits, cx)
});
let (inlay_snapshot, edits) = self.inlay_map.sync(diff_snapshot, edits);
let (fold_snapshot, edits) = self.fold_map.read(inlay_snapshot.clone(), edits);
let tab_size = Self::tab_size(&self.buffer, cx);
let (tab_snapshot, edits) = self.tab_map.sync(fold_snapshot.clone(), edits, tab_size);
@@ -216,7 +228,10 @@ impl DisplayMap {
let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.inlay_map.sync(buffer_snapshot.clone(), edits);
let (snapshot, edits) = self.diff_map.update(cx, |diff_map, cx| {
diff_map.sync(buffer_snapshot.clone(), edits, cx)
});
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
@@ -269,7 +284,7 @@ impl DisplayMap {
let start = buffer_snapshot.anchor_before(range.start);
let end = buffer_snapshot.anchor_after(range.end);
BlockProperties {
placement: BlockPlacement::Replace(start..end),
placement: BlockPlacement::Replace(start..=end),
render,
height,
style,
@@ -289,6 +304,9 @@ impl DisplayMap {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self
.diff_map
.update(cx, |diff_map, cx| diff_map.sync(snapshot, edits, cx));
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
@@ -318,6 +336,9 @@ impl DisplayMap {
.collect::<Vec<_>>();
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self
.diff_map
.update(cx, |diff_map, cx| diff_map.sync(snapshot, edits, cx));
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
@@ -336,6 +357,44 @@ impl DisplayMap {
block_map.remove_intersecting_replace_blocks(offset_ranges, inclusive);
}
pub fn fold_buffer(&mut self, buffer_id: language::BufferId, cx: &mut ModelContext<Self>) {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self
.diff_map
.update(cx, |diff_map, cx| diff_map.sync(snapshot, edits, cx));
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
let mut block_map = self.block_map.write(snapshot, edits);
block_map.fold_buffer(buffer_id, self.buffer.read(cx), cx)
}
pub fn unfold_buffer(&mut self, buffer_id: language::BufferId, cx: &mut ModelContext<Self>) {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self
.diff_map
.update(cx, |diff_map, cx| diff_map.sync(snapshot, edits, cx));
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
let mut block_map = self.block_map.write(snapshot, edits);
block_map.unfold_buffer(buffer_id, self.buffer.read(cx), cx)
}
pub(crate) fn buffer_folded(&self, buffer_id: language::BufferId) -> bool {
self.block_map.folded_buffers.contains(&buffer_id)
}
pub fn insert_creases(
&mut self,
creases: impl IntoIterator<Item = Crease<Anchor>>,
@@ -354,6 +413,71 @@ impl DisplayMap {
self.crease_map.remove(crease_ids, &snapshot)
}
pub fn add_change_set(
&mut self,
change_set: Model<BufferChangeSet>,
cx: &mut ModelContext<Self>,
) {
self.diff_map.update(cx, |diff_map, cx| {
diff_map.add_change_set(change_set, cx);
});
}
pub fn has_multiple_hunks(&self, cx: &AppContext) -> bool {
self.diff_map.read(cx).has_multiple_hunks()
}
pub fn has_expanded_diff_hunks_in_ranges(
&mut self,
ranges: &[Range<multi_buffer::Anchor>],
cx: &mut ModelContext<Self>,
) -> bool {
self.diff_map
.read(cx)
.has_expanded_diff_hunks_in_ranges(ranges)
}
pub fn set_all_hunks_expanded(&mut self, cx: &mut ModelContext<Self>) {
self.update_diff_map(cx, |diff_map, cx| diff_map.set_all_hunks_expanded(cx))
}
pub fn expand_diff_hunks(&mut self, ranges: Vec<Range<Anchor>>, cx: &mut ModelContext<Self>) {
self.update_diff_map(cx, |diff_map, cx| diff_map.expand_diff_hunks(ranges, cx))
}
pub fn collapse_diff_hunks(&mut self, ranges: Vec<Range<Anchor>>, cx: &mut ModelContext<Self>) {
self.update_diff_map(cx, |diff_map, cx| diff_map.collapse_diff_hunks(ranges, cx))
}
pub fn diff_base_for<'a>(
&'a self,
buffer_id: BufferId,
cx: &'a AppContext,
) -> Option<&'a Model<BufferChangeSet>> {
self.diff_map.read(cx).diff_base_for(buffer_id)
}
fn update_diff_map(
&mut self,
cx: &mut ModelContext<Self>,
f: impl FnOnce(&mut DiffMap, &mut ModelContext<DiffMap>),
) {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let (snapshot, edits) = self.diff_map.update(cx, |diff_map, cx| {
f(diff_map, cx);
diff_map.sync(snapshot, edits, cx)
});
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
self.block_map.write(snapshot, edits);
}
pub fn insert_blocks(
&mut self,
blocks: impl IntoIterator<Item = BlockProperties<Anchor>>,
@@ -362,6 +486,9 @@ impl DisplayMap {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self
.diff_map
.update(cx, |diff_map, cx| diff_map.sync(snapshot, edits, cx));
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
@@ -380,6 +507,9 @@ impl DisplayMap {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self
.diff_map
.update(cx, |diff_map, cx| diff_map.sync(snapshot, edits, cx));
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
@@ -398,6 +528,9 @@ impl DisplayMap {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self
.diff_map
.update(cx, |diff_map, cx| diff_map.sync(snapshot, edits, cx));
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
@@ -416,6 +549,9 @@ impl DisplayMap {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self
.diff_map
.update(cx, |diff_map, cx| diff_map.sync(snapshot, edits, cx));
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
@@ -491,7 +627,10 @@ impl DisplayMap {
}
let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let (snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits);
let (snapshot, edits) = self.diff_map.update(cx, |diff_map, cx| {
diff_map.sync(buffer_snapshot.clone(), edits, cx)
});
let (snapshot, edits) = self.inlay_map.sync(snapshot.clone(), edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
@@ -677,17 +816,35 @@ impl DisplaySnapshot {
self.fold_snapshot.fold_count()
}
pub fn diff_hunks<'a>(&'a self) -> impl Iterator<Item = MultiBufferDiffHunk> + 'a {
self.diff_snapshot()
.diff_hunks_in_range(0..self.buffer_snapshot.len())
}
pub fn diff_hunks_in_range<'a, T: ToOffset>(
&'a self,
range: Range<T>,
) -> impl Iterator<Item = MultiBufferDiffHunk> + 'a {
self.diff_snapshot().diff_hunks_in_range(range)
}
pub fn diff_hunks_in_range_rev<'a, T: ToOffset>(
&'a self,
range: Range<T>,
) -> impl Iterator<Item = MultiBufferDiffHunk> + 'a {
self.diff_snapshot().diff_hunks_in_range_rev(range)
}
pub fn has_diff_hunks(&self) -> bool {
self.diff_snapshot().has_diff_hunks()
}
pub fn is_empty(&self) -> bool {
self.buffer_snapshot.len() == 0
}
pub fn buffer_rows(
&self,
start_row: DisplayRow,
) -> impl Iterator<Item = Option<MultiBufferRow>> + '_ {
self.block_snapshot
.buffer_rows(BlockRow(start_row.0))
.map(|row| row.map(MultiBufferRow))
pub fn row_infos(&self, start_row: DisplayRow) -> impl Iterator<Item = RowInfo> + '_ {
self.block_snapshot.row_infos(BlockRow(start_row.0))
}
pub fn widest_line_number(&self) -> u32 {
@@ -696,7 +853,7 @@ impl DisplaySnapshot {
pub fn prev_line_boundary(&self, mut point: MultiBufferPoint) -> (Point, DisplayPoint) {
loop {
let mut inlay_point = self.inlay_snapshot.to_inlay_point(point);
let mut inlay_point = self.inlay_snapshot.make_inlay_point(point);
let mut fold_point = self.fold_snapshot.to_fold_point(inlay_point, Bias::Left);
fold_point.0.column = 0;
inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
@@ -712,9 +869,13 @@ impl DisplaySnapshot {
}
}
pub fn next_line_boundary(&self, mut point: MultiBufferPoint) -> (Point, DisplayPoint) {
pub fn next_line_boundary(
&self,
mut point: MultiBufferPoint,
) -> (MultiBufferPoint, DisplayPoint) {
let original_point = point;
loop {
let mut inlay_point = self.inlay_snapshot.to_inlay_point(point);
let mut inlay_point = self.inlay_snapshot.make_inlay_point(point);
let mut fold_point = self.fold_snapshot.to_fold_point(inlay_point, Bias::Right);
fold_point.0.column = self.fold_snapshot.line_len(fold_point.row());
inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
@@ -723,7 +884,7 @@ impl DisplaySnapshot {
let mut display_point = self.point_to_display_point(point, Bias::Right);
*display_point.column_mut() = self.line_len(display_point.row());
let next_point = self.display_point_to_point(display_point, Bias::Right);
if next_point == point {
if next_point == point || original_point == point || original_point == next_point {
return (point, display_point);
}
point = next_point;
@@ -758,7 +919,7 @@ impl DisplaySnapshot {
}
pub fn point_to_display_point(&self, point: MultiBufferPoint, bias: Bias) -> DisplayPoint {
let inlay_point = self.inlay_snapshot.to_inlay_point(point);
let inlay_point = self.inlay_snapshot.make_inlay_point(point);
let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
let tab_point = self.tab_snapshot.to_tab_point(fold_point);
let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point);
@@ -776,9 +937,15 @@ impl DisplaySnapshot {
.to_offset(self.display_point_to_inlay_point(point, bias))
}
pub fn display_point_to_diff_offset(&self, point: DisplayPoint, bias: Bias) -> DiffOffset {
self.diff_snapshot()
.point_to_offset(self.display_point_to_diff_point(point, bias))
}
pub fn anchor_to_inlay_offset(&self, anchor: Anchor) -> InlayOffset {
self.inlay_snapshot
.to_inlay_offset(anchor.to_offset(&self.buffer_snapshot))
let multibuffer_offset = anchor.to_offset(&self.buffer_snapshot);
let diff_offset = self.diff_snapshot().to_diff_offset(multibuffer_offset);
self.inlay_snapshot.to_inlay_offset(diff_offset)
}
pub fn display_point_to_anchor(&self, point: DisplayPoint, bias: Bias) -> Anchor {
@@ -794,6 +961,17 @@ impl DisplaySnapshot {
fold_point.to_inlay_point(&self.fold_snapshot)
}
fn diff_point_to_display_point(&self, diff_point: DiffPoint, bias: Bias) -> DisplayPoint {
let inlay_point = self.inlay_snapshot.to_inlay_point(diff_point);
let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
self.fold_point_to_display_point(fold_point)
}
fn display_point_to_diff_point(&self, point: DisplayPoint, bias: Bias) -> DiffPoint {
self.inlay_snapshot
.to_diff_point(self.display_point_to_inlay_point(point, bias))
}
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, bias);
@@ -1031,6 +1209,22 @@ impl DisplaySnapshot {
})
}
pub fn anchor_before(&self, point: DisplayPoint) -> DisplayAnchor {
let diff_point = self.display_point_to_diff_point(point, Bias::Left);
self.diff_snapshot().point_to_anchor(diff_point, Bias::Left)
}
pub fn anchor_after(&self, point: DisplayPoint) -> DisplayAnchor {
let diff_point = self.display_point_to_diff_point(point, Bias::Left);
self.diff_snapshot()
.point_to_anchor(diff_point, Bias::Right)
}
pub fn anchor_to_point(&self, anchor: DisplayAnchor) -> DisplayPoint {
let diff_point = self.diff_snapshot().anchor_to_point(anchor);
self.diff_point_to_display_point(diff_point, Bias::Left)
}
pub fn clip_point(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint {
let mut clipped = self.block_snapshot.clip_point(point.0, bias);
if self.clip_at_line_ends {
@@ -1081,10 +1275,6 @@ impl DisplaySnapshot {
|| self.fold_snapshot.is_line_folded(buffer_row)
}
pub fn is_line_replaced(&self, buffer_row: MultiBufferRow) -> bool {
self.block_snapshot.is_line_replaced(buffer_row)
}
pub fn is_block_line(&self, display_row: DisplayRow) -> bool {
self.block_snapshot.is_block_line(BlockRow(display_row.0))
}
@@ -1274,6 +1464,51 @@ impl DisplaySnapshot {
pub fn excerpt_header_height(&self) -> u32 {
self.block_snapshot.excerpt_header_height
}
pub(crate) fn diff_snapshot(&self) -> &DiffMapSnapshot {
&self.inlay_snapshot.diff_map_snapshot
}
}
#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash)]
pub struct DisplayAnchor {
pub anchor: multi_buffer::Anchor,
pub diff_base_anchor: Option<text::Anchor>,
}
impl DisplayAnchor {
pub fn min() -> Self {
Self {
anchor: multi_buffer::Anchor::min(),
diff_base_anchor: None,
}
}
pub fn max() -> Self {
Self {
anchor: multi_buffer::Anchor::max(),
diff_base_anchor: None,
}
}
pub fn cmp(&self, other: &DisplayAnchor, map: &DisplaySnapshot) -> std::cmp::Ordering {
map.diff_snapshot().compare_anchors(self, other)
}
}
pub trait DisplayCoordinate {
fn to_display_anchor(self, map: &DisplaySnapshot) -> DisplayAnchor;
}
impl DisplayCoordinate for DisplayAnchor {
fn to_display_anchor(self, _: &DisplaySnapshot) -> DisplayAnchor {
self
}
}
impl DisplayCoordinate for DisplayPoint {
fn to_display_anchor(self, map: &DisplaySnapshot) -> DisplayAnchor {
map.anchor_before(self)
}
}
#[derive(Copy, Clone, Default, Eq, Ord, PartialOrd, PartialEq)]
@@ -2231,7 +2466,7 @@ pub mod tests {
[BlockProperties {
placement: BlockPlacement::Replace(
buffer_snapshot.anchor_before(Point::new(1, 2))
..buffer_snapshot.anchor_after(Point::new(2, 3)),
..=buffer_snapshot.anchor_after(Point::new(2, 3)),
),
height: 4,
style: BlockStyle::Fixed,

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,174 @@
use collections::BTreeMap;
use gpui::HighlightStyle;
use language::Chunk;
use multi_buffer::{Anchor, MultiBufferChunks, MultiBufferSnapshot, ToOffset as _};
use std::{
any::TypeId,
cmp,
iter::{self, Peekable},
ops::Range,
sync::Arc,
vec,
};
use sum_tree::TreeMap;
pub struct CustomHighlightsChunks<'a> {
buffer_chunks: MultiBufferChunks<'a>,
buffer_chunk: Option<Chunk<'a>>,
offset: usize,
multibuffer_snapshot: &'a MultiBufferSnapshot,
highlight_endpoints: Peekable<vec::IntoIter<HighlightEndpoint>>,
active_highlights: BTreeMap<TypeId, HighlightStyle>,
text_highlights: Option<&'a TreeMap<TypeId, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>>,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
struct HighlightEndpoint {
offset: usize,
is_start: bool,
tag: TypeId,
style: HighlightStyle,
}
impl<'a> CustomHighlightsChunks<'a> {
pub fn new(
range: Range<usize>,
language_aware: bool,
text_highlights: Option<&'a TreeMap<TypeId, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>>,
multibuffer_snapshot: &'a MultiBufferSnapshot,
) -> Self {
Self {
buffer_chunks: multibuffer_snapshot.chunks(range.clone(), language_aware),
buffer_chunk: None,
offset: range.start,
text_highlights,
highlight_endpoints: create_highlight_endpoints(
&range,
text_highlights,
multibuffer_snapshot,
),
active_highlights: Default::default(),
multibuffer_snapshot,
}
}
pub fn seek(&mut self, new_range: Range<usize>) {
self.highlight_endpoints =
create_highlight_endpoints(&new_range, self.text_highlights, self.multibuffer_snapshot);
self.offset = new_range.start;
self.buffer_chunks.seek(new_range);
self.buffer_chunk.take();
self.active_highlights.clear()
}
}
fn create_highlight_endpoints(
range: &Range<usize>,
text_highlights: Option<&TreeMap<TypeId, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>>,
buffer: &MultiBufferSnapshot,
) -> iter::Peekable<vec::IntoIter<HighlightEndpoint>> {
let mut highlight_endpoints = Vec::new();
if let Some(text_highlights) = text_highlights {
let start = buffer.anchor_after(range.start);
let end = buffer.anchor_after(range.end);
for (&tag, text_highlights) in text_highlights.iter() {
let style = text_highlights.0;
let ranges = &text_highlights.1;
let start_ix = match ranges.binary_search_by(|probe| {
let cmp = probe.end.cmp(&start, &buffer);
if cmp.is_gt() {
cmp::Ordering::Greater
} else {
cmp::Ordering::Less
}
}) {
Ok(i) | Err(i) => i,
};
for range in &ranges[start_ix..] {
if range.start.cmp(&end, &buffer).is_ge() {
break;
}
highlight_endpoints.push(HighlightEndpoint {
offset: range.start.to_offset(&buffer),
is_start: true,
tag,
style,
});
highlight_endpoints.push(HighlightEndpoint {
offset: range.end.to_offset(&buffer),
is_start: false,
tag,
style,
});
}
}
highlight_endpoints.sort();
}
highlight_endpoints.into_iter().peekable()
}
impl<'a> Iterator for CustomHighlightsChunks<'a> {
type Item = Chunk<'a>;
fn next(&mut self) -> Option<Self::Item> {
let mut next_highlight_endpoint = usize::MAX;
while let Some(endpoint) = self.highlight_endpoints.peek().copied() {
if endpoint.offset <= self.offset {
if endpoint.is_start {
self.active_highlights.insert(endpoint.tag, endpoint.style);
} else {
self.active_highlights.remove(&endpoint.tag);
}
self.highlight_endpoints.next();
} else {
next_highlight_endpoint = endpoint.offset;
break;
}
}
let chunk = self
.buffer_chunk
.get_or_insert_with(|| self.buffer_chunks.next().unwrap());
if chunk.text.is_empty() {
*chunk = self.buffer_chunks.next().unwrap();
}
let (prefix, suffix) = chunk
.text
.split_at(chunk.text.len().min(next_highlight_endpoint - self.offset));
chunk.text = suffix;
self.offset += prefix.len();
let mut prefix = Chunk {
text: prefix,
..chunk.clone()
};
if !self.active_highlights.is_empty() {
let mut highlight_style = HighlightStyle::default();
for active_highlight in self.active_highlights.values() {
highlight_style.highlight(*active_highlight);
}
prefix.highlight_style = Some(highlight_style);
}
Some(prefix)
}
}
impl PartialOrd for HighlightEndpoint {
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for HighlightEndpoint {
fn cmp(&self, other: &Self) -> cmp::Ordering {
self.offset
.cmp(&other.offset)
.then_with(|| other.is_start.cmp(&self.is_start))
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,5 @@
use crate::RowInfo;
use super::{
inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot},
Highlights,
@@ -139,7 +141,7 @@ impl<'a> FoldMapWriter<'a> {
let mut folds = Vec::new();
let snapshot = self.0.snapshot.inlay_snapshot.clone();
for (range, fold_text) in ranges.into_iter() {
let buffer = &snapshot.buffer;
let buffer = snapshot.buffer();
let range = range.start.to_offset(buffer)..range.end.to_offset(buffer);
// Ignore any empty ranges.
@@ -161,14 +163,14 @@ impl<'a> FoldMapWriter<'a> {
});
let inlay_range =
snapshot.to_inlay_offset(range.start)..snapshot.to_inlay_offset(range.end);
snapshot.make_inlay_offset(range.start)..snapshot.make_inlay_offset(range.end);
edits.push(InlayEdit {
old: inlay_range.clone(),
new: inlay_range,
});
}
let buffer = &snapshot.buffer;
let buffer = snapshot.buffer();
folds.sort_unstable_by(|a, b| sum_tree::SeekTarget::cmp(&a.range, &b.range, buffer));
self.0.snapshot.folds = {
@@ -220,7 +222,7 @@ impl<'a> FoldMapWriter<'a> {
let mut edits = Vec::new();
let mut fold_ixs_to_delete = Vec::new();
let snapshot = self.0.snapshot.inlay_snapshot.clone();
let buffer = &snapshot.buffer;
let buffer = snapshot.buffer();
for range in ranges.into_iter() {
let range = range.start.to_offset(buffer)..range.end.to_offset(buffer);
let mut folds_cursor =
@@ -230,8 +232,8 @@ impl<'a> FoldMapWriter<'a> {
fold.range.start.to_offset(buffer)..fold.range.end.to_offset(buffer);
if should_unfold(fold) {
if offset_range.end > offset_range.start {
let inlay_range = snapshot.to_inlay_offset(offset_range.start)
..snapshot.to_inlay_offset(offset_range.end);
let inlay_range = snapshot.make_inlay_offset(offset_range.start)
..snapshot.make_inlay_offset(offset_range.end);
edits.push(InlayEdit {
old: inlay_range.clone(),
new: inlay_range,
@@ -275,7 +277,7 @@ impl FoldMap {
pub(crate) fn new(inlay_snapshot: InlaySnapshot) -> (Self, FoldSnapshot) {
let this = Self {
snapshot: FoldSnapshot {
folds: SumTree::new(&inlay_snapshot.buffer),
folds: SumTree::new(inlay_snapshot.buffer()),
transforms: SumTree::from_item(
Transform {
summary: TransformSummary {
@@ -336,9 +338,7 @@ impl FoldMap {
let mut folds = self.snapshot.folds.iter().peekable();
while let Some(fold) = folds.next() {
if let Some(next_fold) = folds.peek() {
let comparison = fold
.range
.cmp(&next_fold.range, &self.snapshot.inlay_snapshot.buffer);
let comparison = fold.range.cmp(&next_fold.range, self.snapshot.buffer());
assert!(comparison.is_le());
}
}
@@ -410,31 +410,31 @@ impl FoldMap {
InlayOffset(((edit.new.start + edit.old_len()).0 as isize + delta) as usize);
let anchor = inlay_snapshot
.buffer
.buffer()
.anchor_before(inlay_snapshot.to_buffer_offset(edit.new.start));
let mut folds_cursor = self
.snapshot
.folds
.cursor::<FoldRange>(&inlay_snapshot.buffer);
.cursor::<FoldRange>(&inlay_snapshot.buffer());
folds_cursor.seek(
&FoldRange(anchor..Anchor::max()),
Bias::Left,
&inlay_snapshot.buffer,
&inlay_snapshot.buffer(),
);
let mut folds = iter::from_fn({
let inlay_snapshot = &inlay_snapshot;
move || {
let item = folds_cursor.item().map(|fold| {
let buffer_start = fold.range.start.to_offset(&inlay_snapshot.buffer);
let buffer_end = fold.range.end.to_offset(&inlay_snapshot.buffer);
let buffer_start = fold.range.start.to_offset(&inlay_snapshot.buffer());
let buffer_end = fold.range.end.to_offset(&inlay_snapshot.buffer());
(
fold.clone(),
inlay_snapshot.to_inlay_offset(buffer_start)
..inlay_snapshot.to_inlay_offset(buffer_end),
inlay_snapshot.make_inlay_offset(buffer_start)
..inlay_snapshot.make_inlay_offset(buffer_end),
)
});
folds_cursor.next(&inlay_snapshot.buffer);
folds_cursor.next(&inlay_snapshot.buffer());
item
}
})
@@ -578,6 +578,10 @@ pub struct FoldSnapshot {
}
impl FoldSnapshot {
pub fn buffer(&self) -> &MultiBufferSnapshot {
self.inlay_snapshot.buffer()
}
#[cfg(test)]
pub fn text(&self) -> String {
self.chunks(FoldOffset(0)..self.len(), false, Highlights::default())
@@ -587,7 +591,7 @@ impl FoldSnapshot {
#[cfg(test)]
pub fn fold_count(&self) -> usize {
self.folds.items(&self.inlay_snapshot.buffer).len()
self.folds.items(self.inlay_snapshot.buffer()).len()
}
pub fn text_summary_for_range(&self, range: Range<FoldPoint>) -> TextSummary {
@@ -641,6 +645,32 @@ impl FoldSnapshot {
summary
}
pub fn make_fold_point(&self, point: Point, bias: Bias) -> FoldPoint {
self.to_fold_point(self.inlay_snapshot.make_inlay_point(point), bias)
}
pub fn make_fold_offset(&self, buffer_offset: usize, bias: Bias) -> FoldOffset {
self.to_fold_offset(self.inlay_snapshot.make_inlay_offset(buffer_offset), bias)
}
pub fn to_fold_offset(&self, inlay_offset: InlayOffset, bias: Bias) -> FoldOffset {
let mut cursor = self.transforms.cursor::<(InlayOffset, FoldOffset)>(&());
cursor.seek(&inlay_offset, Bias::Right, &());
if cursor.item().map_or(false, |t| t.is_fold()) {
if bias == Bias::Left || inlay_offset == cursor.start().0 {
cursor.start().1
} else {
cursor.end(&()).1
}
} else {
let overshoot = inlay_offset.0 - cursor.start().0 .0;
FoldOffset(cmp::min(
cursor.start().1 .0 + overshoot,
cursor.end(&()).1 .0,
))
}
}
pub fn to_fold_point(&self, point: InlayPoint, bias: Bias) -> FoldPoint {
let mut cursor = self.transforms.cursor::<(InlayPoint, FoldPoint)>(&());
cursor.seek(&point, Bias::Right, &());
@@ -673,7 +703,7 @@ impl FoldSnapshot {
(line_end - line_start) as u32
}
pub fn buffer_rows(&self, start_row: u32) -> FoldBufferRows {
pub fn row_infos(&self, start_row: u32) -> FoldRows {
if start_row > self.transforms.summary().output.lines.row {
panic!("invalid display row {}", start_row);
}
@@ -684,11 +714,11 @@ impl FoldSnapshot {
let overshoot = fold_point.0 - cursor.start().0 .0;
let inlay_point = InlayPoint(cursor.start().1 .0 + overshoot);
let input_buffer_rows = self.inlay_snapshot.buffer_rows(inlay_point.row());
let input_rows = self.inlay_snapshot.row_infos(inlay_point.row());
FoldBufferRows {
FoldRows {
fold_point,
input_buffer_rows,
input_rows,
cursor,
}
}
@@ -706,12 +736,12 @@ impl FoldSnapshot {
where
T: ToOffset,
{
let buffer = &self.inlay_snapshot.buffer;
let buffer = self.inlay_snapshot.buffer();
let range = range.start.to_offset(buffer)..range.end.to_offset(buffer);
let mut folds = intersecting_folds(&self.inlay_snapshot, &self.folds, range, false);
iter::from_fn(move || {
let item = folds.item();
folds.next(&self.inlay_snapshot.buffer);
folds.next(buffer);
item
})
}
@@ -720,8 +750,8 @@ impl FoldSnapshot {
where
T: ToOffset,
{
let buffer_offset = offset.to_offset(&self.inlay_snapshot.buffer);
let inlay_offset = self.inlay_snapshot.to_inlay_offset(buffer_offset);
let buffer_offset = offset.to_offset(self.inlay_snapshot.buffer());
let inlay_offset = self.inlay_snapshot.make_inlay_offset(buffer_offset);
let mut cursor = self.transforms.cursor::<InlayOffset>(&());
cursor.seek(&inlay_offset, Bias::Right, &());
cursor.item().map_or(false, |t| t.placeholder.is_some())
@@ -730,7 +760,7 @@ impl FoldSnapshot {
pub fn is_line_folded(&self, buffer_row: MultiBufferRow) -> bool {
let mut inlay_point = self
.inlay_snapshot
.to_inlay_point(Point::new(buffer_row.0, 0));
.make_inlay_point(Point::new(buffer_row.0, 0));
let mut cursor = self.transforms.cursor::<InlayPoint>(&());
cursor.seek(&inlay_point, Bias::Right, &());
loop {
@@ -870,7 +900,7 @@ fn intersecting_folds<'a>(
range: Range<usize>,
inclusive: bool,
) -> FilterCursor<'a, impl 'a + FnMut(&FoldSummary) -> bool, Fold, usize> {
let buffer = &inlay_snapshot.buffer;
let buffer = inlay_snapshot.buffer();
let start = buffer.anchor_before(range.start.to_offset(buffer));
let end = buffer.anchor_after(range.end.to_offset(buffer));
let mut cursor = folds.filter::<_, usize>(buffer, move |summary| {
@@ -1134,25 +1164,25 @@ impl<'a> sum_tree::Dimension<'a, FoldSummary> for usize {
}
#[derive(Clone)]
pub struct FoldBufferRows<'a> {
pub struct FoldRows<'a> {
cursor: Cursor<'a, Transform, (FoldPoint, InlayPoint)>,
input_buffer_rows: InlayBufferRows<'a>,
input_rows: InlayBufferRows<'a>,
fold_point: FoldPoint,
}
impl<'a> FoldBufferRows<'a> {
impl<'a> FoldRows<'a> {
pub(crate) fn seek(&mut self, row: u32) {
let fold_point = FoldPoint::new(row, 0);
self.cursor.seek(&fold_point, Bias::Left, &());
let overshoot = fold_point.0 - self.cursor.start().0 .0;
let inlay_point = InlayPoint(self.cursor.start().1 .0 + overshoot);
self.input_buffer_rows.seek(inlay_point.row());
self.input_rows.seek(inlay_point.row());
self.fold_point = fold_point;
}
}
impl<'a> Iterator for FoldBufferRows<'a> {
type Item = Option<u32>;
impl<'a> Iterator for FoldRows<'a> {
type Item = RowInfo;
fn next(&mut self) -> Option<Self::Item> {
let mut traversed_fold = false;
@@ -1166,11 +1196,11 @@ impl<'a> Iterator for FoldBufferRows<'a> {
if self.cursor.item().is_some() {
if traversed_fold {
self.input_buffer_rows.seek(self.cursor.start().1.row());
self.input_buffer_rows.next();
self.input_rows.seek(self.cursor.start().1 .0.row);
self.input_rows.next();
}
*self.fold_point.row_mut() += 1;
self.input_buffer_rows.next()
self.input_rows.next()
} else {
None
}
@@ -1380,7 +1410,10 @@ pub type FoldEdit = Edit<FoldOffset>;
#[cfg(test)]
mod tests {
use super::*;
use crate::{display_map::inlay_map::InlayMap, MultiBuffer, ToPoint};
use crate::{
display_map::{diff_map::DiffMap, inlay_map::InlayMap},
MultiBuffer, ToPoint,
};
use collections::HashSet;
use rand::prelude::*;
use settings::SettingsStore;
@@ -1395,8 +1428,8 @@ mod tests {
init_test(cx);
let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (diff_map, diff_snapshot) = DiffMap::new(buffer.clone(), cx);
let (mut inlay_map, inlay_snapshot) = InlayMap::new(diff_snapshot.clone());
let mut map = FoldMap::new(inlay_snapshot.clone()).0;
let (mut writer, _, _) = map.write(inlay_snapshot, vec![]);
@@ -1431,8 +1464,14 @@ mod tests {
buffer.snapshot(cx)
});
let (inlay_snapshot, inlay_edits) =
inlay_map.sync(buffer_snapshot, subscription.consume().into_inner());
let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| {
diff_map.sync(
buffer_snapshot.clone(),
subscription.consume().into_inner(),
cx,
)
});
let (inlay_snapshot, inlay_edits) = inlay_map.sync(diff_snapshot, diff_edits);
let (snapshot3, edits) = map.read(inlay_snapshot, inlay_edits);
assert_eq!(snapshot3.text(), "123a⋯c123c⋯eeeee");
assert_eq!(
@@ -1453,8 +1492,11 @@ mod tests {
buffer.edit([(Point::new(2, 6)..Point::new(4, 3), "456")], None, cx);
buffer.snapshot(cx)
});
let (inlay_snapshot, inlay_edits) =
inlay_map.sync(buffer_snapshot, subscription.consume().into_inner());
let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| {
diff_map.sync(buffer_snapshot, subscription.consume().into_inner(), cx)
});
let (inlay_snapshot, inlay_edits) = inlay_map.sync(diff_snapshot, diff_edits);
let (snapshot4, _) = map.read(inlay_snapshot.clone(), inlay_edits);
assert_eq!(snapshot4.text(), "123a⋯c123456eee");
@@ -1474,8 +1516,8 @@ mod tests {
init_test(cx);
let buffer = MultiBuffer::build_simple("abcdefghijkl", cx);
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (diff_map, diff_snapshot) = DiffMap::new(buffer.clone(), cx);
let (mut inlay_map, inlay_snapshot) = InlayMap::new(diff_snapshot.clone());
{
let mut map = FoldMap::new(inlay_snapshot.clone()).0;
@@ -1521,8 +1563,10 @@ mod tests {
buffer.edit([(0..1, "12345")], None, cx);
buffer.snapshot(cx)
});
let (inlay_snapshot, inlay_edits) =
inlay_map.sync(buffer_snapshot, subscription.consume().into_inner());
let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| {
diff_map.sync(buffer_snapshot, subscription.consume().into_inner(), cx)
});
let (inlay_snapshot, inlay_edits) = inlay_map.sync(diff_snapshot, diff_edits);
let (snapshot, _) = map.read(inlay_snapshot, inlay_edits);
assert_eq!(snapshot.text(), "12345⋯fghijkl");
}
@@ -1531,8 +1575,8 @@ mod tests {
#[gpui::test]
fn test_overlapping_folds(cx: &mut gpui::AppContext) {
let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
let (_, diff_snapshot) = DiffMap::new(buffer.clone(), cx);
let (_, inlay_snapshot) = InlayMap::new(diff_snapshot);
let mut map = FoldMap::new(inlay_snapshot.clone()).0;
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
writer.fold(vec![
@@ -1550,8 +1594,8 @@ mod tests {
init_test(cx);
let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (diff_map, diff_snapshot) = DiffMap::new(buffer.clone(), cx);
let (mut inlay_map, inlay_snapshot) = InlayMap::new(diff_snapshot.clone());
let mut map = FoldMap::new(inlay_snapshot.clone()).0;
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
@@ -1566,8 +1610,10 @@ mod tests {
buffer.edit([(Point::new(2, 2)..Point::new(3, 1), "")], None, cx);
buffer.snapshot(cx)
});
let (inlay_snapshot, inlay_edits) =
inlay_map.sync(buffer_snapshot, subscription.consume().into_inner());
let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| {
diff_map.sync(buffer_snapshot, subscription.consume().into_inner(), cx)
});
let (inlay_snapshot, inlay_edits) = inlay_map.sync(diff_snapshot, diff_edits);
let (snapshot, _) = map.read(inlay_snapshot, inlay_edits);
assert_eq!(snapshot.text(), "aa⋯eeeee");
}
@@ -1576,7 +1622,8 @@ mod tests {
fn test_folds_in_range(cx: &mut gpui::AppContext) {
let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_, diff_snapshot) = DiffMap::new(buffer.clone(), cx);
let (_, inlay_snapshot) = InlayMap::new(diff_snapshot.clone());
let mut map = FoldMap::new(inlay_snapshot.clone()).0;
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
@@ -1618,7 +1665,8 @@ mod tests {
MultiBuffer::build_random(&mut rng, cx)
};
let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (diff_map, diff_snapshot) = DiffMap::new(buffer.clone(), cx);
let (mut inlay_map, inlay_snapshot) = InlayMap::new(diff_snapshot.clone());
let mut map = FoldMap::new(inlay_snapshot.clone()).0;
let (mut initial_snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
@@ -1648,8 +1696,10 @@ mod tests {
}),
};
let (inlay_snapshot, new_inlay_edits) =
inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| {
diff_map.sync(buffer_snapshot.clone(), buffer_edits, cx)
});
let (inlay_snapshot, new_inlay_edits) = inlay_map.sync(diff_snapshot, diff_edits);
log::info!("inlay text {:?}", inlay_snapshot.text());
let inlay_edits = Patch::new(inlay_edits)
@@ -1660,8 +1710,8 @@ mod tests {
let mut expected_text: String = inlay_snapshot.text().to_string();
for fold_range in map.merged_folds().into_iter().rev() {
let fold_inlay_start = inlay_snapshot.to_inlay_offset(fold_range.start);
let fold_inlay_end = inlay_snapshot.to_inlay_offset(fold_range.end);
let fold_inlay_start = inlay_snapshot.make_inlay_offset(fold_range.start);
let fold_inlay_end = inlay_snapshot.make_inlay_offset(fold_range.end);
expected_text.replace_range(fold_inlay_start.0..fold_inlay_end.0, "");
}
@@ -1676,19 +1726,19 @@ mod tests {
let mut expected_buffer_rows = Vec::new();
for fold_range in map.merged_folds() {
let fold_start = inlay_snapshot
.to_point(inlay_snapshot.to_inlay_offset(fold_range.start))
.to_point(inlay_snapshot.make_inlay_offset(fold_range.start))
.row();
let fold_end = inlay_snapshot
.to_point(inlay_snapshot.to_inlay_offset(fold_range.end))
.to_point(inlay_snapshot.make_inlay_offset(fold_range.end))
.row();
expected_buffer_rows.extend(
inlay_snapshot
.buffer_rows(prev_row)
.row_infos(prev_row)
.take((1 + fold_start - prev_row) as usize),
);
prev_row = 1 + fold_end;
}
expected_buffer_rows.extend(inlay_snapshot.buffer_rows(prev_row));
expected_buffer_rows.extend(inlay_snapshot.row_infos(prev_row));
assert_eq!(
expected_buffer_rows.len(),
@@ -1777,7 +1827,7 @@ mod tests {
let mut fold_row = 0;
while fold_row < expected_buffer_rows.len() as u32 {
assert_eq!(
snapshot.buffer_rows(fold_row).collect::<Vec<_>>(),
snapshot.row_infos(fold_row).collect::<Vec<_>>(),
expected_buffer_rows[(fold_row as usize)..],
"wrong buffer rows starting at fold row {}",
fold_row,
@@ -1879,8 +1929,8 @@ mod tests {
let text = sample_text(6, 6, 'a') + "\n";
let buffer = MultiBuffer::build_simple(&text, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
let (_, diff_snapshot) = DiffMap::new(buffer.clone(), cx);
let (_, inlay_snapshot) = InlayMap::new(diff_snapshot);
let mut map = FoldMap::new(inlay_snapshot.clone()).0;
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
@@ -1892,10 +1942,19 @@ mod tests {
let (snapshot, _) = map.read(inlay_snapshot, vec![]);
assert_eq!(snapshot.text(), "aa⋯cccc\nd⋯eeeee\nffffff\n");
assert_eq!(
snapshot.buffer_rows(0).collect::<Vec<_>>(),
snapshot
.row_infos(0)
.map(|info| info.buffer_row)
.collect::<Vec<_>>(),
[Some(0), Some(3), Some(5), Some(6)]
);
assert_eq!(snapshot.buffer_rows(3).collect::<Vec<_>>(), [Some(6)]);
assert_eq!(
snapshot
.row_infos(3)
.map(|info| info.buffer_row)
.collect::<Vec<_>>(),
[Some(6)]
);
}
fn init_test(cx: &mut gpui::AppContext) {
@@ -1906,7 +1965,7 @@ mod tests {
impl FoldMap {
fn merged_folds(&self) -> Vec<Range<usize>> {
let inlay_snapshot = self.snapshot.inlay_snapshot.clone();
let buffer = &inlay_snapshot.buffer;
let buffer = inlay_snapshot.buffer();
let mut folds = self.snapshot.folds.items(buffer);
// Ensure sorting doesn't change how folds get merged and displayed.
folds.sort_by(|a, b| a.range.cmp(&b.range, buffer));
@@ -1942,7 +2001,7 @@ mod tests {
match rng.gen_range(0..=100) {
0..=39 if !self.snapshot.folds.is_empty() => {
let inlay_snapshot = self.snapshot.inlay_snapshot.clone();
let buffer = &inlay_snapshot.buffer;
let buffer = inlay_snapshot.buffer();
let mut to_unfold = Vec::new();
for _ in 0..rng.gen_range(1..=3) {
let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right);
@@ -1958,7 +2017,7 @@ mod tests {
}
_ => {
let inlay_snapshot = self.snapshot.inlay_snapshot.clone();
let buffer = &inlay_snapshot.buffer;
let buffer = inlay_snapshot.buffer();
let mut to_fold = Vec::new();
for _ in 0..rng.gen_range(1..=2) {
let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right);

File diff suppressed because it is too large Load Diff

View File

@@ -164,7 +164,7 @@ pub struct TabSnapshot {
impl TabSnapshot {
pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
&self.fold_snapshot.inlay_snapshot.buffer
self.fold_snapshot.inlay_snapshot.buffer()
}
pub fn line_len(&self, row: u32) -> u32 {
@@ -272,8 +272,8 @@ impl TabSnapshot {
}
}
pub fn buffer_rows(&self, row: u32) -> fold_map::FoldBufferRows<'_> {
self.fold_snapshot.buffer_rows(row)
pub fn rows(&self, row: u32) -> fold_map::FoldRows<'_> {
self.fold_snapshot.row_infos(row)
}
#[cfg(test)]
@@ -317,8 +317,7 @@ impl TabSnapshot {
}
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);
let fold_point = self.fold_snapshot.make_fold_point(point, bias);
self.to_tab_point(fold_point)
}
@@ -602,7 +601,7 @@ impl<'a> Iterator for TabChunks<'a> {
mod tests {
use super::*;
use crate::{
display_map::{fold_map::FoldMap, inlay_map::InlayMap},
display_map::{diff_map::DiffMap, fold_map::FoldMap, inlay_map::InlayMap},
MultiBuffer,
};
use rand::{prelude::StdRng, Rng};
@@ -610,8 +609,8 @@ mod tests {
#[gpui::test]
fn test_expand_tabs(cx: &mut gpui::AppContext) {
let buffer = MultiBuffer::build_simple("", cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_, diff_snapshot) = DiffMap::new(buffer, cx);
let (_, inlay_snapshot) = InlayMap::new(diff_snapshot.clone());
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
@@ -627,8 +626,8 @@ mod tests {
let output = "A BC DEF G HI J K L M";
let buffer = MultiBuffer::build_simple(input, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_, diff_snapshot) = DiffMap::new(buffer, cx);
let (_, inlay_snapshot) = InlayMap::new(diff_snapshot.clone());
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
@@ -674,8 +673,8 @@ mod tests {
let input = "abcdefg⋯hij";
let buffer = MultiBuffer::build_simple(input, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_, diff_snapshot) = DiffMap::new(buffer.clone(), cx);
let (_, inlay_snapshot) = InlayMap::new(diff_snapshot.clone());
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
@@ -688,8 +687,8 @@ mod tests {
let input = "\t \thello";
let buffer = MultiBuffer::build_simple(input, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_, diff_snapshot) = DiffMap::new(buffer.clone(), cx);
let (_, inlay_snapshot) = InlayMap::new(diff_snapshot);
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
@@ -749,8 +748,11 @@ mod tests {
let buffer_snapshot = buffer.read(cx).snapshot(cx);
log::info!("Buffer text: {:?}", buffer_snapshot.text());
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
log::info!("InlayMap text: {:?}", inlay_snapshot.text());
let (_, diff_snapshot) = DiffMap::new(buffer.clone(), cx);
let (mut inlay_map, inlay_snapshot) = InlayMap::new(diff_snapshot.clone());
log::info!("DiffMap text: {:?}", diff_snapshot.text());
let (mut fold_map, _) = FoldMap::new(inlay_snapshot.clone());
fold_map.randomly_mutate(&mut rng);
let (fold_snapshot, _) = fold_map.read(inlay_snapshot, vec![]);

View File

@@ -1,5 +1,7 @@
use crate::RowInfo;
use super::{
fold_map::FoldBufferRows,
fold_map::FoldRows,
tab_map::{self, TabEdit, TabPoint, TabSnapshot},
Highlights,
};
@@ -60,16 +62,16 @@ pub struct WrapChunks<'a> {
}
#[derive(Clone)]
pub struct WrapBufferRows<'a> {
input_buffer_rows: FoldBufferRows<'a>,
input_buffer_row: Option<u32>,
pub struct WrapRows<'a> {
input_buffer_rows: FoldRows<'a>,
input_buffer_row: RowInfo,
output_row: u32,
soft_wrapped: bool,
max_output_row: u32,
transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>,
}
impl<'a> WrapBufferRows<'a> {
impl<'a> WrapRows<'a> {
pub(crate) fn seek(&mut self, start_row: u32) {
self.transforms
.seek(&WrapPoint::new(start_row, 0), Bias::Left, &());
@@ -717,7 +719,7 @@ impl WrapSnapshot {
self.transforms.summary().output.longest_row
}
pub fn buffer_rows(&self, start_row: u32) -> WrapBufferRows {
pub fn row_infos(&self, start_row: u32) -> WrapRows {
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();
@@ -725,9 +727,9 @@ impl WrapSnapshot {
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.tab_snapshot.buffer_rows(input_row);
let mut input_buffer_rows = self.tab_snapshot.rows(input_row);
let input_buffer_row = input_buffer_rows.next().unwrap();
WrapBufferRows {
WrapRows {
transforms,
input_buffer_row,
input_buffer_rows,
@@ -847,7 +849,7 @@ impl WrapSnapshot {
}
let text = language::Rope::from(self.text().as_str());
let mut input_buffer_rows = self.tab_snapshot.buffer_rows(0);
let mut input_buffer_rows = self.tab_snapshot.rows(0);
let mut expected_buffer_rows = Vec::new();
let mut prev_tab_row = 0;
for display_row in 0..=self.max_point().row() {
@@ -855,7 +857,7 @@ impl WrapSnapshot {
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());
expected_buffer_rows.push(input_buffer_rows.next().unwrap().buffer_row);
}
prev_tab_row = tab_point.row();
@@ -864,7 +866,8 @@ impl WrapSnapshot {
for start_display_row in 0..expected_buffer_rows.len() {
assert_eq!(
self.buffer_rows(start_display_row as u32)
self.row_infos(start_display_row as u32)
.map(|row_info| row_info.buffer_row)
.collect::<Vec<_>>(),
&expected_buffer_rows[start_display_row..],
"invalid buffer_rows({}..)",
@@ -958,8 +961,8 @@ impl<'a> Iterator for WrapChunks<'a> {
}
}
impl<'a> Iterator for WrapBufferRows<'a> {
type Item = Option<u32>;
impl<'a> Iterator for WrapRows<'a> {
type Item = RowInfo;
fn next(&mut self) -> Option<Self::Item> {
if self.output_row > self.max_output_row {
@@ -979,7 +982,11 @@ impl<'a> Iterator for WrapBufferRows<'a> {
self.soft_wrapped = true;
}
Some(if soft_wrapped { None } else { buffer_row })
Some(if soft_wrapped {
RowInfo::default()
} else {
buffer_row
})
}
}
@@ -1161,7 +1168,7 @@ fn consolidate_wrap_edits(edits: Vec<WrapEdit>) -> Vec<WrapEdit> {
mod tests {
use super::*;
use crate::{
display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap},
display_map::{diff_map::DiffMap, fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap},
MultiBuffer,
};
use gpui::{font, px, test::observe};
@@ -1209,9 +1216,11 @@ mod tests {
});
let mut buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
log::info!("Buffer text: {:?}", buffer_snapshot.text());
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (diff_map, diff_snapshot) = cx.update(|cx| DiffMap::new(buffer.clone(), cx));
log::info!("DiffMap text: {:?}", diff_snapshot.text());
let (mut inlay_map, inlay_snapshot) = InlayMap::new(diff_snapshot);
log::info!("InlayMap text: {:?}", inlay_snapshot.text());
let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot.clone());
let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
log::info!("FoldMap text: {:?}", fold_snapshot.text());
let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
let tabs_snapshot = tab_map.set_max_expansion_column(32);
@@ -1293,8 +1302,10 @@ mod tests {
}
log::info!("Buffer text: {:?}", buffer_snapshot.text());
let (inlay_snapshot, inlay_edits) =
inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
let (diff_snapshot, diff_edits) = diff_map.update(cx, |diff_map, cx| {
diff_map.sync(buffer_snapshot.clone(), buffer_edits, cx)
});
let (inlay_snapshot, inlay_edits) = inlay_map.sync(diff_snapshot, diff_edits);
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());

File diff suppressed because it is too large Load Diff

View File

@@ -11,12 +11,14 @@ pub struct EditorSettings {
pub current_line_highlight: CurrentLineHighlight,
pub lsp_highlight_debounce: u64,
pub hover_popover_enabled: bool,
pub hover_popover_delay: u64,
pub toolbar: Toolbar,
pub scrollbar: Scrollbar,
pub gutter: Gutter,
pub scroll_beyond_last_line: ScrollBeyondLastLine,
pub vertical_scroll_margin: f32,
pub autoscroll_on_clicks: bool,
pub horizontal_scroll_margin: f32,
pub scroll_sensitivity: f32,
pub relative_line_numbers: bool,
pub seed_search_query_from_cursor: SeedQuerySetting,
@@ -104,6 +106,7 @@ pub struct Scrollbar {
pub search_results: bool,
pub diagnostics: bool,
pub cursors: bool,
pub axes: ScrollbarAxes,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@@ -131,6 +134,21 @@ pub enum ShowScrollbar {
Never,
}
/// Forcefully enable or disable the scrollbar for each axis
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub struct ScrollbarAxes {
/// When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings.
///
/// Default: true
pub horizontal: bool,
/// When false, forcefully disables the vertical scrollbar. Otherwise, obey other settings.
///
/// Default: true
pub vertical: bool,
}
/// The key to use for adding multiple cursors
///
/// Default: alt
@@ -196,7 +214,10 @@ pub struct EditorSettingsContent {
///
/// Default: true
pub hover_popover_enabled: Option<bool>,
/// Time to wait before showing the informational hover box
///
/// Default: 350
pub hover_popover_delay: Option<u64>,
/// Toolbar related settings
pub toolbar: Option<ToolbarContent>,
/// Scrollbar related settings
@@ -215,6 +236,10 @@ pub struct EditorSettingsContent {
///
/// Default: false
pub autoscroll_on_clicks: Option<bool>,
/// The number of characters to keep on either side when scrolling with the mouse.
///
/// Default: 5.
pub horizontal_scroll_margin: Option<f32>,
/// Scroll sensitivity multiplier. This multiplier is applied
/// to both the horizontal and vertical delta values while scrolling.
///
@@ -324,6 +349,22 @@ pub struct ScrollbarContent {
///
/// Default: true
pub cursors: Option<bool>,
/// Forcefully enable or disable the scrollbar for each axis
pub axes: Option<ScrollbarAxesContent>,
}
/// Forcefully enable or disable the scrollbar for each axis
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct ScrollbarAxesContent {
/// When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings.
///
/// Default: true
horizontal: Option<bool>,
/// When false, forcefully disables the vertical scrollbar. Otherwise, obey other settings.
///
/// Default: true
vertical: Option<bool>,
}
/// Gutter related settings

View File

@@ -5,7 +5,7 @@ use project::project_settings::{InlineBlameSettings, ProjectSettings};
use settings::{EditableSettingControl, Settings};
use theme::{FontFamilyCache, ThemeSettings};
use ui::{
prelude::*, ToggleWithLabel, ContextMenu, DropdownMenu, NumericStepper, SettingsContainer,
prelude::*, CheckboxWithLabel, ContextMenu, DropdownMenu, NumericStepper, SettingsContainer,
SettingsGroup,
};
@@ -258,7 +258,7 @@ impl RenderOnce for BufferFontLigaturesControl {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let value = Self::read(cx);
ToggleWithLabel::new(
CheckboxWithLabel::new(
"buffer-font-ligatures",
Label::new(self.name()),
value.into(),
@@ -311,7 +311,7 @@ impl RenderOnce for InlineGitBlameControl {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let value = Self::read(cx);
ToggleWithLabel::new(
CheckboxWithLabel::new(
"inline-git-blame",
Label::new(self.name()),
value.into(),
@@ -364,7 +364,7 @@ impl RenderOnce for LineNumbersControl {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let value = Self::read(cx);
ToggleWithLabel::new(
CheckboxWithLabel::new(
"line-numbers",
Label::new(self.name()),
value.into(),

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -9,12 +9,13 @@ use git::{
use gpui::{Model, ModelContext, Subscription, Task};
use http_client::HttpClient;
use language::{markdown, Bias, Buffer, BufferSnapshot, Edit, LanguageRegistry, ParsedMarkdown};
use multi_buffer::MultiBufferRow;
use project::{Project, ProjectItem};
use smallvec::SmallVec;
use sum_tree::SumTree;
use url::Url;
use crate::RowInfo;
#[derive(Clone, Debug, Default)]
pub struct GitBlameEntry {
pub rows: u32,
@@ -194,15 +195,15 @@ impl GitBlame {
pub fn blame_for_rows<'a>(
&'a mut self,
rows: impl 'a + IntoIterator<Item = Option<MultiBufferRow>>,
rows: &'a [RowInfo],
cx: &mut ModelContext<Self>,
) -> impl 'a + Iterator<Item = Option<BlameEntry>> {
self.sync(cx);
let mut cursor = self.entries.cursor::<u32>(&());
rows.into_iter().map(move |row| {
let row = row?;
cursor.seek_forward(&row.0, Bias::Right, &());
rows.into_iter().map(move |info| {
let row = info.buffer_row?;
cursor.seek_forward(&row, Bias::Right, &());
cursor.item()?.blame.clone()
})
}
@@ -563,15 +564,38 @@ mod tests {
use unindent::Unindent as _;
use util::RandomCharIter;
macro_rules! assert_blame_rows {
($blame:expr, $rows:expr, $expected:expr, $cx:expr) => {
assert_eq!(
$blame
.blame_for_rows($rows.map(MultiBufferRow).map(Some), $cx)
.collect::<Vec<_>>(),
$expected
);
};
// macro_rules! assert_blame_rows {
// ($blame:expr, $rows:expr, $expected:expr, $cx:expr) => {
// assert_eq!(
// $blame
// .blame_for_rows($rows.map(MultiBufferRow).map(Some), $cx)
// .collect::<Vec<_>>(),
// $expected
// );
// };
// }
#[track_caller]
fn assert_blame_rows(
blame: &mut GitBlame,
rows: Range<u32>,
expected: Vec<Option<BlameEntry>>,
cx: &mut ModelContext<GitBlame>,
) {
assert_eq!(
blame
.blame_for_rows(
&rows
.map(|row| RowInfo {
buffer_row: Some(row),
..Default::default()
})
.collect::<Vec<_>>(),
cx
)
.collect::<Vec<_>>(),
expected
);
}
fn init_test(cx: &mut gpui::TestAppContext) {
@@ -634,7 +658,15 @@ mod tests {
blame.update(cx, |blame, cx| {
assert_eq!(
blame
.blame_for_rows((0..1).map(MultiBufferRow).map(Some), cx)
.blame_for_rows(
&(0..1)
.map(|row| RowInfo {
buffer_row: Some(row),
..Default::default()
})
.collect::<Vec<_>>(),
cx
)
.collect::<Vec<_>>(),
vec![None]
);
@@ -698,7 +730,15 @@ mod tests {
// All lines
assert_eq!(
blame
.blame_for_rows((0..8).map(MultiBufferRow).map(Some), cx)
.blame_for_rows(
&(0..8)
.map(|buffer_row| RowInfo {
buffer_row: Some(buffer_row),
..Default::default()
})
.collect::<Vec<_>>(),
cx
)
.collect::<Vec<_>>(),
vec![
Some(blame_entry("1b1b1b", 0..1)),
@@ -714,7 +754,15 @@ mod tests {
// Subset of lines
assert_eq!(
blame
.blame_for_rows((1..4).map(MultiBufferRow).map(Some), cx)
.blame_for_rows(
&(1..4)
.map(|buffer_row| RowInfo {
buffer_row: Some(buffer_row),
..Default::default()
})
.collect::<Vec<_>>(),
cx
)
.collect::<Vec<_>>(),
vec![
Some(blame_entry("0d0d0d", 1..2)),
@@ -725,7 +773,17 @@ mod tests {
// Subset of lines, with some not displayed
assert_eq!(
blame
.blame_for_rows(vec![Some(MultiBufferRow(1)), None, None], cx)
.blame_for_rows(
&[
RowInfo {
buffer_row: Some(1),
..Default::default()
},
Default::default(),
Default::default(),
],
cx
)
.collect::<Vec<_>>(),
vec![Some(blame_entry("0d0d0d", 1..2)), None, None]
);
@@ -777,16 +835,16 @@ mod tests {
git_blame.update(cx, |blame, cx| {
// Sanity check before edits: make sure that we get the same blame entry for all
// lines.
assert_blame_rows!(
assert_blame_rows(
blame,
(0..4),
0..4,
vec![
Some(blame_entry("1b1b1b", 0..4)),
Some(blame_entry("1b1b1b", 0..4)),
Some(blame_entry("1b1b1b", 0..4)),
Some(blame_entry("1b1b1b", 0..4)),
],
cx
cx,
);
});
@@ -795,11 +853,11 @@ mod tests {
buffer.edit([(Point::new(0, 0)..Point::new(0, 0), "X")], None, cx);
});
git_blame.update(cx, |blame, cx| {
assert_blame_rows!(
assert_blame_rows(
blame,
(0..2),
0..2,
vec![None, Some(blame_entry("1b1b1b", 0..4))],
cx
cx,
);
});
// Modify a single line, in the middle of the line
@@ -807,21 +865,21 @@ mod tests {
buffer.edit([(Point::new(1, 2)..Point::new(1, 2), "X")], None, cx);
});
git_blame.update(cx, |blame, cx| {
assert_blame_rows!(
assert_blame_rows(
blame,
(1..4),
1..4,
vec![
None,
Some(blame_entry("1b1b1b", 0..4)),
Some(blame_entry("1b1b1b", 0..4))
Some(blame_entry("1b1b1b", 0..4)),
],
cx
cx,
);
});
// Before we insert a newline at the end, sanity check:
git_blame.update(cx, |blame, cx| {
assert_blame_rows!(blame, (3..4), vec![Some(blame_entry("1b1b1b", 0..4))], cx);
assert_blame_rows(blame, 3..4, vec![Some(blame_entry("1b1b1b", 0..4))], cx);
});
// Insert a newline at the end
buffer.update(cx, |buffer, cx| {
@@ -829,17 +887,17 @@ mod tests {
});
// Only the new line is marked as edited:
git_blame.update(cx, |blame, cx| {
assert_blame_rows!(
assert_blame_rows(
blame,
(3..5),
3..5,
vec![Some(blame_entry("1b1b1b", 0..4)), None],
cx
cx,
);
});
// Before we insert a newline at the start, sanity check:
git_blame.update(cx, |blame, cx| {
assert_blame_rows!(blame, (2..3), vec![Some(blame_entry("1b1b1b", 0..4)),], cx);
assert_blame_rows(blame, 2..3, vec![Some(blame_entry("1b1b1b", 0..4))], cx);
});
// Usage example
@@ -849,11 +907,11 @@ mod tests {
});
// Only the new line is marked as edited:
git_blame.update(cx, |blame, cx| {
assert_blame_rows!(
assert_blame_rows(
blame,
(2..4),
vec![None, Some(blame_entry("1b1b1b", 0..4)),],
cx
2..4,
vec![None, Some(blame_entry("1b1b1b", 0..4))],
cx,
);
});
}

View File

@@ -150,7 +150,7 @@ impl ProjectDiffEditor {
let editor = cx.new_view(|cx| {
let mut diff_display_editor =
Editor::for_multibuffer(excerpts.clone(), Some(project.clone()), true, cx);
diff_display_editor.set_expand_all_diff_hunks();
diff_display_editor.set_expand_all_diff_hunks(cx);
diff_display_editor
});
@@ -311,9 +311,11 @@ impl ProjectDiffEditor {
.update(&mut cx, |project_diff_editor, cx| {
project_diff_editor.update_excerpts(id, new_changes, new_entry_order, cx);
project_diff_editor.editor.update(cx, |editor, cx| {
for change_set in change_sets {
editor.diff_map.add_change_set(change_set, cx)
}
editor.display_map.update(cx, |display_map, cx| {
for change_set in change_sets {
display_map.add_change_set(change_set, cx)
}
});
});
})
.ok();
@@ -1193,9 +1195,9 @@ mod tests {
cx,
)
});
file_a_editor
.diff_map
.add_change_set(change_set.clone(), cx);
file_a_editor.display_map.update(cx, |display_map, cx| {
display_map.add_change_set(change_set.clone(), cx)
});
project.update(cx, |project, cx| {
project.buffer_store().update(cx, |buffer_store, cx| {
buffer_store.set_change_set(

View File

@@ -23,7 +23,6 @@ use std::{ops::Range, sync::Arc, time::Duration};
use theme::ThemeSettings;
use ui::{prelude::*, window_is_transparent, Scrollbar, ScrollbarState};
use util::TryFutureExt;
pub const HOVER_DELAY_MILLIS: u64 = 350;
pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
@@ -131,10 +130,12 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie
hide_hover(editor, cx);
}
let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay;
let task = cx.spawn(|this, mut cx| {
async move {
cx.background_executor()
.timer(Duration::from_millis(HOVER_DELAY_MILLIS))
.timer(Duration::from_millis(hover_popover_delay))
.await;
this.update(&mut cx, |this, _| {
this.hover_state.diagnostic_popover = None;
@@ -236,6 +237,8 @@ fn show_hover(
}
}
let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay;
let task = cx.spawn(|this, mut cx| {
async move {
// If we need to delay, delay a set amount initially before making the lsp request
@@ -245,7 +248,7 @@ fn show_hover(
// Construct delay task to wait for later
let total_delay = Some(
cx.background_executor()
.timer(Duration::from_millis(HOVER_DELAY_MILLIS)),
.timer(Duration::from_millis(hover_popover_delay)),
);
cx.background_executor()
@@ -856,6 +859,7 @@ mod tests {
InlayId, PointForPosition,
};
use collections::BTreeSet;
use gpui::AppContext;
use indoc::indoc;
use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
use lsp::LanguageServerId;
@@ -865,6 +869,10 @@ mod tests {
use std::sync::atomic::AtomicUsize;
use text::Bias;
fn get_hover_popover_delay(cx: &gpui::TestAppContext) -> u64 {
cx.read(|cx: &AppContext| -> u64 { EditorSettings::get_global(cx).hover_popover_delay })
}
impl InfoPopover {
fn get_rendered_text(&self, cx: &gpui::AppContext) -> String {
let mut rendered_text = String::new();
@@ -889,7 +897,6 @@ mod tests {
cx: &mut gpui::TestAppContext,
) {
init_test(cx, |_| {});
const HOVER_DELAY_MILLIS: u64 = 350;
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
@@ -963,7 +970,7 @@ mod tests {
}))
});
cx.background_executor
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
.advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
requests.next().await;
cx.editor(|editor, cx| {
@@ -1042,7 +1049,7 @@ mod tests {
hover_at(editor, Some(anchor), cx)
});
cx.background_executor
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
.advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
request.next().await;
// verify that the information popover is no longer visible
@@ -1096,7 +1103,7 @@ mod tests {
}))
});
cx.background_executor
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
.advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
requests.next().await;
cx.editor(|editor, cx| {
@@ -1132,7 +1139,7 @@ mod tests {
hover_at(editor, Some(anchor), cx)
});
cx.background_executor
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
.advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
request.next().await;
cx.editor(|editor, _| {
assert!(!editor.hover_state.visible());
@@ -1394,7 +1401,7 @@ mod tests {
}))
});
cx.background_executor
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
.advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
cx.background_executor.run_until_parked();
cx.editor(|Editor { hover_state, .. }, _| {
@@ -1682,7 +1689,7 @@ mod tests {
);
});
cx.background_executor
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
.advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
cx.background_executor.run_until_parked();
cx.update_editor(|editor, cx| {
let hover_state = &editor.hover_state;
@@ -1736,7 +1743,7 @@ mod tests {
);
});
cx.background_executor
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
.advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100));
cx.background_executor.run_until_parked();
cx.update_editor(|editor, cx| {
let hover_state = &editor.hover_state;

View File

@@ -1,17 +1,15 @@
use collections::{HashMap, HashSet};
use git::diff::DiffHunkStatus;
use gpui::{
Action, AnchorCorner, AppContext, CursorStyle, Hsla, Model, MouseButton, Subscription, Task,
View,
Action, AppContext, Corner, CursorStyle, Hsla, Model, MouseButton, Subscription, Task, View,
};
use language::{Buffer, BufferId, Point};
use multi_buffer::{
Anchor, AnchorRangeExt, ExcerptRange, MultiBuffer, MultiBufferDiffHunk, MultiBufferRow,
MultiBufferSnapshot, ToOffset, ToPoint,
MultiBufferSnapshot, ToPoint,
};
use project::buffer_store::BufferChangeSet;
use std::{ops::Range, sync::Arc};
use sum_tree::TreeMap;
use text::OffsetRangeExt;
use ui::{
prelude::*, ActiveTheme, ContextMenu, IconButtonShape, InteractiveElement, IntoElement,
@@ -21,10 +19,10 @@ use util::RangeExt;
use workspace::Item;
use crate::{
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,
editor_settings::CurrentLineHighlight, hunk_status, ApplyAllDiffHunks, ApplyDiffHunk,
BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight, DisplayRow,
DisplaySnapshot, Editor, EditorElement, GoToHunk, GoToPrevHunk, RevertFile,
RevertSelectedHunks, ToDisplayPoint, ToggleHunkDiff,
};
#[derive(Debug, Clone)]
@@ -38,7 +36,6 @@ pub(super) struct HoveredHunk {
pub(super) struct DiffMap {
pub(crate) hunks: Vec<ExpandedHunk>,
pub(crate) diff_bases: HashMap<BufferId, DiffBaseState>,
pub(crate) snapshot: DiffMapSnapshot,
hunk_update_tasks: HashMap<Option<BufferId>, Task<()>>,
expand_all: bool,
}
@@ -52,9 +49,6 @@ pub(super) struct ExpandedHunk {
pub folded: bool,
}
#[derive(Clone, Debug, Default)]
pub(crate) struct DiffMapSnapshot(TreeMap<BufferId, git::diff::BufferDiff>);
pub(crate) struct DiffBaseState {
pub(crate) change_set: Model<BufferChangeSet>,
pub(crate) last_version: Option<usize>,
@@ -75,133 +69,7 @@ pub enum DisplayDiffHunk {
},
}
impl DiffMap {
pub fn snapshot(&self) -> DiffMapSnapshot {
self.snapshot.clone()
}
pub fn add_change_set(
&mut self,
change_set: Model<BufferChangeSet>,
cx: &mut ViewContext<Editor>,
) {
let buffer_id = change_set.read(cx).buffer_id;
self.snapshot
.0
.insert(buffer_id, change_set.read(cx).diff_to_buffer.clone());
self.diff_bases.insert(
buffer_id,
DiffBaseState {
last_version: None,
_subscription: cx.observe(&change_set, move |editor, change_set, cx| {
editor
.diff_map
.snapshot
.0
.insert(buffer_id, change_set.read(cx).diff_to_buffer.clone());
Editor::sync_expanded_diff_hunks(&mut editor.diff_map, buffer_id, cx);
}),
change_set,
},
);
Editor::sync_expanded_diff_hunks(self, buffer_id, cx);
}
pub fn hunks(&self, include_folded: bool) -> impl Iterator<Item = &ExpandedHunk> {
self.hunks
.iter()
.filter(move |hunk| include_folded || !hunk.folded)
}
}
impl DiffMapSnapshot {
pub fn is_empty(&self) -> bool {
self.0.values().all(|diff| diff.is_empty())
}
pub fn diff_hunks<'a>(
&'a self,
buffer_snapshot: &'a MultiBufferSnapshot,
) -> impl Iterator<Item = MultiBufferDiffHunk> + 'a {
self.diff_hunks_in_range(0..buffer_snapshot.len(), buffer_snapshot)
}
pub fn diff_hunks_in_range<'a, T: ToOffset>(
&'a self,
range: Range<T>,
buffer_snapshot: &'a MultiBufferSnapshot,
) -> impl Iterator<Item = MultiBufferDiffHunk> + 'a {
let range = range.start.to_offset(buffer_snapshot)..range.end.to_offset(buffer_snapshot);
buffer_snapshot
.excerpts_for_range(range.clone())
.filter_map(move |excerpt| {
let buffer = excerpt.buffer();
let buffer_id = buffer.remote_id();
let diff = self.0.get(&buffer_id)?;
let buffer_range = excerpt.map_range_to_buffer(range.clone());
let buffer_range =
buffer.anchor_before(buffer_range.start)..buffer.anchor_after(buffer_range.end);
Some(
diff.hunks_intersecting_range(buffer_range, excerpt.buffer())
.map(move |hunk| {
let start =
excerpt.map_point_from_buffer(Point::new(hunk.row_range.start, 0));
let end =
excerpt.map_point_from_buffer(Point::new(hunk.row_range.end, 0));
MultiBufferDiffHunk {
row_range: MultiBufferRow(start.row)..MultiBufferRow(end.row),
buffer_id,
buffer_range: hunk.buffer_range.clone(),
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
}
}),
)
})
.flatten()
}
pub fn diff_hunks_in_range_rev<'a, T: ToOffset>(
&'a self,
range: Range<T>,
buffer_snapshot: &'a MultiBufferSnapshot,
) -> impl Iterator<Item = MultiBufferDiffHunk> + 'a {
let range = range.start.to_offset(buffer_snapshot)..range.end.to_offset(buffer_snapshot);
buffer_snapshot
.excerpts_for_range_rev(range.clone())
.filter_map(move |excerpt| {
let buffer = excerpt.buffer();
let buffer_id = buffer.remote_id();
let diff = self.0.get(&buffer_id)?;
let buffer_range = excerpt.map_range_to_buffer(range.clone());
let buffer_range =
buffer.anchor_before(buffer_range.start)..buffer.anchor_after(buffer_range.end);
Some(
diff.hunks_intersecting_range_rev(buffer_range, excerpt.buffer())
.map(move |hunk| {
let start_row = excerpt
.map_point_from_buffer(Point::new(hunk.row_range.start, 0))
.row;
let end_row = excerpt
.map_point_from_buffer(Point::new(hunk.row_range.end, 0))
.row;
MultiBufferDiffHunk {
row_range: MultiBufferRow(start_row)..MultiBufferRow(end_row),
buffer_id,
buffer_range: hunk.buffer_range.clone(),
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
}
}),
)
})
.flatten()
}
}
impl Editor {
pub fn set_expand_all_diff_hunks(&mut self) {
self.diff_map.expand_all = true;
}
pub(super) fn toggle_hovered_hunk(
&mut self,
hovered_hunk: &HoveredHunk,
@@ -214,47 +82,6 @@ impl Editor {
}
}
pub fn toggle_hunk_diff(&mut self, _: &ToggleHunkDiff, cx: &mut ViewContext<Self>) {
let snapshot = self.snapshot(cx);
let selections = self.selections.all(cx);
self.toggle_hunks_expanded(hunks_for_selections(&snapshot, &selections), cx);
}
pub fn expand_all_hunk_diffs(&mut self, _: &ExpandAllHunkDiffs, cx: &mut ViewContext<Self>) {
let snapshot = self.snapshot(cx);
let display_rows_with_expanded_hunks = self
.diff_map
.hunks(false)
.map(|hunk| &hunk.hunk_range)
.map(|anchor_range| {
(
anchor_range
.start
.to_display_point(&snapshot.display_snapshot)
.row(),
anchor_range
.end
.to_display_point(&snapshot.display_snapshot)
.row(),
)
})
.collect::<HashMap<_, _>>();
let hunks = self
.diff_map
.snapshot
.diff_hunks(&snapshot.display_snapshot.buffer_snapshot)
.filter(|hunk| {
let hunk_display_row_range = Point::new(hunk.row_range.start.0, 0)
.to_display_point(&snapshot.display_snapshot)
..Point::new(hunk.row_range.end.0, 0)
.to_display_point(&snapshot.display_snapshot);
let row_range_end =
display_rows_with_expanded_hunks.get(&hunk_display_row_range.start.row());
row_range_end.is_none() || row_range_end != Some(&hunk_display_row_range.end.row())
});
self.toggle_hunks_expanded(hunks.collect(), cx);
}
fn toggle_hunks_expanded(
&mut self,
hunks_to_toggle: Vec<MultiBufferDiffHunk>,
@@ -498,7 +325,8 @@ impl Editor {
cx: &mut ViewContext<Self>,
) {
let snapshot = self.snapshot(cx);
let hunks = hunks_for_selections(&snapshot, &self.selections.all(cx));
let ranges = self.selections.all(cx).into_iter().map(|s| s.range());
let hunks = snapshot.hunks_for_ranges(ranges);
let mut ranges_by_buffer = HashMap::default();
self.transact(cx, |editor, cx| {
for hunk in hunks {
@@ -522,10 +350,8 @@ impl Editor {
}
}
fn has_multiple_hunks(&self, cx: &AppContext) -> bool {
let snapshot = self.buffer.read(cx).snapshot(cx);
let mut hunks = self.diff_map.snapshot.diff_hunks(&snapshot);
hunks.nth(1).is_some()
fn has_multiple_hunks(&self, cx: &mut WindowContext) -> bool {
self.display_map.read(cx).has_multiple_hunks(cx)
}
fn hunk_header_block(
@@ -743,7 +569,7 @@ impl Editor {
},
),
)
.anchor(AnchorCorner::TopRight)
.anchor(Corner::TopRight)
.with_handle(hunk_controls_menu_handle)
.menu(move |cx| {
let focus = focus.clone();
@@ -866,23 +692,15 @@ impl Editor {
}
pub(super) fn clear_expanded_diff_hunks(&mut self, cx: &mut ViewContext<'_, Editor>) -> bool {
if self.diff_map.expand_all {
return false;
}
self.diff_map.hunk_update_tasks.clear();
self.clear_row_highlights::<DiffRowHighlight>();
let to_remove = self
.diff_map
.hunks
.drain(..)
.flat_map(|expanded_hunk| expanded_hunk.blocks.into_iter())
.collect::<HashSet<_>>();
if to_remove.is_empty() {
false
} else {
self.remove_blocks(to_remove, None, cx);
true
}
self.display_map.update(cx, |diff_map, cx| {
let ranges = vec![Anchor::min()..Anchor::max()];
if diff_map.has_expanded_diff_hunks_in_ranges(&ranges, cx) {
diff_map.collapse_diff_hunks(ranges, cx);
true
} else {
false
}
})
}
pub(super) fn sync_expanded_diff_hunks(
@@ -910,8 +728,7 @@ impl Editor {
.update(&mut cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
let mut recalculated_hunks = snapshot
.diff_map
.diff_hunks(&snapshot.buffer_snapshot)
.diff_hunks()
.filter(|hunk| hunk.buffer_id == buffer_id)
.fuse()
.peekable();
@@ -1419,7 +1236,9 @@ mod tests {
cx,
)
});
editor.diff_map.add_change_set(change_set, cx)
editor.display_map.update(cx, |display_map, cx| {
display_map.add_change_set(change_set, cx)
});
}
})
.unwrap();
@@ -1471,8 +1290,7 @@ mod tests {
assert_eq!(
snapshot
.diff_map
.diff_hunks_in_range(Point::zero()..Point::new(12, 0), &snapshot.buffer_snapshot)
.diff_hunks_in_range(Point::zero()..Point::new(12, 0))
.map(|hunk| (hunk_status(&hunk), hunk.row_range))
.collect::<Vec<_>>(),
&expected,
@@ -1480,11 +1298,7 @@ mod tests {
assert_eq!(
snapshot
.diff_map
.diff_hunks_in_range_rev(
Point::zero()..Point::new(12, 0),
&snapshot.buffer_snapshot
)
.diff_hunks_in_range_rev(Point::zero()..Point::new(12, 0))
.map(|hunk| (hunk_status(&hunk), hunk.row_range))
.collect::<Vec<_>>(),
expected

View File

@@ -56,6 +56,7 @@ impl Editor {
}
Some(indent_guides_in_range(
self,
visible_buffer_range,
self.should_show_indent_guides() == Some(true),
snapshot,
@@ -152,6 +153,7 @@ impl Editor {
}
pub fn indent_guides_in_range(
editor: &Editor,
visible_buffer_range: Range<MultiBufferRow>,
ignore_disabled_for_language: bool,
snapshot: &DisplaySnapshot,
@@ -169,15 +171,19 @@ pub fn indent_guides_in_range(
.indent_guides_in_range(start_anchor..end_anchor, ignore_disabled_for_language, cx)
.into_iter()
.filter(|indent_guide| {
if editor.buffer_folded(indent_guide.buffer_id, cx) {
return false;
}
let start =
MultiBufferRow(indent_guide.multibuffer_row_range.start.0.saturating_sub(1));
// Filter out indent guides that are inside a fold
// All indent guides that are starting "offscreen" have a start value of the first visible row minus one
// Therefore checking if a line is folded at first visible row minus one causes the other indent guides that are not related to the fold to disappear as well
let is_folded = snapshot.is_line_folded(start);
let line_indent = snapshot.line_indent_for_buffer_row(start);
let contained_in_fold =
line_indent.len(indent_guide.tab_size) <= indent_guide.indent_level();
!(is_folded && contained_in_fold)
})
.collect()

View File

@@ -317,6 +317,10 @@ impl InlineCompletionProvider for FakeInlineCompletionProvider {
"fake-completion-provider"
}
fn display_name() -> &'static str {
"Fake Completion Provider"
}
fn is_enabled(
&self,
_buffer: &gpui::Model<language::Buffer>,

View File

@@ -733,7 +733,7 @@ impl Item for Editor {
project: Model<Project>,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
self.report_editor_event("save", None, cx);
self.report_editor_event("Editor Saved", None, cx);
let buffers = self.buffer().clone().read(cx).all_buffers();
let buffers = buffers
.into_iter()
@@ -805,7 +805,7 @@ impl Item for Editor {
.path
.extension()
.map(|a| a.to_string_lossy().to_string());
self.report_editor_event("save", file_extension, cx);
self.report_editor_event("Editor Saved", file_extension, cx);
project.update(cx, |project, cx| project.save_buffer_as(buffer, path, cx))
}

View File

@@ -61,7 +61,7 @@ impl ProposedChangesEditor {
let mut this = Self {
editor: cx.new_view(|cx| {
let mut editor = Editor::for_multibuffer(multibuffer.clone(), project, true, cx);
editor.set_expand_all_diff_hunks();
editor.set_expand_all_diff_hunks(cx);
editor.set_completion_provider(None);
editor.clear_code_action_providers();
editor.set_semantics_provider(
@@ -223,9 +223,11 @@ impl ProposedChangesEditor {
self.buffer_entries = buffer_entries;
self.editor.update(cx, |editor, cx| {
editor.change_selections(None, cx, |selections| selections.refresh());
for change_set in new_change_sets {
editor.diff_map.add_change_set(change_set, cx)
}
editor.display_map.update(cx, |display_map, cx| {
for change_set in new_change_sets {
display_map.add_change_set(change_set, cx)
}
})
});
}

View File

@@ -2,7 +2,7 @@ mod actions;
pub(crate) mod autoscroll;
pub(crate) mod scroll_amount;
use crate::editor_settings::ScrollBeyondLastLine;
use crate::editor_settings::{ScrollBeyondLastLine, ScrollbarAxes};
use crate::{
display_map::{DisplaySnapshot, ToDisplayPoint},
hover_popover::hide_hover,
@@ -11,7 +11,10 @@ use crate::{
InlayHintRefreshReason, MultiBufferSnapshot, RowExt, ToPoint,
};
pub use autoscroll::{Autoscroll, AutoscrollStrategy};
use gpui::{point, px, AppContext, Entity, Global, Pixels, Task, ViewContext, WindowContext};
use core::fmt::Debug;
use gpui::{
point, px, Along, AppContext, Axis, Entity, Global, Pixels, Task, ViewContext, WindowContext,
};
use language::{Bias, Point};
pub use scroll_amount::ScrollAmount;
use settings::Settings;
@@ -60,10 +63,53 @@ impl ScrollAnchor {
}
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum Axis {
Vertical,
Horizontal,
#[derive(Debug, Clone)]
pub struct AxisPair<T: Clone> {
pub vertical: T,
pub horizontal: T,
}
pub fn axis_pair<T: Clone>(horizontal: T, vertical: T) -> AxisPair<T> {
AxisPair {
vertical,
horizontal,
}
}
impl<T: Clone> AxisPair<T> {
pub fn as_xy(&self) -> (&T, &T) {
(&self.horizontal, &self.vertical)
}
}
impl<T: Clone> Along for AxisPair<T> {
type Unit = T;
fn along(&self, axis: gpui::Axis) -> Self::Unit {
match axis {
gpui::Axis::Horizontal => self.horizontal.clone(),
gpui::Axis::Vertical => self.vertical.clone(),
}
}
fn apply_along(&self, axis: gpui::Axis, f: impl FnOnce(Self::Unit) -> Self::Unit) -> Self {
match axis {
gpui::Axis::Horizontal => Self {
horizontal: f(self.horizontal.clone()),
vertical: self.vertical.clone(),
},
gpui::Axis::Vertical => Self {
horizontal: self.horizontal.clone(),
vertical: f(self.vertical.clone()),
},
}
}
}
impl From<ScrollbarAxes> for AxisPair<bool> {
fn from(value: ScrollbarAxes) -> Self {
axis_pair(value.horizontal, value.vertical)
}
}
#[derive(Clone, Copy, Debug)]
@@ -136,7 +182,7 @@ pub struct ScrollManager {
last_autoscroll: Option<(gpui::Point<f32>, f32, f32, AutoscrollStrategy)>,
show_scrollbars: bool,
hide_scrollbar_task: Option<Task<()>>,
dragging_scrollbar: bool,
dragging_scrollbar: AxisPair<bool>,
visible_line_count: Option<f32>,
forbid_vertical_scroll: bool,
}
@@ -150,7 +196,7 @@ impl ScrollManager {
autoscroll_request: None,
show_scrollbars: true,
hide_scrollbar_task: None,
dragging_scrollbar: false,
dragging_scrollbar: axis_pair(false, false),
last_autoscroll: None,
visible_line_count: None,
forbid_vertical_scroll: false,
@@ -311,15 +357,18 @@ impl ScrollManager {
self.autoscroll_request.map(|(autoscroll, _)| autoscroll)
}
pub fn is_dragging_scrollbar(&self) -> bool {
self.dragging_scrollbar
pub fn is_dragging_scrollbar(&self, axis: Axis) -> bool {
self.dragging_scrollbar.along(axis)
}
pub fn set_is_dragging_scrollbar(&mut self, dragging: bool, cx: &mut ViewContext<Editor>) {
if dragging != self.dragging_scrollbar {
self.dragging_scrollbar = dragging;
cx.notify();
}
pub fn set_is_dragging_scrollbar(
&mut self,
axis: Axis,
dragging: bool,
cx: &mut ViewContext<Editor>,
) {
self.dragging_scrollbar = self.dragging_scrollbar.apply_along(axis, |_| dragging);
cx.notify();
}
pub fn clamp_scroll_left(&mut self, max: f32) -> bool {

View File

@@ -1,6 +1,6 @@
use crate::{
display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DiffRowHighlight, DisplayPoint,
Editor, MultiBuffer, RowExt,
display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer,
RowExt,
};
use collections::BTreeMap;
use futures::Future;
@@ -11,11 +11,12 @@ use gpui::{
};
use itertools::Itertools;
use language::{Buffer, BufferSnapshot, LanguageRegistry};
use multi_buffer::{ExcerptRange, ToPoint};
use multi_buffer::ExcerptRange;
use parking_lot::RwLock;
use project::{FakeFs, Project};
use std::{
any::TypeId,
cmp,
ops::{Deref, DerefMut, Range},
path::Path,
sync::{
@@ -23,6 +24,7 @@ use std::{
Arc,
},
};
use text::Bias;
use util::{
assert_set_eq,
test::{generate_marked_text, marked_text_ranges},
@@ -332,83 +334,51 @@ impl EditorTestContext {
///
/// Diff hunks are indicated by lines starting with `+` and `-`.
#[track_caller]
pub fn assert_state_with_diff(&mut self, expected_diff: String) {
let has_diff_markers = expected_diff
.lines()
.any(|line| line.starts_with("+") || line.starts_with("-"));
let expected_diff_text = expected_diff
.split('\n')
.map(|line| {
let trimmed = line.trim();
if trimmed.is_empty() {
String::new()
} else if has_diff_markers {
line.to_string()
} else {
format!(" {line}")
}
})
.join("\n");
pub fn assert_state_with_diff(&mut self, expected_diff_text: String) {
let (snapshot, selections) = self
.editor
.update(&mut self.cx, |editor, cx| editor.selections.all_display(cx));
let actual_selections = self.editor_selections();
let actual_marked_text =
generate_marked_text(&self.buffer_text(), &actual_selections, true);
let diff_snapshot = snapshot.diff_snapshot();
let diff_offsets = selections
.into_iter()
.map(|s| {
let start = snapshot.display_point_to_diff_offset(s.start, Bias::Left).0;
let end = snapshot.display_point_to_diff_offset(s.end, Bias::Left).0;
cmp::min(start, end)..cmp::max(start, end)
})
.collect::<Vec<_>>();
let actual_marked_text = generate_marked_text(&diff_snapshot.text(), &diff_offsets, true);
// Read the actual diff from the editor's row highlights and block
// decorations.
let actual_diff = self.editor.update(&mut self.cx, |editor, cx| {
let snapshot = editor.snapshot(cx);
let insertions = editor
.highlighted_rows::<DiffRowHighlight>()
.map(|(range, _)| {
let start = range.start.to_point(&snapshot.buffer_snapshot);
let end = range.end.to_point(&snapshot.buffer_snapshot);
start.row..end.row
})
.collect::<Vec<_>>();
let deletions = editor
.diff_map
.hunks
.iter()
.filter_map(|hunk| {
if hunk.blocks.is_empty() {
return None;
let line_infos = diff_snapshot.row_infos(0).collect::<Vec<_>>();
let has_diff = line_infos.iter().any(|info| info.diff_status.is_some());
let actual_diff = actual_marked_text
.split('\n')
.zip(line_infos)
.map(|(line, info)| {
let mut marker = match info.diff_status {
Some(DiffHunkStatus::Added) => "+ ",
Some(DiffHunkStatus::Removed) => "- ",
Some(DiffHunkStatus::Modified) => unreachable!(),
None => {
if has_diff {
" "
} else {
""
}
}
let row = hunk
.hunk_range
.start
.to_point(&snapshot.buffer_snapshot)
.row;
let (_, buffer, _) = editor
.buffer()
.read(cx)
.excerpt_containing(hunk.hunk_range.start, cx)
.expect("no excerpt for expanded buffer's hunk start");
let buffer_id = buffer.read(cx).remote_id();
let change_set = &editor
.diff_map
.diff_bases
.get(&buffer_id)
.expect("should have a diff base for expanded hunk")
.change_set;
let deleted_text = change_set
.read(cx)
.base_text
.as_ref()
.expect("no base text for expanded hunk")
.read(cx)
.as_rope()
.slice(hunk.diff_base_byte_range.clone())
.to_string();
if let DiffHunkStatus::Modified | DiffHunkStatus::Removed = hunk.status {
Some((row, deleted_text))
} else {
None
}
})
.collect::<Vec<_>>();
format_diff(actual_marked_text, deletions, insertions)
});
};
if line.is_empty() {
marker = marker.trim();
}
format!("{marker}{line}")
})
.collect::<Vec<_>>()
.join("\n");
pretty_assertions::assert_eq!(actual_diff, expected_diff_text, "unexpected diff state");
}
@@ -503,46 +473,6 @@ impl EditorTestContext {
}
}
fn format_diff(
text: String,
actual_deletions: Vec<(u32, String)>,
actual_insertions: Vec<Range<u32>>,
) -> String {
let mut diff = String::new();
for (row, line) in text.split('\n').enumerate() {
let row = row as u32;
if row > 0 {
diff.push('\n');
}
if let Some(text) = actual_deletions
.iter()
.find_map(|(deletion_row, deleted_text)| {
if *deletion_row == row {
Some(deleted_text)
} else {
None
}
})
{
for line in text.lines() {
diff.push('-');
if !line.is_empty() {
diff.push(' ');
diff.push_str(line);
}
diff.push('\n');
}
}
let marker = if actual_insertions.iter().any(|range| range.contains(&row)) {
"+ "
} else {
" "
};
diff.push_str(format!("{marker}{line}").trim_end());
}
diff
}
impl Deref for EditorTestContext {
type Target = gpui::VisualTestContext;

View File

@@ -43,6 +43,7 @@ serde_json.workspace = true
serde_json_lenient.workspace = true
settings.workspace = true
task.workspace = true
telemetry.workspace = true
tempfile.workspace = true
toml.workspace = true
url.workspace = true

View File

@@ -1001,14 +1001,13 @@ impl ExtensionStore {
extensions_to_unload.len() - reload_count
);
if let Some(telemetry) = &self.telemetry {
for extension_id in &extensions_to_load {
if let Some(extension) = new_index.extensions.get(extension_id) {
telemetry.report_extension_event(
extension_id.clone(),
extension.manifest.version.clone(),
);
}
for extension_id in &extensions_to_load {
if let Some(extension) = new_index.extensions.get(extension_id) {
telemetry::event!(
"Extension Loaded",
extension_id,
version = extension.manifest.version
);
}
}

View File

@@ -19,6 +19,7 @@ const SUGGESTIONS_BY_EXTENSION_ID: &[(&str, &[&str])] = &[
("clojure", &["bb", "clj", "cljc", "cljs", "edn"]),
("neocmake", &["CMakeLists.txt", "cmake"]),
("csharp", &["cs"]),
("cython", &["pyx", "pxd", "pxi"]),
("dart", &["dart"]),
("dockerfile", &["Dockerfile"]),
("elisp", &["el"]),

View File

@@ -113,13 +113,7 @@ impl PickerDelegate for ExtensionVersionSelectorDelegate {
.iter()
.enumerate()
.map(|(id, extension)| {
let text = format!("v{}", extension.manifest.version);
StringMatchCandidate {
id,
char_bag: text.as_str().into(),
string: text,
}
StringMatchCandidate::new(id, &format!("v{}", extension.manifest.version))
})
.collect::<Vec<_>>();

View File

@@ -23,7 +23,7 @@ use project::DirectoryLister;
use release_channel::ReleaseChannel;
use settings::Settings;
use theme::ThemeSettings;
use ui::{prelude::*, ToggleWithLabel, ContextMenu, PopoverMenu, ToggleButton, Tooltip};
use ui::{prelude::*, CheckboxWithLabel, ContextMenu, PopoverMenu, ToggleButton, Tooltip};
use vim_mode_setting::VimModeSetting;
use workspace::{
item::{Item, ItemEvent},
@@ -328,11 +328,7 @@ impl ExtensionsPage {
let match_candidates = dev_extensions
.iter()
.enumerate()
.map(|(ix, manifest)| StringMatchCandidate {
id: ix,
string: manifest.name.clone(),
char_bag: manifest.name.as_str().into(),
})
.map(|(ix, manifest)| StringMatchCandidate::new(ix, &manifest.name))
.collect::<Vec<_>>();
let matches = match_strings(
@@ -453,18 +449,17 @@ impl ExtensionsPage {
.gap_2()
.justify_between()
.child(
div().overflow_x_hidden().text_ellipsis().child(
Label::new(format!(
"{}: {}",
if extension.authors.len() > 1 {
"Authors"
} else {
"Author"
},
extension.authors.join(", ")
))
.size(LabelSize::Small),
),
Label::new(format!(
"{}: {}",
if extension.authors.len() > 1 {
"Authors"
} else {
"Author"
},
extension.authors.join(", ")
))
.size(LabelSize::Small)
.text_ellipsis(),
)
.child(Label::new("<>").size(LabelSize::Small)),
)
@@ -473,11 +468,10 @@ impl ExtensionsPage {
.gap_2()
.justify_between()
.children(extension.description.as_ref().map(|description| {
div().overflow_x_hidden().text_ellipsis().child(
Label::new(description.clone())
.size(LabelSize::Small)
.color(Color::Default),
)
Label::new(description.clone())
.size(LabelSize::Small)
.color(Color::Default)
.text_ellipsis()
}))
.children(repository_url.map(|repository_url| {
IconButton::new(
@@ -554,18 +548,17 @@ impl ExtensionsPage {
.gap_2()
.justify_between()
.child(
div().overflow_x_hidden().text_ellipsis().child(
Label::new(format!(
"{}: {}",
if extension.manifest.authors.len() > 1 {
"Authors"
} else {
"Author"
},
extension.manifest.authors.join(", ")
))
.size(LabelSize::Small),
),
Label::new(format!(
"{}: {}",
if extension.manifest.authors.len() > 1 {
"Authors"
} else {
"Author"
},
extension.manifest.authors.join(", ")
))
.size(LabelSize::Small)
.text_ellipsis(),
)
.child(
Label::new(format!(
@@ -580,11 +573,10 @@ impl ExtensionsPage {
.gap_2()
.justify_between()
.children(extension.manifest.description.as_ref().map(|description| {
div().overflow_x_hidden().text_ellipsis().child(
Label::new(description.clone())
.size(LabelSize::Small)
.color(Color::Default),
)
Label::new(description.clone())
.size(LabelSize::Small)
.color(Color::Default)
.text_ellipsis()
}))
.child(
h_flex()
@@ -994,7 +986,7 @@ impl ExtensionsPage {
.docs_url("https://zed.dev/docs/git#git-integrations"),
Feature::Vim => FeatureUpsell::new(telemetry, "Vim support is built-in to Zed!")
.docs_url("https://zed.dev/docs/vim")
.child(ToggleWithLabel::new(
.child(CheckboxWithLabel::new(
"enable-vim",
Label::new("Enable vim mode"),
if VimModeSetting::get_global(cx).0 {

View File

@@ -1261,8 +1261,8 @@ impl PickerDelegate for FileFinderDelegate {
.child(
PopoverMenu::new("menu-popover")
.with_handle(self.popover_menu_handle.clone())
.attach(gpui::AnchorCorner::TopRight)
.anchor(gpui::AnchorCorner::BottomRight)
.attach(gpui::Corner::TopRight)
.anchor(gpui::Corner::BottomRight)
.trigger(
Button::new("actions-trigger", "Split Options")
.selected_label_color(Color::Accent)

View File

@@ -131,7 +131,7 @@ impl PickerDelegate for OpenPathDelegate {
.iter()
.enumerate()
.map(|(ix, path)| {
StringMatchCandidate::new(ix, path.to_string_lossy().into())
StringMatchCandidate::new(ix, &path.to_string_lossy())
})
.collect::<Vec<_>>();

View File

@@ -18,22 +18,12 @@ pub struct StringMatchCandidate {
pub char_bag: CharBag,
}
impl Match for StringMatch {
fn score(&self) -> f64 {
self.score
}
fn set_positions(&mut self, positions: Vec<usize>) {
self.positions = positions;
}
}
impl StringMatchCandidate {
pub fn new(id: usize, string: String) -> Self {
pub fn new(id: usize, string: &str) -> Self {
Self {
id,
char_bag: CharBag::from(string.as_str()),
string,
string: string.into(),
char_bag: string.into(),
}
}
}
@@ -56,29 +46,39 @@ pub struct StringMatch {
pub string: String,
}
impl Match for StringMatch {
fn score(&self) -> f64 {
self.score
}
fn set_positions(&mut self, positions: Vec<usize>) {
self.positions = positions;
}
}
impl StringMatch {
pub fn ranges(&self) -> impl '_ + Iterator<Item = Range<usize>> {
let mut positions = self.positions.iter().peekable();
iter::from_fn(move || {
if let Some(start) = positions.next().copied() {
if start >= self.string.len() {
let Some(char_len) = self.char_len_at_index(start) else {
log::error!(
"Invariant violation: Index {start} out of range in string {:?}",
"Invariant violation: Index {start} out of range or not on a utf-8 boundary in string {:?}",
self.string
);
return None;
}
let mut end = start + self.char_len_at_index(start);
};
let mut end = start + char_len;
while let Some(next_start) = positions.peek() {
if end == **next_start {
if end >= self.string.len() {
let Some(char_len) = self.char_len_at_index(end) else {
log::error!(
"Invariant violation: Index {end} out of range in string {:?}",
"Invariant violation: Index {end} out of range or not on a utf-8 boundary in string {:?}",
self.string
);
return None;
}
end += self.char_len_at_index(end);
};
end += char_len;
positions.next();
} else {
break;
@@ -91,8 +91,12 @@ impl StringMatch {
})
}
fn char_len_at_index(&self, ix: usize) -> usize {
self.string[ix..].chars().next().unwrap().len_utf8()
/// Gets the byte length of the utf-8 character at a byte offset. If the index is out of range
/// or not on a utf-8 boundary then None is returned.
fn char_len_at_index(&self, ix: usize) -> Option<usize> {
self.string
.get(ix..)
.and_then(|slice| slice.chars().next().map(|char| char.len_utf8()))
}
}

View File

@@ -46,7 +46,8 @@ pub trait GitRepository: Send + Sync {
fn blame(&self, path: &Path, content: Rope) -> Result<crate::blame::Blame>;
fn path(&self) -> PathBuf;
/// Returns the path to the repository, typically the `.git` folder.
fn dot_git_dir(&self) -> PathBuf;
}
impl std::fmt::Debug for dyn GitRepository {
@@ -85,7 +86,7 @@ impl GitRepository for RealGitRepository {
}
}
fn path(&self) -> PathBuf {
fn dot_git_dir(&self) -> PathBuf {
let repo = self.repository.lock();
repo.path().into()
}
@@ -233,7 +234,7 @@ pub struct FakeGitRepository {
#[derive(Debug, Clone)]
pub struct FakeGitRepositoryState {
pub path: PathBuf,
pub dot_git_dir: PathBuf,
pub event_emitter: smol::channel::Sender<PathBuf>,
pub index_contents: HashMap<PathBuf, String>,
pub blames: HashMap<PathBuf, Blame>,
@@ -249,9 +250,9 @@ impl FakeGitRepository {
}
impl FakeGitRepositoryState {
pub fn new(path: PathBuf, event_emitter: smol::channel::Sender<PathBuf>) -> Self {
pub fn new(dot_git_dir: PathBuf, event_emitter: smol::channel::Sender<PathBuf>) -> Self {
FakeGitRepositoryState {
path,
dot_git_dir,
event_emitter,
index_contents: Default::default(),
blames: Default::default(),
@@ -283,9 +284,9 @@ impl GitRepository for FakeGitRepository {
None
}
fn path(&self) -> PathBuf {
fn dot_git_dir(&self) -> PathBuf {
let state = self.state.lock();
state.path.clone()
state.dot_git_dir.clone()
}
fn status(&self, path_prefixes: &[PathBuf]) -> Result<GitStatus> {
@@ -334,7 +335,7 @@ impl GitRepository for FakeGitRepository {
state.current_branch_name = Some(name.to_owned());
state
.event_emitter
.try_send(state.path.clone())
.try_send(state.dot_git_dir.clone())
.expect("Dropped repo change event");
Ok(())
}
@@ -344,7 +345,7 @@ impl GitRepository for FakeGitRepository {
state.branches.insert(name.to_owned());
state
.event_emitter
.try_send(state.path.clone())
.try_send(state.dot_git_dir.clone())
.expect("Dropped repo change event");
Ok(())
}

View File

@@ -188,12 +188,12 @@ impl GitPanel {
}
fn should_show_scrollbar(_cx: &AppContext) -> bool {
// todo!(): plug into settings
// TODO: plug into settings
true
}
fn should_autohide_scrollbar(_cx: &AppContext) -> bool {
// todo!(): plug into settings
// TODO: plug into settings
true
}
@@ -255,34 +255,34 @@ impl GitPanel {
impl GitPanel {
fn stage_all(&mut self, _: &StageAll, _cx: &mut ViewContext<Self>) {
// todo!(): Implement stage all
// TODO: Implement stage all
println!("Stage all triggered");
}
fn unstage_all(&mut self, _: &UnstageAll, _cx: &mut ViewContext<Self>) {
// todo!(): Implement unstage all
// TODO: Implement unstage all
println!("Unstage all triggered");
}
fn discard_all(&mut self, _: &DiscardAll, _cx: &mut ViewContext<Self>) {
// todo!(): Implement discard all
// TODO: Implement discard all
println!("Discard all triggered");
}
/// Commit all staged changes
fn commit_staged_changes(&mut self, _: &CommitStagedChanges, _cx: &mut ViewContext<Self>) {
// todo!(): Implement commit all staged
// TODO: Implement commit all staged
println!("Commit staged changes triggered");
}
/// Commit all changes, regardless of whether they are staged or not
fn commit_all_changes(&mut self, _: &CommitAllChanges, _cx: &mut ViewContext<Self>) {
// todo!(): Implement commit all changes
// TODO: Implement commit all changes
println!("Commit all changes triggered");
}
fn all_staged(&self) -> bool {
// todo!(): Implement all_staged
// TODO: Implement all_staged
true
}
@@ -378,7 +378,7 @@ impl GitPanel {
}
}
// todo!(): Update expanded directory state
// TODO: Update expanded directory state
fn update_visible_entries(
&mut self,
new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,

View File

@@ -41,7 +41,7 @@ const REMOVED_COLOR: Hsla = Hsla {
a: 1.0,
};
// todo!(): Add updated status colors to theme
// TODO: Add updated status colors to theme
pub fn git_status_icon(status: GitFileStatus) -> impl IntoElement {
match status {
GitFileStatus::Added => Icon::new(IconName::SquarePlus).color(Color::Custom(ADDED_COLOR)),

View File

@@ -218,13 +218,13 @@ impl Render for GradientViewer {
let height = square_bounds.size.height;
let horizontal_offset = height;
let vertical_offset = px(30.);
let mut path = gpui::Path::new(square_bounds.lower_left());
let mut path = gpui::Path::new(square_bounds.bottom_left());
path.line_to(square_bounds.origin + point(horizontal_offset, vertical_offset));
path.line_to(
square_bounds.upper_right() + point(-horizontal_offset, vertical_offset),
square_bounds.top_right() + point(-horizontal_offset, vertical_offset),
);
path.line_to(square_bounds.lower_right());
path.line_to(square_bounds.lower_left());
path.line_to(square_bounds.bottom_right());
path.line_to(square_bounds.bottom_left());
cx.paint_path(
path,
linear_gradient(

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