logiguard fork v3: full patch set on verified 8c74db0 tree
Includes prior-session patches (carry forward so the app compiles): - crates/gpui/build.rs: cross-compile manifest fix - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method - crates/gpui/src/window.rs: Window::activate_with_token public API - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix Plus the focus-serial fix: - serial.rs: SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; latest_serial_of() Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
46
crates/extensions_ui/Cargo.toml
Normal file
46
crates/extensions_ui/Cargo.toml
Normal file
@@ -0,0 +1,46 @@
|
||||
[package]
|
||||
name = "extensions_ui"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/extensions_ui.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
client.workspace = true
|
||||
cloud_api_types.workspace = true
|
||||
collections.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
extension.workspace = true
|
||||
extension_host.workspace = true
|
||||
fs.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
num-format.workspace = true
|
||||
picker.workspace = true
|
||||
project.workspace = true
|
||||
release_channel.workspace = true
|
||||
semver.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
smallvec.workspace = true
|
||||
strum.workspace = true
|
||||
telemetry.workspace = true
|
||||
theme_settings.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
vim_mode_setting.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
1
crates/extensions_ui/LICENSE-GPL
Symbolic link
1
crates/extensions_ui/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
3
crates/extensions_ui/src/components.rs
Normal file
3
crates/extensions_ui/src/components.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod extension_card;
|
||||
|
||||
pub use extension_card::*;
|
||||
61
crates/extensions_ui/src/components/extension_card.rs
Normal file
61
crates/extensions_ui/src/components/extension_card.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
use gpui::{AnyElement, prelude::*};
|
||||
use smallvec::SmallVec;
|
||||
use ui::prelude::*;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ExtensionCard {
|
||||
overridden_by_dev_extension: bool,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl ExtensionCard {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
overridden_by_dev_extension: false,
|
||||
children: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn overridden_by_dev_extension(mut self, overridden: bool) -> Self {
|
||||
self.overridden_by_dev_extension = overridden;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for ExtensionCard {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ExtensionCard {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
div().w_full().child(
|
||||
v_flex()
|
||||
.mt_4()
|
||||
.w_full()
|
||||
.h(rems_from_px(110.))
|
||||
.p_3()
|
||||
.gap_2()
|
||||
.bg(cx.theme().colors().elevated_surface_background.opacity(0.5))
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.rounded_md()
|
||||
.children(self.children)
|
||||
.when(self.overridden_by_dev_extension, |card| {
|
||||
card.child(
|
||||
h_flex()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.block_mouse_except_scroll()
|
||||
.cursor_default()
|
||||
.size_full()
|
||||
.justify_center()
|
||||
.bg(cx.theme().colors().elevated_surface_background.alpha(0.8))
|
||||
.child(Label::new("Overridden by dev extension.")),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
252
crates/extensions_ui/src/extension_suggest.rs
Normal file
252
crates/extensions_ui/src/extension_suggest.rs
Normal file
@@ -0,0 +1,252 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
use db::kvp::KeyValueStore;
|
||||
use editor::Editor;
|
||||
use extension_host::ExtensionStore;
|
||||
use gpui::{AppContext as _, Context, Entity, SharedString, Window};
|
||||
use language::Buffer;
|
||||
use ui::prelude::*;
|
||||
use util::ResultExt;
|
||||
use util::rel_path::RelPath;
|
||||
use workspace::notifications::simple_message_notification::MessageNotification;
|
||||
use workspace::{Workspace, notifications::NotificationId};
|
||||
|
||||
const SUGGESTIONS_BY_EXTENSION_ID: &[(&str, &[&str])] = &[
|
||||
("astro", &["astro"]),
|
||||
("beancount", &["beancount"]),
|
||||
("clojure", &["bb", "clj", "cljc", "cljs", "edn"]),
|
||||
("neocmake", &["CMakeLists.txt", "cmake"]),
|
||||
("csharp", &["cs"]),
|
||||
("cython", &["pyx", "pxd", "pxi"]),
|
||||
("dart", &["dart"]),
|
||||
("dockerfile", &["Dockerfile"]),
|
||||
("elisp", &["el"]),
|
||||
("elixir", &["eex", "ex", "exs", "heex", "leex", "neex"]),
|
||||
("elm", &["elm"]),
|
||||
("erlang", &["erl", "hrl"]),
|
||||
("fish", &["fish"]),
|
||||
(
|
||||
"git-firefly",
|
||||
&[
|
||||
".gitconfig",
|
||||
".gitignore",
|
||||
"COMMIT_EDITMSG",
|
||||
"EDIT_DESCRIPTION",
|
||||
"MERGE_MSG",
|
||||
"NOTES_EDITMSG",
|
||||
"TAG_EDITMSG",
|
||||
"git-rebase-todo",
|
||||
],
|
||||
),
|
||||
("gleam", &["gleam"]),
|
||||
("glsl", &["vert", "frag"]),
|
||||
("graphql", &["gql", "graphql"]),
|
||||
("haskell", &["hs"]),
|
||||
("html", &["htm", "html", "shtml"]),
|
||||
("java", &["java"]),
|
||||
("kotlin", &["kt"]),
|
||||
("latex", &["tex"]),
|
||||
("log", &["log"]),
|
||||
("lua", &["lua"]),
|
||||
("make", &["Makefile"]),
|
||||
("nim", &["nim"]),
|
||||
("nix", &["nix"]),
|
||||
("nu", &["nu"]),
|
||||
("ocaml", &["ml", "mli"]),
|
||||
("php", &["php"]),
|
||||
("powershell", &["ps1", "psm1"]),
|
||||
("prisma", &["prisma"]),
|
||||
("proto", &["proto"]),
|
||||
("purescript", &["purs"]),
|
||||
("r", &["r", "R"]),
|
||||
("racket", &["rkt"]),
|
||||
("rescript", &["res", "resi"]),
|
||||
("rst", &["rst"]),
|
||||
("ruby", &["rb", "erb"]),
|
||||
("scheme", &["scm"]),
|
||||
("scss", &["scss"]),
|
||||
("sql", &["sql"]),
|
||||
("svelte", &["svelte"]),
|
||||
("swift", &["swift"]),
|
||||
("templ", &["templ"]),
|
||||
("terraform", &["tf", "tfvars", "hcl"]),
|
||||
("toml", &["Cargo.lock", "toml"]),
|
||||
("typst", &["typ"]),
|
||||
("vue", &["vue"]),
|
||||
("wgsl", &["wgsl"]),
|
||||
("wit", &["wit"]),
|
||||
("xml", &["xml"]),
|
||||
("zig", &["zig"]),
|
||||
];
|
||||
|
||||
fn suggested_extensions() -> &'static HashMap<&'static str, Arc<str>> {
|
||||
static SUGGESTIONS_BY_PATH_SUFFIX: OnceLock<HashMap<&str, Arc<str>>> = OnceLock::new();
|
||||
SUGGESTIONS_BY_PATH_SUFFIX.get_or_init(|| {
|
||||
SUGGESTIONS_BY_EXTENSION_ID
|
||||
.iter()
|
||||
.flat_map(|(name, path_suffixes)| {
|
||||
let name = Arc::<str>::from(*name);
|
||||
path_suffixes
|
||||
.iter()
|
||||
.map(move |suffix| (*suffix, name.clone()))
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
struct SuggestedExtension {
|
||||
pub extension_id: Arc<str>,
|
||||
pub file_name_or_extension: Arc<str>,
|
||||
}
|
||||
|
||||
/// Returns the suggested extension for the given [`Path`].
|
||||
fn suggested_extension(path: &RelPath) -> Option<SuggestedExtension> {
|
||||
let file_extension: Option<Arc<str>> = path.extension().map(|extension| extension.into());
|
||||
let file_name: Option<Arc<str>> = path.file_name().map(|name| name.into());
|
||||
|
||||
let (file_name_or_extension, extension_id) = None
|
||||
// We suggest against file names first, as these suggestions will be more
|
||||
// specific than ones based on the file extension.
|
||||
.or_else(|| {
|
||||
file_name.clone().zip(
|
||||
file_name
|
||||
.as_deref()
|
||||
.and_then(|file_name| suggested_extensions().get(file_name)),
|
||||
)
|
||||
})
|
||||
.or_else(|| {
|
||||
file_extension.clone().zip(
|
||||
file_extension
|
||||
.as_deref()
|
||||
.and_then(|file_extension| suggested_extensions().get(file_extension)),
|
||||
)
|
||||
})?;
|
||||
|
||||
Some(SuggestedExtension {
|
||||
extension_id: extension_id.clone(),
|
||||
file_name_or_extension,
|
||||
})
|
||||
}
|
||||
|
||||
fn language_extension_key(extension_id: &str) -> String {
|
||||
format!("{}_extension_suggest", extension_id)
|
||||
}
|
||||
|
||||
pub(crate) fn suggest(buffer: Entity<Buffer>, window: &mut Window, cx: &mut Context<Workspace>) {
|
||||
let Some(file) = buffer.read(cx).file().cloned() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(SuggestedExtension {
|
||||
extension_id,
|
||||
file_name_or_extension,
|
||||
}) = suggested_extension(file.path())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let key = language_extension_key(&extension_id);
|
||||
let kvp = KeyValueStore::global(cx);
|
||||
let Ok(None) = kvp.read_kvp(&key) else {
|
||||
return;
|
||||
};
|
||||
|
||||
cx.on_next_frame(window, move |workspace, _, cx| {
|
||||
let Some(editor) = workspace.active_item_as::<Editor>(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if editor.read(cx).buffer().read(cx).as_singleton().as_ref() != Some(&buffer) {
|
||||
return;
|
||||
}
|
||||
|
||||
struct ExtensionSuggestionNotification;
|
||||
|
||||
let notification_id = NotificationId::composite::<ExtensionSuggestionNotification>(
|
||||
SharedString::from(extension_id.clone()),
|
||||
);
|
||||
|
||||
workspace.show_notification(notification_id, cx, |cx| {
|
||||
cx.new(move |cx| {
|
||||
MessageNotification::new(
|
||||
format!(
|
||||
"Do you want to install the recommended '{}' extension for '{}' files?",
|
||||
extension_id, file_name_or_extension
|
||||
),
|
||||
cx,
|
||||
)
|
||||
.primary_message("Yes, install extension")
|
||||
.primary_icon(IconName::Check)
|
||||
.primary_icon_color(Color::Success)
|
||||
.primary_on_click({
|
||||
let extension_id = extension_id.clone();
|
||||
move |_window, cx| {
|
||||
let extension_id = extension_id.clone();
|
||||
let extension_store = ExtensionStore::global(cx);
|
||||
extension_store.update(cx, move |store, cx| {
|
||||
store.install_latest_extension(extension_id, cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
.secondary_message("No, don't install it")
|
||||
.secondary_icon(IconName::Close)
|
||||
.secondary_icon_color(Color::Error)
|
||||
.secondary_on_click(move |_window, cx| {
|
||||
let key = language_extension_key(&extension_id);
|
||||
let kvp = KeyValueStore::global(cx);
|
||||
cx.background_spawn(async move {
|
||||
kvp.write_kvp(key, "dismissed".to_string()).await.log_err()
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
})
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use util::rel_path::rel_path;
|
||||
|
||||
#[test]
|
||||
pub fn test_suggested_extension() {
|
||||
assert_eq!(
|
||||
suggested_extension(rel_path("Cargo.toml")),
|
||||
Some(SuggestedExtension {
|
||||
extension_id: "toml".into(),
|
||||
file_name_or_extension: "toml".into()
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
suggested_extension(rel_path("Cargo.lock")),
|
||||
Some(SuggestedExtension {
|
||||
extension_id: "toml".into(),
|
||||
file_name_or_extension: "Cargo.lock".into()
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
suggested_extension(rel_path("Dockerfile")),
|
||||
Some(SuggestedExtension {
|
||||
extension_id: "dockerfile".into(),
|
||||
file_name_or_extension: "Dockerfile".into()
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
suggested_extension(rel_path("a/b/c/d/.gitignore")),
|
||||
Some(SuggestedExtension {
|
||||
extension_id: "git-firefly".into(),
|
||||
file_name_or_extension: ".gitignore".into()
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
suggested_extension(rel_path("a/b/c/d/test.gleam")),
|
||||
Some(SuggestedExtension {
|
||||
extension_id: "gleam".into(),
|
||||
file_name_or_extension: "gleam".into()
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
251
crates/extensions_ui/src/extension_version_selector.rs
Normal file
251
crates/extensions_ui/src/extension_version_selector.rs
Normal file
@@ -0,0 +1,251 @@
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use cloud_api_types::ExtensionMetadata;
|
||||
use extension_host::ExtensionStore;
|
||||
use fs::Fs;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
|
||||
use gpui::{App, DismissEvent, Entity, EventEmitter, Focusable, Task, WeakEntity, prelude::*};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use release_channel::ReleaseChannel;
|
||||
use semver::Version;
|
||||
use settings::update_settings_file;
|
||||
use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*};
|
||||
use util::ResultExt;
|
||||
use workspace::ModalView;
|
||||
|
||||
pub struct ExtensionVersionSelector {
|
||||
picker: Entity<Picker<ExtensionVersionSelectorDelegate>>,
|
||||
}
|
||||
|
||||
impl ModalView for ExtensionVersionSelector {}
|
||||
|
||||
impl EventEmitter<DismissEvent> for ExtensionVersionSelector {}
|
||||
|
||||
impl Focusable for ExtensionVersionSelector {
|
||||
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ExtensionVersionSelector {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex().w(rems(34.)).child(self.picker.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtensionVersionSelector {
|
||||
pub fn new(
|
||||
delegate: ExtensionVersionSelectorDelegate,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
|
||||
Self { picker }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ExtensionVersionSelectorDelegate {
|
||||
fs: Arc<dyn Fs>,
|
||||
selector: WeakEntity<ExtensionVersionSelector>,
|
||||
extension_versions: Vec<ExtensionMetadata>,
|
||||
selected_index: usize,
|
||||
matches: Vec<StringMatch>,
|
||||
}
|
||||
|
||||
impl ExtensionVersionSelectorDelegate {
|
||||
pub fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
selector: WeakEntity<ExtensionVersionSelector>,
|
||||
mut extension_versions: Vec<ExtensionMetadata>,
|
||||
) -> Self {
|
||||
extension_versions.sort_unstable_by(|a, b| {
|
||||
let a_version = Version::from_str(&a.manifest.version);
|
||||
let b_version = Version::from_str(&b.manifest.version);
|
||||
|
||||
match (a_version, b_version) {
|
||||
(Ok(a_version), Ok(b_version)) => b_version.cmp(&a_version),
|
||||
_ => b.published_at.cmp(&a.published_at),
|
||||
}
|
||||
});
|
||||
|
||||
let matches = extension_versions
|
||||
.iter()
|
||||
.map(|extension| StringMatch {
|
||||
candidate_id: 0,
|
||||
score: 0.0,
|
||||
positions: Default::default(),
|
||||
string: format!("v{}", extension.manifest.version),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
fs,
|
||||
selector,
|
||||
extension_versions,
|
||||
selected_index: 0,
|
||||
matches,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for ExtensionVersionSelectorDelegate {
|
||||
type ListItem = ui::ListItem;
|
||||
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||
"Select extension version...".into()
|
||||
}
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(
|
||||
&mut self,
|
||||
ix: usize,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Picker<Self>>,
|
||||
) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let background_executor = cx.background_executor().clone();
|
||||
let candidates = self
|
||||
.extension_versions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, extension)| {
|
||||
StringMatchCandidate::new(id, &format!("v{}", extension.manifest.version))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let matches = if query.is_empty() {
|
||||
candidates
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, candidate)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: candidate.string,
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
false,
|
||||
true,
|
||||
100,
|
||||
&Default::default(),
|
||||
background_executor,
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
this.update(cx, |this, _cx| {
|
||||
this.delegate.matches = matches;
|
||||
this.delegate.selected_index = this
|
||||
.delegate
|
||||
.selected_index
|
||||
.min(this.delegate.matches.len().saturating_sub(1));
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
if self.matches.is_empty() {
|
||||
self.dismissed(window, cx);
|
||||
return;
|
||||
}
|
||||
|
||||
let candidate_id = self.matches[self.selected_index].candidate_id;
|
||||
let extension_version = &self.extension_versions[candidate_id];
|
||||
|
||||
if !extension_host::is_version_compatible(ReleaseChannel::global(cx), extension_version) {
|
||||
return;
|
||||
}
|
||||
|
||||
let extension_store = ExtensionStore::global(cx);
|
||||
extension_store.update(cx, |store, cx| {
|
||||
let extension_id = extension_version.id.clone();
|
||||
let version = extension_version.manifest.version.clone();
|
||||
|
||||
update_settings_file(self.fs.clone(), cx, {
|
||||
let extension_id = extension_id.clone();
|
||||
move |settings, _| {
|
||||
settings
|
||||
.extension
|
||||
.auto_update_extensions
|
||||
.insert(extension_id, false);
|
||||
}
|
||||
});
|
||||
|
||||
store.install_extension(extension_id, version, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
self.selector
|
||||
.update(cx, |_, cx| cx.emit(DismissEvent))
|
||||
.log_err();
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let version_match = &self.matches.get(ix)?;
|
||||
let extension_version = &self.extension_versions.get(version_match.candidate_id)?;
|
||||
|
||||
let is_version_compatible =
|
||||
extension_host::is_version_compatible(ReleaseChannel::global(cx), extension_version);
|
||||
let disabled = !is_version_compatible;
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.disabled(disabled)
|
||||
.child(
|
||||
HighlightedLabel::new(
|
||||
version_match.string.clone(),
|
||||
version_match.positions.clone(),
|
||||
)
|
||||
.when(disabled, |label| label.color(Color::Muted)),
|
||||
)
|
||||
.end_slot(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.when(!is_version_compatible, |this| {
|
||||
this.child(Label::new("Incompatible").color(Color::Muted))
|
||||
})
|
||||
.child(
|
||||
Label::new(
|
||||
extension_version
|
||||
.published_at
|
||||
.format("%Y-%m-%d")
|
||||
.to_string(),
|
||||
)
|
||||
.when(disabled, |label| label.color(Color::Muted)),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
1890
crates/extensions_ui/src/extensions_ui.rs
Normal file
1890
crates/extensions_ui/src/extensions_ui.rs
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user