Add "Fix with Assistant" code action on lines with diagnostics (#18163)
Release Notes: - Added a new "Fix with Assistant" action on code with errors or warnings. --------- Co-authored-by: Nathan <nathan@zed.dev>
This commit is contained in:
parent
1efe87029b
commit
7051bc00c2
13 changed files with 418 additions and 72 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -404,6 +404,7 @@ dependencies = [
|
||||||
"language_model",
|
"language_model",
|
||||||
"languages",
|
"languages",
|
||||||
"log",
|
"log",
|
||||||
|
"lsp",
|
||||||
"markdown",
|
"markdown",
|
||||||
"menu",
|
"menu",
|
||||||
"multi_buffer",
|
"multi_buffer",
|
||||||
|
|
|
@ -51,6 +51,7 @@ indoc.workspace = true
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
language_model.workspace = true
|
language_model.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
lsp.workspace = true
|
||||||
markdown.workspace = true
|
markdown.workspace = true
|
||||||
menu.workspace = true
|
menu.workspace = true
|
||||||
multi_buffer.workspace = true
|
multi_buffer.workspace = true
|
||||||
|
|
|
@ -12,8 +12,9 @@ use editor::{
|
||||||
BlockContext, BlockDisposition, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
|
BlockContext, BlockDisposition, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
|
||||||
ToDisplayPoint,
|
ToDisplayPoint,
|
||||||
},
|
},
|
||||||
Anchor, AnchorRangeExt, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle,
|
Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorElement, EditorEvent, EditorMode,
|
||||||
ExcerptRange, GutterDimensions, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
|
EditorStyle, ExcerptId, ExcerptRange, GutterDimensions, MultiBuffer, MultiBufferSnapshot,
|
||||||
|
ToOffset as _, ToPoint,
|
||||||
};
|
};
|
||||||
use feature_flags::{FeatureFlagAppExt as _, ZedPro};
|
use feature_flags::{FeatureFlagAppExt as _, ZedPro};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
|
@ -35,6 +36,7 @@ use language_model::{
|
||||||
};
|
};
|
||||||
use multi_buffer::MultiBufferRow;
|
use multi_buffer::MultiBufferRow;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
use project::{CodeAction, ProjectTransaction};
|
||||||
use rope::Rope;
|
use rope::Rope;
|
||||||
use settings::{Settings, SettingsStore};
|
use settings::{Settings, SettingsStore};
|
||||||
use smol::future::FutureExt;
|
use smol::future::FutureExt;
|
||||||
|
@ -49,10 +51,11 @@ use std::{
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
use terminal_view::terminal_panel::TerminalPanel;
|
use terminal_view::terminal_panel::TerminalPanel;
|
||||||
|
use text::{OffsetRangeExt, ToPoint as _};
|
||||||
use theme::ThemeSettings;
|
use theme::ThemeSettings;
|
||||||
use ui::{prelude::*, CheckboxWithLabel, IconButtonShape, Popover, Tooltip};
|
use ui::{prelude::*, CheckboxWithLabel, IconButtonShape, Popover, Tooltip};
|
||||||
use util::{RangeExt, ResultExt};
|
use util::{RangeExt, ResultExt};
|
||||||
use workspace::{notifications::NotificationId, Toast, Workspace};
|
use workspace::{notifications::NotificationId, ItemHandle, Toast, Workspace};
|
||||||
|
|
||||||
pub fn init(
|
pub fn init(
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
|
@ -129,8 +132,10 @@ impl InlineAssistant {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register_workspace(&mut self, workspace: &View<Workspace>, cx: &mut WindowContext) {
|
pub fn register_workspace(&mut self, workspace: &View<Workspace>, cx: &mut WindowContext) {
|
||||||
cx.subscribe(workspace, |_, event, cx| {
|
cx.subscribe(workspace, |workspace, event, cx| {
|
||||||
Self::update_global(cx, |this, cx| this.handle_workspace_event(event, cx));
|
Self::update_global(cx, |this, cx| {
|
||||||
|
this.handle_workspace_event(workspace, event, cx)
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
|
@ -150,19 +155,49 @@ impl InlineAssistant {
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_workspace_event(&mut self, event: &workspace::Event, cx: &mut WindowContext) {
|
fn handle_workspace_event(
|
||||||
// When the user manually saves an editor, automatically accepts all finished transformations.
|
&mut self,
|
||||||
if let workspace::Event::UserSavedItem { item, .. } = event {
|
workspace: View<Workspace>,
|
||||||
if let Some(editor) = item.upgrade().and_then(|item| item.act_as::<Editor>(cx)) {
|
event: &workspace::Event,
|
||||||
if let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) {
|
cx: &mut WindowContext,
|
||||||
for assist_id in editor_assists.assist_ids.clone() {
|
) {
|
||||||
let assist = &self.assists[&assist_id];
|
match event {
|
||||||
if let CodegenStatus::Done = assist.codegen.read(cx).status(cx) {
|
workspace::Event::UserSavedItem { item, .. } => {
|
||||||
self.finish_assist(assist_id, false, cx)
|
// When the user manually saves an editor, automatically accepts all finished transformations.
|
||||||
|
if let Some(editor) = item.upgrade().and_then(|item| item.act_as::<Editor>(cx)) {
|
||||||
|
if let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) {
|
||||||
|
for assist_id in editor_assists.assist_ids.clone() {
|
||||||
|
let assist = &self.assists[&assist_id];
|
||||||
|
if let CodegenStatus::Done = assist.codegen.read(cx).status(cx) {
|
||||||
|
self.finish_assist(assist_id, false, cx)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
workspace::Event::ItemAdded { item } => {
|
||||||
|
self.register_workspace_item(&workspace, item.as_ref(), cx);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_workspace_item(
|
||||||
|
&mut self,
|
||||||
|
workspace: &View<Workspace>,
|
||||||
|
item: &dyn ItemHandle,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) {
|
||||||
|
if let Some(editor) = item.act_as::<Editor>(cx) {
|
||||||
|
editor.update(cx, |editor, cx| {
|
||||||
|
editor.push_code_action_provider(
|
||||||
|
Arc::new(AssistantCodeActionProvider {
|
||||||
|
editor: cx.view().downgrade(),
|
||||||
|
workspace: workspace.downgrade(),
|
||||||
|
}),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -332,6 +367,7 @@ impl InlineAssistant {
|
||||||
mut range: Range<Anchor>,
|
mut range: Range<Anchor>,
|
||||||
initial_prompt: String,
|
initial_prompt: String,
|
||||||
initial_transaction_id: Option<TransactionId>,
|
initial_transaction_id: Option<TransactionId>,
|
||||||
|
focus: bool,
|
||||||
workspace: Option<WeakView<Workspace>>,
|
workspace: Option<WeakView<Workspace>>,
|
||||||
assistant_panel: Option<&View<AssistantPanel>>,
|
assistant_panel: Option<&View<AssistantPanel>>,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
|
@ -404,6 +440,11 @@ impl InlineAssistant {
|
||||||
assist_group.assist_ids.push(assist_id);
|
assist_group.assist_ids.push(assist_id);
|
||||||
editor_assists.assist_ids.push(assist_id);
|
editor_assists.assist_ids.push(assist_id);
|
||||||
self.assist_groups.insert(assist_group_id, assist_group);
|
self.assist_groups.insert(assist_group_id, assist_group);
|
||||||
|
|
||||||
|
if focus {
|
||||||
|
self.focus_assist(assist_id, cx);
|
||||||
|
}
|
||||||
|
|
||||||
assist_id
|
assist_id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3289,6 +3330,132 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct AssistantCodeActionProvider {
|
||||||
|
editor: WeakView<Editor>,
|
||||||
|
workspace: WeakView<Workspace>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CodeActionProvider for AssistantCodeActionProvider {
|
||||||
|
fn code_actions(
|
||||||
|
&self,
|
||||||
|
buffer: &Model<Buffer>,
|
||||||
|
range: Range<text::Anchor>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Task<Result<Vec<CodeAction>>> {
|
||||||
|
let snapshot = buffer.read(cx).snapshot();
|
||||||
|
let mut range = range.to_point(&snapshot);
|
||||||
|
|
||||||
|
// Expand the range to line boundaries.
|
||||||
|
range.start.column = 0;
|
||||||
|
range.end.column = snapshot.line_len(range.end.row);
|
||||||
|
|
||||||
|
let mut has_diagnostics = false;
|
||||||
|
for diagnostic in snapshot.diagnostics_in_range::<_, Point>(range.clone(), false) {
|
||||||
|
range.start = cmp::min(range.start, diagnostic.range.start);
|
||||||
|
range.end = cmp::max(range.end, diagnostic.range.end);
|
||||||
|
has_diagnostics = true;
|
||||||
|
}
|
||||||
|
if has_diagnostics {
|
||||||
|
if let Some(symbols_containing_start) = snapshot.symbols_containing(range.start, None) {
|
||||||
|
if let Some(symbol) = symbols_containing_start.last() {
|
||||||
|
range.start = cmp::min(range.start, symbol.range.start.to_point(&snapshot));
|
||||||
|
range.end = cmp::max(range.end, symbol.range.end.to_point(&snapshot));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(symbols_containing_end) = snapshot.symbols_containing(range.end, None) {
|
||||||
|
if let Some(symbol) = symbols_containing_end.last() {
|
||||||
|
range.start = cmp::min(range.start, symbol.range.start.to_point(&snapshot));
|
||||||
|
range.end = cmp::max(range.end, symbol.range.end.to_point(&snapshot));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Task::ready(Ok(vec![CodeAction {
|
||||||
|
server_id: language::LanguageServerId(0),
|
||||||
|
range: snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end),
|
||||||
|
lsp_action: lsp::CodeAction {
|
||||||
|
title: "Fix with Assistant".into(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
}]))
|
||||||
|
} else {
|
||||||
|
Task::ready(Ok(Vec::new()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_code_action(
|
||||||
|
&self,
|
||||||
|
buffer: Model<Buffer>,
|
||||||
|
action: CodeAction,
|
||||||
|
excerpt_id: ExcerptId,
|
||||||
|
_push_to_history: bool,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Task<Result<ProjectTransaction>> {
|
||||||
|
let editor = self.editor.clone();
|
||||||
|
let workspace = self.workspace.clone();
|
||||||
|
cx.spawn(|mut cx| async move {
|
||||||
|
let editor = editor.upgrade().context("editor was released")?;
|
||||||
|
let range = editor
|
||||||
|
.update(&mut cx, |editor, cx| {
|
||||||
|
editor.buffer().update(cx, |multibuffer, cx| {
|
||||||
|
let buffer = buffer.read(cx);
|
||||||
|
let multibuffer_snapshot = multibuffer.read(cx);
|
||||||
|
|
||||||
|
let old_context_range =
|
||||||
|
multibuffer_snapshot.context_range_for_excerpt(excerpt_id)?;
|
||||||
|
let mut new_context_range = old_context_range.clone();
|
||||||
|
if action
|
||||||
|
.range
|
||||||
|
.start
|
||||||
|
.cmp(&old_context_range.start, buffer)
|
||||||
|
.is_lt()
|
||||||
|
{
|
||||||
|
new_context_range.start = action.range.start;
|
||||||
|
}
|
||||||
|
if action.range.end.cmp(&old_context_range.end, buffer).is_gt() {
|
||||||
|
new_context_range.end = action.range.end;
|
||||||
|
}
|
||||||
|
drop(multibuffer_snapshot);
|
||||||
|
|
||||||
|
if new_context_range != old_context_range {
|
||||||
|
multibuffer.resize_excerpt(excerpt_id, new_context_range, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
let multibuffer_snapshot = multibuffer.read(cx);
|
||||||
|
Some(
|
||||||
|
multibuffer_snapshot
|
||||||
|
.anchor_in_excerpt(excerpt_id, action.range.start)?
|
||||||
|
..multibuffer_snapshot
|
||||||
|
.anchor_in_excerpt(excerpt_id, action.range.end)?,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})?
|
||||||
|
.context("invalid range")?;
|
||||||
|
let assistant_panel = workspace.update(&mut cx, |workspace, cx| {
|
||||||
|
workspace
|
||||||
|
.panel::<AssistantPanel>(cx)
|
||||||
|
.context("assistant panel was released")
|
||||||
|
})??;
|
||||||
|
|
||||||
|
cx.update_global(|assistant: &mut InlineAssistant, cx| {
|
||||||
|
let assist_id = assistant.suggest_assist(
|
||||||
|
&editor,
|
||||||
|
range,
|
||||||
|
"Fix Diagnostics".into(),
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
Some(workspace),
|
||||||
|
Some(&assistant_panel),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
assistant.start_assist(assist_id, cx);
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(ProjectTransaction::default())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn prefixes(text: &str) -> impl Iterator<Item = &str> {
|
fn prefixes(text: &str) -> impl Iterator<Item = &str> {
|
||||||
(0..text.len() - 1).map(|ix| &text[..ix + 1])
|
(0..text.len() - 1).map(|ix| &text[..ix + 1])
|
||||||
}
|
}
|
||||||
|
|
|
@ -187,6 +187,7 @@ impl WorkflowSuggestion {
|
||||||
suggestion_range,
|
suggestion_range,
|
||||||
initial_prompt,
|
initial_prompt,
|
||||||
initial_transaction_id,
|
initial_transaction_id,
|
||||||
|
false,
|
||||||
Some(workspace.clone()),
|
Some(workspace.clone()),
|
||||||
Some(assistant_panel),
|
Some(assistant_panel),
|
||||||
cx,
|
cx,
|
||||||
|
|
|
@ -53,6 +53,7 @@ async fn test_sharing_an_ssh_remote_project(
|
||||||
let (project_a, worktree_id) = client_a
|
let (project_a, worktree_id) = client_a
|
||||||
.build_ssh_project("/code/project1", client_ssh, cx_a)
|
.build_ssh_project("/code/project1", client_ssh, cx_a)
|
||||||
.await;
|
.await;
|
||||||
|
executor.run_until_parked();
|
||||||
|
|
||||||
// User A shares the remote project.
|
// User A shares the remote project.
|
||||||
let active_call_a = cx_a.read(ActiveCall::global);
|
let active_call_a = cx_a.read(ActiveCall::global);
|
||||||
|
|
|
@ -68,7 +68,7 @@ use element::LineWithInvisibles;
|
||||||
pub use element::{
|
pub use element::{
|
||||||
CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition,
|
CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition,
|
||||||
};
|
};
|
||||||
use futures::FutureExt;
|
use futures::{future, FutureExt};
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use git::blame::GitBlame;
|
use git::blame::GitBlame;
|
||||||
use git::diff_hunk_to_display;
|
use git::diff_hunk_to_display;
|
||||||
|
@ -569,8 +569,8 @@ pub struct Editor {
|
||||||
find_all_references_task_sources: Vec<Anchor>,
|
find_all_references_task_sources: Vec<Anchor>,
|
||||||
next_completion_id: CompletionId,
|
next_completion_id: CompletionId,
|
||||||
completion_documentation_pre_resolve_debounce: DebouncedDelay,
|
completion_documentation_pre_resolve_debounce: DebouncedDelay,
|
||||||
available_code_actions: Option<(Location, Arc<[CodeAction]>)>,
|
available_code_actions: Option<(Location, Arc<[AvailableCodeAction]>)>,
|
||||||
code_actions_task: Option<Task<()>>,
|
code_actions_task: Option<Task<Result<()>>>,
|
||||||
document_highlights_task: Option<Task<()>>,
|
document_highlights_task: Option<Task<()>>,
|
||||||
linked_editing_range_task: Option<Task<Option<()>>>,
|
linked_editing_range_task: Option<Task<Option<()>>>,
|
||||||
linked_edit_ranges: linked_editing_ranges::LinkedEditingRanges,
|
linked_edit_ranges: linked_editing_ranges::LinkedEditingRanges,
|
||||||
|
@ -590,6 +590,7 @@ pub struct Editor {
|
||||||
gutter_hovered: bool,
|
gutter_hovered: bool,
|
||||||
hovered_link_state: Option<HoveredLinkState>,
|
hovered_link_state: Option<HoveredLinkState>,
|
||||||
inline_completion_provider: Option<RegisteredInlineCompletionProvider>,
|
inline_completion_provider: Option<RegisteredInlineCompletionProvider>,
|
||||||
|
code_action_providers: Vec<Arc<dyn CodeActionProvider>>,
|
||||||
active_inline_completion: Option<CompletionState>,
|
active_inline_completion: Option<CompletionState>,
|
||||||
// enable_inline_completions is a switch that Vim can use to disable
|
// enable_inline_completions is a switch that Vim can use to disable
|
||||||
// inline completions based on its mode.
|
// inline completions based on its mode.
|
||||||
|
@ -1360,10 +1361,16 @@ impl CompletionsMenu {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct AvailableCodeAction {
|
||||||
|
excerpt_id: ExcerptId,
|
||||||
|
action: CodeAction,
|
||||||
|
provider: Arc<dyn CodeActionProvider>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct CodeActionContents {
|
struct CodeActionContents {
|
||||||
tasks: Option<Arc<ResolvedTasks>>,
|
tasks: Option<Arc<ResolvedTasks>>,
|
||||||
actions: Option<Arc<[CodeAction]>>,
|
actions: Option<Arc<[AvailableCodeAction]>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CodeActionContents {
|
impl CodeActionContents {
|
||||||
|
@ -1395,9 +1402,11 @@ impl CodeActionContents {
|
||||||
.map(|(kind, task)| CodeActionsItem::Task(kind.clone(), task.clone()))
|
.map(|(kind, task)| CodeActionsItem::Task(kind.clone(), task.clone()))
|
||||||
})
|
})
|
||||||
.chain(self.actions.iter().flat_map(|actions| {
|
.chain(self.actions.iter().flat_map(|actions| {
|
||||||
actions
|
actions.iter().map(|available| CodeActionsItem::CodeAction {
|
||||||
.iter()
|
excerpt_id: available.excerpt_id,
|
||||||
.map(|action| CodeActionsItem::CodeAction(action.clone()))
|
action: available.action.clone(),
|
||||||
|
provider: available.provider.clone(),
|
||||||
|
})
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
fn get(&self, index: usize) -> Option<CodeActionsItem> {
|
fn get(&self, index: usize) -> Option<CodeActionsItem> {
|
||||||
|
@ -1410,10 +1419,13 @@ impl CodeActionContents {
|
||||||
.cloned()
|
.cloned()
|
||||||
.map(|(kind, task)| CodeActionsItem::Task(kind, task))
|
.map(|(kind, task)| CodeActionsItem::Task(kind, task))
|
||||||
} else {
|
} else {
|
||||||
actions
|
actions.get(index - tasks.templates.len()).map(|available| {
|
||||||
.get(index - tasks.templates.len())
|
CodeActionsItem::CodeAction {
|
||||||
.cloned()
|
excerpt_id: available.excerpt_id,
|
||||||
.map(CodeActionsItem::CodeAction)
|
action: available.action.clone(),
|
||||||
|
provider: available.provider.clone(),
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(Some(tasks), None) => tasks
|
(Some(tasks), None) => tasks
|
||||||
|
@ -1421,7 +1433,15 @@ impl CodeActionContents {
|
||||||
.get(index)
|
.get(index)
|
||||||
.cloned()
|
.cloned()
|
||||||
.map(|(kind, task)| CodeActionsItem::Task(kind, task)),
|
.map(|(kind, task)| CodeActionsItem::Task(kind, task)),
|
||||||
(None, Some(actions)) => actions.get(index).cloned().map(CodeActionsItem::CodeAction),
|
(None, Some(actions)) => {
|
||||||
|
actions
|
||||||
|
.get(index)
|
||||||
|
.map(|available| CodeActionsItem::CodeAction {
|
||||||
|
excerpt_id: available.excerpt_id,
|
||||||
|
action: available.action.clone(),
|
||||||
|
provider: available.provider.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
(None, None) => None,
|
(None, None) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1431,7 +1451,11 @@ impl CodeActionContents {
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
enum CodeActionsItem {
|
enum CodeActionsItem {
|
||||||
Task(TaskSourceKind, ResolvedTask),
|
Task(TaskSourceKind, ResolvedTask),
|
||||||
CodeAction(CodeAction),
|
CodeAction {
|
||||||
|
excerpt_id: ExcerptId,
|
||||||
|
action: CodeAction,
|
||||||
|
provider: Arc<dyn CodeActionProvider>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CodeActionsItem {
|
impl CodeActionsItem {
|
||||||
|
@ -1442,14 +1466,14 @@ impl CodeActionsItem {
|
||||||
Some(task)
|
Some(task)
|
||||||
}
|
}
|
||||||
fn as_code_action(&self) -> Option<&CodeAction> {
|
fn as_code_action(&self) -> Option<&CodeAction> {
|
||||||
let Self::CodeAction(action) = self else {
|
let Self::CodeAction { action, .. } = self else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
Some(action)
|
Some(action)
|
||||||
}
|
}
|
||||||
fn label(&self) -> String {
|
fn label(&self) -> String {
|
||||||
match self {
|
match self {
|
||||||
Self::CodeAction(action) => action.lsp_action.title.clone(),
|
Self::CodeAction { action, .. } => action.lsp_action.title.clone(),
|
||||||
Self::Task(_, task) => task.resolved_label.clone(),
|
Self::Task(_, task) => task.resolved_label.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1588,7 +1612,9 @@ impl CodeActionsMenu {
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.max_by_key(|(_, action)| match action {
|
.max_by_key(|(_, action)| match action {
|
||||||
CodeActionsItem::Task(_, task) => task.resolved_label.chars().count(),
|
CodeActionsItem::Task(_, task) => task.resolved_label.chars().count(),
|
||||||
CodeActionsItem::CodeAction(action) => action.lsp_action.title.chars().count(),
|
CodeActionsItem::CodeAction { action, .. } => {
|
||||||
|
action.lsp_action.title.chars().count()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.map(|(ix, _)| ix),
|
.map(|(ix, _)| ix),
|
||||||
)
|
)
|
||||||
|
@ -1864,6 +1890,11 @@ impl Editor {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut code_action_providers = Vec::new();
|
||||||
|
if let Some(project) = project.clone() {
|
||||||
|
code_action_providers.push(Arc::new(project) as Arc<_>);
|
||||||
|
}
|
||||||
|
|
||||||
let mut this = Self {
|
let mut this = Self {
|
||||||
focus_handle,
|
focus_handle,
|
||||||
show_cursor_when_unfocused: false,
|
show_cursor_when_unfocused: false,
|
||||||
|
@ -1915,6 +1946,7 @@ impl Editor {
|
||||||
next_completion_id: 0,
|
next_completion_id: 0,
|
||||||
completion_documentation_pre_resolve_debounce: DebouncedDelay::new(),
|
completion_documentation_pre_resolve_debounce: DebouncedDelay::new(),
|
||||||
next_inlay_id: 0,
|
next_inlay_id: 0,
|
||||||
|
code_action_providers,
|
||||||
available_code_actions: Default::default(),
|
available_code_actions: Default::default(),
|
||||||
code_actions_task: Default::default(),
|
code_actions_task: Default::default(),
|
||||||
document_highlights_task: Default::default(),
|
document_highlights_task: Default::default(),
|
||||||
|
@ -4553,7 +4585,7 @@ impl Editor {
|
||||||
let action = action.clone();
|
let action = action.clone();
|
||||||
cx.spawn(|editor, mut cx| async move {
|
cx.spawn(|editor, mut cx| async move {
|
||||||
while let Some(prev_task) = task {
|
while let Some(prev_task) = task {
|
||||||
prev_task.await;
|
prev_task.await.log_err();
|
||||||
task = editor.update(&mut cx, |this, _| this.code_actions_task.take())?;
|
task = editor.update(&mut cx, |this, _| this.code_actions_task.take())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4727,17 +4759,16 @@ impl Editor {
|
||||||
Some(Task::ready(Ok(())))
|
Some(Task::ready(Ok(())))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
CodeActionsItem::CodeAction(action) => {
|
CodeActionsItem::CodeAction {
|
||||||
let apply_code_actions = workspace
|
excerpt_id,
|
||||||
.read(cx)
|
action,
|
||||||
.project()
|
provider,
|
||||||
.clone()
|
} => {
|
||||||
.update(cx, |project, cx| {
|
let apply_code_action =
|
||||||
project.apply_code_action(buffer, action, true, cx)
|
provider.apply_code_action(buffer, action, excerpt_id, true, cx);
|
||||||
});
|
|
||||||
let workspace = workspace.downgrade();
|
let workspace = workspace.downgrade();
|
||||||
Some(cx.spawn(|editor, cx| async move {
|
Some(cx.spawn(|editor, cx| async move {
|
||||||
let project_transaction = apply_code_actions.await?;
|
let project_transaction = apply_code_action.await?;
|
||||||
Self::open_project_transaction(
|
Self::open_project_transaction(
|
||||||
&editor,
|
&editor,
|
||||||
workspace,
|
workspace,
|
||||||
|
@ -4835,8 +4866,16 @@ impl Editor {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn push_code_action_provider(
|
||||||
|
&mut self,
|
||||||
|
provider: Arc<dyn CodeActionProvider>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
self.code_action_providers.push(provider);
|
||||||
|
self.refresh_code_actions(cx);
|
||||||
|
}
|
||||||
|
|
||||||
fn refresh_code_actions(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
|
fn refresh_code_actions(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
|
||||||
let project = self.project.clone()?;
|
|
||||||
let buffer = self.buffer.read(cx);
|
let buffer = self.buffer.read(cx);
|
||||||
let newest_selection = self.selections.newest_anchor().clone();
|
let newest_selection = self.selections.newest_anchor().clone();
|
||||||
let (start_buffer, start) = buffer.text_anchor_for_position(newest_selection.start, cx)?;
|
let (start_buffer, start) = buffer.text_anchor_for_position(newest_selection.start, cx)?;
|
||||||
|
@ -4850,13 +4889,30 @@ impl Editor {
|
||||||
.timer(CODE_ACTIONS_DEBOUNCE_TIMEOUT)
|
.timer(CODE_ACTIONS_DEBOUNCE_TIMEOUT)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let actions = if let Ok(code_actions) = project.update(&mut cx, |project, cx| {
|
let (providers, tasks) = this.update(&mut cx, |this, cx| {
|
||||||
project.code_actions(&start_buffer, start..end, cx)
|
let providers = this.code_action_providers.clone();
|
||||||
}) {
|
let tasks = this
|
||||||
code_actions.await
|
.code_action_providers
|
||||||
} else {
|
.iter()
|
||||||
Vec::new()
|
.map(|provider| provider.code_actions(&start_buffer, start..end, cx))
|
||||||
};
|
.collect::<Vec<_>>();
|
||||||
|
(providers, tasks)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut actions = Vec::new();
|
||||||
|
for (provider, provider_actions) in
|
||||||
|
providers.into_iter().zip(future::join_all(tasks).await)
|
||||||
|
{
|
||||||
|
if let Some(provider_actions) = provider_actions.log_err() {
|
||||||
|
actions.extend(provider_actions.into_iter().map(|action| {
|
||||||
|
AvailableCodeAction {
|
||||||
|
excerpt_id: newest_selection.start.excerpt_id,
|
||||||
|
action,
|
||||||
|
provider: provider.clone(),
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.available_code_actions = if actions.is_empty() {
|
this.available_code_actions = if actions.is_empty() {
|
||||||
|
@ -4872,7 +4928,6 @@ impl Editor {
|
||||||
};
|
};
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})
|
})
|
||||||
.log_err();
|
|
||||||
}));
|
}));
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -9685,7 +9740,7 @@ impl Editor {
|
||||||
})
|
})
|
||||||
.context("location tasks preparation")?;
|
.context("location tasks preparation")?;
|
||||||
|
|
||||||
let locations = futures::future::join_all(location_tasks)
|
let locations = future::join_all(location_tasks)
|
||||||
.await
|
.await
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|location| location.transpose())
|
.filter_map(|location| location.transpose())
|
||||||
|
@ -12574,6 +12629,48 @@ pub trait CompletionProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait CodeActionProvider {
|
||||||
|
fn code_actions(
|
||||||
|
&self,
|
||||||
|
buffer: &Model<Buffer>,
|
||||||
|
range: Range<text::Anchor>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Task<Result<Vec<CodeAction>>>;
|
||||||
|
|
||||||
|
fn apply_code_action(
|
||||||
|
&self,
|
||||||
|
buffer_handle: Model<Buffer>,
|
||||||
|
action: CodeAction,
|
||||||
|
excerpt_id: ExcerptId,
|
||||||
|
push_to_history: bool,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Task<Result<ProjectTransaction>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CodeActionProvider for Model<Project> {
|
||||||
|
fn code_actions(
|
||||||
|
&self,
|
||||||
|
buffer: &Model<Buffer>,
|
||||||
|
range: Range<text::Anchor>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Task<Result<Vec<CodeAction>>> {
|
||||||
|
self.update(cx, |project, cx| project.code_actions(buffer, range, cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_code_action(
|
||||||
|
&self,
|
||||||
|
buffer_handle: Model<Buffer>,
|
||||||
|
action: CodeAction,
|
||||||
|
_excerpt_id: ExcerptId,
|
||||||
|
push_to_history: bool,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Task<Result<ProjectTransaction>> {
|
||||||
|
self.update(cx, |project, cx| {
|
||||||
|
project.apply_code_action(buffer_handle, action, push_to_history, cx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn snippet_completions(
|
fn snippet_completions(
|
||||||
project: &Project,
|
project: &Project,
|
||||||
buffer: &Model<Buffer>,
|
buffer: &Model<Buffer>,
|
||||||
|
|
|
@ -407,7 +407,11 @@ impl BackgroundExecutor {
|
||||||
|
|
||||||
/// How many CPUs are available to the dispatcher.
|
/// How many CPUs are available to the dispatcher.
|
||||||
pub fn num_cpus(&self) -> usize {
|
pub fn num_cpus(&self) -> usize {
|
||||||
num_cpus::get()
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
return 4;
|
||||||
|
|
||||||
|
#[cfg(not(any(test, feature = "test-support")))]
|
||||||
|
return num_cpus::get();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether we're on the main thread.
|
/// Whether we're on the main thread.
|
||||||
|
|
|
@ -1810,6 +1810,69 @@ impl MultiBuffer {
|
||||||
self.as_singleton().unwrap().read(cx).is_parsing()
|
self.as_singleton().unwrap().read(cx).is_parsing()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn resize_excerpt(
|
||||||
|
&mut self,
|
||||||
|
id: ExcerptId,
|
||||||
|
range: Range<text::Anchor>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) {
|
||||||
|
self.sync(cx);
|
||||||
|
|
||||||
|
let snapshot = self.snapshot(cx);
|
||||||
|
let locator = snapshot.excerpt_locator_for_id(id);
|
||||||
|
let mut new_excerpts = SumTree::default();
|
||||||
|
let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, usize)>(&());
|
||||||
|
let mut edits = Vec::<Edit<usize>>::new();
|
||||||
|
|
||||||
|
let prefix = cursor.slice(&Some(locator), Bias::Left, &());
|
||||||
|
new_excerpts.append(prefix, &());
|
||||||
|
|
||||||
|
let mut excerpt = cursor.item().unwrap().clone();
|
||||||
|
let old_text_len = excerpt.text_summary.len;
|
||||||
|
|
||||||
|
excerpt.range.context.start = range.start;
|
||||||
|
excerpt.range.context.end = range.end;
|
||||||
|
excerpt.max_buffer_row = range.end.to_point(&excerpt.buffer).row;
|
||||||
|
|
||||||
|
excerpt.text_summary = excerpt
|
||||||
|
.buffer
|
||||||
|
.text_summary_for_range(excerpt.range.context.clone());
|
||||||
|
|
||||||
|
let new_start_offset = new_excerpts.summary().text.len;
|
||||||
|
let old_start_offset = cursor.start().1;
|
||||||
|
let edit = Edit {
|
||||||
|
old: old_start_offset..old_start_offset + old_text_len,
|
||||||
|
new: new_start_offset..new_start_offset + excerpt.text_summary.len,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(last_edit) = edits.last_mut() {
|
||||||
|
if last_edit.old.end == edit.old.start {
|
||||||
|
last_edit.old.end = edit.old.end;
|
||||||
|
last_edit.new.end = edit.new.end;
|
||||||
|
} else {
|
||||||
|
edits.push(edit);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
edits.push(edit);
|
||||||
|
}
|
||||||
|
|
||||||
|
new_excerpts.push(excerpt, &());
|
||||||
|
|
||||||
|
cursor.next(&());
|
||||||
|
|
||||||
|
new_excerpts.append(cursor.suffix(&()), &());
|
||||||
|
|
||||||
|
drop(cursor);
|
||||||
|
self.snapshot.borrow_mut().excerpts = new_excerpts;
|
||||||
|
|
||||||
|
self.subscriptions.publish_mut(edits);
|
||||||
|
cx.emit(Event::Edited {
|
||||||
|
singleton_buffer_edited: false,
|
||||||
|
});
|
||||||
|
cx.emit(Event::ExcerptsExpanded { ids: vec![id] });
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn expand_excerpts(
|
pub fn expand_excerpts(
|
||||||
&mut self,
|
&mut self,
|
||||||
ids: impl IntoIterator<Item = ExcerptId>,
|
ids: impl IntoIterator<Item = ExcerptId>,
|
||||||
|
@ -3139,6 +3202,10 @@ impl MultiBufferSnapshot {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn context_range_for_excerpt(&self, excerpt_id: ExcerptId) -> Option<Range<text::Anchor>> {
|
||||||
|
Some(self.excerpt(excerpt_id)?.range.context.clone())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn can_resolve(&self, anchor: &Anchor) -> bool {
|
pub fn can_resolve(&self, anchor: &Anchor) -> bool {
|
||||||
if anchor.excerpt_id == ExcerptId::min() || anchor.excerpt_id == ExcerptId::max() {
|
if anchor.excerpt_id == ExcerptId::min() || anchor.excerpt_id == ExcerptId::max() {
|
||||||
true
|
true
|
||||||
|
|
|
@ -1431,7 +1431,7 @@ impl LspStore {
|
||||||
buffer_handle: &Model<Buffer>,
|
buffer_handle: &Model<Buffer>,
|
||||||
range: Range<Anchor>,
|
range: Range<Anchor>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Vec<CodeAction>> {
|
) -> Task<Result<Vec<CodeAction>>> {
|
||||||
if let Some((upstream_client, project_id)) = self.upstream_client() {
|
if let Some((upstream_client, project_id)) = self.upstream_client() {
|
||||||
let request_task = upstream_client.request(proto::MultiLspQuery {
|
let request_task = upstream_client.request(proto::MultiLspQuery {
|
||||||
buffer_id: buffer_handle.read(cx).remote_id().into(),
|
buffer_id: buffer_handle.read(cx).remote_id().into(),
|
||||||
|
@ -1451,14 +1451,11 @@ impl LspStore {
|
||||||
let buffer = buffer_handle.clone();
|
let buffer = buffer_handle.clone();
|
||||||
cx.spawn(|weak_project, cx| async move {
|
cx.spawn(|weak_project, cx| async move {
|
||||||
let Some(project) = weak_project.upgrade() else {
|
let Some(project) = weak_project.upgrade() else {
|
||||||
return Vec::new();
|
return Ok(Vec::new());
|
||||||
};
|
};
|
||||||
join_all(
|
let responses = request_task.await?.responses;
|
||||||
request_task
|
let actions = join_all(
|
||||||
.await
|
responses
|
||||||
.log_err()
|
|
||||||
.map(|response| response.responses)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|lsp_response| match lsp_response.response? {
|
.filter_map(|lsp_response| match lsp_response.response? {
|
||||||
proto::lsp_response::Response::GetCodeActionsResponse(response) => {
|
proto::lsp_response::Response::GetCodeActionsResponse(response) => {
|
||||||
|
@ -1470,7 +1467,7 @@ impl LspStore {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.map(|code_actions_response| {
|
.map(|code_actions_response| {
|
||||||
let response = GetCodeActions {
|
GetCodeActions {
|
||||||
range: range.clone(),
|
range: range.clone(),
|
||||||
kinds: None,
|
kinds: None,
|
||||||
}
|
}
|
||||||
|
@ -1479,14 +1476,17 @@ impl LspStore {
|
||||||
project.clone(),
|
project.clone(),
|
||||||
buffer.clone(),
|
buffer.clone(),
|
||||||
cx.clone(),
|
cx.clone(),
|
||||||
);
|
)
|
||||||
async move { response.await.log_err().unwrap_or_default() }
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.await
|
.await;
|
||||||
.into_iter()
|
|
||||||
.flatten()
|
Ok(actions
|
||||||
.collect()
|
.into_iter()
|
||||||
|
.collect::<Result<Vec<Vec<_>>>>()?
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.collect())
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
let all_actions_task = self.request_multiple_lsp_locally(
|
let all_actions_task = self.request_multiple_lsp_locally(
|
||||||
|
@ -1498,7 +1498,9 @@ impl LspStore {
|
||||||
},
|
},
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
cx.spawn(|_, _| async move { all_actions_task.await.into_iter().flatten().collect() })
|
cx.spawn(
|
||||||
|
|_, _| async move { Ok(all_actions_task.await.into_iter().flatten().collect()) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3247,7 +3247,7 @@ impl Project {
|
||||||
buffer_handle: &Model<Buffer>,
|
buffer_handle: &Model<Buffer>,
|
||||||
range: Range<T>,
|
range: Range<T>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Vec<CodeAction>> {
|
) -> Task<Result<Vec<CodeAction>>> {
|
||||||
let buffer = buffer_handle.read(cx);
|
let buffer = buffer_handle.read(cx);
|
||||||
let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end);
|
let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end);
|
||||||
self.lsp_store.update(cx, |lsp_store, cx| {
|
self.lsp_store.update(cx, |lsp_store, cx| {
|
||||||
|
|
|
@ -2708,7 +2708,7 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
|
||||||
.next()
|
.next()
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let action = actions.await[0].clone();
|
let action = actions.await.unwrap()[0].clone();
|
||||||
let apply = project.update(cx, |project, cx| {
|
let apply = project.update(cx, |project, cx| {
|
||||||
project.apply_code_action(buffer.clone(), action, true, cx)
|
project.apply_code_action(buffer.clone(), action, true, cx)
|
||||||
});
|
});
|
||||||
|
@ -5046,6 +5046,7 @@ async fn test_multiple_language_server_actions(cx: &mut gpui::TestAppContext) {
|
||||||
vec!["TailwindServer code action", "TypeScriptServer code action"],
|
vec!["TailwindServer code action", "TypeScriptServer code action"],
|
||||||
code_actions_task
|
code_actions_task
|
||||||
.await
|
.await
|
||||||
|
.unwrap()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|code_action| code_action.lsp_action.title)
|
.map(|code_action| code_action.lsp_action.title)
|
||||||
.sorted()
|
.sorted()
|
||||||
|
|
|
@ -2745,7 +2745,7 @@ pub mod tests {
|
||||||
search_view
|
search_view
|
||||||
.results_editor
|
.results_editor
|
||||||
.update(cx, |editor, cx| editor.display_text(cx)),
|
.update(cx, |editor, cx| editor.display_text(cx)),
|
||||||
"\n\n\nconst ONE: usize = 1;\n\n\n\n\nconst TWO: usize = one::ONE + one::ONE;\n",
|
"\n\n\nconst TWO: usize = one::ONE + one::ONE;\n\n\n\n\nconst ONE: usize = 1;\n",
|
||||||
"New search in directory should have a filter that matches a certain directory"
|
"New search in directory should have a filter that matches a certain directory"
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|
|
@ -675,7 +675,9 @@ impl DelayedDebouncedEditAction {
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
PaneAdded(View<Pane>),
|
PaneAdded(View<Pane>),
|
||||||
PaneRemoved,
|
PaneRemoved,
|
||||||
ItemAdded,
|
ItemAdded {
|
||||||
|
item: Box<dyn ItemHandle>,
|
||||||
|
},
|
||||||
ItemRemoved,
|
ItemRemoved,
|
||||||
ActiveItemChanged,
|
ActiveItemChanged,
|
||||||
UserSavedItem {
|
UserSavedItem {
|
||||||
|
@ -2984,7 +2986,9 @@ impl Workspace {
|
||||||
match event {
|
match event {
|
||||||
pane::Event::AddItem { item } => {
|
pane::Event::AddItem { item } => {
|
||||||
item.added_to_pane(self, pane, cx);
|
item.added_to_pane(self, pane, cx);
|
||||||
cx.emit(Event::ItemAdded);
|
cx.emit(Event::ItemAdded {
|
||||||
|
item: item.boxed_clone(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
pane::Event::Split(direction) => {
|
pane::Event::Split(direction) => {
|
||||||
self.split_and_clone(pane, *direction, cx);
|
self.split_and_clone(pane, *direction, cx);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue