Compare commits

...

53 Commits

Author SHA1 Message Date
Nate Butler
a3e04867ab Update git_ui.rs 2024-12-16 15:30:27 -05:00
Nate Butler
3abf165c1d Update git_panel.rs 2024-12-16 15:30:25 -05:00
Nate Butler
48eae645f2 Revert "Merge branch 'main' into git-panel-commit-editor"
This reverts commit 78b6cae754, reversing
changes made to 1cac4b6c00.
2024-12-16 15:29:44 -05:00
Nate Butler
78b6cae754 Merge branch 'main' into git-panel-commit-editor 2024-12-16 15:26:45 -05:00
Nate Butler
1cac4b6c00 Revert "wip"
This reverts commit aee641d79d.
2024-12-16 15:12:49 -05: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
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
Nate Butler
aee641d79d wip 2024-12-16 13:24:55 -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
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
Nate Butler
4f439ae35f Update git_panel.rs 2024-12-13 21:19:16 -05: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
Nate Butler
c2eea3a474 start on commit editor 2024-12-13 21:06:18 -05:00
Nate Butler
695f06c020 Checkpoint 2024-12-13 20:18:09 -05: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
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
Nate Butler
19d6e067af Toggle & Switch (#21979)
![CleanShot 2024-12-13 at 11 27
39@2x](https://github.com/user-attachments/assets/7c7828c0-c5c7-4dc6-931e-722366d4f15a)

- Adds the Switch component
- Updates `Selected`, `Selectable` -> `ToggleState`, `Toggleable`
- Adds `checkbox` and `switch` functions to align better with other
elements in our layout system.

We decided not to merge Switch and Checkbox. However, in a followup I'll
introduce a Toggle or AnyToggle enum so we can update
`CheckboxWithLabel` -> `ToggleWithLabel` as this component will work
exactly the same with either a Checkbox or a Switch.

Release Notes:

- N/A
2024-12-13 14:23:02 -05:00
Conrad Irwin
2f2e7f0317 Revert "Resolve documentation for visible completions (#21705)" (#21985)
This reverts commit ab595b0d55.

Release Notes:

- (preview only) Fixed a panic in completions
2024-12-13 12:22:26 -07:00
Michael Sloan
2b699053e6 Log invariant violations in fuzzy string match iterator (#21983)
Seeing frequent inscrutable panics here

Release Notes:

- N/A
2024-12-13 11:16:30 -07:00
99 changed files with 1324 additions and 848 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 }}

7
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",
@@ -5030,6 +5032,7 @@ name = "fuzzy"
version = "0.1.0"
dependencies = [
"gpui",
"log",
"util",
]
@@ -5179,14 +5182,17 @@ dependencies = [
"anyhow",
"collections",
"db",
"editor",
"git",
"gpui",
"language",
"project",
"schemars",
"serde",
"serde_derive",
"serde_json",
"settings",
"theme",
"ui",
"util",
"windows 0.58.0",
@@ -13973,7 +13979,6 @@ dependencies = [
"futures-lite 1.13.0",
"git2",
"globset",
"itertools 0.13.0",
"log",
"rand 0.8.5",
"regex",

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

@@ -15,7 +15,7 @@
"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
"f12": "editor::GoToDefinition",
"ctrl-f12": "editor::GoToDefinitionSplit",

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

@@ -18,7 +18,7 @@
"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
"shift-f12": "editor::FindAllReferences",
"alt-cmd-down": "editor::GoToDefinition",

View File

@@ -305,7 +305,7 @@ impl PickerDelegate for SavedContextPickerDelegate {
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.toggle_state(selected)
.child(item),
)
}
@@ -442,7 +442,7 @@ impl AssistantPanel {
)
}
})
.selected(
.toggle_state(
pane.active_item()
.map_or(false, |item| item.downcast::<ContextHistory>().is_some()),
);
@@ -4956,7 +4956,7 @@ fn render_slash_command_output_toggle(
("slash-command-output-fold-indicator", row.0 as u64),
!is_folded,
)
.selected(is_folded)
.toggle_state(is_folded)
.on_click(move |_e, cx| fold(!is_folded, cx))
.into_any_element()
}
@@ -4971,7 +4971,7 @@ fn fold_toggle(
) -> AnyElement {
move |row, is_folded, fold, _cx| {
Disclosure::new((name, row.0 as u64), !is_folded)
.selected(is_folded)
.toggle_state(is_folded)
.on_click(move |_e, cx| fold(!is_folded, cx))
.into_any_element()
}
@@ -5013,7 +5013,7 @@ fn render_quote_selection_output_toggle(
_cx: &mut WindowContext,
) -> AnyElement {
Disclosure::new(("quote-selection-indicator", row.0 as u64), !is_folded)
.selected(is_folded)
.toggle_state(is_folded)
.on_click(move |_e, cx| fold(!is_folded, cx))
.into_any_element()
}
@@ -5036,7 +5036,7 @@ fn render_pending_slash_command_gutter_decoration(
icon = icon.icon_color(Color::Muted);
}
PendingSlashCommandStatus::Running { .. } => {
icon = icon.selected(true);
icon = icon.toggle_state(true);
}
PendingSlashCommandStatus::Error(_) => icon = icon.icon_color(Color::Error),
}

View File

@@ -1534,7 +1534,7 @@ impl Render for PromptEditor {
v_flex()
.child(
IconButton::new("rate-limit-error", IconName::XCircle)
.selected(self.show_rate_limit_notice)
.toggle_state(self.show_rate_limit_notice)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.on_click(cx.listener(Self::toggle_rate_limit_notice)),
@@ -2133,15 +2133,15 @@ impl PromptEditor {
"dont-show-again",
Label::new("Don't show again"),
if dismissed_rate_limit_notice() {
ui::Selection::Selected
ui::ToggleState::Selected
} else {
ui::Selection::Unselected
ui::ToggleState::Unselected
},
|selection, cx| {
let is_dismissed = match selection {
ui::Selection::Unselected => false,
ui::Selection::Indeterminate => return,
ui::Selection::Selected => true,
ui::ToggleState::Unselected => false,
ui::ToggleState::Indeterminate => return,
ui::ToggleState::Selected => true,
};
set_rate_limit_notice_dismissed(is_dismissed, cx)

View File

@@ -232,13 +232,13 @@ impl PickerDelegate for PromptPickerDelegate {
let element = ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.toggle_state(selected)
.child(h_flex().h_5().line_height(relative(1.)).child(Label::new(
prompt.title.clone().unwrap_or("Untitled".into()),
)))
.end_slot::<IconButton>(default.then(|| {
IconButton::new("toggle-default-prompt", IconName::SparkleFilled)
.selected(true)
.toggle_state(true)
.icon_color(Color::Accent)
.shape(IconButtonShape::Square)
.tooltip(move |cx| Tooltip::text("Remove from Default Prompt", cx))
@@ -274,7 +274,7 @@ impl PickerDelegate for PromptPickerDelegate {
})
.child(
IconButton::new("toggle-default-prompt", IconName::Sparkle)
.selected(default)
.toggle_state(default)
.selected_icon(IconName::SparkleFilled)
.icon_color(if default { Color::Accent } else { Color::Muted })
.shape(IconButtonShape::Square)
@@ -1053,7 +1053,7 @@ impl PromptLibrary {
IconName::Sparkle,
)
.style(ButtonStyle::Transparent)
.selected(prompt_metadata.default)
.toggle_state(prompt_metadata.default)
.selected_icon(IconName::SparkleFilled)
.icon_color(if prompt_metadata.default {
Color::Accent

View File

@@ -176,7 +176,7 @@ impl PickerDelegate for SlashCommandDelegate {
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Dense)
.selected(selected)
.toggle_state(selected)
.tooltip({
let description = info.description.clone();
move |cx| cx.new_view(|_| Tooltip::new(description.clone())).into()
@@ -229,7 +229,7 @@ impl PickerDelegate for SlashCommandDelegate {
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Dense)
.selected(selected)
.toggle_state(selected)
.child(renderer(cx)),
),
}

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

@@ -23,4 +23,5 @@ pub struct Context {
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ContextKind {
File,
FetchedUrl,
}

View File

@@ -1,3 +1,4 @@
mod fetch_context_picker;
mod file_context_picker;
use std::sync::Arc;
@@ -11,6 +12,7 @@ use ui::{prelude::*, ListItem, ListItemSpacing, Tooltip};
use util::ResultExt;
use workspace::Workspace;
use crate::context_picker::fetch_context_picker::FetchContextPicker;
use crate::context_picker::file_context_picker::FileContextPicker;
use crate::message_editor::MessageEditor;
@@ -18,6 +20,7 @@ use crate::message_editor::MessageEditor;
enum ContextPickerMode {
Default,
File(View<FileContextPicker>),
Fetch(View<FetchContextPicker>),
}
pub(super) struct ContextPicker {
@@ -47,7 +50,7 @@ impl ContextPicker {
icon: IconName::File,
},
ContextPickerEntry {
name: "web".into(),
name: "fetch".into(),
description: "Fetch content from URL".into(),
icon: IconName::Globe,
},
@@ -77,16 +80,21 @@ impl FocusableView for ContextPicker {
match &self.mode {
ContextPickerMode::Default => self.picker.focus_handle(cx),
ContextPickerMode::File(file_picker) => file_picker.focus_handle(cx),
ContextPickerMode::Fetch(fetch_picker) => fetch_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::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
})
}
}
@@ -144,6 +152,16 @@ impl PickerDelegate for ContextPickerDelegate {
)
}));
}
"fetch" => {
this.mode = ContextPickerMode::Fetch(cx.new_view(|cx| {
FetchContextPicker::new(
self.context_picker.clone(),
self.workspace.clone(),
self.message_editor.clone(),
cx,
)
}));
}
_ => {}
}
@@ -157,7 +175,7 @@ impl PickerDelegate for ContextPickerDelegate {
self.context_picker
.update(cx, |this, cx| match this.mode {
ContextPickerMode::Default => cx.emit(DismissEvent),
ContextPickerMode::File(_) => {}
ContextPickerMode::File(_) | ContextPickerMode::Fetch(_) => {}
})
.log_err();
}
@@ -174,7 +192,7 @@ impl PickerDelegate for ContextPickerDelegate {
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Dense)
.selected(selected)
.toggle_state(selected)
.tooltip({
let description = entry.description.clone();
move |cx| cx.new_view(|_cx| Tooltip::new(description.clone())).into()

View File

@@ -0,0 +1,218 @@
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, WeakView};
use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
use http_client::{AsyncBody, HttpClientWithUrl};
use picker::{Picker, PickerDelegate};
use ui::{prelude::*, ListItem, ListItemSpacing, ViewContext};
use workspace::Workspace;
use crate::context::ContextKind;
use crate::context_picker::ContextPicker;
use crate::message_editor::MessageEditor;
pub struct FetchContextPicker {
picker: View<Picker<FetchContextPickerDelegate>>,
}
impl FetchContextPicker {
pub fn new(
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
message_editor: WeakView<MessageEditor>,
cx: &mut ViewContext<Self>,
) -> Self {
let delegate = FetchContextPickerDelegate::new(context_picker, workspace, message_editor);
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>,
message_editor: WeakView<MessageEditor>,
url: String,
}
impl FetchContextPickerDelegate {
pub fn new(
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
message_editor: WeakView<MessageEditor>,
) -> Self {
FetchContextPickerDelegate {
context_picker,
workspace,
message_editor,
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 {
1
}
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
.message_editor
.update(cx, |message_editor, _cx| {
message_editor.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)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(self.url.clone()),
)
}
}

View File

@@ -245,7 +245,7 @@ impl PickerDelegate for FileContextPickerDelegate {
this.reset_mode();
cx.emit(DismissEvent);
})
.log_err();
.ok();
}
fn render_match(
@@ -260,7 +260,7 @@ impl PickerDelegate for FileContextPickerDelegate {
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.toggle_state(selected)
.child(mat.path.to_string_lossy().to_string()),
)
}

View File

@@ -1623,7 +1623,7 @@ impl Render for PromptEditor {
v_flex()
.child(
IconButton::new("rate-limit-error", IconName::XCircle)
.selected(self.show_rate_limit_notice)
.toggle_state(self.show_rate_limit_notice)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.on_click(cx.listener(Self::toggle_rate_limit_notice)),
@@ -2065,15 +2065,15 @@ impl PromptEditor {
"dont-show-again",
Label::new("Don't show again"),
if dismissed_rate_limit_notice() {
ui::Selection::Selected
ui::ToggleState::Selected
} else {
ui::Selection::Unselected
ui::ToggleState::Unselected
},
|selection, cx| {
let is_dismissed = match selection {
ui::Selection::Unselected => false,
ui::Selection::Indeterminate => return,
ui::Selection::Selected => true,
ui::ToggleState::Unselected => false,
ui::ToggleState::Indeterminate => return,
ui::ToggleState::Selected => true,
};
set_rate_limit_notice_dismissed(is_dismissed, cx)

View File

@@ -268,8 +268,8 @@ impl Render for MessageEditor {
self.use_tools.into(),
cx.listener(|this, selection, _cx| {
this.use_tools = match selection {
Selection::Selected => true,
Selection::Unselected | Selection::Indeterminate => false,
ToggleState::Selected => true,
ToggleState::Unselected | ToggleState::Indeterminate => false,
};
}),
)))

View File

@@ -193,12 +193,19 @@ impl Thread {
if let Some(context) = self.context_for_message(message.id) {
let mut file_context = String::new();
let mut fetch_context = String::new();
for context in context.iter() {
match context.kind {
ContextKind::File => {
file_context.push_str(&context.text);
file_context.push_str("\n");
file_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');
}
}
}
@@ -209,6 +216,11 @@ impl Thread {
context_text.push_str(&file_context);
}
if !fetch_context.is_empty() {
context_text.push_str("The following fetched results are available\n");
context_text.push_str(&fetch_context);
}
request_message
.content
.push(MessageContent::Text(context_text))

View File

@@ -841,7 +841,7 @@ impl CollabPanel {
ListItem::new(SharedString::from(user.github_login.clone()))
.start_slot(Avatar::new(user.avatar_uri.clone()))
.child(Label::new(user.github_login.clone()))
.selected(is_selected)
.toggle_state(is_selected)
.end_slot(if is_pending {
Label::new("Calling").color(Color::Muted).into_any_element()
} else if is_current_user {
@@ -894,7 +894,7 @@ impl CollabPanel {
.into();
ListItem::new(project_id as usize)
.selected(is_selected)
.toggle_state(is_selected)
.on_click(cx.listener(move |this, _, cx| {
this.workspace
.update(cx, |workspace, cx| {
@@ -924,7 +924,7 @@ impl CollabPanel {
let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize);
ListItem::new(("screen", id))
.selected(is_selected)
.toggle_state(is_selected)
.start_slot(
h_flex()
.gap_1()
@@ -964,7 +964,7 @@ impl CollabPanel {
let channel_store = self.channel_store.read(cx);
let has_channel_buffer_changed = channel_store.has_channel_buffer_changed(channel_id);
ListItem::new("channel-notes")
.selected(is_selected)
.toggle_state(is_selected)
.on_click(cx.listener(move |this, _, cx| {
this.open_channel_notes(channel_id, cx);
}))
@@ -996,7 +996,7 @@ impl CollabPanel {
let channel_store = self.channel_store.read(cx);
let has_messages_notification = channel_store.has_new_messages(channel_id);
ListItem::new("channel-chat")
.selected(is_selected)
.toggle_state(is_selected)
.on_click(cx.listener(move |this, _, cx| {
this.join_channel_chat(channel_id, cx);
}))
@@ -2253,7 +2253,7 @@ impl CollabPanel {
})
.inset(true)
.end_slot::<AnyElement>(button)
.selected(is_selected),
.toggle_state(is_selected),
)
}
@@ -2270,7 +2270,7 @@ impl CollabPanel {
let item = ListItem::new(github_login.clone())
.indent_level(1)
.indent_step_size(px(20.))
.selected(is_selected)
.toggle_state(is_selected)
.child(
h_flex()
.w_full()
@@ -2381,7 +2381,7 @@ impl CollabPanel {
ListItem::new(github_login.clone())
.indent_level(1)
.indent_step_size(px(20.))
.selected(is_selected)
.toggle_state(is_selected)
.child(
h_flex()
.w_full()
@@ -2425,7 +2425,7 @@ impl CollabPanel {
];
ListItem::new(("channel-invite", channel.id.0 as usize))
.selected(is_selected)
.toggle_state(is_selected)
.child(
h_flex()
.w_full()
@@ -2448,7 +2448,7 @@ impl CollabPanel {
ListItem::new("contact-placeholder")
.child(Icon::new(IconName::Plus))
.child(Label::new("Add a Contact"))
.selected(is_selected)
.toggle_state(is_selected)
.on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
}
@@ -2547,7 +2547,7 @@ impl CollabPanel {
// Add one level of depth for the disclosure arrow.
.indent_level(depth + 1)
.indent_step_size(px(20.))
.selected(is_selected || is_active)
.toggle_state(is_selected || is_active)
.toggle(disclosed)
.on_toggle(
cx.listener(move |this, _, cx| {

View File

@@ -89,15 +89,15 @@ impl ChannelModal {
cx.notify()
}
fn set_channel_visibility(&mut self, selection: &Selection, cx: &mut ViewContext<Self>) {
fn set_channel_visibility(&mut self, selection: &ToggleState, cx: &mut ViewContext<Self>) {
self.channel_store.update(cx, |channel_store, cx| {
channel_store
.set_channel_visibility(
self.channel_id,
match selection {
Selection::Unselected => ChannelVisibility::Members,
Selection::Selected => ChannelVisibility::Public,
Selection::Indeterminate => return,
ToggleState::Unselected => ChannelVisibility::Members,
ToggleState::Selected => ChannelVisibility::Public,
ToggleState::Indeterminate => return,
},
cx,
)
@@ -159,9 +159,9 @@ impl Render for ChannelModal {
"is-public",
Label::new("Public").size(LabelSize::Small),
if visibility == ChannelVisibility::Public {
ui::Selection::Selected
ui::ToggleState::Selected
} else {
ui::Selection::Unselected
ui::ToggleState::Unselected
},
cx.listener(Self::set_channel_visibility),
))
@@ -386,7 +386,7 @@ impl PickerDelegate for ChannelModalDelegate {
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.toggle_state(selected)
.start_slot(Avatar::new(user.avatar_uri.clone()))
.child(Label::new(user.github_login.clone()))
.end_slot(h_flex().gap_2().map(|slot| {

View File

@@ -151,7 +151,7 @@ impl PickerDelegate for ContactFinderDelegate {
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.toggle_state(selected)
.start_slot(Avatar::new(user.avatar_uri.clone()))
.child(Label::new(user.github_login.clone()))
.end_slot::<Icon>(icon_path.map(Icon::from_path)),

View File

@@ -397,7 +397,7 @@ impl PickerDelegate for CommandPaletteDelegate {
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.toggle_state(selected)
.child(
h_flex()
.w_full()

View File

@@ -1,9 +1,4 @@
use std::{
cell::Cell,
cmp::{min, Reverse},
ops::Range,
sync::Arc,
};
use std::{cell::Cell, cmp::Reverse, ops::Range, sync::Arc};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
@@ -16,14 +11,13 @@ use language::{CodeLabel, Documentation};
use lsp::LanguageServerId;
use multi_buffer::{Anchor, ExcerptId};
use ordered_float::OrderedFloat;
use parking_lot::{Mutex, RwLock};
use parking_lot::RwLock;
use project::{CodeAction, Completion, TaskSourceKind};
use std::iter;
use task::ResolvedTask;
use ui::{
h_flex, ActiveTheme as _, Color, FluentBuilder as _, InteractiveElement as _, IntoElement,
Label, LabelCommon as _, LabelSize, ListItem, ParentElement as _, Popover, Selectable as _,
StatefulInteractiveElement as _, Styled, StyledExt as _,
Label, LabelCommon as _, LabelSize, ListItem, ParentElement as _, Popover,
StatefulInteractiveElement as _, Styled, StyledExt as _, Toggleable as _,
};
use util::ResultExt as _;
use workspace::Workspace;
@@ -151,7 +145,6 @@ pub struct CompletionsMenu {
resolve_completions: bool,
pub aside_was_displayed: Cell<bool>,
show_completion_documentation: bool,
last_rendered_range: Arc<Mutex<Option<Range<usize>>>>,
}
impl CompletionsMenu {
@@ -180,6 +173,7 @@ impl CompletionsMenu {
sort_completions,
initial_position,
buffer,
show_completion_documentation,
completions: Arc::new(RwLock::new(completions)),
match_candidates,
matches: Vec::new().into(),
@@ -187,8 +181,6 @@ impl CompletionsMenu {
scroll_handle: UniformListScrollHandle::new(),
resolve_completions: true,
aside_was_displayed: Cell::new(aside_was_displayed),
show_completion_documentation,
last_rendered_range: Arc::new(Mutex::new(None)),
}
}
@@ -244,7 +236,6 @@ impl CompletionsMenu {
resolve_completions: false,
aside_was_displayed: Cell::new(false),
show_completion_documentation: false,
last_rendered_range: Arc::new(Mutex::new(None)),
}
}
@@ -253,7 +244,11 @@ impl CompletionsMenu {
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
self.update_selection_index(0, provider, cx);
self.selected_item = 0;
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
self.resolve_selected_completion(provider, cx);
cx.notify();
}
fn select_prev(
@@ -261,7 +256,15 @@ impl CompletionsMenu {
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
self.update_selection_index(self.prev_match_index(), provider, cx);
if self.selected_item > 0 {
self.selected_item -= 1;
} else {
self.selected_item = self.matches.len() - 1;
}
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
self.resolve_selected_completion(provider, cx);
cx.notify();
}
fn select_next(
@@ -269,7 +272,15 @@ impl CompletionsMenu {
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
self.update_selection_index(self.next_match_index(), provider, cx);
if self.selected_item + 1 < self.matches.len() {
self.selected_item += 1;
} else {
self.selected_item = 0;
}
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
self.resolve_selected_completion(provider, cx);
cx.notify();
}
fn select_last(
@@ -277,41 +288,14 @@ impl CompletionsMenu {
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
self.update_selection_index(self.matches.len() - 1, provider, cx);
self.selected_item = self.matches.len() - 1;
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
self.resolve_selected_completion(provider, cx);
cx.notify();
}
fn update_selection_index(
&mut self,
match_index: usize,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
) {
if self.selected_item != match_index {
self.selected_item = match_index;
self.scroll_handle
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
self.resolve_visible_completions(provider, cx);
cx.notify();
}
}
fn prev_match_index(&self) -> usize {
if self.selected_item > 0 {
self.selected_item - 1
} else {
self.matches.len() - 1
}
}
fn next_match_index(&self) -> usize {
if self.selected_item + 1 < self.matches.len() {
self.selected_item + 1
} else {
0
}
}
pub fn resolve_visible_completions(
pub fn resolve_selected_completion(
&mut self,
provider: Option<&dyn CompletionProvider>,
cx: &mut ViewContext<Editor>,
@@ -323,59 +307,10 @@ impl CompletionsMenu {
return;
};
// Attempt to resolve completions for every item that will be displayed. This matters
// because single line documentation may be displayed inline with the completion.
//
// When navigating to the very beginning or end of completions, `last_rendered_range` may
// have no overlap with the completions that will be displayed, so instead use a range based
// on the last rendered count.
const APPROXIMATE_VISIBLE_COUNT: usize = 12;
let last_rendered_range = self.last_rendered_range.lock().clone();
let visible_count = last_rendered_range
.clone()
.map_or(APPROXIMATE_VISIBLE_COUNT, |range| range.count());
let matches_range = if self.selected_item == 0 {
0..min(visible_count, self.matches.len())
} else if self.selected_item == self.matches.len() - 1 {
self.matches.len().saturating_sub(visible_count)..self.matches.len()
} else {
last_rendered_range.unwrap_or_else(|| self.selected_item..self.selected_item + 1)
};
// Expand the range to resolve more completions than are predicted to be visible, to reduce
// jank on navigation.
const EXTRA_TO_RESOLVE: usize = 4;
let matches_indices = util::iterate_expanded_and_wrapped_usize_range(
matches_range.clone(),
EXTRA_TO_RESOLVE,
EXTRA_TO_RESOLVE,
self.matches.len(),
);
// Avoid work by sometimes filtering out completions that already have documentation.
// This filtering doesn't happen if the completions are currently being updated.
let candidate_ids = matches_indices.map(|i| self.matches[i].candidate_id);
let candidate_ids = match self.completions.try_read() {
None => candidate_ids.collect::<Vec<usize>>(),
Some(completions) => candidate_ids
.filter(|i| completions[*i].documentation.is_none())
.collect::<Vec<usize>>(),
};
// Current selection is always resolved even if it already has documentation, to handle
// out-of-spec language servers that return more results later.
let selected_candidate_id = self.matches[self.selected_item].candidate_id;
let candidate_ids = iter::once(selected_candidate_id)
.chain(
candidate_ids
.into_iter()
.filter(|id| *id != selected_candidate_id),
)
.collect::<Vec<usize>>();
let completion_index = self.matches[self.selected_item].candidate_id;
let resolve_task = provider.resolve_completions(
self.buffer.clone(),
candidate_ids,
vec![completion_index],
self.completions.clone(),
cx,
);
@@ -471,14 +406,11 @@ impl CompletionsMenu {
.occlude()
});
let last_rendered_range = self.last_rendered_range.clone();
let list = uniform_list(
cx.view().clone(),
"completions",
matches.len(),
move |_editor, range, cx| {
last_rendered_range.lock().replace(range.clone());
let start_ix = range.start;
let completions_guard = completions.read();
@@ -541,7 +473,7 @@ impl CompletionsMenu {
div().min_w(px(220.)).max_w(px(540.)).child(
ListItem::new(mat.candidate_id)
.inset(true)
.selected(item_ix == selected_item)
.toggle_state(item_ix == selected_item)
.on_click(cx.listener(move |editor, _event, cx| {
cx.stop_propagation();
if let Some(task) = editor.confirm_completion(

View File

@@ -3736,7 +3736,7 @@ impl Editor {
if editor.focus_handle.is_focused(cx) && menu.is_some() {
let mut menu = menu.unwrap();
menu.resolve_visible_completions(editor.completion_provider.as_deref(), cx);
menu.resolve_selected_completion(editor.completion_provider.as_deref(), cx);
*context_menu = Some(CodeContextMenu::Completions(menu));
drop(context_menu);
cx.notify();
@@ -4811,7 +4811,7 @@ impl Editor {
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.selected(is_active)
.toggle_state(is_active)
.tooltip({
let focus_handle = self.focus_handle.clone();
move |cx| {
@@ -4988,7 +4988,7 @@ impl Editor {
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.selected(is_active)
.toggle_state(is_active)
.on_click(cx.listener(move |editor, _e, cx| {
editor.focus(cx);
editor.toggle_code_actions(
@@ -6135,29 +6135,7 @@ impl Editor {
});
}
pub fn duplicate_selection(&mut self, _: &DuplicateSelection, cx: &mut ViewContext<Self>) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let buffer = &display_map.buffer_snapshot;
let selections = self.selections.all::<Point>(cx);
let mut edits = Vec::new();
for selection in selections.iter() {
let start = selection.start;
let end = selection.end;
let text = buffer.text_for_range(start..end).collect::<String>();
edits.push((selection.end..selection.end, text));
}
self.transact(cx, |this, cx| {
this.buffer.update(cx, |buffer, cx| {
buffer.edit(edits, None, cx);
});
this.request_autoscroll(Autoscroll::fit(), cx);
});
}
pub fn duplicate_line(&mut self, upwards: bool, cx: &mut ViewContext<Self>) {
pub fn duplicate(&mut self, upwards: bool, whole_lines: bool, cx: &mut ViewContext<Self>) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let buffer = &display_map.buffer_snapshot;
let selections = self.selections.all::<Point>(cx);
@@ -6165,36 +6143,44 @@ impl Editor {
let mut edits = Vec::new();
let mut selections_iter = selections.iter().peekable();
while let Some(selection) = selections_iter.next() {
// Avoid duplicating the same lines twice.
let mut rows = selection.spanned_rows(false, &display_map);
while let Some(next_selection) = selections_iter.peek() {
let next_rows = next_selection.spanned_rows(false, &display_map);
if next_rows.start < rows.end {
rows.end = next_rows.end;
selections_iter.next().unwrap();
} else {
break;
// duplicate line-wise
if whole_lines || selection.start == selection.end {
// Avoid duplicating the same lines twice.
while let Some(next_selection) = selections_iter.peek() {
let next_rows = next_selection.spanned_rows(false, &display_map);
if next_rows.start < rows.end {
rows.end = next_rows.end;
selections_iter.next().unwrap();
} else {
break;
}
}
}
// Copy the text from the selected row region and splice it either at the start
// or end of the region.
let start = Point::new(rows.start.0, 0);
let end = Point::new(
rows.end.previous_row().0,
buffer.line_len(rows.end.previous_row()),
);
let text = buffer
.text_for_range(start..end)
.chain(Some("\n"))
.collect::<String>();
let insert_location = if upwards {
Point::new(rows.end.0, 0)
// Copy the text from the selected row region and splice it either at the start
// or end of the region.
let start = Point::new(rows.start.0, 0);
let end = Point::new(
rows.end.previous_row().0,
buffer.line_len(rows.end.previous_row()),
);
let text = buffer
.text_for_range(start..end)
.chain(Some("\n"))
.collect::<String>();
let insert_location = if upwards {
Point::new(rows.end.0, 0)
} else {
start
};
edits.push((insert_location..insert_location, text));
} else {
start
};
edits.push((insert_location..insert_location, text));
// duplicate character-wise
let start = selection.start;
let end = selection.end;
let text = buffer.text_for_range(start..end).collect::<String>();
edits.push((selection.end..selection.end, text));
}
}
self.transact(cx, |this, cx| {
@@ -6207,11 +6193,15 @@ impl Editor {
}
pub fn duplicate_line_up(&mut self, _: &DuplicateLineUp, cx: &mut ViewContext<Self>) {
self.duplicate_line(true, cx);
self.duplicate(true, true, cx);
}
pub fn duplicate_line_down(&mut self, _: &DuplicateLineDown, cx: &mut ViewContext<Self>) {
self.duplicate_line(false, cx);
self.duplicate(false, true, cx);
}
pub fn duplicate_selection(&mut self, _: &DuplicateSelection, cx: &mut ViewContext<Self>) {
self.duplicate(false, false, cx);
}
pub fn move_line_up(&mut self, _: &MoveLineUp, cx: &mut ViewContext<Self>) {
@@ -13772,7 +13762,7 @@ impl EditorSnapshot {
if folded || (is_foldable && (row_contains_cursor || self.gutter_hovered)) {
Some(
Disclosure::new(("gutter_crease", buffer_row.0), !folded)
.selected(folded)
.toggle_state(folded)
.on_click(cx.listener_for(&editor, move |this, _e, cx| {
if folded {
this.unfold_at(&UnfoldAt { buffer_row }, cx);

View File

@@ -265,8 +265,8 @@ impl RenderOnce for BufferFontLigaturesControl {
|selection, cx| {
Self::write(
match selection {
Selection::Selected => true,
Selection::Unselected | Selection::Indeterminate => false,
ToggleState::Selected => true,
ToggleState::Unselected | ToggleState::Indeterminate => false,
},
cx,
);
@@ -318,8 +318,8 @@ impl RenderOnce for InlineGitBlameControl {
|selection, cx| {
Self::write(
match selection {
Selection::Selected => true,
Selection::Unselected | Selection::Indeterminate => false,
ToggleState::Selected => true,
ToggleState::Unselected | ToggleState::Indeterminate => false,
},
cx,
);
@@ -371,8 +371,8 @@ impl RenderOnce for LineNumbersControl {
|selection, cx| {
Self::write(
match selection {
Selection::Selected => true,
Selection::Unselected | Selection::Indeterminate => false,
ToggleState::Selected => true,
ToggleState::Unselected | ToggleState::Indeterminate => false,
},
cx,
);

View File

@@ -25,18 +25,14 @@ use language::{
use language_settings::{Formatter, FormatterList, IndentGuideSettings};
use multi_buffer::MultiBufferIndentGuide;
use parking_lot::Mutex;
use pretty_assertions::{assert_eq, assert_ne};
use project::{buffer_store::BufferChangeSet, FakeFs};
use project::{
lsp_command::SIGNATURE_HELP_HIGHLIGHT_CURRENT,
project_settings::{LspSettings, ProjectSettings},
};
use serde_json::{self, json};
use std::sync::atomic::{self, AtomicBool, AtomicUsize};
use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
use std::{
iter,
sync::atomic::{self, AtomicUsize},
};
use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
use unindent::Unindent;
use util::{
@@ -10746,62 +10742,6 @@ async fn test_completions_resolve_updates_labels(cx: &mut gpui::TestAppContext)
async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let item_0 = lsp::CompletionItem {
label: "abs".into(),
insert_text: Some("abs".into()),
data: Some(json!({ "very": "special"})),
insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
lsp::InsertReplaceEdit {
new_text: "abs".to_string(),
insert: lsp::Range::default(),
replace: lsp::Range::default(),
},
)),
..lsp::CompletionItem::default()
};
let items = iter::once(item_0.clone())
.chain((11..51).map(|i| lsp::CompletionItem {
label: format!("item_{}", i),
insert_text: Some(format!("item_{}", i)),
insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
..lsp::CompletionItem::default()
}))
.collect::<Vec<_>>();
let default_commit_characters = vec!["?".to_string()];
let default_data = json!({ "default": "data"});
let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
let default_edit_range = lsp::Range {
start: lsp::Position {
line: 0,
character: 5,
},
end: lsp::Position {
line: 0,
character: 5,
},
};
let item_0_out = lsp::CompletionItem {
commit_characters: Some(default_commit_characters.clone()),
insert_text_format: Some(default_insert_text_format),
..item_0
};
let items_out = iter::once(item_0_out)
.chain(items[1..].iter().map(|item| lsp::CompletionItem {
commit_characters: Some(default_commit_characters.clone()),
data: Some(default_data.clone()),
insert_text_mode: Some(default_insert_text_mode),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: default_edit_range,
new_text: item.label.clone(),
})),
..item.clone()
}))
.collect::<Vec<lsp::CompletionItem>>();
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
@@ -10818,15 +10758,138 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
cx.simulate_keystroke(".");
let default_commit_characters = vec!["?".to_string()];
let default_data = json!({ "very": "special"});
let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
let default_edit_range = lsp::Range {
start: lsp::Position {
line: 0,
character: 5,
},
end: lsp::Position {
line: 0,
character: 5,
},
};
let resolve_requests_number = Arc::new(AtomicUsize::new(0));
let expect_first_item = Arc::new(AtomicBool::new(true));
cx.lsp
.server
.on_request::<lsp::request::ResolveCompletionItem, _, _>({
let closure_default_data = default_data.clone();
let closure_resolve_requests_number = resolve_requests_number.clone();
let closure_expect_first_item = expect_first_item.clone();
let closure_default_commit_characters = default_commit_characters.clone();
move |item_to_resolve, _| {
closure_resolve_requests_number.fetch_add(1, atomic::Ordering::Release);
let default_data = closure_default_data.clone();
let default_commit_characters = closure_default_commit_characters.clone();
let expect_first_item = closure_expect_first_item.clone();
async move {
if expect_first_item.load(atomic::Ordering::Acquire) {
assert_eq!(
item_to_resolve.label, "Some(2)",
"Should have selected the first item"
);
assert_eq!(
item_to_resolve.data,
Some(json!({ "very": "special"})),
"First item should bring its own data for resolving"
);
assert_eq!(
item_to_resolve.commit_characters,
Some(default_commit_characters),
"First item had no own commit characters and should inherit the default ones"
);
assert!(
matches!(
item_to_resolve.text_edit,
Some(lsp::CompletionTextEdit::InsertAndReplace { .. })
),
"First item should bring its own edit range for resolving"
);
assert_eq!(
item_to_resolve.insert_text_format,
Some(default_insert_text_format),
"First item had no own insert text format and should inherit the default one"
);
assert_eq!(
item_to_resolve.insert_text_mode,
Some(lsp::InsertTextMode::ADJUST_INDENTATION),
"First item should bring its own insert text mode for resolving"
);
Ok(item_to_resolve)
} else {
assert_eq!(
item_to_resolve.label, "vec![2]",
"Should have selected the last item"
);
assert_eq!(
item_to_resolve.data,
Some(default_data),
"Last item has no own resolve data and should inherit the default one"
);
assert_eq!(
item_to_resolve.commit_characters,
Some(default_commit_characters),
"Last item had no own commit characters and should inherit the default ones"
);
assert_eq!(
item_to_resolve.text_edit,
Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: default_edit_range,
new_text: "vec![2]".to_string()
})),
"Last item had no own edit range and should inherit the default one"
);
assert_eq!(
item_to_resolve.insert_text_format,
Some(lsp::InsertTextFormat::PLAIN_TEXT),
"Last item should bring its own insert text format for resolving"
);
assert_eq!(
item_to_resolve.insert_text_mode,
Some(default_insert_text_mode),
"Last item had no own insert text mode and should inherit the default one"
);
Ok(item_to_resolve)
}
}
}
}).detach();
let completion_data = default_data.clone();
let completion_characters = default_commit_characters.clone();
cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
let default_data = completion_data.clone();
let default_commit_characters = completion_characters.clone();
let items = items.clone();
async move {
Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
items,
items: vec![
lsp::CompletionItem {
label: "Some(2)".into(),
insert_text: Some("Some(2)".into()),
data: Some(json!({ "very": "special"})),
insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
lsp::InsertReplaceEdit {
new_text: "Some(2)".to_string(),
insert: lsp::Range::default(),
replace: lsp::Range::default(),
},
)),
..lsp::CompletionItem::default()
},
lsp::CompletionItem {
label: "vec![2]".into(),
insert_text: Some("vec![2]".into()),
insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
..lsp::CompletionItem::default()
},
],
item_defaults: Some(lsp::CompletionListItemDefaults {
data: Some(default_data.clone()),
commit_characters: Some(default_commit_characters.clone()),
@@ -10843,21 +10906,6 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
.next()
.await;
let resolved_items = Arc::new(Mutex::new(Vec::new()));
cx.lsp
.server
.on_request::<lsp::request::ResolveCompletionItem, _, _>({
let closure_resolved_items = resolved_items.clone();
move |item_to_resolve, _| {
let closure_resolved_items = closure_resolved_items.clone();
async move {
closure_resolved_items.lock().push(item_to_resolve.clone());
Ok(item_to_resolve)
}
}
})
.detach();
cx.condition(|editor, _| editor.context_menu_visible())
.await;
cx.run_until_parked();
@@ -10869,50 +10917,40 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
completions_menu
.matches
.iter()
.map(|c| c.string.clone())
.collect::<Vec<String>>(),
items_out
.iter()
.map(|completion| completion.label.clone())
.collect::<Vec<String>>()
.map(|c| c.string.as_str())
.collect::<Vec<_>>(),
vec!["Some(2)", "vec![2]"]
);
}
CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
}
});
// Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
// with 4 from the end.
assert_eq!(
*resolved_items.lock(),
[
&items_out[0..16],
&items_out[items_out.len() - 4..items_out.len()]
]
.concat()
.iter()
.cloned()
.collect::<Vec<lsp::CompletionItem>>()
resolve_requests_number.load(atomic::Ordering::Acquire),
1,
"While there are 2 items in the completion list, only 1 resolve request should have been sent, for the selected item"
);
resolved_items.lock().clear();
cx.update_editor(|editor, cx| {
editor.context_menu_prev(&ContextMenuPrev, cx);
editor.context_menu_first(&ContextMenuFirst, cx);
});
cx.run_until_parked();
// Completions that have already been resolved are skipped.
assert_eq!(
*resolved_items.lock(),
[
// Selected item is always resolved even if it was resolved before.
&items_out[items_out.len() - 1..items_out.len()],
&items_out[items_out.len() - 16..items_out.len() - 4]
]
.concat()
.iter()
.cloned()
.collect::<Vec<lsp::CompletionItem>>()
resolve_requests_number.load(atomic::Ordering::Acquire),
2,
"After re-selecting the first item, another resolve request should have been sent"
);
expect_first_item.store(false, atomic::Ordering::Release);
cx.update_editor(|editor, cx| {
editor.context_menu_last(&ContextMenuLast, cx);
});
cx.run_until_parked();
assert_eq!(
resolve_requests_number.load(atomic::Ordering::Acquire),
3,
"After selecting the other item, another resolve request should have been sent"
);
resolved_items.lock().clear();
}
#[gpui::test]

View File

@@ -726,7 +726,7 @@ impl Editor {
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.selected(
.toggle_state(
hunk_controls_menu_handle
.is_deployed(),
)

View File

@@ -210,7 +210,7 @@ impl PickerDelegate for ExtensionVersionSelectorDelegate {
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.toggle_state(selected)
.disabled(disabled)
.child(
HighlightedLabel::new(

View File

@@ -933,7 +933,7 @@ impl ExtensionsPage {
fn update_settings<T: Settings>(
&mut self,
selection: &Selection,
selection: &ToggleState,
cx: &mut ViewContext<Self>,
callback: impl 'static + Send + Fn(&mut T::FileContent, bool),
) {
@@ -942,8 +942,8 @@ impl ExtensionsPage {
let selection = *selection;
settings::update_settings_file::<T>(fs, cx, move |settings, _| {
let value = match selection {
Selection::Unselected => false,
Selection::Selected => true,
ToggleState::Unselected => false,
ToggleState::Selected => true,
_ => return,
};
@@ -998,9 +998,9 @@ impl ExtensionsPage {
"enable-vim",
Label::new("Enable vim mode"),
if VimModeSetting::get_global(cx).0 {
ui::Selection::Selected
ui::ToggleState::Selected
} else {
ui::Selection::Unselected
ui::ToggleState::Unselected
},
cx.listener(move |this, selection, cx| {
this.telemetry
@@ -1090,7 +1090,7 @@ impl Render for ExtensionsPage {
ToggleButton::new("filter-all", "All")
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.selected(self.filter == ExtensionFilter::All)
.toggle_state(self.filter == ExtensionFilter::All)
.on_click(cx.listener(|this, _event, cx| {
this.filter = ExtensionFilter::All;
this.filter_extension_entries(cx);
@@ -1104,7 +1104,7 @@ impl Render for ExtensionsPage {
ToggleButton::new("filter-installed", "Installed")
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.selected(self.filter == ExtensionFilter::Installed)
.toggle_state(self.filter == ExtensionFilter::Installed)
.on_click(cx.listener(|this, _event, cx| {
this.filter = ExtensionFilter::Installed;
this.filter_extension_entries(cx);
@@ -1118,7 +1118,9 @@ impl Render for ExtensionsPage {
ToggleButton::new("filter-not-installed", "Not Installed")
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.selected(self.filter == ExtensionFilter::NotInstalled)
.toggle_state(
self.filter == ExtensionFilter::NotInstalled,
)
.on_click(cx.listener(|this, _event, cx| {
this.filter = ExtensionFilter::NotInstalled;
this.filter_extension_entries(cx);

View File

@@ -1228,7 +1228,7 @@ impl PickerDelegate for FileFinderDelegate {
.start_slot::<Icon>(file_icon)
.end_slot::<AnyElement>(history_icon)
.inset(true)
.selected(selected)
.toggle_state(selected)
.child(
h_flex()
.gap_2()

View File

@@ -414,7 +414,7 @@ impl PickerDelegate for NewPathDelegate {
ListItem::new(ix)
.spacing(ListItemSpacing::Sparse)
.inset(true)
.selected(selected)
.toggle_state(selected)
.child(LabelLike::new().child(m.styled_text(self.project.read(cx), cx))),
)
}

View File

@@ -283,7 +283,7 @@ impl PickerDelegate for OpenPathDelegate {
ListItem::new(ix)
.spacing(ListItemSpacing::Sparse)
.inset(true)
.selected(selected)
.toggle_state(selected)
.child(LabelLike::new().child(candidate.string.clone())),
)
}

View File

@@ -15,3 +15,4 @@ doctest = false
[dependencies]
gpui.workspace = true
util.workspace = true
log.workspace = true

View File

@@ -61,9 +61,23 @@ impl StringMatch {
let mut positions = self.positions.iter().peekable();
iter::from_fn(move || {
if let Some(start) = positions.next().copied() {
if start >= self.string.len() {
log::error!(
"Invariant violation: Index {start} out of range in string {:?}",
self.string
);
return None;
}
let mut end = start + self.char_len_at_index(start);
while let Some(next_start) = positions.peek() {
if end == **next_start {
if end >= self.string.len() {
log::error!(
"Invariant violation: Index {end} out of range in string {:?}",
self.string
);
return None;
}
end += self.char_len_at_index(end);
positions.next();
} else {

View File

@@ -14,19 +14,22 @@ path = "src/git_ui.rs"
[dependencies]
anyhow.workspace = true
collections.workspace = true
db.workspace = true
editor.workspace = true
git.workspace = true
gpui.workspace = true
language.workspace = true
project.workspace = true
schemars.workspace = true
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
settings.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
git.workspace = true
collections.workspace = true
[target.'cfg(windows)'.dependencies]
windows.workspace = true

View File

@@ -1,4 +1,7 @@
use anyhow::Context;
use collections::HashMap;
use editor::Editor;
use language::Buffer;
use std::{
cell::OnceCell,
collections::HashSet,
@@ -8,6 +11,7 @@ use std::{
sync::Arc,
time::Duration,
};
use theme::ThemeSettings;
use git::repository::GitFileStatus;
@@ -24,7 +28,7 @@ use ui::{
use workspace::dock::{DockPosition, Panel, PanelEvent};
use workspace::Workspace;
use crate::{git_status_icon, settings::GitPanelSettings};
use crate::{git_status_icon, settings::GitPanelSettings, GitState};
use crate::{CommitAllChanges, CommitStagedChanges, DiscardAll, StageAll, UnstageAll};
actions!(git_panel, [ToggleFocus]);
@@ -84,6 +88,8 @@ pub struct GitPanel {
selected_item: Option<usize>,
show_scrollbar: bool,
expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
git_state: Model<GitState>,
editor: View<Editor>,
// The entries that are currently shown in the panel, aka
// not hidden by folding or such
@@ -104,11 +110,17 @@ impl GitPanel {
}
pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
let git_state = GitState::get_global(cx);
let fs = workspace.app_state().fs.clone();
let weak_workspace = workspace.weak_handle();
let project = workspace.project().clone();
let language_registry = workspace.app_state().languages.clone();
let git_panel = cx.new_view(|cx: &mut ViewContext<Self>| {
let state = git_state.read(cx);
let current_commit_message = state.commit_message.clone();
let focus_handle = cx.focus_handle();
cx.on_focus(&focus_handle, Self::focus_in).detach();
cx.on_focus_out(&focus_handle, |this, _, cx| {
@@ -131,6 +143,55 @@ impl GitPanel {
})
.detach();
let editor = cx.new_view(|cx| {
let theme = ThemeSettings::get_global(cx);
let mut text_style = cx.text_style();
let refinement = TextStyleRefinement {
font_family: Some(theme.buffer_font.family.clone()),
font_features: Some(FontFeatures::disable_ligatures()),
font_size: Some(px(12.).into()),
color: Some(cx.theme().colors().editor_foreground),
background_color: Some(gpui::transparent_black()),
..Default::default()
};
text_style.refine(&refinement);
let mut editor = Editor::auto_height(10, cx);
if let Some(message) = current_commit_message {
editor.set_text(message, cx);
} else {
editor.set_text("", cx);
}
// editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
editor.set_use_autoclose(false);
editor.set_show_gutter(false, cx);
editor.set_show_wrap_guides(false, cx);
editor.set_show_indent_guides(false, cx);
editor.set_text_style_refinement(refinement);
editor.set_placeholder_text("Enter commit message", cx);
editor
});
let buffer = editor
.read(cx)
.buffer()
.read(cx)
.as_singleton()
.expect("commit editor must be singleton");
cx.subscribe(&buffer, Self::on_buffer_event).detach();
let markdown = language_registry.language_for_name("Markdown");
cx.spawn(|_, mut cx| async move {
let markdown = markdown.await.context("failed to load Markdown language")?;
buffer.update(&mut cx, |buffer, cx| {
buffer.set_language(Some(markdown), cx)
})
})
.detach_and_log_err(cx);
let scroll_handle = UniformListScrollHandle::new();
let mut this = Self {
@@ -142,6 +203,8 @@ impl GitPanel {
visible_entries: Vec::new(),
current_modifiers: cx.modifiers(),
expanded_dir_ids: Default::default(),
git_state,
editor,
width: Some(px(360.)),
scrollbar_state: ScrollbarState::new(scroll_handle.clone()).parent_view(cx.view()),
@@ -188,12 +251,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 +318,44 @@ 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");
}
fn clear_message(&mut self, cx: &mut ViewContext<Self>) {
let git_state = self.git_state.clone();
git_state.update(cx, |state, _cx| state.clear_message());
self.editor.update(cx, |editor, cx| editor.set_text("", cx));
}
/// Commit all staged changes
fn commit_staged_changes(&mut self, _: &CommitStagedChanges, _cx: &mut ViewContext<Self>) {
// todo!(): Implement commit all staged
fn commit_staged_changes(&mut self, _: &CommitStagedChanges, cx: &mut ViewContext<Self>) {
self.clear_message(cx);
// 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
fn commit_all_changes(&mut self, _: &CommitAllChanges, cx: &mut ViewContext<Self>) {
self.clear_message(cx);
// 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 +451,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)>,
@@ -426,6 +499,23 @@ impl GitPanel {
cx.notify();
}
fn on_buffer_event(
&mut self,
_buffer: Model<Buffer>,
event: &language::BufferEvent,
cx: &mut ViewContext<Self>,
) {
if let language::BufferEvent::Reparsed | language::BufferEvent::Edited = event {
let commit_message = self.editor.update(cx, |editor, cx| editor.text(cx));
self.git_state.update(cx, |state, _cx| {
state.commit_message = Some(commit_message.into());
});
cx.notify();
}
}
}
impl GitPanel {
@@ -499,6 +589,13 @@ impl GitPanel {
}
pub fn render_commit_editor(&self, cx: &ViewContext<Self>) -> impl IntoElement {
let git_state = self.git_state.clone();
let commit_message = git_state.read(cx).commit_message.clone();
let editor = self.editor.clone();
let editor_focus_handle = editor.read(cx).focus_handle(cx).clone();
println!("{:?}", commit_message);
let focus_handle_1 = self.focus_handle(cx).clone();
let focus_handle_2 = self.focus_handle(cx).clone();
@@ -534,25 +631,26 @@ impl GitPanel {
div().w_full().h(px(140.)).px_2().pt_1().pb_2().child(
v_flex()
.id("commit-editor-container")
.relative()
.h_full()
.py_2p5()
.px_3()
.bg(cx.theme().colors().editor_background)
.font_buffer(cx)
.text_ui_sm(cx)
.text_color(cx.theme().colors().text_muted)
.child("Add a message")
.gap_1()
.child(div().flex_grow())
.child(h_flex().child(div().gap_1().flex_grow()).child(
if self.current_modifiers.alt {
commit_all_button
} else {
commit_staged_button
},
))
.cursor(CursorStyle::OperationNotAllowed)
.opacity(0.5),
.on_click(cx.listener(move |_, _: &ClickEvent, cx| cx.focus(&editor_focus_handle)))
.child(self.editor.clone())
.child(
h_flex()
.absolute()
.bottom_2p5()
.right_3()
.child(div().gap_1().flex_grow())
.child(if self.current_modifiers.alt {
commit_all_button
} else {
commit_staged_button
}),
),
)
}
@@ -658,7 +756,7 @@ impl GitPanel {
) -> impl IntoElement {
let id = id.to_proto() as usize;
let checkbox_id = ElementId::Name(format!("checkbox_{}", id).into());
let is_staged = Selection::Selected;
let is_staged = ToggleState::Selected;
h_flex()
.id(id)

View File

@@ -1,8 +1,8 @@
use ::settings::Settings;
use git::repository::GitFileStatus;
use gpui::{actions, AppContext, Hsla};
use gpui::{actions, prelude::*, AppContext, Global, Hsla, Model};
use settings::GitPanelSettings;
use ui::{Color, Icon, IconName, IntoElement};
use ui::{Color, Icon, IconName, IntoElement, SharedString};
pub mod git_panel;
mod settings;
@@ -14,14 +14,50 @@ actions!(
UnstageAll,
DiscardAll,
CommitStagedChanges,
CommitAllChanges
CommitAllChanges,
ClearMessage
]
);
pub fn init(cx: &mut AppContext) {
GitPanelSettings::register(cx);
let git_state = cx.new_model(|_cx| GitState::new());
cx.set_global(GlobalGitState(git_state));
}
struct GlobalGitState(Model<GitState>);
impl Global for GlobalGitState {}
pub struct GitState {
commit_message: Option<SharedString>,
}
impl GitState {
pub fn new() -> Self {
GitState {
commit_message: None,
}
}
pub fn set_message(&mut self, message: Option<SharedString>) {
self.commit_message = message;
}
pub fn clear_message(&mut self) {
self.commit_message = None;
}
pub fn get_global(cx: &mut AppContext) -> Model<GitState> {
cx.global::<GlobalGitState>().0.clone()
}
}
// impl EventEmitter<Event> for GitState {}
// #[derive(Clone, Debug, PartialEq, Eq)]
// pub enum Event {}
const ADDED_COLOR: Hsla = Hsla {
h: 142. / 360.,
s: 0.68,
@@ -41,7 +77,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

@@ -267,7 +267,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.toggle_state(selected)
.start_slot(
div().pr_0p5().child(
Icon::new(model_info.icon)

View File

@@ -281,7 +281,7 @@ impl PickerDelegate for LanguageSelectorDelegate {
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.toggle_state(selected)
.start_slot::<Icon>(language_icon)
.child(HighlightedLabel::new(label, mat.positions.clone())),
)

View File

@@ -14,7 +14,7 @@ use lsp::{
};
use project::{search::SearchQuery, Project, WorktreeId};
use std::{borrow::Cow, sync::Arc};
use ui::{prelude::*, Button, Checkbox, ContextMenu, Label, PopoverMenu, Selection};
use ui::{prelude::*, Button, Checkbox, ContextMenu, Label, PopoverMenu, ToggleState};
use workspace::{
item::{Item, ItemHandle},
searchable::{SearchEvent, SearchableItem, SearchableItemHandle},
@@ -1251,9 +1251,9 @@ impl Render for LspLogToolbarItemView {
Checkbox::new(
"LspLogEnableRpcTrace",
if rpc_trace_enabled {
Selection::Selected
ToggleState::Selected
} else {
Selection::Unselected
ToggleState::Unselected
},
)
.on_click(cx.listener_for(
@@ -1261,7 +1261,7 @@ impl Render for LspLogToolbarItemView {
move |view, selection, cx| {
let enabled = matches!(
selection,
Selection::Selected
ToggleState::Selected
);
view.toggle_rpc_logging_for_server(
server_id, enabled, cx,

View File

@@ -315,7 +315,10 @@ impl ContextProvider for PythonContextProvider {
toolchains
.active_toolchain(worktree_id, "Python".into(), &mut cx)
.await
.map_or_else(|| "python3".to_owned(), |toolchain| toolchain.path.into())
.map_or_else(
|| "python3".to_owned(),
|toolchain| format!("\"{}\"", toolchain.path),
)
} else {
String::from("python3")
};
@@ -336,14 +339,17 @@ impl ContextProvider for PythonContextProvider {
TaskTemplate {
label: "execute selection".to_owned(),
command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
args: vec!["-c".to_owned(), VariableName::SelectedText.template_value()],
args: vec![
"-c".to_owned(),
VariableName::SelectedText.template_value_with_whitespace(),
],
..TaskTemplate::default()
},
// Execute an entire file
TaskTemplate {
label: format!("run '{}'", VariableName::File.template_value()),
command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
args: vec![VariableName::File.template_value()],
args: vec![VariableName::File.template_value_with_whitespace()],
..TaskTemplate::default()
},
];
@@ -358,7 +364,7 @@ impl ContextProvider for PythonContextProvider {
args: vec![
"-m".to_owned(),
"unittest".to_owned(),
VariableName::File.template_value(),
VariableName::File.template_value_with_whitespace(),
],
..TaskTemplate::default()
},
@@ -369,7 +375,7 @@ impl ContextProvider for PythonContextProvider {
args: vec![
"-m".to_owned(),
"unittest".to_owned(),
"$ZED_CUSTOM_PYTHON_TEST_TARGET".to_owned(),
PYTHON_TEST_TARGET_TASK_VARIABLE.template_value_with_whitespace()
],
tags: vec![
"python-unittest-class".to_owned(),
@@ -388,7 +394,7 @@ impl ContextProvider for PythonContextProvider {
args: vec![
"-m".to_owned(),
"pytest".to_owned(),
VariableName::File.template_value(),
VariableName::File.template_value_with_whitespace(),
],
..TaskTemplate::default()
},
@@ -399,7 +405,7 @@ impl ContextProvider for PythonContextProvider {
args: vec![
"-m".to_owned(),
"pytest".to_owned(),
"$ZED_CUSTOM_PYTHON_TEST_TARGET".to_owned(),
PYTHON_TEST_TARGET_TASK_VARIABLE.template_value_with_whitespace(),
],
tags: vec![
"python-pytest-class".to_owned(),

View File

@@ -20,7 +20,7 @@ use theme::{ActiveTheme, SyntaxTheme, ThemeSettings};
use ui::{
h_flex, relative, tooltip_container, v_flex, Checkbox, Clickable, Color, FluentBuilder,
IconButton, IconName, IconSize, InteractiveElement, Label, LabelCommon, LabelSize, LinkPreview,
Selection, StatefulInteractiveElement, StyledExt, StyledImage, ViewContext, VisibleOnHover,
StatefulInteractiveElement, StyledExt, StyledImage, ToggleState, ViewContext, VisibleOnHover,
VisualContext as _,
};
use workspace::Workspace;
@@ -180,9 +180,9 @@ fn render_markdown_list_item(
Checkbox::new(
"checkbox",
if *checked {
Selection::Selected
ToggleState::Selected
} else {
Selection::Unselected
ToggleState::Unselected
},
)
.when_some(
@@ -192,8 +192,8 @@ fn render_markdown_list_item(
let range = range.clone();
move |selection, cx| {
let checked = match selection {
Selection::Selected => true,
Selection::Unselected => false,
ToggleState::Selected => true,
ToggleState::Unselected => false,
_ => return,
};

View File

@@ -280,7 +280,7 @@ impl PickerDelegate for OutlineViewDelegate {
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.toggle_state(selected)
.child(
div()
.text_ui(cx)

View File

@@ -51,7 +51,7 @@ use workspace::{
ui::{
h_flex, v_flex, ActiveTheme, ButtonCommon, Clickable, Color, ContextMenu, FluentBuilder,
HighlightedLabel, Icon, IconButton, IconButtonShape, IconName, IconSize, Label,
LabelCommon, ListItem, Scrollbar, ScrollbarState, Selectable, StyledExt, StyledTypography,
LabelCommon, ListItem, Scrollbar, ScrollbarState, StyledExt, StyledTypography, Toggleable,
Tooltip,
},
OpenInTerminal, WeakItemHandle, Workspace,
@@ -2045,7 +2045,7 @@ impl OutlinePanel {
ListItem::new(item_id)
.indent_level(depth)
.indent_step_size(px(settings.indent_size))
.selected(is_active)
.toggle_state(is_active)
.when_some(icon_element, |list_item, icon_element| {
list_item.child(h_flex().child(icon_element))
})

View File

@@ -11,7 +11,7 @@ use std::{borrow::Cow, cmp::Reverse, sync::Arc};
use theme::ActiveTheme;
use util::ResultExt;
use workspace::{
ui::{v_flex, Color, Label, LabelCommon, LabelLike, ListItem, ListItemSpacing, Selectable},
ui::{v_flex, Color, Label, LabelCommon, LabelLike, ListItem, ListItemSpacing, Toggleable},
Workspace,
};
@@ -240,7 +240,7 @@ impl PickerDelegate for ProjectSymbolsDelegate {
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.toggle_state(selected)
.child(
v_flex()
.child(

View File

@@ -395,7 +395,7 @@ impl PickerDelegate for RecentProjectsDelegate {
Some(
ListItem::new(ix)
.selected(selected)
.toggle_state(selected)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.child(

View File

@@ -653,7 +653,7 @@ impl RemoteServerProjects {
}))
.child(
ListItem::new(("new-remote-project", ix))
.selected(
.toggle_state(
ssh_server.open_folder.focus_handle.contains_focused(cx),
)
.inset(true)
@@ -688,7 +688,7 @@ impl RemoteServerProjects {
}))
.child(
ListItem::new(("server-options", ix))
.selected(
.toggle_state(
ssh_server.configure.focus_handle.contains_focused(cx),
)
.inset(true)
@@ -772,7 +772,7 @@ impl RemoteServerProjects {
}))
.child(
ListItem::new((element_id_base, ix))
.selected(navigation.focus_handle.contains_focused(cx))
.toggle_state(navigation.focus_handle.contains_focused(cx))
.inset(true)
.spacing(ui::ListItemSpacing::Sparse)
.start_slot(
@@ -984,7 +984,7 @@ impl RemoteServerProjects {
}))
.child(
ListItem::new("add-nickname")
.selected(entries[0].focus_handle.contains_focused(cx))
.toggle_state(entries[0].focus_handle.contains_focused(cx))
.inset(true)
.spacing(ui::ListItemSpacing::Sparse)
.start_slot(Icon::new(IconName::Pencil).color(Color::Muted))
@@ -1043,7 +1043,7 @@ impl RemoteServerProjects {
})
.child(
ListItem::new("copy-server-address")
.selected(entries[1].focus_handle.contains_focused(cx))
.toggle_state(entries[1].focus_handle.contains_focused(cx))
.inset(true)
.spacing(ui::ListItemSpacing::Sparse)
.start_slot(Icon::new(IconName::Copy).color(Color::Muted))
@@ -1116,7 +1116,7 @@ impl RemoteServerProjects {
}))
.child(
ListItem::new("remove-server")
.selected(entries[2].focus_handle.contains_focused(cx))
.toggle_state(entries[2].focus_handle.contains_focused(cx))
.inset(true)
.spacing(ui::ListItemSpacing::Sparse)
.start_slot(Icon::new(IconName::Trash).color(Color::Error))
@@ -1144,7 +1144,7 @@ impl RemoteServerProjects {
}))
.child(
ListItem::new("go-back")
.selected(entries[3].focus_handle.contains_focused(cx))
.toggle_state(entries[3].focus_handle.contains_focused(cx))
.inset(true)
.spacing(ui::ListItemSpacing::Sparse)
.start_slot(
@@ -1233,7 +1233,7 @@ impl RemoteServerProjects {
.anchor_scroll(state.add_new_server.scroll_anchor.clone())
.child(
ListItem::new("register-remove-server-button")
.selected(state.add_new_server.focus_handle.contains_focused(cx))
.toggle_state(state.add_new_server.focus_handle.contains_focused(cx))
.inset(true)
.spacing(ui::ListItemSpacing::Sparse)
.start_slot(Icon::new(IconName::Plus).color(Color::Muted))

View File

@@ -150,7 +150,7 @@ impl PickerDelegate for KernelPickerDelegate {
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.toggle_state(selected)
.child(
h_flex()
.w_full()

View File

@@ -272,7 +272,7 @@ impl Render for BufferSearchBar {
.on_click(cx.listener(|this, _: &ClickEvent, cx| {
this.toggle_replace(&ToggleReplace, cx);
}))
.selected(self.replace_enabled)
.toggle_state(self.replace_enabled)
.tooltip({
let focus_handle = focus_handle.clone();
move |cx| {
@@ -300,7 +300,7 @@ impl Render for BufferSearchBar {
.on_click(cx.listener(|this, _: &ClickEvent, cx| {
this.toggle_selection(&ToggleSelection, cx);
}))
.selected(self.selection_search_enabled)
.toggle_state(self.selection_search_enabled)
.tooltip({
let focus_handle = focus_handle.clone();
move |cx| {

View File

@@ -35,7 +35,7 @@ use std::{
use theme::ThemeSettings;
use ui::{
h_flex, prelude::*, utils::SearchInputWidth, v_flex, Icon, IconButton, IconButtonShape,
IconName, KeyBinding, Label, LabelCommon, LabelSize, Selectable, Tooltip,
IconName, KeyBinding, Label, LabelCommon, LabelSize, Toggleable, Tooltip,
};
use util::paths::PathMatcher;
use workspace::{
@@ -1645,7 +1645,7 @@ impl Render for ProjectSearchBar {
.on_click(cx.listener(|this, _, cx| {
this.toggle_filters(cx);
}))
.selected(
.toggle_state(
self.active_project_search
.as_ref()
.map(|search| search.read(cx).filters_enabled)
@@ -1669,7 +1669,7 @@ impl Render for ProjectSearchBar {
.on_click(cx.listener(|this, _, cx| {
this.toggle_replace(&ToggleReplace, cx);
}))
.selected(
.toggle_state(
self.active_project_search
.as_ref()
.map(|search| search.read(cx).replace_enabled)
@@ -1878,7 +1878,7 @@ impl Render for ProjectSearchBar {
.child(
IconButton::new("project-search-opened-only", IconName::FileSearch)
.shape(IconButtonShape::Square)
.selected(self.is_opened_only_enabled(cx))
.toggle_state(self.is_opened_only_enabled(cx))
.tooltip(|cx| Tooltip::text("Only Search Open Files", cx))
.on_click(cx.listener(|this, _, cx| {
this.toggle_opened_only(cx);

View File

@@ -113,7 +113,7 @@ impl SearchOptions {
.on_click(action)
.style(ButtonStyle::Subtle)
.shape(IconButtonShape::Square)
.selected(active)
.toggle_state(active)
.tooltip({
let action = self.to_toggle_action();
let label = self.label();

View File

@@ -145,7 +145,7 @@ impl RenderOnce for ThemeModeControl {
ToggleButton::new("light", "Light")
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.selected(value == ThemeMode::Light)
.toggle_state(value == ThemeMode::Light)
.on_click(|_, cx| Self::write(ThemeMode::Light, cx))
.first(),
)
@@ -153,7 +153,7 @@ impl RenderOnce for ThemeModeControl {
ToggleButton::new("system", "System")
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.selected(value == ThemeMode::System)
.toggle_state(value == ThemeMode::System)
.on_click(|_, cx| Self::write(ThemeMode::System, cx))
.middle(),
)
@@ -161,7 +161,7 @@ impl RenderOnce for ThemeModeControl {
ToggleButton::new("dark", "Dark")
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.selected(value == ThemeMode::Dark)
.toggle_state(value == ThemeMode::Dark)
.on_click(|_, cx| Self::write(ThemeMode::Dark, cx))
.last(),
)
@@ -375,8 +375,8 @@ impl RenderOnce for UiFontLigaturesControl {
|selection, cx| {
Self::write(
match selection {
Selection::Selected => true,
Selection::Unselected | Selection::Indeterminate => false,
ToggleState::Selected => true,
ToggleState::Unselected | ToggleState::Indeterminate => false,
},
cx,
);

View File

@@ -219,7 +219,7 @@ impl PickerDelegate for ScopeSelectorDelegate {
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.toggle_state(selected)
.child(HighlightedLabel::new(label, mat.positions.clone())),
)
}

View File

@@ -59,7 +59,7 @@ impl PickerDelegate for Delegate {
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.toggle_state(selected)
.child(Label::new(candidate)),
)
}

View File

@@ -407,7 +407,7 @@ impl PickerDelegate for TabSwitcherDelegate {
ListItem::new(ix)
.spacing(ListItemSpacing::Sparse)
.inset(true)
.selected(selected)
.toggle_state(selected)
.child(h_flex().w_full().child(label))
.start_slot::<Icon>(icon)
.map(|el| {

View File

@@ -135,6 +135,10 @@ impl VariableName {
pub fn template_value(&self) -> String {
format!("${self}")
}
/// Generates a `"$VARIABLE"`-like string, to be used instead of `Self::template_value` when expanded value could contain spaces or special characters.
pub fn template_value_with_whitespace(&self) -> String {
format!("\"${self}\"")
}
}
impl FromStr for VariableName {

View File

@@ -13,7 +13,7 @@ use task::{ResolvedTask, TaskContext, TaskTemplate};
use ui::{
div, h_flex, v_flex, ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, Color,
FluentBuilder as _, Icon, IconButton, IconButtonShape, IconName, IconSize, IntoElement,
KeyBinding, LabelSize, ListItem, ListItemSpacing, RenderOnce, Selectable, Tooltip,
KeyBinding, LabelSize, ListItem, ListItemSpacing, RenderOnce, Toggleable, Tooltip,
WindowContext,
};
use util::ResultExt;
@@ -379,7 +379,7 @@ impl PickerDelegate for TasksModalDelegate {
};
item
})
.selected(selected)
.toggle_state(selected)
.child(highlighted_location.render(cx)),
)
}

View File

@@ -26,7 +26,7 @@ use terminal::{
Terminal,
};
use ui::{
prelude::*, ButtonCommon, Clickable, ContextMenu, FluentBuilder, PopoverMenu, Selectable,
prelude::*, ButtonCommon, Clickable, ContextMenu, FluentBuilder, PopoverMenu, Toggleable,
Tooltip,
};
use util::{ResultExt, TryFutureExt};
@@ -201,7 +201,7 @@ impl TerminalPanel {
let zoomed = pane.is_zoomed();
IconButton::new("toggle_zoom", IconName::Maximize)
.icon_size(IconSize::Small)
.selected(zoomed)
.toggle_state(zoomed)
.selected_icon(IconName::Minimize)
.on_click(cx.listener(|pane, _, cx| {
pane.toggle_zoom(&workspace::ToggleZoom, cx);

View File

@@ -285,7 +285,7 @@ impl PickerDelegate for ThemeSelectorDelegate {
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.toggle_state(selected)
.child(HighlightedLabel::new(
theme_match.string.clone(),
theme_match.positions.clone(),

View File

@@ -322,7 +322,7 @@ impl TitleBar {
})
.style(ButtonStyle::Subtle)
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.selected(is_shared)
.toggle_state(is_shared)
.label_size(LabelSize::Small)
.on_click(cx.listener(move |this, _, cx| {
if is_shared {
@@ -380,7 +380,7 @@ impl TitleBar {
})
.style(ButtonStyle::Subtle)
.icon_size(IconSize::Small)
.selected(is_muted)
.toggle_state(is_muted)
.selected_style(ButtonStyle::Tinted(TintColor::Negative))
.on_click(move |_, cx| {
toggle_mute(&Default::default(), cx);
@@ -400,7 +400,7 @@ impl TitleBar {
.style(ButtonStyle::Subtle)
.selected_style(ButtonStyle::Tinted(TintColor::Negative))
.icon_size(IconSize::Small)
.selected(is_deafened)
.toggle_state(is_deafened)
.tooltip(move |cx| {
if is_deafened {
let label = "Unmute Audio";
@@ -430,7 +430,7 @@ impl TitleBar {
IconButton::new("screen-share", ui::IconName::Screen)
.style(ButtonStyle::Subtle)
.icon_size(IconSize::Small)
.selected(is_screen_sharing)
.toggle_state(is_screen_sharing)
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.tooltip(move |cx| {
Tooltip::text(

View File

@@ -345,7 +345,7 @@ impl PickerDelegate for ToolchainSelectorDelegate {
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.toggle_state(selected)
.child(HighlightedLabel::new(label, name_highlights))
.child(
HighlightedLabel::new(path, path_highlights)

View File

@@ -1,6 +1,5 @@
mod avatar;
mod button;
mod checkbox;
mod content_group;
mod context_menu;
mod disclosure;
@@ -28,6 +27,7 @@ mod stack;
mod tab;
mod tab_bar;
mod table;
mod toggle;
mod tool_strip;
mod tooltip;
@@ -36,7 +36,6 @@ mod stories;
pub use avatar::*;
pub use button::*;
pub use checkbox::*;
pub use content_group::*;
pub use context_menu::*;
pub use disclosure::*;
@@ -64,6 +63,7 @@ pub use stack::*;
pub use tab::*;
pub use tab_bar::*;
pub use table::*;
pub use toggle::*;
pub use tool_strip::*;
pub use tooltip::*;

View File

@@ -194,7 +194,7 @@ impl Button {
}
}
impl Selectable for Button {
impl Toggleable for Button {
/// Sets the selected state of the button.
///
/// This method allows the selection state of the button to be specified.
@@ -213,8 +213,8 @@ impl Selectable for Button {
/// ```
///
/// Use [`selected_style`](Button::selected_style) to change the style of the button when it is selected.
fn selected(mut self, selected: bool) -> Self {
self.base = self.base.selected(selected);
fn toggle_state(mut self, selected: bool) -> Self {
self.base = self.base.toggle_state(selected);
self
}
}
@@ -405,7 +405,7 @@ impl RenderOnce for Button {
this.children(self.icon.map(|icon| {
ButtonIcon::new(icon)
.disabled(is_disabled)
.selected(is_selected)
.toggle_state(is_selected)
.selected_icon(self.selected_icon)
.selected_icon_color(self.selected_icon_color)
.size(self.icon_size)
@@ -429,7 +429,7 @@ impl RenderOnce for Button {
this.children(self.icon.map(|icon| {
ButtonIcon::new(icon)
.disabled(is_disabled)
.selected(is_selected)
.toggle_state(is_selected)
.selected_icon(self.selected_icon)
.selected_icon_color(self.selected_icon_color)
.size(self.icon_size)
@@ -500,7 +500,7 @@ impl ComponentPreview for Button {
),
single_example(
"Selected",
Button::new("selected", "Selected").selected(true),
Button::new("selected", "Selected").toggle_state(true),
),
],
),

View File

@@ -65,8 +65,8 @@ impl Disableable for ButtonIcon {
}
}
impl Selectable for ButtonIcon {
fn selected(mut self, selected: bool) -> Self {
impl Toggleable for ButtonIcon {
fn toggle_state(mut self, selected: bool) -> Self {
self.selected = selected;
self
}

View File

@@ -6,7 +6,7 @@ use smallvec::SmallVec;
use crate::{prelude::*, DynamicSpacing, ElevationIndex};
/// A trait for buttons that can be Selected. Enables setting the [`ButtonStyle`] of a button when it is selected.
pub trait SelectableButton: Selectable {
pub trait SelectableButton: Toggleable {
fn selected_style(self, style: ButtonStyle) -> Self;
}
@@ -400,8 +400,8 @@ impl Disableable for ButtonLike {
}
}
impl Selectable for ButtonLike {
fn selected(mut self, selected: bool) -> Self {
impl Toggleable for ButtonLike {
fn toggle_state(mut self, selected: bool) -> Self {
self.selected = selected;
self
}

View File

@@ -66,9 +66,9 @@ impl Disableable for IconButton {
}
}
impl Selectable for IconButton {
fn selected(mut self, selected: bool) -> Self {
self.base = self.base.selected(selected);
impl Toggleable for IconButton {
fn toggle_state(mut self, selected: bool) -> Self {
self.base = self.base.toggle_state(selected);
self
}
}
@@ -157,7 +157,7 @@ impl RenderOnce for IconButton {
.child(
ButtonIcon::new(self.icon)
.disabled(is_disabled)
.selected(is_selected)
.toggle_state(is_selected)
.selected_icon(self.selected_icon)
.when_some(selected_style, |this, style| this.selected_style(style))
.size(self.icon_size)

View File

@@ -57,9 +57,9 @@ impl ToggleButton {
}
}
impl Selectable for ToggleButton {
fn selected(mut self, selected: bool) -> Self {
self.base = self.base.selected(selected);
impl Toggleable for ToggleButton {
fn toggle_state(mut self, selected: bool) -> Self {
self.base = self.base.toggle_state(selected);
self
}
}

View File

@@ -1,248 +0,0 @@
#![allow(missing_docs)]
use gpui::{div, prelude::*, ElementId, IntoElement, Styled, WindowContext};
use crate::prelude::*;
use crate::{Color, Icon, IconName, Selection};
/// # Checkbox
///
/// Checkboxes are used for multiple choices, not for mutually exclusive choices.
/// Each checkbox works independently from other checkboxes in the list,
/// therefore checking an additional box does not affect any other selections.
#[derive(IntoElement)]
pub struct Checkbox {
id: ElementId,
checked: Selection,
disabled: bool,
on_click: Option<Box<dyn Fn(&Selection, &mut WindowContext) + 'static>>,
}
impl Checkbox {
pub fn new(id: impl Into<ElementId>, checked: Selection) -> Self {
Self {
id: id.into(),
checked,
disabled: false,
on_click: None,
}
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn on_click(mut self, handler: impl Fn(&Selection, &mut WindowContext) + 'static) -> Self {
self.on_click = Some(Box::new(handler));
self
}
}
impl RenderOnce for Checkbox {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let group_id = format!("checkbox_group_{:?}", self.id);
let icon = match self.checked {
Selection::Selected => Some(Icon::new(IconName::Check).size(IconSize::Small).color(
if self.disabled {
Color::Disabled
} else {
Color::Selected
},
)),
Selection::Indeterminate => Some(
Icon::new(IconName::Dash)
.size(IconSize::Small)
.color(if self.disabled {
Color::Disabled
} else {
Color::Selected
}),
),
Selection::Unselected => None,
};
let selected =
self.checked == Selection::Selected || self.checked == Selection::Indeterminate;
let (bg_color, border_color) = match (self.disabled, selected) {
(true, _) => (
cx.theme().colors().ghost_element_disabled,
cx.theme().colors().border_disabled,
),
(false, true) => (
cx.theme().colors().element_selected,
cx.theme().colors().border,
),
(false, false) => (
cx.theme().colors().element_background,
cx.theme().colors().border,
),
};
h_flex()
.id(self.id)
.justify_center()
.items_center()
.size(DynamicSpacing::Base20.rems(cx))
.group(group_id.clone())
.child(
div()
.flex()
.flex_none()
.justify_center()
.items_center()
.m(DynamicSpacing::Base04.px(cx))
.size(DynamicSpacing::Base16.rems(cx))
.rounded_sm()
.bg(bg_color)
.border_1()
.border_color(border_color)
.when(!self.disabled, |this| {
this.group_hover(group_id.clone(), |el| {
el.bg(cx.theme().colors().element_hover)
})
})
.children(icon),
)
.when_some(
self.on_click.filter(|_| !self.disabled),
|this, on_click| this.on_click(move |_, cx| on_click(&self.checked.inverse(), cx)),
)
}
}
impl ComponentPreview for Checkbox {
fn description() -> impl Into<Option<&'static str>> {
"A checkbox lets people choose between a pair of opposing states, like enabled and disabled, using a different appearance to indicate each state."
}
fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
vec![
example_group_with_title(
"Default",
vec![
single_example(
"Unselected",
Checkbox::new("checkbox_unselected", Selection::Unselected),
),
single_example(
"Indeterminate",
Checkbox::new("checkbox_indeterminate", Selection::Indeterminate),
),
single_example(
"Selected",
Checkbox::new("checkbox_selected", Selection::Selected),
),
],
),
example_group_with_title(
"Disabled",
vec![
single_example(
"Unselected",
Checkbox::new("checkbox_disabled_unselected", Selection::Unselected)
.disabled(true),
),
single_example(
"Indeterminate",
Checkbox::new("checkbox_disabled_indeterminate", Selection::Indeterminate)
.disabled(true),
),
single_example(
"Selected",
Checkbox::new("checkbox_disabled_selected", Selection::Selected)
.disabled(true),
),
],
),
]
}
}
use std::sync::Arc;
/// A [`Checkbox`] that has a [`Label`].
#[derive(IntoElement)]
pub struct CheckboxWithLabel {
id: ElementId,
label: Label,
checked: Selection,
on_click: Arc<dyn Fn(&Selection, &mut WindowContext) + 'static>,
}
impl CheckboxWithLabel {
pub fn new(
id: impl Into<ElementId>,
label: Label,
checked: Selection,
on_click: impl Fn(&Selection, &mut WindowContext) + 'static,
) -> Self {
Self {
id: id.into(),
label,
checked,
on_click: Arc::new(on_click),
}
}
}
impl RenderOnce for CheckboxWithLabel {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
h_flex()
.gap(DynamicSpacing::Base08.rems(cx))
.child(Checkbox::new(self.id.clone(), self.checked).on_click({
let on_click = self.on_click.clone();
move |checked, cx| {
(on_click)(checked, cx);
}
}))
.child(
div()
.id(SharedString::from(format!("{}-label", self.id)))
.on_click(move |_event, cx| {
(self.on_click)(&self.checked.inverse(), cx);
})
.child(self.label),
)
}
}
impl ComponentPreview for CheckboxWithLabel {
fn description() -> impl Into<Option<&'static str>> {
"A checkbox with an associated label, allowing users to select an option while providing a descriptive text."
}
fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
vec![example_group(vec![
single_example(
"Unselected",
CheckboxWithLabel::new(
"checkbox_with_label_unselected",
Label::new("Always save on quit"),
Selection::Unselected,
|_, _| {},
),
),
single_example(
"Indeterminate",
CheckboxWithLabel::new(
"checkbox_with_label_indeterminate",
Label::new("Always save on quit"),
Selection::Indeterminate,
|_, _| {},
),
),
single_example(
"Selected",
CheckboxWithLabel::new(
"checkbox_with_label_selected",
Label::new("Always save on quit"),
Selection::Selected,
|_, _| {},
),
),
])]
}
}

View File

@@ -434,7 +434,7 @@ impl Render for ContextMenu {
ListItem::new(ix)
.inset(true)
.disabled(*disabled)
.selected(Some(ix) == self.selected_index)
.toggle_state(Some(ix) == self.selected_index)
.when_some(*toggle, |list_item, (position, toggled)| {
let contents = if toggled {
v_flex().flex_none().child(
@@ -495,7 +495,7 @@ impl Render for ContextMenu {
let selectable = *selectable;
ListItem::new(ix)
.inset(true)
.selected(if selectable {
.toggle_state(if selectable {
Some(ix) == self.selected_index
} else {
false

View File

@@ -34,8 +34,8 @@ impl Disclosure {
}
}
impl Selectable for Disclosure {
fn selected(mut self, selected: bool) -> Self {
impl Toggleable for Disclosure {
fn toggle_state(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
@@ -65,7 +65,7 @@ impl RenderOnce for Disclosure {
.shape(IconButtonShape::Square)
.icon_color(Color::Muted)
.icon_size(IconSize::Small)
.selected(self.selected)
.toggle_state(self.selected)
.when_some(self.on_toggle, move |this, on_toggle| {
this.on_click(move |event, cx| on_toggle(event, cx))
})

View File

@@ -85,8 +85,8 @@ impl Disableable for DropdownMenuTrigger {
}
}
impl Selectable for DropdownMenuTrigger {
fn selected(mut self, selected: bool) -> Self {
impl Toggleable for DropdownMenuTrigger {
fn toggle_state(mut self, selected: bool) -> Self {
self.selected = selected;
self
}

View File

@@ -73,8 +73,8 @@ impl ListHeader {
}
}
impl Selectable for ListHeader {
fn selected(mut self, selected: bool) -> Self {
impl Toggleable for ListHeader {
fn toggle_state(mut self, selected: bool) -> Self {
self.selected = selected;
self
}

View File

@@ -156,8 +156,8 @@ impl Disableable for ListItem {
}
}
impl Selectable for ListItem {
fn selected(mut self, selected: bool) -> Self {
impl Toggleable for ListItem {
fn toggle_state(mut self, selected: bool) -> Self {
self.selected = selected;
self
}

View File

@@ -32,8 +32,8 @@ impl ListSubHeader {
}
}
impl Selectable for ListSubHeader {
fn selected(mut self, selected: bool) -> Self {
impl Toggleable for ListSubHeader {
fn toggle_state(mut self, selected: bool) -> Self {
self.selected = selected;
self
}

View File

@@ -11,9 +11,9 @@ use gpui::{
use crate::prelude::*;
pub trait PopoverTrigger: IntoElement + Clickable + Selectable + 'static {}
pub trait PopoverTrigger: IntoElement + Clickable + Toggleable + 'static {}
impl<T: IntoElement + Clickable + Selectable + 'static> PopoverTrigger for T {}
impl<T: IntoElement + Clickable + Toggleable + 'static> PopoverTrigger for T {}
pub struct PopoverMenuHandle<M>(Rc<RefCell<Option<PopoverMenuHandleState<M>>>>);
@@ -129,7 +129,7 @@ impl<M: ManagedView> PopoverMenu<M> {
pub fn trigger<T: PopoverTrigger>(mut self, t: T) -> Self {
self.child_builder = Some(Box::new(|menu, builder| {
let open = menu.borrow().is_some();
t.selected(open)
t.toggle_state(open)
.when_some(builder, |el, builder| {
el.on_click(move |_, cx| show_menu(&builder, &menu, cx))
})

View File

@@ -13,11 +13,11 @@ impl Render for ButtonStory {
.child(Story::label("Default"))
.child(Button::new("default_filled", "Click me"))
.child(Story::label("Selected"))
.child(Button::new("selected_filled", "Click me").selected(true))
.child(Button::new("selected_filled", "Click me").toggle_state(true))
.child(Story::label("Selected with `selected_label`"))
.child(
Button::new("selected_label_filled", "Click me")
.selected(true)
.toggle_state(true)
.selected_label("I have been selected"),
)
.child(Story::label("With `label_color`"))
@@ -27,7 +27,7 @@ impl Render for ButtonStory {
.child(Story::label("Selected with `icon`"))
.child(
Button::new("filled_and_selected_with_icon", "Click me")
.selected(true)
.toggle_state(true)
.icon(IconName::FileGit),
)
.child(Story::label("Default (Subtle)"))

View File

@@ -21,7 +21,7 @@ impl Render for IconButtonStory {
let selected_button = StoryItem::new(
"Selected",
IconButton::new("selected_icon_button", IconName::Hash).selected(true),
IconButton::new("selected_icon_button", IconName::Hash).toggle_state(true),
)
.description("Displays an icon button that is selected.")
.usage(
@@ -33,7 +33,7 @@ impl Render for IconButtonStory {
let selected_with_selected_icon = StoryItem::new(
"Selected with `selected_icon`",
IconButton::new("selected_with_selected_icon_button", IconName::AudioOn)
.selected(true)
.toggle_state(true)
.selected_icon(IconName::AudioOff),
)
.description(
@@ -89,7 +89,7 @@ impl Render for IconButtonStory {
let selected_with_tooltip_button = StoryItem::new(
"Selected with `tooltip`",
IconButton::new("selected_with_tooltip_button", IconName::InlayHint)
.selected(true)
.toggle_state(true)
.tooltip(|cx| Tooltip::text("Toggle inlay hints", cx)),
)
.description("Displays a selected icon button with tooltip.")

View File

@@ -48,7 +48,7 @@ impl Render for TabStory {
h_flex()
.child(
Tab::new("tab_1")
.selected(true)
.toggle_state(true)
.position(TabPosition::First)
.child("Tab 1"),
)
@@ -85,7 +85,7 @@ impl Render for TabStory {
.child(
Tab::new("tab_4")
.position(TabPosition::Last)
.selected(true)
.toggle_state(true)
.child("Tab 4"),
),
)
@@ -100,7 +100,7 @@ impl Render for TabStory {
.child(
Tab::new("tab_2")
.position(TabPosition::Middle(Ordering::Equal))
.selected(true)
.toggle_state(true)
.child("Tab 2"),
)
.child(

View File

@@ -13,7 +13,7 @@ impl Render for TabBarStory {
let tabs = (0..tab_count)
.map(|index| {
Tab::new(index)
.selected(index == selected_tab_index)
.toggle_state(index == selected_tab_index)
.position(if index == 0 {
TabPosition::First
} else if index == tab_count - 1 {

View File

@@ -68,7 +68,7 @@ impl Render for ToggleButtonStory {
ToggleButton::new(2, "Banana")
.style(ButtonStyle::Filled)
.size(ButtonSize::Large)
.selected(true)
.toggle_state(true)
.middle(),
)
.child(

View File

@@ -91,8 +91,8 @@ impl InteractiveElement for Tab {
impl StatefulInteractiveElement for Tab {}
impl Selectable for Tab {
fn selected(mut self, selected: bool) -> Self {
impl Toggleable for Tab {
fn toggle_state(mut self, selected: bool) -> Self {
self.selected = selected;
self
}

View File

@@ -0,0 +1,409 @@
#![allow(missing_docs)]
use gpui::{div, prelude::*, ElementId, IntoElement, Styled, WindowContext};
use std::sync::Arc;
use crate::prelude::*;
use crate::utils::is_light;
use crate::{Color, Icon, IconName, ToggleState};
/// Creates a new checkbox
pub fn checkbox(id: impl Into<ElementId>, toggle_state: ToggleState) -> Checkbox {
Checkbox::new(id, toggle_state)
}
/// Creates a new switch
pub fn switch(id: impl Into<ElementId>, toggle_state: ToggleState) -> Switch {
Switch::new(id, toggle_state)
}
/// # Checkbox
///
/// Checkboxes are used for multiple choices, not for mutually exclusive choices.
/// Each checkbox works independently from other checkboxes in the list,
/// therefore checking an additional box does not affect any other selections.
#[derive(IntoElement)]
pub struct Checkbox {
id: ElementId,
toggle_state: ToggleState,
disabled: bool,
on_click: Option<Box<dyn Fn(&ToggleState, &mut WindowContext) + 'static>>,
}
impl Checkbox {
pub fn new(id: impl Into<ElementId>, checked: ToggleState) -> Self {
Self {
id: id.into(),
toggle_state: checked,
disabled: false,
on_click: None,
}
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn on_click(
mut self,
handler: impl Fn(&ToggleState, &mut WindowContext) + 'static,
) -> Self {
self.on_click = Some(Box::new(handler));
self
}
}
impl RenderOnce for Checkbox {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let group_id = format!("checkbox_group_{:?}", self.id);
let icon = match self.toggle_state {
ToggleState::Selected => Some(Icon::new(IconName::Check).size(IconSize::Small).color(
if self.disabled {
Color::Disabled
} else {
Color::Selected
},
)),
ToggleState::Indeterminate => Some(
Icon::new(IconName::Dash)
.size(IconSize::Small)
.color(if self.disabled {
Color::Disabled
} else {
Color::Selected
}),
),
ToggleState::Unselected => None,
};
let selected = self.toggle_state == ToggleState::Selected
|| self.toggle_state == ToggleState::Indeterminate;
let (bg_color, border_color) = match (self.disabled, selected) {
(true, _) => (
cx.theme().colors().ghost_element_disabled,
cx.theme().colors().border_disabled,
),
(false, true) => (
cx.theme().colors().element_selected,
cx.theme().colors().border,
),
(false, false) => (
cx.theme().colors().element_background,
cx.theme().colors().border,
),
};
h_flex()
.id(self.id)
.justify_center()
.items_center()
.size(DynamicSpacing::Base20.rems(cx))
.group(group_id.clone())
.child(
div()
.flex()
.flex_none()
.justify_center()
.items_center()
.m(DynamicSpacing::Base04.px(cx))
.size(DynamicSpacing::Base16.rems(cx))
.rounded_sm()
.bg(bg_color)
.border_1()
.border_color(border_color)
.when(!self.disabled, |this| {
this.group_hover(group_id.clone(), |el| {
el.bg(cx.theme().colors().element_hover)
})
})
.children(icon),
)
.when_some(
self.on_click.filter(|_| !self.disabled),
|this, on_click| {
this.on_click(move |_, cx| on_click(&self.toggle_state.inverse(), cx))
},
)
}
}
/// A [`Checkbox`] that has a [`Label`].
#[derive(IntoElement)]
pub struct CheckboxWithLabel {
id: ElementId,
label: Label,
checked: ToggleState,
on_click: Arc<dyn Fn(&ToggleState, &mut WindowContext) + 'static>,
}
impl CheckboxWithLabel {
pub fn new(
id: impl Into<ElementId>,
label: Label,
checked: ToggleState,
on_click: impl Fn(&ToggleState, &mut WindowContext) + 'static,
) -> Self {
Self {
id: id.into(),
label,
checked,
on_click: Arc::new(on_click),
}
}
}
impl RenderOnce for CheckboxWithLabel {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
h_flex()
.gap(DynamicSpacing::Base08.rems(cx))
.child(Checkbox::new(self.id.clone(), self.checked).on_click({
let on_click = self.on_click.clone();
move |checked, cx| {
(on_click)(checked, cx);
}
}))
.child(
div()
.id(SharedString::from(format!("{}-label", self.id)))
.on_click(move |_event, cx| {
(self.on_click)(&self.checked.inverse(), cx);
})
.child(self.label),
)
}
}
/// # Switch
///
/// Switches are used to represent opposite states, such as enabled or disabled.
#[derive(IntoElement)]
pub struct Switch {
id: ElementId,
toggle_state: ToggleState,
disabled: bool,
on_click: Option<Box<dyn Fn(&ToggleState, &mut WindowContext) + 'static>>,
}
impl Switch {
pub fn new(id: impl Into<ElementId>, state: ToggleState) -> Self {
Self {
id: id.into(),
toggle_state: state,
disabled: false,
on_click: None,
}
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn on_click(
mut self,
handler: impl Fn(&ToggleState, &mut WindowContext) + 'static,
) -> Self {
self.on_click = Some(Box::new(handler));
self
}
}
impl RenderOnce for Switch {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let is_on = self.toggle_state == ToggleState::Selected;
let adjust_ratio = if is_light(cx) { 1.5 } else { 1.0 };
let base_color = cx.theme().colors().text;
let bg_color = if is_on {
cx.theme()
.colors()
.element_background
.blend(base_color.opacity(0.08))
} else {
cx.theme().colors().element_background
};
let thumb_color = base_color.opacity(0.8);
let thumb_hover_color = base_color;
let border_color = cx.theme().colors().border_variant;
// Lighter themes need higher contrast borders
let border_hover_color = if is_on {
border_color.blend(base_color.opacity(0.16 * adjust_ratio))
} else {
border_color.blend(base_color.opacity(0.05 * adjust_ratio))
};
let thumb_opacity = match (is_on, self.disabled) {
(_, true) => 0.2,
(true, false) => 1.0,
(false, false) => 0.5,
};
let group_id = format!("switch_group_{:?}", self.id);
h_flex()
.id(self.id)
.items_center()
.w(DynamicSpacing::Base32.rems(cx))
.h(DynamicSpacing::Base20.rems(cx))
.group(group_id.clone())
.child(
h_flex()
.when(is_on, |on| on.justify_end())
.when(!is_on, |off| off.justify_start())
.items_center()
.size_full()
.rounded_full()
.px(DynamicSpacing::Base02.px(cx))
.bg(bg_color)
.border_1()
.border_color(border_color)
.when(!self.disabled, |this| {
this.group_hover(group_id.clone(), |el| el.border_color(border_hover_color))
})
.child(
div()
.size(DynamicSpacing::Base12.rems(cx))
.rounded_full()
.bg(thumb_color)
.when(!self.disabled, |this| {
this.group_hover(group_id.clone(), |el| el.bg(thumb_hover_color))
})
.opacity(thumb_opacity),
),
)
.when_some(
self.on_click.filter(|_| !self.disabled),
|this, on_click| {
this.on_click(move |_, cx| on_click(&self.toggle_state.inverse(), cx))
},
)
}
}
impl ComponentPreview for Checkbox {
fn description() -> impl Into<Option<&'static str>> {
"A checkbox lets people choose between a pair of opposing states, like enabled and disabled, using a different appearance to indicate each state."
}
fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
vec![
example_group_with_title(
"Default",
vec![
single_example(
"Unselected",
Checkbox::new("checkbox_unselected", ToggleState::Unselected),
),
single_example(
"Indeterminate",
Checkbox::new("checkbox_indeterminate", ToggleState::Indeterminate),
),
single_example(
"Selected",
Checkbox::new("checkbox_selected", ToggleState::Selected),
),
],
),
example_group_with_title(
"Disabled",
vec![
single_example(
"Unselected",
Checkbox::new("checkbox_disabled_unselected", ToggleState::Unselected)
.disabled(true),
),
single_example(
"Indeterminate",
Checkbox::new(
"checkbox_disabled_indeterminate",
ToggleState::Indeterminate,
)
.disabled(true),
),
single_example(
"Selected",
Checkbox::new("checkbox_disabled_selected", ToggleState::Selected)
.disabled(true),
),
],
),
]
}
}
impl ComponentPreview for Switch {
fn description() -> impl Into<Option<&'static str>> {
"A switch toggles between two mutually exclusive states, typically used for enabling or disabling a setting."
}
fn examples(_cx: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
vec![
example_group_with_title(
"Default",
vec![
single_example(
"Off",
Switch::new("switch_off", ToggleState::Unselected).on_click(|_, _cx| {}),
),
single_example(
"On",
Switch::new("switch_on", ToggleState::Selected).on_click(|_, _cx| {}),
),
],
),
example_group_with_title(
"Disabled",
vec![
single_example(
"Off",
Switch::new("switch_disabled_off", ToggleState::Unselected).disabled(true),
),
single_example(
"On",
Switch::new("switch_disabled_on", ToggleState::Selected).disabled(true),
),
],
),
]
}
}
impl ComponentPreview for CheckboxWithLabel {
fn description() -> impl Into<Option<&'static str>> {
"A checkbox with an associated label, allowing users to select an option while providing a descriptive text."
}
fn examples(_: &mut WindowContext) -> Vec<ComponentExampleGroup<Self>> {
vec![example_group(vec![
single_example(
"Unselected",
CheckboxWithLabel::new(
"checkbox_with_label_unselected",
Label::new("Always save on quit"),
ToggleState::Unselected,
|_, _| {},
),
),
single_example(
"Indeterminate",
CheckboxWithLabel::new(
"checkbox_with_label_indeterminate",
Label::new("Always save on quit"),
ToggleState::Indeterminate,
|_, _| {},
),
),
single_example(
"Selected",
CheckboxWithLabel::new(
"checkbox_with_label_selected",
Label::new("Always save on quit"),
ToggleState::Selected,
|_, _| {},
),
),
])]
}
}

View File

@@ -12,8 +12,8 @@ pub use crate::traits::clickable::*;
pub use crate::traits::component_preview::*;
pub use crate::traits::disableable::*;
pub use crate::traits::fixed::*;
pub use crate::traits::selectable::*;
pub use crate::traits::styled_ext::*;
pub use crate::traits::toggleable::*;
pub use crate::traits::visible_on_hover::*;
pub use crate::DynamicSpacing;
pub use crate::{h_flex, h_group, v_flex, v_group};

View File

@@ -2,6 +2,6 @@ pub mod clickable;
pub mod component_preview;
pub mod disableable;
pub mod fixed;
pub mod selectable;
pub mod styled_ext;
pub mod toggleable;
pub mod visible_on_hover;

View File

@@ -1,14 +1,15 @@
/// A trait for elements that can be selected.
/// A trait for elements that can be toggled.
///
/// Generally used to enable "toggle" or "active" behavior and styles on an element through the [`Selection`] status.
pub trait Selectable {
/// Implement this for elements that are visually distinct
/// when in two opposing states, like checkboxes or switches.
pub trait Toggleable {
/// Sets whether the element is selected.
fn selected(self, selected: bool) -> Self;
fn toggle_state(self, selected: bool) -> Self;
}
/// Represents the selection status of an element.
#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)]
pub enum Selection {
pub enum ToggleState {
/// The element is not selected.
#[default]
Unselected,
@@ -18,7 +19,7 @@ pub enum Selection {
Selected,
}
impl Selection {
impl ToggleState {
/// Returns the inverse of the current selection status.
///
/// Indeterminate states become selected if inverted.
@@ -30,7 +31,7 @@ impl Selection {
}
}
impl From<bool> for Selection {
impl From<bool> for ToggleState {
fn from(selected: bool) -> Self {
if selected {
Self::Selected
@@ -40,7 +41,7 @@ impl From<bool> for Selection {
}
}
impl From<Option<bool>> for Selection {
impl From<Option<bool>> for ToggleState {
fn from(selected: Option<bool>) -> Self {
match selected {
Some(true) => Self::Selected,

View File

@@ -1,5 +1,8 @@
//! UI-related utilities
use gpui::WindowContext;
use theme::ActiveTheme;
mod color_contrast;
mod format_distance;
mod search_input;
@@ -9,3 +12,8 @@ pub use color_contrast::*;
pub use format_distance::*;
pub use search_input::*;
pub use with_rem_size::*;
/// Returns true if the current theme is light or vibrant light.
pub fn is_light(cx: &WindowContext) -> bool {
cx.theme().appearance.is_light()
}

View File

@@ -24,7 +24,6 @@ futures-lite.workspace = true
futures.workspace = true
git2 = { workspace = true, optional = true }
globset.workspace = true
itertools.workspace = true
log.workspace = true
rand = { workspace = true, optional = true }
regex.workspace = true

View File

@@ -8,7 +8,6 @@ pub mod test;
use futures::Future;
use itertools::Either;
use regex::Regex;
use std::sync::OnceLock;
use std::{
@@ -200,35 +199,6 @@ pub fn measure<R>(label: &str, f: impl FnOnce() -> R) -> R {
}
}
pub fn iterate_expanded_and_wrapped_usize_range(
range: Range<usize>,
additional_before: usize,
additional_after: usize,
wrap_length: usize,
) -> impl Iterator<Item = usize> {
let start_wraps = range.start < additional_before;
let end_wraps = wrap_length < range.end + additional_after;
if start_wraps && end_wraps {
Either::Left(0..wrap_length)
} else if start_wraps {
let wrapped_start = (range.start + wrap_length).saturating_sub(additional_before);
if wrapped_start <= range.end {
Either::Left(0..wrap_length)
} else {
Either::Right((0..range.end + additional_after).chain(wrapped_start..wrap_length))
}
} else if end_wraps {
let wrapped_end = range.end + additional_after - wrap_length;
if range.start <= wrapped_end {
Either::Left(0..wrap_length)
} else {
Either::Right((0..wrapped_end).chain(range.start - additional_before..wrap_length))
}
} else {
Either::Left((range.start - additional_before)..(range.end + additional_after))
}
}
pub trait ResultExt<E> {
type Ok;
@@ -763,48 +733,4 @@ Line 2
Line 3"#
);
}
#[test]
fn test_iterate_expanded_and_wrapped_usize_range() {
// Neither wrap
assert_eq!(
iterate_expanded_and_wrapped_usize_range(2..4, 1, 1, 8).collect::<Vec<usize>>(),
(1..5).collect::<Vec<usize>>()
);
// Start wraps
assert_eq!(
iterate_expanded_and_wrapped_usize_range(2..4, 3, 1, 8).collect::<Vec<usize>>(),
((0..5).chain(7..8)).collect::<Vec<usize>>()
);
// Start wraps all the way around
assert_eq!(
iterate_expanded_and_wrapped_usize_range(2..4, 5, 1, 8).collect::<Vec<usize>>(),
(0..8).collect::<Vec<usize>>()
);
// Start wraps all the way around and past 0
assert_eq!(
iterate_expanded_and_wrapped_usize_range(2..4, 10, 1, 8).collect::<Vec<usize>>(),
(0..8).collect::<Vec<usize>>()
);
// End wraps
assert_eq!(
iterate_expanded_and_wrapped_usize_range(3..5, 1, 4, 8).collect::<Vec<usize>>(),
(0..1).chain(2..8).collect::<Vec<usize>>()
);
// End wraps all the way around
assert_eq!(
iterate_expanded_and_wrapped_usize_range(3..5, 1, 5, 8).collect::<Vec<usize>>(),
(0..8).collect::<Vec<usize>>()
);
// End wraps all the way around and past the end
assert_eq!(
iterate_expanded_and_wrapped_usize_range(3..5, 1, 10, 8).collect::<Vec<usize>>(),
(0..8).collect::<Vec<usize>>()
);
// Both start and end wrap
assert_eq!(
iterate_expanded_and_wrapped_usize_range(3..5, 4, 4, 8).collect::<Vec<usize>>(),
(0..8).collect::<Vec<usize>>()
);
}
}

View File

@@ -284,7 +284,7 @@ impl PickerDelegate for BranchListDelegate {
ListItem::new(SharedString::from(format!("vcs-menu-{ix}")))
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.toggle_state(selected)
.map(|parent| match hit {
BranchEntry::Branch(branch) => {
let highlights: Vec<_> = branch

View File

@@ -208,7 +208,7 @@ impl PickerDelegate for BaseKeymapSelectorDelegate {
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.toggle_state(selected)
.child(HighlightedLabel::new(
keymap_match.string.clone(),
keymap_match.positions.clone(),

View File

@@ -273,9 +273,9 @@ impl Render for WelcomePage {
"enable-vim",
Label::new("Enable Vim Mode"),
if VimModeSetting::get_global(cx).0 {
ui::Selection::Selected
ui::ToggleState::Selected
} else {
ui::Selection::Unselected
ui::ToggleState::Unselected
},
cx.listener(move |this, selection, cx| {
this.telemetry
@@ -298,9 +298,9 @@ impl Render for WelcomePage {
"enable-crash",
Label::new("Send Crash Reports"),
if TelemetrySettings::get_global(cx).diagnostics {
ui::Selection::Selected
ui::ToggleState::Selected
} else {
ui::Selection::Unselected
ui::ToggleState::Unselected
},
cx.listener(move |this, selection, cx| {
this.telemetry.report_app_event(
@@ -324,9 +324,9 @@ impl Render for WelcomePage {
"enable-telemetry",
Label::new("Send Telemetry"),
if TelemetrySettings::get_global(cx).metrics {
ui::Selection::Selected
ui::ToggleState::Selected
} else {
ui::Selection::Unselected
ui::ToggleState::Unselected
},
cx.listener(move |this, selection, cx| {
this.telemetry.report_app_event(
@@ -381,7 +381,7 @@ impl WelcomePage {
fn update_settings<T: Settings>(
&mut self,
selection: &Selection,
selection: &ToggleState,
cx: &mut ViewContext<Self>,
callback: impl 'static + Send + Fn(&mut T::FileContent, bool),
) {
@@ -390,8 +390,8 @@ impl WelcomePage {
let selection = *selection;
settings::update_settings_file::<T>(fs, cx, move |settings, _| {
let value = match selection {
Selection::Unselected => false,
Selection::Selected => true,
ToggleState::Unselected => false,
ToggleState::Selected => true,
_ => return,
};

View File

@@ -781,7 +781,7 @@ impl Render for PanelButtons {
.trigger(
IconButton::new(name, icon)
.icon_size(IconSize::Small)
.selected(is_active_button)
.toggle_state(is_active_button)
.on_click({
let action = action.boxed_clone();
move |_, cx| cx.dispatch_action(action.boxed_clone())

View File

@@ -481,7 +481,7 @@ impl Pane {
let zoomed = pane.is_zoomed();
IconButton::new("toggle_zoom", IconName::Maximize)
.icon_size(IconSize::Small)
.selected(zoomed)
.toggle_state(zoomed)
.selected_icon(IconName::Minimize)
.on_click(cx.listener(|pane, _, cx| {
pane.toggle_zoom(&crate::ToggleZoom, cx);
@@ -2038,7 +2038,7 @@ impl Pane {
ClosePosition::Left => ui::TabCloseSide::Start,
ClosePosition::Right => ui::TabCloseSide::End,
})
.selected(is_active)
.toggle_state(is_active)
.on_click(
cx.listener(move |pane: &mut Self, _, cx| pane.activate_item(ix, true, true, cx)),
)
@@ -3273,7 +3273,7 @@ impl Render for DraggedTab {
cx,
);
Tab::new("")
.selected(self.is_active)
.toggle_state(self.is_active)
.child(label)
.render(cx)
.font(ui_font)

View File

@@ -6,7 +6,7 @@ use ui::{
element_cell, prelude::*, string_cell, utils::calculate_contrast_ratio, AudioStatus,
Availability, Avatar, AvatarAudioStatusIndicator, AvatarAvailabilityIndicator, ButtonLike,
Checkbox, CheckboxWithLabel, ContentGroup, DecoratedIcon, ElevationIndex, Facepile,
IconDecoration, Indicator, Table, TintColor, Tooltip,
IconDecoration, Indicator, Switch, Table, TintColor, Tooltip,
};
use crate::{Item, Workspace};
@@ -369,6 +369,7 @@ impl ThemePreview {
.overflow_scroll()
.size_full()
.gap_2()
.child(Switch::render_component_previews(cx))
.child(ContentGroup::render_component_previews(cx))
.child(IconDecoration::render_component_previews(cx))
.child(DecoratedIcon::render_component_previews(cx))
@@ -394,7 +395,7 @@ impl ThemePreview {
this.current_page = p;
cx.notify();
}))
.selected(p == self.current_page)
.toggle_state(p == self.current_page)
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
}))
}

View File

@@ -162,7 +162,7 @@ impl Render for QuickActionBar {
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.selected(self.toggle_selections_handle.is_deployed())
.toggle_state(self.toggle_selections_handle.is_deployed())
.when(!self.toggle_selections_handle.is_deployed(), |this| {
this.tooltip(|cx| Tooltip::text("Selection Controls", cx))
}),
@@ -212,7 +212,7 @@ impl Render for QuickActionBar {
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.selected(self.toggle_settings_handle.is_deployed())
.toggle_state(self.toggle_settings_handle.is_deployed())
.when(!self.toggle_settings_handle.is_deployed(), |this| {
this.tooltip(|cx| Tooltip::text("Editor Controls", cx))
}),
@@ -407,7 +407,7 @@ impl RenderOnce for QuickActionBarButton {
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.selected(self.toggled)
.toggle_state(self.toggled)
.tooltip(move |cx| {
Tooltip::for_action_in(tooltip.clone(), &*action, &self.focus_handle, cx)
})

View File

@@ -578,7 +578,7 @@ impl Render for RateCompletionModal {
.inset(true)
.spacing(ListItemSpacing::Sparse)
.focused(index == self.selected_index)
.selected(selected)
.toggle_state(selected)
.start_slot(if rated {
Icon::new(IconName::Check).color(Color::Success).size(IconSize::Small)
} else if completion.edits.is_empty() {

View File

@@ -927,7 +927,7 @@ impl ZetaInlineCompletionProvider {
impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvider {
fn name() -> &'static str {
"Zeta"
"zeta"
}
fn is_enabled(