mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-19 02:17:35 +03:00
Restructure workflow step resolution and fix inserting newlines (#15720)
Release Notes: - N/A --------- Co-authored-by: Nathan <nathan@zed.dev>
This commit is contained in:
parent
49e736d8ef
commit
0ec29d6866
3
Cargo.lock
generated
3
Cargo.lock
generated
@ -422,7 +422,6 @@ dependencies = [
|
|||||||
"settings",
|
"settings",
|
||||||
"similar",
|
"similar",
|
||||||
"smol",
|
"smol",
|
||||||
"strsim 0.11.1",
|
|
||||||
"telemetry_events",
|
"telemetry_events",
|
||||||
"terminal",
|
"terminal",
|
||||||
"terminal_view",
|
"terminal_view",
|
||||||
@ -5952,6 +5951,7 @@ dependencies = [
|
|||||||
"similar",
|
"similar",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"smol",
|
"smol",
|
||||||
|
"strsim 0.11.1",
|
||||||
"sum_tree",
|
"sum_tree",
|
||||||
"task",
|
"task",
|
||||||
"text",
|
"text",
|
||||||
@ -5994,6 +5994,7 @@ dependencies = [
|
|||||||
"menu",
|
"menu",
|
||||||
"ollama",
|
"ollama",
|
||||||
"open_ai",
|
"open_ai",
|
||||||
|
"parking_lot",
|
||||||
"project",
|
"project",
|
||||||
"proto",
|
"proto",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
|
@ -67,7 +67,6 @@ serde_json.workspace = true
|
|||||||
settings.workspace = true
|
settings.workspace = true
|
||||||
similar.workspace = true
|
similar.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
strsim.workspace = true
|
|
||||||
telemetry_events.workspace = true
|
telemetry_events.workspace = true
|
||||||
terminal.workspace = true
|
terminal.workspace = true
|
||||||
terminal_view.workspace = true
|
terminal_view.workspace = true
|
||||||
@ -86,6 +85,7 @@ ctor.workspace = true
|
|||||||
editor = { workspace = true, features = ["test-support"] }
|
editor = { workspace = true, features = ["test-support"] }
|
||||||
env_logger.workspace = true
|
env_logger.workspace = true
|
||||||
language = { workspace = true, features = ["test-support"] }
|
language = { workspace = true, features = ["test-support"] }
|
||||||
|
language_model = { workspace = true, features = ["test-support"] }
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
project = { workspace = true, features = ["test-support"] }
|
project = { workspace = true, features = ["test-support"] }
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
|
@ -9,10 +9,11 @@ use crate::{
|
|||||||
},
|
},
|
||||||
terminal_inline_assistant::TerminalInlineAssistant,
|
terminal_inline_assistant::TerminalInlineAssistant,
|
||||||
Assist, ConfirmCommand, Context, ContextEvent, ContextId, ContextStore, CycleMessageRole,
|
Assist, ConfirmCommand, Context, ContextEvent, ContextId, ContextStore, CycleMessageRole,
|
||||||
DebugEditSteps, DeployHistory, DeployPromptLibrary, EditStep, EditStepState,
|
DebugEditSteps, DeployHistory, DeployPromptLibrary, EditSuggestionGroup, InlineAssist,
|
||||||
EditStepSuggestions, InlineAssist, InlineAssistId, InlineAssistant, InsertIntoEditor,
|
InlineAssistId, InlineAssistant, InsertIntoEditor, MessageStatus, ModelSelector,
|
||||||
MessageStatus, ModelSelector, PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection,
|
PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection, RemoteContextMetadata,
|
||||||
RemoteContextMetadata, SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector,
|
SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector, WorkflowStep,
|
||||||
|
WorkflowStepEditSuggestions,
|
||||||
};
|
};
|
||||||
use crate::{ContextStoreEvent, ShowConfiguration};
|
use crate::{ContextStoreEvent, ShowConfiguration};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
@ -39,7 +40,8 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
use indexed_docs::IndexedDocsStore;
|
use indexed_docs::IndexedDocsStore;
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::SoftWrap, Capability, LanguageRegistry, LspAdapterDelegate, Point, ToOffset,
|
language_settings::SoftWrap, Buffer, Capability, LanguageRegistry, LspAdapterDelegate, Point,
|
||||||
|
ToOffset,
|
||||||
};
|
};
|
||||||
use language_model::{
|
use language_model::{
|
||||||
provider::cloud::PROVIDER_ID, LanguageModelProvider, LanguageModelProviderId,
|
provider::cloud::PROVIDER_ID, LanguageModelProvider, LanguageModelProviderId,
|
||||||
@ -1284,7 +1286,6 @@ struct ActiveEditStep {
|
|||||||
start: language::Anchor,
|
start: language::Anchor,
|
||||||
assist_ids: Vec<InlineAssistId>,
|
assist_ids: Vec<InlineAssistId>,
|
||||||
editor: Option<WeakView<Editor>>,
|
editor: Option<WeakView<Editor>>,
|
||||||
_open_editor: Task<Result<()>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ContextEditor {
|
pub struct ContextEditor {
|
||||||
@ -1452,23 +1453,21 @@ impl ContextEditor {
|
|||||||
.read(cx)
|
.read(cx)
|
||||||
.buffer()
|
.buffer()
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.text_for_range(step.source_range.clone())
|
.text_for_range(step.tagged_range.clone())
|
||||||
.collect::<String>()
|
.collect::<String>()
|
||||||
));
|
));
|
||||||
match &step.state {
|
match &step.edit_suggestions {
|
||||||
Some(EditStepState::Resolved(resolution)) => {
|
WorkflowStepEditSuggestions::Resolved {
|
||||||
|
title,
|
||||||
|
edit_suggestions,
|
||||||
|
} => {
|
||||||
output.push_str("Resolution:\n");
|
output.push_str("Resolution:\n");
|
||||||
output.push_str(&format!(" {:?}\n", resolution.step_title));
|
output.push_str(&format!(" {:?}\n", title));
|
||||||
for op in &resolution.operations {
|
output.push_str(&format!(" {:?}\n", edit_suggestions));
|
||||||
output.push_str(&format!(" {:?}\n", op));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Some(EditStepState::Pending(_)) => {
|
WorkflowStepEditSuggestions::Pending(_) => {
|
||||||
output.push_str("Resolution: Pending\n");
|
output.push_str("Resolution: Pending\n");
|
||||||
}
|
}
|
||||||
None => {
|
|
||||||
output.push_str("Resolution: None\n");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
output.push('\n');
|
output.push('\n');
|
||||||
}
|
}
|
||||||
@ -1875,222 +1874,165 @@ impl ContextEditor {
|
|||||||
}
|
}
|
||||||
EditorEvent::SelectionsChanged { .. } => {
|
EditorEvent::SelectionsChanged { .. } => {
|
||||||
self.scroll_position = self.cursor_scroll_position(cx);
|
self.scroll_position = self.cursor_scroll_position(cx);
|
||||||
if self
|
self.update_active_workflow_step(cx);
|
||||||
.edit_step_for_cursor(cx)
|
|
||||||
.map(|step| step.source_range.start)
|
|
||||||
!= self.active_edit_step.as_ref().map(|step| step.start)
|
|
||||||
{
|
|
||||||
if let Some(old_active_edit_step) = self.active_edit_step.take() {
|
|
||||||
if let Some(editor) = old_active_edit_step
|
|
||||||
.editor
|
|
||||||
.and_then(|editor| editor.upgrade())
|
|
||||||
{
|
|
||||||
self.workspace
|
|
||||||
.update(cx, |workspace, cx| {
|
|
||||||
if let Some(pane) = workspace.pane_for(&editor) {
|
|
||||||
pane.update(cx, |pane, cx| {
|
|
||||||
let item_id = editor.entity_id();
|
|
||||||
if pane.is_active_preview_item(item_id) {
|
|
||||||
pane.close_item_by_id(
|
|
||||||
item_id,
|
|
||||||
SaveIntent::Skip,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.detach_and_log_err(cx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(new_active_step) = self.edit_step_for_cursor(cx) {
|
|
||||||
let start = new_active_step.source_range.start;
|
|
||||||
let open_editor = new_active_step
|
|
||||||
.edit_suggestions(&self.project, cx)
|
|
||||||
.map(|suggestions| {
|
|
||||||
self.open_editor_for_edit_suggestions(suggestions, cx)
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| Task::ready(Ok(())));
|
|
||||||
self.active_edit_step = Some(ActiveEditStep {
|
|
||||||
start,
|
|
||||||
assist_ids: Vec::new(),
|
|
||||||
editor: None,
|
|
||||||
_open_editor: open_editor,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
cx.emit(event.clone());
|
cx.emit(event.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_editor_for_edit_suggestions(
|
fn update_active_workflow_step(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
&mut self,
|
if self
|
||||||
edit_step_suggestions: Task<EditStepSuggestions>,
|
.workflow_step_for_cursor(cx)
|
||||||
cx: &mut ViewContext<Self>,
|
.map(|step| step.tagged_range.start)
|
||||||
) -> Task<Result<()>> {
|
!= self.active_edit_step.as_ref().map(|step| step.start)
|
||||||
let workspace = self.workspace.clone();
|
{
|
||||||
let project = self.project.clone();
|
if let Some(old_active_edit_step) = self.active_edit_step.take() {
|
||||||
let assistant_panel = self.assistant_panel.clone();
|
if let Some(editor) = old_active_edit_step
|
||||||
cx.spawn(|this, mut cx| async move {
|
.editor
|
||||||
let edit_step_suggestions = edit_step_suggestions.await;
|
.and_then(|editor| editor.upgrade())
|
||||||
|
{
|
||||||
|
self.workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
if let Some(pane) = workspace.pane_for(&editor) {
|
||||||
|
pane.update(cx, |pane, cx| {
|
||||||
|
let item_id = editor.entity_id();
|
||||||
|
if pane.is_active_preview_item(item_id) {
|
||||||
|
pane.close_item_by_id(item_id, SaveIntent::Skip, cx)
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut assist_ids = Vec::new();
|
if let Some(new_active_step) = self.workflow_step_for_cursor(cx) {
|
||||||
let editor = if edit_step_suggestions.suggestions.is_empty() {
|
let start = new_active_step.tagged_range.start;
|
||||||
return Ok(());
|
|
||||||
} else if edit_step_suggestions.suggestions.len() == 1
|
let mut editor = None;
|
||||||
&& edit_step_suggestions
|
let mut assist_ids = Vec::new();
|
||||||
.suggestions
|
if let WorkflowStepEditSuggestions::Resolved {
|
||||||
.values()
|
title,
|
||||||
.next()
|
edit_suggestions,
|
||||||
.unwrap()
|
} = &new_active_step.edit_suggestions
|
||||||
.len()
|
{
|
||||||
== 1
|
if let Some((opened_editor, inline_assist_ids)) =
|
||||||
{
|
self.suggest_edits(title.clone(), edit_suggestions.clone(), cx)
|
||||||
// If there's only one buffer and one suggestion group, open it directly
|
{
|
||||||
let (buffer, suggestion_groups) = edit_step_suggestions
|
editor = Some(opened_editor.downgrade());
|
||||||
.suggestions
|
assist_ids = inline_assist_ids;
|
||||||
.into_iter()
|
}
|
||||||
.next()
|
}
|
||||||
.unwrap();
|
|
||||||
let suggestion_group = suggestion_groups.into_iter().next().unwrap();
|
self.active_edit_step = Some(ActiveEditStep {
|
||||||
let editor = workspace.update(&mut cx, |workspace, cx| {
|
start,
|
||||||
|
assist_ids,
|
||||||
|
editor,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn suggest_edits(
|
||||||
|
&mut self,
|
||||||
|
title: String,
|
||||||
|
edit_suggestions: HashMap<Model<Buffer>, Vec<EditSuggestionGroup>>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Option<(View<Editor>, Vec<InlineAssistId>)> {
|
||||||
|
let assistant_panel = self.assistant_panel.upgrade()?;
|
||||||
|
if edit_suggestions.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let editor;
|
||||||
|
let mut suggestion_groups = Vec::new();
|
||||||
|
if edit_suggestions.len() == 1 && edit_suggestions.values().next().unwrap().len() == 1 {
|
||||||
|
// If there's only one buffer and one suggestion group, open it directly
|
||||||
|
let (buffer, groups) = edit_suggestions.into_iter().next().unwrap();
|
||||||
|
let group = groups.into_iter().next().unwrap();
|
||||||
|
editor = self
|
||||||
|
.workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
let active_pane = workspace.active_pane().clone();
|
let active_pane = workspace.active_pane().clone();
|
||||||
workspace.open_project_item::<Editor>(active_pane, buffer, false, false, cx)
|
workspace.open_project_item::<Editor>(active_pane, buffer, false, false, cx)
|
||||||
})?;
|
})
|
||||||
|
.log_err()?;
|
||||||
|
|
||||||
cx.update(|cx| {
|
let (&excerpt_id, _, _) = editor
|
||||||
for suggestion in suggestion_group.suggestions {
|
.read(cx)
|
||||||
let description = suggestion.description.unwrap_or_else(|| "Delete".into());
|
.buffer()
|
||||||
|
.read(cx)
|
||||||
|
.read(cx)
|
||||||
|
.as_singleton()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let range = {
|
// Scroll the editor to the suggested assist
|
||||||
let multibuffer = editor.read(cx).buffer().read(cx).read(cx);
|
editor.update(cx, |editor, cx| {
|
||||||
let (&excerpt_id, _, _) = multibuffer.as_singleton().unwrap();
|
let multibuffer = editor.buffer().read(cx).snapshot(cx);
|
||||||
multibuffer
|
let (&excerpt_id, _, buffer) = multibuffer.as_singleton().unwrap();
|
||||||
.anchor_in_excerpt(excerpt_id, suggestion.range.start)
|
let anchor = if group.context_range.start.to_offset(buffer) == 0 {
|
||||||
.unwrap()
|
Anchor::min()
|
||||||
..multibuffer
|
} else {
|
||||||
.anchor_in_excerpt(excerpt_id, suggestion.range.end)
|
|
||||||
.unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
InlineAssistant::update_global(cx, |assistant, cx| {
|
|
||||||
let suggestion_id = assistant.suggest_assist(
|
|
||||||
&editor,
|
|
||||||
range,
|
|
||||||
description,
|
|
||||||
suggestion.initial_insertion,
|
|
||||||
Some(workspace.clone()),
|
|
||||||
assistant_panel.upgrade().as_ref(),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
assist_ids.push(suggestion_id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scroll the editor to the suggested assist
|
|
||||||
editor.update(cx, |editor, cx| {
|
|
||||||
let multibuffer = editor.buffer().read(cx).snapshot(cx);
|
|
||||||
let (&excerpt_id, _, buffer) = multibuffer.as_singleton().unwrap();
|
|
||||||
let anchor = if suggestion_group.context_range.start.to_offset(buffer) == 0
|
|
||||||
{
|
|
||||||
Anchor::min()
|
|
||||||
} else {
|
|
||||||
multibuffer
|
|
||||||
.anchor_in_excerpt(excerpt_id, suggestion_group.context_range.start)
|
|
||||||
.unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
editor.set_scroll_anchor(
|
|
||||||
ScrollAnchor {
|
|
||||||
offset: gpui::Point::default(),
|
|
||||||
anchor,
|
|
||||||
},
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
})?;
|
|
||||||
|
|
||||||
editor
|
|
||||||
} else {
|
|
||||||
// If there are multiple buffers or suggestion groups, create a multibuffer
|
|
||||||
let mut inline_assist_suggestions = Vec::new();
|
|
||||||
let multibuffer = cx.new_model(|cx| {
|
|
||||||
let replica_id = project.read(cx).replica_id();
|
|
||||||
let mut multibuffer = MultiBuffer::new(replica_id, Capability::ReadWrite)
|
|
||||||
.with_title(edit_step_suggestions.title);
|
|
||||||
for (buffer, suggestion_groups) in edit_step_suggestions.suggestions {
|
|
||||||
let excerpt_ids = multibuffer.push_excerpts(
|
|
||||||
buffer,
|
|
||||||
suggestion_groups
|
|
||||||
.iter()
|
|
||||||
.map(|suggestion_group| ExcerptRange {
|
|
||||||
context: suggestion_group.context_range.clone(),
|
|
||||||
primary: None,
|
|
||||||
}),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
|
|
||||||
for (excerpt_id, suggestion_group) in
|
|
||||||
excerpt_ids.into_iter().zip(suggestion_groups)
|
|
||||||
{
|
|
||||||
for suggestion in suggestion_group.suggestions {
|
|
||||||
let description =
|
|
||||||
suggestion.description.unwrap_or_else(|| "Delete".into());
|
|
||||||
let range = {
|
|
||||||
let multibuffer = multibuffer.read(cx);
|
|
||||||
multibuffer
|
|
||||||
.anchor_in_excerpt(excerpt_id, suggestion.range.start)
|
|
||||||
.unwrap()
|
|
||||||
..multibuffer
|
|
||||||
.anchor_in_excerpt(excerpt_id, suggestion.range.end)
|
|
||||||
.unwrap()
|
|
||||||
};
|
|
||||||
inline_assist_suggestions.push((
|
|
||||||
range,
|
|
||||||
description,
|
|
||||||
suggestion.initial_insertion,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
multibuffer
|
multibuffer
|
||||||
})?;
|
.anchor_in_excerpt(excerpt_id, group.context_range.start)
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
let editor = cx
|
editor.set_scroll_anchor(
|
||||||
.new_view(|cx| Editor::for_multibuffer(multibuffer, Some(project), true, cx))?;
|
ScrollAnchor {
|
||||||
cx.update(|cx| {
|
offset: gpui::Point::default(),
|
||||||
InlineAssistant::update_global(cx, |assistant, cx| {
|
anchor,
|
||||||
for (range, description, initial_insertion) in inline_assist_suggestions {
|
},
|
||||||
assist_ids.push(assistant.suggest_assist(
|
cx,
|
||||||
&editor,
|
);
|
||||||
range,
|
});
|
||||||
description,
|
|
||||||
initial_insertion,
|
|
||||||
Some(workspace.clone()),
|
|
||||||
assistant_panel.upgrade().as_ref(),
|
|
||||||
cx,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})?;
|
|
||||||
workspace.update(&mut cx, |workspace, cx| {
|
|
||||||
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, false, cx)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
editor
|
suggestion_groups.push((excerpt_id, group));
|
||||||
};
|
} else {
|
||||||
|
// If there are multiple buffers or suggestion groups, create a multibuffer
|
||||||
this.update(&mut cx, |this, _cx| {
|
let multibuffer = cx.new_model(|cx| {
|
||||||
if let Some(step) = this.active_edit_step.as_mut() {
|
let replica_id = self.project.read(cx).replica_id();
|
||||||
step.assist_ids = assist_ids;
|
let mut multibuffer =
|
||||||
step.editor = Some(editor.downgrade());
|
MultiBuffer::new(replica_id, Capability::ReadWrite).with_title(title);
|
||||||
|
for (buffer, groups) in edit_suggestions {
|
||||||
|
let excerpt_ids = multibuffer.push_excerpts(
|
||||||
|
buffer,
|
||||||
|
groups.iter().map(|suggestion_group| ExcerptRange {
|
||||||
|
context: suggestion_group.context_range.clone(),
|
||||||
|
primary: None,
|
||||||
|
}),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
suggestion_groups.extend(excerpt_ids.into_iter().zip(groups));
|
||||||
}
|
}
|
||||||
})
|
multibuffer
|
||||||
})
|
});
|
||||||
|
|
||||||
|
editor = cx.new_view(|cx| {
|
||||||
|
Editor::for_multibuffer(multibuffer, Some(self.project.clone()), true, cx)
|
||||||
|
});
|
||||||
|
self.workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, false, cx)
|
||||||
|
})
|
||||||
|
.log_err()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut assist_ids = Vec::new();
|
||||||
|
for (excerpt_id, suggestion_group) in suggestion_groups {
|
||||||
|
for suggestion in suggestion_group.suggestions {
|
||||||
|
assist_ids.extend(suggestion.show(
|
||||||
|
&editor,
|
||||||
|
excerpt_id,
|
||||||
|
&self.workspace,
|
||||||
|
&assistant_panel,
|
||||||
|
cx,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some((editor, assist_ids))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_editor_search_event(
|
fn handle_editor_search_event(
|
||||||
@ -2374,11 +2316,10 @@ impl ContextEditor {
|
|||||||
|
|
||||||
fn render_send_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render_send_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
let focus_handle = self.focus_handle(cx).clone();
|
let focus_handle = self.focus_handle(cx).clone();
|
||||||
let button_text = match self.edit_step_for_cursor(cx) {
|
let button_text = match self.workflow_step_for_cursor(cx) {
|
||||||
Some(edit_step) => match &edit_step.state {
|
Some(edit_step) => match &edit_step.edit_suggestions {
|
||||||
Some(EditStepState::Pending(_)) => "Computing Changes...",
|
WorkflowStepEditSuggestions::Pending(_) => "Computing Changes...",
|
||||||
Some(EditStepState::Resolved(_)) => "Apply Changes",
|
WorkflowStepEditSuggestions::Resolved { .. } => "Apply Changes",
|
||||||
None => "Send",
|
|
||||||
},
|
},
|
||||||
None => "Send",
|
None => "Send",
|
||||||
};
|
};
|
||||||
@ -2421,7 +2362,7 @@ impl ContextEditor {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn edit_step_for_cursor<'a>(&'a self, cx: &'a AppContext) -> Option<&'a EditStep> {
|
fn workflow_step_for_cursor<'a>(&'a self, cx: &'a AppContext) -> Option<&'a WorkflowStep> {
|
||||||
let newest_cursor = self
|
let newest_cursor = self
|
||||||
.editor
|
.editor
|
||||||
.read(cx)
|
.read(cx)
|
||||||
@ -2435,7 +2376,7 @@ impl ContextEditor {
|
|||||||
let edit_steps = context.edit_steps();
|
let edit_steps = context.edit_steps();
|
||||||
edit_steps
|
edit_steps
|
||||||
.binary_search_by(|step| {
|
.binary_search_by(|step| {
|
||||||
let step_range = step.source_range.clone();
|
let step_range = step.tagged_range.clone();
|
||||||
if newest_cursor.cmp(&step_range.start, buffer).is_lt() {
|
if newest_cursor.cmp(&step_range.start, buffer).is_lt() {
|
||||||
Ordering::Greater
|
Ordering::Greater
|
||||||
} else if newest_cursor.cmp(&step_range.end, buffer).is_gt() {
|
} else if newest_cursor.cmp(&step_range.end, buffer).is_gt() {
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -330,7 +330,12 @@ impl ContextStore {
|
|||||||
|
|
||||||
pub fn create(&mut self, cx: &mut ModelContext<Self>) -> Model<Context> {
|
pub fn create(&mut self, cx: &mut ModelContext<Self>) -> Model<Context> {
|
||||||
let context = cx.new_model(|cx| {
|
let context = cx.new_model(|cx| {
|
||||||
Context::local(self.languages.clone(), Some(self.telemetry.clone()), cx)
|
Context::local(
|
||||||
|
self.languages.clone(),
|
||||||
|
Some(self.project.clone()),
|
||||||
|
Some(self.telemetry.clone()),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
});
|
});
|
||||||
self.register_context(&context, cx);
|
self.register_context(&context, cx);
|
||||||
context
|
context
|
||||||
@ -351,6 +356,7 @@ impl ContextStore {
|
|||||||
let replica_id = project.replica_id();
|
let replica_id = project.replica_id();
|
||||||
let capability = project.capability();
|
let capability = project.capability();
|
||||||
let language_registry = self.languages.clone();
|
let language_registry = self.languages.clone();
|
||||||
|
let project = self.project.clone();
|
||||||
let telemetry = self.telemetry.clone();
|
let telemetry = self.telemetry.clone();
|
||||||
let request = self.client.request(proto::CreateContext { project_id });
|
let request = self.client.request(proto::CreateContext { project_id });
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
@ -363,6 +369,7 @@ impl ContextStore {
|
|||||||
replica_id,
|
replica_id,
|
||||||
capability,
|
capability,
|
||||||
language_registry,
|
language_registry,
|
||||||
|
Some(project),
|
||||||
Some(telemetry),
|
Some(telemetry),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
@ -401,6 +408,7 @@ impl ContextStore {
|
|||||||
|
|
||||||
let fs = self.fs.clone();
|
let fs = self.fs.clone();
|
||||||
let languages = self.languages.clone();
|
let languages = self.languages.clone();
|
||||||
|
let project = self.project.clone();
|
||||||
let telemetry = self.telemetry.clone();
|
let telemetry = self.telemetry.clone();
|
||||||
let load = cx.background_executor().spawn({
|
let load = cx.background_executor().spawn({
|
||||||
let path = path.clone();
|
let path = path.clone();
|
||||||
@ -413,7 +421,14 @@ impl ContextStore {
|
|||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
let saved_context = load.await?;
|
let saved_context = load.await?;
|
||||||
let context = cx.new_model(|cx| {
|
let context = cx.new_model(|cx| {
|
||||||
Context::deserialize(saved_context, path.clone(), languages, Some(telemetry), cx)
|
Context::deserialize(
|
||||||
|
saved_context,
|
||||||
|
path.clone(),
|
||||||
|
languages,
|
||||||
|
Some(project),
|
||||||
|
Some(telemetry),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
})?;
|
})?;
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
if let Some(existing_context) = this.loaded_context_for_path(&path, cx) {
|
if let Some(existing_context) = this.loaded_context_for_path(&path, cx) {
|
||||||
@ -472,6 +487,7 @@ impl ContextStore {
|
|||||||
let replica_id = project.replica_id();
|
let replica_id = project.replica_id();
|
||||||
let capability = project.capability();
|
let capability = project.capability();
|
||||||
let language_registry = self.languages.clone();
|
let language_registry = self.languages.clone();
|
||||||
|
let project = self.project.clone();
|
||||||
let telemetry = self.telemetry.clone();
|
let telemetry = self.telemetry.clone();
|
||||||
let request = self.client.request(proto::OpenContext {
|
let request = self.client.request(proto::OpenContext {
|
||||||
project_id,
|
project_id,
|
||||||
@ -486,6 +502,7 @@ impl ContextStore {
|
|||||||
replica_id,
|
replica_id,
|
||||||
capability,
|
capability,
|
||||||
language_registry,
|
language_registry,
|
||||||
|
Some(project),
|
||||||
Some(telemetry),
|
Some(telemetry),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
@ -237,7 +237,7 @@ impl InlineAssistant {
|
|||||||
editor: &View<Editor>,
|
editor: &View<Editor>,
|
||||||
mut range: Range<Anchor>,
|
mut range: Range<Anchor>,
|
||||||
initial_prompt: String,
|
initial_prompt: String,
|
||||||
initial_insertion: Option<InitialInsertion>,
|
initial_transaction_id: Option<TransactionId>,
|
||||||
workspace: Option<WeakView<Workspace>>,
|
workspace: Option<WeakView<Workspace>>,
|
||||||
assistant_panel: Option<&View<AssistantPanel>>,
|
assistant_panel: Option<&View<AssistantPanel>>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
@ -251,28 +251,15 @@ impl InlineAssistant {
|
|||||||
let buffer = editor.read(cx).buffer().clone();
|
let buffer = editor.read(cx).buffer().clone();
|
||||||
{
|
{
|
||||||
let snapshot = buffer.read(cx).read(cx);
|
let snapshot = buffer.read(cx).read(cx);
|
||||||
|
range.start = range.start.bias_left(&snapshot);
|
||||||
let mut point_range = range.to_point(&snapshot);
|
range.end = range.end.bias_right(&snapshot);
|
||||||
if point_range.is_empty() {
|
|
||||||
point_range.start.column = 0;
|
|
||||||
point_range.end.column = 0;
|
|
||||||
} else {
|
|
||||||
point_range.start.column = 0;
|
|
||||||
if point_range.end.row > point_range.start.row && point_range.end.column == 0 {
|
|
||||||
point_range.end.row -= 1;
|
|
||||||
}
|
|
||||||
point_range.end.column = snapshot.line_len(MultiBufferRow(point_range.end.row));
|
|
||||||
}
|
|
||||||
|
|
||||||
range.start = snapshot.anchor_before(point_range.start);
|
|
||||||
range.end = snapshot.anchor_after(point_range.end);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let codegen = cx.new_model(|cx| {
|
let codegen = cx.new_model(|cx| {
|
||||||
Codegen::new(
|
Codegen::new(
|
||||||
editor.read(cx).buffer().clone(),
|
editor.read(cx).buffer().clone(),
|
||||||
range.clone(),
|
range.clone(),
|
||||||
initial_insertion,
|
initial_transaction_id,
|
||||||
self.telemetry.clone(),
|
self.telemetry.clone(),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
@ -873,13 +860,20 @@ impl InlineAssistant {
|
|||||||
for assist_id in assist_ids {
|
for assist_id in assist_ids {
|
||||||
if let Some(assist) = self.assists.get(assist_id) {
|
if let Some(assist) = self.assists.get(assist_id) {
|
||||||
let codegen = assist.codegen.read(cx);
|
let codegen = assist.codegen.read(cx);
|
||||||
|
let buffer = codegen.buffer.read(cx).read(cx);
|
||||||
foreground_ranges.extend(codegen.last_equal_ranges().iter().cloned());
|
foreground_ranges.extend(codegen.last_equal_ranges().iter().cloned());
|
||||||
|
|
||||||
gutter_pending_ranges
|
let pending_range =
|
||||||
.push(codegen.edit_position.unwrap_or(assist.range.start)..assist.range.end);
|
codegen.edit_position.unwrap_or(assist.range.start)..assist.range.end;
|
||||||
|
if pending_range.end.to_offset(&buffer) > pending_range.start.to_offset(&buffer) {
|
||||||
|
gutter_pending_ranges.push(pending_range);
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(edit_position) = codegen.edit_position {
|
if let Some(edit_position) = codegen.edit_position {
|
||||||
gutter_transformed_ranges.push(assist.range.start..edit_position);
|
let edited_range = assist.range.start..edit_position;
|
||||||
|
if edited_range.end.to_offset(&buffer) > edited_range.start.to_offset(&buffer) {
|
||||||
|
gutter_transformed_ranges.push(edited_range);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if assist.decorations.is_some() {
|
if assist.decorations.is_some() {
|
||||||
@ -1997,13 +1991,13 @@ pub struct Codegen {
|
|||||||
snapshot: MultiBufferSnapshot,
|
snapshot: MultiBufferSnapshot,
|
||||||
edit_position: Option<Anchor>,
|
edit_position: Option<Anchor>,
|
||||||
last_equal_ranges: Vec<Range<Anchor>>,
|
last_equal_ranges: Vec<Range<Anchor>>,
|
||||||
transaction_id: Option<TransactionId>,
|
initial_transaction_id: Option<TransactionId>,
|
||||||
|
transformation_transaction_id: Option<TransactionId>,
|
||||||
status: CodegenStatus,
|
status: CodegenStatus,
|
||||||
generation: Task<()>,
|
generation: Task<()>,
|
||||||
diff: Diff,
|
diff: Diff,
|
||||||
telemetry: Option<Arc<Telemetry>>,
|
telemetry: Option<Arc<Telemetry>>,
|
||||||
_subscription: gpui::Subscription,
|
_subscription: gpui::Subscription,
|
||||||
initial_insertion: Option<InitialInsertion>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum CodegenStatus {
|
enum CodegenStatus {
|
||||||
@ -2027,7 +2021,7 @@ impl Codegen {
|
|||||||
pub fn new(
|
pub fn new(
|
||||||
buffer: Model<MultiBuffer>,
|
buffer: Model<MultiBuffer>,
|
||||||
range: Range<Anchor>,
|
range: Range<Anchor>,
|
||||||
initial_insertion: Option<InitialInsertion>,
|
initial_transaction_id: Option<TransactionId>,
|
||||||
telemetry: Option<Arc<Telemetry>>,
|
telemetry: Option<Arc<Telemetry>>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
@ -2059,13 +2053,13 @@ impl Codegen {
|
|||||||
edit_position: None,
|
edit_position: None,
|
||||||
snapshot,
|
snapshot,
|
||||||
last_equal_ranges: Default::default(),
|
last_equal_ranges: Default::default(),
|
||||||
transaction_id: None,
|
transformation_transaction_id: None,
|
||||||
status: CodegenStatus::Idle,
|
status: CodegenStatus::Idle,
|
||||||
generation: Task::ready(()),
|
generation: Task::ready(()),
|
||||||
diff: Diff::default(),
|
diff: Diff::default(),
|
||||||
telemetry,
|
telemetry,
|
||||||
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
|
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
|
||||||
initial_insertion,
|
initial_transaction_id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2076,8 +2070,8 @@ impl Codegen {
|
|||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) {
|
) {
|
||||||
if let multi_buffer::Event::TransactionUndone { transaction_id } = event {
|
if let multi_buffer::Event::TransactionUndone { transaction_id } = event {
|
||||||
if self.transaction_id == Some(*transaction_id) {
|
if self.transformation_transaction_id == Some(*transaction_id) {
|
||||||
self.transaction_id = None;
|
self.transformation_transaction_id = None;
|
||||||
self.generation = Task::ready(());
|
self.generation = Task::ready(());
|
||||||
cx.emit(CodegenEvent::Undone);
|
cx.emit(CodegenEvent::Undone);
|
||||||
}
|
}
|
||||||
@ -2105,7 +2099,7 @@ impl Codegen {
|
|||||||
|
|
||||||
pub fn start(
|
pub fn start(
|
||||||
&mut self,
|
&mut self,
|
||||||
mut edit_range: Range<Anchor>,
|
edit_range: Range<Anchor>,
|
||||||
user_prompt: String,
|
user_prompt: String,
|
||||||
assistant_panel_context: Option<LanguageModelRequest>,
|
assistant_panel_context: Option<LanguageModelRequest>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
@ -2114,34 +2108,13 @@ impl Codegen {
|
|||||||
.active_model()
|
.active_model()
|
||||||
.context("no active model")?;
|
.context("no active model")?;
|
||||||
|
|
||||||
self.undo(cx);
|
if let Some(transformation_transaction_id) = self.transformation_transaction_id.take() {
|
||||||
|
|
||||||
// Handle initial insertion
|
|
||||||
self.transaction_id = if let Some(initial_insertion) = self.initial_insertion {
|
|
||||||
self.buffer.update(cx, |buffer, cx| {
|
self.buffer.update(cx, |buffer, cx| {
|
||||||
buffer.start_transaction(cx);
|
buffer.undo_transaction(transformation_transaction_id, cx)
|
||||||
let offset = edit_range.start.to_offset(&self.snapshot);
|
});
|
||||||
let edit_position;
|
}
|
||||||
match initial_insertion {
|
|
||||||
InitialInsertion::NewlineBefore => {
|
self.edit_position = Some(edit_range.start.bias_right(&self.snapshot));
|
||||||
buffer.edit([(offset..offset, "\n\n")], None, cx);
|
|
||||||
self.snapshot = buffer.snapshot(cx);
|
|
||||||
edit_position = self.snapshot.anchor_after(offset + 1);
|
|
||||||
}
|
|
||||||
InitialInsertion::NewlineAfter => {
|
|
||||||
buffer.edit([(offset..offset, "\n")], None, cx);
|
|
||||||
self.snapshot = buffer.snapshot(cx);
|
|
||||||
edit_position = self.snapshot.anchor_after(offset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.edit_position = Some(edit_position);
|
|
||||||
edit_range = edit_position.bias_left(&self.snapshot)..edit_position;
|
|
||||||
buffer.end_transaction(cx)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
self.edit_position = Some(edit_range.start.bias_right(&self.snapshot));
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let telemetry_id = model.telemetry_id();
|
let telemetry_id = model.telemetry_id();
|
||||||
let chunks: LocalBoxFuture<Result<BoxStream<Result<String>>>> = if user_prompt
|
let chunks: LocalBoxFuture<Result<BoxStream<Result<String>>>> = if user_prompt
|
||||||
@ -2406,7 +2379,8 @@ impl Codegen {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if let Some(transaction) = transaction {
|
if let Some(transaction) = transaction {
|
||||||
if let Some(first_transaction) = this.transaction_id {
|
if let Some(first_transaction) = this.transformation_transaction_id
|
||||||
|
{
|
||||||
// Group all assistant edits into the first transaction.
|
// Group all assistant edits into the first transaction.
|
||||||
this.buffer.update(cx, |buffer, cx| {
|
this.buffer.update(cx, |buffer, cx| {
|
||||||
buffer.merge_transactions(
|
buffer.merge_transactions(
|
||||||
@ -2416,7 +2390,7 @@ impl Codegen {
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.transaction_id = Some(transaction);
|
this.transformation_transaction_id = Some(transaction);
|
||||||
this.buffer.update(cx, |buffer, cx| {
|
this.buffer.update(cx, |buffer, cx| {
|
||||||
buffer.finalize_last_transaction(cx)
|
buffer.finalize_last_transaction(cx)
|
||||||
});
|
});
|
||||||
@ -2459,10 +2433,15 @@ impl Codegen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
|
pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
|
||||||
if let Some(transaction_id) = self.transaction_id.take() {
|
self.buffer.update(cx, |buffer, cx| {
|
||||||
self.buffer
|
if let Some(transaction_id) = self.transformation_transaction_id.take() {
|
||||||
.update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx));
|
buffer.undo_transaction(transaction_id, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(transaction_id) = self.initial_transaction_id.take() {
|
||||||
|
buffer.undo_transaction(transaction_id, cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_diff(&mut self, edit_range: Range<Anchor>, cx: &mut ModelContext<Self>) {
|
fn update_diff(&mut self, edit_range: Range<Anchor>, cx: &mut ModelContext<Self>) {
|
||||||
|
@ -53,6 +53,7 @@ settings.workspace = true
|
|||||||
similar.workspace = true
|
similar.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
|
strsim.workspace = true
|
||||||
sum_tree.workspace = true
|
sum_tree.workspace = true
|
||||||
task.workspace = true
|
task.workspace = true
|
||||||
text.workspace = true
|
text.workspace = true
|
||||||
|
@ -1876,6 +1876,63 @@ impl Buffer {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inserts newlines at the given position to create an empty line, returning the start of the new line.
|
||||||
|
// You can also request the insertion of empty lines above and below the line starting at the returned point.
|
||||||
|
pub fn insert_empty_line(
|
||||||
|
&mut self,
|
||||||
|
position: impl ToPoint,
|
||||||
|
space_above: bool,
|
||||||
|
space_below: bool,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Point {
|
||||||
|
let mut position = position.to_point(self);
|
||||||
|
|
||||||
|
self.start_transaction();
|
||||||
|
|
||||||
|
self.edit(
|
||||||
|
[(position..position, "\n")],
|
||||||
|
Some(AutoindentMode::EachLine),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
if position.column > 0 {
|
||||||
|
position += Point::new(1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.is_line_blank(position.row) {
|
||||||
|
self.edit(
|
||||||
|
[(position..position, "\n")],
|
||||||
|
Some(AutoindentMode::EachLine),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if space_above {
|
||||||
|
if position.row > 0 && !self.is_line_blank(position.row - 1) {
|
||||||
|
self.edit(
|
||||||
|
[(position..position, "\n")],
|
||||||
|
Some(AutoindentMode::EachLine),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
position.row += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if space_below {
|
||||||
|
if position.row == self.max_point().row || !self.is_line_blank(position.row + 1) {
|
||||||
|
self.edit(
|
||||||
|
[(position..position, "\n")],
|
||||||
|
Some(AutoindentMode::EachLine),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.end_transaction(cx);
|
||||||
|
|
||||||
|
position
|
||||||
|
}
|
||||||
|
|
||||||
/// Applies the given remote operations to the buffer.
|
/// Applies the given remote operations to the buffer.
|
||||||
pub fn apply_ops<I: IntoIterator<Item = Operation>>(
|
pub fn apply_ops<I: IntoIterator<Item = Operation>>(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
@ -1822,6 +1822,92 @@ fn test_autoindent_query_with_outdent_captures(cx: &mut AppContext) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn test_insert_empty_line(cx: &mut AppContext) {
|
||||||
|
init_settings(cx, |_| {});
|
||||||
|
|
||||||
|
// Insert empty line at the beginning, requesting an empty line above
|
||||||
|
cx.new_model(|cx| {
|
||||||
|
let mut buffer = Buffer::local("abc\ndef\nghi", cx);
|
||||||
|
let point = buffer.insert_empty_line(Point::new(0, 0), true, false, cx);
|
||||||
|
assert_eq!(buffer.text(), "\nabc\ndef\nghi");
|
||||||
|
assert_eq!(point, Point::new(0, 0));
|
||||||
|
buffer
|
||||||
|
});
|
||||||
|
|
||||||
|
// Insert empty line at the beginning, requesting an empty line above and below
|
||||||
|
cx.new_model(|cx| {
|
||||||
|
let mut buffer = Buffer::local("abc\ndef\nghi", cx);
|
||||||
|
let point = buffer.insert_empty_line(Point::new(0, 0), true, true, cx);
|
||||||
|
assert_eq!(buffer.text(), "\n\nabc\ndef\nghi");
|
||||||
|
assert_eq!(point, Point::new(0, 0));
|
||||||
|
buffer
|
||||||
|
});
|
||||||
|
|
||||||
|
// Insert empty line at the start of a line, requesting empty lines above and below
|
||||||
|
cx.new_model(|cx| {
|
||||||
|
let mut buffer = Buffer::local("abc\ndef\nghi", cx);
|
||||||
|
let point = buffer.insert_empty_line(Point::new(2, 0), true, true, cx);
|
||||||
|
assert_eq!(buffer.text(), "abc\ndef\n\n\n\nghi");
|
||||||
|
assert_eq!(point, Point::new(3, 0));
|
||||||
|
buffer
|
||||||
|
});
|
||||||
|
|
||||||
|
// Insert empty line in the middle of a line, requesting empty lines above and below
|
||||||
|
cx.new_model(|cx| {
|
||||||
|
let mut buffer = Buffer::local("abc\ndefghi\njkl", cx);
|
||||||
|
let point = buffer.insert_empty_line(Point::new(1, 3), true, true, cx);
|
||||||
|
assert_eq!(buffer.text(), "abc\ndef\n\n\n\nghi\njkl");
|
||||||
|
assert_eq!(point, Point::new(3, 0));
|
||||||
|
buffer
|
||||||
|
});
|
||||||
|
|
||||||
|
// Insert empty line in the middle of a line, requesting empty line above only
|
||||||
|
cx.new_model(|cx| {
|
||||||
|
let mut buffer = Buffer::local("abc\ndefghi\njkl", cx);
|
||||||
|
let point = buffer.insert_empty_line(Point::new(1, 3), true, false, cx);
|
||||||
|
assert_eq!(buffer.text(), "abc\ndef\n\n\nghi\njkl");
|
||||||
|
assert_eq!(point, Point::new(3, 0));
|
||||||
|
buffer
|
||||||
|
});
|
||||||
|
|
||||||
|
// Insert empty line in the middle of a line, requesting empty line below only
|
||||||
|
cx.new_model(|cx| {
|
||||||
|
let mut buffer = Buffer::local("abc\ndefghi\njkl", cx);
|
||||||
|
let point = buffer.insert_empty_line(Point::new(1, 3), false, true, cx);
|
||||||
|
assert_eq!(buffer.text(), "abc\ndef\n\n\nghi\njkl");
|
||||||
|
assert_eq!(point, Point::new(2, 0));
|
||||||
|
buffer
|
||||||
|
});
|
||||||
|
|
||||||
|
// Insert empty line at the end, requesting empty lines above and below
|
||||||
|
cx.new_model(|cx| {
|
||||||
|
let mut buffer = Buffer::local("abc\ndef\nghi", cx);
|
||||||
|
let point = buffer.insert_empty_line(Point::new(2, 3), true, true, cx);
|
||||||
|
assert_eq!(buffer.text(), "abc\ndef\nghi\n\n\n");
|
||||||
|
assert_eq!(point, Point::new(4, 0));
|
||||||
|
buffer
|
||||||
|
});
|
||||||
|
|
||||||
|
// Insert empty line at the end, requesting empty line above only
|
||||||
|
cx.new_model(|cx| {
|
||||||
|
let mut buffer = Buffer::local("abc\ndef\nghi", cx);
|
||||||
|
let point = buffer.insert_empty_line(Point::new(2, 3), true, false, cx);
|
||||||
|
assert_eq!(buffer.text(), "abc\ndef\nghi\n\n");
|
||||||
|
assert_eq!(point, Point::new(4, 0));
|
||||||
|
buffer
|
||||||
|
});
|
||||||
|
|
||||||
|
// Insert empty line at the end, requesting empty line below only
|
||||||
|
cx.new_model(|cx| {
|
||||||
|
let mut buffer = Buffer::local("abc\ndef\nghi", cx);
|
||||||
|
let point = buffer.insert_empty_line(Point::new(2, 3), false, true, cx);
|
||||||
|
assert_eq!(buffer.text(), "abc\ndef\nghi\n\n");
|
||||||
|
assert_eq!(point, Point::new(3, 0));
|
||||||
|
buffer
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_language_scope_at_with_javascript(cx: &mut AppContext) {
|
fn test_language_scope_at_with_javascript(cx: &mut AppContext) {
|
||||||
init_settings(cx, |_| {});
|
init_settings(cx, |_| {});
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use crate::{BufferSnapshot, Point, ToPoint};
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use gpui::{relative, AppContext, BackgroundExecutor, HighlightStyle, StyledText, TextStyle};
|
use gpui::{relative, AppContext, BackgroundExecutor, HighlightStyle, StyledText, TextStyle};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
@ -24,6 +25,27 @@ pub struct OutlineItem<T> {
|
|||||||
pub annotation_range: Option<Range<T>>,
|
pub annotation_range: Option<Range<T>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T: ToPoint> OutlineItem<T> {
|
||||||
|
/// Converts to an equivalent outline item, but with parameterized over Points.
|
||||||
|
pub fn to_point(&self, buffer: &BufferSnapshot) -> OutlineItem<Point> {
|
||||||
|
OutlineItem {
|
||||||
|
depth: self.depth,
|
||||||
|
range: self.range.start.to_point(buffer)..self.range.end.to_point(buffer),
|
||||||
|
text: self.text.clone(),
|
||||||
|
highlight_ranges: self.highlight_ranges.clone(),
|
||||||
|
name_ranges: self.name_ranges.clone(),
|
||||||
|
body_range: self
|
||||||
|
.body_range
|
||||||
|
.as_ref()
|
||||||
|
.map(|r| r.start.to_point(buffer)..r.end.to_point(buffer)),
|
||||||
|
annotation_range: self
|
||||||
|
.annotation_range
|
||||||
|
.as_ref()
|
||||||
|
.map(|r| r.start.to_point(buffer)..r.end.to_point(buffer)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T> Outline<T> {
|
impl<T> Outline<T> {
|
||||||
pub fn new(items: Vec<OutlineItem<T>>) -> Self {
|
pub fn new(items: Vec<OutlineItem<T>>) -> Self {
|
||||||
let mut candidates = Vec::new();
|
let mut candidates = Vec::new();
|
||||||
@ -62,6 +84,16 @@ impl<T> Outline<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Find the most similar symbol to the provided query according to the Jaro-Winkler distance measure.
|
||||||
|
pub fn find_most_similar(&self, query: &str) -> Option<&OutlineItem<T>> {
|
||||||
|
let candidate = self.path_candidates.iter().max_by(|a, b| {
|
||||||
|
strsim::jaro_winkler(&a.string, query)
|
||||||
|
.total_cmp(&strsim::jaro_winkler(&b.string, query))
|
||||||
|
})?;
|
||||||
|
Some(&self.items[candidate.id])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find all outline symbols according to a longest subsequence match with the query, ordered descending by match score.
|
||||||
pub async fn search(&self, query: &str, executor: BackgroundExecutor) -> Vec<StringMatch> {
|
pub async fn search(&self, query: &str, executor: BackgroundExecutor) -> Vec<StringMatch> {
|
||||||
let query = query.trim_start();
|
let query = query.trim_start();
|
||||||
let is_path_query = query.contains(' ');
|
let is_path_query = query.contains(' ');
|
||||||
|
@ -37,6 +37,7 @@ log.workspace = true
|
|||||||
menu.workspace = true
|
menu.workspace = true
|
||||||
ollama = { workspace = true, features = ["schemars"] }
|
ollama = { workspace = true, features = ["schemars"] }
|
||||||
open_ai = { workspace = true, features = ["schemars"] }
|
open_ai = { workspace = true, features = ["schemars"] }
|
||||||
|
parking_lot.workspace = true
|
||||||
proto = { workspace = true, features = ["test-support"] }
|
proto = { workspace = true, features = ["test-support"] }
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
schemars.workspace = true
|
schemars.workspace = true
|
||||||
|
@ -75,6 +75,11 @@ pub trait LanguageModel: Send + Sync {
|
|||||||
schema: serde_json::Value,
|
schema: serde_json::Value,
|
||||||
cx: &AsyncAppContext,
|
cx: &AsyncAppContext,
|
||||||
) -> BoxFuture<'static, Result<serde_json::Value>>;
|
) -> BoxFuture<'static, Result<serde_json::Value>>;
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
fn as_fake(&self) -> &provider::fake::FakeLanguageModel {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl dyn LanguageModel {
|
impl dyn LanguageModel {
|
||||||
|
@ -3,15 +3,17 @@ use crate::{
|
|||||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
||||||
LanguageModelRequest,
|
LanguageModelRequest,
|
||||||
};
|
};
|
||||||
use anyhow::anyhow;
|
use anyhow::Context as _;
|
||||||
use collections::HashMap;
|
use futures::{
|
||||||
use futures::{channel::mpsc, future::BoxFuture, stream::BoxStream, FutureExt, StreamExt};
|
channel::{mpsc, oneshot},
|
||||||
|
future::BoxFuture,
|
||||||
|
stream::BoxStream,
|
||||||
|
FutureExt, StreamExt,
|
||||||
|
};
|
||||||
use gpui::{AnyView, AppContext, AsyncAppContext, Task};
|
use gpui::{AnyView, AppContext, AsyncAppContext, Task};
|
||||||
use http_client::Result;
|
use http_client::Result;
|
||||||
use std::{
|
use parking_lot::Mutex;
|
||||||
future,
|
use std::sync::Arc;
|
||||||
sync::{Arc, Mutex},
|
|
||||||
};
|
|
||||||
use ui::WindowContext;
|
use ui::WindowContext;
|
||||||
|
|
||||||
pub fn language_model_id() -> LanguageModelId {
|
pub fn language_model_id() -> LanguageModelId {
|
||||||
@ -31,9 +33,7 @@ pub fn provider_name() -> LanguageModelProviderName {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct FakeLanguageModelProvider {
|
pub struct FakeLanguageModelProvider;
|
||||||
current_completion_txs: Arc<Mutex<HashMap<String, mpsc::UnboundedSender<String>>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LanguageModelProviderState for FakeLanguageModelProvider {
|
impl LanguageModelProviderState for FakeLanguageModelProvider {
|
||||||
type ObservableEntity = ();
|
type ObservableEntity = ();
|
||||||
@ -53,9 +53,7 @@ impl LanguageModelProvider for FakeLanguageModelProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn provided_models(&self, _: &AppContext) -> Vec<Arc<dyn LanguageModel>> {
|
fn provided_models(&self, _: &AppContext) -> Vec<Arc<dyn LanguageModel>> {
|
||||||
vec![Arc::new(FakeLanguageModel {
|
vec![Arc::new(FakeLanguageModel::default())]
|
||||||
current_completion_txs: self.current_completion_txs.clone(),
|
|
||||||
})]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_authenticated(&self, _: &AppContext) -> bool {
|
fn is_authenticated(&self, _: &AppContext) -> bool {
|
||||||
@ -77,55 +75,80 @@ impl LanguageModelProvider for FakeLanguageModelProvider {
|
|||||||
|
|
||||||
impl FakeLanguageModelProvider {
|
impl FakeLanguageModelProvider {
|
||||||
pub fn test_model(&self) -> FakeLanguageModel {
|
pub fn test_model(&self) -> FakeLanguageModel {
|
||||||
FakeLanguageModel {
|
FakeLanguageModel::default()
|
||||||
current_completion_txs: self.current_completion_txs.clone(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub struct ToolUseRequest {
|
||||||
|
pub request: LanguageModelRequest,
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub schema: serde_json::Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
pub struct FakeLanguageModel {
|
pub struct FakeLanguageModel {
|
||||||
current_completion_txs: Arc<Mutex<HashMap<String, mpsc::UnboundedSender<String>>>>,
|
current_completion_txs: Mutex<Vec<(LanguageModelRequest, mpsc::UnboundedSender<String>)>>,
|
||||||
|
current_tool_use_txs: Mutex<Vec<(ToolUseRequest, oneshot::Sender<Result<serde_json::Value>>)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FakeLanguageModel {
|
impl FakeLanguageModel {
|
||||||
pub fn pending_completions(&self) -> Vec<LanguageModelRequest> {
|
pub fn pending_completions(&self) -> Vec<LanguageModelRequest> {
|
||||||
self.current_completion_txs
|
self.current_completion_txs
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.iter()
|
||||||
.keys()
|
.map(|(request, _)| request.clone())
|
||||||
.map(|k| serde_json::from_str(k).unwrap())
|
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn completion_count(&self) -> usize {
|
pub fn completion_count(&self) -> usize {
|
||||||
self.current_completion_txs.lock().unwrap().len()
|
self.current_completion_txs.lock().len()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_completion_chunk(&self, request: &LanguageModelRequest, chunk: String) {
|
pub fn stream_completion_response(&self, request: &LanguageModelRequest, chunk: String) {
|
||||||
let json = serde_json::to_string(request).unwrap();
|
let current_completion_txs = self.current_completion_txs.lock();
|
||||||
|
let tx = current_completion_txs
|
||||||
|
.iter()
|
||||||
|
.find(|(req, _)| req == request)
|
||||||
|
.map(|(_, tx)| tx)
|
||||||
|
.unwrap();
|
||||||
|
tx.unbounded_send(chunk).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn end_completion_stream(&self, request: &LanguageModelRequest) {
|
||||||
self.current_completion_txs
|
self.current_completion_txs
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.retain(|(req, _)| req != request);
|
||||||
.get(&json)
|
|
||||||
.unwrap()
|
|
||||||
.unbounded_send(chunk)
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send_last_completion_chunk(&self, chunk: String) {
|
pub fn stream_last_completion_response(&self, chunk: String) {
|
||||||
self.send_completion_chunk(self.pending_completions().last().unwrap(), chunk);
|
self.stream_completion_response(self.pending_completions().last().unwrap(), chunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn finish_completion(&self, request: &LanguageModelRequest) {
|
pub fn end_last_completion_stream(&self) {
|
||||||
self.current_completion_txs
|
self.end_completion_stream(self.pending_completions().last().unwrap());
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.remove(&serde_json::to_string(request).unwrap())
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn finish_last_completion(&self) {
|
pub fn respond_to_tool_use(
|
||||||
self.finish_completion(self.pending_completions().last().unwrap());
|
&self,
|
||||||
|
tool_call: &ToolUseRequest,
|
||||||
|
response: Result<serde_json::Value>,
|
||||||
|
) {
|
||||||
|
let mut current_tool_call_txs = self.current_tool_use_txs.lock();
|
||||||
|
if let Some(index) = current_tool_call_txs
|
||||||
|
.iter()
|
||||||
|
.position(|(call, _)| call == tool_call)
|
||||||
|
{
|
||||||
|
let (_, tx) = current_tool_call_txs.remove(index);
|
||||||
|
tx.send(response).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn respond_to_last_tool_use(&self, response: Result<serde_json::Value>) {
|
||||||
|
let mut current_tool_call_txs = self.current_tool_use_txs.lock();
|
||||||
|
let (_, tx) = current_tool_call_txs.pop().unwrap();
|
||||||
|
tx.send(response).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,21 +191,30 @@ impl LanguageModel for FakeLanguageModel {
|
|||||||
_: &AsyncAppContext,
|
_: &AsyncAppContext,
|
||||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||||
let (tx, rx) = mpsc::unbounded();
|
let (tx, rx) = mpsc::unbounded();
|
||||||
self.current_completion_txs
|
self.current_completion_txs.lock().push((request, tx));
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.insert(serde_json::to_string(&request).unwrap(), tx);
|
|
||||||
async move { Ok(rx.map(Ok).boxed()) }.boxed()
|
async move { Ok(rx.map(Ok).boxed()) }.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn use_any_tool(
|
fn use_any_tool(
|
||||||
&self,
|
&self,
|
||||||
_request: LanguageModelRequest,
|
request: LanguageModelRequest,
|
||||||
_name: String,
|
name: String,
|
||||||
_description: String,
|
description: String,
|
||||||
_schema: serde_json::Value,
|
schema: serde_json::Value,
|
||||||
_cx: &AsyncAppContext,
|
_cx: &AsyncAppContext,
|
||||||
) -> BoxFuture<'static, Result<serde_json::Value>> {
|
) -> BoxFuture<'static, Result<serde_json::Value>> {
|
||||||
future::ready(Err(anyhow!("not implemented"))).boxed()
|
let (tx, rx) = oneshot::channel();
|
||||||
|
let tool_call = ToolUseRequest {
|
||||||
|
request,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
schema,
|
||||||
|
};
|
||||||
|
self.current_tool_use_txs.lock().push((tool_call, tx));
|
||||||
|
async move { rx.await.context("FakeLanguageModel was dropped")? }.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_fake(&self) -> &Self {
|
||||||
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -103,7 +103,7 @@ impl LanguageModelRegistry {
|
|||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub fn test(cx: &mut AppContext) -> crate::provider::fake::FakeLanguageModelProvider {
|
pub fn test(cx: &mut AppContext) -> crate::provider::fake::FakeLanguageModelProvider {
|
||||||
let fake_provider = crate::provider::fake::FakeLanguageModelProvider::default();
|
let fake_provider = crate::provider::fake::FakeLanguageModelProvider;
|
||||||
let registry = cx.new_model(|cx| {
|
let registry = cx.new_model(|cx| {
|
||||||
let mut registry = Self::default();
|
let mut registry = Self::default();
|
||||||
registry.register_provider(fake_provider.clone(), cx);
|
registry.register_provider(fake_provider.clone(), cx);
|
||||||
@ -239,7 +239,7 @@ mod tests {
|
|||||||
let registry = cx.new_model(|_| LanguageModelRegistry::default());
|
let registry = cx.new_model(|_| LanguageModelRegistry::default());
|
||||||
|
|
||||||
registry.update(cx, |registry, cx| {
|
registry.update(cx, |registry, cx| {
|
||||||
registry.register_provider(FakeLanguageModelProvider::default(), cx);
|
registry.register_provider(FakeLanguageModelProvider, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
let providers = registry.read(cx).providers();
|
let providers = registry.read(cx).providers();
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
use crate::role::Role;
|
use crate::role::Role;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Hash)]
|
||||||
pub struct LanguageModelRequestMessage {
|
pub struct LanguageModelRequestMessage {
|
||||||
pub role: Role,
|
pub role: Role,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||||
pub struct LanguageModelRequest {
|
pub struct LanguageModelRequest {
|
||||||
pub messages: Vec<LanguageModelRequestMessage>,
|
pub messages: Vec<LanguageModelRequestMessage>,
|
||||||
pub stop: Vec<String>,
|
pub stop: Vec<String>,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt::{self, Display};
|
use std::fmt::{self, Display};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq, Hash)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum Role {
|
pub enum Role {
|
||||||
User,
|
User,
|
||||||
|
@ -742,6 +742,33 @@ impl MultiBuffer {
|
|||||||
tail(self, buffer_edits, autoindent_mode, edited_excerpt_ids, cx);
|
tail(self, buffer_edits, autoindent_mode, edited_excerpt_ids, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inserts newlines at the given position to create an empty line, returning the start of the new line.
|
||||||
|
// You can also request the insertion of empty lines above and below the line starting at the returned point.
|
||||||
|
// Panics if the given position is invalid.
|
||||||
|
pub fn insert_empty_line(
|
||||||
|
&mut self,
|
||||||
|
position: impl ToPoint,
|
||||||
|
space_above: bool,
|
||||||
|
space_below: bool,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Point {
|
||||||
|
let multibuffer_point = position.to_point(&self.read(cx));
|
||||||
|
if let Some(buffer) = self.as_singleton() {
|
||||||
|
buffer.update(cx, |buffer, cx| {
|
||||||
|
buffer.insert_empty_line(multibuffer_point, space_above, space_below, cx)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let (buffer, buffer_point, _) =
|
||||||
|
self.point_to_buffer_point(multibuffer_point, cx).unwrap();
|
||||||
|
self.start_transaction(cx);
|
||||||
|
let empty_line_start = buffer.update(cx, |buffer, cx| {
|
||||||
|
buffer.insert_empty_line(buffer_point, space_above, space_below, cx)
|
||||||
|
});
|
||||||
|
self.end_transaction(cx);
|
||||||
|
multibuffer_point + (empty_line_start - buffer_point)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn start_transaction(&mut self, cx: &mut ModelContext<Self>) -> Option<TransactionId> {
|
pub fn start_transaction(&mut self, cx: &mut ModelContext<Self>) -> Option<TransactionId> {
|
||||||
self.start_transaction_at(Instant::now(), cx)
|
self.start_transaction_at(Instant::now(), cx)
|
||||||
}
|
}
|
||||||
@ -1448,6 +1475,29 @@ impl MultiBuffer {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If point is at the end of the buffer, the last excerpt is returned
|
||||||
|
pub fn point_to_buffer_point<T: ToPoint>(
|
||||||
|
&self,
|
||||||
|
point: T,
|
||||||
|
cx: &AppContext,
|
||||||
|
) -> Option<(Model<Buffer>, Point, ExcerptId)> {
|
||||||
|
let snapshot = self.read(cx);
|
||||||
|
let point = point.to_point(&snapshot);
|
||||||
|
let mut cursor = snapshot.excerpts.cursor::<Point>();
|
||||||
|
cursor.seek(&point, Bias::Right, &());
|
||||||
|
if cursor.item().is_none() {
|
||||||
|
cursor.prev(&());
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.item().map(|excerpt| {
|
||||||
|
let excerpt_start = excerpt.range.context.start.to_point(&excerpt.buffer);
|
||||||
|
let buffer_point = excerpt_start + point - *cursor.start();
|
||||||
|
let buffer = self.buffers.borrow()[&excerpt.buffer_id].buffer.clone();
|
||||||
|
|
||||||
|
(buffer, buffer_point, excerpt.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn range_to_buffer_ranges<T: ToOffset>(
|
pub fn range_to_buffer_ranges<T: ToOffset>(
|
||||||
&self,
|
&self,
|
||||||
range: Range<T>,
|
range: Range<T>,
|
||||||
|
@ -136,7 +136,6 @@ where
|
|||||||
|
|
||||||
pub trait AnchorRangeExt {
|
pub trait AnchorRangeExt {
|
||||||
fn cmp(&self, b: &Range<Anchor>, buffer: &BufferSnapshot) -> Ordering;
|
fn cmp(&self, b: &Range<Anchor>, buffer: &BufferSnapshot) -> Ordering;
|
||||||
fn intersects(&self, other: &Range<Anchor>, buffer: &BufferSnapshot) -> bool;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AnchorRangeExt for Range<Anchor> {
|
impl AnchorRangeExt for Range<Anchor> {
|
||||||
@ -146,8 +145,4 @@ impl AnchorRangeExt for Range<Anchor> {
|
|||||||
ord => ord,
|
ord => ord,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn intersects(&self, other: &Range<Anchor>, buffer: &BufferSnapshot) -> bool {
|
|
||||||
self.start.cmp(&other.end, buffer).is_lt() && other.start.cmp(&self.end, buffer).is_lt()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user