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