566 lines · 18.5 KB · Rust
RawDownload
use std::cell::RefCell; use pathfinder_color::ColorU;use pathfinder_geometry::vector::Vector2F;use warp_core::ui::appearance::Appearance;use warp_core::ui::theme::Fill;use warp_editor::render::element::VerticalExpansionBehavior;use warpui::elements::{    Border, ChildView, Clipped, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Flex,    MainAxisAlignment, MainAxisSize, ParentElement, Radius, Shrinkable, Text,};use warpui::keymap::Keystroke;use warpui::text_layout::ClipConfig;use warpui::units::Pixels;use warpui::{    AppContext, Element, Entity, FocusContext, ModelHandle, SingletonEntity, TypedActionView, View,    ViewContext, ViewHandle,}; use crate::code::editor::comments::{EditorCommentsModel, PendingCommentEvent};use crate::code::editor::line::EditorLineLocation;use crate::code_review::comments::{CommentId, CommentOrigin};use crate::editor::InteractionState;use crate::notebooks::editor::model::NotebooksEditorModel;use crate::notebooks::editor::rich_text_styles;use crate::notebooks::editor::view::{EditorViewEvent, RichTextEditorConfig, RichTextEditorView};use crate::notebooks::link::{NotebookLinks, SessionSource};use crate::settings::FontSettings;use crate::ui_components::blended_colors;use crate::ui_components::icons::Icon;use crate::view_components::action_button::{    ActionButton, ButtonSize, DangerNakedTheme, KeystrokeSource, NakedTheme, PrimaryTheme,}; /// Default width of the comment editor, in pixels.pub(crate) const DEFAULT_COMMENT_MAX_WIDTH: f32 = 750.0; #[derive(Debug)]pub enum CommentEditorEvent {    ContentChanged,    CommentSaved {        id: Option<CommentId>,        comment_text: String,        #[cfg_attr(not(feature = "local_fs"), allow(dead_code))]        line: Option<EditorLineLocation>,    },    CloseEditor,    DeleteComment {        id: CommentId,    },} #[derive(Debug)]pub enum CommentEditorAction {    SaveComment,    CloseEditor,    RemoveComment,} pub struct CommentEditor {    /// Comment ID if editing an existing comment, None for new comments.    comment_id: Option<CommentId>,    editor: ViewHandle<RichTextEditorView>,    save_button: ViewHandle<ActionButton>,    close_button: ViewHandle<ActionButton>,    remove_button: ViewHandle<ActionButton>,    line: Option<EditorLineLocation>,    show_remove_button: bool,    save_button_disabled: bool,    laid_out_size: RefCell<Option<Vector2F>>,    is_imported_comment: bool,} impl CommentEditor {    pub fn new(        ctx: &mut ViewContext<Self>,        comment_model: ModelHandle<EditorCommentsModel>,    ) -> Self {        let editor = create_editable_comment_markdown_editor(None, ctx);         ctx.subscribe_to_view(&editor, |me, _, event, ctx| {            me.handle_editor_event(event, ctx);        });         ctx.subscribe_to_model(&comment_model, |me, _, event, ctx| {            me.handle_comment_model_event(event, ctx);        });         let (save_button, close_button, remove_button) = Self::create_buttons(ctx);         let mut me = Self {            comment_id: None,            editor,            save_button,            close_button,            remove_button,            line: None,            show_remove_button: false,            save_button_disabled: true,            laid_out_size: RefCell::new(None),            is_imported_comment: false,        };        me.update_save_button_state(ctx);        me    }     #[allow(unused)] // TODO(CODE-1464): use this    pub fn new_embedded(        ctx: &mut ViewContext<Self>,        comment_model: ModelHandle<EditorCommentsModel>,        comment_id: Option<CommentId>,        line: EditorLineLocation,    ) -> Self {        let editor = create_editable_comment_markdown_editor(None, ctx);         ctx.subscribe_to_view(&editor, |me, _, event, ctx| {            me.handle_editor_event(event, ctx);        });         ctx.subscribe_to_model(&comment_model, |me, _, event, ctx| {            me.handle_comment_model_event(event, ctx);        });         let (save_button, close_button, remove_button) = Self::create_buttons(ctx);         let show_remove_button = comment_id.is_some();         let mut me = Self {            comment_id,            editor,            save_button,            close_button,            remove_button,            line: Some(line),            show_remove_button,            save_button_disabled: true,            laid_out_size: RefCell::new(None),            is_imported_comment: false,        };        me.update_save_button_state(ctx);        me    }     #[cfg_attr(not(feature = "local_fs"), allow(unused))]    pub fn comment_text(&self, app: &AppContext) -> String {        self.editor.as_ref(app).model().as_ref(app).markdown(app)    }     #[cfg_attr(not(feature = "local_fs"), allow(unused))]    pub fn get_laid_out_size(&self) -> Option<Vector2F> {        self.laid_out_size.borrow().as_ref().cloned()    }     #[allow(unused)] // TODO(CODE-1464): use this    pub fn set_laid_out_size(&self, value: Vector2F) {        self.laid_out_size.replace(Some(value));    }     fn create_buttons(        ctx: &mut ViewContext<Self>,    ) -> (        ViewHandle<ActionButton>,        ViewHandle<ActionButton>,        ViewHandle<ActionButton>,    ) {        let save_button = ctx.add_typed_action_view(|ctx| {            ActionButton::new("Comment", PrimaryTheme)                .with_keybinding(                    KeystrokeSource::Fixed(                        Keystroke::parse(crate::code_review::CODE_REVIEW_SUBMIT_KEYSTROKE)                            .unwrap_or_default(),                    ),                    ctx,                )                .on_click(|ctx| {                    ctx.dispatch_typed_action(CommentEditorAction::SaveComment);                })                .with_size(ButtonSize::Small)        });         save_button.update(ctx, |button, ctx| {            button.set_disabled(true, ctx);        });         let close_button = ctx.add_typed_action_view(|_ctx| {            ActionButton::new("Cancel", NakedTheme)                .on_click(|ctx| {                    ctx.dispatch_typed_action(CommentEditorAction::CloseEditor);                })                .with_size(ButtonSize::Small)        });         let remove_button = ctx.add_typed_action_view(|_ctx| {            ActionButton::new("Remove", DangerNakedTheme)                .on_click(|ctx| {                    ctx.dispatch_typed_action(CommentEditorAction::RemoveComment);                })                .with_size(ButtonSize::Small)        });         (save_button, close_button, remove_button)    }     fn update_save_button_state(&mut self, ctx: &mut ViewContext<Self>) {        let is_empty = self.editor.as_ref(ctx).model().as_ref(ctx).is_empty(ctx);        if is_empty != self.save_button_disabled {            self.save_button_disabled = is_empty;            self.save_button.update(ctx, |button, ctx| {                button.set_disabled(is_empty, ctx);            });        }    }     fn handle_comment_model_event(        &mut self,        event: &PendingCommentEvent,        ctx: &mut ViewContext<Self>,    ) {        match event {            PendingCommentEvent::NewPendingComment(line) => self.attach_to_line(line, ctx),            PendingCommentEvent::ReopenPendingComment {                id,                line,                comment_text,                origin,            } => {                self.reopen_saved_comment(id, Some(line.clone()), comment_text, origin, ctx);            }        }    }     fn handle_editor_event(&mut self, event: &EditorViewEvent, ctx: &mut ViewContext<Self>) {        match event {            EditorViewEvent::Edited => {                self.update_save_button_state(ctx);                ctx.emit(CommentEditorEvent::ContentChanged);            }            EditorViewEvent::CmdEnter => {                self.save_comment(ctx);            }            EditorViewEvent::EscapePressed => {                // Dismiss the comment composer when pressing Escape on an empty draft.                if self.editor.as_ref(ctx).model().as_ref(ctx).is_empty(ctx) {                    self.reset(ctx);                    ctx.emit(CommentEditorEvent::CloseEditor);                }            }            _ => {}        }    }     fn attach_to_line(&mut self, line: &EditorLineLocation, ctx: &mut ViewContext<Self>) {        self.editor.update(ctx, |editor, ctx| {            // TODO: clear_buffer doesn't properly clear code blocks.            // The `reset_with_markdown` call below is a band-aid fix.            editor.reset_with_markdown("", ctx);        });        self.line = Some(line.clone());        self.update_save_button_state(ctx);    }     pub fn reopen_saved_comment(        &mut self,        id: &CommentId,        line: Option<EditorLineLocation>,        comment_text: &str,        origin: &CommentOrigin,        ctx: &mut ViewContext<Self>,    ) {        self.editor.update(ctx, |editor, ctx| {            editor.model().update(ctx, |model, ctx| {                model.reset_with_markdown(comment_text, ctx);            });        });         self.comment_id = Some(*id);        self.line = line;        self.show_remove_button = true;        self.is_imported_comment = origin.is_imported_from_github();         self.save_button.update(ctx, |button, ctx| {            button.set_label("Update", ctx);        });        ctx.notify();         self.update_save_button_state(ctx);    }     fn reset(&mut self, ctx: &mut ViewContext<Self>) {        self.editor.update(ctx, |editor, ctx| {            // TODO: system_clear_buffer doesn't properly clear code blocks.            // The `reset_with_markdown` call below is a band-aid fix.            editor.reset_with_markdown("", ctx);        });        self.comment_id = None;        self.line = None;        self.show_remove_button = false;        self.is_imported_comment = false;         self.save_button.update(ctx, |button, ctx| {            button.set_label("Comment", ctx);        });        ctx.notify();         self.update_save_button_state(ctx);    }     pub fn save_comment(&mut self, ctx: &mut ViewContext<Self>) {        let comment_text = self.editor.as_ref(ctx).model().as_ref(ctx).markdown(ctx);         if comment_text.trim().is_empty() {            log::debug!("CommentEditor attempted to save empty comment, ignoring");            return;        }         ctx.emit(CommentEditorEvent::CommentSaved {            id: self.comment_id,            comment_text: comment_text.clone(),            line: self.line.clone(),        });        self.reset(ctx);        ctx.emit(CommentEditorEvent::CloseEditor);    }     fn render_github_import_indicator(        &self,        appearance: &Appearance,        background: ColorU,    ) -> Box<dyn Element> {        let theme = appearance.theme();        let sub_text_color = theme.sub_text_color(Fill::Solid(background)).into_solid();        let icon = Icon::Github            .to_warpui_icon(Fill::Solid(sub_text_color))            .finish();         let label = Text::new(            "Comment imported from GitHub".to_string(),            appearance.ui_font_family(),            appearance.ui_font_size(),        )        .soft_wrap(false)        .with_clip(ClipConfig::end())        .with_color(sub_text_color)        .finish();         Flex::row()            .with_cross_axis_alignment(CrossAxisAlignment::Center)            .with_spacing(4.)            .with_child(                ConstrainedBox::new(icon)                    .with_width(14.)                    .with_height(14.)                    .finish(),            )            .with_child(Shrinkable::new(1., label).finish())            .finish()    }     fn render_action_buttons(&self) -> Box<dyn Element> {        let mut action_buttons = vec![ChildView::new(&self.close_button).finish()];        if self.show_remove_button {            action_buttons.push(ChildView::new(&self.remove_button).finish());        }        action_buttons.push(ChildView::new(&self.save_button).finish());         Flex::row()            .with_spacing(4.)            .with_children(action_buttons)            .with_main_axis_alignment(MainAxisAlignment::End)            .finish()    }     fn render_footer_row(&self, appearance: &Appearance, background: ColorU) -> Box<dyn Element> {        let action_buttons = self.render_action_buttons();        let footer_row = Flex::row()            .with_main_axis_size(MainAxisSize::Max)            .with_cross_axis_alignment(CrossAxisAlignment::Center);        if self.is_imported_comment {            footer_row                .with_main_axis_alignment(MainAxisAlignment::SpaceBetween)                .with_child(                    Shrinkable::new(                        1.,                        self.render_github_import_indicator(appearance, background),                    )                    .finish(),                )                .with_child(action_buttons)                .finish()        } else {            footer_row                .with_main_axis_alignment(MainAxisAlignment::End)                .with_child(action_buttons)                .finish()        }    }} impl View for CommentEditor {    fn ui_name() -> &'static str {        "CommentEditor"    }     fn render(&self, ctx: &AppContext) -> Box<dyn Element> {        let appearance = Appearance::handle(ctx).as_ref(ctx);        let theme = appearance.theme();        let background = blended_colors::neutral_2(theme);        let border_color = blended_colors::neutral_4(theme);         let footer_row = self.render_footer_row(appearance, background);         Container::new(            ConstrainedBox::new(                Flex::column()                    .with_child(                        Shrinkable::new(                            1.,                            Container::new(                                Clipped::new(ChildView::new(&self.editor).finish()).finish(),                            )                            .with_padding_bottom(4.)                            .with_padding_top(8.)                            .with_horizontal_padding(12.)                            .finish(),                        )                        .finish(),                    )                    .with_child(                        Container::new(footer_row)                            .with_vertical_padding(4.)                            .with_horizontal_padding(4.)                            .with_border(Border::top(1.).with_border_fill(border_color))                            .finish(),                    )                    .finish(),            )            .with_max_height(200.)            .with_max_width(DEFAULT_COMMENT_MAX_WIDTH)            .finish(),        )        .with_corner_radius(CornerRadius::with_all(Radius::Pixels(8.)))        .with_background_color(background)        .with_border(Border::all(1.).with_border_fill(border_color))        .finish()    }     fn on_focus(&mut self, focus_ctx: &FocusContext, ctx: &mut ViewContext<Self>) {        if focus_ctx.is_self_focused() {            ctx.focus(&self.editor);        }    }} impl TypedActionView for CommentEditor {    type Action = CommentEditorAction;     fn handle_action(&mut self, action: &CommentEditorAction, ctx: &mut ViewContext<Self>) {        match action {            CommentEditorAction::SaveComment => self.save_comment(ctx),            CommentEditorAction::CloseEditor => {                self.reset(ctx);                ctx.emit(CommentEditorEvent::CloseEditor);            }            CommentEditorAction::RemoveComment => {                if let Some(comment_id) = self.comment_id {                    self.reset(ctx);                    ctx.emit(CommentEditorEvent::DeleteComment { id: comment_id });                    ctx.emit(CommentEditorEvent::CloseEditor);                }            }        }    }} impl Entity for CommentEditor {    type Event = CommentEditorEvent;} pub(crate) fn create_editable_comment_markdown_editor<V>(    markdown_content: Option<&str>,    ctx: &mut ViewContext<V>,) -> ViewHandle<RichTextEditorView>where    V: View,{    create_comment_markdown_editor_inner(        markdown_content,        false,        Some(Pixels::new(DEFAULT_COMMENT_MAX_WIDTH)),        ctx,    )} pub(crate) fn create_readonly_comment_markdown_editor<V>(    markdown_content: &str,    disable_scrolling: bool,    max_width: Option<Pixels>,    ctx: &mut ViewContext<V>,) -> ViewHandle<RichTextEditorView>where

Lines 1–500 of 566.