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:
Antonio Scandurra 2024-09-23 11:40:34 -06:00 committed by GitHub
parent 1efe87029b
commit 7051bc00c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 418 additions and 72 deletions

1
Cargo.lock generated
View file

@ -404,6 +404,7 @@ dependencies = [
"language_model", "language_model",
"languages", "languages",
"log", "log",
"lsp",
"markdown", "markdown",
"menu", "menu",
"multi_buffer", "multi_buffer",

View file

@ -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

View file

@ -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])
} }

View file

@ -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,

View file

@ -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);

View file

@ -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>,

View file

@ -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.

View file

@ -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

View file

@ -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()) },
)
} }
} }

View file

@ -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| {

View file

@ -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()

View file

@ -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"
); );
}) })

View file

@ -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);