rules_library.rs

   1use anyhow::Result;
   2use collections::{HashMap, HashSet};
   3use editor::SelectionEffects;
   4use editor::{CurrentLineHighlight, Editor, EditorElement, EditorEvent, EditorStyle, actions::Tab};
   5use gpui::{
   6    App, Bounds, DEFAULT_ADDITIONAL_WINDOW_SIZE, Entity, EventEmitter, Focusable, PromptLevel,
   7    Subscription, Task, TextStyle, Tiling, TitlebarOptions, WindowBounds, WindowHandle,
   8    WindowOptions, actions, point, size, transparent_black,
   9};
  10use language::{Buffer, LanguageRegistry, language_settings::SoftWrap};
  11use language_model::{
  12    ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
  13};
  14use picker::{Picker, PickerDelegate};
  15use platform_title_bar::PlatformTitleBar;
  16use release_channel::ReleaseChannel;
  17use rope::Rope;
  18use settings::{ActionSequence, Settings};
  19use std::sync::Arc;
  20use std::sync::atomic::AtomicBool;
  21use std::time::Duration;
  22use theme_settings::ThemeSettings;
  23use ui::{Divider, ListItem, ListItemSpacing, ListSubHeader, Tooltip, prelude::*};
  24use ui_input::ErasedEditor;
  25use util::{ResultExt, TryFutureExt};
  26use workspace::{MultiWorkspace, Workspace, WorkspaceSettings, client_side_decorations};
  27use zed_actions::assistant::InlineAssist;
  28
  29use prompt_store::*;
  30
  31pub fn init(cx: &mut App) {
  32    prompt_store::init(cx);
  33}
  34
  35actions!(
  36    rules_library,
  37    [
  38        /// Creates a new rule in the rules library.
  39        NewRule,
  40        /// Deletes the selected rule.
  41        DeleteRule,
  42        /// Duplicates the selected rule.
  43        DuplicateRule,
  44        /// Toggles whether the selected rule is a default rule.
  45        ToggleDefaultRule,
  46        /// Restores a built-in rule to its default content.
  47        RestoreDefaultContent
  48    ]
  49);
  50
  51pub trait InlineAssistDelegate {
  52    fn assist(
  53        &self,
  54        prompt_editor: &Entity<Editor>,
  55        initial_prompt: Option<String>,
  56        window: &mut Window,
  57        cx: &mut Context<RulesLibrary>,
  58    );
  59
  60    /// Returns whether the Agent panel was focused.
  61    fn focus_agent_panel(
  62        &self,
  63        workspace: &mut Workspace,
  64        window: &mut Window,
  65        cx: &mut Context<Workspace>,
  66    ) -> bool;
  67}
  68
  69/// This function opens a new rules library window if one doesn't exist already.
  70/// If one exists, it brings it to the foreground.
  71///
  72/// Note that, when opening a new window, this waits for the PromptStore to be
  73/// initialized. If it was initialized successfully, it returns a window handle
  74/// to a rules library.
  75pub fn open_rules_library(
  76    language_registry: Arc<LanguageRegistry>,
  77    inline_assist_delegate: Box<dyn InlineAssistDelegate>,
  78    prompt_to_select: Option<PromptId>,
  79    cx: &mut App,
  80) -> Task<Result<WindowHandle<RulesLibrary>>> {
  81    let store = PromptStore::global(cx);
  82    cx.spawn(async move |cx| {
  83        // We query windows in spawn so that all windows have been returned to GPUI
  84        let existing_window = cx.update(|cx| {
  85            let existing_window = cx
  86                .windows()
  87                .into_iter()
  88                .find_map(|window| window.downcast::<RulesLibrary>());
  89            if let Some(existing_window) = existing_window {
  90                existing_window
  91                    .update(cx, |rules_library, window, cx| {
  92                        if let Some(prompt_to_select) = prompt_to_select {
  93                            rules_library.load_rule(prompt_to_select, true, window, cx);
  94                        }
  95                        window.activate_window()
  96                    })
  97                    .ok();
  98
  99                Some(existing_window)
 100            } else {
 101                None
 102            }
 103        });
 104
 105        if let Some(existing_window) = existing_window {
 106            return Ok(existing_window);
 107        }
 108
 109        let store = store.await?;
 110        cx.update(|cx| {
 111            let app_id = ReleaseChannel::global(cx).app_id();
 112            let bounds = Bounds::centered(None, size(px(1024.0), px(768.0)), cx);
 113            let window_decorations = match std::env::var("ZED_WINDOW_DECORATIONS") {
 114                Ok(val) if val == "server" => gpui::WindowDecorations::Server,
 115                Ok(val) if val == "client" => gpui::WindowDecorations::Client,
 116                _ => match WorkspaceSettings::get_global(cx).window_decorations {
 117                    settings::WindowDecorations::Server => gpui::WindowDecorations::Server,
 118                    settings::WindowDecorations::Client => gpui::WindowDecorations::Client,
 119                },
 120            };
 121            cx.open_window(
 122                WindowOptions {
 123                    titlebar: Some(TitlebarOptions {
 124                        title: Some("Rules Library".into()),
 125                        appears_transparent: true,
 126                        traffic_light_position: Some(point(px(12.0), px(12.0))),
 127                    }),
 128                    app_id: Some(app_id.to_owned()),
 129                    window_bounds: Some(WindowBounds::Windowed(bounds)),
 130                    window_background: cx.theme().window_background_appearance(),
 131                    window_decorations: Some(window_decorations),
 132                    window_min_size: Some(DEFAULT_ADDITIONAL_WINDOW_SIZE),
 133                    kind: gpui::WindowKind::Floating,
 134                    ..Default::default()
 135                },
 136                |window, cx| {
 137                    cx.new(|cx| {
 138                        RulesLibrary::new(
 139                            store,
 140                            language_registry,
 141                            inline_assist_delegate,
 142                            prompt_to_select,
 143                            window,
 144                            cx,
 145                        )
 146                    })
 147                },
 148            )
 149        })
 150    })
 151}
 152
 153pub struct RulesLibrary {
 154    title_bar: Option<Entity<PlatformTitleBar>>,
 155    store: Entity<PromptStore>,
 156    language_registry: Arc<LanguageRegistry>,
 157    rule_editors: HashMap<PromptId, RuleEditor>,
 158    active_rule_id: Option<PromptId>,
 159    picker: Entity<Picker<RulePickerDelegate>>,
 160    pending_load: Task<()>,
 161    inline_assist_delegate: Box<dyn InlineAssistDelegate>,
 162    _subscriptions: Vec<Subscription>,
 163}
 164
 165struct RuleEditor {
 166    title_editor: Entity<Editor>,
 167    body_editor: Entity<Editor>,
 168    token_count: Option<u64>,
 169    pending_token_count: Task<Option<()>>,
 170    next_title_and_body_to_save: Option<(String, Rope)>,
 171    pending_save: Option<Task<Option<()>>>,
 172    _subscriptions: Vec<Subscription>,
 173}
 174
 175enum RulePickerEntry {
 176    Header(SharedString),
 177    Rule(PromptMetadata),
 178    Separator,
 179}
 180
 181struct RulePickerDelegate {
 182    store: Entity<PromptStore>,
 183    selected_index: usize,
 184    filtered_entries: Vec<RulePickerEntry>,
 185}
 186
 187enum RulePickerEvent {
 188    Selected { prompt_id: PromptId },
 189    Confirmed { prompt_id: PromptId },
 190    Deleted { prompt_id: PromptId },
 191    ToggledDefault { prompt_id: PromptId },
 192}
 193
 194impl EventEmitter<RulePickerEvent> for Picker<RulePickerDelegate> {}
 195
 196impl PickerDelegate for RulePickerDelegate {
 197    type ListItem = AnyElement;
 198
 199    fn match_count(&self) -> usize {
 200        self.filtered_entries.len()
 201    }
 202
 203    fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
 204        Some("No rules found matching your search.".into())
 205    }
 206
 207    fn selected_index(&self) -> usize {
 208        self.selected_index
 209    }
 210
 211    fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
 212        self.selected_index = ix.min(self.filtered_entries.len().saturating_sub(1));
 213
 214        if let Some(RulePickerEntry::Rule(rule)) = self.filtered_entries.get(self.selected_index) {
 215            cx.emit(RulePickerEvent::Selected { prompt_id: rule.id });
 216        }
 217
 218        cx.notify();
 219    }
 220
 221    fn can_select(&self, ix: usize, _: &mut Window, _: &mut Context<Picker<Self>>) -> bool {
 222        match self.filtered_entries.get(ix) {
 223            Some(RulePickerEntry::Rule(_)) => true,
 224            Some(RulePickerEntry::Header(_)) | Some(RulePickerEntry::Separator) | None => false,
 225        }
 226    }
 227
 228    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
 229        "Search…".into()
 230    }
 231
 232    fn update_matches(
 233        &mut self,
 234        query: String,
 235        window: &mut Window,
 236        cx: &mut Context<Picker<Self>>,
 237    ) -> Task<()> {
 238        let cancellation_flag = Arc::new(AtomicBool::default());
 239        let search = self.store.read(cx).search(query, cancellation_flag, cx);
 240
 241        let prev_prompt_id = self
 242            .filtered_entries
 243            .get(self.selected_index)
 244            .and_then(|entry| {
 245                if let RulePickerEntry::Rule(rule) = entry {
 246                    Some(rule.id)
 247                } else {
 248                    None
 249                }
 250            });
 251
 252        cx.spawn_in(window, async move |this, cx| {
 253            let (filtered_entries, selected_index) = cx
 254                .background_spawn(async move {
 255                    let matches = search.await;
 256
 257                    let (built_in_rules, user_rules): (Vec<_>, Vec<_>) =
 258                        matches.into_iter().partition(|rule| rule.id.is_built_in());
 259                    let (default_rules, other_rules): (Vec<_>, Vec<_>) =
 260                        user_rules.into_iter().partition(|rule| rule.default);
 261
 262                    let mut filtered_entries = Vec::new();
 263
 264                    if !built_in_rules.is_empty() {
 265                        filtered_entries.push(RulePickerEntry::Header("Built-in Rules".into()));
 266
 267                        for rule in built_in_rules {
 268                            filtered_entries.push(RulePickerEntry::Rule(rule));
 269                        }
 270
 271                        filtered_entries.push(RulePickerEntry::Separator);
 272                    }
 273
 274                    if !default_rules.is_empty() {
 275                        filtered_entries.push(RulePickerEntry::Header("Default Rules".into()));
 276
 277                        for rule in default_rules {
 278                            filtered_entries.push(RulePickerEntry::Rule(rule));
 279                        }
 280
 281                        filtered_entries.push(RulePickerEntry::Separator);
 282                    }
 283
 284                    for rule in other_rules {
 285                        filtered_entries.push(RulePickerEntry::Rule(rule));
 286                    }
 287
 288                    let selected_index = prev_prompt_id
 289                        .and_then(|prev_prompt_id| {
 290                            filtered_entries.iter().position(|entry| {
 291                                if let RulePickerEntry::Rule(rule) = entry {
 292                                    rule.id == prev_prompt_id
 293                                } else {
 294                                    false
 295                                }
 296                            })
 297                        })
 298                        .unwrap_or_else(|| {
 299                            filtered_entries
 300                                .iter()
 301                                .position(|entry| matches!(entry, RulePickerEntry::Rule(_)))
 302                                .unwrap_or(0)
 303                        });
 304
 305                    (filtered_entries, selected_index)
 306                })
 307                .await;
 308
 309            this.update_in(cx, |this, window, cx| {
 310                this.delegate.filtered_entries = filtered_entries;
 311                this.set_selected_index(
 312                    selected_index,
 313                    Some(picker::Direction::Down),
 314                    true,
 315                    window,
 316                    cx,
 317                );
 318                cx.notify();
 319            })
 320            .ok();
 321        })
 322    }
 323
 324    fn confirm(&mut self, _secondary: bool, _: &mut Window, cx: &mut Context<Picker<Self>>) {
 325        if let Some(RulePickerEntry::Rule(rule)) = self.filtered_entries.get(self.selected_index) {
 326            cx.emit(RulePickerEvent::Confirmed { prompt_id: rule.id });
 327        }
 328    }
 329
 330    fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context<Picker<Self>>) {}
 331
 332    fn render_match(
 333        &self,
 334        ix: usize,
 335        selected: bool,
 336        _: &mut Window,
 337        cx: &mut Context<Picker<Self>>,
 338    ) -> Option<Self::ListItem> {
 339        match self.filtered_entries.get(ix)? {
 340            RulePickerEntry::Header(title) => {
 341                let tooltip_text = if title.as_ref() == "Built-in Rules" {
 342                    "Built-in rules are those included out of the box with Zed."
 343                } else {
 344                    "Default Rules are attached by default with every new thread."
 345                };
 346
 347                Some(
 348                    ListSubHeader::new(title.clone())
 349                        .end_slot(
 350                            IconButton::new("info", IconName::Info)
 351                                .style(ButtonStyle::Transparent)
 352                                .icon_size(IconSize::Small)
 353                                .icon_color(Color::Muted)
 354                                .tooltip(Tooltip::text(tooltip_text))
 355                                .into_any_element(),
 356                        )
 357                        .inset(true)
 358                        .into_any_element(),
 359                )
 360            }
 361            RulePickerEntry::Separator => Some(
 362                h_flex()
 363                    .py_1()
 364                    .child(Divider::horizontal())
 365                    .into_any_element(),
 366            ),
 367            RulePickerEntry::Rule(rule) => {
 368                let default = rule.default;
 369                let prompt_id = rule.id;
 370
 371                Some(
 372                    ListItem::new(ix)
 373                        .inset(true)
 374                        .spacing(ListItemSpacing::Sparse)
 375                        .toggle_state(selected)
 376                        .child(
 377                            Label::new(rule.title.clone().unwrap_or("Untitled".into()))
 378                                .truncate()
 379                                .mr_10(),
 380                        )
 381                        .end_slot::<IconButton>((default && !prompt_id.is_built_in()).then(|| {
 382                            IconButton::new("toggle-default-rule", IconName::Paperclip)
 383                                .toggle_state(true)
 384                                .icon_color(Color::Accent)
 385                                .icon_size(IconSize::Small)
 386                                .tooltip(Tooltip::text("Remove from Default Rules"))
 387                                .on_click(cx.listener(move |_, _, _, cx| {
 388                                    cx.emit(RulePickerEvent::ToggledDefault { prompt_id })
 389                                }))
 390                        }))
 391                        .when(!prompt_id.is_built_in(), |this| {
 392                            this.end_slot_on_hover(
 393                                h_flex()
 394                                    .child(
 395                                        IconButton::new("delete-rule", IconName::Trash)
 396                                            .icon_color(Color::Muted)
 397                                            .icon_size(IconSize::Small)
 398                                            .tooltip(Tooltip::text("Delete Rule"))
 399                                            .on_click(cx.listener(move |_, _, _, cx| {
 400                                                cx.emit(RulePickerEvent::Deleted { prompt_id })
 401                                            })),
 402                                    )
 403                                    .child(
 404                                        IconButton::new("toggle-default-rule", IconName::Plus)
 405                                            .selected_icon(IconName::Dash)
 406                                            .toggle_state(default)
 407                                            .icon_size(IconSize::Small)
 408                                            .icon_color(if default {
 409                                                Color::Accent
 410                                            } else {
 411                                                Color::Muted
 412                                            })
 413                                            .map(|this| {
 414                                                if default {
 415                                                    this.tooltip(Tooltip::text(
 416                                                        "Remove from Default Rules",
 417                                                    ))
 418                                                } else {
 419                                                    this.tooltip(move |_window, cx| {
 420                                                        Tooltip::with_meta(
 421                                                            "Add to Default Rules",
 422                                                            None,
 423                                                            "Always included in every thread.",
 424                                                            cx,
 425                                                        )
 426                                                    })
 427                                                }
 428                                            })
 429                                            .on_click(cx.listener(move |_, _, _, cx| {
 430                                                cx.emit(RulePickerEvent::ToggledDefault {
 431                                                    prompt_id,
 432                                                })
 433                                            })),
 434                                    ),
 435                            )
 436                        })
 437                        .into_any_element(),
 438                )
 439            }
 440        }
 441    }
 442
 443    fn render_editor(
 444        &self,
 445        editor: &Arc<dyn ErasedEditor>,
 446        _: &mut Window,
 447        cx: &mut Context<Picker<Self>>,
 448    ) -> Div {
 449        let editor = editor.as_any().downcast_ref::<Entity<Editor>>().unwrap();
 450
 451        h_flex()
 452            .py_1()
 453            .px_1p5()
 454            .mx_1()
 455            .gap_1p5()
 456            .rounded_sm()
 457            .bg(cx.theme().colors().editor_background)
 458            .border_1()
 459            .border_color(cx.theme().colors().border)
 460            .child(Icon::new(IconName::MagnifyingGlass).color(Color::Muted))
 461            .child(editor.clone())
 462    }
 463}
 464
 465impl RulesLibrary {
 466    fn new(
 467        store: Entity<PromptStore>,
 468        language_registry: Arc<LanguageRegistry>,
 469        inline_assist_delegate: Box<dyn InlineAssistDelegate>,
 470        rule_to_select: Option<PromptId>,
 471        window: &mut Window,
 472        cx: &mut Context<Self>,
 473    ) -> Self {
 474        let (_selected_index, _matches) = if let Some(rule_to_select) = rule_to_select {
 475            let matches = store.read(cx).all_prompt_metadata();
 476            let selected_index = matches
 477                .iter()
 478                .enumerate()
 479                .find(|(_, metadata)| metadata.id == rule_to_select)
 480                .map_or(0, |(ix, _)| ix);
 481            (selected_index, matches)
 482        } else {
 483            (0, vec![])
 484        };
 485
 486        let picker_delegate = RulePickerDelegate {
 487            store: store.clone(),
 488            selected_index: 0,
 489            filtered_entries: Vec::new(),
 490        };
 491
 492        let picker = cx.new(|cx| {
 493            let picker = Picker::list(picker_delegate, window, cx)
 494                .modal(false)
 495                .max_height(None);
 496            picker.focus(window, cx);
 497            picker
 498        });
 499
 500        Self {
 501            title_bar: if !cfg!(target_os = "macos") {
 502                Some(cx.new(|cx| PlatformTitleBar::new("rules-library-title-bar", cx)))
 503            } else {
 504                None
 505            },
 506            store,
 507            language_registry,
 508            rule_editors: HashMap::default(),
 509            active_rule_id: None,
 510            pending_load: Task::ready(()),
 511            inline_assist_delegate,
 512            _subscriptions: vec![cx.subscribe_in(&picker, window, Self::handle_picker_event)],
 513            picker,
 514        }
 515    }
 516
 517    fn handle_picker_event(
 518        &mut self,
 519        _: &Entity<Picker<RulePickerDelegate>>,
 520        event: &RulePickerEvent,
 521        window: &mut Window,
 522        cx: &mut Context<Self>,
 523    ) {
 524        match event {
 525            RulePickerEvent::Selected { prompt_id } => {
 526                self.load_rule(*prompt_id, false, window, cx);
 527            }
 528            RulePickerEvent::Confirmed { prompt_id } => {
 529                self.load_rule(*prompt_id, true, window, cx);
 530            }
 531            RulePickerEvent::ToggledDefault { prompt_id } => {
 532                self.toggle_default_for_rule(*prompt_id, window, cx);
 533            }
 534            RulePickerEvent::Deleted { prompt_id } => {
 535                self.delete_rule(*prompt_id, window, cx);
 536            }
 537        }
 538    }
 539
 540    pub fn new_rule(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 541        // If we already have an untitled rule, use that instead
 542        // of creating a new one.
 543        if let Some(metadata) = self.store.read(cx).first()
 544            && metadata.title.is_none()
 545        {
 546            self.load_rule(metadata.id, true, window, cx);
 547            return;
 548        }
 549
 550        let prompt_id = PromptId::new();
 551        let save = self.store.update(cx, |store, cx| {
 552            store.save(prompt_id, None, false, "".into(), cx)
 553        });
 554        self.picker
 555            .update(cx, |picker, cx| picker.refresh(window, cx));
 556        cx.spawn_in(window, async move |this, cx| {
 557            save.await?;
 558            this.update_in(cx, |this, window, cx| {
 559                this.load_rule(prompt_id, true, window, cx)
 560            })
 561        })
 562        .detach_and_log_err(cx);
 563    }
 564
 565    pub fn save_rule(&mut self, prompt_id: PromptId, window: &mut Window, cx: &mut Context<Self>) {
 566        const SAVE_THROTTLE: Duration = Duration::from_millis(500);
 567
 568        if !prompt_id.can_edit() {
 569            return;
 570        }
 571
 572        let rule_metadata = self.store.read(cx).metadata(prompt_id).unwrap();
 573        let rule_editor = self.rule_editors.get_mut(&prompt_id).unwrap();
 574        let title = rule_editor.title_editor.read(cx).text(cx);
 575        let body = rule_editor.body_editor.update(cx, |editor, cx| {
 576            editor
 577                .buffer()
 578                .read(cx)
 579                .as_singleton()
 580                .unwrap()
 581                .read(cx)
 582                .as_rope()
 583                .clone()
 584        });
 585
 586        let store = self.store.clone();
 587        let executor = cx.background_executor().clone();
 588
 589        rule_editor.next_title_and_body_to_save = Some((title, body));
 590        if rule_editor.pending_save.is_none() {
 591            rule_editor.pending_save = Some(cx.spawn_in(window, async move |this, cx| {
 592                async move {
 593                    loop {
 594                        let title_and_body = this.update(cx, |this, _| {
 595                            this.rule_editors
 596                                .get_mut(&prompt_id)?
 597                                .next_title_and_body_to_save
 598                                .take()
 599                        })?;
 600
 601                        if let Some((title, body)) = title_and_body {
 602                            let title = if title.trim().is_empty() {
 603                                None
 604                            } else {
 605                                Some(SharedString::from(title))
 606                            };
 607                            cx.update(|_window, cx| {
 608                                store.update(cx, |store, cx| {
 609                                    store.save(prompt_id, title, rule_metadata.default, body, cx)
 610                                })
 611                            })?
 612                            .await
 613                            .log_err();
 614                            this.update_in(cx, |this, window, cx| {
 615                                this.picker
 616                                    .update(cx, |picker, cx| picker.refresh(window, cx));
 617                                cx.notify();
 618                            })?;
 619
 620                            executor.timer(SAVE_THROTTLE).await;
 621                        } else {
 622                            break;
 623                        }
 624                    }
 625
 626                    this.update(cx, |this, _cx| {
 627                        if let Some(rule_editor) = this.rule_editors.get_mut(&prompt_id) {
 628                            rule_editor.pending_save = None;
 629                        }
 630                    })
 631                }
 632                .log_err()
 633                .await
 634            }));
 635        }
 636    }
 637
 638    pub fn delete_active_rule(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 639        if let Some(active_rule_id) = self.active_rule_id {
 640            self.delete_rule(active_rule_id, window, cx);
 641        }
 642    }
 643
 644    pub fn duplicate_active_rule(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 645        if let Some(active_rule_id) = self.active_rule_id {
 646            self.duplicate_rule(active_rule_id, window, cx);
 647        }
 648    }
 649
 650    pub fn toggle_default_for_active_rule(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 651        if let Some(active_rule_id) = self.active_rule_id {
 652            self.toggle_default_for_rule(active_rule_id, window, cx);
 653        }
 654    }
 655
 656    pub fn restore_default_content_for_active_rule(
 657        &mut self,
 658        window: &mut Window,
 659        cx: &mut Context<Self>,
 660    ) {
 661        if let Some(active_rule_id) = self.active_rule_id {
 662            self.restore_default_content(active_rule_id, window, cx);
 663        }
 664    }
 665
 666    pub fn restore_default_content(
 667        &mut self,
 668        prompt_id: PromptId,
 669        window: &mut Window,
 670        cx: &mut Context<Self>,
 671    ) {
 672        let Some(built_in) = prompt_id.as_built_in() else {
 673            return;
 674        };
 675
 676        if let Some(rule_editor) = self.rule_editors.get(&prompt_id) {
 677            rule_editor.body_editor.update(cx, |editor, cx| {
 678                editor.set_text(built_in.default_content(), window, cx);
 679            });
 680        }
 681    }
 682
 683    pub fn toggle_default_for_rule(
 684        &mut self,
 685        prompt_id: PromptId,
 686        window: &mut Window,
 687        cx: &mut Context<Self>,
 688    ) {
 689        self.store.update(cx, move |store, cx| {
 690            if let Some(rule_metadata) = store.metadata(prompt_id) {
 691                store
 692                    .save_metadata(prompt_id, rule_metadata.title, !rule_metadata.default, cx)
 693                    .detach_and_log_err(cx);
 694            }
 695        });
 696        self.picker
 697            .update(cx, |picker, cx| picker.refresh(window, cx));
 698        cx.notify();
 699    }
 700
 701    pub fn load_rule(
 702        &mut self,
 703        prompt_id: PromptId,
 704        focus: bool,
 705        window: &mut Window,
 706        cx: &mut Context<Self>,
 707    ) {
 708        if let Some(rule_editor) = self.rule_editors.get(&prompt_id) {
 709            if focus {
 710                rule_editor
 711                    .body_editor
 712                    .update(cx, |editor, cx| window.focus(&editor.focus_handle(cx), cx));
 713            }
 714            self.set_active_rule(Some(prompt_id), window, cx);
 715        } else if let Some(rule_metadata) = self.store.read(cx).metadata(prompt_id) {
 716            let language_registry = self.language_registry.clone();
 717            let rule = self.store.read(cx).load(prompt_id, cx);
 718            self.pending_load = cx.spawn_in(window, async move |this, cx| {
 719                let rule = rule.await;
 720                let markdown = language_registry.language_for_name("Markdown").await;
 721                this.update_in(cx, |this, window, cx| match rule {
 722                    Ok(rule) => {
 723                        let title_editor = cx.new(|cx| {
 724                            let mut editor = Editor::single_line(window, cx);
 725                            editor.set_placeholder_text("Untitled", window, cx);
 726                            editor.set_text(rule_metadata.title.unwrap_or_default(), window, cx);
 727                            if prompt_id.is_built_in() {
 728                                editor.set_read_only(true);
 729                                editor.set_show_edit_predictions(Some(false), window, cx);
 730                            }
 731                            editor
 732                        });
 733                        let body_editor = cx.new(|cx| {
 734                            let buffer = cx.new(|cx| {
 735                                let mut buffer = Buffer::local(rule, cx);
 736                                buffer.set_language(markdown.log_err(), cx);
 737                                buffer.set_language_registry(language_registry);
 738                                buffer
 739                            });
 740
 741                            let mut editor = Editor::for_buffer(buffer, None, window, cx);
 742                            if !prompt_id.can_edit() {
 743                                editor.set_read_only(true);
 744                                editor.set_show_edit_predictions(Some(false), window, cx);
 745                            }
 746                            editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
 747                            editor.set_show_gutter(false, cx);
 748                            editor.set_show_wrap_guides(false, cx);
 749                            editor.set_show_indent_guides(false, cx);
 750                            editor.set_use_modal_editing(true);
 751                            editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
 752                            if focus {
 753                                window.focus(&editor.focus_handle(cx), cx);
 754                            }
 755                            editor
 756                        });
 757                        let _subscriptions = vec![
 758                            cx.subscribe_in(
 759                                &title_editor,
 760                                window,
 761                                move |this, editor, event, window, cx| {
 762                                    this.handle_rule_title_editor_event(
 763                                        prompt_id, editor, event, window, cx,
 764                                    )
 765                                },
 766                            ),
 767                            cx.subscribe_in(
 768                                &body_editor,
 769                                window,
 770                                move |this, editor, event, window, cx| {
 771                                    this.handle_rule_body_editor_event(
 772                                        prompt_id, editor, event, window, cx,
 773                                    )
 774                                },
 775                            ),
 776                        ];
 777                        this.rule_editors.insert(
 778                            prompt_id,
 779                            RuleEditor {
 780                                title_editor,
 781                                body_editor,
 782                                next_title_and_body_to_save: None,
 783                                pending_save: None,
 784                                token_count: None,
 785                                pending_token_count: Task::ready(None),
 786                                _subscriptions,
 787                            },
 788                        );
 789                        this.set_active_rule(Some(prompt_id), window, cx);
 790                        this.count_tokens(prompt_id, window, cx);
 791                    }
 792                    Err(error) => {
 793                        // TODO: we should show the error in the UI.
 794                        log::error!("error while loading rule: {:?}", error);
 795                    }
 796                })
 797                .ok();
 798            });
 799        }
 800    }
 801
 802    fn set_active_rule(
 803        &mut self,
 804        prompt_id: Option<PromptId>,
 805        window: &mut Window,
 806        cx: &mut Context<Self>,
 807    ) {
 808        self.active_rule_id = prompt_id;
 809        self.picker.update(cx, |picker, cx| {
 810            if let Some(prompt_id) = prompt_id {
 811                if picker
 812                    .delegate
 813                    .filtered_entries
 814                    .get(picker.delegate.selected_index())
 815                    .is_none_or(|old_selected_prompt| {
 816                        if let RulePickerEntry::Rule(rule) = old_selected_prompt {
 817                            rule.id != prompt_id
 818                        } else {
 819                            true
 820                        }
 821                    })
 822                    && let Some(ix) = picker.delegate.filtered_entries.iter().position(|mat| {
 823                        if let RulePickerEntry::Rule(rule) = mat {
 824                            rule.id == prompt_id
 825                        } else {
 826                            false
 827                        }
 828                    })
 829                {
 830                    picker.set_selected_index(ix, None, true, window, cx);
 831                }
 832            } else {
 833                picker.focus(window, cx);
 834            }
 835        });
 836        cx.notify();
 837    }
 838
 839    pub fn delete_rule(
 840        &mut self,
 841        prompt_id: PromptId,
 842        window: &mut Window,
 843        cx: &mut Context<Self>,
 844    ) {
 845        if let Some(metadata) = self.store.read(cx).metadata(prompt_id) {
 846            let confirmation = window.prompt(
 847                PromptLevel::Warning,
 848                &format!(
 849                    "Are you sure you want to delete {}",
 850                    metadata.title.unwrap_or("Untitled".into())
 851                ),
 852                None,
 853                &["Delete", "Cancel"],
 854                cx,
 855            );
 856
 857            cx.spawn_in(window, async move |this, cx| {
 858                if confirmation.await.ok() == Some(0) {
 859                    this.update_in(cx, |this, window, cx| {
 860                        if this.active_rule_id == Some(prompt_id) {
 861                            this.set_active_rule(None, window, cx);
 862                        }
 863                        this.rule_editors.remove(&prompt_id);
 864                        this.store
 865                            .update(cx, |store, cx| store.delete(prompt_id, cx))
 866                            .detach_and_log_err(cx);
 867                        this.picker
 868                            .update(cx, |picker, cx| picker.refresh(window, cx));
 869                        cx.notify();
 870                    })?;
 871                }
 872                anyhow::Ok(())
 873            })
 874            .detach_and_log_err(cx);
 875        }
 876    }
 877
 878    pub fn duplicate_rule(
 879        &mut self,
 880        prompt_id: PromptId,
 881        window: &mut Window,
 882        cx: &mut Context<Self>,
 883    ) {
 884        if let Some(rule) = self.rule_editors.get(&prompt_id) {
 885            const DUPLICATE_SUFFIX: &str = " copy";
 886            let title_to_duplicate = rule.title_editor.read(cx).text(cx);
 887            let existing_titles = self
 888                .rule_editors
 889                .iter()
 890                .filter(|&(&id, _)| id != prompt_id)
 891                .map(|(_, rule_editor)| rule_editor.title_editor.read(cx).text(cx))
 892                .filter(|title| title.starts_with(&title_to_duplicate))
 893                .collect::<HashSet<_>>();
 894
 895            let title = if existing_titles.is_empty() {
 896                title_to_duplicate + DUPLICATE_SUFFIX
 897            } else {
 898                let mut i = 1;
 899                loop {
 900                    let new_title = format!("{title_to_duplicate}{DUPLICATE_SUFFIX} {i}");
 901                    if !existing_titles.contains(&new_title) {
 902                        break new_title;
 903                    }
 904                    i += 1;
 905                }
 906            };
 907
 908            let new_id = PromptId::new();
 909            let body = rule.body_editor.read(cx).text(cx);
 910            let save = self.store.update(cx, |store, cx| {
 911                store.save(new_id, Some(title.into()), false, body.into(), cx)
 912            });
 913            self.picker
 914                .update(cx, |picker, cx| picker.refresh(window, cx));
 915            cx.spawn_in(window, async move |this, cx| {
 916                save.await?;
 917                this.update_in(cx, |rules_library, window, cx| {
 918                    rules_library.load_rule(new_id, true, window, cx)
 919                })
 920            })
 921            .detach_and_log_err(cx);
 922        }
 923    }
 924
 925    fn focus_active_rule(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
 926        if let Some(active_rule) = self.active_rule_id {
 927            self.rule_editors[&active_rule]
 928                .body_editor
 929                .update(cx, |editor, cx| window.focus(&editor.focus_handle(cx), cx));
 930            cx.stop_propagation();
 931        }
 932    }
 933
 934    fn focus_picker(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
 935        self.picker
 936            .update(cx, |picker, cx| picker.focus(window, cx));
 937    }
 938
 939    pub fn inline_assist(
 940        &mut self,
 941        action: &InlineAssist,
 942        window: &mut Window,
 943        cx: &mut Context<Self>,
 944    ) {
 945        let Some(active_rule_id) = self.active_rule_id else {
 946            cx.propagate();
 947            return;
 948        };
 949
 950        let rule_editor = &self.rule_editors[&active_rule_id].body_editor;
 951        let Some(ConfiguredModel { provider, .. }) =
 952            LanguageModelRegistry::read_global(cx).inline_assistant_model()
 953        else {
 954            return;
 955        };
 956
 957        let initial_prompt = action.prompt.clone();
 958        if provider.is_authenticated(cx) {
 959            self.inline_assist_delegate
 960                .assist(rule_editor, initial_prompt, window, cx);
 961        } else {
 962            for window in cx.windows() {
 963                if let Some(multi_workspace) = window.downcast::<MultiWorkspace>() {
 964                    let panel = multi_workspace
 965                        .update(cx, |multi_workspace, window, cx| {
 966                            window.activate_window();
 967                            multi_workspace.workspace().update(cx, |workspace, cx| {
 968                                self.inline_assist_delegate
 969                                    .focus_agent_panel(workspace, window, cx)
 970                            })
 971                        })
 972                        .ok();
 973                    if panel == Some(true) {
 974                        return;
 975                    }
 976                }
 977            }
 978        }
 979    }
 980
 981    fn move_down_from_title(
 982        &mut self,
 983        _: &zed_actions::editor::MoveDown,
 984        window: &mut Window,
 985        cx: &mut Context<Self>,
 986    ) {
 987        if let Some(rule_id) = self.active_rule_id
 988            && let Some(rule_editor) = self.rule_editors.get(&rule_id)
 989        {
 990            window.focus(&rule_editor.body_editor.focus_handle(cx), cx);
 991        }
 992    }
 993
 994    fn move_up_from_body(
 995        &mut self,
 996        _: &zed_actions::editor::MoveUp,
 997        window: &mut Window,
 998        cx: &mut Context<Self>,
 999    ) {
1000        if let Some(rule_id) = self.active_rule_id
1001            && let Some(rule_editor) = self.rule_editors.get(&rule_id)
1002        {
1003            window.focus(&rule_editor.title_editor.focus_handle(cx), cx);
1004        }
1005    }
1006
1007    fn handle_rule_title_editor_event(
1008        &mut self,
1009        prompt_id: PromptId,
1010        title_editor: &Entity<Editor>,
1011        event: &EditorEvent,
1012        window: &mut Window,
1013        cx: &mut Context<Self>,
1014    ) {
1015        match event {
1016            EditorEvent::BufferEdited => {
1017                self.save_rule(prompt_id, window, cx);
1018                self.count_tokens(prompt_id, window, cx);
1019            }
1020            EditorEvent::Blurred => {
1021                title_editor.update(cx, |title_editor, cx| {
1022                    title_editor.change_selections(
1023                        SelectionEffects::no_scroll(),
1024                        window,
1025                        cx,
1026                        |selections| {
1027                            let cursor = selections.oldest_anchor().head();
1028                            selections.select_anchor_ranges([cursor..cursor]);
1029                        },
1030                    );
1031                });
1032            }
1033            _ => {}
1034        }
1035    }
1036
1037    fn handle_rule_body_editor_event(
1038        &mut self,
1039        prompt_id: PromptId,
1040        body_editor: &Entity<Editor>,
1041        event: &EditorEvent,
1042        window: &mut Window,
1043        cx: &mut Context<Self>,
1044    ) {
1045        match event {
1046            EditorEvent::BufferEdited => {
1047                self.save_rule(prompt_id, window, cx);
1048                self.count_tokens(prompt_id, window, cx);
1049            }
1050            EditorEvent::Blurred => {
1051                body_editor.update(cx, |body_editor, cx| {
1052                    body_editor.change_selections(
1053                        SelectionEffects::no_scroll(),
1054                        window,
1055                        cx,
1056                        |selections| {
1057                            let cursor = selections.oldest_anchor().head();
1058                            selections.select_anchor_ranges([cursor..cursor]);
1059                        },
1060                    );
1061                });
1062            }
1063            _ => {}
1064        }
1065    }
1066
1067    fn count_tokens(&mut self, prompt_id: PromptId, window: &mut Window, cx: &mut Context<Self>) {
1068        let Some(ConfiguredModel { model, .. }) =
1069            LanguageModelRegistry::read_global(cx).default_model()
1070        else {
1071            return;
1072        };
1073        if let Some(rule) = self.rule_editors.get_mut(&prompt_id) {
1074            let editor = &rule.body_editor.read(cx);
1075            let buffer = &editor.buffer().read(cx).as_singleton().unwrap().read(cx);
1076            let body = buffer.as_rope().clone();
1077            rule.pending_token_count = cx.spawn_in(window, async move |this, cx| {
1078                async move {
1079                    const DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1);
1080
1081                    cx.background_executor().timer(DEBOUNCE_TIMEOUT).await;
1082                    let token_count = cx
1083                        .update(|_, cx| {
1084                            model.count_tokens(
1085                                LanguageModelRequest {
1086                                    thread_id: None,
1087                                    prompt_id: None,
1088                                    intent: None,
1089                                    messages: vec![LanguageModelRequestMessage {
1090                                        role: Role::System,
1091                                        content: vec![body.to_string().into()],
1092                                        cache: false,
1093                                        reasoning_details: None,
1094                                    }],
1095                                    tools: Vec::new(),
1096                                    tool_choice: None,
1097                                    stop: Vec::new(),
1098                                    temperature: None,
1099                                    thinking_allowed: true,
1100                                    thinking_effort: None,
1101                                    speed: None,
1102                                },
1103                                cx,
1104                            )
1105                        })?
1106                        .await?;
1107
1108                    this.update(cx, |this, cx| {
1109                        let rule_editor = this.rule_editors.get_mut(&prompt_id).unwrap();
1110                        rule_editor.token_count = Some(token_count);
1111                        cx.notify();
1112                    })
1113                }
1114                .log_err()
1115                .await
1116            });
1117        }
1118    }
1119
1120    fn render_rule_list(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
1121        v_flex()
1122            .id("rule-list")
1123            .capture_action(cx.listener(Self::focus_active_rule))
1124            .px_1p5()
1125            .h_full()
1126            .w_64()
1127            .overflow_x_hidden()
1128            .bg(cx.theme().colors().panel_background)
1129            .map(|this| {
1130                if cfg!(target_os = "macos") {
1131                    this.child(
1132                        h_flex()
1133                            .p(DynamicSpacing::Base04.rems(cx))
1134                            .h_9()
1135                            .w_full()
1136                            .flex_none()
1137                            .justify_end()
1138                            .child(
1139                                IconButton::new("new-rule", IconName::Plus)
1140                                    .tooltip(move |_window, cx| {
1141                                        Tooltip::for_action("New Rule", &NewRule, cx)
1142                                    })
1143                                    .on_click(|_, window, cx| {
1144                                        window.dispatch_action(Box::new(NewRule), cx);
1145                                    }),
1146                            ),
1147                    )
1148                } else {
1149                    this.child(
1150                        h_flex().p_1().w_full().child(
1151                            Button::new("new-rule", "New Rule")
1152                                .full_width()
1153                                .style(ButtonStyle::Outlined)
1154                                .start_icon(
1155                                    Icon::new(IconName::Plus)
1156                                        .size(IconSize::Small)
1157                                        .color(Color::Muted),
1158                                )
1159                                .on_click(|_, window, cx| {
1160                                    window.dispatch_action(Box::new(NewRule), cx);
1161                                }),
1162                        ),
1163                    )
1164                }
1165            })
1166            .child(div().flex_grow().child(self.picker.clone()))
1167    }
1168
1169    fn render_active_rule_editor(
1170        &self,
1171        editor: &Entity<Editor>,
1172        read_only: bool,
1173        cx: &mut Context<Self>,
1174    ) -> impl IntoElement {
1175        let settings = ThemeSettings::get_global(cx);
1176        let text_color = if read_only {
1177            cx.theme().colors().text_muted
1178        } else {
1179            cx.theme().colors().text
1180        };
1181
1182        div()
1183            .w_full()
1184            .pl_1()
1185            .border_1()
1186            .border_color(transparent_black())
1187            .rounded_sm()
1188            .when(!read_only, |this| {
1189                this.group_hover("active-editor-header", |this| {
1190                    this.border_color(cx.theme().colors().border_variant)
1191                })
1192            })
1193            .on_action(cx.listener(Self::move_down_from_title))
1194            .child(EditorElement::new(
1195                &editor,
1196                EditorStyle {
1197                    background: cx.theme().system().transparent,
1198                    local_player: cx.theme().players().local(),
1199                    text: TextStyle {
1200                        color: text_color,
1201                        font_family: settings.ui_font.family.clone(),
1202                        font_features: settings.ui_font.features.clone(),
1203                        font_size: HeadlineSize::Medium.rems().into(),
1204                        font_weight: settings.ui_font.weight,
1205                        line_height: relative(settings.buffer_line_height.value()),
1206                        ..Default::default()
1207                    },
1208                    scrollbar_width: Pixels::ZERO,
1209                    syntax: cx.theme().syntax().clone(),
1210                    status: cx.theme().status().clone(),
1211                    inlay_hints_style: editor::make_inlay_hints_style(cx),
1212                    edit_prediction_styles: editor::make_suggestion_styles(cx),
1213                    ..EditorStyle::default()
1214                },
1215            ))
1216    }
1217
1218    fn render_duplicate_rule_button(&self) -> impl IntoElement {
1219        IconButton::new("duplicate-rule", IconName::BookCopy)
1220            .tooltip(move |_window, cx| Tooltip::for_action("Duplicate Rule", &DuplicateRule, cx))
1221            .on_click(|_, window, cx| {
1222                window.dispatch_action(Box::new(DuplicateRule), cx);
1223            })
1224    }
1225
1226    fn render_built_in_rule_controls(&self) -> impl IntoElement {
1227        h_flex()
1228            .gap_1()
1229            .child(self.render_duplicate_rule_button())
1230            .child(
1231                IconButton::new("restore-default", IconName::RotateCcw)
1232                    .tooltip(move |_window, cx| {
1233                        Tooltip::for_action(
1234                            "Restore to Default Content",
1235                            &RestoreDefaultContent,
1236                            cx,
1237                        )
1238                    })
1239                    .on_click(|_, window, cx| {
1240                        window.dispatch_action(Box::new(RestoreDefaultContent), cx);
1241                    }),
1242            )
1243    }
1244
1245    fn render_regular_rule_controls(&self, default: bool) -> impl IntoElement {
1246        h_flex()
1247            .gap_1()
1248            .child(
1249                IconButton::new("toggle-default-rule", IconName::Paperclip)
1250                    .toggle_state(default)
1251                    .when(default, |this| this.icon_color(Color::Accent))
1252                    .map(|this| {
1253                        if default {
1254                            this.tooltip(Tooltip::text("Remove from Default Rules"))
1255                        } else {
1256                            this.tooltip(move |_window, cx| {
1257                                Tooltip::with_meta(
1258                                    "Add to Default Rules",
1259                                    None,
1260                                    "Always included in every thread.",
1261                                    cx,
1262                                )
1263                            })
1264                        }
1265                    })
1266                    .on_click(|_, window, cx| {
1267                        window.dispatch_action(Box::new(ToggleDefaultRule), cx);
1268                    }),
1269            )
1270            .child(self.render_duplicate_rule_button())
1271            .child(
1272                IconButton::new("delete-rule", IconName::Trash)
1273                    .tooltip(move |_window, cx| Tooltip::for_action("Delete Rule", &DeleteRule, cx))
1274                    .on_click(|_, window, cx| {
1275                        window.dispatch_action(Box::new(DeleteRule), cx);
1276                    }),
1277            )
1278    }
1279
1280    fn render_active_rule(&mut self, cx: &mut Context<RulesLibrary>) -> gpui::Stateful<Div> {
1281        div()
1282            .id("rule-editor")
1283            .h_full()
1284            .flex_grow()
1285            .border_l_1()
1286            .border_color(cx.theme().colors().border)
1287            .bg(cx.theme().colors().editor_background)
1288            .children(self.active_rule_id.and_then(|prompt_id| {
1289                let rule_metadata = self.store.read(cx).metadata(prompt_id)?;
1290                let rule_editor = &self.rule_editors[&prompt_id];
1291                let focus_handle = rule_editor.body_editor.focus_handle(cx);
1292                let registry = LanguageModelRegistry::read_global(cx);
1293                let model = registry.default_model().map(|default| default.model);
1294                let built_in = prompt_id.is_built_in();
1295
1296                Some(
1297                    v_flex()
1298                        .id("rule-editor-inner")
1299                        .size_full()
1300                        .relative()
1301                        .overflow_hidden()
1302                        .on_click(cx.listener(move |_, _, window, cx| {
1303                            window.focus(&focus_handle, cx);
1304                        }))
1305                        .child(
1306                            h_flex()
1307                                .group("active-editor-header")
1308                                .h_12()
1309                                .px_2()
1310                                .gap_2()
1311                                .justify_between()
1312                                .child(self.render_active_rule_editor(
1313                                    &rule_editor.title_editor,
1314                                    built_in,
1315                                    cx,
1316                                ))
1317                                .child(
1318                                    h_flex()
1319                                        .h_full()
1320                                        .flex_shrink_0()
1321                                        .children(rule_editor.token_count.map(|token_count| {
1322                                            let token_count: SharedString =
1323                                                token_count.to_string().into();
1324                                            let label_token_count: SharedString =
1325                                                token_count.to_string().into();
1326
1327                                            div()
1328                                                .id("token_count")
1329                                                .mr_1()
1330                                                .flex_shrink_0()
1331                                                .tooltip(move |_window, cx| {
1332                                                    Tooltip::with_meta(
1333                                                        "Token Estimation",
1334                                                        None,
1335                                                        format!(
1336                                                            "Model: {}",
1337                                                            model
1338                                                                .as_ref()
1339                                                                .map(|model| model.name().0)
1340                                                                .unwrap_or_default()
1341                                                        ),
1342                                                        cx,
1343                                                    )
1344                                                })
1345                                                .child(
1346                                                    Label::new(format!(
1347                                                        "{} tokens",
1348                                                        label_token_count
1349                                                    ))
1350                                                    .color(Color::Muted),
1351                                                )
1352                                        }))
1353                                        .map(|this| {
1354                                            if built_in {
1355                                                this.child(self.render_built_in_rule_controls())
1356                                            } else {
1357                                                this.child(self.render_regular_rule_controls(
1358                                                    rule_metadata.default,
1359                                                ))
1360                                            }
1361                                        }),
1362                                ),
1363                        )
1364                        .child(
1365                            div()
1366                                .on_action(cx.listener(Self::focus_picker))
1367                                .on_action(cx.listener(Self::inline_assist))
1368                                .on_action(cx.listener(Self::move_up_from_body))
1369                                .h_full()
1370                                .flex_grow()
1371                                .child(
1372                                    h_flex()
1373                                        .py_2()
1374                                        .pl_2p5()
1375                                        .h_full()
1376                                        .flex_1()
1377                                        .child(rule_editor.body_editor.clone()),
1378                                ),
1379                        ),
1380                )
1381            }))
1382    }
1383}
1384
1385impl Render for RulesLibrary {
1386    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1387        let ui_font = theme_settings::setup_ui_font(window, cx);
1388        let theme = cx.theme().clone();
1389
1390        client_side_decorations(
1391            v_flex()
1392                .id("rules-library")
1393                .key_context("RulesLibrary")
1394                .on_action(
1395                    |action_sequence: &ActionSequence, window: &mut Window, cx: &mut App| {
1396                        for action in &action_sequence.0 {
1397                            window.dispatch_action(action.boxed_clone(), cx);
1398                        }
1399                    },
1400                )
1401                .on_action(cx.listener(|this, &NewRule, window, cx| this.new_rule(window, cx)))
1402                .on_action(
1403                    cx.listener(|this, &DeleteRule, window, cx| {
1404                        this.delete_active_rule(window, cx)
1405                    }),
1406                )
1407                .on_action(cx.listener(|this, &DuplicateRule, window, cx| {
1408                    this.duplicate_active_rule(window, cx)
1409                }))
1410                .on_action(cx.listener(|this, &ToggleDefaultRule, window, cx| {
1411                    this.toggle_default_for_active_rule(window, cx)
1412                }))
1413                .on_action(cx.listener(|this, &RestoreDefaultContent, window, cx| {
1414                    this.restore_default_content_for_active_rule(window, cx)
1415                }))
1416                .size_full()
1417                .overflow_hidden()
1418                .font(ui_font)
1419                .text_color(theme.colors().text)
1420                .children(self.title_bar.clone())
1421                .bg(theme.colors().background)
1422                .child(
1423                    h_flex()
1424                        .flex_1()
1425                        .when(!cfg!(target_os = "macos"), |this| {
1426                            this.border_t_1().border_color(cx.theme().colors().border)
1427                        })
1428                        .child(self.render_rule_list(cx))
1429                        .child(self.render_active_rule(cx)),
1430                ),
1431            window,
1432            cx,
1433            Tiling::default(),
1434        )
1435    }
1436}