rules_library.rs

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