382 lines
13 KiB
Rust
382 lines
13 KiB
Rust
use std::{borrow::Cow, mem, ops::Range, sync::Arc};
|
|
|
|
use string_offset::{ByteOffset, CharOffset};
|
|
use warp_completer::signatures::CommandRegistry;
|
|
use warp_editor::{
|
|
content::{anchor::Anchor, buffer::Buffer, selection_model::BufferSelectionModel},
|
|
editor::EmbeddedItemModel,
|
|
};
|
|
use warp_util::user_input::UserInput;
|
|
use warpui::{
|
|
elements::{
|
|
Align, Border, Container, CrossAxisAlignment, Empty, Flex, MainAxisAlignment,
|
|
MouseStateHandle, ParentElement, Shrinkable,
|
|
},
|
|
platform::Cursor,
|
|
ui_components::{button::ButtonVariant, components::UiComponent},
|
|
AppContext, Element, Entity, ModelAsRef, ModelContext, ModelHandle, SingletonEntity,
|
|
};
|
|
|
|
use crate::{
|
|
appearance::Appearance,
|
|
cloud_object::{model::persistence::CloudModel, CloudObject},
|
|
completer::SessionAgnosticContext,
|
|
notebooks::{
|
|
styles::block_footer_action_button,
|
|
telemetry::{ActionEntrypoint, BlockInfo},
|
|
},
|
|
server::ids::{HashableId, ToServerId},
|
|
settings::FontSettings,
|
|
terminal::input::decorations::{parse_current_commands_and_tokens, ParsedTokensSnapshot},
|
|
themes::theme::AnsiColorIdentifier,
|
|
ui_components::icons::Icon,
|
|
util::bindings::CustomAction,
|
|
workflows::{CloudWorkflow, WorkflowId},
|
|
};
|
|
|
|
use super::{
|
|
embedded_item::EmbeddedWorkflow,
|
|
keys::{custom_action_to_display, NotebookKeybindings},
|
|
model::ChildModelHandle,
|
|
notebook_command::{parsed_token_to_color_style_ranges, transform_ansi_color_to_solid_color},
|
|
rich_text_styles,
|
|
view::EditorViewAction,
|
|
NotebookWorkflow,
|
|
};
|
|
|
|
#[derive(Default)]
|
|
struct MouseStateHandles {
|
|
insert_button_state: MouseStateHandle,
|
|
copy_button_state: MouseStateHandle,
|
|
edit_button_state: MouseStateHandle,
|
|
remove_embedding_button_state: MouseStateHandle,
|
|
}
|
|
|
|
pub struct NotebookEmbed {
|
|
start: Anchor,
|
|
hashed_id: String,
|
|
is_selected: bool,
|
|
content: ModelHandle<Buffer>,
|
|
selection_model: ModelHandle<BufferSelectionModel>,
|
|
mouse_state_handles: MouseStateHandles,
|
|
cached_syntax_color: Option<Vec<(Range<ByteOffset>, AnsiColorIdentifier)>>,
|
|
}
|
|
|
|
impl NotebookEmbed {
|
|
pub fn new(
|
|
start: CharOffset,
|
|
hashed_id: String,
|
|
content: ModelHandle<Buffer>,
|
|
selection_model: ModelHandle<BufferSelectionModel>,
|
|
ctx: &mut ModelContext<Self>,
|
|
) -> Self {
|
|
let start = selection_model.update(ctx, |selection_model, ctx| {
|
|
selection_model.anchor(start, ctx)
|
|
});
|
|
|
|
let embedding = Self {
|
|
start,
|
|
hashed_id,
|
|
content,
|
|
selection_model,
|
|
is_selected: false,
|
|
mouse_state_handles: Default::default(),
|
|
cached_syntax_color: None,
|
|
};
|
|
|
|
embedding.highlight_syntax(ctx);
|
|
embedding
|
|
}
|
|
|
|
pub fn highlight_syntax(&self, ctx: &mut ModelContext<Self>) {
|
|
let completion_context = SessionAgnosticContext::new(CommandRegistry::global_instance());
|
|
if let Some(command) = self
|
|
.maybe_get_workflow(ctx)
|
|
.and_then(|workflow| workflow.model().data.command())
|
|
{
|
|
let command = command.to_string();
|
|
let _ = ctx.spawn(
|
|
async move { parse_current_commands_and_tokens(command, &completion_context).await },
|
|
|notebook_embed, parsed_tokens, ctx| {
|
|
notebook_embed.update_buffer_with_parsed_tokens(parsed_tokens, ctx);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
fn update_buffer_with_parsed_tokens(
|
|
&mut self,
|
|
parsed_tokens: ParsedTokensSnapshot,
|
|
ctx: &mut ModelContext<Self>,
|
|
) {
|
|
let colors = parsed_token_to_color_style_ranges(parsed_tokens.parsed_tokens);
|
|
self.cached_syntax_color = Some(colors.clone());
|
|
|
|
self.update_buffer_with_syntax_color(&colors, ctx);
|
|
}
|
|
|
|
pub fn try_apply_cached_highlighting(&self, ctx: &mut ModelContext<Self>) {
|
|
if let Some(colors) = &self.cached_syntax_color {
|
|
self.update_buffer_with_syntax_color(colors, ctx);
|
|
}
|
|
}
|
|
|
|
fn update_buffer_with_syntax_color(
|
|
&self,
|
|
colors: &[(Range<ByteOffset>, AnsiColorIdentifier)],
|
|
ctx: &mut ModelContext<Self>,
|
|
) {
|
|
let Some(offset) = self.start_offset(ctx) else {
|
|
return;
|
|
};
|
|
|
|
let appearance = Appearance::as_ref(ctx);
|
|
let font_settings = FontSettings::as_ref(ctx);
|
|
let terminal_colors_normal = appearance.theme().terminal_colors().normal.to_owned();
|
|
let background_color = rich_text_styles(appearance, font_settings)
|
|
.embedding_background
|
|
.start_color();
|
|
|
|
self.content.update(ctx, |buffer, ctx| {
|
|
buffer.replace_embedding_at_offset(
|
|
offset,
|
|
Arc::new(
|
|
EmbeddedWorkflow::new(self.hashed_id.clone()).with_syntax_highlighting(
|
|
transform_ansi_color_to_solid_color(
|
|
colors,
|
|
&terminal_colors_normal,
|
|
background_color,
|
|
),
|
|
),
|
|
),
|
|
self.selection_model.clone(),
|
|
ctx,
|
|
)
|
|
});
|
|
}
|
|
|
|
pub fn hashed_id(&self) -> &str {
|
|
self.hashed_id.as_str()
|
|
}
|
|
|
|
pub fn refresh_item_state(&self, ctx: &mut ModelContext<Self>) {
|
|
let Some(offset) = self.start_offset(ctx) else {
|
|
return;
|
|
};
|
|
|
|
self.content.update(ctx, |buffer, ctx| {
|
|
buffer.replace_embedding_at_offset(
|
|
offset,
|
|
Arc::new(EmbeddedWorkflow::new(self.hashed_id.clone())),
|
|
self.selection_model.clone(),
|
|
ctx,
|
|
)
|
|
});
|
|
|
|
// Re-highlight syntax since the command might have changed.
|
|
self.highlight_syntax(ctx);
|
|
}
|
|
|
|
fn maybe_get_workflow<'a>(&self, ctx: &'a AppContext) -> Option<&'a CloudWorkflow> {
|
|
let cloud_model = CloudModel::as_ref(ctx);
|
|
|
|
// Currently we are only supporting embedded workflows. We could support
|
|
// more drive objects in the future.
|
|
let id = WorkflowId::from_hash(&self.hashed_id)?;
|
|
cloud_model
|
|
.get_by_uid(&id.to_server_id().uid())
|
|
.and_then(|object| object.as_any().downcast_ref::<CloudWorkflow>())
|
|
.and_then(|workflow| {
|
|
if workflow.is_trashed(cloud_model) {
|
|
None
|
|
} else {
|
|
Some(workflow)
|
|
}
|
|
})
|
|
}
|
|
|
|
pub fn start_offset(&self, ctx: &impl ModelAsRef) -> Option<CharOffset> {
|
|
self.selection_model.as_ref(ctx).resolve_anchor(&self.start)
|
|
}
|
|
|
|
fn selectable(&self, ctx: &AppContext) -> bool {
|
|
self.maybe_get_workflow(ctx).is_some()
|
|
}
|
|
|
|
fn render_footer_for_workflow(
|
|
&self,
|
|
workflow: &CloudWorkflow,
|
|
appearance: &Appearance,
|
|
ctx: &AppContext,
|
|
) -> Box<dyn Element> {
|
|
let mut footer = Flex::row()
|
|
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
|
.with_main_axis_alignment(MainAxisAlignment::End);
|
|
|
|
let workflow_id = workflow.id;
|
|
let workflow_info = NotebookWorkflow::from_cloud_workflow(Box::new(workflow.clone()));
|
|
let block_info = BlockInfo::EmbeddedWorkflow {
|
|
workflow_id: workflow_id.into_server().map(Into::into),
|
|
team_uid: workflow.permissions.owner.into(),
|
|
};
|
|
|
|
let workflow_content = workflow.model().data.content().to_owned();
|
|
footer.add_child(Shrinkable::new(1.0, Empty::new().finish()).finish());
|
|
footer.add_child(
|
|
Align::new(
|
|
block_footer_action_button(
|
|
appearance,
|
|
Icon::Pencil,
|
|
self.mouse_state_handles.edit_button_state.clone(),
|
|
"Edit",
|
|
None,
|
|
)
|
|
.on_click(move |ctx, _, _| {
|
|
ctx.dispatch_typed_action(EditorViewAction::EditWorkflow(workflow_id));
|
|
})
|
|
.finish(),
|
|
)
|
|
.right()
|
|
.finish(),
|
|
);
|
|
|
|
footer.add_child(
|
|
Align::new(
|
|
block_footer_action_button(
|
|
appearance,
|
|
Icon::Copy,
|
|
self.mouse_state_handles.copy_button_state.clone(),
|
|
"Copy",
|
|
custom_action_to_display(CustomAction::Copy),
|
|
)
|
|
.on_click(move |ctx, _, _| {
|
|
ctx.dispatch_typed_action(EditorViewAction::CopyTextToClipboard {
|
|
text: UserInput::new(workflow_content.clone()),
|
|
block: block_info,
|
|
entrypoint: ActionEntrypoint::Button,
|
|
});
|
|
})
|
|
.finish(),
|
|
)
|
|
.right()
|
|
.finish(),
|
|
);
|
|
|
|
footer.add_child(
|
|
Align::new(
|
|
block_footer_action_button(
|
|
appearance,
|
|
Icon::TerminalInput,
|
|
self.mouse_state_handles.insert_button_state.clone(),
|
|
"Run in terminal",
|
|
NotebookKeybindings::as_ref(ctx).run_commands_keybinding(),
|
|
)
|
|
.on_click(move |ctx, _, _| {
|
|
ctx.dispatch_typed_action(EditorViewAction::RunWorkflow(workflow_info.clone()));
|
|
})
|
|
.finish(),
|
|
)
|
|
.right()
|
|
.finish(),
|
|
);
|
|
footer.finish()
|
|
}
|
|
}
|
|
|
|
impl Entity for NotebookEmbed {
|
|
type Event = ();
|
|
}
|
|
|
|
impl EmbeddedItemModel for NotebookEmbed {
|
|
fn render_item_footer(&self, ctx: &AppContext) -> Option<Box<dyn Element>> {
|
|
// Currently we are only supporting embedded workflows. We could support
|
|
// more drive objects in the future.
|
|
let workflow = self.maybe_get_workflow(ctx);
|
|
let appearance = Appearance::as_ref(ctx);
|
|
|
|
workflow.map(|workflow| self.render_footer_for_workflow(workflow, appearance, ctx))
|
|
}
|
|
|
|
fn border(&self, app: &AppContext) -> Option<Border> {
|
|
if self.is_selected {
|
|
let border_fill = Appearance::as_ref(app).theme().accent();
|
|
Some(Border::all(3.).with_border_fill(border_fill))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn render_remove_embedding_button(&self, ctx: &AppContext) -> Option<Box<dyn Element>> {
|
|
let appearance = Appearance::as_ref(ctx);
|
|
let offset = self.start_offset(ctx)?;
|
|
Some(
|
|
Container::new(
|
|
appearance
|
|
.ui_builder()
|
|
.button(
|
|
ButtonVariant::Text,
|
|
self.mouse_state_handles
|
|
.remove_embedding_button_state
|
|
.clone(),
|
|
)
|
|
.with_text_label("Remove".to_string())
|
|
.build()
|
|
.with_cursor(Cursor::Arrow)
|
|
.on_click(move |ctx, _, _| {
|
|
ctx.dispatch_typed_action(EditorViewAction::RemoveEmbeddingAt(offset));
|
|
})
|
|
.finish(),
|
|
)
|
|
.with_margin_right(12.)
|
|
.finish(),
|
|
)
|
|
}
|
|
}
|
|
|
|
impl ChildModelHandle for ModelHandle<NotebookEmbed> {
|
|
fn start_offset(&self, app: &AppContext) -> Option<CharOffset> {
|
|
self.as_ref(app).start_offset(app)
|
|
}
|
|
|
|
fn end_offset(&self, app: &AppContext) -> Option<CharOffset> {
|
|
// Embedding should always take one character offset.
|
|
self.as_ref(app).start_offset(app).map(|offset| offset + 1)
|
|
}
|
|
|
|
fn selectable(&self, app: &AppContext) -> bool {
|
|
self.as_ref(app).selectable(app)
|
|
}
|
|
|
|
fn as_any(&self) -> &dyn std::any::Any {
|
|
self
|
|
}
|
|
|
|
fn executable_workflow(&self, app: &AppContext) -> Option<NotebookWorkflow> {
|
|
// Currently we are only supporting embedded workflows. We could support
|
|
// more drive objects in the future.
|
|
self.as_ref(app)
|
|
.maybe_get_workflow(app)
|
|
.map(|workflow| NotebookWorkflow::from_cloud_workflow(Box::new(workflow.clone())))
|
|
}
|
|
|
|
fn executable_command<'a>(&'a self, app: &'a AppContext) -> Option<Cow<'a, str>> {
|
|
self.as_ref(app)
|
|
.maybe_get_workflow(app)
|
|
.map(|workflow| workflow.model().data.content().into())
|
|
}
|
|
|
|
fn selected(&self, app: &AppContext) -> bool {
|
|
self.as_ref(app).is_selected
|
|
}
|
|
|
|
fn set_selected(&self, selected: bool, ctx: &mut AppContext) -> bool {
|
|
self.update(ctx, |model, _ctx| {
|
|
mem::replace(&mut model.is_selected, selected)
|
|
})
|
|
}
|
|
|
|
fn clone_boxed(&self) -> Box<dyn ChildModelHandle> {
|
|
Box::new(self.clone())
|
|
}
|
|
}
|