Make slash command output streamable (#19632)
This PR adds support for streaming output from slash commands In this PR we are focused primarily on the interface of the `SlashCommand` trait to support streaming the output. We will follow up later with support for extensions and context servers to take advantage of the streaming nature. Release Notes: - N/A --------- Co-authored-by: David Soria Parra <davidsp@anthropic.com> Co-authored-by: Antonio Scandurra <me@as-cii.com> Co-authored-by: David <david@anthropic.com> Co-authored-by: Antonio <antonio@zed.dev> Co-authored-by: Max <max@zed.dev> Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com> Co-authored-by: Will <will@zed.dev>
This commit is contained in:
parent
f6fbf662b4
commit
b129e18396
14 changed files with 1130 additions and 501 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -463,6 +463,7 @@ dependencies = [
|
||||||
"futures 0.3.30",
|
"futures 0.3.30",
|
||||||
"gpui",
|
"gpui",
|
||||||
"language",
|
"language",
|
||||||
|
"language_model",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::slash_command::file_command::codeblock_fence_for_path;
|
||||||
use crate::{
|
use crate::{
|
||||||
assistant_settings::{AssistantDockPosition, AssistantSettings},
|
assistant_settings::{AssistantDockPosition, AssistantSettings},
|
||||||
humanize_token_count,
|
humanize_token_count,
|
||||||
|
@ -6,24 +7,23 @@ use crate::{
|
||||||
slash_command::{
|
slash_command::{
|
||||||
default_command::DefaultSlashCommand,
|
default_command::DefaultSlashCommand,
|
||||||
docs_command::{DocsSlashCommand, DocsSlashCommandArgs},
|
docs_command::{DocsSlashCommand, DocsSlashCommandArgs},
|
||||||
file_command::{self, codeblock_fence_for_path},
|
file_command, SlashCommandCompletionProvider, SlashCommandRegistry,
|
||||||
SlashCommandCompletionProvider, SlashCommandRegistry,
|
|
||||||
},
|
},
|
||||||
slash_command_picker,
|
slash_command_picker,
|
||||||
terminal_inline_assistant::TerminalInlineAssistant,
|
terminal_inline_assistant::TerminalInlineAssistant,
|
||||||
Assist, AssistantPatch, AssistantPatchStatus, CacheStatus, ConfirmCommand, Content, Context,
|
Assist, AssistantPatch, AssistantPatchStatus, CacheStatus, ConfirmCommand, Content, Context,
|
||||||
ContextEvent, ContextId, ContextStore, ContextStoreEvent, CopyCode, CycleMessageRole,
|
ContextEvent, ContextId, ContextStore, ContextStoreEvent, CopyCode, CycleMessageRole,
|
||||||
DeployHistory, DeployPromptLibrary, Edit, InlineAssistant, InsertDraggedFiles,
|
DeployHistory, DeployPromptLibrary, Edit, InlineAssistant, InsertDraggedFiles,
|
||||||
InsertIntoEditor, Message, MessageId, MessageMetadata, MessageStatus, ModelPickerDelegate,
|
InsertIntoEditor, InvokedSlashCommandStatus, Message, MessageId, MessageMetadata,
|
||||||
ModelSelector, NewContext, PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection,
|
MessageStatus, ModelPickerDelegate, ModelSelector, NewContext, ParsedSlashCommand,
|
||||||
RemoteContextMetadata, RequestType, SavedContextMetadata, Split, ToggleFocus,
|
PendingSlashCommandStatus, QuoteSelection, RemoteContextMetadata, RequestType,
|
||||||
ToggleModelSelector,
|
SavedContextMetadata, SlashCommandId, Split, ToggleFocus, ToggleModelSelector,
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
|
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
|
||||||
use assistant_tool::ToolRegistry;
|
use assistant_tool::ToolRegistry;
|
||||||
use client::{proto, zed_urls, Client, Status};
|
use client::{proto, zed_urls, Client, Status};
|
||||||
use collections::{BTreeSet, HashMap, HashSet};
|
use collections::{hash_map, BTreeSet, HashMap, HashSet};
|
||||||
use editor::{
|
use editor::{
|
||||||
actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt},
|
actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt},
|
||||||
display_map::{
|
display_map::{
|
||||||
|
@ -38,12 +38,12 @@ use editor::{display_map::CreaseId, FoldPlaceholder};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
canvas, div, img, percentage, point, pulsating_between, size, Action, Animation, AnimationExt,
|
canvas, div, img, percentage, point, prelude::*, pulsating_between, size, Action, Animation,
|
||||||
AnyElement, AnyView, AppContext, AsyncWindowContext, ClipboardEntry, ClipboardItem,
|
AnimationExt, AnyElement, AnyView, AppContext, AsyncWindowContext, ClipboardEntry,
|
||||||
CursorStyle, Empty, Entity, EventEmitter, ExternalPaths, FocusHandle, FocusableView,
|
ClipboardItem, CursorStyle, Empty, Entity, EventEmitter, ExternalPaths, FocusHandle,
|
||||||
FontWeight, InteractiveElement, IntoElement, Model, ParentElement, Pixels, Render, RenderImage,
|
FocusableView, FontWeight, InteractiveElement, IntoElement, Model, ParentElement, Pixels,
|
||||||
SharedString, Size, StatefulInteractiveElement, Styled, Subscription, Task, Transformation,
|
Render, RenderImage, SharedString, Size, StatefulInteractiveElement, Styled, Subscription,
|
||||||
UpdateGlobal, View, VisualContext, WeakView, WindowContext,
|
Task, Transformation, UpdateGlobal, View, WeakModel, WeakView,
|
||||||
};
|
};
|
||||||
use indexed_docs::IndexedDocsStore;
|
use indexed_docs::IndexedDocsStore;
|
||||||
use language::{
|
use language::{
|
||||||
|
@ -77,8 +77,8 @@ use text::SelectionGoal;
|
||||||
use ui::{
|
use ui::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
utils::{format_distance_from_now, DateTimeType},
|
utils::{format_distance_from_now, DateTimeType},
|
||||||
Avatar, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding, ListItem,
|
Avatar, ButtonLike, ContextMenu, Disclosure, ElevationIndex, IconButtonShape, KeyBinding,
|
||||||
ListItemSpacing, PopoverMenu, PopoverMenuHandle, TintColor, Tooltip,
|
ListItem, ListItemSpacing, PopoverMenu, PopoverMenuHandle, TintColor, Tooltip,
|
||||||
};
|
};
|
||||||
use util::{maybe, ResultExt};
|
use util::{maybe, ResultExt};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
|
@ -1477,7 +1477,7 @@ pub struct ContextEditor {
|
||||||
scroll_position: Option<ScrollPosition>,
|
scroll_position: Option<ScrollPosition>,
|
||||||
remote_id: Option<workspace::ViewId>,
|
remote_id: Option<workspace::ViewId>,
|
||||||
pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
|
pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
|
||||||
pending_slash_command_blocks: HashMap<Range<language::Anchor>, CustomBlockId>,
|
invoked_slash_command_creases: HashMap<SlashCommandId, CreaseId>,
|
||||||
pending_tool_use_creases: HashMap<Range<language::Anchor>, CreaseId>,
|
pending_tool_use_creases: HashMap<Range<language::Anchor>, CreaseId>,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
patches: HashMap<Range<language::Anchor>, PatchViewState>,
|
patches: HashMap<Range<language::Anchor>, PatchViewState>,
|
||||||
|
@ -1548,7 +1548,7 @@ impl ContextEditor {
|
||||||
workspace,
|
workspace,
|
||||||
project,
|
project,
|
||||||
pending_slash_command_creases: HashMap::default(),
|
pending_slash_command_creases: HashMap::default(),
|
||||||
pending_slash_command_blocks: HashMap::default(),
|
invoked_slash_command_creases: HashMap::default(),
|
||||||
pending_tool_use_creases: HashMap::default(),
|
pending_tool_use_creases: HashMap::default(),
|
||||||
_subscriptions,
|
_subscriptions,
|
||||||
patches: HashMap::default(),
|
patches: HashMap::default(),
|
||||||
|
@ -1573,14 +1573,13 @@ impl ContextEditor {
|
||||||
});
|
});
|
||||||
let command = self.context.update(cx, |context, cx| {
|
let command = self.context.update(cx, |context, cx| {
|
||||||
context.reparse(cx);
|
context.reparse(cx);
|
||||||
context.pending_slash_commands()[0].clone()
|
context.parsed_slash_commands()[0].clone()
|
||||||
});
|
});
|
||||||
self.run_command(
|
self.run_command(
|
||||||
command.source_range,
|
command.source_range,
|
||||||
&command.name,
|
&command.name,
|
||||||
&command.arguments,
|
&command.arguments,
|
||||||
false,
|
false,
|
||||||
false,
|
|
||||||
self.workspace.clone(),
|
self.workspace.clone(),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
@ -1753,7 +1752,6 @@ impl ContextEditor {
|
||||||
&command.name,
|
&command.name,
|
||||||
&command.arguments,
|
&command.arguments,
|
||||||
true,
|
true,
|
||||||
false,
|
|
||||||
workspace.clone(),
|
workspace.clone(),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
@ -1769,7 +1767,6 @@ impl ContextEditor {
|
||||||
name: &str,
|
name: &str,
|
||||||
arguments: &[String],
|
arguments: &[String],
|
||||||
ensure_trailing_newline: bool,
|
ensure_trailing_newline: bool,
|
||||||
expand_result: bool,
|
|
||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
|
@ -1793,9 +1790,9 @@ impl ContextEditor {
|
||||||
self.context.update(cx, |context, cx| {
|
self.context.update(cx, |context, cx| {
|
||||||
context.insert_command_output(
|
context.insert_command_output(
|
||||||
command_range,
|
command_range,
|
||||||
|
name,
|
||||||
output,
|
output,
|
||||||
ensure_trailing_newline,
|
ensure_trailing_newline,
|
||||||
expand_result,
|
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
@ -1865,8 +1862,7 @@ impl ContextEditor {
|
||||||
IconName::PocketKnife,
|
IconName::PocketKnife,
|
||||||
tool_use.name.clone().into(),
|
tool_use.name.clone().into(),
|
||||||
),
|
),
|
||||||
constrain_width: false,
|
..Default::default()
|
||||||
merge_adjacent: false,
|
|
||||||
};
|
};
|
||||||
let render_trailer =
|
let render_trailer =
|
||||||
move |_row, _unfold, _cx: &mut WindowContext| Empty.into_any();
|
move |_row, _unfold, _cx: &mut WindowContext| Empty.into_any();
|
||||||
|
@ -1921,11 +1917,10 @@ impl ContextEditor {
|
||||||
ContextEvent::PatchesUpdated { removed, updated } => {
|
ContextEvent::PatchesUpdated { removed, updated } => {
|
||||||
self.patches_updated(removed, updated, cx);
|
self.patches_updated(removed, updated, cx);
|
||||||
}
|
}
|
||||||
ContextEvent::PendingSlashCommandsUpdated { removed, updated } => {
|
ContextEvent::ParsedSlashCommandsUpdated { removed, updated } => {
|
||||||
self.editor.update(cx, |editor, cx| {
|
self.editor.update(cx, |editor, cx| {
|
||||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||||
let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap();
|
let (&excerpt_id, _, _) = buffer.as_singleton().unwrap();
|
||||||
let excerpt_id = *excerpt_id;
|
|
||||||
|
|
||||||
editor.remove_creases(
|
editor.remove_creases(
|
||||||
removed
|
removed
|
||||||
|
@ -1934,16 +1929,6 @@ impl ContextEditor {
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
editor.remove_blocks(
|
|
||||||
HashSet::from_iter(
|
|
||||||
removed.iter().filter_map(|range| {
|
|
||||||
self.pending_slash_command_blocks.remove(range)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
|
|
||||||
let crease_ids = editor.insert_creases(
|
let crease_ids = editor.insert_creases(
|
||||||
updated.iter().map(|command| {
|
updated.iter().map(|command| {
|
||||||
let workspace = self.workspace.clone();
|
let workspace = self.workspace.clone();
|
||||||
|
@ -1958,7 +1943,6 @@ impl ContextEditor {
|
||||||
&command.name,
|
&command.name,
|
||||||
&command.arguments,
|
&command.arguments,
|
||||||
false,
|
false,
|
||||||
false,
|
|
||||||
workspace.clone(),
|
workspace.clone(),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
@ -1968,8 +1952,7 @@ impl ContextEditor {
|
||||||
});
|
});
|
||||||
let placeholder = FoldPlaceholder {
|
let placeholder = FoldPlaceholder {
|
||||||
render: Arc::new(move |_, _, _| Empty.into_any()),
|
render: Arc::new(move |_, _, _| Empty.into_any()),
|
||||||
constrain_width: false,
|
..Default::default()
|
||||||
merge_adjacent: false,
|
|
||||||
};
|
};
|
||||||
let render_toggle = {
|
let render_toggle = {
|
||||||
let confirm_command = confirm_command.clone();
|
let confirm_command = confirm_command.clone();
|
||||||
|
@ -2011,62 +1994,29 @@ impl ContextEditor {
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
let block_ids = editor.insert_blocks(
|
|
||||||
updated
|
|
||||||
.iter()
|
|
||||||
.filter_map(|command| match &command.status {
|
|
||||||
PendingSlashCommandStatus::Error(error) => {
|
|
||||||
Some((command, error.clone()))
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.map(|(command, error_message)| BlockProperties {
|
|
||||||
style: BlockStyle::Fixed,
|
|
||||||
height: 1,
|
|
||||||
placement: BlockPlacement::Below(Anchor {
|
|
||||||
buffer_id: Some(buffer_id),
|
|
||||||
excerpt_id,
|
|
||||||
text_anchor: command.source_range.start,
|
|
||||||
}),
|
|
||||||
render: slash_command_error_block_renderer(error_message),
|
|
||||||
priority: 0,
|
|
||||||
}),
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
|
|
||||||
self.pending_slash_command_creases.extend(
|
self.pending_slash_command_creases.extend(
|
||||||
updated
|
updated
|
||||||
.iter()
|
.iter()
|
||||||
.map(|command| command.source_range.clone())
|
.map(|command| command.source_range.clone())
|
||||||
.zip(crease_ids),
|
.zip(crease_ids),
|
||||||
);
|
);
|
||||||
|
|
||||||
self.pending_slash_command_blocks.extend(
|
|
||||||
updated
|
|
||||||
.iter()
|
|
||||||
.map(|command| command.source_range.clone())
|
|
||||||
.zip(block_ids),
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
ContextEvent::InvokedSlashCommandChanged { command_id } => {
|
||||||
|
self.update_invoked_slash_command(*command_id, cx);
|
||||||
|
}
|
||||||
|
ContextEvent::SlashCommandOutputSectionAdded { section } => {
|
||||||
|
self.insert_slash_command_output_sections([section.clone()], false, cx);
|
||||||
|
}
|
||||||
ContextEvent::SlashCommandFinished {
|
ContextEvent::SlashCommandFinished {
|
||||||
output_range,
|
output_range: _output_range,
|
||||||
sections,
|
run_commands_in_ranges,
|
||||||
run_commands_in_output,
|
|
||||||
expand_result,
|
|
||||||
} => {
|
} => {
|
||||||
self.insert_slash_command_output_sections(
|
for range in run_commands_in_ranges {
|
||||||
sections.iter().cloned(),
|
|
||||||
*expand_result,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
|
|
||||||
if *run_commands_in_output {
|
|
||||||
let commands = self.context.update(cx, |context, cx| {
|
let commands = self.context.update(cx, |context, cx| {
|
||||||
context.reparse(cx);
|
context.reparse(cx);
|
||||||
context
|
context
|
||||||
.pending_commands_for_range(output_range.clone(), cx)
|
.pending_commands_for_range(range.clone(), cx)
|
||||||
.to_vec()
|
.to_vec()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2076,7 +2026,6 @@ impl ContextEditor {
|
||||||
&command.name,
|
&command.name,
|
||||||
&command.arguments,
|
&command.arguments,
|
||||||
false,
|
false,
|
||||||
false,
|
|
||||||
self.workspace.clone(),
|
self.workspace.clone(),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
@ -2119,8 +2068,7 @@ impl ContextEditor {
|
||||||
IconName::PocketKnife,
|
IconName::PocketKnife,
|
||||||
format!("Tool Result: {tool_use_id}").into(),
|
format!("Tool Result: {tool_use_id}").into(),
|
||||||
),
|
),
|
||||||
constrain_width: false,
|
..Default::default()
|
||||||
merge_adjacent: false,
|
|
||||||
};
|
};
|
||||||
let render_trailer =
|
let render_trailer =
|
||||||
move |_row, _unfold, _cx: &mut WindowContext| Empty.into_any();
|
move |_row, _unfold, _cx: &mut WindowContext| Empty.into_any();
|
||||||
|
@ -2158,6 +2106,77 @@ impl ContextEditor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_invoked_slash_command(
|
||||||
|
&mut self,
|
||||||
|
command_id: SlashCommandId,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
let context_editor = cx.view().downgrade();
|
||||||
|
self.editor.update(cx, |editor, cx| {
|
||||||
|
if let Some(invoked_slash_command) =
|
||||||
|
self.context.read(cx).invoked_slash_command(&command_id)
|
||||||
|
{
|
||||||
|
if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status {
|
||||||
|
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||||
|
let (&excerpt_id, _buffer_id, _buffer_snapshot) =
|
||||||
|
buffer.as_singleton().unwrap();
|
||||||
|
|
||||||
|
let start = buffer
|
||||||
|
.anchor_in_excerpt(excerpt_id, invoked_slash_command.range.start)
|
||||||
|
.unwrap();
|
||||||
|
let end = buffer
|
||||||
|
.anchor_in_excerpt(excerpt_id, invoked_slash_command.range.end)
|
||||||
|
.unwrap();
|
||||||
|
editor.remove_folds_with_type(
|
||||||
|
&[start..end],
|
||||||
|
TypeId::of::<PendingSlashCommand>(),
|
||||||
|
false,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
editor.remove_creases(
|
||||||
|
HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
} else if let hash_map::Entry::Vacant(entry) =
|
||||||
|
self.invoked_slash_command_creases.entry(command_id)
|
||||||
|
{
|
||||||
|
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||||
|
let (&excerpt_id, _buffer_id, _buffer_snapshot) =
|
||||||
|
buffer.as_singleton().unwrap();
|
||||||
|
let context = self.context.downgrade();
|
||||||
|
let crease_start = buffer
|
||||||
|
.anchor_in_excerpt(excerpt_id, invoked_slash_command.range.start)
|
||||||
|
.unwrap();
|
||||||
|
let crease_end = buffer
|
||||||
|
.anchor_in_excerpt(excerpt_id, invoked_slash_command.range.end)
|
||||||
|
.unwrap();
|
||||||
|
let fold_placeholder =
|
||||||
|
invoked_slash_command_fold_placeholder(command_id, context, context_editor);
|
||||||
|
let crease_ids = editor.insert_creases(
|
||||||
|
[Crease::new(
|
||||||
|
crease_start..crease_end,
|
||||||
|
fold_placeholder.clone(),
|
||||||
|
fold_toggle("invoked-slash-command"),
|
||||||
|
|_row, _folded, _cx| Empty.into_any(),
|
||||||
|
)],
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
editor.fold_ranges([(crease_start..crease_end, fold_placeholder)], false, cx);
|
||||||
|
entry.insert(crease_ids[0]);
|
||||||
|
} else {
|
||||||
|
cx.notify()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
editor.remove_creases(
|
||||||
|
HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
cx.notify();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn patches_updated(
|
fn patches_updated(
|
||||||
&mut self,
|
&mut self,
|
||||||
removed: &Vec<Range<text::Anchor>>,
|
removed: &Vec<Range<text::Anchor>>,
|
||||||
|
@ -2229,8 +2248,7 @@ impl ContextEditor {
|
||||||
.unwrap_or_else(|| Empty.into_any())
|
.unwrap_or_else(|| Empty.into_any())
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
constrain_width: false,
|
..Default::default()
|
||||||
merge_adjacent: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let should_refold;
|
let should_refold;
|
||||||
|
@ -2288,7 +2306,7 @@ impl ContextEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
if should_refold {
|
if should_refold {
|
||||||
editor.unfold_ranges([patch_start..patch_end], true, false, cx);
|
editor.unfold_ranges(&[patch_start..patch_end], true, false, cx);
|
||||||
editor.fold_ranges([(patch_start..patch_end, header_placeholder)], false, cx);
|
editor.fold_ranges([(patch_start..patch_end, header_placeholder)], false, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2334,8 +2352,7 @@ impl ContextEditor {
|
||||||
section.icon,
|
section.icon,
|
||||||
section.label.clone(),
|
section.label.clone(),
|
||||||
),
|
),
|
||||||
constrain_width: false,
|
..Default::default()
|
||||||
merge_adjacent: false,
|
|
||||||
},
|
},
|
||||||
render_slash_command_output_toggle,
|
render_slash_command_output_toggle,
|
||||||
|_, _, _| Empty.into_any_element(),
|
|_, _, _| Empty.into_any_element(),
|
||||||
|
@ -3275,13 +3292,12 @@ impl ContextEditor {
|
||||||
Crease::new(
|
Crease::new(
|
||||||
start..end,
|
start..end,
|
||||||
FoldPlaceholder {
|
FoldPlaceholder {
|
||||||
constrain_width: false,
|
|
||||||
render: render_fold_icon_button(
|
render: render_fold_icon_button(
|
||||||
weak_editor.clone(),
|
weak_editor.clone(),
|
||||||
metadata.crease.icon,
|
metadata.crease.icon,
|
||||||
metadata.crease.label.clone(),
|
metadata.crease.label.clone(),
|
||||||
),
|
),
|
||||||
merge_adjacent: false,
|
..Default::default()
|
||||||
},
|
},
|
||||||
render_slash_command_output_toggle,
|
render_slash_command_output_toggle,
|
||||||
|_, _, _| Empty.into_any(),
|
|_, _, _| Empty.into_any(),
|
||||||
|
@ -4947,8 +4963,7 @@ fn quote_selection_fold_placeholder(title: String, editor: WeakView<Editor>) ->
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
constrain_width: false,
|
..Default::default()
|
||||||
merge_adjacent: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4992,7 +5007,7 @@ fn render_pending_slash_command_gutter_decoration(
|
||||||
|
|
||||||
fn render_docs_slash_command_trailer(
|
fn render_docs_slash_command_trailer(
|
||||||
row: MultiBufferRow,
|
row: MultiBufferRow,
|
||||||
command: PendingSlashCommand,
|
command: ParsedSlashCommand,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
if command.arguments.is_empty() {
|
if command.arguments.is_empty() {
|
||||||
|
@ -5076,17 +5091,78 @@ fn make_lsp_adapter_delegate(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn slash_command_error_block_renderer(message: String) -> RenderBlock {
|
enum PendingSlashCommand {}
|
||||||
Box::new(move |_| {
|
|
||||||
div()
|
fn invoked_slash_command_fold_placeholder(
|
||||||
.pl_6()
|
command_id: SlashCommandId,
|
||||||
.child(
|
context: WeakModel<Context>,
|
||||||
Label::new(format!("error: {}", message))
|
context_editor: WeakView<ContextEditor>,
|
||||||
.single_line()
|
) -> FoldPlaceholder {
|
||||||
.color(Color::Error),
|
FoldPlaceholder {
|
||||||
)
|
constrain_width: false,
|
||||||
.into_any()
|
merge_adjacent: false,
|
||||||
})
|
render: Arc::new(move |fold_id, _, cx| {
|
||||||
|
let Some(context) = context.upgrade() else {
|
||||||
|
return Empty.into_any();
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(command) = context.read(cx).invoked_slash_command(&command_id) else {
|
||||||
|
return Empty.into_any();
|
||||||
|
};
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.id(fold_id)
|
||||||
|
.px_1()
|
||||||
|
.ml_6()
|
||||||
|
.gap_2()
|
||||||
|
.bg(cx.theme().colors().surface_background)
|
||||||
|
.rounded_md()
|
||||||
|
.child(Label::new(format!("/{}", command.name.clone())))
|
||||||
|
.map(|parent| match &command.status {
|
||||||
|
InvokedSlashCommandStatus::Running(_) => {
|
||||||
|
parent.child(Icon::new(IconName::ArrowCircle).with_animation(
|
||||||
|
"arrow-circle",
|
||||||
|
Animation::new(Duration::from_secs(4)).repeat(),
|
||||||
|
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
InvokedSlashCommandStatus::Error(message) => parent
|
||||||
|
.child(
|
||||||
|
Label::new(format!("error: {message}"))
|
||||||
|
.single_line()
|
||||||
|
.color(Color::Error),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
IconButton::new("dismiss-error", IconName::Close)
|
||||||
|
.shape(IconButtonShape::Square)
|
||||||
|
.icon_size(IconSize::XSmall)
|
||||||
|
.icon_color(Color::Muted)
|
||||||
|
.on_click({
|
||||||
|
let context_editor = context_editor.clone();
|
||||||
|
move |_event, cx| {
|
||||||
|
context_editor
|
||||||
|
.update(cx, |context_editor, cx| {
|
||||||
|
context_editor.editor.update(cx, |editor, cx| {
|
||||||
|
editor.remove_creases(
|
||||||
|
HashSet::from_iter(
|
||||||
|
context_editor
|
||||||
|
.invoked_slash_command_creases
|
||||||
|
.remove(&command_id),
|
||||||
|
),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
InvokedSlashCommandStatus::Finished => parent,
|
||||||
|
})
|
||||||
|
.into_any_element()
|
||||||
|
}),
|
||||||
|
type_tag: Some(TypeId::of::<PendingSlashCommand>()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum TokenState {
|
enum TokenState {
|
||||||
|
|
|
@ -8,7 +8,8 @@ use crate::{
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Context as _, Result};
|
use anyhow::{anyhow, Context as _, Result};
|
||||||
use assistant_slash_command::{
|
use assistant_slash_command::{
|
||||||
SlashCommandOutput, SlashCommandOutputSection, SlashCommandRegistry, SlashCommandResult,
|
SlashCommandContent, SlashCommandEvent, SlashCommandOutputSection, SlashCommandRegistry,
|
||||||
|
SlashCommandResult,
|
||||||
};
|
};
|
||||||
use assistant_tool::ToolRegistry;
|
use assistant_tool::ToolRegistry;
|
||||||
use client::{self, proto, telemetry::Telemetry};
|
use client::{self, proto, telemetry::Telemetry};
|
||||||
|
@ -47,9 +48,10 @@ use std::{
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
|
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
|
||||||
use text::BufferSnapshot;
|
use text::{BufferSnapshot, ToPoint};
|
||||||
use util::{post_inc, ResultExt, TryFutureExt};
|
use util::{post_inc, ResultExt, TryFutureExt};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
use workspace::ui::IconName;
|
||||||
|
|
||||||
#[derive(Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
|
#[derive(Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
|
||||||
pub struct ContextId(String);
|
pub struct ContextId(String);
|
||||||
|
@ -92,10 +94,21 @@ pub enum ContextOperation {
|
||||||
summary: ContextSummary,
|
summary: ContextSummary,
|
||||||
version: clock::Global,
|
version: clock::Global,
|
||||||
},
|
},
|
||||||
SlashCommandFinished {
|
SlashCommandStarted {
|
||||||
id: SlashCommandId,
|
id: SlashCommandId,
|
||||||
output_range: Range<language::Anchor>,
|
output_range: Range<language::Anchor>,
|
||||||
sections: Vec<SlashCommandOutputSection<language::Anchor>>,
|
name: String,
|
||||||
|
version: clock::Global,
|
||||||
|
},
|
||||||
|
SlashCommandFinished {
|
||||||
|
id: SlashCommandId,
|
||||||
|
timestamp: clock::Lamport,
|
||||||
|
error_message: Option<String>,
|
||||||
|
version: clock::Global,
|
||||||
|
},
|
||||||
|
SlashCommandOutputSectionAdded {
|
||||||
|
timestamp: clock::Lamport,
|
||||||
|
section: SlashCommandOutputSection<language::Anchor>,
|
||||||
version: clock::Global,
|
version: clock::Global,
|
||||||
},
|
},
|
||||||
BufferOperation(language::Operation),
|
BufferOperation(language::Operation),
|
||||||
|
@ -152,31 +165,47 @@ impl ContextOperation {
|
||||||
},
|
},
|
||||||
version: language::proto::deserialize_version(&update.version),
|
version: language::proto::deserialize_version(&update.version),
|
||||||
}),
|
}),
|
||||||
proto::context_operation::Variant::SlashCommandFinished(finished) => {
|
proto::context_operation::Variant::SlashCommandStarted(message) => {
|
||||||
Ok(Self::SlashCommandFinished {
|
Ok(Self::SlashCommandStarted {
|
||||||
id: SlashCommandId(language::proto::deserialize_timestamp(
|
id: SlashCommandId(language::proto::deserialize_timestamp(
|
||||||
finished.id.context("invalid id")?,
|
message.id.context("invalid id")?,
|
||||||
)),
|
)),
|
||||||
output_range: language::proto::deserialize_anchor_range(
|
output_range: language::proto::deserialize_anchor_range(
|
||||||
finished.output_range.context("invalid range")?,
|
message.output_range.context("invalid range")?,
|
||||||
)?,
|
)?,
|
||||||
sections: finished
|
name: message.name,
|
||||||
.sections
|
version: language::proto::deserialize_version(&message.version),
|
||||||
.into_iter()
|
})
|
||||||
.map(|section| {
|
}
|
||||||
Ok(SlashCommandOutputSection {
|
proto::context_operation::Variant::SlashCommandOutputSectionAdded(message) => {
|
||||||
range: language::proto::deserialize_anchor_range(
|
let section = message.section.context("missing section")?;
|
||||||
section.range.context("invalid range")?,
|
Ok(Self::SlashCommandOutputSectionAdded {
|
||||||
)?,
|
timestamp: language::proto::deserialize_timestamp(
|
||||||
icon: section.icon_name.parse()?,
|
message.timestamp.context("missing timestamp")?,
|
||||||
label: section.label.into(),
|
),
|
||||||
metadata: section
|
section: SlashCommandOutputSection {
|
||||||
.metadata
|
range: language::proto::deserialize_anchor_range(
|
||||||
.and_then(|metadata| serde_json::from_str(&metadata).log_err()),
|
section.range.context("invalid range")?,
|
||||||
})
|
)?,
|
||||||
})
|
icon: section.icon_name.parse()?,
|
||||||
.collect::<Result<Vec<_>>>()?,
|
label: section.label.into(),
|
||||||
version: language::proto::deserialize_version(&finished.version),
|
metadata: section
|
||||||
|
.metadata
|
||||||
|
.and_then(|metadata| serde_json::from_str(&metadata).log_err()),
|
||||||
|
},
|
||||||
|
version: language::proto::deserialize_version(&message.version),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
proto::context_operation::Variant::SlashCommandCompleted(message) => {
|
||||||
|
Ok(Self::SlashCommandFinished {
|
||||||
|
id: SlashCommandId(language::proto::deserialize_timestamp(
|
||||||
|
message.id.context("invalid id")?,
|
||||||
|
)),
|
||||||
|
timestamp: language::proto::deserialize_timestamp(
|
||||||
|
message.timestamp.context("missing timestamp")?,
|
||||||
|
),
|
||||||
|
error_message: message.error_message,
|
||||||
|
version: language::proto::deserialize_version(&message.version),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
proto::context_operation::Variant::BufferOperation(op) => Ok(Self::BufferOperation(
|
proto::context_operation::Variant::BufferOperation(op) => Ok(Self::BufferOperation(
|
||||||
|
@ -231,21 +260,33 @@ impl ContextOperation {
|
||||||
},
|
},
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
Self::SlashCommandFinished {
|
Self::SlashCommandStarted {
|
||||||
id,
|
id,
|
||||||
output_range,
|
output_range,
|
||||||
sections,
|
name,
|
||||||
version,
|
version,
|
||||||
} => proto::ContextOperation {
|
} => proto::ContextOperation {
|
||||||
variant: Some(proto::context_operation::Variant::SlashCommandFinished(
|
variant: Some(proto::context_operation::Variant::SlashCommandStarted(
|
||||||
proto::context_operation::SlashCommandFinished {
|
proto::context_operation::SlashCommandStarted {
|
||||||
id: Some(language::proto::serialize_timestamp(id.0)),
|
id: Some(language::proto::serialize_timestamp(id.0)),
|
||||||
output_range: Some(language::proto::serialize_anchor_range(
|
output_range: Some(language::proto::serialize_anchor_range(
|
||||||
output_range.clone(),
|
output_range.clone(),
|
||||||
)),
|
)),
|
||||||
sections: sections
|
name: name.clone(),
|
||||||
.iter()
|
version: language::proto::serialize_version(version),
|
||||||
.map(|section| {
|
},
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
Self::SlashCommandOutputSectionAdded {
|
||||||
|
timestamp,
|
||||||
|
section,
|
||||||
|
version,
|
||||||
|
} => proto::ContextOperation {
|
||||||
|
variant: Some(
|
||||||
|
proto::context_operation::Variant::SlashCommandOutputSectionAdded(
|
||||||
|
proto::context_operation::SlashCommandOutputSectionAdded {
|
||||||
|
timestamp: Some(language::proto::serialize_timestamp(*timestamp)),
|
||||||
|
section: Some({
|
||||||
let icon_name: &'static str = section.icon.into();
|
let icon_name: &'static str = section.icon.into();
|
||||||
proto::SlashCommandOutputSection {
|
proto::SlashCommandOutputSection {
|
||||||
range: Some(language::proto::serialize_anchor_range(
|
range: Some(language::proto::serialize_anchor_range(
|
||||||
|
@ -257,8 +298,23 @@ impl ContextOperation {
|
||||||
serde_json::to_string(metadata).log_err()
|
serde_json::to_string(metadata).log_err()
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
.collect(),
|
version: language::proto::serialize_version(version),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
Self::SlashCommandFinished {
|
||||||
|
id,
|
||||||
|
timestamp,
|
||||||
|
error_message,
|
||||||
|
version,
|
||||||
|
} => proto::ContextOperation {
|
||||||
|
variant: Some(proto::context_operation::Variant::SlashCommandCompleted(
|
||||||
|
proto::context_operation::SlashCommandCompleted {
|
||||||
|
id: Some(language::proto::serialize_timestamp(id.0)),
|
||||||
|
timestamp: Some(language::proto::serialize_timestamp(*timestamp)),
|
||||||
|
error_message: error_message.clone(),
|
||||||
version: language::proto::serialize_version(version),
|
version: language::proto::serialize_version(version),
|
||||||
},
|
},
|
||||||
)),
|
)),
|
||||||
|
@ -278,7 +334,9 @@ impl ContextOperation {
|
||||||
Self::InsertMessage { anchor, .. } => anchor.id.0,
|
Self::InsertMessage { anchor, .. } => anchor.id.0,
|
||||||
Self::UpdateMessage { metadata, .. } => metadata.timestamp,
|
Self::UpdateMessage { metadata, .. } => metadata.timestamp,
|
||||||
Self::UpdateSummary { summary, .. } => summary.timestamp,
|
Self::UpdateSummary { summary, .. } => summary.timestamp,
|
||||||
Self::SlashCommandFinished { id, .. } => id.0,
|
Self::SlashCommandStarted { id, .. } => id.0,
|
||||||
|
Self::SlashCommandOutputSectionAdded { timestamp, .. }
|
||||||
|
| Self::SlashCommandFinished { timestamp, .. } => *timestamp,
|
||||||
Self::BufferOperation(_) => {
|
Self::BufferOperation(_) => {
|
||||||
panic!("reading the timestamp of a buffer operation is not supported")
|
panic!("reading the timestamp of a buffer operation is not supported")
|
||||||
}
|
}
|
||||||
|
@ -291,6 +349,8 @@ impl ContextOperation {
|
||||||
Self::InsertMessage { version, .. }
|
Self::InsertMessage { version, .. }
|
||||||
| Self::UpdateMessage { version, .. }
|
| Self::UpdateMessage { version, .. }
|
||||||
| Self::UpdateSummary { version, .. }
|
| Self::UpdateSummary { version, .. }
|
||||||
|
| Self::SlashCommandStarted { version, .. }
|
||||||
|
| Self::SlashCommandOutputSectionAdded { version, .. }
|
||||||
| Self::SlashCommandFinished { version, .. } => version,
|
| Self::SlashCommandFinished { version, .. } => version,
|
||||||
Self::BufferOperation(_) => {
|
Self::BufferOperation(_) => {
|
||||||
panic!("reading the version of a buffer operation is not supported")
|
panic!("reading the version of a buffer operation is not supported")
|
||||||
|
@ -311,15 +371,19 @@ pub enum ContextEvent {
|
||||||
removed: Vec<Range<language::Anchor>>,
|
removed: Vec<Range<language::Anchor>>,
|
||||||
updated: Vec<Range<language::Anchor>>,
|
updated: Vec<Range<language::Anchor>>,
|
||||||
},
|
},
|
||||||
PendingSlashCommandsUpdated {
|
InvokedSlashCommandChanged {
|
||||||
|
command_id: SlashCommandId,
|
||||||
|
},
|
||||||
|
ParsedSlashCommandsUpdated {
|
||||||
removed: Vec<Range<language::Anchor>>,
|
removed: Vec<Range<language::Anchor>>,
|
||||||
updated: Vec<PendingSlashCommand>,
|
updated: Vec<ParsedSlashCommand>,
|
||||||
|
},
|
||||||
|
SlashCommandOutputSectionAdded {
|
||||||
|
section: SlashCommandOutputSection<language::Anchor>,
|
||||||
},
|
},
|
||||||
SlashCommandFinished {
|
SlashCommandFinished {
|
||||||
output_range: Range<language::Anchor>,
|
output_range: Range<language::Anchor>,
|
||||||
sections: Vec<SlashCommandOutputSection<language::Anchor>>,
|
run_commands_in_ranges: Vec<Range<language::Anchor>>,
|
||||||
run_commands_in_output: bool,
|
|
||||||
expand_result: bool,
|
|
||||||
},
|
},
|
||||||
UsePendingTools,
|
UsePendingTools,
|
||||||
ToolFinished {
|
ToolFinished {
|
||||||
|
@ -478,7 +542,8 @@ pub struct Context {
|
||||||
pending_ops: Vec<ContextOperation>,
|
pending_ops: Vec<ContextOperation>,
|
||||||
operations: Vec<ContextOperation>,
|
operations: Vec<ContextOperation>,
|
||||||
buffer: Model<Buffer>,
|
buffer: Model<Buffer>,
|
||||||
pending_slash_commands: Vec<PendingSlashCommand>,
|
parsed_slash_commands: Vec<ParsedSlashCommand>,
|
||||||
|
invoked_slash_commands: HashMap<SlashCommandId, InvokedSlashCommand>,
|
||||||
edits_since_last_parse: language::Subscription,
|
edits_since_last_parse: language::Subscription,
|
||||||
finished_slash_commands: HashSet<SlashCommandId>,
|
finished_slash_commands: HashSet<SlashCommandId>,
|
||||||
slash_command_output_sections: Vec<SlashCommandOutputSection<language::Anchor>>,
|
slash_command_output_sections: Vec<SlashCommandOutputSection<language::Anchor>>,
|
||||||
|
@ -508,7 +573,7 @@ trait ContextAnnotation {
|
||||||
fn range(&self) -> &Range<language::Anchor>;
|
fn range(&self) -> &Range<language::Anchor>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContextAnnotation for PendingSlashCommand {
|
impl ContextAnnotation for ParsedSlashCommand {
|
||||||
fn range(&self) -> &Range<language::Anchor> {
|
fn range(&self) -> &Range<language::Anchor> {
|
||||||
&self.source_range
|
&self.source_range
|
||||||
}
|
}
|
||||||
|
@ -580,7 +645,8 @@ impl Context {
|
||||||
message_anchors: Default::default(),
|
message_anchors: Default::default(),
|
||||||
contents: Default::default(),
|
contents: Default::default(),
|
||||||
messages_metadata: Default::default(),
|
messages_metadata: Default::default(),
|
||||||
pending_slash_commands: Vec::new(),
|
parsed_slash_commands: Vec::new(),
|
||||||
|
invoked_slash_commands: HashMap::default(),
|
||||||
finished_slash_commands: HashSet::default(),
|
finished_slash_commands: HashSet::default(),
|
||||||
pending_tool_uses_by_id: HashMap::default(),
|
pending_tool_uses_by_id: HashMap::default(),
|
||||||
slash_command_output_sections: Vec::new(),
|
slash_command_output_sections: Vec::new(),
|
||||||
|
@ -827,24 +893,50 @@ impl Context {
|
||||||
summary_changed = true;
|
summary_changed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ContextOperation::SlashCommandFinished {
|
ContextOperation::SlashCommandStarted {
|
||||||
id,
|
id,
|
||||||
output_range,
|
output_range,
|
||||||
sections,
|
name,
|
||||||
..
|
..
|
||||||
|
} => {
|
||||||
|
self.invoked_slash_commands.insert(
|
||||||
|
id,
|
||||||
|
InvokedSlashCommand {
|
||||||
|
name: name.into(),
|
||||||
|
range: output_range,
|
||||||
|
status: InvokedSlashCommandStatus::Running(Task::ready(())),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
cx.emit(ContextEvent::InvokedSlashCommandChanged { command_id: id });
|
||||||
|
}
|
||||||
|
ContextOperation::SlashCommandOutputSectionAdded { section, .. } => {
|
||||||
|
let buffer = self.buffer.read(cx);
|
||||||
|
if let Err(ix) = self
|
||||||
|
.slash_command_output_sections
|
||||||
|
.binary_search_by(|probe| probe.range.cmp(§ion.range, buffer))
|
||||||
|
{
|
||||||
|
self.slash_command_output_sections
|
||||||
|
.insert(ix, section.clone());
|
||||||
|
cx.emit(ContextEvent::SlashCommandOutputSectionAdded { section });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ContextOperation::SlashCommandFinished {
|
||||||
|
id, error_message, ..
|
||||||
} => {
|
} => {
|
||||||
if self.finished_slash_commands.insert(id) {
|
if self.finished_slash_commands.insert(id) {
|
||||||
let buffer = self.buffer.read(cx);
|
if let Some(slash_command) = self.invoked_slash_commands.get_mut(&id) {
|
||||||
self.slash_command_output_sections
|
match error_message {
|
||||||
.extend(sections.iter().cloned());
|
Some(message) => {
|
||||||
self.slash_command_output_sections
|
slash_command.status =
|
||||||
.sort_by(|a, b| a.range.cmp(&b.range, buffer));
|
InvokedSlashCommandStatus::Error(message.into());
|
||||||
cx.emit(ContextEvent::SlashCommandFinished {
|
}
|
||||||
output_range,
|
None => {
|
||||||
sections,
|
slash_command.status = InvokedSlashCommandStatus::Finished;
|
||||||
expand_result: false,
|
}
|
||||||
run_commands_in_output: false,
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
cx.emit(ContextEvent::InvokedSlashCommandChanged { command_id: id });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ContextOperation::BufferOperation(_) => unreachable!(),
|
ContextOperation::BufferOperation(_) => unreachable!(),
|
||||||
|
@ -882,32 +974,34 @@ impl Context {
|
||||||
self.messages_metadata.contains_key(message_id)
|
self.messages_metadata.contains_key(message_id)
|
||||||
}
|
}
|
||||||
ContextOperation::UpdateSummary { .. } => true,
|
ContextOperation::UpdateSummary { .. } => true,
|
||||||
ContextOperation::SlashCommandFinished {
|
ContextOperation::SlashCommandStarted { output_range, .. } => {
|
||||||
output_range,
|
self.has_received_operations_for_anchor_range(output_range.clone(), cx)
|
||||||
sections,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
let version = &self.buffer.read(cx).version;
|
|
||||||
sections
|
|
||||||
.iter()
|
|
||||||
.map(|section| §ion.range)
|
|
||||||
.chain([output_range])
|
|
||||||
.all(|range| {
|
|
||||||
let observed_start = range.start == language::Anchor::MIN
|
|
||||||
|| range.start == language::Anchor::MAX
|
|
||||||
|| version.observed(range.start.timestamp);
|
|
||||||
let observed_end = range.end == language::Anchor::MIN
|
|
||||||
|| range.end == language::Anchor::MAX
|
|
||||||
|| version.observed(range.end.timestamp);
|
|
||||||
observed_start && observed_end
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
ContextOperation::SlashCommandOutputSectionAdded { section, .. } => {
|
||||||
|
self.has_received_operations_for_anchor_range(section.range.clone(), cx)
|
||||||
|
}
|
||||||
|
ContextOperation::SlashCommandFinished { .. } => true,
|
||||||
ContextOperation::BufferOperation(_) => {
|
ContextOperation::BufferOperation(_) => {
|
||||||
panic!("buffer operations should always be applied")
|
panic!("buffer operations should always be applied")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn has_received_operations_for_anchor_range(
|
||||||
|
&self,
|
||||||
|
range: Range<text::Anchor>,
|
||||||
|
cx: &AppContext,
|
||||||
|
) -> bool {
|
||||||
|
let version = &self.buffer.read(cx).version;
|
||||||
|
let observed_start = range.start == language::Anchor::MIN
|
||||||
|
|| range.start == language::Anchor::MAX
|
||||||
|
|| version.observed(range.start.timestamp);
|
||||||
|
let observed_end = range.end == language::Anchor::MIN
|
||||||
|
|| range.end == language::Anchor::MAX
|
||||||
|
|| version.observed(range.end.timestamp);
|
||||||
|
observed_start && observed_end
|
||||||
|
}
|
||||||
|
|
||||||
fn push_op(&mut self, op: ContextOperation, cx: &mut ModelContext<Self>) {
|
fn push_op(&mut self, op: ContextOperation, cx: &mut ModelContext<Self>) {
|
||||||
self.operations.push(op.clone());
|
self.operations.push(op.clone());
|
||||||
cx.emit(ContextEvent::Operation(op));
|
cx.emit(ContextEvent::Operation(op));
|
||||||
|
@ -983,8 +1077,15 @@ impl Context {
|
||||||
.binary_search_by(|probe| probe.range.cmp(&tagged_range, buffer))
|
.binary_search_by(|probe| probe.range.cmp(&tagged_range, buffer))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pending_slash_commands(&self) -> &[PendingSlashCommand] {
|
pub fn parsed_slash_commands(&self) -> &[ParsedSlashCommand] {
|
||||||
&self.pending_slash_commands
|
&self.parsed_slash_commands
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn invoked_slash_command(
|
||||||
|
&self,
|
||||||
|
command_id: &SlashCommandId,
|
||||||
|
) -> Option<&InvokedSlashCommand> {
|
||||||
|
self.invoked_slash_commands.get(command_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn slash_command_output_sections(&self) -> &[SlashCommandOutputSection<language::Anchor>] {
|
pub fn slash_command_output_sections(&self) -> &[SlashCommandOutputSection<language::Anchor>] {
|
||||||
|
@ -1306,7 +1407,7 @@ impl Context {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !updated_slash_commands.is_empty() || !removed_slash_command_ranges.is_empty() {
|
if !updated_slash_commands.is_empty() || !removed_slash_command_ranges.is_empty() {
|
||||||
cx.emit(ContextEvent::PendingSlashCommandsUpdated {
|
cx.emit(ContextEvent::ParsedSlashCommandsUpdated {
|
||||||
removed: removed_slash_command_ranges,
|
removed: removed_slash_command_ranges,
|
||||||
updated: updated_slash_commands,
|
updated: updated_slash_commands,
|
||||||
});
|
});
|
||||||
|
@ -1324,7 +1425,7 @@ impl Context {
|
||||||
&mut self,
|
&mut self,
|
||||||
range: Range<text::Anchor>,
|
range: Range<text::Anchor>,
|
||||||
buffer: &BufferSnapshot,
|
buffer: &BufferSnapshot,
|
||||||
updated: &mut Vec<PendingSlashCommand>,
|
updated: &mut Vec<ParsedSlashCommand>,
|
||||||
removed: &mut Vec<Range<text::Anchor>>,
|
removed: &mut Vec<Range<text::Anchor>>,
|
||||||
cx: &AppContext,
|
cx: &AppContext,
|
||||||
) {
|
) {
|
||||||
|
@ -1358,7 +1459,7 @@ impl Context {
|
||||||
.map_or(command_line.name.end, |argument| argument.end);
|
.map_or(command_line.name.end, |argument| argument.end);
|
||||||
let source_range =
|
let source_range =
|
||||||
buffer.anchor_after(start_ix)..buffer.anchor_after(end_ix);
|
buffer.anchor_after(start_ix)..buffer.anchor_after(end_ix);
|
||||||
let pending_command = PendingSlashCommand {
|
let pending_command = ParsedSlashCommand {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
arguments,
|
arguments,
|
||||||
source_range,
|
source_range,
|
||||||
|
@ -1373,7 +1474,7 @@ impl Context {
|
||||||
offset = lines.offset();
|
offset = lines.offset();
|
||||||
}
|
}
|
||||||
|
|
||||||
let removed_commands = self.pending_slash_commands.splice(old_range, new_commands);
|
let removed_commands = self.parsed_slash_commands.splice(old_range, new_commands);
|
||||||
removed.extend(removed_commands.map(|command| command.source_range));
|
removed.extend(removed_commands.map(|command| command.source_range));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1642,15 +1743,15 @@ impl Context {
|
||||||
&mut self,
|
&mut self,
|
||||||
position: language::Anchor,
|
position: language::Anchor,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Option<&mut PendingSlashCommand> {
|
) -> Option<&mut ParsedSlashCommand> {
|
||||||
let buffer = self.buffer.read(cx);
|
let buffer = self.buffer.read(cx);
|
||||||
match self
|
match self
|
||||||
.pending_slash_commands
|
.parsed_slash_commands
|
||||||
.binary_search_by(|probe| probe.source_range.end.cmp(&position, buffer))
|
.binary_search_by(|probe| probe.source_range.end.cmp(&position, buffer))
|
||||||
{
|
{
|
||||||
Ok(ix) => Some(&mut self.pending_slash_commands[ix]),
|
Ok(ix) => Some(&mut self.parsed_slash_commands[ix]),
|
||||||
Err(ix) => {
|
Err(ix) => {
|
||||||
let cmd = self.pending_slash_commands.get_mut(ix)?;
|
let cmd = self.parsed_slash_commands.get_mut(ix)?;
|
||||||
if position.cmp(&cmd.source_range.start, buffer).is_ge()
|
if position.cmp(&cmd.source_range.start, buffer).is_ge()
|
||||||
&& position.cmp(&cmd.source_range.end, buffer).is_le()
|
&& position.cmp(&cmd.source_range.end, buffer).is_le()
|
||||||
{
|
{
|
||||||
|
@ -1666,9 +1767,9 @@ impl Context {
|
||||||
&self,
|
&self,
|
||||||
range: Range<language::Anchor>,
|
range: Range<language::Anchor>,
|
||||||
cx: &AppContext,
|
cx: &AppContext,
|
||||||
) -> &[PendingSlashCommand] {
|
) -> &[ParsedSlashCommand] {
|
||||||
let range = self.pending_command_indices_for_range(range, cx);
|
let range = self.pending_command_indices_for_range(range, cx);
|
||||||
&self.pending_slash_commands[range]
|
&self.parsed_slash_commands[range]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pending_command_indices_for_range(
|
fn pending_command_indices_for_range(
|
||||||
|
@ -1676,7 +1777,7 @@ impl Context {
|
||||||
range: Range<language::Anchor>,
|
range: Range<language::Anchor>,
|
||||||
cx: &AppContext,
|
cx: &AppContext,
|
||||||
) -> Range<usize> {
|
) -> Range<usize> {
|
||||||
self.indices_intersecting_buffer_range(&self.pending_slash_commands, range, cx)
|
self.indices_intersecting_buffer_range(&self.parsed_slash_commands, range, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn indices_intersecting_buffer_range<T: ContextAnnotation>(
|
fn indices_intersecting_buffer_range<T: ContextAnnotation>(
|
||||||
|
@ -1702,112 +1803,275 @@ impl Context {
|
||||||
|
|
||||||
pub fn insert_command_output(
|
pub fn insert_command_output(
|
||||||
&mut self,
|
&mut self,
|
||||||
command_range: Range<language::Anchor>,
|
command_source_range: Range<language::Anchor>,
|
||||||
|
name: &str,
|
||||||
output: Task<SlashCommandResult>,
|
output: Task<SlashCommandResult>,
|
||||||
ensure_trailing_newline: bool,
|
ensure_trailing_newline: bool,
|
||||||
expand_result: bool,
|
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) {
|
) {
|
||||||
|
let version = self.version.clone();
|
||||||
|
let command_id = SlashCommandId(self.next_timestamp());
|
||||||
|
|
||||||
|
const PENDING_OUTPUT_END_MARKER: &str = "…";
|
||||||
|
|
||||||
|
let (command_range, command_source_range, insert_position) =
|
||||||
|
self.buffer.update(cx, |buffer, cx| {
|
||||||
|
let command_source_range = command_source_range.to_offset(buffer);
|
||||||
|
let mut insertion = format!("\n{PENDING_OUTPUT_END_MARKER}");
|
||||||
|
if ensure_trailing_newline {
|
||||||
|
insertion.push('\n');
|
||||||
|
}
|
||||||
|
buffer.edit(
|
||||||
|
[(
|
||||||
|
command_source_range.end..command_source_range.end,
|
||||||
|
insertion,
|
||||||
|
)],
|
||||||
|
None,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
let insert_position = buffer.anchor_after(command_source_range.end + 1);
|
||||||
|
let command_range = buffer.anchor_before(command_source_range.start)
|
||||||
|
..buffer.anchor_after(
|
||||||
|
command_source_range.end + 1 + PENDING_OUTPUT_END_MARKER.len(),
|
||||||
|
);
|
||||||
|
let command_source_range = buffer.anchor_before(command_source_range.start)
|
||||||
|
..buffer.anchor_before(command_source_range.end + 1);
|
||||||
|
(command_range, command_source_range, insert_position)
|
||||||
|
});
|
||||||
self.reparse(cx);
|
self.reparse(cx);
|
||||||
|
|
||||||
let insert_output_task = cx.spawn(|this, mut cx| {
|
let insert_output_task = cx.spawn(|this, mut cx| async move {
|
||||||
let command_range = command_range.clone();
|
let run_command = async {
|
||||||
async move {
|
let mut stream = output.await?;
|
||||||
let output = output.await;
|
|
||||||
let output = match output {
|
|
||||||
Ok(output) => SlashCommandOutput::from_event_stream(output).await,
|
|
||||||
Err(err) => Err(err),
|
|
||||||
};
|
|
||||||
this.update(&mut cx, |this, cx| match output {
|
|
||||||
Ok(mut output) => {
|
|
||||||
output.ensure_valid_section_ranges();
|
|
||||||
|
|
||||||
// Ensure there is a newline after the last section.
|
struct PendingSection {
|
||||||
if ensure_trailing_newline {
|
start: language::Anchor,
|
||||||
let has_newline_after_last_section =
|
icon: IconName,
|
||||||
output.sections.last().map_or(false, |last_section| {
|
label: SharedString,
|
||||||
output.text[last_section.range.end..].ends_with('\n')
|
metadata: Option<serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pending_section_stack: Vec<PendingSection> = Vec::new();
|
||||||
|
let mut run_commands_in_ranges: Vec<Range<language::Anchor>> = Vec::new();
|
||||||
|
let mut last_role: Option<Role> = None;
|
||||||
|
let mut last_section_range = None;
|
||||||
|
|
||||||
|
while let Some(event) = stream.next().await {
|
||||||
|
let event = event?;
|
||||||
|
match event {
|
||||||
|
SlashCommandEvent::StartMessage {
|
||||||
|
role,
|
||||||
|
merge_same_roles,
|
||||||
|
} => {
|
||||||
|
if !merge_same_roles && Some(role) != last_role {
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
let offset = this.buffer.read_with(cx, |buffer, _cx| {
|
||||||
|
insert_position.to_offset(buffer)
|
||||||
|
});
|
||||||
|
this.insert_message_at_offset(
|
||||||
|
offset,
|
||||||
|
role,
|
||||||
|
MessageStatus::Pending,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
last_role = Some(role);
|
||||||
|
}
|
||||||
|
SlashCommandEvent::StartSection {
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
metadata,
|
||||||
|
} => {
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.buffer.update(cx, |buffer, cx| {
|
||||||
|
let insert_point = insert_position.to_point(buffer);
|
||||||
|
if insert_point.column > 0 {
|
||||||
|
buffer.edit([(insert_point..insert_point, "\n")], None, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pending_section_stack.push(PendingSection {
|
||||||
|
start: buffer.anchor_before(insert_position),
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
metadata,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
if !has_newline_after_last_section {
|
})?;
|
||||||
output.text.push('\n');
|
}
|
||||||
|
SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||||
|
text,
|
||||||
|
run_commands_in_text,
|
||||||
|
}) => {
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
let start = this.buffer.read(cx).anchor_before(insert_position);
|
||||||
|
|
||||||
|
let result = this.buffer.update(cx, |buffer, cx| {
|
||||||
|
buffer.edit(
|
||||||
|
[(insert_position..insert_position, text)],
|
||||||
|
None,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let end = this.buffer.read(cx).anchor_before(insert_position);
|
||||||
|
if run_commands_in_text {
|
||||||
|
run_commands_in_ranges.push(start..end);
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
SlashCommandEvent::EndSection { metadata } => {
|
||||||
|
if let Some(pending_section) = pending_section_stack.pop() {
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
let offset_range = (pending_section.start..insert_position)
|
||||||
|
.to_offset(this.buffer.read(cx));
|
||||||
|
if offset_range.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let range = this.buffer.update(cx, |buffer, _cx| {
|
||||||
|
buffer.anchor_after(offset_range.start)
|
||||||
|
..buffer.anchor_before(offset_range.end)
|
||||||
|
});
|
||||||
|
this.insert_slash_command_output_section(
|
||||||
|
SlashCommandOutputSection {
|
||||||
|
range: range.clone(),
|
||||||
|
icon: pending_section.icon,
|
||||||
|
label: pending_section.label,
|
||||||
|
metadata: metadata.or(pending_section.metadata),
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
last_section_range = Some(range);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.buffer.update(cx, |buffer, cx| {
|
||||||
|
let mut deletions = vec![(command_source_range.to_offset(buffer), "")];
|
||||||
|
let insert_position = insert_position.to_offset(buffer);
|
||||||
|
let command_range_end = command_range.end.to_offset(buffer);
|
||||||
|
|
||||||
|
if buffer.contains_str_at(insert_position, PENDING_OUTPUT_END_MARKER) {
|
||||||
|
deletions.push((
|
||||||
|
insert_position..insert_position + PENDING_OUTPUT_END_MARKER.len(),
|
||||||
|
"",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ensure_trailing_newline
|
||||||
|
&& buffer.contains_str_at(command_range_end, "\n")
|
||||||
|
{
|
||||||
|
let newline_offset = insert_position.saturating_sub(1);
|
||||||
|
if buffer.contains_str_at(newline_offset, "\n")
|
||||||
|
&& last_section_range.map_or(true, |last_section_range| {
|
||||||
|
!last_section_range
|
||||||
|
.to_offset(buffer)
|
||||||
|
.contains(&newline_offset)
|
||||||
|
})
|
||||||
|
{
|
||||||
|
deletions.push((command_range_end..command_range_end + 1, ""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let version = this.version.clone();
|
buffer.edit(deletions, None, cx);
|
||||||
let command_id = SlashCommandId(this.next_timestamp());
|
});
|
||||||
let (operation, event) = this.buffer.update(cx, |buffer, cx| {
|
})?;
|
||||||
let start = command_range.start.to_offset(buffer);
|
|
||||||
let old_end = command_range.end.to_offset(buffer);
|
|
||||||
let new_end = start + output.text.len();
|
|
||||||
buffer.edit([(start..old_end, output.text)], None, cx);
|
|
||||||
|
|
||||||
let mut sections = output
|
debug_assert!(pending_section_stack.is_empty());
|
||||||
.sections
|
|
||||||
.into_iter()
|
|
||||||
.map(|section| SlashCommandOutputSection {
|
|
||||||
range: buffer.anchor_after(start + section.range.start)
|
|
||||||
..buffer.anchor_before(start + section.range.end),
|
|
||||||
icon: section.icon,
|
|
||||||
label: section.label,
|
|
||||||
metadata: section.metadata,
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
sections.sort_by(|a, b| a.range.cmp(&b.range, buffer));
|
|
||||||
|
|
||||||
this.slash_command_output_sections
|
anyhow::Ok(())
|
||||||
.extend(sections.iter().cloned());
|
};
|
||||||
this.slash_command_output_sections
|
|
||||||
.sort_by(|a, b| a.range.cmp(&b.range, buffer));
|
|
||||||
|
|
||||||
let output_range =
|
let command_result = run_command.await;
|
||||||
buffer.anchor_after(start)..buffer.anchor_before(new_end);
|
|
||||||
this.finished_slash_commands.insert(command_id);
|
|
||||||
|
|
||||||
(
|
this.update(&mut cx, |this, cx| {
|
||||||
ContextOperation::SlashCommandFinished {
|
let version = this.version.clone();
|
||||||
id: command_id,
|
let timestamp = this.next_timestamp();
|
||||||
output_range: output_range.clone(),
|
let Some(invoked_slash_command) = this.invoked_slash_commands.get_mut(&command_id)
|
||||||
sections: sections.clone(),
|
else {
|
||||||
version,
|
return;
|
||||||
},
|
};
|
||||||
ContextEvent::SlashCommandFinished {
|
let mut error_message = None;
|
||||||
output_range,
|
match command_result {
|
||||||
sections,
|
Ok(()) => {
|
||||||
run_commands_in_output: output.run_commands_in_text,
|
invoked_slash_command.status = InvokedSlashCommandStatus::Finished;
|
||||||
expand_result,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
this.push_op(operation, cx);
|
|
||||||
cx.emit(event);
|
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
if let Some(pending_command) =
|
let message = error.to_string();
|
||||||
this.pending_command_for_position(command_range.start, cx)
|
invoked_slash_command.status =
|
||||||
{
|
InvokedSlashCommandStatus::Error(message.clone().into());
|
||||||
pending_command.status =
|
error_message = Some(message);
|
||||||
PendingSlashCommandStatus::Error(error.to_string());
|
|
||||||
cx.emit(ContextEvent::PendingSlashCommandsUpdated {
|
|
||||||
removed: vec![pending_command.source_range.clone()],
|
|
||||||
updated: vec![pending_command.clone()],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
.ok();
|
|
||||||
}
|
cx.emit(ContextEvent::InvokedSlashCommandChanged { command_id });
|
||||||
|
this.push_op(
|
||||||
|
ContextOperation::SlashCommandFinished {
|
||||||
|
id: command_id,
|
||||||
|
timestamp,
|
||||||
|
error_message,
|
||||||
|
version,
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(pending_command) = self.pending_command_for_position(command_range.start, cx) {
|
self.invoked_slash_commands.insert(
|
||||||
pending_command.status = PendingSlashCommandStatus::Running {
|
command_id,
|
||||||
_task: insert_output_task.shared(),
|
InvokedSlashCommand {
|
||||||
};
|
name: name.to_string().into(),
|
||||||
cx.emit(ContextEvent::PendingSlashCommandsUpdated {
|
range: command_range.clone(),
|
||||||
removed: vec![pending_command.source_range.clone()],
|
status: InvokedSlashCommandStatus::Running(insert_output_task),
|
||||||
updated: vec![pending_command.clone()],
|
},
|
||||||
});
|
);
|
||||||
}
|
cx.emit(ContextEvent::InvokedSlashCommandChanged { command_id });
|
||||||
|
self.push_op(
|
||||||
|
ContextOperation::SlashCommandStarted {
|
||||||
|
id: command_id,
|
||||||
|
output_range: command_range,
|
||||||
|
name: name.to_string(),
|
||||||
|
version,
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_slash_command_output_section(
|
||||||
|
&mut self,
|
||||||
|
section: SlashCommandOutputSection<language::Anchor>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) {
|
||||||
|
let buffer = self.buffer.read(cx);
|
||||||
|
let insertion_ix = match self
|
||||||
|
.slash_command_output_sections
|
||||||
|
.binary_search_by(|probe| probe.range.cmp(§ion.range, buffer))
|
||||||
|
{
|
||||||
|
Ok(ix) | Err(ix) => ix,
|
||||||
|
};
|
||||||
|
self.slash_command_output_sections
|
||||||
|
.insert(insertion_ix, section.clone());
|
||||||
|
cx.emit(ContextEvent::SlashCommandOutputSectionAdded {
|
||||||
|
section: section.clone(),
|
||||||
|
});
|
||||||
|
let version = self.version.clone();
|
||||||
|
let timestamp = self.next_timestamp();
|
||||||
|
self.push_op(
|
||||||
|
ContextOperation::SlashCommandOutputSectionAdded {
|
||||||
|
timestamp,
|
||||||
|
section,
|
||||||
|
version,
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_tool_output(
|
pub fn insert_tool_output(
|
||||||
|
@ -2312,43 +2576,54 @@ impl Context {
|
||||||
next_message_ix += 1;
|
next_message_ix += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let start = self.buffer.update(cx, |buffer, cx| {
|
let buffer = self.buffer.read(cx);
|
||||||
let offset = self
|
let offset = self
|
||||||
.message_anchors
|
.message_anchors
|
||||||
.get(next_message_ix)
|
.get(next_message_ix)
|
||||||
.map_or(buffer.len(), |message| {
|
.map_or(buffer.len(), |message| {
|
||||||
buffer.clip_offset(message.start.to_offset(buffer) - 1, Bias::Left)
|
buffer.clip_offset(message.start.to_offset(buffer) - 1, Bias::Left)
|
||||||
});
|
});
|
||||||
buffer.edit([(offset..offset, "\n")], None, cx);
|
Some(self.insert_message_at_offset(offset, role, status, cx))
|
||||||
buffer.anchor_before(offset + 1)
|
|
||||||
});
|
|
||||||
|
|
||||||
let version = self.version.clone();
|
|
||||||
let anchor = MessageAnchor {
|
|
||||||
id: MessageId(self.next_timestamp()),
|
|
||||||
start,
|
|
||||||
};
|
|
||||||
let metadata = MessageMetadata {
|
|
||||||
role,
|
|
||||||
status,
|
|
||||||
timestamp: anchor.id.0,
|
|
||||||
cache: None,
|
|
||||||
};
|
|
||||||
self.insert_message(anchor.clone(), metadata.clone(), cx);
|
|
||||||
self.push_op(
|
|
||||||
ContextOperation::InsertMessage {
|
|
||||||
anchor: anchor.clone(),
|
|
||||||
metadata,
|
|
||||||
version,
|
|
||||||
},
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
Some(anchor)
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn insert_message_at_offset(
|
||||||
|
&mut self,
|
||||||
|
offset: usize,
|
||||||
|
role: Role,
|
||||||
|
status: MessageStatus,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> MessageAnchor {
|
||||||
|
let start = self.buffer.update(cx, |buffer, cx| {
|
||||||
|
buffer.edit([(offset..offset, "\n")], None, cx);
|
||||||
|
buffer.anchor_before(offset + 1)
|
||||||
|
});
|
||||||
|
|
||||||
|
let version = self.version.clone();
|
||||||
|
let anchor = MessageAnchor {
|
||||||
|
id: MessageId(self.next_timestamp()),
|
||||||
|
start,
|
||||||
|
};
|
||||||
|
let metadata = MessageMetadata {
|
||||||
|
role,
|
||||||
|
status,
|
||||||
|
timestamp: anchor.id.0,
|
||||||
|
cache: None,
|
||||||
|
};
|
||||||
|
self.insert_message(anchor.clone(), metadata.clone(), cx);
|
||||||
|
self.push_op(
|
||||||
|
ContextOperation::InsertMessage {
|
||||||
|
anchor: anchor.clone(),
|
||||||
|
metadata,
|
||||||
|
version,
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
anchor
|
||||||
|
}
|
||||||
|
|
||||||
pub fn insert_content(&mut self, content: Content, cx: &mut ModelContext<Self>) {
|
pub fn insert_content(&mut self, content: Content, cx: &mut ModelContext<Self>) {
|
||||||
let buffer = self.buffer.read(cx);
|
let buffer = self.buffer.read(cx);
|
||||||
let insertion_ix = match self
|
let insertion_ix = match self
|
||||||
|
@ -2814,13 +3089,27 @@ impl ContextVersion {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct PendingSlashCommand {
|
pub struct ParsedSlashCommand {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub arguments: SmallVec<[String; 3]>,
|
pub arguments: SmallVec<[String; 3]>,
|
||||||
pub status: PendingSlashCommandStatus,
|
pub status: PendingSlashCommandStatus,
|
||||||
pub source_range: Range<language::Anchor>,
|
pub source_range: Range<language::Anchor>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct InvokedSlashCommand {
|
||||||
|
pub name: SharedString,
|
||||||
|
pub range: Range<language::Anchor>,
|
||||||
|
pub status: InvokedSlashCommandStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum InvokedSlashCommandStatus {
|
||||||
|
Running(Task<()>),
|
||||||
|
Error(SharedString),
|
||||||
|
Finished,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum PendingSlashCommandStatus {
|
pub enum PendingSlashCommandStatus {
|
||||||
Idle,
|
Idle,
|
||||||
|
@ -2960,27 +3249,23 @@ impl SavedContext {
|
||||||
version.observe(timestamp);
|
version.observe(timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
let timestamp = next_timestamp.tick();
|
let buffer = buffer.read(cx);
|
||||||
operations.push(ContextOperation::SlashCommandFinished {
|
for section in self.slash_command_output_sections {
|
||||||
id: SlashCommandId(timestamp),
|
let timestamp = next_timestamp.tick();
|
||||||
output_range: language::Anchor::MIN..language::Anchor::MAX,
|
operations.push(ContextOperation::SlashCommandOutputSectionAdded {
|
||||||
sections: self
|
timestamp,
|
||||||
.slash_command_output_sections
|
section: SlashCommandOutputSection {
|
||||||
.into_iter()
|
range: buffer.anchor_after(section.range.start)
|
||||||
.map(|section| {
|
..buffer.anchor_before(section.range.end),
|
||||||
let buffer = buffer.read(cx);
|
icon: section.icon,
|
||||||
SlashCommandOutputSection {
|
label: section.label,
|
||||||
range: buffer.anchor_after(section.range.start)
|
metadata: section.metadata,
|
||||||
..buffer.anchor_before(section.range.end),
|
},
|
||||||
icon: section.icon,
|
version: version.clone(),
|
||||||
label: section.label,
|
});
|
||||||
metadata: section.metadata,
|
|
||||||
}
|
version.observe(timestamp);
|
||||||
})
|
}
|
||||||
.collect(),
|
|
||||||
version: version.clone(),
|
|
||||||
});
|
|
||||||
version.observe(timestamp);
|
|
||||||
|
|
||||||
let timestamp = next_timestamp.tick();
|
let timestamp = next_timestamp.tick();
|
||||||
operations.push(ContextOperation::UpdateSummary {
|
operations.push(ContextOperation::UpdateSummary {
|
||||||
|
|
|
@ -2,14 +2,19 @@ use super::{AssistantEdit, MessageCacheMetadata};
|
||||||
use crate::{
|
use crate::{
|
||||||
assistant_panel, prompt_library, slash_command::file_command, AssistantEditKind, CacheStatus,
|
assistant_panel, prompt_library, slash_command::file_command, AssistantEditKind, CacheStatus,
|
||||||
Context, ContextEvent, ContextId, ContextOperation, MessageId, MessageStatus, PromptBuilder,
|
Context, ContextEvent, ContextId, ContextOperation, MessageId, MessageStatus, PromptBuilder,
|
||||||
|
SlashCommandId,
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use assistant_slash_command::{
|
use assistant_slash_command::{
|
||||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent, SlashCommandOutput,
|
||||||
SlashCommandRegistry, SlashCommandResult,
|
SlashCommandOutputSection, SlashCommandRegistry, SlashCommandResult,
|
||||||
};
|
};
|
||||||
use collections::HashSet;
|
use collections::{HashMap, HashSet};
|
||||||
use fs::FakeFs;
|
use fs::FakeFs;
|
||||||
|
use futures::{
|
||||||
|
channel::mpsc,
|
||||||
|
stream::{self, StreamExt},
|
||||||
|
};
|
||||||
use gpui::{AppContext, Model, SharedString, Task, TestAppContext, WeakView};
|
use gpui::{AppContext, Model, SharedString, Task, TestAppContext, WeakView};
|
||||||
use language::{Buffer, BufferSnapshot, LanguageRegistry, LspAdapterDelegate};
|
use language::{Buffer, BufferSnapshot, LanguageRegistry, LspAdapterDelegate};
|
||||||
use language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Role};
|
use language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Role};
|
||||||
|
@ -27,8 +32,8 @@ use std::{
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
sync::{atomic::AtomicBool, Arc},
|
sync::{atomic::AtomicBool, Arc},
|
||||||
};
|
};
|
||||||
use text::{network::Network, OffsetRangeExt as _, ReplicaId};
|
use text::{network::Network, OffsetRangeExt as _, ReplicaId, ToOffset};
|
||||||
use ui::{Context as _, WindowContext};
|
use ui::{Context as _, IconName, WindowContext};
|
||||||
use unindent::Unindent;
|
use unindent::Unindent;
|
||||||
use util::{
|
use util::{
|
||||||
test::{generate_marked_text, marked_text_ranges},
|
test::{generate_marked_text, marked_text_ranges},
|
||||||
|
@ -381,20 +386,41 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||||
let context =
|
let context =
|
||||||
cx.new_model(|cx| Context::local(registry.clone(), None, None, prompt_builder.clone(), cx));
|
cx.new_model(|cx| Context::local(registry.clone(), None, None, prompt_builder.clone(), cx));
|
||||||
|
|
||||||
let output_ranges = Rc::new(RefCell::new(HashSet::default()));
|
#[derive(Default)]
|
||||||
|
struct ContextRanges {
|
||||||
|
parsed_commands: HashSet<Range<language::Anchor>>,
|
||||||
|
command_outputs: HashMap<SlashCommandId, Range<language::Anchor>>,
|
||||||
|
output_sections: HashSet<Range<language::Anchor>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let context_ranges = Rc::new(RefCell::new(ContextRanges::default()));
|
||||||
context.update(cx, |_, cx| {
|
context.update(cx, |_, cx| {
|
||||||
cx.subscribe(&context, {
|
cx.subscribe(&context, {
|
||||||
let ranges = output_ranges.clone();
|
let context_ranges = context_ranges.clone();
|
||||||
move |_, _, event, _| match event {
|
move |context, _, event, _| {
|
||||||
ContextEvent::PendingSlashCommandsUpdated { removed, updated } => {
|
let mut context_ranges = context_ranges.borrow_mut();
|
||||||
for range in removed {
|
match event {
|
||||||
ranges.borrow_mut().remove(range);
|
ContextEvent::InvokedSlashCommandChanged { command_id } => {
|
||||||
|
let command = context.invoked_slash_command(command_id).unwrap();
|
||||||
|
context_ranges
|
||||||
|
.command_outputs
|
||||||
|
.insert(*command_id, command.range.clone());
|
||||||
}
|
}
|
||||||
for command in updated {
|
ContextEvent::ParsedSlashCommandsUpdated { removed, updated } => {
|
||||||
ranges.borrow_mut().insert(command.source_range.clone());
|
for range in removed {
|
||||||
|
context_ranges.parsed_commands.remove(range);
|
||||||
|
}
|
||||||
|
for command in updated {
|
||||||
|
context_ranges
|
||||||
|
.parsed_commands
|
||||||
|
.insert(command.source_range.clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
ContextEvent::SlashCommandOutputSectionAdded { section } => {
|
||||||
|
context_ranges.output_sections.insert(section.range.clone());
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
@ -406,14 +432,12 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||||
buffer.update(cx, |buffer, cx| {
|
buffer.update(cx, |buffer, cx| {
|
||||||
buffer.edit([(0..0, "/file src/lib.rs")], None, cx);
|
buffer.edit([(0..0, "/file src/lib.rs")], None, cx);
|
||||||
});
|
});
|
||||||
assert_text_and_output_ranges(
|
assert_text_and_context_ranges(
|
||||||
&buffer,
|
&buffer,
|
||||||
&output_ranges.borrow(),
|
&context_ranges,
|
||||||
"
|
&"
|
||||||
«/file src/lib.rs»
|
«/file src/lib.rs»"
|
||||||
"
|
.unindent(),
|
||||||
.unindent()
|
|
||||||
.trim_end(),
|
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -422,14 +446,12 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||||
let edit_offset = buffer.text().find("lib.rs").unwrap();
|
let edit_offset = buffer.text().find("lib.rs").unwrap();
|
||||||
buffer.edit([(edit_offset..edit_offset + "lib".len(), "main")], None, cx);
|
buffer.edit([(edit_offset..edit_offset + "lib".len(), "main")], None, cx);
|
||||||
});
|
});
|
||||||
assert_text_and_output_ranges(
|
assert_text_and_context_ranges(
|
||||||
&buffer,
|
&buffer,
|
||||||
&output_ranges.borrow(),
|
&context_ranges,
|
||||||
"
|
&"
|
||||||
«/file src/main.rs»
|
«/file src/main.rs»"
|
||||||
"
|
.unindent(),
|
||||||
.unindent()
|
|
||||||
.trim_end(),
|
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -442,36 +464,180 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
assert_text_and_output_ranges(
|
assert_text_and_context_ranges(
|
||||||
&buffer,
|
&buffer,
|
||||||
&output_ranges.borrow(),
|
&context_ranges,
|
||||||
|
&"
|
||||||
|
/unknown src/main.rs"
|
||||||
|
.unindent(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Undoing the insertion of an non-existent slash command resorts the previous one.
|
||||||
|
buffer.update(cx, |buffer, cx| buffer.undo(cx));
|
||||||
|
assert_text_and_context_ranges(
|
||||||
|
&buffer,
|
||||||
|
&context_ranges,
|
||||||
|
&"
|
||||||
|
«/file src/main.rs»"
|
||||||
|
.unindent(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
let (command_output_tx, command_output_rx) = mpsc::unbounded();
|
||||||
|
context.update(cx, |context, cx| {
|
||||||
|
let command_source_range = context.parsed_slash_commands[0].source_range.clone();
|
||||||
|
context.insert_command_output(
|
||||||
|
command_source_range,
|
||||||
|
"file",
|
||||||
|
Task::ready(Ok(command_output_rx.boxed())),
|
||||||
|
true,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
assert_text_and_context_ranges(
|
||||||
|
&buffer,
|
||||||
|
&context_ranges,
|
||||||
|
&"
|
||||||
|
⟦«/file src/main.rs»
|
||||||
|
…⟧
|
||||||
"
|
"
|
||||||
/unknown src/main.rs
|
.unindent(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
command_output_tx
|
||||||
|
.unbounded_send(Ok(SlashCommandEvent::StartSection {
|
||||||
|
icon: IconName::Ai,
|
||||||
|
label: "src/main.rs".into(),
|
||||||
|
metadata: None,
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
command_output_tx
|
||||||
|
.unbounded_send(Ok(SlashCommandEvent::Content("src/main.rs".into())))
|
||||||
|
.unwrap();
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert_text_and_context_ranges(
|
||||||
|
&buffer,
|
||||||
|
&context_ranges,
|
||||||
|
&"
|
||||||
|
⟦«/file src/main.rs»
|
||||||
|
src/main.rs…⟧
|
||||||
"
|
"
|
||||||
.unindent()
|
.unindent(),
|
||||||
.trim_end(),
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
command_output_tx
|
||||||
|
.unbounded_send(Ok(SlashCommandEvent::Content("\nfn main() {}".into())))
|
||||||
|
.unwrap();
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert_text_and_context_ranges(
|
||||||
|
&buffer,
|
||||||
|
&context_ranges,
|
||||||
|
&"
|
||||||
|
⟦«/file src/main.rs»
|
||||||
|
src/main.rs
|
||||||
|
fn main() {}…⟧
|
||||||
|
"
|
||||||
|
.unindent(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
command_output_tx
|
||||||
|
.unbounded_send(Ok(SlashCommandEvent::EndSection { metadata: None }))
|
||||||
|
.unwrap();
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert_text_and_context_ranges(
|
||||||
|
&buffer,
|
||||||
|
&context_ranges,
|
||||||
|
&"
|
||||||
|
⟦«/file src/main.rs»
|
||||||
|
⟪src/main.rs
|
||||||
|
fn main() {}⟫…⟧
|
||||||
|
"
|
||||||
|
.unindent(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
drop(command_output_tx);
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert_text_and_context_ranges(
|
||||||
|
&buffer,
|
||||||
|
&context_ranges,
|
||||||
|
&"
|
||||||
|
⟦⟪src/main.rs
|
||||||
|
fn main() {}⟫⟧
|
||||||
|
"
|
||||||
|
.unindent(),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn assert_text_and_output_ranges(
|
fn assert_text_and_context_ranges(
|
||||||
buffer: &Model<Buffer>,
|
buffer: &Model<Buffer>,
|
||||||
ranges: &HashSet<Range<language::Anchor>>,
|
ranges: &RefCell<ContextRanges>,
|
||||||
expected_marked_text: &str,
|
expected_marked_text: &str,
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
) {
|
) {
|
||||||
let (expected_text, expected_ranges) = marked_text_ranges(expected_marked_text, false);
|
let mut actual_marked_text = String::new();
|
||||||
let (actual_text, actual_ranges) = buffer.update(cx, |buffer, _| {
|
buffer.update(cx, |buffer, _| {
|
||||||
let mut ranges = ranges
|
struct Endpoint {
|
||||||
.iter()
|
offset: usize,
|
||||||
.map(|range| range.to_offset(buffer))
|
marker: char,
|
||||||
.collect::<Vec<_>>();
|
}
|
||||||
ranges.sort_by_key(|a| a.start);
|
|
||||||
(buffer.text(), ranges)
|
let ranges = ranges.borrow();
|
||||||
|
let mut endpoints = Vec::new();
|
||||||
|
for range in ranges.command_outputs.values() {
|
||||||
|
endpoints.push(Endpoint {
|
||||||
|
offset: range.start.to_offset(buffer),
|
||||||
|
marker: '⟦',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for range in ranges.parsed_commands.iter() {
|
||||||
|
endpoints.push(Endpoint {
|
||||||
|
offset: range.start.to_offset(buffer),
|
||||||
|
marker: '«',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for range in ranges.output_sections.iter() {
|
||||||
|
endpoints.push(Endpoint {
|
||||||
|
offset: range.start.to_offset(buffer),
|
||||||
|
marker: '⟪',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for range in ranges.output_sections.iter() {
|
||||||
|
endpoints.push(Endpoint {
|
||||||
|
offset: range.end.to_offset(buffer),
|
||||||
|
marker: '⟫',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for range in ranges.parsed_commands.iter() {
|
||||||
|
endpoints.push(Endpoint {
|
||||||
|
offset: range.end.to_offset(buffer),
|
||||||
|
marker: '»',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for range in ranges.command_outputs.values() {
|
||||||
|
endpoints.push(Endpoint {
|
||||||
|
offset: range.end.to_offset(buffer),
|
||||||
|
marker: '⟧',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoints.sort_by_key(|endpoint| endpoint.offset);
|
||||||
|
let mut offset = 0;
|
||||||
|
for endpoint in endpoints {
|
||||||
|
actual_marked_text.extend(buffer.text_for_range(offset..endpoint.offset));
|
||||||
|
actual_marked_text.push(endpoint.marker);
|
||||||
|
offset = endpoint.offset;
|
||||||
|
}
|
||||||
|
actual_marked_text.extend(buffer.text_for_range(offset..buffer.len()));
|
||||||
});
|
});
|
||||||
|
|
||||||
assert_eq!(actual_text, expected_text);
|
assert_eq!(actual_marked_text, expected_marked_text);
|
||||||
assert_eq!(actual_ranges, expected_ranges);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1063,44 +1229,57 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||||
offset + 1..offset + 1 + command_text.len()
|
offset + 1..offset + 1 + command_text.len()
|
||||||
});
|
});
|
||||||
|
|
||||||
let output_len = rng.gen_range(1..=10);
|
|
||||||
let output_text = RandomCharIter::new(&mut rng)
|
let output_text = RandomCharIter::new(&mut rng)
|
||||||
.filter(|c| *c != '\r')
|
.filter(|c| *c != '\r')
|
||||||
.take(output_len)
|
.take(10)
|
||||||
.collect::<String>();
|
.collect::<String>();
|
||||||
|
|
||||||
|
let mut events = vec![Ok(SlashCommandEvent::StartMessage {
|
||||||
|
role: Role::User,
|
||||||
|
merge_same_roles: true,
|
||||||
|
})];
|
||||||
|
|
||||||
let num_sections = rng.gen_range(0..=3);
|
let num_sections = rng.gen_range(0..=3);
|
||||||
let mut sections = Vec::with_capacity(num_sections);
|
let mut section_start = 0;
|
||||||
for _ in 0..num_sections {
|
for _ in 0..num_sections {
|
||||||
let section_start = rng.gen_range(0..output_len);
|
let mut section_end = rng.gen_range(section_start..=output_text.len());
|
||||||
let section_end = rng.gen_range(section_start..=output_len);
|
while !output_text.is_char_boundary(section_end) {
|
||||||
sections.push(SlashCommandOutputSection {
|
section_end += 1;
|
||||||
range: section_start..section_end,
|
}
|
||||||
icon: ui::IconName::Ai,
|
events.push(Ok(SlashCommandEvent::StartSection {
|
||||||
|
icon: IconName::Ai,
|
||||||
label: "section".into(),
|
label: "section".into(),
|
||||||
metadata: None,
|
metadata: None,
|
||||||
});
|
}));
|
||||||
|
events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||||
|
text: output_text[section_start..section_end].to_string(),
|
||||||
|
run_commands_in_text: false,
|
||||||
|
})));
|
||||||
|
events.push(Ok(SlashCommandEvent::EndSection { metadata: None }));
|
||||||
|
section_start = section_end;
|
||||||
|
}
|
||||||
|
|
||||||
|
if section_start < output_text.len() {
|
||||||
|
events.push(Ok(SlashCommandEvent::Content(SlashCommandContent::Text {
|
||||||
|
text: output_text[section_start..].to_string(),
|
||||||
|
run_commands_in_text: false,
|
||||||
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
log::info!(
|
log::info!(
|
||||||
"Context {}: insert slash command output at {:?} with {:?}",
|
"Context {}: insert slash command output at {:?} with {:?} events",
|
||||||
context_index,
|
context_index,
|
||||||
command_range,
|
command_range,
|
||||||
sections
|
events.len()
|
||||||
);
|
);
|
||||||
|
|
||||||
let command_range = context.buffer.read(cx).anchor_after(command_range.start)
|
let command_range = context.buffer.read(cx).anchor_after(command_range.start)
|
||||||
..context.buffer.read(cx).anchor_after(command_range.end);
|
..context.buffer.read(cx).anchor_after(command_range.end);
|
||||||
context.insert_command_output(
|
context.insert_command_output(
|
||||||
command_range,
|
command_range,
|
||||||
Task::ready(Ok(SlashCommandOutput {
|
"/command",
|
||||||
text: output_text,
|
Task::ready(Ok(stream::iter(events).boxed())),
|
||||||
sections,
|
|
||||||
run_commands_in_text: false,
|
|
||||||
}
|
|
||||||
.to_event_stream())),
|
|
||||||
true,
|
true,
|
||||||
false,
|
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -127,7 +127,6 @@ impl SlashCommandCompletionProvider {
|
||||||
&command_name,
|
&command_name,
|
||||||
&[],
|
&[],
|
||||||
true,
|
true,
|
||||||
false,
|
|
||||||
workspace.clone(),
|
workspace.clone(),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
@ -212,7 +211,6 @@ impl SlashCommandCompletionProvider {
|
||||||
&command_name,
|
&command_name,
|
||||||
&completed_arguments,
|
&completed_arguments,
|
||||||
true,
|
true,
|
||||||
false,
|
|
||||||
workspace.clone(),
|
workspace.clone(),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
|
@ -75,12 +75,6 @@ impl SlashCommand for StreamingExampleSlashCommand {
|
||||||
},
|
},
|
||||||
)))?;
|
)))?;
|
||||||
events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection { metadata: None }))?;
|
events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection { metadata: None }))?;
|
||||||
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
|
|
||||||
SlashCommandContent::Text {
|
|
||||||
text: "\n".into(),
|
|
||||||
run_commands_in_text: false,
|
|
||||||
},
|
|
||||||
)))?;
|
|
||||||
|
|
||||||
Timer::after(Duration::from_secs(1)).await;
|
Timer::after(Duration::from_secs(1)).await;
|
||||||
|
|
||||||
|
@ -96,12 +90,6 @@ impl SlashCommand for StreamingExampleSlashCommand {
|
||||||
},
|
},
|
||||||
)))?;
|
)))?;
|
||||||
events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection { metadata: None }))?;
|
events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection { metadata: None }))?;
|
||||||
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
|
|
||||||
SlashCommandContent::Text {
|
|
||||||
text: "\n".into(),
|
|
||||||
run_commands_in_text: false,
|
|
||||||
},
|
|
||||||
)))?;
|
|
||||||
|
|
||||||
for n in 1..=10 {
|
for n in 1..=10 {
|
||||||
Timer::after(Duration::from_secs(1)).await;
|
Timer::after(Duration::from_secs(1)).await;
|
||||||
|
@ -119,12 +107,6 @@ impl SlashCommand for StreamingExampleSlashCommand {
|
||||||
)))?;
|
)))?;
|
||||||
events_tx
|
events_tx
|
||||||
.unbounded_send(Ok(SlashCommandEvent::EndSection { metadata: None }))?;
|
.unbounded_send(Ok(SlashCommandEvent::EndSection { metadata: None }))?;
|
||||||
events_tx.unbounded_send(Ok(SlashCommandEvent::Content(
|
|
||||||
SlashCommandContent::Text {
|
|
||||||
text: "\n".into(),
|
|
||||||
run_commands_in_text: false,
|
|
||||||
},
|
|
||||||
)))?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
anyhow::Ok(())
|
anyhow::Ok(())
|
||||||
|
|
|
@ -18,6 +18,7 @@ derive_more.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
|
language_model.workspace = true
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|
|
@ -5,6 +5,7 @@ use futures::stream::{self, BoxStream};
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use gpui::{AnyElement, AppContext, ElementId, SharedString, Task, WeakView, WindowContext};
|
use gpui::{AnyElement, AppContext, ElementId, SharedString, Task, WeakView, WindowContext};
|
||||||
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt};
|
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt};
|
||||||
|
pub use language_model::Role;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
pub use slash_command_registry::*;
|
pub use slash_command_registry::*;
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -103,7 +104,7 @@ pub type RenderFoldPlaceholder = Arc<
|
||||||
+ Fn(ElementId, Arc<dyn Fn(&mut WindowContext)>, &mut WindowContext) -> AnyElement,
|
+ Fn(ElementId, Arc<dyn Fn(&mut WindowContext)>, &mut WindowContext) -> AnyElement,
|
||||||
>;
|
>;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum SlashCommandContent {
|
pub enum SlashCommandContent {
|
||||||
Text {
|
Text {
|
||||||
text: String,
|
text: String,
|
||||||
|
@ -111,8 +112,21 @@ pub enum SlashCommandContent {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
impl<'a> From<&'a str> for SlashCommandContent {
|
||||||
|
fn from(text: &'a str) -> Self {
|
||||||
|
Self::Text {
|
||||||
|
text: text.into(),
|
||||||
|
run_commands_in_text: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum SlashCommandEvent {
|
pub enum SlashCommandEvent {
|
||||||
|
StartMessage {
|
||||||
|
role: Role,
|
||||||
|
merge_same_roles: bool,
|
||||||
|
},
|
||||||
StartSection {
|
StartSection {
|
||||||
icon: IconName,
|
icon: IconName,
|
||||||
label: SharedString,
|
label: SharedString,
|
||||||
|
@ -232,6 +246,7 @@ impl SlashCommandOutput {
|
||||||
output.sections.push(section);
|
output.sections.push(section);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
SlashCommandEvent::StartMessage { .. } => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ use block_map::{BlockRow, BlockSnapshot};
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
pub use crease_map::*;
|
pub use crease_map::*;
|
||||||
pub use fold_map::{Fold, FoldId, FoldPlaceholder, FoldPoint};
|
pub use fold_map::{Fold, FoldId, FoldPlaceholder, FoldPoint};
|
||||||
use fold_map::{FoldMap, FoldSnapshot};
|
use fold_map::{FoldMap, FoldMapWriter, FoldOffset, FoldSnapshot};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, Font, HighlightStyle, LineLayout, Model, ModelContext, Pixels, UnderlineStyle,
|
AnyElement, Font, HighlightStyle, LineLayout, Model, ModelContext, Pixels, UnderlineStyle,
|
||||||
};
|
};
|
||||||
|
@ -65,7 +65,7 @@ use std::{
|
||||||
};
|
};
|
||||||
use sum_tree::{Bias, TreeMap};
|
use sum_tree::{Bias, TreeMap};
|
||||||
use tab_map::{TabMap, TabSnapshot};
|
use tab_map::{TabMap, TabSnapshot};
|
||||||
use text::LineIndent;
|
use text::{Edit, LineIndent};
|
||||||
use ui::{div, px, IntoElement, ParentElement, SharedString, Styled, WindowContext};
|
use ui::{div, px, IntoElement, ParentElement, SharedString, Styled, WindowContext};
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
use wrap_map::{WrapMap, WrapSnapshot};
|
use wrap_map::{WrapMap, WrapSnapshot};
|
||||||
|
@ -206,34 +206,41 @@ impl DisplayMap {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates folds for the given ranges.
|
||||||
pub fn fold<T: ToOffset>(
|
pub fn fold<T: ToOffset>(
|
||||||
&mut self,
|
&mut self,
|
||||||
ranges: impl IntoIterator<Item = (Range<T>, FoldPlaceholder)>,
|
ranges: impl IntoIterator<Item = (Range<T>, FoldPlaceholder)>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) {
|
) {
|
||||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
self.update_fold_map(cx, |fold_map| fold_map.fold(ranges))
|
||||||
let edits = self.buffer_subscription.consume().into_inner();
|
|
||||||
let tab_size = Self::tab_size(&self.buffer, cx);
|
|
||||||
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
|
|
||||||
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
|
|
||||||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
|
||||||
let (snapshot, edits) = self
|
|
||||||
.wrap_map
|
|
||||||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
|
||||||
self.block_map.read(snapshot, edits);
|
|
||||||
let (snapshot, edits) = fold_map.fold(ranges);
|
|
||||||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
|
||||||
let (snapshot, edits) = self
|
|
||||||
.wrap_map
|
|
||||||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
|
||||||
self.block_map.read(snapshot, edits);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unfold<T: ToOffset>(
|
/// Removes any folds with the given ranges.
|
||||||
|
pub fn remove_folds_with_type<T: ToOffset>(
|
||||||
|
&mut self,
|
||||||
|
ranges: impl IntoIterator<Item = Range<T>>,
|
||||||
|
type_id: TypeId,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) {
|
||||||
|
self.update_fold_map(cx, |fold_map| fold_map.remove_folds(ranges, type_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes any folds whose ranges intersect any of the given ranges.
|
||||||
|
pub fn unfold_intersecting<T: ToOffset>(
|
||||||
&mut self,
|
&mut self,
|
||||||
ranges: impl IntoIterator<Item = Range<T>>,
|
ranges: impl IntoIterator<Item = Range<T>>,
|
||||||
inclusive: bool,
|
inclusive: bool,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
|
) {
|
||||||
|
self.update_fold_map(cx, |fold_map| {
|
||||||
|
fold_map.unfold_intersecting(ranges, inclusive)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_fold_map(
|
||||||
|
&mut self,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
callback: impl FnOnce(&mut FoldMapWriter) -> (FoldSnapshot, Vec<Edit<FoldOffset>>),
|
||||||
) {
|
) {
|
||||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||||
let edits = self.buffer_subscription.consume().into_inner();
|
let edits = self.buffer_subscription.consume().into_inner();
|
||||||
|
@ -245,7 +252,7 @@ impl DisplayMap {
|
||||||
.wrap_map
|
.wrap_map
|
||||||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||||||
self.block_map.read(snapshot, edits);
|
self.block_map.read(snapshot, edits);
|
||||||
let (snapshot, edits) = fold_map.unfold(ranges, inclusive);
|
let (snapshot, edits) = callback(&mut fold_map);
|
||||||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||||||
let (snapshot, edits) = self
|
let (snapshot, edits) = self
|
||||||
.wrap_map
|
.wrap_map
|
||||||
|
@ -1442,7 +1449,7 @@ pub mod tests {
|
||||||
if rng.gen() && fold_count > 0 {
|
if rng.gen() && fold_count > 0 {
|
||||||
log::info!("unfolding ranges: {:?}", ranges);
|
log::info!("unfolding ranges: {:?}", ranges);
|
||||||
map.update(cx, |map, cx| {
|
map.update(cx, |map, cx| {
|
||||||
map.unfold(ranges, true, cx);
|
map.unfold_intersecting(ranges, true, cx);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
log::info!("folding ranges: {:?}", ranges);
|
log::info!("folding ranges: {:?}", ranges);
|
||||||
|
|
|
@ -6,12 +6,14 @@ use gpui::{AnyElement, ElementId, WindowContext};
|
||||||
use language::{Chunk, ChunkRenderer, Edit, Point, TextSummary};
|
use language::{Chunk, ChunkRenderer, Edit, Point, TextSummary};
|
||||||
use multi_buffer::{Anchor, AnchorRangeExt, MultiBufferRow, MultiBufferSnapshot, ToOffset};
|
use multi_buffer::{Anchor, AnchorRangeExt, MultiBufferRow, MultiBufferSnapshot, ToOffset};
|
||||||
use std::{
|
use std::{
|
||||||
|
any::TypeId,
|
||||||
cmp::{self, Ordering},
|
cmp::{self, Ordering},
|
||||||
fmt, iter,
|
fmt, iter,
|
||||||
ops::{Add, AddAssign, Deref, DerefMut, Range, Sub},
|
ops::{Add, AddAssign, Deref, DerefMut, Range, Sub},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
use sum_tree::{Bias, Cursor, FilterCursor, SumTree, Summary};
|
use sum_tree::{Bias, Cursor, FilterCursor, SumTree, Summary};
|
||||||
|
use ui::IntoElement as _;
|
||||||
use util::post_inc;
|
use util::post_inc;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -22,17 +24,29 @@ pub struct FoldPlaceholder {
|
||||||
pub constrain_width: bool,
|
pub constrain_width: bool,
|
||||||
/// If true, merges the fold with an adjacent one.
|
/// If true, merges the fold with an adjacent one.
|
||||||
pub merge_adjacent: bool,
|
pub merge_adjacent: bool,
|
||||||
|
/// Category of the fold. Useful for carefully removing from overlapping folds.
|
||||||
|
pub type_tag: Option<TypeId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FoldPlaceholder {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
render: Arc::new(|_, _, _| gpui::Empty.into_any_element()),
|
||||||
|
constrain_width: true,
|
||||||
|
merge_adjacent: true,
|
||||||
|
type_tag: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FoldPlaceholder {
|
impl FoldPlaceholder {
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub fn test() -> Self {
|
pub fn test() -> Self {
|
||||||
use gpui::IntoElement;
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
render: Arc::new(|_id, _range, _cx| gpui::Empty.into_any_element()),
|
render: Arc::new(|_id, _range, _cx| gpui::Empty.into_any_element()),
|
||||||
constrain_width: true,
|
constrain_width: true,
|
||||||
merge_adjacent: true,
|
merge_adjacent: true,
|
||||||
|
type_tag: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -173,9 +187,34 @@ impl<'a> FoldMapWriter<'a> {
|
||||||
(self.0.snapshot.clone(), edits)
|
(self.0.snapshot.clone(), edits)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn unfold<T: ToOffset>(
|
/// Removes any folds with the given ranges.
|
||||||
|
pub(crate) fn remove_folds<T: ToOffset>(
|
||||||
&mut self,
|
&mut self,
|
||||||
ranges: impl IntoIterator<Item = Range<T>>,
|
ranges: impl IntoIterator<Item = Range<T>>,
|
||||||
|
type_id: TypeId,
|
||||||
|
) -> (FoldSnapshot, Vec<FoldEdit>) {
|
||||||
|
self.remove_folds_with(
|
||||||
|
ranges,
|
||||||
|
|fold| fold.placeholder.type_tag == Some(type_id),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes any folds whose ranges intersect the given ranges.
|
||||||
|
pub(crate) fn unfold_intersecting<T: ToOffset>(
|
||||||
|
&mut self,
|
||||||
|
ranges: impl IntoIterator<Item = Range<T>>,
|
||||||
|
inclusive: bool,
|
||||||
|
) -> (FoldSnapshot, Vec<FoldEdit>) {
|
||||||
|
self.remove_folds_with(ranges, |_| true, inclusive)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes any folds that intersect the given ranges and for which the given predicate
|
||||||
|
/// returns true.
|
||||||
|
fn remove_folds_with<T: ToOffset>(
|
||||||
|
&mut self,
|
||||||
|
ranges: impl IntoIterator<Item = Range<T>>,
|
||||||
|
should_unfold: impl Fn(&Fold) -> bool,
|
||||||
inclusive: bool,
|
inclusive: bool,
|
||||||
) -> (FoldSnapshot, Vec<FoldEdit>) {
|
) -> (FoldSnapshot, Vec<FoldEdit>) {
|
||||||
let mut edits = Vec::new();
|
let mut edits = Vec::new();
|
||||||
|
@ -183,21 +222,23 @@ impl<'a> FoldMapWriter<'a> {
|
||||||
let snapshot = self.0.snapshot.inlay_snapshot.clone();
|
let snapshot = self.0.snapshot.inlay_snapshot.clone();
|
||||||
let buffer = &snapshot.buffer;
|
let buffer = &snapshot.buffer;
|
||||||
for range in ranges.into_iter() {
|
for range in ranges.into_iter() {
|
||||||
// Remove intersecting folds and add their ranges to edits that are passed to sync.
|
let range = range.start.to_offset(buffer)..range.end.to_offset(buffer);
|
||||||
let mut folds_cursor =
|
let mut folds_cursor =
|
||||||
intersecting_folds(&snapshot, &self.0.snapshot.folds, range, inclusive);
|
intersecting_folds(&snapshot, &self.0.snapshot.folds, range.clone(), inclusive);
|
||||||
while let Some(fold) = folds_cursor.item() {
|
while let Some(fold) = folds_cursor.item() {
|
||||||
let offset_range =
|
let offset_range =
|
||||||
fold.range.start.to_offset(buffer)..fold.range.end.to_offset(buffer);
|
fold.range.start.to_offset(buffer)..fold.range.end.to_offset(buffer);
|
||||||
if offset_range.end > offset_range.start {
|
if should_unfold(fold) {
|
||||||
let inlay_range = snapshot.to_inlay_offset(offset_range.start)
|
if offset_range.end > offset_range.start {
|
||||||
..snapshot.to_inlay_offset(offset_range.end);
|
let inlay_range = snapshot.to_inlay_offset(offset_range.start)
|
||||||
edits.push(InlayEdit {
|
..snapshot.to_inlay_offset(offset_range.end);
|
||||||
old: inlay_range.clone(),
|
edits.push(InlayEdit {
|
||||||
new: inlay_range,
|
old: inlay_range.clone(),
|
||||||
});
|
new: inlay_range,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
fold_ixs_to_delete.push(*folds_cursor.start());
|
||||||
}
|
}
|
||||||
fold_ixs_to_delete.push(*folds_cursor.start());
|
|
||||||
folds_cursor.next(buffer);
|
folds_cursor.next(buffer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -665,6 +706,8 @@ impl FoldSnapshot {
|
||||||
where
|
where
|
||||||
T: ToOffset,
|
T: ToOffset,
|
||||||
{
|
{
|
||||||
|
let buffer = &self.inlay_snapshot.buffer;
|
||||||
|
let range = range.start.to_offset(buffer)..range.end.to_offset(buffer);
|
||||||
let mut folds = intersecting_folds(&self.inlay_snapshot, &self.folds, range, false);
|
let mut folds = intersecting_folds(&self.inlay_snapshot, &self.folds, range, false);
|
||||||
iter::from_fn(move || {
|
iter::from_fn(move || {
|
||||||
let item = folds.item();
|
let item = folds.item();
|
||||||
|
@ -821,15 +864,12 @@ fn push_isomorphic(transforms: &mut SumTree<Transform>, summary: TextSummary) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn intersecting_folds<'a, T>(
|
fn intersecting_folds<'a>(
|
||||||
inlay_snapshot: &'a InlaySnapshot,
|
inlay_snapshot: &'a InlaySnapshot,
|
||||||
folds: &'a SumTree<Fold>,
|
folds: &'a SumTree<Fold>,
|
||||||
range: Range<T>,
|
range: Range<usize>,
|
||||||
inclusive: bool,
|
inclusive: bool,
|
||||||
) -> FilterCursor<'a, impl 'a + FnMut(&FoldSummary) -> bool, Fold, usize>
|
) -> FilterCursor<'a, impl 'a + FnMut(&FoldSummary) -> bool, Fold, usize> {
|
||||||
where
|
|
||||||
T: ToOffset,
|
|
||||||
{
|
|
||||||
let buffer = &inlay_snapshot.buffer;
|
let buffer = &inlay_snapshot.buffer;
|
||||||
let start = buffer.anchor_before(range.start.to_offset(buffer));
|
let start = buffer.anchor_before(range.start.to_offset(buffer));
|
||||||
let end = buffer.anchor_after(range.end.to_offset(buffer));
|
let end = buffer.anchor_after(range.end.to_offset(buffer));
|
||||||
|
@ -1419,12 +1459,12 @@ mod tests {
|
||||||
assert_eq!(snapshot4.text(), "123a⋯c123456eee");
|
assert_eq!(snapshot4.text(), "123a⋯c123456eee");
|
||||||
|
|
||||||
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
|
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
|
||||||
writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), false);
|
writer.unfold_intersecting(Some(Point::new(0, 4)..Point::new(0, 4)), false);
|
||||||
let (snapshot5, _) = map.read(inlay_snapshot.clone(), vec![]);
|
let (snapshot5, _) = map.read(inlay_snapshot.clone(), vec![]);
|
||||||
assert_eq!(snapshot5.text(), "123a⋯c123456eee");
|
assert_eq!(snapshot5.text(), "123a⋯c123456eee");
|
||||||
|
|
||||||
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
|
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
|
||||||
writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), true);
|
writer.unfold_intersecting(Some(Point::new(0, 4)..Point::new(0, 4)), true);
|
||||||
let (snapshot6, _) = map.read(inlay_snapshot, vec![]);
|
let (snapshot6, _) = map.read(inlay_snapshot, vec![]);
|
||||||
assert_eq!(snapshot6.text(), "123aaaaa\nbbbbbb\nccc123456eee");
|
assert_eq!(snapshot6.text(), "123aaaaa\nbbbbbb\nccc123456eee");
|
||||||
}
|
}
|
||||||
|
@ -1913,7 +1953,7 @@ mod tests {
|
||||||
log::info!("unfolding {:?} (inclusive: {})", to_unfold, inclusive);
|
log::info!("unfolding {:?} (inclusive: {})", to_unfold, inclusive);
|
||||||
let (mut writer, snapshot, edits) = self.write(inlay_snapshot, vec![]);
|
let (mut writer, snapshot, edits) = self.write(inlay_snapshot, vec![]);
|
||||||
snapshot_edits.push((snapshot, edits));
|
snapshot_edits.push((snapshot, edits));
|
||||||
let (snapshot, edits) = writer.unfold(to_unfold, inclusive);
|
let (snapshot, edits) = writer.unfold_intersecting(to_unfold, inclusive);
|
||||||
snapshot_edits.push((snapshot, edits));
|
snapshot_edits.push((snapshot, edits));
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
|
|
@ -75,8 +75,8 @@ use gpui::{
|
||||||
AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds, ClipboardEntry,
|
AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds, ClipboardEntry,
|
||||||
ClipboardItem, Context, DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusOutEvent,
|
ClipboardItem, Context, DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusOutEvent,
|
||||||
FocusableView, FontId, FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext,
|
FocusableView, FontId, FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext,
|
||||||
ListSizingBehavior, Model, MouseButton, PaintQuad, ParentElement, Pixels, Render, SharedString,
|
ListSizingBehavior, Model, ModelContext, MouseButton, PaintQuad, ParentElement, Pixels, Render,
|
||||||
Size, StrikethroughStyle, Styled, StyledText, Subscription, Task, TextStyle,
|
SharedString, Size, StrikethroughStyle, Styled, StyledText, Subscription, Task, TextStyle,
|
||||||
TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle, View,
|
TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle, View,
|
||||||
ViewContext, ViewInputHandler, VisualContext, WeakFocusHandle, WeakView, WindowContext,
|
ViewContext, ViewInputHandler, VisualContext, WeakFocusHandle, WeakView, WindowContext,
|
||||||
};
|
};
|
||||||
|
@ -1849,7 +1849,7 @@ impl Editor {
|
||||||
editor
|
editor
|
||||||
.update(cx, |editor, cx| {
|
.update(cx, |editor, cx| {
|
||||||
editor.unfold_ranges(
|
editor.unfold_ranges(
|
||||||
[fold_range.start..fold_range.end],
|
&[fold_range.start..fold_range.end],
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
cx,
|
cx,
|
||||||
|
@ -1861,6 +1861,7 @@ impl Editor {
|
||||||
.into_any()
|
.into_any()
|
||||||
}),
|
}),
|
||||||
merge_adjacent: true,
|
merge_adjacent: true,
|
||||||
|
..Default::default()
|
||||||
};
|
};
|
||||||
let display_map = cx.new_model(|cx| {
|
let display_map = cx.new_model(|cx| {
|
||||||
DisplayMap::new(
|
DisplayMap::new(
|
||||||
|
@ -6810,7 +6811,7 @@ impl Editor {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.transact(cx, |this, cx| {
|
self.transact(cx, |this, cx| {
|
||||||
this.unfold_ranges(unfold_ranges, true, true, cx);
|
this.unfold_ranges(&unfold_ranges, true, true, cx);
|
||||||
this.buffer.update(cx, |buffer, cx| {
|
this.buffer.update(cx, |buffer, cx| {
|
||||||
for (range, text) in edits {
|
for (range, text) in edits {
|
||||||
buffer.edit([(range, text)], None, cx);
|
buffer.edit([(range, text)], None, cx);
|
||||||
|
@ -6904,7 +6905,7 @@ impl Editor {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.transact(cx, |this, cx| {
|
self.transact(cx, |this, cx| {
|
||||||
this.unfold_ranges(unfold_ranges, true, true, cx);
|
this.unfold_ranges(&unfold_ranges, true, true, cx);
|
||||||
this.buffer.update(cx, |buffer, cx| {
|
this.buffer.update(cx, |buffer, cx| {
|
||||||
for (range, text) in edits {
|
for (range, text) in edits {
|
||||||
buffer.edit([(range, text)], None, cx);
|
buffer.edit([(range, text)], None, cx);
|
||||||
|
@ -8256,7 +8257,7 @@ impl Editor {
|
||||||
to_unfold.push(selection.start..selection.end);
|
to_unfold.push(selection.start..selection.end);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.unfold_ranges(to_unfold, true, true, cx);
|
self.unfold_ranges(&to_unfold, true, true, cx);
|
||||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||||
s.select_ranges(new_selection_ranges);
|
s.select_ranges(new_selection_ranges);
|
||||||
});
|
});
|
||||||
|
@ -8387,7 +8388,7 @@ impl Editor {
|
||||||
auto_scroll: Option<Autoscroll>,
|
auto_scroll: Option<Autoscroll>,
|
||||||
cx: &mut ViewContext<Editor>,
|
cx: &mut ViewContext<Editor>,
|
||||||
) {
|
) {
|
||||||
this.unfold_ranges([range.clone()], false, true, cx);
|
this.unfold_ranges(&[range.clone()], false, true, cx);
|
||||||
this.change_selections(auto_scroll, cx, |s| {
|
this.change_selections(auto_scroll, cx, |s| {
|
||||||
if replace_newest {
|
if replace_newest {
|
||||||
s.delete(s.newest_anchor().id);
|
s.delete(s.newest_anchor().id);
|
||||||
|
@ -8598,7 +8599,10 @@ impl Editor {
|
||||||
|
|
||||||
select_next_state.done = true;
|
select_next_state.done = true;
|
||||||
self.unfold_ranges(
|
self.unfold_ranges(
|
||||||
new_selections.iter().map(|selection| selection.range()),
|
&new_selections
|
||||||
|
.iter()
|
||||||
|
.map(|selection| selection.range())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
cx,
|
cx,
|
||||||
|
@ -8667,7 +8671,7 @@ impl Editor {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(next_selected_range) = next_selected_range {
|
if let Some(next_selected_range) = next_selected_range {
|
||||||
self.unfold_ranges([next_selected_range.clone()], false, true, cx);
|
self.unfold_ranges(&[next_selected_range.clone()], false, true, cx);
|
||||||
self.change_selections(Some(Autoscroll::newest()), cx, |s| {
|
self.change_selections(Some(Autoscroll::newest()), cx, |s| {
|
||||||
if action.replace_newest {
|
if action.replace_newest {
|
||||||
s.delete(s.newest_anchor().id);
|
s.delete(s.newest_anchor().id);
|
||||||
|
@ -8744,7 +8748,7 @@ impl Editor {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.unfold_ranges(
|
self.unfold_ranges(
|
||||||
selections.iter().map(|s| s.range()).collect::<Vec<_>>(),
|
&selections.iter().map(|s| s.range()).collect::<Vec<_>>(),
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
cx,
|
cx,
|
||||||
|
@ -10986,7 +10990,7 @@ impl Editor {
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
self.unfold_ranges(ranges, true, true, cx);
|
self.unfold_ranges(&ranges, true, true, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unfold_recursive(&mut self, _: &UnfoldRecursive, cx: &mut ViewContext<Self>) {
|
pub fn unfold_recursive(&mut self, _: &UnfoldRecursive, cx: &mut ViewContext<Self>) {
|
||||||
|
@ -11004,7 +11008,7 @@ impl Editor {
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
self.unfold_ranges(ranges, true, true, cx);
|
self.unfold_ranges(&ranges, true, true, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unfold_at(&mut self, unfold_at: &UnfoldAt, cx: &mut ViewContext<Self>) {
|
pub fn unfold_at(&mut self, unfold_at: &UnfoldAt, cx: &mut ViewContext<Self>) {
|
||||||
|
@ -11022,13 +11026,13 @@ impl Editor {
|
||||||
.iter()
|
.iter()
|
||||||
.any(|selection| RangeExt::overlaps(&selection.range(), &intersection_range));
|
.any(|selection| RangeExt::overlaps(&selection.range(), &intersection_range));
|
||||||
|
|
||||||
self.unfold_ranges(std::iter::once(intersection_range), true, autoscroll, cx)
|
self.unfold_ranges(&[intersection_range], true, autoscroll, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unfold_all(&mut self, _: &actions::UnfoldAll, cx: &mut ViewContext<Self>) {
|
pub fn unfold_all(&mut self, _: &actions::UnfoldAll, cx: &mut ViewContext<Self>) {
|
||||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||||
self.unfold_ranges(
|
self.unfold_ranges(
|
||||||
[Point::zero()..display_map.max_point().to_point(&display_map)],
|
&[Point::zero()..display_map.max_point().to_point(&display_map)],
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
cx,
|
cx,
|
||||||
|
@ -11104,39 +11108,63 @@ impl Editor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Removes any folds whose ranges intersect any of the given ranges.
|
||||||
pub fn unfold_ranges<T: ToOffset + Clone>(
|
pub fn unfold_ranges<T: ToOffset + Clone>(
|
||||||
&mut self,
|
&mut self,
|
||||||
ranges: impl IntoIterator<Item = Range<T>>,
|
ranges: &[Range<T>],
|
||||||
inclusive: bool,
|
inclusive: bool,
|
||||||
auto_scroll: bool,
|
auto_scroll: bool,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
let mut unfold_ranges = Vec::new();
|
self.remove_folds_with(ranges, auto_scroll, cx, |map, cx| {
|
||||||
|
map.unfold_intersecting(ranges.iter().cloned(), inclusive, cx)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes any folds with the given ranges.
|
||||||
|
pub fn remove_folds_with_type<T: ToOffset + Clone>(
|
||||||
|
&mut self,
|
||||||
|
ranges: &[Range<T>],
|
||||||
|
type_id: TypeId,
|
||||||
|
auto_scroll: bool,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
self.remove_folds_with(ranges, auto_scroll, cx, |map, cx| {
|
||||||
|
map.remove_folds_with_type(ranges.iter().cloned(), type_id, cx)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_folds_with<T: ToOffset + Clone>(
|
||||||
|
&mut self,
|
||||||
|
ranges: &[Range<T>],
|
||||||
|
auto_scroll: bool,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
update: impl FnOnce(&mut DisplayMap, &mut ModelContext<DisplayMap>),
|
||||||
|
) {
|
||||||
|
if ranges.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let mut buffers_affected = HashMap::default();
|
let mut buffers_affected = HashMap::default();
|
||||||
let multi_buffer = self.buffer().read(cx);
|
let multi_buffer = self.buffer().read(cx);
|
||||||
for range in ranges {
|
for range in ranges {
|
||||||
if let Some((_, buffer, _)) = multi_buffer.excerpt_containing(range.start.clone(), cx) {
|
if let Some((_, buffer, _)) = multi_buffer.excerpt_containing(range.start.clone(), cx) {
|
||||||
buffers_affected.insert(buffer.read(cx).remote_id(), buffer);
|
buffers_affected.insert(buffer.read(cx).remote_id(), buffer);
|
||||||
};
|
};
|
||||||
unfold_ranges.push(range);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut ranges = unfold_ranges.into_iter().peekable();
|
self.display_map.update(cx, update);
|
||||||
if ranges.peek().is_some() {
|
if auto_scroll {
|
||||||
self.display_map
|
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||||
.update(cx, |map, cx| map.unfold(ranges, inclusive, cx));
|
|
||||||
if auto_scroll {
|
|
||||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
for buffer in buffers_affected.into_values() {
|
|
||||||
self.sync_expanded_diff_hunks(buffer, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.notify();
|
|
||||||
self.scrollbar_marker_state.dirty = true;
|
|
||||||
self.active_indent_guides_state.dirty = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for buffer in buffers_affected.into_values() {
|
||||||
|
self.sync_expanded_diff_hunks(buffer, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
self.scrollbar_marker_state.dirty = true;
|
||||||
|
self.active_indent_guides_state.dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default_fold_placeholder(&self, cx: &AppContext) -> FoldPlaceholder {
|
pub fn default_fold_placeholder(&self, cx: &AppContext) -> FoldPlaceholder {
|
||||||
|
|
|
@ -1280,7 +1280,7 @@ impl SearchableItem for Editor {
|
||||||
matches: &[Range<Anchor>],
|
matches: &[Range<Anchor>],
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
self.unfold_ranges([matches[index].clone()], false, true, cx);
|
self.unfold_ranges(&[matches[index].clone()], false, true, cx);
|
||||||
let range = self.range_for_match(&matches[index]);
|
let range = self.range_for_match(&matches[index]);
|
||||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||||
s.select_ranges([range]);
|
s.select_ranges([range]);
|
||||||
|
@ -1288,7 +1288,7 @@ impl SearchableItem for Editor {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
|
fn select_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
|
||||||
self.unfold_ranges(matches.to_vec(), false, false, cx);
|
self.unfold_ranges(matches, false, false, cx);
|
||||||
let mut ranges = Vec::new();
|
let mut ranges = Vec::new();
|
||||||
for m in matches {
|
for m in matches {
|
||||||
ranges.push(self.range_for_match(m))
|
ranges.push(self.range_for_match(m))
|
||||||
|
|
|
@ -2251,10 +2251,14 @@ message ContextOperation {
|
||||||
InsertMessage insert_message = 1;
|
InsertMessage insert_message = 1;
|
||||||
UpdateMessage update_message = 2;
|
UpdateMessage update_message = 2;
|
||||||
UpdateSummary update_summary = 3;
|
UpdateSummary update_summary = 3;
|
||||||
SlashCommandFinished slash_command_finished = 4;
|
|
||||||
BufferOperation buffer_operation = 5;
|
BufferOperation buffer_operation = 5;
|
||||||
|
SlashCommandStarted slash_command_started = 6;
|
||||||
|
SlashCommandOutputSectionAdded slash_command_output_section_added = 7;
|
||||||
|
SlashCommandCompleted slash_command_completed = 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reserved 4;
|
||||||
|
|
||||||
message InsertMessage {
|
message InsertMessage {
|
||||||
ContextMessage message = 1;
|
ContextMessage message = 1;
|
||||||
repeated VectorClockEntry version = 2;
|
repeated VectorClockEntry version = 2;
|
||||||
|
@ -2275,13 +2279,26 @@ message ContextOperation {
|
||||||
repeated VectorClockEntry version = 4;
|
repeated VectorClockEntry version = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SlashCommandFinished {
|
message SlashCommandStarted {
|
||||||
LamportTimestamp id = 1;
|
LamportTimestamp id = 1;
|
||||||
AnchorRange output_range = 2;
|
AnchorRange output_range = 2;
|
||||||
repeated SlashCommandOutputSection sections = 3;
|
string name = 3;
|
||||||
repeated VectorClockEntry version = 4;
|
repeated VectorClockEntry version = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message SlashCommandOutputSectionAdded {
|
||||||
|
LamportTimestamp timestamp = 1;
|
||||||
|
SlashCommandOutputSection section = 2;
|
||||||
|
repeated VectorClockEntry version = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SlashCommandCompleted {
|
||||||
|
LamportTimestamp id = 1;
|
||||||
|
LamportTimestamp timestamp = 3;
|
||||||
|
optional string error_message = 4;
|
||||||
|
repeated VectorClockEntry version = 5;
|
||||||
|
}
|
||||||
|
|
||||||
message BufferOperation {
|
message BufferOperation {
|
||||||
Operation operation = 1;
|
Operation operation = 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1067,7 +1067,7 @@ impl ProjectSearchView {
|
||||||
let range_to_select = match_ranges[new_index].clone();
|
let range_to_select = match_ranges[new_index].clone();
|
||||||
self.results_editor.update(cx, |editor, cx| {
|
self.results_editor.update(cx, |editor, cx| {
|
||||||
let range_to_select = editor.range_for_match(&range_to_select);
|
let range_to_select = editor.range_for_match(&range_to_select);
|
||||||
editor.unfold_ranges([range_to_select.clone()], false, true, cx);
|
editor.unfold_ranges(&[range_to_select.clone()], false, true, cx);
|
||||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||||
s.select_ranges([range_to_select])
|
s.select_ranges([range_to_select])
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue