Compare commits
53 Commits
fix-rust-l
...
git-panel-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3e04867ab | ||
|
|
3abf165c1d | ||
|
|
48eae645f2 | ||
|
|
78b6cae754 | ||
|
|
1cac4b6c00 | ||
|
|
ec741d61ed | ||
|
|
426f94b310 | ||
|
|
eff61ee764 | ||
|
|
aee641d79d | ||
|
|
caefdcd7f1 | ||
|
|
ff2ad63037 | ||
|
|
88f7942f11 | ||
|
|
188c55c8a6 | ||
|
|
2562b488b1 | ||
|
|
bc113e4b51 | ||
|
|
ea012075fc | ||
|
|
ce727fbc07 | ||
|
|
62b3acee5f | ||
|
|
38c0aa303e | ||
|
|
040d9ae222 | ||
|
|
d135ec2b73 | ||
|
|
a94afbc062 | ||
|
|
7b721efe2c | ||
|
|
18b6d142c3 | ||
|
|
53c9af3e61 | ||
|
|
af50261ae2 | ||
|
|
f64fcedabb | ||
|
|
7e6233d70f | ||
|
|
d459f010b6 | ||
|
|
25970650a7 | ||
|
|
9daa426e93 | ||
|
|
6e1cc5dad3 | ||
|
|
c5fe6ef100 | ||
|
|
1ac60289fe | ||
|
|
cbc226597c | ||
|
|
ff2d20780f | ||
|
|
cd5d8b4173 | ||
|
|
0be7cf8ea8 | ||
|
|
4f96706161 | ||
|
|
4f439ae35f | ||
|
|
85c3aec6e7 | ||
|
|
c2eea3a474 | ||
|
|
695f06c020 | ||
|
|
901dbedf8d | ||
|
|
99dc85e6ec | ||
|
|
735849e201 | ||
|
|
06edcd18be | ||
|
|
e53c1a8ee3 | ||
|
|
421974f923 | ||
|
|
c57cc35b03 | ||
|
|
19d6e067af | ||
|
|
2f2e7f0317 | ||
|
|
2b699053e6 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
2
.github/workflows/release_nightly.yml
vendored
2
.github/workflows/release_nightly.yml
vendored
@@ -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
7
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -23,4 +23,5 @@ pub struct Context {
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum ContextKind {
|
||||
File,
|
||||
FetchedUrl,
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
218
crates/assistant2/src/context_picker/fetch_context_picker.rs
Normal file
218
crates/assistant2/src/context_picker/fetch_context_picker.rs
Normal 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()),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}),
|
||||
)))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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())),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,3 +15,4 @@ doctest = false
|
||||
[dependencies]
|
||||
gpui.workspace = true
|
||||
util.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())),
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -395,7 +395,7 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.selected(selected)
|
||||
.toggle_state(selected)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.child(
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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())),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|_, _| {},
|
||||
),
|
||||
),
|
||||
])]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
|
||||
@@ -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)"))
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
409
crates/ui/src/components/toggle.rs
Normal file
409
crates/ui/src/components/toggle.rs
Normal 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,
|
||||
|_, _| {},
|
||||
),
|
||||
),
|
||||
])]
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -927,7 +927,7 @@ impl ZetaInlineCompletionProvider {
|
||||
|
||||
impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvider {
|
||||
fn name() -> &'static str {
|
||||
"Zeta"
|
||||
"zeta"
|
||||
}
|
||||
|
||||
fn is_enabled(
|
||||
|
||||
Reference in New Issue
Block a user