assistant: Polish /workflow and steps UI (#15936)
Fixes #15923 Release Notes: - Assistant workflow steps can now be applied and reverted directly from within the assistant panel. --------- Co-authored-by: Antonio Scandurra <me@as-cii.com> Co-authored-by: Antonio <antonio@zed.dev>
This commit is contained in:
parent
514b79e461
commit
73fb8277fc
15 changed files with 1157 additions and 450 deletions
1
assets/icons/undo.svg
Normal file
1
assets/icons/undo.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-undo"><path d="M3 7v6h6"/><path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"/></svg>
|
After Width: | Height: | Size: 288 B |
|
@ -53,7 +53,7 @@ actions!(
|
||||||
DeployPromptLibrary,
|
DeployPromptLibrary,
|
||||||
ConfirmCommand,
|
ConfirmCommand,
|
||||||
ToggleModelSelector,
|
ToggleModelSelector,
|
||||||
DebugEditSteps
|
DebugWorkflowSteps
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -284,7 +284,8 @@ pub enum ContextEvent {
|
||||||
AssistError(String),
|
AssistError(String),
|
||||||
MessagesEdited,
|
MessagesEdited,
|
||||||
SummaryChanged,
|
SummaryChanged,
|
||||||
WorkflowStepsChanged,
|
WorkflowStepsRemoved(Vec<Range<language::Anchor>>),
|
||||||
|
WorkflowStepUpdated(Range<language::Anchor>),
|
||||||
StreamedCompletion,
|
StreamedCompletion,
|
||||||
PendingSlashCommandsUpdated {
|
PendingSlashCommandsUpdated {
|
||||||
removed: Vec<Range<language::Anchor>>,
|
removed: Vec<Range<language::Anchor>>,
|
||||||
|
@ -360,22 +361,17 @@ pub struct ResolvedWorkflowStep {
|
||||||
pub enum WorkflowStepStatus {
|
pub enum WorkflowStepStatus {
|
||||||
Pending(Task<Option<()>>),
|
Pending(Task<Option<()>>),
|
||||||
Resolved(ResolvedWorkflowStep),
|
Resolved(ResolvedWorkflowStep),
|
||||||
|
Error(Arc<anyhow::Error>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WorkflowStepStatus {
|
impl WorkflowStepStatus {
|
||||||
pub fn as_resolved(&self) -> Option<&ResolvedWorkflowStep> {
|
pub fn into_resolved(&self) -> Option<Result<ResolvedWorkflowStep, Arc<anyhow::Error>>> {
|
||||||
match self {
|
match self {
|
||||||
WorkflowStepStatus::Resolved(suggestions) => Some(suggestions),
|
WorkflowStepStatus::Resolved(resolved) => Some(Ok(resolved.clone())),
|
||||||
|
WorkflowStepStatus::Error(error) => Some(Err(error.clone())),
|
||||||
WorkflowStepStatus::Pending(_) => None,
|
WorkflowStepStatus::Pending(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_resolved(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
WorkflowStepStatus::Resolved(_) => true,
|
|
||||||
WorkflowStepStatus::Pending(_) => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
@ -583,12 +579,16 @@ impl WorkflowSuggestion {
|
||||||
impl Debug for WorkflowStepStatus {
|
impl Debug for WorkflowStepStatus {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
WorkflowStepStatus::Pending(_) => write!(f, "EditStepOperations::Pending"),
|
WorkflowStepStatus::Pending(_) => write!(f, "WorkflowStepStatus::Pending"),
|
||||||
WorkflowStepStatus::Resolved(ResolvedWorkflowStep { title, suggestions }) => f
|
WorkflowStepStatus::Resolved(ResolvedWorkflowStep { title, suggestions }) => f
|
||||||
.debug_struct("EditStepOperations::Parsed")
|
.debug_struct("WorkflowStepStatus::Resolved")
|
||||||
.field("title", title)
|
.field("title", title)
|
||||||
.field("suggestions", suggestions)
|
.field("suggestions", suggestions)
|
||||||
.finish(),
|
.finish(),
|
||||||
|
WorkflowStepStatus::Error(error) => f
|
||||||
|
.debug_tuple("WorkflowStepStatus::Error")
|
||||||
|
.field(error)
|
||||||
|
.finish(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1058,7 +1058,7 @@ impl Context {
|
||||||
language::Event::Edited => {
|
language::Event::Edited => {
|
||||||
self.count_remaining_tokens(cx);
|
self.count_remaining_tokens(cx);
|
||||||
self.reparse_slash_commands(cx);
|
self.reparse_slash_commands(cx);
|
||||||
self.prune_invalid_edit_steps(cx);
|
self.prune_invalid_workflow_steps(cx);
|
||||||
cx.emit(ContextEvent::MessagesEdited);
|
cx.emit(ContextEvent::MessagesEdited);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
@ -1165,46 +1165,59 @@ impl Context {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prune_invalid_edit_steps(&mut self, cx: &mut ModelContext<Self>) {
|
fn prune_invalid_workflow_steps(&mut self, cx: &mut ModelContext<Self>) {
|
||||||
let buffer = self.buffer.read(cx);
|
let buffer = self.buffer.read(cx);
|
||||||
let prev_len = self.workflow_steps.len();
|
let prev_len = self.workflow_steps.len();
|
||||||
|
let mut removed = Vec::new();
|
||||||
self.workflow_steps.retain(|step| {
|
self.workflow_steps.retain(|step| {
|
||||||
step.tagged_range.start.is_valid(buffer) && step.tagged_range.end.is_valid(buffer)
|
if step.tagged_range.start.is_valid(buffer) && step.tagged_range.end.is_valid(buffer) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
removed.push(step.tagged_range.clone());
|
||||||
|
false
|
||||||
|
}
|
||||||
});
|
});
|
||||||
if self.workflow_steps.len() != prev_len {
|
if self.workflow_steps.len() != prev_len {
|
||||||
cx.emit(ContextEvent::WorkflowStepsChanged);
|
cx.emit(ContextEvent::WorkflowStepsRemoved(removed));
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_edit_steps_in_range(
|
fn parse_workflow_steps_in_range(
|
||||||
&mut self,
|
&mut self,
|
||||||
range: Range<usize>,
|
range: Range<usize>,
|
||||||
project: Model<Project>,
|
project: Model<Project>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) {
|
) {
|
||||||
let mut new_edit_steps = Vec::new();
|
let mut new_edit_steps = Vec::new();
|
||||||
|
let mut edits = Vec::new();
|
||||||
|
|
||||||
let buffer = self.buffer.read(cx).snapshot();
|
let buffer = self.buffer.read(cx).snapshot();
|
||||||
let mut message_lines = buffer.as_rope().chunks_in_range(range).lines();
|
let mut message_lines = buffer.as_rope().chunks_in_range(range).lines();
|
||||||
let mut in_step = false;
|
let mut in_step = false;
|
||||||
let mut step_start = 0;
|
let mut step_open_tag_start_ix = 0;
|
||||||
let mut line_start_offset = message_lines.offset();
|
let mut line_start_offset = message_lines.offset();
|
||||||
|
|
||||||
while let Some(line) = message_lines.next() {
|
while let Some(line) = message_lines.next() {
|
||||||
if let Some(step_start_index) = line.find("<step>") {
|
if let Some(step_start_index) = line.find("<step>") {
|
||||||
if !in_step {
|
if !in_step {
|
||||||
in_step = true;
|
in_step = true;
|
||||||
step_start = line_start_offset + step_start_index;
|
step_open_tag_start_ix = line_start_offset + step_start_index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(step_end_index) = line.find("</step>") {
|
if let Some(step_end_index) = line.find("</step>") {
|
||||||
if in_step {
|
if in_step {
|
||||||
let start_anchor = buffer.anchor_after(step_start);
|
let step_open_tag_end_ix = step_open_tag_start_ix + "<step>".len();
|
||||||
let end_anchor =
|
let mut step_end_tag_start_ix = line_start_offset + step_end_index;
|
||||||
buffer.anchor_before(line_start_offset + step_end_index + "</step>".len());
|
let step_end_tag_end_ix = step_end_tag_start_ix + "</step>".len();
|
||||||
let tagged_range = start_anchor..end_anchor;
|
if buffer.reversed_chars_at(step_end_tag_start_ix).next() == Some('\n') {
|
||||||
|
step_end_tag_start_ix -= 1;
|
||||||
|
}
|
||||||
|
edits.push((step_open_tag_start_ix..step_open_tag_end_ix, ""));
|
||||||
|
edits.push((step_end_tag_start_ix..step_end_tag_end_ix, ""));
|
||||||
|
let tagged_range = buffer.anchor_after(step_open_tag_end_ix)
|
||||||
|
..buffer.anchor_before(step_end_tag_start_ix);
|
||||||
|
|
||||||
// Check if a step with the same range already exists
|
// Check if a step with the same range already exists
|
||||||
let existing_step_index = self
|
let existing_step_index = self
|
||||||
|
@ -1212,14 +1225,11 @@ impl Context {
|
||||||
.binary_search_by(|probe| probe.tagged_range.cmp(&tagged_range, &buffer));
|
.binary_search_by(|probe| probe.tagged_range.cmp(&tagged_range, &buffer));
|
||||||
|
|
||||||
if let Err(ix) = existing_step_index {
|
if let Err(ix) = existing_step_index {
|
||||||
// Step doesn't exist, so add it
|
|
||||||
let task =
|
|
||||||
self.resolve_workflow_step(tagged_range.clone(), project.clone(), cx);
|
|
||||||
new_edit_steps.push((
|
new_edit_steps.push((
|
||||||
ix,
|
ix,
|
||||||
WorkflowStep {
|
WorkflowStep {
|
||||||
tagged_range,
|
tagged_range,
|
||||||
status: WorkflowStepStatus::Pending(task),
|
status: WorkflowStepStatus::Pending(Task::ready(None)),
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -1231,144 +1241,176 @@ impl Context {
|
||||||
line_start_offset = message_lines.offset();
|
line_start_offset = message_lines.offset();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert new steps and generate their corresponding tasks
|
let mut updated = Vec::new();
|
||||||
for (index, step) in new_edit_steps.into_iter().rev() {
|
for (index, step) in new_edit_steps.into_iter().rev() {
|
||||||
|
let step_range = step.tagged_range.clone();
|
||||||
|
updated.push(step_range.clone());
|
||||||
self.workflow_steps.insert(index, step);
|
self.workflow_steps.insert(index, step);
|
||||||
|
self.resolve_workflow_step(step_range, project.clone(), cx);
|
||||||
}
|
}
|
||||||
|
self.buffer
|
||||||
cx.emit(ContextEvent::WorkflowStepsChanged);
|
.update(cx, |buffer, cx| buffer.edit(edits, None, cx));
|
||||||
cx.notify();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_workflow_step(
|
pub fn resolve_workflow_step(
|
||||||
&self,
|
&mut self,
|
||||||
tagged_range: Range<language::Anchor>,
|
tagged_range: Range<language::Anchor>,
|
||||||
project: Model<Project>,
|
project: Model<Project>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Option<()>> {
|
) {
|
||||||
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
|
let Ok(step_index) = self
|
||||||
return Task::ready(Err(anyhow!("no active model")).log_err());
|
.workflow_steps
|
||||||
|
.binary_search_by(|step| step.tagged_range.cmp(&tagged_range, self.buffer.read(cx)))
|
||||||
|
else {
|
||||||
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut request = self.to_completion_request(cx);
|
let mut request = self.to_completion_request(cx);
|
||||||
let step_text = self
|
let Some(edit_step) = self.workflow_steps.get_mut(step_index) else {
|
||||||
.buffer
|
return;
|
||||||
.read(cx)
|
};
|
||||||
.text_for_range(tagged_range.clone())
|
|
||||||
.collect::<String>();
|
|
||||||
|
|
||||||
cx.spawn(|this, mut cx| {
|
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
|
||||||
async move {
|
let step_text = self
|
||||||
let mut prompt = this.update(&mut cx, |this, _| {
|
.buffer
|
||||||
this.prompt_builder.generate_step_resolution_prompt()
|
.read(cx)
|
||||||
})??;
|
.text_for_range(tagged_range.clone())
|
||||||
prompt.push_str(&step_text);
|
.collect::<String>();
|
||||||
|
|
||||||
request.messages.push(LanguageModelRequestMessage {
|
let tagged_range = tagged_range.clone();
|
||||||
role: Role::User,
|
edit_step.status = WorkflowStepStatus::Pending(cx.spawn(|this, mut cx| {
|
||||||
content: prompt,
|
async move {
|
||||||
});
|
let result = async {
|
||||||
|
let mut prompt = this.update(&mut cx, |this, _| {
|
||||||
|
this.prompt_builder.generate_step_resolution_prompt()
|
||||||
|
})??;
|
||||||
|
prompt.push_str(&step_text);
|
||||||
|
|
||||||
// Invoke the model to get its edit suggestions for this workflow step.
|
request.messages.push(LanguageModelRequestMessage {
|
||||||
let resolution = model
|
role: Role::User,
|
||||||
.use_tool::<tool::WorkflowStepResolution>(request, &cx)
|
content: prompt,
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Translate the parsed suggestions to our internal types, which anchor the suggestions to locations in the code.
|
|
||||||
let suggestion_tasks: Vec<_> = resolution
|
|
||||||
.suggestions
|
|
||||||
.iter()
|
|
||||||
.map(|suggestion| suggestion.resolve(project.clone(), cx.clone()))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Expand the context ranges of each suggestion and group suggestions with overlapping context ranges.
|
|
||||||
let suggestions = future::join_all(suggestion_tasks)
|
|
||||||
.await
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|task| task.log_err())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let mut suggestions_by_buffer = HashMap::default();
|
|
||||||
for (buffer, suggestion) in suggestions {
|
|
||||||
suggestions_by_buffer
|
|
||||||
.entry(buffer)
|
|
||||||
.or_insert_with(Vec::new)
|
|
||||||
.push(suggestion);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut suggestion_groups_by_buffer = HashMap::default();
|
|
||||||
for (buffer, mut suggestions) in suggestions_by_buffer {
|
|
||||||
let mut suggestion_groups = Vec::<WorkflowSuggestionGroup>::new();
|
|
||||||
let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
|
|
||||||
// Sort suggestions by their range so that earlier, larger ranges come first
|
|
||||||
suggestions.sort_by(|a, b| a.range().cmp(&b.range(), &snapshot));
|
|
||||||
|
|
||||||
// Merge overlapping suggestions
|
|
||||||
suggestions.dedup_by(|a, b| b.try_merge(&a, &snapshot));
|
|
||||||
|
|
||||||
// Create context ranges for each suggestion
|
|
||||||
for suggestion in suggestions {
|
|
||||||
let context_range = {
|
|
||||||
let suggestion_point_range = suggestion.range().to_point(&snapshot);
|
|
||||||
let start_row = suggestion_point_range.start.row.saturating_sub(5);
|
|
||||||
let end_row = cmp::min(
|
|
||||||
suggestion_point_range.end.row + 5,
|
|
||||||
snapshot.max_point().row,
|
|
||||||
);
|
|
||||||
let start = snapshot.anchor_before(Point::new(start_row, 0));
|
|
||||||
let end = snapshot
|
|
||||||
.anchor_after(Point::new(end_row, snapshot.line_len(end_row)));
|
|
||||||
start..end
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(last_group) = suggestion_groups.last_mut() {
|
|
||||||
if last_group
|
|
||||||
.context_range
|
|
||||||
.end
|
|
||||||
.cmp(&context_range.start, &snapshot)
|
|
||||||
.is_ge()
|
|
||||||
{
|
|
||||||
// Merge with the previous group if context ranges overlap
|
|
||||||
last_group.context_range.end = context_range.end;
|
|
||||||
last_group.suggestions.push(suggestion);
|
|
||||||
} else {
|
|
||||||
// Create a new group
|
|
||||||
suggestion_groups.push(WorkflowSuggestionGroup {
|
|
||||||
context_range,
|
|
||||||
suggestions: vec![suggestion],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Create the first group
|
|
||||||
suggestion_groups.push(WorkflowSuggestionGroup {
|
|
||||||
context_range,
|
|
||||||
suggestions: vec![suggestion],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suggestion_groups_by_buffer.insert(buffer, suggestion_groups);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| {
|
|
||||||
let step_index = this
|
|
||||||
.workflow_steps
|
|
||||||
.binary_search_by(|step| {
|
|
||||||
step.tagged_range.cmp(&tagged_range, this.buffer.read(cx))
|
|
||||||
})
|
|
||||||
.map_err(|_| anyhow!("edit step not found"))?;
|
|
||||||
if let Some(edit_step) = this.workflow_steps.get_mut(step_index) {
|
|
||||||
edit_step.status = WorkflowStepStatus::Resolved(ResolvedWorkflowStep {
|
|
||||||
title: resolution.step_title,
|
|
||||||
suggestions: suggestion_groups_by_buffer,
|
|
||||||
});
|
});
|
||||||
cx.emit(ContextEvent::WorkflowStepsChanged);
|
|
||||||
}
|
// Invoke the model to get its edit suggestions for this workflow step.
|
||||||
anyhow::Ok(())
|
let resolution = model
|
||||||
})?
|
.use_tool::<tool::WorkflowStepResolution>(request, &cx)
|
||||||
}
|
.await?;
|
||||||
.log_err()
|
|
||||||
})
|
// Translate the parsed suggestions to our internal types, which anchor the suggestions to locations in the code.
|
||||||
|
let suggestion_tasks: Vec<_> = resolution
|
||||||
|
.suggestions
|
||||||
|
.iter()
|
||||||
|
.map(|suggestion| suggestion.resolve(project.clone(), cx.clone()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Expand the context ranges of each suggestion and group suggestions with overlapping context ranges.
|
||||||
|
let suggestions = future::join_all(suggestion_tasks)
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|task| task.log_err())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let mut suggestions_by_buffer = HashMap::default();
|
||||||
|
for (buffer, suggestion) in suggestions {
|
||||||
|
suggestions_by_buffer
|
||||||
|
.entry(buffer)
|
||||||
|
.or_insert_with(Vec::new)
|
||||||
|
.push(suggestion);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut suggestion_groups_by_buffer = HashMap::default();
|
||||||
|
for (buffer, mut suggestions) in suggestions_by_buffer {
|
||||||
|
let mut suggestion_groups = Vec::<WorkflowSuggestionGroup>::new();
|
||||||
|
let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
|
||||||
|
// Sort suggestions by their range so that earlier, larger ranges come first
|
||||||
|
suggestions.sort_by(|a, b| a.range().cmp(&b.range(), &snapshot));
|
||||||
|
|
||||||
|
// Merge overlapping suggestions
|
||||||
|
suggestions.dedup_by(|a, b| b.try_merge(&a, &snapshot));
|
||||||
|
|
||||||
|
// Create context ranges for each suggestion
|
||||||
|
for suggestion in suggestions {
|
||||||
|
let context_range = {
|
||||||
|
let suggestion_point_range =
|
||||||
|
suggestion.range().to_point(&snapshot);
|
||||||
|
let start_row =
|
||||||
|
suggestion_point_range.start.row.saturating_sub(5);
|
||||||
|
let end_row = cmp::min(
|
||||||
|
suggestion_point_range.end.row + 5,
|
||||||
|
snapshot.max_point().row,
|
||||||
|
);
|
||||||
|
let start = snapshot.anchor_before(Point::new(start_row, 0));
|
||||||
|
let end = snapshot.anchor_after(Point::new(
|
||||||
|
end_row,
|
||||||
|
snapshot.line_len(end_row),
|
||||||
|
));
|
||||||
|
start..end
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(last_group) = suggestion_groups.last_mut() {
|
||||||
|
if last_group
|
||||||
|
.context_range
|
||||||
|
.end
|
||||||
|
.cmp(&context_range.start, &snapshot)
|
||||||
|
.is_ge()
|
||||||
|
{
|
||||||
|
// Merge with the previous group if context ranges overlap
|
||||||
|
last_group.context_range.end = context_range.end;
|
||||||
|
last_group.suggestions.push(suggestion);
|
||||||
|
} else {
|
||||||
|
// Create a new group
|
||||||
|
suggestion_groups.push(WorkflowSuggestionGroup {
|
||||||
|
context_range,
|
||||||
|
suggestions: vec![suggestion],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Create the first group
|
||||||
|
suggestion_groups.push(WorkflowSuggestionGroup {
|
||||||
|
context_range,
|
||||||
|
suggestions: vec![suggestion],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suggestion_groups_by_buffer.insert(buffer, suggestion_groups);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((resolution.step_title, suggestion_groups_by_buffer))
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = result.await;
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
let step_index = this
|
||||||
|
.workflow_steps
|
||||||
|
.binary_search_by(|step| {
|
||||||
|
step.tagged_range.cmp(&tagged_range, this.buffer.read(cx))
|
||||||
|
})
|
||||||
|
.map_err(|_| anyhow!("edit step not found"))?;
|
||||||
|
if let Some(edit_step) = this.workflow_steps.get_mut(step_index) {
|
||||||
|
edit_step.status = match result {
|
||||||
|
Ok((title, suggestions)) => {
|
||||||
|
WorkflowStepStatus::Resolved(ResolvedWorkflowStep {
|
||||||
|
title,
|
||||||
|
suggestions,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(error) => WorkflowStepStatus::Error(Arc::new(error)),
|
||||||
|
};
|
||||||
|
cx.emit(ContextEvent::WorkflowStepUpdated(tagged_range));
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
anyhow::Ok(())
|
||||||
|
})?
|
||||||
|
}
|
||||||
|
.log_err()
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
edit_step.status = WorkflowStepStatus::Error(Arc::new(anyhow!("no active model")));
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.emit(ContextEvent::WorkflowStepUpdated(tagged_range));
|
||||||
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pending_command_for_position(
|
pub fn pending_command_for_position(
|
||||||
|
@ -1587,7 +1629,7 @@ impl Context {
|
||||||
message_start_offset..message_new_end_offset
|
message_start_offset..message_new_end_offset
|
||||||
});
|
});
|
||||||
if let Some(project) = this.project.clone() {
|
if let Some(project) = this.project.clone() {
|
||||||
this.parse_edit_steps_in_range(message_range, project, cx);
|
this.parse_workflow_steps_in_range(message_range, project, cx);
|
||||||
}
|
}
|
||||||
cx.emit(ContextEvent::StreamedCompletion);
|
cx.emit(ContextEvent::StreamedCompletion);
|
||||||
|
|
||||||
|
@ -3011,13 +3053,13 @@ mod tests {
|
||||||
vec![
|
vec![
|
||||||
(
|
(
|
||||||
Point::new(response_start_row + 2, 0)
|
Point::new(response_start_row + 2, 0)
|
||||||
..Point::new(response_start_row + 14, 7),
|
..Point::new(response_start_row + 13, 3),
|
||||||
WorkflowStepEditSuggestionStatus::Pending
|
WorkflowStepTestStatus::Pending
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
Point::new(response_start_row + 16, 0)
|
Point::new(response_start_row + 15, 0)
|
||||||
..Point::new(response_start_row + 28, 7),
|
..Point::new(response_start_row + 26, 3),
|
||||||
WorkflowStepEditSuggestionStatus::Pending
|
WorkflowStepTestStatus::Pending
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
@ -3041,45 +3083,45 @@ mod tests {
|
||||||
// Wait for tool use to be processed.
|
// Wait for tool use to be processed.
|
||||||
cx.run_until_parked();
|
cx.run_until_parked();
|
||||||
|
|
||||||
// Verify that the last edit step is not pending anymore.
|
// Verify that the first edit step is not pending anymore.
|
||||||
context.read_with(cx, |context, cx| {
|
context.read_with(cx, |context, cx| {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
workflow_steps(context, cx),
|
workflow_steps(context, cx),
|
||||||
vec![
|
vec![
|
||||||
(
|
(
|
||||||
Point::new(response_start_row + 2, 0)
|
Point::new(response_start_row + 2, 0)
|
||||||
..Point::new(response_start_row + 14, 7),
|
..Point::new(response_start_row + 13, 3),
|
||||||
WorkflowStepEditSuggestionStatus::Pending
|
WorkflowStepTestStatus::Resolved
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
Point::new(response_start_row + 16, 0)
|
Point::new(response_start_row + 15, 0)
|
||||||
..Point::new(response_start_row + 28, 7),
|
..Point::new(response_start_row + 26, 3),
|
||||||
WorkflowStepEditSuggestionStatus::Resolved
|
WorkflowStepTestStatus::Pending
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
enum WorkflowStepEditSuggestionStatus {
|
enum WorkflowStepTestStatus {
|
||||||
Pending,
|
Pending,
|
||||||
Resolved,
|
Resolved,
|
||||||
|
Error,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn workflow_steps(
|
fn workflow_steps(
|
||||||
context: &Context,
|
context: &Context,
|
||||||
cx: &AppContext,
|
cx: &AppContext,
|
||||||
) -> Vec<(Range<Point>, WorkflowStepEditSuggestionStatus)> {
|
) -> Vec<(Range<Point>, WorkflowStepTestStatus)> {
|
||||||
context
|
context
|
||||||
.workflow_steps
|
.workflow_steps
|
||||||
.iter()
|
.iter()
|
||||||
.map(|step| {
|
.map(|step| {
|
||||||
let buffer = context.buffer.read(cx);
|
let buffer = context.buffer.read(cx);
|
||||||
let status = match &step.status {
|
let status = match &step.status {
|
||||||
WorkflowStepStatus::Pending(_) => WorkflowStepEditSuggestionStatus::Pending,
|
WorkflowStepStatus::Pending(_) => WorkflowStepTestStatus::Pending,
|
||||||
WorkflowStepStatus::Resolved { .. } => {
|
WorkflowStepStatus::Resolved { .. } => WorkflowStepTestStatus::Resolved,
|
||||||
WorkflowStepEditSuggestionStatus::Resolved
|
WorkflowStepStatus::Error(_) => WorkflowStepTestStatus::Error,
|
||||||
}
|
|
||||||
};
|
};
|
||||||
(step.tagged_range.to_point(buffer), status)
|
(step.tagged_range.to_point(buffer), status)
|
||||||
})
|
})
|
||||||
|
|
|
@ -68,6 +68,9 @@ pub struct InlineAssistant {
|
||||||
assists: HashMap<InlineAssistId, InlineAssist>,
|
assists: HashMap<InlineAssistId, InlineAssist>,
|
||||||
assists_by_editor: HashMap<WeakView<Editor>, EditorInlineAssists>,
|
assists_by_editor: HashMap<WeakView<Editor>, EditorInlineAssists>,
|
||||||
assist_groups: HashMap<InlineAssistGroupId, InlineAssistGroup>,
|
assist_groups: HashMap<InlineAssistGroupId, InlineAssistGroup>,
|
||||||
|
assist_observations:
|
||||||
|
HashMap<InlineAssistId, (async_watch::Sender<()>, async_watch::Receiver<()>)>,
|
||||||
|
confirmed_assists: HashMap<InlineAssistId, Model<Codegen>>,
|
||||||
prompt_history: VecDeque<String>,
|
prompt_history: VecDeque<String>,
|
||||||
prompt_builder: Arc<PromptBuilder>,
|
prompt_builder: Arc<PromptBuilder>,
|
||||||
telemetry: Option<Arc<Telemetry>>,
|
telemetry: Option<Arc<Telemetry>>,
|
||||||
|
@ -88,6 +91,8 @@ impl InlineAssistant {
|
||||||
assists: HashMap::default(),
|
assists: HashMap::default(),
|
||||||
assists_by_editor: HashMap::default(),
|
assists_by_editor: HashMap::default(),
|
||||||
assist_groups: HashMap::default(),
|
assist_groups: HashMap::default(),
|
||||||
|
assist_observations: HashMap::default(),
|
||||||
|
confirmed_assists: HashMap::default(),
|
||||||
prompt_history: VecDeque::default(),
|
prompt_history: VecDeque::default(),
|
||||||
prompt_builder,
|
prompt_builder,
|
||||||
telemetry: Some(telemetry),
|
telemetry: Some(telemetry),
|
||||||
|
@ -343,6 +348,7 @@ impl InlineAssistant {
|
||||||
height: prompt_editor_height,
|
height: prompt_editor_height,
|
||||||
render: build_assist_editor_renderer(prompt_editor),
|
render: build_assist_editor_renderer(prompt_editor),
|
||||||
disposition: BlockDisposition::Above,
|
disposition: BlockDisposition::Above,
|
||||||
|
priority: 0,
|
||||||
},
|
},
|
||||||
BlockProperties {
|
BlockProperties {
|
||||||
style: BlockStyle::Sticky,
|
style: BlockStyle::Sticky,
|
||||||
|
@ -357,6 +363,7 @@ impl InlineAssistant {
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}),
|
}),
|
||||||
disposition: BlockDisposition::Below,
|
disposition: BlockDisposition::Below,
|
||||||
|
priority: 0,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -654,8 +661,21 @@ impl InlineAssistant {
|
||||||
|
|
||||||
if undo {
|
if undo {
|
||||||
assist.codegen.update(cx, |codegen, cx| codegen.undo(cx));
|
assist.codegen.update(cx, |codegen, cx| codegen.undo(cx));
|
||||||
|
} else {
|
||||||
|
self.confirmed_assists.insert(assist_id, assist.codegen);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove the assist from the status updates map
|
||||||
|
self.assist_observations.remove(&assist_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn undo_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) -> bool {
|
||||||
|
let Some(codegen) = self.confirmed_assists.remove(&assist_id) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
codegen.update(cx, |this, cx| this.undo(cx));
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dismiss_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) -> bool {
|
fn dismiss_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) -> bool {
|
||||||
|
@ -854,6 +874,10 @@ impl InlineAssistant {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.log_err();
|
.log_err();
|
||||||
|
|
||||||
|
if let Some((tx, _)) = self.assist_observations.get(&assist_id) {
|
||||||
|
tx.send(()).ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stop_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
|
pub fn stop_assist(&mut self, assist_id: InlineAssistId, cx: &mut WindowContext) {
|
||||||
|
@ -864,19 +888,24 @@ impl InlineAssistant {
|
||||||
};
|
};
|
||||||
|
|
||||||
assist.codegen.update(cx, |codegen, cx| codegen.stop(cx));
|
assist.codegen.update(cx, |codegen, cx| codegen.stop(cx));
|
||||||
|
|
||||||
|
if let Some((tx, _)) = self.assist_observations.get(&assist_id) {
|
||||||
|
tx.send(()).ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn status_for_assist(
|
pub fn assist_status(&self, assist_id: InlineAssistId, cx: &AppContext) -> InlineAssistStatus {
|
||||||
&self,
|
if let Some(assist) = self.assists.get(&assist_id) {
|
||||||
assist_id: InlineAssistId,
|
match &assist.codegen.read(cx).status {
|
||||||
cx: &WindowContext,
|
CodegenStatus::Idle => InlineAssistStatus::Idle,
|
||||||
) -> Option<CodegenStatus> {
|
CodegenStatus::Pending => InlineAssistStatus::Pending,
|
||||||
let assist = self.assists.get(&assist_id)?;
|
CodegenStatus::Done => InlineAssistStatus::Done,
|
||||||
match &assist.codegen.read(cx).status {
|
CodegenStatus::Error(_) => InlineAssistStatus::Error,
|
||||||
CodegenStatus::Idle => Some(CodegenStatus::Idle),
|
}
|
||||||
CodegenStatus::Pending => Some(CodegenStatus::Pending),
|
} else if self.confirmed_assists.contains_key(&assist_id) {
|
||||||
CodegenStatus::Done => Some(CodegenStatus::Done),
|
InlineAssistStatus::Confirmed
|
||||||
CodegenStatus::Error(error) => Some(CodegenStatus::Error(anyhow!("{:?}", error))),
|
} else {
|
||||||
|
InlineAssistStatus::Canceled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1051,6 +1080,7 @@ impl InlineAssistant {
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}),
|
}),
|
||||||
disposition: BlockDisposition::Above,
|
disposition: BlockDisposition::Above,
|
||||||
|
priority: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1060,6 +1090,37 @@ impl InlineAssistant {
|
||||||
.collect();
|
.collect();
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn observe_assist(&mut self, assist_id: InlineAssistId) -> async_watch::Receiver<()> {
|
||||||
|
if let Some((_, rx)) = self.assist_observations.get(&assist_id) {
|
||||||
|
rx.clone()
|
||||||
|
} else {
|
||||||
|
let (tx, rx) = async_watch::channel(());
|
||||||
|
self.assist_observations.insert(assist_id, (tx, rx.clone()));
|
||||||
|
rx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum InlineAssistStatus {
|
||||||
|
Idle,
|
||||||
|
Pending,
|
||||||
|
Done,
|
||||||
|
Error,
|
||||||
|
Confirmed,
|
||||||
|
Canceled,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InlineAssistStatus {
|
||||||
|
pub(crate) fn is_pending(&self) -> bool {
|
||||||
|
matches!(self, Self::Pending)
|
||||||
|
}
|
||||||
|
pub(crate) fn is_confirmed(&self) -> bool {
|
||||||
|
matches!(self, Self::Confirmed)
|
||||||
|
}
|
||||||
|
pub(crate) fn is_done(&self) -> bool {
|
||||||
|
matches!(self, Self::Done)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct EditorInlineAssists {
|
struct EditorInlineAssists {
|
||||||
|
@ -1964,6 +2025,8 @@ impl InlineAssist {
|
||||||
|
|
||||||
if assist.decorations.is_none() {
|
if assist.decorations.is_none() {
|
||||||
this.finish_assist(assist_id, false, cx);
|
this.finish_assist(assist_id, false, cx);
|
||||||
|
} else if let Some(tx) = this.assist_observations.get(&assist_id) {
|
||||||
|
tx.0.send(()).ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -2037,7 +2100,7 @@ pub struct Codegen {
|
||||||
builder: Arc<PromptBuilder>,
|
builder: Arc<PromptBuilder>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum CodegenStatus {
|
enum CodegenStatus {
|
||||||
Idle,
|
Idle,
|
||||||
Pending,
|
Pending,
|
||||||
Done,
|
Done,
|
||||||
|
|
|
@ -449,6 +449,7 @@ impl ProjectDiagnosticsEditor {
|
||||||
style: BlockStyle::Sticky,
|
style: BlockStyle::Sticky,
|
||||||
render: diagnostic_header_renderer(primary),
|
render: diagnostic_header_renderer(primary),
|
||||||
disposition: BlockDisposition::Above,
|
disposition: BlockDisposition::Above,
|
||||||
|
priority: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -470,6 +471,7 @@ impl ProjectDiagnosticsEditor {
|
||||||
diagnostic, None, true, true,
|
diagnostic, None, true, true,
|
||||||
),
|
),
|
||||||
disposition: BlockDisposition::Below,
|
disposition: BlockDisposition::Below,
|
||||||
|
priority: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -508,6 +510,7 @@ impl ProjectDiagnosticsEditor {
|
||||||
style: block.style,
|
style: block.style,
|
||||||
render: block.render,
|
render: block.render,
|
||||||
disposition: block.disposition,
|
disposition: block.disposition,
|
||||||
|
priority: 0,
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
Some(Autoscroll::fit()),
|
Some(Autoscroll::fit()),
|
||||||
|
|
|
@ -1281,12 +1281,14 @@ pub mod tests {
|
||||||
position.to_point(&buffer),
|
position.to_point(&buffer),
|
||||||
height
|
height
|
||||||
);
|
);
|
||||||
|
let priority = rng.gen_range(1..100);
|
||||||
BlockProperties {
|
BlockProperties {
|
||||||
style: BlockStyle::Fixed,
|
style: BlockStyle::Fixed,
|
||||||
position,
|
position,
|
||||||
height,
|
height,
|
||||||
disposition,
|
disposition,
|
||||||
render: Box::new(|_| div().into_any()),
|
render: Box::new(|_| div().into_any()),
|
||||||
|
priority: priority,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
|
@ -84,6 +84,7 @@ pub struct CustomBlock {
|
||||||
style: BlockStyle,
|
style: BlockStyle,
|
||||||
render: Arc<Mutex<RenderBlock>>,
|
render: Arc<Mutex<RenderBlock>>,
|
||||||
disposition: BlockDisposition,
|
disposition: BlockDisposition,
|
||||||
|
priority: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct BlockProperties<P> {
|
pub struct BlockProperties<P> {
|
||||||
|
@ -92,6 +93,7 @@ pub struct BlockProperties<P> {
|
||||||
pub style: BlockStyle,
|
pub style: BlockStyle,
|
||||||
pub render: RenderBlock,
|
pub render: RenderBlock,
|
||||||
pub disposition: BlockDisposition,
|
pub disposition: BlockDisposition,
|
||||||
|
pub priority: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<P: Debug> Debug for BlockProperties<P> {
|
impl<P: Debug> Debug for BlockProperties<P> {
|
||||||
|
@ -182,6 +184,7 @@ pub(crate) enum BlockType {
|
||||||
pub(crate) trait BlockLike {
|
pub(crate) trait BlockLike {
|
||||||
fn block_type(&self) -> BlockType;
|
fn block_type(&self) -> BlockType;
|
||||||
fn disposition(&self) -> BlockDisposition;
|
fn disposition(&self) -> BlockDisposition;
|
||||||
|
fn priority(&self) -> usize;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
@ -215,6 +218,14 @@ impl BlockLike for Block {
|
||||||
fn disposition(&self) -> BlockDisposition {
|
fn disposition(&self) -> BlockDisposition {
|
||||||
self.disposition()
|
self.disposition()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn priority(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
Block::Custom(block) => block.priority,
|
||||||
|
Block::ExcerptHeader { .. } => usize::MAX,
|
||||||
|
Block::ExcerptFooter { .. } => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Block {
|
impl Block {
|
||||||
|
@ -660,7 +671,10 @@ impl BlockMap {
|
||||||
(BlockType::Header, BlockType::Header) => Ordering::Equal,
|
(BlockType::Header, BlockType::Header) => Ordering::Equal,
|
||||||
(BlockType::Header, _) => Ordering::Less,
|
(BlockType::Header, _) => Ordering::Less,
|
||||||
(_, BlockType::Header) => Ordering::Greater,
|
(_, BlockType::Header) => Ordering::Greater,
|
||||||
(BlockType::Custom(a_id), BlockType::Custom(b_id)) => a_id.cmp(&b_id),
|
(BlockType::Custom(a_id), BlockType::Custom(b_id)) => block_b
|
||||||
|
.priority()
|
||||||
|
.cmp(&block_a.priority())
|
||||||
|
.then_with(|| a_id.cmp(&b_id)),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
@ -802,6 +816,7 @@ impl<'a> BlockMapWriter<'a> {
|
||||||
render: Arc::new(Mutex::new(block.render)),
|
render: Arc::new(Mutex::new(block.render)),
|
||||||
disposition: block.disposition,
|
disposition: block.disposition,
|
||||||
style: block.style,
|
style: block.style,
|
||||||
|
priority: block.priority,
|
||||||
});
|
});
|
||||||
self.0.custom_blocks.insert(block_ix, new_block.clone());
|
self.0.custom_blocks.insert(block_ix, new_block.clone());
|
||||||
self.0.custom_blocks_by_id.insert(id, new_block);
|
self.0.custom_blocks_by_id.insert(id, new_block);
|
||||||
|
@ -832,6 +847,7 @@ impl<'a> BlockMapWriter<'a> {
|
||||||
style: block.style,
|
style: block.style,
|
||||||
render: block.render.clone(),
|
render: block.render.clone(),
|
||||||
disposition: block.disposition,
|
disposition: block.disposition,
|
||||||
|
priority: block.priority,
|
||||||
};
|
};
|
||||||
let new_block = Arc::new(new_block);
|
let new_block = Arc::new(new_block);
|
||||||
*block = new_block.clone();
|
*block = new_block.clone();
|
||||||
|
@ -1463,6 +1479,7 @@ mod tests {
|
||||||
height: 1,
|
height: 1,
|
||||||
disposition: BlockDisposition::Above,
|
disposition: BlockDisposition::Above,
|
||||||
render: Box::new(|_| div().into_any()),
|
render: Box::new(|_| div().into_any()),
|
||||||
|
priority: 0,
|
||||||
},
|
},
|
||||||
BlockProperties {
|
BlockProperties {
|
||||||
style: BlockStyle::Fixed,
|
style: BlockStyle::Fixed,
|
||||||
|
@ -1470,6 +1487,7 @@ mod tests {
|
||||||
height: 2,
|
height: 2,
|
||||||
disposition: BlockDisposition::Above,
|
disposition: BlockDisposition::Above,
|
||||||
render: Box::new(|_| div().into_any()),
|
render: Box::new(|_| div().into_any()),
|
||||||
|
priority: 0,
|
||||||
},
|
},
|
||||||
BlockProperties {
|
BlockProperties {
|
||||||
style: BlockStyle::Fixed,
|
style: BlockStyle::Fixed,
|
||||||
|
@ -1477,6 +1495,7 @@ mod tests {
|
||||||
height: 3,
|
height: 3,
|
||||||
disposition: BlockDisposition::Below,
|
disposition: BlockDisposition::Below,
|
||||||
render: Box::new(|_| div().into_any()),
|
render: Box::new(|_| div().into_any()),
|
||||||
|
priority: 0,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -1716,6 +1735,7 @@ mod tests {
|
||||||
height: 1,
|
height: 1,
|
||||||
disposition: BlockDisposition::Above,
|
disposition: BlockDisposition::Above,
|
||||||
render: Box::new(|_| div().into_any()),
|
render: Box::new(|_| div().into_any()),
|
||||||
|
priority: 0,
|
||||||
},
|
},
|
||||||
BlockProperties {
|
BlockProperties {
|
||||||
style: BlockStyle::Fixed,
|
style: BlockStyle::Fixed,
|
||||||
|
@ -1723,6 +1743,7 @@ mod tests {
|
||||||
height: 2,
|
height: 2,
|
||||||
disposition: BlockDisposition::Above,
|
disposition: BlockDisposition::Above,
|
||||||
render: Box::new(|_| div().into_any()),
|
render: Box::new(|_| div().into_any()),
|
||||||
|
priority: 0,
|
||||||
},
|
},
|
||||||
BlockProperties {
|
BlockProperties {
|
||||||
style: BlockStyle::Fixed,
|
style: BlockStyle::Fixed,
|
||||||
|
@ -1730,6 +1751,7 @@ mod tests {
|
||||||
height: 3,
|
height: 3,
|
||||||
disposition: BlockDisposition::Below,
|
disposition: BlockDisposition::Below,
|
||||||
render: Box::new(|_| div().into_any()),
|
render: Box::new(|_| div().into_any()),
|
||||||
|
priority: 0,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -1819,6 +1841,7 @@ mod tests {
|
||||||
disposition: BlockDisposition::Above,
|
disposition: BlockDisposition::Above,
|
||||||
render: Box::new(|_| div().into_any()),
|
render: Box::new(|_| div().into_any()),
|
||||||
height: 1,
|
height: 1,
|
||||||
|
priority: 0,
|
||||||
},
|
},
|
||||||
BlockProperties {
|
BlockProperties {
|
||||||
style: BlockStyle::Fixed,
|
style: BlockStyle::Fixed,
|
||||||
|
@ -1826,6 +1849,7 @@ mod tests {
|
||||||
disposition: BlockDisposition::Below,
|
disposition: BlockDisposition::Below,
|
||||||
render: Box::new(|_| div().into_any()),
|
render: Box::new(|_| div().into_any()),
|
||||||
height: 1,
|
height: 1,
|
||||||
|
priority: 0,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -1924,6 +1948,7 @@ mod tests {
|
||||||
height,
|
height,
|
||||||
disposition,
|
disposition,
|
||||||
render: Box::new(|_| div().into_any()),
|
render: Box::new(|_| div().into_any()),
|
||||||
|
priority: 0,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
@ -1944,6 +1969,7 @@ mod tests {
|
||||||
style: props.style,
|
style: props.style,
|
||||||
render: Box::new(|_| div().into_any()),
|
render: Box::new(|_| div().into_any()),
|
||||||
disposition: props.disposition,
|
disposition: props.disposition,
|
||||||
|
priority: 0,
|
||||||
}));
|
}));
|
||||||
for (block_id, props) in block_ids.into_iter().zip(block_properties) {
|
for (block_id, props) in block_ids.into_iter().zip(block_properties) {
|
||||||
custom_blocks.push((block_id, props));
|
custom_blocks.push((block_id, props));
|
||||||
|
@ -2014,6 +2040,7 @@ mod tests {
|
||||||
disposition: block.disposition,
|
disposition: block.disposition,
|
||||||
id: *id,
|
id: *id,
|
||||||
height: block.height,
|
height: block.height,
|
||||||
|
priority: block.priority,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}));
|
}));
|
||||||
|
@ -2235,6 +2262,7 @@ mod tests {
|
||||||
disposition: BlockDisposition,
|
disposition: BlockDisposition,
|
||||||
id: CustomBlockId,
|
id: CustomBlockId,
|
||||||
height: u32,
|
height: u32,
|
||||||
|
priority: usize,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2250,6 +2278,14 @@ mod tests {
|
||||||
fn disposition(&self) -> BlockDisposition {
|
fn disposition(&self) -> BlockDisposition {
|
||||||
self.disposition()
|
self.disposition()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn priority(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
ExpectedBlock::Custom { priority, .. } => *priority,
|
||||||
|
ExpectedBlock::ExcerptHeader { .. } => usize::MAX,
|
||||||
|
ExpectedBlock::ExcerptFooter { .. } => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExpectedBlock {
|
impl ExpectedBlock {
|
||||||
|
@ -2277,6 +2313,7 @@ mod tests {
|
||||||
id: block.id,
|
id: block.id,
|
||||||
disposition: block.disposition,
|
disposition: block.disposition,
|
||||||
height: block.height,
|
height: block.height,
|
||||||
|
priority: block.priority,
|
||||||
},
|
},
|
||||||
Block::ExcerptHeader {
|
Block::ExcerptHeader {
|
||||||
height,
|
height,
|
||||||
|
|
|
@ -9614,6 +9614,7 @@ impl Editor {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
disposition: BlockDisposition::Below,
|
disposition: BlockDisposition::Below,
|
||||||
|
priority: 0,
|
||||||
}],
|
}],
|
||||||
Some(Autoscroll::fit()),
|
Some(Autoscroll::fit()),
|
||||||
cx,
|
cx,
|
||||||
|
@ -9877,6 +9878,7 @@ impl Editor {
|
||||||
height: message_height,
|
height: message_height,
|
||||||
render: diagnostic_block_renderer(diagnostic, None, true, true),
|
render: diagnostic_block_renderer(diagnostic, None, true, true),
|
||||||
disposition: BlockDisposition::Below,
|
disposition: BlockDisposition::Below,
|
||||||
|
priority: 0,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
cx,
|
cx,
|
||||||
|
@ -10182,6 +10184,7 @@ impl Editor {
|
||||||
if let Some(autoscroll) = autoscroll {
|
if let Some(autoscroll) = autoscroll {
|
||||||
self.request_autoscroll(autoscroll, cx);
|
self.request_autoscroll(autoscroll, cx);
|
||||||
}
|
}
|
||||||
|
cx.notify();
|
||||||
blocks
|
blocks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10196,6 +10199,7 @@ impl Editor {
|
||||||
if let Some(autoscroll) = autoscroll {
|
if let Some(autoscroll) = autoscroll {
|
||||||
self.request_autoscroll(autoscroll, cx);
|
self.request_autoscroll(autoscroll, cx);
|
||||||
}
|
}
|
||||||
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn replace_blocks(
|
pub fn replace_blocks(
|
||||||
|
@ -10208,9 +10212,8 @@ impl Editor {
|
||||||
.update(cx, |display_map, _cx| display_map.replace_blocks(renderers));
|
.update(cx, |display_map, _cx| display_map.replace_blocks(renderers));
|
||||||
if let Some(autoscroll) = autoscroll {
|
if let Some(autoscroll) = autoscroll {
|
||||||
self.request_autoscroll(autoscroll, cx);
|
self.request_autoscroll(autoscroll, cx);
|
||||||
} else {
|
|
||||||
cx.notify();
|
|
||||||
}
|
}
|
||||||
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_blocks(
|
pub fn remove_blocks(
|
||||||
|
@ -10225,6 +10228,7 @@ impl Editor {
|
||||||
if let Some(autoscroll) = autoscroll {
|
if let Some(autoscroll) = autoscroll {
|
||||||
self.request_autoscroll(autoscroll, cx);
|
self.request_autoscroll(autoscroll, cx);
|
||||||
}
|
}
|
||||||
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn row_for_block(
|
pub fn row_for_block(
|
||||||
|
|
|
@ -3785,6 +3785,7 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
|
||||||
disposition: BlockDisposition::Below,
|
disposition: BlockDisposition::Below,
|
||||||
height: 1,
|
height: 1,
|
||||||
render: Box::new(|_| div().into_any()),
|
render: Box::new(|_| div().into_any()),
|
||||||
|
priority: 0,
|
||||||
}],
|
}],
|
||||||
Some(Autoscroll::fit()),
|
Some(Autoscroll::fit()),
|
||||||
cx,
|
cx,
|
||||||
|
|
|
@ -6478,6 +6478,7 @@ mod tests {
|
||||||
height: 3,
|
height: 3,
|
||||||
position: Anchor::min(),
|
position: Anchor::min(),
|
||||||
render: Box::new(|cx| div().h(3. * cx.line_height()).into_any()),
|
render: Box::new(|cx| div().h(3. * cx.line_height()).into_any()),
|
||||||
|
priority: 0,
|
||||||
}],
|
}],
|
||||||
None,
|
None,
|
||||||
cx,
|
cx,
|
||||||
|
|
|
@ -525,6 +525,7 @@ impl Editor {
|
||||||
.child(editor_with_deleted_text.clone())
|
.child(editor_with_deleted_text.clone())
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}),
|
}),
|
||||||
|
priority: 0,
|
||||||
}),
|
}),
|
||||||
None,
|
None,
|
||||||
cx,
|
cx,
|
||||||
|
|
|
@ -87,6 +87,7 @@ impl EditorBlock {
|
||||||
style: BlockStyle::Sticky,
|
style: BlockStyle::Sticky,
|
||||||
render: Self::create_output_area_renderer(execution_view.clone(), on_close.clone()),
|
render: Self::create_output_area_renderer(execution_view.clone(), on_close.clone()),
|
||||||
disposition: BlockDisposition::Below,
|
disposition: BlockDisposition::Below,
|
||||||
|
priority: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
let block_id = editor.insert_blocks([block], None, cx)[0];
|
let block_id = editor.insert_blocks([block], None, cx)[0];
|
||||||
|
|
|
@ -50,6 +50,7 @@ pub enum TintColor {
|
||||||
Accent,
|
Accent,
|
||||||
Negative,
|
Negative,
|
||||||
Warning,
|
Warning,
|
||||||
|
Positive,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TintColor {
|
impl TintColor {
|
||||||
|
@ -73,6 +74,12 @@ impl TintColor {
|
||||||
label_color: cx.theme().colors().text,
|
label_color: cx.theme().colors().text,
|
||||||
icon_color: cx.theme().colors().text,
|
icon_color: cx.theme().colors().text,
|
||||||
},
|
},
|
||||||
|
TintColor::Positive => ButtonLikeStyles {
|
||||||
|
background: cx.theme().status().success_background,
|
||||||
|
border_color: cx.theme().status().success_border,
|
||||||
|
label_color: cx.theme().colors().text,
|
||||||
|
icon_color: cx.theme().colors().text,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,6 +90,7 @@ impl From<TintColor> for Color {
|
||||||
TintColor::Accent => Color::Accent,
|
TintColor::Accent => Color::Accent,
|
||||||
TintColor::Negative => Color::Error,
|
TintColor::Negative => Color::Error,
|
||||||
TintColor::Warning => Color::Warning,
|
TintColor::Warning => Color::Warning,
|
||||||
|
TintColor::Positive => Color::Success,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -256,6 +256,7 @@ pub enum IconName {
|
||||||
TextSearch,
|
TextSearch,
|
||||||
Trash,
|
Trash,
|
||||||
TriangleRight,
|
TriangleRight,
|
||||||
|
Undo,
|
||||||
Update,
|
Update,
|
||||||
WholeWord,
|
WholeWord,
|
||||||
XCircle,
|
XCircle,
|
||||||
|
@ -419,6 +420,7 @@ impl IconName {
|
||||||
IconName::Trash => "icons/trash.svg",
|
IconName::Trash => "icons/trash.svg",
|
||||||
IconName::TriangleRight => "icons/triangle_right.svg",
|
IconName::TriangleRight => "icons/triangle_right.svg",
|
||||||
IconName::Update => "icons/update.svg",
|
IconName::Update => "icons/update.svg",
|
||||||
|
IconName::Undo => "icons/undo.svg",
|
||||||
IconName::WholeWord => "icons/word_search.svg",
|
IconName::WholeWord => "icons/word_search.svg",
|
||||||
IconName::XCircle => "icons/error.svg",
|
IconName::XCircle => "icons/error.svg",
|
||||||
IconName::ZedAssistant => "icons/zed_assistant.svg",
|
IconName::ZedAssistant => "icons/zed_assistant.svg",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue